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