cloudagent-daemon 0.5.5__tar.gz → 0.5.6__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.
Files changed (102) hide show
  1. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/CHANGELOG.md +26 -0
  2. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/PKG-INFO +1 -1
  3. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/pyproject.toml +1 -1
  4. cloudagent_daemon-0.5.6/src/cloudagent_daemon/__init__.py +1 -0
  5. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/claude_runner.py +54 -2
  6. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_claude_runner.py +110 -0
  7. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/uv.lock +1 -1
  8. cloudagent_daemon-0.5.5/src/cloudagent_daemon/__init__.py +0 -1
  9. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/.gitignore +0 -0
  10. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/LICENSE +0 -0
  11. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/README.md +0 -0
  12. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/scripts/release.sh +0 -0
  13. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/__main__.py +0 -0
  14. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/agent_download.py +0 -0
  15. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/agent_upload.py +0 -0
  16. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
  17. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/auth_preflight.py +0 -0
  18. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/cli.py +0 -0
  19. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/config.py +0 -0
  20. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/context_fetch.py +0 -0
  21. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/delegation_report.py +0 -0
  22. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/doctor.py +0 -0
  23. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/frame_dispatch.py +0 -0
  24. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/http_outbound.py +0 -0
  25. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/injection_state.py +0 -0
  26. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/jsonl_logging.py +0 -0
  27. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/login.py +0 -0
  28. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/main_loop.py +0 -0
  29. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/mcp_bridge.py +0 -0
  30. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/metrics.py +0 -0
  31. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/metrics_server.py +0 -0
  32. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/outbound_queue.py +0 -0
  33. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/paths.py +0 -0
  34. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/protocol.py +0 -0
  35. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/qoder_credentials.py +0 -0
  36. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
  37. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/runtime_config.py +0 -0
  38. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/runtime_detect.py +0 -0
  39. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/service.py +0 -0
  40. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/spawn_outbox.py +0 -0
  41. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/spawn_queue.py +0 -0
  42. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/sse_client.py +0 -0
  43. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/transport.py +0 -0
  44. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/unix_socket.py +0 -0
  45. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/workspace.py +0 -0
  46. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/src/cloudagent_daemon/ws_client.py +0 -0
  47. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/__init__.py +0 -0
  48. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/_fake_sse_server.py +0 -0
  49. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/conftest.py +0 -0
  50. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
  51. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_e2e_sse.py +0 -0
  52. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_full_e2e.py +0 -0
  53. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_mcp_bridge_round_trip.py +0 -0
  54. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_one_shot_cli.py +0 -0
  55. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_spawn_mcp_bridge_e2e.py +0 -0
  56. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_sse_handshake.py +0 -0
  57. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_sse_watchdog.py +0 -0
  58. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_ws_handshake.py +0 -0
  59. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/integration/test_ws_reconnect.py +0 -0
  60. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/conftest.py +0 -0
  61. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_agent_download.py +0 -0
  62. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_agent_upload.py +0 -0
  63. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_auth_preflight.py +0 -0
  64. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_claude_runner_codex_contract.py +0 -0
  65. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_claude_runner_routing_contract.py +0 -0
  66. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_claude_runner_w3.py +0 -0
  67. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_cli_multi_profile.py +0 -0
  68. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_cli_smoke.py +0 -0
  69. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_config.py +0 -0
  70. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_context_fetch.py +0 -0
  71. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_delegation_report.py +0 -0
  72. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_doctor.py +0 -0
  73. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_frame_dispatch.py +0 -0
  74. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_http_outbound.py +0 -0
  75. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_injection_state.py +0 -0
  76. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_jsonl_logging.py +0 -0
  77. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_login.py +0 -0
  78. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_main_loop_assign.py +0 -0
  79. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_main_loop_preflight.py +0 -0
  80. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_mcp_bridge_agent_retrieval.py +0 -0
  81. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_mcp_bridge_send_message.py +0 -0
  82. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_metrics.py +0 -0
  83. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_metrics_server.py +0 -0
  84. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_multi_tool_wake.py +0 -0
  85. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_outbound_queue.py +0 -0
  86. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
  87. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_packaging_mcp_pin.py +0 -0
  88. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_paths.py +0 -0
  89. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_protocol.py +0 -0
  90. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_qoder_credentials.py +0 -0
  91. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_readonly_bash_guard.py +0 -0
  92. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_runtime_detect.py +0 -0
  93. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_service.py +0 -0
  94. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_service_multi_profile.py +0 -0
  95. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_spawn_outbox.py +0 -0
  96. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_spawn_queue.py +0 -0
  97. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_sse_active_watchdog.py +0 -0
  98. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_start_guard.py +0 -0
  99. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_status_stop.py +0 -0
  100. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_workspace.py +0 -0
  101. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_workspace_root_task113.py +0 -0
  102. {cloudagent_daemon-0.5.5 → cloudagent_daemon-0.5.6}/tests/unit/test_ws_client_insecure.py +0 -0
@@ -4,6 +4,32 @@ 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.6] — 2026-08-30
8
+
9
+ ### Changed
10
+ - Every claude_code wake now passes `--autocompact 200k`. A lane is re-read in
11
+ full on every wake, so an unbounded one charges its whole history each time —
12
+ one agent's lane climbed to 1.07M cached tokens and was replaying all of it 96
13
+ times a day to add a few thousand tokens of new content. An explicit size is
14
+ used rather than `auto` because `auto` tracks the model's own window (1M on
15
+ opus), which is precisely the runaway being bounded. This prevents what
16
+ `_is_context_exhausted` (0.5.x, #114) only cures after a wake has already
17
+ failed and its session been discarded.
18
+ - A timer-fired wake (`message_id="remind:<id>:<n>"`) now runs in a throwaway
19
+ session: it does not resume a lane, does not record one, and its failure may
20
+ not clear one. A reminder tick is a stateless patrol — what it must decide
21
+ comes from the room's current state, which it reads live, not from what it
22
+ said on the previous tick.
23
+
24
+ ### Notes
25
+ - Recording a patrol's session as the room's lane was the same bleed as #95,
26
+ arriving by another door: the next human to speak in that room inherited the
27
+ timer's accumulated history. Ordinary wakes are unaffected — a human message
28
+ in that same room still resumes the room's lane and still records its session.
29
+ - Hosts on 0.5.5 will see supervision-style agents start each tick fresh. That
30
+ is the change, not a regression: such an agent should be reading the room, not
31
+ replaying itself.
32
+
7
33
  ## [0.5.5] — 2026-08-27
8
34
 
9
35
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudagent-daemon
3
- Version: 0.5.5
3
+ Version: 0.5.6
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.5"
7
+ version = "0.5.6"
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.6"
@@ -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)
@@ -1380,6 +1398,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
1380
1398
  model, reasoning_effort = self._model_and_effort_for(ws, payload)
1381
1399
  runtime_env = self._runtime_env_for(ws, payload)
1382
1400
  run_mode = self._run_mode_for(payload)
1401
+ periodic = self._is_periodic_wake(payload)
1383
1402
  scratch_dir = (
1384
1403
  self._readonly_scratch_dir(ws, payload)
1385
1404
  if run_mode == "read_analyze"
@@ -1395,6 +1414,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
1395
1414
  reasoning_effort=reasoning_effort,
1396
1415
  run_mode=run_mode,
1397
1416
  scratch_dir=scratch_dir,
1417
+ periodic=periodic,
1398
1418
  conversation_id=(
1399
1419
  str(payload["conversation_id"])
1400
1420
  if payload.get("conversation_id") else None
@@ -1918,6 +1938,27 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
1918
1938
  raw = payload.get("run_mode")
1919
1939
  return "read_analyze" if raw == "read_analyze" else "full"
1920
1940
 
1941
+ @staticmethod
1942
+ def _is_periodic_wake(payload: dict) -> bool:
1943
+ """True for a timer-fired wake — one occurrence of a reminder.
1944
+
1945
+ Backend stamps these with the synthetic message id
1946
+ ``remind:<reminder_id>:<occurrence>``.
1947
+
1948
+ A periodic wake is a *stateless patrol*: what it has to decide comes
1949
+ from the room's CURRENT state, which it reads live (platform_read),
1950
+ not from what it said on the previous tick. So it neither resumes a
1951
+ lane nor writes one back — it runs in its own throwaway session.
1952
+
1953
+ Before this, a 15-minute supervision timer resumed its channel's lane
1954
+ 96 times a day, replaying an ever-growing history to contribute a few
1955
+ thousand tokens of new content — the replay reached ~1M tokens per
1956
+ wake — and left that history behind for the humans in the room to
1957
+ inherit on their next message.
1958
+ """
1959
+ mid = payload.get("message_id")
1960
+ return isinstance(mid, str) and mid.startswith("remind:")
1961
+
1921
1962
  def _readonly_scratch_dir(
1922
1963
  self, ws: AgentWorkspaceLike, payload: dict
1923
1964
  ) -> "Path":
@@ -2217,6 +2258,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
2217
2258
  run_mode: str = "full",
2218
2259
  scratch_dir: "Path | None" = None,
2219
2260
  conversation_id: str | None = None,
2261
+ periodic: bool = False,
2220
2262
  ) -> list[str]:
2221
2263
  """Build the ``claude`` argv list for one wake.
2222
2264
 
@@ -2243,7 +2285,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
2243
2285
  # 在哪个频道被唤醒都 --resume 同一条会话。
2244
2286
  last_session = (
2245
2287
  self._lane_session(ws, conversation_id)
2246
- if (with_resume and not read_analyze) else None
2288
+ if (with_resume and not read_analyze and not periodic) else None
2247
2289
  )
2248
2290
  if runtime_engine == "codex":
2249
2291
  binary = self.runtime_binaries.get("codex") or "codex"
@@ -2378,6 +2420,16 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
2378
2420
  cmd += ["--model", model]
2379
2421
  if reasoning_effort:
2380
2422
  cmd += ["--effort", reasoning_effort]
2423
+ # #114 followup — cap the context BEFORE it overflows.
2424
+ #
2425
+ # ``_is_context_exhausted`` is the cure and it is expensive: the wake
2426
+ # has already failed, and the remedy throws the whole session away.
2427
+ # ``--autocompact`` is the prevention — the CLI folds older turns into
2428
+ # a summary at the threshold, so a long-lived lane keeps its thread
2429
+ # instead of being discarded whole. It also bounds what every wake
2430
+ # REPLAYS, which is the larger bill: a lane is re-read in full on each
2431
+ # wake, so an unbounded one charges its entire history every time.
2432
+ cmd += ["--autocompact", self.AUTOCOMPACT_WINDOW]
2381
2433
  if last_session:
2382
2434
  cmd += ["--resume", last_session]
2383
2435
  # 1i Bug F — inject operational contract via --append-system-prompt.
@@ -3846,3 +3846,113 @@ 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_ordinary_wake_in_that_room_still_resumes_and_records(
3934
+ stateful_fake_claude, tmp_path
3935
+ ):
3936
+ """B control — only TIMER wakes are throwaway. A human message in the same
3937
+ room still resumes that room's lane and still records its session."""
3938
+ mcp_cfg = tmp_path / "mcp.json"; mcp_cfg.write_text("{}")
3939
+ (tmp_path / ".meta").mkdir(exist_ok=True)
3940
+ (tmp_path / ".meta" / "sessions.json").write_text(
3941
+ json.dumps({"conv_room": "human_thread"})
3942
+ )
3943
+ agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None)
3944
+ runner = _build_runner(
3945
+ stateful_fake_claude([_claude_ok("human_thread_2")]),
3946
+ FakeWorkspace(agent_ws),
3947
+ FakeOutbound(),
3948
+ )
3949
+
3950
+ await runner.run_one(
3951
+ "agent-A", {**_make_payload(), "conversation_id": "conv_room"}
3952
+ )
3953
+
3954
+ argv = _read_argv_n(tmp_path, 0)
3955
+ assert argv[argv.index("--resume") + 1] == "human_thread"
3956
+ lanes = json.loads((tmp_path / ".meta" / "sessions.json").read_text())
3957
+ assert lanes == {"conv_room": "human_thread_2"}
3958
+ assert agent_ws.set_calls == ["human_thread_2"]
@@ -122,7 +122,7 @@ wheels = [
122
122
 
123
123
  [[package]]
124
124
  name = "cloudagent-daemon"
125
- version = "0.5.5"
125
+ version = "0.5.6"
126
126
  source = { editable = "." }
127
127
  dependencies = [
128
128
  { name = "h2" },
@@ -1 +0,0 @@
1
- __version__ = "0.5.5"