fix(cli): include __start__ message in nerve thread show

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
This commit is contained in:
2026-04-28 15:08:13 +00:00
parent a0a91d1699
commit ce79dbea7e
11 changed files with 123 additions and 4 deletions
@@ -255,5 +255,51 @@ describe("LogStore — crash recovery helpers (Phase 3)", () => {
expect(store.getThreadRoundCount("missing")).toBe(0);
expect(store.getThreadRounds("missing", { before: 0, limit: 10 })).toHaveLength(0);
});
it("getThreadStartMessage returns __start__ row with round 0 and excludes it from getThreadRounds", () => {
store.append({
source: "workflow",
type: "thread_workflow_message",
refId: "run-start",
payload: JSON.stringify({
role: "__start__",
content: "launch",
meta: { prompt: "hi" },
timestamp: 50,
}),
timestamp: 50,
});
store.append({
source: "workflow",
type: "thread_command_event",
refId: "run-start",
payload: JSON.stringify({ type: "step_a", role: "alpha", content: "hello", meta: {} }),
timestamp: 51,
});
expect(store.getThreadRoundCount("run-start")).toBe(1);
const start = store.getThreadStartMessage("run-start");
expect(start).not.toBeNull();
expect(start?.round).toBe(0);
expect(start?.message.role).toBe("__start__");
expect(start?.message.content).toBe("launch");
const rounds = store.getThreadRounds("run-start", { before: 0, limit: 50 });
expect(rounds).toHaveLength(1);
expect(rounds[0].round).toBe(1);
expect(rounds[0].message.role).toBe("alpha");
});
it("getThreadStartMessage returns null when no __start__ message", () => {
store.append({
source: "workflow",
type: "thread_command_event",
refId: "run-no-start",
payload: JSON.stringify({ type: "step_a", role: "alpha", content: "x", meta: {} }),
timestamp: 1,
});
expect(store.getThreadStartMessage("run-no-start")).toBeNull();
});
});
});
+25
View File
@@ -166,6 +166,11 @@ export type LogStore = {
* with `round` from ROW_NUMBER() OVER (ORDER BY id ASC). No schema migration — numbering is computed in SQL.
*/
getThreadRounds: (runId: string, params: GetThreadRoundsParams) => ThreadRoundRow[];
/**
* The workflow `__start__` message for a run (if persisted), as a {@link ThreadRoundRow}
* with `round` 0 — not part of {@link getThreadRoundCount} / {@link getThreadRounds} numbering.
*/
getThreadStartMessage: (runId: string) => ThreadRoundRow | null;
/**
* Export logs older than the retention window to `data/archive/logs/YYYY-MM-DD.jsonl`,
* then delete those rows and advance `meta.archived_up_to` in one transaction per day
@@ -396,6 +401,15 @@ export function createLogStore(dbPath: string): LogStore {
LIMIT @lim`,
);
const getThreadStartMessageStmt = sqlite.prepare(
`SELECT id, timestamp, payload FROM logs
WHERE source = 'workflow' AND type IN ('thread_command_event', 'thread_workflow_message') AND ref_id = ?
AND payload IS NOT NULL AND json_valid(payload) = 1
AND COALESCE(json_extract(payload, '$.role'), '') = '__start__'
ORDER BY id ASC
LIMIT 1`,
);
const getActiveWorkflowRunsStmt = sqlite.prepare(
"SELECT run_id, workflow, status, timestamp, exit_code FROM workflow_runs WHERE status IN ('queued', 'started') ORDER BY timestamp ASC",
);
@@ -676,6 +690,16 @@ export function createLogStore(dbPath: string): LogStore {
return out;
}
function getThreadStartMessage(runId: string): ThreadRoundRow | null {
const row = getThreadStartMessageStmt.get(runId) as
| { id: number; timestamp: number; payload: string | null }
| undefined;
if (row === undefined || row.payload === null) return null;
const message = parseRoundPayload(row.payload, row.timestamp);
if (message === null) return null;
return { round: 0, logId: row.id, timestamp: row.timestamp, message };
}
function archiveDayTx(day: string, start: number, endExclusive: number): void {
runInTransaction(sqlite, () => {
deleteLogsForDayStmt.run({ start, endExclusive });
@@ -743,6 +767,7 @@ export function createLogStore(dbPath: string): LogStore {
getThreadMessages,
getThreadRoundCount,
getThreadRounds,
getThreadStartMessage,
archiveLogs,
close,
};