@bbigbang/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. package/package.json +54 -0
@@ -0,0 +1,2511 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import os from 'node:os';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL, buildThreadShortId, normalizeAgentDisabledToolKinds, normalizeThreadShortIdInput, parseAgentPermissionKind, } from '@bbigbang/protocol';
6
+ import { log, createSession, upsertBinding, } from '@bbigbang/runtime-acp';
7
+ import { getRuntimeDriver } from '@bbigbang/protocol';
8
+ import { ExecutionDispatcher } from '../execution/executionDispatcher.js';
9
+ import { parseDreamScheduleHourMinute } from '../scheduler/dreamLogic.js';
10
+ import { getPlatformDirectReplyScope, isPlatformAgentRole } from '../services/platformAgentPolicy.js';
11
+ import { ensureDmThreadContextSnapshot } from './activationContext.js';
12
+ import { deriveSurfaceKind, parseSurfaceKind } from './conversationSurfaceKinds.js';
13
+ import { buildClearedTaskRootArchiveTarget, CHANNEL_TASK_ROOT_PRESERVED_SOURCE, } from './clearedTaskRoots.js';
14
+ import { findThreadRootMessageId, resolveThreadRootLookup } from './threadRoots.js';
15
+ import { clearAgentMentionCooldownsForChannelChatClear } from './agentMentionCooldowns.js';
16
+ import { deleteTargetParticipantsForAgent, deleteTargetParticipantsForAgentInChannel, deleteTargetParticipantsForChannel, } from './targetParticipants.js';
17
+ import { buildDirectReplyTarget, resolveDirectUserName } from './directReplyTargets.js';
18
+ import { appendTaskEvent, deleteTaskEventsForChannel } from './taskEvents.js';
19
+ import { refreshConversationSummary } from './conversationSummaries.js';
20
+ import { ackNotificationDelivery, deleteAgentNotificationV2State, deleteNotificationV2StateForChannel, deleteNotificationV2StateForThreadRoots, } from './notificationRounds.js';
21
+ import { clearRollingSummaryStateForAgent, clearRollingSummaryStateForConversation, maybeRotateCodexConversationAfterSettlement, } from './rollingConversationSummary.js';
22
+ import { deletePeerInboxAggregatesForAgent, deletePeerInboxAggregatesForChannel, } from './peerInboxAggregates.js';
23
+ import { parseConversationBuiltinSkillRoots, serializeConversationBuiltinSkillRoots, } from './conversationBuiltinSkillRoots.js';
24
+ import { hardDeletePanelRowsForConversation, hardDeletePanelsForConversation, unlinkPanelAttachmentStoragePaths, } from './panelLifecycle.js';
25
+ import { listEffectiveAgentRuntimeCapabilities } from './runtimeCapabilities.js';
26
+ function slugifyAgentName(name) {
27
+ return name
28
+ .toLowerCase()
29
+ .replace(/[^\w\s-]/g, '')
30
+ .replace(/[\s_]+/g, '-')
31
+ .replace(/-+/g, '-')
32
+ .replace(/^-|-$/g, '')
33
+ .slice(0, 40);
34
+ }
35
+ function normalizeAgentName(name) {
36
+ return name.trim().toLowerCase();
37
+ }
38
+ function normalizeChannelThreadRootId(threadRootId) {
39
+ return normalizeThreadShortIdInput(threadRootId);
40
+ }
41
+ function normalizeUiPanelEnabled(value) {
42
+ return value === true || value === 1 || value === '1';
43
+ }
44
+ const UI_PANEL_ENABLED_SELECT_SQL = `CASE
45
+ WHEN instr(COALESCE(builtin_skill_roots, ''), '${BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL}') > 0 THEN 1
46
+ ELSE ui_panel_enabled
47
+ END as uiPanelEnabled`;
48
+ export class AgentRuntimeSupportError extends Error {
49
+ statusCode;
50
+ constructor(statusCode, message) {
51
+ super(message);
52
+ this.statusCode = statusCode;
53
+ }
54
+ }
55
+ export class AgentNameConflictError extends Error {
56
+ statusCode;
57
+ constructor(message = 'Agent name already exists') {
58
+ super(message);
59
+ this.statusCode = 409;
60
+ }
61
+ }
62
+ export class AgentConfigValidationError extends Error {
63
+ statusCode;
64
+ constructor(message) {
65
+ super(message);
66
+ this.statusCode = 400;
67
+ }
68
+ }
69
+ export class ConversationManager {
70
+ db;
71
+ config;
72
+ nodeRegistry;
73
+ codexAppServerBroker;
74
+ agentRuntimeCleanupBroker;
75
+ executionDispatcher;
76
+ deletingAgentIds = new Set();
77
+ constructor(params) {
78
+ this.db = params.db;
79
+ this.config = params.config;
80
+ this.nodeRegistry = params.nodeRegistry;
81
+ this.codexAppServerBroker = params.codexAppServerBroker;
82
+ this.agentRuntimeCleanupBroker = params.agentRuntimeCleanupBroker;
83
+ this.executionDispatcher = new ExecutionDispatcher({
84
+ db: params.db,
85
+ config: params.config,
86
+ nodeRegistry: params.nodeRegistry,
87
+ getAgentById: (agentId) => this.getAgent(agentId),
88
+ });
89
+ }
90
+ setLibraryService(libraryService) {
91
+ this.executionDispatcher.setLibraryService(libraryService);
92
+ }
93
+ beginAgentDeletion(agentId) {
94
+ if (this.deletingAgentIds.has(agentId)) {
95
+ throw new Error('Agent deletion is already in progress.');
96
+ }
97
+ if (!this.getAgent(agentId))
98
+ return false;
99
+ this.deletingAgentIds.add(agentId);
100
+ return true;
101
+ }
102
+ finishAgentDeletion(agentId) {
103
+ this.deletingAgentIds.delete(agentId);
104
+ }
105
+ isAgentDeletionInProgress(agentId) {
106
+ return !!agentId && this.deletingAgentIds.has(agentId);
107
+ }
108
+ assertAgentNotDeleting(agentId) {
109
+ if (this.isAgentDeletionInProgress(agentId)) {
110
+ throw new Error('Agent deletion is in progress.');
111
+ }
112
+ }
113
+ getDb() {
114
+ return this.db;
115
+ }
116
+ getConfig() {
117
+ return this.config;
118
+ }
119
+ start() {
120
+ this.backfillConversationReplyTargets();
121
+ this.recoverSteerAcks();
122
+ log.info('ConversationManager ready');
123
+ }
124
+ close() {
125
+ this.executionDispatcher.close();
126
+ }
127
+ // ─── Agent CRUD ───
128
+ createAgent(params) {
129
+ const agentId = randomUUID();
130
+ const agentType = params.agentType ?? 'codex_app_server';
131
+ const name = params.name.trim();
132
+ this.assertNodeSupportsAgentType(params.nodeId, agentType);
133
+ this.assertAgentNameAvailable(name);
134
+ const model = params.model?.trim() || null;
135
+ const reasoningEffort = params.reasoningEffort?.trim() || null;
136
+ const claudePermissionMode = agentType === 'claude_sdk' ? normalizeClaudePermissionMode(params.claudePermissionMode) : null;
137
+ const codexMode = agentType === 'codex_app_server' ? normalizeCodexMode(params.codexMode) : null;
138
+ const codexServiceTier = agentType === 'codex_app_server' ? normalizeCodexServiceTier(params.codexServiceTier) : null;
139
+ const channelId = params.channelId ?? 'default';
140
+ const envVarsJson = params.envVars && Object.keys(params.envVars).length > 0
141
+ ? JSON.stringify(params.envVars)
142
+ : null;
143
+ const disabledToolKinds = normalizeAgentDisabledToolKinds(agentType, params.disabledToolKinds);
144
+ const disabledToolKindsJson = disabledToolKinds && disabledToolKinds.length > 0
145
+ ? JSON.stringify(disabledToolKinds)
146
+ : null;
147
+ const skillRoots = normalizeSkillRootsForStorage(params.skillRoots);
148
+ const skillRootsJson = skillRoots && skillRoots.length > 0
149
+ ? JSON.stringify(skillRoots)
150
+ : null;
151
+ const now = Date.now();
152
+ const workspacePath = params.workspacePath
153
+ ?? path.join(os.homedir(), '.bigbang', 'agents', `${agentId}-${slugifyAgentName(name)}`);
154
+ fs.mkdirSync(workspacePath, { recursive: true });
155
+ const projectPath = params.projectPath?.trim() || null;
156
+ const description = params.description?.trim() || null;
157
+ this.db.prepare(`INSERT INTO agents(agent_id, name, agent_type, agent_role, model, reasoning_effort, claude_permission_mode, codex_mode, codex_service_tier, channel_id, system_prompt, description, memory, env_vars, disabled_tool_kinds, node_id, workspace_path, project_path, skill_roots, enabled_skill_paths, created_at, updated_at)
158
+ VALUES(?, ?, ?, 'user', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(agentId, name, agentType, model, reasoningEffort, claudePermissionMode, codexMode, codexServiceTier, channelId, params.systemPrompt ?? '', description, '', envVarsJson, disabledToolKindsJson, params.nodeId ?? null, workspacePath, projectPath, skillRootsJson, null, now, now);
159
+ this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
160
+ VALUES(?, ?, 1, ?)`).run(agentId, channelId, now);
161
+ return {
162
+ agentId, name, agentType, ...(model ? { model } : {}), ...(reasoningEffort ? { reasoningEffort } : {}),
163
+ ...(claudePermissionMode ? { claudePermissionMode } : {}),
164
+ ...(codexMode ? { codexMode } : {}), ...(codexServiceTier ? { codexServiceTier } : {}),
165
+ channelId, channelIds: [channelId],
166
+ systemPrompt: params.systemPrompt ?? '',
167
+ ...(description ? { description } : {}),
168
+ envVars: params.envVars, disabledToolKinds, nodeId: params.nodeId ?? null,
169
+ workspacePath, projectPath,
170
+ skillRoots,
171
+ skillSelectionMode: 'legacy_all',
172
+ createdAt: now,
173
+ updatedAt: now,
174
+ };
175
+ }
176
+ assertNodeSupportsAgentType(nodeId, agentType) {
177
+ const normalizedNodeId = nodeId?.trim();
178
+ if (!normalizedNodeId)
179
+ return;
180
+ const supportedAgentTypes = this.getKnownNodeAgentTypes(normalizedNodeId);
181
+ if (!supportedAgentTypes || supportedAgentTypes.length === 0)
182
+ return;
183
+ if (supportedAgentTypes.includes(agentType))
184
+ return;
185
+ throw new AgentRuntimeSupportError(400, `Node does not support agent type ${agentType}: ${normalizedNodeId}`);
186
+ }
187
+ getKnownNodeAgentTypes(nodeId) {
188
+ const liveAgentTypes = normalizeAgentTypeList(this.nodeRegistry?.getNode(nodeId)?.agentTypes);
189
+ if (liveAgentTypes.length > 0) {
190
+ return liveAgentTypes;
191
+ }
192
+ const row = this.db.prepare(`SELECT agent_types_json as agentTypesJson
193
+ FROM nodes
194
+ WHERE node_id = ?`).get(nodeId);
195
+ if (!row?.agentTypesJson)
196
+ return null;
197
+ try {
198
+ return normalizeAgentTypeList(JSON.parse(row.agentTypesJson));
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ listAgents(channelId, options) {
205
+ const roleFilter = options?.includeLibraryCurators
206
+ ? ''
207
+ : ` AND COALESCE(a.agent_role, 'user') = 'user'`;
208
+ const sql = channelId
209
+ ? `SELECT a.agent_id as agentId, a.name, COALESCE(a.agent_role, 'user') as agentRole, a.agent_type as agentType, a.model, a.reasoning_effort as reasoningEffort,
210
+ a.claude_permission_mode as claudePermissionMode, a.channel_id as channelId,
211
+ a.codex_mode as codexMode, a.codex_service_tier as codexServiceTier,
212
+ a.system_prompt as systemPrompt, a.description,
213
+ a.env_vars as envVarsJson, a.disabled_tool_kinds as disabledToolKindsJson,
214
+ a.node_id as nodeId, a.workspace_path as workspacePath, a.project_path as projectPath,
215
+ a.skill_roots as skillRootsJson, a.enabled_skill_paths as enabledSkillPathsJson,
216
+ a.memory_network_enabled as memoryNetworkEnabled,
217
+ a.dream_enabled as dreamEnabled, a.dream_schedule as dreamSchedule,
218
+ a.dream_min_messages as dreamMinMessages, a.last_dream_at as lastDreamAt, a.last_dream_seq as lastDreamSeq,
219
+ a.deleted_at as deletedAt, a.created_at as createdAt, a.updated_at as updatedAt
220
+ FROM agents a
221
+ JOIN agent_channel_memberships m ON m.agent_id = a.agent_id
222
+ WHERE m.channel_id = ? AND a.deleted_at IS NULL${roleFilter} ORDER BY a.updated_at DESC`
223
+ : `SELECT agent_id as agentId, name, COALESCE(agent_role, 'user') as agentRole, agent_type as agentType, model, reasoning_effort as reasoningEffort,
224
+ claude_permission_mode as claudePermissionMode, channel_id as channelId,
225
+ codex_mode as codexMode, codex_service_tier as codexServiceTier,
226
+ system_prompt as systemPrompt, description,
227
+ env_vars as envVarsJson, disabled_tool_kinds as disabledToolKindsJson,
228
+ node_id as nodeId, workspace_path as workspacePath, project_path as projectPath,
229
+ skill_roots as skillRootsJson, enabled_skill_paths as enabledSkillPathsJson,
230
+ memory_network_enabled as memoryNetworkEnabled,
231
+ dream_enabled as dreamEnabled, dream_schedule as dreamSchedule,
232
+ dream_min_messages as dreamMinMessages, last_dream_at as lastDreamAt, last_dream_seq as lastDreamSeq,
233
+ deleted_at as deletedAt, created_at as createdAt, updated_at as updatedAt
234
+ FROM agents WHERE deleted_at IS NULL${roleFilter.replace('a.', '')} ORDER BY updated_at DESC`;
235
+ const rows = channelId
236
+ ? this.db.prepare(sql).all(channelId)
237
+ : this.db.prepare(sql).all();
238
+ return rows.map((row) => this.rowToAgentInfo(row));
239
+ }
240
+ getAgent(agentId) {
241
+ const row = this.db.prepare(`SELECT agent_id as agentId, name, COALESCE(agent_role, 'user') as agentRole, agent_type as agentType, model, reasoning_effort as reasoningEffort,
242
+ claude_permission_mode as claudePermissionMode, channel_id as channelId,
243
+ codex_mode as codexMode, codex_service_tier as codexServiceTier,
244
+ system_prompt as systemPrompt, description,
245
+ env_vars as envVarsJson, disabled_tool_kinds as disabledToolKindsJson,
246
+ node_id as nodeId, workspace_path as workspacePath, project_path as projectPath,
247
+ skill_roots as skillRootsJson, enabled_skill_paths as enabledSkillPathsJson,
248
+ memory_network_enabled as memoryNetworkEnabled,
249
+ dream_enabled as dreamEnabled, dream_schedule as dreamSchedule,
250
+ dream_min_messages as dreamMinMessages, last_dream_at as lastDreamAt, last_dream_seq as lastDreamSeq,
251
+ deleted_at as deletedAt, created_at as createdAt, updated_at as updatedAt
252
+ FROM agents WHERE agent_id = ? AND deleted_at IS NULL`).get(agentId);
253
+ return row ? this.rowToAgentInfo(row) : null;
254
+ }
255
+ listActiveAgentIdsForNode(nodeId) {
256
+ const rows = this.db.prepare(`SELECT agent_id as agentId
257
+ FROM agents
258
+ WHERE node_id = ?
259
+ AND deleted_at IS NULL
260
+ ORDER BY updated_at DESC`).all(nodeId);
261
+ return rows.map((row) => row.agentId);
262
+ }
263
+ getAgentIncludingDeleted(agentId) {
264
+ const row = this.db.prepare(`SELECT agent_id as agentId, name, agent_type as agentType, model, reasoning_effort as reasoningEffort,
265
+ claude_permission_mode as claudePermissionMode, channel_id as channelId,
266
+ codex_mode as codexMode, codex_service_tier as codexServiceTier,
267
+ system_prompt as systemPrompt, description,
268
+ env_vars as envVarsJson, disabled_tool_kinds as disabledToolKindsJson,
269
+ node_id as nodeId, workspace_path as workspacePath, project_path as projectPath,
270
+ skill_roots as skillRootsJson, enabled_skill_paths as enabledSkillPathsJson,
271
+ deleted_at as deletedAt, created_at as createdAt, updated_at as updatedAt
272
+ FROM agents WHERE agent_id = ?`).get(agentId);
273
+ return row ? this.rowToAgentInfo(row) : null;
274
+ }
275
+ updateAgent(agentId, req) {
276
+ const existing = this.getAgent(agentId);
277
+ if (!existing)
278
+ return null;
279
+ const existingStorage = this.db.prepare(`SELECT disabled_tool_kinds as disabledToolKindsJson,
280
+ enabled_skill_paths as enabledSkillPathsJson
281
+ FROM agents
282
+ WHERE agent_id = ?`).get(agentId);
283
+ const now = Date.now();
284
+ const name = typeof req.name === 'string' ? req.name.trim() : existing.name;
285
+ if (normalizeAgentName(name) !== normalizeAgentName(existing.name)) {
286
+ this.assertAgentNameAvailable(name, agentId);
287
+ }
288
+ const agentType = existing.agentType;
289
+ const systemPrompt = req.systemPrompt ?? existing.systemPrompt;
290
+ const description = 'description' in req ? (req.description?.trim() || null) : (existing.description ?? null);
291
+ const model = 'model' in req ? (req.model?.trim() || null) : (existing.model ?? null);
292
+ const reasoningEffort = 'reasoningEffort' in req ? (req.reasoningEffort?.trim() || null) : (existing.reasoningEffort ?? null);
293
+ const claudePermissionMode = agentType === 'claude_sdk'
294
+ ? ('claudePermissionMode' in req ? normalizeClaudePermissionMode(req.claudePermissionMode) : (existing.claudePermissionMode ?? null))
295
+ : null;
296
+ const codexMode = agentType === 'codex_app_server'
297
+ ? ('codexMode' in req ? normalizeCodexMode(req.codexMode) : (existing.codexMode ?? null))
298
+ : null;
299
+ const codexServiceTier = agentType === 'codex_app_server'
300
+ ? ('codexServiceTier' in req ? normalizeCodexServiceTier(req.codexServiceTier) : (existing.codexServiceTier ?? null))
301
+ : null;
302
+ const envVars = req.envVars ?? existing.envVars;
303
+ const disabledToolKinds = 'disabledToolKinds' in req
304
+ ? normalizeAgentDisabledToolKinds(agentType, req.disabledToolKinds)
305
+ : existing.disabledToolKinds;
306
+ const skillRoots = 'skillRoots' in req ? normalizeSkillRootsForStorage(req.skillRoots) : existing.skillRoots;
307
+ const storedEnabledSkillPaths = parseStringArray(existingStorage?.enabledSkillPathsJson ?? null);
308
+ const shouldRefreshStoredEnabledSkillPaths = !('enabledSkillPaths' in req)
309
+ && 'skillRoots' in req
310
+ && existingStorage?.enabledSkillPathsJson != null;
311
+ const enabledSkillPaths = 'enabledSkillPaths' in req
312
+ ? normalizeEnabledSkillPaths(req.enabledSkillPaths, skillRoots)
313
+ : shouldRefreshStoredEnabledSkillPaths
314
+ ? retainEnabledSkillPathsWithinRoots(storedEnabledSkillPaths ?? [], skillRoots)
315
+ : storedEnabledSkillPaths;
316
+ const channelId = req.channelId ?? existing.channelId;
317
+ const projectPath = 'projectPath' in req ? (req.projectPath?.trim() || null) : (existing.projectPath ?? null);
318
+ const envVarsJson = envVars && Object.keys(envVars).length > 0
319
+ ? JSON.stringify(envVars)
320
+ : null;
321
+ const disabledToolKindsJson = 'disabledToolKinds' in req
322
+ ? (disabledToolKinds && disabledToolKinds.length > 0 ? JSON.stringify(disabledToolKinds) : null)
323
+ : existingStorage?.disabledToolKindsJson ?? null;
324
+ const skillRootsJson = skillRoots && skillRoots.length > 0
325
+ ? JSON.stringify(skillRoots)
326
+ : null;
327
+ const enabledSkillPathsJson = ('enabledSkillPaths' in req || shouldRefreshStoredEnabledSkillPaths)
328
+ ? JSON.stringify(enabledSkillPaths ?? [])
329
+ : existingStorage?.enabledSkillPathsJson ?? null;
330
+ const memoryNetworkEnabled = 'memoryNetworkEnabled' in req
331
+ ? (req.memoryNetworkEnabled ? 1 : 0)
332
+ : (existing.memoryNetworkEnabled ? 1 : 0);
333
+ if (memoryNetworkEnabled === 1 && isPlatformAgentRole(existing.agentRole)) {
334
+ throw new AgentConfigValidationError('Memory Network cannot be enabled for platform agents');
335
+ }
336
+ let dreamEnabled = 'dreamEnabled' in req
337
+ ? (req.dreamEnabled ? 1 : 0)
338
+ : (existing.dreamEnabled ? 1 : 0);
339
+ if (memoryNetworkEnabled === 0) {
340
+ dreamEnabled = 0;
341
+ }
342
+ if (dreamEnabled === 1 && memoryNetworkEnabled === 0) {
343
+ throw new AgentConfigValidationError('Dream requires Memory Network to be enabled');
344
+ }
345
+ if (dreamEnabled === 1 && isPlatformAgentRole(existing.agentRole)) {
346
+ throw new AgentConfigValidationError('Dream cannot be enabled for platform agents');
347
+ }
348
+ const dreamSchedule = 'dreamSchedule' in req
349
+ ? (req.dreamSchedule?.trim() || '02:00')
350
+ : (existing.dreamSchedule ?? '02:00');
351
+ if (!parseDreamScheduleHourMinute(dreamSchedule)) {
352
+ throw new AgentConfigValidationError('Invalid dream schedule; expected HH:mm');
353
+ }
354
+ const dreamMinMessages = 'dreamMinMessages' in req
355
+ ? Math.max(1, Math.floor(req.dreamMinMessages ?? 5))
356
+ : (existing.dreamMinMessages ?? 5);
357
+ this.db.prepare(`UPDATE agents
358
+ SET name = ?, agent_type = ?, system_prompt = ?, description = ?, model = ?, reasoning_effort = ?, claude_permission_mode = ?, codex_mode = ?, codex_service_tier = ?, env_vars = ?, disabled_tool_kinds = ?, channel_id = ?, project_path = ?, skill_roots = ?, enabled_skill_paths = ?, memory_network_enabled = ?, dream_enabled = ?, dream_schedule = ?, dream_min_messages = ?, updated_at = ?
359
+ WHERE agent_id = ?`).run(name, agentType, systemPrompt, description, model, reasoningEffort, claudePermissionMode, codexMode, codexServiceTier, envVarsJson, disabledToolKindsJson, channelId, projectPath, skillRootsJson, enabledSkillPathsJson, memoryNetworkEnabled, dreamEnabled, dreamSchedule, dreamMinMessages, now, agentId);
360
+ // Migrate home channel membership if channelId changed
361
+ if (req.channelId && req.channelId !== existing.channelId) {
362
+ this.db.prepare(`UPDATE agent_channel_memberships SET is_home = 0 WHERE agent_id = ? AND channel_id = ?`).run(agentId, existing.channelId);
363
+ this.db.prepare(`INSERT INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
364
+ VALUES(?, ?, 1, ?)
365
+ ON CONFLICT(agent_id, channel_id) DO UPDATE SET is_home = 1`).run(agentId, channelId, now);
366
+ }
367
+ return this.getAgent(agentId) ?? {
368
+ ...existing,
369
+ name,
370
+ agentType,
371
+ systemPrompt,
372
+ description: description ?? undefined,
373
+ model: model ?? undefined,
374
+ reasoningEffort: reasoningEffort ?? undefined,
375
+ claudePermissionMode: claudePermissionMode ?? undefined,
376
+ codexMode: codexMode ?? undefined,
377
+ codexServiceTier: codexServiceTier ?? undefined,
378
+ envVars,
379
+ disabledToolKinds,
380
+ projectPath: projectPath ?? undefined,
381
+ skillRoots,
382
+ ...(enabledSkillPaths !== undefined ? { enabledSkillPaths } : {}),
383
+ skillSelectionMode: enabledSkillPathsJson == null ? 'legacy_all' : 'explicit',
384
+ memoryNetworkEnabled: memoryNetworkEnabled === 1,
385
+ dreamEnabled: dreamEnabled === 1,
386
+ dreamSchedule,
387
+ dreamMinMessages,
388
+ channelId,
389
+ updatedAt: now,
390
+ };
391
+ }
392
+ async dispatchDream(agentId) {
393
+ return this.executionDispatcher.dispatchDream(agentId);
394
+ }
395
+ deleteAgent(agentId) {
396
+ const existing = this.db.prepare(`SELECT deleted_at as deletedAt FROM agents WHERE agent_id = ?`).get(agentId);
397
+ if (!existing)
398
+ return { deletedConversations: 0 };
399
+ const rows = this.db.prepare(`SELECT id, session_key as sessionKey
400
+ FROM conversations WHERE agent_id = ?`).all(agentId);
401
+ const dmChannelId = `dm:${agentId}`;
402
+ const now = Date.now();
403
+ const dmAttachmentPaths = this.listAgentDmAttachmentPaths(agentId);
404
+ const panelAttachmentPaths = [];
405
+ for (const row of rows) {
406
+ this.cancelConversationRun(row.id);
407
+ }
408
+ this.db.transaction(() => {
409
+ this.deleteTaskThreadFollowupsForConversationIds(rows.map((row) => row.id));
410
+ for (const row of rows) {
411
+ const deletedPanels = hardDeletePanelRowsForConversation(this.db, row.id);
412
+ panelAttachmentPaths.push(...deletedPanels.storagePaths);
413
+ const bindingKeys = this.db.prepare(`SELECT binding_key as bindingKey FROM bindings WHERE session_key = ?`).all(row.sessionKey);
414
+ const runIds = this.db.prepare(`SELECT run_id as runId FROM runs WHERE session_key = ?`)
415
+ .all(row.sessionKey);
416
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE conversation_id = ?').run(row.id);
417
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE conversation_id = ?').run(row.id);
418
+ for (const run of runIds) {
419
+ this.db.prepare('DELETE FROM delivery_checkpoints WHERE run_id = ?').run(run.runId);
420
+ this.db.prepare('DELETE FROM events WHERE run_id = ?').run(run.runId);
421
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE run_id = ?').run(run.runId);
422
+ }
423
+ this.db.prepare('DELETE FROM claude_session_user_messages WHERE session_key = ?').run(row.sessionKey);
424
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE session_key = ?').run(row.sessionKey);
425
+ this.db.prepare('DELETE FROM runs WHERE session_key = ?').run(row.sessionKey);
426
+ for (const binding of bindingKeys) {
427
+ this.db.prepare('DELETE FROM jobs WHERE binding_key = ?').run(binding.bindingKey);
428
+ this.db.prepare('DELETE FROM tool_policies WHERE binding_key = ?').run(binding.bindingKey);
429
+ this.db.prepare('DELETE FROM delivery_checkpoints WHERE binding_key = ?').run(binding.bindingKey);
430
+ this.db.prepare('DELETE FROM ui_prefs WHERE binding_key = ?').run(binding.bindingKey);
431
+ this.db.prepare('DELETE FROM tool_allow_prefixes WHERE binding_key = ?').run(binding.bindingKey);
432
+ }
433
+ this.db.prepare('DELETE FROM bindings WHERE session_key = ?').run(row.sessionKey);
434
+ this.db.prepare('DELETE FROM conversations WHERE id = ?').run(row.id);
435
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
436
+ }
437
+ const dmTaskIds = this.db.prepare(`SELECT task_id as taskId FROM tasks WHERE channel_id = ?`).all(dmChannelId).map((row) => row.taskId);
438
+ this.deleteTaskRuntimeStateForTaskIds(dmTaskIds);
439
+ this.db.prepare(`DELETE FROM tasks WHERE channel_id = ?`).run(dmChannelId);
440
+ deleteTaskEventsForChannel(this.db, dmChannelId);
441
+ this.db.prepare(`DELETE FROM channel_task_sequences WHERE channel_id = ?`).run(dmChannelId);
442
+ this.db.prepare(`DELETE FROM channel_messages WHERE channel_id = ?`).run(dmChannelId);
443
+ this.deleteTaskThreadFollowupsForChannel(dmChannelId);
444
+ deleteTargetParticipantsForChannel(this.db, dmChannelId);
445
+ this.deleteDmThreadContextSnapshotsForChannel(dmChannelId);
446
+ this.deleteAgentDmAttachmentRows(agentId);
447
+ this.unassignOpenChannelTasksForDeletedAgent(agentId, now);
448
+ this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE agent_id = ?`).run(agentId);
449
+ this.db.prepare(`DELETE FROM conversation_summaries WHERE agent_id = ?`).run(agentId);
450
+ clearRollingSummaryStateForAgent(this.db, agentId);
451
+ this.db.prepare(`DELETE FROM conversation_handoffs WHERE agent_id = ?`).run(agentId);
452
+ this.db.prepare(`DELETE FROM run_pending_tool_attachments WHERE tool_id IN (
453
+ SELECT tool_id FROM workspace_tools WHERE agent_id = ?
454
+ )`).run(agentId);
455
+ this.db.prepare(`UPDATE workspace_tool_runs
456
+ SET status = CASE WHEN status = 'running' THEN 'failed' ELSE status END,
457
+ ended_at = CASE WHEN ended_at IS NULL AND status = 'running' THEN ? ELSE ended_at END,
458
+ terminal_id = CASE WHEN status = 'running' THEN NULL ELSE terminal_id END
459
+ WHERE tool_id IN (SELECT tool_id FROM workspace_tools WHERE agent_id = ?)`).run(now, agentId);
460
+ this.db.prepare(`UPDATE workspace_tools
461
+ SET slug = CASE
462
+ WHEN deleted_at IS NULL AND slug NOT LIKE '__deleted__/%'
463
+ THEN '__deleted__/' || tool_id || '/' || slug
464
+ ELSE slug
465
+ END,
466
+ panel_id = NULL,
467
+ deleted_at = COALESCE(deleted_at, ?),
468
+ updated_at = ?,
469
+ active_terminal_id = NULL
470
+ WHERE agent_id = ?`).run(now, now, agentId);
471
+ this.db.prepare(`DELETE FROM agent_message_checkpoints WHERE agent_id = ?`).run(agentId);
472
+ this.db.prepare(`DELETE FROM agent_channel_memberships WHERE agent_id = ?`).run(agentId);
473
+ this.db.prepare(`DELETE FROM channel_subscriptions WHERE agent_id = ?`).run(agentId);
474
+ this.deleteWorkspaceSessionsForAgent(agentId);
475
+ this.db.prepare(`DELETE FROM agent_project_bindings WHERE agent_id = ?`).run(agentId);
476
+ this.db.prepare(`DELETE FROM workbench_workspace_agents WHERE agent_id = ?`).run(agentId);
477
+ this.db.prepare(`UPDATE workbench_workspaces
478
+ SET archived_at = COALESCE(archived_at, ?), updated_at = ?
479
+ WHERE agent_id = ?`).run(now, now, agentId);
480
+ this.db.prepare(`DELETE FROM agent_task_links WHERE agent_id = ?`).run(agentId);
481
+ this.db.prepare(`DELETE FROM task_participants WHERE agent_id = ?`).run(agentId);
482
+ deleteTargetParticipantsForAgent(this.db, agentId);
483
+ this.deleteAgentNotificationState(agentId);
484
+ this.db.prepare(`UPDATE agents
485
+ SET deleted_at = COALESCE(deleted_at, ?),
486
+ system_prompt = '',
487
+ description = NULL,
488
+ model = NULL,
489
+ reasoning_effort = NULL,
490
+ codex_mode = NULL,
491
+ codex_service_tier = NULL,
492
+ memory = '',
493
+ env_vars = NULL,
494
+ disabled_tool_kinds = NULL,
495
+ node_id = NULL,
496
+ workspace_path = NULL,
497
+ project_path = NULL,
498
+ skill_roots = NULL,
499
+ enabled_skill_paths = NULL,
500
+ updated_at = ?
501
+ WHERE agent_id = ?`).run(now, now, agentId);
502
+ })();
503
+ this.deleteAttachmentFiles(dmAttachmentPaths);
504
+ unlinkPanelAttachmentStoragePaths(panelAttachmentPaths);
505
+ return { deletedConversations: rows.length };
506
+ }
507
+ assertAgentNameAvailable(name, excludeAgentId) {
508
+ const normalizedName = normalizeAgentName(name);
509
+ if (!normalizedName)
510
+ return;
511
+ const rows = (excludeAgentId
512
+ ? this.db.prepare(`SELECT agent_id as agentId, name
513
+ FROM agents
514
+ WHERE deleted_at IS NULL
515
+ AND agent_id != ?`).all(excludeAgentId)
516
+ : this.db.prepare(`SELECT agent_id as agentId, name
517
+ FROM agents
518
+ WHERE deleted_at IS NULL`).all());
519
+ if (rows.some((row) => normalizeAgentName(row.name) === normalizedName)) {
520
+ throw new AgentNameConflictError();
521
+ }
522
+ }
523
+ listAgentDmAttachmentPaths(agentId) {
524
+ if (!this.tableExists('attachments'))
525
+ return [];
526
+ const rows = this.db.prepare(`SELECT DISTINCT storage_path as storagePath
527
+ FROM attachments
528
+ WHERE storage_path IS NOT NULL
529
+ AND (
530
+ channel_id = ?
531
+ OR channel_id LIKE ?
532
+ OR (agent_id = ? AND channel_id IS NULL AND user_id IS NULL)
533
+ )`).all(`dm:${agentId}`, `dm:${agentId}:%`, agentId);
534
+ return rows.map((row) => row.storagePath).filter((path) => !!path);
535
+ }
536
+ deleteAgentDmAttachmentRows(agentId) {
537
+ if (!this.tableExists('attachments'))
538
+ return;
539
+ this.db.prepare(`DELETE FROM attachments
540
+ WHERE channel_id = ?
541
+ OR channel_id LIKE ?
542
+ OR (agent_id = ? AND channel_id IS NULL AND user_id IS NULL)`).run(`dm:${agentId}`, `dm:${agentId}:%`, agentId);
543
+ }
544
+ deleteAttachmentFiles(paths) {
545
+ for (const storagePath of [...new Set(paths)]) {
546
+ try {
547
+ if (fs.existsSync(storagePath))
548
+ fs.unlinkSync(storagePath);
549
+ }
550
+ catch (error) {
551
+ log.warn('[conversation-manager] failed to delete agent DM attachment file', {
552
+ storagePath,
553
+ error: String(error?.message ?? error),
554
+ });
555
+ }
556
+ }
557
+ }
558
+ deleteTaskRuntimeStateForTaskIds(taskIds) {
559
+ const uniqueTaskIds = [...new Set(taskIds.filter(Boolean))];
560
+ if (uniqueTaskIds.length === 0)
561
+ return;
562
+ const placeholders = uniqueTaskIds.map(() => '?').join(', ');
563
+ this.db.prepare(`DELETE FROM task_loop_events
564
+ WHERE loop_id IN (SELECT loop_id FROM task_loops WHERE task_id IN (${placeholders}))`).run(...uniqueTaskIds);
565
+ this.db.prepare(`DELETE FROM task_loop_iterations
566
+ WHERE loop_id IN (SELECT loop_id FROM task_loops WHERE task_id IN (${placeholders}))`).run(...uniqueTaskIds);
567
+ this.db.prepare(`DELETE FROM task_loops WHERE task_id IN (${placeholders})`).run(...uniqueTaskIds);
568
+ this.db.prepare(`DELETE FROM thread_task_bindings WHERE task_id IN (${placeholders})`).run(...uniqueTaskIds);
569
+ this.db.prepare(`DELETE FROM task_thread_followups WHERE task_id IN (${placeholders})`).run(...uniqueTaskIds);
570
+ this.db.prepare(`DELETE FROM task_participants WHERE task_id IN (${placeholders})`).run(...uniqueTaskIds);
571
+ this.db.prepare(`DELETE FROM agent_task_links WHERE task_id IN (${placeholders})`).run(...uniqueTaskIds);
572
+ }
573
+ unassignOpenChannelTasksForDeletedAgent(agentId, now) {
574
+ const rows = this.db.prepare(`SELECT task_id as taskId,
575
+ agent_task_ref as agentTaskRef,
576
+ channel_id as channelId,
577
+ task_number as taskNumber,
578
+ title,
579
+ status,
580
+ message_id as messageId
581
+ FROM tasks
582
+ WHERE claimed_by_agent_id = ?
583
+ AND channel_id != ?
584
+ AND status != 'done'`).all(agentId, `dm:${agentId}`);
585
+ if (rows.length === 0)
586
+ return;
587
+ for (const row of rows) {
588
+ appendTaskEvent(this.db, {
589
+ taskId: row.taskId,
590
+ agentTaskRef: row.agentTaskRef,
591
+ channelId: row.channelId,
592
+ taskNumber: row.taskNumber,
593
+ eventType: 'unclaimed',
594
+ actorType: 'system',
595
+ actorId: 'system',
596
+ actorName: 'system',
597
+ fromStatus: row.status,
598
+ toStatus: row.status,
599
+ claimedByAgentIdAfter: null,
600
+ claimedByNameAfter: null,
601
+ messageId: row.messageId,
602
+ createdAt: now,
603
+ });
604
+ }
605
+ const taskIds = rows.map((row) => row.taskId);
606
+ const placeholders = taskIds.map(() => '?').join(', ');
607
+ this.db.prepare(`UPDATE task_loop_iterations
608
+ SET status = 'cancelled',
609
+ ended_at = COALESCE(ended_at, ?),
610
+ updated_at = ?
611
+ WHERE status IN ('pending', 'running', 'awaiting_approval')
612
+ AND loop_id IN (
613
+ SELECT loop_id FROM task_loops
614
+ WHERE worker_agent_id = ? AND task_id IN (${placeholders})
615
+ )`).run(now, now, agentId, ...taskIds);
616
+ this.db.prepare(`UPDATE task_loops
617
+ SET worker_agent_id = NULL,
618
+ worker_conversation_id = NULL,
619
+ status = CASE
620
+ WHEN status IN ('succeeded', 'failed', 'stopped', 'cancelled') THEN status
621
+ ELSE 'stopped'
622
+ END,
623
+ stop_reason = COALESCE(stop_reason, 'worker_agent_deleted'),
624
+ completed_at = CASE
625
+ WHEN status IN ('succeeded', 'failed', 'stopped', 'cancelled') THEN completed_at
626
+ ELSE COALESCE(completed_at, ?)
627
+ END,
628
+ updated_at = ?
629
+ WHERE worker_agent_id = ?
630
+ AND task_id IN (${placeholders})`).run(now, now, agentId, ...taskIds);
631
+ this.db.prepare(`UPDATE tasks
632
+ SET claimed_by_agent_id = NULL,
633
+ claimed_by_name = NULL,
634
+ assigned_by_user = NULL,
635
+ updated_at = ?
636
+ WHERE task_id IN (${placeholders})`).run(now, ...taskIds);
637
+ }
638
+ deleteDmThreadContextSnapshotsForChannel(channelId) {
639
+ if (!this.tableExists('dm_thread_context_snapshots'))
640
+ return;
641
+ this.db.prepare(`DELETE FROM dm_thread_context_snapshots WHERE channel_id = ?`).run(channelId);
642
+ }
643
+ deleteAgentNotificationState(agentId) {
644
+ deleteAgentNotificationV2State(this.db, agentId);
645
+ deletePeerInboxAggregatesForAgent(this.db, agentId);
646
+ this.db.prepare(`DELETE FROM agent_mention_cooldowns
647
+ WHERE from_agent_id = ? OR to_agent_id = ?`).run(agentId, agentId);
648
+ }
649
+ tableExists(tableName) {
650
+ return !!this.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`).get(tableName);
651
+ }
652
+ withRuntimeCapabilities(conversation) {
653
+ return {
654
+ ...conversation,
655
+ runtimeCapabilities: listEffectiveAgentRuntimeCapabilities(this.db, {
656
+ nodeId: conversation.nodeId,
657
+ agentType: conversation.agentType,
658
+ }),
659
+ };
660
+ }
661
+ // ─── CRUD ───
662
+ createConversation(params) {
663
+ const id = randomUUID();
664
+ const userId = params.userId ?? null;
665
+ // If agentId provided, inherit agent's settings as defaults
666
+ const agent = params.agentId ? this.getAgent(params.agentId) : null;
667
+ const agentType = params.agentType ?? (agent?.agentType ?? 'codex_app_server');
668
+ const workspacePath = params.workspacePath ?? agent?.workspacePath ?? this.config.workspaceRoot;
669
+ const title = params.title ?? '';
670
+ const channelId = params.channelId ?? agent?.channelId ?? 'default';
671
+ const nodeId = params.nodeId ?? agent?.nodeId ?? null;
672
+ const threadKind = params.threadKind ?? 'direct';
673
+ const isPrimaryThread = params.isPrimaryThread ?? false;
674
+ const threadRootId = params.threadRootId ?? null;
675
+ const activeProjectId = 'activeProjectId' in params
676
+ ? this.resolveConversationActiveProjectId(params.agentId ?? null, params.activeProjectId ?? null)
677
+ : this.resolveDefaultConversationActiveProjectId(params.agentId ?? null);
678
+ const surfaceKind = deriveSurfaceKind(threadKind, threadRootId);
679
+ const replyTarget = this.computeReplyTarget({
680
+ conversationId: id,
681
+ channelId,
682
+ threadKind,
683
+ isPrimaryThread,
684
+ threadRootId,
685
+ userId,
686
+ title,
687
+ });
688
+ const envVarsJson = (() => {
689
+ const ev = params.envVars ?? agent?.envVars;
690
+ return ev && Object.keys(ev).length > 0 ? JSON.stringify(ev) : null;
691
+ })();
692
+ const now = Date.now();
693
+ const sessionKey = randomUUID();
694
+ const preset = getRuntimeDriver(agentType);
695
+ // Create session row
696
+ createSession(this.db, {
697
+ sessionKey,
698
+ agentCommand: preset.command,
699
+ agentArgs: preset.args,
700
+ cwd: workspacePath,
701
+ loadSupported: false,
702
+ });
703
+ // bindingKey = web:{channelId}:{conversationId}:{agentType}
704
+ // → each agent type in each thread gets its own isolated session
705
+ upsertBinding(this.db, { platform: 'web', chatId: channelId, threadId: id, userId: agentType }, sessionKey);
706
+ // Create conversations row
707
+ this.db
708
+ .prepare(`INSERT INTO conversations(id, channel_id, reply_target, title, agent_type, workspace_path, session_key, status, thread_kind, is_primary_thread, thread_root_id, surface_kind, env_vars, node_id, agent_id, user_id, active_project_id, history_reset_at, created_at, updated_at)
709
+ VALUES(?, ?, ?, ?, ?, ?, ?, 'idle', ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`)
710
+ .run(id, channelId, replyTarget, title, agentType, workspacePath, sessionKey, threadKind, isPrimaryThread ? 1 : 0, threadRootId, surfaceKind, envVarsJson, nodeId, params.agentId ?? null, userId, activeProjectId, now, now);
711
+ if (params.agentId) {
712
+ const createdAgent = this.getAgent(params.agentId);
713
+ if (createdAgent) {
714
+ refreshConversationSummary(this.db, createdAgent, id);
715
+ }
716
+ }
717
+ return this.withRuntimeCapabilities({
718
+ id,
719
+ channelId,
720
+ replyTarget,
721
+ title,
722
+ agentType,
723
+ threadKind,
724
+ isPrimaryThread,
725
+ threadRootId,
726
+ surfaceKind,
727
+ workspacePath,
728
+ status: 'idle',
729
+ createdAt: now,
730
+ updatedAt: now,
731
+ historyResetAt: null,
732
+ nodeId,
733
+ agentId: params.agentId ?? null,
734
+ userId,
735
+ activeProjectId,
736
+ uiPanelEnabled: false,
737
+ });
738
+ }
739
+ openAgentThread(agentId, userId, activeProjectId) {
740
+ this.assertAgentNotDeleting(agentId);
741
+ const agent = this.getAgent(agentId);
742
+ if (!agent)
743
+ return null;
744
+ // Look for existing primary thread for this (agent, user) pair
745
+ const existing = userId
746
+ ? this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
747
+ reply_target as replyTarget,
748
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
749
+ thread_root_id as threadRootId,
750
+ surface_kind as surfaceKind,
751
+ workspace_path as workspacePath, status, node_id as nodeId,
752
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
753
+ ${UI_PANEL_ENABLED_SELECT_SQL},
754
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
755
+ FROM conversations
756
+ WHERE agent_id = ? AND user_id = ? AND is_primary_thread = 1
757
+ ORDER BY updated_at DESC
758
+ LIMIT 1`).get(agentId, userId)
759
+ : this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
760
+ reply_target as replyTarget,
761
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
762
+ thread_root_id as threadRootId,
763
+ surface_kind as surfaceKind,
764
+ workspace_path as workspacePath, status, node_id as nodeId,
765
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
766
+ ${UI_PANEL_ENABLED_SELECT_SQL},
767
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
768
+ FROM conversations
769
+ WHERE agent_id = ? AND user_id IS NULL AND is_primary_thread = 1
770
+ ORDER BY updated_at DESC
771
+ LIMIT 1`).get(agentId);
772
+ if (existing) {
773
+ const resolvedActiveProjectId = activeProjectId === undefined
774
+ ? existing.activeProjectId ?? null
775
+ : this.resolveConversationActiveProjectId(agentId, activeProjectId);
776
+ const canonicalReplyTarget = this.computeReplyTarget({
777
+ conversationId: existing.id,
778
+ channelId: existing.channelId,
779
+ threadKind: existing.threadKind,
780
+ isPrimaryThread: !!existing.isPrimaryThread,
781
+ threadRootId: existing.threadRootId ?? null,
782
+ userId: existing.userId ?? null,
783
+ title: existing.title,
784
+ });
785
+ if ((existing.replyTarget ?? '').trim() !== canonicalReplyTarget || (existing.activeProjectId ?? null) !== resolvedActiveProjectId) {
786
+ this.db.prepare(`UPDATE conversations
787
+ SET reply_target = ?, surface_kind = ?, active_project_id = ?, updated_at = ?
788
+ WHERE id = ?`).run(canonicalReplyTarget, deriveSurfaceKind(existing.threadKind, existing.threadRootId ?? null), resolvedActiveProjectId, Date.now(), existing.id);
789
+ existing.replyTarget = canonicalReplyTarget;
790
+ existing.surfaceKind = deriveSurfaceKind(existing.threadKind, existing.threadRootId ?? null);
791
+ existing.activeProjectId = resolvedActiveProjectId;
792
+ }
793
+ return this.withRuntimeCapabilities({
794
+ ...existing,
795
+ isPrimaryThread: !!existing.isPrimaryThread,
796
+ surfaceKind: parseSurfaceKind(existing.surfaceKind, {
797
+ threadKind: existing.threadKind,
798
+ threadRootId: existing.threadRootId ?? null,
799
+ }),
800
+ userId: existing.userId ?? null,
801
+ activeProjectId: resolvedActiveProjectId,
802
+ uiPanelEnabled: normalizeUiPanelEnabled(existing.uiPanelEnabled),
803
+ });
804
+ }
805
+ // Create new per-user primary thread
806
+ return this.createConversation({
807
+ agentId,
808
+ agentType: agent.agentType,
809
+ workspacePath: agent.workspacePath ?? undefined,
810
+ channelId: agent.channelId,
811
+ nodeId: agent.nodeId ?? undefined,
812
+ threadKind: 'direct',
813
+ isPrimaryThread: true,
814
+ title: '',
815
+ userId: userId ?? null,
816
+ ...(activeProjectId !== undefined ? { activeProjectId } : {}),
817
+ });
818
+ }
819
+ openNamedAgentPrimaryThread(params) {
820
+ this.assertAgentNotDeleting(params.agentId);
821
+ const agent = this.getAgent(params.agentId);
822
+ if (!agent)
823
+ return null;
824
+ const title = params.title.trim();
825
+ if (!title) {
826
+ return this.openAgentThread(params.agentId, params.userId, params.activeProjectId);
827
+ }
828
+ const userId = params.userId ?? null;
829
+ const trimmedLegacyTitles = (params.legacyTitles ?? [])
830
+ .map((value) => value.trim())
831
+ .filter((value) => value && value !== title);
832
+ const existing = userId
833
+ ? this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
834
+ reply_target as replyTarget,
835
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
836
+ thread_root_id as threadRootId,
837
+ surface_kind as surfaceKind,
838
+ workspace_path as workspacePath, status, node_id as nodeId,
839
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
840
+ ${UI_PANEL_ENABLED_SELECT_SQL},
841
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
842
+ FROM conversations
843
+ WHERE agent_id = ? AND user_id = ? AND is_primary_thread = 1 AND title = ?
844
+ ORDER BY updated_at DESC
845
+ LIMIT 1`).get(params.agentId, userId, title)
846
+ : this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
847
+ reply_target as replyTarget,
848
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
849
+ thread_root_id as threadRootId,
850
+ surface_kind as surfaceKind,
851
+ workspace_path as workspacePath, status, node_id as nodeId,
852
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
853
+ ${UI_PANEL_ENABLED_SELECT_SQL},
854
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
855
+ FROM conversations
856
+ WHERE agent_id = ? AND user_id IS NULL AND is_primary_thread = 1 AND title = ?
857
+ ORDER BY updated_at DESC
858
+ LIMIT 1`).get(params.agentId, title);
859
+ const legacyExisting = !existing && trimmedLegacyTitles.length > 0
860
+ ? (() => {
861
+ const placeholders = trimmedLegacyTitles.map(() => '?').join(', ');
862
+ return userId
863
+ ? this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
864
+ reply_target as replyTarget,
865
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
866
+ thread_root_id as threadRootId,
867
+ surface_kind as surfaceKind,
868
+ workspace_path as workspacePath, status, node_id as nodeId,
869
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
870
+ ${UI_PANEL_ENABLED_SELECT_SQL},
871
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
872
+ FROM conversations
873
+ WHERE agent_id = ? AND user_id = ? AND is_primary_thread = 1 AND title IN (${placeholders})
874
+ ORDER BY updated_at DESC
875
+ LIMIT 1`).get(params.agentId, userId, ...trimmedLegacyTitles)
876
+ : this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
877
+ reply_target as replyTarget,
878
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
879
+ thread_root_id as threadRootId,
880
+ surface_kind as surfaceKind,
881
+ workspace_path as workspacePath, status, node_id as nodeId,
882
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
883
+ ${UI_PANEL_ENABLED_SELECT_SQL},
884
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
885
+ FROM conversations
886
+ WHERE agent_id = ? AND user_id IS NULL AND is_primary_thread = 1 AND title IN (${placeholders})
887
+ ORDER BY updated_at DESC
888
+ LIMIT 1`).get(params.agentId, ...trimmedLegacyTitles);
889
+ })()
890
+ : undefined;
891
+ const resolvedExisting = existing ?? legacyExisting;
892
+ if (resolvedExisting) {
893
+ const resolvedActiveProjectId = params.activeProjectId === undefined
894
+ ? resolvedExisting.activeProjectId ?? null
895
+ : this.resolveConversationActiveProjectId(params.agentId, params.activeProjectId);
896
+ const canonicalReplyTarget = this.computeReplyTarget({
897
+ conversationId: resolvedExisting.id,
898
+ channelId: resolvedExisting.channelId,
899
+ threadKind: resolvedExisting.threadKind,
900
+ isPrimaryThread: !!resolvedExisting.isPrimaryThread,
901
+ threadRootId: resolvedExisting.threadRootId ?? null,
902
+ userId: resolvedExisting.userId ?? null,
903
+ title,
904
+ });
905
+ if ((resolvedExisting.replyTarget ?? '').trim() !== canonicalReplyTarget
906
+ || (resolvedExisting.activeProjectId ?? null) !== resolvedActiveProjectId
907
+ || resolvedExisting.title !== title) {
908
+ this.db.prepare(`UPDATE conversations
909
+ SET reply_target = ?, surface_kind = ?, active_project_id = ?, title = ?, updated_at = ?
910
+ WHERE id = ?`).run(canonicalReplyTarget, deriveSurfaceKind(resolvedExisting.threadKind, resolvedExisting.threadRootId ?? null), resolvedActiveProjectId, title, Date.now(), resolvedExisting.id);
911
+ resolvedExisting.replyTarget = canonicalReplyTarget;
912
+ resolvedExisting.surfaceKind = deriveSurfaceKind(resolvedExisting.threadKind, resolvedExisting.threadRootId ?? null);
913
+ resolvedExisting.activeProjectId = resolvedActiveProjectId;
914
+ resolvedExisting.title = title;
915
+ }
916
+ return this.withRuntimeCapabilities({
917
+ ...resolvedExisting,
918
+ isPrimaryThread: !!resolvedExisting.isPrimaryThread,
919
+ surfaceKind: parseSurfaceKind(resolvedExisting.surfaceKind, {
920
+ threadKind: resolvedExisting.threadKind,
921
+ threadRootId: resolvedExisting.threadRootId ?? null,
922
+ }),
923
+ userId: resolvedExisting.userId ?? null,
924
+ activeProjectId: resolvedActiveProjectId,
925
+ uiPanelEnabled: normalizeUiPanelEnabled(resolvedExisting.uiPanelEnabled),
926
+ });
927
+ }
928
+ return this.createConversation({
929
+ agentId: params.agentId,
930
+ title,
931
+ threadKind: 'direct',
932
+ isPrimaryThread: true,
933
+ userId,
934
+ activeProjectId: params.activeProjectId,
935
+ });
936
+ }
937
+ openAgentDirectThread(agentId, userId, threadRootId, activeProjectId) {
938
+ this.assertAgentNotDeleting(agentId);
939
+ const agent = this.getAgent(agentId);
940
+ if (!agent)
941
+ return null;
942
+ const normalizedThreadRootId = normalizeChannelThreadRootId(threadRootId);
943
+ if (!normalizedThreadRootId)
944
+ return null;
945
+ const dmChannelId = `dm:${agentId}`;
946
+ const threadRootLookup = resolveThreadRootLookup(this.db, dmChannelId, normalizedThreadRootId);
947
+ const canonicalThreadRootId = threadRootLookup?.canonicalThreadRootId ?? normalizedThreadRootId;
948
+ const fallbackThreadRootId = buildThreadShortId(normalizedThreadRootId);
949
+ const equivalentThreadRootIds = new Set([normalizedThreadRootId, fallbackThreadRootId, canonicalThreadRootId].filter((value) => Boolean(value)));
950
+ const existingCandidates = userId
951
+ ? this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
952
+ reply_target as replyTarget,
953
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
954
+ thread_root_id as threadRootId,
955
+ surface_kind as surfaceKind,
956
+ workspace_path as workspacePath, status, node_id as nodeId,
957
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
958
+ ${UI_PANEL_ENABLED_SELECT_SQL},
959
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
960
+ FROM conversations
961
+ WHERE agent_id = ? AND user_id = ? AND thread_kind = 'direct' AND is_primary_thread = 0
962
+ ORDER BY updated_at DESC`).all(agentId, userId)
963
+ : this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
964
+ reply_target as replyTarget,
965
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
966
+ thread_root_id as threadRootId,
967
+ surface_kind as surfaceKind,
968
+ workspace_path as workspacePath, status, node_id as nodeId,
969
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
970
+ ${UI_PANEL_ENABLED_SELECT_SQL},
971
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
972
+ FROM conversations
973
+ WHERE agent_id = ? AND user_id IS NULL AND thread_kind = 'direct' AND is_primary_thread = 0
974
+ ORDER BY updated_at DESC`).all(agentId);
975
+ const existing = existingCandidates.find((candidate) => (typeof candidate.threadRootId === 'string' && equivalentThreadRootIds.has(candidate.threadRootId)));
976
+ if (existing) {
977
+ const resolvedActiveProjectId = activeProjectId === undefined
978
+ ? existing.activeProjectId ?? null
979
+ : this.resolveConversationActiveProjectId(agentId, activeProjectId);
980
+ const resolvedThreadRootId = canonicalThreadRootId ?? existing.threadRootId ?? normalizedThreadRootId;
981
+ const canonicalReplyTarget = this.computeReplyTarget({
982
+ conversationId: existing.id,
983
+ channelId: existing.channelId,
984
+ threadKind: existing.threadKind,
985
+ isPrimaryThread: false,
986
+ threadRootId: resolvedThreadRootId,
987
+ userId: existing.userId ?? userId ?? null,
988
+ });
989
+ if ((existing.replyTarget ?? '').trim() !== canonicalReplyTarget
990
+ || (existing.threadRootId ?? null) !== resolvedThreadRootId
991
+ || (existing.activeProjectId ?? null) !== resolvedActiveProjectId) {
992
+ this.db.prepare(`UPDATE conversations
993
+ SET reply_target = ?, thread_root_id = ?, surface_kind = ?, active_project_id = ?, updated_at = ?
994
+ WHERE id = ?`).run(canonicalReplyTarget, resolvedThreadRootId, deriveSurfaceKind(existing.threadKind, resolvedThreadRootId), resolvedActiveProjectId, Date.now(), existing.id);
995
+ existing.replyTarget = canonicalReplyTarget;
996
+ existing.threadRootId = resolvedThreadRootId;
997
+ existing.surfaceKind = deriveSurfaceKind(existing.threadKind, resolvedThreadRootId);
998
+ existing.activeProjectId = resolvedActiveProjectId;
999
+ }
1000
+ return this.withRuntimeCapabilities({
1001
+ ...existing,
1002
+ isPrimaryThread: !!existing.isPrimaryThread,
1003
+ threadRootId: existing.threadRootId ?? resolvedThreadRootId,
1004
+ surfaceKind: parseSurfaceKind(existing.surfaceKind, {
1005
+ threadKind: existing.threadKind,
1006
+ threadRootId: existing.threadRootId ?? resolvedThreadRootId,
1007
+ }),
1008
+ userId: existing.userId ?? userId ?? null,
1009
+ activeProjectId: resolvedActiveProjectId,
1010
+ uiPanelEnabled: normalizeUiPanelEnabled(existing.uiPanelEnabled),
1011
+ });
1012
+ }
1013
+ return this.createConversation({
1014
+ agentId,
1015
+ agentType: agent.agentType,
1016
+ workspacePath: agent.workspacePath ?? undefined,
1017
+ channelId: agent.channelId,
1018
+ nodeId: agent.nodeId ?? undefined,
1019
+ threadKind: 'direct',
1020
+ isPrimaryThread: false,
1021
+ threadRootId: canonicalThreadRootId,
1022
+ title: '',
1023
+ userId: userId ?? null,
1024
+ ...(activeProjectId !== undefined ? { activeProjectId } : {}),
1025
+ });
1026
+ }
1027
+ openAgentChannelThread(agentId, channelId, threadRootId, activeProjectId) {
1028
+ this.assertAgentNotDeleting(agentId);
1029
+ const agent = this.getAgent(agentId);
1030
+ if (!agent)
1031
+ return null;
1032
+ const normalizedThreadRootId = normalizeChannelThreadRootId(threadRootId);
1033
+ const threadRootLookup = normalizedThreadRootId
1034
+ ? resolveThreadRootLookup(this.db, channelId, normalizedThreadRootId)
1035
+ : null;
1036
+ const canonicalThreadRootId = threadRootLookup?.canonicalThreadRootId ?? normalizedThreadRootId;
1037
+ const fallbackThreadRootId = normalizedThreadRootId ? buildThreadShortId(normalizedThreadRootId) : null;
1038
+ const equivalentThreadRootIds = new Set([normalizedThreadRootId, fallbackThreadRootId, canonicalThreadRootId].filter((value) => Boolean(value)));
1039
+ const existing = (normalizedThreadRootId
1040
+ ? this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
1041
+ reply_target as replyTarget,
1042
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
1043
+ thread_root_id as threadRootId,
1044
+ surface_kind as surfaceKind,
1045
+ workspace_path as workspacePath, status, node_id as nodeId,
1046
+ agent_id as agentId, active_project_id as activeProjectId,
1047
+ ${UI_PANEL_ENABLED_SELECT_SQL},
1048
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
1049
+ FROM conversations
1050
+ WHERE agent_id = ? AND channel_id = ? AND thread_kind = 'branch'
1051
+ ORDER BY updated_at DESC`).all(agentId, channelId).find((candidate) => (typeof candidate.threadRootId === 'string' && equivalentThreadRootIds.has(candidate.threadRootId)))
1052
+ : this.db.prepare(`SELECT id, channel_id as channelId, title, agent_type as agentType,
1053
+ reply_target as replyTarget,
1054
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
1055
+ thread_root_id as threadRootId,
1056
+ surface_kind as surfaceKind,
1057
+ workspace_path as workspacePath, status, node_id as nodeId,
1058
+ agent_id as agentId, active_project_id as activeProjectId,
1059
+ ${UI_PANEL_ENABLED_SELECT_SQL},
1060
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
1061
+ FROM conversations
1062
+ WHERE agent_id = ? AND channel_id = ? AND thread_kind = 'branch' AND thread_root_id IS NULL
1063
+ ORDER BY updated_at DESC
1064
+ LIMIT 1`).get(agentId, channelId));
1065
+ if (existing) {
1066
+ const resolvedActiveProjectId = activeProjectId === undefined
1067
+ ? existing.activeProjectId ?? null
1068
+ : this.resolveConversationActiveProjectId(agentId, activeProjectId);
1069
+ const resolvedThreadRootId = canonicalThreadRootId ?? existing.threadRootId ?? normalizedThreadRootId;
1070
+ const canonicalReplyTarget = this.computeReplyTarget({
1071
+ conversationId: existing.id,
1072
+ channelId: existing.channelId,
1073
+ threadKind: existing.threadKind,
1074
+ isPrimaryThread: false,
1075
+ threadRootId: resolvedThreadRootId,
1076
+ });
1077
+ if ((existing.replyTarget ?? '').trim() !== canonicalReplyTarget
1078
+ || (existing.threadRootId ?? null) !== resolvedThreadRootId
1079
+ || (existing.activeProjectId ?? null) !== resolvedActiveProjectId) {
1080
+ this.db.prepare(`UPDATE conversations
1081
+ SET reply_target = ?, thread_root_id = ?, surface_kind = ?, active_project_id = ?, updated_at = ?
1082
+ WHERE id = ?`).run(canonicalReplyTarget, resolvedThreadRootId, deriveSurfaceKind(existing.threadKind, resolvedThreadRootId), resolvedActiveProjectId, Date.now(), existing.id);
1083
+ existing.replyTarget = canonicalReplyTarget;
1084
+ existing.threadRootId = resolvedThreadRootId;
1085
+ existing.surfaceKind = deriveSurfaceKind(existing.threadKind, resolvedThreadRootId);
1086
+ existing.activeProjectId = resolvedActiveProjectId;
1087
+ }
1088
+ return this.withRuntimeCapabilities({
1089
+ ...existing,
1090
+ isPrimaryThread: !!existing.isPrimaryThread,
1091
+ threadRootId: existing.threadRootId ?? resolvedThreadRootId,
1092
+ surfaceKind: parseSurfaceKind(existing.surfaceKind, {
1093
+ threadKind: existing.threadKind,
1094
+ threadRootId: existing.threadRootId ?? resolvedThreadRootId,
1095
+ }),
1096
+ activeProjectId: resolvedActiveProjectId,
1097
+ uiPanelEnabled: normalizeUiPanelEnabled(existing.uiPanelEnabled),
1098
+ });
1099
+ }
1100
+ return this.createConversation({
1101
+ agentId,
1102
+ agentType: agent.agentType,
1103
+ workspacePath: agent.workspacePath ?? undefined,
1104
+ channelId,
1105
+ nodeId: agent.nodeId ?? undefined,
1106
+ threadKind: 'branch',
1107
+ isPrimaryThread: false,
1108
+ threadRootId: canonicalThreadRootId,
1109
+ title: '',
1110
+ ...(activeProjectId !== undefined ? { activeProjectId } : {}),
1111
+ });
1112
+ }
1113
+ listConversations(filter) {
1114
+ const convSelect = `SELECT id, channel_id as channelId, reply_target as replyTarget, title, agent_type as agentType,
1115
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
1116
+ thread_root_id as threadRootId,
1117
+ surface_kind as surfaceKind,
1118
+ workspace_path as workspacePath, status, node_id as nodeId,
1119
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
1120
+ ${UI_PANEL_ENABLED_SELECT_SQL},
1121
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
1122
+ FROM conversations`;
1123
+ const mapRows = (rows) => rows.map((row) => this.withRuntimeCapabilities({
1124
+ ...row,
1125
+ isPrimaryThread: !!row.isPrimaryThread,
1126
+ threadRootId: row.threadRootId ?? null,
1127
+ surfaceKind: parseSurfaceKind(row.surfaceKind, {
1128
+ threadKind: row.threadKind,
1129
+ threadRootId: row.threadRootId ?? null,
1130
+ }),
1131
+ historyResetAt: row.historyResetAt ?? null,
1132
+ userId: row.userId ?? null,
1133
+ activeProjectId: row.activeProjectId ?? null,
1134
+ uiPanelEnabled: normalizeUiPanelEnabled(row.uiPanelEnabled),
1135
+ }));
1136
+ if (filter?.channelId && filter?.agentId) {
1137
+ return mapRows(this.db.prepare(`${convSelect} WHERE channel_id = ? AND agent_id = ? ORDER BY is_primary_thread DESC, updated_at DESC`)
1138
+ .all(filter.channelId, filter.agentId));
1139
+ }
1140
+ if (filter?.channelId) {
1141
+ return mapRows(this.db.prepare(`${convSelect} WHERE channel_id = ? ORDER BY updated_at DESC`)
1142
+ .all(filter.channelId));
1143
+ }
1144
+ if (filter?.agentId) {
1145
+ return mapRows(this.db.prepare(`${convSelect} WHERE agent_id = ? ORDER BY is_primary_thread DESC, updated_at DESC`)
1146
+ .all(filter.agentId));
1147
+ }
1148
+ // Top-level list: apply per-user DM filter
1149
+ // Real channel threads (branch in non-default channel) are shared; everything else is per-user
1150
+ if (filter?.userId) {
1151
+ if (filter.isAdmin) {
1152
+ // Admin: real channel branches (shared) + their own DMs + legacy NULL-user DMs
1153
+ return mapRows(this.db.prepare(`${convSelect} WHERE (
1154
+ (thread_kind = 'branch' AND channel_id != 'default')
1155
+ OR user_id = ?
1156
+ OR user_id IS NULL
1157
+ ) ORDER BY updated_at DESC`).all(filter.userId));
1158
+ }
1159
+ else {
1160
+ // Regular user: real channel branches (shared) + their own DMs only
1161
+ return mapRows(this.db.prepare(`${convSelect} WHERE (
1162
+ (thread_kind = 'branch' AND channel_id != 'default')
1163
+ OR user_id = ?
1164
+ ) ORDER BY updated_at DESC`).all(filter.userId));
1165
+ }
1166
+ }
1167
+ return mapRows(this.db.prepare(`${convSelect} ORDER BY updated_at DESC`).all());
1168
+ }
1169
+ getConversation(id) {
1170
+ const row = this.db
1171
+ .prepare(`SELECT id, channel_id as channelId, reply_target as replyTarget, title, agent_type as agentType,
1172
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread,
1173
+ thread_root_id as threadRootId,
1174
+ surface_kind as surfaceKind,
1175
+ workspace_path as workspacePath, status, node_id as nodeId,
1176
+ agent_id as agentId, user_id as userId, active_project_id as activeProjectId,
1177
+ ${UI_PANEL_ENABLED_SELECT_SQL},
1178
+ history_reset_at as historyResetAt, created_at as createdAt, updated_at as updatedAt
1179
+ FROM conversations WHERE id = ?`)
1180
+ .get(id);
1181
+ return row
1182
+ ? this.withRuntimeCapabilities({
1183
+ ...row,
1184
+ isPrimaryThread: !!row.isPrimaryThread,
1185
+ threadRootId: row.threadRootId ?? null,
1186
+ surfaceKind: parseSurfaceKind(row.surfaceKind, {
1187
+ threadKind: row.threadKind,
1188
+ threadRootId: row.threadRootId ?? null,
1189
+ }),
1190
+ historyResetAt: row.historyResetAt ?? null,
1191
+ userId: row.userId ?? null,
1192
+ activeProjectId: row.activeProjectId ?? null,
1193
+ uiPanelEnabled: normalizeUiPanelEnabled(row.uiPanelEnabled),
1194
+ })
1195
+ : null;
1196
+ }
1197
+ enableBuiltinSkillRootForConversation(conversationId, builtinSkillRoot) {
1198
+ const row = this.db.prepare(`SELECT builtin_skill_roots as builtinSkillRootsJson
1199
+ FROM conversations
1200
+ WHERE id = ?`).get(conversationId);
1201
+ if (!row)
1202
+ return;
1203
+ const mergedBuiltinSkillRoots = serializeConversationBuiltinSkillRoots([
1204
+ ...parseConversationBuiltinSkillRoots(row.builtinSkillRootsJson),
1205
+ builtinSkillRoot,
1206
+ ]);
1207
+ this.db.prepare(`UPDATE conversations
1208
+ SET builtin_skill_roots = ?,
1209
+ updated_at = ?
1210
+ WHERE id = ?`).run(mergedBuiltinSkillRoots, Date.now(), conversationId);
1211
+ }
1212
+ disableBuiltinSkillRootForConversation(conversationId, builtinSkillRoot) {
1213
+ const row = this.db.prepare(`SELECT builtin_skill_roots as builtinSkillRootsJson
1214
+ FROM conversations
1215
+ WHERE id = ?`).get(conversationId);
1216
+ if (!row)
1217
+ return;
1218
+ const roots = parseConversationBuiltinSkillRoots(row.builtinSkillRootsJson);
1219
+ const filtered = roots.filter((r) => r !== builtinSkillRoot);
1220
+ this.db.prepare(`UPDATE conversations
1221
+ SET builtin_skill_roots = ?,
1222
+ updated_at = ?
1223
+ WHERE id = ?`).run(serializeConversationBuiltinSkillRoots(filtered), Date.now(), conversationId);
1224
+ }
1225
+ setUiPanelEnabledForConversation(conversationId, enabled) {
1226
+ const row = this.db.prepare(`SELECT builtin_skill_roots as builtinSkillRootsJson
1227
+ FROM conversations
1228
+ WHERE id = ?`).get(conversationId);
1229
+ if (!row)
1230
+ return;
1231
+ let roots = parseConversationBuiltinSkillRoots(row.builtinSkillRootsJson);
1232
+ if (enabled) {
1233
+ if (!roots.includes(BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL)) {
1234
+ roots = [...roots, BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL];
1235
+ }
1236
+ }
1237
+ else {
1238
+ roots = roots.filter((r) => r !== BUILTIN_UI_PANEL_SKILL_ROOT_SENTINEL);
1239
+ }
1240
+ this.db.prepare(`UPDATE conversations
1241
+ SET ui_panel_enabled = ?,
1242
+ builtin_skill_roots = ?,
1243
+ updated_at = ?
1244
+ WHERE id = ?`).run(enabled ? 1 : 0, serializeConversationBuiltinSkillRoots(roots), Date.now(), conversationId);
1245
+ }
1246
+ setConversationActiveProject(conversationId, activeProjectId) {
1247
+ const conversation = this.getConversation(conversationId);
1248
+ if (!conversation)
1249
+ return null;
1250
+ const resolvedActiveProjectId = this.resolveConversationActiveProjectId(conversation.agentId ?? null, activeProjectId ?? null);
1251
+ this.db.prepare(`UPDATE conversations
1252
+ SET active_project_id = ?, updated_at = ?
1253
+ WHERE id = ?`).run(resolvedActiveProjectId, Date.now(), conversationId);
1254
+ return this.getConversation(conversationId);
1255
+ }
1256
+ deleteConversation(id) {
1257
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE conversation_id = ?').run(id);
1258
+ hardDeletePanelsForConversation(this.db, id);
1259
+ this.clearWorkspaceToolMaintenanceConversationLinks(id);
1260
+ this.db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
1261
+ }
1262
+ clearWorkspaceToolMaintenanceConversationLinks(conversationId) {
1263
+ this.db.prepare(`UPDATE workspace_tools
1264
+ SET maintenance_conversation_id = NULL,
1265
+ updated_at = ?
1266
+ WHERE maintenance_conversation_id = ?`).run(Date.now(), conversationId);
1267
+ }
1268
+ deleteRunStateForSession(sessionKey) {
1269
+ const runIds = this.db.prepare(`SELECT run_id as runId FROM runs WHERE session_key = ?`).all(sessionKey);
1270
+ for (const run of runIds) {
1271
+ this.db.prepare('DELETE FROM delivery_checkpoints WHERE run_id = ?').run(run.runId);
1272
+ this.db.prepare('DELETE FROM events WHERE run_id = ?').run(run.runId);
1273
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE run_id = ?').run(run.runId);
1274
+ }
1275
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE session_key = ?').run(sessionKey);
1276
+ this.db.prepare('DELETE FROM runs WHERE session_key = ?').run(sessionKey);
1277
+ }
1278
+ deleteBindingStateForSession(sessionKey) {
1279
+ const bindingKeys = this.db.prepare(`SELECT binding_key as bindingKey
1280
+ FROM bindings
1281
+ WHERE session_key = ?`).all(sessionKey);
1282
+ for (const binding of bindingKeys) {
1283
+ this.db.prepare('DELETE FROM jobs WHERE binding_key = ?').run(binding.bindingKey);
1284
+ this.db.prepare('DELETE FROM tool_policies WHERE binding_key = ?').run(binding.bindingKey);
1285
+ this.db.prepare('DELETE FROM delivery_checkpoints WHERE binding_key = ?').run(binding.bindingKey);
1286
+ this.db.prepare('DELETE FROM ui_prefs WHERE binding_key = ?').run(binding.bindingKey);
1287
+ this.db.prepare('DELETE FROM tool_allow_prefixes WHERE binding_key = ?').run(binding.bindingKey);
1288
+ }
1289
+ this.db.prepare('DELETE FROM bindings WHERE session_key = ?').run(sessionKey);
1290
+ }
1291
+ deleteWorkspaceSessionsForAgent(agentId) {
1292
+ if (!this.tableExists('workspace_sessions'))
1293
+ return;
1294
+ const rows = this.db.prepare(`SELECT workspace_session_id as workspaceSessionId,
1295
+ session_key as sessionKey
1296
+ FROM workspace_sessions
1297
+ WHERE agent_id = ?`).all(agentId);
1298
+ for (const row of rows) {
1299
+ this.db.prepare('DELETE FROM workspace_sessions WHERE workspace_session_id = ?').run(row.workspaceSessionId);
1300
+ this.db.prepare('DELETE FROM claude_session_user_messages WHERE session_key = ?').run(row.sessionKey);
1301
+ this.deleteRunStateForSession(row.sessionKey);
1302
+ this.deleteBindingStateForSession(row.sessionKey);
1303
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
1304
+ }
1305
+ }
1306
+ deleteConversationRuntimeState(params) {
1307
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE conversation_id = ?').run(params.conversationId);
1308
+ hardDeletePanelsForConversation(this.db, params.conversationId);
1309
+ this.clearWorkspaceToolMaintenanceConversationLinks(params.conversationId);
1310
+ clearRollingSummaryStateForConversation(this.db, params.conversationId);
1311
+ this.db.prepare('DELETE FROM run_debug_inputs WHERE conversation_id = ?').run(params.conversationId);
1312
+ this.deleteRunStateForSession(params.sessionKey);
1313
+ this.deleteBindingStateForSession(params.sessionKey);
1314
+ if (params.channelId && params.threadRootId) {
1315
+ this.db.prepare(`DELETE FROM target_participants
1316
+ WHERE channel_id = ? AND thread_root_id = ?`).run(params.channelId, params.threadRootId);
1317
+ this.db.prepare(`DELETE FROM agent_message_checkpoints
1318
+ WHERE channel_id = ? AND thread_root_id = ?`).run(params.channelId, params.threadRootId);
1319
+ this.db.prepare(`DELETE FROM agent_mention_cooldowns
1320
+ WHERE channel_id = ? AND thread_root_id = ?`).run(params.channelId, params.threadRootId);
1321
+ deleteNotificationV2StateForThreadRoots(this.db, {
1322
+ channelId: params.channelId,
1323
+ threadRootIds: [params.threadRootId],
1324
+ });
1325
+ const hasDmThreadContextSnapshots = !!this.db.prepare(`SELECT 1
1326
+ FROM sqlite_master
1327
+ WHERE type = 'table' AND name = 'dm_thread_context_snapshots'
1328
+ LIMIT 1`).get();
1329
+ if (hasDmThreadContextSnapshots) {
1330
+ this.db.prepare(`DELETE FROM dm_thread_context_snapshots
1331
+ WHERE channel_id = ? AND thread_root_id = ?`).run(params.channelId, params.threadRootId);
1332
+ }
1333
+ }
1334
+ this.db.prepare('DELETE FROM conversations WHERE id = ?').run(params.conversationId);
1335
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(params.sessionKey);
1336
+ }
1337
+ deleteTaskThreadFollowupsForChannel(channelId) {
1338
+ this.db.prepare(`DELETE FROM task_thread_followups
1339
+ WHERE channel_id = ?`).run(channelId);
1340
+ }
1341
+ deleteTaskThreadFollowupsForConversationIds(conversationIds) {
1342
+ const uniqueIds = [...new Set(conversationIds.map((id) => id.trim()).filter(Boolean))];
1343
+ if (uniqueIds.length === 0)
1344
+ return;
1345
+ const placeholders = uniqueIds.map(() => '?').join(', ');
1346
+ this.db.prepare(`DELETE FROM task_thread_followups
1347
+ WHERE source_conversation_id IN (${placeholders})`).run(...uniqueIds);
1348
+ }
1349
+ listChannelTaskThreadRootIds(channelId) {
1350
+ const rows = this.db.prepare(`SELECT message_id as messageId
1351
+ FROM tasks
1352
+ WHERE channel_id = ?
1353
+ AND message_id IS NOT NULL
1354
+ AND trim(message_id) != ''`).all(channelId);
1355
+ return [...new Set(rows.map((row) => buildThreadShortId(row.messageId)))];
1356
+ }
1357
+ listChannelTaskRootMessageIds(channelId) {
1358
+ const rows = this.db.prepare(`SELECT message_id as messageId
1359
+ FROM tasks
1360
+ WHERE channel_id = ?
1361
+ AND message_id IS NOT NULL
1362
+ AND trim(message_id) != ''`).all(channelId);
1363
+ return [...new Set(rows.map((row) => row.messageId))];
1364
+ }
1365
+ listDirectTaskRootMessageIds(channelId, directTarget) {
1366
+ const brokenDmTaskTarget = `#${channelId}`;
1367
+ const rows = this.db.prepare(`SELECT t.message_id as messageId,
1368
+ t.dm_target as dmTarget,
1369
+ cm.target as messageTarget
1370
+ FROM tasks t
1371
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
1372
+ WHERE t.channel_id = ?
1373
+ AND t.message_id IS NOT NULL
1374
+ AND trim(t.message_id) != ''`).all(channelId);
1375
+ const taskRootMessageIds = new Set();
1376
+ for (const row of rows) {
1377
+ if (row.dmTarget === directTarget) {
1378
+ taskRootMessageIds.add(row.messageId);
1379
+ continue;
1380
+ }
1381
+ if (row.dmTarget)
1382
+ continue;
1383
+ if (row.messageTarget === directTarget) {
1384
+ taskRootMessageIds.add(row.messageId);
1385
+ continue;
1386
+ }
1387
+ if (row.messageTarget === brokenDmTaskTarget
1388
+ && (this.hasDirectThreadStateForTarget(channelId, row.messageId, directTarget)
1389
+ || !this.hasAnyDirectThreadStateForMessage(channelId, row.messageId))) {
1390
+ taskRootMessageIds.add(row.messageId);
1391
+ }
1392
+ }
1393
+ return [...taskRootMessageIds];
1394
+ }
1395
+ hasDirectThreadStateForTarget(channelId, rootMessageId, directTarget) {
1396
+ const directThreadTargetGlob = `${directTarget}:*`;
1397
+ const canonicalThreadRootId = buildThreadShortId(rootMessageId);
1398
+ const threadMessageRow = this.db.prepare(`SELECT 1
1399
+ FROM channel_messages
1400
+ WHERE channel_id = ?
1401
+ AND target GLOB ?
1402
+ AND thread_root_id IN (?, ?)
1403
+ LIMIT 1`).get(channelId, directThreadTargetGlob, canonicalThreadRootId, rootMessageId);
1404
+ if (threadMessageRow)
1405
+ return true;
1406
+ const agentId = channelId.startsWith('dm:') ? channelId.slice('dm:'.length) : null;
1407
+ if (!agentId)
1408
+ return false;
1409
+ const threadConversationRow = this.db.prepare(`SELECT 1
1410
+ FROM conversations
1411
+ WHERE agent_id = ?
1412
+ AND thread_kind = 'direct'
1413
+ AND is_primary_thread = 0
1414
+ AND reply_target GLOB ?
1415
+ AND thread_root_id IN (?, ?)
1416
+ LIMIT 1`).get(agentId, directThreadTargetGlob, canonicalThreadRootId, rootMessageId);
1417
+ return !!threadConversationRow;
1418
+ }
1419
+ hasAnyDirectThreadStateForMessage(channelId, rootMessageId) {
1420
+ const canonicalThreadRootId = buildThreadShortId(rootMessageId);
1421
+ const threadMessageRow = this.db.prepare(`SELECT 1
1422
+ FROM channel_messages
1423
+ WHERE channel_id = ?
1424
+ AND target GLOB 'dm:@*:*'
1425
+ AND thread_root_id IN (?, ?)
1426
+ LIMIT 1`).get(channelId, canonicalThreadRootId, rootMessageId);
1427
+ if (threadMessageRow)
1428
+ return true;
1429
+ const agentId = channelId.startsWith('dm:') ? channelId.slice('dm:'.length) : null;
1430
+ if (!agentId)
1431
+ return false;
1432
+ const threadConversationRow = this.db.prepare(`SELECT 1
1433
+ FROM conversations
1434
+ WHERE agent_id = ?
1435
+ AND thread_kind = 'direct'
1436
+ AND is_primary_thread = 0
1437
+ AND thread_root_id IN (?, ?)
1438
+ LIMIT 1`).get(agentId, canonicalThreadRootId, rootMessageId);
1439
+ return !!threadConversationRow;
1440
+ }
1441
+ listDirectPreservedTaskThreadRootIds(channelId, directTarget) {
1442
+ const taskRootMessageIds = this.listDirectTaskRootMessageIds(channelId, directTarget);
1443
+ if (taskRootMessageIds.length === 0)
1444
+ return [];
1445
+ const taskMessageIdSet = new Set(taskRootMessageIds);
1446
+ const canonicalTaskThreadRootIds = [...new Set(taskRootMessageIds.map((messageId) => buildThreadShortId(messageId)))];
1447
+ const candidateRows = this.db.prepare(`SELECT thread_root_id as threadRootId
1448
+ FROM conversations
1449
+ WHERE channel_id = ? AND thread_kind = 'branch' AND thread_root_id IS NOT NULL
1450
+ UNION
1451
+ SELECT thread_root_id as threadRootId
1452
+ FROM channel_messages
1453
+ WHERE channel_id = ? AND thread_root_id IS NOT NULL
1454
+ UNION
1455
+ SELECT thread_root_id as threadRootId
1456
+ FROM agent_message_checkpoints
1457
+ WHERE channel_id = ? AND trim(thread_root_id) != ''
1458
+ UNION
1459
+ SELECT thread_root_id as threadRootId
1460
+ FROM target_participants
1461
+ WHERE channel_id = ? AND trim(thread_root_id) != ''`).all(channelId, channelId, channelId, channelId);
1462
+ const preservedIds = new Set(canonicalTaskThreadRootIds);
1463
+ for (const row of candidateRows) {
1464
+ const threadRootId = row.threadRootId?.trim();
1465
+ if (!threadRootId)
1466
+ continue;
1467
+ const rootMessageId = findThreadRootMessageId(this.db, channelId, threadRootId);
1468
+ if (rootMessageId && taskMessageIdSet.has(rootMessageId)) {
1469
+ preservedIds.add(threadRootId);
1470
+ }
1471
+ }
1472
+ return [...preservedIds];
1473
+ }
1474
+ listChannelPreservedTaskThreadRootIds(channelId) {
1475
+ const taskRootMessageIds = this.listChannelTaskRootMessageIds(channelId);
1476
+ if (taskRootMessageIds.length === 0)
1477
+ return [];
1478
+ const taskMessageIdSet = new Set(taskRootMessageIds);
1479
+ const canonicalTaskThreadRootIds = this.listChannelTaskThreadRootIds(channelId);
1480
+ const candidateRows = this.db.prepare(`SELECT thread_root_id as threadRootId
1481
+ FROM conversations
1482
+ WHERE channel_id = ? AND thread_kind = 'branch' AND thread_root_id IS NOT NULL
1483
+ UNION
1484
+ SELECT thread_root_id as threadRootId
1485
+ FROM channel_messages
1486
+ WHERE channel_id = ? AND thread_root_id IS NOT NULL
1487
+ UNION
1488
+ SELECT thread_root_id as threadRootId
1489
+ FROM agent_message_checkpoints
1490
+ WHERE channel_id = ? AND trim(thread_root_id) != ''
1491
+ UNION
1492
+ SELECT thread_root_id as threadRootId
1493
+ FROM target_participants
1494
+ WHERE channel_id = ? AND trim(thread_root_id) != ''`).all(channelId, channelId, channelId, channelId);
1495
+ const preservedIds = new Set(canonicalTaskThreadRootIds);
1496
+ for (const row of candidateRows) {
1497
+ const threadRootId = row.threadRootId?.trim();
1498
+ if (!threadRootId)
1499
+ continue;
1500
+ const rootMessageId = findThreadRootMessageId(this.db, channelId, threadRootId);
1501
+ if (rootMessageId && taskMessageIdSet.has(rootMessageId)) {
1502
+ preservedIds.add(threadRootId);
1503
+ }
1504
+ }
1505
+ return [...preservedIds];
1506
+ }
1507
+ /** Returns hostKeys (conversation:{id}:{agentType}) for all conversations of this agent */
1508
+ getAgentHostKeys(agentId) {
1509
+ const rows = this.db.prepare(`SELECT id, agent_type as agentType, node_id as nodeId
1510
+ FROM conversations WHERE agent_id = ? AND node_id IS NOT NULL`).all(agentId);
1511
+ return rows.map((r) => ({ nodeId: r.nodeId, hostKey: `conversation:${r.id}:${r.agentType}` }));
1512
+ }
1513
+ getConversationHostKey(conversationId) {
1514
+ const row = this.db.prepare(`SELECT id, agent_type as agentType, node_id as nodeId
1515
+ FROM conversations WHERE id = ? AND node_id IS NOT NULL`).get(conversationId);
1516
+ if (!row)
1517
+ return null;
1518
+ return { nodeId: row.nodeId, hostKey: `conversation:${row.id}:${row.agentType}` };
1519
+ }
1520
+ /** Clear one conversation's chat/runtime state and rotate it to a new session (keeps workspace files). */
1521
+ clearConversationChat(conversationId) {
1522
+ const row = this.db.prepare(`SELECT id, channel_id as channelId, reply_target as replyTarget, agent_type as agentType,
1523
+ workspace_path as workspacePath, session_key as sessionKey, node_id as nodeId,
1524
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread, agent_id as agentId, user_id as userId
1525
+ FROM conversations
1526
+ WHERE id = ?`).get(conversationId);
1527
+ if (!row)
1528
+ return null;
1529
+ const now = Date.now();
1530
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE conversation_id = ?').run(conversationId);
1531
+ if (row.threadKind === 'direct' && row.agentId) {
1532
+ const directTarget = (row.replyTarget ?? '').trim();
1533
+ const dmChannelId = `dm:${row.agentId}`;
1534
+ const directTaskRootMessageIds = this.listDirectTaskRootMessageIds(dmChannelId, directTarget);
1535
+ const preservedTaskThreadRootIds = this.listDirectPreservedTaskThreadRootIds(dmChannelId, directTarget);
1536
+ const preservedTaskThreadRootIdSet = new Set(preservedTaskThreadRootIds);
1537
+ for (const taskThreadRootId of preservedTaskThreadRootIds) {
1538
+ ensureDmThreadContextSnapshot(this.db, {
1539
+ channelId: dmChannelId,
1540
+ directTarget,
1541
+ threadRootId: taskThreadRootId,
1542
+ });
1543
+ }
1544
+ if (directTaskRootMessageIds.length > 0) {
1545
+ const archiveTargetStmt = this.db.prepare(`UPDATE channel_messages
1546
+ SET target = ?
1547
+ WHERE channel_id = ?
1548
+ AND message_id = ?
1549
+ AND thread_root_id IS NULL`);
1550
+ const preserveDmTargetStmt = this.db.prepare(`UPDATE tasks
1551
+ SET dm_target = COALESCE(dm_target, ?)
1552
+ WHERE channel_id = ?
1553
+ AND message_id = ?`);
1554
+ for (const messageId of directTaskRootMessageIds) {
1555
+ const taskThreadRootId = buildThreadShortId(messageId);
1556
+ archiveTargetStmt.run(buildClearedTaskRootArchiveTarget(taskThreadRootId), dmChannelId, messageId);
1557
+ preserveDmTargetStmt.run(directTarget, dmChannelId, messageId);
1558
+ }
1559
+ }
1560
+ const siblingDirectThreadRows = (row.userId
1561
+ ? this.db.prepare(`SELECT id, session_key as sessionKey, node_id as nodeId, agent_type as agentType, thread_root_id as threadRootId
1562
+ FROM conversations
1563
+ WHERE agent_id = ?
1564
+ AND user_id = ?
1565
+ AND thread_kind = 'direct'
1566
+ AND is_primary_thread = 0
1567
+ AND reply_target GLOB ?`).all(row.agentId, row.userId, `${directTarget}:*`)
1568
+ : this.db.prepare(`SELECT id, session_key as sessionKey, node_id as nodeId, agent_type as agentType, thread_root_id as threadRootId
1569
+ FROM conversations
1570
+ WHERE agent_id = ?
1571
+ AND user_id IS NULL
1572
+ AND thread_kind = 'direct'
1573
+ AND is_primary_thread = 0
1574
+ AND reply_target GLOB ?`).all(row.agentId, `${directTarget}:*`));
1575
+ const siblingRowsToDelete = siblingDirectThreadRows.filter((sibling) => !sibling.threadRootId || !preservedTaskThreadRootIdSet.has(sibling.threadRootId));
1576
+ if (preservedTaskThreadRootIds.length > 0) {
1577
+ const taskThreadPlaceholders = preservedTaskThreadRootIds.map(() => '?').join(', ');
1578
+ this.db.prepare(`DELETE FROM channel_messages
1579
+ WHERE channel_id = ? AND (
1580
+ target = ?
1581
+ OR (
1582
+ target GLOB ?
1583
+ AND (
1584
+ thread_root_id IS NULL
1585
+ OR thread_root_id NOT IN (${taskThreadPlaceholders})
1586
+ )
1587
+ )
1588
+ )`).run(dmChannelId, directTarget, `${directTarget}:*`, ...preservedTaskThreadRootIds);
1589
+ }
1590
+ else {
1591
+ this.db.prepare(`DELETE FROM channel_messages
1592
+ WHERE channel_id = ?
1593
+ AND (
1594
+ target = ?
1595
+ OR target GLOB ?
1596
+ )`).run(dmChannelId, directTarget, `${directTarget}:*`);
1597
+ }
1598
+ this.deleteTaskThreadFollowupsForConversationIds(siblingRowsToDelete.map((sibling) => sibling.id));
1599
+ for (const sibling of siblingRowsToDelete) {
1600
+ if (sibling.nodeId && this.nodeRegistry) {
1601
+ this.nodeRegistry.send(sibling.nodeId, {
1602
+ type: 'host.close',
1603
+ hostKey: `conversation:${sibling.id}:${sibling.agentType}`,
1604
+ });
1605
+ }
1606
+ this.deleteConversationRuntimeState({
1607
+ conversationId: sibling.id,
1608
+ sessionKey: sibling.sessionKey,
1609
+ channelId: dmChannelId,
1610
+ threadRootId: sibling.threadRootId,
1611
+ });
1612
+ }
1613
+ }
1614
+ this.deleteRunStateForSession(row.sessionKey);
1615
+ clearRollingSummaryStateForConversation(this.db, row.id);
1616
+ const newSessionKey = randomUUID();
1617
+ const preset = getRuntimeDriver(row.agentType);
1618
+ createSession(this.db, {
1619
+ sessionKey: newSessionKey,
1620
+ agentCommand: preset.command,
1621
+ agentArgs: preset.args,
1622
+ cwd: row.workspacePath ?? this.config.workspaceRoot,
1623
+ loadSupported: false,
1624
+ });
1625
+ upsertBinding(this.db, { platform: 'web', chatId: row.channelId, threadId: row.id, userId: row.agentType }, newSessionKey);
1626
+ this.db.prepare(`UPDATE conversations
1627
+ SET session_key = ?, status = 'idle',
1628
+ title = CASE WHEN substr(title, 1, 10) = '[platform:' THEN title ELSE '' END,
1629
+ history_reset_at = ?, updated_at = ?
1630
+ WHERE id = ?`).run(newSessionKey, now, now, row.id);
1631
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
1632
+ return this.getConversation(conversationId);
1633
+ }
1634
+ /** Clear chat history and reset session (keeps workspace files) */
1635
+ clearAgentChat(agentId) {
1636
+ const rows = this.db.prepare(`SELECT id, channel_id as channelId, agent_type as agentType, workspace_path as workspacePath,
1637
+ session_key as sessionKey, node_id as nodeId, created_at as createdAt,
1638
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread
1639
+ FROM conversations
1640
+ WHERE agent_id = ?
1641
+ ORDER BY is_primary_thread DESC, updated_at DESC`).all(agentId);
1642
+ if (rows.length === 0)
1643
+ return [];
1644
+ const now = Date.now();
1645
+ this.deleteTaskThreadFollowupsForConversationIds(rows.map((row) => row.id));
1646
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE agent_id = ?').run(agentId);
1647
+ clearRollingSummaryStateForAgent(this.db, agentId);
1648
+ this.db.prepare(`DELETE FROM channel_messages WHERE channel_id = ?`).run(`dm:${agentId}`);
1649
+ this.deleteTaskThreadFollowupsForChannel(`dm:${agentId}`);
1650
+ this.db.prepare(`DELETE FROM agent_message_checkpoints WHERE agent_id = ?`).run(agentId);
1651
+ this.deleteAgentNotificationState(agentId);
1652
+ for (const row of rows) {
1653
+ this.deleteRunStateForSession(row.sessionKey);
1654
+ const newSessionKey = randomUUID();
1655
+ const preset = getRuntimeDriver(row.agentType);
1656
+ createSession(this.db, {
1657
+ sessionKey: newSessionKey,
1658
+ agentCommand: preset.command,
1659
+ agentArgs: preset.args,
1660
+ cwd: row.workspacePath ?? this.config.workspaceRoot,
1661
+ loadSupported: false,
1662
+ });
1663
+ upsertBinding(this.db, { platform: 'web', chatId: row.channelId, threadId: row.id, userId: row.agentType }, newSessionKey);
1664
+ this.db.prepare(`UPDATE conversations
1665
+ SET session_key = ?, status = 'idle',
1666
+ title = CASE WHEN substr(title, 1, 10) = '[platform:' THEN title ELSE '' END,
1667
+ history_reset_at = ?, updated_at = ?
1668
+ WHERE id = ?`).run(newSessionKey, now, now, row.id);
1669
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
1670
+ }
1671
+ return this.listConversations({ agentId });
1672
+ }
1673
+ resetAgent(agentId) {
1674
+ const rows = this.db.prepare(`SELECT id, channel_id as channelId, agent_type as agentType, workspace_path as workspacePath,
1675
+ session_key as sessionKey, node_id as nodeId, created_at as createdAt,
1676
+ thread_kind as threadKind, is_primary_thread as isPrimaryThread
1677
+ FROM conversations
1678
+ WHERE agent_id = ?
1679
+ ORDER BY is_primary_thread DESC, updated_at DESC`).all(agentId);
1680
+ if (rows.length === 0)
1681
+ return [];
1682
+ const now = Date.now();
1683
+ const dmChannelId = `dm:${agentId}`;
1684
+ this.deleteTaskThreadFollowupsForConversationIds(rows.map((row) => row.id));
1685
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE agent_id = ?').run(agentId);
1686
+ clearRollingSummaryStateForAgent(this.db, agentId);
1687
+ this.db.prepare(`DELETE FROM channel_messages WHERE channel_id = ?`).run(dmChannelId);
1688
+ this.deleteTaskThreadFollowupsForChannel(dmChannelId);
1689
+ this.db.prepare(`DELETE FROM tasks WHERE channel_id = ?`).run(dmChannelId);
1690
+ deleteTaskEventsForChannel(this.db, dmChannelId);
1691
+ this.db.prepare(`DELETE FROM channel_task_sequences WHERE channel_id = ?`).run(dmChannelId);
1692
+ deleteTargetParticipantsForChannel(this.db, dmChannelId);
1693
+ const hasDmThreadContextSnapshots = this.db
1694
+ .prepare(`SELECT 1 as hasRow
1695
+ FROM sqlite_master
1696
+ WHERE type = 'table' AND name = 'dm_thread_context_snapshots'
1697
+ LIMIT 1`)
1698
+ .get();
1699
+ if (hasDmThreadContextSnapshots) {
1700
+ this.db.prepare(`DELETE FROM dm_thread_context_snapshots WHERE channel_id = ?`).run(dmChannelId);
1701
+ }
1702
+ this.db.prepare(`DELETE FROM agent_message_checkpoints WHERE agent_id = ?`).run(agentId);
1703
+ this.deleteAgentNotificationState(agentId);
1704
+ for (const row of rows) {
1705
+ this.deleteRunStateForSession(row.sessionKey);
1706
+ const newSessionKey = randomUUID();
1707
+ const preset = getRuntimeDriver(row.agentType);
1708
+ createSession(this.db, {
1709
+ sessionKey: newSessionKey,
1710
+ agentCommand: preset.command,
1711
+ agentArgs: preset.args,
1712
+ cwd: row.workspacePath ?? this.config.workspaceRoot,
1713
+ loadSupported: false,
1714
+ });
1715
+ upsertBinding(this.db, { platform: 'web', chatId: row.channelId, threadId: row.id, userId: row.agentType }, newSessionKey);
1716
+ this.db.prepare(`UPDATE conversations
1717
+ SET session_key = ?, status = 'idle',
1718
+ title = CASE WHEN substr(title, 1, 10) = '[platform:' THEN title ELSE '' END,
1719
+ history_reset_at = ?, updated_at = ?
1720
+ WHERE id = ?`).run(newSessionKey, now, now, row.id);
1721
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
1722
+ }
1723
+ return this.listConversations({ agentId });
1724
+ }
1725
+ async dispatchToNode(conversationId, promptText) {
1726
+ const conversation = this.getConversation(conversationId);
1727
+ this.assertAgentNotDeleting(conversation?.agentId);
1728
+ await this.executionDispatcher.dispatchPrompt(conversationId, promptText);
1729
+ }
1730
+ async submitPrompt(conversationId, promptText, options) {
1731
+ const currentConversation = this.getConversation(conversationId);
1732
+ this.assertAgentNotDeleting(currentConversation?.agentId);
1733
+ const result = await this.executionDispatcher.submitPrompt(conversationId, promptText, options);
1734
+ const conversation = this.getConversation(conversationId);
1735
+ if (conversation?.agentId) {
1736
+ const agent = this.getAgent(conversation.agentId);
1737
+ if (agent) {
1738
+ refreshConversationSummary(this.db, agent, conversationId);
1739
+ }
1740
+ }
1741
+ return result;
1742
+ }
1743
+ async onConversationSettled(conversationId) {
1744
+ const conversation = this.getConversation(conversationId);
1745
+ if (this.isAgentDeletionInProgress(conversation?.agentId))
1746
+ return;
1747
+ await maybeRotateCodexConversationAfterSettlement({
1748
+ db: this.db,
1749
+ config: this.config,
1750
+ conversationId,
1751
+ nodeRegistry: this.nodeRegistry,
1752
+ codexAppServerBroker: this.codexAppServerBroker,
1753
+ agentRuntimeCleanupBroker: this.agentRuntimeCleanupBroker,
1754
+ });
1755
+ await this.executionDispatcher.handleConversationSettled(conversationId);
1756
+ }
1757
+ handleRunAccepted(runId, conversationId) {
1758
+ return this.executionDispatcher.handleRunAccepted(runId, conversationId);
1759
+ }
1760
+ handleNodeHostSnapshotCurrentRun(params) {
1761
+ return this.executionDispatcher.handleNodeHostSnapshotCurrentRun(params);
1762
+ }
1763
+ handleRunFinished(runId, conversationId, options) {
1764
+ return this.executionDispatcher.handleRunFinished(runId, conversationId, options);
1765
+ }
1766
+ handleRunSteerResult(params) {
1767
+ return this.executionDispatcher.handleRunSteerResult(params);
1768
+ }
1769
+ handleRunDeliveryAck(params) {
1770
+ return ackNotificationDelivery(this.db, params);
1771
+ }
1772
+ async sendBestEffortSystemNoticeToActiveRun(params) {
1773
+ return this.executionDispatcher.sendBestEffortSystemNoticeToActiveRun(params);
1774
+ }
1775
+ rejectPendingDispatchesForNode(nodeId, errorMessage) {
1776
+ this.executionDispatcher.rejectPendingDispatchesForNode(nodeId, errorMessage);
1777
+ }
1778
+ rejectPendingSteersForNode(nodeId, errorMessage) {
1779
+ this.executionDispatcher.rejectPendingSteersForNode(nodeId, errorMessage);
1780
+ }
1781
+ rejectPendingSteersForRun(runId, conversationId, errorMessage) {
1782
+ this.executionDispatcher.rejectPendingSteersForRun(runId, conversationId, errorMessage);
1783
+ }
1784
+ recoverSteerAcks() {
1785
+ this.executionDispatcher.recoverSteerAcks();
1786
+ }
1787
+ // ─── Channel CRUD ───
1788
+ joinChannel(agentId, channelId) {
1789
+ const now = Date.now();
1790
+ const membershipCount = this.db.prepare(`SELECT COUNT(*) as count FROM agent_channel_memberships WHERE agent_id = ?`).get(agentId);
1791
+ const makeHome = Number(membershipCount?.count ?? 0) === 0;
1792
+ this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
1793
+ VALUES(?, ?, ?, ?)`).run(agentId, channelId, makeHome ? 1 : 0, now);
1794
+ if (makeHome) {
1795
+ this.db.prepare(`UPDATE agents SET channel_id = ?, updated_at = ? WHERE agent_id = ?`)
1796
+ .run(channelId, now, agentId);
1797
+ }
1798
+ }
1799
+ leaveChannel(agentId, channelId) {
1800
+ const agent = this.getAgent(agentId);
1801
+ this.db.prepare(`DELETE FROM agent_channel_memberships WHERE agent_id = ? AND channel_id = ?`).run(agentId, channelId);
1802
+ deleteTargetParticipantsForAgentInChannel(this.db, { agentId, channelId });
1803
+ if (agent?.channelId === channelId) {
1804
+ const nextHome = this.db.prepare(`SELECT channel_id as channelId
1805
+ FROM agent_channel_memberships
1806
+ WHERE agent_id = ?
1807
+ ORDER BY is_home DESC, joined_at ASC
1808
+ LIMIT 1`).get(agentId);
1809
+ const nextChannelId = nextHome?.channelId ?? 'default';
1810
+ this.db.prepare(`UPDATE agents SET channel_id = ?, updated_at = ? WHERE agent_id = ?`).run(nextChannelId, Date.now(), agentId);
1811
+ this.db.prepare(`UPDATE agent_channel_memberships
1812
+ SET is_home = CASE WHEN channel_id = ? THEN 1 ELSE 0 END
1813
+ WHERE agent_id = ?`).run(nextChannelId, agentId);
1814
+ }
1815
+ }
1816
+ createChannel(params) {
1817
+ const channelId = params.name === 'default' ? 'default' : randomUUID();
1818
+ const now = Date.now();
1819
+ this.db
1820
+ .prepare(`INSERT INTO channels(channel_id, name, workspace_path, description, collaboration_mode, created_at, updated_at)
1821
+ VALUES(?, ?, ?, ?, ?, ?, ?)`)
1822
+ .run(channelId, params.name, params.workspacePath ?? null, params.description ?? null, params.collaborationMode ?? 'mention_only', now, now);
1823
+ return {
1824
+ channelId,
1825
+ name: params.name,
1826
+ workspacePath: params.workspacePath ?? null,
1827
+ description: params.description,
1828
+ collaborationMode: params.collaborationMode ?? 'mention_only',
1829
+ members: [],
1830
+ createdAt: now,
1831
+ updatedAt: now,
1832
+ };
1833
+ }
1834
+ updateChannel(channelId, req) {
1835
+ const existing = this.getChannel(channelId);
1836
+ if (!existing)
1837
+ return null;
1838
+ const now = Date.now();
1839
+ this.db.prepare(`UPDATE channels
1840
+ SET description = ?, collaboration_mode = ?, updated_at = ?
1841
+ WHERE channel_id = ?`).run(req.description ?? existing.description ?? null, req.collaborationMode ?? existing.collaborationMode ?? 'mention_only', now, channelId);
1842
+ return {
1843
+ ...existing,
1844
+ description: req.description ?? existing.description,
1845
+ collaborationMode: req.collaborationMode ?? existing.collaborationMode,
1846
+ updatedAt: now,
1847
+ };
1848
+ }
1849
+ listChannelBranchConversationsForChatClear(channelId) {
1850
+ return this.listConversations({ channelId }).filter((conversation) => conversation.threadKind === 'branch');
1851
+ }
1852
+ clearChannelChat(channelId) {
1853
+ const rows = this.db.prepare(`SELECT id, channel_id as channelId, agent_type as agentType, workspace_path as workspacePath,
1854
+ session_key as sessionKey, thread_root_id as threadRootId
1855
+ FROM conversations
1856
+ WHERE channel_id = ? AND thread_kind = 'branch'
1857
+ ORDER BY updated_at DESC`).all(channelId);
1858
+ const now = Date.now();
1859
+ const preservedTaskRootMessageIds = this.listChannelTaskRootMessageIds(channelId);
1860
+ const rowsToReset = rows;
1861
+ if (preservedTaskRootMessageIds.length > 0) {
1862
+ const taskRootPlaceholders = preservedTaskRootMessageIds.map(() => '?').join(', ');
1863
+ this.db.prepare(`DELETE FROM channel_messages
1864
+ WHERE channel_id = ?
1865
+ AND (
1866
+ (
1867
+ (thread_root_id IS NULL OR thread_root_id = '')
1868
+ AND message_id NOT IN (${taskRootPlaceholders})
1869
+ )
1870
+ OR (
1871
+ thread_root_id IS NOT NULL
1872
+ )
1873
+ )`).run(channelId, ...preservedTaskRootMessageIds);
1874
+ this.db.prepare(`DELETE FROM agent_message_checkpoints
1875
+ WHERE channel_id = ?`).run(channelId);
1876
+ this.db.prepare(`DELETE FROM target_participants
1877
+ WHERE channel_id = ?`).run(channelId);
1878
+ clearAgentMentionCooldownsForChannelChatClear(this.db, {
1879
+ channelId,
1880
+ preservedThreadRootIds: [],
1881
+ });
1882
+ deleteNotificationV2StateForChannel(this.db, channelId);
1883
+ deletePeerInboxAggregatesForChannel(this.db, channelId);
1884
+ }
1885
+ else {
1886
+ if (preservedTaskRootMessageIds.length > 0) {
1887
+ const taskRootPlaceholders = preservedTaskRootMessageIds.map(() => '?').join(', ');
1888
+ this.db.prepare(`DELETE FROM channel_messages
1889
+ WHERE channel_id = ?
1890
+ AND (
1891
+ thread_root_id IS NOT NULL
1892
+ OR message_id NOT IN (${taskRootPlaceholders})
1893
+ )`).run(channelId, ...preservedTaskRootMessageIds);
1894
+ }
1895
+ else {
1896
+ this.db.prepare(`DELETE FROM channel_messages
1897
+ WHERE channel_id = ?`).run(channelId);
1898
+ }
1899
+ this.db.prepare(`DELETE FROM agent_message_checkpoints WHERE channel_id = ?`).run(channelId);
1900
+ deleteTargetParticipantsForChannel(this.db, channelId);
1901
+ clearAgentMentionCooldownsForChannelChatClear(this.db, { channelId });
1902
+ deleteNotificationV2StateForChannel(this.db, channelId);
1903
+ deletePeerInboxAggregatesForChannel(this.db, channelId);
1904
+ }
1905
+ this.deleteTaskThreadFollowupsForChannel(channelId);
1906
+ if (preservedTaskRootMessageIds.length > 0) {
1907
+ const preserveRootStmt = this.db.prepare(`UPDATE channel_messages
1908
+ SET message_source = ?, target = ?
1909
+ WHERE channel_id = ?
1910
+ AND message_id = ?
1911
+ AND COALESCE(thread_root_id, '') = ''`);
1912
+ for (const messageId of preservedTaskRootMessageIds) {
1913
+ preserveRootStmt.run(CHANNEL_TASK_ROOT_PRESERVED_SOURCE, buildClearedTaskRootArchiveTarget(buildThreadShortId(messageId)), channelId, messageId);
1914
+ }
1915
+ }
1916
+ for (const row of rowsToReset) {
1917
+ this.db.prepare('DELETE FROM conversation_prompt_queue WHERE conversation_id = ?').run(row.id);
1918
+ clearRollingSummaryStateForConversation(this.db, row.id);
1919
+ this.deleteRunStateForSession(row.sessionKey);
1920
+ const newSessionKey = randomUUID();
1921
+ const preset = getRuntimeDriver(row.agentType);
1922
+ createSession(this.db, {
1923
+ sessionKey: newSessionKey,
1924
+ agentCommand: preset.command,
1925
+ agentArgs: preset.args,
1926
+ cwd: row.workspacePath ?? this.config.workspaceRoot,
1927
+ loadSupported: false,
1928
+ });
1929
+ upsertBinding(this.db, { platform: 'web', chatId: row.channelId, threadId: row.id, userId: row.agentType }, newSessionKey);
1930
+ this.db.prepare(`UPDATE conversations
1931
+ SET session_key = ?, status = 'idle',
1932
+ title = CASE WHEN substr(title, 1, 10) = '[platform:' THEN title ELSE '' END,
1933
+ history_reset_at = ?, updated_at = ?
1934
+ WHERE id = ?`).run(newSessionKey, now, now, row.id);
1935
+ this.db.prepare('DELETE FROM sessions WHERE session_key = ?').run(row.sessionKey);
1936
+ }
1937
+ const resetConversationIds = new Set(rowsToReset.map((row) => row.id));
1938
+ return this.listConversations({ channelId }).filter((item) => resetConversationIds.has(item.id));
1939
+ }
1940
+ listChannels() {
1941
+ const rows = this.db
1942
+ .prepare(`SELECT channel_id as channelId, name, workspace_path as workspacePath,
1943
+ description,
1944
+ collaboration_mode as collaborationMode,
1945
+ created_at as createdAt, updated_at as updatedAt
1946
+ FROM channels ORDER BY created_at ASC`)
1947
+ .all();
1948
+ return rows.map((row) => ({
1949
+ ...row,
1950
+ members: this.listChannelMembers(row.channelId),
1951
+ }));
1952
+ }
1953
+ getChannel(channelId) {
1954
+ const row = this.db
1955
+ .prepare(`SELECT channel_id as channelId, name, workspace_path as workspacePath,
1956
+ description,
1957
+ collaboration_mode as collaborationMode,
1958
+ created_at as createdAt, updated_at as updatedAt
1959
+ FROM channels WHERE channel_id = ?`)
1960
+ .get(channelId);
1961
+ if (!row)
1962
+ return null;
1963
+ return {
1964
+ ...row,
1965
+ members: this.listChannelMembers(channelId),
1966
+ };
1967
+ }
1968
+ createResourceSpace(params) {
1969
+ const resourceSpaceId = randomUUID();
1970
+ const now = Date.now();
1971
+ const description = params.description?.trim() || null;
1972
+ const channelId = params.channelId?.trim() || null;
1973
+ const nodeId = params.nodeId?.trim() || null;
1974
+ this.db.prepare(`INSERT INTO resource_spaces(
1975
+ resource_space_id,
1976
+ name,
1977
+ resource_type,
1978
+ backend_type,
1979
+ node_id,
1980
+ root_path,
1981
+ channel_id,
1982
+ description,
1983
+ created_at,
1984
+ updated_at
1985
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(resourceSpaceId, params.name, params.resourceType, params.backendType, nodeId, params.rootPath, channelId, description, now, now);
1986
+ return {
1987
+ resourceSpaceId,
1988
+ name: params.name,
1989
+ resourceType: params.resourceType,
1990
+ backendType: params.backendType,
1991
+ nodeId,
1992
+ rootPath: params.rootPath,
1993
+ channelId,
1994
+ description,
1995
+ createdAt: now,
1996
+ updatedAt: now,
1997
+ };
1998
+ }
1999
+ updateResourceSpace(resourceSpaceId, req) {
2000
+ const existing = this.getResourceSpace(resourceSpaceId);
2001
+ if (!existing)
2002
+ return null;
2003
+ const nextName = req.name ?? existing.name;
2004
+ const nextResourceType = req.resourceType ?? existing.resourceType;
2005
+ const nextBackendType = req.backendType ?? existing.backendType;
2006
+ const nextNodeId = req.nodeId !== undefined ? (req.nodeId?.trim() || null) : (existing.nodeId ?? null);
2007
+ const nextRootPath = req.rootPath ?? existing.rootPath;
2008
+ const nextChannelId = req.channelId !== undefined ? (req.channelId?.trim() || null) : (existing.channelId ?? null);
2009
+ const nextDescription = req.description !== undefined
2010
+ ? (req.description?.trim() || null)
2011
+ : (existing.description ?? null);
2012
+ const now = Date.now();
2013
+ this.db.prepare(`UPDATE resource_spaces
2014
+ SET name = ?,
2015
+ resource_type = ?,
2016
+ backend_type = ?,
2017
+ node_id = ?,
2018
+ root_path = ?,
2019
+ channel_id = ?,
2020
+ description = ?,
2021
+ updated_at = ?
2022
+ WHERE resource_space_id = ?`).run(nextName, nextResourceType, nextBackendType, nextNodeId, nextRootPath, nextChannelId, nextDescription, now, resourceSpaceId);
2023
+ return {
2024
+ resourceSpaceId,
2025
+ name: nextName,
2026
+ resourceType: nextResourceType,
2027
+ backendType: nextBackendType,
2028
+ nodeId: nextNodeId,
2029
+ rootPath: nextRootPath,
2030
+ channelId: nextChannelId,
2031
+ description: nextDescription,
2032
+ createdAt: existing.createdAt,
2033
+ updatedAt: now,
2034
+ };
2035
+ }
2036
+ deleteResourceSpace(resourceSpaceId) {
2037
+ const result = this.db.prepare(`DELETE FROM resource_spaces
2038
+ WHERE resource_space_id = ?`).run(resourceSpaceId);
2039
+ return result.changes > 0;
2040
+ }
2041
+ listResourceSpaces() {
2042
+ return this.db.prepare(`SELECT resource_space_id as resourceSpaceId,
2043
+ name,
2044
+ resource_type as resourceType,
2045
+ backend_type as backendType,
2046
+ node_id as nodeId,
2047
+ root_path as rootPath,
2048
+ channel_id as channelId,
2049
+ description,
2050
+ created_at as createdAt,
2051
+ updated_at as updatedAt
2052
+ FROM resource_spaces
2053
+ ORDER BY created_at ASC`).all();
2054
+ }
2055
+ getResourceSpace(resourceSpaceId) {
2056
+ const row = this.db.prepare(`SELECT resource_space_id as resourceSpaceId,
2057
+ name,
2058
+ resource_type as resourceType,
2059
+ backend_type as backendType,
2060
+ node_id as nodeId,
2061
+ root_path as rootPath,
2062
+ channel_id as channelId,
2063
+ description,
2064
+ created_at as createdAt,
2065
+ updated_at as updatedAt
2066
+ FROM resource_spaces
2067
+ WHERE resource_space_id = ?`).get(resourceSpaceId);
2068
+ return row ?? null;
2069
+ }
2070
+ async handleApproval(conversationId, requestId, decision, selectedActionId, responseText, answers) {
2071
+ return this.executionDispatcher.handleApproval(conversationId, requestId, decision, selectedActionId, responseText, answers);
2072
+ }
2073
+ cancelConversationRun(conversationId, expectedRunId, options) {
2074
+ return this.executionDispatcher.cancelConversationRun(conversationId, expectedRunId, options);
2075
+ }
2076
+ updateStatus(conversationId, status) {
2077
+ this.db
2078
+ .prepare('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?')
2079
+ .run(status, Date.now(), conversationId);
2080
+ }
2081
+ computeReplyTarget(params) {
2082
+ if (params.threadKind === 'direct') {
2083
+ const userName = resolveDirectUserName(this.db, params.userId, this.config.humanUserName);
2084
+ return buildDirectReplyTarget({
2085
+ isPrimaryThread: params.isPrimaryThread,
2086
+ userName,
2087
+ threadRootId: params.threadRootId,
2088
+ primaryScope: params.isPrimaryThread ? getPlatformDirectReplyScope(params.title) : null,
2089
+ });
2090
+ }
2091
+ const channel = this.getChannel(params.channelId);
2092
+ const channelName = channel?.name ?? params.channelId;
2093
+ const baseTarget = `#${channelName}`;
2094
+ return params.threadRootId ? `${baseTarget}:${params.threadRootId}` : baseTarget;
2095
+ }
2096
+ backfillConversationReplyTargets() {
2097
+ const rows = this.db.prepare(`SELECT id, channel_id as channelId, title,
2098
+ thread_kind as threadKind,
2099
+ is_primary_thread as isPrimaryThread, thread_root_id as threadRootId,
2100
+ user_id as userId, reply_target as replyTarget, surface_kind as surfaceKind
2101
+ FROM conversations
2102
+ WHERE thread_kind = 'direct' OR reply_target IS NULL OR reply_target = '' OR surface_kind IS NULL OR surface_kind = ''`).all();
2103
+ if (rows.length === 0)
2104
+ return;
2105
+ const updateReplyTarget = this.db.prepare(`UPDATE conversations
2106
+ SET reply_target = ?, surface_kind = ?
2107
+ WHERE id = ?`);
2108
+ for (const row of rows) {
2109
+ const canonicalReplyTarget = this.computeReplyTarget({
2110
+ conversationId: row.id,
2111
+ channelId: row.channelId,
2112
+ threadKind: row.threadKind,
2113
+ isPrimaryThread: row.isPrimaryThread !== 0,
2114
+ threadRootId: row.threadRootId ?? null,
2115
+ userId: row.userId ?? null,
2116
+ title: row.title ?? null,
2117
+ });
2118
+ const canonicalSurfaceKind = deriveSurfaceKind(row.threadKind, row.threadRootId ?? null);
2119
+ if ((row.replyTarget ?? '').trim() === canonicalReplyTarget && row.surfaceKind === canonicalSurfaceKind)
2120
+ continue;
2121
+ updateReplyTarget.run(canonicalReplyTarget, canonicalSurfaceKind, row.id);
2122
+ }
2123
+ }
2124
+ // ─── Machine CRUD ───
2125
+ createMachine(params) {
2126
+ const nodeId = randomUUID();
2127
+ const now = Date.now();
2128
+ const envVarKeysJson = JSON.stringify(params.envVarKeys ?? []);
2129
+ this.db.prepare(`INSERT INTO nodes(node_id, hostname, agent_types_json, version, status, last_seen, created_at, display_name, env_var_keys, provisioned_at)
2130
+ VALUES(?, '', '[]', '', 'pending', 0, 0, ?, ?, ?)`).run(nodeId, params.name, envVarKeysJson, now);
2131
+ return {
2132
+ nodeId,
2133
+ name: params.name,
2134
+ hostname: null,
2135
+ agentTypes: [],
2136
+ version: null,
2137
+ status: 'pending',
2138
+ envVarKeys: params.envVarKeys ?? [],
2139
+ lastSeen: null,
2140
+ provisionedAt: now,
2141
+ createdAt: 0,
2142
+ };
2143
+ }
2144
+ listMachines() {
2145
+ const rows = this.db.prepare(`SELECT node_id as nodeId, hostname, agent_types_json as agentTypesJson, version,
2146
+ status, last_seen as lastSeen, created_at as createdAt,
2147
+ display_name as displayName, env_var_keys as envVarKeysJson,
2148
+ provisioned_at as provisionedAt
2149
+ FROM nodes WHERE status != 'deleted' ORDER BY provisioned_at DESC, created_at ASC`).all();
2150
+ return rows.map((row) => {
2151
+ const isOnline = !!this.nodeRegistry?.getNode(row.nodeId);
2152
+ return rowToMachineInfo(row, isOnline);
2153
+ });
2154
+ }
2155
+ getMachine(nodeId) {
2156
+ const row = this.db.prepare(`SELECT node_id as nodeId, hostname, agent_types_json as agentTypesJson, version,
2157
+ status, last_seen as lastSeen, created_at as createdAt,
2158
+ display_name as displayName, env_var_keys as envVarKeysJson,
2159
+ provisioned_at as provisionedAt
2160
+ FROM nodes WHERE node_id = ? AND status != 'deleted'`).get(nodeId);
2161
+ if (!row)
2162
+ return null;
2163
+ const isOnline = !!this.nodeRegistry?.getNode(nodeId);
2164
+ return rowToMachineInfo(row, isOnline);
2165
+ }
2166
+ deleteMachine(nodeId) {
2167
+ const agentIds = this.db.prepare(`SELECT agent_id as agentId FROM agents WHERE node_id = ? AND deleted_at IS NULL`).all(nodeId);
2168
+ for (const agent of agentIds) {
2169
+ this.deleteAgent(agent.agentId);
2170
+ }
2171
+ this.db.prepare(`UPDATE nodes SET status = 'deleted' WHERE node_id = ?`).run(nodeId);
2172
+ if (this.nodeRegistry) {
2173
+ const node = this.nodeRegistry.getNode(nodeId);
2174
+ if (node) {
2175
+ node.ws?.close(4000, 'Machine has been deleted');
2176
+ this.nodeRegistry.unregister(nodeId);
2177
+ }
2178
+ }
2179
+ }
2180
+ rowToAgentInfo(row) {
2181
+ const memberships = this.db.prepare(`SELECT channel_id as channelId FROM agent_channel_memberships WHERE agent_id = ?`).all(row.agentId);
2182
+ const enabledSkillPaths = parseStringArray(row.enabledSkillPathsJson);
2183
+ return {
2184
+ agentId: row.agentId,
2185
+ name: row.name,
2186
+ ...(row.agentRole && row.agentRole !== 'user' ? { agentRole: row.agentRole } : {}),
2187
+ agentType: row.agentType,
2188
+ ...(row.model ? { model: row.model } : {}),
2189
+ ...(row.reasoningEffort ? { reasoningEffort: row.reasoningEffort } : {}),
2190
+ ...(row.agentType === 'claude_sdk' && row.claudePermissionMode ? { claudePermissionMode: row.claudePermissionMode } : {}),
2191
+ ...(row.agentType === 'codex_app_server' && row.codexMode ? { codexMode: row.codexMode } : {}),
2192
+ ...(row.agentType === 'codex_app_server' && row.codexServiceTier ? { codexServiceTier: row.codexServiceTier } : {}),
2193
+ channelId: row.channelId,
2194
+ channelIds: memberships.map((m) => m.channelId),
2195
+ systemPrompt: row.systemPrompt,
2196
+ ...(row.description ? { description: row.description } : {}),
2197
+ envVars: parseEnvVars(row.envVarsJson),
2198
+ disabledToolKinds: normalizeAgentDisabledToolKinds(row.agentType, parseDisabledToolKinds(row.disabledToolKindsJson)),
2199
+ nodeId: row.nodeId,
2200
+ workspacePath: row.workspacePath,
2201
+ projectPath: row.projectPath,
2202
+ projects: this.listAgentProjectBindings(row.agentId),
2203
+ skillRoots: parseStringArray(row.skillRootsJson),
2204
+ ...(enabledSkillPaths !== undefined
2205
+ ? { enabledSkillPaths }
2206
+ : {}),
2207
+ skillSelectionMode: row.enabledSkillPathsJson == null ? 'legacy_all' : 'explicit',
2208
+ memoryNetworkEnabled: row.memoryNetworkEnabled === 1,
2209
+ dreamEnabled: row.dreamEnabled === 1,
2210
+ dreamSchedule: row.dreamSchedule ?? '02:00',
2211
+ dreamMinMessages: row.dreamMinMessages ?? 5,
2212
+ lastDreamAt: row.lastDreamAt ?? null,
2213
+ lastDreamSeq: row.lastDreamSeq ?? null,
2214
+ deletedAt: row.deletedAt ?? null,
2215
+ createdAt: row.createdAt,
2216
+ updatedAt: row.updatedAt,
2217
+ };
2218
+ }
2219
+ listAgentProjectBindings(agentId) {
2220
+ const rows = this.db.prepare(`SELECT p.project_id as projectId,
2221
+ p.name,
2222
+ p.backend_type as backendType,
2223
+ p.root_path as rootPath,
2224
+ p.node_id as nodeId,
2225
+ p.description as description,
2226
+ b.is_default as isDefault
2227
+ FROM agent_project_bindings b
2228
+ JOIN projects p ON p.project_id = b.project_id
2229
+ WHERE b.agent_id = ?
2230
+ AND p.archived_at IS NULL
2231
+ ORDER BY b.is_default DESC, p.updated_at DESC, p.name COLLATE NOCASE ASC`).all(agentId);
2232
+ return rows.map((row) => ({
2233
+ projectId: row.projectId,
2234
+ name: row.name,
2235
+ backendType: row.backendType,
2236
+ rootPath: row.rootPath,
2237
+ nodeId: row.nodeId,
2238
+ description: row.description,
2239
+ isDefault: row.isDefault !== 0,
2240
+ }));
2241
+ }
2242
+ resolveDefaultConversationActiveProjectId(agentId) {
2243
+ const normalizedAgentId = agentId?.trim();
2244
+ if (!normalizedAgentId)
2245
+ return null;
2246
+ const rows = this.db.prepare(`SELECT b.project_id as projectId,
2247
+ b.is_default as isDefault
2248
+ FROM agent_project_bindings b
2249
+ JOIN projects p ON p.project_id = b.project_id
2250
+ WHERE b.agent_id = ?
2251
+ AND p.archived_at IS NULL
2252
+ ORDER BY b.is_default DESC, b.updated_at DESC`).all(normalizedAgentId);
2253
+ if (rows.length === 0)
2254
+ return null;
2255
+ const defaultRow = rows.find((row) => row.isDefault !== 0);
2256
+ if (defaultRow)
2257
+ return defaultRow.projectId;
2258
+ return rows.length === 1 ? rows[0].projectId : null;
2259
+ }
2260
+ resolveConversationActiveProjectId(agentId, activeProjectId) {
2261
+ const normalizedProjectId = activeProjectId?.trim() || null;
2262
+ if (!normalizedProjectId)
2263
+ return null;
2264
+ const normalizedAgentId = agentId?.trim();
2265
+ if (!normalizedAgentId) {
2266
+ throw new Error('Active project requires an agent-bound conversation.');
2267
+ }
2268
+ const binding = this.db.prepare(`SELECT 1
2269
+ FROM agent_project_bindings b
2270
+ JOIN projects p ON p.project_id = b.project_id
2271
+ WHERE b.agent_id = ?
2272
+ AND b.project_id = ?
2273
+ AND p.archived_at IS NULL
2274
+ LIMIT 1`).get(normalizedAgentId, normalizedProjectId);
2275
+ if (!binding) {
2276
+ throw new Error('Active project is not bound to this agent.');
2277
+ }
2278
+ return normalizedProjectId;
2279
+ }
2280
+ listChannelMembers(channelId) {
2281
+ return this.db.prepare(`SELECT a.agent_id as agentId, a.name
2282
+ FROM agent_channel_memberships m
2283
+ JOIN agents a ON a.agent_id = m.agent_id
2284
+ WHERE m.channel_id = ?
2285
+ ORDER BY a.name COLLATE NOCASE ASC`).all(channelId);
2286
+ }
2287
+ }
2288
+ function normalizeAgentTypeList(value) {
2289
+ if (!Array.isArray(value))
2290
+ return [];
2291
+ return value
2292
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
2293
+ .filter((item) => item.length > 0);
2294
+ }
2295
+ function normalizeCodexMode(value) {
2296
+ return value === 'default' || value === 'plan' ? value : null;
2297
+ }
2298
+ function normalizeClaudePermissionMode(value) {
2299
+ return value === 'default' || value === 'acceptEdits' || value === 'bypassPermissions' ? value : null;
2300
+ }
2301
+ function normalizeCodexServiceTier(value) {
2302
+ return value === 'fast' || value === 'flex' ? value : null;
2303
+ }
2304
+ function rowToMachineInfo(row, isOnline) {
2305
+ let agentTypes = [];
2306
+ try {
2307
+ agentTypes = JSON.parse(row.agentTypesJson);
2308
+ }
2309
+ catch { /* ignore */ }
2310
+ let envVarKeys = [];
2311
+ try {
2312
+ const parsed = JSON.parse(row.envVarKeysJson ?? '[]');
2313
+ if (Array.isArray(parsed))
2314
+ envVarKeys = parsed;
2315
+ }
2316
+ catch { /* ignore */ }
2317
+ const status = isOnline ? 'online'
2318
+ : (row.status === 'pending' ? 'pending' : 'offline');
2319
+ return {
2320
+ nodeId: row.nodeId,
2321
+ name: row.displayName || row.hostname || row.nodeId,
2322
+ hostname: row.hostname || null,
2323
+ agentTypes,
2324
+ version: row.version || null,
2325
+ status,
2326
+ envVarKeys,
2327
+ lastSeen: row.lastSeen || null,
2328
+ provisionedAt: row.provisionedAt,
2329
+ createdAt: row.createdAt,
2330
+ };
2331
+ }
2332
+ function parseEnvVars(raw) {
2333
+ if (!raw)
2334
+ return undefined;
2335
+ try {
2336
+ const parsed = JSON.parse(raw);
2337
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2338
+ return parsed;
2339
+ }
2340
+ }
2341
+ catch {
2342
+ // ignore
2343
+ }
2344
+ return undefined;
2345
+ }
2346
+ function parseDisabledToolKinds(raw) {
2347
+ if (!raw)
2348
+ return undefined;
2349
+ try {
2350
+ const parsed = JSON.parse(raw);
2351
+ if (Array.isArray(parsed)) {
2352
+ const out = [];
2353
+ const seen = new Set();
2354
+ for (const value of parsed) {
2355
+ const kind = parseAgentPermissionKind(value);
2356
+ if (!kind || seen.has(kind))
2357
+ continue;
2358
+ seen.add(kind);
2359
+ out.push(kind);
2360
+ }
2361
+ return out.length > 0 ? out : undefined;
2362
+ }
2363
+ }
2364
+ catch {
2365
+ // ignore
2366
+ }
2367
+ return undefined;
2368
+ }
2369
+ function parseStringArray(raw) {
2370
+ if (!raw)
2371
+ return undefined;
2372
+ try {
2373
+ const parsed = JSON.parse(raw);
2374
+ if (Array.isArray(parsed)) {
2375
+ return parsed.filter((value) => typeof value === 'string' && value.trim().length > 0);
2376
+ }
2377
+ }
2378
+ catch {
2379
+ // ignore
2380
+ }
2381
+ return undefined;
2382
+ }
2383
+ function normalizeEnabledSkillPaths(enabledSkillPaths, skillRoots) {
2384
+ if (!enabledSkillPaths)
2385
+ return [];
2386
+ const seen = new Set();
2387
+ const results = [];
2388
+ for (const rawValue of enabledSkillPaths) {
2389
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
2390
+ if (!value)
2391
+ continue;
2392
+ if (!path.isAbsolute(value)) {
2393
+ throw new AgentConfigValidationError('enabledSkillPaths must contain absolute SKILL.md paths');
2394
+ }
2395
+ const resolved = path.resolve(value);
2396
+ if (path.basename(resolved) !== 'SKILL.md') {
2397
+ throw new AgentConfigValidationError('enabledSkillPaths entries must point to SKILL.md files');
2398
+ }
2399
+ if (!isEnabledSkillPathWithinRoots(resolved, skillRoots)) {
2400
+ throw new AgentConfigValidationError('enabledSkillPaths entries must stay within configured skillRoots');
2401
+ }
2402
+ if (seen.has(resolved))
2403
+ continue;
2404
+ seen.add(resolved);
2405
+ results.push(resolved);
2406
+ }
2407
+ return results;
2408
+ }
2409
+ function normalizeSkillRootsForStorage(skillRoots) {
2410
+ if (!skillRoots)
2411
+ return undefined;
2412
+ const seen = new Set();
2413
+ const results = [];
2414
+ for (const rawValue of skillRoots) {
2415
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
2416
+ if (!value)
2417
+ continue;
2418
+ const normalized = path.isAbsolute(value) ? path.resolve(value) : value;
2419
+ if (seen.has(normalized))
2420
+ continue;
2421
+ seen.add(normalized);
2422
+ results.push(normalized);
2423
+ }
2424
+ return results;
2425
+ }
2426
+ function retainEnabledSkillPathsWithinRoots(enabledSkillPaths, skillRoots) {
2427
+ const seen = new Set();
2428
+ const results = [];
2429
+ for (const value of enabledSkillPaths) {
2430
+ const resolved = path.resolve(value);
2431
+ if (path.basename(resolved) !== 'SKILL.md')
2432
+ continue;
2433
+ if (!isEnabledSkillPathWithinRoots(resolved, skillRoots))
2434
+ continue;
2435
+ if (seen.has(resolved))
2436
+ continue;
2437
+ seen.add(resolved);
2438
+ results.push(resolved);
2439
+ }
2440
+ return results;
2441
+ }
2442
+ function isEnabledSkillPathWithinRoots(skillPath, skillRoots) {
2443
+ const rootBoundaries = buildSkillRootBoundaries(skillRoots);
2444
+ const lexicalAllowed = rootBoundaries.some((root) => {
2445
+ const relative = path.relative(root.path, skillPath);
2446
+ return relative === 'SKILL.md' || (!relative.startsWith('..') && !path.isAbsolute(relative));
2447
+ });
2448
+ if (!lexicalAllowed)
2449
+ return false;
2450
+ const stat = fs.lstatSync(skillPath, { throwIfNoEntry: false });
2451
+ if (!stat)
2452
+ return true;
2453
+ if (stat.isSymbolicLink())
2454
+ return false;
2455
+ const realPath = fs.realpathSync(skillPath);
2456
+ const rootsWithRealPath = rootBoundaries.filter((root) => root.realPath);
2457
+ if (rootsWithRealPath.length === 0)
2458
+ return true;
2459
+ return rootsWithRealPath.some((root) => {
2460
+ const relative = path.relative(root.realPath, realPath);
2461
+ return (relative === 'SKILL.md' || (!relative.startsWith('..') && !path.isAbsolute(relative)))
2462
+ && !pathHasSymlinkDescendant(root.path, skillPath);
2463
+ });
2464
+ }
2465
+ function buildSkillRootBoundaries(skillRoots) {
2466
+ const seen = new Set();
2467
+ const roots = [];
2468
+ for (const rawValue of skillRoots ?? []) {
2469
+ const value = typeof rawValue === 'string' ? rawValue.trim() : '';
2470
+ if (!value || !path.isAbsolute(value))
2471
+ continue;
2472
+ const rootPath = path.resolve(value);
2473
+ if (seen.has(rootPath))
2474
+ continue;
2475
+ const stat = fs.statSync(rootPath, { throwIfNoEntry: false });
2476
+ roots.push({
2477
+ path: rootPath,
2478
+ realPath: stat?.isDirectory() ? fs.realpathSync(rootPath) : null,
2479
+ });
2480
+ seen.add(rootPath);
2481
+ }
2482
+ return roots;
2483
+ }
2484
+ function pathHasSymlinkDescendant(rootPath, skillPath) {
2485
+ const relative = path.relative(rootPath, skillPath);
2486
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
2487
+ return false;
2488
+ let current = rootPath;
2489
+ const parts = relative.split(path.sep).filter(Boolean);
2490
+ for (const part of parts.slice(0, -1)) {
2491
+ current = path.join(current, part);
2492
+ const stat = fs.lstatSync(current, { throwIfNoEntry: false });
2493
+ if (stat?.isSymbolicLink())
2494
+ return true;
2495
+ }
2496
+ return false;
2497
+ }
2498
+ function normalizeStringList(value) {
2499
+ if (!Array.isArray(value))
2500
+ return [];
2501
+ const out = [];
2502
+ const seen = new Set();
2503
+ for (const item of value) {
2504
+ const text = typeof item === 'string' ? item.trim() : '';
2505
+ if (!text || seen.has(text))
2506
+ continue;
2507
+ seen.add(text);
2508
+ out.push(text);
2509
+ }
2510
+ return out;
2511
+ }