@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,612 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createPromptContextSection, } from './promptContextSections.js';
|
|
3
|
+
export function createConversationHandoff(db, params) {
|
|
4
|
+
const handoffId = randomUUID();
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
const payload = normalizeConversationHandoffPayload(params.payload);
|
|
7
|
+
db.prepare(`INSERT INTO conversation_handoffs(
|
|
8
|
+
handoff_id,
|
|
9
|
+
agent_id,
|
|
10
|
+
source_conversation_id,
|
|
11
|
+
target_conversation_id,
|
|
12
|
+
target_reply_target,
|
|
13
|
+
workflow_kind,
|
|
14
|
+
source_disposition,
|
|
15
|
+
task_id,
|
|
16
|
+
task_number,
|
|
17
|
+
mode,
|
|
18
|
+
payload_json,
|
|
19
|
+
status,
|
|
20
|
+
created_at,
|
|
21
|
+
updated_at
|
|
22
|
+
)
|
|
23
|
+
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(handoffId, params.agentId, params.sourceConversationId, params.targetConversationId ?? null, params.targetReplyTarget, params.workflowKind ?? 'generic', params.sourceDisposition ?? (params.mode === 'continue_there' ? 'cancel_current_run' : 'keep_running'), params.taskId ?? null, params.taskNumber ?? null, params.mode, JSON.stringify(payload), params.status, now, now);
|
|
24
|
+
return {
|
|
25
|
+
handoffId,
|
|
26
|
+
agentId: params.agentId,
|
|
27
|
+
sourceConversationId: params.sourceConversationId,
|
|
28
|
+
targetConversationId: params.targetConversationId ?? null,
|
|
29
|
+
targetReplyTarget: params.targetReplyTarget,
|
|
30
|
+
workflowKind: params.workflowKind ?? 'generic',
|
|
31
|
+
sourceDisposition: params.sourceDisposition ?? (params.mode === 'continue_there' ? 'cancel_current_run' : 'keep_running'),
|
|
32
|
+
taskId: params.taskId ?? null,
|
|
33
|
+
taskNumber: typeof params.taskNumber === 'number' ? Math.floor(params.taskNumber) : null,
|
|
34
|
+
mode: params.mode,
|
|
35
|
+
status: params.status,
|
|
36
|
+
payload,
|
|
37
|
+
createdAt: new Date(now).toISOString(),
|
|
38
|
+
updatedAt: new Date(now).toISOString(),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function markConversationHandoffAccepted(db, params) {
|
|
42
|
+
return updateLatestOpenConversationHandoff(db, {
|
|
43
|
+
targetConversationId: params.targetConversationId,
|
|
44
|
+
targetRunId: params.targetRunId,
|
|
45
|
+
status: 'accepted',
|
|
46
|
+
payload: {
|
|
47
|
+
targetRunId: params.targetRunId,
|
|
48
|
+
error: null,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export function markConversationHandoffCompleted(db, params) {
|
|
53
|
+
return updateLatestOpenConversationHandoff(db, {
|
|
54
|
+
targetConversationId: params.targetConversationId,
|
|
55
|
+
targetRunId: params.targetRunId ?? null,
|
|
56
|
+
status: 'completed',
|
|
57
|
+
payload: {
|
|
58
|
+
...(params.targetRunId ? { targetRunId: params.targetRunId } : {}),
|
|
59
|
+
completionMessageId: params.completionMessageId ?? null,
|
|
60
|
+
error: null,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function markConversationHandoffCancelled(db, params) {
|
|
65
|
+
return updateLatestOpenConversationHandoff(db, {
|
|
66
|
+
targetConversationId: params.targetConversationId,
|
|
67
|
+
targetRunId: params.targetRunId ?? null,
|
|
68
|
+
status: 'cancelled',
|
|
69
|
+
payload: {
|
|
70
|
+
...(params.targetRunId ? { targetRunId: params.targetRunId } : {}),
|
|
71
|
+
error: params.error ?? null,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function markConversationHandoffFailed(db, params) {
|
|
76
|
+
return updateLatestOpenConversationHandoff(db, {
|
|
77
|
+
targetConversationId: params.targetConversationId,
|
|
78
|
+
targetRunId: params.targetRunId ?? null,
|
|
79
|
+
status: 'failed',
|
|
80
|
+
payload: {
|
|
81
|
+
...(params.targetRunId ? { targetRunId: params.targetRunId } : {}),
|
|
82
|
+
...(params.error ? { error: params.error } : {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function markConversationHandoffSourceCancelled(db, params) {
|
|
87
|
+
const row = db.prepare(`SELECT handoff_id as handoffId,
|
|
88
|
+
agent_id as agentId,
|
|
89
|
+
source_conversation_id as sourceConversationId,
|
|
90
|
+
target_conversation_id as targetConversationId,
|
|
91
|
+
target_reply_target as targetReplyTarget,
|
|
92
|
+
workflow_kind as workflowKind,
|
|
93
|
+
source_disposition as sourceDisposition,
|
|
94
|
+
task_id as taskId,
|
|
95
|
+
task_number as taskNumber,
|
|
96
|
+
mode,
|
|
97
|
+
status,
|
|
98
|
+
payload_json as payloadJson,
|
|
99
|
+
created_at as createdAt,
|
|
100
|
+
updated_at as updatedAt
|
|
101
|
+
FROM conversation_handoffs
|
|
102
|
+
WHERE handoff_id = ?
|
|
103
|
+
LIMIT 1`).get(params.handoffId);
|
|
104
|
+
if (!row)
|
|
105
|
+
return null;
|
|
106
|
+
const record = mapConversationHandoffRow(row);
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const payload = normalizeConversationHandoffPayload({
|
|
109
|
+
...record.payload,
|
|
110
|
+
cancelRunId: params.cancelRunId,
|
|
111
|
+
goal: record.payload.goal,
|
|
112
|
+
});
|
|
113
|
+
db.prepare(`UPDATE conversation_handoffs
|
|
114
|
+
SET payload_json = ?,
|
|
115
|
+
updated_at = ?
|
|
116
|
+
WHERE handoff_id = ?`).run(JSON.stringify(payload), now, params.handoffId);
|
|
117
|
+
return {
|
|
118
|
+
...record,
|
|
119
|
+
payload,
|
|
120
|
+
updatedAt: new Date(now).toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function listRecentConversationHandoffs(db, agentId, limit = 5) {
|
|
124
|
+
const safeLimit = Math.max(1, Math.min(limit, 10));
|
|
125
|
+
const rows = db.prepare(`SELECT handoff_id as handoffId,
|
|
126
|
+
agent_id as agentId,
|
|
127
|
+
source_conversation_id as sourceConversationId,
|
|
128
|
+
target_conversation_id as targetConversationId,
|
|
129
|
+
target_reply_target as targetReplyTarget,
|
|
130
|
+
workflow_kind as workflowKind,
|
|
131
|
+
source_disposition as sourceDisposition,
|
|
132
|
+
task_id as taskId,
|
|
133
|
+
task_number as taskNumber,
|
|
134
|
+
mode,
|
|
135
|
+
status,
|
|
136
|
+
payload_json as payloadJson,
|
|
137
|
+
created_at as createdAt,
|
|
138
|
+
updated_at as updatedAt
|
|
139
|
+
FROM conversation_handoffs
|
|
140
|
+
WHERE agent_id = ?
|
|
141
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
142
|
+
LIMIT ?`).all(agentId, safeLimit);
|
|
143
|
+
return rows.map(mapConversationHandoffRow);
|
|
144
|
+
}
|
|
145
|
+
export function getConversationHandoffById(db, handoffId) {
|
|
146
|
+
const row = db.prepare(`SELECT handoff_id as handoffId,
|
|
147
|
+
agent_id as agentId,
|
|
148
|
+
source_conversation_id as sourceConversationId,
|
|
149
|
+
target_conversation_id as targetConversationId,
|
|
150
|
+
target_reply_target as targetReplyTarget,
|
|
151
|
+
workflow_kind as workflowKind,
|
|
152
|
+
source_disposition as sourceDisposition,
|
|
153
|
+
task_id as taskId,
|
|
154
|
+
task_number as taskNumber,
|
|
155
|
+
mode,
|
|
156
|
+
status,
|
|
157
|
+
payload_json as payloadJson,
|
|
158
|
+
created_at as createdAt,
|
|
159
|
+
updated_at as updatedAt
|
|
160
|
+
FROM conversation_handoffs
|
|
161
|
+
WHERE handoff_id = ?
|
|
162
|
+
LIMIT 1`).get(handoffId);
|
|
163
|
+
return row ? mapConversationHandoffRow(row) : null;
|
|
164
|
+
}
|
|
165
|
+
export function getHandoffState(db, handoffId) {
|
|
166
|
+
const record = getConversationHandoffById(db, handoffId);
|
|
167
|
+
if (!record)
|
|
168
|
+
return null;
|
|
169
|
+
const runRows = db.prepare(`SELECT run_id as runId,
|
|
170
|
+
agent_id as agentId,
|
|
171
|
+
disposition,
|
|
172
|
+
created_at as createdAt,
|
|
173
|
+
completed_at as completedAt
|
|
174
|
+
FROM handoff_runs
|
|
175
|
+
WHERE handoff_id = ?
|
|
176
|
+
ORDER BY created_at DESC`).all(handoffId);
|
|
177
|
+
const runs = runRows.map((row) => ({
|
|
178
|
+
runId: row.runId,
|
|
179
|
+
agentId: row.agentId,
|
|
180
|
+
disposition: row.disposition,
|
|
181
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
182
|
+
completedAt: row.completedAt ? new Date(row.completedAt).toISOString() : null,
|
|
183
|
+
}));
|
|
184
|
+
return { ...record, runs };
|
|
185
|
+
}
|
|
186
|
+
export function insertHandoffRun(db, params) {
|
|
187
|
+
const handoffExists = db.prepare(`SELECT 1 FROM conversation_handoffs WHERE handoff_id = ? LIMIT 1`).get(params.handoffId);
|
|
188
|
+
if (!handoffExists)
|
|
189
|
+
return null;
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
// Mark previous non-completed runs for this handoff as completed
|
|
192
|
+
db.prepare(`UPDATE handoff_runs
|
|
193
|
+
SET completed_at = ?
|
|
194
|
+
WHERE handoff_id = ?
|
|
195
|
+
AND completed_at IS NULL`).run(now, params.handoffId);
|
|
196
|
+
const id = randomUUID();
|
|
197
|
+
db.prepare(`INSERT INTO handoff_runs(id, handoff_id, run_id, agent_id, disposition, created_at)
|
|
198
|
+
VALUES(?, ?, ?, ?, ?, ?)`).run(id, params.handoffId, params.runId, params.agentId, params.disposition, now);
|
|
199
|
+
return {
|
|
200
|
+
runId: params.runId,
|
|
201
|
+
agentId: params.agentId,
|
|
202
|
+
disposition: params.disposition,
|
|
203
|
+
createdAt: new Date(now).toISOString(),
|
|
204
|
+
completedAt: null,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export function getLatestConversationHandoffForConversation(db, params) {
|
|
208
|
+
const row = db.prepare(`SELECT handoff_id as handoffId,
|
|
209
|
+
agent_id as agentId,
|
|
210
|
+
source_conversation_id as sourceConversationId,
|
|
211
|
+
target_conversation_id as targetConversationId,
|
|
212
|
+
target_reply_target as targetReplyTarget,
|
|
213
|
+
workflow_kind as workflowKind,
|
|
214
|
+
source_disposition as sourceDisposition,
|
|
215
|
+
task_id as taskId,
|
|
216
|
+
task_number as taskNumber,
|
|
217
|
+
mode,
|
|
218
|
+
status,
|
|
219
|
+
payload_json as payloadJson,
|
|
220
|
+
created_at as createdAt,
|
|
221
|
+
updated_at as updatedAt
|
|
222
|
+
FROM conversation_handoffs
|
|
223
|
+
WHERE (source_conversation_id = ? OR target_conversation_id = ?)
|
|
224
|
+
${params.openOnly ? `AND status IN ('started', 'accepted')` : ''}
|
|
225
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
226
|
+
LIMIT 1`).get(params.conversationId, params.conversationId);
|
|
227
|
+
return row ? mapConversationHandoffRow(row) : null;
|
|
228
|
+
}
|
|
229
|
+
export function formatConversationHandoff(record) {
|
|
230
|
+
const summary = summarizeText(record.payload.goal, 120) ?? '(missing goal)';
|
|
231
|
+
const source = record.payload.sourceTarget?.trim() || record.sourceConversationId;
|
|
232
|
+
const error = record.payload.error?.trim();
|
|
233
|
+
const failure = error ? ` error=${summarizeText(error, 100)}` : '';
|
|
234
|
+
const workflow = record.workflowKind === 'generic' ? record.mode : `${record.workflowKind}/${record.mode}`;
|
|
235
|
+
const task = record.taskNumber ? ` task=#${record.taskNumber}` : '';
|
|
236
|
+
return `${workflow} [${record.status}] ${source} -> ${record.targetReplyTarget}${task} goal=${summary}${failure}`;
|
|
237
|
+
}
|
|
238
|
+
function buildContinueThereSourceReminderLines(params) {
|
|
239
|
+
const lines = [
|
|
240
|
+
`Detailed execution intentionally moved from ${params.sourceReplyTarget} to ${params.targetReplyTarget}.`,
|
|
241
|
+
'This was a normal platform handoff, not a user interruption, runtime failure, or accidental cancellation.',
|
|
242
|
+
];
|
|
243
|
+
if (params.relatedTaskRef?.trim()) {
|
|
244
|
+
lines.push(`Related task: ${params.relatedTaskRef.trim()}`);
|
|
245
|
+
}
|
|
246
|
+
lines.push(`If you are later asked about this from ${params.sourceReplyTarget}, describe it as a normal handoff/continue-there transition to ${params.targetReplyTarget}.`, 'Do not say that the earlier work on this surface was interrupted or cut off.', `If you need to report progress or status later, inspect ${params.targetReplyTarget} or structured task/handoff state first instead of relying on this source surface's pre-handoff plan.`);
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
249
|
+
export function buildContinueThereSourceNoticePrompt(params) {
|
|
250
|
+
return [
|
|
251
|
+
'[System handoff notice]',
|
|
252
|
+
...buildContinueThereSourceReminderLines(params),
|
|
253
|
+
`Stop detailed execution on ${params.sourceReplyTarget} now; it continues on ${params.targetReplyTarget}.`,
|
|
254
|
+
].join('\n');
|
|
255
|
+
}
|
|
256
|
+
export function buildSourceSurfaceHandoffReminderSection(db, params) {
|
|
257
|
+
const conversation = resolveSourceSurfaceConversation(db, params);
|
|
258
|
+
if (!conversation?.replyTarget)
|
|
259
|
+
return null;
|
|
260
|
+
const handoff = loadLatestContinueThereSourceHandoff(db, conversation.conversationId);
|
|
261
|
+
if (!handoff)
|
|
262
|
+
return null;
|
|
263
|
+
if (hasLaterAgentVisibleMessageOnSurface(db, conversation, handoff.createdAt))
|
|
264
|
+
return null;
|
|
265
|
+
return createPromptContextSection('source_handoff_reminder', [
|
|
266
|
+
'[Source-surface handoff reminder]',
|
|
267
|
+
...buildContinueThereSourceReminderLines({
|
|
268
|
+
sourceReplyTarget: conversation.replyTarget,
|
|
269
|
+
targetReplyTarget: handoff.targetReplyTarget,
|
|
270
|
+
relatedTaskRef: handoff.payload.relatedTaskRef,
|
|
271
|
+
}),
|
|
272
|
+
].join('\n'));
|
|
273
|
+
}
|
|
274
|
+
export function buildConversationHandoffCapsuleText(params) {
|
|
275
|
+
const lines = [
|
|
276
|
+
'[Handoff capsule]',
|
|
277
|
+
'This is a must-keep execution-context block for the current handoff target.',
|
|
278
|
+
'',
|
|
279
|
+
'[Handoff metadata]',
|
|
280
|
+
`mode: ${params.mode}`,
|
|
281
|
+
`reply_target: ${params.targetReplyTarget}`,
|
|
282
|
+
];
|
|
283
|
+
if (params.payload.sourceTarget) {
|
|
284
|
+
lines.push(`source_target: ${params.payload.sourceTarget}`);
|
|
285
|
+
}
|
|
286
|
+
lines.push('', '[Goal]', params.payload.goal);
|
|
287
|
+
if (params.payload.whyNow) {
|
|
288
|
+
lines.push('', '[Why now]', params.payload.whyNow);
|
|
289
|
+
}
|
|
290
|
+
if (params.payload.alreadyDone.length > 0) {
|
|
291
|
+
lines.push('', '[Already done]', ...params.payload.alreadyDone.map((item) => `- ${item}`));
|
|
292
|
+
}
|
|
293
|
+
if (params.payload.expectedOutput) {
|
|
294
|
+
lines.push('', '[Expected output]', params.payload.expectedOutput);
|
|
295
|
+
}
|
|
296
|
+
if (params.payload.relatedTaskRef) {
|
|
297
|
+
lines.push('', '[Related task]', params.payload.relatedTaskRef);
|
|
298
|
+
}
|
|
299
|
+
if (params.payload.sourceSummary) {
|
|
300
|
+
lines.push('', '[Source surface summary]', params.payload.sourceSummary);
|
|
301
|
+
}
|
|
302
|
+
if (params.payload.sourceRollingSummary) {
|
|
303
|
+
lines.push('', '[Recent dialogue rolling summary]', params.payload.sourceRollingSummary);
|
|
304
|
+
}
|
|
305
|
+
if (params.payload.sourceContext) {
|
|
306
|
+
const lastUserMsg = summarizeText(params.payload.sourceContext.lastUserMessage, 500);
|
|
307
|
+
if (lastUserMsg) {
|
|
308
|
+
lines.push('', '[Source last user message]', lastUserMsg);
|
|
309
|
+
}
|
|
310
|
+
const lastAgentMsg = summarizeText(params.payload.sourceContext.lastAgentMessage, 500);
|
|
311
|
+
if (lastAgentMsg) {
|
|
312
|
+
lines.push('', '[Source last agent message]', lastAgentMsg);
|
|
313
|
+
}
|
|
314
|
+
if (!params.payload.sourceSummary
|
|
315
|
+
&& (params.payload.sourceContext.boundTaskRef
|
|
316
|
+
|| params.payload.sourceContext.boundTaskTitle
|
|
317
|
+
|| params.payload.sourceContext.ownerName
|
|
318
|
+
|| params.payload.sourceContext.myRole
|
|
319
|
+
|| params.payload.sourceContext.participants.length > 0)) {
|
|
320
|
+
lines.push('', '[Source surface context]');
|
|
321
|
+
if (params.payload.sourceContext.boundTaskRef || params.payload.sourceContext.boundTaskTitle) {
|
|
322
|
+
lines.push(`bound_task: ${params.payload.sourceContext.boundTaskRef ?? '(no task ref)'}${params.payload.sourceContext.boundTaskTitle ? ` — ${params.payload.sourceContext.boundTaskTitle}` : ''}`);
|
|
323
|
+
}
|
|
324
|
+
if (params.payload.sourceContext.myRole) {
|
|
325
|
+
lines.push(`my_role: ${params.payload.sourceContext.myRole}`);
|
|
326
|
+
}
|
|
327
|
+
if (params.payload.sourceContext.ownerName) {
|
|
328
|
+
lines.push(`owner: @${params.payload.sourceContext.ownerName}`);
|
|
329
|
+
}
|
|
330
|
+
if (params.payload.sourceContext.participants.length > 0) {
|
|
331
|
+
lines.push(`participants: ${params.payload.sourceContext.participants.map((name) => `@${name}`).join(', ')}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return lines.join('\n');
|
|
336
|
+
}
|
|
337
|
+
export function buildConversationHandoffPrompt(params) {
|
|
338
|
+
const lines = [
|
|
339
|
+
'[Conversation handoff]',
|
|
340
|
+
'You are the same agent continuing work on another surface.',
|
|
341
|
+
'',
|
|
342
|
+
'The full handoff capsule is attached in activation/resume context so it survives queueing and restore.',
|
|
343
|
+
];
|
|
344
|
+
if (params.payload.constraints.length > 0) {
|
|
345
|
+
lines.push('', '[Constraints]', ...params.payload.constraints.map((item) => `- ${item}`));
|
|
346
|
+
}
|
|
347
|
+
if (params.payload.optionalRefs.length > 0) {
|
|
348
|
+
lines.push('', '[Optional refs]', ...params.payload.optionalRefs.map((item) => `- ${item}`));
|
|
349
|
+
}
|
|
350
|
+
lines.push('', 'Rules:');
|
|
351
|
+
lines.push('- This handoff comes from another work surface of the same agent identity.');
|
|
352
|
+
if (params.mode === 'continue_there') {
|
|
353
|
+
lines.push('- Detailed execution should continue here. Treat the source surface as handed off.');
|
|
354
|
+
}
|
|
355
|
+
else if (params.mode === 'delegate_only') {
|
|
356
|
+
lines.push('- Handle the delegated follow-up here. The source surface may still remain active.');
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
lines.push('- Coordinate through summaries and explicit handoffs; avoid duplicating the same work across surfaces.');
|
|
360
|
+
}
|
|
361
|
+
lines.push('- Prefer this target for detailed work that belongs here.');
|
|
362
|
+
lines.push('- Choose the status command by question: bigbang self state for global surface overview, bigbang runtime presence for node/host/runtime facts, bigbang workspace inspect for current workspace git/directory facts, bigbang conversation list for bulk surface discovery, bigbang conversation summary for one known surface, and bigbang context bundle only to shortlist related surfaces/tasks/handoffs before deeper inspection or another handoff.');
|
|
363
|
+
return lines.join('\n');
|
|
364
|
+
}
|
|
365
|
+
function normalizeConversationHandoffPayload(payload) {
|
|
366
|
+
return {
|
|
367
|
+
goal: normalizeRequiredText(payload.goal) ?? '(missing goal)',
|
|
368
|
+
whyNow: summarizeText(payload.whyNow, 400),
|
|
369
|
+
constraints: normalizeStringList(payload.constraints),
|
|
370
|
+
alreadyDone: normalizeStringList(payload.alreadyDone),
|
|
371
|
+
expectedOutput: summarizeText(payload.expectedOutput, 400),
|
|
372
|
+
sourceTarget: summarizeText(payload.sourceTarget, 200),
|
|
373
|
+
relatedTaskRef: summarizeText(payload.relatedTaskRef, 120),
|
|
374
|
+
optionalRefs: normalizeStringList(payload.optionalRefs),
|
|
375
|
+
sourceSummary: summarizeText(payload.sourceSummary, 1200),
|
|
376
|
+
sourceRollingSummary: summarizeText(payload.sourceRollingSummary, 1200),
|
|
377
|
+
targetRunId: summarizeText(payload.targetRunId, 160),
|
|
378
|
+
cancelRunId: summarizeText(payload.cancelRunId, 160),
|
|
379
|
+
completionMessageId: summarizeText(payload.completionMessageId, 160),
|
|
380
|
+
error: summarizeText(payload.error, 400),
|
|
381
|
+
sourceContext: payload.sourceContext
|
|
382
|
+
? {
|
|
383
|
+
lastUserMessage: summarizeText(payload.sourceContext.lastUserMessage, 280),
|
|
384
|
+
lastAgentMessage: summarizeText(payload.sourceContext.lastAgentMessage, 280),
|
|
385
|
+
boundTaskRef: summarizeText(payload.sourceContext.boundTaskRef, 160),
|
|
386
|
+
boundTaskTitle: summarizeText(payload.sourceContext.boundTaskTitle, 200),
|
|
387
|
+
ownerName: summarizeText(payload.sourceContext.ownerName, 160),
|
|
388
|
+
participants: normalizeStringList(payload.sourceContext.participants),
|
|
389
|
+
myRole: summarizeText(payload.sourceContext.myRole, 120),
|
|
390
|
+
}
|
|
391
|
+
: null,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function parseConversationHandoffPayload(raw) {
|
|
395
|
+
try {
|
|
396
|
+
const parsed = JSON.parse(raw);
|
|
397
|
+
return normalizeConversationHandoffPayload({
|
|
398
|
+
goal: typeof parsed?.goal === 'string' ? parsed.goal : '(missing goal)',
|
|
399
|
+
whyNow: typeof parsed?.whyNow === 'string' ? parsed.whyNow : null,
|
|
400
|
+
constraints: Array.isArray(parsed?.constraints) ? parsed.constraints : [],
|
|
401
|
+
alreadyDone: Array.isArray(parsed?.alreadyDone) ? parsed.alreadyDone : [],
|
|
402
|
+
expectedOutput: typeof parsed?.expectedOutput === 'string' ? parsed.expectedOutput : null,
|
|
403
|
+
sourceTarget: typeof parsed?.sourceTarget === 'string' ? parsed.sourceTarget : null,
|
|
404
|
+
relatedTaskRef: typeof parsed?.relatedTaskRef === 'string' ? parsed.relatedTaskRef : null,
|
|
405
|
+
optionalRefs: Array.isArray(parsed?.optionalRefs) ? parsed.optionalRefs : [],
|
|
406
|
+
sourceSummary: typeof parsed?.sourceSummary === 'string' ? parsed.sourceSummary : null,
|
|
407
|
+
sourceRollingSummary: typeof parsed?.sourceRollingSummary === 'string' ? parsed.sourceRollingSummary : null,
|
|
408
|
+
targetRunId: typeof parsed?.targetRunId === 'string' ? parsed.targetRunId : null,
|
|
409
|
+
cancelRunId: typeof parsed?.cancelRunId === 'string' ? parsed.cancelRunId : null,
|
|
410
|
+
completionMessageId: typeof parsed?.completionMessageId === 'string' ? parsed.completionMessageId : null,
|
|
411
|
+
error: typeof parsed?.error === 'string' ? parsed.error : null,
|
|
412
|
+
sourceContext: parsed?.sourceContext && typeof parsed.sourceContext === 'object'
|
|
413
|
+
? {
|
|
414
|
+
lastUserMessage: typeof parsed.sourceContext.lastUserMessage === 'string' ? parsed.sourceContext.lastUserMessage : null,
|
|
415
|
+
lastAgentMessage: typeof parsed.sourceContext.lastAgentMessage === 'string' ? parsed.sourceContext.lastAgentMessage : null,
|
|
416
|
+
boundTaskRef: typeof parsed.sourceContext.boundTaskRef === 'string' ? parsed.sourceContext.boundTaskRef : null,
|
|
417
|
+
boundTaskTitle: typeof parsed.sourceContext.boundTaskTitle === 'string' ? parsed.sourceContext.boundTaskTitle : null,
|
|
418
|
+
ownerName: typeof parsed.sourceContext.ownerName === 'string' ? parsed.sourceContext.ownerName : null,
|
|
419
|
+
participants: Array.isArray(parsed.sourceContext.participants) ? parsed.sourceContext.participants : [],
|
|
420
|
+
myRole: typeof parsed.sourceContext.myRole === 'string' ? parsed.sourceContext.myRole : null,
|
|
421
|
+
}
|
|
422
|
+
: null,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return normalizeConversationHandoffPayload({ goal: '(missing goal)' });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function resolveSourceSurfaceConversation(db, params) {
|
|
430
|
+
const normalizedThreadRootId = params.threadRootId?.trim() || '';
|
|
431
|
+
if (params.replyTarget.startsWith('dm:@')) {
|
|
432
|
+
const row = db.prepare(`SELECT id as conversationId,
|
|
433
|
+
agent_id as agentId,
|
|
434
|
+
channel_id as channelId,
|
|
435
|
+
reply_target as replyTarget,
|
|
436
|
+
thread_kind as threadKind,
|
|
437
|
+
thread_root_id as threadRootId
|
|
438
|
+
FROM conversations
|
|
439
|
+
WHERE agent_id = ?
|
|
440
|
+
AND thread_kind = 'direct'
|
|
441
|
+
AND reply_target = ?
|
|
442
|
+
AND COALESCE(thread_root_id, '') = ?
|
|
443
|
+
ORDER BY updated_at DESC
|
|
444
|
+
LIMIT 1`).get(params.agentId, params.replyTarget, normalizedThreadRootId);
|
|
445
|
+
return row ?? null;
|
|
446
|
+
}
|
|
447
|
+
const row = db.prepare(`SELECT id as conversationId,
|
|
448
|
+
agent_id as agentId,
|
|
449
|
+
channel_id as channelId,
|
|
450
|
+
reply_target as replyTarget,
|
|
451
|
+
thread_kind as threadKind,
|
|
452
|
+
thread_root_id as threadRootId
|
|
453
|
+
FROM conversations
|
|
454
|
+
WHERE agent_id = ?
|
|
455
|
+
AND thread_kind = 'branch'
|
|
456
|
+
AND channel_id = ?
|
|
457
|
+
AND reply_target = ?
|
|
458
|
+
AND COALESCE(thread_root_id, '') = ?
|
|
459
|
+
ORDER BY updated_at DESC
|
|
460
|
+
LIMIT 1`).get(params.agentId, params.channelId, params.replyTarget, normalizedThreadRootId);
|
|
461
|
+
return row ?? null;
|
|
462
|
+
}
|
|
463
|
+
function loadLatestContinueThereSourceHandoff(db, sourceConversationId) {
|
|
464
|
+
const row = db.prepare(`SELECT handoff_id as handoffId,
|
|
465
|
+
agent_id as agentId,
|
|
466
|
+
source_conversation_id as sourceConversationId,
|
|
467
|
+
target_conversation_id as targetConversationId,
|
|
468
|
+
target_reply_target as targetReplyTarget,
|
|
469
|
+
workflow_kind as workflowKind,
|
|
470
|
+
source_disposition as sourceDisposition,
|
|
471
|
+
task_id as taskId,
|
|
472
|
+
task_number as taskNumber,
|
|
473
|
+
h.mode as mode,
|
|
474
|
+
h.status as status,
|
|
475
|
+
h.payload_json as payloadJson,
|
|
476
|
+
h.created_at as createdAt,
|
|
477
|
+
h.updated_at as updatedAt
|
|
478
|
+
FROM conversation_handoffs h
|
|
479
|
+
WHERE h.source_conversation_id = ?
|
|
480
|
+
AND h.mode = 'continue_there'
|
|
481
|
+
AND h.source_disposition = 'cancel_current_run'
|
|
482
|
+
ORDER BY created_at DESC, updated_at DESC
|
|
483
|
+
LIMIT 1`).get(sourceConversationId);
|
|
484
|
+
if (!row)
|
|
485
|
+
return null;
|
|
486
|
+
const handoff = mapConversationHandoffRow(row);
|
|
487
|
+
if (handoff.status !== 'failed')
|
|
488
|
+
return handoff;
|
|
489
|
+
if (handoff.payload.cancelRunId)
|
|
490
|
+
return handoff;
|
|
491
|
+
const legacyCancelledEvent = db.prepare(`SELECT 1 as found
|
|
492
|
+
FROM events e
|
|
493
|
+
WHERE json_extract(e.payload_json, '$.kind') = 'conversation_handoff'
|
|
494
|
+
AND json_extract(e.payload_json, '$.handoffId') = ?
|
|
495
|
+
AND json_extract(e.payload_json, '$.cancelRunId') IS NOT NULL
|
|
496
|
+
LIMIT 1`).get(handoff.handoffId);
|
|
497
|
+
return legacyCancelledEvent ? handoff : null;
|
|
498
|
+
}
|
|
499
|
+
function hasLaterAgentVisibleMessageOnSurface(db, conversation, createdAtIso) {
|
|
500
|
+
const createdAtMs = Date.parse(createdAtIso);
|
|
501
|
+
if (!Number.isFinite(createdAtMs))
|
|
502
|
+
return false;
|
|
503
|
+
const messageChannelId = conversation.threadKind === 'direct'
|
|
504
|
+
? `dm:${conversation.agentId ?? ''}`
|
|
505
|
+
: conversation.channelId;
|
|
506
|
+
if (!messageChannelId || !conversation.replyTarget)
|
|
507
|
+
return false;
|
|
508
|
+
const row = db.prepare(`SELECT 1
|
|
509
|
+
FROM channel_messages
|
|
510
|
+
WHERE channel_id = ?
|
|
511
|
+
AND target = ?
|
|
512
|
+
AND sender_type = 'agent'
|
|
513
|
+
AND COALESCE(thread_root_id, '') = ?
|
|
514
|
+
AND created_at > ?
|
|
515
|
+
ORDER BY created_at DESC, seq DESC
|
|
516
|
+
LIMIT 1`).get(messageChannelId, conversation.replyTarget, conversation.threadRootId ?? '', createdAtMs);
|
|
517
|
+
return Boolean(row);
|
|
518
|
+
}
|
|
519
|
+
function updateLatestOpenConversationHandoff(db, params) {
|
|
520
|
+
const record = findLatestOpenConversationHandoff(db, params.targetConversationId, params.targetRunId ?? null);
|
|
521
|
+
if (!record)
|
|
522
|
+
return null;
|
|
523
|
+
const now = Date.now();
|
|
524
|
+
const payload = normalizeConversationHandoffPayload({
|
|
525
|
+
...record.payload,
|
|
526
|
+
...params.payload,
|
|
527
|
+
goal: params.payload?.goal ?? record.payload.goal,
|
|
528
|
+
});
|
|
529
|
+
db.prepare(`UPDATE conversation_handoffs
|
|
530
|
+
SET target_conversation_id = ?,
|
|
531
|
+
payload_json = ?,
|
|
532
|
+
status = ?,
|
|
533
|
+
updated_at = ?
|
|
534
|
+
WHERE handoff_id = ?`).run(record.targetConversationId ?? params.targetConversationId, JSON.stringify(payload), params.status, now, record.handoffId);
|
|
535
|
+
return {
|
|
536
|
+
...record,
|
|
537
|
+
targetConversationId: record.targetConversationId ?? params.targetConversationId,
|
|
538
|
+
status: params.status,
|
|
539
|
+
payload,
|
|
540
|
+
updatedAt: new Date(now).toISOString(),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function findLatestOpenConversationHandoff(db, targetConversationId, targetRunId) {
|
|
544
|
+
const rows = db.prepare(`SELECT handoff_id as handoffId,
|
|
545
|
+
agent_id as agentId,
|
|
546
|
+
source_conversation_id as sourceConversationId,
|
|
547
|
+
target_conversation_id as targetConversationId,
|
|
548
|
+
target_reply_target as targetReplyTarget,
|
|
549
|
+
workflow_kind as workflowKind,
|
|
550
|
+
source_disposition as sourceDisposition,
|
|
551
|
+
task_id as taskId,
|
|
552
|
+
task_number as taskNumber,
|
|
553
|
+
mode,
|
|
554
|
+
status,
|
|
555
|
+
payload_json as payloadJson,
|
|
556
|
+
created_at as createdAt,
|
|
557
|
+
updated_at as updatedAt
|
|
558
|
+
FROM conversation_handoffs
|
|
559
|
+
WHERE target_conversation_id = ?
|
|
560
|
+
AND status IN ('started', 'accepted')
|
|
561
|
+
ORDER BY updated_at DESC, created_at DESC
|
|
562
|
+
LIMIT 12`).all(targetConversationId);
|
|
563
|
+
const records = rows.map(mapConversationHandoffRow);
|
|
564
|
+
if (targetRunId) {
|
|
565
|
+
const exact = records.find((record) => record.payload.targetRunId === targetRunId);
|
|
566
|
+
if (exact)
|
|
567
|
+
return exact;
|
|
568
|
+
const pending = records.find((record) => !record.payload.targetRunId);
|
|
569
|
+
if (pending)
|
|
570
|
+
return pending;
|
|
571
|
+
}
|
|
572
|
+
return records[0] ?? null;
|
|
573
|
+
}
|
|
574
|
+
function mapConversationHandoffRow(row) {
|
|
575
|
+
return {
|
|
576
|
+
handoffId: row.handoffId,
|
|
577
|
+
agentId: row.agentId,
|
|
578
|
+
sourceConversationId: row.sourceConversationId,
|
|
579
|
+
targetConversationId: row.targetConversationId ?? null,
|
|
580
|
+
targetReplyTarget: row.targetReplyTarget,
|
|
581
|
+
workflowKind: row.workflowKind,
|
|
582
|
+
sourceDisposition: row.sourceDisposition,
|
|
583
|
+
taskId: row.taskId ?? null,
|
|
584
|
+
taskNumber: typeof row.taskNumber === 'number' ? Math.floor(row.taskNumber) : null,
|
|
585
|
+
mode: row.mode,
|
|
586
|
+
status: row.status,
|
|
587
|
+
payload: parseConversationHandoffPayload(row.payloadJson),
|
|
588
|
+
createdAt: new Date(row.createdAt).toISOString(),
|
|
589
|
+
updatedAt: new Date(row.updatedAt).toISOString(),
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function normalizeRequiredText(value) {
|
|
593
|
+
if (typeof value !== 'string')
|
|
594
|
+
return null;
|
|
595
|
+
const trimmed = value.trim();
|
|
596
|
+
return trimmed ? trimmed : null;
|
|
597
|
+
}
|
|
598
|
+
function normalizeStringList(value) {
|
|
599
|
+
if (!Array.isArray(value))
|
|
600
|
+
return [];
|
|
601
|
+
return value
|
|
602
|
+
.map((item) => summarizeText(item, 300))
|
|
603
|
+
.filter((item) => Boolean(item));
|
|
604
|
+
}
|
|
605
|
+
function summarizeText(value, limit) {
|
|
606
|
+
if (typeof value !== 'string')
|
|
607
|
+
return null;
|
|
608
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
609
|
+
if (!normalized)
|
|
610
|
+
return null;
|
|
611
|
+
return normalized.length > limit ? `${normalized.slice(0, limit - 3)}...` : normalized;
|
|
612
|
+
}
|