Skip to content

Commit 65baef3

Browse files
MagicMock/mock.effective_git_name/140286118293392claude
authored andcommitted
feat(pebble): surface timeout budget in system prompt
The Pebble webhook enforces a hard wall-clock timeout (CLAYDE_PEBBLE_TIMEOUT, default 300s) but never told Claude about it, so the agent could take on work too large to finish and get killed mid-task with no result for the user. build_system_prompt now takes timeout_s and states the budget in the prompt: process is killed on overrun, prefer a smaller complete action, and report the most valuable partial result in the JSON summary if the task is too big. worker.py passes the configured timeout through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c6558bd commit 65baef3

5 files changed

Lines changed: 26 additions & 6 deletions

File tree

src/clayde/webhook/skills.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ def _parse_skill(path: Path) -> Skill:
5353
"log", or "capture", write a file there. No git operations — Syncthing
5454
handles sync.
5555
56+
You have a hard wall-clock budget of {timeout_s} seconds for this
57+
entire request. If your process exceeds it, it is killed and the user gets
58+
no result. Scope your work to fit: prefer a smaller, complete action over an
59+
ambitious one that risks timing out. If the request is too big to finish in
60+
time, do the most valuable part you can and say so in the JSON summary.
61+
5662
{skill_section}
5763
5864
Skills are suggestions, not constraints. Use as many as the command needs,
@@ -72,8 +78,12 @@ def _parse_skill(path: Path) -> Skill:
7278
"""
7379

7480

75-
def build_system_prompt(skills: list[Skill]) -> str:
76-
"""Build the system prompt sent to the Claude CLI for a Pebble request."""
81+
def build_system_prompt(skills: list[Skill], timeout_s: int = 300) -> str:
82+
"""Build the system prompt sent to the Claude CLI for a Pebble request.
83+
84+
``timeout_s`` is the hard wall-clock budget enforced by the runner; it is
85+
surfaced in the prompt so Claude can scope work to fit.
86+
"""
7787
if not skills:
7888
skill_section = "Available skills: (none currently registered)"
7989
else:
@@ -85,7 +95,9 @@ def build_system_prompt(skills: list[Skill]) -> str:
8595
"Skill file paths:\n\n"
8696
f"{files}"
8797
)
88-
return _SYSTEM_PROMPT_TEMPLATE.format(skill_section=skill_section)
98+
return _SYSTEM_PROMPT_TEMPLATE.format(
99+
skill_section=skill_section, timeout_s=timeout_s,
100+
)
89101

90102

91103
def build_user_prompt(text: str, timestamp: int) -> str:

src/clayde/webhook/worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async def process_job(job: PebbleJob, *, timeout_s: int, kb_path: str) -> None:
5656

5757
skills = discover_skills(SKILLS_ROOT)
5858
span.set_attribute("pebble.skills_available", len(skills))
59-
system_prompt = build_system_prompt(skills)
59+
system_prompt = build_system_prompt(skills, timeout_s=timeout_s)
6060
user_text = build_user_prompt(job.text, job.timestamp)
6161

6262
t0 = time.monotonic()

tests/test_pebble_e2e.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def fake_invoke(**kwargs):
4040

4141
monkeypatch.setattr(worker_mod, "invoke_claude_pebble", fake_invoke)
4242
monkeypatch.setattr(worker_mod, "discover_skills", lambda root=None: [])
43-
monkeypatch.setattr(worker_mod, "build_system_prompt", lambda skills: "SYS")
43+
monkeypatch.setattr(worker_mod, "build_system_prompt", lambda skills, timeout_s=300: "SYS")
4444
monkeypatch.setattr(worker_mod, "build_user_prompt", lambda text, ts: text)
4545

4646
# Real queue + real worker_loop.

tests/test_webhook_skills.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ def test_build_system_prompt_empty_catalog():
120120
assert "(none currently registered)" in prompt
121121

122122

123+
def test_prompt_mentions_timeout_budget():
124+
p = build_system_prompt([], timeout_s=300)
125+
assert "300 seconds" in p
126+
assert "budget" in p.lower()
127+
# the runner kills on overrun — the agent must be told to scope work
128+
assert "killed" in p.lower()
129+
130+
123131
def test_build_user_prompt():
124132
out = build_user_prompt("hello world", 1778068506)
125133
assert "1778068506" in out

tests/test_webhook_worker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async def fake_send(*, title, body, success, **_):
3636
@pytest.fixture
3737
def fake_skills(monkeypatch):
3838
monkeypatch.setattr(worker, "discover_skills", lambda root=None: [])
39-
monkeypatch.setattr(worker, "build_system_prompt", lambda skills: "SYS")
39+
monkeypatch.setattr(worker, "build_system_prompt", lambda skills, timeout_s=300: "SYS")
4040
monkeypatch.setattr(worker, "build_user_prompt", lambda text, ts: f"USER:{text}")
4141

4242

0 commit comments

Comments
 (0)