Skip to content

feat(pool): --pool-strategy=fill-first (primary/backup seat semantics)#780

Merged
askalf merged 1 commit into
masterfrom
feat/pool-strategy
Jul 17, 2026
Merged

feat(pool): --pool-strategy=fill-first (primary/backup seat semantics)#780
askalf merged 1 commit into
masterfrom
feat/pool-strategy

Conversation

@askalf

@askalf askalf commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What

--pool-strategy=fill-first (env DARIO_POOL_STRATEGY, config pool.strategy, TUI Config tab) — an opt-in alternative to headroom spreading for where unbound (new) conversations land in a multi-account pool. Default behavior is untouched.

Fill-first concentrates new conversations on the alphabetically-first eligible seat until its headroom drains to the 2% floor (POOL_HEADROOM_FLOOR), then spills to the next alias in line. selectExcluding follows the same order, so a 429 failover tries the next alias rather than the max-headroom seat — one failover doesn't defeat the concentration.

Why

Two setups headroom spreading can't serve:

  • Primary/backup seats. A z-backup account keeps fully fresh 5h/7d windows until a-main is actually drained. Spreading nibbles at both from the first request.
  • Cache concentration. Fresh conversations land where prompt-cache pressure already is; the spill seat stays cold until needed.

Decisions worth reviewing

  • Alias order, not insertion order. Accounts load from a readdir whose order the OS doesn't guarantee. Alias naming (1-main, 2-overflow) is a knob the operator actually holds. Documented in multi-account-pool.md.
  • Sticky bindings are strategy-agnostic. selectSticky re-validates and returns an existing binding before any strategy code runs; strategy only picks where a NEW key binds. Tested for both modes.
  • All-seats-at-floor falls back to max-headroom (the terminal state both strategies share), so select() never returns null just because the strategy ran dry.
  • Validation split: invalid --pool-strategy flag fails startup with a clear error; invalid env/config values fall through to the default, matching resolveSessionRotationConfig's leniency.

Tests

test/pool-strategy.mjs — 20 checks: resolver precedence, default-behavior preservation, concentration, floor spill + recovery, ineligible-seat skipping, per-model 7d bucket interaction, failover order in both modes, sticky-binding precedence in both modes. Full suite green locally.

…s on the first seat

headroom spreading stays the default. fill-first lands every unbound
conversation on the alphabetically-first eligible seat until its headroom
drains to the 2% floor, then spills to the next alias — primary/backup
semantics (a z-backup seat keeps fully fresh windows until a-main is
actually drained) and cache concentration. Failover follows fill order
too, so one 429 doesn't defeat the concentration.

Alias order, not insertion order: accounts load from a readdir whose
order the OS doesn't guarantee; alias naming (1-main, 2-overflow) is a
knob the operator actually holds. Sticky bindings behave identically in
both modes — strategy only decides where NEW conversations land.

Sourced from --pool-strategy / DARIO_POOL_STRATEGY / config pool.strategy,
editable in the TUI Config tab (string enum). Invalid flag values fail
startup; invalid env/config values fall through to the default, matching
resolveSessionRotationConfig's leniency.
@github-actions

Copy link
Copy Markdown
Contributor

Wire drift: ✅ CLEAN

Ran node scripts/check-wire-drift.mjs against the runner's claude for commit 3490e84482412d7e3d1852575aa27f43238a0f3f — per-model anthropic-beta (betaForModel) + billing-block cch gate vs the installed claude.

Report
{
  "drift": false,
  "checkedAt": "2026-07-17T00:17:42.143Z",
  "ccVersion": "2.1.211",
  "captured": {
    "claude-opus-4-8": {
      "beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24",
      "cchEmitted": false,
      "billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
    },
    "claude-sonnet-5": {
      "beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24",
      "cchEmitted": false,
      "billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
    },
    "claude-haiku-4-5": {
      "beta": "interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,claude-code-20250219,advisor-tool-2026-03-01",
      "cchEmitted": false,
      "billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
    },
    "claude-fable-5": {
      "beta": "claude-code-20250219,interleaved-thinking-2025-05-14,thinking-token-count-2026-05-13,context-management-2025-06-27,prompt-caching-scope-2026-01-05,mid-conversation-system-2026-04-07,advisor-tool-2026-03-01,effort-2025-11-24,fallback-credit-2026-06-01",
      "cchEmitted": false,
      "billing": "x-anthropic-billing-header: cc_version=2.1.211.678; cc_entrypoint=sdk-cli;"
    }
  },
  "findings": []
}

Full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Compat test: ✅ PASSED

Ran node test/compat.mjs against dario proxy --passthrough on the self-hosted runner for commit 3490e84482412d7e3d1852575aa27f43238a0f3f.

Output
============================================================
  dario Compatibility Validation (--passthrough via dario)
  2026-07-17T00:17:59.375Z
============================================================

--- Anthropic Messages API (Hermes) ---
✅ #1 Anthropic non-stream: "COMPAT OK" | thinking_injected=false | service_tier=-
✅ #2 Anthropic stream: 7 events | order=correct | "STREAM COMPAT"
✅ #3 SSE framing: event:/data: pairs correctly paired

--- Passthrough Verification ---
✅ #4 No thinking injection: No thinking block in response (passthrough clean)
✅ #5 Client betas preserved: Client-requested thinking honored (status=200)

--- Tool Use (OpenClaw) ---
✅ #6 Tool use: tool=get_weather | input={"location":"Tokyo"}
✅ #7 Tool use stream: 12 events | tool_use=true | input_json_delta=true

--- OpenAI Compat ---
✅ #8 OpenAI non-stream: "OPENAI COMPAT"
✅ #9 OpenAI stream: 3 chunks | [DONE]=true | "**STREAM OPENAI**"

--- Header Visibility ---
✅ #10 Header visibility: request-id=true | ratelimit=true (12 headers)

============================================================
  RESULTS: 10 passed, 0 failed, 0 warnings
============================================================

Full workflow run

@sprayberry-reviewer sprayberry-reviewer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review from the Sprayberry Labs fleet code reviewer. This is a
bot-generated verdict, not a human sign-off.

Verdict: No blocking issues found — clean, well-tested addition.


What I checked

src/pool.tspickFillFirst algorithm
Traced through every ordering scenario: accounts arriving out of lexicographic order, the primary seat at the floor, all seats at the floor, ineligible seats mixed in. The two-line loop is correct: the best !== null && a.alias >= best.alias guard ensures best only updates toward a lexicographically-earlier alias, and the headroom check prevents updating with a drained account. Returns null only when every account in accounts is at/below the floor, matching the stated intent.

select() fill-first path (pool.ts:418–430)
When pickFillFirst returns null (all eligible accounts at the floor), control falls through to return pickMaxHeadroom(eligible, family) — identical terminal fallback to the headroom strategy. Verified in the "every seat at/below the floor" test and visually in the code.

selectExcluding() fill-first path (pool.ts:552–565)
Same structure: inner if (this.strategy === 'fill-first') { ...; if (first) return first; } exits cleanly when first is null, and the outer return pickMaxHeadroom(eligible, family) provides the same fallback. The excluded set is filtered before eligible is built, so the fill order correctly advances past the 429'd seat.

CLI precedence (cli.ts:461–463)
poolStrategyFromFlag ?? process.env['DARIO_POOL_STRATEGY'] ?? fileCfg.pool?.strategy — flag beats env beats config, as documented. The flag is validated at parse time (process.exit on invalid); env and config values flow into resolvePoolStrategy in proxy.ts, which silently falls through to 'headroom' on unrecognized strings, matching the codebase convention.

resolvePoolStrategy (pool.ts:152–162)
Iterates [explicit, env.DARIO_POOL_STRATEGY] in order, skips non-strings, lowercases and trims, returns on first valid value. Case and whitespace tolerance confirmed. The function receives explicit which may already carry the env value (merged by cli.ts) — the secondary env lookup is mildly redundant but not harmful; if the explicit value is valid it returns immediately.

sanitize() in config-file.ts
The config-file path only copies pool.strategy when it's exactly 'headroom' or 'fill-first'; any other value is silently ignored, producing undefined downstream. This is consistent with the design note in the PR.

TUI config tab (src/tui/tabs/config.ts)
pool.strategy added to both FIELDS and STRING_ENUMS, so TUI editing constrains input to the two valid values.

Tests (test/pool-strategy.mjs)
20 checks covering: resolver precedence, invalid-value fallback, case/whitespace, default headroom preservation, fill-first concentration and spill, floor recovery, ineligible-seat skipping, all-at-floor max-headroom fallback, per-model 7d bucket interaction, failover order in both modes, sticky-binding precedence in both modes. Comprehensive for a feature of this scope.


Minor (non-blocking)

selectExcluding has no test for the all-at-floor fallback in fill-first mode (the analogous select() case is covered). The code path is correct by inspection and the existing failover test provides partial coverage, but an extra check like check('selectExcluding falls back to max-headroom when all at floor', ...) would close the gap:

header('selectExcluding: fill-first all-at-floor falls back to max-headroom');
{
  const pool = new AccountPool('fill-first');
  addAccount(pool, 'a-main', { util5h: 0.99 });
  addAccount(pool, 'b-other', { util5h: 0.98 });
  // Exclude a-main — both remaining are at the floor
  const next = pool.selectExcluding(new Set(['a-main']));
  // Should return b-other (max-headroom among the drained eligible set)
  check('selectExcluding fill-first all-at-floor fallback', next?.alias === 'b-other');
}

What's good

The pickFillFirst loop is elegantly minimal — two conditions, correct for every ordering. The documentation trail (docstring, commands.md, multi-account-pool.md, help text) is thorough. The "invalid env/config values fall through to default" design is explicitly justified and consistent with resolveSessionRotationConfig. Test structure mirrors the feature description section-by-section.

@askalf
askalf merged commit 09d8eec into master Jul 17, 2026
13 checks passed
@askalf
askalf deleted the feat/pool-strategy branch July 17, 2026 00:30
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.

2 participants