7050b7d165
Root cause: a large write_file `content` arg is truncated (finish_reason=length) before its bytes are emitted, so parse_partial_json drops the key and every retry re-raises "content: Field required", spinning to recursion_limit=200 (~19min). Not a model-can't-count-2-args problem — the huge tool argument gets cut off/parsed away. - L2: detect finish_reason=length + a truncated tool call in the resilience middleware, nudge to write in smaller parts, bounded retry (truncation_retry_limit). - L3: new LinsightToolLoopBreakerMiddleware — soft corrective hint then hard stop (aafter_model raises LinsightToolLoopError) on consecutive same-tool failures, with guards against aborting a model that recovered / switched tools. - L4: classify GraphRecursionError / LinsightToolLoopError as TASK_ABORTED. - Salvage: _handle_task_partial renders the intermediate analysis + retrieved knowledge as a normal (COMPLETED) result instead of a raw recursion error; shared across the fresh/resume/continue drivers via _stash_partial_abort. - Config (LinsightConf): tool_failure_soft_limit=3 / tool_failure_hard_limit=8 / truncation_retry_limit=2. 72 unit tests + 1 integration test (aafter_model raise propagates through a real create_agent().ainvoke(), beating recursion_limit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>