fix: skills not bundled and resumeLastSession ignored (FAR-56, FAR-57)

Two bugs prevented skill content from reaching K8s Job prompts, and
resumeLastSession: false was silently ignored.

Skills fix (execute.ts, FAR-57):
- Add /paperclip/.claude/skills as additional candidate to
  readPaperclipRuntimeSkillEntries — the relative candidates in
  adapter-utils don't resolve to the PVC-mounted skills home
- Read entry.source/SKILL.md instead of entry.source (which is a
  directory path); fall back to source directly for file-based entries
- Mock readPaperclipRuntimeSkillEntries in execute.test.ts to prevent
  real SKILL.md reads from delaying fake-timer registration

Session fix (job-manifest.ts, FAR-56):
- Gate --session flag on asBoolean(config.resumeLastSession, true)
  so setting resumeLastSession: false actually stops session resumption
- Default true preserves existing behaviour for agents without config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 10:11:47 +00:00
parent 0e94e84e2c
commit 2bd8107f1d
4 changed files with 24 additions and 4 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "paperclip-adapter-opencode-k8s", "name": "paperclip-adapter-opencode-k8s",
"version": "0.1.21", "version": "0.1.22",
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs", "description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
+7
View File
@@ -16,6 +16,13 @@ vi.mock("./job-manifest.js", () => ({
LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024, LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024,
})); }));
// Prevent skill loading from reading real SKILL.md files during tests — the
// real filesystem read delays timer registration and breaks fake-timer tests.
vi.mock("@paperclipai/adapter-utils/server-utils", async (importOriginal) => {
const actual = await importOriginal<typeof import("@paperclipai/adapter-utils/server-utils")>();
return { ...actual, readPaperclipRuntimeSkillEntries: vi.fn().mockResolvedValue([]) };
});
const MOCK_SELF_POD = { const MOCK_SELF_POD = {
namespace: "test-ns", namespace: "test-ns",
image: "test-image:latest", image: "test-image:latest",
+13 -2
View File
@@ -2,6 +2,7 @@ import type { AdapterExecutionContext, AdapterExecutionResult } from "@paperclip
import { inferOpenAiCompatibleBiller, redactHomePathUserSegments } from "@paperclipai/adapter-utils"; import { inferOpenAiCompatibleBiller, redactHomePathUserSegments } from "@paperclipai/adapter-utils";
import { asString, asNumber, asBoolean, parseObject, readPaperclipRuntimeSkillEntries, resolvePaperclipDesiredSkillNames } from "@paperclipai/adapter-utils/server-utils"; import { asString, asNumber, asBoolean, parseObject, readPaperclipRuntimeSkillEntries, resolvePaperclipDesiredSkillNames } from "@paperclipai/adapter-utils/server-utils";
import { readFile } from "node:fs/promises"; import { readFile } from "node:fs/promises";
import path from "node:path";
import { import {
parseOpenCodeJsonl, parseOpenCodeJsonl,
isOpenCodeUnknownSessionError, isOpenCodeUnknownSessionError,
@@ -928,14 +929,24 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
let skillsBundleContent = ""; let skillsBundleContent = "";
try { try {
const moduleDir = import.meta.dirname; const moduleDir = import.meta.dirname;
const availableEntries = await readPaperclipRuntimeSkillEntries(config, moduleDir); // Add the standard Paperclip skills dir as an additional candidate — the relative
// candidates in adapter-utils don't resolve to the PVC-mounted skills home.
const paperclipSkillsHome = "/paperclip/.claude/skills";
const availableEntries = await readPaperclipRuntimeSkillEntries(config, moduleDir, [paperclipSkillsHome]);
const desiredSkillKeys = resolvePaperclipDesiredSkillNames(config, availableEntries); const desiredSkillKeys = resolvePaperclipDesiredSkillNames(config, availableEntries);
const skillTexts: string[] = []; const skillTexts: string[] = [];
for (const key of desiredSkillKeys) { for (const key of desiredSkillKeys) {
const entry = availableEntries.find((e) => e.key === key); const entry = availableEntries.find((e) => e.key === key);
if (entry?.source) { if (entry?.source) {
try { try {
const text = (await readFile(entry.source, "utf-8")).trim(); // entry.source from listPaperclipSkillEntries is a directory; read SKILL.md from it.
// Fall back to reading entry.source directly for file-based paperclipRuntimeSkills entries.
let text: string;
try {
text = (await readFile(path.join(entry.source, "SKILL.md"), "utf-8")).trim();
} catch {
text = (await readFile(entry.source, "utf-8")).trim();
}
if (text) skillTexts.push(text); if (text) skillTexts.push(text);
} catch { } catch {
// skip unreadable skill files — non-fatal // skip unreadable skill files — non-fatal
+3 -1
View File
@@ -286,7 +286,9 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
// Build opencode CLI args // Build opencode CLI args
const opencodeArgs = ["run", "--format", "json"]; const opencodeArgs = ["run", "--format", "json"];
if (runtimeSessionId) opencodeArgs.push("--session", runtimeSessionId); // resumeLastSession defaults to true (preserve existing behaviour); set to false to start fresh.
const resumeLastSession = asBoolean(config.resumeLastSession, true);
if (runtimeSessionId && resumeLastSession) opencodeArgs.push("--session", runtimeSessionId);
if (model) opencodeArgs.push("--model", model); if (model) opencodeArgs.push("--model", model);
if (variant) opencodeArgs.push("--variant", variant); if (variant) opencodeArgs.push("--variant", variant);
if (extraArgs.length > 0) opencodeArgs.push(...extraArgs); if (extraArgs.length > 0) opencodeArgs.push(...extraArgs);