@vellumai/assistant 0.10.3 → 0.10.4-staging.1
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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `assistant/src/memory/embedding-cache.ts` — the shared dense-vector
|
|
3
|
+
* cache over the `memory_embeddings` table. Exercises the real SQL against a
|
|
4
|
+
* temp workspace DB (round-trip, dim-mismatch miss, key isolation, upsert).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
afterAll,
|
|
12
|
+
beforeAll,
|
|
13
|
+
beforeEach,
|
|
14
|
+
describe,
|
|
15
|
+
expect,
|
|
16
|
+
mock,
|
|
17
|
+
test,
|
|
18
|
+
} from "bun:test";
|
|
19
|
+
|
|
20
|
+
mock.module("../../util/logger.js", () => ({
|
|
21
|
+
getLogger: () =>
|
|
22
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
let tmpWorkspace: string;
|
|
26
|
+
let previousWorkspaceEnv: string | undefined;
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
tmpWorkspace = mkdtempSync(join(tmpdir(), "embedding-cache-test-"));
|
|
30
|
+
previousWorkspaceEnv = process.env.VELLUM_WORKSPACE_DIR;
|
|
31
|
+
process.env.VELLUM_WORKSPACE_DIR = tmpWorkspace;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
if (previousWorkspaceEnv === undefined) {
|
|
36
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
37
|
+
} else {
|
|
38
|
+
process.env.VELLUM_WORKSPACE_DIR = previousWorkspaceEnv;
|
|
39
|
+
}
|
|
40
|
+
rmSync(tmpWorkspace, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Deferred so internal `getWorkspaceDir()` resolves to the tmpdir set above.
|
|
44
|
+
const { getDb } = await import("../db-connection.js");
|
|
45
|
+
const { resetDbForTesting } =
|
|
46
|
+
await import("../../__tests__/db-test-helpers.js");
|
|
47
|
+
const { initializeDb } = await import("../db-init.js");
|
|
48
|
+
const { memoryEmbeddings } = await import("../schema.js");
|
|
49
|
+
const { readEmbeddingCache, writeEmbeddingCache } =
|
|
50
|
+
await import("../embedding-cache.js");
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
resetDbForTesting();
|
|
54
|
+
// The first run pays the full cold-start migration chain; bump the hook
|
|
55
|
+
// timeout above bun's 5s default so the leading test doesn't flake in CI.
|
|
56
|
+
await initializeDb();
|
|
57
|
+
}, 30_000);
|
|
58
|
+
|
|
59
|
+
const KEY = {
|
|
60
|
+
targetType: "v3_section",
|
|
61
|
+
targetId: "people/alice#0",
|
|
62
|
+
provider: "local",
|
|
63
|
+
model: "test-model",
|
|
64
|
+
expectedDim: 4,
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
function seed(
|
|
68
|
+
overrides: Partial<{
|
|
69
|
+
dense: number[];
|
|
70
|
+
contentHash: string;
|
|
71
|
+
now: number;
|
|
72
|
+
}> = {},
|
|
73
|
+
): void {
|
|
74
|
+
writeEmbeddingCache(getDb(), {
|
|
75
|
+
targetType: KEY.targetType,
|
|
76
|
+
targetId: KEY.targetId,
|
|
77
|
+
provider: KEY.provider,
|
|
78
|
+
model: KEY.model,
|
|
79
|
+
dense: overrides.dense ?? [1, 2, 3, 4],
|
|
80
|
+
contentHash: overrides.contentHash ?? "hash-a",
|
|
81
|
+
now: overrides.now ?? 1000,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("embedding-cache", () => {
|
|
86
|
+
test("read returns null when nothing is cached", () => {
|
|
87
|
+
expect(readEmbeddingCache(getDb(), KEY)).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("write then read round-trips the vector and content hash", () => {
|
|
91
|
+
seed({ dense: [1, 2, 3, 4], contentHash: "hash-a" });
|
|
92
|
+
|
|
93
|
+
const got = readEmbeddingCache(getDb(), KEY);
|
|
94
|
+
expect(got).not.toBeNull();
|
|
95
|
+
expect(got!.dense).toEqual([1, 2, 3, 4]);
|
|
96
|
+
expect(got!.contentHash).toBe("hash-a");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("read misses when the configured dimension differs from the stored row", () => {
|
|
100
|
+
seed();
|
|
101
|
+
expect(readEmbeddingCache(getDb(), { ...KEY, expectedDim: 8 })).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("read is isolated by targetType / targetId / provider / model", () => {
|
|
105
|
+
seed();
|
|
106
|
+
const db = getDb();
|
|
107
|
+
expect(
|
|
108
|
+
readEmbeddingCache(db, { ...KEY, targetType: "concept_page" }),
|
|
109
|
+
).toBeNull();
|
|
110
|
+
expect(
|
|
111
|
+
readEmbeddingCache(db, { ...KEY, targetId: "people/bob#0" }),
|
|
112
|
+
).toBeNull();
|
|
113
|
+
expect(readEmbeddingCache(db, { ...KEY, provider: "openai" })).toBeNull();
|
|
114
|
+
expect(readEmbeddingCache(db, { ...KEY, model: "other-model" })).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("re-writing the same key upserts in place (one row, latest content)", () => {
|
|
118
|
+
const db = getDb();
|
|
119
|
+
seed({ dense: [1, 2, 3, 4], contentHash: "hash-a", now: 1000 });
|
|
120
|
+
seed({ dense: [5, 6, 7, 8], contentHash: "hash-b", now: 2000 });
|
|
121
|
+
|
|
122
|
+
const got = readEmbeddingCache(db, KEY);
|
|
123
|
+
expect(got!.dense).toEqual([5, 6, 7, 8]);
|
|
124
|
+
expect(got!.contentHash).toBe("hash-b");
|
|
125
|
+
|
|
126
|
+
// The unique key collapses both writes onto a single row.
|
|
127
|
+
const rows = db
|
|
128
|
+
.select()
|
|
129
|
+
.from(memoryEmbeddings)
|
|
130
|
+
.all()
|
|
131
|
+
.filter(
|
|
132
|
+
(r) => r.targetType === KEY.targetType && r.targetId === KEY.targetId,
|
|
133
|
+
);
|
|
134
|
+
expect(rows).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { and, desc, eq, lte } from "drizzle-orm";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
|
|
4
|
+
import { type DrizzleDb, getDb } from "./db-connection.js";
|
|
5
|
+
import { conversationCompactionEvents } from "./schema.js";
|
|
6
|
+
|
|
7
|
+
export interface CompactionEvent {
|
|
8
|
+
/** Wall-clock time the compaction ran (= `conversations.context_compacted_at`). */
|
|
9
|
+
compactedAt: number;
|
|
10
|
+
summary: string;
|
|
11
|
+
/** Count of leading persisted messages behind this compaction's summary. */
|
|
12
|
+
compactedMessageCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Append a compaction event to the ledger. Called alongside the
|
|
17
|
+
* `conversations` cache update on every compaction.
|
|
18
|
+
*/
|
|
19
|
+
export function appendCompactionEvent(
|
|
20
|
+
conversationId: string,
|
|
21
|
+
event: CompactionEvent,
|
|
22
|
+
): void {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
db.insert(conversationCompactionEvents)
|
|
25
|
+
.values({
|
|
26
|
+
id: uuid(),
|
|
27
|
+
conversationId,
|
|
28
|
+
compactedAt: event.compactedAt,
|
|
29
|
+
summary: event.summary,
|
|
30
|
+
compactedMessageCount: event.compactedMessageCount,
|
|
31
|
+
createdAt: Date.now(),
|
|
32
|
+
})
|
|
33
|
+
.run();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The most recent compaction event whose `compactedAt` is at-or-before
|
|
38
|
+
* `atOrBefore`, or null if none (or `atOrBefore` is null). This is the fork
|
|
39
|
+
* inheritance rule: a compaction applies to a fork only if it happened before
|
|
40
|
+
* the message being forked from.
|
|
41
|
+
*/
|
|
42
|
+
export function getLatestCompactionEventAtOrBefore(
|
|
43
|
+
conversationId: string,
|
|
44
|
+
atOrBefore: number | null,
|
|
45
|
+
): CompactionEvent | null {
|
|
46
|
+
if (atOrBefore == null) return null;
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const row = db
|
|
49
|
+
.select({
|
|
50
|
+
compactedAt: conversationCompactionEvents.compactedAt,
|
|
51
|
+
summary: conversationCompactionEvents.summary,
|
|
52
|
+
compactedMessageCount: conversationCompactionEvents.compactedMessageCount,
|
|
53
|
+
})
|
|
54
|
+
.from(conversationCompactionEvents)
|
|
55
|
+
.where(
|
|
56
|
+
and(
|
|
57
|
+
eq(conversationCompactionEvents.conversationId, conversationId),
|
|
58
|
+
lte(conversationCompactionEvents.compactedAt, atOrBefore),
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
.orderBy(desc(conversationCompactionEvents.compactedAt))
|
|
62
|
+
.limit(1)
|
|
63
|
+
.get();
|
|
64
|
+
return row ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Copy the source conversation's ledger events with `compactedAt <=
|
|
69
|
+
* boundaryCreatedAt` into the fork, so the fork owns a correct ledger for its
|
|
70
|
+
* own future forks/compactions. Takes the active `db` so the copy joins the
|
|
71
|
+
* caller's transaction. No-op when the boundary is null.
|
|
72
|
+
*/
|
|
73
|
+
export function forkCompactionLedger(
|
|
74
|
+
db: DrizzleDb,
|
|
75
|
+
sourceConversationId: string,
|
|
76
|
+
forkConversationId: string,
|
|
77
|
+
boundaryCreatedAt: number | null,
|
|
78
|
+
): void {
|
|
79
|
+
if (boundaryCreatedAt == null) return;
|
|
80
|
+
const events = db
|
|
81
|
+
.select({
|
|
82
|
+
compactedAt: conversationCompactionEvents.compactedAt,
|
|
83
|
+
summary: conversationCompactionEvents.summary,
|
|
84
|
+
compactedMessageCount: conversationCompactionEvents.compactedMessageCount,
|
|
85
|
+
})
|
|
86
|
+
.from(conversationCompactionEvents)
|
|
87
|
+
.where(
|
|
88
|
+
and(
|
|
89
|
+
eq(conversationCompactionEvents.conversationId, sourceConversationId),
|
|
90
|
+
lte(conversationCompactionEvents.compactedAt, boundaryCreatedAt),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
.all();
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
for (const event of events) {
|
|
96
|
+
db.insert(conversationCompactionEvents)
|
|
97
|
+
.values({
|
|
98
|
+
id: uuid(),
|
|
99
|
+
conversationId: forkConversationId,
|
|
100
|
+
compactedAt: event.compactedAt,
|
|
101
|
+
summary: event.summary,
|
|
102
|
+
compactedMessageCount: event.compactedMessageCount,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
})
|
|
105
|
+
.run();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
|
|
35
35
|
seedEverInjectedFromSlugs,
|
|
36
36
|
} from "../plugins/defaults/memory-v3-shadow/ever-injected-store.js";
|
|
37
|
+
import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
|
|
37
38
|
import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
|
|
38
39
|
import { UserError } from "../util/errors.js";
|
|
39
40
|
import { safeParseRecord } from "../util/json.js";
|
|
@@ -45,6 +46,11 @@ import {
|
|
|
45
46
|
linkAttachmentToMessage,
|
|
46
47
|
} from "./attachments-store.js";
|
|
47
48
|
import { AUTO_ANALYSIS_SOURCE } from "./auto-analysis-constants.js";
|
|
49
|
+
import {
|
|
50
|
+
appendCompactionEvent,
|
|
51
|
+
forkCompactionLedger,
|
|
52
|
+
getLatestCompactionEventAtOrBefore,
|
|
53
|
+
} from "./compaction-ledger-store.js";
|
|
48
54
|
import {
|
|
49
55
|
projectAssistantMessage,
|
|
50
56
|
seedForkedConversationAttention,
|
|
@@ -591,6 +597,7 @@ export function createConversation(
|
|
|
591
597
|
) {
|
|
592
598
|
const db = getDb();
|
|
593
599
|
const now = Date.now();
|
|
600
|
+
const initialSeq = getCurrentSeq();
|
|
594
601
|
const opts =
|
|
595
602
|
typeof titleOrOpts === "string"
|
|
596
603
|
? { title: titleOrOpts }
|
|
@@ -629,6 +636,10 @@ export function createConversation(
|
|
|
629
636
|
memoryScopeId,
|
|
630
637
|
scheduleJobId: opts.scheduleJobId ?? null,
|
|
631
638
|
forkParentConversationId: opts.forkParentConversationId ?? null,
|
|
639
|
+
// Snapshot↔stream alignment baseline, captured at the creation instant.
|
|
640
|
+
// 0 (nothing stamped yet this process) is stored as NULL so `/messages`
|
|
641
|
+
// reports null and the client cold-starts rather than aligning to seq 0.
|
|
642
|
+
seq: initialSeq > 0 ? initialSeq : null,
|
|
632
643
|
};
|
|
633
644
|
|
|
634
645
|
// Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
|
|
@@ -928,16 +939,6 @@ export function forkConversation(params: {
|
|
|
928
939
|
initialBoundaryIndex,
|
|
929
940
|
);
|
|
930
941
|
|
|
931
|
-
const visibleWindowStartIndex = Math.max(
|
|
932
|
-
0,
|
|
933
|
-
Math.min(
|
|
934
|
-
sourceConversation.contextCompactedMessageCount,
|
|
935
|
-
sourceMessages.length,
|
|
936
|
-
),
|
|
937
|
-
);
|
|
938
|
-
const preserveSourceCompactionState =
|
|
939
|
-
copyBoundaryIndex >= visibleWindowStartIndex;
|
|
940
|
-
|
|
941
942
|
const messagesToCopy =
|
|
942
943
|
copyBoundaryIndex >= 0
|
|
943
944
|
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
@@ -953,6 +954,23 @@ export function forkConversation(params: {
|
|
|
953
954
|
sourceHistoryStrippedAt != null &&
|
|
954
955
|
boundaryMessageCreatedAt != null &&
|
|
955
956
|
boundaryMessageCreatedAt >= sourceHistoryStrippedAt;
|
|
957
|
+
|
|
958
|
+
// Inherit compaction by the same temporal rule: apply the most recent
|
|
959
|
+
// compaction whose event time is at-or-before the forked-from message. A
|
|
960
|
+
// compaction that ran after the boundary message did not exist at that point
|
|
961
|
+
// in the conversation, so the fork branches from full uncompacted history.
|
|
962
|
+
const inheritedCompaction = getLatestCompactionEventAtOrBefore(
|
|
963
|
+
sourceConversation.id,
|
|
964
|
+
boundaryMessageCreatedAt,
|
|
965
|
+
);
|
|
966
|
+
// The Slack chronological-context watermark is single-valued on the source
|
|
967
|
+
// row and reflects only the latest compaction, so carry it only when the
|
|
968
|
+
// fork inherits that latest compaction. Pairing the latest watermark with an
|
|
969
|
+
// older inherited summary (a fork between two compactions) would filter out
|
|
970
|
+
// Slack messages the older summary does not cover.
|
|
971
|
+
const inheritsLatestCompaction =
|
|
972
|
+
inheritedCompaction != null &&
|
|
973
|
+
inheritedCompaction.compactedAt === sourceConversation.contextCompactedAt;
|
|
956
974
|
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
957
975
|
const forkTitle =
|
|
958
976
|
params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
@@ -984,19 +1002,14 @@ export function forkConversation(params: {
|
|
|
984
1002
|
.set({
|
|
985
1003
|
forkParentConversationId: sourceConversation.id,
|
|
986
1004
|
forkParentMessageId,
|
|
987
|
-
contextSummary:
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
: 0,
|
|
993
|
-
contextCompactedAt: preserveSourceCompactionState
|
|
994
|
-
? sourceConversation.contextCompactedAt
|
|
995
|
-
: null,
|
|
996
|
-
slackContextCompactionWatermarkTs: preserveSourceCompactionState
|
|
1005
|
+
contextSummary: inheritedCompaction?.summary ?? null,
|
|
1006
|
+
contextCompactedMessageCount:
|
|
1007
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1008
|
+
contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
|
|
1009
|
+
slackContextCompactionWatermarkTs: inheritsLatestCompaction
|
|
997
1010
|
? sourceConversation.slackContextCompactionWatermarkTs
|
|
998
1011
|
: null,
|
|
999
|
-
slackContextCompactionWatermarkAt:
|
|
1012
|
+
slackContextCompactionWatermarkAt: inheritsLatestCompaction
|
|
1000
1013
|
? sourceConversation.slackContextCompactionWatermarkAt
|
|
1001
1014
|
: null,
|
|
1002
1015
|
historyStrippedAt: inheritsHistoryStrippedAt
|
|
@@ -1042,8 +1055,8 @@ export function forkConversation(params: {
|
|
|
1042
1055
|
forkedMessageIds,
|
|
1043
1056
|
latestForkedAssistant,
|
|
1044
1057
|
isFullHistoryFork: copyBoundaryIndex === sourceMessages.length - 1,
|
|
1045
|
-
|
|
1046
|
-
|
|
1058
|
+
inheritedCompactedMessageCount:
|
|
1059
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1047
1060
|
diskSyncQueue,
|
|
1048
1061
|
});
|
|
1049
1062
|
|
|
@@ -1076,8 +1089,13 @@ interface PopulateForkContentsArgs {
|
|
|
1076
1089
|
latestForkedAssistant: { messageId: string; messageAt: number } | null;
|
|
1077
1090
|
/** `copyBoundaryIndex === sourceMessages.length - 1` for the source. */
|
|
1078
1091
|
isFullHistoryFork: boolean;
|
|
1079
|
-
|
|
1080
|
-
|
|
1092
|
+
/**
|
|
1093
|
+
* Count of leading messages behind the compaction event this fork inherits
|
|
1094
|
+
* (0 when the fork branches from uncompacted history). Memory-slug seeding
|
|
1095
|
+
* skips this prefix, since rows behind the fork's summary are not rendered
|
|
1096
|
+
* and must stay re-injectable.
|
|
1097
|
+
*/
|
|
1098
|
+
inheritedCompactedMessageCount: number;
|
|
1081
1099
|
/**
|
|
1082
1100
|
* When provided, a disk-sync entry is appended per copied message for the
|
|
1083
1101
|
* caller to flush after commit. Omitted by the retrospective fork, whose
|
|
@@ -1109,8 +1127,7 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
|
1109
1127
|
forkedMessageIds,
|
|
1110
1128
|
latestForkedAssistant,
|
|
1111
1129
|
isFullHistoryFork,
|
|
1112
|
-
|
|
1113
|
-
visibleWindowStartIndex,
|
|
1130
|
+
inheritedCompactedMessageCount,
|
|
1114
1131
|
diskSyncQueue,
|
|
1115
1132
|
} = args;
|
|
1116
1133
|
const db = getDb();
|
|
@@ -1218,9 +1235,10 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
|
1218
1235
|
// The v2 and v3 layers persist under separate metadata keys with the
|
|
1219
1236
|
// same `# memory/concepts/<slug>.md` header convention, so each seeds
|
|
1220
1237
|
// its own dedup record from its own blocks.
|
|
1221
|
-
const visibleStartIndex =
|
|
1222
|
-
|
|
1223
|
-
|
|
1238
|
+
const visibleStartIndex = Math.min(
|
|
1239
|
+
inheritedCompactedMessageCount,
|
|
1240
|
+
messagesToCopy.length,
|
|
1241
|
+
);
|
|
1224
1242
|
const inheritedSlugs = new Set<string>();
|
|
1225
1243
|
const inheritedV3Slugs = new Set<string>();
|
|
1226
1244
|
for (const message of messagesToCopy.slice(visibleStartIndex)) {
|
|
@@ -1256,6 +1274,15 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
|
|
|
1256
1274
|
forkedMessageIds,
|
|
1257
1275
|
lastCopiedSourceMessageId: messagesToCopy.at(-1)?.id ?? null,
|
|
1258
1276
|
});
|
|
1277
|
+
|
|
1278
|
+
// Carry the source's compaction events that predate the fork boundary so the
|
|
1279
|
+
// fork owns a correct ledger for its own future forks/compactions.
|
|
1280
|
+
forkCompactionLedger(
|
|
1281
|
+
db,
|
|
1282
|
+
sourceConversationId,
|
|
1283
|
+
fork.id,
|
|
1284
|
+
messagesToCopy.at(-1)?.createdAt ?? null,
|
|
1285
|
+
);
|
|
1259
1286
|
}
|
|
1260
1287
|
|
|
1261
1288
|
/**
|
|
@@ -1345,15 +1372,6 @@ export async function forkConversationForRetrospective(params: {
|
|
|
1345
1372
|
sourceMessages,
|
|
1346
1373
|
initialBoundaryIndex,
|
|
1347
1374
|
);
|
|
1348
|
-
const visibleWindowStartIndex = Math.max(
|
|
1349
|
-
0,
|
|
1350
|
-
Math.min(
|
|
1351
|
-
sourceConversation.contextCompactedMessageCount,
|
|
1352
|
-
sourceMessages.length,
|
|
1353
|
-
),
|
|
1354
|
-
);
|
|
1355
|
-
const preserveSourceCompactionState =
|
|
1356
|
-
copyBoundaryIndex >= visibleWindowStartIndex;
|
|
1357
1375
|
const messagesToCopy =
|
|
1358
1376
|
copyBoundaryIndex >= 0
|
|
1359
1377
|
? sourceMessages.slice(0, copyBoundaryIndex + 1)
|
|
@@ -1365,6 +1383,17 @@ export async function forkConversationForRetrospective(params: {
|
|
|
1365
1383
|
sourceHistoryStrippedAt != null &&
|
|
1366
1384
|
boundaryMessageCreatedAt != null &&
|
|
1367
1385
|
boundaryMessageCreatedAt >= sourceHistoryStrippedAt;
|
|
1386
|
+
// Inherit the most recent compaction whose event time is at-or-before the
|
|
1387
|
+
// forked-from message (see `forkConversation`).
|
|
1388
|
+
const inheritedCompaction = getLatestCompactionEventAtOrBefore(
|
|
1389
|
+
sourceConversation.id,
|
|
1390
|
+
boundaryMessageCreatedAt,
|
|
1391
|
+
);
|
|
1392
|
+
// Carry the Slack watermark only when inheriting the latest compaction
|
|
1393
|
+
// (see `forkConversation`).
|
|
1394
|
+
const inheritsLatestCompaction =
|
|
1395
|
+
inheritedCompaction != null &&
|
|
1396
|
+
inheritedCompaction.compactedAt === sourceConversation.contextCompactedAt;
|
|
1368
1397
|
const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
|
|
1369
1398
|
const forkTitle =
|
|
1370
1399
|
params.title ?? `${sourceConversation.title ?? "Untitled"} (Fork)`;
|
|
@@ -1393,19 +1422,14 @@ export async function forkConversationForRetrospective(params: {
|
|
|
1393
1422
|
.set({
|
|
1394
1423
|
forkParentConversationId: sourceConversation.id,
|
|
1395
1424
|
forkParentMessageId,
|
|
1396
|
-
contextSummary:
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
: 0,
|
|
1402
|
-
contextCompactedAt: preserveSourceCompactionState
|
|
1403
|
-
? sourceConversation.contextCompactedAt
|
|
1404
|
-
: null,
|
|
1405
|
-
slackContextCompactionWatermarkTs: preserveSourceCompactionState
|
|
1425
|
+
contextSummary: inheritedCompaction?.summary ?? null,
|
|
1426
|
+
contextCompactedMessageCount:
|
|
1427
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1428
|
+
contextCompactedAt: inheritedCompaction?.compactedAt ?? null,
|
|
1429
|
+
slackContextCompactionWatermarkTs: inheritsLatestCompaction
|
|
1406
1430
|
? sourceConversation.slackContextCompactionWatermarkTs
|
|
1407
1431
|
: null,
|
|
1408
|
-
slackContextCompactionWatermarkAt:
|
|
1432
|
+
slackContextCompactionWatermarkAt: inheritsLatestCompaction
|
|
1409
1433
|
? sourceConversation.slackContextCompactionWatermarkAt
|
|
1410
1434
|
: null,
|
|
1411
1435
|
historyStrippedAt: inheritsHistoryStrippedAt
|
|
@@ -1445,8 +1469,8 @@ export async function forkConversationForRetrospective(params: {
|
|
|
1445
1469
|
forkedMessageIds,
|
|
1446
1470
|
latestForkedAssistant,
|
|
1447
1471
|
isFullHistoryFork: copyBoundaryIndex === sourceMessages.length - 1,
|
|
1448
|
-
|
|
1449
|
-
|
|
1472
|
+
inheritedCompactedMessageCount:
|
|
1473
|
+
inheritedCompaction?.compactedMessageCount ?? 0,
|
|
1450
1474
|
});
|
|
1451
1475
|
});
|
|
1452
1476
|
|
|
@@ -2117,15 +2141,27 @@ export function updateConversationContextWindow(
|
|
|
2117
2141
|
contextCompactedMessageCount: number,
|
|
2118
2142
|
): void {
|
|
2119
2143
|
const db = getDb();
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2144
|
+
const now = Date.now();
|
|
2145
|
+
// Update the hot-path cache columns and append the event to the ledger in a
|
|
2146
|
+
// single transaction so the latest ledger event always matches the cache.
|
|
2147
|
+
db.transaction(() => {
|
|
2148
|
+
db.update(conversations)
|
|
2149
|
+
.set({
|
|
2150
|
+
contextSummary,
|
|
2151
|
+
contextCompactedMessageCount,
|
|
2152
|
+
contextCompactedAt: now,
|
|
2153
|
+
updatedAt: now,
|
|
2154
|
+
})
|
|
2155
|
+
.where(eq(conversations.id, id))
|
|
2156
|
+
.run();
|
|
2157
|
+
if (contextCompactedMessageCount > 0) {
|
|
2158
|
+
appendCompactionEvent(id, {
|
|
2159
|
+
compactedAt: now,
|
|
2160
|
+
summary: contextSummary,
|
|
2161
|
+
compactedMessageCount: contextCompactedMessageCount,
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2129
2165
|
}
|
|
2130
2166
|
|
|
2131
2167
|
export function setConversationHistoryStrippedAt(
|
|
@@ -2218,6 +2254,45 @@ export function isConversationProcessing(id: string): boolean {
|
|
|
2218
2254
|
return row?.processing_started_at != null;
|
|
2219
2255
|
}
|
|
2220
2256
|
|
|
2257
|
+
/**
|
|
2258
|
+
* Highest stream `seq` whose content is durably persisted to this
|
|
2259
|
+
* conversation's message rows, read from the `conversations.seq` column. This
|
|
2260
|
+
* is the snapshot↔stream alignment baseline `/messages` returns so a client
|
|
2261
|
+
* applies only stream events with a higher `seq`. `null` when none was
|
|
2262
|
+
* recorded (created before any stream activity, row predates the column, or
|
|
2263
|
+
* the conversation row is absent), in which case the client cold-starts.
|
|
2264
|
+
*
|
|
2265
|
+
* Seeded at creation with the global high-water seq and advanced on each
|
|
2266
|
+
* persistence flush by {@link recordConversationPersistedSeq}.
|
|
2267
|
+
*/
|
|
2268
|
+
export function getConversationPersistedSeq(id: string): number | null {
|
|
2269
|
+
const row = rawGet<{ seq: number | null }>(
|
|
2270
|
+
"SELECT seq FROM conversations WHERE id = ?",
|
|
2271
|
+
id,
|
|
2272
|
+
);
|
|
2273
|
+
return row?.seq ?? null;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Record that conversation `id` has durably persisted all of its events
|
|
2278
|
+
* through `seq`, writing the `conversations.seq` column. Called at each
|
|
2279
|
+
* persistence flush with the `seq` of the last event whose content the write
|
|
2280
|
+
* committed.
|
|
2281
|
+
*
|
|
2282
|
+
* Monotonic: the `WHERE seq IS NULL OR seq < ?` guard makes the update raise
|
|
2283
|
+
* the high-water mark only, so out-of-order async commits never regress it.
|
|
2284
|
+
* Non-positive or non-finite `seq` values are ignored.
|
|
2285
|
+
*/
|
|
2286
|
+
export function recordConversationPersistedSeq(id: string, seq: number): void {
|
|
2287
|
+
if (!Number.isFinite(seq) || seq <= 0) return;
|
|
2288
|
+
rawRun(
|
|
2289
|
+
"UPDATE conversations SET seq = ? WHERE id = ? AND (seq IS NULL OR seq < ?)",
|
|
2290
|
+
seq,
|
|
2291
|
+
id,
|
|
2292
|
+
seq,
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2221
2296
|
/**
|
|
2222
2297
|
* Set or clear the `surfaced_at` promotion marker for a conversation.
|
|
2223
2298
|
*
|