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.
- package/CLIENT-CONTRACT.md +254 -8
- package/README.md +48 -5
- package/client-ui/index.html +32 -0
- package/client-ui/pc/app.css +89 -1
- package/client-ui/pc/index.html +18 -8
- package/client-ui/pc/js/00-base.js +8 -30
- package/client-ui/pc/js/05-task-list-render.js +4 -1
- package/client-ui/pc/js/07-chat-stream-render.js +2 -85
- package/client-ui/pc/js/10-markdown.js +3 -171
- package/client-ui/pc/js/18-askbox.js +2 -176
- package/client-ui/pc/js/90-glue.js +1 -1
- package/client-ui/pc/js/91-workbench-stubs.js +4 -10
- package/client-ui/pc/js/93-rail.js +10 -3
- package/client-ui/shared/conv/01-dom.js +52 -0
- package/client-ui/shared/conv/02-media-kinds.js +34 -0
- package/client-ui/shared/conv/03-chat-message.js +1561 -0
- package/client-ui/shared/conv/06-ask-answer.js +377 -0
- package/client-ui/shared/conv/07-stream-delegation.js +158 -0
- package/client-ui/{pc/js → shared/conv}/09-stream-apply.js +121 -6
- package/client-ui/shared/conv/10-markdown-render.js +252 -0
- package/client-ui/shared/conv/18-askbox-markup.js +185 -0
- package/client-ui/{pc/js → shared/conv}/19-attachments.js +365 -21
- package/client-ui/shared/conv/27-conv-ledger.js +381 -0
- package/client-ui/{pc/js → shared/conv}/29-msg-variants.js +112 -3
- package/client-ui/shared/conv/README.md +257 -0
- package/client-ui/shared/conv/conv.css +1638 -0
- package/client-ui/shared/conv/dom-map.md +170 -0
- package/client-ui/shared/conv/manifest.json +44 -0
- package/client-ui/shared/conversation.css +368 -190
- package/client-ui/shared/conversation.js +1722 -788
- package/client-ui/shared/host-glue.js +143 -0
- package/client-ui/shared/transport-http.js +130 -6
- package/client-ui-mobile/index.html +2417 -103
- package/cmd/magic-client/clientctl.go +273 -0
- package/cmd/magic-client/main.go +71 -5
- package/go.mod +6 -3
- package/go.sum +4 -1
- package/internal/agent/arkclaw.go +3 -2
- package/internal/agent/arkclaw_stream.go +126 -8
- package/internal/agent/arkclaw_stream_test.go +97 -15
- package/internal/agent/arkclaw_test.go +3 -3
- package/internal/agent/attachments.go +18 -5
- package/internal/agent/attachments_test.go +7 -1
- package/internal/agent/capability_test.go +11 -1
- package/internal/agent/claude.go +96 -158
- package/internal/agent/codebuddy.go +108 -133
- package/internal/agent/codebuddy_gateway.go +8 -4
- package/internal/agent/codebuddy_gateway_stream.go +82 -2
- package/internal/agent/codebuddy_gateway_test.go +236 -1
- package/internal/agent/codebuddy_stream_test.go +262 -0
- package/internal/agent/codex.go +50 -9
- package/internal/agent/codex_stream_test.go +30 -0
- package/internal/agent/codex_test.go +24 -0
- package/internal/agent/custom_models.go +109 -3
- package/internal/agent/custom_models_test.go +74 -0
- package/internal/agent/dsh.go +464 -130
- package/internal/agent/dsh_resume.go +258 -0
- package/internal/agent/dsh_sdk.go +68 -17
- package/internal/agent/dsh_sdk_test.go +102 -2
- package/internal/agent/dsh_test.go +409 -36
- package/internal/agent/engine.go +26 -6
- package/internal/agent/engine_base.go +46 -2
- package/internal/agent/engine_base_test.go +29 -0
- package/internal/agent/engine_test.go +101 -0
- package/internal/agent/env.go +69 -1
- package/internal/agent/itemtracker.go +271 -0
- package/internal/agent/launch.go +192 -0
- package/internal/agent/launch_test.go +206 -0
- package/internal/agent/launch_unix.go +15 -0
- package/internal/agent/launch_windows.go +24 -0
- package/internal/agent/limitnotice.go +103 -0
- package/internal/agent/limitnotice_test.go +211 -0
- package/internal/agent/llmengine.go +2 -2
- package/internal/agent/models.go +3 -2
- package/internal/agent/openclaw.go +4 -3
- package/internal/agent/openclaw_acp.go +7 -0
- package/internal/agent/openclaw_test.go +2 -2
- package/internal/agent/permission.go +100 -11
- package/internal/agent/permission_test.go +100 -36
- package/internal/agent/runcmd.go +13 -3
- package/internal/agent/runner.go +7 -1
- package/internal/agent/runner_test.go +1 -1
- package/internal/agent/stream.go +777 -43
- package/internal/agent/stream_test.go +3 -2
- package/internal/agent/stream_timeline_test.go +905 -0
- package/internal/agent/streamjson.go +36 -2
- package/internal/agent/toolkind.go +20 -1
- package/internal/agent/toolkind_test.go +7 -1
- package/internal/agent/tools_suffix_test.go +10 -3
- package/internal/agent/trae.go +5 -3
- package/internal/agent/workspace_test.go +52 -0
- package/internal/cli/ask.go +260 -33
- package/internal/cli/ask_test.go +15 -11
- package/internal/cli/attach_test.go +32 -20
- package/internal/cli/clientcmd.go +156 -0
- package/internal/cli/events.go +53 -8
- package/internal/cli/events_test.go +230 -0
- package/internal/cli/keepalive_test.go +101 -6
- package/internal/cli/permission_test.go +5 -1
- package/internal/cli/root.go +174 -32
- package/internal/cli/session.go +175 -28
- package/internal/cli/session_test.go +144 -5
- package/internal/cli/sessionlog.go +385 -38
- package/internal/cli/sessionlog_test.go +708 -0
- package/internal/cli/sessionlog_user_test.go +142 -1
- package/internal/cli/sessiontitle.go +141 -0
- package/internal/client/browse.go +456 -0
- package/internal/client/browse_test.go +199 -0
- package/internal/client/cors_test.go +35 -0
- package/internal/client/daemon.go +525 -0
- package/internal/client/daemon_test.go +212 -0
- package/internal/client/detach_unix.go +19 -0
- package/internal/client/detach_windows.go +27 -0
- package/internal/client/pair.go +3 -3
- package/internal/client/plugins.go +376 -0
- package/internal/client/plugins_test.go +223 -0
- package/internal/client/proxy.go +374 -0
- package/internal/client/proxy_test.go +212 -0
- package/internal/client/service.go +1464 -48
- package/internal/client/service_test.go +649 -206
- package/internal/client/session_events_test.go +130 -0
- package/internal/client/wsproxy.go +113 -0
- package/internal/client/wsproxy_test.go +102 -0
- package/internal/session/delete.go +151 -0
- package/internal/session/delete_test.go +103 -0
- package/internal/session/session.go +155 -20
- package/internal/session/session_append.go +79 -21
- package/internal/session/session_test.go +124 -10
- package/internal/session/sessionlog.go +572 -6
- package/internal/session/sessionlog_read_test.go +410 -0
- package/internal/session/title.go +486 -0
- package/internal/session/title_test.go +298 -0
- package/npm/bin/magic-agent.js +101 -10
- package/npm/bin/magic-client.js +118 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/npm/install.js +36 -13
- package/npm/lib/platform.js +27 -5
- package/package.json +8 -3
- package/client-ui/pc/js/03-chat-message.js +0 -880
- package/client-ui/pc/js/06-ask-answer.js +0 -204
- /package/client-ui/{pc/js → shared/conv}/08-ask-event-map.js +0 -0
- /package/client-ui/{pc → shared}/vendor/highlight.min.js +0 -0
- /package/client-ui/{pc → shared}/vendor/marked.min.js +0 -0
- /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
|
-
//
|
|
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 必须按
|
|
223
|
+
// nil ToolsMode 必须按 on 处理(2026-10-01 反转默认)—— magic-agent 的定位是
|
|
224
|
+
// 本机 agent,调用方不显式声明工具 = 想要工具;想关显式传 ToolsOff。
|
|
224
225
|
func TestToolsIsOffDefaultsToOff(t *testing.T) {
|
|
225
|
-
|
|
226
|
-
|
|
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 必须显式禁止「伪造工具返回」。
|
package/internal/agent/trae.go
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
+
}
|