@vellumai/assistant 0.11.1 → 0.11.2-staging.2
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 +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared leaf utilities for the notification pipeline.
|
|
3
3
|
*
|
|
4
|
-
* This module has no intra-pipeline imports
|
|
5
|
-
*
|
|
4
|
+
* This module has no intra-pipeline imports (only Node stdlib and leaf
|
|
5
|
+
* utilities), so any notification module can import it without creating
|
|
6
6
|
* circular dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { isPlainObject } from "../util/object.js";
|
|
10
|
+
|
|
9
11
|
// ── String helpers ──────────────────────────────────────────────────────────
|
|
10
12
|
|
|
11
13
|
/** Return `value` trimmed, or `undefined` when blank/nullish. */
|
|
@@ -26,13 +28,29 @@ export function readPayloadString(
|
|
|
26
28
|
payload: unknown,
|
|
27
29
|
key: string,
|
|
28
30
|
): string | undefined {
|
|
29
|
-
if (!
|
|
31
|
+
if (!isPlainObject(payload)) {
|
|
30
32
|
return undefined;
|
|
31
33
|
}
|
|
32
|
-
const value =
|
|
34
|
+
const value = payload[key];
|
|
33
35
|
return typeof value === "string" ? value : undefined;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Safely read a plain-object property from an unknown-typed payload object.
|
|
40
|
+
* Returns `undefined` when the payload is not an object or the key does not
|
|
41
|
+
* hold a plain object (arrays and `null` are rejected).
|
|
42
|
+
*/
|
|
43
|
+
export function readPayloadObject(
|
|
44
|
+
payload: unknown,
|
|
45
|
+
key: string,
|
|
46
|
+
): Record<string, unknown> | undefined {
|
|
47
|
+
if (!isPlainObject(payload)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const value = payload[key];
|
|
51
|
+
return isPlainObject(value) ? value : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
/** Truncate `text` to `maxLength`, appending "…" when exceeded. */
|
|
37
55
|
export function truncate(text: string, maxLength: number): string {
|
|
38
56
|
if (text.length <= maxLength) {
|
|
@@ -68,3 +86,20 @@ export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
|
|
|
68
86
|
export function sanitizeMessagePreview(value: string): string {
|
|
69
87
|
return sanitize(value, MESSAGE_PREVIEW_MAX_LENGTH);
|
|
70
88
|
}
|
|
89
|
+
|
|
90
|
+
/** Character budget every notification title shares. */
|
|
91
|
+
export const NOTIFICATION_TITLE_MAX_LENGTH = 60;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Flatten an untrusted notification title onto a single line, stripping control
|
|
95
|
+
* characters and newlines and bounding it at `NOTIFICATION_TITLE_MAX_LENGTH`.
|
|
96
|
+
*
|
|
97
|
+
* The newline flattening is the load-bearing part: downstream consumers re-run
|
|
98
|
+
* the value through `normalizeTitle`, whose prose guard discards any string
|
|
99
|
+
* containing a newline outright, so a multi-line conversation title would be
|
|
100
|
+
* thrown away wholesale instead of salvaged. The length bound is a backstop;
|
|
101
|
+
* `normalizeTitle`'s tighter 40-character budget is what callers observe.
|
|
102
|
+
*/
|
|
103
|
+
export function sanitizeNotificationTitle(value: string): string {
|
|
104
|
+
return sanitize(value, NOTIFICATION_TITLE_MAX_LENGTH);
|
|
105
|
+
}
|
|
@@ -122,6 +122,11 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
|
|
|
122
122
|
description:
|
|
123
123
|
"OAuth credential health issue detected (expired, revoked, missing scopes)",
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
id: "chat.assistant_reply",
|
|
127
|
+
description:
|
|
128
|
+
"Assistant finished replying to a user-initiated turn the user is no longer watching",
|
|
129
|
+
},
|
|
125
130
|
{
|
|
126
131
|
id: "telegram.webhook_health_alert",
|
|
127
132
|
description:
|
|
@@ -231,6 +236,11 @@ export interface NotificationSignal<TEventName extends string = string> {
|
|
|
231
236
|
sourceEventName: TEventName; // free-form: 'reminder_fired', 'guardian_question', etc.
|
|
232
237
|
contextPayload: NotificationContextPayload<TEventName>;
|
|
233
238
|
attentionHints: AttentionHints;
|
|
239
|
+
/**
|
|
240
|
+
* Producer-supplied deduplication key. Deterministic decision paths reuse it
|
|
241
|
+
* as the decision's dedupeKey so the decision row matches the event row.
|
|
242
|
+
*/
|
|
243
|
+
dedupeKey?: string;
|
|
234
244
|
/** Routing intent from the source (e.g. reminder). Controls post-decision channel enforcement. */
|
|
235
245
|
routingIntent?: RoutingIntent;
|
|
236
246
|
/** Free-form hints from the source for the decision engine (e.g. preferred channels). */
|
|
@@ -58,6 +58,10 @@ export const DeliveryResultSchema = z.object({
|
|
|
58
58
|
success: z.boolean(),
|
|
59
59
|
error: z.string().optional(),
|
|
60
60
|
messageId: z.string().optional(),
|
|
61
|
+
/** Set only by the platform push adapter: true when the platform accepted
|
|
62
|
+
* at least one device push for delivery (not proof of device receipt). */
|
|
63
|
+
remotePushAccepted: z.boolean().optional(),
|
|
64
|
+
remotePushPlatforms: z.array(z.enum(["ios", "android"])).optional(),
|
|
61
65
|
});
|
|
62
66
|
export type DeliveryResult = z.infer<typeof DeliveryResultSchema>;
|
|
63
67
|
|
|
@@ -95,6 +99,7 @@ export type RenderedChannelCopy = z.infer<typeof RenderedChannelCopySchema>;
|
|
|
95
99
|
|
|
96
100
|
export const ChannelDeliveryPayloadSchema = z.object({
|
|
97
101
|
deliveryId: z.string().optional(),
|
|
102
|
+
correlationId: z.string().optional(),
|
|
98
103
|
sourceEventName: z.string(),
|
|
99
104
|
copy: RenderedChannelCopySchema,
|
|
100
105
|
deepLinkTarget: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -106,6 +111,11 @@ export const ChannelDeliveryPayloadSchema = z.object({
|
|
|
106
111
|
* broadcaster so channel adapters render it without re-parsing
|
|
107
112
|
* `contextPayload`. */
|
|
108
113
|
toolApprovalSource: ToolApprovalSourceViewSchema.optional(),
|
|
114
|
+
/** True when the platform (APNs) channel accepted a remote push for this
|
|
115
|
+
* delivery. Set only on the vellum channel's payload so clients can avoid
|
|
116
|
+
* double-bannering. */
|
|
117
|
+
remotePushDispatched: z.boolean().optional(),
|
|
118
|
+
remotePushPlatforms: z.array(z.enum(["ios", "android"])).optional(),
|
|
109
119
|
});
|
|
110
120
|
export type ChannelDeliveryPayload = z.infer<
|
|
111
121
|
typeof ChannelDeliveryPayloadSchema
|
|
@@ -119,11 +129,16 @@ export interface ChannelUpdatePayload {
|
|
|
119
129
|
// -- Channel adapter interface ------------------------------------------------
|
|
120
130
|
|
|
121
131
|
/** Interface that each channel adapter must implement. */
|
|
132
|
+
export type ChannelDeliveryObserver = {
|
|
133
|
+
onRemotePushPlatforms(platforms: ("ios" | "android")[]): void;
|
|
134
|
+
};
|
|
135
|
+
|
|
122
136
|
export interface ChannelAdapter {
|
|
123
137
|
channel: NotificationChannel;
|
|
124
138
|
send(
|
|
125
139
|
payload: ChannelDeliveryPayload,
|
|
126
140
|
destination: ChannelDestination,
|
|
141
|
+
observer?: ChannelDeliveryObserver,
|
|
127
142
|
): Promise<DeliveryResult>;
|
|
128
143
|
update?(
|
|
129
144
|
delivery: ChannelUpdateContext,
|
|
@@ -183,5 +198,7 @@ export interface NotificationDecision {
|
|
|
183
198
|
dedupeKey: string;
|
|
184
199
|
confidence: number;
|
|
185
200
|
fallbackUsed: boolean;
|
|
201
|
+
/** Set by deterministic pass-through branches whose copy is producer-supplied verbatim; exempts the copy from the event-name-collision quality check. */
|
|
202
|
+
verbatimCopy?: boolean;
|
|
186
203
|
persistedDecisionId?: string;
|
|
187
204
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { desc, eq } from "drizzle-orm";
|
|
1
|
+
import { and, desc, eq, type SQL } from "drizzle-orm";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import type { DrizzleDb } from "./db-connection.js";
|
|
@@ -79,21 +79,30 @@ function rowToSummary(row: BookmarkJoinRow): BookmarkSummary {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* CROSS JOIN constrains SQLite's join order to start from message_bookmarks,
|
|
84
|
+
* which the planner would otherwise skip in favor of a full messages scan
|
|
85
|
+
* because an empty table never has statistics. The join predicates live in
|
|
86
|
+
* WHERE with identical semantics; callers pass extra conditions through
|
|
87
|
+
* `filter` because chaining `.where()` would replace the predicates.
|
|
88
|
+
*/
|
|
89
|
+
function selectBookmarkJoin(db: DrizzleDb, filter?: SQL) {
|
|
90
|
+
const joinMatch = and(
|
|
91
|
+
eq(messages.id, messageBookmarks.messageId),
|
|
92
|
+
eq(conversations.id, messageBookmarks.conversationId),
|
|
93
|
+
);
|
|
83
94
|
return db
|
|
84
95
|
.select(BOOKMARK_JOIN_COLUMNS)
|
|
85
96
|
.from(messageBookmarks)
|
|
86
|
-
.
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
eq(conversations.id, messageBookmarks.conversationId),
|
|
90
|
-
);
|
|
97
|
+
.crossJoin(messages)
|
|
98
|
+
.crossJoin(conversations)
|
|
99
|
+
.where(filter ? and(joinMatch, filter) : joinMatch);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
103
|
* List all bookmarks newest-first, joined against `messages` and
|
|
95
104
|
* `conversations`. Bookmarks whose parent message or conversation has
|
|
96
|
-
* been deleted are naturally excluded by the
|
|
105
|
+
* been deleted are naturally excluded by the join predicates; the
|
|
97
106
|
* `ON DELETE CASCADE` on the FKs means rows should never end up in this
|
|
98
107
|
* orphan state, but the join provides a defense-in-depth guarantee.
|
|
99
108
|
*/
|
|
@@ -179,7 +188,7 @@ function readBookmarkSummaryOrThrow(
|
|
|
179
188
|
db: DrizzleDb,
|
|
180
189
|
id: string,
|
|
181
190
|
): BookmarkSummary {
|
|
182
|
-
const row = selectBookmarkJoin(db
|
|
191
|
+
const row = selectBookmarkJoin(db, eq(messageBookmarks.id, id)).get();
|
|
183
192
|
if (!row) {
|
|
184
193
|
// Unreachable: caller just observed (or inserted) this id.
|
|
185
194
|
throw new Error(`Bookmark ${id} disappeared between insert and read`);
|
|
@@ -61,6 +61,32 @@ export interface AttentionState {
|
|
|
61
61
|
updatedAt: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* True when the latest assistant cursor is ahead of the last-seen cursor.
|
|
66
|
+
*
|
|
67
|
+
* The single definition of "unseen" for the sidebar indicator
|
|
68
|
+
* (`buildAssistantAttention` in `runtime/services/conversation-serializer.ts`)
|
|
69
|
+
* and the assistant-reply push producer, so the badge and the notification
|
|
70
|
+
* cannot disagree. An absent state row reads as seen.
|
|
71
|
+
*/
|
|
72
|
+
export function hasUnseenLatestAssistantMessage(
|
|
73
|
+
state:
|
|
74
|
+
| Pick<
|
|
75
|
+
AttentionState,
|
|
76
|
+
"latestAssistantMessageAt" | "lastSeenAssistantMessageAt"
|
|
77
|
+
>
|
|
78
|
+
| null
|
|
79
|
+
| undefined,
|
|
80
|
+
): boolean {
|
|
81
|
+
if (!state || state.latestAssistantMessageAt == null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
state.lastSeenAssistantMessageAt == null ||
|
|
86
|
+
state.lastSeenAssistantMessageAt < state.latestAssistantMessageAt
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
// ── Row mappers ──────────────────────────────────────────────────────
|
|
65
91
|
|
|
66
92
|
function rowToEvent(
|
|
@@ -74,6 +74,7 @@ import { deleteConversationRowsInBatches } from "./conversation-row-batch-delete
|
|
|
74
74
|
import {
|
|
75
75
|
BACKGROUND_CONVERSATION_TYPES,
|
|
76
76
|
type ConversationCreateType,
|
|
77
|
+
isHiddenMessageMetadata,
|
|
77
78
|
} from "./conversation-types.js";
|
|
78
79
|
import { runAsyncSqlite } from "./db-async-query.js";
|
|
79
80
|
import {
|
|
@@ -297,14 +298,35 @@ export const messageMetadataSchema = z
|
|
|
297
298
|
* treat message text as organic user input.
|
|
298
299
|
*/
|
|
299
300
|
hidden: z.boolean().optional(),
|
|
301
|
+
/**
|
|
302
|
+
* Marks a role-`"user"` row that opened a live phone or in-app voice turn.
|
|
303
|
+
* Test with {@link isVoiceSessionUserMessage}, which documents why the
|
|
304
|
+
* channel/interface fields cannot stand in for it.
|
|
305
|
+
*/
|
|
306
|
+
voiceSessionTurn: z.boolean().optional(),
|
|
300
307
|
/**
|
|
301
308
|
* Discriminates daemon-authored rows from ordinary turns.
|
|
302
309
|
* `"system_card"` marks pre-composed status cards (the /compact, /clean,
|
|
303
310
|
* and summarize-up-to results); see {@link SYSTEM_CARD_MESSAGE_KIND}.
|
|
304
|
-
*
|
|
305
|
-
*
|
|
311
|
+
* `"provider_error"` marks the synthetic assistant row the agent loop
|
|
312
|
+
* persists when a turn dies on the provider-error path; see
|
|
313
|
+
* {@link PROVIDER_ERROR_MESSAGE_KIND}. Kept as a plain string so unknown
|
|
314
|
+
* future kinds never fail metadata validation.
|
|
306
315
|
*/
|
|
307
316
|
messageKind: z.string().optional(),
|
|
317
|
+
/**
|
|
318
|
+
* Stable classified error code (`ClassifiedConversationError.code`, e.g.
|
|
319
|
+
* `"PROVIDER_BILLING"`) stamped alongside
|
|
320
|
+
* `messageKind: "provider_error"` on persisted provider-failure rows.
|
|
321
|
+
*/
|
|
322
|
+
providerErrorCode: z.string().optional(),
|
|
323
|
+
/**
|
|
324
|
+
* Classified error category (`ClassifiedConversationError.errorCategory`,
|
|
325
|
+
* e.g. `"credits_exhausted"`) stamped alongside
|
|
326
|
+
* `messageKind: "provider_error"`. Clients switch on this to pick a
|
|
327
|
+
* themed rendering for the row.
|
|
328
|
+
*/
|
|
329
|
+
providerErrorCategory: z.string().optional(),
|
|
308
330
|
/**
|
|
309
331
|
* Structured terminal record stamped onto a `<background_event
|
|
310
332
|
* source="background-tool">` wake so the web can rebuild the inline
|
|
@@ -348,17 +370,17 @@ export const messageMetadataSchema = z
|
|
|
348
370
|
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
|
|
349
371
|
|
|
350
372
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* consumers of message text — cannot drift.
|
|
373
|
+
* Pure predicates over the `metadata` record above. They live in the
|
|
374
|
+
* `conversation-types` leaf so a caller that only classifies a row does not
|
|
375
|
+
* pull in this module's DB graph, and are re-exported here alongside the
|
|
376
|
+
* schema they read.
|
|
356
377
|
*/
|
|
357
|
-
export
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
378
|
+
export {
|
|
379
|
+
isBackgroundEventMetadata,
|
|
380
|
+
isEchoSuppressedUserMessage,
|
|
381
|
+
isHiddenMessageMetadata,
|
|
382
|
+
isVoiceSessionUserMessage,
|
|
383
|
+
} from "./conversation-types.js";
|
|
362
384
|
|
|
363
385
|
/**
|
|
364
386
|
* `messageKind` value marking a daemon-authored system card — a pre-composed
|
|
@@ -369,6 +391,16 @@ export function isHiddenMessageMetadata(
|
|
|
369
391
|
*/
|
|
370
392
|
export const SYSTEM_CARD_MESSAGE_KIND = "system_card";
|
|
371
393
|
|
|
394
|
+
/**
|
|
395
|
+
* `messageKind` value marking the synthetic assistant row the agent loop
|
|
396
|
+
* persists when a turn terminates on the provider-error path (see the
|
|
397
|
+
* `persistProviderErrorAsAssistantMessage` branch). The row's text stays in
|
|
398
|
+
* LLM history like any assistant message; the marker (plus the
|
|
399
|
+
* `providerErrorCode`/`providerErrorCategory` fields stamped next to it) lets
|
|
400
|
+
* clients render the row as a themed notice instead of persona speech.
|
|
401
|
+
*/
|
|
402
|
+
export const PROVIDER_ERROR_MESSAGE_KIND = "provider_error";
|
|
403
|
+
|
|
372
404
|
/**
|
|
373
405
|
* Shared predicate for the system-card marker on assistant-message metadata
|
|
374
406
|
* (see the `messageKind` field on {@link messageMetadataSchema}). One
|
|
@@ -382,11 +414,24 @@ export function isSystemCardMetadata(
|
|
|
382
414
|
}
|
|
383
415
|
|
|
384
416
|
/**
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
417
|
+
* Shared predicate for the provider-error marker on assistant-message
|
|
418
|
+
* metadata (see the `messageKind` field on {@link messageMetadataSchema}).
|
|
419
|
+
* One definition so persistence stamping and wire projection cannot drift.
|
|
420
|
+
*/
|
|
421
|
+
export function isProviderErrorMetadata(
|
|
422
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
423
|
+
): boolean {
|
|
424
|
+
return metadata?.messageKind === PROVIDER_ERROR_MESSAGE_KIND;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* True when an assistant row is a standalone display turn: a system card or
|
|
429
|
+
* a provider-error notice. Standalone rows never merge with adjacent
|
|
430
|
+
* assistant rows, and turn grouping closes on them, so display merging and
|
|
431
|
+
* the turn resolver agree on boundaries. Takes the raw persisted `metadata`
|
|
432
|
+
* JSON string; malformed JSON and non-assistant roles are never standalone.
|
|
388
433
|
*/
|
|
389
|
-
export function
|
|
434
|
+
export function isStandaloneAssistantMessage(
|
|
390
435
|
role: string,
|
|
391
436
|
metadata: string | null,
|
|
392
437
|
): boolean {
|
|
@@ -394,9 +439,8 @@ export function isSystemCardMessage(
|
|
|
394
439
|
return false;
|
|
395
440
|
}
|
|
396
441
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
);
|
|
442
|
+
const parsed = JSON.parse(metadata) as Record<string, unknown>;
|
|
443
|
+
return isSystemCardMetadata(parsed) || isProviderErrorMetadata(parsed);
|
|
400
444
|
} catch {
|
|
401
445
|
return false;
|
|
402
446
|
}
|
|
@@ -4209,8 +4253,10 @@ export function getTurnTimeBounds(
|
|
|
4209
4253
|
/**
|
|
4210
4254
|
* Resolve all assistant message IDs that belong to the same agent turn
|
|
4211
4255
|
* as the given `messageId`. A "turn" is bounded by:
|
|
4212
|
-
* - The start of the conversation,
|
|
4213
|
-
* - A user message whose content is NOT a tool_result array
|
|
4256
|
+
* - The start of the conversation,
|
|
4257
|
+
* - A user message whose content is NOT a tool_result array, or
|
|
4258
|
+
* - A standalone assistant row (see {@link isStandaloneAssistantMessage}),
|
|
4259
|
+
* which always forms its own single-row turn.
|
|
4214
4260
|
*
|
|
4215
4261
|
* Within a multi-step agent loop, the pattern is:
|
|
4216
4262
|
* user msg → assistant A1 → user (tool_result) → assistant A2 → ...
|
|
@@ -4229,9 +4275,10 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
4229
4275
|
return [messageId];
|
|
4230
4276
|
}
|
|
4231
4277
|
|
|
4232
|
-
// A system card
|
|
4233
|
-
//
|
|
4234
|
-
|
|
4278
|
+
// A standalone row (system card or provider-error notice) is its own
|
|
4279
|
+
// single-row group: its linked calls (e.g. the summarize-up-to compaction
|
|
4280
|
+
// call) never mix into a neighbouring turn.
|
|
4281
|
+
if (isStandaloneAssistantMessage(target.role, target.metadata)) {
|
|
4235
4282
|
return [target.id];
|
|
4236
4283
|
}
|
|
4237
4284
|
|
|
@@ -4261,9 +4308,9 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
4261
4308
|
|
|
4262
4309
|
for (const row of backwardRows) {
|
|
4263
4310
|
if (row.role === "assistant") {
|
|
4264
|
-
if (
|
|
4265
|
-
// A
|
|
4266
|
-
// before
|
|
4311
|
+
if (isStandaloneAssistantMessage(row.role, row.metadata)) {
|
|
4312
|
+
// A standalone row closes the groups on either side of it: rows
|
|
4313
|
+
// before it belong to an earlier display group.
|
|
4267
4314
|
boundaryCreatedAt = row.createdAt;
|
|
4268
4315
|
break;
|
|
4269
4316
|
}
|
|
@@ -4303,10 +4350,10 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
4303
4350
|
|
|
4304
4351
|
for (const row of forwardRows) {
|
|
4305
4352
|
if (row.role === "assistant") {
|
|
4306
|
-
if (
|
|
4307
|
-
// A
|
|
4308
|
-
// /compact result
|
|
4309
|
-
// closes the group.
|
|
4353
|
+
if (isStandaloneAssistantMessage(row.role, row.metadata)) {
|
|
4354
|
+
// A standalone row that is the queried user message's only reply
|
|
4355
|
+
// (e.g. the /compact result card, or a provider-error notice) IS
|
|
4356
|
+
// the turn's response; otherwise it closes the group.
|
|
4310
4357
|
if (assistantIds.length === 0) {
|
|
4311
4358
|
assistantIds.push(row.id);
|
|
4312
4359
|
}
|
|
@@ -4350,7 +4397,7 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
4350
4397
|
for (const row of gapRows) {
|
|
4351
4398
|
if (
|
|
4352
4399
|
row.role === "assistant" &&
|
|
4353
|
-
!
|
|
4400
|
+
!isStandaloneAssistantMessage(row.role, row.metadata) &&
|
|
4354
4401
|
!assistantIds.includes(row.id)
|
|
4355
4402
|
) {
|
|
4356
4403
|
assistantIds.push(row.id);
|
|
@@ -902,16 +902,16 @@ function buildExcerptWithExternalContentMode(
|
|
|
902
902
|
|
|
903
903
|
if (externalContentMode === "display") {
|
|
904
904
|
text = unwrapExternalContentForDisplay(text);
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
905
|
+
return buildExcerptFromText(text, query, DISPLAY_EXCERPT_LEADING_CHARS);
|
|
906
|
+
}
|
|
907
|
+
const envelope = parseExternalContentEnvelope(text);
|
|
908
|
+
if (envelope) {
|
|
909
|
+
const innerExcerpt = buildExcerptFromText(envelope.content, query);
|
|
910
|
+
return wrapRecallEvidenceExcerpt(
|
|
911
|
+
innerExcerpt,
|
|
912
|
+
envelope.source,
|
|
913
|
+
envelope.origin,
|
|
914
|
+
);
|
|
915
915
|
}
|
|
916
916
|
|
|
917
917
|
return buildExcerptFromText(text, query);
|
|
@@ -985,19 +985,34 @@ function findEarliestMatch(
|
|
|
985
985
|
return match ? { index: match.index, length: match[0].length } : null;
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
-
|
|
989
|
-
|
|
988
|
+
const EXCERPT_WINDOW = 100;
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Leading context for palette display excerpts. The result row truncates
|
|
992
|
+
* from the right, so a wide leading window can push the match past the
|
|
993
|
+
* visible cutoff; recall evidence keeps the full window.
|
|
994
|
+
*/
|
|
995
|
+
const DISPLAY_EXCERPT_LEADING_CHARS = 30;
|
|
996
|
+
|
|
997
|
+
function buildExcerptFromText(
|
|
998
|
+
text: string,
|
|
999
|
+
query: string,
|
|
1000
|
+
leadingChars: number = EXCERPT_WINDOW,
|
|
1001
|
+
): string {
|
|
990
1002
|
const match = findEarliestMatch(text, query);
|
|
991
1003
|
if (!match) {
|
|
992
1004
|
// Neither the query nor any of its tokens is present (e.g. the lexical
|
|
993
1005
|
// index matched JSON structure instead); fall back to the text start.
|
|
994
1006
|
return text
|
|
995
|
-
.slice(0,
|
|
1007
|
+
.slice(0, EXCERPT_WINDOW * 2)
|
|
996
1008
|
.replace(/\s+/g, " ")
|
|
997
1009
|
.trim();
|
|
998
1010
|
}
|
|
999
|
-
const start = Math.max(0, match.index -
|
|
1000
|
-
const end = Math.min(
|
|
1011
|
+
const start = Math.max(0, match.index - leadingChars);
|
|
1012
|
+
const end = Math.min(
|
|
1013
|
+
text.length,
|
|
1014
|
+
match.index + match.length + EXCERPT_WINDOW,
|
|
1015
|
+
);
|
|
1001
1016
|
const excerpt =
|
|
1002
1017
|
(start > 0 ? "\u2026" : "") +
|
|
1003
1018
|
text.slice(start, end).replace(/\s+/g, " ").trim() +
|