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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user