Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fe4539c
perf(sessions): memoize streaming markdown block split
richardsolomou Jul 1, 2026
1dd0dd9
perf(sessions): halve GeneratingIndicator tick rate
richardsolomou Jul 1, 2026
5c6c2b4
perf(sessions): cap diff tokenization line length
richardsolomou Jul 1, 2026
9fcae96
perf(sessions): batch streamed events into one flush per frame
richardsolomou Jul 1, 2026
bae8dc5
perf(sessions): cache code-block syntax highlighting across mounts
richardsolomou Jul 1, 2026
ef79846
perf(panels): debounce panel-layout persistence
richardsolomou Jul 1, 2026
f33ad6b
perf(sidebar): stop re-rendering the sidebar on every streamed token
richardsolomou Jul 1, 2026
d0ed56d
perf(sessions): pre-freeze events so immer skips its deep-freeze walk
richardsolomou Jul 1, 2026
473fd71
test: adjust tests for batched events and debounced panel persistence
richardsolomou Jul 1, 2026
6770789
perf(sessions): evict backgrounded transcripts, rehydrate on return
richardsolomou Jul 1, 2026
3110913
perf(sessions): finalize the conversation builder in place on turn end
richardsolomou Jul 1, 2026
d0f7d4c
perf(task-detail): lazy-load the code-review surface
richardsolomou Jul 1, 2026
23637c7
perf(sessions): narrow hot single-field session reads to selectors
richardsolomou Jul 1, 2026
e244524
fix(sessions): harden highlight cache and transcript rehydration
richardsolomou Jul 1, 2026
a9d6661
perf(sessions): disable immer autofreeze on the session store
richardsolomou Jul 1, 2026
73ef0c1
feat(sessions): add a tail read for session logs
richardsolomou Jul 1, 2026
204e553
perf(sessions): paint the log tail first when opening a task
richardsolomou Jul 1, 2026
5ea3d76
style(sessions): format tail-first test to biome canonical
richardsolomou Jul 1, 2026
fee33f7
docs(sessions): trim stale and change-commentary comments
richardsolomou Jul 1, 2026
ab0545d
scope immer autofreeze opt-out to session store
charlesvien Jul 2, 2026
6c7f3a6
parallelize tail paint and clean up eviction state
charlesvien Jul 2, 2026
655809b
refcount transcript viewers before eviction
charlesvien Jul 2, 2026
1cdada3
cap diff tokenization in code preview
charlesvien Jul 2, 2026
1996c21
keep whole first line in aligned tail reads
charlesvien Jul 2, 2026
4840771
use tailwind for review fallback layout
charlesvien Jul 2, 2026
4798c0e
cover idle finalize catch-up and fallback
charlesvien Jul 2, 2026
282c62a
cover highlight cache eviction bound
charlesvien Jul 2, 2026
5c8aa00
restore global autofreeze opt-out in session store
charlesvien Jul 2, 2026
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
2 changes: 2 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,8 @@ container.bind(LOGS_SERVICE).toDynamicValue((ctx) => {
},
readLocalLogs: (taskRunId: string) =>
ws.localLogs.read.query({ taskRunId }),
readLocalLogsTail: (taskRunId: string, maxBytes: number) =>
ws.localLogs.readTail.query({ taskRunId, maxBytes }),
writeLocalLogs: (taskRunId: string, content: string) =>
ws.localLogs.write.mutate({ taskRunId, content }),
};
Expand Down
167 changes: 167 additions & 0 deletions packages/core/src/sessions/sessionEventBatching.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import type { AcpMessage, AgentSession } from "@posthog/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SessionService, type SessionServiceDeps } from "./sessionService";

const TASK_ID = "task-1";
const RUN_ID = "run-1";
const FLUSH_MS = 16;

/** A plain streamed agent-message chunk — the common per-token event that just
* gets appended to the transcript. */
function chunk(text: string): AcpMessage {
return {
ts: 1,
message: {
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: RUN_ID,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text },
},
},
},
} as unknown as AcpMessage;
}

function chunkText(event: AcpMessage): string {
const params = (event.message as { params?: unknown }).params as {
update: { content: { text: string } };
};
return params.update.content.text;
}

function createHarness() {
const sessions: Record<string, AgentSession> = {
[RUN_ID]: {
taskRunId: RUN_ID,
taskId: TASK_ID,
events: [],
messageQueue: [],
pendingPermissions: new Map(),
status: "connected",
} as unknown as AgentSession,
};

const appendEvents = vi.fn(
(taskRunId: string, events: AcpMessage[], newLineCount?: number) => {
const session = sessions[taskRunId];
if (!session) return;
session.events = [...session.events, ...events];
if (newLineCount !== undefined) session.processedLineCount = newLineCount;
},
);

const store = {
getSessions: () => sessions,
getSessionByTaskId: (taskId: string) =>
Object.values(sessions).find((s) => s.taskId === taskId),
setSession: (session: AgentSession) => {
sessions[session.taskRunId] = session;
},
updateSession: (taskRunId: string, updates: Partial<AgentSession>) => {
const session = sessions[taskRunId];
if (session) Object.assign(session, updates);
},
appendEvents,
replaceOptimisticWithEvent: vi.fn(),
setPendingPermissions: vi.fn(),
clearMessageQueue: vi.fn(),
clearTailOptimisticItems: vi.fn(),
appendOptimisticItem: vi.fn(),
};

let onEvent: ((payload: unknown) => void) | undefined;
const noopLog = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};

const deps = {
store,
log: noopLog,
notifyPromptComplete: vi.fn(),
notifyPermissionRequest: vi.fn(),
taskViewedApi: { markActivity: vi.fn() },
getPersistedConfigOptions: () => undefined,
setPersistedConfigOptions: vi.fn(),
trpc: {
agent: {
onSessionEvent: {
subscribe: (
_input: unknown,
handlers: { onData: (payload: unknown) => void },
) => {
onEvent = handlers.onData;
return { unsubscribe: vi.fn() };
},
},
onPermissionRequest: {
subscribe: () => ({ unsubscribe: vi.fn() }),
},
onSessionIdleKilled: {
subscribe: () => ({ unsubscribe: vi.fn() }),
},
},
},
} as unknown as SessionServiceDeps;

const service = new SessionService(deps);
// Register the streamed-event subscription (captures onData).
(
service as unknown as { subscribeToChannel(id: string): void }
).subscribeToChannel(RUN_ID);
if (!onEvent)
throw new Error("subscribeToChannel did not subscribe to events");

return {
service,
appendEvents,
emit: (event: AcpMessage) => onEvent?.(event),
events: () => sessions[RUN_ID].events,
};
}

describe("streamed event batching", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});

it("defers a burst and applies it on one flush tick, in order", () => {
const h = createHarness();

h.emit(chunk("a"));
h.emit(chunk("b"));
h.emit(chunk("c"));

// Nothing is applied synchronously — the burst is buffered.
expect(h.appendEvents).not.toHaveBeenCalled();
expect(h.events()).toHaveLength(0);

// A single flush tick drains the whole burst, in arrival order.
vi.advanceTimersByTime(FLUSH_MS);
expect(h.events().map(chunkText)).toEqual(["a", "b", "c"]);
});

it("flushes buffered events synchronously on teardown", () => {
const h = createHarness();

h.emit(chunk("a"));
h.emit(chunk("b"));
expect(h.events()).toHaveLength(0);

// reset() tears down subscriptions and must not drop buffered events.
h.service.reset();
expect(h.events().map(chunkText)).toEqual(["a", "b"]);

// The flush timer was cleared, so advancing does not re-apply anything.
vi.advanceTimersByTime(FLUSH_MS);
expect(h.events()).toHaveLength(2);
});
});
141 changes: 141 additions & 0 deletions packages/core/src/sessions/sessionEventResidency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { AgentSession, SessionStatus } from "@posthog/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SessionService, type SessionServiceDeps } from "./sessionService";
import { sessionStore, sessionStoreSetters } from "./sessionStore";

const RUN = "run-res";
const TASK = "task-res";
const GRACE_MS = 20_000;

const LOG_LINE = JSON.stringify({
type: "notification",
notification: {
method: "session/update",
params: {
sessionId: RUN,
update: {
sessionUpdate: "agent_message_chunk",
content: { type: "text", text: "restored" },
},
},
},
});

function makeService(readLocalLogs = vi.fn().mockResolvedValue("")) {
const deps = {
store: sessionStoreSetters,
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
notifyPromptComplete: vi.fn(),
notifyPermissionRequest: vi.fn(),
taskViewedApi: { markActivity: vi.fn() },
getPersistedConfigOptions: () => undefined,
setPersistedConfigOptions: vi.fn(),
trpc: {
agent: {
onSessionEvent: { subscribe: () => ({ unsubscribe: vi.fn() }) },
onPermissionRequest: { subscribe: () => ({ unsubscribe: vi.fn() }) },
onSessionIdleKilled: { subscribe: () => ({ unsubscribe: vi.fn() }) },
},
logs: { readLocalLogs: { query: readLocalLogs } },
},
} as unknown as SessionServiceDeps;
return new SessionService(deps);
}

function seed(status: SessionStatus, isPromptPending = false) {
sessionStoreSetters.setSession({
taskRunId: RUN,
taskId: TASK,
events: [],
messageQueue: [],
pendingPermissions: new Map(),
status,
isPromptPending,
} as unknown as AgentSession);
sessionStoreSetters.appendEvents(RUN, [{ ts: 1, message: {} } as never]);
}

const events = () => sessionStore.getState().sessions[RUN]?.events ?? [];

describe("session transcript residency", () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => {
vi.useRealTimers();
sessionStoreSetters.removeSession(RUN);
});

it("evicts a disconnected, idle session after the grace window", () => {
const service = makeService();
seed("disconnected");
service.scheduleEventEviction(TASK);

expect(events()).toHaveLength(1);
vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(0);
});

it("never evicts a connected session", () => {
const service = makeService();
seed("connected");
service.scheduleEventEviction(TASK);

vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(1);
});

it("never evicts a session with a prompt in flight", () => {
const service = makeService();
seed("disconnected", true);
service.scheduleEventEviction(TASK);

vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(1);
});

it("ensureEventsLoaded cancels a pending eviction", () => {
const service = makeService();
seed("disconnected");
service.scheduleEventEviction(TASK);

void service.ensureEventsLoaded(TASK); // return to the view before grace
vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(1);
});

it("rehydrates an evicted transcript from disk on return", async () => {
const readLocalLogs = vi.fn().mockResolvedValue(LOG_LINE);
const service = makeService(readLocalLogs);
seed("disconnected");

service.scheduleEventEviction(TASK);
vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(0);

await service.ensureEventsLoaded(TASK);
expect(readLocalLogs).toHaveBeenCalledWith({ taskRunId: RUN });
expect(events()).toHaveLength(1);
});

it("retries rehydration after a failed log read instead of stranding it", async () => {
const readLocalLogs = vi
.fn()
.mockRejectedValueOnce(new Error("transient"))
.mockResolvedValueOnce(LOG_LINE);
const service = makeService(readLocalLogs);
seed("disconnected");

service.scheduleEventEviction(TASK);
vi.advanceTimersByTime(GRACE_MS);
expect(events()).toHaveLength(0);

// First visit: the log read throws — the transcript stays empty but the
// run is re-marked evicted so a later visit can retry.
await service.ensureEventsLoaded(TASK);
expect(events()).toHaveLength(0);

// Second visit: the read succeeds and the transcript is restored.
await service.ensureEventsLoaded(TASK);
expect(events()).toHaveLength(1);
expect(readLocalLogs).toHaveBeenCalledTimes(2);
});
});
8 changes: 8 additions & 0 deletions packages/core/src/sessions/sessionEvents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ describe("convertStoredEntriesToEvents — imported user prompts", () => {
const msg = events[0].message;
expect("method" in msg && msg.method).toBe("session/update");
});

it("freezes converted events on both the promoted and raw branches", () => {
const events = convertStoredEntriesToEvents([
userChunkEntry("promoted", { importedUserPrompt: true }),
userChunkEntry("raw"),
]);
expect(events.every((event) => Object.isFrozen(event))).toBe(true);
});
});

describe("isAbsoluteFolderPath", () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/sessions/sessionEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ import { extractPromptDisplayContent } from "./promptContent";
function storedEntryToAcpMessage(entry: StoredLogEntry): AcpMessage {
const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
const promoted = promoteImportedUserPrompt(entry, ts);
if (promoted) return promoted;
return {
// Freeze at creation: events assigned via setSession bypass the store's
// per-append freeze, so this keeps them read-only once stored.
if (promoted) return Object.freeze(promoted);
return Object.freeze({
type: "acp_message",
ts,
message: (entry.notification ?? {}) as JsonRpcMessage,
};
});
}

/**
Expand Down
Loading
Loading