Every feature you've studied so far in this course — boot sequence, tool calls, memory, compaction — operates in a fundamentally reactive model: you type, Claude responds, the session ends. KAIROS is Anthropic's internal codename for the system that breaks that contract.
Under KAIROS, Claude Code becomes an always-on daemon. It boots once, holds a persistent session across restarts, schedules its own check-ins, sends you messages when you're not looking, and consolidates its own memory while you sleep. It's less of a CLI tool and more of a background employee.
KAIROS appears throughout the codebase as a Bun build-time feature flag: feature('KAIROS'). It is an Anthropic-internal flag — gated out of external builds entirely via dead-code elimination. All KAIROS code paths are guarded with positive ternaries so the bundler can constant-fold them to false and tree-shake the modules.
The Feature Flag System
KAIROS is not a single switch. The codebase reveals a family of related flags, each shipping a specific slice of the always-on experience independently:
| Flag | What it unlocks | Scope |
|---|---|---|
| KAIROS | Full assistant mode: assistant command, session continuity (--session-id, --continue), SendUserFileTool, PushNotificationTool, assistant settings key, daily-log memory model, workerType: 'claude_code_assistant' in bridge |
ant-only |
| KAIROS_BRIEF | Ships BriefTool (SendUserMessage) independently of full KAIROS. Lets the chat-view / --brief experience reach external users. |
external |
| KAIROS_PUSH_NOTIFICATION | Ships PushNotificationTool independently of full KAIROS. |
external |
| KAIROS_GITHUB_WEBHOOKS | Ships SubscribePRTool — GitHub webhook subscription for PR events. |
ant-only |
| KAIROS_CHANNELS | MCP channel notifications — lets MCP servers push inbound messages into the conversation. The channelsEnabled settings key and allowedChannelPlugins policy. |
external |
| PROACTIVE | Earlier, lighter proactive mode. Many KAIROS paths are guarded feature('PROACTIVE') || feature('KAIROS') — KAIROS is a strict superset. Includes SleepTool and the proactive section of the system prompt. |
ant-only |
| AGENT_TRIGGERS | Cron scheduling system (CronCreate, CronDelete, CronList, .claude/scheduled_tasks.json). Independently shippable — has zero imports into src/assistant/. |
gb-gated |
ScheduleCronTool/prompt.ts spells this out: "AGENT_TRIGGERS is independently shippable from KAIROS — the cron module graph has zero imports into src/assistant/ and no feature('KAIROS') calls."
kairosActiveEverything in KAIROS mode pivots on a single boolean in the global state store. In bootstrap/state.ts:
// bootstrap/state.ts (line 1085)
export function getKairosActive(): boolean {
return STATE.kairosActive // default: false
}
export function setKairosActive(value: boolean): void {
STATE.kairosActive = value
}
This boolean is the runtime "are we in assistant mode right now?" flag. It is set from main.tsx during boot, before any tool availability checks run. Its effects cascade everywhere:
Daily-Log Mode
When kairosActive, loadMemoryPrompt() switches from the standard MEMORY.md reader to buildAssistantDailyLogPrompt() — append-only daily logs instead of a shared index file.
Opt-In Bypass
isBriefEnabled() returns true for kairosActive sessions without requiring explicit user opt-in. The system prompt hard-codes "you MUST use SendUserMessage."
SDK Restriction Lifted
Fast mode (Opus 4.6) is blocked in non-interactive SDK sessions unless getKairosActive() is true. Assistant daemon mode is exempt from the third-party preference check.
Dream Gated Off
The background memory consolidation agent (isGateOpen()) explicitly returns false when kairosActive — assistant mode uses its own disk-skill dream pipeline instead.
Worker Type Change
When isAssistantMode() (which reads kairosActive), the bridge registers the session as workerType: 'claude_code_assistant' — visible as a distinct type in the web UI session picker.
Auto-Enable
The cron scheduler's assistantMode flag bypasses the normal isLoading gate and setScheduledTasksEnabled() handshake — tasks in scheduled_tasks.json start firing immediately at boot.
The core of proactive / KAIROS mode is a heartbeat mechanism called the tick. When running autonomously, the model periodically receives a <tengu_tick> XML message. Think of it as a gentle nudge: "you're awake, what now?"
From the system prompt in constants/prompts.ts (getProactiveSection()):
// The exact text the model receives in its system prompt when proactive is active:
"You are running autonomously. You will receive <tengu_tick> prompts that keep you
alive between turns — just treat them as 'you're awake, what now?' The time in each
<tengu_tick> is the user's current local time."
"Multiple ticks may be batched into a single message. This is normal — just process
the latest one. Never echo or repeat tick content in your response."
"**If you have nothing useful to do on a tick, you MUST call Sleep.** Never respond
with only a status message like 'still waiting' — that wastes a turn and burns tokens."
The SleepTool: Cost-Aware Idling
The Sleep tool is loaded only when feature('PROACTIVE') || feature('KAIROS'). Its entire job is to let the model yield CPU without burning an API call per idle second:
// tools/SleepTool/prompt.ts
export const SLEEP_TOOL_PROMPT = `Wait for a specified duration.
The user can interrupt the sleep at any time.
Use this when you have nothing to do, or when you're waiting for something.
You may receive <tengu_tick> prompts — look for useful work before sleeping.
Each wake-up costs an API call, but the prompt cache expires after 5 minutes
of inactivity — balance accordingly.`
Three key design points baked into the prompt:
- The model chooses its sleep duration — longer when waiting for slow processes, shorter when iterating
- Sleeping is explicitly cheaper than calling
Bash(sleep ...)— no shell process held - The 5-minute prompt cache expiry is a hard cost floor — waking more often than that wastes cache creation tokens
Sleep Interruption: Priority Queues
When a user sends a message mid-sleep, the QueuePriority system in types/textInputTypes.ts handles the wakeup:
// types/textInputTypes.ts
type QueuePriority = 'now' | 'next' | 'later'
// 'now' — interrupt current tool call immediately (Esc + send)
// 'next' — wait for current tool to finish, then inject between tool result and next API call
// Wakes an in-progress SleepTool call.
// 'later'— end-of-turn drain. Also wakes SleepTool.
sleep_progress) is listed in EPHEMERAL_PROGRESS_TYPES alongside bash/powershell/MCP progress — it's stripped from the stored transcript just like shell spinner output. The tick loop generates a lot of noise that would pollute context if kept.
Terminal Focus Awareness
The proactive system prompt gives the model explicit guidance on whether the user's terminal is focused. This changes how autonomous it should be:
"**Unfocused**: The user is away. Lean heavily into autonomous action —
make decisions, explore, commit, push. Only pause for genuinely
irreversible or high-risk actions.
**Focused**: The user is watching. Be more collaborative — surface choices,
ask before committing to large changes."
KAIROS introduces a dedicated set of tools not present in standard Claude Code. Each is conditionally loaded in tools.ts based on its feature flag:
// tools.ts — conditional loading (simplified)
const SleepTool = feature('PROACTIVE') || feature('KAIROS')
? require('./tools/SleepTool/SleepTool.js').SleepTool : null
const SendUserFileTool = feature('KAIROS')
? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool : null
const PushNotificationTool = feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
? require('./tools/PushNotificationTool/PushNotificationTool.js').PushNotificationTool : null
const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool : null
// Cron tools (AGENT_TRIGGERS — independently shippable):
const cronTools = feature('AGENT_TRIGGERS')
? [ CronCreateTool, CronDeleteTool, CronListTool ] : []
Yield execution for a duration without holding a shell process. The model's primary idling mechanism. Respects minSleepDurationMs and maxSleepDurationMs settings for throttling. Can be called concurrently with other tools.
The model's primary output channel in assistant mode. Supports status: 'proactive' (unsolicited update) vs status: 'normal' (reply). Accepts file attachments. Controlled by entitlement + opt-in logic via isBriefEnabled().
Sends a file to the user as an attachment. Distinct from SendUserMessage's attachment parameter — a standalone tool for file delivery. Lives alongside BriefTool attachments logic in tools/SendUserFileTool/.
Sends a system-level push notification to the user's device. Used when Claude completes a long-running task and the user has walked away. The ConfigTool exposes a pushNotificationsEnabled setting gated behind this flag.
Subscribe to GitHub PR webhook events. Lets the assistant wake automatically when a PR review lands or CI finishes, without polling. Appears as both a tool and a slash command (/subscribe-pr).
Schedule prompts on cron expressions. One-shot ("remind me at 2pm") or recurring ("every weekday at 9am"). Durable tasks persist to .claude/scheduled_tasks.json and survive restarts. Auto-expire after a configurable max age (default: days).
BriefTool Deep Dive: Entitlement vs Activation
BriefTool has the most complex enable logic in the codebase. It deliberately separates two questions:
isBriefEntitled()
- Is the user allowed to use Brief?
- Checks: KAIROS active OR env var
CLAUDE_CODE_BRIEFOR GrowthBook flagtengu_kairos_brief - Refreshes every 5 minutes from GrowthBook
- Governs:
--briefflag,defaultView: 'chat',--toolslisting
isBriefEnabled()
- Is Brief active in this session?
- Requires:
kairosActiveORuserMsgOptInAND entitlement - Called from
Tool.isEnabled()— lazy, post-init - Governs: whether model sees the tool, system prompt section, todo-nag suppression
The reason for the split: without it, enrolling a user in tengu_kairos_brief would silently activate Brief for all their sessions. The opt-in (userMsgOptIn) must be set explicitly by user action: --brief, defaultView: 'chat', /brief slash command, or CLAUDE_CODE_BRIEF env var.
// tools/BriefTool/BriefTool.ts (simplified)
export function isBriefEnabled(): boolean {
// Top-level feature() guard is load-bearing for dead-code elimination.
// Bun constant-folds to `false` in external builds.
return feature('KAIROS') || feature('KAIROS_BRIEF')
? (getKairosActive() || getUserMsgOptIn()) && isBriefEntitled()
: false
}
isBriefEntitled() alone (which has its own guard) is semantically equivalent but defeats constant-folding across the boundary." The top-level feature() guard at each call site is what lets Bun tree-shake the entire BriefTool object from external builds.
scheduled_tasks.jsonThe cron system is the mechanism for Claude to schedule its own future work. It lives entirely in utils/cronTasks.ts, utils/cronScheduler.ts, and the three Cron tools.
CronTask Shape
// utils/cronTasks.ts
type CronTask = {
id: string
cron: string // 5-field cron in local timezone
prompt: string // prompt to enqueue when task fires
createdAt: number // epoch ms — anchor for missed-task detection
lastFiredAt?: number // set after each recurring fire
recurring?: boolean // true = reschedule after firing
permanent?: boolean // exempt from recurringMaxAgeMs expiry
durable?: boolean // runtime-only: false = session-only, undefined = disk-backed
agentId?: string // routes fire to a teammate's queue instead of main REPL
}
Two Durability Tiers
Session-Only (durable: false)
- Never written to disk
- Disappears when Claude exits
- For: "remind me in 5 minutes", "check back in an hour"
- Default for most user requests
Durable (durable: true)
- Persists to
.claude/scheduled_tasks.json - Survives restarts — scheduler picks up on next boot
- Missed one-shot tasks surfaced for catch-up
- Auto-expires recurring tasks after
recurringMaxAgeMs
The Jitter System
The CronCreate prompt contains engineering wisdom about fleet-scale load distribution:
// From CronCreateTool prompt (ScheduleCronTool/prompt.ts)
"Every user who asks for '9am' gets `0 9`, and every user who asks for 'hourly' gets
`0 *` — which means requests from across the planet land on the API at the same instant.
When the user's request is approximate, pick a minute that is NOT 0 or 30:
'every morning around 9' → '57 8 * * *' or '3 9 * * *' (not '0 9 * * *')
'hourly' → '7 * * * *' (not '0 * * * *')"
On top of the model's offset choice, the scheduler itself adds deterministic jitter: recurring tasks fire up to 10% of their period late (max 15 min), and one-shot tasks landing on :00 or :30 fire up to 90 seconds early.
Permanent Tasks: Assistant Mode Built-ins
The permanent: true field exists specifically for assistant mode's built-in tasks — daily catch-up, morning check-in, dream consolidation. These are written to scheduled_tasks.json at install time by src/assistant/install.ts and are exempt from age-based expiry. The writeIfMissing() pattern means re-installing never overwrites user customizations.
AutoDream is KAIROS's background maintenance worker. It fires automatically when enough time and sessions have accumulated, spawning a forked sub-agent to consolidate memory without interrupting the main session.
Gate Chain (cheapest-first)
Default thresholds from GrowthBook flag tengu_onyx_plover: 24 hours since last consolidation, 5 sessions minimum. Both can be tuned live without a deploy.
The Consolidation Prompt: 4 Phases
The dream agent receives a structured prompt from services/autoDream/consolidationPrompt.ts:
Orient
ls memory directory, read MEMORY.md index, skim existing topic files to avoid duplicates. If logs/ or sessions/ subdirs exist (assistant-mode layout), review recent entries.
Gather
Daily logs first, then drifted memories, then narrow transcript grep. Never exhaustively read transcripts — only search for things already suspected to matter.
Consolidate
Merge new signal into existing topic files, convert relative dates to absolute, delete contradicted facts at the source.
Prune & Index
Update MEMORY.md: keep under MAX_ENTRYPOINT_LINES lines and ~25KB. Each entry: one line, one-line hook. Never write memory content directly into the index.
Tool Constraints for Dream Runs
The auto-dream sub-agent receives a hardened tool constraint note appended to its prompt:
"Bash is restricted to read-only commands (ls, find, grep, cat, stat, wc, head, tail).
Anything that writes, redirects to a file, or modifies state will be denied.
Plan your exploration with this in mind — no need to probe."
This is appended via the extra parameter only in auto-dream runs — manual /dream skill runs in the main loop with normal permissions.
DreamTask: UI Visibility
The forked dream agent is surfaced in the footer pill and Shift+Down background tasks dialog via tasks/DreamTask/DreamTask.ts. It tracks:
phase: 'starting' | 'updating'— flips to'updating'when the first Edit/Write tool call landsfilesTouched— partial list (misses Bash-mediated writes, only captures pattern-matched tool calls)turns— last 30 assistant turns, tool_use blocks collapsed to a count
consolidationLock.ts. tryAcquireConsolidationLock() returns null if another process is mid-consolidation. If the run fails or the user kills it from the tasks dialog, rollbackConsolidationLock(priorMtime) rewinds the lock file so the next session can retry. The scan throttle (10 minutes) acts as backoff between retries.
Standard Claude Code memory uses a single MEMORY.md index file that the model reads and writes. KAIROS assistant mode uses a fundamentally different model: an append-only daily log.
Standard Mode
- Single
MEMORY.mdfile - Model reads and writes it directly
- Shared across team via TEAMMEM sync
- AutoDream consolidates it periodically
- Scales poorly with high session volume
KAIROS Assistant Mode
- Daily log files:
logs/YYYY/MM/YYYY-MM-DD.md - Append-only during work — no overwrites
- Dream skill distills logs into topic files nightly
- MEMORY.md becomes the synthesized index
- Not compatible with TEAMMEM sync (explicitly gated off)
The daily log path is computed by getAutoMemDailyLogPath() in memdir/paths.ts:
// memdir/paths.ts
export function getAutoMemDailyLogPath(date: Date = new Date()): string {
const yyyy = date.getFullYear().toString()
const mm = (date.getMonth() + 1).toString().padStart(2, '0')
const dd = date.getDate().toString().padStart(2, '0')
return join(getAutoMemPath(), 'logs', yyyy, mm, `${yyyy}-${mm}-${dd}.md`)
// → ~/.claude/memory/logs/2026/03/2026-03-31.md
}
--session-id FlagOne of KAIROS's defining capabilities is persistent session identity. A KAIROS session can be resumed across restarts with:
# Restart and continue the same conversation
claude remote-control --session-id=<id>
claude remote-control --continue # alias: -c
These flags are parsed in bridge/bridgeMain.ts and are explicitly behind feature('KAIROS') guards. From the comments:
// bridge/bridgeMain.ts
// feature('KAIROS') gate: --session-id is ant-only; without the gate,
// external builds would expose an argument that does nothing.
if (feature('KAIROS') && arg === '--session-id' && ...) { ... }
if (feature('KAIROS') && arg.startsWith('--session-id=')) { ... }
if (feature('KAIROS') && (arg === '--continue' || arg === '-c')) { ... }
The session is identified via a perpetual flag in the bridge configuration. When a session is perpetual, the env-less bridge path (which normally provides better performance) falls back to the env-based path to preserve cross-restart session continuity:
// initReplBridge.ts
// perpetual (assistant-mode session continuity via bridge-pointer.json) is
// env-coupled and not yet implemented in env-less — fall back to env-based
// when set so KAIROS users don't silently lose cross-restart continuity.
if (isEnvLessBridgeEnabled() && !perpetual) { ... }
KAIROS adds several settings keys to the Claude Code settings schema (utils/settings/types.ts):
| Key | Type | Purpose | Gate |
|---|---|---|---|
| assistant | boolean | Start Claude in assistant mode (custom system prompt, brief view, scheduled check-in skills) | KAIROS |
| assistantName | string | Display name in the claude.ai session list | KAIROS |
| defaultView | 'chat' | 'transcript' | Chat view = SendUserMessage checkpoints only. Transcript = full tool output. 'chat' activates Brief opt-in. |
KAIROS || KAIROS_BRIEF |
| minSleepDurationMs | number | Minimum sleep the Sleep tool must take. Throttles proactive tick frequency in managed environments. | PROACTIVE || KAIROS |
| maxSleepDurationMs | number (-1 = indefinite) | Max sleep duration. -1 = wait for user input only. Limits idle time in remote environments. | PROACTIVE || KAIROS |
| autoDreamEnabled | boolean | Override GrowthBook default for background memory consolidation. User setting wins over GB flag. | always present |
| channelsEnabled | boolean | Opt-in for MCP channel notifications (push from MCP servers). Default off. | always present |
| allowedChannelPlugins | array | Org-level allowlist of channel plugins. Replaces Anthropic ledger when set. | always present |
When proactive is active, getSystemPrompt() takes a completely different path than the normal structured-sections approach:
// constants/prompts.ts — proactive path
if ((feature('PROACTIVE') || feature('KAIROS')) && proactiveModule?.isProactiveActive()) {
logForDebugging('[SystemPrompt] path=simple-proactive')
return [
`\nYou are an autonomous agent. Use the available tools to do useful work.\n\n${CYBER_RISK_INSTRUCTION}`,
getSystemRemindersSection(),
await loadMemoryPrompt(),
envInfo,
getLanguageSection(settings.language),
isMcpInstructionsDeltaEnabled() ? null : getMcpInstructionsSection(mcpClients),
getScratchpadInstructions(),
getFunctionResultClearingSection(model),
SUMMARIZE_TOOL_RESULTS_SECTION,
getProactiveSection(), // ← tick loop + Sleep + focus instructions
].filter(s => s !== null)
}
The standard path uses a dynamic sections registry with caching and section identifiers. The proactive path returns a flat array — simpler, faster, and optimized for the cache-miss-heavy nature of a continually-running agent.
getBriefSection() includes a guard: when proactive is active, getProactiveSection() already appends BRIEF_PROACTIVE_SECTION inline (at the end of the terminal focus paragraph). Without the guard, the Brief instructions would appear twice in the system prompt.
KAIROS's runtime behavior is extensively gated by GrowthBook feature flags, giving Anthropic the ability to tune or kill subsystems without a deploy. The pattern is consistent across the codebase:
// Pattern: cached refresh with explicit interval
const KAIROS_BRIEF_REFRESH_MS = 5 * 60 * 1000 // 5 minutes
const KAIROS_CRON_REFRESH_MS = 5 * 60 * 1000 // 5 minutes
// Brief entitlement — checks GB flag, refreshes every 5 min
getFeatureValue_CACHED_WITH_REFRESH('tengu_kairos_brief', false, KAIROS_BRIEF_REFRESH_MS)
// Cron kill switch — fleet-wide disable
getFeatureValue_CACHED_WITH_REFRESH('tengu_kairos_cron', true, KAIROS_CRON_REFRESH_MS)
// Durable cron kill switch (narrower — leaves session-only cron untouched)
getFeatureValue_CACHED_WITH_REFRESH('tengu_kairos_cron_durable', true, KAIROS_CRON_REFRESH_MS)
// AutoDream thresholds + enabled gate
getFeatureValue_CACHED_MAY_BE_STALE('tengu_onyx_plover', null)
// Cron jitter config — ops can push during an incident to reduce load
getFeatureValue_CACHED_WITH_REFRESH('tengu_kairos_cron_config', null, ...)
A comment in cronJitterConfig.ts explains the incident story: "During an incident, ops can push tengu_kairos_cron_config with e.g. a high jitter multiplier to spread load across the fleet." The 5-minute refresh interval is tuned specifically so GB changes take effect within one cache window, fast enough for incident response but not so fast it adds constant network pressure.
Studying the KAIROS source reveals several architectural principles that Anthropic applied consistently:
Principle 1: Positive Ternaries for DCE
Every feature-gated block uses positive ternaries rather than negative early-returns:
// CORRECT — DCE works
return feature('KAIROS') ? doKairosThings() : false
// WRONG — DCE fails (negative guard defeats constant-folding)
if (!feature('KAIROS')) return false
return doKairosThings()
Principle 2: Independent Shippability
Each sub-feature is designed so it can ship and be kill-switched independently. The cron module graph has zero imports into src/assistant/. BriefTool has its own KAIROS_BRIEF flag. This means a bug in full assistant mode doesn't block shipping the cron scheduler.
Principle 3: Cheapest Gate First
All KAIROS subsystems check gates in cheapest-to-evaluate order. AutoDream checks kairosActive (one boolean read) before checking time, before reading the filesystem for session count, before acquiring a lock. This matters because these checks run on every agent turn.
Principle 4: Operator Kill Switches
Every GrowthBook-gated subsystem has a local env var override that wins: CLAUDE_CODE_DISABLE_CRON kills the cron scheduler, CLAUDE_CODE_BRIEF enables Brief for dev/testing. This lets individual engineers bypass the fleet gates without touching GrowthBook.
Principle 5: Cost Budgeting in the System Prompt
The model is explicitly told about API call costs and prompt cache mechanics. "Each wake-up costs an API call, but the prompt cache expires after 5 minutes of inactivity — balance accordingly." This is unusually transparent system-prompt engineering — treating the model as a cost-aware participant rather than hiding infrastructure details from it.
Key Takeaways
- KAIROS is a family of build-time flags, not a single switch. The core flag is ant-only; sub-features like
KAIROS_BRIEF,KAIROS_CHANNELS, andAGENT_TRIGGERSship to external users independently. kairosActiveinbootstrap/state.tsis the runtime pivot that changes memory mode, BriefTool opt-in behavior, fast mode availability, and bridge worker type simultaneously.- The tick loop uses
<tengu_tick>XML heartbeats +SleepToolas a cost-aware idle mechanism. The model is told about cache expiry and API call costs explicitly in its system prompt. - BriefTool separates entitlement (allowed to use?) from activation (opted in?) to prevent silent Brief-on-by-default for enrolled users.
- AutoDream fires as a forked sub-agent after 24 hours and 5 sessions, using a file-mtime lock with rollback on failure. It receives a hardened read-only tool constraint not present in manual
/dreamruns. - The cron system has two tiers: session-only (in-memory) and durable (persisted to
.claude/scheduled_tasks.json). Both are kill-switchable independently via GrowthBook without a deploy. - Positive ternaries are mandatory for dead-code elimination — Bun constant-folds
feature('KAIROS')tofalsein external builds only if the guard is a ternary, not a negative early-return.
Check Your Understanding
isBriefEnabled() have its own top-level feature('KAIROS') || feature('KAIROS_BRIEF') guard even though it calls isBriefEntitled() which has its own guard?feature('KAIROS') at the literal call site to constant-fold and tree-shake.kairosActive is true, what happens to AutoDream?autoDream.ts: if (getKairosActive()) return false // KAIROS mode uses disk-skill dream. Assistant mode has its own dream skill installed at .claude/scheduled_tasks.json, so the ad-hoc AutoDream is disabled.isGateOpen() explicitly checks getKairosActive() and returns false. KAIROS has its own dream pipeline via scheduled tasks.recurring: true, permanent: false. What happens after recurringMaxAgeMs has elapsed?permanent: true flag exempts a task from this behavior — it's used for assistant mode's built-in system tasks.permanent: true is what makes a task exempt from this expiry — used for assistant mode's built-in tasks.