@vellumai/assistant 0.4.51 → 0.4.53
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 +2 -2
- package/docs/architecture/keychain-broker.md +19 -6
- package/docs/architecture/memory.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +3 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/asset-materialize-tool.test.ts +0 -1
- package/src/__tests__/asset-search-tool.test.ts +0 -1
- package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
- package/src/__tests__/attachments-store.test.ts +0 -1
- package/src/__tests__/avatar-e2e.test.ts +6 -1
- package/src/__tests__/browser-fill-credential.test.ts +3 -0
- package/src/__tests__/btw-routes.test.ts +39 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +1 -0
- package/src/__tests__/call-routes-http.test.ts +1 -2
- package/src/__tests__/canonical-guardian-store.test.ts +33 -2
- package/src/__tests__/channel-readiness-routes.test.ts +1 -0
- package/src/__tests__/channel-readiness-service.test.ts +1 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +6 -2
- package/src/__tests__/claude-code-tool-profiles.test.ts +7 -2
- package/src/__tests__/config-loader-backfill.test.ts +1 -2
- package/src/__tests__/config-schema.test.ts +6 -37
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
- package/src/__tests__/credential-broker-server-use.test.ts +16 -16
- package/src/__tests__/credential-security-invariants.test.ts +14 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -2
- package/src/__tests__/host-shell-tool.test.ts +0 -1
- package/src/__tests__/http-user-message-parity.test.ts +19 -0
- package/src/__tests__/list-messages-attachments.test.ts +0 -1
- package/src/__tests__/log-export-workspace.test.ts +233 -0
- package/src/__tests__/managed-proxy-context.test.ts +1 -1
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/media-generate-image.test.ts +7 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/memory-regressions.test.ts +0 -1
- 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__/notification-schedule-dedup.test.ts +237 -0
- package/src/__tests__/oauth-cli.test.ts +1 -10
- package/src/__tests__/oauth-store.test.ts +3 -5
- package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
- package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
- package/src/__tests__/onboarding-template-contract.test.ts +1 -2
- package/src/__tests__/pricing.test.ts +0 -11
- package/src/__tests__/provider-commit-message-generator.test.ts +21 -14
- package/src/__tests__/provider-fail-open-selection.test.ts +9 -8
- package/src/__tests__/provider-managed-proxy-integration.test.ts +27 -24
- package/src/__tests__/provider-registry-ollama.test.ts +8 -2
- package/src/__tests__/recording-handler.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +0 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
- package/src/__tests__/runtime-events-sse.test.ts +0 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +4 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -1
- package/src/__tests__/secret-scanner-executor.test.ts +0 -1
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/session-abort-tool-results.test.ts +3 -1
- package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
- package/src/__tests__/session-agent-loop.test.ts +2 -2
- package/src/__tests__/session-confirmation-signals.test.ts +3 -1
- package/src/__tests__/session-error.test.ts +5 -4
- package/src/__tests__/session-history-web-search.test.ts +34 -9
- package/src/__tests__/session-pre-run-repair.test.ts +3 -1
- package/src/__tests__/session-provider-retry-repair.test.ts +31 -26
- package/src/__tests__/session-queue.test.ts +3 -1
- package/src/__tests__/session-runtime-assembly.test.ts +118 -0
- package/src/__tests__/session-slash-known.test.ts +31 -13
- package/src/__tests__/session-slash-queue.test.ts +3 -1
- package/src/__tests__/session-slash-unknown.test.ts +3 -1
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
- package/src/__tests__/session-workspace-injection.test.ts +3 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -1
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/skillssh-registry.test.ts +21 -0
- package/src/__tests__/slack-share-routes.test.ts +1 -1
- package/src/__tests__/swarm-recursion.test.ts +5 -1
- package/src/__tests__/swarm-session-integration.test.ts +25 -14
- package/src/__tests__/swarm-tool.test.ts +5 -2
- package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/trust-store.test.ts +5 -1
- package/src/__tests__/twilio-routes.test.ts +2 -2
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-quality.test.ts +2 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/web-search.test.ts +1 -1
- package/src/agent/loop.ts +17 -1
- package/src/bundler/app-bundler.ts +40 -24
- package/src/calls/call-controller.ts +16 -0
- package/src/calls/relay-server.ts +29 -13
- package/src/calls/voice-control-protocol.ts +1 -0
- package/src/calls/voice-quality.ts +1 -1
- package/src/calls/voice-session-bridge.ts +9 -3
- package/src/channels/types.ts +16 -0
- package/src/cli/commands/bash.ts +173 -0
- package/src/cli/commands/doctor.ts +5 -23
- package/src/cli/commands/oauth/connections.ts +4 -2
- package/src/cli/commands/oauth/providers.ts +1 -13
- package/src/cli/program.ts +2 -0
- package/src/cli/reference.ts +1 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -1
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
- package/src/config/bundled-skills/messaging/TOOLS.json +41 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +2 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +7 -135
- package/src/config/schema.ts +0 -6
- package/src/config/schemas/channels.ts +1 -0
- package/src/config/schemas/elevenlabs.ts +2 -2
- package/src/contacts/contact-store.ts +21 -25
- package/src/contacts/contacts-write.ts +6 -6
- package/src/contacts/types.ts +2 -0
- package/src/context/token-estimator.ts +35 -2
- package/src/context/window-manager.ts +16 -2
- package/src/daemon/config-watcher.ts +24 -6
- package/src/daemon/context-overflow-reducer.ts +13 -2
- package/src/daemon/handlers/config-ingress.ts +25 -8
- package/src/daemon/handlers/config-model.ts +21 -15
- package/src/daemon/handlers/config-telegram.ts +18 -6
- package/src/daemon/handlers/dictation.ts +0 -429
- package/src/daemon/handlers/skills.ts +1 -200
- package/src/daemon/lifecycle.ts +8 -5
- package/src/daemon/message-types/contacts.ts +2 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/sessions.ts +2 -0
- package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
- package/src/daemon/server.ts +23 -2
- package/src/daemon/session-agent-loop-handlers.ts +1 -1
- package/src/daemon/session-agent-loop.ts +27 -79
- package/src/daemon/session-error.ts +5 -4
- package/src/daemon/session-process.ts +17 -10
- package/src/daemon/session-runtime-assembly.ts +50 -0
- package/src/daemon/session-slash.ts +32 -20
- package/src/daemon/session.ts +1 -0
- package/src/events/domain-events.ts +1 -0
- package/src/media/app-icon-generator.ts +2 -1
- package/src/media/avatar-router.ts +3 -2
- package/src/memory/canonical-guardian-store.ts +25 -3
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.ts +25 -16
- package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
- package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
- package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/retriever.test.ts +19 -12
- package/src/memory/schema/contacts.ts +2 -2
- package/src/memory/schema/oauth.ts +0 -1
- package/src/oauth/byo-connection.ts +55 -49
- package/src/oauth/connect-orchestrator.ts +5 -3
- package/src/oauth/connect-types.ts +9 -2
- package/src/oauth/manual-token-connection.ts +9 -7
- package/src/oauth/oauth-store.ts +2 -8
- package/src/oauth/provider-behaviors.ts +10 -0
- package/src/oauth/seed-providers.ts +13 -5
- package/src/permissions/checker.ts +20 -1
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
- package/src/prompts/system-prompt.ts +2 -11
- package/src/prompts/templates/BOOTSTRAP.md +1 -3
- package/src/providers/anthropic/client.ts +16 -8
- package/src/providers/managed-proxy/constants.ts +1 -1
- package/src/providers/registry.ts +21 -15
- package/src/providers/types.ts +1 -1
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-invite-transports/telegram.ts +12 -6
- package/src/runtime/channel-retry-sweep.ts +6 -0
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/middleware/error-handler.ts +1 -2
- package/src/runtime/routes/app-management-routes.ts +1 -0
- package/src/runtime/routes/btw-routes.ts +20 -1
- package/src/runtime/routes/conversation-routes.ts +32 -13
- package/src/runtime/routes/inbound-message-handler.ts +10 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
- package/src/runtime/routes/integrations/slack/share.ts +5 -5
- package/src/runtime/routes/log-export-routes.ts +122 -10
- package/src/runtime/routes/session-query-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +53 -0
- package/src/runtime/routes/workspace-routes.ts +3 -0
- package/src/runtime/verification-templates.ts +1 -1
- package/src/security/oauth2.ts +4 -4
- package/src/security/secure-keys.ts +24 -3
- package/src/security/token-manager.ts +7 -8
- package/src/signals/bash.ts +157 -0
- package/src/skills/skillssh-registry.ts +6 -1
- package/src/swarm/backend-claude-code.ts +6 -6
- package/src/swarm/worker-backend.ts +1 -1
- package/src/swarm/worker-runner.ts +1 -1
- package/src/telegram/bot-username.ts +11 -0
- package/src/tools/claude-code/claude-code.ts +4 -4
- package/src/tools/credentials/broker.ts +7 -5
- package/src/tools/credentials/vault.ts +3 -2
- package/src/tools/network/__tests__/web-search.test.ts +18 -86
- package/src/tools/network/web-search.ts +9 -15
- package/src/util/platform.ts +7 -1
- package/src/util/pricing.ts +0 -1
- package/src/workspace/provider-commit-message-generator.ts +10 -6
package/src/config/schema.ts
CHANGED
|
@@ -232,12 +232,6 @@ export const AssistantConfigSchema = z
|
|
|
232
232
|
imageGenModel: z
|
|
233
233
|
.string({ error: "imageGenModel must be a string" })
|
|
234
234
|
.default("gemini-2.5-flash-image"),
|
|
235
|
-
apiKeys: z
|
|
236
|
-
.record(
|
|
237
|
-
z.string(),
|
|
238
|
-
z.string({ error: "Each apiKeys value must be a string" }),
|
|
239
|
-
)
|
|
240
|
-
.default({} as Record<string, string>),
|
|
241
235
|
webSearchProvider: z
|
|
242
236
|
.enum(VALID_WEB_SEARCH_PROVIDERS, {
|
|
243
237
|
error: `webSearchProvider must be one of: ${VALID_WEB_SEARCH_PROVIDERS.join(
|
|
@@ -34,6 +34,7 @@ export const WhatsAppConfigSchema = z.object({
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
export const TelegramConfigSchema = z.object({
|
|
37
|
+
botId: z.string({ error: "telegram.botId must be a string" }).default(""),
|
|
37
38
|
botUsername: z
|
|
38
39
|
.string({ error: "telegram.botUsername must be a string" })
|
|
39
40
|
.default(""),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
// Default ElevenLabs voice — "
|
|
3
|
+
// Default ElevenLabs voice — "Amelia" (expressive, enthusiastic, British English).
|
|
4
4
|
// Used by both in-app TTS and phone calls (via Twilio ConversationRelay).
|
|
5
5
|
// Mirrored in: clients/macos/.../OpenAIVoiceService.swift (defaultVoiceId)
|
|
6
|
-
export const DEFAULT_ELEVENLABS_VOICE_ID = "
|
|
6
|
+
export const DEFAULT_ELEVENLABS_VOICE_ID = "ZF6FPAbjXT4488VcRRnw";
|
|
7
7
|
|
|
8
8
|
export const ElevenLabsConfigSchema = z.object({
|
|
9
9
|
voiceId: z
|
|
@@ -34,8 +34,8 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
|
|
|
34
34
|
id: row.id,
|
|
35
35
|
displayName: row.displayName,
|
|
36
36
|
notes: row.notes,
|
|
37
|
-
lastInteraction:
|
|
38
|
-
interactionCount:
|
|
37
|
+
lastInteraction: null,
|
|
38
|
+
interactionCount: 0,
|
|
39
39
|
createdAt: row.createdAt,
|
|
40
40
|
updatedAt: row.updatedAt,
|
|
41
41
|
role: row.role as Contact["role"],
|
|
@@ -63,6 +63,8 @@ function parseChannel(
|
|
|
63
63
|
revokedReason: row.revokedReason,
|
|
64
64
|
blockedReason: row.blockedReason,
|
|
65
65
|
lastSeenAt: row.lastSeenAt,
|
|
66
|
+
interactionCount: row.interactionCount,
|
|
67
|
+
lastInteraction: row.lastInteraction,
|
|
66
68
|
updatedAt: row.updatedAt,
|
|
67
69
|
createdAt: row.createdAt,
|
|
68
70
|
};
|
|
@@ -80,7 +82,15 @@ function getChannelsForContact(contactId: string): ContactChannel[] {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
function withChannels(contact: Contact): ContactWithChannels {
|
|
83
|
-
|
|
85
|
+
const channels = getChannelsForContact(contact.id);
|
|
86
|
+
const interactionCount = channels.reduce(
|
|
87
|
+
(sum, ch) => sum + ch.interactionCount,
|
|
88
|
+
0,
|
|
89
|
+
);
|
|
90
|
+
const lastInteraction =
|
|
91
|
+
channels.reduce((max, ch) => Math.max(max, ch.lastInteraction ?? 0), 0) ||
|
|
92
|
+
null;
|
|
93
|
+
return { ...contact, interactionCount, lastInteraction, channels };
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
// ── Channel data type for syncChannels ───────────────────────────────
|
|
@@ -235,8 +245,6 @@ export function upsertContact(params: {
|
|
|
235
245
|
id: contactId,
|
|
236
246
|
displayName: params.displayName,
|
|
237
247
|
notes: params.notes ?? null,
|
|
238
|
-
lastInteraction: null,
|
|
239
|
-
interactionCount: 0,
|
|
240
248
|
role: params.role ?? "contact",
|
|
241
249
|
contactType: params.contactType ?? "human",
|
|
242
250
|
principalId: params.principalId ?? null,
|
|
@@ -488,7 +496,7 @@ export function searchContacts(params: {
|
|
|
488
496
|
.from(contacts)
|
|
489
497
|
.innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
|
|
490
498
|
.where(whereClause)
|
|
491
|
-
.orderBy(desc(contacts.updatedAt)
|
|
499
|
+
.orderBy(desc(contacts.updatedAt))
|
|
492
500
|
.all();
|
|
493
501
|
|
|
494
502
|
const contactIds = [...new Set(rows.map((r) => r.contactId))];
|
|
@@ -509,7 +517,7 @@ export function searchContacts(params: {
|
|
|
509
517
|
.select()
|
|
510
518
|
.from(contacts)
|
|
511
519
|
.where(whereClause)
|
|
512
|
-
.orderBy(desc(contacts.updatedAt)
|
|
520
|
+
.orderBy(desc(contacts.updatedAt))
|
|
513
521
|
.limit(limit)
|
|
514
522
|
.all();
|
|
515
523
|
|
|
@@ -531,11 +539,7 @@ export function listContacts(
|
|
|
531
539
|
.select()
|
|
532
540
|
.from(contacts)
|
|
533
541
|
.where(conditions.length === 1 ? conditions[0] : and(...conditions))
|
|
534
|
-
.orderBy(
|
|
535
|
-
sql`${contacts.role} = 'guardian' DESC`,
|
|
536
|
-
desc(contacts.updatedAt),
|
|
537
|
-
desc(contacts.lastInteraction),
|
|
538
|
-
)
|
|
542
|
+
.orderBy(sql`${contacts.role} = 'guardian' DESC`, desc(contacts.updatedAt))
|
|
539
543
|
.limit(effectiveLimit)
|
|
540
544
|
.all();
|
|
541
545
|
return rows.map((r) => withChannels(parseContact(r)));
|
|
@@ -571,16 +575,8 @@ export function mergeContacts(
|
|
|
571
575
|
.get();
|
|
572
576
|
if (!merge) throw new Error(`Contact "${mergeId}" not found`);
|
|
573
577
|
|
|
574
|
-
// Resolve merged field values — pick the better/more recent value
|
|
575
|
-
const mergedInteractionCount =
|
|
576
|
-
keep.interactionCount + merge.interactionCount;
|
|
577
|
-
const mergedLastInteraction =
|
|
578
|
-
Math.max(keep.lastInteraction ?? 0, merge.lastInteraction ?? 0) || null;
|
|
579
|
-
|
|
580
578
|
tx.update(contacts)
|
|
581
579
|
.set({
|
|
582
|
-
interactionCount: mergedInteractionCount,
|
|
583
|
-
lastInteraction: mergedLastInteraction,
|
|
584
580
|
notes: [keep.notes, merge.notes].filter(Boolean).join("\n") || null,
|
|
585
581
|
updatedAt: now,
|
|
586
582
|
})
|
|
@@ -934,19 +930,19 @@ export function updateChannelLastSeenById(channelId: string): void {
|
|
|
934
930
|
}
|
|
935
931
|
|
|
936
932
|
/**
|
|
937
|
-
* Atomically increment interactionCount and set lastInteraction on a contact.
|
|
933
|
+
* Atomically increment interactionCount and set lastInteraction on a contact channel.
|
|
938
934
|
* Optimized for the hot path — single UPDATE with no prior SELECT.
|
|
939
935
|
*/
|
|
940
|
-
export function
|
|
936
|
+
export function updateChannelInteraction(channelId: string): void {
|
|
941
937
|
const db = getDb();
|
|
942
938
|
const now = Date.now();
|
|
943
|
-
db.update(
|
|
939
|
+
db.update(contactChannels)
|
|
944
940
|
.set({
|
|
945
941
|
lastInteraction: now,
|
|
946
|
-
interactionCount: sql`${
|
|
942
|
+
interactionCount: sql`${contactChannels.interactionCount} + 1`,
|
|
947
943
|
updatedAt: now,
|
|
948
944
|
})
|
|
949
|
-
.where(eq(
|
|
945
|
+
.where(eq(contactChannels.id, channelId))
|
|
950
946
|
.run();
|
|
951
947
|
}
|
|
952
948
|
|
|
@@ -16,9 +16,9 @@ import {
|
|
|
16
16
|
findGuardianForChannel,
|
|
17
17
|
getChannelById,
|
|
18
18
|
getContactInternal,
|
|
19
|
+
updateChannelInteraction,
|
|
19
20
|
updateChannelLastSeenById,
|
|
20
21
|
updateChannelStatus,
|
|
21
|
-
updateContactInteraction,
|
|
22
22
|
upsertContact,
|
|
23
23
|
} from "./contact-store.js";
|
|
24
24
|
import type {
|
|
@@ -287,13 +287,13 @@ export function touchChannelLastSeen(channelId: string): void {
|
|
|
287
287
|
}
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
290
|
+
* Track an interaction on the specific channel that received it.
|
|
291
|
+
* Swallows errors to avoid disrupting the inbound message hot path.
|
|
292
292
|
*/
|
|
293
|
-
export function touchContactInteraction(
|
|
293
|
+
export function touchContactInteraction(channelId: string): void {
|
|
294
294
|
try {
|
|
295
|
-
|
|
295
|
+
updateChannelInteraction(channelId);
|
|
296
296
|
} catch (err) {
|
|
297
|
-
log.warn({ err }, "Failed to update
|
|
297
|
+
log.warn({ err }, "Failed to update channel interaction stats");
|
|
298
298
|
}
|
|
299
299
|
}
|
package/src/contacts/types.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ContentBlock,
|
|
3
|
+
Message,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
} from "../providers/types.js";
|
|
2
6
|
import { parseImageDimensions } from "./image-dimensions.js";
|
|
3
7
|
|
|
4
8
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -29,8 +33,17 @@ const ANTHROPIC_IMAGE_MAX_TOKENS = Math.ceil(
|
|
|
29
33
|
const ANTHROPIC_PDF_TOKENS_PER_BYTE = 0.016;
|
|
30
34
|
const ANTHROPIC_PDF_MIN_TOKENS = 1600; // At least one page
|
|
31
35
|
|
|
36
|
+
// Anthropic wraps each tool definition in XML internally, adding overhead
|
|
37
|
+
// beyond the raw JSON schema. Empirically measured at ~132 tokens/tool via
|
|
38
|
+
// the countTokens API, but the overhead varies by schema complexity.
|
|
39
|
+
// We use per-tool estimation (JSON schema size) plus a fixed XML-wrapping
|
|
40
|
+
// overhead to approximate the actual cost.
|
|
41
|
+
const TOOL_DEFINITION_OVERHEAD_TOKENS = 28;
|
|
42
|
+
|
|
32
43
|
export interface TokenEstimatorOptions {
|
|
33
44
|
providerName?: string;
|
|
45
|
+
/** Pre-computed tool token budget. When provided, added to the prompt total. */
|
|
46
|
+
toolTokenBudget?: number;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
export function estimateTextTokens(text: string): number {
|
|
@@ -185,6 +198,25 @@ export function estimateMessagesTokens(
|
|
|
185
198
|
return total;
|
|
186
199
|
}
|
|
187
200
|
|
|
201
|
+
/** Estimate token cost for a single tool definition. */
|
|
202
|
+
export function estimateToolDefinitionTokens(tool: ToolDefinition): number {
|
|
203
|
+
return (
|
|
204
|
+
TOOL_DEFINITION_OVERHEAD_TOKENS +
|
|
205
|
+
estimateTextTokens(tool.name) +
|
|
206
|
+
estimateTextTokens(tool.description) +
|
|
207
|
+
estimateTextTokens(stableJson(tool.input_schema))
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Estimate total token cost for an array of tool definitions. */
|
|
212
|
+
export function estimateToolsTokens(tools: ToolDefinition[]): number {
|
|
213
|
+
let total = 0;
|
|
214
|
+
for (const tool of tools) {
|
|
215
|
+
total += estimateToolDefinitionTokens(tool);
|
|
216
|
+
}
|
|
217
|
+
return total;
|
|
218
|
+
}
|
|
219
|
+
|
|
188
220
|
export function estimatePromptTokens(
|
|
189
221
|
messages: Message[],
|
|
190
222
|
systemPrompt?: string,
|
|
@@ -193,7 +225,8 @@ export function estimatePromptTokens(
|
|
|
193
225
|
const systemTokens = systemPrompt
|
|
194
226
|
? SYSTEM_PROMPT_OVERHEAD_TOKENS + estimateTextTokens(systemPrompt)
|
|
195
227
|
: 0;
|
|
196
|
-
|
|
228
|
+
const toolTokens = options?.toolTokenBudget ?? 0;
|
|
229
|
+
return systemTokens + toolTokens + estimateMessagesTokens(messages, options);
|
|
197
230
|
}
|
|
198
231
|
|
|
199
232
|
function stableJson(value: unknown): string {
|
|
@@ -85,12 +85,15 @@ export interface ContextWindowManagerOptions {
|
|
|
85
85
|
provider: Provider;
|
|
86
86
|
systemPrompt: string | (() => string);
|
|
87
87
|
config: ContextWindowConfig;
|
|
88
|
+
/** Pre-computed tool token budget to include in all estimations. */
|
|
89
|
+
toolTokenBudget?: number;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
export class ContextWindowManager {
|
|
91
93
|
private readonly provider: Provider;
|
|
92
94
|
private readonly _systemPrompt: string | (() => string);
|
|
93
95
|
private readonly config: ContextWindowConfig;
|
|
96
|
+
private readonly toolTokenBudget: number;
|
|
94
97
|
/**
|
|
95
98
|
* Cached resolved system prompt. Lazily populated on first access via the
|
|
96
99
|
* `systemPrompt` getter and cleared after each compaction pass so the next
|
|
@@ -102,6 +105,7 @@ export class ContextWindowManager {
|
|
|
102
105
|
this.provider = options.provider;
|
|
103
106
|
this._systemPrompt = options.systemPrompt;
|
|
104
107
|
this.config = options.config;
|
|
108
|
+
this.toolTokenBudget = options.toolTokenBudget ?? 0;
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
/** Lazily resolve and cache the system prompt for the duration of a compaction pass. */
|
|
@@ -132,6 +136,7 @@ export class ContextWindowManager {
|
|
|
132
136
|
try {
|
|
133
137
|
const estimated = estimatePromptTokens(messages, this.systemPrompt, {
|
|
134
138
|
providerName: this.provider.name,
|
|
139
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
135
140
|
});
|
|
136
141
|
const threshold = Math.floor(
|
|
137
142
|
this.config.maxInputTokens * this.config.compactThreshold,
|
|
@@ -163,6 +168,7 @@ export class ContextWindowManager {
|
|
|
163
168
|
options?.precomputedEstimate ??
|
|
164
169
|
estimatePromptTokens(messages, this.systemPrompt, {
|
|
165
170
|
providerName: this.provider.name,
|
|
171
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
166
172
|
});
|
|
167
173
|
const thresholdTokens = Math.floor(
|
|
168
174
|
this.config.maxInputTokens * this.config.compactThreshold,
|
|
@@ -245,6 +251,7 @@ export class ContextWindowManager {
|
|
|
245
251
|
const estimatedAfterTruncation = didTruncate
|
|
246
252
|
? estimatePromptTokens(truncatedMessages, this.systemPrompt, {
|
|
247
253
|
providerName: this.provider.name,
|
|
254
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
248
255
|
})
|
|
249
256
|
: previousEstimatedInputTokens;
|
|
250
257
|
return {
|
|
@@ -303,7 +310,10 @@ export class ContextWindowManager {
|
|
|
303
310
|
const projectedInputTokens = estimatePromptTokens(
|
|
304
311
|
projectedMessages,
|
|
305
312
|
this.systemPrompt,
|
|
306
|
-
{
|
|
313
|
+
{
|
|
314
|
+
providerName: this.provider.name,
|
|
315
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
316
|
+
},
|
|
307
317
|
);
|
|
308
318
|
const projectedGainTokens = Math.max(
|
|
309
319
|
0,
|
|
@@ -428,7 +438,10 @@ export class ContextWindowManager {
|
|
|
428
438
|
const estimatedInputTokens = estimatePromptTokens(
|
|
429
439
|
compactedMessages,
|
|
430
440
|
this.systemPrompt,
|
|
431
|
-
{
|
|
441
|
+
{
|
|
442
|
+
providerName: this.provider.name,
|
|
443
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
444
|
+
},
|
|
432
445
|
);
|
|
433
446
|
log.info(
|
|
434
447
|
{
|
|
@@ -502,6 +515,7 @@ export class ContextWindowManager {
|
|
|
502
515
|
);
|
|
503
516
|
return estimatePromptTokens(projectedMessages, this.systemPrompt, {
|
|
504
517
|
providerName: this.provider.name,
|
|
518
|
+
toolTokenBudget: this.toolTokenBudget,
|
|
505
519
|
});
|
|
506
520
|
};
|
|
507
521
|
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
resetAllowlist,
|
|
21
21
|
validateAllowlistFile,
|
|
22
22
|
} from "../security/secret-allowlist.js";
|
|
23
|
+
import { handleBashSignal } from "../signals/bash.js";
|
|
23
24
|
import { handleConfirmationSignal } from "../signals/confirm.js";
|
|
24
25
|
import { handleMcpReloadSignal } from "../signals/mcp-reload.js";
|
|
25
26
|
import { DebouncerMap } from "../util/debounce.js";
|
|
@@ -224,20 +225,37 @@ export class ConfigWatcher {
|
|
|
224
225
|
// If we can't create it, watching will also fail — handled below.
|
|
225
226
|
}
|
|
226
227
|
|
|
227
|
-
const
|
|
228
|
+
const exactSignalHandlers: Record<string, () => void> = {
|
|
228
229
|
"mcp-reload": handleMcpReloadSignal,
|
|
229
230
|
confirm: handleConfirmationSignal,
|
|
230
231
|
};
|
|
231
232
|
|
|
233
|
+
const prefixSignalHandlers: Record<string, (filename: string) => void> = {
|
|
234
|
+
"bash.": handleBashSignal,
|
|
235
|
+
};
|
|
236
|
+
|
|
232
237
|
try {
|
|
233
238
|
const watcher = watch(signalsDir, (_eventType, filename) => {
|
|
234
239
|
if (!filename) return;
|
|
235
240
|
const file = String(filename);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
|
|
242
|
+
if (exactSignalHandlers[file]) {
|
|
243
|
+
this.debounceTimers.schedule(`signal:${file}`, () => {
|
|
244
|
+
log.info({ file }, "Signal file detected");
|
|
245
|
+
exactSignalHandlers[file]();
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for (const [prefix, handler] of Object.entries(prefixSignalHandlers)) {
|
|
251
|
+
if (file.startsWith(prefix) && !file.endsWith(".result")) {
|
|
252
|
+
this.debounceTimers.schedule(`signal:${file}`, () => {
|
|
253
|
+
log.info({ file }, "Signal file detected");
|
|
254
|
+
handler(file);
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
241
259
|
});
|
|
242
260
|
this.watchers.push(watcher);
|
|
243
261
|
log.info({ dir: signalsDir }, "Watching signals directory");
|
|
@@ -88,6 +88,8 @@ export interface ReducerConfig {
|
|
|
88
88
|
contextWindow: ContextWindowConfig;
|
|
89
89
|
/** Target token budget — the reducer tries to get below this. */
|
|
90
90
|
targetTokens: number;
|
|
91
|
+
/** Pre-computed tool token budget to include in estimations. */
|
|
92
|
+
toolTokenBudget?: number;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
/**
|
|
@@ -143,6 +145,7 @@ export async function reduceContextOverflow(
|
|
|
143
145
|
// All tiers exhausted
|
|
144
146
|
const estimatedTokens = estimatePromptTokens(messages, config.systemPrompt, {
|
|
145
147
|
providerName: config.providerName,
|
|
148
|
+
toolTokenBudget: config.toolTokenBudget,
|
|
146
149
|
});
|
|
147
150
|
return {
|
|
148
151
|
messages,
|
|
@@ -175,6 +178,7 @@ async function applyForcedCompaction(
|
|
|
175
178
|
? result.estimatedInputTokens
|
|
176
179
|
: estimatePromptTokens(messages, config.systemPrompt, {
|
|
177
180
|
providerName: config.providerName,
|
|
181
|
+
toolTokenBudget: config.toolTokenBudget,
|
|
178
182
|
});
|
|
179
183
|
|
|
180
184
|
const nextApplied: ReducerTier[] = [...applied, "forced_compaction"];
|
|
@@ -205,7 +209,10 @@ function applyToolResultTruncation(
|
|
|
205
209
|
const estimatedTokens = estimatePromptTokens(
|
|
206
210
|
nextMessages,
|
|
207
211
|
config.systemPrompt,
|
|
208
|
-
{
|
|
212
|
+
{
|
|
213
|
+
providerName: config.providerName,
|
|
214
|
+
toolTokenBudget: config.toolTokenBudget,
|
|
215
|
+
},
|
|
209
216
|
);
|
|
210
217
|
|
|
211
218
|
const nextApplied: ReducerTier[] = [...applied, "tool_result_truncation"];
|
|
@@ -242,7 +249,10 @@ function applyMediaStubbing(
|
|
|
242
249
|
const estimatedTokens = estimatePromptTokens(
|
|
243
250
|
nextMessages,
|
|
244
251
|
config.systemPrompt,
|
|
245
|
-
{
|
|
252
|
+
{
|
|
253
|
+
providerName: config.providerName,
|
|
254
|
+
toolTokenBudget: config.toolTokenBudget,
|
|
255
|
+
},
|
|
246
256
|
);
|
|
247
257
|
|
|
248
258
|
const nextApplied: ReducerTier[] = [...applied, "media_stubbing"];
|
|
@@ -271,6 +281,7 @@ function applyInjectionDowngrade(
|
|
|
271
281
|
// mode, which the caller applies via applyRuntimeInjections().
|
|
272
282
|
const estimatedTokens = estimatePromptTokens(messages, config.systemPrompt, {
|
|
273
283
|
providerName: config.providerName,
|
|
284
|
+
toolTokenBudget: config.toolTokenBudget,
|
|
274
285
|
});
|
|
275
286
|
|
|
276
287
|
const nextApplied: ReducerTier[] = [...applied, "injection_downgrade"];
|
|
@@ -29,6 +29,29 @@ export function computeGatewayTarget(): string {
|
|
|
29
29
|
return getGatewayInternalBaseUrl();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Read the current ingress config from the raw workspace config file.
|
|
34
|
+
* Extracted so it can be called from both the daemon message handler
|
|
35
|
+
* and the HTTP route handler.
|
|
36
|
+
*/
|
|
37
|
+
export function getIngressConfigResult(): {
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
publicBaseUrl: string;
|
|
40
|
+
localGatewayTarget: string;
|
|
41
|
+
success: boolean;
|
|
42
|
+
} {
|
|
43
|
+
const raw = loadRawConfig();
|
|
44
|
+
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
45
|
+
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
|
|
46
|
+
const enabled = (ingress.enabled as boolean | undefined) ?? false;
|
|
47
|
+
return {
|
|
48
|
+
enabled,
|
|
49
|
+
publicBaseUrl,
|
|
50
|
+
localGatewayTarget: computeGatewayTarget(),
|
|
51
|
+
success: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/**
|
|
33
56
|
* Best-effort Twilio webhook sync helper.
|
|
34
57
|
*
|
|
@@ -80,16 +103,10 @@ export async function handleIngressConfig(
|
|
|
80
103
|
const localGatewayTarget = computeGatewayTarget();
|
|
81
104
|
try {
|
|
82
105
|
if (msg.action === "get") {
|
|
83
|
-
const
|
|
84
|
-
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
85
|
-
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? "";
|
|
86
|
-
const enabled = (ingress.enabled as boolean | undefined) ?? false;
|
|
106
|
+
const result = getIngressConfigResult();
|
|
87
107
|
ctx.send({
|
|
88
108
|
type: "ingress_config_response",
|
|
89
|
-
|
|
90
|
-
publicBaseUrl,
|
|
91
|
-
localGatewayTarget,
|
|
92
|
-
success: true,
|
|
109
|
+
...result,
|
|
93
110
|
});
|
|
94
111
|
} else if (msg.action === "set") {
|
|
95
112
|
const value = (msg.publicBaseUrl ?? "").trim().replace(/\/+$/, "");
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
API_KEY_PROVIDERS,
|
|
2
3
|
getConfig,
|
|
3
4
|
loadRawConfig,
|
|
4
5
|
saveRawConfig,
|
|
5
6
|
} from "../../config/loader.js";
|
|
6
7
|
import { initializeProviders } from "../../providers/registry.js";
|
|
8
|
+
import { getSecureKeyAsync } from "../../security/secure-keys.js";
|
|
7
9
|
import type {
|
|
8
10
|
ImageGenModelSetRequest,
|
|
9
11
|
ModelSetRequest,
|
|
@@ -26,11 +28,14 @@ export interface ModelInfo {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
/** Return current model configuration. */
|
|
29
|
-
export function getModelInfo(): ModelInfo {
|
|
31
|
+
export async function getModelInfo(): Promise<ModelInfo> {
|
|
30
32
|
const config = getConfig();
|
|
31
|
-
const configured =
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const configured: string[] = [];
|
|
34
|
+
for (const p of API_KEY_PROVIDERS) {
|
|
35
|
+
if (await getSecureKeyAsync(p)) {
|
|
36
|
+
configured.push(p);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
34
39
|
if (!configured.includes("ollama")) configured.push("ollama");
|
|
35
40
|
return {
|
|
36
41
|
model: config.model,
|
|
@@ -58,7 +63,10 @@ export interface ModelSetContext {
|
|
|
58
63
|
* Set the active model. Returns the resulting ModelInfo, or throws on failure.
|
|
59
64
|
* The caller is responsible for sending the response to the client.
|
|
60
65
|
*/
|
|
61
|
-
export function setModel(
|
|
66
|
+
export async function setModel(
|
|
67
|
+
modelId: string,
|
|
68
|
+
ctx: ModelSetContext,
|
|
69
|
+
): Promise<ModelInfo> {
|
|
62
70
|
// If the requested model is already the current model AND the provider
|
|
63
71
|
// is already aligned with what MODEL_TO_PROVIDER expects, skip expensive
|
|
64
72
|
// reinitialization but still return model_info so the client confirms.
|
|
@@ -68,17 +76,16 @@ export function setModel(modelId: string, ctx: ModelSetContext): ModelInfo {
|
|
|
68
76
|
const providerAligned =
|
|
69
77
|
!expectedProvider || current.provider === expectedProvider;
|
|
70
78
|
if (modelId === current.model && providerAligned) {
|
|
71
|
-
return getModelInfo();
|
|
79
|
+
return await getModelInfo();
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
// Validate API key before switching
|
|
76
84
|
const provider = MODEL_TO_PROVIDER[modelId];
|
|
77
85
|
if (provider && provider !== "ollama") {
|
|
78
|
-
|
|
79
|
-
if (!currentConfig.apiKeys[provider]) {
|
|
86
|
+
if (!(await getSecureKeyAsync(provider))) {
|
|
80
87
|
// Return current model_info so the client resyncs its optimistic state
|
|
81
|
-
return getModelInfo();
|
|
88
|
+
return await getModelInfo();
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
91
|
|
|
@@ -158,20 +165,20 @@ export function setImageGenModel(modelId: string, ctx: ModelSetContext): void {
|
|
|
158
165
|
// HTTP handlers (delegate to shared logic)
|
|
159
166
|
// ---------------------------------------------------------------------------
|
|
160
167
|
|
|
161
|
-
export function handleModelGet(ctx: HandlerContext): void {
|
|
162
|
-
const info = getModelInfo();
|
|
168
|
+
export async function handleModelGet(ctx: HandlerContext): Promise<void> {
|
|
169
|
+
const info = await getModelInfo();
|
|
163
170
|
ctx.send({
|
|
164
171
|
type: "model_info",
|
|
165
172
|
...info,
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
export function handleModelSet(
|
|
176
|
+
export async function handleModelSet(
|
|
170
177
|
msg: ModelSetRequest,
|
|
171
178
|
ctx: HandlerContext,
|
|
172
|
-
): void {
|
|
179
|
+
): Promise<void> {
|
|
173
180
|
try {
|
|
174
|
-
const info = setModel(msg.model, ctx);
|
|
181
|
+
const info = await setModel(msg.model, ctx);
|
|
175
182
|
ctx.send({ type: "model_info", ...info });
|
|
176
183
|
} catch (err) {
|
|
177
184
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -193,4 +200,3 @@ export function handleImageGenModelSet(
|
|
|
193
200
|
log.error({ err }, `Failed to set image gen model: ${message}`);
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
|
-
|