@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,2495 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { log, finishRun } from '@bbigbang/runtime-acp';
|
|
4
|
+
import { AgentFileAccessService, getRunEventSeqCutoff } from '../services/agentFileAccessService.js';
|
|
5
|
+
import { hasOpenPendingWorkspaceApproval, isWorkspaceApprovalProgressEvent, promoteWorkspaceSentApprovalResponses, } from '../services/workspaceSessionRuntimeService.js';
|
|
6
|
+
import { getWorkspaceRunDispatchNodeId, WORKSPACE_RUN_DISPATCH_METHOD, } from '../services/workspaceRunOwnership.js';
|
|
7
|
+
import { iterateParsedNodeEvents } from '../nodeEventHistory.js';
|
|
8
|
+
import { insertHandoffRun, markConversationHandoffAccepted, markConversationHandoffCancelled, markConversationHandoffCompleted, markConversationHandoffFailed, } from './conversationHandoffs.js';
|
|
9
|
+
import { refreshConversationSummariesForConversationIds } from './conversationSummaries.js';
|
|
10
|
+
import { resolveConversationReplyTarget, resolveDirectUserIdFromReplyTarget } from './directReplyTargets.js';
|
|
11
|
+
import { allocateNextChannelMessageSeq } from './channelMessageSequences.js';
|
|
12
|
+
import { getBoundTaskForThread } from './threadTaskBindings.js';
|
|
13
|
+
import { scheduleTaskThreadFollowupForRun } from './taskThreadFollowups.js';
|
|
14
|
+
import { clearTaskThreadReopenedRunBlock } from './taskThreadRuntimeClosure.js';
|
|
15
|
+
import { markCodexRotationPendingIfDue, recordCodexCompactEvent } from './rollingConversationSummary.js';
|
|
16
|
+
import { buildPrimaryDmTaskRootUpdateEventForThreadConversation } from './dmTaskThreadStatusProjection.js';
|
|
17
|
+
import { DM_HANDOFF_BOOTSTRAP_STOP_REASON, getExpectedBootstrapStopReason, getSnapshotConversationStatus, } from './planApprovalState.js';
|
|
18
|
+
/** Persist a ServerEvent from a remote run into core DB as a node/event entry */
|
|
19
|
+
function appendNodeEvent(db, runId, seq, event) {
|
|
20
|
+
const persistedEvent = normalizeReplayEventForPersistence(event);
|
|
21
|
+
db.prepare('INSERT OR IGNORE INTO events(run_id, seq, method, payload_json, created_at) VALUES(?, ?, ?, ?, ?)').run(runId, seq, 'node/event', JSON.stringify(persistedEvent), Date.now());
|
|
22
|
+
}
|
|
23
|
+
function nextNodeEventSeq(db, runId, runSeq) {
|
|
24
|
+
const row = db.prepare(`SELECT COALESCE(MAX(seq), 0) as maxSeq
|
|
25
|
+
FROM events
|
|
26
|
+
WHERE run_id = ?
|
|
27
|
+
AND seq > 0`).get(runId);
|
|
28
|
+
const current = runSeq.get(runId) ?? 0;
|
|
29
|
+
return Math.max(current, row?.maxSeq ?? 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
function normalizeReplayEventForPersistence(event) {
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case 'tool.call':
|
|
34
|
+
return {
|
|
35
|
+
type: 'tool.call',
|
|
36
|
+
toolCallId: event.toolCallId,
|
|
37
|
+
name: event.name,
|
|
38
|
+
input: event.input,
|
|
39
|
+
status: event.status,
|
|
40
|
+
startedAt: event.startedAt,
|
|
41
|
+
};
|
|
42
|
+
case 'tool.result':
|
|
43
|
+
return {
|
|
44
|
+
type: 'tool.result',
|
|
45
|
+
toolCallId: event.toolCallId,
|
|
46
|
+
output: event.output,
|
|
47
|
+
error: event.error,
|
|
48
|
+
status: event.status,
|
|
49
|
+
endedAt: event.endedAt,
|
|
50
|
+
};
|
|
51
|
+
case 'run.usage':
|
|
52
|
+
return {
|
|
53
|
+
type: 'run.usage',
|
|
54
|
+
inputTokens: event.inputTokens,
|
|
55
|
+
cachedInputTokens: event.cachedInputTokens,
|
|
56
|
+
outputTokens: event.outputTokens,
|
|
57
|
+
reasoningOutputTokens: event.reasoningOutputTokens,
|
|
58
|
+
totalTokens: event.totalTokens,
|
|
59
|
+
currentInputTokens: event.currentInputTokens,
|
|
60
|
+
currentCachedInputTokens: event.currentCachedInputTokens,
|
|
61
|
+
modelContextWindow: event.modelContextWindow,
|
|
62
|
+
createdAt: event.createdAt,
|
|
63
|
+
};
|
|
64
|
+
case 'runtime.compact':
|
|
65
|
+
return {
|
|
66
|
+
type: 'runtime.compact',
|
|
67
|
+
threadId: event.threadId,
|
|
68
|
+
turnId: event.turnId,
|
|
69
|
+
itemId: event.itemId,
|
|
70
|
+
source: event.source,
|
|
71
|
+
eventKey: event.eventKey,
|
|
72
|
+
createdAt: event.createdAt,
|
|
73
|
+
};
|
|
74
|
+
default:
|
|
75
|
+
return event;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function applyRunDebugSnapshot(db, params) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
db.prepare(`UPDATE run_debug_inputs
|
|
81
|
+
SET acp_session_id = ?,
|
|
82
|
+
is_fresh_session = ?,
|
|
83
|
+
is_exact = ?,
|
|
84
|
+
system_prompt_text = COALESCE(?, system_prompt_text),
|
|
85
|
+
context_text = ?,
|
|
86
|
+
updated_at = ?
|
|
87
|
+
WHERE run_id = ?`).run(params.acpSessionId, params.isFreshSession ? 1 : 0, params.isExact ? 1 : 0, params.effectiveSystemPromptText ?? null, params.effectiveContextText ?? null, now, params.runId);
|
|
88
|
+
db.prepare(`UPDATE sessions
|
|
89
|
+
SET acp_session_id = ?,
|
|
90
|
+
system_prompt_text = COALESCE(?, system_prompt_text),
|
|
91
|
+
updated_at = ?
|
|
92
|
+
WHERE session_key = ?`).run(params.acpSessionId, params.effectiveSystemPromptText ?? null, now, params.sessionKey);
|
|
93
|
+
}
|
|
94
|
+
/** Event types worth persisting for history replay */
|
|
95
|
+
const REPLAY_EVENT_TYPES = new Set([
|
|
96
|
+
'content.delta',
|
|
97
|
+
'activity.delta',
|
|
98
|
+
'tool.call',
|
|
99
|
+
'tool.result',
|
|
100
|
+
'thinking.delta',
|
|
101
|
+
'plan.update',
|
|
102
|
+
'plan.phase',
|
|
103
|
+
'task.update',
|
|
104
|
+
'run.usage',
|
|
105
|
+
'runtime.compact',
|
|
106
|
+
'run.steer.result',
|
|
107
|
+
]);
|
|
108
|
+
const DM_TASK_HANDOFF_EVENT_METHOD = 'platform/handoff';
|
|
109
|
+
const PLAN_APPROVAL_MESSAGE_SOURCE = 'plan_approval';
|
|
110
|
+
function adoptProvisionedNodeIdentity(db, pendingNodeId, registeredNodeId, hostname, agentTypesJson, version, processInstanceId, now) {
|
|
111
|
+
db.prepare(`UPDATE agents
|
|
112
|
+
SET node_id = ?
|
|
113
|
+
WHERE node_id = ?`).run(registeredNodeId, pendingNodeId);
|
|
114
|
+
db.prepare(`UPDATE conversations
|
|
115
|
+
SET node_id = ?
|
|
116
|
+
WHERE node_id = ?`).run(registeredNodeId, pendingNodeId);
|
|
117
|
+
db.prepare(`UPDATE nodes
|
|
118
|
+
SET node_id = ?,
|
|
119
|
+
hostname = ?,
|
|
120
|
+
agent_types_json = ?,
|
|
121
|
+
version = ?,
|
|
122
|
+
process_instance_id = ?,
|
|
123
|
+
status = 'online',
|
|
124
|
+
last_seen = ?,
|
|
125
|
+
created_at = CASE WHEN created_at = 0 THEN ? ELSE created_at END
|
|
126
|
+
WHERE node_id = ?`).run(registeredNodeId, hostname, agentTypesJson, version, processInstanceId, now, now, pendingNodeId);
|
|
127
|
+
}
|
|
128
|
+
function persistNodeHostSnapshots(db, params) {
|
|
129
|
+
db.transaction(() => {
|
|
130
|
+
if (params.hosts.length === 0) {
|
|
131
|
+
markAssetMaterializationsClearedForNode(db, params.nodeId, params.snapshotAt);
|
|
132
|
+
db.prepare(`DELETE FROM node_host_snapshots WHERE node_id = ?`).run(params.nodeId);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const existingHosts = db.prepare(`SELECT host_key as hostKey, host_instance_id as hostInstanceId
|
|
136
|
+
FROM node_host_snapshots
|
|
137
|
+
WHERE node_id = ?`).all(params.nodeId);
|
|
138
|
+
const activeHostKeys = new Set(params.hosts.map((host) => `${host.hostKey}\u0000${host.hostInstanceId ?? ''}`));
|
|
139
|
+
for (const existingHost of existingHosts) {
|
|
140
|
+
if (!activeHostKeys.has(`${existingHost.hostKey}\u0000${existingHost.hostInstanceId ?? ''}`)) {
|
|
141
|
+
markAssetMaterializationsClearedForHost(db, {
|
|
142
|
+
nodeId: params.nodeId,
|
|
143
|
+
hostKey: existingHost.hostKey,
|
|
144
|
+
hostInstanceId: existingHost.hostInstanceId ?? '',
|
|
145
|
+
clearedAt: params.snapshotAt,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const placeholders = params.hosts.map(() => '?').join(', ');
|
|
150
|
+
db.prepare(`DELETE FROM node_host_snapshots WHERE node_id = ? AND host_key NOT IN (${placeholders})`).run(params.nodeId, ...params.hosts.map((host) => host.hostKey));
|
|
151
|
+
const upsert = db.prepare(`INSERT INTO node_host_snapshots(
|
|
152
|
+
node_id, host_key, host_instance_id, session_key, workspace_root, agent_type, state, current_run_id,
|
|
153
|
+
has_pending_approval, inbox_size, pending_dispatch_count, last_wake_at, last_sleep_at,
|
|
154
|
+
last_error, resumable, snapshot_at
|
|
155
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
156
|
+
ON CONFLICT(node_id, host_key) DO UPDATE SET
|
|
157
|
+
host_instance_id = excluded.host_instance_id,
|
|
158
|
+
session_key = excluded.session_key,
|
|
159
|
+
workspace_root = excluded.workspace_root,
|
|
160
|
+
agent_type = excluded.agent_type,
|
|
161
|
+
state = excluded.state,
|
|
162
|
+
current_run_id = excluded.current_run_id,
|
|
163
|
+
has_pending_approval = excluded.has_pending_approval,
|
|
164
|
+
inbox_size = excluded.inbox_size,
|
|
165
|
+
pending_dispatch_count = excluded.pending_dispatch_count,
|
|
166
|
+
last_wake_at = excluded.last_wake_at,
|
|
167
|
+
last_sleep_at = excluded.last_sleep_at,
|
|
168
|
+
last_error = excluded.last_error,
|
|
169
|
+
resumable = excluded.resumable,
|
|
170
|
+
snapshot_at = excluded.snapshot_at`);
|
|
171
|
+
for (const host of params.hosts) {
|
|
172
|
+
upsert.run(params.nodeId, host.hostKey, host.hostInstanceId ?? '', host.sessionKey, host.workspaceRoot, host.agentType, host.state, host.currentRunId, host.hasPendingApproval ? 1 : 0, host.inboxSize, host.pendingDispatchCount, host.lastWakeAt, host.lastSleepAt, host.lastError, host.resumable ? 1 : 0, params.snapshotAt);
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
}
|
|
176
|
+
function parseConversationHostKey(hostKey) {
|
|
177
|
+
const match = /^conversation:([^:]+):([^:]+)$/.exec(hostKey);
|
|
178
|
+
if (!match)
|
|
179
|
+
return null;
|
|
180
|
+
return { conversationId: match[1], agentType: match[2] };
|
|
181
|
+
}
|
|
182
|
+
function parseDreamHostKey(hostKey) {
|
|
183
|
+
const match = /^dream:([^:]+):claude_sdk$/.exec(hostKey);
|
|
184
|
+
if (!match)
|
|
185
|
+
return null;
|
|
186
|
+
return { agentId: match[1] };
|
|
187
|
+
}
|
|
188
|
+
function validateDreamNodeHostSnapshot(db, params) {
|
|
189
|
+
const parsed = parseDreamHostKey(params.host.hostKey);
|
|
190
|
+
if (!parsed) {
|
|
191
|
+
return { ok: false, host: params.host, conversationId: null, reason: 'invalid dream host key' };
|
|
192
|
+
}
|
|
193
|
+
if (params.host.agentType !== 'claude_sdk') {
|
|
194
|
+
return { ok: false, host: params.host, conversationId: null, reason: 'dream host must use claude_sdk agent type' };
|
|
195
|
+
}
|
|
196
|
+
const row = db.prepare(`SELECT c.id as conversationId,
|
|
197
|
+
c.node_id as nodeId,
|
|
198
|
+
c.session_key as sessionKey,
|
|
199
|
+
a.agent_id as agentId,
|
|
200
|
+
r.run_id as runId,
|
|
201
|
+
r.session_key as runSessionKey,
|
|
202
|
+
rdi.dispatch_mode as dispatchMode
|
|
203
|
+
FROM agents a
|
|
204
|
+
JOIN conversations c ON c.agent_id = a.agent_id
|
|
205
|
+
AND c.thread_kind = 'direct'
|
|
206
|
+
AND c.is_primary_thread = 1
|
|
207
|
+
LEFT JOIN runs r ON r.run_id = ?
|
|
208
|
+
LEFT JOIN run_debug_inputs rdi ON rdi.run_id = r.run_id
|
|
209
|
+
WHERE a.agent_id = ?`).get(params.host.currentRunId ?? '', parsed.agentId);
|
|
210
|
+
if (!row) {
|
|
211
|
+
return { ok: false, host: params.host, conversationId: null, reason: 'dream agent conversation does not exist' };
|
|
212
|
+
}
|
|
213
|
+
if (row.nodeId !== params.nodeId) {
|
|
214
|
+
return { ok: false, host: params.host, conversationId: row.conversationId, reason: 'dream agent belongs to a different node' };
|
|
215
|
+
}
|
|
216
|
+
if (row.sessionKey !== params.host.sessionKey) {
|
|
217
|
+
return { ok: false, host: params.host, conversationId: row.conversationId, reason: 'dream host session mismatch' };
|
|
218
|
+
}
|
|
219
|
+
if (params.host.currentRunId) {
|
|
220
|
+
if (!row.runId) {
|
|
221
|
+
return { ok: false, host: params.host, conversationId: row.conversationId, reason: 'dream current run does not exist' };
|
|
222
|
+
}
|
|
223
|
+
if (row.runSessionKey !== params.host.sessionKey) {
|
|
224
|
+
return { ok: false, host: params.host, conversationId: row.conversationId, reason: 'dream current run session mismatch' };
|
|
225
|
+
}
|
|
226
|
+
if (row.dispatchMode !== 'dream') {
|
|
227
|
+
return { ok: false, host: params.host, conversationId: row.conversationId, reason: 'dream host current run is not dream dispatch' };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { ok: true, host: params.host, conversationId: row.conversationId };
|
|
231
|
+
}
|
|
232
|
+
function isPlatformAgentRole(role) {
|
|
233
|
+
return role === 'platform_agent' || role === 'library_curator' || role === 'suggested_planner';
|
|
234
|
+
}
|
|
235
|
+
function validateNodeHostSnapshot(db, params) {
|
|
236
|
+
if (parseDreamHostKey(params.host.hostKey)) {
|
|
237
|
+
return validateDreamNodeHostSnapshot(db, params);
|
|
238
|
+
}
|
|
239
|
+
const parsed = parseConversationHostKey(params.host.hostKey);
|
|
240
|
+
if (!parsed) {
|
|
241
|
+
return { ok: false, host: params.host, conversationId: null, reason: 'host key is not conversation-scoped' };
|
|
242
|
+
}
|
|
243
|
+
if (parsed.agentType !== params.host.agentType) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
host: params.host,
|
|
247
|
+
conversationId: parsed.conversationId,
|
|
248
|
+
reason: 'host key agent type does not match snapshot',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const row = db.prepare(`SELECT c.id as conversationId,
|
|
252
|
+
c.node_id as nodeId,
|
|
253
|
+
c.agent_type as agentType,
|
|
254
|
+
c.session_key as sessionKey,
|
|
255
|
+
c.agent_id as agentId,
|
|
256
|
+
a.agent_role as agentRole,
|
|
257
|
+
pai.enabled as platformEnabled,
|
|
258
|
+
pai.deleted_at as platformDeletedAt,
|
|
259
|
+
pad.enabled as definitionEnabled,
|
|
260
|
+
r.run_id as runId,
|
|
261
|
+
r.session_key as runSessionKey
|
|
262
|
+
FROM conversations c
|
|
263
|
+
LEFT JOIN agents a ON a.agent_id = c.agent_id
|
|
264
|
+
LEFT JOIN platform_agent_instances pai ON pai.agent_id = c.agent_id
|
|
265
|
+
LEFT JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
266
|
+
LEFT JOIN runs r ON r.run_id = ?
|
|
267
|
+
WHERE c.id = ?`).get(params.host.currentRunId ?? '', parsed.conversationId);
|
|
268
|
+
if (!row) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
host: params.host,
|
|
272
|
+
conversationId: parsed.conversationId,
|
|
273
|
+
reason: 'conversation does not exist',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (row.nodeId !== params.nodeId) {
|
|
277
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'conversation belongs to a different node' };
|
|
278
|
+
}
|
|
279
|
+
if (row.agentType !== params.host.agentType) {
|
|
280
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'conversation agent type mismatch' };
|
|
281
|
+
}
|
|
282
|
+
if (row.sessionKey !== params.host.sessionKey) {
|
|
283
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'conversation session mismatch' };
|
|
284
|
+
}
|
|
285
|
+
if (params.host.currentRunId) {
|
|
286
|
+
if (!row.runId) {
|
|
287
|
+
const queuedDispatch = db.prepare(`SELECT 1
|
|
288
|
+
FROM conversation_prompt_queue
|
|
289
|
+
WHERE conversation_id = ?
|
|
290
|
+
AND dispatching_run_id = ?
|
|
291
|
+
LIMIT 1`).get(parsed.conversationId, params.host.currentRunId);
|
|
292
|
+
if (!queuedDispatch) {
|
|
293
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'current run does not exist' };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if (row.runSessionKey !== params.host.sessionKey) {
|
|
297
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'current run session mismatch' };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (isPlatformAgentRole(row.agentRole)) {
|
|
301
|
+
if (row.platformEnabled !== 1 || row.platformDeletedAt !== null || row.definitionEnabled !== 1) {
|
|
302
|
+
return { ok: false, host: params.host, conversationId: parsed.conversationId, reason: 'platform agent instance is disabled or deleted' };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return { ok: true, host: params.host, conversationId: parsed.conversationId };
|
|
306
|
+
}
|
|
307
|
+
function validateNodeHostSnapshots(db, params) {
|
|
308
|
+
const validHosts = [];
|
|
309
|
+
const rejectedHosts = [];
|
|
310
|
+
for (const host of params.hosts) {
|
|
311
|
+
const result = validateNodeHostSnapshot(db, { nodeId: params.nodeId, host });
|
|
312
|
+
if (result.ok) {
|
|
313
|
+
validHosts.push(result.host);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
rejectedHosts.push(result);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { validHosts, rejectedHosts };
|
|
320
|
+
}
|
|
321
|
+
function cleanupRejectedNodeHostSnapshot(db, params) {
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
const conversation = params.conversationId
|
|
324
|
+
? db.prepare(`SELECT id, session_key as sessionKey
|
|
325
|
+
FROM conversations
|
|
326
|
+
WHERE id = ?`).get(params.conversationId)
|
|
327
|
+
: undefined;
|
|
328
|
+
const sessionOwner = params.host.sessionKey
|
|
329
|
+
? db.prepare(`SELECT id
|
|
330
|
+
FROM conversations
|
|
331
|
+
WHERE session_key = ?
|
|
332
|
+
LIMIT 1`).get(params.host.sessionKey)
|
|
333
|
+
: undefined;
|
|
334
|
+
const sessionBelongsToRejectedConversation = Boolean(conversation && conversation.sessionKey === params.host.sessionKey);
|
|
335
|
+
const sessionIsUnownedInCore = Boolean(params.host.sessionKey && !sessionOwner);
|
|
336
|
+
const shouldCleanupSessionKey = sessionBelongsToRejectedConversation || sessionIsUnownedInCore;
|
|
337
|
+
const runBelongsToRejectedSession = params.host.currentRunId && sessionBelongsToRejectedConversation
|
|
338
|
+
? Boolean(db.prepare(`SELECT 1
|
|
339
|
+
FROM runs
|
|
340
|
+
WHERE run_id = ?
|
|
341
|
+
AND session_key = ?
|
|
342
|
+
LIMIT 1`).get(params.host.currentRunId, params.host.sessionKey))
|
|
343
|
+
: false;
|
|
344
|
+
db.transaction(() => {
|
|
345
|
+
db.prepare(`DELETE FROM node_dispatch_queue
|
|
346
|
+
WHERE host_key = ?
|
|
347
|
+
OR conversation_id = ?`).run(params.host.hostKey, params.conversationId ?? '');
|
|
348
|
+
if (shouldCleanupSessionKey) {
|
|
349
|
+
db.prepare(`DELETE FROM node_dispatch_queue WHERE session_key = ?`).run(params.host.sessionKey);
|
|
350
|
+
}
|
|
351
|
+
if (runBelongsToRejectedSession) {
|
|
352
|
+
db.prepare(`DELETE FROM node_dispatch_queue WHERE run_id = ?`).run(params.host.currentRunId);
|
|
353
|
+
}
|
|
354
|
+
db.prepare(`DELETE FROM node_host_snapshots WHERE node_id = ? AND host_key = ?`).run(params.nodeId, params.host.hostKey);
|
|
355
|
+
if (params.conversationId) {
|
|
356
|
+
db.prepare(`UPDATE conversations
|
|
357
|
+
SET status = CASE
|
|
358
|
+
WHEN status IN ('queued', 'active', 'recovering', 'awaiting_approval') THEN 'idle'
|
|
359
|
+
ELSE status
|
|
360
|
+
END
|
|
361
|
+
WHERE id = ?`).run(params.conversationId);
|
|
362
|
+
}
|
|
363
|
+
if (runBelongsToRejectedSession) {
|
|
364
|
+
db.prepare(`UPDATE runs
|
|
365
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
366
|
+
stop_reason = COALESCE(stop_reason, 'error'),
|
|
367
|
+
error = COALESCE(error, ?)
|
|
368
|
+
WHERE run_id = ?`).run(now, `Rejected stale node host snapshot: ${params.reason}`, params.host.currentRunId);
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
void params.agentRuntimeCleanupBroker?.cleanupAgentRuntime({
|
|
372
|
+
nodeId: params.nodeId,
|
|
373
|
+
agentId: 'node-snapshot-validation',
|
|
374
|
+
hostKeys: [params.host.hostKey],
|
|
375
|
+
sessionKeys: shouldCleanupSessionKey ? [params.host.sessionKey] : [],
|
|
376
|
+
}).catch((error) => {
|
|
377
|
+
log.warn('[node-ws] failed to clean rejected host runtime', {
|
|
378
|
+
nodeId: params.nodeId,
|
|
379
|
+
hostKey: params.host.hostKey,
|
|
380
|
+
sessionKey: params.host.sessionKey,
|
|
381
|
+
reason: params.reason,
|
|
382
|
+
error: String(error?.message ?? error),
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
log.warn('[node-ws] rejected invalid node host snapshot', {
|
|
386
|
+
nodeId: params.nodeId,
|
|
387
|
+
hostKey: params.host.hostKey,
|
|
388
|
+
sessionKey: params.host.sessionKey,
|
|
389
|
+
currentRunId: params.host.currentRunId,
|
|
390
|
+
reason: params.reason,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async function cleanupDisabledPlatformAgentRuntimeForNode(db, params) {
|
|
394
|
+
const rows = db.prepare(`SELECT c.id as conversationId,
|
|
395
|
+
c.agent_type as agentType,
|
|
396
|
+
c.session_key as sessionKey,
|
|
397
|
+
c.agent_id as agentId
|
|
398
|
+
FROM conversations c
|
|
399
|
+
JOIN agents a ON a.agent_id = c.agent_id
|
|
400
|
+
LEFT JOIN platform_agent_instances pai ON pai.agent_id = c.agent_id
|
|
401
|
+
LEFT JOIN platform_agent_definitions pad ON pad.definition_id = pai.definition_id
|
|
402
|
+
WHERE c.node_id = ?
|
|
403
|
+
AND a.agent_role IN ('platform_agent', 'library_curator', 'suggested_planner')
|
|
404
|
+
AND (
|
|
405
|
+
pai.agent_id IS NULL
|
|
406
|
+
OR pai.enabled != 1
|
|
407
|
+
OR pai.deleted_at IS NOT NULL
|
|
408
|
+
OR pad.enabled != 1
|
|
409
|
+
)`).all(params.nodeId);
|
|
410
|
+
if (rows.length === 0)
|
|
411
|
+
return;
|
|
412
|
+
const targetsByAgent = new Map();
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
db.transaction(() => {
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
const hostKey = `conversation:${row.conversationId}:${row.agentType}`;
|
|
417
|
+
db.prepare(`DELETE FROM node_dispatch_queue
|
|
418
|
+
WHERE host_key = ?
|
|
419
|
+
OR session_key = ?
|
|
420
|
+
OR conversation_id = ?`).run(hostKey, row.sessionKey, row.conversationId);
|
|
421
|
+
db.prepare(`DELETE FROM node_host_snapshots
|
|
422
|
+
WHERE node_id = ?
|
|
423
|
+
AND (host_key = ? OR session_key = ?)`).run(params.nodeId, hostKey, row.sessionKey);
|
|
424
|
+
db.prepare(`DELETE FROM conversation_prompt_queue WHERE conversation_id = ?`).run(row.conversationId);
|
|
425
|
+
db.prepare(`UPDATE conversations
|
|
426
|
+
SET status = CASE
|
|
427
|
+
WHEN status IN ('queued', 'active', 'recovering', 'awaiting_approval') THEN 'idle'
|
|
428
|
+
ELSE status
|
|
429
|
+
END
|
|
430
|
+
WHERE id = ?`).run(row.conversationId);
|
|
431
|
+
db.prepare(`UPDATE runs
|
|
432
|
+
SET ended_at = COALESCE(ended_at, ?),
|
|
433
|
+
stop_reason = COALESCE(stop_reason, 'error'),
|
|
434
|
+
error = COALESCE(error, 'Platform agent instance is disabled or deleted.')
|
|
435
|
+
WHERE session_key = ?
|
|
436
|
+
AND ended_at IS NULL`).run(now, row.sessionKey);
|
|
437
|
+
const group = targetsByAgent.get(row.agentId) ?? { hostKeys: new Set(), sessionKeys: new Set() };
|
|
438
|
+
group.hostKeys.add(hostKey);
|
|
439
|
+
group.sessionKeys.add(row.sessionKey);
|
|
440
|
+
targetsByAgent.set(row.agentId, group);
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
443
|
+
if (!params.agentRuntimeCleanupBroker)
|
|
444
|
+
return;
|
|
445
|
+
for (const [agentId, target] of targetsByAgent.entries()) {
|
|
446
|
+
try {
|
|
447
|
+
await params.agentRuntimeCleanupBroker.cleanupAgentRuntime({
|
|
448
|
+
nodeId: params.nodeId,
|
|
449
|
+
agentId,
|
|
450
|
+
hostKeys: [...target.hostKeys],
|
|
451
|
+
sessionKeys: [...target.sessionKeys],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
log.warn('[node-ws] disabled platform runtime cleanup during node register failed', {
|
|
456
|
+
nodeId: params.nodeId,
|
|
457
|
+
agentId,
|
|
458
|
+
error: String(error?.message ?? error),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function recordAssetMaterialized(db, params) {
|
|
464
|
+
db.prepare(`INSERT INTO asset_materializations(
|
|
465
|
+
asset_id, agent_id, conversation_id, node_id, node_process_instance_id,
|
|
466
|
+
host_key, host_instance_id, run_id, local_path, materialized_at, cleared_at
|
|
467
|
+
)
|
|
468
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
469
|
+
ON CONFLICT(asset_id, agent_id, conversation_id, node_id, node_process_instance_id, host_key, host_instance_id) DO UPDATE SET
|
|
470
|
+
run_id = excluded.run_id,
|
|
471
|
+
local_path = excluded.local_path,
|
|
472
|
+
materialized_at = excluded.materialized_at,
|
|
473
|
+
cleared_at = NULL`).run(params.assetId, params.agentId, params.conversationId, params.nodeId, params.nodeProcessInstanceId, params.hostKey, params.hostInstanceId, params.runId ?? null, params.localPath, params.materializedAt);
|
|
474
|
+
}
|
|
475
|
+
function upsertMaterializedHostSnapshot(db, params) {
|
|
476
|
+
const row = db.prepare(`SELECT session_key as sessionKey,
|
|
477
|
+
COALESCE(workspace_path, '') as workspaceRoot,
|
|
478
|
+
agent_type as agentType,
|
|
479
|
+
status
|
|
480
|
+
FROM conversations
|
|
481
|
+
WHERE id = ?`).get(params.conversationId);
|
|
482
|
+
if (!row)
|
|
483
|
+
return;
|
|
484
|
+
const hostState = row.status === 'failed' ? 'failed' : (row.status === 'idle' ? 'idle' : 'active');
|
|
485
|
+
db.prepare(`INSERT INTO node_host_snapshots(
|
|
486
|
+
node_id, host_key, host_instance_id, session_key, workspace_root, agent_type, state, current_run_id,
|
|
487
|
+
has_pending_approval, inbox_size, pending_dispatch_count, last_wake_at, last_sleep_at,
|
|
488
|
+
last_error, resumable, snapshot_at
|
|
489
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0, NULL, NULL, NULL, 1, ?)
|
|
490
|
+
ON CONFLICT(node_id, host_key) DO UPDATE SET
|
|
491
|
+
state = CASE
|
|
492
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.state
|
|
493
|
+
ELSE node_host_snapshots.state
|
|
494
|
+
END,
|
|
495
|
+
host_instance_id = excluded.host_instance_id,
|
|
496
|
+
session_key = excluded.session_key,
|
|
497
|
+
workspace_root = excluded.workspace_root,
|
|
498
|
+
agent_type = excluded.agent_type,
|
|
499
|
+
current_run_id = COALESCE(excluded.current_run_id, node_host_snapshots.current_run_id),
|
|
500
|
+
has_pending_approval = CASE
|
|
501
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.has_pending_approval
|
|
502
|
+
ELSE node_host_snapshots.has_pending_approval
|
|
503
|
+
END,
|
|
504
|
+
inbox_size = CASE
|
|
505
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.inbox_size
|
|
506
|
+
ELSE node_host_snapshots.inbox_size
|
|
507
|
+
END,
|
|
508
|
+
pending_dispatch_count = CASE
|
|
509
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.pending_dispatch_count
|
|
510
|
+
ELSE node_host_snapshots.pending_dispatch_count
|
|
511
|
+
END,
|
|
512
|
+
last_error = CASE
|
|
513
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.last_error
|
|
514
|
+
ELSE node_host_snapshots.last_error
|
|
515
|
+
END,
|
|
516
|
+
resumable = CASE
|
|
517
|
+
WHEN node_host_snapshots.host_instance_id != excluded.host_instance_id THEN excluded.resumable
|
|
518
|
+
ELSE node_host_snapshots.resumable
|
|
519
|
+
END,
|
|
520
|
+
snapshot_at = MAX(node_host_snapshots.snapshot_at, excluded.snapshot_at)
|
|
521
|
+
WHERE node_host_snapshots.host_instance_id = excluded.host_instance_id
|
|
522
|
+
OR node_host_snapshots.snapshot_at <= excluded.snapshot_at`).run(params.nodeId, params.hostKey, params.hostInstanceId, row.sessionKey, row.workspaceRoot, row.agentType, hostState, params.runId ?? null, row.status === 'awaiting_approval' ? 1 : 0, params.snapshotAt);
|
|
523
|
+
}
|
|
524
|
+
function markAssetMaterializationsClearedForHost(db, params) {
|
|
525
|
+
db.prepare(`UPDATE asset_materializations
|
|
526
|
+
SET cleared_at = COALESCE(cleared_at, ?)
|
|
527
|
+
WHERE node_id = ?
|
|
528
|
+
AND host_key = ?
|
|
529
|
+
AND host_instance_id = ?
|
|
530
|
+
AND cleared_at IS NULL`).run(params.clearedAt, params.nodeId, params.hostKey, params.hostInstanceId);
|
|
531
|
+
}
|
|
532
|
+
function markAssetMaterializationsClearedForNode(db, nodeId, clearedAt) {
|
|
533
|
+
db.prepare(`UPDATE asset_materializations
|
|
534
|
+
SET cleared_at = COALESCE(cleared_at, ?)
|
|
535
|
+
WHERE node_id = ?
|
|
536
|
+
AND cleared_at IS NULL`).run(clearedAt, nodeId);
|
|
537
|
+
}
|
|
538
|
+
function persistNodeRuntimeSnapshot(db, params) {
|
|
539
|
+
db.prepare(`INSERT INTO node_runtime_snapshots(
|
|
540
|
+
node_id, process_instance_id, workspace_root, terminal_backend_available, runtime_drivers_json, capabilities_json, snapshot_at
|
|
541
|
+
) VALUES(?, ?, ?, ?, ?, ?, ?)
|
|
542
|
+
ON CONFLICT(node_id) DO UPDATE SET
|
|
543
|
+
process_instance_id = excluded.process_instance_id,
|
|
544
|
+
workspace_root = excluded.workspace_root,
|
|
545
|
+
terminal_backend_available = excluded.terminal_backend_available,
|
|
546
|
+
runtime_drivers_json = excluded.runtime_drivers_json,
|
|
547
|
+
capabilities_json = excluded.capabilities_json,
|
|
548
|
+
snapshot_at = excluded.snapshot_at`).run(params.nodeId, params.processInstanceId ?? null, params.runtime.workspaceRoot, params.runtime.terminalBackendAvailable ? 1 : 0, JSON.stringify(params.runtime.runtimeDrivers), JSON.stringify(params.runtime.capabilities), params.snapshotAt);
|
|
549
|
+
}
|
|
550
|
+
function requiresMcpReplyContract(db, conversationId) {
|
|
551
|
+
const row = db
|
|
552
|
+
.prepare('SELECT agent_id as agentId FROM conversations WHERE id = ?')
|
|
553
|
+
.get(conversationId);
|
|
554
|
+
return Boolean(row?.agentId);
|
|
555
|
+
}
|
|
556
|
+
function isPlatformDebugRun(db, runId) {
|
|
557
|
+
const row = db.prepare(`SELECT activation_metadata_json as activationMetadataJson
|
|
558
|
+
FROM run_debug_inputs
|
|
559
|
+
WHERE run_id = ?`).get(runId);
|
|
560
|
+
if (!row?.activationMetadataJson)
|
|
561
|
+
return false;
|
|
562
|
+
try {
|
|
563
|
+
const parsed = JSON.parse(row.activationMetadataJson);
|
|
564
|
+
const legacyRole = parsed.platformRun?.role ?? parsed.role;
|
|
565
|
+
if ((legacyRole === 'suggested_planner' || legacyRole === 'library_curator')
|
|
566
|
+
&& parsed.platformRun?.mode === 'debug') {
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
return (typeof parsed.platformRun?.definitionId === 'string'
|
|
570
|
+
&& typeof parsed.platformRun?.workflowType === 'string'
|
|
571
|
+
&& parsed.platformRun?.mode === 'debug');
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function hasRunFinalReplyMessage(db, runId) {
|
|
578
|
+
const row = db
|
|
579
|
+
.prepare(`SELECT COUNT(1) as count
|
|
580
|
+
FROM channel_messages
|
|
581
|
+
WHERE run_id = ?
|
|
582
|
+
AND sender_type = 'agent'
|
|
583
|
+
AND message_kind = 'final'`)
|
|
584
|
+
.get(runId);
|
|
585
|
+
return (row?.count ?? 0) > 0;
|
|
586
|
+
}
|
|
587
|
+
function getRunFinalReplyMessage(db, runId) {
|
|
588
|
+
const row = db.prepare(`SELECT message_id as messageId,
|
|
589
|
+
sender_id as senderId,
|
|
590
|
+
sender_name as senderName,
|
|
591
|
+
content,
|
|
592
|
+
seq,
|
|
593
|
+
created_at as createdAt,
|
|
594
|
+
message_source as messageSource,
|
|
595
|
+
tool_ids as toolIds
|
|
596
|
+
FROM channel_messages
|
|
597
|
+
WHERE run_id = ?
|
|
598
|
+
AND sender_type = 'agent'
|
|
599
|
+
AND message_kind = 'final'
|
|
600
|
+
AND deleted_at IS NULL
|
|
601
|
+
ORDER BY created_at DESC, seq DESC
|
|
602
|
+
LIMIT 1`).get(runId);
|
|
603
|
+
return row ?? null;
|
|
604
|
+
}
|
|
605
|
+
function normalizeWorkspaceToolPublishFallbackText(value) {
|
|
606
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
607
|
+
}
|
|
608
|
+
function buildWorkspaceToolPublishFallbackContent(toolRows) {
|
|
609
|
+
if (toolRows.length === 0) {
|
|
610
|
+
return 'Published the workspace tool.';
|
|
611
|
+
}
|
|
612
|
+
if (toolRows.length === 1) {
|
|
613
|
+
const tool = toolRows[0];
|
|
614
|
+
const label = normalizeWorkspaceToolPublishFallbackText(tool.name) || normalizeWorkspaceToolPublishFallbackText(tool.slug) || 'Workspace Tool';
|
|
615
|
+
const description = normalizeWorkspaceToolPublishFallbackText(tool.description);
|
|
616
|
+
const suffix = description ? ` ${description}` : '';
|
|
617
|
+
return `Published Workspace Tool: ${label}.${suffix}`.slice(0, 800);
|
|
618
|
+
}
|
|
619
|
+
const labels = toolRows
|
|
620
|
+
.map((tool) => normalizeWorkspaceToolPublishFallbackText(tool.name) || normalizeWorkspaceToolPublishFallbackText(tool.slug))
|
|
621
|
+
.filter(Boolean);
|
|
622
|
+
return `Published Workspace Tools: ${labels.join(', ')}.`.slice(0, 800);
|
|
623
|
+
}
|
|
624
|
+
function parseWorkspaceToolIdsJson(value) {
|
|
625
|
+
if (!value)
|
|
626
|
+
return [];
|
|
627
|
+
try {
|
|
628
|
+
const parsed = JSON.parse(value);
|
|
629
|
+
if (!Array.isArray(parsed))
|
|
630
|
+
return [];
|
|
631
|
+
const toolIds = [];
|
|
632
|
+
const seen = new Set();
|
|
633
|
+
for (const rawToolId of parsed) {
|
|
634
|
+
const toolId = typeof rawToolId === 'string' ? rawToolId.trim() : '';
|
|
635
|
+
if (!toolId || seen.has(toolId))
|
|
636
|
+
continue;
|
|
637
|
+
seen.add(toolId);
|
|
638
|
+
toolIds.push(toolId);
|
|
639
|
+
}
|
|
640
|
+
return toolIds;
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function consumePendingWorkspaceToolAttachments(params) {
|
|
647
|
+
const toolIds = [...new Set(params.toolIds.map((toolId) => toolId.trim()).filter(Boolean))];
|
|
648
|
+
if (toolIds.length === 0)
|
|
649
|
+
return 0;
|
|
650
|
+
const placeholders = toolIds.map(() => '?').join(', ');
|
|
651
|
+
const result = params.db.prepare(`DELETE FROM run_pending_tool_attachments
|
|
652
|
+
WHERE run_id = ?
|
|
653
|
+
AND conversation_id = ?
|
|
654
|
+
AND agent_id = ?
|
|
655
|
+
AND tool_id IN (${placeholders})`).run(params.runId, params.conversationId, params.agentId, ...toolIds);
|
|
656
|
+
return Number(result.changes ?? 0);
|
|
657
|
+
}
|
|
658
|
+
function persistPendingWorkspaceToolPublishFallback(params) {
|
|
659
|
+
const emptyResult = { emittedCount: 0, updatedCount: 0, toolIds: [] };
|
|
660
|
+
if (!requiresMcpReplyContract(params.db, params.conversationId))
|
|
661
|
+
return emptyResult;
|
|
662
|
+
const context = getFallbackMessageContext(params.db, params.conversationId, params.manager.getConfig().humanUserName);
|
|
663
|
+
if (!context)
|
|
664
|
+
return emptyResult;
|
|
665
|
+
const toolRows = params.db.prepare(`SELECT pending.tool_id as toolId,
|
|
666
|
+
tools.name as name,
|
|
667
|
+
tools.description as description,
|
|
668
|
+
tools.slug as slug,
|
|
669
|
+
tools.user_id as userId
|
|
670
|
+
FROM run_pending_tool_attachments pending
|
|
671
|
+
JOIN workspace_tools tools ON tools.tool_id = pending.tool_id
|
|
672
|
+
WHERE pending.run_id = ?
|
|
673
|
+
AND pending.conversation_id = ?
|
|
674
|
+
AND pending.agent_id = ?
|
|
675
|
+
AND tools.agent_id = pending.agent_id
|
|
676
|
+
AND tools.deleted_at IS NULL
|
|
677
|
+
ORDER BY pending.created_at ASC`).all(params.runId, params.conversationId, context.agentId);
|
|
678
|
+
if (toolRows.length === 0)
|
|
679
|
+
return emptyResult;
|
|
680
|
+
const allToolIds = toolRows.map((row) => row.toolId);
|
|
681
|
+
const consumeToolIds = (toolIds) => consumePendingWorkspaceToolAttachments({
|
|
682
|
+
db: params.db,
|
|
683
|
+
runId: params.runId,
|
|
684
|
+
conversationId: params.conversationId,
|
|
685
|
+
agentId: context.agentId,
|
|
686
|
+
toolIds,
|
|
687
|
+
});
|
|
688
|
+
if (context.agentType !== 'codex_app_server'
|
|
689
|
+
|| !context.channelId.startsWith('dm:')
|
|
690
|
+
|| context.threadRootId
|
|
691
|
+
|| !context.userId) {
|
|
692
|
+
consumeToolIds(allToolIds);
|
|
693
|
+
return emptyResult;
|
|
694
|
+
}
|
|
695
|
+
const attachableToolRows = toolRows.filter((row) => row.userId === context.userId);
|
|
696
|
+
const attachableToolIds = new Set(attachableToolRows.map((row) => row.toolId));
|
|
697
|
+
const invalidToolIds = allToolIds.filter((toolId) => !attachableToolIds.has(toolId));
|
|
698
|
+
if (invalidToolIds.length > 0) {
|
|
699
|
+
consumeToolIds(invalidToolIds);
|
|
700
|
+
}
|
|
701
|
+
if (attachableToolRows.length === 0)
|
|
702
|
+
return emptyResult;
|
|
703
|
+
const toolIds = attachableToolRows.map((row) => row.toolId);
|
|
704
|
+
const existingFinal = getRunFinalReplyMessage(params.db, params.runId);
|
|
705
|
+
if (existingFinal) {
|
|
706
|
+
const mergedToolIds = parseWorkspaceToolIdsJson(existingFinal.toolIds);
|
|
707
|
+
const seenToolIds = new Set(mergedToolIds);
|
|
708
|
+
for (const toolId of toolIds) {
|
|
709
|
+
if (seenToolIds.has(toolId))
|
|
710
|
+
continue;
|
|
711
|
+
seenToolIds.add(toolId);
|
|
712
|
+
mergedToolIds.push(toolId);
|
|
713
|
+
}
|
|
714
|
+
consumeToolIds(toolIds);
|
|
715
|
+
if (JSON.stringify(mergedToolIds) === (existingFinal.toolIds ?? null)) {
|
|
716
|
+
return emptyResult;
|
|
717
|
+
}
|
|
718
|
+
const toolIdsJson = JSON.stringify(mergedToolIds);
|
|
719
|
+
params.db.prepare(`UPDATE channel_messages
|
|
720
|
+
SET tool_ids = ?
|
|
721
|
+
WHERE message_id = ?`).run(toolIdsJson, existingFinal.messageId);
|
|
722
|
+
params.broadcast(params.conversationId, {
|
|
723
|
+
type: 'channel.message',
|
|
724
|
+
message: {
|
|
725
|
+
id: existingFinal.messageId,
|
|
726
|
+
senderId: existingFinal.senderId,
|
|
727
|
+
senderName: existingFinal.senderName,
|
|
728
|
+
senderType: 'agent',
|
|
729
|
+
content: existingFinal.content,
|
|
730
|
+
createdAt: new Date(existingFinal.createdAt).toISOString(),
|
|
731
|
+
seq: existingFinal.seq,
|
|
732
|
+
runId: params.runId,
|
|
733
|
+
messageSource: existingFinal.messageSource ?? 'agent_send',
|
|
734
|
+
messageKind: 'final',
|
|
735
|
+
toolIds: mergedToolIds,
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
refreshConversationSummariesForConversationIds(params.db, (agentId) => params.manager.getAgent(agentId), [params.conversationId]);
|
|
739
|
+
return { emittedCount: 0, updatedCount: 1, toolIds: mergedToolIds };
|
|
740
|
+
}
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
const seq = allocateNextChannelMessageSeq(params.db, context.channelId);
|
|
743
|
+
const messageId = randomUUID();
|
|
744
|
+
const runEventSeqCutoff = getRunEventSeqCutoff(params.db, params.runId);
|
|
745
|
+
const content = buildWorkspaceToolPublishFallbackContent(attachableToolRows);
|
|
746
|
+
const toolIdsJson = JSON.stringify(toolIds);
|
|
747
|
+
params.db.prepare(`INSERT INTO channel_messages(message_id, channel_id, sender_id, sender_name, sender_type, target, content, seq, created_at, run_id, thread_root_id, message_kind, message_source, tool_ids, run_event_seq_cutoff)
|
|
748
|
+
VALUES(?, ?, ?, ?, 'agent', ?, ?, ?, ?, ?, ?, 'final', 'workspace_tool_publish_fallback', ?, ?)`).run(messageId, context.channelId, context.agentId, context.agentName, context.target, content, seq, now, params.runId, context.threadRootId, toolIdsJson, runEventSeqCutoff);
|
|
749
|
+
consumeToolIds(toolIds);
|
|
750
|
+
params.broadcast(params.conversationId, {
|
|
751
|
+
type: 'channel.message',
|
|
752
|
+
message: {
|
|
753
|
+
id: messageId,
|
|
754
|
+
senderId: context.agentId,
|
|
755
|
+
senderName: context.agentName,
|
|
756
|
+
senderType: 'agent',
|
|
757
|
+
content,
|
|
758
|
+
createdAt: new Date(now).toISOString(),
|
|
759
|
+
seq,
|
|
760
|
+
runId: params.runId,
|
|
761
|
+
messageSource: 'workspace_tool_publish_fallback',
|
|
762
|
+
messageKind: 'final',
|
|
763
|
+
toolIds,
|
|
764
|
+
...(context.threadRootId ? { threadRootId: context.threadRootId } : {}),
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
refreshConversationSummariesForConversationIds(params.db, (agentId) => params.manager.getAgent(agentId), [params.conversationId]);
|
|
768
|
+
return { emittedCount: 1, updatedCount: 0, toolIds };
|
|
769
|
+
}
|
|
770
|
+
function isCancelStopReason(stopReason) {
|
|
771
|
+
return Boolean(stopReason?.includes('cancel'));
|
|
772
|
+
}
|
|
773
|
+
function isWorkspaceToolPublishFallbackRecoverableRunEndError(error) {
|
|
774
|
+
return String(error ?? '').toLowerCase().includes('assistant text repair exceeded retry cap');
|
|
775
|
+
}
|
|
776
|
+
function normalizeComparisonText(text) {
|
|
777
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
778
|
+
}
|
|
779
|
+
function stripLegacyStatusText(text) {
|
|
780
|
+
return text
|
|
781
|
+
.split('\n')
|
|
782
|
+
.filter((line) => !/^\s*(?:-\s*)?(?:\[(?:plan|task)\]\s*)?(?:plan|task)\s+updated\s*$/i.test(line))
|
|
783
|
+
.filter((line) => !/^\s*(?:-\s*)?\[(?:plan|task)\]\b/i.test(line))
|
|
784
|
+
.join('\n');
|
|
785
|
+
}
|
|
786
|
+
function stripFallbackNoiseTokens(text) {
|
|
787
|
+
return text
|
|
788
|
+
.replace(/\[(?:plan|task)\]\s*(?:plan|task)\s+updated/ig, '')
|
|
789
|
+
.replace(/(?:plan|task)\s+updated/ig, '');
|
|
790
|
+
}
|
|
791
|
+
function stripInternalReminderTail(text) {
|
|
792
|
+
return text
|
|
793
|
+
.replace(/\(?System reminder acknowledged[\s\S]*$/i, '')
|
|
794
|
+
.trim();
|
|
795
|
+
}
|
|
796
|
+
function cleanFallbackText(text) {
|
|
797
|
+
return stripInternalReminderTail(stripFallbackNoiseTokens(stripLegacyStatusText(text))).trim();
|
|
798
|
+
}
|
|
799
|
+
function isIgnorablePlanOrTaskFallbackText(text) {
|
|
800
|
+
const normalized = text.trim().toLowerCase();
|
|
801
|
+
return normalized === 'plan updated'
|
|
802
|
+
|| normalized === 'task updated'
|
|
803
|
+
|| normalized === '[plan] plan updated'
|
|
804
|
+
|| normalized === '[task] task updated';
|
|
805
|
+
}
|
|
806
|
+
function isIgnorableFallbackText(text) {
|
|
807
|
+
return text.includes(`Empty response: {'content':`)
|
|
808
|
+
|| isIgnorablePlanOrTaskFallbackText(text);
|
|
809
|
+
}
|
|
810
|
+
function hasSubstantiveFallbackText(text) {
|
|
811
|
+
return text.trim().length > 0;
|
|
812
|
+
}
|
|
813
|
+
function listRunAgentMessages(db, runId) {
|
|
814
|
+
return db.prepare(`SELECT content, created_at, channel_id, seq
|
|
815
|
+
FROM channel_messages
|
|
816
|
+
WHERE run_id = ?
|
|
817
|
+
AND sender_type = 'agent'
|
|
818
|
+
ORDER BY created_at ASC, seq ASC`).all(runId);
|
|
819
|
+
}
|
|
820
|
+
function listRunDeltaEvents(db, runId) {
|
|
821
|
+
const deltas = [];
|
|
822
|
+
for (const row of iterateParsedNodeEvents(db, {
|
|
823
|
+
runId,
|
|
824
|
+
types: ['content.delta'],
|
|
825
|
+
})) {
|
|
826
|
+
if (row.event?.type === 'content.delta') {
|
|
827
|
+
deltas.push({
|
|
828
|
+
createdAt: row.createdAt,
|
|
829
|
+
seq: row.seq,
|
|
830
|
+
text: row.event.text,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return deltas;
|
|
835
|
+
}
|
|
836
|
+
function collectFallbackSegments(db, runId) {
|
|
837
|
+
const replyRows = listRunAgentMessages(db, runId);
|
|
838
|
+
const deltaRows = listRunDeltaEvents(db, runId);
|
|
839
|
+
if (deltaRows.length === 0)
|
|
840
|
+
return [];
|
|
841
|
+
const timeline = [
|
|
842
|
+
...replyRows.map((row, index) => ({ type: 'message', createdAt: row.created_at, order: index })),
|
|
843
|
+
...deltaRows.map((row, index) => ({ type: 'delta', createdAt: row.createdAt, order: index, text: row.text, seq: row.seq })),
|
|
844
|
+
].sort((a, b) => {
|
|
845
|
+
if (a.createdAt !== b.createdAt)
|
|
846
|
+
return a.createdAt - b.createdAt;
|
|
847
|
+
if (a.type !== b.type)
|
|
848
|
+
return a.type === 'delta' ? -1 : 1;
|
|
849
|
+
return a.order - b.order;
|
|
850
|
+
});
|
|
851
|
+
let buffer = '';
|
|
852
|
+
const rawSegments = [];
|
|
853
|
+
const flushBuffer = () => {
|
|
854
|
+
if (!buffer)
|
|
855
|
+
return;
|
|
856
|
+
rawSegments.push(buffer);
|
|
857
|
+
buffer = '';
|
|
858
|
+
};
|
|
859
|
+
for (const item of timeline) {
|
|
860
|
+
if (item.type === 'delta') {
|
|
861
|
+
buffer += item.text;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
flushBuffer();
|
|
865
|
+
}
|
|
866
|
+
flushBuffer();
|
|
867
|
+
const existingNormalized = new Set(replyRows.map((row) => normalizeComparisonText(cleanFallbackText(row.content))).filter(Boolean));
|
|
868
|
+
const emittedNormalized = new Set();
|
|
869
|
+
return rawSegments
|
|
870
|
+
.map((segment) => cleanFallbackText(segment))
|
|
871
|
+
.filter((segment) => hasSubstantiveFallbackText(segment))
|
|
872
|
+
.filter((segment) => !isIgnorableFallbackText(segment))
|
|
873
|
+
.filter((segment) => {
|
|
874
|
+
const normalized = normalizeComparisonText(segment);
|
|
875
|
+
if (!normalized)
|
|
876
|
+
return false;
|
|
877
|
+
if (existingNormalized.has(normalized) || emittedNormalized.has(normalized))
|
|
878
|
+
return false;
|
|
879
|
+
emittedNormalized.add(normalized);
|
|
880
|
+
return true;
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function getFallbackMessageContext(db, conversationId, humanUserName) {
|
|
884
|
+
const row = db.prepare(`SELECT c.id as conversationId,
|
|
885
|
+
c.agent_id as agentId,
|
|
886
|
+
c.channel_id as channelId,
|
|
887
|
+
c.thread_kind as threadKind,
|
|
888
|
+
c.is_primary_thread as isPrimaryThread,
|
|
889
|
+
c.thread_root_id as threadRootId,
|
|
890
|
+
c.user_id as userId,
|
|
891
|
+
c.reply_target as replyTarget,
|
|
892
|
+
ch.name as channelName,
|
|
893
|
+
a.name as agentName,
|
|
894
|
+
a.agent_type as agentType
|
|
895
|
+
FROM conversations c
|
|
896
|
+
JOIN agents a ON a.agent_id = c.agent_id
|
|
897
|
+
LEFT JOIN channels ch ON ch.channel_id = c.channel_id
|
|
898
|
+
WHERE c.id = ?`).get(conversationId);
|
|
899
|
+
if (!row?.agentId)
|
|
900
|
+
return null;
|
|
901
|
+
const target = resolveConversationReplyTarget(db, conversationId, humanUserName)
|
|
902
|
+
?? `${`#${row.channelName ?? row.channelId}`}${row.threadRootId ? `:${row.threadRootId}` : ''}`;
|
|
903
|
+
return {
|
|
904
|
+
agentId: row.agentId,
|
|
905
|
+
agentName: row.agentName,
|
|
906
|
+
agentType: row.agentType,
|
|
907
|
+
channelId: row.threadKind === 'direct' ? `dm:${row.agentId}` : row.channelId,
|
|
908
|
+
target,
|
|
909
|
+
threadRootId: row.threadRootId ?? null,
|
|
910
|
+
userId: row.threadKind === 'direct'
|
|
911
|
+
? row.userId?.trim() || resolveDirectUserIdFromReplyTarget(db, row.replyTarget)
|
|
912
|
+
: row.userId ?? null,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function getPlanTextFromApprovalRequest(msg) {
|
|
916
|
+
const input = msg.input && typeof msg.input === 'object' && !Array.isArray(msg.input)
|
|
917
|
+
? msg.input
|
|
918
|
+
: null;
|
|
919
|
+
if (typeof input?.plan === 'string' && input.plan.trim())
|
|
920
|
+
return input.plan.trim();
|
|
921
|
+
const args = msg.toolArgs && typeof msg.toolArgs === 'object' && !Array.isArray(msg.toolArgs)
|
|
922
|
+
? msg.toolArgs
|
|
923
|
+
: null;
|
|
924
|
+
if (typeof args?.plan === 'string' && args.plan.trim())
|
|
925
|
+
return args.plan.trim();
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
function upsertPlanApprovalMessage(params) {
|
|
929
|
+
const context = getFallbackMessageContext(params.db, params.conversationId, params.manager.getConfig().humanUserName);
|
|
930
|
+
if (!context)
|
|
931
|
+
return;
|
|
932
|
+
const now = Date.now();
|
|
933
|
+
const existing = params.db.prepare(`SELECT message_id as messageId, seq, created_at as createdAt
|
|
934
|
+
FROM channel_messages
|
|
935
|
+
WHERE run_id = ?
|
|
936
|
+
AND message_source = ?
|
|
937
|
+
AND message_kind = 'plan'
|
|
938
|
+
ORDER BY created_at DESC, seq DESC
|
|
939
|
+
LIMIT 1`).get(params.runId, PLAN_APPROVAL_MESSAGE_SOURCE);
|
|
940
|
+
if (existing) {
|
|
941
|
+
const runEventSeqCutoff = getRunEventSeqCutoff(params.db, params.runId);
|
|
942
|
+
params.db.prepare(`UPDATE channel_messages
|
|
943
|
+
SET content = ?,
|
|
944
|
+
run_event_seq_cutoff = ?
|
|
945
|
+
WHERE message_id = ?`).run(params.planText, runEventSeqCutoff, existing.messageId);
|
|
946
|
+
const fileRefs = params.agentFileAccessService
|
|
947
|
+
? params.agentFileAccessService.getFileRefsForMessage({
|
|
948
|
+
agentId: context.agentId,
|
|
949
|
+
runId: params.runId,
|
|
950
|
+
content: params.planText,
|
|
951
|
+
createdAt: existing.createdAt,
|
|
952
|
+
runEventSeqCutoff,
|
|
953
|
+
})
|
|
954
|
+
: undefined;
|
|
955
|
+
params.broadcast(params.conversationId, {
|
|
956
|
+
type: 'channel.message',
|
|
957
|
+
message: {
|
|
958
|
+
id: existing.messageId,
|
|
959
|
+
senderId: context.agentId,
|
|
960
|
+
senderName: context.agentName,
|
|
961
|
+
senderType: 'agent',
|
|
962
|
+
content: params.planText,
|
|
963
|
+
createdAt: new Date(existing.createdAt).toISOString(),
|
|
964
|
+
seq: existing.seq,
|
|
965
|
+
runId: params.runId,
|
|
966
|
+
messageSource: PLAN_APPROVAL_MESSAGE_SOURCE,
|
|
967
|
+
messageKind: 'plan',
|
|
968
|
+
...(fileRefs ? { fileRefs } : {}),
|
|
969
|
+
...(context.threadRootId ? { threadRootId: context.threadRootId } : {}),
|
|
970
|
+
},
|
|
971
|
+
});
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
const seq = allocateNextChannelMessageSeq(params.db, context.channelId);
|
|
975
|
+
const messageId = randomUUID();
|
|
976
|
+
const runEventSeqCutoff = getRunEventSeqCutoff(params.db, params.runId);
|
|
977
|
+
params.db.prepare(`INSERT INTO channel_messages(message_id, channel_id, sender_id, sender_name, sender_type, target, content, seq, created_at, run_id, thread_root_id, message_kind, message_source, run_event_seq_cutoff)
|
|
978
|
+
VALUES(?, ?, ?, ?, 'agent', ?, ?, ?, ?, ?, ?, 'plan', ?, ?)`).run(messageId, context.channelId, context.agentId, context.agentName, context.target, params.planText, seq, now, params.runId, context.threadRootId, PLAN_APPROVAL_MESSAGE_SOURCE, runEventSeqCutoff);
|
|
979
|
+
const fileRefs = params.agentFileAccessService
|
|
980
|
+
? params.agentFileAccessService.getFileRefsForMessage({
|
|
981
|
+
agentId: context.agentId,
|
|
982
|
+
runId: params.runId,
|
|
983
|
+
content: params.planText,
|
|
984
|
+
createdAt: now,
|
|
985
|
+
runEventSeqCutoff,
|
|
986
|
+
})
|
|
987
|
+
: undefined;
|
|
988
|
+
params.broadcast(params.conversationId, {
|
|
989
|
+
type: 'channel.message',
|
|
990
|
+
message: {
|
|
991
|
+
id: messageId,
|
|
992
|
+
senderId: context.agentId,
|
|
993
|
+
senderName: context.agentName,
|
|
994
|
+
senderType: 'agent',
|
|
995
|
+
content: params.planText,
|
|
996
|
+
createdAt: new Date(now).toISOString(),
|
|
997
|
+
seq,
|
|
998
|
+
runId: params.runId,
|
|
999
|
+
messageSource: PLAN_APPROVAL_MESSAGE_SOURCE,
|
|
1000
|
+
messageKind: 'plan',
|
|
1001
|
+
...(fileRefs ? { fileRefs } : {}),
|
|
1002
|
+
...(context.threadRootId ? { threadRootId: context.threadRootId } : {}),
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
refreshConversationSummariesForConversationIds(params.db, (agentId) => params.manager.getAgent(agentId), [params.conversationId]);
|
|
1006
|
+
}
|
|
1007
|
+
function persistDeltaFallbackMessages(params) {
|
|
1008
|
+
const emptyResult = { emittedCount: 0, suppressedCount: 0 };
|
|
1009
|
+
if (!requiresMcpReplyContract(params.db, params.conversationId))
|
|
1010
|
+
return emptyResult;
|
|
1011
|
+
const platformDebugRun = isPlatformDebugRun(params.db, params.runId);
|
|
1012
|
+
const context = getFallbackMessageContext(params.db, params.conversationId, params.manager.getConfig().humanUserName);
|
|
1013
|
+
if (!context)
|
|
1014
|
+
return emptyResult;
|
|
1015
|
+
const boundTask = context.threadRootId
|
|
1016
|
+
? getBoundTaskForThread(params.db, {
|
|
1017
|
+
channelId: context.channelId,
|
|
1018
|
+
threadRootId: context.threadRootId,
|
|
1019
|
+
})
|
|
1020
|
+
: undefined;
|
|
1021
|
+
if (platformDebugRun && hasRunFinalReplyMessage(params.db, params.runId)) {
|
|
1022
|
+
return {
|
|
1023
|
+
emittedCount: 0,
|
|
1024
|
+
suppressedCount: 0,
|
|
1025
|
+
suppressedReason: 'final_reply',
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
if (!platformDebugRun && (context.agentType === 'codex_app_server' || context.agentType === 'claude_sdk')) {
|
|
1029
|
+
return {
|
|
1030
|
+
emittedCount: 0,
|
|
1031
|
+
suppressedCount: 0,
|
|
1032
|
+
suppressedReason: context.agentType,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
const segments = collectFallbackSegments(params.db, params.runId);
|
|
1036
|
+
if (segments.length === 0)
|
|
1037
|
+
return emptyResult;
|
|
1038
|
+
if (context.threadRootId
|
|
1039
|
+
&& boundTask
|
|
1040
|
+
&& !context.channelId.startsWith('dm:')
|
|
1041
|
+
&& (boundTask.status === 'in_review' || boundTask.status === 'done')) {
|
|
1042
|
+
return {
|
|
1043
|
+
emittedCount: 0,
|
|
1044
|
+
suppressedCount: segments.length,
|
|
1045
|
+
suppressedReason: 'closed_task_thread',
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
let createdAt = Date.now();
|
|
1049
|
+
let emittedCount = 0;
|
|
1050
|
+
for (const content of segments) {
|
|
1051
|
+
const seq = allocateNextChannelMessageSeq(params.db, context.channelId);
|
|
1052
|
+
const messageId = randomUUID();
|
|
1053
|
+
const runEventSeqCutoff = getRunEventSeqCutoff(params.db, params.runId);
|
|
1054
|
+
params.db.prepare(`INSERT INTO channel_messages(message_id, channel_id, sender_id, sender_name, sender_type, target, content, seq, created_at, run_id, thread_root_id, message_kind, message_source, run_event_seq_cutoff)
|
|
1055
|
+
VALUES(?, ?, ?, ?, 'agent', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(messageId, context.channelId, context.agentId, context.agentName, context.target, content, seq, createdAt, params.runId, context.threadRootId, null, 'delta_fallback', runEventSeqCutoff);
|
|
1056
|
+
const fileRefs = params.agentFileAccessService
|
|
1057
|
+
? params.agentFileAccessService.getFileRefsForMessage({
|
|
1058
|
+
agentId: context.agentId,
|
|
1059
|
+
runId: params.runId,
|
|
1060
|
+
content,
|
|
1061
|
+
createdAt,
|
|
1062
|
+
runEventSeqCutoff,
|
|
1063
|
+
})
|
|
1064
|
+
: [];
|
|
1065
|
+
params.broadcast(params.conversationId, {
|
|
1066
|
+
type: 'channel.message',
|
|
1067
|
+
message: {
|
|
1068
|
+
id: messageId,
|
|
1069
|
+
senderId: context.agentId,
|
|
1070
|
+
senderName: context.agentName,
|
|
1071
|
+
senderType: 'agent',
|
|
1072
|
+
content,
|
|
1073
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
1074
|
+
seq,
|
|
1075
|
+
runId: params.runId,
|
|
1076
|
+
...(fileRefs.length > 0 ? { fileRefs } : {}),
|
|
1077
|
+
messageSource: 'delta_fallback',
|
|
1078
|
+
...(context.threadRootId ? { threadRootId: context.threadRootId } : {}),
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
createdAt += 1;
|
|
1082
|
+
emittedCount += 1;
|
|
1083
|
+
}
|
|
1084
|
+
return { emittedCount, suppressedCount: 0 };
|
|
1085
|
+
}
|
|
1086
|
+
function isDreamDispatchRun(db, runId) {
|
|
1087
|
+
const row = db.prepare(`SELECT dispatch_mode as dispatchMode
|
|
1088
|
+
FROM run_debug_inputs
|
|
1089
|
+
WHERE run_id = ?
|
|
1090
|
+
LIMIT 1`).get(runId);
|
|
1091
|
+
return row?.dispatchMode === 'dream';
|
|
1092
|
+
}
|
|
1093
|
+
function getRunEndError(msg, db, conversationId, runId, wasHandedOff = false, expectedBootstrapStopReason) {
|
|
1094
|
+
if (msg.error)
|
|
1095
|
+
return msg.error;
|
|
1096
|
+
if (isDreamDispatchRun(db, runId))
|
|
1097
|
+
return null;
|
|
1098
|
+
if (isCancelStopReason(msg.stopReason)) {
|
|
1099
|
+
if (wasHandedOff || expectedBootstrapStopReason === DM_HANDOFF_BOOTSTRAP_STOP_REASON)
|
|
1100
|
+
return null;
|
|
1101
|
+
if (hasRunFinalReplyMessage(db, runId))
|
|
1102
|
+
return null;
|
|
1103
|
+
if (requiresMcpReplyContract(db, conversationId)) {
|
|
1104
|
+
return 'Agent run was cancelled before sending a final reply';
|
|
1105
|
+
}
|
|
1106
|
+
return 'Run cancelled before completion';
|
|
1107
|
+
}
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
function wasRunHandedOff(db, runId) {
|
|
1111
|
+
return Boolean(db.prepare(`SELECT 1
|
|
1112
|
+
FROM events
|
|
1113
|
+
WHERE run_id = ?
|
|
1114
|
+
AND method = ?
|
|
1115
|
+
LIMIT 1`).get(runId, DM_TASK_HANDOFF_EVENT_METHOD));
|
|
1116
|
+
}
|
|
1117
|
+
function updateConversationStatus(db, broadcast, conversationId, status) {
|
|
1118
|
+
db.prepare('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?')
|
|
1119
|
+
.run(status, Date.now(), conversationId);
|
|
1120
|
+
broadcast(conversationId, {
|
|
1121
|
+
type: 'conversation.status',
|
|
1122
|
+
conversationId,
|
|
1123
|
+
status,
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
function getWorkspaceRunContext(db, runId) {
|
|
1127
|
+
const row = db.prepare(`SELECT ws.workspace_session_id as workspaceSessionId,
|
|
1128
|
+
ws.session_key as sessionKey,
|
|
1129
|
+
r.ended_at as endedAt
|
|
1130
|
+
FROM runs r
|
|
1131
|
+
JOIN workspace_sessions ws ON ws.session_key = r.session_key
|
|
1132
|
+
WHERE r.run_id = ?
|
|
1133
|
+
LIMIT 1`).get(runId);
|
|
1134
|
+
return row ?? null;
|
|
1135
|
+
}
|
|
1136
|
+
function updateWorkspaceSessionStatus(db, workspaceSessionId, status) {
|
|
1137
|
+
const now = Date.now();
|
|
1138
|
+
db.prepare(`UPDATE workspace_sessions
|
|
1139
|
+
SET status = ?,
|
|
1140
|
+
updated_at = ?,
|
|
1141
|
+
last_active_at = ?
|
|
1142
|
+
WHERE workspace_session_id = ?`).run(status, now, now, workspaceSessionId);
|
|
1143
|
+
}
|
|
1144
|
+
function resolveWorkspaceSessionStatus(db, sessionKey, params) {
|
|
1145
|
+
if (hasOpenPendingWorkspaceApproval(db, sessionKey))
|
|
1146
|
+
return 'awaiting_approval';
|
|
1147
|
+
return getNextOpenWorkspaceRunId(db, sessionKey) ? params.openStatus : params.closedStatus;
|
|
1148
|
+
}
|
|
1149
|
+
function getNextOpenWorkspaceRunId(db, sessionKey) {
|
|
1150
|
+
const row = db.prepare(`SELECT run_id as runId
|
|
1151
|
+
FROM runs
|
|
1152
|
+
WHERE session_key = ?
|
|
1153
|
+
AND ended_at IS NULL
|
|
1154
|
+
ORDER BY started_at ASC, rowid ASC
|
|
1155
|
+
LIMIT 1`).get(sessionKey);
|
|
1156
|
+
return row?.runId ?? null;
|
|
1157
|
+
}
|
|
1158
|
+
function workspaceRunBelongsToCurrentNode(db, runId, currentNodeId) {
|
|
1159
|
+
const ownerNodeId = getWorkspaceRunDispatchNodeId(db, runId);
|
|
1160
|
+
return Boolean(ownerNodeId && currentNodeId && ownerNodeId === currentNodeId);
|
|
1161
|
+
}
|
|
1162
|
+
function listOpenWorkspaceRunsForNode(db, nodeId) {
|
|
1163
|
+
return db.prepare(`SELECT r.run_id as runId,
|
|
1164
|
+
ws.workspace_session_id as workspaceSessionId,
|
|
1165
|
+
ws.session_key as sessionKey
|
|
1166
|
+
FROM runs r
|
|
1167
|
+
JOIN workspace_sessions ws ON ws.session_key = r.session_key
|
|
1168
|
+
WHERE r.ended_at IS NULL
|
|
1169
|
+
AND EXISTS (
|
|
1170
|
+
SELECT 1
|
|
1171
|
+
FROM events owner
|
|
1172
|
+
WHERE owner.run_id = r.run_id
|
|
1173
|
+
AND owner.method = ?
|
|
1174
|
+
AND json_valid(owner.payload_json)
|
|
1175
|
+
AND json_extract(owner.payload_json, '$.nodeId') = ?
|
|
1176
|
+
)`).all(WORKSPACE_RUN_DISPATCH_METHOD, nodeId);
|
|
1177
|
+
}
|
|
1178
|
+
function buildBroadcastNodeEvent(event) {
|
|
1179
|
+
return event.type === 'tool.call'
|
|
1180
|
+
? { ...event, startedAt: event.startedAt ?? Date.now() }
|
|
1181
|
+
: event.type === 'tool.result'
|
|
1182
|
+
? { ...event, endedAt: event.endedAt ?? Date.now() }
|
|
1183
|
+
: event.type === 'run.usage'
|
|
1184
|
+
? { ...event, createdAt: event.createdAt ?? Date.now() }
|
|
1185
|
+
: event.type === 'runtime.compact'
|
|
1186
|
+
? { ...event, createdAt: event.createdAt ?? Date.now() }
|
|
1187
|
+
: event;
|
|
1188
|
+
}
|
|
1189
|
+
function broadcastPrimaryDmTaskRootStatusProjection(params) {
|
|
1190
|
+
void params.manager;
|
|
1191
|
+
const update = buildPrimaryDmTaskRootUpdateEventForThreadConversation(params.db, params.threadConversationId, params.taskThreadStatus);
|
|
1192
|
+
if (!update)
|
|
1193
|
+
return;
|
|
1194
|
+
params.broadcast(update.conversationId, update.event);
|
|
1195
|
+
}
|
|
1196
|
+
function finishConversationRun(params) {
|
|
1197
|
+
const endedAt = Date.now();
|
|
1198
|
+
finishRun(params.db, params.error
|
|
1199
|
+
? { runId: params.runId, error: params.error }
|
|
1200
|
+
: { runId: params.runId, stopReason: params.stopReason ?? 'end_turn' });
|
|
1201
|
+
if (!params.error) {
|
|
1202
|
+
markCodexRotationPendingIfDue(params.db, params.conversationId);
|
|
1203
|
+
}
|
|
1204
|
+
updateConversationStatus(params.db, params.broadcast, params.conversationId, params.error ? 'failed' : 'idle');
|
|
1205
|
+
broadcastPrimaryDmTaskRootStatusProjection({
|
|
1206
|
+
db: params.db,
|
|
1207
|
+
broadcast: params.broadcast,
|
|
1208
|
+
manager: params.manager,
|
|
1209
|
+
threadConversationId: params.conversationId,
|
|
1210
|
+
taskThreadStatus: params.error ? 'failed' : 'idle',
|
|
1211
|
+
});
|
|
1212
|
+
params.broadcast(params.conversationId, {
|
|
1213
|
+
type: 'turn.end',
|
|
1214
|
+
turnId: params.runId,
|
|
1215
|
+
stopReason: params.error ? 'error' : (params.stopReason ?? 'end_turn'),
|
|
1216
|
+
endedAt,
|
|
1217
|
+
error: params.error,
|
|
1218
|
+
});
|
|
1219
|
+
if (params.error) {
|
|
1220
|
+
params.broadcast(params.conversationId, { type: 'error', message: params.error });
|
|
1221
|
+
}
|
|
1222
|
+
scheduleTaskThreadFollowupForRun({
|
|
1223
|
+
db: params.db,
|
|
1224
|
+
conversationId: params.conversationId,
|
|
1225
|
+
runId: params.runId,
|
|
1226
|
+
stopReason: params.stopReason,
|
|
1227
|
+
error: params.error,
|
|
1228
|
+
});
|
|
1229
|
+
void params.onRunFinished?.({
|
|
1230
|
+
db: params.db,
|
|
1231
|
+
runId: params.runId,
|
|
1232
|
+
conversationId: params.conversationId,
|
|
1233
|
+
stopReason: params.stopReason,
|
|
1234
|
+
error: params.error,
|
|
1235
|
+
});
|
|
1236
|
+
void params.manager.onConversationSettled(params.conversationId);
|
|
1237
|
+
}
|
|
1238
|
+
export function handleNodeWebSocket(socket, registry, broadcast, db, manager, workspaceBroker, skillsBroker, codexTranscriptBroker, claudeTranscriptBroker, broadcastToChannel, terminalBroker, inspectBroker, gitBroker, claudeControlBroker, codexAppServerBroker, agentRuntimeCleanupBroker, workspaceSessionRuntimeService, onRunFinished, workspaceToolService, missionService, droidMissionBroker, nativeMissionWorkerBroker, soloSessionRelay) {
|
|
1239
|
+
const agentFileAccessService = workspaceBroker
|
|
1240
|
+
? new AgentFileAccessService({
|
|
1241
|
+
db,
|
|
1242
|
+
getAgentById: (agentId) => manager.getAgent(agentId),
|
|
1243
|
+
broker: workspaceBroker,
|
|
1244
|
+
})
|
|
1245
|
+
: null;
|
|
1246
|
+
let nodeId = null;
|
|
1247
|
+
let connectionProcessInstanceId = null;
|
|
1248
|
+
const isStaleNodeConnection = () => {
|
|
1249
|
+
if (!nodeId)
|
|
1250
|
+
return false;
|
|
1251
|
+
const activeEntry = typeof registry.getNode === 'function' ? registry.getNode(nodeId) : undefined;
|
|
1252
|
+
if (activeEntry?.ws && activeEntry.ws !== socket) {
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
if (!connectionProcessInstanceId)
|
|
1256
|
+
return false;
|
|
1257
|
+
const currentNode = db.prepare(`SELECT process_instance_id as processInstanceId
|
|
1258
|
+
FROM nodes
|
|
1259
|
+
WHERE node_id = ?`).get(nodeId);
|
|
1260
|
+
return currentNode?.processInstanceId !== connectionProcessInstanceId;
|
|
1261
|
+
};
|
|
1262
|
+
// Sequence counter per runId for node/event persistence
|
|
1263
|
+
const runSeq = new Map();
|
|
1264
|
+
const refreshHandoffSummaries = (handoff, fallbackConversationId) => {
|
|
1265
|
+
const conversationIds = new Set();
|
|
1266
|
+
if (fallbackConversationId)
|
|
1267
|
+
conversationIds.add(fallbackConversationId);
|
|
1268
|
+
if (handoff?.sourceConversationId)
|
|
1269
|
+
conversationIds.add(handoff.sourceConversationId);
|
|
1270
|
+
if (handoff?.targetConversationId)
|
|
1271
|
+
conversationIds.add(handoff.targetConversationId);
|
|
1272
|
+
if (conversationIds.size === 0)
|
|
1273
|
+
return;
|
|
1274
|
+
refreshConversationSummariesForConversationIds(db, (agentId) => manager.getAgent(agentId), conversationIds);
|
|
1275
|
+
};
|
|
1276
|
+
// Wrap broadcast: when a branch conversation's status changes, also forward to its channel stream
|
|
1277
|
+
const broadcastWithChannelForward = (conversationId, event) => {
|
|
1278
|
+
broadcast(conversationId, event);
|
|
1279
|
+
if (broadcastToChannel) {
|
|
1280
|
+
const conv = manager.getConversation(conversationId);
|
|
1281
|
+
if (conv?.channelId && conv.threadKind === 'branch' && !conv.channelId.startsWith('dm:')) {
|
|
1282
|
+
if (event.type === 'channel.message') {
|
|
1283
|
+
broadcastToChannel(conv.channelId, event);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (event.type === 'conversation.status') {
|
|
1287
|
+
broadcastToChannel(conv.channelId, {
|
|
1288
|
+
type: 'channel.conversation.status',
|
|
1289
|
+
channelId: conv.channelId,
|
|
1290
|
+
conversation: { ...conv, status: event.status },
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
const failNodeOwnedRuntimeState = (params) => {
|
|
1297
|
+
inspectBroker?.rejectPendingForNode(params.nodeId);
|
|
1298
|
+
gitBroker?.rejectPendingForNode(params.nodeId);
|
|
1299
|
+
workspaceBroker?.rejectPendingForNode(params.nodeId);
|
|
1300
|
+
terminalBroker?.handleNodeDisconnect(params.nodeId);
|
|
1301
|
+
skillsBroker?.rejectPendingForNode(params.nodeId);
|
|
1302
|
+
codexTranscriptBroker?.rejectPendingForNode(params.nodeId);
|
|
1303
|
+
claudeTranscriptBroker?.rejectPendingForNode(params.nodeId);
|
|
1304
|
+
claudeControlBroker?.rejectPendingForNode(params.nodeId);
|
|
1305
|
+
codexAppServerBroker?.rejectPendingForNode(params.nodeId);
|
|
1306
|
+
agentRuntimeCleanupBroker?.rejectPendingForNode(params.nodeId);
|
|
1307
|
+
workspaceSessionRuntimeService?.rejectPendingDispatchesForNode(params.nodeId, params.disconnectMessage);
|
|
1308
|
+
manager.rejectPendingDispatchesForNode(params.nodeId, params.disconnectMessage);
|
|
1309
|
+
manager.rejectPendingSteersForNode(params.nodeId, params.disconnectMessage);
|
|
1310
|
+
nativeMissionWorkerBroker?.rejectNodeAttempts(params.nodeId, new Error(params.disconnectMessage));
|
|
1311
|
+
if (params.unregisterNode) {
|
|
1312
|
+
registry.unregister(params.nodeId);
|
|
1313
|
+
}
|
|
1314
|
+
const now = Date.now();
|
|
1315
|
+
if (params.markNodeOffline) {
|
|
1316
|
+
db.prepare(`UPDATE nodes SET status='offline', last_seen=? WHERE node_id=?`)
|
|
1317
|
+
.run(now, params.nodeId);
|
|
1318
|
+
}
|
|
1319
|
+
markAssetMaterializationsClearedForNode(db, params.nodeId, now);
|
|
1320
|
+
const workspaceOpenRuns = listOpenWorkspaceRunsForNode(db, params.nodeId);
|
|
1321
|
+
const affectedWorkspaceSessions = new Map();
|
|
1322
|
+
for (const run of workspaceOpenRuns) {
|
|
1323
|
+
finishRun(db, { runId: run.runId, error: params.disconnectMessage });
|
|
1324
|
+
affectedWorkspaceSessions.set(run.workspaceSessionId, run.sessionKey);
|
|
1325
|
+
}
|
|
1326
|
+
if (affectedWorkspaceSessions.size > 0) {
|
|
1327
|
+
const stmt = db.prepare(`UPDATE workspace_sessions
|
|
1328
|
+
SET status = ?,
|
|
1329
|
+
updated_at = ?,
|
|
1330
|
+
last_active_at = ?
|
|
1331
|
+
WHERE workspace_session_id = ?`);
|
|
1332
|
+
for (const [workspaceSessionId, sessionKey] of affectedWorkspaceSessions.entries()) {
|
|
1333
|
+
stmt.run(resolveWorkspaceSessionStatus(db, sessionKey, {
|
|
1334
|
+
openStatus: 'queued',
|
|
1335
|
+
closedStatus: 'failed',
|
|
1336
|
+
}), now, now, workspaceSessionId);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
const affected = db.prepare(`SELECT id FROM conversations WHERE node_id = ? AND status != 'idle'`).all(params.nodeId);
|
|
1340
|
+
const openRuns = db.prepare(`SELECT r.run_id as runId, c.id as conversationId
|
|
1341
|
+
FROM runs r
|
|
1342
|
+
JOIN conversations c ON c.session_key = r.session_key
|
|
1343
|
+
WHERE c.node_id = ?
|
|
1344
|
+
AND r.ended_at IS NULL`).all(params.nodeId);
|
|
1345
|
+
const openRunConversationIds = new Set(openRuns.map((run) => run.conversationId));
|
|
1346
|
+
for (const run of openRuns) {
|
|
1347
|
+
finishRun(db, { runId: run.runId, error: params.disconnectMessage });
|
|
1348
|
+
db.prepare(`UPDATE conversations SET status='failed', updated_at=? WHERE id=? AND status != 'idle'`)
|
|
1349
|
+
.run(Date.now(), run.conversationId);
|
|
1350
|
+
broadcastWithChannelForward(run.conversationId, {
|
|
1351
|
+
type: 'turn.end',
|
|
1352
|
+
turnId: run.runId,
|
|
1353
|
+
stopReason: 'error',
|
|
1354
|
+
endedAt: Date.now(),
|
|
1355
|
+
error: params.disconnectMessage,
|
|
1356
|
+
});
|
|
1357
|
+
void onRunFinished?.({
|
|
1358
|
+
db,
|
|
1359
|
+
runId: run.runId,
|
|
1360
|
+
conversationId: run.conversationId,
|
|
1361
|
+
error: params.disconnectMessage,
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
for (const conv of affected) {
|
|
1365
|
+
if (!openRunConversationIds.has(conv.id)) {
|
|
1366
|
+
db.prepare(`UPDATE conversations SET status='failed', updated_at=? WHERE id=? AND status != 'idle'`)
|
|
1367
|
+
.run(Date.now(), conv.id);
|
|
1368
|
+
}
|
|
1369
|
+
broadcastWithChannelForward(conv.id, {
|
|
1370
|
+
type: 'conversation.status',
|
|
1371
|
+
conversationId: conv.id,
|
|
1372
|
+
status: 'failed',
|
|
1373
|
+
});
|
|
1374
|
+
broadcastPrimaryDmTaskRootStatusProjection({
|
|
1375
|
+
db,
|
|
1376
|
+
broadcast: broadcastWithChannelForward,
|
|
1377
|
+
manager,
|
|
1378
|
+
threadConversationId: conv.id,
|
|
1379
|
+
taskThreadStatus: 'failed',
|
|
1380
|
+
});
|
|
1381
|
+
broadcastWithChannelForward(conv.id, {
|
|
1382
|
+
type: 'error',
|
|
1383
|
+
message: params.disconnectMessage,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
socket.on('message', (raw) => {
|
|
1388
|
+
let msg;
|
|
1389
|
+
try {
|
|
1390
|
+
msg = JSON.parse(String(raw));
|
|
1391
|
+
}
|
|
1392
|
+
catch {
|
|
1393
|
+
log.warn('[node-ws] invalid JSON from node');
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (msg.type !== 'node.register' && isStaleNodeConnection()) {
|
|
1397
|
+
log.debug('[node-ws] ignoring message from stale node connection', {
|
|
1398
|
+
nodeId,
|
|
1399
|
+
messageType: msg.type,
|
|
1400
|
+
connectionProcessInstanceId,
|
|
1401
|
+
});
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
switch (msg.type) {
|
|
1405
|
+
case 'node.register': {
|
|
1406
|
+
nodeId = msg.nodeId;
|
|
1407
|
+
const now = Date.now();
|
|
1408
|
+
const processInstanceId = typeof msg.processInstanceId === 'string' && msg.processInstanceId.trim()
|
|
1409
|
+
? msg.processInstanceId.trim()
|
|
1410
|
+
: null;
|
|
1411
|
+
connectionProcessInstanceId = processInstanceId;
|
|
1412
|
+
const previousRegistryEntry = typeof registry.getNode === 'function' ? registry.getNode(msg.nodeId) : undefined;
|
|
1413
|
+
const existing = db.prepare(`SELECT node_id,
|
|
1414
|
+
status,
|
|
1415
|
+
process_instance_id as processInstanceId
|
|
1416
|
+
FROM nodes
|
|
1417
|
+
WHERE node_id = ?`).get(msg.nodeId);
|
|
1418
|
+
if (existing?.status === 'deleted') {
|
|
1419
|
+
log.warn(`[node-ws] connection rejected: node ${msg.nodeId} was deleted`);
|
|
1420
|
+
socket.close(4000, 'Machine has been deleted');
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const replacedProcessInstance = Boolean(existing?.processInstanceId
|
|
1424
|
+
&& processInstanceId
|
|
1425
|
+
&& existing.processInstanceId !== processInstanceId);
|
|
1426
|
+
registry.register({
|
|
1427
|
+
nodeId: msg.nodeId,
|
|
1428
|
+
hostname: msg.hostname,
|
|
1429
|
+
agentTypes: msg.agentTypes,
|
|
1430
|
+
version: msg.version,
|
|
1431
|
+
processInstanceId,
|
|
1432
|
+
terminalBackendAvailable: msg.terminalBackendAvailable,
|
|
1433
|
+
ws: socket,
|
|
1434
|
+
lastSeen: now,
|
|
1435
|
+
});
|
|
1436
|
+
// Persist to DB: update existing pre-provisioned row or insert new
|
|
1437
|
+
const agentTypesJson = JSON.stringify(msg.agentTypes);
|
|
1438
|
+
if (existing) {
|
|
1439
|
+
db.prepare(`UPDATE nodes SET hostname=?, agent_types_json=?, version=?, process_instance_id=?, status='online', last_seen=?,
|
|
1440
|
+
created_at=CASE WHEN created_at=0 THEN ? ELSE created_at END WHERE node_id=?`).run(msg.hostname, agentTypesJson, msg.version, processInstanceId, now, now, msg.nodeId);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
const pending = db.prepare(`SELECT node_id
|
|
1444
|
+
FROM nodes
|
|
1445
|
+
WHERE status = 'pending'
|
|
1446
|
+
AND display_name = ?
|
|
1447
|
+
ORDER BY provisioned_at DESC
|
|
1448
|
+
LIMIT 1`).get(msg.hostname);
|
|
1449
|
+
if (pending) {
|
|
1450
|
+
adoptProvisionedNodeIdentity(db, pending.node_id, msg.nodeId, msg.hostname, agentTypesJson, msg.version, processInstanceId, now);
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
db.prepare(`INSERT INTO nodes(node_id, hostname, agent_types_json, version, process_instance_id, status, last_seen, created_at, provisioned_at, display_name, env_var_keys)
|
|
1454
|
+
VALUES(?,?,?,?,?,'online',?,?,0,NULL,'[]')`).run(msg.nodeId, msg.hostname, agentTypesJson, msg.version, processInstanceId, now, now);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (replacedProcessInstance) {
|
|
1458
|
+
const disconnectMessage = `Agent node process restarted: ${msg.nodeId}`;
|
|
1459
|
+
failNodeOwnedRuntimeState({
|
|
1460
|
+
nodeId: msg.nodeId,
|
|
1461
|
+
disconnectMessage,
|
|
1462
|
+
unregisterNode: false,
|
|
1463
|
+
markNodeOffline: false,
|
|
1464
|
+
});
|
|
1465
|
+
if (previousRegistryEntry?.ws && previousRegistryEntry.ws !== socket) {
|
|
1466
|
+
try {
|
|
1467
|
+
previousRegistryEntry.ws.close();
|
|
1468
|
+
}
|
|
1469
|
+
catch {
|
|
1470
|
+
// Ignore failures from stale sockets during process replacement cleanup.
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
log.warn('[node-ws] replaced active node process and failed open runtime state', {
|
|
1474
|
+
nodeId: msg.nodeId,
|
|
1475
|
+
previousProcessInstanceId: existing?.processInstanceId ?? null,
|
|
1476
|
+
processInstanceId,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
void cleanupDisabledPlatformAgentRuntimeForNode(db, {
|
|
1480
|
+
nodeId: msg.nodeId,
|
|
1481
|
+
agentRuntimeCleanupBroker,
|
|
1482
|
+
}).finally(() => {
|
|
1483
|
+
try {
|
|
1484
|
+
socket.send(JSON.stringify({ type: 'node.ack', nodeId: msg.nodeId }));
|
|
1485
|
+
}
|
|
1486
|
+
catch {
|
|
1487
|
+
// Ignore ack send failures; the socket close path will reconcile node state.
|
|
1488
|
+
}
|
|
1489
|
+
log.info(`[node-ws] registered: ${msg.nodeId} (${msg.hostname})`);
|
|
1490
|
+
void workspaceToolService?.reconcileActiveTerminalsForNode(msg.nodeId).catch((error) => {
|
|
1491
|
+
log.warn('[node-ws] workspace tool active terminal reconcile failed', {
|
|
1492
|
+
nodeId: msg.nodeId,
|
|
1493
|
+
error: String(error?.message ?? error),
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
case 'node.heartbeat': {
|
|
1500
|
+
registry.heartbeat(msg.nodeId);
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
case 'node.hosts.snapshot': {
|
|
1504
|
+
registry.heartbeat(msg.nodeId);
|
|
1505
|
+
db.prepare(`UPDATE nodes SET last_seen = ?, status = 'online' WHERE node_id = ?`)
|
|
1506
|
+
.run(msg.generatedAt, msg.nodeId);
|
|
1507
|
+
const { validHosts, rejectedHosts } = validateNodeHostSnapshots(db, {
|
|
1508
|
+
nodeId: msg.nodeId,
|
|
1509
|
+
hosts: msg.hosts,
|
|
1510
|
+
});
|
|
1511
|
+
for (const rejected of rejectedHosts) {
|
|
1512
|
+
cleanupRejectedNodeHostSnapshot(db, {
|
|
1513
|
+
nodeId: msg.nodeId,
|
|
1514
|
+
host: rejected.host,
|
|
1515
|
+
conversationId: rejected.conversationId,
|
|
1516
|
+
reason: rejected.reason,
|
|
1517
|
+
agentRuntimeCleanupBroker,
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
persistNodeHostSnapshots(db, {
|
|
1521
|
+
nodeId: msg.nodeId,
|
|
1522
|
+
snapshotAt: msg.generatedAt,
|
|
1523
|
+
hosts: validHosts,
|
|
1524
|
+
});
|
|
1525
|
+
const activeConversationIds = new Set();
|
|
1526
|
+
for (const host of validHosts) {
|
|
1527
|
+
if (host.state === 'active' && host.currentRunId) {
|
|
1528
|
+
const status = getSnapshotConversationStatus(db, {
|
|
1529
|
+
runId: host.currentRunId,
|
|
1530
|
+
hasPendingApproval: host.hasPendingApproval,
|
|
1531
|
+
});
|
|
1532
|
+
const conversationIds = manager.handleNodeHostSnapshotCurrentRun({
|
|
1533
|
+
runId: host.currentRunId,
|
|
1534
|
+
nodeId: msg.nodeId,
|
|
1535
|
+
hostKey: host.hostKey,
|
|
1536
|
+
sessionKey: host.sessionKey,
|
|
1537
|
+
agentType: host.agentType,
|
|
1538
|
+
hasPendingApproval: host.hasPendingApproval,
|
|
1539
|
+
});
|
|
1540
|
+
for (const conversationId of conversationIds) {
|
|
1541
|
+
activeConversationIds.add(conversationId);
|
|
1542
|
+
broadcastWithChannelForward(conversationId, {
|
|
1543
|
+
type: 'conversation.status',
|
|
1544
|
+
conversationId,
|
|
1545
|
+
status,
|
|
1546
|
+
});
|
|
1547
|
+
if (status === 'awaiting_approval') {
|
|
1548
|
+
broadcastPrimaryDmTaskRootStatusProjection({
|
|
1549
|
+
db,
|
|
1550
|
+
broadcast: broadcastWithChannelForward,
|
|
1551
|
+
manager,
|
|
1552
|
+
threadConversationId: conversationId,
|
|
1553
|
+
taskThreadStatus: status,
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
const queuedConversationIds = db.prepare(`SELECT DISTINCT q.conversation_id as conversationId
|
|
1560
|
+
FROM conversation_prompt_queue q
|
|
1561
|
+
JOIN conversations c ON c.id = q.conversation_id
|
|
1562
|
+
WHERE c.node_id = ?`).all(msg.nodeId);
|
|
1563
|
+
for (const conversation of queuedConversationIds) {
|
|
1564
|
+
if (activeConversationIds.has(conversation.conversationId))
|
|
1565
|
+
continue;
|
|
1566
|
+
void manager.onConversationSettled(conversation.conversationId);
|
|
1567
|
+
}
|
|
1568
|
+
break;
|
|
1569
|
+
}
|
|
1570
|
+
case 'node.runtime.snapshot': {
|
|
1571
|
+
registry.heartbeat(msg.nodeId);
|
|
1572
|
+
db.prepare(`UPDATE nodes SET last_seen = ?, status = 'online' WHERE node_id = ?`)
|
|
1573
|
+
.run(msg.generatedAt, msg.nodeId);
|
|
1574
|
+
const snapshotProcessInstanceId = typeof msg.processInstanceId === 'string' && msg.processInstanceId.trim()
|
|
1575
|
+
? msg.processInstanceId.trim()
|
|
1576
|
+
: connectionProcessInstanceId;
|
|
1577
|
+
if (connectionProcessInstanceId
|
|
1578
|
+
&& snapshotProcessInstanceId
|
|
1579
|
+
&& snapshotProcessInstanceId !== connectionProcessInstanceId) {
|
|
1580
|
+
log.warn('[node-ws] ignoring runtime snapshot from mismatched node process', {
|
|
1581
|
+
nodeId: msg.nodeId,
|
|
1582
|
+
connectionProcessInstanceId,
|
|
1583
|
+
snapshotProcessInstanceId,
|
|
1584
|
+
});
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
persistNodeRuntimeSnapshot(db, {
|
|
1588
|
+
nodeId: msg.nodeId,
|
|
1589
|
+
processInstanceId: snapshotProcessInstanceId,
|
|
1590
|
+
snapshotAt: msg.generatedAt,
|
|
1591
|
+
runtime: msg.runtime,
|
|
1592
|
+
});
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
case 'asset.materialized': {
|
|
1596
|
+
if (!nodeId) {
|
|
1597
|
+
log.debug('[node-ws] ignoring asset.materialized before node registration', { assetId: msg.assetId });
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
const assetId = msg.assetId?.trim();
|
|
1601
|
+
const agentId = msg.agentId?.trim();
|
|
1602
|
+
const conversationId = msg.conversationId?.trim();
|
|
1603
|
+
const nodeProcessInstanceId = msg.nodeProcessInstanceId?.trim();
|
|
1604
|
+
const hostKey = msg.hostKey?.trim();
|
|
1605
|
+
const hostInstanceId = msg.hostInstanceId?.trim();
|
|
1606
|
+
const localPath = msg.localPath?.trim();
|
|
1607
|
+
if (!assetId
|
|
1608
|
+
|| !agentId
|
|
1609
|
+
|| !conversationId
|
|
1610
|
+
|| !nodeProcessInstanceId
|
|
1611
|
+
|| !hostKey
|
|
1612
|
+
|| !hostInstanceId
|
|
1613
|
+
|| !localPath
|
|
1614
|
+
|| !path.isAbsolute(localPath)) {
|
|
1615
|
+
log.warn('[node-ws] ignoring invalid asset.materialized payload', { assetId, agentId, conversationId, hostKey });
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
const currentNode = db.prepare(`SELECT process_instance_id as processInstanceId
|
|
1619
|
+
FROM nodes
|
|
1620
|
+
WHERE node_id = ?`).get(nodeId);
|
|
1621
|
+
if (currentNode?.processInstanceId !== nodeProcessInstanceId) {
|
|
1622
|
+
log.warn('[node-ws] ignoring asset.materialized for stale node process', {
|
|
1623
|
+
assetId,
|
|
1624
|
+
nodeId,
|
|
1625
|
+
nodeProcessInstanceId,
|
|
1626
|
+
currentProcessInstanceId: currentNode?.processInstanceId ?? null,
|
|
1627
|
+
});
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
const conversation = manager.getConversation(conversationId);
|
|
1631
|
+
if (!conversation || conversation.agentId !== agentId || conversation.nodeId !== nodeId) {
|
|
1632
|
+
log.warn('[node-ws] ignoring asset.materialized for mismatched conversation', {
|
|
1633
|
+
assetId,
|
|
1634
|
+
agentId,
|
|
1635
|
+
conversationId,
|
|
1636
|
+
nodeId,
|
|
1637
|
+
});
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
1640
|
+
const expectedHost = manager.getConversationHostKey(conversationId);
|
|
1641
|
+
if (!expectedHost || expectedHost.nodeId !== nodeId || expectedHost.hostKey !== hostKey) {
|
|
1642
|
+
log.warn('[node-ws] ignoring asset.materialized for mismatched host binding', {
|
|
1643
|
+
assetId,
|
|
1644
|
+
conversationId,
|
|
1645
|
+
nodeId,
|
|
1646
|
+
hostKey,
|
|
1647
|
+
expectedHostKey: expectedHost?.hostKey ?? null,
|
|
1648
|
+
expectedNodeId: expectedHost?.nodeId ?? null,
|
|
1649
|
+
});
|
|
1650
|
+
break;
|
|
1651
|
+
}
|
|
1652
|
+
const assetExists = !!db.prepare(`SELECT 1 FROM attachments WHERE id = ?`).get(assetId);
|
|
1653
|
+
if (!assetExists) {
|
|
1654
|
+
log.warn('[node-ws] ignoring asset.materialized for unknown asset', { assetId, conversationId });
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
if (msg.runId) {
|
|
1658
|
+
const runMatchesConversation = !!db.prepare(`SELECT 1
|
|
1659
|
+
FROM runs r
|
|
1660
|
+
JOIN conversations c ON c.session_key = r.session_key
|
|
1661
|
+
WHERE r.run_id = ?
|
|
1662
|
+
AND c.id = ?`).get(msg.runId, conversationId);
|
|
1663
|
+
if (!runMatchesConversation) {
|
|
1664
|
+
log.warn('[node-ws] ignoring asset.materialized for mismatched run', {
|
|
1665
|
+
assetId,
|
|
1666
|
+
runId: msg.runId,
|
|
1667
|
+
conversationId,
|
|
1668
|
+
});
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const materializedAt = Number.isFinite(msg.materializedAt) ? msg.materializedAt : Date.now();
|
|
1673
|
+
const materializedNodeId = nodeId;
|
|
1674
|
+
const materializedAssetId = assetId;
|
|
1675
|
+
const materializedAgentId = agentId;
|
|
1676
|
+
const materializedConversationId = conversationId;
|
|
1677
|
+
const materializedNodeProcessInstanceId = nodeProcessInstanceId;
|
|
1678
|
+
const materializedHostKey = hostKey;
|
|
1679
|
+
const materializedHostInstanceId = hostInstanceId;
|
|
1680
|
+
const materializedLocalPath = localPath;
|
|
1681
|
+
db.transaction(() => {
|
|
1682
|
+
upsertMaterializedHostSnapshot(db, {
|
|
1683
|
+
nodeId: materializedNodeId,
|
|
1684
|
+
hostKey: materializedHostKey,
|
|
1685
|
+
hostInstanceId: materializedHostInstanceId,
|
|
1686
|
+
conversationId: materializedConversationId,
|
|
1687
|
+
runId: msg.runId ?? null,
|
|
1688
|
+
snapshotAt: materializedAt,
|
|
1689
|
+
});
|
|
1690
|
+
recordAssetMaterialized(db, {
|
|
1691
|
+
assetId: materializedAssetId,
|
|
1692
|
+
agentId: materializedAgentId,
|
|
1693
|
+
conversationId: materializedConversationId,
|
|
1694
|
+
nodeId: materializedNodeId,
|
|
1695
|
+
nodeProcessInstanceId: materializedNodeProcessInstanceId,
|
|
1696
|
+
hostKey: materializedHostKey,
|
|
1697
|
+
hostInstanceId: materializedHostInstanceId,
|
|
1698
|
+
runId: msg.runId ?? null,
|
|
1699
|
+
localPath: materializedLocalPath,
|
|
1700
|
+
materializedAt,
|
|
1701
|
+
});
|
|
1702
|
+
})();
|
|
1703
|
+
break;
|
|
1704
|
+
}
|
|
1705
|
+
case 'asset.materialization.cleared': {
|
|
1706
|
+
if (!nodeId)
|
|
1707
|
+
break;
|
|
1708
|
+
const hostKey = msg.hostKey?.trim();
|
|
1709
|
+
const hostInstanceId = msg.hostInstanceId?.trim();
|
|
1710
|
+
const nodeProcessInstanceId = msg.nodeProcessInstanceId?.trim();
|
|
1711
|
+
if (!hostKey || !hostInstanceId || !nodeProcessInstanceId)
|
|
1712
|
+
break;
|
|
1713
|
+
const currentNode = db.prepare(`SELECT process_instance_id as processInstanceId
|
|
1714
|
+
FROM nodes
|
|
1715
|
+
WHERE node_id = ?`).get(nodeId);
|
|
1716
|
+
if (currentNode?.processInstanceId !== nodeProcessInstanceId) {
|
|
1717
|
+
log.debug('[node-ws] ignoring asset.materialization.cleared for stale node process', {
|
|
1718
|
+
nodeId,
|
|
1719
|
+
hostKey,
|
|
1720
|
+
hostInstanceId,
|
|
1721
|
+
});
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
markAssetMaterializationsClearedForHost(db, {
|
|
1725
|
+
nodeId,
|
|
1726
|
+
hostKey,
|
|
1727
|
+
hostInstanceId,
|
|
1728
|
+
clearedAt: Number.isFinite(msg.clearedAt) ? msg.clearedAt : Date.now(),
|
|
1729
|
+
});
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
case 'mission.run.accepted': {
|
|
1733
|
+
if (!nodeId)
|
|
1734
|
+
break;
|
|
1735
|
+
if (msg.runtimeProvider === 'bigbang_native') {
|
|
1736
|
+
nativeMissionWorkerBroker?.handleAccepted(msg, nodeId);
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
const attempt = droidMissionBroker?.handleAccepted(msg, nodeId);
|
|
1740
|
+
if (!attempt)
|
|
1741
|
+
break;
|
|
1742
|
+
if (attempt.interaction) {
|
|
1743
|
+
missionService?.recordMissionRuntimeEvent({
|
|
1744
|
+
missionId: msg.missionId,
|
|
1745
|
+
eventType: 'mission_interaction_accepted',
|
|
1746
|
+
source: 'agent_node',
|
|
1747
|
+
eventTime: Date.now(),
|
|
1748
|
+
payload: {
|
|
1749
|
+
missionDir: msg.missionDir,
|
|
1750
|
+
pid: msg.pid ?? null,
|
|
1751
|
+
requestId: msg.requestId,
|
|
1752
|
+
},
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
missionService?.markMissionAccepted({
|
|
1757
|
+
missionId: msg.missionId,
|
|
1758
|
+
missionDir: msg.missionDir,
|
|
1759
|
+
pid: msg.pid,
|
|
1760
|
+
requestId: msg.requestId,
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
break;
|
|
1764
|
+
}
|
|
1765
|
+
case 'mission.run.event': {
|
|
1766
|
+
if (!nodeId)
|
|
1767
|
+
break;
|
|
1768
|
+
if (msg.runtimeProvider === 'bigbang_native') {
|
|
1769
|
+
nativeMissionWorkerBroker?.handleEvent(msg, nodeId);
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
if (!droidMissionBroker?.verifyEvent(msg, nodeId))
|
|
1773
|
+
break;
|
|
1774
|
+
missionService?.recordMissionRuntimeEvent({
|
|
1775
|
+
missionId: msg.missionId,
|
|
1776
|
+
eventType: msg.eventType,
|
|
1777
|
+
source: msg.source,
|
|
1778
|
+
eventTime: msg.eventTime,
|
|
1779
|
+
payload: {
|
|
1780
|
+
...msg.payload,
|
|
1781
|
+
requestId: msg.requestId,
|
|
1782
|
+
},
|
|
1783
|
+
});
|
|
1784
|
+
break;
|
|
1785
|
+
}
|
|
1786
|
+
case 'mission.run.end': {
|
|
1787
|
+
if (!nodeId)
|
|
1788
|
+
break;
|
|
1789
|
+
if (msg.runtimeProvider === 'bigbang_native') {
|
|
1790
|
+
nativeMissionWorkerBroker?.handleEnd(msg, nodeId);
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
const attempt = droidMissionBroker?.handleEnd(msg, nodeId);
|
|
1794
|
+
if (!attempt)
|
|
1795
|
+
break;
|
|
1796
|
+
if (attempt.interaction) {
|
|
1797
|
+
const failed = Boolean(msg.error) || (msg.exitCode != null && msg.exitCode !== 0) || Boolean(msg.signal);
|
|
1798
|
+
missionService?.recordMissionRuntimeEvent({
|
|
1799
|
+
missionId: msg.missionId,
|
|
1800
|
+
eventType: failed ? 'mission_interaction_failed' : 'mission_interaction_completed',
|
|
1801
|
+
source: 'agent_node',
|
|
1802
|
+
eventTime: Date.now(),
|
|
1803
|
+
payload: {
|
|
1804
|
+
missionDir: msg.missionDir,
|
|
1805
|
+
factorySessionId: msg.factorySessionId ?? null,
|
|
1806
|
+
factoryArtifactDir: msg.factoryArtifactDir ?? null,
|
|
1807
|
+
exitCode: msg.exitCode ?? null,
|
|
1808
|
+
signal: msg.signal ?? null,
|
|
1809
|
+
error: msg.error ?? null,
|
|
1810
|
+
requestId: msg.requestId,
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
missionService?.finishDroidMission({
|
|
1816
|
+
missionId: msg.missionId,
|
|
1817
|
+
missionDir: msg.missionDir,
|
|
1818
|
+
factorySessionId: msg.factorySessionId,
|
|
1819
|
+
factoryArtifactDir: msg.factoryArtifactDir,
|
|
1820
|
+
exitCode: msg.exitCode,
|
|
1821
|
+
signal: msg.signal,
|
|
1822
|
+
error: msg.error,
|
|
1823
|
+
requestId: msg.requestId,
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
case 'mission.continuation.inspect.response': {
|
|
1829
|
+
if (!nodeId)
|
|
1830
|
+
break;
|
|
1831
|
+
droidMissionBroker?.handleContinuationInspectResponse(msg, nodeId);
|
|
1832
|
+
break;
|
|
1833
|
+
}
|
|
1834
|
+
case 'run.accepted': {
|
|
1835
|
+
const workspaceRun = getWorkspaceRunContext(db, msg.runId);
|
|
1836
|
+
if (workspaceRun) {
|
|
1837
|
+
if (workspaceRun.endedAt != null) {
|
|
1838
|
+
log.debug('[node-ws] ignoring late workspace run.accepted for ended run', {
|
|
1839
|
+
runId: msg.runId,
|
|
1840
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
1841
|
+
});
|
|
1842
|
+
break;
|
|
1843
|
+
}
|
|
1844
|
+
if (!workspaceRunBelongsToCurrentNode(db, msg.runId, nodeId)) {
|
|
1845
|
+
log.debug('[node-ws] ignoring workspace run.accepted from non-owner node', {
|
|
1846
|
+
runId: msg.runId,
|
|
1847
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
1848
|
+
nodeId,
|
|
1849
|
+
});
|
|
1850
|
+
break;
|
|
1851
|
+
}
|
|
1852
|
+
if (!workspaceSessionRuntimeService?.handleRunAccepted(msg.runId)) {
|
|
1853
|
+
log.debug('[node-ws] ignoring workspace run.accepted without pending waiter', {
|
|
1854
|
+
runId: msg.runId,
|
|
1855
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
break;
|
|
1859
|
+
}
|
|
1860
|
+
const accepted = manager.handleRunAccepted(msg.runId, msg.conversationId);
|
|
1861
|
+
if (!accepted) {
|
|
1862
|
+
log.debug('[node-ws] ignoring late run.accepted without pending waiter', {
|
|
1863
|
+
runId: msg.runId,
|
|
1864
|
+
conversationId: msg.conversationId,
|
|
1865
|
+
});
|
|
1866
|
+
break;
|
|
1867
|
+
}
|
|
1868
|
+
broadcastWithChannelForward(msg.conversationId, {
|
|
1869
|
+
type: 'conversation.status',
|
|
1870
|
+
conversationId: msg.conversationId,
|
|
1871
|
+
status: 'active',
|
|
1872
|
+
});
|
|
1873
|
+
const handoff = markConversationHandoffAccepted(db, {
|
|
1874
|
+
targetConversationId: msg.conversationId,
|
|
1875
|
+
targetRunId: msg.runId,
|
|
1876
|
+
});
|
|
1877
|
+
if (handoff) {
|
|
1878
|
+
insertHandoffRun(db, {
|
|
1879
|
+
handoffId: handoff.handoffId,
|
|
1880
|
+
runId: msg.runId,
|
|
1881
|
+
agentId: handoff.agentId,
|
|
1882
|
+
disposition: 'accepted',
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
refreshHandoffSummaries(handoff, msg.conversationId);
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
case 'run.steer.result': {
|
|
1889
|
+
const handled = manager.handleRunSteerResult(msg);
|
|
1890
|
+
if (!handled) {
|
|
1891
|
+
log.debug('[node-ws] ignoring late run.steer.result without pending waiter', {
|
|
1892
|
+
requestId: msg.requestId,
|
|
1893
|
+
runId: msg.runId,
|
|
1894
|
+
conversationId: msg.conversationId,
|
|
1895
|
+
ok: msg.ok,
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
case 'run.delivery.ack': {
|
|
1901
|
+
const handled = manager.handleRunDeliveryAck(msg);
|
|
1902
|
+
if (!handled) {
|
|
1903
|
+
log.debug('[node-ws] ignoring unknown run.delivery.ack', {
|
|
1904
|
+
deliveryId: msg.deliveryId,
|
|
1905
|
+
roundId: msg.roundId,
|
|
1906
|
+
runId: msg.runId,
|
|
1907
|
+
conversationId: msg.conversationId,
|
|
1908
|
+
mode: msg.mode,
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
break;
|
|
1912
|
+
}
|
|
1913
|
+
case 'run.event': {
|
|
1914
|
+
log.debug('[node-ws] run.event', { conversationId: msg.conversationId, eventType: msg.event.type });
|
|
1915
|
+
const workspaceRun = getWorkspaceRunContext(db, msg.runId);
|
|
1916
|
+
if (workspaceRun) {
|
|
1917
|
+
if (workspaceRun.endedAt != null) {
|
|
1918
|
+
log.debug('[node-ws] ignoring workspace run.event for ended run', {
|
|
1919
|
+
runId: msg.runId,
|
|
1920
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
1921
|
+
eventType: msg.event.type,
|
|
1922
|
+
});
|
|
1923
|
+
break;
|
|
1924
|
+
}
|
|
1925
|
+
if (!workspaceRunBelongsToCurrentNode(db, msg.runId, nodeId)) {
|
|
1926
|
+
log.debug('[node-ws] ignoring workspace run.event from non-owner node', {
|
|
1927
|
+
runId: msg.runId,
|
|
1928
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
1929
|
+
eventType: msg.event.type,
|
|
1930
|
+
nodeId,
|
|
1931
|
+
});
|
|
1932
|
+
break;
|
|
1933
|
+
}
|
|
1934
|
+
if (msg.event.type === 'conversation.status') {
|
|
1935
|
+
if (msg.event.status !== 'awaiting_approval') {
|
|
1936
|
+
promoteWorkspaceSentApprovalResponses(db, msg.runId);
|
|
1937
|
+
}
|
|
1938
|
+
updateWorkspaceSessionStatus(db, workspaceRun.workspaceSessionId, hasOpenPendingWorkspaceApproval(db, workspaceRun.sessionKey) ? 'awaiting_approval' : msg.event.status);
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
const broadcastEvent = buildBroadcastNodeEvent(msg.event);
|
|
1942
|
+
if (REPLAY_EVENT_TYPES.has(msg.event.type)) {
|
|
1943
|
+
if (isWorkspaceApprovalProgressEvent(broadcastEvent)) {
|
|
1944
|
+
promoteWorkspaceSentApprovalResponses(db, msg.runId);
|
|
1945
|
+
}
|
|
1946
|
+
const seq = nextNodeEventSeq(db, msg.runId, runSeq);
|
|
1947
|
+
runSeq.set(msg.runId, seq);
|
|
1948
|
+
appendNodeEvent(db, msg.runId, seq, broadcastEvent);
|
|
1949
|
+
if (isWorkspaceApprovalProgressEvent(broadcastEvent)) {
|
|
1950
|
+
updateWorkspaceSessionStatus(db, workspaceRun.workspaceSessionId, hasOpenPendingWorkspaceApproval(db, workspaceRun.sessionKey) ? 'awaiting_approval' : 'active');
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
if (msg.event.type === 'conversation.status') {
|
|
1956
|
+
db.prepare('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?')
|
|
1957
|
+
.run(msg.event.status, Date.now(), msg.conversationId);
|
|
1958
|
+
broadcastWithChannelForward(msg.conversationId, msg.event);
|
|
1959
|
+
break;
|
|
1960
|
+
}
|
|
1961
|
+
// Silently discard events for runs that no longer exist (deleted by reset/clear-chat)
|
|
1962
|
+
const runKnown = !!(db.prepare('SELECT 1 FROM runs WHERE run_id = ?').get(msg.runId));
|
|
1963
|
+
if (!runKnown) {
|
|
1964
|
+
log.debug('[node-ws] ignoring run.event for unknown/deleted run', { runId: msg.runId });
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
const broadcastEvent = buildBroadcastNodeEvent(msg.event);
|
|
1968
|
+
if (broadcastEvent.type === 'runtime.compact') {
|
|
1969
|
+
recordCodexCompactEvent(db, msg.conversationId, broadcastEvent);
|
|
1970
|
+
}
|
|
1971
|
+
broadcastWithChannelForward(msg.conversationId, broadcastEvent);
|
|
1972
|
+
if (broadcastEvent.type === 'run.steer.result') {
|
|
1973
|
+
const handled = manager.handleRunSteerResult({
|
|
1974
|
+
requestId: broadcastEvent.requestId,
|
|
1975
|
+
runId: msg.runId,
|
|
1976
|
+
conversationId: msg.conversationId,
|
|
1977
|
+
ok: broadcastEvent.ok,
|
|
1978
|
+
...(broadcastEvent.error ? { error: broadcastEvent.error } : {}),
|
|
1979
|
+
});
|
|
1980
|
+
if (!handled) {
|
|
1981
|
+
log.debug('[node-ws] ignoring run.event steer result without pending waiter', {
|
|
1982
|
+
requestId: broadcastEvent.requestId,
|
|
1983
|
+
runId: msg.runId,
|
|
1984
|
+
conversationId: msg.conversationId,
|
|
1985
|
+
ok: broadcastEvent.ok,
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
// Persist replay-worthy events to core DB immediately
|
|
1990
|
+
if (REPLAY_EVENT_TYPES.has(msg.event.type)) {
|
|
1991
|
+
const seq = nextNodeEventSeq(db, msg.runId, runSeq);
|
|
1992
|
+
runSeq.set(msg.runId, seq);
|
|
1993
|
+
appendNodeEvent(db, msg.runId, seq, broadcastEvent);
|
|
1994
|
+
if (nodeId) {
|
|
1995
|
+
agentFileAccessService?.recordEventAccesses({
|
|
1996
|
+
nodeId,
|
|
1997
|
+
conversationId: msg.conversationId,
|
|
1998
|
+
runId: msg.runId,
|
|
1999
|
+
event: broadcastEvent,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case 'run.end': {
|
|
2006
|
+
log.info('[node-ws] run.end', { runId: msg.runId, conversationId: msg.conversationId, error: msg.error ?? null });
|
|
2007
|
+
runSeq.delete(msg.runId);
|
|
2008
|
+
const workspaceRun = getWorkspaceRunContext(db, msg.runId);
|
|
2009
|
+
if (workspaceRun) {
|
|
2010
|
+
if (workspaceRun.endedAt != null) {
|
|
2011
|
+
log.debug('[node-ws] ignoring workspace run.end for already ended run', {
|
|
2012
|
+
runId: msg.runId,
|
|
2013
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2014
|
+
});
|
|
2015
|
+
break;
|
|
2016
|
+
}
|
|
2017
|
+
if (!workspaceRunBelongsToCurrentNode(db, msg.runId, nodeId)) {
|
|
2018
|
+
log.debug('[node-ws] ignoring workspace run.end from non-owner node', {
|
|
2019
|
+
runId: msg.runId,
|
|
2020
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2021
|
+
nodeId,
|
|
2022
|
+
});
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
promoteWorkspaceSentApprovalResponses(db, msg.runId);
|
|
2026
|
+
finishRun(db, msg.error
|
|
2027
|
+
? { runId: msg.runId, error: msg.error }
|
|
2028
|
+
: { runId: msg.runId, stopReason: msg.stopReason ?? 'end_turn' });
|
|
2029
|
+
updateWorkspaceSessionStatus(db, workspaceRun.workspaceSessionId, resolveWorkspaceSessionStatus(db, workspaceRun.sessionKey, {
|
|
2030
|
+
openStatus: 'queued',
|
|
2031
|
+
closedStatus: msg.error ? 'failed' : 'idle',
|
|
2032
|
+
}));
|
|
2033
|
+
break;
|
|
2034
|
+
}
|
|
2035
|
+
// Check if this run still exists in core's DB.
|
|
2036
|
+
// After reset/clear-chat the run rows are deleted — ignore stale run.end messages
|
|
2037
|
+
// so they don't overwrite the conversation status set by the reset operation.
|
|
2038
|
+
const runExists = !!(db
|
|
2039
|
+
.prepare('SELECT 1 FROM runs WHERE run_id = ?')
|
|
2040
|
+
.get(msg.runId));
|
|
2041
|
+
if (!runExists) {
|
|
2042
|
+
log.warn('[node-ws] ignoring run.end for unknown/deleted run', { runId: msg.runId });
|
|
2043
|
+
manager.rejectPendingSteersForRun(msg.runId, msg.conversationId, 'Run ended before active-turn steer was acknowledged.');
|
|
2044
|
+
void manager.onConversationSettled(msg.conversationId);
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
manager.rejectPendingSteersForRun(msg.runId, msg.conversationId, 'Run ended before active-turn steer was acknowledged.');
|
|
2048
|
+
manager.handleRunFinished(msg.runId, msg.conversationId, {
|
|
2049
|
+
error: msg.error ?? null,
|
|
2050
|
+
stopReason: msg.stopReason ?? null,
|
|
2051
|
+
});
|
|
2052
|
+
const handedOff = isCancelStopReason(msg.stopReason) && wasRunHandedOff(db, msg.runId);
|
|
2053
|
+
const expectedBootstrapStopReason = isCancelStopReason(msg.stopReason)
|
|
2054
|
+
? getExpectedBootstrapStopReason(db, msg.runId)
|
|
2055
|
+
: null;
|
|
2056
|
+
const runEndError = getRunEndError(msg, db, msg.conversationId, msg.runId, handedOff, expectedBootstrapStopReason);
|
|
2057
|
+
let handoff = null;
|
|
2058
|
+
if (msg.error) {
|
|
2059
|
+
handoff = markConversationHandoffFailed(db, {
|
|
2060
|
+
targetConversationId: msg.conversationId,
|
|
2061
|
+
targetRunId: msg.runId,
|
|
2062
|
+
error: runEndError,
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
else if (isCancelStopReason(msg.stopReason) && !handedOff && !expectedBootstrapStopReason) {
|
|
2066
|
+
handoff = markConversationHandoffCancelled(db, {
|
|
2067
|
+
targetConversationId: msg.conversationId,
|
|
2068
|
+
targetRunId: msg.runId,
|
|
2069
|
+
error: runEndError ?? null,
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
else if (runEndError) {
|
|
2073
|
+
handoff = markConversationHandoffFailed(db, {
|
|
2074
|
+
targetConversationId: msg.conversationId,
|
|
2075
|
+
targetRunId: msg.runId,
|
|
2076
|
+
error: runEndError,
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
else if (!handedOff) {
|
|
2080
|
+
handoff = markConversationHandoffCompleted(db, {
|
|
2081
|
+
targetConversationId: msg.conversationId,
|
|
2082
|
+
targetRunId: msg.runId,
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
if (handoff) {
|
|
2086
|
+
insertHandoffRun(db, {
|
|
2087
|
+
handoffId: handoff.handoffId,
|
|
2088
|
+
runId: msg.runId,
|
|
2089
|
+
agentId: handoff.agentId,
|
|
2090
|
+
disposition: handoff.status === 'failed'
|
|
2091
|
+
? 'failed'
|
|
2092
|
+
: handoff.status === 'cancelled'
|
|
2093
|
+
? 'cancelled'
|
|
2094
|
+
: 'completed',
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
if ((!msg.error || isWorkspaceToolPublishFallbackRecoverableRunEndError(msg.error))
|
|
2098
|
+
&& !isCancelStopReason(msg.stopReason)) {
|
|
2099
|
+
const toolPublishFallbackResult = persistPendingWorkspaceToolPublishFallback({
|
|
2100
|
+
db,
|
|
2101
|
+
conversationId: msg.conversationId,
|
|
2102
|
+
runId: msg.runId,
|
|
2103
|
+
broadcast: broadcastWithChannelForward,
|
|
2104
|
+
manager,
|
|
2105
|
+
});
|
|
2106
|
+
if (toolPublishFallbackResult.emittedCount > 0 || toolPublishFallbackResult.updatedCount > 0) {
|
|
2107
|
+
log.info('[node-ws] surfaced workspace tool publish fallback attachment', {
|
|
2108
|
+
conversationId: msg.conversationId,
|
|
2109
|
+
runId: msg.runId,
|
|
2110
|
+
emittedCount: toolPublishFallbackResult.emittedCount,
|
|
2111
|
+
updatedCount: toolPublishFallbackResult.updatedCount,
|
|
2112
|
+
toolIds: toolPublishFallbackResult.toolIds,
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
const fallbackResult = persistDeltaFallbackMessages({
|
|
2116
|
+
db,
|
|
2117
|
+
conversationId: msg.conversationId,
|
|
2118
|
+
runId: msg.runId,
|
|
2119
|
+
broadcast: broadcastWithChannelForward,
|
|
2120
|
+
manager,
|
|
2121
|
+
agentFileAccessService,
|
|
2122
|
+
});
|
|
2123
|
+
if (fallbackResult.emittedCount > 0) {
|
|
2124
|
+
log.info('[node-ws] emitted delta fallback messages', {
|
|
2125
|
+
conversationId: msg.conversationId,
|
|
2126
|
+
runId: msg.runId,
|
|
2127
|
+
count: fallbackResult.emittedCount,
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
if (fallbackResult.suppressedCount > 0) {
|
|
2131
|
+
log.info('[node-ws] suppressed delta fallback messages', {
|
|
2132
|
+
conversationId: msg.conversationId,
|
|
2133
|
+
runId: msg.runId,
|
|
2134
|
+
count: fallbackResult.suppressedCount,
|
|
2135
|
+
reason: fallbackResult.suppressedReason ?? 'unknown',
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
finishConversationRun({
|
|
2140
|
+
db,
|
|
2141
|
+
broadcast: broadcastWithChannelForward,
|
|
2142
|
+
manager,
|
|
2143
|
+
conversationId: msg.conversationId,
|
|
2144
|
+
runId: msg.runId,
|
|
2145
|
+
stopReason: handedOff ? 'handoff' : (expectedBootstrapStopReason ?? msg.stopReason),
|
|
2146
|
+
error: runEndError ?? undefined,
|
|
2147
|
+
onRunFinished,
|
|
2148
|
+
});
|
|
2149
|
+
clearTaskThreadReopenedRunBlock(msg.conversationId, msg.runId);
|
|
2150
|
+
refreshHandoffSummaries(handoff, msg.conversationId);
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
case 'run.debug.snapshot': {
|
|
2154
|
+
const workspaceRun = getWorkspaceRunContext(db, msg.runId);
|
|
2155
|
+
if (workspaceRun) {
|
|
2156
|
+
if (workspaceRun.endedAt != null) {
|
|
2157
|
+
log.debug('[node-ws] ignoring workspace run.debug.snapshot for ended run', {
|
|
2158
|
+
runId: msg.runId,
|
|
2159
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2160
|
+
});
|
|
2161
|
+
break;
|
|
2162
|
+
}
|
|
2163
|
+
if (workspaceRun.sessionKey !== msg.sessionKey) {
|
|
2164
|
+
log.debug('[node-ws] ignoring workspace run.debug.snapshot with mismatched session key', {
|
|
2165
|
+
runId: msg.runId,
|
|
2166
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2167
|
+
});
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
if (!workspaceRunBelongsToCurrentNode(db, msg.runId, nodeId)) {
|
|
2171
|
+
log.debug('[node-ws] ignoring workspace run.debug.snapshot from non-owner node', {
|
|
2172
|
+
runId: msg.runId,
|
|
2173
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2174
|
+
nodeId,
|
|
2175
|
+
});
|
|
2176
|
+
break;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
applyRunDebugSnapshot(db, {
|
|
2180
|
+
runId: msg.runId,
|
|
2181
|
+
conversationId: msg.conversationId,
|
|
2182
|
+
sessionKey: msg.sessionKey,
|
|
2183
|
+
acpSessionId: msg.acpSessionId,
|
|
2184
|
+
isFreshSession: msg.isFreshSession,
|
|
2185
|
+
isExact: msg.isExact,
|
|
2186
|
+
effectiveSystemPromptText: msg.effectiveSystemPromptText,
|
|
2187
|
+
effectiveContextText: msg.effectiveContextText,
|
|
2188
|
+
});
|
|
2189
|
+
break;
|
|
2190
|
+
}
|
|
2191
|
+
case 'permission.request': {
|
|
2192
|
+
const approvalEvent = {
|
|
2193
|
+
type: 'approval.request',
|
|
2194
|
+
requestId: msg.requestId,
|
|
2195
|
+
toolName: msg.toolName,
|
|
2196
|
+
toolArgs: msg.toolArgs,
|
|
2197
|
+
toolKind: msg.toolKind,
|
|
2198
|
+
...(msg.approvalKind ? { approvalKind: msg.approvalKind } : {}),
|
|
2199
|
+
...(msg.title ? { title: msg.title } : {}),
|
|
2200
|
+
...(msg.description ? { description: msg.description } : {}),
|
|
2201
|
+
...(msg.input !== undefined ? { input: msg.input } : {}),
|
|
2202
|
+
...(msg.actions?.length ? { actions: msg.actions } : {}),
|
|
2203
|
+
};
|
|
2204
|
+
const workspaceRun = getWorkspaceRunContext(db, msg.runId);
|
|
2205
|
+
if (workspaceRun) {
|
|
2206
|
+
if (workspaceRun.endedAt != null) {
|
|
2207
|
+
log.debug('[node-ws] ignoring workspace permission.request for ended run', {
|
|
2208
|
+
runId: msg.runId,
|
|
2209
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2210
|
+
requestId: msg.requestId,
|
|
2211
|
+
});
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
if (!workspaceRunBelongsToCurrentNode(db, msg.runId, nodeId)) {
|
|
2215
|
+
log.debug('[node-ws] ignoring workspace permission.request from non-owner node', {
|
|
2216
|
+
runId: msg.runId,
|
|
2217
|
+
workspaceSessionId: workspaceRun.workspaceSessionId,
|
|
2218
|
+
requestId: msg.requestId,
|
|
2219
|
+
nodeId,
|
|
2220
|
+
});
|
|
2221
|
+
break;
|
|
2222
|
+
}
|
|
2223
|
+
updateWorkspaceSessionStatus(db, workspaceRun.workspaceSessionId, 'awaiting_approval');
|
|
2224
|
+
const runKnown = !!db.prepare('SELECT 1 FROM runs WHERE run_id = ?').get(msg.runId);
|
|
2225
|
+
if (runKnown) {
|
|
2226
|
+
const seq = nextNodeEventSeq(db, msg.runId, runSeq);
|
|
2227
|
+
runSeq.set(msg.runId, seq);
|
|
2228
|
+
appendNodeEvent(db, msg.runId, seq, approvalEvent);
|
|
2229
|
+
}
|
|
2230
|
+
break;
|
|
2231
|
+
}
|
|
2232
|
+
if (msg.approvalKind === 'plan') {
|
|
2233
|
+
const planText = getPlanTextFromApprovalRequest(msg);
|
|
2234
|
+
if (planText) {
|
|
2235
|
+
upsertPlanApprovalMessage({
|
|
2236
|
+
db,
|
|
2237
|
+
manager,
|
|
2238
|
+
conversationId: msg.conversationId,
|
|
2239
|
+
runId: msg.runId,
|
|
2240
|
+
planText,
|
|
2241
|
+
broadcast: broadcastWithChannelForward,
|
|
2242
|
+
agentFileAccessService,
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
db.prepare('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?')
|
|
2247
|
+
.run('awaiting_approval', Date.now(), msg.conversationId);
|
|
2248
|
+
broadcastWithChannelForward(msg.conversationId, {
|
|
2249
|
+
type: 'conversation.status',
|
|
2250
|
+
conversationId: msg.conversationId,
|
|
2251
|
+
status: 'awaiting_approval',
|
|
2252
|
+
});
|
|
2253
|
+
broadcastPrimaryDmTaskRootStatusProjection({
|
|
2254
|
+
db,
|
|
2255
|
+
broadcast: broadcastWithChannelForward,
|
|
2256
|
+
manager,
|
|
2257
|
+
threadConversationId: msg.conversationId,
|
|
2258
|
+
taskThreadStatus: 'awaiting_approval',
|
|
2259
|
+
});
|
|
2260
|
+
const runKnown = !!db.prepare('SELECT 1 FROM runs WHERE run_id = ?').get(msg.runId);
|
|
2261
|
+
if (runKnown) {
|
|
2262
|
+
const seq = nextNodeEventSeq(db, msg.runId, runSeq);
|
|
2263
|
+
runSeq.set(msg.runId, seq);
|
|
2264
|
+
appendNodeEvent(db, msg.runId, seq, approvalEvent);
|
|
2265
|
+
}
|
|
2266
|
+
broadcastWithChannelForward(msg.conversationId, approvalEvent);
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
case 'workspace.list.response': {
|
|
2270
|
+
workspaceBroker?.handleWorkspaceListResponse(msg);
|
|
2271
|
+
break;
|
|
2272
|
+
}
|
|
2273
|
+
case 'workspace.inspect.response': {
|
|
2274
|
+
inspectBroker?.handleInspectResponse(msg);
|
|
2275
|
+
break;
|
|
2276
|
+
}
|
|
2277
|
+
case 'workspace.git_status.response': {
|
|
2278
|
+
gitBroker?.handleGitStatusResponse(msg);
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
case 'workspace.git_diff.response': {
|
|
2282
|
+
gitBroker?.handleGitDiffResponse(msg);
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
case 'workspace.git_action.response': {
|
|
2286
|
+
gitBroker?.handleGitActionResponse(msg);
|
|
2287
|
+
break;
|
|
2288
|
+
}
|
|
2289
|
+
case 'workspace.read.response': {
|
|
2290
|
+
workspaceBroker?.handleWorkspaceReadResponse(msg);
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
case 'workspace.archive.response': {
|
|
2294
|
+
workspaceBroker?.handleWorkspaceArchiveResponse(msg);
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
case 'workspace.extract.response': {
|
|
2298
|
+
workspaceBroker?.handleWorkspaceExtractResponse(msg);
|
|
2299
|
+
break;
|
|
2300
|
+
}
|
|
2301
|
+
case 'library.mirror.apply.response': {
|
|
2302
|
+
workspaceBroker?.handleLibraryMirrorApplyResponse(msg);
|
|
2303
|
+
break;
|
|
2304
|
+
}
|
|
2305
|
+
case 'library.mirror.snapshot.response': {
|
|
2306
|
+
workspaceBroker?.handleLibraryMirrorSnapshotResponse(msg);
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
case 'library.authority.delete.response': {
|
|
2310
|
+
workspaceBroker?.handleLibraryAuthorityDeleteResponse(msg);
|
|
2311
|
+
break;
|
|
2312
|
+
}
|
|
2313
|
+
case 'library.authority.rename.response': {
|
|
2314
|
+
workspaceBroker?.handleLibraryAuthorityRenameResponse(msg);
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
case 'fs.read_path.response': {
|
|
2318
|
+
workspaceBroker?.handleFsReadPathResponse(msg);
|
|
2319
|
+
break;
|
|
2320
|
+
}
|
|
2321
|
+
case 'fs.path_suggest.response': {
|
|
2322
|
+
workspaceBroker?.handleFsPathSuggestResponse(msg);
|
|
2323
|
+
break;
|
|
2324
|
+
}
|
|
2325
|
+
case 'http.fetch_text.response': {
|
|
2326
|
+
workspaceBroker?.handleHttpFetchTextResponse(msg);
|
|
2327
|
+
break;
|
|
2328
|
+
}
|
|
2329
|
+
case 'workspace.service_proxy.response': {
|
|
2330
|
+
workspaceBroker?.handleWorkspaceServiceProxyResponse(msg);
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
case 'fs.read_path.stream.start': {
|
|
2334
|
+
workspaceBroker?.handleFsReadPathStreamStart(msg);
|
|
2335
|
+
break;
|
|
2336
|
+
}
|
|
2337
|
+
case 'fs.read_path.stream.chunk': {
|
|
2338
|
+
workspaceBroker?.handleFsReadPathStreamChunk(msg);
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
case 'fs.read_path.stream.end': {
|
|
2342
|
+
workspaceBroker?.handleFsReadPathStreamEnd(msg);
|
|
2343
|
+
break;
|
|
2344
|
+
}
|
|
2345
|
+
case 'workspace.write.response': {
|
|
2346
|
+
workspaceBroker?.handleWorkspaceWriteResponse(msg);
|
|
2347
|
+
break;
|
|
2348
|
+
}
|
|
2349
|
+
case 'workspace.delete_file.response': {
|
|
2350
|
+
workspaceBroker?.handleWorkspaceDeleteFileResponse(msg);
|
|
2351
|
+
break;
|
|
2352
|
+
}
|
|
2353
|
+
case 'workspace.delete_path.response': {
|
|
2354
|
+
workspaceBroker?.handleWorkspaceDeletePathResponse(msg);
|
|
2355
|
+
break;
|
|
2356
|
+
}
|
|
2357
|
+
case 'workspace.reset.response': {
|
|
2358
|
+
workspaceBroker?.handleWorkspaceResetResponse(msg);
|
|
2359
|
+
break;
|
|
2360
|
+
}
|
|
2361
|
+
case 'agent.cleanup.response': {
|
|
2362
|
+
agentRuntimeCleanupBroker?.handleCleanupResponse(msg);
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
2365
|
+
case 'terminal.list.response': {
|
|
2366
|
+
terminalBroker?.handleListResponse(msg);
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
case 'terminal.create.response': {
|
|
2370
|
+
terminalBroker?.handleCreateResponse(msg);
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2373
|
+
case 'terminal.snapshot.response': {
|
|
2374
|
+
terminalBroker?.handleSnapshotResponse(msg);
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
case 'terminal.input.response': {
|
|
2378
|
+
terminalBroker?.handleInputResponse(msg);
|
|
2379
|
+
break;
|
|
2380
|
+
}
|
|
2381
|
+
case 'terminal.resize.response': {
|
|
2382
|
+
terminalBroker?.handleResizeResponse(msg);
|
|
2383
|
+
break;
|
|
2384
|
+
}
|
|
2385
|
+
case 'terminal.close.response': {
|
|
2386
|
+
terminalBroker?.handleCloseResponse(msg);
|
|
2387
|
+
break;
|
|
2388
|
+
}
|
|
2389
|
+
case 'terminal.output.event': {
|
|
2390
|
+
terminalBroker?.handleOutputEvent(msg);
|
|
2391
|
+
break;
|
|
2392
|
+
}
|
|
2393
|
+
case 'terminal.exit.event': {
|
|
2394
|
+
terminalBroker?.handleExitEvent(msg);
|
|
2395
|
+
break;
|
|
2396
|
+
}
|
|
2397
|
+
case 'skills.list.response': {
|
|
2398
|
+
skillsBroker?.handleSkillsListResponse(msg);
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
case 'skills.read.response': {
|
|
2402
|
+
skillsBroker?.handleSkillsReadResponse(msg);
|
|
2403
|
+
break;
|
|
2404
|
+
}
|
|
2405
|
+
case 'codex.transcript.list.response': {
|
|
2406
|
+
codexTranscriptBroker?.handleListResponse(msg);
|
|
2407
|
+
break;
|
|
2408
|
+
}
|
|
2409
|
+
case 'codex.transcript.read.response': {
|
|
2410
|
+
codexTranscriptBroker?.handleReadResponse(msg);
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
case 'claude.transcript.list.response': {
|
|
2414
|
+
claudeTranscriptBroker?.handleListResponse(msg);
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
case 'claude.transcript.read.response': {
|
|
2418
|
+
claudeTranscriptBroker?.handleReadResponse(msg);
|
|
2419
|
+
break;
|
|
2420
|
+
}
|
|
2421
|
+
case 'claude.controls.response': {
|
|
2422
|
+
claudeControlBroker?.handleControlsResponse(msg);
|
|
2423
|
+
break;
|
|
2424
|
+
}
|
|
2425
|
+
case 'claude.set_mode.response': {
|
|
2426
|
+
claudeControlBroker?.handleSetModeResponse(msg);
|
|
2427
|
+
break;
|
|
2428
|
+
}
|
|
2429
|
+
case 'claude.set_model.response': {
|
|
2430
|
+
claudeControlBroker?.handleSetModelResponse(msg);
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
case 'claude.command.response': {
|
|
2434
|
+
claudeControlBroker?.handleCommandResponse(msg);
|
|
2435
|
+
break;
|
|
2436
|
+
}
|
|
2437
|
+
case 'codex.model.list.response': {
|
|
2438
|
+
codexAppServerBroker?.handleModelListResponse(msg);
|
|
2439
|
+
break;
|
|
2440
|
+
}
|
|
2441
|
+
case 'codex.skills.list.response': {
|
|
2442
|
+
codexAppServerBroker?.handleSkillsListResponse(msg);
|
|
2443
|
+
break;
|
|
2444
|
+
}
|
|
2445
|
+
case 'codex.thread.list.response': {
|
|
2446
|
+
codexAppServerBroker?.handleThreadListResponse(msg);
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
case 'codex.thread.read.response': {
|
|
2450
|
+
codexAppServerBroker?.handleThreadReadResponse(msg);
|
|
2451
|
+
break;
|
|
2452
|
+
}
|
|
2453
|
+
case 'codex.conversation.summary.response': {
|
|
2454
|
+
codexAppServerBroker?.handleConversationSummaryResponse(msg);
|
|
2455
|
+
break;
|
|
2456
|
+
}
|
|
2457
|
+
case 'solo.run.event':
|
|
2458
|
+
case 'solo.run.end':
|
|
2459
|
+
case 'solo.permission.request': {
|
|
2460
|
+
if (!soloSessionRelay?.relayNodeMessage(msg)) {
|
|
2461
|
+
log.debug('[node-ws] solo message with no active client relay', {
|
|
2462
|
+
type: msg.type,
|
|
2463
|
+
soloSessionId: msg.soloSessionId,
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
break;
|
|
2467
|
+
}
|
|
2468
|
+
default: {
|
|
2469
|
+
log.warn('[node-ws] unknown message type', msg.type);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
});
|
|
2473
|
+
socket.on('close', () => {
|
|
2474
|
+
if (nodeId) {
|
|
2475
|
+
if (isStaleNodeConnection()) {
|
|
2476
|
+
log.debug('[node-ws] ignoring stale socket close for replaced node connection', {
|
|
2477
|
+
nodeId,
|
|
2478
|
+
connectionProcessInstanceId,
|
|
2479
|
+
});
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const disconnectMessage = `Agent node disconnected: ${nodeId}`;
|
|
2483
|
+
failNodeOwnedRuntimeState({
|
|
2484
|
+
nodeId,
|
|
2485
|
+
disconnectMessage,
|
|
2486
|
+
unregisterNode: true,
|
|
2487
|
+
markNodeOffline: true,
|
|
2488
|
+
});
|
|
2489
|
+
log.info(`[node-ws] disconnected: ${nodeId}`);
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
socket.on('error', (err) => {
|
|
2493
|
+
log.warn('[node-ws] socket error', err);
|
|
2494
|
+
});
|
|
2495
|
+
}
|