Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Workspaces can also define a system prompt via `workspaces.yaml` for workspace-s

#### Memory backend selection

Semantic memory extraction (the subprocess that proactively writes facts and episode summaries) routes per-user: each user's effective `default_backend` selects the reasoner, and the model comes from the project's `MODEL_REGISTRY` for that `(role, backend)` pair. Claude-effective users get the claude reasoner with the claude-registry model; codex-effective users get the codex reasoner with the codex-registry model; opencode-effective users get the opencode reasoner with the opencode-registry model (a `provider/model` string resolved at runtime by `opencode` against the operator's `opencode auth login` state). There is no per-deployment override for the memory reasoner or model; an operator who wants a different model edits the registry in `config.py`.
Semantic memory extraction (the subprocess that proactively writes facts and episode summaries) routes per-user: each user's effective backend selects the reasoner, and the model comes from the project's `MODEL_REGISTRY` for that `(role, backend)` pair. Claude-effective users get the claude reasoner with the claude-registry model; codex-effective users get the codex reasoner with the codex-registry model; opencode-effective users get the opencode reasoner with the opencode-registry model (a `provider/model` string resolved at runtime by `opencode` against the operator's `opencode auth login` state). There is no per-deployment override for the memory reasoner or model; an operator who wants a different model edits the registry in `config.py`.

OpenCode users get the same one-shot parity as claude and codex users: PR review, issue triage, memory extraction, episode generation, and behavioral eval all dispatch through `OpenCodeOneShotReasoner`, which spawns a fresh `opencode acp` JSON-RPC subprocess per call and denies any tool-permission request mid-stream. No fall-through to `claude --print` or `codex exec` for opencode users on any one-shot site.

Expand Down Expand Up @@ -216,9 +216,9 @@ Authorization, per-user model selection, per-user OS isolation, per-user GitHub
|---|---|---|---|
| `TELEGRAM_BOT_TOKEN` | Yes | | Bot token from BotFather |
| `DEFAULT_BACKEND` | No | `claude` | Global default backend: `claude`, `goose`, `codex`, or `opencode`. Per-user override goes in `users.yaml`. (The former `AGENT_BACKEND` name is still read for one release with a deprecation warning.) |
| `LLM_PROVIDER` | Non-claude | | Global provider for non-claude backends. Per-user override in `users.yaml`. |
| `DEFAULT_PROVIDER` | goose/opencode | | Global provider for the multi-provider backends (`goose`, `opencode`); `claude` and `codex` are single-provider and ignore it. Per-user override in `users.yaml`. (The former `LLM_PROVIDER` name is still read for one release with a deprecation warning.) |
| `DEFAULT_MODEL` | No | `sonnet` | Installation-wide default model. Per-user override in `users.yaml` `model`, or `/settings model`. |
| `AGENT_TIMEOUT_SECONDS` | No | `120` | Installation-wide default per-message timeout. Per-user override in `users.yaml` `timeout`, or `/settings timeout`. |
| `DEFAULT_TIMEOUT` | No | `120` | Installation-wide default per-message timeout. Per-user override in `users.yaml` `timeout`, or `/settings timeout`. (The former `AGENT_TIMEOUT_SECONDS` name is still read for one release with a deprecation warning.) |
| `CLAUDE_AUTOCOMPACT_PCT` | No | `80` | Context compression threshold %, Claude Code only. When usage hits this, Claude compresses history. Can only lower the default (~83%), not raise it. |
| `AGENT_MAX_SESSION_HOURS` | No | `0` | Maximum session age in hours before recycling the subprocess (0 = no limit). Applies to every backend. Recommended: 4-8 on memory-constrained machines. |
| `WORKSPACE_BASE` | No | | Installation-wide default workspace base directory. Per-user override in `users.yaml` `workspace_base`. |
Expand All @@ -240,7 +240,7 @@ Authorization, per-user model selection, per-user OS isolation, per-user GitHub
| `TOTP_LOCKOUT_MINUTES` | No | `15` | TOTP lockout duration in minutes |
| `FILE_RETENTION_DAYS` | No | `0` | Days to keep uploaded files before cleanup (0 to disable) |

There is no spending cap: every supported backend runs on subscription auth where per-token cost numbers are not real billing. Runaway prevention comes from the per-message timeout (`AGENT_TIMEOUT_SECONDS`) and session lifecycle limits (`AGENT_MAX_SESSION_HOURS`, `AGENT_IDLE_TIMEOUT`).
There is no spending cap: every supported backend runs on subscription auth where per-token cost numbers are not real billing. Runaway prevention comes from the per-message timeout (`DEFAULT_TIMEOUT`) and session lifecycle limits (`AGENT_MAX_SESSION_HOURS`, `AGENT_IDLE_TIMEOUT`).

## Running

Expand Down
14 changes: 6 additions & 8 deletions src/kai/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ def _get_user_models(pool: SubprocessPool, chat_id: int, config: Config) -> dict
# programming oversight, not user error. OpenCode is excluded
# alongside codex because its None return is intentional (full
# provider/model IDs are open-ended, not a missing registry
# entry), and the global llm_provider for opencode is usually
# entry), and the global provider for opencode is usually
# empty so the OPEN_ENDED_PROVIDERS check above would not catch
# it.
log.warning(
Expand Down Expand Up @@ -1152,7 +1152,7 @@ def _resolve(db_key: str, yaml_val: object, global_val: object, fmt: object) ->

# Timeout
yaml_timeout = user_config.timeout if user_config else None
timeout, timeout_src = _resolve("timeout", yaml_timeout, config.agent_timeout_seconds, lambda v: f"{int(v)}s")
timeout, timeout_src = _resolve("timeout", yaml_timeout, config.default_timeout, lambda v: f"{int(v)}s")

# Provider info - always show so users know their configuration.
# Uses the shared helper that checks the running instance first,
Expand Down Expand Up @@ -1188,7 +1188,7 @@ def _revert_instance_field(pool: SubprocessPool, chat_id: int, field: str, confi
# If the user's provider differs from the global provider, the
# global default_model may be invalid for their provider.
provider = instance.provider
effective_global = get_effective_provider(config.default_backend, config.llm_provider)
effective_global = get_effective_provider(config.default_backend, config.default_provider)
if provider == effective_global:
instance.model = config.default_model
else:
Expand All @@ -1204,7 +1204,7 @@ def _revert_instance_field(pool: SubprocessPool, chat_id: int, field: str, confi
fallback = config.default_model
instance.model = fallback
elif field == "timeout":
instance.timeout_seconds = user.timeout if user and user.timeout is not None else config.agent_timeout_seconds
instance.timeout_seconds = user.timeout if user and user.timeout is not None else config.default_timeout


async def _handle_settings_reset(
Expand Down Expand Up @@ -1891,7 +1891,7 @@ async def _show_workspace_config(
elif user_config and user_config.timeout is not None:
timeout, timeout_src = user_config.timeout, "users.yaml"
else:
timeout, timeout_src = config.agent_timeout_seconds, "global default"
timeout, timeout_src = config.default_timeout, "global default"
lines.append(f" Timeout: {timeout}s ({timeout_src})")
except (ValueError, TypeError):
lines.append(" Timeout: (corrupted - reset with /workspace config reset timeout)")
Expand Down Expand Up @@ -4499,9 +4499,7 @@ async def _ingest_memory() -> None:
# is mandatory here.
user_config = config.get_user_config(chat_id)
effective_backend = (
user_config.default_backend
if user_config and user_config.default_backend
else config.default_backend
user_config.backend if user_config and user_config.backend else config.default_backend
)
if config.memory_extraction_enabled and effective_backend in ONESHOT_REASONER_BACKENDS:
# Windowed PRIOR CONTEXT for the episode classifier
Expand Down
Loading
Loading