@bbigbang/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/config.js +380 -0
  2. package/dist/execution/executionDispatcher.js +3810 -0
  3. package/dist/main.js +90 -0
  4. package/dist/nodeEventHistory.js +206 -0
  5. package/dist/scheduler/dreamLogic.js +50 -0
  6. package/dist/scheduler/dreamScheduler.js +65 -0
  7. package/dist/services/agentFileAccessService.js +1913 -0
  8. package/dist/services/agentRuntimeCleanupBroker.js +62 -0
  9. package/dist/services/agentSkillsBroker.js +118 -0
  10. package/dist/services/agentSkillsService.js +83 -0
  11. package/dist/services/agentWorkspaceBroker.js +937 -0
  12. package/dist/services/agentWorkspaceService.js +70 -0
  13. package/dist/services/appVersion.js +14 -0
  14. package/dist/services/auth.js +586 -0
  15. package/dist/services/claudeControlBroker.js +154 -0
  16. package/dist/services/claudeTranscriptBroker.js +100 -0
  17. package/dist/services/claudeTranscriptService.js +359 -0
  18. package/dist/services/codexAppServerBroker.js +155 -0
  19. package/dist/services/codexTranscriptBroker.js +98 -0
  20. package/dist/services/codexTranscriptService.js +961 -0
  21. package/dist/services/droidMissionBroker.js +124 -0
  22. package/dist/services/droidMissionImporter.js +630 -0
  23. package/dist/services/droidModelOptions.js +165 -0
  24. package/dist/services/hubServerRegistrationService.js +268 -0
  25. package/dist/services/libraryManifest.js +43 -0
  26. package/dist/services/libraryScaffold.js +26 -0
  27. package/dist/services/libraryService.js +2263 -0
  28. package/dist/services/memoryService.js +386 -0
  29. package/dist/services/missionEvidence.js +377 -0
  30. package/dist/services/missionService.js +2361 -0
  31. package/dist/services/missionTrace.js +158 -0
  32. package/dist/services/nativeMissionBriefParser.js +120 -0
  33. package/dist/services/nativeMissionOrchestrator.js +2045 -0
  34. package/dist/services/nativeMissionReportGenerator.js +227 -0
  35. package/dist/services/nativeMissionValidationRunner.js +452 -0
  36. package/dist/services/nativeMissionWorkerBroker.js +190 -0
  37. package/dist/services/nodeRegistry.js +34 -0
  38. package/dist/services/nodeStateReconciler.js +97 -0
  39. package/dist/services/panelMediaScanner.js +119 -0
  40. package/dist/services/persistentRuntimeJsonlClient.js +153 -0
  41. package/dist/services/platformAgentPolicy.js +180 -0
  42. package/dist/services/platformAgentService.js +2041 -0
  43. package/dist/services/projectAccessResolver.js +93 -0
  44. package/dist/services/projectService.js +392 -0
  45. package/dist/services/resourceSpaceService.js +140 -0
  46. package/dist/services/scenarioRuntimeService.js +1130 -0
  47. package/dist/services/suggestedPlannerService.js +868 -0
  48. package/dist/services/workbenchGitBroker.js +161 -0
  49. package/dist/services/workbenchGitService.js +69 -0
  50. package/dist/services/workbenchInspectBroker.js +65 -0
  51. package/dist/services/workbenchNodePathService.js +79 -0
  52. package/dist/services/workbenchRegistryService.js +240 -0
  53. package/dist/services/workbenchRootService.js +181 -0
  54. package/dist/services/workbenchTerminalBroker.js +378 -0
  55. package/dist/services/workspaceRunOwnership.js +60 -0
  56. package/dist/services/workspaceScaffold.js +105 -0
  57. package/dist/services/workspaceSessionRuntimeService.js +576 -0
  58. package/dist/services/workspaceSessionService.js +245 -0
  59. package/dist/services/workspaceToolActionRunner.js +1582 -0
  60. package/dist/services/workspaceToolErrors.js +10 -0
  61. package/dist/services/workspaceToolExecutionUtils.js +895 -0
  62. package/dist/services/workspaceToolLatestStateProjector.js +91 -0
  63. package/dist/services/workspaceToolManifest.js +572 -0
  64. package/dist/services/workspaceToolMutationQueue.js +43 -0
  65. package/dist/services/workspaceToolPanelProjection.js +460 -0
  66. package/dist/services/workspaceToolPromotion.js +255 -0
  67. package/dist/services/workspaceToolPromotionState.js +224 -0
  68. package/dist/services/workspaceToolPublishDiagnostics.js +189 -0
  69. package/dist/services/workspaceToolPublishIdentityResolver.js +146 -0
  70. package/dist/services/workspaceToolReadModel.js +378 -0
  71. package/dist/services/workspaceToolRunLedger.js +239 -0
  72. package/dist/services/workspaceToolService.js +3067 -0
  73. package/dist/services/workspaceToolSnapshotPanelSync.js +293 -0
  74. package/dist/services/workspaceToolTerminalLifecycle.js +283 -0
  75. package/dist/services/workspaceToolTypes.js +1 -0
  76. package/dist/services/workspaceToolUploadMaterializer.js +228 -0
  77. package/dist/web/actionCardRoutes.js +129 -0
  78. package/dist/web/actionCards.js +469 -0
  79. package/dist/web/activationContext.js +684 -0
  80. package/dist/web/agentChannelGuards.js +48 -0
  81. package/dist/web/agentMentionCooldowns.js +32 -0
  82. package/dist/web/agentReminders.js +1668 -0
  83. package/dist/web/agentRuntimePresence.js +197 -0
  84. package/dist/web/agentSelfState.js +494 -0
  85. package/dist/web/agentTaskLinks.js +26 -0
  86. package/dist/web/agentVisibility.js +79 -0
  87. package/dist/web/assets.js +95 -0
  88. package/dist/web/channelActivationPrompt.js +395 -0
  89. package/dist/web/channelMemoryNotes.js +127 -0
  90. package/dist/web/channelMentions.js +10 -0
  91. package/dist/web/channelMessageSequences.js +19 -0
  92. package/dist/web/channelSubscriptions.js +26 -0
  93. package/dist/web/clearedTaskRoots.js +10 -0
  94. package/dist/web/collaborationPromptGuidance.js +36 -0
  95. package/dist/web/collaborationSurfaceState.js +140 -0
  96. package/dist/web/contextBundleRanking.js +154 -0
  97. package/dist/web/contextBundleResolver.js +488 -0
  98. package/dist/web/conversationBuiltinSkillRoots.js +50 -0
  99. package/dist/web/conversationControls.js +232 -0
  100. package/dist/web/conversationHandoffs.js +612 -0
  101. package/dist/web/conversationManager.js +2511 -0
  102. package/dist/web/conversationSummaries.js +876 -0
  103. package/dist/web/conversationSurfaceKinds.js +17 -0
  104. package/dist/web/conversationTargets.js +173 -0
  105. package/dist/web/directActivationPrompt.js +122 -0
  106. package/dist/web/directReplyTargets.js +69 -0
  107. package/dist/web/directThreadResolver.js +129 -0
  108. package/dist/web/dmTaskHandoffPrompt.js +120 -0
  109. package/dist/web/dmTaskThreadStatusProjection.js +229 -0
  110. package/dist/web/ftsQuery.js +33 -0
  111. package/dist/web/internalAgentRouter.js +11341 -0
  112. package/dist/web/libraryCuratorScheduler.js +58 -0
  113. package/dist/web/libraryDocumentPromptGuidance.js +8 -0
  114. package/dist/web/messageCheckpoints.js +19 -0
  115. package/dist/web/nodeWsHandler.js +2495 -0
  116. package/dist/web/notificationRounds.js +1061 -0
  117. package/dist/web/panelActionMessages.js +108 -0
  118. package/dist/web/panelActivationPrompt.js +18 -0
  119. package/dist/web/panelAudit.js +273 -0
  120. package/dist/web/panelLifecycle.js +222 -0
  121. package/dist/web/panelMediaPolicy.js +43 -0
  122. package/dist/web/panelPathPolicy.js +63 -0
  123. package/dist/web/panelPreviews.js +175 -0
  124. package/dist/web/panelQueryHandles.js +2749 -0
  125. package/dist/web/panelRoutes.js +2147 -0
  126. package/dist/web/panels.js +904 -0
  127. package/dist/web/peerInboxAggregates.js +1247 -0
  128. package/dist/web/planApprovalState.js +92 -0
  129. package/dist/web/platformAgentScheduler.js +66 -0
  130. package/dist/web/proactiveOpportunities.js +452 -0
  131. package/dist/web/promptContextSections.js +242 -0
  132. package/dist/web/promptHistorySanitizer.js +26 -0
  133. package/dist/web/promptSlashCommands.js +158 -0
  134. package/dist/web/rollingConversationSummary.js +453 -0
  135. package/dist/web/routeHelpers.js +11 -0
  136. package/dist/web/routes/handoff.js +288 -0
  137. package/dist/web/routes/history.js +345 -0
  138. package/dist/web/routes/memory.js +258 -0
  139. package/dist/web/routes/selfState.js +171 -0
  140. package/dist/web/routes/workspace.js +154 -0
  141. package/dist/web/runSurfaceWatermarks.js +431 -0
  142. package/dist/web/runtimeCapabilities.js +48 -0
  143. package/dist/web/sameAgentHandoffs.js +494 -0
  144. package/dist/web/server.js +15567 -0
  145. package/dist/web/sharedCollaborationCapsules.js +163 -0
  146. package/dist/web/soloSessionRelay.js +42 -0
  147. package/dist/web/soloWsHandler.js +138 -0
  148. package/dist/web/suggestedPlannerScheduler.js +56 -0
  149. package/dist/web/surfaceActivationPolicy.js +108 -0
  150. package/dist/web/surfaceCollaborators.js +61 -0
  151. package/dist/web/surfaceSystemStatus.js +263 -0
  152. package/dist/web/targetParticipants.js +77 -0
  153. package/dist/web/taskEvents.js +49 -0
  154. package/dist/web/taskLifecycleMessages.js +165 -0
  155. package/dist/web/taskLoops.js +732 -0
  156. package/dist/web/taskMemoryNotes.js +224 -0
  157. package/dist/web/taskNumbers.js +16 -0
  158. package/dist/web/taskOwnerGuards.js +49 -0
  159. package/dist/web/taskParticipantResolver.js +42 -0
  160. package/dist/web/taskParticipants.js +97 -0
  161. package/dist/web/taskSourceDetails.js +20 -0
  162. package/dist/web/taskStateViews.js +210 -0
  163. package/dist/web/taskStatusTransitions.js +9 -0
  164. package/dist/web/taskThreadFollowups.js +599 -0
  165. package/dist/web/taskThreadRuntimeClosure.js +685 -0
  166. package/dist/web/taskUpdateDelivery.js +104 -0
  167. package/dist/web/threadReplyContentHeuristics.js +30 -0
  168. package/dist/web/threadRoots.js +61 -0
  169. package/dist/web/threadTaskBindings.js +365 -0
  170. package/dist/web/uiPanelPromptGuidance.js +27 -0
  171. package/dist/web/workspaceMemoryHints.js +143 -0
  172. package/dist/web/workspaceToolPromptGuidance.js +30 -0
  173. package/dist/web/wsHandler.js +397 -0
  174. package/dist/web/wsSink.js +116 -0
  175. package/package.json +54 -0
@@ -0,0 +1,288 @@
1
+ import { AgentVisibility } from '../agentVisibility.js';
2
+ import { formatConversationHandoff, getHandoffState, } from '../conversationHandoffs.js';
3
+ import { formatConversationSummary, refreshConversationSummariesForConversationIds, refreshConversationSummary, } from '../conversationSummaries.js';
4
+ import { resolveChannelFromTarget, resolveDefaultReplyTarget, resolveThreadRootId, } from '../conversationTargets.js';
5
+ import { findActiveConversationRunId } from '../routeHelpers.js';
6
+ import { startConversationHandoff } from '../sameAgentHandoffs.js';
7
+ import { getStaleTaskThreadReopenedRunBlock } from '../taskThreadRuntimeClosure.js';
8
+ import { getBoundTaskForThread } from '../threadTaskBindings.js';
9
+ import { buildRollingSummarySectionBody } from '../rollingConversationSummary.js';
10
+ function normalizeRequiredText(value) {
11
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
12
+ }
13
+ function normalizeOptionalText(value) {
14
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
15
+ }
16
+ function normalizeStringArray(value) {
17
+ if (!Array.isArray(value))
18
+ return [];
19
+ return value
20
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
21
+ .filter(Boolean);
22
+ }
23
+ function getCanonicalBoundTaskRef(boundTask) {
24
+ if (!boundTask)
25
+ return null;
26
+ const prefixedRef = typeof boundTask.agentTaskRef === 'string' ? boundTask.agentTaskRef.trim() : '';
27
+ return prefixedRef || null;
28
+ }
29
+ function annotateSourceSummaryForHandoff(params) {
30
+ if (!params.sourceSummary)
31
+ return null;
32
+ if (params.mode !== 'continue_there')
33
+ return params.sourceSummary;
34
+ const lines = params.sourceSummary.split('\n');
35
+ const statusLineIndex = lines.findIndex((line) => line.startsWith('Status: '));
36
+ if (statusLineIndex >= 0) {
37
+ lines[statusLineIndex] = `${lines[statusLineIndex]} (pre-handoff snapshot)`;
38
+ }
39
+ else {
40
+ lines.unshift('Handoff: continue_there (pre-handoff snapshot)');
41
+ }
42
+ return lines.join('\n');
43
+ }
44
+ export function registerAgentHandoffRoutes(params) {
45
+ const { app, db, conversationManager, humanUserName, broadcastConversationStatus } = params;
46
+ const visibility = new AgentVisibility(db);
47
+ app.post('/api/internal/agent/:agentId/handoff', async (req, reply) => {
48
+ const { agentId } = req.params;
49
+ const agent = conversationManager.getAgent(agentId);
50
+ if (!agent) {
51
+ reply.code(404);
52
+ return { error: 'Agent not found' };
53
+ }
54
+ const sourceConversationId = typeof req.body?.conversationId === 'string' ? req.body.conversationId.trim() : '';
55
+ const requestedTarget = typeof req.body?.target === 'string' ? req.body.target.trim() : '';
56
+ const mode = (typeof req.body?.mode === 'string' ? req.body.mode.trim() : 'delegate_only');
57
+ const goal = normalizeRequiredText(req.body?.goal);
58
+ if (!sourceConversationId) {
59
+ reply.code(400);
60
+ return { error: 'conversationId is required' };
61
+ }
62
+ if (!requestedTarget) {
63
+ reply.code(400);
64
+ return { error: 'target is required' };
65
+ }
66
+ if (!goal) {
67
+ reply.code(400);
68
+ return { error: 'goal is required' };
69
+ }
70
+ if (!['delegate_only', 'continue_there', 'collab'].includes(mode)) {
71
+ reply.code(400);
72
+ return { error: `Invalid mode: ${req.body?.mode}` };
73
+ }
74
+ const sourceConversation = conversationManager.getConversation(sourceConversationId);
75
+ if (!sourceConversation || sourceConversation.agentId !== agentId) {
76
+ reply.code(400);
77
+ return { error: 'conversationId does not belong to this agent' };
78
+ }
79
+ if (requestedTarget.startsWith('#')) {
80
+ const channelId = resolveChannelFromTarget(requestedTarget, db);
81
+ if (!channelId) {
82
+ reply.code(404);
83
+ return { error: 'Target channel not found' };
84
+ }
85
+ if (visibility.resolveVisibleChannelId(agent, requestedTarget) === null) {
86
+ reply.code(403);
87
+ return { error: 'Agent is not a member of this channel' };
88
+ }
89
+ }
90
+ else if (!visibility.canSeeTarget(agent, requestedTarget)) {
91
+ reply.code(403);
92
+ return { error: 'Agent cannot access this target' };
93
+ }
94
+ const sourceReplyTarget = (resolveDefaultReplyTarget(db, sourceConversationId, humanUserName) ?? sourceConversation.replyTarget ?? '').trim();
95
+ const sourceConversationRunId = findActiveConversationRunId(db, sourceConversationId);
96
+ const sourceThreadRootId = resolveThreadRootId(sourceReplyTarget) ?? sourceConversation.threadRootId ?? null;
97
+ const sourceChannelId = sourceConversation.threadKind === 'direct'
98
+ ? (sourceConversation.agentId ? `dm:${sourceConversation.agentId}` : null)
99
+ : (sourceConversation.channelId ?? null);
100
+ const sourceBoundTask = sourceChannelId && sourceThreadRootId
101
+ ? getBoundTaskForThread(db, { channelId: sourceChannelId, threadRootId: sourceThreadRootId })
102
+ : undefined;
103
+ const staleReopenedRun = getStaleTaskThreadReopenedRunBlock(db, {
104
+ conversationId: sourceConversationId,
105
+ runId: sourceConversationRunId,
106
+ taskId: sourceBoundTask?.taskId,
107
+ });
108
+ if (staleReopenedRun) {
109
+ reply.code(409);
110
+ return {
111
+ error: 'This task-thread was reopened by a user follow-up. Stop this old review-closing run instead of handing off stale context.',
112
+ };
113
+ }
114
+ const targetConversationId = visibility.ensureVisibleConversationId(agent, conversationManager, requestedTarget);
115
+ if (!targetConversationId) {
116
+ reply.code(404);
117
+ return { error: 'Conversation not found for the requested target' };
118
+ }
119
+ const targetConversation = conversationManager.getConversation(targetConversationId);
120
+ if (!targetConversation || targetConversation.agentId !== agentId) {
121
+ reply.code(404);
122
+ return { error: 'Target conversation not found' };
123
+ }
124
+ const targetReplyTarget = (resolveDefaultReplyTarget(db, targetConversationId, humanUserName) ?? targetConversation.replyTarget ?? '').trim();
125
+ if (!sourceReplyTarget || !targetReplyTarget) {
126
+ reply.code(400);
127
+ return { error: 'Unable to resolve handoff targets' };
128
+ }
129
+ if (sourceConversationId === targetConversationId || sourceReplyTarget === targetReplyTarget) {
130
+ reply.code(400);
131
+ return { error: 'Source and target must be different work surfaces' };
132
+ }
133
+ if (mode === 'continue_there' && !sourceConversationRunId) {
134
+ reply.code(409);
135
+ return { error: 'continue_there requires an active source run' };
136
+ }
137
+ const sourceSummary = refreshConversationSummary(db, agent, sourceConversationId);
138
+ const sourceSummaryBoundTask = sourceSummary?.summary.boundTask ?? null;
139
+ const handoffTaskId = sourceSummaryBoundTask?.taskId ?? sourceBoundTask?.taskId ?? null;
140
+ const handoffTaskNumber = sourceSummaryBoundTask?.taskNumber ?? sourceBoundTask?.taskNumber ?? null;
141
+ const explicitRelatedTaskRef = normalizeOptionalText(req.body?.context?.related_task_ref);
142
+ const sourceBoundTaskRef = getCanonicalBoundTaskRef(sourceSummaryBoundTask);
143
+ const sourceSummaryText = annotateSourceSummaryForHandoff({
144
+ mode,
145
+ sourceSummary: sourceSummary ? formatConversationSummary(sourceSummary) : null,
146
+ });
147
+ const sourceRollingSummary = buildRollingSummarySectionBody(db, sourceConversationId);
148
+ const payload = {
149
+ goal,
150
+ whyNow: normalizeOptionalText(req.body?.context?.why_now),
151
+ constraints: normalizeStringArray(req.body?.context?.constraints),
152
+ alreadyDone: normalizeStringArray(req.body?.context?.already_done),
153
+ expectedOutput: normalizeOptionalText(req.body?.context?.expected_output),
154
+ sourceTarget: sourceReplyTarget,
155
+ relatedTaskRef: explicitRelatedTaskRef ?? sourceBoundTaskRef,
156
+ optionalRefs: normalizeStringArray(req.body?.context?.optional_refs),
157
+ sourceSummary: sourceSummaryText,
158
+ sourceRollingSummary,
159
+ targetRunId: null,
160
+ cancelRunId: null,
161
+ completionMessageId: null,
162
+ error: null,
163
+ sourceContext: sourceSummary
164
+ ? {
165
+ lastUserMessage: sourceSummary.summary.lastUserMessage,
166
+ lastAgentMessage: sourceSummary.summary.lastAgentMessage,
167
+ boundTaskRef: sourceBoundTaskRef,
168
+ boundTaskTitle: sourceSummary.summary.boundTask?.title ?? null,
169
+ ownerName: sourceSummary.summary.ownerName,
170
+ participants: sourceSummary.summary.participants,
171
+ myRole: sourceSummary.summary.myRole,
172
+ }
173
+ : null,
174
+ };
175
+ const handoff = await startConversationHandoff({
176
+ agentId,
177
+ db,
178
+ conversationManager,
179
+ humanUserName,
180
+ sourceConversationId,
181
+ sourceConversationRunId,
182
+ sourceReplyTarget,
183
+ targetConversationId,
184
+ targetReplyTarget,
185
+ mode,
186
+ payload,
187
+ taskId: handoffTaskId,
188
+ taskNumber: handoffTaskNumber,
189
+ broadcastConversationStatus,
190
+ });
191
+ refreshConversationSummariesForConversationIds(db, (targetAgentId) => conversationManager.getAgent(targetAgentId), [sourceConversationId, targetConversationId]);
192
+ return {
193
+ handoff: {
194
+ handoffId: handoff.handoffId,
195
+ agentId,
196
+ status: handoff.status,
197
+ mode,
198
+ sourceConversationId,
199
+ sourceReplyTarget,
200
+ targetConversationId,
201
+ targetReplyTarget,
202
+ workflowKind: 'generic',
203
+ sourceDisposition: mode === 'continue_there' ? 'cancel_current_run' : 'keep_running',
204
+ taskId: handoffTaskId,
205
+ taskNumber: handoffTaskNumber,
206
+ payload,
207
+ queued: handoff.queued,
208
+ cancelRequested: handoff.cancelRequested,
209
+ ...(handoff.targetRunId ? { targetRunId: handoff.targetRunId } : {}),
210
+ ...(handoff.cancelRunId ? { cancelRunId: handoff.cancelRunId } : {}),
211
+ ...(handoff.error ? { error: handoff.error } : {}),
212
+ reportText: formatConversationHandoff({
213
+ handoffId: handoff.handoffId,
214
+ agentId,
215
+ sourceConversationId,
216
+ targetConversationId,
217
+ targetReplyTarget,
218
+ workflowKind: 'generic',
219
+ sourceDisposition: mode === 'continue_there' ? 'cancel_current_run' : 'keep_running',
220
+ taskId: handoffTaskId,
221
+ taskNumber: handoffTaskNumber,
222
+ mode,
223
+ status: handoff.status,
224
+ payload: {
225
+ ...payload,
226
+ ...(handoff.error ? { error: handoff.error } : {}),
227
+ },
228
+ createdAt: new Date().toISOString(),
229
+ updatedAt: new Date().toISOString(),
230
+ }),
231
+ },
232
+ };
233
+ });
234
+ app.get('/api/internal/agent/:agentId/handoffs/:handoffId', async (req, reply) => {
235
+ const { agentId, handoffId } = req.params;
236
+ if (!conversationManager.getAgent(agentId)) {
237
+ reply.code(404);
238
+ return { error: 'Agent not found' };
239
+ }
240
+ const state = getHandoffState(db, handoffId);
241
+ if (!state || state.agentId !== agentId) {
242
+ reply.code(404);
243
+ return { error: 'Handoff not found' };
244
+ }
245
+ return {
246
+ handoff: {
247
+ handoffId: state.handoffId,
248
+ agentId: state.agentId,
249
+ sourceConversationId: state.sourceConversationId,
250
+ sourceReplyTarget: state.payload.sourceTarget,
251
+ targetConversationId: state.targetConversationId,
252
+ targetReplyTarget: state.targetReplyTarget,
253
+ workflowKind: state.workflowKind,
254
+ sourceDisposition: state.sourceDisposition,
255
+ taskId: state.taskId,
256
+ taskNumber: state.taskNumber,
257
+ mode: state.mode,
258
+ status: state.status,
259
+ payload: state.payload,
260
+ createdAt: state.createdAt,
261
+ updatedAt: state.updatedAt,
262
+ runs: state.runs.map((r) => ({
263
+ runId: r.runId,
264
+ agentId: r.agentId,
265
+ disposition: r.disposition,
266
+ createdAt: r.createdAt,
267
+ completedAt: r.completedAt,
268
+ })),
269
+ reportText: formatConversationHandoff({
270
+ handoffId: state.handoffId,
271
+ agentId: state.agentId,
272
+ sourceConversationId: state.sourceConversationId,
273
+ targetConversationId: state.targetConversationId,
274
+ targetReplyTarget: state.targetReplyTarget,
275
+ workflowKind: state.workflowKind,
276
+ sourceDisposition: state.sourceDisposition,
277
+ taskId: state.taskId,
278
+ taskNumber: state.taskNumber,
279
+ mode: state.mode,
280
+ status: state.status,
281
+ payload: state.payload,
282
+ createdAt: state.createdAt,
283
+ updatedAt: state.updatedAt,
284
+ }),
285
+ },
286
+ };
287
+ });
288
+ }
@@ -0,0 +1,345 @@
1
+ import { AgentVisibility } from '../agentVisibility.js';
2
+ import { CLEARED_TASK_ROOT_TARGET_GLOB } from '../clearedTaskRoots.js';
3
+ import { findThreadRootMessageId } from '../threadRoots.js';
4
+ import { resolveChannelFromTarget, resolveThreadRootId } from '../conversationTargets.js';
5
+ import { buildFtsMatchQuery } from '../ftsQuery.js';
6
+ import { recordRunSurfaceCatchUpRowsSeen, recordRunSurfaceRowsSeen, resolveRunWatermarkIdentity, } from '../runSurfaceWatermarks.js';
7
+ import { advanceAgentSurfaceSeenThroughSeq } from '../collaborationSurfaceState.js';
8
+ function parseBoundedPositiveInt(value, fallback, max) {
9
+ const parsed = Number(value);
10
+ if (!Number.isFinite(parsed) || parsed <= 0)
11
+ return fallback;
12
+ return Math.min(Math.floor(parsed), max);
13
+ }
14
+ function parseOptionalPositiveInt(value) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ const parsed = Number(value);
18
+ if (!Number.isFinite(parsed) || parsed <= 0)
19
+ return undefined;
20
+ return Math.floor(parsed);
21
+ }
22
+ function buildMessageScope(alias, channelId, threadRootId, target) {
23
+ const params = [channelId];
24
+ if (threadRootId !== null) {
25
+ params.push(threadRootId);
26
+ if (target?.startsWith('dm:@')) {
27
+ params.push(target);
28
+ return {
29
+ clause: `${alias}.channel_id = ? AND (${alias}.thread_root_id = ? OR (${alias}.thread_root_id IS NULL AND ${alias}.target = ?))`,
30
+ params,
31
+ };
32
+ }
33
+ return {
34
+ clause: `${alias}.channel_id = ? AND ${alias}.thread_root_id = ?`,
35
+ params,
36
+ };
37
+ }
38
+ if (target?.startsWith('dm:@')) {
39
+ params.push(target);
40
+ return {
41
+ clause: `${alias}.channel_id = ? AND ${alias}.thread_root_id IS NULL AND ${alias}.target = ?`,
42
+ params,
43
+ };
44
+ }
45
+ params.push(CLEARED_TASK_ROOT_TARGET_GLOB);
46
+ return {
47
+ clause: `${alias}.channel_id = ? AND ${alias}.thread_root_id IS NULL AND ${alias}.target NOT GLOB ?`,
48
+ params,
49
+ };
50
+ }
51
+ function resolveReadableChannelId(db, visibility, agent, target) {
52
+ const visibleChannelId = visibility.resolveVisibleChannelId(agent, target);
53
+ if (visibleChannelId)
54
+ return { channelId: visibleChannelId };
55
+ if (target.startsWith('#')) {
56
+ const channelId = resolveChannelFromTarget(target, db);
57
+ if (!channelId)
58
+ return { channelId: null, errorCode: 400, error: `Cannot resolve channel: ${target}` };
59
+ return { channelId: null, errorCode: 403, error: 'Agent is not a member of this channel' };
60
+ }
61
+ return { channelId: null, errorCode: 403, error: 'Agent cannot access this target' };
62
+ }
63
+ export function registerAgentHistoryRoutes(app, db, conversationManager) {
64
+ const visibility = new AgentVisibility(db);
65
+ app.get('/api/internal/agent/:agentId/search', async (req, reply) => {
66
+ const { agentId } = req.params;
67
+ const agent = conversationManager.getAgent(agentId);
68
+ if (!agent) {
69
+ reply.code(404);
70
+ return { error: 'Agent not found' };
71
+ }
72
+ const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
73
+ if (!query) {
74
+ reply.code(400);
75
+ return { error: 'q query parameter is required' };
76
+ }
77
+ const ftsQuery = buildFtsMatchQuery(query, { prefix: true });
78
+ if (!ftsQuery) {
79
+ reply.code(400);
80
+ return { error: 'q query parameter is required' };
81
+ }
82
+ const limit = parseBoundedPositiveInt(req.query.limit, 10, 20);
83
+ const visibleChannelIds = Array.from(new Set([`dm:${agentId}`, ...(agent.channelIds ?? [])]));
84
+ const whereParts = [
85
+ `cm.channel_id IN (${visibleChannelIds.map(() => '?').join(', ')})`,
86
+ 'cm.target NOT GLOB ?',
87
+ ];
88
+ const params = [ftsQuery, ...visibleChannelIds, CLEARED_TASK_ROOT_TARGET_GLOB];
89
+ const channelTarget = typeof req.query.channel === 'string' ? req.query.channel.trim() : '';
90
+ if (channelTarget) {
91
+ const resolved = resolveReadableChannelId(db, visibility, agent, channelTarget);
92
+ if (!resolved.channelId) {
93
+ reply.code(resolved.errorCode ?? 400);
94
+ return { error: resolved.error ?? `Cannot resolve channel: ${channelTarget}` };
95
+ }
96
+ const threadRootId = resolveThreadRootId(channelTarget);
97
+ if (threadRootId !== null || channelTarget.startsWith('dm:@')) {
98
+ const scope = buildMessageScope('cm', resolved.channelId, threadRootId, channelTarget);
99
+ whereParts.push(scope.clause);
100
+ params.push(...scope.params);
101
+ }
102
+ else {
103
+ whereParts.push('cm.channel_id = ?');
104
+ params.push(resolved.channelId);
105
+ }
106
+ }
107
+ const rows = db.prepare(`SELECT
108
+ cm.message_id as messageId,
109
+ cm.channel_id as channelId,
110
+ cm.sender_id as senderId,
111
+ cm.sender_name as senderName,
112
+ cm.sender_type as senderType,
113
+ cm.target,
114
+ cm.content,
115
+ cm.seq,
116
+ cm.created_at as createdAt,
117
+ snippet(channel_messages_fts, 4, '[', ']', '...', 12) as snippet
118
+ FROM channel_messages_fts
119
+ JOIN channel_messages cm ON cm.message_id = channel_messages_fts.message_id
120
+ WHERE channel_messages_fts MATCH ?
121
+ AND ${whereParts.join(' AND ')}
122
+ ORDER BY bm25(channel_messages_fts), cm.created_at DESC
123
+ LIMIT ?`).all(...params, limit);
124
+ return {
125
+ results: rows.map((row) => ({
126
+ id: row.messageId,
127
+ target: row.target,
128
+ senderName: row.senderName,
129
+ senderType: row.senderType,
130
+ content: row.content,
131
+ seq: row.seq,
132
+ createdAt: new Date(row.createdAt).toISOString(),
133
+ snippet: row.snippet ?? row.content,
134
+ })),
135
+ };
136
+ });
137
+ app.get('/api/internal/agent/:agentId/history', async (req, reply) => {
138
+ const { agentId } = req.params;
139
+ const agent = conversationManager.getAgent(agentId);
140
+ if (!agent) {
141
+ reply.code(404);
142
+ return { error: 'Agent not found' };
143
+ }
144
+ const { channel, limit: limitStr, around: aroundStr, before: beforeStr, after: afterStr, include_root: includeRootStr, runId: runIdStr, conversationId: conversationIdStr, } = req.query;
145
+ if (!channel) {
146
+ reply.code(400);
147
+ return { error: 'channel query parameter is required' };
148
+ }
149
+ const runId = runIdStr?.trim() || null;
150
+ const conversationId = conversationIdStr?.trim() || null;
151
+ if (conversationId) {
152
+ const conversation = conversationManager.getConversation(conversationId);
153
+ if (!conversation || conversation.agentId !== agentId) {
154
+ reply.code(400);
155
+ return { error: 'conversationId does not belong to this agent' };
156
+ }
157
+ }
158
+ const watermarkIdentity = resolveRunWatermarkIdentity(db, agentId, { runId, conversationId });
159
+ if (runId && !watermarkIdentity) {
160
+ reply.code(400);
161
+ return { error: 'runId does not belong to this agent conversation' };
162
+ }
163
+ const resolved = resolveReadableChannelId(db, visibility, agent, channel);
164
+ if (!resolved.channelId) {
165
+ reply.code(resolved.errorCode ?? 400);
166
+ return { error: resolved.error ?? `Cannot resolve channel: ${channel}` };
167
+ }
168
+ const limit = parseBoundedPositiveInt(limitStr, 50, 100);
169
+ const before = parseOptionalPositiveInt(beforeStr);
170
+ const after = parseOptionalPositiveInt(afterStr);
171
+ const around = typeof aroundStr === 'string' ? aroundStr.trim() : '';
172
+ const includeRoot = typeof includeRootStr === 'string' && ['1', 'true', 'yes'].includes(includeRootStr.trim().toLowerCase());
173
+ if (around && (before !== undefined || after !== undefined)) {
174
+ reply.code(400);
175
+ return { error: 'around cannot be combined with before or after' };
176
+ }
177
+ const targetThreadRootId = resolveThreadRootId(channel);
178
+ const scope = buildMessageScope('cm', resolved.channelId, targetThreadRootId, channel);
179
+ const taskJoinSelect = `cm.message_id as messageId, cm.channel_id as channelId, cm.sender_id as senderId,
180
+ cm.sender_name as senderName, cm.sender_type as senderType,
181
+ cm.target, cm.content, cm.seq, cm.created_at as createdAt, cm.thread_root_id as threadRootId,
182
+ t.task_number as taskNumber, t.status as taskStatus, t.claimed_by_name as taskAssigneeName`;
183
+ const taskJoin = `LEFT JOIN tasks t ON t.message_id = cm.message_id`;
184
+ const selectRows = (extraWhere, extraParams, order, rowLimit) => db
185
+ .prepare(`SELECT ${taskJoinSelect}
186
+ FROM channel_messages cm ${taskJoin}
187
+ WHERE ${scope.clause}${extraWhere ? ` AND ${extraWhere}` : ''}
188
+ ORDER BY cm.seq ${order} LIMIT ?`)
189
+ .all(...scope.params, ...extraParams, rowLimit);
190
+ const findAroundAnchorSeq = () => {
191
+ if (!around)
192
+ return null;
193
+ if (/^\d+$/.test(around)) {
194
+ const row = db.prepare(`SELECT cm.seq as seq
195
+ FROM channel_messages cm
196
+ WHERE ${scope.clause} AND cm.seq = ?
197
+ LIMIT 1`).get(...scope.params, Number(around));
198
+ return row?.seq ?? null;
199
+ }
200
+ const row = db.prepare(`SELECT cm.seq as seq
201
+ FROM channel_messages cm
202
+ WHERE ${scope.clause} AND cm.message_id LIKE ?
203
+ ORDER BY cm.seq ASC
204
+ LIMIT 1`).get(...scope.params, `${around}%`);
205
+ return row?.seq ?? null;
206
+ };
207
+ let rows;
208
+ if (around) {
209
+ const anchorSeq = findAroundAnchorSeq();
210
+ if (anchorSeq === null) {
211
+ reply.code(404);
212
+ return { error: `Cannot resolve message around ${around}` };
213
+ }
214
+ const beforeBase = Math.floor((limit - 1) / 2);
215
+ const afterBase = Math.max(limit - beforeBase - 1, 0);
216
+ let beforeRows = selectRows('cm.seq < ?', [anchorSeq], 'DESC', beforeBase);
217
+ let afterRows = selectRows('cm.seq > ?', [anchorSeq], 'ASC', afterBase);
218
+ if (beforeRows.length < beforeBase) {
219
+ afterRows = selectRows('cm.seq > ?', [anchorSeq], 'ASC', afterBase + (beforeBase - beforeRows.length));
220
+ }
221
+ if (afterRows.length < afterBase) {
222
+ beforeRows = selectRows('cm.seq < ?', [anchorSeq], 'DESC', beforeBase + (afterBase - afterRows.length));
223
+ }
224
+ const anchorRows = selectRows('cm.seq = ?', [anchorSeq], 'ASC', 1);
225
+ rows = beforeRows.reverse().concat(anchorRows, afterRows);
226
+ }
227
+ else if (after !== undefined) {
228
+ rows = selectRows('cm.seq > ?', [after], 'ASC', limit);
229
+ }
230
+ else if (before !== undefined) {
231
+ rows = selectRows('cm.seq < ?', [before], 'DESC', limit).reverse();
232
+ }
233
+ else {
234
+ rows = selectRows('', [], 'DESC', limit).reverse();
235
+ }
236
+ if (after !== undefined) {
237
+ recordRunSurfaceCatchUpRowsSeen(db, {
238
+ identity: watermarkIdentity,
239
+ agentId,
240
+ channelId: resolved.channelId,
241
+ threadRootId: targetThreadRootId,
242
+ afterSeq: after,
243
+ rows,
244
+ source: 'read_history',
245
+ });
246
+ }
247
+ else if (around || before === undefined) {
248
+ recordRunSurfaceRowsSeen(db, {
249
+ identity: watermarkIdentity,
250
+ agentId,
251
+ rows,
252
+ source: 'read_history',
253
+ threadRootIdOverride: targetThreadRootId,
254
+ });
255
+ }
256
+ if (rows.length > 0) {
257
+ const maxSeqBySurface = new Map();
258
+ for (const row of rows) {
259
+ const effectiveThreadRootId = row.threadRootId ?? targetThreadRootId;
260
+ const key = `${row.channelId}::${effectiveThreadRootId ?? ''}`;
261
+ const current = maxSeqBySurface.get(key);
262
+ if (!current || row.seq > current.maxSeq) {
263
+ maxSeqBySurface.set(key, {
264
+ channelId: row.channelId,
265
+ threadRootId: effectiveThreadRootId,
266
+ maxSeq: row.seq,
267
+ });
268
+ }
269
+ }
270
+ for (const surface of maxSeqBySurface.values()) {
271
+ advanceAgentSurfaceSeenThroughSeq(db, {
272
+ agentId,
273
+ channelId: surface.channelId,
274
+ threadRootId: surface.threadRootId,
275
+ seenThroughSeq: surface.maxSeq,
276
+ });
277
+ }
278
+ }
279
+ const hasOlder = rows.length > 0
280
+ ? !!db.prepare(`SELECT 1
281
+ FROM channel_messages cm
282
+ WHERE ${scope.clause} AND cm.seq < ?
283
+ LIMIT 1`).get(...scope.params, rows[0].seq)
284
+ : false;
285
+ const hasNewer = rows.length > 0
286
+ ? !!db.prepare(`SELECT 1
287
+ FROM channel_messages cm
288
+ WHERE ${scope.clause} AND cm.seq > ?
289
+ LIMIT 1`).get(...scope.params, rows[rows.length - 1].seq)
290
+ : false;
291
+ const messages = rows.map((r) => {
292
+ const ext = r;
293
+ return {
294
+ id: r.messageId,
295
+ target: r.target,
296
+ senderName: r.senderName,
297
+ senderType: r.senderType,
298
+ content: r.content,
299
+ seq: r.seq,
300
+ createdAt: new Date(r.createdAt).toISOString(),
301
+ ...(ext.taskNumber != null ? {
302
+ taskNumber: ext.taskNumber,
303
+ taskStatus: ext.taskStatus,
304
+ taskAssigneeName: ext.taskAssigneeName,
305
+ } : {}),
306
+ };
307
+ });
308
+ if (includeRoot && targetThreadRootId) {
309
+ const rootMessageId = findThreadRootMessageId(db, resolved.channelId, targetThreadRootId);
310
+ if (rootMessageId && !messages.some((message) => message.id === rootMessageId)) {
311
+ const rootRow = db.prepare(`SELECT ${taskJoinSelect}
312
+ FROM channel_messages cm ${taskJoin}
313
+ WHERE cm.channel_id = ?
314
+ AND cm.message_id = ?
315
+ AND cm.target NOT GLOB ?
316
+ LIMIT 1`).get(resolved.channelId, rootMessageId, CLEARED_TASK_ROOT_TARGET_GLOB);
317
+ if (rootRow) {
318
+ messages.unshift({
319
+ id: rootRow.messageId,
320
+ target: rootRow.target,
321
+ senderName: rootRow.senderName,
322
+ senderType: rootRow.senderType,
323
+ content: rootRow.content,
324
+ seq: rootRow.seq,
325
+ createdAt: new Date(rootRow.createdAt).toISOString(),
326
+ ...(rootRow.taskNumber != null ? {
327
+ taskNumber: rootRow.taskNumber,
328
+ taskStatus: rootRow.taskStatus,
329
+ taskAssigneeName: rootRow.taskAssigneeName,
330
+ } : {}),
331
+ });
332
+ }
333
+ }
334
+ }
335
+ return {
336
+ channel,
337
+ messages,
338
+ hasOlder,
339
+ hasNewer,
340
+ has_older: hasOlder,
341
+ has_newer: hasNewer,
342
+ has_more: hasOlder || hasNewer,
343
+ };
344
+ });
345
+ }