@shardworks/claude-code-apparatus
v0.1.310
Published
Claude Code session provider apparatus — launches claude sessions via the Animator
Downloads
7,825
Readme
@shardworks/claude-code-apparatus
Claude Code session provider apparatus for Nexus. Implements the AnimatorSessionProvider interface for the Claude Code CLI, enabling the Animator to launch and manage AI sessions. Also provides the Session Babysitter — a detached process that hosts sessions independently of the guild lifecycle.
Depends on @shardworks/animator-apparatus (types), @shardworks/tools-apparatus (tool definitions and routing), and @shardworks/nexus-core.
Installation
{
"dependencies": {
"@shardworks/claude-code-apparatus": "workspace:*"
}
}API
Session Provider
The default export is a Plugin whose apparatus provides an AnimatorSessionProvider:
import createClaudeCodeProvider from '@shardworks/claude-code-apparatus';
// In guild.json:
// { "animator": { "sessionProvider": "claude-code" } }The provider implements launch() and cancel():
launch(config)— spawns a detached babysitter process that hosts the session independently of the guild. The babysitter spawnsclaudein autonomous mode, streams transcripts to SQLite, and reports lifecycle events via HTTP. Returns{ chunks, result, processInfo }where:chunkscompletes immediately (empty) — real-time output is available via the transcripts bookresultpolls the sessions book for terminal status (resolves when the babysitter callssession-record)processInfopolls the SessionDoc forcancelHandle(set by the babysitter viasession-running), falls back to{ kind: 'local-pgid', pgid: babysitterPid }
cancel(cancelMetadata)— dispatches oncancelMetadata.kind. Forlocal-pgid, sends SIGTERM to the process group viaprocess.kill(-pgid, 'SIGTERM'), killing both the babysitter and the claude process. Unknown kinds are logged and skipped.
Stream Parsing
Exported utilities for parsing Claude's NDJSON output:
import {
processNdjsonBuffer,
parseStreamJsonMessage,
extractFinalAssistantText,
} from '@shardworks/claude-code-apparatus';processNdjsonBuffer(buffer, handler)— splits NDJSON buffer on newlines, calls handler for each parsed JSON object, returns remaining incomplete buffer.parseStreamJsonMessage(msg, acc)— processes a single NDJSON message, accumulates transcript/metrics, returnsSessionChunk[].extractFinalAssistantText(transcript)— walks transcript backwards to find the last assistant message's text content.
Rate-Limit Detection
The provider runs a single-branch, evidence-driven NDJSON detector to identify rate-limited terminations and attach a structured terminationTag to the session result. The Animator's back-off state machine consumes the tag and transitions its pause-state doc accordingly.
import { detectRateLimitFromNdjson } from '@shardworks/claude-code-apparatus';The active branch matches the rate-limit pattern against the top-level error field (peer of message) on every NDJSON message — the shape claude actually emits on rate-limited assistant termination:
{ "type": "assistant", "message": { "...": "..." }, "error": "rate_limit" }When the regex matches, a tag with source: 'ndjson-result' is emitted. The detector is intentionally narrow: branches are added only when a real provider emission is observed, not pre-emptively.
Retired branches (do not re-introduce without a live observation):
subtype— earlier speculative branch that emitted a tag whenmsg.subtypecontainedrate_limit/rate-limit. Retired because no live provider emission ever fired it.is_error— earlier speculative branch that emitted a tag whenmsg.is_error === trueand the carried error text matched the rate-limit pattern. Retired for the same reason.result-text and stderr/exit-code cascades — retired earlier for false-positive pauses (an assistant's prose summary of a prior rate-limit / a generic non-zero exit code each tripped a false-positive pause once in production).
Everything else surfaces as plain failed. Generic non-zero exit codes do not produce a rate-limit tag; the babysitter no longer samples claude's stderr for pattern matches, only forwards it to the per-session log file.
When a non-zero exit arrives without an NDJSON termination tag, the babysitter captures a terminationDiagnostic: { exitCode, stderrExcerpt? } on the session-record payload so operators can review the signal that fell through — without the Animator widening its pause gate on it.
Session Babysitter
The babysitter is a standalone Node.js script that runs as a detached process, hosting a claude session independently of the guild. It survives guild restarts.
Entry Point
node dist/babysitter.js # reads config from stdinOr import the module for programmatic use:
import { runBabysitter } from '@shardworks/claude-code-apparatus/babysitter';Config (via stdin)
The spawning process writes JSON config to the babysitter's stdin:
interface BabysitterConfig {
sessionId: string; // Pre-generated session ID
guildToolUrl: string; // Guild's Tool HTTP API URL (e.g. "http://127.0.0.1:7471")
dbPath: string; // Path to guild's SQLite database
logDir: string; // Directory for per-session log files
claudeArgs: string[]; // CLI args for claude (--model, --system-prompt-file, etc.)
cwd: string; // Working directory for the claude process
env: Record<string, string>; // Environment variables for the claude process
prompt: string; // Initial prompt piped to claude's stdin
tools: SerializedTool[]; // Tool definitions with JSON Schema params
startedAt: string; // ISO timestamp of session start
provider: string; // Provider name (e.g. "claude-code")
metadata?: Record<string, unknown>; // Optional session metadata
systemPromptTmpDir?: string; // Temp dir for system prompt file (cleaned up in finally)
}
interface SerializedTool {
name: string;
description: string;
params: Record<string, unknown>; // JSON Schema
method: 'GET' | 'POST' | 'DELETE'; // HTTP method for tool server routing
}Lifecycle
- Read config from stdin, parse JSON, validate required fields
- Open SQLite (WAL mode) for real-time transcript streaming
- Start MCP/SSE proxy server — registers tools that forward calls to the guild's Tool HTTP API with retry and exponential backoff
- Prepare session files — temp directory, mcp-config.json pointing to the proxy server
- Spawn claude — pipes prompt to stdin, captures NDJSON stdout
- Report "running" — calls
session-runningtool on guild via HTTP (DLQ fallback). ReportscancelHandle: { kind: 'local-pgid', pgid: process.pid }(babysitter's own PID, which equals its PGID because it was spawned withdetached: true) - Start heartbeat — after running report completes, sends
session-heartbeatevery 30s to refreshlastActivityAton the guild. Heartbeat failures are silently dropped (the 90s staleness threshold tolerates missed heartbeats) - Install SIGTERM handler — sets a
cancelledBySignalflag, stops the heartbeat timer, and propagates SIGTERM to the claude child process. The normal exit path checks this flag and reports statuscancelledinstead of computing from exit code - Stream transcript — parses NDJSON, writes to
books_animator_transcriptstable in SQLite after each message batch - Report result — stops heartbeat, calls
session-recordtool on guild via HTTP (DLQ fallback) - Cleanup — close MCP server, close SQLite, remove temp directory and system prompt temp directory
Session Logs
Each babysitter process writes its own per-session log file, independent of the guild's stderr. This eliminates EPIPE crashes when the guild restarts (which would invalidate an inherited stderr fd).
- Location:
<guildHome>/logs/sessions/<sessionId>.log - Format: Plain text,
[babysitter]-prefixed lines. Claude subprocess stderr is also forwarded into the same file. - Lifetime: Log files persist until manually deleted. They are not cleaned up automatically.
- Ownership: Each log file is owned by its babysitter process. The babysitter opens the file in append mode immediately after reading config from stdin, redirects all
process.stderr.writeoutput to it, and closes the fd on exit.
The first line of each log file is a startup banner:
[babysitter] session=<sessionId> pid=<pid> pgid=<pgid> log=<path> started at <iso>Error Handling
- Tool call proxy errors: retried with exponential backoff (1s initial, 8s max, 60s timeout). If retries exhaust, returns error to claude as MCP tool result — doesn't crash.
- Lifecycle reporting errors: if guild is unreachable, payload is written to
.nexus/dlq/{sessionId}[-running].jsonfor later drain. - Top-level errors: attempts to report
status: 'failed'to guild, falls back to DLQ, then exits non-zero.
Exports
| Entry point | Description |
|---|---|
| . (src/index.ts) | Session provider plugin, stream parsing utilities |
| ./babysitter (src/babysitter.ts) | Babysitter module — runBabysitter(), config parsing, proxy server, transcript DB |
Internal Modules
| Module | Description |
|---|---|
| src/detached.ts | Detached launch — launchDetached(), tool serialization (serializeTools()), polling helpers |
Configuration
Configured in guild.json under the animator key:
{
"animator": {
"sessionProvider": "claude-code"
}
}No additional configuration fields. The model is passed per-session via the Animator.
