Improve code agent trace inspection tooling

This commit is contained in:
Codex
2026-05-28 11:28:58 +08:00
parent 912d6251a1
commit f72905243e
7 changed files with 408 additions and 2 deletions
@@ -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"
+68
View File
@@ -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;
+119
View File
@@ -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-"));
+9
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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(' ')}`;