@vellumai/assistant 0.5.0 → 0.5.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/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/filesystem-tools.test.ts +4 -2
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +103 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
- package/src/__tests__/skill-feature-flags.test.ts +13 -13
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +8 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/history-repair.ts +28 -8
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +62 -19
- package/src/prompts/system-prompt.ts +2 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +4 -21
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { getConversationsDir } from "../util/platform.js";
|
|
5
|
+
|
|
6
|
+
function getConversationDirTimestamp(createdAtMs: number): string {
|
|
7
|
+
return new Date(createdAtMs).toISOString().replace(/:/g, "-");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getLegacyConversationDirName(
|
|
11
|
+
id: string,
|
|
12
|
+
createdAtMs: number,
|
|
13
|
+
): string {
|
|
14
|
+
return `${id}_${getConversationDirTimestamp(createdAtMs)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a filesystem-safe directory name for a conversation.
|
|
19
|
+
* Format: `{isoDate}_{id}` where colons in the ISO date are replaced with
|
|
20
|
+
* hyphens so the name is valid on all platforms (Windows forbids colons).
|
|
21
|
+
*/
|
|
22
|
+
export function getConversationDirName(
|
|
23
|
+
id: string,
|
|
24
|
+
createdAtMs: number,
|
|
25
|
+
): string {
|
|
26
|
+
return `${getConversationDirTimestamp(createdAtMs)}_${id}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return the absolute path to a conversation's timestamp-first disk-view
|
|
31
|
+
* directory.
|
|
32
|
+
*/
|
|
33
|
+
export function getConversationDirPath(
|
|
34
|
+
id: string,
|
|
35
|
+
createdAtMs: number,
|
|
36
|
+
): string {
|
|
37
|
+
return join(getConversationsDir(), getConversationDirName(id, createdAtMs));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLegacyConversationDirPath(
|
|
41
|
+
id: string,
|
|
42
|
+
createdAtMs: number,
|
|
43
|
+
): string {
|
|
44
|
+
return join(
|
|
45
|
+
getConversationsDir(),
|
|
46
|
+
getLegacyConversationDirName(id, createdAtMs),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ResolvedConversationDirectoryPaths {
|
|
51
|
+
canonicalDirPath: string;
|
|
52
|
+
canonicalDirName: string;
|
|
53
|
+
legacyDirPath: string;
|
|
54
|
+
legacyDirName: string;
|
|
55
|
+
resolvedDirPath: string;
|
|
56
|
+
resolvedDirName: string;
|
|
57
|
+
hasCanonicalDir: boolean;
|
|
58
|
+
hasLegacyDir: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveConversationDirectoryPaths(
|
|
62
|
+
id: string,
|
|
63
|
+
createdAtMs: number,
|
|
64
|
+
conversationsDir: string = getConversationsDir(),
|
|
65
|
+
): ResolvedConversationDirectoryPaths {
|
|
66
|
+
const canonicalDirName = getConversationDirName(id, createdAtMs);
|
|
67
|
+
const canonicalDirPath = join(conversationsDir, canonicalDirName);
|
|
68
|
+
const hasCanonicalDir = existsSync(canonicalDirPath);
|
|
69
|
+
|
|
70
|
+
const legacyDirName = getLegacyConversationDirName(id, createdAtMs);
|
|
71
|
+
const legacyDirPath = join(conversationsDir, legacyDirName);
|
|
72
|
+
const hasLegacyDir = existsSync(legacyDirPath);
|
|
73
|
+
|
|
74
|
+
const resolvedDirPath = hasCanonicalDir
|
|
75
|
+
? canonicalDirPath
|
|
76
|
+
: hasLegacyDir
|
|
77
|
+
? legacyDirPath
|
|
78
|
+
: canonicalDirPath;
|
|
79
|
+
const resolvedDirName = hasCanonicalDir
|
|
80
|
+
? canonicalDirName
|
|
81
|
+
: hasLegacyDir
|
|
82
|
+
? legacyDirName
|
|
83
|
+
: canonicalDirName;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
canonicalDirPath,
|
|
87
|
+
canonicalDirName,
|
|
88
|
+
legacyDirPath,
|
|
89
|
+
legacyDirName,
|
|
90
|
+
resolvedDirPath,
|
|
91
|
+
resolvedDirName,
|
|
92
|
+
hasCanonicalDir,
|
|
93
|
+
hasLegacyDir,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the active conversation directory path:
|
|
99
|
+
* 1) prefer timestamp-first when it exists;
|
|
100
|
+
* 2) otherwise reuse legacy sibling when present;
|
|
101
|
+
* 3) otherwise fall back to timestamp-first as the creation target.
|
|
102
|
+
*/
|
|
103
|
+
export function getResolvedConversationDirPath(
|
|
104
|
+
id: string,
|
|
105
|
+
createdAtMs: number,
|
|
106
|
+
): string {
|
|
107
|
+
return resolveConversationDirectoryPaths(id, createdAtMs).resolvedDirPath;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getResolvedConversationDirName(
|
|
111
|
+
id: string,
|
|
112
|
+
createdAtMs: number,
|
|
113
|
+
): string {
|
|
114
|
+
return resolveConversationDirectoryPaths(id, createdAtMs).resolvedDirName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getConversationAttachmentsDirPath(
|
|
118
|
+
conversationId: string,
|
|
119
|
+
createdAtMs: number,
|
|
120
|
+
): string {
|
|
121
|
+
return join(
|
|
122
|
+
getResolvedConversationDirPath(conversationId, createdAtMs),
|
|
123
|
+
"attachments",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-through disk view for conversations.
|
|
3
|
+
*
|
|
4
|
+
* Projects conversation metadata, messages, and attachments to a browsable
|
|
5
|
+
* filesystem layout under ~/.vellum/workspace/conversations/. This enables
|
|
6
|
+
* the assistant to search/read/manipulate conversation data using standard
|
|
7
|
+
* file tools.
|
|
8
|
+
*
|
|
9
|
+
* All disk writes are best-effort — failures are logged but never thrown,
|
|
10
|
+
* so the disk view cannot break DB operations.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
appendFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { getLogger } from "../util/logger.js";
|
|
23
|
+
import {
|
|
24
|
+
getAttachmentContent,
|
|
25
|
+
getAttachmentMetadataForMessage,
|
|
26
|
+
getFilePathForAttachment,
|
|
27
|
+
} from "./attachments-store.js";
|
|
28
|
+
import {
|
|
29
|
+
getConversation,
|
|
30
|
+
getMessageById,
|
|
31
|
+
getMessages,
|
|
32
|
+
} from "./conversation-crud.js";
|
|
33
|
+
import {
|
|
34
|
+
getConversationDirName,
|
|
35
|
+
getConversationDirPath,
|
|
36
|
+
getLegacyConversationDirPath,
|
|
37
|
+
getResolvedConversationDirPath,
|
|
38
|
+
} from "./conversation-directories.js";
|
|
39
|
+
|
|
40
|
+
const log = getLogger("conversation-disk-view");
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Directory helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
getConversationDirName,
|
|
48
|
+
getConversationDirPath,
|
|
49
|
+
getResolvedConversationDirPath,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function ensureConversationDirPath(id: string, createdAtMs: number): string {
|
|
53
|
+
const dirPath = getResolvedConversationDirPath(id, createdAtMs);
|
|
54
|
+
mkdirSync(dirPath, { recursive: true });
|
|
55
|
+
return dirPath;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Write operations
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create the conversation directory and write the initial meta.json.
|
|
64
|
+
*/
|
|
65
|
+
export function initConversationDir(conv: {
|
|
66
|
+
id: string;
|
|
67
|
+
title: string | null;
|
|
68
|
+
createdAt: number;
|
|
69
|
+
conversationType: string;
|
|
70
|
+
originChannel: string | null;
|
|
71
|
+
}): void {
|
|
72
|
+
try {
|
|
73
|
+
const dirPath = ensureConversationDirPath(conv.id, conv.createdAt);
|
|
74
|
+
|
|
75
|
+
const meta = {
|
|
76
|
+
id: conv.id,
|
|
77
|
+
title: conv.title,
|
|
78
|
+
type: conv.conversationType,
|
|
79
|
+
channel: conv.originChannel,
|
|
80
|
+
createdAt: new Date(conv.createdAt).toISOString(),
|
|
81
|
+
updatedAt: new Date(conv.createdAt).toISOString(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(dirPath, "meta.json"),
|
|
86
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
87
|
+
);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
log.warn(
|
|
90
|
+
{ err, conversationId: conv.id },
|
|
91
|
+
"Failed to init conversation dir",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Rewrite meta.json with updated fields.
|
|
98
|
+
*/
|
|
99
|
+
export function updateMetaFile(conv: {
|
|
100
|
+
id: string;
|
|
101
|
+
title: string | null;
|
|
102
|
+
createdAt: number;
|
|
103
|
+
updatedAt: number;
|
|
104
|
+
conversationType: string;
|
|
105
|
+
originChannel: string | null;
|
|
106
|
+
}): void {
|
|
107
|
+
try {
|
|
108
|
+
const dirPath = ensureConversationDirPath(conv.id, conv.createdAt);
|
|
109
|
+
|
|
110
|
+
const meta = {
|
|
111
|
+
id: conv.id,
|
|
112
|
+
title: conv.title,
|
|
113
|
+
type: conv.conversationType,
|
|
114
|
+
channel: conv.originChannel,
|
|
115
|
+
createdAt: new Date(conv.createdAt).toISOString(),
|
|
116
|
+
updatedAt: new Date(conv.updatedAt).toISOString(),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
writeFileSync(
|
|
120
|
+
join(dirPath, "meta.json"),
|
|
121
|
+
JSON.stringify(meta, null, 2) + "\n",
|
|
122
|
+
);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
log.warn({ err, conversationId: conv.id }, "Failed to update meta file");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Content flattening
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
interface ContentBlock {
|
|
133
|
+
type: string;
|
|
134
|
+
text?: string;
|
|
135
|
+
name?: string;
|
|
136
|
+
input?: unknown;
|
|
137
|
+
content?: unknown;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface FlattenedContent {
|
|
141
|
+
content: string;
|
|
142
|
+
toolCalls: Array<{ name: string; input: unknown }>;
|
|
143
|
+
toolResults: Array<{ content: unknown }>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse the message `content` JSON string (ContentBlock[]) and extract
|
|
148
|
+
* text, tool_use, and tool_result blocks into flat fields.
|
|
149
|
+
*/
|
|
150
|
+
export function flattenContentBlocks(rawContent: string): FlattenedContent {
|
|
151
|
+
const result: FlattenedContent = {
|
|
152
|
+
content: "",
|
|
153
|
+
toolCalls: [],
|
|
154
|
+
toolResults: [],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let blocks: ContentBlock[];
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(rawContent);
|
|
160
|
+
if (!Array.isArray(parsed)) {
|
|
161
|
+
// Plain text content (not block array)
|
|
162
|
+
return { ...result, content: rawContent };
|
|
163
|
+
}
|
|
164
|
+
blocks = parsed;
|
|
165
|
+
} catch {
|
|
166
|
+
// Not valid JSON — treat as plain text
|
|
167
|
+
return { ...result, content: rawContent };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const textParts: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const block of blocks) {
|
|
173
|
+
switch (block.type) {
|
|
174
|
+
case "text":
|
|
175
|
+
if (typeof block.text === "string") {
|
|
176
|
+
textParts.push(block.text);
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "tool_use":
|
|
180
|
+
if (typeof block.name === "string") {
|
|
181
|
+
result.toolCalls.push({ name: block.name, input: block.input });
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case "tool_result":
|
|
185
|
+
result.toolResults.push({ content: block.content });
|
|
186
|
+
break;
|
|
187
|
+
// Skip "image" and "file" blocks — represented via attachments
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result.content = textParts.join("\n");
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Attachment projection
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a unique filename within a directory, handling collisions by
|
|
201
|
+
* appending a suffix (e.g., `photo-2.png`, `photo-3.png`).
|
|
202
|
+
*/
|
|
203
|
+
export function resolveUniqueFilename(dir: string, filename: string): string {
|
|
204
|
+
const sanitized = basename(filename);
|
|
205
|
+
if (!existsSync(join(dir, sanitized))) return sanitized;
|
|
206
|
+
|
|
207
|
+
const ext = extname(sanitized);
|
|
208
|
+
const base = basename(sanitized, ext);
|
|
209
|
+
let counter = 2;
|
|
210
|
+
let candidate: string;
|
|
211
|
+
do {
|
|
212
|
+
candidate = `${base}-${counter}${ext}`;
|
|
213
|
+
counter++;
|
|
214
|
+
} while (existsSync(join(dir, candidate)));
|
|
215
|
+
|
|
216
|
+
return candidate;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Ensure an attachment is present in the conversation's attachments/
|
|
221
|
+
* subdirectory and return the filename recorded in the disk view.
|
|
222
|
+
*/
|
|
223
|
+
function writeAttachmentFile(
|
|
224
|
+
conversationDirPath: string,
|
|
225
|
+
attachmentId: string,
|
|
226
|
+
originalFilename: string,
|
|
227
|
+
): string | null {
|
|
228
|
+
try {
|
|
229
|
+
const attachDir = join(conversationDirPath, "attachments");
|
|
230
|
+
mkdirSync(attachDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const existingPath = getFilePathForAttachment(attachmentId);
|
|
233
|
+
if (
|
|
234
|
+
existingPath &&
|
|
235
|
+
existsSync(existingPath) &&
|
|
236
|
+
dirname(existingPath) === attachDir
|
|
237
|
+
) {
|
|
238
|
+
return basename(existingPath);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const content = getAttachmentContent(attachmentId);
|
|
242
|
+
if (!content) return null;
|
|
243
|
+
|
|
244
|
+
const resolvedName = resolveUniqueFilename(attachDir, originalFilename);
|
|
245
|
+
writeFileSync(join(attachDir, resolvedName), content);
|
|
246
|
+
return resolvedName;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
log.warn(
|
|
249
|
+
{ err, attachmentId, originalFilename },
|
|
250
|
+
"Failed to write attachment file to disk",
|
|
251
|
+
);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Message sync
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read a message and its attachments from DB, flatten content, and append
|
|
262
|
+
* a JSONL line to `messages.jsonl` in the conversation's disk-view directory.
|
|
263
|
+
* Attachment filenames are recorded from the conversation's attachments/
|
|
264
|
+
* subdirectory, materializing legacy rows there only when needed.
|
|
265
|
+
*
|
|
266
|
+
* Requires `createdAtMs` of the conversation to resolve the directory path.
|
|
267
|
+
*/
|
|
268
|
+
export function syncMessageToDisk(
|
|
269
|
+
conversationId: string,
|
|
270
|
+
messageId: string,
|
|
271
|
+
createdAtMs: number,
|
|
272
|
+
): void {
|
|
273
|
+
try {
|
|
274
|
+
const message = getMessageById(messageId, conversationId);
|
|
275
|
+
if (!message) {
|
|
276
|
+
log.warn(
|
|
277
|
+
{ conversationId, messageId },
|
|
278
|
+
"syncMessageToDisk: message not found",
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const dirPath = ensureConversationDirPath(conversationId, createdAtMs);
|
|
284
|
+
const { content, toolCalls, toolResults } = flattenContentBlocks(
|
|
285
|
+
message.content,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Project attachments to disk
|
|
289
|
+
const attachmentMeta = getAttachmentMetadataForMessage(messageId);
|
|
290
|
+
const attachmentFilenames: string[] = [];
|
|
291
|
+
for (const att of attachmentMeta) {
|
|
292
|
+
const resolved = writeAttachmentFile(
|
|
293
|
+
dirPath,
|
|
294
|
+
att.id,
|
|
295
|
+
att.originalFilename,
|
|
296
|
+
);
|
|
297
|
+
if (resolved) {
|
|
298
|
+
attachmentFilenames.push(resolved);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Build JSONL record
|
|
303
|
+
const record: Record<string, unknown> = {
|
|
304
|
+
role: message.role,
|
|
305
|
+
ts: new Date(message.createdAt).toISOString(),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (content) record.content = content;
|
|
309
|
+
if (toolCalls.length > 0) record.toolCalls = toolCalls;
|
|
310
|
+
if (toolResults.length > 0) record.toolResults = toolResults;
|
|
311
|
+
if (attachmentFilenames.length > 0)
|
|
312
|
+
record.attachments = attachmentFilenames;
|
|
313
|
+
|
|
314
|
+
appendFileSync(
|
|
315
|
+
join(dirPath, "messages.jsonl"),
|
|
316
|
+
JSON.stringify(record) + "\n",
|
|
317
|
+
);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
log.warn(
|
|
320
|
+
{ err, conversationId, messageId },
|
|
321
|
+
"Failed to sync message to disk",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Rebuild a single conversation's disk view from current DB state.
|
|
328
|
+
*
|
|
329
|
+
* This rewrites append-only `messages.jsonl` and replays all persisted messages
|
|
330
|
+
* in DB order so disk data matches post-mutation state (e.g., after assistant-
|
|
331
|
+
* message consolidation). Existing attachment files are preserved to avoid
|
|
332
|
+
* losing file-backed rows where base64 payloads were already compacted out.
|
|
333
|
+
*/
|
|
334
|
+
export function rebuildConversationDiskViewFromDbState(
|
|
335
|
+
conversationId: string,
|
|
336
|
+
): void {
|
|
337
|
+
try {
|
|
338
|
+
const conv = getConversation(conversationId);
|
|
339
|
+
if (!conv) {
|
|
340
|
+
log.warn(
|
|
341
|
+
{ conversationId },
|
|
342
|
+
"rebuildConversationDiskViewFromDbState: conversation not found",
|
|
343
|
+
);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const dirPath = ensureConversationDirPath(conversationId, conv.createdAt);
|
|
348
|
+
const messagesPath = join(dirPath, "messages.jsonl");
|
|
349
|
+
|
|
350
|
+
rmSync(messagesPath, { force: true });
|
|
351
|
+
writeFileSync(messagesPath, "");
|
|
352
|
+
// Preserve attachment files: many attachment rows are file-backed with
|
|
353
|
+
// data_base64 cleared, so deleting attachments/ can make content
|
|
354
|
+
// unrecoverable for replay.
|
|
355
|
+
mkdirSync(join(dirPath, "attachments"), { recursive: true });
|
|
356
|
+
|
|
357
|
+
const convMessages = getMessages(conversationId);
|
|
358
|
+
for (const msg of convMessages) {
|
|
359
|
+
syncMessageToDisk(conversationId, msg.id, conv.createdAt);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
updateMetaFile(conv);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
log.warn(
|
|
365
|
+
{ err, conversationId },
|
|
366
|
+
"Failed to rebuild conversation disk view from DB state",
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Removal
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Remove a conversation's disk-view directory entirely.
|
|
377
|
+
*/
|
|
378
|
+
export function removeConversationDir(id: string, createdAtMs: number): void {
|
|
379
|
+
try {
|
|
380
|
+
const dirPaths = new Set([
|
|
381
|
+
getConversationDirPath(id, createdAtMs),
|
|
382
|
+
getLegacyConversationDirPath(id, createdAtMs),
|
|
383
|
+
]);
|
|
384
|
+
for (const dirPath of dirPaths) {
|
|
385
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
log.warn({ err, conversationId: id }, "Failed to remove conversation dir");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { eq } from "drizzle-orm";
|
|
11
11
|
import { v4 as uuid } from "uuid";
|
|
12
12
|
|
|
13
|
+
import { initConversationDir } from "./conversation-disk-view.js";
|
|
13
14
|
import { GENERATING_TITLE } from "./conversation-title-service.js";
|
|
14
15
|
import { getDb } from "./db.js";
|
|
15
16
|
import { conversationKeys, conversations } from "./schema.js";
|
|
@@ -138,7 +139,7 @@ export function getOrCreateConversation(
|
|
|
138
139
|
const db = getDb();
|
|
139
140
|
const conversationType = opts?.conversationType ?? "standard";
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
const result = db.transaction((tx) => {
|
|
142
143
|
const existing = tx
|
|
143
144
|
.select()
|
|
144
145
|
.from(conversationKeys)
|
|
@@ -146,7 +147,7 @@ export function getOrCreateConversation(
|
|
|
146
147
|
.get();
|
|
147
148
|
|
|
148
149
|
if (existing) {
|
|
149
|
-
return { conversationId: existing.conversationId, created: false };
|
|
150
|
+
return { conversationId: existing.conversationId, created: false as const };
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
// Check if the conversationKey itself is an existing conversation ID.
|
|
@@ -167,18 +168,19 @@ export function getOrCreateConversation(
|
|
|
167
168
|
createdAt: Date.now(),
|
|
168
169
|
})
|
|
169
170
|
.run();
|
|
170
|
-
return { conversationId: existingConversation.id, created: false };
|
|
171
|
+
return { conversationId: existingConversation.id, created: false as const };
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
const now = Date.now();
|
|
174
175
|
const conversationId = uuid();
|
|
176
|
+
const title = GENERATING_TITLE;
|
|
175
177
|
const memoryScopeId =
|
|
176
178
|
conversationType === "private" ? `private:${conversationId}` : "default";
|
|
177
179
|
|
|
178
180
|
tx.insert(conversations)
|
|
179
181
|
.values({
|
|
180
182
|
id: conversationId,
|
|
181
|
-
title
|
|
183
|
+
title,
|
|
182
184
|
createdAt: now,
|
|
183
185
|
updatedAt: now,
|
|
184
186
|
totalInputTokens: 0,
|
|
@@ -201,6 +203,16 @@ export function getOrCreateConversation(
|
|
|
201
203
|
})
|
|
202
204
|
.run();
|
|
203
205
|
|
|
204
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
conversationId,
|
|
208
|
+
created: true as const,
|
|
209
|
+
conversation: { id: conversationId, title, createdAt: now, conversationType },
|
|
210
|
+
};
|
|
205
211
|
});
|
|
212
|
+
|
|
213
|
+
if (result.created) {
|
|
214
|
+
initConversationDir({ ...result.conversation, originChannel: null });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { conversationId: result.conversationId, created: result.created };
|
|
206
218
|
}
|
|
@@ -101,7 +101,11 @@ export function isLastUserMessageToolResult(conversationId: string): boolean {
|
|
|
101
101
|
parsed.every(
|
|
102
102
|
(block: Record<string, unknown>) =>
|
|
103
103
|
block.type === "tool_result" ||
|
|
104
|
-
block.type === "web_search_tool_result"
|
|
104
|
+
block.type === "web_search_tool_result" ||
|
|
105
|
+
(block.type === "text" &&
|
|
106
|
+
typeof block.text === "string" &&
|
|
107
|
+
block.text.startsWith("<system_notice>") &&
|
|
108
|
+
block.text.endsWith("</system_notice>")),
|
|
105
109
|
)
|
|
106
110
|
) {
|
|
107
111
|
return true;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { getConfig } from "../config/loader.js";
|
|
11
11
|
import { getConfiguredProvider } from "../providers/provider-send-message.js";
|
|
12
12
|
import type { Provider } from "../providers/types.js";
|
|
13
|
+
import { runBtwSidechain } from "../runtime/btw-sidechain.js";
|
|
13
14
|
import { getLogger } from "../util/logger.js";
|
|
14
15
|
import { truncate } from "../util/truncate.js";
|
|
15
16
|
import {
|
|
@@ -129,30 +130,16 @@ export async function generateAndPersistConversationTitle(
|
|
|
129
130
|
|
|
130
131
|
const config = getConfig();
|
|
131
132
|
const prompt = buildTitlePrompt(context, userMessage, assistantResponse);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
config: {
|
|
143
|
-
max_tokens: config.daemon.titleGenerationMaxTokens,
|
|
144
|
-
modelIntent: "latency-optimized",
|
|
145
|
-
},
|
|
146
|
-
signal: combinedSignal,
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
150
|
-
if (textBlock && textBlock.type === "text") {
|
|
151
|
-
let title = normalizeTitle(textBlock.text);
|
|
152
|
-
if (!title) {
|
|
153
|
-
title = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
|
|
154
|
-
}
|
|
155
|
-
|
|
133
|
+
const result = await runBtwSidechain({
|
|
134
|
+
content: prompt,
|
|
135
|
+
provider,
|
|
136
|
+
maxTokens: config.daemon.titleGenerationMaxTokens,
|
|
137
|
+
modelIntent: "latency-optimized",
|
|
138
|
+
signal,
|
|
139
|
+
timeoutMs: 10_000,
|
|
140
|
+
});
|
|
141
|
+
const title = normalizeTitle(result.text);
|
|
142
|
+
if (title) {
|
|
156
143
|
// Re-check replaceability before persisting (race guard)
|
|
157
144
|
const current = getConversation(conversationId);
|
|
158
145
|
if (current && !isReplaceableTitle(current.title)) {
|
|
@@ -246,31 +233,16 @@ export async function regenerateConversationTitle(
|
|
|
246
233
|
|
|
247
234
|
const prompt = buildRegenerationPrompt(recentMessages);
|
|
248
235
|
const config = getConfig();
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
config: {
|
|
260
|
-
max_tokens: config.daemon.titleGenerationMaxTokens,
|
|
261
|
-
modelIntent: "latency-optimized",
|
|
262
|
-
},
|
|
263
|
-
signal: combinedSignal,
|
|
264
|
-
},
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
268
|
-
if (textBlock && textBlock.type === "text") {
|
|
269
|
-
const title = normalizeTitle(textBlock.text);
|
|
270
|
-
if (!title) {
|
|
271
|
-
return { title: conversation.title ?? UNTITLED_FALLBACK, updated: false };
|
|
272
|
-
}
|
|
273
|
-
|
|
236
|
+
const result = await runBtwSidechain({
|
|
237
|
+
content: prompt,
|
|
238
|
+
provider,
|
|
239
|
+
maxTokens: config.daemon.titleGenerationMaxTokens,
|
|
240
|
+
modelIntent: "latency-optimized",
|
|
241
|
+
signal,
|
|
242
|
+
timeoutMs: 10_000,
|
|
243
|
+
});
|
|
244
|
+
const title = normalizeTitle(result.text);
|
|
245
|
+
if (title) {
|
|
274
246
|
// Re-check isAutoTitle before persisting (race guard against manual rename)
|
|
275
247
|
const current = getConversation(conversationId);
|
|
276
248
|
if (!current || !current.isAutoTitle) {
|