Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/oxide-code/src/client/anthropic/betas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub(super) fn compute_betas(
.split('-')
.any(|tok| tok.eq_ignore_ascii_case("haiku"));

// Order mirrors `docs/research/anthropic-api.md` → Per-model beta
// Order mirrors `docs/research/api/anthropic-api.md` → Per-model beta
// sets: identity / auth → universal agentic → capability-gated.
let mut out = Vec::with_capacity(8);

Expand Down
2 changes: 1 addition & 1 deletion crates/oxide-code/src/session/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! before this drain runs; isolated writes (a text-only turn, the
//! AI title append, the final summary) flush immediately because
//! the drain returns `Empty` after the first cmd. No interval timer
//! — see `docs/research/session-persistence.md`.
//! — see `docs/research/design/session-persistence.md`.

use std::sync::Arc;

Expand Down
26 changes: 18 additions & 8 deletions docs/research/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
# Research Notes

Architecture research and API reference notes for oxide-code development.
Architecture research and API reference notes for oxide-code development. Split by direction:

- [`api/`](api/) — outward-facing: how the Anthropic API works and what oxide-code has to send to it.
- [`design/`](design/) — inward-facing: surveys of reference projects + design choices for oxide-code.

## API references

| Document | Description |
| --------------------------------------------- | ---------------------------------------------------------------------- |
| [Anthropic API](anthropic-api.md) | Anthropic API auth: OAuth flow, required headers, system prompt prefix |
| [Extended Thinking](extended-thinking.md) | Extended thinking: content block types, signatures, round-tripping |
| [System Prompt](system-prompt.md) | System prompt architecture: section assembly, CLAUDE.md, caching |
| [Session Persistence](session-persistence.md) | Session persistence: JSONL format, storage layout, listing strategy |
| [Terminal UI](tui.md) | TUI research: reference projects, flickering prevention, crate stack |
| [Tool Output Truncation](tool-truncation.md) | Tool dispatcher truncation: per-tool vs central, caps and spillover |
| [File Change Tracking](file-tracking.md) | Read-before-Edit gate, staleness detection, persistence across resume |
| [Anthropic API](api/anthropic-api.md) | Anthropic API auth: OAuth flow, required headers, system prompt prefix |
| [Extended Thinking](api/extended-thinking.md) | Extended thinking: content block types, signatures, round-tripping |
| [System Prompt](api/system-prompt.md) | System prompt architecture: section assembly, CLAUDE.md, caching |

## Design surveys

| Document | Description |
| ---------------------------------------------------- | --------------------------------------------------------------------- |
| [Session Persistence](design/session-persistence.md) | Session persistence: JSONL format, storage layout, listing strategy |
| [Terminal UI](design/tui.md) | TUI research: reference projects, flickering prevention, crate stack |
| [Tool Output Truncation](design/tool-truncation.md) | Tool dispatcher truncation: per-tool vs central, caps and spillover |
| [File Change Tracking](design/file-tracking.md) | Read-before-Edit gate, staleness detection, persistence across resume |
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Invalidation order (from the Anthropic caching docs) is `tools → system → me

### `thinking.display`

See [Extended Thinking § Display modes (Opus 4.7+)](./extended-thinking.md#display-modes-opus-47). Opus 4.7 silently flipped the default to `"omitted"`; `show_thinking=true` in oxide-code opts back into `"summarized"`.
See [Extended Thinking § Display modes (Opus 4.7+)](extended-thinking.md#display-modes-opus-47). Opus 4.7 silently flipped the default to `"omitted"`; `show_thinking=true` in oxide-code opts back into `"summarized"`.

## Third-Party Tool Restrictions

Expand All @@ -292,7 +292,7 @@ Re-distribution gateways front the upstream API and reject anything that doesn't
4. **Beta header set and order.** Match Claude Code's emit order on agentic 4.6+ requests: `claude-code-20250219, [oauth-2025-04-20,] interleaved-thinking-2025-05-14, context-management-2025-06-27, prompt-caching-scope-2026-01-05, effort-2025-11-24`. `prompt-caching-scope-2026-01-05` ships unconditionally — the matching `cache_control.scope: "global"` body field stays gated on `is_first_party_base_url` because 3P gateways reject the scope downstream of tool definitions, but the header alone keeps the wire fingerprint intact.
5. **`metadata.user_id` shape.** Field order: `device_id`, `account_uuid`, `session_id`. Empty `account_uuid` is still required as a present field, not a missing key. Use a typed struct rather than `serde_json::json!` so the wire order matches the source declaration — `json!` ships fields alphabetically without the `preserve_order` feature, which trips the verifier. `device_id` is minted as 64 lowercase hex chars (32 random bytes) and persisted at `$XDG_DATA_HOME/ox/user-id`; verifiers check shape, not whether the value round-trips to Claude Code's `~/.claude.json#userID`.

System-prompt block content is a separate axis the gateway validates — see [system-prompt § Third-Party Gateway Validation](./system-prompt.md#third-party-gateway-validation) for the prompt-content side of the same check.
System-prompt block content is a separate axis the gateway validates — see [System Prompt § Third-Party Gateway Validation](system-prompt.md#third-party-gateway-validation) for the prompt-content side of the same check.

A residual rejection band remains under sampled stricter checks. Suspected vectors: TLS / HTTP/2 fingerprint (reqwest + rustls vs Node + native TLS) and request body field order — Claude Code 2.1.121 emits `model, messages, system, ...` while oxide-code's `CreateMessageRequest` puts `system` before `messages` so `inject_cch`'s `replacen` is unambiguous when tool results contain literal `cch=00000` text. Reordering would require either Bun-style byte-level placeholder discovery or accepting that user message content will never carry the exact placeholder string.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Tool schemas are sent via the API `tools` parameter, **not** in the system promp

The API supports prompt caching via `cache_control` on `TextBlockParam` blocks. Cache scopes:

- `global` — static instructions identical across all sessions. **First-party only**; 3P gateways reject a `scope: "global"` block downstream of tool definitions (they render before `system` and taint the cache prefix). See [Prompt Caching Scope](./anthropic-api.md#prompt-caching-scope) for the full invariance rule.
- `global` — static instructions identical across all sessions. **First-party only**; 3P gateways reject a `scope: "global"` block downstream of tool definitions (they render before `system` and taint the cache prefix). See [Anthropic API § Prompt Caching Scope](anthropic-api.md#prompt-caching-scope) for the full invariance rule.
- _(absent)_ — default (org-scoped) ephemeral cache. Universally accepted.
- `null` (no `cache_control`) — dynamic content, not cached.

Expand Down Expand Up @@ -211,11 +211,11 @@ The URL includes a `?beta=true` query parameter.

Third-party gateways validate the system-block layout in addition to wire-shape signals. Empirically, prompt-content checks are content-similarity-based and treat the static prefix as load-bearing:

- The identity prefix block (`"You are Claude Code..."` and friends, see [`CLI_SYSPROMPT_PREFIXES`](./anthropic-api.md#2-system-prompt-prefix-as-a-separate-block)) must occupy its own block. Concatenating it into the prompt body fails the same way 1P fails non-Haiku OAuth.
- The identity prefix block (`"You are Claude Code..."` and friends, see [Anthropic API § System prompt prefix](anthropic-api.md#2-system-prompt-prefix-as-a-separate-block)) must occupy its own block. Concatenating it into the prompt body fails the same way 1P fails non-Haiku OAuth.
- Static section text is accepted when it closely matches the known Claude Code prompt content shipped with this version. Heavily customized static prompts can trip the verifier even when every header is correct.
- Dynamic sections (after the `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` marker) are accepted alongside valid static content. The boundary itself is consumed by `splitSysPromptPrefix()` and never reaches the wire.

For the wire-shape side of the check (Stainless headers, billing attestation, beta header set, `metadata.user_id` shape, `User-Agent`), see [anthropic-api § Third-Party Gateway Validation](./anthropic-api.md#third-party-gateway-validation).
For the wire-shape side of the check (Stainless headers, billing attestation, beta header set, `metadata.user_id` shape, `User-Agent`), see [Anthropic API § Third-Party Gateway Validation](anthropic-api.md#third-party-gateway-validation).

## Sources

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type FileState = {

**Concurrency:** single-threaded JavaScript with the LRU cache as implicit serialization point. No `Mutex` / `Semaphore`.

**Sources:** `claude-code/src/tools/FileEditTool/FileEditTool.ts:275-311` (Edit-time gate + mtime / content fallback), `claude-code/src/utils/fileStateCache.ts` (LRU cache shape, eviction), `claude-code/src/utils/queryHelpers.ts:346-501` (resume rehydration via message history — the approach we explicitly do **not** take).

### OpenAI Codex (Rust)

No explicit cache. File validation is deferred to the `apply_patch` verification layer (`codex-rs/core/src/tools/handlers/apply_patch.rs:340-464`).
Expand All @@ -54,9 +56,11 @@ There is no "must Read first" rule. Code can be edited that the model has never

**Concurrency:** per-session `Mutex<SessionState>`; no per-file lock.

### opencode (TypeScript / Effect)
**Sources:** `codex-rs/core/src/tools/handlers/apply_patch.rs:340-464` (deferred apply-time validation).

### opencode (TypeScript)

Per-file `Semaphore` lock (`packages/opencode/src/tool/edit.ts:36-46`) prevents concurrent edits to the same path; no content / timestamp cache.
Per-file `Semaphore` lock (`opencode/packages/opencode/src/tool/edit.ts:36-46`) prevents concurrent edits to the same path; no content / timestamp cache.

```typescript
const locks = new Map<string, Semaphore.Semaphore>() // module-global
Expand All @@ -75,6 +79,8 @@ A stale `oldString` triggers a "string not found" error — the model must adjus

**Concurrency:** per-file `Semaphore` from `effect` library (`withPermits(1)`). Different files edit in parallel; same file serializes.

**Sources:** `opencode/packages/opencode/src/tool/edit.ts:36-46` (per-file `Semaphore`), `opencode/packages/opencode/src/tool/edit.ts:115-118` (disk re-read on every Edit).

## Comparison

| Repo | Tracker | Read-before-Edit | Stale check | Resume | Concurrency |
Expand All @@ -90,7 +96,7 @@ Read / Write / Edit tools are unit structs (`ReadTool`, `WriteTool`, `EditTool`)

The only existing signal for external modification is Edit's `"old_string not found in {path}"` error — a false negative if the user happens to leave the matched substring intact while changing surrounding lines. The model would then edit the file based on stale context.

The session machinery already parses past tool_use / tool_result pairs via `crates/oxide-code/src/session/history.rs`. That gives us the building blocks for claude-code-style message-history rehydration if we want it. But the JSONL schema's `Entry::Unknown` `#[serde(other)]` catch-all (see `crates/oxide-code/src/session/entry.rs` and `docs/research/session-persistence.md` § Forward Compatibility) also makes it cheap to add a new entry type — explicit persistence is more direct than parsing message bodies and avoids coupling to sanitization shape.
The session machinery already parses past tool_use / tool_result pairs via `crates/oxide-code/src/session/history.rs`. That gives us the building blocks for claude-code-style message-history rehydration if we want it. But the JSONL schema's `Entry::Unknown` `#[serde(other)]` catch-all (see `crates/oxide-code/src/session/entry.rs` and [Session Persistence § Forward Compatibility](session-persistence.md#forward-compatibility)) also makes it cheap to add a new entry type — explicit persistence is more direct than parsing message bodies and avoids coupling to sanitization shape.

## Design Decisions for oxide-code

Expand All @@ -107,19 +113,8 @@ The roadmap item is: skip re-reads when content hasn't changed, and guard agains

## Sources

### oxide-code

- `crates/oxide-code/src/session/entry.rs` — JSONL forward-compat (`Entry::Unknown` `#[serde(other)]` catch-all).
- `crates/oxide-code/src/session/history.rs` — past tool_use / tool_result pairing (alternative resume strategy: rehydrate from message history rather than persisted snapshots).
- `crates/oxide-code/src/tool/edit.rs:185-266` — read-before-replace flow (line 212 reads pre-edit content; line 256 writes post-edit).
- `crates/oxide-code/src/tool/read.rs:103-195` — file open + bytes-in-memory point where post-Read hashing would slot in.
- `crates/oxide-code/src/tool/write.rs:78-101` — `is_new` detection pattern (today's closest analog to "we touched this file").

### Reference projects

- `claude-code/src/tools/FileEditTool/FileEditTool.ts:275-311` — Edit-time gate + mtime/content fallback.
- `claude-code/src/utils/fileStateCache.ts` — LRU cache shape, eviction.
- `claude-code/src/utils/queryHelpers.ts:346-501` — resume rehydration via message history (the approach we explicitly do **not** take).
- `codex-rs/core/src/tools/handlers/apply_patch.rs:340-464` — codex's deferred apply-time validation.
- `opencode/packages/opencode/src/tool/edit.ts:36-46` — per-file `Semaphore`.
- `opencode/packages/opencode/src/tool/edit.ts:115-118` — disk re-read on every Edit.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Session Persistence

Research findings for oxide-code session persistence, based on analysis of reference projects ([Claude Code](https://github.com/hakula139/claude-code) (v2.1.87), [OpenAI Codex](https://github.com/openai/codex), [learn-claude-code](https://github.com/the-pocket/learn-claude-code)), POSIX append-only semantics, and Anthropic Messages API ordering requirements.
Research findings for oxide-code session persistence, based on analysis of reference projects ([Claude Code](https://github.com/hakula139/claude-code) (v2.1.87), [OpenAI Codex](https://github.com/openai/codex)), POSIX append-only semantics, and Anthropic Messages API ordering requirements.

## Reference Implementations

Expand All @@ -22,12 +22,6 @@ Research findings for oxide-code session persistence, based on analysis of refer
- Bounded `mpsc` channel + `tokio::spawn`-ed `RolloutWriterTask`. Cmds: `AddItems`, `Persist`, `Flush`, `Shutdown` — each barrier carries an `oneshot::Sender<io::Result<()>>` ack. Terminal task failure is read post-mortem from a `Mutex<Option<Arc<IoError>>>` slot on the recorder.
- Receive-and-drain inside the writer loop — `recv().await` for the first cmd, then `try_recv()` non-blocking to coalesce queued cmds into a single batch flush. No interval timer (Rust + mpsc subsumes claude-code's `FLUSH_INTERVAL_MS = 100` JS-event-loop workaround).

### learn-claude-code (Python)

- Project-local `.transcripts/transcript_{TIMESTAMP}.jsonl`.
- 3-layer context compression (micro, auto, manual).
- Auto-archive full transcript before summarization.

## Storage Format

Every session is a single `.jsonl` file — one JSON object per line. Append-only (crash-safe with write-then-flush), streamable (incremental read / write), and universal (used by all reference implementations).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@ Per-tool defaults are tightened by tool config; per-message budget overrides ind

No per-line truncation in the dispatcher; that's the tool schema's responsibility.

**Sources:** `claude-code/src/services/streamingToolExecutor.ts` (per-tool then per-message cap application), `claude-code/src/utils/toolLimits.ts` (tiered cap constants).

### OpenAI Codex (Rust)

No system-wide cap. Tools either bound their own output (e.g., `MATCH_LIMIT = 50` for ripgrep results in `fuzzy_file_search.rs`) or return whatever the underlying command produces. Pagination caps appear on message-level features (`THREAD_LIST_DEFAULT_LIMIT = 25`, `THREAD_TURNS_MAX_LIMIT = 100`) but those bound list responses, not tool output bytes.

The implication: a `bash cat large.log` returns however many bytes ripgrep / cat printed. Codex relies on the model to ask for tighter ranges if a tool emits too much.

### opencode (TypeScript / Effect)
**Sources:** `codex-rs/core/src/tools/handlers/fuzzy_file_search.rs` (`MATCH_LIMIT = 50`, per-tool with no central layer).

### opencode (TypeScript)

Centralized via `Truncate.Service` (one truncation pass after the tool runs, before the result is appended to the message):

Expand All @@ -44,6 +48,8 @@ When either limit trips, the service:

The hint string adapts to agent capabilities — with a Task tool it reads `"Use the Task tool to have explore agent process this file..."`; without it, `"Use Grep / Read with offset/limit on the full content..."`.

**Sources:** `opencode/packages/opencode/src/tool/truncate.ts` (`Truncate.Service`: `MAX_LINES = 2000`, `MAX_BYTES = 50 KB`, `RETENTION = 7 days`, head / tail + file spillover with adapted hint).

## Comparison

| Repo | Cap location | Per-tool cap | System cap | Strategy | Spillover | Per-tool override |
Expand Down Expand Up @@ -91,16 +97,7 @@ The roadmap calls for centralizing the byte-budget at the dispatcher. The decisi

## Sources

### oxide-code

- `crates/oxide-code/src/tool.rs` — `ToolRegistry::run` (dispatcher cap entry point), `cap_output()` (head-tail), `MAX_OUTPUT_BYTES`, `TRUNCATION_OVERHEAD`, `MAX_LINE_LENGTH`, `truncate_line()`.
- `crates/oxide-code/src/tool/glob.rs` — `MAX_RESULTS`, view-shape `truncated_total` setter.
- `crates/oxide-code/src/tool/grep.rs` — `DEFAULT_HEAD_LIMIT`, per-mode row caps, `MAX_GREP_FILE_SIZE`.
- `crates/oxide-code/src/tool/read.rs` — `DEFAULT_LINE_LIMIT`, `MAX_READ_FILE_SIZE`, view-shape footer.

### Reference projects

- `claude-code/src/services/streamingToolExecutor.ts` — dispatcher cap application (per-tool then per-message).
- `claude-code/src/utils/toolLimits.ts` — tiered cap constants (`DEFAULT_MAX_RESULT_SIZE_CHARS`, `MAX_TOOL_RESULT_TOKENS`, `MAX_TOOL_RESULT_BYTES`, `MAX_TOOL_RESULTS_PER_MESSAGE_CHARS`, `BYTES_PER_TOKEN`).
- `codex-rs/core/src/tools/handlers/fuzzy_file_search.rs` — `MATCH_LIMIT = 50` (per-tool, no central layer).
- `opencode/packages/opencode/src/tool/truncate.ts` — `Truncate.Service` (`MAX_LINES = 2000`, `MAX_BYTES = 50 KB`, `RETENTION = 7 days`, head/tail + file spillover with adapted hint).
Loading