@bbigbang/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. package/package.json +54 -0
@@ -0,0 +1,2041 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { log } from '@bbigbang/runtime-acp';
5
+ import { insertPlannerTaskCandidateOpportunity } from '../web/proactiveOpportunities.js';
6
+ import { getLibraryCuratorTerminalError, } from './libraryService.js';
7
+ import { getBuiltinPlatformDefinition, getPlatformAgentConversationTitle, getPlatformAgentLegacyConversationTitles, getPlatformWorkflowCatalog, getPlatformWorkflowCatalogEntry, isBuiltinPlatformAgentDefinitionId, normalizePlatformAgentToolPolicy, normalizePlatformResourceScopes, } from './platformAgentPolicy.js';
8
+ import { getSuggestedPlannerTerminalError, parseSuggestedPlannerOutput, } from './suggestedPlannerService.js';
9
+ const DEFAULT_PLATFORM_AGENT_INTERVAL_MS = 6 * 60 * 60 * 1000;
10
+ const DEFAULT_LOOKBACK_DAYS = 14;
11
+ const PLATFORM_AGENT_WORKSPACE_DIRECTORY_NAME = '.platform-agents';
12
+ const PLANNER_WORKSPACE_DIRECTORY_NAME = '.suggested-planner';
13
+ const LIBRARY_ROOT_DIRECTORY_NAME = '.library-curator';
14
+ const CURATOR_WORKSPACE_DIRECTORY_NAME = 'workspaces';
15
+ const PLATFORM_AGENT_TYPE = 'claude_sdk';
16
+ const LEGACY_SUGGESTED_PLANNER_PROMPT_PREFIX = '# Suggested Planner run';
17
+ const LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR = 'Legacy Suggested Planner workflow run was cleaned up after the platform-agent migration.';
18
+ export function getPlatformWorkflowTerminalError(workflowType, stopReason, error) {
19
+ if (workflowType === 'planner_suggestions') {
20
+ return getSuggestedPlannerTerminalError(stopReason, error);
21
+ }
22
+ return getLibraryCuratorTerminalError(stopReason, error);
23
+ }
24
+ function safeUsername(username) {
25
+ return username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
26
+ }
27
+ function parseJsonRecord(raw) {
28
+ if (!raw?.trim())
29
+ return {};
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
33
+ ? parsed
34
+ : {};
35
+ }
36
+ catch {
37
+ return {};
38
+ }
39
+ }
40
+ function parseJsonStringArray(raw) {
41
+ if (!raw?.trim())
42
+ return [];
43
+ try {
44
+ const parsed = JSON.parse(raw);
45
+ return Array.isArray(parsed)
46
+ ? parsed.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim())
47
+ : [];
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function mapDefinitionRow(row) {
54
+ return {
55
+ definitionId: row.definitionId,
56
+ workflowType: row.workflowType,
57
+ label: row.label,
58
+ description: row.description,
59
+ resourceScopes: normalizePlatformResourceScopes(row.workflowType, parseJsonStringArray(row.resourceScopesJson)),
60
+ resourceConfig: parseJsonRecord(row.resourceConfigJson),
61
+ promptTemplateOverride: row.promptTemplateOverride,
62
+ toolPolicy: normalizePlatformAgentToolPolicy(row.workflowType, parseJsonRecord(row.toolPolicyJson)),
63
+ enabled: row.enabled === 1,
64
+ createdAt: row.createdAt,
65
+ updatedAt: row.updatedAt,
66
+ };
67
+ }
68
+ function mapInstanceRow(row) {
69
+ const runtimeConfigRecord = parseJsonRecord(row.runtimeConfigJson);
70
+ return {
71
+ userId: row.userId,
72
+ definitionId: row.definitionId,
73
+ agentId: row.agentId,
74
+ enabled: row.enabled === 1,
75
+ scheduleIntervalMs: row.scheduleIntervalMs,
76
+ workflowConfig: parseJsonRecord(row.workflowConfigJson),
77
+ runtimeConfig: {
78
+ model: typeof runtimeConfigRecord.model === 'string' ? runtimeConfigRecord.model : null,
79
+ reasoningEffort: typeof runtimeConfigRecord.reasoningEffort === 'string' ? runtimeConfigRecord.reasoningEffort : null,
80
+ envVars: normalizeEnvVars(runtimeConfigRecord.envVars),
81
+ },
82
+ lastRunAt: row.lastRunAt,
83
+ nextRunAt: row.nextRunAt,
84
+ createdAt: row.createdAt,
85
+ updatedAt: row.updatedAt,
86
+ };
87
+ }
88
+ function mapRunRow(row) {
89
+ let resourcesAccessed = null;
90
+ if (row.resourcesAccessedJson) {
91
+ try {
92
+ const parsed = JSON.parse(row.resourcesAccessedJson);
93
+ if (parsed &&
94
+ typeof parsed === 'object' &&
95
+ !Array.isArray(parsed) &&
96
+ typeof parsed.savedItems === 'number' &&
97
+ typeof parsed.tasks === 'number' &&
98
+ typeof parsed.messages === 'number') {
99
+ resourcesAccessed = parsed;
100
+ }
101
+ }
102
+ catch {
103
+ // ignore invalid JSON
104
+ }
105
+ }
106
+ return {
107
+ runId: row.runId,
108
+ definitionId: row.definitionId,
109
+ userId: row.userId,
110
+ mode: row.mode,
111
+ status: row.status,
112
+ createdAt: row.createdAt,
113
+ completedAt: row.completedAt,
114
+ error: row.error,
115
+ generatedCount: row.generatedCount,
116
+ resourcesAccessed,
117
+ };
118
+ }
119
+ function normalizeEnvVars(value) {
120
+ if (!value || typeof value !== 'object' || Array.isArray(value))
121
+ return {};
122
+ const out = {};
123
+ for (const [key, raw] of Object.entries(value)) {
124
+ if (typeof raw === 'string')
125
+ out[key] = raw;
126
+ }
127
+ return out;
128
+ }
129
+ function buildPlannerWorkspacePath(workspaceRoot, username) {
130
+ return path.join(workspaceRoot, PLANNER_WORKSPACE_DIRECTORY_NAME, safeUsername(username));
131
+ }
132
+ function buildCuratorWorkspacePath(workspaceRoot, username) {
133
+ return path.join(workspaceRoot, LIBRARY_ROOT_DIRECTORY_NAME, CURATOR_WORKSPACE_DIRECTORY_NAME, safeUsername(username));
134
+ }
135
+ function buildGenericPlatformWorkspacePath(workspaceRoot, definitionId, username) {
136
+ return path.join(workspaceRoot, PLATFORM_AGENT_WORKSPACE_DIRECTORY_NAME, definitionId, safeUsername(username));
137
+ }
138
+ function buildPlannerEnvVars() {
139
+ const envVars = {};
140
+ const baseUrl = process.env.SUGGESTED_PLANNER_ANTHROPIC_BASE_URL?.trim()
141
+ || process.env.ANTHROPIC_BASE_URL?.trim();
142
+ const model = process.env.SUGGESTED_PLANNER_ANTHROPIC_MODEL?.trim()
143
+ || process.env.ANTHROPIC_MODEL?.trim()
144
+ || 'deepseek-v4-pro';
145
+ const authToken = process.env.SUGGESTED_PLANNER_ANTHROPIC_AUTH_TOKEN?.trim()
146
+ || process.env.SUGGESTED_PLANNER_ANTHROPIC_API_KEY?.trim()
147
+ || process.env.ANTHROPIC_AUTH_TOKEN?.trim()
148
+ || process.env.ANTHROPIC_API_KEY?.trim();
149
+ if (baseUrl)
150
+ envVars.ANTHROPIC_BASE_URL = baseUrl;
151
+ if (model)
152
+ envVars.ANTHROPIC_MODEL = model;
153
+ if (authToken)
154
+ envVars.ANTHROPIC_AUTH_TOKEN = authToken;
155
+ for (const key of [
156
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
157
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
158
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
159
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
160
+ 'CLAUDE_CODE_EFFORT_LEVEL',
161
+ ]) {
162
+ const value = process.env[key]?.trim();
163
+ if (value)
164
+ envVars[key] = value;
165
+ }
166
+ return envVars;
167
+ }
168
+ function buildCuratorEnvVars() {
169
+ const envVars = {};
170
+ const baseUrl = process.env.LIBRARY_CURATOR_ANTHROPIC_BASE_URL?.trim()
171
+ || process.env.ANTHROPIC_BASE_URL?.trim();
172
+ const model = process.env.LIBRARY_CURATOR_ANTHROPIC_MODEL?.trim()
173
+ || process.env.ANTHROPIC_MODEL?.trim()
174
+ || 'deepseek-v4-pro';
175
+ const authToken = process.env.LIBRARY_CURATOR_ANTHROPIC_AUTH_TOKEN?.trim()
176
+ || process.env.LIBRARY_CURATOR_ANTHROPIC_API_KEY?.trim()
177
+ || process.env.ANTHROPIC_AUTH_TOKEN?.trim()
178
+ || process.env.ANTHROPIC_API_KEY?.trim();
179
+ if (baseUrl)
180
+ envVars.ANTHROPIC_BASE_URL = baseUrl;
181
+ if (model)
182
+ envVars.ANTHROPIC_MODEL = model;
183
+ if (authToken)
184
+ envVars.ANTHROPIC_AUTH_TOKEN = authToken;
185
+ for (const key of [
186
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
187
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
188
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
189
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
190
+ 'CLAUDE_CODE_EFFORT_LEVEL',
191
+ ]) {
192
+ const value = process.env[key]?.trim();
193
+ if (value)
194
+ envVars[key] = value;
195
+ }
196
+ return envVars;
197
+ }
198
+ function pickPlannerNodeId(nodeRegistry) {
199
+ const localNode = nodeRegistry.getNode('local-node-1');
200
+ if (localNode?.agentTypes.includes('claude_sdk'))
201
+ return localNode.nodeId;
202
+ for (const node of nodeRegistry.listNodes()) {
203
+ const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
204
+ if (agentTypes.includes('claude_sdk') && !node.nodeId.startsWith('scenario-'))
205
+ return node.nodeId;
206
+ }
207
+ for (const node of nodeRegistry.listNodes()) {
208
+ const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
209
+ if (agentTypes.includes('claude_sdk'))
210
+ return node.nodeId;
211
+ }
212
+ return nodeRegistry.listNodes()[0]?.nodeId ?? null;
213
+ }
214
+ function pickLibraryNodeId(nodeRegistry) {
215
+ for (const node of nodeRegistry.listNodes()) {
216
+ const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
217
+ if (agentTypes.includes('claude_sdk'))
218
+ return node.nodeId;
219
+ }
220
+ return nodeRegistry.listNodes()[0]?.nodeId ?? null;
221
+ }
222
+ function plannerDedupeHash(value) {
223
+ return createHash('sha256').update(value).digest('hex').slice(0, 24);
224
+ }
225
+ function normalizePlannerString(value, limit) {
226
+ return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, limit);
227
+ }
228
+ function normalizePriority(value) {
229
+ const normalized = String(value ?? '').trim().toLowerCase();
230
+ if (normalized === 'high' || normalized === 'medium' || normalized === 'low')
231
+ return normalized;
232
+ return 'medium';
233
+ }
234
+ function isSafeEvidenceUrl(url) {
235
+ if (url.startsWith('/') && !url.startsWith('//'))
236
+ return true;
237
+ try {
238
+ const parsed = new URL(url);
239
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ function normalizeEvidenceLinks(value) {
246
+ if (!Array.isArray(value))
247
+ return [];
248
+ return value.slice(0, 4)
249
+ .map((item) => {
250
+ if (!item || typeof item !== 'object')
251
+ return null;
252
+ const record = item;
253
+ const label = normalizePlannerString(record.label, 100);
254
+ const url = normalizePlannerString(record.url, 1000);
255
+ const summary = normalizePlannerString(record.summary, 240);
256
+ if (!label || !url || !isSafeEvidenceUrl(url))
257
+ return null;
258
+ return { label, url, ...(summary ? { summary } : {}) };
259
+ })
260
+ .filter((item) => item != null);
261
+ }
262
+ function normalizePlannerTarget(value) {
263
+ if (!value || typeof value !== 'object')
264
+ return { channelId: null, agentId: null, targetType: 'general' };
265
+ const record = value;
266
+ const type = normalizePlannerString(record.type, 40).toLowerCase();
267
+ const agentId = normalizePlannerString(record.agentId ?? record.agent_id, 200) || null;
268
+ const channelId = normalizePlannerString(record.channelId ?? record.channel_id, 200) || null;
269
+ if (type === 'dm' && agentId)
270
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
271
+ if (type === 'channel' && channelId)
272
+ return { channelId, agentId: null, targetType: 'channel' };
273
+ if (channelId?.startsWith('dm:'))
274
+ return { channelId, agentId: agentId ?? channelId.slice(3), targetType: 'dm' };
275
+ if (channelId)
276
+ return { channelId, agentId: null, targetType: 'channel' };
277
+ if (agentId)
278
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
279
+ return { channelId: null, agentId: null, targetType: type || 'general' };
280
+ }
281
+ function sanitizeDefinitionId(value) {
282
+ const trimmed = value.trim();
283
+ if (!trimmed)
284
+ throw new Error('Definition id is required.');
285
+ if (!/^[a-z0-9._-]+$/.test(trimmed))
286
+ throw new Error('Definition id may contain only lowercase letters, numbers, ".", "_" and "-".');
287
+ return trimmed;
288
+ }
289
+ function parseCuratorPlan(raw) {
290
+ const parsed = parseJsonRecord(raw);
291
+ if (!Array.isArray(parsed.operations)) {
292
+ throw new Error('Library curator JSON is missing operations array.');
293
+ }
294
+ if (!Array.isArray(parsed.processedSavedItemIds)) {
295
+ throw new Error('Library curator JSON is missing processedSavedItemIds array.');
296
+ }
297
+ const operations = parsed.operations.map((item) => {
298
+ if (!item || typeof item !== 'object') {
299
+ throw new Error('Library curator operation must be an object.');
300
+ }
301
+ const record = item;
302
+ const kind = record.kind === 'create' || record.kind === 'update' ? record.kind : null;
303
+ const relativePath = normalizePlannerString(record.relativePath, 400);
304
+ const title = normalizePlannerString(record.title, 200);
305
+ const summary = normalizePlannerString(record.summary, 400);
306
+ const content = typeof record.content === 'string' ? record.content.replace(/\r\n/g, '\n') : '';
307
+ const sourceSavedItemIds = Array.isArray(record.sourceSavedItemIds)
308
+ ? record.sourceSavedItemIds.filter((value) => typeof value === 'string' && value.trim().length > 0).map((value) => value.trim())
309
+ : [];
310
+ if (!kind || !relativePath || !content.trim()) {
311
+ throw new Error('Library curator operation is missing required fields.');
312
+ }
313
+ return {
314
+ kind,
315
+ relativePath,
316
+ title: title || path.posix.basename(relativePath, path.posix.extname(relativePath)),
317
+ summary,
318
+ content,
319
+ sourceSavedItemIds,
320
+ };
321
+ });
322
+ const processedSavedItemIds = parsed.processedSavedItemIds
323
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
324
+ .map((value) => value.trim());
325
+ return {
326
+ operations,
327
+ processedSavedItemIds: [...new Set(processedSavedItemIds)],
328
+ };
329
+ }
330
+ export class PlatformAgentService {
331
+ db;
332
+ conversationManager;
333
+ nodeRegistry;
334
+ workspaceBroker;
335
+ workspaceRoot;
336
+ libraryService;
337
+ suggestedPlannerService;
338
+ agentRuntimeCleanupBroker;
339
+ workflowHandlers;
340
+ constructor(db, conversationManager, nodeRegistry, workspaceBroker, workspaceRoot, libraryService, suggestedPlannerService, agentRuntimeCleanupBroker) {
341
+ this.db = db;
342
+ this.conversationManager = conversationManager;
343
+ this.nodeRegistry = nodeRegistry;
344
+ this.workspaceBroker = workspaceBroker;
345
+ this.workspaceRoot = workspaceRoot;
346
+ this.libraryService = libraryService;
347
+ this.suggestedPlannerService = suggestedPlannerService;
348
+ this.agentRuntimeCleanupBroker = agentRuntimeCleanupBroker;
349
+ this.workflowHandlers = {
350
+ planner_suggestions: {
351
+ workflowType: 'planner_suggestions',
352
+ defaultLabel: 'Planner Suggestions',
353
+ defaultDescription: 'Reviews operational context and proposes concrete follow-up opportunities.',
354
+ buildDefaultWorkflowConfig: () => ({ lookbackDays: DEFAULT_LOOKBACK_DAYS }),
355
+ buildPrompt: ({ definition, instance, snapshot }) => {
356
+ const lookbackDays = this.getPlannerLookbackDays(instance.workflowConfig);
357
+ const template = definition.promptTemplateOverride?.trim()
358
+ || getPlatformWorkflowCatalogEntry('planner_suggestions').defaultPromptTemplate;
359
+ return [
360
+ '# Platform planner workflow run',
361
+ '',
362
+ `Definition: ${definition.label} (${definition.definitionId})`,
363
+ `Lookback days: ${lookbackDays}`,
364
+ '',
365
+ template,
366
+ '',
367
+ 'Return only a JSON object. Do not include markdown, comments, or prose outside the JSON.',
368
+ 'Schema:',
369
+ '{',
370
+ ' "opportunities": [',
371
+ ' {',
372
+ ' "title": "short task candidate title",',
373
+ ' "rationale": "why this likely matters now",',
374
+ ' "suggestedAction": "what task should be created if the user accepts",',
375
+ ' "priority": "high | medium | low",',
376
+ ' "target": { "type": "dm | channel | general", "agentId": "optional agent id", "channelId": "optional channel id" },',
377
+ ' "evidenceLinks": [{ "label": "source label", "url": "app-relative or absolute URL", "summary": "short source summary" }]',
378
+ ' }',
379
+ ' ]',
380
+ '}',
381
+ '',
382
+ 'Rules:',
383
+ '- Include at most 8 opportunities.',
384
+ '- Prefer concrete follow-ups backed by the provided resources.',
385
+ '- Use target.type "dm" only when the target agent id is known; use "channel" only when channel id is known; otherwise use "general".',
386
+ '- If there is no meaningful next work, return {"opportunities":[]}.',
387
+ '',
388
+ '```json',
389
+ JSON.stringify({
390
+ workflowConfig: { lookbackDays },
391
+ resources: snapshot,
392
+ }, null, 2),
393
+ '```',
394
+ ].join('\n');
395
+ },
396
+ finalize: async ({ userId, runId, rawOutputText }) => {
397
+ const parsed = parseSuggestedPlannerOutput(rawOutputText);
398
+ if (parsed.error)
399
+ throw new Error(parsed.error);
400
+ const now = Date.now();
401
+ const opportunities = parsed.opportunities.map((opportunity) => {
402
+ const target = this.resolveAccessiblePlannerTarget(userId, opportunity);
403
+ const dedupePayload = [
404
+ opportunity.title.toLowerCase(),
405
+ target.targetType,
406
+ target.channelId ?? '',
407
+ target.agentId ?? '',
408
+ opportunity.evidenceLinks.map((link) => link.url).join('|'),
409
+ ].join('\n');
410
+ return {
411
+ dedupeKey: `planner:v2:${plannerDedupeHash(dedupePayload)}`,
412
+ opportunity,
413
+ target,
414
+ };
415
+ });
416
+ const seenKeys = new Set();
417
+ let generatedCount = 0;
418
+ for (const item of opportunities) {
419
+ if (seenKeys.has(item.dedupeKey))
420
+ continue;
421
+ seenKeys.add(item.dedupeKey);
422
+ insertPlannerTaskCandidateOpportunity(this.db, {
423
+ userId,
424
+ dedupeKey: item.dedupeKey,
425
+ title: item.opportunity.title,
426
+ rationale: item.opportunity.rationale,
427
+ channelId: item.target.channelId,
428
+ agentId: item.target.agentId,
429
+ evidence: {
430
+ source: 'suggested_planner',
431
+ plannerRunId: runId,
432
+ priority: item.opportunity.priority,
433
+ targetType: item.target.targetType,
434
+ channelId: item.target.channelId,
435
+ agentId: item.target.agentId,
436
+ requestedTargetType: item.opportunity.targetType,
437
+ requestedChannelId: item.opportunity.channelId,
438
+ requestedAgentId: item.opportunity.agentId,
439
+ suggestedTitle: item.opportunity.title,
440
+ suggestedDescription: item.opportunity.suggestedAction || item.opportunity.rationale,
441
+ suggestedAction: item.opportunity.suggestedAction,
442
+ },
443
+ evidenceLinks: item.opportunity.evidenceLinks,
444
+ now,
445
+ });
446
+ generatedCount += 1;
447
+ }
448
+ return {
449
+ outputJson: { opportunities: parsed.opportunities },
450
+ resultJson: { generatedCount },
451
+ generatedCount,
452
+ };
453
+ },
454
+ },
455
+ library_curate: {
456
+ workflowType: 'library_curate',
457
+ defaultLabel: 'Library Curate',
458
+ defaultDescription: 'Organizes saved information into a personal markdown library.',
459
+ buildDefaultWorkflowConfig: () => ({ categories: [] }),
460
+ buildPrompt: ({ definition, instance, snapshot }) => {
461
+ const categories = this.getCuratorCategories(instance.workflowConfig);
462
+ const template = definition.promptTemplateOverride?.trim()
463
+ || getPlatformWorkflowCatalogEntry('library_curate').defaultPromptTemplate;
464
+ return [
465
+ '# Platform library curate workflow run',
466
+ '',
467
+ `Definition: ${definition.label} (${definition.definitionId})`,
468
+ '',
469
+ template,
470
+ '',
471
+ 'Return only a JSON object. Do not include markdown, comments, or prose outside the JSON.',
472
+ 'Schema:',
473
+ '{',
474
+ ' "operations": [',
475
+ ' {',
476
+ ' "kind": "create | update",',
477
+ ' "relativePath": "category/topic/file.md",',
478
+ ' "title": "document title",',
479
+ ' "summary": "short summary",',
480
+ ' "content": "# Markdown content",',
481
+ ' "sourceSavedItemIds": ["saved-item-id"]',
482
+ ' }',
483
+ ' ],',
484
+ ' "processedSavedItemIds": ["saved-item-id"]',
485
+ '}',
486
+ '',
487
+ 'Rules:',
488
+ '- Only create or update markdown documents under the personal library.',
489
+ '- Prefer updating an existing document before creating a new one.',
490
+ '- Do not rename, move, or delete documents.',
491
+ '- Do not target reserved/system paths such as `.meta/*` or `INDEX.md`.',
492
+ '- Every processedSavedItemId must come from the provided inbox items.',
493
+ '',
494
+ '```json',
495
+ JSON.stringify({
496
+ workflowConfig: { categories },
497
+ resources: snapshot,
498
+ }, null, 2),
499
+ '```',
500
+ ].join('\n');
501
+ },
502
+ shouldSkip: async ({ snapshot }) => {
503
+ const savedItems = Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems : [];
504
+ if (savedItems.length === 0)
505
+ return { skipped: true, reason: 'No inbox items to organize' };
506
+ return null;
507
+ },
508
+ finalize: async ({ userId, runId, rawOutputText, snapshot }) => {
509
+ const plan = parseCuratorPlan(rawOutputText);
510
+ const savedItems = Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems : [];
511
+ const result = await this.libraryService.applyStructuredCuratorPlan({
512
+ userId,
513
+ runId,
514
+ plan,
515
+ savedItems,
516
+ });
517
+ return {
518
+ outputJson: plan,
519
+ resultJson: result,
520
+ generatedCount: result.writtenDocuments,
521
+ };
522
+ },
523
+ },
524
+ };
525
+ }
526
+ async ensureBuiltinPlatformAgentsForUser(params) {
527
+ await this.suggestedPlannerService.ensureConfigForUser(params);
528
+ await this.libraryService.ensureConfigForUser(params);
529
+ await this.syncBuiltinSuggestedPlannerInstance(params);
530
+ await this.syncBuiltinLibraryCuratorInstance(params);
531
+ }
532
+ getDefinition(definitionId) {
533
+ const row = this.db.prepare(`SELECT definition_id as definitionId,
534
+ workflow_type as workflowType,
535
+ label,
536
+ description,
537
+ resource_scopes_json as resourceScopesJson,
538
+ resource_config_json as resourceConfigJson,
539
+ prompt_template_override as promptTemplateOverride,
540
+ tool_policy_json as toolPolicyJson,
541
+ enabled,
542
+ created_at as createdAt,
543
+ updated_at as updatedAt
544
+ FROM platform_agent_definitions
545
+ WHERE definition_id = ?`).get(definitionId);
546
+ return row ? mapDefinitionRow(row) : null;
547
+ }
548
+ listDefinitions() {
549
+ const rows = this.db.prepare(`SELECT definition_id as definitionId,
550
+ workflow_type as workflowType,
551
+ label,
552
+ description,
553
+ resource_scopes_json as resourceScopesJson,
554
+ resource_config_json as resourceConfigJson,
555
+ prompt_template_override as promptTemplateOverride,
556
+ tool_policy_json as toolPolicyJson,
557
+ enabled,
558
+ created_at as createdAt,
559
+ updated_at as updatedAt
560
+ FROM platform_agent_definitions
561
+ ORDER BY definition_id ASC`).all();
562
+ return rows.map(mapDefinitionRow);
563
+ }
564
+ getWorkflowCatalog() {
565
+ return getPlatformWorkflowCatalog();
566
+ }
567
+ getInstance(userId, definitionId) {
568
+ const row = this.db.prepare(`SELECT user_id as userId,
569
+ definition_id as definitionId,
570
+ agent_id as agentId,
571
+ enabled,
572
+ schedule_interval_ms as scheduleIntervalMs,
573
+ workflow_config_json as workflowConfigJson,
574
+ runtime_config_json as runtimeConfigJson,
575
+ last_run_at as lastRunAt,
576
+ next_run_at as nextRunAt,
577
+ created_at as createdAt,
578
+ updated_at as updatedAt,
579
+ deleted_at as deletedAt
580
+ FROM platform_agent_instances
581
+ WHERE user_id = ? AND definition_id = ? AND deleted_at IS NULL`).get(userId, definitionId);
582
+ return row ? mapInstanceRow(row) : null;
583
+ }
584
+ getInstanceRow(userId, definitionId) {
585
+ return this.db.prepare(`SELECT user_id as userId,
586
+ definition_id as definitionId,
587
+ agent_id as agentId,
588
+ enabled,
589
+ schedule_interval_ms as scheduleIntervalMs,
590
+ workflow_config_json as workflowConfigJson,
591
+ runtime_config_json as runtimeConfigJson,
592
+ last_run_at as lastRunAt,
593
+ next_run_at as nextRunAt,
594
+ created_at as createdAt,
595
+ updated_at as updatedAt,
596
+ deleted_at as deletedAt
597
+ FROM platform_agent_instances
598
+ WHERE user_id = ? AND definition_id = ?`).get(userId, definitionId);
599
+ }
600
+ getLastRun(userId, definitionId) {
601
+ if (definitionId === 'suggested_planner') {
602
+ this.purgeLegacySuggestedPlannerWorkflowRuns({ userId });
603
+ }
604
+ const row = this.db.prepare(`SELECT run_id as runId,
605
+ user_id as userId,
606
+ definition_id as definitionId,
607
+ mode,
608
+ status,
609
+ created_at as createdAt,
610
+ completed_at as completedAt,
611
+ error,
612
+ generated_count as generatedCount,
613
+ resources_accessed_json as resourcesAccessedJson
614
+ FROM platform_agent_runs
615
+ WHERE user_id = ? AND definition_id = ?
616
+ ORDER BY created_at DESC
617
+ LIMIT 1`).get(userId, definitionId);
618
+ return row ? mapRunRow(row) : null;
619
+ }
620
+ getPlatformRunContextForAgent(agentId) {
621
+ const row = this.db.prepare(`SELECT pai.definition_id as definitionId,
622
+ pad.workflow_type as workflowType
623
+ FROM platform_agent_instances pai
624
+ JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
625
+ WHERE pai.agent_id = ?
626
+ LIMIT 1`).get(agentId);
627
+ return row ?? null;
628
+ }
629
+ async listPlatformAgentsForUser(params) {
630
+ this.purgeLegacySuggestedPlannerWorkflowRuns({ userId: params.userId });
631
+ await this.ensureBuiltinPlatformAgentsForUser(params);
632
+ for (const definition of this.listDefinitions()) {
633
+ const row = this.getInstanceRow(params.userId, definition.definitionId);
634
+ if (row?.deletedAt)
635
+ continue; // skip deleted instances; do not auto-recreate
636
+ await this.ensureInstanceForUser({ ...params, definitionId: definition.definitionId });
637
+ }
638
+ const result = [];
639
+ for (const definition of this.listDefinitions()) {
640
+ const instance = this.getInstance(params.userId, definition.definitionId);
641
+ if (!instance)
642
+ continue;
643
+ result.push({
644
+ definition,
645
+ instance,
646
+ agent: this.conversationManager.getAgent(instance.agentId),
647
+ lastRun: this.getLastRun(params.userId, definition.definitionId),
648
+ });
649
+ }
650
+ return result;
651
+ }
652
+ async createDefinition(params) {
653
+ const definitionId = sanitizeDefinitionId(params.definitionId);
654
+ if (this.getDefinition(definitionId)) {
655
+ throw new Error(`Platform definition "${definitionId}" already exists.`);
656
+ }
657
+ const now = Date.now();
658
+ const toolPolicy = normalizePlatformAgentToolPolicy(params.workflowType, params.toolPolicy);
659
+ const resourceScopes = normalizePlatformResourceScopes(params.workflowType, params.resourceScopes);
660
+ this.db.prepare(`INSERT INTO platform_agent_definitions(
661
+ definition_id, workflow_type, label, description, resource_scopes_json,
662
+ resource_config_json, prompt_template_override, tool_policy_json,
663
+ enabled, created_at, updated_at
664
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(definitionId, params.workflowType, params.label.trim(), params.description.trim(), JSON.stringify(resourceScopes), JSON.stringify(params.resourceConfig ?? {}), params.promptTemplateOverride?.trim() || null, JSON.stringify(toolPolicy), params.enabled === false ? 0 : 1, now, now);
665
+ const users = this.db.prepare(`SELECT id as userId, username FROM users`).all();
666
+ for (const user of users) {
667
+ await this.ensureInstanceForUser({ userId: user.userId, username: user.username, definitionId });
668
+ }
669
+ return this.getDefinition(definitionId);
670
+ }
671
+ async updateDefinition(definitionId, patch) {
672
+ const existing = this.getDefinition(definitionId);
673
+ if (!existing)
674
+ throw new Error('Platform definition not found.');
675
+ const now = Date.now();
676
+ const nextWorkflowType = existing.workflowType;
677
+ const nextLabel = patch.label?.trim() || existing.label;
678
+ const nextDescription = patch.description?.trim() || existing.description;
679
+ const nextResourceScopes = patch.resourceScopes
680
+ ? normalizePlatformResourceScopes(nextWorkflowType, patch.resourceScopes)
681
+ : existing.resourceScopes;
682
+ const nextResourceConfig = Object.prototype.hasOwnProperty.call(patch, 'resourceConfig')
683
+ ? (patch.resourceConfig ?? {})
684
+ : (existing.resourceConfig ?? {});
685
+ const nextPromptTemplate = Object.prototype.hasOwnProperty.call(patch, 'promptTemplateOverride')
686
+ ? patch.promptTemplateOverride?.trim() || null
687
+ : existing.promptTemplateOverride ?? null;
688
+ const nextToolPolicy = patch.toolPolicy
689
+ ? normalizePlatformAgentToolPolicy(nextWorkflowType, patch.toolPolicy)
690
+ : normalizePlatformAgentToolPolicy(nextWorkflowType, existing.toolPolicy);
691
+ const nextEnabled = patch.enabled ?? existing.enabled;
692
+ this.db.prepare(`UPDATE platform_agent_definitions
693
+ SET label = ?,
694
+ description = ?,
695
+ resource_scopes_json = ?,
696
+ resource_config_json = ?,
697
+ prompt_template_override = ?,
698
+ tool_policy_json = ?,
699
+ enabled = ?,
700
+ updated_at = ?
701
+ WHERE definition_id = ?`).run(nextLabel, nextDescription, JSON.stringify(nextResourceScopes), JSON.stringify(nextResourceConfig), nextPromptTemplate, JSON.stringify(nextToolPolicy), nextEnabled ? 1 : 0, now, definitionId);
702
+ if (nextEnabled && !existing.enabled) {
703
+ const users = this.db.prepare(`SELECT id as userId, username FROM users`).all();
704
+ for (const user of users) {
705
+ const row = this.getInstanceRow(user.userId, definitionId);
706
+ if (row?.deletedAt)
707
+ continue; // do not auto-recreate deleted instances
708
+ await this.ensureInstanceForUser({ userId: user.userId, username: user.username, definitionId });
709
+ }
710
+ }
711
+ await this.syncInstancesForDefinition(definitionId);
712
+ if (!nextEnabled && existing.enabled) {
713
+ await this.cleanupPlatformAgentDefinitionRuntime({
714
+ definitionId,
715
+ reason: 'Platform agent definition disabled.',
716
+ });
717
+ }
718
+ return this.getDefinition(definitionId);
719
+ }
720
+ async ensureInstanceForUser(params) {
721
+ const existingRow = this.getInstanceRow(params.userId, params.definitionId);
722
+ if (existingRow?.deletedAt) {
723
+ throw new Error('Platform agent instance has been deleted.');
724
+ }
725
+ if (params.definitionId === 'suggested_planner') {
726
+ return this.syncBuiltinSuggestedPlannerInstance(params);
727
+ }
728
+ if (params.definitionId === 'library_curator') {
729
+ return this.syncBuiltinLibraryCuratorInstance(params);
730
+ }
731
+ const definition = this.getDefinition(params.definitionId);
732
+ if (!definition)
733
+ throw new Error('Platform definition not found.');
734
+ const existing = this.getInstance(params.userId, params.definitionId);
735
+ if (existing) {
736
+ return this.syncExistingCustomInstance({ ...params, definition, existing });
737
+ }
738
+ const now = Date.now();
739
+ const workspacePath = this.buildCustomWorkspacePath(definition, params.username);
740
+ fs.mkdirSync(workspacePath, { recursive: true });
741
+ const runtimeConfig = this.buildDefaultRuntimeConfig(definition.workflowType);
742
+ const nodeId = this.pickNodeId(definition.workflowType);
743
+ const toolPolicy = normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy);
744
+ const agentId = randomUUID();
745
+ this.db.prepare(`INSERT INTO agents(
746
+ agent_id, name, agent_type, agent_role, model, reasoning_effort, claude_permission_mode,
747
+ codex_mode, codex_service_tier, channel_id, system_prompt, description, memory,
748
+ env_vars, disabled_tool_kinds, node_id, workspace_path, project_path, skill_roots,
749
+ created_at, updated_at
750
+ ) VALUES(?, ?, ?, 'platform_agent', ?, ?, ?, ?, ?, ?, '', ?, '', ?, ?, ?, ?, NULL, NULL, ?, ?)`).run(agentId, definition.label, PLATFORM_AGENT_TYPE, runtimeConfig.model ?? 'deepseek-v4-pro', runtimeConfig.reasoningEffort ?? null, 'bypassPermissions', null, null, 'default', definition.description, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), nodeId, workspacePath, now, now);
751
+ this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
752
+ VALUES(?, 'default', 1, ?)`).run(agentId, now);
753
+ this.db.prepare(`INSERT OR IGNORE INTO user_agent_access(user_id, agent_id, granted_at) VALUES(?, ?, ?)`).run(params.userId, agentId, now);
754
+ const instance = this.upsertInstance({
755
+ userId: params.userId,
756
+ definitionId: params.definitionId,
757
+ agentId,
758
+ enabled: definition.enabled,
759
+ scheduleIntervalMs: DEFAULT_PLATFORM_AGENT_INTERVAL_MS,
760
+ workflowConfig: this.workflowHandlers[definition.workflowType].buildDefaultWorkflowConfig(),
761
+ runtimeConfig,
762
+ lastRunAt: null,
763
+ nextRunAt: now + DEFAULT_PLATFORM_AGENT_INTERVAL_MS,
764
+ createdAt: now,
765
+ updatedAt: now,
766
+ });
767
+ return instance;
768
+ }
769
+ async openConversation(userId, username, definitionId, mode) {
770
+ const definition = this.getDefinition(definitionId);
771
+ if (!definition?.enabled)
772
+ return null;
773
+ const instance = await this.ensureInstanceForUser({ userId, username, definitionId });
774
+ if (!instance.enabled)
775
+ return null;
776
+ const title = getPlatformAgentConversationTitle({ definitionId, mode });
777
+ return this.conversationManager.openNamedAgentPrimaryThread?.({
778
+ agentId: instance.agentId,
779
+ title,
780
+ userId,
781
+ legacyTitles: mode === 'debug' ? getPlatformAgentLegacyConversationTitles(definitionId) : undefined,
782
+ }) ?? this.conversationManager.openAgentThread(instance.agentId, userId);
783
+ }
784
+ async updateInstance(params) {
785
+ const definition = this.getDefinition(params.definitionId);
786
+ if (!definition)
787
+ throw new Error('Platform definition not found.');
788
+ let instance = await this.ensureInstanceForUser(params);
789
+ if (isBuiltinPlatformAgentDefinitionId(params.definitionId)) {
790
+ instance = await this.updateBuiltinInstance({
791
+ ...params,
792
+ definitionId: params.definitionId,
793
+ }, definition);
794
+ }
795
+ else {
796
+ instance = this.updateCustomInstance({
797
+ definition,
798
+ instance,
799
+ enabled: params.enabled,
800
+ scheduleIntervalMs: params.scheduleIntervalMs,
801
+ workflowConfig: params.workflowConfig,
802
+ runtimeConfig: params.runtimeConfig,
803
+ });
804
+ }
805
+ if (params.enabled === false) {
806
+ await this.cleanupPlatformAgentInstanceRuntime({
807
+ userId: params.userId,
808
+ definitionId: params.definitionId,
809
+ agentId: instance.agentId,
810
+ reason: 'Platform agent instance disabled.',
811
+ });
812
+ }
813
+ return {
814
+ definition,
815
+ instance,
816
+ agent: this.conversationManager.getAgent(instance.agentId),
817
+ lastRun: this.getLastRun(params.userId, params.definitionId),
818
+ };
819
+ }
820
+ deleteInstance(userId, definitionId) {
821
+ const definition = this.getDefinition(definitionId);
822
+ if (!definition)
823
+ return { ok: false, error: 'Platform definition not found' };
824
+ const existing = this.getInstanceRow(userId, definitionId);
825
+ if (!existing || existing.deletedAt) {
826
+ return { ok: false, error: 'Platform agent instance not found or already deleted' };
827
+ }
828
+ const now = Date.now();
829
+ const result = this.db.prepare(`UPDATE platform_agent_instances
830
+ SET deleted_at = ?,
831
+ enabled = 0,
832
+ next_run_at = NULL,
833
+ updated_at = ?
834
+ WHERE user_id = ? AND definition_id = ? AND deleted_at IS NULL`).run(now, now, userId, definitionId);
835
+ if (result.changes === 0) {
836
+ return { ok: false, error: 'Platform agent instance not found or already deleted' };
837
+ }
838
+ this.syncBuiltinLegacyMirrorFromRow({
839
+ ...existing,
840
+ enabled: 0,
841
+ nextRunAt: null,
842
+ updatedAt: now,
843
+ deletedAt: now,
844
+ });
845
+ void this.cleanupPlatformAgentInstanceRuntime({
846
+ userId,
847
+ definitionId,
848
+ agentId: existing.agentId,
849
+ reason: 'Platform agent instance deleted.',
850
+ });
851
+ return { ok: true };
852
+ }
853
+ async cleanupPlatformAgentInstanceRuntime(params) {
854
+ const targets = this.db.prepare(`SELECT id as conversationId,
855
+ node_id as nodeId,
856
+ agent_type as agentType,
857
+ session_key as sessionKey
858
+ FROM conversations
859
+ WHERE agent_id = ?`).all(params.agentId);
860
+ const uniqueTargets = new Map();
861
+ for (const target of targets) {
862
+ const hostKey = `conversation:${target.conversationId}:${target.agentType}`;
863
+ uniqueTargets.set(hostKey, {
864
+ conversationId: target.conversationId,
865
+ nodeId: target.nodeId,
866
+ hostKey,
867
+ sessionKey: target.sessionKey,
868
+ });
869
+ }
870
+ const now = Date.now();
871
+ this.db.transaction(() => {
872
+ for (const target of uniqueTargets.values()) {
873
+ this.db.prepare(`DELETE FROM node_dispatch_queue
874
+ WHERE host_key = ?
875
+ OR session_key = ?
876
+ OR conversation_id = ?`).run(target.hostKey, target.sessionKey, target.conversationId);
877
+ this.db.prepare(`DELETE FROM node_host_snapshots
878
+ WHERE host_key = ?
879
+ OR session_key = ?`).run(target.hostKey, target.sessionKey);
880
+ this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(target.conversationId);
881
+ this.db.prepare(`UPDATE conversations
882
+ SET status = CASE
883
+ WHEN status IN ('queued', 'active', 'recovering', 'awaiting_approval') THEN 'idle'
884
+ ELSE status
885
+ END
886
+ WHERE id = ?`).run(target.conversationId);
887
+ this.db.prepare(`UPDATE runs
888
+ SET ended_at = COALESCE(ended_at, ?),
889
+ stop_reason = COALESCE(stop_reason, 'error'),
890
+ error = COALESCE(error, ?)
891
+ WHERE session_key = ?
892
+ AND ended_at IS NULL`).run(now, params.reason, target.sessionKey);
893
+ }
894
+ this.db.prepare(`UPDATE platform_agent_runs
895
+ SET status = 'failed',
896
+ completed_at = COALESCE(completed_at, ?),
897
+ error = COALESCE(error, ?)
898
+ WHERE user_id = ?
899
+ AND definition_id = ?
900
+ AND status = 'running'`).run(now, params.reason, params.userId, params.definitionId);
901
+ })();
902
+ if (!this.agentRuntimeCleanupBroker)
903
+ return;
904
+ const targetsByNode = new Map();
905
+ for (const target of uniqueTargets.values()) {
906
+ if (!target.nodeId)
907
+ continue;
908
+ const entry = targetsByNode.get(target.nodeId) ?? { hostKeys: new Set(), sessionKeys: new Set() };
909
+ entry.hostKeys.add(target.hostKey);
910
+ entry.sessionKeys.add(target.sessionKey);
911
+ targetsByNode.set(target.nodeId, entry);
912
+ }
913
+ for (const [nodeId, target] of targetsByNode.entries()) {
914
+ try {
915
+ await this.agentRuntimeCleanupBroker.cleanupAgentRuntime({
916
+ nodeId,
917
+ agentId: params.agentId,
918
+ hostKeys: [...target.hostKeys],
919
+ sessionKeys: [...target.sessionKeys],
920
+ });
921
+ }
922
+ catch (error) {
923
+ log.warn('[platform-agent] runtime cleanup failed; stale node state will be rejected on reconnect', {
924
+ userId: params.userId,
925
+ definitionId: params.definitionId,
926
+ agentId: params.agentId,
927
+ nodeId,
928
+ error: String(error?.message ?? error),
929
+ });
930
+ }
931
+ }
932
+ }
933
+ async cleanupPlatformAgentDefinitionRuntime(params) {
934
+ const rows = this.db.prepare(`SELECT user_id as userId,
935
+ definition_id as definitionId,
936
+ agent_id as agentId,
937
+ enabled,
938
+ schedule_interval_ms as scheduleIntervalMs,
939
+ workflow_config_json as workflowConfigJson,
940
+ runtime_config_json as runtimeConfigJson,
941
+ last_run_at as lastRunAt,
942
+ next_run_at as nextRunAt,
943
+ created_at as createdAt,
944
+ updated_at as updatedAt,
945
+ deleted_at as deletedAt
946
+ FROM platform_agent_instances
947
+ WHERE definition_id = ?
948
+ AND deleted_at IS NULL`).all(params.definitionId);
949
+ for (const row of rows) {
950
+ await this.cleanupPlatformAgentInstanceRuntime({
951
+ userId: row.userId,
952
+ definitionId: row.definitionId,
953
+ agentId: row.agentId,
954
+ reason: params.reason,
955
+ });
956
+ }
957
+ }
958
+ syncBuiltinLegacyMirrorFromRow(row) {
959
+ this.syncBuiltinLegacyMirror(mapInstanceRow(row));
960
+ }
961
+ syncBuiltinLegacyMirror(instance) {
962
+ if (instance.definitionId === 'suggested_planner') {
963
+ this.syncSuggestedPlannerLegacyMirror(instance);
964
+ return;
965
+ }
966
+ if (instance.definitionId === 'library_curator') {
967
+ this.syncLibraryCuratorLegacyMirror(instance);
968
+ }
969
+ }
970
+ syncSuggestedPlannerLegacyMirror(instance) {
971
+ try {
972
+ const now = Date.now();
973
+ this.db.prepare(`UPDATE suggested_planner_configs
974
+ SET enabled = ?,
975
+ schedule_interval_ms = ?,
976
+ lookback_days = ?,
977
+ last_run_at = ?,
978
+ next_run_at = ?,
979
+ updated_at = ?
980
+ WHERE user_id = ?`).run(instance.enabled ? 1 : 0, Math.max(60_000, instance.scheduleIntervalMs), this.getPlannerLookbackDays(instance.workflowConfig), instance.lastRunAt, instance.enabled ? instance.nextRunAt : null, now, instance.userId);
981
+ }
982
+ catch {
983
+ // Legacy mirror may be absent in narrow tests or future schemas.
984
+ }
985
+ }
986
+ syncLibraryCuratorLegacyMirror(instance) {
987
+ try {
988
+ const now = Date.now();
989
+ this.db.prepare(`UPDATE library_curator_configs
990
+ SET enabled = ?,
991
+ schedule_interval_ms = ?,
992
+ categories_json = ?,
993
+ last_run_at = ?,
994
+ next_run_at = ?,
995
+ updated_at = ?
996
+ WHERE user_id = ?`).run(instance.enabled ? 1 : 0, Math.max(60_000, instance.scheduleIntervalMs), JSON.stringify(this.getCuratorCategories(instance.workflowConfig)), instance.lastRunAt, instance.enabled ? instance.nextRunAt : null, now, instance.userId);
997
+ }
998
+ catch {
999
+ // Legacy mirror may be absent in narrow tests or future schemas.
1000
+ }
1001
+ }
1002
+ restoreInstance(userId, definitionId) {
1003
+ const definition = this.getDefinition(definitionId);
1004
+ if (!definition)
1005
+ throw new Error('Platform definition not found.');
1006
+ const now = Date.now();
1007
+ const row = this.getInstanceRow(userId, definitionId);
1008
+ if (!row || !row.deletedAt) {
1009
+ throw new Error('Platform agent instance not found or not deleted');
1010
+ }
1011
+ const scheduleIntervalMs = row.scheduleIntervalMs ?? DEFAULT_PLATFORM_AGENT_INTERVAL_MS;
1012
+ const result = this.db.prepare(`UPDATE platform_agent_instances
1013
+ SET deleted_at = NULL,
1014
+ enabled = 1,
1015
+ next_run_at = ?,
1016
+ updated_at = ?
1017
+ WHERE user_id = ? AND definition_id = ? AND deleted_at IS NOT NULL`).run(now + scheduleIntervalMs, now, userId, definitionId);
1018
+ if (result.changes === 0) {
1019
+ throw new Error('Platform agent instance not found or not deleted');
1020
+ }
1021
+ const restored = this.getInstance(userId, definitionId);
1022
+ if (!restored) {
1023
+ throw new Error('Platform agent instance restore failed');
1024
+ }
1025
+ this.syncBuiltinLegacyMirror(restored);
1026
+ return restored;
1027
+ }
1028
+ async dispatchWorkflowRun(params) {
1029
+ if (params.definitionId === 'suggested_planner') {
1030
+ this.purgeLegacySuggestedPlannerWorkflowRuns({ userId: params.userId });
1031
+ }
1032
+ const definition = this.getDefinition(params.definitionId);
1033
+ if (!definition)
1034
+ return { ok: false, error: 'Platform definition not found' };
1035
+ const instance = await this.ensureInstanceForUser(params);
1036
+ if (!definition.enabled) {
1037
+ return { ok: true, skipped: true, reason: 'Platform definition is disabled' };
1038
+ }
1039
+ if (!instance.enabled) {
1040
+ return { ok: true, skipped: true, reason: 'Platform instance is disabled' };
1041
+ }
1042
+ this.reconcileTerminalPlatformRunRuntimeState(params.userId, params.definitionId);
1043
+ await this.reconcileCompletedRunningRuns(params.userId, params.definitionId);
1044
+ const running = this.db.prepare(`SELECT run_id as runId
1045
+ FROM platform_agent_runs
1046
+ WHERE user_id = ? AND definition_id = ? AND status = 'running'
1047
+ ORDER BY created_at DESC
1048
+ LIMIT 1`).get(params.userId, params.definitionId);
1049
+ if (running) {
1050
+ return { ok: true, skipped: true, queued: true, runId: running.runId, reason: 'Platform agent run already in progress' };
1051
+ }
1052
+ const handler = this.workflowHandlers[definition.workflowType];
1053
+ const snapshot = await this.buildResourceSnapshot({
1054
+ userId: params.userId,
1055
+ username: params.username,
1056
+ definition,
1057
+ instance,
1058
+ });
1059
+ const dispatchInput = {
1060
+ definition,
1061
+ instance,
1062
+ userId: params.userId,
1063
+ username: params.username,
1064
+ snapshot,
1065
+ };
1066
+ const skip = await handler.shouldSkip?.(dispatchInput);
1067
+ if (skip) {
1068
+ this.scheduleNextRun(params.userId, params.definitionId, Date.now(), instance.scheduleIntervalMs);
1069
+ return { ok: true, skipped: true, reason: skip.reason };
1070
+ }
1071
+ const conversation = await this.openConversation(params.userId, params.username, params.definitionId, 'workflow');
1072
+ if (!conversation)
1073
+ return { ok: false, error: 'Failed to open platform workflow conversation' };
1074
+ const prompt = handler.buildPrompt(dispatchInput);
1075
+ const activationMetadata = {
1076
+ platformRun: {
1077
+ definitionId: definition.definitionId,
1078
+ workflowType: definition.workflowType,
1079
+ mode: 'workflow',
1080
+ },
1081
+ suppressReplyContract: true,
1082
+ };
1083
+ const result = await this.conversationManager.submitPrompt(conversation.id, prompt, {
1084
+ recordAsUserMessage: false,
1085
+ activationMetadata,
1086
+ suppressReplyContract: true,
1087
+ });
1088
+ const now = Date.now();
1089
+ if (!result.runId) {
1090
+ this.scheduleNextRun(params.userId, params.definitionId, now, instance.scheduleIntervalMs);
1091
+ return { ok: false, error: 'Failed to start platform agent run', queued: result.queued };
1092
+ }
1093
+ const existingRun = this.db.prepare(`SELECT status
1094
+ FROM platform_agent_runs
1095
+ WHERE run_id = ?`).get(result.runId);
1096
+ if (existingRun) {
1097
+ this.reconcileTerminalPlatformRunRuntimeState(params.userId, params.definitionId);
1098
+ return {
1099
+ ok: true,
1100
+ skipped: true,
1101
+ queued: true,
1102
+ runId: result.runId,
1103
+ reason: existingRun.status === 'running'
1104
+ ? 'Platform agent run already in progress'
1105
+ : 'Platform agent runtime returned an already recorded run',
1106
+ };
1107
+ }
1108
+ const resourcesAccessed = {
1109
+ savedItems: Array.isArray(snapshot.savedInboxItems) ? snapshot.savedInboxItems.length : 0,
1110
+ tasks: Array.isArray(snapshot.openTasks) ? snapshot.openTasks.length : 0,
1111
+ messages: Array.isArray(snapshot.recentMessages) ? snapshot.recentMessages.length : 0,
1112
+ };
1113
+ this.db.prepare(`INSERT INTO platform_agent_runs(
1114
+ run_id, user_id, definition_id, agent_id, mode, status, created_at,
1115
+ input_snapshot_json, generated_count, resources_accessed_json
1116
+ ) VALUES(?, ?, ?, ?, 'workflow', 'running', ?, ?, 0, ?)`).run(result.runId, params.userId, definition.definitionId, instance.agentId, now, JSON.stringify(snapshot), JSON.stringify(resourcesAccessed));
1117
+ this.db.prepare(`UPDATE platform_agent_instances
1118
+ SET last_run_at = ?,
1119
+ updated_at = ?
1120
+ WHERE user_id = ? AND definition_id = ?`).run(now, now, params.userId, definition.definitionId);
1121
+ await this.reconcileRunStateAfterInsert({
1122
+ runId: result.runId,
1123
+ definitionId: definition.definitionId,
1124
+ });
1125
+ return { ok: true, runId: result.runId, queued: result.queued, generatedCount: 0 };
1126
+ }
1127
+ async finalizeRun(params) {
1128
+ const row = this.db.prepare(`SELECT run_id as runId,
1129
+ user_id as userId,
1130
+ definition_id as definitionId,
1131
+ mode,
1132
+ status,
1133
+ created_at as createdAt,
1134
+ completed_at as completedAt,
1135
+ error,
1136
+ generated_count as generatedCount,
1137
+ resources_accessed_json as resourcesAccessedJson
1138
+ FROM platform_agent_runs
1139
+ WHERE run_id = ?`).get(params.runId);
1140
+ if (!row || row.status !== 'running')
1141
+ return;
1142
+ const definition = this.getDefinition(row.definitionId);
1143
+ const instance = definition ? this.getInstance(row.userId, row.definitionId) : null;
1144
+ if (!definition || !instance)
1145
+ return;
1146
+ const now = Date.now();
1147
+ const persistedError = params.error?.trim() || null;
1148
+ if (persistedError) {
1149
+ this.db.prepare(`UPDATE platform_agent_runs
1150
+ SET status = 'failed',
1151
+ completed_at = ?,
1152
+ error = ?
1153
+ WHERE run_id = ?`).run(now, persistedError, params.runId);
1154
+ this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
1155
+ return;
1156
+ }
1157
+ try {
1158
+ const rawOutputText = this.getRunOutputText(params.runId);
1159
+ const snapshotRow = this.db.prepare(`SELECT input_snapshot_json as inputSnapshotJson
1160
+ FROM platform_agent_runs
1161
+ WHERE run_id = ?`).get(params.runId);
1162
+ if (definition.definitionId === 'library_curator' && !snapshotRow?.inputSnapshotJson?.trim()) {
1163
+ const legacy = await this.finalizeLegacyCuratorRun(params.runId, row.userId, now);
1164
+ this.db.prepare(`UPDATE platform_agent_runs
1165
+ SET status = ?,
1166
+ completed_at = ?,
1167
+ raw_output_text = ?,
1168
+ error = ?,
1169
+ generated_count = ?
1170
+ WHERE run_id = ?`).run(legacy.status, legacy.completedAt, rawOutputText || null, legacy.error, legacy.generatedCount, params.runId);
1171
+ this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
1172
+ return;
1173
+ }
1174
+ const handler = this.workflowHandlers[definition.workflowType];
1175
+ const result = await handler.finalize({
1176
+ definition,
1177
+ instance,
1178
+ userId: row.userId,
1179
+ username: this.getUsernameForUserId(row.userId) ?? 'user',
1180
+ runId: params.runId,
1181
+ rawOutputText,
1182
+ snapshot: parseJsonRecord(snapshotRow?.inputSnapshotJson),
1183
+ });
1184
+ this.db.prepare(`UPDATE platform_agent_runs
1185
+ SET status = 'completed',
1186
+ completed_at = ?,
1187
+ raw_output_text = ?,
1188
+ output_json = ?,
1189
+ result_json = ?,
1190
+ generated_count = ?
1191
+ WHERE run_id = ?`).run(now, rawOutputText || null, result.outputJson ? JSON.stringify(result.outputJson) : null, result.resultJson ? JSON.stringify(result.resultJson) : null, result.generatedCount ?? 0, params.runId);
1192
+ }
1193
+ catch (error) {
1194
+ this.db.prepare(`UPDATE platform_agent_runs
1195
+ SET status = 'failed',
1196
+ completed_at = ?,
1197
+ raw_output_text = ?,
1198
+ error = ?
1199
+ WHERE run_id = ?`).run(now, this.getRunOutputText(params.runId) || null, String(error?.message ?? error), params.runId);
1200
+ }
1201
+ this.scheduleNextRun(row.userId, row.definitionId, now, instance.scheduleIntervalMs);
1202
+ }
1203
+ listDueInstances(now) {
1204
+ const rows = this.db.prepare(`SELECT pai.user_id as userId,
1205
+ pai.definition_id as definitionId,
1206
+ pai.agent_id as agentId,
1207
+ pai.enabled,
1208
+ pai.schedule_interval_ms as scheduleIntervalMs,
1209
+ pai.workflow_config_json as workflowConfigJson,
1210
+ pai.runtime_config_json as runtimeConfigJson,
1211
+ pai.last_run_at as lastRunAt,
1212
+ pai.next_run_at as nextRunAt,
1213
+ pai.created_at as createdAt,
1214
+ pai.updated_at as updatedAt,
1215
+ pai.deleted_at as deletedAt
1216
+ FROM platform_agent_instances pai
1217
+ JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
1218
+ WHERE pai.enabled = 1
1219
+ AND pad.enabled = 1
1220
+ AND pai.next_run_at IS NOT NULL
1221
+ AND pai.next_run_at <= ?
1222
+ AND pai.deleted_at IS NULL`).all(now);
1223
+ return rows.map(mapInstanceRow);
1224
+ }
1225
+ purgeLegacySuggestedPlannerWorkflowRuns(params) {
1226
+ const filters = [
1227
+ `r.ended_at IS NULL`,
1228
+ `r.prompt_text LIKE ?`,
1229
+ `(spr.run_id IS NOT NULL OR (par.run_id IS NOT NULL AND par.definition_id = 'suggested_planner') OR c.title = ?)`,
1230
+ ];
1231
+ const args = [
1232
+ `${LEGACY_SUGGESTED_PLANNER_PROMPT_PREFIX}%`,
1233
+ getPlatformAgentConversationTitle({ definitionId: 'suggested_planner', mode: 'workflow' }),
1234
+ ];
1235
+ if (params?.userId) {
1236
+ filters.push(`COALESCE(spr.user_id, par.user_id, c.user_id) = ?`);
1237
+ args.push(params.userId);
1238
+ }
1239
+ if (params?.runIds?.length) {
1240
+ filters.push(`r.run_id IN (${params.runIds.map(() => '?').join(', ')})`);
1241
+ args.push(...params.runIds);
1242
+ }
1243
+ const rows = this.db.prepare(`SELECT r.run_id as runId,
1244
+ COALESCE(spr.user_id, par.user_id, c.user_id) as userId,
1245
+ r.session_key as sessionKey,
1246
+ c.id as conversationId,
1247
+ c.agent_type as agentType,
1248
+ c.node_id as nodeId,
1249
+ h.current_run_id as snapshotRunId,
1250
+ h.host_key as hostKey,
1251
+ COALESCE(spc.schedule_interval_ms, pai.schedule_interval_ms, ?) as scheduleIntervalMs
1252
+ FROM runs r
1253
+ LEFT JOIN suggested_planner_runs spr ON spr.run_id = r.run_id
1254
+ LEFT JOIN platform_agent_runs par ON par.run_id = r.run_id
1255
+ LEFT JOIN conversations c ON c.session_key = r.session_key
1256
+ LEFT JOIN node_host_snapshots h
1257
+ ON h.node_id = c.node_id
1258
+ AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
1259
+ AND h.state = 'active'
1260
+ AND h.current_run_id = r.run_id
1261
+ LEFT JOIN suggested_planner_configs spc ON spc.user_id = COALESCE(spr.user_id, par.user_id, c.user_id)
1262
+ LEFT JOIN platform_agent_instances pai
1263
+ ON pai.user_id = COALESCE(spr.user_id, par.user_id, c.user_id)
1264
+ AND pai.definition_id = 'suggested_planner'
1265
+ WHERE ${filters.join(' AND ')}
1266
+ ORDER BY r.started_at ASC`).all(DEFAULT_PLATFORM_AGENT_INTERVAL_MS, ...args);
1267
+ if (rows.length === 0)
1268
+ return [];
1269
+ for (const row of rows) {
1270
+ if (!row.conversationId)
1271
+ continue;
1272
+ this.conversationManager.cancelConversationRun(row.conversationId, row.runId, {
1273
+ strictExpectedRunId: true,
1274
+ });
1275
+ }
1276
+ const now = Date.now();
1277
+ const transaction = this.db.transaction(() => {
1278
+ for (const row of rows) {
1279
+ const nextRunAt = now + Math.max(5_000, row.scheduleIntervalMs ?? DEFAULT_PLATFORM_AGENT_INTERVAL_MS);
1280
+ this.db.prepare(`UPDATE runs
1281
+ SET ended_at = COALESCE(ended_at, ?),
1282
+ stop_reason = COALESCE(stop_reason, 'error'),
1283
+ error = COALESCE(error, ?)
1284
+ WHERE run_id = ?
1285
+ AND ended_at IS NULL`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
1286
+ this.db.prepare(`UPDATE platform_agent_runs
1287
+ SET status = 'failed',
1288
+ completed_at = COALESCE(completed_at, ?),
1289
+ error = COALESCE(error, ?)
1290
+ WHERE run_id = ?
1291
+ AND status = 'running'`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
1292
+ this.db.prepare(`UPDATE suggested_planner_runs
1293
+ SET status = 'failed',
1294
+ completed_at = COALESCE(completed_at, ?),
1295
+ error = COALESCE(error, ?)
1296
+ WHERE run_id = ?
1297
+ AND status = 'running'`).run(now, LEGACY_SUGGESTED_PLANNER_CLEANUP_ERROR, row.runId);
1298
+ if (row.userId) {
1299
+ this.db.prepare(`UPDATE suggested_planner_configs
1300
+ SET next_run_at = ?,
1301
+ updated_at = ?
1302
+ WHERE user_id = ?`).run(nextRunAt, now, row.userId);
1303
+ this.db.prepare(`UPDATE platform_agent_instances
1304
+ SET next_run_at = ?,
1305
+ updated_at = ?
1306
+ WHERE user_id = ?
1307
+ AND definition_id = 'suggested_planner'`).run(nextRunAt, now, row.userId);
1308
+ }
1309
+ this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.runId);
1310
+ if (row.conversationId) {
1311
+ this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
1312
+ this.db.prepare(`UPDATE conversations
1313
+ SET status = 'idle',
1314
+ updated_at = ?
1315
+ WHERE id = ?
1316
+ AND status IN ('queued', 'active', 'recovering', 'awaiting_approval')`).run(now, row.conversationId);
1317
+ }
1318
+ if (row.nodeId && row.hostKey) {
1319
+ this.db.prepare(`DELETE FROM node_host_snapshots
1320
+ WHERE node_id = ?
1321
+ AND host_key = ?
1322
+ AND current_run_id = ?`).run(row.nodeId, row.hostKey, row.runId);
1323
+ }
1324
+ }
1325
+ });
1326
+ transaction();
1327
+ return rows.map((row) => row.runId);
1328
+ }
1329
+ getUsernameForUserId(userId) {
1330
+ const row = this.db.prepare(`SELECT username FROM users WHERE id = ?`).get(userId);
1331
+ return row?.username?.trim() || null;
1332
+ }
1333
+ async syncBuiltinSuggestedPlannerInstance(params, override) {
1334
+ const config = await this.suggestedPlannerService.ensureConfigForUser(params);
1335
+ const definition = this.getDefinition('suggested_planner') ?? getBuiltinPlatformDefinition({ definitionId: 'suggested_planner' });
1336
+ const agent = this.suggestedPlannerService.getPlannerAgent(params.userId);
1337
+ const existing = this.getInstance(params.userId, 'suggested_planner');
1338
+ const defaultRuntimeConfig = {
1339
+ model: agent?.model ?? null,
1340
+ reasoningEffort: agent?.reasoningEffort ?? null,
1341
+ envVars: agent?.envVars ?? {},
1342
+ };
1343
+ if (agent?.agentRole !== 'platform_agent') {
1344
+ this.db.prepare(`UPDATE agents
1345
+ SET agent_role = 'platform_agent',
1346
+ description = ?,
1347
+ disabled_tool_kinds = ?,
1348
+ updated_at = ?
1349
+ WHERE agent_id = ?`).run(definition.description, JSON.stringify(normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy).disabledToolKinds ?? []), Date.now(), config.plannerAgentId);
1350
+ }
1351
+ const workflowConfig = override?.workflowConfig
1352
+ ?? existing?.workflowConfig
1353
+ ?? { lookbackDays: config.lookbackDays };
1354
+ const runtimeConfig = override?.runtimeConfig
1355
+ ?? existing?.runtimeConfig
1356
+ ?? defaultRuntimeConfig;
1357
+ const instance = this.upsertInstance({
1358
+ userId: params.userId,
1359
+ definitionId: 'suggested_planner',
1360
+ agentId: config.plannerAgentId,
1361
+ enabled: override?.enabled ?? existing?.enabled ?? config.enabled,
1362
+ scheduleIntervalMs: override?.scheduleIntervalMs ?? existing?.scheduleIntervalMs ?? config.scheduleIntervalMs,
1363
+ workflowConfig,
1364
+ runtimeConfig,
1365
+ lastRunAt: override && Object.prototype.hasOwnProperty.call(override, 'lastRunAt')
1366
+ ? override.lastRunAt ?? null
1367
+ : existing
1368
+ ? existing.lastRunAt
1369
+ : config.lastRunAt,
1370
+ nextRunAt: override && Object.prototype.hasOwnProperty.call(override, 'nextRunAt')
1371
+ ? override.nextRunAt ?? null
1372
+ : existing
1373
+ ? existing.nextRunAt
1374
+ : config.nextRunAt,
1375
+ createdAt: config.createdAt,
1376
+ updatedAt: Date.now(),
1377
+ });
1378
+ this.syncSuggestedPlannerLegacyMirror(instance);
1379
+ return instance;
1380
+ }
1381
+ async syncBuiltinLibraryCuratorInstance(params, override) {
1382
+ const config = await this.libraryService.ensureConfigForUser(params);
1383
+ const definition = this.getDefinition('library_curator') ?? getBuiltinPlatformDefinition({ definitionId: 'library_curator' });
1384
+ const agent = this.libraryService.getCuratorAgent(params.userId);
1385
+ const existing = this.getInstance(params.userId, 'library_curator');
1386
+ const defaultRuntimeConfig = {
1387
+ model: agent?.model ?? null,
1388
+ reasoningEffort: agent?.reasoningEffort ?? null,
1389
+ envVars: agent?.envVars ?? {},
1390
+ };
1391
+ if (agent?.agentRole !== 'platform_agent') {
1392
+ this.db.prepare(`UPDATE agents
1393
+ SET agent_role = 'platform_agent',
1394
+ description = ?,
1395
+ disabled_tool_kinds = ?,
1396
+ updated_at = ?
1397
+ WHERE agent_id = ?`).run(definition.description, JSON.stringify(normalizePlatformAgentToolPolicy(definition.workflowType, definition.toolPolicy).disabledToolKinds ?? []), Date.now(), config.curatorAgentId);
1398
+ }
1399
+ const workflowConfig = override?.workflowConfig
1400
+ ?? existing?.workflowConfig
1401
+ ?? {
1402
+ categories: config.categories,
1403
+ libraryRootPath: config.libraryRootPath,
1404
+ };
1405
+ const runtimeConfig = override?.runtimeConfig
1406
+ ?? existing?.runtimeConfig
1407
+ ?? defaultRuntimeConfig;
1408
+ const instance = this.upsertInstance({
1409
+ userId: params.userId,
1410
+ definitionId: 'library_curator',
1411
+ agentId: config.curatorAgentId,
1412
+ enabled: override?.enabled ?? existing?.enabled ?? config.enabled,
1413
+ scheduleIntervalMs: override?.scheduleIntervalMs ?? existing?.scheduleIntervalMs ?? config.scheduleIntervalMs,
1414
+ workflowConfig,
1415
+ runtimeConfig,
1416
+ lastRunAt: override && Object.prototype.hasOwnProperty.call(override, 'lastRunAt')
1417
+ ? override.lastRunAt ?? null
1418
+ : existing
1419
+ ? existing.lastRunAt
1420
+ : config.lastRunAt,
1421
+ nextRunAt: override && Object.prototype.hasOwnProperty.call(override, 'nextRunAt')
1422
+ ? override.nextRunAt ?? null
1423
+ : existing
1424
+ ? existing.nextRunAt
1425
+ : config.nextRunAt,
1426
+ createdAt: config.createdAt,
1427
+ updatedAt: Date.now(),
1428
+ });
1429
+ this.syncLibraryCuratorLegacyMirror(instance);
1430
+ return instance;
1431
+ }
1432
+ async updateBuiltinInstance(params, definition) {
1433
+ if (params.definitionId === 'suggested_planner') {
1434
+ const existing = await this.suggestedPlannerService.ensureConfigForUser(params);
1435
+ const existingInstance = this.getInstance(params.userId, 'suggested_planner');
1436
+ const workflowConfig = {
1437
+ lookbackDays: this.getPlannerLookbackDays(params.workflowConfig ?? existingInstance?.workflowConfig ?? { lookbackDays: existing.lookbackDays }),
1438
+ };
1439
+ const runtimeConfig = {
1440
+ ...(existingInstance?.runtimeConfig ?? { model: null, reasoningEffort: null, envVars: {} }),
1441
+ ...(params.runtimeConfig ?? {}),
1442
+ envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars')
1443
+ ? params.runtimeConfig?.envVars ?? {}
1444
+ : existingInstance?.runtimeConfig.envVars ?? {},
1445
+ };
1446
+ const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? existingInstance?.scheduleIntervalMs ?? existing.scheduleIntervalMs);
1447
+ const enabled = params.enabled ?? existingInstance?.enabled ?? existing.enabled;
1448
+ const now = Date.now();
1449
+ const nextRunAt = enabled
1450
+ ? existingInstance?.nextRunAt ?? existing.nextRunAt ?? now + scheduleIntervalMs
1451
+ : null;
1452
+ this.suggestedPlannerService.updateConfig(params.userId, {
1453
+ enabled,
1454
+ scheduleIntervalMs,
1455
+ lookbackDays: this.getPlannerLookbackDays(workflowConfig),
1456
+ model: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'model') ? params.runtimeConfig?.model ?? null : undefined,
1457
+ reasoningEffort: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'reasoningEffort') ? params.runtimeConfig?.reasoningEffort ?? null : undefined,
1458
+ envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars') ? params.runtimeConfig?.envVars ?? {} : undefined,
1459
+ });
1460
+ return this.syncBuiltinSuggestedPlannerInstance(params, {
1461
+ enabled,
1462
+ scheduleIntervalMs,
1463
+ workflowConfig,
1464
+ runtimeConfig,
1465
+ nextRunAt,
1466
+ lastRunAt: existingInstance?.lastRunAt ?? existing.lastRunAt,
1467
+ });
1468
+ }
1469
+ const existing = await this.libraryService.ensureConfigForUser(params);
1470
+ const existingInstance = this.getInstance(params.userId, 'library_curator');
1471
+ const workflowConfig = {
1472
+ categories: this.getCuratorCategories(params.workflowConfig ?? existingInstance?.workflowConfig ?? { categories: existing.categories }),
1473
+ libraryRootPath: typeof existingInstance?.workflowConfig.libraryRootPath === 'string'
1474
+ ? existingInstance.workflowConfig.libraryRootPath
1475
+ : existing.libraryRootPath,
1476
+ };
1477
+ const runtimeConfig = {
1478
+ ...(existingInstance?.runtimeConfig ?? { model: null, reasoningEffort: null, envVars: {} }),
1479
+ ...(params.runtimeConfig ?? {}),
1480
+ envVars: Object.prototype.hasOwnProperty.call(params.runtimeConfig ?? {}, 'envVars')
1481
+ ? params.runtimeConfig?.envVars ?? {}
1482
+ : existingInstance?.runtimeConfig.envVars ?? {},
1483
+ };
1484
+ const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? existingInstance?.scheduleIntervalMs ?? existing.scheduleIntervalMs);
1485
+ const enabled = params.enabled ?? existingInstance?.enabled ?? existing.enabled;
1486
+ const now = Date.now();
1487
+ const nextRunAt = enabled
1488
+ ? existingInstance?.nextRunAt ?? existing.nextRunAt ?? now + scheduleIntervalMs
1489
+ : null;
1490
+ this.libraryService.updateConfig(params.userId, {
1491
+ enabled,
1492
+ scheduleIntervalMs,
1493
+ categories: workflowConfig.categories,
1494
+ });
1495
+ if (params.runtimeConfig) {
1496
+ this.libraryService.updateCuratorAgentRuntime(params.userId, params.runtimeConfig);
1497
+ }
1498
+ return this.syncBuiltinLibraryCuratorInstance(params, {
1499
+ enabled,
1500
+ scheduleIntervalMs,
1501
+ workflowConfig,
1502
+ runtimeConfig,
1503
+ nextRunAt,
1504
+ lastRunAt: existingInstance?.lastRunAt ?? existing.lastRunAt,
1505
+ });
1506
+ }
1507
+ updateCustomInstance(params) {
1508
+ const now = Date.now();
1509
+ const workflowConfig = this.sanitizeWorkflowConfig(params.definition.workflowType, params.workflowConfig ?? params.instance.workflowConfig);
1510
+ const runtimeConfig = {
1511
+ ...params.instance.runtimeConfig,
1512
+ ...(params.runtimeConfig ?? {}),
1513
+ envVars: params.runtimeConfig?.envVars ? { ...params.runtimeConfig.envVars } : (params.instance.runtimeConfig.envVars ?? {}),
1514
+ };
1515
+ const enabled = params.enabled ?? params.instance.enabled;
1516
+ const scheduleIntervalMs = Math.max(60_000, params.scheduleIntervalMs ?? params.instance.scheduleIntervalMs);
1517
+ const nextRunAt = enabled ? params.instance.nextRunAt ?? now + scheduleIntervalMs : null;
1518
+ this.db.prepare(`UPDATE platform_agent_instances
1519
+ SET enabled = ?,
1520
+ schedule_interval_ms = ?,
1521
+ workflow_config_json = ?,
1522
+ runtime_config_json = ?,
1523
+ next_run_at = ?,
1524
+ updated_at = ?
1525
+ WHERE user_id = ? AND definition_id = ?`).run(enabled ? 1 : 0, scheduleIntervalMs, JSON.stringify(workflowConfig), JSON.stringify(runtimeConfig), nextRunAt, now, params.instance.userId, params.instance.definitionId);
1526
+ const toolPolicy = normalizePlatformAgentToolPolicy(params.definition.workflowType, params.definition.toolPolicy);
1527
+ this.db.prepare(`UPDATE agents
1528
+ SET name = ?,
1529
+ description = ?,
1530
+ model = ?,
1531
+ reasoning_effort = ?,
1532
+ env_vars = ?,
1533
+ disabled_tool_kinds = ?,
1534
+ updated_at = ?
1535
+ WHERE agent_id = ?`).run(params.definition.label, params.definition.description, runtimeConfig.model ?? null, runtimeConfig.reasoningEffort ?? null, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), now, params.instance.agentId);
1536
+ return this.getInstance(params.instance.userId, params.instance.definitionId);
1537
+ }
1538
+ async syncExistingCustomInstance(params) {
1539
+ const now = Date.now();
1540
+ const desiredWorkspacePath = this.buildCustomWorkspacePath(params.definition, params.username);
1541
+ fs.mkdirSync(desiredWorkspacePath, { recursive: true });
1542
+ const runtimeConfig = {
1543
+ ...params.existing.runtimeConfig,
1544
+ envVars: params.existing.runtimeConfig.envVars ?? {},
1545
+ };
1546
+ const toolPolicy = normalizePlatformAgentToolPolicy(params.definition.workflowType, params.definition.toolPolicy);
1547
+ const nodeId = this.pickNodeId(params.definition.workflowType);
1548
+ this.db.prepare(`UPDATE agents
1549
+ SET agent_role = 'platform_agent',
1550
+ name = ?,
1551
+ description = ?,
1552
+ node_id = ?,
1553
+ workspace_path = ?,
1554
+ model = ?,
1555
+ reasoning_effort = ?,
1556
+ env_vars = ?,
1557
+ disabled_tool_kinds = ?,
1558
+ updated_at = ?
1559
+ WHERE agent_id = ?`).run(params.definition.label, params.definition.description, nodeId, desiredWorkspacePath, runtimeConfig.model ?? null, runtimeConfig.reasoningEffort ?? null, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, JSON.stringify(toolPolicy.disabledToolKinds ?? []), now, params.existing.agentId);
1560
+ this.db.prepare(`UPDATE conversations
1561
+ SET node_id = ?,
1562
+ workspace_path = ?,
1563
+ env_vars = ?,
1564
+ updated_at = ?
1565
+ WHERE agent_id = ?`).run(nodeId, desiredWorkspacePath, Object.keys(runtimeConfig.envVars ?? {}).length > 0 ? JSON.stringify(runtimeConfig.envVars) : null, now, params.existing.agentId);
1566
+ return this.getInstance(params.userId, params.definition.definitionId) ?? params.existing;
1567
+ }
1568
+ upsertInstance(params) {
1569
+ this.db.prepare(`INSERT INTO platform_agent_instances(
1570
+ user_id, definition_id, agent_id, enabled, schedule_interval_ms,
1571
+ workflow_config_json, runtime_config_json, last_run_at, next_run_at,
1572
+ created_at, updated_at
1573
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1574
+ ON CONFLICT(user_id, definition_id) DO UPDATE SET
1575
+ agent_id = excluded.agent_id,
1576
+ enabled = excluded.enabled,
1577
+ schedule_interval_ms = excluded.schedule_interval_ms,
1578
+ workflow_config_json = excluded.workflow_config_json,
1579
+ runtime_config_json = excluded.runtime_config_json,
1580
+ last_run_at = excluded.last_run_at,
1581
+ next_run_at = excluded.next_run_at,
1582
+ updated_at = excluded.updated_at`).run(params.userId, params.definitionId, params.agentId, params.enabled ? 1 : 0, Math.max(60_000, params.scheduleIntervalMs), JSON.stringify(this.sanitizeWorkflowConfig(this.getDefinition(params.definitionId)?.workflowType ?? 'planner_suggestions', params.workflowConfig)), JSON.stringify({
1583
+ model: params.runtimeConfig.model ?? null,
1584
+ reasoningEffort: params.runtimeConfig.reasoningEffort ?? null,
1585
+ envVars: params.runtimeConfig.envVars ?? {},
1586
+ }), params.lastRunAt, params.nextRunAt, params.createdAt, params.updatedAt);
1587
+ return this.getInstance(params.userId, params.definitionId);
1588
+ }
1589
+ async syncInstancesForDefinition(definitionId) {
1590
+ const definition = this.getDefinition(definitionId);
1591
+ if (!definition)
1592
+ return;
1593
+ if (isBuiltinPlatformAgentDefinitionId(definitionId))
1594
+ return;
1595
+ const rows = this.db.prepare(`SELECT user_id as userId FROM platform_agent_instances WHERE definition_id = ? AND deleted_at IS NULL`).all(definitionId);
1596
+ for (const row of rows) {
1597
+ const username = this.getUsernameForUserId(row.userId);
1598
+ if (!username)
1599
+ continue;
1600
+ await this.ensureInstanceForUser({ userId: row.userId, username, definitionId });
1601
+ }
1602
+ }
1603
+ async reconcileRunStateAfterInsert(params) {
1604
+ const row = this.db.prepare(`SELECT r.stop_reason as stopReason,
1605
+ r.error as error,
1606
+ pad.workflow_type as workflowType,
1607
+ r.ended_at as endedAt
1608
+ FROM runs r
1609
+ JOIN platform_agent_definitions pad ON pad.definition_id = ?
1610
+ WHERE r.run_id = ?`).get(params.definitionId, params.runId);
1611
+ if (!row?.endedAt)
1612
+ return;
1613
+ await this.finalizeRun({
1614
+ runId: params.runId,
1615
+ error: getPlatformWorkflowTerminalError(row.workflowType, row.stopReason, row.error),
1616
+ });
1617
+ }
1618
+ async reconcileCompletedRunningRuns(userId, definitionId) {
1619
+ const filters = [`par.status = 'running'`, `r.ended_at IS NOT NULL`];
1620
+ const args = [];
1621
+ if (userId) {
1622
+ filters.push(`par.user_id = ?`);
1623
+ args.push(userId);
1624
+ }
1625
+ if (definitionId) {
1626
+ filters.push(`par.definition_id = ?`);
1627
+ args.push(definitionId);
1628
+ }
1629
+ const rows = this.db.prepare(`SELECT par.run_id as runId,
1630
+ par.user_id as userId,
1631
+ pad.workflow_type as workflowType,
1632
+ r.stop_reason as stopReason,
1633
+ r.error as error
1634
+ FROM platform_agent_runs par
1635
+ JOIN runs r ON r.run_id = par.run_id
1636
+ JOIN platform_agent_definitions pad ON pad.definition_id = par.definition_id
1637
+ WHERE ${filters.join(' AND ')}
1638
+ ORDER BY par.created_at ASC`).all(...args);
1639
+ for (const row of rows) {
1640
+ await this.finalizeRun({
1641
+ runId: row.runId,
1642
+ error: getPlatformWorkflowTerminalError(row.workflowType, row.stopReason, row.error),
1643
+ });
1644
+ }
1645
+ }
1646
+ reconcileTerminalPlatformRunRuntimeState(userId, definitionId) {
1647
+ const filters = [`par.status <> 'running'`, `r.ended_at IS NULL`];
1648
+ const args = [];
1649
+ if (userId) {
1650
+ filters.push(`par.user_id = ?`);
1651
+ args.push(userId);
1652
+ }
1653
+ if (definitionId) {
1654
+ filters.push(`par.definition_id = ?`);
1655
+ args.push(definitionId);
1656
+ }
1657
+ const rows = this.db.prepare(`SELECT par.run_id as runId,
1658
+ par.status as status,
1659
+ par.completed_at as completedAt,
1660
+ par.error as error,
1661
+ c.id as conversationId
1662
+ FROM platform_agent_runs par
1663
+ JOIN runs r ON r.run_id = par.run_id
1664
+ LEFT JOIN conversations c ON c.session_key = r.session_key
1665
+ WHERE ${filters.join(' AND ')}
1666
+ ORDER BY par.created_at ASC`).all(...args);
1667
+ if (rows.length === 0)
1668
+ return;
1669
+ const now = Date.now();
1670
+ const transaction = this.db.transaction(() => {
1671
+ for (const row of rows) {
1672
+ const endedAt = row.completedAt ?? now;
1673
+ const completed = row.status === 'completed';
1674
+ this.db.prepare(`UPDATE runs
1675
+ SET ended_at = COALESCE(ended_at, ?),
1676
+ stop_reason = COALESCE(stop_reason, ?),
1677
+ error = CASE
1678
+ WHEN ? IS NULL THEN error
1679
+ ELSE COALESCE(error, ?)
1680
+ END
1681
+ WHERE run_id = ?
1682
+ AND ended_at IS NULL`).run(endedAt, completed ? 'completed' : 'error', completed ? null : row.error ?? 'Platform agent run already ended.', completed ? null : row.error ?? 'Platform agent run already ended.', row.runId);
1683
+ this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.runId);
1684
+ this.db.prepare(`DELETE FROM node_host_snapshots WHERE current_run_id = ?`).run(row.runId);
1685
+ if (row.conversationId) {
1686
+ this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
1687
+ this.db.prepare(`UPDATE conversations
1688
+ SET status = 'idle',
1689
+ updated_at = ?
1690
+ WHERE id = ?
1691
+ AND status IN ('queued', 'active', 'recovering', 'awaiting_approval')`).run(now, row.conversationId);
1692
+ }
1693
+ }
1694
+ });
1695
+ transaction();
1696
+ }
1697
+ async finalizeLegacyCuratorRun(runId, userId, now) {
1698
+ await this.libraryService.finalizeOrganizeRun({ runId, userId });
1699
+ const row = this.db.prepare(`SELECT status,
1700
+ completed_at as completedAt,
1701
+ error
1702
+ FROM library_curator_runs
1703
+ WHERE run_id = ?`).get(runId);
1704
+ if (!row) {
1705
+ return {
1706
+ status: 'failed',
1707
+ completedAt: now,
1708
+ error: 'Legacy library curator run was not found during platform finalization.',
1709
+ generatedCount: 0,
1710
+ };
1711
+ }
1712
+ if (row.status === 'completed') {
1713
+ return {
1714
+ status: 'completed',
1715
+ completedAt: row.completedAt ?? now,
1716
+ error: null,
1717
+ generatedCount: 0,
1718
+ };
1719
+ }
1720
+ return {
1721
+ status: 'failed',
1722
+ completedAt: row.completedAt ?? now,
1723
+ error: row.error ?? 'Legacy library curator run did not complete successfully.',
1724
+ generatedCount: 0,
1725
+ };
1726
+ }
1727
+ buildCustomWorkspacePath(definition, username) {
1728
+ if (definition.definitionId === 'suggested_planner')
1729
+ return buildPlannerWorkspacePath(this.workspaceRoot, username);
1730
+ if (definition.definitionId === 'library_curator')
1731
+ return buildCuratorWorkspacePath(this.workspaceRoot, username);
1732
+ return buildGenericPlatformWorkspacePath(this.workspaceRoot, definition.definitionId, username);
1733
+ }
1734
+ buildDefaultRuntimeConfig(workflowType) {
1735
+ const envVars = workflowType === 'library_curate' ? buildCuratorEnvVars() : buildPlannerEnvVars();
1736
+ return {
1737
+ model: envVars.ANTHROPIC_MODEL ?? 'deepseek-v4-pro',
1738
+ reasoningEffort: null,
1739
+ envVars,
1740
+ };
1741
+ }
1742
+ pickNodeId(workflowType) {
1743
+ return workflowType === 'library_curate'
1744
+ ? pickLibraryNodeId(this.nodeRegistry)
1745
+ : pickPlannerNodeId(this.nodeRegistry);
1746
+ }
1747
+ getPlannerLookbackDays(workflowConfig) {
1748
+ const raw = workflowConfig.lookbackDays;
1749
+ const value = typeof raw === 'number' ? raw : Number(raw);
1750
+ if (!Number.isFinite(value))
1751
+ return DEFAULT_LOOKBACK_DAYS;
1752
+ return Math.max(1, Math.min(90, Math.round(value)));
1753
+ }
1754
+ getCuratorCategories(workflowConfig) {
1755
+ const raw = workflowConfig.categories;
1756
+ if (!Array.isArray(raw))
1757
+ return [];
1758
+ return raw.filter((item) => typeof item === 'string' && item.trim().length > 0).map((item) => item.trim());
1759
+ }
1760
+ sanitizeWorkflowConfig(workflowType, workflowConfig) {
1761
+ if (workflowType === 'planner_suggestions') {
1762
+ return { lookbackDays: this.getPlannerLookbackDays(workflowConfig) };
1763
+ }
1764
+ return { categories: this.getCuratorCategories(workflowConfig) };
1765
+ }
1766
+ async buildResourceSnapshot(params) {
1767
+ const snapshot = {};
1768
+ const lookbackDays = this.getPlannerLookbackDays(params.instance.workflowConfig);
1769
+ const resourceConfig = params.definition.resourceConfig ?? {};
1770
+ for (const scope of params.definition.resourceScopes) {
1771
+ if (scope === 'saved_inbox') {
1772
+ snapshot.savedInboxItems = this.libraryService.listInboxSavedItems(params.userId);
1773
+ continue;
1774
+ }
1775
+ if (scope === 'open_tasks') {
1776
+ snapshot.openTasks = this.listAccessibleOpenTasks(params.userId, params.username);
1777
+ continue;
1778
+ }
1779
+ if (scope === 'recent_messages') {
1780
+ snapshot.recentMessages = this.listRecentMessages(params.userId, params.username, lookbackDays, resourceConfig);
1781
+ continue;
1782
+ }
1783
+ if (scope === 'conversation_summaries') {
1784
+ snapshot.conversationSummaries = this.listConversationSummaries(params.userId, lookbackDays, resourceConfig);
1785
+ continue;
1786
+ }
1787
+ if (scope === 'agent_memory') {
1788
+ snapshot.agentMemory = await this.readAccessibleAgentMemory(params.userId, resourceConfig);
1789
+ continue;
1790
+ }
1791
+ if (scope === 'agent_notes') {
1792
+ snapshot.agentNotes = await this.readAccessibleAgentNotes(params.userId, resourceConfig);
1793
+ continue;
1794
+ }
1795
+ if (scope === 'personal_library_inventory') {
1796
+ snapshot.personalLibraryInventory = await this.libraryService.listDocuments(params.userId, undefined, 'personal');
1797
+ continue;
1798
+ }
1799
+ if (scope === 'personal_library_documents') {
1800
+ snapshot.personalLibraryDocuments = await this.readPersonalLibraryDocuments(params.userId, resourceConfig);
1801
+ }
1802
+ }
1803
+ return snapshot;
1804
+ }
1805
+ listAccessibleOpenTasks(userId, username) {
1806
+ const directTarget = `dm:@${username}`;
1807
+ return this.db.prepare(`SELECT t.task_id as taskId, t.channel_id as channelId, t.task_number as taskNumber,
1808
+ t.title, t.description, t.status, t.updated_at as updatedAt,
1809
+ t.claimed_by_name as assigneeName, c.name as channelName,
1810
+ CASE WHEN t.channel_id LIKE 'dm:%' THEN 'dm' ELSE 'channel' END as sourceType
1811
+ FROM tasks t
1812
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
1813
+ LEFT JOIN channels c ON c.channel_id = t.channel_id
1814
+ WHERE t.status != 'done'
1815
+ AND (
1816
+ (t.channel_id NOT LIKE 'dm:%' AND EXISTS (
1817
+ SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = t.channel_id AND uca.user_id = ?
1818
+ ))
1819
+ OR (t.channel_id LIKE 'dm:%' AND EXISTS (
1820
+ SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(t.channel_id, 4)
1821
+ ) AND COALESCE(NULLIF(t.dm_target, ''), cm.target) = ?)
1822
+ )
1823
+ ORDER BY t.updated_at DESC
1824
+ LIMIT 40`).all(userId, userId, directTarget);
1825
+ }
1826
+ listRecentMessages(userId, username, lookbackDays, resourceConfig) {
1827
+ const directTarget = `dm:@${username}`;
1828
+ const directThreadTargetGlob = `${directTarget}:*`;
1829
+ const since = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
1830
+ const limit = Math.max(5, Math.min(100, Number(resourceConfig.maxRecentMessages ?? 30) || 30));
1831
+ return this.db.prepare(`SELECT message_id as messageId, channel_id as channelId, sender_name as senderName,
1832
+ target, content, created_at as createdAt
1833
+ FROM channel_messages
1834
+ WHERE created_at >= ?
1835
+ AND (
1836
+ (channel_id NOT LIKE 'dm:%' AND EXISTS (
1837
+ SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = channel_messages.channel_id AND uca.user_id = ?
1838
+ ))
1839
+ OR (channel_id LIKE 'dm:%' AND EXISTS (
1840
+ SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(channel_id, 4)
1841
+ ) AND (target = ? OR target GLOB ?))
1842
+ )
1843
+ ORDER BY created_at DESC
1844
+ LIMIT ?`).all(since, userId, userId, directTarget, directThreadTargetGlob, limit);
1845
+ }
1846
+ listConversationSummaries(userId, lookbackDays, resourceConfig) {
1847
+ const since = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
1848
+ const limit = Math.max(5, Math.min(50, Number(resourceConfig.maxConversationSummaries ?? 20) || 20));
1849
+ const rows = this.db.prepare(`SELECT cs.conversation_id as conversationId, cs.reply_target as replyTarget,
1850
+ cs.summary_json as summaryJson, cs.updated_at as updatedAt
1851
+ FROM conversation_summaries cs
1852
+ JOIN conversations conv ON conv.id = cs.conversation_id
1853
+ WHERE conv.user_id = ?
1854
+ AND cs.updated_at >= ?
1855
+ ORDER BY cs.updated_at DESC
1856
+ LIMIT ?`).all(userId, since, limit);
1857
+ return rows.map((summary) => ({
1858
+ conversationId: summary.conversationId,
1859
+ replyTarget: summary.replyTarget,
1860
+ updatedAt: summary.updatedAt,
1861
+ summary: parseJsonRecord(summary.summaryJson),
1862
+ }));
1863
+ }
1864
+ async readAccessibleAgentMemory(userId, resourceConfig) {
1865
+ const agents = this.listAccessibleSnapshotAgents(userId, resourceConfig);
1866
+ const results = [];
1867
+ for (const agent of agents) {
1868
+ const content = await this.readWorkspaceTextFile(agent, 'MEMORY.md');
1869
+ if (!content)
1870
+ continue;
1871
+ results.push({
1872
+ agentId: agent.agentId,
1873
+ agentName: agent.name,
1874
+ content,
1875
+ });
1876
+ }
1877
+ return results;
1878
+ }
1879
+ async readAccessibleAgentNotes(userId, resourceConfig) {
1880
+ const agents = this.listAccessibleSnapshotAgents(userId, resourceConfig);
1881
+ const results = [];
1882
+ const maxNotesPerAgent = Math.max(1, Math.min(10, Number(resourceConfig.maxNotesPerAgent ?? 5) || 5));
1883
+ for (const agent of agents) {
1884
+ if (!agent.workspacePath?.trim())
1885
+ continue;
1886
+ try {
1887
+ const listing = agent.nodeId
1888
+ ? await this.workspaceBroker.listDirectory(agent.nodeId, agent.workspacePath, 'notes', { scaffold: false, agentName: agent.name, limit: 50 })
1889
+ : null;
1890
+ const files = (listing?.entries ?? [])
1891
+ .filter((entry) => entry.kind === 'file' && entry.path.toLowerCase().endsWith('.md'))
1892
+ .slice(0, maxNotesPerAgent);
1893
+ const notes = [];
1894
+ for (const file of files) {
1895
+ const content = await this.readWorkspaceTextFile(agent, file.path);
1896
+ if (!content)
1897
+ continue;
1898
+ notes.push({ relativePath: file.path, content });
1899
+ }
1900
+ if (notes.length > 0) {
1901
+ results.push({
1902
+ agentId: agent.agentId,
1903
+ agentName: agent.name,
1904
+ notes,
1905
+ });
1906
+ }
1907
+ }
1908
+ catch {
1909
+ continue;
1910
+ }
1911
+ }
1912
+ return results;
1913
+ }
1914
+ async readPersonalLibraryDocuments(userId, resourceConfig) {
1915
+ const docs = await this.libraryService.listDocuments(userId, undefined, 'personal');
1916
+ const maxDocuments = Math.max(1, Math.min(50, Number(resourceConfig.maxLibraryDocuments ?? 20) || 20));
1917
+ const selected = docs.slice(0, maxDocuments);
1918
+ const results = [];
1919
+ for (const doc of selected) {
1920
+ try {
1921
+ const file = await this.libraryService.readLibraryFile(userId, doc.relativePath, 'personal');
1922
+ results.push({
1923
+ title: doc.title,
1924
+ category: doc.category,
1925
+ topicPath: doc.topicPath,
1926
+ relativePath: doc.relativePath,
1927
+ summary: doc.summary,
1928
+ content: file.content,
1929
+ });
1930
+ }
1931
+ catch {
1932
+ continue;
1933
+ }
1934
+ }
1935
+ return results;
1936
+ }
1937
+ listAccessibleSnapshotAgents(userId, resourceConfig) {
1938
+ const requestedAgentIds = Array.isArray(resourceConfig.agentIds)
1939
+ ? resourceConfig.agentIds.filter((value) => typeof value === 'string' && value.trim().length > 0).map((value) => value.trim())
1940
+ : [];
1941
+ const rows = this.db.prepare(`SELECT a.agent_id as agentId
1942
+ FROM agents a
1943
+ JOIN user_agent_access uaa ON uaa.agent_id = a.agent_id
1944
+ WHERE uaa.user_id = ?
1945
+ AND a.deleted_at IS NULL
1946
+ ORDER BY a.updated_at DESC`).all(userId);
1947
+ const agents = rows
1948
+ .map((row) => this.conversationManager.getAgent(row.agentId))
1949
+ .filter((agent) => {
1950
+ if (!agent)
1951
+ return false;
1952
+ return agent.agentRole !== 'platform_agent' && agent.agentRole !== 'library_curator' && agent.agentRole !== 'suggested_planner';
1953
+ });
1954
+ if (requestedAgentIds.length > 0) {
1955
+ const allowed = new Set(requestedAgentIds);
1956
+ return agents.filter((agent) => allowed.has(agent.agentId));
1957
+ }
1958
+ return agents.slice(0, 6);
1959
+ }
1960
+ async readWorkspaceTextFile(agent, relativePath) {
1961
+ if (!agent.workspacePath?.trim())
1962
+ return null;
1963
+ try {
1964
+ if (agent.nodeId) {
1965
+ const result = await this.workspaceBroker.readFile(agent.nodeId, agent.workspacePath, relativePath, {
1966
+ scaffold: false,
1967
+ agentName: agent.name,
1968
+ });
1969
+ return result.content;
1970
+ }
1971
+ const absolutePath = path.join(agent.workspacePath, relativePath);
1972
+ return fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf8') : null;
1973
+ }
1974
+ catch {
1975
+ return null;
1976
+ }
1977
+ }
1978
+ resolveAccessiblePlannerTarget(userId, opportunity) {
1979
+ const agentId = opportunity.agentId?.trim() || null;
1980
+ const channelId = opportunity.channelId?.trim() || null;
1981
+ if (agentId && this.userCanAccessAgent(userId, agentId)) {
1982
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
1983
+ }
1984
+ if (channelId?.startsWith('dm:')) {
1985
+ const dmAgentId = channelId.slice(3).trim();
1986
+ if (dmAgentId && this.userCanAccessAgent(userId, dmAgentId)) {
1987
+ return { channelId, agentId: dmAgentId, targetType: 'dm' };
1988
+ }
1989
+ return { channelId: null, agentId: null, targetType: 'general' };
1990
+ }
1991
+ if (channelId && this.userCanAccessChannel(userId, channelId)) {
1992
+ return { channelId, agentId: null, targetType: 'channel' };
1993
+ }
1994
+ return { channelId: null, agentId: null, targetType: 'general' };
1995
+ }
1996
+ userCanAccessAgent(userId, agentId) {
1997
+ return Boolean(this.db.prepare(`SELECT 1
1998
+ FROM agents a
1999
+ JOIN user_agent_access uaa ON uaa.agent_id = a.agent_id
2000
+ WHERE a.agent_id = ?
2001
+ AND a.deleted_at IS NULL
2002
+ AND uaa.user_id = ?
2003
+ LIMIT 1`).get(agentId, userId));
2004
+ }
2005
+ userCanAccessChannel(userId, channelId) {
2006
+ return Boolean(this.db.prepare(`SELECT 1
2007
+ FROM channels c
2008
+ JOIN user_channel_access uca ON uca.channel_id = c.channel_id
2009
+ WHERE c.channel_id = ?
2010
+ AND uca.user_id = ?
2011
+ LIMIT 1`).get(channelId, userId));
2012
+ }
2013
+ getRunOutputText(runId) {
2014
+ const rows = this.db.prepare(`SELECT payload_json as payloadJson
2015
+ FROM events
2016
+ WHERE run_id = ?
2017
+ AND method = 'node/event'
2018
+ AND json_valid(payload_json)
2019
+ AND json_extract(payload_json, '$.type') IN ('content.delta', 'activity.delta')
2020
+ ORDER BY seq ASC`).all(runId);
2021
+ const byType = { 'content.delta': [], 'activity.delta': [] };
2022
+ for (const row of rows) {
2023
+ try {
2024
+ const payload = JSON.parse(row.payloadJson);
2025
+ if (typeof payload.type === 'string' && typeof payload.text === 'string') {
2026
+ byType[payload.type]?.push(payload.text);
2027
+ }
2028
+ }
2029
+ catch {
2030
+ continue;
2031
+ }
2032
+ }
2033
+ return byType['content.delta'].join('').trim() || byType['activity.delta'].join('').trim();
2034
+ }
2035
+ scheduleNextRun(userId, definitionId, now, scheduleIntervalMs) {
2036
+ this.db.prepare(`UPDATE platform_agent_instances
2037
+ SET next_run_at = ?,
2038
+ updated_at = ?
2039
+ WHERE user_id = ? AND definition_id = ?`).run(now + scheduleIntervalMs, now, userId, definitionId);
2040
+ }
2041
+ }