330db43b5f
Each turn (assistant response / tool result) is appended to a JSONL file at ~/.uncaged/workflow/sessions/<sessionId>.jsonl during the loop. On completion, the JSONL is read back, each turn is stored as a CAS node, and the detail payload references them as a flat turns[] array in chronological order. The session file is then deleted. Benefits: - Real-time observability: tail -f the JSONL to watch loop progress - Crash recovery: partial JSONL survives process death - Zero write contention: one file per session - Detail stays a flat array for easy consumption by CLI/dashboard Changes: - New session.ts: initSessionDir, appendSessionTurn, readSessionTurns, removeSession - loop.ts: append JSONL each turn instead of accumulating in-memory - detail.ts: reads session JSONL → persists turns to CAS → stores detail - agent.ts: passes storageRoot/sessionId to loop, cleans up session on completion - types.ts: remove index from TurnPayload (order is implicit in JSONL/array) - schemas.ts: sync with type changes Ref: #433
60 lines
1.8 KiB
TypeScript
60 lines
1.8 KiB
TypeScript
import { appendFile, mkdir, readFile, rm } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { createLogger } from "@uncaged/workflow-util";
|
|
|
|
import type { BuiltinTurnPayload } from "./types.js";
|
|
|
|
const log = createLogger({ sink: { kind: "stderr" } });
|
|
|
|
function sessionsDir(storageRoot: string): string {
|
|
return join(storageRoot, "sessions");
|
|
}
|
|
|
|
function sessionFile(storageRoot: string, sessionId: string): string {
|
|
return join(sessionsDir(storageRoot), `${sessionId}.jsonl`);
|
|
}
|
|
|
|
/** Ensure sessions directory exists. */
|
|
export async function initSessionDir(storageRoot: string): Promise<void> {
|
|
await mkdir(sessionsDir(storageRoot), { recursive: true });
|
|
}
|
|
|
|
/** Append a turn to the session jsonl file. */
|
|
export async function appendSessionTurn(
|
|
storageRoot: string,
|
|
sessionId: string,
|
|
turn: BuiltinTurnPayload,
|
|
): Promise<void> {
|
|
const line = `${JSON.stringify(turn)}\n`;
|
|
await appendFile(sessionFile(storageRoot, sessionId), line, "utf-8");
|
|
log("3XQVN8KR", `session ${sessionId} appended ${turn.role} turn`);
|
|
}
|
|
|
|
/** Read all turns from session jsonl. Returns empty array if file does not exist. */
|
|
export async function readSessionTurns(
|
|
storageRoot: string,
|
|
sessionId: string,
|
|
): Promise<BuiltinTurnPayload[]> {
|
|
try {
|
|
const content = await readFile(sessionFile(storageRoot, sessionId), "utf-8");
|
|
const lines = content
|
|
.trim()
|
|
.split("\n")
|
|
.filter((l) => l.length > 0);
|
|
return lines.map((l) => JSON.parse(l) as BuiltinTurnPayload);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/** Remove session jsonl file (called after detail is persisted to step CAS). */
|
|
export async function removeSession(storageRoot: string, sessionId: string): Promise<void> {
|
|
try {
|
|
await rm(sessionFile(storageRoot, sessionId));
|
|
log("7FWDP2MJ", `session ${sessionId} removed`);
|
|
} catch {
|
|
// already gone — fine
|
|
}
|
|
}
|