Initial commit

This commit is contained in:
Amos
2026-06-26 22:25:19 +03:00
commit c6c00f89a7
29 changed files with 14358 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
import { appendFileSync, copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { randomBytes, randomUUID } from "node:crypto";
import { dirname, join } from "node:path";
export interface SessionEntry {
type: string;
id: string;
parentId?: string;
[key: string]: unknown;
}
export interface MessageEntry extends SessionEntry {
type: "message";
message: {
role: "user" | "assistant" | "toolResult";
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
};
}
export type SeededSubagentSessionMode = "lineage-only" | "fork";
function getForkContentLines(parentSessionFile: string): string[] {
const raw = readFileSync(parentSessionFile, "utf8");
const lines = raw.split("\n").filter((line) => line.trim());
let truncateAt = lines.length;
for (let i = lines.length - 1; i >= 0; i--) {
try {
const entry = JSON.parse(lines[i]);
if (entry.type === "message" && entry.message?.role === "user") {
truncateAt = i;
break;
}
} catch {
// ignore malformed lines
}
}
return lines.slice(0, truncateAt).filter((line) => {
try {
return JSON.parse(line).type !== "session";
} catch {
return true;
}
});
}
export function seedSubagentSessionFile(params: {
mode: SeededSubagentSessionMode;
parentSessionFile: string;
childSessionFile: string;
childCwd: string;
}): void {
const header = {
type: "session",
version: 3,
id: randomUUID(),
timestamp: new Date().toISOString(),
cwd: params.childCwd,
parentSession: params.parentSessionFile,
};
const contentLines =
params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
const lines = [JSON.stringify(header), ...contentLines];
mkdirSync(dirname(params.childSessionFile), { recursive: true });
writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");
}
function readEntries(sessionFile: string): SessionEntry[] {
const raw = readFileSync(sessionFile, "utf8");
return raw
.split("\n")
.filter((line) => line.trim())
.map((line) => JSON.parse(line) as SessionEntry);
}
/**
* Return the id of the last entry in the session file (current branch point / leaf).
*/
export function getLeafId(sessionFile: string): string | null {
const entries = readEntries(sessionFile);
return entries.length > 0 ? entries[entries.length - 1].id : null;
}
/**
* Return entries added after `afterLine` (1-indexed count of existing entries).
*/
export function getNewEntries(sessionFile: string, afterLine: number): SessionEntry[] {
const raw = readFileSync(sessionFile, "utf8");
const lines = raw.split("\n").filter((line) => line.trim());
return lines.slice(afterLine).map((line) => JSON.parse(line) as SessionEntry);
}
/**
* Find the last assistant message text in a list of entries.
*
* Falls back to the `errorMessage` field when the last assistant message has
* `stopReason: "error"` and no usable text content — this happens when
* auto-retry exhausts on a provider overload / rate limit / server error, and
* without this fallback the parent would silently see a stale earlier message.
*/
export function findLastAssistantMessage(entries: SessionEntry[]): string | null {
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type !== "message") continue;
const msg = entry as MessageEntry;
if (msg.message.role !== "assistant") continue;
const texts = msg.message.content
.filter(
(block) =>
block.type === "text" && typeof block.text === "string" && block.text.trim() !== "",
)
.map((block) => block.text as string);
if (texts.length > 0 && texts.join("").trim()) return texts.join("\n");
const stopReason = (msg.message as { stopReason?: unknown }).stopReason;
const errorMessage = (msg.message as { errorMessage?: unknown }).errorMessage;
if (
stopReason === "error" &&
typeof errorMessage === "string" &&
errorMessage.trim() !== ""
) {
return `Subagent error: ${errorMessage.trim()}`;
}
}
return null;
}
/**
* Append a branch_summary entry to the session file.
* Returns the new entry's id.
*/
export function appendBranchSummary(
sessionFile: string,
branchPointId: string,
fromId: string | null,
summary: string,
): string {
const id = randomBytes(4).toString("hex");
const entry = {
type: "branch_summary",
id,
parentId: branchPointId,
timestamp: new Date().toISOString(),
fromId: fromId ?? branchPointId,
summary,
};
appendFileSync(sessionFile, JSON.stringify(entry) + "\n", "utf8");
return id;
}
/**
* Copy the session file to destDir for parallel worker isolation.
* Returns the path of the copy.
*/
export function copySessionFile(sessionFile: string, destDir: string): string {
const id = randomBytes(4).toString("hex");
const dest = join(destDir, `subagent-${id}.jsonl`);
copyFileSync(sessionFile, dest);
return dest;
}
/**
* Read new entries from sourceFile (after afterLine), append them to targetFile.
* Returns the appended entries.
*/
export function mergeNewEntries(
sourceFile: string,
targetFile: string,
afterLine: number,
): SessionEntry[] {
const entries = getNewEntries(sourceFile, afterLine);
for (const entry of entries) {
appendFileSync(targetFile, JSON.stringify(entry) + "\n", "utf8");
}
return entries;
}