Skip to content

fix: context writes go to the session, never the bus; turns_remaining on set_context - #605

Closed
JarbasAl wants to merge 1 commit into
devfrom
fix/context-1-no-bus-mutation-turns-remaining
Closed

JarbasAl wants to merge 1 commit into
devfrom
fix/context-1-no-bus-mutation-turns-remaining

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Sep 7, 2026

Copy link
Copy Markdown
Member

🤖 Auto-generated by Claude Fable 5.1 (claude-fable-5-1) via Claude Code — NOT human-reviewed. Verify before acting.

Governing clauses (OVOS-CONTEXT-1, architecture repo, intent-context.md):

§5.0 The session is the only context write path. ... There is no context-mutation topic: no participant emits a Message whose purpose is to announce a context change to the orchestrator or to another component.

§1.2 ... a skill records that the conversation is in some state ... The classic illustration is a confirmation branch: the top-level intent asks "do you want milk with that?" and sets a confirming_milk flag ... { "value": null, "turns_remaining": 1 }

§3 Scope is encoded in the key shape: shared<key> — bare, no :.

set_cross_skill_context/remove_cross_skill_context in ovos_workshop/skills/ovos.py emitted mycroft.skill.set_cross_context/remove_cross_context as their only mutation. Every receiving skill's own handle_set_cross_context listener then called self.set_context(...), writing the entry into that skill's private adapt context - a bus topic mutating another component's context, which §5.0 rules out, and not even the shared scope §3 defines. Under the CONTEXT-1 session model this fan-out is also unnecessary: a shared entry is a bare key in session.intent_context, already visible to every intent that reads the same session. Both methods now write the shared entry directly into the session bound to the current dispatch message (the same Session.set_intent_context path the private set_context already uses), keeping the bus broadcast only as a deprecated compat mirror for orchestrators that predate the spec - tagged with a computed removal version (VERSION_MAJOR + 1, the same pattern common_play.py's _OCP_VOC_MATCH_REMOVAL_VERSION already uses) instead of a hand-written placeholder.

gh search code --owner OpenVoiceOS "set_cross_skill_context" turned up no skill callers org-wide - only the technical-manual docs, the message-spec table, and the method's own definition/tests in this repo. Fixing the implementation in place changes no caller's public API.

Separately, set_context (and the _AdaptIntentApi.set_context it delegates to) computed expires_at from context.timeout config and never exposed turns_remaining, even though Session.set_intent_context already supports it. That made §1.2's flagship one-turn confirmation gate unreachable from the skill API. set_context and set_cross_skill_context now take optional turns_remaining/expires_at keyword arguments; both default to None, so existing callers are unaffected.

test/end2end/test_context1_cross_skill_e2e.py boots a real MiniCroft (ovoscope) with two skills: context1_setter_test_skill calls the real set_cross_skill_context while handling one template intent, context1_reader_test_skill registers a different template intent gated by requires_context on the same bare shared key. The test drops the legacy mycroft.skill.set_cross_context broadcast before it reaches the bus for its whole duration, so the reader's gate can only open via the session write under test, and reads the entry back from the terminal ovos.utterance.handled message's own session rather than a private local reference. Adapt is excluded from this run's pipeline in favor of padacioso: an unrelated, pre-existing ovos-core bug (IntentManifest.get_slot_names treats the INTENT-4 §5.2 required/optional descriptor dicts as bare slot names) crashes the §7 context-supplied-slot step for any adapt intent combining keyword requirements with requires_context, independently of this PR.

Also fixed: three new OVOSSkill instances in the unit tests now call default_shutdown() before going out of scope, and TestApp.setUp() in test_abstract_app.py flushes with gc.collect() before every test - both needed because test_default_shutdown's single-method @patch window was picking up an unrelated, earlier test's cyclic garbage once this PR's new tests shifted the allocation count enough to move a GC sweep into that window.

Fail-before (source reverted to the pre-PR baseline, keeping the tests): the three unit tests fail - two with TypeError: unexpected keyword argument 'turns_remaining', one with an AssertionError on the missing shared session entry; the end2end test fails with AssertionError: shared 'person' entry never landed in session.intent_context (the legacy broadcast alone, which still fires, does not satisfy the gate). Restoring the fix makes all four pass. Full unit suite: 703 passed, 0 regressed (run three times in default order). End2end suite: 14 passed, 0 regressed.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

I've completed my sweep! Here's the situation. 🧹

I've aggregated the results of the automated checks for this PR below.

📋 Repo Health

How's the repo's pulse? Let's take a look. 💓

✅ All required files present.

Latest Version: 9.7.0a2

ovos_workshop/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
⚠️ setup.py — setup.py
CHANGELOG.md — Changelog
ovos_workshop/version.py has valid version block markers

🔍 Lint

Evaluating the overall impact of your changes. 📈

ruff: issues found — see job log

📡 Channel Compat — stable

Here's the latest update from the automation pipeline. 🏗️

🚧 Channel unresolvable with this checkout — the repo's dependency floors exceed what the channel pins (fleet finding; see the install log).

Constraints: https://raw.githubusercontent.com/OpenVoiceOS/OpenVoiceOS/main/constraints-stable.txt
Resolved stack: channel-compat-stable artifact (constraints + pip freeze).

📡 Channel Compat — testing

The results have been compiled and are ready for review. 📑

🚧 Channel unresolvable with this checkout — the repo's dependency floors exceed what the channel pins (fleet finding; see the install log).

Constraints: https://raw.githubusercontent.com/OpenVoiceOS/OpenVoiceOS/main/constraints-testing.txt
Resolved stack: channel-compat-testing artifact (constraints + pip freeze).

⚖️ License Check

Ensuring our EULA (if any) is still valid. 📑

✅ No license violations found.

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

🔒 Security (pip-audit)

Security report: No threats detected in the area. ✅

✅ No known vulnerabilities found (72 packages scanned).

🔨 Build Tests

The build bots have finished their assembly. 🤖

Python Build Install Tests
3.10 ⚠️
3.11 ⚠️
3.12 ⚠️
3.13 ⚠️
3.14 ⚠️

❌ 3.10: Install OK, tests failed
❌ 3.11: Install OK, tests failed
❌ 3.12: Install OK, tests failed
❌ 3.13: Install OK, tests failed
❌ 3.14: Install OK, tests failed
Check job logs for details.


May your merges be conflict-free! 🕊️

@github-actions github-actions Bot added the fix label Sep 7, 2026
@JarbasAl
JarbasAl force-pushed the fix/context-1-no-bus-mutation-turns-remaining branch from 70aaf29 to fc03a34 Compare September 7, 2026 19:17
… on set_context (OVOS-CONTEXT-1 §5.0, §1.2)

set_cross_skill_context/remove_cross_skill_context emitted a bare bus
message (mycroft.skill.set_cross_context/remove_cross_context) as the
only mutation - every receiving skill's handle_set_cross_context then
wrote the entry into its OWN private adapt context, which is neither
CONTEXT-1-conformant (§5.0 forbids a mutation topic distinct from the
session it modifies) nor actually shared (§3's shared scope is a bare
key, visible to every intent through the one session all pipeline
stages read - no fan-out required). Both methods now write the shared
entry directly into the session bound to the current dispatch message
(same session.set_intent_context path set_context already uses for
private entries) and keep the legacy bus broadcast only as a
deprecated compat mirror for pre-spec orchestrators, tagged with a
computed removal version (VERSION_MAJOR + 1, the pattern already used
by common_play.py's _OCP_VOC_MATCH_REMOVAL_VERSION) instead of a
hand-written placeholder.

set_context (skill API) and the underlying _AdaptIntentApi.set_context
computed expires_at from config and never exposed turns_remaining, so
the §1.2 flagship one-turn confirmation gate
({"value": ..., "turns_remaining": 1}) was unreachable from the skill
API even though Session.set_intent_context already supports it.
set_context/set_cross_skill_context now take optional
turns_remaining/expires_at kwargs, defaulting to None (unchanged
behaviour).

gh search code --owner OpenVoiceOS "set_cross_skill_context" found no
skill callers org-wide, only docs/spec references and the method's
own definition - fixing the implementation in place changes no
caller's public API.

Three new OVOSSkill instances created by the new unit tests (as locals
that go out of scope) explicitly call default_shutdown() before
returning, so the idempotency guard is set before GC ever drops the
last reference - the __del__ that eventually fires is a no-op instead
of an unbounded-delay call into whatever @patch on
OVOSSkill.default_shutdown another test has active at the time.
test_abstract_app.py's test_default_shutdown was exposed to exactly
that: an OVOSSkill instance from an EARLIER, unrelated test can sit as
cyclic garbage (skill <-> bus event-handler reference cycle, only
collectable by the generational GC, not refcounting) until whichever
later test happens to trigger a GC pass; adding this PR's tests
shifted the allocation count enough that the sweep started landing
inside test_default_shutdown's single-method @patch window, inflating
its call count. TestApp.setUp() now flushes with gc.collect() before
every test in that class, ahead of any patch, so that class no longer
depends on incidental cross-test GC timing.

test/end2end/test_context1_cross_skill_e2e.py boots a real MiniCroft
(ovoscope) with two skills: context1_setter_test_skill calls the real
set_cross_skill_context while handling one template intent,
context1_reader_test_skill registers a different template intent
gated by requires_context on the same bare shared key. The test drops
the legacy mycroft.skill.set_cross_context broadcast before it reaches
the bus for its whole duration, so the reader's gate can only open via
the session write under test, and reads the entry back from the
terminal ovos.utterance.handled message's own session rather than a
private local reference. Adapt is excluded from this run's pipeline in
favor of padacioso: an unrelated, pre-existing ovos-core bug
(IntentManifest.get_slot_names treats the INTENT-4 §5.2
required/optional descriptor dicts as bare slot names) crashes the §7
context-supplied-slot step for any adapt intent combining keyword
requirements with requires_context, independently of this PR.

Fail-before (source revert to the pre-PR baseline, keeping the tests):
the three unit tests fail - two with TypeError: unexpected keyword
argument 'turns_remaining', one with AssertionError on the missing
shared session entry; the end2end test fails with AssertionError:
shared 'person' entry never landed in session.intent_context (the
legacy broadcast alone, which still fires, does not satisfy the gate).
Restoring the fix makes all four pass. Full suite in default order,
run three times: 703 passed each time, 0 regressed. End2end suite: 14
passed (including the new test), 0 regressed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant