@vellumai/assistant 0.4.31 → 0.4.33
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/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/access-request-decision.test.ts +83 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +0 -1
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -23
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +6 -5
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- 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 +2 -2
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- 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__/relay-server.test.ts +145 -2
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/amazon/session.ts +30 -91
- 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 +271 -956
- 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/channels/config.ts +41 -2
- package/src/config/agent-schema.ts +1 -1
- 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-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -14
- package/src/config/feature-flag-registry.json +5 -5
- package/src/config/loader.ts +19 -0
- package/src/config/schema.ts +2 -2
- package/src/config/user-reference.ts +47 -9
- package/src/daemon/handlers/config-channels.ts +11 -10
- package/src/daemon/handlers/contacts.ts +5 -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 -1338
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +18 -55
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +9 -1
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/invite-store.ts +71 -1
- package/src/memory/job-handlers/conflict.ts +24 -0
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
- package/src/memory/migrations/index.ts +1 -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 +127 -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 -1385
- package/src/memory/slack-thread-store.ts +0 -69
- 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 +52 -26
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +1 -3
- package/src/runtime/channel-invite-transport.ts +121 -34
- package/src/runtime/channel-invite-transports/email.ts +50 -0
- package/src/runtime/channel-invite-transports/slack.ts +81 -0
- package/src/runtime/channel-invite-transports/sms.ts +70 -0
- package/src/runtime/channel-invite-transports/telegram.ts +29 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/http-server.ts +83 -722
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/invite-redemption-service.ts +193 -0
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/access-request-decision.ts +52 -6
- 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 -0
- 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 +96 -6
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +190 -193
- 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 +15 -0
- package/src/runtime/routes/guardian-action-routes.ts +22 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +21 -6
- 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 +9 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +295 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +32 -0
- package/src/runtime/routes/migration-routes.ts +30 -0
- 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/browser/browser-manager.ts +10 -1
- package/src/tools/browser/runtime-check.ts +3 -1
- 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/tools/shared/shell-output.ts +7 -2
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/contacts/startup-migration.ts +0 -21
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
import * as net from "node:net";
|
|
2
|
+
|
|
3
|
+
import { v4 as uuid } from "uuid";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createAssistantMessage,
|
|
7
|
+
createUserMessage,
|
|
8
|
+
} from "../../agent/message-types.js";
|
|
9
|
+
import {
|
|
10
|
+
type ChannelId,
|
|
11
|
+
type InterfaceId,
|
|
12
|
+
parseChannelId,
|
|
13
|
+
parseInterfaceId,
|
|
14
|
+
} from "../../channels/types.js";
|
|
15
|
+
import { getConfig } from "../../config/loader.js";
|
|
16
|
+
import {
|
|
17
|
+
listCanonicalGuardianRequests,
|
|
18
|
+
listPendingCanonicalGuardianRequestsByDestinationConversation,
|
|
19
|
+
} from "../../memory/canonical-guardian-store.js";
|
|
20
|
+
import * as conversationStore from "../../memory/conversation-store.js";
|
|
21
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
|
|
22
|
+
import { routeGuardianReply } from "../../runtime/guardian-reply-router.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveLocalIpcAuthContext,
|
|
25
|
+
resolveLocalIpcTrustContext,
|
|
26
|
+
} from "../../runtime/local-actor-identity.js";
|
|
27
|
+
import * as pendingInteractions from "../../runtime/pending-interactions.js";
|
|
28
|
+
import { checkIngressForSecrets } from "../../security/secret-ingress.js";
|
|
29
|
+
import {
|
|
30
|
+
compileCustomPatterns,
|
|
31
|
+
redactSecrets,
|
|
32
|
+
} from "../../security/secret-scanner.js";
|
|
33
|
+
import { createApprovalConversationGenerator } from "../approval-generators.js";
|
|
34
|
+
import { getAssistantName } from "../identity-helpers.js";
|
|
35
|
+
import type { UserMessageAttachment } from "../ipc-contract.js";
|
|
36
|
+
import type { ServerMessage, UserMessage } from "../ipc-protocol.js";
|
|
37
|
+
import { executeRecordingIntent } from "../recording-executor.js";
|
|
38
|
+
import { resolveRecordingIntent } from "../recording-intent.js";
|
|
39
|
+
import {
|
|
40
|
+
classifyRecordingIntentFallback,
|
|
41
|
+
containsRecordingKeywords,
|
|
42
|
+
} from "../recording-intent-fallback.js";
|
|
43
|
+
import type { Session } from "../session.js";
|
|
44
|
+
import {
|
|
45
|
+
buildSessionErrorMessage,
|
|
46
|
+
classifySessionError,
|
|
47
|
+
} from "../session-error.js";
|
|
48
|
+
import { resolveChannelCapabilities } from "../session-runtime-assembly.js";
|
|
49
|
+
import {
|
|
50
|
+
handleRecordingPause,
|
|
51
|
+
handleRecordingRestart,
|
|
52
|
+
handleRecordingResume,
|
|
53
|
+
handleRecordingStart,
|
|
54
|
+
handleRecordingStop,
|
|
55
|
+
} from "./recording.js";
|
|
56
|
+
import {
|
|
57
|
+
makeIpcEventSender,
|
|
58
|
+
syncCanonicalStatusFromIpcConfirmationDecision,
|
|
59
|
+
} from "./sessions.js";
|
|
60
|
+
import { type HandlerContext, log, wireEscalationHandler } from "./shared.js";
|
|
61
|
+
|
|
62
|
+
const desktopApprovalConversationGenerator =
|
|
63
|
+
createApprovalConversationGenerator();
|
|
64
|
+
|
|
65
|
+
// ── Recording command persistence helper ─────────────────────────────
|
|
66
|
+
// Several recording command actions share identical logic: send a response
|
|
67
|
+
// delta + complete, persist user/assistant messages, and sync in-memory
|
|
68
|
+
// session history. This helper consolidates that pattern.
|
|
69
|
+
async function persistRecordingExchange(
|
|
70
|
+
sessionId: string,
|
|
71
|
+
messageText: string,
|
|
72
|
+
responseText: string,
|
|
73
|
+
session: Session,
|
|
74
|
+
socket: net.Socket,
|
|
75
|
+
ctx: HandlerContext,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
ctx.send(socket, {
|
|
78
|
+
type: "assistant_text_delta",
|
|
79
|
+
text: responseText,
|
|
80
|
+
sessionId,
|
|
81
|
+
});
|
|
82
|
+
ctx.send(socket, {
|
|
83
|
+
type: "message_complete",
|
|
84
|
+
sessionId,
|
|
85
|
+
});
|
|
86
|
+
await conversationStore.addMessage(
|
|
87
|
+
sessionId,
|
|
88
|
+
"user",
|
|
89
|
+
JSON.stringify([{ type: "text", text: messageText }]),
|
|
90
|
+
);
|
|
91
|
+
await conversationStore.addMessage(
|
|
92
|
+
sessionId,
|
|
93
|
+
"assistant",
|
|
94
|
+
JSON.stringify([{ type: "text", text: responseText }]),
|
|
95
|
+
);
|
|
96
|
+
if (!session.isProcessing()) {
|
|
97
|
+
session.messages.push({
|
|
98
|
+
role: "user",
|
|
99
|
+
content: [{ type: "text", text: messageText }],
|
|
100
|
+
});
|
|
101
|
+
session.messages.push({
|
|
102
|
+
role: "assistant",
|
|
103
|
+
content: [{ type: "text", text: responseText }],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Secret ingress check and redirect ────────────────────────────────
|
|
109
|
+
// Returns true if the message was blocked (caller should return early).
|
|
110
|
+
function handleSecretIngress(
|
|
111
|
+
msg: UserMessage,
|
|
112
|
+
messageText: string,
|
|
113
|
+
socket: net.Socket,
|
|
114
|
+
ctx: HandlerContext,
|
|
115
|
+
session: Session,
|
|
116
|
+
rlog: typeof log,
|
|
117
|
+
dispatchUserMessage: DispatchUserMessageFn,
|
|
118
|
+
): boolean {
|
|
119
|
+
if (msg.bypassSecretCheck) return false;
|
|
120
|
+
|
|
121
|
+
const ingressCheck = checkIngressForSecrets(messageText);
|
|
122
|
+
if (!ingressCheck.blocked) return false;
|
|
123
|
+
|
|
124
|
+
rlog.warn(
|
|
125
|
+
{ detectedTypes: ingressCheck.detectedTypes },
|
|
126
|
+
"Blocked user message containing secrets",
|
|
127
|
+
);
|
|
128
|
+
ctx.send(socket, {
|
|
129
|
+
type: "error",
|
|
130
|
+
message: ingressCheck.userNotice!,
|
|
131
|
+
category: "secret_blocked",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const config = getConfig();
|
|
135
|
+
const compiledCustom = config.secretDetection.customPatterns?.length
|
|
136
|
+
? compileCustomPatterns(config.secretDetection.customPatterns)
|
|
137
|
+
: undefined;
|
|
138
|
+
const redactedMessageText = redactSecrets(
|
|
139
|
+
messageText,
|
|
140
|
+
{
|
|
141
|
+
enabled: true,
|
|
142
|
+
base64Threshold: config.secretDetection.entropyThreshold,
|
|
143
|
+
},
|
|
144
|
+
compiledCustom,
|
|
145
|
+
).trim();
|
|
146
|
+
|
|
147
|
+
session.redirectToSecurePrompt(ingressCheck.detectedTypes, {
|
|
148
|
+
onStored: (record) => {
|
|
149
|
+
ctx.send(socket, {
|
|
150
|
+
type: "assistant_text_delta",
|
|
151
|
+
sessionId: msg.sessionId,
|
|
152
|
+
text: "Saved your secret securely. Continuing with your request.",
|
|
153
|
+
});
|
|
154
|
+
ctx.send(socket, {
|
|
155
|
+
type: "message_complete",
|
|
156
|
+
sessionId: msg.sessionId,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const continuationParts: string[] = [];
|
|
160
|
+
if (redactedMessageText.length > 0)
|
|
161
|
+
continuationParts.push(redactedMessageText);
|
|
162
|
+
continuationParts.push(
|
|
163
|
+
`I entered the redacted secret via the Secure Credential UI and saved it as credential ${record.service}/${record.field}. ` +
|
|
164
|
+
"Continue with my request using that stored credential and do not ask me to paste the secret again.",
|
|
165
|
+
);
|
|
166
|
+
const continuationMessage = continuationParts.join("\n\n");
|
|
167
|
+
const continuationRequestId = uuid();
|
|
168
|
+
dispatchUserMessage(
|
|
169
|
+
continuationMessage,
|
|
170
|
+
msg.attachments ?? [],
|
|
171
|
+
continuationRequestId,
|
|
172
|
+
"secure_redirect_resume",
|
|
173
|
+
msg.activeSurfaceId,
|
|
174
|
+
msg.currentPage,
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Structured recording command intent ──────────────────────────────
|
|
183
|
+
// Returns true if the command was fully handled (caller should return early).
|
|
184
|
+
async function handleStructuredRecordingIntent(
|
|
185
|
+
msg: UserMessage,
|
|
186
|
+
messageText: string,
|
|
187
|
+
session: Session,
|
|
188
|
+
socket: net.Socket,
|
|
189
|
+
ctx: HandlerContext,
|
|
190
|
+
rlog: typeof log,
|
|
191
|
+
): Promise<boolean> {
|
|
192
|
+
const config = getConfig();
|
|
193
|
+
if (
|
|
194
|
+
!config.daemon.standaloneRecording ||
|
|
195
|
+
msg.commandIntent?.domain !== "screen_recording"
|
|
196
|
+
) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const action = msg.commandIntent.action;
|
|
201
|
+
rlog.info(
|
|
202
|
+
{ action, source: "commandIntent" },
|
|
203
|
+
"Recording command intent received in user_message",
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (action === "start") {
|
|
207
|
+
const recordingId = handleRecordingStart(
|
|
208
|
+
msg.sessionId,
|
|
209
|
+
{ promptForSource: true },
|
|
210
|
+
socket,
|
|
211
|
+
ctx,
|
|
212
|
+
);
|
|
213
|
+
const responseText = recordingId
|
|
214
|
+
? "Starting screen recording."
|
|
215
|
+
: "A recording is already active.";
|
|
216
|
+
await persistRecordingExchange(
|
|
217
|
+
msg.sessionId,
|
|
218
|
+
messageText,
|
|
219
|
+
responseText,
|
|
220
|
+
session,
|
|
221
|
+
socket,
|
|
222
|
+
ctx,
|
|
223
|
+
);
|
|
224
|
+
return true;
|
|
225
|
+
} else if (action === "stop") {
|
|
226
|
+
const stopped = handleRecordingStop(msg.sessionId, ctx) !== undefined;
|
|
227
|
+
const responseText = stopped
|
|
228
|
+
? "Stopping the recording."
|
|
229
|
+
: "No active recording to stop.";
|
|
230
|
+
await persistRecordingExchange(
|
|
231
|
+
msg.sessionId,
|
|
232
|
+
messageText,
|
|
233
|
+
responseText,
|
|
234
|
+
session,
|
|
235
|
+
socket,
|
|
236
|
+
ctx,
|
|
237
|
+
);
|
|
238
|
+
return true;
|
|
239
|
+
} else if (action === "restart") {
|
|
240
|
+
const restartResult = handleRecordingRestart(msg.sessionId, socket, ctx);
|
|
241
|
+
await persistRecordingExchange(
|
|
242
|
+
msg.sessionId,
|
|
243
|
+
messageText,
|
|
244
|
+
restartResult.responseText,
|
|
245
|
+
session,
|
|
246
|
+
socket,
|
|
247
|
+
ctx,
|
|
248
|
+
);
|
|
249
|
+
return true;
|
|
250
|
+
} else if (action === "pause") {
|
|
251
|
+
const paused = handleRecordingPause(msg.sessionId, ctx) !== undefined;
|
|
252
|
+
const responseText = paused
|
|
253
|
+
? "Pausing the recording."
|
|
254
|
+
: "No active recording to pause.";
|
|
255
|
+
await persistRecordingExchange(
|
|
256
|
+
msg.sessionId,
|
|
257
|
+
messageText,
|
|
258
|
+
responseText,
|
|
259
|
+
session,
|
|
260
|
+
socket,
|
|
261
|
+
ctx,
|
|
262
|
+
);
|
|
263
|
+
return true;
|
|
264
|
+
} else if (action === "resume") {
|
|
265
|
+
const resumed = handleRecordingResume(msg.sessionId, ctx) !== undefined;
|
|
266
|
+
const responseText = resumed
|
|
267
|
+
? "Resuming the recording."
|
|
268
|
+
: "No active recording to resume.";
|
|
269
|
+
await persistRecordingExchange(
|
|
270
|
+
msg.sessionId,
|
|
271
|
+
messageText,
|
|
272
|
+
responseText,
|
|
273
|
+
session,
|
|
274
|
+
socket,
|
|
275
|
+
ctx,
|
|
276
|
+
);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Unrecognized action — fall through to normal text handling
|
|
281
|
+
rlog.warn(
|
|
282
|
+
{ action, source: "commandIntent" },
|
|
283
|
+
"Unrecognized screen_recording action, falling through to text handling",
|
|
284
|
+
);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Standalone recording intent interception ─────────────────────────
|
|
289
|
+
// Returns the original content before strip (if recording keywords were
|
|
290
|
+
// stripped from the message), or undefined if the message was fully handled
|
|
291
|
+
// or no recording intent was detected.
|
|
292
|
+
async function handleStandaloneRecordingIntent(
|
|
293
|
+
msg: UserMessage,
|
|
294
|
+
messageText: string,
|
|
295
|
+
session: Session,
|
|
296
|
+
socket: net.Socket,
|
|
297
|
+
ctx: HandlerContext,
|
|
298
|
+
rlog: typeof log,
|
|
299
|
+
): Promise<{
|
|
300
|
+
handled: boolean;
|
|
301
|
+
originalContentBeforeStrip?: string;
|
|
302
|
+
updatedMessageText: string;
|
|
303
|
+
}> {
|
|
304
|
+
const config = getConfig();
|
|
305
|
+
if (!config.daemon.standaloneRecording || !messageText) {
|
|
306
|
+
return { handled: false, updatedMessageText: messageText };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const name = getAssistantName();
|
|
310
|
+
const dynamicNames = [name].filter(Boolean) as string[];
|
|
311
|
+
const intentResult = resolveRecordingIntent(messageText, dynamicNames);
|
|
312
|
+
|
|
313
|
+
// Pure recording-only intents
|
|
314
|
+
if (
|
|
315
|
+
intentResult.kind === "start_only" ||
|
|
316
|
+
intentResult.kind === "stop_only" ||
|
|
317
|
+
intentResult.kind === "start_and_stop_only" ||
|
|
318
|
+
intentResult.kind === "restart_only" ||
|
|
319
|
+
intentResult.kind === "pause_only" ||
|
|
320
|
+
intentResult.kind === "resume_only"
|
|
321
|
+
) {
|
|
322
|
+
const execResult = executeRecordingIntent(intentResult, {
|
|
323
|
+
conversationId: msg.sessionId,
|
|
324
|
+
socket,
|
|
325
|
+
ctx,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (execResult.handled) {
|
|
329
|
+
rlog.info(
|
|
330
|
+
{ kind: intentResult.kind },
|
|
331
|
+
"Recording intent intercepted in user_message",
|
|
332
|
+
);
|
|
333
|
+
await persistRecordingExchange(
|
|
334
|
+
msg.sessionId,
|
|
335
|
+
messageText,
|
|
336
|
+
execResult.responseText!,
|
|
337
|
+
session,
|
|
338
|
+
socket,
|
|
339
|
+
ctx,
|
|
340
|
+
);
|
|
341
|
+
return { handled: true, updatedMessageText: messageText };
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Recording intent with remainder text
|
|
346
|
+
if (
|
|
347
|
+
intentResult.kind === "start_with_remainder" ||
|
|
348
|
+
intentResult.kind === "stop_with_remainder" ||
|
|
349
|
+
intentResult.kind === "start_and_stop_with_remainder" ||
|
|
350
|
+
intentResult.kind === "restart_with_remainder"
|
|
351
|
+
) {
|
|
352
|
+
const execResult = executeRecordingIntent(intentResult, {
|
|
353
|
+
conversationId: msg.sessionId,
|
|
354
|
+
socket,
|
|
355
|
+
ctx,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const originalContentBeforeStrip = messageText;
|
|
359
|
+
const updatedText = execResult.remainderText ?? messageText;
|
|
360
|
+
msg.content = updatedText;
|
|
361
|
+
|
|
362
|
+
if (intentResult.kind === "stop_with_remainder") {
|
|
363
|
+
handleRecordingStop(msg.sessionId, ctx);
|
|
364
|
+
}
|
|
365
|
+
if (intentResult.kind === "start_with_remainder") {
|
|
366
|
+
handleRecordingStart(
|
|
367
|
+
msg.sessionId,
|
|
368
|
+
{ promptForSource: true },
|
|
369
|
+
socket,
|
|
370
|
+
ctx,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
intentResult.kind === "restart_with_remainder" ||
|
|
375
|
+
intentResult.kind === "start_and_stop_with_remainder"
|
|
376
|
+
) {
|
|
377
|
+
const restartResult = handleRecordingRestart(msg.sessionId, socket, ctx);
|
|
378
|
+
if (
|
|
379
|
+
!restartResult.initiated &&
|
|
380
|
+
restartResult.reason === "no_active_recording" &&
|
|
381
|
+
intentResult.kind === "start_and_stop_with_remainder"
|
|
382
|
+
) {
|
|
383
|
+
handleRecordingStart(
|
|
384
|
+
msg.sessionId,
|
|
385
|
+
{ promptForSource: true },
|
|
386
|
+
socket,
|
|
387
|
+
ctx,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
rlog.info(
|
|
393
|
+
{ remaining: updatedText, kind: intentResult.kind },
|
|
394
|
+
"Recording intent with remainder — continuing with remaining text",
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
handled: false,
|
|
399
|
+
originalContentBeforeStrip,
|
|
400
|
+
updatedMessageText: updatedText,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 'none' — deterministic resolver found nothing; try LLM fallback
|
|
405
|
+
// if the text contains recording-related keywords.
|
|
406
|
+
if (intentResult.kind === "none" && containsRecordingKeywords(messageText)) {
|
|
407
|
+
const fallback = await classifyRecordingIntentFallback(messageText);
|
|
408
|
+
rlog.info(
|
|
409
|
+
{
|
|
410
|
+
fallbackAction: fallback.action,
|
|
411
|
+
fallbackConfidence: fallback.confidence,
|
|
412
|
+
},
|
|
413
|
+
"Recording intent LLM fallback result",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
if (fallback.action !== "none" && fallback.confidence === "high") {
|
|
417
|
+
const kindMap: Record<
|
|
418
|
+
string,
|
|
419
|
+
import("../recording-intent.js").RecordingIntentResult
|
|
420
|
+
> = {
|
|
421
|
+
start: { kind: "start_only" },
|
|
422
|
+
stop: { kind: "stop_only" },
|
|
423
|
+
restart: { kind: "restart_only" },
|
|
424
|
+
pause: { kind: "pause_only" },
|
|
425
|
+
resume: { kind: "resume_only" },
|
|
426
|
+
};
|
|
427
|
+
const mapped = kindMap[fallback.action];
|
|
428
|
+
if (mapped) {
|
|
429
|
+
const execResult = executeRecordingIntent(mapped, {
|
|
430
|
+
conversationId: msg.sessionId,
|
|
431
|
+
socket,
|
|
432
|
+
ctx,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
if (execResult.handled) {
|
|
436
|
+
rlog.info(
|
|
437
|
+
{ kind: mapped.kind, source: "llm_fallback" },
|
|
438
|
+
"Recording intent intercepted via LLM fallback",
|
|
439
|
+
);
|
|
440
|
+
await persistRecordingExchange(
|
|
441
|
+
msg.sessionId,
|
|
442
|
+
messageText,
|
|
443
|
+
execResult.responseText!,
|
|
444
|
+
session,
|
|
445
|
+
socket,
|
|
446
|
+
ctx,
|
|
447
|
+
);
|
|
448
|
+
return { handled: true, updatedMessageText: messageText };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { handled: false, updatedMessageText: messageText };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Pending confirmation reply interception ──────────────────────────
|
|
458
|
+
// Returns true if the message was consumed as an inline approval reply.
|
|
459
|
+
async function handlePendingConfirmationReply(
|
|
460
|
+
msg: UserMessage,
|
|
461
|
+
messageText: string,
|
|
462
|
+
requestId: string,
|
|
463
|
+
session: Session,
|
|
464
|
+
ipcChannel: ChannelId,
|
|
465
|
+
ipcInterface: InterfaceId,
|
|
466
|
+
socket: net.Socket,
|
|
467
|
+
ctx: HandlerContext,
|
|
468
|
+
rlog: typeof log,
|
|
469
|
+
): Promise<boolean> {
|
|
470
|
+
if (!session.hasAnyPendingConfirmation() || messageText.trim().length === 0) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const pendingInteractionRequestIdsForConversation = pendingInteractions
|
|
476
|
+
.getByConversation(msg.sessionId)
|
|
477
|
+
.filter(
|
|
478
|
+
(interaction) =>
|
|
479
|
+
interaction.kind === "confirmation" &&
|
|
480
|
+
interaction.session === session &&
|
|
481
|
+
session.hasPendingConfirmation(interaction.requestId),
|
|
482
|
+
)
|
|
483
|
+
.map((interaction) => interaction.requestId);
|
|
484
|
+
|
|
485
|
+
const pendingCanonicalRequestIdsForConversation = [
|
|
486
|
+
...listPendingCanonicalGuardianRequestsByDestinationConversation(
|
|
487
|
+
msg.sessionId,
|
|
488
|
+
ipcChannel,
|
|
489
|
+
)
|
|
490
|
+
.filter((request) => request.kind === "tool_approval")
|
|
491
|
+
.map((request) => request.id),
|
|
492
|
+
...listCanonicalGuardianRequests({
|
|
493
|
+
status: "pending",
|
|
494
|
+
conversationId: msg.sessionId,
|
|
495
|
+
kind: "tool_approval",
|
|
496
|
+
}).map((request) => request.id),
|
|
497
|
+
].filter((pendingRequestId) =>
|
|
498
|
+
session.hasPendingConfirmation(pendingRequestId),
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
const pendingRequestIdsForConversation = Array.from(
|
|
502
|
+
new Set([
|
|
503
|
+
...pendingInteractionRequestIdsForConversation,
|
|
504
|
+
...pendingCanonicalRequestIdsForConversation,
|
|
505
|
+
]),
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (pendingRequestIdsForConversation.length === 0) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const localCtx = resolveLocalIpcTrustContext(ipcChannel);
|
|
513
|
+
const routerResult = await routeGuardianReply({
|
|
514
|
+
messageText: messageText.trim(),
|
|
515
|
+
channel: ipcChannel,
|
|
516
|
+
actor: {
|
|
517
|
+
actorPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
518
|
+
actorExternalUserId: localCtx.guardianExternalUserId,
|
|
519
|
+
channel: ipcChannel,
|
|
520
|
+
guardianPrincipalId: localCtx.guardianPrincipalId ?? undefined,
|
|
521
|
+
},
|
|
522
|
+
conversationId: msg.sessionId,
|
|
523
|
+
pendingRequestIds: pendingRequestIdsForConversation,
|
|
524
|
+
approvalConversationGenerator: desktopApprovalConversationGenerator,
|
|
525
|
+
emissionContext: {
|
|
526
|
+
source: "inline_nl",
|
|
527
|
+
causedByRequestId: requestId,
|
|
528
|
+
decisionText: messageText.trim(),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (routerResult.consumed && routerResult.type !== "nl_keep_pending") {
|
|
533
|
+
if (routerResult.requestId && !routerResult.decisionApplied) {
|
|
534
|
+
session.emitConfirmationStateChanged({
|
|
535
|
+
sessionId: msg.sessionId,
|
|
536
|
+
requestId: routerResult.requestId,
|
|
537
|
+
state: "resolved_stale",
|
|
538
|
+
source: "inline_nl",
|
|
539
|
+
causedByRequestId: requestId,
|
|
540
|
+
decisionText: messageText.trim(),
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const consumedChannelMeta = {
|
|
545
|
+
userMessageChannel: ipcChannel,
|
|
546
|
+
assistantMessageChannel: ipcChannel,
|
|
547
|
+
userMessageInterface: ipcInterface,
|
|
548
|
+
assistantMessageInterface: ipcInterface,
|
|
549
|
+
provenanceTrustClass: "guardian" as const,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const consumedUserMessage = createUserMessage(
|
|
553
|
+
messageText,
|
|
554
|
+
msg.attachments ?? [],
|
|
555
|
+
);
|
|
556
|
+
await conversationStore.addMessage(
|
|
557
|
+
msg.sessionId,
|
|
558
|
+
"user",
|
|
559
|
+
JSON.stringify(consumedUserMessage.content),
|
|
560
|
+
consumedChannelMeta,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const replyText =
|
|
564
|
+
routerResult.replyText?.trim() ||
|
|
565
|
+
(routerResult.decisionApplied
|
|
566
|
+
? "Decision applied."
|
|
567
|
+
: "Request already resolved.");
|
|
568
|
+
const consumedAssistantMessage = createAssistantMessage(replyText);
|
|
569
|
+
await conversationStore.addMessage(
|
|
570
|
+
msg.sessionId,
|
|
571
|
+
"assistant",
|
|
572
|
+
JSON.stringify(consumedAssistantMessage.content),
|
|
573
|
+
consumedChannelMeta,
|
|
574
|
+
);
|
|
575
|
+
if (!session.isProcessing()) {
|
|
576
|
+
session.messages.push(consumedUserMessage, consumedAssistantMessage);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
ctx.send(socket, {
|
|
580
|
+
type: "message_queued",
|
|
581
|
+
sessionId: msg.sessionId,
|
|
582
|
+
requestId,
|
|
583
|
+
position: 0,
|
|
584
|
+
});
|
|
585
|
+
ctx.send(socket, {
|
|
586
|
+
type: "message_dequeued",
|
|
587
|
+
sessionId: msg.sessionId,
|
|
588
|
+
requestId,
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (!session.isProcessing()) {
|
|
592
|
+
ctx.send(socket, {
|
|
593
|
+
type: "assistant_text_delta",
|
|
594
|
+
text: replyText,
|
|
595
|
+
sessionId: msg.sessionId,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
ctx.send(socket, {
|
|
599
|
+
type: "message_request_complete",
|
|
600
|
+
sessionId: msg.sessionId,
|
|
601
|
+
requestId,
|
|
602
|
+
runStillActive: session.isProcessing(),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
rlog.info(
|
|
606
|
+
{
|
|
607
|
+
routerType: routerResult.type,
|
|
608
|
+
decisionApplied: routerResult.decisionApplied,
|
|
609
|
+
routerRequestId: routerResult.requestId,
|
|
610
|
+
},
|
|
611
|
+
"Consumed pending-confirmation reply before auto-deny",
|
|
612
|
+
);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
} catch (err) {
|
|
616
|
+
rlog.warn(
|
|
617
|
+
{ err },
|
|
618
|
+
"Failed to process pending-confirmation reply; falling back to auto-deny behavior",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ── Auto-deny pending confirmations ──────────────────────────────────
|
|
626
|
+
function autoDenyPendingConfirmations(
|
|
627
|
+
msg: UserMessage,
|
|
628
|
+
requestId: string,
|
|
629
|
+
session: Session,
|
|
630
|
+
rlog: typeof log,
|
|
631
|
+
): void {
|
|
632
|
+
if (!session.hasAnyPendingConfirmation()) return;
|
|
633
|
+
|
|
634
|
+
rlog.info("Auto-denying pending confirmation(s) due to new user message");
|
|
635
|
+
for (const interaction of pendingInteractions.getByConversation(
|
|
636
|
+
msg.sessionId,
|
|
637
|
+
)) {
|
|
638
|
+
if (
|
|
639
|
+
interaction.session === session &&
|
|
640
|
+
interaction.kind === "confirmation"
|
|
641
|
+
) {
|
|
642
|
+
session.emitConfirmationStateChanged({
|
|
643
|
+
sessionId: msg.sessionId,
|
|
644
|
+
requestId: interaction.requestId,
|
|
645
|
+
state: "denied",
|
|
646
|
+
source: "auto_deny",
|
|
647
|
+
causedByRequestId: requestId,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
session.denyAllPendingConfirmations();
|
|
652
|
+
for (const interaction of pendingInteractions.getByConversation(
|
|
653
|
+
msg.sessionId,
|
|
654
|
+
)) {
|
|
655
|
+
if (
|
|
656
|
+
interaction.session === session &&
|
|
657
|
+
interaction.kind === "confirmation"
|
|
658
|
+
) {
|
|
659
|
+
syncCanonicalStatusFromIpcConfirmationDecision(
|
|
660
|
+
interaction.requestId,
|
|
661
|
+
"deny",
|
|
662
|
+
);
|
|
663
|
+
pendingInteractions.resolve(interaction.requestId);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ── Dispatch user message function type ──────────────────────────────
|
|
669
|
+
type DispatchUserMessageFn = (
|
|
670
|
+
content: string,
|
|
671
|
+
attachments: UserMessageAttachment[],
|
|
672
|
+
dispatchRequestId: string,
|
|
673
|
+
source: "user_message" | "secure_redirect_resume",
|
|
674
|
+
activeSurfaceId?: string,
|
|
675
|
+
currentPage?: string,
|
|
676
|
+
displayContent?: string,
|
|
677
|
+
) => void;
|
|
678
|
+
|
|
679
|
+
// ── Build dispatch function ──────────────────────────────────────────
|
|
680
|
+
// Creates the dispatchUserMessage closure used to enqueue or immediately
|
|
681
|
+
// process a user message through the session.
|
|
682
|
+
function buildDispatchUserMessage(params: {
|
|
683
|
+
msg: UserMessage;
|
|
684
|
+
session: Session;
|
|
685
|
+
sendEvent: (event: ServerMessage) => void;
|
|
686
|
+
ipcChannel: ChannelId;
|
|
687
|
+
ipcInterface: InterfaceId;
|
|
688
|
+
socket: net.Socket;
|
|
689
|
+
ctx: HandlerContext;
|
|
690
|
+
rlog: typeof log;
|
|
691
|
+
}): DispatchUserMessageFn {
|
|
692
|
+
const {
|
|
693
|
+
msg,
|
|
694
|
+
session,
|
|
695
|
+
sendEvent,
|
|
696
|
+
ipcChannel,
|
|
697
|
+
ipcInterface,
|
|
698
|
+
socket,
|
|
699
|
+
ctx,
|
|
700
|
+
rlog,
|
|
701
|
+
} = params;
|
|
702
|
+
|
|
703
|
+
const queuedChannelMetadata = {
|
|
704
|
+
userMessageChannel: ipcChannel,
|
|
705
|
+
assistantMessageChannel: ipcChannel,
|
|
706
|
+
userMessageInterface: ipcInterface,
|
|
707
|
+
assistantMessageInterface: ipcInterface,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
return (
|
|
711
|
+
content: string,
|
|
712
|
+
attachments: UserMessageAttachment[],
|
|
713
|
+
dispatchRequestId: string,
|
|
714
|
+
source: "user_message" | "secure_redirect_resume",
|
|
715
|
+
activeSurfaceId?: string,
|
|
716
|
+
currentPage?: string,
|
|
717
|
+
displayContent?: string,
|
|
718
|
+
): void => {
|
|
719
|
+
const receivedDescription =
|
|
720
|
+
source === "user_message"
|
|
721
|
+
? "User message received"
|
|
722
|
+
: "Resuming message after secure credential save";
|
|
723
|
+
const queuedDescription =
|
|
724
|
+
source === "user_message"
|
|
725
|
+
? "Message queued (session busy)"
|
|
726
|
+
: "Resumed message queued (session busy)";
|
|
727
|
+
|
|
728
|
+
session.traceEmitter.emit("request_received", receivedDescription, {
|
|
729
|
+
requestId: dispatchRequestId,
|
|
730
|
+
status: "info",
|
|
731
|
+
attributes: { source },
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
const result = session.enqueueMessage(
|
|
735
|
+
content,
|
|
736
|
+
attachments,
|
|
737
|
+
sendEvent,
|
|
738
|
+
dispatchRequestId,
|
|
739
|
+
activeSurfaceId,
|
|
740
|
+
currentPage,
|
|
741
|
+
queuedChannelMetadata,
|
|
742
|
+
undefined,
|
|
743
|
+
displayContent,
|
|
744
|
+
);
|
|
745
|
+
if (result.rejected) {
|
|
746
|
+
rlog.warn({ source }, "Message rejected — queue is full");
|
|
747
|
+
session.traceEmitter.emit(
|
|
748
|
+
"request_error",
|
|
749
|
+
"Message rejected — queue is full",
|
|
750
|
+
{
|
|
751
|
+
requestId: dispatchRequestId,
|
|
752
|
+
status: "error",
|
|
753
|
+
attributes: {
|
|
754
|
+
reason: "queue_full",
|
|
755
|
+
queueDepth: session.getQueueDepth(),
|
|
756
|
+
source,
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
ctx.send(
|
|
761
|
+
socket,
|
|
762
|
+
buildSessionErrorMessage(msg.sessionId, {
|
|
763
|
+
code: "QUEUE_FULL",
|
|
764
|
+
userMessage:
|
|
765
|
+
"Message queue is full (max depth: 10). Please wait for current messages to be processed.",
|
|
766
|
+
retryable: true,
|
|
767
|
+
debugDetails: "Message rejected — session queue is full",
|
|
768
|
+
}),
|
|
769
|
+
);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (result.queued) {
|
|
773
|
+
const position = session.getQueueDepth();
|
|
774
|
+
rlog.info({ source, position }, queuedDescription);
|
|
775
|
+
session.traceEmitter.emit(
|
|
776
|
+
"request_queued",
|
|
777
|
+
`Message queued at position ${position}`,
|
|
778
|
+
{
|
|
779
|
+
requestId: dispatchRequestId,
|
|
780
|
+
status: "info",
|
|
781
|
+
attributes: { position, source },
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
ctx.send(socket, {
|
|
785
|
+
type: "message_queued",
|
|
786
|
+
sessionId: msg.sessionId,
|
|
787
|
+
requestId: dispatchRequestId,
|
|
788
|
+
position,
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
rlog.info({ source }, "Processing user message");
|
|
794
|
+
session.emitActivityState(
|
|
795
|
+
"thinking",
|
|
796
|
+
"message_dequeued",
|
|
797
|
+
"assistant_turn",
|
|
798
|
+
dispatchRequestId,
|
|
799
|
+
);
|
|
800
|
+
session.setTurnChannelContext({
|
|
801
|
+
userMessageChannel: ipcChannel,
|
|
802
|
+
assistantMessageChannel: ipcChannel,
|
|
803
|
+
});
|
|
804
|
+
session.setTurnInterfaceContext({
|
|
805
|
+
userMessageInterface: ipcInterface,
|
|
806
|
+
assistantMessageInterface: ipcInterface,
|
|
807
|
+
});
|
|
808
|
+
session.setAssistantId(DAEMON_INTERNAL_ASSISTANT_ID);
|
|
809
|
+
session.setTrustContext(resolveLocalIpcTrustContext(ipcChannel));
|
|
810
|
+
session.setAuthContext(resolveLocalIpcAuthContext(msg.sessionId));
|
|
811
|
+
session.setCommandIntent(null);
|
|
812
|
+
session
|
|
813
|
+
.processMessage(
|
|
814
|
+
content,
|
|
815
|
+
attachments,
|
|
816
|
+
sendEvent,
|
|
817
|
+
dispatchRequestId,
|
|
818
|
+
activeSurfaceId,
|
|
819
|
+
currentPage,
|
|
820
|
+
undefined,
|
|
821
|
+
displayContent,
|
|
822
|
+
)
|
|
823
|
+
.catch((err) => {
|
|
824
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
825
|
+
rlog.error(
|
|
826
|
+
{ err, source },
|
|
827
|
+
"Error processing user message (session or provider failure)",
|
|
828
|
+
);
|
|
829
|
+
ctx.send(socket, {
|
|
830
|
+
type: "error",
|
|
831
|
+
message: `Failed to process message: ${message}`,
|
|
832
|
+
});
|
|
833
|
+
const classified = classifySessionError(err, { phase: "agent_loop" });
|
|
834
|
+
ctx.send(socket, buildSessionErrorMessage(msg.sessionId, classified));
|
|
835
|
+
});
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export async function handleUserMessage(
|
|
840
|
+
msg: UserMessage,
|
|
841
|
+
socket: net.Socket,
|
|
842
|
+
ctx: HandlerContext,
|
|
843
|
+
): Promise<void> {
|
|
844
|
+
const requestId = uuid();
|
|
845
|
+
const rlog = log.child({ sessionId: msg.sessionId, requestId });
|
|
846
|
+
try {
|
|
847
|
+
ctx.socketToSession.set(socket, msg.sessionId);
|
|
848
|
+
const session = await ctx.getOrCreateSession(msg.sessionId, socket, true);
|
|
849
|
+
// Only wire the escalation handler if one isn't already set — handleTaskSubmit
|
|
850
|
+
// sets a handler with the client's actual screen dimensions, and overwriting it
|
|
851
|
+
// here would replace those dimensions with the daemon's defaults.
|
|
852
|
+
if (!session.hasEscalationHandler()) {
|
|
853
|
+
wireEscalationHandler(session, socket, ctx);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const ipcChannel = parseChannelId(msg.channel) ?? "vellum";
|
|
857
|
+
const sendEvent = makeIpcEventSender({
|
|
858
|
+
ctx,
|
|
859
|
+
socket,
|
|
860
|
+
session,
|
|
861
|
+
conversationId: msg.sessionId,
|
|
862
|
+
sourceChannel: ipcChannel,
|
|
863
|
+
});
|
|
864
|
+
// Route prompter-originated events (confirmation_request/secret_request)
|
|
865
|
+
// through the IPC wrapper so pending-interactions + canonical tracking
|
|
866
|
+
// are updated before the message is sent to the client.
|
|
867
|
+
session.updateClient(sendEvent, false);
|
|
868
|
+
const ipcInterface = parseInterfaceId(msg.interface);
|
|
869
|
+
if (!ipcInterface) {
|
|
870
|
+
ctx.send(socket, {
|
|
871
|
+
type: "error",
|
|
872
|
+
message:
|
|
873
|
+
"Invalid user_message: interface is required and must be valid",
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Update channel capabilities eagerly so both immediate and queued paths
|
|
879
|
+
// reflect the latest PTT / microphone state from the client.
|
|
880
|
+
session.setChannelCapabilities(
|
|
881
|
+
resolveChannelCapabilities(ipcChannel, ipcInterface, {
|
|
882
|
+
pttActivationKey: msg.pttActivationKey,
|
|
883
|
+
microphonePermissionGranted: msg.microphonePermissionGranted,
|
|
884
|
+
}),
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const dispatchUserMessage = buildDispatchUserMessage({
|
|
888
|
+
msg,
|
|
889
|
+
session,
|
|
890
|
+
sendEvent,
|
|
891
|
+
ipcChannel,
|
|
892
|
+
ipcInterface,
|
|
893
|
+
socket,
|
|
894
|
+
ctx,
|
|
895
|
+
rlog,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
let messageText = msg.content ?? "";
|
|
899
|
+
|
|
900
|
+
// Block inbound messages that contain secrets and redirect to secure prompt
|
|
901
|
+
if (
|
|
902
|
+
handleSecretIngress(
|
|
903
|
+
msg,
|
|
904
|
+
messageText,
|
|
905
|
+
socket,
|
|
906
|
+
ctx,
|
|
907
|
+
session,
|
|
908
|
+
rlog,
|
|
909
|
+
dispatchUserMessage,
|
|
910
|
+
)
|
|
911
|
+
) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ── Structured command intent (bypasses text parsing) ──────────────────
|
|
916
|
+
if (
|
|
917
|
+
await handleStructuredRecordingIntent(
|
|
918
|
+
msg,
|
|
919
|
+
messageText,
|
|
920
|
+
session,
|
|
921
|
+
socket,
|
|
922
|
+
ctx,
|
|
923
|
+
rlog,
|
|
924
|
+
)
|
|
925
|
+
) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ── Standalone recording intent interception ──────────────────────────
|
|
930
|
+
const recordingResult = await handleStandaloneRecordingIntent(
|
|
931
|
+
msg,
|
|
932
|
+
messageText,
|
|
933
|
+
session,
|
|
934
|
+
socket,
|
|
935
|
+
ctx,
|
|
936
|
+
rlog,
|
|
937
|
+
);
|
|
938
|
+
if (recordingResult.handled) return;
|
|
939
|
+
messageText = recordingResult.updatedMessageText;
|
|
940
|
+
const originalContentBeforeStrip =
|
|
941
|
+
recordingResult.originalContentBeforeStrip;
|
|
942
|
+
|
|
943
|
+
// ── Pending confirmation reply interception ───────────────────────────
|
|
944
|
+
if (
|
|
945
|
+
await handlePendingConfirmationReply(
|
|
946
|
+
msg,
|
|
947
|
+
messageText,
|
|
948
|
+
requestId,
|
|
949
|
+
session,
|
|
950
|
+
ipcChannel,
|
|
951
|
+
ipcInterface,
|
|
952
|
+
socket,
|
|
953
|
+
ctx,
|
|
954
|
+
rlog,
|
|
955
|
+
)
|
|
956
|
+
) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ── Auto-deny pending confirmations ───────────────────────────────────
|
|
961
|
+
autoDenyPendingConfirmations(msg, requestId, session, rlog);
|
|
962
|
+
|
|
963
|
+
dispatchUserMessage(
|
|
964
|
+
messageText,
|
|
965
|
+
msg.attachments ?? [],
|
|
966
|
+
requestId,
|
|
967
|
+
"user_message",
|
|
968
|
+
msg.activeSurfaceId,
|
|
969
|
+
msg.currentPage,
|
|
970
|
+
originalContentBeforeStrip,
|
|
971
|
+
);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
974
|
+
rlog.error({ err }, "Error setting up user message processing");
|
|
975
|
+
ctx.send(socket, {
|
|
976
|
+
type: "error",
|
|
977
|
+
message: `Failed to process message: ${message}`,
|
|
978
|
+
});
|
|
979
|
+
const classified = classifySessionError(err, { phase: "handler" });
|
|
980
|
+
ctx.send(socket, buildSessionErrorMessage(msg.sessionId, classified));
|
|
981
|
+
}
|
|
982
|
+
}
|