@vellumai/assistant 0.6.0 → 0.6.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +4 -27
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/group-crud.ts +25 -9
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +41 -278
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -18
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -91,10 +91,7 @@ export function backfillMessageIdOnLogs(
|
|
|
91
91
|
* for a conversation), this targets only the given log rows — safe for
|
|
92
92
|
* concurrent watch/assistant turns.
|
|
93
93
|
*/
|
|
94
|
-
export function setMessageIdOnLogs(
|
|
95
|
-
logIds: string[],
|
|
96
|
-
messageId: string,
|
|
97
|
-
): void {
|
|
94
|
+
export function setMessageIdOnLogs(logIds: string[], messageId: string): void {
|
|
98
95
|
if (logIds.length === 0) return;
|
|
99
96
|
const db = getDb();
|
|
100
97
|
db.update(llmRequestLogs)
|
|
@@ -185,18 +182,75 @@ function selectOrphanedLogsInRange(
|
|
|
185
182
|
.all();
|
|
186
183
|
}
|
|
187
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Find unlinked logs — logs with `message_id IS NULL` that haven't been
|
|
187
|
+
* backfilled yet. This covers the race where the client queries the inspector
|
|
188
|
+
* before `backfillMessageIdOnLogs` runs in `handleMessageComplete`, or when
|
|
189
|
+
* the backfill fails silently (try-catch in the agent loop).
|
|
190
|
+
*
|
|
191
|
+
* Scoped to a single conversation and a time range to avoid cross-turn bleed.
|
|
192
|
+
*/
|
|
193
|
+
function selectUnlinkedLogsInRange(
|
|
194
|
+
conversationId: string,
|
|
195
|
+
startTime: number,
|
|
196
|
+
endTime: number,
|
|
197
|
+
): LogRow[] {
|
|
198
|
+
if (endTime <= startTime) return [];
|
|
199
|
+
const db = getDb();
|
|
200
|
+
return db
|
|
201
|
+
.select({
|
|
202
|
+
id: llmRequestLogs.id,
|
|
203
|
+
conversationId: llmRequestLogs.conversationId,
|
|
204
|
+
messageId: llmRequestLogs.messageId,
|
|
205
|
+
provider: llmRequestLogs.provider,
|
|
206
|
+
requestPayload: llmRequestLogs.requestPayload,
|
|
207
|
+
responsePayload: llmRequestLogs.responsePayload,
|
|
208
|
+
createdAt: llmRequestLogs.createdAt,
|
|
209
|
+
})
|
|
210
|
+
.from(llmRequestLogs)
|
|
211
|
+
.where(
|
|
212
|
+
and(
|
|
213
|
+
eq(llmRequestLogs.conversationId, conversationId),
|
|
214
|
+
gte(llmRequestLogs.createdAt, startTime),
|
|
215
|
+
lte(llmRequestLogs.createdAt, endTime),
|
|
216
|
+
isNull(llmRequestLogs.messageId),
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
.orderBy(llmRequestLogs.createdAt)
|
|
220
|
+
.all();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function getRequestLogById(logId: string): LogRow | null {
|
|
224
|
+
const db = getDb();
|
|
225
|
+
return (
|
|
226
|
+
db
|
|
227
|
+
.select({
|
|
228
|
+
id: llmRequestLogs.id,
|
|
229
|
+
conversationId: llmRequestLogs.conversationId,
|
|
230
|
+
messageId: llmRequestLogs.messageId,
|
|
231
|
+
provider: llmRequestLogs.provider,
|
|
232
|
+
requestPayload: llmRequestLogs.requestPayload,
|
|
233
|
+
responsePayload: llmRequestLogs.responsePayload,
|
|
234
|
+
createdAt: llmRequestLogs.createdAt,
|
|
235
|
+
})
|
|
236
|
+
.from(llmRequestLogs)
|
|
237
|
+
.where(eq(llmRequestLogs.id, logId))
|
|
238
|
+
.get() ?? null
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
export function getRequestLogsByMessageId(messageId: string): LogRow[] {
|
|
189
243
|
// Resolve all assistant message IDs in the same turn so the inspector
|
|
190
244
|
// shows every LLM call from the entire agent turn, not just the queried message.
|
|
191
245
|
const turnMessageIds = getAssistantMessageIdsInTurn(messageId);
|
|
192
246
|
const turnLogs = selectLogsByMessageIds(turnMessageIds);
|
|
193
247
|
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
248
|
+
// Recovery: find logs in the turn's time window that the message-ID-based
|
|
249
|
+
// query missed. Two categories:
|
|
250
|
+
// 1. Orphaned — messageId references a deleted message (retry/deleteLastExchange).
|
|
251
|
+
// 2. Unlinked — messageId is still NULL because the backfill hasn't run yet
|
|
252
|
+
// or failed silently. This covers the race where the client queries the
|
|
253
|
+
// inspector before handleMessageComplete persists and backfills.
|
|
200
254
|
const message = getMessageById(messageId);
|
|
201
255
|
if (message) {
|
|
202
256
|
const bounds = getTurnTimeBounds(message.conversationId, message.createdAt);
|
|
@@ -206,10 +260,16 @@ export function getRequestLogsByMessageId(messageId: string): LogRow[] {
|
|
|
206
260
|
bounds.startTime,
|
|
207
261
|
bounds.endTime,
|
|
208
262
|
);
|
|
209
|
-
|
|
263
|
+
const unlinkedLogs = selectUnlinkedLogsInRange(
|
|
264
|
+
message.conversationId,
|
|
265
|
+
bounds.startTime,
|
|
266
|
+
bounds.endTime,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (orphanedLogs.length > 0 || unlinkedLogs.length > 0) {
|
|
210
270
|
const seen = new Set(turnLogs.map((l) => l.id));
|
|
211
271
|
const merged = [...turnLogs];
|
|
212
|
-
for (const log of orphanedLogs) {
|
|
272
|
+
for (const log of [...orphanedLogs, ...unlinkedLogs]) {
|
|
213
273
|
if (!seen.has(log.id)) {
|
|
214
274
|
merged.push(log);
|
|
215
275
|
seen.add(log.id);
|
|
@@ -218,6 +278,30 @@ export function getRequestLogsByMessageId(messageId: string): LogRow[] {
|
|
|
218
278
|
merged.sort(
|
|
219
279
|
(a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id),
|
|
220
280
|
);
|
|
281
|
+
|
|
282
|
+
// Opportunistically backfill recovered unlinked logs so future queries
|
|
283
|
+
// hit the fast indexed-by-messageId path. Guard with isNull so this
|
|
284
|
+
// recovery path never overwrites a messageId already set by an
|
|
285
|
+
// authoritative caller (e.g. watch-notifier).
|
|
286
|
+
if (unlinkedLogs.length > 0 && turnMessageIds.length > 0) {
|
|
287
|
+
try {
|
|
288
|
+
const db = getDb();
|
|
289
|
+
const ids = unlinkedLogs.map((l) => l.id);
|
|
290
|
+
const targetMessageId = turnMessageIds[turnMessageIds.length - 1]!;
|
|
291
|
+
db.update(llmRequestLogs)
|
|
292
|
+
.set({ messageId: targetMessageId })
|
|
293
|
+
.where(
|
|
294
|
+
and(
|
|
295
|
+
inArray(llmRequestLogs.id, ids),
|
|
296
|
+
isNull(llmRequestLogs.messageId),
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
.run();
|
|
300
|
+
} catch {
|
|
301
|
+
// non-fatal — the recovery already returned the right data
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
221
305
|
return merged;
|
|
222
306
|
}
|
|
223
307
|
}
|
|
@@ -23,9 +23,12 @@ export interface RecordMemoryRecallLogParams {
|
|
|
23
23
|
topCandidatesJson: unknown;
|
|
24
24
|
injectedText?: string;
|
|
25
25
|
reason?: string;
|
|
26
|
+
queryContext?: string;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function recordMemoryRecallLog(
|
|
29
|
+
export function recordMemoryRecallLog(
|
|
30
|
+
params: RecordMemoryRecallLogParams,
|
|
31
|
+
): void {
|
|
29
32
|
const db = getDb();
|
|
30
33
|
db.insert(memoryRecallLogs)
|
|
31
34
|
.values({
|
|
@@ -51,6 +54,7 @@ export function recordMemoryRecallLog(params: RecordMemoryRecallLogParams): void
|
|
|
51
54
|
topCandidatesJson: JSON.stringify(params.topCandidatesJson),
|
|
52
55
|
injectedText: params.injectedText ?? null,
|
|
53
56
|
reason: params.reason ?? null,
|
|
57
|
+
queryContext: params.queryContext ?? null,
|
|
54
58
|
createdAt: Date.now(),
|
|
55
59
|
})
|
|
56
60
|
.run();
|
|
@@ -90,6 +94,47 @@ export interface MemoryRecallLog {
|
|
|
90
94
|
topCandidates: unknown;
|
|
91
95
|
injectedText: string | null;
|
|
92
96
|
reason: string | null;
|
|
97
|
+
queryContext: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Normalizes top-candidate entries from the stored SSE-event format
|
|
102
|
+
* (key/finalScore/semantic/recency/kind) to the inspector format expected
|
|
103
|
+
* by the Swift MemoryRecallCandidate struct (nodeId/score/semanticSimilarity/recencyBoost).
|
|
104
|
+
* Entries already in inspector format pass through unchanged.
|
|
105
|
+
*/
|
|
106
|
+
export function normalizeTopCandidates(raw: unknown): unknown {
|
|
107
|
+
if (!Array.isArray(raw)) return raw;
|
|
108
|
+
return raw.flatMap((entry: Record<string, unknown>) => {
|
|
109
|
+
if (!entry || typeof entry !== "object") return [];
|
|
110
|
+
|
|
111
|
+
// Start with a shallow copy, then apply field renames
|
|
112
|
+
const { key, finalScore, semantic, recency, kind: _kind, ...rest } = entry;
|
|
113
|
+
|
|
114
|
+
// nodeId: prefer existing nodeId, fall back to key
|
|
115
|
+
if (rest.nodeId === undefined && key !== undefined) {
|
|
116
|
+
rest.nodeId = key;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// score: prefer existing score, fall back to finalScore
|
|
120
|
+
if (rest.score === undefined && finalScore !== undefined) {
|
|
121
|
+
rest.score = finalScore;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// semanticSimilarity: prefer existing, fall back to semantic
|
|
125
|
+
if (rest.semanticSimilarity === undefined && semantic !== undefined) {
|
|
126
|
+
rest.semanticSimilarity = semantic;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// recencyBoost: prefer existing, fall back to recency
|
|
130
|
+
if (rest.recencyBoost === undefined && recency !== undefined) {
|
|
131
|
+
rest.recencyBoost = recency;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// kind is stripped (not in the Swift model) — already excluded via destructuring
|
|
135
|
+
|
|
136
|
+
return rest;
|
|
137
|
+
});
|
|
93
138
|
}
|
|
94
139
|
|
|
95
140
|
export function getMemoryRecallLogByMessageIds(
|
|
@@ -109,9 +154,7 @@ export function getMemoryRecallLogByMessageIds(
|
|
|
109
154
|
degraded: !!row.degraded,
|
|
110
155
|
provider: row.provider,
|
|
111
156
|
model: row.model,
|
|
112
|
-
degradation: row.degradationJson
|
|
113
|
-
? JSON.parse(row.degradationJson)
|
|
114
|
-
: null,
|
|
157
|
+
degradation: row.degradationJson ? JSON.parse(row.degradationJson) : null,
|
|
115
158
|
semanticHits: row.semanticHits,
|
|
116
159
|
mergedCount: row.mergedCount,
|
|
117
160
|
selectedCount: row.selectedCount,
|
|
@@ -121,8 +164,9 @@ export function getMemoryRecallLogByMessageIds(
|
|
|
121
164
|
sparseVectorUsed: !!row.sparseVectorUsed,
|
|
122
165
|
injectedTokens: row.injectedTokens,
|
|
123
166
|
latencyMs: row.latencyMs,
|
|
124
|
-
topCandidates: JSON.parse(row.topCandidatesJson),
|
|
167
|
+
topCandidates: normalizeTopCandidates(JSON.parse(row.topCandidatesJson)),
|
|
125
168
|
injectedText: row.injectedText,
|
|
126
169
|
reason: row.reason,
|
|
170
|
+
queryContext: row.queryContext,
|
|
127
171
|
};
|
|
128
172
|
}
|
|
@@ -6,13 +6,45 @@ import { getSqliteFrom } from "../db-connection.js";
|
|
|
6
6
|
*
|
|
7
7
|
* All consumers have been migrated to memory_graph_nodes (#22698).
|
|
8
8
|
* These tables are now dead weight.
|
|
9
|
+
*
|
|
10
|
+
* Safety: only drops tables when they are empty or the tool-created-items
|
|
11
|
+
* migration has already copied relevant rows into memory_graph_nodes.
|
|
12
|
+
* Workspaces that haven't run migrateToolCreatedItems() yet keep the tables
|
|
13
|
+
* so data isn't silently lost.
|
|
9
14
|
*/
|
|
10
15
|
export function migrateDropMemoryItemsTables(database: DrizzleDb): void {
|
|
11
16
|
const raw = getSqliteFrom(database);
|
|
12
17
|
|
|
18
|
+
// Guard: verify tables are safe to drop (empty or already migrated).
|
|
19
|
+
try {
|
|
20
|
+
const row = raw
|
|
21
|
+
.prepare(
|
|
22
|
+
/*sql*/ `SELECT COUNT(*) as cnt FROM memory_items WHERE status = 'active'`,
|
|
23
|
+
)
|
|
24
|
+
.get() as { cnt: number } | undefined;
|
|
25
|
+
|
|
26
|
+
if (row && row.cnt > 0) {
|
|
27
|
+
// Tables have active rows — only drop if the migration checkpoint exists.
|
|
28
|
+
const checkpoint = raw
|
|
29
|
+
.prepare(
|
|
30
|
+
/*sql*/ `SELECT value FROM memory_checkpoints WHERE key = ?`,
|
|
31
|
+
)
|
|
32
|
+
.get("graph_bootstrap:migrated_tool_items") as
|
|
33
|
+
| { value: string }
|
|
34
|
+
| undefined;
|
|
35
|
+
|
|
36
|
+
if (!checkpoint?.value) {
|
|
37
|
+
// Data exists but hasn't been migrated — skip the drop to prevent data loss.
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// Table doesn't exist (fresh install) — proceed with drop (IF EXISTS is a no-op).
|
|
43
|
+
}
|
|
44
|
+
|
|
13
45
|
// Drop indexes first (idempotent — IF EXISTS).
|
|
14
46
|
raw.exec(
|
|
15
|
-
/*sql*/ `DROP INDEX IF EXISTS idx_memory_item_sources_memory_item_id
|
|
47
|
+
/*sql*/ `DROP INDEX IF EXISTS idx_memory_item_sources_memory_item_id`,
|
|
16
48
|
);
|
|
17
49
|
raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_memory_items_scope_id`);
|
|
18
50
|
raw.exec(/*sql*/ `DROP INDEX IF EXISTS idx_memory_items_fingerprint`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
export function migrateCreateMemoryGraphNodeEdits(database: DrizzleDb): void {
|
|
5
|
+
const raw = getSqliteFrom(database);
|
|
6
|
+
raw.exec(`
|
|
7
|
+
CREATE TABLE IF NOT EXISTS memory_graph_node_edits (
|
|
8
|
+
id TEXT PRIMARY KEY,
|
|
9
|
+
node_id TEXT NOT NULL REFERENCES memory_graph_nodes(id) ON DELETE CASCADE,
|
|
10
|
+
previous_content TEXT NOT NULL,
|
|
11
|
+
new_content TEXT NOT NULL,
|
|
12
|
+
source TEXT NOT NULL,
|
|
13
|
+
conversation_id TEXT,
|
|
14
|
+
created INTEGER NOT NULL
|
|
15
|
+
);
|
|
16
|
+
CREATE INDEX IF NOT EXISTS idx_graph_node_edits_node_id ON memory_graph_node_edits(node_id);
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_graph_node_edits_created ON memory_graph_node_edits(created);
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
4
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
5
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Remove image attachments that contain HTML error pages instead of actual
|
|
9
|
+
* image data. This can happen when a CDN (e.g. Slack) returns an HTML sign-in
|
|
10
|
+
* page due to a missing OAuth scope, and the gateway stores the response body
|
|
11
|
+
* as an image attachment.
|
|
12
|
+
*
|
|
13
|
+
* Handles both inline (data_base64) and on-disk (file_path) storage.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const HTML_MARKERS = ["<!doctype", "<html"];
|
|
17
|
+
|
|
18
|
+
function looksLikeHtml(bytes: Buffer): boolean {
|
|
19
|
+
// Strip leading BOM / whitespace
|
|
20
|
+
const text = bytes.toString("utf-8").trimStart().toLowerCase();
|
|
21
|
+
return HTML_MARKERS.some((marker) => text.startsWith(marker));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function migrateScrubCorruptedImageAttachments(
|
|
25
|
+
database: DrizzleDb,
|
|
26
|
+
): void {
|
|
27
|
+
withCrashRecovery(
|
|
28
|
+
database,
|
|
29
|
+
"migration_scrub_corrupted_image_attachments_v1",
|
|
30
|
+
() => {
|
|
31
|
+
const raw = getSqliteFrom(database);
|
|
32
|
+
|
|
33
|
+
function deleteCorruptedAttachment(
|
|
34
|
+
id: string,
|
|
35
|
+
filePath: string | null,
|
|
36
|
+
): void {
|
|
37
|
+
raw
|
|
38
|
+
.query(`DELETE FROM message_attachments WHERE attachment_id = ?`)
|
|
39
|
+
.run(id);
|
|
40
|
+
raw.query(`DELETE FROM attachments WHERE id = ?`).run(id);
|
|
41
|
+
|
|
42
|
+
if (filePath) {
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(filePath);
|
|
45
|
+
} catch {
|
|
46
|
+
// File already missing — ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(
|
|
51
|
+
`[scrub-corrupted-attachments] Removed corrupted attachment ${id}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Step A — Find and remove corrupted attachments stored inline (data_base64)
|
|
56
|
+
// Process in batches using rowid cursor to ensure all rows are scanned
|
|
57
|
+
// even when corrupted rows are non-contiguous.
|
|
58
|
+
const BATCH_SIZE = 100;
|
|
59
|
+
let lastRowid = 0;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const rows = raw
|
|
62
|
+
.query(
|
|
63
|
+
`SELECT rowid, id, data_base64, file_path FROM attachments
|
|
64
|
+
WHERE mime_type LIKE 'image/%'
|
|
65
|
+
AND data_base64 IS NOT NULL
|
|
66
|
+
AND data_base64 != ''
|
|
67
|
+
AND rowid > ?
|
|
68
|
+
ORDER BY rowid
|
|
69
|
+
LIMIT ?`,
|
|
70
|
+
)
|
|
71
|
+
.all(lastRowid, BATCH_SIZE) as Array<{
|
|
72
|
+
rowid: number;
|
|
73
|
+
id: string;
|
|
74
|
+
data_base64: string;
|
|
75
|
+
file_path: string | null;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
if (rows.length === 0) break;
|
|
79
|
+
|
|
80
|
+
for (const row of rows) {
|
|
81
|
+
lastRowid = row.rowid;
|
|
82
|
+
try {
|
|
83
|
+
const decoded = Buffer.from(
|
|
84
|
+
row.data_base64.slice(0, 200),
|
|
85
|
+
"base64",
|
|
86
|
+
);
|
|
87
|
+
if (looksLikeHtml(decoded)) {
|
|
88
|
+
deleteCorruptedAttachment(row.id, row.file_path);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Skip rows with invalid base64
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Step B — Find and remove corrupted attachments stored on disk (file_path)
|
|
97
|
+
// Disk-backed attachments are typically fewer; query all at once.
|
|
98
|
+
const diskRows = raw
|
|
99
|
+
.query(
|
|
100
|
+
`SELECT id, file_path FROM attachments
|
|
101
|
+
WHERE mime_type LIKE 'image/%'
|
|
102
|
+
AND file_path IS NOT NULL
|
|
103
|
+
AND (data_base64 IS NULL OR data_base64 = '')`,
|
|
104
|
+
)
|
|
105
|
+
.all() as Array<{ id: string; file_path: string }>;
|
|
106
|
+
|
|
107
|
+
for (const row of diskRows) {
|
|
108
|
+
try {
|
|
109
|
+
const bytes = readFileSync(row.file_path);
|
|
110
|
+
const head = bytes.subarray(0, 100);
|
|
111
|
+
if (looksLikeHtml(head)) {
|
|
112
|
+
deleteCorruptedAttachment(row.id, row.file_path);
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// File doesn't exist or can't be read — skip
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reverse: no-op.
|
|
124
|
+
*
|
|
125
|
+
* Corrupted data (HTML stored as image) has no value to restore.
|
|
126
|
+
*/
|
|
127
|
+
export function migrateScrubCorruptedImageAttachmentsDown(
|
|
128
|
+
_database: DrizzleDb,
|
|
129
|
+
): void {
|
|
130
|
+
// No-op — corrupted data (HTML stored as image) has no value to restore.
|
|
131
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Persist ConversationGraphMemory + InContextTracker state across eviction.
|
|
6
|
+
* Idempotent — uses CREATE TABLE IF NOT EXISTS.
|
|
7
|
+
*/
|
|
8
|
+
export function migrateCreateConversationGraphMemoryState(
|
|
9
|
+
database: DrizzleDb,
|
|
10
|
+
): void {
|
|
11
|
+
const raw = getSqliteFrom(database);
|
|
12
|
+
raw.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS conversation_graph_memory_state (
|
|
14
|
+
conversation_id TEXT PRIMARY KEY REFERENCES conversations(id) ON DELETE CASCADE,
|
|
15
|
+
state_json TEXT NOT NULL,
|
|
16
|
+
created_at INTEGER NOT NULL,
|
|
17
|
+
updated_at INTEGER NOT NULL
|
|
18
|
+
)
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Add last_message_at denormalized column to conversations for sorting by
|
|
6
|
+
* latest message timestamp instead of updatedAt (which is bumped by non-message
|
|
7
|
+
* events like title changes and context compaction).
|
|
8
|
+
*
|
|
9
|
+
* Idempotent — uses ALTER TABLE try/catch and IF NOT EXISTS guards.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateConversationsLastMessageAt(database: DrizzleDb): void {
|
|
12
|
+
const raw = getSqliteFrom(database);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
raw.exec(`ALTER TABLE conversations ADD COLUMN last_message_at INTEGER`);
|
|
16
|
+
} catch {
|
|
17
|
+
// Column already exists
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Backfill from the latest message in each conversation.
|
|
21
|
+
// Idempotent: re-running produces the same result.
|
|
22
|
+
raw.exec(`
|
|
23
|
+
UPDATE conversations
|
|
24
|
+
SET last_message_at = (
|
|
25
|
+
SELECT MAX(created_at) FROM messages
|
|
26
|
+
WHERE messages.conversation_id = conversations.id
|
|
27
|
+
)
|
|
28
|
+
WHERE last_message_at IS NULL
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
raw.exec(`
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_last_message_at
|
|
33
|
+
ON conversations(last_message_at)
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Strip thinking and redacted_thinking blocks from all assistant messages.
|
|
7
|
+
*
|
|
8
|
+
* Consolidated messages merge thinking blocks from different API responses,
|
|
9
|
+
* making their cryptographic signatures invalid. Previously the Anthropic
|
|
10
|
+
* provider stripped these on every request, mutating the conversation prefix
|
|
11
|
+
* and defeating prompt caching. This migration cleans them at rest so the
|
|
12
|
+
* provider no longer needs to strip, enabling append-only conversation
|
|
13
|
+
* history and stable prefix caching.
|
|
14
|
+
*
|
|
15
|
+
* Idempotent — safe to re-run.
|
|
16
|
+
*/
|
|
17
|
+
export function migrateStripThinkingFromConsolidated(
|
|
18
|
+
database: DrizzleDb,
|
|
19
|
+
): void {
|
|
20
|
+
withCrashRecovery(
|
|
21
|
+
database,
|
|
22
|
+
"migration_strip_thinking_from_consolidated_v1",
|
|
23
|
+
() => {
|
|
24
|
+
const raw = getSqliteFrom(database);
|
|
25
|
+
|
|
26
|
+
const BATCH_SIZE = 100;
|
|
27
|
+
let lastRowid = 0;
|
|
28
|
+
|
|
29
|
+
for (;;) {
|
|
30
|
+
const rows = raw
|
|
31
|
+
.query(
|
|
32
|
+
`SELECT rowid, id, content FROM messages
|
|
33
|
+
WHERE role = 'assistant'
|
|
34
|
+
AND rowid > ?
|
|
35
|
+
ORDER BY rowid
|
|
36
|
+
LIMIT ?`,
|
|
37
|
+
)
|
|
38
|
+
.all(lastRowid, BATCH_SIZE) as Array<{
|
|
39
|
+
rowid: number;
|
|
40
|
+
id: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
if (rows.length === 0) break;
|
|
45
|
+
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
lastRowid = row.rowid;
|
|
48
|
+
|
|
49
|
+
let blocks: Array<{ type: string }>;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(row.content);
|
|
52
|
+
if (!Array.isArray(parsed)) continue;
|
|
53
|
+
blocks = parsed;
|
|
54
|
+
} catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hasThinking = blocks.some(
|
|
59
|
+
(b) => b.type === "thinking" || b.type === "redacted_thinking",
|
|
60
|
+
);
|
|
61
|
+
if (!hasThinking) continue;
|
|
62
|
+
|
|
63
|
+
const stripped = blocks.filter(
|
|
64
|
+
(b) => b.type !== "thinking" && b.type !== "redacted_thinking",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Preserve at least one block so the message isn't empty.
|
|
68
|
+
const finalContent =
|
|
69
|
+
stripped.length > 0
|
|
70
|
+
? stripped
|
|
71
|
+
: [
|
|
72
|
+
{
|
|
73
|
+
type: "text" as const,
|
|
74
|
+
text: "\x00__PLACEHOLDER__[internal blocks omitted]",
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
raw
|
|
79
|
+
.query(`UPDATE messages SET content = ? WHERE id = ?`)
|
|
80
|
+
.run(JSON.stringify(finalContent), row.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
export function migrateScheduleReuseConversation(database: DrizzleDb): void {
|
|
5
|
+
const raw = getSqliteFrom(database);
|
|
6
|
+
try {
|
|
7
|
+
raw.exec(
|
|
8
|
+
`ALTER TABLE cron_jobs ADD COLUMN reuse_conversation INTEGER NOT NULL DEFAULT 0`,
|
|
9
|
+
);
|
|
10
|
+
} catch {
|
|
11
|
+
// Column already exists — nothing to do.
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
import { tableHasColumn } from "./schema-introspection.js";
|
|
4
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
5
|
+
|
|
6
|
+
const CHECKPOINT_KEY = "migration_memory_recall_logs_query_context_v1";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Add query_context column to memory_recall_logs to persist the query text
|
|
10
|
+
* that drove semantic search, enabling the inspector to show what was searched.
|
|
11
|
+
*/
|
|
12
|
+
export function migrateMemoryRecallLogsQueryContext(database: DrizzleDb): void {
|
|
13
|
+
withCrashRecovery(database, CHECKPOINT_KEY, () => {
|
|
14
|
+
if (!tableHasColumn(database, "memory_recall_logs", "query_context")) {
|
|
15
|
+
const raw = getSqliteFrom(database);
|
|
16
|
+
raw.exec(
|
|
17
|
+
`ALTER TABLE memory_recall_logs ADD COLUMN query_context TEXT`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
import { withCrashRecovery } from "./validate-migration-state.js";
|
|
4
|
+
|
|
5
|
+
const CHECKPOINT_KEY = "migration_llm_request_logs_created_at_index_v1";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add an index on `llm_request_logs.created_at` so time-range deletes
|
|
9
|
+
* (used by the log-pruning GC job) can scan efficiently without a full
|
|
10
|
+
* table scan.
|
|
11
|
+
*/
|
|
12
|
+
export function migrateLlmRequestLogsCreatedAtIndex(database: DrizzleDb): void {
|
|
13
|
+
withCrashRecovery(database, CHECKPOINT_KEY, () => {
|
|
14
|
+
const raw = getSqliteFrom(database);
|
|
15
|
+
raw.exec(
|
|
16
|
+
`CREATE INDEX IF NOT EXISTS idx_llm_request_logs_created_at ON llm_request_logs(created_at)`,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -146,6 +146,14 @@ export { migrateCreateMemoryGraphTables } from "./202-memory-graph-tables.js";
|
|
|
146
146
|
export { migrateDropMemoryItemsTables } from "./203-drop-memory-items-tables.js";
|
|
147
147
|
export { migrateRenameMemoryGraphTypeValues } from "./204-rename-memory-graph-type-values.js";
|
|
148
148
|
export { migrateMemoryGraphImageRefs } from "./205-memory-graph-image-refs.js";
|
|
149
|
+
export { migrateCreateMemoryGraphNodeEdits } from "./206-memory-graph-node-edits.js";
|
|
150
|
+
export { migrateScrubCorruptedImageAttachments } from "./206-scrub-corrupted-image-attachments.js";
|
|
151
|
+
export { migrateCreateConversationGraphMemoryState } from "./207-conversation-graph-memory-state.js";
|
|
152
|
+
export { migrateConversationsLastMessageAt } from "./208-conversations-last-message-at.js";
|
|
153
|
+
export { migrateStripThinkingFromConsolidated } from "./209-strip-thinking-from-consolidated.js";
|
|
154
|
+
export { migrateScheduleReuseConversation } from "./210-schedule-reuse-conversation.js";
|
|
155
|
+
export { migrateMemoryRecallLogsQueryContext } from "./211-memory-recall-logs-query-context.js";
|
|
156
|
+
export { migrateLlmRequestLogsCreatedAtIndex } from "./212-llm-request-logs-created-at-index.js";
|
|
149
157
|
export {
|
|
150
158
|
MIGRATION_REGISTRY,
|
|
151
159
|
type MigrationRegistryEntry,
|
|
@@ -40,6 +40,7 @@ import { migrateBackfillAudioAttachmentMimeTypesDown } from "./191-backfill-audi
|
|
|
40
40
|
import { migrateAddSourceTypeColumnsDown } from "./193-add-source-type-columns.js";
|
|
41
41
|
import { migrateStripIntegrationPrefixFromProviderKeysDown } from "./196-strip-integration-prefix-from-provider-keys.js";
|
|
42
42
|
import { migrateRenameMemoryGraphTypeValuesDown } from "./204-rename-memory-graph-type-values.js";
|
|
43
|
+
import { migrateScrubCorruptedImageAttachmentsDown } from "./206-scrub-corrupted-image-attachments.js";
|
|
43
44
|
|
|
44
45
|
export interface MigrationRegistryEntry {
|
|
45
46
|
/** The checkpoint key written to memory_checkpoints on completion. */
|
|
@@ -349,6 +350,13 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
|
|
|
349
350
|
"Rename legacy memory graph node type values: style → behavioral, relationship → semantic",
|
|
350
351
|
down: migrateRenameMemoryGraphTypeValuesDown,
|
|
351
352
|
},
|
|
353
|
+
{
|
|
354
|
+
key: "migration_scrub_corrupted_image_attachments_v1",
|
|
355
|
+
version: 40,
|
|
356
|
+
description:
|
|
357
|
+
"Remove image attachments containing HTML error pages instead of image data",
|
|
358
|
+
down: migrateScrubCorruptedImageAttachmentsDown,
|
|
359
|
+
},
|
|
352
360
|
];
|
|
353
361
|
|
|
354
362
|
export function getMaxMigrationVersion(): number {
|