@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.
- package/dist/config.js +380 -0
- package/dist/execution/executionDispatcher.js +3810 -0
- package/dist/main.js +90 -0
- package/dist/nodeEventHistory.js +206 -0
- package/dist/scheduler/dreamLogic.js +50 -0
- package/dist/scheduler/dreamScheduler.js +65 -0
- package/dist/services/agentFileAccessService.js +1913 -0
- package/dist/services/agentRuntimeCleanupBroker.js +62 -0
- package/dist/services/agentSkillsBroker.js +118 -0
- package/dist/services/agentSkillsService.js +83 -0
- package/dist/services/agentWorkspaceBroker.js +937 -0
- package/dist/services/agentWorkspaceService.js +70 -0
- package/dist/services/appVersion.js +14 -0
- package/dist/services/auth.js +586 -0
- package/dist/services/claudeControlBroker.js +154 -0
- package/dist/services/claudeTranscriptBroker.js +100 -0
- package/dist/services/claudeTranscriptService.js +359 -0
- package/dist/services/codexAppServerBroker.js +155 -0
- package/dist/services/codexTranscriptBroker.js +98 -0
- package/dist/services/codexTranscriptService.js +961 -0
- package/dist/services/droidMissionBroker.js +124 -0
- package/dist/services/droidMissionImporter.js +630 -0
- package/dist/services/droidModelOptions.js +165 -0
- package/dist/services/hubServerRegistrationService.js +268 -0
- package/dist/services/libraryManifest.js +43 -0
- package/dist/services/libraryScaffold.js +26 -0
- package/dist/services/libraryService.js +2263 -0
- package/dist/services/memoryService.js +386 -0
- package/dist/services/missionEvidence.js +377 -0
- package/dist/services/missionService.js +2361 -0
- package/dist/services/missionTrace.js +158 -0
- package/dist/services/nativeMissionBriefParser.js +120 -0
- package/dist/services/nativeMissionOrchestrator.js +2045 -0
- package/dist/services/nativeMissionReportGenerator.js +227 -0
- package/dist/services/nativeMissionValidationRunner.js +452 -0
- package/dist/services/nativeMissionWorkerBroker.js +190 -0
- package/dist/services/nodeRegistry.js +34 -0
- package/dist/services/nodeStateReconciler.js +97 -0
- package/dist/services/panelMediaScanner.js +119 -0
- package/dist/services/persistentRuntimeJsonlClient.js +153 -0
- package/dist/services/platformAgentPolicy.js +180 -0
- package/dist/services/platformAgentService.js +2041 -0
- package/dist/services/projectAccessResolver.js +93 -0
- package/dist/services/projectService.js +392 -0
- package/dist/services/resourceSpaceService.js +140 -0
- package/dist/services/scenarioRuntimeService.js +1130 -0
- package/dist/services/suggestedPlannerService.js +868 -0
- package/dist/services/workbenchGitBroker.js +161 -0
- package/dist/services/workbenchGitService.js +69 -0
- package/dist/services/workbenchInspectBroker.js +65 -0
- package/dist/services/workbenchNodePathService.js +79 -0
- package/dist/services/workbenchRegistryService.js +240 -0
- package/dist/services/workbenchRootService.js +181 -0
- package/dist/services/workbenchTerminalBroker.js +378 -0
- package/dist/services/workspaceRunOwnership.js +60 -0
- package/dist/services/workspaceScaffold.js +105 -0
- package/dist/services/workspaceSessionRuntimeService.js +576 -0
- package/dist/services/workspaceSessionService.js +245 -0
- package/dist/services/workspaceToolActionRunner.js +1582 -0
- package/dist/services/workspaceToolErrors.js +10 -0
- package/dist/services/workspaceToolExecutionUtils.js +895 -0
- package/dist/services/workspaceToolLatestStateProjector.js +91 -0
- package/dist/services/workspaceToolManifest.js +572 -0
- package/dist/services/workspaceToolMutationQueue.js +43 -0
- package/dist/services/workspaceToolPanelProjection.js +460 -0
- package/dist/services/workspaceToolPromotion.js +255 -0
- package/dist/services/workspaceToolPromotionState.js +224 -0
- package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
- package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
- package/dist/services/workspaceToolReadModel.js +378 -0
- package/dist/services/workspaceToolRunLedger.js +239 -0
- package/dist/services/workspaceToolService.js +3067 -0
- package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
- package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
- package/dist/services/workspaceToolTypes.js +1 -0
- package/dist/services/workspaceToolUploadMaterializer.js +228 -0
- package/dist/web/actionCardRoutes.js +129 -0
- package/dist/web/actionCards.js +469 -0
- package/dist/web/activationContext.js +684 -0
- package/dist/web/agentChannelGuards.js +48 -0
- package/dist/web/agentMentionCooldowns.js +32 -0
- package/dist/web/agentReminders.js +1668 -0
- package/dist/web/agentRuntimePresence.js +197 -0
- package/dist/web/agentSelfState.js +494 -0
- package/dist/web/agentTaskLinks.js +26 -0
- package/dist/web/agentVisibility.js +79 -0
- package/dist/web/assets.js +95 -0
- package/dist/web/channelActivationPrompt.js +395 -0
- package/dist/web/channelMemoryNotes.js +127 -0
- package/dist/web/channelMentions.js +10 -0
- package/dist/web/channelMessageSequences.js +19 -0
- package/dist/web/channelSubscriptions.js +26 -0
- package/dist/web/clearedTaskRoots.js +10 -0
- package/dist/web/collaborationPromptGuidance.js +36 -0
- package/dist/web/collaborationSurfaceState.js +140 -0
- package/dist/web/contextBundleRanking.js +154 -0
- package/dist/web/contextBundleResolver.js +488 -0
- package/dist/web/conversationBuiltinSkillRoots.js +50 -0
- package/dist/web/conversationControls.js +232 -0
- package/dist/web/conversationHandoffs.js +612 -0
- package/dist/web/conversationManager.js +2511 -0
- package/dist/web/conversationSummaries.js +876 -0
- package/dist/web/conversationSurfaceKinds.js +17 -0
- package/dist/web/conversationTargets.js +173 -0
- package/dist/web/directActivationPrompt.js +122 -0
- package/dist/web/directReplyTargets.js +69 -0
- package/dist/web/directThreadResolver.js +129 -0
- package/dist/web/dmTaskHandoffPrompt.js +120 -0
- package/dist/web/dmTaskThreadStatusProjection.js +229 -0
- package/dist/web/ftsQuery.js +33 -0
- package/dist/web/internalAgentRouter.js +11341 -0
- package/dist/web/libraryCuratorScheduler.js +58 -0
- package/dist/web/libraryDocumentPromptGuidance.js +8 -0
- package/dist/web/messageCheckpoints.js +19 -0
- package/dist/web/nodeWsHandler.js +2495 -0
- package/dist/web/notificationRounds.js +1061 -0
- package/dist/web/panelActionMessages.js +108 -0
- package/dist/web/panelActivationPrompt.js +18 -0
- package/dist/web/panelAudit.js +273 -0
- package/dist/web/panelLifecycle.js +222 -0
- package/dist/web/panelMediaPolicy.js +43 -0
- package/dist/web/panelPathPolicy.js +63 -0
- package/dist/web/panelPreviews.js +175 -0
- package/dist/web/panelQueryHandles.js +2749 -0
- package/dist/web/panelRoutes.js +2147 -0
- package/dist/web/panels.js +904 -0
- package/dist/web/peerInboxAggregates.js +1247 -0
- package/dist/web/planApprovalState.js +92 -0
- package/dist/web/platformAgentScheduler.js +66 -0
- package/dist/web/proactiveOpportunities.js +452 -0
- package/dist/web/promptContextSections.js +242 -0
- package/dist/web/promptHistorySanitizer.js +26 -0
- package/dist/web/promptSlashCommands.js +158 -0
- package/dist/web/rollingConversationSummary.js +453 -0
- package/dist/web/routeHelpers.js +11 -0
- package/dist/web/routes/handoff.js +288 -0
- package/dist/web/routes/history.js +345 -0
- package/dist/web/routes/memory.js +258 -0
- package/dist/web/routes/selfState.js +171 -0
- package/dist/web/routes/workspace.js +154 -0
- package/dist/web/runSurfaceWatermarks.js +431 -0
- package/dist/web/runtimeCapabilities.js +48 -0
- package/dist/web/sameAgentHandoffs.js +494 -0
- package/dist/web/server.js +15567 -0
- package/dist/web/sharedCollaborationCapsules.js +163 -0
- package/dist/web/soloSessionRelay.js +42 -0
- package/dist/web/soloWsHandler.js +138 -0
- package/dist/web/suggestedPlannerScheduler.js +56 -0
- package/dist/web/surfaceActivationPolicy.js +108 -0
- package/dist/web/surfaceCollaborators.js +61 -0
- package/dist/web/surfaceSystemStatus.js +263 -0
- package/dist/web/targetParticipants.js +77 -0
- package/dist/web/taskEvents.js +49 -0
- package/dist/web/taskLifecycleMessages.js +165 -0
- package/dist/web/taskLoops.js +732 -0
- package/dist/web/taskMemoryNotes.js +224 -0
- package/dist/web/taskNumbers.js +16 -0
- package/dist/web/taskOwnerGuards.js +49 -0
- package/dist/web/taskParticipantResolver.js +42 -0
- package/dist/web/taskParticipants.js +97 -0
- package/dist/web/taskSourceDetails.js +20 -0
- package/dist/web/taskStateViews.js +210 -0
- package/dist/web/taskStatusTransitions.js +9 -0
- package/dist/web/taskThreadFollowups.js +599 -0
- package/dist/web/taskThreadRuntimeClosure.js +685 -0
- package/dist/web/taskUpdateDelivery.js +104 -0
- package/dist/web/threadReplyContentHeuristics.js +30 -0
- package/dist/web/threadRoots.js +61 -0
- package/dist/web/threadTaskBindings.js +365 -0
- package/dist/web/uiPanelPromptGuidance.js +27 -0
- package/dist/web/workspaceMemoryHints.js +143 -0
- package/dist/web/workspaceToolPromptGuidance.js +30 -0
- package/dist/web/wsHandler.js +397 -0
- package/dist/web/wsSink.js +116 -0
- 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
|
+
}
|