Compare commits

...

4 Commits

Author SHA1 Message Date
Chris Farhood cc942ca818 0.2.3 2026-04-30 08:03:08 -04:00
Chris Farhood 83a2d25062 fix(execute): assign captured stdout to outer binding so parse sees it
The filesystem-tail rewrite (8bd5042) declared `const stdout` inside the
try block, shadowing the outer `let stdout = ""`. parseClaudeStreamJson
then ran on the empty outer binding, so every run failed with "Failed to
parse Claude JSON output" and resultJson={stdout:""} despite live
log-streaming working fine. Drop the `const` so the assignment lands on
the outer let.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:03:05 -04:00
Chris Farhood c8429cfde1 fix: write logs to /paperclip/instances/default/data/run-logs/ to match server PVC layout
v0.2.1 introduced filesystem-tail log delivery with buildPodLogPath()
returning /paperclip/instances/default/run-logs/... but the paperclip
server creates and tails from /paperclip/instances/default/data/run-logs/
on the shared PVC. The missing /data/ segment meant:

  1. The init container's mkdir -p /paperclip/instances/... ran in a
     directory busybox UID 1000 can't write to — it's the init
     container's ephemeral rootfs, since the PVC is only mounted in
     the main container. Init exited 1, the && short-circuited, and
     the prompt copy never happened. Job failed with "Init container
     'write-prompt' failed with exit code 1".
  2. Even if the mkdir had worked, the main container's tee would
     have written to a path the server doesn't tail.

Fix: drop the misplaced mkdir from both init container variants and
correct buildPodLogPath() to include /data/. The directory already
exists on the PVC because the paperclip server creates it; both
containers run as UID 1000 with fsGroup 1000, so the main container's
tee writes to the pre-existing path with no setup needed.

Bump to 0.2.2.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 22:15:15 -04:00
Chris Farhood 1502039d70 Merge pull request #11 from farhoodlabs/feat/filesystem-log-tail
feat: replace k8s log API streaming with filesystem tailing
2026-04-27 22:26:02 -04:00
5 changed files with 13 additions and 16 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "paperclip-adapter-claude-k8s",
"version": "0.2.0",
"version": "0.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paperclip-adapter-claude-k8s",
"version": "0.2.0",
"version": "0.2.3",
"license": "MIT",
"dependencies": {
"@kubernetes/client-node": "^1.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paperclip-adapter-claude-k8s",
"version": "0.2.0",
"version": "0.2.3",
"description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -1234,7 +1234,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath, jobObserver).then(r => { logStopSignal.stopped = true; return r; }),
]);
const stdout = tailResult.status === "fulfilled" ? tailResult.value : "";
stdout = tailResult.status === "fulfilled" ? tailResult.value : "";
if (completionResult.status === "fulfilled") {
jobTimedOut = completionResult.value.timedOut;
+6 -9
View File
@@ -301,8 +301,7 @@ describe("buildJobManifest", () => {
const init = job.spec?.template?.spec?.initContainers?.[0];
expect(init?.command?.[0]).toBe("sh");
expect(init?.command?.[1]).toBe("-c");
expect(init?.command?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/");
expect(init?.command?.[2]).toContain("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt");
expect(init?.command?.[2]).toBe("printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt");
});
it("write-prompt mounts prompt volume", () => {
@@ -808,28 +807,26 @@ describe("buildJobManifest", () => {
const { job } = buildJobManifest({ ctx, selfPod });
const cmd = job.spec?.template?.spec?.containers[0]?.command?.[2] ?? "";
expect(cmd).toContain("| tee");
expect(cmd).toContain("/paperclip/instances/default/run-logs/");
expect(cmd).toContain("/paperclip/instances/default/data/run-logs/");
});
it("podLogPath is returned from buildJobManifest", () => {
const result = buildJobManifest({ ctx, selfPod });
expect(result.podLogPath).toBe(
"/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
"/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
);
});
it("buildPodLogPath returns correctly formatted path", () => {
expect(buildPodLogPath("co1", "agent-abc", "run-abc12345")).toBe(
"/paperclip/instances/default/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
"/paperclip/instances/default/data/run-logs/co1/agent-abc/run-abc12345.pod.ndjson",
);
});
it("init container creates log directory", () => {
it("init container does not create log directory (server pre-creates it on shared PVC)", () => {
const { job } = buildJobManifest({ ctx, selfPod });
const initCmd = job.spec?.template?.spec?.initContainers?.[0]?.command;
expect(initCmd?.[0]).toBe("sh");
expect(initCmd?.[1]).toBe("-c");
expect(initCmd?.[2]).toContain("mkdir -p /paperclip/instances/default/run-logs/");
expect(initCmd?.[2]).not.toContain("mkdir -p /paperclip");
});
it("sanitizes companyId with / to valid path component for log path", () => {
+3 -3
View File
@@ -23,7 +23,7 @@ function sanitizeForK8sPath(value: string): string {
}
export function buildPodLogPath(companyId: string, agentId: string, runId: string): string {
return `/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
return `/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
}
/** Prompts above this size (bytes) are staged via a Secret instead of an
@@ -504,7 +504,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
name: "write-prompt",
image: "busybox:1.36",
imagePullPolicy: "IfNotPresent",
command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt`],
command: ["sh", "-c", "cp /tmp/prompt-secret/prompt.txt /tmp/prompt/prompt.txt"],
volumeMounts: [
{ name: "prompt", mountPath: "/tmp/prompt" },
{ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true },
@@ -519,7 +519,7 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
name: "write-prompt",
image: "busybox:1.36",
imagePullPolicy: "IfNotPresent",
command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${agent.companyId}/${agent.id} && printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`],
command: ["sh", "-c", `printf '%s' "$PROMPT_CONTENT" > /tmp/prompt/prompt.txt`],
env: [{ name: "PROMPT_CONTENT", value: prompt }],
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
securityContext,