Repository navigation
Converse can be timed out and fall through to an entity match during post-boot GIL contention #1027
Description
What happened
A production hub (11 languages, 18 skills) restarted. Within the first ~60s after "Skills Manager is ready", a session mid-way through a converse-driven game skill (thalovant-skill-twenty-questions, waiting on a yes/no reply) sent "yes". Instead of continuing the game, it matched an unrelated real-world entity ("YES Network", a TV channel) via the m2v-prototype-high pipeline.
Root cause (as far as we can tell from the client side)
IntentService.handle_utterance's pipeline loop (ovos_core/intent_services/service.py) tries each configured pipeline in order through _call_match_bounded, which enforces intents.<pipe_id>.match_timeout (default DEFAULT_MATCH_TIMEOUT = 10). ovos-converse-pipeline-plugin runs first in our pipeline order, ahead of the entity matchers.
Right after a restart, while all of a hub's configured languages are still being trained/compiled in the background (padatious nets, m2v prototypes), the whole process is under heavy GIL contention. We measured individual pipeline match calls taking 5-8.6s of real wall time during this window on otherwise-trivial requests (a plain date-time lookup). Converse's own single match call is future.result(timeout=10); if real wall time for that one call exceeds 10s during this window, _call_match_bounded raises FutureTimeoutError, logs a WARNING, and returns None -- a correct, by-design decline per OVOS-PIPELINE-1 §4.4. The loop then falls through to the next pipeline (m2v-prototype-high), which matches a literal entity instead.
So this isn't a bug in the bounded-match mechanism itself -- it does exactly what its docstring says. It's that a generic, uniform match_timeout (same default for every pipeline) is least appropriate for converse: a stateful, already-in-progress interaction, where declining too early produces a worse outcome (a jarring wrong answer) than a late answer would. Every other pipeline in the chain is a fresh, stateless match where a quick decline-and-fallthrough is the right trade-off.
What we did on our side (not a request to copy it, just data)
intents.<pipe_id>.match_timeout is already overridable per pipeline, so we raised ovos-converse-pipeline-plugin's to 25s on our fleet as a mitigation. This is a config change on our end, not an ovos-core change, and we're reporting it rather than treating it as the real fix.
Ask
Two independent things, either of which would help:
- Consider a boot-aware grace period, or a lower bound that increases while any pipeline is still "warming" (there's already a natural signal for this per-plugin, e.g. padatious's own "every language compiled" event) -- so a generic
match_timeoutisn't the only knob available during the riskiest window. - Consider whether the converse pipeline specifically should get a longer default timeout, or whether a declined converse match should suppress/down-rank (rather than fully enable) the immediately-following literal-entity pipelines for that same utterance, since "wrong answer to an active game" is a worse failure mode than "slightly slow answer".
We don't have a concrete PR here -- this felt like a design trade-off worth maintainer judgment rather than something to patch blind. Happy to provide the measured timings/logs if useful.
Activity
JarbasAl commentedon Oct 13, 2026
increasing converse default timeout is acceptable, but in the end OVOS shouldnt be used before
mycroft.readysignal, perhaps we need to revisit this and block any incoming utterance with a default error dialog.May be worth PR'ing this to the architecture repo if you have any suggestion, then we officially adopt a new namespace (
ovos.ready?) and make this a requirement for spec compliancegoldyfruit commentedon Oct 13, 2026
I agree but I think while its loading, we should be able to handle the intents and redirect them to "I'm still starting, keep waiting few more seconds" (something like that).
goldyfruit commentedon Oct 13, 2026
Posted both follow-ups:
ovos.readynamespace and the one requirement that an utterance arriving before the orchestrator is ready must not enter the pipeline loop, per the decline-with-dialog design described above.handle_utterancechecks the existingProcessStatus/ProcessState.READYsignal right after the malformed-carrier guard; while unready it speaks a short localized "still starting" dialog and still emits the one terminal event owed, instead of running transformers/disambiguation/the pipeline loop at all.Measuring the before/after on a production hub now and will post the numbers here.
goldyfruit commentedon Oct 13, 2026
Correction to my own #1028: it gates on the wrong signal and does not fix the bug this issue describes. Posting the exact timeline from a saved log of the run the "YES Network" example came from (an 11-language hub):
"every language compiled" never appears in this window -- the background compile had not finished when "yes" arrived, ~20s after BOTH readiness signals were already true. A second run against the same hub (same "compiling in background" lines at 14:05:22-14:05:40) passed cleanly only because the test harness I used held the probe until padatious logged "every language compiled".
So:
IntentService.status.check_ready()(what #1028 gates on) is true for the entire window this bug lives in. #1028 cannot fix it as written.The real signal is padatious's own internal state: it already knows the difference between "compiling in background, serving last compiled state" and "every language compiled" (
self._reported_all_compiledinopm.py), it just isn't on the bus. Reworking #1028 and the architecture#285 proposal to define "ready" as pipelines warm -- each plugin that has one reports its own warm/cold transition, the orchestrator aggregates "every configured pipeline warm" -- rather than process status. A plugin with no such signal counts as warm by default, so this doesn't block on something that can't report.One more thing worth stating in the spec: "serving last compiled state in the meantime" on a freshly-started process is serving a stale state (there is no earlier compiled state to serve), which is why the fallthrough lands on a wrong literal entity rather than just being slow -- an unready pipeline should decline, not serve partial/stale state.
Reworked PRs incoming.
JarbasAl commentedon Oct 13, 2026
then this seems like a third bug, we should delay ProcessState.READY until pipelines are fully ready to serve requests
spec(ready-1): define ready as pipelines warm, not process status; fi…
fix: gate on the warm aggregate, not process status, and resolve lang…
goldyfruit commentedon Oct 13, 2026
skill_manager.py has its own ProcessStatus('skills', ...) and reads
self.status.state == ProcessState.READYfor its own "done loading skills" check. It is a different component from the intents one, but it is the same mechanism, andmycroft.readyis built from several of these per-component statuses.I'd keep
ovos.ready(the pipelines-warm aggregate) separate fromProcessState.READY/mycroft.ready, and not make the intents component's own READY wait on it. READY today means "this component finished its own bootstrap", and other deployments may depend on that narrower meaning. Folding pipeline warmth into it would change behavior for every existing consumer ofmycroft.ready, not just the utterance lifecycle.So the question for you: are you fine with that change, or should
ovos.readystay additive and exist only for this gate? If you'd rather delay READY itself, OVOS-READY-1 needs a compatibility note for the current consumers.feat: a pipeline's own warm/cold transition on the bus (OVOS-READY-1)
JarbasAl commentedon Oct 13, 2026
I'd rather explain it to my claude directly ;)
goldyfruit commentedon Oct 13, 2026
Sure, it will be easier!
goldyfruit commentedon Oct 13, 2026
The gap my last canary showed is closed in #1028 (eb18e47, plus a follow-up). Ready to answer now also needs the skill manager's first load: the gate waits on
mycroft.skills.initialized, latched, so a skill installed or reloaded later cannot turn a ready system back into a not-ready one. Before, on the same hub, a bare "yes" after "let's play twenty questions" misrouted from 87 to 110 s after container start (89 to 120 s on another restart), because padatious was warm at about 80 s while skills kept loading until about 125 s. With the change I restarted it three times, once with extra load probes, and probed every 3 s. There were 0 misroutes in all three. "Still starting" ended at 138, 156 and 141 s, and every answer after that was the twenty-questions skill's own. The spec side is in OpenVoiceOS/architecture#285.goldyfruit commentedon Oct 14, 2026
The design in #1028 now follows the model discussed here:
IntentService.statusreaches READY only once no pipeline is loading (pipelines are loading or loaded, no warm/cold), and an utterance gets the "still starting" reply until the service is READY, the first skill load is done and no pipeline is loading. One open question for you is on that PR: whether the skills condition should live in the skills status instead. OpenVoiceOS/architecture#285 is aligned.JarbasAl commentedon Oct 14, 2026
OpenVoiceOS/ovos-workshop#714 this regression was probably a factor in your initial timeouts