fix(anthropic): stack overflow guard + adaptive thinking format + Fable 5 support - #1548
Open
JFernandoAmorim2005 wants to merge 21 commits into
Open
Conversation
release: promote dev to stable
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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!).
ResolveForwardCompatcould 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.content_block_stophandler forRawAssistantContentthinking-block passback —ThinkingSignaturewas only set after the stream ended (so tool-use passback always omitted it, causing a 400 on the next turn), andstripThinking=trueproduced an emptythinkingfield (also a 400). Both fixed by accumulating per-block signature/content directly.thinking:{type:adaptive}+output_config.effort) rather than the older{type:enabled,budget_tokens}format, which returns HTTP 400 on these models. Addedclaude-fable-5to the model registry.TodoRead/NotebookReadfrom--disallowedToolsin 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 currentmain— the stack overflow is independently reproducible on a fresh checkout (regression test included, reliably crashes within seconds without the guard).Test plan
go build ./...cleango test ./internal/providers/...— all existing tests pass, no regressionsSkipsTemperatureForClaude46test to coverclaude-fable-5🤖 Generated with Claude Code