@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,868 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getPlatformAgentConversationTitle, getPlatformAgentDisabledToolKinds, getPlatformAgentLegacyConversationTitles, } from './platformAgentPolicy.js';
5
+ import { insertPlannerTaskCandidateOpportunity } from '../web/proactiveOpportunities.js';
6
+ export const SUGGESTED_PLANNER_AGENT_NAME = 'Suggested Planner';
7
+ export const DEFAULT_SUGGESTED_PLANNER_INTERVAL_MS = 6 * 60 * 60 * 1000;
8
+ const DEFAULT_LOOKBACK_DAYS = 14;
9
+ const PLANNER_WORKSPACE_DIRECTORY_NAME = '.suggested-planner';
10
+ const PLANNER_DISABLED_TOOL_KINDS_JSON = JSON.stringify(getPlatformAgentDisabledToolKinds('suggested_planner'));
11
+ function mapConfigRow(row) {
12
+ return {
13
+ userId: row.userId,
14
+ plannerAgentId: row.plannerAgentId,
15
+ nodeId: row.nodeId,
16
+ scheduleIntervalMs: row.scheduleIntervalMs,
17
+ lookbackDays: row.lookbackDays,
18
+ lastRunAt: row.lastRunAt,
19
+ nextRunAt: row.nextRunAt,
20
+ enabled: row.enabled === 1,
21
+ createdAt: row.createdAt,
22
+ updatedAt: row.updatedAt,
23
+ };
24
+ }
25
+ function safeUsername(username) {
26
+ return username.replace(/[^a-zA-Z0-9._-]+/g, '_').trim() || 'user';
27
+ }
28
+ function buildPlannerWorkspacePath(workspaceRoot, username) {
29
+ return path.join(workspaceRoot, PLANNER_WORKSPACE_DIRECTORY_NAME, safeUsername(username));
30
+ }
31
+ function buildPlannerEnvVars() {
32
+ const envVars = {};
33
+ const baseUrl = process.env.SUGGESTED_PLANNER_ANTHROPIC_BASE_URL?.trim()
34
+ || process.env.ANTHROPIC_BASE_URL?.trim();
35
+ const model = process.env.SUGGESTED_PLANNER_ANTHROPIC_MODEL?.trim()
36
+ || process.env.ANTHROPIC_MODEL?.trim()
37
+ || 'deepseek-v4-pro';
38
+ const authToken = process.env.SUGGESTED_PLANNER_ANTHROPIC_AUTH_TOKEN?.trim()
39
+ || process.env.SUGGESTED_PLANNER_ANTHROPIC_API_KEY?.trim()
40
+ || process.env.ANTHROPIC_AUTH_TOKEN?.trim()
41
+ || process.env.ANTHROPIC_API_KEY?.trim();
42
+ if (baseUrl)
43
+ envVars.ANTHROPIC_BASE_URL = baseUrl;
44
+ if (model)
45
+ envVars.ANTHROPIC_MODEL = model;
46
+ if (authToken)
47
+ envVars.ANTHROPIC_AUTH_TOKEN = authToken;
48
+ for (const key of [
49
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
50
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
51
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
52
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
53
+ 'CLAUDE_CODE_EFFORT_LEVEL',
54
+ ]) {
55
+ const value = process.env[key]?.trim();
56
+ if (value)
57
+ envVars[key] = value;
58
+ }
59
+ return envVars;
60
+ }
61
+ function pickPlannerNodeId(nodeRegistry) {
62
+ const localNode = nodeRegistry.getNode('local-node-1');
63
+ if (localNode?.agentTypes.includes('claude_sdk'))
64
+ return localNode.nodeId;
65
+ for (const node of nodeRegistry.listNodes()) {
66
+ const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
67
+ if (agentTypes.includes('claude_sdk') && !node.nodeId.startsWith('scenario-'))
68
+ return node.nodeId;
69
+ }
70
+ for (const node of nodeRegistry.listNodes()) {
71
+ const agentTypes = Array.isArray(node.agentTypes) ? node.agentTypes : [];
72
+ if (agentTypes.includes('claude_sdk'))
73
+ return node.nodeId;
74
+ }
75
+ return nodeRegistry.listNodes()[0]?.nodeId ?? null;
76
+ }
77
+ function normalizeText(value, limit = 500) {
78
+ return String(value ?? '').replace(/\s+/g, ' ').trim().slice(0, limit);
79
+ }
80
+ function parseJsonObject(raw) {
81
+ if (!raw)
82
+ return {};
83
+ try {
84
+ const parsed = JSON.parse(raw);
85
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
86
+ ? parsed
87
+ : {};
88
+ }
89
+ catch {
90
+ return {};
91
+ }
92
+ }
93
+ function parseJsonRecord(raw) {
94
+ if (!raw?.trim())
95
+ return null;
96
+ try {
97
+ const parsed = JSON.parse(raw);
98
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
99
+ ? parsed
100
+ : null;
101
+ }
102
+ catch {
103
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
104
+ if (fenced && fenced !== raw)
105
+ return parseJsonRecord(fenced);
106
+ const start = raw.indexOf('{');
107
+ const end = raw.lastIndexOf('}');
108
+ if (start >= 0 && end > start)
109
+ return parseJsonRecord(raw.slice(start, end + 1));
110
+ return null;
111
+ }
112
+ }
113
+ function normalizePlannerString(value, limit) {
114
+ return normalizeText(value, limit);
115
+ }
116
+ function normalizePriority(value) {
117
+ const normalized = String(value ?? '').trim().toLowerCase();
118
+ if (normalized === 'high' || normalized === 'medium' || normalized === 'low')
119
+ return normalized;
120
+ return 'medium';
121
+ }
122
+ function isSafeEvidenceUrl(url) {
123
+ if (url.startsWith('/') && !url.startsWith('//'))
124
+ return true;
125
+ try {
126
+ const parsed = new URL(url);
127
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ function normalizeEvidenceLinks(value) {
134
+ if (!Array.isArray(value))
135
+ return [];
136
+ return value.slice(0, 4)
137
+ .map((item) => {
138
+ if (!item || typeof item !== 'object')
139
+ return null;
140
+ const record = item;
141
+ const label = normalizePlannerString(record.label, 100);
142
+ const url = normalizePlannerString(record.url, 1000);
143
+ const summary = normalizePlannerString(record.summary, 240);
144
+ if (!label || !url)
145
+ return null;
146
+ if (!isSafeEvidenceUrl(url))
147
+ return null;
148
+ return { label, url, ...(summary ? { summary } : {}) };
149
+ })
150
+ .filter((item) => item != null);
151
+ }
152
+ function normalizePlannerTarget(value) {
153
+ if (!value || typeof value !== 'object')
154
+ return { channelId: null, agentId: null, targetType: 'general' };
155
+ const record = value;
156
+ const type = normalizePlannerString(record.type, 40).toLowerCase();
157
+ const agentId = normalizePlannerString(record.agentId ?? record.agent_id, 200) || null;
158
+ const channelId = normalizePlannerString(record.channelId ?? record.channel_id, 200) || null;
159
+ if (type === 'dm' && agentId)
160
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
161
+ if (type === 'channel' && channelId)
162
+ return { channelId, agentId: null, targetType: 'channel' };
163
+ if (channelId?.startsWith('dm:'))
164
+ return { channelId, agentId: agentId ?? channelId.slice(3), targetType: 'dm' };
165
+ if (channelId)
166
+ return { channelId, agentId: null, targetType: 'channel' };
167
+ if (agentId)
168
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
169
+ return { channelId: null, agentId: null, targetType: type || 'general' };
170
+ }
171
+ function plannerDedupeHash(value) {
172
+ return createHash('sha256').update(value).digest('hex').slice(0, 24);
173
+ }
174
+ export function parseSuggestedPlannerOutput(raw) {
175
+ const parsed = parseJsonRecord(raw);
176
+ if (!parsed)
177
+ return { opportunities: [], error: 'Suggested planner returned invalid JSON.' };
178
+ if (!Object.prototype.hasOwnProperty.call(parsed, 'opportunities')) {
179
+ return { opportunities: [], error: 'Suggested planner JSON is missing opportunities array.' };
180
+ }
181
+ if (!Array.isArray(parsed.opportunities)) {
182
+ return { opportunities: [], error: 'Suggested planner JSON has invalid opportunities array.' };
183
+ }
184
+ const rawOpportunities = parsed.opportunities;
185
+ if (rawOpportunities.length === 0)
186
+ return { opportunities: [] };
187
+ const opportunities = [];
188
+ for (const item of rawOpportunities.slice(0, 8)) {
189
+ if (!item || typeof item !== 'object')
190
+ continue;
191
+ const record = item;
192
+ const title = normalizePlannerString(record.title, 160);
193
+ const rationale = normalizePlannerString(record.rationale, 800);
194
+ const suggestedAction = normalizePlannerString(record.suggestedAction ?? record.suggested_action, 500);
195
+ if (!title || !rationale)
196
+ continue;
197
+ const target = normalizePlannerTarget(record.target);
198
+ opportunities.push({
199
+ title,
200
+ rationale,
201
+ suggestedAction: suggestedAction || title,
202
+ priority: normalizePriority(record.priority),
203
+ channelId: target.channelId,
204
+ agentId: target.agentId,
205
+ targetType: target.targetType,
206
+ evidenceLinks: normalizeEvidenceLinks(record.evidenceLinks ?? record.evidence_links),
207
+ });
208
+ }
209
+ if (opportunities.length === 0)
210
+ return { opportunities, error: 'Suggested planner returned no valid opportunities.' };
211
+ return { opportunities };
212
+ }
213
+ export class SuggestedPlannerService {
214
+ db;
215
+ conversationManager;
216
+ nodeRegistry;
217
+ workspaceRoot;
218
+ constructor(db, conversationManager, nodeRegistry, workspaceRoot) {
219
+ this.db = db;
220
+ this.conversationManager = conversationManager;
221
+ this.nodeRegistry = nodeRegistry;
222
+ this.workspaceRoot = workspaceRoot;
223
+ }
224
+ getConfig(userId) {
225
+ const row = this.db.prepare(`SELECT user_id as userId, planner_agent_id as plannerAgentId, node_id as nodeId,
226
+ schedule_interval_ms as scheduleIntervalMs, lookback_days as lookbackDays,
227
+ last_run_at as lastRunAt, next_run_at as nextRunAt, enabled,
228
+ created_at as createdAt, updated_at as updatedAt
229
+ FROM suggested_planner_configs
230
+ WHERE user_id = ?`).get(userId);
231
+ return row ? mapConfigRow(row) : null;
232
+ }
233
+ async ensureConfigForUser(params) {
234
+ const existing = this.getConfig(params.userId);
235
+ if (existing)
236
+ return this.syncExistingConfigForUser({ ...params, existing });
237
+ const now = Date.now();
238
+ const nodeId = pickPlannerNodeId(this.nodeRegistry);
239
+ const plannerAgentId = randomUUID();
240
+ const workspacePath = buildPlannerWorkspacePath(this.workspaceRoot, params.username);
241
+ const envVars = buildPlannerEnvVars();
242
+ fs.mkdirSync(workspacePath, { recursive: true });
243
+ this.db.prepare(`INSERT INTO agents(
244
+ agent_id, name, agent_type, agent_role, model, reasoning_effort, claude_permission_mode,
245
+ codex_mode, codex_service_tier, channel_id, system_prompt, description, memory,
246
+ env_vars, disabled_tool_kinds, node_id, workspace_path, project_path, skill_roots,
247
+ created_at, updated_at
248
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(plannerAgentId, SUGGESTED_PLANNER_AGENT_NAME, 'claude_sdk', 'platform_agent', envVars.ANTHROPIC_MODEL ?? 'deepseek-v4-pro', null, 'bypassPermissions', null, null, 'default', '', 'System workbench agent that proposes proactive next work from tasks and chat context.', '', Object.keys(envVars).length > 0 ? JSON.stringify(envVars) : null, PLANNER_DISABLED_TOOL_KINDS_JSON, nodeId, workspacePath, null, null, now, now);
249
+ this.db.prepare(`INSERT OR IGNORE INTO agent_channel_memberships(agent_id, channel_id, is_home, joined_at)
250
+ VALUES(?, 'default', 1, ?)`).run(plannerAgentId, now);
251
+ this.db.prepare(`INSERT OR IGNORE INTO user_agent_access(user_id, agent_id, granted_at) VALUES(?, ?, ?)`).run(params.userId, plannerAgentId, now);
252
+ this.db.prepare(`INSERT INTO suggested_planner_configs(
253
+ user_id, planner_agent_id, node_id, schedule_interval_ms, lookback_days,
254
+ last_run_at, next_run_at, enabled, created_at, updated_at
255
+ ) VALUES(?, ?, ?, ?, ?, NULL, ?, 1, ?, ?)`).run(params.userId, plannerAgentId, nodeId, DEFAULT_SUGGESTED_PLANNER_INTERVAL_MS, DEFAULT_LOOKBACK_DAYS, now + DEFAULT_SUGGESTED_PLANNER_INTERVAL_MS, now, now);
256
+ const created = this.getConfig(params.userId);
257
+ if (!created)
258
+ throw new Error('Failed to create suggested planner config');
259
+ this.syncPlatformAgentInstance(created);
260
+ return created;
261
+ }
262
+ syncExistingConfigForUser(params) {
263
+ const nodeId = this.resolvePlannerNodeId(params.existing.nodeId);
264
+ const workspacePath = buildPlannerWorkspacePath(this.workspaceRoot, params.username);
265
+ fs.mkdirSync(workspacePath, { recursive: true });
266
+ const agentRow = this.db.prepare(`SELECT system_prompt as systemPrompt, workspace_path as workspacePath, disabled_tool_kinds as disabledToolKindsJson FROM agents WHERE agent_id = ?`).get(params.existing.plannerAgentId);
267
+ const assignmentChanged = nodeId !== params.existing.nodeId
268
+ || path.resolve(agentRow?.workspacePath ?? '') !== path.resolve(workspacePath);
269
+ const hasPersistedSystemPrompt = Boolean(agentRow?.systemPrompt?.trim());
270
+ const disabledToolKindsChanged = agentRow?.disabledToolKindsJson !== PLANNER_DISABLED_TOOL_KINDS_JSON;
271
+ if ((hasPersistedSystemPrompt || disabledToolKindsChanged) && !assignmentChanged) {
272
+ this.db.prepare(`UPDATE agents
273
+ SET system_prompt = '',
274
+ disabled_tool_kinds = ?,
275
+ updated_at = ?
276
+ WHERE agent_id = ?`).run(PLANNER_DISABLED_TOOL_KINDS_JSON, Date.now(), params.existing.plannerAgentId);
277
+ }
278
+ if (assignmentChanged || disabledToolKindsChanged) {
279
+ const now = Date.now();
280
+ this.db.prepare(`UPDATE suggested_planner_configs
281
+ SET node_id = ?,
282
+ updated_at = ?
283
+ WHERE user_id = ?`).run(nodeId, now, params.userId);
284
+ this.db.prepare(`UPDATE agents
285
+ SET node_id = ?,
286
+ workspace_path = ?,
287
+ system_prompt = '',
288
+ disabled_tool_kinds = ?,
289
+ updated_at = ?
290
+ WHERE agent_id = ?`).run(nodeId, workspacePath, PLANNER_DISABLED_TOOL_KINDS_JSON, now, params.existing.plannerAgentId);
291
+ this.db.prepare(`UPDATE conversations
292
+ SET node_id = ?,
293
+ workspace_path = ?,
294
+ updated_at = ?
295
+ WHERE agent_id = ?`).run(nodeId, workspacePath, now, params.existing.plannerAgentId);
296
+ }
297
+ const synced = this.getConfig(params.userId) ?? params.existing;
298
+ this.syncPlatformAgentInstance(synced);
299
+ return synced;
300
+ }
301
+ resolvePlannerNodeId(currentNodeId) {
302
+ const normalizedCurrentNodeId = currentNodeId?.trim() || null;
303
+ const preferredNodeId = pickPlannerNodeId(this.nodeRegistry);
304
+ if (preferredNodeId === 'local-node-1' && normalizedCurrentNodeId !== preferredNodeId) {
305
+ return preferredNodeId;
306
+ }
307
+ if (normalizedCurrentNodeId) {
308
+ const currentNode = this.nodeRegistry.getNode(normalizedCurrentNodeId);
309
+ if (currentNode?.agentTypes.includes('claude_sdk') && !normalizedCurrentNodeId.startsWith('scenario-')) {
310
+ return normalizedCurrentNodeId;
311
+ }
312
+ }
313
+ return preferredNodeId ?? normalizedCurrentNodeId;
314
+ }
315
+ getPlannerAgent(userId) {
316
+ const config = this.getConfig(userId);
317
+ if (!config || typeof this.conversationManager.getAgent !== 'function')
318
+ return null;
319
+ return this.conversationManager.getAgent(config.plannerAgentId);
320
+ }
321
+ syncPlatformAgentInstance(config) {
322
+ const agent = this.getPlannerAgent(config.userId);
323
+ try {
324
+ this.db.prepare(`INSERT INTO platform_agent_instances(
325
+ user_id, definition_id, agent_id, enabled, schedule_interval_ms,
326
+ workflow_config_json, runtime_config_json, last_run_at, next_run_at,
327
+ created_at, updated_at
328
+ ) VALUES(?, 'suggested_planner', ?, ?, ?, ?, ?, ?, ?, ?, ?)
329
+ ON CONFLICT(user_id, definition_id) DO UPDATE SET
330
+ agent_id = excluded.agent_id,
331
+ last_run_at = COALESCE(platform_agent_instances.last_run_at, excluded.last_run_at),
332
+ updated_at = excluded.updated_at`).run(config.userId, config.plannerAgentId, config.enabled ? 1 : 0, Math.max(60_000, config.scheduleIntervalMs), JSON.stringify({ lookbackDays: config.lookbackDays }), JSON.stringify({
333
+ model: agent?.model ?? null,
334
+ reasoningEffort: agent?.reasoningEffort ?? null,
335
+ envVars: agent?.envVars ?? {},
336
+ }), config.lastRunAt, config.nextRunAt, config.createdAt, Date.now());
337
+ }
338
+ catch {
339
+ // Older tests and pre-v124 schemas may not have the generic platform-agent tables yet.
340
+ }
341
+ }
342
+ openPlannerConversation(userId, mode = 'debug') {
343
+ const config = this.getConfig(userId);
344
+ if (!config)
345
+ return null;
346
+ const title = getPlatformAgentConversationTitle({
347
+ definitionId: 'suggested_planner',
348
+ mode,
349
+ });
350
+ return this.conversationManager.openNamedAgentPrimaryThread?.({
351
+ agentId: config.plannerAgentId,
352
+ title,
353
+ userId,
354
+ legacyTitles: mode === 'debug'
355
+ ? getPlatformAgentLegacyConversationTitles('suggested_planner')
356
+ : undefined,
357
+ }) ?? this.conversationManager.openAgentThread(config.plannerAgentId, userId);
358
+ }
359
+ updateConfig(userId, patch) {
360
+ const existing = this.getConfig(userId);
361
+ if (!existing)
362
+ throw new Error('Suggested planner config not found');
363
+ const now = Date.now();
364
+ const scheduleIntervalMs = Math.max(60_000, patch.scheduleIntervalMs ?? existing.scheduleIntervalMs);
365
+ const lookbackDays = Math.max(1, Math.min(90, Math.round(patch.lookbackDays ?? existing.lookbackDays)));
366
+ const enabled = patch.enabled ?? existing.enabled;
367
+ const nextRunAt = existing.nextRunAt ?? (now + scheduleIntervalMs);
368
+ this.db.prepare(`UPDATE suggested_planner_configs
369
+ SET schedule_interval_ms = ?,
370
+ lookback_days = ?,
371
+ enabled = ?,
372
+ next_run_at = ?,
373
+ updated_at = ?
374
+ WHERE user_id = ?`).run(scheduleIntervalMs, lookbackDays, enabled ? 1 : 0, nextRunAt, now, userId);
375
+ if (Object.prototype.hasOwnProperty.call(patch, 'model')
376
+ || Object.prototype.hasOwnProperty.call(patch, 'reasoningEffort')
377
+ || Object.prototype.hasOwnProperty.call(patch, 'envVars')) {
378
+ this.updatePlannerAgentRuntime(userId, patch);
379
+ }
380
+ const updated = this.getConfig(userId);
381
+ if (!updated)
382
+ throw new Error('Suggested planner config not found after update');
383
+ this.syncPlatformAgentInstance(updated);
384
+ return updated;
385
+ }
386
+ updatePlannerAgentRuntime(userId, patch) {
387
+ const config = this.getConfig(userId);
388
+ if (!config)
389
+ return null;
390
+ const existing = this.conversationManager.getAgent(config.plannerAgentId);
391
+ if (!existing)
392
+ return null;
393
+ const nextModel = Object.prototype.hasOwnProperty.call(patch, 'model')
394
+ ? patch.model?.trim() || null
395
+ : existing.model ?? null;
396
+ const nextReasoning = Object.prototype.hasOwnProperty.call(patch, 'reasoningEffort')
397
+ ? patch.reasoningEffort?.trim() || null
398
+ : existing.reasoningEffort ?? null;
399
+ const nextEnvVars = Object.prototype.hasOwnProperty.call(patch, 'envVars')
400
+ ? patch.envVars ?? {}
401
+ : existing.envVars ?? {};
402
+ const envVarsJson = Object.keys(nextEnvVars).length > 0 ? JSON.stringify(nextEnvVars) : null;
403
+ const now = Date.now();
404
+ this.db.prepare(`UPDATE agents
405
+ SET model = ?,
406
+ reasoning_effort = ?,
407
+ env_vars = ?,
408
+ disabled_tool_kinds = ?,
409
+ updated_at = ?
410
+ WHERE agent_id = ?`).run(nextModel, nextReasoning, envVarsJson, PLANNER_DISABLED_TOOL_KINDS_JSON, now, config.plannerAgentId);
411
+ this.db.prepare(`UPDATE conversations
412
+ SET env_vars = ?,
413
+ updated_at = ?
414
+ WHERE agent_id = ?`).run(envVarsJson, now, config.plannerAgentId);
415
+ const updatedAgent = this.conversationManager.getAgent(config.plannerAgentId);
416
+ this.syncPlatformAgentInstance(this.getConfig(userId) ?? config);
417
+ return updatedAgent;
418
+ }
419
+ listDueConfigs(now) {
420
+ const rows = this.db.prepare(`SELECT user_id as userId, planner_agent_id as plannerAgentId, node_id as nodeId,
421
+ schedule_interval_ms as scheduleIntervalMs, lookback_days as lookbackDays,
422
+ last_run_at as lastRunAt, next_run_at as nextRunAt, enabled,
423
+ created_at as createdAt, updated_at as updatedAt
424
+ FROM suggested_planner_configs
425
+ WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= ?`).all(now);
426
+ return rows.map(mapConfigRow);
427
+ }
428
+ getUsernameForUserId(userId) {
429
+ const row = this.db.prepare(`SELECT username FROM users WHERE id = ?`).get(userId);
430
+ return row?.username?.trim() || null;
431
+ }
432
+ getLastRun(userId) {
433
+ this.reconcileCompletedRunningRuns(userId);
434
+ this.unblockRunningPlannerHost(userId);
435
+ const row = this.db.prepare(`SELECT run_id as runId, status, created_at as createdAt, completed_at as completedAt, error,
436
+ generated_count as generatedCount
437
+ FROM suggested_planner_runs
438
+ WHERE user_id = ?
439
+ ORDER BY created_at DESC
440
+ LIMIT 1`).get(userId);
441
+ return row ?? null;
442
+ }
443
+ async dispatchPlanRun(params) {
444
+ const config = await this.ensureConfigForUser({ userId: params.userId, username: params.username });
445
+ if (!config.enabled && !params.manual) {
446
+ return { ok: true, skipped: true, reason: 'Suggested planner is disabled' };
447
+ }
448
+ this.reconcileCompletedRunningRuns(params.userId);
449
+ this.unblockRunningPlannerHost(params.userId);
450
+ const running = this.db.prepare(`SELECT run_id as runId FROM suggested_planner_runs
451
+ WHERE user_id = ? AND status = 'running'
452
+ ORDER BY created_at DESC LIMIT 1`).get(params.userId);
453
+ if (running) {
454
+ return { ok: true, skipped: true, queued: true, runId: running.runId, reason: 'Suggested planner run already in progress' };
455
+ }
456
+ this.clearOrphanActivePlannerConversation({
457
+ userId: params.userId,
458
+ plannerAgentId: config.plannerAgentId,
459
+ });
460
+ const snapshot = this.buildSnapshot(params.userId, params.username, config);
461
+ const conversation = this.openPlannerConversation(params.userId, 'workflow');
462
+ if (!conversation)
463
+ return { ok: false, error: 'Failed to open planner conversation' };
464
+ const prompt = this.buildPlannerPrompt({ config, snapshot });
465
+ const activationMetadata = {
466
+ platformRun: {
467
+ definitionId: 'suggested_planner',
468
+ workflowType: 'planner_suggestions',
469
+ mode: 'workflow',
470
+ },
471
+ suppressReplyContract: true,
472
+ };
473
+ const result = await this.conversationManager.submitPrompt(conversation.id, prompt, {
474
+ recordAsUserMessage: false,
475
+ activationMetadata,
476
+ suppressReplyContract: true,
477
+ });
478
+ const now = Date.now();
479
+ if (!result.runId) {
480
+ this.scheduleNextRun(params.userId, now);
481
+ return { ok: false, error: 'Failed to start suggested planner run', queued: result.queued, generatedCount: 0 };
482
+ }
483
+ this.db.prepare(`INSERT INTO suggested_planner_runs(run_id, user_id, status, created_at, generated_count)
484
+ VALUES(?, ?, 'running', ?, ?)`).run(result.runId, params.userId, now, 0);
485
+ this.db.prepare(`UPDATE suggested_planner_configs SET last_run_at = ?, updated_at = ? WHERE user_id = ?`).run(now, now, params.userId);
486
+ await this.reconcileRunStateAfterInsert(result.runId, params.userId);
487
+ return { ok: true, runId: result.runId, queued: result.queued, generatedCount: 0 };
488
+ }
489
+ async finalizePlanRun(params) {
490
+ this.finalizePlanRunNow(params);
491
+ }
492
+ finalizePlanRunNow(params) {
493
+ const row = this.db.prepare(`SELECT run_id as runId, status FROM suggested_planner_runs WHERE run_id = ?`).get(params.runId);
494
+ if (!row || row.status !== 'running')
495
+ return;
496
+ const now = Date.now();
497
+ let error = params.error;
498
+ let generatedCount = 0;
499
+ if (!error) {
500
+ const ingestResult = this.ingestPlannerRunOutput({
501
+ userId: params.userId,
502
+ runId: params.runId,
503
+ now,
504
+ });
505
+ generatedCount = ingestResult.generatedCount;
506
+ error = ingestResult.error;
507
+ }
508
+ this.db.prepare(`UPDATE suggested_planner_runs
509
+ SET status = ?,
510
+ completed_at = ?,
511
+ error = ?,
512
+ generated_count = ?
513
+ WHERE run_id = ?`).run(error ? 'failed' : 'completed', now, error ?? null, generatedCount, params.runId);
514
+ this.scheduleNextRun(params.userId, now);
515
+ }
516
+ reconcileCompletedRunningRuns(userId) {
517
+ const rows = this.db.prepare(`SELECT spr.run_id as runId,
518
+ spr.user_id as userId,
519
+ r.stop_reason as stopReason,
520
+ r.error as error
521
+ FROM suggested_planner_runs spr
522
+ JOIN runs r ON r.run_id = spr.run_id
523
+ WHERE spr.status = 'running'
524
+ AND r.ended_at IS NOT NULL
525
+ ${userId ? 'AND spr.user_id = ?' : ''}`).all(...(userId ? [userId] : []));
526
+ for (const row of rows) {
527
+ this.finalizePlanRunNow({
528
+ runId: row.runId,
529
+ userId: row.userId,
530
+ error: getSuggestedPlannerTerminalError(row.stopReason, row.error),
531
+ });
532
+ }
533
+ }
534
+ clearOrphanActivePlannerConversation(params) {
535
+ const workflowTitle = getPlatformAgentConversationTitle({
536
+ definitionId: 'suggested_planner',
537
+ mode: 'workflow',
538
+ });
539
+ const rows = this.db.prepare(`SELECT c.id as conversationId,
540
+ c.session_key as sessionKey,
541
+ c.agent_type as agentType,
542
+ r.run_id as runId,
543
+ h.current_run_id as snapshotRunId
544
+ FROM conversations c
545
+ LEFT JOIN runs r ON r.session_key = c.session_key AND r.ended_at IS NULL
546
+ LEFT JOIN node_host_snapshots h
547
+ ON h.node_id = c.node_id
548
+ AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
549
+ AND h.state = 'active'
550
+ AND h.current_run_id IS NOT NULL
551
+ WHERE c.agent_id = ?
552
+ AND c.user_id = ?
553
+ AND c.title = ?
554
+ AND c.status IN ('queued', 'active', 'recovering', 'awaiting_approval')
555
+ AND NOT EXISTS (
556
+ SELECT 1
557
+ FROM suggested_planner_runs spr
558
+ WHERE spr.run_id = COALESCE(h.current_run_id, r.run_id)
559
+ AND spr.status = 'running'
560
+ )`).all(params.plannerAgentId, params.userId, workflowTitle);
561
+ if (rows.length === 0)
562
+ return;
563
+ for (const row of rows) {
564
+ this.conversationManager.cancelConversationRun(row.conversationId, row.snapshotRunId ?? row.runId ?? undefined);
565
+ }
566
+ const now = Date.now();
567
+ const transaction = this.db.transaction(() => {
568
+ for (const row of rows) {
569
+ if (row.snapshotRunId) {
570
+ this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.snapshotRunId);
571
+ this.db.prepare(`UPDATE runs
572
+ SET ended_at = COALESCE(ended_at, ?),
573
+ error = COALESCE(error, ?)
574
+ WHERE run_id = ?
575
+ AND ended_at IS NULL`).run(now, 'Superseded by a new Suggested Planner run', row.snapshotRunId);
576
+ }
577
+ else if (row.sessionKey) {
578
+ this.db.prepare(`DELETE FROM node_dispatch_queue WHERE conversation_id = ?`).run(row.conversationId);
579
+ this.db.prepare(`UPDATE runs
580
+ SET ended_at = COALESCE(ended_at, ?),
581
+ error = COALESCE(error, ?)
582
+ WHERE session_key = ?
583
+ AND ended_at IS NULL`).run(now, 'Superseded by a new Suggested Planner run', row.sessionKey);
584
+ }
585
+ this.db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
586
+ this.db.prepare(`DELETE FROM node_host_snapshots
587
+ WHERE host_key = ?`).run(`conversation:${row.conversationId}:${row.agentType}`);
588
+ this.db.prepare(`UPDATE conversations
589
+ SET status = 'idle',
590
+ updated_at = ?
591
+ WHERE id = ?`).run(now, row.conversationId);
592
+ }
593
+ });
594
+ transaction();
595
+ }
596
+ unblockRunningPlannerHost(userId) {
597
+ const rows = this.db.prepare(`SELECT c.id as conversationId,
598
+ h.current_run_id as snapshotRunId,
599
+ h.host_key as hostKey,
600
+ h.node_id as nodeId
601
+ FROM suggested_planner_runs spr
602
+ JOIN runs running_run ON running_run.run_id = spr.run_id
603
+ JOIN conversations c ON c.session_key = running_run.session_key
604
+ JOIN node_host_snapshots h
605
+ ON h.node_id = c.node_id
606
+ AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
607
+ AND h.state = 'active'
608
+ AND h.current_run_id IS NOT NULL
609
+ LEFT JOIN suggested_planner_runs current_spr
610
+ ON current_spr.run_id = h.current_run_id
611
+ AND current_spr.status = 'running'
612
+ WHERE spr.user_id = ?
613
+ AND spr.status = 'running'
614
+ AND h.current_run_id != spr.run_id
615
+ AND current_spr.run_id IS NULL`).all(userId);
616
+ if (rows.length === 0)
617
+ return;
618
+ const now = Date.now();
619
+ const transaction = this.db.transaction(() => {
620
+ for (const row of rows) {
621
+ this.conversationManager.cancelConversationRun(row.conversationId, row.snapshotRunId);
622
+ this.db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(row.snapshotRunId);
623
+ this.db.prepare(`UPDATE runs
624
+ SET ended_at = COALESCE(ended_at, ?),
625
+ error = COALESCE(error, ?)
626
+ WHERE run_id = ?
627
+ AND ended_at IS NULL`).run(now, 'Superseded by a newer Suggested Planner run', row.snapshotRunId);
628
+ this.db.prepare(`DELETE FROM node_host_snapshots
629
+ WHERE node_id = ?
630
+ AND host_key = ?
631
+ AND current_run_id = ?`).run(row.nodeId, row.hostKey, row.snapshotRunId);
632
+ }
633
+ });
634
+ transaction();
635
+ }
636
+ async reconcileRunStateAfterInsert(runId, userId) {
637
+ const runRow = this.db.prepare(`SELECT ended_at as endedAt, stop_reason as stopReason, error
638
+ FROM runs
639
+ WHERE run_id = ?`).get(runId);
640
+ if (!runRow?.endedAt)
641
+ return;
642
+ this.finalizePlanRunNow({
643
+ runId,
644
+ userId,
645
+ error: getSuggestedPlannerTerminalError(runRow.stopReason, runRow.error),
646
+ });
647
+ }
648
+ scheduleNextRun(userId, now) {
649
+ const config = this.getConfig(userId);
650
+ if (!config)
651
+ return;
652
+ this.db.prepare(`UPDATE suggested_planner_configs SET next_run_at = ?, updated_at = ? WHERE user_id = ?`).run(now + config.scheduleIntervalMs, now, userId);
653
+ }
654
+ buildSnapshot(userId, username, config) {
655
+ const since = Date.now() - config.lookbackDays * 24 * 60 * 60 * 1000;
656
+ const directTarget = `dm:@${username}`;
657
+ const directThreadTargetGlob = `${directTarget}:*`;
658
+ const tasks = this.db.prepare(`SELECT t.task_id as taskId, t.channel_id as channelId, t.task_number as taskNumber,
659
+ t.title, t.description, t.status, t.updated_at as updatedAt,
660
+ t.claimed_by_name as assigneeName, c.name as channelName, a.name as agentName,
661
+ CASE WHEN t.channel_id LIKE 'dm:%' THEN 'dm' ELSE 'channel' END as sourceType
662
+ FROM tasks t
663
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
664
+ LEFT JOIN channels c ON c.channel_id = t.channel_id
665
+ LEFT JOIN agents a ON a.agent_id = substr(t.channel_id, 4)
666
+ WHERE t.status != 'done'
667
+ AND (
668
+ (t.channel_id NOT LIKE 'dm:%' AND EXISTS (
669
+ SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = t.channel_id AND uca.user_id = ?
670
+ ))
671
+ OR (t.channel_id LIKE 'dm:%' AND EXISTS (
672
+ SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(t.channel_id, 4)
673
+ ) AND COALESCE(NULLIF(t.dm_target, ''), cm.target) = ?)
674
+ )
675
+ ORDER BY t.updated_at DESC
676
+ LIMIT 40`).all(userId, userId, directTarget);
677
+ const saved = this.db.prepare(`SELECT id, sender_name as senderName, content, context_url as contextUrl, source_kind as sourceKind, created_at as createdAt
678
+ FROM saved_items
679
+ WHERE user_id = ? AND process_status = 'inbox'
680
+ ORDER BY created_at DESC
681
+ LIMIT 20`).all(userId);
682
+ const summaries = this.db.prepare(`SELECT cs.conversation_id as conversationId, cs.reply_target as replyTarget,
683
+ cs.summary_json as summaryJson, cs.updated_at as updatedAt
684
+ FROM conversation_summaries cs
685
+ JOIN conversations conv ON conv.id = cs.conversation_id
686
+ WHERE conv.user_id = ?
687
+ AND cs.updated_at >= ?
688
+ ORDER BY cs.updated_at DESC
689
+ LIMIT 20`).all(userId, since);
690
+ const recentMessages = this.db.prepare(`SELECT message_id as messageId, channel_id as channelId, sender_name as senderName,
691
+ target, content, created_at as createdAt
692
+ FROM channel_messages
693
+ WHERE created_at >= ?
694
+ AND (
695
+ (channel_id NOT LIKE 'dm:%' AND EXISTS (
696
+ SELECT 1 FROM user_channel_access uca WHERE uca.channel_id = channel_messages.channel_id AND uca.user_id = ?
697
+ ))
698
+ OR (channel_id LIKE 'dm:%' AND EXISTS (
699
+ SELECT 1 FROM user_agent_access uaa WHERE uaa.user_id = ? AND uaa.agent_id = substr(channel_id, 4)
700
+ ) AND (target = ? OR target GLOB ?))
701
+ )
702
+ ORDER BY created_at DESC
703
+ LIMIT 30`).all(since, userId, userId, directTarget, directThreadTargetGlob);
704
+ const mappedSummaries = summaries.map((summary) => ({
705
+ conversationId: summary.conversationId,
706
+ replyTarget: summary.replyTarget,
707
+ updatedAt: summary.updatedAt,
708
+ summary: parseJsonObject(String(summary.summaryJson ?? '')),
709
+ }));
710
+ return { tasks, saved, summaries: mappedSummaries, recentMessages };
711
+ }
712
+ getPlannerRunOutputText(runId) {
713
+ const rows = this.db.prepare(`SELECT payload_json as payloadJson
714
+ FROM events
715
+ WHERE run_id = ?
716
+ AND method = 'node/event'
717
+ AND json_valid(payload_json)
718
+ AND json_extract(payload_json, '$.type') IN ('content.delta', 'activity.delta')
719
+ ORDER BY seq ASC`).all(runId);
720
+ const byType = { 'content.delta': [], 'activity.delta': [] };
721
+ for (const row of rows) {
722
+ try {
723
+ const payload = JSON.parse(row.payloadJson);
724
+ if (typeof payload.type === 'string' && typeof payload.text === 'string') {
725
+ byType[payload.type]?.push(payload.text);
726
+ }
727
+ }
728
+ catch {
729
+ // Ignore malformed historical rows; invalid output is handled by the parser.
730
+ }
731
+ }
732
+ const contentText = byType['content.delta'].join('').trim();
733
+ return contentText || byType['activity.delta'].join('').trim();
734
+ }
735
+ ingestPlannerRunOutput(params) {
736
+ const outputText = this.getPlannerRunOutputText(params.runId);
737
+ const parsed = parseSuggestedPlannerOutput(outputText);
738
+ if (parsed.error)
739
+ return { generatedCount: 0, error: parsed.error };
740
+ let generatedCount = 0;
741
+ const seenKeys = new Set();
742
+ for (const opportunity of parsed.opportunities) {
743
+ const target = this.resolveAccessiblePlannerTarget(params.userId, opportunity);
744
+ const dedupePayload = [
745
+ opportunity.title.toLowerCase(),
746
+ target.targetType,
747
+ target.channelId ?? '',
748
+ target.agentId ?? '',
749
+ opportunity.evidenceLinks.map((link) => link.url).join('|'),
750
+ ].join('\n');
751
+ const dedupeKey = `planner:v2:${plannerDedupeHash(dedupePayload)}`;
752
+ if (seenKeys.has(dedupeKey))
753
+ continue;
754
+ seenKeys.add(dedupeKey);
755
+ insertPlannerTaskCandidateOpportunity(this.db, {
756
+ userId: params.userId,
757
+ dedupeKey,
758
+ title: opportunity.title,
759
+ rationale: opportunity.rationale,
760
+ channelId: target.channelId,
761
+ agentId: target.agentId,
762
+ evidence: {
763
+ source: 'suggested_planner',
764
+ plannerRunId: params.runId,
765
+ priority: opportunity.priority,
766
+ targetType: target.targetType,
767
+ channelId: target.channelId,
768
+ agentId: target.agentId,
769
+ requestedTargetType: opportunity.targetType,
770
+ requestedChannelId: opportunity.channelId,
771
+ requestedAgentId: opportunity.agentId,
772
+ suggestedTitle: opportunity.title,
773
+ suggestedDescription: opportunity.suggestedAction || opportunity.rationale,
774
+ suggestedAction: opportunity.suggestedAction,
775
+ },
776
+ evidenceLinks: opportunity.evidenceLinks,
777
+ now: params.now,
778
+ });
779
+ generatedCount += 1;
780
+ }
781
+ return { generatedCount };
782
+ }
783
+ resolveAccessiblePlannerTarget(userId, opportunity) {
784
+ const agentId = opportunity.agentId?.trim() || null;
785
+ const channelId = opportunity.channelId?.trim() || null;
786
+ if (agentId && this.userCanAccessAgent(userId, agentId)) {
787
+ return { channelId: `dm:${agentId}`, agentId, targetType: 'dm' };
788
+ }
789
+ if (channelId?.startsWith('dm:')) {
790
+ const dmAgentId = channelId.slice(3).trim();
791
+ if (dmAgentId && this.userCanAccessAgent(userId, dmAgentId)) {
792
+ return { channelId, agentId: dmAgentId, targetType: 'dm' };
793
+ }
794
+ return { channelId: null, agentId: null, targetType: 'general' };
795
+ }
796
+ if (channelId && this.userCanAccessChannel(userId, channelId)) {
797
+ return { channelId, agentId: null, targetType: 'channel' };
798
+ }
799
+ return { channelId: null, agentId: null, targetType: 'general' };
800
+ }
801
+ userCanAccessAgent(userId, agentId) {
802
+ return Boolean(this.db.prepare(`SELECT 1
803
+ FROM agents a
804
+ JOIN user_agent_access uaa ON uaa.agent_id = a.agent_id
805
+ WHERE a.agent_id = ?
806
+ AND a.deleted_at IS NULL
807
+ AND uaa.user_id = ?
808
+ LIMIT 1`).get(agentId, userId));
809
+ }
810
+ userCanAccessChannel(userId, channelId) {
811
+ return Boolean(this.db.prepare(`SELECT 1
812
+ FROM channels c
813
+ JOIN user_channel_access uca ON uca.channel_id = c.channel_id
814
+ WHERE c.channel_id = ?
815
+ AND uca.user_id = ?
816
+ LIMIT 1`).get(channelId, userId));
817
+ }
818
+ buildPlannerPrompt(params) {
819
+ return [
820
+ '# Suggested Planner run',
821
+ '',
822
+ `Lookback days: ${params.config.lookbackDays}`,
823
+ '',
824
+ 'Review the JSON snapshot and propose concrete next work. The platform will only surface suggestions for user confirmation.',
825
+ 'Do not execute the work. Do not create tasks yourself.',
826
+ '',
827
+ 'Return only a JSON object. Do not include markdown, comments, or prose outside the JSON.',
828
+ 'Schema:',
829
+ '{',
830
+ ' "opportunities": [',
831
+ ' {',
832
+ ' "title": "short task candidate title",',
833
+ ' "rationale": "why this likely matters now",',
834
+ ' "suggestedAction": "what task should be created if the user accepts",',
835
+ ' "priority": "high | medium | low",',
836
+ ' "target": { "type": "dm | channel | general", "agentId": "optional agent id", "channelId": "optional channel id" },',
837
+ ' "evidenceLinks": [{ "label": "source label", "url": "app-relative or absolute URL", "summary": "short source summary" }]',
838
+ ' }',
839
+ ' ]',
840
+ '}',
841
+ '',
842
+ 'Rules:',
843
+ '- Include at most 8 opportunities.',
844
+ '- Prefer concrete follow-ups backed by tasks, summaries, saved inbox items, or recent messages.',
845
+ '- Use target.type "dm" only when the target agent id is known; use "channel" only when channel id is known; otherwise use "general".',
846
+ '- If there is no meaningful next work, return {"opportunities":[]}.',
847
+ '',
848
+ '```json',
849
+ JSON.stringify(params.snapshot, null, 2),
850
+ '```',
851
+ ].join('\n');
852
+ }
853
+ }
854
+ export function getSuggestedPlannerTerminalError(stopReason, error) {
855
+ const explicitError = error?.trim();
856
+ if (explicitError)
857
+ return explicitError;
858
+ const normalized = stopReason?.trim().toLowerCase();
859
+ if (!normalized || normalized === 'end_turn' || normalized === 'stop_sequence')
860
+ return undefined;
861
+ if (normalized.includes('cancel') || normalized.includes('abort') || normalized === 'error') {
862
+ return `Suggested planner run ended before completion: ${stopReason}`;
863
+ }
864
+ return undefined;
865
+ }
866
+ export function isPlatformAgent(agent) {
867
+ return agent?.agentRole === 'platform_agent' || agent?.agentRole === 'library_curator' || agent?.agentRole === 'suggested_planner';
868
+ }