markdown.engineering
Lesson 46 — Deep Dive

KAIROS: The Always-On Persistent Agent Mode

How Claude Code transforms from a request-response CLI into an autonomous agent that lives between sessions, schedules its own work, and reaches out to you proactively.

01 What Is KAIROS?

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.

Source
The name 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:

FlagWhat it unlocksScope
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
Architecture Note
The flags are designed so each capability can ship and be killed-switched independently. A comment in 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."
02 The State Pivot: kairosActive

Everything 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:

Memory

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.

BriefTool

Opt-In Bypass

isBriefEnabled() returns true for kairosActive sessions without requiring explicit user opt-in. The system prompt hard-codes "you MUST use SendUserMessage."

Fast Mode

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.

AutoDream

Dream Gated Off

The background memory consolidation agent (isGateOpen()) explicitly returns false when kairosActive — assistant mode uses its own disk-skill dream pipeline instead.

Bridge

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.

Scheduler

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.

03 The Tick Loop: How the Agent Stays Alive

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.
Key Insight
Sleep progress (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."
04 The KAIROS Tool Suite

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 ] : []
Sleep
PROACTIVE || KAIROS

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.

SendUserMessage (Brief)
KAIROS || KAIROS_BRIEF

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().

SendUserFile
KAIROS only

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/.

PushNotification
KAIROS || KAIROS_PUSH_NOTIFICATION

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.

SubscribePR
KAIROS_GITHUB_WEBHOOKS

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).

CronCreate / CronDelete / CronList
AGENT_TRIGGERS

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_BRIEF OR GrowthBook flag tengu_kairos_brief
  • Refreshes every 5 minutes from GrowthBook
  • Governs: --brief flag, defaultView: 'chat', --tools listing

isBriefEnabled()

  • Is Brief active in this session?
  • Requires: kairosActive OR userMsgOptIn AND 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
}
DCE Gotcha
Comments in BriefTool explicitly warn: "Composing 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.
05 Cron Scheduling: scheduled_tasks.json

The 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.

06 AutoDream: Background Memory Consolidation

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)

flowchart TD A[Post-sampling hook fires] --> B{isGateOpen?} B -->|kairosActive| X[Skip — KAIROS uses disk-skill dream] B -->|isRemoteMode| Y[Skip] B -->|!autoMemEnabled| Z[Skip] B -->|!autoDreamEnabled| AA[Skip] B -->|pass| C{Time gate\nhoursSince >= minHours?} C -->|no| Skip1[Return] C -->|yes| D{Scan throttle\nlastScanMs >= 10min?} D -->|no| Skip2[Return] D -->|yes| E[Scan sessions since lastConsolidatedAt] E --> F{Session count >= minSessions?} F -->|no| Skip3[Return] F -->|yes| G[tryAcquireConsolidationLock] G -->|null — locked| Skip4[Return] G -->|priorMtime| H[registerDreamTask UI] H --> I[runForkedAgent — dream prompt] I --> J[completeDreamTask\nappendSystemMessage]

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:

Phase 1

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.

Phase 2

Gather

Daily logs first, then drifted memories, then narrow transcript grep. Never exhaustively read transcripts — only search for things already suspected to matter.

Phase 3

Consolidate

Merge new signal into existing topic files, convert relative dates to absolute, delete contradicted facts at the source.

Phase 4

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 lands
  • filesTouched — partial list (misses Bash-mediated writes, only captures pattern-matched tool calls)
  • turns — last 30 assistant turns, tool_use blocks collapsed to a count
Lock Mechanics
Dream uses a file-mtime lock in 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.
07 Assistant-Mode Memory: Daily Logs vs MEMORY.md

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.md file
  • 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
}
08 Session Continuity: The --session-id Flag

One 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) { ... }
09 Settings: How Users Configure Assistant Mode

KAIROS adds several settings keys to the Claude Code settings schema (utils/settings/types.ts):

KeyTypePurposeGate
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
10 The System Prompt in Proactive Mode

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.

Brief Section Deduplication
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.
11 The GrowthBook Kill-Switch Architecture

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.

12 Architecture Diagram: Full KAIROS System
graph TD subgraph "Build Time" F1["feature('KAIROS')"] F2["feature('KAIROS_BRIEF')"] F3["feature('AGENT_TRIGGERS')"] F4["feature('KAIROS_GITHUB_WEBHOOKS')"] F5["feature('PROACTIVE') || KAIROS"] end subgraph "Runtime State" KA[kairosActive: boolean] UMO[userMsgOptIn: boolean] end subgraph "Output Tools" BT[BriefTool / SendUserMessage] SFT[SendUserFileTool] PNT[PushNotificationTool] end subgraph "Lifecycle Tools" ST[SleepTool] SPRT[SubscribePRTool] CC[CronCreate] CD[CronDelete] CL[CronList] end subgraph "Background Services" ADR[AutoDream\nforked sub-agent] CS[CronScheduler\n1s poll loop] end subgraph "Memory" DL["Daily Logs\nlogs/YYYY/MM/DD.md"] MEM[MEMORY.md index] CP[ConsolidationPrompt\n4-phase dream] end F1 --> KA F1 --> BT F1 --> SFT F1 --> PNT F2 --> BT F3 --> CC & CD & CL F4 --> SPRT F5 --> ST KA --> DL KA --> BT KA --> ADR CS --> |fires prompt| ST CC --> CS ADR --> CP CP --> DL CP --> MEM
13 Key Design Principles

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, and AGENT_TRIGGERS ship to external users independently.
  • kairosActive in bootstrap/state.ts is 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 + SleepTool as 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 /dream runs.
  • 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') to false in external builds only if the guard is a ternary, not a negative early-return.

Check Your Understanding

Q1. Why does isBriefEnabled() have its own top-level feature('KAIROS') || feature('KAIROS_BRIEF') guard even though it calls isBriefEntitled() which has its own guard?
Q2. When kairosActive is true, what happens to AutoDream?
Q3. A cron task is created with recurring: true, permanent: false. What happens after recurringMaxAgeMs has elapsed?
Q4. Why does the proactive system prompt tell the model about the 5-minute prompt cache expiry window?