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
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { parseFirstJsonlLine } from "../session-transcript-header";
import { parseTaskArtifactOwnerLocator, type TaskArtifactOwnerLocator } from "../task-artifact-owner-codec";

const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });

/** Resolve the path-free owner locator using transcript replay's header-patch semantics. */
export function taskArtifactOwnerLocatorFromTranscriptBytes(
bytes: Uint8Array,
expectedSessionId: string,
): TaskArtifactOwnerLocator | undefined {
const header = parseFirstJsonlLine(bytes);
if (header?.type !== "session" || header.id !== expectedSessionId)
throw new Error("task_artifact_owner_transcript_header_invalid");
let locator = parseTaskArtifactOwnerLocator(header.taskArtifactOwner);
if (typeof header.version !== "number" || header.version < 4) return locator;
const firstEnd = bytes.indexOf(0x0a);
let start = firstEnd < 0 ? bytes.byteLength : firstEnd + 1;
while (start < bytes.byteLength) {
const newline = bytes.indexOf(0x0a, start);
const end = newline < 0 ? bytes.byteLength : newline;
const line = bytes.subarray(start, end);
let record: Record<string, unknown> | undefined;
try {
if (line.byteLength > 0) {
const parsed: unknown = JSON.parse(transcriptDecoder.decode(line));
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
record = parsed as Record<string, unknown>;
}
} catch {
const text = Buffer.from(line).toString("utf8");
if (text.includes("header_patch") && text.includes("taskArtifactOwner"))
throw new Error("task_artifact_owner_patch_invalid");
}
if (
record?.type === "header_patch" &&
typeof record.patch === "object" &&
record.patch !== null &&
!Array.isArray(record.patch) &&
Object.hasOwn(record.patch, "taskArtifactOwner")
) {
const patch = record.patch as Record<string, unknown>;
if (
!Object.keys(record).every(key => key === "type" || key === "patch") ||
!Object.keys(patch).every(
key =>
key === "cwd" ||
key === "title" ||
key === "titleSource" ||
key === "starred" ||
key === "taskArtifactOwner",
) ||
(patch.cwd !== undefined && typeof patch.cwd !== "string") ||
(patch.title !== undefined && typeof patch.title !== "string") ||
(patch.titleSource !== undefined && patch.titleSource !== "auto" && patch.titleSource !== "user") ||
(patch.starred !== undefined && typeof patch.starred !== "boolean")
)
throw new Error("task_artifact_owner_patch_invalid");
try {
locator = parseTaskArtifactOwnerLocator(patch.taskArtifactOwner);
} catch {
throw new Error("task_artifact_owner_patch_invalid");
}
}
if (newline < 0) break;
start = newline + 1;
}
return locator;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, it } from "bun:test";
import { taskArtifactOwnerLocatorFromTranscriptBytes } from "../src/session/internal/task-artifact-owner-transcript";
import { ownerIdForSession, type TaskArtifactOwnerLocator } from "../src/session/task-artifact-owner-codec";

const sessionId = "transcript-owner-session";
const initial: TaskArtifactOwnerLocator = {
schemaVersion: 1,
ownerId: ownerIdForSession(sessionId),
directoryDev: "12",
directoryIno: "34",
};
const updated: TaskArtifactOwnerLocator = { ...initial, directoryIno: "56" };
const encode = (records: readonly unknown[], trailingNewline = true): Uint8Array =>
new TextEncoder().encode(records.map(record => JSON.stringify(record)).join("\n") + (trailingNewline ? "\n" : ""));
const header = (version = 4) => ({ type: "session", version, id: sessionId, taskArtifactOwner: initial });

describe("task owner transcript locator replay", () => {
it("replays the latest v4 locator patch without requiring a trailing newline", () => {
const bytes = encode(
[
header(),
{ type: "message", role: "assistant", content: "unrelated" },
{ type: "header_patch", patch: { title: "retained title" } },
{ type: "header_patch", patch: { taskArtifactOwner: updated, cwd: "/workspace", starred: true } },
],
false,
);
expect(taskArtifactOwnerLocatorFromTranscriptBytes(bytes, sessionId)).toEqual(updated);
});

it("introduces a locator through a valid v4 patch and leaves v3 replay unchanged", () => {
const patch = { type: "header_patch", patch: { taskArtifactOwner: updated, titleSource: "auto" } };
expect(
taskArtifactOwnerLocatorFromTranscriptBytes(
encode([{ type: "session", version: 4, id: sessionId }, patch]),
sessionId,
),
).toEqual(updated);
expect(taskArtifactOwnerLocatorFromTranscriptBytes(encode([header(3), patch]), sessionId)).toEqual(initial);
});

it("rejects malformed later owner patches rather than falling back to the header locator", () => {
for (const patch of [
{ type: "header_patch", patch: { taskArtifactOwner: updated }, unexpected: true },
{ type: "header_patch", patch: { taskArtifactOwner: updated, unexpected: true } },
{ type: "header_patch", patch: { taskArtifactOwner: updated, titleSource: "manual" } },
{ type: "header_patch", patch: { taskArtifactOwner: updated, starred: "yes" } },
{ type: "header_patch", patch: { taskArtifactOwner: { ...updated, directoryIno: "056" } } },
{ type: "header_patch", patch: { taskArtifactOwner: null } },
]) {
expect(() => taskArtifactOwnerLocatorFromTranscriptBytes(encode([header(), patch]), sessionId)).toThrow(
"task_artifact_owner_patch_invalid",
);
}
});

it("refuses truncated and invalid UTF-8 owner patch records", () => {
const prefix = encode([header()]);
const truncated = Buffer.concat([prefix, Buffer.from('{"type":"header_patch","patch":{"taskArtifactOwner":')]);
expect(() => taskArtifactOwnerLocatorFromTranscriptBytes(truncated, sessionId)).toThrow(
"task_artifact_owner_patch_invalid",
);
const invalidUtf8 = Buffer.concat([
prefix,
Buffer.from('{"type":"header_patch","patch":{"taskArtifactOwner":"'),
Buffer.from([0xff]),
Buffer.from('"}}\n'),
]);
expect(() => taskArtifactOwnerLocatorFromTranscriptBytes(invalidUtf8, sessionId)).toThrow(
"task_artifact_owner_patch_invalid",
);
});

it("rejects a different logical session and an invalid initial locator", () => {
expect(() => taskArtifactOwnerLocatorFromTranscriptBytes(encode([header()]), "another-session")).toThrow(
"task_artifact_owner_transcript_header_invalid",
);
expect(() =>
taskArtifactOwnerLocatorFromTranscriptBytes(
encode([{ ...header(), taskArtifactOwner: { ...initial, ownerId: "bad" } }]),
sessionId,
),
).toThrow("task_artifact_owner_locator_invalid");
});
});
Loading