@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -156,7 +156,9 @@ export async function createInboundChallenge(
|
|
|
156
156
|
success: false,
|
|
157
157
|
error: "already_bound",
|
|
158
158
|
message:
|
|
159
|
-
|
|
159
|
+
resolvedChannel === "phone"
|
|
160
|
+
? "A guardian is already bound for this channel. On phone, verify through an outbound call with rebind: true. To a new number, it replaces the bound number once the code is entered. To the bound number, it confirms that number again and leaves the binding as it is. An inbound challenge cannot be completed by phone: a call collects a six-digit code, which an inbound challenge does not issue."
|
|
161
|
+
: "A guardian is already bound for this channel. To verify a different account, revoke the existing binding first. Set rebind: true only to verify the same account again.",
|
|
160
162
|
channel: resolvedChannel,
|
|
161
163
|
};
|
|
162
164
|
}
|
|
@@ -14,6 +14,12 @@ export interface ConversationSearchParams {
|
|
|
14
14
|
query: string;
|
|
15
15
|
limit?: number;
|
|
16
16
|
maxMessagesPerConversation?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Reach archived conversations as well as live ones. Defaults to false, so
|
|
19
|
+
* this search matches what the sidebar shows. Callers opt in where an
|
|
20
|
+
* archived chat is a filed chat rather than a hidden one.
|
|
21
|
+
*/
|
|
22
|
+
includeArchived?: boolean;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/** Search conversations and return results (no transport dependency). */
|
|
@@ -22,7 +28,10 @@ export async function performConversationSearch(
|
|
|
22
28
|
) {
|
|
23
29
|
// Treat "*" as a list-all wildcard — FTS treats it as a literal character.
|
|
24
30
|
if (params.query.trim() === "*") {
|
|
25
|
-
const rows = listConversations({
|
|
31
|
+
const rows = listConversations({
|
|
32
|
+
limit: params.limit,
|
|
33
|
+
archiveStatus: params.includeArchived ? "all" : "active",
|
|
34
|
+
});
|
|
26
35
|
return rows.map((r) => ({
|
|
27
36
|
conversationId: r.id,
|
|
28
37
|
conversationTitle: r.title,
|
|
@@ -33,6 +42,7 @@ export async function performConversationSearch(
|
|
|
33
42
|
return searchConversations(params.query, {
|
|
34
43
|
limit: params.limit,
|
|
35
44
|
maxMessagesPerConversation: params.maxMessagesPerConversation,
|
|
45
|
+
includeArchived: params.includeArchived,
|
|
36
46
|
});
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -10,9 +10,11 @@ import { resolveConversationId } from "../../persistence/conversation-key-store.
|
|
|
10
10
|
import { broadcastMessage } from "../../runtime/assistant-event-hub.js";
|
|
11
11
|
import { resolveCapabilities } from "../../runtime/capabilities.js";
|
|
12
12
|
import * as pendingInteractions from "../../runtime/pending-interactions.js";
|
|
13
|
+
import { resolvePendingQuestion } from "../../runtime/question-resolution.js";
|
|
13
14
|
import { getSubagentManager } from "../../subagent/index.js";
|
|
14
15
|
import { createAbortReason } from "../../util/abort-reasons.js";
|
|
15
16
|
import { UserError } from "../../util/errors.js";
|
|
17
|
+
import type { Conversation } from "../conversation.js";
|
|
16
18
|
import { touchConversation } from "../conversation-evictor.js";
|
|
17
19
|
import { forceClearStaleProcessing } from "../conversation-lifecycle.js";
|
|
18
20
|
import {
|
|
@@ -24,7 +26,7 @@ import {
|
|
|
24
26
|
conversationEntries,
|
|
25
27
|
findConversation,
|
|
26
28
|
} from "../conversation-registry.js";
|
|
27
|
-
import { resolveSlash } from "../conversation-slash.js";
|
|
29
|
+
import { classifySlash, resolveSlash } from "../conversation-slash.js";
|
|
28
30
|
import {
|
|
29
31
|
clearAllActiveConversations,
|
|
30
32
|
getOrCreateConversation,
|
|
@@ -328,23 +330,36 @@ export function deleteQueuedMessage(
|
|
|
328
330
|
);
|
|
329
331
|
return { removed: false, reason: "forbidden" };
|
|
330
332
|
}
|
|
331
|
-
conversation
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
333
|
+
consumeQueuedMessage(conversation, conversationId, queued);
|
|
334
|
+
return { removed: true };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Drop a queued message from the queue and close out the client row it
|
|
339
|
+
* created, so a message that will never run leaves no pending indicator
|
|
340
|
+
* behind.
|
|
341
|
+
*
|
|
342
|
+
* Queue events come in pairs, so an entry that never produced a
|
|
343
|
+
* `message_queued` ack (a suppressed daemon-injected send, or a hidden machine
|
|
344
|
+
* send) must not produce a delete either: clients have no row to retire and an
|
|
345
|
+
* unpaired event would decrement a counter that was never incremented.
|
|
346
|
+
*/
|
|
347
|
+
function consumeQueuedMessage(
|
|
348
|
+
conversation: Conversation,
|
|
349
|
+
conversationId: string,
|
|
350
|
+
queued: QueuedMessage,
|
|
351
|
+
): void {
|
|
352
|
+
conversation.removeQueuedMessage(queued.requestId);
|
|
337
353
|
if (!isSuppressedQueuedMessage(queued.metadata)) {
|
|
338
354
|
queued.onEvent({
|
|
339
355
|
type: "message_queued_deleted",
|
|
340
356
|
conversationId,
|
|
341
|
-
requestId,
|
|
357
|
+
requestId: queued.requestId,
|
|
342
358
|
...(queued.clientMessageId
|
|
343
359
|
? { clientMessageId: queued.clientMessageId }
|
|
344
360
|
: {}),
|
|
345
361
|
});
|
|
346
362
|
}
|
|
347
|
-
return { removed: true };
|
|
348
363
|
}
|
|
349
364
|
|
|
350
365
|
/**
|
|
@@ -458,6 +473,92 @@ export function steerToMessage(
|
|
|
458
473
|
return { steered: true };
|
|
459
474
|
}
|
|
460
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Answer an open `ask_question` prompt with a message the user typed into the
|
|
478
|
+
* chat while the card was up.
|
|
479
|
+
*
|
|
480
|
+
* Typing an answer instead of using the card is the same act as typing it into
|
|
481
|
+
* the card's free-text field, so it resolves the prompt rather than cancelling
|
|
482
|
+
* it: the parked turn keeps running and the model reads the answer in the tool
|
|
483
|
+
* result. Channel replies already work this way (the bare-text branch of
|
|
484
|
+
* `runtime/guardian-reply-router.ts`); this is the app path saying the same
|
|
485
|
+
* thing through the same `resolvePendingQuestion` core.
|
|
486
|
+
*
|
|
487
|
+
* The message is consumed, not run as a turn of its own: it reaches the model
|
|
488
|
+
* once, through the tool result, and the answered card carries the user's own
|
|
489
|
+
* words in the transcript. Consuming it retires the client's queued row the
|
|
490
|
+
* same way a queue delete does.
|
|
491
|
+
*
|
|
492
|
+
* Deliberately narrow. Anything the free-text field of a card could not have
|
|
493
|
+
* carried, or that a single answer cannot honestly stand in for, falls through
|
|
494
|
+
* to {@link steerOnEnqueuedMessageIfQuestionParked} and supersedes as before:
|
|
495
|
+
* - more than one parked question, so which prompt the text answers is a guess
|
|
496
|
+
* - a message no longer on the queue, which cannot be consumed
|
|
497
|
+
* - attachments, which the tool result has no way to carry
|
|
498
|
+
* - a send that is not a person typing: automated, non-interactive, or
|
|
499
|
+
* echo-suppressed machine signals
|
|
500
|
+
* - empty text, which answers nothing
|
|
501
|
+
* - a slash command, which is an instruction to the assistant rather than prose
|
|
502
|
+
*
|
|
503
|
+
* Returns `true` when a parked question was answered and the message consumed.
|
|
504
|
+
*/
|
|
505
|
+
export function answerParkedQuestionWithEnqueuedMessage(
|
|
506
|
+
conversationId: string,
|
|
507
|
+
enqueuedRequestId: string,
|
|
508
|
+
): boolean {
|
|
509
|
+
const conversation = findConversation(conversationId);
|
|
510
|
+
if (!conversation) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const parked = pendingInteractions
|
|
515
|
+
.getByConversation(conversationId)
|
|
516
|
+
.filter((interaction) => interaction.kind === "question");
|
|
517
|
+
if (parked.length !== 1) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const queued = conversation.queue.findByRequestId(enqueuedRequestId);
|
|
522
|
+
if (!queued || queued.attachments.length > 0) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
if (
|
|
526
|
+
queued.isInteractive === false ||
|
|
527
|
+
queued.metadata?.automated === true ||
|
|
528
|
+
isSuppressedQueuedMessage(queued.metadata)
|
|
529
|
+
) {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const text = queued.content.trim();
|
|
534
|
+
if (text.length === 0 || classifySlash(text) !== "passthrough") {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const outcome = resolvePendingQuestion(parked[0].requestId, {
|
|
539
|
+
kind: "chat_reply",
|
|
540
|
+
text,
|
|
541
|
+
});
|
|
542
|
+
if (outcome.status !== "resolved") {
|
|
543
|
+
log.warn(
|
|
544
|
+
{
|
|
545
|
+
conversationId,
|
|
546
|
+
requestId: parked[0].requestId,
|
|
547
|
+
outcome: outcome.status,
|
|
548
|
+
},
|
|
549
|
+
"Parked question did not resolve from the enqueued message",
|
|
550
|
+
);
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
consumeQueuedMessage(conversation, conversationId, queued);
|
|
555
|
+
log.info(
|
|
556
|
+
{ conversationId, requestId: parked[0].requestId, enqueuedRequestId },
|
|
557
|
+
"Enqueued message answered a parked question",
|
|
558
|
+
);
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
|
|
461
562
|
/**
|
|
462
563
|
* Supersede an open `ask_question` prompt when a new chat message is enqueued
|
|
463
564
|
* for the same conversation.
|
|
@@ -495,7 +596,9 @@ export function steerOnEnqueuedMessageIfQuestionParked(
|
|
|
495
596
|
* gateway request-status sync *before* clearing the prompter-owned
|
|
496
597
|
* confirmations, so a later guardian reply can't match a stale "pending"
|
|
497
598
|
* record and fail with `pending_interaction_not_found`.
|
|
498
|
-
* 2.
|
|
599
|
+
* 2. Answer a parked ask_question with the enqueued message when the message
|
|
600
|
+
* reads as an answer ({@link answerParkedQuestionWithEnqueuedMessage}), and
|
|
601
|
+
* otherwise supersede it by steering to the enqueued message.
|
|
499
602
|
*
|
|
500
603
|
* Order matters: the steer aborts the turn, which denies the prompter's
|
|
501
604
|
* confirmations as a side effect, so the status/notification sync must be
|
|
@@ -508,6 +611,11 @@ export function supersedePendingInteractionsOnEnqueue(
|
|
|
508
611
|
enqueuedRequestId: string,
|
|
509
612
|
): void {
|
|
510
613
|
denyPendingConfirmationsOnSupersession(conversationId);
|
|
614
|
+
if (
|
|
615
|
+
answerParkedQuestionWithEnqueuedMessage(conversationId, enqueuedRequestId)
|
|
616
|
+
) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
511
619
|
steerOnEnqueuedMessageIfQuestionParked(conversationId, enqueuedRequestId);
|
|
512
620
|
}
|
|
513
621
|
|
|
@@ -6,7 +6,7 @@ import { buildPkbReminder } from "./pkb-reminder-builder.js";
|
|
|
6
6
|
// future edit to the BODY text is caught by tests.
|
|
7
7
|
const BASE_REMINDER =
|
|
8
8
|
"<system_reminder>" +
|
|
9
|
-
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark
|
|
9
|
+
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark: corrections (highest priority), plans, decisions, felt moments. A later pass may review this conversation and save what you did not capture, but it is not guaranteed and never runs over scheduled work, so save now anything that must survive." +
|
|
10
10
|
"\nIf you're unsure about something that may live in the workspace — past decisions, prior conversations, files — use `recall` before asking or guessing." +
|
|
11
11
|
"\n</system_reminder>";
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ describe("buildPkbReminder", () => {
|
|
|
19
19
|
const out = buildPkbReminder(["projects/alpha.md"]);
|
|
20
20
|
const expected =
|
|
21
21
|
"<system_reminder>" +
|
|
22
|
-
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark
|
|
22
|
+
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark: corrections (highest priority), plans, decisions, felt moments. A later pass may review this conversation and save what you did not capture, but it is not guaranteed and never runs over scheduled work, so save now anything that must survive." +
|
|
23
23
|
"\nIf you're unsure about something that may live in the workspace — past decisions, prior conversations, files — use `recall` before asking or guessing." +
|
|
24
24
|
"\nBased on the current context, these files look especially relevant:" +
|
|
25
25
|
"\n- projects/alpha.md" +
|
|
@@ -39,7 +39,7 @@ describe("buildPkbReminder", () => {
|
|
|
39
39
|
const out = buildPkbReminder(hints);
|
|
40
40
|
const expected =
|
|
41
41
|
"<system_reminder>" +
|
|
42
|
-
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark
|
|
42
|
+
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark: corrections (highest priority), plans, decisions, felt moments. A later pass may review this conversation and save what you did not capture, but it is not guaranteed and never runs over scheduled work, so save now anything that must survive." +
|
|
43
43
|
"\nIf you're unsure about something that may live in the workspace — past decisions, prior conversations, files — use `recall` before asking or guessing." +
|
|
44
44
|
"\nBased on the current context, these files look especially relevant:" +
|
|
45
45
|
"\n- a.md" +
|
|
@@ -57,6 +57,13 @@ describe("buildPkbReminder", () => {
|
|
|
57
57
|
expect(idxC).toBeGreaterThan(idxB);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test("says later capture is conditional and never covers scheduled work", () => {
|
|
61
|
+
const out = buildPkbReminder([]);
|
|
62
|
+
expect(out).toContain("not guaranteed");
|
|
63
|
+
expect(out).toContain("never runs over scheduled work");
|
|
64
|
+
expect(out).not.toContain("reviews this conversation in the background");
|
|
65
|
+
});
|
|
66
|
+
|
|
60
67
|
test("hints with special chars (< and &) are emitted verbatim (no escaping)", () => {
|
|
61
68
|
const hints = ["weird<name>.md", "foo&bar.md"];
|
|
62
69
|
const out = buildPkbReminder(hints);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const BODY =
|
|
2
|
-
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark
|
|
2
|
+
"\nStay present in this conversation. Use `remember` when something feels worth pausing to mark: corrections (highest priority), plans, decisions, felt moments. A later pass may review this conversation and save what you did not capture, but it is not guaranteed and never runs over scheduled work, so save now anything that must survive." +
|
|
3
3
|
"\nIf you're unsure about something that may live in the workspace — past decisions, prior conversations, files — use `recall` before asking or guessing.";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -134,6 +134,7 @@ export function desktopChromeArguments(
|
|
|
134
134
|
`--class=${DESKTOP_CHROME_WINDOW_CLASS}`,
|
|
135
135
|
"--force-renderer-accessibility",
|
|
136
136
|
"--disable-dev-shm-usage",
|
|
137
|
+
"--disable-features=Prerender2",
|
|
137
138
|
...(shouldRestoreDesktopChromeSession(profileDir)
|
|
138
139
|
? ["--restore-last-session", "--hide-crash-restore-bubble"]
|
|
139
140
|
: []),
|
package/src/home/feed-types.ts
CHANGED
|
@@ -24,7 +24,10 @@ import { getConfig } from "../config/loader.js";
|
|
|
24
24
|
import { resolvePlatformAssistantId } from "../config/platform-identity.js";
|
|
25
25
|
import { ipcRegisterWebhookRoute } from "../ipc/gateway-client.js";
|
|
26
26
|
import { credentialKey } from "../security/credential-key.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getSecureKeyAsync,
|
|
29
|
+
getSecureKeyResultAsync,
|
|
30
|
+
} from "../security/secure-keys.js";
|
|
28
31
|
import { getLogger } from "../util/logger.js";
|
|
29
32
|
import { resolveClaimedPodWebhookUrl } from "./pod-webhook-claim.js";
|
|
30
33
|
import {
|
|
@@ -39,17 +42,23 @@ export interface PlatformCallbackRegistrationContext {
|
|
|
39
42
|
isPlatform: boolean;
|
|
40
43
|
platformBaseUrl: string;
|
|
41
44
|
assistantId: string;
|
|
42
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Whether a managed assistant API key is available: `null` when the
|
|
47
|
+
* credential store could not be read, so its absence is not evidence that
|
|
48
|
+
* no key is stored. A client deciding whether to provision one must not
|
|
49
|
+
* read `null` as "missing".
|
|
50
|
+
*/
|
|
51
|
+
hasAssistantApiKey: boolean | null;
|
|
43
52
|
authHeader: string | null;
|
|
44
53
|
enabled: boolean;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
export async function resolvePlatformCallbackRegistrationContext(): Promise<PlatformCallbackRegistrationContext> {
|
|
48
57
|
const platform = getIsPlatform();
|
|
49
|
-
const [storedBaseUrlRaw,
|
|
58
|
+
const [storedBaseUrlRaw, storedAssistantApiKey, assistantId] =
|
|
50
59
|
await Promise.all([
|
|
51
60
|
getSecureKeyAsync(credentialKey("vellum", "platform_base_url")),
|
|
52
|
-
|
|
61
|
+
getSecureKeyResultAsync(credentialKey("vellum", "assistant_api_key")),
|
|
53
62
|
resolvePlatformAssistantId(),
|
|
54
63
|
]);
|
|
55
64
|
|
|
@@ -60,16 +69,20 @@ export async function resolvePlatformCallbackRegistrationContext(): Promise<Plat
|
|
|
60
69
|
);
|
|
61
70
|
const envAssistantCredential = process.env.ASSISTANT_API_KEY?.trim();
|
|
62
71
|
const assistantCredential =
|
|
63
|
-
|
|
72
|
+
storedAssistantApiKey.value?.trim() || envAssistantCredential || undefined;
|
|
64
73
|
const authHeader = assistantCredential
|
|
65
74
|
? `Api-Key ${assistantCredential}`
|
|
66
75
|
: null;
|
|
76
|
+
const hasAssistantApiKey =
|
|
77
|
+
!assistantCredential && storedAssistantApiKey.unreachable
|
|
78
|
+
? null
|
|
79
|
+
: !!assistantCredential;
|
|
67
80
|
|
|
68
81
|
return {
|
|
69
82
|
isPlatform: platform,
|
|
70
83
|
platformBaseUrl,
|
|
71
84
|
assistantId,
|
|
72
|
-
hasAssistantApiKey
|
|
85
|
+
hasAssistantApiKey,
|
|
73
86
|
authHeader,
|
|
74
87
|
// Enabled when we have enough context to register callback routes.
|
|
75
88
|
// Does NOT require IS_PLATFORM — self-hosted assistants with stored
|
|
@@ -16,6 +16,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
16
16
|
|
|
17
17
|
const recordCalls: Array<[string, number]> = [];
|
|
18
18
|
const publishCalls: string[] = [];
|
|
19
|
+
const listPublishCalls: Array<[string, string | string[]]> = [];
|
|
19
20
|
|
|
20
21
|
mock.module("../../../persistence/conversation-crud.js", () => ({
|
|
21
22
|
recordConversationPersistedSeq: (id: string, seq: number) => {
|
|
@@ -27,6 +28,12 @@ mock.module("../../../runtime/sync/resource-sync-events.js", () => ({
|
|
|
27
28
|
publishConversationMessagesChanged: (id: string) => {
|
|
28
29
|
publishCalls.push(id);
|
|
29
30
|
},
|
|
31
|
+
publishConversationListAndMetadataChanged: (
|
|
32
|
+
reason: string,
|
|
33
|
+
ids: string | string[],
|
|
34
|
+
) => {
|
|
35
|
+
listPublishCalls.push([reason, ids]);
|
|
36
|
+
},
|
|
30
37
|
}));
|
|
31
38
|
|
|
32
39
|
import type { AssistantEventEnvelope } from "../../../api/index.js";
|
|
@@ -40,9 +47,13 @@ import {
|
|
|
40
47
|
_resetStreamStateForTesting,
|
|
41
48
|
stampAndBuffer,
|
|
42
49
|
} from "../../../runtime/assistant-stream-state.js";
|
|
43
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
NOTIFY_CONVERSATION_LIST_CHANGED_IPC_METHOD,
|
|
52
|
+
NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD,
|
|
53
|
+
} from "../../../runtime/sync/worker-daemon-notify.js";
|
|
44
54
|
import {
|
|
45
55
|
CONVERSATION_SYNC_IPC_METHODS,
|
|
56
|
+
handleNotifyConversationListChanged,
|
|
46
57
|
handleNotifyConversationPersisted,
|
|
47
58
|
} from "../conversation-sync-ipc-routes.js";
|
|
48
59
|
|
|
@@ -176,3 +187,40 @@ describe("conversation-sync IPC route", () => {
|
|
|
176
187
|
).toBe(false);
|
|
177
188
|
});
|
|
178
189
|
});
|
|
190
|
+
|
|
191
|
+
describe("conversation-list-changed IPC route", () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
listPublishCalls.length = 0;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("republishes the worker's list invalidation on the daemon's hub", () => {
|
|
197
|
+
const result = handleNotifyConversationListChanged({
|
|
198
|
+
body: { reason: "reordered", conversationIds: ["conv-1"] },
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result).toEqual({ ok: true });
|
|
202
|
+
expect(listPublishCalls).toEqual([["reordered", ["conv-1"]]]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("rejects an unknown reason and an empty id list", () => {
|
|
206
|
+
expect(() =>
|
|
207
|
+
handleNotifyConversationListChanged({
|
|
208
|
+
body: { reason: "resurfaced", conversationIds: ["conv-1"] },
|
|
209
|
+
}),
|
|
210
|
+
).toThrow();
|
|
211
|
+
expect(() =>
|
|
212
|
+
handleNotifyConversationListChanged({
|
|
213
|
+
body: { reason: "reordered", conversationIds: [] },
|
|
214
|
+
}),
|
|
215
|
+
).toThrow();
|
|
216
|
+
expect(listPublishCalls).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("is reachable on the IPC surface under the shared method name", () => {
|
|
220
|
+
expect(
|
|
221
|
+
typeof CONVERSATION_SYNC_IPC_METHODS[
|
|
222
|
+
NOTIFY_CONVERSATION_LIST_CHANGED_IPC_METHOD
|
|
223
|
+
],
|
|
224
|
+
).toBe("function");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -29,13 +29,30 @@ import { getInflightFlushedContentSeq } from "../../daemon/inflight-turn-registr
|
|
|
29
29
|
import { recordConversationPersistedSeq } from "../../persistence/conversation-crud.js";
|
|
30
30
|
import { getCurrentSeq } from "../../runtime/assistant-stream-state.js";
|
|
31
31
|
import type { RouteHandlerArgs } from "../../runtime/routes/types.js";
|
|
32
|
-
import {
|
|
33
|
-
|
|
32
|
+
import {
|
|
33
|
+
publishConversationListAndMetadataChanged,
|
|
34
|
+
publishConversationMessagesChanged,
|
|
35
|
+
} from "../../runtime/sync/resource-sync-events.js";
|
|
36
|
+
import {
|
|
37
|
+
NOTIFY_CONVERSATION_LIST_CHANGED_IPC_METHOD,
|
|
38
|
+
NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD,
|
|
39
|
+
} from "../../runtime/sync/worker-daemon-notify.js";
|
|
34
40
|
|
|
35
41
|
const NotifyConversationPersistedParamsSchema = z.object({
|
|
36
42
|
conversationId: z.string().min(1),
|
|
37
43
|
});
|
|
38
44
|
|
|
45
|
+
const NotifyConversationListChangedParamsSchema = z.object({
|
|
46
|
+
reason: z.enum([
|
|
47
|
+
"created",
|
|
48
|
+
"renamed",
|
|
49
|
+
"deleted",
|
|
50
|
+
"reordered",
|
|
51
|
+
"seen_changed",
|
|
52
|
+
]),
|
|
53
|
+
conversationIds: z.array(z.string().min(1)).min(1),
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
/**
|
|
40
57
|
* Record the daemon-issued snapshot anchor for a worker-persisted turn and
|
|
41
58
|
* republish its messages-changed invalidation to the daemon's subscribers.
|
|
@@ -61,6 +78,23 @@ export function handleNotifyConversationPersisted({
|
|
|
61
78
|
return { ok: true };
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Republish a worker's conversation-list-and-metadata invalidation on the
|
|
83
|
+
* daemon's hub, where the SSE subscribers live.
|
|
84
|
+
*
|
|
85
|
+
* A worker turn that changes which rows belong in the list (clearing
|
|
86
|
+
* `archived_at` on a Done conversation it woke) has to reach connected
|
|
87
|
+
* sidebars, and a publish in the worker reaches none of them.
|
|
88
|
+
*/
|
|
89
|
+
export function handleNotifyConversationListChanged({
|
|
90
|
+
body = {},
|
|
91
|
+
}: RouteHandlerArgs) {
|
|
92
|
+
const { reason, conversationIds } =
|
|
93
|
+
NotifyConversationListChangedParamsSchema.parse(body);
|
|
94
|
+
publishConversationListAndMetadataChanged(reason, conversationIds);
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
64
98
|
/**
|
|
65
99
|
* IPC-only conversation-sync methods, keyed by operationId. Registered directly
|
|
66
100
|
* on the assistant IPC server (see `assistant-server.ts`).
|
|
@@ -70,4 +104,6 @@ export const CONVERSATION_SYNC_IPC_METHODS: Record<
|
|
|
70
104
|
(args: RouteHandlerArgs) => unknown
|
|
71
105
|
> = {
|
|
72
106
|
[NOTIFY_CONVERSATION_PERSISTED_IPC_METHOD]: handleNotifyConversationPersisted,
|
|
107
|
+
[NOTIFY_CONVERSATION_LIST_CHANGED_IPC_METHOD]:
|
|
108
|
+
handleNotifyConversationListChanged,
|
|
73
109
|
};
|
|
@@ -238,6 +238,33 @@ function assistantDeltaTexts(frames: LiveVoiceServerFrame[]): string[] {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
describe("LiveVoiceSession assistant turn", () => {
|
|
241
|
+
test.each([true, false])(
|
|
242
|
+
"passes current screen sharing state %p to the desktop voice bridge",
|
|
243
|
+
async (screenSharing) => {
|
|
244
|
+
const startVoiceTurn = mock(async (_options: VoiceTurnOptions) => ({
|
|
245
|
+
turnId: "bridge-turn-1",
|
|
246
|
+
abort: mock(),
|
|
247
|
+
}));
|
|
248
|
+
const { session, transcriber } = createSessionHarness({
|
|
249
|
+
startFrame: { ...START_FRAME, client: "macos" },
|
|
250
|
+
startVoiceTurn,
|
|
251
|
+
});
|
|
252
|
+
await session.start();
|
|
253
|
+
await session.handleClientFrame({
|
|
254
|
+
type: "update_config",
|
|
255
|
+
screenSharing: true,
|
|
256
|
+
});
|
|
257
|
+
await session.handleClientFrame({ type: "update_config", screenSharing });
|
|
258
|
+
transcriber.emit({ type: "final", text: "Show me the speed control" });
|
|
259
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
260
|
+
expect(startVoiceTurn.mock.calls[0]?.[0]).toMatchObject({
|
|
261
|
+
macosDesktopSession: true,
|
|
262
|
+
screenSharing,
|
|
263
|
+
});
|
|
264
|
+
await session.close("client_end");
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
241
268
|
test("runs final transcripts through the voice bridge and forwards ordered assistant events", async () => {
|
|
242
269
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
243
270
|
options.callbacks?.assistant_text_delta?.({
|
|
@@ -77,6 +77,28 @@ describe("parseLiveVoiceClientTextFrame", () => {
|
|
|
77
77
|
expect(result).toEqual({ ok: true, frame });
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
test.each([true, false])(
|
|
81
|
+
"parses screen sharing state %p",
|
|
82
|
+
(screenSharing) => {
|
|
83
|
+
const frame = { type: "update_config", screenSharing } as const;
|
|
84
|
+
expect(parseLiveVoiceClientTextFrame(JSON.stringify(frame))).toEqual({
|
|
85
|
+
ok: true,
|
|
86
|
+
frame,
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
test("rejects malformed screen sharing state", () => {
|
|
92
|
+
expect(
|
|
93
|
+
parseLiveVoiceClientTextFrame(
|
|
94
|
+
JSON.stringify({
|
|
95
|
+
type: "update_config",
|
|
96
|
+
screenSharing: "true",
|
|
97
|
+
}),
|
|
98
|
+
).ok,
|
|
99
|
+
).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
80
102
|
test("parses an update_config frame with a single field, omitting the other", () => {
|
|
81
103
|
const result = parseLiveVoiceClientTextFrame(
|
|
82
104
|
JSON.stringify({ type: "update_config", silenceThresholdMs: 900 }),
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import {
|
|
18
18
|
askTypesafeNoul,
|
|
19
19
|
resolveTypesafeProvider,
|
|
20
|
-
} from "../
|
|
20
|
+
} from "../providers/jev/ask.js";
|
|
21
21
|
import type { Provider } from "../providers/types.js";
|
|
22
22
|
|
|
23
23
|
export const VOICE_CONTINUATION_JUDGE_CALL_SITE = "voiceContinuationJudge";
|
|
@@ -1217,6 +1217,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
1217
1217
|
private releaseModeSessionResidency?: () => void;
|
|
1218
1218
|
private cameraModeSessions?: CameraModeSessionProducer;
|
|
1219
1219
|
private sightFrameSequence: Promise<void> = Promise.resolve();
|
|
1220
|
+
private screenSharing = false;
|
|
1220
1221
|
/**
|
|
1221
1222
|
* Mirrors phase changes to the iOS Live Activity through the platform, for
|
|
1222
1223
|
* the case the client cannot cover: an app backgrounded long enough for iOS
|
|
@@ -2084,13 +2085,13 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
2084
2085
|
}
|
|
2085
2086
|
|
|
2086
2087
|
/**
|
|
2087
|
-
* Apply
|
|
2088
|
-
*
|
|
2089
|
-
* sensitivity") without reconnecting. Each field is optional and independent;
|
|
2090
|
-
* changes take effect from the next utterance. A no-op on manual (non-
|
|
2091
|
-
* server_vad) sessions, which have no turn detector.
|
|
2088
|
+
* Apply screen-sharing state and turn-detection tuning independently.
|
|
2089
|
+
* Changes take effect from the next utterance.
|
|
2092
2090
|
*/
|
|
2093
2091
|
private applyConfigUpdate(frame: LiveVoiceClientUpdateConfigFrame): void {
|
|
2092
|
+
if (frame.screenSharing !== undefined) {
|
|
2093
|
+
this.screenSharing = frame.screenSharing;
|
|
2094
|
+
}
|
|
2094
2095
|
if (frame.silenceThresholdMs !== undefined) {
|
|
2095
2096
|
this.turnDetector?.setSilenceThresholdMs(frame.silenceThresholdMs);
|
|
2096
2097
|
this.silenceThresholdMs = frame.silenceThresholdMs;
|
|
@@ -6602,7 +6603,7 @@ export class LiveVoiceSession implements LiveVoiceSessionContract {
|
|
|
6602
6603
|
userMessageInterface: "macos",
|
|
6603
6604
|
assistantMessageInterface: "macos",
|
|
6604
6605
|
...(this.context.startFrame.client === "macos"
|
|
6605
|
-
? { macosDesktopSession: true }
|
|
6606
|
+
? { macosDesktopSession: true, screenSharing: this.screenSharing }
|
|
6606
6607
|
: {}),
|
|
6607
6608
|
voiceTelemetry: {
|
|
6608
6609
|
sessionId: this.context.sessionId,
|
|
@@ -276,16 +276,16 @@ export interface LiveVoiceClientEndFrame {
|
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
/**
|
|
279
|
-
* Mid-session
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* optional and independently applied; the same bounds as the start frame apply.
|
|
283
|
-
* Only meaningful for `server_vad` sessions.
|
|
279
|
+
* Mid-session screen-sharing state and turn-detection tuning. Each field is
|
|
280
|
+
* optional and independently applied. Tuning uses the start frame's bounds
|
|
281
|
+
* and affects `server_vad` sessions.
|
|
284
282
|
*/
|
|
285
283
|
export interface LiveVoiceClientUpdateConfigFrame {
|
|
286
284
|
readonly type: "update_config";
|
|
287
285
|
readonly silenceThresholdMs?: number;
|
|
288
286
|
readonly bargeInMinSpeechMs?: number;
|
|
287
|
+
/** A shared desktop surface is available for annotation. */
|
|
288
|
+
readonly screenSharing?: boolean;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
/**
|
|
@@ -1279,6 +1279,14 @@ function validateSightFrameTiming(
|
|
|
1279
1279
|
function validateUpdateConfigFrame(
|
|
1280
1280
|
value: Record<string, unknown>,
|
|
1281
1281
|
): LiveVoiceParseResult<LiveVoiceClientUpdateConfigFrame> {
|
|
1282
|
+
if ("screenSharing" in value && typeof value.screenSharing !== "boolean") {
|
|
1283
|
+
return protocolError(
|
|
1284
|
+
"invalid_field",
|
|
1285
|
+
"update_config field screenSharing must be a boolean",
|
|
1286
|
+
"screenSharing",
|
|
1287
|
+
"update_config",
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1282
1290
|
if (
|
|
1283
1291
|
"silenceThresholdMs" in value &&
|
|
1284
1292
|
!isIntInRange(
|
|
@@ -1315,6 +1323,9 @@ function validateUpdateConfigFrame(
|
|
|
1315
1323
|
ok: true,
|
|
1316
1324
|
frame: {
|
|
1317
1325
|
type: "update_config",
|
|
1326
|
+
...(typeof value.screenSharing === "boolean"
|
|
1327
|
+
? { screenSharing: value.screenSharing }
|
|
1328
|
+
: {}),
|
|
1318
1329
|
...(typeof value.silenceThresholdMs === "number"
|
|
1319
1330
|
? { silenceThresholdMs: value.silenceThresholdMs }
|
|
1320
1331
|
: {}),
|