magic-agent 1.0.3 → 1.0.49

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 (153) hide show
  1. package/CLIENT-CONTRACT.md +254 -8
  2. package/README.md +48 -5
  3. package/client-ui/index.html +32 -0
  4. package/client-ui/pc/app.css +89 -1
  5. package/client-ui/pc/index.html +18 -8
  6. package/client-ui/pc/js/00-base.js +8 -30
  7. package/client-ui/pc/js/05-task-list-render.js +4 -1
  8. package/client-ui/pc/js/07-chat-stream-render.js +2 -85
  9. package/client-ui/pc/js/10-markdown.js +3 -171
  10. package/client-ui/pc/js/18-askbox.js +2 -176
  11. package/client-ui/pc/js/90-glue.js +1 -1
  12. package/client-ui/pc/js/91-workbench-stubs.js +4 -10
  13. package/client-ui/pc/js/93-rail.js +10 -3
  14. package/client-ui/shared/conv/01-dom.js +52 -0
  15. package/client-ui/shared/conv/02-media-kinds.js +34 -0
  16. package/client-ui/shared/conv/03-chat-message.js +1561 -0
  17. package/client-ui/shared/conv/06-ask-answer.js +377 -0
  18. package/client-ui/shared/conv/07-stream-delegation.js +158 -0
  19. package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
  20. package/client-ui/shared/conv/10-markdown-render.js +252 -0
  21. package/client-ui/shared/conv/18-askbox-markup.js +185 -0
  22. package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
  23. package/client-ui/shared/conv/27-conv-ledger.js +381 -0
  24. package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
  25. package/client-ui/shared/conv/README.md +257 -0
  26. package/client-ui/shared/conv/conv.css +1638 -0
  27. package/client-ui/shared/conv/dom-map.md +170 -0
  28. package/client-ui/shared/conv/manifest.json +44 -0
  29. package/client-ui/shared/conversation.css +368 -190
  30. package/client-ui/shared/conversation.js +1722 -788
  31. package/client-ui/shared/host-glue.js +143 -0
  32. package/client-ui/shared/transport-http.js +130 -6
  33. package/client-ui-mobile/index.html +2417 -103
  34. package/cmd/magic-client/clientctl.go +273 -0
  35. package/cmd/magic-client/main.go +71 -5
  36. package/go.mod +6 -3
  37. package/go.sum +4 -1
  38. package/internal/agent/arkclaw.go +3 -2
  39. package/internal/agent/arkclaw_stream.go +126 -8
  40. package/internal/agent/arkclaw_stream_test.go +97 -15
  41. package/internal/agent/arkclaw_test.go +3 -3
  42. package/internal/agent/attachments.go +18 -5
  43. package/internal/agent/attachments_test.go +7 -1
  44. package/internal/agent/capability_test.go +11 -1
  45. package/internal/agent/claude.go +96 -158
  46. package/internal/agent/codebuddy.go +108 -133
  47. package/internal/agent/codebuddy_gateway.go +8 -4
  48. package/internal/agent/codebuddy_gateway_stream.go +82 -2
  49. package/internal/agent/codebuddy_gateway_test.go +236 -1
  50. package/internal/agent/codebuddy_stream_test.go +262 -0
  51. package/internal/agent/codex.go +50 -9
  52. package/internal/agent/codex_stream_test.go +30 -0
  53. package/internal/agent/codex_test.go +24 -0
  54. package/internal/agent/custom_models.go +109 -3
  55. package/internal/agent/custom_models_test.go +74 -0
  56. package/internal/agent/dsh.go +464 -130
  57. package/internal/agent/dsh_resume.go +258 -0
  58. package/internal/agent/dsh_sdk.go +68 -17
  59. package/internal/agent/dsh_sdk_test.go +102 -2
  60. package/internal/agent/dsh_test.go +409 -36
  61. package/internal/agent/engine.go +26 -6
  62. package/internal/agent/engine_base.go +46 -2
  63. package/internal/agent/engine_base_test.go +29 -0
  64. package/internal/agent/engine_test.go +101 -0
  65. package/internal/agent/env.go +69 -1
  66. package/internal/agent/itemtracker.go +271 -0
  67. package/internal/agent/launch.go +192 -0
  68. package/internal/agent/launch_test.go +206 -0
  69. package/internal/agent/launch_unix.go +15 -0
  70. package/internal/agent/launch_windows.go +24 -0
  71. package/internal/agent/limitnotice.go +103 -0
  72. package/internal/agent/limitnotice_test.go +211 -0
  73. package/internal/agent/llmengine.go +2 -2
  74. package/internal/agent/models.go +3 -2
  75. package/internal/agent/openclaw.go +4 -3
  76. package/internal/agent/openclaw_acp.go +7 -0
  77. package/internal/agent/openclaw_test.go +2 -2
  78. package/internal/agent/permission.go +100 -11
  79. package/internal/agent/permission_test.go +100 -36
  80. package/internal/agent/runcmd.go +13 -3
  81. package/internal/agent/runner.go +7 -1
  82. package/internal/agent/runner_test.go +1 -1
  83. package/internal/agent/stream.go +777 -43
  84. package/internal/agent/stream_test.go +3 -2
  85. package/internal/agent/stream_timeline_test.go +905 -0
  86. package/internal/agent/streamjson.go +36 -2
  87. package/internal/agent/toolkind.go +20 -1
  88. package/internal/agent/toolkind_test.go +7 -1
  89. package/internal/agent/tools_suffix_test.go +10 -3
  90. package/internal/agent/trae.go +5 -3
  91. package/internal/agent/workspace_test.go +52 -0
  92. package/internal/cli/ask.go +260 -33
  93. package/internal/cli/ask_test.go +15 -11
  94. package/internal/cli/attach_test.go +32 -20
  95. package/internal/cli/clientcmd.go +156 -0
  96. package/internal/cli/events.go +53 -8
  97. package/internal/cli/events_test.go +230 -0
  98. package/internal/cli/keepalive_test.go +101 -6
  99. package/internal/cli/permission_test.go +5 -1
  100. package/internal/cli/root.go +174 -32
  101. package/internal/cli/session.go +175 -28
  102. package/internal/cli/session_test.go +144 -5
  103. package/internal/cli/sessionlog.go +385 -38
  104. package/internal/cli/sessionlog_test.go +708 -0
  105. package/internal/cli/sessionlog_user_test.go +142 -1
  106. package/internal/cli/sessiontitle.go +141 -0
  107. package/internal/client/browse.go +456 -0
  108. package/internal/client/browse_test.go +199 -0
  109. package/internal/client/cors_test.go +35 -0
  110. package/internal/client/daemon.go +525 -0
  111. package/internal/client/daemon_test.go +212 -0
  112. package/internal/client/detach_unix.go +19 -0
  113. package/internal/client/detach_windows.go +27 -0
  114. package/internal/client/pair.go +3 -3
  115. package/internal/client/plugins.go +376 -0
  116. package/internal/client/plugins_test.go +223 -0
  117. package/internal/client/proxy.go +374 -0
  118. package/internal/client/proxy_test.go +212 -0
  119. package/internal/client/service.go +1464 -48
  120. package/internal/client/service_test.go +649 -206
  121. package/internal/client/session_events_test.go +130 -0
  122. package/internal/client/wsproxy.go +113 -0
  123. package/internal/client/wsproxy_test.go +102 -0
  124. package/internal/session/delete.go +151 -0
  125. package/internal/session/delete_test.go +103 -0
  126. package/internal/session/session.go +155 -20
  127. package/internal/session/session_append.go +79 -21
  128. package/internal/session/session_test.go +124 -10
  129. package/internal/session/sessionlog.go +572 -6
  130. package/internal/session/sessionlog_read_test.go +410 -0
  131. package/internal/session/title.go +486 -0
  132. package/internal/session/title_test.go +298 -0
  133. package/npm/bin/magic-agent.js +101 -10
  134. package/npm/bin/magic-client.js +118 -0
  135. package/npm/dist/darwin-arm64/magic-agent +0 -0
  136. package/npm/dist/darwin-arm64/magic-client +0 -0
  137. package/npm/dist/darwin-x64/magic-agent +0 -0
  138. package/npm/dist/darwin-x64/magic-client +0 -0
  139. package/npm/dist/linux-arm64/magic-agent +0 -0
  140. package/npm/dist/linux-arm64/magic-client +0 -0
  141. package/npm/dist/linux-x64/magic-agent +0 -0
  142. package/npm/dist/linux-x64/magic-client +0 -0
  143. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  144. package/npm/dist/win32-x64/magic-client.exe +0 -0
  145. package/npm/install.js +36 -13
  146. package/npm/lib/platform.js +27 -5
  147. package/package.json +8 -3
  148. package/client-ui/pc/js/03-chat-message.js +0 -880
  149. package/client-ui/pc/js/06-ask-answer.js +0 -204
  150. /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
  151. /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
  152. /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
  153. /package/client-ui/{pc → shared}/vendor/purify.min.js +0 -0
@@ -15,6 +15,7 @@ package agent
15
15
  import (
16
16
  "context"
17
17
  "encoding/json"
18
+ "errors"
18
19
  "fmt"
19
20
  "io"
20
21
  "strings"
@@ -33,15 +34,19 @@ type streamJSONResult struct {
33
34
  // runStreamJSONIn 跑一次 stream-json 调用:增量喂给 acc,收尾行填进 fin。
34
35
  // 返回是否见到了 result 行(未见 = 协议没跑完,调用方按错误处理)。
35
36
  //
37
+ // stderrOut 非空时把 CLI 的 stderr 尾巴收进它 —— 调用方在「没见到 result 行」时
38
+ // 要拿它拼错误(见 streamNoResultErr)。
39
+ //
36
40
  // stdin 为 nil 时不喂 stdin(提示词走命令行位置参数)。每见到一次 result 行,
37
41
  // 除填 fin 外还会向调用方发一个 KindTurnEnd 事件 —— 常驻会话(Request.Append)
38
42
  // 靠它判断「这轮做完了,可以继续追加」。
39
43
  func runStreamJSONIn(
40
44
  ctx context.Context, dir string, extraEnv []string, bin string, args []string,
41
45
  stdin io.Reader, acc *streamAccumulator, fin *streamJSONResult,
46
+ stderrOut *streamStderrBuf,
42
47
  ) (bool, error) {
43
48
  seen := false
44
- err := runStreamCLIStdinReaderIn(ctx, dir, extraEnv, bin, args, stdin, func(line string) error {
49
+ err := runStreamCLIStdinReaderIn(ctx, dir, extraEnv, bin, args, stdin, stderrOut, func(line string) error {
45
50
  isResult, perr := acc.handleNDJSONLine(line)
46
51
  if perr != nil {
47
52
  return perr
@@ -49,6 +54,20 @@ func runStreamJSONIn(
49
54
  if isResult {
50
55
  _ = json.Unmarshal([]byte(line), fin)
51
56
  seen = true
57
+ /* ⚠️ **先判「正文是不是供应商的通告」再发轮次边界**(2026-10-10,见 limitnotice.go):
58
+ 发 turn_end 等于告诉消费方「这一轮成功收尾」—— 先发出去,界面上就成了
59
+ 「正文 + 额度通告 + 已完成」的假成功(用户报障原话:「codebuddy ai 没有额度了
60
+ 没有输出一个独立的错误消息用于提示吗 现在混在回复里了」)。
61
+ 命中时改发 turn_failed(原因 = 通告原文,Text = 剥掉通告的正文),并把错误
62
+ 抛给调用方 —— 与「没吐 result 行」走**同一个收口**(CLI 据此写失败信封、
63
+ 非零退出;消费方不必自己判)。
64
+ ⚠️ 抛错会让 lineHandler 中止读循环,那里会顺手 kill 掉引擎进程组
65
+ (见 runStreamCLIStdinReaderIn),所以这里不能改成「先把事件转发出去、
66
+ 等收尾再抛」—— 那样 turn_failed 会晚到,消费方早就按 turn_end 收尾了。 */
67
+ if notice, body, hit := splitLimitNotice(fin.Result); hit {
68
+ acc.emitLimitNoticeFailed(fin.SessionID, notice, body)
69
+ return errors.New(notice)
70
+ }
52
71
  // 轮次边界事件(Text = 该轮最终正文,SessionID = 该轮会话 id)。
53
72
  // ⚠️ 它必须带 session_id:常驻会话的最终 result 信封要等会话结束才输出
54
73
  //(默认空闲 5m),调用方要靠这一条把会话锚点绑上(见 StreamEvent.SessionID)。
@@ -141,7 +160,7 @@ func hasImageAttachment(atts []Attachment) bool {
141
160
  // setArgValue 把 args 里 <flag> 的下一个值改成 want(找不到就追加 <flag> want)。
142
161
  // claude/codebuddy 的 --output-format json → stream-json 就靠它。
143
162
  func setArgValue(args []string, flag, want string) []string {
144
- for i := 0; i < len(args)-1; i++ {
163
+ for i := 0; len(args) > 0 && i < len(args)-1; i++ {
145
164
  if args[i] == flag {
146
165
  args[i+1] = want
147
166
  return args
@@ -183,3 +202,18 @@ func streamJSONArgs(baseArgs []string, prompt string, streaming bool) []string {
183
202
  func errEmptyStreamContent(label string) error {
184
203
  return fmt.Errorf("%s CLI returned empty result", label)
185
204
  }
205
+
206
+ // streamNoResultErr 「协议没跑完」的错误:把 CLI 自己的 stderr 尾巴带上。
207
+ //
208
+ // 为什么必须带(2026-10-09 加):这条错误意味着 CLI **没走完协议就退出了**
209
+ // —— 自己崩了 / 被上游拒了 / 鉴权或额度失败,而真正的原因几乎只出现在它的
210
+ // stderr 里。不带的话用户和排障者永远只看到一句
211
+ // 「stream ended without result line」,无从下手(用户报障截图就是这个)。
212
+ //
213
+ // stderr 为空(典型:被魔改的 CLI 什么都不打)时退回原句,不拼空摘要。
214
+ func streamNoResultErr(label string, stderr string) error {
215
+ if s := stderrSummary(stderr); s != "" {
216
+ return fmt.Errorf("%s CLI stream ended without result line;CLI 最后输出:%s", label, s)
217
+ }
218
+ return fmt.Errorf("%s CLI stream ended without result line", label)
219
+ }
@@ -31,7 +31,20 @@ const (
31
31
  ToolKindInputRequest = "input_request"
32
32
  // ToolKindPermission 请求授权(权限确认那一类,会渲染成审批卡)。
33
33
  ToolKindPermission = "permission"
34
- // ToolKindToolCall 其余**已知**工具(读文件 / 列目录 / 待办…)。
34
+ // ToolKindTodo 待办清单(Claude 系 TodoWrite / Codex 系 update_plan 一类)。
35
+ //
36
+ // ⚠️ 这一档**不在**本文件上面那份「与 anywhere 的 ToolContentKind 对齐」的词表里
37
+ //(command / file_change / web_search / mcp / agent_call / input_request /
38
+ // permission / tool_call / unknown),是**本仓库补的**:anywhere 把待办画成 timeline 的
39
+ // `todo-row`,并没有一个同名的 ToolContentKind。
40
+ // 补它的理由:待办清单跨引擎高频出现,而且是唯一「形态判据比名字更可靠」的一类 ——
41
+ // 清单永远装在参数里(`todos` / `plan`),各家工具名却各不相同。所以两侧分工固定:
42
+ // **这里按名字判定**,名字表没见过的引擎由前端按参数兜底(见 29-msg-variants 的 qaToolKindOf)。
43
+ // ⚠️ 加这一档要同时改三处,缺一就是「服务端给了、前端不认」:
44
+ // ① 这里(含下面的名字表);② toolkind_test.go 的契约表;
45
+ // ③ 前端 29-msg-variants.js 的 qaToolKindMerge 映射表。
46
+ ToolKindTodo = "todo"
47
+ // ToolKindToolCall 其余**已知**工具(读文件 / 列目录…)。
35
48
  //
36
49
  // ⚠️ 认不出来也归这里,**不归 unknown** —— 与 anywhere 的 claude projector 一致
37
50
  //(未知工具名 → 通用 ToolCallContent):它是「有一次工具调用但形态未细分」,
@@ -64,6 +77,12 @@ var toolKindByName = map[string]string{
64
77
  // 子代理
65
78
  "task": ToolKindAgentCall, "agent": ToolKindAgentCall, "subagent": ToolKindAgentCall,
66
79
  "delegate": ToolKindAgentCall,
80
+ // 待办清单(Claude 系 TodoWrite,Codex 系 update_plan,以及各家的等价名)。
81
+ // ⚠️ 本函数**只看名字**(见下方 args 参数说明)—— 参数里的 todos / plan 由前端
82
+ // qaToolKindOf 兜底,两侧词表必须一致,别只改一边。
83
+ "todowrite": ToolKindTodo, "todo_write": ToolKindTodo, "todo__write": ToolKindTodo,
84
+ "update_plan": ToolKindTodo, "plan_write": ToolKindTodo,
85
+ "write_todos": ToolKindTodo, "writetodos": ToolKindTodo,
67
86
  // 提问 / 要输入
68
87
  "askuserquestion": ToolKindInputRequest, "ask_user_question": ToolKindInputRequest,
69
88
  "elicit": ToolKindInputRequest, "input_request": ToolKindInputRequest,
@@ -41,11 +41,17 @@ func TestToolKindFor(t *testing.T) {
41
41
  {"builtin.Bash", ToolKindCommand},
42
42
  {"tools.Edit", ToolKindFileChange},
43
43
  {"acp.Task", ToolKindAgentCall},
44
+ // 待办清单(TodoWrite / update_plan → 本仓库补的一档,见 toolkind.go 的 ToolKindTodo)
45
+ {"TodoWrite", ToolKindTodo},
46
+ {"todo_write", ToolKindTodo},
47
+ {"update_plan", ToolKindTodo},
48
+ {"write_todos", ToolKindTodo},
49
+ // 命名空间形状的待办工具:剥前缀后同样要查得出来(引擎给的可能是 openclaw.TodoWrite)
50
+ {"builtin.TodoWrite", ToolKindTodo},
44
51
  // 已知但未细分的工具 → tool_call(对齐 anywhere 的通用 ToolCallContent)
45
52
  {"Read", ToolKindToolCall},
46
53
  {"Glob", ToolKindToolCall},
47
54
  {"Grep", ToolKindToolCall},
48
- {"TodoWrite", ToolKindToolCall},
49
55
  {"exit_plan_mode", ToolKindToolCall},
50
56
  // 从没见过的名字也归 tool_call,**不是** unknown
51
57
  {"some_brand_new_tool", ToolKindToolCall},
@@ -220,10 +220,14 @@ func hasArgToken(args, want string) bool {
220
220
  return false
221
221
  }
222
222
 
223
- // nil ToolsMode 必须按 off 处理(默认关工具,向后兼容)。
223
+ // nil ToolsMode 必须按 on 处理(2026-10-01 反转默认)—— magic-agent 的定位是
224
+ // 本机 agent,调用方不显式声明工具 = 想要工具;想关显式传 ToolsOff。
224
225
  func TestToolsIsOffDefaultsToOff(t *testing.T) {
225
- if !toolsIsOff(Request{}) {
226
- t.Error("空 Tools 应视为 off")
226
+ /* 旧断言是「空 Tools 应视为 off」—— 那是早期 magic-agent 当 chat 套壳的
227
+ 口径。2026-10-01 用户报障「claude 只闲聊不读文件」就是这个默认兜底。
228
+ 本测试改名 + 反转断言,钉住新默认。 */
229
+ if toolsIsOff(Request{}) {
230
+ t.Error("空 Tools 在 2026-10-01 反转后视为 on(magic-agent 默认开工具)")
227
231
  }
228
232
  if toolsIsOff(Request{Tools: ToolsOn}) {
229
233
  t.Error("ToolsOn 不应判定为 off")
@@ -231,6 +235,9 @@ func TestToolsIsOffDefaultsToOff(t *testing.T) {
231
235
  if toolsIsOff(Request{Tools: ToolsAllowlist([]string{"Read"})}) {
232
236
  t.Error("白名单不应判定为 off")
233
237
  }
238
+ if !toolsIsOff(Request{Tools: ToolsOff}) {
239
+ t.Error("显式 ToolsOff 仍应判定为 off(兜底反转不影响显式传值)")
240
+ }
234
241
  }
235
242
 
236
243
  // noToolSuffix 必须显式禁止「伪造工具返回」。
@@ -45,8 +45,8 @@ type TraeEngine struct {
45
45
  // Name 实现 Engine。
46
46
  func (e *TraeEngine) Name() string { return "trae" }
47
47
 
48
- // DefaultTraeTimeout 单次尝试默认超时。
49
- const DefaultTraeTimeout = 10 * time.Minute
48
+ // DefaultTraeTimeout 单次尝试默认超时(2026-10-10 由 10 分钟统一上调到 1 小时)。
49
+ const DefaultTraeTimeout = time.Hour
50
50
 
51
51
  // traeOffDisableTools off 模式下逐个 --disallowed-tool 禁用的实体工具。
52
52
  //
@@ -302,7 +302,9 @@ func (e *TraeEngine) Stream(ctx context.Context, req Request, onEvent func(Strea
302
302
  return StreamResult{}, err
303
303
  }
304
304
  if !seenResult {
305
- return StreamResult{}, fmt.Errorf("trae CLI stream ended without result line")
305
+ err := fmt.Errorf("trae CLI stream ended without result line")
306
+ acc.emitTurnFailed(acc.SessionID, err)
307
+ return StreamResult{}, err
306
308
  }
307
309
  if fin.IsError {
308
310
  return StreamResult{}, fmt.Errorf("trae CLI error (subtype=%s): %s", fin.Subtype, truncateStr(fin.Result, 500))
@@ -164,3 +164,55 @@ func TestCodexOmitsCDWithoutWorkspace(t *testing.T) {
164
164
  t.Errorf("未指定 workspace 时不该有 -C:%s", args)
165
165
  }
166
166
  }
167
+
168
+ // codexPwdStub 造一个假 codex:既把 pwd 与收到的参数写进 log,又回放 codex 形状的 NDJSON
169
+ // (pwdStub 那份回的是 claude 的 result 信封,codex 的 parseStdout 认不出来)。
170
+ func codexPwdStub(t *testing.T, dir string) (bin, log string) {
171
+ t.Helper()
172
+ log = filepath.Join(dir, "ws.log")
173
+ bin = filepath.Join(dir, "codex")
174
+ script := "#!/bin/sh\n" +
175
+ "{ pwd; printf 'args: %s\\n' \"$*\"; } > " + log + "\n" +
176
+ `printf '%s\n' '{"type":"thread.started","thread_id":"th-1"}'` + "\n" +
177
+ `printf '%s\n' '{"type":"item.completed","item":{"type":"agent_message","text":"ok"}}'` + "\n"
178
+ if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
179
+ t.Fatal(err)
180
+ }
181
+ return bin, log
182
+ }
183
+
184
+ // 续接轮(`exec resume`)**不能带 `-C`** —— 2026-10-18 线上事故。
185
+ //
186
+ // codex 0.162.0 的 resume 子命令参数表里没有 `-C`(也没有 `-m`):传了 clap 直接
187
+ // `error: unexpected argument '-C' found` + exit 2、stdout 全空,core 侧只能报
188
+ // 「codex CLI returned empty result · 总耗时 0.0s」(用户手机上那条「继续」就是这么死的)。
189
+ // 工作目录改由**子进程 cwd** 承担(本用例同时钉住这点),sandbox 根延用首次会话。
190
+ func TestCodexResumeOmitsCDButKeepsCwd(t *testing.T) {
191
+ dir := t.TempDir()
192
+ ws := filepath.Join(dir, "proj")
193
+ if err := os.MkdirAll(ws, 0o755); err != nil {
194
+ t.Fatal(err)
195
+ }
196
+ bin, log := codexPwdStub(t, dir)
197
+ e := &CodexEngine{BinPath: bin}
198
+ if _, err := e.Complete(context.Background(), Request{
199
+ SessionID: "th-1",
200
+ Workspace: ws,
201
+ Messages: []Message{{Role: "user", Content: "继续"}},
202
+ }); err != nil {
203
+ t.Fatalf("Complete: %v", err)
204
+ }
205
+ data, _ := readFile(log)
206
+ out := string(data)
207
+ if strings.Contains(out, "-C ") {
208
+ t.Errorf("续接轮不该有 -C(codex exec resume 不认它):%s", out)
209
+ }
210
+ if !strings.Contains(out, "exec resume th-1") {
211
+ t.Errorf("应走 exec resume 子命令:%s", out)
212
+ }
213
+ // cwd 仍要落在 workspace —— 这是续接轮唯一的目录表达方式。
214
+ first := strings.SplitN(out, "\n", 2)[0]
215
+ if !samePath(t, strings.TrimSpace(first), ws) {
216
+ t.Errorf("续接轮子进程 cwd = %q,期望 %q", strings.TrimSpace(first), ws)
217
+ }
218
+ }