cloudagent-daemon 0.5.3__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.
Files changed (102) hide show
  1. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/CHANGELOG.md +29 -0
  2. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/PKG-INFO +1 -1
  3. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/pyproject.toml +1 -1
  4. cloudagent_daemon-0.5.4/src/cloudagent_daemon/__init__.py +1 -0
  5. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/claude_runner.py +70 -9
  6. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner.py +143 -5
  7. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/uv.lock +1 -1
  8. cloudagent_daemon-0.5.3/src/cloudagent_daemon/__init__.py +0 -1
  9. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/.gitignore +0 -0
  10. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/LICENSE +0 -0
  11. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/README.md +0 -0
  12. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/scripts/release.sh +0 -0
  13. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/__main__.py +0 -0
  14. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_download.py +0 -0
  15. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_upload.py +0 -0
  16. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/agent_upload_denylist.py +0 -0
  17. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/auth_preflight.py +0 -0
  18. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/cli.py +0 -0
  19. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/config.py +0 -0
  20. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/context_fetch.py +0 -0
  21. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/delegation_report.py +0 -0
  22. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/doctor.py +0 -0
  23. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/frame_dispatch.py +0 -0
  24. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/http_outbound.py +0 -0
  25. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/injection_state.py +0 -0
  26. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/jsonl_logging.py +0 -0
  27. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/login.py +0 -0
  28. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/main_loop.py +0 -0
  29. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/mcp_bridge.py +0 -0
  30. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/metrics.py +0 -0
  31. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/metrics_server.py +0 -0
  32. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/outbound_queue.py +0 -0
  33. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/paths.py +0 -0
  34. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/protocol.py +0 -0
  35. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/qoder_credentials.py +0 -0
  36. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/readonly_bash_guard.py +0 -0
  37. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/runtime_config.py +0 -0
  38. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/runtime_detect.py +0 -0
  39. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/service.py +0 -0
  40. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/spawn_outbox.py +0 -0
  41. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/spawn_queue.py +0 -0
  42. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/sse_client.py +0 -0
  43. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/transport.py +0 -0
  44. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/unix_socket.py +0 -0
  45. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/workspace.py +0 -0
  46. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/src/cloudagent_daemon/ws_client.py +0 -0
  47. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/__init__.py +0 -0
  48. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/_fake_sse_server.py +0 -0
  49. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/conftest.py +0 -0
  50. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_e2e_deliver_to_spawn.py +0 -0
  51. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_e2e_sse.py +0 -0
  52. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_full_e2e.py +0 -0
  53. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_mcp_bridge_round_trip.py +0 -0
  54. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_one_shot_cli.py +0 -0
  55. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_spawn_mcp_bridge_e2e.py +0 -0
  56. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_sse_handshake.py +0 -0
  57. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_sse_watchdog.py +0 -0
  58. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_ws_handshake.py +0 -0
  59. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/integration/test_ws_reconnect.py +0 -0
  60. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/conftest.py +0 -0
  61. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_agent_download.py +0 -0
  62. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_agent_upload.py +0 -0
  63. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_auth_preflight.py +0 -0
  64. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_codex_contract.py +0 -0
  65. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_routing_contract.py +0 -0
  66. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_claude_runner_w3.py +0 -0
  67. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_cli_multi_profile.py +0 -0
  68. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_cli_smoke.py +0 -0
  69. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_config.py +0 -0
  70. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_context_fetch.py +0 -0
  71. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_delegation_report.py +0 -0
  72. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_doctor.py +0 -0
  73. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_frame_dispatch.py +0 -0
  74. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_http_outbound.py +0 -0
  75. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_injection_state.py +0 -0
  76. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_jsonl_logging.py +0 -0
  77. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_login.py +0 -0
  78. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_main_loop_assign.py +0 -0
  79. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_main_loop_preflight.py +0 -0
  80. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_mcp_bridge_agent_retrieval.py +0 -0
  81. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_mcp_bridge_send_message.py +0 -0
  82. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_metrics.py +0 -0
  83. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_metrics_server.py +0 -0
  84. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_multi_tool_wake.py +0 -0
  85. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_outbound_queue.py +0 -0
  86. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_outbox_stuck_watchdog.py +0 -0
  87. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_packaging_mcp_pin.py +0 -0
  88. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_paths.py +0 -0
  89. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_protocol.py +0 -0
  90. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_qoder_credentials.py +0 -0
  91. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_readonly_bash_guard.py +0 -0
  92. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_runtime_detect.py +0 -0
  93. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_service.py +0 -0
  94. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_service_multi_profile.py +0 -0
  95. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_spawn_outbox.py +0 -0
  96. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_spawn_queue.py +0 -0
  97. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_sse_active_watchdog.py +0 -0
  98. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_start_guard.py +0 -0
  99. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_status_stop.py +0 -0
  100. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_workspace.py +0 -0
  101. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_workspace_root_task113.py +0 -0
  102. {cloudagent_daemon-0.5.3 → cloudagent_daemon-0.5.4}/tests/unit/test_ws_client_insecure.py +0 -0
@@ -4,6 +4,35 @@ 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
+
7
36
  ## [0.5.3] — 2026-08-22
8
37
 
9
38
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudagent-daemon
3
- Version: 0.5.3
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.3"
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"
@@ -984,21 +984,79 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
984
984
  )
985
985
  ws.write_last_spawn(rc, spawn_record_session, stderr_tail)
986
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
987
996
  if timeout_hit:
988
997
  final_status = "timeout"
989
- elif attempt.had_api_error:
990
- # M8 — Claude Code swallows upstream Anthropic/Bedrock API
991
- # errors: it prints a synthetic ``isApiErrorMessage:true``
992
- # event and exits rc=0 *without* a terminating ``type=
993
- # result``. Without this branch the wake would be reported
994
- # as ``idle`` (cleanly completed) — leaving the dispatching
995
- # chief silently waiting for a reply that will never come.
996
- final_status = "crashed"
998
+ elif model_side_error:
999
+ final_status = "idle"
997
1000
  elif rc == 0:
998
1001
  final_status = "idle"
999
1002
  else:
1000
1003
  final_status = "crashed"
1001
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
+
1002
1060
  # Wave C #3 — surface terminal status counts for /metrics +
1003
1061
  # doctor. Label-keyed so a single counter renders three
1004
1062
  # series (idle / crashed / timeout). Failure to publish must
@@ -1034,7 +1092,10 @@ The daemon discards stdout — only the mcp tool call reaches the channel.
1034
1092
  payload.get("parent_msg_id") or payload.get("message_id")
1035
1093
  )
1036
1094
  if (
1037
- final_status in ("crashed", "timeout")
1095
+ # model_side_error 现在报 idle(不进崩溃配额),但**这条链仍要通知**:
1096
+ # a2a 的 chief 是在等一条回复,不给它就是永远等下去。状态码用于计数,
1097
+ # 这里用于告知——两件事,不能因为改了前者而丢掉后者。
1098
+ (final_status in ("crashed", "timeout") or model_side_error)
1038
1099
  # This block only fires for task/peer_request/question kinds
1039
1100
  # (WAKE_REPLY_EXPECTED_KINDS, checked below), for which
1040
1101
  # fallback_tool is always send_message — so called_fallback_tool
@@ -2461,11 +2461,22 @@ def _make_dispatch_payload(
2461
2461
  }
2462
2462
 
2463
2463
 
2464
- async def test_isApiErrorMessage_promotes_wake_to_crashed(
2464
+ async def test_isApiErrorMessage_does_not_burn_the_crash_quota(
2465
2465
  fake_claude, tmp_path
2466
2466
  ):
2467
- """M8 — when claude emits ``isApiErrorMessage:true`` and exits rc=0,
2468
- the runner must report ``agent_status="crashed"`` (not ``idle``).
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] == "crashed", (
2497
- f"isApiErrorMessage with rc=0 must report crashed, got {final[0][2]}"
2507
+ assert final[0][2] == "idle", (
2508
+ f"模型侧报错记成了 {final[0][2]} —— 会累计进隔离配额并最终锁死 agent"
2498
2509
  )
2499
2510
 
2500
2511
 
@@ -3590,3 +3601,130 @@ def test_context_markers_avoid_rewritable_punctuation():
3590
3601
  f"marker {marker!r} is not lowercase; _is_context_exhausted "
3591
3602
  f"lowercases the text, so this marker can never match"
3592
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
@@ -122,7 +122,7 @@ wheels = [
122
122
 
123
123
  [[package]]
124
124
  name = "cloudagent-daemon"
125
- version = "0.5.3"
125
+ version = "0.5.4"
126
126
  source = { editable = "." }
127
127
  dependencies = [
128
128
  { name = "h2" },
@@ -1 +0,0 @@
1
- __version__ = "0.5.3"