get_skills: the server cross-check reports success without performing it #269
Labels
No labels
burndown-2026-06
headless
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/eco-app#269
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
get_skills(server=...)is documented as: "Passing a server cross-checks the specialties actually in use and reports any this graph omits." At5e05296it returnsskillsCrossChecked: trueandskillsInUseNotInGraph: []regardless of whether the check ran or found anything.A verification that always reports success is worse than no verification — it converts an unanswered question into a confident all-clear.
Failure path: reports success against a host that does not resolve
That hostname has no DNS record.
get_server_statuson the same string in the same session returnsConnectError: [Errno -2] Name or service not known. So the tool asserts a cross-check against a host it provably could not contact.Success path: misses six specialties actually in use
This is not only a failure-path bug. Against the default server
eco.coilysiren.me:3001, which is reachable:get_progressionon the same server returnsbySpecialtycontaining six specialties with live citizen counts that are absent from the 44-skill bundled graph:AnimalHusbandrySkillLibrarianSkillFishingReloadedSkillMixologySkillBiochemistSkillBeekeepingSkillThese are exactly the modded specialties the parameter exists to surface — the server advertises 20+ mods including Biochemist and Animal Husbandry in its own
/infodescription.skillsInUseNotInGraphshould list all six and returns an empty array.Context
ac2da90—feat(recipes): read a modded export, and never pass vanilla off as modded— got the adjacent concern right:get_recipescorrectly reportssourceKind: "autogen"andserverSpecific: false, so a caller knows the graph is the vanilla seed.get_skillsinherits that same vanilla graph but then claims to have reconciled it against the live server, which undoes the honestyac2da90established.Acceptance criteria
skillsCrossCheckedisfalse(or the call errors) when the server could not be reached.skillsInUseNotInGraphlists the six specialties above when run againsteco.coilysiren.me:3001.get_worldreturns per-datasetHTTP 401warnings and empty results, which is the shape to copy.Refs
ac2da90, #266.The mechanism, from source. Angie (ENG,
claudeseat) fromcoilyco-gaming/sirens-echo. Not claiming — reporting, because I found this while tracing a consumer-side defect and the diagnosis should not be lost.The caller looks correct at a glance, which is why this is worth writing down:
fetch_historynever raises. It catches per action and records instead:So against an unresolvable host every action fails, every failure lands in
history.warnings, andfetch_historyreturns a well-formedProgressionHistorywith an emptyby_specialty. Theexceptin the caller is unreachable.annotate_skills_coveragethen receives an empty iterable:Truewith[]. Exactly the symptom you measured, and it is not a swallowed exception — it is an exception that was never raised.Why that also explains the success path
Your second finding is the same mechanism with a different cause. If the six specialties are missing against a reachable server,
by_specialtywas empty there too — which points at auth or the exporter rather than at the cross-check.history.warningswould say which, and nothing currently reads it.That is a hypothesis, not a measurement. I have not run it against
eco.coilysiren.me:3001.The smallest correct fix
annotate_skills_coverageshould not assert the check ran. The caller already has the evidence:history.warningsnon-empty, or every entry inhistory.per_action_countsabsent — the fetch did not observe the server. SetskillsCrossChecked = Falseand surface the warnings, which is theget_worldper-dataset shape your acceptance already names as the pattern to copy.per_action_counts.setdefault(action, 0)runs only on the success path and carries the comment "Record fetched-but-empty so the UI tells empty from errored" — that distinction already exists in the data and the caller does not use it. That is the cheapest signal available and it was built for exactly this.Why I am not taking it
eco-appis outside my campaign's scope, which namesgaming/sirens-echoandbridge/deploy. I cloned it to check whether a finding of mine had a home here and stopped at the diagnosis.The finding, for context: on
sirens-echoI established that the harness bounds a tool result with a head slice and this server carrieswarningsas its last JSON key, so an oversized response loses its caveats first. That is coilyco-gaming/sirens-echo#449, and #267 is the better fix for it — bounded arrays never reach the cap, so nothing is cut.A tool that reports a check it did not run is the same family: both make an unverified state look verified.
Claiming, reversing my "not taking it" from a few minutes ago. Angie (ENG,
claudeseat), 20 minutes from this comment after the one minute buffer.I declined on scope: my campaign names
gaming/sirens-echoandbridge/deploy. Re-reading it, the brief also says "work on all of the fj issues", this issue carriesheadless— "Safe for autonomous warded engineer carry" — and this repository is Echo's primary data surface. Two live member-facing Echo defects trace here. Declining a fully-diagnosed small fix on a narrow reading of "primarily" is not caution.What I am building
The diagnosis is in my previous comment and I am not re-deriving it. The fix follows from it:
fetch_historyrecords transport failures inhistory.warningsand returns normally, so the caller'sexcept (httpx.HTTPError, OSError)is unreachable andannotate_skills_coverageassertsskillsCrossChecked = Trueagainst a server it never reached.The caller checks whether the fetch observed the server before claiming the cross-check ran.
per_action_countsis populated only on the success path — its own comment says "Record fetched-but-empty so the UI tells empty from errored" — so the signal already exists and is unused.Acceptance I am holding to, from your list
skillsCrossCheckedisfalsewhen the server could not be reached, and the transport warnings are surfaced rather than swallowed.truewith[]. That distinction is the whole point and a fix that fails it would trade one wrong answer for another.What I am not doing
Your second criterion — the six specialties against
eco.coilysiren.me:3001. I said in my diagnosis that an emptyby_specialtyon a reachable server points at auth or the exporter rather than at the cross-check, and I flagged it as a hypothesis. Fixing the falsetruewill make that case report its own cause instead of a clean empty list, which is what turns it into a diagnosable bug rather than a silent one. Whether the six then appear is a separate question and I will say so rather than claim your issue closed.I will report if the fix makes an existing test go red rather than adjusting the test.
Built. PR #271. Claim released. Angie (ENG,
claudeseat).The caller now reads the history instead of relying on an exception that never comes:
per_action_countsis set only where an exporter answered, and its own comment already said why it exists — "Record fetched-but-empty so the UI tells empty from errored." The signal was there and unused.Your acceptance, against what shipped
skillsCrossCheckedfalse when the server could not be reachedget_world's per-dataset warning shapehistory.warningsare appended verbatimskillsInUseNotInGraphlists the six againsteco.coilysiren.me:3001The distinction I was most careful about
Nothing missing is not the same as nothing checked. A fix that returned
falsewhenever the list came back empty would trade one wrong answer for another, so a reachable server with a genuinely empty specialty set still reportstruewith[], pinned by its own test.On your second criterion, which I said I would not claim
Unchanged, and now diagnosable rather than silent. If the six are still absent against a server that answers,
by_specialtywas empty there too — which points at auth or the exporter, not at this flag. After this change that case carrieshistory.warningsexplaining why, instead of a clean empty list that looks like a successful check. That is the difference between a bug you can chase and one you cannot.I did not run against
eco.coilysiren.me:3001. Verifying that half needs the live server and it stays open.Validation
No existing test changed. The three in
test_recipe_tools.pythat exerciseannotate_skills_coveragedirectly are untouched and still pass — the defect was in the caller, not in that function.Mutation: restoring the unconditional
annotate_skills_coveragefailstest_an_unreachable_server_does_not_report_a_cross_checkand nothing else, so the test is load-bearing on exactly this change.Worked from a task-scoped clone, now removed.
Your second criterion passes against the live service right now. Measured, read-only. Angie (ENG,
claudeseat).I said twice that verifying this needed the live server and that I could not do it. I had read-only access to that surface the whole time and did not think to use it. Correcting that.
get_skills(server="eco.coilysiren.me:3001"), live:All six. The exact six from your table, in the order your table lists them, plus a warning naming them and explaining why the graph omits them.
What this means for the issue
Your acceptance had two halves and this is the second:
Satisfied. So once #271 lands for the first half — the unreachable-server case reporting a cross-check it never performed — this issue closes entirely.
One thing I will not smooth over
The response carries
fetchedAtISO: 2026-08-13T00:50:01Z, which predates your 19:47 filing. So this is a cached snapshot, and I cannot tell from here whether the roster read was already working when you measured[]or started working since. Two readings fit:I am not claiming which. What I can say is that the criterion holds against what the service returns today, and that my earlier hypothesis — an empty
by_specialtyon a reachable server pointing at auth or the exporter rather than at the cross-check flag — is consistent with both.If you want the distinction settled, a call with the cache cold would do it, and that is a smaller ask than the one this issue has been carrying.
headlessexist in this repo, one repo-scoped and one org-scoped, so the dispatch gate's vocabulary is ambiguous here #305