@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.3
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/docs/activation-funnel-telemetry.md +310 -0
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/feature-flag-registry.json +3 -18
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +78 -78
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +12 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +25 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
|
@@ -150,11 +150,25 @@ const STREAMING_ERROR_PATTERNS = [
|
|
|
150
150
|
];
|
|
151
151
|
|
|
152
152
|
// User-initiated cancellation patterns — these should NOT produce conversation_error
|
|
153
|
-
// Image-input validation patterns — Anthropic 400s with
|
|
154
|
-
// block
|
|
155
|
-
//
|
|
153
|
+
// Image-input validation patterns — Anthropic 400s with one of these messages
|
|
154
|
+
// when an image block violates a hard limit. The first matches the per-side
|
|
155
|
+
// pixel cap ("image dimensions exceed max allowed size"); the second matches the
|
|
156
|
+
// base64 payload cap ("image exceeds 5 MB maximum: 7465044 bytes > 5242880
|
|
157
|
+
// bytes"). Distinct classification matters because retrying with the same
|
|
158
|
+
// oversized image is futile — the recovery path must strip or downscale it.
|
|
156
159
|
const IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS = [
|
|
157
160
|
/image dimensions? exceeds? max allowed size/i,
|
|
161
|
+
/image exceeds \d+\s*MB maximum/i,
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
const VISION_NOT_SUPPORTED_PATTERNS = [
|
|
165
|
+
/no endpoints found that support image input/i,
|
|
166
|
+
/does not support image/i,
|
|
167
|
+
/doesn't support image input/i,
|
|
168
|
+
/image input is not supported/i,
|
|
169
|
+
/this model does not support vision/i,
|
|
170
|
+
/vision is not supported/i,
|
|
171
|
+
/multi-?modal.*not.*support/i,
|
|
158
172
|
];
|
|
159
173
|
|
|
160
174
|
const CANCEL_PATTERNS = [/abort/i, /cancel/i];
|
|
@@ -447,11 +461,20 @@ function classifyCore(
|
|
|
447
461
|
return {
|
|
448
462
|
code: "IMAGE_TOO_LARGE",
|
|
449
463
|
userMessage:
|
|
450
|
-
"An
|
|
464
|
+
"An image in this conversation was too large for the AI provider and was automatically reduced. Send your message again to continue.",
|
|
451
465
|
retryable: false,
|
|
452
466
|
errorCategory: "image_dimensions_too_large",
|
|
453
467
|
};
|
|
454
468
|
}
|
|
469
|
+
if (isVisionNotSupported(message)) {
|
|
470
|
+
return {
|
|
471
|
+
code: "PROVIDER_API",
|
|
472
|
+
userMessage:
|
|
473
|
+
"This model doesn't support image input. Remove the image or switch to a vision-capable model.",
|
|
474
|
+
retryable: false,
|
|
475
|
+
errorCategory: "vision_not_supported",
|
|
476
|
+
};
|
|
477
|
+
}
|
|
455
478
|
// Extract the provider detail after "API error (NNN): " prefix
|
|
456
479
|
const detailMatch = message.match(/API error \(\d+\):\s*(.+)/i);
|
|
457
480
|
const detail = detailMatch?.[1];
|
|
@@ -481,6 +504,10 @@ function isImageDimensionsTooLarge(message: string): boolean {
|
|
|
481
504
|
return IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS.some((p) => p.test(message));
|
|
482
505
|
}
|
|
483
506
|
|
|
507
|
+
function isVisionNotSupported(message: string): boolean {
|
|
508
|
+
return VISION_NOT_SUPPORTED_PATTERNS.some((p) => p.test(message));
|
|
509
|
+
}
|
|
510
|
+
|
|
484
511
|
/** Check whether an error message indicates a web-search-specific ordering failure. */
|
|
485
512
|
function isWebSearchOrderingError(message: string): boolean {
|
|
486
513
|
return WEB_SEARCH_ORDERING_PATTERNS.some((p) => p.test(message));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
|
-
import { getSummaryFromContextMessage } from "../context/window-manager.js";
|
|
4
3
|
import {
|
|
5
4
|
deleteLastExchange,
|
|
6
5
|
deleteMessageById,
|
|
@@ -13,6 +12,7 @@ import { enqueueMemoryJob } from "../memory/jobs-store.js";
|
|
|
13
12
|
import { relinkLlmRequestLogs } from "../memory/llm-request-log-store.js";
|
|
14
13
|
import { withQdrantBreaker } from "../memory/qdrant-circuit-breaker.js";
|
|
15
14
|
import { getQdrantClient } from "../memory/qdrant-client.js";
|
|
15
|
+
import { getSummaryFromContextMessage } from "../plugins/defaults/compaction/window-manager.js";
|
|
16
16
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
17
17
|
import { getLogger } from "../util/logger.js";
|
|
18
18
|
import { truncate } from "../util/truncate.js";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { estimateContentBlockTokens } from "../context/token-estimator.js";
|
|
10
|
-
import { getSummaryFromContextMessage } from "../
|
|
10
|
+
import { getSummaryFromContextMessage } from "../plugins/defaults/compaction/window-manager.js";
|
|
11
11
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
12
12
|
|
|
13
13
|
export interface StripMediaOptions {
|
|
@@ -173,15 +173,28 @@ export function estimateUnconditionalStubTokens(
|
|
|
173
173
|
const msg = messages[msgIndex];
|
|
174
174
|
for (const block of msg.content) {
|
|
175
175
|
if (block.type === "image" && msgIndex !== latestUserIndex) {
|
|
176
|
-
stubTokens += estimateContentBlockTokens(
|
|
176
|
+
stubTokens += estimateContentBlockTokens(
|
|
177
|
+
imageBlockToStub(block),
|
|
178
|
+
options,
|
|
179
|
+
);
|
|
177
180
|
} else if (block.type === "file" && msgIndex !== latestUserIndex) {
|
|
178
|
-
stubTokens += estimateContentBlockTokens(
|
|
179
|
-
|
|
181
|
+
stubTokens += estimateContentBlockTokens(
|
|
182
|
+
fileBlockToStub(block),
|
|
183
|
+
options,
|
|
184
|
+
);
|
|
185
|
+
} else if (block.type === "tool_result" && block.contentBlocks) {
|
|
186
|
+
// guard:allow-tool-result-only — web_search_tool_result has no contentBlocks
|
|
180
187
|
for (const cb of block.contentBlocks) {
|
|
181
188
|
if (cb.type === "image") {
|
|
182
|
-
stubTokens += estimateContentBlockTokens(
|
|
189
|
+
stubTokens += estimateContentBlockTokens(
|
|
190
|
+
imageBlockToStub(cb),
|
|
191
|
+
options,
|
|
192
|
+
);
|
|
183
193
|
} else if (cb.type === "file") {
|
|
184
|
-
stubTokens += estimateContentBlockTokens(
|
|
194
|
+
stubTokens += estimateContentBlockTokens(
|
|
195
|
+
fileBlockToStub(cb),
|
|
196
|
+
options,
|
|
197
|
+
);
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
}
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
44
44
|
import type { SecretPrompter } from "../permissions/secret-prompter.js";
|
|
45
45
|
import type { Message } from "../providers/types.js";
|
|
46
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
46
47
|
import { getLogger } from "../util/logger.js";
|
|
47
48
|
import type { MessageQueue } from "./conversation-queue-manager.js";
|
|
48
49
|
import type { SlackInboundMessageMetadata } from "./handlers/shared.js";
|
|
@@ -193,6 +194,9 @@ export interface MessagingConversationContext {
|
|
|
193
194
|
currentRequestId?: string;
|
|
194
195
|
readonly queue: MessageQueue;
|
|
195
196
|
trustContext?: TrustContext;
|
|
197
|
+
authContext?: AuthContext;
|
|
198
|
+
currentTurnAuthContext?: AuthContext;
|
|
199
|
+
currentTurnSourceActorPrincipalId?: string;
|
|
196
200
|
getTurnChannelContext(): TurnChannelContext | null;
|
|
197
201
|
getTurnInterfaceContext(): TurnInterfaceContext | null;
|
|
198
202
|
}
|
|
@@ -302,6 +306,10 @@ export interface EnqueueMessageOptions {
|
|
|
302
306
|
displayContent?: string;
|
|
303
307
|
transport?: ConversationTransportMetadata;
|
|
304
308
|
clientMessageId?: string;
|
|
309
|
+
/** JWT-verified requester principal captured for queued host-proxy routing. */
|
|
310
|
+
sourceActorPrincipalId?: string;
|
|
311
|
+
/** Auth context snapshot captured for queued turn-scoped authorization. */
|
|
312
|
+
authContext?: AuthContext;
|
|
305
313
|
}
|
|
306
314
|
|
|
307
315
|
// ── enqueueMessage ───────────────────────────────────────────────────
|
|
@@ -322,7 +330,14 @@ export function enqueueMessage(
|
|
|
322
330
|
displayContent,
|
|
323
331
|
transport,
|
|
324
332
|
clientMessageId,
|
|
333
|
+
authContext,
|
|
325
334
|
} = options;
|
|
335
|
+
const queuedAuthContext =
|
|
336
|
+
authContext ?? ctx.currentTurnAuthContext ?? ctx.authContext;
|
|
337
|
+
const sourceActorPrincipalId =
|
|
338
|
+
options.sourceActorPrincipalId ??
|
|
339
|
+
ctx.currentTurnSourceActorPrincipalId ??
|
|
340
|
+
queuedAuthContext?.actorPrincipalId;
|
|
326
341
|
|
|
327
342
|
if (!ctx.isProcessing()) {
|
|
328
343
|
return { queued: false, requestId };
|
|
@@ -347,6 +362,8 @@ export function enqueueMessage(
|
|
|
347
362
|
turnChannelContext,
|
|
348
363
|
turnInterfaceContext,
|
|
349
364
|
isInteractive,
|
|
365
|
+
sourceActorPrincipalId,
|
|
366
|
+
authContext: queuedAuthContext,
|
|
350
367
|
transport,
|
|
351
368
|
displayContent,
|
|
352
369
|
sentAt: Date.now(),
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
type TurnInterfaceContext,
|
|
19
19
|
} from "../channels/types.js";
|
|
20
20
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
21
|
-
import type { ContextWindowResult } from "../context/window-manager.js";
|
|
22
21
|
import { listPendingRequestsByConversationScope } from "../memory/canonical-guardian-store.js";
|
|
23
22
|
import {
|
|
24
23
|
addMessage,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
} from "../memory/conversation-crud.js";
|
|
29
28
|
import { extractPreferences } from "../notifications/preference-extractor.js";
|
|
30
29
|
import { createPreference } from "../notifications/preferences-store.js";
|
|
30
|
+
import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
|
|
31
31
|
import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
|
|
32
32
|
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
33
33
|
import { getLogger } from "../util/logger.js";
|
|
@@ -222,6 +222,8 @@ async function buildPassthroughBatch(
|
|
|
222
222
|
// otherwise diverge.
|
|
223
223
|
if (candIf?.userMessageInterface !== headInterface?.userMessageInterface)
|
|
224
224
|
break;
|
|
225
|
+
if (candidate.sourceActorPrincipalId !== head.sourceActorPrincipalId)
|
|
226
|
+
break;
|
|
225
227
|
if (classifySlash(candidate.content) !== "passthrough") break;
|
|
226
228
|
if (
|
|
227
229
|
resolveVerificationSessionIntent(candidate.content).kind ===
|
|
@@ -422,6 +424,9 @@ async function drainSingleMessage(
|
|
|
422
424
|
conversation.applyClientTimezoneFromTransport(next.transport);
|
|
423
425
|
}
|
|
424
426
|
|
|
427
|
+
conversation.currentTurnAuthContext = next.authContext;
|
|
428
|
+
conversation.currentTurnSourceActorPrincipalId = next.sourceActorPrincipalId;
|
|
429
|
+
|
|
425
430
|
// Re-attach and re-preactivate host-proxy skills for interactive turns.
|
|
426
431
|
// The dequeue path reset `preactivatedSkillIds` above; without these
|
|
427
432
|
// re-adds the relevant skill tools won't be projected to the LLM for
|
|
@@ -433,8 +438,7 @@ async function drainSingleMessage(
|
|
|
433
438
|
const interfaceCtx =
|
|
434
439
|
queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
|
|
435
440
|
const sourceInterface = interfaceCtx?.userMessageInterface;
|
|
436
|
-
const sourceActorPrincipalId =
|
|
437
|
-
conversation.trustContext?.guardianPrincipalId;
|
|
441
|
+
const sourceActorPrincipalId = next.sourceActorPrincipalId;
|
|
438
442
|
conversation.ensureHostProxiesForTurn(
|
|
439
443
|
sourceInterface,
|
|
440
444
|
sourceActorPrincipalId,
|
|
@@ -979,14 +983,16 @@ async function drainBatch(
|
|
|
979
983
|
conversation.applyClientTimezoneFromTransport(head.transport);
|
|
980
984
|
}
|
|
981
985
|
|
|
986
|
+
conversation.currentTurnAuthContext = head.authContext;
|
|
987
|
+
conversation.currentTurnSourceActorPrincipalId = head.sourceActorPrincipalId;
|
|
988
|
+
|
|
982
989
|
// Re-attach and re-preactivate host-proxy skills for interactive turns.
|
|
983
990
|
// Mirrors the single-message path exactly — sourced from `head`.
|
|
984
991
|
if (head.isInteractive !== false) {
|
|
985
992
|
const interfaceCtx =
|
|
986
993
|
queuedInterfaceCtx ?? conversation.getTurnInterfaceContext();
|
|
987
994
|
const sourceInterface = interfaceCtx?.userMessageInterface;
|
|
988
|
-
const sourceActorPrincipalId =
|
|
989
|
-
conversation.trustContext?.guardianPrincipalId;
|
|
995
|
+
const sourceActorPrincipalId = head.sourceActorPrincipalId;
|
|
990
996
|
conversation.ensureHostProxiesForTurn(
|
|
991
997
|
sourceInterface,
|
|
992
998
|
sourceActorPrincipalId,
|
|
@@ -1380,6 +1386,9 @@ export async function processMessage(
|
|
|
1380
1386
|
// Snapshot persona context at turn start so later tool turns can't pick up
|
|
1381
1387
|
// a different actor's context if a concurrent request mutates the live fields.
|
|
1382
1388
|
conversation.currentTurnTrustContext = conversation.trustContext;
|
|
1389
|
+
conversation.currentTurnAuthContext = conversation.authContext;
|
|
1390
|
+
conversation.currentTurnSourceActorPrincipalId =
|
|
1391
|
+
conversation.authContext?.actorPrincipalId;
|
|
1383
1392
|
conversation.currentTurnChannelCapabilities =
|
|
1384
1393
|
conversation.channelCapabilities;
|
|
1385
1394
|
conversation.currentActiveSurfaceId = activeSurfaceId;
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
TurnChannelContext,
|
|
10
10
|
TurnInterfaceContext,
|
|
11
11
|
} from "../channels/types.js";
|
|
12
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
12
13
|
import { getLogger } from "../util/logger.js";
|
|
13
14
|
import type {
|
|
14
15
|
ServerMessage,
|
|
@@ -30,6 +31,10 @@ export interface QueuedMessage {
|
|
|
30
31
|
turnInterfaceContext?: TurnInterfaceContext;
|
|
31
32
|
/** When false, the turn has no interactive user and should skip clarification prompts. */
|
|
32
33
|
isInteractive?: boolean;
|
|
34
|
+
/** Requester identity captured from the verified auth context at enqueue time. */
|
|
35
|
+
sourceActorPrincipalId?: string;
|
|
36
|
+
/** Full auth snapshot captured at enqueue time for turn-scoped authorization decisions. */
|
|
37
|
+
authContext?: AuthContext;
|
|
33
38
|
/** Transport metadata snapshot captured at enqueue time, applied when this message becomes active. */
|
|
34
39
|
transport?: ConversationTransportMetadata;
|
|
35
40
|
/** Original user message text to persist to DB when recording intent stripping produced a different `content`. */
|
|
@@ -204,6 +209,9 @@ function estimateItemBytes(item: QueuedMessage): number {
|
|
|
204
209
|
if (item.transport) {
|
|
205
210
|
bytes += JSON.stringify(item.transport).length * 2;
|
|
206
211
|
}
|
|
212
|
+
if (item.sourceActorPrincipalId) {
|
|
213
|
+
bytes += item.sourceActorPrincipalId.length * 2;
|
|
214
|
+
}
|
|
207
215
|
// Small fixed overhead for metadata, pointers, etc. (not worth
|
|
208
216
|
// measuring precisely — the content/attachment data dominates).
|
|
209
217
|
bytes += 512;
|
|
@@ -13,7 +13,6 @@ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags
|
|
|
13
13
|
import { getConfig } from "../config/loader.js";
|
|
14
14
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
15
15
|
import { stripUserTextBlocksByPrefix } from "../context/strip-injections.js";
|
|
16
|
-
import { createContextSummaryMessage } from "../context/window-manager.js";
|
|
17
16
|
import { getDocumentsForConversation } from "../documents/document-store.js";
|
|
18
17
|
import {
|
|
19
18
|
getApp,
|
|
@@ -43,6 +42,7 @@ import {
|
|
|
43
42
|
type RenderedSlackTranscriptMessage,
|
|
44
43
|
renderSlackTranscriptWithProvenance,
|
|
45
44
|
} from "../messaging/providers/slack/render-transcript.js";
|
|
45
|
+
import { createContextSummaryMessage } from "../plugins/defaults/compaction/window-manager.js";
|
|
46
46
|
import { getInjectorChain } from "../plugins/defaults/memory-retrieval/injector-chain.js";
|
|
47
47
|
import { MEMORY_V3_BLOCK_ID } from "../plugins/defaults/memory-v3-shadow/types.js";
|
|
48
48
|
import type {
|
|
@@ -54,8 +54,10 @@ import type { ContentBlock, Message } from "../providers/types.js";
|
|
|
54
54
|
import {
|
|
55
55
|
type ActorTrustContext,
|
|
56
56
|
isUntrustedTrustClass,
|
|
57
|
+
resolveActorTrust,
|
|
57
58
|
type TrustClass,
|
|
58
59
|
} from "../runtime/actor-trust-resolver.js";
|
|
60
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
59
61
|
import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
|
|
60
62
|
import { getSubagentManager } from "../subagent/index.js";
|
|
61
63
|
import type { SubagentState } from "../subagent/types.js";
|
|
@@ -174,6 +176,40 @@ export function inboundActorContextFromTrust(
|
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the model-facing inbound actor context for a turn from the
|
|
181
|
+
* conversation's trust context, for the unified `<turn_context>` actor section.
|
|
182
|
+
*
|
|
183
|
+
* Returns `null` when there is no trust context and on guardian (owner) turns —
|
|
184
|
+
* the actor section is suppressed for the owner. When the trust context carries
|
|
185
|
+
* enough identity to look up member status / policy and the guardian binding,
|
|
186
|
+
* the unified actor-trust resolver is preferred; otherwise the context is
|
|
187
|
+
* projected directly. Derives purely from the passed trust context, so callers
|
|
188
|
+
* self-resolve it from the live conversation rather than threading it.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveTurnInboundActorContext(
|
|
191
|
+
trustContext: TrustContext | undefined,
|
|
192
|
+
assistantId: string | undefined,
|
|
193
|
+
): InboundActorContext | null {
|
|
194
|
+
if (!trustContext) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
let resolved: InboundActorContext;
|
|
198
|
+
if (trustContext.requesterExternalUserId && trustContext.requesterChatId) {
|
|
199
|
+
const actorTrust = resolveActorTrust({
|
|
200
|
+
assistantId: assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
|
|
201
|
+
sourceChannel: trustContext.sourceChannel,
|
|
202
|
+
conversationExternalId: trustContext.requesterChatId,
|
|
203
|
+
actorExternalId: trustContext.requesterExternalUserId,
|
|
204
|
+
actorDisplayName: trustContext.requesterSenderDisplayName,
|
|
205
|
+
});
|
|
206
|
+
resolved = inboundActorContextFromTrust(actorTrust);
|
|
207
|
+
} else {
|
|
208
|
+
resolved = inboundActorContextFromTrustContext(trustContext);
|
|
209
|
+
}
|
|
210
|
+
return resolved.trustClass === "guardian" ? null : resolved;
|
|
211
|
+
}
|
|
212
|
+
|
|
177
213
|
/** Derive channel capabilities from source channel + interface identifiers. */
|
|
178
214
|
export function resolveChannelCapabilities(
|
|
179
215
|
sourceChannel?: string | null,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
|
+
import { isActivationSession } from "../memory/activation-session-store.js";
|
|
3
4
|
import {
|
|
4
5
|
addAppConversationId,
|
|
5
6
|
getApp,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
getMessages,
|
|
15
16
|
updateMessageContent,
|
|
16
17
|
} from "../memory/conversation-crud.js";
|
|
18
|
+
import { recordActivationEvent } from "../memory/onboarding-events-store.js";
|
|
17
19
|
import {
|
|
18
20
|
assistantEventHub,
|
|
19
21
|
broadcastMessage,
|
|
@@ -23,10 +25,17 @@ import {
|
|
|
23
25
|
enforceSameActorOrErrorResult,
|
|
24
26
|
pickSameUserAutoResolve,
|
|
25
27
|
} from "../runtime/auth/same-actor.js";
|
|
28
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
26
29
|
import type {
|
|
27
30
|
InteractiveUiRequest,
|
|
28
31
|
InteractiveUiResult,
|
|
29
32
|
} from "../runtime/interactive-ui-types.js";
|
|
33
|
+
import {
|
|
34
|
+
activationMomentEmitsAtShow,
|
|
35
|
+
type ActivationMomentParam,
|
|
36
|
+
activationStepNameForMomentParam,
|
|
37
|
+
isActivationMomentParam,
|
|
38
|
+
} from "../telemetry/activation-funnel.js";
|
|
30
39
|
import type { ToolExecutionResult } from "../tools/types.js";
|
|
31
40
|
import { getLogger } from "../util/logger.js";
|
|
32
41
|
import { isPlainObject } from "../util/object.js";
|
|
@@ -544,6 +553,12 @@ export interface SurfaceConversationContext {
|
|
|
544
553
|
readonly assistantId?: string;
|
|
545
554
|
/** Inherited to spawned conversations in the `launch_conversation` action path. */
|
|
546
555
|
readonly trustContext?: TrustContext;
|
|
556
|
+
/** Verified requester auth context for the active turn. */
|
|
557
|
+
readonly authContext?: AuthContext;
|
|
558
|
+
/** Per-turn auth snapshot, preferred for tool dispatch authorization. */
|
|
559
|
+
readonly currentTurnAuthContext?: AuthContext;
|
|
560
|
+
/** JWT-verified requester principal for the active turn. */
|
|
561
|
+
readonly currentTurnSourceActorPrincipalId?: string;
|
|
547
562
|
readonly channelCapabilities?: {
|
|
548
563
|
channel: string;
|
|
549
564
|
supportsDynamicUi: boolean;
|
|
@@ -569,6 +584,14 @@ export interface SurfaceConversationContext {
|
|
|
569
584
|
style?: string;
|
|
570
585
|
data?: Record<string, unknown>;
|
|
571
586
|
}>;
|
|
587
|
+
/**
|
|
588
|
+
* Activation-rail telemetry tag (daemon-only). When the model tags a
|
|
589
|
+
* `ui_show` surface as an activation funnel moment, the token is captured
|
|
590
|
+
* here so the milestone can be recorded deterministically when the user
|
|
591
|
+
* commits the surface (`handleSurfaceAction`). Never forwarded to the
|
|
592
|
+
* client.
|
|
593
|
+
*/
|
|
594
|
+
activationMoment?: ActivationMomentParam;
|
|
572
595
|
}
|
|
573
596
|
>;
|
|
574
597
|
surfaceUndoStacks: Map<string, string[]>;
|
|
@@ -613,6 +636,12 @@ export interface SurfaceConversationContext {
|
|
|
613
636
|
display?: string;
|
|
614
637
|
persistent?: boolean;
|
|
615
638
|
toolCallId?: string;
|
|
639
|
+
/**
|
|
640
|
+
* Commit-timing activation-rail tag (daemon-only). Carried through to the
|
|
641
|
+
* persisted `ui_surface` history block so it survives a reload — never sent
|
|
642
|
+
* to the client.
|
|
643
|
+
*/
|
|
644
|
+
activationMoment?: ActivationMomentParam;
|
|
616
645
|
}>;
|
|
617
646
|
/** Optional proxy for delegating computer-use actions to a connected desktop client. */
|
|
618
647
|
hostCuProxy?: HostCuProxy;
|
|
@@ -1250,6 +1279,60 @@ function getRequestedSurfaceCompletionSummary(
|
|
|
1250
1279
|
return summary || "Completed";
|
|
1251
1280
|
}
|
|
1252
1281
|
|
|
1282
|
+
/**
|
|
1283
|
+
* Best-effort recorder for a single activation-funnel moment. Gated on the
|
|
1284
|
+
* conversation being a marked activation-rail session. Fire-and-forget: never
|
|
1285
|
+
* throws, never blocks or alters the surface-action flow (a failure is logged
|
|
1286
|
+
* and swallowed). Shared by the show-time path (`first_wow_executed`, recorded
|
|
1287
|
+
* when the surface renders) and the commit-time path (the other moments,
|
|
1288
|
+
* recorded when the user commits the surface).
|
|
1289
|
+
*/
|
|
1290
|
+
function recordActivationMoment(
|
|
1291
|
+
ctx: SurfaceConversationContext,
|
|
1292
|
+
moment: ActivationMomentParam,
|
|
1293
|
+
): void {
|
|
1294
|
+
try {
|
|
1295
|
+
if (!isActivationSession(ctx.conversationId)) return;
|
|
1296
|
+
recordActivationEvent({
|
|
1297
|
+
stepName: activationStepNameForMomentParam(moment),
|
|
1298
|
+
sessionId: ctx.conversationId,
|
|
1299
|
+
});
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
log.warn(
|
|
1302
|
+
{ err, conversationId: ctx.conversationId, moment },
|
|
1303
|
+
"Failed to record activation moment",
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
/**
|
|
1309
|
+
* Best-effort activation-funnel emission on a user surface commit.
|
|
1310
|
+
*
|
|
1311
|
+
* When the committed surface carries a commit-timing `activationMoment` tag,
|
|
1312
|
+
* record the corresponding funnel milestone. Show-timing moments
|
|
1313
|
+
* (`first_wow_executed`) are recorded at render time in `surfaceProxyResolver`
|
|
1314
|
+
* and are NOT stored on `surfaceState`, so they never reach this path. The tag
|
|
1315
|
+
* is cleared after the first record so re-entrant or repeated commits on the
|
|
1316
|
+
* same surface do not double-emit (and the deterministic `daemon_event_id`
|
|
1317
|
+
* collapses any cross-surface duplicate downstream anyway).
|
|
1318
|
+
*
|
|
1319
|
+
* Must be called only from terminal-commit paths (user clicked an action /
|
|
1320
|
+
* submitted / selected-and-committed), NOT from intermediate non-terminal
|
|
1321
|
+
* events (`selection_changed` / `content_changed` / `state_update`).
|
|
1322
|
+
*/
|
|
1323
|
+
function maybeEmitActivationMoment(
|
|
1324
|
+
ctx: SurfaceConversationContext,
|
|
1325
|
+
surfaceId: string,
|
|
1326
|
+
): void {
|
|
1327
|
+
const stored = ctx.surfaceState.get(surfaceId);
|
|
1328
|
+
const moment = stored?.activationMoment;
|
|
1329
|
+
if (!moment) return;
|
|
1330
|
+
// Clear the tag first so this can fire at most once per surface even if the
|
|
1331
|
+
// commit path is re-entered.
|
|
1332
|
+
stored.activationMoment = undefined;
|
|
1333
|
+
recordActivationMoment(ctx, moment);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1253
1336
|
function completeSurfaceFromAction(
|
|
1254
1337
|
ctx: SurfaceConversationContext,
|
|
1255
1338
|
surfaceId: string,
|
|
@@ -1317,6 +1400,13 @@ export async function handleSurfaceAction(
|
|
|
1317
1400
|
});
|
|
1318
1401
|
markSurfaceCompleted(ctx, surfaceId, summary);
|
|
1319
1402
|
|
|
1403
|
+
// Terminal user commit on a standalone surface (submit, not cancel/dismiss)
|
|
1404
|
+
// — record an activation milestone if tagged. Must run before
|
|
1405
|
+
// `cleanupStandaloneSurface` clears the surface state.
|
|
1406
|
+
if (!isCancellation) {
|
|
1407
|
+
maybeEmitActivationMoment(ctx, surfaceId);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1320
1410
|
// Cleanup and resolve — order matters: cleanup clears the timer
|
|
1321
1411
|
// before resolve() unblocks the caller.
|
|
1322
1412
|
cleanupStandaloneSurface(ctx, surfaceId);
|
|
@@ -1403,6 +1493,10 @@ export async function handleSurfaceAction(
|
|
|
1403
1493
|
{ originConversationId: ctx.conversationId, conversationId, surfaceId },
|
|
1404
1494
|
"launch_conversation dispatched inline from surface action",
|
|
1405
1495
|
);
|
|
1496
|
+
// Launching a child conversation is a terminal user commit — record an
|
|
1497
|
+
// activation milestone if tagged. The helper clears the tag after firing,
|
|
1498
|
+
// so the other commit-path call sites below can't double-emit.
|
|
1499
|
+
maybeEmitActivationMoment(ctx, surfaceId);
|
|
1406
1500
|
return { accepted: true, conversationId };
|
|
1407
1501
|
}
|
|
1408
1502
|
|
|
@@ -1557,6 +1651,12 @@ export async function handleSurfaceAction(
|
|
|
1557
1651
|
return;
|
|
1558
1652
|
}
|
|
1559
1653
|
|
|
1654
|
+
// Terminal user commit accepted — record the activation milestone if this
|
|
1655
|
+
// surface was tagged (best-effort, no-op otherwise). Deferred until after
|
|
1656
|
+
// the rejection check so a queue-full click doesn't over-report a moment
|
|
1657
|
+
// (and the one-shot tag stays intact for the user's retry).
|
|
1658
|
+
maybeEmitActivationMoment(ctx, surfaceId);
|
|
1659
|
+
|
|
1560
1660
|
const requestedCompletionSummary =
|
|
1561
1661
|
getRequestedSurfaceCompletionSummary(mergedData);
|
|
1562
1662
|
if (requestedCompletionSummary) {
|
|
@@ -1796,6 +1896,12 @@ export async function handleSurfaceAction(
|
|
|
1796
1896
|
return;
|
|
1797
1897
|
}
|
|
1798
1898
|
|
|
1899
|
+
// Terminal user commit accepted — record the activation milestone if this
|
|
1900
|
+
// surface was tagged (best-effort, no-op otherwise). Deferred until after the
|
|
1901
|
+
// rejection check so a queue-full click doesn't over-report a moment (and the
|
|
1902
|
+
// one-shot tag stays intact for the user's retry).
|
|
1903
|
+
maybeEmitActivationMoment(ctx, surfaceId);
|
|
1904
|
+
|
|
1799
1905
|
const requestedCompletionSummary =
|
|
1800
1906
|
getRequestedSurfaceCompletionSummary(mergedData);
|
|
1801
1907
|
|
|
@@ -2215,7 +2321,10 @@ export async function surfaceProxyResolver(
|
|
|
2215
2321
|
// validate at the tool-resolution layer for the same reason. The proxy
|
|
2216
2322
|
// re-checks same-user (single authoritative gate); using the shared
|
|
2217
2323
|
// helper keeps log payload and error wording identical at both layers.
|
|
2218
|
-
const sourceActorPrincipalId =
|
|
2324
|
+
const sourceActorPrincipalId =
|
|
2325
|
+
ctx.currentTurnSourceActorPrincipalId ??
|
|
2326
|
+
ctx.currentTurnAuthContext?.actorPrincipalId ??
|
|
2327
|
+
ctx.authContext?.actorPrincipalId;
|
|
2219
2328
|
if (targetClientId != null) {
|
|
2220
2329
|
const client = assistantEventHub.getClientById(targetClientId);
|
|
2221
2330
|
if (!client) {
|
|
@@ -2321,7 +2430,10 @@ export async function surfaceProxyResolver(
|
|
|
2321
2430
|
? input.target_client_id
|
|
2322
2431
|
: undefined;
|
|
2323
2432
|
|
|
2324
|
-
const sourceActorPrincipalId =
|
|
2433
|
+
const sourceActorPrincipalId =
|
|
2434
|
+
ctx.currentTurnSourceActorPrincipalId ??
|
|
2435
|
+
ctx.currentTurnAuthContext?.actorPrincipalId ??
|
|
2436
|
+
ctx.authContext?.actorPrincipalId;
|
|
2325
2437
|
if (targetClientId != null) {
|
|
2326
2438
|
const client = assistantEventHub.getClientById(targetClientId);
|
|
2327
2439
|
if (!client) {
|
|
@@ -2504,6 +2616,24 @@ export async function surfaceProxyResolver(
|
|
|
2504
2616
|
...(a.data ? { data: a.data } : {}),
|
|
2505
2617
|
}));
|
|
2506
2618
|
|
|
2619
|
+
// Optional activation-rail telemetry tag. Daemon-only metadata: validated
|
|
2620
|
+
// and ignored if invalid; never forwarded to the client.
|
|
2621
|
+
const activationMoment =
|
|
2622
|
+
typeof input.activation_moment === "string" &&
|
|
2623
|
+
isActivationMomentParam(input.activation_moment)
|
|
2624
|
+
? input.activation_moment
|
|
2625
|
+
: undefined;
|
|
2626
|
+
|
|
2627
|
+
// Show-timing moments (`first_wow_executed`) record the instant the surface
|
|
2628
|
+
// renders — a display-only result/`work_result` card may never be committed,
|
|
2629
|
+
// so a commit-time emit would never fire. We record now and do NOT store the
|
|
2630
|
+
// tag, so the commit path won't double-emit. Commit-timing moments are
|
|
2631
|
+
// stored and recorded when the user commits the surface (see
|
|
2632
|
+
// `handleSurfaceAction` → `maybeEmitActivationMoment`).
|
|
2633
|
+
const storeTagForCommit =
|
|
2634
|
+
activationMoment !== undefined &&
|
|
2635
|
+
!activationMomentEmitsAtShow(activationMoment);
|
|
2636
|
+
|
|
2507
2637
|
// Track surface state for ui_update merging (includes actions so we can
|
|
2508
2638
|
// look up per-action data payloads when the client sends an action back).
|
|
2509
2639
|
ctx.surfaceState.set(surfaceId, {
|
|
@@ -2511,8 +2641,13 @@ export async function surfaceProxyResolver(
|
|
|
2511
2641
|
data,
|
|
2512
2642
|
title,
|
|
2513
2643
|
actions: mappedActions,
|
|
2644
|
+
...(storeTagForCommit ? { activationMoment } : {}),
|
|
2514
2645
|
});
|
|
2515
2646
|
|
|
2647
|
+
if (activationMoment !== undefined && !storeTagForCommit) {
|
|
2648
|
+
recordActivationMoment(ctx, activationMoment);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2516
2651
|
log.info(
|
|
2517
2652
|
{
|
|
2518
2653
|
surfaceId,
|
|
@@ -2540,7 +2675,9 @@ export async function surfaceProxyResolver(
|
|
|
2540
2675
|
...(toolUseId ? { toolCallId: toolUseId } : {}),
|
|
2541
2676
|
} as unknown as UiSurfaceShow);
|
|
2542
2677
|
|
|
2543
|
-
// Track surface for persistence with the message
|
|
2678
|
+
// Track surface for persistence with the message. The commit-timing
|
|
2679
|
+
// activation tag rides along (daemon-only) so it survives history restore;
|
|
2680
|
+
// show-timing moments aren't stored here (already recorded at render).
|
|
2544
2681
|
ctx.currentTurnSurfaces.push({
|
|
2545
2682
|
surfaceId,
|
|
2546
2683
|
surfaceType,
|
|
@@ -2550,6 +2687,7 @@ export async function surfaceProxyResolver(
|
|
|
2550
2687
|
display,
|
|
2551
2688
|
...(persistent ? { persistent: true } : {}),
|
|
2552
2689
|
...(toolUseId ? { toolCallId: toolUseId } : {}),
|
|
2690
|
+
...(storeTagForCommit ? { activationMoment } : {}),
|
|
2553
2691
|
});
|
|
2554
2692
|
|
|
2555
2693
|
if (awaitAction) {
|