@vellumai/assistant 0.6.0 → 0.6.2
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 +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- 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__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- 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-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- 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__/credential-execution-approval-bridge.test.ts +0 -2
- 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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- 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 +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- 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__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -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__/pkb-autoinject.test.ts +96 -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__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- 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-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +4 -27
- 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__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- 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/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -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/default-action.ts +68 -1
- 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/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- 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.json +1 -1
- 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/settings/tools/navigate-settings-tab.ts +8 -3
- 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/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- 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/index.ts +1 -1
- 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 +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- 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/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- 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/group-crud.ts +25 -9
- 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/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -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 +185 -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 +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- 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/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +41 -278
- 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/inline-command-runner.ts +12 -14
- 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 -18
- 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/secret-detection-handler.ts +0 -1
- 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/skills/sandbox-runner.ts +3 -6
- 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/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- 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 +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -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
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { seedPkbAutoinjectMigration } from "../workspace/migrations/030-seed-pkb-autoinject.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
let workspaceDir: string;
|
|
19
|
+
let pkbDir: string;
|
|
20
|
+
|
|
21
|
+
function freshWorkspace(): void {
|
|
22
|
+
workspaceDir = join(
|
|
23
|
+
tmpdir(),
|
|
24
|
+
`vellum-migration-030-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
25
|
+
);
|
|
26
|
+
pkbDir = join(workspaceDir, "pkb");
|
|
27
|
+
mkdirSync(pkbDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Setup / Teardown
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const dirs: string[] = [];
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
freshWorkspace();
|
|
38
|
+
dirs.push(workspaceDir);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
for (const dir of dirs.splice(0)) {
|
|
43
|
+
rmSync(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Tests
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
describe("030-seed-pkb-autoinject migration", () => {
|
|
52
|
+
test("has correct migration id", () => {
|
|
53
|
+
expect(seedPkbAutoinjectMigration.id).toBe("030-seed-pkb-autoinject");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ─── run() ──────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
test("creates _autoinject.md with default content", () => {
|
|
59
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
60
|
+
|
|
61
|
+
const filePath = join(pkbDir, "_autoinject.md");
|
|
62
|
+
expect(existsSync(filePath)).toBe(true);
|
|
63
|
+
|
|
64
|
+
const content = readFileSync(filePath, "utf-8");
|
|
65
|
+
expect(content).toContain("INDEX.md");
|
|
66
|
+
expect(content).toContain("essentials.md");
|
|
67
|
+
expect(content).toContain("threads.md");
|
|
68
|
+
expect(content).toContain("buffer.md");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("no-op when pkb/ does not exist", () => {
|
|
72
|
+
rmSync(pkbDir, { recursive: true, force: true });
|
|
73
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
74
|
+
expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("idempotent — does not overwrite existing _autoinject.md", () => {
|
|
78
|
+
const customContent = "INDEX.md\ncustom-topic.md\n";
|
|
79
|
+
writeFileSync(join(pkbDir, "_autoinject.md"), customContent, "utf-8");
|
|
80
|
+
|
|
81
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
82
|
+
|
|
83
|
+
const content = readFileSync(join(pkbDir, "_autoinject.md"), "utf-8");
|
|
84
|
+
expect(content).toBe(customContent);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("appends _autoinject.md entry to INDEX.md", () => {
|
|
88
|
+
const indexContent =
|
|
89
|
+
"# Knowledge Base\n\n## Always Loaded\n" +
|
|
90
|
+
"- essentials.md — Core facts\n" +
|
|
91
|
+
"- threads.md — Active threads\n" +
|
|
92
|
+
"- buffer.md — Inbox\n\n" +
|
|
93
|
+
"## Topics\n";
|
|
94
|
+
writeFileSync(join(pkbDir, "INDEX.md"), indexContent, "utf-8");
|
|
95
|
+
|
|
96
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
97
|
+
|
|
98
|
+
const updated = readFileSync(join(pkbDir, "INDEX.md"), "utf-8");
|
|
99
|
+
expect(updated).toContain("_autoinject.md");
|
|
100
|
+
// Should appear after buffer.md
|
|
101
|
+
const bufferIdx = updated.indexOf("buffer.md");
|
|
102
|
+
const autoinjectIdx = updated.indexOf("_autoinject.md");
|
|
103
|
+
expect(autoinjectIdx).toBeGreaterThan(bufferIdx);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("does not duplicate _autoinject.md entry in INDEX.md", () => {
|
|
107
|
+
const indexContent =
|
|
108
|
+
"# Knowledge Base\n\n## Always Loaded\n" +
|
|
109
|
+
"- buffer.md — Inbox\n" +
|
|
110
|
+
"- _autoinject.md — Controls autoinjection\n\n" +
|
|
111
|
+
"## Topics\n";
|
|
112
|
+
writeFileSync(join(pkbDir, "INDEX.md"), indexContent, "utf-8");
|
|
113
|
+
|
|
114
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
115
|
+
|
|
116
|
+
const updated = readFileSync(join(pkbDir, "INDEX.md"), "utf-8");
|
|
117
|
+
const matches = updated.match(/_autoinject\.md/g);
|
|
118
|
+
expect(matches?.length).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("handles missing INDEX.md gracefully", () => {
|
|
122
|
+
// No INDEX.md — should still create _autoinject.md without error
|
|
123
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
124
|
+
expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ─── down() ─────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe("down()", () => {
|
|
130
|
+
test("removes _autoinject.md when content matches template", () => {
|
|
131
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
132
|
+
expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
|
|
133
|
+
|
|
134
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
135
|
+
expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("preserves _autoinject.md when user has customized it", () => {
|
|
139
|
+
const customContent = "INDEX.md\nmy-custom-file.md\n";
|
|
140
|
+
writeFileSync(join(pkbDir, "_autoinject.md"), customContent, "utf-8");
|
|
141
|
+
|
|
142
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
143
|
+
|
|
144
|
+
expect(existsSync(join(pkbDir, "_autoinject.md"))).toBe(true);
|
|
145
|
+
expect(readFileSync(join(pkbDir, "_autoinject.md"), "utf-8")).toBe(
|
|
146
|
+
customContent,
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("no-op when _autoinject.md does not exist", () => {
|
|
151
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
152
|
+
// Should not throw
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("no-op when pkb/ does not exist", () => {
|
|
156
|
+
rmSync(pkbDir, { recursive: true, force: true });
|
|
157
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
158
|
+
// Should not throw
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("idempotent — calling down() twice is safe", () => {
|
|
162
|
+
seedPkbAutoinjectMigration.run(workspaceDir);
|
|
163
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
164
|
+
seedPkbAutoinjectMigration.down(workspaceDir);
|
|
165
|
+
// Should not throw
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -198,7 +198,7 @@ describe("isWorkspaceScopedInvocation", () => {
|
|
|
198
198
|
// ── Bash ───────────────────────────────────────────────────────────
|
|
199
199
|
|
|
200
200
|
describe("bash", () => {
|
|
201
|
-
test("returns true (
|
|
201
|
+
test("returns true (container handles isolation)", () => {
|
|
202
202
|
expect(
|
|
203
203
|
isWorkspaceScopedInvocation(
|
|
204
204
|
"bash",
|
|
@@ -255,12 +255,7 @@ describe("isWorkspaceScopedInvocation", () => {
|
|
|
255
255
|
// ── Always-scoped safe tools ───────────────────────────────────────
|
|
256
256
|
|
|
257
257
|
describe("always-scoped tools", () => {
|
|
258
|
-
const safeTools = [
|
|
259
|
-
"skill_load",
|
|
260
|
-
"recall",
|
|
261
|
-
"ui_update",
|
|
262
|
-
"ui_dismiss",
|
|
263
|
-
];
|
|
258
|
+
const safeTools = ["skill_load", "recall", "ui_update", "ui_dismiss"];
|
|
264
259
|
|
|
265
260
|
for (const tool of safeTools) {
|
|
266
261
|
test(`${tool} is workspace-scoped`, () => {
|
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 {
|
|
@@ -199,7 +201,6 @@ export class AgentLoop {
|
|
|
199
201
|
): Promise<Message[]> {
|
|
200
202
|
const history = [...messages];
|
|
201
203
|
let toolUseTurns = 0;
|
|
202
|
-
let nudgedForEmptyResponse = false;
|
|
203
204
|
let consecutiveErrorTurns = 0;
|
|
204
205
|
let lastLlmCallTime = 0;
|
|
205
206
|
const rlog = requestId ? log.child({ requestId }) : log;
|
|
@@ -252,6 +253,10 @@ export class AgentLoop {
|
|
|
252
253
|
providerConfig.tool_choice = this.config.toolChoice;
|
|
253
254
|
}
|
|
254
255
|
|
|
256
|
+
if (this.config.cacheTtl) {
|
|
257
|
+
providerConfig.cacheTtl = this.config.cacheTtl;
|
|
258
|
+
}
|
|
259
|
+
|
|
255
260
|
const preLlmResult = await getHookManager().trigger("pre-llm-call", {
|
|
256
261
|
systemPrompt: turnSystemPrompt,
|
|
257
262
|
messages: history,
|
|
@@ -397,35 +402,7 @@ export class AgentLoop {
|
|
|
397
402
|
block.type === "tool_use",
|
|
398
403
|
);
|
|
399
404
|
|
|
400
|
-
// Check if the assistant turn contained any visible text (used for
|
|
401
|
-
// the empty-response nudge).
|
|
402
|
-
const hasTextBlock = response.content.some(
|
|
403
|
-
(block) => block.type === "text" && block.text.trim().length > 0,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
405
|
if (toolUseBlocks.length === 0 || !this.toolExecutor) {
|
|
407
|
-
// Check if the LLM returned no text after tool results — nudge it to respond
|
|
408
|
-
const lastUserMsg =
|
|
409
|
-
history.length >= 2 ? history[history.length - 2] : undefined;
|
|
410
|
-
const lastWasToolResult =
|
|
411
|
-
lastUserMsg?.role === "user" &&
|
|
412
|
-
lastUserMsg.content.some((block) => block.type === "tool_result");
|
|
413
|
-
|
|
414
|
-
if (!hasTextBlock && lastWasToolResult && !nudgedForEmptyResponse) {
|
|
415
|
-
nudgedForEmptyResponse = true;
|
|
416
|
-
history.push({
|
|
417
|
-
role: "user",
|
|
418
|
-
content: [
|
|
419
|
-
{
|
|
420
|
-
type: "text",
|
|
421
|
-
text: "<system_notice>You executed tools but didn't tell the user what happened. Provide a brief, conversational summary of the results.</system_notice>",
|
|
422
|
-
},
|
|
423
|
-
],
|
|
424
|
-
});
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// No tool calls or no executor — done
|
|
429
406
|
break;
|
|
430
407
|
}
|
|
431
408
|
|
|
@@ -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}`,
|