@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,387 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mocks — must come before any imports that depend on them
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const testDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
10
|
+
const workspaceDir = testDir;
|
|
11
|
+
const conversationsDir = join(workspaceDir, "conversations");
|
|
12
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
mock.module("../util/logger.js", () => ({
|
|
15
|
+
getLogger: () =>
|
|
16
|
+
new Proxy({} as Record<string, unknown>, {
|
|
17
|
+
get: () => () => {},
|
|
18
|
+
}),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
mock.module("../config/loader.js", () => ({
|
|
22
|
+
getConfig: () => ({
|
|
23
|
+
ui: {},
|
|
24
|
+
model: "test",
|
|
25
|
+
provider: "test",
|
|
26
|
+
memory: { enabled: false },
|
|
27
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
28
|
+
}),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Imports — after mocks
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
import { getDb, initializeDb } from "../memory/db.js";
|
|
36
|
+
import { conversations, messages } from "../memory/schema.js";
|
|
37
|
+
import { recoverConversationsFromDiskViewMigration } from "../workspace/migrations/028-recover-conversations-from-disk-view.js";
|
|
38
|
+
|
|
39
|
+
initializeDb();
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
function resetTables() {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
db.run("DELETE FROM messages");
|
|
48
|
+
db.run("DELETE FROM conversations");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resetConversationsDir() {
|
|
52
|
+
rmSync(conversationsDir, { recursive: true, force: true });
|
|
53
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createDiskViewDir(
|
|
57
|
+
id: string,
|
|
58
|
+
meta: Record<string, unknown>,
|
|
59
|
+
messagesJsonl?: string,
|
|
60
|
+
): string {
|
|
61
|
+
const createdAt =
|
|
62
|
+
typeof meta.createdAt === "string" ? meta.createdAt : new Date().toISOString();
|
|
63
|
+
const timestamp = createdAt.replace(/:/g, "-");
|
|
64
|
+
const dirName = `${timestamp}_${id}`;
|
|
65
|
+
const dirPath = join(conversationsDir, dirName);
|
|
66
|
+
mkdirSync(dirPath, { recursive: true });
|
|
67
|
+
writeFileSync(join(dirPath, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
68
|
+
if (messagesJsonl !== undefined) {
|
|
69
|
+
writeFileSync(join(dirPath, "messages.jsonl"), messagesJsonl);
|
|
70
|
+
}
|
|
71
|
+
return dirPath;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Tests
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
describe("028-recover-conversations-from-disk-view migration", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
resetTables();
|
|
81
|
+
resetConversationsDir();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("recovers conversation with messages", () => {
|
|
85
|
+
const id = "conv-028-basic";
|
|
86
|
+
const createdAt = "2026-03-18T14:23:00.000Z";
|
|
87
|
+
const updatedAt = "2026-03-18T14:25:00.000Z";
|
|
88
|
+
|
|
89
|
+
const userLine = JSON.stringify({
|
|
90
|
+
role: "user",
|
|
91
|
+
ts: "2026-03-18T14:23:30.000Z",
|
|
92
|
+
content: "Hello, world",
|
|
93
|
+
});
|
|
94
|
+
const assistantLine = JSON.stringify({
|
|
95
|
+
role: "assistant",
|
|
96
|
+
ts: "2026-03-18T14:24:00.000Z",
|
|
97
|
+
content: "Hi there!",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
createDiskViewDir(
|
|
101
|
+
id,
|
|
102
|
+
{ id, title: "Basic Recovery", type: "standard", channel: "desktop", createdAt, updatedAt },
|
|
103
|
+
userLine + "\n" + assistantLine + "\n",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
107
|
+
|
|
108
|
+
const db = getDb();
|
|
109
|
+
const convRows = db.select().from(conversations).all();
|
|
110
|
+
expect(convRows).toHaveLength(1);
|
|
111
|
+
expect(convRows[0].id).toBe(id);
|
|
112
|
+
expect(convRows[0].title).toBe("Basic Recovery");
|
|
113
|
+
expect(convRows[0].conversationType).toBe("standard");
|
|
114
|
+
expect(convRows[0].createdAt).toBe(Date.parse(createdAt));
|
|
115
|
+
expect(convRows[0].updatedAt).toBe(Date.parse(updatedAt));
|
|
116
|
+
|
|
117
|
+
const msgRows = db.select().from(messages).all();
|
|
118
|
+
expect(msgRows).toHaveLength(2);
|
|
119
|
+
|
|
120
|
+
const userMsg = msgRows.find((m) => m.role === "user")!;
|
|
121
|
+
expect(userMsg).toBeDefined();
|
|
122
|
+
const userContent = JSON.parse(userMsg.content);
|
|
123
|
+
expect(userContent).toEqual([{ type: "text", text: "Hello, world" }]);
|
|
124
|
+
expect(userMsg.createdAt).toBe(Date.parse("2026-03-18T14:23:30.000Z"));
|
|
125
|
+
|
|
126
|
+
const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
|
|
127
|
+
expect(assistantMsg).toBeDefined();
|
|
128
|
+
const assistantContent = JSON.parse(assistantMsg.content);
|
|
129
|
+
expect(assistantContent).toEqual([{ type: "text", text: "Hi there!" }]);
|
|
130
|
+
expect(assistantMsg.createdAt).toBe(Date.parse("2026-03-18T14:24:00.000Z"));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("handles toolCalls and toolResults", () => {
|
|
134
|
+
const id = "conv-028-tools";
|
|
135
|
+
const createdAt = "2026-03-18T15:00:00.000Z";
|
|
136
|
+
|
|
137
|
+
const toolCallLine = JSON.stringify({
|
|
138
|
+
role: "assistant",
|
|
139
|
+
ts: "2026-03-18T15:00:10.000Z",
|
|
140
|
+
toolCalls: [{ name: "bash", input: { command: "ls" } }],
|
|
141
|
+
});
|
|
142
|
+
const toolResultLine = JSON.stringify({
|
|
143
|
+
role: "user",
|
|
144
|
+
ts: "2026-03-18T15:00:20.000Z",
|
|
145
|
+
toolResults: [{ content: "file.txt" }],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
createDiskViewDir(
|
|
149
|
+
id,
|
|
150
|
+
{ id, title: "Tool Test", type: "standard", createdAt, updatedAt: createdAt },
|
|
151
|
+
toolCallLine + "\n" + toolResultLine + "\n",
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
155
|
+
|
|
156
|
+
const db = getDb();
|
|
157
|
+
const msgRows = db.select().from(messages).all();
|
|
158
|
+
expect(msgRows).toHaveLength(2);
|
|
159
|
+
|
|
160
|
+
const assistantMsg = msgRows.find((m) => m.role === "assistant")!;
|
|
161
|
+
const assistantContent = JSON.parse(assistantMsg.content);
|
|
162
|
+
expect(assistantContent).toHaveLength(1);
|
|
163
|
+
expect(assistantContent[0].type).toBe("tool_use");
|
|
164
|
+
expect(assistantContent[0].name).toBe("bash");
|
|
165
|
+
expect(assistantContent[0].input).toEqual({ command: "ls" });
|
|
166
|
+
// tool_use blocks get a random UUID id — just check it's a string
|
|
167
|
+
expect(typeof assistantContent[0].id).toBe("string");
|
|
168
|
+
|
|
169
|
+
const userMsg = msgRows.find((m) => m.role === "user")!;
|
|
170
|
+
const userContent = JSON.parse(userMsg.content);
|
|
171
|
+
expect(userContent).toHaveLength(1);
|
|
172
|
+
expect(userContent[0].type).toBe("tool_result");
|
|
173
|
+
expect(userContent[0].content).toBe("file.txt");
|
|
174
|
+
expect(userContent[0].tool_use_id).toBe("");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("handles mixed content + toolCalls on the same message", () => {
|
|
178
|
+
const id = "conv-028-mixed";
|
|
179
|
+
const createdAt = "2026-03-18T15:30:00.000Z";
|
|
180
|
+
|
|
181
|
+
const mixedLine = JSON.stringify({
|
|
182
|
+
role: "assistant",
|
|
183
|
+
ts: "2026-03-18T15:30:10.000Z",
|
|
184
|
+
content: "Let me check that",
|
|
185
|
+
toolCalls: [{ name: "bash", input: { command: "ls" } }],
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
createDiskViewDir(
|
|
189
|
+
id,
|
|
190
|
+
{ id, title: "Mixed Test", type: "standard", createdAt, updatedAt: createdAt },
|
|
191
|
+
mixedLine + "\n",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
195
|
+
|
|
196
|
+
const db = getDb();
|
|
197
|
+
const msgRows = db.select().from(messages).all();
|
|
198
|
+
expect(msgRows).toHaveLength(1);
|
|
199
|
+
|
|
200
|
+
const assistantMsg = msgRows[0];
|
|
201
|
+
expect(assistantMsg.role).toBe("assistant");
|
|
202
|
+
|
|
203
|
+
const contentBlocks = JSON.parse(assistantMsg.content);
|
|
204
|
+
expect(contentBlocks).toHaveLength(2);
|
|
205
|
+
|
|
206
|
+
expect(contentBlocks[0].type).toBe("text");
|
|
207
|
+
expect(contentBlocks[0].text).toBe("Let me check that");
|
|
208
|
+
|
|
209
|
+
expect(contentBlocks[1].type).toBe("tool_use");
|
|
210
|
+
expect(contentBlocks[1].name).toBe("bash");
|
|
211
|
+
expect(contentBlocks[1].input).toEqual({ command: "ls" });
|
|
212
|
+
expect(typeof contentBlocks[1].id).toBe("string");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("skips existing conversations", () => {
|
|
216
|
+
const id = "conv-028-existing";
|
|
217
|
+
const createdAt = "2026-03-18T16:00:00.000Z";
|
|
218
|
+
const createdAtMs = Date.parse(createdAt);
|
|
219
|
+
|
|
220
|
+
// Pre-insert the conversation in the DB
|
|
221
|
+
const db = getDb();
|
|
222
|
+
db.insert(conversations)
|
|
223
|
+
.values({
|
|
224
|
+
id,
|
|
225
|
+
title: "Already Here",
|
|
226
|
+
createdAt: createdAtMs,
|
|
227
|
+
updatedAt: createdAtMs,
|
|
228
|
+
conversationType: "standard",
|
|
229
|
+
source: "user",
|
|
230
|
+
memoryScopeId: "default",
|
|
231
|
+
})
|
|
232
|
+
.run();
|
|
233
|
+
|
|
234
|
+
// Create matching disk-view dir with a message
|
|
235
|
+
createDiskViewDir(
|
|
236
|
+
id,
|
|
237
|
+
{ id, title: "Already Here", type: "standard", createdAt, updatedAt: createdAt },
|
|
238
|
+
JSON.stringify({ role: "user", ts: createdAt, content: "Should not be imported" }) + "\n",
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
242
|
+
|
|
243
|
+
// Verify no duplication: still 1 conversation, 0 messages (the disk-view message was not imported)
|
|
244
|
+
const convRows = db.select().from(conversations).all();
|
|
245
|
+
expect(convRows).toHaveLength(1);
|
|
246
|
+
expect(convRows[0].title).toBe("Already Here");
|
|
247
|
+
|
|
248
|
+
const msgRows = db.select().from(messages).all();
|
|
249
|
+
expect(msgRows).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("idempotent — running twice produces same result", () => {
|
|
253
|
+
const id = "conv-028-idem";
|
|
254
|
+
const createdAt = "2026-03-18T17:00:00.000Z";
|
|
255
|
+
|
|
256
|
+
createDiskViewDir(
|
|
257
|
+
id,
|
|
258
|
+
{ id, title: "Idempotency Test", type: "standard", createdAt, updatedAt: createdAt },
|
|
259
|
+
JSON.stringify({ role: "user", ts: createdAt, content: "First message" }) + "\n" +
|
|
260
|
+
JSON.stringify({ role: "assistant", ts: "2026-03-18T17:01:00.000Z", content: "Reply" }) + "\n",
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
264
|
+
|
|
265
|
+
const db = getDb();
|
|
266
|
+
const convCountAfterFirst = db.select().from(conversations).all().length;
|
|
267
|
+
const msgCountAfterFirst = db.select().from(messages).all().length;
|
|
268
|
+
expect(convCountAfterFirst).toBe(1);
|
|
269
|
+
expect(msgCountAfterFirst).toBe(2);
|
|
270
|
+
|
|
271
|
+
// Run again
|
|
272
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
273
|
+
|
|
274
|
+
const convCountAfterSecond = db.select().from(conversations).all().length;
|
|
275
|
+
const msgCountAfterSecond = db.select().from(messages).all().length;
|
|
276
|
+
expect(convCountAfterSecond).toBe(convCountAfterFirst);
|
|
277
|
+
expect(msgCountAfterSecond).toBe(msgCountAfterFirst);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("handles missing messages.jsonl", () => {
|
|
281
|
+
const id = "conv-028-no-messages";
|
|
282
|
+
const createdAt = "2026-03-18T18:00:00.000Z";
|
|
283
|
+
|
|
284
|
+
// Create dir with only meta.json — no messages.jsonl
|
|
285
|
+
createDiskViewDir(
|
|
286
|
+
id,
|
|
287
|
+
{ id, title: "No Messages", type: "standard", createdAt, updatedAt: createdAt },
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
291
|
+
|
|
292
|
+
const db = getDb();
|
|
293
|
+
const convRows = db.select().from(conversations).all();
|
|
294
|
+
expect(convRows).toHaveLength(1);
|
|
295
|
+
expect(convRows[0].id).toBe(id);
|
|
296
|
+
expect(convRows[0].title).toBe("No Messages");
|
|
297
|
+
|
|
298
|
+
const msgRows = db.select().from(messages).all();
|
|
299
|
+
expect(msgRows).toHaveLength(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("handles malformed JSONL lines", () => {
|
|
303
|
+
const id = "conv-028-malformed-jsonl";
|
|
304
|
+
const createdAt = "2026-03-18T19:00:00.000Z";
|
|
305
|
+
|
|
306
|
+
const validLine = JSON.stringify({ role: "user", ts: createdAt, content: "Valid" });
|
|
307
|
+
const invalidLine = "{ this is not valid json }}}";
|
|
308
|
+
|
|
309
|
+
createDiskViewDir(
|
|
310
|
+
id,
|
|
311
|
+
{ id, title: "Malformed JSONL", type: "standard", createdAt, updatedAt: createdAt },
|
|
312
|
+
validLine + "\n" + invalidLine + "\n",
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
316
|
+
|
|
317
|
+
const db = getDb();
|
|
318
|
+
const convRows = db.select().from(conversations).all();
|
|
319
|
+
expect(convRows).toHaveLength(1);
|
|
320
|
+
|
|
321
|
+
// Only the valid line should produce a message row
|
|
322
|
+
const msgRows = db.select().from(messages).all();
|
|
323
|
+
expect(msgRows).toHaveLength(1);
|
|
324
|
+
expect(msgRows[0].role).toBe("user");
|
|
325
|
+
const content = JSON.parse(msgRows[0].content);
|
|
326
|
+
expect(content).toEqual([{ type: "text", text: "Valid" }]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("handles malformed meta.json", () => {
|
|
330
|
+
const id = "conv-028-malformed-meta";
|
|
331
|
+
const createdAt = "2026-03-18T20:00:00.000Z";
|
|
332
|
+
const timestamp = createdAt.replace(/:/g, "-");
|
|
333
|
+
const dirName = `${timestamp}_${id}`;
|
|
334
|
+
const dirPath = join(conversationsDir, dirName);
|
|
335
|
+
mkdirSync(dirPath, { recursive: true });
|
|
336
|
+
|
|
337
|
+
// Write broken JSON directly
|
|
338
|
+
writeFileSync(join(dirPath, "meta.json"), "{ broken json");
|
|
339
|
+
|
|
340
|
+
// Migration should complete without error
|
|
341
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
342
|
+
|
|
343
|
+
const db = getDb();
|
|
344
|
+
const convRows = db.select().from(conversations).all();
|
|
345
|
+
expect(convRows).toHaveLength(0);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("no-op when conversations dir missing", () => {
|
|
349
|
+
// Remove the conversations dir entirely
|
|
350
|
+
rmSync(conversationsDir, { recursive: true, force: true });
|
|
351
|
+
expect(existsSync(conversationsDir)).toBe(false);
|
|
352
|
+
|
|
353
|
+
// Migration should complete without error
|
|
354
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
355
|
+
|
|
356
|
+
// No conversations should exist since we can't access the DB rows through a missing dir
|
|
357
|
+
const db = getDb();
|
|
358
|
+
const convRows = db.select().from(conversations).all();
|
|
359
|
+
expect(convRows).toHaveLength(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("processes multiple directories", () => {
|
|
363
|
+
const ids = ["conv-028-multi-a", "conv-028-multi-b", "conv-028-multi-c"];
|
|
364
|
+
const baseTime = Date.parse("2026-03-18T21:00:00.000Z");
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < ids.length; i++) {
|
|
367
|
+
const ts = new Date(baseTime + i * 60_000).toISOString();
|
|
368
|
+
createDiskViewDir(
|
|
369
|
+
ids[i],
|
|
370
|
+
{ id: ids[i], title: `Multi ${i + 1}`, type: "standard", createdAt: ts, updatedAt: ts },
|
|
371
|
+
JSON.stringify({ role: "user", ts, content: `Message ${i + 1}` }) + "\n",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
recoverConversationsFromDiskViewMigration.run(workspaceDir);
|
|
376
|
+
|
|
377
|
+
const db = getDb();
|
|
378
|
+
const convRows = db.select().from(conversations).all();
|
|
379
|
+
expect(convRows).toHaveLength(3);
|
|
380
|
+
|
|
381
|
+
const recoveredIds = convRows.map((c) => c.id).sort();
|
|
382
|
+
expect(recoveredIds).toEqual([...ids].sort());
|
|
383
|
+
|
|
384
|
+
const msgRows = db.select().from(messages).all();
|
|
385
|
+
expect(msgRows).toHaveLength(3);
|
|
386
|
+
});
|
|
387
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface AgentLoopConfig {
|
|
|
31
31
|
| { type: "tool"; name: string };
|
|
32
32
|
/** Minimum interval (ms) between consecutive LLM calls to prevent spin when tools return instantly */
|
|
33
33
|
minTurnIntervalMs?: number;
|
|
34
|
+
/** Override the default prompt cache TTL sent to the provider (e.g. "5m" for short-lived subagents). */
|
|
35
|
+
cacheTtl?: "5m" | "1h";
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface CheckpointInfo {
|
|
@@ -252,6 +254,10 @@ export class AgentLoop {
|
|
|
252
254
|
providerConfig.tool_choice = this.config.toolChoice;
|
|
253
255
|
}
|
|
254
256
|
|
|
257
|
+
if (this.config.cacheTtl) {
|
|
258
|
+
providerConfig.cacheTtl = this.config.cacheTtl;
|
|
259
|
+
}
|
|
260
|
+
|
|
255
261
|
const preLlmResult = await getHookManager().trigger("pre-llm-call", {
|
|
256
262
|
systemPrompt: turnSystemPrompt,
|
|
257
263
|
messages: history,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { answerCall } from "../calls/call-domain.js";
|
|
15
15
|
import { getGatewayInternalBaseUrl } from "../config/env.js";
|
|
16
|
+
import { findContactChannel } from "../contacts/contact-store.js";
|
|
16
17
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
17
18
|
import {
|
|
18
19
|
type CanonicalGuardianRequest,
|
|
@@ -396,6 +397,25 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
396
397
|
const desktopDeliverUrl = resolveDeliverCallbackUrlForChannel(channel);
|
|
397
398
|
const desktopBearerToken = mintDaemonDeliveryToken();
|
|
398
399
|
|
|
400
|
+
// Resolve display names from the contacts database for enriched payloads
|
|
401
|
+
const requesterContactResult = requesterExternalUserId
|
|
402
|
+
? findContactChannel({
|
|
403
|
+
channelType: channel,
|
|
404
|
+
externalUserId: requesterExternalUserId,
|
|
405
|
+
})
|
|
406
|
+
: null;
|
|
407
|
+
const requesterDisplayName =
|
|
408
|
+
requesterContactResult?.contact.displayName ?? null;
|
|
409
|
+
|
|
410
|
+
const decidedByContactResult = decidedByExternalUserId
|
|
411
|
+
? findContactChannel({
|
|
412
|
+
channelType: channel,
|
|
413
|
+
externalUserId: decidedByExternalUserId,
|
|
414
|
+
})
|
|
415
|
+
: null;
|
|
416
|
+
const decidedByDisplayName =
|
|
417
|
+
decidedByContactResult?.contact.displayName ?? null;
|
|
418
|
+
|
|
399
419
|
if (decision.action === "reject") {
|
|
400
420
|
log.info(
|
|
401
421
|
{ event: "resolver_access_request_denied", requestId: request.id },
|
|
@@ -435,6 +455,8 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
435
455
|
requesterExternalUserId,
|
|
436
456
|
requesterChatId,
|
|
437
457
|
decidedByExternalUserId,
|
|
458
|
+
requesterDisplayName,
|
|
459
|
+
decidedByDisplayName,
|
|
438
460
|
decision: "denied" as const,
|
|
439
461
|
};
|
|
440
462
|
|
|
@@ -726,6 +748,8 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
726
748
|
sourceChannel: channel,
|
|
727
749
|
requesterExternalUserId,
|
|
728
750
|
requesterChatId,
|
|
751
|
+
requesterDisplayName,
|
|
752
|
+
decidedByDisplayName,
|
|
729
753
|
verificationSessionId: session.sessionId,
|
|
730
754
|
},
|
|
731
755
|
dedupeKey: `trusted-contact:verification-sent:${session.sessionId}`,
|
|
@@ -3,7 +3,7 @@ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { getLogger } from "../util/logger.js";
|
|
6
|
-
import {
|
|
6
|
+
import { AVATAR_IMAGE_FILENAME, getAvatarDir } from "../util/platform.js";
|
|
7
7
|
import { renderCharacterAscii } from "./ascii-renderer.js";
|
|
8
8
|
import { getCharacterComponents } from "./character-components.js";
|
|
9
9
|
import { renderCharacterPng } from "./png-renderer.js";
|
|
@@ -64,7 +64,7 @@ function writeAvatarFiles(
|
|
|
64
64
|
pngBuffer: Buffer,
|
|
65
65
|
asciiArt: string | null,
|
|
66
66
|
): boolean {
|
|
67
|
-
const pngPath = join(avatarDir,
|
|
67
|
+
const pngPath = join(avatarDir, AVATAR_IMAGE_FILENAME);
|
|
68
68
|
const pngTmp = `${pngPath}.${randomUUID()}.tmp`;
|
|
69
69
|
writeFileSync(pngTmp, pngBuffer);
|
|
70
70
|
renameSync(pngTmp, pngPath);
|
|
@@ -144,7 +144,7 @@ export function writeTraitsAndRenderAvatar(
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
const avatarDir =
|
|
147
|
+
const avatarDir = getAvatarDir();
|
|
148
148
|
const traitsPath = join(avatarDir, "character-traits.json");
|
|
149
149
|
|
|
150
150
|
try {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI test utility — run an assistant CLI command via the real program,
|
|
3
|
+
* capturing stdout.
|
|
4
|
+
*/
|
|
5
|
+
export async function runAssistantCommand(...args: string[]): Promise<string> {
|
|
6
|
+
const { buildCliProgram } = await import("../program.js");
|
|
7
|
+
const program = buildCliProgram();
|
|
8
|
+
program.exitOverride();
|
|
9
|
+
program.configureOutput({ writeErr: () => {}, writeOut: () => {} });
|
|
10
|
+
|
|
11
|
+
const chunks: string[] = [];
|
|
12
|
+
const originalWrite = process.stdout.write;
|
|
13
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
14
|
+
chunks.push(
|
|
15
|
+
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk),
|
|
16
|
+
);
|
|
17
|
+
return true;
|
|
18
|
+
}) as typeof process.stdout.write;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await program.parseAsync(["node", "assistant", ...args]);
|
|
22
|
+
} catch {
|
|
23
|
+
/* commander exit override throws */
|
|
24
|
+
} finally {
|
|
25
|
+
process.stdout.write = originalWrite;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return chunks.join("");
|
|
29
|
+
}
|