@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,599 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { log } from '@bbigbang/runtime-acp';
|
|
3
|
+
import { buildChannelSurfaceActivationEnvelope, buildDirectSurfaceActivationEnvelope, } from './activationContext.js';
|
|
4
|
+
import { getExpectedBootstrapStopReason } from './planApprovalState.js';
|
|
5
|
+
import { getBoundTaskForThread, listThreadCollaborationParticipants } from './threadTaskBindings.js';
|
|
6
|
+
export const TASK_THREAD_FOLLOWUP_KIND = 'task_status_reminder';
|
|
7
|
+
export const TASK_THREAD_FOLLOWUP_DELAY_MS = 10 * 60 * 1000;
|
|
8
|
+
export const TASK_THREAD_FOLLOWUP_POLL_INTERVAL_MS = 30 * 1000;
|
|
9
|
+
export const TASK_THREAD_FOLLOWUP_EVENT_METHOD = 'platform/task-thread-followup';
|
|
10
|
+
function nextSyntheticRunEventSeq(db, runId) {
|
|
11
|
+
const row = db.prepare(`SELECT COALESCE(MAX(seq), 0) as maxSeq
|
|
12
|
+
FROM events
|
|
13
|
+
WHERE run_id = ?`).get(runId);
|
|
14
|
+
return (row?.maxSeq ?? 0) + 1;
|
|
15
|
+
}
|
|
16
|
+
function appendTaskThreadFollowupEvent(db, runId, payload) {
|
|
17
|
+
if (!runId)
|
|
18
|
+
return;
|
|
19
|
+
db.prepare(`INSERT INTO events(run_id, seq, method, payload_json, created_at)
|
|
20
|
+
VALUES(?, ?, ?, ?, ?)`).run(runId, nextSyntheticRunEventSeq(db, runId), TASK_THREAD_FOLLOWUP_EVENT_METHOD, JSON.stringify(payload), Date.now());
|
|
21
|
+
}
|
|
22
|
+
function isCancelStopReason(stopReason) {
|
|
23
|
+
return stopReason === 'cancelled' || stopReason === 'canceled';
|
|
24
|
+
}
|
|
25
|
+
function wasRunHandedOff(db, runId) {
|
|
26
|
+
return Boolean(db.prepare(`SELECT 1
|
|
27
|
+
FROM events
|
|
28
|
+
WHERE run_id = ?
|
|
29
|
+
AND method = 'platform/handoff'
|
|
30
|
+
LIMIT 1`).get(runId));
|
|
31
|
+
}
|
|
32
|
+
export function getRunSystemFollowupMetadata(db, runId) {
|
|
33
|
+
if (!runId)
|
|
34
|
+
return null;
|
|
35
|
+
const row = db.prepare(`SELECT activation_metadata_json as activationMetadataJson
|
|
36
|
+
FROM run_debug_inputs
|
|
37
|
+
WHERE run_id = ?
|
|
38
|
+
LIMIT 1`).get(runId);
|
|
39
|
+
if (!row?.activationMetadataJson)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(row.activationMetadataJson);
|
|
43
|
+
if (parsed?.systemFollowup?.kind === TASK_THREAD_FOLLOWUP_KIND
|
|
44
|
+
&& typeof parsed.systemFollowup.followupId === 'string'
|
|
45
|
+
&& parsed.systemFollowup.followupId.trim()
|
|
46
|
+
&& typeof parsed.systemFollowup.taskId === 'string'
|
|
47
|
+
&& parsed.systemFollowup.taskId.trim()
|
|
48
|
+
&& typeof parsed.systemFollowup.channelId === 'string'
|
|
49
|
+
&& parsed.systemFollowup.channelId.trim()
|
|
50
|
+
&& typeof parsed.systemFollowup.threadRootId === 'string'
|
|
51
|
+
&& parsed.systemFollowup.threadRootId.trim()) {
|
|
52
|
+
return {
|
|
53
|
+
kind: TASK_THREAD_FOLLOWUP_KIND,
|
|
54
|
+
followupId: parsed.systemFollowup.followupId.trim(),
|
|
55
|
+
taskId: parsed.systemFollowup.taskId.trim(),
|
|
56
|
+
channelId: parsed.systemFollowup.channelId.trim(),
|
|
57
|
+
threadRootId: parsed.systemFollowup.threadRootId.trim(),
|
|
58
|
+
...(typeof parsed.systemFollowup.delayMs === 'number'
|
|
59
|
+
&& Number.isFinite(parsed.systemFollowup.delayMs)
|
|
60
|
+
&& parsed.systemFollowup.delayMs > 0
|
|
61
|
+
? { delayMs: parsed.systemFollowup.delayMs }
|
|
62
|
+
: {}),
|
|
63
|
+
...(typeof parsed.systemFollowup.baselineThreadSeq === 'number'
|
|
64
|
+
&& Number.isFinite(parsed.systemFollowup.baselineThreadSeq)
|
|
65
|
+
&& parsed.systemFollowup.baselineThreadSeq >= 0
|
|
66
|
+
? { baselineThreadSeq: parsed.systemFollowup.baselineThreadSeq }
|
|
67
|
+
: {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// ignore malformed activation metadata
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function loadLatestThreadSeq(db, params) {
|
|
77
|
+
const row = db.prepare(`SELECT COALESCE(MAX(seq), 0) as maxSeq
|
|
78
|
+
FROM channel_messages
|
|
79
|
+
WHERE channel_id = ? AND thread_root_id = ?`).get(params.channelId, params.threadRootId);
|
|
80
|
+
return row?.maxSeq ?? 0;
|
|
81
|
+
}
|
|
82
|
+
function resolveTaskThreadChannelId(params) {
|
|
83
|
+
if (params.threadKind === 'direct' && params.agentId) {
|
|
84
|
+
return `dm:${params.agentId}`;
|
|
85
|
+
}
|
|
86
|
+
return params.conversationChannelId;
|
|
87
|
+
}
|
|
88
|
+
function isBlockedTask(task) {
|
|
89
|
+
return Boolean(task && (task.blockedAt ?? task.blockedBy ?? task.blockedReason));
|
|
90
|
+
}
|
|
91
|
+
function shouldKeepTaskThreadFollowup(task) {
|
|
92
|
+
if (!task)
|
|
93
|
+
return false;
|
|
94
|
+
if (task.status !== 'todo' && task.status !== 'in_progress')
|
|
95
|
+
return false;
|
|
96
|
+
return !isBlockedTask(task);
|
|
97
|
+
}
|
|
98
|
+
function readFollowupDelayMsFromMetadata(metadata) {
|
|
99
|
+
const delayMs = metadata?.delayMs;
|
|
100
|
+
return typeof delayMs === 'number' && Number.isFinite(delayMs) && delayMs > 0
|
|
101
|
+
? delayMs
|
|
102
|
+
: TASK_THREAD_FOLLOWUP_DELAY_MS;
|
|
103
|
+
}
|
|
104
|
+
function readFollowupDelayMsFromRow(row) {
|
|
105
|
+
const derivedDelay = row.scheduledFor - row.updatedAt;
|
|
106
|
+
return Number.isFinite(derivedDelay) && derivedDelay > 0
|
|
107
|
+
? derivedDelay
|
|
108
|
+
: TASK_THREAD_FOLLOWUP_DELAY_MS;
|
|
109
|
+
}
|
|
110
|
+
function hasExternalThreadActivitySinceBaseline(params) {
|
|
111
|
+
if (!Number.isFinite(params.baselineThreadSeq))
|
|
112
|
+
return true;
|
|
113
|
+
return Boolean(params.db.prepare(`SELECT 1
|
|
114
|
+
FROM channel_messages
|
|
115
|
+
WHERE channel_id = ?
|
|
116
|
+
AND thread_root_id = ?
|
|
117
|
+
AND seq > ?
|
|
118
|
+
AND (run_id IS NULL OR run_id != ?)
|
|
119
|
+
LIMIT 1`).get(params.channelId, params.threadRootId, params.baselineThreadSeq, params.currentRunId));
|
|
120
|
+
}
|
|
121
|
+
export function scheduleTaskThreadFollowupForRun(params) {
|
|
122
|
+
const { db, conversationId, runId } = params;
|
|
123
|
+
if (params.error)
|
|
124
|
+
return;
|
|
125
|
+
const systemFollowup = getRunSystemFollowupMetadata(db, runId);
|
|
126
|
+
if (params.stopReason === 'handoff' || isCancelStopReason(params.stopReason)) {
|
|
127
|
+
if (wasRunHandedOff(db, runId) || getExpectedBootstrapStopReason(db, runId) === 'handoff_bootstrap') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (params.stopReason === 'handoff_bootstrap')
|
|
133
|
+
return;
|
|
134
|
+
const conversation = db.prepare(`SELECT channel_id as channelId,
|
|
135
|
+
thread_kind as threadKind,
|
|
136
|
+
agent_id as agentId,
|
|
137
|
+
thread_root_id as threadRootId
|
|
138
|
+
FROM conversations
|
|
139
|
+
WHERE id = ?
|
|
140
|
+
LIMIT 1`).get(conversationId);
|
|
141
|
+
if (!conversation?.threadRootId)
|
|
142
|
+
return;
|
|
143
|
+
const taskThreadChannelId = resolveTaskThreadChannelId({
|
|
144
|
+
conversationChannelId: conversation.channelId,
|
|
145
|
+
threadKind: conversation.threadKind,
|
|
146
|
+
agentId: conversation.agentId,
|
|
147
|
+
});
|
|
148
|
+
const boundTask = getBoundTaskForThread(db, {
|
|
149
|
+
channelId: taskThreadChannelId,
|
|
150
|
+
threadRootId: conversation.threadRootId,
|
|
151
|
+
});
|
|
152
|
+
if (!boundTask || !shouldKeepTaskThreadFollowup(boundTask))
|
|
153
|
+
return;
|
|
154
|
+
const delayMs = systemFollowup?.kind === TASK_THREAD_FOLLOWUP_KIND
|
|
155
|
+
? (hasExternalThreadActivitySinceBaseline({
|
|
156
|
+
db,
|
|
157
|
+
channelId: taskThreadChannelId,
|
|
158
|
+
threadRootId: conversation.threadRootId,
|
|
159
|
+
baselineThreadSeq: systemFollowup.baselineThreadSeq,
|
|
160
|
+
currentRunId: runId,
|
|
161
|
+
})
|
|
162
|
+
? TASK_THREAD_FOLLOWUP_DELAY_MS
|
|
163
|
+
: readFollowupDelayMsFromMetadata(systemFollowup) * 2)
|
|
164
|
+
: TASK_THREAD_FOLLOWUP_DELAY_MS;
|
|
165
|
+
const now = params.now ?? Date.now();
|
|
166
|
+
const scheduledFor = now + delayMs;
|
|
167
|
+
const baselineThreadSeq = loadLatestThreadSeq(db, {
|
|
168
|
+
channelId: taskThreadChannelId,
|
|
169
|
+
threadRootId: conversation.threadRootId,
|
|
170
|
+
});
|
|
171
|
+
const existing = db.prepare(`SELECT followup_id as followupId
|
|
172
|
+
FROM task_thread_followups
|
|
173
|
+
WHERE kind = ? AND channel_id = ? AND thread_root_id = ?
|
|
174
|
+
LIMIT 1`).get(TASK_THREAD_FOLLOWUP_KIND, taskThreadChannelId, conversation.threadRootId);
|
|
175
|
+
const followupId = existing?.followupId ?? randomUUID();
|
|
176
|
+
db.prepare(`INSERT INTO task_thread_followups(
|
|
177
|
+
followup_id, kind, task_id, channel_id, thread_root_id, source_conversation_id,
|
|
178
|
+
created_by_run_id, baseline_thread_seq, scheduled_for, created_at, updated_at
|
|
179
|
+
)
|
|
180
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
181
|
+
ON CONFLICT(kind, channel_id, thread_root_id) DO UPDATE SET
|
|
182
|
+
task_id = excluded.task_id,
|
|
183
|
+
source_conversation_id = excluded.source_conversation_id,
|
|
184
|
+
created_by_run_id = excluded.created_by_run_id,
|
|
185
|
+
baseline_thread_seq = excluded.baseline_thread_seq,
|
|
186
|
+
scheduled_for = excluded.scheduled_for,
|
|
187
|
+
updated_at = excluded.updated_at`).run(followupId, TASK_THREAD_FOLLOWUP_KIND, boundTask.taskId, taskThreadChannelId, conversation.threadRootId, conversationId, runId, baselineThreadSeq, scheduledFor, now, now);
|
|
188
|
+
appendTaskThreadFollowupEvent(db, runId, {
|
|
189
|
+
followupId,
|
|
190
|
+
state: 'scheduled',
|
|
191
|
+
kind: TASK_THREAD_FOLLOWUP_KIND,
|
|
192
|
+
taskId: boundTask.taskId,
|
|
193
|
+
channelId: taskThreadChannelId,
|
|
194
|
+
threadRootId: conversation.threadRootId,
|
|
195
|
+
sourceConversationId: conversationId,
|
|
196
|
+
baselineThreadSeq,
|
|
197
|
+
delayMs,
|
|
198
|
+
scheduledFor,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function deleteTaskThreadFollowupsForTask(db, taskId) {
|
|
202
|
+
return db.prepare(`DELETE FROM task_thread_followups
|
|
203
|
+
WHERE task_id = ?`).run(taskId).changes;
|
|
204
|
+
}
|
|
205
|
+
function loadOpenTask(db, taskId) {
|
|
206
|
+
const row = db.prepare(`SELECT task_id as taskId,
|
|
207
|
+
channel_id as channelId,
|
|
208
|
+
task_number as taskNumber,
|
|
209
|
+
title,
|
|
210
|
+
description,
|
|
211
|
+
status,
|
|
212
|
+
claimed_by_agent_id as claimedByAgentId,
|
|
213
|
+
claimed_by_name as claimedByName,
|
|
214
|
+
blocked_reason as blockedReason,
|
|
215
|
+
blocked_at as blockedAt,
|
|
216
|
+
blocked_by as blockedBy
|
|
217
|
+
FROM tasks
|
|
218
|
+
WHERE task_id = ?
|
|
219
|
+
LIMIT 1`).get(taskId);
|
|
220
|
+
return row ?? null;
|
|
221
|
+
}
|
|
222
|
+
function deleteFollowup(db, followupId) {
|
|
223
|
+
db.prepare('DELETE FROM task_thread_followups WHERE followup_id = ?').run(followupId);
|
|
224
|
+
}
|
|
225
|
+
function buildTaskBoardTarget(replyTarget) {
|
|
226
|
+
if (replyTarget.startsWith('dm:@')) {
|
|
227
|
+
const parts = replyTarget.split(':');
|
|
228
|
+
return parts.length >= 3 ? parts.slice(0, 2).join(':') : replyTarget;
|
|
229
|
+
}
|
|
230
|
+
if (replyTarget.startsWith('#')) {
|
|
231
|
+
const idx = replyTarget.lastIndexOf(':');
|
|
232
|
+
return idx > 0 ? replyTarget.slice(0, idx) : replyTarget;
|
|
233
|
+
}
|
|
234
|
+
return replyTarget;
|
|
235
|
+
}
|
|
236
|
+
function formatFollowupDelayLabel(delayMs) {
|
|
237
|
+
const roundedMinutes = Math.round(delayMs / 60_000);
|
|
238
|
+
if (roundedMinutes > 0 && roundedMinutes % 60 === 0) {
|
|
239
|
+
const hours = roundedMinutes / 60;
|
|
240
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
241
|
+
}
|
|
242
|
+
return `${roundedMinutes} minute${roundedMinutes === 1 ? '' : 's'}`;
|
|
243
|
+
}
|
|
244
|
+
function buildTaskThreadFollowupPrompt(params) {
|
|
245
|
+
const boardTarget = buildTaskBoardTarget(params.replyTarget);
|
|
246
|
+
return [
|
|
247
|
+
'[System: Task-thread follow-up]',
|
|
248
|
+
`It has been ${formatFollowupDelayLabel(params.delayMs)} since your last run on this task thread ended, and the task is still open.`,
|
|
249
|
+
'',
|
|
250
|
+
'[Current conversation target]',
|
|
251
|
+
`reply_target: ${params.replyTarget}`,
|
|
252
|
+
`Task: #${params.task.taskNumber} ${params.task.title}`,
|
|
253
|
+
`Current task status: ${params.task.status}`,
|
|
254
|
+
'',
|
|
255
|
+
'Rules:',
|
|
256
|
+
`- If the substantive result is ready, send it in this thread and run bigbang task update --channel "${boardTarget}" --number ${params.task.taskNumber} --status in_review.`,
|
|
257
|
+
'- If the work is still ongoing, continue the task and send a progress update in this thread.',
|
|
258
|
+
'- If you are blocked on user input, approval, or missing context, ask the user exactly what you need in this thread.',
|
|
259
|
+
'- If the latest visible state is already waiting on the user or reviewer and you have nothing new to add, do not repeat the waiting message; update memory/notes only if needed, then stop.',
|
|
260
|
+
'- Do not send a redundant second completion-summary after the substantive final result.',
|
|
261
|
+
].join('\n');
|
|
262
|
+
}
|
|
263
|
+
function canAttemptFollowupDispatch(db, nodeRegistry, conversationId) {
|
|
264
|
+
const row = db.prepare(`SELECT session_key as sessionKey,
|
|
265
|
+
node_id as nodeId,
|
|
266
|
+
status
|
|
267
|
+
FROM conversations
|
|
268
|
+
WHERE id = ?
|
|
269
|
+
LIMIT 1`).get(conversationId);
|
|
270
|
+
if (!row?.nodeId)
|
|
271
|
+
return false;
|
|
272
|
+
if (nodeRegistry && !nodeRegistry.getNode(row.nodeId))
|
|
273
|
+
return false;
|
|
274
|
+
if (row.status === 'queued' || row.status === 'active' || row.status === 'recovering' || row.status === 'awaiting_approval') {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return !db.prepare(`SELECT 1
|
|
278
|
+
FROM runs
|
|
279
|
+
WHERE session_key = ? AND ended_at IS NULL
|
|
280
|
+
LIMIT 1`).get(row.sessionKey);
|
|
281
|
+
}
|
|
282
|
+
function resolveDmConversation(db, conversationManager, row) {
|
|
283
|
+
const expectedAgentId = row.channelId.startsWith('dm:')
|
|
284
|
+
? row.channelId.slice(3)
|
|
285
|
+
: null;
|
|
286
|
+
const sourceConversation = row.sourceConversationId
|
|
287
|
+
? conversationManager.getConversation(row.sourceConversationId)
|
|
288
|
+
: null;
|
|
289
|
+
if (sourceConversation
|
|
290
|
+
&& sourceConversation.threadKind === 'direct'
|
|
291
|
+
&& sourceConversation.threadRootId === row.threadRootId
|
|
292
|
+
&& (!expectedAgentId || sourceConversation.agentId === expectedAgentId)) {
|
|
293
|
+
return sourceConversation;
|
|
294
|
+
}
|
|
295
|
+
const existing = db.prepare(`SELECT id
|
|
296
|
+
FROM conversations
|
|
297
|
+
WHERE thread_kind = 'direct'
|
|
298
|
+
AND thread_root_id = ?
|
|
299
|
+
AND (? IS NULL OR agent_id = ?)
|
|
300
|
+
ORDER BY updated_at DESC
|
|
301
|
+
LIMIT 1`).get(row.threadRootId, expectedAgentId, expectedAgentId);
|
|
302
|
+
return existing ? conversationManager.getConversation(existing.id) : null;
|
|
303
|
+
}
|
|
304
|
+
function resolveChannelConversation(conversationManager, task, row) {
|
|
305
|
+
if (task.claimedByAgentId) {
|
|
306
|
+
const ownerConversation = conversationManager.openAgentChannelThread(task.claimedByAgentId, row.channelId, row.threadRootId);
|
|
307
|
+
if (ownerConversation)
|
|
308
|
+
return ownerConversation;
|
|
309
|
+
}
|
|
310
|
+
const recentParticipants = listThreadCollaborationParticipants(conversationManager.getDb(), {
|
|
311
|
+
channelId: row.channelId,
|
|
312
|
+
threadRootId: row.threadRootId,
|
|
313
|
+
});
|
|
314
|
+
const fallbackParticipant = recentParticipants.find((participant) => Boolean(conversationManager.getAgent(participant.agentId)));
|
|
315
|
+
if (!fallbackParticipant)
|
|
316
|
+
return null;
|
|
317
|
+
return conversationManager.openAgentChannelThread(fallbackParticipant.agentId, row.channelId, row.threadRootId);
|
|
318
|
+
}
|
|
319
|
+
function deleteLatestQueuedFollowupPrompt(db, params) {
|
|
320
|
+
db.prepare(`DELETE FROM conversation_prompt_queue
|
|
321
|
+
WHERE queue_id IN (
|
|
322
|
+
SELECT queue_id
|
|
323
|
+
FROM conversation_prompt_queue
|
|
324
|
+
WHERE conversation_id = ?
|
|
325
|
+
AND prompt_text = ?
|
|
326
|
+
AND record_as_user_message = 0
|
|
327
|
+
AND created_at >= ?
|
|
328
|
+
ORDER BY queue_id DESC
|
|
329
|
+
LIMIT 1
|
|
330
|
+
)`).run(params.conversationId, params.promptText, params.createdAfter);
|
|
331
|
+
}
|
|
332
|
+
function restoreIdleStatusAfterQueuedFollowupRemoval(db, conversationId) {
|
|
333
|
+
const row = db.prepare(`SELECT session_key as sessionKey,
|
|
334
|
+
status
|
|
335
|
+
FROM conversations
|
|
336
|
+
WHERE id = ?
|
|
337
|
+
LIMIT 1`).get(conversationId);
|
|
338
|
+
if (!row || row.status !== 'queued')
|
|
339
|
+
return;
|
|
340
|
+
const hasQueuedPrompt = Boolean(db.prepare(`SELECT 1
|
|
341
|
+
FROM conversation_prompt_queue
|
|
342
|
+
WHERE conversation_id = ?
|
|
343
|
+
AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
|
|
344
|
+
LIMIT 1`).get(conversationId));
|
|
345
|
+
if (hasQueuedPrompt)
|
|
346
|
+
return;
|
|
347
|
+
const hasOpenRun = Boolean(db.prepare(`SELECT 1
|
|
348
|
+
FROM runs
|
|
349
|
+
WHERE session_key = ? AND ended_at IS NULL
|
|
350
|
+
LIMIT 1`).get(row.sessionKey));
|
|
351
|
+
if (hasOpenRun)
|
|
352
|
+
return;
|
|
353
|
+
db.prepare(`UPDATE conversations
|
|
354
|
+
SET status = 'idle', updated_at = ?
|
|
355
|
+
WHERE id = ? AND status = 'queued'`).run(Date.now(), conversationId);
|
|
356
|
+
}
|
|
357
|
+
function buildFollowupOptions(params) {
|
|
358
|
+
const conversation = params.conversationManager.getConversation(params.conversationId);
|
|
359
|
+
if (!conversation?.agentId || !conversation.replyTarget)
|
|
360
|
+
return null;
|
|
361
|
+
const activationMetadata = {
|
|
362
|
+
systemFollowup: {
|
|
363
|
+
kind: TASK_THREAD_FOLLOWUP_KIND,
|
|
364
|
+
followupId: params.followupId,
|
|
365
|
+
taskId: params.task.taskId,
|
|
366
|
+
channelId: params.channelId,
|
|
367
|
+
threadRootId: params.threadRootId,
|
|
368
|
+
delayMs: params.delayMs,
|
|
369
|
+
baselineThreadSeq: params.triggerSeq - 1,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
if (conversation.threadKind === 'direct') {
|
|
373
|
+
const envelope = buildDirectSurfaceActivationEnvelope(params.db, {
|
|
374
|
+
agentId: conversation.agentId,
|
|
375
|
+
channelId: params.channelId,
|
|
376
|
+
replyTarget: conversation.replyTarget,
|
|
377
|
+
triggerSeq: params.triggerSeq,
|
|
378
|
+
threadRootId: params.threadRootId,
|
|
379
|
+
forceTaskNotes: true,
|
|
380
|
+
});
|
|
381
|
+
return {
|
|
382
|
+
promptText: buildTaskThreadFollowupPrompt({
|
|
383
|
+
replyTarget: conversation.replyTarget,
|
|
384
|
+
task: params.task,
|
|
385
|
+
delayMs: params.delayMs,
|
|
386
|
+
}),
|
|
387
|
+
options: {
|
|
388
|
+
recordAsUserMessage: false,
|
|
389
|
+
activationContextText: envelope.activationContextText,
|
|
390
|
+
activationContextSections: envelope.activationContextSections,
|
|
391
|
+
activationContextMode: 'resume_only',
|
|
392
|
+
resumeContextText: envelope.resumeContextText,
|
|
393
|
+
resumeContextSections: envelope.resumeContextSections,
|
|
394
|
+
replayOverlapRecentMessages: envelope.replayOverlapRecentMessages,
|
|
395
|
+
activationMetadata,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const channel = params.conversationManager.getChannel(params.channelId);
|
|
400
|
+
const envelope = buildChannelSurfaceActivationEnvelope(params.db, {
|
|
401
|
+
agentId: conversation.agentId,
|
|
402
|
+
channelId: params.channelId,
|
|
403
|
+
replyTarget: conversation.replyTarget,
|
|
404
|
+
triggerSeq: params.triggerSeq,
|
|
405
|
+
threadRootId: params.threadRootId,
|
|
406
|
+
channelName: channel?.name,
|
|
407
|
+
reason: 'thread_reply',
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
promptText: buildTaskThreadFollowupPrompt({
|
|
411
|
+
replyTarget: conversation.replyTarget,
|
|
412
|
+
task: params.task,
|
|
413
|
+
delayMs: params.delayMs,
|
|
414
|
+
}),
|
|
415
|
+
options: {
|
|
416
|
+
recordAsUserMessage: false,
|
|
417
|
+
activationContextText: envelope.activationContextText,
|
|
418
|
+
activationContextSections: envelope.activationContextSections,
|
|
419
|
+
activationContextMode: 'resume_only',
|
|
420
|
+
resumeContextText: envelope.resumeContextText,
|
|
421
|
+
resumeContextSections: envelope.resumeContextSections,
|
|
422
|
+
replayOverlapRecentMessages: envelope.replayOverlapRecentMessages,
|
|
423
|
+
activationMetadata,
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function processFollowupRow(params) {
|
|
428
|
+
const { db, row, now, conversationManager, nodeRegistry } = params;
|
|
429
|
+
const task = loadOpenTask(db, row.taskId);
|
|
430
|
+
if (task && isBlockedTask(task)) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!task || !shouldKeepTaskThreadFollowup(task)) {
|
|
434
|
+
appendTaskThreadFollowupEvent(db, row.createdByRunId, {
|
|
435
|
+
followupId: row.followupId,
|
|
436
|
+
state: 'skipped',
|
|
437
|
+
reason: task ? 'task_closed' : 'task_missing',
|
|
438
|
+
kind: row.kind,
|
|
439
|
+
taskId: row.taskId,
|
|
440
|
+
channelId: row.channelId,
|
|
441
|
+
threadRootId: row.threadRootId,
|
|
442
|
+
});
|
|
443
|
+
deleteFollowup(db, row.followupId);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const latestThreadSeq = loadLatestThreadSeq(db, {
|
|
447
|
+
channelId: row.channelId,
|
|
448
|
+
threadRootId: row.threadRootId,
|
|
449
|
+
});
|
|
450
|
+
if (latestThreadSeq > row.baselineThreadSeq) {
|
|
451
|
+
appendTaskThreadFollowupEvent(db, row.createdByRunId, {
|
|
452
|
+
followupId: row.followupId,
|
|
453
|
+
state: 'cancelled',
|
|
454
|
+
reason: 'thread_activity',
|
|
455
|
+
kind: row.kind,
|
|
456
|
+
taskId: row.taskId,
|
|
457
|
+
channelId: row.channelId,
|
|
458
|
+
threadRootId: row.threadRootId,
|
|
459
|
+
latestThreadSeq,
|
|
460
|
+
baselineThreadSeq: row.baselineThreadSeq,
|
|
461
|
+
});
|
|
462
|
+
deleteFollowup(db, row.followupId);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const conversation = row.channelId.startsWith('dm:')
|
|
466
|
+
? resolveDmConversation(db, conversationManager, row)
|
|
467
|
+
: resolveChannelConversation(conversationManager, task, row);
|
|
468
|
+
if (!conversation?.id) {
|
|
469
|
+
appendTaskThreadFollowupEvent(db, row.createdByRunId, {
|
|
470
|
+
followupId: row.followupId,
|
|
471
|
+
state: 'skipped',
|
|
472
|
+
reason: 'no_target_conversation',
|
|
473
|
+
kind: row.kind,
|
|
474
|
+
taskId: row.taskId,
|
|
475
|
+
channelId: row.channelId,
|
|
476
|
+
threadRootId: row.threadRootId,
|
|
477
|
+
});
|
|
478
|
+
deleteFollowup(db, row.followupId);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!canAttemptFollowupDispatch(db, nodeRegistry, conversation.id)) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const prompt = buildFollowupOptions({
|
|
485
|
+
db,
|
|
486
|
+
conversationManager,
|
|
487
|
+
conversationId: conversation.id,
|
|
488
|
+
channelId: row.channelId,
|
|
489
|
+
threadRootId: row.threadRootId,
|
|
490
|
+
followupId: row.followupId,
|
|
491
|
+
task,
|
|
492
|
+
delayMs: readFollowupDelayMsFromRow(row),
|
|
493
|
+
triggerSeq: Math.max(row.baselineThreadSeq, latestThreadSeq) + 1,
|
|
494
|
+
});
|
|
495
|
+
if (!prompt) {
|
|
496
|
+
appendTaskThreadFollowupEvent(db, row.createdByRunId, {
|
|
497
|
+
followupId: row.followupId,
|
|
498
|
+
state: 'skipped',
|
|
499
|
+
reason: 'target_context_missing',
|
|
500
|
+
kind: row.kind,
|
|
501
|
+
taskId: row.taskId,
|
|
502
|
+
channelId: row.channelId,
|
|
503
|
+
threadRootId: row.threadRootId,
|
|
504
|
+
});
|
|
505
|
+
deleteFollowup(db, row.followupId);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const attemptStartedAt = now;
|
|
509
|
+
try {
|
|
510
|
+
const result = await conversationManager.submitPrompt(conversation.id, prompt.promptText, prompt.options);
|
|
511
|
+
if (result.queued) {
|
|
512
|
+
deleteLatestQueuedFollowupPrompt(db, {
|
|
513
|
+
conversationId: conversation.id,
|
|
514
|
+
promptText: prompt.promptText,
|
|
515
|
+
createdAfter: attemptStartedAt,
|
|
516
|
+
});
|
|
517
|
+
restoreIdleStatusAfterQueuedFollowupRemoval(db, conversation.id);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
appendTaskThreadFollowupEvent(db, row.createdByRunId, {
|
|
521
|
+
followupId: row.followupId,
|
|
522
|
+
state: 'dispatched',
|
|
523
|
+
kind: row.kind,
|
|
524
|
+
taskId: row.taskId,
|
|
525
|
+
channelId: row.channelId,
|
|
526
|
+
threadRootId: row.threadRootId,
|
|
527
|
+
targetConversationId: conversation.id,
|
|
528
|
+
reminderRunId: result.runId ?? null,
|
|
529
|
+
});
|
|
530
|
+
deleteFollowup(db, row.followupId);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
log.warn('[task-thread-followup] dispatch failed; keeping pending follow-up', {
|
|
534
|
+
followupId: row.followupId,
|
|
535
|
+
taskId: row.taskId,
|
|
536
|
+
conversationId: conversation.id,
|
|
537
|
+
error: String(error?.message ?? error),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export async function processDueTaskThreadFollowups(params) {
|
|
542
|
+
const now = params.now ?? Date.now();
|
|
543
|
+
const rows = params.db.prepare(`SELECT followup_id as followupId,
|
|
544
|
+
kind,
|
|
545
|
+
task_id as taskId,
|
|
546
|
+
channel_id as channelId,
|
|
547
|
+
thread_root_id as threadRootId,
|
|
548
|
+
source_conversation_id as sourceConversationId,
|
|
549
|
+
created_by_run_id as createdByRunId,
|
|
550
|
+
baseline_thread_seq as baselineThreadSeq,
|
|
551
|
+
scheduled_for as scheduledFor,
|
|
552
|
+
created_at as createdAt,
|
|
553
|
+
updated_at as updatedAt
|
|
554
|
+
FROM task_thread_followups
|
|
555
|
+
WHERE scheduled_for <= ?
|
|
556
|
+
ORDER BY scheduled_for ASC, created_at ASC`).all(now);
|
|
557
|
+
for (const row of rows) {
|
|
558
|
+
await processFollowupRow({
|
|
559
|
+
db: params.db,
|
|
560
|
+
row,
|
|
561
|
+
now,
|
|
562
|
+
conversationManager: params.conversationManager,
|
|
563
|
+
nodeRegistry: params.nodeRegistry,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
export function startTaskThreadFollowupService(params) {
|
|
568
|
+
const intervalMs = Math.max(5_000, params.intervalMs ?? TASK_THREAD_FOLLOWUP_POLL_INTERVAL_MS);
|
|
569
|
+
let timer = null;
|
|
570
|
+
let running = false;
|
|
571
|
+
const tick = async () => {
|
|
572
|
+
if (running)
|
|
573
|
+
return;
|
|
574
|
+
running = true;
|
|
575
|
+
try {
|
|
576
|
+
await processDueTaskThreadFollowups({
|
|
577
|
+
db: params.db,
|
|
578
|
+
conversationManager: params.conversationManager,
|
|
579
|
+
nodeRegistry: params.nodeRegistry,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
running = false;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
timer = setInterval(() => {
|
|
587
|
+
void tick();
|
|
588
|
+
}, intervalMs);
|
|
589
|
+
timer.unref?.();
|
|
590
|
+
return {
|
|
591
|
+
tick,
|
|
592
|
+
stop: () => {
|
|
593
|
+
if (!timer)
|
|
594
|
+
return;
|
|
595
|
+
clearInterval(timer);
|
|
596
|
+
timer = null;
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|