Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 2 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
7 changes: 5 additions & 2 deletions packages/core/src/sessions/sessionEventResidency.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { AgentSession, SessionStatus } from "@posthog/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SessionService, type SessionServiceDeps } from "./sessionService";
import {
SESSION_EVENT_EVICT_GRACE_MS as GRACE_MS,
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",
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/sessions/sessionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,10 @@ const SESSION_EVENT_FLUSH_MS = 16;
* A backgrounded session's transcript is freed this long after it stops being
* viewed, and reloaded from disk on return. Only disconnected (idle, no live
* subscription) sessions are eligible, so no streamed event can append to an
* evicted transcript.
* evicted transcript. Generous on purpose: reloading means a full log read and
* re-parse, so flipping between tasks within a few minutes must stay free.
*/
const SESSION_EVENT_EVICT_GRACE_MS = 20_000;
export const SESSION_EVENT_EVICT_GRACE_MS = 180_000;
/**
* On open, paint the last this-many bytes of the log immediately so a big
* transcript shows its latest turns in tens of ms, while the authoritative
Expand Down Expand Up @@ -7027,6 +7028,12 @@ export class SessionService {
if (session && session.eventCount > 0) return;
if (!task.latest_run?.id || !task.latest_run?.log_url) return;

// Logs-only hydration grows the store like a connect does; bound the
// resident-session budget the same way (connectToTask and the cloud
// reconcile branch already do).
this.sessionLastUsedAt.set(task.id, Date.now());
void this.evictIdleSessions(task.id);

this.loadLogsOnly({
taskId: task.id,
taskRunId: task.latest_run.id,
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/sessions/sessionServiceEviction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,33 @@ describe("SessionService idle session eviction", () => {
expect(removeSession).not.toHaveBeenCalledWith("run-cloud-active");
});

it("bounds the budget when reconciling a logs-only task", async () => {
const seeds = seedIdleSessions();
const { service, removeSession } = createHarness(seeds);
const loadLogsOnly = vi
.spyOn(service, "loadLogsOnly")
.mockResolvedValue(undefined);

service.reconcileTaskConnection({
task: {
id: "logs-only",
title: "logs-only",
description: "logs-only",
latest_run: { id: "run-logs-only", log_url: "https://logs" },
} as Task,
session: undefined,
repoPath: null,
isCloud: false,
isOnline: true,
} as ReconcileTaskConnectionParams);

await vi.waitFor(() => {
expect(removeSession).toHaveBeenCalledTimes(1);
});
expect(removeSession).toHaveBeenCalledWith("run-idle-0");
expect(loadLogsOnly).toHaveBeenCalled();
});

it("evicts cloud sessions once their runs are terminal", async () => {
const seeds = Array.from({ length: MAX_CONNECTED_SESSIONS }, (_, i) =>
makeSession(`cloud-${i}`, i + 1, {
Expand Down
Loading