@vellumai/assistant 0.5.1 → 0.5.3
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 +163 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/docs/skills.md +100 -0
- 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 +156 -5
- package/src/__tests__/conversation-agent-loop.test.ts +297 -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-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- 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-wipe.test.ts +226 -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__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -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__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -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-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +7 -7
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- 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__/parse-identity-fields.test.ts +129 -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-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- 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__/vellum-self-knowledge-inline-command.test.ts +320 -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/SKILL.md +1 -1
- 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 +24 -0
- package/src/config/loader.ts +65 -0
- package/src/config/raw-config-utils.ts +58 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +20 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/services.ts +8 -6
- package/src/config/skills.ts +50 -4
- 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 +54 -8
- package/src/daemon/conversation-agent-loop.ts +127 -20
- 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 +50 -16
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +22 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +170 -24
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +69 -30
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +156 -0
- package/src/daemon/handlers/config-model.ts +62 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +115 -65
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +18 -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/followups/followup-store.ts +47 -1
- 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/archive-store.ts +400 -0
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +591 -8
- 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 +40 -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 +114 -21
- 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 +2 -4
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -0
- 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/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/schema/oauth.ts +6 -0
- package/src/memory/search/semantic.ts +17 -4
- 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 +99 -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 +160 -14
- package/src/permissions/defaults.ts +14 -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 +74 -0
- package/src/runtime/routes/conversation-query-routes.ts +187 -10
- package/src/runtime/routes/conversation-routes.ts +269 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1212 -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 +94 -5
- 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 +30 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +30 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/tasks/task-store.ts +43 -1
- 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/permission-checker.ts +8 -1
- 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/skills/load.ts +140 -6
- 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 +24 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- 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/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
- 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 +11 -1
- 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
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
asc,
|
|
6
|
+
count,
|
|
7
|
+
desc,
|
|
8
|
+
eq,
|
|
9
|
+
gt,
|
|
10
|
+
inArray,
|
|
11
|
+
isNull,
|
|
12
|
+
lte,
|
|
13
|
+
sql,
|
|
14
|
+
} from "drizzle-orm";
|
|
2
15
|
import { v4 as uuid } from "uuid";
|
|
3
16
|
import { z } from "zod";
|
|
4
17
|
|
|
@@ -7,10 +20,24 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
|
|
|
7
20
|
import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from "../channels/types.js";
|
|
8
21
|
import { getConfig } from "../config/loader.js";
|
|
9
22
|
import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
|
|
23
|
+
import { UserError } from "../util/errors.js";
|
|
10
24
|
import { getLogger } from "../util/logger.js";
|
|
25
|
+
import { getConversationsDir } from "../util/platform.js";
|
|
11
26
|
import { createRowMapper } from "../util/row-mapper.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
27
|
+
import {
|
|
28
|
+
deleteOrphanAttachments,
|
|
29
|
+
linkAttachmentToMessage,
|
|
30
|
+
} from "./attachments-store.js";
|
|
31
|
+
import {
|
|
32
|
+
projectAssistantMessage,
|
|
33
|
+
seedForkedConversationAttention,
|
|
34
|
+
} from "./conversation-attention-store.js";
|
|
35
|
+
import {
|
|
36
|
+
initConversationDir,
|
|
37
|
+
removeConversationDir,
|
|
38
|
+
syncMessageToDisk,
|
|
39
|
+
updateMetaFile,
|
|
40
|
+
} from "./conversation-disk-view.js";
|
|
14
41
|
import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
|
|
15
42
|
import { getDb, rawAll, rawExec, rawGet, rawRun } from "./db.js";
|
|
16
43
|
import { indexMessageNow } from "./indexer.js";
|
|
@@ -18,6 +45,7 @@ import { enqueueMemoryJob } from "./jobs-store.js";
|
|
|
18
45
|
import {
|
|
19
46
|
channelInboundEvents,
|
|
20
47
|
conversations,
|
|
48
|
+
conversationStarters,
|
|
21
49
|
llmRequestLogs,
|
|
22
50
|
memoryEmbeddings,
|
|
23
51
|
memoryItems,
|
|
@@ -47,6 +75,9 @@ const subagentNotificationSchema = z.object({
|
|
|
47
75
|
conversationId: z.string().optional(),
|
|
48
76
|
});
|
|
49
77
|
|
|
78
|
+
export const PRIVATE_CONVERSATION_FORK_ERROR =
|
|
79
|
+
"Private conversations cannot be forked";
|
|
80
|
+
|
|
50
81
|
export const messageMetadataSchema = z
|
|
51
82
|
.object({
|
|
52
83
|
userMessageChannel: channelIdSchema.optional(),
|
|
@@ -67,11 +98,42 @@ export const messageMetadataSchema = z
|
|
|
67
98
|
provenanceGuardianExternalUserId: z.string().optional(),
|
|
68
99
|
provenanceRequesterIdentifier: z.string().optional(),
|
|
69
100
|
automated: z.boolean().optional(),
|
|
101
|
+
forkSourceMessageId: z.string().optional(),
|
|
102
|
+
/** Image source paths from desktop attachments, keyed by filename. */
|
|
103
|
+
imageSourcePaths: z.record(z.string(), z.string()).optional(),
|
|
70
104
|
})
|
|
71
105
|
.passthrough();
|
|
72
106
|
|
|
73
107
|
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
|
|
74
108
|
|
|
109
|
+
function cloneForkMessageMetadata(
|
|
110
|
+
metadata: string | null,
|
|
111
|
+
sourceMessageId: string,
|
|
112
|
+
): string {
|
|
113
|
+
if (!metadata) {
|
|
114
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const parsed = JSON.parse(metadata);
|
|
119
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
120
|
+
const sourceRecord = parsed as Record<string, unknown>;
|
|
121
|
+
const forkSourceMessageId =
|
|
122
|
+
typeof sourceRecord.forkSourceMessageId === "string"
|
|
123
|
+
? sourceRecord.forkSourceMessageId
|
|
124
|
+
: sourceMessageId;
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
...sourceRecord,
|
|
127
|
+
forkSourceMessageId,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// Fall through to source-only metadata.
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
135
|
+
}
|
|
136
|
+
|
|
75
137
|
/**
|
|
76
138
|
* Extract provenance metadata fields from a TrustContext.
|
|
77
139
|
* When no guardian context is provided, defaults to 'unknown' because the
|
|
@@ -106,8 +168,13 @@ export interface ConversationRow {
|
|
|
106
168
|
memoryScopeId: string;
|
|
107
169
|
originChannel: string | null;
|
|
108
170
|
originInterface: string | null;
|
|
171
|
+
forkParentConversationId: string | null;
|
|
172
|
+
forkParentMessageId: string | null;
|
|
109
173
|
isAutoTitle: number;
|
|
110
174
|
scheduleJobId: string | null;
|
|
175
|
+
memoryReducedThroughMessageId: string | null;
|
|
176
|
+
memoryDirtyTailSinceMessageId: string | null;
|
|
177
|
+
memoryLastReducedAt: number | null;
|
|
111
178
|
}
|
|
112
179
|
|
|
113
180
|
export const parseConversation = createRowMapper<
|
|
@@ -129,8 +196,13 @@ export const parseConversation = createRowMapper<
|
|
|
129
196
|
memoryScopeId: "memoryScopeId",
|
|
130
197
|
originChannel: "originChannel",
|
|
131
198
|
originInterface: "originInterface",
|
|
199
|
+
forkParentConversationId: "forkParentConversationId",
|
|
200
|
+
forkParentMessageId: "forkParentMessageId",
|
|
132
201
|
isAutoTitle: "isAutoTitle",
|
|
133
202
|
scheduleJobId: "scheduleJobId",
|
|
203
|
+
memoryReducedThroughMessageId: "memoryReducedThroughMessageId",
|
|
204
|
+
memoryDirtyTailSinceMessageId: "memoryDirtyTailSinceMessageId",
|
|
205
|
+
memoryLastReducedAt: "memoryLastReducedAt",
|
|
134
206
|
});
|
|
135
207
|
|
|
136
208
|
export interface MessageRow {
|
|
@@ -232,6 +304,8 @@ export function createConversation(
|
|
|
232
304
|
}
|
|
233
305
|
}
|
|
234
306
|
|
|
307
|
+
initConversationDir({ ...conversation, originChannel: null });
|
|
308
|
+
|
|
235
309
|
return conversation;
|
|
236
310
|
}
|
|
237
311
|
|
|
@@ -258,6 +332,213 @@ export function getConversationMemoryScopeId(conversationId: string): string {
|
|
|
258
332
|
return conv?.memoryScopeId ?? "default";
|
|
259
333
|
}
|
|
260
334
|
|
|
335
|
+
export function forkConversation(params: {
|
|
336
|
+
conversationId: string;
|
|
337
|
+
throughMessageId?: string;
|
|
338
|
+
}): ConversationRow {
|
|
339
|
+
const { conversationId, throughMessageId } = params;
|
|
340
|
+
const db = getDb();
|
|
341
|
+
const sourceConversation = getConversation(conversationId);
|
|
342
|
+
|
|
343
|
+
if (!sourceConversation) {
|
|
344
|
+
throw new UserError(`Conversation ${conversationId} not found`);
|
|
345
|
+
}
|
|
346
|
+
if (sourceConversation.conversationType === "private") {
|
|
347
|
+
throw new UserError(PRIVATE_CONVERSATION_FORK_ERROR);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const sourceMessages = getMessages(conversationId);
|
|
351
|
+
|
|
352
|
+
if (sourceMessages.length === 0) {
|
|
353
|
+
throw new UserError(
|
|
354
|
+
`Conversation ${conversationId} has no persisted messages to fork`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const copyBoundaryIndex =
|
|
359
|
+
throughMessageId == null
|
|
360
|
+
? sourceMessages.length - 1
|
|
361
|
+
: sourceMessages.findIndex((message) => message.id === throughMessageId);
|
|
362
|
+
|
|
363
|
+
if (throughMessageId != null && copyBoundaryIndex === -1) {
|
|
364
|
+
throw new UserError(
|
|
365
|
+
`Message ${throughMessageId} does not belong to conversation ${conversationId}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const visibleWindowStartIndex = Math.max(
|
|
370
|
+
0,
|
|
371
|
+
Math.min(
|
|
372
|
+
sourceConversation.contextCompactedMessageCount,
|
|
373
|
+
sourceMessages.length,
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
const preserveSourceCompactionState =
|
|
377
|
+
copyBoundaryIndex >= visibleWindowStartIndex;
|
|
378
|
+
|
|
379
|
+
const messagesToCopy =
|
|
380
|
+
copyBoundaryIndex >= 0
|
|
381
|
+
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
382
|
+
: ([] as MessageRow[]);
|
|
383
|
+
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
384
|
+
const forkTitle = `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
385
|
+
|
|
386
|
+
// Collect disk-sync work to run after the transaction commits.
|
|
387
|
+
const diskSyncQueue: Array<{
|
|
388
|
+
conversationId: string;
|
|
389
|
+
messageId: string;
|
|
390
|
+
createdAt: number;
|
|
391
|
+
}> = [];
|
|
392
|
+
|
|
393
|
+
// Wrap all DB mutations in a single transaction so a mid-flight failure
|
|
394
|
+
// rolls back cleanly instead of leaving a partial fork. Helper functions
|
|
395
|
+
// (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
|
|
396
|
+
// use the same underlying bun:sqlite connection, so their writes participate
|
|
397
|
+
// in this transaction automatically.
|
|
398
|
+
const forkedConversation = db.transaction(() => {
|
|
399
|
+
const fc = createConversation({
|
|
400
|
+
title: forkTitle,
|
|
401
|
+
conversationType: "standard",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
db.update(conversations)
|
|
405
|
+
.set({
|
|
406
|
+
forkParentConversationId: sourceConversation.id,
|
|
407
|
+
forkParentMessageId,
|
|
408
|
+
contextSummary: preserveSourceCompactionState
|
|
409
|
+
? sourceConversation.contextSummary
|
|
410
|
+
: null,
|
|
411
|
+
contextCompactedMessageCount: preserveSourceCompactionState
|
|
412
|
+
? sourceConversation.contextCompactedMessageCount
|
|
413
|
+
: 0,
|
|
414
|
+
contextCompactedAt: preserveSourceCompactionState
|
|
415
|
+
? sourceConversation.contextCompactedAt
|
|
416
|
+
: null,
|
|
417
|
+
})
|
|
418
|
+
.where(eq(conversations.id, fc.id))
|
|
419
|
+
.run();
|
|
420
|
+
|
|
421
|
+
const forkedMessageIds = new Map<string, string>();
|
|
422
|
+
let latestForkedAssistant: {
|
|
423
|
+
messageId: string;
|
|
424
|
+
messageAt: number;
|
|
425
|
+
} | null = null;
|
|
426
|
+
|
|
427
|
+
for (const message of messagesToCopy) {
|
|
428
|
+
const forkedMessageId = uuid();
|
|
429
|
+
db.insert(messages)
|
|
430
|
+
.values({
|
|
431
|
+
id: forkedMessageId,
|
|
432
|
+
conversationId: fc.id,
|
|
433
|
+
role: message.role,
|
|
434
|
+
content: message.content,
|
|
435
|
+
createdAt: message.createdAt,
|
|
436
|
+
metadata: cloneForkMessageMetadata(message.metadata, message.id),
|
|
437
|
+
})
|
|
438
|
+
.run();
|
|
439
|
+
forkedMessageIds.set(message.id, forkedMessageId);
|
|
440
|
+
|
|
441
|
+
if (message.role === "assistant") {
|
|
442
|
+
latestForkedAssistant = {
|
|
443
|
+
messageId: forkedMessageId,
|
|
444
|
+
messageAt: message.createdAt,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const attachmentIdMap = new Map<string, string>();
|
|
450
|
+
for (const message of messagesToCopy) {
|
|
451
|
+
const forkedMessageId = forkedMessageIds.get(message.id);
|
|
452
|
+
if (!forkedMessageId) continue;
|
|
453
|
+
|
|
454
|
+
const attachmentLinks = db
|
|
455
|
+
.select({
|
|
456
|
+
attachmentId: messageAttachments.attachmentId,
|
|
457
|
+
position: messageAttachments.position,
|
|
458
|
+
})
|
|
459
|
+
.from(messageAttachments)
|
|
460
|
+
.where(eq(messageAttachments.messageId, message.id))
|
|
461
|
+
.orderBy(messageAttachments.position)
|
|
462
|
+
.all();
|
|
463
|
+
const uncachedAttachmentLinks = attachmentLinks.filter(
|
|
464
|
+
(link) => !attachmentIdMap.has(link.attachmentId),
|
|
465
|
+
);
|
|
466
|
+
const stagingMessageId =
|
|
467
|
+
uncachedAttachmentLinks.length > 0 ? uuid() : null;
|
|
468
|
+
|
|
469
|
+
if (stagingMessageId) {
|
|
470
|
+
db.insert(messages)
|
|
471
|
+
.values({
|
|
472
|
+
id: stagingMessageId,
|
|
473
|
+
conversationId: fc.id,
|
|
474
|
+
role: message.role,
|
|
475
|
+
content: "",
|
|
476
|
+
createdAt: message.createdAt,
|
|
477
|
+
metadata: null,
|
|
478
|
+
})
|
|
479
|
+
.run();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const link of attachmentLinks) {
|
|
483
|
+
const cachedAttachmentId = attachmentIdMap.get(link.attachmentId);
|
|
484
|
+
if (cachedAttachmentId) {
|
|
485
|
+
db.insert(messageAttachments)
|
|
486
|
+
.values({
|
|
487
|
+
id: uuid(),
|
|
488
|
+
messageId: forkedMessageId,
|
|
489
|
+
attachmentId: cachedAttachmentId,
|
|
490
|
+
position: link.position,
|
|
491
|
+
createdAt: Date.now(),
|
|
492
|
+
})
|
|
493
|
+
.run();
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const scopedAttachmentId = linkAttachmentToMessage(
|
|
498
|
+
stagingMessageId ?? forkedMessageId,
|
|
499
|
+
link.attachmentId,
|
|
500
|
+
link.position,
|
|
501
|
+
);
|
|
502
|
+
attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (stagingMessageId) {
|
|
506
|
+
relinkAttachments([stagingMessageId], forkedMessageId);
|
|
507
|
+
db.delete(messages).where(eq(messages.id, stagingMessageId)).run();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
diskSyncQueue.push({
|
|
511
|
+
conversationId: fc.id,
|
|
512
|
+
messageId: forkedMessageId,
|
|
513
|
+
createdAt: fc.createdAt,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
seedForkedConversationAttention({
|
|
518
|
+
conversationId: fc.id,
|
|
519
|
+
latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
|
|
520
|
+
latestAssistantMessageAt: latestForkedAssistant?.messageAt ?? null,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return fc;
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Disk-view sync runs after commit — file I/O is idempotent and
|
|
527
|
+
// conversation deletion cleans up orphaned directories.
|
|
528
|
+
for (const entry of diskSyncQueue) {
|
|
529
|
+
syncMessageToDisk(entry.conversationId, entry.messageId, entry.createdAt);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const persistedFork = getConversation(forkedConversation.id);
|
|
533
|
+
if (!persistedFork) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`Failed to load forked conversation ${forkedConversation.id} after creation`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return persistedFork;
|
|
540
|
+
}
|
|
541
|
+
|
|
261
542
|
/**
|
|
262
543
|
* Delete a conversation and all its messages, cleaning up orphaned memory
|
|
263
544
|
* artifacts (items, embeddings). Returns segment and orphaned item IDs so
|
|
@@ -265,7 +546,18 @@ export function getConversationMemoryScopeId(conversationId: string): string {
|
|
|
265
546
|
*/
|
|
266
547
|
export function deleteConversation(id: string): DeletedMemoryIds {
|
|
267
548
|
const db = getDb();
|
|
268
|
-
const result: DeletedMemoryIds = {
|
|
549
|
+
const result: DeletedMemoryIds = {
|
|
550
|
+
segmentIds: [],
|
|
551
|
+
orphanedItemIds: [],
|
|
552
|
+
deletedSummaryIds: [],
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Capture createdAt before the transaction deletes the row — needed to
|
|
556
|
+
// resolve the conversation's disk-view directory path after deletion.
|
|
557
|
+
const convBeforeDelete = getConversation(id);
|
|
558
|
+
const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
|
|
559
|
+
const memoryScopeId = convBeforeDelete?.memoryScopeId;
|
|
560
|
+
const isPrivateScope = memoryScopeId?.startsWith("private:") ?? false;
|
|
269
561
|
|
|
270
562
|
db.transaction((tx) => {
|
|
271
563
|
// Collect all message IDs for this conversation.
|
|
@@ -354,9 +646,73 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
354
646
|
.run();
|
|
355
647
|
}
|
|
356
648
|
|
|
649
|
+
if (isPrivateScope && memoryScopeId) {
|
|
650
|
+
// Sweep remaining memory items with this private scopeId.
|
|
651
|
+
const scopeItems = tx
|
|
652
|
+
.select({ id: memoryItems.id })
|
|
653
|
+
.from(memoryItems)
|
|
654
|
+
.where(eq(memoryItems.scopeId, memoryScopeId))
|
|
655
|
+
.all();
|
|
656
|
+
const alreadyDeleted = new Set(result.orphanedItemIds);
|
|
657
|
+
const scopeItemIds = scopeItems
|
|
658
|
+
.map((r) => r.id)
|
|
659
|
+
.filter((id) => !alreadyDeleted.has(id));
|
|
660
|
+
|
|
661
|
+
if (scopeItemIds.length > 0) {
|
|
662
|
+
tx.delete(memoryEmbeddings)
|
|
663
|
+
.where(
|
|
664
|
+
and(
|
|
665
|
+
eq(memoryEmbeddings.targetType, "item"),
|
|
666
|
+
inArray(memoryEmbeddings.targetId, scopeItemIds),
|
|
667
|
+
),
|
|
668
|
+
)
|
|
669
|
+
.run();
|
|
670
|
+
tx.delete(memoryItemSources)
|
|
671
|
+
.where(inArray(memoryItemSources.memoryItemId, scopeItemIds))
|
|
672
|
+
.run();
|
|
673
|
+
tx.delete(memoryItems)
|
|
674
|
+
.where(inArray(memoryItems.id, scopeItemIds))
|
|
675
|
+
.run();
|
|
676
|
+
result.orphanedItemIds.push(...scopeItemIds);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Sweep memory summaries with this private scopeId.
|
|
680
|
+
const scopeSummaries = tx
|
|
681
|
+
.select({ id: memorySummaries.id })
|
|
682
|
+
.from(memorySummaries)
|
|
683
|
+
.where(eq(memorySummaries.scopeId, memoryScopeId))
|
|
684
|
+
.all();
|
|
685
|
+
const scopeSummaryIds = scopeSummaries.map((r) => r.id);
|
|
686
|
+
|
|
687
|
+
if (scopeSummaryIds.length > 0) {
|
|
688
|
+
tx.delete(memoryEmbeddings)
|
|
689
|
+
.where(
|
|
690
|
+
and(
|
|
691
|
+
eq(memoryEmbeddings.targetType, "summary"),
|
|
692
|
+
inArray(memoryEmbeddings.targetId, scopeSummaryIds),
|
|
693
|
+
),
|
|
694
|
+
)
|
|
695
|
+
.run();
|
|
696
|
+
tx.delete(memorySummaries)
|
|
697
|
+
.where(inArray(memorySummaries.id, scopeSummaryIds))
|
|
698
|
+
.run();
|
|
699
|
+
result.deletedSummaryIds.push(...scopeSummaryIds);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Sweep conversation starters with this private scopeId.
|
|
703
|
+
tx.delete(conversationStarters)
|
|
704
|
+
.where(eq(conversationStarters.scopeId, memoryScopeId))
|
|
705
|
+
.run();
|
|
706
|
+
}
|
|
707
|
+
|
|
357
708
|
tx.delete(conversations).where(eq(conversations.id, id)).run();
|
|
358
709
|
});
|
|
359
710
|
|
|
711
|
+
// Remove the conversation's disk-view directory after the DB transaction
|
|
712
|
+
if (createdAtForDiskCleanup != null) {
|
|
713
|
+
removeConversationDir(id, createdAtForDiskCleanup);
|
|
714
|
+
}
|
|
715
|
+
|
|
360
716
|
return result;
|
|
361
717
|
}
|
|
362
718
|
|
|
@@ -541,7 +897,10 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
541
897
|
return {
|
|
542
898
|
...deletedMemoryIds,
|
|
543
899
|
unsupersededItemIds,
|
|
544
|
-
deletedSummaryIds
|
|
900
|
+
deletedSummaryIds: [
|
|
901
|
+
...deletedSummaryIds,
|
|
902
|
+
...deletedMemoryIds.deletedSummaryIds,
|
|
903
|
+
],
|
|
545
904
|
cancelledJobCount,
|
|
546
905
|
};
|
|
547
906
|
}
|
|
@@ -563,16 +922,25 @@ export function purgePrivateConversations(): {
|
|
|
563
922
|
.all();
|
|
564
923
|
|
|
565
924
|
if (privateConvs.length === 0) {
|
|
566
|
-
return {
|
|
925
|
+
return {
|
|
926
|
+
count: 0,
|
|
927
|
+
deletedMemory: {
|
|
928
|
+
segmentIds: [],
|
|
929
|
+
orphanedItemIds: [],
|
|
930
|
+
deletedSummaryIds: [],
|
|
931
|
+
},
|
|
932
|
+
};
|
|
567
933
|
}
|
|
568
934
|
|
|
569
935
|
const allSegmentIds: string[] = [];
|
|
570
936
|
const allOrphanedItemIds: string[] = [];
|
|
937
|
+
const allDeletedSummaryIds: string[] = [];
|
|
571
938
|
|
|
572
939
|
for (const conv of privateConvs) {
|
|
573
940
|
const deleted = deleteConversation(conv.id);
|
|
574
941
|
allSegmentIds.push(...deleted.segmentIds);
|
|
575
942
|
allOrphanedItemIds.push(...deleted.orphanedItemIds);
|
|
943
|
+
allDeletedSummaryIds.push(...deleted.deletedSummaryIds);
|
|
576
944
|
}
|
|
577
945
|
|
|
578
946
|
return {
|
|
@@ -580,6 +948,7 @@ export function purgePrivateConversations(): {
|
|
|
580
948
|
deletedMemory: {
|
|
581
949
|
segmentIds: allSegmentIds,
|
|
582
950
|
orphanedItemIds: allOrphanedItemIds,
|
|
951
|
+
deletedSummaryIds: allDeletedSummaryIds,
|
|
583
952
|
},
|
|
584
953
|
};
|
|
585
954
|
}
|
|
@@ -662,6 +1031,13 @@ export async function addMessage(
|
|
|
662
1031
|
throw err;
|
|
663
1032
|
}
|
|
664
1033
|
}
|
|
1034
|
+
|
|
1035
|
+
// Mark the conversation dirty for delayed memory reduction. This runs
|
|
1036
|
+
// after the insert transaction succeeds so the reducer knows which
|
|
1037
|
+
// conversations have unprocessed messages. The helper preserves the
|
|
1038
|
+
// earliest unreduced boundary (no-op when already dirty).
|
|
1039
|
+
markConversationMemoryDirty(conversationId, messageId);
|
|
1040
|
+
|
|
665
1041
|
const message = {
|
|
666
1042
|
id: messageId,
|
|
667
1043
|
conversationId,
|
|
@@ -759,6 +1135,12 @@ export function updateConversationTitle(
|
|
|
759
1135
|
const set: Record<string, unknown> = { title, updatedAt: Date.now() };
|
|
760
1136
|
if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
|
|
761
1137
|
db.update(conversations).set(set).where(eq(conversations.id, id)).run();
|
|
1138
|
+
|
|
1139
|
+
// Update disk view meta.json with the new title
|
|
1140
|
+
const conv = getConversation(id);
|
|
1141
|
+
if (conv) {
|
|
1142
|
+
updateMetaFile(conv);
|
|
1143
|
+
}
|
|
762
1144
|
}
|
|
763
1145
|
|
|
764
1146
|
export function updateConversationUsage(
|
|
@@ -864,6 +1246,14 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
864
1246
|
);
|
|
865
1247
|
}
|
|
866
1248
|
|
|
1249
|
+
// Clear the disk-view conversations directory and recreate it empty
|
|
1250
|
+
try {
|
|
1251
|
+
rmSync(getConversationsDir(), { recursive: true, force: true });
|
|
1252
|
+
mkdirSync(getConversationsDir(), { recursive: true });
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
log.warn({ err }, "clearAll: failed to reset conversations directory");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
867
1257
|
return { conversations: convCount, messages: msgCount };
|
|
868
1258
|
}
|
|
869
1259
|
|
|
@@ -942,11 +1332,11 @@ export function deleteLastExchange(conversationId: string): number {
|
|
|
942
1332
|
export interface DeletedMemoryIds {
|
|
943
1333
|
segmentIds: string[];
|
|
944
1334
|
orphanedItemIds: string[];
|
|
1335
|
+
deletedSummaryIds: string[];
|
|
945
1336
|
}
|
|
946
1337
|
|
|
947
1338
|
export interface WipeConversationResult extends DeletedMemoryIds {
|
|
948
1339
|
unsupersededItemIds: string[];
|
|
949
|
-
deletedSummaryIds: string[];
|
|
950
1340
|
cancelledJobCount: number;
|
|
951
1341
|
}
|
|
952
1342
|
|
|
@@ -1012,7 +1402,11 @@ export function relinkAttachments(
|
|
|
1012
1402
|
*/
|
|
1013
1403
|
export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
1014
1404
|
const db = getDb();
|
|
1015
|
-
const result: DeletedMemoryIds = {
|
|
1405
|
+
const result: DeletedMemoryIds = {
|
|
1406
|
+
segmentIds: [],
|
|
1407
|
+
orphanedItemIds: [],
|
|
1408
|
+
deletedSummaryIds: [],
|
|
1409
|
+
};
|
|
1016
1410
|
|
|
1017
1411
|
// Collect attachment IDs linked to this message before cascade-delete
|
|
1018
1412
|
// so we can scope orphan cleanup to only those candidates.
|
|
@@ -1100,6 +1494,28 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
|
1100
1494
|
return result;
|
|
1101
1495
|
}
|
|
1102
1496
|
|
|
1497
|
+
/**
|
|
1498
|
+
* Mark a conversation as having unreduced messages starting from the given
|
|
1499
|
+
* message. Sets `memoryDirtyTailSinceMessageId` only when it is currently
|
|
1500
|
+
* null so the earliest unreduced boundary is preserved across multiple
|
|
1501
|
+
* messages — later messages must not clobber the original dirty marker.
|
|
1502
|
+
*/
|
|
1503
|
+
export function markConversationMemoryDirty(
|
|
1504
|
+
conversationId: string,
|
|
1505
|
+
messageId: string,
|
|
1506
|
+
): void {
|
|
1507
|
+
const db = getDb();
|
|
1508
|
+
db.update(conversations)
|
|
1509
|
+
.set({ memoryDirtyTailSinceMessageId: messageId })
|
|
1510
|
+
.where(
|
|
1511
|
+
and(
|
|
1512
|
+
eq(conversations.id, conversationId),
|
|
1513
|
+
isNull(conversations.memoryDirtyTailSinceMessageId),
|
|
1514
|
+
),
|
|
1515
|
+
)
|
|
1516
|
+
.run();
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1103
1519
|
export function setConversationOriginChannelIfUnset(
|
|
1104
1520
|
conversationId: string,
|
|
1105
1521
|
channel: ChannelId,
|
|
@@ -1232,3 +1648,170 @@ export function getDisplayMetaForConversations(
|
|
|
1232
1648
|
}
|
|
1233
1649
|
return result;
|
|
1234
1650
|
}
|
|
1651
|
+
|
|
1652
|
+
// ── Turn boundary resolution ─────────────────────────────────────────
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* Returns `true` if a message is a tool-result user message — i.e. its
|
|
1656
|
+
* role is "user" and its content is a JSON array where every block has
|
|
1657
|
+
* `type === "tool_result"`. These synthetic user messages are injected
|
|
1658
|
+
* between assistant messages within a single agent turn and should NOT
|
|
1659
|
+
* be treated as turn boundaries.
|
|
1660
|
+
*/
|
|
1661
|
+
function isToolResultMessage(role: string, content: string): boolean {
|
|
1662
|
+
if (role !== "user") return false;
|
|
1663
|
+
try {
|
|
1664
|
+
const parsed = JSON.parse(content);
|
|
1665
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return false;
|
|
1666
|
+
return parsed.every(
|
|
1667
|
+
(block: unknown) =>
|
|
1668
|
+
block != null &&
|
|
1669
|
+
typeof block === "object" &&
|
|
1670
|
+
(block as Record<string, unknown>).type === "tool_result",
|
|
1671
|
+
);
|
|
1672
|
+
} catch {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Resolve all assistant message IDs that belong to the same agent turn
|
|
1679
|
+
* as the given `messageId`. A "turn" is bounded by:
|
|
1680
|
+
* - The start of the conversation, or
|
|
1681
|
+
* - A user message whose content is NOT a tool_result array.
|
|
1682
|
+
*
|
|
1683
|
+
* Within a multi-step agent loop, the pattern is:
|
|
1684
|
+
* user msg → assistant A1 → user (tool_result) → assistant A2 → ...
|
|
1685
|
+
* All assistant messages from A1 through the queried message (and beyond,
|
|
1686
|
+
* up to the next real user message) are part of the same turn.
|
|
1687
|
+
*
|
|
1688
|
+
* Returns `[messageId]` as a fallback if the message is not found,
|
|
1689
|
+
* preserving backward compatibility for callers.
|
|
1690
|
+
*/
|
|
1691
|
+
export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
1692
|
+
const db = getDb();
|
|
1693
|
+
|
|
1694
|
+
// Look up the target message to get its conversationId and createdAt.
|
|
1695
|
+
const target = getMessageById(messageId);
|
|
1696
|
+
if (!target) return [messageId];
|
|
1697
|
+
|
|
1698
|
+
// Walk backward from the target message to find the turn boundary.
|
|
1699
|
+
// Limit to 50 rows — sufficient for even aggressive tool-use loops.
|
|
1700
|
+
const backwardRows = db
|
|
1701
|
+
.select({
|
|
1702
|
+
id: messages.id,
|
|
1703
|
+
role: messages.role,
|
|
1704
|
+
content: messages.content,
|
|
1705
|
+
createdAt: messages.createdAt,
|
|
1706
|
+
})
|
|
1707
|
+
.from(messages)
|
|
1708
|
+
.where(
|
|
1709
|
+
and(
|
|
1710
|
+
eq(messages.conversationId, target.conversationId),
|
|
1711
|
+
lte(messages.createdAt, target.createdAt),
|
|
1712
|
+
),
|
|
1713
|
+
)
|
|
1714
|
+
.orderBy(desc(messages.createdAt))
|
|
1715
|
+
.limit(50)
|
|
1716
|
+
.all();
|
|
1717
|
+
|
|
1718
|
+
const assistantIds: string[] = [];
|
|
1719
|
+
let boundaryCreatedAt: number | null = null;
|
|
1720
|
+
|
|
1721
|
+
for (const row of backwardRows) {
|
|
1722
|
+
if (row.role === "assistant") {
|
|
1723
|
+
assistantIds.push(row.id);
|
|
1724
|
+
} else if (row.role === "user") {
|
|
1725
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
1726
|
+
// Tool-result user message — still within the same turn, continue.
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
// Real user message — this is the turn boundary.
|
|
1730
|
+
boundaryCreatedAt = row.createdAt;
|
|
1731
|
+
break;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Walk forward from the target to collect any later assistant messages
|
|
1736
|
+
// still within the same turn (e.g. when querying an intermediate
|
|
1737
|
+
// message like A1 in a multi-step turn A1 → tool_result → A2).
|
|
1738
|
+
const forwardRows = db
|
|
1739
|
+
.select({
|
|
1740
|
+
id: messages.id,
|
|
1741
|
+
role: messages.role,
|
|
1742
|
+
content: messages.content,
|
|
1743
|
+
createdAt: messages.createdAt,
|
|
1744
|
+
})
|
|
1745
|
+
.from(messages)
|
|
1746
|
+
.where(
|
|
1747
|
+
and(
|
|
1748
|
+
eq(messages.conversationId, target.conversationId),
|
|
1749
|
+
gt(messages.createdAt, target.createdAt),
|
|
1750
|
+
),
|
|
1751
|
+
)
|
|
1752
|
+
.orderBy(asc(messages.createdAt))
|
|
1753
|
+
.limit(50)
|
|
1754
|
+
.all();
|
|
1755
|
+
|
|
1756
|
+
for (const row of forwardRows) {
|
|
1757
|
+
if (row.role === "assistant") {
|
|
1758
|
+
if (!assistantIds.includes(row.id)) {
|
|
1759
|
+
assistantIds.push(row.id);
|
|
1760
|
+
}
|
|
1761
|
+
} else if (row.role === "user") {
|
|
1762
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
1763
|
+
// Tool-result user message — still within the same turn.
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
// Real user message — end of the turn.
|
|
1767
|
+
break;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Also query forward from the backward-walk boundary to pick up any
|
|
1772
|
+
// assistant messages between the boundary and the target that may have
|
|
1773
|
+
// been missed (e.g. due to the 50-row limit in the backward walk).
|
|
1774
|
+
if (boundaryCreatedAt != null) {
|
|
1775
|
+
const gapRows = db
|
|
1776
|
+
.select({
|
|
1777
|
+
id: messages.id,
|
|
1778
|
+
role: messages.role,
|
|
1779
|
+
createdAt: messages.createdAt,
|
|
1780
|
+
})
|
|
1781
|
+
.from(messages)
|
|
1782
|
+
.where(
|
|
1783
|
+
and(
|
|
1784
|
+
eq(messages.conversationId, target.conversationId),
|
|
1785
|
+
gt(messages.createdAt, boundaryCreatedAt),
|
|
1786
|
+
lte(messages.createdAt, target.createdAt),
|
|
1787
|
+
),
|
|
1788
|
+
)
|
|
1789
|
+
.orderBy(asc(messages.createdAt))
|
|
1790
|
+
.all();
|
|
1791
|
+
|
|
1792
|
+
for (const row of gapRows) {
|
|
1793
|
+
if (row.role === "assistant" && !assistantIds.includes(row.id)) {
|
|
1794
|
+
assistantIds.push(row.id);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// Sort by createdAt to ensure stable ordering.
|
|
1800
|
+
// Re-fetch createdAt for all collected IDs so the sort is accurate.
|
|
1801
|
+
if (assistantIds.length <= 1) return assistantIds;
|
|
1802
|
+
|
|
1803
|
+
const idSet = new Set(assistantIds);
|
|
1804
|
+
const sorted = db
|
|
1805
|
+
.select({ id: messages.id, createdAt: messages.createdAt })
|
|
1806
|
+
.from(messages)
|
|
1807
|
+
.where(
|
|
1808
|
+
and(
|
|
1809
|
+
eq(messages.conversationId, target.conversationId),
|
|
1810
|
+
inArray(messages.id, [...idSet]),
|
|
1811
|
+
),
|
|
1812
|
+
)
|
|
1813
|
+
.orderBy(asc(messages.createdAt))
|
|
1814
|
+
.all();
|
|
1815
|
+
|
|
1816
|
+
return sorted.map((r) => r.id);
|
|
1817
|
+
}
|