diff --git a/internal/cloud/code-agent-session-registry.mjs b/internal/cloud/code-agent-session-registry.mjs index 9950e7f4..897249d6 100644 --- a/internal/cloud/code-agent-session-registry.mjs +++ b/internal/cloud/code-agent-session-registry.mjs @@ -209,18 +209,19 @@ export function createCodeAgentSessionRegistry(options = {}) { const session = findSession(params); const conversationId = optionalId(params.conversationId) ?? session?.conversationId ?? session?.conversationIds?.[0] ?? null; const facts = conversationId ? getConversationFacts(conversationId) : null; + const factsFound = hasConversationFacts(facts); const requestedTraceId = optionalId(params.traceId); const traceIds = uniqueStrings([ - requestedTraceId, facts?.latestTraceId, session?.currentTraceId, session?.lastTraceId, ...(Array.isArray(facts?.traceIds) ? facts.traceIds : []) ]); + const found = Boolean(session || factsFound || traceIds.length > 0); return { - ok: Boolean(session || facts || traceIds.length > 0), + ok: found, action: "code-agent.session.inspect", - status: session || facts ? "found" : "not_found", + status: found ? "found" : "not_found", query: { conversationId: optionalId(params.conversationId), sessionId: optionalId(params.sessionId), @@ -613,6 +614,17 @@ function uniqueStrings(values) { return [...new Set(values.map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean))]; } +function hasConversationFacts(facts) { + return Boolean(facts && ( + facts.sessionId || + facts.sessionStatus || + facts.latestTraceId || + Number(facts.turnCount) > 0 || + (Array.isArray(facts.traceIds) && facts.traceIds.length > 0) || + (Array.isArray(facts.facts) && facts.facts.length > 0) + )); +} + function positiveInteger(value, fallback) { return Number.isInteger(value) && value > 0 ? value : fallback; } diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 26d885aa..3ff2fc07 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -259,6 +259,17 @@ test("code agent session registry inspect locates conversation, session, and tra const byTrace = registry.inspect({ traceId: "trc_inspect_registry_1" }); assert.equal(byTrace.ok, true); assert.equal(byTrace.session.sessionId, "ses_inspect_registry"); + + const missingTrace = registry.inspect({ traceId: "trc_inspect_registry_missing" }); + assert.equal(missingTrace.ok, false); + assert.equal(missingTrace.status, "not_found"); + assert.deepEqual(missingTrace.traceIds, []); + + const missing = registry.inspect({ conversationId: "cnv_inspect_registry_missing" }); + assert.equal(missing.ok, false); + assert.equal(missing.status, "not_found"); + assert.equal(missing.conversationFacts.turnCount, 0); + assert.deepEqual(missing.traceIds, []); }); test("code agent session registry reports busy sessions as structured blocker", () => { diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index 107feab7..5e3004d0 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -1604,8 +1604,10 @@ async function handleCodeAgentInspectHttp(request, response, url, options) { : { ok: false, status: "unavailable", traceIds: [], session: null, conversationFacts: null }; const session = matchedManagerSession ?? registryInspect.session ?? null; const conversationFacts = registryInspect.conversationFacts ?? null; + const requestedRunnerTrace = query.traceId ? traceStore.snapshot(query.traceId) : null; + const requestedTraceFound = Boolean(requestedRunnerTrace && requestedRunnerTrace.status !== "missing"); const traceIds = uniqueStrings([ - query.traceId, + requestedTraceFound ? query.traceId : null, session?.currentTraceId, session?.lastTraceId, conversationFacts?.latestTraceId, @@ -1613,7 +1615,11 @@ async function handleCodeAgentInspectHttp(request, response, url, options) { ...(Array.isArray(registryInspect.traceIds) ? registryInspect.traceIds : []) ]); const latestTraceId = traceIds[0] ?? null; - const runnerTrace = latestTraceId ? traceStore.snapshot(latestTraceId) : null; + const runnerTrace = latestTraceId + ? latestTraceId === query.traceId && requestedRunnerTrace + ? requestedRunnerTrace + : traceStore.snapshot(latestTraceId) + : null; const found = Boolean(registryInspect.ok || session || latestTraceId); sendJson(response, found ? 200 : 404, { ok: found, diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index 19bf57eb..23c89b08 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -2005,6 +2005,29 @@ test("cloud api /v1/agent/chat/inspect maps conversation session and thread deta } }); +test("cloud api /v1/agent/chat/inspect returns not_found for empty conversation facts", async () => { + const server = createCloudApiServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + + try { + const { port } = server.address(); + const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect-missing&sessionId=ses_server_test_missing&threadId=019e6c72-2373-75e3-9856-eff286db7163&traceId=trc_server_test_missing`); + assert.equal(response.status, 404); + const body = await response.json(); + assert.equal(body.ok, false); + assert.equal(body.status, "not_found"); + assert.equal(body.latestTraceId, null); + assert.deepEqual(body.traceIds, []); + assert.equal(body.traceUrl, null); + assert.equal(body.resultUrl, null); + assert.equal(body.conversationFacts.turnCount, 0); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }); + } +}); + test("cloud api result polling compacts large runnerTrace while preserving providerTrace", async () => { const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-")); const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-codex-home-")); diff --git a/skills/device-pod-cli/SKILL.md b/skills/device-pod-cli/SKILL.md index a8c4e859..7a4f5d9d 100644 --- a/skills/device-pod-cli/SKILL.md +++ b/skills/device-pod-cli/SKILL.md @@ -45,6 +45,11 @@ not the HWLAB runner contract and cause stale instructions. commands in order. Do not discard the failure context, slash the route into an ad hoc command, or replace `device-pod-cli` with direct gateway shell unless the `next` field explicitly instructs that bootstrap/repair action. +- Before rerouting, gateway repair, or host asset repair, run + `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id --probe`. + If it reports `gateway-session-mismatch`, `gateway-session-unavailable`, or + `host-cmd-unavailable`, report that blocker and its `next` field; do not keep + retrying `/app/tools/hwlab-gateway-tran.mjs` or raw gateway shell commands. - 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 @@ -88,9 +93,9 @@ For `device-pod-71-00075-11`, the required order is: `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11`. 2. Create `/workspace/hwlab/.device-pod/device-pod-71-00075-11.json` with `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force`. -3. Upload `/app/skills/device-pod-cli/assets/device-host-cli.mjs` and the new - profile to `gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11` using - `/app/tools/tran.mjs`. +3. Run `doctor --pod-id device-pod-71-00075-11 --probe` and follow its + `next.uploadHostCli`, `next.uploadProfile`, and `next.hostHealth` commands; + do not hard-code a gateway session id from old examples. 4. Verify Windows host health through `node tools\device-host-cli.mjs --profile .device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health`. @@ -118,9 +123,8 @@ directory. ```sh node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /workspace/hwlab/.device-pod/device-pod-71-00075-11.json .device-pod/device-pod-71-00075-11.json -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\device-host-cli.mjs --profile .device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health +node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-00075-11 --probe +# Follow the JSON next.uploadHostCli / next.uploadProfile / next.hostHealth commands from doctor. node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11 node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-00075-11:workspace:/ ls --dry-run @@ -185,7 +189,7 @@ cat > "$profile_file" <<'JSON' ] }, "cloudApiUrl": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "gatewaySessionId": "gws_DESKTOP-1MHOD9I", + "gatewaySessionId": "gws_YUAN", "resourceId": "res_windows_host", "capabilityId": "cap_windows_cmd_exec", "hostWorkspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11", @@ -194,9 +198,8 @@ cat > "$profile_file" <<'JSON' JSON sha256sum "$profile_file" -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload "$profile_file" .device-pod/device-pod-71-00075-11.json -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\device-host-cli.mjs --profile .device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health +node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-00075-11 --probe +# Follow the JSON next.uploadHostCli / next.uploadProfile / next.hostHealth commands from doctor. node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11 node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list @@ -309,7 +312,7 @@ cat > "$profile_file" <<'JSON' ] }, "cloudApiUrl": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "gatewaySessionId": "gws_DESKTOP-1MHOD9I", + "gatewaySessionId": "gws_YUAN", "resourceId": "res_windows_host", "capabilityId": "cap_windows_cmd_exec", "hostWorkspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11", @@ -318,9 +321,8 @@ cat > "$profile_file" <<'JSON' JSON node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11 -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload "$profile_file" .device-pod/device-pod-71-00075-11.json -node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\\device-host-cli.mjs --profile .device-pod\\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health +node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-00075-11 --probe +# Follow the JSON next.uploadHostCli / next.uploadProfile / next.hostHealth commands from doctor. ``` After bootstrap, validate selector isolation before touching hardware: diff --git a/tools/device-pod-cli.mjs b/tools/device-pod-cli.mjs index 726c337a..02f08af8 100644 --- a/tools/device-pod-cli.mjs +++ b/tools/device-pod-cli.mjs @@ -32,6 +32,9 @@ function classifyBlocker(message) { if (/profile not found/u.test(message)) return 'profile-missing'; if (/profile invalid|requires/u.test(message)) return 'profile-invalid'; if (/approval/u.test(message)) return 'approval-required'; + if (/resource is not registered under the requested gateway session|resourceGatewaySessionId/u.test(message)) return 'gateway-session-mismatch'; + if (/gateway session .*not found|gateway session .*not registered|gateway.*not.*registered/u.test(message)) return 'gateway-session-unavailable'; + if (/spawn C:\\Windows\\system32\\cmd\.exe ENOENT|cmd\.exe ENOENT/u.test(message)) return 'host-cmd-unavailable'; if (/unsupported|invalid target selector|invalid io-probe (?:path|selector)/u.test(message)) return 'invalid-request'; return 'operation-failed'; } @@ -46,6 +49,8 @@ function nextForBlocker(blocker, details = {}) { ]; } if (blocker === 'profile-invalid') { + const names = availableProfileNames(); + if (names.length > 1) return profileChoiceNext(names); return [ `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`, `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id ${podId}` @@ -60,9 +65,39 @@ function nextForBlocker(blocker, details = {}) { `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}` ]; } + if (blocker === 'gateway-session-mismatch') { + return [ + `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`, + 'update the profile gatewaySessionId to the resourceGatewaySessionId reported by cloud-api, then rerun doctor --probe', + 'do not bypass device-pod-cli with generic gateway shell' + ]; + } + if (blocker === 'gateway-session-unavailable') { + return [ + `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`, + 'restart or reconnect the target hwlab-gateway session, then rerun the same device-pod-cli command', + 'do not guess another gateway session without updating the profile' + ]; + } + if (blocker === 'host-cmd-unavailable') { + return [ + `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`, + 'the gateway route was reached but cannot spawn Windows cmd.exe; repair the Windows gateway host or use a profile bound to a Windows cmd-capable gateway', + 'stop retrying lower-level gateway shell commands until doctor --probe is green' + ]; + } return [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`]; } +function profileChoiceNext(names) { + const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs'; + return [ + `${script} profile list`, + ...names.map((name) => `${script} doctor --pod-id ${name}`), + ...names.map((name) => `${script} health --pod-id ${name}`) + ]; +} + function podIdFromMessage(message) { const match = String(message || '').match(/([A-Za-z0-9_.-]+)\.json\b/u); return match?.[1] || null; @@ -143,11 +178,15 @@ function parseOptions(argv) { function resolvePodId(requested) { if (requested) return requested; if (process.env.DEVICE_POD_ID) return process.env.DEVICE_POD_ID; - const names = fs.existsSync(PROFILE_DIR) - ? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json')) - : []; + const names = availableProfileNames(); if (names.length === 1) return names[0]; - throw new Error('profile invalid: pass : or set DEVICE_POD_ID when multiple/no profiles exist'); + throw new Error(`profile invalid: pass : or set DEVICE_POD_ID when multiple/no profiles exist; available profiles: ${names.join(', ') || '(none)'}`); +} + +function availableProfileNames() { + return fs.existsSync(PROFILE_DIR) + ? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json')).sort() + : []; } function readProfile(podId) { @@ -228,7 +267,7 @@ function conStart710007511Profile(overrides = {}) { ] }, cloudApiUrl: 'http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667', - gatewaySessionId: 'gws_DESKTOP-1MHOD9I', + gatewaySessionId: 'gws_YUAN', resourceId: 'res_windows_host', capabilityId: 'cap_windows_cmd_exec', hostWorkspaceRoot: 'F:\\Work\\ConStart\\projects\\71-00075-11', @@ -302,18 +341,22 @@ function profileMissingDoctor(podId, file) { }; } -function doctor(parsed) { +async function doctor(parsed) { const podId = parsed.podId || parsed._[0] || process.env.DEVICE_POD_ID || null; try { const profile = readProfile(podId); const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs'; const selector = `${profile.podId}:workspace:/`; const hostProfile = hostProfilePath(profile); + const hostHealthCommand = buildHostHealthCommand(profile); + const probe = parsed.probe === true + ? await probeHostHealth(profile, hostHealthCommand, parsed) + : undefined; const uploadRoot = `${profile.gatewaySessionId}:${String(profile.hostWorkspaceRoot || '').replace(/^[A-Za-z]:/u, (drive) => `/${drive[0].toLowerCase()}`).replace(/\\/gu, '/')}`; print({ ok: true, action: 'doctor', - status: 'succeeded', + status: probe?.ok === false ? 'degraded' : 'succeeded', ...profileSummary(profile), profileDir: PROFILE_DIR, route: routeSummary(profile), @@ -327,10 +370,12 @@ function doctor(parsed) { show: `${script} profile show --pod-id ${profile.podId}`, health: `${script} health --pod-id ${profile.podId}`, plan: `${script} ${selector} ls --dry-run`, + probe: `${script} doctor --pod-id ${profile.podId} --probe`, uploadHostCli: `node /app/tools/tran.mjs ${uploadRoot} upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs`, uploadProfile: `node /app/tools/tran.mjs ${uploadRoot} upload ${profile.__profilePath} .device-pod/${profile.podId}.json`, hostHealth: `node /app/tools/tran.mjs ${uploadRoot} cmd -- node tools\\device-host-cli.mjs --profile ${hostProfile} --pod-id ${profile.podId} health` }, + probe, valuesRedacted: true, secretMaterialStored: false }); @@ -345,6 +390,35 @@ function doctor(parsed) { } } +function buildHostHealthCommand(profile) { + const hostArgs = ['--profile', hostProfilePath(profile), '--pod-id', profile.podId, 'health']; + return `${profile.hostCli} ${hostArgs.map(cmdQuote).join(' ')}`; +} + +async function probeHostHealth(profile, command, parsed) { + const result = await invoke(profile, command, { + ...parsed, + timeoutMs: Number(parsed.timeoutMs || 10000), + traceId: parsed.traceId || `trc_device_pod_doctor_${Date.now()}` + }); + const diagnosis = diagnoseInvokeFailure(profile, result, { operation: 'doctor.host-health' }); + return { + ok: result.status === 'succeeded', + status: result.status, + command, + traceId: result.traceId, + operationId: result.operationId, + requestId: result.requestId, + httpStatus: result.httpStatus, + blocker: diagnosis.blocker, + next: diagnosis.next, + dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), + hostJson: result.hostJson, + evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, + gatewayBody: parsed.full ? result.gatewayBody : undefined + }; +} + function handleProfileCommand(parsed) { const subcommand = parsed._[0] || 'list'; if (subcommand === 'list') return listProfiles(); @@ -477,6 +551,35 @@ async function invoke(profile, command, parsed) { return { requestId, operationId, traceId, httpStatus: response.status, status, dispatch, hostJson, gatewayBody: response.body }; } +function diagnoseInvokeFailure(profile, result, context = {}) { + if (result.status === 'succeeded') { + return { ok: true, blocker: null, next: [] }; + } + const text = JSON.stringify({ + error: result.gatewayBody?.error, + dispatch: result.dispatch, + hostJson: result.hostJson + }); + const blocker = classifyBlocker(text); + const next = nextForBlocker(blocker, { podId: profile.podId, message: text }); + return { + ok: false, + blocker, + next, + operation: context.operation || null, + summary: summarizeFailureText(text), + route: routeSummary(profile), + valuesRedacted: true, + secretMaterialStored: false + }; +} + +function summarizeFailureText(text) { + return String(text || '') + .replace(/\s+/gu, ' ') + .slice(0, 500); +} + function requestJson(url, options = {}) { const body = JSON.stringify(options.body || {}); const target = new URL(url); @@ -533,6 +636,7 @@ function hostJsonSummary(hostJson) { function help() { print({ ok: true, action: 'help', scope: 'HWLAB internal code agents only; not for external UniDesk workspaces.', usage: [ 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq', + 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq --probe', 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list', 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id device-pod-71-freq', 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force', @@ -553,6 +657,20 @@ async function main() { const parsed = parseOptions(argv); if (selector.kind === 'doctor') return doctor(parsed); if (selector.kind === 'profile') return handleProfileCommand(parsed); + if (selector.kind === 'health' && !parsed.podId && !process.env.DEVICE_POD_ID) { + const names = availableProfileNames(); + if (names.length !== 1) return print({ + ok: false, + action: 'health', + status: 'failed', + blocker: 'profile-invalid', + profileDir: PROFILE_DIR, + availableProfiles: names, + next: profileChoiceNext(names), + valuesRedacted: true, + secretMaterialStored: false + }, 1); + } const profile = readProfile(selector.podId || parsed.podId); if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile), next: [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${profile.podId}`] }); const built = await buildHostCommand(selector, parsed); @@ -561,7 +679,8 @@ async function main() { if (parsed.dryRun) return print({ ok: true, action: 'device-pod.plan', status: 'planned', selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile) }); const result = await invoke(profile, command, parsed); const ok = result.status === 'succeeded'; - print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1); + const diagnosis = ok ? null : diagnoseInvokeFailure(profile, result, { operation: built.operation }); + print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, blocker: diagnosis?.blocker, next: diagnosis?.next, diagnosis, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1); } main().catch((error) => fail('device-pod-cli', error));