Compare commits
1 Commits
32bbea0f32
...
aaadab4445
| Author | SHA1 | Date | |
|---|---|---|---|
| aaadab4445 |
@@ -6,6 +6,7 @@ import {
|
||||
type AgentContext,
|
||||
type AgentRunResult,
|
||||
buildContinuationPrompt,
|
||||
buildFrontmatterRetryPrompt,
|
||||
buildRolePrompt,
|
||||
buildThreadProgress,
|
||||
createAgent,
|
||||
@@ -176,8 +177,12 @@ async function runClaudeCode(ctx: AgentContext, model: string | null): Promise<A
|
||||
|
||||
log("K7R2M4N8", `prompt for role=${ctx.role} (length=${fullPrompt.length}):\n${fullPrompt}`);
|
||||
|
||||
// Try resuming a cached session for re-entry scenarios (e.g. reviewer reject → developer re-entry).
|
||||
if (!ctx.isFirstVisit) {
|
||||
// Try resuming a cached session. This covers both normal re-entry
|
||||
// (e.g. reviewer reject → developer re-entry) AND the case where a
|
||||
// previous run completed but frontmatter validation failed — the step
|
||||
// was never written to CAS so isFirstVisit is still true, but the
|
||||
// session cache holds a valid session we should resume.
|
||||
{
|
||||
const cachedSessionId = await getCachedSessionId(
|
||||
"claude-code",
|
||||
ctx.threadId,
|
||||
@@ -185,13 +190,20 @@ 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,
|
||||
fullPrompt,
|
||||
resumePrompt,
|
||||
model,
|
||||
);
|
||||
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, fullPrompt);
|
||||
const result = await processClaudeOutput(stdout, stderr, exitCode, ctx.store, resumePrompt);
|
||||
if (result.sessionId !== undefined && result.sessionId !== "") {
|
||||
await setCachedSessionId(
|
||||
"claude-code",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type AgentContext,
|
||||
type AgentRunResult,
|
||||
buildContinuationPrompt,
|
||||
buildFrontmatterRetryPrompt,
|
||||
buildRolePrompt,
|
||||
buildThreadProgress,
|
||||
createAgent,
|
||||
@@ -102,6 +103,8 @@ 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(
|
||||
@@ -110,28 +113,36 @@ async function prepareSession(
|
||||
cwd: string,
|
||||
resumeDisabled: boolean,
|
||||
): Promise<PromptAttempt> {
|
||||
if (ctx.isFirstVisit || resumeDisabled) {
|
||||
if (resumeDisabled) {
|
||||
await client.connect(cwd);
|
||||
return { useContinuation: false, resumed: false };
|
||||
return { useContinuation: false, resumed: false, frontmatterRetry: false };
|
||||
}
|
||||
|
||||
// Check session cache regardless of isFirstVisit. A previous run may
|
||||
// have completed and cached its session but failed frontmatter
|
||||
// validation — the step never got written to CAS so isFirstVisit is
|
||||
// still true, yet we should resume the existing session.
|
||||
const cachedSessionId = await getCachedSessionId(ctx.threadId, ctx.role, ctx.storageRoot);
|
||||
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 };
|
||||
return { useContinuation: false, resumed: false, frontmatterRetry: false };
|
||||
}
|
||||
|
||||
try {
|
||||
await client.resume(cachedSessionId, cwd);
|
||||
log("9MHT4V2P", `resumed hermes session ${cachedSessionId} for ${ctx.threadId}:${ctx.role}`);
|
||||
return { useContinuation: true, resumed: true };
|
||||
return {
|
||||
useContinuation: !ctx.isFirstVisit,
|
||||
resumed: true,
|
||||
frontmatterRetry: ctx.isFirstVisit,
|
||||
};
|
||||
} 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 };
|
||||
return { useContinuation: false, resumed: false, frontmatterRetry: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +165,12 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
|
||||
ctx: AgentContext,
|
||||
useContinuation: boolean,
|
||||
beforeTurns: TurnsSnapshot,
|
||||
frontmatterRetry: boolean,
|
||||
): Promise<AgentRunResult> {
|
||||
const effectiveCtx = useContinuation ? ctx : { ...ctx, isFirstVisit: true };
|
||||
const fullPrompt = buildHermesPrompt(effectiveCtx);
|
||||
// Frontmatter retry: session has full context, just re-output the format.
|
||||
const fullPrompt = frontmatterRetry
|
||||
? buildFrontmatterRetryPrompt(ctx.outputFormatInstruction)
|
||||
: buildHermesPrompt(useContinuation ? ctx : { ...ctx, isFirstVisit: true });
|
||||
const startMs = Date.now();
|
||||
const { text, sessionId, usage: acpUsage } = await client.prompt(fullPrompt);
|
||||
const durationSec = (Date.now() - startMs) / 1000;
|
||||
@@ -188,7 +202,7 @@ export function createHermesAgent(resumeDisabled: boolean): () => Promise<void>
|
||||
const beforeTurns = snapshotTurns(beforeSession);
|
||||
|
||||
try {
|
||||
return await runPrompt(ctx, attempt.useContinuation, beforeTurns);
|
||||
return await runPrompt(ctx, attempt.useContinuation, beforeTurns, attempt.frontmatterRetry);
|
||||
} catch (error) {
|
||||
if (!attempt.resumed) {
|
||||
throw error;
|
||||
@@ -199,7 +213,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);
|
||||
return runPrompt(ctx, false, ZERO_TURNS, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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";
|
||||
|
||||
Reference in New Issue
Block a user