devops-bot-sdk 1.4.22__tar.gz → 1.4.24__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.
Files changed (38) hide show
  1. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/pyproject.toml +1 -1
  4. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/client.py +1 -1
  6. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/ipc/handlers.py +54 -5
  7. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/local_exec.py +85 -3
  8. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/README.md +0 -0
  9. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/collectors/__init__.py +0 -0
  15. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/collectors/files.py +0 -0
  16. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/collectors/process.py +0 -0
  17. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/collectors/screenshot.py +0 -0
  18. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/config.py +0 -0
  19. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/crucial.py +0 -0
  20. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/exceptions.py +0 -0
  21. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/git_ops.py +0 -0
  22. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/graphify.py +0 -0
  23. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/hooks/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/hooks/crucial_guard.py +0 -0
  25. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/ipc/__init__.py +0 -0
  26. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/ipc/electron_bridge.py +0 -0
  27. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/models/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/models/envelope.py +0 -0
  29. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/models/requests.py +0 -0
  30. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/models/responses.py +0 -0
  31. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/models/snapshots.py +0 -0
  32. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/py.typed +0 -0
  33. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/run_auto.py +0 -0
  34. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/sse.py +0 -0
  35. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/test.py +0 -0
  36. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/test_pipeline.py +0 -0
  37. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/sdk/updater.py +0 -0
  38. {devops_bot_sdk-1.4.22 → devops_bot_sdk-1.4.24}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.22
3
+ Version: 1.4.24
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.22
3
+ Version: 1.4.24
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devops-bot-sdk"
7
- version = "1.4.22"
7
+ version = "1.4.24"
8
8
  description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.22
3
+ Version: 1.4.24
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.22"
33
+ __version__ = "1.4.24"
34
34
  __author__ = "AgentOS"
35
35
 
36
36
  from sdk.client import BackendClient
@@ -36,7 +36,7 @@ from sdk.sse import _check_status, stream_with_reconnect
36
36
 
37
37
  logger = logging.getLogger(__name__)
38
38
 
39
- SDK_VERSION = "1.4.22"
39
+ SDK_VERSION = "1.4.24"
40
40
  _POLL_INTERVAL = 3.0
41
41
  _POLL_TIMEOUT = 600.0
42
42
  _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
@@ -296,8 +296,14 @@ async def _run_local_pipeline(
296
296
  body["user_id"] = derived
297
297
 
298
298
  # 1. Kick off the background run (server plans, gates — does NOT execute).
299
+ # The kickoff sends background=True and should return a session_id quickly, but
300
+ # under load the backend can take well over 30s to ack — which previously raised
301
+ # a (blank-message) ReadTimeout that sent the whole ticket to Backlog. Connect
302
+ # fast, but allow a generous read window so a slow-but-working ack isn't killed.
299
303
  import httpx
300
- async with httpx.AsyncClient(timeout=30.0) as http:
304
+ _kick_read_s = float(os.getenv("AGENTOS_ORCHESTRATE_KICKOFF_TIMEOUT_S", "") or 120.0)
305
+ _kick_timeout = httpx.Timeout(_kick_read_s, connect=15.0)
306
+ async with httpx.AsyncClient(timeout=_kick_timeout) as http:
301
307
  resp = await http.post(client._url("/api/v1/orchestrate"), json=body, headers=client._headers)
302
308
  if resp.status_code >= 400:
303
309
  await send(_error_envelope(thread_id, RuntimeError(f"orchestrate failed: {resp.status_code} {resp.text[:200]}")).model_dump())
@@ -560,6 +566,36 @@ async def _code_one_repo(
560
566
  )
561
567
  session_id = result.get("session_id") or session_id
562
568
 
569
+ # 4b. output-token-cap recovery. A single assistant turn that would exceed the
570
+ # CLI's per-response output cap aborts the run ("response exceeded the N output
571
+ # token maximum"). Resume the session with a doubled cap and an instruction to
572
+ # emit the work in smaller chunks, so a large dump finishes across turns instead
573
+ # of failing the ticket.
574
+ cap_resumes = 0
575
+ next_cap = local_exec._MAX_OUTPUT_TOKENS
576
+ while (not result.get("ok")) and result.get("output_cap_exceeded") \
577
+ and session_id and cap_resumes < _MAX_OUTPUT_CAP_RESUMES:
578
+ cap_resumes += 1
579
+ next_cap = min(next_cap * 2, 128000) # CLI/model output ceiling
580
+ await send(Envelope(
581
+ type="step_update", thread_id=thread_id,
582
+ data={"status": "running",
583
+ "current_step": f"[{name}] Response exceeded output-token cap — "
584
+ f"resuming with cap {next_cap} and chunked output "
585
+ f"(attempt {cap_resumes}/{_MAX_OUTPUT_CAP_RESUMES})"},
586
+ ).model_dump())
587
+ result = await local_exec.run_claude_local(
588
+ "Your previous response was cut off because it exceeded the output-token "
589
+ "limit for a single turn. Continue from where you left off and finish the "
590
+ "task. Keep each response within the limit: write results incrementally to "
591
+ "files in smaller steps rather than emitting one very large response, and "
592
+ "avoid dumping large content back as chat text.",
593
+ repo_path, on_event=_on_event,
594
+ github_token=gh_token, resume_session_id=session_id,
595
+ approval_dir=approval_dir, max_output_tokens=next_cap,
596
+ )
597
+ session_id = result.get("session_id") or session_id
598
+
563
599
  # 5. mid-run crucial-decision gate
564
600
  if agent_mode and session_id:
565
601
  from sdk import crucial
@@ -1529,6 +1565,9 @@ _FIRST_GATE_TIMEOUT_S = 900.0
1529
1565
  _APPROVAL_POLL_TIMEOUT_S = 1200.0
1530
1566
  # Max times a single ticket's session is resumed after a context overflow.
1531
1567
  _MAX_CONTEXT_RESUMES = 3
1568
+ # Max times a ticket's session is resumed after hitting the per-response output-token
1569
+ # cap, each time with a doubled cap, to let a large turn finish across chunks.
1570
+ _MAX_OUTPUT_CAP_RESUMES = 3
1532
1571
  # Max mid-run crucial-decision approval gates per ticket (safety bound).
1533
1572
  _MAX_CRUCIAL_GATES = 10
1534
1573
  # Max times a ticket is retried after hitting the Claude usage limit.
@@ -1832,13 +1871,23 @@ async def handle_orchestrate_auto_local_run(
1832
1871
  ).model_dump())
1833
1872
  return
1834
1873
  continue # re-run this ticket from phase 1
1835
- # Out of retries leave it in Backlog for a later run.
1874
+ # Out of retries. The usage/session limit is ACCOUNT-WIDE, so
1875
+ # every remaining ticket would hit the same wall and burn its
1876
+ # own retry budget (up to 6h each). Pause the whole run instead
1877
+ # of marching on — this ticket and the rest stay in To Do/
1878
+ # Backlog, and `agentos-auto resume` picks them up after reset.
1836
1879
  await send(Envelope(
1837
1880
  type="step_update", thread_id=thread_id,
1838
- data={"current_step": f"{jira_key}: usage limit — max retries reached, "
1839
- f"left in Backlog"},
1881
+ data={"current_step": f"{jira_key}: usage limit — max retries reached; "
1882
+ f"pausing run (resume after the window resets)"},
1840
1883
  ).model_dump())
1841
- break
1884
+ await send(Envelope(
1885
+ type=Envelope.TYPE_DONE, thread_id=thread_id,
1886
+ data={"mode": "orchestrate-auto-local", "status": "paused",
1887
+ "reason": "usage_limit", "processed": processed,
1888
+ "task_count": total},
1889
+ ).model_dump())
1890
+ return
1842
1891
 
1843
1892
  processed += 1
1844
1893
  break
@@ -23,7 +23,7 @@ import os
23
23
  import re
24
24
  import shutil
25
25
  import sys
26
- from datetime import datetime
26
+ from datetime import datetime, timedelta
27
27
  from pathlib import Path
28
28
  from typing import Any, Awaitable, Callable
29
29
 
@@ -62,6 +62,23 @@ _CONTEXT_MARKERS = (
62
62
  "token limit", "prompt is too long", "exceeds the maximum", "context_length_exceeded",
63
63
  )
64
64
 
65
+ # Markers that indicate the CLI's per-response OUTPUT-token cap was hit — distinct
66
+ # from a context overflow (input side) and a usage/rate limit (account side). The
67
+ # CLI aborts a single assistant turn that would exceed CLAUDE_CODE_MAX_OUTPUT_TOKENS
68
+ # (default 32000). We raise that cap below and, if it's still hit, resume the
69
+ # session to finish the turn in smaller chunks.
70
+ _OUTPUT_CAP_MARKERS = (
71
+ "output token maximum", "max_output_tokens", "claude_code_max_output_tokens",
72
+ "max output tokens", "output_tokens exceed",
73
+ )
74
+
75
+ # Per-response output-token cap handed to the CLI via CLAUDE_CODE_MAX_OUTPUT_TOKENS.
76
+ # The CLI default is 32000, which a large single dump (e.g. extracting a whole
77
+ # taxonomy from a spreadsheet) can blow past, failing the whole ticket. 64000 is a
78
+ # safe headroom for the Opus/Sonnet families. Env-tunable; a value the user already
79
+ # exported in the environment is respected and never overridden.
80
+ _MAX_OUTPUT_TOKENS = int(_f("AGENTOS_MAX_OUTPUT_TOKENS", 64000.0))
81
+
65
82
  # Markers that indicate the Claude USAGE/RATE limit (the N-hour window) was hit —
66
83
  # distinct from a context overflow. The run didn't fail on the task; the account
67
84
  # is throttled until the window resets.
@@ -69,9 +86,21 @@ _LIMIT_MARKERS = (
69
86
  "usage limit reached", "usage limit", "rate limit", "rate_limit",
70
87
  "too many requests", "limit will reset", "limit reached", "overloaded_error",
71
88
  "resets at", "429",
89
+ # Newer Claude Code CLI wording: "You've hit your session limit · resets 10:20pm
90
+ # (Asia/Karachi)" and "You've hit your usage limit". Without these the message
91
+ # fell through to the generic-failure path and the auto-loop never waited for
92
+ # the window to reset — every following ticket hit the same limit and cascaded
93
+ # to Backlog.
94
+ "session limit", "you've hit your", "youve hit your",
72
95
  )
73
96
  # A unix-seconds reset timestamp claude sometimes appends (e.g. "...reached|1719500400").
74
97
  _RESET_EPOCH_RE = re.compile(r"\b(1[6-9]\d{8}|20\d{8})\b")
98
+ # A wall-clock reset the CLI prints instead, e.g. "resets 10:20pm (Asia/Karachi)"
99
+ # or "resets at 10pm". Captures hour, optional minutes, am/pm, optional IANA tz.
100
+ _RESET_CLOCK_RE = re.compile(
101
+ r"reset[s]?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*([ap]m)\b(?:\s*\(([^)]+)\))?",
102
+ re.IGNORECASE,
103
+ )
75
104
  # Fallback wait when the limit message carries no reset time (env-tunable).
76
105
  _LIMIT_DEFAULT_WAIT_S = _f("AGENTOS_LIMIT_DEFAULT_WAIT_S", 60 * 60.0) # 1 h
77
106
 
@@ -82,7 +111,15 @@ def _is_limit(text: str | None) -> bool:
82
111
 
83
112
 
84
113
  def _parse_reset_epoch(text: str | None) -> int | None:
85
- """A unix-seconds reset time embedded in a limit message, if present."""
114
+ """A unix-seconds reset time embedded in a limit message, if present.
115
+
116
+ Handles two shapes the CLI uses:
117
+ 1. A literal unix timestamp (e.g. "...reached|1719500400").
118
+ 2. A wall-clock time (e.g. "resets 10:20pm (Asia/Karachi)") — resolved to
119
+ the next future occurrence of that time in the named tz (local tz when
120
+ none/unknown). Returns None when neither is present, so callers fall
121
+ back to the default wait.
122
+ """
86
123
  if not text:
87
124
  return None
88
125
  m = _RESET_EPOCH_RE.search(text)
@@ -91,7 +128,33 @@ def _parse_reset_epoch(text: str | None) -> int | None:
91
128
  return int(m.group(1))
92
129
  except ValueError:
93
130
  return None
94
- return None
131
+ return _parse_reset_clock(text)
132
+
133
+
134
+ def _parse_reset_clock(text: str) -> int | None:
135
+ """Resolve a 'resets 10:20pm (Asia/Karachi)'-style reset to a unix epoch."""
136
+ m = _RESET_CLOCK_RE.search(text)
137
+ if not m:
138
+ return None
139
+ try:
140
+ hour = int(m.group(1)) % 12 # 12am → 0, 12pm → 0 (+12 below)
141
+ minute = int(m.group(2) or 0)
142
+ if m.group(3).lower() == "pm":
143
+ hour += 12
144
+ tz = None
145
+ if m.group(4):
146
+ try:
147
+ from zoneinfo import ZoneInfo
148
+ tz = ZoneInfo(m.group(4).strip())
149
+ except Exception: # noqa: BLE001 — unknown tz → local time
150
+ tz = None
151
+ now = datetime.now(tz)
152
+ target = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
153
+ if target <= now: # already passed today → next day
154
+ target += timedelta(days=1)
155
+ return int(target.timestamp())
156
+ except Exception: # noqa: BLE001 — never let reset parsing break limit handling
157
+ return None
95
158
 
96
159
 
97
160
  # ── Per-run claude log file (on the user's machine, local-timezone stamps) ──
@@ -162,6 +225,12 @@ def _ctx_exceeded(text: str | None) -> bool:
162
225
  low = (text or "").lower()
163
226
  return any(m in low for m in _CONTEXT_MARKERS)
164
227
 
228
+
229
+ def _output_cap_exceeded(text: str | None) -> bool:
230
+ """True when the CLI aborted because the response hit the per-turn output cap."""
231
+ low = (text or "").lower()
232
+ return any(m in low for m in _OUTPUT_CAP_MARKERS)
233
+
165
234
  EventHandler = Callable[[dict], Awaitable[None]]
166
235
 
167
236
 
@@ -186,6 +255,7 @@ async def run_claude_local(
186
255
  resume_session_id: str | None = None,
187
256
  approval_dir: str | None = None,
188
257
  idle_timeout: float = _IDLE_TIMEOUT_S,
258
+ max_output_tokens: int | None = None,
189
259
  ) -> dict[str, Any]:
190
260
  """Run the local `claude` CLI non-interactively against ``project_path``.
191
261
 
@@ -258,6 +328,16 @@ async def run_claude_local(
258
328
  # `git clone https://github.com/owner/repo` authenticates — without touching
259
329
  # the user's global git config. Tokens stay in the subprocess env only.
260
330
  env = dict(os.environ)
331
+
332
+ # Raise the CLI's per-response output-token cap. Without this the CLI errors
333
+ # out ("Claude's response exceeded the 32000 output token maximum") the moment
334
+ # a single assistant turn would exceed 32000 tokens — a large dump (extracting
335
+ # a whole taxonomy, generating many files at once) fails the entire ticket.
336
+ # A value the operator already exported wins; the `max_output_tokens` override
337
+ # lets a retry escalate the cap after a cap-exceeded run (see handlers.py).
338
+ if "CLAUDE_CODE_MAX_OUTPUT_TOKENS" not in env or max_output_tokens:
339
+ env["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = str(max_output_tokens or _MAX_OUTPUT_TOKENS)
340
+
261
341
  if github_token:
262
342
  env["GH_TOKEN"] = github_token
263
343
  env["GITHUB_TOKEN"] = github_token
@@ -419,6 +499,7 @@ async def run_claude_local(
419
499
  "cost_usd": 0.0, "files_changed": files_changed,
420
500
  "session_id": session_id, "context_exceeded": _ctx_exceeded(err),
421
501
  "limit_exceeded": _is_limit(err), "limit_reset_epoch": _parse_reset_epoch(err),
502
+ "output_cap_exceeded": _output_cap_exceeded(err),
422
503
  })
423
504
 
424
505
  usage = result_event.get("usage") or {}
@@ -453,4 +534,5 @@ async def run_claude_local(
453
534
  "context_exceeded": is_error and _ctx_exceeded(err_text),
454
535
  "limit_exceeded": limit_hit,
455
536
  "limit_reset_epoch": reset_epoch,
537
+ "output_cap_exceeded": is_error and _output_cap_exceeded(err_text),
456
538
  })