fix(world-model): scope WorldModel state per-task to prevent cross-task contamination
WorldModel routed all state through a single mutable _current_task_id, so concurrent tasks sharing one instance could corrupt each other's state. start_task() now returns a TaskWorldView handle scoped to that task; TaskRunner.run() threads it through as a local variable instead of reading self.world_model implicitly. Also extracts _start_world_view/ _record_step_result as reusable TaskRunner methods for composed runners. openspec: world-model capability, archived change world-model-runtime
This commit is contained in:
@@ -129,6 +129,52 @@ def test_observe_skips_malformed_fields_without_raising() -> None:
|
||||
assert len(model.state.history) == 2
|
||||
|
||||
|
||||
def test_concurrent_tasks_on_same_world_model_stay_isolated() -> None:
|
||||
model = WorldModel(config=WorldConfig(history_size=5))
|
||||
launch_a = PlannedStep(
|
||||
action="launch_app",
|
||||
description="launch a's app",
|
||||
args={"app_id": "com.example.a"},
|
||||
)
|
||||
launch_b = PlannedStep(
|
||||
action="launch_app",
|
||||
description="launch b's app",
|
||||
args={"app_id": "com.example.b"},
|
||||
)
|
||||
|
||||
task_a = model.start_task("task-a")
|
||||
task_a.observe(_scene(), _semantic_scene("A-Home"), launch_a, _result(launch_a))
|
||||
|
||||
# Task B starts on the SAME WorldModel instance *after* task A has
|
||||
# already observed an event but *before* task A observes its next one --
|
||||
# this is the interleaving that corrupted state when WorldModel tracked
|
||||
# "the current task" via a single shared mutable attribute.
|
||||
task_b = model.start_task("task-b")
|
||||
task_b.observe(_scene(), _semantic_scene("B-Home"), launch_b, _result(launch_b))
|
||||
|
||||
remember_a = PlannedStep(
|
||||
action="tap",
|
||||
description="a remembers",
|
||||
args={"remember": {"contact": "Alice"}},
|
||||
)
|
||||
task_a.observe(_scene(), _semantic_scene("A-Chat"), remember_a, _result(remember_a))
|
||||
|
||||
assert task_a.state.current_app == "com.example.a"
|
||||
assert task_a.state.current_page == "A-Chat"
|
||||
assert task_a.state.variables == {"contact": "Alice"}
|
||||
assert [event.action for event in task_a.state.history] == ["launch_app", "tap"]
|
||||
|
||||
assert task_b.state.current_app == "com.example.b"
|
||||
assert task_b.state.current_page == "B-Home"
|
||||
assert task_b.state.variables == {}
|
||||
assert [event.action for event in task_b.state.history] == ["launch_app"]
|
||||
|
||||
# External readers keep working via state_for().
|
||||
assert model.state_for("task-a") is task_a.state
|
||||
assert model.state_for("task-b") is task_b.state
|
||||
assert model.state_for("task-a") is not model.state_for("task-b")
|
||||
|
||||
|
||||
def test_observe_appends_semantic_or_raw_scene_history_with_eviction() -> None:
|
||||
model = WorldModel(config=WorldConfig(history_size=2))
|
||||
first = PlannedStep(action="first", description="first")
|
||||
|
||||
Reference in New Issue
Block a user