diff --git a/package.json b/package.json index ca105c2..4dc7dfd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@farhoodliquor/paperclip-adapter-claude-k8s", - "version": "0.1.10", + "version": "0.1.11", "description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs", "license": "MIT", "repository": { diff --git a/src/server/execute.ts b/src/server/execute.ts index 5236a03..64e5170 100644 --- a/src/server/execute.ts +++ b/src/server/execute.ts @@ -12,6 +12,7 @@ import type * as k8s from "@kubernetes/client-node"; import { Writable } from "node:stream"; const POLL_INTERVAL_MS = 2000; +const KEEPALIVE_INTERVAL_MS = 15_000; /** * Wait for the Job's pod to reach a terminal or running state. @@ -331,6 +332,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise | null = null; try { // Wait for pod to be ready for log streaming @@ -357,8 +359,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0 ? (timeoutSec + graceSec) * 1000 : 0; + // Keepalive: periodically send a status line via onLog so the + // Paperclip server knows the adapter is still alive even when the + // pod produces no output (e.g. Claude is in a long thinking phase). + let lastLogAt = Date.now(); + keepaliveTimer = setInterval(() => { + const silenceSec = Math.round((Date.now() - lastLogAt) / 1000); + void onLog("stdout", `[paperclip] keepalive — job ${jobName} running (${silenceSec}s since last output)\n`); + }, KEEPALIVE_INTERVAL_MS); + const wrappedOnLog: typeof onLog = async (stream, chunk) => { + lastLogAt = Date.now(); + return onLog(stream, chunk); + }; + const [logResult, completionResult] = await Promise.allSettled([ - streamPodLogs(namespace, podName, onLog, kubeconfigPath), + streamPodLogs(namespace, podName, wrappedOnLog, kubeconfigPath), waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath), ]); @@ -384,6 +399,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise