@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,92 @@
1
+ export const DM_HANDOFF_BOOTSTRAP_STOP_REASON = 'handoff_bootstrap';
2
+ export function hasRunPendingApprovalRequest(db, runId) {
3
+ const row = db.prepare(`SELECT 1 as found
4
+ FROM events approval
5
+ WHERE approval.run_id = ?
6
+ AND approval.method = 'node/event'
7
+ AND json_valid(approval.payload_json)
8
+ AND json_extract(approval.payload_json, '$.type') = 'approval.request'
9
+ AND NOT EXISTS (
10
+ SELECT 1
11
+ FROM events later
12
+ WHERE later.run_id = approval.run_id
13
+ AND later.seq > approval.seq
14
+ AND json_valid(later.payload_json)
15
+ AND (
16
+ (
17
+ later.method = 'approval/response'
18
+ AND json_extract(later.payload_json, '$.type') = 'approval.response'
19
+ AND json_extract(later.payload_json, '$.requestId') = json_extract(approval.payload_json, '$.requestId')
20
+ )
21
+ OR (
22
+ later.method = 'node/event'
23
+ AND json_extract(later.payload_json, '$.type') IN (
24
+ 'content.delta',
25
+ 'activity.delta',
26
+ 'thinking.delta',
27
+ 'tool.call',
28
+ 'tool.result',
29
+ 'plan.update',
30
+ 'plan.phase',
31
+ 'task.update',
32
+ 'runtime.compact'
33
+ )
34
+ )
35
+ )
36
+ )
37
+ LIMIT 1`).get(runId);
38
+ return Boolean(row);
39
+ }
40
+ export function getSnapshotConversationStatus(db, params) {
41
+ return params.hasPendingApproval || hasRunPendingApprovalRequest(db, params.runId)
42
+ ? 'awaiting_approval'
43
+ : 'active';
44
+ }
45
+ export function hasRunPlanImplementationStarted(db, runId) {
46
+ const row = db.prepare(`SELECT 1 as found
47
+ FROM events
48
+ WHERE run_id = ?
49
+ AND method = 'node/event'
50
+ AND json_valid(payload_json)
51
+ AND json_extract(payload_json, '$.type') = 'plan.phase'
52
+ AND json_extract(payload_json, '$.phase') = 'implementation'
53
+ LIMIT 1`).get(runId);
54
+ return Boolean(row);
55
+ }
56
+ function hasRunApprovedImplementResponse(db, runId) {
57
+ const row = db.prepare(`SELECT 1 as found
58
+ FROM events
59
+ WHERE run_id = ?
60
+ AND method = 'approval/response'
61
+ AND json_valid(payload_json)
62
+ AND json_extract(payload_json, '$.type') = 'approval.response'
63
+ AND json_extract(payload_json, '$.decision') = 'allow'
64
+ AND json_extract(payload_json, '$.selectedActionId') = 'implement'
65
+ LIMIT 1`).get(runId);
66
+ return Boolean(row);
67
+ }
68
+ // Only use this to relax the user-visible send_message guard for the same run.
69
+ // Runtime/bootstrap state transitions must still rely on hasRunPlanImplementationStarted().
70
+ export function hasRunPlanSendMessageUnlocked(db, runId) {
71
+ return hasRunPlanImplementationStarted(db, runId) || hasRunApprovedImplementResponse(db, runId);
72
+ }
73
+ export function getExpectedBootstrapStopReason(db, runId) {
74
+ const row = db.prepare(`SELECT activation_metadata_json as activationMetadataJson
75
+ FROM run_debug_inputs
76
+ WHERE run_id = ?
77
+ LIMIT 1`).get(runId);
78
+ if (!row?.activationMetadataJson)
79
+ return null;
80
+ try {
81
+ const parsed = JSON.parse(row.activationMetadataJson);
82
+ if (parsed?.expectedTermination?.kind === 'dm_handoff_bootstrap'
83
+ && parsed.expectedTermination.stopReason === DM_HANDOFF_BOOTSTRAP_STOP_REASON
84
+ && !hasRunPlanImplementationStarted(db, runId)) {
85
+ return DM_HANDOFF_BOOTSTRAP_STOP_REASON;
86
+ }
87
+ }
88
+ catch {
89
+ // Ignore malformed activation metadata; live dispatch still owns the source of truth.
90
+ }
91
+ return null;
92
+ }
@@ -0,0 +1,66 @@
1
+ import { getPlatformWorkflowTerminalError } from '../services/platformAgentService.js';
2
+ export const PLATFORM_AGENT_POLL_INTERVAL_MS = 60_000;
3
+ export function startPlatformAgentScheduler(params) {
4
+ const intervalMs = Math.max(5_000, params.intervalMs ?? PLATFORM_AGENT_POLL_INTERVAL_MS);
5
+ let closed = false;
6
+ let running = false;
7
+ let timer = null;
8
+ const tick = async () => {
9
+ if (closed || running)
10
+ return;
11
+ running = true;
12
+ try {
13
+ const now = Date.now();
14
+ for (const instance of params.platformAgentService.listDueInstances(now)) {
15
+ const username = params.platformAgentService.getUsernameForUserId(instance.userId);
16
+ if (!username)
17
+ continue;
18
+ try {
19
+ await params.platformAgentService.dispatchWorkflowRun({
20
+ userId: instance.userId,
21
+ username,
22
+ definitionId: instance.definitionId,
23
+ });
24
+ }
25
+ catch (error) {
26
+ console.error('[platform-agent-scheduler] dispatch failed', {
27
+ userId: instance.userId,
28
+ definitionId: instance.definitionId,
29
+ error: String(error?.message ?? error),
30
+ });
31
+ }
32
+ }
33
+ }
34
+ finally {
35
+ running = false;
36
+ }
37
+ };
38
+ timer = setInterval(() => {
39
+ void tick();
40
+ }, intervalMs);
41
+ timer.unref?.();
42
+ return {
43
+ tick,
44
+ stop: () => {
45
+ closed = true;
46
+ if (!timer)
47
+ return;
48
+ clearInterval(timer);
49
+ timer = null;
50
+ },
51
+ };
52
+ }
53
+ export async function handlePlatformAgentRunFinished(params) {
54
+ const row = params.db.prepare(`SELECT pad.workflow_type as workflowType,
55
+ par.status as status
56
+ FROM platform_agent_runs par
57
+ JOIN platform_agent_definitions pad ON pad.definition_id = par.definition_id
58
+ WHERE par.run_id = ?`).get(params.runId);
59
+ if (!row || row.status !== 'running')
60
+ return;
61
+ const terminalError = getPlatformWorkflowTerminalError(row.workflowType, params.stopReason, params.error);
62
+ await params.platformAgentService.finalizeRun({
63
+ runId: params.runId,
64
+ error: terminalError,
65
+ });
66
+ }
@@ -0,0 +1,452 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { buildThreadShortId, } from '@bbigbang/protocol';
3
+ export const PROACTIVE_REVIEW_WAITING_THRESHOLD_MS = 24 * 60 * 60 * 1000;
4
+ export const PROACTIVE_BLOCKED_WAITING_THRESHOLD_MS = 24 * 60 * 60 * 1000;
5
+ export const PROACTIVE_UNASSIGNED_TODO_THRESHOLD_MS = 48 * 60 * 60 * 1000;
6
+ export const PROACTIVE_STALE_IN_PROGRESS_THRESHOLD_MS = 72 * 60 * 60 * 1000;
7
+ export const DEFAULT_PROACTIVE_SNOOZE_MS = 24 * 60 * 60 * 1000;
8
+ const TASK_OPPORTUNITY_KINDS = [
9
+ 'task_review_waiting',
10
+ 'task_blocked_waiting',
11
+ 'task_unassigned_todo',
12
+ 'task_stale_in_progress',
13
+ ];
14
+ function parseTimestamp(value) {
15
+ if (typeof value === 'number' && Number.isFinite(value))
16
+ return value;
17
+ if (typeof value !== 'string')
18
+ return null;
19
+ const parsed = Number(value.trim());
20
+ return Number.isFinite(parsed) ? parsed : null;
21
+ }
22
+ function isTaskBlocked(task) {
23
+ return Boolean(task.blockedAt ?? task.blockedBy ?? task.blockedReason);
24
+ }
25
+ function isTaskUnassigned(task) {
26
+ return !task.assigneeId && !task.assigneeName && task.participantCount === 0;
27
+ }
28
+ function taskSourceLabel(task) {
29
+ if (task.sourceType === 'dm')
30
+ return `DM · @${task.agentName ?? task.agentId ?? 'agent'}`;
31
+ return `Channel · #${task.channelName ?? task.channelId}`;
32
+ }
33
+ function buildDedupeKey(task, kind, referenceAt) {
34
+ return `task:${task.taskId}:${kind}:${referenceAt}`;
35
+ }
36
+ function evaluateTaskOpportunity(task, now) {
37
+ if (task.status === 'done')
38
+ return null;
39
+ if (isTaskBlocked(task)) {
40
+ const referenceAt = parseTimestamp(task.blockedAt) ?? task.updatedAt ?? task.createdAt;
41
+ const ageMs = now - referenceAt;
42
+ if (ageMs >= PROACTIVE_BLOCKED_WAITING_THRESHOLD_MS) {
43
+ const kind = 'task_blocked_waiting';
44
+ return {
45
+ kind,
46
+ dedupeKey: buildDedupeKey(task, kind, referenceAt),
47
+ title: `Blocked task waiting: ${task.title}`,
48
+ rationale: `${taskSourceLabel(task)} task #${task.taskNumber} has been blocked for at least 24h.`,
49
+ referenceAt,
50
+ thresholdMs: PROACTIVE_BLOCKED_WAITING_THRESHOLD_MS,
51
+ ageMs,
52
+ };
53
+ }
54
+ return null;
55
+ }
56
+ if (task.status === 'in_review') {
57
+ const referenceAt = task.updatedAt ?? task.createdAt;
58
+ const ageMs = now - referenceAt;
59
+ if (ageMs >= PROACTIVE_REVIEW_WAITING_THRESHOLD_MS) {
60
+ const kind = 'task_review_waiting';
61
+ return {
62
+ kind,
63
+ dedupeKey: buildDedupeKey(task, kind, referenceAt),
64
+ title: `Review waiting: ${task.title}`,
65
+ rationale: `${taskSourceLabel(task)} task #${task.taskNumber} has been in review for at least 24h.`,
66
+ referenceAt,
67
+ thresholdMs: PROACTIVE_REVIEW_WAITING_THRESHOLD_MS,
68
+ ageMs,
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+ if (task.status === 'todo' && isTaskUnassigned(task)) {
74
+ const referenceAt = task.updatedAt ?? task.createdAt;
75
+ const ageMs = now - referenceAt;
76
+ if (ageMs >= PROACTIVE_UNASSIGNED_TODO_THRESHOLD_MS) {
77
+ const kind = 'task_unassigned_todo';
78
+ return {
79
+ kind,
80
+ dedupeKey: buildDedupeKey(task, kind, referenceAt),
81
+ title: `Unassigned TODO: ${task.title}`,
82
+ rationale: `${taskSourceLabel(task)} task #${task.taskNumber} has been unclaimed for at least 48h.`,
83
+ referenceAt,
84
+ thresholdMs: PROACTIVE_UNASSIGNED_TODO_THRESHOLD_MS,
85
+ ageMs,
86
+ };
87
+ }
88
+ return null;
89
+ }
90
+ if (task.status === 'in_progress') {
91
+ const referenceAt = task.updatedAt ?? task.createdAt;
92
+ const ageMs = now - referenceAt;
93
+ if (ageMs >= PROACTIVE_STALE_IN_PROGRESS_THRESHOLD_MS) {
94
+ const kind = 'task_stale_in_progress';
95
+ return {
96
+ kind,
97
+ dedupeKey: buildDedupeKey(task, kind, referenceAt),
98
+ title: `Stale in-progress task: ${task.title}`,
99
+ rationale: `${taskSourceLabel(task)} task #${task.taskNumber} has had no task update for at least 72h.`,
100
+ referenceAt,
101
+ thresholdMs: PROACTIVE_STALE_IN_PROGRESS_THRESHOLD_MS,
102
+ ageMs,
103
+ };
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+ function buildEvidence(task, candidate) {
109
+ return {
110
+ taskId: task.taskId,
111
+ taskNumber: task.taskNumber,
112
+ sourceType: task.sourceType,
113
+ status: task.status,
114
+ referenceAt: candidate.referenceAt,
115
+ thresholdMs: candidate.thresholdMs,
116
+ ageMs: Math.max(0, candidate.ageMs),
117
+ assigneeId: task.assigneeId ?? null,
118
+ assigneeName: task.assigneeName ?? null,
119
+ participantCount: task.participantCount,
120
+ blockedReason: task.blockedReason ?? null,
121
+ blockedAt: task.blockedAt ?? null,
122
+ blockedBy: task.blockedBy ?? null,
123
+ };
124
+ }
125
+ function insertOrRefreshOpportunity(db, params) {
126
+ const evidenceJson = JSON.stringify(buildEvidence(params.task, params.candidate));
127
+ db.prepare(`INSERT OR IGNORE INTO proactive_opportunities(
128
+ opportunity_id, user_id, kind, status, dedupe_key, task_id, source_type,
129
+ channel_id, agent_id, title, rationale, evidence_json,
130
+ created_at, updated_at
131
+ )
132
+ VALUES(?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(randomUUID(), params.userId, params.candidate.kind, params.candidate.dedupeKey, params.task.taskId, params.task.sourceType, params.task.channelId, params.task.agentId ?? null, params.candidate.title, params.candidate.rationale, evidenceJson, params.now, params.now);
133
+ db.prepare(`UPDATE proactive_opportunities
134
+ SET task_id = ?,
135
+ source_type = ?,
136
+ channel_id = ?,
137
+ agent_id = ?,
138
+ title = ?,
139
+ rationale = ?,
140
+ evidence_json = ?,
141
+ updated_at = ?
142
+ WHERE user_id = ?
143
+ AND dedupe_key = ?
144
+ AND status = 'open'`).run(params.task.taskId, params.task.sourceType, params.task.channelId, params.task.agentId ?? null, params.candidate.title, params.candidate.rationale, evidenceJson, params.now, params.userId, params.candidate.dedupeKey);
145
+ }
146
+ function resolveInactiveTaskOpportunities(db, params) {
147
+ const kindPlaceholders = TASK_OPPORTUNITY_KINDS.map(() => '?').join(', ');
148
+ if (params.activeDedupeKeys.length === 0) {
149
+ db.prepare(`UPDATE proactive_opportunities
150
+ SET status = 'resolved',
151
+ resolved_at = COALESCE(resolved_at, ?),
152
+ updated_at = ?
153
+ WHERE user_id = ?
154
+ AND status IN ('open', 'snoozed')
155
+ AND kind IN (${kindPlaceholders})`).run(params.now, params.now, params.userId, ...TASK_OPPORTUNITY_KINDS);
156
+ return;
157
+ }
158
+ const activePlaceholders = params.activeDedupeKeys.map(() => '?').join(', ');
159
+ db.prepare(`UPDATE proactive_opportunities
160
+ SET status = 'resolved',
161
+ resolved_at = COALESCE(resolved_at, ?),
162
+ updated_at = ?
163
+ WHERE user_id = ?
164
+ AND status IN ('open', 'snoozed')
165
+ AND kind IN (${kindPlaceholders})
166
+ AND dedupe_key NOT IN (${activePlaceholders})`).run(params.now, params.now, params.userId, ...TASK_OPPORTUNITY_KINDS, ...params.activeDedupeKeys);
167
+ }
168
+ export function scanProactiveTaskOpportunities(db, params) {
169
+ const now = params.now ?? Date.now();
170
+ db.transaction(() => {
171
+ db.prepare(`UPDATE proactive_opportunities
172
+ SET status = 'open',
173
+ snoozed_until = NULL,
174
+ updated_at = ?
175
+ WHERE user_id = ?
176
+ AND status = 'snoozed'
177
+ AND snoozed_until IS NOT NULL
178
+ AND snoozed_until <= ?`).run(now, params.userId, now);
179
+ const activeDedupeKeys = [];
180
+ for (const task of params.tasks) {
181
+ const candidate = evaluateTaskOpportunity(task, now);
182
+ if (!candidate)
183
+ continue;
184
+ activeDedupeKeys.push(candidate.dedupeKey);
185
+ insertOrRefreshOpportunity(db, {
186
+ userId: params.userId,
187
+ task,
188
+ candidate,
189
+ now,
190
+ });
191
+ }
192
+ resolveInactiveTaskOpportunities(db, {
193
+ userId: params.userId,
194
+ activeDedupeKeys,
195
+ now,
196
+ });
197
+ })();
198
+ }
199
+ function parseEvidence(value) {
200
+ try {
201
+ const parsed = JSON.parse(value);
202
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
203
+ ? parsed
204
+ : {};
205
+ }
206
+ catch {
207
+ return {};
208
+ }
209
+ }
210
+ function parseEvidenceLinks(evidence) {
211
+ const links = evidence.evidenceLinks;
212
+ if (!Array.isArray(links))
213
+ return [];
214
+ return links
215
+ .map((item) => {
216
+ if (!item || typeof item !== 'object')
217
+ return null;
218
+ const record = item;
219
+ const label = typeof record.label === 'string' ? record.label.trim() : '';
220
+ const url = typeof record.url === 'string' ? record.url.trim() : '';
221
+ const summary = typeof record.summary === 'string' ? record.summary.trim() : '';
222
+ if (!label || !url)
223
+ return null;
224
+ return { label, url, ...(summary ? { summary } : {}) };
225
+ })
226
+ .filter((item) => item != null);
227
+ }
228
+ function mapOpportunityRow(row) {
229
+ const linkedThreadId = row.messageId ? buildThreadShortId(row.messageId) : null;
230
+ const canAskUpdate = row.kind !== 'task_unassigned_todo'
231
+ && Boolean(row.messageId)
232
+ && (row.sourceType === 'dm'
233
+ ? Boolean(row.taskAgentId && row.dmTarget)
234
+ : Boolean(row.assigneeId));
235
+ const actions = canAskUpdate ? ['ask_update'] : [];
236
+ return {
237
+ opportunityId: row.opportunityId,
238
+ kind: row.kind,
239
+ status: row.status,
240
+ actions,
241
+ dedupeKey: row.dedupeKey,
242
+ sourceType: row.sourceType,
243
+ title: row.title,
244
+ rationale: row.rationale,
245
+ evidence: parseEvidence(row.evidenceJson),
246
+ task: {
247
+ taskId: row.taskId,
248
+ ...(row.taskRef != null ? { taskRef: row.taskRef } : {}),
249
+ sourceType: row.sourceType,
250
+ channelId: row.taskChannelId,
251
+ channelName: row.channelName,
252
+ agentId: row.taskAgentId,
253
+ agentName: row.agentName,
254
+ taskNumber: row.taskNumber,
255
+ title: row.taskTitle,
256
+ ...(row.description != null ? { description: row.description } : {}),
257
+ status: row.taskStatus,
258
+ assigneeId: row.assigneeId,
259
+ assigneeName: row.assigneeName,
260
+ messageId: row.messageId,
261
+ ...(linkedThreadId ? { linkedThreadId, linkedThreadShortId: linkedThreadId } : {}),
262
+ blockedReason: row.blockedReason,
263
+ blockedAt: row.blockedAt,
264
+ blockedBy: row.blockedBy,
265
+ createdAt: row.taskCreatedAt,
266
+ updatedAt: row.taskUpdatedAt,
267
+ },
268
+ snoozedUntil: row.snoozedUntil,
269
+ dismissedAt: row.dismissedAt,
270
+ resolvedAt: row.resolvedAt,
271
+ createdAt: row.createdAt,
272
+ updatedAt: row.updatedAt,
273
+ };
274
+ }
275
+ function mapGenericOpportunityRow(row) {
276
+ const parsedEvidence = parseEvidence(row.evidenceJson);
277
+ const targetType = row.agentId || row.channelId?.startsWith('dm:')
278
+ ? 'dm'
279
+ : row.channelId
280
+ ? 'channel'
281
+ : (typeof parsedEvidence.targetType === 'string' ? parsedEvidence.targetType : 'general');
282
+ const evidence = {
283
+ ...parsedEvidence,
284
+ targetType,
285
+ ...(row.channelId ? { channelId: parsedEvidence.channelId ?? row.channelId } : {}),
286
+ ...(row.agentId ? { agentId: parsedEvidence.agentId ?? row.agentId } : {}),
287
+ };
288
+ return {
289
+ opportunityId: row.opportunityId,
290
+ kind: row.kind,
291
+ status: row.status,
292
+ actions: row.kind === 'planner_task_candidate' && (row.channelId || row.agentId) ? ['create_task'] : [],
293
+ dedupeKey: row.dedupeKey,
294
+ sourceType: row.sourceType,
295
+ title: row.title,
296
+ rationale: row.rationale,
297
+ evidence,
298
+ evidenceLinks: parseEvidenceLinks(evidence),
299
+ snoozedUntil: row.snoozedUntil,
300
+ dismissedAt: row.dismissedAt,
301
+ resolvedAt: row.resolvedAt,
302
+ createdAt: row.createdAt,
303
+ updatedAt: row.updatedAt,
304
+ };
305
+ }
306
+ export function listOpenProactiveOpportunities(db, params) {
307
+ const taskRows = db.prepare(`SELECT po.opportunity_id as opportunityId,
308
+ po.kind as kind,
309
+ po.status as status,
310
+ po.dedupe_key as dedupeKey,
311
+ po.source_type as sourceType,
312
+ po.title as title,
313
+ po.rationale as rationale,
314
+ po.evidence_json as evidenceJson,
315
+ po.snoozed_until as snoozedUntil,
316
+ po.dismissed_at as dismissedAt,
317
+ po.resolved_at as resolvedAt,
318
+ po.created_at as createdAt,
319
+ po.updated_at as updatedAt,
320
+ t.task_id as taskId,
321
+ t.agent_task_ref as taskRef,
322
+ t.channel_id as taskChannelId,
323
+ c.name as channelName,
324
+ CASE
325
+ WHEN po.source_type = 'dm' THEN COALESCE(po.agent_id, substr(t.channel_id, 4))
326
+ ELSE po.agent_id
327
+ END as taskAgentId,
328
+ a.name as agentName,
329
+ t.task_number as taskNumber,
330
+ t.title as taskTitle,
331
+ t.description as description,
332
+ t.status as taskStatus,
333
+ t.claimed_by_agent_id as assigneeId,
334
+ t.claimed_by_name as assigneeName,
335
+ t.message_id as messageId,
336
+ COALESCE(NULLIF(t.dm_target, ''), cm.target) as dmTarget,
337
+ t.blocked_reason as blockedReason,
338
+ t.blocked_at as blockedAt,
339
+ t.blocked_by as blockedBy,
340
+ t.created_at as taskCreatedAt,
341
+ t.updated_at as taskUpdatedAt
342
+ FROM proactive_opportunities po
343
+ JOIN tasks t ON t.task_id = po.task_id
344
+ LEFT JOIN channel_messages cm ON cm.message_id = t.message_id
345
+ LEFT JOIN channels c ON c.channel_id = t.channel_id
346
+ LEFT JOIN agents a ON a.agent_id = CASE
347
+ WHEN po.source_type = 'dm' THEN COALESCE(po.agent_id, substr(t.channel_id, 4))
348
+ ELSE po.agent_id
349
+ END
350
+ WHERE po.user_id = ?
351
+ AND po.status = 'open'
352
+ ORDER BY po.updated_at DESC, po.created_at DESC`).all(params.userId);
353
+ const genericRows = db.prepare(`SELECT po.opportunity_id as opportunityId,
354
+ po.kind as kind,
355
+ po.status as status,
356
+ po.dedupe_key as dedupeKey,
357
+ po.source_type as sourceType,
358
+ po.title as title,
359
+ po.rationale as rationale,
360
+ po.evidence_json as evidenceJson,
361
+ po.snoozed_until as snoozedUntil,
362
+ po.dismissed_at as dismissedAt,
363
+ po.resolved_at as resolvedAt,
364
+ po.created_at as createdAt,
365
+ po.updated_at as updatedAt,
366
+ po.channel_id as channelId,
367
+ po.agent_id as agentId
368
+ FROM proactive_opportunities po
369
+ WHERE po.user_id = ?
370
+ AND po.status = 'open'
371
+ AND po.task_id IS NULL
372
+ ORDER BY po.updated_at DESC, po.created_at DESC`).all(params.userId);
373
+ return [
374
+ ...taskRows.map(mapOpportunityRow),
375
+ ...genericRows.map(mapGenericOpportunityRow),
376
+ ].sort((left, right) => right.updatedAt - left.updatedAt || right.createdAt - left.createdAt);
377
+ }
378
+ export function insertPlannerTaskCandidateOpportunity(db, params) {
379
+ const now = params.now ?? Date.now();
380
+ const evidenceJson = JSON.stringify({
381
+ ...params.evidence,
382
+ evidenceLinks: params.evidenceLinks ?? [],
383
+ });
384
+ db.prepare(`INSERT OR IGNORE INTO proactive_opportunities(
385
+ opportunity_id, user_id, kind, status, dedupe_key, task_id, source_type,
386
+ channel_id, agent_id, title, rationale, evidence_json,
387
+ created_at, updated_at
388
+ )
389
+ VALUES(?, ?, 'planner_task_candidate', 'open', ?, NULL, 'planner', ?, ?, ?, ?, ?, ?, ?)`).run(randomUUID(), params.userId, params.dedupeKey, params.channelId ?? null, params.agentId ?? null, params.title, params.rationale, evidenceJson, now, now);
390
+ db.prepare(`UPDATE proactive_opportunities
391
+ SET channel_id = ?,
392
+ agent_id = ?,
393
+ title = ?,
394
+ rationale = ?,
395
+ evidence_json = ?,
396
+ updated_at = ?
397
+ WHERE user_id = ?
398
+ AND dedupe_key = ?
399
+ AND status = 'open'
400
+ AND task_id IS NULL`).run(params.channelId ?? null, params.agentId ?? null, params.title, params.rationale, evidenceJson, now, params.userId, params.dedupeKey);
401
+ }
402
+ export function getProactiveOpportunityTaskRef(db, params) {
403
+ const row = db.prepare(`SELECT opportunity_id as opportunityId,
404
+ kind,
405
+ status,
406
+ task_id as taskId,
407
+ source_type as sourceType,
408
+ channel_id as channelId,
409
+ agent_id as agentId
410
+ FROM proactive_opportunities
411
+ WHERE opportunity_id = ?
412
+ AND user_id = ?
413
+ LIMIT 1`).get(params.opportunityId, params.userId);
414
+ return row ?? null;
415
+ }
416
+ function updateOpportunityStatus(db, params) {
417
+ const result = db.prepare(`UPDATE proactive_opportunities
418
+ SET status = ?,
419
+ snoozed_until = ?,
420
+ dismissed_at = CASE WHEN ? = 'dismissed' THEN COALESCE(dismissed_at, ?) ELSE dismissed_at END,
421
+ resolved_at = CASE WHEN ? = 'resolved' THEN COALESCE(resolved_at, ?) ELSE resolved_at END,
422
+ updated_at = ?
423
+ WHERE opportunity_id = ?
424
+ AND user_id = ?
425
+ AND status IN ('open', 'snoozed')`).run(params.status, params.status === 'snoozed' ? params.snoozedUntil ?? null : null, params.status, params.now, params.status, params.now, params.now, params.opportunityId, params.userId);
426
+ return result.changes > 0;
427
+ }
428
+ export function dismissProactiveOpportunity(db, params) {
429
+ return updateOpportunityStatus(db, {
430
+ userId: params.userId,
431
+ opportunityId: params.opportunityId,
432
+ status: 'dismissed',
433
+ now: params.now ?? Date.now(),
434
+ });
435
+ }
436
+ export function snoozeProactiveOpportunity(db, params) {
437
+ return updateOpportunityStatus(db, {
438
+ userId: params.userId,
439
+ opportunityId: params.opportunityId,
440
+ status: 'snoozed',
441
+ snoozedUntil: params.snoozedUntil,
442
+ now: params.now ?? Date.now(),
443
+ });
444
+ }
445
+ export function resolveProactiveOpportunity(db, params) {
446
+ return updateOpportunityStatus(db, {
447
+ userId: params.userId,
448
+ opportunityId: params.opportunityId,
449
+ status: 'resolved',
450
+ now: params.now ?? Date.now(),
451
+ });
452
+ }