feat(kanban): add worker-lane registry for the dispatcher - #2
Open
nikitaBarkov wants to merge 1 commit into
Open
feat(kanban): add worker-lane registry for the dispatcher#2nikitaBarkov wants to merge 1 commit into
nikitaBarkov wants to merge 1 commit into
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
invalid-argument-type |
1 |
First entries
tests/hermes_cli/test_worker_lanes.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/hermes_cli/test_worker_lanes.py:89: [invalid-argument-type] invalid-argument-type: Argument is incorrect: Expected `(...) -> int | None`, found `Literal["not-callable"]`
✅ Fixed issues (1):
| Rule | Count |
|---|---|
invalid-assignment |
1 |
First entries
hermes_cli/kanban_db.py:7321: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `def profile_exists(name: str) -> bool`
Unchanged: 6036 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Let an out-of-tree integration register a custom spawn_fn for a non-Hermes assignee, so the kanban dispatcher can launch that runtime (e.g. a Codex / Claude Code CLI runner) directly instead of the default `hermes -p <profile>` worker. Paves the "external CLI worker lane" path the worker-lanes doc describes. - hermes_cli/worker_lanes.py: process-local WorkerLane registry (register / get / list / clear, name normalization, kanban_worker_env) with an optional per-lane max_concurrency. - hermes_cli/plugins.py: ctx.register_worker_lane(...) so a plugin can register a lane at load time. - hermes_cli/kanban_db.py: the dispatcher resolves the spawn_fn from the registry (_resolve_spawn_fn: an explicit arg wins, then a registered lane, then the default Hermes-profile spawn); a registered lane assignee counts as spawnable and honors the lane's max_concurrency cap. - tests/hermes_cli/test_worker_lanes.py: registry, dispatcher-resolution, and concurrency-cap coverage.
nikitaBarkov
force-pushed
the
nikita.barkov/workers
branch
from
July 3, 2026 08:08
303f344 to
3461018
Compare
nikitaBarkov
pushed a commit
that referenced
this pull request
Aug 3, 2026
… a broken chat A completely unconfigured install previously booted into a working-looking chat (banner showed model 'unknown'), accepted a message, spun ~30s, then failed with 'Set OPENROUTER_API_KEY' — a provider the user never chose — and never offered setup. - HermesCLI.run() now probes provider readiness at startup (TTY only) and offers the shared provider picker (hermes model flow, which fronts Quick Setup / Nous Portal OAuth) when nothing is configured. Decline is respected; picker state re-syncs into the live CLI so the next turn works without a restart. - New silent probe _runtime_credentials_ready(): no printing, no state mutation; handles keyless local endpoints and callable bearer providers. - The empty-api-key error is provider-aware: names the actual resolved provider and points at 'hermes model' / 'hermes setup' instead of hardcoding OPENROUTER_API_KEY. - Banner: unconfigured installs render 'no model configured — run /model' in red instead of the silent 'unknown' model slug. Consumer-onboarding audit finding #2 (sev 5), Aug 2026.
nikitaBarkov
pushed a commit
that referenced
this pull request
Aug 3, 2026
A wedged adapter transport (network hang, dead websocket) previously blocked _check_session_stalls forever: sibling candidates in the same pass were never evaluated and the watcher stopped ticking. Wrap the send in asyncio.wait_for (15s); on timeout log a WARNING and do NOT latch, so the next tick retries. Regression uses a never-resolving fake adapter and proves the pass completes, a healthy sibling candidate is still notified in the same pass, and the watcher ticks again (sabotage-verified against the unbounded send).
nikitaBarkov
pushed a commit
that referenced
this pull request
Aug 15, 2026
Addresses both review findings on the remote-gateway download PR: 1. Unbounded buffering (finding #1). fetchBuffer / fetchBufferViaOauthSession accumulated the entire response (then copied it again via Buffer.concat) before saveGatewayFile even opened the save dialog, so a large gateway file could exhaust the native process. Both auth paths now stream: once response headers arrive the connect timeout is cleared, the filename is derived, the save dialog is shown, and the body is piped to the chosen destination with backpressure. A read/write error tears down the stream and unlinks the partial file. The byte-moving, data-URL decoding, and filename/path helpers are extracted into gateway-file-download.ts so they're unit-testable without Electron. 2. No fallback for older gateways (finding #2). saveGatewayFile required the new /api/fs/download route. Desktop and the remote gateway update independently, so a gateway predating this PR 404s. Added a 404-only compatibility fallback to the existing capped /api/fs/read-data-url route (bounded, so it only serves smaller files — enough to keep older backends working). Tests: gateway-file-download.test.ts covers streaming, backpressure, error-cleanup (unlink on write/response error), data-URL decoding, filename derivation (incl. traversal reduction), and 404 detection; gateway-file-download-transport.test.ts asserts both transports stream (no whole-body Buffer.concat) and that the 404 fallback is wired. Both registered in the desktop platform test list. Server-side /api/fs/download tests (streaming + sensitive-file reject) already pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Alexander Prendota (AlexanderPrendota)
pushed a commit
that referenced
this pull request
Sep 2, 2026
Review P1 #2. _append_to_transcript_serialized() writes the compression continuation to child_id BEFORE publishing either _transcript_reroutes or the _entries update — that ordering is load-bearing for backlog order, so it must not move. At that moment nothing in the routing index points at the child, so _db_for_session_id(child_id) missed its scan and fell through to _db_for_key(None), i.e. the ambient store. The fail-closed guard did not fire because root is a live handle. The row therefore targeted root rather than the already-proven parent owner. With no child row there the append is rejected by the FOREIGN KEY constraint, the pending queue never drains and the reroute cannot advance; against a split-brain root the message would instead be written cross-profile. Record ownership before the mutation instead of moving the publication: a private _session_owner_hints map carries session_id -> owning key for ids whose owner is proven but not yet published, consulted by the new _owner_key_for_session_id() after the index scan misses, and dropped as soon as routing publishes. Signatures are unchanged, so the existing suites that stub _append_transcript_message keep working untouched; the map is read through getattr for stores built via object.__new__. The regression is physical rather than mocked: an ended compression parent and a live child that exist only in profiles/fitness/state.db, no active profile scope, append to the parent, then assert all four effects — the row lands on the child in the profile store, the pending queue drains, the reroute and the routing entry advance, and root state.db stays untouched. Without the hint it fails exactly as the review predicted, on "FOREIGN KEY constraint failed" against root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Alexander Prendota (AlexanderPrendota)
pushed a commit
that referenced
this pull request
Sep 2, 2026
Addresses teknium1's review (NousResearch#64195) finding #2: the multi-rung resolver needs Electron tests covering precedence, stale-PID rejection, fallback behavior, and the remote boot path. The pure decision helpers are now covered by 29 unit tests in `profile-migration.test.ts` (vitest electron project). Coverage: - precedence: legacy > single-running-gateway > state.db heuristic - stale-PID rejection: recycled PIDs not owned by hermes are dropped - malformed pid files: JSON parse errors, non-integer PIDs, zero/negative - scoring edge cases: ancient files (recency floored at 0.1), tiny files (size floored at MIN_SIZE), larger DB beats smaller at similar recency - single-profile fallback: best === 'default' suppresses the write - no-op cases: preference file already exists, missing profiles root The remote boot path is verified by code review of the call-site move (commit preceding this one) — `migrateActiveProfileIfMissing()` now runs before `primaryProfileKey()` is first read in `startHermes()`. The pure decision logic that the orchestrator relies on is covered end- to-end below; this matches the repo's testable-helper pattern (see `profile-delete-routing.test.ts`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add a worker-lane registry so an out-of-tree integration can register a custom
spawn_fnfor a non-Hermes assignee — letting the kanban dispatcher launch that runtime directly instead of the defaulthermes -p <profile>worker.What does this PR do?
Problem. The dispatcher's worker spawn is hardcoded to
hermes -p <assignee> chat(_default_spawn). There was no supported way for an out-of-tree integration (an external CLI runner) to be spawned for a task — the worker-lanes doc explicitly calls this "not yet a paved path." An assignee that isn't a Hermes profile just sits inreadyasskipped_nonspawnable.Solution. A process-local worker-lane registry. A plugin registers a lane at load time via
ctx.register_worker_lane(WorkerLane(name, spawn_fn, ...)); the dispatcher then consults the registry when it resolves a ready task's assignee:_resolve_spawn_fn): an explicitspawn_fn(tests / callers) wins, else a registered lane'sspawn_fn, else the default Hermes-profile spawn — so existing profile lanes are unchanged._assignee_is_spawnable/has_spawnable_ready/has_spawnable_review), so a lane task is dispatched rather than skipped or stranded.max_concurrencycaps in-flight workers for that lane (falls back to the global per-profile cap).kanban_worker_env(theHERMES_KANBAN_*env vars), so an external runner sees an identical task/workspace/run environment.The registry is process-local, so registration must happen in the process that runs the dispatcher (typically the gateway that owns kanban dispatch). The lane contract a spawned worker must satisfy (exactly one terminal kanban action, heartbeat, etc.) is documented in
website/docs/user-guide/features/kanban-worker-lanes.md.Related Issue
N/A — no tracking issue in this fork. Context: the worker-lanes doc references upstream NousResearch issue #19931 (external CLI worker lane) and the closed-unmerged Codex-specific PR #19924; this PR paves the generic mechanism those describe.
A more recent upstream attempt at the same feature is #29777 — its author OK'd a separate implementation (comment). That PR has grown large and drifted from
main, so this is a focused, independent take on the same idea.Type of Change
Changes Made
hermes_cli/worker_lanes.py(new): process-localWorkerLaneregistry —register_worker_lane/get_worker_lane/is_worker_lane_assignee/list_worker_lanes/clear_worker_lanes, lane-name normalization, andkanban_worker_env(the sharedHERMES_KANBAN_*worker contract).WorkerLane(name, spawn_fn, kind="", max_concurrency=None)with validation.hermes_cli/plugins.py:PluginContext.register_worker_lane(lane, *, replace=False)so a plugin registers a lane at load time.hermes_cli/kanban_db.py: dispatcher integration —_get_worker_lane,_resolve_spawn_fn(explicit → lane → default), a registered lane assignee treated as spawnable in_assignee_is_spawnable/has_spawnable_ready/has_spawnable_review, and per-lanemax_concurrencyhonored in the dispatch loop's effective concurrency cap. Default profile-spawn path unchanged.tests/hermes_cli/test_worker_lanes.py(new): registry (register / normalize / duplicate-reject / validation), spawn resolution precedence,kanban_worker_envcontract, spawnable-counting, dispatch routing, and per-lane concurrency cap (dry-run and real-run).How to Test
→ 14 passed.
Behavior check: from a plugin's
register(ctx), callctx.register_worker_lane(WorkerLane(name="my-runner", spawn_fn=my_spawn)); create a task withassignee="my-runner"; run the dispatcher. It resolvesmy_spawn(nothermes -p), passes theHERMES_KANBAN_*env, and — withmax_concurrencyset — caps that lane's in-flight workers. An unregistered lane assignee staysskipped_nonspawnable(not silently dropped).Checklist
Code
feat(kanban):)mainpytest tests/hermes_cli/test_worker_lanes.py -q) — 14 passed; please run the fullpytest tests/ -qbefore mergeDocumentation & Housekeeping
website/docs/user-guide/features/kanban-worker-lanes.md(existing) — no doc change needed in this PRAGENTS.mdunchanged) — or N/A