Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3169f49f23 | |||
| e0b35d230f |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "paperclip-adapter-claude-k8s",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paperclip-adapter-claude-k8s",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperclip-adapter-claude-k8s",
|
||||
"version": "0.1.46",
|
||||
"version": "0.1.47",
|
||||
"description": "Paperclip adapter plugin that runs Claude Code agents as Kubernetes Jobs",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
@@ -150,10 +150,10 @@ describe("buildPartialRunError", () => {
|
||||
expect(buildPartialRunError(null, "", "")).toBe("Claude exited with code -1");
|
||||
});
|
||||
|
||||
it("skips system/init events and returns generic message when only init captured", () => {
|
||||
it("returns init-only message when stdout is init-only with non-zero exit code (FAR-101)", () => {
|
||||
const msg = buildPartialRunError(1, "claude-sonnet-4-6", initLine);
|
||||
expect(msg).toBe(
|
||||
"Claude started but did not produce a result (model: claude-sonnet-4-6) — check API credentials, model support, and adapter config",
|
||||
"Claude exited immediately after init (model: claude-sonnet-4-6) (exit code 1) — the model may be unsupported or the session may have been rejected before producing output",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,15 +170,15 @@ describe("buildPartialRunError", () => {
|
||||
expect(msg).toBe("Claude exited with code 1: Error: no API key configured");
|
||||
});
|
||||
|
||||
it("skips result events (structured protocol artefact — not surfaced verbatim)", () => {
|
||||
it("returns init-only message when stdout has init + result event but no plain content (structured artefact, not surfaced verbatim)", () => {
|
||||
// In production, buildPartialRunError is only called when parseClaudeStreamJson
|
||||
// returns null (no result event). If somehow a result event appears here, the
|
||||
// raw JSON blob must not be shown — the "did not produce a result" message is
|
||||
// cleaner and avoids leaking protocol internals to the UI.
|
||||
// raw JSON blob must not be shown — the init-only message is cleaner and avoids
|
||||
// leaking protocol internals to the UI.
|
||||
const resultLike = JSON.stringify({ type: "result", subtype: "error", result: "rate limit" });
|
||||
const stdout = [initLine, resultLike].join("\n");
|
||||
const msg = buildPartialRunError(2, "claude-sonnet-4-6", stdout);
|
||||
expect(msg).toContain("did not produce a result");
|
||||
expect(msg).toContain("Claude exited immediately after init");
|
||||
expect(msg).toContain("claude-sonnet-4-6");
|
||||
expect(msg).not.toMatch(/\{.*type.*result/);
|
||||
});
|
||||
|
||||
+45
-21
@@ -110,24 +110,12 @@ export function shouldAbortForCancellation(runStatus: string | undefined): boole
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the error message when Claude's stdout contains no result event.
|
||||
* Skips system/init event lines so the UI doesn't display the raw init JSON.
|
||||
* Exported for unit tests.
|
||||
* Returns the first non-JSON/plain-text line in stdout, treating JSON objects
|
||||
* with a "type" field as protocol artefacts and skipping them.
|
||||
* Used by buildPartialRunError to detect init-only runs.
|
||||
*/
|
||||
export function buildPartialRunError(
|
||||
exitCode: number | null,
|
||||
model: string,
|
||||
stdout: string,
|
||||
): string {
|
||||
if (exitCode === 0) return "Failed to parse Claude JSON output";
|
||||
|
||||
// Walk stdout lines and skip every structured streaming event (any JSON
|
||||
// object that carries a non-empty "type" field: system, assistant, user,
|
||||
// rate_limit_event, result, …). All of these are protocol artefacts and
|
||||
// produce confusing raw-JSON blobs when surfaced verbatim as an error
|
||||
// message. Only plain-text lines (non-JSON, or JSON without a type field)
|
||||
// are treated as human-readable content worth including in the error.
|
||||
const firstContentLine = stdout.split(/\r?\n/)
|
||||
function firstContentLine(stdout: string): string {
|
||||
return stdout.split(/\r?\n/)
|
||||
.map((l) => l.trim())
|
||||
.find((l) => {
|
||||
if (!l) return false;
|
||||
@@ -142,19 +130,55 @@ export function buildPartialRunError(
|
||||
}
|
||||
return true;
|
||||
}) ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when stdout contains only init/system/assistant events from the
|
||||
* given model with no human-readable content lines. Used to detect init-only
|
||||
* non-zero-exit runs that should be classified as claude_init_failed rather than
|
||||
* the generic "Claude exited with code N" message.
|
||||
*/
|
||||
function isInitOnlyRun(model: string, stdout: string): boolean {
|
||||
if (!stdout.trim() || !model) return false;
|
||||
const content = firstContentLine(stdout);
|
||||
if (content) return false;
|
||||
// Check that at least the init event for this model was seen
|
||||
const hasModelInit = stdout.includes(`"model":"${model}"`) || stdout.includes(`"model":"${model.replace(/-/g, "_")}"`);
|
||||
return hasModelInit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the error message when Claude's stdout contains no result event.
|
||||
* Skips system/init event lines so the UI doesn't display the raw init JSON.
|
||||
* Exported for unit tests.
|
||||
*/
|
||||
export function buildPartialRunError(
|
||||
exitCode: number | null,
|
||||
model: string,
|
||||
stdout: string,
|
||||
): string {
|
||||
if (exitCode === 0) return "Failed to parse Claude JSON output";
|
||||
|
||||
// If the stream contained only structured events with no plain-text output,
|
||||
// surface the model name so the operator can diagnose missing credentials
|
||||
// or unsupported/misconfigured model.
|
||||
const initOnlyOutput = stdout.trim() !== "" && model !== "" && !firstContentLine;
|
||||
const contentLine = firstContentLine(stdout);
|
||||
if (contentLine) {
|
||||
return `Claude exited with code ${exitCode ?? -1}: ${contentLine}`;
|
||||
}
|
||||
|
||||
if (isInitOnlyRun(model, stdout) && (exitCode ?? 0) !== 0) {
|
||||
const modelHint = model ? ` (model: ${model})` : "";
|
||||
return `Claude exited immediately after init${modelHint} (exit code ${exitCode ?? -1}) — the model may be unsupported or the session may have been rejected before producing output`;
|
||||
}
|
||||
|
||||
const initOnlyOutput = stdout.trim() !== "" && model !== "";
|
||||
if (initOnlyOutput) {
|
||||
const modelHint = model ? ` (model: ${model})` : "";
|
||||
return `Claude started but did not produce a result${modelHint} — check API credentials, model support, and adapter config`;
|
||||
}
|
||||
|
||||
return firstContentLine
|
||||
? `Claude exited with code ${exitCode ?? -1}: ${firstContentLine}`
|
||||
: `Claude exited with code ${exitCode ?? -1}`;
|
||||
return `Claude exited with code ${exitCode ?? -1}`;
|
||||
}
|
||||
|
||||
export type OrphanClassification =
|
||||
|
||||
Reference in New Issue
Block a user