@vellumai/assistant 0.5.16 → 0.6.0
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 +1 -1
- package/Dockerfile +0 -3
- package/knip.json +2 -1
- package/openapi.yaml +660 -80
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +68 -0
- package/src/__tests__/agent-loop.test.ts +0 -32
- package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
- package/src/__tests__/anthropic-provider.test.ts +57 -3
- package/src/__tests__/app-compiler.test.ts +120 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/call-conversation-messages.test.ts +2 -6
- package/src/__tests__/call-domain.test.ts +2 -6
- package/src/__tests__/call-pointer-messages.test.ts +2 -14
- package/src/__tests__/call-recovery.test.ts +2 -6
- package/src/__tests__/call-routes-http.test.ts +2 -6
- package/src/__tests__/call-store.test.ts +2 -6
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
- package/src/__tests__/canonical-guardian-store.test.ts +2 -6
- package/src/__tests__/channel-delivery-store.test.ts +2 -6
- package/src/__tests__/channel-retry-sweep.test.ts +2 -6
- package/src/__tests__/checker.test.ts +25 -3
- package/src/__tests__/clawhub.test.ts +54 -24
- package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
- package/src/__tests__/cli-memory.test.ts +74 -69
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-set-platform-guard.test.ts +302 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
- package/src/__tests__/contacts-tools.test.ts +31 -0
- package/src/__tests__/context-overflow-reducer.test.ts +86 -0
- package/src/__tests__/context-token-estimator.test.ts +175 -10
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
- package/src/__tests__/conversation-agent-loop.test.ts +9 -0
- package/src/__tests__/conversation-attachments.test.ts +2 -6
- package/src/__tests__/conversation-attention-store.test.ts +2 -6
- package/src/__tests__/conversation-clear-safety.test.ts +2 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
- package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
- package/src/__tests__/conversation-disk-view.test.ts +2 -6
- package/src/__tests__/conversation-error.test.ts +33 -2
- package/src/__tests__/conversation-fork-crud.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
- package/src/__tests__/conversation-media-retry.test.ts +91 -0
- package/src/__tests__/conversation-starter-routes.test.ts +20 -11
- package/src/__tests__/conversation-store.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +2 -6
- package/src/__tests__/conversation-wipe.test.ts +11 -408
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +2 -0
- package/src/__tests__/followup-tools.test.ts +2 -6
- package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
- package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
- package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
- package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
- package/src/__tests__/guardian-action-store.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
- package/src/__tests__/guardian-dispatch.test.ts +2 -6
- package/src/__tests__/guardian-grant-minting.test.ts +2 -14
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
- package/src/__tests__/guardian-routing-state.test.ts +2 -6
- package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
- package/src/__tests__/injection-block.test.ts +154 -0
- package/src/__tests__/install-meta.test.ts +506 -0
- package/src/__tests__/install-skill-routing.test.ts +292 -0
- package/src/__tests__/invite-redemption-service.test.ts +2 -6
- package/src/__tests__/invite-routes-http.test.ts +2 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
- package/src/__tests__/list-messages-attachments.test.ts +2 -6
- package/src/__tests__/llm-context-route-provider.test.ts +2 -6
- package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
- package/src/__tests__/llm-usage-store.test.ts +2 -6
- package/src/__tests__/log-export-workspace.test.ts +2 -6
- package/src/__tests__/managed-store.test.ts +38 -11
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
- package/src/__tests__/memory-recall-log-store.test.ts +2 -6
- package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
- package/src/__tests__/non-member-access-request.test.ts +2 -6
- package/src/__tests__/notification-guardian-path.test.ts +2 -6
- package/src/__tests__/oauth-cli.test.ts +364 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
- package/src/__tests__/outlook-attachments.test.ts +301 -0
- package/src/__tests__/outlook-automation-tools.test.ts +425 -0
- package/src/__tests__/outlook-categories.test.ts +212 -0
- package/src/__tests__/outlook-client-automation.test.ts +246 -0
- package/src/__tests__/outlook-compose-tools.test.ts +325 -0
- package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
- package/src/__tests__/outlook-email-watcher.test.ts +322 -0
- package/src/__tests__/outlook-follow-up.test.ts +196 -0
- package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
- package/src/__tests__/outlook-trash.test.ts +77 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
- package/src/__tests__/platform-callback-registration.test.ts +4 -4
- package/src/__tests__/playbook-execution.test.ts +76 -80
- package/src/__tests__/playbook-tools.test.ts +5 -7
- package/src/__tests__/provider-error-scenarios.test.ts +21 -0
- package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/require-fresh-approval.test.ts +64 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
- package/src/__tests__/runtime-events-sse.test.ts +2 -6
- package/src/__tests__/schedule-store.test.ts +2 -6
- package/src/__tests__/schedule-tools.test.ts +2 -6
- package/src/__tests__/scheduler-recurrence.test.ts +1 -5
- package/src/__tests__/scoped-approval-grants.test.ts +2 -6
- package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
- package/src/__tests__/search-skills-unified.test.ts +421 -0
- package/src/__tests__/secret-onetime-send.test.ts +2 -0
- package/src/__tests__/send-endpoint-busy.test.ts +2 -6
- package/src/__tests__/sequence-store.test.ts +2 -6
- package/src/__tests__/server-history-render.test.ts +2 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
- package/src/__tests__/skill-feature-flags.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
- package/src/__tests__/skill-memory.test.ts +140 -98
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +1 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -6
- package/src/__tests__/task-compiler.test.ts +2 -6
- package/src/__tests__/task-management-tools.test.ts +2 -6
- package/src/__tests__/task-memory-cleanup.test.ts +173 -229
- package/src/__tests__/task-runner.test.ts +2 -6
- package/src/__tests__/task-scheduler.test.ts +2 -6
- package/src/__tests__/test-preload.ts +3 -0
- package/src/__tests__/tool-approval-handler.test.ts +2 -6
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
- package/src/__tests__/trust-store.test.ts +1 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
- package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
- package/src/__tests__/trusted-contact-verification.test.ts +2 -6
- package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
- package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
- package/src/__tests__/usage-routes.test.ts +2 -6
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-invite-redemption.test.ts +2 -6
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
- package/src/__tests__/voice-session-bridge.test.ts +2 -6
- package/src/__tests__/volume-security-guard.test.ts +2 -0
- package/src/__tests__/workspace-lifecycle.test.ts +29 -1
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
- package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
- package/src/__tests__/workspace-policy.test.ts +1 -1
- package/src/agent/attachments.ts +7 -2
- package/src/agent/image-optimize.ts +165 -0
- package/src/agent/loop.ts +1 -15
- package/src/bundler/app-compiler.ts +179 -2
- package/src/bundler/package-resolver.ts +3 -5
- package/src/cli/__tests__/notifications.test.ts +1 -2
- package/src/cli/cli-memory.ts +67 -64
- package/src/cli/commands/avatar.ts +3 -3
- package/src/cli/commands/config.ts +26 -13
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/memory.ts +41 -55
- package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
- package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
- package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
- package/src/cli/commands/oauth/connect.ts +11 -6
- package/src/cli/commands/oauth/mode.ts +7 -0
- package/src/cli/commands/oauth/shared.ts +39 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
- package/src/cli/commands/platform/index.ts +16 -16
- package/src/cli/commands/skills.ts +88 -16
- package/src/cli/commands/trust.ts +2 -2
- package/src/cli/lib/daemon-credential-client.ts +2 -3
- package/src/config/bundled-skills/acp/TOOLS.json +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +0 -1
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
- package/src/config/bundled-skills/gmail/SKILL.md +2 -10
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
- package/src/config/bundled-skills/messaging/SKILL.md +10 -18
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
- package/src/config/bundled-skills/outlook/SKILL.md +189 -0
- package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
- package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
- package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
- package/src/config/bundled-skills/slack/SKILL.md +1 -7
- package/src/config/bundled-tool-registry.ts +56 -4
- package/src/config/env-registry.ts +15 -8
- package/src/config/feature-flag-registry.json +21 -124
- package/src/config/schemas/platform.ts +8 -0
- package/src/config/schemas/timeouts.ts +1 -1
- package/src/config/skills.ts +18 -7
- package/src/context/token-estimator.ts +25 -18
- package/src/context/window-manager.ts +6 -2
- package/src/credential-execution/process-manager.ts +3 -1
- package/src/daemon/context-overflow-reducer.ts +46 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
- package/src/daemon/conversation-agent-loop.ts +96 -61
- package/src/daemon/conversation-error.ts +31 -8
- package/src/daemon/conversation-lifecycle.ts +33 -0
- package/src/daemon/conversation-media-retry.ts +85 -7
- package/src/daemon/conversation-notifiers.ts +4 -1
- package/src/daemon/conversation-runtime-assembly.ts +5 -0
- package/src/daemon/conversation.ts +41 -2
- package/src/daemon/daemon-control.ts +8 -2
- package/src/daemon/handlers/shared.ts +22 -12
- package/src/daemon/handlers/skills.ts +416 -202
- package/src/daemon/lifecycle.ts +40 -1
- package/src/daemon/main.ts +5 -1
- package/src/daemon/message-types/conversations.ts +4 -1
- package/src/daemon/message-types/messages.ts +3 -1
- package/src/daemon/message-types/skills.ts +97 -36
- package/src/daemon/providers-setup.ts +5 -0
- package/src/daemon/server.ts +11 -2
- package/src/daemon/tool-side-effects.ts +27 -5
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/hooks/cli.ts +2 -2
- package/src/hooks/runner.ts +15 -38
- package/src/inbound/platform-callback-registration.ts +14 -14
- package/src/memory/admin.ts +11 -45
- package/src/memory/conversation-bootstrap.ts +2 -0
- package/src/memory/conversation-crud.ts +242 -348
- package/src/memory/conversation-group-migration.ts +157 -0
- package/src/memory/conversation-queries.ts +4 -2
- package/src/memory/db-init.ts +30 -3
- package/src/memory/embed.ts +73 -0
- package/src/memory/embedding-backend.ts +8 -14
- package/src/memory/embedding-runtime-manager.ts +12 -114
- package/src/memory/fingerprint.ts +2 -2
- package/src/memory/graph/bootstrap.ts +512 -0
- package/src/memory/graph/capability-seed.ts +297 -0
- package/src/memory/graph/consolidation.ts +691 -0
- package/src/memory/graph/conversation-graph-memory.ts +630 -0
- package/src/memory/graph/decay.test.ts +208 -0
- package/src/memory/graph/decay.ts +195 -0
- package/src/memory/graph/extraction-job.ts +69 -0
- package/src/memory/graph/extraction.test.ts +936 -0
- package/src/memory/graph/extraction.ts +1254 -0
- package/src/memory/graph/graph-search.ts +266 -0
- package/src/memory/graph/image-ref-utils.ts +29 -0
- package/src/memory/graph/injection.test.ts +513 -0
- package/src/memory/graph/injection.ts +439 -0
- package/src/memory/graph/inspect.ts +534 -0
- package/src/memory/graph/narrative.ts +267 -0
- package/src/memory/graph/pattern-scan.ts +269 -0
- package/src/memory/graph/retriever.ts +1008 -0
- package/src/memory/graph/scoring.test.ts +548 -0
- package/src/memory/graph/scoring.ts +232 -0
- package/src/memory/graph/serendipity.ts +65 -0
- package/src/memory/graph/store.test.ts +1050 -0
- package/src/memory/graph/store.ts +699 -0
- package/src/memory/graph/tool-handlers.ts +426 -0
- package/src/memory/graph/tools.ts +141 -0
- package/src/memory/graph/triggers.test.ts +487 -0
- package/src/memory/graph/triggers.ts +223 -0
- package/src/memory/graph/types.ts +271 -0
- package/src/memory/group-crud.ts +191 -0
- package/src/memory/indexer.ts +37 -19
- package/src/memory/job-handlers/cleanup.ts +0 -53
- package/src/memory/job-handlers/conversation-starters.ts +91 -53
- package/src/memory/job-handlers/embedding.ts +5 -31
- package/src/memory/job-handlers/index-maintenance.ts +23 -11
- package/src/memory/job-handlers/summarization.ts +32 -17
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +50 -70
- package/src/memory/jobs-worker.ts +147 -112
- package/src/memory/message-content.ts +1 -0
- package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
- package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
- package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
- package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-client.ts +44 -17
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/memory-graph.ts +139 -0
- package/src/memory/search/semantic.ts +47 -91
- package/src/memory/task-memory-cleanup.ts +28 -50
- package/src/messaging/providers/outlook/adapter.ts +8 -1
- package/src/messaging/providers/outlook/client.ts +299 -0
- package/src/messaging/providers/outlook/types.ts +118 -0
- package/src/notifications/adapters/macos.ts +1 -0
- package/src/notifications/copy-composer.ts +9 -0
- package/src/notifications/signal.ts +16 -0
- package/src/oauth/seed-providers.ts +2 -1
- package/src/permissions/checker.ts +24 -3
- package/src/permissions/defaults.ts +4 -4
- package/src/permissions/workspace-policy.ts +1 -1
- package/src/playbooks/playbook-compiler.ts +19 -18
- package/src/playbooks/types.ts +4 -3
- package/src/prompts/system-prompt.ts +3 -29
- package/src/providers/anthropic/client.ts +47 -19
- package/src/providers/gemini/client.ts +1 -1
- package/src/providers/openai/client.ts +1 -1
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +19 -3
- package/src/runtime/actor-trust-resolver.ts +5 -1
- package/src/runtime/auth/route-policy.ts +7 -0
- package/src/runtime/guardian-reply-router.ts +5 -1
- package/src/runtime/http-server.ts +23 -3
- package/src/runtime/middleware/auth.ts +20 -0
- package/src/runtime/routes/attachment-routes.test.ts +106 -0
- package/src/runtime/routes/attachment-routes.ts +106 -16
- package/src/runtime/routes/brain-graph-routes.ts +21 -22
- package/src/runtime/routes/btw-routes.ts +8 -0
- package/src/runtime/routes/conversation-management-routes.ts +2 -0
- package/src/runtime/routes/conversation-starter-routes.ts +2 -2
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/global-search-routes.ts +21 -19
- package/src/runtime/routes/group-routes.ts +207 -0
- package/src/runtime/routes/guardian-action-routes.ts +21 -10
- package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
- package/src/runtime/routes/inbound-message-handler.ts +19 -0
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
- package/src/runtime/routes/memory-item-routes.test.ts +2 -14
- package/src/runtime/routes/memory-item-routes.ts +341 -388
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/skills-routes.ts +103 -37
- package/src/runtime/routes/work-items-routes.test.ts +2 -6
- package/src/schedule/scheduler.ts +8 -1
- package/src/security/oauth2.ts +1 -1
- package/src/security/secure-keys.ts +4 -8
- package/src/shared/provider-env-vars.ts +19 -0
- package/src/skills/catalog-cache.ts +5 -0
- package/src/skills/catalog-install.ts +15 -14
- package/src/skills/clawhub.ts +134 -154
- package/src/skills/install-meta.ts +208 -0
- package/src/skills/managed-store.ts +27 -16
- package/src/skills/skill-memory.ts +152 -77
- package/src/skills/skillssh-registry.ts +19 -17
- package/src/tasks/task-runner.ts +3 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
- package/src/tools/browser/runtime-check.ts +3 -1
- package/src/tools/memory/register.ts +63 -46
- package/src/tools/permission-checker.ts +7 -1
- package/src/tools/shared/filesystem/image-read.ts +22 -85
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/tool-manifest.ts +3 -3
- package/src/util/browser.ts +25 -10
- package/src/util/bun-runtime.ts +172 -0
- package/src/watcher/providers/outlook-calendar.ts +343 -0
- package/src/watcher/providers/outlook.ts +198 -0
- package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
- package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
- package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/__tests__/context-memory-e2e.test.ts +0 -415
- package/src/__tests__/journal-context.test.ts +0 -268
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
- package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
- package/src/__tests__/memory-query-builder.test.ts +0 -59
- package/src/__tests__/memory-recall-quality.test.ts +0 -1046
- package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
- package/src/__tests__/memory-regressions.test.ts +0 -3696
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
- package/src/daemon/conversation-memory.ts +0 -207
- package/src/memory/conversation-starters-cadence.ts +0 -74
- package/src/memory/items-extractor.ts +0 -860
- package/src/memory/job-handlers/batch-extraction.ts +0 -753
- package/src/memory/job-handlers/extraction.ts +0 -40
- package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
- package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
- package/src/memory/journal-memory.ts +0 -224
- package/src/memory/query-builder.ts +0 -47
- package/src/memory/query-expansion.ts +0 -83
- package/src/memory/retriever.test.ts +0 -1592
- package/src/memory/retriever.ts +0 -1331
- package/src/memory/search/formatting.test.ts +0 -140
- package/src/memory/search/formatting.ts +0 -262
- package/src/memory/search/mmr.ts +0 -139
- package/src/memory/search/ranking.ts +0 -15
- package/src/memory/search/staleness.ts +0 -40
- package/src/memory/search/tier-classifier.ts +0 -18
- package/src/memory/search/types.ts +0 -121
- package/src/prompts/journal-context.ts +0 -154
- package/src/tools/memory/definitions.ts +0 -69
- package/src/tools/memory/handlers.test.ts +0 -562
- package/src/tools/memory/handlers.ts +0 -434
|
@@ -41,17 +41,15 @@ import {
|
|
|
41
41
|
updateMetaFile,
|
|
42
42
|
} from "./conversation-disk-view.js";
|
|
43
43
|
import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
|
|
44
|
-
import {
|
|
44
|
+
import { ensureGroupMigration } from "./conversation-group-migration.js";
|
|
45
|
+
import { getDb, rawExec, rawGet, rawRun } from "./db.js";
|
|
45
46
|
import { indexMessageNow } from "./indexer.js";
|
|
46
|
-
import { enqueueMemoryJob } from "./jobs-store.js";
|
|
47
47
|
import {
|
|
48
48
|
channelInboundEvents,
|
|
49
49
|
conversations,
|
|
50
50
|
conversationStarters,
|
|
51
51
|
llmRequestLogs,
|
|
52
52
|
memoryEmbeddings,
|
|
53
|
-
memoryItems,
|
|
54
|
-
memoryItemSources,
|
|
55
53
|
memorySegments,
|
|
56
54
|
memorySummaries,
|
|
57
55
|
messageAttachments,
|
|
@@ -110,7 +108,7 @@ export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
|
|
|
110
108
|
|
|
111
109
|
function cloneForkMessageMetadata(
|
|
112
110
|
metadata: string | null,
|
|
113
|
-
sourceMessageId: string
|
|
111
|
+
sourceMessageId: string
|
|
114
112
|
): string {
|
|
115
113
|
if (!metadata) {
|
|
116
114
|
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
@@ -143,7 +141,7 @@ function cloneForkMessageMetadata(
|
|
|
143
141
|
* callers with actual guardian trust should always supply a real context.
|
|
144
142
|
*/
|
|
145
143
|
export function provenanceFromTrustContext(
|
|
146
|
-
ctx: TrustContext | null | undefined
|
|
144
|
+
ctx: TrustContext | null | undefined
|
|
147
145
|
): Record<string, unknown> {
|
|
148
146
|
if (!ctx) return { provenanceTrustClass: "unknown" };
|
|
149
147
|
return {
|
|
@@ -244,19 +242,27 @@ export function createConversation(
|
|
|
244
242
|
conversationType?: "standard" | "private" | "background";
|
|
245
243
|
source?: string;
|
|
246
244
|
scheduleJobId?: string;
|
|
247
|
-
|
|
245
|
+
groupId?: string;
|
|
246
|
+
}
|
|
248
247
|
) {
|
|
249
248
|
const db = getDb();
|
|
250
249
|
const now = Date.now();
|
|
251
250
|
const opts =
|
|
252
251
|
typeof titleOrOpts === "string"
|
|
253
252
|
? { title: titleOrOpts }
|
|
254
|
-
:
|
|
253
|
+
: titleOrOpts ?? {};
|
|
255
254
|
const conversationType = opts.conversationType ?? "standard";
|
|
256
255
|
const source = opts.source ?? "user";
|
|
256
|
+
const groupId = opts.groupId;
|
|
257
257
|
const id = uuid();
|
|
258
258
|
const memoryScopeId =
|
|
259
259
|
conversationType === "private" ? `private:${id}` : "default";
|
|
260
|
+
|
|
261
|
+
// Ensure group_id column exists for deterministic schema readiness,
|
|
262
|
+
// even when this conversation has no groupId (a subsequent query or
|
|
263
|
+
// reorder may reference the column).
|
|
264
|
+
ensureGroupMigration();
|
|
265
|
+
|
|
260
266
|
const conversation = {
|
|
261
267
|
id,
|
|
262
268
|
title: opts.title ?? null,
|
|
@@ -276,6 +282,12 @@ export function createConversation(
|
|
|
276
282
|
|
|
277
283
|
// Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
|
|
278
284
|
// contention can cause the first attempt to fail even under normal load.
|
|
285
|
+
// INSERT and group_id UPDATE are retried independently so a transient failure
|
|
286
|
+
// on the UPDATE doesn't re-execute the already-succeeded INSERT (which would
|
|
287
|
+
// hit a unique constraint violation).
|
|
288
|
+
// No explicit BEGIN/COMMIT here — callers that need atomicity (e.g.
|
|
289
|
+
// forkConversation) wrap in their own transaction, and nesting raw BEGIN
|
|
290
|
+
// inside Drizzle's db.transaction() would crash SQLite.
|
|
279
291
|
const MAX_RETRIES = 3;
|
|
280
292
|
for (let attempt = 0; ; attempt++) {
|
|
281
293
|
try {
|
|
@@ -289,10 +301,8 @@ export function createConversation(
|
|
|
289
301
|
) {
|
|
290
302
|
log.warn(
|
|
291
303
|
{ attempt, conversationId: id, code },
|
|
292
|
-
"createConversation: transient
|
|
304
|
+
"createConversation: INSERT transient error, retrying"
|
|
293
305
|
);
|
|
294
|
-
// Synchronous sleep — createConversation is synchronous and the
|
|
295
|
-
// retry window is short (50-150ms), so Bun.sleepSync is appropriate.
|
|
296
306
|
Bun.sleepSync(50 * (attempt + 1));
|
|
297
307
|
continue;
|
|
298
308
|
}
|
|
@@ -300,6 +310,35 @@ export function createConversation(
|
|
|
300
310
|
}
|
|
301
311
|
}
|
|
302
312
|
|
|
313
|
+
// group_id is NOT in the Drizzle schema (raw-query-only pattern).
|
|
314
|
+
// Set via raw SQL after the INSERT succeeds.
|
|
315
|
+
if (groupId) {
|
|
316
|
+
for (let attempt = 0; ; attempt++) {
|
|
317
|
+
try {
|
|
318
|
+
rawRun(
|
|
319
|
+
"UPDATE conversations SET group_id = ? WHERE id = ?",
|
|
320
|
+
groupId,
|
|
321
|
+
id
|
|
322
|
+
);
|
|
323
|
+
break;
|
|
324
|
+
} catch (err) {
|
|
325
|
+
const code = (err as { code?: string }).code ?? "";
|
|
326
|
+
if (
|
|
327
|
+
attempt < MAX_RETRIES &&
|
|
328
|
+
(code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_IOERR"))
|
|
329
|
+
) {
|
|
330
|
+
log.warn(
|
|
331
|
+
{ attempt, conversationId: id, code },
|
|
332
|
+
"createConversation: group_id UPDATE transient error, retrying"
|
|
333
|
+
);
|
|
334
|
+
Bun.sleepSync(50 * (attempt + 1));
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
303
342
|
initConversationDir({ ...conversation, originChannel: null });
|
|
304
343
|
|
|
305
344
|
return conversation;
|
|
@@ -321,18 +360,18 @@ export function getConversation(id: string): ConversationRow | null {
|
|
|
321
360
|
* (i.e. no other conversations still reference it).
|
|
322
361
|
*/
|
|
323
362
|
export function countConversationsByScheduleJobId(
|
|
324
|
-
scheduleJobId: string
|
|
363
|
+
scheduleJobId: string
|
|
325
364
|
): number {
|
|
326
365
|
return (
|
|
327
366
|
rawGet<{ c: number }>(
|
|
328
367
|
"SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
|
|
329
|
-
scheduleJobId
|
|
368
|
+
scheduleJobId
|
|
330
369
|
)?.c ?? 0
|
|
331
370
|
);
|
|
332
371
|
}
|
|
333
372
|
|
|
334
373
|
export function getConversationType(
|
|
335
|
-
conversationId: string
|
|
374
|
+
conversationId: string
|
|
336
375
|
): "standard" | "private" {
|
|
337
376
|
const conv = getConversation(conversationId);
|
|
338
377
|
const raw = conv?.conversationType;
|
|
@@ -344,6 +383,20 @@ export function getConversationMemoryScopeId(conversationId: string): string {
|
|
|
344
383
|
return conv?.memoryScopeId ?? "default";
|
|
345
384
|
}
|
|
346
385
|
|
|
386
|
+
/**
|
|
387
|
+
* Fetch group_id for a conversation via raw SQL. group_id is NOT in the
|
|
388
|
+
* Drizzle schema (raw-query-only pattern), so ConversationRow doesn't
|
|
389
|
+
* include it. This helper is used by forkConversation to inherit group_id.
|
|
390
|
+
*/
|
|
391
|
+
export function getConversationGroupId(conversationId: string): string | null {
|
|
392
|
+
ensureGroupMigration();
|
|
393
|
+
const row = rawGet<{ group_id: string | null }>(
|
|
394
|
+
"SELECT group_id FROM conversations WHERE id = ?",
|
|
395
|
+
conversationId
|
|
396
|
+
);
|
|
397
|
+
return row?.group_id ?? null;
|
|
398
|
+
}
|
|
399
|
+
|
|
347
400
|
export function forkConversation(params: {
|
|
348
401
|
conversationId: string;
|
|
349
402
|
throughMessageId?: string;
|
|
@@ -363,7 +416,7 @@ export function forkConversation(params: {
|
|
|
363
416
|
|
|
364
417
|
if (sourceMessages.length === 0) {
|
|
365
418
|
throw new UserError(
|
|
366
|
-
`Conversation ${conversationId} has no persisted messages to fork
|
|
419
|
+
`Conversation ${conversationId} has no persisted messages to fork`
|
|
367
420
|
);
|
|
368
421
|
}
|
|
369
422
|
|
|
@@ -374,7 +427,7 @@ export function forkConversation(params: {
|
|
|
374
427
|
|
|
375
428
|
if (throughMessageId != null && copyBoundaryIndex === -1) {
|
|
376
429
|
throw new UserError(
|
|
377
|
-
`Message ${throughMessageId} does not belong to conversation ${conversationId}
|
|
430
|
+
`Message ${throughMessageId} does not belong to conversation ${conversationId}`
|
|
378
431
|
);
|
|
379
432
|
}
|
|
380
433
|
|
|
@@ -382,8 +435,8 @@ export function forkConversation(params: {
|
|
|
382
435
|
0,
|
|
383
436
|
Math.min(
|
|
384
437
|
sourceConversation.contextCompactedMessageCount,
|
|
385
|
-
sourceMessages.length
|
|
386
|
-
)
|
|
438
|
+
sourceMessages.length
|
|
439
|
+
)
|
|
387
440
|
);
|
|
388
441
|
const preserveSourceCompactionState =
|
|
389
442
|
copyBoundaryIndex >= visibleWindowStartIndex;
|
|
@@ -407,10 +460,14 @@ export function forkConversation(params: {
|
|
|
407
460
|
// (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
|
|
408
461
|
// use the same underlying bun:sqlite connection, so their writes participate
|
|
409
462
|
// in this transaction automatically.
|
|
463
|
+
// Inherit group_id from parent via raw SQL helper (group_id is not in Drizzle schema)
|
|
464
|
+
const parentGroupId = getConversationGroupId(conversationId);
|
|
465
|
+
|
|
410
466
|
const forkedConversation = db.transaction(() => {
|
|
411
467
|
const fc = createConversation({
|
|
412
468
|
title: forkTitle,
|
|
413
469
|
conversationType: "standard",
|
|
470
|
+
groupId: parentGroupId ?? undefined,
|
|
414
471
|
});
|
|
415
472
|
|
|
416
473
|
db.update(conversations)
|
|
@@ -473,7 +530,7 @@ export function forkConversation(params: {
|
|
|
473
530
|
.orderBy(messageAttachments.position)
|
|
474
531
|
.all();
|
|
475
532
|
const uncachedAttachmentLinks = attachmentLinks.filter(
|
|
476
|
-
(link) => !attachmentIdMap.has(link.attachmentId)
|
|
533
|
+
(link) => !attachmentIdMap.has(link.attachmentId)
|
|
477
534
|
);
|
|
478
535
|
const stagingMessageId =
|
|
479
536
|
uncachedAttachmentLinks.length > 0 ? uuid() : null;
|
|
@@ -509,7 +566,7 @@ export function forkConversation(params: {
|
|
|
509
566
|
const scopedAttachmentId = linkAttachmentToMessage(
|
|
510
567
|
stagingMessageId ?? forkedMessageId,
|
|
511
568
|
link.attachmentId,
|
|
512
|
-
link.position
|
|
569
|
+
link.position
|
|
513
570
|
);
|
|
514
571
|
attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
|
|
515
572
|
}
|
|
@@ -544,7 +601,7 @@ export function forkConversation(params: {
|
|
|
544
601
|
const persistedFork = getConversation(forkedConversation.id);
|
|
545
602
|
if (!persistedFork) {
|
|
546
603
|
throw new Error(
|
|
547
|
-
`Failed to load forked conversation ${forkedConversation.id} after creation
|
|
604
|
+
`Failed to load forked conversation ${forkedConversation.id} after creation`
|
|
548
605
|
);
|
|
549
606
|
}
|
|
550
607
|
|
|
@@ -589,16 +646,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
589
646
|
.all();
|
|
590
647
|
result.segmentIds = linkedSegments.map((r) => r.id);
|
|
591
648
|
|
|
592
|
-
// Collect memory item IDs linked to these messages before cascade.
|
|
593
|
-
const linkedItems = tx
|
|
594
|
-
.select({ memoryItemId: memoryItemSources.memoryItemId })
|
|
595
|
-
.from(memoryItemSources)
|
|
596
|
-
.where(inArray(memoryItemSources.messageId, messageIds))
|
|
597
|
-
.all();
|
|
598
|
-
const candidateItemIds = [
|
|
599
|
-
...new Set(linkedItems.map((r) => r.memoryItemId)),
|
|
600
|
-
];
|
|
601
|
-
|
|
602
649
|
// Delete non-cascading tables first.
|
|
603
650
|
tx.delete(llmRequestLogs)
|
|
604
651
|
.where(eq(llmRequestLogs.conversationId, id))
|
|
@@ -606,7 +653,7 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
606
653
|
tx.delete(toolInvocations)
|
|
607
654
|
.where(eq(toolInvocations.conversationId, id))
|
|
608
655
|
.run();
|
|
609
|
-
// Cascade deletes memory_segments,
|
|
656
|
+
// Cascade deletes memory_segments, message_attachments.
|
|
610
657
|
tx.delete(messages).where(eq(messages.conversationId, id)).run();
|
|
611
658
|
|
|
612
659
|
// Clean up segment embeddings.
|
|
@@ -615,39 +662,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
615
662
|
.where(
|
|
616
663
|
and(
|
|
617
664
|
eq(memoryEmbeddings.targetType, "segment"),
|
|
618
|
-
inArray(memoryEmbeddings.targetId, result.segmentIds)
|
|
619
|
-
)
|
|
665
|
+
inArray(memoryEmbeddings.targetId, result.segmentIds)
|
|
666
|
+
)
|
|
620
667
|
)
|
|
621
668
|
.run();
|
|
622
669
|
}
|
|
623
|
-
|
|
624
|
-
// Clean up orphaned memory items whose only sources were in this conversation.
|
|
625
|
-
if (candidateItemIds.length > 0) {
|
|
626
|
-
const surviving = tx
|
|
627
|
-
.select({ memoryItemId: memoryItemSources.memoryItemId })
|
|
628
|
-
.from(memoryItemSources)
|
|
629
|
-
.where(inArray(memoryItemSources.memoryItemId, candidateItemIds))
|
|
630
|
-
.all();
|
|
631
|
-
const survivingIds = new Set(surviving.map((r) => r.memoryItemId));
|
|
632
|
-
const orphanedIds = candidateItemIds.filter(
|
|
633
|
-
(itemId) => !survivingIds.has(itemId),
|
|
634
|
-
);
|
|
635
|
-
result.orphanedItemIds = orphanedIds;
|
|
636
|
-
|
|
637
|
-
if (orphanedIds.length > 0) {
|
|
638
|
-
tx.delete(memoryEmbeddings)
|
|
639
|
-
.where(
|
|
640
|
-
and(
|
|
641
|
-
eq(memoryEmbeddings.targetType, "item"),
|
|
642
|
-
inArray(memoryEmbeddings.targetId, orphanedIds),
|
|
643
|
-
),
|
|
644
|
-
)
|
|
645
|
-
.run();
|
|
646
|
-
tx.delete(memoryItems)
|
|
647
|
-
.where(inArray(memoryItems.id, orphanedIds))
|
|
648
|
-
.run();
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
670
|
} else {
|
|
652
671
|
// No messages — just clean up non-message tables.
|
|
653
672
|
tx.delete(llmRequestLogs)
|
|
@@ -659,35 +678,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
659
678
|
}
|
|
660
679
|
|
|
661
680
|
if (isPrivateScope && memoryScopeId) {
|
|
662
|
-
// Sweep remaining memory items with this private scopeId.
|
|
663
|
-
const scopeItems = tx
|
|
664
|
-
.select({ id: memoryItems.id })
|
|
665
|
-
.from(memoryItems)
|
|
666
|
-
.where(eq(memoryItems.scopeId, memoryScopeId))
|
|
667
|
-
.all();
|
|
668
|
-
const alreadyDeleted = new Set(result.orphanedItemIds);
|
|
669
|
-
const scopeItemIds = scopeItems
|
|
670
|
-
.map((r) => r.id)
|
|
671
|
-
.filter((id) => !alreadyDeleted.has(id));
|
|
672
|
-
|
|
673
|
-
if (scopeItemIds.length > 0) {
|
|
674
|
-
tx.delete(memoryEmbeddings)
|
|
675
|
-
.where(
|
|
676
|
-
and(
|
|
677
|
-
eq(memoryEmbeddings.targetType, "item"),
|
|
678
|
-
inArray(memoryEmbeddings.targetId, scopeItemIds),
|
|
679
|
-
),
|
|
680
|
-
)
|
|
681
|
-
.run();
|
|
682
|
-
tx.delete(memoryItemSources)
|
|
683
|
-
.where(inArray(memoryItemSources.memoryItemId, scopeItemIds))
|
|
684
|
-
.run();
|
|
685
|
-
tx.delete(memoryItems)
|
|
686
|
-
.where(inArray(memoryItems.id, scopeItemIds))
|
|
687
|
-
.run();
|
|
688
|
-
result.orphanedItemIds.push(...scopeItemIds);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
681
|
// Sweep memory summaries with this private scopeId.
|
|
692
682
|
const scopeSummaries = tx
|
|
693
683
|
.select({ id: memorySummaries.id })
|
|
@@ -701,8 +691,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
701
691
|
.where(
|
|
702
692
|
and(
|
|
703
693
|
eq(memoryEmbeddings.targetType, "summary"),
|
|
704
|
-
inArray(memoryEmbeddings.targetId, scopeSummaryIds)
|
|
705
|
-
)
|
|
694
|
+
inArray(memoryEmbeddings.targetId, scopeSummaryIds)
|
|
695
|
+
)
|
|
706
696
|
)
|
|
707
697
|
.run();
|
|
708
698
|
tx.delete(memorySummaries)
|
|
@@ -747,65 +737,6 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
747
737
|
// the cancellation queries join on `messages`).
|
|
748
738
|
const cancelledJobCount = cancelPendingJobsForConversation(id);
|
|
749
739
|
|
|
750
|
-
// Step B — Un-supersede memory items with explicit `supersededBy` links.
|
|
751
|
-
// Find memory items whose `superseded_by` points to an item sourced
|
|
752
|
-
// exclusively from this conversation.
|
|
753
|
-
const explicitSuperseded = rawAll<{ oldItemId: string }>(
|
|
754
|
-
`SELECT DISTINCT mi_old.id AS oldItemId
|
|
755
|
-
FROM memory_items mi_old
|
|
756
|
-
JOIN memory_items mi_new ON mi_old.superseded_by = mi_new.id
|
|
757
|
-
WHERE mi_old.status = 'superseded'
|
|
758
|
-
AND mi_new.id IN (
|
|
759
|
-
SELECT mis.memory_item_id
|
|
760
|
-
FROM memory_item_sources mis
|
|
761
|
-
JOIN messages m ON m.id = mis.message_id
|
|
762
|
-
WHERE m.conversation_id = ?
|
|
763
|
-
)
|
|
764
|
-
AND NOT EXISTS (
|
|
765
|
-
SELECT 1 FROM memory_item_sources mis2
|
|
766
|
-
JOIN messages m2 ON m2.id = mis2.message_id
|
|
767
|
-
WHERE mis2.memory_item_id = mi_new.id
|
|
768
|
-
AND m2.conversation_id != ?
|
|
769
|
-
)
|
|
770
|
-
AND NOT EXISTS (
|
|
771
|
-
SELECT 1 FROM memory_items mi_active
|
|
772
|
-
WHERE mi_active.kind = mi_old.kind
|
|
773
|
-
AND mi_active.subject = mi_old.subject
|
|
774
|
-
AND mi_active.scope_id = mi_old.scope_id
|
|
775
|
-
AND mi_active.status = 'active'
|
|
776
|
-
AND mi_active.id != mi_old.id
|
|
777
|
-
-- Exclude items sourced exclusively from the conversation being
|
|
778
|
-
-- wiped — deleteConversation will remove them, so they should not
|
|
779
|
-
-- block restoration of mi_old.
|
|
780
|
-
AND NOT (
|
|
781
|
-
EXISTS (
|
|
782
|
-
SELECT 1 FROM memory_item_sources mis_a
|
|
783
|
-
JOIN messages m_a ON m_a.id = mis_a.message_id
|
|
784
|
-
WHERE mis_a.memory_item_id = mi_active.id
|
|
785
|
-
AND m_a.conversation_id = ?
|
|
786
|
-
)
|
|
787
|
-
AND NOT EXISTS (
|
|
788
|
-
SELECT 1 FROM memory_item_sources mis_b
|
|
789
|
-
JOIN messages m_b ON m_b.id = mis_b.message_id
|
|
790
|
-
WHERE mis_b.memory_item_id = mi_active.id
|
|
791
|
-
AND m_b.conversation_id != ?
|
|
792
|
-
)
|
|
793
|
-
)
|
|
794
|
-
)`,
|
|
795
|
-
id,
|
|
796
|
-
id,
|
|
797
|
-
id,
|
|
798
|
-
id,
|
|
799
|
-
);
|
|
800
|
-
for (const { oldItemId } of explicitSuperseded) {
|
|
801
|
-
rawRun(
|
|
802
|
-
"UPDATE memory_items SET status = 'active', superseded_by = NULL WHERE id = ?",
|
|
803
|
-
oldItemId,
|
|
804
|
-
);
|
|
805
|
-
enqueueMemoryJob("embed_item", { itemId: oldItemId });
|
|
806
|
-
unsupersededItemIds.push(oldItemId);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
740
|
// Step C — Delete conversation-scoped memory summaries and their embeddings.
|
|
810
741
|
const summaryRows = db
|
|
811
742
|
.select({ id: memorySummaries.id })
|
|
@@ -813,8 +744,8 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
813
744
|
.where(
|
|
814
745
|
and(
|
|
815
746
|
eq(memorySummaries.scope, "conversation"),
|
|
816
|
-
eq(memorySummaries.scopeKey, id)
|
|
817
|
-
)
|
|
747
|
+
eq(memorySummaries.scopeKey, id)
|
|
748
|
+
)
|
|
818
749
|
)
|
|
819
750
|
.all();
|
|
820
751
|
const summaryIds = summaryRows.map((r) => r.id);
|
|
@@ -823,8 +754,8 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
823
754
|
.where(
|
|
824
755
|
and(
|
|
825
756
|
eq(memoryEmbeddings.targetType, "summary"),
|
|
826
|
-
inArray(memoryEmbeddings.targetId, summaryIds)
|
|
827
|
-
)
|
|
757
|
+
inArray(memoryEmbeddings.targetId, summaryIds)
|
|
758
|
+
)
|
|
828
759
|
)
|
|
829
760
|
.run();
|
|
830
761
|
db.delete(memorySummaries)
|
|
@@ -833,79 +764,12 @@ export function wipeConversation(id: string): WipeConversationResult {
|
|
|
833
764
|
}
|
|
834
765
|
deletedSummaryIds.push(...summaryIds);
|
|
835
766
|
|
|
836
|
-
// Step D —
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
// Step D.5 — Collect kind + subject pairs of items that will be orphaned
|
|
840
|
-
// by deleteConversation. These are items sourced from this conversation's
|
|
841
|
-
// messages that have NO sources from any other conversation. We need this
|
|
842
|
-
// before deletion so we can scope Step F to only restore superseded items
|
|
843
|
-
// matching the specific kind + subject pairs that just lost their active
|
|
844
|
-
// replacement.
|
|
845
|
-
const orphanedKindSubjects = rawAll<{ kind: string; subject: string }>(
|
|
846
|
-
`SELECT DISTINCT mi.kind, mi.subject
|
|
847
|
-
FROM memory_items mi
|
|
848
|
-
JOIN memory_item_sources mis ON mis.memory_item_id = mi.id
|
|
849
|
-
JOIN messages m ON m.id = mis.message_id
|
|
850
|
-
WHERE m.conversation_id = ?
|
|
851
|
-
AND NOT EXISTS (
|
|
852
|
-
SELECT 1 FROM memory_item_sources mis2
|
|
853
|
-
JOIN messages m2 ON m2.id = mis2.message_id
|
|
854
|
-
WHERE mis2.memory_item_id = mi.id
|
|
855
|
-
AND m2.conversation_id != ?
|
|
856
|
-
)`,
|
|
857
|
-
id,
|
|
858
|
-
id,
|
|
859
|
-
);
|
|
860
|
-
|
|
861
|
-
// Step E — Delegate to deleteConversation which handles messages (cascade
|
|
862
|
-
// segments, item_sources, attachments), llmRequestLogs, toolInvocations,
|
|
863
|
-
// orphaned memory items + embeddings, and the conversation row.
|
|
767
|
+
// Step D — Delegate to deleteConversation which handles messages (cascade
|
|
768
|
+
// segments, attachments), llmRequestLogs, toolInvocations,
|
|
769
|
+
// embeddings, and the conversation row.
|
|
864
770
|
const deletedMemoryIds = deleteConversation(id);
|
|
865
771
|
|
|
866
|
-
// Step
|
|
867
|
-
// deleteConversation removes superseding items, find superseded items
|
|
868
|
-
// with no supersededBy link where no active item with the same
|
|
869
|
-
// kind + subject + scope_id exists. Scoped to only the kind + subject
|
|
870
|
-
// pairs of items that were just orphaned by deleteConversation, so we
|
|
871
|
-
// don't accidentally restore items superseded by unrelated conversations.
|
|
872
|
-
let orphanedSuperseded: Array<{ id: string }> = [];
|
|
873
|
-
if (orphanedKindSubjects.length > 0) {
|
|
874
|
-
const placeholders = orphanedKindSubjects.map(() => "(?, ?)").join(", ");
|
|
875
|
-
const params: Array<string> = [scopeId];
|
|
876
|
-
for (const { kind, subject } of orphanedKindSubjects) {
|
|
877
|
-
params.push(kind, subject);
|
|
878
|
-
}
|
|
879
|
-
orphanedSuperseded = rawAll<{ id: string }>(
|
|
880
|
-
`SELECT id FROM (
|
|
881
|
-
SELECT id, ROW_NUMBER() OVER (
|
|
882
|
-
PARTITION BY kind, subject, scope_id
|
|
883
|
-
ORDER BY last_seen_at DESC
|
|
884
|
-
) AS rn
|
|
885
|
-
FROM memory_items
|
|
886
|
-
WHERE status = 'superseded'
|
|
887
|
-
AND superseded_by IS NULL
|
|
888
|
-
AND scope_id = ?
|
|
889
|
-
AND (kind, subject) IN (VALUES ${placeholders})
|
|
890
|
-
AND NOT EXISTS (
|
|
891
|
-
SELECT 1 FROM memory_items mi2
|
|
892
|
-
WHERE mi2.kind = memory_items.kind
|
|
893
|
-
AND mi2.subject = memory_items.subject
|
|
894
|
-
AND mi2.scope_id = memory_items.scope_id
|
|
895
|
-
AND mi2.status = 'active'
|
|
896
|
-
AND mi2.id != memory_items.id
|
|
897
|
-
)
|
|
898
|
-
) WHERE rn = 1`,
|
|
899
|
-
...params,
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
for (const { id: itemId } of orphanedSuperseded) {
|
|
903
|
-
rawRun("UPDATE memory_items SET status = 'active' WHERE id = ?", itemId);
|
|
904
|
-
enqueueMemoryJob("embed_item", { itemId });
|
|
905
|
-
unsupersededItemIds.push(itemId);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Step G — Return the combined result.
|
|
772
|
+
// Step E — Return the combined result.
|
|
909
773
|
return {
|
|
910
774
|
...deletedMemoryIds,
|
|
911
775
|
unsupersededItemIds,
|
|
@@ -970,7 +834,7 @@ export async function addMessage(
|
|
|
970
834
|
role: string,
|
|
971
835
|
content: string,
|
|
972
836
|
metadata?: Record<string, unknown>,
|
|
973
|
-
opts?: { skipIndexing?: boolean }
|
|
837
|
+
opts?: { skipIndexing?: boolean }
|
|
974
838
|
) {
|
|
975
839
|
const db = getDb();
|
|
976
840
|
const messageId = uuid();
|
|
@@ -980,7 +844,7 @@ export async function addMessage(
|
|
|
980
844
|
if (!result.success) {
|
|
981
845
|
log.warn(
|
|
982
846
|
{ conversationId, messageId, issues: result.error.issues },
|
|
983
|
-
"Invalid message metadata, storing as-is"
|
|
847
|
+
"Invalid message metadata, storing as-is"
|
|
984
848
|
);
|
|
985
849
|
}
|
|
986
850
|
}
|
|
@@ -1015,8 +879,8 @@ export async function addMessage(
|
|
|
1015
879
|
.where(
|
|
1016
880
|
and(
|
|
1017
881
|
eq(conversations.id, conversationId),
|
|
1018
|
-
isNull(conversations.originChannel)
|
|
1019
|
-
)
|
|
882
|
+
isNull(conversations.originChannel)
|
|
883
|
+
)
|
|
1020
884
|
)
|
|
1021
885
|
.run();
|
|
1022
886
|
}
|
|
@@ -1035,7 +899,7 @@ export async function addMessage(
|
|
|
1035
899
|
) {
|
|
1036
900
|
log.warn(
|
|
1037
901
|
{ attempt, conversationId, code: errCode },
|
|
1038
|
-
"addMessage: transient SQLite error, retrying"
|
|
902
|
+
"addMessage: transient SQLite error, retrying"
|
|
1039
903
|
);
|
|
1040
904
|
await Bun.sleep(50 * (attempt + 1));
|
|
1041
905
|
continue;
|
|
@@ -1074,12 +938,12 @@ export async function addMessage(
|
|
|
1074
938
|
provenanceTrustClass,
|
|
1075
939
|
automated,
|
|
1076
940
|
},
|
|
1077
|
-
config.memory
|
|
941
|
+
config.memory
|
|
1078
942
|
);
|
|
1079
943
|
} catch (err) {
|
|
1080
944
|
log.warn(
|
|
1081
945
|
{ err, conversationId, messageId: message.id },
|
|
1082
|
-
"Failed to index message for memory"
|
|
946
|
+
"Failed to index message for memory"
|
|
1083
947
|
);
|
|
1084
948
|
}
|
|
1085
949
|
}
|
|
@@ -1094,7 +958,7 @@ export async function addMessage(
|
|
|
1094
958
|
} catch (err) {
|
|
1095
959
|
log.warn(
|
|
1096
960
|
{ err, conversationId, messageId: message.id },
|
|
1097
|
-
"Failed to project assistant message for attention tracking"
|
|
961
|
+
"Failed to project assistant message for attention tracking"
|
|
1098
962
|
);
|
|
1099
963
|
}
|
|
1100
964
|
}
|
|
@@ -1121,7 +985,7 @@ export interface PaginatedMessagesResult {
|
|
|
1121
985
|
export function getMessagesPaginated(
|
|
1122
986
|
conversationId: string,
|
|
1123
987
|
limit: number | undefined,
|
|
1124
|
-
beforeTimestamp?: number
|
|
988
|
+
beforeTimestamp?: number
|
|
1125
989
|
): PaginatedMessagesResult {
|
|
1126
990
|
const db = getDb();
|
|
1127
991
|
|
|
@@ -1165,7 +1029,7 @@ export function getMessagesPaginated(
|
|
|
1165
1029
|
|
|
1166
1030
|
export function getLastAssistantTimestampBefore(
|
|
1167
1031
|
conversationId: string,
|
|
1168
|
-
beforeTimestamp: number
|
|
1032
|
+
beforeTimestamp: number
|
|
1169
1033
|
): number {
|
|
1170
1034
|
const db = getDb();
|
|
1171
1035
|
const row = db
|
|
@@ -1175,8 +1039,8 @@ export function getLastAssistantTimestampBefore(
|
|
|
1175
1039
|
and(
|
|
1176
1040
|
eq(messages.conversationId, conversationId),
|
|
1177
1041
|
eq(messages.role, "assistant"),
|
|
1178
|
-
lt(messages.createdAt, beforeTimestamp)
|
|
1179
|
-
)
|
|
1042
|
+
lt(messages.createdAt, beforeTimestamp)
|
|
1043
|
+
)
|
|
1180
1044
|
)
|
|
1181
1045
|
.orderBy(desc(messages.createdAt))
|
|
1182
1046
|
.limit(1)
|
|
@@ -1187,7 +1051,7 @@ export function getLastAssistantTimestampBefore(
|
|
|
1187
1051
|
/** Fetch a single message by ID, optionally scoped to a specific conversation. */
|
|
1188
1052
|
export function getMessageById(
|
|
1189
1053
|
messageId: string,
|
|
1190
|
-
conversationId?: string
|
|
1054
|
+
conversationId?: string
|
|
1191
1055
|
): MessageRow | null {
|
|
1192
1056
|
const db = getDb();
|
|
1193
1057
|
const conditions = [eq(messages.id, messageId)];
|
|
@@ -1205,7 +1069,7 @@ export function getMessageById(
|
|
|
1205
1069
|
export function updateConversationTitle(
|
|
1206
1070
|
id: string,
|
|
1207
1071
|
title: string,
|
|
1208
|
-
isAutoTitle?: number
|
|
1072
|
+
isAutoTitle?: number
|
|
1209
1073
|
): void {
|
|
1210
1074
|
const db = getDb();
|
|
1211
1075
|
const set: Record<string, unknown> = { title, updatedAt: Date.now() };
|
|
@@ -1223,7 +1087,7 @@ export function updateConversationUsage(
|
|
|
1223
1087
|
id: string,
|
|
1224
1088
|
totalInputTokens: number,
|
|
1225
1089
|
totalOutputTokens: number,
|
|
1226
|
-
totalEstimatedCost: number
|
|
1090
|
+
totalEstimatedCost: number
|
|
1227
1091
|
): void {
|
|
1228
1092
|
const db = getDb();
|
|
1229
1093
|
db.update(conversations)
|
|
@@ -1240,7 +1104,7 @@ export function updateConversationUsage(
|
|
|
1240
1104
|
export function updateConversationContextWindow(
|
|
1241
1105
|
id: string,
|
|
1242
1106
|
contextSummary: string,
|
|
1243
|
-
contextCompactedMessageCount: number
|
|
1107
|
+
contextCompactedMessageCount: number
|
|
1244
1108
|
): void {
|
|
1245
1109
|
const db = getDb();
|
|
1246
1110
|
db.update(conversations)
|
|
@@ -1265,18 +1129,16 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
1265
1129
|
const convCount =
|
|
1266
1130
|
rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
|
|
1267
1131
|
|
|
1268
|
-
// Delete in dependency order. Cascades handle memory_segments
|
|
1269
|
-
//
|
|
1270
|
-
//
|
|
1132
|
+
// Delete in dependency order. Cascades handle memory_segments and
|
|
1133
|
+
// tool_invocations, but we explicitly clear non-cascading memory
|
|
1134
|
+
// tables too.
|
|
1271
1135
|
//
|
|
1272
1136
|
// FTS virtual tables are cleared before their base tables. If an FTS
|
|
1273
1137
|
// table is corrupted, the DELETE will fail — we drop the associated
|
|
1274
1138
|
// triggers so that the subsequent base-table DELETEs don't also fail
|
|
1275
1139
|
// (SQLite triggers are atomic with the triggering statement, so a
|
|
1276
1140
|
// corrupted FTS table would roll back every base-table DELETE).
|
|
1277
|
-
rawExec("DELETE FROM memory_item_sources");
|
|
1278
1141
|
rawExec("DELETE FROM memory_segments");
|
|
1279
|
-
rawExec("DELETE FROM memory_items");
|
|
1280
1142
|
rawExec("DELETE FROM memory_summaries");
|
|
1281
1143
|
rawExec("DELETE FROM memory_embeddings");
|
|
1282
1144
|
rawExec("DELETE FROM memory_jobs");
|
|
@@ -1292,7 +1154,7 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
1292
1154
|
} catch (err) {
|
|
1293
1155
|
log.warn(
|
|
1294
1156
|
{ err },
|
|
1295
|
-
"clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed"
|
|
1157
|
+
"clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed"
|
|
1296
1158
|
);
|
|
1297
1159
|
rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
|
|
1298
1160
|
rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
|
|
@@ -1308,7 +1170,7 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
1308
1170
|
`INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
|
|
1309
1171
|
uuid(),
|
|
1310
1172
|
"conversations_clear_all",
|
|
1311
|
-
Date.now()
|
|
1173
|
+
Date.now()
|
|
1312
1174
|
);
|
|
1313
1175
|
|
|
1314
1176
|
// Rebuild corrupted FTS tables and restore triggers after all base-table
|
|
@@ -1318,16 +1180,16 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
1318
1180
|
if (messagesFtsCorrupted) {
|
|
1319
1181
|
rawExec("DROP TABLE IF EXISTS messages_fts");
|
|
1320
1182
|
rawExec(
|
|
1321
|
-
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)
|
|
1183
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`
|
|
1322
1184
|
);
|
|
1323
1185
|
rawExec(
|
|
1324
|
-
`CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END
|
|
1186
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`
|
|
1325
1187
|
);
|
|
1326
1188
|
rawExec(
|
|
1327
|
-
`CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END
|
|
1189
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`
|
|
1328
1190
|
);
|
|
1329
1191
|
rawExec(
|
|
1330
|
-
`CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END
|
|
1192
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`
|
|
1331
1193
|
);
|
|
1332
1194
|
}
|
|
1333
1195
|
|
|
@@ -1352,8 +1214,8 @@ export function deleteLastExchange(conversationId: string): number {
|
|
|
1352
1214
|
.where(
|
|
1353
1215
|
and(
|
|
1354
1216
|
eq(messages.conversationId, conversationId),
|
|
1355
|
-
eq(messages.role, "user")
|
|
1356
|
-
)
|
|
1217
|
+
eq(messages.role, "user")
|
|
1218
|
+
)
|
|
1357
1219
|
)
|
|
1358
1220
|
.orderBy(sql`rowid DESC`)
|
|
1359
1221
|
.limit(1)
|
|
@@ -1367,7 +1229,7 @@ export function deleteLastExchange(conversationId: string): number {
|
|
|
1367
1229
|
const rowidSubquery = sql`(SELECT rowid FROM messages WHERE id = ${lastUserMsg.id})`;
|
|
1368
1230
|
const condition = and(
|
|
1369
1231
|
eq(messages.conversationId, conversationId),
|
|
1370
|
-
sql`rowid >= ${rowidSubquery}
|
|
1232
|
+
sql`rowid >= ${rowidSubquery}`
|
|
1371
1233
|
);
|
|
1372
1234
|
|
|
1373
1235
|
const [{ deleted }] = db
|
|
@@ -1431,7 +1293,7 @@ export interface WipeConversationResult extends DeletedMemoryIds {
|
|
|
1431
1293
|
*/
|
|
1432
1294
|
export function updateMessageContent(
|
|
1433
1295
|
messageId: string,
|
|
1434
|
-
newContent: string
|
|
1296
|
+
newContent: string
|
|
1435
1297
|
): void {
|
|
1436
1298
|
const db = getDb();
|
|
1437
1299
|
db.update(messages)
|
|
@@ -1440,6 +1302,27 @@ export function updateMessageContent(
|
|
|
1440
1302
|
.run();
|
|
1441
1303
|
}
|
|
1442
1304
|
|
|
1305
|
+
/**
|
|
1306
|
+
* Merge `updates` into the metadata JSON of an existing message.
|
|
1307
|
+
* Reads the current metadata, shallow-merges the new fields, and writes back.
|
|
1308
|
+
*/
|
|
1309
|
+
export function updateMessageMetadata(
|
|
1310
|
+
messageId: string,
|
|
1311
|
+
updates: Record<string, unknown>,
|
|
1312
|
+
): void {
|
|
1313
|
+
const db = getDb();
|
|
1314
|
+
const row = db
|
|
1315
|
+
.select({ metadata: messages.metadata })
|
|
1316
|
+
.from(messages)
|
|
1317
|
+
.where(eq(messages.id, messageId))
|
|
1318
|
+
.get();
|
|
1319
|
+
const existing = row?.metadata ? JSON.parse(row.metadata) : {};
|
|
1320
|
+
db.update(messages)
|
|
1321
|
+
.set({ metadata: JSON.stringify({ ...existing, ...updates }) })
|
|
1322
|
+
.where(eq(messages.id, messageId))
|
|
1323
|
+
.run();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1443
1326
|
/**
|
|
1444
1327
|
* Re-link all attachments from a set of source messages to a target message.
|
|
1445
1328
|
* Used during message consolidation so that attachments linked to deleted
|
|
@@ -1447,7 +1330,7 @@ export function updateMessageContent(
|
|
|
1447
1330
|
*/
|
|
1448
1331
|
export function relinkAttachments(
|
|
1449
1332
|
fromMessageIds: string[],
|
|
1450
|
-
toMessageId: string
|
|
1333
|
+
toMessageId: string
|
|
1451
1334
|
): number {
|
|
1452
1335
|
if (fromMessageIds.length === 0) return 0;
|
|
1453
1336
|
const db = getDb();
|
|
@@ -1475,15 +1358,8 @@ export function relinkAttachments(
|
|
|
1475
1358
|
* NULL before the message row is removed, so associated run and event
|
|
1476
1359
|
* records survive.
|
|
1477
1360
|
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
* cascades memory_item_sources but leaves the memory_items active.
|
|
1481
|
-
* Without cleanup, those items would leak into summaries and recall.
|
|
1482
|
-
* We delete any memory_items that become orphaned (no remaining sources)
|
|
1483
|
-
* after this message is removed.
|
|
1484
|
-
*
|
|
1485
|
-
* Returns segment and orphaned item IDs so the caller can clean up the
|
|
1486
|
-
* corresponding Qdrant vector entries.
|
|
1361
|
+
* Returns segment IDs so the caller can clean up the corresponding
|
|
1362
|
+
* Qdrant vector entries.
|
|
1487
1363
|
*/
|
|
1488
1364
|
export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
1489
1365
|
const db = getDb();
|
|
@@ -1512,22 +1388,14 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
|
1512
1388
|
.all();
|
|
1513
1389
|
result.segmentIds = linkedSegments.map((r) => r.id);
|
|
1514
1390
|
|
|
1515
|
-
// Collect memory item IDs linked to this message before cascade.
|
|
1516
|
-
const linkedItems = tx
|
|
1517
|
-
.select({ memoryItemId: memoryItemSources.memoryItemId })
|
|
1518
|
-
.from(memoryItemSources)
|
|
1519
|
-
.where(eq(memoryItemSources.messageId, messageId))
|
|
1520
|
-
.all();
|
|
1521
|
-
const candidateItemIds = linkedItems.map((r) => r.memoryItemId);
|
|
1522
|
-
|
|
1523
1391
|
// Detach nullable FK references so the cascade doesn't destroy them.
|
|
1524
1392
|
tx.update(channelInboundEvents)
|
|
1525
1393
|
.set({ messageId: null })
|
|
1526
1394
|
.where(eq(channelInboundEvents.messageId, messageId))
|
|
1527
1395
|
.run();
|
|
1528
1396
|
|
|
1529
|
-
// Now safe to delete — NOT NULL cascades remove
|
|
1530
|
-
//
|
|
1397
|
+
// Now safe to delete — NOT NULL cascades remove memory_segments
|
|
1398
|
+
// and message_attachments.
|
|
1531
1399
|
tx.delete(messages).where(eq(messages.id, messageId)).run();
|
|
1532
1400
|
|
|
1533
1401
|
// Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
|
|
@@ -1536,42 +1404,11 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
|
1536
1404
|
.where(
|
|
1537
1405
|
and(
|
|
1538
1406
|
eq(memoryEmbeddings.targetType, "segment"),
|
|
1539
|
-
inArray(memoryEmbeddings.targetId, result.segmentIds)
|
|
1540
|
-
)
|
|
1407
|
+
inArray(memoryEmbeddings.targetId, result.segmentIds)
|
|
1408
|
+
)
|
|
1541
1409
|
)
|
|
1542
1410
|
.run();
|
|
1543
1411
|
}
|
|
1544
|
-
|
|
1545
|
-
// Clean up orphaned memory items whose only source was this message.
|
|
1546
|
-
if (candidateItemIds.length > 0) {
|
|
1547
|
-
// Find which items still have at least one remaining source.
|
|
1548
|
-
const surviving = tx
|
|
1549
|
-
.select({ memoryItemId: memoryItemSources.memoryItemId })
|
|
1550
|
-
.from(memoryItemSources)
|
|
1551
|
-
.where(inArray(memoryItemSources.memoryItemId, candidateItemIds))
|
|
1552
|
-
.all();
|
|
1553
|
-
const survivingIds = new Set(surviving.map((r) => r.memoryItemId));
|
|
1554
|
-
const orphanedIds = candidateItemIds.filter(
|
|
1555
|
-
(id) => !survivingIds.has(id),
|
|
1556
|
-
);
|
|
1557
|
-
result.orphanedItemIds = orphanedIds;
|
|
1558
|
-
|
|
1559
|
-
if (orphanedIds.length > 0) {
|
|
1560
|
-
// Delete embeddings referencing these items.
|
|
1561
|
-
tx.delete(memoryEmbeddings)
|
|
1562
|
-
.where(
|
|
1563
|
-
and(
|
|
1564
|
-
eq(memoryEmbeddings.targetType, "item"),
|
|
1565
|
-
inArray(memoryEmbeddings.targetId, orphanedIds),
|
|
1566
|
-
),
|
|
1567
|
-
)
|
|
1568
|
-
.run();
|
|
1569
|
-
// Delete the orphaned memory items themselves.
|
|
1570
|
-
tx.delete(memoryItems)
|
|
1571
|
-
.where(inArray(memoryItems.id, orphanedIds))
|
|
1572
|
-
.run();
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
1412
|
});
|
|
1576
1413
|
|
|
1577
1414
|
deleteOrphanAttachments(candidateAttachmentIds);
|
|
@@ -1581,7 +1418,7 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
|
1581
1418
|
|
|
1582
1419
|
export function setConversationOriginChannelIfUnset(
|
|
1583
1420
|
conversationId: string,
|
|
1584
|
-
channel: ChannelId
|
|
1421
|
+
channel: ChannelId
|
|
1585
1422
|
): void {
|
|
1586
1423
|
const db = getDb();
|
|
1587
1424
|
db.update(conversations)
|
|
@@ -1589,14 +1426,14 @@ export function setConversationOriginChannelIfUnset(
|
|
|
1589
1426
|
.where(
|
|
1590
1427
|
and(
|
|
1591
1428
|
eq(conversations.id, conversationId),
|
|
1592
|
-
isNull(conversations.originChannel)
|
|
1593
|
-
)
|
|
1429
|
+
isNull(conversations.originChannel)
|
|
1430
|
+
)
|
|
1594
1431
|
)
|
|
1595
1432
|
.run();
|
|
1596
1433
|
}
|
|
1597
1434
|
|
|
1598
1435
|
export function getConversationOriginChannel(
|
|
1599
|
-
conversationId: string
|
|
1436
|
+
conversationId: string
|
|
1600
1437
|
): ChannelId | null {
|
|
1601
1438
|
const db = getDb();
|
|
1602
1439
|
const row = db
|
|
@@ -1609,7 +1446,7 @@ export function getConversationOriginChannel(
|
|
|
1609
1446
|
|
|
1610
1447
|
export function setConversationOriginInterfaceIfUnset(
|
|
1611
1448
|
conversationId: string,
|
|
1612
|
-
interfaceId: InterfaceId
|
|
1449
|
+
interfaceId: InterfaceId
|
|
1613
1450
|
): void {
|
|
1614
1451
|
const db = getDb();
|
|
1615
1452
|
db.update(conversations)
|
|
@@ -1617,14 +1454,14 @@ export function setConversationOriginInterfaceIfUnset(
|
|
|
1617
1454
|
.where(
|
|
1618
1455
|
and(
|
|
1619
1456
|
eq(conversations.id, conversationId),
|
|
1620
|
-
isNull(conversations.originInterface)
|
|
1621
|
-
)
|
|
1457
|
+
isNull(conversations.originInterface)
|
|
1458
|
+
)
|
|
1622
1459
|
)
|
|
1623
1460
|
.run();
|
|
1624
1461
|
}
|
|
1625
1462
|
|
|
1626
1463
|
export function getConversationOriginInterface(
|
|
1627
|
-
conversationId: string
|
|
1464
|
+
conversationId: string
|
|
1628
1465
|
): InterfaceId | null {
|
|
1629
1466
|
const db = getDb();
|
|
1630
1467
|
const row = db
|
|
@@ -1644,13 +1481,13 @@ export function getConversationOriginInterface(
|
|
|
1644
1481
|
* conversation itself isn't a desktop-origin private conversation).
|
|
1645
1482
|
*/
|
|
1646
1483
|
export function getConversationRecentProvenanceTrustClass(
|
|
1647
|
-
conversationId: string
|
|
1484
|
+
conversationId: string
|
|
1648
1485
|
): "guardian" | "trusted_contact" | "unknown" | undefined {
|
|
1649
1486
|
const row = rawGet<{ metadata: string | null }>(
|
|
1650
1487
|
`SELECT metadata FROM messages
|
|
1651
1488
|
WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
|
|
1652
1489
|
ORDER BY created_at DESC LIMIT 1`,
|
|
1653
|
-
conversationId
|
|
1490
|
+
conversationId
|
|
1654
1491
|
);
|
|
1655
1492
|
if (!row?.metadata) return undefined;
|
|
1656
1493
|
try {
|
|
@@ -1670,18 +1507,66 @@ export function batchSetDisplayOrders(
|
|
|
1670
1507
|
id: string;
|
|
1671
1508
|
displayOrder: number | null;
|
|
1672
1509
|
isPinned: boolean;
|
|
1673
|
-
|
|
1510
|
+
groupId?: string | null;
|
|
1511
|
+
}>
|
|
1674
1512
|
): void {
|
|
1675
1513
|
ensureDisplayOrderMigration();
|
|
1514
|
+
ensureGroupMigration();
|
|
1676
1515
|
rawExec("BEGIN");
|
|
1677
1516
|
try {
|
|
1678
1517
|
for (const update of updates) {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1518
|
+
if (update.groupId !== undefined) {
|
|
1519
|
+
// New client: groupId is authoritative.
|
|
1520
|
+
// Derive is_pinned from groupId.
|
|
1521
|
+
// Sanitize: if groupId references a deleted/unknown group, fall back
|
|
1522
|
+
// to NULL to avoid FK violation that would roll back the entire batch.
|
|
1523
|
+
let safeGroupId = update.groupId;
|
|
1524
|
+
if (
|
|
1525
|
+
safeGroupId !== null &&
|
|
1526
|
+
!rawGet<{ id: string }>(
|
|
1527
|
+
"SELECT id FROM conversation_groups WHERE id = ?",
|
|
1528
|
+
safeGroupId
|
|
1529
|
+
)
|
|
1530
|
+
) {
|
|
1531
|
+
safeGroupId = null;
|
|
1532
|
+
}
|
|
1533
|
+
rawRun(
|
|
1534
|
+
"UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
|
|
1535
|
+
update.displayOrder,
|
|
1536
|
+
safeGroupId === "system:pinned" ? 1 : 0,
|
|
1537
|
+
safeGroupId,
|
|
1538
|
+
update.id
|
|
1539
|
+
);
|
|
1540
|
+
} else {
|
|
1541
|
+
// Old client: no groupId in payload
|
|
1542
|
+
// isPinned true -> set group_id = system:pinned
|
|
1543
|
+
// isPinned false -> clear group_id ONLY IF currently system:pinned
|
|
1544
|
+
// otherwise preserve existing group_id
|
|
1545
|
+
if (update.isPinned) {
|
|
1546
|
+
rawRun(
|
|
1547
|
+
"UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
1548
|
+
update.displayOrder,
|
|
1549
|
+
update.id
|
|
1550
|
+
);
|
|
1551
|
+
} else {
|
|
1552
|
+
// Restore system group from source/conversationType when old clients
|
|
1553
|
+
// unpin, instead of clearing to NULL (which would lose provenance).
|
|
1554
|
+
rawRun(
|
|
1555
|
+
`UPDATE conversations SET display_order = ?, is_pinned = 0,
|
|
1556
|
+
group_id = CASE WHEN group_id = 'system:pinned' THEN
|
|
1557
|
+
CASE
|
|
1558
|
+
WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
|
|
1559
|
+
WHEN source IN ('heartbeat', 'task') THEN 'system:background'
|
|
1560
|
+
WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
|
|
1561
|
+
ELSE NULL
|
|
1562
|
+
END
|
|
1563
|
+
ELSE group_id END
|
|
1564
|
+
WHERE id = ?`,
|
|
1565
|
+
update.displayOrder,
|
|
1566
|
+
update.id
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1685
1570
|
}
|
|
1686
1571
|
rawExec("COMMIT");
|
|
1687
1572
|
} catch (err) {
|
|
@@ -1691,22 +1576,31 @@ export function batchSetDisplayOrders(
|
|
|
1691
1576
|
}
|
|
1692
1577
|
|
|
1693
1578
|
export function getDisplayMetaForConversations(
|
|
1694
|
-
conversationIds: string[]
|
|
1695
|
-
): Map<
|
|
1579
|
+
conversationIds: string[]
|
|
1580
|
+
): Map<
|
|
1581
|
+
string,
|
|
1582
|
+
{ displayOrder: number | null; isPinned: boolean; groupId: string | null }
|
|
1583
|
+
> {
|
|
1696
1584
|
ensureDisplayOrderMigration();
|
|
1585
|
+
ensureGroupMigration();
|
|
1697
1586
|
const result = new Map<
|
|
1698
1587
|
string,
|
|
1699
|
-
{ displayOrder: number | null; isPinned: boolean }
|
|
1588
|
+
{ displayOrder: number | null; isPinned: boolean; groupId: string | null }
|
|
1700
1589
|
>();
|
|
1701
1590
|
if (conversationIds.length === 0) return result;
|
|
1702
1591
|
for (const id of conversationIds) {
|
|
1703
1592
|
const row = rawGet<{
|
|
1704
1593
|
display_order: number | null;
|
|
1705
1594
|
is_pinned: number | null;
|
|
1706
|
-
|
|
1595
|
+
group_id: string | null;
|
|
1596
|
+
}>(
|
|
1597
|
+
"SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
|
|
1598
|
+
id
|
|
1599
|
+
);
|
|
1707
1600
|
result.set(id, {
|
|
1708
1601
|
displayOrder: row?.display_order ?? null,
|
|
1709
1602
|
isPinned: (row?.is_pinned ?? 0) === 1,
|
|
1603
|
+
groupId: row?.group_id ?? null,
|
|
1710
1604
|
});
|
|
1711
1605
|
}
|
|
1712
1606
|
return result;
|
|
@@ -1730,7 +1624,7 @@ function isToolResultMessage(role: string, content: string): boolean {
|
|
|
1730
1624
|
(block: unknown) =>
|
|
1731
1625
|
block != null &&
|
|
1732
1626
|
typeof block === "object" &&
|
|
1733
|
-
(block as Record<string, unknown>).type === "tool_result"
|
|
1627
|
+
(block as Record<string, unknown>).type === "tool_result"
|
|
1734
1628
|
);
|
|
1735
1629
|
} catch {
|
|
1736
1630
|
return false;
|
|
@@ -1750,7 +1644,7 @@ function isToolResultMessage(role: string, content: string): boolean {
|
|
|
1750
1644
|
*/
|
|
1751
1645
|
export function getTurnTimeBounds(
|
|
1752
1646
|
conversationId: string,
|
|
1753
|
-
messageCreatedAt: number
|
|
1647
|
+
messageCreatedAt: number
|
|
1754
1648
|
): { startTime: number; endTime: number } | null {
|
|
1755
1649
|
const db = getDb();
|
|
1756
1650
|
|
|
@@ -1772,8 +1666,8 @@ export function getTurnTimeBounds(
|
|
|
1772
1666
|
.where(
|
|
1773
1667
|
and(
|
|
1774
1668
|
eq(messages.conversationId, conversationId),
|
|
1775
|
-
sql`rowid <= ${rowidSubquery}
|
|
1776
|
-
)
|
|
1669
|
+
sql`rowid <= ${rowidSubquery}`
|
|
1670
|
+
)
|
|
1777
1671
|
)
|
|
1778
1672
|
.orderBy(sql`rowid DESC`)
|
|
1779
1673
|
.limit(50)
|
|
@@ -1804,8 +1698,8 @@ export function getTurnTimeBounds(
|
|
|
1804
1698
|
.where(
|
|
1805
1699
|
and(
|
|
1806
1700
|
eq(messages.conversationId, conversationId),
|
|
1807
|
-
sql`rowid > ${forwardRowidSubquery}
|
|
1808
|
-
)
|
|
1701
|
+
sql`rowid > ${forwardRowidSubquery}`
|
|
1702
|
+
)
|
|
1809
1703
|
)
|
|
1810
1704
|
.orderBy(sql`rowid ASC`)
|
|
1811
1705
|
.limit(50)
|
|
@@ -1846,8 +1740,8 @@ export function getTurnTimeBounds(
|
|
|
1846
1740
|
and(
|
|
1847
1741
|
eq(llmRequestLogs.conversationId, conversationId),
|
|
1848
1742
|
gte(llmRequestLogs.createdAt, startTime),
|
|
1849
|
-
lte(llmRequestLogs.createdAt, hardCeiling)
|
|
1850
|
-
)
|
|
1743
|
+
lte(llmRequestLogs.createdAt, hardCeiling)
|
|
1744
|
+
)
|
|
1851
1745
|
)
|
|
1852
1746
|
.orderBy(desc(llmRequestLogs.createdAt))
|
|
1853
1747
|
.limit(1)
|
|
@@ -1895,8 +1789,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
1895
1789
|
.where(
|
|
1896
1790
|
and(
|
|
1897
1791
|
eq(messages.conversationId, target.conversationId),
|
|
1898
|
-
lte(messages.createdAt, target.createdAt)
|
|
1899
|
-
)
|
|
1792
|
+
lte(messages.createdAt, target.createdAt)
|
|
1793
|
+
)
|
|
1900
1794
|
)
|
|
1901
1795
|
.orderBy(desc(messages.createdAt))
|
|
1902
1796
|
.limit(50)
|
|
@@ -1933,8 +1827,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
1933
1827
|
.where(
|
|
1934
1828
|
and(
|
|
1935
1829
|
eq(messages.conversationId, target.conversationId),
|
|
1936
|
-
gt(messages.createdAt, target.createdAt)
|
|
1937
|
-
)
|
|
1830
|
+
gt(messages.createdAt, target.createdAt)
|
|
1831
|
+
)
|
|
1938
1832
|
)
|
|
1939
1833
|
.orderBy(asc(messages.createdAt))
|
|
1940
1834
|
.limit(50)
|
|
@@ -1970,8 +1864,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
1970
1864
|
and(
|
|
1971
1865
|
eq(messages.conversationId, target.conversationId),
|
|
1972
1866
|
gt(messages.createdAt, boundaryCreatedAt),
|
|
1973
|
-
lte(messages.createdAt, target.createdAt)
|
|
1974
|
-
)
|
|
1867
|
+
lte(messages.createdAt, target.createdAt)
|
|
1868
|
+
)
|
|
1975
1869
|
)
|
|
1976
1870
|
.orderBy(asc(messages.createdAt))
|
|
1977
1871
|
.all();
|
|
@@ -1994,8 +1888,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
|
1994
1888
|
.where(
|
|
1995
1889
|
and(
|
|
1996
1890
|
eq(messages.conversationId, target.conversationId),
|
|
1997
|
-
inArray(messages.id, [...idSet])
|
|
1998
|
-
)
|
|
1891
|
+
inArray(messages.id, [...idSet])
|
|
1892
|
+
)
|
|
1999
1893
|
)
|
|
2000
1894
|
.orderBy(asc(messages.createdAt))
|
|
2001
1895
|
.all();
|