@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
|
@@ -31,7 +31,6 @@ mock.module("../daemon/handlers/shared.js", () => ({
|
|
|
31
31
|
|
|
32
32
|
import { eq } from "drizzle-orm";
|
|
33
33
|
|
|
34
|
-
import { upsertContact } from "../contacts/contact-store.js";
|
|
35
34
|
import { getDb } from "../memory/db-connection.js";
|
|
36
35
|
import { initializeDb } from "../memory/db-init.js";
|
|
37
36
|
import * as deliveryChannels from "../memory/delivery-channels.js";
|
|
@@ -39,7 +38,10 @@ import { resetTestTables } from "../memory/raw-query.js";
|
|
|
39
38
|
import { attachments, conversationAttentionEvents } from "../memory/schema.js";
|
|
40
39
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
41
40
|
import { resetDbForTesting } from "./db-test-helpers.js";
|
|
42
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
handleChannelInbound,
|
|
43
|
+
seedContactChannel,
|
|
44
|
+
} from "./helpers/channel-test-adapter.js";
|
|
43
45
|
|
|
44
46
|
await initializeDb();
|
|
45
47
|
|
|
@@ -69,16 +71,12 @@ function resetTables(): void {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
function ensureTestContact(): void {
|
|
72
|
-
|
|
74
|
+
seedContactChannel({
|
|
75
|
+
sourceChannel: "telegram",
|
|
76
|
+
externalUserId: "telegram-user-default",
|
|
73
77
|
displayName: "Test User",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
type: "telegram",
|
|
77
|
-
address: "telegram-user-default",
|
|
78
|
-
status: "active",
|
|
79
|
-
policy: "allow",
|
|
80
|
-
},
|
|
81
|
-
],
|
|
78
|
+
status: "active",
|
|
79
|
+
policy: "allow",
|
|
82
80
|
});
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -623,6 +623,24 @@ describe("classifyConversationError", () => {
|
|
|
623
623
|
expect(result.errorCategory).toBe("provider_invalid_key");
|
|
624
624
|
});
|
|
625
625
|
|
|
626
|
+
it("classifies managed-proxy auth failures as managed credential refresh failures", () => {
|
|
627
|
+
providerRoutingSources.anthropic = "managed-proxy";
|
|
628
|
+
const err = new ProviderError(
|
|
629
|
+
'Anthropic API error (403): {"detail":"API key has expired."}',
|
|
630
|
+
"anthropic",
|
|
631
|
+
403,
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const result = classifyConversationError(err, baseCtx);
|
|
635
|
+
|
|
636
|
+
expect(result.code).toBe("MANAGED_KEY_INVALID");
|
|
637
|
+
expect(result.userMessage).toBe(
|
|
638
|
+
"Couldn't refresh assistant credentials.",
|
|
639
|
+
);
|
|
640
|
+
expect(result.retryable).toBe(false);
|
|
641
|
+
expect(result.errorCategory).toBe("managed_key_invalid");
|
|
642
|
+
});
|
|
643
|
+
|
|
626
644
|
it("classifies ProviderError 401 with 'invalid x-api-key' message as PROVIDER_INVALID_KEY", () => {
|
|
627
645
|
// Regex-match branch — Anthropic's standard 401 wording.
|
|
628
646
|
const err = new ProviderError(
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
linkAttachmentToMessage,
|
|
27
27
|
uploadAttachment,
|
|
28
28
|
} from "../memory/attachments-store.js";
|
|
29
|
+
import { appendCompactionEvent } from "../memory/compaction-ledger-store.js";
|
|
29
30
|
import {
|
|
30
31
|
getAttentionStateByConversationIds,
|
|
31
32
|
markConversationUnread,
|
|
@@ -54,6 +55,7 @@ import {
|
|
|
54
55
|
activationState,
|
|
55
56
|
channelInboundEvents,
|
|
56
57
|
conversationAssistantAttentionState,
|
|
58
|
+
conversationCompactionEvents,
|
|
57
59
|
conversationGraphMemoryState,
|
|
58
60
|
conversations,
|
|
59
61
|
externalConversationBindings,
|
|
@@ -78,6 +80,7 @@ function resetTables(): void {
|
|
|
78
80
|
db.delete(externalConversationBindings).run();
|
|
79
81
|
db.delete(conversationAssistantAttentionState).run();
|
|
80
82
|
db.delete(activationState).run();
|
|
83
|
+
db.delete(conversationCompactionEvents).run();
|
|
81
84
|
db.delete(conversationGraphMemoryState).run();
|
|
82
85
|
db.delete(memoryRetrospectiveState).run();
|
|
83
86
|
getLogsDb()!.delete(llmRequestLogs).run();
|
|
@@ -346,24 +349,37 @@ describe("forkConversation", () => {
|
|
|
346
349
|
expect(toolResultUserRow.id).not.toBe(anchor.id);
|
|
347
350
|
});
|
|
348
351
|
|
|
349
|
-
test("
|
|
352
|
+
test("inherits the most recent compaction at-or-before the forked-from message", async () => {
|
|
350
353
|
const source = createConversation("Compacted thread");
|
|
351
|
-
await addMessage(source.id, "user", "Message 1", {
|
|
354
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
352
355
|
skipIndexing: true,
|
|
353
356
|
});
|
|
354
|
-
await addMessage(source.id, "assistant", "Message 2", {
|
|
357
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
355
358
|
skipIndexing: true,
|
|
356
359
|
});
|
|
357
360
|
const branchPoint = await addMessage(source.id, "user", "Message 3", {
|
|
358
361
|
skipIndexing: true,
|
|
359
362
|
});
|
|
360
|
-
await addMessage(source.id, "assistant", "Message 4", {
|
|
363
|
+
const m4 = await addMessage(source.id, "assistant", "Message 4", {
|
|
361
364
|
skipIndexing: true,
|
|
362
365
|
});
|
|
363
366
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
// Pin timestamps so the compaction event sits strictly between M2 and M3:
|
|
368
|
+
// it covers M1+M2 and ran before M3 was sent.
|
|
369
|
+
const db = getDb();
|
|
370
|
+
const base = Date.now();
|
|
371
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
372
|
+
db.run(
|
|
373
|
+
`UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
|
|
374
|
+
);
|
|
375
|
+
db.run(
|
|
376
|
+
`UPDATE messages SET created_at = ${base + 3} WHERE id = '${branchPoint.id}'`,
|
|
377
|
+
);
|
|
378
|
+
db.run(
|
|
379
|
+
`UPDATE messages SET created_at = ${base + 4} WHERE id = '${m4.id}'`,
|
|
380
|
+
);
|
|
381
|
+
const compactedAt = base + 2;
|
|
382
|
+
db.update(conversations)
|
|
367
383
|
.set({
|
|
368
384
|
contextSummary: "Compacted summary",
|
|
369
385
|
contextCompactedMessageCount: 2,
|
|
@@ -371,7 +387,14 @@ describe("forkConversation", () => {
|
|
|
371
387
|
})
|
|
372
388
|
.where(eq(conversations.id, source.id))
|
|
373
389
|
.run();
|
|
390
|
+
appendCompactionEvent(source.id, {
|
|
391
|
+
compactedAt,
|
|
392
|
+
summary: "Compacted summary",
|
|
393
|
+
compactedMessageCount: 2,
|
|
394
|
+
});
|
|
374
395
|
|
|
396
|
+
// M3 was sent after the compaction, so forking through it reproduces the
|
|
397
|
+
// compacted working context.
|
|
375
398
|
const fork = forkConversation({
|
|
376
399
|
conversationId: source.id,
|
|
377
400
|
throughMessageId: branchPoint.id,
|
|
@@ -384,6 +407,67 @@ describe("forkConversation", () => {
|
|
|
384
407
|
expect(fork.forkParentMessageId).toBe(branchPoint.id);
|
|
385
408
|
});
|
|
386
409
|
|
|
410
|
+
test("does not inherit a compaction that ran after the forked-from message", async () => {
|
|
411
|
+
const source = createConversation("Compacted thread");
|
|
412
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
413
|
+
skipIndexing: true,
|
|
414
|
+
});
|
|
415
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
416
|
+
skipIndexing: true,
|
|
417
|
+
});
|
|
418
|
+
const m3 = await addMessage(source.id, "user", "Message 3", {
|
|
419
|
+
skipIndexing: true,
|
|
420
|
+
});
|
|
421
|
+
const m4 = await addMessage(source.id, "assistant", "Message 4", {
|
|
422
|
+
skipIndexing: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// `/compact` ran after M4 — the compaction postdates every message.
|
|
426
|
+
const db = getDb();
|
|
427
|
+
const base = Date.now();
|
|
428
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
429
|
+
db.run(
|
|
430
|
+
`UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
|
|
431
|
+
);
|
|
432
|
+
db.run(
|
|
433
|
+
`UPDATE messages SET created_at = ${base + 2} WHERE id = '${m3.id}'`,
|
|
434
|
+
);
|
|
435
|
+
db.run(
|
|
436
|
+
`UPDATE messages SET created_at = ${base + 3} WHERE id = '${m4.id}'`,
|
|
437
|
+
);
|
|
438
|
+
const compactedAt = base + 10;
|
|
439
|
+
db.update(conversations)
|
|
440
|
+
.set({
|
|
441
|
+
contextSummary: "Compacted summary",
|
|
442
|
+
contextCompactedMessageCount: 2,
|
|
443
|
+
contextCompactedAt: compactedAt,
|
|
444
|
+
})
|
|
445
|
+
.where(eq(conversations.id, source.id))
|
|
446
|
+
.run();
|
|
447
|
+
appendCompactionEvent(source.id, {
|
|
448
|
+
compactedAt,
|
|
449
|
+
summary: "Compacted summary",
|
|
450
|
+
compactedMessageCount: 2,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Forking through the final message yields the full uncompacted history:
|
|
454
|
+
// the compaction did not exist when M4 was the latest turn.
|
|
455
|
+
const fork = forkConversation({
|
|
456
|
+
conversationId: source.id,
|
|
457
|
+
throughMessageId: m4.id,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(fork.contextSummary).toBeNull();
|
|
461
|
+
expect(fork.contextCompactedMessageCount).toBe(0);
|
|
462
|
+
expect(fork.contextCompactedAt).toBeNull();
|
|
463
|
+
expect(getMessages(fork.id).map((message) => message.content)).toEqual([
|
|
464
|
+
"Message 1",
|
|
465
|
+
"Message 2",
|
|
466
|
+
"Message 3",
|
|
467
|
+
"Message 4",
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
|
|
387
471
|
test("forks from the compacted-away prefix without inheriting source compaction state", async () => {
|
|
388
472
|
const source = createConversation("Compacted thread");
|
|
389
473
|
const compactedBranchPoint = await addMessage(
|
|
@@ -399,15 +483,21 @@ describe("forkConversation", () => {
|
|
|
399
483
|
skipIndexing: true,
|
|
400
484
|
});
|
|
401
485
|
|
|
486
|
+
const compactedAt = Date.now();
|
|
402
487
|
getDb()
|
|
403
488
|
.update(conversations)
|
|
404
489
|
.set({
|
|
405
490
|
contextSummary: "Compacted summary",
|
|
406
491
|
contextCompactedMessageCount: 2,
|
|
407
|
-
contextCompactedAt:
|
|
492
|
+
contextCompactedAt: compactedAt,
|
|
408
493
|
})
|
|
409
494
|
.where(eq(conversations.id, source.id))
|
|
410
495
|
.run();
|
|
496
|
+
appendCompactionEvent(source.id, {
|
|
497
|
+
compactedAt,
|
|
498
|
+
summary: "Compacted summary",
|
|
499
|
+
compactedMessageCount: 2,
|
|
500
|
+
});
|
|
411
501
|
|
|
412
502
|
const fork = forkConversation({
|
|
413
503
|
conversationId: source.id,
|
|
@@ -1000,35 +1090,70 @@ describe("forkConversation", () => {
|
|
|
1000
1090
|
|
|
1001
1091
|
test("truncated fork ignores attachments behind an inherited compaction boundary", async () => {
|
|
1002
1092
|
const source = createConversation("Compacted truncated thread");
|
|
1003
|
-
await addMessage(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1093
|
+
const compactedTurn = await addMessage(
|
|
1094
|
+
source.id,
|
|
1095
|
+
"user",
|
|
1096
|
+
"compacted turn",
|
|
1097
|
+
{
|
|
1098
|
+
metadata: {
|
|
1099
|
+
memoryInjectedBlock:
|
|
1100
|
+
"# memory/concepts/topics/page-compacted.md\nOld summary",
|
|
1101
|
+
},
|
|
1102
|
+
skipIndexing: true,
|
|
1007
1103
|
},
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1104
|
+
);
|
|
1105
|
+
const compactedReply = await addMessage(
|
|
1106
|
+
source.id,
|
|
1107
|
+
"assistant",
|
|
1108
|
+
"compacted reply",
|
|
1109
|
+
{ skipIndexing: true },
|
|
1110
|
+
);
|
|
1013
1111
|
const boundaryMessage = await addMessage(source.id, "user", "live turn", {
|
|
1014
1112
|
metadata: {
|
|
1015
1113
|
memoryInjectedBlock: "# memory/concepts/topics/page-live.md\nSummary",
|
|
1016
1114
|
},
|
|
1017
1115
|
skipIndexing: true,
|
|
1018
1116
|
});
|
|
1019
|
-
await addMessage(source.id, "assistant", "live reply", {
|
|
1117
|
+
const liveReply = await addMessage(source.id, "assistant", "live reply", {
|
|
1020
1118
|
skipIndexing: true,
|
|
1021
1119
|
});
|
|
1022
|
-
await addMessage(source.id, "user", "past boundary", {
|
|
1120
|
+
const pastBoundary = await addMessage(source.id, "user", "past boundary", {
|
|
1023
1121
|
skipIndexing: true,
|
|
1024
1122
|
});
|
|
1025
|
-
// First two messages sit behind
|
|
1026
|
-
//
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1123
|
+
// First two messages sit behind a compaction that ran before the live
|
|
1124
|
+
// turn: their injected blocks are not rendered, so the fork must not
|
|
1125
|
+
// claim them.
|
|
1126
|
+
const db = getDb();
|
|
1127
|
+
const base = Date.now();
|
|
1128
|
+
db.run(
|
|
1129
|
+
`UPDATE messages SET created_at = ${base} WHERE id = '${compactedTurn.id}'`,
|
|
1130
|
+
);
|
|
1131
|
+
db.run(
|
|
1132
|
+
`UPDATE messages SET created_at = ${base + 1} WHERE id = '${compactedReply.id}'`,
|
|
1133
|
+
);
|
|
1134
|
+
db.run(
|
|
1135
|
+
`UPDATE messages SET created_at = ${base + 3} WHERE id = '${boundaryMessage.id}'`,
|
|
1136
|
+
);
|
|
1137
|
+
db.run(
|
|
1138
|
+
`UPDATE messages SET created_at = ${base + 4} WHERE id = '${liveReply.id}'`,
|
|
1139
|
+
);
|
|
1140
|
+
db.run(
|
|
1141
|
+
`UPDATE messages SET created_at = ${base + 5} WHERE id = '${pastBoundary.id}'`,
|
|
1142
|
+
);
|
|
1143
|
+
const compactedAt = base + 2;
|
|
1144
|
+
db.update(conversations)
|
|
1145
|
+
.set({
|
|
1146
|
+
contextSummary: "Compacted summary",
|
|
1147
|
+
contextCompactedMessageCount: 2,
|
|
1148
|
+
contextCompactedAt: compactedAt,
|
|
1149
|
+
})
|
|
1030
1150
|
.where(eq(conversations.id, source.id))
|
|
1031
1151
|
.run();
|
|
1152
|
+
appendCompactionEvent(source.id, {
|
|
1153
|
+
compactedAt,
|
|
1154
|
+
summary: "Compacted summary",
|
|
1155
|
+
compactedMessageCount: 2,
|
|
1156
|
+
});
|
|
1032
1157
|
|
|
1033
1158
|
const fork = forkConversation({
|
|
1034
1159
|
conversationId: source.id,
|
|
@@ -1382,4 +1507,209 @@ describe("forkConversation + memory_retrospective_state", () => {
|
|
|
1382
1507
|
|
|
1383
1508
|
expect(getRetrospectiveState(fork.id)?.lastRunAt).toBe(1_700_000_000_000);
|
|
1384
1509
|
});
|
|
1510
|
+
|
|
1511
|
+
test("inherits the earlier of two compactions when forking between them", async () => {
|
|
1512
|
+
const source = createConversation("Twice-compacted thread");
|
|
1513
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
1514
|
+
skipIndexing: true,
|
|
1515
|
+
});
|
|
1516
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
1517
|
+
skipIndexing: true,
|
|
1518
|
+
});
|
|
1519
|
+
const m3 = await addMessage(source.id, "user", "Message 3", {
|
|
1520
|
+
skipIndexing: true,
|
|
1521
|
+
});
|
|
1522
|
+
const m4 = await addMessage(source.id, "assistant", "Message 4", {
|
|
1523
|
+
skipIndexing: true,
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
const db = getDb();
|
|
1527
|
+
const base = Date.now();
|
|
1528
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
1529
|
+
db.run(
|
|
1530
|
+
`UPDATE messages SET created_at = ${base + 2} WHERE id = '${m2.id}'`,
|
|
1531
|
+
);
|
|
1532
|
+
db.run(
|
|
1533
|
+
`UPDATE messages SET created_at = ${base + 4} WHERE id = '${m3.id}'`,
|
|
1534
|
+
);
|
|
1535
|
+
db.run(
|
|
1536
|
+
`UPDATE messages SET created_at = ${base + 6} WHERE id = '${m4.id}'`,
|
|
1537
|
+
);
|
|
1538
|
+
// C1 ran after M1 (covers 1 message); C2 ran after M3 (covers 3).
|
|
1539
|
+
appendCompactionEvent(source.id, {
|
|
1540
|
+
compactedAt: base + 1,
|
|
1541
|
+
summary: "Summary 1",
|
|
1542
|
+
compactedMessageCount: 1,
|
|
1543
|
+
});
|
|
1544
|
+
appendCompactionEvent(source.id, {
|
|
1545
|
+
compactedAt: base + 5,
|
|
1546
|
+
summary: "Summary 2",
|
|
1547
|
+
compactedMessageCount: 3,
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// Forking through M3 lands between the two compactions, so it inherits the
|
|
1551
|
+
// earlier one — the capability a single stored pointer cannot provide.
|
|
1552
|
+
const fork = forkConversation({
|
|
1553
|
+
conversationId: source.id,
|
|
1554
|
+
throughMessageId: m3.id,
|
|
1555
|
+
});
|
|
1556
|
+
expect(fork.contextSummary).toBe("Summary 1");
|
|
1557
|
+
expect(fork.contextCompactedMessageCount).toBe(1);
|
|
1558
|
+
expect(fork.contextCompactedAt).toBe(base + 1);
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
test("carries a ledger into the fork so re-forks resolve compaction", async () => {
|
|
1562
|
+
const source = createConversation("Re-fork thread");
|
|
1563
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
1564
|
+
skipIndexing: true,
|
|
1565
|
+
});
|
|
1566
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
1567
|
+
skipIndexing: true,
|
|
1568
|
+
});
|
|
1569
|
+
const m3 = await addMessage(source.id, "user", "Message 3", {
|
|
1570
|
+
skipIndexing: true,
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
const db = getDb();
|
|
1574
|
+
const base = Date.now();
|
|
1575
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
1576
|
+
db.run(
|
|
1577
|
+
`UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
|
|
1578
|
+
);
|
|
1579
|
+
db.run(
|
|
1580
|
+
`UPDATE messages SET created_at = ${base + 3} WHERE id = '${m3.id}'`,
|
|
1581
|
+
);
|
|
1582
|
+
const compactedAt = base + 2; // covers M1+M2, before M3
|
|
1583
|
+
db.update(conversations)
|
|
1584
|
+
.set({
|
|
1585
|
+
contextSummary: "Compacted summary",
|
|
1586
|
+
contextCompactedMessageCount: 2,
|
|
1587
|
+
contextCompactedAt: compactedAt,
|
|
1588
|
+
})
|
|
1589
|
+
.where(eq(conversations.id, source.id))
|
|
1590
|
+
.run();
|
|
1591
|
+
appendCompactionEvent(source.id, {
|
|
1592
|
+
compactedAt,
|
|
1593
|
+
summary: "Compacted summary",
|
|
1594
|
+
compactedMessageCount: 2,
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
1598
|
+
expect(fork.contextCompactedMessageCount).toBe(2);
|
|
1599
|
+
|
|
1600
|
+
// The fork owns a copy of the ledger, so a re-fork resolves the inherited
|
|
1601
|
+
// compaction without walking back to the original source.
|
|
1602
|
+
const reFork = forkConversation({ conversationId: fork.id });
|
|
1603
|
+
expect(reFork.contextSummary).toBe("Compacted summary");
|
|
1604
|
+
expect(reFork.contextCompactedMessageCount).toBe(2);
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
test("drops the stale Slack watermark when forking inherits an older compaction", async () => {
|
|
1608
|
+
const source = createConversation("Slack twice-compacted thread");
|
|
1609
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
1610
|
+
skipIndexing: true,
|
|
1611
|
+
});
|
|
1612
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
1613
|
+
skipIndexing: true,
|
|
1614
|
+
});
|
|
1615
|
+
const m3 = await addMessage(source.id, "user", "Message 3", {
|
|
1616
|
+
skipIndexing: true,
|
|
1617
|
+
});
|
|
1618
|
+
const m4 = await addMessage(source.id, "assistant", "Message 4", {
|
|
1619
|
+
skipIndexing: true,
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
const db = getDb();
|
|
1623
|
+
const base = Date.now();
|
|
1624
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
1625
|
+
db.run(
|
|
1626
|
+
`UPDATE messages SET created_at = ${base + 2} WHERE id = '${m2.id}'`,
|
|
1627
|
+
);
|
|
1628
|
+
db.run(
|
|
1629
|
+
`UPDATE messages SET created_at = ${base + 4} WHERE id = '${m3.id}'`,
|
|
1630
|
+
);
|
|
1631
|
+
db.run(
|
|
1632
|
+
`UPDATE messages SET created_at = ${base + 6} WHERE id = '${m4.id}'`,
|
|
1633
|
+
);
|
|
1634
|
+
appendCompactionEvent(source.id, {
|
|
1635
|
+
compactedAt: base + 1,
|
|
1636
|
+
summary: "Summary 1",
|
|
1637
|
+
compactedMessageCount: 1,
|
|
1638
|
+
});
|
|
1639
|
+
appendCompactionEvent(source.id, {
|
|
1640
|
+
compactedAt: base + 5,
|
|
1641
|
+
summary: "Summary 2",
|
|
1642
|
+
compactedMessageCount: 3,
|
|
1643
|
+
});
|
|
1644
|
+
// The source's single-valued watermark reflects only the latest compaction.
|
|
1645
|
+
db.update(conversations)
|
|
1646
|
+
.set({
|
|
1647
|
+
contextSummary: "Summary 2",
|
|
1648
|
+
contextCompactedMessageCount: 3,
|
|
1649
|
+
contextCompactedAt: base + 5,
|
|
1650
|
+
slackContextCompactionWatermarkTs: "ts-latest",
|
|
1651
|
+
slackContextCompactionWatermarkAt: base + 5,
|
|
1652
|
+
})
|
|
1653
|
+
.where(eq(conversations.id, source.id))
|
|
1654
|
+
.run();
|
|
1655
|
+
|
|
1656
|
+
// Forking through M3 inherits the OLDER compaction (Summary 1); the latest
|
|
1657
|
+
// watermark must not ride along, or it would hide Slack messages the older
|
|
1658
|
+
// summary does not cover.
|
|
1659
|
+
const fork = forkConversation({
|
|
1660
|
+
conversationId: source.id,
|
|
1661
|
+
throughMessageId: m3.id,
|
|
1662
|
+
});
|
|
1663
|
+
expect(fork.contextSummary).toBe("Summary 1");
|
|
1664
|
+
expect(fork.contextCompactedMessageCount).toBe(1);
|
|
1665
|
+
expect(fork.slackContextCompactionWatermarkTs).toBeNull();
|
|
1666
|
+
expect(fork.slackContextCompactionWatermarkAt).toBeNull();
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
test("carries the Slack watermark when forking inherits the latest compaction", async () => {
|
|
1670
|
+
const source = createConversation("Slack compacted thread");
|
|
1671
|
+
const m1 = await addMessage(source.id, "user", "Message 1", {
|
|
1672
|
+
skipIndexing: true,
|
|
1673
|
+
});
|
|
1674
|
+
const m2 = await addMessage(source.id, "assistant", "Message 2", {
|
|
1675
|
+
skipIndexing: true,
|
|
1676
|
+
});
|
|
1677
|
+
const m3 = await addMessage(source.id, "user", "Message 3", {
|
|
1678
|
+
skipIndexing: true,
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
const db = getDb();
|
|
1682
|
+
const base = Date.now();
|
|
1683
|
+
db.run(`UPDATE messages SET created_at = ${base} WHERE id = '${m1.id}'`);
|
|
1684
|
+
db.run(
|
|
1685
|
+
`UPDATE messages SET created_at = ${base + 1} WHERE id = '${m2.id}'`,
|
|
1686
|
+
);
|
|
1687
|
+
db.run(
|
|
1688
|
+
`UPDATE messages SET created_at = ${base + 3} WHERE id = '${m3.id}'`,
|
|
1689
|
+
);
|
|
1690
|
+
const compactedAt = base + 2; // latest (and only) compaction, covers M1+M2
|
|
1691
|
+
appendCompactionEvent(source.id, {
|
|
1692
|
+
compactedAt,
|
|
1693
|
+
summary: "Compacted summary",
|
|
1694
|
+
compactedMessageCount: 2,
|
|
1695
|
+
});
|
|
1696
|
+
db.update(conversations)
|
|
1697
|
+
.set({
|
|
1698
|
+
contextSummary: "Compacted summary",
|
|
1699
|
+
contextCompactedMessageCount: 2,
|
|
1700
|
+
contextCompactedAt: compactedAt,
|
|
1701
|
+
slackContextCompactionWatermarkTs: "ts-latest",
|
|
1702
|
+
slackContextCompactionWatermarkAt: compactedAt,
|
|
1703
|
+
})
|
|
1704
|
+
.where(eq(conversations.id, source.id))
|
|
1705
|
+
.run();
|
|
1706
|
+
|
|
1707
|
+
const fork = forkConversation({
|
|
1708
|
+
conversationId: source.id,
|
|
1709
|
+
throughMessageId: m3.id,
|
|
1710
|
+
});
|
|
1711
|
+
expect(fork.contextCompactedMessageCount).toBe(2);
|
|
1712
|
+
expect(fork.slackContextCompactionWatermarkTs).toBe("ts-latest");
|
|
1713
|
+
expect(fork.slackContextCompactionWatermarkAt).toBe(compactedAt);
|
|
1714
|
+
});
|
|
1385
1715
|
});
|