@vellumai/assistant 0.10.3 → 0.10.4-dev.202607010028.1a4efcc
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 +13 -13
- package/Dockerfile +5 -15
- package/bun.lock +165 -3
- package/docs/architecture/memory.md +26 -0
- package/eslint-rules/cli-no-daemon-internals.js +9 -8
- package/knip.json +0 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
- package/openapi.yaml +1057 -88
- package/package.json +8 -3
- package/scripts/generate-openapi.ts +149 -62
- package/src/__tests__/acp-session.test.ts +2 -2
- package/src/__tests__/activation-early-marking.test.ts +4 -4
- package/src/__tests__/actor-token-service.test.ts +2 -3
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/agent-loop-compaction-events.test.ts +1 -0
- package/src/__tests__/agent-loop-compaction-strip.test.ts +2 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +2 -2
- package/src/__tests__/agent-loop-regrowth-guard.test.ts +8 -0
- package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
- package/src/__tests__/agent-loop.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -3
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -3
- package/src/__tests__/annotate-activity-metadata.test.ts +4 -4
- package/src/__tests__/annotate-risk-options.test.ts +4 -4
- package/src/__tests__/anthropic-provider.test.ts +53 -2
- package/src/__tests__/app-builder-tool-scripts.test.ts +2 -2
- package/src/__tests__/app-bundler.test.ts +1 -1
- package/src/__tests__/app-conversation-ids-backfill.test.ts +4 -4
- package/src/__tests__/app-conversation-ids.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +2 -2
- package/src/__tests__/app-executors.test.ts +1 -1
- package/src/__tests__/app-git-history.test.ts +2 -2
- package/src/__tests__/app-git-service.test.ts +2 -2
- package/src/__tests__/app-routes-csp.test.ts +2 -2
- package/src/__tests__/app-source-watcher.test.ts +1 -1
- package/src/__tests__/app-store-dir-names.test.ts +1 -1
- package/src/__tests__/approval-cascade.test.ts +7 -7
- package/src/__tests__/approval-interception-trust-gates.test.ts +1 -1
- package/src/__tests__/approval-primitive.test.ts +3 -3
- package/src/__tests__/approval-routes-http.test.ts +2 -2
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +1 -1
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -2
- package/src/__tests__/assistant-events-sse-shed.test.ts +2 -2
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/attachment-upload-trusted-source.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +9 -6
- package/src/__tests__/audit-log-rotation.test.ts +6 -4
- package/src/__tests__/auth-fallback-events-store.test.ts +4 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +13 -9
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +12 -0
- package/src/__tests__/background-tool-registry.test.ts +97 -0
- package/src/__tests__/background-tool-routes.test.ts +72 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +17 -10
- package/src/__tests__/btw-routes.test.ts +4 -5
- package/src/__tests__/build-persisted-content.test.ts +4 -4
- package/src/__tests__/call-controller.test.ts +6 -6
- package/src/__tests__/call-conversation-messages.test.ts +4 -4
- package/src/__tests__/call-domain.test.ts +5 -5
- package/src/__tests__/call-pointer-messages.test.ts +5 -5
- package/src/__tests__/call-recovery.test.ts +3 -3
- package/src/__tests__/call-routes-http.test.ts +3 -3
- package/src/__tests__/call-store.test.ts +3 -3
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -3
- package/src/__tests__/canonical-guardian-store.test.ts +3 -3
- package/src/__tests__/channel-approval-routes.test.ts +30 -33
- package/src/__tests__/channel-delivery-store.test.ts +36 -8
- package/src/__tests__/channel-guardian.test.ts +62 -54
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +15 -23
- package/src/__tests__/channel-reply-delivery.test.ts +8 -4
- package/src/__tests__/channel-retry-sweep.test.ts +49 -4
- package/src/__tests__/clawhub.test.ts +4 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +2 -2
- package/src/__tests__/compaction-events.test.ts +8 -8
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/compaction-trail-store.test.ts +5 -5
- package/src/__tests__/compaction.benchmark.test.ts +5 -5
- package/src/__tests__/compactor-call-site-logging.test.ts +5 -5
- package/src/__tests__/compactor-image-manifest-trust.test.ts +7 -4
- package/src/__tests__/compactor-low-watermark-cut.test.ts +5 -5
- package/src/__tests__/compactor-preserved-tail-count.test.ts +4 -4
- package/src/__tests__/compactor-summary-call-truncation.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +4 -4
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-backfill.test.ts +230 -102
- package/src/__tests__/config-set-route.test.ts +5 -2
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +3 -3
- package/src/__tests__/config-watcher-skill-reseed.test.ts +39 -112
- package/src/__tests__/config-watcher.test.ts +54 -31
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -4
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +98 -91
- package/src/__tests__/contacts-relay-reads.test.ts +27 -12
- package/src/__tests__/contacts-tools.test.ts +2 -2
- package/src/__tests__/contacts-write.test.ts +2 -4
- package/src/__tests__/context-search-agent-protocol.test.ts +2 -2
- package/src/__tests__/context-search-agent-runner.test.ts +2 -2
- package/src/__tests__/context-search-conversations-source.test.ts +5 -5
- package/src/__tests__/context-search-fanout.test.ts +3 -3
- package/src/__tests__/context-search-memory-source.test.ts +9 -8
- package/src/__tests__/context-search-memory-v2-source.test.ts +9 -8
- package/src/__tests__/context-search-types.test.ts +2 -2
- package/src/__tests__/context-search-workspace-source.test.ts +2 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +5 -5
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +4 -3
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +40 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +11 -8
- package/src/__tests__/conversation-agent-loop.test.ts +150 -20
- package/src/__tests__/conversation-analysis-routes.test.ts +4 -4
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -1
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +7 -7
- package/src/__tests__/conversation-attachments.test.ts +7 -4
- package/src/__tests__/conversation-attention-store.test.ts +4 -4
- package/src/__tests__/conversation-attention-telegram.test.ts +17 -16
- package/src/__tests__/conversation-clear-safety.test.ts +3 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +7 -7
- package/src/__tests__/conversation-crud-inference-profile.test.ts +3 -3
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +25 -26
- package/src/__tests__/conversation-directories-parse.test.ts +1 -1
- package/src/__tests__/conversation-disk-view-integration.test.ts +5 -5
- package/src/__tests__/conversation-disk-view.test.ts +6 -6
- package/src/__tests__/conversation-error.test.ts +18 -18
- package/src/__tests__/conversation-evictor.test.ts +32 -7
- package/src/__tests__/conversation-fork-crud.test.ts +441 -44
- package/src/__tests__/conversation-fork-retrospective.test.ts +12 -10
- package/src/__tests__/conversation-fork-route.test.ts +6 -3
- package/src/__tests__/conversation-history-web-search.test.ts +8 -145
- package/src/__tests__/conversation-inference-profile-list.test.ts +3 -3
- package/src/__tests__/conversation-inference-profile-route.test.ts +3 -3
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
- package/src/__tests__/conversation-key-store-disk-view.test.ts +7 -4
- package/src/__tests__/conversation-lifecycle.test.ts +10 -13
- package/src/__tests__/conversation-list-source.test.ts +3 -3
- package/src/__tests__/conversation-load-history-repair.test.ts +4 -4
- package/src/__tests__/conversation-load-history-stripped.test.ts +4 -4
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
- package/src/__tests__/conversation-pairing.test.ts +4 -4
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +5 -5
- package/src/__tests__/conversation-process-callsite.test.ts +42 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -5
- package/src/__tests__/conversation-queue.test.ts +142 -9
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -5
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
- package/src/__tests__/conversation-routes-slash-commands.test.ts +57 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +25 -19
- package/src/__tests__/conversation-runtime-workspace.test.ts +8 -1
- package/src/__tests__/conversation-skill-tools.test.ts +155 -2
- package/src/__tests__/conversation-slash-queue.test.ts +6 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -6
- package/src/__tests__/conversation-speed-override.test.ts +7 -7
- package/src/__tests__/conversation-starter-routes.test.ts +2 -2
- package/src/__tests__/conversation-store.test.ts +6 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +8 -5
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +2 -2
- package/src/__tests__/conversation-sync-tags.test.ts +7 -4
- package/src/__tests__/conversation-title-service.test.ts +223 -203
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +6 -6
- package/src/__tests__/conversation-wipe.test.ts +6 -4
- package/src/__tests__/conversation-workspace-cache-state.test.ts +9 -9
- package/src/__tests__/conversation-workspace-injection.test.ts +9 -9
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +9 -9
- package/src/__tests__/conversations-import-system-filter.test.ts +5 -5
- package/src/__tests__/credential-security-invariants.test.ts +4 -7
- package/src/__tests__/db-acp-history.test.ts +4 -4
- package/src/__tests__/db-activation-state-fk-cascade.test.ts +4 -4
- package/src/__tests__/db-activation-state.test.ts +3 -3
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/db-connection-isolation.test.ts +1 -1
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +3 -3
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +6 -6
- package/src/__tests__/db-drop-contact-acl-columns-migration.test.ts +159 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +3 -3
- package/src/__tests__/db-memory-graph-event-date-repair.test.ts +3 -3
- package/src/__tests__/db-migration-rollback.test.ts +48 -43
- package/src/__tests__/db-proxy-transaction.test.ts +2 -2
- package/src/__tests__/db-rename-inference-profile-snake-case-migration.test.ts +3 -3
- package/src/__tests__/db-schedule-syntax-migration.test.ts +4 -4
- package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +3 -3
- package/src/__tests__/db-slack-external-content-normalization.test.ts +4 -4
- package/src/__tests__/db-test-helpers.ts +7 -0
- package/src/__tests__/delete-propagation.test.ts +9 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +3 -3
- package/src/__tests__/device-id.test.ts +38 -1
- package/src/__tests__/dm-backfill.test.ts +9 -7
- package/src/__tests__/dm-persistence.test.ts +5 -5
- package/src/__tests__/document-conversations.test.ts +3 -3
- package/src/__tests__/document-create-dedupe.test.ts +1 -1
- package/src/__tests__/document-find-replace.test.ts +1 -1
- package/src/__tests__/document-tool-security.test.ts +1 -1
- package/src/__tests__/document-update-default-surface.test.ts +1 -1
- package/src/__tests__/drop-capability-card-state-migration.test.ts +3 -5
- package/src/__tests__/edit-propagation.test.ts +6 -6
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/empty-state-greeting-cache.test.ts +1 -1
- package/src/__tests__/events-client-registration.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +7 -23
- package/src/__tests__/file-write-tool.test.ts +3 -3
- package/src/__tests__/filing-service.test.ts +6 -5
- package/src/__tests__/followup-tools.test.ts +2 -2
- package/src/__tests__/get-skill-detail-audit.test.ts +1 -1
- package/src/__tests__/graph-extraction-event-date.test.ts +1 -1
- package/src/__tests__/guardian-action-sweep.test.ts +3 -3
- package/src/__tests__/guardian-binding-drift-heal.test.ts +48 -25
- package/src/__tests__/guardian-card-withdrawal.test.ts +3 -3
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +4 -4
- package/src/__tests__/guardian-dispatch.test.ts +28 -11
- package/src/__tests__/guardian-expiry-notifier.test.ts +4 -4
- package/src/__tests__/guardian-outbound-http.test.ts +3 -4
- package/src/__tests__/guardian-principal-id-roundtrip.test.ts +3 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +3 -3
- package/src/__tests__/guardian-routing-state.test.ts +12 -14
- package/src/__tests__/guardian-verification-voice-binding.test.ts +4 -4
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -3
- package/src/__tests__/heartbeat-disk-pressure.test.ts +7 -13
- package/src/__tests__/heartbeat-service.test.ts +31 -19
- package/src/__tests__/helpers/channel-test-adapter.ts +25 -18
- package/src/__tests__/helpers/create-guardian-binding.ts +16 -24
- package/src/__tests__/helpers/derive-guardian-delivery.ts +66 -0
- package/src/__tests__/helpers/gateway-acl-store.ts +81 -0
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +117 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/home-state-routes.test.ts +1 -1
- package/src/__tests__/http-conversation-lineage.test.ts +4 -4
- package/src/__tests__/http-user-message-parity.test.ts +8 -12
- package/src/__tests__/identity-routes.test.ts +113 -25
- package/src/__tests__/image-recovery-hook.test.ts +3 -3
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -12
- package/src/__tests__/inbound-slack-persistence.test.ts +5 -5
- package/src/__tests__/inbound-trust-verdict.test.ts +5 -4
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +13 -6
- package/src/__tests__/inference-profile-reaper.test.ts +3 -3
- package/src/__tests__/inference-profile-session-handler.test.ts +3 -3
- package/src/__tests__/inference-profile-session-ipc.test.ts +3 -3
- package/src/__tests__/init-feature-flag-overrides.test.ts +1 -4
- package/src/__tests__/injection-block.test.ts +5 -2
- package/src/__tests__/injector-background-turn.test.ts +3 -5
- package/src/__tests__/injector-chain.test.ts +25 -16
- package/src/__tests__/injector-config-quarantine-notice.test.ts +3 -5
- package/src/__tests__/injector-disk-pressure.test.ts +10 -8
- package/src/__tests__/injector-document-comments.test.ts +5 -3
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +5 -2
- package/src/__tests__/injector-registry-order-guard.test.ts +132 -0
- package/src/__tests__/injector-v3-suppression.test.ts +15 -9
- package/src/__tests__/install-meta.test.ts +104 -0
- package/src/__tests__/install-skill-routing.test.ts +1 -1
- package/src/__tests__/internal-telemetry-routes.test.ts +4 -4
- package/src/__tests__/invite-redemption-service.test.ts +274 -57
- package/src/__tests__/invite-routes-http.test.ts +37 -3
- package/src/__tests__/invite-service-ipc.test.ts +68 -5
- package/src/__tests__/job-handler-registry-guard.test.ts +129 -0
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +8 -8
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +7 -4
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -8
- package/src/__tests__/list-messages-attachments.test.ts +7 -4
- package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
- package/src/__tests__/list-messages-client-message-id.test.ts +6 -3
- package/src/__tests__/list-messages-hidden-metadata.test.ts +5 -5
- package/src/__tests__/list-messages-page-latest.test.ts +176 -7
- package/src/__tests__/list-messages-queued.test.ts +223 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +6 -3
- package/src/__tests__/llm-context-route-provider.test.ts +3 -3
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +4 -4
- package/src/__tests__/llm-request-log-call-site.test.ts +5 -5
- package/src/__tests__/llm-request-log-error-payload.test.ts +1 -1
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -1
- package/src/__tests__/llm-request-log-source-factory.test.ts +3 -3
- package/src/__tests__/llm-request-log-turn-query.test.ts +9 -6
- package/src/__tests__/llm-usage-store.test.ts +137 -3
- package/src/__tests__/log-export-routes.test.ts +3 -3
- package/src/__tests__/log-export-workspace.test.ts +3 -3
- package/src/__tests__/managed-profile-guard.test.ts +1 -1
- package/src/__tests__/managed-skill-lifecycle.test.ts +3 -3
- package/src/__tests__/managed-store.test.ts +381 -0
- package/src/__tests__/mcp-config-secret-boundary.test.ts +5 -1
- package/src/__tests__/media-stream-server-integration.test.ts +1 -1
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +4 -4
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +11 -9
- package/src/__tests__/memory-recall-log-store.test.ts +4 -4
- package/src/__tests__/memory-retrieval-budget.test.ts +1 -1
- package/src/__tests__/memory-retrieval-hook.test.ts +8 -8
- package/src/__tests__/memory-upsert-concurrency.test.ts +9 -5
- package/src/__tests__/memory-v2-static-injector.test.ts +3 -3
- package/src/__tests__/messages-after-tiebreaker.test.ts +4 -4
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/mock-gateway-ipc.ts +1 -1
- package/src/__tests__/mtime-cache.test.ts +19 -0
- package/src/__tests__/native-web-search.test.ts +11 -11
- package/src/__tests__/non-member-access-request.test.ts +24 -20
- package/src/__tests__/notification-candidate-guardian-context.test.ts +4 -4
- package/src/__tests__/notification-deep-link.test.ts +3 -3
- package/src/__tests__/notification-guardian-path.test.ts +3 -3
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +3 -3
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +2 -2
- package/src/__tests__/oauth-store.test.ts +6 -6
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/onboarding-template-contract.test.ts +27 -43
- package/src/__tests__/openai-provider.test.ts +33 -0
- package/src/__tests__/openai-responses-provider.test.ts +40 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +8 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +3 -3
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +70 -0
- package/src/__tests__/persistence-layering-guard.test.ts +222 -0
- package/src/__tests__/persistence-secret-redaction.test.ts +6 -6
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/pkb-autoinject.test.ts +1 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +3 -3
- package/src/__tests__/playbook-execution.test.ts +4 -4
- package/src/__tests__/playbook-tools.test.ts +3 -3
- package/src/__tests__/plugin-bootstrap.test.ts +78 -19
- package/src/__tests__/plugin-config-data-migration.test.ts +263 -0
- package/src/__tests__/plugin-disabled-state.test.ts +323 -0
- package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +506 -0
- package/src/__tests__/plugin-injector-contribution.test.ts +133 -0
- package/src/__tests__/plugin-route-contribution.test.ts +62 -115
- package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
- package/src/__tests__/pointer-conversation-trust.test.ts +141 -0
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +8 -1
- package/src/__tests__/process-message-background-slack.test.ts +6 -6
- package/src/__tests__/process-message-display-content.test.ts +5 -5
- package/src/__tests__/processing-flag-persist-failure.test.ts +280 -0
- package/src/__tests__/provider-usage-tracking.test.ts +4 -4
- package/src/__tests__/prune-jobs-changes-parser.test.ts +1 -1
- package/src/__tests__/prune-old-conversations-job.test.ts +5 -5
- package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
- package/src/__tests__/published-app-updater.test.ts +3 -3
- package/src/__tests__/qdrant-collection-migration.test.ts +5 -2
- package/src/__tests__/qdrant-manager.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +141 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +60 -10
- package/src/__tests__/rebuild-index-graph-nodes.test.ts +5 -5
- package/src/__tests__/recording-handler.test.ts +2 -2
- package/src/__tests__/relay-server.test.ts +183 -142
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/resolve-app-id.test.ts +3 -3
- package/src/__tests__/runtime-attachment-metadata.test.ts +16 -18
- package/src/__tests__/runtime-events-sse-bilingual.test.ts +3 -3
- package/src/__tests__/runtime-events-sse-parity.test.ts +3 -3
- package/src/__tests__/runtime-events-sse-reconnect.test.ts +3 -3
- package/src/__tests__/runtime-events-sse.test.ts +3 -3
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +489 -2
- package/src/__tests__/schedule-retry.test.ts +91 -67
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +47 -43
- package/src/__tests__/schedule-routes.test.ts +199 -187
- package/src/__tests__/schedule-store.test.ts +174 -172
- package/src/__tests__/schedule-tools.test.ts +2 -2
- package/src/__tests__/scheduler-disk-pressure.test.ts +20 -9
- package/src/__tests__/scheduler-recurrence.test.ts +72 -122
- package/src/__tests__/scheduler-reuse-conversation.test.ts +65 -60
- package/src/__tests__/scheduler-wake.test.ts +21 -31
- package/src/__tests__/scoped-approval-grants.test.ts +4 -4
- package/src/__tests__/scoped-grant-security-matrix.test.ts +4 -4
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +2 -2
- package/src/__tests__/search-skills-unified.test.ts +1 -1
- package/src/__tests__/secret-ingress-channel.test.ts +3 -3
- package/src/__tests__/secret-ingress-http.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +9 -8
- package/src/__tests__/send-endpoint-busy.test.ts +6 -6
- package/src/__tests__/sequence-store.test.ts +3 -3
- package/src/__tests__/server-history-render.test.ts +7 -4
- package/src/__tests__/settings-routes.test.ts +34 -2
- package/src/__tests__/shell-observability.test.ts +1 -0
- package/src/__tests__/skill-boundary-guard.test.ts +3 -4
- package/src/__tests__/skill-docs-sync-guard.test.ts +34 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +2 -1
- package/src/__tests__/skill-script-runner-host.test.ts +53 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
- package/src/__tests__/skill-tool-factory.test.ts +20 -0
- package/src/__tests__/skill-version-hash.test.ts +32 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +1 -1
- package/src/__tests__/skills-files-catalog-fallback.test.ts +1 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +2 -2
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +4 -4
- package/src/__tests__/slack-share-routes.test.ts +1 -1
- package/src/__tests__/slim-skill-category.test.ts +34 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/starter-task-flow.test.ts +1 -1
- package/src/__tests__/strip-memory-injections.test.ts +2 -2
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +2 -2
- package/src/__tests__/subagent-detail.test.ts +1 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-notify-parent.test.ts +3 -3
- package/src/__tests__/subagent-persistence.test.ts +122 -0
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +3 -3
- package/src/__tests__/subagent-terminal-message.test.ts +99 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
- package/src/__tests__/subagent-tools.test.ts +396 -4
- package/src/__tests__/suggestion-routes.test.ts +4 -4
- package/src/__tests__/system-prompt.test.ts +4 -4
- package/src/__tests__/task-compiler.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-memory-cleanup.test.ts +9 -7
- package/src/__tests__/task-progress-nudge-hook.test.ts +37 -94
- package/src/__tests__/task-scheduler.test.ts +94 -115
- package/src/__tests__/test-support/tool-invocation-seed.ts +5 -2
- package/src/__tests__/thread-backfill.test.ts +7 -7
- package/src/__tests__/title-generate-hook.test.ts +6 -6
- package/src/__tests__/tool-approval-handler.test.ts +4 -4
- package/src/__tests__/tool-audit-listener.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +5 -5
- package/src/__tests__/tool-executor-redaction.test.ts +11 -0
- package/src/__tests__/tool-executor.test.ts +24 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +5 -5
- package/src/__tests__/tool-preview-lifecycle.test.ts +102 -17
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +4 -4
- package/src/__tests__/tool-result-truncate-hook.test.ts +2 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -1
- package/src/__tests__/tool-start-timestamp.test.ts +69 -5
- package/src/__tests__/tools-get-route.test.ts +44 -1
- package/src/__tests__/trace-emitter.test.ts +1 -1
- package/src/__tests__/trust-context-guards.test.ts +1 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +20 -12
- package/src/__tests__/trusted-contact-multichannel.test.ts +17 -8
- package/src/__tests__/trusted-contact-verification.test.ts +60 -57
- package/src/__tests__/turn-boundary-resolution.test.ts +7 -4
- package/src/__tests__/turn-call-site.test.ts +29 -0
- package/src/__tests__/turn-events-store.test.ts +8 -5
- package/src/__tests__/twilio-routes.test.ts +3 -3
- package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +4 -8
- package/src/__tests__/usage-grouped-buckets.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +61 -3
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -6
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -24
- package/src/__tests__/voice-quality.test.ts +38 -12
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +22 -14
- package/src/__tests__/voice-session-bridge.test.ts +16 -10
- package/src/__tests__/web-search-backend-failure.test.ts +4 -4
- package/src/__tests__/workspace-lifecycle.test.ts +29 -18
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +4 -4
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +5 -5
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +3 -3
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-113-swap-balanced-profile-to-glm-5p2.test.ts +241 -0
- package/src/__tests__/workspace-migration-114-swap-quality-profile-to-opus.test.ts +172 -0
- package/src/__tests__/workspace-migration-116-rename-memory-plugin-disabled-sentinel.test.ts +128 -0
- package/src/__tests__/workspace-migration-117-normalize-stale-lean-memory-v3-defaults.test.ts +221 -0
- package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +273 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +41 -49
- package/src/a2a/__tests__/task-store.test.ts +2 -2
- package/src/a2a/task-store.ts +4 -4
- package/src/acp/__tests__/client-handler.test.ts +680 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +32 -4
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
- package/src/acp/__tests__/session-manager-persistence.test.ts +215 -7
- package/src/acp/__tests__/session-manager-resume.test.ts +113 -2
- package/src/acp/__tests__/session-manager-startup.test.ts +3 -3
- package/src/acp/__tests__/session-manager-usage.test.ts +113 -0
- package/src/acp/auto-install.ts +12 -8
- package/src/acp/client-handler.ts +140 -21
- package/src/acp/prepare-agent-env.ts +0 -12
- package/src/acp/resolve-agent.test.ts +6 -45
- package/src/acp/resolve-agent.ts +2 -4
- package/src/acp/session-manager.ts +127 -4
- package/src/acp/types.ts +18 -0
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +321 -149
- package/src/api/events/acp-session-completed.ts +33 -0
- package/src/api/events/acp-session-error.ts +21 -0
- package/src/api/events/acp-session-spawned.ts +32 -0
- package/src/api/events/acp-session-update.test.ts +122 -0
- package/src/api/events/acp-session-update.ts +51 -0
- package/src/api/events/acp-session-usage.test.ts +49 -0
- package/src/api/events/acp-session-usage.ts +31 -0
- package/src/api/events/background-tool-completed.test.ts +51 -0
- package/src/api/events/background-tool-completed.ts +29 -0
- package/src/api/events/background-tool-started.test.ts +36 -0
- package/src/api/events/background-tool-started.ts +28 -0
- package/src/api/events/conversation-error.ts +0 -1
- package/src/api/events/subagent-status-changed.ts +4 -1
- package/src/api/events/tool-use-preview-start.ts +11 -0
- package/src/api/events/tool-use-start.ts +14 -0
- package/src/api/index.ts +51 -0
- package/src/api/responses/conversation-message.ts +80 -1
- package/src/api/responses/llm-request-log-entry.ts +41 -0
- package/src/api/responses/memory-v3-selection-log.ts +5 -7
- package/src/approvals/approval-primitive.ts +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +2 -2
- package/src/approvals/guardian-decision-primitive.ts +1 -1
- package/src/approvals/guardian-expiry-notifier.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +19 -7
- package/src/approvals/scoped-approval-grants.ts +551 -0
- package/src/apps/app-store.ts +1058 -0
- package/src/apps/published-pages-store.ts +67 -0
- package/src/apps/shared-app-links-store.ts +130 -0
- package/src/background-wake/background-wake-routes.test.ts +53 -23
- package/src/background-wake/wake-intent-hooks.test.ts +18 -12
- package/src/backup/restore.ts +1 -1
- package/src/bundler/app-bundler.ts +1 -1
- package/src/calls/__tests__/relay-setup-router.test.ts +11 -19
- package/src/calls/call-controller.ts +5 -5
- package/src/calls/call-conversation-messages.ts +1 -1
- package/src/calls/call-domain.ts +5 -5
- package/src/calls/call-pointer-messages.ts +1 -1
- package/src/calls/call-store.ts +3 -3
- package/src/calls/guardian-action-sweep.ts +1 -1
- package/src/calls/guardian-dispatch.ts +15 -12
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/media-stream-server.ts +1 -1
- package/src/calls/relay-access-wait.ts +7 -7
- package/src/calls/relay-server.ts +25 -5
- package/src/calls/relay-setup-router.ts +11 -11
- package/src/calls/tts-call-strategy.ts +4 -3
- package/src/calls/voice-quality.ts +47 -59
- package/src/calls/voice-session-bridge.ts +2 -16
- package/src/channels/__tests__/types.test.ts +28 -0
- package/src/channels/channel-verification-sessions.ts +557 -0
- package/src/channels/types.ts +23 -0
- package/src/cli/commands/__tests__/conversations-new.test.ts +186 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +2 -1
- package/src/cli/commands/__tests__/conversations-wake.test.ts +147 -0
- package/src/cli/commands/__tests__/ps.test.ts +205 -0
- package/src/cli/commands/__tests__/schedules.test.ts +52 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/conversations.ts +90 -30
- package/src/cli/commands/db/__tests__/repair.test.ts +4 -3
- package/src/cli/commands/db/repair-step-conversation-backfill.ts +2 -2
- package/src/cli/commands/memory/__tests__/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/memory/__tests__/worker.test.ts +112 -183
- package/src/cli/commands/memory/memory-retrospective.ts +32 -28
- package/src/cli/commands/memory/memory-v2-compare-render.ts +1 -1
- package/src/cli/commands/memory/memory-v2.ts +2 -2
- package/src/cli/commands/memory/memory-v3.ts +2 -2
- package/src/cli/commands/memory/worker.ts +103 -113
- package/src/cli/commands/plugins.ts +150 -157
- package/src/cli/commands/ps.ts +90 -0
- package/src/cli/commands/schedules.ts +52 -8
- package/src/cli/commands/tools-run.ts +114 -0
- package/src/cli/commands/tools.ts +19 -5
- package/src/cli/lib/__tests__/cli-colors.test.ts +30 -1
- package/src/cli/lib/__tests__/install-from-github.test.ts +168 -8
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +23 -18
- package/src/cli/lib/__tests__/parse-github-plugin-spec.test.ts +121 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +10 -4
- package/src/cli/lib/cli-colors.ts +6 -0
- package/src/cli/lib/diff-plugin.ts +3 -7
- package/src/cli/lib/inspect-plugin.ts +2 -4
- package/src/cli/lib/install-from-github.ts +96 -24
- package/src/cli/lib/merge-plugin-tree.ts +5 -3
- package/src/cli/lib/parse-github-plugin-spec.ts +163 -0
- package/src/cli/lib/plugin-fingerprint.ts +22 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/cli/lib/search-plugins.ts +6 -0
- package/src/cli/lib/upgrade-plugin.ts +6 -5
- package/src/cli/program.ts +5 -8
- package/src/cli.ts +6 -3
- package/src/config/__tests__/deployment-context-defaults.test.ts +228 -0
- package/src/config/__tests__/proc-to-skills.test.ts +21 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +7 -9
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/AGENTS.md +11 -0
- package/src/config/bundled-skills/acp/SKILL.md +13 -45
- package/src/config/bundled-skills/acp/TOOLS.json +3 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +2 -2
- package/src/config/bundled-skills/app-builder/tools/app-delete.ts +2 -2
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +2 -2
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +2 -2
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +1 -1
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +1 -1
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +1 -1
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +5 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +6 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +8 -5
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +4 -1
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -4
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +7 -7
- package/src/config/bundled-skills/skill-management/TOOLS.json +45 -0
- package/src/config/bundled-skills/skill-management/tools/find-similar.ts +12 -0
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/call-site-defaults.ts +0 -8
- package/src/config/feature-flag-registry.json +16 -24
- package/src/config/llm-resolver.ts +17 -3
- package/src/config/loader.ts +38 -58
- package/src/config/memory-v3-gate.ts +16 -0
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
- package/src/config/schemas/__tests__/memory-v3.test.ts +16 -5
- package/src/config/schemas/call-site-catalog.ts +0 -19
- package/src/config/schemas/conversations.ts +8 -0
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -14
- package/src/config/schemas/memory-lifecycle.ts +10 -1
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/config/schemas/memory-v3.ts +56 -6
- package/src/config/schemas/migrations.ts +25 -0
- package/src/config/schemas/services.ts +0 -37
- package/src/config/seed-inference-profiles.ts +88 -84
- package/src/config/sync-gated-profiles.ts +6 -20
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/guardian-contact-reader.test.ts +89 -0
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +3 -14
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/canonical-guardian-store.ts +891 -0
- package/src/contacts/contact-store.ts +37 -245
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/guardian-contact-reader.ts +52 -0
- package/src/contacts/guardian-delivery-reader.ts +21 -23
- package/src/contacts/guardian-rate-limits.ts +191 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/notify-contacts-changed.ts +17 -0
- package/src/contacts/types.ts +3 -15
- package/src/context/compactor.ts +3 -3
- package/src/context/strip-injections.ts +1 -1
- package/src/conversations/__tests__/message-consolidation.test.ts +10 -26
- package/src/conversations/job-handlers/summarization.ts +246 -0
- package/src/conversations/message-consolidation.ts +1 -1
- package/src/credential-execution/ces-runtime.ts +285 -0
- package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +25 -22
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +5 -4
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/__tests__/daemon-readiness.test.ts +51 -0
- package/src/daemon/__tests__/embedding-reconcile.test.ts +456 -0
- package/src/daemon/__tests__/inference-profile-notification.test.ts +9 -6
- package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +119 -0
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +2 -2
- package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +3 -5
- package/src/daemon/app-source-watcher.ts +71 -10
- package/src/daemon/channel-ui-capability.ts +25 -0
- package/src/daemon/config-watcher.ts +71 -47
- package/src/daemon/conversation-agent-loop-handlers.ts +266 -49
- package/src/daemon/conversation-agent-loop.ts +166 -89
- package/src/daemon/conversation-attachments.ts +2 -2
- package/src/daemon/conversation-error.ts +8 -25
- package/src/daemon/conversation-evictor.ts +42 -9
- package/src/daemon/conversation-history.ts +8 -234
- package/src/daemon/conversation-initial-prompt.ts +77 -0
- package/src/daemon/conversation-launch.ts +2 -2
- package/src/daemon/conversation-lifecycle.ts +32 -5
- package/src/daemon/conversation-messaging.ts +25 -11
- package/src/daemon/conversation-notifiers.ts +1 -1
- package/src/daemon/conversation-process.ts +27 -15
- package/src/daemon/conversation-queue-manager.ts +6 -0
- package/src/daemon/conversation-registry.ts +6 -5
- package/src/daemon/conversation-runtime-assembly.ts +45 -31
- package/src/daemon/conversation-skill-tools.ts +26 -2
- package/src/daemon/conversation-slash.ts +1 -1
- package/src/daemon/conversation-store.ts +47 -42
- package/src/daemon/conversation-surfaces.ts +4 -4
- package/src/daemon/conversation-tool-setup.ts +2 -11
- package/src/daemon/conversation-usage.ts +4 -2
- package/src/daemon/conversation-workspace.ts +2 -2
- package/src/daemon/conversation.ts +104 -17
- package/src/daemon/daemon-readiness.ts +33 -0
- package/src/daemon/embedding-reconcile.ts +340 -0
- package/src/daemon/event-loop-watchdog.ts +1 -1
- package/src/daemon/external-plugins-bootstrap.ts +123 -92
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +2 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +3 -5
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +3 -3
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +7 -13
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +2 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +22 -21
- package/src/daemon/handlers/config-a2a.ts +3 -3
- package/src/daemon/handlers/config-channels.ts +21 -33
- package/src/daemon/handlers/config-embeddings.ts +1 -1
- package/src/daemon/handlers/conversation-history.ts +2 -2
- package/src/daemon/handlers/conversations.ts +7 -56
- package/src/daemon/handlers/recording.ts +6 -3
- package/src/daemon/handlers/shared.ts +2 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +714 -1242
- package/src/daemon/message-protocol.ts +3 -3
- package/src/daemon/message-provenance.ts +1 -1
- package/src/daemon/message-types/acp.ts +27 -1
- package/src/daemon/message-types/background-tools.ts +28 -0
- package/src/daemon/message-types/bookmarks.ts +1 -1
- package/src/daemon/message-types/conversations.ts +10 -0
- package/src/daemon/message-types/skills.ts +20 -2
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/pointer-conversation-trust.ts +66 -0
- package/src/daemon/process-message.ts +24 -3
- package/src/daemon/providers-setup.ts +16 -14
- package/src/daemon/shutdown-handlers.ts +195 -166
- package/src/daemon/skill-memory-refresh.ts +3 -3
- package/src/daemon/status.ts +15 -0
- package/src/daemon/tool-setup-types.ts +17 -0
- package/src/daemon/tool-side-effects.ts +4 -4
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/trust-context.ts +1 -1
- package/src/daemon/turn-call-site.ts +25 -0
- package/src/daemon/turn-latency-tracker.test.ts +137 -0
- package/src/daemon/turn-latency-tracker.ts +134 -0
- package/src/daemon/wake-conversation-ops.ts +14 -4
- package/src/documents/document-comments-store.test.ts +2 -2
- package/src/documents/document-comments-store.ts +1 -1
- package/src/documents/document-store.ts +1 -1
- package/src/events/tool-audit-listener.ts +4 -4
- package/src/events/tool-permission-telemetry-listener.ts +1 -1
- package/src/export/__tests__/transcript-formatter.test.ts +1 -1
- package/src/export/transcript-formatter.ts +1 -1
- package/src/followups/followup-store.ts +2 -2
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +2 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +45 -39
- package/src/heartbeat/heartbeat-run-store.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +34 -11
- package/src/home/__tests__/relationship-state-writer.test.ts +1 -1
- package/src/home/__tests__/suggested-prompts.test.ts +1 -1
- package/src/home/conversation-starter-checkpoints.ts +64 -0
- package/src/home/feed-source-enrichment.ts +3 -3
- package/src/home/home-greeting-cache.ts +1 -1
- package/src/home/job-handlers/conversation-starters.ts +484 -0
- package/src/home/relationship-state-writer.ts +6 -1
- package/src/home/suggested-prompts.ts +1 -1
- package/src/hooks/hook-loader.ts +141 -6
- package/src/hooks/registry.ts +129 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +1 -1
- package/src/ipc/__tests__/inject-local-actor-header.test.ts +33 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +79 -33
- package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +3 -3
- package/src/ipc/routes/db-proxy-transaction.ts +1 -1
- package/src/ipc/routes/db-proxy.ts +4 -2
- package/src/jobs/register-job-handlers.ts +78 -0
- package/src/jobs/worker.ts +91 -0
- package/src/live-voice/__tests__/live-voice-archive.test.ts +5 -5
- package/src/live-voice/live-voice-archive.ts +2 -2
- package/src/mcp/manager.ts +11 -0
- package/src/media/app-icon-generator.ts +1 -1
- package/src/media/job-handlers/media-processing.ts +126 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/content/parse.test.ts +34 -0
- package/src/messaging/content/parse.ts +23 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +196 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/render.test.ts +309 -0
- package/src/messaging/providers/slack/render.ts +459 -0
- package/src/messaging/providers/slack/send.test.ts +39 -0
- package/src/messaging/providers/slack/send.ts +38 -18
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -2
- package/src/messaging/providers/telegram-bot/api.ts +17 -3
- package/src/messaging/providers/telegram-bot/render.test.ts +146 -0
- package/src/messaging/providers/telegram-bot/render.ts +292 -0
- package/src/messaging/providers/telegram-bot/send.test.ts +197 -0
- package/src/messaging/providers/telegram-bot/send.ts +70 -2
- package/src/messaging/providers/telegram-bot/transport.ts +60 -0
- package/src/messaging/providers/whatsapp/adapter.ts +2 -2
- package/src/messaging/providers/whatsapp/send.ts +1 -1
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/__tests__/deterministic-checks.test.ts +3 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -1
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/canonical-delivery-recorder.ts +1 -1
- package/src/notifications/conversation-candidates.ts +3 -3
- package/src/notifications/conversation-pairing.ts +25 -16
- package/src/notifications/decisions-store.ts +2 -2
- package/src/notifications/deliveries-store.ts +3 -3
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/deterministic-checks.ts +2 -2
- package/src/notifications/emit-signal.ts +6 -12
- package/src/notifications/events-store.ts +2 -2
- package/src/notifications/home-feed-side-effect.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -2
- package/src/notifications/signal.ts +1 -1
- package/src/oauth/oauth-store.ts +3 -3
- package/src/onboarding/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/onboarding/checkin-event.test.ts +5 -2
- package/src/onboarding/checkin-event.ts +7 -1
- package/src/onboarding/onboarding-events-store.ts +161 -0
- package/src/permissions/checker.test.ts +319 -1
- package/src/permissions/checker.ts +52 -0
- package/src/permissions/types.ts +23 -2
- package/src/persistence/__tests__/db-init-migrations-ok.test.ts +20 -0
- package/src/persistence/__tests__/worker-control.test.ts +260 -0
- package/src/persistence/attachments-store.ts +1136 -0
- package/src/persistence/checkpoints.ts +65 -0
- package/src/persistence/compaction-ledger-store.ts +107 -0
- package/src/persistence/conversation-attention-store.ts +689 -0
- package/src/persistence/conversation-bootstrap.ts +59 -0
- package/src/persistence/conversation-crud.ts +3404 -0
- package/src/persistence/conversation-group-migration.test.ts +99 -0
- package/src/persistence/conversation-key-store.ts +274 -0
- package/src/persistence/conversation-queries.ts +768 -0
- package/src/persistence/conversation-row-batch-delete.ts +212 -0
- package/src/persistence/conversation-title-service.ts +755 -0
- package/src/persistence/db-async-query.ts +329 -0
- package/src/persistence/db-connection.ts +242 -0
- package/src/persistence/db-init.ts +282 -0
- package/src/persistence/db-maintenance.ts +158 -0
- package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +156 -0
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +307 -0
- package/src/persistence/embeddings/__tests__/reconcile-decision.test.ts +95 -0
- package/src/persistence/embeddings/embed.ts +87 -0
- package/src/persistence/embeddings/embedding-backend.test.ts +355 -0
- package/src/persistence/embeddings/embedding-backend.ts +968 -0
- package/src/persistence/embeddings/embedding-billing-breaker.ts +96 -0
- package/src/persistence/embeddings/embedding-cache.ts +139 -0
- package/src/persistence/embeddings/embedding-identity.ts +88 -0
- package/src/persistence/embeddings/embedding-local.ts +587 -0
- package/src/persistence/embeddings/embedding-ollama.ts +79 -0
- package/src/persistence/embeddings/embedding-runtime-manager.ts +573 -0
- package/src/persistence/embeddings/embedding-types.ts +97 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +368 -0
- package/src/persistence/embeddings/qdrant-circuit-breaker.ts +119 -0
- package/src/persistence/embeddings/qdrant-client.test.ts +60 -0
- package/src/persistence/embeddings/qdrant-client.ts +842 -0
- package/src/persistence/embeddings/qdrant-manager.ts +450 -0
- package/src/persistence/embeddings/reconcile-decision.ts +53 -0
- package/src/persistence/embeddings/sparse-tokenize.ts +51 -0
- package/src/persistence/external-conversation-store.ts +315 -0
- package/src/persistence/fork-message-copy.ts +225 -0
- package/src/persistence/job-handlers/cleanup.ts +266 -0
- package/src/persistence/job-utils.ts +286 -0
- package/src/persistence/jobs-store.ts +866 -0
- package/src/persistence/jobs-worker.ts +842 -0
- package/src/persistence/llm-request-log-source-clickhouse.ts +525 -0
- package/src/persistence/llm-request-log-store.ts +740 -0
- package/src/persistence/llm-usage-store.ts +961 -0
- package/src/persistence/memory-lifecycle-hooks.test.ts +81 -0
- package/src/persistence/memory-lifecycle-hooks.ts +120 -0
- package/src/persistence/message-content.test.ts +35 -0
- package/src/persistence/migrations/126-backfill-guardian-principal-id.ts +300 -0
- package/src/persistence/migrations/140-backfill-usage-cache-accounting.ts +364 -0
- package/src/persistence/migrations/142-rename-verification-session-id-column.ts +55 -0
- package/src/persistence/migrations/182-oauth-providers-display-metadata.ts +22 -0
- package/src/persistence/migrations/196-strip-integration-prefix-from-provider-keys.ts +175 -0
- package/src/persistence/migrations/199-guardian-request-enrichment-columns.ts +50 -0
- package/src/persistence/migrations/202-memory-graph-tables.ts +128 -0
- package/src/persistence/migrations/203-drop-memory-items-tables.ts +53 -0
- package/src/persistence/migrations/206-scrub-corrupted-image-attachments.ts +121 -0
- package/src/persistence/migrations/209-strip-thinking-from-consolidated.ts +196 -0
- package/src/persistence/migrations/211-memory-recall-logs-query-context.ts +14 -0
- package/src/persistence/migrations/220-normalize-user-file-by-principal.ts +171 -0
- package/src/persistence/migrations/222-strip-placeholder-sentinels-from-messages.ts +179 -0
- package/src/persistence/migrations/236-tool-invocations-matched-rule-id.ts +23 -0
- package/src/persistence/migrations/243-provider-connections.ts +80 -0
- package/src/persistence/migrations/244-provider-connection-status-label.ts +29 -0
- package/src/persistence/migrations/246-backfill-provider-connection-label.ts +83 -0
- package/src/persistence/migrations/255-channel-inbound-delivery-attempts.ts +17 -0
- package/src/persistence/migrations/301-create-watchdog-events.ts +42 -0
- package/src/persistence/migrations/302-create-compaction-events.ts +107 -0
- package/src/persistence/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/persistence/migrations/304-add-llm-usage-cron-run-id.ts +31 -0
- package/src/persistence/migrations/305-drop-contact-acl-columns.ts +49 -0
- package/src/persistence/migrations/306-rewrite-frontier-profile-pins.test.ts +110 -0
- package/src/persistence/migrations/306-rewrite-frontier-profile-pins.ts +65 -0
- package/src/persistence/migrations/307-acp-session-history-usage-columns.test.ts +124 -0
- package/src/persistence/migrations/307-acp-session-history-usage-columns.ts +36 -0
- package/src/persistence/migrations/308-acp-session-history-token-columns.test.ts +119 -0
- package/src/persistence/migrations/308-acp-session-history-token-columns.ts +32 -0
- package/src/persistence/migrations/309-drop-redundant-indexes.ts +60 -0
- package/src/persistence/migrations/310-llm-request-log-latency-breakdown.ts +37 -0
- package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
- package/src/persistence/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +328 -0
- package/src/persistence/migrations/__tests__/222-strip-placeholder-sentinels-from-messages.test.ts +273 -0
- package/src/persistence/migrations/__tests__/244-provider-connection-status-label.test.ts +90 -0
- package/src/persistence/migrations/__tests__/246-backfill-provider-connection-label.test.ts +250 -0
- package/src/persistence/migrations/__tests__/run-migrations.test.ts +380 -0
- package/src/persistence/migrations/helpers/relocation.ts +230 -0
- package/src/persistence/migrations/validate-migration-state.ts +279 -0
- package/src/persistence/schedule-attribution-sql.ts +106 -0
- package/src/persistence/schema/acp.ts +51 -0
- package/src/persistence/schema/contacts.ts +110 -0
- package/src/persistence/schema/conversations.ts +235 -0
- package/src/persistence/schema/infrastructure.ts +389 -0
- package/src/persistence/steps.ts +1312 -0
- package/src/persistence/subagent-store.ts +122 -0
- package/src/persistence/worker-control.ts +277 -0
- package/src/platform/sync-identity.ts +24 -0
- package/src/playbooks/playbook-compiler.ts +2 -2
- package/src/plugin-api/event-hub-facade.ts +172 -0
- package/src/plugin-api/index.ts +14 -1
- package/src/plugin-api/types.ts +61 -24
- package/src/plugins/defaults/channel/injectors.ts +101 -0
- package/src/plugins/defaults/channel/package.json +14 -0
- package/src/plugins/defaults/compaction/compact.ts +2 -1
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +2 -1
- package/src/plugins/defaults/compaction/window-manager.ts +8 -7
- package/src/plugins/defaults/documents/injectors.ts +125 -0
- package/src/plugins/defaults/documents/package.json +14 -0
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +6 -2
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/history-repair/terminal.ts +1 -1
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +23 -9
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +2 -3
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +7 -14
- package/src/plugins/defaults/image-fallback/src/vision-caption.ts +12 -4
- package/src/plugins/defaults/image-recovery/recover.ts +6 -3
- package/src/plugins/defaults/index.ts +235 -78
- package/src/plugins/defaults/injection-presence.ts +80 -0
- package/src/plugins/defaults/injector-order.ts +54 -0
- package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +5 -2
- package/src/plugins/defaults/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +264 -0
- package/src/plugins/defaults/memory/__tests__/bookmark-schema.test.ts +181 -0
- package/src/plugins/defaults/memory/__tests__/clear-stale-processing-flags.test.ts +52 -0
- package/src/plugins/defaults/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +843 -0
- package/src/plugins/defaults/memory/__tests__/conversation-row-batch-delete.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/conversation-types.test.ts +36 -0
- package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +172 -0
- package/src/plugins/defaults/memory/__tests__/db-busy-timeout.test.ts +26 -0
- package/src/plugins/defaults/memory/__tests__/db-logs-attach.test.ts +113 -0
- package/src/plugins/defaults/memory/__tests__/db-maintenance.test.ts +238 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +116 -0
- package/src/plugins/defaults/memory/__tests__/embedding-cache.test.ts +137 -0
- package/src/plugins/defaults/memory/__tests__/find-analysis-conversation.test.ts +197 -0
- package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +138 -0
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +232 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +245 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-job-classes.test.ts +52 -0
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +115 -0
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +508 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +160 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +1568 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +549 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +195 -0
- package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +221 -0
- package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +275 -0
- package/src/plugins/defaults/memory/__tests__/memory-v3-selections-migration.test.ts +103 -0
- package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/plugins/defaults/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +130 -0
- package/src/plugins/defaults/memory/activation-session-store.ts +43 -0
- package/src/plugins/defaults/memory/anisotropy.ts +443 -0
- package/src/plugins/defaults/memory/context-search/agent-protocol.ts +428 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1311 -0
- package/src/plugins/defaults/memory/context-search/search.ts +297 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +341 -0
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +600 -0
- package/src/plugins/defaults/memory/context-search/sources/memory.ts +101 -0
- package/src/plugins/defaults/memory/context-search/types.ts +48 -0
- package/src/plugins/defaults/memory/filing-service.ts +430 -0
- package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +663 -0
- package/src/plugins/defaults/memory/graph/__tests__/handle-remember-v2.test.ts +321 -0
- package/src/plugins/defaults/memory/graph/bootstrap.test.ts +287 -0
- package/src/plugins/defaults/memory/graph/bootstrap.ts +544 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +454 -0
- package/src/plugins/defaults/memory/graph/consolidation.ts +712 -0
- package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +1181 -0
- package/src/plugins/defaults/memory/graph/decay.ts +195 -0
- package/src/plugins/defaults/memory/graph/extraction-job.ts +77 -0
- package/src/plugins/defaults/memory/graph/extraction.ts +1462 -0
- package/src/plugins/defaults/memory/graph/graph-memory-state-store.ts +50 -0
- package/src/plugins/defaults/memory/graph/graph-search.test.ts +132 -0
- package/src/plugins/defaults/memory/graph/graph-search.ts +294 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +29 -0
- package/src/plugins/defaults/memory/graph/injection.ts +469 -0
- package/src/plugins/defaults/memory/graph/narrative.ts +267 -0
- package/src/plugins/defaults/memory/graph/pattern-scan.ts +269 -0
- package/src/plugins/defaults/memory/graph/retriever.test.ts +624 -0
- package/src/plugins/defaults/memory/graph/retriever.ts +1357 -0
- package/src/plugins/defaults/memory/graph/store.test.ts +1093 -0
- package/src/plugins/defaults/memory/graph/store.ts +923 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +195 -0
- package/src/plugins/defaults/memory/graph/tools.ts +95 -0
- package/src/plugins/defaults/memory/hooks/init.ts +26 -0
- package/src/plugins/defaults/memory/hooks/post-compact.ts +115 -0
- package/src/plugins/defaults/memory/hooks/shutdown.ts +14 -0
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +371 -0
- package/src/plugins/defaults/memory/indexer.ts +409 -0
- package/src/plugins/defaults/memory/injectors.ts +388 -0
- package/src/plugins/defaults/memory/job-handlers/__tests__/backfill-lexical-index.test.ts +310 -0
- package/src/plugins/defaults/memory/job-handlers/__tests__/index-message-lexical.test.ts +265 -0
- package/src/plugins/defaults/memory/job-handlers/backfill-lexical-index.ts +108 -0
- package/src/plugins/defaults/memory/job-handlers/backfill.ts +107 -0
- package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +236 -0
- package/src/plugins/defaults/memory/job-handlers/embedding.ts +133 -0
- package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +117 -0
- package/src/plugins/defaults/memory/job-handlers/index-message-lexical.ts +106 -0
- package/src/plugins/defaults/memory/job-handlers.ts +220 -0
- package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +565 -0
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +424 -0
- package/src/plugins/defaults/memory/jobs/embed-pkb-file.test.ts +169 -0
- package/src/plugins/defaults/memory/jobs/embed-pkb-file.ts +59 -0
- package/src/plugins/defaults/memory/memory-recall-log-store.ts +172 -0
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +60 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +131 -0
- package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +94 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +958 -0
- package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +264 -0
- package/src/plugins/defaults/memory/memory-retrospective-state.ts +259 -0
- package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +91 -0
- package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +216 -0
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +173 -0
- package/src/plugins/defaults/memory/package.json +14 -0
- package/src/plugins/defaults/memory/persistence-hooks.ts +119 -0
- package/src/plugins/defaults/memory/pkb/autoinject.ts +61 -0
- package/src/plugins/defaults/memory/pkb/context.ts +50 -0
- package/src/plugins/defaults/memory/pkb/pkb-index.test.ts +371 -0
- package/src/plugins/defaults/memory/pkb/pkb-index.ts +256 -0
- package/src/plugins/defaults/memory/pkb/pkb-reconcile.test.ts +254 -0
- package/src/plugins/defaults/memory/pkb/pkb-reconcile.ts +144 -0
- package/src/plugins/defaults/memory/pkb/pkb-search.test.ts +589 -0
- package/src/plugins/defaults/memory/pkb/pkb-search.ts +178 -0
- package/src/plugins/defaults/memory/pkb/types.ts +67 -0
- package/src/plugins/defaults/memory/rerank-local.ts +374 -0
- package/src/plugins/defaults/memory/routes/__tests__/memory-v2-routes.test.ts +165 -0
- package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +347 -0
- package/src/plugins/defaults/memory/routes/memory-eval-routes.ts +179 -0
- package/src/plugins/defaults/memory/routes/memory-item-routes.test.ts +922 -0
- package/src/plugins/defaults/memory/routes/memory-item-routes.ts +814 -0
- package/src/plugins/defaults/memory/routes/memory-v2-routes.ts +850 -0
- package/src/plugins/defaults/memory/routes/memory-v3-routes.ts +125 -0
- package/src/plugins/defaults/memory/routes/messages-lexical-routes.ts +67 -0
- package/src/plugins/defaults/memory/search/semantic.ts +241 -0
- package/src/plugins/defaults/memory/segmenter.ts +76 -0
- package/src/plugins/defaults/memory/startup.ts +234 -0
- package/src/plugins/defaults/memory/tail-reinjection-strip.ts +65 -0
- package/src/plugins/defaults/memory/task-memory-cleanup.ts +241 -0
- package/src/plugins/defaults/memory/tools.test.ts +382 -0
- package/src/plugins/defaults/memory/tools.ts +87 -0
- package/src/plugins/defaults/memory/v2/__tests__/activation-store.test.ts +207 -0
- package/src/plugins/defaults/memory/v2/__tests__/activation.test.ts +1273 -0
- package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +510 -0
- package/src/plugins/defaults/memory/v2/__tests__/cli-command-store.test.ts +432 -0
- package/src/plugins/defaults/memory/v2/__tests__/consolidation-job.test.ts +744 -0
- package/src/plugins/defaults/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +205 -0
- package/src/plugins/defaults/memory/v2/__tests__/frontmatter-sweep.test.ts +136 -0
- package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +186 -0
- package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +257 -0
- package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +230 -0
- package/src/plugins/defaults/memory/v2/__tests__/harness-runner.test.ts +135 -0
- package/src/plugins/defaults/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +2308 -0
- package/src/plugins/defaults/memory/v2/__tests__/memory-v2-startup.test.ts +86 -0
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +940 -0
- package/src/plugins/defaults/memory/v2/__tests__/page-index.test.ts +760 -0
- package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +332 -0
- package/src/plugins/defaults/memory/v2/__tests__/qdrant.test.ts +974 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1198 -0
- package/src/plugins/defaults/memory/v2/__tests__/sim.test.ts +704 -0
- package/src/plugins/defaults/memory/v2/__tests__/skill-content.test.ts +115 -0
- package/src/plugins/defaults/memory/v2/__tests__/skill-store.test.ts +986 -0
- package/src/plugins/defaults/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/plugins/defaults/memory/v2/__tests__/static-context.test.ts +223 -0
- package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +530 -0
- package/src/plugins/defaults/memory/v2/activation-store.ts +185 -0
- package/src/plugins/defaults/memory/v2/activation.ts +560 -0
- package/src/plugins/defaults/memory/v2/backfill-jobs.ts +296 -0
- package/src/plugins/defaults/memory/v2/cli-command-store.ts +341 -0
- package/src/plugins/defaults/memory/v2/consolidation-job.ts +550 -0
- package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +98 -0
- package/src/plugins/defaults/memory/v2/harness/compare.ts +57 -0
- package/src/plugins/defaults/memory/v2/harness/oracle.ts +148 -0
- package/src/plugins/defaults/memory/v2/harness/replay-input.ts +243 -0
- package/src/plugins/defaults/memory/v2/harness/retriever.ts +74 -0
- package/src/plugins/defaults/memory/v2/harness/router-retriever.ts +43 -0
- package/src/plugins/defaults/memory/v2/injected-block-slugs.ts +79 -0
- package/src/plugins/defaults/memory/v2/injection-events.ts +101 -0
- package/src/plugins/defaults/memory/v2/injection.ts +932 -0
- package/src/plugins/defaults/memory/v2/memory-v2-startup.test.ts +203 -0
- package/src/plugins/defaults/memory/v2/memory-v2-startup.ts +378 -0
- package/src/plugins/defaults/memory/v2/migration.ts +774 -0
- package/src/plugins/defaults/memory/v2/page-index.ts +459 -0
- package/src/plugins/defaults/memory/v2/page-store.ts +443 -0
- package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +868 -0
- package/src/plugins/defaults/memory/v2/prompts/router.ts +148 -0
- package/src/plugins/defaults/memory/v2/qdrant.ts +939 -0
- package/src/plugins/defaults/memory/v2/reranker.ts +184 -0
- package/src/plugins/defaults/memory/v2/router.ts +602 -0
- package/src/plugins/defaults/memory/v2/sim.ts +310 -0
- package/src/plugins/defaults/memory/v2/skill-content.ts +61 -0
- package/src/plugins/defaults/memory/v2/skill-store.ts +431 -0
- package/src/plugins/defaults/memory/v2/sparse-bm25.ts +245 -0
- package/src/plugins/defaults/memory/v2/static-context.ts +100 -0
- package/src/plugins/defaults/memory/v2/sweep-job.ts +323 -0
- package/src/plugins/defaults/memory/v3/__tests__/candidate-match.test.ts +316 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +1163 -0
- package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +211 -0
- package/src/plugins/defaults/memory/v3/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +685 -0
- package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +365 -0
- package/src/plugins/defaults/memory/v3/__tests__/maintain-job.test.ts +1012 -0
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +977 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +599 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +128 -0
- package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +756 -0
- package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +422 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +501 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +885 -0
- package/src/plugins/defaults/memory/v3/__tests__/tuning-profile.test.ts +96 -0
- package/src/plugins/defaults/memory/v3/candidate-match.ts +197 -0
- package/src/plugins/defaults/memory/v3/capabilities.ts +88 -0
- package/src/plugins/defaults/memory/v3/card.ts +160 -0
- package/src/plugins/defaults/memory/v3/dense.ts +108 -0
- package/src/plugins/defaults/memory/v3/edge.ts +252 -0
- package/src/plugins/defaults/memory/v3/entity-lane.test.ts +124 -0
- package/src/plugins/defaults/memory/v3/entity-lane.ts +113 -0
- package/src/plugins/defaults/memory/v3/ever-injected-store.test.ts +307 -0
- package/src/plugins/defaults/memory/v3/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory/v3/hot-set.test.ts +143 -0
- package/src/plugins/defaults/memory/v3/hot-set.ts +85 -0
- package/src/plugins/defaults/memory/v3/injector.ts +423 -0
- package/src/plugins/defaults/memory/v3/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory/v3/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory/v3/maintain-job.ts +988 -0
- package/src/plugins/defaults/memory/v3/orchestrate.ts +483 -0
- package/src/plugins/defaults/memory/v3/page-content.ts +102 -0
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +147 -0
- package/src/plugins/defaults/memory/v3/pool-select.ts +586 -0
- package/src/plugins/defaults/memory/v3/prune.test.ts +760 -0
- package/src/plugins/defaults/memory/v3/prune.ts +469 -0
- package/src/plugins/defaults/memory/v3/render-injection.ts +92 -0
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +471 -0
- package/src/plugins/defaults/memory/v3/section-needle.ts +215 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +277 -0
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +656 -0
- package/src/plugins/defaults/memory/v3/tuning-profile.ts +91 -0
- package/src/plugins/defaults/memory/v3/types.ts +130 -0
- package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +731 -0
- package/src/plugins/defaults/session/injectors.ts +84 -0
- package/src/plugins/defaults/session/package.json +14 -0
- package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +6 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +39 -45
- package/src/plugins/defaults/title-generate/hooks/stop.ts +4 -5
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +3 -3
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +5 -3
- package/src/plugins/defaults/turn-context/injectors.ts +53 -0
- package/src/plugins/defaults/turn-context/package.json +14 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +235 -0
- package/src/plugins/defaults/workspace/injectors.ts +295 -0
- package/src/plugins/defaults/workspace/package.json +14 -0
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/injector-registry.ts +127 -0
- package/src/plugins/job-handler-registry.ts +94 -0
- package/src/plugins/mtime-cache.ts +27 -47
- package/src/plugins/pipeline.ts +1 -1
- package/src/plugins/registry.ts +40 -132
- package/src/plugins/types.ts +44 -1
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/prompts/system-prompt.ts +1 -1
- package/src/prompts/templates/BOOTSTRAP.md +16 -119
- package/src/prompts/templates/SOUL.md +4 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/inference.test.ts +6 -6
- package/src/providers/__tests__/placeholder-sentinels.test.ts +80 -0
- package/src/providers/__tests__/retry-callsite.test.ts +31 -3
- package/src/providers/__tests__/satellite-connection-routing.test.ts +1 -1
- package/src/providers/anthropic/client.ts +23 -24
- package/src/providers/call-site-routing.ts +42 -1
- package/src/providers/connection-resolution.ts +7 -3
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +7 -8
- package/src/providers/inference/__tests__/base-url-security.test.ts +8 -8
- package/src/providers/inference/__tests__/connections-label.test.ts +5 -5
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +4 -4
- package/src/providers/inference/backfill.ts +9 -5
- package/src/providers/inference/connections.ts +2 -2
- package/src/providers/inference/resolve-auth.ts +3 -3
- package/src/providers/model-catalog.ts +38 -0
- package/src/providers/openai/chat-completions-provider.ts +14 -1
- package/src/providers/openai/responses-provider.ts +18 -0
- package/src/providers/placeholder-sentinels.ts +50 -15
- package/src/providers/provider-send-message.ts +7 -1
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +3 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/stop-reasons.ts +32 -0
- package/src/providers/tool-progress-events.test.ts +50 -0
- package/src/providers/tool-progress-events.ts +99 -0
- package/src/providers/types.ts +14 -1
- package/src/providers/usage-tracking.ts +7 -1
- package/src/runtime/__tests__/agent-wake.test.ts +100 -29
- package/src/runtime/__tests__/background-job-runner.test.ts +19 -2
- package/src/runtime/__tests__/channel-verification-service.test.ts +9 -5
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +37 -32
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +3 -3
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/pre-first-message-gate.test.ts +1 -1
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +4 -5
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/agent-wake.ts +69 -12
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-event.ts +86 -12
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/background-job-runner.ts +30 -10
- package/src/runtime/channel-reply-delivery.ts +6 -11
- package/src/runtime/channel-retry-sweep.ts +23 -3
- package/src/runtime/channel-verification-service.ts +9 -9
- package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
- package/src/runtime/finalize-event-delivery.ts +2 -2
- package/src/runtime/guardian-action-service.ts +1 -2
- package/src/runtime/guardian-reply-router.ts +1 -1
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/http-server.ts +45 -1
- package/src/runtime/invite-redemption-service.ts +36 -13
- package/src/runtime/invite-service.ts +6 -4
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +1 -1
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +74 -8
- package/src/runtime/routes/__tests__/backup-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +3 -3
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +17 -23
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +3 -3
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +5 -5
- package/src/runtime/routes/__tests__/contact-routes.test.ts +188 -14
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +5 -5
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +5 -5
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +3 -3
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +12 -12
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +4 -4
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +3 -3
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +3 -3
- package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +21 -7
- package/src/runtime/routes/__tests__/memory-worker-routes.test.ts +306 -0
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +395 -26
- package/src/runtime/routes/__tests__/ps-routes.test.ts +114 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +6 -6
- package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +72 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +4 -4
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +3 -2
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/wake-conversation-routes.test.ts +125 -0
- package/src/runtime/routes/acp-routes-event-log.test.ts +283 -0
- package/src/runtime/routes/acp-routes-list.test.ts +24 -25
- package/src/runtime/routes/acp-routes.test.ts +4 -4
- package/src/runtime/routes/acp-routes.ts +31 -4
- package/src/runtime/routes/app-management-routes.ts +9 -9
- package/src/runtime/routes/app-routes.ts +3 -7
- package/src/runtime/routes/approval-routes.ts +2 -4
- package/src/runtime/routes/attachment-routes.ts +2 -2
- package/src/runtime/routes/audit-routes.ts +2 -3
- package/src/runtime/routes/background-tool-routes.ts +34 -5
- package/src/runtime/routes/background-wake-routes.ts +19 -5
- package/src/runtime/routes/backup-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +3 -3
- package/src/runtime/routes/brain-graph-routes.ts +2 -2
- package/src/runtime/routes/btw-routes.ts +1 -1
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +1 -1
- package/src/runtime/routes/channel-delivery-routes.ts +1 -1
- package/src/runtime/routes/channel-readiness-routes.ts +7 -3
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +3 -8
- package/src/runtime/routes/consolidation-routes.ts +6 -6
- package/src/runtime/routes/contact-routes.ts +98 -32
- package/src/runtime/routes/conversation-attention-routes.ts +2 -2
- package/src/runtime/routes/conversation-cli-routes.ts +32 -7
- package/src/runtime/routes/conversation-compaction-routes.ts +4 -4
- package/src/runtime/routes/conversation-list-routes.ts +9 -37
- package/src/runtime/routes/conversation-management-routes.ts +9 -46
- package/src/runtime/routes/conversation-query-routes.ts +35 -11
- package/src/runtime/routes/conversation-routes.ts +174 -23
- package/src/runtime/routes/conversation-starter-routes.ts +11 -5
- package/src/runtime/routes/conversations-import-routes.ts +10 -6
- package/src/runtime/routes/debug-routes.ts +3 -3
- package/src/runtime/routes/defer-routes.ts +4 -4
- package/src/runtime/routes/documents-routes.ts +1 -1
- package/src/runtime/routes/empty-state-greeting-cache.ts +1 -1
- package/src/runtime/routes/events-routes.ts +4 -4
- package/src/runtime/routes/filing-routes.ts +1 -1
- package/src/runtime/routes/global-search-routes.ts +4 -4
- package/src/runtime/routes/group-routes.ts +1 -1
- package/src/runtime/routes/guardian-action-routes.ts +1 -1
- package/src/runtime/routes/heartbeat-routes.ts +2 -2
- package/src/runtime/routes/home-feed-routes.ts +10 -5
- package/src/runtime/routes/identity-routes.ts +33 -14
- package/src/runtime/routes/inbound-conversation.ts +3 -3
- package/src/runtime/routes/inbound-message-handler.ts +25 -25
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +8 -4
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +13 -16
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +5 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +15 -19
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +12 -12
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +27 -8
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +5 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- package/src/runtime/routes/index.ts +8 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profile-session-reaper.ts +1 -1
- package/src/runtime/routes/inference-provider-connection-routes.ts +5 -2
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +3 -4
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +1 -1
- package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
- package/src/runtime/routes/log-export-routes.ts +2 -2
- package/src/runtime/routes/memory-worker-routes.ts +227 -0
- package/src/runtime/routes/migration-rollback-routes.ts +11 -12
- package/src/runtime/routes/migration-routes.ts +211 -10
- package/src/runtime/routes/notification-routes.ts +2 -2
- package/src/runtime/routes/platform-routes.ts +3 -0
- package/src/runtime/routes/playground/helpers.ts +3 -3
- package/src/runtime/routes/plugins-routes.ts +180 -7
- package/src/runtime/routes/ps-routes.ts +37 -66
- package/src/runtime/routes/publish-routes.ts +3 -3
- package/src/runtime/routes/rename-conversation-routes.ts +1 -1
- package/src/runtime/routes/retrospective-routes.ts +5 -5
- package/src/runtime/routes/schedule-routes.ts +88 -45
- package/src/runtime/routes/secret-routes.ts +11 -10
- package/src/runtime/routes/sequence-routes.ts +1 -1
- package/src/runtime/routes/settings-routes.ts +26 -12
- package/src/runtime/routes/skills-routes.ts +11 -1
- package/src/runtime/routes/slack-channel-routes.ts +4 -4
- package/src/runtime/routes/subagents-routes.ts +2 -2
- package/src/runtime/routes/surface-conversation-resolver.ts +1 -1
- package/src/runtime/routes/task-routes.ts +18 -29
- package/src/runtime/routes/telemetry-routes.ts +1 -1
- package/src/runtime/routes/trace-event-routes.ts +1 -1
- package/src/runtime/routes/types.ts +7 -0
- package/src/runtime/routes/usage-routes.ts +2 -2
- package/src/runtime/routes/wake-conversation-routes.ts +44 -8
- package/src/runtime/routes/wipe-conversation-routes.ts +3 -3
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +1 -1
- package/src/runtime/runtime-mode.ts +5 -7
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
- package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +362 -0
- package/src/runtime/services/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +178 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -1
- package/src/runtime/services/analyze-conversation.ts +22 -13
- package/src/runtime/services/auto-analysis-enqueue.ts +130 -0
- package/src/runtime/services/auto-analysis-guard.ts +13 -0
- package/src/runtime/services/conversation-analyze-job.ts +53 -0
- package/src/runtime/services/conversation-serializer.ts +10 -53
- package/src/runtime/tool-grant-request-helper.ts +1 -1
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/schedule/__tests__/run-script.test.ts +21 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +16 -4
- package/src/schedule/schedule-recovery.ts +8 -5
- package/src/schedule/schedule-store.ts +270 -162
- package/src/schedule/schedule-usage-store.ts +2 -2
- package/src/schedule/scheduler-types.ts +6 -0
- package/src/schedule/scheduler.ts +218 -121
- package/src/security/auth-fallback-events-store.ts +94 -0
- package/src/security/redaction.ts +18 -14
- package/src/sequence/engine.ts +1 -1
- package/src/sequence/store.ts +3 -3
- package/src/services/published-app-updater.ts +2 -2
- package/src/signals/cancel.ts +1 -1
- package/src/signals/user-message.ts +2 -2
- package/src/skills/__tests__/categories-cache.test.ts +38 -0
- package/src/skills/catalog-install.ts +2 -1
- package/src/skills/categories-cache.ts +40 -5
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/install-meta.ts +44 -0
- package/src/skills/managed-store.ts +120 -3
- package/src/skills/skillssh-registry.ts +1 -0
- package/src/skills/version-hash.ts +20 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/subagent/__tests__/consult-transcript.test.ts +184 -0
- package/src/subagent/consult-prompt.ts +57 -0
- package/src/subagent/consult-transcript.ts +86 -0
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +502 -72
- package/src/subagent/types.ts +14 -3
- package/src/subagent/validate-allowlists.ts +65 -0
- package/src/tasks/task-compiler.ts +2 -2
- package/src/tasks/task-runner.ts +3 -3
- package/src/tasks/task-store.ts +2 -2
- package/src/telemetry/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/telemetry/skill-loaded-events-store.test.ts +152 -0
- package/src/telemetry/skill-loaded-events-store.ts +95 -0
- package/src/telemetry/tool-executed-events-store.test.ts +219 -0
- package/src/telemetry/tool-executed-events-store.ts +102 -0
- package/src/telemetry/tool-usage-store.ts +114 -0
- package/src/telemetry/trace-event-store.ts +133 -0
- package/src/telemetry/turn-events-store.ts +173 -0
- package/src/telemetry/turn-trace-store.test.ts +736 -0
- package/src/telemetry/turn-trace-store.ts +364 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +12 -12
- package/src/telemetry/usage-telemetry-reporter.ts +46 -17
- package/src/telemetry/watchdog-events-store.ts +87 -0
- package/src/tools/__tests__/run-standalone.test.ts +19 -0
- package/src/tools/acp/list-agents.test.ts +0 -6
- package/src/tools/acp/spawn.test.ts +18 -7
- package/src/tools/acp/spawn.ts +1 -0
- package/src/tools/apps/executors.ts +2 -2
- package/src/tools/apps/resolve-app-id.ts +1 -1
- package/src/tools/background-tool-registry.ts +61 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +3 -3
- package/src/tools/document/document-comment-tool.test.ts +2 -2
- package/src/tools/filesystem/write.ts +5 -4
- package/src/tools/host-terminal/host-shell.test.ts +397 -0
- package/src/tools/host-terminal/host-shell.ts +203 -14
- package/src/tools/policy-context.ts +21 -0
- package/src/tools/registry.ts +41 -4
- package/src/tools/run-standalone.ts +101 -0
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schedule/delete.ts +1 -1
- package/src/tools/schedule/update.ts +1 -1
- package/src/tools/skills/find-similar-skills.test.ts +277 -0
- package/src/tools/skills/find-similar-skills.ts +120 -0
- package/src/tools/skills/scaffold-managed.ts +123 -4
- package/src/tools/skills/skill-script-runner.ts +17 -0
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/read.ts +1 -1
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/terminal/shell.test.ts +177 -0
- package/src/tools/terminal/shell.ts +98 -4
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-manifest.ts +1 -1
- package/src/tools/tool-types.ts +445 -0
- package/src/tools/types.ts +28 -17
- package/src/usage/types.ts +7 -0
- package/src/util/__tests__/process-tree.test.ts +123 -0
- package/src/util/device-id.ts +52 -12
- package/src/util/logger.ts +9 -0
- package/src/util/process-tree.ts +203 -0
- package/src/util/sqlite-retry.ts +90 -0
- package/src/watcher/__tests__/telemetry.test.ts +2 -2
- package/src/watcher/telemetry.ts +2 -2
- package/src/watcher/watcher-store.ts +3 -3
- package/src/work-items/work-item-store.ts +2 -2
- package/src/workflows/engine-integration.test.ts +2 -2
- package/src/workflows/engine.test.ts +2 -2
- package/src/workflows/fanout-load.test.ts +2 -2
- package/src/workflows/journal-store.test.ts +3 -3
- package/src/workflows/journal-store.ts +1 -1
- package/src/workflows/leaf-runner.test.ts +9 -6
- package/src/workflows/leaf-runner.ts +1 -1
- package/src/workspace/heartbeat-service.ts +29 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +1 -1
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +5 -2
- package/src/workspace/migrations/031-drop-user-md.ts +258 -148
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +1 -1
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/113-swap-balanced-profile-to-glm-5p2.ts +101 -0
- package/src/workspace/migrations/114-swap-quality-profile-to-opus.ts +83 -0
- package/src/workspace/migrations/115-drop-frontier-profile.ts +129 -0
- package/src/workspace/migrations/116-rename-memory-plugin-disabled-sentinel.ts +53 -0
- package/src/workspace/migrations/117-normalize-stale-lean-memory-v3-defaults.ts +116 -0
- package/src/workspace/migrations/118-seed-now-md.ts +64 -0
- package/src/workspace/migrations/__tests__/backfill-v3-leaves.test.ts +5 -2
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +4 -4
- package/src/workspace/migrations/registry.ts +14 -0
- package/drizzle/0000_dizzy_maggott.sql +0 -301
- package/drizzle/meta/0000_snapshot.json +0 -1933
- package/drizzle/meta/_journal.json +0 -13
- package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
- package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
- package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
- package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
- package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
- package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +0 -446
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
- package/src/__tests__/config-managed-gemini-defaults.test.ts +0 -277
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
- package/src/__tests__/slack-block-formatting.test.ts +0 -210
- package/src/background-wake/runtime-registry.ts +0 -24
- package/src/contacts/contact-events.ts +0 -21
- package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
- package/src/daemon/__tests__/meet-host-supervisor.test.ts +0 -587
- package/src/daemon/__tests__/meet-manifest-loader.test.ts +0 -476
- package/src/daemon/daemon-skill-host.ts +0 -262
- package/src/daemon/meet-host-startup.ts +0 -51
- package/src/daemon/meet-host-supervisor.ts +0 -782
- package/src/daemon/meet-manifest-loader.ts +0 -396
- package/src/daemon/memory-v2-startup.test.ts +0 -203
- package/src/daemon/memory-v2-startup.ts +0 -345
- package/src/daemon/message-types/meet.ts +0 -143
- package/src/daemon/server.ts +0 -423
- package/src/daemon/shutdown-registry.ts +0 -40
- package/src/filing/filing-service.ts +0 -429
- package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
- package/src/ipc/__tests__/skill-server.test.ts +0 -182
- package/src/ipc/skill-ipc-types.ts +0 -54
- package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
- package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
- package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
- package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
- package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
- package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
- package/src/ipc/skill-routes/config.ts +0 -47
- package/src/ipc/skill-routes/events.ts +0 -147
- package/src/ipc/skill-routes/identity.ts +0 -21
- package/src/ipc/skill-routes/index.ts +0 -37
- package/src/ipc/skill-routes/log.ts +0 -40
- package/src/ipc/skill-routes/memory.ts +0 -92
- package/src/ipc/skill-routes/platform.ts +0 -39
- package/src/ipc/skill-routes/providers.ts +0 -162
- package/src/ipc/skill-routes/registries.ts +0 -340
- package/src/ipc/skill-server.ts +0 -795
- package/src/ipc/skill-socket-path.ts +0 -20
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +0 -110
- package/src/memory/__tests__/activation-session-store.test.ts +0 -41
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +0 -359
- package/src/memory/__tests__/auto-analysis-guard.test.ts +0 -57
- package/src/memory/__tests__/bookmark-crud.test.ts +0 -264
- package/src/memory/__tests__/bookmark-schema.test.ts +0 -181
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +0 -432
- package/src/memory/__tests__/conversation-analyze-job.test.ts +0 -178
- package/src/memory/__tests__/conversation-group-migration.test.ts +0 -99
- package/src/memory/__tests__/conversation-queries.test.ts +0 -843
- package/src/memory/__tests__/conversation-types.test.ts +0 -36
- package/src/memory/__tests__/db-async-query.test.ts +0 -165
- package/src/memory/__tests__/db-logs-attach.test.ts +0 -110
- package/src/memory/__tests__/db-maintenance.test.ts +0 -235
- package/src/memory/__tests__/db-memory-attach.test.ts +0 -113
- package/src/memory/__tests__/find-analysis-conversation.test.ts +0 -197
- package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +0 -140
- package/src/memory/__tests__/fork-message-copy.test.ts +0 -232
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +0 -241
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +0 -52
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +0 -113
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +0 -505
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +0 -160
- package/src/memory/__tests__/memory-retrospective-job.test.ts +0 -1443
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +0 -544
- package/src/memory/__tests__/memory-retrospective-state.test.ts +0 -195
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -221
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +0 -275
- package/src/memory/__tests__/memory-v3-selections-migration.test.ts +0 -103
- package/src/memory/__tests__/message-content.test.ts +0 -35
- package/src/memory/__tests__/onboarding-events-store.test.ts +0 -80
- package/src/memory/__tests__/prompt-override.test.ts +0 -192
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +0 -49
- package/src/memory/__tests__/table-relocation.test.ts +0 -129
- package/src/memory/__tests__/watchdog-events-store.test.ts +0 -161
- package/src/memory/activation-session-store.ts +0 -43
- package/src/memory/anisotropy.ts +0 -443
- package/src/memory/app-store.ts +0 -1058
- package/src/memory/attachments-store.ts +0 -1136
- package/src/memory/auth-fallback-events-store.ts +0 -94
- package/src/memory/auto-analysis-enqueue.ts +0 -127
- package/src/memory/auto-analysis-guard.ts +0 -17
- package/src/memory/canonical-guardian-store.ts +0 -891
- package/src/memory/channel-verification-sessions.ts +0 -557
- package/src/memory/checkpoints.ts +0 -65
- package/src/memory/context-search/agent-protocol.ts +0 -428
- package/src/memory/context-search/agent-runner.ts +0 -1310
- package/src/memory/context-search/search.ts +0 -297
- package/src/memory/context-search/sources/conversations.ts +0 -341
- package/src/memory/context-search/sources/memory-v2.ts +0 -600
- package/src/memory/context-search/sources/memory.ts +0 -101
- package/src/memory/context-search/types.ts +0 -48
- package/src/memory/conversation-analyze-job.ts +0 -53
- package/src/memory/conversation-attention-store.ts +0 -689
- package/src/memory/conversation-bootstrap.ts +0 -50
- package/src/memory/conversation-crud.ts +0 -3361
- package/src/memory/conversation-key-store.ts +0 -274
- package/src/memory/conversation-queries.ts +0 -768
- package/src/memory/conversation-starter-checkpoints.ts +0 -64
- package/src/memory/conversation-title-service.ts +0 -606
- package/src/memory/db-async-query.ts +0 -298
- package/src/memory/db-connection.ts +0 -228
- package/src/memory/db-init.ts +0 -265
- package/src/memory/db-maintenance.ts +0 -154
- package/src/memory/embed.ts +0 -73
- package/src/memory/embedding-backend.test.ts +0 -204
- package/src/memory/embedding-backend.ts +0 -855
- package/src/memory/embedding-billing-breaker.ts +0 -96
- package/src/memory/embedding-local.ts +0 -587
- package/src/memory/embedding-ollama.ts +0 -79
- package/src/memory/embedding-runtime-manager.ts +0 -573
- package/src/memory/embedding-types.ts +0 -90
- package/src/memory/external-conversation-store.ts +0 -315
- package/src/memory/fork-message-copy.ts +0 -170
- package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +0 -119
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +0 -655
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +0 -321
- package/src/memory/graph/bootstrap.test.ts +0 -280
- package/src/memory/graph/bootstrap.ts +0 -532
- package/src/memory/graph/capability-seed.ts +0 -451
- package/src/memory/graph/consolidation.ts +0 -712
- package/src/memory/graph/conversation-graph-memory.ts +0 -1185
- package/src/memory/graph/decay.ts +0 -195
- package/src/memory/graph/extraction-job.ts +0 -74
- package/src/memory/graph/extraction.ts +0 -1462
- package/src/memory/graph/graph-memory-state-store.ts +0 -50
- package/src/memory/graph/graph-search.test.ts +0 -129
- package/src/memory/graph/graph-search.ts +0 -292
- package/src/memory/graph/image-ref-utils.ts +0 -29
- package/src/memory/graph/injection.ts +0 -469
- package/src/memory/graph/narrative.ts +0 -267
- package/src/memory/graph/pattern-scan.ts +0 -269
- package/src/memory/graph/retriever.test.ts +0 -624
- package/src/memory/graph/retriever.ts +0 -1356
- package/src/memory/graph/store.test.ts +0 -1093
- package/src/memory/graph/store.ts +0 -923
- package/src/memory/graph/tool-handlers.ts +0 -195
- package/src/memory/graph/tools.ts +0 -95
- package/src/memory/guardian-rate-limits.ts +0 -191
- package/src/memory/indexer.ts +0 -406
- package/src/memory/job-handlers/backfill.ts +0 -104
- package/src/memory/job-handlers/cleanup.ts +0 -237
- package/src/memory/job-handlers/conversation-starters.ts +0 -481
- package/src/memory/job-handlers/embedding.test.ts +0 -234
- package/src/memory/job-handlers/embedding.ts +0 -133
- package/src/memory/job-handlers/index-maintenance.ts +0 -111
- package/src/memory/job-handlers/media-processing.ts +0 -120
- package/src/memory/job-handlers/summarization.ts +0 -240
- package/src/memory/job-utils.ts +0 -286
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +0 -559
- package/src/memory/jobs/embed-concept-page.ts +0 -417
- package/src/memory/jobs/embed-pkb-file.test.ts +0 -169
- package/src/memory/jobs/embed-pkb-file.ts +0 -59
- package/src/memory/jobs-store.ts +0 -813
- package/src/memory/jobs-worker.ts +0 -975
- package/src/memory/llm-request-log-source-clickhouse.ts +0 -521
- package/src/memory/llm-request-log-store.ts +0 -717
- package/src/memory/llm-usage-store.ts +0 -900
- package/src/memory/memory-recall-log-store.ts +0 -172
- package/src/memory/memory-retrospective-constants.ts +0 -50
- package/src/memory/memory-retrospective-enqueue.ts +0 -125
- package/src/memory/memory-retrospective-fork-boundary.ts +0 -94
- package/src/memory/memory-retrospective-job.ts +0 -879
- package/src/memory/memory-retrospective-startup-cleanup.ts +0 -261
- package/src/memory/memory-retrospective-state.ts +0 -245
- package/src/memory/memory-retrospective-trigger-check.ts +0 -91
- package/src/memory/memory-v2-activation-log-store.ts +0 -216
- package/src/memory/memory-v2-concept-frequency.ts +0 -173
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +0 -303
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +0 -365
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +0 -55
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +0 -20
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +0 -179
- package/src/memory/migrations/199-guardian-request-enrichment-columns.ts +0 -64
- package/src/memory/migrations/202-memory-graph-tables.ts +0 -130
- package/src/memory/migrations/203-drop-memory-items-tables.ts +0 -55
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +0 -124
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +0 -152
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +0 -16
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +0 -183
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +0 -76
- package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +0 -21
- package/src/memory/migrations/243-provider-connections.ts +0 -68
- package/src/memory/migrations/244-provider-connection-status-label.ts +0 -23
- package/src/memory/migrations/246-backfill-provider-connection-label.ts +0 -81
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +0 -19
- package/src/memory/migrations/301-create-watchdog-events.ts +0 -45
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +0 -224
- package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +0 -84
- package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +0 -192
- package/src/memory/migrations/__tests__/run-migrations.test.ts +0 -378
- package/src/memory/migrations/helpers/relocation.ts +0 -227
- package/src/memory/migrations/validate-migration-state.ts +0 -277
- package/src/memory/onboarding-events-store.ts +0 -161
- package/src/memory/pkb/autoinject.ts +0 -61
- package/src/memory/pkb/context.ts +0 -50
- package/src/memory/pkb/pkb-index.test.ts +0 -368
- package/src/memory/pkb/pkb-index.ts +0 -255
- package/src/memory/pkb/pkb-reconcile.test.ts +0 -251
- package/src/memory/pkb/pkb-reconcile.ts +0 -148
- package/src/memory/pkb/pkb-search.test.ts +0 -586
- package/src/memory/pkb/pkb-search.ts +0 -178
- package/src/memory/pkb/types.ts +0 -67
- package/src/memory/published-pages-store.ts +0 -67
- package/src/memory/qdrant-circuit-breaker.ts +0 -119
- package/src/memory/qdrant-client.test.ts +0 -60
- package/src/memory/qdrant-client.ts +0 -834
- package/src/memory/qdrant-manager.ts +0 -422
- package/src/memory/rerank-local.ts +0 -374
- package/src/memory/schedule-attribution-sql.ts +0 -104
- package/src/memory/schema/acp.ts +0 -34
- package/src/memory/schema/contacts.ts +0 -116
- package/src/memory/schema/conversations.ts +0 -197
- package/src/memory/schema/infrastructure.ts +0 -381
- package/src/memory/scoped-approval-grants.ts +0 -551
- package/src/memory/search/semantic.ts +0 -238
- package/src/memory/segmenter.ts +0 -76
- package/src/memory/shared-app-links-store.ts +0 -130
- package/src/memory/skill-loaded-events-store.test.ts +0 -152
- package/src/memory/skill-loaded-events-store.ts +0 -95
- package/src/memory/sparse-tokenize.ts +0 -49
- package/src/memory/steps.ts +0 -573
- package/src/memory/task-memory-cleanup.ts +0 -236
- package/src/memory/tool-executed-events-store.test.ts +0 -219
- package/src/memory/tool-executed-events-store.ts +0 -102
- package/src/memory/tool-usage-store.ts +0 -113
- package/src/memory/trace-event-store.ts +0 -133
- package/src/memory/turn-events-store.ts +0 -173
- package/src/memory/turn-trace-store.test.ts +0 -736
- package/src/memory/turn-trace-store.ts +0 -364
- package/src/memory/v2/__tests__/activation-store.test.ts +0 -204
- package/src/memory/v2/__tests__/activation.test.ts +0 -1227
- package/src/memory/v2/__tests__/backfill-jobs.test.ts +0 -504
- package/src/memory/v2/__tests__/cli-command-store.test.ts +0 -429
- package/src/memory/v2/__tests__/consolidation-job.test.ts +0 -747
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -189
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +0 -136
- package/src/memory/v2/__tests__/harness-compare.test.ts +0 -186
- package/src/memory/v2/__tests__/harness-oracle.test.ts +0 -257
- package/src/memory/v2/__tests__/harness-replay-input.test.ts +0 -230
- package/src/memory/v2/__tests__/harness-runner.test.ts +0 -135
- package/src/memory/v2/__tests__/injection-events.test.ts +0 -318
- package/src/memory/v2/__tests__/injection.test.ts +0 -2301
- package/src/memory/v2/__tests__/migration.test.ts +0 -939
- package/src/memory/v2/__tests__/page-index.test.ts +0 -760
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +0 -313
- package/src/memory/v2/__tests__/qdrant.test.ts +0 -950
- package/src/memory/v2/__tests__/reranker.test.ts +0 -338
- package/src/memory/v2/__tests__/router.test.ts +0 -1197
- package/src/memory/v2/__tests__/sim.test.ts +0 -670
- package/src/memory/v2/__tests__/skill-content.test.ts +0 -115
- package/src/memory/v2/__tests__/skill-store.test.ts +0 -983
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +0 -292
- package/src/memory/v2/__tests__/static-context.test.ts +0 -223
- package/src/memory/v2/__tests__/sweep-job.test.ts +0 -526
- package/src/memory/v2/activation-store.ts +0 -185
- package/src/memory/v2/activation.ts +0 -548
- package/src/memory/v2/backfill-jobs.ts +0 -296
- package/src/memory/v2/cli-command-store.ts +0 -341
- package/src/memory/v2/consolidation-job.ts +0 -561
- package/src/memory/v2/frontmatter-sweep.ts +0 -98
- package/src/memory/v2/harness/compare.ts +0 -57
- package/src/memory/v2/harness/oracle.ts +0 -145
- package/src/memory/v2/harness/replay-input.ts +0 -240
- package/src/memory/v2/harness/retriever.ts +0 -74
- package/src/memory/v2/harness/router-retriever.ts +0 -43
- package/src/memory/v2/injected-block-slugs.ts +0 -79
- package/src/memory/v2/injection-events.ts +0 -101
- package/src/memory/v2/injection.ts +0 -932
- package/src/memory/v2/migration.ts +0 -776
- package/src/memory/v2/page-index.ts +0 -459
- package/src/memory/v2/page-store.ts +0 -443
- package/src/memory/v2/prompts/consolidation.ts +0 -857
- package/src/memory/v2/prompts/router.ts +0 -148
- package/src/memory/v2/qdrant.ts +0 -892
- package/src/memory/v2/reranker.ts +0 -184
- package/src/memory/v2/router.ts +0 -601
- package/src/memory/v2/sim.ts +0 -296
- package/src/memory/v2/skill-content.ts +0 -61
- package/src/memory/v2/skill-store.ts +0 -431
- package/src/memory/v2/sparse-bm25.ts +0 -245
- package/src/memory/v2/static-context.ts +0 -100
- package/src/memory/v2/sweep-job.ts +0 -320
- package/src/memory/v3-eval/eval-packets.ts +0 -731
- package/src/memory/watchdog-events-store.ts +0 -87
- package/src/memory/worker-control.ts +0 -118
- package/src/memory/worker-process.ts +0 -72
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +0 -147
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/steering.ts +0 -79
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
- package/src/plugins/defaults/advisor/transcript.ts +0 -76
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +0 -116
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +0 -370
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +0 -41
- package/src/plugins/defaults/memory-retrieval/injectors.ts +0 -1061
- package/src/plugins/defaults/memory-retrieval/package.json +0 -14
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +0 -64
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +0 -231
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +0 -1164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +0 -181
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +0 -247
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +0 -717
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +0 -365
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +0 -736
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +0 -839
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +0 -598
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +0 -128
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +0 -469
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +0 -425
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +0 -493
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +0 -856
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +0 -91
- package/src/plugins/defaults/memory-v3-shadow/card.ts +0 -160
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +0 -97
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +0 -252
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +0 -305
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +0 -278
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +0 -138
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +0 -85
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -453
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +0 -169
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +0 -178
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +0 -794
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +0 -431
- package/src/plugins/defaults/memory-v3-shadow/package.json +0 -14
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +0 -102
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +0 -146
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +0 -578
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +0 -758
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +0 -471
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +0 -92
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +0 -269
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +0 -200
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +0 -281
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +0 -642
- package/src/plugins/defaults/memory-v3-shadow/types.ts +0 -127
- package/src/plugins/feature-gate.ts +0 -22
- package/src/runtime/__tests__/slack-block-formatting.test.ts +0 -510
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +0 -165
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +0 -347
- package/src/runtime/routes/memory-eval-routes.ts +0 -179
- package/src/runtime/routes/memory-item-routes.test.ts +0 -912
- package/src/runtime/routes/memory-item-routes.ts +0 -815
- package/src/runtime/routes/memory-v2-routes.ts +0 -850
- package/src/runtime/routes/memory-v3-routes.ts +0 -122
- package/src/runtime/routes/secrets-deps.ts +0 -24
- package/src/runtime/slack-block-formatting.ts +0 -601
- package/src/tools/memory/register.test.ts +0 -382
- package/src/tools/memory/register.ts +0 -81
- /package/src/{memory → apps}/app-git-service.ts +0 -0
- /package/src/{memory → channels}/slack-thread-store.ts +0 -0
- /package/src/{memory → home}/conversation-starter-validation.ts +0 -0
- /package/src/{memory → persistence}/auto-analysis-constants.ts +0 -0
- /package/src/{memory → persistence}/bookmark-crud.ts +0 -0
- /package/src/{memory → persistence}/cleanup-schedule-state.ts +0 -0
- /package/src/{memory → persistence}/compaction-log-store-clickhouse.ts +0 -0
- /package/src/{memory → persistence}/conversation-directories.ts +0 -0
- /package/src/{memory → persistence}/conversation-disk-view.ts +0 -0
- /package/src/{memory → persistence}/conversation-display-order-migration.ts +0 -0
- /package/src/{memory → persistence}/conversation-group-migration.ts +0 -0
- /package/src/{memory → persistence}/conversation-types.ts +0 -0
- /package/src/{memory → persistence}/db-singleton.ts +0 -0
- /package/src/{memory → persistence}/delivery-channels.ts +0 -0
- /package/src/{memory → persistence}/delivery-crud.ts +0 -0
- /package/src/{memory → persistence}/delivery-status.ts +0 -0
- /package/src/{memory → persistence/embeddings}/__tests__/sparse-tokenize.test.ts +0 -0
- /package/src/{memory → persistence/embeddings}/embedding-gemini.test.ts +0 -0
- /package/src/{memory → persistence/embeddings}/embedding-gemini.ts +0 -0
- /package/src/{memory → persistence/embeddings}/embedding-openai.ts +0 -0
- /package/src/{memory → persistence/embeddings}/embedding-types.test.ts +0 -0
- /package/src/{memory → persistence}/fingerprint.ts +0 -0
- /package/src/{memory → persistence}/group-crud.ts +0 -0
- /package/src/{memory → persistence}/invite-store.ts +0 -0
- /package/src/{memory → persistence}/lifecycle-events-store.ts +0 -0
- /package/src/{memory → persistence}/llm-request-log-source-local.ts +0 -0
- /package/src/{memory → persistence}/llm-request-log-source.ts +0 -0
- /package/src/{memory → persistence}/media-store.ts +0 -0
- /package/src/{memory → persistence}/message-content.ts +0 -0
- /package/src/{memory → persistence}/migrations/000-core-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/001-job-deferrals.ts +0 -0
- /package/src/{memory → persistence}/migrations/002-tool-invocations-fk.ts +0 -0
- /package/src/{memory → persistence}/migrations/003-memory-fts-backfill.ts +0 -0
- /package/src/{memory → persistence}/migrations/004-entity-relation-dedup.ts +0 -0
- /package/src/{memory → persistence}/migrations/005-fingerprint-scope-unique.ts +0 -0
- /package/src/{memory → persistence}/migrations/006-scope-salted-fingerprints.ts +0 -0
- /package/src/{memory → persistence}/migrations/007-assistant-id-to-self.ts +0 -0
- /package/src/{memory → persistence}/migrations/008-remove-assistant-id-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/009-llm-usage-events-drop-assistant-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/011-call-sessions-provider-sid-dedup.ts +0 -0
- /package/src/{memory → persistence}/migrations/012-call-sessions-add-initiated-from.ts +0 -0
- /package/src/{memory → persistence}/migrations/013-guardian-action-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/014-backfill-inbox-thread-state.ts +0 -0
- /package/src/{memory → persistence}/migrations/015-drop-active-search-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/016-memory-segments-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/017-memory-items-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/018-remaining-table-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/019-notification-tables-schema-migration.ts +0 -0
- /package/src/{memory → persistence}/migrations/020-rename-macos-ios-channel-to-vellum.ts +0 -0
- /package/src/{memory → persistence}/migrations/021-conversation-status-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/022-add-origin-interface.ts +0 -0
- /package/src/{memory → persistence}/migrations/023-memory-item-sources-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/024-embedding-vector-blob.ts +0 -0
- /package/src/{memory → persistence}/migrations/025-messages-fts-backfill.ts +0 -0
- /package/src/{memory → persistence}/migrations/026-guardian-verification-sessions.ts +0 -0
- /package/src/{memory → persistence}/migrations/026a-embeddings-nullable-vector-json.ts +0 -0
- /package/src/{memory → persistence}/migrations/027-notification-delivery-pairing-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/027a-guardian-bootstrap-token.ts +0 -0
- /package/src/{memory → persistence}/migrations/028-call-session-mode.ts +0 -0
- /package/src/{memory → persistence}/migrations/028-notification-delivery-client-ack.ts +0 -0
- /package/src/{memory → persistence}/migrations/029-channel-inbound-delivered-segments.ts +0 -0
- /package/src/{memory → persistence}/migrations/030-guardian-action-followup.ts +0 -0
- /package/src/{memory → persistence}/migrations/030-guardian-verification-purpose.ts +0 -0
- /package/src/{memory → persistence}/migrations/031-conversations-thread-type-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/032-guardian-delivery-conversation-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/032-notification-delivery-thread-decision.ts +0 -0
- /package/src/{memory → persistence}/migrations/033-scoped-approval-grants.ts +0 -0
- /package/src/{memory → persistence}/migrations/034-guardian-action-tool-metadata.ts +0 -0
- /package/src/{memory → persistence}/migrations/035-guardian-action-supersession.ts +0 -0
- /package/src/{memory → persistence}/migrations/036-normalize-phone-identities.ts +0 -0
- /package/src/{memory → persistence}/migrations/037-voice-invite-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/040-invite-code-hash-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/041-approval-prompt-ts-tracker.ts +0 -0
- /package/src/{memory → persistence}/migrations/101-watchers-and-logs.ts +0 -0
- /package/src/{memory → persistence}/migrations/102-alter-table-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/104-core-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/105-contacts-and-triage.ts +0 -0
- /package/src/{memory → persistence}/migrations/106-call-sessions.ts +0 -0
- /package/src/{memory → persistence}/migrations/107-followups.ts +0 -0
- /package/src/{memory → persistence}/migrations/108-tasks-and-work-items.ts +0 -0
- /package/src/{memory → persistence}/migrations/109-external-conversation-bindings.ts +0 -0
- /package/src/{memory → persistence}/migrations/110-channel-guardian.ts +0 -0
- /package/src/{memory → persistence}/migrations/111-media-assets.ts +0 -0
- /package/src/{memory → persistence}/migrations/112-assistant-inbox.ts +0 -0
- /package/src/{memory → persistence}/migrations/114-notifications.ts +0 -0
- /package/src/{memory → persistence}/migrations/115-sequences.ts +0 -0
- /package/src/{memory → persistence}/migrations/116-messages-fts.ts +0 -0
- /package/src/{memory → persistence}/migrations/117-conversation-attention.ts +0 -0
- /package/src/{memory → persistence}/migrations/118-reminder-routing-intent.ts +0 -0
- /package/src/{memory → persistence}/migrations/119-schema-indexes-and-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/120-fk-cascade-rebuilds.ts +0 -0
- /package/src/{memory → persistence}/migrations/121-canonical-guardian-requests.ts +0 -0
- /package/src/{memory → persistence}/migrations/122-canonical-guardian-requester-chat-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/123-canonical-guardian-deliveries-destination-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/124-voice-invite-display-metadata.ts +0 -0
- /package/src/{memory → persistence}/migrations/125-guardian-principal-id-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/127-guardian-principal-id-not-null.ts +0 -0
- /package/src/{memory → persistence}/migrations/128-contacts-role-principal.ts +0 -0
- /package/src/{memory → persistence}/migrations/129-contact-channels-access-fields.ts +0 -0
- /package/src/{memory → persistence}/migrations/130-contact-channels-type-ext-chat-id-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/131-drop-legacy-member-guardian-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/132-contacts-assistant-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/133-assistant-contact-metadata.ts +0 -0
- /package/src/{memory → persistence}/migrations/134-contacts-notes-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/135-backfill-contact-interaction-stats.ts +0 -0
- /package/src/{memory → persistence}/migrations/136-drop-assistant-id-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/137-usage-dashboard-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/139-drop-usage-composite-indexes.ts +0 -0
- /package/src/{memory → persistence}/migrations/141-rename-verification-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/143-rename-guardian-verification-values.ts +0 -0
- /package/src/{memory → persistence}/migrations/144-rename-voice-to-phone.ts +0 -0
- /package/src/{memory → persistence}/migrations/145-drop-accounts-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/146-schedule-oneshot-routing.ts +0 -0
- /package/src/{memory → persistence}/migrations/147-migrate-reminders-to-schedules.ts +0 -0
- /package/src/{memory → persistence}/migrations/148-drop-reminders-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/149-oauth-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/150-oauth-apps-client-secret-path.ts +0 -0
- /package/src/{memory → persistence}/migrations/151-oauth-providers-ping-url.ts +0 -0
- /package/src/{memory → persistence}/migrations/152-memory-item-supersession.ts +0 -0
- /package/src/{memory → persistence}/migrations/153-drop-entity-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/154-drop-fts.ts +0 -0
- /package/src/{memory → persistence}/migrations/155-drop-conflicts.ts +0 -0
- /package/src/{memory → persistence}/migrations/156-call-session-invite-metadata.ts +0 -0
- /package/src/{memory → persistence}/migrations/157-invite-contact-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/158-channel-interaction-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/159-drop-contact-interaction-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/160-drop-loopback-port-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/161-drop-orphaned-media-tables.ts +0 -0
- /package/src/{memory → persistence}/migrations/162-guardian-timestamps-epoch-ms.ts +0 -0
- /package/src/{memory → persistence}/migrations/163-rename-notification-thread-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/164-rename-conversation-type-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/165-rename-inbox-thread-state-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/166-rename-followups-thread-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/167-rename-sequence-enrollments-thread-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/168-rename-sequence-steps-reply-key.ts +0 -0
- /package/src/{memory → persistence}/migrations/169-rename-gmail-provider-key-to-google.ts +0 -0
- /package/src/{memory → persistence}/migrations/170-thread-starters-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/171-capability-card-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/172-rename-created-by-session-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/173-rename-source-session-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/174-rename-thread-starters-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/175-create-lifecycle-events.ts +0 -0
- /package/src/{memory → persistence}/migrations/176-drop-capability-card-state.ts +0 -0
- /package/src/{memory → persistence}/migrations/177-create-trace-events-table.ts +0 -0
- /package/src/{memory → persistence}/migrations/178-oauth-providers-managed-service-config-key.ts +0 -0
- /package/src/{memory → persistence}/migrations/179-llm-request-log-message-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/180-backfill-inline-attachments-to-disk.ts +0 -0
- /package/src/{memory → persistence}/migrations/181-rename-thread-starters-checkpoints.ts +0 -0
- /package/src/{memory → persistence}/migrations/183-add-conversation-fork-lineage.ts +0 -0
- /package/src/{memory → persistence}/migrations/184-llm-request-log-provider.ts +0 -0
- /package/src/{memory → persistence}/migrations/188-schedule-quiet-flag.ts +0 -0
- /package/src/{memory → persistence}/migrations/189-drop-simplified-memory.ts +0 -0
- /package/src/{memory → persistence}/migrations/190-call-session-skip-disclosure.ts +0 -0
- /package/src/{memory → persistence}/migrations/191-backfill-audio-attachment-mime-types.ts +0 -0
- /package/src/{memory → persistence}/migrations/192-contacts-user-file-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/193-add-source-type-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/194-memory-recall-logs.ts +0 -0
- /package/src/{memory → persistence}/migrations/195-oauth-providers-ping-config.ts +0 -0
- /package/src/{memory → persistence}/migrations/196-messages-conversation-created-at-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/197-oauth-providers-behavior-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/198-drop-setup-skill-id-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/200-usage-llm-call-count.ts +0 -0
- /package/src/{memory → persistence}/migrations/201-oauth-providers-feature-flag.ts +0 -0
- /package/src/{memory → persistence}/migrations/202-drop-callback-transport-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/204-rename-memory-graph-type-values.ts +0 -0
- /package/src/{memory → persistence}/migrations/205-memory-graph-image-refs.ts +0 -0
- /package/src/{memory → persistence}/migrations/206-memory-graph-node-edits.ts +0 -0
- /package/src/{memory → persistence}/migrations/207-conversation-graph-memory-state.ts +0 -0
- /package/src/{memory → persistence}/migrations/208-conversations-last-message-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/210-schedule-reuse-conversation.ts +0 -0
- /package/src/{memory → persistence}/migrations/212-llm-request-logs-created-at-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/213-oauth-providers-scope-separator.ts +0 -0
- /package/src/{memory → persistence}/migrations/214-oauth-providers-refresh-url.ts +0 -0
- /package/src/{memory → persistence}/migrations/215-oauth-providers-revoke.ts +0 -0
- /package/src/{memory → persistence}/migrations/216-oauth-providers-token-auth-method.ts +0 -0
- /package/src/{memory → persistence}/migrations/217-conversation-host-access.ts +0 -0
- /package/src/{memory → persistence}/migrations/218-oauth-providers-logo-url.ts +0 -0
- /package/src/{memory → persistence}/migrations/219-oauth-providers-token-exchange-body-format.ts +0 -0
- /package/src/{memory → persistence}/migrations/221-conversations-archived-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/223-schedule-script-column.ts +0 -0
- /package/src/{memory → persistence}/migrations/224-oauth-providers-managed-service-is-paid.ts +0 -0
- /package/src/{memory → persistence}/migrations/225-oauth-providers-available-scopes.ts +0 -0
- /package/src/{memory → persistence}/migrations/226-schedule-wake-conversation-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/227-add-conversation-inference-profile.ts +0 -0
- /package/src/{memory → persistence}/migrations/228-rename-inference-profile-snake-case.ts +0 -0
- /package/src/{memory → persistence}/migrations/229-delete-private-conversations.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/229-delete-private-conversations.ts +0 -0
- /package/src/{memory → persistence}/migrations/230-acp-session-history.ts +0 -0
- /package/src/{memory → persistence}/migrations/231-repair-memory-graph-event-dates.ts +0 -0
- /package/src/{memory → persistence}/migrations/232-activation-state.ts +0 -0
- /package/src/{memory → persistence}/migrations/233-document-conversations.ts +0 -0
- /package/src/{memory → persistence}/migrations/234-memory-v2-activation-logs.ts +0 -0
- /package/src/{memory → persistence}/migrations/235-llm-usage-attribution.ts +0 -0
- /package/src/{memory → persistence}/migrations/235-slack-compaction-watermark.ts +0 -0
- /package/src/{memory → persistence}/migrations/237-heartbeat-runs.ts +0 -0
- /package/src/{memory → persistence}/migrations/238-schedule-retry-policy.ts +0 -0
- /package/src/{memory → persistence}/migrations/239-trace-events-created-at-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/240-conversation-inference-profile-session.ts +0 -0
- /package/src/{memory → persistence}/migrations/241-activation-state-fk-cascade.ts +0 -0
- /package/src/{memory → persistence}/migrations/242-message-bookmarks.ts +0 -0
- /package/src/{memory → persistence}/migrations/245-memory-retrospective-state.ts +0 -0
- /package/src/{memory → persistence}/migrations/247-external-conversation-binding-thread-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/248-create-onboarding-events.ts +0 -0
- /package/src/{memory → persistence}/migrations/249-normalize-slack-external-content.ts +0 -0
- /package/src/{memory → persistence}/migrations/250-provider-connection-base-url-and-models.ts +0 -0
- /package/src/{memory → persistence}/migrations/251-a2a-tasks.ts +0 -0
- /package/src/{memory → persistence}/migrations/252-llm-request-log-agent-loop-exit-reason.ts +0 -0
- /package/src/{memory → persistence}/migrations/253-conversation-last-notified-profile.ts +0 -0
- /package/src/{memory → persistence}/migrations/253-document-comments.ts +0 -0
- /package/src/{memory → persistence}/migrations/254-external-conversation-binding-chat-name.ts +0 -0
- /package/src/{memory → persistence}/migrations/256-memory-v2-injection-events.ts +0 -0
- /package/src/{memory → persistence}/migrations/257-strip-base-url-non-openai-compatible.ts +0 -0
- /package/src/{memory → persistence}/migrations/258-onboarding-events-prior-assistants.ts +0 -0
- /package/src/{memory → persistence}/migrations/259-conversation-cleaned-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/260-rename-cleaned-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/261-llm-usage-add-raw-usage.ts +0 -0
- /package/src/{memory → persistence}/migrations/262-memory-v3-coactivation.ts +0 -0
- /package/src/{memory → persistence}/migrations/263-memory-v3-auto-edges.ts +0 -0
- /package/src/{memory → persistence}/migrations/264-llm-request-log-call-site.ts +0 -0
- /package/src/{memory → persistence}/migrations/265-drop-provider-connection-status.ts +0 -0
- /package/src/{memory → persistence}/migrations/266-messages-client-message-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/267-llm-usage-events-add-assistant-version.ts +0 -0
- /package/src/{memory → persistence}/migrations/268-add-memory-v3-selections.ts +0 -0
- /package/src/{memory → persistence}/migrations/269-schedule-script-timeout.ts +0 -0
- /package/src/{memory → persistence}/migrations/270-messages-role-created-at-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/270-schedule-description.ts +0 -0
- /package/src/{memory → persistence}/migrations/270-schedule-source-conversation.ts +0 -0
- /package/src/{memory → persistence}/migrations/271-create-auth-fallback-events.ts +0 -0
- /package/src/{memory → persistence}/migrations/272-acp-session-history-cwd.ts +0 -0
- /package/src/{memory → persistence}/migrations/273-onboarding-events-funnel-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/274-create-activation-sessions.ts +0 -0
- /package/src/{memory → persistence}/migrations/275-tool-invocations-add-skill-id.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/275-tool-invocations-add-skill-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/276-tool-invocations-created-at-id-index.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/276-tool-invocations-created-at-id-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/277-add-memory-v3-ever-injected.ts +0 -0
- /package/src/{memory → persistence}/migrations/278-tool-invocations-telemetry-columns.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/278-tool-invocations-telemetry-columns.ts +0 -0
- /package/src/{memory → persistence}/migrations/279-create-skill-loaded-events.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/279-create-skill-loaded-events.ts +0 -0
- /package/src/{memory → persistence}/migrations/280-conversations-surfaced-at.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/280-conversations-surfaced-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/281-memory-retrospective-remembered-log.ts +0 -0
- /package/src/{memory → persistence}/migrations/282-schedule-inference-profile.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/282-schedule-inference-profile.ts +0 -0
- /package/src/{memory → persistence}/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/283-memory-v3-selections-message-id-and-sections.ts +0 -0
- /package/src/{memory → persistence}/migrations/284-workflow-runs.ts +0 -0
- /package/src/{memory → persistence}/migrations/285-schedule-workflow-mode.ts +0 -0
- /package/src/{memory → persistence}/migrations/286-workflow-run-trust.ts +0 -0
- /package/src/{memory → persistence}/migrations/287-conversation-origin-channel-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/288-backfill-origin-channel-from-bindings.ts +0 -0
- /package/src/{memory → persistence}/migrations/289-contact-channels-unique-ext-user.ts +0 -0
- /package/src/{memory → persistence}/migrations/290-schedule-capabilities.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/290-schedule-capabilities.ts +0 -0
- /package/src/{memory → persistence}/migrations/291-contact-channels-renormalize-addresses.ts +0 -0
- /package/src/{memory → persistence}/migrations/292-schedule-default-no-reuse-conversation.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/292-schedule-default-no-reuse-conversation.ts +0 -0
- /package/src/{memory → persistence}/migrations/293-workflow-journal-leaf-tokens.ts +0 -0
- /package/src/{memory → persistence}/migrations/294-drop-external-user-id.ts +0 -0
- /package/src/{memory → persistence}/migrations/295-drop-approval-prompt-ts-tracker.ts +0 -0
- /package/src/{memory → persistence}/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/296-rewrite-balanced-economy-profile-pins.ts +0 -0
- /package/src/{memory → persistence}/migrations/297-move-llm-request-logs-to-logs-db.ts +0 -0
- /package/src/{memory → persistence}/migrations/298-move-memory-jobs-to-memory-db.ts +0 -0
- /package/src/{memory → persistence}/migrations/299-canonical-guardian-deliveries-conversation-index.ts +0 -0
- /package/src/{memory → persistence}/migrations/300-add-processing-started-at.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/136-drop-assistant-id-columns.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/234-memory-v2-activation-logs.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/245-memory-retrospective-state.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/267-llm-usage-events-add-assistant-version.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/__tests__/297-move-llm-request-logs.test.ts +0 -0
- /package/src/{memory → persistence}/migrations/run-migrations.ts +0 -0
- /package/src/{memory → persistence}/migrations/schema-introspection.ts +0 -0
- /package/src/{memory → persistence}/raw-query.ts +0 -0
- /package/src/{memory → persistence}/schema/a2a.ts +0 -0
- /package/src/{memory → persistence}/schema/bookmarks.ts +0 -0
- /package/src/{memory → persistence}/schema/calls.ts +0 -0
- /package/src/{memory → persistence}/schema/guardian.ts +0 -0
- /package/src/{memory → persistence}/schema/index.ts +0 -0
- /package/src/{memory → persistence}/schema/inference.ts +0 -0
- /package/src/{memory → persistence}/schema/memory-core.ts +0 -0
- /package/src/{memory → persistence}/schema/memory-graph.ts +0 -0
- /package/src/{memory → persistence}/schema/notifications.ts +0 -0
- /package/src/{memory → persistence}/schema/oauth.ts +0 -0
- /package/src/{memory → persistence}/schema/tasks.ts +0 -0
- /package/src/{memory → persistence}/schema.ts +0 -0
- /package/src/{memory → persistence}/usage-buckets.ts +0 -0
- /package/src/{memory → persistence}/usage-grouped-buckets.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/__tests__/fixtures/memory-v2-activation-fixtures.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/__tests__/memory-retrospective-trigger-check.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/anisotropy.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/context-search/__tests__/agent-runner-redaction.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/context-search/format.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/context-search/limits.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/context-search/sources/workspace.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/decay.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/extraction.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/injection.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/scoring.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/scoring.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/serendipity.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/triggers.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/triggers.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/graph/types.ts +0 -0
- /package/src/plugins/defaults/{memory-retrieval → memory}/hooks/__tests__/user-prompt-submit.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/memory-marker.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/prompt-override.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/retrieval-budget.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/concept-page-frontmatter-schema.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/edge-index.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/harness-metrics.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/injected-block-slugs.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/page-store.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/__tests__/prompts-router.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/cli-command-content.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/constants.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/edge-index.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/harness/metrics.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/harness/runner.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/harness/trace.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/now-text.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/prompts/sweep.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v2/types.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/card.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/fresh-set.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/section-needle.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/sections.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/card.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/core-set.test.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/core-set.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/README.md +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/core.json +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/fresh-set.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
- /package/src/plugins/defaults/{memory-v3-shadow → memory/v3}/sections.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v3-eval/__tests__/eval-packets.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v3-eval/__tests__/eval-tally.test.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/v3-eval/eval-tally.ts +0 -0
- /package/src/{memory → plugins/defaults/memory}/validation.ts +0 -0
|
@@ -0,0 +1,3404 @@
|
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
asc,
|
|
6
|
+
count,
|
|
7
|
+
desc,
|
|
8
|
+
eq,
|
|
9
|
+
gt,
|
|
10
|
+
gte,
|
|
11
|
+
inArray,
|
|
12
|
+
isNotNull,
|
|
13
|
+
isNull,
|
|
14
|
+
like,
|
|
15
|
+
lt,
|
|
16
|
+
lte,
|
|
17
|
+
or,
|
|
18
|
+
sql,
|
|
19
|
+
} from "drizzle-orm";
|
|
20
|
+
import { v4 as uuid } from "uuid";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
|
|
23
|
+
import type { ChannelId, InterfaceId } from "../channels/types.js";
|
|
24
|
+
import { parseChannelId, parseInterfaceId } from "../channels/types.js";
|
|
25
|
+
import { CHANNEL_IDS, isChannelId } from "../channels/types.js";
|
|
26
|
+
import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.js";
|
|
27
|
+
import { findConversation } from "../daemon/conversation-registry.js";
|
|
28
|
+
import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
|
|
29
|
+
import type { TrustContext } from "../daemon/trust-context.js";
|
|
30
|
+
import { clearAllConversationIds } from "../home/feed-writer.js";
|
|
31
|
+
import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
|
|
32
|
+
import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
|
|
33
|
+
import { UserError } from "../util/errors.js";
|
|
34
|
+
import { safeParseRecord } from "../util/json.js";
|
|
35
|
+
import { getLogger } from "../util/logger.js";
|
|
36
|
+
import { getLogsDbPath } from "../util/logs-db-path.js";
|
|
37
|
+
import { getConversationsDir } from "../util/platform.js";
|
|
38
|
+
import { createRowMapper } from "../util/row-mapper.js";
|
|
39
|
+
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
40
|
+
import {
|
|
41
|
+
deleteOrphanAttachments,
|
|
42
|
+
linkAttachmentToMessage,
|
|
43
|
+
} from "./attachments-store.js";
|
|
44
|
+
import { AUTO_ANALYSIS_SOURCE } from "./auto-analysis-constants.js";
|
|
45
|
+
import {
|
|
46
|
+
appendCompactionEvent,
|
|
47
|
+
forkCompactionLedger,
|
|
48
|
+
getLatestCompactionEventAtOrBefore,
|
|
49
|
+
} from "./compaction-ledger-store.js";
|
|
50
|
+
import {
|
|
51
|
+
projectAssistantMessage,
|
|
52
|
+
seedForkedConversationAttention,
|
|
53
|
+
} from "./conversation-attention-store.js";
|
|
54
|
+
import {
|
|
55
|
+
initConversationDir,
|
|
56
|
+
removeConversationDir,
|
|
57
|
+
syncMessageToDisk,
|
|
58
|
+
updateMetaFile,
|
|
59
|
+
} from "./conversation-disk-view.js";
|
|
60
|
+
import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
|
|
61
|
+
import { ensureGroupMigration } from "./conversation-group-migration.js";
|
|
62
|
+
import { deleteConversationRowsInBatches } from "./conversation-row-batch-delete.js";
|
|
63
|
+
import { runAsyncSqlite } from "./db-async-query.js";
|
|
64
|
+
import {
|
|
65
|
+
type DrizzleDb,
|
|
66
|
+
getDb,
|
|
67
|
+
getLogsDb,
|
|
68
|
+
getSqliteFrom,
|
|
69
|
+
} from "./db-connection.js";
|
|
70
|
+
import {
|
|
71
|
+
copyForkMessagesViaSubprocess,
|
|
72
|
+
type ForkIdPair,
|
|
73
|
+
} from "./fork-message-copy.js";
|
|
74
|
+
import { getMemoryPersistenceHooks } from "./memory-lifecycle-hooks.js";
|
|
75
|
+
import {
|
|
76
|
+
rawExec,
|
|
77
|
+
rawGet,
|
|
78
|
+
rawLogsRun,
|
|
79
|
+
rawMemoryRun,
|
|
80
|
+
rawRun,
|
|
81
|
+
} from "./raw-query.js";
|
|
82
|
+
import {
|
|
83
|
+
channelInboundEvents,
|
|
84
|
+
conversations,
|
|
85
|
+
llmRequestLogs,
|
|
86
|
+
memoryEmbeddings,
|
|
87
|
+
memorySegments,
|
|
88
|
+
memorySummaries,
|
|
89
|
+
messageAttachments,
|
|
90
|
+
messages,
|
|
91
|
+
skillLoadedEvents,
|
|
92
|
+
toolInvocations,
|
|
93
|
+
} from "./schema/index.js";
|
|
94
|
+
|
|
95
|
+
const log = getLogger("conversation-store");
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The logs connection (`assistant-logs.db`), where `llm_request_logs` lives.
|
|
99
|
+
* Throws if the file cannot be opened — the few call sites here that touch
|
|
100
|
+
* request logs (conversation deletes, turn-window anchoring) have no fallback.
|
|
101
|
+
*/
|
|
102
|
+
function logsDb(): DrizzleDb {
|
|
103
|
+
const db = getLogsDb();
|
|
104
|
+
if (!db) {
|
|
105
|
+
throw new Error("logs database unavailable");
|
|
106
|
+
}
|
|
107
|
+
return db;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Message metadata Zod schema ──────────────────────────────────────
|
|
111
|
+
// Validates the JSON stored in messages.metadata. Known fields are typed;
|
|
112
|
+
// extra keys are allowed via passthrough so callers can attach ad-hoc data.
|
|
113
|
+
|
|
114
|
+
const channelIdSchema = z.enum(CHANNEL_IDS);
|
|
115
|
+
// Accept both canonical INTERFACE_IDS and the legacy "vellum" alias,
|
|
116
|
+
// normalizing to "web" on read so downstream code only handles canonical IDs.
|
|
117
|
+
const interfaceIdSchema = z
|
|
118
|
+
.string()
|
|
119
|
+
.transform((v) => parseInterfaceId(v))
|
|
120
|
+
.refine((v): v is InterfaceId => v !== null);
|
|
121
|
+
|
|
122
|
+
const subagentNotificationSchema = z.object({
|
|
123
|
+
subagentId: z.string(),
|
|
124
|
+
label: z.string(),
|
|
125
|
+
status: z.enum(["running", "completed", "failed", "aborted"]),
|
|
126
|
+
error: z.string().optional(),
|
|
127
|
+
conversationId: z.string().optional(),
|
|
128
|
+
objective: z.string().optional(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const acpNotificationSchema = z.object({
|
|
132
|
+
acpSessionId: z.string(),
|
|
133
|
+
agent: z.string().optional(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const backgroundToolCompletionMetadataSchema = z.object({
|
|
137
|
+
id: z.string(),
|
|
138
|
+
toolName: z.string(),
|
|
139
|
+
conversationId: z.string(),
|
|
140
|
+
command: z.string(),
|
|
141
|
+
startedAt: z.number(),
|
|
142
|
+
status: z.enum(["completed", "failed", "cancelled"]),
|
|
143
|
+
exitCode: z.number().nullable(),
|
|
144
|
+
output: z.string(),
|
|
145
|
+
completedAt: z.number(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export const messageMetadataSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
userMessageChannel: channelIdSchema.optional(),
|
|
151
|
+
assistantMessageChannel: channelIdSchema.optional(),
|
|
152
|
+
userMessageInterface: interfaceIdSchema.optional(),
|
|
153
|
+
assistantMessageInterface: interfaceIdSchema.optional(),
|
|
154
|
+
/**
|
|
155
|
+
* Optional client-side metadata bag attached to user messages by HTTP
|
|
156
|
+
* header middleware (reads `x-vellum-browser-family`,
|
|
157
|
+
* `x-vellum-browser-version`, `x-vellum-client-os`,
|
|
158
|
+
* `x-vellum-interface-version`). Forwarded verbatim onto
|
|
159
|
+
* `TurnTelemetryEvent.client` for downstream analytics. Kept as a
|
|
160
|
+
* permissive `record` so adding a new client field doesn't require a
|
|
161
|
+
* migration -- dbt can unpack later via JSON_VALUE.
|
|
162
|
+
*/
|
|
163
|
+
client: z.record(z.string(), z.unknown()).optional(),
|
|
164
|
+
subagentNotification: subagentNotificationSchema.optional(),
|
|
165
|
+
acpNotification: acpNotificationSchema.optional(),
|
|
166
|
+
/**
|
|
167
|
+
* Trust class of the actor at the time this message was persisted.
|
|
168
|
+
* This is a durable snapshot -- it does NOT change if the actor's
|
|
169
|
+
* trust status changes later. Used by the memory write gate (indexer)
|
|
170
|
+
* and read gate (conversation history loading) to enforce trust-aware access.
|
|
171
|
+
*/
|
|
172
|
+
provenanceTrustClass: z
|
|
173
|
+
.enum(["guardian", "trusted_contact", "unverified_contact", "unknown"])
|
|
174
|
+
.optional(),
|
|
175
|
+
provenanceSourceChannel: channelIdSchema.optional(),
|
|
176
|
+
provenanceGuardianExternalUserId: z.string().optional(),
|
|
177
|
+
provenanceRequesterIdentifier: z.string().optional(),
|
|
178
|
+
automated: z.boolean().optional(),
|
|
179
|
+
/**
|
|
180
|
+
* Structured terminal record stamped onto a `<background_event
|
|
181
|
+
* source="background-tool">` wake so the web can rebuild the inline
|
|
182
|
+
* bash/host_bash card from history after a daemon restart.
|
|
183
|
+
*/
|
|
184
|
+
backgroundToolCompletion: backgroundToolCompletionMetadataSchema.optional(),
|
|
185
|
+
forkSourceMessageId: z.string().optional(),
|
|
186
|
+
/** Image source paths from desktop attachments, keyed by filename. */
|
|
187
|
+
imageSourcePaths: z.record(z.string(), z.string()).optional(),
|
|
188
|
+
memoryInjectedBlock: z.string().optional(),
|
|
189
|
+
/** Memory-v3 frozen net-new card block (unwrapped) — the v3 counterpart
|
|
190
|
+
* of `memoryInjectedBlock`. A row carries at most one of the two. The key
|
|
191
|
+
* matches the memory plugin's `MEMORY_V3_INJECTED_BLOCK_METADATA_KEY`, kept
|
|
192
|
+
* as a literal here (like `memoryInjectedBlock`) so the storage schema does
|
|
193
|
+
* not import the memory feature. */
|
|
194
|
+
memoryV3InjectedBlock: z.string().optional(),
|
|
195
|
+
turnContextBlock: z.string().optional(),
|
|
196
|
+
pkbSystemReminderBlock: z.string().optional(),
|
|
197
|
+
workspaceBlock: z.string().optional(),
|
|
198
|
+
nowScratchpadBlock: z.string().optional(),
|
|
199
|
+
pkbContextBlock: z.string().optional(),
|
|
200
|
+
memoryV2StaticBlock: z.string().optional(),
|
|
201
|
+
/** `<background_turn>` block (background/scheduled non-interactive turns),
|
|
202
|
+
* rehydrated by `loadFromDb` for reload/fork prefix-cache parity. */
|
|
203
|
+
backgroundTurnBlock: z.string().optional(),
|
|
204
|
+
/** `<channel_capabilities>` block, rehydrated for the same reason. */
|
|
205
|
+
channelCapabilitiesBlock: z.string().optional(),
|
|
206
|
+
/** `<non_interactive_context>` block, rehydrated for the same reason. */
|
|
207
|
+
nonInteractiveContextBlock: z.string().optional(),
|
|
208
|
+
})
|
|
209
|
+
.passthrough();
|
|
210
|
+
|
|
211
|
+
function cloneForkMessageMetadata(
|
|
212
|
+
metadata: string | null,
|
|
213
|
+
sourceMessageId: string,
|
|
214
|
+
): string {
|
|
215
|
+
if (!metadata) {
|
|
216
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const parsed = JSON.parse(metadata);
|
|
221
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
222
|
+
const sourceRecord = parsed as Record<string, unknown>;
|
|
223
|
+
const forkSourceMessageId =
|
|
224
|
+
typeof sourceRecord.forkSourceMessageId === "string"
|
|
225
|
+
? sourceRecord.forkSourceMessageId
|
|
226
|
+
: sourceMessageId;
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
...sourceRecord,
|
|
229
|
+
forkSourceMessageId,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Fall through to source-only metadata.
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract provenance metadata fields from a TrustContext.
|
|
241
|
+
* When no guardian context is provided, defaults to 'unknown' because the
|
|
242
|
+
* absence of trust context means we cannot verify trust —
|
|
243
|
+
* callers with actual guardian trust should always supply a real context.
|
|
244
|
+
*/
|
|
245
|
+
export function provenanceFromTrustContext(
|
|
246
|
+
ctx: TrustContext | null | undefined,
|
|
247
|
+
): Record<string, unknown> {
|
|
248
|
+
if (!ctx) return { provenanceTrustClass: "unknown" };
|
|
249
|
+
return {
|
|
250
|
+
provenanceTrustClass: ctx.trustClass,
|
|
251
|
+
provenanceSourceChannel: ctx.sourceChannel,
|
|
252
|
+
provenanceGuardianExternalUserId: ctx.guardianExternalUserId,
|
|
253
|
+
provenanceRequesterIdentifier: ctx.requesterIdentifier,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Extract image file paths from resolved attachments for message metadata. */
|
|
258
|
+
export function extractImageSourcePaths(
|
|
259
|
+
attachments: ReadonlyArray<{
|
|
260
|
+
filename: string;
|
|
261
|
+
mimeType: string;
|
|
262
|
+
filePath?: string;
|
|
263
|
+
}>,
|
|
264
|
+
): Record<string, string> | undefined {
|
|
265
|
+
const paths: Record<string, string> = {};
|
|
266
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
267
|
+
const a = attachments[i];
|
|
268
|
+
if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
|
|
269
|
+
paths[`${i}:${a.filename}`] = a.filePath;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return Object.keys(paths).length > 0 ? paths : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface ConversationRow {
|
|
276
|
+
id: string;
|
|
277
|
+
title: string | null;
|
|
278
|
+
createdAt: number;
|
|
279
|
+
updatedAt: number;
|
|
280
|
+
totalInputTokens: number;
|
|
281
|
+
totalOutputTokens: number;
|
|
282
|
+
totalEstimatedCost: number;
|
|
283
|
+
contextSummary: string | null;
|
|
284
|
+
contextCompactedMessageCount: number;
|
|
285
|
+
contextCompactedAt: number | null;
|
|
286
|
+
historyStrippedAt: number | null;
|
|
287
|
+
slackContextCompactionWatermarkTs: string | null;
|
|
288
|
+
slackContextCompactionWatermarkAt: number | null;
|
|
289
|
+
conversationType: string;
|
|
290
|
+
source: string;
|
|
291
|
+
memoryScopeId: string;
|
|
292
|
+
originChannel: string | null;
|
|
293
|
+
originInterface: string | null;
|
|
294
|
+
forkParentConversationId: string | null;
|
|
295
|
+
forkParentMessageId: string | null;
|
|
296
|
+
isAutoTitle: number;
|
|
297
|
+
scheduleJobId: string | null;
|
|
298
|
+
lastMessageAt: number | null;
|
|
299
|
+
archivedAt: number | null;
|
|
300
|
+
surfacedAt: number | null;
|
|
301
|
+
inferenceProfile: string | null;
|
|
302
|
+
inferenceProfileSessionId: string | null;
|
|
303
|
+
inferenceProfileExpiresAt: number | null;
|
|
304
|
+
lastNotifiedInferenceProfile: string | null;
|
|
305
|
+
processingStartedAt: number | null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const parseConversation = createRowMapper<
|
|
309
|
+
typeof conversations.$inferSelect,
|
|
310
|
+
ConversationRow
|
|
311
|
+
>({
|
|
312
|
+
id: "id",
|
|
313
|
+
title: "title",
|
|
314
|
+
createdAt: "createdAt",
|
|
315
|
+
updatedAt: "updatedAt",
|
|
316
|
+
totalInputTokens: "totalInputTokens",
|
|
317
|
+
totalOutputTokens: "totalOutputTokens",
|
|
318
|
+
totalEstimatedCost: "totalEstimatedCost",
|
|
319
|
+
contextSummary: "contextSummary",
|
|
320
|
+
contextCompactedMessageCount: "contextCompactedMessageCount",
|
|
321
|
+
contextCompactedAt: "contextCompactedAt",
|
|
322
|
+
historyStrippedAt: "historyStrippedAt",
|
|
323
|
+
slackContextCompactionWatermarkTs: "slackContextCompactionWatermarkTs",
|
|
324
|
+
slackContextCompactionWatermarkAt: "slackContextCompactionWatermarkAt",
|
|
325
|
+
conversationType: "conversationType",
|
|
326
|
+
source: "source",
|
|
327
|
+
memoryScopeId: "memoryScopeId",
|
|
328
|
+
originChannel: "originChannel",
|
|
329
|
+
originInterface: "originInterface",
|
|
330
|
+
forkParentConversationId: "forkParentConversationId",
|
|
331
|
+
forkParentMessageId: "forkParentMessageId",
|
|
332
|
+
isAutoTitle: "isAutoTitle",
|
|
333
|
+
scheduleJobId: "scheduleJobId",
|
|
334
|
+
lastMessageAt: "lastMessageAt",
|
|
335
|
+
archivedAt: "archivedAt",
|
|
336
|
+
surfacedAt: "surfacedAt",
|
|
337
|
+
inferenceProfile: "inferenceProfile",
|
|
338
|
+
inferenceProfileSessionId: "inferenceProfileSessionId",
|
|
339
|
+
inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
|
|
340
|
+
lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
|
|
341
|
+
processingStartedAt: "processingStartedAt",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
/** Allowed values for the `role` column on `messages`. */
|
|
345
|
+
export type MessageRole = "user" | "assistant" | "system";
|
|
346
|
+
|
|
347
|
+
export interface MessageRow {
|
|
348
|
+
id: string;
|
|
349
|
+
conversationId: string;
|
|
350
|
+
role: string;
|
|
351
|
+
content: string;
|
|
352
|
+
createdAt: number;
|
|
353
|
+
metadata: string | null;
|
|
354
|
+
clientMessageId: string | null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const parseMessage = createRowMapper<typeof messages.$inferSelect, MessageRow>({
|
|
358
|
+
id: "id",
|
|
359
|
+
conversationId: "conversationId",
|
|
360
|
+
role: "role",
|
|
361
|
+
content: "content",
|
|
362
|
+
createdAt: "createdAt",
|
|
363
|
+
metadata: "metadata",
|
|
364
|
+
clientMessageId: "clientMessageId",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
export type ConversationCreateType = "standard" | "background" | "scheduled";
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Monotonic timestamp source for message ordering. Two messages saved within
|
|
371
|
+
* the same millisecond (e.g., tool_results user message + assistant message in
|
|
372
|
+
* message_complete) would get the same Date.now(), making their reload order
|
|
373
|
+
* non-deterministic. This counter ensures every call returns a strictly
|
|
374
|
+
* increasing value so insertion order is always preserved.
|
|
375
|
+
*/
|
|
376
|
+
let lastTimestamp = 0;
|
|
377
|
+
function monotonicNow(): number {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
lastTimestamp = Math.max(now, lastTimestamp + 1);
|
|
380
|
+
return lastTimestamp;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── insertMessageCore ─────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
/** Shape returned by {@link insertMessageCore} and its public wrappers. */
|
|
386
|
+
interface InsertedMessage {
|
|
387
|
+
id: string;
|
|
388
|
+
conversationId: string;
|
|
389
|
+
role: MessageRole;
|
|
390
|
+
content: string;
|
|
391
|
+
createdAt: number;
|
|
392
|
+
metadata?: string;
|
|
393
|
+
clientMessageId?: string;
|
|
394
|
+
deduplicated: boolean;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
interface InsertMessageCoreParams {
|
|
398
|
+
conversationId: string;
|
|
399
|
+
role: MessageRole;
|
|
400
|
+
content: string;
|
|
401
|
+
metadata?: Record<string, unknown>;
|
|
402
|
+
clientMessageId?: string;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Core message persistence primitive shared by {@link addMessage} and
|
|
407
|
+
* {@link reserveMessage}.
|
|
408
|
+
*
|
|
409
|
+
* Inserts a message row inside a transaction that atomically bumps the
|
|
410
|
+
* parent conversation's `updatedAt` / `lastMessageAt` timestamps and
|
|
411
|
+
* conditionally sets the conversation's `originChannel` when the first
|
|
412
|
+
* channel-originated message arrives.
|
|
413
|
+
*
|
|
414
|
+
* When a `clientMessageId` is provided the insert runs inside a
|
|
415
|
+
* SAVEPOINT. If the partial unique index on
|
|
416
|
+
* `(conversation_id, client_message_id)` raises
|
|
417
|
+
* `SQLITE_CONSTRAINT_UNIQUE`, the SAVEPOINT is rolled back, the
|
|
418
|
+
* existing row is fetched, and returned with `deduplicated: true`.
|
|
419
|
+
* This makes the operation idempotent for client-generated
|
|
420
|
+
* correlation nonces.
|
|
421
|
+
*
|
|
422
|
+
* Retries up to 3 times on `SQLITE_BUSY*` / `SQLITE_IOERR*` to handle
|
|
423
|
+
* WAL contention. The timestamp is recomputed each attempt so a late
|
|
424
|
+
* retry doesn't persist a stale `updatedAt`.
|
|
425
|
+
*/
|
|
426
|
+
async function insertMessageCore(
|
|
427
|
+
params: InsertMessageCoreParams,
|
|
428
|
+
): Promise<InsertedMessage> {
|
|
429
|
+
const { conversationId, role, content, metadata, clientMessageId } = params;
|
|
430
|
+
const db = getDb();
|
|
431
|
+
const messageId = uuid();
|
|
432
|
+
|
|
433
|
+
if (metadata) {
|
|
434
|
+
const result = messageMetadataSchema.safeParse(metadata);
|
|
435
|
+
if (!result.success) {
|
|
436
|
+
log.warn(
|
|
437
|
+
{ conversationId, messageId, issues: result.error.issues },
|
|
438
|
+
"Invalid message metadata, storing as-is",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const metadataStr = metadata ? JSON.stringify(metadata) : undefined;
|
|
444
|
+
const originChannelCandidate =
|
|
445
|
+
metadata && isChannelId(metadata.userMessageChannel)
|
|
446
|
+
? metadata.userMessageChannel
|
|
447
|
+
: null;
|
|
448
|
+
|
|
449
|
+
// The timestamp is recomputed each attempt so a late retry doesn't persist a
|
|
450
|
+
// stale `updatedAt`.
|
|
451
|
+
return withSqliteRetry(
|
|
452
|
+
(): InsertedMessage => {
|
|
453
|
+
const now = monotonicNow();
|
|
454
|
+
const values = {
|
|
455
|
+
id: messageId,
|
|
456
|
+
conversationId,
|
|
457
|
+
role,
|
|
458
|
+
content,
|
|
459
|
+
createdAt: now,
|
|
460
|
+
...(metadataStr ? { metadata: metadataStr } : {}),
|
|
461
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
if (clientMessageId) {
|
|
465
|
+
// Idempotent insert: skip silently if this clientMessageId was
|
|
466
|
+
// already persisted for the conversation.
|
|
467
|
+
const raw = getSqliteFrom(db);
|
|
468
|
+
raw.exec("SAVEPOINT insert_msg");
|
|
469
|
+
try {
|
|
470
|
+
db.insert(messages).values(values).run();
|
|
471
|
+
if (originChannelCandidate) {
|
|
472
|
+
db.update(conversations)
|
|
473
|
+
.set({ originChannel: originChannelCandidate })
|
|
474
|
+
.where(
|
|
475
|
+
and(
|
|
476
|
+
eq(conversations.id, conversationId),
|
|
477
|
+
isNull(conversations.originChannel),
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
.run();
|
|
481
|
+
}
|
|
482
|
+
db.update(conversations)
|
|
483
|
+
.set({ updatedAt: now, lastMessageAt: now })
|
|
484
|
+
.where(eq(conversations.id, conversationId))
|
|
485
|
+
.run();
|
|
486
|
+
raw.exec("RELEASE insert_msg");
|
|
487
|
+
} catch (insertErr) {
|
|
488
|
+
raw.exec("ROLLBACK TO insert_msg");
|
|
489
|
+
raw.exec("RELEASE insert_msg");
|
|
490
|
+
const code = (insertErr as { code?: string }).code ?? "";
|
|
491
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
492
|
+
// Duplicate clientMessageId — return the existing row.
|
|
493
|
+
const existing = db
|
|
494
|
+
.select()
|
|
495
|
+
.from(messages)
|
|
496
|
+
.where(
|
|
497
|
+
and(
|
|
498
|
+
eq(messages.conversationId, conversationId),
|
|
499
|
+
eq(messages.clientMessageId, clientMessageId),
|
|
500
|
+
),
|
|
501
|
+
)
|
|
502
|
+
.get();
|
|
503
|
+
if (existing) {
|
|
504
|
+
return {
|
|
505
|
+
id: existing.id,
|
|
506
|
+
conversationId: existing.conversationId,
|
|
507
|
+
role: existing.role as MessageRole,
|
|
508
|
+
content: existing.content,
|
|
509
|
+
createdAt: existing.createdAt,
|
|
510
|
+
...(existing.metadata ? { metadata: existing.metadata } : {}),
|
|
511
|
+
clientMessageId: existing.clientMessageId ?? undefined,
|
|
512
|
+
deduplicated: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
throw insertErr;
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
// No clientMessageId — standard insert inside a transaction.
|
|
520
|
+
db.transaction((tx) => {
|
|
521
|
+
tx.insert(messages).values(values).run();
|
|
522
|
+
if (originChannelCandidate) {
|
|
523
|
+
tx.update(conversations)
|
|
524
|
+
.set({ originChannel: originChannelCandidate })
|
|
525
|
+
.where(
|
|
526
|
+
and(
|
|
527
|
+
eq(conversations.id, conversationId),
|
|
528
|
+
isNull(conversations.originChannel),
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
.run();
|
|
532
|
+
}
|
|
533
|
+
tx.update(conversations)
|
|
534
|
+
.set({ updatedAt: now, lastMessageAt: now })
|
|
535
|
+
.where(eq(conversations.id, conversationId))
|
|
536
|
+
.run();
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
id: messageId,
|
|
542
|
+
conversationId,
|
|
543
|
+
role,
|
|
544
|
+
content,
|
|
545
|
+
createdAt: now,
|
|
546
|
+
...(metadataStr ? { metadata: metadataStr } : {}),
|
|
547
|
+
...(clientMessageId ? { clientMessageId } : {}),
|
|
548
|
+
deduplicated: false,
|
|
549
|
+
};
|
|
550
|
+
},
|
|
551
|
+
{ op: "insertMessageCore", context: { conversationId } },
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function createConversation(
|
|
556
|
+
titleOrOpts?:
|
|
557
|
+
| string
|
|
558
|
+
| {
|
|
559
|
+
title?: string;
|
|
560
|
+
/**
|
|
561
|
+
* Override the `is_auto_title` column (schema default 1). Pass
|
|
562
|
+
* `AUTO_TITLE_DETERMINISTIC` (2) when the title was derived
|
|
563
|
+
* deterministically at bootstrap so later generation passes know
|
|
564
|
+
* they may replace it.
|
|
565
|
+
*/
|
|
566
|
+
isAutoTitle?: number;
|
|
567
|
+
conversationType?: ConversationCreateType;
|
|
568
|
+
source?: string;
|
|
569
|
+
scheduleJobId?: string;
|
|
570
|
+
groupId?: string;
|
|
571
|
+
forkParentConversationId?: string;
|
|
572
|
+
},
|
|
573
|
+
) {
|
|
574
|
+
const db = getDb();
|
|
575
|
+
const now = Date.now();
|
|
576
|
+
const initialSeq = getCurrentSeq();
|
|
577
|
+
const opts =
|
|
578
|
+
typeof titleOrOpts === "string"
|
|
579
|
+
? { title: titleOrOpts }
|
|
580
|
+
: (titleOrOpts ?? {});
|
|
581
|
+
const requestedConversationType = opts.conversationType;
|
|
582
|
+
const conversationType: ConversationCreateType =
|
|
583
|
+
requestedConversationType ?? "standard";
|
|
584
|
+
const source = opts.source ?? "user";
|
|
585
|
+
const groupId = opts.groupId;
|
|
586
|
+
const id = uuid();
|
|
587
|
+
const memoryScopeId = "default";
|
|
588
|
+
|
|
589
|
+
// Ensure group_id column exists for deterministic schema readiness,
|
|
590
|
+
// even when this conversation has no groupId (a subsequent query or
|
|
591
|
+
// reorder may reference the column).
|
|
592
|
+
ensureGroupMigration();
|
|
593
|
+
|
|
594
|
+
const conversation = {
|
|
595
|
+
id,
|
|
596
|
+
title: opts.title ?? null,
|
|
597
|
+
...(opts.isAutoTitle !== undefined
|
|
598
|
+
? { isAutoTitle: opts.isAutoTitle }
|
|
599
|
+
: {}),
|
|
600
|
+
createdAt: now,
|
|
601
|
+
updatedAt: now,
|
|
602
|
+
totalInputTokens: 0,
|
|
603
|
+
totalOutputTokens: 0,
|
|
604
|
+
totalEstimatedCost: 0,
|
|
605
|
+
contextSummary: null as string | null,
|
|
606
|
+
contextCompactedMessageCount: 0,
|
|
607
|
+
contextCompactedAt: null as number | null,
|
|
608
|
+
slackContextCompactionWatermarkTs: null as string | null,
|
|
609
|
+
slackContextCompactionWatermarkAt: null as number | null,
|
|
610
|
+
conversationType,
|
|
611
|
+
source,
|
|
612
|
+
memoryScopeId,
|
|
613
|
+
scheduleJobId: opts.scheduleJobId ?? null,
|
|
614
|
+
forkParentConversationId: opts.forkParentConversationId ?? null,
|
|
615
|
+
// Snapshot↔stream alignment baseline, captured at the creation instant.
|
|
616
|
+
// 0 (nothing stamped yet this process) is stored as NULL so `/messages`
|
|
617
|
+
// reports null and the client cold-starts rather than aligning to seq 0.
|
|
618
|
+
seq: initialSeq > 0 ? initialSeq : null,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// Insert the row and set its group_id (raw-SQL-only, not in the Drizzle
|
|
622
|
+
// schema, so it's a second statement) atomically. A SAVEPOINT — not
|
|
623
|
+
// `db.transaction()` — because createConversation also runs INSIDE the fork
|
|
624
|
+
// paths' transactions, where a nested `BEGIN` would error; a savepoint nests
|
|
625
|
+
// cleanly and is still atomic at the top level. createConversation is a
|
|
626
|
+
// synchronous primitive and does not retry on contention: callers on
|
|
627
|
+
// write-contended paths wrap the call in `withSqliteRetry`, and because the
|
|
628
|
+
// insert+update are atomic here, such a retry re-runs the whole thing cleanly
|
|
629
|
+
// (a failed attempt rolls back, so no half-written row is ever left behind).
|
|
630
|
+
const effectiveGroupId = groupId ?? "system:all";
|
|
631
|
+
const raw = getSqliteFrom(db);
|
|
632
|
+
raw.exec("SAVEPOINT create_conv");
|
|
633
|
+
try {
|
|
634
|
+
db.insert(conversations).values(conversation).run();
|
|
635
|
+
rawRun(
|
|
636
|
+
"UPDATE conversations SET group_id = ?, is_pinned = ? WHERE id = ?",
|
|
637
|
+
effectiveGroupId,
|
|
638
|
+
effectiveGroupId === "system:pinned" ? 1 : 0,
|
|
639
|
+
id,
|
|
640
|
+
);
|
|
641
|
+
raw.exec("RELEASE create_conv");
|
|
642
|
+
} catch (err) {
|
|
643
|
+
raw.exec("ROLLBACK TO create_conv");
|
|
644
|
+
raw.exec("RELEASE create_conv");
|
|
645
|
+
throw err;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
initConversationDir({ ...conversation, originChannel: null });
|
|
649
|
+
|
|
650
|
+
return conversation;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function getConversation(id: string): ConversationRow | null {
|
|
654
|
+
const db = getDb();
|
|
655
|
+
const row = db
|
|
656
|
+
.select()
|
|
657
|
+
.from(conversations)
|
|
658
|
+
.where(eq(conversations.id, id))
|
|
659
|
+
.get();
|
|
660
|
+
return row ? parseConversation(row) : null;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Count conversations that reference a given schedule job ID.
|
|
665
|
+
* Useful for determining whether a schedule can be safely deleted
|
|
666
|
+
* (i.e. no other conversations still reference it).
|
|
667
|
+
*/
|
|
668
|
+
export function countConversationsByScheduleJobId(
|
|
669
|
+
scheduleJobId: string,
|
|
670
|
+
): number {
|
|
671
|
+
return (
|
|
672
|
+
rawGet<{ c: number }>(
|
|
673
|
+
"SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
|
|
674
|
+
scheduleJobId,
|
|
675
|
+
)?.c ?? 0
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Find the rolling analysis conversation for a given source conversation,
|
|
681
|
+
* or null if none exists yet. Used by the auto-analyze loop to append
|
|
682
|
+
* to an existing analysis conversation rather than creating a new one
|
|
683
|
+
* each time the analyze job fires.
|
|
684
|
+
*
|
|
685
|
+
* Returns the most recently updated match if multiple exist (defensive —
|
|
686
|
+
* shouldn't happen in normal operation but the contract is well-defined).
|
|
687
|
+
*
|
|
688
|
+
* Hits `idx_conversations_fork_parent_conversation_id` for the
|
|
689
|
+
* `forkParentConversationId` lookup.
|
|
690
|
+
*/
|
|
691
|
+
export function findAnalysisConversationFor(
|
|
692
|
+
parentConversationId: string,
|
|
693
|
+
): { id: string } | null {
|
|
694
|
+
const db = getDb();
|
|
695
|
+
const row = db
|
|
696
|
+
.select({ id: conversations.id })
|
|
697
|
+
.from(conversations)
|
|
698
|
+
.where(
|
|
699
|
+
and(
|
|
700
|
+
eq(conversations.source, AUTO_ANALYSIS_SOURCE),
|
|
701
|
+
eq(conversations.forkParentConversationId, parentConversationId),
|
|
702
|
+
),
|
|
703
|
+
)
|
|
704
|
+
.orderBy(desc(conversations.updatedAt))
|
|
705
|
+
.limit(1)
|
|
706
|
+
.get();
|
|
707
|
+
return row ? { id: row.id } : null;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Returns the `source` column for the given conversation, or null if
|
|
712
|
+
* not found. Tiny convenience used by the recursion guard in the
|
|
713
|
+
* auto-analyze loop.
|
|
714
|
+
*/
|
|
715
|
+
export function getConversationSource(conversationId: string): string | null {
|
|
716
|
+
const db = getDb();
|
|
717
|
+
const row = db
|
|
718
|
+
.select({ source: conversations.source })
|
|
719
|
+
.from(conversations)
|
|
720
|
+
.where(eq(conversations.id, conversationId))
|
|
721
|
+
.get();
|
|
722
|
+
return row?.source ?? null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Fetch group_id for a conversation via raw SQL. group_id is NOT in the
|
|
727
|
+
* Drizzle schema (raw-query-only pattern), so ConversationRow doesn't
|
|
728
|
+
* include it. This helper is used by forkConversation to inherit group_id.
|
|
729
|
+
*/
|
|
730
|
+
function getConversationGroupId(conversationId: string): string | null {
|
|
731
|
+
ensureGroupMigration();
|
|
732
|
+
const row = rawGet<{ group_id: string | null }>(
|
|
733
|
+
"SELECT group_id FROM conversations WHERE id = ?",
|
|
734
|
+
conversationId,
|
|
735
|
+
);
|
|
736
|
+
return row?.group_id ?? null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function forkConversation(params: {
|
|
740
|
+
conversationId: string;
|
|
741
|
+
throughMessageId?: string;
|
|
742
|
+
/**
|
|
743
|
+
* Override the fork's `source` column. Defaults to the standard
|
|
744
|
+
* `createConversation` default (`"user"`). Used by fork-based memory
|
|
745
|
+
* retrospectives to mark the fork as a retrospective artifact distinct
|
|
746
|
+
* from a user-initiated fork, so dedup and cleanup queries can scope
|
|
747
|
+
* correctly.
|
|
748
|
+
*/
|
|
749
|
+
source?: string;
|
|
750
|
+
/**
|
|
751
|
+
* Optional title for the fork. Defaults to `<parent title> (Fork)`.
|
|
752
|
+
*/
|
|
753
|
+
title?: string;
|
|
754
|
+
/**
|
|
755
|
+
* Override the fork's `conversationType` column. Defaults to `"standard"`.
|
|
756
|
+
* Used by fork-based memory retrospectives to bucket the fork as a
|
|
757
|
+
* `"background"` conversation so it doesn't surface in the user's
|
|
758
|
+
* conversation list.
|
|
759
|
+
*/
|
|
760
|
+
conversationType?: ConversationCreateType;
|
|
761
|
+
/**
|
|
762
|
+
* Override the fork's `groupId`. Defaults to the parent conversation's
|
|
763
|
+
* group (or `"system:all"` when the parent has none). Used by fork-based
|
|
764
|
+
* memory retrospectives to route the fork into a dedicated background
|
|
765
|
+
* group.
|
|
766
|
+
*/
|
|
767
|
+
groupId?: string;
|
|
768
|
+
}): ConversationRow {
|
|
769
|
+
const { conversationId, throughMessageId } = params;
|
|
770
|
+
const db = getDb();
|
|
771
|
+
const sourceConversation = getConversation(conversationId);
|
|
772
|
+
|
|
773
|
+
if (!sourceConversation) {
|
|
774
|
+
throw new UserError(`Conversation ${conversationId} not found`);
|
|
775
|
+
}
|
|
776
|
+
const sourceMessages = getMessages(conversationId);
|
|
777
|
+
if (throughMessageId != null) {
|
|
778
|
+
// `getMessages` orders by `createdAt` only; when rows share an identical
|
|
779
|
+
// millisecond timestamp the tie order is unspecified. Callers that pin the
|
|
780
|
+
// fork to a cutoff choose it from a `(createdAt, id)` cursor (e.g. the
|
|
781
|
+
// memory-retrospective job, via `getMessagesAfter`), so slicing through
|
|
782
|
+
// `throughMessageId` under the unstable order could include same-timestamp
|
|
783
|
+
// siblings the cursor considers *after* the cutoff (reprocessed next run)
|
|
784
|
+
// or exclude ones it considers *before* it (skipped forever). Re-sort on
|
|
785
|
+
// `(createdAt, id)` so the slice agrees with the cutoff. The unpinned full
|
|
786
|
+
// fork copies every row regardless of order, so it keeps source order.
|
|
787
|
+
sourceMessages.sort(
|
|
788
|
+
(a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (sourceMessages.length === 0) {
|
|
793
|
+
throw new UserError(
|
|
794
|
+
`Conversation ${conversationId} has no persisted messages to fork`,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const initialBoundaryIndex =
|
|
799
|
+
throughMessageId == null
|
|
800
|
+
? sourceMessages.length - 1
|
|
801
|
+
: sourceMessages.findIndex((message) => message.id === throughMessageId);
|
|
802
|
+
|
|
803
|
+
if (throughMessageId != null && initialBoundaryIndex === -1) {
|
|
804
|
+
throw new UserError(
|
|
805
|
+
`Message ${throughMessageId} does not belong to conversation ${conversationId}`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Extend the boundary to cover the full display turn the client
|
|
810
|
+
// addressed. The read-path collapses each assistant turn across
|
|
811
|
+
// multiple DB rows — consecutive assistant rows AND tool-result-only
|
|
812
|
+
// user rows between them — so "fork through message X" semantically
|
|
813
|
+
// means "fork through the entire display turn containing X" no matter
|
|
814
|
+
// which DB row in the cluster the client supplied. Single source of
|
|
815
|
+
// truth is `findDisplayTurnEndIndex`, shared with the read path so
|
|
816
|
+
// both stay in sync.
|
|
817
|
+
const copyBoundaryIndex = findDisplayTurnEndIndex(
|
|
818
|
+
sourceMessages,
|
|
819
|
+
initialBoundaryIndex,
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
const messagesToCopy =
|
|
823
|
+
copyBoundaryIndex >= 0
|
|
824
|
+
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
825
|
+
: ([] as MessageRow[]);
|
|
826
|
+
|
|
827
|
+
// Inherit the history-strip marker only when the fork boundary is at-or-
|
|
828
|
+
// after the strip event. Pre-strip forks branch from history that pre-
|
|
829
|
+
// dates the strip, so the marker would be a no-op and is misleading to
|
|
830
|
+
// copy.
|
|
831
|
+
const sourceHistoryStrippedAt = sourceConversation.historyStrippedAt ?? null;
|
|
832
|
+
const boundaryMessageCreatedAt = messagesToCopy.at(-1)?.createdAt ?? null;
|
|
833
|
+
const inheritsHistoryStrippedAt =
|
|
834
|
+
sourceHistoryStrippedAt != null &&
|
|
835
|
+
boundaryMessageCreatedAt != null &&
|
|
836
|
+
boundaryMessageCreatedAt >= sourceHistoryStrippedAt;
|
|
837
|
+
|
|
838
|
+
// Inherit compaction by the same temporal rule: apply the most recent
|
|
839
|
+
// compaction whose event time is at-or-before the forked-from message. A
|
|
840
|
+
// compaction that ran after the boundary message did not exist at that point
|
|
841
|
+
// in the conversation, so the fork branches from full uncompacted history.
|
|
842
|
+
const inheritedCompaction = getLatestCompactionEventAtOrBefore(
|
|
843
|
+
sourceConversation.id,
|
|
844
|
+
boundaryMessageCreatedAt,
|
|
845
|
+
);
|
|
846
|
+
// The Slack chronological-context watermark is single-valued on the source
|
|
847
|
+
// row and reflects only the latest compaction, so carry it only when the
|
|
848
|
+
// fork inherits that latest compaction. Pairing the latest watermark with an
|
|
849
|
+
// older inherited summary (a fork between two compactions) would filter out
|
|
850
|
+
// Slack messages the older summary does not cover.
|
|
851
|
+
const inheritsLatestCompaction =
|
|
852
|
+
inheritedCompaction != null &&
|
|
853
|
+
inheritedCompaction.compactedAt === sourceConversation.contextCompactedAt;
|
|
854
|
+
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
855
|
+
const forkTitle =
|
|
856
|
+
params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
857
|
+
|
|
858
|
+
// Collect disk-sync work to run after the transaction commits.
|
|
859
|
+
const diskSyncQueue: Array<{
|
|
860
|
+
conversationId: string;
|
|
861
|
+
messageId: string;
|
|
862
|
+
createdAt: number;
|
|
863
|
+
}> = [];
|
|
864
|
+
|
|
865
|
+
// Wrap all DB mutations in a single transaction so a mid-flight failure
|
|
866
|
+
// rolls back cleanly instead of leaving a partial fork. Helper functions
|
|
867
|
+
// (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
|
|
868
|
+
// use the same underlying bun:sqlite connection, so their writes participate
|
|
869
|
+
// in this transaction automatically.
|
|
870
|
+
// Inherit group_id from parent via raw SQL helper (group_id is not in Drizzle schema)
|
|
871
|
+
const parentGroupId = getConversationGroupId(conversationId);
|
|
872
|
+
|
|
873
|
+
const forkedConversation = db.transaction(() => {
|
|
874
|
+
const fc = createConversation({
|
|
875
|
+
title: forkTitle,
|
|
876
|
+
conversationType: params.conversationType ?? "standard",
|
|
877
|
+
groupId: params.groupId ?? parentGroupId ?? "system:all",
|
|
878
|
+
...(params.source != null ? { source: params.source } : {}),
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
db.update(conversations)
|
|
882
|
+
.set({
|
|
883
|
+
forkParentConversationId: sourceConversation.id,
|
|
884
|
+
forkParentMessageId,
|
|
885
|
+
contextSummary: inheritedCompaction?.summary ?? null,
|
|
886
|
+
contextCompactedMessageCount:
|
|
887
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
888
|
+
contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
|
|
889
|
+
slackContextCompactionWatermarkTs: inheritsLatestCompaction
|
|
890
|
+
? sourceConversation.slackContextCompactionWatermarkTs
|
|
891
|
+
: null,
|
|
892
|
+
slackContextCompactionWatermarkAt: inheritsLatestCompaction
|
|
893
|
+
? sourceConversation.slackContextCompactionWatermarkAt
|
|
894
|
+
: null,
|
|
895
|
+
historyStrippedAt: inheritsHistoryStrippedAt
|
|
896
|
+
? sourceHistoryStrippedAt
|
|
897
|
+
: null,
|
|
898
|
+
inferenceProfile: sourceConversation.inferenceProfile,
|
|
899
|
+
})
|
|
900
|
+
.where(eq(conversations.id, fc.id))
|
|
901
|
+
.run();
|
|
902
|
+
|
|
903
|
+
const forkedMessageIds = new Map<string, string>();
|
|
904
|
+
let latestForkedAssistant: {
|
|
905
|
+
messageId: string;
|
|
906
|
+
messageAt: number;
|
|
907
|
+
} | null = null;
|
|
908
|
+
|
|
909
|
+
const forkedMessageValues = messagesToCopy.map((message) => {
|
|
910
|
+
const forkedMessageId = uuid();
|
|
911
|
+
forkedMessageIds.set(message.id, forkedMessageId);
|
|
912
|
+
|
|
913
|
+
if (message.role === "assistant") {
|
|
914
|
+
latestForkedAssistant = {
|
|
915
|
+
messageId: forkedMessageId,
|
|
916
|
+
messageAt: message.createdAt,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
id: forkedMessageId,
|
|
922
|
+
conversationId: fc.id,
|
|
923
|
+
role: message.role,
|
|
924
|
+
content: message.content,
|
|
925
|
+
createdAt: message.createdAt,
|
|
926
|
+
metadata: cloneForkMessageMetadata(message.metadata, message.id),
|
|
927
|
+
};
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// Insert in chunks of one multi-row statement each so a large fork takes
|
|
931
|
+
// the SQLite write lock O(messages / chunk) times instead of once per row.
|
|
932
|
+
const FORK_INSERT_CHUNK_SIZE = 200;
|
|
933
|
+
for (
|
|
934
|
+
let i = 0;
|
|
935
|
+
i < forkedMessageValues.length;
|
|
936
|
+
i += FORK_INSERT_CHUNK_SIZE
|
|
937
|
+
) {
|
|
938
|
+
const chunk = forkedMessageValues.slice(i, i + FORK_INSERT_CHUNK_SIZE);
|
|
939
|
+
db.insert(messages).values(chunk).run();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
populateForkContentsInProcess({
|
|
943
|
+
fork: fc,
|
|
944
|
+
sourceConversationId: sourceConversation.id,
|
|
945
|
+
messagesToCopy,
|
|
946
|
+
forkedMessageIds,
|
|
947
|
+
latestForkedAssistant,
|
|
948
|
+
isFullHistoryFork: copyBoundaryIndex === sourceMessages.length - 1,
|
|
949
|
+
inheritedCompactedMessageCount:
|
|
950
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
951
|
+
diskSyncQueue,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
return fc;
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Disk-view sync runs after commit — file I/O is idempotent and
|
|
958
|
+
// conversation deletion cleans up orphaned directories.
|
|
959
|
+
for (const entry of diskSyncQueue) {
|
|
960
|
+
syncMessageToDisk(entry.conversationId, entry.messageId, entry.createdAt);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const persistedFork = getConversation(forkedConversation.id);
|
|
964
|
+
if (!persistedFork) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`Failed to load forked conversation ${forkedConversation.id} after creation`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return persistedFork;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
interface PopulateForkContentsArgs {
|
|
974
|
+
/** The freshly-created fork conversation row (needs `id` + `createdAt`). */
|
|
975
|
+
fork: { id: string; createdAt: number };
|
|
976
|
+
sourceConversationId: string;
|
|
977
|
+
messagesToCopy: MessageRow[];
|
|
978
|
+
/** Source→fork message-id map for every copied row. */
|
|
979
|
+
forkedMessageIds: Map<string, string>;
|
|
980
|
+
latestForkedAssistant: { messageId: string; messageAt: number } | null;
|
|
981
|
+
/** `copyBoundaryIndex === sourceMessages.length - 1` for the source. */
|
|
982
|
+
isFullHistoryFork: boolean;
|
|
983
|
+
/**
|
|
984
|
+
* Count of leading messages behind the compaction event this fork inherits
|
|
985
|
+
* (0 when the fork branches from uncompacted history). Memory-slug seeding
|
|
986
|
+
* skips this prefix, since rows behind the fork's summary are not rendered
|
|
987
|
+
* and must stay re-injectable.
|
|
988
|
+
*/
|
|
989
|
+
inheritedCompactedMessageCount: number;
|
|
990
|
+
/**
|
|
991
|
+
* When provided, a disk-sync entry is appended per copied message for the
|
|
992
|
+
* caller to flush after commit. Omitted by the retrospective fork, whose
|
|
993
|
+
* throwaway conversation needs no disk-view projection.
|
|
994
|
+
*/
|
|
995
|
+
diskSyncQueue?: Array<{
|
|
996
|
+
conversationId: string;
|
|
997
|
+
messageId: string;
|
|
998
|
+
createdAt: number;
|
|
999
|
+
}>;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* In-process tail of a conversation fork: relink each copied message's
|
|
1004
|
+
* attachments (scoped per-conversation), set `lastMessageAt`, seed attention,
|
|
1005
|
+
* and carry the parent's per-conversation memory state.
|
|
1006
|
+
*
|
|
1007
|
+
* Assumes the fork's message ROWS already exist — copied either in-process
|
|
1008
|
+
* (synchronous fork) or via the off-event-loop subprocess (retrospective
|
|
1009
|
+
* fork). Single source of truth shared by `forkConversation` and
|
|
1010
|
+
* `forkConversationForRetrospective`; must run inside a transaction on the
|
|
1011
|
+
* main connection.
|
|
1012
|
+
*/
|
|
1013
|
+
function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
1014
|
+
const {
|
|
1015
|
+
fork,
|
|
1016
|
+
sourceConversationId,
|
|
1017
|
+
messagesToCopy,
|
|
1018
|
+
forkedMessageIds,
|
|
1019
|
+
latestForkedAssistant,
|
|
1020
|
+
isFullHistoryFork,
|
|
1021
|
+
inheritedCompactedMessageCount,
|
|
1022
|
+
diskSyncQueue,
|
|
1023
|
+
} = args;
|
|
1024
|
+
const db = getDb();
|
|
1025
|
+
|
|
1026
|
+
const attachmentIdMap = new Map<string, string>();
|
|
1027
|
+
for (const message of messagesToCopy) {
|
|
1028
|
+
const forkedMessageId = forkedMessageIds.get(message.id);
|
|
1029
|
+
if (!forkedMessageId) continue;
|
|
1030
|
+
|
|
1031
|
+
const attachmentLinks = db
|
|
1032
|
+
.select({
|
|
1033
|
+
attachmentId: messageAttachments.attachmentId,
|
|
1034
|
+
position: messageAttachments.position,
|
|
1035
|
+
})
|
|
1036
|
+
.from(messageAttachments)
|
|
1037
|
+
.where(eq(messageAttachments.messageId, message.id))
|
|
1038
|
+
.orderBy(messageAttachments.position)
|
|
1039
|
+
.all();
|
|
1040
|
+
const uncachedAttachmentLinks = attachmentLinks.filter(
|
|
1041
|
+
(link) => !attachmentIdMap.has(link.attachmentId),
|
|
1042
|
+
);
|
|
1043
|
+
const stagingMessageId = uncachedAttachmentLinks.length > 0 ? uuid() : null;
|
|
1044
|
+
|
|
1045
|
+
if (stagingMessageId) {
|
|
1046
|
+
db.insert(messages)
|
|
1047
|
+
.values({
|
|
1048
|
+
id: stagingMessageId,
|
|
1049
|
+
conversationId: fork.id,
|
|
1050
|
+
role: message.role,
|
|
1051
|
+
content: "",
|
|
1052
|
+
createdAt: message.createdAt,
|
|
1053
|
+
metadata: null,
|
|
1054
|
+
})
|
|
1055
|
+
.run();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
for (const link of attachmentLinks) {
|
|
1059
|
+
const cachedAttachmentId = attachmentIdMap.get(link.attachmentId);
|
|
1060
|
+
if (cachedAttachmentId) {
|
|
1061
|
+
db.insert(messageAttachments)
|
|
1062
|
+
.values({
|
|
1063
|
+
id: uuid(),
|
|
1064
|
+
messageId: forkedMessageId,
|
|
1065
|
+
attachmentId: cachedAttachmentId,
|
|
1066
|
+
position: link.position,
|
|
1067
|
+
createdAt: Date.now(),
|
|
1068
|
+
})
|
|
1069
|
+
.run();
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const scopedAttachmentId = linkAttachmentToMessage(
|
|
1074
|
+
stagingMessageId ?? forkedMessageId,
|
|
1075
|
+
link.attachmentId,
|
|
1076
|
+
link.position,
|
|
1077
|
+
);
|
|
1078
|
+
attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (stagingMessageId) {
|
|
1082
|
+
relinkAttachments([stagingMessageId], forkedMessageId);
|
|
1083
|
+
db.delete(messages).where(eq(messages.id, stagingMessageId)).run();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
diskSyncQueue?.push({
|
|
1087
|
+
conversationId: fork.id,
|
|
1088
|
+
messageId: forkedMessageId,
|
|
1089
|
+
createdAt: fork.createdAt,
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Set lastMessageAt to the max createdAt of copied messages so the
|
|
1094
|
+
// forked conversation sorts correctly by message recency.
|
|
1095
|
+
const lastCopiedMessage = messagesToCopy.at(-1);
|
|
1096
|
+
if (lastCopiedMessage) {
|
|
1097
|
+
db.update(conversations)
|
|
1098
|
+
.set({ lastMessageAt: lastCopiedMessage.createdAt })
|
|
1099
|
+
.where(eq(conversations.id, fork.id))
|
|
1100
|
+
.run();
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
seedForkedConversationAttention({
|
|
1104
|
+
conversationId: fork.id,
|
|
1105
|
+
latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
|
|
1106
|
+
latestAssistantMessageAt: latestForkedAssistant?.messageAt ?? null,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// Carry the parent's per-conversation memory state into the child (activation
|
|
1110
|
+
// and injection logs, graph state, retrospective state). Runs synchronously
|
|
1111
|
+
// inside the fork transaction; a no-op when memory is absent or disabled.
|
|
1112
|
+
getMemoryPersistenceHooks().onConversationForked({
|
|
1113
|
+
db,
|
|
1114
|
+
sourceConversationId,
|
|
1115
|
+
forkId: fork.id,
|
|
1116
|
+
isFullHistoryFork,
|
|
1117
|
+
messagesToCopy,
|
|
1118
|
+
forkedMessageIds,
|
|
1119
|
+
inheritedCompactedMessageCount,
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
// Carry the source's compaction events that predate the fork boundary so the
|
|
1123
|
+
// fork owns a correct ledger for its own future forks/compactions.
|
|
1124
|
+
forkCompactionLedger(
|
|
1125
|
+
db,
|
|
1126
|
+
sourceConversationId,
|
|
1127
|
+
fork.id,
|
|
1128
|
+
messagesToCopy.at(-1)?.createdAt ?? null,
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve the fork id + timestamp of the LAST assistant message among the
|
|
1134
|
+
* copied rows. The synchronous copy loop tracks this inline; the off-loop
|
|
1135
|
+
* subprocess copy does not, so the async fork derives it from the id map.
|
|
1136
|
+
*/
|
|
1137
|
+
function latestForkedAssistantFrom(
|
|
1138
|
+
messagesToCopy: MessageRow[],
|
|
1139
|
+
forkedMessageIds: Map<string, string>,
|
|
1140
|
+
): { messageId: string; messageAt: number } | null {
|
|
1141
|
+
for (let i = messagesToCopy.length - 1; i >= 0; i--) {
|
|
1142
|
+
const message = messagesToCopy[i]!;
|
|
1143
|
+
if (message.role !== "assistant") continue;
|
|
1144
|
+
const forkedMessageId = forkedMessageIds.get(message.id);
|
|
1145
|
+
if (forkedMessageId) {
|
|
1146
|
+
return { messageId: forkedMessageId, messageAt: message.createdAt };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Async variant of {@link forkConversation} for the memory-retrospective job,
|
|
1154
|
+
* which forks the entire source conversation into a throwaway background
|
|
1155
|
+
* conversation on a hot path that must not stall the daemon.
|
|
1156
|
+
*
|
|
1157
|
+
* The dominant cost — copying every source message row — runs OFF the event
|
|
1158
|
+
* loop in a `sqlite3` subprocess (see {@link copyForkMessagesViaSubprocess}),
|
|
1159
|
+
* so `/healthz` and gateway IPC stay responsive during the copy. The cheap
|
|
1160
|
+
* tail (conversation row, attachment relink, memory-state seeding) runs
|
|
1161
|
+
* in-process and reuses {@link populateForkContentsInProcess}, the same helper
|
|
1162
|
+
* the synchronous fork uses, so the two paths cannot drift on that logic. The
|
|
1163
|
+
* boundary/compaction computation mirrors {@link forkConversation} and is
|
|
1164
|
+
* pinned by a parity test.
|
|
1165
|
+
*
|
|
1166
|
+
* The disk-view projection (`syncMessageToDisk`) is intentionally skipped: the
|
|
1167
|
+
* fork is GC'd after the retrospective pass and never browsed, and the agent
|
|
1168
|
+
* reads it from the database, not the on-disk JSONL.
|
|
1169
|
+
*
|
|
1170
|
+
* Atomicity spans two connections (the in-process row/tail and the subprocess
|
|
1171
|
+
* copy), so a mid-flight failure can leave a partial fork. The partial is
|
|
1172
|
+
* deleted best-effort on error; a crash between phases is reclaimed by the
|
|
1173
|
+
* worker's startup orphan sweep. Callers only ever observe a fully-built fork
|
|
1174
|
+
* because the returned promise resolves after every phase commits.
|
|
1175
|
+
*/
|
|
1176
|
+
export async function forkConversationForRetrospective(params: {
|
|
1177
|
+
conversationId: string;
|
|
1178
|
+
throughMessageId?: string;
|
|
1179
|
+
source?: string;
|
|
1180
|
+
title?: string;
|
|
1181
|
+
conversationType?: ConversationCreateType;
|
|
1182
|
+
groupId?: string;
|
|
1183
|
+
}): Promise<ConversationRow> {
|
|
1184
|
+
const { conversationId, throughMessageId } = params;
|
|
1185
|
+
const db = getDb();
|
|
1186
|
+
const sourceConversation = getConversation(conversationId);
|
|
1187
|
+
if (!sourceConversation) {
|
|
1188
|
+
throw new UserError(`Conversation ${conversationId} not found`);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const sourceMessages = getMessages(conversationId);
|
|
1192
|
+
if (throughMessageId != null) {
|
|
1193
|
+
// Re-sort on `(createdAt, id)` so the cutoff slice agrees with the cursor
|
|
1194
|
+
// the caller chose it from — see the matching note in `forkConversation`.
|
|
1195
|
+
sourceMessages.sort(
|
|
1196
|
+
(a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id),
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
if (sourceMessages.length === 0) {
|
|
1200
|
+
throw new UserError(
|
|
1201
|
+
`Conversation ${conversationId} has no persisted messages to fork`,
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const initialBoundaryIndex =
|
|
1206
|
+
throughMessageId == null
|
|
1207
|
+
? sourceMessages.length - 1
|
|
1208
|
+
: sourceMessages.findIndex((message) => message.id === throughMessageId);
|
|
1209
|
+
if (throughMessageId != null && initialBoundaryIndex === -1) {
|
|
1210
|
+
throw new UserError(
|
|
1211
|
+
`Message ${throughMessageId} does not belong to conversation ${conversationId}`,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const copyBoundaryIndex = findDisplayTurnEndIndex(
|
|
1216
|
+
sourceMessages,
|
|
1217
|
+
initialBoundaryIndex,
|
|
1218
|
+
);
|
|
1219
|
+
const messagesToCopy =
|
|
1220
|
+
copyBoundaryIndex >= 0
|
|
1221
|
+
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
1222
|
+
: ([] as MessageRow[]);
|
|
1223
|
+
|
|
1224
|
+
const sourceHistoryStrippedAt = sourceConversation.historyStrippedAt ?? null;
|
|
1225
|
+
const boundaryMessageCreatedAt = messagesToCopy.at(-1)?.createdAt ?? null;
|
|
1226
|
+
const inheritsHistoryStrippedAt =
|
|
1227
|
+
sourceHistoryStrippedAt != null &&
|
|
1228
|
+
boundaryMessageCreatedAt != null &&
|
|
1229
|
+
boundaryMessageCreatedAt >= sourceHistoryStrippedAt;
|
|
1230
|
+
// Inherit the most recent compaction whose event time is at-or-before the
|
|
1231
|
+
// forked-from message (see `forkConversation`).
|
|
1232
|
+
const inheritedCompaction = getLatestCompactionEventAtOrBefore(
|
|
1233
|
+
sourceConversation.id,
|
|
1234
|
+
boundaryMessageCreatedAt,
|
|
1235
|
+
);
|
|
1236
|
+
// Carry the Slack watermark only when inheriting the latest compaction
|
|
1237
|
+
// (see `forkConversation`).
|
|
1238
|
+
const inheritsLatestCompaction =
|
|
1239
|
+
inheritedCompaction != null &&
|
|
1240
|
+
inheritedCompaction.compactedAt === sourceConversation.contextCompactedAt;
|
|
1241
|
+
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
1242
|
+
const forkTitle =
|
|
1243
|
+
params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
1244
|
+
const parentGroupId = getConversationGroupId(conversationId);
|
|
1245
|
+
|
|
1246
|
+
// Pre-generate the id map in JS so the same map drives the off-loop copy and
|
|
1247
|
+
// the in-process attachment relink that follows.
|
|
1248
|
+
const idPairs: ForkIdPair[] = messagesToCopy.map((message) => ({
|
|
1249
|
+
oldId: message.id,
|
|
1250
|
+
newId: uuid(),
|
|
1251
|
+
}));
|
|
1252
|
+
const forkedMessageIds = new Map<string, string>(
|
|
1253
|
+
idPairs.map((pair) => [pair.oldId, pair.newId]),
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
// Phase 1 (in-process, tiny): create the fork conversation row + lineage so
|
|
1257
|
+
// the subprocess connection sees it before inserting messages. The whole
|
|
1258
|
+
// transaction is the retry unit — it rolls back atomically on contention, so
|
|
1259
|
+
// re-running it (with a fresh conversation id) is safe.
|
|
1260
|
+
const fork = await withSqliteRetry(
|
|
1261
|
+
() =>
|
|
1262
|
+
db.transaction(() => {
|
|
1263
|
+
const fc = createConversation({
|
|
1264
|
+
title: forkTitle,
|
|
1265
|
+
conversationType: params.conversationType ?? "standard",
|
|
1266
|
+
groupId: params.groupId ?? parentGroupId ?? "system:all",
|
|
1267
|
+
...(params.source != null ? { source: params.source } : {}),
|
|
1268
|
+
});
|
|
1269
|
+
db.update(conversations)
|
|
1270
|
+
.set({
|
|
1271
|
+
forkParentConversationId: sourceConversation.id,
|
|
1272
|
+
forkParentMessageId,
|
|
1273
|
+
contextSummary: inheritedCompaction?.summary ?? null,
|
|
1274
|
+
contextCompactedMessageCount:
|
|
1275
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1276
|
+
contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
|
|
1277
|
+
slackContextCompactionWatermarkTs: inheritsLatestCompaction
|
|
1278
|
+
? sourceConversation.slackContextCompactionWatermarkTs
|
|
1279
|
+
: null,
|
|
1280
|
+
slackContextCompactionWatermarkAt: inheritsLatestCompaction
|
|
1281
|
+
? sourceConversation.slackContextCompactionWatermarkAt
|
|
1282
|
+
: null,
|
|
1283
|
+
historyStrippedAt: inheritsHistoryStrippedAt
|
|
1284
|
+
? sourceHistoryStrippedAt
|
|
1285
|
+
: null,
|
|
1286
|
+
inferenceProfile: sourceConversation.inferenceProfile,
|
|
1287
|
+
})
|
|
1288
|
+
.where(eq(conversations.id, fc.id))
|
|
1289
|
+
.run();
|
|
1290
|
+
return fc;
|
|
1291
|
+
}),
|
|
1292
|
+
{ op: "forkConversationForRetrospective.create" },
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
// Phase 2 (off the event loop): copy the message rows in a sqlite3
|
|
1297
|
+
// subprocess so the daemon stays responsive during the heavy copy.
|
|
1298
|
+
const copy = await copyForkMessagesViaSubprocess({
|
|
1299
|
+
forkConversationId: fork.id,
|
|
1300
|
+
idPairs,
|
|
1301
|
+
});
|
|
1302
|
+
if (!copy.ok) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`fork message copy failed (${copy.backend}): ${copy.error ?? "unknown"}`,
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Phase 3 (in-process): attachments + memory-state seeding, reusing the
|
|
1309
|
+
// same helper as the synchronous fork. Disk-view projection is skipped.
|
|
1310
|
+
const latestForkedAssistant = latestForkedAssistantFrom(
|
|
1311
|
+
messagesToCopy,
|
|
1312
|
+
forkedMessageIds,
|
|
1313
|
+
);
|
|
1314
|
+
db.transaction(() => {
|
|
1315
|
+
populateForkContentsInProcess({
|
|
1316
|
+
fork,
|
|
1317
|
+
sourceConversationId: sourceConversation.id,
|
|
1318
|
+
messagesToCopy,
|
|
1319
|
+
forkedMessageIds,
|
|
1320
|
+
latestForkedAssistant,
|
|
1321
|
+
isFullHistoryFork: copyBoundaryIndex === sourceMessages.length - 1,
|
|
1322
|
+
inheritedCompactedMessageCount:
|
|
1323
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
const persistedFork = getConversation(fork.id);
|
|
1328
|
+
if (!persistedFork) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`Failed to load forked conversation ${fork.id} after creation`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return persistedFork;
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
try {
|
|
1336
|
+
deleteConversation(fork.id);
|
|
1337
|
+
} catch {
|
|
1338
|
+
// Best-effort cleanup; the worker's startup orphan sweep is the backstop.
|
|
1339
|
+
}
|
|
1340
|
+
throw err;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Delete a conversation and all its messages, cleaning up orphaned memory
|
|
1346
|
+
* artifacts (embeddings). Returns segment IDs so callers can clean up
|
|
1347
|
+
* the corresponding Qdrant vector entries.
|
|
1348
|
+
*/
|
|
1349
|
+
export function deleteConversation(id: string): DeletedMemoryIds {
|
|
1350
|
+
const db = getDb();
|
|
1351
|
+
const result: DeletedMemoryIds = {
|
|
1352
|
+
segmentIds: [],
|
|
1353
|
+
deletedSummaryIds: [],
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// Capture createdAt before the transaction deletes the row — needed to
|
|
1357
|
+
// resolve the conversation's disk-view directory path after deletion.
|
|
1358
|
+
const convBeforeDelete = getConversation(id);
|
|
1359
|
+
const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
|
|
1360
|
+
|
|
1361
|
+
// llm_request_logs lives in the dedicated logs connection, so it is deleted
|
|
1362
|
+
// there — separately from the main-DB transaction below.
|
|
1363
|
+
logsDb()
|
|
1364
|
+
.delete(llmRequestLogs)
|
|
1365
|
+
.where(eq(llmRequestLogs.conversationId, id))
|
|
1366
|
+
.run();
|
|
1367
|
+
|
|
1368
|
+
db.transaction((tx) => {
|
|
1369
|
+
// Collect all message IDs for this conversation.
|
|
1370
|
+
const messageRows = tx
|
|
1371
|
+
.select({ id: messages.id })
|
|
1372
|
+
.from(messages)
|
|
1373
|
+
.where(eq(messages.conversationId, id))
|
|
1374
|
+
.all();
|
|
1375
|
+
const messageIds = messageRows.map((r) => r.id);
|
|
1376
|
+
|
|
1377
|
+
if (messageIds.length > 0) {
|
|
1378
|
+
// Collect memory segment IDs linked to these messages before cascade.
|
|
1379
|
+
const linkedSegments = tx
|
|
1380
|
+
.select({ id: memorySegments.id })
|
|
1381
|
+
.from(memorySegments)
|
|
1382
|
+
.where(inArray(memorySegments.messageId, messageIds))
|
|
1383
|
+
.all();
|
|
1384
|
+
result.segmentIds = linkedSegments.map((r) => r.id);
|
|
1385
|
+
|
|
1386
|
+
// Delete non-cascading tables first.
|
|
1387
|
+
tx.delete(toolInvocations)
|
|
1388
|
+
.where(eq(toolInvocations.conversationId, id))
|
|
1389
|
+
.run();
|
|
1390
|
+
tx.delete(skillLoadedEvents)
|
|
1391
|
+
.where(eq(skillLoadedEvents.conversationId, id))
|
|
1392
|
+
.run();
|
|
1393
|
+
// Cascade deletes memory_segments, message_attachments.
|
|
1394
|
+
tx.delete(messages).where(eq(messages.conversationId, id)).run();
|
|
1395
|
+
|
|
1396
|
+
// Clean up segment embeddings.
|
|
1397
|
+
if (result.segmentIds.length > 0) {
|
|
1398
|
+
tx.delete(memoryEmbeddings)
|
|
1399
|
+
.where(
|
|
1400
|
+
and(
|
|
1401
|
+
eq(memoryEmbeddings.targetType, "segment"),
|
|
1402
|
+
inArray(memoryEmbeddings.targetId, result.segmentIds),
|
|
1403
|
+
),
|
|
1404
|
+
)
|
|
1405
|
+
.run();
|
|
1406
|
+
}
|
|
1407
|
+
} else {
|
|
1408
|
+
// No messages — just clean up non-message tables.
|
|
1409
|
+
tx.delete(toolInvocations)
|
|
1410
|
+
.where(eq(toolInvocations.conversationId, id))
|
|
1411
|
+
.run();
|
|
1412
|
+
tx.delete(skillLoadedEvents)
|
|
1413
|
+
.where(eq(skillLoadedEvents.conversationId, id))
|
|
1414
|
+
.run();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
tx.delete(conversations).where(eq(conversations.id, id)).run();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Remove the conversation's disk-view directory after the DB transaction
|
|
1421
|
+
if (createdAtForDiskCleanup != null) {
|
|
1422
|
+
removeConversationDir(id, createdAtForDiskCleanup);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return result;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Delete a conversation like {@link deleteConversation}, but move the bulk
|
|
1430
|
+
* row deletes off the event loop in lock-friendly batches (see
|
|
1431
|
+
* {@link deleteConversationRowsInBatches}). The synchronous variant runs each
|
|
1432
|
+
* `DELETE` as one implicit transaction that holds the SQLite write lock for its
|
|
1433
|
+
* full duration — fine for an interactive conversation, but a conversation that
|
|
1434
|
+
* carries a full copy of a source's message history (e.g. a memory-retrospective
|
|
1435
|
+
* GC target) has huge `messages` and `llm_request_logs` tables, so on a large
|
|
1436
|
+
* database those single deletes peg the event loop and starve live user turns.
|
|
1437
|
+
* This variant sends the two heavy tables (`messages` on the main DB,
|
|
1438
|
+
* `llm_request_logs` on the logs DB) in chunks that each release the lock,
|
|
1439
|
+
* while the cheap bookkeeping (non-cascading tables, segment embeddings, the
|
|
1440
|
+
* conversation row) stays in-process.
|
|
1441
|
+
*
|
|
1442
|
+
* The `messages` batch enables `PRAGMA foreign_keys=ON` so the cascade
|
|
1443
|
+
* (memory_segments, message_attachments, …) fires just as it does on the daemon
|
|
1444
|
+
* connection. Segment ids are captured up front (before the cascade removes
|
|
1445
|
+
* them) so the caller can still clean up the corresponding Qdrant vectors. A
|
|
1446
|
+
* subprocess failure throws — any rows already deleted are gone, the
|
|
1447
|
+
* conversation row remains, and the worker's orphan sweep is the backstop, so
|
|
1448
|
+
* callers should treat this as best-effort like the synchronous variant.
|
|
1449
|
+
*/
|
|
1450
|
+
export async function deleteConversationGently(
|
|
1451
|
+
id: string,
|
|
1452
|
+
): Promise<DeletedMemoryIds> {
|
|
1453
|
+
const db = getDb();
|
|
1454
|
+
const result: DeletedMemoryIds = {
|
|
1455
|
+
segmentIds: [],
|
|
1456
|
+
deletedSummaryIds: [],
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// Capture createdAt before deletion — needed to resolve the conversation's
|
|
1460
|
+
// disk-view directory path afterwards.
|
|
1461
|
+
const convBeforeDelete = getConversation(id);
|
|
1462
|
+
const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
|
|
1463
|
+
|
|
1464
|
+
// Collect the linked memory segment ids before the message cascade removes
|
|
1465
|
+
// them, so the caller can clean up the matching Qdrant vector entries.
|
|
1466
|
+
result.segmentIds = db
|
|
1467
|
+
.select({ id: memorySegments.id })
|
|
1468
|
+
.from(memorySegments)
|
|
1469
|
+
.where(eq(memorySegments.conversationId, id))
|
|
1470
|
+
.all()
|
|
1471
|
+
.map((r) => r.id);
|
|
1472
|
+
|
|
1473
|
+
// llm_request_logs lives in the dedicated logs connection, and each row is
|
|
1474
|
+
// bulky, so drain it off the event loop in batches against the logs DB file.
|
|
1475
|
+
const logsDel = await deleteConversationRowsInBatches({
|
|
1476
|
+
conversationId: id,
|
|
1477
|
+
table: "llm_request_logs",
|
|
1478
|
+
dbPath: getLogsDbPath(),
|
|
1479
|
+
});
|
|
1480
|
+
if (!logsDel.ok) {
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
`gentle conversation delete failed (llm_request_logs, ${logsDel.backend}): ${logsDel.error ?? "unknown"}`,
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Bulk message delete off the event loop, in lock-friendly batches. Cascades
|
|
1487
|
+
// to memory_segments, message_attachments, bookmarks, channel_inbound_events.
|
|
1488
|
+
const del = await deleteConversationRowsInBatches({
|
|
1489
|
+
conversationId: id,
|
|
1490
|
+
table: "messages",
|
|
1491
|
+
enableForeignKeys: true,
|
|
1492
|
+
});
|
|
1493
|
+
if (!del.ok) {
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
`gentle conversation delete failed (messages, ${del.backend}): ${del.error ?? "unknown"}`,
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Remaining cleanup is cheap (bounded, non-message tables) so it stays a
|
|
1500
|
+
// single in-process transaction.
|
|
1501
|
+
db.transaction((tx) => {
|
|
1502
|
+
tx.delete(toolInvocations)
|
|
1503
|
+
.where(eq(toolInvocations.conversationId, id))
|
|
1504
|
+
.run();
|
|
1505
|
+
tx.delete(skillLoadedEvents)
|
|
1506
|
+
.where(eq(skillLoadedEvents.conversationId, id))
|
|
1507
|
+
.run();
|
|
1508
|
+
|
|
1509
|
+
// Clean up segment embeddings (not FK-linked to segments, so the message
|
|
1510
|
+
// cascade above did not remove them).
|
|
1511
|
+
if (result.segmentIds.length > 0) {
|
|
1512
|
+
tx.delete(memoryEmbeddings)
|
|
1513
|
+
.where(
|
|
1514
|
+
and(
|
|
1515
|
+
eq(memoryEmbeddings.targetType, "segment"),
|
|
1516
|
+
inArray(memoryEmbeddings.targetId, result.segmentIds),
|
|
1517
|
+
),
|
|
1518
|
+
)
|
|
1519
|
+
.run();
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Conversation row deletion cascades to remaining dependent tables.
|
|
1523
|
+
tx.delete(conversations).where(eq(conversations.id, id)).run();
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
// Remove the conversation's disk-view directory after the DB transaction.
|
|
1527
|
+
if (createdAtForDiskCleanup != null) {
|
|
1528
|
+
removeConversationDir(id, createdAtForDiskCleanup);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
return result;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
/**
|
|
1535
|
+
* Wipe a conversation and revert all memory changes it caused.
|
|
1536
|
+
*
|
|
1537
|
+
* Extends `deleteConversation` with:
|
|
1538
|
+
* - Cancelling pending memory jobs before deletion
|
|
1539
|
+
* - Deleting conversation-scoped memory summaries and their embeddings
|
|
1540
|
+
*/
|
|
1541
|
+
export function wipeConversation(id: string): WipeConversationResult {
|
|
1542
|
+
const db = getDb();
|
|
1543
|
+
const deletedSummaryIds: string[] = [];
|
|
1544
|
+
|
|
1545
|
+
// Step A — Cancel pending memory jobs (before deleting messages, since
|
|
1546
|
+
// the cancellation queries join on `messages`). Cleanup runs even while the
|
|
1547
|
+
// memory plugin is disabled; returns 0 when memory is not present.
|
|
1548
|
+
const cancelledJobCount = getMemoryPersistenceHooks().onConversationWiped(id);
|
|
1549
|
+
|
|
1550
|
+
// Step C — Delete conversation-scoped memory summaries and their embeddings.
|
|
1551
|
+
const summaryRows = db
|
|
1552
|
+
.select({ id: memorySummaries.id })
|
|
1553
|
+
.from(memorySummaries)
|
|
1554
|
+
.where(
|
|
1555
|
+
and(
|
|
1556
|
+
eq(memorySummaries.scope, "conversation"),
|
|
1557
|
+
eq(memorySummaries.scopeKey, id),
|
|
1558
|
+
),
|
|
1559
|
+
)
|
|
1560
|
+
.all();
|
|
1561
|
+
const summaryIds = summaryRows.map((r) => r.id);
|
|
1562
|
+
if (summaryIds.length > 0) {
|
|
1563
|
+
db.delete(memoryEmbeddings)
|
|
1564
|
+
.where(
|
|
1565
|
+
and(
|
|
1566
|
+
eq(memoryEmbeddings.targetType, "summary"),
|
|
1567
|
+
inArray(memoryEmbeddings.targetId, summaryIds),
|
|
1568
|
+
),
|
|
1569
|
+
)
|
|
1570
|
+
.run();
|
|
1571
|
+
db.delete(memorySummaries)
|
|
1572
|
+
.where(inArray(memorySummaries.id, summaryIds))
|
|
1573
|
+
.run();
|
|
1574
|
+
}
|
|
1575
|
+
deletedSummaryIds.push(...summaryIds);
|
|
1576
|
+
|
|
1577
|
+
// Step D — Delegate to deleteConversation which handles messages (cascade
|
|
1578
|
+
// segments, attachments), llmRequestLogs, toolInvocations,
|
|
1579
|
+
// skillLoadedEvents, embeddings, and the conversation row.
|
|
1580
|
+
const deletedMemoryIds = deleteConversation(id);
|
|
1581
|
+
|
|
1582
|
+
// Step E — Return the combined result.
|
|
1583
|
+
return {
|
|
1584
|
+
...deletedMemoryIds,
|
|
1585
|
+
deletedSummaryIds: [
|
|
1586
|
+
...deletedSummaryIds,
|
|
1587
|
+
...deletedMemoryIds.deletedSummaryIds,
|
|
1588
|
+
],
|
|
1589
|
+
cancelledJobCount,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** Options for {@link addMessage}. Only `skipIndexing` and `clientMessageId`
|
|
1594
|
+
* have defaults; `metadata` is genuinely optional. */
|
|
1595
|
+
export interface AddMessageOptions {
|
|
1596
|
+
metadata?: Record<string, unknown>;
|
|
1597
|
+
skipIndexing?: boolean;
|
|
1598
|
+
/** Client-generated nonce for idempotent inserts. When provided,
|
|
1599
|
+
* duplicate inserts for the same `(conversationId, clientMessageId)`
|
|
1600
|
+
* pair are silently skipped. */
|
|
1601
|
+
clientMessageId?: string;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Persist a message and run post-insert side effects (memory indexing,
|
|
1606
|
+
* attention projection). Delegates the core insert + retry logic to
|
|
1607
|
+
* {@link insertMessageCore}.
|
|
1608
|
+
*/
|
|
1609
|
+
export async function addMessage(
|
|
1610
|
+
conversationId: string,
|
|
1611
|
+
role: MessageRole,
|
|
1612
|
+
content: string,
|
|
1613
|
+
options?: AddMessageOptions,
|
|
1614
|
+
) {
|
|
1615
|
+
const { metadata, skipIndexing, clientMessageId } = options ?? {};
|
|
1616
|
+
const inserted = await insertMessageCore({
|
|
1617
|
+
conversationId,
|
|
1618
|
+
role,
|
|
1619
|
+
content,
|
|
1620
|
+
metadata,
|
|
1621
|
+
clientMessageId,
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
if (inserted.deduplicated) {
|
|
1625
|
+
return inserted;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const message = inserted;
|
|
1629
|
+
|
|
1630
|
+
if (!skipIndexing) {
|
|
1631
|
+
try {
|
|
1632
|
+
const parsed = metadata
|
|
1633
|
+
? messageMetadataSchema.safeParse(metadata)
|
|
1634
|
+
: null;
|
|
1635
|
+
const provenanceTrustClass = parsed?.success
|
|
1636
|
+
? parsed.data.provenanceTrustClass
|
|
1637
|
+
: undefined;
|
|
1638
|
+
const automated = parsed?.success ? parsed.data.automated : undefined;
|
|
1639
|
+
await getMemoryPersistenceHooks().onMessagePersisted({
|
|
1640
|
+
messageId: message.id,
|
|
1641
|
+
conversationId: message.conversationId,
|
|
1642
|
+
role: message.role,
|
|
1643
|
+
content: message.content,
|
|
1644
|
+
createdAt: message.createdAt,
|
|
1645
|
+
provenanceTrustClass,
|
|
1646
|
+
automated,
|
|
1647
|
+
});
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
log.warn(
|
|
1650
|
+
{ err, conversationId, messageId: message.id },
|
|
1651
|
+
"Failed to index message for memory",
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (role === "assistant") {
|
|
1657
|
+
try {
|
|
1658
|
+
const attentionStateChanged = projectAssistantMessage({
|
|
1659
|
+
conversationId,
|
|
1660
|
+
messageId: message.id,
|
|
1661
|
+
messageAt: message.createdAt,
|
|
1662
|
+
});
|
|
1663
|
+
if (attentionStateChanged) {
|
|
1664
|
+
void publishSyncInvalidation([
|
|
1665
|
+
conversationMetadataSyncTag(conversationId),
|
|
1666
|
+
]);
|
|
1667
|
+
}
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
log.warn(
|
|
1670
|
+
{ err, conversationId, messageId: message.id },
|
|
1671
|
+
"Failed to project assistant message for attention tracking",
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return message;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
export function getMessages(conversationId: string): MessageRow[] {
|
|
1680
|
+
const db = getDb();
|
|
1681
|
+
return db
|
|
1682
|
+
.select()
|
|
1683
|
+
.from(messages)
|
|
1684
|
+
.where(eq(messages.conversationId, conversationId))
|
|
1685
|
+
.orderBy(asc(messages.createdAt))
|
|
1686
|
+
.all()
|
|
1687
|
+
.map(parseMessage);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/**
|
|
1691
|
+
* Return raw `metadata` strings for messages whose metadata looks like it may
|
|
1692
|
+
* contain Slack metadata, capped at `limit` and skipping the first `offset`
|
|
1693
|
+
* matches. Pushes `LIKE` + `LIMIT`/`OFFSET` into SQL so warm Slack DM
|
|
1694
|
+
* conversations don't require a full-table scan on the webhook critical path.
|
|
1695
|
+
* The substring match is an indexable prefilter only — callers must parse and
|
|
1696
|
+
* validate each returned string against the Slack metadata schema, because a
|
|
1697
|
+
* malformed row (partial write, legacy format, unrelated key accidentally
|
|
1698
|
+
* containing the literal) can still slip through the substring match. Callers
|
|
1699
|
+
* that need a fixed number of *valid* rows should iterate with increasing
|
|
1700
|
+
* offsets until the target is reached (capped at a reasonable maximum to bound
|
|
1701
|
+
* scan cost).
|
|
1702
|
+
*/
|
|
1703
|
+
export function selectSlackMetaCandidateMetadata(
|
|
1704
|
+
conversationId: string,
|
|
1705
|
+
limit: number,
|
|
1706
|
+
offset = 0,
|
|
1707
|
+
opts?: { includeFlatLegacy?: boolean },
|
|
1708
|
+
): string[] {
|
|
1709
|
+
const db = getDb();
|
|
1710
|
+
const rows = db
|
|
1711
|
+
.select({ metadata: messages.metadata })
|
|
1712
|
+
.from(messages)
|
|
1713
|
+
.where(
|
|
1714
|
+
and(
|
|
1715
|
+
eq(messages.conversationId, conversationId),
|
|
1716
|
+
opts?.includeFlatLegacy
|
|
1717
|
+
? or(
|
|
1718
|
+
like(messages.metadata, '%"slackMeta"%'),
|
|
1719
|
+
like(messages.metadata, '%"source":"slack"%'),
|
|
1720
|
+
)
|
|
1721
|
+
: like(messages.metadata, '%"slackMeta"%'),
|
|
1722
|
+
),
|
|
1723
|
+
)
|
|
1724
|
+
.orderBy(asc(messages.createdAt))
|
|
1725
|
+
.limit(limit)
|
|
1726
|
+
.offset(offset)
|
|
1727
|
+
.all();
|
|
1728
|
+
const out: string[] = [];
|
|
1729
|
+
for (const r of rows) {
|
|
1730
|
+
if (typeof r.metadata === "string" && r.metadata.length > 0) {
|
|
1731
|
+
out.push(r.metadata);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return out;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/**
|
|
1738
|
+
* Count messages in a conversation that were created strictly after the
|
|
1739
|
+
* `afterMessageId` reference message. If `afterMessageId` is `null` or empty,
|
|
1740
|
+
* counts all messages in the conversation. If the referenced message no
|
|
1741
|
+
* longer exists (e.g. deleted by a separate flow), returns 0 — callers
|
|
1742
|
+
* decide how to react to a vanished reference, and the conservative answer
|
|
1743
|
+
* here is "no new work."
|
|
1744
|
+
*
|
|
1745
|
+
* Used by the memory-retrospective trigger check to decide whether to fire
|
|
1746
|
+
* the message-count trigger without loading message bodies.
|
|
1747
|
+
*/
|
|
1748
|
+
export function countMessagesAfter(
|
|
1749
|
+
conversationId: string,
|
|
1750
|
+
afterMessageId: string | null,
|
|
1751
|
+
): number {
|
|
1752
|
+
const db = getDb();
|
|
1753
|
+
if (afterMessageId === null || afterMessageId === "") {
|
|
1754
|
+
const row = db
|
|
1755
|
+
.select({ c: count() })
|
|
1756
|
+
.from(messages)
|
|
1757
|
+
.where(eq(messages.conversationId, conversationId))
|
|
1758
|
+
.get();
|
|
1759
|
+
return row?.c ?? 0;
|
|
1760
|
+
}
|
|
1761
|
+
const ref = db
|
|
1762
|
+
.select({ createdAt: messages.createdAt })
|
|
1763
|
+
.from(messages)
|
|
1764
|
+
.where(eq(messages.id, afterMessageId))
|
|
1765
|
+
.get();
|
|
1766
|
+
if (!ref) return 0;
|
|
1767
|
+
// Tie-breaker on `messages.id` so rows that share a millisecond timestamp
|
|
1768
|
+
// with the reference are not permanently skipped. Mirrors the
|
|
1769
|
+
// `(createdAt, id)` cursor pattern used by the backfill job-handler and
|
|
1770
|
+
// turn-events-store.
|
|
1771
|
+
const row = db
|
|
1772
|
+
.select({ c: count() })
|
|
1773
|
+
.from(messages)
|
|
1774
|
+
.where(
|
|
1775
|
+
and(
|
|
1776
|
+
eq(messages.conversationId, conversationId),
|
|
1777
|
+
or(
|
|
1778
|
+
gt(messages.createdAt, ref.createdAt),
|
|
1779
|
+
and(
|
|
1780
|
+
eq(messages.createdAt, ref.createdAt),
|
|
1781
|
+
gt(messages.id, afterMessageId),
|
|
1782
|
+
),
|
|
1783
|
+
),
|
|
1784
|
+
),
|
|
1785
|
+
)
|
|
1786
|
+
.get();
|
|
1787
|
+
return row?.c ?? 0;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/**
|
|
1791
|
+
* Return messages in a conversation created strictly after the
|
|
1792
|
+
* `afterMessageId` reference. If the reference is `null`/empty, returns all
|
|
1793
|
+
* messages. If the reference doesn't exist, returns an empty array (mirrors
|
|
1794
|
+
* `countMessagesAfter`'s conservative semantics). Used by the
|
|
1795
|
+
* memory-retrospective job handler to load the message slice it processes.
|
|
1796
|
+
*/
|
|
1797
|
+
export function getMessagesAfter(
|
|
1798
|
+
conversationId: string,
|
|
1799
|
+
afterMessageId: string | null,
|
|
1800
|
+
): MessageRow[] {
|
|
1801
|
+
const db = getDb();
|
|
1802
|
+
if (afterMessageId === null || afterMessageId === "") {
|
|
1803
|
+
// Secondary `asc(messages.id)` matches the non-null path's cursor
|
|
1804
|
+
// ordering, so callers tracking `cutoffMessageId` across runs see a
|
|
1805
|
+
// consistent ordering when multiple rows share a millisecond timestamp.
|
|
1806
|
+
return db
|
|
1807
|
+
.select()
|
|
1808
|
+
.from(messages)
|
|
1809
|
+
.where(eq(messages.conversationId, conversationId))
|
|
1810
|
+
.orderBy(asc(messages.createdAt), asc(messages.id))
|
|
1811
|
+
.all()
|
|
1812
|
+
.map(parseMessage);
|
|
1813
|
+
}
|
|
1814
|
+
const ref = db
|
|
1815
|
+
.select({ createdAt: messages.createdAt })
|
|
1816
|
+
.from(messages)
|
|
1817
|
+
.where(eq(messages.id, afterMessageId))
|
|
1818
|
+
.get();
|
|
1819
|
+
if (!ref) return [];
|
|
1820
|
+
// Same `(createdAt, id)` cursor as `countMessagesAfter` — rows sharing
|
|
1821
|
+
// the reference's millisecond timestamp would otherwise be skipped.
|
|
1822
|
+
return db
|
|
1823
|
+
.select()
|
|
1824
|
+
.from(messages)
|
|
1825
|
+
.where(
|
|
1826
|
+
and(
|
|
1827
|
+
eq(messages.conversationId, conversationId),
|
|
1828
|
+
or(
|
|
1829
|
+
gt(messages.createdAt, ref.createdAt),
|
|
1830
|
+
and(
|
|
1831
|
+
eq(messages.createdAt, ref.createdAt),
|
|
1832
|
+
gt(messages.id, afterMessageId),
|
|
1833
|
+
),
|
|
1834
|
+
),
|
|
1835
|
+
),
|
|
1836
|
+
)
|
|
1837
|
+
.orderBy(asc(messages.createdAt), asc(messages.id))
|
|
1838
|
+
.all()
|
|
1839
|
+
.map(parseMessage);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Efficient existence check — returns true if the conversation has at least
|
|
1844
|
+
* one message row. Uses `LIMIT 1` + `select({ 1 })` to avoid loading and
|
|
1845
|
+
* parsing any message content.
|
|
1846
|
+
*/
|
|
1847
|
+
export function hasMessages(conversationId: string): boolean {
|
|
1848
|
+
const db = getDb();
|
|
1849
|
+
const row = db
|
|
1850
|
+
.select({ one: sql`1` })
|
|
1851
|
+
.from(messages)
|
|
1852
|
+
.where(eq(messages.conversationId, conversationId))
|
|
1853
|
+
.limit(1)
|
|
1854
|
+
.get();
|
|
1855
|
+
return row !== undefined;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
interface PaginatedMessagesResult {
|
|
1859
|
+
messages: MessageRow[];
|
|
1860
|
+
hasMore: boolean;
|
|
1861
|
+
/**
|
|
1862
|
+
* Position of the last row scanned when the loop stops on
|
|
1863
|
+
* `PAGINATION_SCAN_CAP` rather than DB exhaustion. Callers derive their
|
|
1864
|
+
* client cursor from the visible page's oldest row, but a cap-truncated
|
|
1865
|
+
* page can be empty (a contiguous block of filtered-out rows longer than
|
|
1866
|
+
* the cap), leaving nothing to resume from. Surfacing the last scanned
|
|
1867
|
+
* `(createdAt, id)` lets the caller hand the client a cursor so it can
|
|
1868
|
+
* request the next window and keep draining instead of stalling.
|
|
1869
|
+
*/
|
|
1870
|
+
nextCursor?: { createdAt: number; id: string };
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const PAGINATION_CHUNK_MIN = 50;
|
|
1874
|
+
const PAGINATION_SCAN_CAP = 10_000;
|
|
1875
|
+
|
|
1876
|
+
// Test-only override for PAGINATION_SCAN_CAP so tests can exercise the
|
|
1877
|
+
// cap-truncation branch with a small cap instead of seeding >10k rows (which
|
|
1878
|
+
// makes the suite slow and the post-test DELETE flaky under parallel CI load).
|
|
1879
|
+
// `undefined` restores the production cap.
|
|
1880
|
+
let paginationScanCapOverride: number | undefined;
|
|
1881
|
+
export function _setPaginationScanCapForTesting(cap: number | undefined): void {
|
|
1882
|
+
paginationScanCapOverride = cap;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
export function getMessagesPaginated(
|
|
1886
|
+
conversationId: string,
|
|
1887
|
+
limit: number | undefined,
|
|
1888
|
+
beforeTimestamp?: number,
|
|
1889
|
+
filter?: (row: MessageRow) => boolean,
|
|
1890
|
+
): PaginatedMessagesResult {
|
|
1891
|
+
const db = getDb();
|
|
1892
|
+
|
|
1893
|
+
if (limit === undefined) {
|
|
1894
|
+
const conditions = [eq(messages.conversationId, conversationId)];
|
|
1895
|
+
if (beforeTimestamp !== undefined) {
|
|
1896
|
+
conditions.push(lt(messages.createdAt, beforeTimestamp));
|
|
1897
|
+
}
|
|
1898
|
+
const rows = db
|
|
1899
|
+
.select()
|
|
1900
|
+
.from(messages)
|
|
1901
|
+
.where(and(...conditions))
|
|
1902
|
+
.orderBy(asc(messages.createdAt))
|
|
1903
|
+
.all()
|
|
1904
|
+
.map(parseMessage);
|
|
1905
|
+
return {
|
|
1906
|
+
messages: filter ? rows.filter(filter) : rows,
|
|
1907
|
+
hasMore: false,
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// Walk pages newest→oldest, applying `filter` in TS (metadata parsing is
|
|
1912
|
+
// JSON, not a structured column). Keep fetching until we have `limit + 1`
|
|
1913
|
+
// visible rows or the DB is exhausted, so `hasMore` and the cursor reflect
|
|
1914
|
+
// the visible page rather than the unfiltered row count. Without this loop,
|
|
1915
|
+
// a fully-hidden page returns `{ messages: [], hasMore: true }` with no
|
|
1916
|
+
// cursor, which stalls the web client's older-page fetch.
|
|
1917
|
+
let cursorCreatedAt = beforeTimestamp;
|
|
1918
|
+
let cursorMessageId: string | undefined;
|
|
1919
|
+
const visible: MessageRow[] = [];
|
|
1920
|
+
const chunkSize = Math.max(limit + 1, PAGINATION_CHUNK_MIN);
|
|
1921
|
+
// Bound the work a single request can do when `filter` rejects nearly every
|
|
1922
|
+
// row — otherwise a pathological filter against a huge conversation would
|
|
1923
|
+
// tie up a connection for thousands of roundtrips.
|
|
1924
|
+
let rowsScanned = 0;
|
|
1925
|
+
// Distinguish "stopped because we hit the scan cap" from "stopped because the
|
|
1926
|
+
// DB ran out of rows". On a cap-truncated stop there may be more visible rows
|
|
1927
|
+
// past the scanned window, so `hasMore` must stay true and we record the last
|
|
1928
|
+
// scanned position as a resume cursor (the visible page may be empty).
|
|
1929
|
+
let scanCapTruncated = false;
|
|
1930
|
+
let lastScanned: { createdAt: number; id: string } | undefined;
|
|
1931
|
+
const scanCap = paginationScanCapOverride ?? PAGINATION_SCAN_CAP;
|
|
1932
|
+
|
|
1933
|
+
while (visible.length < limit + 1) {
|
|
1934
|
+
if (rowsScanned >= scanCap) {
|
|
1935
|
+
scanCapTruncated = true;
|
|
1936
|
+
break;
|
|
1937
|
+
}
|
|
1938
|
+
const cursorPredicate =
|
|
1939
|
+
cursorCreatedAt === undefined
|
|
1940
|
+
? undefined
|
|
1941
|
+
: cursorMessageId === undefined
|
|
1942
|
+
? lt(messages.createdAt, cursorCreatedAt)
|
|
1943
|
+
: or(
|
|
1944
|
+
lt(messages.createdAt, cursorCreatedAt),
|
|
1945
|
+
and(
|
|
1946
|
+
eq(messages.createdAt, cursorCreatedAt),
|
|
1947
|
+
lt(messages.id, cursorMessageId),
|
|
1948
|
+
),
|
|
1949
|
+
);
|
|
1950
|
+
|
|
1951
|
+
const chunk = db
|
|
1952
|
+
.select()
|
|
1953
|
+
.from(messages)
|
|
1954
|
+
.where(and(eq(messages.conversationId, conversationId), cursorPredicate))
|
|
1955
|
+
.orderBy(desc(messages.createdAt), desc(messages.id))
|
|
1956
|
+
.limit(chunkSize)
|
|
1957
|
+
.all()
|
|
1958
|
+
.map(parseMessage);
|
|
1959
|
+
|
|
1960
|
+
if (chunk.length === 0) break;
|
|
1961
|
+
rowsScanned += chunk.length;
|
|
1962
|
+
|
|
1963
|
+
for (const row of chunk) {
|
|
1964
|
+
if (!filter || filter(row)) visible.push(row);
|
|
1965
|
+
if (visible.length >= limit + 1) break;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (chunk.length < chunkSize) break;
|
|
1969
|
+
const lastRow = chunk[chunk.length - 1];
|
|
1970
|
+
lastScanned = { createdAt: lastRow.createdAt, id: lastRow.id };
|
|
1971
|
+
cursorCreatedAt = lastRow.createdAt;
|
|
1972
|
+
cursorMessageId = lastRow.id;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const filledPage = visible.length > limit;
|
|
1976
|
+
// A cap-truncated stop means the DB may still hold older visible rows past
|
|
1977
|
+
// the scanned window, so report `hasMore: true` to keep the client draining
|
|
1978
|
+
// — returning `false` here is the stall this loop exists to prevent.
|
|
1979
|
+
const hasMore = filledPage || scanCapTruncated;
|
|
1980
|
+
if (filledPage) visible.splice(limit);
|
|
1981
|
+
visible.reverse();
|
|
1982
|
+
|
|
1983
|
+
// Only hand back a resume cursor when the cap (not DB exhaustion) cut the
|
|
1984
|
+
// search short; callers fall back to it when the visible page came back
|
|
1985
|
+
// empty and has no oldest row to anchor the next request.
|
|
1986
|
+
const nextCursor = scanCapTruncated ? lastScanned : undefined;
|
|
1987
|
+
|
|
1988
|
+
return { messages: visible, hasMore, nextCursor };
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
export function getLastUserTimestampBefore(
|
|
1992
|
+
conversationId: string,
|
|
1993
|
+
beforeTimestamp: number,
|
|
1994
|
+
): number {
|
|
1995
|
+
const db = getDb();
|
|
1996
|
+
const row = db
|
|
1997
|
+
.select({ createdAt: messages.createdAt })
|
|
1998
|
+
.from(messages)
|
|
1999
|
+
.where(
|
|
2000
|
+
and(
|
|
2001
|
+
eq(messages.conversationId, conversationId),
|
|
2002
|
+
eq(messages.role, "user"),
|
|
2003
|
+
lt(messages.createdAt, beforeTimestamp),
|
|
2004
|
+
),
|
|
2005
|
+
)
|
|
2006
|
+
.orderBy(desc(messages.createdAt))
|
|
2007
|
+
.limit(1)
|
|
2008
|
+
.get();
|
|
2009
|
+
return row?.createdAt ?? 0;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Most recent user-message timestamp (epoch ms) across all conversations, or
|
|
2014
|
+
* `0` when no user message exists.
|
|
2015
|
+
*
|
|
2016
|
+
* Ordered by `created_at` rather than insertion order, because `forkConversation`
|
|
2017
|
+
* copies a parent's messages into the fork while preserving their original
|
|
2018
|
+
* `created_at`. Those copies receive fresh row ids, so an insertion-order scan
|
|
2019
|
+
* could surface an old forked turn as if it were the latest activity and let
|
|
2020
|
+
* maintenance run while the user is in fact active. The `(role, created_at)`
|
|
2021
|
+
* index (`idx_messages_role_created_at`) makes this an indexed seek to the
|
|
2022
|
+
* newest `role = "user"` row rather than a scan of the whole (potentially
|
|
2023
|
+
* multi-GB) table.
|
|
2024
|
+
*/
|
|
2025
|
+
export function getLastUserMessageTimestamp(): number {
|
|
2026
|
+
const db = getDb();
|
|
2027
|
+
const row = db
|
|
2028
|
+
.select({ createdAt: messages.createdAt })
|
|
2029
|
+
.from(messages)
|
|
2030
|
+
.where(eq(messages.role, "user"))
|
|
2031
|
+
.orderBy(desc(messages.createdAt))
|
|
2032
|
+
.limit(1)
|
|
2033
|
+
.get();
|
|
2034
|
+
return row?.createdAt ?? 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/** Fetch a single message by ID, optionally scoped to a specific conversation. */
|
|
2038
|
+
export function getMessageById(
|
|
2039
|
+
messageId: string,
|
|
2040
|
+
conversationId?: string,
|
|
2041
|
+
): MessageRow | null {
|
|
2042
|
+
const db = getDb();
|
|
2043
|
+
const conditions = [eq(messages.id, messageId)];
|
|
2044
|
+
if (conversationId) {
|
|
2045
|
+
conditions.push(eq(messages.conversationId, conversationId));
|
|
2046
|
+
}
|
|
2047
|
+
const row = db
|
|
2048
|
+
.select()
|
|
2049
|
+
.from(messages)
|
|
2050
|
+
.where(and(...conditions))
|
|
2051
|
+
.get();
|
|
2052
|
+
return row ? parseMessage(row) : null;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
export function updateConversationTitle(
|
|
2056
|
+
id: string,
|
|
2057
|
+
title: string,
|
|
2058
|
+
isAutoTitle?: number,
|
|
2059
|
+
): void {
|
|
2060
|
+
const db = getDb();
|
|
2061
|
+
const set: Record<string, unknown> = { title, updatedAt: Date.now() };
|
|
2062
|
+
if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
|
|
2063
|
+
db.update(conversations).set(set).where(eq(conversations.id, id)).run();
|
|
2064
|
+
|
|
2065
|
+
// Update disk view meta.json with the new title
|
|
2066
|
+
const conv = getConversation(id);
|
|
2067
|
+
if (conv) {
|
|
2068
|
+
updateMetaFile(conv);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
export function updateConversationUsage(
|
|
2073
|
+
id: string,
|
|
2074
|
+
totalInputTokens: number,
|
|
2075
|
+
totalOutputTokens: number,
|
|
2076
|
+
totalEstimatedCost: number,
|
|
2077
|
+
): void {
|
|
2078
|
+
const db = getDb();
|
|
2079
|
+
db.update(conversations)
|
|
2080
|
+
.set({
|
|
2081
|
+
totalInputTokens,
|
|
2082
|
+
totalOutputTokens,
|
|
2083
|
+
totalEstimatedCost,
|
|
2084
|
+
updatedAt: Date.now(),
|
|
2085
|
+
})
|
|
2086
|
+
.where(eq(conversations.id, id))
|
|
2087
|
+
.run();
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
export function updateConversationContextWindow(
|
|
2091
|
+
id: string,
|
|
2092
|
+
contextSummary: string,
|
|
2093
|
+
contextCompactedMessageCount: number,
|
|
2094
|
+
): void {
|
|
2095
|
+
const db = getDb();
|
|
2096
|
+
const now = Date.now();
|
|
2097
|
+
// Update the hot-path cache columns and append the event to the ledger in a
|
|
2098
|
+
// single transaction so the latest ledger event always matches the cache.
|
|
2099
|
+
db.transaction(() => {
|
|
2100
|
+
db.update(conversations)
|
|
2101
|
+
.set({
|
|
2102
|
+
contextSummary,
|
|
2103
|
+
contextCompactedMessageCount,
|
|
2104
|
+
contextCompactedAt: now,
|
|
2105
|
+
updatedAt: now,
|
|
2106
|
+
})
|
|
2107
|
+
.where(eq(conversations.id, id))
|
|
2108
|
+
.run();
|
|
2109
|
+
if (contextCompactedMessageCount > 0) {
|
|
2110
|
+
appendCompactionEvent(id, {
|
|
2111
|
+
compactedAt: now,
|
|
2112
|
+
summary: contextSummary,
|
|
2113
|
+
compactedMessageCount: contextCompactedMessageCount,
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
export function setConversationHistoryStrippedAt(
|
|
2120
|
+
id: string,
|
|
2121
|
+
historyStrippedAt: number | null,
|
|
2122
|
+
): void {
|
|
2123
|
+
const db = getDb();
|
|
2124
|
+
db.update(conversations)
|
|
2125
|
+
.set({
|
|
2126
|
+
historyStrippedAt,
|
|
2127
|
+
updatedAt: Date.now(),
|
|
2128
|
+
})
|
|
2129
|
+
.where(eq(conversations.id, id))
|
|
2130
|
+
.run();
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
export function updateConversationSlackContextWatermark(
|
|
2134
|
+
id: string,
|
|
2135
|
+
watermarkTs: string,
|
|
2136
|
+
compactedAt: number = Date.now(),
|
|
2137
|
+
): void {
|
|
2138
|
+
const db = getDb();
|
|
2139
|
+
db.update(conversations)
|
|
2140
|
+
.set({
|
|
2141
|
+
slackContextCompactionWatermarkTs: watermarkTs,
|
|
2142
|
+
slackContextCompactionWatermarkAt: compactedAt,
|
|
2143
|
+
updatedAt: Date.now(),
|
|
2144
|
+
})
|
|
2145
|
+
.where(eq(conversations.id, id))
|
|
2146
|
+
.run();
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
export function archiveConversation(id: string): boolean {
|
|
2150
|
+
const conv = getConversation(id);
|
|
2151
|
+
if (!conv) return false;
|
|
2152
|
+
const now = Date.now();
|
|
2153
|
+
rawRun(
|
|
2154
|
+
"UPDATE conversations SET archived_at = ?, updated_at = ? WHERE id = ?",
|
|
2155
|
+
now,
|
|
2156
|
+
now,
|
|
2157
|
+
id,
|
|
2158
|
+
);
|
|
2159
|
+
return true;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
export function unarchiveConversation(id: string): boolean {
|
|
2163
|
+
const conv = getConversation(id);
|
|
2164
|
+
if (!conv) return false;
|
|
2165
|
+
const now = Date.now();
|
|
2166
|
+
rawRun(
|
|
2167
|
+
"UPDATE conversations SET archived_at = NULL, updated_at = ? WHERE id = ?",
|
|
2168
|
+
now,
|
|
2169
|
+
id,
|
|
2170
|
+
);
|
|
2171
|
+
return true;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/**
|
|
2175
|
+
* Persist the processing-start timestamp for a conversation. Called by
|
|
2176
|
+
* `Conversation.setProcessing(true)` so out-of-process callers can detect
|
|
2177
|
+
* mid-turn state by reading the `conversations` row directly. Pass `null`
|
|
2178
|
+
* to clear (turn ended).
|
|
2179
|
+
*/
|
|
2180
|
+
export function setConversationProcessingStartedAt(
|
|
2181
|
+
id: string,
|
|
2182
|
+
startedAt: number | null,
|
|
2183
|
+
): void {
|
|
2184
|
+
rawRun(
|
|
2185
|
+
"UPDATE conversations SET processing_started_at = ? WHERE id = ?",
|
|
2186
|
+
startedAt,
|
|
2187
|
+
id,
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* Clear the persisted processing flag on every conversation that still has one
|
|
2193
|
+
* set, returning the number of rows cleared. Called at daemon startup to reset
|
|
2194
|
+
* conversations whose `processing_started_at` was left non-NULL because the
|
|
2195
|
+
* previous process shut down mid-turn — the in-memory agent loop driving that
|
|
2196
|
+
* turn is gone, so the flag is stale.
|
|
2197
|
+
*/
|
|
2198
|
+
export function clearStaleProcessingFlags(): number {
|
|
2199
|
+
return rawRun(
|
|
2200
|
+
"UPDATE conversations SET processing_started_at = NULL WHERE processing_started_at IS NOT NULL",
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
/**
|
|
2205
|
+
* Read whether a conversation is currently processing. Checks the in-memory
|
|
2206
|
+
* `Conversation._processing` flag first (hot path for resident conversations),
|
|
2207
|
+
* falling back to the persisted `processing_started_at` column for cold
|
|
2208
|
+
* (evicted / never-loaded) conversations. This is the single entry point for
|
|
2209
|
+
* processing state — callers don't need to layer `findConversation` themselves.
|
|
2210
|
+
* Returns `false` when the conversation row doesn't exist.
|
|
2211
|
+
*/
|
|
2212
|
+
export function isConversationProcessing(id: string): boolean {
|
|
2213
|
+
const inMemory = findConversation(id)?.isProcessing();
|
|
2214
|
+
if (inMemory != null) return inMemory;
|
|
2215
|
+
const row = rawGet<{ processing_started_at: number | null }>(
|
|
2216
|
+
"SELECT processing_started_at FROM conversations WHERE id = ?",
|
|
2217
|
+
id,
|
|
2218
|
+
);
|
|
2219
|
+
return row?.processing_started_at != null;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
/**
|
|
2223
|
+
* Highest stream `seq` whose content is durably persisted to this
|
|
2224
|
+
* conversation's message rows, read from the `conversations.seq` column. This
|
|
2225
|
+
* is the snapshot↔stream alignment baseline `/messages` returns so a client
|
|
2226
|
+
* applies only stream events with a higher `seq`. `null` when none was
|
|
2227
|
+
* recorded (created before any stream activity, row predates the column, or
|
|
2228
|
+
* the conversation row is absent), in which case the client cold-starts.
|
|
2229
|
+
*
|
|
2230
|
+
* Seeded at creation with the global high-water seq and advanced on each
|
|
2231
|
+
* persistence flush by {@link recordConversationPersistedSeq}.
|
|
2232
|
+
*/
|
|
2233
|
+
export function getConversationPersistedSeq(id: string): number | null {
|
|
2234
|
+
const row = rawGet<{ seq: number | null }>(
|
|
2235
|
+
"SELECT seq FROM conversations WHERE id = ?",
|
|
2236
|
+
id,
|
|
2237
|
+
);
|
|
2238
|
+
return row?.seq ?? null;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Record that conversation `id` has durably persisted all of its events
|
|
2243
|
+
* through `seq`, writing the `conversations.seq` column. Called at each
|
|
2244
|
+
* persistence flush with the `seq` of the last event whose content the write
|
|
2245
|
+
* committed.
|
|
2246
|
+
*
|
|
2247
|
+
* Monotonic: the `WHERE seq IS NULL OR seq < ?` guard makes the update raise
|
|
2248
|
+
* the high-water mark only, so out-of-order async commits never regress it.
|
|
2249
|
+
* Non-positive or non-finite `seq` values are ignored.
|
|
2250
|
+
*/
|
|
2251
|
+
export function recordConversationPersistedSeq(id: string, seq: number): void {
|
|
2252
|
+
if (!Number.isFinite(seq) || seq <= 0) return;
|
|
2253
|
+
rawRun(
|
|
2254
|
+
"UPDATE conversations SET seq = ? WHERE id = ? AND (seq IS NULL OR seq < ?)",
|
|
2255
|
+
seq,
|
|
2256
|
+
id,
|
|
2257
|
+
seq,
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Set or clear the `surfaced_at` promotion marker for a conversation.
|
|
2263
|
+
*
|
|
2264
|
+
* A non-null `surfaced_at` promotes a background/scheduled conversation
|
|
2265
|
+
* into the default ("standard") conversation listing so clients show it in
|
|
2266
|
+
* the Recents sidebar grouping. Promotion is always explicit — callers are
|
|
2267
|
+
* product flows that decide a background run deserves foreground visibility
|
|
2268
|
+
* (e.g. the user sent a follow-up message in it). Nothing sets this
|
|
2269
|
+
* automatically.
|
|
2270
|
+
*
|
|
2271
|
+
* Returns `null` when the conversation does not exist; otherwise the new
|
|
2272
|
+
* `surfacedAt` value (`number` when surfacing, `null` when clearing).
|
|
2273
|
+
*/
|
|
2274
|
+
export function setConversationSurfaced(
|
|
2275
|
+
id: string,
|
|
2276
|
+
surfaced: boolean,
|
|
2277
|
+
): { surfacedAt: number | null } | null {
|
|
2278
|
+
const conv = getConversation(id);
|
|
2279
|
+
if (!conv) return null;
|
|
2280
|
+
const now = Date.now();
|
|
2281
|
+
const surfacedAt = surfaced ? now : null;
|
|
2282
|
+
rawRun(
|
|
2283
|
+
"UPDATE conversations SET surfaced_at = ?, updated_at = ? WHERE id = ?",
|
|
2284
|
+
surfacedAt,
|
|
2285
|
+
now,
|
|
2286
|
+
id,
|
|
2287
|
+
);
|
|
2288
|
+
return { surfacedAt };
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Set or clear the inference profile override for a conversation.
|
|
2293
|
+
* Pass `null` to clear the override and fall back to the workspace
|
|
2294
|
+
* `llm.activeProfile` resolution.
|
|
2295
|
+
*
|
|
2296
|
+
* Also clears any stale session columns (`inferenceProfileSessionId`,
|
|
2297
|
+
* `inferenceProfileExpiresAt`) so that the reaper and lazy expiry check
|
|
2298
|
+
* cannot later clobber the newly-set profile.
|
|
2299
|
+
*/
|
|
2300
|
+
export function setConversationInferenceProfile(
|
|
2301
|
+
conversationId: string,
|
|
2302
|
+
profile: string | null,
|
|
2303
|
+
): void {
|
|
2304
|
+
const db = getDb();
|
|
2305
|
+
db.update(conversations)
|
|
2306
|
+
.set({
|
|
2307
|
+
inferenceProfile: profile,
|
|
2308
|
+
inferenceProfileSessionId: null,
|
|
2309
|
+
inferenceProfileExpiresAt: null,
|
|
2310
|
+
updatedAt: Date.now(),
|
|
2311
|
+
})
|
|
2312
|
+
.where(eq(conversations.id, conversationId))
|
|
2313
|
+
.run();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* Atomically set the inference profile, session id, and expiry timestamp for
|
|
2318
|
+
* a conversation. Pass `null` for all three to clear the session-backed
|
|
2319
|
+
* override and fall back to the workspace `llm.activeProfile` resolution.
|
|
2320
|
+
*/
|
|
2321
|
+
export function setConversationInferenceProfileSession(
|
|
2322
|
+
conversationId: string,
|
|
2323
|
+
profile: string | null,
|
|
2324
|
+
sessionId: string | null,
|
|
2325
|
+
expiresAt: number | null,
|
|
2326
|
+
): void {
|
|
2327
|
+
const db = getDb();
|
|
2328
|
+
db.update(conversations)
|
|
2329
|
+
.set({
|
|
2330
|
+
inferenceProfile: profile,
|
|
2331
|
+
inferenceProfileSessionId: sessionId,
|
|
2332
|
+
inferenceProfileExpiresAt: expiresAt,
|
|
2333
|
+
updatedAt: Date.now(),
|
|
2334
|
+
})
|
|
2335
|
+
.where(eq(conversations.id, conversationId))
|
|
2336
|
+
.run();
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* Clear all conversations whose session-backed inference profile has expired.
|
|
2341
|
+
* Returns an array of `{ conversationId, sessionId }` for each cleared row so
|
|
2342
|
+
* callers can emit the appropriate update events.
|
|
2343
|
+
*/
|
|
2344
|
+
export function clearExpiredInferenceProfiles(
|
|
2345
|
+
now: number,
|
|
2346
|
+
): Array<{ conversationId: string; sessionId: string | null }> {
|
|
2347
|
+
const raw = getSqliteFrom(getDb());
|
|
2348
|
+
// Two-step approach: SELECT to get pre-clear sessionIds, then UPDATE.
|
|
2349
|
+
// The UPDATE re-applies the WHERE condition for CAS safety.
|
|
2350
|
+
// RETURNING the id lets us know which rows were actually cleared.
|
|
2351
|
+
const expired = raw
|
|
2352
|
+
.prepare(
|
|
2353
|
+
`
|
|
2354
|
+
SELECT id AS conversationId, inference_profile_session_id AS sessionId
|
|
2355
|
+
FROM conversations
|
|
2356
|
+
WHERE inference_profile_expires_at IS NOT NULL AND inference_profile_expires_at <= ?
|
|
2357
|
+
`,
|
|
2358
|
+
)
|
|
2359
|
+
.all(now) as Array<{ conversationId: string; sessionId: string | null }>;
|
|
2360
|
+
|
|
2361
|
+
if (expired.length === 0) return [];
|
|
2362
|
+
|
|
2363
|
+
const ids = expired.map((r) => r.conversationId);
|
|
2364
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
2365
|
+
|
|
2366
|
+
const actuallyCleared = raw
|
|
2367
|
+
.prepare(
|
|
2368
|
+
`
|
|
2369
|
+
UPDATE conversations
|
|
2370
|
+
SET inference_profile = NULL, inference_profile_session_id = NULL, inference_profile_expires_at = NULL
|
|
2371
|
+
WHERE id IN (${placeholders}) AND inference_profile_expires_at IS NOT NULL AND inference_profile_expires_at <= ?
|
|
2372
|
+
RETURNING id AS conversationId
|
|
2373
|
+
`,
|
|
2374
|
+
)
|
|
2375
|
+
.all(...ids, now) as Array<{ conversationId: string }>;
|
|
2376
|
+
|
|
2377
|
+
const clearedSet = new Set(actuallyCleared.map((r) => r.conversationId));
|
|
2378
|
+
return expired.filter((r) => clearedSet.has(r.conversationId));
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
/**
|
|
2382
|
+
* List conversations with an active (non-expired) session-backed inference
|
|
2383
|
+
* profile. Pass a `conversationId` to narrow to a single conversation.
|
|
2384
|
+
*/
|
|
2385
|
+
export function listActiveInferenceProfileSessions(
|
|
2386
|
+
conversationId?: string,
|
|
2387
|
+
): Array<{
|
|
2388
|
+
conversationId: string;
|
|
2389
|
+
conversationTitle: string | null;
|
|
2390
|
+
profile: string;
|
|
2391
|
+
sessionId: string;
|
|
2392
|
+
expiresAt: number;
|
|
2393
|
+
}> {
|
|
2394
|
+
const db = getDb();
|
|
2395
|
+
const now = Date.now();
|
|
2396
|
+
const baseConditions = [
|
|
2397
|
+
isNotNull(conversations.inferenceProfile),
|
|
2398
|
+
isNotNull(conversations.inferenceProfileExpiresAt),
|
|
2399
|
+
gt(conversations.inferenceProfileExpiresAt, now),
|
|
2400
|
+
isNotNull(conversations.inferenceProfileSessionId),
|
|
2401
|
+
];
|
|
2402
|
+
if (conversationId) {
|
|
2403
|
+
baseConditions.push(eq(conversations.id, conversationId));
|
|
2404
|
+
}
|
|
2405
|
+
return db
|
|
2406
|
+
.select({
|
|
2407
|
+
conversationId: conversations.id,
|
|
2408
|
+
conversationTitle: conversations.title,
|
|
2409
|
+
profile: conversations.inferenceProfile,
|
|
2410
|
+
sessionId: conversations.inferenceProfileSessionId,
|
|
2411
|
+
expiresAt: conversations.inferenceProfileExpiresAt,
|
|
2412
|
+
})
|
|
2413
|
+
.from(conversations)
|
|
2414
|
+
.where(and(...baseConditions))
|
|
2415
|
+
.all() as Array<{
|
|
2416
|
+
conversationId: string;
|
|
2417
|
+
conversationTitle: string | null;
|
|
2418
|
+
profile: string;
|
|
2419
|
+
sessionId: string;
|
|
2420
|
+
expiresAt: number;
|
|
2421
|
+
}>;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/**
|
|
2425
|
+
* The conversation fields needed to resolve a per-turn inference-profile
|
|
2426
|
+
* override. Satisfied by both the DB {@link ConversationRow} and the live
|
|
2427
|
+
* in-memory `Conversation`, so callers can derive the override from whichever
|
|
2428
|
+
* representation they already hold without a redundant row fetch.
|
|
2429
|
+
*/
|
|
2430
|
+
export interface OverrideProfileFields {
|
|
2431
|
+
conversationType?: string | null;
|
|
2432
|
+
inferenceProfile?: string | null;
|
|
2433
|
+
inferenceProfileExpiresAt?: number | null;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Resolve the per-turn inference-profile override from a conversation's
|
|
2438
|
+
* fields. Returns the `inferenceProfile` for interactive conversations,
|
|
2439
|
+
* `undefined` for automation threads (subagent fan-out, scheduled
|
|
2440
|
+
* tasks) so they run on the workspace defaults rather than
|
|
2441
|
+
* inheriting an interactive override.
|
|
2442
|
+
*/
|
|
2443
|
+
export function resolveOverrideProfile(
|
|
2444
|
+
fields: OverrideProfileFields | null,
|
|
2445
|
+
): string | undefined {
|
|
2446
|
+
if (
|
|
2447
|
+
fields?.conversationType === "background" ||
|
|
2448
|
+
fields?.conversationType === "scheduled"
|
|
2449
|
+
) {
|
|
2450
|
+
return undefined;
|
|
2451
|
+
}
|
|
2452
|
+
// Treat an expired session as if the override is absent. The eager reaper
|
|
2453
|
+
// clears the row and emits the update event; the lazy check here ensures
|
|
2454
|
+
// correctness on read paths before the reaper fires.
|
|
2455
|
+
//
|
|
2456
|
+
// `<=` (not `<`) for boundary consistency with the rest of the session
|
|
2457
|
+
// logic: the reaper SQL uses `expires_at <= ?`, and the active-session
|
|
2458
|
+
// queries use `expiresAt > now` (i.e. treat exact-expiry as inactive).
|
|
2459
|
+
// Without this, a session at the exact-expiry millisecond would be served
|
|
2460
|
+
// for one extra turn here while being cleared by the reaper.
|
|
2461
|
+
if (
|
|
2462
|
+
fields?.inferenceProfileExpiresAt != null &&
|
|
2463
|
+
fields.inferenceProfileExpiresAt <= Date.now()
|
|
2464
|
+
) {
|
|
2465
|
+
return undefined;
|
|
2466
|
+
}
|
|
2467
|
+
return fields?.inferenceProfile ?? undefined;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* Resolve the per-turn inference-profile override by conversation id.
|
|
2472
|
+
* Convenience wrapper for standalone callers (e.g. subagent spawn,
|
|
2473
|
+
* opportunity-wake) that don't already have the conversation in hand.
|
|
2474
|
+
*/
|
|
2475
|
+
export function getConversationOverrideProfile(
|
|
2476
|
+
conversationId: string,
|
|
2477
|
+
): string | undefined {
|
|
2478
|
+
return resolveOverrideProfile(getConversation(conversationId));
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
export function setLastNotifiedInferenceProfile(
|
|
2482
|
+
conversationId: string,
|
|
2483
|
+
profileKey: string | null,
|
|
2484
|
+
): void {
|
|
2485
|
+
rawRun(
|
|
2486
|
+
"UPDATE conversations SET last_notified_inference_profile = ? WHERE id = ?",
|
|
2487
|
+
profileKey,
|
|
2488
|
+
conversationId,
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
/**
|
|
2493
|
+
* Delete all conversations, messages, and related data (tool invocations,
|
|
2494
|
+
* memory segments, etc.) from the daemon database.
|
|
2495
|
+
* Returns { conversations, messages } counts.
|
|
2496
|
+
*
|
|
2497
|
+
* Each bulk DELETE is dispatched through {@link runAsyncSqlite}: when
|
|
2498
|
+
* the host has a `sqlite3` CLI it executes in a subprocess and the
|
|
2499
|
+
* daemon's main event loop stays responsive while large tables
|
|
2500
|
+
* (`llm_request_logs`, `tool_invocations`, etc.) are wiped. On hosts
|
|
2501
|
+
* without the CLI the abstraction falls back to in-process blocking
|
|
2502
|
+
* execution — the same behaviour the daemon had before.
|
|
2503
|
+
*/
|
|
2504
|
+
export async function clearAll(): Promise<{
|
|
2505
|
+
conversations: number;
|
|
2506
|
+
messages: number;
|
|
2507
|
+
}> {
|
|
2508
|
+
const msgCount =
|
|
2509
|
+
rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM messages")?.c ?? 0;
|
|
2510
|
+
const convCount =
|
|
2511
|
+
rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
|
|
2512
|
+
|
|
2513
|
+
// Each DELETE goes through `runAsyncSqlite`. The original code threw
|
|
2514
|
+
// on rawExec failure; mirror that here by throwing when the async
|
|
2515
|
+
// result reports `ok: false`, so the route handler still returns 500.
|
|
2516
|
+
const runOrThrow = async (
|
|
2517
|
+
sql: string,
|
|
2518
|
+
options?: { dbPath?: string },
|
|
2519
|
+
): Promise<void> => {
|
|
2520
|
+
const result = await runAsyncSqlite(sql, `clearAll: ${sql}`, options);
|
|
2521
|
+
if (!result.ok) {
|
|
2522
|
+
throw new Error(
|
|
2523
|
+
`clearAll: \`${sql}\` failed (${result.backend}): ${result.error ?? "unknown"}`,
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
|
|
2528
|
+
// Delete in dependency order. Cascades handle memory_segments and
|
|
2529
|
+
// tool_invocations, but we explicitly clear non-cascading memory
|
|
2530
|
+
// tables too.
|
|
2531
|
+
//
|
|
2532
|
+
// FTS virtual tables are cleared before their base tables. If an FTS
|
|
2533
|
+
// table is corrupted, the DELETE will fail — we drop the associated
|
|
2534
|
+
// triggers so that the subsequent base-table DELETEs don't also fail
|
|
2535
|
+
// (SQLite triggers are atomic with the triggering statement, so a
|
|
2536
|
+
// corrupted FTS table would roll back every base-table DELETE).
|
|
2537
|
+
await runOrThrow("DELETE FROM memory_segments");
|
|
2538
|
+
await runOrThrow("DELETE FROM memory_summaries");
|
|
2539
|
+
await runOrThrow("DELETE FROM memory_embeddings");
|
|
2540
|
+
// memory_jobs and llm_request_logs each live in their own dedicated
|
|
2541
|
+
// connection; clear them directly on those connections rather than through a
|
|
2542
|
+
// sqlite3 subprocess.
|
|
2543
|
+
rawMemoryRun("DELETE FROM memory_jobs");
|
|
2544
|
+
await runOrThrow("DELETE FROM memory_checkpoints");
|
|
2545
|
+
rawLogsRun("DELETE FROM llm_request_logs");
|
|
2546
|
+
await runOrThrow("DELETE FROM llm_usage_events");
|
|
2547
|
+
await runOrThrow("DELETE FROM message_attachments");
|
|
2548
|
+
await runOrThrow("DELETE FROM attachments");
|
|
2549
|
+
await runOrThrow("DELETE FROM tool_invocations");
|
|
2550
|
+
await runOrThrow("DELETE FROM skill_loaded_events");
|
|
2551
|
+
let messagesFtsCorrupted = false;
|
|
2552
|
+
const ftsResult = await runAsyncSqlite(
|
|
2553
|
+
"DELETE FROM messages_fts",
|
|
2554
|
+
"clearAll:messages-fts",
|
|
2555
|
+
);
|
|
2556
|
+
if (!ftsResult.ok) {
|
|
2557
|
+
log.warn(
|
|
2558
|
+
{ error: ftsResult.error, backend: ftsResult.backend },
|
|
2559
|
+
"clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
|
|
2560
|
+
);
|
|
2561
|
+
rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
|
|
2562
|
+
rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
|
|
2563
|
+
rawExec("DROP TRIGGER IF EXISTS messages_fts_au");
|
|
2564
|
+
messagesFtsCorrupted = true;
|
|
2565
|
+
}
|
|
2566
|
+
await runOrThrow("DELETE FROM messages");
|
|
2567
|
+
await runOrThrow("DELETE FROM conversations");
|
|
2568
|
+
|
|
2569
|
+
// Record audit event — lifecycle_events is NOT deleted by clearAll(),
|
|
2570
|
+
// so this survives the wipe and provides a permanent trail.
|
|
2571
|
+
rawRun(
|
|
2572
|
+
`INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
|
|
2573
|
+
uuid(),
|
|
2574
|
+
"conversations_clear_all",
|
|
2575
|
+
Date.now(),
|
|
2576
|
+
);
|
|
2577
|
+
|
|
2578
|
+
// Rebuild corrupted FTS tables and restore triggers after all base-table
|
|
2579
|
+
// DELETEs have completed. Dropping the virtual table clears the corruption,
|
|
2580
|
+
// and recreating it + triggers means subsequent writes maintain FTS
|
|
2581
|
+
// consistency without requiring a daemon restart.
|
|
2582
|
+
if (messagesFtsCorrupted) {
|
|
2583
|
+
rawExec("DROP TABLE IF EXISTS messages_fts");
|
|
2584
|
+
rawExec(
|
|
2585
|
+
`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
|
|
2586
|
+
);
|
|
2587
|
+
rawExec(
|
|
2588
|
+
`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`,
|
|
2589
|
+
);
|
|
2590
|
+
rawExec(
|
|
2591
|
+
`CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
|
|
2592
|
+
);
|
|
2593
|
+
rawExec(
|
|
2594
|
+
`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`,
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// Clear the disk-view conversations directory and recreate it empty
|
|
2599
|
+
try {
|
|
2600
|
+
rmSync(getConversationsDir(), { recursive: true, force: true });
|
|
2601
|
+
mkdirSync(getConversationsDir(), { recursive: true });
|
|
2602
|
+
} catch (err) {
|
|
2603
|
+
log.warn({ err }, "clearAll: failed to reset conversations directory");
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
void clearAllConversationIds();
|
|
2607
|
+
|
|
2608
|
+
return { conversations: convCount, messages: msgCount };
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
export function deleteLastExchange(conversationId: string): number {
|
|
2612
|
+
const db = getDb();
|
|
2613
|
+
|
|
2614
|
+
// Find the last user message's id
|
|
2615
|
+
const lastUserMsg = db
|
|
2616
|
+
.select({ id: messages.id })
|
|
2617
|
+
.from(messages)
|
|
2618
|
+
.where(
|
|
2619
|
+
and(
|
|
2620
|
+
eq(messages.conversationId, conversationId),
|
|
2621
|
+
eq(messages.role, "user"),
|
|
2622
|
+
),
|
|
2623
|
+
)
|
|
2624
|
+
.orderBy(sql`rowid DESC`)
|
|
2625
|
+
.limit(1)
|
|
2626
|
+
.get();
|
|
2627
|
+
|
|
2628
|
+
if (!lastUserMsg) return 0;
|
|
2629
|
+
|
|
2630
|
+
// Use rowid to identify the last user message and everything after it.
|
|
2631
|
+
// rowid is monotonically increasing for inserts, so this is safe even if
|
|
2632
|
+
// multiple messages share the same millisecond timestamp.
|
|
2633
|
+
const rowidSubquery = sql`(SELECT rowid FROM messages WHERE id = ${lastUserMsg.id})`;
|
|
2634
|
+
const condition = and(
|
|
2635
|
+
eq(messages.conversationId, conversationId),
|
|
2636
|
+
sql`rowid >= ${rowidSubquery}`,
|
|
2637
|
+
);
|
|
2638
|
+
|
|
2639
|
+
const [{ deleted }] = db
|
|
2640
|
+
.select({ deleted: count() })
|
|
2641
|
+
.from(messages)
|
|
2642
|
+
.where(condition)
|
|
2643
|
+
.all();
|
|
2644
|
+
if (deleted === 0) return 0;
|
|
2645
|
+
|
|
2646
|
+
// Collect attachment IDs linked to the messages being deleted so we can
|
|
2647
|
+
// scope orphan cleanup to only those candidates (not freshly uploaded ones).
|
|
2648
|
+
const messageIds = db
|
|
2649
|
+
.select({ id: messages.id })
|
|
2650
|
+
.from(messages)
|
|
2651
|
+
.where(condition)
|
|
2652
|
+
.all()
|
|
2653
|
+
.map((r) => r.id);
|
|
2654
|
+
const candidateAttachmentIds =
|
|
2655
|
+
messageIds.length > 0
|
|
2656
|
+
? db
|
|
2657
|
+
.select({ attachmentId: messageAttachments.attachmentId })
|
|
2658
|
+
.from(messageAttachments)
|
|
2659
|
+
.where(inArray(messageAttachments.messageId, messageIds))
|
|
2660
|
+
.all()
|
|
2661
|
+
.map((r) => r.attachmentId)
|
|
2662
|
+
.filter((id): id is string => id != null)
|
|
2663
|
+
: [];
|
|
2664
|
+
|
|
2665
|
+
db.transaction((tx) => {
|
|
2666
|
+
tx.delete(messages).where(condition).run();
|
|
2667
|
+
const maxResult = tx
|
|
2668
|
+
.select({ maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})` })
|
|
2669
|
+
.from(messages)
|
|
2670
|
+
.where(eq(messages.conversationId, conversationId))
|
|
2671
|
+
.get();
|
|
2672
|
+
tx.update(conversations)
|
|
2673
|
+
.set({
|
|
2674
|
+
updatedAt: Date.now(),
|
|
2675
|
+
lastMessageAt: maxResult?.maxCreatedAt ?? null,
|
|
2676
|
+
})
|
|
2677
|
+
.where(eq(conversations.id, conversationId))
|
|
2678
|
+
.run();
|
|
2679
|
+
});
|
|
2680
|
+
|
|
2681
|
+
deleteOrphanAttachments(candidateAttachmentIds);
|
|
2682
|
+
|
|
2683
|
+
return deleted;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* IDs collected during message deletion for Qdrant vector cleanup.
|
|
2688
|
+
* Callers must delete these from the Qdrant collection after the
|
|
2689
|
+
* SQLite transaction commits.
|
|
2690
|
+
*/
|
|
2691
|
+
interface DeletedMemoryIds {
|
|
2692
|
+
segmentIds: string[];
|
|
2693
|
+
deletedSummaryIds: string[];
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
interface WipeConversationResult extends DeletedMemoryIds {
|
|
2697
|
+
cancelledJobCount: number;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/**
|
|
2701
|
+
* Reserve an empty message row so the agent loop can stamp outbound
|
|
2702
|
+
* streaming events with a stable identity before content is produced.
|
|
2703
|
+
*
|
|
2704
|
+
* Intentionally skips Qdrant indexing and attention projection — an empty
|
|
2705
|
+
* placeholder is meaningless for either. The caller writes final content
|
|
2706
|
+
* via {@link updateMessageContent} and handles indexing/projection itself.
|
|
2707
|
+
*
|
|
2708
|
+
* Delegates the core insert + retry logic to {@link insertMessageCore}.
|
|
2709
|
+
*/
|
|
2710
|
+
export async function reserveMessage(
|
|
2711
|
+
conversationId: string,
|
|
2712
|
+
role: MessageRole,
|
|
2713
|
+
metadata?: Record<string, unknown>,
|
|
2714
|
+
) {
|
|
2715
|
+
return insertMessageCore({
|
|
2716
|
+
conversationId,
|
|
2717
|
+
role,
|
|
2718
|
+
content: "[]",
|
|
2719
|
+
metadata,
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
/**
|
|
2724
|
+
* Update the content of an existing message. Used when consolidating
|
|
2725
|
+
* multiple assistant messages into one.
|
|
2726
|
+
*/
|
|
2727
|
+
export function updateMessageContent(
|
|
2728
|
+
messageId: string,
|
|
2729
|
+
newContent: string,
|
|
2730
|
+
): void {
|
|
2731
|
+
const db = getDb();
|
|
2732
|
+
db.update(messages)
|
|
2733
|
+
.set({ content: newContent })
|
|
2734
|
+
.where(eq(messages.id, messageId))
|
|
2735
|
+
.run();
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Merge `updates` into the metadata JSON of an existing message.
|
|
2740
|
+
* Reads the current metadata, shallow-merges the new fields, and writes back.
|
|
2741
|
+
*/
|
|
2742
|
+
export function updateMessageMetadata(
|
|
2743
|
+
messageId: string,
|
|
2744
|
+
updates: Record<string, unknown>,
|
|
2745
|
+
): void {
|
|
2746
|
+
const db = getDb();
|
|
2747
|
+
const row = db
|
|
2748
|
+
.select({ metadata: messages.metadata })
|
|
2749
|
+
.from(messages)
|
|
2750
|
+
.where(eq(messages.id, messageId))
|
|
2751
|
+
.get();
|
|
2752
|
+
const existing = row?.metadata ? JSON.parse(row.metadata) : {};
|
|
2753
|
+
db.update(messages)
|
|
2754
|
+
.set({ metadata: JSON.stringify({ ...existing, ...updates }) })
|
|
2755
|
+
.where(eq(messages.id, messageId))
|
|
2756
|
+
.run();
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/**
|
|
2760
|
+
* Atomically update both `content` and (shallow-merged) `metadata` for a
|
|
2761
|
+
* message. Used by edit-propagation paths that need to update the message
|
|
2762
|
+
* body and stamp metadata (e.g. `slackMeta.editedAt`) in a single
|
|
2763
|
+
* transaction so a partial write cannot leak.
|
|
2764
|
+
*
|
|
2765
|
+
* `metadataUpdates` is shallow-merged into the existing top-level metadata
|
|
2766
|
+
* object. To merge into a nested sub-key (e.g. `slackMeta`), the caller
|
|
2767
|
+
* must compute the merged sub-value first and pass `{ slackMeta: merged }`.
|
|
2768
|
+
*/
|
|
2769
|
+
export function updateMessageContentAndMetadata(
|
|
2770
|
+
messageId: string,
|
|
2771
|
+
newContent: string,
|
|
2772
|
+
metadataUpdates: Record<string, unknown>,
|
|
2773
|
+
): void {
|
|
2774
|
+
const db = getDb();
|
|
2775
|
+
db.transaction((tx) => {
|
|
2776
|
+
const row = tx
|
|
2777
|
+
.select({ metadata: messages.metadata })
|
|
2778
|
+
.from(messages)
|
|
2779
|
+
.where(eq(messages.id, messageId))
|
|
2780
|
+
.get();
|
|
2781
|
+
const existing = row?.metadata ? safeParseRecord(row.metadata) : {};
|
|
2782
|
+
tx.update(messages)
|
|
2783
|
+
.set({
|
|
2784
|
+
content: newContent,
|
|
2785
|
+
metadata: JSON.stringify({ ...existing, ...metadataUpdates }),
|
|
2786
|
+
})
|
|
2787
|
+
.where(eq(messages.id, messageId))
|
|
2788
|
+
.run();
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
/**
|
|
2793
|
+
* Re-link all attachments from a set of source messages to a target message.
|
|
2794
|
+
* Used during message consolidation so that attachments linked to deleted
|
|
2795
|
+
* messages survive the ON DELETE CASCADE on message_attachments.
|
|
2796
|
+
*/
|
|
2797
|
+
export function relinkAttachments(
|
|
2798
|
+
fromMessageIds: string[],
|
|
2799
|
+
toMessageId: string,
|
|
2800
|
+
): number {
|
|
2801
|
+
if (fromMessageIds.length === 0) return 0;
|
|
2802
|
+
const db = getDb();
|
|
2803
|
+
|
|
2804
|
+
// Count how many links will be moved before updating.
|
|
2805
|
+
const [{ total }] = db
|
|
2806
|
+
.select({ total: count() })
|
|
2807
|
+
.from(messageAttachments)
|
|
2808
|
+
.where(inArray(messageAttachments.messageId, fromMessageIds))
|
|
2809
|
+
.all();
|
|
2810
|
+
|
|
2811
|
+
if (total === 0) return 0;
|
|
2812
|
+
|
|
2813
|
+
db.update(messageAttachments)
|
|
2814
|
+
.set({ messageId: toMessageId })
|
|
2815
|
+
.where(inArray(messageAttachments.messageId, fromMessageIds))
|
|
2816
|
+
.run();
|
|
2817
|
+
|
|
2818
|
+
return total;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/**
|
|
2822
|
+
* Delete a single message by ID without cascading to message_runs or
|
|
2823
|
+
* channel_inbound_events. Nullable FK columns in those tables are set to
|
|
2824
|
+
* NULL before the message row is removed, so associated run and event
|
|
2825
|
+
* records survive.
|
|
2826
|
+
*
|
|
2827
|
+
* Returns segment IDs so the caller can clean up the corresponding
|
|
2828
|
+
* Qdrant vector entries.
|
|
2829
|
+
*/
|
|
2830
|
+
export function deleteMessageById(messageId: string): DeletedMemoryIds {
|
|
2831
|
+
const db = getDb();
|
|
2832
|
+
const result: DeletedMemoryIds = {
|
|
2833
|
+
segmentIds: [],
|
|
2834
|
+
deletedSummaryIds: [],
|
|
2835
|
+
};
|
|
2836
|
+
|
|
2837
|
+
// Collect attachment IDs linked to this message before cascade-delete
|
|
2838
|
+
// so we can scope orphan cleanup to only those candidates.
|
|
2839
|
+
const candidateAttachmentIds = db
|
|
2840
|
+
.select({ attachmentId: messageAttachments.attachmentId })
|
|
2841
|
+
.from(messageAttachments)
|
|
2842
|
+
.where(eq(messageAttachments.messageId, messageId))
|
|
2843
|
+
.all()
|
|
2844
|
+
.map((r) => r.attachmentId)
|
|
2845
|
+
.filter((id): id is string => id !== undefined);
|
|
2846
|
+
|
|
2847
|
+
// Look up the conversation before the transaction so we can recalculate lastMessageAt.
|
|
2848
|
+
const msgRow = db
|
|
2849
|
+
.select({ conversationId: messages.conversationId })
|
|
2850
|
+
.from(messages)
|
|
2851
|
+
.where(eq(messages.id, messageId))
|
|
2852
|
+
.get();
|
|
2853
|
+
|
|
2854
|
+
db.transaction((tx) => {
|
|
2855
|
+
// Collect memory segment IDs linked to this message before cascade.
|
|
2856
|
+
const linkedSegments = tx
|
|
2857
|
+
.select({ id: memorySegments.id })
|
|
2858
|
+
.from(memorySegments)
|
|
2859
|
+
.where(eq(memorySegments.messageId, messageId))
|
|
2860
|
+
.all();
|
|
2861
|
+
result.segmentIds = linkedSegments.map((r) => r.id);
|
|
2862
|
+
|
|
2863
|
+
// Detach nullable FK references so the cascade doesn't destroy them.
|
|
2864
|
+
tx.update(channelInboundEvents)
|
|
2865
|
+
.set({ messageId: null })
|
|
2866
|
+
.where(eq(channelInboundEvents.messageId, messageId))
|
|
2867
|
+
.run();
|
|
2868
|
+
|
|
2869
|
+
// Now safe to delete — NOT NULL cascades remove memory_segments
|
|
2870
|
+
// and message_attachments.
|
|
2871
|
+
tx.delete(messages).where(eq(messages.id, messageId)).run();
|
|
2872
|
+
|
|
2873
|
+
// Recalculate lastMessageAt after deletion.
|
|
2874
|
+
if (msgRow) {
|
|
2875
|
+
const maxResult = tx
|
|
2876
|
+
.select({
|
|
2877
|
+
maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})`,
|
|
2878
|
+
})
|
|
2879
|
+
.from(messages)
|
|
2880
|
+
.where(eq(messages.conversationId, msgRow.conversationId))
|
|
2881
|
+
.get();
|
|
2882
|
+
tx.update(conversations)
|
|
2883
|
+
.set({ lastMessageAt: maxResult?.maxCreatedAt ?? null })
|
|
2884
|
+
.where(eq(conversations.id, msgRow.conversationId))
|
|
2885
|
+
.run();
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
|
|
2889
|
+
if (result.segmentIds.length > 0) {
|
|
2890
|
+
tx.delete(memoryEmbeddings)
|
|
2891
|
+
.where(
|
|
2892
|
+
and(
|
|
2893
|
+
eq(memoryEmbeddings.targetType, "segment"),
|
|
2894
|
+
inArray(memoryEmbeddings.targetId, result.segmentIds),
|
|
2895
|
+
),
|
|
2896
|
+
)
|
|
2897
|
+
.run();
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
deleteOrphanAttachments(candidateAttachmentIds);
|
|
2902
|
+
|
|
2903
|
+
return result;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
export function setConversationOriginChannelIfUnset(
|
|
2907
|
+
conversationId: string,
|
|
2908
|
+
channel: ChannelId,
|
|
2909
|
+
): void {
|
|
2910
|
+
const db = getDb();
|
|
2911
|
+
db.update(conversations)
|
|
2912
|
+
.set({ originChannel: channel })
|
|
2913
|
+
.where(
|
|
2914
|
+
and(
|
|
2915
|
+
eq(conversations.id, conversationId),
|
|
2916
|
+
isNull(conversations.originChannel),
|
|
2917
|
+
),
|
|
2918
|
+
)
|
|
2919
|
+
.run();
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
export function getConversationOriginChannel(
|
|
2923
|
+
conversationId: string,
|
|
2924
|
+
): ChannelId | null {
|
|
2925
|
+
const db = getDb();
|
|
2926
|
+
const row = db
|
|
2927
|
+
.select({ originChannel: conversations.originChannel })
|
|
2928
|
+
.from(conversations)
|
|
2929
|
+
.where(eq(conversations.id, conversationId))
|
|
2930
|
+
.get();
|
|
2931
|
+
return parseChannelId(row?.originChannel) ?? null;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
export function setConversationOriginInterfaceIfUnset(
|
|
2935
|
+
conversationId: string,
|
|
2936
|
+
interfaceId: InterfaceId,
|
|
2937
|
+
): void {
|
|
2938
|
+
const db = getDb();
|
|
2939
|
+
db.update(conversations)
|
|
2940
|
+
.set({ originInterface: interfaceId })
|
|
2941
|
+
.where(
|
|
2942
|
+
and(
|
|
2943
|
+
eq(conversations.id, conversationId),
|
|
2944
|
+
isNull(conversations.originInterface),
|
|
2945
|
+
),
|
|
2946
|
+
)
|
|
2947
|
+
.run();
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
export function getConversationOriginInterface(
|
|
2951
|
+
conversationId: string,
|
|
2952
|
+
): InterfaceId | null {
|
|
2953
|
+
const db = getDb();
|
|
2954
|
+
const row = db
|
|
2955
|
+
.select({ originInterface: conversations.originInterface })
|
|
2956
|
+
.from(conversations)
|
|
2957
|
+
.where(eq(conversations.id, conversationId))
|
|
2958
|
+
.get();
|
|
2959
|
+
return parseInterfaceId(row?.originInterface) ?? null;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
/**
|
|
2963
|
+
* Return the most recent non-null provenanceTrustClass from user messages
|
|
2964
|
+
* in the given conversation, or `undefined` if none is found.
|
|
2965
|
+
*
|
|
2966
|
+
* Used by the pointer message trust resolver to detect conversations
|
|
2967
|
+
* whose audience is a guardian, trusted_contact, or unverified_contact
|
|
2968
|
+
* outside desktop-origin conversations.
|
|
2969
|
+
*/
|
|
2970
|
+
export function getConversationRecentProvenanceTrustClass(
|
|
2971
|
+
conversationId: string,
|
|
2972
|
+
):
|
|
2973
|
+
| "guardian"
|
|
2974
|
+
| "trusted_contact"
|
|
2975
|
+
| "unverified_contact"
|
|
2976
|
+
| "unknown"
|
|
2977
|
+
| undefined {
|
|
2978
|
+
const row = rawGet<{ metadata: string | null }>(
|
|
2979
|
+
`SELECT metadata FROM messages
|
|
2980
|
+
WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
|
|
2981
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
2982
|
+
conversationId,
|
|
2983
|
+
);
|
|
2984
|
+
if (!row?.metadata) return undefined;
|
|
2985
|
+
try {
|
|
2986
|
+
const parsed = messageMetadataSchema.safeParse(JSON.parse(row.metadata));
|
|
2987
|
+
return parsed.success ? parsed.data.provenanceTrustClass : undefined;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return undefined;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// ---------------------------------------------------------------------------
|
|
2994
|
+
// CRUD functions for display_order and is_pinned
|
|
2995
|
+
// ---------------------------------------------------------------------------
|
|
2996
|
+
|
|
2997
|
+
export function batchSetDisplayOrders(
|
|
2998
|
+
updates: Array<{
|
|
2999
|
+
id: string;
|
|
3000
|
+
displayOrder: number | null;
|
|
3001
|
+
isPinned?: boolean;
|
|
3002
|
+
groupId?: string | null;
|
|
3003
|
+
}>,
|
|
3004
|
+
): void {
|
|
3005
|
+
ensureDisplayOrderMigration();
|
|
3006
|
+
ensureGroupMigration();
|
|
3007
|
+
rawExec("BEGIN");
|
|
3008
|
+
try {
|
|
3009
|
+
for (const update of updates) {
|
|
3010
|
+
if (update.groupId !== undefined) {
|
|
3011
|
+
// New client: groupId is authoritative.
|
|
3012
|
+
// Derive is_pinned from groupId.
|
|
3013
|
+
// Sanitize: if groupId is null or references a deleted/unknown group,
|
|
3014
|
+
// fall back to "system:all" to avoid FK violation that would roll back
|
|
3015
|
+
// the entire batch.
|
|
3016
|
+
let safeGroupId = update.groupId;
|
|
3017
|
+
if (safeGroupId === null) {
|
|
3018
|
+
safeGroupId = "system:all";
|
|
3019
|
+
} else if (
|
|
3020
|
+
!rawGet<{ id: string }>(
|
|
3021
|
+
"SELECT id FROM conversation_groups WHERE id = ?",
|
|
3022
|
+
safeGroupId,
|
|
3023
|
+
)
|
|
3024
|
+
) {
|
|
3025
|
+
safeGroupId = "system:all";
|
|
3026
|
+
}
|
|
3027
|
+
// Moving a conversation into the Scheduled/Background system groups
|
|
3028
|
+
// is an explicit demotion out of Recents, so clear any `surfaced_at`
|
|
3029
|
+
// promotion in the same write — otherwise the surfaced marker would
|
|
3030
|
+
// keep the row in the standard listing and the move would appear to
|
|
3031
|
+
// do nothing.
|
|
3032
|
+
const clearsSurfaced =
|
|
3033
|
+
safeGroupId === "system:background" ||
|
|
3034
|
+
safeGroupId === "system:scheduled";
|
|
3035
|
+
rawRun(
|
|
3036
|
+
`UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ?${
|
|
3037
|
+
clearsSurfaced ? ", surfaced_at = NULL" : ""
|
|
3038
|
+
} WHERE id = ?`,
|
|
3039
|
+
update.displayOrder,
|
|
3040
|
+
safeGroupId === "system:pinned" ? 1 : 0,
|
|
3041
|
+
safeGroupId,
|
|
3042
|
+
update.id,
|
|
3043
|
+
);
|
|
3044
|
+
} else if (update.isPinned === undefined) {
|
|
3045
|
+
// Only displayOrder provided — preserve existing pin state and group.
|
|
3046
|
+
rawRun(
|
|
3047
|
+
"UPDATE conversations SET display_order = ? WHERE id = ?",
|
|
3048
|
+
update.displayOrder,
|
|
3049
|
+
update.id,
|
|
3050
|
+
);
|
|
3051
|
+
} else if (update.isPinned) {
|
|
3052
|
+
rawRun(
|
|
3053
|
+
"UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
3054
|
+
update.displayOrder,
|
|
3055
|
+
update.id,
|
|
3056
|
+
);
|
|
3057
|
+
} else {
|
|
3058
|
+
// Restore system group from source/conversationType when unpinning,
|
|
3059
|
+
// instead of clearing to NULL (which would lose provenance).
|
|
3060
|
+
rawRun(
|
|
3061
|
+
`UPDATE conversations SET display_order = ?, is_pinned = 0,
|
|
3062
|
+
group_id = CASE WHEN group_id = 'system:pinned' THEN
|
|
3063
|
+
CASE
|
|
3064
|
+
WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
|
|
3065
|
+
WHEN source IN ('heartbeat', 'task') THEN 'system:background'
|
|
3066
|
+
WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
|
|
3067
|
+
ELSE 'system:all'
|
|
3068
|
+
END
|
|
3069
|
+
ELSE group_id END
|
|
3070
|
+
WHERE id = ?`,
|
|
3071
|
+
update.displayOrder,
|
|
3072
|
+
update.id,
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
rawExec("COMMIT");
|
|
3077
|
+
} catch (err) {
|
|
3078
|
+
rawExec("ROLLBACK");
|
|
3079
|
+
throw err;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
export function getDisplayMetaForConversations(
|
|
3084
|
+
conversationIds: string[],
|
|
3085
|
+
): Map<
|
|
3086
|
+
string,
|
|
3087
|
+
{ displayOrder: number | null; isPinned: boolean; groupId: string | null }
|
|
3088
|
+
> {
|
|
3089
|
+
ensureDisplayOrderMigration();
|
|
3090
|
+
ensureGroupMigration();
|
|
3091
|
+
const result = new Map<
|
|
3092
|
+
string,
|
|
3093
|
+
{ displayOrder: number | null; isPinned: boolean; groupId: string | null }
|
|
3094
|
+
>();
|
|
3095
|
+
if (conversationIds.length === 0) return result;
|
|
3096
|
+
for (const id of conversationIds) {
|
|
3097
|
+
const row = rawGet<{
|
|
3098
|
+
display_order: number | null;
|
|
3099
|
+
is_pinned: number | null;
|
|
3100
|
+
group_id: string | null;
|
|
3101
|
+
}>(
|
|
3102
|
+
"SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
|
|
3103
|
+
id,
|
|
3104
|
+
);
|
|
3105
|
+
result.set(id, {
|
|
3106
|
+
displayOrder: row?.display_order ?? null,
|
|
3107
|
+
isPinned: (row?.is_pinned ?? 0) === 1,
|
|
3108
|
+
groupId: row?.group_id ?? null,
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
return result;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// ── Turn boundary resolution ─────────────────────────────────────────
|
|
3115
|
+
|
|
3116
|
+
/**
|
|
3117
|
+
* Returns `true` if a message is a tool-result user message — i.e. its
|
|
3118
|
+
* role is "user" and its content is a JSON array where every block has
|
|
3119
|
+
* `type === "tool_result"`. These synthetic user messages are injected
|
|
3120
|
+
* between assistant messages within a single agent turn and should NOT
|
|
3121
|
+
* be treated as turn boundaries.
|
|
3122
|
+
*/
|
|
3123
|
+
function isToolResultMessage(role: string, content: string): boolean {
|
|
3124
|
+
if (role !== "user") return false;
|
|
3125
|
+
try {
|
|
3126
|
+
const parsed = JSON.parse(content);
|
|
3127
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return false;
|
|
3128
|
+
return parsed.every(
|
|
3129
|
+
(block: unknown) =>
|
|
3130
|
+
block != null &&
|
|
3131
|
+
typeof block === "object" &&
|
|
3132
|
+
(block as Record<string, unknown>).type === "tool_result",
|
|
3133
|
+
);
|
|
3134
|
+
} catch {
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
/**
|
|
3140
|
+
* Returns the time boundaries (start/end `createdAt` values) for the turn
|
|
3141
|
+
* containing the given message. The bounds span from the real user message
|
|
3142
|
+
* that started the turn to just before the real user message that starts the
|
|
3143
|
+
* next turn (or to the end of the conversation if this is the last turn).
|
|
3144
|
+
*
|
|
3145
|
+
* Also extends the end boundary to capture orphaned LLM request logs from
|
|
3146
|
+
* deleted intermediate messages (e.g. removed by retry/deleteLastExchange).
|
|
3147
|
+
*
|
|
3148
|
+
* Returns null if the message is the only one in the conversation.
|
|
3149
|
+
*/
|
|
3150
|
+
export function getTurnTimeBounds(
|
|
3151
|
+
conversationId: string,
|
|
3152
|
+
messageCreatedAt: number,
|
|
3153
|
+
): { startTime: number; endTime: number } | null {
|
|
3154
|
+
const db = getDb();
|
|
3155
|
+
|
|
3156
|
+
// Walk backward (by rowid, not just createdAt) to find the real user
|
|
3157
|
+
// message that starts this turn.
|
|
3158
|
+
const rowidSubquery = sql`(
|
|
3159
|
+
SELECT rowid FROM messages
|
|
3160
|
+
WHERE conversation_id = ${conversationId}
|
|
3161
|
+
AND created_at <= ${messageCreatedAt}
|
|
3162
|
+
ORDER BY rowid DESC LIMIT 1
|
|
3163
|
+
)`;
|
|
3164
|
+
const backwardRows = db
|
|
3165
|
+
.select({
|
|
3166
|
+
role: messages.role,
|
|
3167
|
+
content: messages.content,
|
|
3168
|
+
createdAt: messages.createdAt,
|
|
3169
|
+
})
|
|
3170
|
+
.from(messages)
|
|
3171
|
+
.where(
|
|
3172
|
+
and(
|
|
3173
|
+
eq(messages.conversationId, conversationId),
|
|
3174
|
+
sql`rowid <= ${rowidSubquery}`,
|
|
3175
|
+
),
|
|
3176
|
+
)
|
|
3177
|
+
.orderBy(sql`rowid DESC`)
|
|
3178
|
+
.limit(50)
|
|
3179
|
+
.all();
|
|
3180
|
+
|
|
3181
|
+
let startTime = messageCreatedAt;
|
|
3182
|
+
for (const row of backwardRows) {
|
|
3183
|
+
if (row.role === "user" && !isToolResultMessage(row.role, row.content)) {
|
|
3184
|
+
startTime = row.createdAt;
|
|
3185
|
+
break;
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
// Walk forward (by rowid) to find the next real user message.
|
|
3190
|
+
const forwardRowidSubquery = sql`(
|
|
3191
|
+
SELECT rowid FROM messages
|
|
3192
|
+
WHERE conversation_id = ${conversationId}
|
|
3193
|
+
AND created_at >= ${messageCreatedAt}
|
|
3194
|
+
ORDER BY rowid DESC LIMIT 1
|
|
3195
|
+
)`;
|
|
3196
|
+
const forwardRows = db
|
|
3197
|
+
.select({
|
|
3198
|
+
role: messages.role,
|
|
3199
|
+
content: messages.content,
|
|
3200
|
+
createdAt: messages.createdAt,
|
|
3201
|
+
})
|
|
3202
|
+
.from(messages)
|
|
3203
|
+
.where(
|
|
3204
|
+
and(
|
|
3205
|
+
eq(messages.conversationId, conversationId),
|
|
3206
|
+
sql`rowid > ${forwardRowidSubquery}`,
|
|
3207
|
+
),
|
|
3208
|
+
)
|
|
3209
|
+
.orderBy(sql`rowid ASC`)
|
|
3210
|
+
.limit(50)
|
|
3211
|
+
.all();
|
|
3212
|
+
|
|
3213
|
+
let endTime = messageCreatedAt;
|
|
3214
|
+
let nextTurnStart: number | null = null;
|
|
3215
|
+
for (const row of forwardRows) {
|
|
3216
|
+
if (row.role === "user" && !isToolResultMessage(row.role, row.content)) {
|
|
3217
|
+
nextTurnStart = row.createdAt;
|
|
3218
|
+
break;
|
|
3219
|
+
}
|
|
3220
|
+
endTime = row.createdAt;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// When the next turn start has a strictly greater timestamp, use it minus
|
|
3224
|
+
// 1ms as the hard upper bound. When timestamps collide (e.g. in tests),
|
|
3225
|
+
// don't extend — the message-ID-based query is authoritative.
|
|
3226
|
+
if (nextTurnStart != null && nextTurnStart > endTime) {
|
|
3227
|
+
endTime = nextTurnStart - 1;
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// Extend end boundary to the latest log that falls within the turn window.
|
|
3231
|
+
// Orphaned logs from deleted intermediate messages may have timestamps
|
|
3232
|
+
// beyond any surviving message. Cap at 30 minutes to avoid sweeping in
|
|
3233
|
+
// logs from a much later turn.
|
|
3234
|
+
const MAX_TURN_DURATION_MS = 30 * 60 * 1000;
|
|
3235
|
+
const hardCeiling =
|
|
3236
|
+
nextTurnStart != null && nextTurnStart > startTime
|
|
3237
|
+
? nextTurnStart - 1
|
|
3238
|
+
: startTime + MAX_TURN_DURATION_MS;
|
|
3239
|
+
|
|
3240
|
+
if (hardCeiling > endTime) {
|
|
3241
|
+
// llm_request_logs lives in the dedicated logs connection.
|
|
3242
|
+
const latestLog = logsDb()
|
|
3243
|
+
.select({ createdAt: llmRequestLogs.createdAt })
|
|
3244
|
+
.from(llmRequestLogs)
|
|
3245
|
+
.where(
|
|
3246
|
+
and(
|
|
3247
|
+
eq(llmRequestLogs.conversationId, conversationId),
|
|
3248
|
+
gte(llmRequestLogs.createdAt, startTime),
|
|
3249
|
+
lte(llmRequestLogs.createdAt, hardCeiling),
|
|
3250
|
+
),
|
|
3251
|
+
)
|
|
3252
|
+
.orderBy(desc(llmRequestLogs.createdAt))
|
|
3253
|
+
.limit(1)
|
|
3254
|
+
.get();
|
|
3255
|
+
|
|
3256
|
+
if (latestLog && latestLog.createdAt > endTime) {
|
|
3257
|
+
endTime = latestLog.createdAt;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
return { startTime, endTime };
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
/**
|
|
3265
|
+
* Resolve all assistant message IDs that belong to the same agent turn
|
|
3266
|
+
* as the given `messageId`. A "turn" is bounded by:
|
|
3267
|
+
* - The start of the conversation, or
|
|
3268
|
+
* - A user message whose content is NOT a tool_result array.
|
|
3269
|
+
*
|
|
3270
|
+
* Within a multi-step agent loop, the pattern is:
|
|
3271
|
+
* user msg → assistant A1 → user (tool_result) → assistant A2 → ...
|
|
3272
|
+
* All assistant messages from A1 through the queried message (and beyond,
|
|
3273
|
+
* up to the next real user message) are part of the same turn.
|
|
3274
|
+
*
|
|
3275
|
+
* Returns `[messageId]` as a fallback if the message is not found,
|
|
3276
|
+
* preserving backward compatibility for callers.
|
|
3277
|
+
*/
|
|
3278
|
+
export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
3279
|
+
const db = getDb();
|
|
3280
|
+
|
|
3281
|
+
// Look up the target message to get its conversationId and createdAt.
|
|
3282
|
+
const target = getMessageById(messageId);
|
|
3283
|
+
if (!target) return [messageId];
|
|
3284
|
+
|
|
3285
|
+
// Walk backward from the target message to find the turn boundary.
|
|
3286
|
+
// Limit to 50 rows — sufficient for even aggressive tool-use loops.
|
|
3287
|
+
const backwardRows = db
|
|
3288
|
+
.select({
|
|
3289
|
+
id: messages.id,
|
|
3290
|
+
role: messages.role,
|
|
3291
|
+
content: messages.content,
|
|
3292
|
+
createdAt: messages.createdAt,
|
|
3293
|
+
})
|
|
3294
|
+
.from(messages)
|
|
3295
|
+
.where(
|
|
3296
|
+
and(
|
|
3297
|
+
eq(messages.conversationId, target.conversationId),
|
|
3298
|
+
lte(messages.createdAt, target.createdAt),
|
|
3299
|
+
),
|
|
3300
|
+
)
|
|
3301
|
+
.orderBy(desc(messages.createdAt))
|
|
3302
|
+
.limit(50)
|
|
3303
|
+
.all();
|
|
3304
|
+
|
|
3305
|
+
const assistantIds: string[] = [];
|
|
3306
|
+
let boundaryCreatedAt: number | null = null;
|
|
3307
|
+
|
|
3308
|
+
for (const row of backwardRows) {
|
|
3309
|
+
if (row.role === "assistant") {
|
|
3310
|
+
assistantIds.push(row.id);
|
|
3311
|
+
} else if (row.role === "user") {
|
|
3312
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
3313
|
+
// Tool-result user message — still within the same turn, continue.
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
// Real user message — this is the turn boundary.
|
|
3317
|
+
boundaryCreatedAt = row.createdAt;
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// Walk forward from the target to collect any later assistant messages
|
|
3323
|
+
// still within the same turn (e.g. when querying an intermediate
|
|
3324
|
+
// message like A1 in a multi-step turn A1 → tool_result → A2).
|
|
3325
|
+
const forwardRows = db
|
|
3326
|
+
.select({
|
|
3327
|
+
id: messages.id,
|
|
3328
|
+
role: messages.role,
|
|
3329
|
+
content: messages.content,
|
|
3330
|
+
createdAt: messages.createdAt,
|
|
3331
|
+
})
|
|
3332
|
+
.from(messages)
|
|
3333
|
+
.where(
|
|
3334
|
+
and(
|
|
3335
|
+
eq(messages.conversationId, target.conversationId),
|
|
3336
|
+
gt(messages.createdAt, target.createdAt),
|
|
3337
|
+
),
|
|
3338
|
+
)
|
|
3339
|
+
.orderBy(asc(messages.createdAt))
|
|
3340
|
+
.limit(50)
|
|
3341
|
+
.all();
|
|
3342
|
+
|
|
3343
|
+
for (const row of forwardRows) {
|
|
3344
|
+
if (row.role === "assistant") {
|
|
3345
|
+
if (!assistantIds.includes(row.id)) {
|
|
3346
|
+
assistantIds.push(row.id);
|
|
3347
|
+
}
|
|
3348
|
+
} else if (row.role === "user") {
|
|
3349
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
3350
|
+
// Tool-result user message — still within the same turn.
|
|
3351
|
+
continue;
|
|
3352
|
+
}
|
|
3353
|
+
// Real user message — end of the turn.
|
|
3354
|
+
break;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// Also query forward from the backward-walk boundary to pick up any
|
|
3359
|
+
// assistant messages between the boundary and the target that may have
|
|
3360
|
+
// been missed (e.g. due to the 50-row limit in the backward walk).
|
|
3361
|
+
if (boundaryCreatedAt != null) {
|
|
3362
|
+
const gapRows = db
|
|
3363
|
+
.select({
|
|
3364
|
+
id: messages.id,
|
|
3365
|
+
role: messages.role,
|
|
3366
|
+
createdAt: messages.createdAt,
|
|
3367
|
+
})
|
|
3368
|
+
.from(messages)
|
|
3369
|
+
.where(
|
|
3370
|
+
and(
|
|
3371
|
+
eq(messages.conversationId, target.conversationId),
|
|
3372
|
+
gt(messages.createdAt, boundaryCreatedAt),
|
|
3373
|
+
lte(messages.createdAt, target.createdAt),
|
|
3374
|
+
),
|
|
3375
|
+
)
|
|
3376
|
+
.orderBy(asc(messages.createdAt))
|
|
3377
|
+
.all();
|
|
3378
|
+
|
|
3379
|
+
for (const row of gapRows) {
|
|
3380
|
+
if (row.role === "assistant" && !assistantIds.includes(row.id)) {
|
|
3381
|
+
assistantIds.push(row.id);
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// Sort by createdAt to ensure stable ordering.
|
|
3387
|
+
// Re-fetch createdAt for all collected IDs so the sort is accurate.
|
|
3388
|
+
if (assistantIds.length <= 1) return assistantIds;
|
|
3389
|
+
|
|
3390
|
+
const idSet = new Set(assistantIds);
|
|
3391
|
+
const sorted = db
|
|
3392
|
+
.select({ id: messages.id, createdAt: messages.createdAt })
|
|
3393
|
+
.from(messages)
|
|
3394
|
+
.where(
|
|
3395
|
+
and(
|
|
3396
|
+
eq(messages.conversationId, target.conversationId),
|
|
3397
|
+
inArray(messages.id, [...idSet]),
|
|
3398
|
+
),
|
|
3399
|
+
)
|
|
3400
|
+
.orderBy(asc(messages.createdAt))
|
|
3401
|
+
.all();
|
|
3402
|
+
|
|
3403
|
+
return sorted.map((r) => r.id);
|
|
3404
|
+
}
|