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
46 changes: 43 additions & 3 deletions .claude/docs/oauth-continuation.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,11 +552,51 @@ transport-failure replay, although an OAuth 401 refresh can still start a new au
**This policy can recover only while no model output has been exposed downstream**; replay after
partial output could duplicate content or tool calls.

### Schema defaults are filler on both sides

When a tool call goes through Claude Code's permission path, the echoed call comes back with its zod
defaults filled in: an `Edit` the model emitted without `replace_all` returns as `replace_all: false`.
The head snapshot holds the model's raw arguments, so the strict-prefix comparison failed on every
such call and re-sent the whole conversation (measured 2026-09-11 on one Luna session: 2 of 14 turns,
~88k tokens each; 77.8% → 92.1% cached input with the fix).

This is not a version change. `replace_all` carries the same `.default(false)` in 2.1.267 as in
2.1.268, and captured echoes from both binaries agree: under `--dangerously-skip-permissions` the
property is never filled, while `acceptEdits` and `--allowedTools` fill it in both versions. What
fills it is `checkPermissions` writing `updatedInput` back to the transcript, so the trigger is the
permission path, not a release.

`toolSchemaDefaults(payload)` derives `{tool → {property → canonical default}}` from ONE request's
`tools` array — namespaced tool groups included — and `normalizeToolCallJson` drops, from BOTH sides,
any `arguments` property whose value equals its declared default. Compare-only: the outgoing payload
is untouched. A value that differs from the default (`replace_all: true`) and a property with no
declared default still diverge, as before.

The map is per-request and pure, for the same reason `headRequiredToolProps` snapshots `required`
from the head's own turn. A process-global map keyed only by tool name is last-writer-wins across
every client, partition and session one `clodex server` handles. Independently, a request in the same
partition can carry a different tool list — for example, a main-agent auxiliary request or a
mid-session tool-list change — and populate a head memo under a map the next request no longer uses.
An in-process subagent has its own partition because `responsesWebSocketPartitionKey` includes
`x-claude-code-agent-id`, so it never scans its parent's heads. Without keyed invalidation,
under-stripping costs a chain that should have continued and over-stripping can continue changed
history. The two prefix memos and the in-flight `canonicalInput` memo are therefore keyed on a
fingerprint of the defaults map they were built under and recomputed when it changes. While a
request's tool defaults are unchanged, the fingerprint is stable and the memos still do their job;
when the tool list changes, keyed invalidation recomputes them.

Trade-off: a request whose `tools` omit a tool that appears in its own history gets no stripping for
that tool, which is the pre-fix behaviour. Subagent histories never contain the parent's calls, so
the residual is narrow.

### Mismatch diagnostics

On a history mismatch the head-decision log includes `expected_hash`/`actual_hash` (SHA-256 of each
side's canonical item bytes) whenever at least one side has an item at the divergent index, so
same-kind mismatches are diagnosable without exposing content; `none` marks an unavailable side.
The mismatch index, hashes, tool-argument gap check, and opt-in dump all use the same per-request
tool-defaults map as head matching, so a default-stripped continuation records the full matched
prefix instead of a misleading normalization gap.

`CLODEX_MISMATCH_DUMP=1` additionally writes both divergent items' canonical bytes (capped per line,
`(absent)` past a history's end) into the adapter debug log. **Privacy tradeoff:** the dump contains
Expand All @@ -574,9 +614,9 @@ genuine rewind or branch regenerates the call under a new one. These record
`toolArgumentNormalizationGap` (`tool`, `equalAfterStrip`) on the head-decision diagnostic.

- **Only `equalAfterStrip: true` warns on stderr**, deduplicated by tool and hard-capped (the
terminal is shared with Claude Code's UI). It means the two items are identical once the shared
filler-strip rule is applied to `arguments` — nothing but filler stood between the head and its
own echo.
terminal is shared with Claude Code's UI). It means the two items are identical once head
matching's schema-default normalization and the shared filler-strip rule are both applied to
`arguments` — nothing but filler stood between the head and its own echo.
- **Coverage is narrower than it looks, in two directions.** It fires only when the divergent
`function_call` is the *first* divergent item, with one alignment: a stored reasoning item Claude
legitimately omitted (`continuationMatch`'s omitted-reasoning mode) shifts divergence onto a
Expand Down
Loading
Loading