Skip to content

feat(cli): PTY-based Claude backend for interactive billing#82

Draft
Jacky040124 wants to merge 3 commits into
mainfrom
feat/claude-p-backend
Draft

feat(cli): PTY-based Claude backend for interactive billing#82
Jacky040124 wants to merge 3 commits into
mainfrom
feat/claude-p-backend

Conversation

@Jacky040124

Copy link
Copy Markdown
Collaborator

Summary

  • Adds ClaudePTYBackend that spawns Claude Code in an interactive PTY session instead of claude -p, so usage is classified as "interactive" rather than "programmatic" under Anthropic's June 15 billing split
  • Reads all events (text, thinking, tool-use, tool-result) from Claude's session JSONL file, preserving full streaming — no UI degradation
  • Switched via ALOOK_CLAUDE_BACKEND=pty env var; default (pipe) behavior unchanged

New files

  • src/cli/daemon/agent/claude-pty.ts — ClaudePTYBackend using Bun.Terminal API + session JSONL polling
  • src/cli/daemon/agent/ansi-scanner.ts — Responds to Ink's DA1/DA2/DSR/XTVERSION terminal queries to prevent TUI hang

Key design decisions

  • PTY output is NOT parsed for content — only used for lifecycle control (readiness detection + /exit)
  • Business data comes entirely from ~/.claude/projects/<encoded-cwd>/sessions/<id>.jsonl
  • Incremental byte-offset JSONL reading for performance on long sessions
  • Zero new npm dependencies (uses Bun native PTY API)

Test plan

  • Set ALOOK_CLAUDE_BACKEND=pty and run a full task: prompt → thinking → tool-use → tool-result → final reply
  • Verify AgentMessage stream matches pipe mode output
  • Verify timeout handling (PTY startup timeout, execution timeout, JSONL inactivity)
  • Verify no zombie processes after task completion
  • Verify ALOOK_CLAUDE_BACKEND=pipe (default) still works unchanged
  • Verify on Max subscription that usage is not billed to Agent SDK pool

🤖 Generated with Claude Code

After June 15, Anthropic separates `claude -p` usage into a dedicated
Agent SDK billing pool. This adds ClaudePTYBackend that spawns Claude
in an interactive PTY session and reads events from session JSONL,
preserving full streaming while being classified as interactive usage.

Switch via ALOOK_CLAUDE_BACKEND=pty (default: pipe, no behavior change).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Jacky040124 Jacky040124 marked this pull request as draft May 18, 2026 03:27
jackythe King and others added 2 commits May 17, 2026 20:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace `import from "bun"` with runtime `globalThis.Bun` access
so the bundler does not fail when target !== "bun".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@gusye1234 gusye1234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Request Changes

The PTY-based Claude backend concept is sound, but there are several blocking issues:

Blocking:

  1. JSONL path encoding is likely incorrect — The code assumes Claude Code encodes cwd by "replacing / with -", but the actual encoding is more complex (percent-encoding or base64 depending on version). If wrong, the JSONL file will never be found and the backend silently produces zero messages. This needs verification against the actual Claude Code implementation.

  2. Premature /exit on first end_turn — If Claude produces multiple assistant messages (e.g., after tool use), the first end_turn stop reason triggers /exit, killing the session before tool results are processed and the final response is generated. Multi-turn tool-use flows will break.

  3. Hardcoded --permission-mode bypassPermissions — This is extremely permissive. Claude can execute any tool without confirmation. This should at minimum be configurable or inherited from the caller's options, and documented as a security trade-off.

  4. Monolithic execute() method (~280 lines) with deeply nested closures. The JSONL parsing, PTY lifecycle, and message queueing should be separate methods for testability and maintainability.

Non-blocking:

  • readiness detection is fragile — will break if Claude Code changes its prompt character.
  • No input sanitization on prompt before writing to PTY stdin. Control characters or /exit in the prompt could break the session.
  • stripAnsi regex is incomplete for all CSI sequences. Consider a well-tested pattern.
  • lastOutput naming is misleading — it only stores the last text block, not all output.

Recommend addressing items 1-4 before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants