Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions src/cli/commands/record-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type RecordExportResult,
} from '../../protocol/messages.js';
import {
appendArtifact,
appendArtifactWithRollback,
createArtifactEntry,
} from '../../storage/artifactManifest.js';
import {
Expand Down Expand Up @@ -371,18 +371,26 @@ export async function runRecordExportCommand(
sha256 = await computeFileHash(artifactOutputPath);
}

await appendArtifact(
sessionDirectory,
createArtifactEntry({
kind: artifactKind,
filename: basename(artifactOutputPath),
sessionId: manifest.sessionId,
capturedAtSeq,
sha256,
bytes,
metadata: artifactMetadata,
}),
);
const artifactEntry = createArtifactEntry({
kind: artifactKind,
filename: basename(artifactOutputPath),
sessionId: manifest.sessionId,
capturedAtSeq,
sha256,
bytes,
metadata: artifactMetadata,
});

// Explicit --out files belong to the user and are valid without a
// manifest entry; only clean up default in-session artifacts that would
// otherwise be orphaned.
await appendArtifactWithRollback({
sessionDir: sessionDirectory,
Comment thread
ThomasK33 marked this conversation as resolved.
entry: artifactEntry,
...(options.out === undefined
? { rollbackArtifactPath: artifactOutputPath }
: {}),
Comment thread
ThomasK33 marked this conversation as resolved.
});

const rawResult = {
sessionId: manifest.sessionId,
Expand Down
47 changes: 25 additions & 22 deletions src/screenshot/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { RendererBackend } from '../renderer/backend.js';
import { ScreenshotResultSchema } from '../protocol/schemas.js';
import { parseValidatedResult } from '../protocol/validation.js';
import {
appendArtifact,
appendArtifactWithRollback,
createArtifactEntry,
} from '../storage/artifactManifest.js';
import {
Expand Down Expand Up @@ -133,28 +133,31 @@ export async function captureScreenshotResult(
sha256: parsedResult.sha256,
};

const artifactEntry = createArtifactEntry({
kind: 'screenshot',
filename,
sessionId: publicResult.sessionId,
capturedAtSeq: publicResult.capturedAtSeq,
sha256,
metadata: {
profileName: publicResult.profileName,
cols: publicResult.cols,
rows: publicResult.rows,
pngSizeBytes: publicResult.pngSizeBytes,
cursorVisible: publicResult.cursorVisible,
rendererBackend: publicResult.rendererBackend,
pixelWidth: publicResult.pixelWidth,
pixelHeight: publicResult.pixelHeight,
renderProfileHash: publicResult.renderProfileHash,
},
});

await rename(temporaryOutputPath, finalArtifactPath);
await appendArtifact(
options.sessionDir,
createArtifactEntry({
kind: 'screenshot',
filename,
sessionId: publicResult.sessionId,
capturedAtSeq: publicResult.capturedAtSeq,
sha256,
metadata: {
profileName: publicResult.profileName,
cols: publicResult.cols,
rows: publicResult.rows,
pngSizeBytes: publicResult.pngSizeBytes,
cursorVisible: publicResult.cursorVisible,
rendererBackend: publicResult.rendererBackend,
pixelWidth: publicResult.pixelWidth,
pixelHeight: publicResult.pixelHeight,
renderProfileHash: publicResult.renderProfileHash,
},
}),
);
await appendArtifactWithRollback({
sessionDir: options.sessionDir,
entry: artifactEntry,
rollbackArtifactPath: finalArtifactPath,
});

return publicResult;
} catch (error) {
Expand Down
45 changes: 24 additions & 21 deletions src/snapshot/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ERROR_CODES, makeCliError } from '../protocol/errors.js';
import { SnapshotResultSchema } from '../protocol/schemas.js';
import { parseValidatedResult } from '../protocol/validation.js';
import {
appendArtifact,
appendArtifactWithRollback,
createArtifactEntry,
} from '../storage/artifactManifest.js';
import {
Expand Down Expand Up @@ -126,33 +126,36 @@ export async function persistSnapshotArtifact(
);
const snapshotArtifactPath = artifactPath(options.sessionDir, filename);

const artifactEntry = createArtifactEntry({
kind: 'snapshot',
filename,
sessionId: options.snapshot.sessionId,
capturedAtSeq: options.snapshot.capturedAtSeq,
metadata: {
format: options.format,
rendererBackend: options.rendererBackend,
cols: options.snapshot.cols,
rows: options.snapshot.rows,
cursorRow: options.snapshot.cursorRow,
cursorCol: options.snapshot.cursorCol,
...(options.snapshot.scrollbackLines === undefined
? {}
: { scrollbackLineCount: options.snapshot.scrollbackLines.length }),
},
});

await writeTextFileAtomic({
path: snapshotArtifactPath,
pathLabel: 'snapshot artifact path',
contents: `${JSON.stringify(options.result, null, 2)}\n`,
writeErrorMessage: `Failed to write snapshot artifact at ${snapshotArtifactPath}.`,
});

await appendArtifact(
options.sessionDir,
createArtifactEntry({
kind: 'snapshot',
filename,
sessionId: options.snapshot.sessionId,
capturedAtSeq: options.snapshot.capturedAtSeq,
metadata: {
format: options.format,
rendererBackend: options.rendererBackend,
cols: options.snapshot.cols,
rows: options.snapshot.rows,
cursorRow: options.snapshot.cursorRow,
cursorCol: options.snapshot.cursorCol,
...(options.snapshot.scrollbackLines === undefined
? {}
: { scrollbackLineCount: options.snapshot.scrollbackLines.length }),
},
}),
);
await appendArtifactWithRollback({
sessionDir: options.sessionDir,
entry: artifactEntry,
rollbackArtifactPath: snapshotArtifactPath,
});
}

export async function captureSnapshotResult(
Expand Down
59 changes: 50 additions & 9 deletions src/storage/artifactManifest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { basename, resolve } from 'node:path';
import { rm } from 'node:fs/promises';
import { basename, isAbsolute, resolve } from 'node:path';

import { ulid } from 'ulid';
import { z } from 'zod';
Expand Down Expand Up @@ -171,23 +172,63 @@ export async function writeArtifactManifest(
});
}

export async function appendArtifact(
export interface AppendArtifactWithRollbackOptions {
sessionDir: string;
entry: ArtifactEntry;
rollbackArtifactPath?: string;
}

async function appendArtifact(
sessionDir: string,
entry: ArtifactEntry,
rollbackArtifactPath: string | undefined,
): Promise<void> {
const resolvedSessionDir = resolve(sessionDir);
const expectedSessionId = sessionIdFromSessionDir(resolvedSessionDir);
const validatedEntry = validateArtifactEntry(entry, expectedSessionId);

await appendSerializer.run(resolvedSessionDir, async () => {
const manifest = await readArtifactManifest(resolvedSessionDir);
await writeArtifactManifest(resolvedSessionDir, {
...manifest,
artifacts: [...manifest.artifacts, validatedEntry],
});
try {
const validatedEntry = validateArtifactEntry(entry, expectedSessionId);
const manifest = await readArtifactManifest(resolvedSessionDir);
await writeArtifactManifest(resolvedSessionDir, {
...manifest,
artifacts: [...manifest.artifacts, validatedEntry],
});
} catch (error) {
if (rollbackArtifactPath !== undefined) {
// Best-effort: swallow rm errors so the original manifest failure propagates.
await rm(rollbackArtifactPath, { force: true }).catch(() => undefined);
}
throw error;
}
});
}

/**
* Append an artifact entry to the session manifest, removing the artifact file
* at `rollbackArtifactPath` if the append fails. Rollback is best-effort: rm
* errors are swallowed so the original manifest error propagates.
*/
export async function appendArtifactWithRollback(
Comment thread
ThomasK33 marked this conversation as resolved.
options: AppendArtifactWithRollbackOptions,
): Promise<void> {
if (options.rollbackArtifactPath !== undefined) {
invariant(
options.rollbackArtifactPath.length > 0,
'rollbackArtifactPath must be a non-empty string',
);
invariant(
isAbsolute(options.rollbackArtifactPath),
'rollbackArtifactPath must be absolute',
);
}

await appendArtifact(
options.sessionDir,
options.entry,
options.rollbackArtifactPath,
);
}
Comment thread
ThomasK33 marked this conversation as resolved.

export function createArtifactEntry(
entry: Omit<ArtifactEntry, 'id' | 'createdAt'>,
): ArtifactEntry {
Expand Down
Loading
Loading