Skip to content

fix(anthropic): stack overflow guard + adaptive thinking format + Fable 5 support - #1548

Open
JFernandoAmorim2005 wants to merge 21 commits into
nextlevelbuilder:devfrom
JFernandoAmorim2005:pr/claude-anthropic-model-support
Open

fix(anthropic): stack overflow guard + adaptive thinking format + Fable 5 support#1548
JFernandoAmorim2005 wants to merge 21 commits into
nextlevelbuilder:devfrom
JFernandoAmorim2005:pr/claude-anthropic-model-support

Conversation

@JFernandoAmorim2005

Copy link
Copy Markdown

Summary

3 fixes/additions to the Anthropic provider, developed against our internal fork while running goclaw as internal AI agent infrastructure (self-hosted, non-commercial-redistribution use per license clarification with @nextlevelbuilder — thanks for the quick response!).

  • Bug fix: ResolveForwardCompat could parse a datestamp (e.g. 20260501) as a minor version when a model ID lacks the -N- separator between major version and date (e.g. claude-opus-4-20260501), decrementing it in an unbounded recursive loop and crashing the gateway with a stack overflow. Guarded — real minor versions are always < 100.
  • Bug fix: two related bugs in the content_block_stop handler for RawAssistantContent thinking-block passback — ThinkingSignature was only set after the stream ended (so tool-use passback always omitted it, causing a 400 on the next turn), and stripThinking=true produced an empty thinking field (also a 400). Both fixed by accumulating per-block signature/content directly.
  • Feature: Opus 4.6/4.7/4.8, Sonnet 4.6, and the Fable family use the newer "adaptive" extended-thinking format (thinking:{type:adaptive} + output_config.effort) rather than the older {type:enabled,budget_tokens} format, which returns HTTP 400 on these models. Added claude-fable-5 to the model registry.
  • Small fix: removed TodoRead/NotebookRead from --disallowedTools in the Claude CLI provider — these tools don't exist in the current Claude CLI and caused the headless invocation to abort.

All changes are scoped to internal/providers/ (Anthropic + Claude CLI providers only). Verified against current main — the stack overflow is independently reproducible on a fresh checkout (regression test included, reliably crashes within seconds without the guard).

Test plan

  • go build ./... clean
  • go test ./internal/providers/... — all existing tests pass, no regressions
  • New regression test for the stack overflow (crashes without the fix, passes with it)
  • New tests for adaptive-thinking format selection (Opus 4.6/4.8, Sonnet 4.6, Fable 5) and legacy-format fallback (Sonnet 4.5)
  • Extended existing SkipsTemperatureForClaude46 test to cover claude-fable-5

🤖 Generated with Claude Code

ducconit and others added 21 commits March 24, 2026 23:03
Co-authored-by: GoClaw Operator <operator@goclaw>
…el (nextlevelbuilder#1141)

session/update notifications carrying a ToolCall (or inline tool_call /
tool_call_update kind) were only dumped at Debug level inside the params
blob. Operators could see `security.tool_granted` (permission granted) but
had no way to tell whether the tool actually executed successfully — both
"permission granted then failed silently" and "permission granted and
succeeded" looked identical in journalctl.

Add a structured Info log emitting toolCallId, name/title, status, and a
content preview (truncated at 400 chars) whenever the notification
contains tool-call state. This is what made it possible to diagnose the
recent .goclaw/-path-deny regression — `status=failed` immediately after
`security.tool_granted` revealed the gap that the granted-only log hid.

No behavior change beyond logging volume; preview is bounded.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xtlevelbuilder#1140)

The ExecTool path-deny rule blocks any token containing `.goclaw/` unless
it matches one of the AllowPathExemptions prefixes (skills-store, tenants).
This silently rejected legitimate commands invoking the goclaw-managed
Python interpreter via its absolute path:

    /home/user/.goclaw/venv/bin/python3 .../script.py

The first token `/home/user/.goclaw/venv/bin/python3` matched the deny
pattern but no exemption, so the entire command was denied.

Naive exemption (".goclaw/venv/bin/") does not work: matchesAnyPathExemption
resolves both tokens and exemption candidates via EvalSymlinks, and the
venv's python3 is a symlink into the host's python cellar (e.g. linuxbrew).
The token canonicalizes to /home/linuxbrew/.../python3.14 while a literal
".goclaw/venv/bin/" prefix never gets touched.

Fix: resolve venv/bin/python3 once at startup and exempt the dirname of
the resolved target. Failure to resolve (no venv present) silently falls
through.

Without this, ACP-driven agents either fail outright or work only via
fragile heuristics (cwd-local symlinks generated on the fly by the LLM).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…1139)

The cron job handler reset the session when stateless=false and skipped
reset when stateless=true — the opposite of what every UI locale labels
the field (en/ko/zh/vi all describe stateless as "each run starts fresh
without loading previous messages").

The buggy gate caused stateless=true crons to silently accumulate session
history across every execution, leading to context bloat and increasing
the chance of LLMs short-circuiting tool calls in favor of replaying
prior assistant turns. One affected daily ETL cron grew to 38 messages
over 18 days before the regression was noticed.

Fix: gate the Reset on `if job.Stateless` so the runtime matches the UI
contract. No DB migration is required — existing values were set by users
based on the UI label, so they already encode the intended behavior.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lder#1332)

* fix(providers): retry transient Codex response failures

* test(cron): tolerate nil session store in handler tests
Co-authored-by: Collective Developer <man@collective.dev>
…der#1375)

Co-authored-by: Trí Đào Nguyễn Minh <tri.dao@buymed.com>
…ng (nextlevelbuilder#554)

Approved by github-maintain automation. Clean implementation of additive Upsert() for fork-specific tool seeding. Solves nextlevelbuilder#336.
…tlevelbuilder#407)

- Check resp.StatusCode range (2xx) before JSON decoding
- Provide clear actionable errors with HTTP status codes for 5xx failures
- Prevent cryptic JSON parse errors from HTML error pages
…extlevelbuilder#713)

When X-GoClaw-User-Id header is provided, build a stable session key
using BuildSessionKey() — same canonical format as Telegram, Discord,
Slack channels. This allows HTTP API clients to maintain conversation
history across multiple requests.

Without the header, behavior is unchanged (random session per request).

Session key format:
  With userId: agent:{id}:http:direct:{userId}  (persistent)
  Without:     agent:{id}:http-{random}          (stateless)

Co-authored-by: Claude Code <claude@anthropic.com>
…extlevelbuilder#714)

The CLI agent chat command connects via WebSocket but never sends
user_id in the connect params. The gateway requires user_id for
chat.send, making the CLI unusable for any agent interaction.

All other clients (browser, Telegram, LINE, etc.) send user_id
during connect. The CLI was the only channel missing it.

Adds --user / -u flag that passes user_id in the connect frame.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…uilder#1505)

Implements nextlevelbuilder#688.

Adds a SpanExporter that signs exported spans as Ed25519 receipts, chains them,
and anchors the sequence with a signed checkpoint, so a third party can verify
what a run did without trusting the collector, the database, or the operator.

Standard library crypto only. No new dependencies, and no existing file is
modified: it attaches through SetExporter the same way OTLP does.

Design notes worth reviewing:

- Coverage counts spans this exporter received, never spans the runtime
  executed. Collector.EmitSpan drops spans when its buffer fills, which is
  correct for observability and means an exporter can only sign what reaches
  it. Every checkpoint carries a note saying so, so a receipt set reads as a
  floor on what happened rather than a complete record.
- The checkpoint is not optional. A hash chain cannot detect truncation:
  removing the newest receipts leaves every remaining link valid. Verification
  reports Valid=false when no checkpoint is supplied, because without one it
  can only say nothing was altered, never that nothing is missing.
- A checkpoint stored beside its receipts is deletable by whoever deletes the
  receipts, so Sink keeps the two destinations separate to make publishing the
  anchor elsewhere straightforward.
- Content is committed by digest rather than copied, since the package redacts
  previews and a second store of that content would widen a leak.
- Ordering comes from an exporter sequence rather than a clock.
- Revocation is not retroactive; RevokedAt covers the compromise case.
- Signature and chain failures are reported separately, and a failed sink write
  does not advance the chain head.

26 tests, most adversarial: tampering, forged signatures under a claimed kid,
cross-domain signature replay, validity windows, revocation ordering, middle
deletion, reordering, end truncation, empty bundles, sink failure, and
concurrent export. go vet and go test -race clean.

Co-authored-by: tommylauren <tfarley@utexas.edu>
…ble-docker-ports

chore(docker): make host ports customizable via environment variables
…nextlevelbuilder#1470)

Mid-loop compaction output now runs through sanitizeHistory so orphaned
tool messages never reach the provider (OpenAI rejects role:tool without
a preceding assistant tool_calls). Background summarization truncates on
tool-chain boundaries via the shared toolChainSplitIndex helper so the
kept tail never starts mid tool-chain.

Co-authored-by: Trí Đào Nguyễn Minh <tri.dao@buymed.com>
…tools inexistentes no Claude CLI atual abortavam o headless)

(cherry picked from commit 0092eba)
…ndo stripThinking=true

Quando stripThinking era true, result.Thinking ficava vazio porque os thinking_delta
events nao eram acumulados. buildRawBlock usava result.Thinking="" para construir o
bloco de thinking, produzindo {type:thinking,thinking:""} invalido.

A API Anthropic rejeita com 400 'messages.1.content.0.thinking.thinking: Field required'
quando um bloco de thinking tem o campo thinking vazio — necessario em tool-use passback.

Fix: rawThinkingBlock acumula sempre o conteudo de thinking (independente de stripThinking).
Em content_block_stop, se stripThinking activo, usa rawThinkingBlock temporariamente
para buildRawBlock e restaura result.Thinking="" (stripped) no fim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit d770cdb)
…hinking bugs

Two bugs in the content_block_stop handler when building RawAssistantContent:

1. result.ThinkingSignature is set only AFTER the stream ends, so buildRawBlock
   was always omitting the signature — Anthropic rejects on iter >0 with
   "messages.N.content.thinking: Field required".

2. When stripThinking=true, result.Thinking is empty so buildRawBlock produced
   {"type":"thinking","thinking":""} which Anthropic also rejects.

Fix: build thinking blocks directly in content_block_stop from per-block accumulators
(rawThinkingBlock for content, perBlockSignature for signature). The all-stream
thinkingSignature accumulator is preserved unchanged for result.ThinkingSignature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 909b818)
…flow

ResolveForwardCompat parses the digits after a model's major version as a
minor version to decrement when looking for a known template (e.g.
"claude-opus-4-7" -> tries "claude-opus-4-6"). When a model ID places a
datestamp where a minor version is expected (e.g.
"claude-opus-4-20260501", no "-N-" separating major from date), that
datestamp gets parsed as version=20260501 and decremented in an
unbounded loop (ResolveForwardCompat -> CloneFromTemplate -> Resolve ->
ResolveForwardCompat), overflowing the goroutine stack and crashing the
gateway.

Real minor versions are always < 100 -- guard against anything larger.

Regression test included; without the guard it reliably reproduces
"fatal error: stack overflow" within a few seconds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Opus 4.6/4.7/4.8, Sonnet 4.6, and the Fable family use the newer
"adaptive" extended-thinking format (thinking:{type:adaptive} +
output_config.effort) instead of the older {type:enabled,budget_tokens}
format -- the older format returns HTTP 400 on these models. This was
already handled for temperature-skipping (anthropicSkipsTemperature)
but not for the thinking-block format itself.

- anthropicUsesAdaptiveThinking: selects the request format per model.
- anthropicEffort: maps thinking_level (low/medium/high) to
  output_config.effort.
- anthropicSkipsTemperature: extended to also match the "claude-fable-"
  prefix (Fable rejects sampling params like Opus 4.7/4.8).
- model_registry.go: register claude-fable-5 (200K ctx, 32K output,
  reasoning+vision), following the same shape as the existing
  claude-opus-4-6/claude-sonnet-4-6 entries.

Tests cover both the adaptive-format models and the legacy-format
fallback (claude-sonnet-4-5) to confirm neither path regresses the
other.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@clark-cant clark-cant added agent:github-maintain Processed by github-maintain automation maintain:triaged Triaged by maintain workflow labels Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:github-maintain Processed by github-maintain automation maintain:triaged Triaged by maintain workflow

Projects

None yet

Development

Successfully merging this pull request may close these issues.