@vellumai/assistant 0.6.0 → 0.6.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for handleListMessages tool_result merging.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that tool_result blocks from user messages are merged into the
|
|
5
|
+
* preceding assistant message so they render with proper tool names instead
|
|
6
|
+
* of "Unknown" after a conversation reload.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
mock.module("../util/logger.js", () => ({
|
|
12
|
+
getLogger: () =>
|
|
13
|
+
new Proxy({} as Record<string, unknown>, {
|
|
14
|
+
get: () => () => {},
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module("../config/loader.js", () => ({
|
|
19
|
+
getConfig: () => ({
|
|
20
|
+
ui: {},
|
|
21
|
+
model: "test",
|
|
22
|
+
provider: "test",
|
|
23
|
+
memory: { enabled: false },
|
|
24
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { addMessage, createConversation } from "../memory/conversation-crud.js";
|
|
29
|
+
import { getDb, initializeDb } from "../memory/db.js";
|
|
30
|
+
import { handleListMessages } from "../runtime/routes/conversation-routes.js";
|
|
31
|
+
|
|
32
|
+
initializeDb();
|
|
33
|
+
|
|
34
|
+
function resetTables() {
|
|
35
|
+
const db = getDb();
|
|
36
|
+
db.run("DELETE FROM message_attachments");
|
|
37
|
+
db.run("DELETE FROM attachments");
|
|
38
|
+
db.run("DELETE FROM messages");
|
|
39
|
+
db.run("DELETE FROM conversations");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createTestUrl(conversationId: string): URL {
|
|
43
|
+
return new URL(
|
|
44
|
+
`http://localhost/v1/messages?conversationId=${conversationId}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ToolCallPayload {
|
|
49
|
+
name: string;
|
|
50
|
+
input: Record<string, unknown>;
|
|
51
|
+
result?: string;
|
|
52
|
+
isError?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface MessagePayload {
|
|
56
|
+
role: string;
|
|
57
|
+
content: string;
|
|
58
|
+
toolCalls?: ToolCallPayload[];
|
|
59
|
+
textSegments?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("handleListMessages tool_result merging", () => {
|
|
63
|
+
beforeEach(resetTables);
|
|
64
|
+
|
|
65
|
+
test("merges tool_result from user message into preceding assistant", async () => {
|
|
66
|
+
const conv = createConversation();
|
|
67
|
+
// User prompt
|
|
68
|
+
await addMessage(
|
|
69
|
+
conv.id,
|
|
70
|
+
"user",
|
|
71
|
+
JSON.stringify([{ type: "text", text: "run ls" }]),
|
|
72
|
+
);
|
|
73
|
+
// Assistant with tool_use
|
|
74
|
+
await addMessage(
|
|
75
|
+
conv.id,
|
|
76
|
+
"assistant",
|
|
77
|
+
JSON.stringify([
|
|
78
|
+
{ type: "text", text: "Running command." },
|
|
79
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
// Tool result (separate user message)
|
|
83
|
+
await addMessage(
|
|
84
|
+
conv.id,
|
|
85
|
+
"user",
|
|
86
|
+
JSON.stringify([
|
|
87
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "file1.txt\nfile2.txt" },
|
|
88
|
+
]),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
92
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
93
|
+
|
|
94
|
+
// Should be 2 messages: user prompt + assistant (tool_result user msg suppressed)
|
|
95
|
+
expect(body.messages).toHaveLength(2);
|
|
96
|
+
expect(body.messages[0].role).toBe("user");
|
|
97
|
+
expect(body.messages[1].role).toBe("assistant");
|
|
98
|
+
|
|
99
|
+
// Assistant tool call should have proper name AND result
|
|
100
|
+
const toolCalls = body.messages[1].toolCalls;
|
|
101
|
+
expect(toolCalls).toBeDefined();
|
|
102
|
+
expect(toolCalls).toHaveLength(1);
|
|
103
|
+
expect(toolCalls![0].name).toBe("bash");
|
|
104
|
+
expect(toolCalls![0].result).toBe("file1.txt\nfile2.txt");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("merges multiple tool_results into matching tool_uses", async () => {
|
|
108
|
+
const conv = createConversation();
|
|
109
|
+
await addMessage(
|
|
110
|
+
conv.id,
|
|
111
|
+
"user",
|
|
112
|
+
JSON.stringify([{ type: "text", text: "do stuff" }]),
|
|
113
|
+
);
|
|
114
|
+
await addMessage(
|
|
115
|
+
conv.id,
|
|
116
|
+
"assistant",
|
|
117
|
+
JSON.stringify([
|
|
118
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
|
|
119
|
+
{ type: "text", text: "and also" },
|
|
120
|
+
{ type: "tool_use", id: "tu2", name: "file_read", input: { path: "/tmp/a" } },
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
await addMessage(
|
|
124
|
+
conv.id,
|
|
125
|
+
"user",
|
|
126
|
+
JSON.stringify([
|
|
127
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "dir listing" },
|
|
128
|
+
{ type: "tool_result", tool_use_id: "tu2", content: "file contents" },
|
|
129
|
+
]),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
133
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
134
|
+
|
|
135
|
+
expect(body.messages).toHaveLength(2);
|
|
136
|
+
const toolCalls = body.messages[1].toolCalls!;
|
|
137
|
+
expect(toolCalls).toHaveLength(2);
|
|
138
|
+
expect(toolCalls[0].name).toBe("bash");
|
|
139
|
+
expect(toolCalls[0].result).toBe("dir listing");
|
|
140
|
+
expect(toolCalls[1].name).toBe("file_read");
|
|
141
|
+
expect(toolCalls[1].result).toBe("file contents");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("plain user message passes through unchanged", async () => {
|
|
145
|
+
const conv = createConversation();
|
|
146
|
+
await addMessage(
|
|
147
|
+
conv.id,
|
|
148
|
+
"user",
|
|
149
|
+
JSON.stringify([{ type: "text", text: "hello" }]),
|
|
150
|
+
);
|
|
151
|
+
await addMessage(
|
|
152
|
+
conv.id,
|
|
153
|
+
"assistant",
|
|
154
|
+
JSON.stringify([{ type: "text", text: "hi there" }]),
|
|
155
|
+
);
|
|
156
|
+
await addMessage(
|
|
157
|
+
conv.id,
|
|
158
|
+
"user",
|
|
159
|
+
JSON.stringify([{ type: "text", text: "how are you?" }]),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
163
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
164
|
+
|
|
165
|
+
expect(body.messages).toHaveLength(3);
|
|
166
|
+
expect(body.messages[2].role).toBe("user");
|
|
167
|
+
expect(body.messages[2].content).toBe("how are you?");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("tool_result at start of array (no preceding assistant) is preserved", async () => {
|
|
171
|
+
const conv = createConversation();
|
|
172
|
+
// Orphan tool_result with no preceding assistant (pagination boundary).
|
|
173
|
+
// The preceding assistant tool_use lives in the previous page — dropping
|
|
174
|
+
// the result would be unrecoverable, so it is kept as-is.
|
|
175
|
+
await addMessage(
|
|
176
|
+
conv.id,
|
|
177
|
+
"user",
|
|
178
|
+
JSON.stringify([
|
|
179
|
+
{ type: "tool_result", tool_use_id: "tu_orphan", content: "stale result" },
|
|
180
|
+
]),
|
|
181
|
+
);
|
|
182
|
+
await addMessage(
|
|
183
|
+
conv.id,
|
|
184
|
+
"assistant",
|
|
185
|
+
JSON.stringify([{ type: "text", text: "response" }]),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
189
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
190
|
+
|
|
191
|
+
// Orphan tool_result is preserved (not suppressed) to avoid data loss
|
|
192
|
+
expect(body.messages).toHaveLength(2);
|
|
193
|
+
expect(body.messages[0].role).toBe("user");
|
|
194
|
+
// The preserved message must retain the actual tool_result payload
|
|
195
|
+
const orphanToolCalls = body.messages[0].toolCalls;
|
|
196
|
+
expect(orphanToolCalls).toBeDefined();
|
|
197
|
+
expect(orphanToolCalls).toHaveLength(1);
|
|
198
|
+
expect(orphanToolCalls![0].result).toBe("stale result");
|
|
199
|
+
expect(body.messages[1].role).toBe("assistant");
|
|
200
|
+
expect(body.messages[1].content).toBe("response");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("multi-turn: each tool_result merges into correct assistant", async () => {
|
|
204
|
+
const conv = createConversation();
|
|
205
|
+
// Turn 1
|
|
206
|
+
await addMessage(
|
|
207
|
+
conv.id,
|
|
208
|
+
"user",
|
|
209
|
+
JSON.stringify([{ type: "text", text: "list files" }]),
|
|
210
|
+
);
|
|
211
|
+
await addMessage(
|
|
212
|
+
conv.id,
|
|
213
|
+
"assistant",
|
|
214
|
+
JSON.stringify([
|
|
215
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
|
|
216
|
+
]),
|
|
217
|
+
);
|
|
218
|
+
await addMessage(
|
|
219
|
+
conv.id,
|
|
220
|
+
"user",
|
|
221
|
+
JSON.stringify([
|
|
222
|
+
{ type: "tool_result", tool_use_id: "tu1", content: "files" },
|
|
223
|
+
]),
|
|
224
|
+
);
|
|
225
|
+
// Turn 2
|
|
226
|
+
await addMessage(
|
|
227
|
+
conv.id,
|
|
228
|
+
"assistant",
|
|
229
|
+
JSON.stringify([
|
|
230
|
+
{ type: "text", text: "Now reading:" },
|
|
231
|
+
{ type: "tool_use", id: "tu2", name: "file_read", input: { path: "/x" } },
|
|
232
|
+
]),
|
|
233
|
+
);
|
|
234
|
+
await addMessage(
|
|
235
|
+
conv.id,
|
|
236
|
+
"user",
|
|
237
|
+
JSON.stringify([
|
|
238
|
+
{ type: "tool_result", tool_use_id: "tu2", content: "file data" },
|
|
239
|
+
]),
|
|
240
|
+
);
|
|
241
|
+
// Turn 3: real user message
|
|
242
|
+
await addMessage(
|
|
243
|
+
conv.id,
|
|
244
|
+
"user",
|
|
245
|
+
JSON.stringify([{ type: "text", text: "thanks" }]),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
249
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
250
|
+
|
|
251
|
+
// user("list files"), assistant(bash), assistant(file_read), user("thanks")
|
|
252
|
+
expect(body.messages).toHaveLength(4);
|
|
253
|
+
expect(body.messages[0].role).toBe("user");
|
|
254
|
+
expect(body.messages[1].role).toBe("assistant");
|
|
255
|
+
expect(body.messages[1].toolCalls![0].name).toBe("bash");
|
|
256
|
+
expect(body.messages[1].toolCalls![0].result).toBe("files");
|
|
257
|
+
expect(body.messages[2].role).toBe("assistant");
|
|
258
|
+
expect(body.messages[2].toolCalls![0].name).toBe("file_read");
|
|
259
|
+
expect(body.messages[2].toolCalls![0].result).toBe("file data");
|
|
260
|
+
expect(body.messages[3].role).toBe("user");
|
|
261
|
+
expect(body.messages[3].content).toBe("thanks");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("tool_result with is_error propagates error status", async () => {
|
|
265
|
+
const conv = createConversation();
|
|
266
|
+
await addMessage(
|
|
267
|
+
conv.id,
|
|
268
|
+
"user",
|
|
269
|
+
JSON.stringify([{ type: "text", text: "do it" }]),
|
|
270
|
+
);
|
|
271
|
+
await addMessage(
|
|
272
|
+
conv.id,
|
|
273
|
+
"assistant",
|
|
274
|
+
JSON.stringify([
|
|
275
|
+
{ type: "tool_use", id: "tu1", name: "bash", input: { command: "fail" } },
|
|
276
|
+
]),
|
|
277
|
+
);
|
|
278
|
+
await addMessage(
|
|
279
|
+
conv.id,
|
|
280
|
+
"user",
|
|
281
|
+
JSON.stringify([
|
|
282
|
+
{
|
|
283
|
+
type: "tool_result",
|
|
284
|
+
tool_use_id: "tu1",
|
|
285
|
+
content: "command not found",
|
|
286
|
+
is_error: true,
|
|
287
|
+
},
|
|
288
|
+
]),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
292
|
+
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
293
|
+
|
|
294
|
+
expect(body.messages).toHaveLength(2);
|
|
295
|
+
const tc = body.messages[1].toolCalls![0];
|
|
296
|
+
expect(tc.name).toBe("bash");
|
|
297
|
+
expect(tc.result).toBe("command not found");
|
|
298
|
+
expect(tc.isError).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -278,12 +278,6 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
278
278
|
role: "user",
|
|
279
279
|
text: "Find the latest changelog.",
|
|
280
280
|
},
|
|
281
|
-
{
|
|
282
|
-
kind: "message",
|
|
283
|
-
label: "Assistant message 2",
|
|
284
|
-
role: "assistant",
|
|
285
|
-
text: "Checking sources.",
|
|
286
|
-
},
|
|
287
281
|
{
|
|
288
282
|
kind: "reasoning",
|
|
289
283
|
label: "Assistant message 2 reasoning",
|
|
@@ -296,6 +290,12 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
296
290
|
role: "assistant",
|
|
297
291
|
text: "[redacted thinking]",
|
|
298
292
|
},
|
|
293
|
+
{
|
|
294
|
+
kind: "message",
|
|
295
|
+
label: "Assistant message 2",
|
|
296
|
+
role: "assistant",
|
|
297
|
+
text: "Checking sources.",
|
|
298
|
+
},
|
|
299
299
|
{
|
|
300
300
|
kind: "tool_use",
|
|
301
301
|
label: "Assistant message 2 tool use",
|
|
@@ -331,12 +331,6 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
331
331
|
},
|
|
332
332
|
]);
|
|
333
333
|
expect(normalized.responseSections).toEqual([
|
|
334
|
-
{
|
|
335
|
-
kind: "message",
|
|
336
|
-
label: "Assistant response",
|
|
337
|
-
role: "assistant",
|
|
338
|
-
text: "I found the changelog.",
|
|
339
|
-
},
|
|
340
334
|
{
|
|
341
335
|
kind: "reasoning",
|
|
342
336
|
label: "Assistant response reasoning",
|
|
@@ -349,6 +343,12 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
349
343
|
role: "assistant",
|
|
350
344
|
text: "[redacted thinking]",
|
|
351
345
|
},
|
|
346
|
+
{
|
|
347
|
+
kind: "message",
|
|
348
|
+
label: "Assistant response",
|
|
349
|
+
role: "assistant",
|
|
350
|
+
text: "I found the changelog.",
|
|
351
|
+
},
|
|
352
352
|
{
|
|
353
353
|
kind: "tool_use",
|
|
354
354
|
label: "Assistant response tool use",
|
|
@@ -412,12 +412,6 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
412
412
|
toolCallNames: undefined,
|
|
413
413
|
});
|
|
414
414
|
expect(normalized.responseSections).toEqual([
|
|
415
|
-
{
|
|
416
|
-
kind: "message",
|
|
417
|
-
label: "Assistant response",
|
|
418
|
-
role: "assistant",
|
|
419
|
-
text: "The answer is 42.",
|
|
420
|
-
},
|
|
421
415
|
{
|
|
422
416
|
kind: "reasoning",
|
|
423
417
|
label: "Assistant response reasoning",
|
|
@@ -430,6 +424,12 @@ describe("normalizeLlmContextPayloads", () => {
|
|
|
430
424
|
role: "assistant",
|
|
431
425
|
text: "[redacted thinking]",
|
|
432
426
|
},
|
|
427
|
+
{
|
|
428
|
+
kind: "message",
|
|
429
|
+
label: "Assistant response",
|
|
430
|
+
role: "assistant",
|
|
431
|
+
text: "The answer is 42.",
|
|
432
|
+
},
|
|
433
433
|
]);
|
|
434
434
|
});
|
|
435
435
|
|
|
@@ -33,6 +33,27 @@ function dispatchLlmContext(messageId: string): Promise<Response> | Response {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function dispatchLogPayload(logId: string): Promise<Response> | Response {
|
|
37
|
+
const url = new URL(
|
|
38
|
+
`http://localhost/v1/llm-request-logs/${logId}/payload`,
|
|
39
|
+
);
|
|
40
|
+
const route = routes.find(
|
|
41
|
+
(r) =>
|
|
42
|
+
r.method === "GET" && r.endpoint === "llm-request-logs/:id/payload",
|
|
43
|
+
);
|
|
44
|
+
if (!route) {
|
|
45
|
+
throw new Error("No llm-request-logs payload route found");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return route.handler({
|
|
49
|
+
req: new Request(url.toString(), { method: "GET" }),
|
|
50
|
+
url,
|
|
51
|
+
server: null as never,
|
|
52
|
+
authContext: {} as never,
|
|
53
|
+
params: { id: logId },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
function clearRequestLogs(): void {
|
|
37
58
|
getDb().delete(llmRequestLogs).run();
|
|
38
59
|
}
|
|
@@ -189,4 +210,84 @@ describe("GET /v1/messages/:id/llm-context provider preference", () => {
|
|
|
189
210
|
expect(body.logs).toHaveLength(1);
|
|
190
211
|
expect(body.logs[0]?.summary).toEqual({ provider: "ollama" });
|
|
191
212
|
});
|
|
213
|
+
|
|
214
|
+
test("returns null payloads to keep the initial response lightweight", async () => {
|
|
215
|
+
seedRequestLog({
|
|
216
|
+
id: "log-null-payload",
|
|
217
|
+
messageId: "msg-null-payload",
|
|
218
|
+
provider: "openrouter",
|
|
219
|
+
requestPayload: openAiRequestPayload,
|
|
220
|
+
responsePayload: openAiResponsePayload,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const response = await dispatchLlmContext("msg-null-payload");
|
|
224
|
+
expect(response.status).toBe(200);
|
|
225
|
+
|
|
226
|
+
const body = (await response.json()) as {
|
|
227
|
+
logs: Array<{
|
|
228
|
+
requestPayload: unknown;
|
|
229
|
+
responsePayload: unknown;
|
|
230
|
+
}>;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
expect(body.logs).toHaveLength(1);
|
|
234
|
+
expect(body.logs[0]?.requestPayload).toBeNull();
|
|
235
|
+
expect(body.logs[0]?.responsePayload).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("GET /v1/llm-request-logs/:id/payload", () => {
|
|
240
|
+
test("returns parsed payloads for a valid log", async () => {
|
|
241
|
+
const reqPayload = JSON.stringify({ model: "gpt-4.1", messages: [] });
|
|
242
|
+
const resPayload = JSON.stringify({
|
|
243
|
+
choices: [{ message: { content: "hi" } }],
|
|
244
|
+
});
|
|
245
|
+
seedRequestLog({
|
|
246
|
+
id: "log-payload-ok",
|
|
247
|
+
messageId: "msg-payload-ok",
|
|
248
|
+
provider: "openai",
|
|
249
|
+
requestPayload: reqPayload,
|
|
250
|
+
responsePayload: resPayload,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const response = await dispatchLogPayload("log-payload-ok");
|
|
254
|
+
expect(response.status).toBe(200);
|
|
255
|
+
|
|
256
|
+
const body = (await response.json()) as {
|
|
257
|
+
id: string;
|
|
258
|
+
requestPayload: unknown;
|
|
259
|
+
responsePayload: unknown;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
expect(body.id).toBe("log-payload-ok");
|
|
263
|
+
expect(body.requestPayload).toEqual(JSON.parse(reqPayload));
|
|
264
|
+
expect(body.responsePayload).toEqual(JSON.parse(resPayload));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("returns 404 for a nonexistent log", async () => {
|
|
268
|
+
const response = await dispatchLogPayload("does-not-exist");
|
|
269
|
+
expect(response.status).toBe(404);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("falls back to string values for non-JSON payloads", async () => {
|
|
273
|
+
seedRequestLog({
|
|
274
|
+
id: "log-raw-strings",
|
|
275
|
+
messageId: "msg-raw-strings",
|
|
276
|
+
provider: null,
|
|
277
|
+
requestPayload: "raw-request-text",
|
|
278
|
+
responsePayload: "raw-response-text",
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const response = await dispatchLogPayload("log-raw-strings");
|
|
282
|
+
expect(response.status).toBe(200);
|
|
283
|
+
|
|
284
|
+
const body = (await response.json()) as {
|
|
285
|
+
id: string;
|
|
286
|
+
requestPayload: unknown;
|
|
287
|
+
responsePayload: unknown;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
expect(body.requestPayload).toBe("raw-request-text");
|
|
291
|
+
expect(body.responsePayload).toBe("raw-response-text");
|
|
292
|
+
});
|
|
192
293
|
});
|
|
@@ -301,6 +301,168 @@ describe("getRequestLogsByMessageId — turn-aware query", () => {
|
|
|
301
301
|
expect(logs[2]?.id).toBe("log-surviving");
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
test("recovers unlinked logs (messageId IS NULL) within the turn time range", () => {
|
|
305
|
+
// Simulate the race: logs recorded with NULL messageId, backfill hasn't run yet.
|
|
306
|
+
const T = 1_700_000_000_000;
|
|
307
|
+
const db = getDb();
|
|
308
|
+
const conv = createConversation("unlinked-test");
|
|
309
|
+
|
|
310
|
+
db.run(
|
|
311
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-ul', ${conv.id}, 'user', '"Do the task"', ${T})`,
|
|
312
|
+
);
|
|
313
|
+
db.run(
|
|
314
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-ul', ${conv.id}, 'assistant', '"Done!"', ${T + 30000})`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Unlinked log: messageId is NULL (backfill hasn't run yet)
|
|
318
|
+
db.insert(llmRequestLogs)
|
|
319
|
+
.values({
|
|
320
|
+
id: "log-unlinked-1",
|
|
321
|
+
conversationId: conv.id,
|
|
322
|
+
messageId: null,
|
|
323
|
+
provider: "anthropic",
|
|
324
|
+
requestPayload: '{"step":1}',
|
|
325
|
+
responsePayload: '{"tool":"bash"}',
|
|
326
|
+
createdAt: T + 5000,
|
|
327
|
+
})
|
|
328
|
+
.run();
|
|
329
|
+
|
|
330
|
+
// Linked log: already backfilled to the assistant message
|
|
331
|
+
db.insert(llmRequestLogs)
|
|
332
|
+
.values({
|
|
333
|
+
id: "log-linked-1",
|
|
334
|
+
conversationId: conv.id,
|
|
335
|
+
messageId: "a1-ul",
|
|
336
|
+
provider: "anthropic",
|
|
337
|
+
requestPayload: '{"step":2}',
|
|
338
|
+
responsePayload: '{"text":"Done!"}',
|
|
339
|
+
createdAt: T + 29_000,
|
|
340
|
+
})
|
|
341
|
+
.run();
|
|
342
|
+
|
|
343
|
+
const logs = getRequestLogsByMessageId("a1-ul");
|
|
344
|
+
expect(logs).toHaveLength(2);
|
|
345
|
+
expect(logs[0]?.id).toBe("log-unlinked-1");
|
|
346
|
+
expect(logs[1]?.id).toBe("log-linked-1");
|
|
347
|
+
|
|
348
|
+
// Verify opportunistic backfill ran: the unlinked log should now have a messageId
|
|
349
|
+
const backfilledLog = db
|
|
350
|
+
.select({ messageId: llmRequestLogs.messageId })
|
|
351
|
+
.from(llmRequestLogs)
|
|
352
|
+
.where(sql`${llmRequestLogs.id} = 'log-unlinked-1'`)
|
|
353
|
+
.get();
|
|
354
|
+
expect(backfilledLog?.messageId).toBe("a1-ul");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("unlinked logs from different conversations don't bleed", () => {
|
|
358
|
+
const T = 1_700_000_000_000;
|
|
359
|
+
const db = getDb();
|
|
360
|
+
const convA = createConversation("conv-a");
|
|
361
|
+
const convB = createConversation("conv-b");
|
|
362
|
+
|
|
363
|
+
// Conversation A: user + assistant + unlinked log
|
|
364
|
+
db.run(
|
|
365
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-a', ${convA.id}, 'user', '"Hello A"', ${T})`,
|
|
366
|
+
);
|
|
367
|
+
db.run(
|
|
368
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-a', ${convA.id}, 'assistant', '"Hi A"', ${T + 10000})`,
|
|
369
|
+
);
|
|
370
|
+
db.insert(llmRequestLogs)
|
|
371
|
+
.values({
|
|
372
|
+
id: "log-conv-a",
|
|
373
|
+
conversationId: convA.id,
|
|
374
|
+
messageId: null,
|
|
375
|
+
provider: "anthropic",
|
|
376
|
+
requestPayload: '{"conv":"A"}',
|
|
377
|
+
responsePayload: '{"r":"A"}',
|
|
378
|
+
createdAt: T + 5000,
|
|
379
|
+
})
|
|
380
|
+
.run();
|
|
381
|
+
|
|
382
|
+
// Conversation B: user + assistant + unlinked log (overlapping timestamps)
|
|
383
|
+
db.run(
|
|
384
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-b', ${convB.id}, 'user', '"Hello B"', ${T})`,
|
|
385
|
+
);
|
|
386
|
+
db.run(
|
|
387
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-b', ${convB.id}, 'assistant', '"Hi B"', ${T + 10000})`,
|
|
388
|
+
);
|
|
389
|
+
db.insert(llmRequestLogs)
|
|
390
|
+
.values({
|
|
391
|
+
id: "log-conv-b",
|
|
392
|
+
conversationId: convB.id,
|
|
393
|
+
messageId: null,
|
|
394
|
+
provider: "anthropic",
|
|
395
|
+
requestPayload: '{"conv":"B"}',
|
|
396
|
+
responsePayload: '{"r":"B"}',
|
|
397
|
+
createdAt: T + 5000,
|
|
398
|
+
})
|
|
399
|
+
.run();
|
|
400
|
+
|
|
401
|
+
// Query from conv A → should only find conv A's log
|
|
402
|
+
const logsA = getRequestLogsByMessageId("a1-a");
|
|
403
|
+
expect(logsA).toHaveLength(1);
|
|
404
|
+
expect(logsA[0]?.id).toBe("log-conv-a");
|
|
405
|
+
|
|
406
|
+
// Query from conv B → should only find conv B's log
|
|
407
|
+
const logsB = getRequestLogsByMessageId("a1-b");
|
|
408
|
+
expect(logsB).toHaveLength(1);
|
|
409
|
+
expect(logsB[0]?.id).toBe("log-conv-b");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("unlinked logs from different turns don't bleed", () => {
|
|
413
|
+
const T = 1_700_000_000_000;
|
|
414
|
+
const db = getDb();
|
|
415
|
+
const conv = createConversation("two-turn-unlinked");
|
|
416
|
+
|
|
417
|
+
// Turn 1: user → assistant (with linked log)
|
|
418
|
+
db.run(
|
|
419
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-t', ${conv.id}, 'user', '"Turn 1"', ${T})`,
|
|
420
|
+
);
|
|
421
|
+
db.run(
|
|
422
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-t', ${conv.id}, 'assistant', '"Answer 1"', ${T + 10000})`,
|
|
423
|
+
);
|
|
424
|
+
db.insert(llmRequestLogs)
|
|
425
|
+
.values({
|
|
426
|
+
id: "log-turn1-unlinked",
|
|
427
|
+
conversationId: conv.id,
|
|
428
|
+
messageId: null,
|
|
429
|
+
provider: "anthropic",
|
|
430
|
+
requestPayload: '{"turn":1}',
|
|
431
|
+
responsePayload: '{"r":1}',
|
|
432
|
+
createdAt: T + 5000,
|
|
433
|
+
})
|
|
434
|
+
.run();
|
|
435
|
+
|
|
436
|
+
// Turn 2: user → assistant (with unlinked log)
|
|
437
|
+
db.run(
|
|
438
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u2-t', ${conv.id}, 'user', '"Turn 2"', ${T + 60000})`,
|
|
439
|
+
);
|
|
440
|
+
db.run(
|
|
441
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a2-t', ${conv.id}, 'assistant', '"Answer 2"', ${T + 70000})`,
|
|
442
|
+
);
|
|
443
|
+
db.insert(llmRequestLogs)
|
|
444
|
+
.values({
|
|
445
|
+
id: "log-turn2-unlinked",
|
|
446
|
+
conversationId: conv.id,
|
|
447
|
+
messageId: null,
|
|
448
|
+
provider: "anthropic",
|
|
449
|
+
requestPayload: '{"turn":2}',
|
|
450
|
+
responsePayload: '{"r":2}',
|
|
451
|
+
createdAt: T + 65000,
|
|
452
|
+
})
|
|
453
|
+
.run();
|
|
454
|
+
|
|
455
|
+
// Query turn 2 → should only find turn 2's unlinked log
|
|
456
|
+
const turn2Logs = getRequestLogsByMessageId("a2-t");
|
|
457
|
+
expect(turn2Logs).toHaveLength(1);
|
|
458
|
+
expect(turn2Logs[0]?.id).toBe("log-turn2-unlinked");
|
|
459
|
+
|
|
460
|
+
// Query turn 1 → should only find turn 1's unlinked log
|
|
461
|
+
const turn1Logs = getRequestLogsByMessageId("a1-t");
|
|
462
|
+
expect(turn1Logs).toHaveLength(1);
|
|
463
|
+
expect(turn1Logs[0]?.id).toBe("log-turn1-unlinked");
|
|
464
|
+
});
|
|
465
|
+
|
|
304
466
|
test("relinkLlmRequestLogs moves logs from deleted messages to consolidated message", async () => {
|
|
305
467
|
const conv = createConversation("relink-test");
|
|
306
468
|
|