-
Notifications
You must be signed in to change notification settings - Fork 0
Emit crash-safe v2 eval campaigns #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
.agents/plans/02-eval-engineering/evidence/phase-4-pilot.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "schemaVersion": 1, | ||
| "reportId": "flow-v2-2026-08-25T04-34-58-778Z", | ||
| "scenario": "happy-path", | ||
| "passed": true, | ||
| "planSha256": "sha256:79325b57e4fba2e823b503a8f50758e32a69d7907604d0ce105c4a8bc67c19da", | ||
| "cellCount": 1, | ||
| "attemptCount": 1, | ||
| "completionStatus": "complete", | ||
| "completionCause": "fixed-target", | ||
| "outputTokens": 3238, | ||
| "costUsd": 0.1978994, | ||
| "artifactTarballSha256": "sha256:c89f7363248ccc3e3f69728c1aa42044a25938cd533e470d9f73ef08bc64ad24", | ||
| "attemptOutcome": "product", | ||
| "managerActualIdentity": "unobserved-full-v2-identity", | ||
| "reviewerActualIdentity": "unobserved-full-v2-identity", | ||
| "instructionCount": 3, | ||
| "transcriptSha256": "sha256:588356d860cec23ab95d12f39f4d91f67ff3dd2a87c9849c1fc1a4719d1bc52d", | ||
| "strictParsePassed": true | ||
| } |
16 changes: 16 additions & 0 deletions
16
.agents/plans/02-eval-engineering/evidence/phase-4-review.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Phase 4 Interrogate review | ||
|
|
||
| Phase 4 freezes cells before launch, publishes one immutable transcript and | ||
| attempt per cell, and finalizes only reports accepted by the strict v2 parser. | ||
|
|
||
| The four-model review fixed three integrity gaps. Transcript SHA-256 now comes | ||
| from stored bytes and must equal the provenance digest. Attempts publish through | ||
| one atomic cell-keyed no-replace claim, so concurrent attempt IDs cannot both win. | ||
| Handled persistence failures clean temporary files while real crash leftovers are | ||
| ignored during reconciliation. Host and mid-flight attempt errors finalize with a | ||
| host stop cause. | ||
|
|
||
| The final recheck found no unresolved blocker. Store fault-injection, concurrent | ||
| writer, replay, transcript, truncated-ledger, report, and full product gates pass. | ||
| The final paid pilot emitted a complete v2 report which independently parsed with | ||
| one product attempt and no placeholder provenance or policy. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| import { createHash } from "node:crypto"; | ||
| import { link, mkdir, open, readdir, readFile, unlink } from "node:fs/promises"; | ||
| import { dirname, join } from "node:path"; | ||
| import { canonicalJson } from "./canonical-json.js"; | ||
| import type { ValidatedCaseCatalog } from "./catalog.js"; | ||
| import { | ||
| type AttemptRecordV2, | ||
| type CampaignCompletion, | ||
| type CampaignPlan, | ||
| CampaignPlanSchema, | ||
| parseReport, | ||
| type ValidatedReport, | ||
| } from "./report.js"; | ||
|
|
||
| export type PersistenceCheckpoint = | ||
| | "before-write" | ||
| | "after-file-sync" | ||
| | "after-rename" | ||
| | "before-directory-sync"; | ||
|
|
||
| export type ReportStoreHooks = { | ||
| readonly checkpoint?: (checkpoint: PersistenceCheckpoint) => Promise<void>; | ||
| }; | ||
|
|
||
| export class ReportStoreError extends Error { | ||
| readonly code = "FLOW_REPORT_STORE"; | ||
| } | ||
|
|
||
| type StoredAttempt = { | ||
| readonly file: string; | ||
| readonly value: unknown; | ||
| readonly cellId: string | null; | ||
| }; | ||
|
|
||
| function fail(message: string, cause?: unknown): never { | ||
| throw new ReportStoreError(message, cause ? { cause } : undefined); | ||
| } | ||
|
|
||
| function isRecord(value: unknown): value is Record<string, unknown> { | ||
| return value !== null && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
|
|
||
| function cellId(value: unknown): string | null { | ||
| return isRecord(value) && typeof value.cellId === "string" | ||
| ? value.cellId | ||
| : null; | ||
| } | ||
|
|
||
| function attemptFileName(attemptId: string): string { | ||
| return `${Buffer.from(attemptId).toString("base64url")}.json`; | ||
| } | ||
|
|
||
| function cellFileName(cellId: string): string { | ||
| return `${Buffer.from(cellId).toString("base64url")}.json`; | ||
| } | ||
|
|
||
| function temporaryPath(path: string): string { | ||
| return `${path}.tmp-${process.pid}-${crypto.randomUUID()}`; | ||
| } | ||
|
|
||
| function sha256(bytes: Uint8Array): string { | ||
| return `sha256:${createHash("sha256").update(bytes).digest("hex")}`; | ||
| } | ||
|
|
||
| async function syncDirectory(directory: string): Promise<void> { | ||
| try { | ||
| const handle = await open(directory, "r"); | ||
| try { | ||
| await handle.sync(); | ||
| } finally { | ||
| await handle.close(); | ||
| } | ||
| } catch { | ||
| // Windows and some filesystems do not permit opening directories for sync. | ||
| } | ||
| } | ||
|
|
||
| async function checkpoint( | ||
| hooks: ReportStoreHooks, | ||
| name: PersistenceCheckpoint, | ||
| ): Promise<void> { | ||
| await hooks.checkpoint?.(name); | ||
| } | ||
|
|
||
| async function writeImmutable( | ||
| path: string, | ||
| bytes: Buffer, | ||
| hooks: ReportStoreHooks, | ||
| ): Promise<"written" | "replayed"> { | ||
| try { | ||
| const existing = await readFile(path); | ||
| if (existing.equals(bytes)) return "replayed"; | ||
| fail(`Immutable report store entry conflicts: ${path}.`); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; | ||
| } | ||
|
|
||
| await checkpoint(hooks, "before-write"); | ||
| const temporary = temporaryPath(path); | ||
| const handle = await open(temporary, "wx", 0o600); | ||
| try { | ||
| await handle.writeFile(bytes); | ||
| await handle.sync(); | ||
| } finally { | ||
| await handle.close(); | ||
| } | ||
| try { | ||
| await checkpoint(hooks, "after-file-sync"); | ||
| try { | ||
| await link(temporary, path); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; | ||
| const existing = await readFile(path); | ||
| await unlink(temporary); | ||
| await syncDirectory(dirname(path)); | ||
| if (existing.equals(bytes)) return "replayed"; | ||
| fail(`Immutable report store entry conflicts: ${path}.`); | ||
| } | ||
| await checkpoint(hooks, "after-rename"); | ||
| await unlink(temporary); | ||
| await checkpoint(hooks, "before-directory-sync"); | ||
| await syncDirectory(dirname(path)); | ||
| return "written"; | ||
| } catch (error) { | ||
| await unlink(temporary).catch(() => {}); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function readJson(path: string): Promise<unknown> { | ||
| try { | ||
| return JSON.parse(await readFile(path, "utf8")); | ||
| } catch (error) { | ||
| fail(`Could not read report store JSON: ${path}.`, error); | ||
| } | ||
| } | ||
|
|
||
| export class ReportStore { | ||
| private readonly attemptsDirectory: string; | ||
| private readonly transcriptsDirectory: string; | ||
| private readonly planPath: string; | ||
| private readonly completionPath: string; | ||
| private readonly reportPath: string; | ||
| private readonly catalog: ValidatedCaseCatalog; | ||
| private readonly hooks: ReportStoreHooks; | ||
|
|
||
| constructor( | ||
| directory: string, | ||
| catalog: ValidatedCaseCatalog, | ||
| hooks: ReportStoreHooks = {}, | ||
| ) { | ||
| this.catalog = catalog; | ||
| this.hooks = hooks; | ||
| this.attemptsDirectory = join(directory, "attempts"); | ||
| this.transcriptsDirectory = join(directory, "transcripts"); | ||
| this.planPath = join(directory, "plan.json"); | ||
| this.completionPath = join(directory, "completion.json"); | ||
| this.reportPath = join(directory, "report.json"); | ||
| } | ||
|
|
||
| async initialize(plan: CampaignPlan): Promise<"written" | "replayed"> { | ||
| await mkdir(this.attemptsDirectory, { recursive: true, mode: 0o700 }); | ||
| return writeImmutable( | ||
| this.planPath, | ||
| Buffer.from(canonicalJson(plan)), | ||
| this.hooks, | ||
| ); | ||
| } | ||
|
|
||
| private async plan(): Promise<CampaignPlan> { | ||
| const parsed = CampaignPlanSchema.safeParse(await readJson(this.planPath)); | ||
| if (!parsed.success) fail("Stored campaign plan is invalid."); | ||
| return parsed.data; | ||
| } | ||
|
|
||
| private async attempts(): Promise<readonly StoredAttempt[]> { | ||
| let files: string[]; | ||
| try { | ||
| files = await readdir(this.attemptsDirectory); | ||
| } catch (error) { | ||
| if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; | ||
| throw error; | ||
| } | ||
| const attempts: StoredAttempt[] = []; | ||
| for (const file of files | ||
| .filter((entry) => entry.endsWith(".json")) | ||
| .sort()) { | ||
| const value = await readJson(join(this.attemptsDirectory, file)); | ||
| attempts.push({ file, value, cellId: cellId(value) }); | ||
| } | ||
| return attempts; | ||
| } | ||
|
|
||
| async writeAttempt( | ||
| attempt: AttemptRecordV2, | ||
| ): Promise<"written" | "replayed"> { | ||
| const plan = await this.plan(); | ||
| if (!plan.cells.some((cell) => cell.cellId === attempt.cellId)) { | ||
| fail(`Attempt references an unknown plan cell: ${attempt.cellId}.`); | ||
| } | ||
| return writeImmutable( | ||
| join(this.attemptsDirectory, cellFileName(attempt.cellId)), | ||
| Buffer.from(canonicalJson(attempt)), | ||
| this.hooks, | ||
| ); | ||
| } | ||
|
|
||
| async writeTranscript(input: { | ||
| readonly attemptId: string; | ||
| readonly text: string; | ||
| }): Promise<{ readonly artifact: string; readonly sha256: string }> { | ||
| await mkdir(this.transcriptsDirectory, { recursive: true, mode: 0o700 }); | ||
| const artifact = `transcripts/${attemptFileName(input.attemptId)}`; | ||
| const bytes = Buffer.from(input.text, "utf8"); | ||
| await writeImmutable( | ||
| join(this.transcriptsDirectory, attemptFileName(input.attemptId)), | ||
| bytes, | ||
| this.hooks, | ||
| ); | ||
| return { artifact, sha256: sha256(bytes) }; | ||
| } | ||
|
|
||
| private orderedAttempts( | ||
| plan: CampaignPlan, | ||
| attempts: readonly StoredAttempt[], | ||
| ): readonly unknown[] { | ||
| const ordered: unknown[] = []; | ||
| const consumed = new Set<string>(); | ||
| for (const cell of plan.cells) { | ||
| for (const attempt of attempts) { | ||
| if (attempt.cellId === cell.cellId) { | ||
| ordered.push(attempt.value); | ||
| consumed.add(attempt.file); | ||
| } | ||
| } | ||
| } | ||
| for (const attempt of attempts) { | ||
| if (!consumed.has(attempt.file)) ordered.push(attempt.value); | ||
| } | ||
| return ordered; | ||
| } | ||
|
|
||
| async finalize(input: { | ||
| readonly reportId: string; | ||
| readonly completion: CampaignCompletion; | ||
| readonly allocationCommitmentSha256: string | null; | ||
| }): Promise<ValidatedReport> { | ||
| const plan = await this.plan(); | ||
| const report = { | ||
| schemaVersion: 2, | ||
| reportId: input.reportId, | ||
| plan, | ||
| attempts: this.orderedAttempts(plan, await this.attempts()), | ||
| completion: input.completion, | ||
| allocationCommitmentSha256: input.allocationCommitmentSha256, | ||
| }; | ||
| const parsed = parseReport(report, this.catalog); | ||
| if (!parsed.ok) { | ||
| fail( | ||
| `Refusing to finalize invalid report: ${parsed.issues | ||
| .map((issue) => `${issue.path} ${issue.message}`) | ||
| .join("; ")}`, | ||
| ); | ||
| } | ||
| const completionBytes = Buffer.from(canonicalJson(input.completion)); | ||
| const reportBytes = Buffer.from(canonicalJson(report)); | ||
| await writeImmutable(this.completionPath, completionBytes, this.hooks); | ||
| await writeImmutable(this.reportPath, reportBytes, this.hooks); | ||
| return parsed.value; | ||
| } | ||
| } | ||
|
|
||
| export function createReportStore(input: { | ||
| readonly directory: string; | ||
| readonly catalog: ValidatedCaseCatalog; | ||
| readonly hooks?: ReportStoreHooks; | ||
| }): ReportStore { | ||
| return new ReportStore(input.directory, input.catalog, input.hooks); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before finalizing a campaign, verify that each referenced transcript exists under the report directory and that its stored bytes match
attempt.transcript.sha256. Currentlyfinalize()passes ledger values directly toparseReport(), which only validates the artifact path and digest format; as demonstrated by the existing finalization tests, a report with no transcript files at all is accepted, so deleted, tampered, or never-written evidence can silently produce a validated report.Useful? React with 👍 / 👎.