@vellumai/assistant 0.4.30 → 0.4.32
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/ARCHITECTURE.md +1 -1
- package/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
- package/src/__tests__/config-schema.test.ts +6 -14
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +269 -899
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/memory-schema.ts +0 -10
- package/src/config/schema.ts +2 -2
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1337
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +24 -7
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1402
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +83 -710
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +371 -29
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +192 -194
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +17 -2
- package/src/runtime/routes/guardian-action-routes.ts +23 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +47 -17
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/contacts/startup-migration.ts +0 -21
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -2,10 +2,6 @@ import * as net from "node:net";
|
|
|
2
2
|
|
|
3
3
|
import { v4 as uuid } from "uuid";
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
createAssistantMessage,
|
|
7
|
-
createUserMessage,
|
|
8
|
-
} from "../../agent/message-types.js";
|
|
9
5
|
import {
|
|
10
6
|
type InterfaceId,
|
|
11
7
|
isChannelId,
|
|
@@ -13,16 +9,9 @@ import {
|
|
|
13
9
|
parseInterfaceId,
|
|
14
10
|
} from "../../channels/types.js";
|
|
15
11
|
import { getConfig } from "../../config/loader.js";
|
|
16
|
-
import {
|
|
17
|
-
getAttachmentsForMessage,
|
|
18
|
-
getFilePathForAttachment,
|
|
19
|
-
setAttachmentThumbnail,
|
|
20
|
-
} from "../../memory/attachments-store.js";
|
|
21
12
|
import {
|
|
22
13
|
createCanonicalGuardianRequest,
|
|
23
14
|
generateCanonicalRequestCode,
|
|
24
|
-
listCanonicalGuardianRequests,
|
|
25
|
-
listPendingCanonicalGuardianRequestsByDestinationConversation,
|
|
26
15
|
resolveCanonicalGuardianRequest,
|
|
27
16
|
} from "../../memory/canonical-guardian-store.js";
|
|
28
17
|
import { getAttentionStateByConversationIds } from "../../memory/conversation-attention-store.js";
|
|
@@ -33,34 +22,15 @@ import {
|
|
|
33
22
|
UNTITLED_FALLBACK,
|
|
34
23
|
} from "../../memory/conversation-title-service.js";
|
|
35
24
|
import * as externalConversationStore from "../../memory/external-conversation-store.js";
|
|
36
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
|
|
37
|
-
import { routeGuardianReply } from "../../runtime/guardian-reply-router.js";
|
|
38
|
-
import {
|
|
39
|
-
resolveLocalIpcAuthContext,
|
|
40
|
-
resolveLocalIpcTrustContext,
|
|
41
|
-
} from "../../runtime/local-actor-identity.js";
|
|
42
25
|
import * as pendingInteractions from "../../runtime/pending-interactions.js";
|
|
43
|
-
import { checkIngressForSecrets } from "../../security/secret-ingress.js";
|
|
44
|
-
import {
|
|
45
|
-
compileCustomPatterns,
|
|
46
|
-
redactSecrets,
|
|
47
|
-
} from "../../security/secret-scanner.js";
|
|
48
26
|
import { getSubagentManager } from "../../subagent/index.js";
|
|
49
|
-
import { silentlyWithLog } from "../../util/silently.js";
|
|
50
27
|
import { truncate } from "../../util/truncate.js";
|
|
51
|
-
import { createApprovalConversationGenerator } from "../approval-generators.js";
|
|
52
|
-
import { getAssistantName } from "../identity-helpers.js";
|
|
53
|
-
import type { UserMessageAttachment } from "../ipc-contract.js";
|
|
54
28
|
import type {
|
|
55
29
|
CancelRequest,
|
|
56
30
|
ConfirmationResponse,
|
|
57
|
-
ConversationSearchRequest,
|
|
58
31
|
DeleteQueuedMessage,
|
|
59
|
-
HistoryRequest,
|
|
60
|
-
MessageContentRequest,
|
|
61
32
|
RegenerateRequest,
|
|
62
33
|
ReorderThreadsRequest,
|
|
63
|
-
SandboxSetRequest,
|
|
64
34
|
SecretResponse,
|
|
65
35
|
ServerMessage,
|
|
66
36
|
SessionCreateRequest,
|
|
@@ -68,46 +38,30 @@ import type {
|
|
|
68
38
|
SessionSwitchRequest,
|
|
69
39
|
UndoRequest,
|
|
70
40
|
UsageRequest,
|
|
71
|
-
UserMessage,
|
|
72
41
|
} from "../ipc-protocol.js";
|
|
73
42
|
import { normalizeThreadType } from "../ipc-protocol.js";
|
|
74
|
-
import { executeRecordingIntent } from "../recording-executor.js";
|
|
75
|
-
import { resolveRecordingIntent } from "../recording-intent.js";
|
|
76
|
-
import {
|
|
77
|
-
classifyRecordingIntentFallback,
|
|
78
|
-
containsRecordingKeywords,
|
|
79
|
-
} from "../recording-intent-fallback.js";
|
|
80
43
|
import type { Session } from "../session.js";
|
|
81
44
|
import {
|
|
82
45
|
buildSessionErrorMessage,
|
|
83
46
|
classifySessionError,
|
|
84
47
|
} from "../session-error.js";
|
|
85
|
-
import { resolveChannelCapabilities } from "../session-runtime-assembly.js";
|
|
86
|
-
import { generateVideoThumbnail } from "../video-thumbnail.js";
|
|
87
48
|
import {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
handleRecordingStop,
|
|
93
|
-
} from "./recording.js";
|
|
49
|
+
handleConversationSearch,
|
|
50
|
+
handleHistoryRequest,
|
|
51
|
+
handleMessageContentRequest,
|
|
52
|
+
} from "./session-history.js";
|
|
94
53
|
import {
|
|
95
54
|
defineHandlers,
|
|
96
55
|
type HandlerContext,
|
|
97
|
-
type HistorySurface,
|
|
98
|
-
type HistoryToolCall,
|
|
99
56
|
log,
|
|
100
|
-
mergeToolResults,
|
|
101
|
-
type ParsedHistoryMessage,
|
|
102
57
|
pendingStandaloneSecrets,
|
|
103
|
-
renderHistoryContent,
|
|
104
58
|
wireEscalationHandler,
|
|
105
59
|
} from "./shared.js";
|
|
60
|
+
// Re-export for backward compatibility — tests and other consumers import from sessions.js
|
|
61
|
+
export { handleUserMessage } from "./session-user-message.js";
|
|
62
|
+
import { handleUserMessage } from "./session-user-message.js";
|
|
106
63
|
|
|
107
|
-
|
|
108
|
-
createApprovalConversationGenerator();
|
|
109
|
-
|
|
110
|
-
function syncCanonicalStatusFromIpcConfirmationDecision(
|
|
64
|
+
export function syncCanonicalStatusFromIpcConfirmationDecision(
|
|
111
65
|
requestId: string,
|
|
112
66
|
decision: ConfirmationResponse["decision"],
|
|
113
67
|
): void {
|
|
@@ -128,7 +82,7 @@ function syncCanonicalStatusFromIpcConfirmationDecision(
|
|
|
128
82
|
}
|
|
129
83
|
}
|
|
130
84
|
|
|
131
|
-
function makeIpcEventSender(params: {
|
|
85
|
+
export function makeIpcEventSender(params: {
|
|
132
86
|
ctx: HandlerContext;
|
|
133
87
|
socket: net.Socket;
|
|
134
88
|
session: Session;
|
|
@@ -187,904 +141,6 @@ function makeIpcEventSender(params: {
|
|
|
187
141
|
};
|
|
188
142
|
}
|
|
189
143
|
|
|
190
|
-
export async function handleUserMessage(
|
|
191
|
-
msg: UserMessage,
|
|
192
|
-
socket: net.Socket,
|
|
193
|
-
ctx: HandlerContext,
|
|
194
|
-
): Promise<void> {
|
|
195
|
-
const requestId = uuid();
|
|
196
|
-
const rlog = log.child({ sessionId: msg.sessionId, requestId });
|
|
197
|
-
try {
|
|
198
|
-
ctx.socketToSession.set(socket, msg.sessionId);
|
|
199
|
-
const session = await ctx.getOrCreateSession(msg.sessionId, socket, true);
|
|
200
|
-
// Only wire the escalation handler if one isn't already set — handleTaskSubmit
|
|
201
|
-
// sets a handler with the client's actual screen dimensions, and overwriting it
|
|
202
|
-
// here would replace those dimensions with the daemon's defaults.
|
|
203
|
-
if (!session.hasEscalationHandler()) {
|
|
204
|
-
wireEscalationHandler(session, socket, ctx);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const ipcChannel = parseChannelId(msg.channel) ?? "vellum";
|
|
208
|
-
const sendEvent = makeIpcEventSender({
|
|
209
|
-
ctx,
|
|
210
|
-
socket,
|
|
211
|
-
session,
|
|
212
|
-
conversationId: msg.sessionId,
|
|
213
|
-
sourceChannel: ipcChannel,
|
|
214
|
-
});
|
|
215
|
-
// Route prompter-originated events (confirmation_request/secret_request)
|
|
216
|
-
// through the IPC wrapper so pending-interactions + canonical tracking
|
|
217
|
-
// are updated before the message is sent to the client.
|
|
218
|
-
session.updateClient(sendEvent, false);
|
|
219
|
-
const ipcInterface = parseInterfaceId(msg.interface);
|
|
220
|
-
if (!ipcInterface) {
|
|
221
|
-
ctx.send(socket, {
|
|
222
|
-
type: "error",
|
|
223
|
-
message:
|
|
224
|
-
"Invalid user_message: interface is required and must be valid",
|
|
225
|
-
});
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const queuedChannelMetadata = {
|
|
229
|
-
userMessageChannel: ipcChannel,
|
|
230
|
-
assistantMessageChannel: ipcChannel,
|
|
231
|
-
userMessageInterface: ipcInterface,
|
|
232
|
-
assistantMessageInterface: ipcInterface,
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
// Update channel capabilities eagerly so both immediate and queued paths
|
|
236
|
-
// reflect the latest PTT / microphone state from the client.
|
|
237
|
-
session.setChannelCapabilities(
|
|
238
|
-
resolveChannelCapabilities(ipcChannel, ipcInterface, {
|
|
239
|
-
pttActivationKey: msg.pttActivationKey,
|
|
240
|
-
microphonePermissionGranted: msg.microphonePermissionGranted,
|
|
241
|
-
}),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
const dispatchUserMessage = (
|
|
245
|
-
content: string,
|
|
246
|
-
attachments: UserMessageAttachment[],
|
|
247
|
-
dispatchRequestId: string,
|
|
248
|
-
source: "user_message" | "secure_redirect_resume",
|
|
249
|
-
activeSurfaceId?: string,
|
|
250
|
-
currentPage?: string,
|
|
251
|
-
displayContent?: string,
|
|
252
|
-
): void => {
|
|
253
|
-
const receivedDescription =
|
|
254
|
-
source === "user_message"
|
|
255
|
-
? "User message received"
|
|
256
|
-
: "Resuming message after secure credential save";
|
|
257
|
-
const queuedDescription =
|
|
258
|
-
source === "user_message"
|
|
259
|
-
? "Message queued (session busy)"
|
|
260
|
-
: "Resumed message queued (session busy)";
|
|
261
|
-
|
|
262
|
-
session.traceEmitter.emit("request_received", receivedDescription, {
|
|
263
|
-
requestId: dispatchRequestId,
|
|
264
|
-
status: "info",
|
|
265
|
-
attributes: { source },
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
const result = session.enqueueMessage(
|
|
269
|
-
content,
|
|
270
|
-
attachments,
|
|
271
|
-
sendEvent,
|
|
272
|
-
dispatchRequestId,
|
|
273
|
-
activeSurfaceId,
|
|
274
|
-
currentPage,
|
|
275
|
-
queuedChannelMetadata,
|
|
276
|
-
undefined,
|
|
277
|
-
displayContent,
|
|
278
|
-
);
|
|
279
|
-
if (result.rejected) {
|
|
280
|
-
rlog.warn({ source }, "Message rejected — queue is full");
|
|
281
|
-
session.traceEmitter.emit(
|
|
282
|
-
"request_error",
|
|
283
|
-
"Message rejected — queue is full",
|
|
284
|
-
{
|
|
285
|
-
requestId: dispatchRequestId,
|
|
286
|
-
status: "error",
|
|
287
|
-
attributes: {
|
|
288
|
-
reason: "queue_full",
|
|
289
|
-
queueDepth: session.getQueueDepth(),
|
|
290
|
-
source,
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
);
|
|
294
|
-
ctx.send(
|
|
295
|
-
socket,
|
|
296
|
-
buildSessionErrorMessage(msg.sessionId, {
|
|
297
|
-
code: "QUEUE_FULL",
|
|
298
|
-
userMessage:
|
|
299
|
-
"Message queue is full (max depth: 10). Please wait for current messages to be processed.",
|
|
300
|
-
retryable: true,
|
|
301
|
-
debugDetails: "Message rejected — session queue is full",
|
|
302
|
-
}),
|
|
303
|
-
);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (result.queued) {
|
|
307
|
-
const position = session.getQueueDepth();
|
|
308
|
-
rlog.info({ source, position }, queuedDescription);
|
|
309
|
-
session.traceEmitter.emit(
|
|
310
|
-
"request_queued",
|
|
311
|
-
`Message queued at position ${position}`,
|
|
312
|
-
{
|
|
313
|
-
requestId: dispatchRequestId,
|
|
314
|
-
status: "info",
|
|
315
|
-
attributes: { position, source },
|
|
316
|
-
},
|
|
317
|
-
);
|
|
318
|
-
ctx.send(socket, {
|
|
319
|
-
type: "message_queued",
|
|
320
|
-
sessionId: msg.sessionId,
|
|
321
|
-
requestId: dispatchRequestId,
|
|
322
|
-
position,
|
|
323
|
-
});
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
rlog.info({ source }, "Processing user message");
|
|
328
|
-
session.emitActivityState(
|
|
329
|
-
"thinking",
|
|
330
|
-
"message_dequeued",
|
|
331
|
-
"assistant_turn",
|
|
332
|
-
dispatchRequestId,
|
|
333
|
-
);
|
|
334
|
-
session.setTurnChannelContext({
|
|
335
|
-
userMessageChannel: ipcChannel,
|
|
336
|
-
assistantMessageChannel: ipcChannel,
|
|
337
|
-
});
|
|
338
|
-
session.setTurnInterfaceContext({
|
|
339
|
-
userMessageInterface: ipcInterface,
|
|
340
|
-
assistantMessageInterface: ipcInterface,
|
|
341
|
-
});
|
|
342
|
-
session.setAssistantId(DAEMON_INTERNAL_ASSISTANT_ID);
|
|
343
|
-
// Resolve local IPC actor identity through the same trust pipeline
|
|
344
|
-
// used by HTTP channel ingress. The vellum guardian binding provides
|
|
345
|
-
// the guardianPrincipalId, and resolveTrustContext classifies the
|
|
346
|
-
// local user as 'guardian' via binding match.
|
|
347
|
-
session.setTrustContext(resolveLocalIpcTrustContext(ipcChannel));
|
|
348
|
-
// Align IPC sessions with the same AuthContext shape as HTTP sessions.
|
|
349
|
-
session.setAuthContext(resolveLocalIpcAuthContext(msg.sessionId));
|
|
350
|
-
session.setCommandIntent(null);
|
|
351
|
-
// Fire-and-forget: don't block the IPC handler so the connection can
|
|
352
|
-
// continue receiving messages (e.g. cancel, confirmations, or
|
|
353
|
-
// additional user_message that will be queued by the session).
|
|
354
|
-
session
|
|
355
|
-
.processMessage(
|
|
356
|
-
content,
|
|
357
|
-
attachments,
|
|
358
|
-
sendEvent,
|
|
359
|
-
dispatchRequestId,
|
|
360
|
-
activeSurfaceId,
|
|
361
|
-
currentPage,
|
|
362
|
-
undefined,
|
|
363
|
-
displayContent,
|
|
364
|
-
)
|
|
365
|
-
.catch((err) => {
|
|
366
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
367
|
-
rlog.error(
|
|
368
|
-
{ err, source },
|
|
369
|
-
"Error processing user message (session or provider failure)",
|
|
370
|
-
);
|
|
371
|
-
ctx.send(socket, {
|
|
372
|
-
type: "error",
|
|
373
|
-
message: `Failed to process message: ${message}`,
|
|
374
|
-
});
|
|
375
|
-
const classified = classifySessionError(err, { phase: "agent_loop" });
|
|
376
|
-
ctx.send(socket, buildSessionErrorMessage(msg.sessionId, classified));
|
|
377
|
-
});
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
const config = getConfig();
|
|
381
|
-
let messageText = msg.content ?? "";
|
|
382
|
-
|
|
383
|
-
// Block inbound messages that contain secrets and redirect to secure prompt
|
|
384
|
-
if (!msg.bypassSecretCheck) {
|
|
385
|
-
const ingressCheck = checkIngressForSecrets(messageText);
|
|
386
|
-
if (ingressCheck.blocked) {
|
|
387
|
-
rlog.warn(
|
|
388
|
-
{ detectedTypes: ingressCheck.detectedTypes },
|
|
389
|
-
"Blocked user message containing secrets",
|
|
390
|
-
);
|
|
391
|
-
ctx.send(socket, {
|
|
392
|
-
type: "error",
|
|
393
|
-
message: ingressCheck.userNotice!,
|
|
394
|
-
category: "secret_blocked",
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const compiledCustom = config.secretDetection.customPatterns?.length
|
|
398
|
-
? compileCustomPatterns(config.secretDetection.customPatterns)
|
|
399
|
-
: undefined;
|
|
400
|
-
const redactedMessageText = redactSecrets(
|
|
401
|
-
messageText,
|
|
402
|
-
{
|
|
403
|
-
enabled: true,
|
|
404
|
-
base64Threshold: config.secretDetection.entropyThreshold,
|
|
405
|
-
},
|
|
406
|
-
compiledCustom,
|
|
407
|
-
).trim();
|
|
408
|
-
|
|
409
|
-
// Redirect: trigger a secure prompt so the user can enter the secret safely.
|
|
410
|
-
// After save, continue the same request with redacted text so the model keeps
|
|
411
|
-
// user intent without ever receiving the raw secret value.
|
|
412
|
-
session.redirectToSecurePrompt(ingressCheck.detectedTypes, {
|
|
413
|
-
onStored: (record) => {
|
|
414
|
-
ctx.send(socket, {
|
|
415
|
-
type: "assistant_text_delta",
|
|
416
|
-
sessionId: msg.sessionId,
|
|
417
|
-
text: "Saved your secret securely. Continuing with your request.",
|
|
418
|
-
});
|
|
419
|
-
ctx.send(socket, {
|
|
420
|
-
type: "message_complete",
|
|
421
|
-
sessionId: msg.sessionId,
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
const continuationParts: string[] = [];
|
|
425
|
-
if (redactedMessageText.length > 0)
|
|
426
|
-
continuationParts.push(redactedMessageText);
|
|
427
|
-
continuationParts.push(
|
|
428
|
-
`I entered the redacted secret via the Secure Credential UI and saved it as credential ${record.service}/${record.field}. ` +
|
|
429
|
-
"Continue with my request using that stored credential and do not ask me to paste the secret again.",
|
|
430
|
-
);
|
|
431
|
-
const continuationMessage = continuationParts.join("\n\n");
|
|
432
|
-
const continuationRequestId = uuid();
|
|
433
|
-
dispatchUserMessage(
|
|
434
|
-
continuationMessage,
|
|
435
|
-
msg.attachments ?? [],
|
|
436
|
-
continuationRequestId,
|
|
437
|
-
"secure_redirect_resume",
|
|
438
|
-
msg.activeSurfaceId,
|
|
439
|
-
msg.currentPage,
|
|
440
|
-
);
|
|
441
|
-
},
|
|
442
|
-
});
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// ── Structured command intent (bypasses text parsing) ──────────────────
|
|
448
|
-
if (
|
|
449
|
-
config.daemon.standaloneRecording &&
|
|
450
|
-
msg.commandIntent?.domain === "screen_recording"
|
|
451
|
-
) {
|
|
452
|
-
const action = msg.commandIntent.action;
|
|
453
|
-
rlog.info(
|
|
454
|
-
{ action, source: "commandIntent" },
|
|
455
|
-
"Recording command intent received in user_message",
|
|
456
|
-
);
|
|
457
|
-
if (action === "start") {
|
|
458
|
-
const recordingId = handleRecordingStart(
|
|
459
|
-
msg.sessionId,
|
|
460
|
-
{ promptForSource: true },
|
|
461
|
-
socket,
|
|
462
|
-
ctx,
|
|
463
|
-
);
|
|
464
|
-
const responseText = recordingId
|
|
465
|
-
? "Starting screen recording."
|
|
466
|
-
: "A recording is already active.";
|
|
467
|
-
ctx.send(socket, {
|
|
468
|
-
type: "assistant_text_delta",
|
|
469
|
-
text: responseText,
|
|
470
|
-
sessionId: msg.sessionId,
|
|
471
|
-
});
|
|
472
|
-
ctx.send(socket, {
|
|
473
|
-
type: "message_complete",
|
|
474
|
-
sessionId: msg.sessionId,
|
|
475
|
-
});
|
|
476
|
-
await conversationStore.addMessage(
|
|
477
|
-
msg.sessionId,
|
|
478
|
-
"user",
|
|
479
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
480
|
-
);
|
|
481
|
-
await conversationStore.addMessage(
|
|
482
|
-
msg.sessionId,
|
|
483
|
-
"assistant",
|
|
484
|
-
JSON.stringify([{ type: "text", text: responseText }]),
|
|
485
|
-
);
|
|
486
|
-
// Keep in-memory session history aligned with DB so regenerate() and
|
|
487
|
-
// other history operations that rely on session.messages stay consistent.
|
|
488
|
-
// Only push when agent loop is NOT active to avoid corrupting role alternation.
|
|
489
|
-
if (!session.isProcessing()) {
|
|
490
|
-
session.messages.push({
|
|
491
|
-
role: "user",
|
|
492
|
-
content: [{ type: "text", text: messageText }],
|
|
493
|
-
});
|
|
494
|
-
session.messages.push({
|
|
495
|
-
role: "assistant",
|
|
496
|
-
content: [{ type: "text", text: responseText }],
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
return;
|
|
500
|
-
} else if (action === "stop") {
|
|
501
|
-
const stopped = handleRecordingStop(msg.sessionId, ctx) !== undefined;
|
|
502
|
-
const responseText = stopped
|
|
503
|
-
? "Stopping the recording."
|
|
504
|
-
: "No active recording to stop.";
|
|
505
|
-
ctx.send(socket, {
|
|
506
|
-
type: "assistant_text_delta",
|
|
507
|
-
text: responseText,
|
|
508
|
-
sessionId: msg.sessionId,
|
|
509
|
-
});
|
|
510
|
-
ctx.send(socket, {
|
|
511
|
-
type: "message_complete",
|
|
512
|
-
sessionId: msg.sessionId,
|
|
513
|
-
});
|
|
514
|
-
await conversationStore.addMessage(
|
|
515
|
-
msg.sessionId,
|
|
516
|
-
"user",
|
|
517
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
518
|
-
);
|
|
519
|
-
await conversationStore.addMessage(
|
|
520
|
-
msg.sessionId,
|
|
521
|
-
"assistant",
|
|
522
|
-
JSON.stringify([{ type: "text", text: responseText }]),
|
|
523
|
-
);
|
|
524
|
-
if (!session.isProcessing()) {
|
|
525
|
-
session.messages.push({
|
|
526
|
-
role: "user",
|
|
527
|
-
content: [{ type: "text", text: messageText }],
|
|
528
|
-
});
|
|
529
|
-
session.messages.push({
|
|
530
|
-
role: "assistant",
|
|
531
|
-
content: [{ type: "text", text: responseText }],
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
return;
|
|
535
|
-
} else if (action === "restart") {
|
|
536
|
-
const restartResult = handleRecordingRestart(
|
|
537
|
-
msg.sessionId,
|
|
538
|
-
socket,
|
|
539
|
-
ctx,
|
|
540
|
-
);
|
|
541
|
-
ctx.send(socket, {
|
|
542
|
-
type: "assistant_text_delta",
|
|
543
|
-
text: restartResult.responseText,
|
|
544
|
-
sessionId: msg.sessionId,
|
|
545
|
-
});
|
|
546
|
-
ctx.send(socket, {
|
|
547
|
-
type: "message_complete",
|
|
548
|
-
sessionId: msg.sessionId,
|
|
549
|
-
});
|
|
550
|
-
await conversationStore.addMessage(
|
|
551
|
-
msg.sessionId,
|
|
552
|
-
"user",
|
|
553
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
554
|
-
);
|
|
555
|
-
await conversationStore.addMessage(
|
|
556
|
-
msg.sessionId,
|
|
557
|
-
"assistant",
|
|
558
|
-
JSON.stringify([{ type: "text", text: restartResult.responseText }]),
|
|
559
|
-
);
|
|
560
|
-
if (!session.isProcessing()) {
|
|
561
|
-
session.messages.push({
|
|
562
|
-
role: "user",
|
|
563
|
-
content: [{ type: "text", text: messageText }],
|
|
564
|
-
});
|
|
565
|
-
session.messages.push({
|
|
566
|
-
role: "assistant",
|
|
567
|
-
content: [{ type: "text", text: restartResult.responseText }],
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
return;
|
|
571
|
-
} else if (action === "pause") {
|
|
572
|
-
const paused = handleRecordingPause(msg.sessionId, ctx) !== undefined;
|
|
573
|
-
const responseText = paused
|
|
574
|
-
? "Pausing the recording."
|
|
575
|
-
: "No active recording to pause.";
|
|
576
|
-
ctx.send(socket, {
|
|
577
|
-
type: "assistant_text_delta",
|
|
578
|
-
text: responseText,
|
|
579
|
-
sessionId: msg.sessionId,
|
|
580
|
-
});
|
|
581
|
-
ctx.send(socket, {
|
|
582
|
-
type: "message_complete",
|
|
583
|
-
sessionId: msg.sessionId,
|
|
584
|
-
});
|
|
585
|
-
await conversationStore.addMessage(
|
|
586
|
-
msg.sessionId,
|
|
587
|
-
"user",
|
|
588
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
589
|
-
);
|
|
590
|
-
await conversationStore.addMessage(
|
|
591
|
-
msg.sessionId,
|
|
592
|
-
"assistant",
|
|
593
|
-
JSON.stringify([{ type: "text", text: responseText }]),
|
|
594
|
-
);
|
|
595
|
-
if (!session.isProcessing()) {
|
|
596
|
-
session.messages.push({
|
|
597
|
-
role: "user",
|
|
598
|
-
content: [{ type: "text", text: messageText }],
|
|
599
|
-
});
|
|
600
|
-
session.messages.push({
|
|
601
|
-
role: "assistant",
|
|
602
|
-
content: [{ type: "text", text: responseText }],
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
return;
|
|
606
|
-
} else if (action === "resume") {
|
|
607
|
-
const resumed = handleRecordingResume(msg.sessionId, ctx) !== undefined;
|
|
608
|
-
const responseText = resumed
|
|
609
|
-
? "Resuming the recording."
|
|
610
|
-
: "No active recording to resume.";
|
|
611
|
-
ctx.send(socket, {
|
|
612
|
-
type: "assistant_text_delta",
|
|
613
|
-
text: responseText,
|
|
614
|
-
sessionId: msg.sessionId,
|
|
615
|
-
});
|
|
616
|
-
ctx.send(socket, {
|
|
617
|
-
type: "message_complete",
|
|
618
|
-
sessionId: msg.sessionId,
|
|
619
|
-
});
|
|
620
|
-
await conversationStore.addMessage(
|
|
621
|
-
msg.sessionId,
|
|
622
|
-
"user",
|
|
623
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
624
|
-
);
|
|
625
|
-
await conversationStore.addMessage(
|
|
626
|
-
msg.sessionId,
|
|
627
|
-
"assistant",
|
|
628
|
-
JSON.stringify([{ type: "text", text: responseText }]),
|
|
629
|
-
);
|
|
630
|
-
if (!session.isProcessing()) {
|
|
631
|
-
session.messages.push({
|
|
632
|
-
role: "user",
|
|
633
|
-
content: [{ type: "text", text: messageText }],
|
|
634
|
-
});
|
|
635
|
-
session.messages.push({
|
|
636
|
-
role: "assistant",
|
|
637
|
-
content: [{ type: "text", text: responseText }],
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
return;
|
|
641
|
-
} else {
|
|
642
|
-
// Unrecognized action — fall through to normal text handling
|
|
643
|
-
rlog.warn(
|
|
644
|
-
{ action, source: "commandIntent" },
|
|
645
|
-
"Unrecognized screen_recording action, falling through to text handling",
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// ── Standalone recording intent interception ──────────────────────────
|
|
651
|
-
let originalContentBeforeStrip: string | undefined;
|
|
652
|
-
if (config.daemon.standaloneRecording && messageText) {
|
|
653
|
-
const name = getAssistantName();
|
|
654
|
-
const dynamicNames = [name].filter(Boolean) as string[];
|
|
655
|
-
const intentResult = resolveRecordingIntent(messageText, dynamicNames);
|
|
656
|
-
|
|
657
|
-
if (
|
|
658
|
-
intentResult.kind === "start_only" ||
|
|
659
|
-
intentResult.kind === "stop_only" ||
|
|
660
|
-
intentResult.kind === "start_and_stop_only" ||
|
|
661
|
-
intentResult.kind === "restart_only" ||
|
|
662
|
-
intentResult.kind === "pause_only" ||
|
|
663
|
-
intentResult.kind === "resume_only"
|
|
664
|
-
) {
|
|
665
|
-
const execResult = executeRecordingIntent(intentResult, {
|
|
666
|
-
conversationId: msg.sessionId,
|
|
667
|
-
socket,
|
|
668
|
-
ctx,
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
if (execResult.handled) {
|
|
672
|
-
rlog.info(
|
|
673
|
-
{ kind: intentResult.kind },
|
|
674
|
-
"Recording intent intercepted in user_message",
|
|
675
|
-
);
|
|
676
|
-
ctx.send(socket, {
|
|
677
|
-
type: "assistant_text_delta",
|
|
678
|
-
text: execResult.responseText!,
|
|
679
|
-
sessionId: msg.sessionId,
|
|
680
|
-
});
|
|
681
|
-
ctx.send(socket, {
|
|
682
|
-
type: "message_complete",
|
|
683
|
-
sessionId: msg.sessionId,
|
|
684
|
-
});
|
|
685
|
-
await conversationStore.addMessage(
|
|
686
|
-
msg.sessionId,
|
|
687
|
-
"user",
|
|
688
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
689
|
-
);
|
|
690
|
-
await conversationStore.addMessage(
|
|
691
|
-
msg.sessionId,
|
|
692
|
-
"assistant",
|
|
693
|
-
JSON.stringify([{ type: "text", text: execResult.responseText! }]),
|
|
694
|
-
);
|
|
695
|
-
if (!session.isProcessing()) {
|
|
696
|
-
session.messages.push({
|
|
697
|
-
role: "user",
|
|
698
|
-
content: [{ type: "text", text: messageText }],
|
|
699
|
-
});
|
|
700
|
-
session.messages.push({
|
|
701
|
-
role: "assistant",
|
|
702
|
-
content: [{ type: "text", text: execResult.responseText! }],
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
intentResult.kind === "start_with_remainder" ||
|
|
711
|
-
intentResult.kind === "stop_with_remainder" ||
|
|
712
|
-
intentResult.kind === "start_and_stop_with_remainder" ||
|
|
713
|
-
intentResult.kind === "restart_with_remainder"
|
|
714
|
-
) {
|
|
715
|
-
const execResult = executeRecordingIntent(intentResult, {
|
|
716
|
-
conversationId: msg.sessionId,
|
|
717
|
-
socket,
|
|
718
|
-
ctx,
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
// Preserve the original text so the DB stores the full message
|
|
722
|
-
originalContentBeforeStrip = messageText;
|
|
723
|
-
|
|
724
|
-
// Continue with stripped text for downstream processing
|
|
725
|
-
msg.content = execResult.remainderText ?? messageText;
|
|
726
|
-
messageText = msg.content;
|
|
727
|
-
|
|
728
|
-
// Execute the recording side effects that executeRecordingIntent deferred
|
|
729
|
-
if (intentResult.kind === "stop_with_remainder") {
|
|
730
|
-
handleRecordingStop(msg.sessionId, ctx);
|
|
731
|
-
}
|
|
732
|
-
if (intentResult.kind === "start_with_remainder") {
|
|
733
|
-
handleRecordingStart(
|
|
734
|
-
msg.sessionId,
|
|
735
|
-
{ promptForSource: true },
|
|
736
|
-
socket,
|
|
737
|
-
ctx,
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
// start_and_stop_with_remainder / restart_with_remainder — route through
|
|
741
|
-
// handleRecordingRestart which properly cleans up maps between stop and start.
|
|
742
|
-
if (
|
|
743
|
-
intentResult.kind === "restart_with_remainder" ||
|
|
744
|
-
intentResult.kind === "start_and_stop_with_remainder"
|
|
745
|
-
) {
|
|
746
|
-
const restartResult = handleRecordingRestart(
|
|
747
|
-
msg.sessionId,
|
|
748
|
-
socket,
|
|
749
|
-
ctx,
|
|
750
|
-
);
|
|
751
|
-
// Only fall back to plain start for start_and_stop_with_remainder.
|
|
752
|
-
// restart_with_remainder should NOT silently start a new recording when idle.
|
|
753
|
-
if (
|
|
754
|
-
!restartResult.initiated &&
|
|
755
|
-
restartResult.reason === "no_active_recording" &&
|
|
756
|
-
intentResult.kind === "start_and_stop_with_remainder"
|
|
757
|
-
) {
|
|
758
|
-
handleRecordingStart(
|
|
759
|
-
msg.sessionId,
|
|
760
|
-
{ promptForSource: true },
|
|
761
|
-
socket,
|
|
762
|
-
ctx,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
rlog.info(
|
|
768
|
-
{ remaining: msg.content, kind: intentResult.kind },
|
|
769
|
-
"Recording intent with remainder — continuing with remaining text",
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// 'none' — deterministic resolver found nothing; try LLM fallback
|
|
774
|
-
// if the text contains recording-related keywords.
|
|
775
|
-
if (
|
|
776
|
-
intentResult.kind === "none" &&
|
|
777
|
-
containsRecordingKeywords(messageText)
|
|
778
|
-
) {
|
|
779
|
-
const fallback = await classifyRecordingIntentFallback(messageText);
|
|
780
|
-
rlog.info(
|
|
781
|
-
{
|
|
782
|
-
fallbackAction: fallback.action,
|
|
783
|
-
fallbackConfidence: fallback.confidence,
|
|
784
|
-
},
|
|
785
|
-
"Recording intent LLM fallback result",
|
|
786
|
-
);
|
|
787
|
-
|
|
788
|
-
if (fallback.action !== "none" && fallback.confidence === "high") {
|
|
789
|
-
const kindMap: Record<
|
|
790
|
-
string,
|
|
791
|
-
import("../recording-intent.js").RecordingIntentResult
|
|
792
|
-
> = {
|
|
793
|
-
start: { kind: "start_only" },
|
|
794
|
-
stop: { kind: "stop_only" },
|
|
795
|
-
restart: { kind: "restart_only" },
|
|
796
|
-
pause: { kind: "pause_only" },
|
|
797
|
-
resume: { kind: "resume_only" },
|
|
798
|
-
};
|
|
799
|
-
const mapped = kindMap[fallback.action];
|
|
800
|
-
if (mapped) {
|
|
801
|
-
const execResult = executeRecordingIntent(mapped, {
|
|
802
|
-
conversationId: msg.sessionId,
|
|
803
|
-
socket,
|
|
804
|
-
ctx,
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
if (execResult.handled) {
|
|
808
|
-
rlog.info(
|
|
809
|
-
{ kind: mapped.kind, source: "llm_fallback" },
|
|
810
|
-
"Recording intent intercepted via LLM fallback",
|
|
811
|
-
);
|
|
812
|
-
ctx.send(socket, {
|
|
813
|
-
type: "assistant_text_delta",
|
|
814
|
-
text: execResult.responseText!,
|
|
815
|
-
sessionId: msg.sessionId,
|
|
816
|
-
});
|
|
817
|
-
ctx.send(socket, {
|
|
818
|
-
type: "message_complete",
|
|
819
|
-
sessionId: msg.sessionId,
|
|
820
|
-
});
|
|
821
|
-
await conversationStore.addMessage(
|
|
822
|
-
msg.sessionId,
|
|
823
|
-
"user",
|
|
824
|
-
JSON.stringify([{ type: "text", text: messageText }]),
|
|
825
|
-
);
|
|
826
|
-
await conversationStore.addMessage(
|
|
827
|
-
msg.sessionId,
|
|
828
|
-
"assistant",
|
|
829
|
-
JSON.stringify([
|
|
830
|
-
{ type: "text", text: execResult.responseText! },
|
|
831
|
-
]),
|
|
832
|
-
);
|
|
833
|
-
if (!session.isProcessing()) {
|
|
834
|
-
session.messages.push({
|
|
835
|
-
role: "user",
|
|
836
|
-
content: [{ type: "text", text: messageText }],
|
|
837
|
-
});
|
|
838
|
-
session.messages.push({
|
|
839
|
-
role: "assistant",
|
|
840
|
-
content: [{ type: "text", text: execResult.responseText! }],
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// If a live turn is waiting on confirmation, try to consume this text as
|
|
851
|
-
// an inline approval decision before auto-deny. We intentionally do not
|
|
852
|
-
// gate on queue depth: users often retry "approve"/"yes" while the queue
|
|
853
|
-
// is draining after a prior denial, and requiring an empty queue causes a
|
|
854
|
-
// deny/retry cascade where natural-language approvals never land.
|
|
855
|
-
if (session.hasAnyPendingConfirmation() && messageText.trim().length > 0) {
|
|
856
|
-
try {
|
|
857
|
-
const pendingInteractionRequestIdsForConversation = pendingInteractions
|
|
858
|
-
.getByConversation(msg.sessionId)
|
|
859
|
-
.filter(
|
|
860
|
-
(interaction) =>
|
|
861
|
-
interaction.kind === "confirmation" &&
|
|
862
|
-
interaction.session === session &&
|
|
863
|
-
session.hasPendingConfirmation(interaction.requestId),
|
|
864
|
-
)
|
|
865
|
-
.map((interaction) => interaction.requestId);
|
|
866
|
-
|
|
867
|
-
const pendingCanonicalRequestIdsForConversation = [
|
|
868
|
-
...listPendingCanonicalGuardianRequestsByDestinationConversation(
|
|
869
|
-
msg.sessionId,
|
|
870
|
-
ipcChannel,
|
|
871
|
-
)
|
|
872
|
-
.filter((request) => request.kind === "tool_approval")
|
|
873
|
-
.map((request) => request.id),
|
|
874
|
-
...listCanonicalGuardianRequests({
|
|
875
|
-
status: "pending",
|
|
876
|
-
conversationId: msg.sessionId,
|
|
877
|
-
kind: "tool_approval",
|
|
878
|
-
}).map((request) => request.id),
|
|
879
|
-
].filter((pendingRequestId) =>
|
|
880
|
-
session.hasPendingConfirmation(pendingRequestId),
|
|
881
|
-
);
|
|
882
|
-
|
|
883
|
-
const pendingRequestIdsForConversation = Array.from(
|
|
884
|
-
new Set([
|
|
885
|
-
...pendingInteractionRequestIdsForConversation,
|
|
886
|
-
...pendingCanonicalRequestIdsForConversation,
|
|
887
|
-
]),
|
|
888
|
-
);
|
|
889
|
-
|
|
890
|
-
if (pendingRequestIdsForConversation.length > 0) {
|
|
891
|
-
// Resolve the local IPC actor's principal via the vellum guardian binding
|
|
892
|
-
// for principal-based authorization in the canonical decision primitive.
|
|
893
|
-
const localCtx = resolveLocalIpcTrustContext(ipcChannel);
|
|
894
|
-
const routerResult = await routeGuardianReply({
|
|
895
|
-
messageText: messageText.trim(),
|
|
896
|
-
channel: ipcChannel,
|
|
897
|
-
actor: {
|
|
898
|
-
externalUserId: localCtx.guardianExternalUserId,
|
|
899
|
-
channel: ipcChannel,
|
|
900
|
-
guardianPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
901
|
-
},
|
|
902
|
-
conversationId: msg.sessionId,
|
|
903
|
-
pendingRequestIds: pendingRequestIdsForConversation,
|
|
904
|
-
approvalConversationGenerator: desktopApprovalConversationGenerator,
|
|
905
|
-
emissionContext: {
|
|
906
|
-
source: "inline_nl",
|
|
907
|
-
causedByRequestId: requestId,
|
|
908
|
-
decisionText: messageText.trim(),
|
|
909
|
-
},
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
if (
|
|
913
|
-
routerResult.consumed &&
|
|
914
|
-
routerResult.type !== "nl_keep_pending"
|
|
915
|
-
) {
|
|
916
|
-
// Success-path emissions (approved/denied) are handled centrally
|
|
917
|
-
// by handleConfirmationResponse (called via the resolver chain).
|
|
918
|
-
// However, stale/failed paths never reach handleConfirmationResponse,
|
|
919
|
-
// so we emit resolved_stale here for those cases.
|
|
920
|
-
if (routerResult.requestId && !routerResult.decisionApplied) {
|
|
921
|
-
session.emitConfirmationStateChanged({
|
|
922
|
-
sessionId: msg.sessionId,
|
|
923
|
-
requestId: routerResult.requestId,
|
|
924
|
-
state: "resolved_stale",
|
|
925
|
-
source: "inline_nl",
|
|
926
|
-
causedByRequestId: requestId,
|
|
927
|
-
decisionText: messageText.trim(),
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const consumedChannelMeta = {
|
|
932
|
-
userMessageChannel: ipcChannel,
|
|
933
|
-
assistantMessageChannel: ipcChannel,
|
|
934
|
-
userMessageInterface: ipcInterface,
|
|
935
|
-
assistantMessageInterface: ipcInterface,
|
|
936
|
-
provenanceTrustClass: "guardian" as const,
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
const consumedUserMessage = createUserMessage(
|
|
940
|
-
messageText,
|
|
941
|
-
msg.attachments ?? [],
|
|
942
|
-
);
|
|
943
|
-
await conversationStore.addMessage(
|
|
944
|
-
msg.sessionId,
|
|
945
|
-
"user",
|
|
946
|
-
JSON.stringify(consumedUserMessage.content),
|
|
947
|
-
consumedChannelMeta,
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
const replyText =
|
|
951
|
-
routerResult.replyText?.trim() ||
|
|
952
|
-
(routerResult.decisionApplied
|
|
953
|
-
? "Decision applied."
|
|
954
|
-
: "Request already resolved.");
|
|
955
|
-
const consumedAssistantMessage = createAssistantMessage(replyText);
|
|
956
|
-
await conversationStore.addMessage(
|
|
957
|
-
msg.sessionId,
|
|
958
|
-
"assistant",
|
|
959
|
-
JSON.stringify(consumedAssistantMessage.content),
|
|
960
|
-
consumedChannelMeta,
|
|
961
|
-
);
|
|
962
|
-
// Avoid mutating in-memory history while an agent loop is active;
|
|
963
|
-
// the loop owns history reconstruction for the in-flight turn.
|
|
964
|
-
if (!session.isProcessing()) {
|
|
965
|
-
// Keep in-memory history aligned with persisted transcript so
|
|
966
|
-
// session-history operations (undo/regenerate) target the same turn.
|
|
967
|
-
session.messages.push(
|
|
968
|
-
consumedUserMessage,
|
|
969
|
-
consumedAssistantMessage,
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// Mirror the normal queued/dequeued lifecycle so desktop clients can
|
|
974
|
-
// reconcile queued bubble state for this just-sent user message.
|
|
975
|
-
ctx.send(socket, {
|
|
976
|
-
type: "message_queued",
|
|
977
|
-
sessionId: msg.sessionId,
|
|
978
|
-
requestId,
|
|
979
|
-
position: 0,
|
|
980
|
-
});
|
|
981
|
-
ctx.send(socket, {
|
|
982
|
-
type: "message_dequeued",
|
|
983
|
-
sessionId: msg.sessionId,
|
|
984
|
-
requestId,
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
// Only emit the reply delta when no agent turn is in-flight.
|
|
988
|
-
// When the agent is active, currentAssistantMessageId on the client
|
|
989
|
-
// points to the agent's streaming message and this delta would
|
|
990
|
-
// contaminate it. The reply is already persisted to the DB, so the
|
|
991
|
-
// client will see it on the next transcript reload / session switch.
|
|
992
|
-
if (!session.isProcessing()) {
|
|
993
|
-
ctx.send(socket, {
|
|
994
|
-
type: "assistant_text_delta",
|
|
995
|
-
text: replyText,
|
|
996
|
-
sessionId: msg.sessionId,
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
ctx.send(socket, {
|
|
1000
|
-
type: "message_request_complete",
|
|
1001
|
-
sessionId: msg.sessionId,
|
|
1002
|
-
requestId,
|
|
1003
|
-
runStillActive: session.isProcessing(),
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
rlog.info(
|
|
1007
|
-
{
|
|
1008
|
-
routerType: routerResult.type,
|
|
1009
|
-
decisionApplied: routerResult.decisionApplied,
|
|
1010
|
-
routerRequestId: routerResult.requestId,
|
|
1011
|
-
},
|
|
1012
|
-
"Consumed pending-confirmation reply before auto-deny",
|
|
1013
|
-
);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
} catch (err) {
|
|
1018
|
-
rlog.warn(
|
|
1019
|
-
{ err },
|
|
1020
|
-
"Failed to process pending-confirmation reply; falling back to auto-deny behavior",
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// If the session has a pending tool confirmation, auto-deny it so the
|
|
1026
|
-
// agent can process the user's follow-up message instead. The agent
|
|
1027
|
-
// will see the denial and can re-request the tool if still needed.
|
|
1028
|
-
if (session.hasAnyPendingConfirmation()) {
|
|
1029
|
-
rlog.info("Auto-denying pending confirmation(s) due to new user message");
|
|
1030
|
-
// Emit authoritative confirmation state for each auto-denied request
|
|
1031
|
-
// before the prompter clears them.
|
|
1032
|
-
for (const interaction of pendingInteractions.getByConversation(
|
|
1033
|
-
msg.sessionId,
|
|
1034
|
-
)) {
|
|
1035
|
-
if (
|
|
1036
|
-
interaction.session === session &&
|
|
1037
|
-
interaction.kind === "confirmation"
|
|
1038
|
-
) {
|
|
1039
|
-
session.emitConfirmationStateChanged({
|
|
1040
|
-
sessionId: msg.sessionId,
|
|
1041
|
-
requestId: interaction.requestId,
|
|
1042
|
-
state: "denied",
|
|
1043
|
-
source: "auto_deny",
|
|
1044
|
-
causedByRequestId: requestId,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
session.denyAllPendingConfirmations();
|
|
1049
|
-
// Keep the pending-interaction tracker aligned with the prompter so
|
|
1050
|
-
// stale request IDs are not reused as routing candidates.
|
|
1051
|
-
for (const interaction of pendingInteractions.getByConversation(
|
|
1052
|
-
msg.sessionId,
|
|
1053
|
-
)) {
|
|
1054
|
-
if (
|
|
1055
|
-
interaction.session === session &&
|
|
1056
|
-
interaction.kind === "confirmation"
|
|
1057
|
-
) {
|
|
1058
|
-
syncCanonicalStatusFromIpcConfirmationDecision(
|
|
1059
|
-
interaction.requestId,
|
|
1060
|
-
"deny",
|
|
1061
|
-
);
|
|
1062
|
-
pendingInteractions.resolve(interaction.requestId);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
dispatchUserMessage(
|
|
1068
|
-
messageText,
|
|
1069
|
-
msg.attachments ?? [],
|
|
1070
|
-
requestId,
|
|
1071
|
-
"user_message",
|
|
1072
|
-
msg.activeSurfaceId,
|
|
1073
|
-
msg.currentPage,
|
|
1074
|
-
originalContentBeforeStrip,
|
|
1075
|
-
);
|
|
1076
|
-
} catch (err) {
|
|
1077
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1078
|
-
rlog.error({ err }, "Error setting up user message processing");
|
|
1079
|
-
ctx.send(socket, {
|
|
1080
|
-
type: "error",
|
|
1081
|
-
message: `Failed to process message: ${message}`,
|
|
1082
|
-
});
|
|
1083
|
-
const classified = classifySessionError(err, { phase: "handler" });
|
|
1084
|
-
ctx.send(socket, buildSessionErrorMessage(msg.sessionId, classified));
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
144
|
export function handleConfirmationResponse(
|
|
1089
145
|
msg: ConfirmationResponse,
|
|
1090
146
|
_socket: net.Socket,
|
|
@@ -1463,275 +519,6 @@ export function handleCancel(
|
|
|
1463
519
|
}
|
|
1464
520
|
}
|
|
1465
521
|
|
|
1466
|
-
export function handleHistoryRequest(
|
|
1467
|
-
msg: HistoryRequest,
|
|
1468
|
-
socket: net.Socket,
|
|
1469
|
-
ctx: HandlerContext,
|
|
1470
|
-
): void {
|
|
1471
|
-
// Default to unlimited when callers don't specify a limit, preserving
|
|
1472
|
-
// backward-compatible behavior of returning full conversation history.
|
|
1473
|
-
const limit = msg.limit;
|
|
1474
|
-
|
|
1475
|
-
// Resolve include flags: explicit flags override mode, mode provides defaults.
|
|
1476
|
-
// Default mode is 'light' when no mode and no include flags are specified.
|
|
1477
|
-
const isFullMode = msg.mode === "full";
|
|
1478
|
-
const includeAttachments = msg.includeAttachments ?? isFullMode;
|
|
1479
|
-
const includeToolImages = msg.includeToolImages ?? isFullMode;
|
|
1480
|
-
const includeSurfaceData = msg.includeSurfaceData ?? isFullMode;
|
|
1481
|
-
|
|
1482
|
-
const { messages: dbMessages, hasMore } =
|
|
1483
|
-
conversationStore.getMessagesPaginated(
|
|
1484
|
-
msg.sessionId,
|
|
1485
|
-
limit,
|
|
1486
|
-
msg.beforeTimestamp,
|
|
1487
|
-
msg.beforeMessageId,
|
|
1488
|
-
);
|
|
1489
|
-
|
|
1490
|
-
const parsed: ParsedHistoryMessage[] = dbMessages.map((m) => {
|
|
1491
|
-
let text = "";
|
|
1492
|
-
let toolCalls: HistoryToolCall[] = [];
|
|
1493
|
-
let toolCallsBeforeText = false;
|
|
1494
|
-
let textSegments: string[] = [];
|
|
1495
|
-
let contentOrder: string[] = [];
|
|
1496
|
-
let surfaces: HistorySurface[] = [];
|
|
1497
|
-
try {
|
|
1498
|
-
const content = JSON.parse(m.content);
|
|
1499
|
-
const rendered = renderHistoryContent(content);
|
|
1500
|
-
text = rendered.text;
|
|
1501
|
-
toolCalls = rendered.toolCalls;
|
|
1502
|
-
toolCallsBeforeText = rendered.toolCallsBeforeText;
|
|
1503
|
-
textSegments = rendered.textSegments;
|
|
1504
|
-
contentOrder = rendered.contentOrder;
|
|
1505
|
-
surfaces = rendered.surfaces;
|
|
1506
|
-
if (m.role === "assistant" && toolCalls.length > 0) {
|
|
1507
|
-
log.info(
|
|
1508
|
-
{
|
|
1509
|
-
messageId: m.id,
|
|
1510
|
-
toolCallCount: toolCalls.length,
|
|
1511
|
-
text: truncate(text, 100, ""),
|
|
1512
|
-
},
|
|
1513
|
-
"History message with tool calls",
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
} catch (err) {
|
|
1517
|
-
log.debug(
|
|
1518
|
-
{ err, messageId: m.id },
|
|
1519
|
-
"Failed to parse message content as JSON, using raw text",
|
|
1520
|
-
);
|
|
1521
|
-
text = m.content;
|
|
1522
|
-
textSegments = text ? [text] : [];
|
|
1523
|
-
contentOrder = text ? ["text:0"] : [];
|
|
1524
|
-
surfaces = [];
|
|
1525
|
-
}
|
|
1526
|
-
let subagentNotification: ParsedHistoryMessage["subagentNotification"];
|
|
1527
|
-
if (m.metadata) {
|
|
1528
|
-
try {
|
|
1529
|
-
subagentNotification = (
|
|
1530
|
-
JSON.parse(m.metadata) as {
|
|
1531
|
-
subagentNotification?: ParsedHistoryMessage["subagentNotification"];
|
|
1532
|
-
}
|
|
1533
|
-
).subagentNotification;
|
|
1534
|
-
} catch (err) {
|
|
1535
|
-
log.debug(
|
|
1536
|
-
{ err, messageId: m.id },
|
|
1537
|
-
"Failed to parse message metadata as JSON, ignoring",
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
return {
|
|
1542
|
-
id: m.id,
|
|
1543
|
-
role: m.role,
|
|
1544
|
-
text,
|
|
1545
|
-
timestamp: m.createdAt,
|
|
1546
|
-
toolCalls,
|
|
1547
|
-
toolCallsBeforeText,
|
|
1548
|
-
textSegments,
|
|
1549
|
-
contentOrder,
|
|
1550
|
-
surfaces,
|
|
1551
|
-
...(subagentNotification ? { subagentNotification } : {}),
|
|
1552
|
-
};
|
|
1553
|
-
});
|
|
1554
|
-
|
|
1555
|
-
// Merge tool_result data from user messages into the preceding assistant
|
|
1556
|
-
// message's toolCalls, and suppress user messages that only contain
|
|
1557
|
-
// tool_result blocks (internal agent-loop turns).
|
|
1558
|
-
const merged = mergeToolResults(parsed);
|
|
1559
|
-
|
|
1560
|
-
const historyMessages = merged.map((m) => {
|
|
1561
|
-
let attachments: UserMessageAttachment[] | undefined;
|
|
1562
|
-
if (m.role === "assistant" && m.id) {
|
|
1563
|
-
const linked = getAttachmentsForMessage(m.id);
|
|
1564
|
-
if (linked.length > 0) {
|
|
1565
|
-
if (includeAttachments) {
|
|
1566
|
-
// Full attachment data: same behavior as before
|
|
1567
|
-
const MAX_INLINE_B64_SIZE = 512 * 1024;
|
|
1568
|
-
attachments = linked.map((a) => {
|
|
1569
|
-
const isFileBacked = !a.dataBase64;
|
|
1570
|
-
const omit =
|
|
1571
|
-
isFileBacked ||
|
|
1572
|
-
(a.mimeType.startsWith("video/") &&
|
|
1573
|
-
a.dataBase64.length > MAX_INLINE_B64_SIZE);
|
|
1574
|
-
|
|
1575
|
-
if (
|
|
1576
|
-
a.mimeType.startsWith("video/") &&
|
|
1577
|
-
!a.thumbnailBase64 &&
|
|
1578
|
-
a.dataBase64
|
|
1579
|
-
) {
|
|
1580
|
-
const attachmentId = a.id;
|
|
1581
|
-
const base64 = a.dataBase64;
|
|
1582
|
-
silentlyWithLog(
|
|
1583
|
-
generateVideoThumbnail(base64).then((thumb) => {
|
|
1584
|
-
if (thumb) setAttachmentThumbnail(attachmentId, thumb);
|
|
1585
|
-
}),
|
|
1586
|
-
"video thumbnail generation",
|
|
1587
|
-
);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
const fp = getFilePathForAttachment(a.id);
|
|
1591
|
-
return {
|
|
1592
|
-
id: a.id,
|
|
1593
|
-
filename: a.originalFilename,
|
|
1594
|
-
mimeType: a.mimeType,
|
|
1595
|
-
data: omit ? "" : a.dataBase64,
|
|
1596
|
-
...(omit ? { sizeBytes: a.sizeBytes } : {}),
|
|
1597
|
-
...(a.thumbnailBase64
|
|
1598
|
-
? { thumbnailData: a.thumbnailBase64 }
|
|
1599
|
-
: {}),
|
|
1600
|
-
...(fp ? { filePath: fp } : {}),
|
|
1601
|
-
};
|
|
1602
|
-
});
|
|
1603
|
-
} else {
|
|
1604
|
-
// Light mode: metadata only, strip base64 data
|
|
1605
|
-
attachments = linked.map((a) => {
|
|
1606
|
-
const fp = getFilePathForAttachment(a.id);
|
|
1607
|
-
return {
|
|
1608
|
-
id: a.id,
|
|
1609
|
-
filename: a.originalFilename,
|
|
1610
|
-
mimeType: a.mimeType,
|
|
1611
|
-
data: "",
|
|
1612
|
-
sizeBytes: a.sizeBytes,
|
|
1613
|
-
...(a.thumbnailBase64
|
|
1614
|
-
? { thumbnailData: a.thumbnailBase64 }
|
|
1615
|
-
: {}),
|
|
1616
|
-
...(fp ? { filePath: fp } : {}),
|
|
1617
|
-
};
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// In light mode, strip imageData from tool calls
|
|
1624
|
-
const filteredToolCalls =
|
|
1625
|
-
m.toolCalls.length > 0
|
|
1626
|
-
? includeToolImages
|
|
1627
|
-
? m.toolCalls
|
|
1628
|
-
: m.toolCalls.map((tc) => {
|
|
1629
|
-
if (tc.imageData) {
|
|
1630
|
-
const { imageData: _, ...rest } = tc;
|
|
1631
|
-
return rest;
|
|
1632
|
-
}
|
|
1633
|
-
return tc;
|
|
1634
|
-
})
|
|
1635
|
-
: m.toolCalls;
|
|
1636
|
-
|
|
1637
|
-
// In light mode, strip full data from surfaces (keep metadata)
|
|
1638
|
-
const filteredSurfaces =
|
|
1639
|
-
m.surfaces.length > 0
|
|
1640
|
-
? includeSurfaceData
|
|
1641
|
-
? m.surfaces
|
|
1642
|
-
: m.surfaces.map((s) => ({
|
|
1643
|
-
surfaceId: s.surfaceId,
|
|
1644
|
-
surfaceType: s.surfaceType,
|
|
1645
|
-
title: s.title,
|
|
1646
|
-
data: {
|
|
1647
|
-
...(s.surfaceType === "dynamic_page"
|
|
1648
|
-
? {
|
|
1649
|
-
...(s.data.preview ? { preview: s.data.preview } : {}),
|
|
1650
|
-
...(s.data.appId ? { appId: s.data.appId } : {}),
|
|
1651
|
-
}
|
|
1652
|
-
: {}),
|
|
1653
|
-
} as Record<string, unknown>,
|
|
1654
|
-
...(s.actions ? { actions: s.actions } : {}),
|
|
1655
|
-
...(s.display ? { display: s.display } : {}),
|
|
1656
|
-
}))
|
|
1657
|
-
: m.surfaces;
|
|
1658
|
-
|
|
1659
|
-
// Apply text truncation when maxTextChars is set
|
|
1660
|
-
let wasTruncated = false;
|
|
1661
|
-
let textWasTruncated = false;
|
|
1662
|
-
let text = m.text;
|
|
1663
|
-
if (msg.maxTextChars !== undefined && text.length > msg.maxTextChars) {
|
|
1664
|
-
text = text.slice(0, msg.maxTextChars) + " \u2026 [truncated]";
|
|
1665
|
-
wasTruncated = true;
|
|
1666
|
-
textWasTruncated = true;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
// Apply tool result truncation when maxToolResultChars is set
|
|
1670
|
-
const truncatedToolCalls =
|
|
1671
|
-
msg.maxToolResultChars !== undefined && filteredToolCalls.length > 0
|
|
1672
|
-
? filteredToolCalls.map((tc) => {
|
|
1673
|
-
if (
|
|
1674
|
-
tc.result !== undefined &&
|
|
1675
|
-
tc.result.length > msg.maxToolResultChars!
|
|
1676
|
-
) {
|
|
1677
|
-
wasTruncated = true;
|
|
1678
|
-
return {
|
|
1679
|
-
...tc,
|
|
1680
|
-
result:
|
|
1681
|
-
tc.result.slice(0, msg.maxToolResultChars!) +
|
|
1682
|
-
" \u2026 [truncated]",
|
|
1683
|
-
};
|
|
1684
|
-
}
|
|
1685
|
-
return tc;
|
|
1686
|
-
})
|
|
1687
|
-
: filteredToolCalls;
|
|
1688
|
-
|
|
1689
|
-
return {
|
|
1690
|
-
...(m.id ? { id: m.id } : {}),
|
|
1691
|
-
role: m.role,
|
|
1692
|
-
text,
|
|
1693
|
-
timestamp: m.timestamp,
|
|
1694
|
-
...(truncatedToolCalls.length > 0
|
|
1695
|
-
? {
|
|
1696
|
-
toolCalls: truncatedToolCalls,
|
|
1697
|
-
toolCallsBeforeText: m.toolCallsBeforeText,
|
|
1698
|
-
}
|
|
1699
|
-
: {}),
|
|
1700
|
-
...(attachments ? { attachments } : {}),
|
|
1701
|
-
...(!textWasTruncated && m.textSegments.length > 0
|
|
1702
|
-
? { textSegments: m.textSegments }
|
|
1703
|
-
: {}),
|
|
1704
|
-
...(!textWasTruncated && m.contentOrder.length > 0
|
|
1705
|
-
? { contentOrder: m.contentOrder }
|
|
1706
|
-
: {}),
|
|
1707
|
-
...(filteredSurfaces.length > 0 ? { surfaces: filteredSurfaces } : {}),
|
|
1708
|
-
...(m.subagentNotification
|
|
1709
|
-
? { subagentNotification: m.subagentNotification }
|
|
1710
|
-
: {}),
|
|
1711
|
-
...(wasTruncated ? { wasTruncated: true } : {}),
|
|
1712
|
-
};
|
|
1713
|
-
});
|
|
1714
|
-
|
|
1715
|
-
const oldestTimestamp =
|
|
1716
|
-
historyMessages.length > 0 ? historyMessages[0].timestamp : undefined;
|
|
1717
|
-
// Provide the oldest message ID as a tie-breaker cursor so clients can
|
|
1718
|
-
// paginate without skipping same-millisecond messages at page boundaries.
|
|
1719
|
-
const oldestMessageId =
|
|
1720
|
-
historyMessages.length > 0 ? historyMessages[0].id : undefined;
|
|
1721
|
-
|
|
1722
|
-
ctx.send(socket, {
|
|
1723
|
-
type: "history_response",
|
|
1724
|
-
sessionId: msg.sessionId,
|
|
1725
|
-
messages: historyMessages,
|
|
1726
|
-
hasMore,
|
|
1727
|
-
...(oldestTimestamp !== undefined ? { oldestTimestamp } : {}),
|
|
1728
|
-
...(oldestMessageId ? { oldestMessageId } : {}),
|
|
1729
|
-
});
|
|
1730
|
-
|
|
1731
|
-
// Surfaces are now included directly in the history_response message (in the surfaces array),
|
|
1732
|
-
// so we no longer emit separate ui_surface_show messages during history loading.
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
522
|
export function handleUndo(
|
|
1736
523
|
msg: UndoRequest,
|
|
1737
524
|
socket: net.Socket,
|
|
@@ -1822,17 +609,6 @@ export function handleUsageRequest(
|
|
|
1822
609
|
});
|
|
1823
610
|
}
|
|
1824
611
|
|
|
1825
|
-
export function handleSandboxSet(
|
|
1826
|
-
msg: SandboxSetRequest,
|
|
1827
|
-
_socket: net.Socket,
|
|
1828
|
-
_ctx: HandlerContext,
|
|
1829
|
-
): void {
|
|
1830
|
-
log.warn(
|
|
1831
|
-
{ enabled: msg.enabled },
|
|
1832
|
-
"Received deprecated sandbox_set message. Runtime sandbox overrides are ignored.",
|
|
1833
|
-
);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
612
|
export function handleDeleteQueuedMessage(
|
|
1837
613
|
msg: DeleteQueuedMessage,
|
|
1838
614
|
socket: net.Socket,
|
|
@@ -1861,109 +637,6 @@ export function handleDeleteQueuedMessage(
|
|
|
1861
637
|
}
|
|
1862
638
|
}
|
|
1863
639
|
|
|
1864
|
-
export function handleConversationSearch(
|
|
1865
|
-
msg: ConversationSearchRequest,
|
|
1866
|
-
socket: net.Socket,
|
|
1867
|
-
ctx: HandlerContext,
|
|
1868
|
-
): void {
|
|
1869
|
-
const results = conversationStore.searchConversations(msg.query, {
|
|
1870
|
-
limit: msg.limit,
|
|
1871
|
-
maxMessagesPerConversation: msg.maxMessagesPerConversation,
|
|
1872
|
-
});
|
|
1873
|
-
ctx.send(socket, {
|
|
1874
|
-
type: "conversation_search_response",
|
|
1875
|
-
query: msg.query,
|
|
1876
|
-
results,
|
|
1877
|
-
});
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
export function handleMessageContentRequest(
|
|
1881
|
-
msg: MessageContentRequest,
|
|
1882
|
-
socket: net.Socket,
|
|
1883
|
-
ctx: HandlerContext,
|
|
1884
|
-
): void {
|
|
1885
|
-
const dbMessage = conversationStore.getMessageById(
|
|
1886
|
-
msg.messageId,
|
|
1887
|
-
msg.sessionId,
|
|
1888
|
-
);
|
|
1889
|
-
if (!dbMessage) {
|
|
1890
|
-
ctx.send(socket, {
|
|
1891
|
-
type: "error",
|
|
1892
|
-
message: `Message ${msg.messageId} not found in session ${msg.sessionId}`,
|
|
1893
|
-
});
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
let text: string | undefined;
|
|
1898
|
-
let toolCalls:
|
|
1899
|
-
| Array<{ name: string; result?: string; input?: Record<string, unknown> }>
|
|
1900
|
-
| undefined;
|
|
1901
|
-
|
|
1902
|
-
try {
|
|
1903
|
-
const content = JSON.parse(dbMessage.content);
|
|
1904
|
-
const rendered = renderHistoryContent(content);
|
|
1905
|
-
text = rendered.text || undefined;
|
|
1906
|
-
const mergedToolCalls = rendered.toolCalls;
|
|
1907
|
-
|
|
1908
|
-
// Handle legacy conversations where tool_result blocks are stored in the
|
|
1909
|
-
// following user message rather than inline with the assistant message.
|
|
1910
|
-
// This mirrors the mergeToolResults logic used by handleHistoryRequest.
|
|
1911
|
-
if (
|
|
1912
|
-
dbMessage.role === "assistant" &&
|
|
1913
|
-
mergedToolCalls.some((tc) => tc.result === undefined)
|
|
1914
|
-
) {
|
|
1915
|
-
const nextMsg = conversationStore.getNextMessage(
|
|
1916
|
-
msg.sessionId,
|
|
1917
|
-
dbMessage.createdAt,
|
|
1918
|
-
dbMessage.id,
|
|
1919
|
-
);
|
|
1920
|
-
if (nextMsg && nextMsg.role === "user") {
|
|
1921
|
-
try {
|
|
1922
|
-
const nextContent = JSON.parse(nextMsg.content);
|
|
1923
|
-
const nextRendered = renderHistoryContent(nextContent);
|
|
1924
|
-
if (
|
|
1925
|
-
nextRendered.text.trim() === "" &&
|
|
1926
|
-
nextRendered.toolCalls.length > 0
|
|
1927
|
-
) {
|
|
1928
|
-
for (const resultEntry of nextRendered.toolCalls) {
|
|
1929
|
-
const unresolved = mergedToolCalls.find(
|
|
1930
|
-
(tc) => tc.result === undefined,
|
|
1931
|
-
);
|
|
1932
|
-
if (unresolved) {
|
|
1933
|
-
unresolved.result = resultEntry.result;
|
|
1934
|
-
unresolved.isError = resultEntry.isError;
|
|
1935
|
-
if (resultEntry.imageData)
|
|
1936
|
-
unresolved.imageData = resultEntry.imageData;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
} catch {
|
|
1941
|
-
// Next message isn't valid JSON — skip merging
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
if (mergedToolCalls.length > 0) {
|
|
1947
|
-
toolCalls = mergedToolCalls.map((tc) => ({
|
|
1948
|
-
name: tc.name,
|
|
1949
|
-
input: tc.input,
|
|
1950
|
-
...(tc.result !== undefined ? { result: tc.result } : {}),
|
|
1951
|
-
}));
|
|
1952
|
-
}
|
|
1953
|
-
} catch {
|
|
1954
|
-
// Raw text content (not JSON)
|
|
1955
|
-
text = dbMessage.content || undefined;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
ctx.send(socket, {
|
|
1959
|
-
type: "message_content_response",
|
|
1960
|
-
sessionId: msg.sessionId,
|
|
1961
|
-
messageId: msg.messageId,
|
|
1962
|
-
...(text !== undefined ? { text } : {}),
|
|
1963
|
-
...(toolCalls ? { toolCalls } : {}),
|
|
1964
|
-
});
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
640
|
export function handleReorderThreads(
|
|
1968
641
|
msg: ReorderThreadsRequest,
|
|
1969
642
|
_socket: net.Socket,
|
|
@@ -1998,7 +671,6 @@ export const sessionHandlers = defineHandlers({
|
|
|
1998
671
|
undo: handleUndo,
|
|
1999
672
|
regenerate: handleRegenerate,
|
|
2000
673
|
usage_request: handleUsageRequest,
|
|
2001
|
-
sandbox_set: handleSandboxSet,
|
|
2002
674
|
conversation_search: handleConversationSearch,
|
|
2003
675
|
reorder_threads: handleReorderThreads,
|
|
2004
676
|
});
|