mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
Stabilize live run transcript excerpts
This commit is contained in:
1 parent
173aab2045
commit
dcbab22437
9 files changed
+66
-2
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface IssueChatLinkedRun {
|
||||
startedAt: Date | string | null;
|
||||
finishedAt?: Date | string | null;
|
||||
hasStoredOutput?: boolean;
|
||||
logBytes?: number | null;
|
||||
}
|
||||
|
||||
export interface IssueChatTranscriptEntry {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user