feat(FN-2979): add droid CLI probe module for node diagnostics

Added a new `droid-cli-probe` module to the dashboard package with test coverage, implementing a CLI probe capability for the droid system.

Fusion-Task-Id: FN-2979
This commit is contained in:
Fusion
2026-05-01 04:03:02 -07:00
committed by gsxdsm
parent 0ec5dfa3b1
commit 09551f3d3f
12 changed files with 784 additions and 43 deletions

View File

@@ -7,7 +7,7 @@
* edited as normal project files.
*/
import { mkdir, readFile, writeFile, readdir, unlink, rename, access } from "node:fs/promises";
import { mkdir, readFile, writeFile, readdir, unlink, rename, access, appendFile } from "node:fs/promises";
import { constants as fsConstants } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import { randomUUID, randomBytes, createHash } from "node:crypto";
@@ -36,6 +36,7 @@ import type {
AgentRatingSummary,
AgentRatingInput,
Task,
AgentLogEntry,
} from "./types.js";
import { AGENT_VALID_TRANSITIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath } from "./types.js";
import type { RunMutationContext } from "./types.js";
@@ -1790,6 +1791,70 @@ export class AgentStore extends EventEmitter {
.filter((run): run is AgentHeartbeatRun => run !== null);
}
// ─────────────────────────────────────────────────────────────────────────
// Run-scoped log storage (JSONL files alongside run JSON in agentsDir)
// ─────────────────────────────────────────────────────────────────────────
/** Maximum byte size for any single log entry field (64 KB) to bound disk growth. */
private static readonly RUN_LOG_ENTRY_MAX_BYTES = 64 * 1024;
/** Return the path to the JSONL run-log file for a given agent/run pair. */
private runLogPath(agentId: string, runId: string): string {
return join(this.agentsDir, `${agentId}-runlogs-${runId}.jsonl`);
}
/**
* Append a single {@link AgentLogEntry} to the JSONL run log for the given run.
* Individual `text` and `detail` fields are capped at 64 KB so one large tool
* result cannot grow the file unboundedly.
* @param agentId - The agent ID
* @param runId - The run ID
* @param entry - The log entry to append
*/
async appendRunLog(agentId: string, runId: string, entry: AgentLogEntry): Promise<void> {
const cap = AgentStore.RUN_LOG_ENTRY_MAX_BYTES;
const safeEntry: AgentLogEntry = {
...entry,
text: entry.text.length > cap ? `${entry.text.slice(0, cap)}\n\n... (truncated, ${entry.text.length} chars)` : entry.text,
...(entry.detail !== undefined && {
detail: entry.detail.length > cap ? `${entry.detail.slice(0, cap)}\n\n... (truncated, ${entry.detail.length} chars)` : entry.detail,
}),
};
const line = JSON.stringify(safeEntry) + "\n";
await appendFile(this.runLogPath(agentId, runId), line, "utf-8");
}
/**
* Read all log entries for a given run from its JSONL file.
* Returns an empty array when the file does not exist (e.g., the run had no
* logs or was recorded before this feature was added).
* @param agentId - The agent ID
* @param runId - The run ID
* @param opts.limit - Optional maximum number of entries to return (newest-first capped)
*/
async getRunLogs(agentId: string, runId: string, opts?: { limit?: number }): Promise<AgentLogEntry[]> {
const filePath = this.runLogPath(agentId, runId);
let raw: string;
try {
raw = await readFile(filePath, "utf-8");
} catch {
return [];
}
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
const entries: AgentLogEntry[] = [];
for (const line of lines) {
try {
entries.push(JSON.parse(line) as AgentLogEntry);
} catch {
// Skip malformed lines — append-only means partial writes can occur on crash
}
}
if (opts?.limit !== undefined && entries.length > opts.limit) {
return entries.slice(entries.length - opts.limit);
}
return entries;
}
/**
* Get the most recently persisted blocked-task dedup state for an agent.
*/