ce79dbea7e
When 'nerve thread show' is called without --before, the initial user prompt (__start__ message) is now displayed first, followed by the most recent role rounds within the budget. - Add getThreadStartMessage() to LogStore - Modify buildThreadCommandOutput to accept optional startRow - Pass start message from threadShowCommand when before===0 - Add tests for new behavior Fixes #231
280 lines
7.9 KiB
TypeScript
280 lines
7.9 KiB
TypeScript
import { defineCommand } from "citty";
|
|
|
|
import { isRemoteDaemonCli } from "../cli-global.js";
|
|
import { resolveDaemonTransport } from "../daemon-client.js";
|
|
import { isRunning } from "../workspace.js";
|
|
import {
|
|
DEFAULT_PAGE_SIZE,
|
|
DEFAULT_THREAD_BUDGET_CHARS,
|
|
THREAD_ROUNDS_FETCH_LIMIT,
|
|
buildInspectOutput,
|
|
buildListOutput,
|
|
buildThreadCommandOutput,
|
|
getAllWorkflowRuns,
|
|
openStore,
|
|
parseIntArg,
|
|
} from "./workflow.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nerve thread list
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const threadListCommand = defineCommand({
|
|
meta: {
|
|
name: "list",
|
|
description: "List active (queued/started) workflow runs from logs",
|
|
},
|
|
args: {
|
|
all: {
|
|
type: "boolean",
|
|
description: "Include completed/failed/crashed runs",
|
|
default: false,
|
|
},
|
|
workflow: {
|
|
type: "string",
|
|
description: "Filter by workflow name",
|
|
default: "",
|
|
},
|
|
limit: {
|
|
type: "string",
|
|
description: `Max runs to show (default: ${DEFAULT_PAGE_SIZE})`,
|
|
default: String(DEFAULT_PAGE_SIZE),
|
|
},
|
|
offset: {
|
|
type: "string",
|
|
description: "Skip first N runs (for pagination)",
|
|
default: "0",
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
const store = await openStore();
|
|
|
|
try {
|
|
const limit = Math.max(1, parseIntArg(args.limit, DEFAULT_PAGE_SIZE));
|
|
const offset = Math.max(0, parseIntArg(args.offset, 0));
|
|
const filterWorkflow = args.workflow.length > 0 ? args.workflow : null;
|
|
|
|
const runs = args.all
|
|
? getAllWorkflowRuns(store, filterWorkflow)
|
|
: store.getActiveWorkflowRuns(filterWorkflow ?? undefined);
|
|
|
|
const { lines, paginationHint } = buildListOutput(
|
|
runs,
|
|
offset,
|
|
limit,
|
|
args.all,
|
|
filterWorkflow,
|
|
);
|
|
|
|
for (const line of lines) {
|
|
process.stdout.write(line);
|
|
}
|
|
if (paginationHint !== null) {
|
|
process.stdout.write(paginationHint);
|
|
}
|
|
} finally {
|
|
store.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nerve thread show <runId>
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const threadShowCommand = defineCommand({
|
|
meta: {
|
|
name: "show",
|
|
description: "Print role rounds for a workflow run (agent-oriented, budget-limited)",
|
|
},
|
|
args: {
|
|
runId: {
|
|
type: "positional",
|
|
description: "The run ID to dump role rounds for",
|
|
},
|
|
before: {
|
|
type: "string",
|
|
description:
|
|
"Exclusive upper bound on 1-based round index (use with hint from prior output to load older rounds)",
|
|
default: "0",
|
|
},
|
|
budget: {
|
|
type: "string",
|
|
description: `Max output characters including header (default: ${String(DEFAULT_THREAD_BUDGET_CHARS)})`,
|
|
default: String(DEFAULT_THREAD_BUDGET_CHARS),
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
const store = await openStore();
|
|
|
|
try {
|
|
const before = Math.max(0, parseIntArg(args.before, 0));
|
|
const budgetChars = Math.max(1, parseIntArg(args.budget, DEFAULT_THREAD_BUDGET_CHARS));
|
|
|
|
const run = store.getWorkflowRun(args.runId);
|
|
if (run === null) {
|
|
process.stderr.write(`❌ No workflow run found with runId: ${args.runId}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const startRow = before === 0 ? store.getThreadStartMessage(args.runId) : null;
|
|
const totalRoleRounds = store.getThreadRoundCount(args.runId);
|
|
if (totalRoleRounds === 0 && startRow === null) {
|
|
process.stdout.write(
|
|
`🧵 Workflow thread: ${run.runId}\n workflow: ${run.workflow}\n status: ${run.status}\n\n📭 No role rounds recorded for this run.\n`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const descRows = store.getThreadRounds(args.runId, {
|
|
before,
|
|
limit: THREAD_ROUNDS_FETCH_LIMIT,
|
|
});
|
|
|
|
const prefixLines = [
|
|
"🧵 Role rounds (workflow thread)\n",
|
|
` runId: ${run.runId}\n`,
|
|
` workflow: ${run.workflow}\n`,
|
|
` status: ${run.status}\n`,
|
|
` rounds: ${String(totalRoleRounds)} role event(s) total\n\n`,
|
|
];
|
|
|
|
const { lines, paginationHint } = buildThreadCommandOutput(
|
|
prefixLines,
|
|
descRows,
|
|
budgetChars,
|
|
args.runId,
|
|
startRow,
|
|
);
|
|
|
|
for (const line of lines) {
|
|
process.stdout.write(line);
|
|
}
|
|
if (paginationHint !== null) {
|
|
process.stdout.write(paginationHint);
|
|
}
|
|
|
|
if (descRows.length === 0 && before > 0) {
|
|
process.stdout.write(`\n📭 No rounds with index < ${String(before)}.\n`);
|
|
}
|
|
} finally {
|
|
store.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nerve thread inspect <runId>
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const threadInspectCommand = defineCommand({
|
|
meta: {
|
|
name: "inspect",
|
|
description: "Show details and thread events for a workflow run",
|
|
},
|
|
args: {
|
|
runId: {
|
|
type: "positional",
|
|
description: "The run ID to inspect",
|
|
},
|
|
limit: {
|
|
type: "string",
|
|
description: `Max log entries to show (default: ${DEFAULT_PAGE_SIZE})`,
|
|
default: String(DEFAULT_PAGE_SIZE),
|
|
},
|
|
offset: {
|
|
type: "string",
|
|
description: "Skip first N log entries (for pagination)",
|
|
default: "0",
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
const store = await openStore();
|
|
|
|
try {
|
|
const limit = Math.max(1, parseIntArg(args.limit, DEFAULT_PAGE_SIZE));
|
|
const offset = Math.max(0, parseIntArg(args.offset, 0));
|
|
|
|
const run = store.getWorkflowRun(args.runId);
|
|
if (run === null) {
|
|
process.stderr.write(`❌ No workflow run found with runId: ${args.runId}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const allLogs = store.query({ source: "workflow", refId: args.runId });
|
|
const { header, eventLines, paginationHint } = buildInspectOutput(
|
|
run,
|
|
allLogs,
|
|
offset,
|
|
limit,
|
|
);
|
|
|
|
for (const line of [...header, ...eventLines]) {
|
|
process.stdout.write(line);
|
|
}
|
|
if (paginationHint !== null) {
|
|
process.stdout.write(paginationHint);
|
|
}
|
|
} finally {
|
|
store.close();
|
|
}
|
|
},
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nerve thread kill <runId>
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const threadKillCommand = defineCommand({
|
|
meta: {
|
|
name: "kill",
|
|
description: "Kill a running or queued workflow thread by runId",
|
|
},
|
|
args: {
|
|
runId: {
|
|
type: "positional",
|
|
description: "The run ID to kill",
|
|
},
|
|
},
|
|
async run({ args }) {
|
|
if (!isRemoteDaemonCli() && !isRunning()) {
|
|
process.stderr.write("❌ Nerve daemon is not running. Start it with `nerve daemon start`.\n");
|
|
process.exit(1);
|
|
}
|
|
|
|
const transport = resolveDaemonTransport();
|
|
let response: { ok: true } | { ok: false; error: string };
|
|
try {
|
|
response = await transport.killWorkflow(args.runId);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
process.stderr.write(`❌ Failed to contact daemon: ${msg}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
process.stderr.write(`❌ Kill failed: ${response.error}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
process.stdout.write(`✅ Kill signal sent for run "${args.runId}".\n`);
|
|
},
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// nerve thread (parent command)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export const threadCommand = defineCommand({
|
|
meta: {
|
|
name: "thread",
|
|
description: "Inspect and manage workflow threads (runs)",
|
|
},
|
|
subCommands: {
|
|
list: threadListCommand,
|
|
show: threadShowCommand,
|
|
inspect: threadInspectCommand,
|
|
kill: threadKillCommand,
|
|
},
|
|
});
|