fix: improve device pod trace diagnostics

This commit is contained in:
Codex
2026-05-28 12:22:34 +08:00
parent da06165cac
commit 067ccf6b7e
6 changed files with 200 additions and 27 deletions
+15 -3
View File
@@ -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;
}
@@ -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", () => {
+8 -2
View File
@@ -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,
+23
View File
@@ -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-"));
+16 -14
View File
@@ -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 <devicePodId> --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:
+127 -8
View File
@@ -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 <devicePodId>:<surface> or set DEVICE_POD_ID when multiple/no profiles exist');
throw new Error(`profile invalid: pass <devicePodId>:<surface> 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));