cloudagent-daemon 0.5.4__tar.gz → 0.5.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/CHANGELOG.md +23 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/PKG-INFO +1 -1
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/pyproject.toml +1 -1
- cloudagent_daemon-0.5.5/src/cloudagent_daemon/__init__.py +1 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/claude_runner.py +75 -51
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_claude_runner.py +121 -3
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/uv.lock +1 -1
- cloudagent_daemon-0.5.4/src/cloudagent_daemon/__init__.py +0 -1
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/.gitignore +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/LICENSE +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/README.md +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/scripts/release.sh +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/__main__.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/agent_download.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/agent_upload.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/cli.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/config.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/context_fetch.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/delegation_report.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/doctor.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/http_outbound.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/injection_state.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/login.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/main_loop.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/mcp_bridge.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/metrics.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/metrics_server.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/paths.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/protocol.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/runtime_config.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/service.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/sse_client.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/transport.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/unix_socket.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/workspace.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/ws_client.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/__init__.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/_fake_sse_server.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/conftest.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_e2e_sse.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_full_e2e.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_mcp_bridge_round_trip.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_one_shot_cli.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_spawn_mcp_bridge_e2e.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_sse_handshake.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_sse_watchdog.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_ws_handshake.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_ws_reconnect.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/conftest.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_agent_download.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_agent_upload.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_claude_runner_codex_contract.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_claude_runner_routing_contract.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_claude_runner_w3.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_cli_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_cli_smoke.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_config.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_context_fetch.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_delegation_report.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_doctor.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_http_outbound.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_injection_state.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_login.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_main_loop_assign.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_main_loop_preflight.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_mcp_bridge_agent_retrieval.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_mcp_bridge_send_message.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_metrics.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_metrics_server.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_multi_tool_wake.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_packaging_mcp_pin.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_paths.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_protocol.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_service.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_service_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_sse_active_watchdog.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_start_guard.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_status_stop.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_workspace.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_workspace_root_task113.py +0 -0
- {cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_ws_client_insecure.py +0 -0
|
@@ -4,6 +4,29 @@ All notable changes to the local daemon. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); versions are
|
|
5
5
|
[PEP 440](https://peps.python.org/pep-0440/). Release with `scripts/release.sh`.
|
|
6
6
|
|
|
7
|
+
## [0.5.5] — 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Every engine now gets one runtime session per conversation, not one per agent.
|
|
11
|
+
An agent resumed the same CLI session no matter which channel woke it, so
|
|
12
|
+
#debug, the DM and #SSS all shared a single context: the per-channel prompt
|
|
13
|
+
leaked between rooms, and each wake replayed the union of every room's
|
|
14
|
+
history — muddled context and a token bill that grew without bound. Lanes
|
|
15
|
+
already fixed this for qoder; the bleed was never qoder-specific, so
|
|
16
|
+
claude_code and codex now resume from the same per-conversation lane, keyed
|
|
17
|
+
on the waking conversation (channel, DM and thread each carry their own).
|
|
18
|
+
Wakes with no conversation — timers, spawn chains — keep the global lane.
|
|
19
|
+
|
|
20
|
+
### Notes
|
|
21
|
+
- The lane file is now `.meta/sessions.json`. `qoder-sessions.json` is still
|
|
22
|
+
read so an upgraded host keeps its lanes; the first write folds it into the
|
|
23
|
+
new file and removes it, or a cleared lane would come back to life from the
|
|
24
|
+
stale value left behind.
|
|
25
|
+
- Agents carrying a global session into this version start fresh on their first
|
|
26
|
+
wake in each conversation. That is the fix rather than a side effect —
|
|
27
|
+
inheriting it is precisely the bleed being removed — but expect an agent to
|
|
28
|
+
look like it "forgot" an in-flight thread once, right after the upgrade.
|
|
29
|
+
|
|
7
30
|
## [0.5.4] — 2026-08-24
|
|
8
31
|
|
|
9
32
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudagent-daemon
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.5
|
|
4
4
|
Summary: Local daemon for cloudagent — bridges Claude Code, Codex, and Qoder CLI agents to the cloudagent backend.
|
|
5
5
|
Project-URL: Homepage, https://cloudagent.chat
|
|
6
6
|
Project-URL: Repository, https://codeup.aliyun.com/69bd0a4d29ad98af4065ce71/cloudagent
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloudagent-daemon"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.5"
|
|
8
8
|
description = "Local daemon for cloudagent — bridges Claude Code, Codex, and Qoder CLI agents to the cloudagent backend."
|
|
9
9
|
authors = [{name = "chaoyin", email = "tudoucyx@gmail.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.5"
|
|
@@ -680,11 +680,10 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
680
680
|
stale_conv = (
|
|
681
681
|
str(payload["conversation_id"]) if payload.get("conversation_id") else None
|
|
682
682
|
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
)
|
|
683
|
+
# #95 — the lane IS the session this wake resumed from (the
|
|
684
|
+
# global last_session only when there is no conversation), so
|
|
685
|
+
# it is also the one to drop once it proves unusable.
|
|
686
|
+
stale_lane_sid = self._lane_session(ws, stale_conv)
|
|
688
687
|
# #114 — 上下文撑满与 stale-session 是同一类问题:**当前会话已经
|
|
689
688
|
# 不能再用了**,区别只在于一个是引擎不认识它、另一个是它太大。补救
|
|
690
689
|
# 动作完全相同(清掉会话、免 resume 重跑一次),所以走同一条路径,
|
|
@@ -701,12 +700,12 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
701
700
|
)
|
|
702
701
|
or context_exhausted
|
|
703
702
|
)
|
|
704
|
-
and
|
|
703
|
+
and stale_lane_sid is not None
|
|
705
704
|
):
|
|
706
|
-
stale_sid =
|
|
707
|
-
# #95 —
|
|
708
|
-
if
|
|
709
|
-
self.
|
|
705
|
+
stale_sid = stale_lane_sid
|
|
706
|
+
# #95 — 过期会话就在 lane 里,清 lane 才能真正脱离它
|
|
707
|
+
if stale_conv:
|
|
708
|
+
self._set_lane(ws, stale_conv, None)
|
|
710
709
|
logger.warning(
|
|
711
710
|
"claude_runner: dropping unusable session_id=%s "
|
|
712
711
|
"(cause=%s, detail=%r). Clearing and retrying "
|
|
@@ -782,21 +781,20 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
782
781
|
# 防外人注入的上下文渗进 owner 后续 full 会话。仍上报 session_start
|
|
783
782
|
# 供实时展示,但不落 last-spawn(下次 full 唤醒仍 resume 干净的 owner 轨)。
|
|
784
783
|
is_read_analyze = self._run_mode_for(payload) == "read_analyze"
|
|
785
|
-
# #95 —
|
|
786
|
-
# (last-spawn.json 可观测性 +
|
|
787
|
-
#
|
|
784
|
+
# #95 — 所有引擎都按 conversation_id 分轨存会话;全局 last_session 仍写
|
|
785
|
+
# (last-spawn.json 可观测性 + 非平台唤醒兜底),但平台唤醒的 resume
|
|
786
|
+
# 只读 lane(见 _build_cmd),不会被别的频道的会话覆盖。
|
|
788
787
|
conv_for_lane = (
|
|
789
788
|
str(payload["conversation_id"]) if payload.get("conversation_id") else None
|
|
790
789
|
)
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
):
|
|
797
|
-
self._qoder_set_lane(ws, conv_for_lane, captured_session)
|
|
798
|
-
if captured_session and captured_session != ws.last_session_id():
|
|
790
|
+
# 比较基准取**本轨**上一个会话,不是全局 last_session:分轨后全局会被
|
|
791
|
+
# 最后说话的频道盖掉,拿它比会让 A/B 频道交替唤醒每轮都像是新会话,
|
|
792
|
+
# 把 session_start 刷成噪音。必须在 _set_lane 之前读。
|
|
793
|
+
prev_lane_session = self._lane_session(ws, conv_for_lane)
|
|
794
|
+
if captured_session and captured_session != prev_lane_session:
|
|
799
795
|
if not is_read_analyze:
|
|
796
|
+
if conv_for_lane:
|
|
797
|
+
self._set_lane(ws, conv_for_lane, captured_session)
|
|
800
798
|
ws.set_last_session_id(captured_session)
|
|
801
799
|
await self.outbound.send_agent_session_start(
|
|
802
800
|
agent_id, captured_session
|
|
@@ -2098,53 +2096,79 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2098
2096
|
)
|
|
2099
2097
|
|
|
2100
2098
|
# ------------------------------------------------------------------
|
|
2101
|
-
# #95 —
|
|
2099
|
+
# #95 — runtime-session lanes, one per conversation.
|
|
2102
2100
|
#
|
|
2103
|
-
#
|
|
2104
|
-
#
|
|
2105
|
-
#
|
|
2106
|
-
#
|
|
2107
|
-
#
|
|
2101
|
+
# A single global last_session shared by DM/#debug/#SSS made every room
|
|
2102
|
+
# bleed into every other one: the agent resumed the same CLI session no
|
|
2103
|
+
# matter which channel woke it, so #89's per-channel prompt leaked
|
|
2104
|
+
# across rooms and each wake replayed the union of all rooms' history
|
|
2105
|
+
# (context noise + token burn). Each conversation therefore gets its own
|
|
2106
|
+
# lane: ``.meta/sessions.json`` = {conversation_id: session_id}.
|
|
2108
2107
|
# Non-platform wakes (no conversation_id) keep the legacy global lane.
|
|
2108
|
+
#
|
|
2109
|
+
# Lanes started out qoder-only (qodercli ``--resume`` additionally
|
|
2110
|
+
# refuses to adopt a new ``--append-system-prompt``), but the bleed is
|
|
2111
|
+
# engine-independent — claude_code and codex resumed one global session
|
|
2112
|
+
# too. All engines now share this path; the superseded qoder file is
|
|
2113
|
+
# still read so an upgraded host keeps its existing lanes.
|
|
2109
2114
|
# ------------------------------------------------------------------
|
|
2110
2115
|
|
|
2116
|
+
#: Superseded lane file — read-only, for hosts upgraded from the
|
|
2117
|
+
#: qoder-only era. Writes always land in ``sessions.json``.
|
|
2118
|
+
LEGACY_QODER_LANES_FILENAME = "qoder-sessions.json"
|
|
2119
|
+
|
|
2111
2120
|
@staticmethod
|
|
2112
|
-
def
|
|
2113
|
-
return ws.path / ".meta" / "
|
|
2121
|
+
def _lanes_path(ws: AgentWorkspaceLike) -> "Path":
|
|
2122
|
+
return ws.path / ".meta" / "sessions.json"
|
|
2123
|
+
|
|
2124
|
+
@classmethod
|
|
2125
|
+
def _read_lanes(cls, ws: AgentWorkspaceLike) -> dict:
|
|
2126
|
+
"""Current lane file layered over the superseded qoder one."""
|
|
2127
|
+
merged: dict = {}
|
|
2128
|
+
for path in (
|
|
2129
|
+
ws.path / ".meta" / cls.LEGACY_QODER_LANES_FILENAME,
|
|
2130
|
+
cls._lanes_path(ws),
|
|
2131
|
+
):
|
|
2132
|
+
try:
|
|
2133
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
2134
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
2135
|
+
continue
|
|
2136
|
+
if isinstance(data, dict):
|
|
2137
|
+
merged.update(data)
|
|
2138
|
+
return merged
|
|
2114
2139
|
|
|
2115
|
-
def
|
|
2140
|
+
def _lane_session(
|
|
2116
2141
|
self, ws: AgentWorkspaceLike, conversation_id: str | None
|
|
2117
2142
|
) -> str | None:
|
|
2118
2143
|
if not conversation_id:
|
|
2119
2144
|
return ws.last_session_id()
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
sid = data.get(conversation_id) if isinstance(data, dict) else None
|
|
2145
|
+
# Deliberately NO fallback to the global last_session: the first
|
|
2146
|
+
# wake in a conversation must start fresh, or it would inherit
|
|
2147
|
+
# whatever room spoke last — the exact bleed lanes exist to remove.
|
|
2148
|
+
sid = self._read_lanes(ws).get(conversation_id)
|
|
2125
2149
|
return sid if isinstance(sid, str) and sid else None
|
|
2126
2150
|
|
|
2127
|
-
def
|
|
2151
|
+
def _set_lane(
|
|
2128
2152
|
self, ws: AgentWorkspaceLike, conversation_id: str, session_id: str | None
|
|
2129
2153
|
) -> None:
|
|
2130
2154
|
"""Set (or clear with None) the lane. Best-effort; never raises."""
|
|
2131
|
-
p = self.
|
|
2155
|
+
p = self._lanes_path(ws)
|
|
2156
|
+
legacy = ws.path / ".meta" / self.LEGACY_QODER_LANES_FILENAME
|
|
2132
2157
|
try:
|
|
2133
|
-
|
|
2134
|
-
data = json.loads(p.read_text(encoding="utf-8"))
|
|
2135
|
-
if not isinstance(data, dict):
|
|
2136
|
-
data = {}
|
|
2137
|
-
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
2138
|
-
data = {}
|
|
2158
|
+
data = self._read_lanes(ws)
|
|
2139
2159
|
if session_id:
|
|
2140
2160
|
data[conversation_id] = session_id
|
|
2141
2161
|
else:
|
|
2142
2162
|
data.pop(conversation_id, None)
|
|
2143
2163
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
2144
2164
|
p.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
2165
|
+
# The merge above already carried every legacy entry across, so
|
|
2166
|
+
# drop the old file — otherwise a *cleared* lane would come back
|
|
2167
|
+
# to life on the next read from the stale value left behind.
|
|
2168
|
+
legacy.unlink(missing_ok=True)
|
|
2145
2169
|
except OSError:
|
|
2146
2170
|
logger.warning(
|
|
2147
|
-
"claude_runner:
|
|
2171
|
+
"claude_runner: lane write failed conv=%s", conversation_id,
|
|
2148
2172
|
exc_info=True,
|
|
2149
2173
|
)
|
|
2150
2174
|
|
|
@@ -2214,8 +2238,12 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2214
2238
|
故此模式下 ``last_session`` 恒为 None。
|
|
2215
2239
|
"""
|
|
2216
2240
|
read_analyze = run_mode == "read_analyze"
|
|
2241
|
+
# #95 — 每个 conversation 一条轨(无 conversation_id 的非平台唤醒落回
|
|
2242
|
+
# 全局轨)。原先这里直接读 ws.last_session_id(),于是同一个 agent 无论
|
|
2243
|
+
# 在哪个频道被唤醒都 --resume 同一条会话。
|
|
2217
2244
|
last_session = (
|
|
2218
|
-
|
|
2245
|
+
self._lane_session(ws, conversation_id)
|
|
2246
|
+
if (with_resume and not read_analyze) else None
|
|
2219
2247
|
)
|
|
2220
2248
|
if runtime_engine == "codex":
|
|
2221
2249
|
binary = self.runtime_binaries.get("codex") or "codex"
|
|
@@ -2290,14 +2318,10 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2290
2318
|
# 输出走既有 stream_json 消费器(联调时如发现事件形状偏差再适配)。
|
|
2291
2319
|
# 认证:QODER_PERSONAL_ACCESS_TOKEN 环境变量或 qodercli login。
|
|
2292
2320
|
binary = self.runtime_binaries.get("qoder") or "qodercli"
|
|
2293
|
-
# #95 —
|
|
2294
|
-
# 全局 last_session;无 conversation_id 的非平台唤醒沿用全局轨。
|
|
2321
|
+
# #95 — 分轨已上提到 last_session(所有引擎共用),这里直接复用。
|
|
2295
2322
|
# #107 — read_analyze 与 claude/codex 一样绝不 resume owner 轨,
|
|
2296
2323
|
# 防外部指挥上下文渗进后续 full 会话。
|
|
2297
|
-
lane_session =
|
|
2298
|
-
self._qoder_lane_session(ws, conversation_id)
|
|
2299
|
-
if with_resume and not read_analyze else None
|
|
2300
|
-
)
|
|
2324
|
+
lane_session = last_session
|
|
2301
2325
|
cmd = [binary]
|
|
2302
2326
|
# r6 同款:模型 + 推理强度由 chairman 在编辑页选(qoder 档位
|
|
2303
2327
|
# low|medium|high|xhigh|max,与 claude_code 同集)。
|
|
@@ -3293,14 +3293,18 @@ async def test_qoder_sessions_are_isolated_per_conversation(
|
|
|
3293
3293
|
a3 = _read_argv_n(tmp_path, 3)
|
|
3294
3294
|
assert a3[a3.index("--resume") + 1] == "sess_dm"
|
|
3295
3295
|
|
|
3296
|
-
lanes = json.loads((tmp_path / ".meta" / "
|
|
3296
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3297
3297
|
assert lanes == {"conv_debug": "sess_debug", "conv_dm": "sess_dm"}
|
|
3298
3298
|
|
|
3299
3299
|
|
|
3300
3300
|
async def test_qoder_stale_resume_clears_only_that_lane(
|
|
3301
3301
|
stateful_fake_claude, tmp_path
|
|
3302
3302
|
):
|
|
3303
|
-
"""lane 里的过期会话被 CLI 拒绝 → 清该 lane 并免 resume 重试;别的 lane 不动。
|
|
3303
|
+
"""lane 里的过期会话被 CLI 拒绝 → 清该 lane 并免 resume 重试;别的 lane 不动。
|
|
3304
|
+
|
|
3305
|
+
seed 故意写在**旧** ``qoder-sessions.json`` 上,于是这条同时验收升级路径:
|
|
3306
|
+
旧 lane 仍可 resume,首次写入把两条都搬进 ``sessions.json`` 并删掉旧文件。
|
|
3307
|
+
"""
|
|
3304
3308
|
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3305
3309
|
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3306
3310
|
(tmp_path / ".meta").mkdir(parents=True, exist_ok=True)
|
|
@@ -3319,8 +3323,122 @@ async def test_qoder_stale_resume_clears_only_that_lane(
|
|
|
3319
3323
|
a0 = _read_argv_n(tmp_path, 0); a1 = _read_argv_n(tmp_path, 1)
|
|
3320
3324
|
assert a0[a0.index("--resume") + 1] == "old_A"
|
|
3321
3325
|
assert "--resume" not in a1
|
|
3322
|
-
lanes = json.loads((tmp_path / ".meta" / "
|
|
3326
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3323
3327
|
assert lanes == {"conv_A": "new_A", "conv_B": "keep_B"}
|
|
3328
|
+
# 迁移后旧文件必须消失,否则被清掉的 lane 会从残留值里复活
|
|
3329
|
+
assert not (tmp_path / ".meta" / "qoder-sessions.json").exists()
|
|
3330
|
+
|
|
3331
|
+
|
|
3332
|
+
# ---------------------------------------------------------------------------
|
|
3333
|
+
# #95 — lanes are engine-independent: claude_code / codex get them too
|
|
3334
|
+
# ---------------------------------------------------------------------------
|
|
3335
|
+
|
|
3336
|
+
|
|
3337
|
+
def _claude_ok(sid: str) -> dict:
|
|
3338
|
+
return {
|
|
3339
|
+
"stdout": [
|
|
3340
|
+
json.dumps({"type": "system", "subtype": "init", "session_id": sid}),
|
|
3341
|
+
json.dumps({"type": "result", "subtype": "success", "result": "ok"}),
|
|
3342
|
+
],
|
|
3343
|
+
"exit_code": 0,
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
|
|
3347
|
+
async def test_claude_code_sessions_are_isolated_per_conversation(
|
|
3348
|
+
stateful_fake_claude, tmp_path
|
|
3349
|
+
):
|
|
3350
|
+
"""claude_code 同样按 conversation 分轨:频道/DM 各自续聊,互不串味。
|
|
3351
|
+
|
|
3352
|
+
这条是本次改动的本体 —— 分轨此前只有 qoder 走,claude_code 不管在哪个
|
|
3353
|
+
频道被唤醒都 --resume 同一条全局会话。
|
|
3354
|
+
"""
|
|
3355
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3356
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3357
|
+
fake = stateful_fake_claude([
|
|
3358
|
+
_claude_ok("sess_debug"), # 0: #debug 首轮
|
|
3359
|
+
_claude_ok("sess_dm"), # 1: DM 首轮 —— 不得继承 #debug 的会话
|
|
3360
|
+
_claude_ok("sess_debug"), # 2: 回 #debug —— resume 自己的轨
|
|
3361
|
+
_claude_ok("sess_dm"), # 3: 回 DM —— resume DM 的轨
|
|
3362
|
+
])
|
|
3363
|
+
runner = _build_runner(fake, ws, FakeOutbound())
|
|
3364
|
+
dbg = {**_make_payload(), "conversation_id": "conv_debug"}
|
|
3365
|
+
dm = {**_make_payload(), "conversation_id": "conv_dm"}
|
|
3366
|
+
|
|
3367
|
+
await runner.run_one("agent-A", dbg)
|
|
3368
|
+
assert "--resume" not in _read_argv_n(tmp_path, 0)
|
|
3369
|
+
await runner.run_one("agent-A", dm)
|
|
3370
|
+
assert "--resume" not in _read_argv_n(tmp_path, 1)
|
|
3371
|
+
await runner.run_one("agent-A", dbg)
|
|
3372
|
+
a2 = _read_argv_n(tmp_path, 2)
|
|
3373
|
+
assert a2[a2.index("--resume") + 1] == "sess_debug"
|
|
3374
|
+
await runner.run_one("agent-A", dm)
|
|
3375
|
+
a3 = _read_argv_n(tmp_path, 3)
|
|
3376
|
+
assert a3[a3.index("--resume") + 1] == "sess_dm"
|
|
3377
|
+
|
|
3378
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3379
|
+
assert lanes == {"conv_debug": "sess_debug", "conv_dm": "sess_dm"}
|
|
3380
|
+
|
|
3381
|
+
|
|
3382
|
+
async def test_new_conversation_never_inherits_the_global_session(
|
|
3383
|
+
stateful_fake_claude, tmp_path
|
|
3384
|
+
):
|
|
3385
|
+
"""升级现场的回归闸:老 agent 带着全局 last_session 进入本版本,新频道
|
|
3386
|
+
首轮必须从零开始 —— 继承它正是分轨要消灭的那条串味路径。"""
|
|
3387
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3388
|
+
ws = FakeWorkspace(
|
|
3389
|
+
FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="global_old")
|
|
3390
|
+
)
|
|
3391
|
+
fake = stateful_fake_claude([_claude_ok("sess_fresh")])
|
|
3392
|
+
runner = _build_runner(fake, ws, FakeOutbound())
|
|
3393
|
+
|
|
3394
|
+
await runner.run_one(
|
|
3395
|
+
"agent-A", {**_make_payload(), "conversation_id": "conv_new"}
|
|
3396
|
+
)
|
|
3397
|
+
assert "--resume" not in _read_argv_n(tmp_path, 0)
|
|
3398
|
+
|
|
3399
|
+
|
|
3400
|
+
async def test_non_platform_wake_still_uses_the_global_lane(
|
|
3401
|
+
stateful_fake_claude, tmp_path
|
|
3402
|
+
):
|
|
3403
|
+
"""无 conversation_id 的唤醒(定时器 / spawn 链)沿用全局轨,不受分轨影响。"""
|
|
3404
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3405
|
+
ws = FakeWorkspace(
|
|
3406
|
+
FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="global_old")
|
|
3407
|
+
)
|
|
3408
|
+
fake = stateful_fake_claude([_claude_ok("global_old")])
|
|
3409
|
+
runner = _build_runner(fake, ws, FakeOutbound())
|
|
3410
|
+
|
|
3411
|
+
await runner.run_one("agent-A", _make_payload())
|
|
3412
|
+
a0 = _read_argv_n(tmp_path, 0)
|
|
3413
|
+
assert a0[a0.index("--resume") + 1] == "global_old"
|
|
3414
|
+
|
|
3415
|
+
|
|
3416
|
+
async def test_codex_sessions_are_isolated_per_conversation(
|
|
3417
|
+
stateful_fake_claude, tmp_path
|
|
3418
|
+
):
|
|
3419
|
+
"""codex 分轨;resume 形态是 ``exec resume … <thread_id> -``。"""
|
|
3420
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3421
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3422
|
+
fake = stateful_fake_claude([
|
|
3423
|
+
{"stdout": _codex_ok("thread_a"), "exit_code": 0}, # 0: conv_A 首轮
|
|
3424
|
+
{"stdout": _codex_ok("thread_b"), "exit_code": 0}, # 1: conv_B 首轮
|
|
3425
|
+
{"stdout": _codex_ok("thread_a"), "exit_code": 0}, # 2: 回 conv_A
|
|
3426
|
+
])
|
|
3427
|
+
runner = _codex_runner(fake, ws, FakeOutbound())
|
|
3428
|
+
a = {**_make_payload(), "runtime_engine": "codex", "conversation_id": "conv_A"}
|
|
3429
|
+
b = {**_make_payload(), "runtime_engine": "codex", "conversation_id": "conv_B"}
|
|
3430
|
+
|
|
3431
|
+
await runner.run_one("agent-A", a)
|
|
3432
|
+
assert "resume" not in _read_argv_n(tmp_path, 0)
|
|
3433
|
+
await runner.run_one("agent-A", b)
|
|
3434
|
+
assert "resume" not in _read_argv_n(tmp_path, 1)
|
|
3435
|
+
await runner.run_one("agent-A", a)
|
|
3436
|
+
a2 = _read_argv_n(tmp_path, 2)
|
|
3437
|
+
assert "resume" in a2
|
|
3438
|
+
assert a2[-2] == "thread_a" # …<thread_id> "-"
|
|
3439
|
+
|
|
3440
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3441
|
+
assert lanes == {"conv_A": "thread_a", "conv_B": "thread_b"}
|
|
3324
3442
|
|
|
3325
3443
|
|
|
3326
3444
|
# ---------------------------------------------------------------------------
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/agent_upload_denylist.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/delegation_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/injection_state.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/qoder_credentials.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/src/cloudagent_daemon/readonly_bash_guard.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_e2e_deliver_to_spawn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_mcp_bridge_round_trip.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/integration/test_spawn_mcp_bridge_e2e.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_claude_runner_codex_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_mcp_bridge_agent_retrieval.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_mcp_bridge_send_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_outbox_stuck_watchdog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_service_multi_profile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.4 → cloudagent_daemon-0.5.5}/tests/unit/test_workspace_root_task113.py
RENAMED
|
File without changes
|
|
File without changes
|