@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,197 @@
|
|
|
1
|
+
function toIso(timestamp) {
|
|
2
|
+
return typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0
|
|
3
|
+
? new Date(timestamp).toISOString()
|
|
4
|
+
: null;
|
|
5
|
+
}
|
|
6
|
+
function parseSurfaceKind(value) {
|
|
7
|
+
if (value === 'dm')
|
|
8
|
+
return 'dm';
|
|
9
|
+
if (value === 'dm_thread')
|
|
10
|
+
return 'dm_thread';
|
|
11
|
+
if (value === 'channel' || value === 'channel_root')
|
|
12
|
+
return 'channel';
|
|
13
|
+
if (value === 'channel_thread' || value === 'task_thread')
|
|
14
|
+
return 'channel_thread';
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function compareHosts(a, b) {
|
|
18
|
+
const rank = (state) => {
|
|
19
|
+
switch (state) {
|
|
20
|
+
case 'active': return 0;
|
|
21
|
+
case 'idle': return 1;
|
|
22
|
+
case 'failed': return 2;
|
|
23
|
+
default: return 3;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const rankDelta = rank(a.state) - rank(b.state);
|
|
27
|
+
if (rankDelta !== 0)
|
|
28
|
+
return rankDelta;
|
|
29
|
+
return a.hostKey.localeCompare(b.hostKey);
|
|
30
|
+
}
|
|
31
|
+
export function collectAgentRuntimePresence(db, manager, agent, options) {
|
|
32
|
+
const machine = agent.nodeId ? manager.getMachine(agent.nodeId) : null;
|
|
33
|
+
const currentHostBinding = options?.conversationId ? manager.getConversationHostKey(options.conversationId) : null;
|
|
34
|
+
const hostBindings = manager.getAgentHostKeys(agent.agentId);
|
|
35
|
+
const bindingsOnNode = agent.nodeId
|
|
36
|
+
? hostBindings.filter((binding) => binding.nodeId === agent.nodeId)
|
|
37
|
+
: [];
|
|
38
|
+
const conversationRows = agent.nodeId
|
|
39
|
+
? db.prepare(`SELECT id as conversationId,
|
|
40
|
+
reply_target as replyTarget,
|
|
41
|
+
surface_kind as surfaceKind,
|
|
42
|
+
agent_type as agentType
|
|
43
|
+
FROM conversations
|
|
44
|
+
WHERE agent_id = ? AND node_id = ?`).all(agent.agentId, agent.nodeId)
|
|
45
|
+
: [];
|
|
46
|
+
const conversationByHostKey = new Map();
|
|
47
|
+
for (const row of conversationRows) {
|
|
48
|
+
conversationByHostKey.set(`conversation:${row.conversationId}:${row.agentType}`, {
|
|
49
|
+
replyTarget: row.replyTarget,
|
|
50
|
+
surfaceKind: parseSurfaceKind(row.surfaceKind),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const hostRows = bindingsOnNode.length > 0
|
|
54
|
+
? db.prepare(`SELECT host_key as hostKey,
|
|
55
|
+
state,
|
|
56
|
+
current_run_id as currentRunId,
|
|
57
|
+
has_pending_approval as hasPendingApproval,
|
|
58
|
+
inbox_size as inboxSize,
|
|
59
|
+
pending_dispatch_count as pendingDispatchCount,
|
|
60
|
+
resumable,
|
|
61
|
+
last_wake_at as lastWakeAt,
|
|
62
|
+
last_sleep_at as lastSleepAt,
|
|
63
|
+
last_error as lastError
|
|
64
|
+
FROM node_host_snapshots
|
|
65
|
+
WHERE node_id = ? AND host_key IN (${bindingsOnNode.map(() => '?').join(', ')})`).all(agent.nodeId, ...bindingsOnNode.map((binding) => binding.hostKey))
|
|
66
|
+
: [];
|
|
67
|
+
const hosts = hostRows
|
|
68
|
+
.map((row) => {
|
|
69
|
+
const conversation = conversationByHostKey.get(row.hostKey);
|
|
70
|
+
const host = {
|
|
71
|
+
hostKey: row.hostKey,
|
|
72
|
+
replyTarget: conversation?.replyTarget ?? null,
|
|
73
|
+
surfaceKind: conversation?.surfaceKind ?? null,
|
|
74
|
+
state: row.state,
|
|
75
|
+
currentRunId: row.currentRunId,
|
|
76
|
+
hasPendingApproval: row.hasPendingApproval === 1,
|
|
77
|
+
inboxSize: row.inboxSize,
|
|
78
|
+
pendingDispatchCount: row.pendingDispatchCount,
|
|
79
|
+
resumable: row.resumable === 1,
|
|
80
|
+
lastWakeAt: toIso(row.lastWakeAt),
|
|
81
|
+
lastSleepAt: toIso(row.lastSleepAt),
|
|
82
|
+
lastError: row.lastError,
|
|
83
|
+
};
|
|
84
|
+
return host;
|
|
85
|
+
})
|
|
86
|
+
.sort(compareHosts);
|
|
87
|
+
const currentHostKey = currentHostBinding && currentHostBinding.nodeId === agent.nodeId
|
|
88
|
+
? currentHostBinding.hostKey
|
|
89
|
+
: null;
|
|
90
|
+
const currentHost = currentHostKey ? hosts.find((host) => host.hostKey === currentHostKey) ?? null : null;
|
|
91
|
+
const otherHosts = hosts.filter((host) => host.hostKey !== currentHostKey).slice(0, 4);
|
|
92
|
+
const runtimeRow = agent.nodeId
|
|
93
|
+
? db.prepare(`SELECT workspace_root as workspaceRoot,
|
|
94
|
+
terminal_backend_available as terminalBackendAvailable,
|
|
95
|
+
runtime_drivers_json as runtimeDriversJson,
|
|
96
|
+
capabilities_json as capabilitiesJson
|
|
97
|
+
FROM node_runtime_snapshots
|
|
98
|
+
WHERE node_id = ?`).get(agent.nodeId)
|
|
99
|
+
: undefined;
|
|
100
|
+
let runtimeDrivers = [];
|
|
101
|
+
if (runtimeRow?.runtimeDriversJson) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(runtimeRow.runtimeDriversJson);
|
|
104
|
+
if (Array.isArray(parsed))
|
|
105
|
+
runtimeDrivers = parsed;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
runtimeDrivers = [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let capabilities = null;
|
|
112
|
+
if (runtimeRow?.capabilitiesJson) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(runtimeRow.capabilitiesJson);
|
|
115
|
+
if (parsed && typeof parsed === 'object')
|
|
116
|
+
capabilities = parsed;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
capabilities = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
generatedAt: new Date().toISOString(),
|
|
124
|
+
node: {
|
|
125
|
+
nodeId: agent.nodeId ?? null,
|
|
126
|
+
machineName: machine?.name ?? null,
|
|
127
|
+
hostname: machine?.hostname ?? null,
|
|
128
|
+
status: agent.nodeId ? (machine?.status ?? 'offline') : 'unassigned',
|
|
129
|
+
version: machine?.version ?? null,
|
|
130
|
+
lastSeen: toIso(machine?.lastSeen ?? null),
|
|
131
|
+
},
|
|
132
|
+
runtime: {
|
|
133
|
+
workspaceRoot: runtimeRow?.workspaceRoot ?? null,
|
|
134
|
+
terminalBackendAvailable: runtimeRow ? Boolean(runtimeRow.terminalBackendAvailable) : null,
|
|
135
|
+
runtimeDrivers,
|
|
136
|
+
capabilities,
|
|
137
|
+
},
|
|
138
|
+
summary: {
|
|
139
|
+
totalHosts: hosts.length,
|
|
140
|
+
activeHosts: hosts.filter((host) => host.state === 'active').length,
|
|
141
|
+
idleHosts: hosts.filter((host) => host.state === 'idle').length,
|
|
142
|
+
failedHosts: hosts.filter((host) => host.state === 'failed').length,
|
|
143
|
+
hostsWithPendingApproval: hosts.filter((host) => host.hasPendingApproval).length,
|
|
144
|
+
resumableHosts: hosts.filter((host) => host.resumable).length,
|
|
145
|
+
},
|
|
146
|
+
currentHost,
|
|
147
|
+
otherHosts,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function buildAgentRuntimePresenceReport(presence) {
|
|
151
|
+
const driverNames = presence.runtime.runtimeDrivers.map((driver) => driver.agentType).join(', ') || 'none';
|
|
152
|
+
const capabilityParts = presence.runtime.capabilities
|
|
153
|
+
? Object.entries(presence.runtime.capabilities)
|
|
154
|
+
.filter(([, enabled]) => Boolean(enabled))
|
|
155
|
+
.map(([name]) => name)
|
|
156
|
+
: [];
|
|
157
|
+
const lines = [
|
|
158
|
+
'[Runtime presence]',
|
|
159
|
+
`node: ${presence.node.nodeId ?? 'none'}${presence.node.machineName ? ` (${presence.node.machineName})` : ''} [${presence.node.status}]${presence.node.version ? ` version=${presence.node.version}` : ''}`,
|
|
160
|
+
`runtime: workspace_root=${presence.runtime.workspaceRoot ?? 'unknown'} | terminals=${presence.runtime.terminalBackendAvailable == null ? 'unknown' : (presence.runtime.terminalBackendAvailable ? 'yes' : 'no')} | drivers=${driverNames}`,
|
|
161
|
+
`hosts: total=${presence.summary.totalHosts}, active=${presence.summary.activeHosts}, idle=${presence.summary.idleHosts}, failed=${presence.summary.failedHosts}, approval=${presence.summary.hostsWithPendingApproval}, resumable=${presence.summary.resumableHosts}`,
|
|
162
|
+
];
|
|
163
|
+
if (capabilityParts.length > 0) {
|
|
164
|
+
lines.push(`capabilities: ${capabilityParts.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
if (presence.currentHost) {
|
|
167
|
+
const current = presence.currentHost;
|
|
168
|
+
const extras = [
|
|
169
|
+
`pending_dispatch=${current.pendingDispatchCount}`,
|
|
170
|
+
`inbox=${current.inboxSize}`,
|
|
171
|
+
`approval=${current.hasPendingApproval ? 'yes' : 'no'}`,
|
|
172
|
+
`resumable=${current.resumable ? 'yes' : 'no'}`,
|
|
173
|
+
];
|
|
174
|
+
if (current.lastError)
|
|
175
|
+
extras.push(`last_error=${current.lastError}`);
|
|
176
|
+
lines.push(`current_host: ${(current.replyTarget ?? current.hostKey)} [${current.state}] | ${extras.join(' | ')}`);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
lines.push('current_host: none');
|
|
180
|
+
}
|
|
181
|
+
if (presence.otherHosts.length === 0) {
|
|
182
|
+
lines.push('other_hosts: none');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push('other_hosts:');
|
|
186
|
+
for (const host of presence.otherHosts) {
|
|
187
|
+
const extras = [
|
|
188
|
+
`pending_dispatch=${host.pendingDispatchCount}`,
|
|
189
|
+
`approval=${host.hasPendingApproval ? 'yes' : 'no'}`,
|
|
190
|
+
];
|
|
191
|
+
if (host.lastError)
|
|
192
|
+
extras.push(`last_error=${host.lastError}`);
|
|
193
|
+
lines.push(`- ${(host.replyTarget ?? host.hostKey)} [${host.state}] | ${extras.join(' | ')}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { buildThreadShortId, } from '@bbigbang/protocol';
|
|
2
|
+
import { formatConversationHandoff, listRecentConversationHandoffs } from './conversationHandoffs.js';
|
|
3
|
+
import { AgentVisibility } from './agentVisibility.js';
|
|
4
|
+
import { parseSurfaceKind } from './conversationSurfaceKinds.js';
|
|
5
|
+
import { getAgentMessageCheckpoint } from './messageCheckpoints.js';
|
|
6
|
+
import { TASK_LIFECYCLE_SOURCE } from './taskLifecycleMessages.js';
|
|
7
|
+
import { buildTaskSourceTarget, buildTaskThreadTarget } from './taskStateViews.js';
|
|
8
|
+
export function collectAgentSelfState(db, agent, options) {
|
|
9
|
+
const visibility = new AgentVisibility(db);
|
|
10
|
+
const joinedChannels = loadJoinedChannels(db, agent);
|
|
11
|
+
const conversationRows = db.prepare(`SELECT c.id as conversationId,
|
|
12
|
+
c.title,
|
|
13
|
+
c.status,
|
|
14
|
+
c.channel_id as channelId,
|
|
15
|
+
ch.name as channelName,
|
|
16
|
+
c.thread_kind as threadKind,
|
|
17
|
+
c.is_primary_thread as isPrimaryThread,
|
|
18
|
+
c.thread_root_id as threadRootId,
|
|
19
|
+
c.surface_kind as surfaceKind,
|
|
20
|
+
c.reply_target as replyTarget,
|
|
21
|
+
c.user_id as userId,
|
|
22
|
+
c.updated_at as updatedAt,
|
|
23
|
+
COALESCE(q.queueCount, 0) as queuedPromptCount,
|
|
24
|
+
ar.runId as activeRunId
|
|
25
|
+
FROM conversations c
|
|
26
|
+
LEFT JOIN channels ch ON ch.channel_id = c.channel_id
|
|
27
|
+
LEFT JOIN (
|
|
28
|
+
SELECT conversation_id, COUNT(*) as queueCount
|
|
29
|
+
FROM conversation_prompt_queue
|
|
30
|
+
WHERE COALESCE(dispatch_kind, '') != 'legacy_unknown'
|
|
31
|
+
GROUP BY conversation_id
|
|
32
|
+
) q ON q.conversation_id = c.id
|
|
33
|
+
LEFT JOIN (
|
|
34
|
+
SELECT c2.id as conversationId, MIN(r.run_id) as runId
|
|
35
|
+
FROM conversations c2
|
|
36
|
+
JOIN runs r ON r.session_key = c2.session_key
|
|
37
|
+
WHERE r.ended_at IS NULL
|
|
38
|
+
GROUP BY c2.id
|
|
39
|
+
) ar ON ar.conversationId = c.id
|
|
40
|
+
WHERE c.agent_id = ?
|
|
41
|
+
ORDER BY c.updated_at DESC, c.created_at DESC`).all(agent.agentId);
|
|
42
|
+
const visibleChannelIds = new Set(agent.channelIds ?? []);
|
|
43
|
+
const visibleTaskChannelIds = Array.from(new Set([`dm:${agent.agentId}`, ...visibleChannelIds]));
|
|
44
|
+
const visibleTaskChannelPlaceholders = visibleTaskChannelIds.map(() => '?').join(', ');
|
|
45
|
+
const conversations = conversationRows
|
|
46
|
+
.filter((row) => row.channelId.startsWith('dm:') || visibleChannelIds.has(row.channelId))
|
|
47
|
+
.map((row) => toConversationState(db, agent.agentId, row))
|
|
48
|
+
.sort(compareConversations(options?.conversationId ?? null));
|
|
49
|
+
const currentConversation = options?.conversationId
|
|
50
|
+
? conversations.find((conversation) => conversation.conversationId === options.conversationId) ?? null
|
|
51
|
+
: null;
|
|
52
|
+
const taskLimit = Math.max(1, Math.min(options?.maxTasks ?? 6, 12));
|
|
53
|
+
const taskSummaryRow = db.prepare(`SELECT COUNT(DISTINCT t.task_id) as involvedTasks,
|
|
54
|
+
COUNT(DISTINCT CASE WHEN t.claimed_by_agent_id = ? THEN t.task_id END) as claimedTasks,
|
|
55
|
+
COUNT(DISTINCT CASE WHEN t.status = 'in_review' THEN t.task_id END) as inReviewTasks
|
|
56
|
+
FROM tasks t
|
|
57
|
+
LEFT JOIN task_participants tp
|
|
58
|
+
ON tp.task_id = t.task_id
|
|
59
|
+
AND tp.agent_id = ?
|
|
60
|
+
WHERE (
|
|
61
|
+
t.claimed_by_agent_id = ?
|
|
62
|
+
AND t.channel_id IN (${visibleTaskChannelPlaceholders})
|
|
63
|
+
)
|
|
64
|
+
OR (
|
|
65
|
+
tp.agent_id IS NOT NULL
|
|
66
|
+
AND t.channel_id IN (${visibleTaskChannelPlaceholders})
|
|
67
|
+
)`).get(agent.agentId, agent.agentId, agent.agentId, ...visibleTaskChannelIds, ...visibleTaskChannelIds);
|
|
68
|
+
const bufferedTaskLimit = Math.min(taskLimit * 2, 24);
|
|
69
|
+
const taskRows = db.prepare(`SELECT t.task_id as taskId,
|
|
70
|
+
t.agent_task_ref as agentTaskRef,
|
|
71
|
+
t.channel_id as channelId,
|
|
72
|
+
t.task_number as taskNumber,
|
|
73
|
+
t.title,
|
|
74
|
+
t.status,
|
|
75
|
+
t.updated_at as updatedAt,
|
|
76
|
+
t.message_id as messageId,
|
|
77
|
+
COALESCE(t.dm_target, cm.target) as sourceTarget,
|
|
78
|
+
ch.name as channelName,
|
|
79
|
+
CASE WHEN tp.agent_id IS NULL THEN 0 ELSE 1 END as participant,
|
|
80
|
+
t.claimed_by_agent_id as claimedByAgentId
|
|
81
|
+
FROM tasks t
|
|
82
|
+
LEFT JOIN task_participants tp
|
|
83
|
+
ON tp.task_id = t.task_id
|
|
84
|
+
AND tp.agent_id = ?
|
|
85
|
+
LEFT JOIN channel_messages cm
|
|
86
|
+
ON cm.message_id = t.message_id
|
|
87
|
+
LEFT JOIN channels ch
|
|
88
|
+
ON ch.channel_id = t.channel_id
|
|
89
|
+
WHERE (
|
|
90
|
+
t.claimed_by_agent_id = ?
|
|
91
|
+
AND t.channel_id IN (${visibleTaskChannelPlaceholders})
|
|
92
|
+
)
|
|
93
|
+
OR (
|
|
94
|
+
tp.agent_id IS NOT NULL
|
|
95
|
+
AND t.channel_id IN (${visibleTaskChannelPlaceholders})
|
|
96
|
+
)
|
|
97
|
+
ORDER BY t.updated_at DESC, t.created_at DESC
|
|
98
|
+
LIMIT ?`).all(agent.agentId, agent.agentId, ...visibleTaskChannelIds, ...visibleTaskChannelIds, bufferedTaskLimit);
|
|
99
|
+
const recentTasks = taskRows.slice(0, taskLimit).map((row) => ({
|
|
100
|
+
taskId: row.taskId,
|
|
101
|
+
agentTaskRef: row.agentTaskRef,
|
|
102
|
+
taskNumber: row.taskNumber,
|
|
103
|
+
title: row.title,
|
|
104
|
+
status: row.status,
|
|
105
|
+
sourceTarget: buildTaskSourceTarget(row.channelId, row.sourceTarget, row.channelName),
|
|
106
|
+
threadTarget: buildTaskThreadTarget(buildTaskSourceTarget(row.channelId, row.sourceTarget, row.channelName), row.messageId),
|
|
107
|
+
claimedByMe: row.claimedByAgentId === agent.agentId,
|
|
108
|
+
participant: row.participant === 1,
|
|
109
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
110
|
+
}));
|
|
111
|
+
const recentHandoffs = listRecentConversationHandoffs(db, agent.agentId, 5)
|
|
112
|
+
.filter((handoff) => visibility.canSeeHandoff(agent, {
|
|
113
|
+
handoffId: handoff.handoffId,
|
|
114
|
+
mode: handoff.mode,
|
|
115
|
+
status: handoff.status,
|
|
116
|
+
sourceConversationId: handoff.sourceConversationId,
|
|
117
|
+
targetConversationId: handoff.targetConversationId,
|
|
118
|
+
targetReplyTarget: handoff.targetReplyTarget,
|
|
119
|
+
sourceTarget: handoff.payload.sourceTarget,
|
|
120
|
+
goal: handoff.payload.goal,
|
|
121
|
+
error: handoff.payload.error,
|
|
122
|
+
updatedAt: handoff.updatedAt,
|
|
123
|
+
}))
|
|
124
|
+
.map((handoff) => ({
|
|
125
|
+
handoffId: handoff.handoffId,
|
|
126
|
+
mode: handoff.mode,
|
|
127
|
+
status: handoff.status,
|
|
128
|
+
sourceConversationId: handoff.sourceConversationId,
|
|
129
|
+
sourceTarget: handoff.payload.sourceTarget,
|
|
130
|
+
targetConversationId: handoff.targetConversationId,
|
|
131
|
+
targetReplyTarget: handoff.targetReplyTarget,
|
|
132
|
+
goal: handoff.payload.goal,
|
|
133
|
+
error: handoff.payload.error,
|
|
134
|
+
updatedAt: handoff.updatedAt,
|
|
135
|
+
summaryText: formatConversationHandoff(handoff),
|
|
136
|
+
}));
|
|
137
|
+
const summary = {
|
|
138
|
+
totalConversations: conversations.length,
|
|
139
|
+
activeConversations: conversations.filter((conversation) => conversation.status === 'active').length,
|
|
140
|
+
queuedConversations: conversations.filter((conversation) => conversation.status === 'queued').length,
|
|
141
|
+
recoveringConversations: conversations.filter((conversation) => conversation.status === 'recovering').length,
|
|
142
|
+
awaitingApprovalConversations: conversations.filter((conversation) => conversation.status === 'awaiting_approval').length,
|
|
143
|
+
failedConversations: conversations.filter((conversation) => conversation.status === 'failed').length,
|
|
144
|
+
totalUnreadMessages: conversations.reduce((sum, conversation) => sum + conversation.unreadCount, 0),
|
|
145
|
+
surfacesWithUnread: conversations.filter((conversation) => conversation.unreadCount > 0).length,
|
|
146
|
+
pendingPrompts: conversations.reduce((sum, conversation) => sum + conversation.queuedPromptCount, 0),
|
|
147
|
+
claimedTasks: Number(taskSummaryRow?.claimedTasks ?? 0),
|
|
148
|
+
involvedTasks: Number(taskSummaryRow?.involvedTasks ?? 0),
|
|
149
|
+
inReviewTasks: Number(taskSummaryRow?.inReviewTasks ?? 0),
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
generatedAt: new Date().toISOString(),
|
|
153
|
+
agent: {
|
|
154
|
+
agentId: agent.agentId,
|
|
155
|
+
name: agent.name,
|
|
156
|
+
agentType: agent.agentType,
|
|
157
|
+
nodeId: agent.nodeId ?? null,
|
|
158
|
+
workspacePath: agent.workspacePath,
|
|
159
|
+
homeChannelId: agent.channelId,
|
|
160
|
+
joinedChannels,
|
|
161
|
+
},
|
|
162
|
+
summary,
|
|
163
|
+
currentConversation,
|
|
164
|
+
conversations,
|
|
165
|
+
recentTasks,
|
|
166
|
+
recentHandoffs,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function buildAgentGlobalSnapshot(state) {
|
|
170
|
+
const current = state.currentConversation;
|
|
171
|
+
const currentSurface = current
|
|
172
|
+
? `${current.replyTarget} [${current.surfaceKind}, status=${current.status}, unread=${current.unreadCount}, queued_prompts=${current.queuedPromptCount}]`
|
|
173
|
+
: 'unknown';
|
|
174
|
+
const otherLiveSurfaces = state.conversations
|
|
175
|
+
.filter((conversation) => !current || conversation.conversationId !== current.conversationId)
|
|
176
|
+
.filter((conversation) => conversation.status !== 'idle' || conversation.unreadCount > 0 || conversation.queuedPromptCount > 0)
|
|
177
|
+
.slice(0, 4);
|
|
178
|
+
const lines = [
|
|
179
|
+
'[Agent global snapshot]',
|
|
180
|
+
`agent: @${state.agent.name}`,
|
|
181
|
+
`current_surface: ${currentSurface}`,
|
|
182
|
+
`global: conversations=${state.summary.totalConversations}, active=${state.summary.activeConversations}, queued=${state.summary.queuedConversations}, awaiting_approval=${state.summary.awaitingApprovalConversations}, unread_messages=${state.summary.totalUnreadMessages} on ${state.summary.surfacesWithUnread} surfaces, pending_prompts=${state.summary.pendingPrompts}`,
|
|
183
|
+
`tasks: claimed=${state.summary.claimedTasks}, involved=${state.summary.involvedTasks}, in_review=${state.summary.inReviewTasks}`,
|
|
184
|
+
];
|
|
185
|
+
if (otherLiveSurfaces.length > 0) {
|
|
186
|
+
lines.push('other_live_surfaces:');
|
|
187
|
+
for (const conversation of otherLiveSurfaces) {
|
|
188
|
+
lines.push(`- ${conversation.replyTarget} [${conversation.status}, unread=${conversation.unreadCount}, queued_prompts=${conversation.queuedPromptCount}]`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
lines.push('other_live_surfaces: none');
|
|
193
|
+
}
|
|
194
|
+
if (state.recentHandoffs.length > 0) {
|
|
195
|
+
lines.push('recent_handoffs:');
|
|
196
|
+
for (const handoff of state.recentHandoffs.slice(0, 3)) {
|
|
197
|
+
lines.push(`- ${handoff.summaryText}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
lines.push('recent_handoffs: none');
|
|
202
|
+
}
|
|
203
|
+
return lines.join('\n');
|
|
204
|
+
}
|
|
205
|
+
export function buildAgentSelfStateReport(state, options) {
|
|
206
|
+
const current = state.currentConversation;
|
|
207
|
+
const joinedChannels = formatJoinedChannels(state.agent.joinedChannels.map((channel) => channel.target));
|
|
208
|
+
const currentSurface = current
|
|
209
|
+
? formatSelfStateSurface(current)
|
|
210
|
+
: 'none';
|
|
211
|
+
const lines = [
|
|
212
|
+
'[Agent self state]',
|
|
213
|
+
`agent: @${state.agent.name} (${state.agent.agentType}) | current: ${currentSurface}`,
|
|
214
|
+
`joined_channels: ${joinedChannels.text}`,
|
|
215
|
+
`global: conv=${state.summary.totalConversations}, active=${state.summary.activeConversations}, queued=${state.summary.queuedConversations}, recovering=${state.summary.recoveringConversations}, approval=${state.summary.awaitingApprovalConversations}, failed=${state.summary.failedConversations}, new_activity=${state.summary.surfacesWithUnread}, pending=${state.summary.pendingPrompts} | tasks claimed=${state.summary.claimedTasks}, involved=${state.summary.involvedTasks}, review=${state.summary.inReviewTasks}`,
|
|
216
|
+
];
|
|
217
|
+
if (options?.currentContext) {
|
|
218
|
+
if (options.currentContext.myRole)
|
|
219
|
+
lines.push(`role_here: ${options.currentContext.myRole}`);
|
|
220
|
+
if (options.currentContext.boundTask) {
|
|
221
|
+
lines.push(`bound_task: ${formatTaskIdentity(options.currentContext.boundTask.agentTaskRef, options.currentContext.boundTask.taskNumber)} [${options.currentContext.boundTask.status}]`);
|
|
222
|
+
}
|
|
223
|
+
if (options.currentContext.ownerName)
|
|
224
|
+
lines.push(`owner_here: @${options.currentContext.ownerName}`);
|
|
225
|
+
if (options.currentContext.participants.length > 0) {
|
|
226
|
+
lines.push(`participants_here: ${options.currentContext.participants.map((name) => `@${name}`).join(', ')}`);
|
|
227
|
+
}
|
|
228
|
+
if (options.currentContext.blockers.length > 0) {
|
|
229
|
+
lines.push(`blockers_here: ${truncateSelfStateValue(options.currentContext.blockers.join('; '), 96)}`);
|
|
230
|
+
}
|
|
231
|
+
if (options.currentContext.activeHandoff) {
|
|
232
|
+
lines.push(`active_handoff_here: ${options.currentContext.activeHandoff.handoffId} ${options.currentContext.activeHandoff.mode} [${options.currentContext.activeHandoff.status}] -> ${options.currentContext.activeHandoff.targetReplyTarget}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const currentConversationId = current?.conversationId ?? null;
|
|
236
|
+
const otherConversations = state.conversations
|
|
237
|
+
.filter((conversation) => conversation.conversationId !== currentConversationId);
|
|
238
|
+
const surfaces = otherConversations.slice(0, 4);
|
|
239
|
+
lines.push('other_surfaces:');
|
|
240
|
+
if (surfaces.length === 0) {
|
|
241
|
+
lines.push('- none');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
for (const conversation of surfaces) {
|
|
245
|
+
const context = options?.conversationContexts?.[conversation.conversationId];
|
|
246
|
+
const extras = [];
|
|
247
|
+
if (context?.myRole)
|
|
248
|
+
extras.push(`role=${context.myRole}`);
|
|
249
|
+
if (conversation.unreadCount > 0)
|
|
250
|
+
extras.push(`unread=${conversation.unreadCount}`);
|
|
251
|
+
if (context?.boundTask) {
|
|
252
|
+
extras.push(`task=${formatTaskIdentity(context.boundTask.agentTaskRef, context.boundTask.taskNumber)} [${context.boundTask.status}]`);
|
|
253
|
+
}
|
|
254
|
+
if (context?.blockers.length)
|
|
255
|
+
extras.push(`blockers=${truncateSelfStateValue(context.blockers.join('; '), 80)}`);
|
|
256
|
+
if (context?.activeHandoff) {
|
|
257
|
+
extras.push(`handoff=${context.activeHandoff.handoffId} ${context.activeHandoff.mode} [${context.activeHandoff.status}]`);
|
|
258
|
+
}
|
|
259
|
+
const teaser = buildSelfStateTeaser(context);
|
|
260
|
+
if (teaser)
|
|
261
|
+
extras.push(`teaser=${truncateSelfStateValue(teaser, 64)}`);
|
|
262
|
+
lines.push(`- ${formatSelfStateSurface(conversation)}${extras.length ? ` | ${extras.join(' | ')}` : ''}`);
|
|
263
|
+
}
|
|
264
|
+
const hiddenSurfaces = otherConversations.slice(surfaces.length);
|
|
265
|
+
const hiddenSurfaceCount = hiddenSurfaces.length;
|
|
266
|
+
if (hiddenSurfaceCount > 0) {
|
|
267
|
+
lines.push(`- hidden=${hiddenSurfaceCount}${formatHiddenSurfaceBreakdown(hiddenSurfaces)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const currentBoundTaskId = options?.currentContext?.boundTask?.taskId ?? null;
|
|
271
|
+
const recentTasks = state.recentTasks
|
|
272
|
+
.filter((task) => task.taskId !== currentBoundTaskId)
|
|
273
|
+
.slice(0, 3);
|
|
274
|
+
const totalOtherTaskCount = Math.max(0, state.summary.involvedTasks - (currentBoundTaskId ? 1 : 0));
|
|
275
|
+
if (state.recentTasks.length === 0) {
|
|
276
|
+
lines.push('tasks: none');
|
|
277
|
+
}
|
|
278
|
+
else if (recentTasks.length > 0) {
|
|
279
|
+
lines.push('other_tasks:');
|
|
280
|
+
for (const task of recentTasks) {
|
|
281
|
+
const identity = formatTaskIdentity(task.agentTaskRef, task.taskNumber);
|
|
282
|
+
const source = task.threadTarget ?? task.sourceTarget ?? 'unknown';
|
|
283
|
+
const ownership = task.claimedByMe ? 'claimed' : (task.participant ? 'participant' : 'linked');
|
|
284
|
+
lines.push(`- ${identity} [${task.status}, ${ownership}] ${truncateSelfStateValue(task.title, 72)} @ ${source}`);
|
|
285
|
+
}
|
|
286
|
+
if (totalOtherTaskCount > recentTasks.length) {
|
|
287
|
+
lines.push(`- ... ${totalOtherTaskCount - recentTasks.length} more task(s) hidden`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
lines.push(totalOtherTaskCount > 0 ? `other_tasks: ${totalOtherTaskCount} hidden` : 'other_tasks: none');
|
|
292
|
+
}
|
|
293
|
+
const recentHandoffs = state.recentHandoffs
|
|
294
|
+
.filter((handoff) => handoff.status !== 'completed')
|
|
295
|
+
.slice(0, 3);
|
|
296
|
+
if (recentHandoffs.length === 0) {
|
|
297
|
+
lines.push('recent_handoffs: none');
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
lines.push('recent_handoffs:');
|
|
301
|
+
for (const handoff of recentHandoffs) {
|
|
302
|
+
lines.push(`- ${handoff.handoffId} ${truncateSelfStateValue(handoff.summaryText, 120)}`);
|
|
303
|
+
}
|
|
304
|
+
if (state.recentHandoffs.filter((handoff) => handoff.status !== 'completed').length > recentHandoffs.length) {
|
|
305
|
+
lines.push(`- ... ${state.recentHandoffs.filter((handoff) => handoff.status !== 'completed').length - recentHandoffs.length} more handoff(s) hidden`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|
|
310
|
+
function formatSelfStateSurface(conversation) {
|
|
311
|
+
return `${conversation.replyTarget} [${conversation.surfaceKind}, ${conversation.status}]`;
|
|
312
|
+
}
|
|
313
|
+
function buildSelfStateTeaser(context) {
|
|
314
|
+
if (!context)
|
|
315
|
+
return null;
|
|
316
|
+
const candidates = [
|
|
317
|
+
context.boundTask?.title,
|
|
318
|
+
context.goal,
|
|
319
|
+
context.lastMessage?.preview,
|
|
320
|
+
];
|
|
321
|
+
for (const candidate of candidates) {
|
|
322
|
+
const trimmed = candidate?.trim();
|
|
323
|
+
if (trimmed)
|
|
324
|
+
return trimmed;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function formatHiddenSurfaceBreakdown(conversations) {
|
|
329
|
+
if (conversations.length === 0)
|
|
330
|
+
return '';
|
|
331
|
+
const order = ['active', 'queued', 'recovering', 'awaiting_approval', 'failed', 'idle'];
|
|
332
|
+
const counts = new Map();
|
|
333
|
+
for (const conversation of conversations) {
|
|
334
|
+
counts.set(conversation.status, (counts.get(conversation.status) ?? 0) + 1);
|
|
335
|
+
}
|
|
336
|
+
const parts = order
|
|
337
|
+
.map((status) => {
|
|
338
|
+
const count = counts.get(status) ?? 0;
|
|
339
|
+
return count > 0 ? `${status}=${count}` : null;
|
|
340
|
+
})
|
|
341
|
+
.filter((part) => Boolean(part));
|
|
342
|
+
return parts.length > 0 ? `(${parts.join(',')})` : '';
|
|
343
|
+
}
|
|
344
|
+
function formatTaskIdentity(agentTaskRef, taskNumber) {
|
|
345
|
+
if (agentTaskRef && taskNumber != null)
|
|
346
|
+
return `${agentTaskRef} · #t${taskNumber}`;
|
|
347
|
+
if (agentTaskRef)
|
|
348
|
+
return agentTaskRef;
|
|
349
|
+
if (taskNumber != null)
|
|
350
|
+
return `#t${taskNumber}`;
|
|
351
|
+
return 'task';
|
|
352
|
+
}
|
|
353
|
+
function truncateSelfStateValue(value, maxLength = 72) {
|
|
354
|
+
const trimmed = value?.trim() ?? '';
|
|
355
|
+
if (!trimmed)
|
|
356
|
+
return '';
|
|
357
|
+
if (trimmed.length <= maxLength)
|
|
358
|
+
return trimmed;
|
|
359
|
+
return `${trimmed.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
360
|
+
}
|
|
361
|
+
function formatJoinedChannels(targets, maxChannels = 6) {
|
|
362
|
+
if (targets.length === 0)
|
|
363
|
+
return { text: '(none)', truncated: false };
|
|
364
|
+
const visibleTargets = targets.slice(0, maxChannels);
|
|
365
|
+
const hiddenCount = Math.max(0, targets.length - visibleTargets.length);
|
|
366
|
+
return {
|
|
367
|
+
text: hiddenCount > 0
|
|
368
|
+
? `${visibleTargets.join(', ')} ... +${hiddenCount} more`
|
|
369
|
+
: visibleTargets.join(', '),
|
|
370
|
+
truncated: hiddenCount > 0,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function loadJoinedChannels(db, agent) {
|
|
374
|
+
const rows = db.prepare(`SELECT c.channel_id as channelId, ch.name as channelName
|
|
375
|
+
FROM agent_channel_memberships c
|
|
376
|
+
LEFT JOIN channels ch ON ch.channel_id = c.channel_id
|
|
377
|
+
WHERE c.agent_id = ?
|
|
378
|
+
ORDER BY CASE WHEN c.channel_id = ? THEN 0 ELSE 1 END, c.joined_at ASC`).all(agent.agentId, agent.channelId);
|
|
379
|
+
return rows.map((row) => ({
|
|
380
|
+
channelId: row.channelId,
|
|
381
|
+
name: row.channelName ?? null,
|
|
382
|
+
target: row.channelId.startsWith('dm:') ? row.channelId : `#${row.channelName ?? row.channelId}`,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
function toConversationState(db, agentId, row) {
|
|
386
|
+
const replyTarget = resolveConversationTarget(row);
|
|
387
|
+
const messageChannelId = row.threadKind === 'direct' ? `dm:${agentId}` : row.channelId;
|
|
388
|
+
return {
|
|
389
|
+
conversationId: row.conversationId,
|
|
390
|
+
title: row.title,
|
|
391
|
+
status: row.status,
|
|
392
|
+
channelId: messageChannelId,
|
|
393
|
+
channelName: row.channelName,
|
|
394
|
+
replyTarget,
|
|
395
|
+
surfaceKind: parseSurfaceKind(row.surfaceKind, {
|
|
396
|
+
threadKind: row.threadKind,
|
|
397
|
+
threadRootId: row.threadRootId,
|
|
398
|
+
}),
|
|
399
|
+
threadKind: row.threadKind,
|
|
400
|
+
isPrimaryThread: row.isPrimaryThread === 1,
|
|
401
|
+
threadRootId: row.threadRootId,
|
|
402
|
+
unreadCount: countUnreadMessages(db, agentId, row, messageChannelId, replyTarget),
|
|
403
|
+
queuedPromptCount: Number(row.queuedPromptCount ?? 0),
|
|
404
|
+
hasActiveRun: Boolean(row.activeRunId),
|
|
405
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function resolveConversationTarget(row) {
|
|
409
|
+
const trimmedReplyTarget = row.replyTarget?.trim();
|
|
410
|
+
if (trimmedReplyTarget)
|
|
411
|
+
return trimmedReplyTarget;
|
|
412
|
+
if (row.channelId.startsWith('dm:')) {
|
|
413
|
+
const peer = row.userId?.trim() ? `dm:@${row.userId.trim()}` : 'dm:@unknown';
|
|
414
|
+
if (row.threadRootId)
|
|
415
|
+
return `${peer}:${buildThreadShortId(row.threadRootId)}`;
|
|
416
|
+
return peer;
|
|
417
|
+
}
|
|
418
|
+
const baseTarget = `#${row.channelName ?? row.channelId}`;
|
|
419
|
+
if (row.threadRootId)
|
|
420
|
+
return `${baseTarget}:${buildThreadShortId(row.threadRootId)}`;
|
|
421
|
+
return baseTarget;
|
|
422
|
+
}
|
|
423
|
+
function countUnreadMessages(db, agentId, row, messageChannelId, replyTarget) {
|
|
424
|
+
const checkpoint = getAgentMessageCheckpoint(db, agentId, messageChannelId, row.threadRootId);
|
|
425
|
+
if (row.threadRootId) {
|
|
426
|
+
if ((row.threadKind === 'direct' || replyTarget.startsWith('dm:@')) && replyTarget.startsWith('dm:@')) {
|
|
427
|
+
const result = db.prepare(`SELECT COUNT(*) as count
|
|
428
|
+
FROM channel_messages
|
|
429
|
+
WHERE channel_id = ?
|
|
430
|
+
AND seq > ?
|
|
431
|
+
AND COALESCE(sender_id, '') != ?
|
|
432
|
+
AND COALESCE(message_source, '') != ?
|
|
433
|
+
AND (
|
|
434
|
+
thread_root_id = ?
|
|
435
|
+
OR (thread_root_id IS NULL AND target = ?)
|
|
436
|
+
)`).get(messageChannelId, checkpoint, agentId, TASK_LIFECYCLE_SOURCE, row.threadRootId, replyTarget);
|
|
437
|
+
return Number(result.count ?? 0);
|
|
438
|
+
}
|
|
439
|
+
const result = db.prepare(`SELECT COUNT(*) as count
|
|
440
|
+
FROM channel_messages
|
|
441
|
+
WHERE channel_id = ?
|
|
442
|
+
AND seq > ?
|
|
443
|
+
AND COALESCE(sender_id, '') != ?
|
|
444
|
+
AND COALESCE(message_source, '') != ?
|
|
445
|
+
AND thread_root_id = ?`).get(messageChannelId, checkpoint, agentId, TASK_LIFECYCLE_SOURCE, row.threadRootId);
|
|
446
|
+
return Number(result.count ?? 0);
|
|
447
|
+
}
|
|
448
|
+
const result = db.prepare(replyTarget.startsWith('dm:@')
|
|
449
|
+
? `SELECT COUNT(*) as count
|
|
450
|
+
FROM channel_messages
|
|
451
|
+
WHERE channel_id = ?
|
|
452
|
+
AND seq > ?
|
|
453
|
+
AND COALESCE(sender_id, '') != ?
|
|
454
|
+
AND COALESCE(message_source, '') != ?
|
|
455
|
+
AND thread_root_id IS NULL
|
|
456
|
+
AND target = ?`
|
|
457
|
+
: `SELECT COUNT(*) as count
|
|
458
|
+
FROM channel_messages
|
|
459
|
+
WHERE channel_id = ?
|
|
460
|
+
AND seq > ?
|
|
461
|
+
AND COALESCE(sender_id, '') != ?
|
|
462
|
+
AND COALESCE(message_source, '') != ?
|
|
463
|
+
AND thread_root_id IS NULL`).get(...(replyTarget.startsWith('dm:@')
|
|
464
|
+
? [messageChannelId, checkpoint, agentId, TASK_LIFECYCLE_SOURCE, replyTarget]
|
|
465
|
+
: [messageChannelId, checkpoint, agentId, TASK_LIFECYCLE_SOURCE]));
|
|
466
|
+
return Number(result.count ?? 0);
|
|
467
|
+
}
|
|
468
|
+
function compareConversations(currentConversationId) {
|
|
469
|
+
const statusWeight = {
|
|
470
|
+
active: 0,
|
|
471
|
+
recovering: 1,
|
|
472
|
+
awaiting_approval: 2,
|
|
473
|
+
queued: 3,
|
|
474
|
+
failed: 4,
|
|
475
|
+
idle: 5,
|
|
476
|
+
};
|
|
477
|
+
return (left, right) => {
|
|
478
|
+
if (currentConversationId) {
|
|
479
|
+
if (left.conversationId === currentConversationId && right.conversationId !== currentConversationId)
|
|
480
|
+
return -1;
|
|
481
|
+
if (right.conversationId === currentConversationId && left.conversationId !== currentConversationId)
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
const leftWeight = statusWeight[left.status] ?? 99;
|
|
485
|
+
const rightWeight = statusWeight[right.status] ?? 99;
|
|
486
|
+
if (leftWeight !== rightWeight)
|
|
487
|
+
return leftWeight - rightWeight;
|
|
488
|
+
if (left.unreadCount !== right.unreadCount)
|
|
489
|
+
return right.unreadCount - left.unreadCount;
|
|
490
|
+
if (left.queuedPromptCount !== right.queuedPromptCount)
|
|
491
|
+
return right.queuedPromptCount - left.queuedPromptCount;
|
|
492
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
493
|
+
};
|
|
494
|
+
}
|