Skip to content

feat(BA-7322): derive the start-service launch budget from agent config - #13685

Draft
jopemachine wants to merge 27 commits into
mainfrom
feat/BA-7322-configurable-service-launch-budget
Draft

feat(BA-7322): derive the start-service launch budget from agent config#13685
jopemachine wants to merge 27 commits into
mainfrom
feat/BA-7322-configurable-service-launch-budget

Conversation

@jopemachine

@jopemachine jopemachine commented Aug 11, 2026

Copy link
Copy Markdown
Member

📚 Stacked PRs

Part of the start-service timeout work. Merge in order (bottom-up):

  1. fix(BA-7258): wait longer than the kernel runner for a start-service reply #13684fix(BA-7258): wait longer than the kernel runner for a start-service reply
  2. 👉 feat(BA-7322): derive the start-service launch budget from agent config #13685feat(BA-7322): derive the start-service launch timeout from agent config ← you are here

#13684 is merged, and backported as #13696 (26.8) and #13697 (26.4). This PR stays on main.

Resolves #13683 (BA-7322)

Summary

  • The agent and the kernel runner each carried their own hardcoded service-launch timeout, kept in order only by a comment and a test, so an edit to either one could silently re-invert the pair.
  • Add service-launch-timeout-sec to the [kernel-lifecycles] section of the agent config and send it in the start-service payload: the runner uses it as the port-wait timeout, and the agent waits that long plus a fixed margin for the reply. The ordering then holds by construction rather than by convention, and operators can tune it.
  • The default reproduces fix(BA-7258): wait longer than the kernel runner for a start-service reply #13684's behaviour exactly — a 30s launch timeout with the agent giving up at 35s — so this PR is behaviour-preserving and only adds the knob.
  • A payload without the key keeps the runner's own default, so containers created before the upgrade are unaffected.

Why the knob is worth having

#13684 moves the false-failure cutoff from 10s to the runner's own 30s, which covered every launch measured under a saturated pinned core (7/7 succeeded, 22.9s–26.8s). But that measurement peaked ~3s short of the 30s ceiling, and pushing contention further produced repeatable failures at ~31.7s. A site whose images bind that slowly currently has no way to buy headroom without a code change; this PR gives it one.

Test plan

  • tests/unit/agent/test_service_launch_timeout.py — 7 passed: across configured timeouts the agent always outlasts the runner, the runner takes the timeout from the payload, and it falls back when the key is absent.
  • Regenerated configs/agent/sample.toml; the diff is the new key only.
  • Live stack: the running agent loads 'kernel-lifecycles': {... 'service-launch-timeout-sec': 30.0} from agent.toml.
  • Live stack, the knob reaches the runner inside the container and drives both sides: with it set to 0.2, launching jupyter fails at 0.35s carrying the runner's own extra_data: 'opening the service port timed out: jupyter' rather than the agent's generic 'timeout'; back at 30.0, the same app on the same session returns 200 in 0.67s.
  • The default leaves fix(BA-7258): wait longer than the kernel runner for a start-service reply #13684's saturated-core results unchanged, since it keeps the same 30s/35s pair.

Known gap

Lowering the configured value below 25s re-inverts the pair for containers created before the upgrade: their runner predates the payload key and stays at its own 30s default, while the agent would wait only configured + 5. Raising the value is safe; lowering it only takes effect for containers created afterwards.

🤖 Generated with Claude Code

jopemachine and others added 3 commits August 11, 2026 11:24
…reply

The agent gave the kernel runner 10 seconds to answer a start-service
request, but the runner is allowed 30 seconds to wait for the app's port
to open. Any app that took longer than 10 seconds to bind was reported
to the manager as {"status": "failed", "error": "timeout"} and surfaced
as an HTTP 500, even though the app went on to start moments later and a
retry would succeed.

Raise the agent's reply timeout past the runner's launch timeout so the
inner budget always expires first and the caller sees the runner's real
verdict.

The runner's 30 second launch timeout is deliberately left alone: it
terminates the service process when it expires, so shortening it would
kill apps that currently survive to be picked up by a retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The agent and the kernel runner each carried their own hardcoded
service-launch timeout. They live in packages that cannot import each
other, since the runner ships inside the container, so nothing stopped a
future edit to one of them from re-inverting the pair.

Add `service-launch-timeout-sec` to the `[kernel-lifecycles]` section of
the agent config and send it in the start-service payload. The runner
uses it as the port-wait budget and the agent waits that long plus a
fixed margin for the reply, so both sides now come from one number and
operators can tune it per deployment.

The default reproduces the previous behaviour exactly: a 30 second
launch budget with the agent giving up at 35. A payload without the key
keeps the runner's own default, so containers created before the upgrade
are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size:L 100~500 LoC comp:agent Related to Agent component labels Aug 11, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jopemachine and others added 21 commits August 11, 2026 14:50
The agent's reply budget and the kernel runner's launch budget are two
literals in packages that cannot import each other, since the runner
ships inside the container. Nothing but a test can keep them ordered,
and getting them backwards is the defect this branch fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pair it guarded no longer exists: both budgets now derive from one
configured value, and test_service_launch_budget.py asserts the ordering
across configured budgets instead of between two literals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comparing the two literals missed the case where the constant stays
right but the call site hardcodes its own number. Drive
feed_start_service instead, with the reply arriving only after a delay,
and assert on the budget it actually installs.

Verified against three ways of reintroducing the defect: lowering the
constant, hardcoding the call site, and raising the runner's own budget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	tests/unit/agent/test_service_launch_timeout_ordering.py
The delayed-reply task and the timeout-branch case were decoration: the
budget the call installs is what catches the defect. Still fails on all
three ways of reintroducing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	tests/unit/agent/test_service_launch_timeout_ordering.py
'Budget' was invented here; these two files say launch_timeout,
exec_timeout, timeout_seconds. 'Installed' was invented too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
'Budget' was invented for these changes; the surrounding code says
launch_timeout, exec_timeout, timeout_seconds. Renames the test file,
the case dataclass, the config description and the news fragment to
match, and regenerates sample.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old comment gave the cross-package reason the two sides carry
separate numbers, which reads as if it justified the introspection. It
does not: there is simply no constant to import, only a parameter
default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two files with near-identical names covering the same subject was an
artifact of the stack, not a decision: the ordering test has to stand
alone on the fix branch, which is the one being backported. Here it
belongs with the rest, and reading the runner's timeout by introspection
goes away -- this branch gives it a name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The follow-up adds the configured-timeout cases to the same subject, so
a name scoped to 'ordering' would have forced a second, near-identically
named file next to it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	tests/unit/agent/test_service_launch_timeout.py
Both are fixed at import time, so driving feed_start_service against a
mocked socket pair and a pre-filled queue proved nothing the comparison
does not -- the reply was already queued, so nothing ever waited. The
machinery only covered a call site that ignores its own constant, which
is not a way this breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	tests/unit/agent/test_service_launch_timeout.py
Reading it back out of the function signature was a workaround for it
not having one. Naming it changes no behaviour -- it stays the default
of _start_service -- and the test then just compares two imported
numbers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	src/ai/backend/kernel/base.py
#	tests/unit/agent/test_service_launch_timeout.py
Neither name says which end of the handshake it is, so the assertion
read as two unrelated numbers. Aliased, it states the invariant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-configurable-service-launch-budget

# Conflicts:
#	tests/unit/agent/test_service_launch_timeout.py
Base automatically changed from fix/BA-7258-start-service-reply-timeout to main August 11, 2026 07:06
jopemachine and others added 2 commits August 11, 2026 16:09
…able-service-launch-budget

# Conflicts:
#	src/ai/backend/agent/kernel.py
#	src/ai/backend/kernel/base.py
#	tests/unit/agent/test_service_launch_timeout.py
ruff wanted the config lookup on one line, and mypy reads the field
defaults declared inside Annotated[..., Field(default=...)] as required
constructor arguments -- model_validate goes through the alias the
production code reads anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:agent Related to Agent component size:L 100~500 LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Derive the start-service launch budget from agent configuration instead of hardcoded constants

1 participant