Skip to content

XERK-521: Per-host default runtime (TURMA_DEFAULT_RUNTIME) for unpinned ticket/session processing - #554

Merged
xerhab merged 4 commits into
mainfrom
XERK-521
Aug 29, 2026
Merged

XERK-521: Per-host default runtime (TURMA_DEFAULT_RUNTIME) for unpinned ticket/session processing#554
xerhab merged 4 commits into
mainfrom
XERK-521

Conversation

@xerhab

@xerhab xerhab commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

XERK-521 — Per-host default runtime (TURMA_DEFAULT_RUNTIME) for unpinned work

Give each host a DEFAULT runtime so auto-started/unpinned tickets and bare
+ New session spawns run on the right runtime without pinning every one.
Runtime-generic (claude/dsh/qwen). Per-host (env), resolved agent-side — the
alternative org/hub-level default was declined in the ticket.

Mechanism (single choke point)

  • TURMA_DEFAULT_RUNTIME in agent/native/turma-agent.env (enum {claude,dsh,qwen}, charset/enum-gated). UNSET → claude, so every current host is byte-for-byte unchanged.
  • Precedence resolved in ONE place (resolve_agent_type), so no route diverges: explicit agentType → TURMA_DEFAULT_RUNTIME → claude. The default rung applies only via apply_default=True, passed by the single fresh-spawn call; every rebuild/resume/migration path passes the STORED runtime with apply_default=False, so a resumed/migrated session keeps the runtime it already had (a blank pre-field record stays claude, never adopts the default).
  • Self-validating / fail-safe (default_runtime()): checked against THIS host's own capability (dsh_configured/qwen_configured) — a host that sets qwen but hasn't configured Qwen falls back to claude and SAYS so (log + the heartbeat's effective value). Never a broken launch.
  • Heartbeat reports the EFFECTIVE (post-fallback) value as top-level defaultRuntime; hub whitelists + coerces it (normalizeDefaultRuntime in normalizeRecord/HEARTBEAT_KNOWN_KEYS): absent → claude; a runtime whose fleet-wide kill switch is off is forced to claude for consistency with the zeroed capability block.
  • No hub dispatch capability-filter for the default path: an unpinned ticket carries no runtime, so findTicketHost routes to the most-available host and the CLAIMING host applies its own runnable default (unchanged). Explicit pins still filter + block.
  • Composer: the Runtime dropdown PRE-SELECTS the reported host default, and an explicit "Claude Code"/"Claude Code Local" pick SENDS agentType:"claude" on a non-claude-default host so the explicit pick beats the default (byte-for-byte unchanged on every claude-default host).

Files

agent/hub-agent.py (precedence + env read + capability self-check + defaultRuntime heartbeat), agent/native/turma-agent.env + .claude/rules/agent-native.md (env docs), turma/server.js (normalizeDefaultRuntime + known key), turma/public/sessions.html (pre-select + explicit-claude), android/PARITY.md + .claude/rules/turma-board.md (parity/gap notes), tests.

Acceptance

  • ✅ Host TURMA_DEFAULT_RUNTIME=qwen auto-starts an unpinned ticket → runs Qwen (agent resolves the effective default).
  • ✅ An explicit composer pick overrides the host default (dsh/qwen send their agentType; claude/local now send explicit agentType:"claude").
  • ✅ Host TURMA_DEFAULT_RUNTIME=qwen with Qwen not configured → falls back to claude, reports it, no broken launch.
  • ✅ Heartbeat defaultRuntime reflects the effective (post-fallback) value; unset → claude; older hub/clients degrade.
  • ✅ Bare + New session with the dropdown untouched uses the host default (and the dropdown pre-selects it).
  • ⚠️ KNOWN GAP (deferred): an explicit per-ticket CLAUDE pin does not yet override a non-claude host default. The board Runtime row treats {runtime:"claude"} as RELEASE (drops the pin), so a claude-pinned ticket carries no agentType and adopts the host default; dsh/qwen pins DO override (they store + forward their agentType). Closing it needs the Runtime picker to distinguish "Auto — host default" from a pinned "Claude Code" across board.js + vendored board.cjs + Board.kt + glasses (a UX/design change) — see the gap notes in agent-native.md / turma-board.md. Latent today: dsh/qwen ship behind their kill switches (DSH_ENABLED/QWEN_ENABLED = false), so no host has a non-claude effective default until GA.

Known limitation (by design)

Per-host is NONDETERMINISTIC in a MIXED org — an unpinned ticket runs whatever host frees a slot first defaults to. Determinism needs a per-ticket pin (XERK-515) or homogeneous hosts. Documented in the rules and accepted per the ticket.

Verification — QA (adversarial qa + qa-delta)

  • qa (pass 1): FAIL → found that an explicit "Claude Code" composer pick could not beat a non-claude host default (the wire couldn't distinguish "explicit claude" from "unpinned"). Fixed: the composer now sends explicit agentType:"claude" on a non-claude-default host. The second half QA reported (a claude ticket-PIN not overriding) is the documented KNOWN GAP above, deferred as a board picker redesign; QA agreed it does not block (latent behind the kill switches).
  • qa-delta (pass 2): PASS → drove the real composer with an independent driver: on a defaultRuntime:"qwen" host an explicit Claude pick now posts {"repo":…,"agentType":"claude"} and the agent resolves it to claude over the default; claude-default/defaultless hosts remain byte-for-byte unchanged; mutation-checked the new guard test. No regressions.
  • Suites: agent.tests.test_hub_agent 1852 OK (5 skipped), turma/tests/server.test.js 616/0, turma/tests/sessions.test.js 113/0.
  • Not runtime-verified (out of scope / latent): real dsh/qwen launch end-to-end (kill switches off, binaries not installed); Android SpawnDialog (no defaultRuntime support yet — PARITY.md P3); real-browser DOM/click (no chromium in env — exercised via the exported composer functions).

xerhab added 4 commits August 29, 2026 12:52
…ed work

Give each host a default runtime so auto-started/unpinned tickets and bare
'+ New session' spawns run on the right runtime without pinning every one.

- resolve_agent_type gains apply_default; the ONE fresh-spawn call passes it,
  resolving a blank agentType to default_runtime() (explicit pick/pin still wins,
  every rebuild/resume path keeps the stored runtime).
- default_runtime() reads TURMA_DEFAULT_RUNTIME, self-validates against this
  host's own capability (agent_type_configured) and falls back to claude + logs
  when set-but-unrunnable — never a broken launch. UNSET -> claude, unchanged.
- Heartbeat carries the EFFECTIVE default as top-level defaultRuntime; hub
  whitelists + coerces it (normalizeDefaultRuntime, kill-switch-consistent),
  absent -> claude.
- Composer pre-selects the reported host default in the Runtime dropdown so a
  bare spawn shows which runtime it will use.
- No hub dispatch capability-filter for the default path: the claiming host
  applies its own runnable default (findTicketHost unchanged).
- Docs: turma-agent.env + agent-native.md (incl. the mixed-org nondeterminism
  limitation); android/PARITY.md line for the composer pre-select.
- Tests: default_runtime/precedence (test_hub_agent.py), normalizeDefaultRuntime
  + heartbeat passthrough (server.test.js), composer pre-select (sessions.test.js).
…ost default

QA (FAIL) found the wire cannot distinguish 'explicit claude' from 'unpinned':
a composer 'Claude Code'/'Claude Code Local' pick omitted agentType, which now
resolves to the host default agent-side — so on a non-claude-default host an
explicit claude pick would run the default runtime, violating the precedence
contract (an explicit pick must win).

- sessions.html: a claude/local composer pick now SENDS agentType:'claude' when
  the host default is non-claude (byte-for-byte unchanged on every claude-default
  host). Test added.
- Documented the remaining KNOWN GAP: a per-ticket CLAUDE pin still does not
  override a non-claude host default (the board Runtime row treats {runtime:claude}
  as release), because closing it needs the picker to distinguish 'Auto — host
  default' from a pinned 'Claude Code' across board.js/board.cjs/Board.kt/glasses
  — a UX change deferred to a follow-up. Latent today (dsh/qwen kill switches off).
  Noted in agent-native.md and turma-board.md.
…for Android

qa-delta LOW: the composer behavior change (an explicit claude/local pick must
SEND agentType:claude on a non-claude-default host) was not reflected in the
android/PARITY.md P3 entry, so an Android implementer building only the
pre-select would reintroduce the defect. Added the load-bearing half to the P3
line.
# Conflicts:
#	.claude/rules/agent-native.md
#	.claude/rules/turma-board.md
@xerhab
xerhab merged commit 6d7f33a into main Aug 29, 2026
6 checks passed
@xerhab
xerhab deleted the XERK-521 branch August 29, 2026 17:56
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