@vellumai/assistant 0.3.15 → 0.3.18
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 +211 -12
- package/Dockerfile +1 -1
- package/README.md +11 -5
- package/docs/architecture/http-token-refresh.md +274 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +328 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +19 -15
- package/src/__tests__/checker.test.ts +103 -48
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +356 -0
- package/src/__tests__/conversation-pairing.test.ts +127 -27
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-store.test.ts +182 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-dispatch.test.ts +120 -0
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +23 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +281 -0
- package/src/__tests__/notification-broadcaster.test.ts +115 -4
- package/src/__tests__/notification-decision-strategy.test.ts +138 -1
- package/src/__tests__/notification-deep-link.test.ts +44 -1
- package/src/__tests__/notification-guardian-path.test.ts +157 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +38 -22
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +323 -0
- package/src/__tests__/update-template-contract.test.ts +24 -0
- package/src/__tests__/voice-session-bridge.test.ts +109 -9
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +241 -39
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/guardian-dispatch.ts +8 -0
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +8 -6
- package/src/cli/core-commands.ts +43 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +15 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +54 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin-template-path.ts +6 -0
- package/src/config/update-bulletin.ts +97 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +4 -2
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +219 -8
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/identity.ts +45 -25
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract/workspace.ts +12 -1
- package/src/daemon/ipc-contract-inventory.json +6 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +314 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +31 -9
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +546 -59
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +60 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +35 -567
- package/src/memory/channel-guardian-store.ts +63 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +44 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +33 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +136 -0
- package/src/memory/guardian-action-store.ts +418 -5
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +521 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
- package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +10 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +4 -0
- package/src/memory/schema.ts +31 -8
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +159 -18
- package/src/notifications/broadcaster.ts +69 -33
- package/src/notifications/conversation-pairing.ts +99 -21
- package/src/notifications/decision-engine.ts +176 -8
- package/src/notifications/deliveries-store.ts +39 -8
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +269 -0
- package/src/notifications/types.ts +19 -0
- package/src/permissions/checker.ts +1 -16
- package/src/permissions/defaults.ts +25 -5
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +271 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/version.ts +29 -2
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -4,6 +4,7 @@ import { getLogger } from '../../util/logger.js';
|
|
|
4
4
|
import { getDb, rawExec } from '../db.js';
|
|
5
5
|
import { asString, BackendUnavailableError } from '../job-utils.js';
|
|
6
6
|
import { enqueueMemoryJob, type MemoryJob } from '../jobs-store.js';
|
|
7
|
+
import { withQdrantBreaker } from '../qdrant-circuit-breaker.js';
|
|
7
8
|
import { getQdrantClient } from '../qdrant-client.js';
|
|
8
9
|
import { memoryEmbeddings, memoryItems, memorySegments, memorySummaries } from '../schema.js';
|
|
9
10
|
|
|
@@ -50,6 +51,6 @@ export async function deleteQdrantVectorsJob(job: MemoryJob): Promise<void> {
|
|
|
50
51
|
throw new BackendUnavailableError('Qdrant client not initialized');
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
await qdrant.deleteByTarget(targetType, targetId);
|
|
54
|
+
await withQdrantBreaker(() => qdrant.deleteByTarget(targetType, targetId));
|
|
54
55
|
log.info({ targetType, targetId }, 'Retried Qdrant vector deletion succeeded');
|
|
55
56
|
}
|
package/src/memory/job-utils.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { BackendUnavailableError } from '../util/errors.js';
|
|
|
7
7
|
import { getLogger } from '../util/logger.js';
|
|
8
8
|
import { getDb } from './db.js';
|
|
9
9
|
import { embedWithBackend, getMemoryBackendStatus } from './embedding-backend.js';
|
|
10
|
+
import { withQdrantBreaker } from './qdrant-circuit-breaker.js';
|
|
10
11
|
import { getQdrantClient } from './qdrant-client.js';
|
|
11
12
|
import { memoryEmbeddings } from './schema.js';
|
|
12
13
|
|
|
@@ -213,11 +214,13 @@ export async function embedAndUpsert(
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
try {
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
217
|
+
await withQdrantBreaker(() =>
|
|
218
|
+
qdrant.upsert(targetType, targetId, vector, {
|
|
219
|
+
text,
|
|
220
|
+
created_at: (extraPayload?.created_at as number) ?? now,
|
|
221
|
+
...(extraPayload as Record<string, unknown> | undefined),
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
221
224
|
} catch (err) {
|
|
222
225
|
log.warn({ err, targetType, targetId }, 'Failed to upsert embedding to Qdrant');
|
|
223
226
|
throw err;
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { and, asc, eq, inArray,lte, notInArray } from 'drizzle-orm';
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
|
|
4
|
+
import { getLogger } from '../util/logger.js';
|
|
4
5
|
import { truncate } from '../util/truncate.js';
|
|
5
|
-
import { getDb, rawAll,rawGet } from './db.js';
|
|
6
|
+
import { getDb, rawAll, rawChanges, rawGet } from './db.js';
|
|
6
7
|
import { memoryJobs } from './schema.js';
|
|
7
8
|
|
|
9
|
+
const log = getLogger('memory-jobs-store');
|
|
10
|
+
|
|
8
11
|
export type MemoryJobType =
|
|
9
12
|
| 'embed_segment'
|
|
10
13
|
| 'embed_item'
|
|
@@ -22,6 +25,7 @@ export type MemoryJobType =
|
|
|
22
25
|
| 'build_conversation_summary'
|
|
23
26
|
| 'backfill'
|
|
24
27
|
| 'rebuild_index'
|
|
28
|
+
| 'reconcile_fts'
|
|
25
29
|
| 'delete_qdrant_vectors'
|
|
26
30
|
| 'media_processing';
|
|
27
31
|
|
|
@@ -36,6 +40,7 @@ export interface MemoryJob<T = Record<string, unknown>> {
|
|
|
36
40
|
deferrals: number;
|
|
37
41
|
runAfter: number;
|
|
38
42
|
lastError: string | null;
|
|
43
|
+
startedAt: number | null;
|
|
39
44
|
createdAt: number;
|
|
40
45
|
updatedAt: number;
|
|
41
46
|
}
|
|
@@ -244,6 +249,21 @@ export function enqueuePruneOldConversationsJob(retentionDays?: number): string
|
|
|
244
249
|
return enqueueMemoryJob('prune_old_conversations', payload);
|
|
245
250
|
}
|
|
246
251
|
|
|
252
|
+
export function enqueueReconcileFtsJob(): string {
|
|
253
|
+
const db = getDb();
|
|
254
|
+
const existing = db
|
|
255
|
+
.select()
|
|
256
|
+
.from(memoryJobs)
|
|
257
|
+
.where(and(
|
|
258
|
+
eq(memoryJobs.type, 'reconcile_fts'),
|
|
259
|
+
inArray(memoryJobs.status, ['pending', 'running']),
|
|
260
|
+
))
|
|
261
|
+
.orderBy(asc(memoryJobs.createdAt))
|
|
262
|
+
.get();
|
|
263
|
+
if (existing) return existing.id;
|
|
264
|
+
return enqueueMemoryJob('reconcile_fts', {});
|
|
265
|
+
}
|
|
266
|
+
|
|
247
267
|
export function claimMemoryJobs(limit: number): MemoryJob[] {
|
|
248
268
|
if (limit <= 0) return [];
|
|
249
269
|
const db = getDb();
|
|
@@ -275,14 +295,15 @@ export function claimMemoryJobs(limit: number): MemoryJob[] {
|
|
|
275
295
|
|
|
276
296
|
const claimed: MemoryJob[] = [];
|
|
277
297
|
for (const row of candidates) {
|
|
278
|
-
|
|
279
|
-
.set({ status: 'running', updatedAt: now })
|
|
298
|
+
db.update(memoryJobs)
|
|
299
|
+
.set({ status: 'running', startedAt: now, updatedAt: now })
|
|
280
300
|
.where(and(eq(memoryJobs.id, row.id), eq(memoryJobs.status, 'pending')))
|
|
281
|
-
.run()
|
|
282
|
-
if ((
|
|
301
|
+
.run();
|
|
302
|
+
if (rawChanges() === 0) continue;
|
|
283
303
|
claimed.push(parseRow({
|
|
284
304
|
...row,
|
|
285
305
|
status: 'running',
|
|
306
|
+
startedAt: now,
|
|
286
307
|
updatedAt: now,
|
|
287
308
|
}));
|
|
288
309
|
}
|
|
@@ -298,7 +319,9 @@ export function completeMemoryJob(id: string): void {
|
|
|
298
319
|
}
|
|
299
320
|
|
|
300
321
|
/** Max times a job can be deferred before it is marked as failed. */
|
|
301
|
-
const MAX_DEFERRALS =
|
|
322
|
+
const MAX_DEFERRALS = 50;
|
|
323
|
+
/** Warn when deferrals reach 80% of the limit. */
|
|
324
|
+
const DEFER_WARNING_THRESHOLD = Math.floor(MAX_DEFERRALS * 0.8);
|
|
302
325
|
/** Base delay in ms for deferred jobs (grows with exponential backoff). */
|
|
303
326
|
const DEFER_BASE_DELAY_MS = 30_000;
|
|
304
327
|
/** Maximum delay cap for deferred jobs (5 minutes). */
|
|
@@ -328,6 +351,7 @@ export function deferMemoryJob(id: string): 'deferred' | 'failed' {
|
|
|
328
351
|
const now = Date.now();
|
|
329
352
|
|
|
330
353
|
if (deferrals >= MAX_DEFERRALS) {
|
|
354
|
+
log.error({ jobId: id, type: row.type, deferrals }, 'Job exceeded max deferrals, marking as failed');
|
|
331
355
|
db.update(memoryJobs)
|
|
332
356
|
.set({
|
|
333
357
|
status: 'failed',
|
|
@@ -340,6 +364,10 @@ export function deferMemoryJob(id: string): 'deferred' | 'failed' {
|
|
|
340
364
|
return 'failed';
|
|
341
365
|
}
|
|
342
366
|
|
|
367
|
+
if (deferrals >= DEFER_WARNING_THRESHOLD) {
|
|
368
|
+
log.warn({ jobId: id, type: row.type, deferrals, max: MAX_DEFERRALS }, 'Job approaching max deferral limit');
|
|
369
|
+
}
|
|
370
|
+
|
|
343
371
|
// Exponential backoff: 30s, 60s, 120s, ... capped at 5 minutes
|
|
344
372
|
const delay = Math.min(DEFER_BASE_DELAY_MS * Math.pow(2, Math.min(deferrals - 1, 10)), DEFER_MAX_DELAY_MS);
|
|
345
373
|
db.update(memoryJobs)
|
|
@@ -403,6 +431,37 @@ export function resetRunningJobsToPending(): number {
|
|
|
403
431
|
return runningRows.length;
|
|
404
432
|
}
|
|
405
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Fail running jobs whose `startedAt` is older than `timeoutMs` ago.
|
|
436
|
+
* Returns the number of jobs that were timed out.
|
|
437
|
+
*/
|
|
438
|
+
export function failStalledJobs(timeoutMs: number): number {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
const cutoff = now - timeoutMs;
|
|
441
|
+
const stalled = rawAll<{ id: string; type: string }>(`
|
|
442
|
+
SELECT id, type
|
|
443
|
+
FROM memory_jobs
|
|
444
|
+
WHERE status = 'running'
|
|
445
|
+
AND started_at IS NOT NULL
|
|
446
|
+
AND started_at < ?
|
|
447
|
+
`, cutoff);
|
|
448
|
+
if (stalled.length === 0) return 0;
|
|
449
|
+
|
|
450
|
+
const db = getDb();
|
|
451
|
+
for (const row of stalled) {
|
|
452
|
+
db.update(memoryJobs)
|
|
453
|
+
.set({
|
|
454
|
+
status: 'failed',
|
|
455
|
+
updatedAt: now,
|
|
456
|
+
lastError: `Job timed out after ${Math.round(timeoutMs / 60_000)} minutes`,
|
|
457
|
+
})
|
|
458
|
+
.where(and(eq(memoryJobs.id, row.id), eq(memoryJobs.status, 'running')))
|
|
459
|
+
.run();
|
|
460
|
+
log.warn({ jobId: row.id, type: row.type, timeoutMs }, 'Failed stalled memory job due to timeout');
|
|
461
|
+
}
|
|
462
|
+
return stalled.length;
|
|
463
|
+
}
|
|
464
|
+
|
|
406
465
|
export function getMemoryJobCounts(): Record<string, number> {
|
|
407
466
|
const rows = rawAll<{ status: string; c: number }>(`
|
|
408
467
|
SELECT status, COUNT(*) AS c
|
|
@@ -432,6 +491,7 @@ function parseRow(row: typeof memoryJobs.$inferSelect): MemoryJob {
|
|
|
432
491
|
deferrals: row.deferrals,
|
|
433
492
|
runAfter: row.runAfter,
|
|
434
493
|
lastError: row.lastError,
|
|
494
|
+
startedAt: row.startedAt,
|
|
435
495
|
createdAt: row.createdAt,
|
|
436
496
|
updatedAt: row.updatedAt,
|
|
437
497
|
};
|
|
@@ -2,6 +2,7 @@ import { getConfig } from '../config/loader.js';
|
|
|
2
2
|
import type { AssistantConfig } from '../config/types.js';
|
|
3
3
|
import { getLogger } from '../util/logger.js';
|
|
4
4
|
import { rawRun } from './db.js';
|
|
5
|
+
import { reconcileFtsIndexes } from './fts-reconciler.js';
|
|
5
6
|
import { backfillEntityRelationsJob,backfillJob } from './job-handlers/backfill.js';
|
|
6
7
|
import { checkContradictionsJob, cleanupStaleSupersededItemsJob, pruneOldConversationsJob } from './job-handlers/cleanup.js';
|
|
7
8
|
import { cleanupResolvedConflictsJob,resolvePendingConflictsForMessageJob } from './job-handlers/conflict.js';
|
|
@@ -24,10 +25,13 @@ import {
|
|
|
24
25
|
enqueueCleanupResolvedConflictsJob,
|
|
25
26
|
enqueueCleanupStaleSupersededItemsJob,
|
|
26
27
|
enqueuePruneOldConversationsJob,
|
|
28
|
+
enqueueReconcileFtsJob,
|
|
27
29
|
failMemoryJob,
|
|
30
|
+
failStalledJobs,
|
|
28
31
|
type MemoryJob,
|
|
29
32
|
resetRunningJobsToPending,
|
|
30
33
|
} from './jobs-store.js';
|
|
34
|
+
import { QdrantCircuitOpenError } from './qdrant-circuit-breaker.js';
|
|
31
35
|
import { bumpMemoryVersion } from './recall-cache.js';
|
|
32
36
|
|
|
33
37
|
// Re-export public utilities consumed by tests and other modules
|
|
@@ -86,6 +90,12 @@ export async function runMemoryJobsOnce(
|
|
|
86
90
|
// Periodic stale item sweep (throttled to at most once per hour)
|
|
87
91
|
sweepStaleItems(config);
|
|
88
92
|
|
|
93
|
+
// Fail jobs that have been running longer than the configured timeout
|
|
94
|
+
const timedOut = failStalledJobs(config.memory.jobs.stalledJobTimeoutMs);
|
|
95
|
+
if (timedOut > 0) {
|
|
96
|
+
log.warn({ timedOut }, 'Timed out stalled memory jobs');
|
|
97
|
+
}
|
|
98
|
+
|
|
89
99
|
const batchSize = Math.max(1, config.memory.jobs.batchSize);
|
|
90
100
|
const concurrency = Math.max(1, config.memory.jobs.workerConcurrency);
|
|
91
101
|
const jobs = claimMemoryJobs(batchSize);
|
|
@@ -174,10 +184,17 @@ function handleJobError(job: MemoryJob, err: unknown): void {
|
|
|
174
184
|
if (err instanceof BackendUnavailableError) {
|
|
175
185
|
const result = deferMemoryJob(job.id);
|
|
176
186
|
if (result === 'failed') {
|
|
177
|
-
log.
|
|
187
|
+
log.error({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, job exceeded max deferrals');
|
|
178
188
|
} else {
|
|
179
189
|
log.debug({ jobId: job.id, type: job.type }, 'Embedding backend unavailable, deferring job');
|
|
180
190
|
}
|
|
191
|
+
} else if (err instanceof QdrantCircuitOpenError) {
|
|
192
|
+
const result = deferMemoryJob(job.id);
|
|
193
|
+
if (result === 'failed') {
|
|
194
|
+
log.error({ jobId: job.id, type: job.type }, 'Qdrant circuit breaker open, job exceeded max deferrals');
|
|
195
|
+
} else {
|
|
196
|
+
log.debug({ jobId: job.id, type: job.type }, 'Qdrant circuit breaker open, deferring job');
|
|
197
|
+
}
|
|
181
198
|
} else {
|
|
182
199
|
const message = err instanceof Error ? err.message : String(err);
|
|
183
200
|
const category = classifyError(err);
|
|
@@ -247,6 +264,9 @@ async function processJob(job: MemoryJob, config: AssistantConfig): Promise<void
|
|
|
247
264
|
case 'rebuild_index':
|
|
248
265
|
rebuildIndexJob();
|
|
249
266
|
return;
|
|
267
|
+
case 'reconcile_fts':
|
|
268
|
+
reconcileFtsIndexes();
|
|
269
|
+
return;
|
|
250
270
|
case 'delete_qdrant_vectors':
|
|
251
271
|
await deleteQdrantVectorsJob(job);
|
|
252
272
|
return;
|
|
@@ -281,11 +301,13 @@ export function maybeEnqueueScheduledCleanupJobs(config: AssistantConfig, nowMs
|
|
|
281
301
|
const pruneConversationsJobId = cleanup.conversationRetentionDays > 0
|
|
282
302
|
? enqueuePruneOldConversationsJob(cleanup.conversationRetentionDays)
|
|
283
303
|
: null;
|
|
304
|
+
const reconcileFtsJobId = enqueueReconcileFtsJob();
|
|
284
305
|
lastScheduledCleanupEnqueueMs = nowMs;
|
|
285
306
|
log.debug({
|
|
286
307
|
resolvedConflictsJobId,
|
|
287
308
|
staleSupersededItemsJobId,
|
|
288
309
|
pruneConversationsJobId,
|
|
310
|
+
reconcileFtsJobId,
|
|
289
311
|
enqueueIntervalMs: cleanup.enqueueIntervalMs,
|
|
290
312
|
resolvedConflictRetentionMs: cleanup.resolvedConflictRetentionMs,
|
|
291
313
|
supersededItemRetentionMs: cleanup.supersededItemRetentionMs,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add follow-up lifecycle columns to guardian_action_requests.
|
|
5
|
+
*
|
|
6
|
+
* These columns track why a request expired (expired_reason), the
|
|
7
|
+
* post-timeout follow-up state machine (followup_state), and any
|
|
8
|
+
* late answer that arrived after the timeout.
|
|
9
|
+
*
|
|
10
|
+
* Uses ALTER TABLE ADD COLUMN with try/catch for idempotency.
|
|
11
|
+
*/
|
|
12
|
+
export function migrateGuardianActionFollowup(database: DrizzleDb): void {
|
|
13
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN expired_reason TEXT`); } catch { /* already exists */ }
|
|
14
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN followup_state TEXT NOT NULL DEFAULT 'none'`); } catch { /* already exists */ }
|
|
15
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN late_answer_text TEXT`); } catch { /* already exists */ }
|
|
16
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN late_answered_at INTEGER`); } catch { /* already exists */ }
|
|
17
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN followup_action TEXT`); } catch { /* already exists */ }
|
|
18
|
+
try { database.run(/*sql*/ `ALTER TABLE guardian_action_requests ADD COLUMN followup_completed_at INTEGER`); } catch { /* already exists */ }
|
|
19
|
+
|
|
20
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_action_requests_followup_state ON guardian_action_requests(followup_state)`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add verification_purpose column to channel_guardian_verification_challenges.
|
|
5
|
+
* Distinguishes guardian outbound verification from trusted contact verification
|
|
6
|
+
* so the consume path knows whether to create a guardian binding.
|
|
7
|
+
*
|
|
8
|
+
* Uses ALTER TABLE ADD COLUMN which is a no-op if the column already
|
|
9
|
+
* exists (caught by try/catch).
|
|
10
|
+
*/
|
|
11
|
+
export function migrateGuardianVerificationPurpose(database: DrizzleDb): void {
|
|
12
|
+
try {
|
|
13
|
+
database.run(
|
|
14
|
+
/*sql*/ `ALTER TABLE channel_guardian_verification_challenges ADD COLUMN verification_purpose TEXT DEFAULT 'guardian'`,
|
|
15
|
+
);
|
|
16
|
+
} catch { /* already exists */ }
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add index on guardian_action_deliveries.destination_conversation_id.
|
|
5
|
+
*
|
|
6
|
+
* Several lookup paths (getPendingDeliveryByConversation,
|
|
7
|
+
* getExpiredDeliveryByConversation, getFollowupDeliveryByConversation)
|
|
8
|
+
* filter deliveries by destination_conversation_id. Without an index
|
|
9
|
+
* these degrade to full table scans as delivery history grows.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateGuardianDeliveryConversationIndex(database: DrizzleDb): void {
|
|
12
|
+
database.run(
|
|
13
|
+
/*sql*/ `CREATE INDEX IF NOT EXISTS idx_guardian_action_deliveries_dest_conversation ON guardian_action_deliveries(destination_conversation_id)`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add thread decision audit columns to notification_deliveries.
|
|
5
|
+
*
|
|
6
|
+
* These columns record the model's per-channel thread action (start_new
|
|
7
|
+
* or reuse_existing), the target conversation ID for reuse, and whether
|
|
8
|
+
* a fallback to start_new was needed due to an invalid/stale target.
|
|
9
|
+
*/
|
|
10
|
+
export function migrateNotificationDeliveryThreadDecision(database: DrizzleDb): void {
|
|
11
|
+
try {
|
|
12
|
+
database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_action TEXT`);
|
|
13
|
+
} catch { /* Column already exists */ }
|
|
14
|
+
try {
|
|
15
|
+
database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_target_conversation_id TEXT`);
|
|
16
|
+
} catch { /* Column already exists */ }
|
|
17
|
+
try {
|
|
18
|
+
database.run(/*sql*/ `ALTER TABLE notification_deliveries ADD COLUMN thread_decision_fallback_used INTEGER`);
|
|
19
|
+
} catch { /* Column already exists */ }
|
|
20
|
+
}
|
|
@@ -28,7 +28,7 @@ export function createCoreTables(database: DrizzleDb): void {
|
|
|
28
28
|
database.run(/*sql*/ `
|
|
29
29
|
CREATE TABLE IF NOT EXISTS messages (
|
|
30
30
|
id TEXT PRIMARY KEY,
|
|
31
|
-
conversation_id TEXT NOT NULL REFERENCES conversations(id),
|
|
31
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
32
32
|
role TEXT NOT NULL,
|
|
33
33
|
content TEXT NOT NULL,
|
|
34
34
|
created_at INTEGER NOT NULL
|
|
@@ -108,6 +108,10 @@ export function createWatchersAndLogsTables(database: DrizzleDb): void {
|
|
|
108
108
|
`);
|
|
109
109
|
|
|
110
110
|
// FTS table for lexical retrieval over memory_segments.text.
|
|
111
|
+
// Triggers below are atomic with the triggering statement: if the FTS
|
|
112
|
+
// operation fails, the base table write rolls back too. A corrupted FTS
|
|
113
|
+
// table will block all memory_segments writes until rebuilt. See the
|
|
114
|
+
// analogous comment in 116-messages-fts.ts for recovery steps.
|
|
111
115
|
database.run(/*sql*/ `
|
|
112
116
|
CREATE VIRTUAL TABLE IF NOT EXISTS memory_segment_fts USING fts5(
|
|
113
117
|
segment_id UNINDEXED,
|
|
@@ -22,7 +22,7 @@ export function createTasksAndWorkItemsTables(database: DrizzleDb): void {
|
|
|
22
22
|
database.run(/*sql*/ `
|
|
23
23
|
CREATE TABLE IF NOT EXISTS task_runs (
|
|
24
24
|
id TEXT PRIMARY KEY,
|
|
25
|
-
task_id TEXT NOT NULL REFERENCES tasks(id),
|
|
25
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
26
26
|
conversation_id TEXT,
|
|
27
27
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
28
28
|
started_at INTEGER,
|
|
@@ -40,7 +40,7 @@ export function createAssistantInboxTables(database: DrizzleDb): void {
|
|
|
40
40
|
username TEXT,
|
|
41
41
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
42
42
|
policy TEXT NOT NULL DEFAULT 'allow',
|
|
43
|
-
invite_id TEXT REFERENCES assistant_ingress_invites(id),
|
|
43
|
+
invite_id TEXT REFERENCES assistant_ingress_invites(id) ON DELETE CASCADE,
|
|
44
44
|
created_by_session_id TEXT,
|
|
45
45
|
revoked_reason TEXT,
|
|
46
46
|
blocked_reason TEXT,
|
|
@@ -9,7 +9,7 @@ import { migrateConversationStatusIndexes } from './021-conversation-status-inde
|
|
|
9
9
|
import { migrateAddOriginInterface } from './022-add-origin-interface.js';
|
|
10
10
|
import { migrateMemoryItemSourcesIndexes } from './023-memory-item-sources-indexes.js';
|
|
11
11
|
import { migrateEmbeddingVectorBlob } from './024-embedding-vector-blob.js';
|
|
12
|
-
import { migrateEmbeddingsNullableVectorJson } from './
|
|
12
|
+
import { migrateEmbeddingsNullableVectorJson } from './026a-embeddings-nullable-vector-json.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Late-stage migrations that must run after all tables and indexes exist:
|
|
@@ -8,6 +8,19 @@ import type { DrizzleDb } from '../db-connection.js';
|
|
|
8
8
|
* (type, text, tool_use) are short common words that rarely matter as search
|
|
9
9
|
* terms. The existing buildExcerpt() in conversation-store handles extracting
|
|
10
10
|
* readable text from JSON for display after matching.
|
|
11
|
+
*
|
|
12
|
+
* ## Trigger atomicity and failure modes
|
|
13
|
+
*
|
|
14
|
+
* SQLite triggers execute atomically within the triggering statement's
|
|
15
|
+
* transaction. If the FTS trigger fails (e.g., corrupted FTS index), the
|
|
16
|
+
* entire statement — including the base table INSERT/UPDATE/DELETE — is
|
|
17
|
+
* rolled back. This means a trigger failure does NOT silently lose FTS
|
|
18
|
+
* data; instead, it prevents the base operation from succeeding at all.
|
|
19
|
+
*
|
|
20
|
+
* The real risk is the reverse: a corrupted FTS virtual table will cause
|
|
21
|
+
* ALL writes to the messages table to fail until the FTS table is rebuilt.
|
|
22
|
+
* If this happens, `messages_fts` should be dropped and recreated, then
|
|
23
|
+
* backfilled via `migrateMessagesFtsBackfill`.
|
|
11
24
|
*/
|
|
12
25
|
export function createMessagesFts(database: DrizzleDb): void {
|
|
13
26
|
database.run(/*sql*/ `
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add indexes, a column, and a unique constraint for schema improvements:
|
|
5
|
+
* - Index on call_sessions(status) for status-based queries
|
|
6
|
+
* - Index on llm_usage_events(conversation_id) for per-conversation usage queries
|
|
7
|
+
* - startedAt column on memory_jobs for detecting stalled jobs
|
|
8
|
+
* - Unique index on notification_deliveries(notification_decision_id, channel)
|
|
9
|
+
*/
|
|
10
|
+
export function migrateSchemaIndexesAndColumns(database: DrizzleDb): void {
|
|
11
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_call_sessions_status ON call_sessions(status)`);
|
|
12
|
+
database.run(/*sql*/ `CREATE INDEX IF NOT EXISTS idx_llm_usage_events_conversation_id ON llm_usage_events(conversation_id)`);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
database.run(/*sql*/ `ALTER TABLE memory_jobs ADD COLUMN started_at INTEGER`);
|
|
16
|
+
} catch { /* already exists */ }
|
|
17
|
+
|
|
18
|
+
// Deduplicate before creating the unique index — the prior schema allowed
|
|
19
|
+
// multiple rows per (notification_decision_id, channel) via the wider
|
|
20
|
+
// (decision_id, channel, destination, attempt) unique index. Keep the
|
|
21
|
+
// row with the latest updated_at for each group.
|
|
22
|
+
database.run(/*sql*/ `
|
|
23
|
+
DELETE FROM notification_deliveries
|
|
24
|
+
WHERE id NOT IN (
|
|
25
|
+
SELECT id FROM (
|
|
26
|
+
SELECT id, ROW_NUMBER() OVER (
|
|
27
|
+
PARTITION BY notification_decision_id, channel
|
|
28
|
+
ORDER BY updated_at DESC
|
|
29
|
+
) AS rn
|
|
30
|
+
FROM notification_deliveries
|
|
31
|
+
)
|
|
32
|
+
WHERE rn = 1
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
database.run(/*sql*/ `CREATE UNIQUE INDEX IF NOT EXISTS idx_notification_deliveries_decision_channel ON notification_deliveries(notification_decision_id, channel)`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type DrizzleDb, getSqliteFrom } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rebuild messages, task_runs, and assistant_ingress_members tables to add
|
|
5
|
+
* ON DELETE CASCADE to their FK constraints. SQLite does not support
|
|
6
|
+
* ALTER TABLE to change FK behavior, so a table rebuild is required.
|
|
7
|
+
*
|
|
8
|
+
* Follows the same pattern as 002-tool-invocations-fk.ts: check if the
|
|
9
|
+
* DDL already contains ON DELETE CASCADE, and skip if so.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateFkCascadeRebuilds(database: DrizzleDb): void {
|
|
12
|
+
const raw = getSqliteFrom(database);
|
|
13
|
+
|
|
14
|
+
rebuildMessages(raw);
|
|
15
|
+
rebuildTaskRuns(raw);
|
|
16
|
+
rebuildAssistantIngressMembers(raw);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function hasCascade(raw: ReturnType<typeof getSqliteFrom>, tableName: string): boolean {
|
|
20
|
+
const row = raw.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`).get(tableName) as { sql: string } | null;
|
|
21
|
+
if (!row) return true; // table doesn't exist yet — will be created with correct DDL
|
|
22
|
+
return row.sql.includes('ON DELETE CASCADE');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rebuildMessages(raw: ReturnType<typeof getSqliteFrom>): void {
|
|
26
|
+
if (hasCascade(raw, 'messages')) return;
|
|
27
|
+
|
|
28
|
+
raw.exec('PRAGMA foreign_keys = OFF');
|
|
29
|
+
try {
|
|
30
|
+
raw.exec(/*sql*/ `
|
|
31
|
+
BEGIN;
|
|
32
|
+
|
|
33
|
+
DROP TRIGGER IF EXISTS messages_fts_ai;
|
|
34
|
+
DROP TRIGGER IF EXISTS messages_fts_ad;
|
|
35
|
+
DROP TRIGGER IF EXISTS messages_fts_au;
|
|
36
|
+
|
|
37
|
+
CREATE TABLE messages_new (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
40
|
+
role TEXT NOT NULL,
|
|
41
|
+
content TEXT NOT NULL,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
metadata TEXT
|
|
44
|
+
);
|
|
45
|
+
INSERT INTO messages_new SELECT id, conversation_id, role, content, created_at, metadata FROM messages;
|
|
46
|
+
DROP TABLE messages;
|
|
47
|
+
ALTER TABLE messages_new RENAME TO messages;
|
|
48
|
+
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at);
|
|
51
|
+
|
|
52
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ai
|
|
53
|
+
AFTER INSERT ON messages
|
|
54
|
+
BEGIN
|
|
55
|
+
INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content);
|
|
56
|
+
END;
|
|
57
|
+
|
|
58
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_ad
|
|
59
|
+
AFTER DELETE ON messages
|
|
60
|
+
BEGIN
|
|
61
|
+
DELETE FROM messages_fts WHERE message_id = old.id;
|
|
62
|
+
END;
|
|
63
|
+
|
|
64
|
+
CREATE TRIGGER IF NOT EXISTS messages_fts_au
|
|
65
|
+
AFTER UPDATE ON messages
|
|
66
|
+
BEGIN
|
|
67
|
+
DELETE FROM messages_fts WHERE message_id = old.id;
|
|
68
|
+
INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content);
|
|
69
|
+
END;
|
|
70
|
+
|
|
71
|
+
COMMIT;
|
|
72
|
+
`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
75
|
+
throw e;
|
|
76
|
+
} finally {
|
|
77
|
+
raw.exec('PRAGMA foreign_keys = ON');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rebuildTaskRuns(raw: ReturnType<typeof getSqliteFrom>): void {
|
|
82
|
+
if (hasCascade(raw, 'task_runs')) return;
|
|
83
|
+
|
|
84
|
+
raw.exec('PRAGMA foreign_keys = OFF');
|
|
85
|
+
try {
|
|
86
|
+
raw.exec(/*sql*/ `
|
|
87
|
+
BEGIN;
|
|
88
|
+
CREATE TABLE task_runs_new (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
91
|
+
conversation_id TEXT,
|
|
92
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
93
|
+
started_at INTEGER,
|
|
94
|
+
finished_at INTEGER,
|
|
95
|
+
error TEXT,
|
|
96
|
+
principal_id TEXT,
|
|
97
|
+
memory_scope_id TEXT,
|
|
98
|
+
created_at INTEGER NOT NULL
|
|
99
|
+
);
|
|
100
|
+
INSERT INTO task_runs_new SELECT id, task_id, conversation_id, status, started_at, finished_at, error, principal_id, memory_scope_id, created_at FROM task_runs;
|
|
101
|
+
DROP TABLE task_runs;
|
|
102
|
+
ALTER TABLE task_runs_new RENAME TO task_runs;
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_task_id ON task_runs(task_id);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_conversation_status ON task_runs(conversation_id, status);
|
|
107
|
+
|
|
108
|
+
COMMIT;
|
|
109
|
+
`);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
112
|
+
throw e;
|
|
113
|
+
} finally {
|
|
114
|
+
raw.exec('PRAGMA foreign_keys = ON');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rebuildAssistantIngressMembers(raw: ReturnType<typeof getSqliteFrom>): void {
|
|
119
|
+
if (hasCascade(raw, 'assistant_ingress_members')) return;
|
|
120
|
+
|
|
121
|
+
raw.exec('PRAGMA foreign_keys = OFF');
|
|
122
|
+
try {
|
|
123
|
+
raw.exec(/*sql*/ `
|
|
124
|
+
BEGIN;
|
|
125
|
+
CREATE TABLE assistant_ingress_members_new (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
assistant_id TEXT NOT NULL DEFAULT 'self',
|
|
128
|
+
source_channel TEXT NOT NULL,
|
|
129
|
+
external_user_id TEXT,
|
|
130
|
+
external_chat_id TEXT,
|
|
131
|
+
display_name TEXT,
|
|
132
|
+
username TEXT,
|
|
133
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
134
|
+
policy TEXT NOT NULL DEFAULT 'allow',
|
|
135
|
+
invite_id TEXT REFERENCES assistant_ingress_invites(id) ON DELETE CASCADE,
|
|
136
|
+
created_by_session_id TEXT,
|
|
137
|
+
revoked_reason TEXT,
|
|
138
|
+
blocked_reason TEXT,
|
|
139
|
+
last_seen_at INTEGER,
|
|
140
|
+
created_at INTEGER NOT NULL,
|
|
141
|
+
updated_at INTEGER NOT NULL,
|
|
142
|
+
CHECK (external_user_id IS NOT NULL OR external_chat_id IS NOT NULL)
|
|
143
|
+
);
|
|
144
|
+
INSERT INTO assistant_ingress_members_new SELECT id, assistant_id, source_channel, external_user_id, external_chat_id, display_name, username, status, policy, invite_id, created_by_session_id, revoked_reason, blocked_reason, last_seen_at, created_at, updated_at FROM assistant_ingress_members;
|
|
145
|
+
DROP TABLE assistant_ingress_members;
|
|
146
|
+
ALTER TABLE assistant_ingress_members_new RENAME TO assistant_ingress_members;
|
|
147
|
+
|
|
148
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_ingress_members_user ON assistant_ingress_members(assistant_id, source_channel, external_user_id) WHERE external_user_id IS NOT NULL;
|
|
149
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_ingress_members_chat ON assistant_ingress_members(assistant_id, source_channel, external_chat_id) WHERE external_chat_id IS NOT NULL;
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_ingress_members_status_policy ON assistant_ingress_members(assistant_id, source_channel, status, policy);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_ingress_members_updated ON assistant_ingress_members(assistant_id, source_channel, updated_at);
|
|
152
|
+
|
|
153
|
+
COMMIT;
|
|
154
|
+
`);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
try { raw.exec('ROLLBACK'); } catch { /* no active transaction */ }
|
|
157
|
+
throw e;
|
|
158
|
+
} finally {
|
|
159
|
+
raw.exec('PRAGMA foreign_keys = ON');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -23,12 +23,17 @@ export { migrateAddOriginInterface } from './022-add-origin-interface.js';
|
|
|
23
23
|
export { migrateMemoryItemSourcesIndexes } from './023-memory-item-sources-indexes.js';
|
|
24
24
|
export { migrateEmbeddingVectorBlob } from './024-embedding-vector-blob.js';
|
|
25
25
|
export { migrateMessagesFtsBackfill } from './025-messages-fts-backfill.js';
|
|
26
|
-
export { migrateEmbeddingsNullableVectorJson } from './026-embeddings-nullable-vector-json.js';
|
|
27
26
|
export { migrateGuardianVerificationSessions } from './026-guardian-verification-sessions.js';
|
|
28
|
-
export {
|
|
27
|
+
export { migrateEmbeddingsNullableVectorJson } from './026a-embeddings-nullable-vector-json.js';
|
|
29
28
|
export { migrateNotificationDeliveryPairingColumns } from './027-notification-delivery-pairing-columns.js';
|
|
29
|
+
export { migrateGuardianBootstrapToken } from './027a-guardian-bootstrap-token.js';
|
|
30
30
|
export { migrateCallSessionMode } from './028-call-session-mode.js';
|
|
31
31
|
export { migrateChannelInboundDeliveredSegments } from './029-channel-inbound-delivered-segments.js';
|
|
32
|
+
export { migrateGuardianActionFollowup } from './030-guardian-action-followup.js';
|
|
33
|
+
export { migrateGuardianVerificationPurpose } from './030-guardian-verification-purpose.js';
|
|
34
|
+
export { migrateConversationsThreadTypeIndex } from './031-conversations-thread-type-index.js';
|
|
35
|
+
export { migrateGuardianDeliveryConversationIndex } from './032-guardian-delivery-conversation-index.js';
|
|
36
|
+
export { migrateNotificationDeliveryThreadDecision } from './032-notification-delivery-thread-decision.js';
|
|
32
37
|
export { createCoreTables } from './100-core-tables.js';
|
|
33
38
|
export { createWatchersAndLogsTables } from './101-watchers-and-logs.js';
|
|
34
39
|
export { addCoreColumns } from './102-alter-table-columns.js';
|
|
@@ -48,9 +53,11 @@ export { createSequenceTables } from './115-sequences.js';
|
|
|
48
53
|
export { createMessagesFts } from './116-messages-fts.js';
|
|
49
54
|
export { createConversationAttentionTables } from './117-conversation-attention.js';
|
|
50
55
|
export { migrateReminderRoutingIntent } from './118-reminder-routing-intent.js';
|
|
56
|
+
export { migrateSchemaIndexesAndColumns } from './119-schema-indexes-and-columns.js';
|
|
57
|
+
export { migrateFkCascadeRebuilds } from './120-fk-cascade-rebuilds.js';
|
|
51
58
|
export {
|
|
52
59
|
MIGRATION_REGISTRY,
|
|
53
60
|
type MigrationRegistryEntry,
|
|
54
61
|
type MigrationValidationResult,
|
|
55
62
|
} from './registry.js';
|
|
56
|
-
export { validateMigrationState } from './validate-migration-state.js';
|
|
63
|
+
export { recoverCrashedMigrations, validateMigrationState, withCrashRecovery } from './validate-migration-state.js';
|