relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
787 lines (744 loc) • 32.2 kB
JavaScript
import { createHash } from "node:crypto";
import fs from "node:fs";
const MAX_ROWS = 4_000;
const MAX_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
function string(value) {
return String(value == null ? "" : value);
}
function hash(value) {
return createHash("sha256").update(string(value)).digest("hex").slice(0, 24);
}
function atMs(value, fallback = null) {
const parsed = Date.parse(string(value));
return Number.isFinite(parsed) ? parsed : fallback;
}
function textContent(content) {
if (typeof content === "string") return content;
if (!Array.isArray(content)) return "";
return content
.filter((block) => block?.type === "text" && typeof block.text === "string")
.map((block) => block.text)
.join("\n");
}
function contentBlocks(row) {
return Array.isArray(row?.message?.content) ? row.message.content : [];
}
function hasToolResult(row) {
return contentBlocks(row).some((block) => block?.type === "tool_result");
}
function isUserAttachmentBlock(block) {
return block && /^(?:image|file|attachment)$/i.test(string(block.type));
}
function hasUserAttachment(row) {
return contentBlocks(row).some(isUserAttachmentBlock);
}
function safeClaudeUserAttachments(row) {
return contentBlocks(row).filter(isUserAttachmentBlock).slice(0, 20).map((block, index) => {
const source = block?.source && typeof block.source === "object" ? block.source : {};
const mimeType = string(
block.mimeType || block.contentType || block.mediaType
|| source.media_type || source.mimeType || source.contentType,
).trim();
const kind = /^image(?:\/|$)/i.test(mimeType) || /^image$/i.test(string(block.type)) ? "image" : "file";
return {
id: `claude-attachment:${hash(`${string(row?.uuid || row?.promptId)}\0${index}`)}`,
kind,
name: string(block.name || block.fileName || block.filename).trim() || (kind === "image" ? "Image" : `Attachment ${index + 1}`),
mimeType: mimeType || null,
size: Number.isFinite(Number(block.size ?? block.bytes)) ? Math.max(0, Number(block.size ?? block.bytes)) : null,
path: null,
url: null,
};
});
}
function internalRelayPrompt(value) {
const body = string(value).trim();
return /^<relay-envelope>/i.test(body)
|| /^<relay-documents>/i.test(body)
|| /<relay-runtime-contract>/i.test(body);
}
function isHumanPrompt(row) {
if (row?.type !== "user" || hasToolResult(row)) return false;
if (row.isCompactSummary || row.isVisibleInTranscriptOnly || row.isMeta) return false;
if (row.sourceToolAssistantUUID || row.toolUseResult) return false;
if (["task-notification", "hook", "system", "compact-summary"].includes(string(row?.origin?.kind))) return false;
const body = textContent(row.message?.content);
if ((!body && !hasUserAttachment(row)) || internalRelayPrompt(body) || /<task-notification>[\s\S]*<\/task-notification>/i.test(body)) return false;
return true;
}
function stableNativeCoordinate(row) {
const event = row?.event || {};
const timestamp = string(row?.timestamp ?? event.timestamp);
const offset = string(
row?.nativeOffset ?? row?.native_offset ?? row?.offset ?? row?.sequence ?? row?.sequence_num
?? event.nativeOffset ?? event.native_offset ?? event.offset ?? event.sequence ?? event.sequence_num,
);
return `${timestamp}|${offset}|${string(row?.event_id ?? event.id)}`;
}
function rowKey(row) {
if (row?.uuid) return `uuid:${row.uuid}`;
if (row?.event_id) return `event:${row.event_id}`;
if (row?.type === "stream_event") {
const event = row.event || {};
const request = row.requestId || row.request_id || event.requestId || event.request_id || "";
const identity = event.message?.id || row.message?.id || "";
const block = event.index ?? event.content_block?.id ?? "";
const coordinate = stableNativeCoordinate(row);
const delta = event.delta?.text ?? event.content_block?.text ?? event.delta?.type ?? "";
return `stream:${request}:${identity}:${block}:${event.type}:${coordinate}:${hash(delta)}`;
}
return `row:${row?.type || "unknown"}:${hash(JSON.stringify(row || {}))}`;
}
function canonicalClaudeDagRows(inputRows) {
const rows = Array.isArray(inputRows) ? inputRows : [];
const latestPrompt = rows.findLast((row) => row?.type === "last-prompt" && string(row.leafUuid));
const byUuid = new Map(rows.filter((row) => string(row?.uuid)).map((row) => [string(row.uuid), row]));
const chain = new Set();
let cursor = string(latestPrompt?.leafUuid);
if (!cursor) cursor = string(rows.findLast((row) => isHumanPrompt(row))?.uuid);
if (!cursor) return rows.filter((row) => row?.type !== "last-prompt");
if (!latestPrompt) {
const children = new Map();
for (const row of rows) {
const parent = string(row?.parentUuid);
const uuid = string(row?.uuid);
if (!parent || !uuid || !["user", "assistant", "system", "attachment"].includes(string(row?.type))) continue;
if (!children.has(parent)) children.set(parent, new Set());
children.get(parent).add(uuid);
}
if (![...children.values()].some((set) => set.size > 1)) {
return rows.filter((row) => row?.type !== "last-prompt");
}
}
while (cursor && !chain.has(cursor)) {
chain.add(cursor);
cursor = string(byUuid.get(cursor)?.parentUuid);
}
// Without a persisted last-prompt leaf, the newest genuine human prompt is
// the authoritative branch anchor. Keep its ancestors and every descendant
// on that branch; a later-written stale sibling cannot replace its answer.
if (!latestPrompt) {
const descendants = new Set([string(rows.findLast((row) => isHumanPrompt(row))?.uuid)]);
let changed = true;
while (changed) {
changed = false;
for (const row of rows) {
const uuid = string(row?.uuid);
if (!uuid || descendants.has(uuid) || !descendants.has(string(row?.parentUuid))) continue;
descendants.add(uuid);
chain.add(uuid);
changed = true;
}
}
}
return rows.filter((row) => {
if (row?.type === "last-prompt") return false;
const uuid = string(row?.uuid);
// Stream-json transport records generally have no transcript UUID. Their
// request/message identity is reconciled against the selected transcript
// branch later; UUID-bearing persisted records must be on the canonical
// ancestor path and can never win merely because they occur later.
const dagRecord = ["user", "assistant", "system", "attachment"].includes(string(row?.type));
return !dagRecord || !uuid || chain.has(uuid);
});
}
function itemId(turn, kind, nativeId, ordinal = 0) {
return `claude:${kind}:${hash(`${turn.id}\0${nativeId || ordinal}`)}`;
}
function nativeToolInput(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function questionText(input) {
const questions = Array.isArray(input?.questions) ? input.questions : [];
const parts = [];
for (const question of questions.slice(0, 3)) {
const prompt = string(question?.question || question?.prompt || question?.header).trim();
const options = Array.isArray(question?.options)
? question.options.slice(0, 8).map((option) => string(option?.label || option).trim()).filter(Boolean)
: [];
if (prompt) parts.push(options.length ? `${prompt} ${options.join(" · ")}` : prompt);
}
return parts.join("\n") || string(input?.question || input?.prompt || "Claude needs your input.");
}
function backgroundUpdate(row) {
const started = [];
const finished = [];
const direct = row?.toolUseResult?.backgroundTaskId
|| row?.tool_use_result?.backgroundTaskId
|| row?.tool_use_result?.background_task_id;
if (direct) started.push(string(direct));
const body = textContent(row?.message?.content);
const start = body.match(/background with ID:\s*([a-zA-Z0-9_-]+)/i);
if (start) started.push(start[1]);
for (const match of body.matchAll(/<task-notification>[\s\S]*?<task-id>([^<]+)<\/task-id>[\s\S]*?<status>(completed|failed|stopped|cancelled)<\/status>[\s\S]*?<\/task-notification>/gi)) {
finished.push({ id: match[1].trim(), status: match[2].toLowerCase() });
}
return { started: [...new Set(started)], finished };
}
function makeTurn(sessionId, seed, ordinal, startedAtMs, hiddenSeed = false) {
return {
id: `claude-turn:${hash(`${sessionId}\0${seed || ordinal}`)}`,
ordinal,
startedAtMs,
completedAtMs: null,
status: "inProgress",
hiddenSeed,
items: [],
itemById: new Map(),
toolByUseId: new Map(),
backgroundById: new Map(),
pendingBackground: new Set(),
pendingRequests: new Set(),
textSlots: [],
currentStreamText: null,
result: null,
sawEndTurn: false,
error: null,
};
}
function addItem(turn, item) {
const previous = turn.itemById.get(item.id);
if (previous) {
Object.assign(previous, item);
return previous;
}
turn.itemById.set(item.id, item);
turn.items.push(item);
return item;
}
function finishPreviousTurn(turn, completedAtMs) {
if (!turn || turn.status !== "inProgress") return;
if (turn.pendingRequests.size || turn.pendingBackground.size) return;
turn.status = turn.error ? "failed" : "completed";
turn.completedAtMs = completedAtMs;
}
function nativeToolItem(turn, row, block, timestamp, activityGroup = null) {
const toolUseId = string(block.id || `${row.uuid}:tool:${turn.items.length}`);
const input = nativeToolInput(block.input);
const requestId = string(row.requestId || row.request_id);
const assistantUuid = string(row.uuid);
const base = {
id: itemId(turn, "tool", toolUseId),
type: "claudeToolUse",
status: "inProgress",
startedAtMs: timestamp,
completedAtMs: null,
nativeProvider: "claude",
nativeType: "tool_use",
tool: string(block.name || "Tool"),
toolUseId,
input,
requestId: requestId || null,
promptId: string(row.promptId) || null,
assistantUuid: assistantUuid || null,
parentUuid: string(row.parentUuid) || null,
activityGroup,
};
return addItem(turn, base);
}
function streamTextItem(turn, timestamp, messageId = "") {
if (turn.currentStreamText) return turn.currentStreamText;
const slot = turn.textSlots.length;
const item = addItem(turn, {
id: itemId(turn, "text", slot),
type: "assistantText",
text: "",
phase: "commentary",
status: "inProgress",
startedAtMs: timestamp,
completedAtMs: null,
nativeProvider: "claude",
nativeType: "text_delta",
messageId: messageId || null,
authoritative: false,
});
turn.textSlots.push(item);
turn.currentStreamText = item;
return item;
}
function authoritativeTextItem(turn, row, block, timestamp) {
const body = string(block.text);
const messageId = string(row.message?.id);
let item = turn.textSlots.find((candidate) => candidate.authoritative
&& candidate.text === body
&& (!messageId || !candidate.messageId || candidate.messageId === messageId));
if (!item) item = turn.textSlots.find((candidate) => !candidate.authoritative
&& (candidate.text === body || body.startsWith(candidate.text) || candidate.text.startsWith(body)));
if (!item) {
const slot = turn.textSlots.length;
item = addItem(turn, {
id: itemId(turn, "text", slot),
type: "assistantText",
startedAtMs: timestamp,
});
turn.textSlots.push(item);
}
Object.assign(item, {
text: body,
phase: row.message?.stop_reason === "end_turn" ? "final_candidate" : "commentary",
status: "completed",
completedAtMs: timestamp,
nativeProvider: "claude",
nativeType: "text",
assistantUuid: string(row.uuid) || null,
parentUuid: string(row.parentUuid) || null,
requestId: string(row.requestId || row.request_id) || null,
messageId: string(row.message?.id) || item.messageId || null,
stopReason: string(row.message?.stop_reason) || null,
authoritative: true,
});
if (turn.currentStreamText === item) turn.currentStreamText = null;
if (row.message?.stop_reason === "end_turn") turn.sawEndTurn = true;
return item;
}
function parseClaudeRows(inputRows, options) {
const sessionId = string(options.sessionId || "claude");
const rows = [];
const seen = new Set();
const allRows = canonicalClaudeDagRows(inputRows);
const cutoff = Math.max(0, allRows.length - MAX_ROWS * 2);
const selectedRows = allRows.slice(cutoff);
if (cutoff > 0) {
const boundary = allRows.slice(0, cutoff).findLast((row) => isHumanPrompt(row));
if (boundary) selectedRows.unshift(boundary);
}
for (const [index, row] of selectedRows.entries()) {
if (!row || typeof row !== "object" || row.isSidechain === true) continue;
const key = rowKey(row);
if (seen.has(key)) continue;
seen.add(key);
rows.push({ row, index });
}
const turns = [];
const toolIndex = new Map();
const backgroundIndex = new Map();
let current = null;
let lastAt = null;
const ensureTurn = (row, timestamp, hiddenSeed = false) => {
if (!current) {
current = makeTurn(sessionId, row?.uuid || row?.promptId || `seed:${turns.length}`, turns.length, timestamp, hiddenSeed);
turns.push(current);
}
return current;
};
for (const { row, index } of rows) {
const timestamp = atMs(row.timestamp, lastAt == null ? index : lastAt + 1);
lastAt = timestamp;
if (isHumanPrompt(row)) {
finishPreviousTurn(current, timestamp);
current = makeTurn(sessionId, row.uuid || row.promptId || `human:${turns.length}`, turns.length, timestamp, false);
turns.push(current);
addItem(current, {
id: itemId(current, "user", row.uuid || row.promptId || index),
type: "human",
text: textContent(row.message?.content),
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
nativeProvider: "claude",
nativeType: "user",
uuid: string(row.uuid) || null,
promptId: string(row.promptId) || null,
parentUuid: string(row.parentUuid) || null,
attachments: safeClaudeUserAttachments(row),
});
continue;
}
if (row.type === "user") {
const turn = ensureTurn(row, timestamp, true);
const update = backgroundUpdate(row);
for (const backgroundId of update.started) {
const tool = contentBlocks(row).find((block) => block?.type === "tool_result")?.tool_use_id;
const indexed = toolIndex.get(string(tool));
if (indexed) {
indexed.turn.pendingBackground.add(backgroundId);
indexed.turn.backgroundById.set(backgroundId, indexed.item);
backgroundIndex.set(backgroundId, indexed);
}
}
for (const finished of update.finished) {
const indexed = backgroundIndex.get(finished.id) || { turn, item: turn.backgroundById.get(finished.id) };
if (!indexed?.item) continue;
indexed.turn.pendingBackground.delete(finished.id);
indexed.item.status = finished.status === "completed" ? "completed" : "failed";
indexed.item.completedAtMs = timestamp;
if (finished.status !== "completed") indexed.turn.error ||= { message: `Background work ${finished.status}.`, code: "CLAUDE_BACKGROUND_FAILED" };
}
for (const block of contentBlocks(row)) {
if (block?.type !== "tool_result") continue;
const indexed = toolIndex.get(string(block.tool_use_id));
if (!indexed) continue;
const isError = Boolean(block.is_error || row.toolDenialKind || row.interruptedByShutdown);
const background = update.started.length > 0;
if (!background) {
indexed.item.status = isError ? "failed" : "completed";
indexed.item.completedAtMs = timestamp;
}
indexed.item.nativeResult = {
nativeType: "tool_result",
toolUseId: string(block.tool_use_id),
sourceToolAssistantUUID: string(row.sourceToolAssistantUUID) || null,
isError,
denialKind: string(row.toolDenialKind) || null,
};
if (indexed.item.tool === "AskUserQuestion") {
indexed.turn.pendingRequests.delete(indexed.item.toolUseId);
indexed.item.resolvedAtMs = timestamp;
}
if (isError) {
const detail = textContent(block.content) || string(block.content) || "Tool use was rejected.";
indexed.turn.error ||= { message: detail, code: row.toolDenialKind || "CLAUDE_TOOL_FAILED" };
addItem(indexed.turn, {
id: itemId(indexed.turn, "tool-error", block.tool_use_id),
type: "error",
text: detail,
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
});
}
}
continue;
}
if (row.type === "assistant") {
const turn = ensureTurn(row, timestamp, true);
// Claude can serialize a terminal provider failure as an assistant row:
// the readable failure lives in a text block while `error` carries the
// machine code (for example `authentication_failed`). It is not
// assistant commentary. Projecting both made Work render the same
// failure as prose and again as a provider error.
const nativeError = string(row.error || row.message?.error);
if (nativeError) {
const detail = contentBlocks(row)
.filter((block) => block?.type === "text")
.map((block) => string(block.text))
.filter(Boolean)
.join("\n\n") || nativeError;
turn.error ||= { message: detail, code: nativeError };
addItem(turn, {
id: itemId(turn, "error", row.uuid || row.message?.id || index),
type: "error",
text: detail,
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
nativeProvider: "claude",
nativeType: "assistant_error",
errorCode: nativeError,
});
continue;
}
const toolBlocks = contentBlocks(row).filter((block) => block?.type === "tool_use");
const activityGroup = toolBlocks.length > 1 ? `claude-tools:${string(row.uuid || row.message?.id || index)}` : null;
for (const block of contentBlocks(row)) {
if (!block || block.type === "thinking" || block.type === "redacted_thinking" || block.type === "signature") continue;
if (block.type === "text" && block.text) authoritativeTextItem(turn, row, block, timestamp);
if (block.type === "tool_use") {
const item = nativeToolItem(turn, row, block, timestamp, activityGroup);
toolIndex.set(item.toolUseId, { turn, item });
if (item.tool === "AskUserQuestion") {
turn.pendingRequests.add(item.toolUseId);
item.question = questionText(item.input);
}
}
}
continue;
}
if (row.type === "stream_event") {
const turn = ensureTurn(row, timestamp, true);
const stream = row.event || {};
if (stream.type === "message_start") turn.currentStreamText = null;
if (stream.type === "content_block_start" && stream.content_block?.type === "text") {
const item = streamTextItem(turn, timestamp, stream.message?.id);
item.text += string(stream.content_block.text);
}
if (stream.type === "content_block_delta" && stream.delta?.type === "text_delta") {
const item = streamTextItem(turn, timestamp, stream.message?.id);
item.text += string(stream.delta.text);
}
if (stream.type === "message_stop") turn.currentStreamText = null;
continue;
}
if (row.type === "system") {
const turn = ensureTurn(row, timestamp, true);
const subtype = string(row.subtype).toLowerCase();
if (subtype === "api_error") {
const attempt = Number(row.retryAttempt || 1);
const maxRetries = Number(row.maxRetries || attempt);
addItem(turn, {
id: itemId(turn, "retry", row.uuid || `${attempt}:${timestamp}`),
type: "retry",
text: `Reconnecting ${attempt}/${Math.max(attempt, maxRetries)}`,
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
nativeProvider: "claude",
nativeType: "api_error",
retryAttempt: attempt,
maxRetries,
});
if (attempt >= maxRetries) turn.error ||= { message: string(row.error || "Claude could not reconnect."), code: "CLAUDE_RETRY_EXHAUSTED" };
} else if (subtype === "compact_boundary" || subtype === "context_compaction") {
addItem(turn, {
id: itemId(turn, "compaction", row.uuid || timestamp),
type: "compaction",
text: "Context compacted",
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
nativeProvider: "claude",
nativeType: subtype,
});
} else if (/permission.*request/.test(subtype)) {
const requestId = string(row.requestId || row.uuid || `permission:${timestamp}`);
turn.pendingRequests.add(requestId);
addItem(turn, {
id: itemId(turn, "permission", requestId),
type: "permission",
requestId,
text: string(row.question || row.prompt || row.reason || "Claude needs permission to continue."),
status: "pending",
startedAtMs: timestamp,
completedAtMs: null,
nativeProvider: "claude",
nativeType: subtype,
});
}
continue;
}
if (row.type === "permission-request" || row.type === "permission_request") {
const turn = ensureTurn(row, timestamp, true);
const requestId = string(row.requestId || row.uuid || `permission:${timestamp}`);
turn.pendingRequests.add(requestId);
addItem(turn, {
id: itemId(turn, "permission", requestId),
type: "permission",
requestId,
text: string(row.question || row.prompt || row.reason || "Claude needs permission to continue."),
status: "pending",
startedAtMs: timestamp,
completedAtMs: null,
nativeProvider: "claude",
nativeType: row.type,
});
continue;
}
if (row.type === "result") {
const turn = ensureTurn(row, timestamp, true);
turn.result = row;
const failed = Boolean(row.is_error) || ["error", "failed"].includes(string(row.subtype).toLowerCase());
if (failed) {
const message = string(row.result || row.error || row.message || `Claude ended ${row.terminal_reason || row.subtype || "with an error"}.`);
turn.error = { message, code: string(row.terminal_reason || row.subtype || "CLAUDE_RESULT_ERROR") };
addItem(turn, {
id: itemId(turn, "result-error", row.uuid || timestamp),
type: "error",
text: message,
status: "completed",
startedAtMs: timestamp,
completedAtMs: timestamp,
});
turn.status = "failed";
turn.completedAtMs = timestamp;
} else if (!turn.pendingBackground.size && !turn.pendingRequests.size && string(row.stop_reason) !== "tool_use") {
const resultText = string(row.result).trim();
const resultItem = resultText
? turn.textSlots.findLast((item) => item.text.trim() === resultText)
: turn.textSlots.findLast((item) => item.text.trim());
if (resultItem && !resultItem.stopReason) {
resultItem.stopReason = "end_turn";
resultItem.authoritative = true;
resultItem.status = "completed";
resultItem.completedAtMs = timestamp;
turn.sawEndTurn = true;
}
turn.status = "completed";
turn.completedAtMs = timestamp;
}
}
}
const latest = turns.at(-1);
if (latest?.status === "inProgress" && !latest.pendingBackground.size && !latest.pendingRequests.size) {
if (!options.ownerAlive && !options.expectedActive && latest.sawEndTurn && !latest.error) {
latest.status = "completed";
latest.completedAtMs = lastAt;
} else if (!options.ownerAlive && options.expectedActive) {
latest.error ||= { message: "Claude Code is no longer connected. Restart this turn to continue.", code: "CLAUDE_OWNER_DETACHED" };
addItem(latest, {
id: itemId(latest, "detached", latest.id),
type: "error",
text: latest.error.message,
status: "completed",
startedAtMs: lastAt,
completedAtMs: lastAt,
});
latest.status = "failed";
latest.completedAtMs = lastAt;
}
}
for (const turn of turns) {
const terminal = turn.status === "completed";
const candidates = turn.textSlots.filter((item) => item.authoritative && item.stopReason === "end_turn" && item.text.trim());
const final = terminal ? candidates.at(-1) : null;
for (const item of turn.textSlots) item.phase = item === final ? "final_answer" : "commentary";
}
return turns;
}
function canonicalEvents(turns) {
const events = [];
for (const turn of turns.slice(-20)) {
events.push({
eventId: `${turn.id}:started`, method: "turn/started", emittedAtMs: turn.startedAtMs,
params: { turnId: turn.id, startedAtMs: turn.startedAtMs },
});
for (const item of turn.items) {
if (item.type === "human") {
events.push({
eventId: `${item.id}:completed`, method: "item/completed", emittedAtMs: item.completedAtMs,
params: { turnId: turn.id, completedAtMs: item.completedAtMs, item: { ...item, type: "userMessage" } },
});
} else if (item.type === "assistantText") {
if (!item.text) continue;
const lifecycle = item.status === "completed" ? "completed" : "started";
events.push({
eventId: `${item.id}:${lifecycle}:${item.phase}:${hash(item.text)}`, method: `item/${lifecycle}`, emittedAtMs: item.completedAtMs ?? item.startedAtMs,
params: {
turnId: turn.id,
[lifecycle === "completed" ? "completedAtMs" : "startedAtMs"]: item.completedAtMs ?? item.startedAtMs,
item: { ...item, type: "agentMessage" },
},
});
} else if (item.type === "claudeToolUse") {
if (item.tool === "AskUserQuestion") {
events.push({
eventId: `${item.id}:request`, method: "claude/requestUserInput", emittedAtMs: item.startedAtMs,
params: { turnId: turn.id, requestId: item.toolUseId, question: item.question, native: item },
});
if (item.resolvedAtMs != null) events.push({
eventId: `${item.id}:resolved`, method: "serverRequest/resolved", emittedAtMs: item.resolvedAtMs,
params: { requestId: item.toolUseId, result: { answered: true } },
});
continue;
}
events.push({
eventId: `${item.id}:started`, method: "item/started", emittedAtMs: item.startedAtMs,
params: { turnId: turn.id, startedAtMs: item.startedAtMs, item: { ...item } },
});
if (["completed", "failed"].includes(item.status)) events.push({
eventId: `${item.id}:completed:${item.status}`, method: "item/completed", emittedAtMs: item.completedAtMs,
params: { turnId: turn.id, completedAtMs: item.completedAtMs, item: { ...item } },
});
} else if (item.type === "retry") {
events.push({
eventId: `${item.id}:completed`, method: "item/completed", emittedAtMs: item.completedAtMs,
params: { turnId: turn.id, completedAtMs: item.completedAtMs, item: { ...item } },
});
} else if (item.type === "compaction") {
events.push({
eventId: `${item.id}:completed`, method: "item/completed", emittedAtMs: item.completedAtMs,
params: { turnId: turn.id, completedAtMs: item.completedAtMs, item: { ...item, type: "contextCompaction" } },
});
} else if (item.type === "permission") {
events.push({
eventId: `${item.id}:request`, method: "claude/permissions/request", emittedAtMs: item.startedAtMs,
params: { turnId: turn.id, requestId: item.requestId, question: item.text, native: item },
});
} else if (item.type === "error") {
events.push({
eventId: `${item.id}:error`, method: "error", emittedAtMs: item.completedAtMs,
params: { turnId: turn.id, error: { message: item.text, code: turn.error?.code, willRetry: false } },
});
}
}
if (turn.status !== "inProgress") {
events.push({
eventId: `${turn.id}:completed:${turn.status}:${turn.completedAtMs}`,
method: "turn/completed", emittedAtMs: turn.completedAtMs,
params: {
turnId: turn.id,
completedAtMs: turn.completedAtMs,
status: turn.status,
...(turn.error ? { error: turn.error } : {}),
},
});
}
}
return events;
}
/**
* Translate Claude Code's own stream-json/transcript event families without
* routing them through the legacy provider-neutral transcript projection.
*/
export function claudeNativeEventsToWorkEvents(rows, options = {}) {
return canonicalEvents(parseClaudeRows(rows, options));
}
/**
* Incremental live reconciler. Persisted history is reduced once. Thereafter
* only the current native turn is reparsed as its exact callback rows arrive;
* completed history and the transcript file are never rescanned per token.
*/
export function createClaudeNativeWorkEventReconciler(initialRows = [], options = {}) {
const source = canonicalClaudeDagRows(Array.isArray(initialRows) ? initialRows : []);
let currentOptions = { ...options };
let baseline = claudeNativeEventsToWorkEvents(source, currentOptions);
const emitted = new Set(baseline.map((event) => event.eventId));
const lastHuman = source.findLastIndex((row) => isHumanPrompt(row));
let currentRows = source.slice(Math.max(0, lastHuman));
return {
snapshotEvents() {
return baseline.slice();
},
push(row, nextOptions = {}) {
if (!row || typeof row !== "object") return [];
currentOptions = { ...currentOptions, ...nextOptions };
const humanBoundary = isHumanPrompt(row);
currentRows.push(row);
if (currentRows.length > MAX_ROWS) currentRows = currentRows.slice(-MAX_ROWS);
const events = claudeNativeEventsToWorkEvents(currentRows, currentOptions);
const fresh = [];
for (const event of events) {
if (emitted.has(event.eventId)) continue;
emitted.add(event.eventId);
fresh.push(event);
}
// Parse the boundary together with the preceding live turn once so its
// end-turn text can transition from commentary to final and the turn can
// settle. Subsequent rows only need the new native turn.
if (humanBoundary) currentRows = [row];
return fresh;
},
};
}
/** Read a bounded chronological tail of the native Claude transcript. */
export function readClaudeNativeTranscriptRows(filePath, {
maxRows = MAX_ROWS,
maxBytes = MAX_TRANSCRIPT_BYTES,
} = {}) {
const target = string(filePath);
if (!target) return [];
let fd;
try {
fd = fs.openSync(target, "r");
const size = fs.fstatSync(fd).size;
const length = Math.min(size, Math.max(1, Number(maxBytes) || MAX_TRANSCRIPT_BYTES));
const start = Math.max(0, size - length);
const buffer = Buffer.alloc(length);
fs.readSync(fd, buffer, 0, length, start);
let source = buffer.toString("utf8");
if (start > 0) source = source.slice(Math.max(0, source.indexOf("\n") + 1));
const parsed = [];
for (const line of source.split("\n")) {
if (!line.trim()) continue;
try { parsed.push(JSON.parse(line)); } catch {}
}
const limit = Math.max(1, Number(maxRows) || MAX_ROWS);
if (parsed.length <= limit) return parsed;
const tail = parsed.slice(-limit);
const boundary = parsed.slice(0, -limit).findLast((row) => isHumanPrompt(row));
return boundary ? [boundary, ...tail] : tail;
} catch {
return [];
} finally {
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
}
}