cloudagent-daemon 0.5.2__tar.gz → 0.5.4__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.2 → cloudagent_daemon-0.5.4}/CHANGELOG.md +53 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/PKG-INFO +1 -1
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/pyproject.toml +1 -1
- cloudagent_daemon-0.5.4/src/cloudagent_daemon/__init__.py +1 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/claude_runner.py +163 -21
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner.py +423 -5
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/uv.lock +1 -1
- cloudagent_daemon-0.5.2/src/cloudagent_daemon/__init__.py +0 -1
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/.gitignore +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/LICENSE +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/README.md +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/scripts/release.sh +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/__main__.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_download.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_upload.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/cli.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/config.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/context_fetch.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/delegation_report.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/doctor.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/http_outbound.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/injection_state.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/login.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/main_loop.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/mcp_bridge.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/metrics.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/metrics_server.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/paths.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/protocol.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/runtime_config.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/service.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/sse_client.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/transport.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/unix_socket.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/workspace.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/ws_client.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/__init__.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/_fake_sse_server.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/conftest.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_e2e_sse.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_full_e2e.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_mcp_bridge_round_trip.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_one_shot_cli.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_spawn_mcp_bridge_e2e.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_sse_handshake.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_sse_watchdog.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_ws_handshake.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_ws_reconnect.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/conftest.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_agent_download.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_agent_upload.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_auth_preflight.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_codex_contract.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_routing_contract.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_w3.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_cli_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_cli_smoke.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_config.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_context_fetch.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_delegation_report.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_doctor.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_frame_dispatch.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_http_outbound.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_injection_state.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_jsonl_logging.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_login.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_main_loop_assign.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_main_loop_preflight.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_mcp_bridge_agent_retrieval.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_mcp_bridge_send_message.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_metrics.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_metrics_server.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_multi_tool_wake.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_outbound_queue.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_packaging_mcp_pin.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_paths.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_protocol.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_qoder_credentials.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_readonly_bash_guard.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_runtime_detect.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_service.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_service_multi_profile.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_spawn_outbox.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_spawn_queue.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_sse_active_watchdog.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_start_guard.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_status_stop.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_workspace.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_workspace_root_task113.py +0 -0
- {cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/unit/test_ws_client_insecure.py +0 -0
|
@@ -4,6 +4,59 @@ 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.4] — 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- A model-side error no longer counts toward the crash quota, and can no longer
|
|
11
|
+
quarantine an agent. Three consecutive `crashed` reports quarantine an agent
|
|
12
|
+
server-side, after which every wake is dropped and nobody is told; a model
|
|
13
|
+
refusing one turn is not the same as the agent being broken. This is how a
|
|
14
|
+
whole delivery line went dark twice — six hours on 2026-08-21, four on 08-23
|
|
15
|
+
(the latter from a content-safety flag raised while reviewing security
|
|
16
|
+
material). The wake now reports `idle`, which keeps the counter still.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- A failed wake posts a notice into the originating channel carrying the
|
|
20
|
+
engine's own error text. Until now this path only wrote a log line and a
|
|
21
|
+
metric — "the silence must be VISIBLE" (U0.4) was true only for whoever reads
|
|
22
|
+
logs, and nobody does; the user found out by noticing an agent had stopped
|
|
23
|
+
answering, hours later. The notice is explicitly labelled as daemon-emitted so
|
|
24
|
+
it can never be mistaken for the agent's own answer, which is what the
|
|
25
|
+
no-fallback rule was protecting.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
- Status code and notification are now separate concerns: the code drives
|
|
29
|
+
counting, the notice does the telling. Model errors report `idle` AND still
|
|
30
|
+
trigger the a2a `wake_aborted` system reply — dropping the latter would leave
|
|
31
|
+
a dispatching chief waiting forever.
|
|
32
|
+
- Process-level failures (binary missing, auth file unreadable) still count.
|
|
33
|
+
Retrying those forever fixes nothing, so the protection is kept — but they now
|
|
34
|
+
post a channel notice too, so they are no longer silent either.
|
|
35
|
+
|
|
36
|
+
## [0.5.3] — 2026-08-22
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- A wake that dies because the model's context window is full now drops the
|
|
40
|
+
oversized session and re-spawns WITHOUT `--resume`, instead of replaying the
|
|
41
|
+
same history after an 8s backoff. Replaying could never fit, so the retry was
|
|
42
|
+
guaranteed to fail; three of them in a row crossed the crash threshold and
|
|
43
|
+
quarantined the agent — silently, since a quarantined agent stops being woken
|
|
44
|
+
(#114). The cleared session id is persisted, so later wakes do not resume the
|
|
45
|
+
oversized thread either.
|
|
46
|
+
- The engine's own error text is now carried on the spawn result. Previously
|
|
47
|
+
only a bare `had_api_error` boolean survived, so the retry layer could not
|
|
48
|
+
tell a transient rate-limit (retry with resume) from a permanent
|
|
49
|
+
context-window overflow (start a new thread) and treated both the same.
|
|
50
|
+
- A context-exhausted wake reports `context_exhausted` rather than the generic
|
|
51
|
+
`upstream_api_error`, so the cause is visible without reading daemon logs.
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
- Detection matches an apostrophe-free substring: the engine writes "the
|
|
55
|
+
model's context window", and a smart-quote rewrite upstream would otherwise
|
|
56
|
+
silently disable the whole fix. A test pins that invariant.
|
|
57
|
+
- No 0.5.2 entry exists in this file; 0.5.2 was published manually and its
|
|
58
|
+
changelog entry was never written.
|
|
59
|
+
|
|
7
60
|
## [0.5.1] — 2026-08-20
|
|
8
61
|
|
|
9
62
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: cloudagent-daemon
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
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.4"
|
|
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.4"
|
|
@@ -338,6 +338,13 @@ class _SpawnAttempt:
|
|
|
338
338
|
# M8 — set True when claude emitted an ``isApiErrorMessage:true``
|
|
339
339
|
# event during the wake (upstream API error swallowed by the CLI).
|
|
340
340
|
had_api_error: bool = False
|
|
341
|
+
# #114 — the engine's OWN explanation for that error. codex reports
|
|
342
|
+
# upstream failures as a ``turn.failed``/``error`` stdout event whose
|
|
343
|
+
# message previously reached only the log, so the retry layer saw a
|
|
344
|
+
# bare boolean and could not tell a transient rate-limit apart from a
|
|
345
|
+
# permanent context-window overflow — and retried both the same way.
|
|
346
|
+
# Keeping the text is what lets the wake layer branch on the cause.
|
|
347
|
+
api_error_text: str | None = None
|
|
341
348
|
|
|
342
349
|
|
|
343
350
|
def _build_lazy_identity(agent_id: str, payload: dict) -> dict:
|
|
@@ -467,6 +474,22 @@ class ClaudeRunner:
|
|
|
467
474
|
"no rollout found for thread",
|
|
468
475
|
)
|
|
469
476
|
|
|
477
|
+
#: #114 — 上下文窗口撑满。这是**永久性**前提失败,不是瞬时错误:退避几秒
|
|
478
|
+
#: 再把同一份历史送一次,必然再次溢出。codex 自己给出的处置就是 "Start a
|
|
479
|
+
#: new thread or clear earlier history",与 stale-session 的补救动作完全
|
|
480
|
+
#: 一致(清掉会话、免 resume 重跑),故复用同一条路径而非退避重试。
|
|
481
|
+
#: 匹配按**小写子串**做,同时覆盖 codex 的自然语言提示和 OpenAI API 的
|
|
482
|
+
#: 标准错误码,避免只认一种措辞。
|
|
483
|
+
#: 刻意**不含撇号**:codex 的原文是 "the model's context window",但撇号是
|
|
484
|
+
#: 最容易被改写成弯引号(’)的字符,一旦不匹配整个修复就静默失效——而这个修复
|
|
485
|
+
#: 存在的意义正是消除静默失败。去掉撇号后的子串依然足够specific。
|
|
486
|
+
CONTEXT_EXHAUSTED_MARKERS: tuple[str, ...] = (
|
|
487
|
+
"ran out of room in the model",
|
|
488
|
+
"context_length_exceeded",
|
|
489
|
+
"maximum context length",
|
|
490
|
+
"context window",
|
|
491
|
+
)
|
|
492
|
+
|
|
470
493
|
#: 会产生外部副作用的工具(发消息/建任务类)。瞬时错误重试只在本次 wake
|
|
471
494
|
#: **没**调过这些工具时才做 —— 只读工具(list_messages 等)重跑无害。
|
|
472
495
|
SIDE_EFFECT_TOOLS: frozenset[str] = frozenset({
|
|
@@ -662,10 +685,22 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
662
685
|
self._qoder_lane_session(ws, stale_conv)
|
|
663
686
|
if stale_is_qoder and stale_conv else None
|
|
664
687
|
)
|
|
688
|
+
# #114 — 上下文撑满与 stale-session 是同一类问题:**当前会话已经
|
|
689
|
+
# 不能再用了**,区别只在于一个是引擎不认识它、另一个是它太大。补救
|
|
690
|
+
# 动作完全相同(清掉会话、免 resume 重跑一次),所以走同一条路径,
|
|
691
|
+
# 而不是掉进下面的瞬时退避重试——那会带着同一份撑满的历史重放,
|
|
692
|
+
# 注定再次失败,白烧一轮唤醒和一次计费,然后累计成 crash 进隔离。
|
|
693
|
+
context_exhausted = self._is_context_exhausted(attempt)
|
|
665
694
|
if (
|
|
666
695
|
self._run_mode_for(payload) != "read_analyze"
|
|
667
696
|
and attempt.exit_code != 0
|
|
668
|
-
and
|
|
697
|
+
and (
|
|
698
|
+
any(
|
|
699
|
+
m in attempt.stderr_tail
|
|
700
|
+
for m in self.STALE_SESSION_STDERR_MARKERS
|
|
701
|
+
)
|
|
702
|
+
or context_exhausted
|
|
703
|
+
)
|
|
669
704
|
and (ws.last_session_id() is not None or qoder_lane_sid is not None)
|
|
670
705
|
):
|
|
671
706
|
stale_sid = qoder_lane_sid or ws.last_session_id()
|
|
@@ -673,11 +708,12 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
673
708
|
if stale_is_qoder and stale_conv:
|
|
674
709
|
self._qoder_set_lane(ws, stale_conv, None)
|
|
675
710
|
logger.warning(
|
|
676
|
-
"claude_runner:
|
|
677
|
-
"
|
|
678
|
-
"
|
|
711
|
+
"claude_runner: dropping unusable session_id=%s "
|
|
712
|
+
"(cause=%s, detail=%r). Clearing and retrying "
|
|
713
|
+
"without resume.",
|
|
679
714
|
stale_sid,
|
|
680
|
-
|
|
715
|
+
"context_exhausted" if context_exhausted else "stale_session",
|
|
716
|
+
(attempt.api_error_text or attempt.stderr_tail)[-160:],
|
|
681
717
|
)
|
|
682
718
|
try:
|
|
683
719
|
ws.clear_last_session_id()
|
|
@@ -697,6 +733,12 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
697
733
|
attempt.exit_code != 0
|
|
698
734
|
and attempt.had_api_error
|
|
699
735
|
and not attempt.timeout_hit
|
|
736
|
+
# #114 — 永久性失败不走瞬时重试。退避 8 秒不会让上下文变小,
|
|
737
|
+
# 带 resume 重放同一份历史必然再次撑满;上面那条路径已经用
|
|
738
|
+
# "清会话 + 免 resume" 处理过它了。注意这里重新判定的是**重试
|
|
739
|
+
# 之后**的 attempt:若换新会话仍然撑满(例如注入的正文本身过
|
|
740
|
+
# 大),同样不该再退避重放。
|
|
741
|
+
and not self._is_context_exhausted(attempt)
|
|
700
742
|
and not any(
|
|
701
743
|
t.rsplit("__", 1)[-1] in self.SIDE_EFFECT_TOOLS
|
|
702
744
|
for t in attempt.tool_uses
|
|
@@ -942,21 +984,79 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
942
984
|
)
|
|
943
985
|
ws.write_last_spawn(rc, spawn_record_session, stderr_tail)
|
|
944
986
|
|
|
987
|
+
# 主人拍板(2026-08-24):**大模型端的报错一律直接回报给人,不再计入崩溃
|
|
988
|
+
# 配额、不再据此隔离 agent。** 模型拒绝一次不等于这个 agent 坏了;把它锁死
|
|
989
|
+
# 的代价是整条交付线静默停摆——8-21 六小时、8-23 四小时,两次都是这么来的
|
|
990
|
+
# (连续 3 次模型侧失败 → quarantined → 此后所有唤醒静默丢弃,没人被告知)。
|
|
991
|
+
#
|
|
992
|
+
# 报 idle 而非 crashed:idle 不进 record_agent_crash 的计数,且会顺带解除
|
|
993
|
+
# 历史遗留的隔离(record_agent_recovery)。失败本身**不靠这个状态码传达**,
|
|
994
|
+
# 由下面的频道通知负责——状态码只有运维看得到,人看的是频道。
|
|
995
|
+
model_side_error = attempt.had_api_error
|
|
945
996
|
if timeout_hit:
|
|
946
997
|
final_status = "timeout"
|
|
947
|
-
elif
|
|
948
|
-
|
|
949
|
-
# errors: it prints a synthetic ``isApiErrorMessage:true``
|
|
950
|
-
# event and exits rc=0 *without* a terminating ``type=
|
|
951
|
-
# result``. Without this branch the wake would be reported
|
|
952
|
-
# as ``idle`` (cleanly completed) — leaving the dispatching
|
|
953
|
-
# chief silently waiting for a reply that will never come.
|
|
954
|
-
final_status = "crashed"
|
|
998
|
+
elif model_side_error:
|
|
999
|
+
final_status = "idle"
|
|
955
1000
|
elif rc == 0:
|
|
956
1001
|
final_status = "idle"
|
|
957
1002
|
else:
|
|
958
1003
|
final_status = "crashed"
|
|
959
1004
|
|
|
1005
|
+
# 主人拍板(2026-08-24):**模型侧报错必须直接回到人眼前。**
|
|
1006
|
+
#
|
|
1007
|
+
# 此前这里什么都不发:U0.4 定的口径是"频道唤醒不做 fallback,沉默必须可见
|
|
1008
|
+
# (log + metric)"。出发点对——别拿 daemon 的兜底伪造 agent 的回答——但
|
|
1009
|
+
# "可见"只可见于日志和指标,**没有人在看**。实际后果:agent 崩了,频道里
|
|
1010
|
+
# 一个字都没有,发指令的人只能靠"叫它没反应"发现,中间隔了 4 个小时。
|
|
1011
|
+
#
|
|
1012
|
+
# 所以这里补的**不是 agent 的回答,是一条明确署名的系统通知**:诚实口径不破
|
|
1013
|
+
# (不冒充 agent),可见性拿回来。已有的 platform_send 兜底只在 rc==0 且有
|
|
1014
|
+
# 输出时触发("说了话但没调工具"),失败路径正是它漏掉的那半边。
|
|
1015
|
+
failure_note: str | None = None
|
|
1016
|
+
if model_side_error:
|
|
1017
|
+
failure_note = (attempt.api_error_text or "").strip() or "模型侧返回错误(无详细信息)"
|
|
1018
|
+
elif timeout_hit:
|
|
1019
|
+
failure_note = f"本轮超时({int(self._single_wake_timeout)} 秒内未结束)"
|
|
1020
|
+
elif rc != 0:
|
|
1021
|
+
failure_note = (attempt.stderr_tail or "").strip()[-400:] or f"进程非零退出(rc={rc})"
|
|
1022
|
+
notice_conv = payload.get("conversation_id")
|
|
1023
|
+
notice_session = payload.get("session_id")
|
|
1024
|
+
if (
|
|
1025
|
+
failure_note
|
|
1026
|
+
and isinstance(notice_conv, str) and notice_conv
|
|
1027
|
+
and isinstance(notice_session, str) and notice_session
|
|
1028
|
+
):
|
|
1029
|
+
kind_label = "模型侧报错" if model_side_error else ("超时" if timeout_hit else "进程异常")
|
|
1030
|
+
body = (
|
|
1031
|
+
f"⚠️ 我这一轮没能完成:**{kind_label}**\n\n"
|
|
1032
|
+
f"```\n{failure_note[:600]}\n```\n"
|
|
1033
|
+
+ (
|
|
1034
|
+
"这是**模型端**返回的错误,不是这个 agent 坏了——它**没有被隔离**,"
|
|
1035
|
+
"下次被唤醒会照常重试。"
|
|
1036
|
+
if model_side_error
|
|
1037
|
+
else "这是运行时/进程层面的失败,重试未必能解决,可能需要看一下机器。"
|
|
1038
|
+
)
|
|
1039
|
+
+ "\n(本条由 daemon 代发的系统通知,不是 agent 的回答。)"
|
|
1040
|
+
)
|
|
1041
|
+
try:
|
|
1042
|
+
await self.outbound.send_tool_call(
|
|
1043
|
+
agent_id=agent_id,
|
|
1044
|
+
session_id=notice_session,
|
|
1045
|
+
tool_name="platform_send",
|
|
1046
|
+
args={
|
|
1047
|
+
"content": body,
|
|
1048
|
+
"conversation_id": notice_conv,
|
|
1049
|
+
"acknowledge_new_messages": True,
|
|
1050
|
+
**({"delivery_id": _effective_delivery_id} if _effective_delivery_id else {}),
|
|
1051
|
+
},
|
|
1052
|
+
timeout=15.0,
|
|
1053
|
+
)
|
|
1054
|
+
except Exception: # noqa: BLE001 — 通知失败绝不能再把这一轮拖垮
|
|
1055
|
+
logger.warning(
|
|
1056
|
+
"claude_runner: failed to post failure notice agent=%s conv=%s",
|
|
1057
|
+
agent_id, notice_conv, exc_info=True,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
960
1060
|
# Wave C #3 — surface terminal status counts for /metrics +
|
|
961
1061
|
# doctor. Label-keyed so a single counter renders three
|
|
962
1062
|
# series (idle / crashed / timeout). Failure to publish must
|
|
@@ -992,7 +1092,10 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
992
1092
|
payload.get("parent_msg_id") or payload.get("message_id")
|
|
993
1093
|
)
|
|
994
1094
|
if (
|
|
995
|
-
|
|
1095
|
+
# model_side_error 现在报 idle(不进崩溃配额),但**这条链仍要通知**:
|
|
1096
|
+
# a2a 的 chief 是在等一条回复,不给它就是永远等下去。状态码用于计数,
|
|
1097
|
+
# 这里用于告知——两件事,不能因为改了前者而丢掉后者。
|
|
1098
|
+
(final_status in ("crashed", "timeout") or model_side_error)
|
|
996
1099
|
# This block only fires for task/peer_request/question kinds
|
|
997
1100
|
# (WAKE_REPLY_EXPECTED_KINDS, checked below), for which
|
|
998
1101
|
# fallback_tool is always send_message — so called_fallback_tool
|
|
@@ -1004,7 +1107,13 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1004
1107
|
and parent_for_abort not in (None, "")
|
|
1005
1108
|
):
|
|
1006
1109
|
if attempt.had_api_error:
|
|
1007
|
-
|
|
1110
|
+
# #114 — 把永久性的"会话撑满"和笼统的上游错误区分开:否则频道
|
|
1111
|
+
# 里那条 wake_aborted 只说"上游错误",真实原因只能靠翻日志。
|
|
1112
|
+
reason = (
|
|
1113
|
+
"context_exhausted"
|
|
1114
|
+
if self._is_context_exhausted(attempt)
|
|
1115
|
+
else "upstream_api_error"
|
|
1116
|
+
)
|
|
1008
1117
|
elif timeout_hit:
|
|
1009
1118
|
reason = "wake_timeout"
|
|
1010
1119
|
else:
|
|
@@ -1465,6 +1574,7 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1465
1574
|
final_text,
|
|
1466
1575
|
result_usage,
|
|
1467
1576
|
had_api_error,
|
|
1577
|
+
api_error_text,
|
|
1468
1578
|
) = await self._consume_stdout(
|
|
1469
1579
|
proc,
|
|
1470
1580
|
parse_mode=(
|
|
@@ -1495,12 +1605,34 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
|
|
|
1495
1605
|
final_result_text=final_text,
|
|
1496
1606
|
result_usage=result_usage,
|
|
1497
1607
|
had_api_error=had_api_error,
|
|
1608
|
+
api_error_text=api_error_text,
|
|
1498
1609
|
)
|
|
1499
1610
|
|
|
1500
1611
|
# ------------------------------------------------------------------
|
|
1501
1612
|
# Internal helpers
|
|
1502
1613
|
# ------------------------------------------------------------------
|
|
1503
1614
|
|
|
1615
|
+
def _is_context_exhausted(self, attempt: _SpawnAttempt) -> bool:
|
|
1616
|
+
"""#114 — True iff *attempt* failed because the model's context
|
|
1617
|
+
window is full.
|
|
1618
|
+
|
|
1619
|
+
A **permanent** premise failure, not a transient upstream error:
|
|
1620
|
+
the same history replayed after any backoff overflows the window
|
|
1621
|
+
again, so retrying with ``--resume`` burns a wake and a billing
|
|
1622
|
+
cycle to reach the identical failure. The remedy is the one the
|
|
1623
|
+
engine itself prints — start a new thread — which is exactly what
|
|
1624
|
+
the stale-session path already does, so both share it.
|
|
1625
|
+
|
|
1626
|
+
Reads only :attr:`_SpawnAttempt.api_error_text` (the engine's own
|
|
1627
|
+
message). An attempt that carried no explanation returns False:
|
|
1628
|
+
unknown cause keeps the previous, more conservative handling
|
|
1629
|
+
rather than silently discarding a session that may be fine.
|
|
1630
|
+
"""
|
|
1631
|
+
text = (attempt.api_error_text or "").lower()
|
|
1632
|
+
if not text:
|
|
1633
|
+
return False
|
|
1634
|
+
return any(m in text for m in self.CONTEXT_EXHAUSTED_MARKERS)
|
|
1635
|
+
|
|
1504
1636
|
#: 1i Bug E — the daemon-injected ``cloudagent`` MCP toolset MUST run
|
|
1505
1637
|
#: unattended in ``--print`` headless mode (otherwise claude prompts
|
|
1506
1638
|
#: the user for permission, the prompt is never answered, and the
|
|
@@ -2286,7 +2418,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2286
2418
|
*,
|
|
2287
2419
|
partial_session_id: str | None = None,
|
|
2288
2420
|
partial_agent_id: str | None = None,
|
|
2289
|
-
) -> tuple[str | None, list[str], str | None, dict | None, bool]:
|
|
2421
|
+
) -> tuple[str | None, list[str], str | None, dict | None, bool, str | None]:
|
|
2290
2422
|
"""Consume ``codex exec --json`` NDJSON.
|
|
2291
2423
|
|
|
2292
2424
|
Codex emits ``thread.started`` with ``thread_id`` and one or more
|
|
@@ -2304,13 +2436,16 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2304
2436
|
# codex 把上游 API 错误(模型 at capacity / 限流)作为 turn.failed/error
|
|
2305
2437
|
# 事件吐在 stdout —— 记为 had_api_error,wake 层据此做瞬时错误重试。
|
|
2306
2438
|
had_api_error = False
|
|
2439
|
+
# #114 — 同时留下引擎给的原因文本。只有一个布尔时,"限流"和"上下文
|
|
2440
|
+
# 撑满"在 wake 层无法区分,于是永久性失败被当成瞬时错误反复重放。
|
|
2441
|
+
api_error_text: str | None = None
|
|
2307
2442
|
publish_enabled = bool(
|
|
2308
2443
|
self._partial_publisher
|
|
2309
2444
|
and partial_session_id
|
|
2310
2445
|
and partial_agent_id
|
|
2311
2446
|
)
|
|
2312
2447
|
if proc.stdout is None:
|
|
2313
|
-
return None, tool_uses, None, None, False
|
|
2448
|
+
return None, tool_uses, None, None, False, None
|
|
2314
2449
|
|
|
2315
2450
|
async def _emit(event_type: str, payload: "dict[str, Any]") -> None:
|
|
2316
2451
|
if (
|
|
@@ -2468,9 +2603,12 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2468
2603
|
msg = raw if isinstance(raw, str) else None
|
|
2469
2604
|
logger.warning("CodexRunner: stdout error event=%s msg=%r", evt_type, msg)
|
|
2470
2605
|
had_api_error = True
|
|
2606
|
+
# 第一条为准:后续事件通常是同一个失败的级联噪声,首条才是根因。
|
|
2607
|
+
if api_error_text is None and isinstance(msg, str) and msg:
|
|
2608
|
+
api_error_text = msg
|
|
2471
2609
|
|
|
2472
2610
|
final_text = "\n\n".join(final_chunks) if final_chunks else None
|
|
2473
|
-
return captured, tool_uses, final_text, result_usage, had_api_error
|
|
2611
|
+
return captured, tool_uses, final_text, result_usage, had_api_error, api_error_text
|
|
2474
2612
|
|
|
2475
2613
|
async def _consume_stdout(
|
|
2476
2614
|
self,
|
|
@@ -2479,7 +2617,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2479
2617
|
parse_mode: str = "stream_json",
|
|
2480
2618
|
partial_session_id: str | None = None,
|
|
2481
2619
|
partial_agent_id: str | None = None,
|
|
2482
|
-
) -> tuple[str | None, list[str], str | None, dict | None, bool]:
|
|
2620
|
+
) -> tuple[str | None, list[str], str | None, dict | None, bool, str | None]:
|
|
2483
2621
|
"""Stream NDJSON; return ``(session_id, tool_uses, result_text, result_usage, had_api_error)``.
|
|
2484
2622
|
|
|
2485
2623
|
Accepts BOTH session-init event shapes for forward/backward
|
|
@@ -2531,6 +2669,8 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2531
2669
|
# M8 — set True when claude emits a synthetic isApiErrorMessage
|
|
2532
2670
|
# assistant event (upstream API error swallowed by the CLI).
|
|
2533
2671
|
had_api_error: bool = False
|
|
2672
|
+
# #114 — 与 codex 侧同构:保留引擎自己的错误文本供 wake 层判定原因。
|
|
2673
|
+
api_error_text: str | None = None
|
|
2534
2674
|
# 1k74 — running buffer for token-level streaming. claude emits
|
|
2535
2675
|
# one ``stream_event`` per content_block_delta (~80 chars at a
|
|
2536
2676
|
# time); we concatenate them and forward the running total to
|
|
@@ -2570,7 +2710,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2570
2710
|
)
|
|
2571
2711
|
|
|
2572
2712
|
if proc.stdout is None:
|
|
2573
|
-
return None, tool_uses, final_text, result_usage, had_api_error
|
|
2713
|
+
return None, tool_uses, final_text, result_usage, had_api_error, api_error_text
|
|
2574
2714
|
|
|
2575
2715
|
async for line_b in proc.stdout:
|
|
2576
2716
|
line = line_b.decode("utf-8", errors="replace").strip()
|
|
@@ -2685,6 +2825,8 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2685
2825
|
if isinstance(t, str):
|
|
2686
2826
|
err_text = t[:200]
|
|
2687
2827
|
break
|
|
2828
|
+
if api_error_text is None and err_text:
|
|
2829
|
+
api_error_text = err_text
|
|
2688
2830
|
logger.warning(
|
|
2689
2831
|
"ClaudeRunner: stdout isApiErrorMessage event — "
|
|
2690
2832
|
"wake will be marked crashed (text=%r)",
|
|
@@ -2824,7 +2966,7 @@ TASKS (频道任务板) — 频道里的分工协作走任务认领原语,纪
|
|
|
2824
2966
|
"claude_runner: cache metrics publish failed", exc_info=True
|
|
2825
2967
|
)
|
|
2826
2968
|
|
|
2827
|
-
return captured, tool_uses, final_text, result_usage, had_api_error
|
|
2969
|
+
return captured, tool_uses, final_text, result_usage, had_api_error, api_error_text
|
|
2828
2970
|
|
|
2829
2971
|
async def _wait_with_timeout(
|
|
2830
2972
|
self, proc: asyncio.subprocess.Process
|
|
@@ -2461,11 +2461,22 @@ def _make_dispatch_payload(
|
|
|
2461
2461
|
}
|
|
2462
2462
|
|
|
2463
2463
|
|
|
2464
|
-
async def
|
|
2464
|
+
async def test_isApiErrorMessage_does_not_burn_the_crash_quota(
|
|
2465
2465
|
fake_claude, tmp_path
|
|
2466
2466
|
):
|
|
2467
|
-
"""
|
|
2468
|
-
|
|
2467
|
+
"""2026-08-24 主人拍板改写:模型侧报错**不再**记 crashed。
|
|
2468
|
+
|
|
2469
|
+
这条测试原本断言的是相反的行为(M8:``isApiErrorMessage`` + rc=0 → 报
|
|
2470
|
+
``crashed``)。改写而不是删除,因为它当初的关切依然成立、只是解法换了:
|
|
2471
|
+
|
|
2472
|
+
当初的关切 —— "别报 idle,否则派活的 chief 会以为干完了、永远等下去"。
|
|
2473
|
+
现在的解法 —— 状态码只负责**计数**,告知由 ``wake_aborted`` 的系统回复
|
|
2474
|
+
和频道通知负责(见 ``test_wake_aborted_emits_system_reply_when_api_error``
|
|
2475
|
+
与 ``test_model_side_error_is_reported_into_the_channel``)。两件事拆开了。
|
|
2476
|
+
|
|
2477
|
+
为什么必须拆:报 crashed 会累计进隔离配额,连续 3 次就把 agent 锁死,此后
|
|
2478
|
+
**所有**唤醒被静默丢弃、无人被告知。模型拒绝一次不等于这个 agent 坏了。
|
|
2479
|
+
2026-08-21 与 08-23 两次整条交付线停摆(6 小时 / 4 小时)都源于此。
|
|
2469
2480
|
"""
|
|
2470
2481
|
mcp_cfg = tmp_path / "mcp.json"
|
|
2471
2482
|
mcp_cfg.write_text("{}")
|
|
@@ -2493,8 +2504,8 @@ async def test_isApiErrorMessage_promotes_wake_to_crashed(
|
|
|
2493
2504
|
statuses = [c for c in outbound.calls if c[0] == "agent_status"]
|
|
2494
2505
|
final = [s for s in statuses if s[2] != "running"]
|
|
2495
2506
|
assert len(final) == 1
|
|
2496
|
-
assert final[0][2] == "
|
|
2497
|
-
f"
|
|
2507
|
+
assert final[0][2] == "idle", (
|
|
2508
|
+
f"模型侧报错记成了 {final[0][2]} —— 会累计进隔离配额并最终锁死 agent"
|
|
2498
2509
|
)
|
|
2499
2510
|
|
|
2500
2511
|
|
|
@@ -3310,3 +3321,410 @@ async def test_qoder_stale_resume_clears_only_that_lane(
|
|
|
3310
3321
|
assert "--resume" not in a1
|
|
3311
3322
|
lanes = json.loads((tmp_path / ".meta" / "qoder-sessions.json").read_text())
|
|
3312
3323
|
assert lanes == {"conv_A": "new_A", "conv_B": "keep_B"}
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
# ---------------------------------------------------------------------------
|
|
3327
|
+
# #114 — context-window exhaustion is a PERMANENT failure, not a transient one
|
|
3328
|
+
# ---------------------------------------------------------------------------
|
|
3329
|
+
|
|
3330
|
+
|
|
3331
|
+
_CTX_FULL_MSG = (
|
|
3332
|
+
"Codex ran out of room in the model's context window. "
|
|
3333
|
+
"Start a new thread or clear earlier history before retrying."
|
|
3334
|
+
)
|
|
3335
|
+
|
|
3336
|
+
|
|
3337
|
+
def _codex_turn_failed(msg: str) -> str:
|
|
3338
|
+
"""One codex NDJSON stdout event reporting a failed turn."""
|
|
3339
|
+
return json.dumps({"type": "turn.failed", "error": {"message": msg}})
|
|
3340
|
+
|
|
3341
|
+
|
|
3342
|
+
def _codex_ok(thread_id: str = "thread_fresh") -> list[str]:
|
|
3343
|
+
return [
|
|
3344
|
+
json.dumps({"type": "thread.started", "thread_id": thread_id}),
|
|
3345
|
+
json.dumps({
|
|
3346
|
+
"type": "item.completed",
|
|
3347
|
+
"item": {"type": "agent_message", "text": "ok"},
|
|
3348
|
+
}),
|
|
3349
|
+
]
|
|
3350
|
+
|
|
3351
|
+
|
|
3352
|
+
def _codex_runner(fake: str, ws: FakeWorkspace, outbound: FakeOutbound):
|
|
3353
|
+
return ClaudeRunner(
|
|
3354
|
+
outbound=outbound,
|
|
3355
|
+
workspace=ws,
|
|
3356
|
+
runtime_binaries={"codex": fake},
|
|
3357
|
+
single_wake_timeout=10.0,
|
|
3358
|
+
)
|
|
3359
|
+
|
|
3360
|
+
|
|
3361
|
+
async def test_context_exhausted_starts_a_fresh_session_instead_of_replaying(
|
|
3362
|
+
stateful_fake_claude, tmp_path
|
|
3363
|
+
):
|
|
3364
|
+
"""#114 — a wake that dies because the context window is full must drop
|
|
3365
|
+
the oversized session and re-spawn WITHOUT resume.
|
|
3366
|
+
|
|
3367
|
+
This is the failure that took Dex down on 2026-08-21: the runner treated
|
|
3368
|
+
"ran out of room in the model's context window" as a transient upstream
|
|
3369
|
+
error and replayed the *same* oversized history, which cannot ever fit.
|
|
3370
|
+
Three of those in a row crossed the crash threshold and the agent was
|
|
3371
|
+
quarantined — silently, since a quarantined agent stops being woken.
|
|
3372
|
+
"""
|
|
3373
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3374
|
+
mcp_cfg.write_text("{}")
|
|
3375
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_big")
|
|
3376
|
+
ws = FakeWorkspace(agent_ws)
|
|
3377
|
+
outbound = FakeOutbound()
|
|
3378
|
+
|
|
3379
|
+
fake = stateful_fake_claude([
|
|
3380
|
+
{"stdout": [_codex_turn_failed(_CTX_FULL_MSG)], "stderr": [], "exit_code": 1},
|
|
3381
|
+
{"stdout": _codex_ok("thread_fresh"), "stderr": [], "exit_code": 0},
|
|
3382
|
+
])
|
|
3383
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3384
|
+
|
|
3385
|
+
result = await runner.run_one(
|
|
3386
|
+
"agent-A", {**_make_payload(), "runtime_engine": "codex"}
|
|
3387
|
+
)
|
|
3388
|
+
|
|
3389
|
+
argv0 = _read_argv_n(tmp_path, 0)
|
|
3390
|
+
argv1 = _read_argv_n(tmp_path, 1)
|
|
3391
|
+
# The first attempt resumed the oversized thread…
|
|
3392
|
+
assert "resume" in argv0 and "thread_big" in argv0
|
|
3393
|
+
# …and the retry must abandon it entirely — resuming it again is the bug.
|
|
3394
|
+
assert "resume" not in argv1, (
|
|
3395
|
+
"retry replayed the oversized history; that can only fail again"
|
|
3396
|
+
)
|
|
3397
|
+
assert "thread_big" not in argv1
|
|
3398
|
+
|
|
3399
|
+
# The poisoned id is cleared, so LATER wakes don't resume it either —
|
|
3400
|
+
# without this the agent recovers for one wake and dies on the next.
|
|
3401
|
+
assert agent_ws.clear_last_session_calls == 1
|
|
3402
|
+
assert agent_ws.set_calls == ["thread_fresh"]
|
|
3403
|
+
|
|
3404
|
+
assert result.exit_code == 0
|
|
3405
|
+
final_status = [c for c in outbound.calls if c[0] == "agent_status"][-1]
|
|
3406
|
+
assert final_status == ("agent_status", "agent-A", "idle", 0)
|
|
3407
|
+
|
|
3408
|
+
|
|
3409
|
+
async def test_context_exhausted_is_not_retried_as_a_transient_error(
|
|
3410
|
+
stateful_fake_claude, tmp_path
|
|
3411
|
+
):
|
|
3412
|
+
"""#114 — when even the fresh session overflows, STOP.
|
|
3413
|
+
|
|
3414
|
+
The transient path backs off 8s and re-spawns *with* resume. Waiting does
|
|
3415
|
+
not shrink a context window, so letting an exhausted wake fall through to
|
|
3416
|
+
it burns a third spawn (and a third bill) to reach the same failure.
|
|
3417
|
+
Exactly two spawns may happen: the original and the fresh-session retry.
|
|
3418
|
+
"""
|
|
3419
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3420
|
+
mcp_cfg.write_text("{}")
|
|
3421
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_big")
|
|
3422
|
+
ws = FakeWorkspace(agent_ws)
|
|
3423
|
+
outbound = FakeOutbound()
|
|
3424
|
+
|
|
3425
|
+
fake = stateful_fake_claude([
|
|
3426
|
+
{"stdout": [_codex_turn_failed(_CTX_FULL_MSG)], "stderr": [], "exit_code": 1},
|
|
3427
|
+
{"stdout": [_codex_turn_failed(_CTX_FULL_MSG)], "stderr": [], "exit_code": 1},
|
|
3428
|
+
{"stdout": _codex_ok(), "stderr": [], "exit_code": 0},
|
|
3429
|
+
])
|
|
3430
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3431
|
+
|
|
3432
|
+
await runner.run_one("agent-A", {**_make_payload(), "runtime_engine": "codex"})
|
|
3433
|
+
|
|
3434
|
+
assert (tmp_path / "argv-1.json").exists(), "fresh-session retry must happen"
|
|
3435
|
+
assert not (tmp_path / "argv-2.json").exists(), (
|
|
3436
|
+
"a permanently-failing wake was retried a third time via the "
|
|
3437
|
+
"transient-error backoff path"
|
|
3438
|
+
)
|
|
3439
|
+
|
|
3440
|
+
|
|
3441
|
+
async def test_transient_upstream_error_still_backs_off_and_resumes(
|
|
3442
|
+
stateful_fake_claude, tmp_path, monkeypatch
|
|
3443
|
+
):
|
|
3444
|
+
"""#114 regression guard — narrowing the retry must not disarm it.
|
|
3445
|
+
|
|
3446
|
+
A genuine transient upstream error (rate limit / model at capacity) still
|
|
3447
|
+
has to take the original path: back off, then retry WITH resume so the
|
|
3448
|
+
conversation keeps its history. If this goes red, the fix over-matched.
|
|
3449
|
+
"""
|
|
3450
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3451
|
+
mcp_cfg.write_text("{}")
|
|
3452
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_ok")
|
|
3453
|
+
ws = FakeWorkspace(agent_ws)
|
|
3454
|
+
outbound = FakeOutbound()
|
|
3455
|
+
|
|
3456
|
+
slept: list[float] = []
|
|
3457
|
+
|
|
3458
|
+
async def _no_sleep(seconds: float) -> None:
|
|
3459
|
+
slept.append(seconds)
|
|
3460
|
+
|
|
3461
|
+
monkeypatch.setattr(claude_runner_module.asyncio, "sleep", _no_sleep)
|
|
3462
|
+
|
|
3463
|
+
fake = stateful_fake_claude([
|
|
3464
|
+
{
|
|
3465
|
+
"stdout": [_codex_turn_failed("stream error: model is at capacity")],
|
|
3466
|
+
"stderr": [],
|
|
3467
|
+
"exit_code": 1,
|
|
3468
|
+
},
|
|
3469
|
+
{"stdout": _codex_ok("thread_ok"), "stderr": [], "exit_code": 0},
|
|
3470
|
+
])
|
|
3471
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3472
|
+
|
|
3473
|
+
await runner.run_one("agent-A", {**_make_payload(), "runtime_engine": "codex"})
|
|
3474
|
+
|
|
3475
|
+
assert slept == [8], "transient error must still back off before retrying"
|
|
3476
|
+
argv1 = _read_argv_n(tmp_path, 1)
|
|
3477
|
+
assert "resume" in argv1 and "thread_ok" in argv1, (
|
|
3478
|
+
"a transient failure must keep the session — only exhaustion drops it"
|
|
3479
|
+
)
|
|
3480
|
+
# The session was NOT thrown away for a merely transient hiccup.
|
|
3481
|
+
assert agent_ws.clear_last_session_calls == 0
|
|
3482
|
+
|
|
3483
|
+
|
|
3484
|
+
async def test_read_analyze_context_exhaustion_never_clears_the_owner_session(
|
|
3485
|
+
stateful_fake_claude, tmp_path
|
|
3486
|
+
):
|
|
3487
|
+
"""§7.4 + #114 — a read_analyze wake must not touch the owner's lane.
|
|
3488
|
+
|
|
3489
|
+
read_analyze never resumes, so its own overflow says nothing about the
|
|
3490
|
+
owner's session. Clearing it here would let a read-only round destroy
|
|
3491
|
+
state it is explicitly forbidden to write.
|
|
3492
|
+
"""
|
|
3493
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3494
|
+
mcp_cfg.write_text("{}")
|
|
3495
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_owner")
|
|
3496
|
+
ws = FakeWorkspace(agent_ws)
|
|
3497
|
+
outbound = FakeOutbound()
|
|
3498
|
+
|
|
3499
|
+
fake = stateful_fake_claude([
|
|
3500
|
+
{"stdout": [_codex_turn_failed(_CTX_FULL_MSG)], "stderr": [], "exit_code": 1},
|
|
3501
|
+
{"stdout": _codex_ok(), "stderr": [], "exit_code": 0},
|
|
3502
|
+
])
|
|
3503
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3504
|
+
|
|
3505
|
+
await runner.run_one("agent-A", _ra_payload(runtime_engine="codex"))
|
|
3506
|
+
|
|
3507
|
+
assert agent_ws.clear_last_session_calls == 0
|
|
3508
|
+
assert agent_ws.set_calls == []
|
|
3509
|
+
|
|
3510
|
+
|
|
3511
|
+
async def test_context_exhausted_survives_a_curly_apostrophe(
|
|
3512
|
+
stateful_fake_claude, tmp_path
|
|
3513
|
+
):
|
|
3514
|
+
"""#114 — a pretty-quoted variant of the message is still handled.
|
|
3515
|
+
|
|
3516
|
+
End-to-end guard only: it asserts the wake recovers, without pinning WHICH
|
|
3517
|
+
marker matched. The apostrophe-free property itself is pinned by
|
|
3518
|
+
:func:`test_context_markers_avoid_rewritable_punctuation` — reverse mutation
|
|
3519
|
+
showed this test alone passes via the generic ``context window`` marker, so
|
|
3520
|
+
on its own it does NOT prove the hardening.
|
|
3521
|
+
"""
|
|
3522
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3523
|
+
mcp_cfg.write_text("{}")
|
|
3524
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_big")
|
|
3525
|
+
ws = FakeWorkspace(agent_ws)
|
|
3526
|
+
|
|
3527
|
+
curly = (
|
|
3528
|
+
"Codex ran out of room in the model’s context window. "
|
|
3529
|
+
"Start a new thread or clear earlier history before retrying."
|
|
3530
|
+
)
|
|
3531
|
+
fake = stateful_fake_claude([
|
|
3532
|
+
{"stdout": [_codex_turn_failed(curly)], "stderr": [], "exit_code": 1},
|
|
3533
|
+
{"stdout": _codex_ok("thread_fresh"), "stderr": [], "exit_code": 0},
|
|
3534
|
+
])
|
|
3535
|
+
runner = _codex_runner(fake, ws, FakeOutbound())
|
|
3536
|
+
|
|
3537
|
+
await runner.run_one("agent-A", {**_make_payload(), "runtime_engine": "codex"})
|
|
3538
|
+
|
|
3539
|
+
assert agent_ws.clear_last_session_calls == 1, (
|
|
3540
|
+
"a curly apostrophe defeated the marker — detection is punctuation-fragile"
|
|
3541
|
+
)
|
|
3542
|
+
assert "resume" not in _read_argv_n(tmp_path, 1)
|
|
3543
|
+
|
|
3544
|
+
|
|
3545
|
+
async def test_rate_limit_mentioning_tokens_is_not_read_as_exhaustion(
|
|
3546
|
+
stateful_fake_claude, tmp_path, monkeypatch
|
|
3547
|
+
):
|
|
3548
|
+
"""#114 — pins the deliberately-broad ``context window`` marker.
|
|
3549
|
+
|
|
3550
|
+
Detection is intentionally generous (a missed exhaustion costs a crash
|
|
3551
|
+
loop; a false positive only costs one session's history). "Generous" must
|
|
3552
|
+
still exclude the common transient that talks about tokens and limits —
|
|
3553
|
+
a quota error must keep its session and take the backoff path.
|
|
3554
|
+
"""
|
|
3555
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3556
|
+
mcp_cfg.write_text("{}")
|
|
3557
|
+
agent_ws = FakeAgentWorkspace(mcp_path=mcp_cfg, last_session="thread_ok")
|
|
3558
|
+
ws = FakeWorkspace(agent_ws)
|
|
3559
|
+
|
|
3560
|
+
async def _no_sleep(seconds: float) -> None:
|
|
3561
|
+
return None
|
|
3562
|
+
|
|
3563
|
+
monkeypatch.setattr(claude_runner_module.asyncio, "sleep", _no_sleep)
|
|
3564
|
+
|
|
3565
|
+
quota = (
|
|
3566
|
+
"Rate limit reached: limit 30000 tokens per minute. "
|
|
3567
|
+
"Please try again later."
|
|
3568
|
+
)
|
|
3569
|
+
fake = stateful_fake_claude([
|
|
3570
|
+
{"stdout": [_codex_turn_failed(quota)], "stderr": [], "exit_code": 1},
|
|
3571
|
+
{"stdout": _codex_ok("thread_ok"), "stderr": [], "exit_code": 0},
|
|
3572
|
+
])
|
|
3573
|
+
runner = _codex_runner(fake, ws, FakeOutbound())
|
|
3574
|
+
|
|
3575
|
+
await runner.run_one("agent-A", {**_make_payload(), "runtime_engine": "codex"})
|
|
3576
|
+
|
|
3577
|
+
assert agent_ws.clear_last_session_calls == 0, (
|
|
3578
|
+
"a rate-limit error was misread as context exhaustion — marker too broad"
|
|
3579
|
+
)
|
|
3580
|
+
assert "resume" in _read_argv_n(tmp_path, 1)
|
|
3581
|
+
|
|
3582
|
+
|
|
3583
|
+
def test_context_markers_avoid_rewritable_punctuation():
|
|
3584
|
+
"""#114 — no marker may hinge on a character that gets auto-rewritten.
|
|
3585
|
+
|
|
3586
|
+
Straight apostrophes and quotes are exactly what smart-quote passes
|
|
3587
|
+
rewrite. A marker containing one can be silently defeated by a cosmetic
|
|
3588
|
+
transformation upstream, switching the whole fix off with no signal — the
|
|
3589
|
+
opposite of what this fix is for. Markers must also stay lowercase because
|
|
3590
|
+
matching lowercases the text first; an uppercase marker can never match.
|
|
3591
|
+
"""
|
|
3592
|
+
for marker in ClaudeRunner.CONTEXT_EXHAUSTED_MARKERS:
|
|
3593
|
+
assert "'" not in marker and "\u2019" not in marker, (
|
|
3594
|
+
f"marker {marker!r} hinges on an apostrophe — a smart-quote pass "
|
|
3595
|
+
f"upstream would silently disable context-exhaustion detection"
|
|
3596
|
+
)
|
|
3597
|
+
assert '"' not in marker and "\u201c" not in marker, (
|
|
3598
|
+
f"marker {marker!r} hinges on a quote character"
|
|
3599
|
+
)
|
|
3600
|
+
assert marker == marker.lower(), (
|
|
3601
|
+
f"marker {marker!r} is not lowercase; _is_context_exhausted "
|
|
3602
|
+
f"lowercases the text, so this marker can never match"
|
|
3603
|
+
)
|
|
3604
|
+
|
|
3605
|
+
|
|
3606
|
+
# ---------------------------------------------------------------------------
|
|
3607
|
+
# 2026-08-24 主人拍板:模型侧报错直接回报给人,不计入崩溃配额、不据此隔离
|
|
3608
|
+
# ---------------------------------------------------------------------------
|
|
3609
|
+
|
|
3610
|
+
|
|
3611
|
+
def _platform_payload(**extra):
|
|
3612
|
+
"""一次频道唤醒的 payload —— 带 conversation_id 才谈得上把失败发回频道。"""
|
|
3613
|
+
return {
|
|
3614
|
+
**_make_payload(),
|
|
3615
|
+
"kind": "platform_mention",
|
|
3616
|
+
"conversation_id": "conv_test_1",
|
|
3617
|
+
"session_id": "sess_test_1",
|
|
3618
|
+
"runtime_engine": "codex",
|
|
3619
|
+
**extra,
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
|
|
3623
|
+
async def test_model_side_error_is_not_counted_as_a_crash(fake_claude, tmp_path):
|
|
3624
|
+
"""模型报错必须报 idle —— 报 crashed 就会累计进隔离配额。
|
|
3625
|
+
|
|
3626
|
+
连续 3 次 crashed 会让服务端把 agent 隔离,此后**所有**唤醒被静默丢弃、无人
|
|
3627
|
+
被告知。Dex 2026-08-23 就是这样消失了 4 小时:模型侧的内容安全拦截被当成
|
|
3628
|
+
"这个 agent 坏了"。模型拒绝一次不等于 agent 坏了。
|
|
3629
|
+
"""
|
|
3630
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3631
|
+
mcp_cfg.write_text("{}")
|
|
3632
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3633
|
+
outbound = FakeOutbound()
|
|
3634
|
+
fake = fake_claude(
|
|
3635
|
+
stdout_lines=[_codex_turn_failed(
|
|
3636
|
+
"This content was flagged for possible cybersecurity risk"
|
|
3637
|
+
)],
|
|
3638
|
+
exit_code=1,
|
|
3639
|
+
)
|
|
3640
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3641
|
+
|
|
3642
|
+
await runner.run_one("agent-A", _platform_payload())
|
|
3643
|
+
|
|
3644
|
+
final = [c for c in outbound.calls if c[0] == "agent_status"][-1]
|
|
3645
|
+
assert final[2] == "idle", (
|
|
3646
|
+
f"模型侧报错被记成了 {final[2]} —— 会累计进隔离配额"
|
|
3647
|
+
)
|
|
3648
|
+
|
|
3649
|
+
|
|
3650
|
+
async def test_model_side_error_is_reported_into_the_channel(fake_claude, tmp_path):
|
|
3651
|
+
"""失败必须出现在人看得见的地方,而且要带原始错误文本。
|
|
3652
|
+
|
|
3653
|
+
此前这条路径只写日志和指标(U0.4 的"沉默必须可见"),而日志没有人看——
|
|
3654
|
+
使用者只能靠"叫它没反应"发现,中间隔了几个小时。
|
|
3655
|
+
"""
|
|
3656
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3657
|
+
mcp_cfg.write_text("{}")
|
|
3658
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3659
|
+
outbound = FakeOutbound()
|
|
3660
|
+
flagged = "This content was flagged for possible cybersecurity risk"
|
|
3661
|
+
fake = fake_claude(stdout_lines=[_codex_turn_failed(flagged)], exit_code=1)
|
|
3662
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3663
|
+
|
|
3664
|
+
await runner.run_one("agent-A", _platform_payload())
|
|
3665
|
+
|
|
3666
|
+
sends = [
|
|
3667
|
+
inv for inv in outbound.tool_call_invocations
|
|
3668
|
+
if inv.get("tool_name") == "platform_send"
|
|
3669
|
+
]
|
|
3670
|
+
assert sends, "失败没有发回频道 —— 使用者无从知道它崩了"
|
|
3671
|
+
content = sends[-1]["args"]["content"]
|
|
3672
|
+
assert flagged in content, f"通知里没有原始错误文本:{content}"
|
|
3673
|
+
assert sends[-1]["args"]["conversation_id"] == "conv_test_1"
|
|
3674
|
+
# 诚实口径:这是 daemon 的系统通知,不能冒充成 agent 的回答。
|
|
3675
|
+
assert "系统通知" in content and "不是 agent 的回答" in content
|
|
3676
|
+
# 并且要说清楚它没被隔离 —— 否则人会以为又要去捞它。
|
|
3677
|
+
assert "没有被隔离" in content
|
|
3678
|
+
|
|
3679
|
+
|
|
3680
|
+
async def test_process_level_failure_still_counts_as_a_crash(fake_claude, tmp_path):
|
|
3681
|
+
"""回归护栏:放宽的只是**模型侧**报错,进程级故障的保护不能一并拆掉。
|
|
3682
|
+
|
|
3683
|
+
二进制起不来、auth 文件权限坏这类,重试多少次都一样,仍应计入崩溃配额。
|
|
3684
|
+
"""
|
|
3685
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3686
|
+
mcp_cfg.write_text("{}")
|
|
3687
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3688
|
+
outbound = FakeOutbound()
|
|
3689
|
+
# 没有任何模型事件,只有非零退出 + stderr —— 典型的进程级失败。
|
|
3690
|
+
fake = fake_claude(stdout_lines=[], stderr_lines=["exec: not found"], exit_code=127)
|
|
3691
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3692
|
+
|
|
3693
|
+
await runner.run_one("agent-A", _platform_payload())
|
|
3694
|
+
|
|
3695
|
+
final = [c for c in outbound.calls if c[0] == "agent_status"][-1]
|
|
3696
|
+
assert final[2] == "crashed", (
|
|
3697
|
+
f"进程级失败被记成了 {final[2]} —— 保护被误拆"
|
|
3698
|
+
)
|
|
3699
|
+
# 但它同样要让人看见。
|
|
3700
|
+
sends = [
|
|
3701
|
+
inv for inv in outbound.tool_call_invocations
|
|
3702
|
+
if inv.get("tool_name") == "platform_send"
|
|
3703
|
+
]
|
|
3704
|
+
assert sends, "进程级失败也必须发回频道"
|
|
3705
|
+
assert "进程异常" in sends[-1]["args"]["content"]
|
|
3706
|
+
|
|
3707
|
+
|
|
3708
|
+
async def test_no_conversation_id_means_no_notice_but_never_a_crash_of_the_wake(
|
|
3709
|
+
fake_claude, tmp_path
|
|
3710
|
+
):
|
|
3711
|
+
"""拿不到频道就不发通知 —— 但绝不能因此把这一轮弄挂。"""
|
|
3712
|
+
mcp_cfg = tmp_path / "mcp.json"
|
|
3713
|
+
mcp_cfg.write_text("{}")
|
|
3714
|
+
ws = FakeWorkspace(FakeAgentWorkspace(mcp_path=mcp_cfg, last_session=None))
|
|
3715
|
+
outbound = FakeOutbound()
|
|
3716
|
+
fake = fake_claude(
|
|
3717
|
+
stdout_lines=[_codex_turn_failed("boom")], exit_code=1,
|
|
3718
|
+
)
|
|
3719
|
+
runner = _codex_runner(fake, ws, outbound)
|
|
3720
|
+
|
|
3721
|
+
payload = _platform_payload()
|
|
3722
|
+
payload.pop("conversation_id")
|
|
3723
|
+
result = await runner.run_one("agent-A", payload) # 不抛异常
|
|
3724
|
+
|
|
3725
|
+
assert result is not None
|
|
3726
|
+
sends = [
|
|
3727
|
+
inv for inv in outbound.tool_call_invocations
|
|
3728
|
+
if inv.get("tool_name") == "platform_send"
|
|
3729
|
+
]
|
|
3730
|
+
assert not sends
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.2"
|
|
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.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/delegation_report.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/qoder_credentials.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/tests/integration/test_e2e_deliver_to_spawn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/tests/integration/test_mcp_bridge_round_trip.py
RENAMED
|
File without changes
|
|
File without changes
|
{cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/tests/unit/test_mcp_bridge_agent_retrieval.py
RENAMED
|
File without changes
|
{cloudagent_daemon-0.5.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/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.2 → cloudagent_daemon-0.5.4}/tests/unit/test_workspace_root_task113.py
RENAMED
|
File without changes
|
|
File without changes
|