---
summary: "Sub-kois: spawning isolated koi runs that announce results back to the requester chat"
read_when:
  - You want background/parallel work via the koi
  - You are changing sessions_spawn or sub-koi tool policy
title: "Sub-Kois"
---

# Sub-kois

Sub-kois are background koi runs spawned from an existing koi run. They run in their own session (`koi:<koiId>:subkoi:<uuid>`) and, when finished, **announce** their result back to the requester chat channel.

## Slash command

Use `/subkois` to inspect or control sub-koi runs for the **current session**:

- `/subkois list`
- `/subkois stop <id|#|all>`
- `/subkois log <id|#> [limit] [tools]`
- `/subkois info <id|#>`
- `/subkois send <id|#> <message>`

`/subkois info` shows run metadata (status, timestamps, session id, transcript path, cleanup).

Primary goals:

- Parallelize “research / long task / slow tool” work without blocking the main run.
- Keep sub-kois isolated by default (session separation + optional sandboxing).
- Keep the tool surface hard to misuse: sub-kois do **not** get session tools by default.
- Avoid nested fan-out: sub-kois cannot spawn sub-kois.

Cost note: each sub-koi has its **own** context and token usage. For heavy or repetitive
tasks, set a cheaper model for sub-kois and keep your main koi on a higher-quality model.
You can configure this via `kois.defaults.subkois.model` or per-koi overrides.

## Tool

Use `sessions_spawn`:

- Starts a sub-koi run (`deliver: false`, global lane: `subkoi`)
- Then runs an announce step and posts the announce reply to the requester chat channel
- Default model: inherits the caller unless you set `kois.defaults.subkois.model` (or per-koi `kois.list[].subkois.model`); an explicit `sessions_spawn.model` still wins.
- Default thinking: inherits the caller unless you set `kois.defaults.subkois.thinking` (or per-koi `kois.list[].subkois.thinking`); an explicit `sessions_spawn.thinking` still wins.

Tool params:

- `task` (required)
- `label?` (optional)
- `koiId?` (optional; spawn under another koi id if allowed)
- `model?` (optional; overrides the sub-koi model; invalid values are skipped and the sub-koi runs on the default model with a warning in the tool result)
- `thinking?` (optional; overrides thinking level for the sub-koi run)
- `runTimeoutSeconds?` (default `0`; when set, the sub-koi run is aborted after N seconds)
- `cleanup?` (`delete|keep`, default `keep`)

Allowlist:

- `kois.list[].subkois.allowKois`: list of koi ids that can be targeted via `koiId` (`["*"]` to allow any). Default: only the requester koi.

Discovery:

- Use `kois_list` to see which koi ids are currently allowed for `sessions_spawn`.

Auto-archive:

- Sub-koi sessions are automatically archived after `kois.defaults.subkois.archiveAfterMinutes` (default: 60).
- Archive uses `sessions.delete` and renames the transcript to `*.deleted.<timestamp>` (same folder).
- `cleanup: "delete"` archives immediately after announce (still keeps the transcript via rename).
- Auto-archive is best-effort; pending timers are lost if the gateway restarts.
- `runTimeoutSeconds` does **not** auto-archive; it only stops the run. The session remains until auto-archive.

## Authentication

Sub-koi auth is resolved by **koi id**, not by session type:

- The sub-koi session key is `koi:<koiId>:subkoi:<uuid>`.
- The auth store is loaded from that koi’s `koiDir`.
- The main koi’s auth profiles are merged in as a **fallback**; koi profiles override main profiles on conflicts.

Note: the merge is additive, so main profiles are always available as fallbacks. Fully isolated auth per koi is not supported yet.

## Announce

Sub-kois report back via an announce step:

- The announce step runs inside the sub-koi session (not the requester session).
- If the sub-koi replies exactly `ANNOUNCE_SKIP`, nothing is posted.
- Otherwise the announce reply is posted to the requester chat channel via a follow-up `koi` call (`deliver=true`).
- Announce replies preserve thread/topic routing when available (Slack threads, Telegram topics, Matrix threads).
- Announce messages are normalized to a stable template:
  - `Status:` derived from the run outcome (`success`, `error`, `timeout`, or `unknown`).
  - `Result:` the summary content from the announce step (or `(not available)` if missing).
  - `Notes:` error details and other useful context.
- `Status` is not inferred from model output; it comes from runtime outcome signals.

Announce payloads include a stats line at the end (even when wrapped):

- Runtime (e.g., `runtime 5m12s`)
- Token usage (input/output/total)
- Estimated cost when model pricing is configured (`models.providers.*.models[].cost`)
- `sessionKey`, `sessionId`, and transcript path (so the main koi can fetch history via `sessions_history` or inspect the file on disk)

## Tool Policy (sub-koi tools)

By default, sub-kois get **all tools except session tools**:

- `sessions_list`
- `sessions_history`
- `sessions_send`
- `sessions_spawn`

Override via config:

```json5
{
  kois: {
    defaults: {
      subkois: {
        maxConcurrent: 1,
      },
    },
  },
  tools: {
    subkois: {
      tools: {
        // deny wins
        deny: ["gateway", "cron"],
        // if allow is set, it becomes allow-only (deny still wins)
        // allow: ["read", "exec", "process"]
      },
    },
  },
}
```

## Concurrency

Sub-kois use a dedicated in-process queue lane:

- Lane name: `subkoi`
- Concurrency: `kois.defaults.subkois.maxConcurrent` (default `8`)

## Stopping

- Sending `/stop` in the requester chat aborts the requester session and stops any active sub-koi runs spawned from it.

## Limitations

- Sub-koi announce is **best-effort**. If the gateway restarts, pending “announce back” work is lost.
- Sub-kois still share the same gateway process resources; treat `maxConcurrent` as a safety valve.
- `sessions_spawn` is always non-blocking: it returns `{ status: "accepted", runId, childSessionKey }` immediately.
- Sub-koi context only injects `KOIS.md` + `TOOLS.md` (no `SOUL.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, or `BOOTSTRAP.md`).
