@vellumai/assistant 0.8.3 → 0.8.4
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/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -33,11 +33,52 @@ let priorRetroMessages: Array<{ role: string; content: string }> = [];
|
|
|
33
33
|
|
|
34
34
|
let mockWakeResult: { invoked: boolean; reason?: string } = { invoked: true };
|
|
35
35
|
let mockWakeThrows: Error | null = null;
|
|
36
|
-
let wakeCalls: Array<{
|
|
36
|
+
let wakeCalls: Array<{
|
|
37
|
+
conversationId: string;
|
|
38
|
+
hint: string;
|
|
39
|
+
opts: Record<string, unknown>;
|
|
40
|
+
}> = [];
|
|
37
41
|
let bootstrappedConversationId = "bg-conv-new";
|
|
38
42
|
let bootstrapCalls: Array<{ forkParentConversationId?: string }> = [];
|
|
39
43
|
let deletedConversationIds: string[] = [];
|
|
40
44
|
|
|
45
|
+
// Fork-path mocks. Flag off by default so legacy-path tests stay untouched.
|
|
46
|
+
let forkFlagEnabled = false;
|
|
47
|
+
let forkedConversationId = "fork-conv-1";
|
|
48
|
+
let forkCalls: Array<{
|
|
49
|
+
conversationId: string;
|
|
50
|
+
throughMessageId?: string;
|
|
51
|
+
source: string;
|
|
52
|
+
title: string;
|
|
53
|
+
conversationType?: string;
|
|
54
|
+
groupId?: string;
|
|
55
|
+
}> = [];
|
|
56
|
+
let addMessageCalls: Array<{
|
|
57
|
+
conversationId: string;
|
|
58
|
+
role: string;
|
|
59
|
+
content: string;
|
|
60
|
+
metadata: unknown;
|
|
61
|
+
}> = [];
|
|
62
|
+
|
|
63
|
+
// Per-conversation overrides for getConversation. Lets fork-path tests stage
|
|
64
|
+
// a fork-kind prior retrospective row alongside the default legacy stub.
|
|
65
|
+
type ConversationStub = {
|
|
66
|
+
source: string;
|
|
67
|
+
forkParentMessageId: string | null;
|
|
68
|
+
title?: string;
|
|
69
|
+
};
|
|
70
|
+
let conversationOverrides: Record<string, ConversationStub> = {};
|
|
71
|
+
|
|
72
|
+
// Per-conversation overrides for getMessages so fork-path tests can return
|
|
73
|
+
// fork-shaped message rows (with metadata stamps + createdAt boundaries).
|
|
74
|
+
type StubMessage = {
|
|
75
|
+
role: string;
|
|
76
|
+
content: string;
|
|
77
|
+
createdAt: number;
|
|
78
|
+
metadata: string | null;
|
|
79
|
+
};
|
|
80
|
+
let messagesByConversationId: Record<string, StubMessage[]> = {};
|
|
81
|
+
|
|
41
82
|
mock.module("../memory-retrospective-state.js", () => ({
|
|
42
83
|
getRetrospectiveState: (_id: string) => mockState,
|
|
43
84
|
upsertRetrospectiveState: (args: {
|
|
@@ -55,16 +96,61 @@ mock.module("../memory-retrospective-state.js", () => ({
|
|
|
55
96
|
mock.module("../conversation-crud.js", () => ({
|
|
56
97
|
getMessagesAfter: (_id: string, _afterId: string | null) => newMessages,
|
|
57
98
|
getMessages: (id: string) => {
|
|
99
|
+
if (messagesByConversationId[id]) return messagesByConversationId[id];
|
|
58
100
|
if (id === priorRetroId) return priorRetroMessages;
|
|
59
101
|
return [];
|
|
60
102
|
},
|
|
61
103
|
findMostRecentRetrospectiveFor: (_id: string) =>
|
|
62
104
|
priorRetroId ? { id: priorRetroId } : null,
|
|
105
|
+
// The fork path calls `getConversation(sourceConversationId)` to read the
|
|
106
|
+
// source's title for the fork title. `collectPriorRetrospectiveRemembers`
|
|
107
|
+
// also calls it with the prior retro id to discriminate legacy vs fork
|
|
108
|
+
// sources — for that id return a legacy-shaped row by default so existing
|
|
109
|
+
// tests exercise the unchanged extract-everything code path.
|
|
110
|
+
// `conversationOverrides` lets per-test setup stage fork-kind priors.
|
|
111
|
+
getConversation: (id: string) => {
|
|
112
|
+
if (conversationOverrides[id]) return conversationOverrides[id];
|
|
113
|
+
if (id === priorRetroId) {
|
|
114
|
+
return {
|
|
115
|
+
source: "memory-retrospective",
|
|
116
|
+
forkParentMessageId: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
source: "user",
|
|
121
|
+
forkParentMessageId: null,
|
|
122
|
+
title: "Source conversation",
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
forkConversation: (params: {
|
|
126
|
+
conversationId: string;
|
|
127
|
+
throughMessageId?: string;
|
|
128
|
+
source: string;
|
|
129
|
+
title: string;
|
|
130
|
+
conversationType?: string;
|
|
131
|
+
groupId?: string;
|
|
132
|
+
}) => {
|
|
133
|
+
forkCalls.push(params);
|
|
134
|
+
return { id: forkedConversationId };
|
|
135
|
+
},
|
|
136
|
+
addMessage: async (
|
|
137
|
+
conversationId: string,
|
|
138
|
+
role: string,
|
|
139
|
+
content: string,
|
|
140
|
+
metadata: unknown,
|
|
141
|
+
) => {
|
|
142
|
+
addMessageCalls.push({ conversationId, role, content, metadata });
|
|
143
|
+
},
|
|
63
144
|
deleteConversation: (id: string) => {
|
|
64
145
|
deletedConversationIds.push(id);
|
|
65
146
|
},
|
|
66
147
|
}));
|
|
67
148
|
|
|
149
|
+
mock.module("../../config/assistant-feature-flags.js", () => ({
|
|
150
|
+
isAssistantFeatureFlagEnabled: (flag: string) =>
|
|
151
|
+
flag === "memory-retrospective-fork" && forkFlagEnabled,
|
|
152
|
+
}));
|
|
153
|
+
|
|
68
154
|
let transcriptFormatterCalls: Array<{
|
|
69
155
|
messageIds: string[];
|
|
70
156
|
timeZone?: string;
|
|
@@ -117,11 +203,14 @@ mock.module("../../daemon/trust-context.js", () => ({
|
|
|
117
203
|
}));
|
|
118
204
|
|
|
119
205
|
mock.module("../../runtime/agent-wake.js", () => ({
|
|
120
|
-
wakeAgentForOpportunity: async (
|
|
121
|
-
conversationId: string;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
206
|
+
wakeAgentForOpportunity: async (
|
|
207
|
+
opts: { conversationId: string; hint: string } & Record<string, unknown>,
|
|
208
|
+
) => {
|
|
209
|
+
wakeCalls.push({
|
|
210
|
+
conversationId: opts.conversationId,
|
|
211
|
+
hint: opts.hint,
|
|
212
|
+
opts,
|
|
213
|
+
});
|
|
125
214
|
if (mockWakeThrows) throw mockWakeThrows;
|
|
126
215
|
return mockWakeResult;
|
|
127
216
|
},
|
|
@@ -200,6 +289,12 @@ describe("memoryRetrospectiveJob", () => {
|
|
|
200
289
|
transcriptFormatterCalls = [];
|
|
201
290
|
mockAssistantName = "Bob";
|
|
202
291
|
mockUserName = "Alice";
|
|
292
|
+
forkFlagEnabled = false;
|
|
293
|
+
forkedConversationId = "fork-conv-1";
|
|
294
|
+
forkCalls = [];
|
|
295
|
+
addMessageCalls = [];
|
|
296
|
+
conversationOverrides = {};
|
|
297
|
+
messagesByConversationId = {};
|
|
203
298
|
});
|
|
204
299
|
|
|
205
300
|
test("first-run happy path: no state row, no prior retrospective, both pointer fields set on success", async () => {
|
|
@@ -408,4 +503,223 @@ describe("memoryRetrospectiveJob", () => {
|
|
|
408
503
|
const hint = wakeCalls[0]!.hint;
|
|
409
504
|
expect(hint).toContain("<\u200B/already_remembered>");
|
|
410
505
|
});
|
|
506
|
+
|
|
507
|
+
test("fork path: persisted instruction is stamped with hidden: true so the UI list serializer drops it", async () => {
|
|
508
|
+
forkFlagEnabled = true;
|
|
509
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
510
|
+
|
|
511
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
512
|
+
expect(addMessageCalls[0]!.conversationId).toBe("fork-conv-1");
|
|
513
|
+
expect(addMessageCalls[0]!.role).toBe("user");
|
|
514
|
+
expect(addMessageCalls[0]!.metadata).toEqual({
|
|
515
|
+
kind: "memory_retrospective_instruction",
|
|
516
|
+
hidden: true,
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("fork path: forked retrospective is bucketed as background under the retrospective group", async () => {
|
|
521
|
+
forkFlagEnabled = true;
|
|
522
|
+
const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
523
|
+
|
|
524
|
+
expect(outcome.kind).toBe("invoked");
|
|
525
|
+
expect(forkCalls).toHaveLength(1);
|
|
526
|
+
expect(forkCalls[0]!.conversationType).toBe("background");
|
|
527
|
+
expect(forkCalls[0]!.groupId).toBe("system:background");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("fork path: wake opts include suppressWakeSurface so clients don't render an empty wake card on top of the '(Retrospective)' fork", async () => {
|
|
531
|
+
forkFlagEnabled = true;
|
|
532
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
533
|
+
|
|
534
|
+
expect(forkCalls).toHaveLength(1);
|
|
535
|
+
expect(wakeCalls).toHaveLength(1);
|
|
536
|
+
expect(wakeCalls[0]!.conversationId).toBe("fork-conv-1");
|
|
537
|
+
const opts = wakeCalls[0]!.opts;
|
|
538
|
+
expect(opts.suppressWakeSurface).toBe(true);
|
|
539
|
+
// Sanity: the other fork-specific opts the handler relies on are still set.
|
|
540
|
+
expect(opts.skipHintInjection).toBe(true);
|
|
541
|
+
expect(opts.suppressAutoCompaction).toBe(true);
|
|
542
|
+
expect(opts.hintRole).toBe("user");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("fork path: fork is pinned to the computed cutoffMessageId so late-arriving messages don't sneak into this run", async () => {
|
|
546
|
+
// Without `throughMessageId`, the fork snapshots the latest source
|
|
547
|
+
// message at fork time. If a new user/assistant turn lands between the
|
|
548
|
+
// slice read and the fork, this run would process the late turn while
|
|
549
|
+
// state advances only to `cutoffMessageId`, causing the next
|
|
550
|
+
// retrospective to reprocess it.
|
|
551
|
+
forkFlagEnabled = true;
|
|
552
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
553
|
+
|
|
554
|
+
expect(forkCalls).toHaveLength(1);
|
|
555
|
+
expect(forkCalls[0]!.throughMessageId).toBe("m3");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test("fork path: prior fork-kind retrospective with nested-fork ancestry still surfaces its post-fork remembers in <already_remembered>", async () => {
|
|
559
|
+
// The source conversation was itself a fork. Its assistant messages
|
|
560
|
+
// therefore carry `forkSourceMessageId` values pointing at the
|
|
561
|
+
// ANCESTOR's message ids — not at the new fork's `forkParentMessageId`.
|
|
562
|
+
// The boundary detector must locate the boundary by scanning for the
|
|
563
|
+
// last metadata stamp regardless of value, not by equality against
|
|
564
|
+
// `forkParentMessageId` (which would miss every copied row and lose
|
|
565
|
+
// dedup context).
|
|
566
|
+
forkFlagEnabled = true;
|
|
567
|
+
priorRetroId = "prior-fork-retro-1";
|
|
568
|
+
|
|
569
|
+
// The fork's `forkParentMessageId` is the source conv's tip ("m-src-2"),
|
|
570
|
+
// but the cloned messages preserve ancestor stamps ("m-ancestor-*").
|
|
571
|
+
conversationOverrides[priorRetroId] = {
|
|
572
|
+
source: "memory-retrospective-fork",
|
|
573
|
+
forkParentMessageId: "m-src-2",
|
|
574
|
+
};
|
|
575
|
+
messagesByConversationId[priorRetroId] = [
|
|
576
|
+
// Copied prefix — note metadata stamps point at the ANCESTOR, not
|
|
577
|
+
// `forkParentMessageId`. The old detector would return null here.
|
|
578
|
+
{
|
|
579
|
+
role: "user",
|
|
580
|
+
content: JSON.stringify([{ type: "text", text: "hi" }]),
|
|
581
|
+
createdAt: 1000,
|
|
582
|
+
metadata: JSON.stringify({ forkSourceMessageId: "m-ancestor-1" }),
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
role: "assistant",
|
|
586
|
+
// An inline `remember` from the source conv (should NOT leak into
|
|
587
|
+
// dedup baseline — it's part of the copied prefix, not the post-fork
|
|
588
|
+
// retrospective tail).
|
|
589
|
+
content: JSON.stringify([
|
|
590
|
+
{
|
|
591
|
+
type: "tool_use",
|
|
592
|
+
name: "remember",
|
|
593
|
+
input: { content: "source-inline save — must be excluded" },
|
|
594
|
+
},
|
|
595
|
+
]),
|
|
596
|
+
createdAt: 2000,
|
|
597
|
+
metadata: JSON.stringify({ forkSourceMessageId: "m-ancestor-2" }),
|
|
598
|
+
},
|
|
599
|
+
// Post-fork instruction (no forkSourceMessageId) + the wake's tail
|
|
600
|
+
// assistant turn with the retrospective's own remember call.
|
|
601
|
+
{
|
|
602
|
+
role: "user",
|
|
603
|
+
content: JSON.stringify([
|
|
604
|
+
{ type: "text", text: "Retrospective instruction" },
|
|
605
|
+
]),
|
|
606
|
+
createdAt: 3000,
|
|
607
|
+
metadata: JSON.stringify({
|
|
608
|
+
kind: "memory_retrospective_instruction",
|
|
609
|
+
hidden: true,
|
|
610
|
+
}),
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
role: "assistant",
|
|
614
|
+
content: JSON.stringify([
|
|
615
|
+
{
|
|
616
|
+
type: "tool_use",
|
|
617
|
+
name: "remember",
|
|
618
|
+
input: { content: "retrospective save — must be included" },
|
|
619
|
+
},
|
|
620
|
+
]),
|
|
621
|
+
createdAt: 4000,
|
|
622
|
+
metadata: null,
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
627
|
+
|
|
628
|
+
// The fork path persists the prompt as a user-role message, not via the
|
|
629
|
+
// wake's hint. Pull the rendered text block out of the persisted JSON.
|
|
630
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
631
|
+
const blocks = JSON.parse(addMessageCalls[0]!.content) as Array<{
|
|
632
|
+
type: string;
|
|
633
|
+
text: string;
|
|
634
|
+
}>;
|
|
635
|
+
const instructionText = blocks[0]!.text;
|
|
636
|
+
expect(instructionText).toContain(
|
|
637
|
+
"- retrospective save — must be included",
|
|
638
|
+
);
|
|
639
|
+
expect(instructionText).not.toContain("source-inline save");
|
|
640
|
+
// Sanity: the "first retrospective" sentinel should not appear — we
|
|
641
|
+
// located dedup context.
|
|
642
|
+
expect(instructionText).not.toContain(
|
|
643
|
+
"(none — this is your first retrospective over this conversation)",
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("fork path: prior fork-kind retrospective with no copied messages degrades to empty dedup", async () => {
|
|
648
|
+
// Corrupted/empty fork-kind prior: no message carries
|
|
649
|
+
// `forkSourceMessageId`. The detector should return null and the
|
|
650
|
+
// handler should treat dedup as empty rather than dumping everything
|
|
651
|
+
// (which would leak any pre-fork content into the baseline).
|
|
652
|
+
forkFlagEnabled = true;
|
|
653
|
+
priorRetroId = "prior-fork-retro-2";
|
|
654
|
+
|
|
655
|
+
conversationOverrides[priorRetroId] = {
|
|
656
|
+
source: "memory-retrospective-fork",
|
|
657
|
+
forkParentMessageId: "m-src-2",
|
|
658
|
+
};
|
|
659
|
+
messagesByConversationId[priorRetroId] = [
|
|
660
|
+
{
|
|
661
|
+
role: "assistant",
|
|
662
|
+
content: JSON.stringify([
|
|
663
|
+
{
|
|
664
|
+
type: "tool_use",
|
|
665
|
+
name: "remember",
|
|
666
|
+
input: { content: "would-be-leaked save" },
|
|
667
|
+
},
|
|
668
|
+
]),
|
|
669
|
+
createdAt: 1000,
|
|
670
|
+
metadata: null,
|
|
671
|
+
},
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
675
|
+
|
|
676
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
677
|
+
const blocks = JSON.parse(addMessageCalls[0]!.content) as Array<{
|
|
678
|
+
type: string;
|
|
679
|
+
text: string;
|
|
680
|
+
}>;
|
|
681
|
+
const instructionText = blocks[0]!.text;
|
|
682
|
+
expect(instructionText).not.toContain("- would-be-leaked save");
|
|
683
|
+
expect(instructionText).toContain(
|
|
684
|
+
"(none — this is your first retrospective over this conversation)",
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("fork path: prompt anchors review window at first turn_context current_time and disambiguates first-pass vs incremental", async () => {
|
|
689
|
+
forkFlagEnabled = true;
|
|
690
|
+
// Stage a user turn whose content carries a turn_context current_time
|
|
691
|
+
// block — the handler should anchor the prompt at that timestamp.
|
|
692
|
+
newMessages = [
|
|
693
|
+
{
|
|
694
|
+
id: "m1",
|
|
695
|
+
createdAt: Date.parse("2026-05-11T10:00:00Z"),
|
|
696
|
+
role: "user",
|
|
697
|
+
content: JSON.stringify([
|
|
698
|
+
{
|
|
699
|
+
type: "text",
|
|
700
|
+
text: "<turn_context>\ncurrent_time: 2026-05-11T10:00:00-07:00\n</turn_context>\n\nhi",
|
|
701
|
+
},
|
|
702
|
+
]),
|
|
703
|
+
},
|
|
704
|
+
// Wake's response — no turn_context, not used as anchor.
|
|
705
|
+
{
|
|
706
|
+
id: "m2",
|
|
707
|
+
createdAt: Date.parse("2026-05-11T10:05:00Z"),
|
|
708
|
+
role: "assistant",
|
|
709
|
+
content: JSON.stringify([{ type: "text", text: "hello" }]),
|
|
710
|
+
},
|
|
711
|
+
] as Array<{ id: string; createdAt: number } & Record<string, unknown>>;
|
|
712
|
+
|
|
713
|
+
// Incremental run — `lastProcessedMessageId` already set.
|
|
714
|
+
mockState = {
|
|
715
|
+
conversationId: "src-conv-1",
|
|
716
|
+
lastProcessedMessageId: "prev-msg",
|
|
717
|
+
lastRunAt: Date.now() - 60 * 60 * 1000,
|
|
718
|
+
};
|
|
719
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
720
|
+
|
|
721
|
+
expect(addMessageCalls).toHaveLength(1);
|
|
722
|
+
expect(forkCalls).toHaveLength(1);
|
|
723
|
+
expect(forkCalls[0]!.throughMessageId).toBe("m2");
|
|
724
|
+
});
|
|
411
725
|
});
|
|
@@ -50,7 +50,7 @@ import { ensureGroupMigration } from "./conversation-group-migration.js";
|
|
|
50
50
|
import { getDb, getSqliteFrom } from "./db-connection.js";
|
|
51
51
|
import { forkGraphMemoryState } from "./graph/graph-memory-state-store.js";
|
|
52
52
|
import { indexMessageNow } from "./indexer.js";
|
|
53
|
-
import {
|
|
53
|
+
import { MEMORY_RETROSPECTIVE_SOURCES } from "./memory-retrospective-constants.js";
|
|
54
54
|
import { forkRetrospectiveState } from "./memory-retrospective-state.js";
|
|
55
55
|
import { rawExec, rawGet, rawRun } from "./raw-query.js";
|
|
56
56
|
import {
|
|
@@ -190,6 +190,7 @@ export interface ConversationRow {
|
|
|
190
190
|
contextSummary: string | null;
|
|
191
191
|
contextCompactedMessageCount: number;
|
|
192
192
|
contextCompactedAt: number | null;
|
|
193
|
+
cleanedAt: number | null;
|
|
193
194
|
slackContextCompactionWatermarkTs: string | null;
|
|
194
195
|
slackContextCompactionWatermarkAt: number | null;
|
|
195
196
|
conversationType: string;
|
|
@@ -206,6 +207,7 @@ export interface ConversationRow {
|
|
|
206
207
|
inferenceProfile: string | null;
|
|
207
208
|
inferenceProfileSessionId: string | null;
|
|
208
209
|
inferenceProfileExpiresAt: number | null;
|
|
210
|
+
lastNotifiedInferenceProfile: string | null;
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
export const parseConversation = createRowMapper<
|
|
@@ -222,6 +224,7 @@ export const parseConversation = createRowMapper<
|
|
|
222
224
|
contextSummary: "contextSummary",
|
|
223
225
|
contextCompactedMessageCount: "contextCompactedMessageCount",
|
|
224
226
|
contextCompactedAt: "contextCompactedAt",
|
|
227
|
+
cleanedAt: "cleanedAt",
|
|
225
228
|
slackContextCompactionWatermarkTs: "slackContextCompactionWatermarkTs",
|
|
226
229
|
slackContextCompactionWatermarkAt: "slackContextCompactionWatermarkAt",
|
|
227
230
|
conversationType: "conversationType",
|
|
@@ -238,6 +241,7 @@ export const parseConversation = createRowMapper<
|
|
|
238
241
|
inferenceProfile: "inferenceProfile",
|
|
239
242
|
inferenceProfileSessionId: "inferenceProfileSessionId",
|
|
240
243
|
inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
|
|
244
|
+
lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
|
|
241
245
|
});
|
|
242
246
|
|
|
243
247
|
export interface MessageRow {
|
|
@@ -482,7 +486,7 @@ export function findMostRecentRetrospectiveFor(
|
|
|
482
486
|
.from(conversations)
|
|
483
487
|
.where(
|
|
484
488
|
and(
|
|
485
|
-
|
|
489
|
+
inArray(conversations.source, MEMORY_RETROSPECTIVE_SOURCES),
|
|
486
490
|
eq(conversations.forkParentConversationId, currentId),
|
|
487
491
|
),
|
|
488
492
|
)
|
|
@@ -535,6 +539,32 @@ function getConversationGroupId(conversationId: string): string | null {
|
|
|
535
539
|
export function forkConversation(params: {
|
|
536
540
|
conversationId: string;
|
|
537
541
|
throughMessageId?: string;
|
|
542
|
+
/**
|
|
543
|
+
* Override the fork's `source` column. Defaults to the standard
|
|
544
|
+
* `createConversation` default (`"user"`). Used by fork-based memory
|
|
545
|
+
* retrospectives to mark the fork as a retrospective artifact distinct
|
|
546
|
+
* from a user-initiated fork, so dedup and cleanup queries can scope
|
|
547
|
+
* correctly.
|
|
548
|
+
*/
|
|
549
|
+
source?: string;
|
|
550
|
+
/**
|
|
551
|
+
* Optional title for the fork. Defaults to `<parent title> (Fork)`.
|
|
552
|
+
*/
|
|
553
|
+
title?: string;
|
|
554
|
+
/**
|
|
555
|
+
* Override the fork's `conversationType` column. Defaults to `"standard"`.
|
|
556
|
+
* Used by fork-based memory retrospectives to bucket the fork as a
|
|
557
|
+
* `"background"` conversation so it doesn't surface in the user's
|
|
558
|
+
* conversation list.
|
|
559
|
+
*/
|
|
560
|
+
conversationType?: ConversationCreateType;
|
|
561
|
+
/**
|
|
562
|
+
* Override the fork's `groupId`. Defaults to the parent conversation's
|
|
563
|
+
* group (or `"system:all"` when the parent has none). Used by fork-based
|
|
564
|
+
* memory retrospectives to route the fork into a dedicated background
|
|
565
|
+
* group.
|
|
566
|
+
*/
|
|
567
|
+
groupId?: string;
|
|
538
568
|
}): ConversationRow {
|
|
539
569
|
const { conversationId, throughMessageId } = params;
|
|
540
570
|
const db = getDb();
|
|
@@ -576,8 +606,19 @@ export function forkConversation(params: {
|
|
|
576
606
|
copyBoundaryIndex >= 0
|
|
577
607
|
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
578
608
|
: ([] as MessageRow[]);
|
|
609
|
+
|
|
610
|
+
// Inherit /clean state only when the fork boundary is at-or-after the
|
|
611
|
+
// clean event. Pre-clean forks branch from history that pre-dates the
|
|
612
|
+
// clean, so the marker would be a no-op and is misleading to copy.
|
|
613
|
+
const sourceCleanedAt = sourceConversation.cleanedAt ?? null;
|
|
614
|
+
const boundaryMessageCreatedAt = messagesToCopy.at(-1)?.createdAt ?? null;
|
|
615
|
+
const inheritsCleanedAt =
|
|
616
|
+
sourceCleanedAt != null &&
|
|
617
|
+
boundaryMessageCreatedAt != null &&
|
|
618
|
+
boundaryMessageCreatedAt >= sourceCleanedAt;
|
|
579
619
|
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
580
|
-
const forkTitle =
|
|
620
|
+
const forkTitle =
|
|
621
|
+
params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
581
622
|
|
|
582
623
|
// Collect disk-sync work to run after the transaction commits.
|
|
583
624
|
const diskSyncQueue: Array<{
|
|
@@ -597,8 +638,9 @@ export function forkConversation(params: {
|
|
|
597
638
|
const forkedConversation = db.transaction(() => {
|
|
598
639
|
const fc = createConversation({
|
|
599
640
|
title: forkTitle,
|
|
600
|
-
conversationType: "standard",
|
|
601
|
-
groupId: parentGroupId ?? "system:all",
|
|
641
|
+
conversationType: params.conversationType ?? "standard",
|
|
642
|
+
groupId: params.groupId ?? parentGroupId ?? "system:all",
|
|
643
|
+
...(params.source != null ? { source: params.source } : {}),
|
|
602
644
|
});
|
|
603
645
|
|
|
604
646
|
db.update(conversations)
|
|
@@ -620,6 +662,7 @@ export function forkConversation(params: {
|
|
|
620
662
|
slackContextCompactionWatermarkAt: preserveSourceCompactionState
|
|
621
663
|
? sourceConversation.slackContextCompactionWatermarkAt
|
|
622
664
|
: null,
|
|
665
|
+
cleanedAt: inheritsCleanedAt ? sourceCleanedAt : null,
|
|
623
666
|
inferenceProfile: sourceConversation.inferenceProfile,
|
|
624
667
|
})
|
|
625
668
|
.where(eq(conversations.id, fc.id))
|
|
@@ -1229,10 +1272,14 @@ interface PaginatedMessagesResult {
|
|
|
1229
1272
|
hasMore: boolean;
|
|
1230
1273
|
}
|
|
1231
1274
|
|
|
1275
|
+
const PAGINATION_CHUNK_MIN = 50;
|
|
1276
|
+
const PAGINATION_SCAN_CAP = 10_000;
|
|
1277
|
+
|
|
1232
1278
|
export function getMessagesPaginated(
|
|
1233
1279
|
conversationId: string,
|
|
1234
1280
|
limit: number | undefined,
|
|
1235
1281
|
beforeTimestamp?: number,
|
|
1282
|
+
filter?: (row: MessageRow) => boolean,
|
|
1236
1283
|
): PaginatedMessagesResult {
|
|
1237
1284
|
const db = getDb();
|
|
1238
1285
|
|
|
@@ -1248,51 +1295,69 @@ export function getMessagesPaginated(
|
|
|
1248
1295
|
.orderBy(asc(messages.createdAt))
|
|
1249
1296
|
.all()
|
|
1250
1297
|
.map(parseMessage);
|
|
1251
|
-
return {
|
|
1298
|
+
return {
|
|
1299
|
+
messages: filter ? rows.filter(filter) : rows,
|
|
1300
|
+
hasMore: false,
|
|
1301
|
+
};
|
|
1252
1302
|
}
|
|
1253
1303
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1304
|
+
// Walk pages newest→oldest, applying `filter` in TS (metadata parsing is
|
|
1305
|
+
// JSON, not a structured column). Keep fetching until we have `limit + 1`
|
|
1306
|
+
// visible rows or the DB is exhausted, so `hasMore` and the cursor reflect
|
|
1307
|
+
// the visible page rather than the unfiltered row count. Without this loop,
|
|
1308
|
+
// a fully-hidden page returns `{ messages: [], hasMore: true }` with no
|
|
1309
|
+
// cursor, which stalls the web client's older-page fetch.
|
|
1310
|
+
let cursorCreatedAt = beforeTimestamp;
|
|
1311
|
+
let cursorMessageId: string | undefined;
|
|
1312
|
+
const visible: MessageRow[] = [];
|
|
1313
|
+
const chunkSize = Math.max(limit + 1, PAGINATION_CHUNK_MIN);
|
|
1314
|
+
// Bound the work a single request can do when `filter` rejects nearly every
|
|
1315
|
+
// row — otherwise a pathological filter against a huge conversation would
|
|
1316
|
+
// tie up a connection for thousands of roundtrips.
|
|
1317
|
+
let rowsScanned = 0;
|
|
1318
|
+
|
|
1319
|
+
while (visible.length < limit + 1 && rowsScanned < PAGINATION_SCAN_CAP) {
|
|
1320
|
+
const cursorPredicate =
|
|
1321
|
+
cursorCreatedAt === undefined
|
|
1322
|
+
? undefined
|
|
1323
|
+
: cursorMessageId === undefined
|
|
1324
|
+
? lt(messages.createdAt, cursorCreatedAt)
|
|
1325
|
+
: or(
|
|
1326
|
+
lt(messages.createdAt, cursorCreatedAt),
|
|
1327
|
+
and(
|
|
1328
|
+
eq(messages.createdAt, cursorCreatedAt),
|
|
1329
|
+
lt(messages.id, cursorMessageId),
|
|
1330
|
+
),
|
|
1331
|
+
);
|
|
1258
1332
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1333
|
+
const chunk = db
|
|
1334
|
+
.select()
|
|
1335
|
+
.from(messages)
|
|
1336
|
+
.where(and(eq(messages.conversationId, conversationId), cursorPredicate))
|
|
1337
|
+
.orderBy(desc(messages.createdAt), desc(messages.id))
|
|
1338
|
+
.limit(chunkSize)
|
|
1339
|
+
.all()
|
|
1340
|
+
.map(parseMessage);
|
|
1267
1341
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1342
|
+
if (chunk.length === 0) break;
|
|
1343
|
+
rowsScanned += chunk.length;
|
|
1344
|
+
|
|
1345
|
+
for (const row of chunk) {
|
|
1346
|
+
if (!filter || filter(row)) visible.push(row);
|
|
1347
|
+
if (visible.length >= limit + 1) break;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (chunk.length < chunkSize) break;
|
|
1351
|
+
const lastRow = chunk[chunk.length - 1];
|
|
1352
|
+
cursorCreatedAt = lastRow.createdAt;
|
|
1353
|
+
cursorMessageId = lastRow.id;
|
|
1271
1354
|
}
|
|
1272
|
-
rows.reverse();
|
|
1273
1355
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1356
|
+
const hasMore = visible.length > limit;
|
|
1357
|
+
if (hasMore) visible.splice(limit);
|
|
1358
|
+
visible.reverse();
|
|
1276
1359
|
|
|
1277
|
-
|
|
1278
|
-
conversationId: string,
|
|
1279
|
-
beforeTimestamp: number,
|
|
1280
|
-
): number {
|
|
1281
|
-
const db = getDb();
|
|
1282
|
-
const row = db
|
|
1283
|
-
.select({ createdAt: messages.createdAt })
|
|
1284
|
-
.from(messages)
|
|
1285
|
-
.where(
|
|
1286
|
-
and(
|
|
1287
|
-
eq(messages.conversationId, conversationId),
|
|
1288
|
-
eq(messages.role, "assistant"),
|
|
1289
|
-
lt(messages.createdAt, beforeTimestamp),
|
|
1290
|
-
),
|
|
1291
|
-
)
|
|
1292
|
-
.orderBy(desc(messages.createdAt))
|
|
1293
|
-
.limit(1)
|
|
1294
|
-
.get();
|
|
1295
|
-
return row?.createdAt ?? 0;
|
|
1360
|
+
return { messages: visible, hasMore };
|
|
1296
1361
|
}
|
|
1297
1362
|
|
|
1298
1363
|
export function getLastUserTimestampBefore(
|
|
@@ -1386,6 +1451,20 @@ export function updateConversationContextWindow(
|
|
|
1386
1451
|
.run();
|
|
1387
1452
|
}
|
|
1388
1453
|
|
|
1454
|
+
export function setConversationCleanedAt(
|
|
1455
|
+
id: string,
|
|
1456
|
+
cleanedAt: number | null,
|
|
1457
|
+
): void {
|
|
1458
|
+
const db = getDb();
|
|
1459
|
+
db.update(conversations)
|
|
1460
|
+
.set({
|
|
1461
|
+
cleanedAt,
|
|
1462
|
+
updatedAt: Date.now(),
|
|
1463
|
+
})
|
|
1464
|
+
.where(eq(conversations.id, id))
|
|
1465
|
+
.run();
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1389
1468
|
export function updateConversationSlackContextWatermark(
|
|
1390
1469
|
id: string,
|
|
1391
1470
|
watermarkTs: string,
|
|
@@ -1609,6 +1688,17 @@ export function getConversationOverrideProfile(
|
|
|
1609
1688
|
return getConversationOverrideProfileFromRow(getConversation(conversationId));
|
|
1610
1689
|
}
|
|
1611
1690
|
|
|
1691
|
+
export function setLastNotifiedInferenceProfile(
|
|
1692
|
+
conversationId: string,
|
|
1693
|
+
profileKey: string | null,
|
|
1694
|
+
): void {
|
|
1695
|
+
rawRun(
|
|
1696
|
+
"UPDATE conversations SET last_notified_inference_profile = ? WHERE id = ?",
|
|
1697
|
+
profileKey,
|
|
1698
|
+
conversationId,
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1612
1702
|
/**
|
|
1613
1703
|
* Delete all conversations, messages, and related data (tool invocations,
|
|
1614
1704
|
* memory segments, etc.) from the daemon database.
|