Improve code agent trace inspection tooling
This commit is contained in:
@@ -183,6 +183,59 @@ export function createCodeAgentSessionRegistry(options = {}) {
|
||||
return sessionId ? get(sessionId, { ...params, conversationId }) : null;
|
||||
}
|
||||
|
||||
function findSession(params = {}) {
|
||||
const conversationId = optionalId(params.conversationId);
|
||||
const sessionId = optionalId(params.sessionId);
|
||||
const traceId = optionalId(params.traceId);
|
||||
const threadId = optionalId(params.threadId);
|
||||
if (sessionId) {
|
||||
const session = get(sessionId, { conversationId });
|
||||
if (session) return session;
|
||||
}
|
||||
if (conversationId) {
|
||||
const session = getByConversation(conversationId, { conversationId });
|
||||
if (session) return session;
|
||||
}
|
||||
for (const session of sessions.values()) {
|
||||
const conversationIds = [...session.conversationIds];
|
||||
if (conversationId && conversationIds.includes(conversationId)) return publicSession(session, { conversationId });
|
||||
if (traceId && (session.lastTraceId === traceId || session.currentTraceId === traceId)) return publicSession(session, { conversationId: conversationIds[0] ?? null });
|
||||
if (threadId && session.threadId === threadId) return publicSession(session, { conversationId: conversationIds[0] ?? null });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inspect(params = {}) {
|
||||
const session = findSession(params);
|
||||
const conversationId = optionalId(params.conversationId) ?? session?.conversationId ?? session?.conversationIds?.[0] ?? null;
|
||||
const facts = conversationId ? getConversationFacts(conversationId) : null;
|
||||
const requestedTraceId = optionalId(params.traceId);
|
||||
const traceIds = uniqueStrings([
|
||||
requestedTraceId,
|
||||
facts?.latestTraceId,
|
||||
session?.currentTraceId,
|
||||
session?.lastTraceId,
|
||||
...(Array.isArray(facts?.traceIds) ? facts.traceIds : [])
|
||||
]);
|
||||
return {
|
||||
ok: Boolean(session || facts || traceIds.length > 0),
|
||||
action: "code-agent.session.inspect",
|
||||
status: session || facts ? "found" : "not_found",
|
||||
query: {
|
||||
conversationId: optionalId(params.conversationId),
|
||||
sessionId: optionalId(params.sessionId),
|
||||
threadId: optionalId(params.threadId),
|
||||
traceId: requestedTraceId
|
||||
},
|
||||
session,
|
||||
conversationFacts: facts,
|
||||
traceIds,
|
||||
latestTraceId: traceIds[0] ?? null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
}
|
||||
|
||||
function recordFact(conversationIdValue, fact, params = {}) {
|
||||
const conversationId = requiredId(conversationIdValue, "cnv");
|
||||
const timestamp = timestampFor(params.now ?? defaultNow);
|
||||
@@ -285,6 +338,8 @@ export function createCodeAgentSessionRegistry(options = {}) {
|
||||
interrupt,
|
||||
get,
|
||||
getByConversation,
|
||||
findSession,
|
||||
inspect,
|
||||
recordFact,
|
||||
getConversationFacts,
|
||||
expireIdle,
|
||||
@@ -554,6 +609,10 @@ function optionalId(value) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
return [...new Set(values.map((value) => typeof value === "string" ? value.trim() : "").filter(Boolean))];
|
||||
}
|
||||
|
||||
function positiveInteger(value, fallback) {
|
||||
return Number.isInteger(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,55 @@ test("code agent session registry stores bounded non-sensitive conversation fact
|
||||
assert.match(facts.summary, /workspace=\/workspace\/hwlab/u);
|
||||
});
|
||||
|
||||
test("code agent session registry inspect locates conversation, session, and trace summary", () => {
|
||||
const registry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_inspect_registry"
|
||||
});
|
||||
const acquired = registry.acquire({
|
||||
conversationId: "cnv_inspect_registry",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
capabilityLevel: "long-lived-codex-stdio-session",
|
||||
traceId: "trc_inspect_registry_1",
|
||||
now: "2026-05-23T00:00:00.000Z"
|
||||
});
|
||||
registry.release(acquired.session.sessionId, {
|
||||
conversationId: "cnv_inspect_registry",
|
||||
traceId: "trc_inspect_registry_1",
|
||||
now: "2026-05-23T00:00:00.010Z"
|
||||
});
|
||||
registry.recordFact("cnv_inspect_registry", {
|
||||
traceId: "trc_inspect_registry_1",
|
||||
provider: "codex-stdio",
|
||||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
session: acquired.session,
|
||||
runner: { kind: "codex-app-server-stdio-runner", sessionId: acquired.session.sessionId },
|
||||
runnerTrace: {
|
||||
traceId: "trc_inspect_registry_1",
|
||||
runnerKind: "codex-app-server-stdio-runner",
|
||||
sessionMode: "codex-app-server-stdio-long-lived",
|
||||
valuesPrinted: false
|
||||
}
|
||||
}, { now: "2026-05-23T00:00:00.020Z" });
|
||||
|
||||
const byConversation = registry.inspect({ conversationId: "cnv_inspect_registry" });
|
||||
assert.equal(byConversation.ok, true);
|
||||
assert.equal(byConversation.status, "found");
|
||||
assert.equal(byConversation.session.sessionId, "ses_inspect_registry");
|
||||
assert.equal(byConversation.conversationFacts.conversationId, "cnv_inspect_registry");
|
||||
assert.deepEqual(byConversation.traceIds, ["trc_inspect_registry_1"]);
|
||||
assert.equal(byConversation.latestTraceId, "trc_inspect_registry_1");
|
||||
assert.equal(byConversation.valuesRedacted, true);
|
||||
assert.equal(byConversation.secretMaterialStored, false);
|
||||
|
||||
const byTrace = registry.inspect({ traceId: "trc_inspect_registry_1" });
|
||||
assert.equal(byTrace.ok, true);
|
||||
assert.equal(byTrace.session.sessionId, "ses_inspect_registry");
|
||||
});
|
||||
|
||||
test("code agent session registry reports busy sessions as structured blocker", () => {
|
||||
const registry = createCodeAgentSessionRegistry({
|
||||
idFactory: () => "ses_busy"
|
||||
|
||||
@@ -370,6 +370,11 @@ async function handleRestAdapter(request, response, url, options) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname === "/v1/agent/chat/inspect") {
|
||||
await handleCodeAgentInspectHttp(request, response, url, options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/result/")) {
|
||||
await handleCodeAgentChatResultHttp(request, response, url, options);
|
||||
return;
|
||||
@@ -1578,6 +1583,64 @@ async function handleCodeAgentChatResultHttp(request, response, url, options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCodeAgentInspectHttp(request, response, url, options) {
|
||||
const query = {
|
||||
conversationId: safeConversationId(url.searchParams.get("conversationId")),
|
||||
sessionId: safeSessionId(url.searchParams.get("sessionId")),
|
||||
threadId: safeOpaqueId(url.searchParams.get("threadId")),
|
||||
traceId: safeTraceId(url.searchParams.get("traceId"))
|
||||
};
|
||||
const sessionRegistry = options.sessionRegistry;
|
||||
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
|
||||
const manager = options.codexStdioManager;
|
||||
const managerDescription = manager && typeof manager.describe === "function" ? manager.describe() : null;
|
||||
const managerRecentSessions = Array.isArray(managerDescription?.recentSessions) ? managerDescription.recentSessions : [];
|
||||
const directManagerSession = query.sessionId && manager && typeof manager.get === "function"
|
||||
? manager.get(query.sessionId, { conversationId: query.conversationId })
|
||||
: null;
|
||||
const matchedManagerSession = directManagerSession ?? managerRecentSessions.find((session) => sessionMatchesInspectQuery(session, query)) ?? null;
|
||||
const registryInspect = sessionRegistry && typeof sessionRegistry.inspect === "function"
|
||||
? sessionRegistry.inspect(query)
|
||||
: { ok: false, status: "unavailable", traceIds: [], session: null, conversationFacts: null };
|
||||
const session = matchedManagerSession ?? registryInspect.session ?? null;
|
||||
const conversationFacts = registryInspect.conversationFacts ?? null;
|
||||
const traceIds = uniqueStrings([
|
||||
query.traceId,
|
||||
session?.currentTraceId,
|
||||
session?.lastTraceId,
|
||||
conversationFacts?.latestTraceId,
|
||||
...(Array.isArray(conversationFacts?.traceIds) ? conversationFacts.traceIds : []),
|
||||
...(Array.isArray(registryInspect.traceIds) ? registryInspect.traceIds : [])
|
||||
]);
|
||||
const latestTraceId = traceIds[0] ?? null;
|
||||
const runnerTrace = latestTraceId ? traceStore.snapshot(latestTraceId) : null;
|
||||
const found = Boolean(registryInspect.ok || session || latestTraceId);
|
||||
sendJson(response, found ? 200 : 404, {
|
||||
ok: found,
|
||||
action: "code-agent.chat.inspect",
|
||||
status: found ? "found" : "not_found",
|
||||
query,
|
||||
session,
|
||||
conversationFacts,
|
||||
traceIds,
|
||||
latestTraceId,
|
||||
traceUrl: latestTraceId ? `/v1/agent/chat/trace/${encodeURIComponent(latestTraceId)}` : null,
|
||||
resultUrl: latestTraceId ? `/v1/agent/chat/result/${encodeURIComponent(latestTraceId)}` : null,
|
||||
runnerTrace,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
});
|
||||
}
|
||||
|
||||
function sessionMatchesInspectQuery(session, query) {
|
||||
if (!session || typeof session !== "object") return false;
|
||||
if (query.sessionId && session.sessionId === query.sessionId) return true;
|
||||
if (query.threadId && session.threadId === query.threadId) return true;
|
||||
if (query.conversationId && session.conversationId === query.conversationId) return true;
|
||||
if (query.traceId && (session.currentTraceId === query.traceId || session.lastTraceId === query.traceId)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function handleCodeAgentCancelHttp(request, response, options) {
|
||||
const body = await readBody(request, options.bodyLimitBytes);
|
||||
let params = {};
|
||||
@@ -2217,6 +2280,11 @@ function safeConversationId(value) {
|
||||
return /^cnv_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
|
||||
}
|
||||
|
||||
function safeOpaqueId(value) {
|
||||
const text = String(value ?? "").trim();
|
||||
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value, fallback) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
|
||||
@@ -1886,6 +1886,125 @@ test("cloud api /v1/agent/chat supports short submit and result polling", async
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api /v1/agent/chat/inspect maps conversation session and thread details to trace evidence", async () => {
|
||||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-"));
|
||||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-codex-home-"));
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
CODEX_HOME: codexHome,
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||||
OPENAI_API_KEY: "test-openai-key-material",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||||
},
|
||||
codexStdioManager: {
|
||||
describe() {
|
||||
return {
|
||||
...codexStdioReadyFixture({ workspace, codexHome }),
|
||||
sandbox: "danger-full-access",
|
||||
recentSessions: [{
|
||||
sessionId: "ses_server_stdio_pwd",
|
||||
conversationId: "cnv_server-test-inspect",
|
||||
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
||||
status: "idle",
|
||||
currentTraceId: null,
|
||||
lastTraceId: "trc_server-test-inspect",
|
||||
workspace,
|
||||
sandbox: "danger-full-access",
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
}]
|
||||
};
|
||||
},
|
||||
async probe() {
|
||||
return {
|
||||
...codexStdioReadyFixture({ workspace, codexHome }),
|
||||
sandbox: "danger-full-access"
|
||||
};
|
||||
},
|
||||
async chat(params = {}) {
|
||||
const base = codexStdioChatFixture({ workspace, codexHome, params });
|
||||
return {
|
||||
...base,
|
||||
sandbox: "danger-full-access",
|
||||
session: {
|
||||
...base.session,
|
||||
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
||||
sandbox: "danger-full-access"
|
||||
},
|
||||
sessionReuse: {
|
||||
...base.sessionReuse,
|
||||
threadId: "019e6c72-2373-75e3-9856-eff286db7163"
|
||||
}
|
||||
};
|
||||
},
|
||||
get(sessionId) {
|
||||
if (sessionId !== "ses_server_stdio_pwd") return null;
|
||||
return {
|
||||
sessionId,
|
||||
conversationId: "cnv_server-test-inspect",
|
||||
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
||||
status: "idle",
|
||||
lastTraceId: "trc_server-test-inspect",
|
||||
workspace,
|
||||
sandbox: "danger-full-access",
|
||||
secretMaterialStored: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
},
|
||||
cancel() {},
|
||||
reapIdle() {}
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const traceId = "trc_server-test-inspect";
|
||||
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-trace-id": traceId,
|
||||
"prefer": "respond-async",
|
||||
"x-hwlab-short-connection": "1"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversationId: "cnv_server-test-inspect",
|
||||
message: "inspect mapping smoke"
|
||||
})
|
||||
});
|
||||
assert.equal(submit.status, 202);
|
||||
const payload = await pollAgentResult(port, traceId);
|
||||
assert.equal(payload.status, "completed");
|
||||
|
||||
const inspect = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect&sessionId=ses_server_stdio_pwd&threadId=019e6c72-2373-75e3-9856-eff286db7163`);
|
||||
assert.equal(inspect.status, 200);
|
||||
const body = await inspect.json();
|
||||
assert.equal(body.ok, true);
|
||||
assert.equal(body.status, "found");
|
||||
assert.equal(body.latestTraceId, traceId);
|
||||
assert.equal(body.traceUrl, `/v1/agent/chat/trace/${traceId}`);
|
||||
assert.equal(body.resultUrl, `/v1/agent/chat/result/${traceId}`);
|
||||
assert.equal(body.query.threadId, "019e6c72-2373-75e3-9856-eff286db7163");
|
||||
assert.equal(body.session.sessionId, "ses_server_stdio_pwd");
|
||||
assert.equal(body.conversationFacts.conversationId, "cnv_server-test-inspect");
|
||||
assert.equal(body.runnerTrace.traceId, traceId);
|
||||
assert.equal(body.valuesRedacted, true);
|
||||
assert.equal(body.secretMaterialStored, false);
|
||||
assert.equal(JSON.stringify(body).includes("test-openai-key-material"), false);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
await rm(codexHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
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-"));
|
||||
|
||||
@@ -29,6 +29,11 @@ not the HWLAB runner contract and cause stale instructions.
|
||||
Do not start by calling `/app/tools/hwlab-gateway-tran.mjs` directly, do not
|
||||
hunt for Windows-side Keil skills, and do not call `keil-cli.py` directly when
|
||||
a matching device-pod profile exists.
|
||||
- When a profile, selector, host route, or upload step is ambiguous, run
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id <devicePodId>`
|
||||
before trying lower-level commands. Treat the `doctor` JSON as the source of
|
||||
truth for profile path, profile hash, route, host profile path, selectors, and
|
||||
the exact upload/host-health commands.
|
||||
- The profile already carries `hostWorkspaceRoot`, `projectWorkspace`,
|
||||
`debugInterface.uv4Path`, probe UID, UART port, and gateway route. Let
|
||||
`device-pod-cli -> device-host-cli` apply those values; do not rediscover the
|
||||
@@ -36,6 +41,10 @@ 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.
|
||||
- If a command returns a JSON `next` array or object, follow those CLI-owned next
|
||||
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.
|
||||
- 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
|
||||
|
||||
@@ -19,6 +19,10 @@ Use this skill when working on HWLAB agent runtime skeletons.
|
||||
- Keep the control plane long-lived and worker execution session-scoped.
|
||||
- Model the lifecycle as create, start, trace, finish, cleanup.
|
||||
- Always carry explicit evidence record fields through the run plan.
|
||||
- When investigating a Cloud Workbench or DeepSeek task from UI session details,
|
||||
start with `GET /v1/agent/chat/inspect?conversationId=<cnv_...>&sessionId=<ses_...>&threadId=<thread-or-uuid>`.
|
||||
Use its `traceUrl`, `resultUrl`, `latestTraceId`, `conversationFacts`, and
|
||||
`runnerTrace` instead of guessing a trace id from frontend text.
|
||||
- Use repo-local skills artifacts only when `commitId` and `version` are
|
||||
supplied explicitly.
|
||||
- Do not infer or fall back to external skill sources.
|
||||
|
||||
+100
-2
@@ -25,7 +25,8 @@ function print(body, exitCode = 0) {
|
||||
}
|
||||
function fail(action, error, details = {}) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
print({ ok: false, action, status: 'failed', blocker: classifyBlocker(message), error: message, details }, 1);
|
||||
const blocker = classifyBlocker(message);
|
||||
print({ ok: false, action, status: 'failed', blocker, error: message, details, next: nextForBlocker(blocker, { ...details, message }) }, 1);
|
||||
}
|
||||
function classifyBlocker(message) {
|
||||
if (/profile not found/u.test(message)) return 'profile-missing';
|
||||
@@ -35,8 +36,41 @@ function classifyBlocker(message) {
|
||||
return 'operation-failed';
|
||||
}
|
||||
|
||||
function nextForBlocker(blocker, details = {}) {
|
||||
const podId = details.podId || podIdFromMessage(details.message) || process.env.DEVICE_POD_ID || '<devicePodId>';
|
||||
if (blocker === 'profile-missing') {
|
||||
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 create --pod-id ${podId} --force`,
|
||||
'upload the generated profile and device-host-cli asset using the doctor next.upload commands'
|
||||
];
|
||||
}
|
||||
if (blocker === 'profile-invalid') {
|
||||
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}`
|
||||
];
|
||||
}
|
||||
if (blocker === 'approval-required') {
|
||||
return ['rerun the mutating operation with --approved --reason <text> after the user explicitly requested it'];
|
||||
}
|
||||
if (blocker === 'invalid-request') {
|
||||
return [
|
||||
'keep the device selector as one token, for example device-pod-71-00075-11:io-probe:/uart/1 read',
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`
|
||||
];
|
||||
}
|
||||
return [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`];
|
||||
}
|
||||
|
||||
function podIdFromMessage(message) {
|
||||
const match = String(message || '').match(/([A-Za-z0-9_.-]+)\.json\b/u);
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
function parseTarget(raw) {
|
||||
if (!raw || raw === '--help' || raw === 'help') return { kind: 'help' };
|
||||
if (raw === 'doctor') return { kind: 'doctor' };
|
||||
if (raw === 'health') return { kind: 'health' };
|
||||
if (raw === 'profile') return { kind: 'profile' };
|
||||
const match = String(raw).trim().match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u);
|
||||
@@ -249,6 +283,68 @@ function showProfile(parsed) {
|
||||
print({ ok: true, action: 'profile.show', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile), projectWorkspace: profile.projectWorkspace || null, debugInterface: profile.debugInterface || null, ioInterface: profile.ioInterface || null, hostProfilePath: hostProfilePath(profile) });
|
||||
}
|
||||
|
||||
function profileMissingDoctor(podId, file) {
|
||||
const templateAvailable = Boolean(podId);
|
||||
return {
|
||||
ok: false,
|
||||
action: 'doctor',
|
||||
status: 'failed',
|
||||
blocker: 'profile-missing',
|
||||
profileDir: PROFILE_DIR,
|
||||
devicePodId: podId || null,
|
||||
expectedProfilePath: file || (podId ? path.join(PROFILE_DIR, `${podId}.json`) : null),
|
||||
next: {
|
||||
create: templateAvailable ? `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id ${podId} --force` : 'pass --pod-id <devicePodId> or set DEVICE_POD_ID',
|
||||
list: 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list'
|
||||
},
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
}
|
||||
|
||||
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 uploadRoot = `${profile.gatewaySessionId}:${String(profile.hostWorkspaceRoot || '').replace(/^[A-Za-z]:/u, (drive) => `/${drive[0].toLowerCase()}`).replace(/\\/gu, '/')}`;
|
||||
print({
|
||||
ok: true,
|
||||
action: 'doctor',
|
||||
status: 'succeeded',
|
||||
...profileSummary(profile),
|
||||
profileDir: PROFILE_DIR,
|
||||
route: routeSummary(profile),
|
||||
hostProfilePath: hostProfile,
|
||||
selectors: {
|
||||
workspace: `${profile.podId}:workspace:/`,
|
||||
debugProbe: `${profile.podId}:debug-probe`,
|
||||
ioProbe: `${profile.podId}:io-probe:/uart/1`
|
||||
},
|
||||
next: {
|
||||
show: `${script} profile show --pod-id ${profile.podId}`,
|
||||
health: `${script} health --pod-id ${profile.podId}`,
|
||||
plan: `${script} ${selector} ls --dry-run`,
|
||||
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`
|
||||
},
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const missingMatch = message.match(/profile not found: (.+)$/u);
|
||||
if (classifyBlocker(message) === 'profile-missing') {
|
||||
print(profileMissingDoctor(podId, missingMatch?.[1] || null), 1);
|
||||
return;
|
||||
}
|
||||
fail('doctor', error, { podId });
|
||||
}
|
||||
}
|
||||
|
||||
function handleProfileCommand(parsed) {
|
||||
const subcommand = parsed._[0] || 'list';
|
||||
if (subcommand === 'list') return listProfiles();
|
||||
@@ -436,6 +532,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 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',
|
||||
@@ -454,9 +551,10 @@ async function main() {
|
||||
const selector = parseTarget(rawTarget);
|
||||
if (selector.kind === 'help') return help();
|
||||
const parsed = parseOptions(argv);
|
||||
if (selector.kind === 'doctor') return doctor(parsed);
|
||||
if (selector.kind === 'profile') return handleProfileCommand(parsed);
|
||||
const profile = readProfile(selector.podId || parsed.podId);
|
||||
if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile) });
|
||||
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);
|
||||
const hostArgs = ['--profile', hostProfilePath(profile), '--pod-id', profile.podId, ...built.commandArgs];
|
||||
const command = `${profile.hostCli} ${hostArgs.map(cmdQuote).join(' ')}`;
|
||||
|
||||
Reference in New Issue
Block a user