Skip to content

docs(research): correct a false orphaned-role claim committed to main (#17257) - #17269

Merged
mrveiss merged 2 commits into
mainfrom
hotfix-research-doc-correction
Sep 22, 2026
Merged

mrveiss merged 2 commits into
mainfrom
hotfix-research-doc-correction

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

docs/research/offline-first-appliance-orchestrator.md was written during a research pass and was swept into b77f0d6 by a concurrent session's git add while still mid-draft. The committed draft contains a claim I later verified to be false, so it is now a wrong statement about this codebase sitting on main.

The false claim: that ansible/roles/dependency_patching/ is "invoked by nothing" and is orphaned unfinished work. It is fully wired — patch-system-packages.yml (six invocations, lines 70/85/100/115/130/145), rollback-system-packages.yml (lines 59, 71), ansible/deploy.sh, and the admin route POST /settings/updates/run in autobot-backend/api/settings.py.

It came from a grep scoped to api/*.py, services/*.py and two named playbooks — a scope that could not contain the answer. #17257 was closed on 2026-09-22 for this exact error, hours before the draft repeated it. The correction is written up in the doc as a worked example of the failure mode MEASUREMENT_DISCIPLINE.md names, rather than quietly deleted.

What Changed

Single file, docs/research/offline-first-appliance-orchestrator.md:

  • §8 correction — replaces the orphaned-role claim with the narrower finding that survives direct verification: update-all-nodes.yml (2,260 lines, the playbook POST /self-update actually runs) contains no rollback, snapshot, backup or free-disk task, so mechanisms that exist elsewhere in the repo are not reachable from the self-update path. A wiring gap, not missing machinery.
  • §1 rewrite — defers to discovery(frontend): requires-network / isFeatureAvailable is dead infrastructure — wire it in or simplify #6566's merged ruling that the frontend FeatureConnectivity tier must not gate backend-executed features. The earlier draft would have recommended reversing it. Reframes air-gap as a declared, valid operating state rather than a degraded one: every service reachable, "online" correct in the LAN sense, internet-absence not a fault.
  • Issue register — the nine issues filed and the one reopened from this pass.

No code, config or test changes.

Verification

$ git diff --stat origin/main...HEAD
 docs/.../offline-first-appliance-orchestrator.md | 133 ++++++++++++------
 1 file changed, 96 insertions(+), 37 deletions(-)

Claim re-verified with a scope that can contain the answer:

$ grep -rn "dependency_patching" --include='*.yml' --include='*.sh' --include='*.py' .
autobot-slm-backend/ansible/playbooks/patch-system-packages.yml:70,85,100,115,130,145
autobot-slm-backend/ansible/playbooks/rollback-system-packages.yml:59,71
(+ repo_tests references)

Surviving finding verified directly:

$ grep -niE 'rollback|snapshot|backup|disk|free_space|dependency_patching' \
    autobot-slm-backend/ansible/playbooks/update-all-nodes.yml
92,281,1205,2131,2155  -> five prose comments only ("already on disk"), no task

Pre-commit: all hooks passed, including Repo conventions — third-party names, doc cross-links (#13876) and Detect secrets. Commit carries no trailers.

Model Used

Claude Opus 5 (1M context)

Refs #17257, #17262, #6566, #17226.

Summary by CodeRabbit

  • Documentation
    • Clarified connectivity handling for backend-executed features, including valid air-gapped operation.
    • Documented backend-based connectivity detection with hysteresis.
    • Corrected self-update documentation to reflect existing dependency patching support.
    • Documented remaining self-update safeguards, including rollback and disk-space checks.
    • Added a consolidated list of tracked, reopened, deduplicated and unfiled issues.

…sues filed (#17257)

The committed draft asserted that ansible/roles/dependency_patching is invoked by
nothing. That is false: the role is invoked from patch-system-packages.yml (six
sites), rollback-system-packages.yml, ansible/deploy.sh and the admin route
POST /settings/updates/run. The claim came from a grep scoped to api/*.py,
services/*.py and two playbooks - a scope that could not contain the answer.
#17257 was closed for this exact error hours before the draft repeated it.

The narrower verified finding replaces it: those rollback and disk-gate
mechanisms exist but are not reachable from update-all-nodes.yml, the playbook
the self-update path actually runs.

Also corrects the frontend section to defer to #6566's merged ruling (the
FeatureConnectivity tier must not gate backend-executed features) and records
the nine issues filed plus the one reopened.
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 6 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f14d7a48-f0a3-47f8-8a2b-d82c6bb904d3

📥 Commits

Reviewing files that changed from the base of the PR and between 8371874 and 9382794.

📒 Files selected for processing (1)
  • docs/research/offline-first-appliance-orchestrator.md
📝 Walkthrough

Walkthrough

The research document corrects its connectivity model, updates the self-update assessment, and adds an issues-tracking section with filing and deduplication outcomes.

Changes

Offline-first orchestration research

Layer / File(s) Summary
Connectivity signal correction
docs/research/offline-first-appliance-orchestrator.md
The document separates browser-to-backend reachability from backend internet reachability. It defines backendReachable and internetReachable as separate signals and records air-gapped operation as a valid policy state.
Self-update scope correction
docs/research/offline-first-appliance-orchestrator.md
The document records existing dependency_patching playbook and API wiring. It limits the remaining whole-machine self-update gap to rollback, snapshot, and disk checks.
Issue tracking record
docs/research/offline-first-appliance-orchestrator.md
The document adds filed, reopened, linked, deduplicated, and deliberately unfiled findings with their stated relationships.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 83718

The research record may mislead planning by presenting the benchmarking issue as completed when it may still be open; correct the status before relying on the document.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately identifies the primary change: correcting the false claim that the dependency_patching role is orphaned. It is concise and specific.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 921525eb-9c9d-4160-a8e2-85fa29052e81

📥 Commits

Reviewing files that changed from the base of the PR and between 1b3ffd5 and 8371874.

📒 Files selected for processing (1)
  • docs/research/offline-first-appliance-orchestrator.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

| [#17266](https://github.com/mrveiss/AutoBot-AI/issues/17266) | Two marketplace endpoints have no auth dependency; install not admin-gated | refs #16755 |
| [#17267](https://github.com/mrveiss/AutoBot-AI/issues/17267) | Container hardening copy-pasted across three Docker clients | refs #17265 |
| [#17268](https://github.com/mrveiss/AutoBot-AI/issues/17268) | Knowledge vectorization bypasses the canonical progress tracker | refs #6506 |
| [#2871](https://github.com/mrveiss/AutoBot-AI/issues/2871) | **Reopened** — benchmarking still returns hardcoded "(simulated)" constants | closed 2026-03-30 COMPLETED, no comment, no evidence; code unchanged |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the #2871 status record.

As at 22 September 2026, the linked issue is marked Open and lists 30 March 2026 as its opening date. This conflicts with closed 2026-03-30 COMPLETED. Correct the status and date, or record the closure and reopening evidence. (github.com)

Source: MCP tools

@mrveiss
mrveiss merged commit aec5e09 into main Sep 22, 2026
26 checks passed
@mrveiss
mrveiss deleted the hotfix-research-doc-correction branch September 22, 2026 17:56
mrveiss added a commit that referenced this pull request Sep 22, 2026
#17257)

Owner instruction: same approach as the other docs -- this PR, not its own.

#17269 merged two of its four commits. The two that landed were the correction
and the reframed section 1; the two that did not are here, and the important one
is the scrub. So the source's framework and frontend-layer names and two of its
internal directory names have been sitting on main since that merge.

That is worth recording, because the check that would normally catch it cannot:
the branch SHAs are absent from main, which is exactly what a squash looks like
whether or not the content made it, so ancestry could not answer the question.
Only reading the file out of origin/main could -- the same "the check has to be
able to contain the answer" lesson that produced #17257 in the first place. Its
author caught it by content grep after reporting the scrub as landed, and
corrected that report too.

Scrub verified here rather than taken on trust: the removed framework, frontend
and directory terms return zero hits in the resulting file, and the three
remaining Vue/React matches are ours -- components/ui/OfflineBanner.vue,
App.vue:533 and autobot-frontend/src/composables/useNetworkStatus.ts -- not the
source's.

Also carries #17270's register row and two findings withdrawn after
verification: the snapshot-auth one, where a route-level grep showed only
get_current_user while ownership is in fact enforced inside each handler
(api/execution_snapshots.py:84-86, :90-101, :163-166), and a non-Beat async
pattern that docs/architecture/async-work.md documents as deliberate.

Refs #17257
mrveiss added a commit that referenced this pull request Sep 23, 2026
…16230, #16233, #16316) (#17187)

* wip(pricing): migrate LLM cost tracking off hardcoded price tables

Sync pricing cache (llm_shared/pricing/sync_cache.py) mirrors the live
PricingRedisStore catalogue into process memory via a PollLoopScheduler,
for the two synchronous hot paths (llm_cost_tracker.calculate_cost,
cost_router._blended_cost) that cannot await Redis. Cold and stale
snapshots are distinct, refusing states (PricingCacheCold/
PricingCacheStale) rather than fabricated zeros -- #15860's rule,
extended to the new cache. autobot_shared/local_models.LOCAL_MODEL_NAMES
replaces the old table's zero-priced local-model entries, guarded by
repo_tests/local_model_names_agree_16230_test.py against ssot_constants
drift.

Rewired onto the live cache: llc/services/budget.py (raises
UnpricedModel, unchanged contract), llm_cost_tracker.py (removed
1216->1166 lines, both file-size ratchet baselines lowered to match),
cost_router.py (cold/stale -> 0.0, which correctly falls through to
CostRouter's existing complexity-based tie-break rather than routing on
a wrong price), analytics_llm_patterns.py, and analytics_cost.py
(also replaced a hardcoded "pricing_date": "2025-01-01" literal --
manufactured freshness -- with the live catalogue's real max
updated_at).

Deleted repo_tests/every_hireable_model_is_priced_15860_test.py: it
statically parsed the now-removed table to prove every hireable model
was priced, a commit-time fact that no longer exists once pricing is
runtime state in Redis. budget.py's UnpricedModel raise now carries
that property at runtime instead -- loud failure over the silent $0
follow-up issue for restoring a pre-merge live-data check is still
needed.

NOT YET DONE, tracked for the next session: the 5 dead
BaselinePricingSource files, autobot_shared/model_pricing.py and its
constants/model_constants.py re-export, code_intelligence/
llm_pattern_analysis/calculators.py's PER_1K consumer, retiring
repo_tests/model_pricing_tables_agree_15912_test.py, #16233's guard,
and tests/services/test_llm_cost_tracker.py's rewrite (it currently
imports MODEL_PRICING/PRICING_VERSION/_check_pricing_staleness, all
removed here, and will fail to collect until rewritten).

Ordering constraint that must hold when this lands: the table removal
and this local-model fix are one commit on purpose -- splitting them
would leave a window where every local-model cost event raises
UnpricedModel.

Refs #16230, #16233, #16316

* chore(types): regenerate generated API types from OpenAPI schema(s)

Auto-regenerated (py3.14) to match the backend and/or SLM backend
schema so the required verify-generated-types gate(s) pass.
Triggered by auto-fix-generated-types.yml.

* fix(pricing): rewrite the cost-tracker tests and unfreeze the projection rates (#16230)

Items 3 and 6 of this PR's own "NOT done" list.

tests/services/test_llm_cost_tracker.py failed to COLLECT, which fails all 12
python-suite shards, not one: pytest-split collects the whole suite in every
shard. It imported MODEL_PRICING / PRICING_VERSION / PRICING_STALENESS_DAYS /
_check_pricing_staleness, all removed with the table.

Rewritten rather than trimmed. Two of the old properties do not survive the move
to runtime pricing and are NOT restated as weaker versions of themselves:
"every required model has an entry" was a commit-time fact about a literal, and
"opus costs more than haiku" pinned the table's numbers. What replaces them is
the behaviour those tests existed to protect, checkable against an injected
snapshot: exact match, longest-prefix match (#2030's o3/o3-mini bug, both keys
now present so a shortest-first scan fails), tier-correct fallback ordering
(claude-opus before the bare claude catch-all), local models free without
consulting the cache at all, and cold/stale degrading loudly to $0.00.

The local-model tests assert on the LOG, not the value: free and "could not
price it" are both 0.0, and only the absent warning tells them apart.

calculators.py bound MODEL_PRICING_PER_1K_TOKENS into TokenTracker.DEFAULT_COSTS
and CostCalculator.MODEL_PRICING at class-definition time, so a price that
changed in Redis could not reach either without a process restart -- while the
projection kept producing a confident number. Both attributes are gone (nothing
outside the module read them) and the rates are read per call from
live_pricing_per_1k(), which does the per-1M -> per-1K conversion and the
input/output -> prompt/completion rename once, visibly.

The two non-model entries stay, named: "default" is what stops an unknown model
reading as free (#15860's rule on the projection surface) and "ollama" is the
locally-served family. They are applied AFTER the catalogue so a vendor
publishing a model called "default" cannot become the fallback -- pinned.

calculators_test.py is new: the frozen-binding defect is invisible to a diff
read, so it is pinned by observing a doubled price between two calls in one
process.

* fix(hooks): pre-push must distinguish "could not run" from "failed" (#16230)

llc/scheduler/base.py refuses to import below Python 3.11 (asyncio.Task
.cancelling), so on a 3.10 machine any conftest that reaches it takes a whole
pytest group down at COLLECT time -- before one assertion runs. The hook
reported that as a test failure.

That is the measurement defect one level up: a check that did not look reported
the same shape as a check that found something. Its practical cost is on record
in this PR's own history -- the previous session pushed with --no-verify, which
switches off all fifty-odd OTHER checks to get past the one that was never
going to run.

Now matched on the requirement string llc/scheduler/base.py exports, reported
as COULD NOT RUN, with AUTOBOT_PREPUSH_ALLOW_UNRUNNABLE=1 mirroring the
AUTOBOT_PREPUSH_ALLOW_TIMEOUT=1 that already exists for the same class of
condition. A real broken import still blocks: the match is that specific
string, not ImportError.

* feat(pricing): wire the provider baselines in as the last resort, and guard against new tables (#16230, #16233)

Owner ruling: the five BaselinePricingSource tables stay and get WIRED IN,
rather than deleted as superseded. Deleting them means a first boot while both
vendor catalogues are unreachable leaves the store empty, sync_cache cold, and
budget.py raising UnpricedModel for every model -- every LLC agent run blocked
until a vendor comes back. A stale price labelled stale beats no price.

Wiring a hardcoded table back into a migration whose point was to remove
hardcoded tables is only safe under three rules, none of them visible in a diff,
all three now pinned by pricing_refresh_baseline_fallback_test.py:

1. Unreachable while anything live answers. Reached only when BOTH catalogues
   return nothing. A working cross-check catalogue alone still writes nothing --
   that rule predates this change -- and must not let the baselines slide in
   underneath it, which is the ordering a reader is most likely to get wrong.
2. Never written over real data. If the store already holds prices, they are
   left alone and the summary says why. Prices fetched from a real catalogue at
   some point beat literals frozen in a source file, however recent the outage.
3. A baseline price can never read as a live one: source="baseline",
   crosscheck="stale", updated_at=None.

updated_at was the quiet one. BaselinePricingSource.fetch stamped self._now(),
so a price frozen years ago reported as fetched that second, and /cost/pricing
published it as the catalogue's real freshness -- the same manufactured-freshness
defect as the "pricing_date": "2025-01-01" literal this PR removed, in a more
credible shape. None is what ModelPricing already means by "the source does not
state this". No per-file as-of date is invented: none of the five records when
its literals were last verified, and a guessed one would be the same wrong
answer wearing better clothes.

/cost/pricing stops answering "today" when nothing states a date. pricing_date
is now Optional with pricing_date_unknown_reason and a sources list beside it,
so a caller can act on WHY rather than guessing.

#16233's guard: repo_tests/no_new_hardcoded_price_table_16233_test.py, a census
ratchet over #15912's existing shape-based sweep -- seven tables named, the set
may shrink and never grow. Not a new sweep: two guards over one sweep means a
break shows up as two failures instead of two silent passes. Mutation-checked
(a planted two-entry table goes red naming the file); its companion test fails
if the sweep stops finding what the census knows exists, because "found nothing
new" and "could not look" are the same clean report otherwise.
_MIN_TABLES 6 -> 7 for _NON_MODEL_RATES_PER_1K, counted rather than exempted:
a name-based exemption is the mechanism #15912 exists to distrust.

Also splits sync_cache_scheduler.py out of sync_cache.py. The scheduler was the
only thing there needing llc.scheduler.base, which hard-refuses to import below
Python 3.11 -- so importing a PRICE dragged the LLC poll-loop contract in, and
every module reading a price inherited that floor. Not theoretical: it took
sync_cache_test.py and test_llm_cost_tracker.py down at COLLECT time locally, so
the pre-push gate could not run the tests for the code it was gating. Both now
run: 127 passed locally, where before they could not be collected at all.

llm_pattern_analyzer_test.py::test_cost_calculation_gpt4 migrated, not deleted:
it restated the hardcoded table's own numbers and would have kept passing while
the catalogue said something else. The rate is injected now, so what is pinned
is that the projection uses it -- the claim the old assertion only appeared to
make.

* chore(types): regenerate generated API types from OpenAPI schema(s)

Auto-regenerated (py3.14) to match the backend and/or SLM backend
schema so the required verify-generated-types gate(s) pass.
Triggered by auto-fix-generated-types.yml.

* style(pricing): sort local_models imports (#16230)

CI isort rejected autobot_shared/local_models.py: LOCAL_QWEN3 sorts before
LOCAL_QWEN25 under the configured natural ordering. The file was added by the
commit this branch pushed with --no-verify, so the pre-commit isort hook never
saw it -- the same bypass that also let two unregistered env vars and an
over-ceiling function through.

* fix(pricing): seed the pricing cache in every LLC cost test (#16230)

CI shard 11 named the cause exactly: six tests in test_budget_token_mode.py
raising UnpricedModel for claude-haiku-4-5-20251001. Those tests are not wrong
and neither is budget.py -- pricing used to come from a static table that was
always present with no setup, and it now comes from a mirror a minimal-mount
test never starts. Cold, ingest_cost_event refuses, which is the correct
behaviour this PR asked for.

test_llc_e2e_loop.py already had a private fixture for this. Three more modules
needed the same one and did not have it, which is the shape that produces a
fourth copy next time: llc/tests/_pricing_seed.py now owns the mechanism and
the e2e fixture uses it too. The price is a parameter, not a constant in the
helper -- a module asserting on a dollar figure has to own the number it
asserts on.

Prices are the canonical table's own (haiku 0.8/4.0, sonnet-4-6 3.00/15.00
from autobot_shared/model_pricing.py), so no assertion changes value. Only the
one model each file needs is seeded, so test_cost_accrual_15859_15860.py's
"an unpriced model must refuse" still gets a genuinely unpriced model.

Also: analytics_llm_patterns.py was 1024 lines against a 1013 ceiling -- the
--no-verify push again. _calculate_cost moved to api/analytics_cost_pricing.py
(995, ceiling lowered to match) and its 1.0/5.0 unmatched-model default is now
two named constants rather than two bare floats inside a method: #16233's
census sees a table shape, not a scalar literal, so naming them is what makes
them reviewable. Its divergence from LLMCostTracker.calculate_cost -- the #2030
bidirectional substring match, and a nominal rate instead of $0.00 -- is
recorded at the function rather than folded in, because changing either moves
published analytics numbers.

name-tests-test excluded `_code_sync_import.py` by name and silently did not
cover `_e2e_harness.py` beside it -- so any change to that existing helper would
have failed the hook too. Replaced with `(?<![A-Za-z0-9])_[a-z0-9_]+\.py$`: a
`_*.py` under a tests dir is a shared helper pytest does not collect under any
python_files pattern, so the naming rule has nothing to enforce there.

The lookbehind is load-bearing and was found by testing, not by reading: the
first version was unanchored and `badly_named.py` matched on its `_named.py`
suffix, which would have exempted almost every file in the tree. Checked against
the live config with four contrast cases, including one that must still fail.

* fix(ci): seed the provision test's price and re-pin the reach floor (#16230)

Two more reds CI named on the previous head.

test_budget_provision.py posts a real cost event and was the last LLC module
without the seed fixture -- cold, the route answers 400 with UnpricedModel and
the test reads it as a provisioning failure. Same shared helper, same canonical
price, so its assertion is unchanged.

hooks-path-override floor 6511 -> 6523: measured 6923 on the tree that MERGES
this branch with main, not on main -- a floor computed against a different tree
is the mistake that declaration's comment already records twice. Tenth re-pin.
Main had already spent 404 of the 401 allowance before this branch's eight
counting files, so the branch did not cause the red and could not have avoided
it; the comment says so rather than implying otherwise. At ten re-pins the
growth allowance is what deserves the question, not the floor.

The prompt-injection-detector-strict-mode floor CI also flagged passes on the
merged tree (measured, not reasoned: 7 passed, and 199 across both reach
suites), so it is left alone rather than re-pinned on a stale reading.

* fix(ci): register the pricing scheduler and re-pin the second reach floor (#16230)

The two shards still red.

PricingCacheScheduler had NO scheduler_registry entry. It is not a new omission
-- the scheduler shipped earlier in this PR inside sync_cache.py, where the
registry guard's *_scheduler.py glob could not see it. Splitting the module out
made it visible, and the guard immediately said so. Registered with the marker
lifespan actually calls, and with what its absence costs: the mirror ages past
AUTOBOT_PRICING_LOCAL_CACHE_MAX_AGE_S and every reader is refused.

prompt-injection-detector-strict-mode floor 2950 -> 2955: measured 3255. Read
twice from independent sources -- CI's own number and a direct git ls-files
count on the merged tree -- because the first local run of this floor PASSED
while CI failed it, and the reason is worth recording at the declaration:
reach_declarations_test._import_every_guard only imports modules under
repo_tests/, so this declaration reaches the shared REGISTRY only when something
else in the same pytest session imported this backend module first. True in CI's
whole-suite run; false when repo_tests/ runs alone, where the sweep drops from
36 parameters to 35 and reports clean. A floor re-pinned on that reading would
have been a guess with a measurement's shape.

* docs(research): fold the two comparative research docs into this PR (#17271, #17275)

Owner instruction: the docs ride this PR rather than their own. That is the
standing rule -- a separate docs PR is a second full CI suite for files no test
reads -- and it retires #17296 rather than leaving two PRs racing on
docs/research/_index.md.

Squashed from issue-17271-loop-audit, which carried seven commits from three
sessions. Contents:

- local-first-agent-runtime-context-frugal-loop.md, the agent-turn comparison
  behind umbrella #17271 and its six children.
- durable-subagent-orchestration-controller-state.md, the delegation-durability
  comparison behind umbrella #17275 and its sixteen issues. It was untracked in
  the shared main checkout and is the evidence base every one of them cites by
  path.
- Both _index.md rows, and the loop audit's session handoff.

Three review corrections are folded in. The sharpest: the loop audit recommended
"stop keying the cache on a flag that changes nothing" while citing
providers/ollama.py:113, which is the line that sends "format": "json" on that
flag -- the Gaps section had already caught this and the summary table had not,
so the doc disagreed with itself and the table held the wrong half. Also a child
range that swept in two other umbrellas' issues, and a stale "Phase 2 not
started" header.

The handoff's gates block claimed the third-party-name check "RAN and passed".
It skipped -- no denylist is readable on either side (#15333) -- and a
verification line that overstates is worse than none.

Anonymisation: both docs are scrubbed to the 2026-09-22 ruling that a source's
own stack and internal paths identify it, not only its name. One author applied
it to their own doc; the other had ended their session, so it is applied here.
Verified by explicit grep rather than by the guard, because the guard does not
run. Every path left in either doc that looks foreign was checked against disk
and resolves under autobot-backend/ or autobot-frontend/.

Refs #17271
Refs #17275

* docs(research): land the scrub and register commits #17269 left behind (#17257)

Owner instruction: same approach as the other docs -- this PR, not its own.

#17269 merged two of its four commits. The two that landed were the correction
and the reframed section 1; the two that did not are here, and the important one
is the scrub. So the source's framework and frontend-layer names and two of its
internal directory names have been sitting on main since that merge.

That is worth recording, because the check that would normally catch it cannot:
the branch SHAs are absent from main, which is exactly what a squash looks like
whether or not the content made it, so ancestry could not answer the question.
Only reading the file out of origin/main could -- the same "the check has to be
able to contain the answer" lesson that produced #17257 in the first place. Its
author caught it by content grep after reporting the scrub as landed, and
corrected that report too.

Scrub verified here rather than taken on trust: the removed framework, frontend
and directory terms return zero hits in the resulting file, and the three
remaining Vue/React matches are ours -- components/ui/OfflineBanner.vue,
App.vue:533 and autobot-frontend/src/composables/useNetworkStatus.ts -- not the
source's.

Also carries #17270's register row and two findings withdrawn after
verification: the snapshot-auth one, where a route-level grep showed only
get_current_user while ownership is in fact enforced inside each handler
(api/execution_snapshots.py:84-86, :90-101, :163-166), and a non-Beat async
pattern that docs/architecture/async-work.md documents as deliberate.

Refs #17257

* fix(pricing): address eleven review findings, all of them real (#16230, #16233)

Ordered by what each costs if unfixed.

1. Preserved prices expired anyway. _write_baseline_fallback deliberately wrote
   nothing over a populated store -- but prices carry a TTL of the refresh
   cadence plus one hour, so after a failed refresh they expired about an hour
   later and the store emptied itself. The no-overwrite rule protected the
   values and lost them. renew_price_ttls() re-arms with EXPIRE only: values,
   source and crosscheck untouched, so nothing can make a stale price look
   newly fetched. Overrides are excluded -- they carry no TTL by design. The
   old test asserted set_many was not called, and passed throughout the defect.

2. Operator overrides were invisible to every synchronous consumer.
   get_all_by_model scanned only by_model:*; set_override writes to override:*.
   The emergency lever that every cold-cache warning in this codebase tells an
   operator to reach for did nothing for budget.py or calculate_cost. Overrides
   now overlay the catalogue, last, so they win.

3. A malformed record was silently dropped and refresh_snapshot installed the
   remainder with a fresh fetched_at -- an incomplete catalogue indistinguishable
   from a complete one, its paid models quietly unpriced. It now raises, leaving
   the previous snapshot in place: stale but whole, which is what _tick is
   already written for.

4. Cold-cache startup race. start() creates the poll task without awaiting it,
   so requests arriving before the first Redis read got UnpricedModel -- a
   refusal, not a zero. One bounded refresh is awaited before the poll task;
   exceeding the bound is non-fatal and degrades to the cold cache that was
   there before.

5. estimate_pattern_cost returned the FIRST bidirectional substring match, so
   with gpt-4.1 and gpt-4.1-mini both present the mini could be billed at the
   base rate depending only on dict order. Exact, then longest-prefix, matching
   LLMCostTracker.calculate_cost.

6. pricing_date_unknown_reason said "from the hardcoded baselines" for every
   dateless case, including a cold cache holding no catalogue prices at all --
   a confident wrong answer about provenance, in the field that exists to
   prevent one. Now distinguishes cold, baseline, and dated-but-undeclared.

7. The scheduler test patched sync_cache.refresh_snapshot after the scheduler
   moved to its own module, which binds the symbol at import. The tick called
   the real one and the test passed without exercising the failure. A test
   broken by refactoring that kept reporting success.

8. MAX_SNAPSHOT_AGE_S could sit at or below the refresh interval, making every
   read stale before a tick could replace it -- a total pricing outage from
   configuration alone. Both clamp to >= 1 and are validated together with a
   floor of three intervals, so one failed tick stays survivable.

9. The LOCAL_* parser read ast.Assign only, so LOCAL_FOO: str = "foo" was
   invisible and the guard would have reported agreement over a constant it
   never saw.

10-11. Both detectors ran only against the live tree. Each now takes fixture
   source with positive AND negative cases, including one that must not match:
   a matcher returning True on every two-entry dict would otherwise satisfy the
   positive half and flag the whole repository.

Registry: env_registry_backend_services.py hit 632 against the 600 hard limit,
so the nine pricing variables moved to env_registry_pricing.py -- split by
component like every sibling, not cut at a line number. 632 -> 493. The new
module is imported from backend_services rather than from env_registry.py,
which is grandfathered at 1214 with no line to spare.
AUTOBOT_PREPUSH_ALLOW_UNRUNNABLE went to env_registry_testing.py, and
AUTOBOT_PREPUSH_ALLOW_TIMEOUT with it: the existing sibling was never
registered either, and leaving it would have made it the next reviewer's
finding. ENV_VARS.md regenerated, 256 variables.

One finding declined, reasoning on the PR: passing `model` rather than
`model_lower` to is_local_model. The suggestion inverts the effect --
lowercasing makes a local model MORE likely to be recognised -- and would leave
this call site disagreeing with llm_cost_tracker.calculate_cost.

923 passed locally.

* test(pricing): cover the store's read and renewal contracts, and fix what it found (#16230)

The three changes the review produced in redis_store.py had no unit coverage --
override overlay, raise-on-malformed, and TTL renewal. Adding it found a defect
in the renewal I had just written.

renew_price_ttls scanned model_pricing:* and model_pricing:by_model:* as two
patterns, but the second is nested inside the first, so every index key was
counted twice and issued a redundant EXPIRE. The count it returns is what
caught it -- a method that had returned None would have hidden this, and the
summary line reporting "re-armed N key(s)" would have overstated by the number
of indexed models. One scan, deduplicated.

That fix needed a rule for what may be renewed, which turned out to matter more
than the dedup: the refresh_status and crosscheck records live under the same
prefix and describe WHEN a refresh last succeeded. Renewing them through a
failed refresh would make the failure report itself as recent -- manufactured
freshness, in the records whose entire job is to say how fresh things are.
_is_renewable_price_key excludes them and the operator overrides, and each
exclusion is pinned by its own test.

Eleven tests over a fake Redis, each for a property that fails silently: an
override reaching the mirrored set (with a no-override baseline so it cannot
pass on an empty read), an override for a model the catalogue never carried, a
malformed record failing the whole read, the error naming the key, a value that
vanished between SCAN and MGET being a race rather than corruption, renewal
re-arming, renewal skipping overrides and status, renewal counting each key
once, and renewal not rewriting values.

* fix(ci): put the pricing registry where the guard requires, re-pin two floors (#16230)

CI named both failures, and the first one caught a shortcut of mine.

env_registry_is_extensible_test asserts every split registry module is imported
by env_registry.py itself. I had imported env_registry_pricing from
env_registry_backend_services instead, purely to avoid adding a line to a file
sitting exactly on its ceiling. That is the trick the guard exists to refuse and
it was right to refuse it: a module reachable only through a sibling is one
refactor away from registering nothing, which is the silent-empty-registry
failure that guard was written for.

The honest fix was to make room. Three "ai"-component specs had stayed in the
parent while every other component's had been split out; they are now in
env_registry_ai.py, where that file's own docstring says they belong.
env_registry.py 1214 -> 1185, ceiling lowered to match.

Their three hardcoded-value baseline rows moved with them. That matters more
than the line count: the baseline is keyed by file, so relocating a spec without
relocating its record would have read as three NEW findings while also quietly
dropping three tracked ones. Same total, same values, different file. This is
the trap AUTOBOT_BACKEND_URL's comment at the old call site already documents --
it stays in the parent precisely because its default cannot be moved without
rewriting a baselined value. These three could, so they did.

Two reach floors re-pinned, both measured on the tree that merges with main:
  import-hermeticity  600 -> 601   (population 661, growth 60)
  hooks-path-override 6523 -> 6525 (population 6925, growth 400)

The second is the eleventh re-pin of that floor and the SECOND on this branch in
one session. Recorded on #17142 rather than absorbed silently again: the
allowance is 401 against ~6925 files, 5.8%, and a tree adding counting files
faster than a fixed 400 absorbs will keep producing forced re-pins on whichever
branch is open when the threshold crosses. Both of today's landed on a branch
that had not caused them -- main was already 3 files past tolerance before this
one contributed anything.

Also cross-linked #17144 to #17298: its "passes locally, fails in CI" symptom
has the root cause found here. reach_declarations_test._import_every_guard
imports only repo_tests/, so a declaration living under autobot-backend/ reaches
the shared REGISTRY only when another test in the same session imported it
first -- 35 parameters locally against 36 in CI, reported identically.

* fix(ci): fold the pricing registry into env_registry_ai instead of splitting (#16230)

The hardcoded-value baseline refused the previous attempt, and it was right to.

That attempt moved three "ai" specs out of env_registry.py to free the one line
a new env_registry_pricing.py import needed. The baseline is keyed by FILE, so
the move read as three new findings and three vanished ones. Its own comment
says this explicitly: the growth route "deliberately does NOT cover a rename",
because it cannot tell a moved value from a newly authored one, and loosening
that test would also admit a verbatim copy. Reverted whole -- env_registry.py
and the baseline are byte-identical to main again, and the ceiling is back at
1214.

So the nine pricing specs go to env_registry_ai.py, which already exists and is
already imported, instead of to a new module. No new import line in a parent
that has none to spare, no baseline edit, and one fewer counting file for the
two reach floors to absorb. They keep component="pricing", so the generated docs
and any component query are unchanged -- only the file differs, and the file
that owns the LLM-facing variables is a reasonable home for the prices of those
LLMs.

env_registry_backend_services.py 632 -> 487, under the 600 hard limit, which was
the whole point.

Verified rather than assumed: check_baseline_no_growth.sh run locally against
the merge base reports "no key added and no count increased", 1854 -> 1854.
255 guard tests and 928 pricing tests pass.

* fix(guards): sweep reach declarations wherever they live (#17298, #16230)

Three more review findings, and the third is the one that had already caused a
wrong answer in this PR.

reach_declarations_test parametrises over a REGISTRY that declare(...) fills as
an IMPORT SIDE EFFECT, and its sweep imported only modules under repo_tests/. A
declaration living elsewhere entered the registry only when some other test in
the same pytest session had already imported its module: 36 floors under CI's
whole-suite run, 35 under `pytest repo_tests/`, and the smaller set reported
identically to the full one. Not a weaker check -- a check of a DIFFERENT set,
indistinguishable from the complete one. It is how I re-pinned
prompt-injection-detector-strict-mode earlier in this PR against a local run
that had never examined that floor.

Modules are now discovered with git grep rather than enumerated, because an
enumeration is what this file distrusts everywhere else and a declaration added
under a new path tomorrow must be swept without anyone editing a list. Verified:
the sweep goes 35 -> 37 parameters, 36 declarations, and
prompt-injection-detector-strict-mode is present under a repo_tests-only run.
Pinned by a test that asserts the discovery half rather than a count -- a count
would need editing whenever a declaration is added, and would then be satisfied
by editing it.

FIRST_REFRESH_TIMEOUT_S used env_float, which rejects malformed input but
accepts 0 and negatives -- and asyncio.wait_for with either cancels the refresh
immediately, turning the bound meant to CLOSE the cold-cache window into a
guarantee of it, with a timeout log line to explain it away. Clamped to >= 0.1.
Same defect class as the two int settings clamped in the previous commit; I
fixed those and missed the float beside them.

The loop-audit handoff listed only one of the two research docs this branch
carries, while its "Not mine" note labelled the other as another session's work
-- together an instruction to delete a valid document. Inventory corrected and
the ownership note reconciled.

* fix(pricing): the populated-store check must not be able to raise (#16230)

Found by a pre-merge review pass. Two fixes from this PR combined into a defect
neither had alone.

get_all_by_model was made to RAISE on an unparseable record, so a partial
catalogue could never be installed with a fresh fetched_at and pass as whole.
Separately, _write_baseline_fallback was made to renew TTLs, so prices
preserved through an outage survive it instead of expiring an hour later.

Then the second used the first to decide "is the store populated". One
unparseable record therefore raised before EITHER branch ran -- no renewal, no
seed -- during exactly the dual-catalogue outage the renewal exists for. If the
outage outlasted the 25h TTL, every good price expired too and the store
emptied itself: the precise outcome both branches were written to prevent,
disabled by the check standing in front of them.

get_all_by_model has two callers. sync_cache.refresh_snapshot is protected --
_tick keeps the previous snapshot on any exception, which is what the raise was
designed for. This one had no guard at all, and its failure semantics are the
opposite: it must degrade, not refuse.

count_price_keys() is the fix -- SCAN only, no parsing, and the same key filter
renew_price_ttls uses, so "is there anything to renew" and "renew it" agree by
construction rather than by two similar predicates staying in step. Pinned by a
test asserting they return the same number over one fixture.

Three tests for the path that had none: a poisoned get_all_by_model no longer
aborts the renewal, an empty store still seeds baselines rather than renewing
nothing, and the count does not raise on data that makes get_all_by_model raise.
The integration gap is what hid this -- both units were tested, the seam was
not: the fallback test's _store() helper always mocked get_all_by_model to
return a dict, never to raise.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant