Compare commits

...

1 Commits

Author SHA1 Message Date
xiaoju 24802f51db fix: address PR review — sessionId guard, resume error logging, atomic cache write
1. Guard against undefined sessionId before writing to cache
2. Log resume failures instead of silent catch
3. Atomic write (temp + rename) for session cache file
4. Add @uncaged/workflow-util dependency to claude-code agent

Refs #418
2026-05-23 08:03:39 +00:00
3 changed files with 26 additions and 7 deletions
@@ -22,7 +22,8 @@
},
"dependencies": {
"@uncaged/json-cas": "^0.4.0",
"@uncaged/workflow-agent-kit": "workspace:^"
"@uncaged/workflow-agent-kit": "workspace:^",
"@uncaged/workflow-util": "workspace:^"
},
"devDependencies": {
"typescript": "^5.8.3"
@@ -9,12 +9,15 @@ import {
getCachedSessionId,
setCachedSessionId,
} from "@uncaged/workflow-agent-kit";
import { createLogger } from "@uncaged/workflow-util";
import { parseClaudeCodeJsonOutput, storeClaudeCodeDetail } from "./session-detail.js";
const CLAUDE_COMMAND = "claude";
const CLAUDE_MAX_TURNS = 90;
const log = createLogger({ sink: { kind: "stderr" } });
function buildHistorySummary(steps: AgentContext["steps"]): string {
if (steps.length === 0) {
return "";
@@ -135,17 +138,22 @@ async function runClaudeCode(ctx: AgentContext): Promise<AgentRunResult> {
try {
const { stdout } = await spawnClaudeResume(cachedSessionId, fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store);
await setCachedSessionId(ctx.threadId, ctx.role, result.sessionId);
if (result.sessionId !== "") {
await setCachedSessionId(ctx.threadId, ctx.role, result.sessionId);
}
return result;
} catch {
// Resume failed — fall through to fresh run.
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
log("5VKR8N3Q", `session resume failed, falling back to new session: ${message}`);
}
}
}
const { stdout } = await spawnClaudeRun(fullPrompt);
const result = await processClaudeOutput(stdout, ctx.store);
await setCachedSessionId(ctx.threadId, ctx.role, result.sessionId);
if (result.sessionId !== "") {
await setCachedSessionId(ctx.threadId, ctx.role, result.sessionId);
}
return result;
}
@@ -1,4 +1,4 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type { ThreadId } from "@uncaged/workflow-protocol";
@@ -43,10 +43,20 @@ async function readCache(): Promise<SessionCache> {
}
}
/**
* Atomic write: write to a temp file, then rename.
* Prevents partial reads if another process reads mid-write.
* Note: read-modify-write is still not concurrency-safe across processes;
* the current workflow engine runs agent steps sequentially (execFileSync),
* so this is sufficient. If parallel execution is added later, a proper
* lockfile (e.g. proper-lockfile) will be needed.
*/
async function writeCache(cache: SessionCache): Promise<void> {
const path = getCachePath();
const tmpPath = `${path}.${process.pid}.tmp`;
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
await writeFile(tmpPath, `${JSON.stringify(cache, null, 2)}\n`, "utf8");
await rename(tmpPath, path);
}
/** Read the cached session ID for a thread+role pair. */