From 904649533bd5e4376c1dfa3ce2546bef76781adb Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 28 May 2026 09:46:24 +0800 Subject: [PATCH] fix: reduce device pod cli runner friction --- docs/reference/code-agent-chat-readiness.md | 2 + internal/cloud/code-agent-chat.mjs | 4 +- .../code-agent-session-registry.test.mjs | 3 + internal/cloud/codex-stdio-session.mjs | 38 +++--- skills/device-pod-cli/SKILL.md | 12 ++ .../device-pod-cli/assets/device-host-cli.mjs | 119 +++++++++++++++--- tools/device-pod-cli.mjs | 69 ++++++++-- 7 files changed, 202 insertions(+), 45 deletions(-) diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index ffd6cc98..eebc0cb7 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -210,6 +210,8 @@ Workbench 与 Code Agent 的用户请求必须是短连接 submit + 短连接 re Code Agent backend 的 completed 语义只能来自真实 Codex app-server `turn/completed` 成功事件。`item/agentMessage/delta`、`item/completed`、已有 assistant 文本、transport close 或 activity idle timeout 都不能单独升级成 `status: "completed"`。如果已经收到部分 assistant 文本但没有收到 `turn/completed`,终态必须是 timeout/partial blocker,并保留 trace、session、thread、partial output 摘要和可重试提示;Workbench 只能显示“部分回复/超时”,不能标 DEV-LIVE reply pass。 +`Codex app-server transport closed after partial assistant output but before turn/completed` 不能单独判定为 CI/CD 滚动中断。排查必须同时看三类证据:失败 trace 的 `providerTrace`/`runnerTrace`、对应 `hwlab-cloud-api` Pod 的 restart/age、以及同一时间窗口内的 Kubernetes rollout/kill 事件。若 Pod 在 trace 开始前已经稳定且 `RESTARTS=0`,应归类为 Codex app-server transport/session 失败,提示用户用新 session 重试;只有 trace 时间窗口内存在当前会话所在 Pod 的删除、重建或 restart 证据时,才归类为滚动导致的中断。失败响应也必须尽量保留 `providerTrace`,即使 `terminalStatus=failed`,避免前端把“providerTrace 缺失”误报成未知降级。 + cloud-web 同源代理必须把短连接语义原样转发给 cloud-api,至少包括 `Prefer: respond-async`、`X-HWLAB-Short-Connection` 和 `X-Trace-Id`。如果这些 header 在 cloud-web 层被过滤,cloud-api 会把同一个请求当成长同步请求处理,用户入口会表现为 `17666` 卡住或代理超时,而 `17667` 直连 cloud-api 正常。此类问题应先比对同一 trace 在 `17666` 与 `17667` 的 submit 行为,再修代理 header 透传,而不是调大前端等待时间。 `/v1/agent/chat/result/` 是终态摘要接口,不是完整 trace 下载接口。它可以携带压缩后的 `runnerTrace` 窗口用于传输保活,但 Workbench 用户界面不得把该窗口显示为“压缩窗口”或“显示全部”。只要结果或轮询快照声明 `eventsCompacted=true`,前端必须自动请求 `/v1/agent/chat/trace/` 并用完整 trace 替换可视事件线;回放完成前只能显示“完整 trace 回放中/当前已载入”状态。result 响应仍必须保留 `eventCount`、`lastEvent`、`providerTrace`、`threadId/sessionId` 和终态 reply/blocker;完整 trace 只能从 `/v1/agent/chat/trace/`、复制 JSON 或下载 trace 入口取得。默认 result trace 窗口上限由 `HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT` 控制;不要把数百个大 chunk 原样塞进 result 响应,避免 cloud-web 代理层或浏览器 fetch 把“正常执行中的大响应”表现成 503、非 JSON 或空响应。 diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index 7b9b60d7..181f6d4f 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -223,6 +223,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) { if (error.skills !== undefined) payload.skills = error.skills; if (error.runner !== undefined) payload.runner = error.runner; if (error.runnerTrace !== undefined) payload.runnerTrace = error.runnerTrace; + if (error.providerTrace !== undefined) payload.providerTrace = error.providerTrace; if (payload.runnerTrace === undefined) { payload.runnerTrace = traceRecorder.runnerTrace({ runnerKind: CODEX_STDIO_RUNNER_KIND, @@ -917,6 +918,7 @@ async function callCodexStdioRunner({ message, conversationId, sessionId, traceI idleMs: error.idleMs, lastActivityAt: error.lastActivityAt, waitingFor: error.waitingFor, + providerTrace: error.providerTrace, stage: error.stage, missingTools: error.missingTools, availability: await describeCodeAgentAvailability(env, { codexStdioManager: manager, workspace }) @@ -1404,7 +1406,7 @@ function errorTaxonomy(code, error = {}) { layer: "runner", category: "runner_blocked", retryable: true, - userMessage: "Codex stdio runner 执行失败,可稍后重试。" + userMessage: "Codex stdio runner 执行失败;若 trace 显示 transport closed after partial assistant output,请新建 session 重试,并结合 providerTrace、runnerTrace 与 pod restart/rollout 事件判断是否为滚动中断。" }, codex_stdio_network_failed: { layer: "runner", diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index b6127d0c..173a0f3f 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1802,6 +1802,9 @@ test("Codex app-server failed turn is not wrapped as completed assistant output" assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed")); assert.equal(payload.error.message.includes("sk-test-secret"), false); assert.equal(payload.error.message.includes("TOKEN=abc123"), false); + assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio"); + assert.equal(payload.providerTrace.terminalStatus, "failed"); + assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start"); assert.deepEqual(calls.filter((call) => call.method !== "initialize").map((call) => call.method), ["thread/start", "turn/start"]); assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false); await rm(fakeCodex.root, { recursive: true, force: true }); diff --git a/internal/cloud/codex-stdio-session.mjs b/internal/cloud/codex-stdio-session.mjs index 70ece9f5..1fe59fff 100644 --- a/internal/cloud/codex-stdio-session.mjs +++ b/internal/cloud/codex-stdio-session.mjs @@ -527,13 +527,18 @@ export function createCodexStdioSessionManager(options = {}) { waitingFor: "assistant-message" }); if (turnResult.terminalStatus !== "completed") { + const providerTrace = codexStdioProviderTrace({ availability, toolName, turnResult, session }); throw codexStdioError("codex_stdio_failed", redactText(turnResult.terminalError || `Codex app-server turn finished with status=${turnResult.terminalStatus || "unknown"}.`), { availability, session, terminalStatus: turnResult.terminalStatus, threadId: turnResult.threadId, turnId: turnResult.turnId, - appServerExit: turnResult.appServerExit + appServerExit: turnResult.appServerExit, + idleMs: turnResult.idleMs, + lastActivityAt: turnResult.lastActivityAt, + waitingFor: turnResult.waitingFor, + providerTrace }); } const assistantContent = redactText(firstNonEmpty(turnResult.finalResponse, turnResult.assistantText)); @@ -633,19 +638,7 @@ export function createCodexStdioSessionManager(options = {}) { outputTruncated: sidecar.outputTruncated }), capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, - providerTrace: { - transport: "stdio", - protocol: CODEX_APP_SERVER_PROTOCOL, - wireApi: CODEX_APP_SERVER_WIRE_API, - runnerKind: CODEX_STDIO_RUNNER_KIND, - implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, - command: `${availability.command} app-server --listen stdio://`, - toolName, - threadId: turnResult.threadId ?? session.threadId ?? null, - turnId: turnResult.turnId ?? null, - terminalStatus: turnResult.terminalStatus, - valuesPrinted: false - } + providerTrace: codexStdioProviderTrace({ availability, toolName, turnResult, session }) }; } catch (error) { closeRpcClient(); @@ -2035,6 +2028,23 @@ function summarizeAppServerTurn(turnResult) { ].filter(Boolean).join(" "); } +function codexStdioProviderTrace({ availability, toolName, turnResult, session } = {}) { + return { + transport: "stdio", + protocol: CODEX_APP_SERVER_PROTOCOL, + wireApi: CODEX_APP_SERVER_WIRE_API, + runnerKind: CODEX_STDIO_RUNNER_KIND, + implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, + command: `${availability?.command || "/app/node_modules/.bin/codex"} app-server --listen stdio://`, + toolName, + threadId: turnResult?.threadId ?? session?.threadId ?? null, + turnId: turnResult?.turnId ?? null, + terminalStatus: turnResult?.terminalStatus ?? null, + appServerExit: turnResult?.appServerExit ?? null, + valuesPrinted: false + }; +} + function codexAppServerTextInput(text) { return [{ type: "text", text: String(text ?? ""), text_elements: [] }]; } diff --git a/skills/device-pod-cli/SKILL.md b/skills/device-pod-cli/SKILL.md index 0cb4d26c..515fc753 100644 --- a/skills/device-pod-cli/SKILL.md +++ b/skills/device-pod-cli/SKILL.md @@ -36,6 +36,18 @@ not the HWLAB runner contract and cause stale instructions. - If a command returns `profile-missing`, run profile bootstrap. If it returns a device-host-cli JSON failure, fix the named device-pod operation or host CLI; do not bypass the profile with generic gateway shell. +- Workspace search follows a compact ripgrep-like shape: + `device-pod-71-freq:workspace:/ rg "printf|main" projects/01_baseline -g "*.c" -g "*.h" -l`. + The optional path after the pattern is honored as the search root; use it to + avoid scanning vendor PDFs or generated text outside the project under test. +- For debug probe jobs, `download start` is mutating and requires + `--approved --reason`, but `download status ` is read-only and must be + queried without approval friction. If `download status` includes UART capture, + read the top-level `hostUartCapture` field first before falling back to job + files or direct `tran`. +- UART ids are slash-normalized by the CLI, but write them as `uart/1` in + prompts and commands. Do not write `uart / 1`; that form is only tolerated for + recovery from model spacing drift. Fast build smoke for `device-pod-71-freq`: diff --git a/skills/device-pod-cli/assets/device-host-cli.mjs b/skills/device-pod-cli/assets/device-host-cli.mjs index 5d9a71b2..dbfeb128 100644 --- a/skills/device-pod-cli/assets/device-host-cli.mjs +++ b/skills/device-pod-cli/assets/device-host-cli.mjs @@ -16,27 +16,33 @@ function parseGlobalCli(argv) { for (let i = 0; i < argv.length; i += 1) { const item = argv[i]; if (item === '--profile') { - options.profile = argv[i + 1]; + options.profile = cleanArg(argv[i + 1]); i += 1; } else if (item.startsWith('--profile=')) { - options.profile = item.slice('--profile='.length); + options.profile = cleanArg(item.slice('--profile='.length)); } else if (item === '--pod-id') { - options.podId = argv[i + 1]; + options.podId = cleanArg(argv[i + 1]); i += 1; } else if (item.startsWith('--pod-id=')) { - options.podId = item.slice('--pod-id='.length); + options.podId = cleanArg(item.slice('--pod-id='.length)); } else if (item === '--workspace') { - options.workspace = argv[i + 1]; + options.workspace = cleanArg(argv[i + 1]); i += 1; } else if (item.startsWith('--workspace=')) { - options.workspace = item.slice('--workspace='.length); + options.workspace = cleanArg(item.slice('--workspace='.length)); } else { - args.push(item); + args.push(cleanArg(item)); } } return { options, args }; } +function cleanArg(value) { + const text = String(value ?? ''); + if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) return text.slice(1, -1); + return text; +} + function selectedPodId() { return GLOBAL_CLI.options.podId || process.env.DEVICE_POD_ID || DEFAULT_POD_ID; } @@ -182,27 +188,44 @@ function parseArgs(args) { const out = { _: [] }; for (let i = 0; i < args.length; i += 1) { const arg = args[i]; + if (/^-[A-Za-z]$/u.test(arg)) { + const key = arg.slice(1); + const next = args[i + 1]; + if (['g', 't'].includes(key) && next !== undefined && !next.startsWith('-')) { + setOption(out, key, cleanArg(next)); + i += 1; + } else { + setOption(out, key, true); + } + continue; + } if (!arg.startsWith('--')) { - out._.push(arg); + out._.push(cleanArg(arg)); continue; } const eq = arg.indexOf('='); if (eq >= 0) { - out[arg.slice(2, eq)] = arg.slice(eq + 1); + setOption(out, arg.slice(2, eq), cleanArg(arg.slice(eq + 1))); continue; } const key = arg.slice(2); const next = args[i + 1]; if (next !== undefined && !next.startsWith('--')) { - out[key] = next; + setOption(out, key, cleanArg(next)); i += 1; } else { - out[key] = true; + setOption(out, key, true); } } return out; } +function setOption(out, key, value) { + if (out[key] === undefined) out[key] = value; + else if (Array.isArray(out[key])) out[key].push(value); + else out[key] = [out[key], value]; +} + function findFile(candidates) { for (const candidate of candidates.filter(Boolean)) { if (fs.existsSync(candidate)) return candidate; @@ -293,26 +316,82 @@ function walkFiles(root, rel, results, limit = 5000) { } } -function rgWorkspace(pattern, rel) { +function rgWorkspace(pattern, rel, options = {}) { const profile = loadProfile(); const root = resolveWorkspacePath(profile, rel || '.'); const files = []; const stat = fs.statSync(root); if (stat.isFile()) files.push(path.relative(profile.__workspaceRoot, root)); - else walkFiles(root, '.', files); + else { + const nested = []; + const rootRel = path.relative(profile.__workspaceRoot, root) || '.'; + walkFiles(root, '.', nested); + files.push(...nested.map((fileRel) => rootRel === '.' ? fileRel : path.join(rootRel, fileRel))); + } const re = new RegExp(pattern, 'i'); const matches = []; + const fileMatches = []; + const filesWithMatches = Boolean(options.l || options.filesWithMatches || options['files-with-matches'] || options._?.includes('-l')); + const globs = normalizeList(options.g).concat(normalizeList(options.glob), normalizeList(options.iglob)); + const types = normalizeList(options.t).concat(normalizeList(options.type)); for (const fileRel of files) { - if (matches.length >= 100) break; + if (!rgFileAllowed(fileRel, { globs, types })) continue; + if (!filesWithMatches && matches.length >= 100) break; const full = resolveWorkspacePath(profile, fileRel); const buffer = fs.readFileSync(full); if (buffer.includes(0)) continue; const lines = buffer.toString('utf8').split(/\r?\n/u); - lines.forEach((line, index) => { - if (matches.length < 100 && re.test(line)) matches.push({ path: fileRel, line: index + 1, text: line.slice(0, 500) }); - }); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + if (!re.test(line)) continue; + if (filesWithMatches) { + fileMatches.push(fileRel); + break; + } + if (matches.length < 100) matches.push({ path: fileRel, line: index + 1, text: line.slice(0, 500) }); + } } - ok('workspace.rg', { pattern, root: path.relative(profile.__workspaceRoot, root) || '.', count: matches.length, matches }); + ok('workspace.rg', { pattern, root: path.relative(profile.__workspaceRoot, root) || '.', count: filesWithMatches ? fileMatches.length : matches.length, files: filesWithMatches ? fileMatches : undefined, matches: filesWithMatches ? undefined : matches, options: { globs, types, filesWithMatches } }); +} + +function normalizeList(value) { + if (value === undefined || value === false) return []; + return (Array.isArray(value) ? value : [value]).map((item) => String(item)).filter(Boolean); +} + +function rgFileAllowed(fileRel, options = {}) { + const rel = fileRel.replace(/\\/gu, '/'); + if (options.types?.length) { + const ext = path.extname(rel).toLowerCase(); + const allowed = new Set(); + for (const type of options.types) { + if (type === 'c') ['.c', '.h'].forEach((item) => allowed.add(item)); + else if (type === 'cc') ['.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx'].forEach((item) => allowed.add(item)); + } + if (allowed.size && !allowed.has(ext)) return false; + } + if (!options.globs?.length) return true; + return options.globs.some((glob) => matchSimpleGlob(rel, glob)); +} + +function matchSimpleGlob(fileRel, glob) { + const raw = String(glob || '').replace(/\\/gu, '/'); + const target = raw.includes('/') ? fileRel : path.basename(fileRel); + return new RegExp(`^${globToRegExp(raw)}$`, 'iu').test(target); +} + +function globToRegExp(glob) { + let out = ''; + for (let i = 0; i < glob.length; i += 1) { + const char = glob[i]; + if (char === '*' && glob[i + 1] === '*') { + out += '.*'; + i += 1; + } else if (char === '*') out += '[^/]*'; + else if (char === '?') out += '[^/]'; + else out += char.replace(/[.+^${}()|[\]\\]/gu, '\\$&'); + } + return out; } function workspaceApplyPatch(baseRel, options = {}) { @@ -748,7 +827,7 @@ async function serialPorts() { } function resolveUart(profile, uartId, options = {}) { - const normalized = String(uartId || 'uart/1').replace(/^\/+/, ''); + const normalized = String(uartId || 'uart/1').trim().replace(/\\/gu, '/').replace(/\s*\/\s*/gu, '/').replace(/^\/+/, ''); const item = (profile.ioInterface?.uart || []).find((entry) => entry.id === normalized); if (!item) throw new Error(`uart profile not found: ${normalized}`); const resolved = { ...item }; @@ -899,7 +978,7 @@ async function main() { if (group === 'workspace') { if (command === 'ls') return listWorkspace(rest[0] || '.'); if (command === 'cat') return catWorkspace(rest[0], parseArgs(rest.slice(1)).limit); - if (command === 'rg') return rgWorkspace(rest[0], rest[1] || '.'); + if (command === 'rg') return rgWorkspace(rest[0], rest[1] || '.', parseArgs(rest.slice(2))); if (command === 'apply-patch') return workspaceApplyPatch(rest[0] || '.', parseArgs(rest.slice(1))); if (command === 'build') { const sub = rest[0] || 'start'; diff --git a/tools/device-pod-cli.mjs b/tools/device-pod-cli.mjs index eeee6a7f..b4f88a20 100644 --- a/tools/device-pod-cli.mjs +++ b/tools/device-pod-cli.mjs @@ -39,9 +39,47 @@ function parseTarget(raw) { if (!raw || raw === '--help' || raw === 'help') return { kind: 'help' }; if (raw === 'health') return { kind: 'health' }; if (raw === 'profile') return { kind: 'profile' }; - const match = String(raw).match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u); + const match = String(raw).trim().match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u); if (!match) throw new Error(`invalid target selector: ${raw}`); - return { kind: 'invoke', podId: match[1], surface: match[2], path: match[3] || '' }; + return { kind: 'invoke', podId: match[1], surface: match[2], path: normalizeDevicePath(match[3] || '') }; +} + +function normalizeDevicePath(value) { + const normalized = String(value || '') + .trim() + .replace(/\\/gu, '/') + .replace(/\s*\/\s*/gu, '/') + .replace(/\/+/gu, '/') + .replace(/^\/+/u, '') + .replace(/\/+$/u, ''); + return normalized || '.'; +} + +function joinDevicePath(base, child) { + const left = normalizeDevicePath(base); + const right = normalizeDevicePath(child); + if (!right || right === '.') return left; + if (String(child || '').trim().startsWith('/')) return right; + if (!left || left === '.') return right; + return normalizeDevicePath(`${left}/${right}`); +} + +function normalizeProbePath(value) { + return String(value || '').trim().replace(/\\/gu, '/').replace(/\s*\/\s*/gu, '/').replace(/^\/+/u, ''); +} + +const WORKSPACE_COMMANDS = new Set(['ls', 'cat', 'rg', 'apply-patch', 'build']); + +function normalizeWorkspaceSelector(selector, parsed) { + let workspacePath = normalizeDevicePath(selector.path || '.'); + if (parsed._.length >= 2 && WORKSPACE_COMMANDS.has(parsed._[1])) { + const maybePath = String(parsed._[0] || '').trim(); + if (maybePath === '/' || maybePath === '.' || maybePath.startsWith('/') || maybePath.includes('/')) { + workspacePath = joinDevicePath(workspacePath, maybePath); + parsed._ = parsed._.slice(1); + } + } + return workspacePath; } function parseOptions(argv) { @@ -242,20 +280,27 @@ async function buildHostCommand(selector, parsed) { const args = []; const options = hostOptionArgs(parsed); const operation = parsed._[0] || (selector.surface === 'workspace' ? 'ls' : selector.surface === 'debug-probe' ? 'status' : 'read'); - const mutating = isMutating(selector.surface, operation); + const mutating = isMutating(selector.surface, operation, parsed); if (mutating) requireApproval(parsed, `${selector.surface}.${operation}`); if (selector.surface === 'workspace') { - const workspacePath = selector.path.replace(/^\/+/, '') || '.'; - if (operation === 'ls' || operation === 'cat') args.push('workspace', operation, parsed._[1] ? path.posix.join(workspacePath, parsed._[1]) : workspacePath, ...parsed._.slice(2), ...options); - else if (operation === 'rg') args.push('workspace', 'rg', parsed._[1] || '', workspacePath, ...parsed._.slice(2), ...options); - else if (operation === 'apply-patch') args.push('workspace', 'apply-patch', workspacePath, '--patch-b64', Buffer.from(await resolvePatchText(parsed), 'utf8').toString('base64')); + const workspacePath = normalizeWorkspaceSelector(selector, parsed); + if (operation === 'ls' || operation === 'cat') args.push('workspace', operation, joinDevicePath(workspacePath, parsed._[1] || ''), ...parsed._.slice(2), ...options); + else if (operation === 'rg') { + const pattern = parsed._[1] || ''; + const rootArg = parsed._[2] && !String(parsed._[2]).startsWith('-') ? parsed._[2] : ''; + const restStart = rootArg ? 3 : 2; + args.push('workspace', 'rg', pattern, joinDevicePath(workspacePath, rootArg), ...parsed._.slice(restStart), ...options); + } else if (operation === 'apply-patch') { + const baseArg = parsed._[1] && !String(parsed._[1]).startsWith('-') ? parsed._[1] : ''; + args.push('workspace', 'apply-patch', joinDevicePath(workspacePath, baseArg), '--patch-b64', Buffer.from(await resolvePatchText(parsed), 'utf8').toString('base64')); + } else if (operation === 'build') args.push('workspace', 'build', ...(parsed._.slice(1).length ? parsed._.slice(1) : ['start']), ...options); else throw new Error(`unsupported workspace command: ${operation}`); } else if (selector.surface === 'debug-probe') { args.push('debug-probe', operation, ...parsed._.slice(1), ...options); } else if (selector.surface === 'io-probe') { - const probePath = selector.path.replace(/^\/+/, '') || 'uart/1'; + const probePath = normalizeProbePath(selector.path) || 'uart/1'; if (probePath.startsWith('inner/')) throw new Error(`unsupported io-probe path in MVP: ${probePath}`); args.push('io-probe', probePath, operation, ...parsed._.slice(1), ...options); } else { @@ -264,9 +309,9 @@ async function buildHostCommand(selector, parsed) { return { operation, mutating, commandArgs: args }; } -function isMutating(surface, operation) { +function isMutating(surface, operation, parsed = { _: [] }) { return (surface === 'workspace' && operation === 'apply-patch') - || (surface === 'debug-probe' && (operation === 'download' || operation === 'reset' || operation === 'launch-flash')) + || (surface === 'debug-probe' && (operation === 'reset' || operation === 'launch-flash' || (operation === 'download' && !['status', 'wait'].includes(String(parsed._[1] || 'start'))))) || (surface === 'io-probe' && (operation === 'write' || operation === 'read-after-launch-flash')); } function requireApproval(parsed, operation) { @@ -345,10 +390,14 @@ function hostJsonSummary(hostJson) { : {}; const jobId = data.jobId || hostJson.jobId || hostJson.job_id || result.jobId || result.job_id || null; const status = data.status || hostJson.status || result.status || null; + const uartCapture = result.uartCapture || data.uartCapture || hostJson.uartCapture || undefined; return { + hostAction: hostJson.action || null, hostJobId: jobId, hostStatus: status, hostSuccess: typeof hostJson.ok === 'boolean' ? hostJson.ok : result.success, + hostError: hostJson.error || data.error || result.error || undefined, + hostUartCapture: uartCapture, hostResult: result && Object.keys(result).length > 0 ? result : undefined }; }