v2-refactor-temp/docs/ai/data-cluster.md
| Subpath | What changed |
|---|---|
src/main/data/db/schemas/ | Agent / session / workspace / agent-message tables restructured |
src/main/data/services/ | AgentSessionService.ts + AgentWorkspaceService.ts new; AgentService.ts, AgentSessionMessageService.ts, MessageService.ts heavy rewrites |
src/main/data/api/handlers/ | agentSessions.ts + workspace handlers new; agents.ts slimmed (~100 LOC); messages.ts extended; assistants.ts + topics.ts extended |
src/main/data/migration/v2/migrators/ | AgentsMigrator.ts + AgentsDbMappings.ts rewrites; ChatMigrator.ts parts conversion; ProviderModelMigrator.ts adapterFamily backfill |
src/shared/data/types/ | agentMessage.ts + uiParts.ts new; agent.ts slimmed via Zod inference; message.ts heavy rewrite (parts model) |
src/shared/agents/ | agentSlashCommands.ts new (builtin SDK command list, off the data layer) |
src/shared/data/api/schemas/ | agent session + workspace schemas new; agents.ts slimmed by 126 LOC; messages.ts + assistants.ts + providers.ts extended |
Total surface: ~94 files modified across src/main/data/ and
src/shared/data/.
The AI pipeline refactor exposed three v1 data-layer constraints:
mcps / allowedTools / configuration /
accessible paths. Two sessions of the same agent could drift, and
the renderer had to chase both rows to render an agent profile.accessible_paths: string[] field on each agent. Sessions inherited
it. There was no row identity for "this directory" — renaming,
reordering, or reusing a directory across agents meant duplicating
the string.blocks: MessageBlock[] — Cherry
v1's custom block model. AI SDK v6 produces UIMessage.parts
directly. Continuing to convert one to the other on every read /
write meant maintaining a translation layer that has zero v2-side
consumers.The refactor:
agent keeps cognitive config; agent_session keeps
per-session state + workspace binding. Sessions reference their agent
by FK with onDelete: 'set null' so orphan sessions can still render.agent_workspace is a separate table with unique
path index and a type discriminator (user or system). Sessions
FK to workspaces with onDelete: 'cascade'; deleting a workspace row
deletes sessions bound to it.agent_session_message.content stores
AgentPersistedMessage (AI-SDK-native parts) directly. The
blocks field is gone end-to-end; the migrator converts legacy rows
in place.See Multi-model & data shape changes below for the multi-model + plan/small model split that motivated the agent schema rewrite.
agent (rewritten — src/main/data/db/schemas/agent.ts)export const agentTable = sqliteTable('agent', {
id: uuidPrimaryKey(),
type: text().notNull(), // 'claude-code', future agent kinds
name: text().notNull(),
description: text().notNull().default(''),
instructions: text().notNull(),
// Multi-model: chat / plan / small as three separate FK columns to userModel.
// Plan model = used for high-level planning when the agent supports it;
// small model = used for cheap helper calls (compaction, summaries).
model: text().references(() => userModelTable.id, { onDelete: 'set null' }),
planModel: text().references(() => userModelTable.id, { onDelete: 'set null' }),
smallModel: text().references(() => userModelTable.id, { onDelete: 'set null' }),
mcps: text({ mode: 'json' }).$type<string[]>().notNull().default('[]'),
allowedTools: text({ mode: 'json' }).$type<string[]>().notNull().default('[]'),
configuration: text({ mode: 'json' }).$type<Record<string, unknown>>().notNull().default('{}'),
...orderKeyColumns,
...createUpdateDeleteTimestamps
})
Removed from this table (relative to v1): accessible_paths,
per-session config fields, enableAutoTools, model ids as strings.
configuration is intentionally .loose() (Zod passthrough) at the
schema layer — see AgentConfigurationSchema for the typed keys (avatar,
permission_mode, max_turns, scheduler_, heartbeat_, env_vars). Historical
soul_enabled values may still be stored, but are ignored by the loose schema.
Unknown extras are preserved across read/write so older / newer app versions
don't silently drop fields.
agent_session (rewritten — agentSession.ts)export const agentSessionTable = sqliteTable('agent_session', {
id: uuidPrimaryKey(),
agentId: text().references(() => agentTable.id, { onDelete: 'set null' }),
name: text().notNull(),
description: text().notNull().default(''),
workspaceId: text()
.notNull()
.references(() => agentWorkspaceTable.id, { onDelete: 'cascade' }),
...orderKeyColumns,
...createUpdateTimestamps
})
Removed (relative to v1): every cognitive-config field. The renderer
fetches them via useAgent(session.agentId).
Insert-only workspace. UpdateSessionDto deliberately does not
include workspaceId — a running session can't be re-pointed at a new
directory. Newly created sessions must bind an explicit workspace source:
either { type: 'user', workspaceId } for an existing user workspace, or
{ type: 'system' } for a deterministic app-owned system workspace row.
agent_workspace (new — agentWorkspace.ts)export const agentWorkspaceTable = sqliteTable('agent_workspace', {
id: uuidPrimaryKey(),
name: text().notNull(),
path: text().notNull(),
type: text().notNull(), // CHECK type IN ('user', 'system')
...orderKeyColumns,
...createUpdateTimestamps
}, t => [
uniqueIndex('agent_workspace_path_unique_idx').on(t.path),
check('agent_workspace_type_check', sql`${t.type} IN (...)`),
...
])
path is the unique key. AgentWorkspaceService is DB-only: it
normalizes paths, creates/reuses user workspace rows, creates deterministic
system workspace rows, and never touches the filesystem. Filesystem
directory validation and creation live on the Claude Code runtime consumer
for user workspaces, and only app-owned system workspace directories are
auto-created.
agent_session_message (rewritten — agentSessionMessage.ts)export const agentSessionMessageTable = sqliteTable('agent_session_message', {
id: uuidPrimaryKeyOrdered(),
sessionId: text().notNull().references(() => agentSessionTable.id, { onDelete: 'cascade' }),
role: text().notNull(),
content: text({ mode: 'json' }).$type<AgentPersistedMessage>().notNull(),
// Opaque runtime resume token; null when the session never ran or was reset.
runtimeResumeToken: text(),
metadata: text({ mode: 'json' }).$type<Record<string, unknown>>(),
...createUpdateTimestamps
})
content is now AgentPersistedMessage (parts model). The legacy
blocks column is gone. runtimeResumeToken is an opaque recovery token
owned by the active agent runtime driver; Claude Code maps it to its SDK
session id, while other drivers may use a different resume token or none.
agent_channel + agent_channel_task (new — agentChannel.ts)Channel adapters for Discord / Slack / Telegram / Feishu / WeChat / QQ.
config is JSON, permissionMode is constrained via check to the
Claude Agent SDK's permission modes, and agent_channel_task is the
join table to scheduled agent_task rows.
agent_task, agent_task_run_log, agent_global_skill, agent_skillAgent-domain scheduled-task storage. Each agent_task carries cron /
interval / one-time scheduling plus a prompt; agent_task_run_log
stores per-run outputs. agent_global_skill + agent_skill model the
(currently flat) "skill" catalog.
job / job_schedule stackjob and job_schedule are the generic scheduling backbone owned
by JobManager + SchedulerService + JobService /
JobScheduleService. They are not removed — they were added on
origin/v2 in parallel with this branch's agent work and arrived here
via the v2 merge. See
docs/references/job-and-scheduler/
for the design.
The agent_task tables are layered ABOVE the job stack — the agent
scheduler will register typed handlers against JobManager and store
its own task metadata in agent_task. Reviewing changes here should
treat the two as complementary, not as a replacement.
AgentSessionService.ts. Cursor-paginated list with order keys,
transactional create that requires an explicit workspace source. User
sources must reference user workspace rows; system sources create a
deterministic session-owned workspace row. Workspace binding remains
insert-only.AgentWorkspaceService.ts. DB-only workspace row access, path
normalization, find-or-create for user workspace rows, deterministic
system workspace row creation, and reorder. It owns no filesystem
side effects; runtime consumers validate user directories and create
only app-owned system directories when needed.AgentService.ts (+241 LOC change). Foreign-keyed model fields
expand into joined reads; cognitive config snapshot
(getAgentForRun(sessionId)) consolidates fields the AI pipeline needs.
See commit f2229a881 refactor(agents): harden agent model field to UniqueModelId end-to-end.AgentSessionMessageService.ts (+215 LOC). Cursor-paginated
history (newest-first); persists AgentPersistedMessage; idempotent
upsert keyed on (sessionId, content.id) so retried persistence
doesn't double-insert.MessageService.ts (+390 LOC). Tree operations under the v2 parts
model — createUserMessageWithPlaceholders (transactional), tree path
reads, sibling groups, branch active-path tracking. Drops every
blocks reference.| Endpoint | Status | Notes |
|---|---|---|
GET/POST /agent-sessions, /agent-sessions/:id | new | session CRUD |
GET /agent-sessions/:id/messages | new | cursor-paginated |
GET /agent-workspaces, /agent-workspaces/:id, reorder endpoints | new | workspace reads + reorder |
/agents/* | slimmed | ~100 LOC removed; legacy order endpoints gone |
/messages/* | extended | parts read/write, tree path, sibling helpers |
/topics/* | extended | branch-aware active-node tracking |
/jobs/*, /job_schedules/* | added on origin/v2 (out of scope here) | see docs/references/job-and-scheduler/ |
AgentsMigratorReads the legacy standalone agents.db and folds it into the main
SQLite database. Source tables → targets:
| Source | Target |
|---|---|
agents | agent (+ join: model id → user_model) |
sessions | agent_session |
sessions.accessible_paths[0] | agent_workspace (one workspace per session, first valid path) |
session_messages | agent_session_message (with transformBlocksToParts) |
skills | agent_global_skill |
agent_skills | agent_skill |
scheduled_tasks | agent_task |
task_run_logs | agent_task_run_log |
channels | agent_channel |
channel_task_subscriptions | agent_channel_task |
Key points:
accessible_paths[0] is migrated to a
workspace row. Additional paths are not preserved. See
2026-05-19-agent-session-primary-workspace.md.blocks → parts for legacy session messages, via the same
transformBlocksToParts that ChatMigrator uses for the chat tree.notNullCol(name, defaultExpr) in
AgentsDbMappings covers the case where legacy rows have NULL in
columns that are NOT NULL in v2; a plain SELECT col would
otherwise hit SQLITE_CONSTRAINT_NOTNULL.generateOrderKeySequence synthesizes
fractional-indexing order keys for every migrated row so the v2
reorder UX works on migrated data.ChatMigratorThe transformBlocksToParts helper is shared with AgentsMigrator.
Both produce CherryMessagePart[]; no legacy blocks survives the
migration.
ProviderModelMigratorBackfills adapterFamily per endpoint config. See
adapter-family.md.
src/shared/data/types/agentMessage.ts (new)The AgentPersistedMessage shape stored on agent_session_message.content.
src/shared/data/types/uiParts.ts (new)Lifted the UI part type definitions out of message.ts so the agents
domain can consume them without taking the full chat-message dependency
graph.
src/shared/data/types/agent.ts (slimmed)Replaced hand-written types with Zod-inferred types from api/schemas/agents.ts.
src/shared/data/types/message.ts (rewritten)Removed the legacy blocks field and the type machinery built around
it. CherryMessagePart, CherryUIMessage, Message,
AssistantMessageStatus are the v2 vocabulary.
src/shared/data/api/schemas/agentSessions.ts (new)Entity + DTO schemas for sessions. UpdateSessionSchema deliberately
omits workspaceId to enforce insert-only binding.
src/shared/data/api/schemas/agentWorkspaces.ts (new)Entity + DTO schemas for workspaces. Path validation matches
the shared main-process normalizeWorkspacePath helper.
src/shared/data/api/schemas/agents.ts (slimmed)AgentEntity derived from the new schema. The 126-LOC reduction comes
from dropping per-session fields that moved to sessions.ts.
Three places where the data model now distinguishes models:
userModel: model (default chat),
planModel (planning), smallModel (helper calls). Migration maps
v1's single model_id to all three (the renderer overrides
plan / small as the user configures them).modelId: UniqueModelId (providerId::modelId) is the
v2 model identifier; v1's provider: string + model_id: string
merge into this. Persistent chats already used UniqueModelId; agent
messages now use it too.siblings_group_id (already on the
message tree) groups parallel assistant replies. The migrator
preserves existing sibling groups; the stream-manager's persistent
provider allocates new ones for fresh multi-model turns.UpdateSessionDto adding model/mcps/allowedTools is wrong —
those changes go through UpdateAgentDto.sessionService.update(id, { workspaceId: ... }). The schema rejects
this; reviewer should also catch any handler / hook that bypasses the
schema.agent_session.workspaceId
is non-null and uses onDelete: 'cascade'. A system workspace row is
owned one-to-one by its session and is deleted with that session; user
workspace rows must not be deleted when deleting one bound session.blocks field is gone. Any newly added code that reads
data.blocks or message.blocks is wrong. The migration is the only
place legacy blocks ever appears, and it converts to parts.runtime_resume_token (opaque runtime resume token) is null until first
run. Persistence writes it; reads must accept null.${serverName}__${toolName} (double underscore) in
the allowedTools JSON arrays. mcps is string[] of server ids.agent_task is not the generic job stack. Review of agent
tasks here should not touch JobManager / SchedulerService /
job_schedule — those land via origin/v2 independently.services/__tests__/AgentService.test.ts, AgentSessionService.test.ts,
AgentWorkspaceService.test.ts,
MessageService.test.ts, AgentChannelService.test.ts,
AgentTaskService.test.ts.migration/v2/migrators/__tests__/AgentsMigrator.test.ts,
AgentsMigrator.transforms.test.ts,
mappings/__tests__/AgentsDbMappings.test.ts,
remapAgentPrefixIds.test.ts.migration/v2/migrators/__tests__/ChatMigrator.test.ts for
transformBlocksToParts.src/shared/data/api/schemas/__tests__/agents.test.ts,
agentWorkspaces.test.ts.api/handlers/__tests__/agents.test.ts,
temporaryChats.test.ts, temporaryChats.integration.test.ts.agents.db lives as a separate SQLite file pre-migration. After v2
GA the legacy file should be deleted; for now the migrator just reads
it._skills tables (agent_global_skill, agent_skill) currently
mirror the v1 catalog 1:1. A future "skill registry" pass may
re-shape both.MessageService.ts is at 1163 LOC — splitting into tree / sibling /
branch helpers is queued for a follow-up.agent_task execution onto the generic JobManager (instead
of an agent-local scheduler) — depends on the
job-and-scheduler
stack landing on this branch via merge and is not reviewed in this
cluster.