fix(athena): tie prompt-file lifecycle to council_finalize
Remove blind 30-min setTimeout cleanup from prepare_council_prompt. Prompt files now stay until council_finalize archives them in a try/finally block. Extract shared resolvePromptTempFilePath helper used by both movePromptFile and the new cleanupPromptFile safety net. Co-authored-by: Vacbo <2445>
This commit is contained in:
@@ -27,6 +27,33 @@ export function isPathEscaping(expectedRoot: string, targetPath: string): boolea
|
||||
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute(rel)
|
||||
}
|
||||
|
||||
function resolvePromptTempFilePath(promptFilePath: string, base: string): string | undefined {
|
||||
const absPromptPath = isAbsolute(promptFilePath) ? promptFilePath : resolve(base, promptFilePath)
|
||||
const expectedPromptRoot = join(base, ".sisyphus", "tmp")
|
||||
|
||||
if (isPathEscaping(expectedPromptRoot, absPromptPath)) {
|
||||
log("[council-finalize] Rejected prompt_file outside .sisyphus/tmp/", { promptFile: promptFilePath })
|
||||
return undefined
|
||||
}
|
||||
|
||||
return absPromptPath
|
||||
}
|
||||
|
||||
export async function cleanupPromptFile(promptFilePath: string, base: string): Promise<void> {
|
||||
const absPromptPath = resolvePromptTempFilePath(promptFilePath, base)
|
||||
if (!absPromptPath) return
|
||||
|
||||
try {
|
||||
await unlink(absPromptPath)
|
||||
log("[council-finalize] Cleaned up prompt temp file", { promptFile: promptFilePath })
|
||||
} catch (err) {
|
||||
const code = (err as NodeJS.ErrnoException).code
|
||||
if (code !== "ENOENT") {
|
||||
log("[council-finalize] Failed to clean up prompt temp file", { promptFile: promptFilePath, error: String(err), code })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function movePromptFile(
|
||||
promptFilePath: string,
|
||||
base: string,
|
||||
@@ -35,12 +62,8 @@ export async function movePromptFile(
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const promptFilename = "council-prompt.md"
|
||||
const absPromptSrc = isAbsolute(promptFilePath) ? promptFilePath : resolve(base, promptFilePath)
|
||||
const expectedPromptRoot = join(base, ".sisyphus", "tmp")
|
||||
if (isPathEscaping(expectedPromptRoot, absPromptSrc)) {
|
||||
log("[council-finalize] Rejected prompt_file outside .sisyphus/tmp/", { promptFile: promptFilePath })
|
||||
return undefined
|
||||
}
|
||||
const absPromptSrc = resolvePromptTempFilePath(promptFilePath, base)
|
||||
if (!absPromptSrc) return undefined
|
||||
const absPromptDest = join(absArchiveDir, promptFilename)
|
||||
await rename(absPromptSrc, absPromptDest).catch(async (renameErr) => {
|
||||
log("[council-finalize] Rename failed, falling back to copy", { promptFile: promptFilePath, error: String(renameErr) })
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFile, writeFile, mkdir } from "node:fs/promises"
|
||||
import { join } from "node:path"
|
||||
import { randomBytes } from "node:crypto"
|
||||
import { extractCouncilResponse } from "./council-response-extractor"
|
||||
import { TASK_ID_PATTERN, slugify, toPosixPath, extractAgentFromFrontmatter, isPathEscaping, movePromptFile } from "./council-finalize-helpers"
|
||||
import { TASK_ID_PATTERN, slugify, toPosixPath, extractAgentFromFrontmatter, isPathEscaping, movePromptFile, cleanupPromptFile } from "./council-finalize-helpers"
|
||||
import { formatMetaYaml, type MetaMember } from "./meta-yaml-formatter"
|
||||
import {
|
||||
buildAthenaRuntimeGuidance,
|
||||
@@ -43,6 +43,8 @@ export function createCouncilFinalize(
|
||||
}
|
||||
|
||||
const base = basePath ?? process.cwd()
|
||||
|
||||
try {
|
||||
const hexId = randomBytes(COUNCIL_DEFAULTS.ARCHIVE_ID_BYTES).toString("hex")
|
||||
const safeName = slugify(args.name) || "unnamed"
|
||||
const archiveName = `council-${safeName}-${hexId}`
|
||||
@@ -79,12 +81,11 @@ export function createCouncilFinalize(
|
||||
continue
|
||||
}
|
||||
|
||||
// Task ID is pre-validated by TASK_ID_PATTERN — path escaping is impossible
|
||||
// Task ID is pre-validated by TASK_ID_PATTERN - path escaping is impossible
|
||||
const relTaskOutput = join(".sisyphus", "task-outputs", `${taskId}.md`)
|
||||
const relTaskOutputForOutput = toPosixPath(relTaskOutput)
|
||||
const absTaskOutput = join(base, relTaskOutput)
|
||||
|
||||
|
||||
let fileContent: string
|
||||
try {
|
||||
fileContent = await readFile(absTaskOutput, "utf-8")
|
||||
@@ -162,6 +163,11 @@ export function createCouncilFinalize(
|
||||
const resolvedMode = (args.mode === "non-interactive" ? "non-interactive" : "interactive") as CouncilGuidanceMode
|
||||
const guidance = buildAthenaRuntimeGuidance(resolvedIntent, resolvedMode)
|
||||
return JSON.stringify(result, null, 2) + "\n\n" + guidance
|
||||
} finally {
|
||||
if (args.prompt_file) {
|
||||
await cleanupPromptFile(args.prompt_file, base)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -111,14 +111,6 @@ Returns the file path to reference in subsequent task() calls.`
|
||||
|
||||
await writeFile(filePath, content, "utf-8")
|
||||
|
||||
setTimeout(() => {
|
||||
unlink(filePath).catch((err) => {
|
||||
const code = (err as NodeJS.ErrnoException).code
|
||||
if (code !== "ENOENT") {
|
||||
log("[prepare-council-prompt] Failed to clean up temp file", { filePath, error: String(err) })
|
||||
}
|
||||
})
|
||||
}, CLEANUP_DELAY_MS)
|
||||
|
||||
log("[prepare-council-prompt] Saved prompt", { filePath, length: args.prompt.length, mode })
|
||||
|
||||
@@ -127,7 +119,7 @@ Returns the file path to reference in subsequent task() calls.`
|
||||
Use this path in each council member's task() call:
|
||||
- prompt: "Read ${filePath} for your instructions."
|
||||
|
||||
The file auto-deletes after 30 minutes.`
|
||||
The file stays in .sisyphus/tmp until council_finalize archives and cleans it up.`
|
||||
} catch (err) {
|
||||
return `Error saving council prompt: ${String(err)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user