Add RTK integration for token-optimized command output

When enableRtk is set in adapter config, the adapter:
- Adds an init container (curlimages/curl) to download the RTK binary
- Mounts RTK binary in the main container via shared emptyDir volume
- Runs `rtk install claude-code` before invoking Claude to set up hooks
- Disables RTK telemetry (RTK_NO_TELEMETRY=1) for automated environments
- Supports optional rtkVersion config for pinning specific versions

RTK filters CLI command output before it reaches the LLM context,
reducing token consumption by ~80%.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-14 01:27:28 +00:00
parent 77ba40d9bf
commit d074cb2a8c
14 changed files with 238 additions and 16 deletions
+47 -7
View File
@@ -5,6 +5,7 @@ import { buildJobManifest } from "./job-manifest.js";
import { Writable } from "node:stream";
const POLL_INTERVAL_MS = 2000;
const KEEPALIVE_INTERVAL_MS = 15_000;
const LOG_STREAM_RECONNECT_DELAY_MS = 3_000;
/**
* Wait for the Job's pod to reach a terminal or running state.
* Returns the pod name once logs can be streamed, or throws on failure.
@@ -99,10 +100,10 @@ async function waitForPod(namespace, jobName, timeoutMs, onLog, kubeconfigPath)
throw new Error(`Timed out waiting for pod to be scheduled (${Math.round(timeoutMs / 1000)}s)`);
}
/**
* Stream pod logs and accumulate stdout for result parsing.
* Returns accumulated stdout when the stream ends.
* Stream pod logs once via follow. Returns accumulated stdout when the
* stream ends (container exit, API disconnect, or abort signal).
*/
async function streamPodLogs(namespace, podName, onLog, kubeconfigPath) {
async function streamPodLogsOnce(namespace, podName, onLog, kubeconfigPath, sinceSeconds) {
const logApi = getLogApi(kubeconfigPath);
const chunks = [];
const writable = new Writable({
@@ -116,14 +117,47 @@ async function streamPodLogs(namespace, podName, onLog, kubeconfigPath) {
await logApi.log(namespace, podName, "claude", writable, {
follow: true,
pretty: false,
...(sinceSeconds ? { sinceSeconds } : {}),
});
}
catch {
// follow may fail if the container already exited — not fatal,
// we'll try a one-shot read below
// follow may fail if the container already exited or the API
// connection dropped — not fatal, caller decides whether to retry.
}
return chunks.join("");
}
/**
* Stream pod logs with automatic reconnection. Keeps retrying the log
* stream until the stop signal fires (job completed) or the container
* exits normally. This handles silent K8s API connection drops that
* would otherwise cause the UI to stop receiving real output.
*/
async function streamPodLogs(namespace, podName, onLog, kubeconfigPath, stopSignal) {
const allChunks = [];
let attempt = 0;
const streamStartedAt = Math.floor(Date.now() / 1000);
while (!stopSignal?.stopped) {
// On reconnect, ask for logs since the stream originally started to
// avoid missing output during the reconnect gap. Duplicates are
// tolerable — the UI deduplicates log chunks.
const sinceSeconds = attempt > 0
? Math.max(1, Math.floor(Date.now() / 1000) - streamStartedAt + 5)
: undefined;
if (attempt > 0) {
await onLog("stdout", `[paperclip] Log stream disconnected — reconnecting (attempt ${attempt})...\n`);
}
const result = await streamPodLogsOnce(namespace, podName, onLog, kubeconfigPath, sinceSeconds);
if (result)
allChunks.push(result);
attempt++;
// If the job is done or the container exited, no need to reconnect.
if (stopSignal?.stopped)
break;
// Brief pause before reconnecting to avoid tight loops.
await new Promise((resolve) => setTimeout(resolve, LOG_STREAM_RECONNECT_DELAY_MS));
}
return allChunks.join("");
}
/**
* One-shot read of pod logs (no follow). Used as fallback when the
* follow stream missed output because the container exited quickly.
@@ -308,9 +342,15 @@ export async function execute(ctx) {
lastLogAt = Date.now();
return onLog(stream, chunk);
};
// Shared signal: when job completion resolves, tell the log
// streamer to stop reconnecting.
const logStopSignal = { stopped: false };
const [logResult, completionResult] = await Promise.allSettled([
streamPodLogs(namespace, podName, wrappedOnLog, kubeconfigPath),
waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath),
streamPodLogs(namespace, podName, wrappedOnLog, kubeconfigPath, logStopSignal),
waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath).then((r) => {
logStopSignal.stopped = true;
return r;
}),
]);
if (logResult.status === "fulfilled") {
stdout = logResult.value;