5. Cap log stream reconnect attempts at 50 — prevents infinite
reconnect loops during sustained API partitions.
6. Fire keepalive refresh earlier — tick 1 + every 12 ticks (~3min)
instead of every 16 ticks (~4min), providing better safety margin
under the 5-minute reaper window.
7. Catch rejections from onLog inside keepalive — add .catch(() => {})
to prevent unhandledRejection on SSE backpressure.
8. Prevent sanitized-name collisions — extend slugs to 16 chars each,
add a 6-char SHA-256 hash suffix, shorten prefix to `ac-` to stay
well within the 63-char DNS label limit.
10. Fix config-hint parity for nodeSelector and labels — parse both
`key=value` multiline text and JSON objects, matching what the
textarea hint promises.
11. Large-prompt fallback via Secret — prompts >256 KiB are staged as a
K8s Secret and mounted as a volume instead of passed via env var,
protecting against the ~1 MiB PodSpec limit.
13. Track last-seen log timestamp on reconnect — anchor sinceSeconds at
the last received log line instead of stream start, fixing FAR-105
duplicative logs. Belt-and-braces: dedupe assistantTexts at the
parser boundary in parse.ts.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
1. Inherit envFrom and env.valueFrom from self pod — secrets wired via
valueFrom.secretKeyRef or envFrom.secretRef are now forwarded to Job
pods, fixing credentials silently dropped for K8s-idiomatic secret
patterns (e.g. ANTHROPIC_API_KEY via Secret).
2. Distinguish 404 vs transient errors in keepalive — only mark the
keepalive as terminal on 404 (Job deleted). Transient 5xx/connection
errors are logged and retried on the next tick, preventing premature
reaper kills during API instability.
3. Fail closed on concurrency-guard read failure — a failing
listNamespacedJob now returns k8s_concurrency_guard_unreachable
instead of silently proceeding, protecting against zombie Jobs on
shared PVCs.
4. Bound the waitForJobCompletion re-check — pass a 60s timeout instead
of polling forever, preventing indefinite hangs when the K8s API is
degraded.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Busybox echo interprets escape sequences by default (\c, \n, \t, \0NNN, etc.).
If the prompt contains \c (common in file paths or shell references), echo
silently stops output at that point, truncating the prompt file. This can
leave Claude CLI with an empty or garbled stdin, causing it to hang with
zero output — manifesting as endless keepalive messages in the UI.
printf '%s' passes content through verbatim, avoiding the issue.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add missing us.anthropic.claude-sonnet-4-6 entry
- Correct sonnet version from v2:0 to v1:0 (verified against AWS docs)
- All model IDs verified against current Bedrock documentation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The previous fix (df856e6) made the keepalive timer call onSpawn every
~4 minutes to refresh the run's updatedAt in the DB, so the stale-run
reaper wouldn't kill live runs in multi-instance deployments. That was
correct for live jobs, but it was unconditional — if execute() stalled
after the pod terminated (slow K8s API call, hung log stream drain, or
a Job whose Complete condition lags pod termination), the keepalive
kept the run marked "alive" indefinitely even though the pod was gone.
That manifests as the opposite of the original bug: the UI shows jobs
as running when they have actually finished.
Two changes:
1. Verify the Job is still alive before the keepalive refreshes
updatedAt. If the Job has reached a terminal Complete/Failed
condition (or has been deleted / the API read fails), stop
refreshing. If execute() truly ends up stuck past that point, the
reaper will catch the run within the normal 5-minute staleness
window instead of never.
2. Clear the keepalive interval immediately once Promise.allSettled
resolves, rather than only in the finally block. Post-completion
work (exit-code fetch, log fallback read, job cleanup) must not be
able to emit another onSpawn refresh that keeps the run "alive".
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Two root causes behind the "plugin losing sync" issue:
1. After a server restart, the in-memory activeRunExecutions set is lost.
The K8s Job keeps running but the reaper marks the server-side run as
failed after 5 min (stale updatedAt). Next heartbeat fires a new run,
the adapter's concurrency guard blocks it because the old Job is still
alive, and this loops indefinitely.
Fix: the concurrency guard now compares each running Job's
paperclip.io/run-id label against the current runId. Jobs from a
previous (dead) run are cleaned up automatically so the new run
can proceed.
2. onLog (keepalive) does NOT update the run's updatedAt in the DB —
it only writes to the log store and publishes SSE events. In
multi-instance deployments, a reaper on instance B can mark a run
being executed on instance A as stale after 5 min of no DB updates.
Fix: the keepalive timer now calls onSpawn every ~4 min (16 ticks)
to refresh updatedAt, staying within the 5-min reaper threshold.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add claude-opus-4-7 and Bedrock Opus 4.7 to model lists
- Set models export to undefined (like opencode_k8s) to allow free-text model entry
- Move direct models list into server/models.ts
- Bump version to 0.1.17
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The k8s adapter never called ctx.onSpawn(), so the Paperclip server
had no processStartedAt timestamp for the run. The stale-run reaper
(reapOrphanedRuns) would then mark live k8s runs as failed/orphaned,
causing the UI to show no active runs and triggering duplicate run
attempts that hit the concurrency guard.
Uses pid=-1 as a sentinel since there is no local process — the
server's isProcessAlive check safely returns false for pid <= 0.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When waitForJobCompletion threw and the job was still not terminal, we
were returning an error but still deleting the job in the finally block.
This left the UI holding an error while the job (still alive) would be
cleaned up by Kubernetes, causing the next heartbeat to find nothing and
think it was safe to retry — spawning a concurrent pod.
Now we set skipCleanup=true when returning the mismatch error, so the
job is retained and the heartbeat can still find and wait on it.
Also removes a duplicate empty-stdout fallback block.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When waitForJobCompletion threw a transient error (API disconnect, etc.),
the code fell through with jobTimedOut=true and returned a result even
though the job was still running. This caused the UI to think the run
was complete while the job kept running, resulting in concurrency errors.
Now when completion throws, we re-check the job's actual state. If still
not terminal, we return a k8s_job_state_mismatch error so the UI knows
the run is not done.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds supportsInstructionsBundle, instructionsPathKey, and
requiresMaterializedRuntimeSkills flags so the UI renders the
bundle editor for claude_k8s agents. Bumps adapter-utils peer
dep to the canary that includes the capability type fields.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Bring the K8s adapter up to parity with the fork's ServerAdapterModule
contract by adding sessionManagement, listSkills/syncSkills, listModels
with Bedrock detection, and promptBundleKey support in the session codec.
- Declare sessionManagement with nativeContextManagement: "confirmed"
so Paperclip skips threshold-based session compaction (Claude manages
its own context)
- Add ephemeral skill management (listSkills/syncSkills) mirroring
claude_local — reports skill state without runtime persistence since
skills are injected via prompt bundle into ephemeral Job pods
- Add listModels() with Bedrock environment detection, returning
region-qualified model IDs when CLAUDE_CODE_USE_BEDROCK or
ANTHROPIC_BEDROCK_BASE_URL are set
- Extend session codec to round-trip promptBundleKey field
- Remove the `as ServerAdapterModule` cast — the return type now
satisfies the full interface
Co-Authored-By: Paperclip <noreply@paperclip.ing>
When enableRtk is set in adapter config, the adapter:
- Adds an init container (curlimages/curl) to download the RTK binary
- Mounts RTK binary in the main container via shared emptyDir volume
- Runs `rtk install claude-code` before invoking Claude to set up hooks
- Disables RTK telemetry (RTK_NO_TELEMETRY=1) for automated environments
- Supports optional rtkVersion config for pinning specific versions
RTK filters CLI command output before it reaches the LLM context,
reducing token consumption by ~80%.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The adapter opened a single follow-stream to the K8s API for pod logs.
If that TCP connection silently dropped (API server hiccup, network
timeout, load-balancer idle cut), streamPodLogs returned early and no
more real Claude output reached the UI — only keepalive pings. The
pod kept running and producing logs (visible via kubectl), but the
adapter never reconnected.
Splits streamPodLogs into streamPodLogsOnce (single follow attempt) and
a reconnecting wrapper that retries with sinceSeconds until a shared
stop signal fires when waitForJobCompletion resolves. On reconnect,
requests logs from the original stream start time (+5s overlap) so no
output is lost; the UI deduplicates chunks.
Bumps version to 0.1.12.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The adapter had no mechanism to signal liveness while a K8s Job was
running. When Claude entered long thinking phases with no log output,
the Paperclip UI could lose sync and consider the run stuck even though
the pod was still actively working.
Adds a 15-second interval keepalive that sends status messages via
onLog during execution. The keepalive tracks time since last real log
output and reports it, keeping the connection alive. The timer is
cleaned up in the finally block to prevent leaks on any exit path.
Bumps version to 0.1.11.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Keep adapter-specific maxTurnsPerRun (default 1000) in the config
schema since the platform UI does not provide it for external adapters.
Platform-provided fields (model, effort, instructionsFilePath,
timeoutSec, graceSec) remain excluded to avoid duplication.
Add config-schema.test.ts with assertions that platform-provided
fields are absent and adapter-specific fields have correct defaults.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Fields like model, reasoning effort, instructions file path, max turns,
timeout, and grace period are either surfaced elsewhere in the platform
UI or are internal operational settings that shouldn't be user-facing
in the adapter config panel. These values remain functional when set
via the API/backend — only the UI exposure is removed.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds detailed prerequisites section covering ReadWriteMany PVC setup,
complete RBAC Role/RoleBinding/ServiceAccount manifests, and API key
secret configuration. Includes full configuration reference tables and
a How It Works section explaining the adapter lifecycle.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- CI publish job failed because it tried to re-publish existing versions
(npm returns 404 for scoped packages on duplicate version). Added a
version-exists check before npm publish to skip gracefully.
- Also fixed the auth env var from NPM_TOKEN to NODE_AUTH_TOKEN which
is what actions/setup-node's registry-url option expects.
- Added missing core and operational fields to getConfigSchema() so the
Paperclip UI surfaces model, effort, maxTurnsPerRun, skipPermissions,
instructionsFilePath, timeoutSec, and graceSec alongside existing K8s
infrastructure fields.
- Bumped version to 0.1.10.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add src/cli/ with format-event.ts (printClaudeStreamEvent) exported from
CLIAdapterModule
- Fix env var forwarding: read from pod spec container env dynamically instead
of static allowlist; agent config env overrides pod values
- Rename K8s Job prefix from agent- to agent-claude-
- Add fsGroupChangePolicy: "OnRootMismatch" to skip PVC chown on subsequent runs
- Add comprehensive test coverage (159 tests across 5 test files)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Paperclip AdapterConfigSchema type expects a flat fields array, not
nested sections. Also maps description -> hint per the schema type.
Defines types locally since @paperclipai/adapter-utils@0.3.1 on npm
does not yet export AdapterConfigSchema/ConfigFieldSchema (those exist
in the monorepo but aren't released to npm yet).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs typecheck and tests on push/PR to master, then publishes to npm
on successful master pushes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds AdapterConfigSchema with three sections (Kubernetes, Resource Limits,
Scheduling) exposing: namespace, image, imagePullPolicy, kubeconfig,
resources.{requests,limits}.{cpu,memory}, nodeSelector, tolerations,
labels, ttlSecondsAfterFinished, retainJobs.
Paperclip's server fetches GET /api/adapters/:type/config-schema and
caches the result, automatically assigning ConfigFields to external
adapters. The adapter now wires getConfigSchema into createServerAdapter().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>