tsugite-claude-code 0.24.0__tar.gz → 0.25.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -196,3 +196,4 @@ plugins/tsugite-daemon/tsugite_daemon/web/
196
196
  !plugins/tsugite-daemon/frontend/src/lib/**
197
197
  # Per-job git worktrees provisioned by the daemon.
198
198
  .tsugite-jobs/
199
+ /packaging/runtime/.cache/
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tsugite-claude-code
3
- Version: 0.24.0
3
+ Version: 0.25.0
4
4
  Summary: Tsugite plugin: Claude Code provider (routes LLM calls through the claude CLI)
5
5
  Author: Justyn Shull
6
6
  Requires-Python: >=3.11
7
- Requires-Dist: tsugite-cli==0.24.0
7
+ Requires-Dist: tsugite-cli==0.25.0
@@ -1,11 +1,11 @@
1
1
  [project]
2
2
  name = "tsugite-claude-code"
3
- version = "0.24.0"
3
+ version = "0.25.0"
4
4
  description = "Tsugite plugin: Claude Code provider (routes LLM calls through the claude CLI)"
5
5
  authors = [{ name = "Justyn Shull" }]
6
6
  requires-python = ">=3.11"
7
7
  dependencies = [
8
- "tsugite-cli==0.24.0",
8
+ "tsugite-cli==0.25.0",
9
9
  ]
10
10
 
11
11
  [project.entry-points."tsugite.providers"]
@@ -366,6 +366,7 @@ class ClaudeCodeProcess:
366
366
  "context_window": context_window,
367
367
  "is_error": bool(event.get("is_error")),
368
368
  "subtype": event.get("subtype"),
369
+ "errors": event.get("errors") or [],
369
370
  }
370
371
  return
371
372
 
@@ -40,16 +40,12 @@ _CLAUDE_CODE_MODELS: dict[str, ModelInfo] = {
40
40
  # --- END GENERATED MODELS (claude_code) --- #
41
41
 
42
42
  # Bare tier aliases track the newest model in that tier; version-pinned aliases
43
- # stay for reproducible configs. Exception: opus stays on 4-8. Opus 5's replies
44
- # through Claude Code's resume path can leave an empty assistant block that wedges
45
- # the sidecar transcript (400 "text content blocks must be non-empty"), and the
46
- # fresh-session fallback only covers the first send, so opus-5 and opus-5-5 are
47
- # opt-in via their pinned aliases until a mid-conversation poison recovers cleanly.
43
+ # stay for reproducible configs.
48
44
  _ALIASES = {
49
45
  "fable": "claude-fable-5-1",
50
46
  "fable-5-1": "claude-fable-5-1",
51
47
  "fable-5": "claude-fable-5",
52
- "opus": "claude-opus-4-8",
48
+ "opus": "claude-opus-5-5",
53
49
  "opus-5-5": "claude-opus-5-5",
54
50
  "opus-5": "claude-opus-5",
55
51
  "opus-4-8": "claude-opus-4-8",
@@ -57,7 +53,7 @@ _ALIASES = {
57
53
  "opus-4-6": "claude-opus-4-6",
58
54
  "opus-4-5": "claude-opus-4-5",
59
55
  "opus-4-5-20251101": "claude-opus-4-5-20251101",
60
- "sonnet": "claude-sonnet-5",
56
+ "sonnet": "claude-sonnet-5-5",
61
57
  "sonnet-5-5": "claude-sonnet-5-5",
62
58
  "sonnet-5": "claude-sonnet-5",
63
59
  "sonnet-4-6": "claude-sonnet-4-6",
@@ -70,14 +66,6 @@ _ALIASES = {
70
66
  }
71
67
 
72
68
 
73
- def _is_unresumable_history_error(exc: BaseException) -> bool:
74
- """A 400 on a resume replay means the sidecar transcript itself is malformed
75
- (e.g. an empty text content block) — retrying the same resume can never
76
- succeed. Anything else (prompt too long, 429/529, execution errors) must
77
- keep surfacing so the daemon's existing retry paths handle it."""
78
- return "API Error: 400" in str(exc)
79
-
80
-
81
69
  def _raise_if_error(result_event: dict) -> None:
82
70
  """Translate a Claude CLI error result into AgentExecutionError.
83
71
 
@@ -88,8 +76,14 @@ def _raise_if_error(result_event: dict) -> None:
88
76
  """
89
77
  if not result_event.get("is_error"):
90
78
  return
91
- text = result_event.get("text") or "Claude Code returned an error result"
79
+ text = (
80
+ result_event.get("text")
81
+ or "; ".join(result_event.get("errors") or [])
82
+ or "Claude Code returned an error result"
83
+ )
92
84
  subtype = result_event.get("subtype") or "error"
85
+ if "Failed to authenticate" in text:
86
+ text = f"{text.rstrip('.')}. Run `claude /login` on the daemon host to sign in again."
93
87
  raise AgentExecutionError(f"{text} (subtype={subtype})")
94
88
 
95
89
 
@@ -204,14 +198,20 @@ class ClaudeCodeProvider:
204
198
  return await self._collect(retry_content)
205
199
 
206
200
  def _should_fallback_to_fresh_session(self, first_turn: bool, err: BaseException) -> bool:
207
- """Only a first-send 400 on a live resume is a poisoned transcript we can recover from."""
208
- return bool(first_turn and self._resume_session and _is_unresumable_history_error(err))
201
+ """A first-send 400 on a resume means the sidecar transcript itself is malformed
202
+ (e.g. an empty text content block), and a missing transcript was deleted by the
203
+ CLI. Retrying the same resume can never succeed. Anything else (prompt too long,
204
+ 429/529, other execution errors) must keep surfacing so the daemon's retry paths
205
+ handle it."""
206
+ if not (first_turn and self._resume_session):
207
+ return False
208
+ message = str(err)
209
+ return "API Error: 400" in message or "No conversation found with session ID" in message
209
210
 
210
211
  async def _fallback_to_fresh_session(self, err: BaseException) -> str:
211
- """Sever a poisoned resume: replace the subprocess with a fresh session
212
- seeded from tsugite's serialized history, and return the rebuilt first
213
- message. The serializer renders "Role: content" lines, so a turn whose
214
- raw content was empty survives as a non-empty block."""
212
+ """Sever an unresumable resume (poisoned or deleted transcript): replace the
213
+ subprocess with a fresh session seeded from tsugite's serialized history,
214
+ and return the rebuilt first message."""
215
215
  from tsugite_claude_code.process import ClaudeCodeProcess
216
216
 
217
217
  logger.warning(