@vellumai/assistant 0.5.1 → 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__/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-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__/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 +32 -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__/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__/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 +8 -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 +48 -7
- 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/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 +71 -8
- 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 +2 -7
- 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
package/src/util/pricing.ts
CHANGED
|
@@ -25,6 +25,7 @@ const PROVIDER_PRICING: Record<string, Record<string, ModelPricing>> = {
|
|
|
25
25
|
},
|
|
26
26
|
openai: {
|
|
27
27
|
"gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
|
|
28
|
+
"gpt-5.4-mini": { inputPer1M: 0.5, outputPer1M: 3 },
|
|
28
29
|
"gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25 },
|
|
29
30
|
"gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
|
|
30
31
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
|
package/src/util/retry.ts
CHANGED
|
@@ -108,9 +108,7 @@ export function isRetryableNetworkError(error: unknown): boolean {
|
|
|
108
108
|
|
|
109
109
|
// Fall back to message-based detection for errors without errno codes
|
|
110
110
|
// (e.g. Bun's "The socket connection was closed unexpectedly")
|
|
111
|
-
if (
|
|
112
|
-
RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))
|
|
113
|
-
) {
|
|
111
|
+
if (RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))) {
|
|
114
112
|
return true;
|
|
115
113
|
}
|
|
116
114
|
|
|
@@ -16,8 +16,8 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
|
|
|
16
16
|
"Backfill installationId into lockfile from SQLite checkpoint and clean up stale row",
|
|
17
17
|
run(_workspaceDir: string): void {
|
|
18
18
|
// a. Read existing installation ID from SQLite, or generate a new one.
|
|
19
|
-
// On fresh installs the memory_checkpoints table may not exist yet
|
|
20
|
-
//
|
|
19
|
+
// On fresh installs the memory_checkpoints table may not exist yet,
|
|
20
|
+
// so treat errors as null.
|
|
21
21
|
let existingId: string | null = null;
|
|
22
22
|
try {
|
|
23
23
|
existingId = getMemoryCheckpoint("telemetry:installation_id");
|
|
@@ -26,19 +26,30 @@ export const backfillInstallationIdMigration: WorkspaceMigration = {
|
|
|
26
26
|
}
|
|
27
27
|
const installationId = existingId || randomUUID();
|
|
28
28
|
|
|
29
|
-
// b. Read the lockfile
|
|
29
|
+
// b. Read the lockfile — check both the current and legacy lockfile paths
|
|
30
|
+
// to support installs that haven't migrated the filename yet.
|
|
30
31
|
const base = process.env.BASE_DATA_DIR?.trim() || homedir();
|
|
31
|
-
const
|
|
32
|
-
|
|
32
|
+
const lockCandidates = [
|
|
33
|
+
join(base, ".vellum.lock.json"),
|
|
34
|
+
join(base, ".vellum.lockfile.json"),
|
|
35
|
+
];
|
|
33
36
|
|
|
34
|
-
let
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
let lockPath: string | undefined;
|
|
38
|
+
let lockData: Record<string, unknown> | undefined;
|
|
39
|
+
for (const candidate of lockCandidates) {
|
|
40
|
+
if (!existsSync(candidate)) continue;
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
43
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
44
|
+
lockPath = candidate;
|
|
45
|
+
lockData = raw as Record<string, unknown>;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Malformed — try next candidate.
|
|
50
|
+
}
|
|
41
51
|
}
|
|
52
|
+
if (!lockPath || !lockData) return;
|
|
42
53
|
|
|
43
54
|
// c. Find the assistant entry that corresponds to this daemon instance
|
|
44
55
|
const assistants = lockData.assistants as
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
|
|
5
|
-
import {
|
|
4
|
+
import { getDeviceIdBaseDir } from "../../util/device-id.js";
|
|
6
5
|
import type { WorkspaceMigration } from "./types.js";
|
|
7
6
|
|
|
8
7
|
export const seedDeviceIdMigration: WorkspaceMigration = {
|
|
9
8
|
id: "003-seed-device-id",
|
|
10
9
|
description:
|
|
11
|
-
"Seed
|
|
10
|
+
"Seed device.json deviceId from the most recent lockfile installationId for continuity",
|
|
12
11
|
run(_workspaceDir: string): void {
|
|
13
|
-
const base =
|
|
12
|
+
const base = getDeviceIdBaseDir();
|
|
14
13
|
const vellumDir = join(base, ".vellum");
|
|
15
14
|
const devicePath = join(vellumDir, "device.json");
|
|
16
15
|
|
|
@@ -83,6 +83,11 @@ export const servicesConfigMigration: WorkspaceMigration = {
|
|
|
83
83
|
...existingServices,
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
// Legacy top-level fields (provider, model) are the user's actual
|
|
87
|
+
// configuration from before the services structure existed. If they're
|
|
88
|
+
// present as strings they take precedence over any `existingServices`
|
|
89
|
+
// values, which are just defaults written by backfillConfigDefaults().
|
|
90
|
+
// The spread preserves any extra keys that future backfills may add.
|
|
86
91
|
services.inference = {
|
|
87
92
|
...(existingServices.inference ?? {}),
|
|
88
93
|
mode: inferenceMode,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const voiceTimeoutAndMaxStepsMigration: WorkspaceMigration = {
|
|
4
|
+
id: "008-voice-timeout-and-max-steps",
|
|
5
|
+
description:
|
|
6
|
+
"Add elevenlabs.conversationTimeoutSeconds and maxStepsPerSession to config schema (defaults handle new installs; macOS client syncs existing UserDefaults values on startup)",
|
|
7
|
+
run(_workspaceDir: string): void {
|
|
8
|
+
// No-op — schema defaults handle new installs.
|
|
9
|
+
// Existing users: macOS client will sync UserDefaults values
|
|
10
|
+
// to config on next startup via settings sync endpoints.
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { rebuildConversationDiskViewFromDb } from "./rebuild-conversation-disk-view.js";
|
|
2
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const backfillConversationDiskViewMigration: WorkspaceMigration = {
|
|
5
|
+
id: "009-backfill-conversation-disk-view",
|
|
6
|
+
description: "Rebuild conversation disk view for existing conversations",
|
|
7
|
+
run(_workspaceDir: string): void {
|
|
8
|
+
rebuildConversationDiskViewFromDb();
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace migration 010: Rename UUID-based app directories and files to
|
|
3
|
+
* human-readable slugified names.
|
|
4
|
+
*
|
|
5
|
+
* Inline slugify + dedup logic (not imported from app-store) so the migration
|
|
6
|
+
* remains stable even if runtime code changes in the future.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: safe to re-run after interruption at any point. Handles
|
|
9
|
+
* partially-renamed states (crash between JSON write and file rename).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import {
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
renameSync,
|
|
20
|
+
unlinkSync,
|
|
21
|
+
writeFileSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Self-contained slug generation (do NOT import from app-store)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function slugify(name: string): string {
|
|
32
|
+
let slug = name
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
35
|
+
.replace(/-{2,}/g, "-")
|
|
36
|
+
.replace(/^-+|-+$/g, "");
|
|
37
|
+
|
|
38
|
+
if (slug.length > 60) {
|
|
39
|
+
slug = slug.slice(0, 60).replace(/-+$/, "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!slug) {
|
|
43
|
+
slug = `app-${randomUUID().slice(0, 8)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return slug;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function generateUniqueDirName(name: string, usedNames: Set<string>): string {
|
|
50
|
+
const base = slugify(name);
|
|
51
|
+
if (!usedNames.has(base)) return base;
|
|
52
|
+
let counter = 2;
|
|
53
|
+
while (usedNames.has(`${base}-${counter}`)) {
|
|
54
|
+
counter++;
|
|
55
|
+
}
|
|
56
|
+
return `${base}-${counter}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Defense-in-depth: reject dirNames that could cause path traversal. */
|
|
60
|
+
function isValidDirName(dirName: string): boolean {
|
|
61
|
+
return (
|
|
62
|
+
!!dirName &&
|
|
63
|
+
!dirName.includes("/") &&
|
|
64
|
+
!dirName.includes("\\") &&
|
|
65
|
+
!dirName.includes("..") &&
|
|
66
|
+
dirName === dirName.trim()
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Migration
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
export const appDirRenameMigration: WorkspaceMigration = {
|
|
75
|
+
id: "010-app-dir-rename",
|
|
76
|
+
description:
|
|
77
|
+
"Rename UUID-based app directories and files to human-readable slugified names",
|
|
78
|
+
|
|
79
|
+
run(workspaceDir: string): void {
|
|
80
|
+
const appsDir = join(workspaceDir, "data", "apps");
|
|
81
|
+
if (!existsSync(appsDir)) return;
|
|
82
|
+
|
|
83
|
+
// Read all JSON files (sorted for deterministic ordering)
|
|
84
|
+
const jsonFiles = readdirSync(appsDir)
|
|
85
|
+
.filter((f) => f.endsWith(".json"))
|
|
86
|
+
.sort();
|
|
87
|
+
|
|
88
|
+
if (jsonFiles.length === 0) return;
|
|
89
|
+
|
|
90
|
+
const usedNames = new Set<string>();
|
|
91
|
+
|
|
92
|
+
for (const jsonFile of jsonFiles) {
|
|
93
|
+
const jsonPath = join(appsDir, jsonFile);
|
|
94
|
+
let raw: string;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(jsonPath, "utf-8");
|
|
97
|
+
} catch {
|
|
98
|
+
continue; // skip unreadable files
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let parsed: {
|
|
102
|
+
id?: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
dirName?: string;
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
continue; // skip malformed JSON
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const appId = parsed.id;
|
|
113
|
+
const appName = parsed.name ?? "untitled";
|
|
114
|
+
if (!appId) continue;
|
|
115
|
+
|
|
116
|
+
// Check if already migrated: has dirName AND filesystem matches
|
|
117
|
+
if (parsed.dirName && isValidDirName(parsed.dirName)) {
|
|
118
|
+
const expectedJsonFile = `${parsed.dirName}.json`;
|
|
119
|
+
if (
|
|
120
|
+
jsonFile === expectedJsonFile &&
|
|
121
|
+
existsSync(join(appsDir, parsed.dirName))
|
|
122
|
+
) {
|
|
123
|
+
// Already fully migrated -- just track the name
|
|
124
|
+
usedNames.add(parsed.dirName);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Partially renamed: JSON has dirName but files may still be at old paths.
|
|
129
|
+
// Use the dirName from JSON but rename from wherever the files actually are.
|
|
130
|
+
const dirName = parsed.dirName;
|
|
131
|
+
usedNames.add(dirName);
|
|
132
|
+
renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// No dirName yet -- generate one
|
|
137
|
+
const dirName = generateUniqueDirName(appName, usedNames);
|
|
138
|
+
if (!isValidDirName(dirName)) continue; // safety check
|
|
139
|
+
usedNames.add(dirName);
|
|
140
|
+
renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Best-effort git commit
|
|
144
|
+
try {
|
|
145
|
+
const gitDir = join(appsDir, ".git");
|
|
146
|
+
if (existsSync(gitDir)) {
|
|
147
|
+
execSync(
|
|
148
|
+
"git add -A && git commit -m 'Migration 010: rename app dirs to slugified names' --allow-empty",
|
|
149
|
+
{
|
|
150
|
+
cwd: appsDir,
|
|
151
|
+
stdio: "ignore",
|
|
152
|
+
timeout: 10_000,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Git failure is non-fatal -- log nothing since we don't have
|
|
158
|
+
// the logger available in migrations. The next commitAppChange()
|
|
159
|
+
// call will pick up the renamed files naturally.
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Rename app files from their current location to dirName-based paths.
|
|
166
|
+
* Each step checks existence to handle partial completion.
|
|
167
|
+
*/
|
|
168
|
+
function renameAppFiles(
|
|
169
|
+
appsDir: string,
|
|
170
|
+
currentJsonFile: string,
|
|
171
|
+
appId: string,
|
|
172
|
+
dirName: string,
|
|
173
|
+
parsed: Record<string, unknown>,
|
|
174
|
+
_rawJson: string,
|
|
175
|
+
): void {
|
|
176
|
+
const targetJsonFile = `${dirName}.json`;
|
|
177
|
+
const targetPreviewFile = `${dirName}.preview`;
|
|
178
|
+
|
|
179
|
+
// 1. Rename the app directory: {appId}/ -> {dirName}/
|
|
180
|
+
const oldDir = join(appsDir, appId);
|
|
181
|
+
const newDir = join(appsDir, dirName);
|
|
182
|
+
if (existsSync(oldDir) && !existsSync(newDir) && oldDir !== newDir) {
|
|
183
|
+
renameSync(oldDir, newDir);
|
|
184
|
+
} else if (!existsSync(newDir)) {
|
|
185
|
+
// Directory doesn't exist at either location -- create it
|
|
186
|
+
mkdirSync(newDir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. Rename the preview file: {appId}.preview -> {dirName}.preview
|
|
190
|
+
const oldPreview = join(appsDir, `${appId}.preview`);
|
|
191
|
+
const newPreview = join(appsDir, targetPreviewFile);
|
|
192
|
+
if (
|
|
193
|
+
existsSync(oldPreview) &&
|
|
194
|
+
!existsSync(newPreview) &&
|
|
195
|
+
oldPreview !== newPreview
|
|
196
|
+
) {
|
|
197
|
+
renameSync(oldPreview, newPreview);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 3. Rename the JSON file: {currentFilename} -> {dirName}.json
|
|
201
|
+
// Also update the dirName field in the JSON content.
|
|
202
|
+
const currentJsonPath = join(appsDir, currentJsonFile);
|
|
203
|
+
const targetJsonPath = join(appsDir, targetJsonFile);
|
|
204
|
+
|
|
205
|
+
// Update the JSON with dirName field
|
|
206
|
+
const updatedParsed = { ...parsed, dirName };
|
|
207
|
+
const updatedJson = JSON.stringify(updatedParsed, null, 2);
|
|
208
|
+
|
|
209
|
+
if (currentJsonFile !== targetJsonFile) {
|
|
210
|
+
// Write to new location, then remove old
|
|
211
|
+
writeFileSync(targetJsonPath, updatedJson, "utf-8");
|
|
212
|
+
if (existsSync(currentJsonPath) && currentJsonPath !== targetJsonPath) {
|
|
213
|
+
try {
|
|
214
|
+
unlinkSync(currentJsonPath);
|
|
215
|
+
} catch {
|
|
216
|
+
// Old file cleanup is best-effort
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
// Just update the content in place
|
|
221
|
+
writeFileSync(targetJsonPath, updatedJson, "utf-8");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace migration 012: Rename legacy conversation disk-view directories
|
|
3
|
+
* from `{conversationId}_{timestamp}` to `{timestamp}_{conversationId}`.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent and conservative:
|
|
6
|
+
* - skips directories that already use the new format
|
|
7
|
+
* - skips non-matching directories
|
|
8
|
+
* - leaves an existing target directory alone rather than clobbering it
|
|
9
|
+
* - continues past per-directory rename failures
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readdirSync, renameSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const LEGACY_CONVERSATION_DIR_PATTERN =
|
|
18
|
+
/^(.*)_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)$/;
|
|
19
|
+
|
|
20
|
+
function parseLegacyConversationDirName(
|
|
21
|
+
dirName: string,
|
|
22
|
+
): { conversationId: string; timestamp: string } | null {
|
|
23
|
+
const match = dirName.match(LEGACY_CONVERSATION_DIR_PATTERN);
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
conversationId: match[1],
|
|
28
|
+
timestamp: match[2],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const renameConversationDiskViewDirsMigration: WorkspaceMigration = {
|
|
33
|
+
id: "012-rename-conversation-disk-view-dirs",
|
|
34
|
+
description:
|
|
35
|
+
"Rename legacy conversation disk-view directories to timestamp-first names",
|
|
36
|
+
|
|
37
|
+
run(workspaceDir: string): void {
|
|
38
|
+
const conversationsDir = join(workspaceDir, "conversations");
|
|
39
|
+
if (!existsSync(conversationsDir)) return;
|
|
40
|
+
|
|
41
|
+
const entries = readdirSync(conversationsDir, { withFileTypes: true })
|
|
42
|
+
.filter((entry) => entry.isDirectory())
|
|
43
|
+
.map((entry) => entry.name)
|
|
44
|
+
.sort();
|
|
45
|
+
|
|
46
|
+
for (const dirName of entries) {
|
|
47
|
+
const parsed = parseLegacyConversationDirName(dirName);
|
|
48
|
+
if (!parsed) continue;
|
|
49
|
+
|
|
50
|
+
const sourcePath = join(conversationsDir, dirName);
|
|
51
|
+
const targetName = `${parsed.timestamp}_${parsed.conversationId}`;
|
|
52
|
+
const targetPath = join(conversationsDir, targetName);
|
|
53
|
+
|
|
54
|
+
if (sourcePath === targetPath) continue;
|
|
55
|
+
if (existsSync(targetPath)) continue;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
renameSync(sourcePath, targetPath);
|
|
59
|
+
} catch {
|
|
60
|
+
// Best-effort: leave the old directory in place if a single rename fails.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { rebuildConversationDiskViewFromDb } from "./rebuild-conversation-disk-view.js";
|
|
2
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const repairConversationDiskViewMigration: WorkspaceMigration = {
|
|
5
|
+
id: "013-repair-conversation-disk-view",
|
|
6
|
+
description:
|
|
7
|
+
"Repair missing conversation disk-view folders skipped by the conversationKey creation path",
|
|
8
|
+
run(_workspaceDir: string): void {
|
|
9
|
+
rebuildConversationDiskViewFromDb();
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { asc, eq } from "drizzle-orm";
|
|
12
|
+
|
|
13
|
+
import { resolveConversationDirectoryPaths } from "../../memory/conversation-directories.js";
|
|
14
|
+
import {
|
|
15
|
+
initConversationDir,
|
|
16
|
+
syncMessageToDisk,
|
|
17
|
+
updateMetaFile,
|
|
18
|
+
} from "../../memory/conversation-disk-view.js";
|
|
19
|
+
import { getDb } from "../../memory/db.js";
|
|
20
|
+
import { conversations, messages } from "../../memory/schema.js";
|
|
21
|
+
import { getLogger } from "../../util/logger.js";
|
|
22
|
+
|
|
23
|
+
const log = getLogger("workspace-migrations");
|
|
24
|
+
|
|
25
|
+
function hasExpectedDiskViewArtifacts(
|
|
26
|
+
conv: { updatedAt: number },
|
|
27
|
+
dirPath: string,
|
|
28
|
+
): boolean {
|
|
29
|
+
const metaPath = join(dirPath, "meta.json");
|
|
30
|
+
const messagesPath = join(dirPath, "messages.jsonl");
|
|
31
|
+
const attachDir = join(dirPath, "attachments");
|
|
32
|
+
if (
|
|
33
|
+
!existsSync(metaPath) ||
|
|
34
|
+
!existsSync(messagesPath) ||
|
|
35
|
+
!existsSync(attachDir)
|
|
36
|
+
)
|
|
37
|
+
return false;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const existing = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
41
|
+
const expectedUpdatedAt = new Date(conv.updatedAt).toISOString();
|
|
42
|
+
return existing.updatedAt === expectedUpdatedAt;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function convergeDualConversationDirsToCanonical(
|
|
49
|
+
conv: { updatedAt: number },
|
|
50
|
+
canonicalDirPath: string,
|
|
51
|
+
legacyDirPath: string,
|
|
52
|
+
): void {
|
|
53
|
+
if (!existsSync(canonicalDirPath) || !existsSync(legacyDirPath)) return;
|
|
54
|
+
if (!hasExpectedDiskViewArtifacts(conv, canonicalDirPath)) return;
|
|
55
|
+
rmSync(legacyDirPath, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getProjectedAttachmentFilenames(messagesPath: string): Set<string> {
|
|
59
|
+
const filenames = new Set<string>();
|
|
60
|
+
if (!existsSync(messagesPath)) return filenames;
|
|
61
|
+
|
|
62
|
+
const raw = readFileSync(messagesPath, "utf-8");
|
|
63
|
+
for (const line of raw.split("\n")) {
|
|
64
|
+
if (!line.trim()) continue;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(line) as { attachments?: unknown };
|
|
67
|
+
if (!Array.isArray(parsed.attachments)) continue;
|
|
68
|
+
for (const attachment of parsed.attachments) {
|
|
69
|
+
if (typeof attachment === "string") {
|
|
70
|
+
filenames.add(attachment);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Ignore malformed lines. A later replay will rewrite them.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return filenames;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pruneUnreferencedProjectedAttachments(
|
|
82
|
+
attachDir: string,
|
|
83
|
+
messagesPath: string,
|
|
84
|
+
): void {
|
|
85
|
+
if (!existsSync(attachDir)) return;
|
|
86
|
+
|
|
87
|
+
const referenced = getProjectedAttachmentFilenames(messagesPath);
|
|
88
|
+
for (const entry of readdirSync(attachDir)) {
|
|
89
|
+
if (referenced.has(entry)) continue;
|
|
90
|
+
rmSync(join(attachDir, entry), { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rebuild the conversation disk view for all persisted conversations.
|
|
96
|
+
*
|
|
97
|
+
* Conversations are processed by ascending createdAt so replay ordering is
|
|
98
|
+
* stable and deterministic across runs.
|
|
99
|
+
*/
|
|
100
|
+
export function rebuildConversationDiskViewFromDb(): void {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
|
|
103
|
+
const allConversations = db
|
|
104
|
+
.select()
|
|
105
|
+
.from(conversations)
|
|
106
|
+
.orderBy(asc(conversations.createdAt))
|
|
107
|
+
.all();
|
|
108
|
+
|
|
109
|
+
const total = allConversations.length;
|
|
110
|
+
let processed = 0;
|
|
111
|
+
|
|
112
|
+
for (const conv of allConversations) {
|
|
113
|
+
const {
|
|
114
|
+
canonicalDirPath,
|
|
115
|
+
legacyDirPath,
|
|
116
|
+
resolvedDirPath: dirPath,
|
|
117
|
+
} = resolveConversationDirectoryPaths(conv.id, conv.createdAt);
|
|
118
|
+
const metaPath = join(dirPath, "meta.json");
|
|
119
|
+
const messagesPath = join(dirPath, "messages.jsonl");
|
|
120
|
+
const attachDir = join(dirPath, "attachments");
|
|
121
|
+
|
|
122
|
+
// Check if already migrated (idempotent)
|
|
123
|
+
if (existsSync(metaPath) && hasExpectedDiskViewArtifacts(conv, dirPath)) {
|
|
124
|
+
// Prefer the timestamp-first canonical directory whenever both sibling
|
|
125
|
+
// directories exist and the canonical projection is complete.
|
|
126
|
+
convergeDualConversationDirsToCanonical(
|
|
127
|
+
conv,
|
|
128
|
+
canonicalDirPath,
|
|
129
|
+
legacyDirPath,
|
|
130
|
+
);
|
|
131
|
+
processed++;
|
|
132
|
+
if (processed % 50 === 0) {
|
|
133
|
+
log.info(`Backfilled ${processed}/${total} conversations to disk`);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Create dir + meta.json (initConversationDir sets updatedAt = createdAt)
|
|
139
|
+
initConversationDir(conv);
|
|
140
|
+
|
|
141
|
+
// Clear stale data from any previous interrupted run so append-only
|
|
142
|
+
// syncMessageToDisk calls below don't produce duplicates.
|
|
143
|
+
if (existsSync(messagesPath)) {
|
|
144
|
+
rmSync(messagesPath, { force: true });
|
|
145
|
+
}
|
|
146
|
+
writeFileSync(messagesPath, "");
|
|
147
|
+
|
|
148
|
+
// Preserve already materialized attachment files across repair replay.
|
|
149
|
+
// Some rows have data_base64 compacted away and only retain their
|
|
150
|
+
// conversation-scoped file_path, so removing attachments/ here would
|
|
151
|
+
// make the content unrecoverable.
|
|
152
|
+
mkdirSync(attachDir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
// Query all messages for this conversation and sync each to disk
|
|
155
|
+
const convMessages = db
|
|
156
|
+
.select()
|
|
157
|
+
.from(messages)
|
|
158
|
+
.where(eq(messages.conversationId, conv.id))
|
|
159
|
+
.orderBy(asc(messages.createdAt))
|
|
160
|
+
.all();
|
|
161
|
+
|
|
162
|
+
for (const msg of convMessages) {
|
|
163
|
+
syncMessageToDisk(conv.id, msg.id, conv.createdAt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Write the real updatedAt only AFTER all messages are synced so the
|
|
167
|
+
// idempotency check won't skip a conversation with incomplete messages
|
|
168
|
+
// if the migration is interrupted mid-loop.
|
|
169
|
+
updateMetaFile(conv);
|
|
170
|
+
pruneUnreferencedProjectedAttachments(attachDir, messagesPath);
|
|
171
|
+
convergeDualConversationDirsToCanonical(
|
|
172
|
+
conv,
|
|
173
|
+
canonicalDirPath,
|
|
174
|
+
legacyDirPath,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
processed++;
|
|
178
|
+
if (processed % 50 === 0) {
|
|
179
|
+
log.info(`Backfilled ${processed}/${total} conversations to disk`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (total > 0) {
|
|
184
|
+
log.info(`Backfilled ${processed}/${total} conversations to disk`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -5,6 +5,11 @@ import { extractCollectUsageDataMigration } from "./004-extract-collect-usage-da
|
|
|
5
5
|
import { addSendDiagnosticsMigration } from "./005-add-send-diagnostics.js";
|
|
6
6
|
import { servicesConfigMigration } from "./006-services-config.js";
|
|
7
7
|
import { webSearchProviderRenameMigration } from "./007-web-search-provider-rename.js";
|
|
8
|
+
import { voiceTimeoutAndMaxStepsMigration } from "./008-voice-timeout-and-max-steps.js";
|
|
9
|
+
import { backfillConversationDiskViewMigration } from "./009-backfill-conversation-disk-view.js";
|
|
10
|
+
import { appDirRenameMigration } from "./010-app-dir-rename.js";
|
|
11
|
+
import { renameConversationDiskViewDirsMigration } from "./012-rename-conversation-disk-view-dirs.js";
|
|
12
|
+
import { repairConversationDiskViewMigration } from "./013-repair-conversation-disk-view.js";
|
|
8
13
|
import type { WorkspaceMigration } from "./types.js";
|
|
9
14
|
|
|
10
15
|
/**
|
|
@@ -19,4 +24,9 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
19
24
|
addSendDiagnosticsMigration,
|
|
20
25
|
servicesConfigMigration,
|
|
21
26
|
webSearchProviderRenameMigration,
|
|
27
|
+
voiceTimeoutAndMaxStepsMigration,
|
|
28
|
+
backfillConversationDiskViewMigration,
|
|
29
|
+
appDirRenameMigration,
|
|
30
|
+
renameConversationDiskViewDirsMigration,
|
|
31
|
+
repairConversationDiskViewMigration,
|
|
22
32
|
];
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { TopLevelSnapshot } from "./top-level-scanner.js";
|
|
2
2
|
|
|
3
|
+
export interface WorkspaceTopLevelRenderOptions {
|
|
4
|
+
currentConversationPath?: string | null;
|
|
5
|
+
currentConversationAttachmentsPath?: string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* Render a workspace top-level snapshot into a compact XML-like block
|
|
5
10
|
* suitable for injection into user messages.
|
|
@@ -8,11 +13,18 @@ import type { TopLevelSnapshot } from "./top-level-scanner.js";
|
|
|
8
13
|
*/
|
|
9
14
|
export function renderWorkspaceTopLevelContext(
|
|
10
15
|
snapshot: TopLevelSnapshot,
|
|
16
|
+
options: WorkspaceTopLevelRenderOptions = {},
|
|
11
17
|
): string {
|
|
12
18
|
const lines: string[] = ["<workspace_top_level>"];
|
|
13
19
|
lines.push(`Root: ${snapshot.rootPath}`);
|
|
14
20
|
lines.push(`Directories: ${snapshot.directories.join(", ")}`);
|
|
15
21
|
lines.push(`Files: ${snapshot.files.join(", ")}`);
|
|
22
|
+
if (options.currentConversationPath) {
|
|
23
|
+
lines.push(`Current conversation folder: ${options.currentConversationPath}`);
|
|
24
|
+
}
|
|
25
|
+
if (options.currentConversationAttachmentsPath) {
|
|
26
|
+
lines.push(`Attachment files: ${options.currentConversationAttachmentsPath}`);
|
|
27
|
+
}
|
|
16
28
|
if (snapshot.truncated) {
|
|
17
29
|
lines.push("(list truncated — more entries exist)");
|
|
18
30
|
}
|