Compare commits

..

1 Commits

Author SHA1 Message Date
xiaoju 32bbea0f32 fix: decouple session resume from isFirstVisit guard
CI / check (pull_request) Successful in 1m58s
When frontmatter validation fails, the step is never written to CAS, so
isFirstVisit remains true on the next run.  Both agent-claude-code and
agent-hermes gated session cache lookup behind !isFirstVisit, which
caused them to start a fresh session (and a new worktree) instead of
resuming the one that already has all the work done.

Remove the isFirstVisit guard from both adapters so they always check
the session cache.  If no cache entry exists, the behavior is unchanged
(fresh session).  If a cache entry exists, the agent resumes the
previous session regardless of CAS step state.

Fixes #139
2026-06-07 01:49:04 +00:00
5 changed files with 10 additions and 73 deletions
+2 -10
View File
@@ -6,7 +6,6 @@ import {
type AgentContext,
type AgentRunResult,
buildContinuationPrompt,
buildFrontmatterRetryPrompt,
buildRolePrompt,
buildThreadProgress,
createAgent,
@@ -190,20 +189,13 @@ async function runClaudeCode(ctx: AgentContext, model: string | null): Promise<A
ctx.storageRoot,
);
if (cachedSessionId !== null) {
// isFirstVisit + cache hit = previous run completed but frontmatter
// validation failed. The session already has full context — send a
// minimal correction prompt instead of the full initial prompt.
const resumePrompt = ctx.isFirstVisit
? buildFrontmatterRetryPrompt(ctx.outputFormatInstruction)
: fullPrompt;
try {
const { stdout, stderr, exitCode } = await spawnClaudeResume(
cachedSessionId,
resumePrompt,
fullPrompt,
model,
);
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, resumePrompt);
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
if (result.sessionId !== undefined && result.sessionId !== "") {
await setCachedSessionId(
"claude-code",
+8 -18
View File
@@ -5,7 +5,6 @@ import {
type AgentContext,
type AgentRunResult,
buildContinuationPrompt,
buildFrontmatterRetryPrompt,
buildRolePrompt,
buildThreadProgress,
createAgent,
@@ -103,8 +102,6 @@ async function storePromptResult(store: Store, sessionId: string): Promise<{ det
type PromptAttempt = {
useContinuation: boolean;
resumed: boolean;
/** True when resuming after a frontmatter-only failure (isFirstVisit + cache hit). */
frontmatterRetry: boolean;
};
async function prepareSession(
@@ -115,7 +112,7 @@ async function prepareSession(
): Promise<PromptAttempt> {
if (resumeDisabled) {
await client.connect(cwd);
return { useContinuation: false, resumed: false, frontmatterRetry: false };
return { useContinuation: false, resumed: false };
}
// Check session cache regardless of isFirstVisit. A previous run may
@@ -126,23 +123,19 @@ async function prepareSession(
if (cachedSessionId === null) {
log("6RWK3N8Q", `no cached session for ${ctx.threadId}:${ctx.role}, starting new session`);
await client.connect(cwd);
return { useContinuation: false, resumed: false, frontmatterRetry: false };
return { useContinuation: false, resumed: false };
}
try {
await client.resume(cachedSessionId, cwd);
log("9MHT4V2P", `resumed hermes session ${cachedSessionId} for ${ctx.threadId}:${ctx.role}`);
return {
useContinuation: !ctx.isFirstVisit,
resumed: true,
frontmatterRetry: ctx.isFirstVisit,
};
return { useContinuation: true, resumed: true };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log("3XPN7K4W", `session resume failed, falling back to new session: ${message}`);
await client.close();
await client.connect(cwd);
return { useContinuation: false, resumed: false, frontmatterRetry: false };
return { useContinuation: false, resumed: false };
}
}
@@ -165,12 +158,9 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
ctx: AgentContext,
useContinuation: boolean,
beforeTurns: TurnsSnapshot,
frontmatterRetry: boolean,
): Promise<AgentRunResult> {
// Frontmatter retry: session has full context, just re-output the format.
const fullPrompt = frontmatterRetry
? buildFrontmatterRetryPrompt(ctx.outputFormatInstruction)
: buildHermesPrompt(useContinuation ? ctx : { ...ctx, isFirstVisit: true });
const effectiveCtx = useContinuation ? ctx : { ...ctx, isFirstVisit: true };
const fullPrompt = buildHermesPrompt(effectiveCtx);
const startMs = Date.now();
const { text, sessionId, usage: acpUsage } = await client.prompt(fullPrompt);
const durationSec = (Date.now() - startMs) / 1000;
@@ -202,7 +192,7 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
const beforeTurns = snapshotTurns(beforeSession);
try {
return await runPrompt(ctx, attempt.useContinuation, beforeTurns, attempt.frontmatterRetry);
return await runPrompt(ctx, attempt.useContinuation, beforeTurns);
} catch (error) {
if (!attempt.resumed) {
throw error;
@@ -213,7 +203,7 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
await client.close();
await client.connect(cwd);
// Fresh session after retry — reset snapshot to zero
return runPrompt(ctx, false, ZERO_TURNS, false);
return runPrompt(ctx, false, ZERO_TURNS);
}
}
@@ -1,23 +0,0 @@
import { describe, expect, test } from "vitest";
import { buildFrontmatterRetryPrompt } from "../src/frontmatter-retry-prompt.js";
describe("buildFrontmatterRetryPrompt", () => {
test("includes correction instruction", () => {
const result = buildFrontmatterRetryPrompt("Use YAML frontmatter");
expect(result).toContain("previous run completed");
expect(result).toContain("do NOT need to redo any work");
expect(result).toContain("corrected YAML frontmatter");
});
test("includes outputFormatInstruction when provided", () => {
const instruction = "---\nstatus: $done | $review\nsummary: string\n---";
const result = buildFrontmatterRetryPrompt(instruction);
expect(result).toContain(instruction);
});
test("works with empty outputFormatInstruction", () => {
const result = buildFrontmatterRetryPrompt("");
expect(result).not.toContain("\n\n\n");
expect(result).toContain("corrected YAML frontmatter");
});
});
@@ -1,21 +0,0 @@
/**
* Build a minimal prompt for retrying frontmatter output on a resumed session.
*
* Used when a previous run completed successfully but frontmatter validation
* failed — the session already has full context, we just need the agent to
* re-output correctly formatted frontmatter without redoing any work.
*/
export function buildFrontmatterRetryPrompt(outputFormatInstruction: string): string {
const parts: string[] = [
"Your previous run completed all work successfully, but the output format was incorrect.",
"You do NOT need to redo any work — all changes are already in place.",
"",
];
if (outputFormatInstruction !== "") {
parts.push(outputFormatInstruction, "");
}
parts.push(
"Please output ONLY the corrected YAML frontmatter block (--- delimited) followed by a brief summary of the work you completed.",
);
return parts.join("\n");
}
-1
View File
@@ -12,7 +12,6 @@ export {
} from "./extract.js";
export type { FrontmatterFastPathResult } from "./frontmatter.js";
export { tryFrontmatterFastPath } from "./frontmatter.js";
export { buildFrontmatterRetryPrompt } from "./frontmatter-retry-prompt.js";
export { createAgent, parseArgv } from "./run.js";
export { getCachedSessionId, getCachePath, setCachedSessionId } from "./session-cache.js";
export { getConfigPath, getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";