Merge pull request #529 from pikasTech/device-pod-friction-20260528
fix: improve device pod trace diagnostics
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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-"));
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user