Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf9e2cd3d6 | |||
| 7a99c1a9d6 | |||
| 546237db85 | |||
| 1ed7e32067 | |||
| bd5e5a435b | |||
| 67e689ff1a |
@@ -161,12 +161,11 @@ thread
|
||||
thread
|
||||
.command("fork")
|
||||
.description("Fork a thread from a specific step")
|
||||
.argument("<thread-id>", "Thread ULID")
|
||||
.argument("<step-hash>", "CAS hash of the step to fork from")
|
||||
.action((threadId: string, stepHash: string) => {
|
||||
.argument("<step-hash>", "CAS hash of the StartNode or StepNode to fork from")
|
||||
.action((stepHash: string) => {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
runAction(async () => {
|
||||
const result = await cmdThreadFork(storageRoot, threadId, stepHash);
|
||||
const result = await cmdThreadFork(storageRoot, stepHash);
|
||||
writeOutput(result);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,35 +521,15 @@ export async function cmdThreadSteps(
|
||||
|
||||
export async function cmdThreadFork(
|
||||
storageRoot: string,
|
||||
threadId: ThreadId,
|
||||
stepHash: CasRef,
|
||||
): Promise<ThreadForkOutput> {
|
||||
const headHash = await resolveHeadHash(storageRoot, threadId);
|
||||
const uwf = await createUwfStore(storageRoot);
|
||||
|
||||
// Verify stepHash belongs to this thread by walking the chain
|
||||
let found = false;
|
||||
let cur: CasRef | null = headHash;
|
||||
while (cur !== null) {
|
||||
if (cur === stepHash) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
const node = uwf.store.get(cur);
|
||||
if (node === null) break;
|
||||
if (node.type === uwf.schemas.startNode) {
|
||||
// startHash check
|
||||
if (cur === stepHash) {
|
||||
found = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const payload = node.payload as StepNodePayload;
|
||||
cur = payload.prev;
|
||||
const node = uwf.store.get(stepHash);
|
||||
if (node === null) {
|
||||
fail(`CAS node not found: ${stepHash}`);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
fail(`step ${stepHash} not found in thread ${threadId}`);
|
||||
if (node.type !== uwf.schemas.startNode && node.type !== uwf.schemas.stepNode) {
|
||||
fail(`node ${stepHash} is not a StartNode or StepNode`);
|
||||
}
|
||||
|
||||
const newThreadId = generateUlid(Date.now()) as ThreadId;
|
||||
@@ -560,7 +540,6 @@ export async function cmdThreadFork(
|
||||
return {
|
||||
thread: newThreadId,
|
||||
forkedFrom: {
|
||||
thread: threadId,
|
||||
step: stepHash,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,12 @@ describe("parseSessionIdFromStdout", () => {
|
||||
expect(parseSessionIdFromStdout(stdout)).toBe("20260518_223724_45ab80");
|
||||
});
|
||||
|
||||
test("returns null when trailing line is not session_id", () => {
|
||||
test("reads session_id from the first line (quiet mode)", () => {
|
||||
const stdout = "session_id: 20260518_165315_3467a1\nHello world\n";
|
||||
expect(parseSessionIdFromStdout(stdout)).toBe("20260518_165315_3467a1");
|
||||
});
|
||||
|
||||
test("returns null when no session_id line present", () => {
|
||||
expect(parseSessionIdFromStdout("only assistant text\n")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ export function buildHermesPrompt(ctx: AgentContext): string {
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
function spawnHermesChat(prompt: string): Promise<string> {
|
||||
function spawnHermesChat(prompt: string): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"chat",
|
||||
@@ -76,7 +76,7 @@ function spawnHermesChat(prompt: string): Promise<string> {
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
resolve({ stdout, stderr });
|
||||
return;
|
||||
}
|
||||
const detail = stderr.trim() !== "" ? ` stderr=${stderr.trim()}` : "";
|
||||
@@ -87,10 +87,11 @@ function spawnHermesChat(prompt: string): Promise<string> {
|
||||
|
||||
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
const fullPrompt = buildHermesPrompt(ctx);
|
||||
const rawOutput = await spawnHermesChat(fullPrompt);
|
||||
const { stdout, stderr } = await spawnHermesChat(fullPrompt);
|
||||
const { store } = ctx;
|
||||
|
||||
const sessionId = parseSessionIdFromStdout(rawOutput);
|
||||
// --quiet mode: session_id may be on stdout or stderr
|
||||
const sessionId = parseSessionIdFromStdout(stderr) ?? parseSessionIdFromStdout(stdout);
|
||||
if (sessionId !== null) {
|
||||
const session = await loadHermesSession(sessionId);
|
||||
if (session !== null) {
|
||||
@@ -99,8 +100,8 @@ async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
}
|
||||
}
|
||||
|
||||
const detailHash = await storeHermesRawOutput(store, rawOutput);
|
||||
return { output: rawOutput, detailHash };
|
||||
const detailHash = await storeHermesRawOutput(store, stdout);
|
||||
return { output: stdout, detailHash };
|
||||
}
|
||||
|
||||
/** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */
|
||||
|
||||
@@ -24,19 +24,14 @@ export function getHermesSessionPath(sessionId: string): string {
|
||||
return join(getHermesSessionsDir(), `session_${sessionId}.json`);
|
||||
}
|
||||
|
||||
/** Parse `session_id: …` from the last non-empty line of Hermes stdout. */
|
||||
/** Parse `session_id: …` from any line of Hermes stdout. */
|
||||
export function parseSessionIdFromStdout(stdout: string): string | null {
|
||||
const lines = stdout.split(/\r?\n/).map((line) => line.trim());
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
if (line === undefined || line === "") {
|
||||
continue;
|
||||
}
|
||||
const match = SESSION_ID_LINE.exec(line);
|
||||
const lines = stdout.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const match = SESSION_ID_LINE.exec(line.trim());
|
||||
if (match?.[1] !== undefined) {
|
||||
return match[1];
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,6 @@ export type ThreadStepsOutput = {
|
||||
export type ThreadForkOutput = {
|
||||
thread: ThreadId;
|
||||
forkedFrom: {
|
||||
thread: ThreadId;
|
||||
step: CasRef;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user