@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,3810 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { getRuntimeDriver, } from '@bbigbang/protocol';
|
|
4
|
+
import { buildAgentSessionSystemPromptText, buildDreamSystemPrompt, loadDreamAnthropicEnv, resolveDreamEnvPath, } from '@bbigbang/memory';
|
|
5
|
+
import { buildDreamContextText, claimDreamRunLock, countAgentMessagesSinceSeq, finalizeDreamRun, listAgentMessagesSinceSeq, releaseDreamRunLock, } from '../services/memoryService.js';
|
|
6
|
+
import { createRun, finishRun, log } from '@bbigbang/runtime-acp';
|
|
7
|
+
import { iterateParsedNodeEvents } from '../nodeEventHistory.js';
|
|
8
|
+
import { buildPlatformAgentSystemPrompt, isPlatformAgentRole, } from '../services/platformAgentPolicy.js';
|
|
9
|
+
import { buildDirectActivationContextSections, buildDirectActivationPrompt, } from '../web/directActivationPrompt.js';
|
|
10
|
+
import { resolveConversationReplyTarget } from '../web/directReplyTargets.js';
|
|
11
|
+
import { allocateNextChannelMessageSeq } from '../web/channelMessageSequences.js';
|
|
12
|
+
import { buildDirectSurfaceActivationEnvelope, } from '../web/activationContext.js';
|
|
13
|
+
import { refreshConversationSummariesForMessageScope } from '../web/conversationSummaries.js';
|
|
14
|
+
import { buildContextBundle, buildRelevantContextHintSection } from '../web/contextBundleResolver.js';
|
|
15
|
+
import { getSnapshotConversationStatus } from '../web/planApprovalState.js';
|
|
16
|
+
import { buildRollingSummaryPromptSection, isCodexRuntimeRotationPending, } from '../web/rollingConversationSummary.js';
|
|
17
|
+
import { extractRelevantContextStrongSignals, shouldPreinjectRelevantContextHint } from '../web/surfaceActivationPolicy.js';
|
|
18
|
+
import { sanitizePromptHistoryContent } from '../web/promptHistorySanitizer.js';
|
|
19
|
+
import { EXACT_TARGET_HISTORY_SECTION_KINDS, createPromptContextSection, inferPromptContextSections, isPromptContextSectionKind, renderPromptContextSections, } from '../web/promptContextSections.js';
|
|
20
|
+
import { buildActiveProjectSection, buildDefaultDirectMemoryHints, buildWorkspaceMemoryReminder, getCurrentISOWeek, } from '../web/workspaceMemoryHints.js';
|
|
21
|
+
import { buildSurfaceSystemStatusSection } from '../web/surfaceSystemStatus.js';
|
|
22
|
+
import { hasEffectiveAgentRuntimeCapability } from '../web/runtimeCapabilities.js';
|
|
23
|
+
import { buildUiPanelGuidancePromptSection } from '../web/uiPanelPromptGuidance.js';
|
|
24
|
+
import { buildLibraryDocumentGuidancePromptSection } from '../web/libraryDocumentPromptGuidance.js';
|
|
25
|
+
import { buildWorkspaceToolGuidancePromptSection } from '../web/workspaceToolPromptGuidance.js';
|
|
26
|
+
import { conversationBuiltinSkillRootsForSlashCommand, normalizeConversationBuiltinSkillRoots, parseConversationBuiltinSkillRoots, serializeConversationBuiltinSkillRoots, } from '../web/conversationBuiltinSkillRoots.js';
|
|
27
|
+
import { isLayoutPlanPromptSlashCommandName, isPanelPromptSlashCommandName, isWorkspaceToolPromptSlashCommandName, normalizePromptSlashCommandName, } from '../web/promptSlashCommands.js';
|
|
28
|
+
function mergeDispatchSkillRoots(skillRoots, builtinSkillRoots) {
|
|
29
|
+
const merged = [...builtinSkillRoots];
|
|
30
|
+
for (const root of skillRoots ?? []) {
|
|
31
|
+
const normalized = root.trim();
|
|
32
|
+
if (!normalized || merged.includes(normalized))
|
|
33
|
+
continue;
|
|
34
|
+
merged.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
return merged.length > 0 ? merged : undefined;
|
|
37
|
+
}
|
|
38
|
+
function normalizeDispatchEnabledSkillPaths(paths) {
|
|
39
|
+
if (!paths)
|
|
40
|
+
return undefined;
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const normalized = [];
|
|
43
|
+
for (const value of paths) {
|
|
44
|
+
const trimmed = value.trim();
|
|
45
|
+
if (!trimmed || seen.has(trimmed))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(trimmed);
|
|
48
|
+
normalized.push(trimmed);
|
|
49
|
+
}
|
|
50
|
+
return normalized.length > 0 ? normalized : [];
|
|
51
|
+
}
|
|
52
|
+
function buildTurnReplyContract(today = getCurrentISOWeek(), agentSurfaceMode = 'bigbang') {
|
|
53
|
+
if (agentSurfaceMode === 'mcp') {
|
|
54
|
+
return [
|
|
55
|
+
'[Reply contract]',
|
|
56
|
+
`Current date (Beijing / UTC+8): ${today.week} (${today.date})`,
|
|
57
|
+
'Reply only via `mcp__chat__send_message`. Do not output user-visible text directly.',
|
|
58
|
+
'Use `mcp__chat__send_message(content="...", kind="progress")` only while work is ongoing. Before the run ends, send one final user-visible message with `mcp__chat__send_message(content="...", kind="final")`.',
|
|
59
|
+
'If there is no visible or peer-visible value to add, do not send a message. On shared channels/threads, leave peer_delivery unset for coordination-relevant updates so peers receive the default batch delivery; use peer_delivery="silent" only for visible no-action status, local bookkeeping, duplicate presence, or waiting updates that should not ordinary-notify peers. If unsure, prefer default batch. Do not set peer delivery in DMs.',
|
|
60
|
+
'- Use `mcp__chat__get_self_state` only for a broad agent-wide work overview across surfaces.',
|
|
61
|
+
'- Use `mcp__chat__get_runtime_presence` only for node / host / runtime facts.',
|
|
62
|
+
'- Use `mcp__chat__inspect_workspace` only for current workspace git / directory facts.',
|
|
63
|
+
'- Use `mcp__chat__list_my_conversations` to rediscover surfaces in bulk, and `mcp__chat__get_conversation_summary` for one known surface.',
|
|
64
|
+
'- Use `mcp__chat__get_relevant_context_bundle` only to narrow among related surfaces, tasks, or handoffs before deeper inspection or handoff; it is not the canonical state source.',
|
|
65
|
+
'- Use `mcp__chat__list_my_tasks` to rediscover tasks in bulk, `mcp__chat__get_task_status` for one known task\'s current state, and `mcp__chat__get_task_history` only when lifecycle details matter.',
|
|
66
|
+
'- Do not call broad state tools by default when the current activation context or exact-target history already answers the request.',
|
|
67
|
+
'If the request requires trackable work beyond replying, claim or create a task before starting. Prefer `mcp__chat__claim_tasks`; use `mcp__chat__create_tasks` only when you genuinely need a new task.',
|
|
68
|
+
'On shared task threads, if collaborator work is explicitly part of the task, wait for the collaborator result or a clear user instruction to stop waiting before sending the final result or moving the task to in_review/done.',
|
|
69
|
+
'If this run produced durable user, project, or task knowledge, proactively update MEMORY.md and relevant notes before ending the run.',
|
|
70
|
+
].join('\n');
|
|
71
|
+
}
|
|
72
|
+
return [
|
|
73
|
+
'[Reply contract]',
|
|
74
|
+
`Current date (Beijing / UTC+8): ${today.week} (${today.date})`,
|
|
75
|
+
'Reply only via `bigbang message send`. Do not output user-visible text directly.',
|
|
76
|
+
'Use `bigbang message send --kind progress` only while work is ongoing. Before the run ends, send one final user-visible message with `bigbang message send --kind final`.',
|
|
77
|
+
'If there is no visible or peer-visible value to add, do not send a message. On shared channels/threads, leave `--peer-delivery` unset for coordination-relevant updates so peers receive the default batch delivery; use `--peer-delivery silent` only for visible no-action status, local bookkeeping, duplicate presence, or waiting updates that should not ordinary-notify peers. If unsure, prefer default batch. Do not set peer delivery in DMs.',
|
|
78
|
+
'- Use `bigbang self state` only for a broad agent-wide work overview across surfaces.',
|
|
79
|
+
'- Use `bigbang runtime presence` only for node / host / runtime facts.',
|
|
80
|
+
'- Use `bigbang workspace inspect` only for current workspace git / directory facts.',
|
|
81
|
+
'- Use `bigbang conversation list` to rediscover surfaces in bulk, and `bigbang conversation summary` for one known surface.',
|
|
82
|
+
'- Use `bigbang context bundle` only to narrow among related surfaces, tasks, or handoffs before deeper inspection or handoff; it is not the canonical state source.',
|
|
83
|
+
'- Use `bigbang task my` to rediscover tasks in bulk, `bigbang task status` for one known task\'s current state, and `bigbang task history` only when lifecycle details matter.',
|
|
84
|
+
'- Do not call broad state tools by default when the current activation context or exact-target history already answers the request.',
|
|
85
|
+
'If the request requires trackable work beyond replying, claim or create a task before starting. Prefer `bigbang task claim`; use `bigbang task create` only when you genuinely need a new task.',
|
|
86
|
+
'On shared task threads, if collaborator work is explicitly part of the task, wait for the collaborator result or a clear user instruction to stop waiting before sending the final result or moving the task to in_review/done.',
|
|
87
|
+
'If this run produced durable user, project, or task knowledge, proactively update MEMORY.md and relevant notes before ending the run.',
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
const ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS = 35_000;
|
|
91
|
+
const CLAUDE_SDK_ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS = 6 * 60_000;
|
|
92
|
+
const MAX_PROCESSED_STEER_RESULT_REQUEST_IDS = 10_000;
|
|
93
|
+
const ORPHANED_QUEUE_ATTEMPT_STALE_MS = 5 * 60_000;
|
|
94
|
+
function inferPlatformRunMode(recordAsUserMessage) {
|
|
95
|
+
return recordAsUserMessage ? 'debug' : 'workflow';
|
|
96
|
+
}
|
|
97
|
+
function isPlatformWorkflowType(value) {
|
|
98
|
+
return value === 'planner_suggestions' || value === 'library_curate';
|
|
99
|
+
}
|
|
100
|
+
function normalizePlatformRunMetadata(value) {
|
|
101
|
+
if (!value || typeof value.definitionId !== 'string' || !value.definitionId.trim())
|
|
102
|
+
return undefined;
|
|
103
|
+
if (!isPlatformWorkflowType(value.workflowType))
|
|
104
|
+
return undefined;
|
|
105
|
+
if (value.mode !== 'workflow' && value.mode !== 'debug')
|
|
106
|
+
return undefined;
|
|
107
|
+
return {
|
|
108
|
+
definitionId: value.definitionId.trim(),
|
|
109
|
+
workflowType: value.workflowType,
|
|
110
|
+
mode: value.mode,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizeOwnerAwarenessMetadata(value) {
|
|
114
|
+
if (!value || value.mode !== 'collaborator_only_thread_reply' || !Number.isFinite(value.triggerSeq)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const mentionedAgentIds = Array.isArray(value.mentionedAgentIds)
|
|
118
|
+
? [...new Set(value.mentionedAgentIds
|
|
119
|
+
.filter((agentId) => typeof agentId === 'string' && agentId.trim().length > 0)
|
|
120
|
+
.map((agentId) => agentId.trim()))]
|
|
121
|
+
: [];
|
|
122
|
+
if (mentionedAgentIds.length === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
return {
|
|
125
|
+
mode: 'collaborator_only_thread_reply',
|
|
126
|
+
triggerSeq: Math.floor(value.triggerSeq),
|
|
127
|
+
mentionedAgentIds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function resolvePlatformRunContext(db, agentRole, agentId) {
|
|
131
|
+
if (!agentId?.trim())
|
|
132
|
+
return null;
|
|
133
|
+
if (agentRole === 'library_curator') {
|
|
134
|
+
return { definitionId: 'library_curator', workflowType: 'library_curate' };
|
|
135
|
+
}
|
|
136
|
+
if (agentRole === 'suggested_planner') {
|
|
137
|
+
return { definitionId: 'suggested_planner', workflowType: 'planner_suggestions' };
|
|
138
|
+
}
|
|
139
|
+
if (agentRole !== 'platform_agent')
|
|
140
|
+
return null;
|
|
141
|
+
try {
|
|
142
|
+
const row = db.prepare(`SELECT pai.definition_id as definitionId,
|
|
143
|
+
pad.workflow_type as workflowType
|
|
144
|
+
FROM platform_agent_instances pai
|
|
145
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
146
|
+
WHERE pai.agent_id = ?
|
|
147
|
+
LIMIT 1`).get(agentId);
|
|
148
|
+
if (!row || !isPlatformWorkflowType(row.workflowType))
|
|
149
|
+
return null;
|
|
150
|
+
return row;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Fall through to legacy builtin config tables below.
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const planner = db.prepare(`SELECT 1
|
|
157
|
+
FROM suggested_planner_configs
|
|
158
|
+
WHERE planner_agent_id = ?
|
|
159
|
+
LIMIT 1`).get(agentId);
|
|
160
|
+
if (planner) {
|
|
161
|
+
return { definitionId: 'suggested_planner', workflowType: 'planner_suggestions' };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Ignore missing legacy tables.
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const curator = db.prepare(`SELECT 1
|
|
169
|
+
FROM library_curator_configs
|
|
170
|
+
WHERE curator_agent_id = ?
|
|
171
|
+
LIMIT 1`).get(agentId);
|
|
172
|
+
if (curator) {
|
|
173
|
+
return { definitionId: 'library_curator', workflowType: 'library_curate' };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Ignore missing legacy tables.
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function ensurePlatformRunMetadata(db, agentRole, agentId, metadata, recordAsUserMessage) {
|
|
182
|
+
if (!isPlatformAgentRole(agentRole))
|
|
183
|
+
return metadata;
|
|
184
|
+
const platformRun = normalizePlatformRunMetadata(metadata?.platformRun);
|
|
185
|
+
const resolved = resolvePlatformRunContext(db, agentRole, agentId);
|
|
186
|
+
if (!resolved && !platformRun)
|
|
187
|
+
return {
|
|
188
|
+
...(metadata ?? {}),
|
|
189
|
+
suppressReplyContract: true,
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
...(metadata ?? {}),
|
|
193
|
+
platformRun: platformRun ?? {
|
|
194
|
+
definitionId: resolved.definitionId,
|
|
195
|
+
workflowType: resolved.workflowType,
|
|
196
|
+
mode: inferPlatformRunMode(recordAsUserMessage),
|
|
197
|
+
},
|
|
198
|
+
suppressReplyContract: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function getPlatformAgentDispatchBlockReason(db, agentRole, agentId, metadata) {
|
|
202
|
+
if (!agentId || !isPlatformAgentRole(agentRole))
|
|
203
|
+
return null;
|
|
204
|
+
const platformRun = normalizePlatformRunMetadata(metadata?.platformRun);
|
|
205
|
+
const resolved = platformRun ?? resolvePlatformRunContext(db, agentRole, agentId);
|
|
206
|
+
if (!resolved)
|
|
207
|
+
return null;
|
|
208
|
+
try {
|
|
209
|
+
const row = db.prepare(`SELECT pai.enabled as instanceEnabled,
|
|
210
|
+
pai.deleted_at as deletedAt,
|
|
211
|
+
pad.enabled as definitionEnabled
|
|
212
|
+
FROM platform_agent_instances pai
|
|
213
|
+
JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
214
|
+
WHERE pai.agent_id = ?
|
|
215
|
+
AND pai.definition_id = ?
|
|
216
|
+
LIMIT 1`).get(agentId, resolved.definitionId);
|
|
217
|
+
if (!row)
|
|
218
|
+
return null;
|
|
219
|
+
if (row.deletedAt != null)
|
|
220
|
+
return 'Platform agent instance has been deleted';
|
|
221
|
+
if (row.definitionEnabled !== 1)
|
|
222
|
+
return 'Platform definition is disabled';
|
|
223
|
+
if (row.instanceEnabled !== 1)
|
|
224
|
+
return 'Platform instance is disabled';
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Legacy schemas may not have generic platform-agent tables yet.
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const RECOVERABLE_ENDED_RUN_ERRORS = new Set([
|
|
232
|
+
'Core restarted before run completed',
|
|
233
|
+
'Node disconnected during dispatch',
|
|
234
|
+
'Node not connected',
|
|
235
|
+
]);
|
|
236
|
+
function isRecoverableEndedRunError(error) {
|
|
237
|
+
const normalized = error?.trim();
|
|
238
|
+
if (!normalized)
|
|
239
|
+
return false;
|
|
240
|
+
return RECOVERABLE_ENDED_RUN_ERRORS.has(normalized) || normalized.startsWith('Agent node disconnected: ');
|
|
241
|
+
}
|
|
242
|
+
export class ExecutionDispatcher {
|
|
243
|
+
db;
|
|
244
|
+
config;
|
|
245
|
+
nodeRegistry;
|
|
246
|
+
getAgentById;
|
|
247
|
+
pendingDispatchAcceptances = new Map();
|
|
248
|
+
pendingSteerResponses = new Map();
|
|
249
|
+
processedSteerResultRequestIds = new Set();
|
|
250
|
+
steerChains = new Map();
|
|
251
|
+
queueRetryTimers = new Map();
|
|
252
|
+
libraryService;
|
|
253
|
+
constructor(params) {
|
|
254
|
+
this.db = params.db;
|
|
255
|
+
this.config = params.config;
|
|
256
|
+
this.nodeRegistry = params.nodeRegistry;
|
|
257
|
+
this.getAgentById = params.getAgentById;
|
|
258
|
+
this.libraryService = params.libraryService;
|
|
259
|
+
}
|
|
260
|
+
setLibraryService(libraryService) {
|
|
261
|
+
this.libraryService = libraryService;
|
|
262
|
+
}
|
|
263
|
+
async dispatchDream(agentId) {
|
|
264
|
+
const agent = this.getAgentById(agentId);
|
|
265
|
+
if (!agent)
|
|
266
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
267
|
+
if (!agent.memoryNetworkEnabled)
|
|
268
|
+
throw new Error('Memory Network is not enabled for this agent');
|
|
269
|
+
if (!agent.dreamEnabled)
|
|
270
|
+
throw new Error('Dream is not enabled for this agent');
|
|
271
|
+
if (!agent.nodeId)
|
|
272
|
+
throw new Error(`Agent has no node: ${agentId}`);
|
|
273
|
+
const conversation = this.db.prepare(`SELECT id as conversationId,
|
|
274
|
+
session_key as sessionKey,
|
|
275
|
+
workspace_path as workspacePath
|
|
276
|
+
FROM conversations
|
|
277
|
+
WHERE agent_id = ?
|
|
278
|
+
AND thread_kind = 'direct'
|
|
279
|
+
AND is_primary_thread = 1
|
|
280
|
+
LIMIT 1`).get(agentId);
|
|
281
|
+
if (!conversation)
|
|
282
|
+
throw new Error(`No primary DM conversation for agent: ${agentId}`);
|
|
283
|
+
const lastDreamSeq = agent.lastDreamSeq ?? null;
|
|
284
|
+
const dreamMessages = listAgentMessagesSinceSeq(this.db, agentId, lastDreamSeq);
|
|
285
|
+
const newMessageCount = dreamMessages.length || countAgentMessagesSinceSeq(this.db, agentId, lastDreamSeq);
|
|
286
|
+
if (newMessageCount <= 0)
|
|
287
|
+
throw new Error('No new messages for dream');
|
|
288
|
+
if (!claimDreamRunLock(this.db, agentId))
|
|
289
|
+
throw new Error(`Dream already in progress: ${agentId}`);
|
|
290
|
+
const dreamContextText = buildDreamContextText(dreamMessages);
|
|
291
|
+
const dreamPrompt = buildDreamSystemPrompt({
|
|
292
|
+
lastDreamAt: agent.lastDreamAt ?? null,
|
|
293
|
+
newMessageCount,
|
|
294
|
+
});
|
|
295
|
+
const runId = randomUUID();
|
|
296
|
+
const hostKey = `dream:${agentId}:claude_sdk`;
|
|
297
|
+
const dispatchMode = 'dream';
|
|
298
|
+
const dreamDisabledToolKinds = ['execute', 'edit', 'delete', 'move'];
|
|
299
|
+
createRun(this.db, { runId, sessionKey: conversation.sessionKey, promptText: dreamPrompt });
|
|
300
|
+
upsertPendingRunDebugInput(this.db, {
|
|
301
|
+
runId,
|
|
302
|
+
conversationId: conversation.conversationId,
|
|
303
|
+
sessionKey: conversation.sessionKey,
|
|
304
|
+
dispatchMode,
|
|
305
|
+
replyTarget: null,
|
|
306
|
+
systemPromptText: dreamPrompt,
|
|
307
|
+
contextText: dreamContextText,
|
|
308
|
+
promptText: dreamPrompt,
|
|
309
|
+
dispatchedPromptText: dreamContextText
|
|
310
|
+
? `${dreamPrompt}\n\n## 新消息\n${dreamContextText}`
|
|
311
|
+
: dreamPrompt,
|
|
312
|
+
agentSurfaceMode: 'bigbang',
|
|
313
|
+
activationMetadataJson: JSON.stringify({ dreamRun: true, agentId, dreamStartSeq: lastDreamSeq }),
|
|
314
|
+
});
|
|
315
|
+
const node = this.nodeRegistry?.getNode(agent.nodeId);
|
|
316
|
+
if (!node) {
|
|
317
|
+
releaseDreamRunLock(this.db, agentId);
|
|
318
|
+
finishRun(this.db, { runId, error: 'Node not connected' });
|
|
319
|
+
throw new Error(`Node not connected: ${agent.nodeId}`);
|
|
320
|
+
}
|
|
321
|
+
if (!node.agentTypes.includes('claude_sdk')) {
|
|
322
|
+
const errorMessage = 'Node does not support claude_sdk for dream runs';
|
|
323
|
+
releaseDreamRunLock(this.db, agentId);
|
|
324
|
+
finishRun(this.db, { runId, error: errorMessage });
|
|
325
|
+
throw new Error(`${errorMessage}: ${agent.nodeId}`);
|
|
326
|
+
}
|
|
327
|
+
const dispatchAgentSurfaceMode = 'bigbang';
|
|
328
|
+
const nodeSurfaceReadiness = this.loadNodeSurfaceReadiness(agent.nodeId);
|
|
329
|
+
const failDreamNodeSurfaceReadiness = (errorMessage) => {
|
|
330
|
+
releaseDreamRunLock(this.db, agentId);
|
|
331
|
+
finishRun(this.db, { runId, error: errorMessage });
|
|
332
|
+
throw new Error(`${errorMessage}: ${agent.nodeId}`);
|
|
333
|
+
};
|
|
334
|
+
if (!nodeSurfaceReadiness.hasSnapshot) {
|
|
335
|
+
failDreamNodeSurfaceReadiness('Node surface readiness missing');
|
|
336
|
+
}
|
|
337
|
+
if (node.processInstanceId && nodeSurfaceReadiness.processInstanceId !== node.processInstanceId) {
|
|
338
|
+
failDreamNodeSurfaceReadiness('Node surface readiness stale');
|
|
339
|
+
}
|
|
340
|
+
if (nodeSurfaceReadiness.agentSurfaceMode !== dispatchAgentSurfaceMode) {
|
|
341
|
+
const actual = nodeSurfaceReadiness.agentSurfaceMode ?? 'missing';
|
|
342
|
+
failDreamNodeSurfaceReadiness(`Node surface mismatch: expected ${dispatchAgentSurfaceMode}, got ${actual}`);
|
|
343
|
+
}
|
|
344
|
+
if (dispatchAgentSurfaceMode === 'bigbang' && nodeSurfaceReadiness.bigbangCliAvailable !== true) {
|
|
345
|
+
failDreamNodeSurfaceReadiness('Bigbang CLI unavailable on node');
|
|
346
|
+
}
|
|
347
|
+
const channelBridgeConfig = {
|
|
348
|
+
agentId,
|
|
349
|
+
conversationId: conversation.conversationId,
|
|
350
|
+
serverUrl: this.resolveChannelBridgeServerUrl(agent.nodeId),
|
|
351
|
+
authToken: this.config.internalAgentAuthToken,
|
|
352
|
+
};
|
|
353
|
+
const dreamEnvVars = loadDreamAnthropicEnv(resolveDreamEnvPath());
|
|
354
|
+
const acceptance = this.waitForDispatchAcceptance(runId, agent.nodeId, conversation.conversationId);
|
|
355
|
+
const sent = this.nodeRegistry.send(agent.nodeId, {
|
|
356
|
+
type: 'run.dispatch',
|
|
357
|
+
runId,
|
|
358
|
+
conversationId: conversation.conversationId,
|
|
359
|
+
agentType: 'claude_sdk',
|
|
360
|
+
agentSurfaceMode: 'bigbang',
|
|
361
|
+
workspacePath: conversation.workspacePath ?? agent.workspacePath ?? null,
|
|
362
|
+
agentName: agent.name,
|
|
363
|
+
envVars: dreamEnvVars,
|
|
364
|
+
disabledToolKinds: dreamDisabledToolKinds,
|
|
365
|
+
prompt: dreamPrompt,
|
|
366
|
+
sessionKey: conversation.sessionKey,
|
|
367
|
+
hostKey,
|
|
368
|
+
dispatchMode,
|
|
369
|
+
systemPromptText: dreamPrompt,
|
|
370
|
+
contextText: dreamContextText ?? undefined,
|
|
371
|
+
channelBridgeConfig,
|
|
372
|
+
});
|
|
373
|
+
if (!sent) {
|
|
374
|
+
this.clearPendingDispatchAcceptance(runId);
|
|
375
|
+
releaseDreamRunLock(this.db, agentId);
|
|
376
|
+
finishRun(this.db, { runId, error: 'Node disconnected during dispatch' });
|
|
377
|
+
throw new Error(`Node disconnected: ${agent.nodeId}`);
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
await acceptance;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
releaseDreamRunLock(this.db, agentId);
|
|
384
|
+
finishRun(this.db, { runId, error: String(error?.message ?? error) });
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
return { runId };
|
|
388
|
+
}
|
|
389
|
+
async dispatchPrompt(conversationId, promptText, options) {
|
|
390
|
+
const row = this.db.prepare(`SELECT session_key as sessionKey, agent_type as agentType,
|
|
391
|
+
channel_id as channelId, is_primary_thread as isPrimaryThread,
|
|
392
|
+
workspace_path as workspacePath, env_vars as envVarsJson,
|
|
393
|
+
node_id as nodeId, agent_id as agentId, reply_target as replyTarget,
|
|
394
|
+
thread_kind as threadKind, thread_root_id as threadRootId,
|
|
395
|
+
active_project_id as activeProjectId, user_id as userId, title
|
|
396
|
+
, personal_library_enabled as personalLibraryEnabled
|
|
397
|
+
, builtin_skill_roots as builtinSkillRootsJson
|
|
398
|
+
FROM conversations WHERE id = ?`).get(conversationId);
|
|
399
|
+
if (!row)
|
|
400
|
+
throw new Error(`Unknown conversation: ${conversationId}`);
|
|
401
|
+
if (!row.nodeId)
|
|
402
|
+
throw new Error('No agent node assigned. Connect an agent-node first.');
|
|
403
|
+
const runId = randomUUID();
|
|
404
|
+
const hostKey = `conversation:${conversationId}:${row.agentType}`;
|
|
405
|
+
const dispatchMode = this.getDispatchMode(row.sessionKey);
|
|
406
|
+
const driver = getRuntimeDriver(row.agentType);
|
|
407
|
+
const recordAsUserMessage = options?.recordAsUserMessage ?? true;
|
|
408
|
+
const promptAttachments = sanitizeFileRefs(options?.attachments);
|
|
409
|
+
const configAgentSurfaceMode = normalizeDispatchAgentSurfaceMode(this.config.agentSurfaceMode, 'bigbang');
|
|
410
|
+
const dispatchAgentSurfaceMode = normalizeDispatchAgentSurfaceMode(options?.agentSurfaceMode, configAgentSurfaceMode);
|
|
411
|
+
// Persist run and user message BEFORE node connectivity check so they survive
|
|
412
|
+
// even if the node is offline — the user's message will still be visible after refresh.
|
|
413
|
+
createRun(this.db, { runId, sessionKey: row.sessionKey, promptText });
|
|
414
|
+
if (options?.queuedPromptId != null) {
|
|
415
|
+
this.markQueueDispatchAttempt(options.queuedPromptId, runId, 'dispatch');
|
|
416
|
+
}
|
|
417
|
+
let contextText = '';
|
|
418
|
+
let resumeContextText = '';
|
|
419
|
+
const contextSections = [];
|
|
420
|
+
const resumeSections = [];
|
|
421
|
+
let optionActivationExactHistorySupersedesResumeExact = false;
|
|
422
|
+
let systemPromptText = '';
|
|
423
|
+
const activeProject = row.activeProjectId
|
|
424
|
+
? this.db.prepare(`SELECT name, backend_type as backendType, root_path as rootPath
|
|
425
|
+
FROM projects
|
|
426
|
+
WHERE project_id = ?
|
|
427
|
+
AND archived_at IS NULL`).get(row.activeProjectId)
|
|
428
|
+
: undefined;
|
|
429
|
+
const activeProjectSection = buildActiveProjectSection(activeProject ?? null);
|
|
430
|
+
const activeProjectContextSection = createPromptContextSection('active_project', activeProjectSection);
|
|
431
|
+
const dispatchToday = getCurrentISOWeek();
|
|
432
|
+
let agentEnvVars;
|
|
433
|
+
let disabledToolKinds;
|
|
434
|
+
let activationMetadata = options?.activationMetadata
|
|
435
|
+
? { ...options.activationMetadata }
|
|
436
|
+
: undefined;
|
|
437
|
+
if (options?.suppressReplyContract === true) {
|
|
438
|
+
activationMetadata = {
|
|
439
|
+
...(activationMetadata ?? {}),
|
|
440
|
+
suppressReplyContract: true,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (promptAttachments.length > 0) {
|
|
444
|
+
activationMetadata = {
|
|
445
|
+
...(activationMetadata ?? {}),
|
|
446
|
+
attachments: promptAttachments,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const promptAttachmentIds = sanitizeAttachmentIds(options?.attachmentIds);
|
|
450
|
+
if (promptAttachmentIds.length > 0) {
|
|
451
|
+
activationMetadata = {
|
|
452
|
+
...(activationMetadata ?? {}),
|
|
453
|
+
attachmentIds: promptAttachmentIds,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const runtimeOverrides = normalizePromptRuntimeOverrides(options?.runtimeOverrides);
|
|
457
|
+
if (runtimeOverrides) {
|
|
458
|
+
activationMetadata = {
|
|
459
|
+
...(activationMetadata ?? {}),
|
|
460
|
+
runtimeOverrides,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const promptSlashCommandName = activationMetadata?.slashCommand?.name ?? null;
|
|
464
|
+
const workspaceToolSlashRequested = isWorkspaceToolPromptSlashCommandName(promptSlashCommandName);
|
|
465
|
+
const suppressDirectMemoryHints = workspaceToolSlashRequested;
|
|
466
|
+
if (row.agentId) {
|
|
467
|
+
const agent = this.getAgentById(row.agentId);
|
|
468
|
+
if (agent) {
|
|
469
|
+
activationMetadata = ensurePlatformRunMetadata(this.db, agent.agentRole, row.agentId, activationMetadata, recordAsUserMessage);
|
|
470
|
+
agentEnvVars = agent.envVars;
|
|
471
|
+
disabledToolKinds = agent.disabledToolKinds;
|
|
472
|
+
const skipDirectActivationEnvelope = isPlatformAgentRole(agent.agentRole)
|
|
473
|
+
&& activationMetadata?.platformRun?.mode === 'debug';
|
|
474
|
+
systemPromptText = isPlatformAgentRole(agent.agentRole)
|
|
475
|
+
? buildPlatformAgentSystemPrompt({
|
|
476
|
+
definitionId: activationMetadata?.platformRun?.definitionId ?? 'platform',
|
|
477
|
+
workflowType: activationMetadata?.platformRun?.workflowType ?? 'planner_suggestions',
|
|
478
|
+
mode: activationMetadata?.platformRun?.mode ?? inferPlatformRunMode(recordAsUserMessage),
|
|
479
|
+
agentName: agent.name,
|
|
480
|
+
workspacePath: row.workspacePath ?? this.config.workspaceRoot,
|
|
481
|
+
})
|
|
482
|
+
: buildAgentSessionSystemPromptText({
|
|
483
|
+
agentName: agent.name,
|
|
484
|
+
agentBio: agent.description || undefined,
|
|
485
|
+
agentDescription: agent.systemPrompt || undefined,
|
|
486
|
+
workspacePath: row.workspacePath ?? this.config.workspaceRoot,
|
|
487
|
+
agentSurfaceMode: dispatchAgentSurfaceMode,
|
|
488
|
+
extraCriticalRules: workspaceToolSlashRequested
|
|
489
|
+
? [
|
|
490
|
+
'- For this workspace-tool slash-command run, the slash command body and injected workspace-tool guidance are the startup context. Skip the default Startup sequence memory pre-read; do not read MEMORY.md, notes, work logs, channel history, or library documents before acting unless the user explicitly asks for those files.',
|
|
491
|
+
dispatchAgentSurfaceMode === 'mcp'
|
|
492
|
+
? '- In workspace-tool publish mode, after publish_workspace_tool succeeds, immediately send one final visible reply with `mcp__chat__send_message(content="...", kind="final")` before any other inspection or cleanup.'
|
|
493
|
+
: '- In workspace-tool publish mode, after bigbang tool publish succeeds, immediately send one final visible reply with `bigbang message send --kind final` before any other inspection or cleanup.',
|
|
494
|
+
]
|
|
495
|
+
: undefined,
|
|
496
|
+
});
|
|
497
|
+
let replayOverlapMessages = options?.replayOverlapRecentMessages;
|
|
498
|
+
let directMemoryHints = suppressDirectMemoryHints
|
|
499
|
+
? []
|
|
500
|
+
: buildDefaultDirectMemoryHints({
|
|
501
|
+
threadRootId: row.threadRootId,
|
|
502
|
+
currentWeek: dispatchToday.week,
|
|
503
|
+
});
|
|
504
|
+
let dmChannelId = null;
|
|
505
|
+
let humanUserName = null;
|
|
506
|
+
let dmReplyTarget = null;
|
|
507
|
+
let msgSeq = null;
|
|
508
|
+
let directPromptContent = null;
|
|
509
|
+
let directMessageContentOverride = null;
|
|
510
|
+
if (recordAsUserMessage && row.threadKind === 'direct') {
|
|
511
|
+
const persistedMessage = this.persistDirectUserMessageForConversation({
|
|
512
|
+
conversationId,
|
|
513
|
+
conversation: {
|
|
514
|
+
agentId: row.agentId,
|
|
515
|
+
replyTarget: row.replyTarget,
|
|
516
|
+
threadKind: row.threadKind,
|
|
517
|
+
threadRootId: row.threadRootId,
|
|
518
|
+
},
|
|
519
|
+
promptText,
|
|
520
|
+
options,
|
|
521
|
+
attachments: promptAttachments,
|
|
522
|
+
attachmentIds: promptAttachmentIds,
|
|
523
|
+
});
|
|
524
|
+
dmChannelId = persistedMessage.channelId;
|
|
525
|
+
humanUserName = persistedMessage.senderName;
|
|
526
|
+
dmReplyTarget = persistedMessage.replyTarget;
|
|
527
|
+
msgSeq = persistedMessage.seq;
|
|
528
|
+
activationMetadata = {
|
|
529
|
+
...(activationMetadata ?? {}),
|
|
530
|
+
triggerMessage: {
|
|
531
|
+
messageId: persistedMessage.messageId,
|
|
532
|
+
seq: msgSeq,
|
|
533
|
+
target: dmReplyTarget,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
directPromptContent = buildDispatchPromptContentForDirectMessage({
|
|
537
|
+
promptText,
|
|
538
|
+
slashCommand: activationMetadata?.slashCommand,
|
|
539
|
+
});
|
|
540
|
+
directMessageContentOverride = buildDisplayContentOverrideForDirectMessage({
|
|
541
|
+
slashCommand: activationMetadata?.slashCommand,
|
|
542
|
+
});
|
|
543
|
+
if (skipDirectActivationEnvelope) {
|
|
544
|
+
promptText = directPromptContent ?? promptText;
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
promptText = buildDirectActivationPrompt({
|
|
548
|
+
agentName: agent.name,
|
|
549
|
+
senderName: humanUserName,
|
|
550
|
+
replyTarget: dmReplyTarget,
|
|
551
|
+
content: directPromptContent,
|
|
552
|
+
memoryHints: directMemoryHints,
|
|
553
|
+
today: dispatchToday,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (!skipDirectActivationEnvelope && dispatchMode !== 'cold_start' && dmReplyTarget && msgSeq != null && directPromptContent != null) {
|
|
557
|
+
const claudeSdkRootDmResume = agent.agentType === 'claude_sdk'
|
|
558
|
+
&& !row.threadRootId
|
|
559
|
+
&& dmReplyTarget.startsWith('dm:@');
|
|
560
|
+
const envelope = buildDirectSurfaceActivationEnvelope(this.db, {
|
|
561
|
+
agentId: row.agentId,
|
|
562
|
+
channelId: dmChannelId,
|
|
563
|
+
replyTarget: dmReplyTarget,
|
|
564
|
+
triggerSeq: msgSeq,
|
|
565
|
+
threadRootId: row.threadRootId,
|
|
566
|
+
...(directMessageContentOverride
|
|
567
|
+
? {
|
|
568
|
+
messageContentOverrides: {
|
|
569
|
+
[persistedMessage.messageId]: directMessageContentOverride,
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
: {}),
|
|
573
|
+
today: dispatchToday,
|
|
574
|
+
});
|
|
575
|
+
replayOverlapMessages = envelope.replayOverlapRecentMessages;
|
|
576
|
+
const directPolicy = envelope.policy.surfaceKind === 'direct' ? envelope.policy : null;
|
|
577
|
+
const isRootDirectThread = !row.threadRootId;
|
|
578
|
+
const suppressExactTargetHistoryForAppServerRootDm = agent.agentType === 'codex_app_server'
|
|
579
|
+
&& isRootDirectThread
|
|
580
|
+
&& dmReplyTarget.startsWith('dm:@');
|
|
581
|
+
promptText = buildDirectActivationPrompt({
|
|
582
|
+
agentName: agent.name,
|
|
583
|
+
senderName: humanUserName,
|
|
584
|
+
replyTarget: dmReplyTarget,
|
|
585
|
+
content: directPromptContent,
|
|
586
|
+
memoryHints: directMemoryHints,
|
|
587
|
+
today: dispatchToday,
|
|
588
|
+
});
|
|
589
|
+
const envelopeResumeContextSections = stripWorkspaceMemoryReminderSections(envelope.resumeContextSections, workspaceToolSlashRequested);
|
|
590
|
+
const claudeSdkResumeSupplementSections = claudeSdkRootDmResume
|
|
591
|
+
? [
|
|
592
|
+
...(suppressDirectMemoryHints
|
|
593
|
+
? []
|
|
594
|
+
: [
|
|
595
|
+
createPromptContextSection('workspace_memory_reminder', `[Workspace memory reminder]
|
|
596
|
+
${buildWorkspaceMemoryReminder(directMemoryHints, dispatchToday)}`),
|
|
597
|
+
]),
|
|
598
|
+
...buildDirectActivationContextSections({
|
|
599
|
+
target: dmReplyTarget,
|
|
600
|
+
recentMessages: [],
|
|
601
|
+
olderUnreadCount: 0,
|
|
602
|
+
rootMessage: envelope.activationContext.rootMessage,
|
|
603
|
+
dmContextSnapshot: envelope.activationContext.dmContextSnapshot,
|
|
604
|
+
dmActiveTaskThreads: envelope.activationContext.dmActiveTaskThreads,
|
|
605
|
+
dmActiveTaskThreadCount: envelope.activationContext.dmActiveTaskThreadCount,
|
|
606
|
+
}, {
|
|
607
|
+
includeDmContextSnapshot: directPolicy?.promptIncludeDmSnapshotByDefault ?? false,
|
|
608
|
+
historyLimit: 0,
|
|
609
|
+
dmActiveTaskThreadsMode: directPolicy?.dmTaskThreadPresence.renderMode ?? 'off',
|
|
610
|
+
includeRootMessage: true,
|
|
611
|
+
}),
|
|
612
|
+
].filter((section) => Boolean(section))
|
|
613
|
+
: envelopeResumeContextSections;
|
|
614
|
+
if (claudeSdkRootDmResume) {
|
|
615
|
+
if (claudeSdkResumeSupplementSections?.length) {
|
|
616
|
+
resumeSections.push(...claudeSdkResumeSupplementSections);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const envelopeActivationContextSections = stripSurfaceSystemStatusSections(envelope.activationContextSections);
|
|
621
|
+
const activationContextSplit = splitExactTargetHistorySections(envelopeActivationContextSections);
|
|
622
|
+
const liveUnreadEnvelope = buildDirectSurfaceActivationEnvelope(this.db, {
|
|
623
|
+
agentId: row.agentId,
|
|
624
|
+
channelId: dmChannelId,
|
|
625
|
+
replyTarget: dmReplyTarget,
|
|
626
|
+
triggerSeq: msgSeq,
|
|
627
|
+
threadRootId: row.threadRootId,
|
|
628
|
+
constrainRecentMessagesToUnread: true,
|
|
629
|
+
excludeRecentMessageSenderId: row.agentId,
|
|
630
|
+
...(directMessageContentOverride
|
|
631
|
+
? {
|
|
632
|
+
messageContentOverrides: {
|
|
633
|
+
[persistedMessage.messageId]: directMessageContentOverride,
|
|
634
|
+
},
|
|
635
|
+
}
|
|
636
|
+
: {}),
|
|
637
|
+
today: dispatchToday,
|
|
638
|
+
});
|
|
639
|
+
const liveUnreadExactTargetSections = extractExactTargetHistorySections(stripSurfaceSystemStatusSections(liveUnreadEnvelope.activationContextSections));
|
|
640
|
+
const nonSnapshotActivationSections = (activationContextSplit.nonExact ?? [])
|
|
641
|
+
.filter((section) => section.kind !== 'dm_context_snapshot');
|
|
642
|
+
const activationContextSections = [
|
|
643
|
+
...nonSnapshotActivationSections.filter(shouldKeepResumeOnlyActivationSectionLive),
|
|
644
|
+
...(row.threadRootId && !suppressExactTargetHistoryForAppServerRootDm ? liveUnreadExactTargetSections : []),
|
|
645
|
+
];
|
|
646
|
+
const resumeOnlyActivationSections = nonSnapshotActivationSections
|
|
647
|
+
.filter((section) => !shouldKeepResumeOnlyActivationSectionLive(section));
|
|
648
|
+
const resumeContextSections = stripSurfaceSystemStatusSections(claudeSdkResumeSupplementSections);
|
|
649
|
+
if (activationContextSections.length) {
|
|
650
|
+
contextSections.push(...activationContextSections);
|
|
651
|
+
}
|
|
652
|
+
else if (!envelopeActivationContextSections?.length && envelope.activationContextText?.trim()) {
|
|
653
|
+
const inferredActivationSections = inferPromptContextSections(envelope.activationContextText);
|
|
654
|
+
if (inferredActivationSections.length > 0) {
|
|
655
|
+
const split = splitResumeOnlyActivationSections(inferredActivationSections);
|
|
656
|
+
contextSections.push(...split.live);
|
|
657
|
+
resumeSections.push(...split.resume);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
resumeSections.push(...resumeOnlyActivationSections);
|
|
661
|
+
resumeSections.push(...activationContextSplit.exact);
|
|
662
|
+
if (resumeContextSections?.length) {
|
|
663
|
+
resumeSections.push(...resumeContextSections);
|
|
664
|
+
}
|
|
665
|
+
else if (envelope.resumeContextText?.trim()) {
|
|
666
|
+
resumeSections.push(...inferPromptContextSections(envelope.resumeContextText)
|
|
667
|
+
.filter((section) => shouldKeepIncomingPromptContextSection(section, {
|
|
668
|
+
suppressWorkspaceMemoryReminder: workspaceToolSlashRequested,
|
|
669
|
+
})));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const relevantContextBundle = buildContextBundle(this.db, agent, conversationId, {
|
|
675
|
+
maxSurfaces: 1,
|
|
676
|
+
maxTasks: 1,
|
|
677
|
+
maxHandoffs: 1,
|
|
678
|
+
});
|
|
679
|
+
const relevantContextHintText = shouldPreinjectRelevantContextHint({
|
|
680
|
+
target: row.replyTarget ?? '',
|
|
681
|
+
strongSignals: extractRelevantContextStrongSignals(relevantContextBundle),
|
|
682
|
+
})
|
|
683
|
+
? buildRelevantContextHintSection(relevantContextBundle)
|
|
684
|
+
: '';
|
|
685
|
+
const slashCommandName = promptSlashCommandName;
|
|
686
|
+
const uiPanelRequested = isPanelPromptSlashCommandName(slashCommandName);
|
|
687
|
+
const uiPanelGuidanceSection = uiPanelRequested
|
|
688
|
+
? createPromptContextSection('ui_panel_guidance', buildUiPanelGuidancePromptSection({
|
|
689
|
+
layoutPlan: isLayoutPlanPromptSlashCommandName(slashCommandName),
|
|
690
|
+
}))
|
|
691
|
+
: null;
|
|
692
|
+
if (uiPanelGuidanceSection) {
|
|
693
|
+
contextSections.push(uiPanelGuidanceSection);
|
|
694
|
+
}
|
|
695
|
+
const workspaceToolRequested = isWorkspaceToolPromptSlashCommandName(slashCommandName);
|
|
696
|
+
const workspaceToolGuidanceSection = workspaceToolRequested
|
|
697
|
+
? createPromptContextSection('workspace_tool_guidance', buildWorkspaceToolGuidancePromptSection({
|
|
698
|
+
layoutPlan: isLayoutPlanPromptSlashCommandName(slashCommandName),
|
|
699
|
+
}))
|
|
700
|
+
: null;
|
|
701
|
+
if (workspaceToolGuidanceSection) {
|
|
702
|
+
contextSections.push(workspaceToolGuidanceSection);
|
|
703
|
+
}
|
|
704
|
+
const documentLibraryRequested = activationMetadata?.slashCommand?.name === 'document';
|
|
705
|
+
const documentGuidanceSection = documentLibraryRequested
|
|
706
|
+
? createPromptContextSection('library_document_guidance', buildLibraryDocumentGuidancePromptSection())
|
|
707
|
+
: null;
|
|
708
|
+
if (documentGuidanceSection) {
|
|
709
|
+
contextSections.push(documentGuidanceSection);
|
|
710
|
+
}
|
|
711
|
+
if (options?.activationContextSections?.length || options?.activationContextText?.trim()) {
|
|
712
|
+
// Keep live context limited to external surface state. Exact history
|
|
713
|
+
// belongs in resume/recovery context unless it is an unread delta
|
|
714
|
+
// assembled from the current activation path.
|
|
715
|
+
const activationContextSections = stripWorkspaceMemoryReminderSections(stripSurfaceSystemStatusSections(options.activationContextSections), workspaceToolSlashRequested);
|
|
716
|
+
const useResumeOnlyActivationContext = options.activationContextMode === 'resume_only'
|
|
717
|
+
&& dispatchMode !== 'cold_start';
|
|
718
|
+
const includeExactTargetHistoryInLiveContext = activationMetadata?.liveContextIncludeExactTargetHistory === true;
|
|
719
|
+
if (activationContextSections?.length) {
|
|
720
|
+
if (useResumeOnlyActivationContext) {
|
|
721
|
+
const split = splitResumeOnlyActivationSections(activationContextSections, {
|
|
722
|
+
includeExactTargetHistory: includeExactTargetHistoryInLiveContext,
|
|
723
|
+
});
|
|
724
|
+
if (split.live.length > 0) {
|
|
725
|
+
contextSections.push(...split.live);
|
|
726
|
+
}
|
|
727
|
+
if (split.resume.length > 0) {
|
|
728
|
+
resumeSections.push(...split.resume);
|
|
729
|
+
}
|
|
730
|
+
optionActivationExactHistorySupersedesResumeExact = extractExactTargetHistorySections([
|
|
731
|
+
...split.live,
|
|
732
|
+
...split.resume,
|
|
733
|
+
]).length > 0;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
const split = splitExactTargetHistorySections(activationContextSections);
|
|
737
|
+
contextSections.push(...split.nonExact);
|
|
738
|
+
if (dispatchMode === 'cold_start') {
|
|
739
|
+
contextSections.push(...split.exact);
|
|
740
|
+
}
|
|
741
|
+
else if (includeExactTargetHistoryInLiveContext) {
|
|
742
|
+
contextSections.push(...split.exact);
|
|
743
|
+
optionActivationExactHistorySupersedesResumeExact = split.exact.length > 0;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
resumeSections.push(...split.exact);
|
|
747
|
+
optionActivationExactHistorySupersedesResumeExact = split.exact.length > 0;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const activationSections = inferPromptContextSections(stripSurfaceSystemStatusText(options.activationContextText))
|
|
753
|
+
.filter((section) => shouldKeepIncomingPromptContextSection(section, {
|
|
754
|
+
suppressWorkspaceMemoryReminder: workspaceToolSlashRequested,
|
|
755
|
+
}));
|
|
756
|
+
if (activationSections.length > 0) {
|
|
757
|
+
if (useResumeOnlyActivationContext) {
|
|
758
|
+
const split = splitResumeOnlyActivationSections(activationSections, {
|
|
759
|
+
includeExactTargetHistory: includeExactTargetHistoryInLiveContext,
|
|
760
|
+
});
|
|
761
|
+
contextSections.push(...split.live);
|
|
762
|
+
resumeSections.push(...split.resume);
|
|
763
|
+
optionActivationExactHistorySupersedesResumeExact = extractExactTargetHistorySections([
|
|
764
|
+
...split.live,
|
|
765
|
+
...split.resume,
|
|
766
|
+
]).length > 0;
|
|
767
|
+
}
|
|
768
|
+
else if (dispatchMode !== 'cold_start') {
|
|
769
|
+
const split = splitExactTargetHistorySections(activationSections);
|
|
770
|
+
if (split.nonExact.length > 0) {
|
|
771
|
+
contextSections.push(...split.nonExact);
|
|
772
|
+
// Text-only activation context is a legacy queued-dispatch fallback.
|
|
773
|
+
// Keep its exact recent window live so reused runtime sessions can
|
|
774
|
+
// see the queued turn's target-local delta, while still suppressing
|
|
775
|
+
// stale exact history from resumeContextText below.
|
|
776
|
+
contextSections.push(...split.exact);
|
|
777
|
+
optionActivationExactHistorySupersedesResumeExact = split.exact.length > 0;
|
|
778
|
+
}
|
|
779
|
+
else if (includeExactTargetHistoryInLiveContext) {
|
|
780
|
+
contextSections.push(...split.exact);
|
|
781
|
+
optionActivationExactHistorySupersedesResumeExact = split.exact.length > 0;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
resumeSections.push(...split.exact);
|
|
785
|
+
optionActivationExactHistorySupersedesResumeExact = split.exact.length > 0;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
contextSections.push(...activationSections);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const latestSurfaceSystemStatusSection = buildSurfaceSystemStatusSection(this.db, {
|
|
795
|
+
channelId: row.threadKind === 'direct' && row.agentId ? `dm:${row.agentId}` : row.channelId,
|
|
796
|
+
replyTarget: (dmReplyTarget ?? row.replyTarget ?? '').trim(),
|
|
797
|
+
threadRootId: row.threadRootId,
|
|
798
|
+
});
|
|
799
|
+
if (latestSurfaceSystemStatusSection) {
|
|
800
|
+
contextSections.push(latestSurfaceSystemStatusSection);
|
|
801
|
+
}
|
|
802
|
+
const savedAcpSessionId = getSessionAcpSessionId(this.db, row.sessionKey);
|
|
803
|
+
const willStartFreshRuntimeSession = dispatchMode === 'cold_start'
|
|
804
|
+
|| (agent.agentType === 'codex_app_server' && !savedAcpSessionId);
|
|
805
|
+
if (willStartFreshRuntimeSession) {
|
|
806
|
+
const rollingSummarySectionText = buildRollingSummaryPromptSection(this.db, conversationId);
|
|
807
|
+
const rollingSummarySection = createPromptContextSection('conversation_rolling_summary', rollingSummarySectionText);
|
|
808
|
+
if (rollingSummarySection) {
|
|
809
|
+
contextSections.push(rollingSummarySection);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (relevantContextHintText) {
|
|
813
|
+
const relevantContextSection = createPromptContextSection('relevant_context_hint', relevantContextHintText);
|
|
814
|
+
if (dispatchMode === 'cold_start') {
|
|
815
|
+
if (relevantContextSection) {
|
|
816
|
+
contextSections.push(relevantContextSection);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else if (relevantContextSection) {
|
|
820
|
+
// Fresh resume wake-ups merge resumeContextText + contextText, so resume-only hints
|
|
821
|
+
// must live in exactly one field to avoid duplicate prompt sections.
|
|
822
|
+
resumeSections.push(relevantContextSection);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (options?.resumeContextSections?.length || options?.resumeContextText?.trim()) {
|
|
826
|
+
const resumeContextSections = stripWorkspaceMemoryReminderSections(stripSurfaceSystemStatusSections(options.resumeContextSections), workspaceToolSlashRequested);
|
|
827
|
+
if (resumeContextSections?.length) {
|
|
828
|
+
const sections = optionActivationExactHistorySupersedesResumeExact
|
|
829
|
+
? stripExactTargetHistorySections(resumeContextSections)
|
|
830
|
+
: resumeContextSections;
|
|
831
|
+
if (sections?.length) {
|
|
832
|
+
resumeSections.push(...sections);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
const resumeSectionsFromText = inferPromptContextSections(stripSurfaceSystemStatusText(options.resumeContextText))
|
|
837
|
+
.filter((section) => shouldKeepIncomingPromptContextSection(section, {
|
|
838
|
+
suppressWorkspaceMemoryReminder: workspaceToolSlashRequested,
|
|
839
|
+
}));
|
|
840
|
+
for (const resumeSection of resumeSectionsFromText) {
|
|
841
|
+
if (optionActivationExactHistorySupersedesResumeExact
|
|
842
|
+
&& EXACT_TARGET_HISTORY_SECTION_KINDS.has(resumeSection.kind)) {
|
|
843
|
+
// Activation text is the live exact-history fallback for this
|
|
844
|
+
// queued dispatch; skip stale resume exact history.
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
resumeSections.push(resumeSection);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const composedBeforeReplay = composePromptContextTexts({
|
|
853
|
+
contextSections: [
|
|
854
|
+
...(activeProjectContextSection ? [activeProjectContextSection] : []),
|
|
855
|
+
...contextSections,
|
|
856
|
+
],
|
|
857
|
+
resumeSections,
|
|
858
|
+
});
|
|
859
|
+
if (dispatchMode !== 'cold_start') {
|
|
860
|
+
const replayText = this.buildConversationReplayText(conversationId, row.sessionKey, runId, replayOverlapMessages, composedBeforeReplay.hasVisibleRecentWindow);
|
|
861
|
+
if (replayText) {
|
|
862
|
+
const continuitySection = createPromptContextSection('continuity', replayText);
|
|
863
|
+
if (continuitySection) {
|
|
864
|
+
resumeSections.push(continuitySection);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const composedContextTexts = composePromptContextTexts({
|
|
869
|
+
contextSections: [
|
|
870
|
+
...(activeProjectContextSection ? [activeProjectContextSection] : []),
|
|
871
|
+
...contextSections,
|
|
872
|
+
],
|
|
873
|
+
resumeSections,
|
|
874
|
+
});
|
|
875
|
+
contextText = composedContextTexts.contextText;
|
|
876
|
+
resumeContextText = composedContextTexts.resumeContextText;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const dispatchAgent = row.agentId ? this.getAgentById(row.agentId) : null;
|
|
880
|
+
const dispatchAgentIsCodexAppServer = dispatchAgent?.agentType === 'codex_app_server';
|
|
881
|
+
const dispatchRuntimeOverrides = normalizePromptRuntimeOverrides(options?.runtimeOverrides);
|
|
882
|
+
const dispatchPlanMode = dispatchRuntimeOverrides?.planMode === true || dispatchRuntimeOverrides?.codexMode === 'plan';
|
|
883
|
+
const nodeRuntimeOverrides = dispatchRuntimeOverrides && dispatchAgentIsCodexAppServer && dispatchPlanMode
|
|
884
|
+
? { ...dispatchRuntimeOverrides, codexMode: 'plan' }
|
|
885
|
+
: dispatchRuntimeOverrides;
|
|
886
|
+
const shouldSkipReplyContract = options?.suppressReplyContract === true
|
|
887
|
+
|| activationMetadata?.suppressReplyContract === true
|
|
888
|
+
|| (dispatchAgentIsCodexAppServer && dispatchPlanMode);
|
|
889
|
+
const surfacePromptText = adaptPromptTextForAgentSurface(promptText, dispatchAgentSurfaceMode);
|
|
890
|
+
const surfaceContextText = contextText
|
|
891
|
+
? adaptPromptTextForAgentSurface(contextText, dispatchAgentSurfaceMode)
|
|
892
|
+
: contextText;
|
|
893
|
+
const surfaceResumeContextText = resumeContextText
|
|
894
|
+
? adaptPromptTextForAgentSurface(resumeContextText, dispatchAgentSurfaceMode)
|
|
895
|
+
: resumeContextText;
|
|
896
|
+
const dispatchedPrompt = shouldSkipReplyContract
|
|
897
|
+
? surfacePromptText
|
|
898
|
+
: prependTurnReplyContract(surfacePromptText, dispatchToday, dispatchAgentSurfaceMode);
|
|
899
|
+
const candidateSystemPromptText = dispatchMode === 'cold_start'
|
|
900
|
+
? systemPromptText
|
|
901
|
+
: getSessionSystemPromptText(this.db, row.sessionKey) || systemPromptText;
|
|
902
|
+
upsertPendingRunDebugInput(this.db, {
|
|
903
|
+
runId,
|
|
904
|
+
conversationId,
|
|
905
|
+
sessionKey: row.sessionKey,
|
|
906
|
+
dispatchMode,
|
|
907
|
+
replyTarget: row.replyTarget?.trim() || null,
|
|
908
|
+
systemPromptText: candidateSystemPromptText || null,
|
|
909
|
+
contextText: surfaceContextText || null,
|
|
910
|
+
promptText,
|
|
911
|
+
dispatchedPromptText: dispatchedPrompt,
|
|
912
|
+
agentSurfaceMode: dispatchAgentSurfaceMode,
|
|
913
|
+
activationMetadataJson: serializePromptActivationMetadata(activationMetadata),
|
|
914
|
+
});
|
|
915
|
+
const node = this.nodeRegistry?.getNode(row.nodeId);
|
|
916
|
+
if (!node) {
|
|
917
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
918
|
+
finishRun(this.db, { runId, error: 'Node not connected' });
|
|
919
|
+
this.updateStatus(conversationId, 'idle');
|
|
920
|
+
log.warn('[dispatcher] node not connected', { nodeId: row.nodeId, conversationId });
|
|
921
|
+
throw new Error(`Node not connected: ${row.nodeId}`);
|
|
922
|
+
}
|
|
923
|
+
if (!node.agentTypes.includes(row.agentType)) {
|
|
924
|
+
const errorMessage = `Node does not support agent type ${row.agentType}`;
|
|
925
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
926
|
+
finishRun(this.db, { runId, error: errorMessage });
|
|
927
|
+
this.updateStatus(conversationId, 'idle');
|
|
928
|
+
log.warn('[dispatcher] node does not support requested agent type', {
|
|
929
|
+
nodeId: row.nodeId,
|
|
930
|
+
conversationId,
|
|
931
|
+
agentType: row.agentType,
|
|
932
|
+
supportedAgentTypes: node.agentTypes,
|
|
933
|
+
});
|
|
934
|
+
throw new Error(`${errorMessage}: ${row.nodeId}`);
|
|
935
|
+
}
|
|
936
|
+
const nodeSurfaceReadiness = this.loadNodeSurfaceReadiness(row.nodeId);
|
|
937
|
+
const failNodeSurfaceReadiness = (errorMessage, details) => {
|
|
938
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
939
|
+
finishRun(this.db, { runId, error: errorMessage });
|
|
940
|
+
this.updateStatus(conversationId, 'idle');
|
|
941
|
+
log.warn('[dispatcher] node agent surface readiness failed', {
|
|
942
|
+
nodeId: row.nodeId,
|
|
943
|
+
conversationId,
|
|
944
|
+
runId,
|
|
945
|
+
...details,
|
|
946
|
+
});
|
|
947
|
+
throw new Error(`${errorMessage}: ${row.nodeId}`);
|
|
948
|
+
};
|
|
949
|
+
if (!nodeSurfaceReadiness.hasSnapshot) {
|
|
950
|
+
failNodeSurfaceReadiness('Node surface readiness missing', {
|
|
951
|
+
expectedAgentSurfaceMode: dispatchAgentSurfaceMode,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
if (node.processInstanceId && nodeSurfaceReadiness.processInstanceId !== node.processInstanceId) {
|
|
955
|
+
failNodeSurfaceReadiness('Node surface readiness stale', {
|
|
956
|
+
expectedProcessInstanceId: node.processInstanceId,
|
|
957
|
+
snapshotProcessInstanceId: nodeSurfaceReadiness.processInstanceId,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
if (nodeSurfaceReadiness.agentSurfaceMode !== dispatchAgentSurfaceMode) {
|
|
961
|
+
const actual = nodeSurfaceReadiness.agentSurfaceMode ?? 'missing';
|
|
962
|
+
failNodeSurfaceReadiness(`Node surface mismatch: expected ${dispatchAgentSurfaceMode}, got ${actual}`, {
|
|
963
|
+
expectedAgentSurfaceMode: dispatchAgentSurfaceMode,
|
|
964
|
+
actualAgentSurfaceMode: nodeSurfaceReadiness.agentSurfaceMode,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
if (dispatchAgentSurfaceMode === 'bigbang' && nodeSurfaceReadiness.bigbangCliAvailable !== true) {
|
|
968
|
+
failNodeSurfaceReadiness('Bigbang CLI unavailable on node', {
|
|
969
|
+
bigbangCliAvailable: nodeSurfaceReadiness.bigbangCliAvailable,
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
log.info('[dispatcher] dispatching prompt', {
|
|
973
|
+
nodeId: row.nodeId,
|
|
974
|
+
conversationId,
|
|
975
|
+
runId,
|
|
976
|
+
dispatchMode,
|
|
977
|
+
hostKey,
|
|
978
|
+
agentSurfaceMode: dispatchAgentSurfaceMode,
|
|
979
|
+
});
|
|
980
|
+
let channelBridgeConfig;
|
|
981
|
+
if (row.agentId) {
|
|
982
|
+
const serverUrl = this.resolveChannelBridgeServerUrl(row.nodeId);
|
|
983
|
+
channelBridgeConfig = {
|
|
984
|
+
agentId: row.agentId,
|
|
985
|
+
conversationId,
|
|
986
|
+
serverUrl,
|
|
987
|
+
authToken: this.config.internalAgentAuthToken,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (this.libraryService) {
|
|
991
|
+
try {
|
|
992
|
+
const personalLibraryRequested = activationMetadata?.slashCommand?.name === 'document';
|
|
993
|
+
if (personalLibraryRequested) {
|
|
994
|
+
this.libraryService.enablePersonalLibraryForConversation(conversationId);
|
|
995
|
+
}
|
|
996
|
+
await this.libraryService.prepareRunLibraryMirror({
|
|
997
|
+
runId,
|
|
998
|
+
conversationId,
|
|
999
|
+
userId: row.userId,
|
|
1000
|
+
nodeId: row.nodeId,
|
|
1001
|
+
workspacePath: row.workspacePath,
|
|
1002
|
+
personalEnabled: personalLibraryRequested || row.personalLibraryEnabled === 1,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
1007
|
+
finishRun(this.db, { runId, error: String(error?.message ?? error) });
|
|
1008
|
+
this.updateStatus(conversationId, 'failed');
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const requestedBuiltinSkillRoots = conversationBuiltinSkillRootsForSlashCommand(activationMetadata?.slashCommand?.name);
|
|
1013
|
+
const acceptance = this.waitForDispatchAcceptance(runId, row.nodeId, conversationId, options?.queuedPromptId);
|
|
1014
|
+
const dispatchBuiltinSkillRoots = normalizeConversationBuiltinSkillRoots([
|
|
1015
|
+
...parseConversationBuiltinSkillRoots(row.builtinSkillRootsJson),
|
|
1016
|
+
...requestedBuiltinSkillRoots,
|
|
1017
|
+
]);
|
|
1018
|
+
const dispatchEnabledSkillPaths = dispatchAgent?.skillSelectionMode === 'explicit'
|
|
1019
|
+
? normalizeDispatchEnabledSkillPaths(dispatchAgent.enabledSkillPaths)
|
|
1020
|
+
: undefined;
|
|
1021
|
+
const dispatchSkillRoots = mergeDispatchSkillRoots(dispatchAgent?.skillRoots, dispatchBuiltinSkillRoots);
|
|
1022
|
+
const sent = this.nodeRegistry.send(row.nodeId, {
|
|
1023
|
+
type: 'run.dispatch',
|
|
1024
|
+
runId,
|
|
1025
|
+
conversationId,
|
|
1026
|
+
agentType: row.agentType,
|
|
1027
|
+
agentSurfaceMode: dispatchAgentSurfaceMode,
|
|
1028
|
+
...(dispatchAgent?.model ? { model: dispatchAgent.model } : {}),
|
|
1029
|
+
...(dispatchAgent?.reasoningEffort ? { reasoningEffort: dispatchAgent.reasoningEffort } : {}),
|
|
1030
|
+
...(dispatchAgent?.agentType === 'claude_sdk' && dispatchAgent.claudePermissionMode ? { claudePermissionMode: dispatchAgent.claudePermissionMode } : {}),
|
|
1031
|
+
...(dispatchAgentIsCodexAppServer && dispatchPlanMode ? { codexMode: 'plan' } : {}),
|
|
1032
|
+
...(dispatchAgentIsCodexAppServer && !dispatchPlanMode && dispatchRuntimeOverrides?.codexMode ? { codexMode: dispatchRuntimeOverrides.codexMode } : {}),
|
|
1033
|
+
...(dispatchAgentIsCodexAppServer && dispatchAgent?.codexServiceTier ? { codexServiceTier: dispatchAgent.codexServiceTier } : {}),
|
|
1034
|
+
workspacePath: row.workspacePath,
|
|
1035
|
+
...(dispatchAgent?.name ? { agentName: dispatchAgent.name } : {}),
|
|
1036
|
+
...(dispatchSkillRoots && dispatchSkillRoots.length > 0 ? { skillRoots: dispatchSkillRoots } : {}),
|
|
1037
|
+
...(dispatchEnabledSkillPaths !== undefined ? { enabledSkillPaths: dispatchEnabledSkillPaths } : {}),
|
|
1038
|
+
envVars: {
|
|
1039
|
+
...(agentEnvVars ?? {}),
|
|
1040
|
+
...(parseEnvVars(row.envVarsJson) ?? {}),
|
|
1041
|
+
...(driver.defaultEnv ?? {}),
|
|
1042
|
+
},
|
|
1043
|
+
disabledToolKinds,
|
|
1044
|
+
prompt: dispatchedPrompt,
|
|
1045
|
+
...(promptAttachments.length > 0 ? { attachments: promptAttachments } : {}),
|
|
1046
|
+
...(nodeRuntimeOverrides ? { runtimeOverrides: nodeRuntimeOverrides } : {}),
|
|
1047
|
+
sessionKey: row.sessionKey,
|
|
1048
|
+
hostKey,
|
|
1049
|
+
dispatchMode,
|
|
1050
|
+
systemPromptText: systemPromptText || undefined,
|
|
1051
|
+
contextText: surfaceContextText || undefined,
|
|
1052
|
+
resumeContextText: surfaceResumeContextText || undefined,
|
|
1053
|
+
...(options?.notificationDelivery ? { delivery: options.notificationDelivery } : {}),
|
|
1054
|
+
channelBridgeConfig,
|
|
1055
|
+
});
|
|
1056
|
+
if (!sent) {
|
|
1057
|
+
this.libraryService?.abortRunLibraryMirror(runId);
|
|
1058
|
+
this.clearPendingDispatchAcceptance(runId);
|
|
1059
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
1060
|
+
finishRun(this.db, { runId, error: 'Node disconnected during dispatch' });
|
|
1061
|
+
this.updateStatus(conversationId, 'idle');
|
|
1062
|
+
throw new Error(`Node disconnected: ${row.nodeId}`);
|
|
1063
|
+
}
|
|
1064
|
+
this.markQueueDispatchSent(options?.queuedPromptId, runId);
|
|
1065
|
+
try {
|
|
1066
|
+
await acceptance;
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
this.libraryService?.abortRunLibraryMirror(runId);
|
|
1070
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, runId);
|
|
1071
|
+
finishRun(this.db, { runId, error: String(error?.message ?? error) });
|
|
1072
|
+
this.updateStatus(conversationId, 'failed');
|
|
1073
|
+
throw error;
|
|
1074
|
+
}
|
|
1075
|
+
return { runId, dispatchMode, hostKey };
|
|
1076
|
+
}
|
|
1077
|
+
async submitPrompt(conversationId, promptText, options) {
|
|
1078
|
+
const row = this.db.prepare(`SELECT agent_id as agentId,
|
|
1079
|
+
agent_type as agentType,
|
|
1080
|
+
node_id as nodeId,
|
|
1081
|
+
channel_id as channelId,
|
|
1082
|
+
reply_target as replyTarget,
|
|
1083
|
+
thread_kind as threadKind,
|
|
1084
|
+
thread_root_id as threadRootId
|
|
1085
|
+
FROM conversations
|
|
1086
|
+
WHERE id = ?`).get(conversationId);
|
|
1087
|
+
if (!row)
|
|
1088
|
+
throw new Error(`Unknown conversation: ${conversationId}`);
|
|
1089
|
+
const agent = row.agentId ? this.getAgentById(row.agentId) : null;
|
|
1090
|
+
const recordAsUserMessage = options?.recordAsUserMessage ?? true;
|
|
1091
|
+
const normalizedActivationMetadata = ensurePlatformRunMetadata(this.db, agent?.agentRole, agent?.agentId, options?.activationMetadata, recordAsUserMessage);
|
|
1092
|
+
const normalizedOptions = normalizedActivationMetadata === options?.activationMetadata
|
|
1093
|
+
? options
|
|
1094
|
+
: {
|
|
1095
|
+
...(options ?? {}),
|
|
1096
|
+
...(normalizedActivationMetadata ? { activationMetadata: normalizedActivationMetadata } : {}),
|
|
1097
|
+
};
|
|
1098
|
+
const platformBlockReason = getPlatformAgentDispatchBlockReason(this.db, agent?.agentRole, agent?.agentId, normalizedOptions?.activationMetadata);
|
|
1099
|
+
if (platformBlockReason) {
|
|
1100
|
+
throw new Error(platformBlockReason);
|
|
1101
|
+
}
|
|
1102
|
+
if (!row.agentId) {
|
|
1103
|
+
const dispatched = await this.dispatchPrompt(conversationId, promptText, normalizedOptions);
|
|
1104
|
+
return { queued: false, runId: dispatched.runId };
|
|
1105
|
+
}
|
|
1106
|
+
let effectiveOptions = normalizedOptions;
|
|
1107
|
+
let queuedFallbackId;
|
|
1108
|
+
this.deleteAcceptedQueueHeads(conversationId);
|
|
1109
|
+
if (isCodexRuntimeRotationPending(this.db, conversationId)) {
|
|
1110
|
+
if (normalizedOptions?.allowQueue === false) {
|
|
1111
|
+
return { queued: true };
|
|
1112
|
+
}
|
|
1113
|
+
effectiveOptions = this.persistDirectUserMessageForQueuedPromptIfNeeded(conversationId, row, promptText, normalizedOptions);
|
|
1114
|
+
this.enqueuePrompt(row.agentId, conversationId, promptText, effectiveOptions);
|
|
1115
|
+
this.updateStatus(conversationId, 'queued');
|
|
1116
|
+
return { queued: true };
|
|
1117
|
+
}
|
|
1118
|
+
const blocking = this.findBlockingConversation(conversationId);
|
|
1119
|
+
if (blocking) {
|
|
1120
|
+
if (row.threadKind === 'direct' && recordAsUserMessage) {
|
|
1121
|
+
const persistedMessage = this.persistDirectUserMessageForConversation({
|
|
1122
|
+
conversationId,
|
|
1123
|
+
conversation: {
|
|
1124
|
+
agentId: row.agentId,
|
|
1125
|
+
replyTarget: row.replyTarget,
|
|
1126
|
+
threadKind: row.threadKind,
|
|
1127
|
+
threadRootId: row.threadRootId,
|
|
1128
|
+
},
|
|
1129
|
+
promptText,
|
|
1130
|
+
options: normalizedOptions,
|
|
1131
|
+
attachments: sanitizeFileRefs(options?.attachments),
|
|
1132
|
+
attachmentIds: sanitizeAttachmentIds(options?.attachmentIds),
|
|
1133
|
+
});
|
|
1134
|
+
effectiveOptions = {
|
|
1135
|
+
...normalizedOptions,
|
|
1136
|
+
clientMessageId: persistedMessage.messageId,
|
|
1137
|
+
persistedDirectUserMessage: persistedMessage,
|
|
1138
|
+
activationMetadata: {
|
|
1139
|
+
...(normalizedOptions?.activationMetadata ?? {}),
|
|
1140
|
+
triggerMessage: {
|
|
1141
|
+
messageId: persistedMessage.messageId,
|
|
1142
|
+
seq: persistedMessage.seq,
|
|
1143
|
+
target: persistedMessage.replyTarget,
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
};
|
|
1147
|
+
if (normalizedOptions?.allowQueue !== false) {
|
|
1148
|
+
queuedFallbackId = this.enqueuePrompt(row.agentId, conversationId, promptText, effectiveOptions);
|
|
1149
|
+
effectiveOptions = {
|
|
1150
|
+
...effectiveOptions,
|
|
1151
|
+
queuedPromptId: queuedFallbackId,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
if (normalizedOptions?.allowActiveSteer !== false) {
|
|
1156
|
+
const steered = await this.trySteerActiveRun({
|
|
1157
|
+
conversationId,
|
|
1158
|
+
promptText,
|
|
1159
|
+
options: effectiveOptions,
|
|
1160
|
+
conversation: row,
|
|
1161
|
+
blocking,
|
|
1162
|
+
});
|
|
1163
|
+
if (steered) {
|
|
1164
|
+
if (queuedFallbackId != null) {
|
|
1165
|
+
this.deleteQueuedPrompt(queuedFallbackId);
|
|
1166
|
+
}
|
|
1167
|
+
return { queued: false, runId: steered.runId, steered: true };
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
if (this.findBlockingConversation(conversationId)) {
|
|
1171
|
+
if (normalizedOptions?.allowQueue === false) {
|
|
1172
|
+
return { queued: true };
|
|
1173
|
+
}
|
|
1174
|
+
if (queuedFallbackId == null) {
|
|
1175
|
+
queuedFallbackId = this.enqueuePrompt(row.agentId, conversationId, promptText, effectiveOptions);
|
|
1176
|
+
}
|
|
1177
|
+
this.updateStatus(conversationId, 'queued');
|
|
1178
|
+
if (hasEffectiveAgentRuntimeCapability(this.db, {
|
|
1179
|
+
nodeId: row.nodeId,
|
|
1180
|
+
agentType: row.agentType,
|
|
1181
|
+
capability: 'activeTurnSteer',
|
|
1182
|
+
})
|
|
1183
|
+
&& row.threadRootId
|
|
1184
|
+
&& (effectiveOptions?.recordAsUserMessage ?? true)
|
|
1185
|
+
&& blocking.runId
|
|
1186
|
+
&& row.nodeId) {
|
|
1187
|
+
const sent = this.nodeRegistry?.send(row.nodeId, {
|
|
1188
|
+
type: 'run.cancel',
|
|
1189
|
+
runId: blocking.runId,
|
|
1190
|
+
});
|
|
1191
|
+
if (!sent) {
|
|
1192
|
+
log.warn('[dispatcher] failed to preempt active Codex task-thread run for queued human prompt', {
|
|
1193
|
+
nodeId: row.nodeId,
|
|
1194
|
+
conversationId,
|
|
1195
|
+
runId: blocking.runId,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return { queued: true };
|
|
1200
|
+
}
|
|
1201
|
+
const dispatched = await this.dispatchPrompt(conversationId, promptText, effectiveOptions);
|
|
1202
|
+
if (queuedFallbackId != null) {
|
|
1203
|
+
this.deleteQueuedPrompt(queuedFallbackId);
|
|
1204
|
+
}
|
|
1205
|
+
return { queued: false, runId: dispatched.runId };
|
|
1206
|
+
}
|
|
1207
|
+
const pendingQueueHead = this.getConversationQueueHead(conversationId);
|
|
1208
|
+
if (pendingQueueHead) {
|
|
1209
|
+
if (normalizedOptions?.allowQueue === false) {
|
|
1210
|
+
this.scheduleQueueRetryForHead(conversationId, pendingQueueHead);
|
|
1211
|
+
return { queued: true };
|
|
1212
|
+
}
|
|
1213
|
+
if (row.threadKind === 'direct' && recordAsUserMessage) {
|
|
1214
|
+
const persistedMessage = this.persistDirectUserMessageForConversation({
|
|
1215
|
+
conversationId,
|
|
1216
|
+
conversation: {
|
|
1217
|
+
agentId: row.agentId,
|
|
1218
|
+
replyTarget: row.replyTarget,
|
|
1219
|
+
threadKind: row.threadKind,
|
|
1220
|
+
threadRootId: row.threadRootId,
|
|
1221
|
+
},
|
|
1222
|
+
promptText,
|
|
1223
|
+
options: normalizedOptions,
|
|
1224
|
+
attachments: sanitizeFileRefs(options?.attachments),
|
|
1225
|
+
attachmentIds: sanitizeAttachmentIds(options?.attachmentIds),
|
|
1226
|
+
});
|
|
1227
|
+
effectiveOptions = {
|
|
1228
|
+
...normalizedOptions,
|
|
1229
|
+
clientMessageId: persistedMessage.messageId,
|
|
1230
|
+
persistedDirectUserMessage: persistedMessage,
|
|
1231
|
+
activationMetadata: {
|
|
1232
|
+
...(normalizedOptions?.activationMetadata ?? {}),
|
|
1233
|
+
triggerMessage: {
|
|
1234
|
+
messageId: persistedMessage.messageId,
|
|
1235
|
+
seq: persistedMessage.seq,
|
|
1236
|
+
target: persistedMessage.replyTarget,
|
|
1237
|
+
},
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
this.enqueuePrompt(row.agentId, conversationId, promptText, effectiveOptions);
|
|
1242
|
+
this.updateStatus(conversationId, 'queued');
|
|
1243
|
+
this.scheduleQueueRetryForHead(conversationId, pendingQueueHead);
|
|
1244
|
+
return { queued: true };
|
|
1245
|
+
}
|
|
1246
|
+
const dispatched = await this.dispatchPrompt(conversationId, promptText, normalizedOptions);
|
|
1247
|
+
return { queued: false, runId: dispatched.runId };
|
|
1248
|
+
}
|
|
1249
|
+
async trySteerActiveRun(params) {
|
|
1250
|
+
const { conversation, blocking, conversationId, options } = params;
|
|
1251
|
+
if (!hasEffectiveAgentRuntimeCapability(this.db, {
|
|
1252
|
+
nodeId: conversation.nodeId,
|
|
1253
|
+
agentType: conversation.agentType,
|
|
1254
|
+
capability: 'activeTurnSteer',
|
|
1255
|
+
}))
|
|
1256
|
+
return null;
|
|
1257
|
+
if (blocking.status !== 'active' || !blocking.runId)
|
|
1258
|
+
return null;
|
|
1259
|
+
if (!conversation.agentId || !conversation.nodeId)
|
|
1260
|
+
return null;
|
|
1261
|
+
const runtimeOverrides = normalizePromptRuntimeOverrides(options?.runtimeOverrides);
|
|
1262
|
+
if (runtimeOverrides?.planMode === true || runtimeOverrides?.codexMode === 'plan')
|
|
1263
|
+
return null;
|
|
1264
|
+
return this.withSteerSerial(blocking.runId, async () => {
|
|
1265
|
+
const latestBlocking = this.findBlockingConversation(conversationId);
|
|
1266
|
+
if (!latestBlocking || latestBlocking.status !== 'active' || latestBlocking.runId !== blocking.runId) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
if (!this.getAgentById(conversation.agentId))
|
|
1270
|
+
return null;
|
|
1271
|
+
const shouldPersistDirectUserMessage = (conversation.threadKind === 'direct'
|
|
1272
|
+
&& (options?.recordAsUserMessage ?? true) === true);
|
|
1273
|
+
const promptAttachments = sanitizeFileRefs(options?.attachments);
|
|
1274
|
+
const dmChannelId = `dm:${conversation.agentId}`;
|
|
1275
|
+
const humanUserName = options?.senderName ?? this.config.humanUserName;
|
|
1276
|
+
const surfaceChannelId = conversation.threadKind === 'direct'
|
|
1277
|
+
? dmChannelId
|
|
1278
|
+
: conversation.channelId;
|
|
1279
|
+
const replyTarget = conversation.threadKind === 'direct'
|
|
1280
|
+
? resolveConversationReplyTarget(this.db, conversationId, humanUserName) ?? conversation.replyTarget?.trim() ?? `dm:@${humanUserName}`
|
|
1281
|
+
: conversation.replyTarget?.trim() ?? '';
|
|
1282
|
+
if (!replyTarget)
|
|
1283
|
+
return null;
|
|
1284
|
+
let steerPrompt;
|
|
1285
|
+
let activeSteerContextSectionsForMetadata = {};
|
|
1286
|
+
if (shouldPersistDirectUserMessage) {
|
|
1287
|
+
const latestSurfaceSystemStatusSection = buildSurfaceSystemStatusSection(this.db, {
|
|
1288
|
+
channelId: surfaceChannelId,
|
|
1289
|
+
replyTarget,
|
|
1290
|
+
threadRootId: conversation.threadRootId,
|
|
1291
|
+
});
|
|
1292
|
+
activeSteerContextSectionsForMetadata = {
|
|
1293
|
+
activation: latestSurfaceSystemStatusSection ? [latestSurfaceSystemStatusSection] : [],
|
|
1294
|
+
};
|
|
1295
|
+
steerPrompt = joinSteerPromptParts(latestSurfaceSystemStatusSection
|
|
1296
|
+
? renderPromptContextSections([latestSurfaceSystemStatusSection])
|
|
1297
|
+
: null, buildMinimalDirectFollowupSteerPrompt(params.promptText));
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
const strippedActivationSections = stripSurfaceSystemStatusSections(options?.activationContextSections);
|
|
1301
|
+
const includeExactTargetHistoryInActiveSteer = options?.activationMetadata?.activeSteerIncludeExactTargetHistory === true;
|
|
1302
|
+
const priorActivationSections = options?.activationContextMode === 'resume_only'
|
|
1303
|
+
? strippedActivationSections?.filter((section) => shouldKeepResumeOnlyActivationSectionLive(section)
|
|
1304
|
+
|| (includeExactTargetHistoryInActiveSteer && EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind)))
|
|
1305
|
+
: strippedActivationSections;
|
|
1306
|
+
const latestSurfaceSystemStatusSection = buildSurfaceSystemStatusSection(this.db, {
|
|
1307
|
+
channelId: surfaceChannelId,
|
|
1308
|
+
replyTarget,
|
|
1309
|
+
threadRootId: conversation.threadRootId,
|
|
1310
|
+
});
|
|
1311
|
+
const actualActivationSections = [
|
|
1312
|
+
...(latestSurfaceSystemStatusSection ? [latestSurfaceSystemStatusSection] : []),
|
|
1313
|
+
...(priorActivationSections ?? []),
|
|
1314
|
+
];
|
|
1315
|
+
activeSteerContextSectionsForMetadata = {
|
|
1316
|
+
activation: actualActivationSections,
|
|
1317
|
+
};
|
|
1318
|
+
const fallbackActivationContextText = options?.activationContextMode === 'resume_only'
|
|
1319
|
+
? undefined
|
|
1320
|
+
: stripSurfaceSystemStatusText(options?.activationContextText);
|
|
1321
|
+
const steerActivationContextText = actualActivationSections.length
|
|
1322
|
+
? renderPromptContextSections(actualActivationSections)
|
|
1323
|
+
: fallbackActivationContextText;
|
|
1324
|
+
const joinedSteerPrompt = joinSteerPromptParts(steerActivationContextText, params.promptText);
|
|
1325
|
+
const activeRunAgentSurfaceMode = this.getRunDebugInputAgentSurfaceMode(blocking.runId)
|
|
1326
|
+
?? normalizeDispatchAgentSurfaceMode(this.config.agentSurfaceMode, 'bigbang');
|
|
1327
|
+
const surfaceJoinedSteerPrompt = adaptPromptTextForAgentSurface(joinedSteerPrompt, activeRunAgentSurfaceMode);
|
|
1328
|
+
steerPrompt = options?.suppressReplyContract === true
|
|
1329
|
+
|| options?.activationMetadata?.suppressReplyContract === true
|
|
1330
|
+
? surfaceJoinedSteerPrompt
|
|
1331
|
+
: prependTurnReplyContract(surfaceJoinedSteerPrompt, getCurrentISOWeek(), activeRunAgentSurfaceMode);
|
|
1332
|
+
}
|
|
1333
|
+
const requestId = randomUUID();
|
|
1334
|
+
if (options?.queuedPromptId != null) {
|
|
1335
|
+
this.markQueueDispatchAttempt(options.queuedPromptId, blocking.runId, 'steer', requestId);
|
|
1336
|
+
}
|
|
1337
|
+
let restoreRunDebugInputAfterFailedSteer = null;
|
|
1338
|
+
const response = this.waitForSteerResponse(requestId, conversation.nodeId, blocking.runId, conversationId, options?.queuedPromptId, conversation.agentType === 'claude_sdk'
|
|
1339
|
+
? CLAUDE_SDK_ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS
|
|
1340
|
+
: ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS);
|
|
1341
|
+
const sent = this.nodeRegistry?.send(conversation.nodeId, {
|
|
1342
|
+
type: 'run.steer',
|
|
1343
|
+
requestId,
|
|
1344
|
+
runId: blocking.runId,
|
|
1345
|
+
conversationId,
|
|
1346
|
+
prompt: steerPrompt,
|
|
1347
|
+
...(promptAttachments.length > 0 ? { attachments: promptAttachments } : {}),
|
|
1348
|
+
...(options?.notificationDelivery ? { delivery: options.notificationDelivery } : {}),
|
|
1349
|
+
});
|
|
1350
|
+
if (!sent) {
|
|
1351
|
+
this.clearPendingSteerResponse(requestId);
|
|
1352
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, blocking.runId, requestId);
|
|
1353
|
+
log.warn('[dispatcher] active-turn steer send failed; falling back to queue', {
|
|
1354
|
+
agentType: conversation.agentType,
|
|
1355
|
+
nodeId: conversation.nodeId,
|
|
1356
|
+
conversationId,
|
|
1357
|
+
runId: blocking.runId,
|
|
1358
|
+
});
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
restoreRunDebugInputAfterFailedSteer = patchRunDebugInputForActiveSteer(this.db, {
|
|
1362
|
+
runId: blocking.runId,
|
|
1363
|
+
promptText: params.promptText,
|
|
1364
|
+
dispatchedPromptText: steerPrompt,
|
|
1365
|
+
activationMetadata: enrichPromptActivationMetadataWithContextSections(options, activeSteerContextSectionsForMetadata),
|
|
1366
|
+
});
|
|
1367
|
+
this.markQueueDispatchSent(options?.queuedPromptId, blocking.runId);
|
|
1368
|
+
try {
|
|
1369
|
+
await response;
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
restoreRunDebugInputAfterFailedSteer?.();
|
|
1373
|
+
this.clearQueueDispatchAttempt(options?.queuedPromptId, blocking.runId, requestId);
|
|
1374
|
+
log.warn('[dispatcher] active-turn steer failed; falling back to queue', {
|
|
1375
|
+
agentType: conversation.agentType,
|
|
1376
|
+
nodeId: conversation.nodeId,
|
|
1377
|
+
conversationId,
|
|
1378
|
+
runId: blocking.runId,
|
|
1379
|
+
error: String(error?.message ?? error),
|
|
1380
|
+
});
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
return { runId: blocking.runId };
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
async handleApproval(conversationId, requestId, decision, selectedActionId, responseText, answers) {
|
|
1387
|
+
const convRow = this.db
|
|
1388
|
+
.prepare('SELECT node_id as nodeId FROM conversations WHERE id = ?')
|
|
1389
|
+
.get(conversationId);
|
|
1390
|
+
if (!convRow)
|
|
1391
|
+
return { ok: false, message: 'Unknown conversation.' };
|
|
1392
|
+
if (!convRow.nodeId)
|
|
1393
|
+
return { ok: false, message: 'No agent node assigned to this conversation.' };
|
|
1394
|
+
const sent = this.nodeRegistry?.send(convRow.nodeId, {
|
|
1395
|
+
type: 'permission.response',
|
|
1396
|
+
requestId,
|
|
1397
|
+
decision,
|
|
1398
|
+
selectedActionId,
|
|
1399
|
+
...(responseText?.trim() ? { responseText: responseText.trim() } : {}),
|
|
1400
|
+
...(answers ? { answers } : {}),
|
|
1401
|
+
});
|
|
1402
|
+
if (sent) {
|
|
1403
|
+
this.updateStatus(conversationId, 'active');
|
|
1404
|
+
return { ok: true, message: '' };
|
|
1405
|
+
}
|
|
1406
|
+
return { ok: false, message: 'Node not connected.' };
|
|
1407
|
+
}
|
|
1408
|
+
cancelConversationRun(conversationId, expectedRunId, options) {
|
|
1409
|
+
const row = expectedRunId
|
|
1410
|
+
? this.db.prepare(`SELECT c.node_id as nodeId,
|
|
1411
|
+
r.run_id as coreRunId,
|
|
1412
|
+
h.current_run_id as snapshotRunId
|
|
1413
|
+
FROM conversations c
|
|
1414
|
+
LEFT JOIN runs r
|
|
1415
|
+
ON r.session_key = c.session_key
|
|
1416
|
+
AND r.ended_at IS NULL
|
|
1417
|
+
AND r.run_id = ?
|
|
1418
|
+
LEFT JOIN node_host_snapshots h
|
|
1419
|
+
ON h.node_id = c.node_id
|
|
1420
|
+
AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
|
|
1421
|
+
AND h.state = 'active'
|
|
1422
|
+
AND h.current_run_id IS NOT NULL
|
|
1423
|
+
WHERE c.id = ?
|
|
1424
|
+
LIMIT 1`).get(expectedRunId, conversationId)
|
|
1425
|
+
: this.db.prepare(`SELECT c.node_id as nodeId,
|
|
1426
|
+
r.run_id as coreRunId,
|
|
1427
|
+
h.current_run_id as snapshotRunId
|
|
1428
|
+
FROM conversations c
|
|
1429
|
+
LEFT JOIN runs r ON r.session_key = c.session_key AND r.ended_at IS NULL
|
|
1430
|
+
LEFT JOIN node_host_snapshots h
|
|
1431
|
+
ON h.node_id = c.node_id
|
|
1432
|
+
AND h.host_key = 'conversation:' || c.id || ':' || c.agent_type
|
|
1433
|
+
AND h.state = 'active'
|
|
1434
|
+
AND h.current_run_id IS NOT NULL
|
|
1435
|
+
WHERE c.id = ?
|
|
1436
|
+
ORDER BY r.started_at DESC, r.rowid DESC
|
|
1437
|
+
LIMIT 1`).get(conversationId);
|
|
1438
|
+
if (!row)
|
|
1439
|
+
return { ok: false, message: 'Unknown conversation.' };
|
|
1440
|
+
const runId = expectedRunId && options?.strictExpectedRunId
|
|
1441
|
+
? row.coreRunId ?? (row.snapshotRunId === expectedRunId ? row.snapshotRunId : null)
|
|
1442
|
+
: row.snapshotRunId ?? row.coreRunId;
|
|
1443
|
+
if (!runId)
|
|
1444
|
+
return { ok: false, message: 'No active run to cancel.' };
|
|
1445
|
+
if (!row.nodeId)
|
|
1446
|
+
return { ok: false, message: 'No agent node assigned to this conversation.' };
|
|
1447
|
+
const sent = this.nodeRegistry?.send(row.nodeId, {
|
|
1448
|
+
type: 'run.cancel',
|
|
1449
|
+
runId,
|
|
1450
|
+
});
|
|
1451
|
+
return sent
|
|
1452
|
+
? { ok: true, message: '', runId }
|
|
1453
|
+
: { ok: false, message: 'Node not connected.' };
|
|
1454
|
+
}
|
|
1455
|
+
persistDirectUserMessageForConversation(params) {
|
|
1456
|
+
const { conversation, conversationId, options } = params;
|
|
1457
|
+
if (conversation.threadKind !== 'direct' || !conversation.agentId) {
|
|
1458
|
+
throw new Error('Direct user message persistence requires a direct agent conversation.');
|
|
1459
|
+
}
|
|
1460
|
+
const existing = options?.persistedDirectUserMessage;
|
|
1461
|
+
if (existing)
|
|
1462
|
+
return existing;
|
|
1463
|
+
const dmChannelId = `dm:${conversation.agentId}`;
|
|
1464
|
+
const humanUserName = options?.senderName ?? this.config.humanUserName;
|
|
1465
|
+
const replyTarget = resolveConversationReplyTarget(this.db, conversationId, humanUserName) ?? conversation.replyTarget?.trim() ?? `dm:@${humanUserName}`;
|
|
1466
|
+
const dmThreadRootId = conversation.threadRootId ?? null;
|
|
1467
|
+
const messageId = options?.clientMessageId ?? randomUUID();
|
|
1468
|
+
const attachNoteIdx = findPromptAttachmentNoteIndex(params.promptText);
|
|
1469
|
+
const displayContent = attachNoteIdx >= 0 ? params.promptText.slice(0, attachNoteIdx) : params.promptText;
|
|
1470
|
+
const attachmentIds = extractPromptAttachmentIds(params.promptText, params.attachments, params.attachmentIds);
|
|
1471
|
+
const existingRow = this.db.prepare(`SELECT message_id as messageId, channel_id as channelId,
|
|
1472
|
+
sender_name as senderName, target as replyTarget,
|
|
1473
|
+
content, seq, thread_root_id as threadRootId,
|
|
1474
|
+
attachment_ids as attachmentIdsJson,
|
|
1475
|
+
panel_ids as panelIdsJson
|
|
1476
|
+
FROM channel_messages
|
|
1477
|
+
WHERE message_id = ?
|
|
1478
|
+
LIMIT 1`).get(messageId);
|
|
1479
|
+
if (existingRow) {
|
|
1480
|
+
const existingAttachmentIds = parseAttachmentIdsJson(existingRow.attachmentIdsJson);
|
|
1481
|
+
if (existingRow.channelId !== dmChannelId
|
|
1482
|
+
|| (existingRow.threadRootId ?? null) !== dmThreadRootId
|
|
1483
|
+
|| existingRow.replyTarget !== replyTarget) {
|
|
1484
|
+
throw new Error(`Client message id already exists on a different surface: ${messageId}`);
|
|
1485
|
+
}
|
|
1486
|
+
if ((existingRow.senderName ?? humanUserName) !== humanUserName
|
|
1487
|
+
|| existingRow.content !== displayContent
|
|
1488
|
+
|| !stringArraysEqual(existingAttachmentIds, attachmentIds)) {
|
|
1489
|
+
throw new Error(`Client message id already exists with a different message payload: ${messageId}`);
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
messageId: existingRow.messageId,
|
|
1493
|
+
channelId: existingRow.channelId,
|
|
1494
|
+
replyTarget: existingRow.replyTarget,
|
|
1495
|
+
threadRootId: existingRow.threadRootId ?? null,
|
|
1496
|
+
seq: existingRow.seq,
|
|
1497
|
+
senderName: existingRow.senderName ?? humanUserName,
|
|
1498
|
+
content: existingRow.content,
|
|
1499
|
+
attachmentIds: existingAttachmentIds,
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
const msgSeq = allocateNextChannelMessageSeq(this.db, dmChannelId);
|
|
1503
|
+
this.db.prepare(`INSERT INTO channel_messages(message_id, channel_id, sender_id, sender_name, sender_type, target, content, seq, created_at, thread_root_id, attachment_ids)
|
|
1504
|
+
VALUES(?, ?, 'user', ?, 'user', ?, ?, ?, ?, ?, ?)`).run(messageId, dmChannelId, humanUserName, replyTarget, displayContent, msgSeq, Date.now(), dmThreadRootId, attachmentIds.length ? JSON.stringify(attachmentIds) : null);
|
|
1505
|
+
refreshConversationSummariesForMessageScope(this.db, (targetAgentId) => this.getAgentById(targetAgentId), {
|
|
1506
|
+
channelId: dmChannelId,
|
|
1507
|
+
replyTarget,
|
|
1508
|
+
threadRootId: dmThreadRootId,
|
|
1509
|
+
});
|
|
1510
|
+
return {
|
|
1511
|
+
messageId,
|
|
1512
|
+
channelId: dmChannelId,
|
|
1513
|
+
replyTarget,
|
|
1514
|
+
threadRootId: dmThreadRootId,
|
|
1515
|
+
seq: msgSeq,
|
|
1516
|
+
senderName: humanUserName,
|
|
1517
|
+
content: displayContent,
|
|
1518
|
+
attachmentIds,
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async handleConversationSettled(conversationId) {
|
|
1522
|
+
const row = this.db.prepare(`SELECT agent_id as agentId,
|
|
1523
|
+
status
|
|
1524
|
+
FROM conversations
|
|
1525
|
+
WHERE id = ?`).get(conversationId);
|
|
1526
|
+
if (!row?.agentId)
|
|
1527
|
+
return;
|
|
1528
|
+
if (row.status === 'awaiting_approval')
|
|
1529
|
+
return;
|
|
1530
|
+
while (true) {
|
|
1531
|
+
if (isCodexRuntimeRotationPending(this.db, conversationId))
|
|
1532
|
+
return;
|
|
1533
|
+
if (this.findBlockingConversation(conversationId))
|
|
1534
|
+
return;
|
|
1535
|
+
const next = this.db.prepare(`SELECT queue_id as queueId, agent_id as agentId, conversation_id as conversationId,
|
|
1536
|
+
prompt_text as promptText, record_as_user_message as recordAsUserMessage,
|
|
1537
|
+
activation_context_text as activationContextText,
|
|
1538
|
+
activation_context_mode as activationContextMode,
|
|
1539
|
+
resume_context_text as resumeContextText,
|
|
1540
|
+
replay_overlap_recent_messages_json as replayOverlapRecentMessagesJson,
|
|
1541
|
+
activation_metadata_json as activationMetadataJson,
|
|
1542
|
+
agent_surface_mode as agentSurfaceMode,
|
|
1543
|
+
sender_name as senderName,
|
|
1544
|
+
client_message_id as clientMessageId,
|
|
1545
|
+
dispatching_run_id as dispatchingRunId,
|
|
1546
|
+
dispatch_kind as dispatchKind,
|
|
1547
|
+
dispatch_sent_at as dispatchSentAt,
|
|
1548
|
+
dispatch_accepted_at as dispatchAcceptedAt
|
|
1549
|
+
FROM conversation_prompt_queue
|
|
1550
|
+
WHERE conversation_id = ?
|
|
1551
|
+
AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
|
|
1552
|
+
ORDER BY created_at ASC, queue_id ASC
|
|
1553
|
+
LIMIT 1`).get(conversationId);
|
|
1554
|
+
if (!next)
|
|
1555
|
+
return;
|
|
1556
|
+
if (next.dispatchAcceptedAt != null) {
|
|
1557
|
+
this.deleteQueuedPrompt(next.queueId);
|
|
1558
|
+
continue;
|
|
1559
|
+
}
|
|
1560
|
+
if (next.dispatchingRunId) {
|
|
1561
|
+
if (next.dispatchKind == null) {
|
|
1562
|
+
// Legacy in-flight rows from before dispatch_kind are ambiguous. Do not
|
|
1563
|
+
// auto-replay them after migration; that can duplicate already-run work.
|
|
1564
|
+
// Keep a DB-visible marker so operators can inspect the abandoned row.
|
|
1565
|
+
this.markQueueLegacyUnknown(next.queueId, next.dispatchingRunId);
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
if (next.dispatchSentAt != null) {
|
|
1569
|
+
const retryInMs = next.dispatchSentAt + ORPHANED_QUEUE_ATTEMPT_STALE_MS - Date.now();
|
|
1570
|
+
if (retryInMs > 0) {
|
|
1571
|
+
this.scheduleQueueSettledRetry(conversationId, retryInMs);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.clearQueueDispatchAttempt(next.queueId, next.dispatchingRunId);
|
|
1575
|
+
}
|
|
1576
|
+
else {
|
|
1577
|
+
this.clearQueueDispatchAttempt(next.queueId, next.dispatchingRunId);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
try {
|
|
1581
|
+
const queuedActivationMetadata = parsePromptActivationMetadata(next.activationMetadataJson);
|
|
1582
|
+
await this.dispatchPrompt(next.conversationId, next.promptText, {
|
|
1583
|
+
recordAsUserMessage: next.recordAsUserMessage !== 0,
|
|
1584
|
+
activationContextText: next.activationContextText ?? undefined,
|
|
1585
|
+
activationContextMode: next.activationContextMode === 'resume_only' ? 'resume_only' : undefined,
|
|
1586
|
+
activationContextSections: queuedActivationMetadata?.promptContextSections?.activation,
|
|
1587
|
+
resumeContextText: next.resumeContextText ?? undefined,
|
|
1588
|
+
resumeContextSections: queuedActivationMetadata?.promptContextSections?.resume,
|
|
1589
|
+
replayOverlapRecentMessages: parseReplayOverlapRecentMessages(next.replayOverlapRecentMessagesJson),
|
|
1590
|
+
activationMetadata: queuedActivationMetadata,
|
|
1591
|
+
attachments: queuedActivationMetadata?.attachments,
|
|
1592
|
+
attachmentIds: queuedActivationMetadata?.attachmentIds,
|
|
1593
|
+
runtimeOverrides: queuedActivationMetadata?.runtimeOverrides,
|
|
1594
|
+
agentSurfaceMode: normalizeDispatchAgentSurfaceMode(next.agentSurfaceMode, normalizeDispatchAgentSurfaceMode(this.config.agentSurfaceMode, 'bigbang')),
|
|
1595
|
+
senderName: next.senderName ?? undefined,
|
|
1596
|
+
clientMessageId: next.clientMessageId ?? undefined,
|
|
1597
|
+
suppressReplyContract: queuedActivationMetadata?.suppressReplyContract === true,
|
|
1598
|
+
queuedPromptId: next.queueId,
|
|
1599
|
+
});
|
|
1600
|
+
this.deleteQueuedPrompt(next.queueId);
|
|
1601
|
+
}
|
|
1602
|
+
catch {
|
|
1603
|
+
this.updateStatus(next.conversationId, 'failed');
|
|
1604
|
+
}
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
handleRunAccepted(runId, conversationId) {
|
|
1609
|
+
const pending = this.resolvePendingDispatchAcceptance(runId);
|
|
1610
|
+
const acceptedConversationIds = pending?.queuedPromptId != null
|
|
1611
|
+
? this.markQueueDispatchAccepted(runId, { queueId: pending.queuedPromptId, kind: 'dispatch' })
|
|
1612
|
+
: this.markQueueDispatchAccepted(runId, { kind: 'dispatch' });
|
|
1613
|
+
if (!pending && acceptedConversationIds.length <= 0)
|
|
1614
|
+
return false;
|
|
1615
|
+
const conversationIds = new Set([
|
|
1616
|
+
conversationId,
|
|
1617
|
+
pending?.conversationId,
|
|
1618
|
+
...acceptedConversationIds,
|
|
1619
|
+
].filter((id) => !!id));
|
|
1620
|
+
for (const id of conversationIds) {
|
|
1621
|
+
this.enableConversationBuiltinSkillRootsForAcceptedRun(runId, id);
|
|
1622
|
+
this.updateStatus(id, 'active');
|
|
1623
|
+
}
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
handleNodeHostSnapshotCurrentRun(params) {
|
|
1627
|
+
const runId = typeof params === 'string' ? params : params.runId;
|
|
1628
|
+
const status = getSnapshotConversationStatus(this.db, {
|
|
1629
|
+
runId,
|
|
1630
|
+
hasPendingApproval: typeof params === 'string' ? null : params.hasPendingApproval,
|
|
1631
|
+
});
|
|
1632
|
+
const pending = this.resolvePendingDispatchAcceptance(runId);
|
|
1633
|
+
const conversationIds = pending?.queuedPromptId != null
|
|
1634
|
+
? this.markQueueDispatchAccepted(runId, { queueId: pending.queuedPromptId, kind: 'dispatch' })
|
|
1635
|
+
: this.markQueueDispatchAccepted(runId, { kind: 'dispatch' });
|
|
1636
|
+
if (pending?.conversationId)
|
|
1637
|
+
conversationIds.push(pending.conversationId);
|
|
1638
|
+
if (typeof params !== 'string') {
|
|
1639
|
+
const reattachedConversationId = this.reattachActiveRunFromHostSnapshot(params);
|
|
1640
|
+
if (reattachedConversationId)
|
|
1641
|
+
conversationIds.push(reattachedConversationId);
|
|
1642
|
+
}
|
|
1643
|
+
const uniqueConversationIds = [...new Set(conversationIds)];
|
|
1644
|
+
for (const conversationId of uniqueConversationIds) {
|
|
1645
|
+
this.enableConversationBuiltinSkillRootsForAcceptedRun(runId, conversationId);
|
|
1646
|
+
this.updateStatus(conversationId, status);
|
|
1647
|
+
}
|
|
1648
|
+
return uniqueConversationIds;
|
|
1649
|
+
}
|
|
1650
|
+
enableConversationBuiltinSkillRootsForAcceptedRun(runId, conversationId) {
|
|
1651
|
+
const requestedBuiltinSkillRoots = this.getConversationBuiltinSkillRootsForAcceptedRun(runId);
|
|
1652
|
+
if (requestedBuiltinSkillRoots.length === 0)
|
|
1653
|
+
return;
|
|
1654
|
+
const row = this.db.prepare(`SELECT builtin_skill_roots as builtinSkillRootsJson
|
|
1655
|
+
FROM conversations
|
|
1656
|
+
WHERE id = ?`).get(conversationId);
|
|
1657
|
+
if (!row)
|
|
1658
|
+
return;
|
|
1659
|
+
const existingBuiltinSkillRoots = parseConversationBuiltinSkillRoots(row.builtinSkillRootsJson);
|
|
1660
|
+
const mergedBuiltinSkillRoots = normalizeConversationBuiltinSkillRoots([
|
|
1661
|
+
...existingBuiltinSkillRoots,
|
|
1662
|
+
...requestedBuiltinSkillRoots,
|
|
1663
|
+
]);
|
|
1664
|
+
if (stringArraysEqual(existingBuiltinSkillRoots, mergedBuiltinSkillRoots))
|
|
1665
|
+
return;
|
|
1666
|
+
this.db.prepare(`UPDATE conversations
|
|
1667
|
+
SET builtin_skill_roots = ?,
|
|
1668
|
+
updated_at = ?
|
|
1669
|
+
WHERE id = ?`).run(serializeConversationBuiltinSkillRoots(mergedBuiltinSkillRoots), Date.now(), conversationId);
|
|
1670
|
+
}
|
|
1671
|
+
getConversationBuiltinSkillRootsForAcceptedRun(runId) {
|
|
1672
|
+
const row = this.db.prepare(`SELECT activation_metadata_json as activationMetadataJson
|
|
1673
|
+
FROM run_debug_inputs
|
|
1674
|
+
WHERE run_id = ?
|
|
1675
|
+
LIMIT 1`).get(runId);
|
|
1676
|
+
const activationMetadata = parsePromptActivationMetadata(row?.activationMetadataJson);
|
|
1677
|
+
return conversationBuiltinSkillRootsForSlashCommand(activationMetadata?.slashCommand?.name);
|
|
1678
|
+
}
|
|
1679
|
+
getRunDebugInputAgentSurfaceMode(runId) {
|
|
1680
|
+
const row = this.db.prepare(`SELECT agent_surface_mode as agentSurfaceMode
|
|
1681
|
+
FROM run_debug_inputs
|
|
1682
|
+
WHERE run_id = ?
|
|
1683
|
+
LIMIT 1`).get(runId);
|
|
1684
|
+
return normalizeDispatchAgentSurfaceMode(row?.agentSurfaceMode, null);
|
|
1685
|
+
}
|
|
1686
|
+
reattachActiveRunFromHostSnapshot(params) {
|
|
1687
|
+
const row = this.db.prepare(`SELECT c.id as conversationId,
|
|
1688
|
+
c.node_id as nodeId,
|
|
1689
|
+
c.agent_type as agentType,
|
|
1690
|
+
c.session_key as conversationSessionKey,
|
|
1691
|
+
r.session_key as runSessionKey,
|
|
1692
|
+
r.ended_at as endedAt,
|
|
1693
|
+
r.stop_reason as stopReason,
|
|
1694
|
+
r.error as error
|
|
1695
|
+
FROM runs r
|
|
1696
|
+
JOIN conversations c ON c.session_key = r.session_key
|
|
1697
|
+
WHERE r.run_id = ?
|
|
1698
|
+
LIMIT 1`).get(params.runId);
|
|
1699
|
+
if (!row)
|
|
1700
|
+
return null;
|
|
1701
|
+
const expectedHostKey = `conversation:${row.conversationId}:${row.agentType}`;
|
|
1702
|
+
if (row.nodeId !== params.nodeId
|
|
1703
|
+
|| row.agentType !== params.agentType
|
|
1704
|
+
|| row.conversationSessionKey !== params.sessionKey
|
|
1705
|
+
|| row.runSessionKey !== params.sessionKey
|
|
1706
|
+
|| params.hostKey !== expectedHostKey) {
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
if (row.endedAt == null) {
|
|
1710
|
+
return row.conversationId;
|
|
1711
|
+
}
|
|
1712
|
+
if (row.stopReason != null || !isRecoverableEndedRunError(row.error)) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
const result = this.db.prepare(`UPDATE runs
|
|
1716
|
+
SET ended_at = NULL,
|
|
1717
|
+
stop_reason = NULL,
|
|
1718
|
+
error = NULL
|
|
1719
|
+
WHERE run_id = ?
|
|
1720
|
+
AND ended_at IS NOT NULL`).run(params.runId);
|
|
1721
|
+
if (result.changes <= 0)
|
|
1722
|
+
return null;
|
|
1723
|
+
log.warn('[dispatcher] reattached active run from node host snapshot', {
|
|
1724
|
+
conversationId: row.conversationId,
|
|
1725
|
+
runId: params.runId,
|
|
1726
|
+
nodeId: params.nodeId,
|
|
1727
|
+
agentType: row.agentType,
|
|
1728
|
+
previousError: row.error,
|
|
1729
|
+
});
|
|
1730
|
+
return row.conversationId;
|
|
1731
|
+
}
|
|
1732
|
+
handleRunFinished(runId, conversationId, options) {
|
|
1733
|
+
const dreamDebug = this.db.prepare(`SELECT dispatch_mode as dispatchMode,
|
|
1734
|
+
activation_metadata_json as activationMetadataJson
|
|
1735
|
+
FROM run_debug_inputs
|
|
1736
|
+
WHERE run_id = ?
|
|
1737
|
+
LIMIT 1`).get(runId);
|
|
1738
|
+
if (dreamDebug?.dispatchMode === 'dream') {
|
|
1739
|
+
let dreamAgentId = null;
|
|
1740
|
+
let dreamStartSeq = null;
|
|
1741
|
+
if (dreamDebug.activationMetadataJson) {
|
|
1742
|
+
try {
|
|
1743
|
+
const parsed = JSON.parse(dreamDebug.activationMetadataJson);
|
|
1744
|
+
dreamAgentId = parsed.agentId?.trim() || null;
|
|
1745
|
+
dreamStartSeq = typeof parsed.dreamStartSeq === 'number' ? parsed.dreamStartSeq : null;
|
|
1746
|
+
}
|
|
1747
|
+
catch {
|
|
1748
|
+
dreamAgentId = null;
|
|
1749
|
+
dreamStartSeq = null;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (!dreamAgentId) {
|
|
1753
|
+
const conversation = this.db.prepare(`SELECT agent_id as agentId FROM conversations WHERE id = ?`).get(conversationId);
|
|
1754
|
+
dreamAgentId = conversation?.agentId ?? null;
|
|
1755
|
+
}
|
|
1756
|
+
const run = this.db.prepare(`SELECT error, stop_reason as stopReason FROM runs WHERE run_id = ?`).get(runId);
|
|
1757
|
+
let finalized = false;
|
|
1758
|
+
if (dreamAgentId) {
|
|
1759
|
+
finalized = finalizeDreamRun(this.db, dreamAgentId, {
|
|
1760
|
+
startSeq: dreamStartSeq,
|
|
1761
|
+
error: options?.error ?? run?.error,
|
|
1762
|
+
stopReason: options?.stopReason ?? run?.stopReason,
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
this.updateStatus(conversationId, 'idle');
|
|
1766
|
+
return finalized;
|
|
1767
|
+
}
|
|
1768
|
+
const pending = this.resolvePendingDispatchAcceptance(runId);
|
|
1769
|
+
const acceptedConversationIds = pending?.queuedPromptId != null
|
|
1770
|
+
? this.markQueueDispatchAccepted(runId, { queueId: pending.queuedPromptId, kind: 'dispatch' })
|
|
1771
|
+
: this.markQueueDispatchAccepted(runId, { kind: 'dispatch' });
|
|
1772
|
+
if (!pending && acceptedConversationIds.length <= 0)
|
|
1773
|
+
return false;
|
|
1774
|
+
const conversationIds = new Set([
|
|
1775
|
+
conversationId,
|
|
1776
|
+
pending?.conversationId,
|
|
1777
|
+
...acceptedConversationIds,
|
|
1778
|
+
].filter((id) => !!id));
|
|
1779
|
+
for (const id of conversationIds) {
|
|
1780
|
+
this.updateStatus(id, 'active');
|
|
1781
|
+
}
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
close() {
|
|
1785
|
+
for (const timer of this.queueRetryTimers.values()) {
|
|
1786
|
+
clearTimeout(timer);
|
|
1787
|
+
}
|
|
1788
|
+
this.queueRetryTimers.clear();
|
|
1789
|
+
}
|
|
1790
|
+
/** Recover steer acks that were persisted to the events table but whose
|
|
1791
|
+
* pendingSteerResponses Map entry was lost across a core restart.
|
|
1792
|
+
*
|
|
1793
|
+
* Scans events for orphan `run.steer.result` events, matches them to
|
|
1794
|
+
* pending queue entries with `dispatch_kind = 'steer'`, and either:
|
|
1795
|
+
* - ack: marks the queue entry as accepted (ok=true)
|
|
1796
|
+
* - fallback: clears the steer attempt and marks as dispatch (ok=false)
|
|
1797
|
+
*/
|
|
1798
|
+
recoverSteerAcks() {
|
|
1799
|
+
// 1. Collect all steer result events from the events table
|
|
1800
|
+
const steerResultRows = this.db.prepare(`SELECT run_id as runId,
|
|
1801
|
+
seq,
|
|
1802
|
+
payload_json as payloadJson,
|
|
1803
|
+
created_at as createdAt
|
|
1804
|
+
FROM events
|
|
1805
|
+
WHERE method = 'node/event'
|
|
1806
|
+
AND json_valid(payload_json)
|
|
1807
|
+
AND json_extract(payload_json, '$.type') = 'run.steer.result'
|
|
1808
|
+
ORDER BY created_at ASC, seq ASC`).all();
|
|
1809
|
+
if (steerResultRows.length === 0)
|
|
1810
|
+
return;
|
|
1811
|
+
const steerResults = [];
|
|
1812
|
+
for (const row of steerResultRows) {
|
|
1813
|
+
try {
|
|
1814
|
+
const payload = JSON.parse(row.payloadJson);
|
|
1815
|
+
if (typeof payload.requestId === 'string'
|
|
1816
|
+
&& typeof payload.runId === 'string'
|
|
1817
|
+
&& typeof payload.ok === 'boolean') {
|
|
1818
|
+
steerResults.push({
|
|
1819
|
+
requestId: payload.requestId,
|
|
1820
|
+
runId: payload.runId,
|
|
1821
|
+
ok: payload.ok,
|
|
1822
|
+
createdAt: row.createdAt,
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
catch {
|
|
1827
|
+
// skip malformed payloads
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
if (steerResults.length === 0)
|
|
1831
|
+
return;
|
|
1832
|
+
// 2. Collect pending steer queue entries (not yet accepted)
|
|
1833
|
+
const pendingSteerQueueRows = this.db.prepare(`SELECT q.queue_id as queueId,
|
|
1834
|
+
q.dispatching_run_id as dispatchingRunId,
|
|
1835
|
+
q.dispatch_request_id as dispatchRequestId,
|
|
1836
|
+
q.dispatch_sent_at as dispatchSentAt,
|
|
1837
|
+
r.ended_at as runEndedAt,
|
|
1838
|
+
q.created_at as createdAt
|
|
1839
|
+
FROM conversation_prompt_queue q
|
|
1840
|
+
JOIN runs r ON r.run_id = q.dispatching_run_id
|
|
1841
|
+
WHERE q.dispatch_kind = 'steer'
|
|
1842
|
+
AND q.dispatch_accepted_at IS NULL
|
|
1843
|
+
AND q.dispatching_run_id IS NOT NULL
|
|
1844
|
+
AND q.dispatch_request_id IS NOT NULL
|
|
1845
|
+
ORDER BY q.created_at ASC, q.queue_id ASC`).all();
|
|
1846
|
+
if (pendingSteerQueueRows.length === 0)
|
|
1847
|
+
return;
|
|
1848
|
+
// 3. Group steer results and queue entries by exact request id.
|
|
1849
|
+
const steerResultsByRequestId = new Map();
|
|
1850
|
+
for (const sr of steerResults) {
|
|
1851
|
+
steerResultsByRequestId.set(sr.requestId, sr);
|
|
1852
|
+
}
|
|
1853
|
+
const queueEntriesByRequestId = new Map();
|
|
1854
|
+
for (const qe of pendingSteerQueueRows) {
|
|
1855
|
+
queueEntriesByRequestId.set(qe.dispatchRequestId, qe);
|
|
1856
|
+
}
|
|
1857
|
+
// 4. Recover only exact request-id matches. Chronological run matching can
|
|
1858
|
+
// attach a late ack to a different follow-up on the same long-running turn.
|
|
1859
|
+
let recoveredCount = 0;
|
|
1860
|
+
for (const [requestId, result] of steerResultsByRequestId) {
|
|
1861
|
+
const entry = queueEntriesByRequestId.get(requestId);
|
|
1862
|
+
if (!entry || entry.dispatchingRunId !== result.runId)
|
|
1863
|
+
continue;
|
|
1864
|
+
if (entry.runEndedAt != null) {
|
|
1865
|
+
this.clearQueueDispatchAttempt(entry.queueId, result.runId, result.requestId);
|
|
1866
|
+
this.markSteerResultRequestProcessed(result.requestId);
|
|
1867
|
+
recoveredCount++;
|
|
1868
|
+
log.warn('[dispatcher] ignored steer ack for ended run during recovery; queued prompt will fallback to dispatch', {
|
|
1869
|
+
runId: result.runId,
|
|
1870
|
+
queueId: entry.queueId,
|
|
1871
|
+
requestId: result.requestId,
|
|
1872
|
+
});
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
if (result.ok) {
|
|
1876
|
+
// ok steers: mark queue entry as accepted so handleConversationSettled will delete it
|
|
1877
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
1878
|
+
SET dispatch_accepted_at = ?, updated_at = ?
|
|
1879
|
+
WHERE queue_id = ?`).run(Date.now(), Date.now(), entry.queueId);
|
|
1880
|
+
log.info('[dispatcher] recovered steer ack (ok) from events table', {
|
|
1881
|
+
runId: result.runId,
|
|
1882
|
+
queueId: entry.queueId,
|
|
1883
|
+
requestId: result.requestId,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
// failed steers: fall back to queue dispatch
|
|
1888
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
1889
|
+
SET dispatch_kind = 'dispatch',
|
|
1890
|
+
dispatching_run_id = NULL,
|
|
1891
|
+
dispatch_request_id = NULL,
|
|
1892
|
+
dispatch_sent_at = NULL,
|
|
1893
|
+
dispatch_accepted_at = NULL,
|
|
1894
|
+
updated_at = ?
|
|
1895
|
+
WHERE queue_id = ?`).run(Date.now(), entry.queueId);
|
|
1896
|
+
log.info('[dispatcher] recovered steer ack (fail → fallback to dispatch) from events table', {
|
|
1897
|
+
runId: result.runId,
|
|
1898
|
+
queueId: entry.queueId,
|
|
1899
|
+
requestId: result.requestId,
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
this.markSteerResultRequestProcessed(result.requestId);
|
|
1903
|
+
recoveredCount++;
|
|
1904
|
+
}
|
|
1905
|
+
if (recoveredCount > 0) {
|
|
1906
|
+
log.info('[dispatcher] steer ack recovery complete', { recoveredCount });
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
recoverOrphanedSteerResult(params) {
|
|
1910
|
+
const entry = this.db.prepare(`SELECT q.queue_id as queueId,
|
|
1911
|
+
r.ended_at as runEndedAt
|
|
1912
|
+
FROM conversation_prompt_queue q
|
|
1913
|
+
JOIN runs r ON r.run_id = q.dispatching_run_id
|
|
1914
|
+
WHERE q.conversation_id = ?
|
|
1915
|
+
AND q.dispatching_run_id = ?
|
|
1916
|
+
AND q.dispatch_request_id = ?
|
|
1917
|
+
AND q.dispatch_kind = 'steer'
|
|
1918
|
+
AND q.dispatch_accepted_at IS NULL
|
|
1919
|
+
ORDER BY q.created_at ASC, q.queue_id ASC
|
|
1920
|
+
LIMIT 1`).get(params.conversationId, params.runId, params.requestId);
|
|
1921
|
+
if (!entry)
|
|
1922
|
+
return false;
|
|
1923
|
+
const now = Date.now();
|
|
1924
|
+
if (entry.runEndedAt != null) {
|
|
1925
|
+
this.clearQueueDispatchAttempt(entry.queueId, params.runId, params.requestId);
|
|
1926
|
+
log.warn('[dispatcher] ignored live orphan steer ack for ended run; queued prompt will fallback to dispatch', {
|
|
1927
|
+
requestId: params.requestId,
|
|
1928
|
+
runId: params.runId,
|
|
1929
|
+
conversationId: params.conversationId,
|
|
1930
|
+
queueId: entry.queueId,
|
|
1931
|
+
});
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
if (params.ok) {
|
|
1935
|
+
const result = this.db.prepare(`UPDATE conversation_prompt_queue
|
|
1936
|
+
SET dispatch_accepted_at = ?, updated_at = ?
|
|
1937
|
+
WHERE queue_id = ?
|
|
1938
|
+
AND conversation_id = ?
|
|
1939
|
+
AND dispatching_run_id = ?
|
|
1940
|
+
AND dispatch_request_id = ?
|
|
1941
|
+
AND dispatch_kind = 'steer'
|
|
1942
|
+
AND dispatch_accepted_at IS NULL`).run(now, now, entry.queueId, params.conversationId, params.runId, params.requestId);
|
|
1943
|
+
if (result.changes <= 0)
|
|
1944
|
+
return false;
|
|
1945
|
+
log.info('[dispatcher] recovered live orphan steer ack', {
|
|
1946
|
+
requestId: params.requestId,
|
|
1947
|
+
runId: params.runId,
|
|
1948
|
+
conversationId: params.conversationId,
|
|
1949
|
+
queueId: entry.queueId,
|
|
1950
|
+
});
|
|
1951
|
+
return true;
|
|
1952
|
+
}
|
|
1953
|
+
const result = this.db.prepare(`UPDATE conversation_prompt_queue
|
|
1954
|
+
SET dispatch_kind = 'dispatch',
|
|
1955
|
+
dispatching_run_id = NULL,
|
|
1956
|
+
dispatch_request_id = NULL,
|
|
1957
|
+
dispatch_sent_at = NULL,
|
|
1958
|
+
dispatch_accepted_at = NULL,
|
|
1959
|
+
updated_at = ?
|
|
1960
|
+
WHERE queue_id = ?
|
|
1961
|
+
AND conversation_id = ?
|
|
1962
|
+
AND dispatching_run_id = ?
|
|
1963
|
+
AND dispatch_request_id = ?
|
|
1964
|
+
AND dispatch_kind = 'steer'
|
|
1965
|
+
AND dispatch_accepted_at IS NULL`).run(now, entry.queueId, params.conversationId, params.runId, params.requestId);
|
|
1966
|
+
if (result.changes <= 0)
|
|
1967
|
+
return false;
|
|
1968
|
+
log.info('[dispatcher] recovered live orphan failed steer ack for queue fallback', {
|
|
1969
|
+
requestId: params.requestId,
|
|
1970
|
+
runId: params.runId,
|
|
1971
|
+
conversationId: params.conversationId,
|
|
1972
|
+
queueId: entry.queueId,
|
|
1973
|
+
error: params.error,
|
|
1974
|
+
});
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
markSteerResultRequestProcessed(requestId) {
|
|
1978
|
+
this.processedSteerResultRequestIds.add(requestId);
|
|
1979
|
+
if (this.processedSteerResultRequestIds.size <= MAX_PROCESSED_STEER_RESULT_REQUEST_IDS)
|
|
1980
|
+
return;
|
|
1981
|
+
const first = this.processedSteerResultRequestIds.values().next().value;
|
|
1982
|
+
if (first)
|
|
1983
|
+
this.processedSteerResultRequestIds.delete(first);
|
|
1984
|
+
}
|
|
1985
|
+
handleRunSteerResult(params) {
|
|
1986
|
+
const pending = this.pendingSteerResponses.get(params.requestId);
|
|
1987
|
+
if (!pending) {
|
|
1988
|
+
if (this.processedSteerResultRequestIds.has(params.requestId))
|
|
1989
|
+
return false;
|
|
1990
|
+
const recovered = this.recoverOrphanedSteerResult(params);
|
|
1991
|
+
if (recovered)
|
|
1992
|
+
this.markSteerResultRequestProcessed(params.requestId);
|
|
1993
|
+
return recovered;
|
|
1994
|
+
}
|
|
1995
|
+
if (pending.timer)
|
|
1996
|
+
clearTimeout(pending.timer);
|
|
1997
|
+
this.pendingSteerResponses.delete(params.requestId);
|
|
1998
|
+
this.markSteerResultRequestProcessed(params.requestId);
|
|
1999
|
+
if (pending.runId !== params.runId || pending.conversationId !== params.conversationId) {
|
|
2000
|
+
pending.reject(new Error('Node steer response did not match the pending request.'));
|
|
2001
|
+
return true;
|
|
2002
|
+
}
|
|
2003
|
+
if (!params.ok) {
|
|
2004
|
+
pending.reject(new Error(params.error?.trim() || 'Active runtime turn is not steerable.'));
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
this.markQueueDispatchAccepted(params.runId, pending.queuedPromptId != null
|
|
2008
|
+
? { queueId: pending.queuedPromptId, kind: 'steer' }
|
|
2009
|
+
: { kind: 'steer' });
|
|
2010
|
+
pending.resolve();
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
async sendBestEffortSystemNoticeToActiveRun(params) {
|
|
2014
|
+
const row = this.db.prepare(`SELECT agent_id as agentId,
|
|
2015
|
+
agent_type as agentType,
|
|
2016
|
+
node_id as nodeId
|
|
2017
|
+
FROM conversations
|
|
2018
|
+
WHERE id = ?`).get(params.conversationId);
|
|
2019
|
+
if (!row?.agentId || !row.nodeId || !hasEffectiveAgentRuntimeCapability(this.db, {
|
|
2020
|
+
nodeId: row.nodeId,
|
|
2021
|
+
agentType: row.agentType,
|
|
2022
|
+
capability: 'activeTurnSteer',
|
|
2023
|
+
}))
|
|
2024
|
+
return false;
|
|
2025
|
+
if (this.steerChains.has(params.runId))
|
|
2026
|
+
return false;
|
|
2027
|
+
let resolveAttempt = null;
|
|
2028
|
+
let attemptSettled = false;
|
|
2029
|
+
const attemptPromise = new Promise((resolve) => {
|
|
2030
|
+
resolveAttempt = resolve;
|
|
2031
|
+
});
|
|
2032
|
+
const settleAttempt = (value) => {
|
|
2033
|
+
if (attemptSettled)
|
|
2034
|
+
return;
|
|
2035
|
+
attemptSettled = true;
|
|
2036
|
+
resolveAttempt?.(value);
|
|
2037
|
+
};
|
|
2038
|
+
void this.withSteerSerial(params.runId, async () => {
|
|
2039
|
+
const active = this.db.prepare(`SELECT 1
|
|
2040
|
+
FROM conversations c
|
|
2041
|
+
JOIN runs r ON r.session_key = c.session_key
|
|
2042
|
+
WHERE c.id = ?
|
|
2043
|
+
AND r.run_id = ?
|
|
2044
|
+
AND r.ended_at IS NULL
|
|
2045
|
+
LIMIT 1`).get(params.conversationId, params.runId);
|
|
2046
|
+
if (!active) {
|
|
2047
|
+
settleAttempt(false);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
const requestId = randomUUID();
|
|
2051
|
+
const response = this.waitForSteerResponse(requestId, row.nodeId, params.runId, params.conversationId, undefined, ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS);
|
|
2052
|
+
const sent = this.nodeRegistry?.send(row.nodeId, {
|
|
2053
|
+
type: 'run.steer',
|
|
2054
|
+
requestId,
|
|
2055
|
+
runId: params.runId,
|
|
2056
|
+
conversationId: params.conversationId,
|
|
2057
|
+
prompt: params.promptText,
|
|
2058
|
+
});
|
|
2059
|
+
if (!sent) {
|
|
2060
|
+
this.clearPendingSteerResponse(requestId);
|
|
2061
|
+
settleAttempt(false);
|
|
2062
|
+
log.warn('[dispatcher] source handoff notice steer send failed', {
|
|
2063
|
+
nodeId: row.nodeId,
|
|
2064
|
+
conversationId: params.conversationId,
|
|
2065
|
+
runId: params.runId,
|
|
2066
|
+
});
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
await response;
|
|
2071
|
+
settleAttempt(true);
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
settleAttempt(false);
|
|
2075
|
+
log.warn('[dispatcher] source handoff notice steer failed', {
|
|
2076
|
+
nodeId: row.nodeId,
|
|
2077
|
+
conversationId: params.conversationId,
|
|
2078
|
+
runId: params.runId,
|
|
2079
|
+
error: String(error?.message ?? error),
|
|
2080
|
+
});
|
|
2081
|
+
}
|
|
2082
|
+
}).catch((error) => {
|
|
2083
|
+
settleAttempt(false);
|
|
2084
|
+
log.warn('[dispatcher] source handoff notice steer crashed', {
|
|
2085
|
+
conversationId: params.conversationId,
|
|
2086
|
+
runId: params.runId,
|
|
2087
|
+
error: String(error?.message ?? error),
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
2090
|
+
return attemptPromise;
|
|
2091
|
+
}
|
|
2092
|
+
rejectPendingDispatchesForNode(nodeId, errorMessage) {
|
|
2093
|
+
for (const [runId, pending] of this.pendingDispatchAcceptances.entries()) {
|
|
2094
|
+
if (pending.nodeId !== nodeId)
|
|
2095
|
+
continue;
|
|
2096
|
+
if (pending.timer)
|
|
2097
|
+
clearTimeout(pending.timer);
|
|
2098
|
+
pending.reject(new Error(errorMessage));
|
|
2099
|
+
this.pendingDispatchAcceptances.delete(runId);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
rejectPendingSteersForNode(nodeId, errorMessage) {
|
|
2103
|
+
for (const [requestId, pending] of this.pendingSteerResponses.entries()) {
|
|
2104
|
+
if (pending.nodeId !== nodeId)
|
|
2105
|
+
continue;
|
|
2106
|
+
if (pending.timer)
|
|
2107
|
+
clearTimeout(pending.timer);
|
|
2108
|
+
pending.reject(new Error(errorMessage));
|
|
2109
|
+
this.pendingSteerResponses.delete(requestId);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
rejectPendingSteersForRun(runId, conversationId, errorMessage) {
|
|
2113
|
+
this.clearQueueSteerAttemptsForRun(runId, conversationId);
|
|
2114
|
+
for (const [requestId, pending] of this.pendingSteerResponses.entries()) {
|
|
2115
|
+
if (pending.runId !== runId || pending.conversationId !== conversationId)
|
|
2116
|
+
continue;
|
|
2117
|
+
if (pending.timer)
|
|
2118
|
+
clearTimeout(pending.timer);
|
|
2119
|
+
pending.reject(new Error(errorMessage));
|
|
2120
|
+
this.pendingSteerResponses.delete(requestId);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
ensureConversationSessionAgent(agentType) {
|
|
2124
|
+
const driver = getRuntimeDriver(agentType);
|
|
2125
|
+
return { command: driver.command, args: [...driver.args] };
|
|
2126
|
+
}
|
|
2127
|
+
isIgnorableReplayAssistantText(text) {
|
|
2128
|
+
return text.includes(`Empty response: {'content':`)
|
|
2129
|
+
|| text.trim() === '[stop_reason] handoff'
|
|
2130
|
+
|| text.trim() === '[stop_reason] handoff_bootstrap';
|
|
2131
|
+
}
|
|
2132
|
+
compactReplayFailureSummary(error) {
|
|
2133
|
+
const compact = sanitizePromptHistoryContent(error, 'agent')
|
|
2134
|
+
.replace(/\s+/g, ' ')
|
|
2135
|
+
.trim();
|
|
2136
|
+
if (compact.length <= 300)
|
|
2137
|
+
return compact;
|
|
2138
|
+
return `${compact.slice(0, 297).trimEnd()}...`;
|
|
2139
|
+
}
|
|
2140
|
+
getDispatchMode(sessionKey) {
|
|
2141
|
+
const row = this.db
|
|
2142
|
+
.prepare('SELECT COUNT(*) as count FROM runs WHERE session_key = ?')
|
|
2143
|
+
.get(sessionKey);
|
|
2144
|
+
return row.count > 0 ? 'resume' : 'cold_start';
|
|
2145
|
+
}
|
|
2146
|
+
loadNodeSurfaceReadiness(nodeId) {
|
|
2147
|
+
const row = this.db.prepare(`SELECT process_instance_id as processInstanceId,
|
|
2148
|
+
capabilities_json as capabilitiesJson
|
|
2149
|
+
FROM node_runtime_snapshots
|
|
2150
|
+
WHERE node_id = ?
|
|
2151
|
+
LIMIT 1`).get(nodeId);
|
|
2152
|
+
if (!row) {
|
|
2153
|
+
return {
|
|
2154
|
+
hasSnapshot: false,
|
|
2155
|
+
processInstanceId: null,
|
|
2156
|
+
agentSurfaceMode: null,
|
|
2157
|
+
bigbangCliAvailable: null,
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
let capabilities;
|
|
2161
|
+
try {
|
|
2162
|
+
capabilities = row.capabilitiesJson ? JSON.parse(row.capabilitiesJson) : {};
|
|
2163
|
+
}
|
|
2164
|
+
catch {
|
|
2165
|
+
capabilities = {};
|
|
2166
|
+
}
|
|
2167
|
+
const record = capabilities && typeof capabilities === 'object' && !Array.isArray(capabilities)
|
|
2168
|
+
? capabilities
|
|
2169
|
+
: {};
|
|
2170
|
+
const agentSurfaceMode = normalizeDispatchAgentSurfaceMode(record.agentSurfaceMode, null);
|
|
2171
|
+
return {
|
|
2172
|
+
hasSnapshot: true,
|
|
2173
|
+
processInstanceId: row.processInstanceId,
|
|
2174
|
+
agentSurfaceMode,
|
|
2175
|
+
bigbangCliAvailable: typeof record.bigbangCliAvailable === 'boolean' ? record.bigbangCliAvailable : null,
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2179
|
+
* Rebuild recent conversation history from core's DB (content.delta events) so that
|
|
2180
|
+
* a freshly restarted ACP process can recover context it would otherwise have lost.
|
|
2181
|
+
*/
|
|
2182
|
+
buildConversationReplayText(conversationId, sessionKey, excludeRunId, overlapRecentMessages, hasVisibleRecentWindow = false) {
|
|
2183
|
+
if (!this.config.contextReplayEnabled || this.config.contextReplayRuns <= 0)
|
|
2184
|
+
return '';
|
|
2185
|
+
const conversationAgentName = getConversationAgentName(this.db, conversationId);
|
|
2186
|
+
const resetRow = this.db.prepare(`SELECT history_reset_at as historyResetAt
|
|
2187
|
+
FROM conversations
|
|
2188
|
+
WHERE id = ?`).get(conversationId);
|
|
2189
|
+
const historyResetAt = resetRow?.historyResetAt ?? null;
|
|
2190
|
+
const runs = this.db.prepare(`SELECT run_id as runId, prompt_text as promptText, stop_reason as stopReason, error
|
|
2191
|
+
FROM runs
|
|
2192
|
+
WHERE session_key = ?
|
|
2193
|
+
AND run_id != ?
|
|
2194
|
+
AND ended_at IS NOT NULL
|
|
2195
|
+
AND (? IS NULL OR started_at >= ?)
|
|
2196
|
+
ORDER BY started_at DESC
|
|
2197
|
+
LIMIT ?`).all(sessionKey, excludeRunId, historyResetAt, historyResetAt, this.config.contextReplayRuns);
|
|
2198
|
+
const chronological = runs.slice().reverse();
|
|
2199
|
+
const blocks = [];
|
|
2200
|
+
for (const run of chronological) {
|
|
2201
|
+
const visibleAgentMessages = this.db.prepare(`SELECT content, message_kind as messageKind, sender_name as senderName
|
|
2202
|
+
FROM channel_messages
|
|
2203
|
+
WHERE run_id = ?
|
|
2204
|
+
AND sender_type = 'agent'
|
|
2205
|
+
ORDER BY created_at ASC, seq ASC`).all(run.runId);
|
|
2206
|
+
let assistantLine = '';
|
|
2207
|
+
const finalMessage = [...visibleAgentMessages].reverse().find((row) => row.messageKind === 'final');
|
|
2208
|
+
const lastVisibleMessage = visibleAgentMessages.length > 0
|
|
2209
|
+
? visibleAgentMessages[visibleAgentMessages.length - 1]
|
|
2210
|
+
: undefined;
|
|
2211
|
+
const hasVisibleAgentMessage = Boolean(finalMessage?.content?.trim() || lastVisibleMessage?.content?.trim());
|
|
2212
|
+
if (finalMessage?.content?.trim()) {
|
|
2213
|
+
assistantLine = sanitizePromptHistoryContent(finalMessage.content, 'agent');
|
|
2214
|
+
}
|
|
2215
|
+
else if (lastVisibleMessage?.content?.trim()) {
|
|
2216
|
+
assistantLine = sanitizePromptHistoryContent(lastVisibleMessage.content, 'agent');
|
|
2217
|
+
}
|
|
2218
|
+
let hasAssistantDelta = false;
|
|
2219
|
+
if (!assistantLine) {
|
|
2220
|
+
let assistantText = '';
|
|
2221
|
+
for (const ev of iterateParsedNodeEvents(this.db, {
|
|
2222
|
+
runId: run.runId,
|
|
2223
|
+
types: ['content.delta'],
|
|
2224
|
+
})) {
|
|
2225
|
+
if (ev.event?.type === 'content.delta') {
|
|
2226
|
+
assistantText += ev.event.text;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const cleanedAssistantText = assistantText.trim();
|
|
2230
|
+
hasAssistantDelta = Boolean(cleanedAssistantText && !this.isIgnorableReplayAssistantText(cleanedAssistantText));
|
|
2231
|
+
assistantLine = cleanedAssistantText && !this.isIgnorableReplayAssistantText(cleanedAssistantText)
|
|
2232
|
+
? cleanedAssistantText
|
|
2233
|
+
: run.error
|
|
2234
|
+
? `[error] ${run.error}`
|
|
2235
|
+
: run.stopReason && !this.isIgnorableReplayAssistantText(`[stop_reason] ${run.stopReason}`)
|
|
2236
|
+
? `[stop_reason] ${run.stopReason}`
|
|
2237
|
+
: '';
|
|
2238
|
+
}
|
|
2239
|
+
if (!hasVisibleAgentMessage && !hasAssistantDelta && run.error?.trim()) {
|
|
2240
|
+
const assistantBlock = createReplayRenderableBlock({
|
|
2241
|
+
speaker: 'agent',
|
|
2242
|
+
renderLabel: conversationAgentName || 'Assistant',
|
|
2243
|
+
text: `Previous attempt failed before agent response: ${this.compactReplayFailureSummary(run.error)}`,
|
|
2244
|
+
});
|
|
2245
|
+
if (assistantBlock) {
|
|
2246
|
+
blocks.push(assistantBlock);
|
|
2247
|
+
}
|
|
2248
|
+
continue;
|
|
2249
|
+
}
|
|
2250
|
+
const normalizedUserText = normalizeReplayUserText(run.promptText);
|
|
2251
|
+
if (normalizedUserText) {
|
|
2252
|
+
const userBlock = createReplayRenderableBlock({
|
|
2253
|
+
speaker: 'user',
|
|
2254
|
+
renderLabel: 'User',
|
|
2255
|
+
text: normalizedUserText,
|
|
2256
|
+
});
|
|
2257
|
+
if (userBlock) {
|
|
2258
|
+
blocks.push(userBlock);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (assistantLine) {
|
|
2262
|
+
const assistantLabel = finalMessage?.senderName?.trim()
|
|
2263
|
+
|| lastVisibleMessage?.senderName?.trim()
|
|
2264
|
+
|| conversationAgentName
|
|
2265
|
+
|| 'Assistant';
|
|
2266
|
+
const assistantBlock = createReplayRenderableBlock({
|
|
2267
|
+
speaker: 'agent',
|
|
2268
|
+
renderLabel: assistantLabel,
|
|
2269
|
+
text: assistantLine,
|
|
2270
|
+
});
|
|
2271
|
+
if (assistantBlock) {
|
|
2272
|
+
blocks.push(assistantBlock);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
const trimmedBlocks = trimReplayBlocksAgainstRecentMessages(blocks, overlapRecentMessages);
|
|
2277
|
+
const raw = trimmedBlocks.map(renderReplayBlock).join('\n');
|
|
2278
|
+
if (!raw.trim())
|
|
2279
|
+
return '';
|
|
2280
|
+
const header = hasVisibleRecentWindow
|
|
2281
|
+
? 'Context (older messages from before the recent window, for continuity after restart):\n'
|
|
2282
|
+
: 'Context (previous messages, for continuity after restart):\n';
|
|
2283
|
+
const full = header + raw;
|
|
2284
|
+
if (full.length <= this.config.contextReplayMaxChars)
|
|
2285
|
+
return full;
|
|
2286
|
+
return header + raw.slice(Math.max(0, raw.length - this.config.contextReplayMaxChars));
|
|
2287
|
+
}
|
|
2288
|
+
updateStatus(conversationId, status) {
|
|
2289
|
+
this.db
|
|
2290
|
+
.prepare('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?')
|
|
2291
|
+
.run(status, Date.now(), conversationId);
|
|
2292
|
+
}
|
|
2293
|
+
findBlockingConversation(conversationId) {
|
|
2294
|
+
const row = this.db.prepare(`SELECT c.id, c.status, r.run_id as runId
|
|
2295
|
+
FROM conversations c
|
|
2296
|
+
LEFT JOIN runs r ON r.session_key = c.session_key AND r.ended_at IS NULL
|
|
2297
|
+
WHERE c.id = ?
|
|
2298
|
+
AND (
|
|
2299
|
+
r.run_id IS NOT NULL
|
|
2300
|
+
OR c.status IN ('active', 'recovering', 'awaiting_approval')
|
|
2301
|
+
)
|
|
2302
|
+
LIMIT 1`).get(conversationId);
|
|
2303
|
+
return row ?? null;
|
|
2304
|
+
}
|
|
2305
|
+
enqueuePrompt(agentId, conversationId, promptText, options) {
|
|
2306
|
+
const now = Date.now();
|
|
2307
|
+
const result = this.db.prepare(`INSERT INTO conversation_prompt_queue(
|
|
2308
|
+
agent_id, conversation_id, prompt_text, record_as_user_message, activation_context_text, activation_context_mode, resume_context_text, replay_overlap_recent_messages_json, activation_metadata_json, agent_surface_mode, sender_name, client_message_id, created_at, updated_at
|
|
2309
|
+
)
|
|
2310
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(agentId, conversationId, promptText, (options?.recordAsUserMessage ?? true) ? 1 : 0, options?.activationContextText?.trim() || null, options?.activationContextMode === 'resume_only' ? 'resume_only' : null, options?.resumeContextText?.trim() || null, serializeReplayOverlapRecentMessages(options?.replayOverlapRecentMessages), serializePromptActivationMetadata(enrichPromptActivationMetadataWithContextSections(options)), normalizeDispatchAgentSurfaceMode(options?.agentSurfaceMode, normalizeDispatchAgentSurfaceMode(this.config.agentSurfaceMode, 'bigbang')), options?.senderName ?? null, options?.clientMessageId ?? null, now, now);
|
|
2311
|
+
return Number(result.lastInsertRowid);
|
|
2312
|
+
}
|
|
2313
|
+
persistDirectUserMessageForQueuedPromptIfNeeded(conversationId, conversation, promptText, options) {
|
|
2314
|
+
if (!conversation.agentId || conversation.threadKind !== 'direct' || (options?.recordAsUserMessage ?? true) === false) {
|
|
2315
|
+
return options;
|
|
2316
|
+
}
|
|
2317
|
+
const persistedMessage = this.persistDirectUserMessageForConversation({
|
|
2318
|
+
conversationId,
|
|
2319
|
+
conversation,
|
|
2320
|
+
promptText,
|
|
2321
|
+
options,
|
|
2322
|
+
attachments: sanitizeFileRefs(options?.attachments),
|
|
2323
|
+
attachmentIds: sanitizeAttachmentIds(options?.attachmentIds),
|
|
2324
|
+
});
|
|
2325
|
+
return {
|
|
2326
|
+
...options,
|
|
2327
|
+
clientMessageId: persistedMessage.messageId,
|
|
2328
|
+
persistedDirectUserMessage: persistedMessage,
|
|
2329
|
+
activationMetadata: {
|
|
2330
|
+
...(options?.activationMetadata ?? {}),
|
|
2331
|
+
triggerMessage: {
|
|
2332
|
+
messageId: persistedMessage.messageId,
|
|
2333
|
+
seq: persistedMessage.seq,
|
|
2334
|
+
target: persistedMessage.replyTarget,
|
|
2335
|
+
},
|
|
2336
|
+
},
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
getConversationQueueHead(conversationId) {
|
|
2340
|
+
return this.db.prepare(`SELECT queue_id as queueId,
|
|
2341
|
+
dispatching_run_id as dispatchingRunId,
|
|
2342
|
+
dispatch_sent_at as dispatchSentAt,
|
|
2343
|
+
dispatch_accepted_at as dispatchAcceptedAt
|
|
2344
|
+
FROM conversation_prompt_queue
|
|
2345
|
+
WHERE conversation_id = ?
|
|
2346
|
+
AND COALESCE(dispatch_kind, '') != 'legacy_unknown'
|
|
2347
|
+
ORDER BY created_at ASC, queue_id ASC
|
|
2348
|
+
LIMIT 1`).get(conversationId);
|
|
2349
|
+
}
|
|
2350
|
+
deleteAcceptedQueueHeads(conversationId) {
|
|
2351
|
+
while (true) {
|
|
2352
|
+
const head = this.getConversationQueueHead(conversationId);
|
|
2353
|
+
if (!head || head.dispatchAcceptedAt == null)
|
|
2354
|
+
return;
|
|
2355
|
+
this.deleteQueuedPrompt(head.queueId);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
scheduleQueueRetryForHead(conversationId, head) {
|
|
2359
|
+
if (!head.dispatchingRunId || head.dispatchAcceptedAt != null || head.dispatchSentAt == null)
|
|
2360
|
+
return;
|
|
2361
|
+
const retryInMs = head.dispatchSentAt + ORPHANED_QUEUE_ATTEMPT_STALE_MS - Date.now();
|
|
2362
|
+
this.scheduleQueueSettledRetry(conversationId, Math.max(0, retryInMs));
|
|
2363
|
+
}
|
|
2364
|
+
scheduleQueueSettledRetry(conversationId, delayMs) {
|
|
2365
|
+
if (this.queueRetryTimers.has(conversationId))
|
|
2366
|
+
return;
|
|
2367
|
+
const timer = setTimeout(() => {
|
|
2368
|
+
this.queueRetryTimers.delete(conversationId);
|
|
2369
|
+
void this.handleConversationSettled(conversationId).catch((error) => {
|
|
2370
|
+
log.warn('[dispatcher] queued prompt retry failed', {
|
|
2371
|
+
conversationId,
|
|
2372
|
+
error: String(error?.message ?? error),
|
|
2373
|
+
});
|
|
2374
|
+
});
|
|
2375
|
+
}, Math.max(0, delayMs));
|
|
2376
|
+
timer.unref?.();
|
|
2377
|
+
this.queueRetryTimers.set(conversationId, timer);
|
|
2378
|
+
}
|
|
2379
|
+
deleteQueuedPrompt(queueId) {
|
|
2380
|
+
this.db.prepare('DELETE FROM conversation_prompt_queue WHERE queue_id = ?').run(queueId);
|
|
2381
|
+
}
|
|
2382
|
+
markQueueDispatchAttempt(queueId, runId, kind, requestId) {
|
|
2383
|
+
if (queueId == null)
|
|
2384
|
+
return;
|
|
2385
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2386
|
+
SET dispatching_run_id = ?,
|
|
2387
|
+
dispatch_kind = ?,
|
|
2388
|
+
dispatch_request_id = ?,
|
|
2389
|
+
dispatch_sent_at = NULL,
|
|
2390
|
+
dispatch_accepted_at = NULL,
|
|
2391
|
+
updated_at = ?
|
|
2392
|
+
WHERE queue_id = ?`).run(runId, kind, kind === 'steer' ? requestId ?? null : null, Date.now(), queueId);
|
|
2393
|
+
}
|
|
2394
|
+
markQueueDispatchSent(queueId, runId) {
|
|
2395
|
+
if (queueId == null)
|
|
2396
|
+
return;
|
|
2397
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2398
|
+
SET dispatch_sent_at = ?, updated_at = ?
|
|
2399
|
+
WHERE queue_id = ? AND dispatching_run_id = ? AND dispatch_accepted_at IS NULL`).run(Date.now(), Date.now(), queueId, runId);
|
|
2400
|
+
}
|
|
2401
|
+
markQueueDispatchAccepted(runId, options) {
|
|
2402
|
+
const now = Date.now();
|
|
2403
|
+
const kind = options?.kind;
|
|
2404
|
+
if (options?.queueId != null) {
|
|
2405
|
+
const kindPredicate = kind === 'dispatch'
|
|
2406
|
+
? `AND dispatch_kind = 'dispatch'`
|
|
2407
|
+
: kind === 'steer'
|
|
2408
|
+
? `AND dispatch_kind = 'steer'`
|
|
2409
|
+
: '';
|
|
2410
|
+
const result = this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2411
|
+
SET dispatch_accepted_at = ?, updated_at = ?
|
|
2412
|
+
WHERE queue_id = ? AND dispatching_run_id = ?
|
|
2413
|
+
${kindPredicate}`).run(now, now, options.queueId, runId);
|
|
2414
|
+
if (result.changes <= 0)
|
|
2415
|
+
return [];
|
|
2416
|
+
const row = this.db.prepare(`SELECT conversation_id as conversationId
|
|
2417
|
+
FROM conversation_prompt_queue
|
|
2418
|
+
WHERE queue_id = ?`).get(options.queueId);
|
|
2419
|
+
return row ? [row.conversationId] : [];
|
|
2420
|
+
}
|
|
2421
|
+
const kindPredicate = kind === 'dispatch'
|
|
2422
|
+
? `AND dispatch_kind = 'dispatch'`
|
|
2423
|
+
: kind === 'steer'
|
|
2424
|
+
? `AND dispatch_kind = 'steer'`
|
|
2425
|
+
: '';
|
|
2426
|
+
const rows = this.db.prepare(`SELECT DISTINCT conversation_id as conversationId
|
|
2427
|
+
FROM conversation_prompt_queue
|
|
2428
|
+
WHERE dispatching_run_id = ?
|
|
2429
|
+
${kindPredicate}`).all(runId);
|
|
2430
|
+
if (rows.length === 0)
|
|
2431
|
+
return [];
|
|
2432
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2433
|
+
SET dispatch_accepted_at = ?, updated_at = ?
|
|
2434
|
+
WHERE dispatching_run_id = ?
|
|
2435
|
+
${kindPredicate}`).run(now, now, runId);
|
|
2436
|
+
return rows.map((row) => row.conversationId);
|
|
2437
|
+
}
|
|
2438
|
+
clearQueueDispatchAttempt(queueId, runId, requestId) {
|
|
2439
|
+
if (queueId == null)
|
|
2440
|
+
return;
|
|
2441
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2442
|
+
SET dispatching_run_id = NULL,
|
|
2443
|
+
dispatch_kind = NULL,
|
|
2444
|
+
dispatch_request_id = NULL,
|
|
2445
|
+
dispatch_sent_at = NULL,
|
|
2446
|
+
dispatch_accepted_at = NULL,
|
|
2447
|
+
updated_at = ?
|
|
2448
|
+
WHERE queue_id = ?
|
|
2449
|
+
AND dispatching_run_id = ?
|
|
2450
|
+
AND dispatch_accepted_at IS NULL
|
|
2451
|
+
AND (? IS NULL OR dispatch_request_id = ?)`).run(Date.now(), queueId, runId, requestId ?? null, requestId ?? null);
|
|
2452
|
+
}
|
|
2453
|
+
clearQueueSteerAttemptsForRun(runId, conversationId) {
|
|
2454
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2455
|
+
SET dispatching_run_id = NULL,
|
|
2456
|
+
dispatch_kind = NULL,
|
|
2457
|
+
dispatch_request_id = NULL,
|
|
2458
|
+
dispatch_sent_at = NULL,
|
|
2459
|
+
dispatch_accepted_at = NULL,
|
|
2460
|
+
updated_at = ?
|
|
2461
|
+
WHERE conversation_id = ?
|
|
2462
|
+
AND dispatching_run_id = ?
|
|
2463
|
+
AND dispatch_kind = 'steer'
|
|
2464
|
+
AND dispatch_accepted_at IS NULL`).run(Date.now(), conversationId, runId);
|
|
2465
|
+
}
|
|
2466
|
+
markQueueLegacyUnknown(queueId, runId) {
|
|
2467
|
+
if (queueId == null)
|
|
2468
|
+
return;
|
|
2469
|
+
this.db.prepare(`UPDATE conversation_prompt_queue
|
|
2470
|
+
SET dispatch_kind = 'legacy_unknown',
|
|
2471
|
+
dispatch_request_id = NULL,
|
|
2472
|
+
dispatch_sent_at = NULL,
|
|
2473
|
+
dispatch_accepted_at = NULL,
|
|
2474
|
+
updated_at = ?
|
|
2475
|
+
WHERE queue_id = ? AND dispatching_run_id = ? AND dispatch_kind IS NULL`).run(Date.now(), queueId, runId);
|
|
2476
|
+
}
|
|
2477
|
+
waitForDispatchAcceptance(runId, nodeId, conversationId, queuedPromptId) {
|
|
2478
|
+
return new Promise((resolve, reject) => {
|
|
2479
|
+
const timer = setTimeout(() => {
|
|
2480
|
+
this.pendingDispatchAcceptances.delete(runId);
|
|
2481
|
+
reject(new Error('Node did not acknowledge dispatch in time'));
|
|
2482
|
+
}, this.config.nodeDispatchAckTimeoutMs);
|
|
2483
|
+
this.pendingDispatchAcceptances.set(runId, {
|
|
2484
|
+
nodeId,
|
|
2485
|
+
conversationId,
|
|
2486
|
+
queuedPromptId,
|
|
2487
|
+
resolve,
|
|
2488
|
+
reject,
|
|
2489
|
+
timer,
|
|
2490
|
+
});
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
waitForSteerResponse(requestId, nodeId, runId, conversationId, queuedPromptId, timeoutMs = ACTIVE_TURN_STEER_RESPONSE_TIMEOUT_MS) {
|
|
2494
|
+
return new Promise((resolve, reject) => {
|
|
2495
|
+
const timer = timeoutMs == null
|
|
2496
|
+
? undefined
|
|
2497
|
+
: setTimeout(() => {
|
|
2498
|
+
this.pendingSteerResponses.delete(requestId);
|
|
2499
|
+
this.clearQueueDispatchAttempt(queuedPromptId, runId, requestId);
|
|
2500
|
+
this.markSteerResultRequestProcessed(requestId);
|
|
2501
|
+
reject(new Error('Node did not acknowledge active-turn steer in time'));
|
|
2502
|
+
}, timeoutMs);
|
|
2503
|
+
this.pendingSteerResponses.set(requestId, {
|
|
2504
|
+
nodeId,
|
|
2505
|
+
runId,
|
|
2506
|
+
conversationId,
|
|
2507
|
+
queuedPromptId,
|
|
2508
|
+
resolve,
|
|
2509
|
+
reject,
|
|
2510
|
+
...(timer ? { timer } : {}),
|
|
2511
|
+
});
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
clearPendingDispatchAcceptance(runId) {
|
|
2515
|
+
const pending = this.pendingDispatchAcceptances.get(runId);
|
|
2516
|
+
if (!pending)
|
|
2517
|
+
return;
|
|
2518
|
+
clearTimeout(pending.timer);
|
|
2519
|
+
this.pendingDispatchAcceptances.delete(runId);
|
|
2520
|
+
}
|
|
2521
|
+
resolvePendingDispatchAcceptance(runId) {
|
|
2522
|
+
const pending = this.pendingDispatchAcceptances.get(runId);
|
|
2523
|
+
if (!pending)
|
|
2524
|
+
return null;
|
|
2525
|
+
clearTimeout(pending.timer);
|
|
2526
|
+
this.pendingDispatchAcceptances.delete(runId);
|
|
2527
|
+
pending.resolve();
|
|
2528
|
+
return { conversationId: pending.conversationId, queuedPromptId: pending.queuedPromptId };
|
|
2529
|
+
}
|
|
2530
|
+
clearPendingSteerResponse(requestId) {
|
|
2531
|
+
const pending = this.pendingSteerResponses.get(requestId);
|
|
2532
|
+
if (!pending)
|
|
2533
|
+
return;
|
|
2534
|
+
if (pending.timer)
|
|
2535
|
+
clearTimeout(pending.timer);
|
|
2536
|
+
this.pendingSteerResponses.delete(requestId);
|
|
2537
|
+
}
|
|
2538
|
+
async withSteerSerial(runId, task) {
|
|
2539
|
+
const previous = this.steerChains.get(runId) ?? Promise.resolve();
|
|
2540
|
+
const current = previous.catch(() => undefined).then(task);
|
|
2541
|
+
const tracked = current.catch(() => undefined);
|
|
2542
|
+
this.steerChains.set(runId, tracked);
|
|
2543
|
+
try {
|
|
2544
|
+
return await current;
|
|
2545
|
+
}
|
|
2546
|
+
finally {
|
|
2547
|
+
if (this.steerChains.get(runId) === tracked) {
|
|
2548
|
+
this.steerChains.delete(runId);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
resolveChannelBridgeServerUrl(nodeId) {
|
|
2553
|
+
const bridgeUrl = this.config.agentBridgeServerUrl?.trim();
|
|
2554
|
+
if (bridgeUrl) {
|
|
2555
|
+
if (this.isLoopbackBridgeUrl(bridgeUrl) && !this.isLocalBridgeNode(nodeId)) {
|
|
2556
|
+
return this.resolveReachableCoreServerUrl();
|
|
2557
|
+
}
|
|
2558
|
+
return bridgeUrl.replace(/\/+$/, '');
|
|
2559
|
+
}
|
|
2560
|
+
return this.resolveReachableCoreServerUrl();
|
|
2561
|
+
}
|
|
2562
|
+
resolveReachableCoreServerUrl() {
|
|
2563
|
+
const legacyPublicUrl = this.config.publicServerUrl?.trim();
|
|
2564
|
+
if (legacyPublicUrl && !this.config.publicBasePath?.trim()) {
|
|
2565
|
+
return legacyPublicUrl.replace(/\/+$/, '');
|
|
2566
|
+
}
|
|
2567
|
+
const host = this.resolveReachableCoreHost();
|
|
2568
|
+
return `http://${host}:${this.config.webPort}`;
|
|
2569
|
+
}
|
|
2570
|
+
resolveReachableCoreHost() {
|
|
2571
|
+
const configuredHost = this.config.webHost?.trim() || '127.0.0.1';
|
|
2572
|
+
if (configuredHost !== '0.0.0.0' && configuredHost !== '::')
|
|
2573
|
+
return configuredHost;
|
|
2574
|
+
for (const entries of Object.values(os.networkInterfaces())) {
|
|
2575
|
+
for (const entry of entries ?? []) {
|
|
2576
|
+
if (entry.family !== 'IPv4' || entry.internal)
|
|
2577
|
+
continue;
|
|
2578
|
+
if (!entry.address || entry.address === '0.0.0.0')
|
|
2579
|
+
continue;
|
|
2580
|
+
return entry.address;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const hostname = os.hostname().trim();
|
|
2584
|
+
return hostname || '127.0.0.1';
|
|
2585
|
+
}
|
|
2586
|
+
isLocalBridgeNode(nodeId) {
|
|
2587
|
+
if (!nodeId)
|
|
2588
|
+
return false;
|
|
2589
|
+
if (nodeId === 'local-node-1')
|
|
2590
|
+
return true;
|
|
2591
|
+
const node = this.nodeRegistry?.getNode(nodeId);
|
|
2592
|
+
const hostname = node?.hostname?.trim();
|
|
2593
|
+
if (!hostname)
|
|
2594
|
+
return false;
|
|
2595
|
+
return hostname === os.hostname() || hostname === os.hostname().split('.')[0];
|
|
2596
|
+
}
|
|
2597
|
+
isLoopbackBridgeUrl(rawUrl) {
|
|
2598
|
+
try {
|
|
2599
|
+
const parsed = new URL(rawUrl);
|
|
2600
|
+
const host = parsed.hostname.toLowerCase();
|
|
2601
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
2602
|
+
}
|
|
2603
|
+
catch {
|
|
2604
|
+
return false;
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
function parseEnvVars(raw) {
|
|
2609
|
+
if (!raw)
|
|
2610
|
+
return undefined;
|
|
2611
|
+
try {
|
|
2612
|
+
const parsed = JSON.parse(raw);
|
|
2613
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
2614
|
+
return parsed;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
catch {
|
|
2618
|
+
// ignore
|
|
2619
|
+
}
|
|
2620
|
+
return undefined;
|
|
2621
|
+
}
|
|
2622
|
+
function joinContextBlocks(parts) {
|
|
2623
|
+
const visibleParts = parts
|
|
2624
|
+
.map((part) => part?.trim() ?? '')
|
|
2625
|
+
.filter(Boolean);
|
|
2626
|
+
return visibleParts.join('\n\n');
|
|
2627
|
+
}
|
|
2628
|
+
function stripReplyContract(promptText) {
|
|
2629
|
+
const normalized = promptText.trimStart();
|
|
2630
|
+
if (!normalized.startsWith('[Reply contract]'))
|
|
2631
|
+
return promptText;
|
|
2632
|
+
const splitIndex = normalized.indexOf('\n\n');
|
|
2633
|
+
return splitIndex >= 0 ? normalized.slice(splitIndex + 2) : normalized;
|
|
2634
|
+
}
|
|
2635
|
+
function normalizeComparableReplayText(value) {
|
|
2636
|
+
return (value ?? '').replace(/\s+/g, ' ').trim();
|
|
2637
|
+
}
|
|
2638
|
+
function detectTaskHandoffPromptKind(promptText) {
|
|
2639
|
+
if (promptText.includes('[DM Task Thread Handoff]'))
|
|
2640
|
+
return 'dm';
|
|
2641
|
+
if (promptText.includes('[Reminder Task Thread Handoff]'))
|
|
2642
|
+
return 'dm';
|
|
2643
|
+
if (promptText.includes('[Channel Task Thread Handoff]'))
|
|
2644
|
+
return 'channel';
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
function extractTaskLabel(promptText) {
|
|
2648
|
+
const match = /(?:^|\n)Task:\s*(.+)$/m.exec(promptText);
|
|
2649
|
+
return match?.[1]?.trim() || null;
|
|
2650
|
+
}
|
|
2651
|
+
function findFinalRulesSectionStart(promptText) {
|
|
2652
|
+
const idx = promptText.lastIndexOf('\n\nRules:\n');
|
|
2653
|
+
return idx >= 0 ? idx : null;
|
|
2654
|
+
}
|
|
2655
|
+
function findTriggeredMetadataSectionStart(promptText, rulesStart) {
|
|
2656
|
+
const marker = '\n\n[Triggered message metadata]\n';
|
|
2657
|
+
const upperBound = rulesStart ?? promptText.length;
|
|
2658
|
+
let searchFrom = upperBound;
|
|
2659
|
+
while (searchFrom >= 0) {
|
|
2660
|
+
const idx = promptText.lastIndexOf(marker, searchFrom);
|
|
2661
|
+
if (idx < 0)
|
|
2662
|
+
return null;
|
|
2663
|
+
const section = promptText.slice(idx + 2, upperBound).trim();
|
|
2664
|
+
if (/^\[Triggered message metadata\]\n/.test(section)
|
|
2665
|
+
&& /\ntarget: /.test(section)
|
|
2666
|
+
&& /\nsender: /.test(section)
|
|
2667
|
+
&& /\n\n\[Triggered message body\]\n/.test(section)) {
|
|
2668
|
+
return idx;
|
|
2669
|
+
}
|
|
2670
|
+
searchFrom = idx - 1;
|
|
2671
|
+
}
|
|
2672
|
+
return null;
|
|
2673
|
+
}
|
|
2674
|
+
function isExactTaskAttachmentSection(section) {
|
|
2675
|
+
const lines = section.trim().split('\n');
|
|
2676
|
+
if (lines.length < 3)
|
|
2677
|
+
return false;
|
|
2678
|
+
const header = lines[0];
|
|
2679
|
+
if (header !== '[Task attachment]' && header !== '[Task attachments]')
|
|
2680
|
+
return false;
|
|
2681
|
+
const attachmentIds = [];
|
|
2682
|
+
let index = 1;
|
|
2683
|
+
while (index < lines.length && lines[index].startsWith('attachment_id: ')) {
|
|
2684
|
+
const attachmentId = lines[index].slice('attachment_id: '.length).trim();
|
|
2685
|
+
if (!/^[A-Fa-f0-9-]{36}$/.test(attachmentId))
|
|
2686
|
+
return false;
|
|
2687
|
+
attachmentIds.push(attachmentId);
|
|
2688
|
+
index += 1;
|
|
2689
|
+
}
|
|
2690
|
+
if (attachmentIds.length === 0 || index !== lines.length - 1)
|
|
2691
|
+
return false;
|
|
2692
|
+
const attachmentKinds = ['image', 'file'];
|
|
2693
|
+
for (const attachmentKind of attachmentKinds) {
|
|
2694
|
+
const plural = attachmentIds.length > 1 ? 's' : '';
|
|
2695
|
+
const expectedPlaceholderLine = `Use bigbang attachment view --attachment-id "<one of the IDs above>" to inspect the attached ${attachmentKind}${plural} before acting on this task.`;
|
|
2696
|
+
if (lines[index] === expectedPlaceholderLine)
|
|
2697
|
+
return true;
|
|
2698
|
+
const legacyPlaceholderLine = `Use view_file(attachment_id="<one of the IDs above>") to inspect the attached ${attachmentKind}${plural} before acting on this task.`;
|
|
2699
|
+
if (lines[index] === legacyPlaceholderLine)
|
|
2700
|
+
return true;
|
|
2701
|
+
}
|
|
2702
|
+
if (attachmentIds.length !== 1)
|
|
2703
|
+
return false;
|
|
2704
|
+
return attachmentKinds.some((attachmentKind) => (lines[index] === `Use bigbang attachment view --attachment-id "${attachmentIds[0]}" to inspect the attached ${attachmentKind} before acting on this task.`
|
|
2705
|
+
|| lines[index] === `Use view_file(attachment_id="${attachmentIds[0]}") to inspect the attached ${attachmentKind} before acting on this task.`));
|
|
2706
|
+
}
|
|
2707
|
+
function findTaskAttachmentSectionStart(promptText, sectionEnd) {
|
|
2708
|
+
if (sectionEnd == null)
|
|
2709
|
+
return null;
|
|
2710
|
+
const marker = '\n\n[Task attachment';
|
|
2711
|
+
let searchFrom = sectionEnd;
|
|
2712
|
+
while (searchFrom >= 0) {
|
|
2713
|
+
const idx = promptText.lastIndexOf(marker, searchFrom);
|
|
2714
|
+
if (idx < 0)
|
|
2715
|
+
return null;
|
|
2716
|
+
if (idx >= sectionEnd) {
|
|
2717
|
+
searchFrom = idx - 1;
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
const section = promptText.slice(idx + 2, sectionEnd).trim();
|
|
2721
|
+
if (isExactTaskAttachmentSection(section)) {
|
|
2722
|
+
return idx;
|
|
2723
|
+
}
|
|
2724
|
+
searchFrom = idx - 1;
|
|
2725
|
+
}
|
|
2726
|
+
return null;
|
|
2727
|
+
}
|
|
2728
|
+
function extractTaskBriefSection(promptText, kind) {
|
|
2729
|
+
const marker = 'Task brief / goal / done criteria:\n';
|
|
2730
|
+
const start = promptText.indexOf(marker);
|
|
2731
|
+
if (start < 0)
|
|
2732
|
+
return null;
|
|
2733
|
+
const contentStart = start + marker.length;
|
|
2734
|
+
const rulesStart = findFinalRulesSectionStart(promptText);
|
|
2735
|
+
const triggeredStart = kind === 'dm' ? findTriggeredMetadataSectionStart(promptText, rulesStart) : null;
|
|
2736
|
+
const attachmentStart = kind === 'dm'
|
|
2737
|
+
? findTaskAttachmentSectionStart(promptText, triggeredStart ?? rulesStart)
|
|
2738
|
+
: null;
|
|
2739
|
+
const stopCandidates = [
|
|
2740
|
+
attachmentStart,
|
|
2741
|
+
triggeredStart,
|
|
2742
|
+
rulesStart,
|
|
2743
|
+
].filter((idx) => idx != null && idx >= contentStart);
|
|
2744
|
+
const end = stopCandidates.length > 0 ? Math.min(...stopCandidates) : promptText.length;
|
|
2745
|
+
const body = promptText.slice(contentStart, end).trim();
|
|
2746
|
+
return body || null;
|
|
2747
|
+
}
|
|
2748
|
+
function extractTaskAttachmentSection(promptText, kind) {
|
|
2749
|
+
if (kind !== 'dm')
|
|
2750
|
+
return null;
|
|
2751
|
+
const rulesStart = findFinalRulesSectionStart(promptText);
|
|
2752
|
+
const triggeredStart = findTriggeredMetadataSectionStart(promptText, rulesStart);
|
|
2753
|
+
const start = findTaskAttachmentSectionStart(promptText, triggeredStart ?? rulesStart);
|
|
2754
|
+
if (start == null)
|
|
2755
|
+
return null;
|
|
2756
|
+
const sectionEnd = triggeredStart ?? rulesStart ?? promptText.length;
|
|
2757
|
+
const section = promptText.slice(start + 2, sectionEnd).trim();
|
|
2758
|
+
return section || null;
|
|
2759
|
+
}
|
|
2760
|
+
function extractTriggeredMessageBody(promptText) {
|
|
2761
|
+
const marker = '[Triggered message body]\n';
|
|
2762
|
+
const start = promptText.indexOf(marker);
|
|
2763
|
+
if (start < 0)
|
|
2764
|
+
return null;
|
|
2765
|
+
const remainder = promptText.slice(start + marker.length);
|
|
2766
|
+
// DM task handoff prompts append a Rules block after the original trigger text.
|
|
2767
|
+
// Keep ordinary user messages intact even if they literally contain "Rules:".
|
|
2768
|
+
const rulesIndex = detectTaskHandoffPromptKind(promptText) === 'dm'
|
|
2769
|
+
? remainder.lastIndexOf('\n\nRules:\n')
|
|
2770
|
+
: -1;
|
|
2771
|
+
const body = (rulesIndex >= 0 ? remainder.slice(0, rulesIndex) : remainder).trim();
|
|
2772
|
+
return body || null;
|
|
2773
|
+
}
|
|
2774
|
+
function buildTaskHandoffReplayUserText(promptText) {
|
|
2775
|
+
const kind = detectTaskHandoffPromptKind(promptText);
|
|
2776
|
+
if (!kind)
|
|
2777
|
+
return null;
|
|
2778
|
+
const triggerBody = extractTriggeredMessageBody(promptText);
|
|
2779
|
+
const taskBrief = extractTaskBriefSection(promptText, kind);
|
|
2780
|
+
const attachmentSection = extractTaskAttachmentSection(promptText, kind);
|
|
2781
|
+
const taskLabel = extractTaskLabel(promptText);
|
|
2782
|
+
if (!taskBrief && !attachmentSection && !taskLabel)
|
|
2783
|
+
return triggerBody;
|
|
2784
|
+
const sameBriefAsTrigger = taskBrief
|
|
2785
|
+
&& triggerBody
|
|
2786
|
+
&& normalizeComparableReplayText(taskBrief) === normalizeComparableReplayText(triggerBody);
|
|
2787
|
+
if (sameBriefAsTrigger) {
|
|
2788
|
+
return attachmentSection
|
|
2789
|
+
? [triggerBody, attachmentSection].filter(Boolean).join('\n\n')
|
|
2790
|
+
: triggerBody;
|
|
2791
|
+
}
|
|
2792
|
+
const parts = [];
|
|
2793
|
+
if (!taskBrief && !triggerBody && taskLabel) {
|
|
2794
|
+
parts.push(`Task bootstrap: ${taskLabel}`);
|
|
2795
|
+
}
|
|
2796
|
+
if (taskBrief)
|
|
2797
|
+
parts.push(`Task brief / goal / done criteria:\n${taskBrief}`);
|
|
2798
|
+
if (attachmentSection)
|
|
2799
|
+
parts.push(attachmentSection);
|
|
2800
|
+
if (triggerBody)
|
|
2801
|
+
parts.push(`Original trigger:\n${triggerBody}`);
|
|
2802
|
+
return parts.length > 0 ? parts.join('\n\n') : null;
|
|
2803
|
+
}
|
|
2804
|
+
function normalizeReplayUserText(promptText) {
|
|
2805
|
+
const stripped = stripReplyContract(promptText).trim();
|
|
2806
|
+
return buildTaskHandoffReplayUserText(stripped) ?? extractTriggeredMessageBody(stripped) ?? stripped;
|
|
2807
|
+
}
|
|
2808
|
+
function getConversationAgentName(db, conversationId) {
|
|
2809
|
+
const row = db.prepare(`SELECT a.name as agentName
|
|
2810
|
+
FROM conversations c
|
|
2811
|
+
LEFT JOIN agents a ON a.agent_id = c.agent_id
|
|
2812
|
+
WHERE c.id = ?`).get(conversationId);
|
|
2813
|
+
return row?.agentName?.trim() || null;
|
|
2814
|
+
}
|
|
2815
|
+
function trimReplayBlocksAgainstRecentMessages(replayBlocks, recentMessages) {
|
|
2816
|
+
if (!recentMessages?.length || replayBlocks.length === 0)
|
|
2817
|
+
return replayBlocks;
|
|
2818
|
+
const recentReplayBlocks = recentMessages
|
|
2819
|
+
.map(formatReplayBlockForRecentMessage)
|
|
2820
|
+
.filter((block) => Boolean(block));
|
|
2821
|
+
if (recentReplayBlocks.length === 0)
|
|
2822
|
+
return replayBlocks;
|
|
2823
|
+
let replayIndex = replayBlocks.length - 1;
|
|
2824
|
+
let recentIndex = recentReplayBlocks.length - 1;
|
|
2825
|
+
let matchedCount = 0;
|
|
2826
|
+
while (replayIndex >= 0 && recentIndex >= 0) {
|
|
2827
|
+
const replayBlock = replayBlocks[replayIndex];
|
|
2828
|
+
const recentBlock = recentReplayBlocks[recentIndex];
|
|
2829
|
+
if (replayBlock.speaker === recentBlock.speaker
|
|
2830
|
+
&& replayBlock.comparableText === recentBlock.comparableText) {
|
|
2831
|
+
matchedCount += 1;
|
|
2832
|
+
replayIndex -= 1;
|
|
2833
|
+
recentIndex -= 1;
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
recentIndex -= 1;
|
|
2837
|
+
}
|
|
2838
|
+
if (matchedCount <= 0)
|
|
2839
|
+
return replayBlocks;
|
|
2840
|
+
return replayBlocks.slice(0, replayBlocks.length - matchedCount);
|
|
2841
|
+
}
|
|
2842
|
+
function formatReplayBlockForRecentMessage(message) {
|
|
2843
|
+
const content = sanitizePromptHistoryContent(message.content, message.senderType);
|
|
2844
|
+
if (!content)
|
|
2845
|
+
return null;
|
|
2846
|
+
return {
|
|
2847
|
+
speaker: message.senderType === 'agent' ? 'agent' : 'user',
|
|
2848
|
+
comparableText: normalizeComparableReplayText(content),
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
function serializeReplayOverlapRecentMessages(messages) {
|
|
2852
|
+
if (!messages?.length)
|
|
2853
|
+
return null;
|
|
2854
|
+
return JSON.stringify(messages);
|
|
2855
|
+
}
|
|
2856
|
+
function parseReplayOverlapRecentMessages(raw) {
|
|
2857
|
+
if (!raw?.trim())
|
|
2858
|
+
return undefined;
|
|
2859
|
+
try {
|
|
2860
|
+
const parsed = JSON.parse(raw);
|
|
2861
|
+
if (!Array.isArray(parsed))
|
|
2862
|
+
return undefined;
|
|
2863
|
+
return parsed.filter(isActivationContextMessage);
|
|
2864
|
+
}
|
|
2865
|
+
catch {
|
|
2866
|
+
return undefined;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
function isActivationContextMessage(value) {
|
|
2870
|
+
if (!value || typeof value !== 'object')
|
|
2871
|
+
return false;
|
|
2872
|
+
const row = value;
|
|
2873
|
+
return typeof row.messageId === 'string'
|
|
2874
|
+
&& typeof row.seq === 'number'
|
|
2875
|
+
&& typeof row.target === 'string'
|
|
2876
|
+
&& typeof row.senderName === 'string'
|
|
2877
|
+
&& (row.senderType === 'user' || row.senderType === 'agent')
|
|
2878
|
+
&& typeof row.content === 'string'
|
|
2879
|
+
&& typeof row.createdAt === 'number';
|
|
2880
|
+
}
|
|
2881
|
+
function createReplayRenderableBlock(params) {
|
|
2882
|
+
const text = params.text.trim();
|
|
2883
|
+
if (!text)
|
|
2884
|
+
return null;
|
|
2885
|
+
return {
|
|
2886
|
+
speaker: params.speaker,
|
|
2887
|
+
comparableText: normalizeComparableReplayText(text),
|
|
2888
|
+
renderLabel: params.renderLabel,
|
|
2889
|
+
text,
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
function renderReplayBlock(block) {
|
|
2893
|
+
return `${block.renderLabel}: ${block.text}`;
|
|
2894
|
+
}
|
|
2895
|
+
function composePromptContextTexts(params) {
|
|
2896
|
+
const dedupedContextSections = dedupePromptContextSections(params.contextSections);
|
|
2897
|
+
const contextSeenText = new Set(dedupedContextSections.map((section) => section.text));
|
|
2898
|
+
const dedupedResumeSections = [];
|
|
2899
|
+
const resumeSeenText = new Set();
|
|
2900
|
+
for (const section of normalizePromptContextSections(params.resumeSections)) {
|
|
2901
|
+
if (resumeSeenText.has(section.text))
|
|
2902
|
+
continue;
|
|
2903
|
+
if (contextSeenText.has(section.text))
|
|
2904
|
+
continue;
|
|
2905
|
+
if (section.kind === 'active_project'
|
|
2906
|
+
&& dedupedContextSections.some((contextSection) => contextSection.kind === 'active_project')) {
|
|
2907
|
+
continue;
|
|
2908
|
+
}
|
|
2909
|
+
resumeSeenText.add(section.text);
|
|
2910
|
+
dedupedResumeSections.push(section);
|
|
2911
|
+
}
|
|
2912
|
+
return {
|
|
2913
|
+
contextText: renderPromptContextSections(dedupedContextSections),
|
|
2914
|
+
resumeContextText: renderPromptContextSections(dedupedResumeSections),
|
|
2915
|
+
hasVisibleRecentWindow: [...dedupedContextSections, ...dedupedResumeSections]
|
|
2916
|
+
.some((section) => EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind)),
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function stripExactTargetHistorySections(sections) {
|
|
2920
|
+
if (!sections?.length)
|
|
2921
|
+
return undefined;
|
|
2922
|
+
const stripped = sections.filter((section) => !EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind));
|
|
2923
|
+
return stripped.length > 0 ? stripped : undefined;
|
|
2924
|
+
}
|
|
2925
|
+
function extractExactTargetHistorySections(sections) {
|
|
2926
|
+
return (sections ?? []).filter((section) => EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind));
|
|
2927
|
+
}
|
|
2928
|
+
function splitExactTargetHistorySections(sections) {
|
|
2929
|
+
const exact = [];
|
|
2930
|
+
const nonExact = [];
|
|
2931
|
+
for (const section of sections ?? []) {
|
|
2932
|
+
if (EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind)) {
|
|
2933
|
+
exact.push(section);
|
|
2934
|
+
}
|
|
2935
|
+
else {
|
|
2936
|
+
nonExact.push(section);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
return { exact, nonExact };
|
|
2940
|
+
}
|
|
2941
|
+
function shouldKeepResumeOnlyActivationSectionLive(section) {
|
|
2942
|
+
if (EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind))
|
|
2943
|
+
return false;
|
|
2944
|
+
return ![
|
|
2945
|
+
'workspace_memory_reminder',
|
|
2946
|
+
'bound_task_dm_thread',
|
|
2947
|
+
'dm_context_snapshot',
|
|
2948
|
+
'dm_active_task_threads',
|
|
2949
|
+
'dm_active_task_thread_hint',
|
|
2950
|
+
'continuity',
|
|
2951
|
+
].includes(section.kind);
|
|
2952
|
+
}
|
|
2953
|
+
function splitResumeOnlyActivationSections(sections, options) {
|
|
2954
|
+
const live = [];
|
|
2955
|
+
const resume = [];
|
|
2956
|
+
for (const section of sections ?? []) {
|
|
2957
|
+
if (shouldKeepResumeOnlyActivationSectionLive(section)
|
|
2958
|
+
|| (options?.includeExactTargetHistory === true && EXACT_TARGET_HISTORY_SECTION_KINDS.has(section.kind))) {
|
|
2959
|
+
live.push(section);
|
|
2960
|
+
}
|
|
2961
|
+
else {
|
|
2962
|
+
resume.push(section);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return { live, resume };
|
|
2966
|
+
}
|
|
2967
|
+
function stripSurfaceSystemStatusSections(sections) {
|
|
2968
|
+
if (!sections?.length)
|
|
2969
|
+
return undefined;
|
|
2970
|
+
const stripped = sections.filter((section) => section.kind !== 'surface_system_status');
|
|
2971
|
+
return stripped.length > 0 ? stripped : undefined;
|
|
2972
|
+
}
|
|
2973
|
+
function stripWorkspaceMemoryReminderSections(sections, suppressWorkspaceMemoryReminder) {
|
|
2974
|
+
if (!sections?.length)
|
|
2975
|
+
return undefined;
|
|
2976
|
+
if (!suppressWorkspaceMemoryReminder)
|
|
2977
|
+
return sections;
|
|
2978
|
+
const stripped = sections.filter((section) => section.kind !== 'workspace_memory_reminder');
|
|
2979
|
+
return stripped.length > 0 ? stripped : undefined;
|
|
2980
|
+
}
|
|
2981
|
+
function shouldKeepIncomingPromptContextSection(section, options) {
|
|
2982
|
+
if (section.kind === 'surface_system_status')
|
|
2983
|
+
return false;
|
|
2984
|
+
if (section.kind === 'reminder_guidance'
|
|
2985
|
+
|| section.kind === 'ui_panel_guidance'
|
|
2986
|
+
|| section.kind === 'workspace_tool_guidance'
|
|
2987
|
+
|| section.kind === 'library_document_guidance') {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
if (options?.suppressWorkspaceMemoryReminder === true && section.kind === 'workspace_memory_reminder')
|
|
2991
|
+
return false;
|
|
2992
|
+
return true;
|
|
2993
|
+
}
|
|
2994
|
+
function stripSurfaceSystemStatusText(text) {
|
|
2995
|
+
const normalizedText = text?.trim();
|
|
2996
|
+
if (!normalizedText)
|
|
2997
|
+
return undefined;
|
|
2998
|
+
const firstLine = normalizedText.split('\n', 1)[0]?.trim();
|
|
2999
|
+
if (firstLine === '[Recent messages on this exact target]'
|
|
3000
|
+
|| firstLine === '[History cursor]'
|
|
3001
|
+
|| firstLine === '[Recent summary]'
|
|
3002
|
+
|| firstLine === '[Unread summary]'
|
|
3003
|
+
|| firstLine === '[Inbox]') {
|
|
3004
|
+
return normalizedText;
|
|
3005
|
+
}
|
|
3006
|
+
return normalizedText
|
|
3007
|
+
.split(/\n{2,}/u)
|
|
3008
|
+
.map((part) => part.trim())
|
|
3009
|
+
.filter((part) => part && part.split('\n', 1)[0]?.trim() !== '[System status messages on this surface]')
|
|
3010
|
+
.join('\n\n')
|
|
3011
|
+
.trim() || undefined;
|
|
3012
|
+
}
|
|
3013
|
+
function normalizePromptContextSections(sections) {
|
|
3014
|
+
return sections
|
|
3015
|
+
.map((section) => {
|
|
3016
|
+
const normalizedText = section.text.trim();
|
|
3017
|
+
return normalizedText
|
|
3018
|
+
? {
|
|
3019
|
+
kind: section.kind,
|
|
3020
|
+
text: normalizedText,
|
|
3021
|
+
}
|
|
3022
|
+
: null;
|
|
3023
|
+
})
|
|
3024
|
+
.filter((section) => Boolean(section));
|
|
3025
|
+
}
|
|
3026
|
+
function dedupePromptContextSections(sections) {
|
|
3027
|
+
const dedupedSections = [];
|
|
3028
|
+
const seenTexts = new Set();
|
|
3029
|
+
for (const section of normalizePromptContextSections(sections)) {
|
|
3030
|
+
if (seenTexts.has(section.text))
|
|
3031
|
+
continue;
|
|
3032
|
+
seenTexts.add(section.text);
|
|
3033
|
+
dedupedSections.push(section);
|
|
3034
|
+
}
|
|
3035
|
+
return dedupedSections;
|
|
3036
|
+
}
|
|
3037
|
+
function escapeMcpHintString(value) {
|
|
3038
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
3039
|
+
}
|
|
3040
|
+
export function adaptPromptTextForAgentSurface(promptText, agentSurfaceMode) {
|
|
3041
|
+
if (agentSurfaceMode !== 'mcp')
|
|
3042
|
+
return promptText;
|
|
3043
|
+
return promptText
|
|
3044
|
+
.replace(/bigbang message read --channel "([^"]+)" --around "([^"]+)" --limit (\d+) --include-root/g, 'read_history(channel="$1", around="$2", limit=$3, include_root=true)')
|
|
3045
|
+
.replace(/bigbang message read --channel "([^"]+)" --around "([^"]+)" --limit (\d+)/g, 'read_history(channel="$1", around="$2", limit=$3)')
|
|
3046
|
+
.replace(/bigbang message read --channel "([^"]+)" --around (\d+) --limit (\d+) --include-root/g, 'read_history(channel="$1", around=$2, limit=$3, include_root=true)')
|
|
3047
|
+
.replace(/bigbang message read --channel "([^"]+)" --around (\d+) --limit (\d+)/g, 'read_history(channel="$1", around=$2, limit=$3)')
|
|
3048
|
+
.replace(/bigbang message read --channel "([^"]+)" --after (\d+) --limit (\d+)/g, 'read_history(channel="$1", after=$2, limit=$3)')
|
|
3049
|
+
.replace(/bigbang message read --channel "([^"]+)" --before (\d+) --limit (\d+)/g, 'read_history(channel="$1", before=$2, limit=$3)')
|
|
3050
|
+
.replace(/bigbang message read --channel "([^"]+)" --around "([^"]+)"/g, 'read_history(channel="$1", around="$2")')
|
|
3051
|
+
.replace(/bigbang message read --channel "([^"]+)" --around (\d+)/g, 'read_history(channel="$1", around=$2)')
|
|
3052
|
+
.replace(/bigbang message read --channel "([^"]+)" --after (\d+)/g, 'read_history(channel="$1", after=$2)')
|
|
3053
|
+
.replace(/bigbang message read --channel "([^"]+)" --before (\d+)/g, 'read_history(channel="$1", before=$2)')
|
|
3054
|
+
.replace(/bigbang attachment view --attachment-id "([^"]+)"/g, 'view_file(attachment_id="$1")')
|
|
3055
|
+
.replace(/bigbang handoff create <<'EOF'\s*\n(\{[^\n]+\})\nEOF/g, (_match, payload) => {
|
|
3056
|
+
try {
|
|
3057
|
+
const parsed = JSON.parse(payload);
|
|
3058
|
+
const target = typeof parsed.target === 'string' ? parsed.target : '';
|
|
3059
|
+
const mode = parsed.mode === 'delegate_only' || parsed.mode === 'continue_there' || parsed.mode === 'collab'
|
|
3060
|
+
? parsed.mode
|
|
3061
|
+
: 'delegate_only';
|
|
3062
|
+
const goal = typeof parsed.goal === 'string' && parsed.goal.trim().length > 0
|
|
3063
|
+
? parsed.goal
|
|
3064
|
+
: `Continue this work on ${target}.`;
|
|
3065
|
+
if (!target)
|
|
3066
|
+
return _match;
|
|
3067
|
+
return `handoff_to_target(target="${escapeMcpHintString(target)}", mode="${mode}", goal="${escapeMcpHintString(goal)}")`;
|
|
3068
|
+
}
|
|
3069
|
+
catch {
|
|
3070
|
+
return _match;
|
|
3071
|
+
}
|
|
3072
|
+
})
|
|
3073
|
+
.replace(/bigbang handoff create --target "([^"]+)"/g, (_match, target) => (`handoff_to_target(target="${escapeMcpHintString(target)}", mode="collab", goal="${escapeMcpHintString(`Continue this work on ${target}.`)}")`))
|
|
3074
|
+
.replace(/bigbang task update --channel "([^"]+)" --number (\d+) --status ([a-z_]+)/g, 'update_task_status(channel="$1", task_number=$2, status="$3")')
|
|
3075
|
+
.replace(/bigbang panel patch --panel-id ([^\s.,)]+)/g, 'patch_panel(panel_id=$1)')
|
|
3076
|
+
.replace(/bigbang panel read-rows --panel-id ([^\s.,)]+)/g, 'read_panel_rows(panel_id=$1)')
|
|
3077
|
+
.replace(/`bigbang message send --kind progress`/g, '`mcp__chat__send_message(content="...", kind="progress")`')
|
|
3078
|
+
.replace(/`bigbang message send --kind final`/g, '`mcp__chat__send_message(content="...", kind="final")`')
|
|
3079
|
+
.replace(/`bigbang message send`/g, '`mcp__chat__send_message`')
|
|
3080
|
+
.replace(/\bbigbang inbox check\b/g, 'check_messages()')
|
|
3081
|
+
.replace(/\bbigbang message check\b/g, 'check_messages()')
|
|
3082
|
+
.replace(/\bbigbang message read\b/g, 'read_history')
|
|
3083
|
+
.replace(/\bbigbang task list\b/g, 'list_tasks()')
|
|
3084
|
+
.replace(/\bbigbang task my\b/g, 'list_my_tasks()')
|
|
3085
|
+
.replace(/\bbigbang task claim\b/g, 'claim_tasks(...)')
|
|
3086
|
+
.replace(/\bbigbang task create\b/g, 'create_tasks(...)')
|
|
3087
|
+
.replace(/\bbigbang task update\b/g, 'update_task_status(...)')
|
|
3088
|
+
.replace(/\bbigbang task history\b/g, 'get_task_history(...)')
|
|
3089
|
+
.replace(/\bbigbang context bundle --format json\b/g, 'mcp__chat__get_relevant_context_bundle()')
|
|
3090
|
+
.replace(/\bbigbang context bundle\b/g, 'get_relevant_context_bundle')
|
|
3091
|
+
.replace(/\bbigbang conversation list\b/g, 'list_my_conversations()')
|
|
3092
|
+
.replace(/\bbigbang conversation summary\b/g, 'get_conversation_summary')
|
|
3093
|
+
.replace(/\bbigbang self state\b/g, 'get_self_state')
|
|
3094
|
+
.replace(/\bbigbang runtime presence\b/g, 'get_runtime_presence')
|
|
3095
|
+
.replace(/\bbigbang workspace inspect\b/g, 'inspect_workspace')
|
|
3096
|
+
.replace(/\bbigbang tool publish\b/g, 'publish_workspace_tool')
|
|
3097
|
+
.replace(/\bbigbang panel components\b/g, 'list_ui_components')
|
|
3098
|
+
.replace(/\bbigbang panel create\b/g, 'render_panel')
|
|
3099
|
+
.replace(/\bbigbang panel patch\b/g, 'patch_panel')
|
|
3100
|
+
.replace(/\bbigbang panel upsert\b/g, 'upsert_panel(handle=...)')
|
|
3101
|
+
.replace(/\bbigbang panel read-rows\b/g, 'read_panel_rows')
|
|
3102
|
+
.replace(/\bbigbang reminder submit-for-review\b/g, 'submit_reminder_occurrence_for_review')
|
|
3103
|
+
.replace(/Bigbang CLI commands/g, 'MCP tools')
|
|
3104
|
+
.replace(/Bigbang commands/g, 'MCP tools')
|
|
3105
|
+
.replace(/Bigbang/g, 'MCP');
|
|
3106
|
+
}
|
|
3107
|
+
function prependTurnReplyContract(promptText, today = getCurrentISOWeek(), agentSurfaceMode = 'bigbang') {
|
|
3108
|
+
if (promptText.trimStart().startsWith('[Reply contract]'))
|
|
3109
|
+
return promptText;
|
|
3110
|
+
return `${buildTurnReplyContract(today, agentSurfaceMode)}\n\n${promptText}`;
|
|
3111
|
+
}
|
|
3112
|
+
function normalizeDispatchAgentSurfaceMode(value, fallback) {
|
|
3113
|
+
if (value === 'bigbang' || value === 'mcp')
|
|
3114
|
+
return value;
|
|
3115
|
+
return fallback;
|
|
3116
|
+
}
|
|
3117
|
+
function joinSteerPromptParts(...parts) {
|
|
3118
|
+
return parts
|
|
3119
|
+
.map((part) => part?.trim())
|
|
3120
|
+
.filter((part) => Boolean(part))
|
|
3121
|
+
.join('\n\n');
|
|
3122
|
+
}
|
|
3123
|
+
function buildMinimalDirectFollowupSteerPrompt(promptText) {
|
|
3124
|
+
return `[Triggered message body]\n${promptText}`;
|
|
3125
|
+
}
|
|
3126
|
+
function getSessionSystemPromptText(db, sessionKey) {
|
|
3127
|
+
const row = db.prepare('SELECT system_prompt_text as systemPromptText FROM sessions WHERE session_key = ?').get(sessionKey);
|
|
3128
|
+
return row?.systemPromptText ?? null;
|
|
3129
|
+
}
|
|
3130
|
+
function getSessionAcpSessionId(db, sessionKey) {
|
|
3131
|
+
const row = db.prepare('SELECT acp_session_id as acpSessionId FROM sessions WHERE session_key = ?').get(sessionKey);
|
|
3132
|
+
return row?.acpSessionId?.trim() || null;
|
|
3133
|
+
}
|
|
3134
|
+
function upsertPendingRunDebugInput(db, params) {
|
|
3135
|
+
const now = Date.now();
|
|
3136
|
+
db.prepare(`INSERT INTO run_debug_inputs(
|
|
3137
|
+
run_id, conversation_id, session_key, dispatch_mode, reply_target,
|
|
3138
|
+
system_prompt_text, context_text, prompt_text, dispatched_prompt_text, agent_surface_mode, activation_metadata_json,
|
|
3139
|
+
created_at, updated_at
|
|
3140
|
+
)
|
|
3141
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3142
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
3143
|
+
conversation_id = excluded.conversation_id,
|
|
3144
|
+
session_key = excluded.session_key,
|
|
3145
|
+
dispatch_mode = excluded.dispatch_mode,
|
|
3146
|
+
reply_target = excluded.reply_target,
|
|
3147
|
+
system_prompt_text = excluded.system_prompt_text,
|
|
3148
|
+
context_text = excluded.context_text,
|
|
3149
|
+
prompt_text = excluded.prompt_text,
|
|
3150
|
+
dispatched_prompt_text = excluded.dispatched_prompt_text,
|
|
3151
|
+
agent_surface_mode = excluded.agent_surface_mode,
|
|
3152
|
+
activation_metadata_json = excluded.activation_metadata_json,
|
|
3153
|
+
updated_at = excluded.updated_at`).run(params.runId, params.conversationId, params.sessionKey, params.dispatchMode, params.replyTarget, params.systemPromptText, params.contextText, params.promptText, params.dispatchedPromptText, params.agentSurfaceMode, params.activationMetadataJson, now, now);
|
|
3154
|
+
}
|
|
3155
|
+
function patchRunDebugInputForActiveSteer(db, params) {
|
|
3156
|
+
const previous = db.prepare(`SELECT prompt_text as promptText,
|
|
3157
|
+
dispatched_prompt_text as dispatchedPromptText,
|
|
3158
|
+
activation_metadata_json as activationMetadataJson
|
|
3159
|
+
FROM run_debug_inputs
|
|
3160
|
+
WHERE run_id = ?
|
|
3161
|
+
LIMIT 1`).get(params.runId);
|
|
3162
|
+
if (!previous)
|
|
3163
|
+
return null;
|
|
3164
|
+
const activationMetadataJson = serializePromptActivationMetadata(params.activationMetadata);
|
|
3165
|
+
db.prepare(`UPDATE run_debug_inputs
|
|
3166
|
+
SET prompt_text = ?,
|
|
3167
|
+
dispatched_prompt_text = ?,
|
|
3168
|
+
activation_metadata_json = ?,
|
|
3169
|
+
updated_at = ?
|
|
3170
|
+
WHERE run_id = ?`).run(params.promptText, params.dispatchedPromptText, activationMetadataJson, Date.now(), params.runId);
|
|
3171
|
+
return () => {
|
|
3172
|
+
db.prepare(`UPDATE run_debug_inputs
|
|
3173
|
+
SET prompt_text = ?,
|
|
3174
|
+
dispatched_prompt_text = ?,
|
|
3175
|
+
activation_metadata_json = ?,
|
|
3176
|
+
updated_at = ?
|
|
3177
|
+
WHERE run_id = ?`).run(previous.promptText, previous.dispatchedPromptText, previous.activationMetadataJson, Date.now(), params.runId);
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
function sanitizeFileRefs(value) {
|
|
3181
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
3182
|
+
return [];
|
|
3183
|
+
const refs = [];
|
|
3184
|
+
const seen = new Set();
|
|
3185
|
+
for (const item of value) {
|
|
3186
|
+
const uri = typeof item?.uri === 'string'
|
|
3187
|
+
? item.uri.trim()
|
|
3188
|
+
: '';
|
|
3189
|
+
if (!uri || seen.has(uri))
|
|
3190
|
+
continue;
|
|
3191
|
+
seen.add(uri);
|
|
3192
|
+
const mimeType = typeof item?.mimeType === 'string'
|
|
3193
|
+
? item.mimeType.trim()
|
|
3194
|
+
: '';
|
|
3195
|
+
refs.push({
|
|
3196
|
+
uri,
|
|
3197
|
+
...(mimeType ? { mimeType } : {}),
|
|
3198
|
+
});
|
|
3199
|
+
}
|
|
3200
|
+
return refs;
|
|
3201
|
+
}
|
|
3202
|
+
function sanitizeAttachmentIds(value) {
|
|
3203
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
3204
|
+
return [];
|
|
3205
|
+
const ids = [];
|
|
3206
|
+
const seen = new Set();
|
|
3207
|
+
for (const item of value) {
|
|
3208
|
+
const raw = typeof item === 'string' ? item.trim() : '';
|
|
3209
|
+
const id = raw.startsWith('attachment:') ? raw.slice('attachment:'.length).trim() : raw;
|
|
3210
|
+
if (!/^[a-f0-9-]{36}$/i.test(id))
|
|
3211
|
+
continue;
|
|
3212
|
+
const normalized = id.toLowerCase();
|
|
3213
|
+
if (seen.has(normalized))
|
|
3214
|
+
continue;
|
|
3215
|
+
seen.add(normalized);
|
|
3216
|
+
ids.push(normalized);
|
|
3217
|
+
}
|
|
3218
|
+
return ids;
|
|
3219
|
+
}
|
|
3220
|
+
function parseAttachmentIdsJson(value) {
|
|
3221
|
+
if (!value)
|
|
3222
|
+
return [];
|
|
3223
|
+
try {
|
|
3224
|
+
return sanitizeAttachmentIds(JSON.parse(value));
|
|
3225
|
+
}
|
|
3226
|
+
catch {
|
|
3227
|
+
return [];
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
function sanitizePanelIds(value) {
|
|
3231
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
3232
|
+
return [];
|
|
3233
|
+
const ids = [];
|
|
3234
|
+
const seen = new Set();
|
|
3235
|
+
for (const item of value) {
|
|
3236
|
+
const id = typeof item === 'string' ? item.trim() : '';
|
|
3237
|
+
if (!id || seen.has(id))
|
|
3238
|
+
continue;
|
|
3239
|
+
seen.add(id);
|
|
3240
|
+
ids.push(id);
|
|
3241
|
+
}
|
|
3242
|
+
return ids;
|
|
3243
|
+
}
|
|
3244
|
+
function parsePanelIdsJson(value) {
|
|
3245
|
+
if (!value)
|
|
3246
|
+
return [];
|
|
3247
|
+
try {
|
|
3248
|
+
return sanitizePanelIds(JSON.parse(value));
|
|
3249
|
+
}
|
|
3250
|
+
catch {
|
|
3251
|
+
return [];
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
function stringArraysEqual(left, right) {
|
|
3255
|
+
return left.length === right.length && left.every((item, index) => item === right[index]);
|
|
3256
|
+
}
|
|
3257
|
+
function extractPromptAttachmentIds(_promptText, attachments, explicitAttachmentIds = []) {
|
|
3258
|
+
const ids = new Set();
|
|
3259
|
+
for (const id of explicitAttachmentIds) {
|
|
3260
|
+
ids.add(id);
|
|
3261
|
+
}
|
|
3262
|
+
for (const attachment of attachments) {
|
|
3263
|
+
if (!attachment.uri.startsWith('attachment:'))
|
|
3264
|
+
continue;
|
|
3265
|
+
const id = attachment.uri.slice('attachment:'.length).trim();
|
|
3266
|
+
if (/^[a-f0-9-]{36}$/i.test(id)) {
|
|
3267
|
+
ids.add(id.toLowerCase());
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
return [...ids];
|
|
3271
|
+
}
|
|
3272
|
+
function enrichPromptActivationMetadataWithContextSections(options, contextSectionsOverride) {
|
|
3273
|
+
const activationMetadata = options?.activationMetadata
|
|
3274
|
+
? { ...options.activationMetadata }
|
|
3275
|
+
: undefined;
|
|
3276
|
+
if (contextSectionsOverride && activationMetadata) {
|
|
3277
|
+
delete activationMetadata.promptContextSections;
|
|
3278
|
+
}
|
|
3279
|
+
const attachments = sanitizeFileRefs(options?.attachments);
|
|
3280
|
+
const attachmentIds = sanitizeAttachmentIds(options?.attachmentIds);
|
|
3281
|
+
const runtimeOverrides = normalizePromptRuntimeOverrides(options?.runtimeOverrides ?? activationMetadata?.runtimeOverrides);
|
|
3282
|
+
const slashCommand = normalizePromptSlashCommand(activationMetadata?.slashCommand);
|
|
3283
|
+
const activationSections = contextSectionsOverride
|
|
3284
|
+
? contextSectionsOverride.activation
|
|
3285
|
+
: options?.activationContextSections;
|
|
3286
|
+
const resumeSections = contextSectionsOverride
|
|
3287
|
+
? contextSectionsOverride.resume
|
|
3288
|
+
: options?.resumeContextSections;
|
|
3289
|
+
if (!activationSections?.length && !resumeSections?.length && attachments.length === 0 && attachmentIds.length === 0 && !runtimeOverrides && !slashCommand && options?.suppressReplyContract !== true) {
|
|
3290
|
+
return activationMetadata;
|
|
3291
|
+
}
|
|
3292
|
+
return {
|
|
3293
|
+
...(activationMetadata ?? {}),
|
|
3294
|
+
...(options?.suppressReplyContract === true ? { suppressReplyContract: true } : {}),
|
|
3295
|
+
...(runtimeOverrides ? { runtimeOverrides } : {}),
|
|
3296
|
+
...(slashCommand ? { slashCommand } : {}),
|
|
3297
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
3298
|
+
...(attachmentIds.length > 0 ? { attachmentIds } : {}),
|
|
3299
|
+
...(activationSections?.length || resumeSections?.length
|
|
3300
|
+
? {
|
|
3301
|
+
promptContextSections: {
|
|
3302
|
+
...(activationSections?.length ? { activation: activationSections } : {}),
|
|
3303
|
+
...(resumeSections?.length ? { resume: resumeSections } : {}),
|
|
3304
|
+
},
|
|
3305
|
+
}
|
|
3306
|
+
: {}),
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
function serializePromptActivationMetadata(metadata) {
|
|
3310
|
+
if (!metadata)
|
|
3311
|
+
return null;
|
|
3312
|
+
const payload = {};
|
|
3313
|
+
const platformRun = normalizePlatformRunMetadata(metadata.platformRun);
|
|
3314
|
+
if (platformRun) {
|
|
3315
|
+
payload.platformRun = platformRun;
|
|
3316
|
+
}
|
|
3317
|
+
if (metadata.mentionSuppression) {
|
|
3318
|
+
const peerMentionedAgentIds = metadata.mentionSuppression.peerMentionedAgentIds
|
|
3319
|
+
.map((agentId) => agentId.trim())
|
|
3320
|
+
.filter(Boolean);
|
|
3321
|
+
if (peerMentionedAgentIds.length > 0) {
|
|
3322
|
+
payload.mentionSuppression = {
|
|
3323
|
+
mode: metadata.mentionSuppression.mode,
|
|
3324
|
+
triggerSeq: metadata.mentionSuppression.triggerSeq,
|
|
3325
|
+
peerMentionedAgentIds,
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
if (metadata.triggerMessage
|
|
3330
|
+
&& typeof metadata.triggerMessage.messageId === 'string'
|
|
3331
|
+
&& metadata.triggerMessage.messageId.trim()
|
|
3332
|
+
&& Number.isFinite(metadata.triggerMessage.seq)
|
|
3333
|
+
&& typeof metadata.triggerMessage.target === 'string'
|
|
3334
|
+
&& metadata.triggerMessage.target.trim()) {
|
|
3335
|
+
payload.triggerMessage = {
|
|
3336
|
+
messageId: metadata.triggerMessage.messageId.trim(),
|
|
3337
|
+
seq: metadata.triggerMessage.seq,
|
|
3338
|
+
target: metadata.triggerMessage.target.trim(),
|
|
3339
|
+
...(typeof metadata.triggerMessage.userId === 'string' && metadata.triggerMessage.userId.trim()
|
|
3340
|
+
? { userId: metadata.triggerMessage.userId.trim() }
|
|
3341
|
+
: {}),
|
|
3342
|
+
};
|
|
3343
|
+
}
|
|
3344
|
+
if (metadata.expectedTermination?.kind === 'dm_handoff_bootstrap'
|
|
3345
|
+
&& metadata.expectedTermination.stopReason === 'handoff_bootstrap') {
|
|
3346
|
+
payload.expectedTermination = {
|
|
3347
|
+
kind: 'dm_handoff_bootstrap',
|
|
3348
|
+
stopReason: 'handoff_bootstrap',
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
if (metadata.systemFollowup?.kind === 'task_status_reminder'
|
|
3352
|
+
&& typeof metadata.systemFollowup.followupId === 'string'
|
|
3353
|
+
&& metadata.systemFollowup.followupId.trim()
|
|
3354
|
+
&& typeof metadata.systemFollowup.taskId === 'string'
|
|
3355
|
+
&& metadata.systemFollowup.taskId.trim()
|
|
3356
|
+
&& typeof metadata.systemFollowup.channelId === 'string'
|
|
3357
|
+
&& metadata.systemFollowup.channelId.trim()
|
|
3358
|
+
&& typeof metadata.systemFollowup.threadRootId === 'string'
|
|
3359
|
+
&& metadata.systemFollowup.threadRootId.trim()) {
|
|
3360
|
+
payload.systemFollowup = {
|
|
3361
|
+
kind: 'task_status_reminder',
|
|
3362
|
+
followupId: metadata.systemFollowup.followupId.trim(),
|
|
3363
|
+
taskId: metadata.systemFollowup.taskId.trim(),
|
|
3364
|
+
channelId: metadata.systemFollowup.channelId.trim(),
|
|
3365
|
+
threadRootId: metadata.systemFollowup.threadRootId.trim(),
|
|
3366
|
+
...(typeof metadata.systemFollowup.delayMs === 'number'
|
|
3367
|
+
&& Number.isFinite(metadata.systemFollowup.delayMs)
|
|
3368
|
+
&& metadata.systemFollowup.delayMs > 0
|
|
3369
|
+
? { delayMs: metadata.systemFollowup.delayMs }
|
|
3370
|
+
: {}),
|
|
3371
|
+
...(typeof metadata.systemFollowup.baselineThreadSeq === 'number'
|
|
3372
|
+
&& Number.isFinite(metadata.systemFollowup.baselineThreadSeq)
|
|
3373
|
+
&& metadata.systemFollowup.baselineThreadSeq >= 0
|
|
3374
|
+
? { baselineThreadSeq: metadata.systemFollowup.baselineThreadSeq }
|
|
3375
|
+
: {}),
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
if (metadata.taskPeerUpdate
|
|
3379
|
+
&& typeof metadata.taskPeerUpdate.taskId === 'string'
|
|
3380
|
+
&& metadata.taskPeerUpdate.taskId.trim()
|
|
3381
|
+
&& typeof metadata.taskPeerUpdate.channelId === 'string'
|
|
3382
|
+
&& metadata.taskPeerUpdate.channelId.trim()
|
|
3383
|
+
&& typeof metadata.taskPeerUpdate.threadRootId === 'string'
|
|
3384
|
+
&& metadata.taskPeerUpdate.threadRootId.trim()
|
|
3385
|
+
&& typeof metadata.taskPeerUpdate.fromAgentId === 'string'
|
|
3386
|
+
&& metadata.taskPeerUpdate.fromAgentId.trim()) {
|
|
3387
|
+
payload.taskPeerUpdate = {
|
|
3388
|
+
taskId: metadata.taskPeerUpdate.taskId.trim(),
|
|
3389
|
+
channelId: metadata.taskPeerUpdate.channelId.trim(),
|
|
3390
|
+
threadRootId: metadata.taskPeerUpdate.threadRootId.trim(),
|
|
3391
|
+
fromAgentId: metadata.taskPeerUpdate.fromAgentId.trim(),
|
|
3392
|
+
...(typeof metadata.taskPeerUpdate.firstMessageId === 'string' && metadata.taskPeerUpdate.firstMessageId.trim()
|
|
3393
|
+
? { firstMessageId: metadata.taskPeerUpdate.firstMessageId.trim() }
|
|
3394
|
+
: {}),
|
|
3395
|
+
...(typeof metadata.taskPeerUpdate.lastMessageId === 'string' && metadata.taskPeerUpdate.lastMessageId.trim()
|
|
3396
|
+
? { lastMessageId: metadata.taskPeerUpdate.lastMessageId.trim() }
|
|
3397
|
+
: {}),
|
|
3398
|
+
...(typeof metadata.taskPeerUpdate.messageCount === 'number'
|
|
3399
|
+
&& Number.isFinite(metadata.taskPeerUpdate.messageCount)
|
|
3400
|
+
&& metadata.taskPeerUpdate.messageCount > 0
|
|
3401
|
+
? { messageCount: metadata.taskPeerUpdate.messageCount }
|
|
3402
|
+
: {}),
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
if (metadata.handoffDispatch
|
|
3406
|
+
&& typeof metadata.handoffDispatch.correlationId === 'string'
|
|
3407
|
+
&& metadata.handoffDispatch.correlationId.trim()
|
|
3408
|
+
&& typeof metadata.handoffDispatch.sourceConversationId === 'string'
|
|
3409
|
+
&& metadata.handoffDispatch.sourceConversationId.trim()
|
|
3410
|
+
&& typeof metadata.handoffDispatch.targetConversationId === 'string'
|
|
3411
|
+
&& metadata.handoffDispatch.targetConversationId.trim()) {
|
|
3412
|
+
payload.handoffDispatch = {
|
|
3413
|
+
correlationId: metadata.handoffDispatch.correlationId.trim(),
|
|
3414
|
+
sourceConversationId: metadata.handoffDispatch.sourceConversationId.trim(),
|
|
3415
|
+
targetConversationId: metadata.handoffDispatch.targetConversationId.trim(),
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
if (metadata.peerInboxAggregate
|
|
3419
|
+
&& typeof metadata.peerInboxAggregate.aggregateId === 'string'
|
|
3420
|
+
&& metadata.peerInboxAggregate.aggregateId.trim()
|
|
3421
|
+
&& Array.isArray(metadata.peerInboxAggregate.itemMessageIds)
|
|
3422
|
+
&& Number.isFinite(metadata.peerInboxAggregate.firstSeq)
|
|
3423
|
+
&& Number.isFinite(metadata.peerInboxAggregate.lastSeq)
|
|
3424
|
+
&& Number.isFinite(metadata.peerInboxAggregate.messageCount)
|
|
3425
|
+
&& (metadata.peerInboxAggregate.flushPolicy === 'batch' || metadata.peerInboxAggregate.flushPolicy === 'immediate')) {
|
|
3426
|
+
const itemMessageIds = metadata.peerInboxAggregate.itemMessageIds
|
|
3427
|
+
.filter((messageId) => typeof messageId === 'string' && messageId.trim().length > 0)
|
|
3428
|
+
.map((messageId) => messageId.trim());
|
|
3429
|
+
if (itemMessageIds.length > 0) {
|
|
3430
|
+
payload.peerInboxAggregate = {
|
|
3431
|
+
aggregateId: metadata.peerInboxAggregate.aggregateId.trim(),
|
|
3432
|
+
itemMessageIds,
|
|
3433
|
+
firstSeq: metadata.peerInboxAggregate.firstSeq,
|
|
3434
|
+
lastSeq: metadata.peerInboxAggregate.lastSeq,
|
|
3435
|
+
messageCount: metadata.peerInboxAggregate.messageCount,
|
|
3436
|
+
flushPolicy: metadata.peerInboxAggregate.flushPolicy,
|
|
3437
|
+
...(metadata.peerInboxAggregate.closeSummary === true ? { closeSummary: true } : {}),
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
if (metadata.sharedCollaboration
|
|
3442
|
+
&& typeof metadata.sharedCollaboration.capsuleId === 'string'
|
|
3443
|
+
&& metadata.sharedCollaboration.capsuleId.trim()
|
|
3444
|
+
&& typeof metadata.sharedCollaboration.channelId === 'string'
|
|
3445
|
+
&& metadata.sharedCollaboration.channelId.trim()
|
|
3446
|
+
&& typeof metadata.sharedCollaboration.threadRootId === 'string'
|
|
3447
|
+
&& typeof metadata.sharedCollaboration.originMessageId === 'string'
|
|
3448
|
+
&& metadata.sharedCollaboration.originMessageId.trim()
|
|
3449
|
+
&& Number.isFinite(metadata.sharedCollaboration.originSeq)
|
|
3450
|
+
&& Number.isFinite(metadata.sharedCollaboration.contributionCount)
|
|
3451
|
+
&& Number.isFinite(metadata.sharedCollaboration.maxVisibleAgentMessages)) {
|
|
3452
|
+
payload.sharedCollaboration = {
|
|
3453
|
+
capsuleId: metadata.sharedCollaboration.capsuleId.trim(),
|
|
3454
|
+
channelId: metadata.sharedCollaboration.channelId.trim(),
|
|
3455
|
+
threadRootId: metadata.sharedCollaboration.threadRootId.trim(),
|
|
3456
|
+
originMessageId: metadata.sharedCollaboration.originMessageId.trim(),
|
|
3457
|
+
originSeq: Math.max(0, Math.floor(metadata.sharedCollaboration.originSeq)),
|
|
3458
|
+
contributionCount: Math.max(0, Math.floor(metadata.sharedCollaboration.contributionCount)),
|
|
3459
|
+
maxVisibleAgentMessages: Math.max(1, Math.floor(metadata.sharedCollaboration.maxVisibleAgentMessages)),
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
const ownerAwareness = normalizeOwnerAwarenessMetadata(metadata.ownerAwareness);
|
|
3463
|
+
if (ownerAwareness) {
|
|
3464
|
+
payload.ownerAwareness = ownerAwareness;
|
|
3465
|
+
}
|
|
3466
|
+
const promptContextSections = serializePromptContextSectionsMetadata(metadata.promptContextSections);
|
|
3467
|
+
if (promptContextSections) {
|
|
3468
|
+
payload.promptContextSections = promptContextSections;
|
|
3469
|
+
}
|
|
3470
|
+
if (metadata.activeSteerIncludeExactTargetHistory === true) {
|
|
3471
|
+
payload.activeSteerIncludeExactTargetHistory = true;
|
|
3472
|
+
}
|
|
3473
|
+
if (metadata.liveContextIncludeExactTargetHistory === true) {
|
|
3474
|
+
payload.liveContextIncludeExactTargetHistory = true;
|
|
3475
|
+
}
|
|
3476
|
+
const runtimeOverrides = normalizePromptRuntimeOverrides(metadata.runtimeOverrides);
|
|
3477
|
+
if (runtimeOverrides) {
|
|
3478
|
+
payload.runtimeOverrides = runtimeOverrides;
|
|
3479
|
+
}
|
|
3480
|
+
const attachments = sanitizeFileRefs(metadata.attachments);
|
|
3481
|
+
if (attachments.length > 0) {
|
|
3482
|
+
payload.attachments = attachments;
|
|
3483
|
+
}
|
|
3484
|
+
const attachmentIds = sanitizeAttachmentIds(metadata.attachmentIds);
|
|
3485
|
+
if (attachmentIds.length > 0) {
|
|
3486
|
+
payload.attachmentIds = attachmentIds;
|
|
3487
|
+
}
|
|
3488
|
+
const slashCommand = normalizePromptSlashCommand(metadata.slashCommand);
|
|
3489
|
+
if (slashCommand) {
|
|
3490
|
+
payload.slashCommand = slashCommand;
|
|
3491
|
+
}
|
|
3492
|
+
if (metadata.suppressReplyContract === true) {
|
|
3493
|
+
payload.suppressReplyContract = true;
|
|
3494
|
+
}
|
|
3495
|
+
if (!payload.platformRun && !payload.mentionSuppression && !payload.triggerMessage && !payload.expectedTermination && !payload.systemFollowup && !payload.taskPeerUpdate && !payload.peerInboxAggregate && !payload.sharedCollaboration && !payload.ownerAwareness && !payload.activeSteerIncludeExactTargetHistory && !payload.liveContextIncludeExactTargetHistory && !payload.handoffDispatch && !payload.promptContextSections && !payload.runtimeOverrides && !payload.attachments && !payload.attachmentIds && !payload.slashCommand && !payload.suppressReplyContract)
|
|
3496
|
+
return null;
|
|
3497
|
+
return JSON.stringify(payload);
|
|
3498
|
+
}
|
|
3499
|
+
function normalizePromptRuntimeOverrides(value) {
|
|
3500
|
+
if (!value)
|
|
3501
|
+
return undefined;
|
|
3502
|
+
const planMode = value.planMode === true || value.codexMode === 'plan';
|
|
3503
|
+
const codexMode = value.codexMode === 'default' || value.codexMode === 'plan'
|
|
3504
|
+
? value.codexMode
|
|
3505
|
+
: null;
|
|
3506
|
+
if (!planMode && !codexMode)
|
|
3507
|
+
return undefined;
|
|
3508
|
+
return {
|
|
3509
|
+
...(planMode ? { planMode: true } : {}),
|
|
3510
|
+
...(codexMode ? { codexMode } : {}),
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
function parsePromptActivationMetadata(value) {
|
|
3514
|
+
if (!value)
|
|
3515
|
+
return undefined;
|
|
3516
|
+
try {
|
|
3517
|
+
const parsed = JSON.parse(value);
|
|
3518
|
+
const result = {};
|
|
3519
|
+
const platformRun = normalizePlatformRunMetadata(parsed?.platformRun);
|
|
3520
|
+
if (platformRun) {
|
|
3521
|
+
result.platformRun = platformRun;
|
|
3522
|
+
}
|
|
3523
|
+
const mentionSuppression = parsed?.mentionSuppression;
|
|
3524
|
+
if (mentionSuppression) {
|
|
3525
|
+
const peerMentionedAgentIds = Array.isArray(mentionSuppression.peerMentionedAgentIds)
|
|
3526
|
+
? mentionSuppression.peerMentionedAgentIds.filter((agentId) => typeof agentId === 'string' && agentId.trim().length > 0)
|
|
3527
|
+
: [];
|
|
3528
|
+
if (mentionSuppression.mode === 'root_user_multi_mention' && Number.isFinite(mentionSuppression.triggerSeq) && peerMentionedAgentIds.length > 0) {
|
|
3529
|
+
result.mentionSuppression = {
|
|
3530
|
+
mode: 'root_user_multi_mention',
|
|
3531
|
+
triggerSeq: mentionSuppression.triggerSeq,
|
|
3532
|
+
peerMentionedAgentIds,
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
const triggerMessage = parsed?.triggerMessage;
|
|
3537
|
+
if (triggerMessage
|
|
3538
|
+
&& typeof triggerMessage.messageId === 'string'
|
|
3539
|
+
&& triggerMessage.messageId.trim()
|
|
3540
|
+
&& Number.isFinite(triggerMessage.seq)
|
|
3541
|
+
&& typeof triggerMessage.target === 'string'
|
|
3542
|
+
&& triggerMessage.target.trim()) {
|
|
3543
|
+
result.triggerMessage = {
|
|
3544
|
+
messageId: triggerMessage.messageId.trim(),
|
|
3545
|
+
seq: triggerMessage.seq,
|
|
3546
|
+
target: triggerMessage.target.trim(),
|
|
3547
|
+
...(typeof triggerMessage.userId === 'string' && triggerMessage.userId.trim()
|
|
3548
|
+
? { userId: triggerMessage.userId.trim() }
|
|
3549
|
+
: {}),
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
if (parsed?.expectedTermination?.kind === 'dm_handoff_bootstrap'
|
|
3553
|
+
&& parsed.expectedTermination.stopReason === 'handoff_bootstrap') {
|
|
3554
|
+
result.expectedTermination = {
|
|
3555
|
+
kind: 'dm_handoff_bootstrap',
|
|
3556
|
+
stopReason: 'handoff_bootstrap',
|
|
3557
|
+
};
|
|
3558
|
+
}
|
|
3559
|
+
if (parsed?.systemFollowup?.kind === 'task_status_reminder'
|
|
3560
|
+
&& typeof parsed.systemFollowup.followupId === 'string'
|
|
3561
|
+
&& parsed.systemFollowup.followupId.trim()
|
|
3562
|
+
&& typeof parsed.systemFollowup.taskId === 'string'
|
|
3563
|
+
&& parsed.systemFollowup.taskId.trim()
|
|
3564
|
+
&& typeof parsed.systemFollowup.channelId === 'string'
|
|
3565
|
+
&& parsed.systemFollowup.channelId.trim()
|
|
3566
|
+
&& typeof parsed.systemFollowup.threadRootId === 'string'
|
|
3567
|
+
&& parsed.systemFollowup.threadRootId.trim()) {
|
|
3568
|
+
result.systemFollowup = {
|
|
3569
|
+
kind: 'task_status_reminder',
|
|
3570
|
+
followupId: parsed.systemFollowup.followupId.trim(),
|
|
3571
|
+
taskId: parsed.systemFollowup.taskId.trim(),
|
|
3572
|
+
channelId: parsed.systemFollowup.channelId.trim(),
|
|
3573
|
+
threadRootId: parsed.systemFollowup.threadRootId.trim(),
|
|
3574
|
+
...(typeof parsed.systemFollowup.delayMs === 'number'
|
|
3575
|
+
&& Number.isFinite(parsed.systemFollowup.delayMs)
|
|
3576
|
+
&& parsed.systemFollowup.delayMs > 0
|
|
3577
|
+
? { delayMs: parsed.systemFollowup.delayMs }
|
|
3578
|
+
: {}),
|
|
3579
|
+
...(typeof parsed.systemFollowup.baselineThreadSeq === 'number'
|
|
3580
|
+
&& Number.isFinite(parsed.systemFollowup.baselineThreadSeq)
|
|
3581
|
+
&& parsed.systemFollowup.baselineThreadSeq >= 0
|
|
3582
|
+
? { baselineThreadSeq: parsed.systemFollowup.baselineThreadSeq }
|
|
3583
|
+
: {}),
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
if (parsed?.taskPeerUpdate
|
|
3587
|
+
&& typeof parsed.taskPeerUpdate.taskId === 'string'
|
|
3588
|
+
&& parsed.taskPeerUpdate.taskId.trim()
|
|
3589
|
+
&& typeof parsed.taskPeerUpdate.channelId === 'string'
|
|
3590
|
+
&& parsed.taskPeerUpdate.channelId.trim()
|
|
3591
|
+
&& typeof parsed.taskPeerUpdate.threadRootId === 'string'
|
|
3592
|
+
&& parsed.taskPeerUpdate.threadRootId.trim()
|
|
3593
|
+
&& typeof parsed.taskPeerUpdate.fromAgentId === 'string'
|
|
3594
|
+
&& parsed.taskPeerUpdate.fromAgentId.trim()) {
|
|
3595
|
+
result.taskPeerUpdate = {
|
|
3596
|
+
taskId: parsed.taskPeerUpdate.taskId.trim(),
|
|
3597
|
+
channelId: parsed.taskPeerUpdate.channelId.trim(),
|
|
3598
|
+
threadRootId: parsed.taskPeerUpdate.threadRootId.trim(),
|
|
3599
|
+
fromAgentId: parsed.taskPeerUpdate.fromAgentId.trim(),
|
|
3600
|
+
...(typeof parsed.taskPeerUpdate.firstMessageId === 'string' && parsed.taskPeerUpdate.firstMessageId.trim()
|
|
3601
|
+
? { firstMessageId: parsed.taskPeerUpdate.firstMessageId.trim() }
|
|
3602
|
+
: {}),
|
|
3603
|
+
...(typeof parsed.taskPeerUpdate.lastMessageId === 'string' && parsed.taskPeerUpdate.lastMessageId.trim()
|
|
3604
|
+
? { lastMessageId: parsed.taskPeerUpdate.lastMessageId.trim() }
|
|
3605
|
+
: {}),
|
|
3606
|
+
...(typeof parsed.taskPeerUpdate.messageCount === 'number'
|
|
3607
|
+
&& Number.isFinite(parsed.taskPeerUpdate.messageCount)
|
|
3608
|
+
&& parsed.taskPeerUpdate.messageCount > 0
|
|
3609
|
+
? { messageCount: parsed.taskPeerUpdate.messageCount }
|
|
3610
|
+
: {}),
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
if (parsed?.handoffDispatch
|
|
3614
|
+
&& typeof parsed.handoffDispatch.correlationId === 'string'
|
|
3615
|
+
&& parsed.handoffDispatch.correlationId.trim()
|
|
3616
|
+
&& typeof parsed.handoffDispatch.sourceConversationId === 'string'
|
|
3617
|
+
&& parsed.handoffDispatch.sourceConversationId.trim()
|
|
3618
|
+
&& typeof parsed.handoffDispatch.targetConversationId === 'string'
|
|
3619
|
+
&& parsed.handoffDispatch.targetConversationId.trim()) {
|
|
3620
|
+
result.handoffDispatch = {
|
|
3621
|
+
correlationId: parsed.handoffDispatch.correlationId.trim(),
|
|
3622
|
+
sourceConversationId: parsed.handoffDispatch.sourceConversationId.trim(),
|
|
3623
|
+
targetConversationId: parsed.handoffDispatch.targetConversationId.trim(),
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
if (parsed?.peerInboxAggregate
|
|
3627
|
+
&& typeof parsed.peerInboxAggregate.aggregateId === 'string'
|
|
3628
|
+
&& parsed.peerInboxAggregate.aggregateId.trim()
|
|
3629
|
+
&& Array.isArray(parsed.peerInboxAggregate.itemMessageIds)
|
|
3630
|
+
&& Number.isFinite(parsed.peerInboxAggregate.firstSeq)
|
|
3631
|
+
&& Number.isFinite(parsed.peerInboxAggregate.lastSeq)
|
|
3632
|
+
&& Number.isFinite(parsed.peerInboxAggregate.messageCount)
|
|
3633
|
+
&& (parsed.peerInboxAggregate.flushPolicy === 'batch' || parsed.peerInboxAggregate.flushPolicy === 'immediate')) {
|
|
3634
|
+
const itemMessageIds = parsed.peerInboxAggregate.itemMessageIds
|
|
3635
|
+
.filter((messageId) => typeof messageId === 'string' && messageId.trim().length > 0)
|
|
3636
|
+
.map((messageId) => messageId.trim());
|
|
3637
|
+
if (itemMessageIds.length > 0) {
|
|
3638
|
+
result.peerInboxAggregate = {
|
|
3639
|
+
aggregateId: parsed.peerInboxAggregate.aggregateId.trim(),
|
|
3640
|
+
itemMessageIds,
|
|
3641
|
+
firstSeq: parsed.peerInboxAggregate.firstSeq,
|
|
3642
|
+
lastSeq: parsed.peerInboxAggregate.lastSeq,
|
|
3643
|
+
messageCount: parsed.peerInboxAggregate.messageCount,
|
|
3644
|
+
flushPolicy: parsed.peerInboxAggregate.flushPolicy,
|
|
3645
|
+
...(parsed.peerInboxAggregate.closeSummary === true ? { closeSummary: true } : {}),
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
if (parsed?.sharedCollaboration
|
|
3650
|
+
&& typeof parsed.sharedCollaboration.capsuleId === 'string'
|
|
3651
|
+
&& parsed.sharedCollaboration.capsuleId.trim()
|
|
3652
|
+
&& typeof parsed.sharedCollaboration.channelId === 'string'
|
|
3653
|
+
&& parsed.sharedCollaboration.channelId.trim()
|
|
3654
|
+
&& typeof parsed.sharedCollaboration.threadRootId === 'string'
|
|
3655
|
+
&& typeof parsed.sharedCollaboration.originMessageId === 'string'
|
|
3656
|
+
&& parsed.sharedCollaboration.originMessageId.trim()
|
|
3657
|
+
&& Number.isFinite(parsed.sharedCollaboration.originSeq)
|
|
3658
|
+
&& Number.isFinite(parsed.sharedCollaboration.contributionCount)
|
|
3659
|
+
&& Number.isFinite(parsed.sharedCollaboration.maxVisibleAgentMessages)) {
|
|
3660
|
+
result.sharedCollaboration = {
|
|
3661
|
+
capsuleId: parsed.sharedCollaboration.capsuleId.trim(),
|
|
3662
|
+
channelId: parsed.sharedCollaboration.channelId.trim(),
|
|
3663
|
+
threadRootId: parsed.sharedCollaboration.threadRootId.trim(),
|
|
3664
|
+
originMessageId: parsed.sharedCollaboration.originMessageId.trim(),
|
|
3665
|
+
originSeq: Math.max(0, Math.floor(parsed.sharedCollaboration.originSeq)),
|
|
3666
|
+
contributionCount: Math.max(0, Math.floor(parsed.sharedCollaboration.contributionCount)),
|
|
3667
|
+
maxVisibleAgentMessages: Math.max(1, Math.floor(parsed.sharedCollaboration.maxVisibleAgentMessages)),
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
const ownerAwareness = normalizeOwnerAwarenessMetadata(parsed?.ownerAwareness);
|
|
3671
|
+
if (ownerAwareness) {
|
|
3672
|
+
result.ownerAwareness = ownerAwareness;
|
|
3673
|
+
}
|
|
3674
|
+
const promptContextSections = parsePromptContextSectionsMetadata(parsed?.promptContextSections);
|
|
3675
|
+
if (promptContextSections) {
|
|
3676
|
+
result.promptContextSections = promptContextSections;
|
|
3677
|
+
}
|
|
3678
|
+
if (parsed?.activeSteerIncludeExactTargetHistory === true) {
|
|
3679
|
+
result.activeSteerIncludeExactTargetHistory = true;
|
|
3680
|
+
}
|
|
3681
|
+
if (parsed?.liveContextIncludeExactTargetHistory === true) {
|
|
3682
|
+
result.liveContextIncludeExactTargetHistory = true;
|
|
3683
|
+
}
|
|
3684
|
+
const runtimeOverrides = normalizePromptRuntimeOverrides(parsed?.runtimeOverrides);
|
|
3685
|
+
if (runtimeOverrides) {
|
|
3686
|
+
result.runtimeOverrides = runtimeOverrides;
|
|
3687
|
+
}
|
|
3688
|
+
const attachments = sanitizeFileRefs(parsed?.attachments);
|
|
3689
|
+
if (attachments.length > 0) {
|
|
3690
|
+
result.attachments = attachments;
|
|
3691
|
+
}
|
|
3692
|
+
const attachmentIds = sanitizeAttachmentIds(parsed?.attachmentIds);
|
|
3693
|
+
if (attachmentIds.length > 0) {
|
|
3694
|
+
result.attachmentIds = attachmentIds;
|
|
3695
|
+
}
|
|
3696
|
+
const slashCommand = normalizePromptSlashCommand(parsed?.slashCommand);
|
|
3697
|
+
if (slashCommand) {
|
|
3698
|
+
result.slashCommand = slashCommand;
|
|
3699
|
+
}
|
|
3700
|
+
if (parsed?.suppressReplyContract === true) {
|
|
3701
|
+
result.suppressReplyContract = true;
|
|
3702
|
+
}
|
|
3703
|
+
return result.platformRun || result.mentionSuppression || result.triggerMessage || result.expectedTermination || result.systemFollowup || result.taskPeerUpdate || result.peerInboxAggregate || result.sharedCollaboration || result.ownerAwareness || result.activeSteerIncludeExactTargetHistory || result.liveContextIncludeExactTargetHistory || result.handoffDispatch || result.promptContextSections || result.runtimeOverrides || result.attachments || result.attachmentIds || result.slashCommand || result.suppressReplyContract
|
|
3704
|
+
? result
|
|
3705
|
+
: undefined;
|
|
3706
|
+
}
|
|
3707
|
+
catch {
|
|
3708
|
+
return undefined;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
function normalizePromptSlashCommand(value) {
|
|
3712
|
+
if (!value || typeof value.name !== 'string' || typeof value.bodyText !== 'string') {
|
|
3713
|
+
return undefined;
|
|
3714
|
+
}
|
|
3715
|
+
const name = normalizePromptSlashCommandName(value.name);
|
|
3716
|
+
if (name === 'reminder') {
|
|
3717
|
+
const bodyText = value.bodyText.trim();
|
|
3718
|
+
if (!bodyText)
|
|
3719
|
+
return undefined;
|
|
3720
|
+
return { name, bodyText };
|
|
3721
|
+
}
|
|
3722
|
+
if (name === 'document') {
|
|
3723
|
+
return { name, bodyText: value.bodyText.trim() };
|
|
3724
|
+
}
|
|
3725
|
+
if (name === 'panel' || name === 'panel:plan') {
|
|
3726
|
+
const bodyText = value.bodyText.trim();
|
|
3727
|
+
if (!bodyText)
|
|
3728
|
+
return undefined;
|
|
3729
|
+
return { name, bodyText };
|
|
3730
|
+
}
|
|
3731
|
+
if (name === 'tool' || name === 'tool:plan') {
|
|
3732
|
+
const bodyText = value.bodyText.trim();
|
|
3733
|
+
if (!bodyText)
|
|
3734
|
+
return undefined;
|
|
3735
|
+
return { name, bodyText };
|
|
3736
|
+
}
|
|
3737
|
+
return undefined;
|
|
3738
|
+
}
|
|
3739
|
+
function findPromptAttachmentNoteIndex(promptText) {
|
|
3740
|
+
const fileIdx = promptText.indexOf('\n\n[Attached file');
|
|
3741
|
+
if (fileIdx >= 0)
|
|
3742
|
+
return fileIdx;
|
|
3743
|
+
return promptText.indexOf('\n\n[Attached image');
|
|
3744
|
+
}
|
|
3745
|
+
function buildDisplayContentOverrideForDirectMessage(params) {
|
|
3746
|
+
const slashCommand = normalizePromptSlashCommand(params.slashCommand);
|
|
3747
|
+
if (!slashCommand)
|
|
3748
|
+
return null;
|
|
3749
|
+
if (slashCommand.name === 'document') {
|
|
3750
|
+
return slashCommand.bodyText || 'Use the document library for this conversation.';
|
|
3751
|
+
}
|
|
3752
|
+
return slashCommand.bodyText;
|
|
3753
|
+
}
|
|
3754
|
+
function buildDispatchPromptContentForDirectMessage(params) {
|
|
3755
|
+
const overrideDisplayContent = buildDisplayContentOverrideForDirectMessage(params);
|
|
3756
|
+
if (!overrideDisplayContent)
|
|
3757
|
+
return params.promptText;
|
|
3758
|
+
const attachNoteIdx = findPromptAttachmentNoteIndex(params.promptText);
|
|
3759
|
+
if (attachNoteIdx < 0)
|
|
3760
|
+
return overrideDisplayContent;
|
|
3761
|
+
return `${overrideDisplayContent}${params.promptText.slice(attachNoteIdx)}`;
|
|
3762
|
+
}
|
|
3763
|
+
function serializePromptContextSectionsMetadata(value) {
|
|
3764
|
+
if (!value)
|
|
3765
|
+
return undefined;
|
|
3766
|
+
const activation = serializePromptContextSectionsArray(value.activation);
|
|
3767
|
+
const resume = serializePromptContextSectionsArray(value.resume);
|
|
3768
|
+
if (!activation && !resume)
|
|
3769
|
+
return undefined;
|
|
3770
|
+
return {
|
|
3771
|
+
...(activation ? { activation } : {}),
|
|
3772
|
+
...(resume ? { resume } : {}),
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3775
|
+
function serializePromptContextSectionsArray(sections) {
|
|
3776
|
+
const serialized = normalizePromptContextSections(sections ?? [])
|
|
3777
|
+
.map((section) => ({
|
|
3778
|
+
kind: section.kind,
|
|
3779
|
+
text: section.text,
|
|
3780
|
+
}));
|
|
3781
|
+
return serialized.length > 0 ? serialized : undefined;
|
|
3782
|
+
}
|
|
3783
|
+
function parsePromptContextSectionsMetadata(value) {
|
|
3784
|
+
if (!value || typeof value !== 'object')
|
|
3785
|
+
return undefined;
|
|
3786
|
+
const row = value;
|
|
3787
|
+
const activation = parsePromptContextSectionsArray(row.activation);
|
|
3788
|
+
const resume = parsePromptContextSectionsArray(row.resume);
|
|
3789
|
+
if (!activation && !resume)
|
|
3790
|
+
return undefined;
|
|
3791
|
+
return {
|
|
3792
|
+
...(activation ? { activation } : {}),
|
|
3793
|
+
...(resume ? { resume } : {}),
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
function parsePromptContextSectionsArray(value) {
|
|
3797
|
+
if (!Array.isArray(value))
|
|
3798
|
+
return undefined;
|
|
3799
|
+
const sections = value
|
|
3800
|
+
.map((item) => {
|
|
3801
|
+
if (!item || typeof item !== 'object')
|
|
3802
|
+
return null;
|
|
3803
|
+
const row = item;
|
|
3804
|
+
if (!isPromptContextSectionKind(row.kind) || typeof row.text !== 'string')
|
|
3805
|
+
return null;
|
|
3806
|
+
return createPromptContextSection(row.kind, row.text);
|
|
3807
|
+
})
|
|
3808
|
+
.filter((section) => Boolean(section));
|
|
3809
|
+
return sections.length > 0 ? sections : undefined;
|
|
3810
|
+
}
|