Skip to content

Session

A session keeps a single Claude Code process alive across multiple ask() calls, preserving conversation context.

One-shot classifiers don't belong in a session

Sessions keep the full conversation in context -- every turn sees all prior turns. For stateless one-shot work (classifiers, extractors, routers), use claude.askJson() instead. See Stateless Classifier Pattern.

Creating a Session

ts
import { claude } from "@pivanov/claude-wire";

const session = claude.session({
  cwd: "/my/project",
  model: "sonnet",
  maxCostUsd: 1.00,
});

session.ask(prompt, options?)

Send a message and wait for the complete response. Each call reads events until it hits a turn_complete, then stops - leaving the process alive for the next call.

ts
const r1 = await session.ask("Read package.json and summarize it");
console.log(r1.text);

const r2 = await session.ask("Now add a lint script");
console.log(r2.text);

Returns: Promise<TAskResult> with the same shape as claude.ask().

Per-ask options (IAskOptions)

Pass a second argument to override session-level callbacks for a single call -- useful for request-scoped logging in daemon-style consumers:

ts
async function handleRequest(req) {
  return session.ask(req.prompt, {
    onRetry: (attempt, error) => {
      logger.warn(`req ${req.id} retry ${attempt}`, error);
    },
    signal: AbortSignal.timeout(30_000),  // per-request timeout
  });
}
OptionTypeDescription
onRetry(attempt: number, error: unknown) => voidPer-ask retry observer. Fires alongside the session-level onRetry when both are set.
signalAbortSignalPer-ask abort. Aborts this ask only (session stays alive). Composes with the session-level signal -- either firing aborts the ask.

session.askJson(prompt, schema, options?)

Same as claude.askJson() but within a session. The response is parsed and validated against the schema, and the session's conversation context is preserved.

ts
import { z } from "zod";

const session = claude.session({ model: "sonnet" });

const { data } = await session.askJson(
  "What are the top 3 files by size? Return JSON: { files: { name: string, bytes: number }[] }",
  z.object({ files: z.array(z.object({ name: z.string(), bytes: z.number() })) }),
);

console.log(data.files);

await session.close();

Accepts the same schema inputs as claude.askJson() -- Standard Schema objects or raw JSON Schema strings. Throws JsonValidationError on parse/validation failure.

Returns: Promise<IJsonResult<T>>

session.close()

Kill the underlying process and release resources. Always call this when done.

ts
try {
  const r1 = await session.ask("First question");
  const r2 = await session.ask("Follow-up");
} finally {
  await session.close();
}

session.sessionId

The session ID assigned by Claude Code after the first turn. Available after the first ask() call.

ts
const r1 = await session.ask("Hello");
console.log(session.sessionId); // "sess-abc123..."

Cost Accumulation

Cost tracks across all turns in the session. The costUsd in each TAskResult reflects the cumulative total, and tokens accumulate:

ts
const r1 = await session.ask("First question");
console.log(r1.costUsd);  // 0.003

const r2 = await session.ask("Second question");
console.log(r2.costUsd);  // 0.007 (cumulative)

If maxCostUsd is set, a BudgetExceededError is thrown when the budget is exceeded, and the process is killed.

Error Handling

ask() can reject with several error types:

  • ProcessError -- the CLI exited without completing the turn (non-transient exit code, stderr attached when available).
  • AbortError -- an AbortSignal fired during the turn.
  • BudgetExceededError -- maxCostUsd was exceeded. The session is marked closed.
  • KnownError("retry-exhausted") -- auto-respawn budget was used up by consecutive transient failures. The session is marked closed.
  • ClaudeError("Session is closed") -- a prior fatal error already closed the session, or close() was called.

Only KnownError and BudgetExceededError close the session. All other errors leave it usable; the caller may decide whether to retry at a higher level.

Resilience -- Auto-Respawn

Transient failures (SIGKILL/SIGTERM/SIGPIPE, ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENETUNREACH, EHOSTUNREACH, Anthropic overloaded_error, broken pipes, etc. -- see isTransientError) trigger an automatic respawn inside a single ask() call.

  • Budget: up to LIMITS.maxRespawnAttempts (currently 3) respawns per ask().
  • Backoff: 500ms → 1s → 2s between retries.
  • Cost preservation: a cost offset is snapshotted before each respawn so cumulative totals and maxCostUsd enforcement survive the new process.
  • Budget exhaustion: when the cap is reached the session throws KnownError("retry-exhausted") and closes itself.
  • Reset on success: consecutiveCrashes resets to 0 after any turn that completes.

Observing retries

Pass onRetry to see every respawn in progress (does not affect retry behavior):

ts
const session = claude.session({
  model: "sonnet",
  maxCostUsd: 1.00,
  onRetry: (attempt, error) => {
    console.warn(`respawn ${attempt}:`, error);
  },
});

Use onWarning from IClaudeOptions to route library-emitted warnings (user callback threw, invalid tool decision, etc.) through your telemetry instead of the default console.warn.

Turn Limits

After 100 turns, the session pre-emptively kills and respawns the process to prevent context window overflow. This is transparent to the caller.

AbortSignal Support

Sessions respect the signal option from IClaudeOptions:

ts
const session = claude.session({ signal: AbortSignal.timeout(60_000) });

Timeouts

Each read operation has a 5-minute inactivity timeout (TIMEOUTS.defaultAbortMs). If no data is received within this window, a TimeoutError is thrown. The timeout resets on every chunk, so a turn that keeps streaming data can run indefinitely.

Not affiliated with or endorsed by Anthropic. Supported by LogicStar AI.