@vellumai/assistant 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +71 -8
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -163,6 +163,76 @@ export function projectAssistantMessage(params: {
|
|
|
163
163
|
.run();
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Seed a forked conversation's assistant-attention projection so copied
|
|
168
|
+
* assistant history is treated as already seen from the outset.
|
|
169
|
+
*/
|
|
170
|
+
export function seedForkedConversationAttention(params: {
|
|
171
|
+
conversationId: string;
|
|
172
|
+
latestAssistantMessageId: string | null;
|
|
173
|
+
latestAssistantMessageAt: number | null;
|
|
174
|
+
}): void {
|
|
175
|
+
const {
|
|
176
|
+
conversationId,
|
|
177
|
+
latestAssistantMessageId,
|
|
178
|
+
latestAssistantMessageAt,
|
|
179
|
+
} = params;
|
|
180
|
+
|
|
181
|
+
if (!latestAssistantMessageId || latestAssistantMessageAt == null) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const db = getDb();
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const existing = db
|
|
188
|
+
.select()
|
|
189
|
+
.from(conversationAssistantAttentionState)
|
|
190
|
+
.where(
|
|
191
|
+
eq(conversationAssistantAttentionState.conversationId, conversationId),
|
|
192
|
+
)
|
|
193
|
+
.get();
|
|
194
|
+
|
|
195
|
+
if (!existing) {
|
|
196
|
+
db.insert(conversationAssistantAttentionState)
|
|
197
|
+
.values({
|
|
198
|
+
conversationId,
|
|
199
|
+
latestAssistantMessageId,
|
|
200
|
+
latestAssistantMessageAt,
|
|
201
|
+
lastSeenAssistantMessageId: latestAssistantMessageId,
|
|
202
|
+
lastSeenAssistantMessageAt: latestAssistantMessageAt,
|
|
203
|
+
lastSeenEventAt: null,
|
|
204
|
+
lastSeenConfidence: null,
|
|
205
|
+
lastSeenSignalType: null,
|
|
206
|
+
lastSeenSourceChannel: null,
|
|
207
|
+
lastSeenSource: null,
|
|
208
|
+
lastSeenEvidenceText: null,
|
|
209
|
+
createdAt: now,
|
|
210
|
+
updatedAt: now,
|
|
211
|
+
})
|
|
212
|
+
.run();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
db.update(conversationAssistantAttentionState)
|
|
217
|
+
.set({
|
|
218
|
+
latestAssistantMessageId,
|
|
219
|
+
latestAssistantMessageAt,
|
|
220
|
+
lastSeenAssistantMessageId: latestAssistantMessageId,
|
|
221
|
+
lastSeenAssistantMessageAt: latestAssistantMessageAt,
|
|
222
|
+
lastSeenEventAt: null,
|
|
223
|
+
lastSeenConfidence: null,
|
|
224
|
+
lastSeenSignalType: null,
|
|
225
|
+
lastSeenSourceChannel: null,
|
|
226
|
+
lastSeenSource: null,
|
|
227
|
+
lastSeenEvidenceText: null,
|
|
228
|
+
updatedAt: now,
|
|
229
|
+
})
|
|
230
|
+
.where(
|
|
231
|
+
eq(conversationAssistantAttentionState.conversationId, conversationId),
|
|
232
|
+
)
|
|
233
|
+
.run();
|
|
234
|
+
}
|
|
235
|
+
|
|
166
236
|
// ── recordConversationSeenSignal ─────────────────────────────────────
|
|
167
237
|
|
|
168
238
|
/**
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
and,
|
|
5
|
+
asc,
|
|
6
|
+
count,
|
|
7
|
+
desc,
|
|
8
|
+
eq,
|
|
9
|
+
gt,
|
|
10
|
+
inArray,
|
|
11
|
+
isNull,
|
|
12
|
+
lte,
|
|
13
|
+
sql,
|
|
14
|
+
} from "drizzle-orm";
|
|
2
15
|
import { v4 as uuid } from "uuid";
|
|
3
16
|
import { z } from "zod";
|
|
4
17
|
|
|
@@ -7,10 +20,24 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
|
|
|
7
20
|
import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from "../channels/types.js";
|
|
8
21
|
import { getConfig } from "../config/loader.js";
|
|
9
22
|
import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
|
|
23
|
+
import { UserError } from "../util/errors.js";
|
|
10
24
|
import { getLogger } from "../util/logger.js";
|
|
25
|
+
import { getConversationsDir } from "../util/platform.js";
|
|
11
26
|
import { createRowMapper } from "../util/row-mapper.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
27
|
+
import {
|
|
28
|
+
deleteOrphanAttachments,
|
|
29
|
+
linkAttachmentToMessage,
|
|
30
|
+
} from "./attachments-store.js";
|
|
31
|
+
import {
|
|
32
|
+
projectAssistantMessage,
|
|
33
|
+
seedForkedConversationAttention,
|
|
34
|
+
} from "./conversation-attention-store.js";
|
|
35
|
+
import {
|
|
36
|
+
initConversationDir,
|
|
37
|
+
removeConversationDir,
|
|
38
|
+
syncMessageToDisk,
|
|
39
|
+
updateMetaFile,
|
|
40
|
+
} from "./conversation-disk-view.js";
|
|
14
41
|
import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
|
|
15
42
|
import { getDb, rawAll, rawExec, rawGet, rawRun } from "./db.js";
|
|
16
43
|
import { indexMessageNow } from "./indexer.js";
|
|
@@ -47,6 +74,9 @@ const subagentNotificationSchema = z.object({
|
|
|
47
74
|
conversationId: z.string().optional(),
|
|
48
75
|
});
|
|
49
76
|
|
|
77
|
+
export const PRIVATE_CONVERSATION_FORK_ERROR =
|
|
78
|
+
"Private conversations cannot be forked";
|
|
79
|
+
|
|
50
80
|
export const messageMetadataSchema = z
|
|
51
81
|
.object({
|
|
52
82
|
userMessageChannel: channelIdSchema.optional(),
|
|
@@ -67,11 +97,42 @@ export const messageMetadataSchema = z
|
|
|
67
97
|
provenanceGuardianExternalUserId: z.string().optional(),
|
|
68
98
|
provenanceRequesterIdentifier: z.string().optional(),
|
|
69
99
|
automated: z.boolean().optional(),
|
|
100
|
+
forkSourceMessageId: z.string().optional(),
|
|
101
|
+
/** Image source paths from desktop attachments, keyed by filename. */
|
|
102
|
+
imageSourcePaths: z.record(z.string(), z.string()).optional(),
|
|
70
103
|
})
|
|
71
104
|
.passthrough();
|
|
72
105
|
|
|
73
106
|
export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
|
|
74
107
|
|
|
108
|
+
function cloneForkMessageMetadata(
|
|
109
|
+
metadata: string | null,
|
|
110
|
+
sourceMessageId: string,
|
|
111
|
+
): string {
|
|
112
|
+
if (!metadata) {
|
|
113
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(metadata);
|
|
118
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
119
|
+
const sourceRecord = parsed as Record<string, unknown>;
|
|
120
|
+
const forkSourceMessageId =
|
|
121
|
+
typeof sourceRecord.forkSourceMessageId === "string"
|
|
122
|
+
? sourceRecord.forkSourceMessageId
|
|
123
|
+
: sourceMessageId;
|
|
124
|
+
return JSON.stringify({
|
|
125
|
+
...sourceRecord,
|
|
126
|
+
forkSourceMessageId,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Fall through to source-only metadata.
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return JSON.stringify({ forkSourceMessageId: sourceMessageId });
|
|
134
|
+
}
|
|
135
|
+
|
|
75
136
|
/**
|
|
76
137
|
* Extract provenance metadata fields from a TrustContext.
|
|
77
138
|
* When no guardian context is provided, defaults to 'unknown' because the
|
|
@@ -106,6 +167,8 @@ export interface ConversationRow {
|
|
|
106
167
|
memoryScopeId: string;
|
|
107
168
|
originChannel: string | null;
|
|
108
169
|
originInterface: string | null;
|
|
170
|
+
forkParentConversationId: string | null;
|
|
171
|
+
forkParentMessageId: string | null;
|
|
109
172
|
isAutoTitle: number;
|
|
110
173
|
scheduleJobId: string | null;
|
|
111
174
|
}
|
|
@@ -129,6 +192,8 @@ export const parseConversation = createRowMapper<
|
|
|
129
192
|
memoryScopeId: "memoryScopeId",
|
|
130
193
|
originChannel: "originChannel",
|
|
131
194
|
originInterface: "originInterface",
|
|
195
|
+
forkParentConversationId: "forkParentConversationId",
|
|
196
|
+
forkParentMessageId: "forkParentMessageId",
|
|
132
197
|
isAutoTitle: "isAutoTitle",
|
|
133
198
|
scheduleJobId: "scheduleJobId",
|
|
134
199
|
});
|
|
@@ -232,6 +297,8 @@ export function createConversation(
|
|
|
232
297
|
}
|
|
233
298
|
}
|
|
234
299
|
|
|
300
|
+
initConversationDir({ ...conversation, originChannel: null });
|
|
301
|
+
|
|
235
302
|
return conversation;
|
|
236
303
|
}
|
|
237
304
|
|
|
@@ -258,6 +325,187 @@ export function getConversationMemoryScopeId(conversationId: string): string {
|
|
|
258
325
|
return conv?.memoryScopeId ?? "default";
|
|
259
326
|
}
|
|
260
327
|
|
|
328
|
+
export function forkConversation(params: {
|
|
329
|
+
conversationId: string;
|
|
330
|
+
throughMessageId?: string;
|
|
331
|
+
}): ConversationRow {
|
|
332
|
+
const { conversationId, throughMessageId } = params;
|
|
333
|
+
const db = getDb();
|
|
334
|
+
const sourceConversation = getConversation(conversationId);
|
|
335
|
+
|
|
336
|
+
if (!sourceConversation) {
|
|
337
|
+
throw new UserError(`Conversation ${conversationId} not found`);
|
|
338
|
+
}
|
|
339
|
+
if (sourceConversation.conversationType === "private") {
|
|
340
|
+
throw new UserError(PRIVATE_CONVERSATION_FORK_ERROR);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const sourceMessages = getMessages(conversationId);
|
|
344
|
+
|
|
345
|
+
if (sourceMessages.length === 0) {
|
|
346
|
+
throw new UserError(
|
|
347
|
+
`Conversation ${conversationId} has no persisted messages to fork`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const copyBoundaryIndex =
|
|
352
|
+
throughMessageId == null
|
|
353
|
+
? sourceMessages.length - 1
|
|
354
|
+
: sourceMessages.findIndex((message) => message.id === throughMessageId);
|
|
355
|
+
|
|
356
|
+
if (throughMessageId != null && copyBoundaryIndex === -1) {
|
|
357
|
+
throw new UserError(
|
|
358
|
+
`Message ${throughMessageId} does not belong to conversation ${conversationId}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const visibleWindowStartIndex = Math.max(
|
|
363
|
+
0,
|
|
364
|
+
Math.min(
|
|
365
|
+
sourceConversation.contextCompactedMessageCount,
|
|
366
|
+
sourceMessages.length,
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
const preserveSourceCompactionState =
|
|
370
|
+
copyBoundaryIndex >= visibleWindowStartIndex;
|
|
371
|
+
|
|
372
|
+
const messagesToCopy =
|
|
373
|
+
copyBoundaryIndex >= 0
|
|
374
|
+
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
375
|
+
: ([] as MessageRow[]);
|
|
376
|
+
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
377
|
+
const forkTitle = `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
378
|
+
const forkedConversation = createConversation({
|
|
379
|
+
title: forkTitle,
|
|
380
|
+
conversationType: "standard",
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
db.update(conversations)
|
|
384
|
+
.set({
|
|
385
|
+
forkParentConversationId: sourceConversation.id,
|
|
386
|
+
forkParentMessageId,
|
|
387
|
+
contextSummary: preserveSourceCompactionState
|
|
388
|
+
? sourceConversation.contextSummary
|
|
389
|
+
: null,
|
|
390
|
+
contextCompactedMessageCount: preserveSourceCompactionState
|
|
391
|
+
? sourceConversation.contextCompactedMessageCount
|
|
392
|
+
: 0,
|
|
393
|
+
contextCompactedAt: preserveSourceCompactionState
|
|
394
|
+
? sourceConversation.contextCompactedAt
|
|
395
|
+
: null,
|
|
396
|
+
})
|
|
397
|
+
.where(eq(conversations.id, forkedConversation.id))
|
|
398
|
+
.run();
|
|
399
|
+
|
|
400
|
+
const forkedMessageIds = new Map<string, string>();
|
|
401
|
+
let latestForkedAssistant: { messageId: string; messageAt: number } | null =
|
|
402
|
+
null;
|
|
403
|
+
|
|
404
|
+
for (const message of messagesToCopy) {
|
|
405
|
+
const forkedMessageId = uuid();
|
|
406
|
+
db.insert(messages)
|
|
407
|
+
.values({
|
|
408
|
+
id: forkedMessageId,
|
|
409
|
+
conversationId: forkedConversation.id,
|
|
410
|
+
role: message.role,
|
|
411
|
+
content: message.content,
|
|
412
|
+
createdAt: message.createdAt,
|
|
413
|
+
metadata: cloneForkMessageMetadata(message.metadata, message.id),
|
|
414
|
+
})
|
|
415
|
+
.run();
|
|
416
|
+
forkedMessageIds.set(message.id, forkedMessageId);
|
|
417
|
+
|
|
418
|
+
if (message.role === "assistant") {
|
|
419
|
+
latestForkedAssistant = {
|
|
420
|
+
messageId: forkedMessageId,
|
|
421
|
+
messageAt: message.createdAt,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const attachmentIdMap = new Map<string, string>();
|
|
427
|
+
for (const message of messagesToCopy) {
|
|
428
|
+
const forkedMessageId = forkedMessageIds.get(message.id);
|
|
429
|
+
if (!forkedMessageId) continue;
|
|
430
|
+
|
|
431
|
+
const attachmentLinks = db
|
|
432
|
+
.select({
|
|
433
|
+
attachmentId: messageAttachments.attachmentId,
|
|
434
|
+
position: messageAttachments.position,
|
|
435
|
+
})
|
|
436
|
+
.from(messageAttachments)
|
|
437
|
+
.where(eq(messageAttachments.messageId, message.id))
|
|
438
|
+
.orderBy(messageAttachments.position)
|
|
439
|
+
.all();
|
|
440
|
+
const uncachedAttachmentLinks = attachmentLinks.filter(
|
|
441
|
+
(link) => !attachmentIdMap.has(link.attachmentId),
|
|
442
|
+
);
|
|
443
|
+
const stagingMessageId = uncachedAttachmentLinks.length > 0 ? uuid() : null;
|
|
444
|
+
|
|
445
|
+
if (stagingMessageId) {
|
|
446
|
+
db.insert(messages)
|
|
447
|
+
.values({
|
|
448
|
+
id: stagingMessageId,
|
|
449
|
+
conversationId: forkedConversation.id,
|
|
450
|
+
role: message.role,
|
|
451
|
+
content: "",
|
|
452
|
+
createdAt: message.createdAt,
|
|
453
|
+
metadata: null,
|
|
454
|
+
})
|
|
455
|
+
.run();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const link of attachmentLinks) {
|
|
459
|
+
const cachedAttachmentId = attachmentIdMap.get(link.attachmentId);
|
|
460
|
+
if (cachedAttachmentId) {
|
|
461
|
+
db.insert(messageAttachments)
|
|
462
|
+
.values({
|
|
463
|
+
id: uuid(),
|
|
464
|
+
messageId: forkedMessageId,
|
|
465
|
+
attachmentId: cachedAttachmentId,
|
|
466
|
+
position: link.position,
|
|
467
|
+
createdAt: Date.now(),
|
|
468
|
+
})
|
|
469
|
+
.run();
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const scopedAttachmentId = linkAttachmentToMessage(
|
|
474
|
+
stagingMessageId ?? forkedMessageId,
|
|
475
|
+
link.attachmentId,
|
|
476
|
+
link.position,
|
|
477
|
+
);
|
|
478
|
+
attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (stagingMessageId) {
|
|
482
|
+
relinkAttachments([stagingMessageId], forkedMessageId);
|
|
483
|
+
db.delete(messages).where(eq(messages.id, stagingMessageId)).run();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
syncMessageToDisk(
|
|
487
|
+
forkedConversation.id,
|
|
488
|
+
forkedMessageId,
|
|
489
|
+
forkedConversation.createdAt,
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
seedForkedConversationAttention({
|
|
494
|
+
conversationId: forkedConversation.id,
|
|
495
|
+
latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
|
|
496
|
+
latestAssistantMessageAt: latestForkedAssistant?.messageAt ?? null,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
const persistedFork = getConversation(forkedConversation.id);
|
|
500
|
+
if (!persistedFork) {
|
|
501
|
+
throw new Error(
|
|
502
|
+
`Failed to load forked conversation ${forkedConversation.id} after creation`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return persistedFork;
|
|
507
|
+
}
|
|
508
|
+
|
|
261
509
|
/**
|
|
262
510
|
* Delete a conversation and all its messages, cleaning up orphaned memory
|
|
263
511
|
* artifacts (items, embeddings). Returns segment and orphaned item IDs so
|
|
@@ -267,6 +515,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
267
515
|
const db = getDb();
|
|
268
516
|
const result: DeletedMemoryIds = { segmentIds: [], orphanedItemIds: [] };
|
|
269
517
|
|
|
518
|
+
// Capture createdAt before the transaction deletes the row — needed to
|
|
519
|
+
// resolve the conversation's disk-view directory path after deletion.
|
|
520
|
+
const convBeforeDelete = getConversation(id);
|
|
521
|
+
const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
|
|
522
|
+
|
|
270
523
|
db.transaction((tx) => {
|
|
271
524
|
// Collect all message IDs for this conversation.
|
|
272
525
|
const messageRows = tx
|
|
@@ -357,6 +610,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
|
|
|
357
610
|
tx.delete(conversations).where(eq(conversations.id, id)).run();
|
|
358
611
|
});
|
|
359
612
|
|
|
613
|
+
// Remove the conversation's disk-view directory after the DB transaction
|
|
614
|
+
if (createdAtForDiskCleanup != null) {
|
|
615
|
+
removeConversationDir(id, createdAtForDiskCleanup);
|
|
616
|
+
}
|
|
617
|
+
|
|
360
618
|
return result;
|
|
361
619
|
}
|
|
362
620
|
|
|
@@ -759,6 +1017,12 @@ export function updateConversationTitle(
|
|
|
759
1017
|
const set: Record<string, unknown> = { title, updatedAt: Date.now() };
|
|
760
1018
|
if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
|
|
761
1019
|
db.update(conversations).set(set).where(eq(conversations.id, id)).run();
|
|
1020
|
+
|
|
1021
|
+
// Update disk view meta.json with the new title
|
|
1022
|
+
const conv = getConversation(id);
|
|
1023
|
+
if (conv) {
|
|
1024
|
+
updateMetaFile(conv);
|
|
1025
|
+
}
|
|
762
1026
|
}
|
|
763
1027
|
|
|
764
1028
|
export function updateConversationUsage(
|
|
@@ -864,6 +1128,14 @@ export function clearAll(): { conversations: number; messages: number } {
|
|
|
864
1128
|
);
|
|
865
1129
|
}
|
|
866
1130
|
|
|
1131
|
+
// Clear the disk-view conversations directory and recreate it empty
|
|
1132
|
+
try {
|
|
1133
|
+
rmSync(getConversationsDir(), { recursive: true, force: true });
|
|
1134
|
+
mkdirSync(getConversationsDir(), { recursive: true });
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
log.warn({ err }, "clearAll: failed to reset conversations directory");
|
|
1137
|
+
}
|
|
1138
|
+
|
|
867
1139
|
return { conversations: convCount, messages: msgCount };
|
|
868
1140
|
}
|
|
869
1141
|
|
|
@@ -1232,3 +1504,170 @@ export function getDisplayMetaForConversations(
|
|
|
1232
1504
|
}
|
|
1233
1505
|
return result;
|
|
1234
1506
|
}
|
|
1507
|
+
|
|
1508
|
+
// ── Turn boundary resolution ─────────────────────────────────────────
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Returns `true` if a message is a tool-result user message — i.e. its
|
|
1512
|
+
* role is "user" and its content is a JSON array where every block has
|
|
1513
|
+
* `type === "tool_result"`. These synthetic user messages are injected
|
|
1514
|
+
* between assistant messages within a single agent turn and should NOT
|
|
1515
|
+
* be treated as turn boundaries.
|
|
1516
|
+
*/
|
|
1517
|
+
function isToolResultMessage(role: string, content: string): boolean {
|
|
1518
|
+
if (role !== "user") return false;
|
|
1519
|
+
try {
|
|
1520
|
+
const parsed = JSON.parse(content);
|
|
1521
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return false;
|
|
1522
|
+
return parsed.every(
|
|
1523
|
+
(block: unknown) =>
|
|
1524
|
+
block != null &&
|
|
1525
|
+
typeof block === "object" &&
|
|
1526
|
+
(block as Record<string, unknown>).type === "tool_result",
|
|
1527
|
+
);
|
|
1528
|
+
} catch {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Resolve all assistant message IDs that belong to the same agent turn
|
|
1535
|
+
* as the given `messageId`. A "turn" is bounded by:
|
|
1536
|
+
* - The start of the conversation, or
|
|
1537
|
+
* - A user message whose content is NOT a tool_result array.
|
|
1538
|
+
*
|
|
1539
|
+
* Within a multi-step agent loop, the pattern is:
|
|
1540
|
+
* user msg → assistant A1 → user (tool_result) → assistant A2 → ...
|
|
1541
|
+
* All assistant messages from A1 through the queried message (and beyond,
|
|
1542
|
+
* up to the next real user message) are part of the same turn.
|
|
1543
|
+
*
|
|
1544
|
+
* Returns `[messageId]` as a fallback if the message is not found,
|
|
1545
|
+
* preserving backward compatibility for callers.
|
|
1546
|
+
*/
|
|
1547
|
+
export function getAssistantMessageIdsInTurn(messageId: string): string[] {
|
|
1548
|
+
const db = getDb();
|
|
1549
|
+
|
|
1550
|
+
// Look up the target message to get its conversationId and createdAt.
|
|
1551
|
+
const target = getMessageById(messageId);
|
|
1552
|
+
if (!target) return [messageId];
|
|
1553
|
+
|
|
1554
|
+
// Walk backward from the target message to find the turn boundary.
|
|
1555
|
+
// Limit to 50 rows — sufficient for even aggressive tool-use loops.
|
|
1556
|
+
const backwardRows = db
|
|
1557
|
+
.select({
|
|
1558
|
+
id: messages.id,
|
|
1559
|
+
role: messages.role,
|
|
1560
|
+
content: messages.content,
|
|
1561
|
+
createdAt: messages.createdAt,
|
|
1562
|
+
})
|
|
1563
|
+
.from(messages)
|
|
1564
|
+
.where(
|
|
1565
|
+
and(
|
|
1566
|
+
eq(messages.conversationId, target.conversationId),
|
|
1567
|
+
lte(messages.createdAt, target.createdAt),
|
|
1568
|
+
),
|
|
1569
|
+
)
|
|
1570
|
+
.orderBy(desc(messages.createdAt))
|
|
1571
|
+
.limit(50)
|
|
1572
|
+
.all();
|
|
1573
|
+
|
|
1574
|
+
const assistantIds: string[] = [];
|
|
1575
|
+
let boundaryCreatedAt: number | null = null;
|
|
1576
|
+
|
|
1577
|
+
for (const row of backwardRows) {
|
|
1578
|
+
if (row.role === "assistant") {
|
|
1579
|
+
assistantIds.push(row.id);
|
|
1580
|
+
} else if (row.role === "user") {
|
|
1581
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
1582
|
+
// Tool-result user message — still within the same turn, continue.
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
// Real user message — this is the turn boundary.
|
|
1586
|
+
boundaryCreatedAt = row.createdAt;
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// Walk forward from the target to collect any later assistant messages
|
|
1592
|
+
// still within the same turn (e.g. when querying an intermediate
|
|
1593
|
+
// message like A1 in a multi-step turn A1 → tool_result → A2).
|
|
1594
|
+
const forwardRows = db
|
|
1595
|
+
.select({
|
|
1596
|
+
id: messages.id,
|
|
1597
|
+
role: messages.role,
|
|
1598
|
+
content: messages.content,
|
|
1599
|
+
createdAt: messages.createdAt,
|
|
1600
|
+
})
|
|
1601
|
+
.from(messages)
|
|
1602
|
+
.where(
|
|
1603
|
+
and(
|
|
1604
|
+
eq(messages.conversationId, target.conversationId),
|
|
1605
|
+
gt(messages.createdAt, target.createdAt),
|
|
1606
|
+
),
|
|
1607
|
+
)
|
|
1608
|
+
.orderBy(asc(messages.createdAt))
|
|
1609
|
+
.limit(50)
|
|
1610
|
+
.all();
|
|
1611
|
+
|
|
1612
|
+
for (const row of forwardRows) {
|
|
1613
|
+
if (row.role === "assistant") {
|
|
1614
|
+
if (!assistantIds.includes(row.id)) {
|
|
1615
|
+
assistantIds.push(row.id);
|
|
1616
|
+
}
|
|
1617
|
+
} else if (row.role === "user") {
|
|
1618
|
+
if (isToolResultMessage(row.role, row.content)) {
|
|
1619
|
+
// Tool-result user message — still within the same turn.
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
// Real user message — end of the turn.
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Also query forward from the backward-walk boundary to pick up any
|
|
1628
|
+
// assistant messages between the boundary and the target that may have
|
|
1629
|
+
// been missed (e.g. due to the 50-row limit in the backward walk).
|
|
1630
|
+
if (boundaryCreatedAt != null) {
|
|
1631
|
+
const gapRows = db
|
|
1632
|
+
.select({
|
|
1633
|
+
id: messages.id,
|
|
1634
|
+
role: messages.role,
|
|
1635
|
+
createdAt: messages.createdAt,
|
|
1636
|
+
})
|
|
1637
|
+
.from(messages)
|
|
1638
|
+
.where(
|
|
1639
|
+
and(
|
|
1640
|
+
eq(messages.conversationId, target.conversationId),
|
|
1641
|
+
gt(messages.createdAt, boundaryCreatedAt),
|
|
1642
|
+
lte(messages.createdAt, target.createdAt),
|
|
1643
|
+
),
|
|
1644
|
+
)
|
|
1645
|
+
.orderBy(asc(messages.createdAt))
|
|
1646
|
+
.all();
|
|
1647
|
+
|
|
1648
|
+
for (const row of gapRows) {
|
|
1649
|
+
if (row.role === "assistant" && !assistantIds.includes(row.id)) {
|
|
1650
|
+
assistantIds.push(row.id);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Sort by createdAt to ensure stable ordering.
|
|
1656
|
+
// Re-fetch createdAt for all collected IDs so the sort is accurate.
|
|
1657
|
+
if (assistantIds.length <= 1) return assistantIds;
|
|
1658
|
+
|
|
1659
|
+
const idSet = new Set(assistantIds);
|
|
1660
|
+
const sorted = db
|
|
1661
|
+
.select({ id: messages.id, createdAt: messages.createdAt })
|
|
1662
|
+
.from(messages)
|
|
1663
|
+
.where(
|
|
1664
|
+
and(
|
|
1665
|
+
eq(messages.conversationId, target.conversationId),
|
|
1666
|
+
inArray(messages.id, [...idSet]),
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
.orderBy(asc(messages.createdAt))
|
|
1670
|
+
.all();
|
|
1671
|
+
|
|
1672
|
+
return sorted.map((r) => r.id);
|
|
1673
|
+
}
|