* 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
81 KiB
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:
sharedshould 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
coreor apps. - AI SDK response
X-Request-IDmetadata travels on the existing modelfinishevent intoafterModel.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: anendedevent 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 changesconfig.sessionId: doing so would turn a new session into a restart.afterModelstarts 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 aspartial, without dirty flags. The field-level contract isGitSnapshotPropertiesinpackages/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:
agentsshould 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-entrysubpath - 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:
coreis the app-facing orchestration layer overagents.session/fork-metadataowns fork ancestry and removal of inherited handoff markers; hosts retain title, transcript, and workspace-restore policies.@cline/core/cloudowns remote cloud-session state and emits immutable snapshots and events. Viewers hydrating active runs withreadMessagesreconcile 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/modelsowns cloud model eligibility;services/cloud-handoffowns 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 helpersdaemon/contains detached daemon startup, entrypoint, and local runtime handler wiringdiscovery/contains endpoint defaults, discovery records, and workspace owner resolutionserver/contains WebSocket server startup, native/browser socket adapters, server transport, server helpers, andhandlers/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 tosettings.changed.
Runtime Flows
Local In-Process Runtime
- Host constructs a
RuntimeHostthrough@cline/core. @cline/coreselectsLocalRuntimeHostthroughpackages/core/src/runtime/host.ts.- Hosts normalize broad local config into
RuntimeSessionConfigpluslocalRuntimeoverrides before callingRuntimeHost.start(...). @cline/coreprepares a local bootstrap artifact fromlocalRuntime, then builds the runtime from it.@cline/corecreates anAgentfrom@cline/agents.@cline/agentsruns the loop using@cline/llmshandlers.@cline/corepersists 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.
- Host constructs a
RuntimeHostthrough@cline/core. @cline/coreselectsHubRuntimeHostorRemoteRuntimeHostthroughpackages/core/src/runtime/host.ts.- When no compatible local hub is already discovered,
@cline/corecan 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'sNo compatible hub runtime is availableerror carries the underlying reason, and the daemon reports its own startup failure to telemetry (hub.daemon.startup) before exiting. - 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 stampsCLINE_HUB_LAUNCHER_CLIENT,CLINE_HUB_LAUNCHER_VERSION, andCLINE_HUB_LAUNCHER_TRANSPORTon the daemon environment (withHubLauncherIdentityin@cline/shared), defaulting to the spawning process'sClineClientIdentityandlocal. An embedding host that starts the hub through the CLI (the desktop app runningcline-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 theenvprefix of the remotehub ensurecommand withtransport=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 asresourceAttributes.*, so a desktop session hosted by a CLI-started hub is attributable to that CLI build. - The hub-hosted runtime executes the agent loop using
@cline/agentsand@cline/llms. @cline/corehub services broker sessions, events, approvals, schedules, and client-owned runtime capabilities such as session-local tool executors.- 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.startedis emitted only after the target session is resolved and carries the originating command'srequestIdandclientId, allowing multi-client hosts to correlate delivery acknowledgments. - 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;HubSessionClientpasses metadata through at registration so client events, refreshed lists, and reconnects retain the same details. - Hub
session.getrecords include both canonical root-session usage and explicit aggregate usage from the hub-ownedRuntimeHost, 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:
@cline/llmsvalidates provider output once and creates canonicalGeneratedMediavalues. 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.- 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.
@cline/agentsappends 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.@cline/coreprojects live media ascontent_end(media). The hub publishesassistant.media, preserving the same media ID, and clients deduplicate live and hydrated content by that ID.- Web clients share
GeneratedMediaContentfrom@cline/uifor 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
apps/cliowns OpenTUI startup and must render the first frame without waiting for detached hub startup.- 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. - Hub-required flows such as
cline hub, schedules, connectors, and--zenmay still call the explicit ensure path because those commands require a live hub before proceeding. - Resume hydration is deferred until after
renderOpenTui()so loading previous messages cannot block initial TUI paint. - 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
@cline/shared/dbowns the low-level SQLite connector store and the one-time legacy JSON import.- 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.
@cline/coreowns 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.- 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.
- 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.
- 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
- A host or core wrapper fetches a normalized
RemoteConfigBundle. @cline/shared/remote-configcaches the bundle when configured.- Shared remote-config materializes managed rules/workflows/skills under workspace-local
.cline/<plugin>/. - Shared remote-config derives generic OpenTelemetry config and session blob upload metadata from the bundle.
@cline/coreexposes the app-facing integration wrapper that applies extensions, telemetry, and session metadata toStartSessionInput.@cline/coreconsumes 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 undersrc/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.tsand feeds the builder rather than bypassing it
3. Runtime Host Boundary
Core exposes one shared execution boundary: RuntimeHost.
Concrete implementations:
LocalRuntimeHostfor in-process executionHubRuntimeHostfor shared local hub executionRemoteRuntimeHostfor explicit remote hub endpoints
Design implication:
- host selection happens in
packages/core/src/runtime/host.ts ClineCoredelegates uniformly toRuntimeHostand does not branch on local vs hub behavior- transport-specific translation belongs inside concrete hosts, not in top-level orchestration
RuntimeHostinputs stay transport-safe, whileClineCore.start(...)is the app-facing facade that normalizes broad local config before delegationRuntimeSessionConfigis transport-neutral across local, shared hub, and remote hub modes; host-local bootstrap concerns stay underlocalRuntime- 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.pendingPromptsservice. Usage summary lookup and active-session model switching are also service-style capabilities exposed throughClineCorewhen the concrete transport implements them. These service APIs are intentionally outside the minimalRuntimeHostprimitive vocabulary. session.abortremains the cancellation boundary for a root session in both local and hub-backed execution. The owningLocalRuntimeHostaborts 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-offspawn_agentdelegations observe the parent turn's abort signal through theirSessionRuntime. 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:usagefor the root/lead agent andaggregateUsagefor 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
outputTokensand a separate optionalreasoningTokenCountdelta.RuntimeEventAdapterderives both deltas from cumulative runtime usage; the hub'susage.updated.deltapayload preserves them whenHubRuntimeHostreconstructs the event.task.tokenstelemetry likewise reports reasoning separately fromtokensOut. 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.changedwith 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— requireddebugandlog; optionalerror. Hosts map these to their backend (Pino, VS CodeOutputChannel, etc.). Many runtime options takelogger?: BasicLogger; when omitted, components skip logging or usenoopBasicLoggerwhere a full object is required.BasicLogMetadata— optional structured fields (sessionId,runId,providerId,toolName,durationMs, …) plusseverityonlogwhen a single method must represent both informational and warning-style messages (for example the CLI Pino bridge mapsseverity: "warn"to Pinowarn).
Naming clarity:
CliLoggerAdapter(CLI) — a host bundle: holds the rawpinologger (for file paths, rotation, and CLI-only concerns) and exposes.core: BasicLoggerfor anything that consumes the SDK contract. It is not anITelemetryAdapter.TelemetryLoggerSink(@cline/core) — anITelemetryAdapterthat mirrors telemetry events and metrics into aBasicLogger. 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?: BasicLoggerparameter 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/agentsowns 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/coreowns 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 agentsstays 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 expansionextensions/plugin: runtime plugin discovery, loading, the plugin registry, the worker/subprocess sandbox transports, and their shared bootstrapextensions/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 anuncaughtfailure 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-sessionnode/bunsubprocess. 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.createpayloadplugins) is shaped liketoolPolicies:"*"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 throughtoolPolicies.setup()runs once per session with a context that carries the session'scwd,session.sessionId, andemitEvent. Plugin tools also receivecwdandemitEventin their call context.globalThis.__clinePluginHost.emitEventremains 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.resolveAndLoadAgentPluginsoverrides (importTimeoutMs,hookTimeoutMs,contributionTimeoutMs) apply per session. A failing tool or command throws its error back to the caller. A failing hook followshookErrorMode("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.statusreturn each plugin's status, last error, and the sessions using it;plugins.reloadre-imports one plugin by name or path without restarting the Hub.cline hub plugins [--reload <plugin>]wraps these.- The Hub broadcasts
plugin.status_changedto attached clients. session.createreplies andsession.createdevents carrypluginIssuesfor 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.pluginIssuesexposes 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 (AgentNoticeEventwithmetadata.pluginIssue). - Registry logs are structured lines with
pluginName,phase, andsessionId; in the Hub they go throughlogHubMessage. - Settings listings (
listPluginToolsWithDiagnostics,CoreSettingsService) report failed plugins with their state andloadError. 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 notchdir(Bun would change the host's cwd), so the bootstrap overridesprocess.cwd()inside the worker. The bootstrap postsreadyonce 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 anode/bunchild 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 thetaskstool and the desktop Agenda UI are temporarily disabled while the Agenda UX is reworked (AGENDA_TODO_TOOL_ENABLEDinhub-server-transport.tsandAGENDA_UI_ENABLEDin the desktop webview). While the flag is off the Hub also skips the agenda spec-file watchers — nothing consumes watcher-driven task events, andtask.*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
AgendaTaskManagerApiboundary 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.mdfor global tasks and<workspace>/.cline/tasks/*.task.mdfor workspace tasks. Operational fields such as status, revision, approval, and session IDs are not writable in a spec.AgendaTaskSpecFileStoreconfines paths to the selected task directory and writes specs atomically. SqliteAgendaTaskStoreownstasks.db(resolved byresolveTasksDbPath()). 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_reconcilerand 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 taskrevisionthroughapprovedRevision; 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.createandtask.updatepayloads 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 displayedexpectedRevision; specifically,task.approve,task.cancel, andtask.runreject a missing or stale revision. - P0 is the most urgent priority and P5 is the least urgent.
expiresAtis a required latest-start boundary: expiry prevents a new run but does not abort a session that already started. - Starting a task creates an
AgendaTaskRunRecordand a normal Hub session. The run owns the task/revision/session association, while the task keepscurrentRunId,lastRunId, andlastSessionIdprojections 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, andtask.automation.set. Registered task events aretask.created,task.updated,task.deleted,task.run.started,task.run.completed,task.run.failed, andtask.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
taskstool with a requiredkinddiscriminator.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 toHubScheduleServiceand 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 Todoavailable_atis not a timer, requires clarification for ambiguous “remind me” requests, and prevents creating both record kinds unless the user explicitly requests both. AgendaAutomationPolicyis user-owned Hub state.manualpreserves the per-task review gate;auto_startandunattendedare explicit opt-ins. The manager's automation pump enforces the policy's concurrency, chain-depth, and hourly-start guardrails.auto_startwaits for a connected client with the tool-approval capability and starts an interactive task session that conservatively requires approval for each tool. Explicitunattendedmode may run headlessly and auto-approves enabled tools.- Automation never infers user consent from a raw task-file change. For
manager-backed intent,
applyToAgentCreatedgoverns 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/reminderquick 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
-
Spec parser (
cron/specs/cron-spec-parser.ts): parses YAML frontmatter + body into aCronSpecdiscriminated union (one_off | schedule | event). Types live in@cline/sharedundersrc/cron/cron-spec-types.tsso other packages can consume them without the YAML parser. Schedule expressions and timezones are validated before a spec can become runnable. -
Store (
cron/store/sqlite-cron-store.ts): ownscron.dbatresolveCronDbPath()(default.cline/data/db/cron.db). Schema is bootstrapped fromcron/store/cron-schema.ts— sessions and cron live in separate DBs so their lifecycles stay decoupled. -
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 withparse_status='invalid'so state is durable rather than silently dropped. Files that disappear between scans getremoved=1and their queued runs are cancelled. -
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. -
Materializer (
cron/runner/cron-materializer.ts): turns file-triggered specs into queuedcron_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-awaregetNextCronTime. 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. -
Event ingress (
cron/events/cron-event-ingress.ts): accepts already-normalizedAutomationEventEnvelopevalues, persists them intocron_event_log, matches enabled event specs byevent_typeplus declarative filters, applies dedupe/debounce/cooldown policy, and enqueuescron_runswithtrigger_kind='event'. It never executes agents directly. Plugins can declareautomationEventsand submit normalized events throughctx.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. -
Runner (
cron/runner/cron-runner.ts): pollscron.db, atomically claims queued runs, executes them via the existingHubScheduleRuntimeHandlers(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 persistmode: "automation"for every run and record the spec-defined trigger source assessionHistoryOrigin.triggerin session metadata. Event runs include the normalized trigger event context in the prompt. -
Reports (
cron/reports/cron-report-writer.ts): writes.cline/cron/reports/<run-id>.mdwith run frontmatter plus## Summary,## Usage,## Tool Calls, and, for event runs,## Trigger Eventsections. -
Service (
cron/service/cron-service.ts): orchestrates all of the above.ClineCore.create({ automation })owns the SDK-facing lifecycle and exposescline.automation.*methods. Hub-side callers can submit normalized events through thecron.event.ingestcommand.
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*.tsin examples — runnable example files (plugins, hooks)*.mdfiles inapps/examples/— documentation and markdown-based specs (cron, events)
Key Type Locations
ClineCore—packages/core/src/index.ts— the main SDK orchestratorAgent—packages/agents/src/agent.ts— the agent loopRuntimeHost—packages/core/src/runtime/host/runtime-host.ts— execution abstractionAgentPlugin—packages/shared/src/plugin/— plugin contractCronSpec—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 implementationapps/webview— VS Code webviewapps/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.