diff --git a/src/server/config-schema.ts b/src/server/config-schema.ts index 0c2f111..daf0b03 100644 --- a/src/server/config-schema.ts +++ b/src/server/config-schema.ts @@ -125,6 +125,14 @@ export function getConfigSchema(): AdapterConfigSchema { hint: "Skip cleanup on completion for debugging", group: "Kubernetes", }, + { + key: "reattachOrphanedJobs", + label: "Reattach Orphaned Jobs", + type: "toggle", + default: false, + hint: "When a running Job for the same task is found after a server restart, reattach (stream logs and await completion) instead of blocking", + group: "Kubernetes", + }, // Operational fields (timeoutSec and graceSec are provided by the platform) ], diff --git a/src/server/job-manifest.test.ts b/src/server/job-manifest.test.ts index 79c986d..cdc2460 100644 --- a/src/server/job-manifest.test.ts +++ b/src/server/job-manifest.test.ts @@ -70,6 +70,48 @@ describe("buildJobManifest", () => { expect(result.job.metadata?.labels?.["paperclip.io/run-id"]).toBe("run123456"); }); + it("sets paperclip.io/task-id label when context.taskId is present", () => { + const ctx = { + ...mockCtx, + context: { ...mockCtx.context, taskId: "7e0829c0-cbf7-4652-9554-4d777ce84bff" }, + }; + const result = buildJobManifest({ ctx, selfPod: mockSelfPod }); + + expect(result.job.metadata?.labels?.["paperclip.io/task-id"]).toBe("7e0829c0-cbf7-4652-9554-4d777ce84bff"); + }); + + it("falls back to context.issueId for paperclip.io/task-id when taskId is null", () => { + const ctx = { + ...mockCtx, + context: { ...mockCtx.context, taskId: null, issueId: "issue-uuid-xyz" }, + }; + const result = buildJobManifest({ ctx, selfPod: mockSelfPod }); + + expect(result.job.metadata?.labels?.["paperclip.io/task-id"]).toBe("issue-uuid-xyz"); + }); + + it("omits paperclip.io/task-id label when context.taskId and issueId are both null", () => { + const result = buildJobManifest({ ctx: mockCtx, selfPod: mockSelfPod }); + + expect(result.job.metadata?.labels?.["paperclip.io/task-id"]).toBeUndefined(); + }); + + it("sets paperclip.io/session-id label when runtime.sessionParams.sessionId is present", () => { + const ctx = { + ...mockCtx, + runtime: { ...mockCtx.runtime, sessionParams: { sessionId: "ses_abc123" } }, + }; + const result = buildJobManifest({ ctx, selfPod: mockSelfPod }); + + expect(result.job.metadata?.labels?.["paperclip.io/session-id"]).toBe("ses_abc123"); + }); + + it("omits paperclip.io/session-id label when no session exists", () => { + const result = buildJobManifest({ ctx: mockCtx, selfPod: mockSelfPod }); + + expect(result.job.metadata?.labels?.["paperclip.io/session-id"]).toBeUndefined(); + }); + it("creates init container for prompt", () => { const result = buildJobManifest({ ctx: mockCtx, selfPod: mockSelfPod }); diff --git a/src/server/job-manifest.ts b/src/server/job-manifest.ts index b43a2fe..bfea102 100644 --- a/src/server/job-manifest.ts +++ b/src/server/job-manifest.ts @@ -320,6 +320,9 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult { "paperclip.io/company-id": sanitizeLabelValue(agent.companyId, warnLabel), "paperclip.io/adapter-type": "opencode_k8s", }; + const taskId = asString(context.taskId ?? context.issueId, "").trim(); + if (taskId) labels["paperclip.io/task-id"] = sanitizeLabelValue(taskId, warnLabel); + if (runtimeSessionId) labels["paperclip.io/session-id"] = sanitizeLabelValue(runtimeSessionId, warnLabel); for (const [key, value] of Object.entries(extraLabels)) { if (typeof value === "string") labels[key] = sanitizeLabelValue(value, warnLabel); }