Skip to content

Swarm 0.10.0: out-of-band transport, telemetry, model discovery, one config parser - #54

Open
gering wants to merge 10 commits into
mainfrom
task/fix-swarm-timeout-ceiling
Open

Swarm 0.10.0: out-of-band transport, telemetry, model discovery, one config parser#54
gering wants to merge 10 commits into
mainfrom
task/fix-swarm-timeout-ceiling

Conversation

@gering

@gering gering commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

Swarm reviews reproducibly lost an entire model family — three runs in a row, always grok × breakage at exactly 600 s. This PR removes the adjacent failure mode, measures away several wrong theories, and makes the remaining one legible. It does not raise the 600 s ceiling; that needs an async transport and is split out into tasks/async-poll-external-voices.md.

  • The 120 KiB prompt wall is gone. The prompt no longer travels on argv, so exec's MAX_ARG_STRLEN is no longer the binding limit. Above the old cap the skill dropped every external voice — the same damage as a timeout, from a limit that was never inherent to the CLIs.
  • What actually drives runtime was measured, and it is not what everyone assumed. Prompt size is ruled out; the lens cluster dominates.
  • Timeouts now report themselves. Both timeouts derive from one value, so rc=124 survives; per-call telemetry exposes voices approaching the wall before they cross it; a lost model family is called out where it changes meaning — in the consensus line.
  • The grok model is discovered, not pinned — a hard-coded id had already silently dropped grok from every review once.
  • The numeric configuration has exactly one parser. Three review rounds found three instances of the same split-brain bug; the third answers the class instead of the instance.

Measurements (these drove the decisions)

Same 42 KB diff, one variable at a time:

Backend Effort Cluster Duration Findings
grok high breakage (3 lenses) 374 s 4
grok low breakage 161 s 4
grok high consistency 28 s 6
codex high breakage 104 s 2

Control: a 164 KiB prompt returned in 20 s (grok) / 8.6 s (codex) — 4× the bytes, 1/20th the time.

  • Diff chunking: rejected. It targets the one variable measurement excludes.
  • grok --max-turns: rejected. N=10 → 10 s but zero findings; N=20 → 279 s. The truncated run exits rc=0 with empty findings, so the pipeline reads a silenced voice as "reviewed cleanly, found nothing" — worse than a timeout, which at least reaches backendErrors.
  • Effort is the largest untried runtime lever (2.3×, same findings) — but that axis belongs to add-swarm-review-profiles.

Changes

Transport (0.8.0) — codex reads the prompt from stdin (-- -), grok via --prompt-file; the adapter passes the file path through instead of reading it into a shell variable. Cap is now model context (SWARM_MAX_PROMPT_BYTES, 512 KiB), read by adapter and skill from the same knob so an override reaches both. Verified end-to-end at 164 KiB on both backends.

Telemetry (0.8.1)agents.sh run --telemetry <file> --unit <name> records duration, effective effort/model, prompt bytes, backend rc, timed_out and the wall the call ran under, written from the EXIT trap so timeouts are captured. telemetry-report.py flags any surviving call at ≥60 % of its wall — the case backendErrors structurally cannot show.

Lens split (0.9.0)cross-file-trace moves into its own reach cluster. The measured reason is lens crowd-out: the combined 3-lens call returned 3 of 4 findings from one lens; split, the other two found 4 issues it had missed entirely. Not a speed fix — the longest call drops only 374 → 313 s.

Diagnosis (0.9.1) — both timeouts derive from one value, with the adapter cap pinned below the Bash window so it wins the race and rc=124 survives (the old 600/600 tie is why raising SWARM_TIMEOUT looked counterproductive). The balance block now names a lost model family and states that consensus weakened — or became unreachable.

Model discovery (0.9.2) — the grok model is discovered from grok models instead of pinned: a canonical-id filter, component-wise version ordering (so grok-4.20 sorts above grok-4.6, which string comparison gets backwards), and a hard verification gate — a model must be schema-verified before the adapter will run it, because a CLI that merely accepts --json-schema and returns structuredOutput: null fails only after burning a full review. The remaining pin is a floor, deliberately the oldest verified id. This section exists because grok CLI 1.0.3 changed its listing format to mark only the default with *, and the old *-only matcher concluded the pinned model was not offered — dropping grok from every review, silently. test_grok_models.py now parses the shipped awk program out of agents.sh (never a retyped copy) and pins both listing formats against it.

One parser for the numeric knobs (0.10.0) — new verb agents.sh config prints the resolved, validated configuration (max_prompt_bytes, cap_headroom, oversize_threshold, timeout_seconds, probe_timeout_seconds); the skill's prep block reads those lines instead of re-deriving them. One _resolve_int enforces digits-only, 10# decimal forcing, a range check after conversion, a length guard against 64-bit wrap, and an explicit upper bound. Also: with_timeout escalates to SIGKILL (-k 3) — plain SIGTERM let a backend outlive the cap, so the outer Bash window killed the adapter instead and no telemetry survived; SWARM_PROBE_TIMEOUT is capped at 20 s and the coupling to the workflow's TIMEOUT_MARGIN_S is documented on both sides; grok's --prompt-file probe moved into readiness (a CLI without it now reports not-ready once, instead of every cluster failing identically); a --model override is validated against the offered list; _bounded_probe uses 126, not 127, as its "cannot bound this call" sentinel, since timeout itself exits 127 for a missing command.

Prompt heredoc (0.10.1)cat <<HDR is unquoted on purpose ($NONCE/$CAP_RULES must expand), which also evaluates backticks in the body. The rule listing the allowed finding prefixes was written with markdown backticks around `[lens]`, so bash ran it as a command: every review since 0.7.0 printed [lens]: command not found and shipped a prompt whose rule had lost the very tag it describes. Found by executing the prep block rather than reading it — the three self-reviews below could not see it for exactly that reason. test_lens_sync.py now rejects any backtick or $( inside an unquoted heredoc there.

Review of 0.10.x (0.10.2) — the first run on this branch's own adapter, all three families live, 190 KiB prompt (the size that would have dropped every external voice before 0.8.0). 21 findings, 13 applied. The critical one closed the loop on 0.10.0: TIMEOUT_MARGIN_S was a hand-derived literal restating the adapter's probe constants in a comment, and 0.10.0's third bounded probe pushed the worst case to 3 × 23 = 69 s against a 60 s margin — the outer window would win the race again and destroy the rc=124 + telemetry evidence. agents.sh config reports probe_budget_seconds now, the skill passes it, the workflow derives its margin from it, and the --help probe is memoized. Also: rc=137 counts as a timeout everywhere (-k SIGKILLs a SIGTERM-ignoring backend — which grok is documented as being — and every consumer keyed on 124 alone); a malformed SWARM_TIMEOUT no longer exits 2 from --help/jail/list; the oversize message quotes the cap that actually fired instead of a hard-coded 512 KiB.

What three self-reviews found

The branch reviewed itself three times (/swarm:review --fix). Round 1 found real defects in the feature code — the skill's oversize guard read SWARM_MAX_PROMPT_BYTES without the adapter's validation, so a malformed value made the threshold negative and dropped every external voice silently; the grok capability probe ran grok --help unbounded outside with_timeout.

Rounds 2 and 3 then found only defects introduced by the previous round's fixes, and always in the same shape: a value two places must agree on, corrected in one of them.

Round Critical Shape
1 cap unvalidated in the skill one side validates, the other does not
2 cap decimal-forced in the skill, not the adapter one side normalizes, the other does not
3 timeout decimal-forced in the skill, not the adapter the same bug, one knob over

Patching the reported instance never ended the class, so 0.10.0 removes the second parser rather than fixing it a fourth time. test_lens_sync.py guards the design directly — it requires the single parser and the config dispatch, and negatively asserts that no second parse reappears in the skill.

Readiness

  • ✅ Tests — 6 swarm test files pass (3 new on this branch: test_telemetry_report.py, test_grok_models.py, plus transport + coupling guards in the existing suites); check-structure.py clean
  • ✅ Changelog — entries for 0.8.0 → 0.10.2, each naming what it declined and why
  • ✅ Knowledge — swarm-backend-adapter.md + swarm-review-pipeline.md carry the measurement matrix, the rejected alternatives, the discovery design and the one-parser rationale
  • ✅ README / CLAUDE.md / marketplace — cluster count, transport and version (0.10.x) in sync
  • ✅ Rebased onto main
  • ✅ Reviewed on its own code — the 0.10.x ensemble ran against this branch (35 agents, ~16 min); the 13 agreed findings are applied in 0.10.2, the 8 declined ones are listed in the changelog with reasons

Test plan

  • /swarm:review on a normal diff — three full rounds run on this branch; externals participate, Voices: timing section appears under the balance block
  • Oversize transport — verified on both paths: 164 KiB end-to-end through the adapter, and the skill-side prep block reporting EXTERNALS_OVERSIZE=0 at 190 KiB against the adapter-reported threshold (the old 118 784-byte constant would have skipped every external voice)
  • SWARM_MAX_PROMPT_BYTES=abcagents.sh config exits 2 with one message, so the skill stops instead of silently reducing the ensemble to Claude-only. Also verified: 0100000 resolves to 100000 (not octal 32768), a cap at/below the 4096-byte headroom is refused, and SWARM_PROBE_TIMEOUT=300 is refused against the 20 s ceiling
  • Force a backend timeout — SWARM_TIMEOUT=5 against grok returned grok timed out after 5s in 6.7 s wall (cap + probe overhead), adapter_rc=1, and the telemetry record {"backend_rc":124,"timed_out":true,"timeout_seconds":5,"model":"grok-4.6"} — i.e. the inner cap wins the race, the EXIT trap still fires, and discovery picked the newest verified model. The consensus-line wording downstream of this is workflow-side and unchanged by this branch

🤖 Generated with Claude Code

https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub

gering and others added 8 commits August 22, 2026 19:53
The prompt travelled as one argv word, so exec's MAX_ARG_STRLEN (128 KiB on
Linux) was the binding limit and forced a 120 KiB cap. Above it the skill set
EXTERNALS_OVERSIZE and dropped EVERY external voice — the same damage as a
backend timeout, from a limit that was never inherent to the CLIs.

Neither CLI needs the prompt on argv: codex reads it from stdin (`-- -`), grok
takes `--prompt-file`. The adapter now normalizes every input form to one file
and hands over the PATH, so the diff never enters a shell variable either.

- Cap now bounds model context, not exec: SWARM_MAX_PROMPT_BYTES (default
  512 KiB). Adapter and the skill's oversize guard read the same env knob with
  the same default, so an override reaches the externals instead of being
  short-circuited by a skip that never heard about it.
- Temp prompts are chmod 600 before content lands and removed by the EXIT trap
  on every path; a caller-owned --prompt-file is never mutated, so concurrent
  per-cluster voices sharing one prompt file cannot corrupt each other.
- grok's --prompt-file is preflighted (stubbable, so the argv tests stay
  hermetic) with an upgrade error — never a silent fallback to --single, which
  would reinstate the wall as a mystery failure on big diffs.
- test_sandbox_deny.py pins the transport itself (regressions are silent: only
  large diffs would start failing); test_lens_sync.py pins the two cap defaults
  together and keeps the 4 KiB --lens-instr headroom covered.

Verified end-to-end at 164 KiB through both backends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
The timeouts were long attributed to prompt size. Measured on one 42 KB diff,
one variable at a time, that is wrong:

  grok  high  breakage      374s   4 findings
  grok  low   breakage      161s   4 findings
  grok  high  consistency    28s   6 findings
  codex high  breakage      104s   2 findings

A 164 KiB control prompt returned in 20s (grok) / 8.6s (codex). The lens
CLUSTER dominates (13x), effort is secondary (2.3x, and bought zero extra
findings here), and backends differ 3.6x — grok is simply the slow voice on the
cluster whose briefs demand exploration (cross-file-trace reads neighboring
files). Size is ruled out.

Chunking the diff would therefore target the one variable measurement excludes;
splitting by LENS targets the one that dominates. Recorded in the knowledge
entry so the next round starts from data instead of the old assumption.

- agents.sh: `run --telemetry <file> --unit <name>` appends one JSON line per
  call (duration, effective effort/model, prompt bytes, backend rc, timed_out,
  and the wall the call actually ran under). Written from the EXIT trap, so a
  timeout is recorded too; the backend's own rc is captured before it is
  translated into the adapter exit code, or 124 and 1 would be indistinguishable.
- telemetry-report.py renders it under the balance block and flags any
  SURVIVING call at >=60% of its wall — the case backendErrors structurally
  cannot show, since a voice finishing at 550s and one at 20s are both "ok".
- Opt-in end to end: no --telemetry means the previous behaviour, byte for byte.
  Diagnostics never fail a review — a missing, truncated or malformed file
  degrades to less output, never a non-zero exit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
Proposed as a speed fix; the measurement corrected that, and the correction is
recorded rather than quietly dropped:

  old: one 3-lens breakage call   374s -> 4 findings, THREE of them cross-file-trace
  new: breakage (2 lenses)        313s -> 4 findings the combined call missed entirely
  new: reach (cross-file-trace)   126s -> 4 findings, ~the combined call's set

The real defect was LENS CROWD-OUT: one lens consumed the call while
correctness/removed-behavior barely reported. Split, the diff-local lenses found
four issues the combined call never surfaced. It is NOT a throughput win — the
longest call drops only 374->313s and total work rises to 439s, so no single
lens split clears the 600s wall. Effort remains the largest untried runtime
lever (374->161s for identical findings).

Two further effects, neither reachable by lowering effort: a timeout now costs
one lens instead of three (the family-critical case — grok is the only
third-family voice), and `reach` holds no MANDATORY lens, so the gate may prune
the whole call on a diff with no cross-file surface.

Fixes found by the new breakage voice reviewing this very branch:
- The skill's EXTERNALS_OVERSIZE guard read SWARM_MAX_PROMPT_BYTES without the
  adapter's validation. A malformed value expands to 0, the threshold goes
  negative, and EVERY external voice is dropped SILENTLY — while the adapter
  refuses the same value loudly. Now rejected symmetrically (SWARM_CFG_ERR),
  pinned by test_lens_sync.py in both directions.
- The grok --prompt-file capability probe ran `grok --help` unbounded, outside
  with_timeout: a wedged CLI would hang the review before any review work. Now
  capped by SWARM_PROBE_TIMEOUT with -k, degrading to "assume supported" where
  no timeout binary exists rather than hanging or refusing.
- A header comment still promised a --single fallback the preflight replaced
  with a hard error.

Also measured and REJECTED: `grok --max-turns` (10 -> 10s but ZERO findings;
20 -> 279s, 4). The truncated run exits rc=0 with empty findings, so the
pipeline reads a silenced voice as "reviewed cleanly, found nothing" — worse
than a timeout, which at least reaches backendErrors. Documented in the
knowledge entry so it is not retried blind.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
Two leftovers from the timeout task, both about making an unavoidable failure
legible rather than preventing it.

Timeout passthrough. The adapter's cap and the Bash window the transport agent
runs under both defaulted to 600s, so which fired first was undefined — and when
the outer one won, the run lost rc=124, the "timed out after Ns" message and the
telemetry timeout flag, leaving a bare killed command. That lost diagnosis is
why raising SWARM_TIMEOUT looked counterproductive. It now travels skill →
workflow, which pins the adapter cap a margin below the Bash window so it always
wins the race, and says so when a requested value exceeds what one Bash call can
hold. SWARM_TIMEOUT=0 is passed through, with a log line that the outer window
still kills at 600s and will report generically. This does NOT raise the ceiling
— only async transport can (tasks/async-poll-external-voices.md).

Family coverage. Consensus is defined as ">=2 agreeing families", so losing one
silently changes what every CONSENSUS and every solo MEANS: a finding that would
have been corroborated is routed through the adversarial verifier instead. The
counts look identical to a healthy run — that silent degradation is the reason
this task existed. balance now carries familiesExpected/Present/Lost and
consensusReachable, and the report prints a warning directly under Bilanz:,
including the case where fewer than two families survived and NO finding can
reach consensus. Presence is per family, not per call: one dead cluster beside a
live one is not a lost family (that stays a backendErrors entry).

Both pinned by test_lens_sync.py, including that the Bash window is derived
rather than a second hard-coded literal — the tie is the bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
grok had been absent from EVERY review since CLI 1.0.3 — silently. That release
changed the `grok models` bullet marker so only the DEFAULT keeps `*`:

    * grok-4.6 (default)
    - grok-4.5          <- the pinned model, invisible to a `*`-only matcher

So readiness reported "this CLI does not offer grok-4.5" for a CLI that offers
it, and the sole third model family dropped out of the ensemble. No timeout, no
error — just two families where the report claimed three. Same damage as the
600s wall this branch is about, reached by a different route.

Rather than re-pin to 4.6 and wait for the next break, the model is now
DISCOVERED. Ported from ~/dotfiles' cc-harness-agents (same provider), with one
gate substituted: that helper withholds an upgrade until a model's context
window is known; the adapter withholds it until --json-schema ENFORCEMENT is
known, because a model that merely accepts the flag returns
structuredOutput:null and fails after a full review is paid for.

- GROK_CANONICAL_RE accepts only bare version ids, major >= 4 — rejecting dated
  snapshots, reasoning/non-reasoning splits, multi-agent, build, composer and
  image/video variants. Major >= 4 keeps a catalog regressing to grok-3* from
  pulling the ensemble backwards.
- Ordering is component-wise: grok-4.20 beats grok-4.6. As a decimal fraction it
  would lose, but the provider means the 20th minor release and already ships
  4.20-derived ids.
- GROK_SCHEMA_VERIFIED is the hard gate AND the upgrade ritual: a newer
  canonical model is named on stderr, never selected. Verified on CLI 1.0.3 that
  grok-4.5 and grok-4.6 both return an envelope whose .structuredOutput carries
  the schema's findings; reviews now run on grok-4.6.
- Readiness and the run_grok preflight moved from "is THIS id listed/requested"
  to "is a schema-verified model on offer/requested" — the exact-id form is what
  let the marker change drop the backend. An explicit --model still bypasses
  discovery, never the schema gate.

test_grok_models.py pins both listing formats against the SHIPPED awk program
(extracted, never re-typed) plus the ordering, the filter against the live
14-id catalog, the verified gate, and that the fallback pin is itself verified.
Two self-inflicted test bugs found and fixed while writing it: an extraction
regex that silently matched nothing (every assertion then passing over empty
output), and a verdict block left mid-file so the discovery half's failures
were recorded but never read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
A full /swarm:review of this branch: 23 findings across 3 model families and 11
lenses, 17 agreed, 5 partial, 1 declined. The two critical ones were introduced
by the preceding commits in this very PR.

Degraded-discovery fallback pointed at the NEWEST verified model. That value is
reached ONLY when the model list could not be read — precisely when least is
known about the host — so a CLI too old to offer grok-4.6 got an unknown model
id, rejected every call, and lost the whole grok family for the run. The exact
silent-family-loss this branch exists to prevent, reintroduced by its own model
bump. It is now the OLDEST verified id; discovery still selects grok-4.6
whenever the list is readable, and the test pins the invariant (oldest verified)
rather than the literal value.

--unit / --telemetry were interpolated into the transport command unquoted while
the neighbouring --lens-instr was shell-quoted. Both go through shQuote now.

Also fixed:
- The `grok --help` probe read a timed-out probe (rc 124/137, empty stdout) as
  "flag absent", telling users to upgrade an already-current CLI while the voice
  died. It degrades like the sibling model probe now. ready_hint blamed the CLI
  for what is often a missing GROK_SCHEMA_VERIFIED entry; it names the right
  remedy per case.
- Timeout margin 30s -> 60s: it did not cover the adapter's own bounded probes,
  so the outer Bash window could still win the race. The default no longer
  requests a value it will always cap, so the "exceeds what one Bash call can
  hold" warning stops firing on every default run.
- Telemetry string fields are JSON-escaped (a malformed record is silently
  SKIPPED by the reader — it reads as "that voice never ran"), and the report
  marks calls that died in the adapter before the backend ran, previously
  indistinguishable from a fast success.
- The consensus-unreachable warning no longer hides inside the families-lost
  branch: a run that only ever had one family loses nothing yet still cannot
  form consensus, and printed nothing at all.
- SWARM_MAX_PROMPT_BYTES normalized with 10# (leading zero was read as octal);
  the stdin path bounds what it writes to disk instead of measuring after; the
  report labels grok by family, not by an id it cannot keep current.
- Docs swept for discovery + the 5-cluster split. Activation surfaces
  (plugin.json, marketplace.json, CLAUDE.md) are model-name-free — they load in
  every session and a version there goes stale by itself.
- Test hygiene the review caught in the tests I added last round: a shadowed
  name, mid-file imports, an unreachable check(), and fixtures that never got
  cleaned up.

Declined: replacing the hand-rolled version ordering with `sort -V`. It works on
this host, but the ordering deliberately mirrors the cc-harness-agents
implementation that ranks the same ids; two implementations free to diverge cost
more than the 40 lines saved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
17 findings, 3 model families, no backend errors. Both criticals were HALF-DONE
fixes from 0.9.3 — the same mistake twice, each time in one half of a pair:

SWARM_MAX_PROMPT_BYTES was decimal-forced in the skill but not in the adapter.
The two sides of one shared gate then read the same value differently: 0100000
is 100000 in the skill (lets every voice through) and octal 32768 in the adapter
(rejects each one), converting the single deterministic skip into exactly the
per-call error storm that guard exists to prevent. 080000 additionally died in
arithmetic under set -e instead of erroring cleanly. Both sides normalize now,
and the test requires it of BOTH rather than of one.

The transport command quoted only the arguments 0.9.3 added. ADAPTER and
EXTERNAL_PROMPT sat in plain double quotes on the same line — which do not stop
$(...), backticks or ${...} from expanding in the shell that runs the string —
immediately under the comment explaining why TMPDIR-derived paths must be
quoted. Every interpolated path goes through shQuote now.

Also:
- The empty-prompt guard had weakened from "no visible content" to "zero bytes"
  when 0.8.0 moved the prompt out of a shell variable, so a whitespace-only
  prompt would spend a full backend call. Checks for non-whitespace again.
- SWARM_TIMEOUT reaches the workflow only when the user actually set it —
  hardcoding 600 meant the derived default was never used and the "exceeds what
  one Bash call can hold" warning still fired on every stock run, which 0.9.3
  claimed to have stopped. It is decimal-forced too: the value lands in a bare
  JS numeric literal where 0600 is legacy octal 384.
- Telemetry: _json_escape now covers every control character (JSON forbids all
  of them unescaped, and the reader DROPS a malformed record — which reads as
  "that voice never ran"); timeout_seconds:0 means cap-disabled, not
  field-missing, so an uncapped call stops being scored against a wall that
  never applied; a non-numeric field can no longer crash a report whose whole
  contract is to never fail a review.
- One _bounded_probe primitive replaces a timeout prelude that existed three
  times and had already drifted: one copy checked rc before trusting output,
  another swallowed it and read empty output as a definite "flag absent".
- Smaller: no empty "(ausgefallen: )" in the case that warning was written for;
  _grok_version_newer refuses leading-zero components instead of aborting;
  ready_hint forks once; the prep block reads the prompt size once; the agents
  skill and pipeline-blueprint caught up with discovery and the 5-cluster split.

Declined: folding GROK_CANONICAL_RE into the verified table (the filter must
also see UNVERIFIED newer models — that is the entire upgrade signal), and
caching grok probes across adapter processes (staleness in a path whose probes
are already bounded).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
Third review round found its criticals in the previous round's fixes
again — same shape each time: a value two places must agree on, fixed in
one of them. Answer the class, not the instance.

- Add `agents.sh config`: the resolved, validated numeric configuration
  as key=value lines. The skill reads it instead of re-parsing SWARM_*.
- Add `_resolve_int` as the one parser (digits-only, 10# decimal, range
  check after conversion, wrap guard, explicit upper bound).
- Enforce the timeout with `-k`: SIGTERM alone let a backend outlive the
  cap, so the outer window killed the adapter and no telemetry survived.
- Cap SWARM_PROBE_TIMEOUT at 20s and document the coupling to the
  workflow's TIMEOUT_MARGIN_S.
- Move grok's --prompt-file probe into readiness; validate a --model
  override against the offered list.
- Use 126, not 127, as _bounded_probe's "cannot bound" sentinel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
@gering
gering force-pushed the task/fix-swarm-timeout-ceiling branch from 77ad813 to 1bbd385 Compare August 22, 2026 17:54
@gering gering changed the title Fix swarm transport walls: out-of-band prompt, telemetry, lens split Swarm 0.10.0: out-of-band transport, telemetry, model discovery, one config parser Aug 22, 2026
gering and others added 2 commits August 22, 2026 22:32
`cat <<HDR` must stay unquoted so $NONCE and $CAP_RULES expand, but that
also evaluates backticks in the body. The rule naming the allowed finding
prefixes carried markdown backticks around `[lens]`, so bash ran it: every
review since 0.7.0 printed "[lens]: command not found" and shipped a
prompt whose rule had lost the tag it was describing.

Found by running the prep block end-to-end rather than reading it.
test_lens_sync.py now rejects any backtick or $( inside an unquoted
heredoc there, and was verified to fail on the reintroduced bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
First review run on 0.10.x, all three families live, 190 KiB prompt.
13 agreed findings applied.

- agents.sh config reports probe_budget_seconds; the skill passes it and
  the workflow builds TIMEOUT_MARGIN_S from it. The old literal restated
  the adapter's constants in a comment, and 0.10.0's third bounded probe
  pushed the worst case to 69s against a 60s margin — the outer window
  would win the race and destroy the rc=124 + telemetry evidence.
- Memoize the --help capability probe (at most two probes per run).
- Treat rc 137 as a timeout everywhere via _is_timeout_rc: -k SIGKILLs a
  SIGTERM-ignoring backend, and every consumer keyed on 124 alone.
- Resolve SWARM_TIMEOUT/SWARM_PROBE_TIMEOUT lazily so --help/jail/list
  survive a malformed knob; run resolves in the main shell so the wall
  reaches both the message and the telemetry record.
- Quote the cap that actually fired, not a hard-coded 512 KiB.
- Drop the empty require_valid_timeout stub; fix stale 126/127 comments,
  the third grok remedy, the degrade warning, a duplicated test, the
  telemetry-report mode bit, and a wrong claim in the 0.9.4 entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKreruna9RfZHcEq6YPub
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant