feat(kanban,mcp): orphaned-card reconciliation + per-server MCP identity header

Two small config-gated features:

1. Kanban orphaned-card reconciliation (kanban.reconcile_orphans, default
   true, config.yaml): a running card with broken claim bookkeeping
   (claim_lock or claim_expires NULL — crash mid-claim, manual SQL, DB
   restore) is invisible to all existing recovery paths
   (release_stale_claims requires claim_expires NOT NULL,
   detect_crashed_workers requires host-local lock + pid,
   detect_stale_running is config-disabled by default) and shows Running
   forever. New reconcile_orphaned_running() pass in kanban_db.py runs
   each dispatch_once tick: requeues orphans to ready with an explanatory
   comment, closes any leaked run, emits a 'reconciled' event, and defers
   when the recorded PID is still alive on this host (never requeue
   beside a live worker). Surfaced via DispatchResult.reconciled_orphans.

2. Per-server MCP identity header (mcp_servers.<name>.identity_header,
   config.yaml): optional {name, value_from: static|profile, value}
   mapping; the header is attached to that server's HTTP/SSE transport
   requests. 'static' sends the config value; 'profile' resolves the
   active Hermes profile name once at connect time (no per-call
   mutation). Explicit per-server headers of the same name (any casing)
   win. Invalid blocks warn-and-ignore; stdio servers warn-and-ignore.

Tests: tests/gateway/test_kanban_reconcile_orphans.py (9),
tests/tools/test_mcp_identity_header.py (13), all written first (RED)
then implemented (GREEN). No new HERMES_* env vars.

Inspired by: openai/symphony tracker reconciliation (Apache-2.0) +
Poke per-user MCP identity (idea-level).
This commit is contained in:
Teknium
2026-08-07 08:05:55 -07:00
parent 5db1b72b1f
commit 9fad45fcda
8 changed files with 688 additions and 0 deletions
+7
View File
@@ -1111,6 +1111,12 @@ class GatewayKanbanWatchersMixin:
)
stale_timeout_seconds = 0
# kanban.reconcile_orphans (config.yaml, default true): each tick,
# requeue 'running' cards whose claim bookkeeping is broken (no
# valid claim, dead/gone worker) — the zombie-card reconciliation
# pass. Set false to keep orphans frozen for manual forensics.
reconcile_orphans = bool(kanban_cfg.get("reconcile_orphans", True))
# Read kanban.default_assignee — fallback profile for tasks
# created without an explicit assignee (e.g. via the dashboard).
# When set, the dispatcher applies it to unassigned ready tasks
@@ -1247,6 +1253,7 @@ class GatewayKanbanWatchersMixin:
stale_timeout_seconds=stale_timeout_seconds,
default_assignee=default_assignee,
max_in_progress_per_profile=max_in_progress_per_profile,
reconcile_orphans=reconcile_orphans,
)
except sqlite3.DatabaseError as exc:
if _is_corrupt_board_db_error(exc):