Files
cline/sdk/ARCHITECTURE.md
T
Bee f3249a77cf feat(desktop): add hub controls to General settings (#14653)
* feat(desktop): add hub controls to About settings

* fix(desktop): require trusted connection for hub status

* fix(desktop): isolate local hub activity and track restart identity
2026-10-20 18:15:49 -07:00

81 KiB
Raw Blame History

Cline SDK Architecture

This document is the architecture source of truth for the Cline SDK repository. It describes how the system is organized, how components interact, and the design principles that guide development decisions.

Who should read this?

  • SDK contributors working across multiple packages
  • Developers building integrations or host applications using @cline/core
  • Plugin authors understanding the runtime and extension systems

What this covers:

  • Package boundaries and responsibilities
  • Dependency direction and layering rules
  • Runtime flows (local, hub-backed, remote-config managed)
  • Design seams (repeated patterns instead of one-off integrations)
  • Architectural constraints and why they exist

What this is NOT:

  • An onboarding guide for new contributors (see README.md and CONTRIBUTING.md)
  • A detailed API reference (see package READMEs and inline JSDoc)
  • A user guide (see the main documentation)

Layered Model

The workspace is organized as a layered runtime stack.

flowchart LR
  shared["@cline/shared"]
  llms["@cline/llms"]
  agents["@cline/agents"]
  core["@cline/core"]
  apps["Host Apps"]

  llms --> shared
  agents --> llms
  agents --> shared
  core --> agents
  core --> llms
  core --> shared
  apps --> core

Package Responsibilities

@cline/shared

Owns reusable low-level contracts and infrastructure:

  • shared types and schemas
  • path resolution
  • hook contracts/engine
  • extension registry contracts
  • prompt and parsing helpers
  • storage path helpers
  • remote-config schemas, managed instruction materialization, telemetry normalization, and blob upload primitives

Design rule:

  • shared should not depend on higher-level runtime packages.

@cline/llms

Owns model/provider runtime concerns:

  • provider settings/config resolution
  • model catalogs and manifests
  • shared gateway-style provider contracts
  • handler creation via an internal gateway registry
  • AI SDK-backed provider execution code

Design rule:

  • provider-specific behavior should be isolated here, not spread across core or apps.
  • AI SDK response X-Request-ID metadata travels on the existing model finish event into afterModel.requestId. It identifies the final surfaced step, not every hidden HTTP retry. Hosts can use existing model hooks for observations without wrapping transports or adding a request callback API. VS Code Git observations belong to the open conversation, not the SDK runtime: an ended event may leave the chat open after a failure. Startup cleanup handles unopened observers; explicit host stop/disposal closes both normal and restored observation windows. The observer binds to the actual ID returned by Core's start/restore result. VS Code starts interactively without a prompt and sends prompts only after that result. Start and restore explicitly prepare their own input and observer, so overlapping starts need no async-context bridge. Telemetry never supplies or changes config.sessionId: doing so would turn a new session into a restart. afterModel starts a bounded background Git read and emits with the triggering response's request ID when available, without waiting before tools execute. Each observer skips captures while a read is already in flight, avoiding queues and out-of-order emissions. Tools may change files during the read: this is state observed after response R, not an atomic pre-tool snapshot or proof of changes caused by R. Each window emits its first snapshot, then only changes to Git fields or workspace-root count. There are no opening, yield, or idle emissions or Git-extension watchers. Changes after the final model call require a later call to be observed. Consumers must carry observations forward within that window rather than expect an event per request. Repeated edits while already dirty need not change the recorded flags. Status limits preserve cheap identity reads as partial, without dirty flags. The field-level contract is GitSnapshotProperties in packages/core/src/services/telemetry/core-events.ts.

@cline/agents

Owns the stateless runtime loop:

  • agent iteration loop
  • tool orchestration
  • runtime event emission
  • hook/extension execution
  • turn preparation before provider calls
  • in-memory team/runtime primitives

Design rule:

  • agents should not own persistent storage or host lifecycle concerns.

@cline/core

Owns stateful orchestration:

  • runtime composition
  • session lifecycle
  • storage and persistence
  • config watching/loading and watcher projections
  • settings listing and mutation orchestration
  • default host tool assembly
  • plugin discovery/loading
  • default context compaction policy
  • telemetry integration
  • hub server and scheduled-runtime services under src/hub/
  • hub discovery, the detached hub daemon, and the @cline/core/hub/daemon-entry subpath
  • host-side hub client adapters (NodeHubClient, HubSessionClient, HubUIClient, connectToHub) exported from @cline/core/hub
  • the experimental cloud-session client exported from @cline/core/cloud: cloud API access, remote session lifecycle, and transcript reconciliation

Design rules:

  • core is the app-facing orchestration layer over agents.
  • session/fork-metadata owns fork ancestry and removal of inherited handoff markers; hosts retain title, transcript, and workspace-restore policies.
  • @cline/core/cloud owns remote cloud-session state and emits immutable snapshots and events. Viewers hydrating active runs with readMessages reconcile canonical history at completion even if they missed the run start. Hosts supply authentication and project those snapshots into their UI; feature flags, account selection, and host persistence remain outside the controller. Importing this subpath does not initialize a local agent.
  • cloud/models owns cloud model eligibility; services/cloud-handoff owns Git preflight, fingerprints, and transcript verification. Hosts own transfer orchestration, source locks, persistence, feature gating, and draft recovery.
  • The cloud controller resumes suspended sessions and restores saved tasks.
  • Desktop retains pending first-task creation options in a context-owned map across credential-driven controller replacement. The shared controller consumes that intent when an inner task exists or is created; retaining an ID without its approval policy is not sufficient.
  • hub-related modules live under packages/core/src/hub/, grouped by service:
    • client/ contains host-facing hub clients and browser connection helpers
    • daemon/ contains detached daemon startup, entrypoint, and local runtime handler wiring
    • discovery/ contains endpoint defaults, discovery records, and workspace owner resolution
    • server/ contains WebSocket server startup, native/browser socket adapters, server transport, server helpers, and handlers/ for hub command dispatch
  • settings mutations belong in core services and hub commands, not in host-specific file writes. Hosts should call the core settings facade or the settings.* hub command family and react to settings.changed.

Runtime Flows

Local In-Process Runtime

  1. Host constructs a RuntimeHost through @cline/core.
  2. @cline/core selects LocalRuntimeHost through packages/core/src/runtime/host.ts.
  3. Hosts normalize broad local config into RuntimeSessionConfig plus localRuntime overrides before calling RuntimeHost.start(...).
  4. @cline/core prepares a local bootstrap artifact from localRuntime, then builds the runtime from it.
  5. @cline/core creates an Agent from @cline/agents.
  6. @cline/agents runs the loop using @cline/llms handlers.
  7. @cline/core persists state, artifacts, and metadata.

Completion telemetry is anchored to the assistant's explicit completion declaration, not session shutdown. After each agent turn, the local runtime inspects AgentResult.toolCalls and emits task.completed the moment a successful submit_and_exit (the SDK analog of original Cline's attempt_completion) is observed. A single teardown choke point (emitTaskCompletedOnTeardown(...)) retains a fallback emission for sessions whose final turn finished cleanly without an explicit completion-tool observation (non-interactive runs not using the yolo preset, or hosts that disable submit_and_exit). It is invoked from every session exit path — both shutdownSession(...) and releaseSessionRuntime(...) — so the emission never depends on which teardown branch a stop routes through. Each session emits at most one task.completed. See DOC.md for the event payload and source field.

Hub-Backed Runtime

The hub health endpoint includes its hubId. Restart callers compare this instance identity while waiting for shutdown, so a replacement started by reconnecting clients at the same address counts as retirement of the original hub. Desktop local activity is recorded only at its local hub observer subscription; cloud session projection does not write to that activity list.

  1. Host constructs a RuntimeHost through @cline/core.
  2. @cline/core selects HubRuntimeHost or RemoteRuntimeHost through packages/core/src/runtime/host.ts.
  3. When no compatible local hub is already discovered, @cline/core can spawn a detached hub daemon and reconnect through discovery. The spawner waits up to 15s for the daemon to publish a usable discovery record (cold starts of the compiled binary on Windows regularly need more than the previous 8s). If the daemon still fails to come up, the runtime host's No compatible hub runtime is available error carries the underlying reason, and the daemon reports its own startup failure to telemetry (hub.daemon.startup) before exiting.
  4. Hosts attach and detach from shared sessions without stopping the authority runtime, so another client can keep streaming or resume the same session later. One daemon serves every Cline client on the machine, and a hub-hosted session's telemetry carries only its own client (clientName/clientVersion), so the daemon also records whose build started it. The spawner stamps CLINE_HUB_LAUNCHER_CLIENT, CLINE_HUB_LAUNCHER_VERSION, and CLINE_HUB_LAUNCHER_TRANSPORT on the daemon environment (withHubLauncherIdentity in @cline/shared), defaulting to the spawning process's ClineClientIdentity and local. An embedding host that starts the hub through the CLI (the desktop app running cline-cli hub ensure) stamps itself first, and a stamp already present is kept as a whole, so the CLI that performs the spawn never records its own name or fills in its version next to the host's name. SSH remote hubs are stamped by the client over the env prefix of the remote hub ensure command with transport=ssh. The daemon claims the stamp out of its environment at startup (claimHubLauncherIdentity, same hazard as the daemon sentinel: hosted sessions spawn processes that inherit the environment, and a hub one of them starts must record itself) and publishes it with its build id and executable name as OTel resource attributes: cline.hub.launcher.client, cline.hub.launcher.version, cline.hub.launcher.transport, cline.hub.build_id, cline.hub.runtime. Collectors and Langfuse surface these as resourceAttributes.*, so a desktop session hosted by a CLI-started hub is attributable to that CLI build.
  5. The hub-hosted runtime executes the agent loop using @cline/agents and @cline/llms.
  6. @cline/core hub services broker sessions, events, approvals, schedules, and client-owned runtime capabilities such as session-local tool executors.
  7. Hub event forwarding preserves structured streaming lifecycle boundaries: text/reasoning deltas, final text/reasoning completion, tool start/update/finish, and agent done events are translated across the hub transport so host UIs can reliably close loading/streaming state. run.started is emitted only after the target session is resolved and carries the originating command's requestId and clientId, allowing multi-client hosts to correlate delivery acknowledgments.
  8. Hub client adapters exported from @cline/core/hub (NodeHubClient, HubSessionClient, HubUIClient, connectToHub) translate command/reply and event streams into host-facing APIs. Node clients register their version and PID together with caller metadata on every connection; HubSessionClient passes metadata through at registration so client events, refreshed lists, and reconnects retain the same details.
  9. Hub session.get records include both canonical root-session usage and explicit aggregate usage from the hub-owned RuntimeHost, so attached clients can intentionally render either root-only or root-plus-teammate costs without replaying event streams.

Hub session.send_input accepts a nonblank prompt or at least one nonblank image/file attachment; requests with neither are rejected before starting a turn. NodeHubClient commands may supply a synchronous, local beforeDispatch guard. It runs after connection setup, before allocating or sending the command, on each attempt. Throwing prevents that attempt's dispatch; already-dispatched runs still require run.abort.

Session status is reported, never fabricated. A session's initial status reflects whether a turn actually runs inside start(...): prompt-bearing starts (one-shot or interactive) begin running, interactive starts without a prompt begin idle until their first turn, and resumed sessions report their persisted status. Each turn owns its running → idle transition. On the client side, HubRuntimeHost projects a status event only when the hub session record or the session snapshot actually carries one; snapshot-only session.updated events (asynchronous persistence updates, which can trail a turn's final idle update) report the snapshot's real status. Hosts that gate workspace-wide operations on busy sessions (for example the desktop's checkpoint-restore gate) depend on this: a defaulted running with no owning turn leaves such gates blocked with nothing to clear them.

Command progress follows the same runtime event boundary as other agent output. Shell executors emit structured stdout/stderr chunks through AgentToolContext.emitUpdate; the agent runtime projects them as tool content_update events, and the Hub publishes them as tool.updated with the session, tool-call, and tool identifiers intact. Hub clients reconstruct the tool update for their host-facing event stream. Client-contributed executors must forward capability progress through this same path rather than creating a host-specific side channel. The built-in shell executor coalesces output on a short interval and bounds each stream's pending tail before it enters the event pipeline; consumers independently coalesce and cap their rendered scrollback.

Proceed-while-running is an explicit command lifecycle, separate from client or session detachment. A shell process advertises detachability only after it has spawned and registered with the host-scoped command execution controller. The client sends run.proceed_while_running with the owning sessionId and, when available, toolCallId; the Hub delegates to the authoritative RuntimeHost, which releases every matching registered process from the tool call. The executor removes its abort and timeout ownership, resolves the tool call with the current bounded output and a temporary log path, and continues draining the process into that log. Detached logs are size-capped, retained for a bounded inspection window after the command exits, and then their temporary directories are removed. Every process that constructs a LocalRuntimeHost starts one detached-log reconciliation: it reaps completed logs outside the retention window, reschedules retained logs, and follows active detached-command identities until they exit, so cleanup does not depend on timers from the process that launched the command. Hub daemons and direct embedders therefore share the same detachment and restart lifecycle instead of relying on a daemon-specific entrypoint. Active-command markers pair the PID with a process-generation start token, preventing an unrelated process that later reuses the PID from extending the log lifetime. Completion markers distinguish a live, possibly silent command from a completed log. Process probes distinguish an absent process from an unavailable identity provider. Transient probe failures retain the active marker and are never treated as evidence of command completion. Reconciliation keeps the advertised log and retries until the provider can prove that the original process still exists, its PID belongs to a replacement process, or the process is absent. A host exit alone never starts the retention window for a surviving command; the replacement host continues polling the process identity and begins retention only after the command ends. During persistent provider unavailability, the capped log may outlive the normal retention window because preserving a potentially live command's advertised path takes precedence over guessing that it exited. A detached client connection alone never changes process ownership or command execution.

Generated Media Operation and Event Flow

Model modalities and provider operations are separate facts. Modalities describe values a model accepts or produces; the explicit operation selects the provider transport. Language models keep the normal agent loop even when they can emit media, while operation: "image-generation" selects generateImage and operation: "transcription" selects a declared speech-to-text transport. Operation-specific execution variants such as recorded and realtime transcription live in operationModes, not in the generic capability list. Specialized operations fail closed unless the provider manifest and adapter both implement them, so an OpenAI-compatible chat endpoint never implies an image, audio, transcription, or video endpoint.

Generated media crosses package boundaries as follows:

  1. @cline/llms validates provider output once and creates canonical GeneratedMedia values. The contract carries a stable ID, modality, MIME type, and a discriminated base64, HTTP(S), or artifact source. Current producers emit images; audio, video, and large artifact-backed files use the same contract.
  2. Provider model tools are adapters, not raw AI SDK tools. An adapter owns its native result projection into canonical media. The generic stream layer coalesces preliminary or repeated results, enforces the per-turn media budget, and persists only a compact activity summary rather than duplicating base64 in model-tool metadata.
  3. @cline/agents appends media events at their exact stream position in the assistant message. That message is the canonical replay and persistence source; observational provider-tool activity remains display-only metadata.
  4. @cline/core projects live media as content_end(media). The hub publishes assistant.media, preserving the same media ID, and clients deduplicate live and hydrated content by that ID.
  5. Web clients share GeneratedMediaContent from @cline/ui for image, audio, video, file, and unavailable-source rendering. Inline bytes are exposed only through short-lived browser-owned object URLs; remote and artifact sources require a client-owned trusted resolver. CLI and ACP clients provide transport-appropriate materialization or fallback output without changing the canonical message.

Image-edit inference is intentionally local: when a dedicated image model accepts image input and the current user message has no explicit image, only an image on the immediately preceding assistant message is reused. Older images are not implicitly attached across intervening turns.

Session history provenance keeps the client surface and initiation mode separate. StartSessionInput.source identifies the client (vscode, desktop, cli, core, and so on), while top-level StartSessionInput.mode identifies how the session began (user, automation, subagent, or team). The persisted messages envelope records both values, along with client version and child-session lineage. Missing initiation mode defaults to user; automation runtime adapters must pass mode: "automation" explicitly.

Root-session persistence is lazy. Starting a runtime allocates its session ID and keeps configuration or seeded history in memory, but does not create a database row, manifest, or messages artifact. The first accepted user turn persists that same ID and its artifacts. Closing a runtime before a user turn therefore leaves no empty history entry, and persistence code never allocates a replacement ID for an unknown session.

Session history listing filters child rows at the persistence layer. Subagent and team-task sessions are stored in the same table as the roots that spawned them and always sort newer, so listSessionHistory asks the runtime host for rootOnly rows. LocalRuntimeHost passes the option to the session backend, which applies it in the query before the limit; HubRuntimeHost sends it as session.list { limit, rootOnly } and the hub handler forwards it to its session host. Omitting the flag returns every row, which is what callers that render subagent trees rely on. History keeps a client-side root filter with a widening scan only as a fallback for older hubs that ignore the flag.

Workspace bootstrap is owned by the runtime that executes the session. Hub clients preserve an omitted cwd and workspaceRoot across the transport so the hub-side execution host can place the session in the shared chat workspace on its own filesystem at <cline-data-dir>/workspaces/chat (by default ~/.cline/data/workspaces/chat). The chat workspace is seeded with an AGENTS.md rules file that tells the agent to treat the session as a chat and to create a named project folder only when the user asks for one. The resolved paths are returned in the session snapshot and are the source of truth for client-side manifests; transport clients must not invent a local path for a remote runtime.

Detached daemon startup retries transient ETXTBSY spawn failures before polling discovery. This covers package-manager updates that replace the CLI binary immediately before a command restarts the shared hub.

Local hub discovery also carries the authentication contract for the shared daemon. On startup, the hub server generates a cryptographically random per-process auth token, stores it in the owner discovery record, and writes that record with owner-only file permissions. Local clients resolve the token from the discovery file at connection time rather than embedding it in endpoint URLs. The server validates the token with a constant-time comparison before accepting /hub WebSocket upgrades or /shutdown requests; WebSocket clients send it via the Sec-WebSocket-Protocol header and shutdown requests use an Authorization: Bearer header. Unauthenticated local processes can still probe public health/build metadata, but they cannot attach to sessions, issue commands, or stop the daemon.

Remote proxies that authenticate the client-facing WebSocket upgrade with HTTP headers use NodeHubClient.resolveConnectionHeaders. The resolver runs for every new socket, including reconnects, so hosts can refresh short-lived credentials. Header authentication is mutually exclusive with the local hub-token subprotocol; the proxy is responsible for authenticating the client and adding any private upstream hub credentials. Resolver failures and rejected protocol headers fail the connection and remain available through the client's connection-error state. Clients with active subscriptions keep retrying after header-resolution failures, even when no socket was created.

Local hub rediscovery is limited to managed shared-daemon endpoints obtained through discovery or ensure*HubServer(...) startup paths. Managed local hubs must match both the supported wire protocol and the current Hub build identity; a protocol-compatible daemon from another build is retired before its replacement starts so upgrades cannot keep executing stale runtime code. SDK builds embed a deterministic fingerprint of the runtime sources, package manifests, build configuration, and dependency lock, so the identity changes with the executable Hub code even before package versions are bumped. Builds also embed a build epoch that orders them in time: when the fingerprints differ, a managed Hub produced after the client's own build is reused over the compatible wire protocol instead of being retired (replacing it would downgrade the daemon), and the client's build-mismatch watcher prompts the user to update and restart. Hubs that are older, unordered, or missing build metadata are retired and replaced as before, so two concurrently running installations converge on the newest build instead of repeatedly replacing each other's daemons. Explicit endpoints, including loopback URLs such as ws://127.0.0.1:<port>/hub, are sticky exact targets and remain protocol-only: reconnects may retry the same socket URL, but command recovery and startup-deadlock recovery must not replace them with the workspace-discovered hub. This keeps custom local hubs and remote hubs from silently drifting to a different process.

Interactive CLI Startup

  1. apps/cli owns OpenTUI startup and must render the first frame without waiting for detached hub startup.
  2. Interactive sessions use backendMode: "auto" so an already-compatible hub can be reused immediately, while a missing hub is only prewarmed in the background and the TUI falls back to a local runtime for responsiveness.
  3. Hub-required flows such as cline hub, schedules, connectors, and --zen may still call the explicit ensure path because those commands require a live hub before proceeding.
  4. Resume hydration is deferred until after renderOpenTui() so loading previous messages cannot block initial TUI paint.
  5. Any future CLI/TUI startup work should follow the same rule: daemon startup, discovery polling, provider catalog refreshes, file indexing, and resume reads must be background or user-action gated unless a command explicitly requires their result before output.

Temporary external tool result recovery

Cache admission sizes the persisted result using the same JSON/string preview representation as MessageBuilder, ensuring every result truncated by the per-result limit is eligible for recovery. Native images are separated from structured preview text and excluded from the recovery copy. Plain strings remain unchanged, while structured cached text uses YAML with literal multiline blocks and no automatic line wrapping. YAML size controls the cache byte limit, not the truncation threshold. This preserves MCP payload line breaks instead of escaping them into a single JSON line. Recovery requires an available read_files tool and uses its existing output and per-line limits. Disabling the reader does not disable caching or remove notices.

MCP and Composio tools declare resultPolicy: "cache-oversized" at registration; custom tools may opt in. After result-transforming hooks finish, Core caches oversized output in a session-owned ToolResultCache while retaining original output in conversation history and tool events. The existing synchronous MessageBuilder.buildForApi produces bounded previews and model-only recovery instructions, preserving native images. Other tools keep their existing projection behavior. No disk cache or asynchronous recovery builder is involved.

Recovery URIs have the form cline://cache/<encoded-session-id>/<unique-result-id>.result.txt. Core injects the owning cache into the runtime tool context. The built-in file reader resolves these URIs before filesystem handling and uses the same bounded line reader, inclusive ranges, output caps, and abort handling as ordinary text files. URIs are restricted to the owning session; shell commands and filesystem search do not resolve them.

Entries expire after five additional model iterations without an explicit cache read. A session-level counter spans follow-up turns; including a URI in a model request does not refresh it. Reads refresh both the iteration age and least-recently-used order. Cached text is limited to 16 MiB of UTF-8 bytes per session, evicting the least recently read entry first; individually larger results receive a preview without a URI. Shutdown, history reset, and restore clear the cache. Resume and copied sessions do not rehydrate it from history. Missing entries return an explicit refetch instruction through read_files, without automatically repeating tools or side effects.

Cache eviction releases cache-owned text only; original output remains under ordinary history retention. URI references survive eviction so earlier model-facing recovery notices remain unchanged. Cache-miss feedback is emitted only by an attempted read. Generated recovery instructions reserve at most half the aggregate model text budget; instructions that cannot fit are omitted. Tool-returned metadata cannot grant recovery-instruction status.

Connector Persistence and Recovery

  1. @cline/shared/db owns the low-level SQLite connector store and the one-time legacy JSON import.
  2. Dashboard configuration and CLI connection state are recorded separately. Configuration edits replace only dashboard-owned connector and security flags in stored reconnect arguments, preserving CLI-only runtime options, and refresh arguments only for connectors that have previously started successfully.
  3. @cline/core owns connector autostart persistence and reconnect orchestration. The detached hub daemon is the sole startup reconnect owner, preventing dashboard startup from racing it and launching duplicate processes.
  4. Detached connector starts are persisted only after a child process is created. Internal detached children preserve that state when they exit, while a clean user-interactive exit disables autostart.
  5. CLI and dashboard hosts pass their connector CLI launch specification through the detached process environment. The package-owned daemon entrypoint uses that specification to start connector reconnect wrappers without importing application code.
  6. The detached hub entrypoint exposes hubDaemonReady, which resolves only after the WebSocket server is listening. It begins reconnect attempts after signaling readiness, and reconnect failures remain best-effort rather than taking down the hub.

Remote-Config Managed Runtime

  1. A host or core wrapper fetches a normalized RemoteConfigBundle.
  2. @cline/shared/remote-config caches the bundle when configured.
  3. Shared remote-config materializes managed rules/workflows/skills under workspace-local .cline/<plugin>/.
  4. Shared remote-config derives generic OpenTelemetry config and session blob upload metadata from the bundle.
  5. @cline/core exposes the app-facing integration wrapper that applies extensions, telemetry, and session metadata to StartSessionInput.
  6. @cline/core consumes the prepared local overrides during local bootstrap.

This keeps reusable remote-config behavior in shared while the session-specific bridge remains in core.

Design Seams

The codebase relies on a few repeated seams instead of one-off integration paths.

1. Config Watchers

Core uses file-based discovery and watchers for:

  • rules
  • workflows
  • skills
  • agents
  • hooks
  • plugins

Design implication:

  • new instruction sources should usually materialize into files and reuse watcher-based loading instead of inventing parallel in-memory execution paths.
  • in packages/core, config-facing discovery, parsing, watching, and slash-command projection live under src/extensions/config

2. Runtime Builder Inputs

DefaultRuntimeBuilder composes a runtime from generic inputs:

  • tools
  • hooks
  • extensions
  • user instruction watcher
  • telemetry

Design implication:

  • higher-level integrations should prefer feeding those seams rather than patching agent internals directly.
  • the local runtime bootstrap lives in packages/core/src/services/local-runtime-bootstrap.ts and feeds the builder rather than bypassing it

3. Runtime Host Boundary

Core exposes one shared execution boundary: RuntimeHost.

Concrete implementations:

  • LocalRuntimeHost for in-process execution
  • HubRuntimeHost for shared local hub execution
  • RemoteRuntimeHost for explicit remote hub endpoints

Design implication:

  • host selection happens in packages/core/src/runtime/host.ts
  • ClineCore delegates uniformly to RuntimeHost and does not branch on local vs hub behavior
  • transport-specific translation belongs inside concrete hosts, not in top-level orchestration
  • RuntimeHost inputs stay transport-safe, while ClineCore.start(...) is the app-facing facade that normalizes broad local config before delegation
  • RuntimeSessionConfig is transport-neutral across local, shared hub, and remote hub modes; host-local bootstrap concerns stay under localRuntime
  • client-local runtime behaviors that must survive hub mode, such as defaultToolExecutors, are attached at session start and proxied through hub capability requests instead of changing host selection
  • pending prompt list/update/delete are exposed through the grouped ClineCore.pendingPrompts service. Usage summary lookup and active-session model switching are also service-style capabilities exposed through ClineCore when the concrete transport implements them. These service APIs are intentionally outside the minimal RuntimeHost primitive vocabulary.
  • session.abort remains the cancellation boundary for a root session in both local and hub-backed execution. The owning LocalRuntimeHost aborts the lead agent and asks only that session's team runtime to cancel active synchronous teammate work plus running or queued async runs. Teammate definitions and conversation state remain available for later turns; idle and unrelated team runtimes are not stopped. One-off spawn_agent delegations observe the parent turn's abort signal through their SessionRuntime. The team runtime marks intentional abort task-end events as cancelled so persistence does not record them as failures.
  • The usage service's getAccumulatedUsage(sessionId) method returns a summary with two explicit buckets: usage for the root/lead agent and aggregateUsage for root plus teammates/subagents. Local execution tracks root usage and teammate usage as separate buckets, then derives aggregate totals from those buckets while telemetry remains scoped to the primary lead/root agent.
  • Usage events report non-reasoning outputTokens and a separate optional reasoningTokenCount delta. RuntimeEventAdapter derives both deltas from cumulative runtime usage; the hub's usage.updated.delta payload preserves them when HubRuntimeHost reconstructs the event. task.tokens telemetry likewise reports reasoning separately from tokensOut. Provider billing still includes reasoning tokens at the output rate.

4. Settings Mutation Boundary

Core owns settings snapshots and mutations through packages/core/src/settings. The hub exposes the same path through settings.list and settings.toggle.

Design implication:

  • hosts should not mutate skill, tool, MCP, provider, or other settings files directly
  • domain-specific persistence helpers, such as skill markdown frontmatter writes, stay internal to the owning settings provider/service
  • successful hub-backed mutations return an updated settings snapshot and publish settings.changed with the changed settings types
  • CLI settings surfaces may keep local snapshot rendering for startup responsiveness, but mutation flow must refresh the relevant watcher before reloading UI data

5. Session Startup Bootstrap

ClineCore.create(...) exposes a generic prepare(input) hook.

Design implication:

  • higher-level packages can prepare workspace-scoped runtime state before a session starts
  • core stays unaware of enterprise-specific contracts
  • cleanup stays at the host boundary rather than inside the agent loop

6. Logging

Cross-package logging uses a small injected interface exported from @cline/shared:

  • BasicLogger — required debug and log; optional error. Hosts map these to their backend (Pino, VS Code OutputChannel, etc.). Many runtime options take logger?: BasicLogger; when omitted, components skip logging or use noopBasicLogger where a full object is required.
  • BasicLogMetadata — optional structured fields (sessionId, runId, providerId, toolName, durationMs, …) plus severity on log when a single method must represent both informational and warning-style messages (for example the CLI Pino bridge maps severity: "warn" to Pino warn).

Naming clarity:

  • CliLoggerAdapter (CLI) — a host bundle: holds the raw pino logger (for file paths, rotation, and CLI-only concerns) and exposes .core: BasicLogger for anything that consumes the SDK contract. It is not an ITelemetryAdapter.
  • TelemetryLoggerSink (@cline/core) — an ITelemetryAdapter that mirrors telemetry events and metrics into a BasicLogger. It is a telemetry sink, not a host logging implementation.

The agent and other call sites route former info / warn semantics through log (warnings include severity: "warn" in metadata). Errors prefer error when implemented; otherwise log with severity: "error" is used as a fallback.

Design implication:

  • logging is injectable and transport-agnostic, allowing host environments (CLI, VS Code, browser) to wire their own backends
  • do not hardcode logging calls; accept a logger?: BasicLogger parameter instead

6.1 Langfuse telemetry ownership

@cline/llms owns Langfuse instrumentation, trace attribute propagation, and the LangfuseAttributesSpanProcessor. @cline/core owns the host OpenTelemetry provider and installs that processor when constructing an OTLP trace pipeline. The processor copies context attributes onto cline-provider-langfuse spans; it does not create an exporter or require Langfuse credentials. User and session IDs must be span attributes, not only observation metadata, for Langfuse tracking.

In the collector relay path, spans use the existing host OTLP exporter. The collector owns Langfuse credentials and downstream filtering and sampling. The relay exists only when the host registered a traces exporter, which release builds inline at build time (OTEL_TRACES_EXPORTER, CLINE_TRACE_*) into the CLI, the extension, and the desktop backend and its bundled CLI; a packaged app has no runtime environment to read them from. The --telemetry-selfcheck report includes traces_exporter so publish workflows can gate on it. llms checks provider eligibility, client sampling, opt-out, and content policy per request. The host owns flushing and shutting down its provider.

When no host relay is available, explicitly configured direct Langfuse export uses an isolated provider owned and disposed by llms. It does not replace or shut down an ambient host provider. When both routes are configured, the relay takes precedence so a request is not exported through both routes.

Each issued agent model request gets a modelRequestId, shared by request-start and stream lifecycle events, provider error logs, and AI SDK runtime context. Transport retries within one AI SDK call share that ID; a new agent request gets a new ID, even within the same run and iteration. Provider errors also include run, agent, session, and conversation IDs when available. The AI SDK callId is recorded separately as aiSdkCallId on errors.

Terminal stream failures notify both application error reporting and tracing integrations. Native timeout aborts are failures; explicit caller cancellation does not emit a provider sdk.error. AI SDK stream recovery permits three retries per model step. Recovered attempts do not produce terminal sdk.error reports or mark the operation as failed. Exhausted stream recovery disables the agent's whole-turn retry so the two retry budgets do not multiply. Previously streamed text and reasoning follow AI SDK's append semantics and may repeat on recovery.

7. Storage Adapters

Stateful persistence should be isolated behind adapter/service layers.

Design implication:

  • file-backed, SQLite-backed, RPC-backed, and enterprise-specific persistence should share service logic where possible and isolate backend differences in adapters.

8. Extension and Hook System

Extensibility is split deliberately:

  • extensions register runtime contributions
  • hooks intercept lifecycle stages

Design implication:

  • additive runtime behavior should usually enter through these extension points instead of bespoke special-case host code.

9. Context Compaction

Context compaction is owned by core.

  • @cline/agents owns the generic turn-preparation seam:
    • run normal lifecycle hooks
    • allow hosts to project message history or system prompt before the provider call
    • keep its canonical runtime transcript append-only when a projection is returned
  • @cline/core owns compaction policy:
    • inject a prepare-turn pipeline for root sessions
    • choose between built-in strategies through a registry map
    • persist the latest compacted working context as a session compaction artifact
    • keep compaction logic out of the low-level agent message builder

Design implications:

  • compaction is a context-pipeline concern owned by core
  • canonical session history lives in the session messages artifact at full fidelity; compaction state lives separately in ${sessionId}.compaction.json
  • resume loads the canonical transcript for history/debugging and, when present, reuses the latest compaction state only after validating a hash of the canonical prefix covered by that state; valid state is projected by appending canonical messages written after the compaction boundary
  • sessions that were already persisted with compacted messages before this model are best-effort only because the omitted original transcript is not recoverable from the compacted artifact
  • a session imported from another coding agent (metadata.importedFrom) that is resumed without compaction state summarizes its whole foreign transcript on the first turn, regardless of the auto-compaction setting; the summary persists as normal compaction state, so the model never replays the source agent's tool calls while the canonical transcript stays intact
  • agents stays focused on the stateless loop and provider/tool orchestration
  • delegated/subagent flows should inherit compaction behavior through core session config, not through a separate agent-level compaction hook surface

10. Extension Layering Inside Core

packages/core/src/extensions is split by concern:

  • extensions/config: config loaders, parsers, watchers, and watcher projections such as runtime slash-command expansion
  • extensions/plugin: runtime plugin discovery, loading, the plugin registry, the worker/subprocess sandbox transports, and their shared bootstrap
  • extensions/context: core-owned context/message pipeline concerns such as compaction

Design implications:

  • avoid mixing config discovery code into runtime/plugin code
  • avoid creating thin runtime wrapper files when a helper is fundamentally projecting watcher state

Plugin execution

Plugin code never runs on a host's main thread. Every plugin a session uses goes through one PluginRegistry per process (getProcessPluginRegistry(), extensions/plugin/plugin-registry.ts). The registry owns status, failure counting, timeouts, the session policy, and reload; the execution mode (CLINE_PLUGIN_MODE, resolvePluginExecutionMode) only decides which isolated transport a session's copy of a plugin runs in:

  • worker (default): each plugin runs in its own worker thread per session (WorkerSandbox), so the host never imports plugin code. The copy the registry wraps is a proxy whose calls go to the worker over the sandbox RPC protocol (plugin-sandbox.ts / plugin-sandbox-bootstrap.ts). One worker per plugin means a worker that dies on its own (uncaught error, unhandled rejection, process.exit()) is attributed to that plugin with no stack inspection: the registry records an uncaught failure and turns the plugin off, and the host and other plugins keep running. A call that times out terminates its worker, which stops synchronous loops; the next call starts a fresh worker. Released copies end their worker, which stops everything the plugin started.
  • sandbox: the previous per-session node/bun subprocess. It bypasses the registry (no status, policy, or reload) and is kept as an escape hatch.

Plugins written for the per-session sandbox keep state at module level, often one plugin object per import that refuses a second setup(), so both modes give each session its own copy. The session bootstrap, plugin tool listing (listPluginToolsWithDiagnostics), plugin MCP settings sync, and plugin slash commands all load through the registry, so they see the same mode. Worker mode starts no child process, so plugins work in the compiled CLI, the Hub it starts, the desktop app, the SSH remote host, and VS Code/Electron without a runtime on PATH, and plugins can use the host runtime's globals (Bun under the Bun-built Hub, including inside a Bun worker).

In the Hub daemon the registry is Hub-owned: the daemon checks global and workspace plugins at startup in throwaway workers so status is known before any session exists.

Each session gets its own guarded view of the plugins it enabled:

  • StartSessionInput.plugins (session.create payload plugins) is shaped like toolPolicies: "*" sets the default and named entries override it. Plugins are enabled unless a policy turns them off. Tools, hooks, rules, and providers come only from enabled plugins; plugin tools still go through toolPolicies.
  • setup() runs once per session with a context that carries the session's cwd, session.sessionId, and emitEvent. Plugin tools also receive cwd and emitEvent in their call context. globalThis.__clinePluginHost.emitEvent remains as a shim for sandbox-era plugins and routes to the calling session through async context.
  • Every import (4 s), setup (4 s), hook (3 s), and tool, command, rule content, and message-builder call (60 s, or a tool's own timeoutMs) is wrapped with a timeout and error capture. resolveAndLoadAgentPlugins overrides (importTimeoutMs, hookTimeoutMs, contributionTimeoutMs) apply per session. A failing tool or command throws its error back to the caller. A failing hook follows hookErrorMode ("ignore" by default; "throw" rethrows). A failing rule contributes no text, and a failing message builder leaves the messages unchanged. Calls into a copy that is turned off, whose setup failed, or whose session ended are refused.
  • Registrations are buffered (and capability-checked) during setup, so a setup that throws contributes nothing. If the host rejects a registration while committing, the copy's setup is recorded as failed and the error is rethrown so the contribution registry discards what was already committed.
  • Each copy owns its cleanup. ctx.onDispose(fn) registers cleanup that runs when the session releases the copy (session stop, dispose, or a failed start). Plugins must stop everything they start for a session there: timers, child processes, sockets, listeners on shared emitters. The host does not track or cancel them.

The registry tracks one status per plugin: loading, ready, degraded, failed, or disabled. It records the last error with its phase (discover, import, setup, dispose, hook:<name>, tool:<name>, command:<name>, rule:<id>, messageBuilder:<name>, or uncaught), message, stack, plugin path, timestamp, and session, plus error and timeout counts.

Plugins are bound to a session when it starts. A plugin that is added, changed, or reloaded reaches only sessions started afterwards; a running session keeps the plugins, and the plugin code, it started with, even if its setup() first runs on a later turn. Restarting a session (a CLI mode switch, resuming it, or the Hub rebuilding a missing session) is a new start and gets the current plugins. A change is detected by a content fingerprint of the plugin's entry file and every file it reaches through relative static imports (fingerprintPluginSources), so edits outside the entry file count too. Files reached only through dynamic imports, require() of computed paths, or node_modules are not fingerprinted; after changing those, use plugins.reload.

Failures are counted per plugin. Import failures, discovery failures, an attributed uncaught error, and five consecutive call failures turn the plugin off for every session using it; a re-import (plugins.reload or a fingerprint change) starts over with a clean slate. Running sessions keep the copy they set up, which stays callable unless the plugin is turned off (a broken reload fails closed for them too). Each copy is stamped with the import it came from; a failure in a copy from before a re-import is reported to its own session but does not count against, or change the status of, the current module. A setup() failure is per session: that session loses its copy (no tools or hooks), the status shows failed with the error, only that session is told, other sessions keep their working copies, and the next session tries setup again; a successful setup returns the status to ready.

Status is surfaced, never only logged:

  • plugins.list / plugins.status return each plugin's status, last error, and the sessions using it; plugins.reload re-imports one plugin by name or path without restarting the Hub. cline hub plugins [--reload <plugin>] wraps these.
  • The Hub broadcasts plugin.status_changed to attached clients.
  • session.create replies and session.created events carry pluginIssues for plugins the session asked for but does not have: failed, disabled in settings (reason: "settings"), or turned off by the session's policy (reason: "session_policy"). StartSessionResult.pluginIssues exposes the same list to SDK hosts.
  • Because setup() and hooks run on the first turn, after the start reply, later degraded/failed transitions reach each affected session as a status notice (AgentNoticeEvent with metadata.pluginIssue).
  • Registry logs are structured lines with pluginName, phase, and sessionId; in the Hub they go through logHubMessage.
  • Settings listings (listPluginToolsWithDiagnostics, CoreSettingsService) report failed plugins with their state and loadError. Their inspection cache is keyed on each plugin's registry status, so a plugin a running session turned off is never answered from an earlier clean inspection.

Because no plugin code runs on the daemon's thread, its uncaughtException / unhandledRejection handlers treat every error as the daemon's own and go to shutdownFatal; there is no stack attribution to plugins.

Worker and subprocess transports

Both transports share one RPC protocol and one bootstrap (plugin-sandbox-bootstrap.ts). After SDK compilation, Core builds dist/plugin-runtime/, containing the self-contained bootstrap and the complete SDK import graph under node_modules/@cline. Package exports come from the SDK manifests, including browser subpaths. Hosts ship this directory as one artifact:

  • WorkerSandbox (worker mode): structured clone over the worker's message port. A worker must not chdir (Bun would change the host's cwd), so the bootstrap overrides process.cwd() inside the worker. The bootstrap posts ready once it listens, and the host queues calls (as clones) until then, because Bun drops messages that arrive earlier. Plugin stdout is discarded inside the worker, since Bun workers write straight to the host's stdout. Workers share the host's memory limit, env snapshot, and native addons: crash isolation, not a security boundary.
  • SubprocessSandbox (sandbox mode): JSON IPC to a node/bun child with stdout ignored.

Core resolves the complete artifact through CLINE_PLUGIN_RUNTIME_DIR, a package-local plugin-runtime directory, or embedded resources. Source hosts prefer their sibling TypeScript bootstrap over packaged runtimes so development and tests execute the current bootstrap and module importer without rebuilding. The CLI compiler embeds the runtime tree and dashboard assets in Bun's read-only filesystem. Worker and subprocess transports read the same embedded bootstrap and SDK exports without extracting files or creating a runtime cache. VS Code and npm CLI copy the complete artifact into their bundles. Desktop ships it beside its external backend script, whose plain Bun host does not mount the CLI's filesystem. A host without a bootstrap fails closed with PluginBootstrapMissingError; plugin code never runs on the host main thread.

Compiled hosts register their subprocess entrypoint with setPluginSandboxLauncher. The CLI and Desktop’s bundled CLI use the internal --plugin-sandbox flag, which lazily imports the embedded bootstrap with bunfs mounted and compiled runtime flags (including system CA trust) intact. Generic script sandboxes still use plain Bun mode when reusing a compiled runtime.

LocalRuntimeHost.pluginCommands owns plugin command discovery and execution, exposed to clients through Hub RPC (plugins.commands.list / plugins.commands.run). The desktop sidecar, CLI, and connectors reuse their existing runtime connections and do not load plugins themselves. Plugin command execution can recover a connection before dispatch, but never resends a dispatched action whose reply was lost.

A resident session lists and executes commands from its own contribution registry, retaining the plugin instance and context shared with its hooks. Session registrations keep their session lifetime. Without a resident session, the runtime uses a per-workspace catalog. Each request fingerprints the enabled discoverable plugin entries and their imported source graph, and reloads the catalog when that changes; other settings, setup logs, and state files beside a plugin do not. Each load uses its own PluginRegistry, so a catalog import failure cannot block a resident session's copy of the same plugin, and the retry re-imports it. A failed load keeps healthy commands, reports status: "error", and is retried by a request after 30 seconds. Catalogs idle for 10 minutes release their workers. Reload, eviction, and disposal wait for running handlers. Clients re-list to stay current (for example when a slash menu opens or the active session changes); there is no catalog push event.

The shared command executor records task.tool_used once per matched handler, including failures, for both direct runtime and Hub calls. Telemetry failures cannot replace a command's result or original error. Hub status and replacement checks count active command requests even without a chat participant; draining refuses new catalog and command requests. CLI and desktop fallbacks treat a failed catalog read as "not a plugin command" so skills and workflows still work, while errors from a listed plugin's handler remain visible.

Sandboxed plugin subprocesses and workers are session-local but lazily recreatable. Core reclaims a sandbox after 30 minutes without an in-flight RPC call (configurable through PluginSandboxOptions.idleTimeoutMs or CLINE_PLUGIN_IDLE_TIMEOUT_MS), and the next plugin call starts and reinitializes it transparently. Pending requests are associated with the child generation that owns them so an old process exiting cannot reject work sent to its replacement. The bootstrap also exits when its parent IPC channel disconnects. The parent is the single authority for idle shutdown so competing deadlines cannot terminate a child while the parent is dispatching new work.

Design implications:

  • worker and subprocess counts scale with recently active sessions (workers: sessions × plugins), not every session observed since hub startup
  • eviction never interrupts an in-flight plugin call
  • sandboxed plugin state is ephemeral across idle eviction; durable plugin state belongs in persistent storage
  • a sandbox must never outlive its owning hub process

Architectural Constraints

Keep agents Stateless

Do not move these concerns into @cline/agents:

  • session persistence
  • provider settings storage
  • RPC lifecycle
  • host-specific approvals
  • remote-config policy caching

Keep core Generic

Do not make @cline/core organization- or provider-specific.

If a capability is truly generic and app-facing, add a generic core seam. Reusable remote-config parsing, materialization, and upload primitives belong in @cline/shared/remote-config.

Use One-Way Optional Layers

Optional higher-level integrations may depend on lower layers. Lower layers should not depend on optional feature packages.

For remote config, that means shared owns the reusable bundle/materialization/blob primitives and core owns only the session-oriented wrapper exported to apps.

Hub-Owned Agenda Task Queue

Agenda tasks are durable proposals for future work. They are intentionally separate from cron specs, queued prompts inside an existing session, and the agent-team task board. Shared, browser-safe contracts use AgendaTaskRecord and AgendaTaskRunRecord; orchestration and persistence remain in @cline/core.

Status: the agent-facing kind: "todo" half of the tasks tool and the desktop Agenda UI are temporarily disabled while the Agenda UX is reworked (AGENDA_TODO_TOOL_ENABLED in hub-server-transport.ts and AGENDA_UI_ENABLED in the desktop webview). While the flag is off the Hub also skips the agenda spec-file watchers — nothing consumes watcher-driven task events, and task.* commands reconcile spec files on demand. The backend described below — the manager, storage, task.* Hub commands, and desktop plumbing — stays fully wired, and the schedule kind remains active.

Authority and persistence

  • A Hub process owns one Agenda task manager. Its AgendaTaskManagerApi boundary is the only place allowed to change task lifecycle, approval, run, or session-link state. Hub commands, file import, and the agent tool all route mutations through that boundary.
  • User-editable intent is represented as Markdown with YAML frontmatter in ~/.cline/tasks/*.task.md for global tasks and <workspace>/.cline/tasks/*.task.md for workspace tasks. Operational fields such as status, revision, approval, and session IDs are not writable in a spec. AgendaTaskSpecFileStore confines paths to the selected task directory and writes specs atomically.
  • SqliteAgendaTaskStore owns tasks.db (resolved by resolveTasksDbPath()). SQLite is the operational source of truth for task status, revisions, run attempts, session links, and the automation policy; the Markdown file is the canonical editable task description, not an append-only queue or a substitute for concurrency control.
  • Task-file parsing and reconciliation must feed the manager instead of writing SQLite directly. This preserves one validation and audit boundary whether a change came from an editor, desktop client, SDK client, or agent.
  • Manager startup scans global task specs, recovers persisted task/run state, and reattaches every known workspace whose directory still exists. Missing historical workspaces are preserved in SQLite without recreating project directories. Selecting/listing a workspace registers it so even its first hand-authored task file is discovered and watched. Filesystem changes are reconciled back through the same manager rather than applied from watcher callbacks.
  • Raw-file creation and edits are attributed to system:file_reconciler and always leave changed intent awaiting manual task approval. File edits never reopen completed, cancelled, or expired tasks; terminal records remain terminal and retain their last-known-good operational state.

Approval and session execution

  • Every new task starts at pending_approval. Approval is bound to the exact task revision through approvedRevision; an execution-relevant edit increments the revision and revokes stale approval. An approved revision is therefore immutable at the point it is claimed for execution.
  • Approval and execution synchronously reconcile the backing Markdown file to close the watcher debounce window. Both fail closed if that canonical spec is missing, malformed, or does not semantically match the SQLite revision; stale last-known-good intent is never approved or executed.
  • Hub task.create and task.update payloads omit actor fields; the command service derives the user actor from the calling Hub client. Update, approval, cancellation, and run requests carry the caller's displayed expectedRevision; specifically, task.approve, task.cancel, and task.run reject a missing or stale revision.
  • P0 is the most urgent priority and P5 is the least urgent. expiresAt is a required latest-start boundary: expiry prevents a new run but does not abort a session that already started.
  • Starting a task creates an AgendaTaskRunRecord and a normal Hub session. The run owns the task/revision/session association, while the task keeps currentRunId, lastRunId, and lastSessionId projections for UI lookup. A global task remains globally scoped and resolves the Hub's shared chat workspace only when its session starts.
  • The manager correlates Hub session completion, failure, and cancellation with the linked run and updates both run and task. Startup recovery follows the same boundary rather than manufacturing a second run. A completed task does not own or delete its session; the linked session remains normal session history that a user can reopen.

Hub, agent, and desktop surfaces

  • The Hub command family is task.create, task.list, task.get, task.update, task.approve, task.cancel, task.run, task.automation.get, and task.automation.set. Registered task events are task.created, task.updated, task.deleted, task.run.started, task.run.completed, task.run.failed, and task.automation.updated. Events are invalidation and lifecycle signals; clients re-read the current task or policy projection rather than rebuilding authority state from the event stream.
  • Hub-hosted agent sessions receive one snake-case tasks tool with a required kind discriminator. kind: "todo" creates, updates, lists, and gets durable Agenda items within the current session's workspace (or the global scope for chat sessions). It cannot approve, cancel, or start a Todo, so an agent cannot self-authorize or terminate queue work.
  • kind: "scheduled" routes to HubScheduleService and manages the same records shown by the desktop Routine view; the unified tool does not merge the two persistence or lifecycle domains. Scheduled operations inherit the current session workspace, cwd, and model, filter reads and mutations to that workspace, and allow mutations only from an interactive session. One-time schedules accept an exact future ISO timestamp; recurring schedules accept a five-field cron expression and optional IANA timezone.
  • The Hub contributes one tool-conditional system-prompt rule for tasks. It distinguishes reviewed Todo work from autonomous scheduled execution, states that Todo available_at is not a timer, requires clarification for ambiguous “remind me” requests, and prevents creating both record kinds unless the user explicitly requests both.
  • AgendaAutomationPolicy is user-owned Hub state. manual preserves the per-task review gate; auto_start and unattended are explicit opt-ins. The manager's automation pump enforces the policy's concurrency, chain-depth, and hourly-start guardrails. auto_start waits for a connected client with the tool-approval capability and starts an interactive task session that conservatively requires approval for each tool. Explicit unattended mode may run headlessly and auto-approves enabled tools.
  • Automation never infers user consent from a raw task-file change. For manager-backed intent, applyToAgentCreated governs tasks originally created by an agent and tasks whose latest manager-backed edit came from an agent; disabling it leaves those revisions pending manual approval.
  • Cline Code projects the same Hub state into an Agenda section in the desktop sidebar and workspace-filtered suggestion/reminder quick actions below the welcome composer. The sidebar supports review, start, cancellation, linked-session navigation, and the automation toggle; it does not maintain a second task store.

File-Based And Event-Driven Automation (ClineCore / CronService)

@cline/core ships a file-based automation subsystem under packages/core/src/cron/. It lets operators author recurring and one-off tasks as Markdown files under global ~/.cline/cron/ by default, and event-driven tasks as events/*.event.md specs. All trigger kinds run through the same durable queue and runtime handlers. ClineCore exposes the SDK-facing cline.automation.* entry points; CronService is the internal orchestrator used by core and hub layers.

Layers

  1. Spec parser (cron/specs/cron-spec-parser.ts): parses YAML frontmatter + body into a CronSpec discriminated union (one_off | schedule | event). Types live in @cline/shared under src/cron/cron-spec-types.ts so other packages can consume them without the YAML parser. Schedule expressions and timezones are validated before a spec can become runnable.

  2. Store (cron/store/sqlite-cron-store.ts): owns cron.db at resolveCronDbPath() (default .cline/data/db/cron.db). Schema is bootstrapped from cron/store/cron-schema.ts — sessions and cron live in separate DBs so their lifecycles stay decoupled.

  3. Reconciler (cron/specs/cron-reconciler.ts): scans the configured cron specs directory (global ~/.cline/cron/ by default, or workspace-scoped when configured), parses each file independently, and upserts spec state. Invalid specs are recorded with parse_status='invalid' so state is durable rather than silently dropped. Files that disappear between scans get removed=1 and their queued runs are cancelled.

  4. Watcher (cron/specs/cron-watcher.ts): node:fs watch({ recursive: true }) with a ~250ms per-path debounce. Watcher events always trigger a re-reconcile — the reconciler is always the source of truth, not the watcher stream.

  5. Materializer (cron/runner/cron-materializer.ts): turns file-triggered specs into queued cron_runs. One-off: at most one run record per (spec_id, revision), including failed runs so specs do not retry accidentally. Schedule: "one overdue catch-up on startup then advance" using timezone-aware getNextCronTime. New hub schedules persist the local IANA timezone when none is provided. The desktop form sends its own local timezone; explicit timezone choices and existing schedule timezones are preserved.

  6. Event ingress (cron/events/cron-event-ingress.ts): accepts already-normalized AutomationEventEnvelope values, persists them into cron_event_log, matches enabled event specs by event_type plus declarative filters, applies dedupe/debounce/cooldown policy, and enqueues cron_runs with trigger_kind='event'. It never executes agents directly. Plugins can declare automationEvents and submit normalized events through ctx.automation.ingestEvent(...); sandboxed plugins forward those events through the core plugin event bridge. Acceptance is one synchronous SQLite write transaction: the event log, matching runs, debounce changes, materialization pointers, and final processing status commit together. A failure rolls all of them back and propagates to the caller, which must redeliver the event to retry. Other connections cannot observe or claim partial fan-out. Committed events remain deduplicated by event ID, including a retry after a lost response. Failures are logged outside the transaction, not persisted as deduplication tombstones.

  7. Runner (cron/runner/cron-runner.ts): polls cron.db, atomically claims queued runs, executes them via the existing HubScheduleRuntimeHandlers (startSession → sendSession → stopSession / abortSession), dispatches new work independently of unfinished agent turns, and renews locally active claims before polling expired work after system sleep. Startup installs polling without waiting for the initial batch to finish. It also renews the run claim while execution is active, writes a markdown report per run, and transactionally updates status. Optional scheduler telemetry records run start/finish, trigger kind, attempt count, start delay, duration, and outcome through the normal telemetry service. It excludes prompts, paths, and raw errors; capture failures do not interrupt execution. File specs can constrain tool availability, config extension loading (rules, skills, plugins), trigger source, and a notes directory that is injected into the system prompt. The automation runtime adapters explicitly persist mode: "automation" for every run and record the spec-defined trigger source as sessionHistoryOrigin.trigger in session metadata. Event runs include the normalized trigger event context in the prompt.

  8. Reports (cron/reports/cron-report-writer.ts): writes .cline/cron/reports/<run-id>.md with run frontmatter plus ## Summary, ## Usage, ## Tool Calls, and, for event runs, ## Trigger Event sections.

  9. Service (cron/service/cron-service.ts): orchestrates all of the above. ClineCore.create({ automation }) owns the SDK-facing lifecycle and exposes cline.automation.* methods. Hub-side callers can submit normalized events through the cron.event.ingest command.

The detached hub daemon passes its workspace root as cronOptions, so normal CLI/hub startup watches ${workspaceRoot}/.cline/cron/ without a custom host needing to opt in.

Programmatic hub schedules are stored as cron_specs with source hub-schedule and execute through the same cron_runs claim/requeue/report flow as file-backed one-off, recurring, and event-driven specs. The hub schedule command surface remains a thin adapter; there is no separate schedules table, schedule store, or schedule runner.

Runner claims enforce global and per-spec capacity inside the SQLite claim transaction, skipping saturated specs before choosing the next due run. This keeps multiple database connections from exceeding a schedule's parallelism and prevents a backlog of blocked siblings from starving other schedules. Each execution has a cancellation controller and a deadline covering request preparation, session startup, and the turn. Shutdown cancels and drains tracked executions with bounded session cleanup; interrupted runs are recorded as cancelled, not automatically replayed after possibly performing external work. Late startup responses are cleaned up without sending a turn or touching the closed store. Losing a lease cancels the old execution, and attaching its session requires the current claim token. Terminal status is persisted before writing the optional report, so a report filesystem failure cannot replay a completed turn. Report and cleanup failures are logged separately.

Navigating the Codebase

Starting Points by Task

I want to understand the agent loop and tool execution:

  • Start: packages/agents/src/agent.ts — the stateless runtime loop
  • Then: packages/agents/src/agent-step.ts — individual iteration steps
  • Extensions: packages/core/src/extensions/plugin/ — plugin discovery, the registry, and the worker/subprocess transports

I want to understand session persistence and state:

  • Start: packages/core/src/runtime/host/local-runtime-host.ts — local session lifecycle
  • Then: packages/core/src/runtime/orchestration/ — session orchestration
  • Settings: packages/core/src/settings/ — settings mutation and state

I want to understand the hub system:

  • Start: packages/core/src/hub/server/ — WebSocket server and hub command handlers
  • Clients: packages/core/src/hub/client/ — host-side hub clients
  • Transport: packages/core/src/hub/runtime-host/ — hub-backed runtime hosts

I want to add a new tool:

  • Tools registry: packages/core/src/extensions/tools/ — built-in tool definitions
  • Tool execution: packages/agents/src/tool-use.ts — how tools are called
  • Plugin tools: packages/core/src/extensions/plugin/ — plugin-registered tools

I want to understand settings and configuration:

  • Watcher system: packages/core/src/extensions/config/ — file watching and loading
  • Provider config: packages/core/src/runtime/config/ — provider settings resolution
  • Settings services: packages/core/src/settings/ — settings state and mutation

I want to add a new runtime feature (hook/extension):

  • Hook contracts: packages/shared/src/hooks/ — hook types and engine
  • Plugin system: packages/core/src/extensions/plugin/ — plugin discovery and execution
  • Runtime builder: packages/core/src/services/local-runtime-bootstrap.ts — how runtime is composed

File Naming Conventions

  • *.ts — TypeScript source
  • *.test.ts — unit tests (Vitest)
  • *.e2e.test.ts — end-to-end tests requiring full integration
  • *.ts in examples — runnable example files (plugins, hooks)
  • *.md files in apps/examples/ — documentation and markdown-based specs (cron, events)

Key Type Locations

  • ClineCore — packages/core/src/index.ts — the main SDK orchestrator
  • Agent — packages/agents/src/agent.ts — the agent loop
  • RuntimeHost — packages/core/src/runtime/host/runtime-host.ts — execution abstraction
  • AgentPlugin — packages/shared/src/plugin/ — plugin contract
  • CronSpec — packages/shared/src/cron/cron-spec-types.ts — automation specs

Publishability Constraint

This repo has both publishable SDK packages and internal workspace packages.

Architectural consequence:

  • internal packages must not accidentally become part of the publishable SDK surface
  • release automation should only target the intended published packages
  • internal code may compose with published packages, but published packages should not take hard dependencies on internal-only workspace layers unless you explicitly intend to publish that integration

Published Packages

The following packages are published to npm:

  • @cline/shared — shared types, contracts, and low-level utilities
  • @cline/llms — provider integrations and model manifests
  • @cline/agents — the agent loop and tool orchestration
  • @cline/core — the main SDK with session management, hub, and configuration

Internal Apps

The following workspace apps are internal and not published as SDK packages:

  • apps/cli — CLI implementation
  • apps/webview — VS Code webview
  • apps/examples — example plugins and integrations

Display-only session errors

Terminal run errors are persisted in session message history with metadata.displayOnly: true and metadata.displayRole: "error". Desktop and CLI render these entries on history reload. The core message codec excludes them from agent state (including model requests and compaction); the conversation store retains their transcript positions when replacing agent snapshots.

Display-only failures are recorded once after automatic authentication retries settle; recovered attempts do not emit a terminal error. The session.error_recorded telemetry event reports session ID, provider, model, and whether the terminal failure returned or threw, without error/transcript text. Desktop reconciliation retains the full live failed turn until a saved terminal error reaches its user-run count and is not a previously displayed error ID.

Host connector transport

LocalRuntimeHost and createLocalHubScheduleRuntimeHandlers accept an optional connectorsRequest callback for discovery and execution during startup and resume. It supplies host authentication without reading local connector credentials or state. Without it, the local behavior described below is unchanged.

Composio beta access

Composio management in the desktop sidecar and tool registration/execution in local runtimes (including the detached hub) require the account-scoped PostHog flag CLINE_COMPOSIO_BETA to be exactly true. The shared core account flag evaluator reads the current Cline account ID from provider settings, caches the evaluation in memory for one minute, and discards grants on account changes. Missing identity, provider configuration, or flag values deny access; internal email domains do not bypass this gate. Saved connector schemas alone cannot enable tools. Existing sessions recheck access before each tool execution. Disconnect/cancel cleanup remains available after access is removed. The Cline API proxy must enforce the same flag server-side for authenticated requests.

The connector client uses /api/v1/connectors with the Cline { success, data } envelope. The sidecar fetches every catalog and connection page, including empty pages with continuation tokens, and rejects failed, malformed, or cyclic pagination before caching or reconciliation. Disabled accounts (is_disabled) are excluded. Connection state stores IDs, timestamps, and display metadata; connection and status-refresh flows never fetch or persist app tool schemas. The connector dialog uses the catalog tool count when known.

At local session bootstrap, core creates a server-owned tool-router session and registers only COMPOSIO_SEARCH_TOOLS, COMPOSIO_MANAGE_CONNECTIONS, COMPOSIO_WAIT_FOR_CONNECTIONS, and COMPOSIO_MULTI_EXECUTE_TOOL. The model searches for relevant app tools, receives their schemas in search results, and executes them through multi-execute without registering app tools in subsequent model requests. The flow works without saved connections and can connect apps within the same session. Missing or failed meta-session creation leaves connector tools unavailable; saved app schemas never provide a fallback. Execution uses /meta-tools/{slug}/execute with the session ID and arguments.

Customize > Connectors displays the usage-ranked catalog, with search across all loaded apps and installation/connection management in its detail dialog. The backend includes managed-auth toolkits before anyone has connected them, and provisions their auth configuration on first installation. This requires the full-catalog backend in core-platform PR #3383. Catalog responses must use the paginated contract; malformed responses are rejected.

Connector metadata and cancellation tombstones live in settings/composio/<sha256-account-id>.json. Each account has separate availability/catalog caches and pending operations. Async management requests retain their initiating account and refuse to send with another account's token. The core extension does not read this state file. It scopes meta-session creation to the signed-in account and checks identity again before registration and execution in an existing session.

RuntimeOAuthTokenManager serializes credential reads, refreshes, and saves using a SQLite exclusive transaction keyed by provider settings path and storage provider ID. This coordinates the sidecar, hub, and other local processes; OS locks release on process exit. Waiters reread persisted credentials under the lock and reuse a token another process refreshed, including for forced refresh requests. A refresh result is discarded if sign-out or sign-in replaced the credentials while the request was in flight.

Queue steering

Queue steering through pendingPrompts.steerFirst selects and promotes the current queue head in one synchronous core operation. Desktop Enter sends this intent through the sidecar and Hub without fetching a prompt ID first; explicit per-prompt steering continues to update by ID. Concurrent clients therefore cannot make Enter promote an entry from a stale queue snapshot.

SSH environments

core/src/remote owns the reusable SSH environment service and standalone remote helper entrypoint. Clients use RemoteEnvironmentService.connect to obtain an authenticated loopback endpoint, then instantiate the ordinary ClineCore remote backend. The helper uploads are content-addressed and the remote Hub binds only to loopback. SSH forwards that endpoint to a local ephemeral port. The helper's explicit discovery record is separate from the remote account's default Hub.

Desktop retains presentation, packaged-resource lookup, and its environment-to- runtime bindings. Settings and the chat environment selector call the shared service; each runtime binding supplies the same session/approval/event APIs. Workspace and session reads route by environment identity. System-prompt bootstrap happens on the remote host when the caller omits a prompt, so local filesystem metadata is not embedded in remote sessions. Login-shell PATH resolution also lives in core and is reused by the helper and desktop startup. The primary shell probe allows 5 seconds for slow profiles; a fallback shell gets half that budget, bounding the combined wait to 7.5 seconds.

Configured subagent approvals

Configured subagents execute their available tools without inheriting the parent session’s tool approval policies or approval callback, matching generic subagents and teammates. The parent’s subagent_<name> delegation call still follows the parent’s approval policy. Tool allowlists and disabled-tool filtering remain in effect when constructing child tools. Inherited runtime hooks are unchanged.