Skip to content

Commit bd200be

Browse files
Merge pull request #174 from EvanProgramming/codex/issue-168-project-scope
fix: isolate interaction state by project scope
2 parents 2f49056 + 5d1e488 commit bd200be

3 files changed

Lines changed: 43 additions & 5 deletions

File tree

‎main.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,16 +2748,25 @@ def interaction_envelope(user_input: str = "") -> dict[str, Any]:
27482748
return _interaction_controller.envelope(user_input)
27492749

27502750

2751+
def interaction_workspace_id(context: LaunchContext | None = None) -> str:
2752+
"""Keep project interaction state separate without splitting global memory."""
2753+
active = context if context is not None else _launch_context
2754+
if isinstance(active, LaunchContext) and not active.is_global:
2755+
return active.source_scope_id
2756+
return memory_bank.workspace_id
2757+
2758+
27512759
def bind_interaction_scope(session_id: str, *, user_id: str | None = None) -> None:
27522760
"""Bind the shared task and interaction facades to one local surface."""
27532761
global tasks, _interaction_controller
27542762
owner = user_id or memory_bank.user_id
2763+
workspace_id = interaction_workspace_id()
27552764
tasks = TaskManager(
2756-
memory_bank.store, workspace_id=memory_bank.workspace_id,
2765+
memory_bank.store, workspace_id=workspace_id,
27572766
session_id=session_id, user_id=owner,
27582767
)
27592768
_interaction_controller = InteractionController(
2760-
memory_bank.store, user_id=owner, workspace_id=memory_bank.workspace_id,
2769+
memory_bank.store, user_id=owner, workspace_id=workspace_id,
27612770
session_id=session_id,
27622771
)
27632772
_restore_ponytail_level()

‎server.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def _task_manager(session_id: str | None) -> TaskManager:
218218
"""Build a task manager bound to the authenticated deployment scope."""
219219
return TaskManager(
220220
_agent.memory_bank.store,
221-
workspace_id=_agent.memory_bank.workspace_id,
221+
workspace_id=_agent.interaction_workspace_id(),
222222
session_id=session_id,
223223
user_id=_SERVER_ACTOR_ID,
224224
)
@@ -369,7 +369,7 @@ def _get_or_create_session(session_id: str, user_id: str = "anonymous") -> dict:
369369
def _interaction_for_session(session: dict[str, Any]):
370370
return _agent.InteractionController(
371371
_agent.memory_bank.store, user_id=session.get("user_id", _SERVER_ACTOR_ID),
372-
workspace_id=_agent.memory_bank.workspace_id, session_id=session["session_id"],
372+
workspace_id=_agent.interaction_workspace_id(), session_id=session["session_id"],
373373
)
374374

375375

@@ -475,7 +475,7 @@ def _run_session_chat(session: dict[str, Any], message: str) -> str:
475475
_agent.TOOLS_LIST = _agent._build_tools_list()
476476
_agent.short_term_memory = list(session["messages"])
477477
_agent.tasks = TaskManager(
478-
_agent.memory_bank.store, workspace_id=_agent.memory_bank.workspace_id,
478+
_agent.memory_bank.store, workspace_id=_agent.interaction_workspace_id(),
479479
session_id=session_id, user_id=session["user_id"],
480480
)
481481
_agent.tasks.recover()

‎tests/test_interaction.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from task_engine import TaskManager
1717
from capability_tokens import issue_capability_token
18+
from workspace_context import resolve_launch_context
1819

1920

2021
class InteractionTests(unittest.TestCase):
@@ -36,6 +37,34 @@ def test_auto_routing_and_mode_capabilities_are_narrowing_only(self):
3637
self.assertEqual(mode_capabilities(base, "plan"), frozenset({"read", "network"}))
3738
self.assertEqual(mode_capabilities(frozenset({"read"}), "agent"), frozenset({"read"}))
3839

40+
def test_project_interaction_state_is_isolated_from_other_projects(self):
41+
with tempfile.TemporaryDirectory() as home, tempfile.TemporaryDirectory() as first, \
42+
tempfile.TemporaryDirectory() as second:
43+
first_context = resolve_launch_context(home=home, project_path=first)
44+
second_context = resolve_launch_context(home=home, project_path=second)
45+
first_scope = main.interaction_workspace_id(first_context)
46+
second_scope = main.interaction_workspace_id(second_context)
47+
self.assertNotEqual(first_scope, second_scope)
48+
49+
first_controller = InteractionController(
50+
self.store, workspace_id=first_scope, session_id="surface:tui",
51+
)
52+
first_controller.propose_plan({
53+
"title": "First", "summary": "Only the first project.", "assumptions": [],
54+
"steps": [{"id": "one", "title": "One", "description": "Change first.",
55+
"acceptance": ["First changed"]}],
56+
})
57+
second_controller = InteractionController(
58+
self.store, workspace_id=second_scope, session_id="surface:tui",
59+
)
60+
self.assertIsNotNone(first_controller.state()["pending_plan"])
61+
self.assertIsNone(second_controller.state()["pending_plan"])
62+
63+
global_context = resolve_launch_context(home=home, global_mode=True)
64+
self.assertEqual(
65+
main.interaction_workspace_id(global_context), main.memory_bank.workspace_id,
66+
)
67+
3968
def test_question_bounds_and_event_reconstruction(self):
4069
question = self.controller.request_question({"questions": [{
4170
"id": "scope", "header": "Scope", "prompt": "Which package?",

0 commit comments

Comments
 (0)