State Model
September 5, 2026 · View on GitHub
This document describes the persistent on-disk state used by TaroCub.
The goal is to answer five practical questions for each file:
- Where does it live?
- Which module owns it?
- What data is authoritative?
- What are the write and recovery rules?
- How sensitive is it?
This document covers both:
- authoritative state: files the runtime depends on for behavior
- operational artifacts: logs, inbox files, backups, and registry files that are useful but not the source of truth for core behavior
Root Layout
Per-instance state lives under:
~/.cctb/<instance>/
Typical files and directories:
.env
config.json
access.json
session.json
runtime-state.json
usage.json
usage.last-good.json
file-workflow.json
cron-jobs.json
board.json
mini-bus.json
delivery-obligations.json
restart-loop.json
audit.log.jsonl
instance.lock.json
workspace/
inbox/
service.stdout.log
service.stderr.log
Some related control-plane state lives one level above the instance:
~/.cctb/.bus-registry.json
Classification
The project should reason about state in four buckets.
1. Authoritative configuration
.envconfig.json
These files define how the instance should run.
2. Authoritative runtime state
access.jsonsession.jsonruntime-state.jsonusage.jsonfile-workflow.jsoncron-jobs.jsonboard.jsonmini-bus.jsondelivery-obligations.json
These files represent the durable control state of the instance.
3. Append-only or operational evidence
audit.log.jsonlservice.stdout.logservice.stderr.log
These help with auditability and debugging, but the instance should not need them to compute its current truth.
4. Derived or transient artifacts
workspace/inbox/- backup archives
usage.last-good.json(validated recovery snapshot forusage.json)- migration leftovers
instance.lock.jsonrestart-loop.json(unclean-boot window for the restart-loop breaker; cleared on clean shutdown).bus-registry.json
These files are important, but they are not all the same. Some are transient coordination files, others are user data or deliverables.
Shared Storage Rules
These rules apply across most state files.
JSON storage primitive
src/state/json-store.ts is the shared primitive for most structured state.
It provides:
ENOENT -> defaultreads- atomic temp-file + rename writes
- owner-only permissions on write (
0700dirs,0600files) - schema-version stamping
- downgrade protection when loading newer state
- quarantine support for unreadable/corrupt files
Implication:
- if a new state file is structured JSON, it should usually be built on
JsonStore - if a file is not using
JsonStore, the code should explain why
Concurrency rule
Atomic rename prevents partial writes, but it does not prevent lost updates in read-modify-write flows.
Stores that mutate counters or collections must serialize each read-modify-write transaction and every destructive recovery with the same file-scoped mutex across processes. A read that can quarantine/reset state must also hold that mutex; otherwise it can rename a newer writer's valid file as corrupt.
Current examples:
SessionStoreRuntimeStateStoreUsageStoreFileWorkflowStoreCronStore
Some stores also keep an in-process promise queue to preserve call order, but that queue is not a substitute for the cross-process file mutex.
Security rule
Anything under the instance state directory should be treated as private unless there is a strong reason otherwise.
That includes:
- bot token
- pairing/access policy
- session mappings
- usage/cost history
- workflow records
- scheduled task prompts and history
- audit events
Per-File Model
.env
Path
<stateDir>/.env
Owner
- read path: src/service.ts
- CLI config commands also write and maintain it
Purpose
Stores the Telegram bot token for the instance when it is not provided through the ambient process environment.
The key field is:
TELEGRAM_BOT_TOKEN
Authoritative data
The token is authoritative if the service was started without TELEGRAM_BOT_TOKEN already set in the process environment.
Write rules
- should remain owner-readable only
- should be updated without clobbering unrelated env lines
- should not be regenerated from runtime state
Recovery rules
- if missing, the instance can still start if
TELEGRAM_BOT_TOKENexists in the process environment - if missing in both places, startup must fail
Sensitivity
Highest sensitivity.
Compromise of this file is effectively bot compromise.
config.json
Path
<stateDir>/config.json
Owner
This file has multiple readers and writers.
Primary readers:
- src/telegram/delivery.ts
- src/service.ts
- adapter modules in src/codex
Primary writers:
- CLI commands in src/commands/cli.ts
- runtime updates in src/telegram/delivery.ts
Purpose
Stores persistent instance configuration and a small amount of durable control state.
Important fields currently include:
engineapprovalModelocaleverbositybudgetUsdeffortmodelresume(legacy migration input only; new bindings are stored per conversation insession.json)bus
Authoritative data
This file is authoritative for instance configuration.
An old instance may still contain resume. Runtime code migrates that value onto the matching session.json record only when the owning session can be identified. New /resume and /detach writes never use the instance-global field.
Write rules
- must be written atomically
- the state directory and
config.jsonmust remain owner-only (0700and0600) even when replacing a pre-existing file with broader permissions - readers tolerate missing file by falling back to defaults
- runtime should not silently overwrite unreadable/corrupt config
- a web-console engine change clears incompatible provider-session bindings and commits the config update under one session transaction; if the config write fails, the original session bindings are restored before the mutex is released
Current behavior is split:
- CLI reads treat parse failure as
{}and continue - delivery/runtime reads log malformed config and run on defaults
- runtime writes refuse to overwrite unreadable non-
ENOENTconfig
This asymmetry is worth remembering.
Recovery rules
- missing file means "fresh instance defaults"
- malformed file currently causes runtime fallback with loud logging, not automatic repair
Sensitivity
Moderate sensitivity.
Usually not credential-bearing, but it can reveal instance topology, bus peers, legacy resume state, model selection, and operator intent.
access.json
Path
<stateDir>/access.json
Owner
Purpose
Stores Telegram access policy and pairing state.
Schema:
policy: "pairing" | "allowlist"pairedUsers[]allowlist[]pendingPairs[]
Authoritative data
This file is authoritative for:
- who is allowed to talk to the bot
- whether pairing or allowlist mode is active
- outstanding pairing codes
Write rules
Current writes are read-modify-write operations serialized in-process and across processes. Status reads that can trigger corruption recovery use the same file mutex as mutations.
Recovery rules
- missing file -> default state:
policy = pairing- empty allowlist/pairs/pending codes
- corrupt JSON or invalid shape is quarantined and reset to the fail-closed default (
pairing, no authorized chats) on a repair-capable operation - transient I/O and permission errors are rethrown rather than mistaken for corruption
Sensitivity
High sensitivity.
Leaking this file exposes pairing codes, authorized chats, and chat/user linkage.
session.json
Path
<stateDir>/session.json
Owner
Purpose
Maps Telegram and Lark conversations to provider session identifiers and conversation-scoped resumed workspaces.
Schema:
chats[]conversationKey(canonical private-chat/group/topic identity)telegramChatId(numeric compatibility key; Lark uses a stable derived value)telegramThreadId(Telegram topic, when present)codexSessionIdresume(provider session metadata plus resumed workspace root)suspendedPrevious(binding restored by/detachwhen available)statusupdatedAt
Authoritative data
This file is authoritative for durable conversation -> engine session and resumed-workspace binding.
It lets Telegram private chats/topics and Lark private chats/groups/topics continue independently across process restarts. A /resume in one conversation cannot change the file-delivery root or provider session of another conversation.
Write rules
- reads and writes are serialized with a cross-process file mutex; writes also retain in-process ordering
- updates are upsert-style by canonical
conversationKey, with legacy chat/thread keys normalized on read/write - removal supports recovery behavior on corrupt state
clearAllThen()holds the session mutex across session clearing plus its durable companion update, and restores the original bindings if that update fails
Recovery rules
This store has the best repair story in the project today.
It can:
- inspect unreadable state without throwing to the outer system
- distinguish repairable vs non-repairable failures
- quarantine corrupt files to
*.corrupt.*.bak - reset to an empty default state when safe to do so
Sensitivity
Moderate to high sensitivity.
It exposes chat IDs and engine session identifiers, which are operationally sensitive even if not credential-equivalent.
runtime-state.json
Path
<stateDir>/runtime-state.json
Owner
Purpose
Stores polling progress.
Current schema:
lastHandledUpdateId
Authoritative data
This file is authoritative for "how far Telegram polling has durably advanced".
Write rules
- reads and writes are serialized with a cross-process file mutex; writes also retain in-process ordering
- updates are monotonic: lower update IDs do not replace higher ones
Recovery rules
- missing file ->
lastHandledUpdateId = null - invalid file throws
This file matters because if its semantics drift from update acknowledgment semantics, the system can either replay or lose updates.
Sensitivity
Low sensitivity, but high correctness importance.
usage.json
Path
<stateDir>/usage.json
Owner
Purpose
Stores cumulative token and cost accounting for the instance.
Schema:
totalInputTokenstotalOutputTokenstotalCachedTokenstotalCostUsdrequestCountlastUpdatedAt
Authoritative data
This file is authoritative for user-visible cumulative usage and budget comparisons.
Write rules
- read-modify-write
- candidate records are schema-validated before persistence
- writes are serialized both in-process and across processes
- a successful write updates
usage.json, thenusage.last-good.json
Important nuance:
usage.last-good.json is recovery state, not a second independently mutable
ledger. Callers must always read and mutate through UsageStore.
Recovery rules
- missing file -> zeroed counters
- invalid primary + valid
usage.last-good.json-> quarantine the primary, restore the validated snapshot, and continue - invalid primary + missing/invalid backup -> throw
UsageStateCorruptErrorand leave the corrupt primary in place (fail closed; never reset budget usage to zero)
Sensitivity
Moderate sensitivity.
It reveals usage volume, cost, and activity timing.
cron-jobs.json
Path
<stateDir>/cron-jobs.json
Owner
Purpose
Stores persistent Telegram scheduled tasks.
Schema:
jobs[]idchatIduserIdchatTypecronExprpromptdescription?enabledrunOncetargetAt?sessionMode(new_per_runby default for new jobs;reuseonly for explicit continuation jobs; older persisted jobs keep their stored value until updated)mutesilenttimeoutMinsmaxFailurescreatedAtupdatedAtlastRunAt?lastSuccessAt?lastError?failureCountrunHistory[]
Authoritative data
This file is authoritative for which scheduled jobs exist and whether they are enabled. runOnce jobs are disabled after their first execution attempt. Recurring jobs track consecutive failureCount, keep the latest 10 runHistory entries, and are disabled when failureCount >= maxFailures. Use /cron mode <job-id> new_per_run to update older recurring jobs that were persisted with sessionMode: "reuse" before isolated cron runs became the default.
Write rules
- built on
JsonStore - writes are serialized per
CronStoreinstance - scheduler and
/croncommands should share the active runtime store where possible
Recovery rules
- missing file -> no scheduled tasks
- invalid file throws and prevents the cron runtime from starting
- operator repair should quarantine/reset this file rather than silently dropping jobs
Sensitivity
High sensitivity.
It can contain durable prompts, chat IDs, user IDs, schedule intent, and last error details.
board.json
Path
<stateDir>/board.json
Owner
Purpose
Stores durable Kanban task state for /board commands. It is intentionally separate from Mini Bus and Agent Bus topology: the board tracks work, while bus layers decide who can execute work.
Schema:
nextTaskIdnextRunIdtasks[]idtitlestatus:todo,ready,running,review,blocked,done, orarchiveddescription?acceptanceCriteria[]priority:low,normal,high, orurgentlabels[]checklist[]idtextdonecreatedAtcompletedAt?
artifacts[]kindvaluecreatedAt?
reviewrequiredreviewer?
assignee?dependencies[]workspace?mode:default,dir,worktree, orscratchpath?branch?
blockedReason?summary?createdAtupdatedAtcompletedAt?createdBychatIduserIdmessageThreadId?conversationKey
runs[]idstatusstartedAtlastHeartbeatAt?heartbeatNote?completedAt?summary?error?
Authoritative data
This file is authoritative for Board task ids, task status, dependencies, assignees, blocked reasons, completion summaries, and lightweight run history.
It is also authoritative for card metadata used by planner/dispatcher flows: description, acceptance criteria, priority, labels, checklist, artifacts, review requirement, optional workspace metadata, run heartbeat evidence, and WIP limits.
It is not authoritative for access control, Mini Bus peers, or Agent Bus peer configuration.
Write rules
- built on
JsonStore - writes are serialized with a file mutex so concurrent command handlers and processes do not lose tasks
- task ids are normalized as
B<number> - dependency completion promotes waiting
todotasks toreadywhen all dependencies are done - WIP limits are enforced before a task can start a new run
- default WIP limits are
global=3,perAssignee=1, andperConversation=1 /board plan <goal>creates a task graph from the current engine's JSON output and stores the resulting dependencies atomically/board heartbeat <id>updates the active run'slastHeartbeatAtand optionalheartbeatNote/board recover [minutes]closes stale running attempts as failed and blocks those tasks for operator review/board worktree <id> [path] [branch]stores optional per-task workspace metadata; task metadata by itself does not grant access or change bus topology- starting a task is rejected while dependencies are incomplete
- dependency cycles are rejected when adding a dependency
- each
startcreates one run attempt; duplicate concurrent running attempts are rejected /board run <id>starts one ready task, sends the task card to its assignee, then closes the run as done or failed/board run <id>resolves assignees by preferring Mini Bus peers in the current group, then falling back to Agent Bus instance namesfailcloses the active run as failed and moves the task toblocked- tasks with
review.requiredmove toreviewafter completion; dependents are promoted only after approval - completed tasks retain their original source chat/topic metadata for auditability
Recovery rules
- missing file -> empty board
- invalid file throws and prevents
/boardcommand handling from using stale or partial task state - old/missing counters are normalized from the maximum stored task/run ids
Sensitivity
High sensitivity.
It can contain durable task titles, operator intent, summaries, chat IDs, user IDs, topic IDs, and workflow topology hints.
mini-bus.json
Path
<stateDir>/mini-bus.json
Owner
Purpose
Stores Telegram group-local Mini Bus configuration. A Mini Bus lets one bot treat different topics in the same allowed group as named peers.
Schema:
groups<chatId>peers[]namechatIdmessageThreadId?conversationKeycreatedAtupdatedAt
parallel[]chain[]verifier?rolesresearcher?analyst?writer?reviewer?
crewmaxResearchQuestionsmaxRevisionRounds
Authoritative data
This file is authoritative for Mini Bus peer names, default fan/chain order, verifier selection, and Mini crew role mapping inside each Telegram group.
It is not authoritative for Telegram access. Group access still comes from access.json and config.json group mode settings.
Write rules
- built on
JsonStore - writes are serialized with a file mutex so concurrent topic registrations do not clobber each other
- peer names and role names are normalized before persistence
- removing a peer also removes references from
parallel,chain,verifier, androles
Recovery rules
- missing file -> no Mini Bus peers configured
- invalid file throws and prevents Mini Bus command handling from using stale or partial state
- old records without
rolesorcreware normalized with default empty roles and default crew limits
Sensitivity
Moderate sensitivity.
It reveals group chat IDs, topic IDs, peer topology, and operator workflow intent. It does not contain bot tokens or engine credentials.
file-workflow.json
Path
<stateDir>/file-workflow.json
Owner
src/state/file-workflow-store.ts
Purpose
Tracks attachment and archive processing workflows that span multiple Telegram messages.
Schema:
records[]uploadIdchatIduserIdkindstatussourceFilesderivedFilessummarysummaryMessageId?extractedPath?createdAtupdatedAt
Authoritative data
This file is authoritative for in-progress and resumable file-processing state, especially archive workflows that wait for a follow-up /continue.
Write rules
- writes and corruption recovery are serialized with a cross-process file mutex; writes also retain in-process ordering
- ordinary snapshot reads rely on atomic replacement and do not mutate or quarantine state
- records are append/update/remove by
uploadId - status is mutated over time;
updatedAtis refreshed on mutation
Recovery rules
Like session.json, this store supports:
- inspect-with-warning
- corruption detection
- quarantine to backup
- reset-based repair
This is important because users can still receive successful Telegram output even if bookkeeping later fails.
Sensitivity
Moderate sensitivity.
It contains local file paths, chat IDs, and summaries of processed content.
audit.log.jsonl
Path
<stateDir>/audit.log.jsonl
Owner
Purpose
Append-only operational and forensic event log.
Each line is an AuditEvent with fields such as:
timestamptypeinstanceNamechatIduserIdupdateIdoutcomedetailmetadata
Authoritative data
This file is not authoritative for current runtime truth.
It is authoritative only as a historical record of what the system attempted and reported.
Write rules
- append-only JSONL
- best effort in some call sites
- no compaction semantics
Callers should never rely on this file for reconstructing canonical current state.
Recovery rules
- malformed lines are ignored by parsers
- summaries are best-effort over whatever lines remain parseable
Sensitivity
High sensitivity.
It may contain prompts, error details, chat IDs, workflow metadata, and operational clues.
instance.lock.json
Path
<stateDir>/instance.lock.json
Owner
Purpose
Ensures only one service process owns an instance state directory at a time.
Schema:
pidtokenacquiredAt
Authoritative data
This file is authoritative only for process exclusivity at startup/runtime, not for user-visible behavior.
Write rules
- created with exclusive write (
wx) - stale file is removed only after verifying the recorded process is dead
- release checks both
pidand randomtoken
Recovery rules
- missing file is normal
- stale file is automatically pruned
- live holder means startup must fail
Sensitivity
Low confidentiality sensitivity, high coordination importance.
service.stdout.log and service.stderr.log
Path
<stateDir>/service.stdout.log<stateDir>/service.stderr.log
Owner
Service management commands under src/commands/service.ts
Purpose
Operational logs for managed background service processes.
Authoritative data
Not authoritative for runtime behavior.
Useful for diagnosis only.
Write rules
- append as process output streams
- subject to rotation via src/state/log-rotation.ts
Recovery rules
Safe to truncate or rotate.
Sensitivity
Moderate sensitivity because logs may include prompts, errors, config paths, and stack traces.
workspace/
Path
<stateDir>/workspace/
Owner
Created by service/adapters, then effectively co-owned by the engine and the user.
Purpose
The instance working directory.
This is where:
- engine tasks operate
CLAUDE.mdmay live- generated files may be created before Telegram delivery
- resumed-session workspace alignment matters
Authoritative data
Not canonical bridge control state, but authoritative as the working tree the engine sees.
Write rules
- contents are not schema-managed
- should be treated as user/engine data, not config
Recovery rules
Backed up and restored as part of instance archives.
Sensitivity
Potentially very high, depending on project contents.
inbox/
Path
<stateDir>/inbox/
Owner
Telegram delivery and workflow code.
Purpose
Stores downloaded Telegram attachments and quoted files for local processing.
Authoritative data
Not authoritative long-term control state, but important transient input material for workflows.
Write rules
- created on demand
- contains downloaded user data
Recovery rules
Safe to treat as ephemeral operational data, but note that in-progress workflows may still reference files under it.
Sensitivity
High sensitivity because it contains raw user attachments.
Backup archives (*.cctb.gz)
Path
User-chosen output path, not a fixed in-place state file.
Owner
Purpose
Portable backup/restore format for an instance state directory.
Authoritative data
A backup is authoritative only as a snapshot artifact, not as live state.
Write rules
- skips symlinks
- skips oversized files
- skips regenerable dependency/VCS/scratch directories and operational logs by default
- streams source file bodies through gzip rather than buffering the complete archive
- rejects manifests above 10,000 files and archives above the 128 MiB expanded-format limit
- writes a gzipped custom archive format through a private temporary file before atomic replacement
Recovery rules
- restore validates archive magic and version
- restore prevents path traversal
- restore rejects compressed inputs above 64 MiB, expanded archives above 128 MiB, manifests above 10,000 files, and individual members above 100 MiB
- restore expands and copies members through bounded streams instead of loading the complete archive or every member into memory
- restore reapplies restrictive permissions
Sensitivity
Highest sensitivity.
A backup may contain nearly the entire private state of an instance.
.bus-registry.json
Path
<channelRoot>/.bus-registry.json
Where channelRoot = dirname(stateDir), usually ~/.cctb/.
Owner
Purpose
Shared control-plane registry of locally running instances participating in the bus.
Schema:
instances[instanceName]portpidsecretupdatedAt
Authoritative data
This file is authoritative for local instance discovery on the bus.
It is not part of a single instance's isolated state; it is shared across sibling instances.
Write rules
- currently plain read-modify-write, not
JsonStore - should be treated as coordination state
- liveness is verified via
/api/health, not by trusting the file alone
Recovery rules
- missing/corrupt registry -> empty registry
- stale entries are pruned by active probing
Sensitivity
High sensitivity for local control-plane security because it contains bus secrets and routing info.
Files That Are Not Canonical State
These files may exist under the state directory but should not be treated as authoritative business state:
runtime.log- rotated logs like
audit.log.jsonl.1 engine-home.migrated-*- temporary
*.tmpfiles - transient Telegram-out directories
They matter operationally, but no feature should depend on them as the sole source of truth.
Current Gaps
These are the most important state-model gaps in the current code.
1. config.json has multiple owners
That is manageable, but only if all writers keep using atomic write and we stay disciplined about which fields are runtime state vs operator config.
2. Not every JSON store has the same repair semantics
session.json and file-workflow.json are more mature here than access.json, usage.json, and runtime-state.json.
3. Some coordination files do not use JsonStore
That is not automatically wrong, but the difference should be intentional and documented.
4. Shared-state files and per-instance files are easy to conflate
.bus-registry.json is not just another instance file. It belongs to the local mesh, not one bot.
Recommended Next Steps
- Keep this file updated whenever a new persistent file is introduced.
- Normalize repair behavior across state stores where it materially helps operators.
- Remove the legacy
config.json.resumecompatibility path after all supported upgrade windows have migrated. - Keep
docs/security-boundaries.mdaligned whenever a state file changes trust or sensitivity.
delivery-obligations.json
Path
<stateDir>/delivery-obligations.json
Owner
src/state/delivery-obligation-store.ts. Written by the Lark service process during ordinary-turn final delivery and by the boot-time sweep.
Purpose
Durable delivery-obligation ledger (Hermes-inspired): one row per outbound final engine response with checkpoints around the send (pending → attempting → delivered/failed). A row reaches delivered only after every requested artifact and text chunk receives a definite success; a rejected or ambiguous artifact ACK keeps the row failed and the turn partial. On boot, sweepRecoverable claims undelivered rows whose owning process is dead and src/lark/delivery-recovery.ts redelivers them — plainly for pending (send never started), with a visible ♻️ recovered-reply marker for attempting/failed (the platform may already have the message; honest at-least-once, never a silent duplicate).
Write rules
- Atomic tmp+rename writes serialized by a file mutex; every operation is best-effort and must never block or fail an actual send.
- Attempts are capped (3) and rows older than 24h are
abandoned— a poison reply cannot crash-loop redelivery, and a day-old reply never suddenly reappears. - Bounded: settled rows pruned after 7 days, 200-row cap, replies over 200KB are never recorded (Doc-overflow territory).
- Kill-switch:
CCTB_DELIVERY_LEDGER=offdisables recording and sweeping. - Recovery parses persisted responses through the normal guarded artifact pipeline. It may replay
send.file,send.image,send.audio,send.video,send.batch, and whole-responsefile:blocks, but it strips cron and all other side-effect tools rather than executing them again. - Artifact identity uses the real file (device/inode, with canonical-path fallback), so structured and legacy tags cannot resend one file through symlink, hard-link, or case aliases.
- Image cards split only after an explicit platform size rejection. Timeout, disconnect, and other ambiguous acknowledgements stop without an immediate retry and surface a “request a resend” notice.
Recovery rules
Corrupt file → treated as empty and rewritten on the next record; never fatal.
restart-loop.json
Path
<stateDir>/restart-loop.json
Owner
src/runtime/restart-loop-guard.ts.
Purpose
Rolling window of UNCLEAN boot timestamps (boot recovered a stale service lock = previous run died without clean shutdown). Three unclean boots inside 10 minutes trips the breaker: that boot skips boot-recovery work (interrupted-turn marking, delivery-ledger redelivery) so a poison replay cannot keep killing a supervised service; live traffic is still served. Operator restarts shut down cleanly, release the lock, and are never counted; a clean shutdown also deletes this file.
Recovery rules
All failures fail OPEN (no trip). Delete the file to manually reset the window.