Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/system-specs/modules/persistent-agent-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ Closing a channel cancels live agent tasks, broadcasts the close, and removes it

`api_channel_clear_context` resets either one agent session or every channel-agent session. An agent-scope reset preserves shared messages and exchange counts; an all-scope reset also clears both, persists the channel, and broadcasts `channel_context_cleared` so other browser clients discard stale messages.

The handler does not take a per-channel lock. A post concurrent with an all-scope reset can be cleared by the reset, and an in-flight approval future is not cancelled by the handler; it resolves through the agent task after the session reset. This is the current concurrency gap, not a guarantee of serialized channel mutation.
The clear runs under the channel's log lock, which `post` also takes, so a post cannot reach the inbox mid-clear and one already queued refuses its member rather than being wiped unacknowledged. An in-flight approval future is still not cancelled by the handler; it resolves through the agent task after the session reset.

Thread pointers resolve under that same lock, and a message carries `thread_id` and `reply_to` as a PAIR: both set, or neither. `reply_to` is knowable only from the parent, so a reply whose parent is gone by the time the append runs -- an all-scope clear empties the index under this lock -- posts TOP-LEVEL with both fields cleared. Retaining the id there would store a pointer no reader can resolve beside an empty `reply_to`, and dropping the message would lose content its sender was told had been accepted. Pinned by `test_channel_orphan_thread.py`, whose third case asserts the pair is never half-set.

## Security

Expand Down
432 changes: 427 additions & 5 deletions docs/system-specs/modules/session.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/kiro_crew/acp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3565,9 +3565,9 @@ def __init__(
else:
# config.paths is a stdlib-only leaf: importing it here can't
# re-enter the config.loader -> providers.acp -> acp.client cycle.
from kiro_crew.config.paths import config_dir
from kiro_crew.config.paths import default_workspace_dir

self._work_dir = config_dir() / "workspace"
self._work_dir = default_workspace_dir()
# Once-per-instance guard for the ensure_ready work-dir check: True
# after the first (off-loop) mkdir, so the per-prompt warm path pays
# no filesystem syscall at all.
Expand Down
6 changes: 4 additions & 2 deletions src/kiro_crew/acp/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -728,9 +728,9 @@ def __init__(
else:
# config.paths is a stdlib-only leaf: importing it here can't
# re-enter the config.loader -> providers.acp -> acp.client cycle.
from kiro_crew.config.paths import config_dir
from kiro_crew.config.paths import default_workspace_dir

self._work_dir = config_dir() / "workspace"
self._work_dir = default_workspace_dir()
self._agent = agent
# Canonical Kiro Crew agent identity (a cfg.agents key) resolved by the
# surface that created this runtime — a DIFFERENT namespace from
Expand Down Expand Up @@ -3321,6 +3321,7 @@ async def create_session(
runtime=self,
watchdog=_wd,
crew_agent=_crew,
bound_cwd=str(session_work_dir),
)

# Populate state from session/new response (configOptions, available models)
Expand Down Expand Up @@ -3637,6 +3638,7 @@ async def load_session(
runtime=self,
watchdog=_wd,
crew_agent=_crew,
bound_cwd=str(load_params["cwd"]),
)
handle.store_session_config(resp)
# session/load echoes ``currentModelId`` exactly like session/new, and a
Expand Down
4 changes: 4 additions & 0 deletions src/kiro_crew/acp/session_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,14 @@ def __init__(
runtime: AcpRuntimeProtocol,
watchdog: WatchdogSettings | None = None,
crew_agent: str = "",
bound_cwd: str = "",
) -> None:
self._session_id = session_id
self._queue = queue
self._runtime = runtime
# The directory THIS session was opened against, which on a shared runtime is
# not the runtime's own: sessions for different projects live on one process.
self._bound_cwd = bound_cwd
# When True, destroy() skips the transcript unlink (subagent
# continuability: the transcript is spawn_continue's resume material).
self.keep_transcript = False
Expand Down
15 changes: 15 additions & 0 deletions src/kiro_crew/acp/session_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,21 @@ def _work_dir(self) -> Path:
"""Working directory (AcpClient-compatible attribute)."""
return self._runtime._work_dir

@property
def cwd(self) -> str:
"""The directory THIS session is bound to, not the runtime's.

Overrides the ``LLMProvider`` default ("") so reuse validation reads the real
path through the public capability rather than probing a private attribute.
Reads it off the HANDLE: a shared runtime carries sessions opened against
different projects, so answering with the runtime's own directory would report a
workspace this session never bound, and reuse validation would evict a live
session -- losing its conversation -- for failing to be somewhere it never was.
Falls back to the runtime for a handle predating the record, which is the
single-session case where the two agree anyway.
"""
return str(getattr(self._handle, "_bound_cwd", "") or self._work_dir)

@property
def _permission_mode(self) -> str:
"""Permission mode — always empty for kiro (no CC permission modes)."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,12 +650,15 @@ async def _ensure_worker_slot(
return None
try:
slot._app = APP_NAME
# cwd for the worker's CLI process (chat_runner: cwd=slot.project).
# cwd for the worker's CLI process (chat_runner: cwd=slot.claim_cwd).
# Without it the agent must `cd <project>` before every command, which
# turns every tool pill in the chat into identical cd-noise -- and for a
# discovered spec it would edit files outside the project entirely.
if safe_wd is not None:
slot.project = str(safe_wd)
# The MARKER too: `claim_cwd` reads it before the project, so a slot cleared
# earlier would hand the turn the default workspace and misplace every write.
slot.project_cleared = False
# '' = inherit: the session layer's resolution chain applies unchanged.
# A concrete pick rides slot.model, which chat_runner already resolves
# first — and if the pick stops being served, its withhold keeps the pin
Expand Down
Loading
Loading