Uh oh!
There was an error while loading. Please reload this page.
OpenClaw backfill: CLI-backend denylist replaces the provider allowlist, rows carry per-turn provider (LLP 0193/0194) - #643
Conversation
…st, rows carry per-turn provider (LLP 0193/0194)
Lane B's projection gate flips from the {anthropic, openai} vendor
allowlist to a two-rung denylist: a record is excluded when its effective
backend stamps api "cli" (the mechanism marker verified live on OpenClaw
2026.7.1-2) or its provider matches a sibling-adapter prefix; a record
whose backend is unresolvable still fails closed as unknown. Everything
else projects, so ollama and future direct-API providers land at
transcript fidelity from birth (the LLP 0167 coverage statement, now true
as written). The (provider, api) pair smears as a unit so a neighbor's
provider is never stitched to a different neighbor's api.
Mixed-provider sessions also stop reading as their first projected turn's
vendor: AiGatewayProjectedMessage gains an optional per-message provider,
the row builder prefers it over the exchange value (the LLP 0026 model
precedence extended to the column it skipped), and the OpenClaw backfill
stamps every row from its turn's smeared backend, prompts included.
Verified end-to-end on a live install: a native-Ollama turn recorded by
the scheduled sweep within the interval, rows stamped provider=ollama,
model=gemma4:12b; all-CLI sessions still project nothing.
Lane A (live gateway) ollama capture remains open in #640.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>philcunliffe
commented
Aug 5, 2026
Neutral review round 1 of 2 (adopted PR, |
| run | result |
|---|---|
usage-policy-fold at clean master 7116f95 | 23 pass / 0 fail |
usage-policy-fold at 9ebba5e | 23 pass / 0 fail |
full npm test at 9ebba5e | 3578 pass / 0 fail / 1 skip, exit 0 |
The suite is completely green. That file exercises case-insensitive volume folding, so the failure you saw is almost certainly macOS-specific. Your verification section undersells the PR. tsc --noEmit exit 0 and all three smokes exit 0, both confirmed.
Verified sound
- "Projectors that never set the field are byte-for-byte unchanged" is TRUE, and I went looking hard for the falsy-override hole.
message_projector.js:712usesstringValue(ctx.message.provider) ?? ctx.projection.provider, andstringValuecollapsesundefined,null,''and non-strings alike toundefined. Exhaustive grep finds exactly one site in the non-test codebase that assignsprovideronto a projected message, and it is yours. All five construction paths build messages field-by-field rather than spreading wire bodies, so no vendor payload can leak aproviderkey in. - The kernel type change is purely additive, not breaking: an optional property on an interface plugins construct.
- Unresolvable-fails-closed holds.
partitionByBackend:414requiresbackend?.provider !== undefined; no path lets an unresolvable backend through. - The
(provider, api)pair really does smear as a unit. I could not construct a case that stitches one neighbour's provider to another's api. That claim is true. - The LLP 0161 edit is legitimate under the immutability rule: a pure
Extended-by:forward-ref, no body text touched. @refhygiene is clean. All 9 refs added or moved resolve, and every gloss is accurate about both the code and the cited section.llp-ref-hygienepasses 10/10. Zero stalePROJECTABLE_PROVIDERSreferences in code, tests, docs or smokes. No em dashes anywhere in the diff.- Partitioning, dedupe and settlement are provably unaffected:
partition.js:234readsprovideronly as the third fallback behindclient_nameandconversation_source, both of which OpenClaw sets.
HIGH - a sibling-owned record projects when its turn's anchor record is missing
backfill.js:452-470. Double-capture is reachable today, with the current verified stamping. This is a regression, not a pre-existing hazard.
The smear preference is "nearest following anchor, else nearest preceding". A record is an anchor iff it states provider or api. There is no notion of a turn boundary, so a CLI turn whose assistant record is absent from the list has no anchor of its own and inherits the next turn's backend.
Demonstrated. Session in append order:
u1 role:user <- prompt routed to the claude-cli backend
e1 type:"error" <- CLI child exited nonzero; not type "message",
so parseOpenclawSessionMessage drops it entirely
u2 role:user
a2 assistant provider:"ollama" api:"ollama"
Result: projected: [[u1,'ollama'], [u2,'ollama'], [a2,'ollama']], excluded events: [].
u1 is the prompt of a claude-cli turn. Per LLP 0147 the claude child already wrote it to ~/.claude/projects/<slug>/<uuid>.jsonl, so @hypaware/claude captures it under client_name='claude'. It now also lands under client_name='openclaw', and per LLP 0193's own Context the two adapters mint disjoint identities, so part_id dedupe cannot collapse them. No excluded_backend event fires, so the duplicate is invisible in telemetry, which is exactly the "visible, retroactively recoverable" property the allowlist was chosen for.
Under the allowlist the smeared value ollama was not projectable, so u1 was excluded. The flip widens the misattribution target from {anthropic, openai} to every non-CLI provider, which is unbounded.
The mirror case is silent data loss: an errored ollama turn's prompt smears onto a following claude-cli turn, and three records are dropped as covered_by: claude_transcript when the Claude transcript does not contain them.
Suggested fix: make the smear turn-scoped. At minimum, do not let an unanchored record cross a role:'user' boundary in either direction; a record that ends a turn with no following anchor should resolve to unknown (excluded) rather than borrowing the next turn's pair.
This also makes one claim false today.backfill.js:381 and LLP 0193 state that the turns the sibling adapters own "stay out and never double-count". Whichever way you fix the code, that sentence needs to narrow to what it delivers.
MEDIUM - the import window truncates records before backends are resolved
backfill.js:311-312. Two reviewers hit this independently from different directions, which is why it is listed separately from the HIGH even though it is the same family.
constwindowed=filterByWindow(records,window)const{ projectable, excludedByProvider }=partitionByBackend(windowed)filterByWindow is per record, so it can delete the only anchor of a turn while keeping the rest. With --until falling between a prompt and its assistant, the prompt backward-smears to the previous turn's backend and projects, duplicating a row the Claude transcript owns, again with zero exclusion events. filterByWindow also keeps timestamp-less records unconditionally, producing the same orphaning with no window boundary crossed.
LLP 0194's own Consequences note that backfill dedupe skips existing part_ids, so a later full-window sweep will not correct it. The misattribution is permanent short of purge-and-reimport.
@hypaware/hermes already solved this shape by making --since select which sessions to import rather than which of a session's messages (hermes/src/backfill.js:41-46, "identity is always computed from a session's full row set, never a partial window").
Suggested fix: resolve effectiveBackends over the full records array, then window the partition result. Two-line reorder, removes the class entirely.
MEDIUM - prefix matching has no delimiter boundary
backfill.js:483. lowered.startsWith('codex') matches codexcloud, codex-proxy, codex-router. OpenClaw providers are user-authored ids in openclaw.json, so a direct-API provider whose id happens to start with codex is silently excluded and labelled covered_by: 'codex_sessions_rollout', a false claim that another route holds the data.
Fix:lowered === prefix || lowered.startsWith(prefix + '-') || lowered.startsWith(prefix + '/'), matching the claude-cli/* model-ref form LLP 0147 documents.
MEDIUM - LLP 0172 and 0173 are now false and got no forward-ref
llp/0172:710-714 (Status: Active, and it has no Extended-by: line at all) asserts about the current tree: "backfill.js's PROJECTABLE_PROVIDERS = new Set(['anthropic', 'openai']) and effectiveProviders()'s forward/backward fill are untouched". Both are deleted here. llp/0173:222,399 repeat it.
You applied the forward-ref discipline correctly to 0161; this is the same mechanical edit, allowed on Active docs, and it is the only stale-doc consequence of the deletion.
MEDIUM - LLP 0193 undersells the codex half of the residual
llp/0193:114-120 calls the fail-open residual "speculative" and says "the two-rung key requires both conventions to change before it bites". But lines 61-64 of the same doc admit codex stamping is unverified, and 51-58 establish that OpenClaw stamps the wire shape rather than cli in a sibling file kind. A codex-backed turn stamped provider:"openai" + api:"openai-responses" clears both rungs and double-counts against @hypaware/codextoday, with no convention change.
(It double-counted under the allowlist too, so not a regression, but "speculative" is the wrong word for a path the doc itself flags as unverified.) Point the bullet at {#verify}.
MEDIUM - two coverage holes, both proven by mutation
Mutation testing against the real provider: 4 of 5 core mutations caught (removing either rung, making unresolvable project, ignoring per-message provider). Two survivors:
- The
(provider, api)pairing invariant is completely untested. Replacing the paired smear with two independent per-field smears leaves the entire suite green. Not a no-op mutation: on a session of[user, assistant(provider:'ollama', no api), user, assistant(provider:'claude-cli', api:'cli')]the head projects two ollama rows, while the mutant projects zero and mislabels the exclusion as anollamaexclusion. That is the silent-drop class the JSDoc calls load-bearing. - The
codexentry inSIBLING_ADAPTER_COVERAGEhas zero tests.grep -n codex test/plugins/openclaw-backfill.test.jsreturns nothing; deleting the entry leaves the suite green. Under the allowlist it only affected an event label; under the denylist it is load-bearing for exclusion, so its coverage should have gone up in this PR.
LOW
backfill.js:439- whitespace flips the gate open.api: "cli "projects, no exclusion event. Case is folded, whitespace is not. Same for" claude-cli". Add.trim().backfill.js:455- a record statingapibut no usableproviderbecomes a blocking anchor. It is correctly excluded asunknown, but it also shadows the real anchor behind it for every preceding unanchored record, so a session with one blank-provider assistant loses that record and the preceding prompt where master projected both. Fail-closed direction, but silent data loss introduced here. Anchor only on records that stateprovider, carrying that record's ownapialong; the pair stays atomic either way.backfill.js:439- rung 1 excludes CLI backends nothing covers. A future OpenClaw CLI backend with no HypAware adapter is dropped with nocovered_byand nothing else capturing it, which is the LLP 0167 coverage-gap failure this PR exists to fix, re-created on the other axis. The direction is defensible; make it loud. Emit the coverage-less exclusion atwarn, notinfo.backfill.js:223- stale comment. SayseffectiveProviders/partitionByBackend; this PR renames it toeffectiveBackends.- Prefix and case normalisation untested.
startsWithto===survives, and dropping either.toLowerCase()survives. One prefixed-but-not-equal provider and oneapi:'CLI'assertion close all three. openclaw-backfill.test.js:647- the comment states a precondition the code does not enforce: "provided its records carry a wire-shape api".apiis not required;{provider:'some-future-vendor'}with noapiat all projects. That is the most dangerous fail-open cell of the new denylist, and it is both untested and mis-documented. Correct the comment and pin the no-apicase deliberately.llp/0193:121-124overstates test stability. "the suite's existing R10 assertions hold without edits" - the fixtures are indeed unedited, but the R10 block header was reworded and one test rewritten. Narrow to "the claude-cli fixtures' assertions hold".- The
{#verify}codex probe has no durable home.docs/ACCEPTANCE.md'sopenclaw_captureis untouched, so the pre-acceptance obligation lives only in an LLP verify list nobody runs. - LLP 0194 should record the semantic reservation of the
providername for out-of-tree projectors. A third-party adapter that spreads an OpenRouter-style body (which carries a top-levelprovider) previously had it ignored and now has it silently win on every row. No in-repo projector does this, so it is a doc and release-note item, not a code one. - The "154/154
@refannotations validate" figure is not reproducible from any obvious scope (repo-wide is 1944; the changed files total 72). Nothing is broken by it, but it should not be quoted as repo-wide validation.
What I did not do, and why
Neutral can push to this branch and healing an adopted PR is normally its job. I have deliberately not pushed fixes here, because the HIGH and the two MEDIUMs above (backfill.js:311, :452, :455) are all one design question, how the smear should be scoped to a turn, and the answer is yours to make. It also needs LLP 0193's "never double-count" claim amended to match whatever you choose, and that is your design record, not neutral's to rewrite.
If you would rather neutral took the mechanical subset off your plate, say so on this thread and it will push them: the delimiter boundary, the .trim(), the stale comment, the LLP 0172/0173 forward-refs, and the four missing tests. The turn-scoping fix and the LLP 0193 wording should stay with you either way.
philcunliffe
left a comment
There was a problem hiding this comment.
Changes requested at 9ebba5e. Full review record with evidence is in the review-record comment on this thread.
Blocking: one HIGH.backfill.js:452-470 - a sibling-owned record projects when its turn's anchor record is missing from the list, because the smear has no notion of a turn boundary and borrows the next turn's (provider, api) pair. Demonstrated: a claude-cli prompt whose assistant record was dropped (a type:"error" CLI abort) projects under ollama, duplicating a row @hypaware/claude already owns under a disjoint identity that part_id dedupe cannot collapse, and no excluded_backend event fires, so the duplicate is invisible. Under the allowlist that smeared value was not projectable; the flip widens the target from {anthropic, openai} to every non-CLI provider.
The mirror case is silent data loss, and the same root cause drives the MEDIUM at backfill.js:311 (the window truncates records before backends resolve, which two reviewers found independently) and the LOW at :455.
Also needing your call: backfill.js:381 and LLP 0193 currently state that sibling-owned turns "never double-count", which is not true today whichever way the code is fixed.
Not blocking, but worth doing in the same pass: prefix matching has no delimiter boundary (codex matches codexcloud and mislabels it covered_by), llp/0172/0173 are now false and want the same mechanical Extended-by: you correctly gave 0161, LLP 0193 undersells the codex residual, and mutation testing found two live coverage holes (the (provider, api) pairing invariant and the whole codex denylist entry both survive deletion with the suite green).
In your favour: the usage-policy-fold failure you flagged as pre-existing does not reproduce on either tree. The full suite is 3578 pass / 0 fail at your head, and on clean master too. Your verification section undersells the PR.
The byte-for-byte-unchanged claim for live capture verifies rigorously, the kernel type change is purely additive, unresolvable-fails-closed holds, the (provider, api) pair genuinely does smear as a unit, the LLP 0161 forward-ref is legitimate, and all 9 new @refs resolve with accurate glosses. The design is sound; it is the smear's turn-scoping that needs the fix.
PR #643 (branch openclaw-cli-backend-denylist) is open and older, and it already claims both 0193 and 0194. Master's highest is 0192, so scanning only master made 0193 look free. The minting rule takes the next number past the highest across the default branch and every branch, so this PR yields and moves to 0195, the first number free everywhere. Mechanical: the file, its title line, and the four @refs that point at it. No prose or decision content changes. Co-Authored-By: Claude <noreply@anthropic.com>
…r not at all (review fixes for LLP 0193) The borrow in effectiveBackends ran file-wide, so a turn whose anchor record was missing (CLI abort, crash mid-write, turn still in flight) inherited the NEXT turn's (provider, api) pair: a sibling-owned prompt projected as a duplicate row part_id dedupe cannot collapse, with no excluded_backend event, and the mirror case silently dropped direct-API records. Resolution is now fenced at turn boundaries (every user record opens a turn; forward preferred, backward within the turn as fallback); a turn that states nothing resolves to unknown, excluded visibly, and since exclusion writes nothing an in-flight turn imports intact one sweep later. Also from the review: resolution happens BEFORE the date window so a --since/--until cut cannot sever a record from its turn's anchor (partitionByBackend now takes the window and filterByWindow is unused here); the sibling prefix match is delimiter-bounded (codex matches codex/codex-*, not codexcloud); the "never double-count" overclaim is rewritten in code and LLP 0193 to what is actually guaranteed; LLP 0193 records the codex-stamping residual honestly plus the queued-prompt fail-closed residual; LLP 0172/0173 carry Extended-by forward-refs. Six new tests pin the behavior, including killers for the two mutants that survived the first cut (the codex denylist entry and the pair-as-a-unit smearing invariant); the codex-entry mutant was verified to fail the suite before restore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bgmcmullen
commented
Aug 6, 2026
Review fixes pushed in 1a22e26, all four blocking/needs-a-call items addressed:
Also done: On |
…exclusions, probe home (LLP 0193/0194) - isCliBackend and siblingCoverageFor trim before folding, so "cli " and " claude-cli" no longer slip a gate their trimmed forms close. - Only provider-stating records anchor the borrow: a record stating api alone keeps its own pair (excluded as unknown) but neither feeds the borrow nor shadows the turn's real anchor behind it, so a blank-provider assistant no longer drops the preceding prompt with it. - A CLI-denied exclusion with no covered_by logs at warn: dropped here and recorded nowhere else is the LLP 0167 coverage-gap failure on the other axis, and it should be loud. unknown stays info (routinely a turn in flight, re-evaluated every sweep). - Stale effectiveProviders comment updated. - Tests: codex-mini pins prefix (not equality) semantics; " CLI" and " claude-cli" pin both normalizations; the api-only shadow case is pinned; the no-api provider-only cell, the denylist's widest fail-open cell, is pinned deliberately and its test comment corrected (the gate never required an api). - LLP 0193 narrows the fixtures-stability claim and names the probe's durable home; docs/ACCEPTANCE.md openclaw_capture gains step 8, the codex-backend stamping probe, with the fail-open interpretation of a wire-shape result spelled out. LLP 0194 records that the provider message-field name is now semantically reserved for out-of-tree projectors that spread wire bodies (release-note obligation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bgmcmullen
commented
Aug 6, 2026
LOW pass pushed in cbf96a2; every remaining item from the review record is addressed:
Full suite at |
philcunliffe
commented
Aug 6, 2026
Neutral review round 2 of 2 (adopted PR, |
| # | Finding | Status | Evidence |
|---|---|---|---|
| HIGH | Sibling-owned record projects when its turn's anchor is missing | fixed | backfill.js:502-517 segments at every role:'user'; fillSegment (:535-548) is fenced to [start,end). Round 1's exact demonstrated session now yields projected:[u2,a2], excluded:[['unknown',1]], pinned at openclaw-backfill.test.js:783-802; mirror case :809-834; trailing in-flight prompt :838-857 |
| MED | Import window truncates before backends resolve | fixed | filterByWindow import dropped; partitionByBackend(records, window) (:313, :432) resolves over the full list and applies the window inline at :442-446, byte-equivalent to scan_util.js:65-73 including keep-if-unstamped. Pinned :857-877 |
| MED | Prefix matching has no delimiter boundary | fixed | siblingCoverageFor:560-567 requires rest === '' || /^[^a-z0-9]/ - broader than the suggested -// fix, correctly so. Pinned :900-925 (codex, codex-mini denied; codexcloud projects and is not mislabeled) |
| MED | LLP 0172/0173 made false, no forward-ref | fixed | Both gain **Extended-by:** LLP 0193, matching corpus convention |
| MED | LLP 0193 undersells the codex residual | fixed | llp/0193:128-136 drops "speculative", states the codex stamping is UNVERIFIED, points at {#verify}, names the consequence |
| MED | Coverage hole: (provider, api) pair invariant | fixed | Test :878-897. Mutation-verified: an independent-per-field smear now fails 2 tests (round 1's version survived) |
| MED | Coverage hole: codex denylist entry | fixed | Deleting the entry now fails 1 test |
| LOW | x8 (whitespace gate, anchor shadowing, loud coverage-less exclusion, stale comment, normalization tests, test-comment precondition, LLP test-stability claim, {#verify} home, LLP 0194 field reservation) | all fixed | .trim() at :476/:561; fillSegment anchors only on records[i].provider !== undefined (:944-966); :322 warn/info split; :219-222; :900-943; :661-672; docs/ACCEPTANCE.mdopenclaw_capture step 8 |
Independent correctness checks
part_idis<message_id>#<part_index>(:597-599,:664), not positional, so the
new transientunknownexclusion cannot shift identity. The "in-flight turn imports
intact one sweep later" claim holds and re-import will not duplicate.- The turn fence does not chop agentic loops. OpenClaw spells tool output
role:"toolResult", not"user"(match_key.js:26,233,settle.js:565,backfill.js:702). Had it been"user", every tool result would have opened a turn.
The tests use the correct spelling. This was the main way the redesign could have gone
wrong, and it does not. provider/api/roleparse throughnonBlankString, so the bare!== undefined
anchor test cannot be tripped by''or a non-string.
Mutation testing (24 mutants against the 39-test suite)
Killed: turn-fence removal (3 fails), merging consecutive user records, anchor-on-any-stated
in the backward pass, both .trim()s, api case fold, delimiter boundary, startsWith→===,codex entry, claude-cli entry (6), window-before-resolution, either denylist rung,
unresolvable-projects (3), per-message provider stamping, forward-borrow removal (19),
independent-field smear (2).
Two survivors, both non-blocking preferences, neither a defect:
backfill.js:545- anchoring the forward pass oneffective[i] !== undefinedrather
thanrecords[i].provider !== undefinedsurvives. Not a no-op: on[user, assistant(ollama), assistant(api-only), toolResult]the head resolves the
trailing record toollama, the mutant tounknown. The backward pass has the
symmetric guard and is covered; only the forward direction is unpinned. Fail-closed
either way and the shape is exotic. One appended record on the:944fixture closes it.backfill.js:322- the warn/info split is unpinned; the tests assert events, not levels.
Fresh pass on the new commits
No new findings. No em dashes, no semicolons introduced, all new @refs resolve
(LLP 0193#decision, 0194#decision, 0147) with glosses accurate to both the code and
the cited section.
Worth flagging for whoever merges, as a note rather than a finding: LLP 0193 :137-141
records a genuine new fail-closed residual - a queued prompt (two user records
answered by one assistant) now loses the earlier prompt as unknown, where master would
have projected it. It is visible (excluded_backend, counted in records_excluded),
unverified as a real OpenClaw shape, and has a named clean fix (#659). Documenting it
rather than guessing is the right call.
Verification - and an important caveat about CI
node scripts/run-tests.js→ 3586 pass / 0 fail / 1 skipped, exit 0.npx tsc -p tsconfig.json --noEmit→ exit 0.- Smokes
backfill_openclaw_fixture,backfill_openclaw_session_shape,gateway_codex_capture→ all exit 0. - Run twice independently, in clean isolated worktrees at
cbf96a2.
GitHub CI has not validated this head. All three workflow runs for cbf96a2 are stuckqueued with zero jobs dispatched; the status-check rollup at this head is empty. The
earlier runs that showed red were cancelled, not failed - steps: [], all four jobs
terminated at the identical second 15m02s in, against a workflow timeout-minutes: 5 that
never engaged. That is a runner-queue cancellation, not a signal about your code, and no
code change was made or requested on account of it. Neutral re-queued those runs.
So the local runs above are currently the only execution evidence for this head. This
review verdict covers code correctness, not CI. Treat CI as still owed before merge.
philcunliffe
commented
Aug 6, 2026
Verdict: approved - held for a maintainerRound 2 came back clean at
Two things to carry into that decision:
|
Uh oh!
There was an error while loading. Please reload this page.
…93 records why the gateway idles (#653) (#658) * An upstream-less ai-gateway source idles instead of throwing (#649) A picker run selecting `hermes` and nothing else composed `@hypaware/ai-gateway` with `{ upstreams: [] }` and then failed at source start with "ai-gateway: at least one upstream must be configured before start", so a reachable first-run choice produced a broken install. The gateway plugin does two separable jobs: at activation it contributes the `ai_gateway_messages` dataset and the shared `ai_gateway.projected_exchange` materializer, and at source start it runs the proxy. `@hypaware/hermes` wants the first alone. It is "never modified, configured, or proxied" (LLP 0119) so it contributes no `gateway_upstream`, yet the materializer is a hard `requires.plugins` dependency (LLP 0120) so its picker row must still compose the gateway plugin. So the source now treats an empty compiled routing table as idle: no listener bound, `state.listen` left unset (`localEndpoint()` keeps throwing rather than handing an attach a URL nothing is listening on), `status()` reporting `listening: false` with a reason. A `reload()` that brings an upstream binds normally, so idling is recoverable. Co-Authored-By: Claude <noreply@anthropic.com> * An idle gateway that was configured with upstreams is visible in status (#650 review) Letting an upstream-less `ai-gateway` source idle rather than fail its start is right for the config that wants it (hermes composes the plugin for its materializer alone, LLP 0120), but the same path swallowed a real misconfiguration. A gateway-only install whose TOML says `url =` where `base_url` was meant has every entry dropped by `compileUpstreams` without complaint, and `diagnoseV1Config`'s `gateway_missing_*_upstream` check does not fire for that shape. On master that install read `[failed]` / `degraded`; on this branch it read `[started]` / `healthy` while the user's client got ECONNREFUSED, with the reason living only in `status.message`, which the daemon snapshot never captures. `hyp status` now emits a non-degrading `gateway_idle_no_upstreams` warning when the gateway reports `listening: false` and a non-empty `details.upstreams`. That field is the raw configured names, pre-compile, so it discriminates exactly: a hermes-only install publishes `[]` and stays quiet and healthy, a dropped or misspelled upstream publishes `['anthropic']` and warns, naming the entry and the field that drops it. Also: `aigw.idle_no_upstreams` logs at `warn` with the configured count and names when the config listed upstreams and compiled to none, staying at `info` for the genuine hermes-only case; and `status()` reads the context `reload()` handed it rather than the boot-time closure, so `details.upstreams` describes the config in force (pre-existing, but the new diagnostic reads that field). Tests: the diagnostic fires for the dropped-upstream shape and not for the hermes-only one, and never degrades `overall`; a reload that removes every upstream tears a live listener down and idles without error, which ends capture for already-attached clients and is now pinned deliberately. Co-Authored-By: Claude <noreply@anthropic.com> * An idle gateway whose upstreams have no usable name is visible too (#650 review 2) The `gateway_idle_no_upstreams` warning gated on `details.upstreams`, the raw configured names, so it could not see the config it most needed to see. `compileUpstreams` drops an entry for a missing `name` exactly as silently as for a missing `base_url`, and a nameless entry contributes no name at all, so `provider = "anthropic", base_url = "..."` published `upstreams: []` and read as hermes-only: `healthy`, no diagnostic, ECONNREFUSED at the client. The repo's own `diagnoseV1Config` affirms that config, because it matches an upstream by its `provider` field. The gateway source already computed the count (its idle log is a `warn` on the strength of it); it just never published it. It now does, as `details.upstreams_configured`, and core gates on the count instead of the names. The names still ride along and still make the message concrete when they exist; a status file from a build that predates the count falls back to counting the names, so an older daemon's dropped `base_url` stays visible. Hermes-only (0 configured) stays quiet and healthy, and the diagnostic remains non-degrading. The repair lines pointed at `hyp config validate`, which prints `config ok` and exits 0 for exactly this config: `@hypaware/ai-gateway` registers no config section, so nothing checks upstream shape. Replaced with the edit that fixes it, naming the config file and both required keys, followed by the restart that makes it take effect. Co-Authored-By: Claude <noreply@anthropic.com> * A partially dropped gateway upstream is visible in status (#653) `compileUpstreams` drops an upstream entry missing `name` or `base_url`, per entry and silently. #650 made the all-dropped case visible; a partial loss stayed invisible, because the proxy binds, `listening` is never set, and the diagnostic only fired when it was `false`. A user who typo'd one of two upstreams got a working gateway that routed nothing for that provider. The gateway source now publishes `upstreams_dropped` (raw configured entries minus compiled routes) and the names it can attribute, and warns at boot when the routing table is smaller than the config asked for. `hyp status` reads the difference: one comparison, two mutually exclusive kinds - `gateway_idle_no_upstreams` when nothing survived, `gateway_upstreams_dropped` when some did. Both non-degrading, following the `gateway_port_fallback` precedent. Also mints LLP 0193, recording the "idle rather than throw" decision #650 established (which lived only in `proxy.js` and a PR thread) and its accepted visibility tradeoff. Co-Authored-By: Claude <noreply@anthropic.com> * Renumber the gateway-idle decision from LLP 0193 to 0195 (#658) PR #643 (branch openclaw-cli-backend-denylist) is open and older, and it already claims both 0193 and 0194. Master's highest is 0192, so scanning only master made 0193 look free. The minting rule takes the next number past the highest across the default branch and every branch, so this PR yields and moves to 0195, the first number free everywhere. Mechanical: the file, its title line, and the four @refs that point at it. No prose or decision content changes. Co-Authored-By: Claude <noreply@anthropic.com> * Fix gateway_upstreams_dropped message overclaim, pluralization, dupe names The non-idle status message claimed traffic for a dropped upstream is "not proxied and nothing is captured" unconditionally, but mergeUpstreams backfills a registered adapter preset (e.g. @hypaware/claude's `anthropic`) whenever its name is not already in the compiled config table, so a typo'd override can still route through the preset's default endpoint. Reword to hedge on that case, and fix the message's hardcoded plural to match the idle branch's configured/upstream(s) treatment (reachable at dropped === configured === 1 exactly via the preset-backfill path). Also dedupe readConfiguredUpstreams' droppedNames, which could print a name twice when two same-named config entries both fail to compile, and split the LLP 0195 @ref on gateway_upstreams_dropped so the "does not flip overall" half cites #consequences instead of #visible-when-unintended, which never says that. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: test <test@example.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: test <test@test.com>
Closes the Lane B half of #640.
What
{anthropic, openai}vendor allowlist to a two-rung CLI-backend denylist. A record is excluded when its effective backend stampsapi: "cli"(the mechanism marker, verified live on OpenClaw 2026.7.1-2) or its provider matches a sibling-adapter prefix (claude-cli,codex); an unresolvable backend still fails closed asunknown. Everything else projects, so ollama and any future direct-API provider are captured at transcript fidelity from birth, with no HypAware release per vendor. The(provider, api)pair smears as a unit so a neighbor's provider is never stitched to a different neighbor's api.SIBLING_ADAPTER_COVERAGEbecomes load-bearing for the exclusion (was labeling only);PROJECTABLE_PROVIDERSis deleted.AiGatewayProjectedMessagegains an optional per-messageprovider; the gateway row builder prefers it over the exchange value (the LLP 0026modelprecedence extended to the column it skipped); the OpenClaw backfill stamps every row from its turn's smeared backend, prompts and tool results included. Mixed-provider sessions stop folding minority turns into the first projected turn's vendor. Projectors that never set the field (all live capture) are byte-for-byte unchanged.Extended-byforward-ref its living-list section called for; stale allowlist glosses insession_file.js/projector.jsupdated.Why
An OpenClaw turn on the
ollamaprovider was recorded nowhere: the allowlist silently excluded it from the sweep while LLP 0167's coverage statement claimed it covered ("every OpenClaw turn is captured at least at transcript fidelity"). The allowlist direction was chosen when the CLI-backend provider strings were unverifiable (LLP 0161 s10); live verification has now expired that ignorance. The accepted fail-open residual (a future sibling-captured mechanism stamped neitherclinor a known prefix) is recorded in LLP 0193.Verification
npm test: green (one pre-existingusage-policy-foldfailure, also fails on clean master)npm run typecheck: cleanbackfill_openclaw_fixture,backfill_openclaw_session_shape,gateway_claude_captureall okapi: "cli"excludes an unrecognized provider with nocovered_by; realclaude-cli+clistamping excludes withcovered_by: claude_transcript; gateway-level row-by-row provider override and fallbackprovider=ollama,model=gemma4:12b; the all-claude-clisession still projects nothing@refannotations validateStill open in #640
Lane A (live gateway) ollama capture (NDJSON native dialect or the openai-compat reroute experiment), and the one-turn probe confirming a codex-backend turn stamps
api: "cli".🤖 Generated with Claude Code