cloudagent-daemon 0.5.5__tar.gz → 0.5.7__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.5 → cloudagent_daemon-0.5.7}/CHANGELOG.md +46 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/PKG-INFO +1 -1
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/pyproject.toml +1 -1
- cloudagent_daemon-0.5.7/src/cloudagent_daemon/__init__.py +1 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/claude_runner.py +60 -3
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_claude_runner.py +137 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/uv.lock +1 -1
- cloudagent_daemon-0.5.5/src/cloudagent_daemon/__init__.py +0 -1
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/.gitignore +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/LICENSE +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/README.md +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/scripts/release.sh +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/__main__.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/agent_download.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/agent_upload.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/cli.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/config.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/context_fetch.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/delegation_report.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/doctor.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/http_outbound.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/injection_state.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/login.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/main_loop.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/mcp_bridge.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/metrics.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/metrics_server.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/paths.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/protocol.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/runtime_config.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/service.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/sse_client.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/transport.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/unix_socket.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/workspace.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/ws_client.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/__init__.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/_fake_sse_server.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/conftest.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_e2e_sse.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_full_e2e.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_mcp_bridge_round_trip.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_one_shot_cli.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_spawn_mcp_bridge_e2e.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_sse_handshake.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_sse_watchdog.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_ws_handshake.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_ws_reconnect.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/conftest.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_agent_download.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_agent_upload.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_claude_runner_codex_contract.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_claude_runner_routing_contract.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_claude_runner_w3.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_cli_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_cli_smoke.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_config.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_context_fetch.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_delegation_report.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_doctor.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_http_outbound.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_injection_state.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_login.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_main_loop_assign.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_main_loop_preflight.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_mcp_bridge_agent_retrieval.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_mcp_bridge_send_message.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_metrics.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_metrics_server.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_multi_tool_wake.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_packaging_mcp_pin.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_paths.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_protocol.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_service.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_service_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_sse_active_watchdog.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_start_guard.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_status_stop.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_workspace.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_workspace_root_task113.py +0 -0
- {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/unit/test_ws_client_insecure.py +0 -0
|
@@ -4,6 +4,52 @@ 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.7] — 2026-08-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- A periodic (timer) wake no longer publishes its throwaway session as the
|
|
11
|
+
global lane. 0.5.6 closed `set_last_session_id` but not `write_last_spawn`,
|
|
12
|
+
which rewrites `last-spawn.json` wholesale — and `last_session_id()` reads
|
|
13
|
+
that file's `session_id` back as the global lane. A patrol therefore still
|
|
14
|
+
left its thread there, and the next wake without a conversation resumed it:
|
|
15
|
+
the same bleed 0.5.6 set out to remove, one door further along. Caught on a
|
|
16
|
+
live host, not in review — after upgrading, the tick correctly dropped
|
|
17
|
+
`--resume` and left the room's lane alone, but `last-spawn.json` still moved.
|
|
18
|
+
|
|
19
|
+
### Notes
|
|
20
|
+
- `read_analyze` already carried the owner's value through this call for the
|
|
21
|
+
same reason; periodic wakes now share that path. The spawn record still
|
|
22
|
+
captures the tick's rc/stderr, so operators lose no diagnostics.
|
|
23
|
+
- Anyone who ran 0.5.6 should check `.meta/last-spawn.json`: if its
|
|
24
|
+
`session_id` points at a patrol session, setting it to `null` makes the next
|
|
25
|
+
conversation-less wake start clean.
|
|
26
|
+
|
|
27
|
+
## [0.5.6] — 2026-08-30
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Every claude_code wake now passes `--autocompact 200k`. A lane is re-read in
|
|
31
|
+
full on every wake, so an unbounded one charges its whole history each time —
|
|
32
|
+
one agent's lane climbed to 1.07M cached tokens and was replaying all of it 96
|
|
33
|
+
times a day to add a few thousand tokens of new content. An explicit size is
|
|
34
|
+
used rather than `auto` because `auto` tracks the model's own window (1M on
|
|
35
|
+
opus), which is precisely the runaway being bounded. This prevents what
|
|
36
|
+
`_is_context_exhausted` (0.5.x, #114) only cures after a wake has already
|
|
37
|
+
failed and its session been discarded.
|
|
38
|
+
- A timer-fired wake (`message_id="remind:<id>:<n>"`) now runs in a throwaway
|
|
39
|
+
session: it does not resume a lane, does not record one, and its failure may
|
|
40
|
+
not clear one. A reminder tick is a stateless patrol — what it must decide
|
|
41
|
+
comes from the room's current state, which it reads live, not from what it
|
|
42
|
+
said on the previous tick.
|
|
43
|
+
|
|
44
|
+
### Notes
|
|
45
|
+
- Recording a patrol's session as the room's lane was the same bleed as #95,
|
|
46
|
+
arriving by another door: the next human to speak in that room inherited the
|
|
47
|
+
timer's accumulated history. Ordinary wakes are unaffected — a human message
|
|
48
|
+
in that same room still resumes the room's lane and still records its session.
|
|
49
|
+
- Hosts on 0.5.5 will see supervision-style agents start each tick fresh. That
|
|
50
|
+
is the change, not a regression: such an agent should be reading the room, not
|
|
51
|
+
replaying itself.
|
|
52
|
+
|
|
7
53
|
## [0.5.5] — 2026-08-27
|
|
8
54
|
|
|
9
55
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudagent-daemon
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.7
|
|
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.7"
|
|
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.7"
|
|
@@ -474,6 +474,16 @@ class ClaudeRunner:
|
|
|
474
474
|
"no rollout found for thread",
|
|
475
475
|
)
|
|
476
476
|
|
|
477
|
+
#: #114 followup — ``--autocompact`` window for claude_code wakes.
|
|
478
|
+
#:
|
|
479
|
+
#: An explicit size rather than ``auto``: ``auto`` tracks the model's own
|
|
480
|
+
#: window (1M on opus), which is the runaway this exists to bound — a lane
|
|
481
|
+
#: is replayed in full on every wake, so "compacts just before overflow"
|
|
482
|
+
#: still means every wake pays for a near-window-sized history. 200k keeps
|
|
483
|
+
#: days of one room's working context while cutting per-wake replay ~5x.
|
|
484
|
+
#: The CLI accepts ``auto`` or 100k–1M (``claude --help``).
|
|
485
|
+
AUTOCOMPACT_WINDOW: str = "200k"
|
|
486
|
+
|
|
477
487
|
#: #114 — 上下文窗口撑满。这是**永久性**前提失败,不是瞬时错误:退避几秒
|
|
478
488
|
#: 再把同一份历史送一次,必然再次溢出。codex 自己给出的处置就是 "Start a
|
|
479
489
|
#: new thread or clear earlier history",与 stale-session 的补救动作完全
|
|
@@ -692,6 +702,10 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
692
702
|
context_exhausted = self._is_context_exhausted(attempt)
|
|
693
703
|
if (
|
|
694
704
|
self._run_mode_for(payload) != "read_analyze"
|
|
705
|
+
# A periodic wake resumed nothing, so a stale/exhausted session
|
|
706
|
+
# is not its to drop — clearing here would evict the lane the
|
|
707
|
+
# room's humans are still using.
|
|
708
|
+
and not self._is_periodic_wake(payload)
|
|
695
709
|
and attempt.exit_code != 0
|
|
696
710
|
and (
|
|
697
711
|
any(
|
|
@@ -781,6 +795,10 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
781
795
|
# 防外人注入的上下文渗进 owner 后续 full 会话。仍上报 session_start
|
|
782
796
|
# 供实时展示,但不落 last-spawn(下次 full 唤醒仍 resume 干净的 owner 轨)。
|
|
783
797
|
is_read_analyze = self._run_mode_for(payload) == "read_analyze"
|
|
798
|
+
# #114 followup — a periodic wake ran in a throwaway session (it never
|
|
799
|
+
# resumed one). Recording it would hand the room's humans a lane whose
|
|
800
|
+
# history is a patrol tick, which is the bleed we just removed.
|
|
801
|
+
is_periodic = self._is_periodic_wake(payload)
|
|
784
802
|
# #95 — 所有引擎都按 conversation_id 分轨存会话;全局 last_session 仍写
|
|
785
803
|
# (last-spawn.json 可观测性 + 非平台唤醒兜底),但平台唤醒的 resume
|
|
786
804
|
# 只读 lane(见 _build_cmd),不会被别的频道的会话覆盖。
|
|
@@ -792,7 +810,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
792
810
|
# 把 session_start 刷成噪音。必须在 _set_lane 之前读。
|
|
793
811
|
prev_lane_session = self._lane_session(ws, conv_for_lane)
|
|
794
812
|
if captured_session and captured_session != prev_lane_session:
|
|
795
|
-
if not is_read_analyze:
|
|
813
|
+
if not is_read_analyze and not is_periodic:
|
|
796
814
|
if conv_for_lane:
|
|
797
815
|
self._set_lane(ws, conv_for_lane, captured_session)
|
|
798
816
|
ws.set_last_session_id(captured_session)
|
|
@@ -977,8 +995,13 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
977
995
|
# 的读取源(下次 resume 依据)。read_analyze 若把自己的 thread 写进去就会
|
|
978
996
|
# 渗进 owner 的 full 会话,故这里保留 owner 轨原有 session_id(仍记录本次
|
|
979
997
|
# 的 exit_code/stderr 供调试),read_analyze 的 thread 不落任何持久轨。
|
|
998
|
+
# A periodic wake is throwaway for the SAME reason: it resumed nothing,
|
|
999
|
+
# so recording its thread here would publish it as the global lane and
|
|
1000
|
+
# the next wake without a conversation would resume the patrol.
|
|
980
1001
|
spawn_record_session = (
|
|
981
|
-
ws.last_session_id()
|
|
1002
|
+
ws.last_session_id()
|
|
1003
|
+
if (is_read_analyze or is_periodic)
|
|
1004
|
+
else captured_session
|
|
982
1005
|
)
|
|
983
1006
|
ws.write_last_spawn(rc, spawn_record_session, stderr_tail)
|
|
984
1007
|
|
|
@@ -1380,6 +1403,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1380
1403
|
model, reasoning_effort = self._model_and_effort_for(ws, payload)
|
|
1381
1404
|
runtime_env = self._runtime_env_for(ws, payload)
|
|
1382
1405
|
run_mode = self._run_mode_for(payload)
|
|
1406
|
+
periodic = self._is_periodic_wake(payload)
|
|
1383
1407
|
scratch_dir = (
|
|
1384
1408
|
self._readonly_scratch_dir(ws, payload)
|
|
1385
1409
|
if run_mode == "read_analyze"
|
|
@@ -1395,6 +1419,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1395
1419
|
reasoning_effort=reasoning_effort,
|
|
1396
1420
|
run_mode=run_mode,
|
|
1397
1421
|
scratch_dir=scratch_dir,
|
|
1422
|
+
periodic=periodic,
|
|
1398
1423
|
conversation_id=(
|
|
1399
1424
|
str(payload["conversation_id"])
|
|
1400
1425
|
if payload.get("conversation_id") else None
|
|
@@ -1918,6 +1943,27 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
1918
1943
|
raw = payload.get("run_mode")
|
|
1919
1944
|
return "read_analyze" if raw == "read_analyze" else "full"
|
|
1920
1945
|
|
|
1946
|
+
@staticmethod
|
|
1947
|
+
def _is_periodic_wake(payload: dict) -> bool:
|
|
1948
|
+
"""True for a timer-fired wake — one occurrence of a reminder.
|
|
1949
|
+
|
|
1950
|
+
Backend stamps these with the synthetic message id
|
|
1951
|
+
``remind:<reminder_id>:<occurrence>``.
|
|
1952
|
+
|
|
1953
|
+
A periodic wake is a *stateless patrol*: what it has to decide comes
|
|
1954
|
+
from the room's CURRENT state, which it reads live (platform_read),
|
|
1955
|
+
not from what it said on the previous tick. So it neither resumes a
|
|
1956
|
+
lane nor writes one back — it runs in its own throwaway session.
|
|
1957
|
+
|
|
1958
|
+
Before this, a 15-minute supervision timer resumed its channel's lane
|
|
1959
|
+
96 times a day, replaying an ever-growing history to contribute a few
|
|
1960
|
+
thousand tokens of new content — the replay reached ~1M tokens per
|
|
1961
|
+
wake — and left that history behind for the humans in the room to
|
|
1962
|
+
inherit on their next message.
|
|
1963
|
+
"""
|
|
1964
|
+
mid = payload.get("message_id")
|
|
1965
|
+
return isinstance(mid, str) and mid.startswith("remind:")
|
|
1966
|
+
|
|
1921
1967
|
def _readonly_scratch_dir(
|
|
1922
1968
|
self, ws: AgentWorkspaceLike, payload: dict
|
|
1923
1969
|
) -> "Path":
|
|
@@ -2217,6 +2263,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2217
2263
|
run_mode: str = "full",
|
|
2218
2264
|
scratch_dir: "Path | None" = None,
|
|
2219
2265
|
conversation_id: str | None = None,
|
|
2266
|
+
periodic: bool = False,
|
|
2220
2267
|
) -> list[str]:
|
|
2221
2268
|
"""Build the ``claude`` argv list for one wake.
|
|
2222
2269
|
|
|
@@ -2243,7 +2290,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2243
2290
|
# 在哪个频道被唤醒都 --resume 同一条会话。
|
|
2244
2291
|
last_session = (
|
|
2245
2292
|
self._lane_session(ws, conversation_id)
|
|
2246
|
-
if (with_resume and not read_analyze) else None
|
|
2293
|
+
if (with_resume and not read_analyze and not periodic) else None
|
|
2247
2294
|
)
|
|
2248
2295
|
if runtime_engine == "codex":
|
|
2249
2296
|
binary = self.runtime_binaries.get("codex") or "codex"
|
|
@@ -2378,6 +2425,16 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2378
2425
|
cmd += ["--model", model]
|
|
2379
2426
|
if reasoning_effort:
|
|
2380
2427
|
cmd += ["--effort", reasoning_effort]
|
|
2428
|
+
# #114 followup — cap the context BEFORE it overflows.
|
|
2429
|
+
#
|
|
2430
|
+
# ``_is_context_exhausted`` is the cure and it is expensive: the wake
|
|
2431
|
+
# has already failed, and the remedy throws the whole session away.
|
|
2432
|
+
# ``--autocompact`` is the prevention — the CLI folds older turns into
|
|
2433
|
+
# a summary at the threshold, so a long-lived lane keeps its thread
|
|
2434
|
+
# instead of being discarded whole. It also bounds what every wake
|
|
2435
|
+
# REPLAYS, which is the larger bill: a lane is re-read in full on each
|
|
2436
|
+
# wake, so an unbounded one charges its entire history every time.
|
|
2437
|
+
cmd += ["--autocompact", self.AUTOCOMPACT_WINDOW]
|
|
2381
2438
|
if last_session:
|
|
2382
2439
|
cmd += ["--resume", last_session]
|
|
2383
2440
|
# 1i Bug F — inject operational contract via --append-system-prompt.
|
|
@@ -3846,3 +3846,140 @@ async def test_no_conversation_id_means_no_notice_but_never_a_crash_of_the_wake(
|
|
|
3846
3846
|
if inv.get("tool_name") == "platform_send"
|
|
3847
3847
|
]
|
|
3848
3848
|
assert not sends
|
|
3849
|
+
|
|
3850
|
+
|
|
3851
|
+
# ---------------------------------------------------------------------------
|
|
3852
|
+
# #114 followup — bound what a wake replays (A: --autocompact), and keep
|
|
3853
|
+
# timers off the room's thread (B: a periodic wake is a throwaway session).
|
|
3854
|
+
#
|
|
3855
|
+
# Both target the same bill from opposite ends. A lane is re-read IN FULL on
|
|
3856
|
+
# every wake, so an unbounded one charges its whole history each time; a
|
|
3857
|
+
# 15-minute timer made that 96 times a day while contributing a few thousand
|
|
3858
|
+
# tokens of new content, and left the accumulated patrol history behind for
|
|
3859
|
+
# whoever spoke in the room next.
|
|
3860
|
+
# ---------------------------------------------------------------------------
|
|
3861
|
+
|
|
3862
|
+
|
|
3863
|
+
def _reminder_payload(reminder_id: str = "rem_1", occurrence: int = 0, **extra) -> dict:
|
|
3864
|
+
"""A timer wake as the backend stamps it: ``remind:<reminder_id>:<n>``."""
|
|
3865
|
+
return {
|
|
3866
|
+
**_make_payload(message_id=f"remind:{reminder_id}:{occurrence}"),
|
|
3867
|
+
**extra,
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
|
|
3871
|
+
async def test_every_claude_wake_caps_its_context_window(
|
|
3872
|
+
stateful_fake_claude, tmp_path
|
|
3873
|
+
):
|
|
3874
|
+
"""A — the cap is on every wake, not only the ones that already grew."""
|
|
3875
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3876
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3877
|
+
runner = _build_runner(
|
|
3878
|
+
stateful_fake_claude([_claude_ok("s1")]), ws, FakeOutbound()
|
|
3879
|
+
)
|
|
3880
|
+
|
|
3881
|
+
await runner.run_one("agent-A", _make_payload())
|
|
3882
|
+
|
|
3883
|
+
argv = _read_argv_n(tmp_path, 0)
|
|
3884
|
+
assert argv[argv.index("--autocompact") + 1] == ClaudeRunner.AUTOCOMPACT_WINDOW
|
|
3885
|
+
|
|
3886
|
+
|
|
3887
|
+
async def test_periodic_wake_does_not_resume_the_rooms_lane(
|
|
3888
|
+
stateful_fake_claude, tmp_path
|
|
3889
|
+
):
|
|
3890
|
+
"""B — a patrol reads the room's CURRENT state; it must not replay its past."""
|
|
3891
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3892
|
+
(tmp_path / ".meta").mkdir(exist_ok=True)
|
|
3893
|
+
(tmp_path / ".meta" / "sessions.json").write_text(
|
|
3894
|
+
json.dumps({"conv_room": "human_thread"})
|
|
3895
|
+
)
|
|
3896
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3897
|
+
runner = _build_runner(
|
|
3898
|
+
stateful_fake_claude([_claude_ok("patrol_thread")]), ws, FakeOutbound()
|
|
3899
|
+
)
|
|
3900
|
+
|
|
3901
|
+
await runner.run_one("agent-A", _reminder_payload(conversation_id="conv_room"))
|
|
3902
|
+
|
|
3903
|
+
assert "--resume" not in _read_argv_n(tmp_path, 0)
|
|
3904
|
+
|
|
3905
|
+
|
|
3906
|
+
async def test_periodic_wake_leaves_the_rooms_lane_untouched(
|
|
3907
|
+
stateful_fake_claude, tmp_path
|
|
3908
|
+
):
|
|
3909
|
+
"""B — and it writes nothing back: neither the room's lane nor the global.
|
|
3910
|
+
|
|
3911
|
+
This is the regression itself. A timer that records its throwaway session
|
|
3912
|
+
hands its patrol history to the next human who speaks in that room.
|
|
3913
|
+
"""
|
|
3914
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3915
|
+
(tmp_path / ".meta").mkdir(exist_ok=True)
|
|
3916
|
+
(tmp_path / ".meta" / "sessions.json").write_text(
|
|
3917
|
+
json.dumps({"conv_room": "human_thread"})
|
|
3918
|
+
)
|
|
3919
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None)
|
|
3920
|
+
runner = _build_runner(
|
|
3921
|
+
stateful_fake_claude([_claude_ok("patrol_thread")]),
|
|
3922
|
+
FakeWorkspace(agent_ws),
|
|
3923
|
+
FakeOutbound(),
|
|
3924
|
+
)
|
|
3925
|
+
|
|
3926
|
+
await runner.run_one("agent-A", _reminder_payload(conversation_id="conv_room"))
|
|
3927
|
+
|
|
3928
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3929
|
+
assert lanes == {"conv_room": "human_thread"} # patrol_thread did NOT land
|
|
3930
|
+
assert agent_ws.set_calls == [] # nor did the global lane move
|
|
3931
|
+
|
|
3932
|
+
|
|
3933
|
+
async def test_periodic_wake_does_not_publish_itself_as_the_global_lane(
|
|
3934
|
+
stateful_fake_claude, tmp_path
|
|
3935
|
+
):
|
|
3936
|
+
"""B — the global lane has TWO writers; a patrol may use neither.
|
|
3937
|
+
|
|
3938
|
+
``set_last_session_id`` is the obvious one, but ``write_last_spawn``
|
|
3939
|
+
rewrites last-spawn.json wholesale — and ``last_session_id()`` reads its
|
|
3940
|
+
``session_id`` back as the global lane. Blocking only the first still let
|
|
3941
|
+
a patrol publish its throwaway thread, so the next wake with no
|
|
3942
|
+
conversation resumed it. read_analyze already carried the owner's value
|
|
3943
|
+
through here for exactly this reason; a periodic wake now does too.
|
|
3944
|
+
"""
|
|
3945
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3946
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="owner_thread")
|
|
3947
|
+
runner = _build_runner(
|
|
3948
|
+
stateful_fake_claude([_claude_ok("patrol_thread")]),
|
|
3949
|
+
FakeWorkspace(agent_ws),
|
|
3950
|
+
FakeOutbound(),
|
|
3951
|
+
)
|
|
3952
|
+
|
|
3953
|
+
await runner.run_one("agent-A", _reminder_payload(conversation_id="conv_room"))
|
|
3954
|
+
|
|
3955
|
+
assert agent_ws.write_last_spawn_calls, "the spawn record is still written"
|
|
3956
|
+
recorded_session = agent_ws.write_last_spawn_calls[-1][1]
|
|
3957
|
+
assert recorded_session == "owner_thread" # NOT patrol_thread
|
|
3958
|
+
|
|
3959
|
+
|
|
3960
|
+
async def test_ordinary_wake_in_that_room_still_resumes_and_records(
|
|
3961
|
+
stateful_fake_claude, tmp_path
|
|
3962
|
+
):
|
|
3963
|
+
"""B control — only TIMER wakes are throwaway. A human message in the same
|
|
3964
|
+
room still resumes that room's lane and still records its session."""
|
|
3965
|
+
mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
|
|
3966
|
+
(tmp_path / ".meta").mkdir(exist_ok=True)
|
|
3967
|
+
(tmp_path / ".meta" / "sessions.json").write_text(
|
|
3968
|
+
json.dumps({"conv_room": "human_thread"})
|
|
3969
|
+
)
|
|
3970
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None)
|
|
3971
|
+
runner = _build_runner(
|
|
3972
|
+
stateful_fake_claude([_claude_ok("human_thread_2")]),
|
|
3973
|
+
FakeWorkspace(agent_ws),
|
|
3974
|
+
FakeOutbound(),
|
|
3975
|
+
)
|
|
3976
|
+
|
|
3977
|
+
await runner.run_one(
|
|
3978
|
+
"agent-A", {**_make_payload(), "conversation_id": "conv_room"}
|
|
3979
|
+
)
|
|
3980
|
+
|
|
3981
|
+
argv = _read_argv_n(tmp_path, 0)
|
|
3982
|
+
assert argv[argv.index("--resume") + 1] == "human_thread"
|
|
3983
|
+
lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
|
|
3984
|
+
assert lanes == {"conv_room": "human_thread_2"}
|
|
3985
|
+
assert agent_ws.set_calls == ["human_thread_2"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.5"
|
|
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.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/delegation_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/src/cloudagent_daemon/qoder_credentials.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/tests/integration/test_e2e_deliver_to_spawn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/tests/integration/test_mcp_bridge_round_trip.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/tests/unit/test_mcp_bridge_agent_retrieval.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/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.5 → cloudagent_daemon-0.5.7}/tests/unit/test_workspace_root_task113.py
RENAMED
|
File without changes
|
|
File without changes
|