Skip to content

Commit 2d90dcc

Browse files
authored
feat(settings): add rtk compression toggles for local and cloud runs
Two checkboxes in one settings row, both default on: Local (covers local and worktree sessions) and Cloud. Opting out sends POSTHOG_RTK=0 to local sessions via configureProcessEnv, which owns every per-session process env write, and rtk_enabled: false into cloud run creation and resume. Only false is ever sent so the enabled default stays a server-side decision. When Local is enabled but no rtk binary is installed, the row shows a binary-required hint backed by a new agent.rtkStatus query and detectRtkBinary, which reports installation independent of the toggle state a prior session left in the environment.
1 parent 886d163 commit 2d90dcc

19 files changed

Lines changed: 261 additions & 3 deletions

File tree

packages/agent/src/adapters/claude/session/rtk.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
66
import type { Logger } from "../../../utils/logger";
77
import {
88
createRtkRewriteHook,
9+
detectRtkBinary,
910
resolveRtkPrefix,
1011
rewriteBashForRtk,
1112
} from "./rtk";
@@ -133,6 +134,50 @@ describe("resolveRtkPrefix", () => {
133134
});
134135
});
135136

137+
describe("detectRtkBinary", () => {
138+
let dir: string;
139+
let binary: string;
140+
141+
beforeAll(() => {
142+
dir = fs.mkdtempSync(path.join(os.tmpdir(), "rtk-detect-"));
143+
binary = path.join(dir, "rtk");
144+
fs.writeFileSync(binary, "#!/bin/sh\n");
145+
});
146+
147+
afterAll(() => {
148+
fs.rmSync(dir, { recursive: true, force: true });
149+
});
150+
151+
// The per-session toggle must not hide an installed binary from the
152+
// status probe — a prior session leaving POSTHOG_RTK=0 in the process
153+
// env would otherwise flap the settings hint.
154+
test.each([
155+
["unset", undefined],
156+
["0", "0"],
157+
["false", "false"],
158+
["1", "1"],
159+
["true", "true"],
160+
])("finds the PATH binary when POSTHOG_RTK is %s", (_label, value) => {
161+
expect(detectRtkBinary({ POSTHOG_RTK: value, PATH: dir })).toBe(binary);
162+
});
163+
164+
test("reports no binary when rtk is not on PATH", () => {
165+
expect(detectRtkBinary({ PATH: "/nonexistent" })).toBeUndefined();
166+
});
167+
168+
test("honors an explicit path override that exists", () => {
169+
expect(detectRtkBinary({ POSTHOG_RTK: binary, PATH: "/nonexistent" })).toBe(
170+
binary,
171+
);
172+
});
173+
174+
test("reports no binary for a broken explicit path, matching the resolver", () => {
175+
expect(
176+
detectRtkBinary({ POSTHOG_RTK: path.join(dir, "missing"), PATH: dir }),
177+
).toBeUndefined();
178+
});
179+
});
180+
136181
describe("createRtkRewriteHook", () => {
137182
const logger = {
138183
info() {},

packages/agent/src/adapters/claude/session/rtk.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,29 @@ export function resolveRtkPrefix(env: NodeJS.ProcessEnv): string | undefined {
129129
return findOnPath("rtk", env);
130130
}
131131

132+
/**
133+
* Detects the rtk binary a session on this host could use. The on/off flag
134+
* values of POSTHOG_RTK ("0"/"false"/"1"/"true"/unset) all mean auto-detect
135+
* here, so the answer reflects installation, not the per-session toggle a
136+
* previous session may have left in the environment. An explicit binary-path
137+
* override mirrors the resolver: honored when it exists, otherwise no binary.
138+
*/
139+
export function detectRtkBinary(env: NodeJS.ProcessEnv): string | undefined {
140+
const raw = env.POSTHOG_RTK?.trim();
141+
const lowered = raw?.toLowerCase();
142+
const isFlagValue =
143+
!raw || ["0", "false", "1", "true"].includes(lowered ?? "");
144+
if (!isFlagValue && raw) {
145+
try {
146+
if (fs.statSync(raw).isFile()) return raw;
147+
} catch {
148+
// Explicit path doesn't exist — sessions would get no rtk either.
149+
}
150+
return undefined;
151+
}
152+
return findOnPath("rtk", env);
153+
}
154+
132155
export const createRtkRewriteHook =
133156
(rtkPrefix: string, logger: Logger): HookCallback =>
134157
async (input: HookInput, _toolUseID: string | undefined) => {

packages/agent/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ export {
44
isMcpToolReadOnly,
55
type McpToolMetadata,
66
} from "./adapters/claude/mcp/tool-metadata";
7+
export { detectRtkBinary } from "./adapters/claude/session/rtk";
78
export type { PostHogProductId } from "./posthog-products";

packages/api-client/src/posthog-client.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,41 @@ describe("PostHogAPIClient", () => {
277277
expect(body.initial_permission_mode).toBe("plan");
278278
});
279279

280+
it("serializes an rtk opt-out as rtk_enabled false on run creation", async () => {
281+
const fetch = vi.fn().mockResolvedValue({
282+
ok: true,
283+
json: async () => ({ id: "run-123", environment: "cloud" }),
284+
});
285+
const client = new PostHogAPIClient(
286+
"http://localhost:8000",
287+
async () => "token",
288+
async () => "token",
289+
123,
290+
);
291+
292+
(
293+
client as unknown as {
294+
api: { baseUrl: string; fetcher: { fetch: typeof fetch } };
295+
}
296+
).api = {
297+
baseUrl: "http://localhost:8000",
298+
fetcher: { fetch },
299+
};
300+
301+
await client.createTaskRun("task-123", {
302+
environment: "cloud",
303+
mode: "interactive",
304+
rtkEnabled: false,
305+
});
306+
307+
const request = fetch.mock.calls[0][0] as {
308+
overrides: { body: string };
309+
};
310+
expect(JSON.parse(request.overrides.body)).toMatchObject({
311+
rtk_enabled: false,
312+
});
313+
});
314+
280315
it("omits the permission mode from created task runs without an adapter", async () => {
281316
const fetch = vi.fn().mockResolvedValue({
282317
ok: true,

packages/api-client/src/posthog-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,8 @@ interface CloudRunOptions {
497497
customImageId?: string;
498498
prAuthorshipMode?: PrAuthorshipMode;
499499
autoPublish?: boolean;
500+
/** Only false is sent: opts the run out of rtk command-output compression. */
501+
rtkEnabled?: boolean;
500502
runSource?: CloudRunSource;
501503
signalReportId?: string;
502504
initialPermissionMode?: ExecutionMode;
@@ -583,6 +585,9 @@ function buildCloudRunRequestBody(
583585
if (options?.autoPublish) {
584586
body.auto_publish = options.autoPublish;
585587
}
588+
if (options?.rtkEnabled === false) {
589+
body.rtk_enabled = false;
590+
}
586591
if (options?.runSource) {
587592
body.run_source = options.runSource;
588593
}

packages/core/src/sessions/sessionService.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,11 @@ export interface SessionServiceDeps {
299299
setAdapter(taskRunId: string, adapter: Adapter): void;
300300
removeAdapter(taskRunId: string): void;
301301
};
302-
readonly settings: { customInstructions?: string | null };
302+
readonly settings: {
303+
customInstructions?: string | null;
304+
rtkEnabledLocal?: boolean;
305+
rtkEnabledCloud?: boolean;
306+
};
303307
usageLimit: { show: (...args: any[]) => any };
304308
readonly addDirectoryDialog: { open: boolean };
305309
taskViewedApi: { markActivity(taskId: string): void };
@@ -1027,11 +1031,12 @@ export class SessionService {
10271031
this.d.log.warn("Failed to verify workspace", { taskId, err });
10281032
});
10291033

1030-
const { customInstructions } = this.d.settings;
1034+
const { customInstructions, rtkEnabledLocal } = this.d.settings;
10311035
const result = await this.d.trpc.agent.reconnect.mutate({
10321036
taskId,
10331037
taskRunId,
10341038
repoPath,
1039+
rtkEnabled: rtkEnabledLocal,
10351040
apiHost: auth.apiHost,
10361041
projectId: auth.projectId,
10371042
logUrl,
@@ -1362,6 +1367,7 @@ export class SessionService {
13621367
permissionMode: executionMode,
13631368
adapter,
13641369
customInstructions: startCustomInstructions || undefined,
1370+
rtkEnabled: this.d.settings.rtkEnabledLocal,
13651371
effort: effortLevelSchema.safeParse(reasoningLevel).success
13661372
? (reasoningLevel as EffortLevel)
13671373
: undefined,
@@ -3087,6 +3093,7 @@ export class SessionService {
30873093
artifactIds.length > 0 ? artifactIds : undefined,
30883094
prAuthorshipMode,
30893095
autoPublish: previousState.auto_publish === true || undefined,
3096+
rtkEnabled: this.d.settings.rtkEnabledCloud,
30903097
runSource: getCloudRunSource(previousState),
30913098
signalReportId:
30923099
typeof previousState.signal_report_id === "string"

packages/core/src/task-detail/taskCreationApiClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface CreateTaskRunClientOptions {
1616
customImageId?: string;
1717
prAuthorshipMode?: PrAuthorshipMode;
1818
autoPublish?: boolean;
19+
rtkEnabled?: boolean;
1920
runSource?: CloudRunSource;
2021
signalReportId?: string;
2122
initialPermissionMode?: string;

packages/core/src/task-detail/taskCreationSaga.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ describe("TaskCreationSaga", () => {
153153
model: "gpt-5.4",
154154
reasoningLevel: "high",
155155
cloudAutoPublish: true,
156+
cloudRtkEnabled: false,
156157
});
157158

158159
expect(result.success).toBe(true);
@@ -170,6 +171,7 @@ describe("TaskCreationSaga", () => {
170171
sandboxEnvironmentId: undefined,
171172
prAuthorshipMode: "user",
172173
autoPublish: true,
174+
rtkEnabled: false,
173175
runSource: "manual",
174176
signalReportId: undefined,
175177
initialPermissionMode: "auto",

packages/core/src/task-detail/taskCreationSaga.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ export class TaskCreationSaga extends Saga<
403403
customImageId: input.customImageId,
404404
prAuthorshipMode,
405405
autoPublish: input.cloudAutoPublish,
406+
rtkEnabled: input.cloudRtkEnabled,
406407
runSource: input.cloudRunSource ?? "manual",
407408
signalReportId: input.signalReportId,
408409
homeQuickAction: input.homeQuickActionLabel,

packages/core/src/task-detail/taskInput.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export interface PrepareTaskInputOptions {
2929
channelId?: string;
3030
customInstructions?: string;
3131
autoPublishCloudRuns?: boolean;
32+
rtkEnabledCloud?: boolean;
3233
allowNoRepo?: boolean;
3334
}
3435

@@ -64,6 +65,7 @@ export function prepareTaskInput(
6465
cloudRunSource:
6566
options.signalReportId && isCloud ? "signal_report" : undefined,
6667
cloudAutoPublish: isCloud ? options.autoPublishCloudRuns : undefined,
68+
cloudRtkEnabled: isCloud ? options.rtkEnabledCloud : undefined,
6769
signalReportId: options.signalReportId,
6870
additionalDirectories: isCloud ? undefined : options.additionalDirectories,
6971
channelContext: options.channelContext,

0 commit comments

Comments
 (0)