Stabilize live run transcript excerpts

This commit is contained in:
Dotta committed 2026-04-24 13:39:09 -05:00
1 parent 173aab2045
commit dcbab22437
9 files changed
+66 -2

No files matched your search

+2
View File
@@ -2498,6 +2498,7 @@ export function agentRoutes(db: Db) {
agentId: heartbeatRuns.agentId,
agentName: agentsTable.name,
adapterType: agentsTable.adapterType,
logBytes: heartbeatRuns.logBytes,
livenessState: heartbeatRuns.livenessState,
livenessReason: heartbeatRuns.livenessReason,
continuationAttempt: heartbeatRuns.continuationAttempt,
@@ -2778,6 +2779,7 @@ export function agentRoutes(db: Db) {
agentId: heartbeatRuns.agentId,
agentName: agentsTable.name,
adapterType: agentsTable.adapterType,
logBytes: heartbeatRuns.logBytes,
livenessState: heartbeatRuns.livenessState,
livenessReason: heartbeatRuns.livenessReason,
continuationAttempt: heartbeatRuns.continuationAttempt,
+1
View File
@@ -744,6 +744,7 @@ const heartbeatRunIssueSummaryColumns = {
finishedAt: heartbeatRuns.finishedAt,
createdAt: heartbeatRuns.createdAt,
agentId: heartbeatRuns.agentId,
logBytes: heartbeatRuns.logBytes,
processStartedAt: heartbeatRuns.processStartedAt,
livenessState: heartbeatRuns.livenessState,
livenessReason: heartbeatRuns.livenessReason,
+4
View File
@@ -20,6 +20,8 @@ export interface ActiveRunForIssue {
agentId: string;
agentName: string;
adapterType: string;
logBytes?: number | null;
lastOutputBytes?: number | null;
issueId?: string | null;
livenessState?: RunLivenessFields["livenessState"];
livenessReason?: string | null;
@@ -39,6 +41,8 @@ export interface LiveRunForIssue {
agentId: string;
agentName: string;
adapterType: string;
logBytes?: number | null;
lastOutputBytes?: number | null;
issueId?: string | null;
livenessState?: RunLivenessFields["livenessState"];
livenessReason?: string | null;
+2
View File
@@ -2411,6 +2411,8 @@ export function IssueChatThread({
agentId: activeRun.agentId,
agentName: activeRun.agentName,
adapterType: activeRun.adapterType,
logBytes: activeRun.logBytes,
lastOutputBytes: activeRun.lastOutputBytes,
});
}
return [...deduped.values()].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
+2
View File
@@ -59,6 +59,8 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
agentId: activeRun.agentId,
agentName: activeRun.agentName,
adapterType: activeRun.adapterType,
logBytes: activeRun.logBytes,
lastOutputBytes: activeRun.lastOutputBytes,
issueId,
});
}
@@ -258,6 +258,34 @@ describe("useLiveRunTranscripts", () => {
container.remove();
});
it("starts persisted-log hydration from the newest bytes when the visible window is truncated", async () => {
function Harness() {
useLiveRunTranscripts({
companyId: "company-1",
runs: [{ id: "run-1", status: "running", adapterType: "codex_local", lastOutputBytes: 100_000 }],
enableRealtimeUpdates: false,
logReadLimitBytes: 64_000,
});
return null;
}
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
await act(async () => {
root.render(<Harness />);
await Promise.resolve();
});
expect(logMock).toHaveBeenCalledWith("run-1", 36_000, 64_000);
act(() => {
root.unmount();
});
container.remove();
});
it("rebuilds only the transcript for the run that receives live output", async () => {
function Harness() {
useLiveRunTranscripts({
@@ -16,6 +16,8 @@ export interface RunTranscriptSource {
status: string;
adapterType: string;
hasStoredOutput?: boolean;
logBytes?: number | null;
lastOutputBytes?: number | null;
}
interface UseLiveRunTranscriptsOptions {
@@ -35,6 +37,19 @@ function isTerminalStatus(status: string): boolean {
return status === "failed" || status === "timed_out" || status === "cancelled" || status === "succeeded";
}
function runKnownLogBytes(run: RunTranscriptSource): number | null {
const bytes = run.status === "queued"
? run.logBytes
: run.lastOutputBytes ?? run.logBytes;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes > 0 ? bytes : null;
}
export function resolveInitialLogOffset(run: RunTranscriptSource, limitBytes: number): number {
const knownBytes = runKnownLogBytes(run);
if (knownBytes === null) return 0;
return Math.max(0, knownBytes - Math.max(0, limitBytes));
}
function parsePersistedLogContent(
runId: string,
content: string,
@@ -82,7 +97,11 @@ export function useLiveRunTranscripts({
const runsKey = useMemo(
() =>
runs
.map((run) => `${run.id}:${run.status}:${run.adapterType}:${run.hasStoredOutput === true ? "1" : "0"}`)
.map((run) => {
const logBytes = typeof run.logBytes === "number" ? run.logBytes : "";
const lastOutputBytes = typeof run.lastOutputBytes === "number" ? run.lastOutputBytes : "";
return `${run.id}:${run.status}:${run.adapterType}:${run.hasStoredOutput === true ? "1" : "0"}:${logBytes}:${lastOutputBytes}`;
})
.sort((a, b) => a.localeCompare(b))
.join(","),
[runs],
@@ -197,7 +216,7 @@ export function useLiveRunTranscripts({
if (missingTerminalLogRunIdsRef.current.has(run.id)) {
return;
}
const offset = logOffsetByRunRef.current.get(run.id) ?? 0;
const offset = logOffsetByRunRef.current.get(run.id) ?? resolveInitialLogOffset(run, logReadLimitBytes);
try {
const result = await heartbeatsApi.log(run.id, offset, logReadLimitBytes);
if (cancelled) return;
+1
View File
@@ -43,6 +43,7 @@ export interface IssueChatLinkedRun {
startedAt: Date | string | null;
finishedAt?: Date | string | null;
hasStoredOutput?: boolean;
logBytes?: number | null;
}
export interface IssueChatTranscriptEntry {
+5
View File
@@ -15,6 +15,8 @@ export function resolveIssueChatTranscriptRuns(args: {
id: run.id,
status: run.status,
adapterType: run.adapterType,
logBytes: run.logBytes,
lastOutputBytes: run.lastOutputBytes,
});
}
@@ -23,6 +25,8 @@ export function resolveIssueChatTranscriptRuns(args: {
id: activeRun.id,
status: activeRun.status,
adapterType: activeRun.adapterType,
logBytes: activeRun.logBytes,
lastOutputBytes: activeRun.lastOutputBytes,
});
}
@@ -35,6 +39,7 @@ export function resolveIssueChatTranscriptRuns(args: {
status: run.status,
adapterType,
hasStoredOutput: run.hasStoredOutput,
logBytes: run.logBytes,
});
}