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>
The K8s log client v1.x closes the follow-stream prematurely due to a
known upstream bug — causing the grace timer to fire 30 s after log
stream exit even when the container is still running. The old behaviour
(`waitForPodTermination` with a hardcoded 120 s timeout) was too short
for agents whose opencode runs take several minutes, leading to premature
failure and issues stuck in `blocked`.
Fix: the grace poller now calls `readNamespacedPod` before resolving the
completion promise. If the pod is still Running/Pending, it resets
`logExitTime` to defer the grace deadline. A `graceCheckPending` guard
prevents concurrent checks. A `graceMaxWaitMs` cap (= completionTimeoutMs
when set, 20 min otherwise) ensures we never wait forever for unlimited
jobs. Version bumped to 0.1.21.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The @kubernetes/client-node v1.x Log.follow stream closes prematurely
(known upstream TODO). Combined with Node.js buffering stdout to pipes,
the live log stream always returns empty. When the 30s grace timer fires
and the stream is empty, the container may still be running.
Add waitForPodTermination() to block in the empty-stdout fallback path
until the container actually exits (up to 120s), then read its complete
output with readNamespacedPodLog. This makes runs complete successfully
instead of looping indefinitely in in_progress.
Bump version to 0.1.20.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
TypeScript CFA does not trace the assignment inside the vi.spyOn
mockImplementation callback, so it narrows capturedHandler to null at
the if-check, making the body unreachable (never). Cast at the call
site breaks the false narrowing without changing runtime behaviour.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Poll PAPERCLIP_API_URL/api/heartbeat-runs/{runId} at keepalive cadence
during log streaming. When status != "running", delete the Job with
propagationPolicy=Background and return errorCode="cancelled" as a
distinct result, matching the claude_k8s reference implementation.
Also includes: reattachOrphanedJobs config field that lets the adapter
reattach to a same-task Job left over from a prior server restart;
task-id and session-id K8s labels on Job manifests for observability.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add agentCreationMutex (Map<agentId, Promise>) that serializes
guard-check + job-create per agent, eliminating the TOCTOU race where
two concurrent execute() calls both pass the list-then-create check.
- Change catch {} on listNamespacedJob errors to return
errorCode: "k8s_concurrency_guard_unreachable" (fail-closed) instead
of silently bypassing the concurrency guard.
- Add ensureSigtermHandler() which tracks active Jobs in activeJobs Map
and deletes all of them (plus prompt Secrets) on SIGTERM before exit.
- Track orphaned-job reattaches in activeJobs for consistent cleanup.
- Update execute.test.ts: change "proceeds on list error" test to assert
k8s_concurrency_guard_unreachable; add mutex serialization test and
SIGTERM handler registration tests.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add `sanitizeLabelValue()` export: strips [^a-z0-9._-], lowercases, truncates to 63 chars, warns on drop
- Apply sanitizer to all paperclip.io/* label values (agent-id, run-id, company-id, extra labels)
- Job name now includes 6-char sha256 hash over raw agent.id+runId for collision resistance
- Trailing hyphens stripped from final job name
- Slugs extended from 8 to 16 chars to match claude_k8s reference
- 32 unit tests covering sanitizeLabelValue, job name format, determinism, and collision avoidance
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add describe block "execute — large-prompt Secret path" with 5 cases:
buildJobManifest called twice (promptSecretName on second call),
Secret created before Job, ownerReference patched after Job creation,
Secret deleted in finally block, Secret cleaned up on Job create failure
- Update vi.mock for job-manifest to export LARGE_PROMPT_THRESHOLD_BYTES
- Add createNamespacedSecret/deleteNamespacedSecret/patchNamespacedSecret
to makeCoreApi for completeness
- Update makeBatchApi to return { metadata: { uid } } so ownerRef tests work
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Split streamPodLogs into streamPodLogsOnce (with bail timer + stopSignal)
and streamPodLogs (reconnect loop, up to MAX_LOG_RECONNECT_ATTEMPTS=50)
- LogLineDedupFilter suppresses replayed JSONL events on reconnect, keyed
by type+sessionID+part.id (OpenCode shape)
- Bail timer (LOG_STREAM_BAIL_TIMEOUT_MS=3s) forces writable.destroy() +
promise resolution when stopSignal fires and logApi.log hangs
- Keepalive: emits '[paperclip] keepalive — job X running (Ns since last output)'
every 15s during silent phases, with 2-consecutive-reading latch to avoid
false-positive terminal detections
- completionGraced uses logExitTime + grace poller so log stream stop signal
is set immediately when job condition resolves
- All 235 tests pass, tsc clean
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- SelfPodInfo gains inheritedEnvValueFrom (V1EnvVar[]) and inheritedEnvFrom (V1EnvFromSource[])
- Container selection now prefers the container named "paperclip", falls back to first
- buildJobManifest appends valueFrom env vars (skipping names already overridden)
and sets envFrom on the opencode container when present
- Tests updated: mock updated, 5 new cases covering secretKeyRef forwarding,
dedup, envFrom passthrough, and empty-envFrom omission
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When both a JSONL error (e.g. "killed") and a pod terminated reason (e.g. "OOMKilled")
are present, join them with "; " so the richer pod classification is never silently
dropped by the parsedError short-circuit.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Replace getPodExitCode with getPodTerminatedInfo to capture exit code
and reason (OOMKilled, Error, etc.) from terminated container state;
pod failure description now surfaces in returned errorMessage
- Add partial-stdout fallback: readPodLogs is triggered when stdout is
non-empty but contains no sessionId (missing session result), not just
when stdout is fully empty
- Detect empty LLM response: when a session ran but produced 0 output
tokens and no messages, return errorCode "llm_api_error"
- Add 13 new unit tests covering all three new paths
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- config-schema: add instructionsFilePath UI field (Core group, text type)
- server/index.ts: set supportsInstructionsBundle=true, instructionsPathKey="instructionsFilePath"
- execute.ts: read instructionsFilePath file + desired skill markdown files from PVC; pass to buildJobManifest as instructionsContent / skillsBundleContent
- job-manifest.ts: accept instructionsContent + skillsBundleContent in JobBuildInput; prepend both to prompt via joinPromptSections; add instructionsChars + skillsBundleChars to promptMetrics
- index.ts: document instructionsFilePath and skills injection in agentConfigurationDoc
- CLAUDE.md: document skill materialization (ephemeral mode) and instructionsFilePath field
- Bump version to 0.1.18
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Implement skill sync handlers that were missing, matching the approach
used in the claude_k8s adapter. The adapter now surfaces available,
configured, and external skills from /paperclip/.claude/skills in K8s
pods, resolving desired skills from config and reporting missing ones.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Upgrade from ^2026.411.0-canary.8 to 2026.415.0-canary.7 to get
ServerAdapterModule capability flag fields (supportsInstructionsBundle,
instructionsPathKey, requiresMaterializedRuntimeSkills).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Declare supportsInstructionsBundle, instructionsPathKey, and
requiresMaterializedRuntimeSkills on ServerAdapterModule. opencode_k8s
does not support instructions bundles (instructions are piped via init
container) and does not require materialized runtime skills (bundled in
container image).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace joinPromptSections, stringifyPaperclipWakePayload, and
renderPaperclipWakePrompt with imports from adapter-utils/server-utils
(the fork's renderPaperclipWakePrompt adds execution stage routing,
resume delta sections, and full comment batch rendering)
- Replace local inferOpenAiCompatibleBiller with import from adapter-utils
- Declare sessionManagement using getAdapterSessionManagement("opencode_local")
with fallback defaults for proper session compaction policy
- Add log redaction via redactHomePathUserSegments in streamPodLogs
- Bump peerDependency to >=0.3.1 and version to 0.1.14
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Lock file was stale at 0.1.11 with an outdated peerDependency constraint;
bring it in line with package.json (0.1.13, >=0.3.0).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The adapter config schema was re-declaring model, promptTemplate, env,
extraArgs, timeoutSec, and graceSec which the Paperclip platform already
surfaces as standard fields, causing duplicate controls in the agent
configuration UI.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The peer dependency on @paperclipai/adapter-utils was pinned to
>=2026.411.0-canary.8 which conflicts with the stable 2026.403.0
version used by other adapters (claude-k8s, hermes-k8s). Since the
canary types (AdapterConfigSchema, getConfigSchema on ServerAdapterModule)
are only needed at compile time, we can safely relax the peer dep to
>=0.3.0 while keeping the canary as a devDependency for our own build.
Bumps version to 0.1.13.
Closes FAR-49
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Rewrites the README to clearly document all prerequisites for running the
adapter on Kubernetes: shared ReadWriteMany PVC at /paperclip, full RBAC
Role/RoleBinding with all required permissions, namespace scoping, security
context, and resource defaults. Uses deployment "paperclip" in namespace
"paperclip" as example nomenclature throughout.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The UI wasn't surfacing config parameters because getConfigSchema wasn't
part of the ServerAdapterModule interface in adapter-utils >=0.3.0. The
canary release (2026.411.0-canary.8) adds ConfigFieldSchema,
AdapterConfigSchema, and getConfigSchema to the type. This removes the
local type augmentation workaround and the unsafe `as ServerAdapterModule`
cast, letting TypeScript properly validate the schema contract.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Avoids 403 E403 "cannot publish over previously published version" error
when pushing non-version-bump commits to master.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- src/server/config-schema.ts: define schema with Core, Kubernetes,
and Operational field groups matching agentConfigurationDoc
- src/server/config-schema.test.ts: 10 tests covering field types,
defaults, options, and group structure
- src/server/index.ts: attach getConfigSchema to ServerAdapterModule
- Revert k8s-client.ts loadFromConfig change (not available in installed
@kubernetes/client-node version)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ui-parser.ts: inline all logic, zero external imports (matches Paperclip
adapter plugin UI parser contract)
- Export parseStdoutLine as named export from index.ts (like claude_k8s
exports printClaudeStreamEvent directly)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New src/cli/format-event.ts handles formatting OpenCode JSONL events:
- step_start: skip in normal mode, show in debug
- text: display as-is
- tool_use: show errors, skip in normal mode
- step_finish: show message + tokens/cost in debug
- error: display error message
Exports cliAdapter.formatStdoutEvent for Paperclip UI to call.
Also fixes ui-parser.ts to re-export from format-event.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Skip step_start/step_finish/tool_use events in UI display, extract
text from type:text events. Provides cleaner real-time output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OpenCode outputs the final response in step_finish.part.message, not
just as type:text events. Added parsing of part.message to ensure
the summary is captured when the agent responds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove INHERITED_ENV_KEYS; read directly from mainContainer.env
- Any Deployment env var is now forwarded automatically to Job pods
- Layer ordering preserved: pod spec → paperclip vars → agent config env
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Runs typecheck and tests on push/PR to master
- Publishes to npm on master push (requires NPM_TOKEN secret)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add vitest with 26 passing tests for parse and job-manifest
- Set models to undefined for free-text model input
- Add fsGroupChangePolicy: "OnRootMismatch" to reduce volume chown delays
- Change job name prefix to agent-opencode- for adapter identification
- Add .npmrc to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename to @farhoodliquor/paperclip-adapter-opencode-k8s
- Add .gitignore
- Emphasize RWX PVC requirement in README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>