Compare commits

...

3 Commits

Author SHA1 Message Date
Chris Farhood 357f035418 fix: skip K8s jobs with deletionTimestamp in concurrency guard (FAR-34)
Jobs being deleted via kubectl enter a Terminating state where
deletionTimestamp is set but no Complete/Failed condition is added.
The concurrency guard previously treated these as running, blocking
all subsequent heartbeat runs for the agent until the job fully
disappeared from the K8s API.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 18:36:19 +00:00
Chris Farhood f340ce52ee 0.1.42 2026-04-24 17:56:14 +00:00
Chris Farhood ecc477d0be fix: stream raw stream-json to onLog so Paperclip UI renders structured transcript entries (FAR-32)
The prior approach (commit b607657) converted Claude's stream-json into
flat plain text before calling onLog.  This stripped the structure the
Paperclip UI needs — its adapter ui-parser (src/ui-parser.ts, exported
via the package's ./ui-parser entry) expects raw stream-json lines and
emits structured transcript entries (assistant / thinking / tool_call /
tool_result / init / result) that the UI renders as rich blocks, just
like claude_local.

claude_local passes stdout through unchanged to onLog for the same
reason — the server persists raw lines and the UI parser turns them
into rendered transcript entries.  Mirror that here.

formatClaudeStreamLine stays as an internal helper for future CLI use,
but is no longer applied in the K8s streaming path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 17:56:10 +00:00
3 changed files with 11 additions and 14 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "paperclip-adapter-claude-k8s",
"version": "0.1.41",
"version": "0.1.42",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paperclip-adapter-claude-k8s",
"version": "0.1.41",
"version": "0.1.42",
"license": "MIT",
"dependencies": {
"@kubernetes/client-node": "^1.0.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paperclip-adapter-claude-k8s",
"version": "0.1.41",
"version": "0.1.43",
"description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs",
"license": "MIT",
"repository": {
+8 -11
View File
@@ -19,7 +19,6 @@ import {
import { getSelfPodInfo, getBatchApi, getCoreApi, getLogApi } from "./k8s-client.js";
import { buildJobManifest, sanitizeLabelValue } from "./job-manifest.js";
import { LogLineDedupFilter } from "./log-dedup.js";
import { formatClaudeStreamLine } from "../cli/format-event.js";
import type * as k8s from "@kubernetes/client-node";
import { Writable } from "node:stream";
@@ -354,21 +353,17 @@ export async function streamPodLogsOnce(
const writable = new Writable({
write(chunk: Buffer, _encoding, callback) {
const text = chunk.toString("utf-8");
// Always store raw text — parseClaudeStreamJson needs the original
// stream-json lines to extract session IDs, usage, and result events.
chunks.push(text);
const emitted = dedup ? dedup.filter(text) : text;
if (!emitted) {
callback();
return;
}
// Format each stream-json event into human-readable text before the
// Paperclip server sees it, matching claude_local output style.
// Non-JSON lines (adapter status messages, plain errors) pass through.
const formatted = emitted.split("\n")
.map((line) => formatClaudeStreamLine(line) ?? "")
.join("\n");
void onLog("stdout", formatted).then(() => callback(), callback);
// Forward raw stream-json lines unchanged. The Paperclip UI uses the
// adapter's ui-parser export (src/ui-parser.ts) to render structured
// transcript entries — pre-formatting here would strip that structure
// and produce flat plain text that looks nothing like claude_local.
void onLog("stdout", emitted).then(() => callback(), callback);
},
});
@@ -654,7 +649,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
labelSelector: `paperclip.io/agent-id=${sanitizedAgentId},paperclip.io/adapter-type=claude_k8s`,
});
const running = existing.items.filter(
(j) => !j.status?.conditions?.some((c) => (c.type === "Complete" || c.type === "Failed") && c.status === "True"),
(j) =>
!j.metadata?.deletionTimestamp &&
!j.status?.conditions?.some((c) => (c.type === "Complete" || c.type === "Failed") && c.status === "True"),
);
if (running.length > 0) {
// Separate orphaned jobs (from a previous server-side run) from truly