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)
|
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(
|
export async function movePromptFile(
|
||||||
promptFilePath: string,
|
promptFilePath: string,
|
||||||
base: string,
|
base: string,
|
||||||
@@ -35,12 +62,8 @@ export async function movePromptFile(
|
|||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const promptFilename = "council-prompt.md"
|
const promptFilename = "council-prompt.md"
|
||||||
const absPromptSrc = isAbsolute(promptFilePath) ? promptFilePath : resolve(base, promptFilePath)
|
const absPromptSrc = resolvePromptTempFilePath(promptFilePath, base)
|
||||||
const expectedPromptRoot = join(base, ".sisyphus", "tmp")
|
if (!absPromptSrc) return undefined
|
||||||
if (isPathEscaping(expectedPromptRoot, absPromptSrc)) {
|
|
||||||
log("[council-finalize] Rejected prompt_file outside .sisyphus/tmp/", { promptFile: promptFilePath })
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
const absPromptDest = join(absArchiveDir, promptFilename)
|
const absPromptDest = join(absArchiveDir, promptFilename)
|
||||||
await rename(absPromptSrc, absPromptDest).catch(async (renameErr) => {
|
await rename(absPromptSrc, absPromptDest).catch(async (renameErr) => {
|
||||||
log("[council-finalize] Rename failed, falling back to copy", { promptFile: promptFilePath, error: String(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 { join } from "node:path"
|
||||||
import { randomBytes } from "node:crypto"
|
import { randomBytes } from "node:crypto"
|
||||||
import { extractCouncilResponse } from "./council-response-extractor"
|
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 { formatMetaYaml, type MetaMember } from "./meta-yaml-formatter"
|
||||||
import {
|
import {
|
||||||
buildAthenaRuntimeGuidance,
|
buildAthenaRuntimeGuidance,
|
||||||
@@ -43,6 +43,8 @@ export function createCouncilFinalize(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base = basePath ?? process.cwd()
|
const base = basePath ?? process.cwd()
|
||||||
|
|
||||||
|
try {
|
||||||
const hexId = randomBytes(COUNCIL_DEFAULTS.ARCHIVE_ID_BYTES).toString("hex")
|
const hexId = randomBytes(COUNCIL_DEFAULTS.ARCHIVE_ID_BYTES).toString("hex")
|
||||||
const safeName = slugify(args.name) || "unnamed"
|
const safeName = slugify(args.name) || "unnamed"
|
||||||
const archiveName = `council-${safeName}-${hexId}`
|
const archiveName = `council-${safeName}-${hexId}`
|
||||||
@@ -79,12 +81,11 @@ export function createCouncilFinalize(
|
|||||||
continue
|
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 relTaskOutput = join(".sisyphus", "task-outputs", `${taskId}.md`)
|
||||||
const relTaskOutputForOutput = toPosixPath(relTaskOutput)
|
const relTaskOutputForOutput = toPosixPath(relTaskOutput)
|
||||||
const absTaskOutput = join(base, relTaskOutput)
|
const absTaskOutput = join(base, relTaskOutput)
|
||||||
|
|
||||||
|
|
||||||
let fileContent: string
|
let fileContent: string
|
||||||
try {
|
try {
|
||||||
fileContent = await readFile(absTaskOutput, "utf-8")
|
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 resolvedMode = (args.mode === "non-interactive" ? "non-interactive" : "interactive") as CouncilGuidanceMode
|
||||||
const guidance = buildAthenaRuntimeGuidance(resolvedIntent, resolvedMode)
|
const guidance = buildAthenaRuntimeGuidance(resolvedIntent, resolvedMode)
|
||||||
return JSON.stringify(result, null, 2) + "\n\n" + guidance
|
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")
|
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 })
|
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:
|
Use this path in each council member's task() call:
|
||||||
- prompt: "Read ${filePath} for your instructions."
|
- 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) {
|
} catch (err) {
|
||||||
return `Error saving council prompt: ${String(err)}`
|
return `Error saving council prompt: ${String(err)}`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user