Repository navigation
fix(gateway): one session queue, LRU eviction, telegram-only /start, awaited shutdown, webhook usage/502 - #174
Conversation
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest decision-model and Ollama research, add the decision-models concept page, note ollama.com cloud auth and stale defaults on the providers page, and link the fast lane from action-terminal mode. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Ingest a read of Hermes upstream at bed0d535 and record how it mixes models (fallback chain plus per-task auxiliary models), where embeddings live (memory plugins), and how its 14 community Jev plugins work. Add comparison rows and point decision models and plugin hooks at the revised #148/#149 scope. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
llama3.2 (3B) is weak at tool calling; qwen3:8b is the common local pick for agents. Switch the README, docs, persona example and the setup wizard default to qwen3:8b, keeping llama3.2 as the low-memory note. The Ollama provider already sends a Bearer header when api_key or api_key_env resolves, so Ollama cloud works today. Document it (LICH_BASE_URL=https://ollama.com, LICH_API_KEY_ENV=OLLAMA_API_KEY) and replace "unused by ollama" with "optional for ollama". Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…press Optional `models` block names provider chains per role. `chat` sets the main loop's failover order; `compress` routes context compression and falls back to the chat chain on failure. ProviderRouter.for_role shares built clients. Without `models`, behavior is unchanged. The TUI labels the first chat-role provider. Part of #149. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
… fallback Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…hook
Plugin entries may be { path, settings?, models? } beside a bare path.
Hooks and plugin tools receive the frozen settings and models.chat(role,
...), which refuses roles the entry was not granted. A new
before_llm_call hook may return a note that is capped, sent as a
trailing system message on that one main-loop call, and never saved to
history; throwing hooks fail open.
Part of #149.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…, deep-freeze loader settings Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…gin host features Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
A plugin-owned client for Ollama's local /v1/systemone endpoint asks one choice question per enemy action (and target) before each LLM turn. Shadow mode logs one JSONL line per decision; act mode queues orders through game_bridge's enemy_actions tool and meteor veto when every answer clears the threshold, else the LLM decides. Requests are checked against Ollama's limits before sending; failures fall back and are logged. bench.mjs replays saved snapshots for latency, coverage and agreement with the LLM. Part of #148. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old, keep log under .lich/game Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…ed; no tools on the last turn - A tools_enabled list (top-level or gateway) now applies to plugin tools, including the gatekeeper's git_commit. The commander persona lists its three game_bridge tools explicitly. - A before_tool_call hook that throws blocks the call (blocked_by_plugin: hook_error) instead of allowing it. - Tool calls on the max_turns turn are not executed; they are closed with a turn_budget_exhausted result so history stays valid. Part of #133. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…pts persist them Also document the last-turn skip in docs/architecture/agent-loop.md. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…st-turn tool skip) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
- CLI one-shot and chat pass an abort signal to agent.run. Ctrl+C aborts the one-shot run (exit 1) or the running chat turn (session kept); at the chat prompt it ends chat. A second Ctrl+C while cancelling exits 130. - TUI: Esc cancels a running turn; an aborted run shows "run cancelled". - write_lich_config update writes a temp file and renames it into place, keeping the existing mode, so readers never see a partial config. - gatekeeper, tools, run_tests, skills_search and kanban_audit tests remove the dirs they create under test/.tmp. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…old reply On abort the loop's `final` is the last assistant so far (often the previous turn's reply) and `messages` ends with the cancelled user line. Chat and the TUI now show only the cancel notice and keep history_after_abort(messages), which drops trailing user lines. The config temp file is created with the existing mode. Tests cover a second Ctrl+C (exit 130) and the chat turn after a cancel. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
reply_after_abort(outcome) returns `final` only when it comes after the last user line, i.e. this run wrote it. Chat prints it and the TUI shows it before the cancel notice; an earlier turn's reply is still skipped. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Roadmap map lists #133 (P0), #134 (P1), #117 (P2), #144, #148 and #149 with their wiki pages; the kanban skill names the phase children. Supersedes the stale #135. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…fig (#117) Phase 0 + 1 of #117. Discovery is now project .lich/config.json merged over ~/.lich/config.json (shallow, project wins per key). A project providers array replaces the global one and drops the global models unless the project sets its own. The global layer ignores work_dir and session_dir, and its relative plugin paths resolve against ~/.lich/. ~/.config/lich/config.json is read only when ~/.lich/config.json is absent, with a hint to move it; nothing writes there. --config still replaces the whole chain. Bare lich no longer pins an existing config as --config when it skips the wizard, so the merge applies there too. first_run tests use an empty HOME instead of mocking the home lookup. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…n is absent With work_dir = home the global file is the project file; the legacy ~/.config/lich file was then picked as the base under it. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…117) lich init --global writes the starter config to ~/.lich/config.json (never overwrites; no work_dir/session_dir). The setup wizard ends with "Save as the global default?" (default no). Yes writes the answers to ~/.lich/config.json; discovered .lich/plugins entries stay in the project file since they are project paths. The wizard no longer pins its written file as --config, so discovery merges project over global. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…s home With work_dir = home the project file is ~/.lich/config.json, so the plugins-only project write hit "already exists" and dropped plugins. Test also asserts the merged TUI plugins. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…json from home Other projects load that file as the global layer and resolve relative plugin paths against ~/.lich, so .lich/plugins/x would become ~/.lich/.lich/plugins/x. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Phase 2 of #117. ~/.lich/profiles/<name>.json, plus an optional <name>.md used as the system prompt, merges between the global and the project config (global < profile < project). Selection: --profile, then LICH_PROFILE, then a project `profile` key, then the default in ~/.lich/config.json. Profile plugin paths resolve against ~/.lich/profiles; work_dir/session_dir in a profile are ignored. New `lich profile list|show|create|use`: create runs the wizard into the profile file (never overwrites), use sets `profile` in the global file. A requested profile skips the first-run wizard; --profile with --config is an error. File tools now refuse .lich/profiles/. tui/chat/serve/gateway no longer replace every config error with "no model configured"; only that case gets the hint. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…t_dir/disk_usage - work_dir = home: ~/.lich/config.json is the project file, so the profile now merges over it instead of under it. - "profile not found" / "already exists" errors carry no absolute path. - list_dir and disk_usage check the root with assert_file_tool_access, and list_dir skips denied entries, so .lich/profiles is not listed. - Docs: LICH_PROFILE is ignored with --config. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…children The home-dir branch now applies only when ~/.lich/config.json exists, so a legacy ~/.config/lich file stays the base. disk_usage leaves out entries file tools may not read (.lich/profiles, .lich/config.json). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
New entities/lich-config.md: global < profile < project merge, writers, file-tool guard, and config profile vs runtime Profile. Guardrails page and runtime-profile-session updated; index providers line fixed (#165). SCHEMA gains the `config` tag. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
…awaited shutdown, webhook usage/502 (#144) Slice 1 of #144 (A1 plus the G-10 and webhook fixes): - GatewayBus queues conversations on SessionManager instead of its own promise chains (one queue implementation). - max_conversations evicts the least recently used conversation. - Only telegram's /start (/start, /start@bot, /start <payload>) becomes "hello"; /started and other platforms pass through. - Shutdown awaits adapter stop (bounded by 5 s) before exit. - bus.reply() returns { text, usage, failed }; the webhook returns the run's usage and 502 {"error"} on agent failure. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
Stale comment
PR #174 — Risk: HIGH (breaking webhook wire contract is an ambiguous public API, so one level up from MEDIUM; gateway session queue and shutdown. Auth, loopback bind, allowlists,
LICH_GATEWAY_TOKEN, andgateway.tools_enabledare unchanged. 12 files, +271/−71.)
Findings: Critical 0 / Warning 1 / Suggestion 2 (src/gateway/bus.ts:101enforce the cap on store and touch LRU at run start;src/gateway/runner.ts:134closeAllConnectionsso webhookstop()can finish;docs/user-guide/gateway.md:7mention HTTP 502)
Regressions of settled findings: none (first review)
Not reviewed: none (full three-dot diff, plus session queue, reply formatting, adapterstop, and the webhook token check)
Action: escalated to @Moikapy
Log: 2026-10-05 UTC, COMMENT, HIGH because the webhook success/failure shape is a breaking client contract and the gateway lifecycle changed; auto-merge left off. Also blocked by head commit authorclaude(not @Moikapy) andtypecheck_and_test(20) and (22) still in progress.needs-reviewadded.Sent by Cursor Automation: PR REVIEW
Concurrent new chats could each pass the pre-run eviction and all store, leaving the map over the cap. store_history now trims least recently used entries after each write. Adds a keep-alive stop regression test. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
There was a problem hiding this comment.
PR #174 — Risk: HIGH (breaking webhook client contract is an ambiguous public API, so one level up from MEDIUM; gateway session queue and shutdown. Auth, loopback bind, allowlists, LICH_GATEWAY_TOKEN, and gateway.tools_enabled are unchanged. 12 files, +334/−71.)
Findings: Critical 0 / Warning 0 / Suggestion 0
Regressions of settled findings: none
src/gateway/bus.tscap on store: fixed. Concurrent new chats trim tomax_conversationsand the just-stored key is kept.docs/user-guide/gateway.mdHTTP 502: fixed in the overview sentence.src/gateway/runner.tskeep-alive close: stated decision stands (enginesisnode >= 20; the new stop test passed on CI).
Not reviewed: none (delta b95516b5..53d81181; earlier commits reviewed at b95516b5)
Action: escalated to @Moikapy
Log: 2026-10-05 05:53 UTC, COMMENT, HIGH unchanged after the follow-up commit. Auto-merge left off. needs-review already set. typecheck_and_test (20) and (22), ossuary, wiki_lint, WIP, and AccessLint are green. Merge is also blocked because every commit author is claude. Auto-merge allows @Moikapy or a configured approved bot, and no approved-bot list was set.
Sent by Cursor Automation: PR REVIEW


Part of #144. This is the first PR for #144: section A's first item, plus the G-10 and webhook fixes. Persistence and per-chat profiles are deferred to a follow-up, as agreed.
Changes
GatewayBusnow queues each conversation on the sharedSessionManager(src/session/manager.ts) instead of its ownchainsmap of promise chains. Behavior is unchanged: one conversation runs in order, and different conversations run concurrently.max_conversations(200), the least recently used conversation is evicted. Before, the oldest inserted one was. History is re-inserted on every store, so the Map's order tracks use./start: only Telegram's bot-start command (/start,/start@bot,/start <payload>) is rewritten to "hello". Before, any message on any platform that started with/start, including/started ..., was replaced.stop(), capped at 5 s so a hung platform socket can't block exit, before releasing the bus and agent and exiting. Before,stop()calls were fire-and-forget, followed by an immediateprocess.exit. This lives in the new exportedshutdown_gateway.bus.reply()returns{ text, usage, failed }.POST /messagenow returns the run's tokenusageinstead ofnull.502 {"error":"agent error: ..."}instead of200with the error inreply.InboundHandleraccepts either a string or the reply object, andreply_text()normalizes it.gateway.md,godot.md,getting-started.md, the persona_orchestrator README and the CHANGELOG are updated for the new webhook shape and the eviction and/startbehavior.Tests
gateway.test.ts:/start: the bare,@botand payload forms are rewritten;/startedand webhook/startare not.reply()returns usage and a failure flag.200with real usage, and502with the error.gateway_shutdown.test.ts: adapters are awaited beforebus.stopandagent.close, including one adapter that throws, and the timeout path when an adapter hangs.chainsmap. They now checkSessionManager.pending_count()instead, with the same intent: settled queues are released (G-6), and an evicted conversation keeps its in-flight queue.Checks
tsc --noEmit: clean.serve_transport"accepts ::1 clients" (no IPv6) andtools_review_shouldfixS-6 (process-group zombies).vitepress build docs: passes.🤖 Generated with Claude Code
https://claude.ai/code/session_01LHQAfXHofy6QkAgmfmJava
Generated by Claude Code