@vellumai/assistant 0.5.1 → 0.5.3
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/ARCHITECTURE.md +163 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/docs/skills.md +100 -0
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
- package/src/__tests__/conversation-agent-loop.test.ts +297 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-wipe.test.ts +226 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
- package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +32 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/inline-command-runner.test.ts +311 -0
- package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
- package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
- package/src/__tests__/list-messages-attachments.test.ts +96 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
- package/src/__tests__/memory-brief-time.test.ts +285 -0
- package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
- package/src/__tests__/memory-chunk-archive.test.ts +400 -0
- package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
- package/src/__tests__/memory-episode-archive.test.ts +370 -0
- package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-observation-archive.test.ts +375 -0
- package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
- package/src/__tests__/memory-recall-quality.test.ts +7 -7
- package/src/__tests__/memory-reducer-store.test.ts +728 -0
- package/src/__tests__/memory-reducer-types.test.ts +699 -0
- package/src/__tests__/memory-reducer.test.ts +698 -0
- package/src/__tests__/memory-regressions.test.ts +6 -4
- package/src/__tests__/memory-simplified-config.test.ts +281 -0
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/parse-identity-fields.test.ts +129 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skill-load-inline-command.test.ts +598 -0
- package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
- package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
- package/src/__tests__/skills-transitive-hash.test.ts +333 -0
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +24 -0
- package/src/config/loader.ts +65 -0
- package/src/config/raw-config-utils.ts +58 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +20 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-simplified.ts +101 -0
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/services.ts +8 -6
- package/src/config/skills.ts +50 -4
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
- package/src/daemon/conversation-agent-loop.ts +127 -20
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +50 -16
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +22 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +170 -24
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +69 -30
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +156 -0
- package/src/daemon/handlers/config-model.ts +62 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/identity.ts +12 -1
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +115 -65
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +18 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/followups/followup-store.ts +47 -1
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/archive-store.ts +400 -0
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/brief-formatting.ts +33 -0
- package/src/memory/brief-open-loops.ts +266 -0
- package/src/memory/brief-time.ts +161 -0
- package/src/memory/brief.ts +75 -0
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +591 -8
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +40 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +114 -21
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +2 -4
- package/src/memory/job-handlers/embedding.ts +83 -0
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +6 -0
- package/src/memory/jobs-worker.ts +12 -0
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/185-memory-brief-state.ts +52 -0
- package/src/memory/migrations/186-memory-archive.ts +109 -0
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/qdrant-client.ts +23 -4
- package/src/memory/reducer-store.ts +271 -0
- package/src/memory/reducer-types.ts +99 -0
- package/src/memory/reducer.ts +453 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/index.ts +2 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/memory-archive.ts +121 -0
- package/src/memory/schema/memory-brief.ts +55 -0
- package/src/memory/schema/oauth.ts +6 -0
- package/src/memory/search/semantic.ts +17 -4
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +99 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +160 -14
- package/src/permissions/defaults.ts +14 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +74 -0
- package/src/runtime/routes/conversation-query-routes.ts +187 -10
- package/src/runtime/routes/conversation-routes.ts +269 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +2 -35
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1212 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +94 -5
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +30 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/surface-action-routes.ts +68 -1
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +30 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/skills/inline-command-expansions.ts +204 -0
- package/src/skills/inline-command-render.ts +127 -0
- package/src/skills/inline-command-runner.ts +242 -0
- package/src/skills/transitive-version-hash.ts +88 -0
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/tasks/task-store.ts +43 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/permission-checker.ts +8 -1
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +2 -7
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/load.ts +140 -6
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +24 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +11 -1
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -15,6 +15,8 @@ mock.module("../util/platform.js", () => ({
|
|
|
15
15
|
getLogPath: () => join(testDir, "test.log"),
|
|
16
16
|
ensureDataDir: () => {},
|
|
17
17
|
getRootDir: () => testDir,
|
|
18
|
+
getWorkspaceDir: () => join(testDir, "workspace"),
|
|
19
|
+
getConversationsDir: () => join(testDir, "workspace", "conversations"),
|
|
18
20
|
}));
|
|
19
21
|
|
|
20
22
|
mock.module("../util/logger.js", () => ({
|
|
@@ -40,8 +42,10 @@ import {
|
|
|
40
42
|
deleteAttachment,
|
|
41
43
|
deleteOrphanAttachments,
|
|
42
44
|
getAttachmentById,
|
|
45
|
+
getAttachmentContent,
|
|
43
46
|
getAttachmentsByIds,
|
|
44
47
|
getAttachmentsForMessage,
|
|
48
|
+
getFilePathForAttachment,
|
|
45
49
|
isValidBase64,
|
|
46
50
|
linkAttachmentToMessage,
|
|
47
51
|
MAX_UPLOAD_BYTES,
|
|
@@ -49,7 +53,8 @@ import {
|
|
|
49
53
|
validateAttachmentUpload,
|
|
50
54
|
} from "../memory/attachments-store.js";
|
|
51
55
|
import { addMessage, createConversation } from "../memory/conversation-crud.js";
|
|
52
|
-
import {
|
|
56
|
+
import { getConversationDirPath } from "../memory/conversation-disk-view.js";
|
|
57
|
+
import { getDb, initializeDb, rawGet, rawRun, resetDb } from "../memory/db.js";
|
|
53
58
|
|
|
54
59
|
initializeDb();
|
|
55
60
|
|
|
@@ -70,8 +75,24 @@ function resetTables() {
|
|
|
70
75
|
db.run("DELETE FROM conversations");
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
function getConversationTimestamp(createdAt: number): string {
|
|
79
|
+
return new Date(createdAt).toISOString().replace(/:/g, "-");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getLegacyConversationDirPath(
|
|
83
|
+
conversationId: string,
|
|
84
|
+
createdAt: number,
|
|
85
|
+
): string {
|
|
86
|
+
return join(
|
|
87
|
+
testDir,
|
|
88
|
+
"workspace",
|
|
89
|
+
"conversations",
|
|
90
|
+
`${conversationId}_${getConversationTimestamp(createdAt)}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
73
94
|
// ---------------------------------------------------------------------------
|
|
74
|
-
// uploadAttachment
|
|
95
|
+
// uploadAttachment — stages until linked
|
|
75
96
|
// ---------------------------------------------------------------------------
|
|
76
97
|
|
|
77
98
|
describe("uploadAttachment", () => {
|
|
@@ -88,6 +109,29 @@ describe("uploadAttachment", () => {
|
|
|
88
109
|
expect(stored.createdAt).toBeGreaterThan(0);
|
|
89
110
|
});
|
|
90
111
|
|
|
112
|
+
test("keeps uploads staged until linked", () => {
|
|
113
|
+
const stored = uploadAttachment("small.txt", "text/plain", "aGVsbG8=");
|
|
114
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
115
|
+
|
|
116
|
+
expect(filePath).toBeNull();
|
|
117
|
+
expect(getAttachmentContent(stored.id)?.toString()).toBe("hello");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("stores base64 in the DB row until linked", () => {
|
|
121
|
+
const stored = uploadAttachment("test.txt", "text/plain", "dGVzdA==");
|
|
122
|
+
|
|
123
|
+
// Staged uploads keep the payload inline until they are attached to a message.
|
|
124
|
+
const rawRow = rawGet<{ data_base64: string }>(
|
|
125
|
+
"SELECT data_base64 FROM attachments WHERE id = ?",
|
|
126
|
+
stored.id,
|
|
127
|
+
);
|
|
128
|
+
expect(rawRow!.data_base64).toBe("dGVzdA==");
|
|
129
|
+
|
|
130
|
+
const row = getAttachmentById(stored.id, { hydrateFileData: true });
|
|
131
|
+
expect(row).not.toBeNull();
|
|
132
|
+
expect(row!.dataBase64).toBe("dGVzdA==");
|
|
133
|
+
});
|
|
134
|
+
|
|
91
135
|
test("classifies image MIME as image kind", () => {
|
|
92
136
|
const stored = uploadAttachment("pic.jpg", "image/jpeg", "AAAA");
|
|
93
137
|
expect(stored.kind).toBe("image");
|
|
@@ -105,12 +149,12 @@ describe("uploadAttachment", () => {
|
|
|
105
149
|
});
|
|
106
150
|
|
|
107
151
|
test("computes sizeBytes from base64 correctly", () => {
|
|
108
|
-
// "hello" = "aGVsbG8=" (8 chars, 1 pad
|
|
152
|
+
// "hello" = "aGVsbG8=" (8 chars, 1 pad -> 5 bytes)
|
|
109
153
|
const stored = uploadAttachment("hello.txt", "text/plain", "aGVsbG8=");
|
|
110
154
|
expect(stored.sizeBytes).toBe(5);
|
|
111
155
|
});
|
|
112
156
|
|
|
113
|
-
test("
|
|
157
|
+
test("does not deduplicate identical uploads before linking", () => {
|
|
114
158
|
const first = uploadAttachment(
|
|
115
159
|
"photo.png",
|
|
116
160
|
"image/png",
|
|
@@ -121,10 +165,10 @@ describe("uploadAttachment", () => {
|
|
|
121
165
|
"image/png",
|
|
122
166
|
"iVBORw0KGgoAAAANSUh",
|
|
123
167
|
);
|
|
124
|
-
expect(second.id).toBe(first.id);
|
|
168
|
+
expect(second.id).not.toBe(first.id);
|
|
125
169
|
});
|
|
126
170
|
|
|
127
|
-
test("
|
|
171
|
+
test("does not deduplicate identical content when filenames differ", () => {
|
|
128
172
|
const first = uploadAttachment(
|
|
129
173
|
"original.png",
|
|
130
174
|
"image/png",
|
|
@@ -135,7 +179,7 @@ describe("uploadAttachment", () => {
|
|
|
135
179
|
"image/png",
|
|
136
180
|
"DUPECONTENT123",
|
|
137
181
|
);
|
|
138
|
-
expect(second.id).toBe(first.id);
|
|
182
|
+
expect(second.id).not.toBe(first.id);
|
|
139
183
|
});
|
|
140
184
|
|
|
141
185
|
test("does not deduplicate different content", () => {
|
|
@@ -146,7 +190,7 @@ describe("uploadAttachment", () => {
|
|
|
146
190
|
|
|
147
191
|
test("rejects payloads exceeding MAX_UPLOAD_BYTES", () => {
|
|
148
192
|
// Build a base64 string that decodes to just over the limit.
|
|
149
|
-
// 4 base64 chars
|
|
193
|
+
// 4 base64 chars -> 3 bytes, so we need ceil((MAX_UPLOAD_BYTES+1)/3)*4 chars.
|
|
150
194
|
const oversizedLength = Math.ceil((MAX_UPLOAD_BYTES + 1) / 3) * 4;
|
|
151
195
|
const oversizedData = "A".repeat(oversizedLength);
|
|
152
196
|
|
|
@@ -162,7 +206,7 @@ describe("uploadAttachment", () => {
|
|
|
162
206
|
});
|
|
163
207
|
|
|
164
208
|
test("accepts base64 with non-standard padding/length", () => {
|
|
165
|
-
// Lenient on length
|
|
209
|
+
// Lenient on length -- only character set is validated
|
|
166
210
|
expect(() => uploadAttachment("ok.txt", "text/plain", "AAA")).not.toThrow();
|
|
167
211
|
});
|
|
168
212
|
|
|
@@ -178,6 +222,41 @@ describe("uploadAttachment", () => {
|
|
|
178
222
|
});
|
|
179
223
|
});
|
|
180
224
|
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// getAttachmentContent — staged inline or materialized on disk
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
describe("getAttachmentContent", () => {
|
|
230
|
+
beforeEach(resetTables);
|
|
231
|
+
|
|
232
|
+
test("returns staged content before the attachment is linked", () => {
|
|
233
|
+
const stored = uploadAttachment("hello.txt", "text/plain", "aGVsbG8=");
|
|
234
|
+
const content = getAttachmentContent(stored.id);
|
|
235
|
+
|
|
236
|
+
expect(content).not.toBeNull();
|
|
237
|
+
expect(content!.toString()).toBe("hello");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("returns null for nonexistent attachment", () => {
|
|
241
|
+
const content = getAttachmentContent("no-such-id");
|
|
242
|
+
expect(content).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("returns null when a materialized on-disk file is missing (ENOENT)", async () => {
|
|
246
|
+
const conv = createConversation();
|
|
247
|
+
const msg = await addMessage(conv.id, "assistant", "File");
|
|
248
|
+
const stored = uploadAttachment("test.txt", "text/plain", "dGVzdA==");
|
|
249
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
250
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
251
|
+
|
|
252
|
+
// Remove the file to simulate ENOENT
|
|
253
|
+
rmSync(filePath!);
|
|
254
|
+
|
|
255
|
+
const content = getAttachmentContent(stored.id);
|
|
256
|
+
expect(content).toBeNull();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
181
260
|
// ---------------------------------------------------------------------------
|
|
182
261
|
// isValidBase64
|
|
183
262
|
// ---------------------------------------------------------------------------
|
|
@@ -219,6 +298,23 @@ describe("deleteAttachment", () => {
|
|
|
219
298
|
expect(fetched).toBeNull();
|
|
220
299
|
});
|
|
221
300
|
|
|
301
|
+
test("cleans up on-disk file when deleting", async () => {
|
|
302
|
+
const conv = createConversation();
|
|
303
|
+
const msg = await addMessage(conv.id, "assistant", "cleanup");
|
|
304
|
+
const stored = uploadAttachment("cleanup.txt", "text/plain", "dGVzdA==");
|
|
305
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
306
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
307
|
+
expect(existsSync(filePath!)).toBe(true);
|
|
308
|
+
|
|
309
|
+
rawRun(
|
|
310
|
+
"DELETE FROM message_attachments WHERE attachment_id = ?",
|
|
311
|
+
stored.id,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
deleteAttachment(stored.id);
|
|
315
|
+
expect(existsSync(filePath!)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
222
318
|
test("returns not_found for nonexistent attachment", () => {
|
|
223
319
|
const result = deleteAttachment("nonexistent-id");
|
|
224
320
|
expect(result).toBe("not_found");
|
|
@@ -229,13 +325,9 @@ describe("deleteAttachment", () => {
|
|
|
229
325
|
const msg1 = await addMessage(conv.id, "user", "First upload");
|
|
230
326
|
const msg2 = await addMessage(conv.id, "user", "Duplicate upload");
|
|
231
327
|
|
|
232
|
-
// Dedup: both uploads return the same attachment row
|
|
233
328
|
const first = uploadAttachment("photo.png", "image/png", "SHAREDCONTENT1");
|
|
234
|
-
const second = uploadAttachment("photo.png", "image/png", "SHAREDCONTENT1");
|
|
235
|
-
expect(second.id).toBe(first.id);
|
|
236
|
-
|
|
237
329
|
linkAttachmentToMessage(msg1.id, first.id, 0);
|
|
238
|
-
linkAttachmentToMessage(msg2.id,
|
|
330
|
+
linkAttachmentToMessage(msg2.id, first.id, 0);
|
|
239
331
|
|
|
240
332
|
// Delete should return still_referenced and NOT remove the attachment row
|
|
241
333
|
const result = deleteAttachment(first.id);
|
|
@@ -254,7 +346,7 @@ describe("deleteAttachment", () => {
|
|
|
254
346
|
|
|
255
347
|
test("deletes attachment when no messages reference it", () => {
|
|
256
348
|
const stored = uploadAttachment("lonely.txt", "text/plain", "UNREFERENCED");
|
|
257
|
-
// No linkAttachmentToMessage call
|
|
349
|
+
// No linkAttachmentToMessage call -- zero references
|
|
258
350
|
const result = deleteAttachment(stored.id);
|
|
259
351
|
expect(result).toBe("deleted");
|
|
260
352
|
|
|
@@ -270,11 +362,13 @@ describe("deleteAttachment", () => {
|
|
|
270
362
|
describe("getAttachmentsByIds", () => {
|
|
271
363
|
beforeEach(resetTables);
|
|
272
364
|
|
|
273
|
-
test("returns matching attachments with
|
|
365
|
+
test("returns matching attachments with hydrated dataBase64", () => {
|
|
274
366
|
const a = uploadAttachment("a.txt", "text/plain", "AAAA");
|
|
275
367
|
const b = uploadAttachment("b.txt", "text/plain", "BBBB");
|
|
276
368
|
|
|
277
|
-
const results = getAttachmentsByIds([a.id, b.id]
|
|
369
|
+
const results = getAttachmentsByIds([a.id, b.id], {
|
|
370
|
+
hydrateFileData: true,
|
|
371
|
+
});
|
|
278
372
|
expect(results).toHaveLength(2);
|
|
279
373
|
expect(results[0].dataBase64).toBe("AAAA");
|
|
280
374
|
expect(results[1].dataBase64).toBe("BBBB");
|
|
@@ -299,9 +393,9 @@ describe("getAttachmentsByIds", () => {
|
|
|
299
393
|
describe("getAttachmentById", () => {
|
|
300
394
|
beforeEach(resetTables);
|
|
301
395
|
|
|
302
|
-
test("returns attachment with
|
|
396
|
+
test("returns attachment with hydrated dataBase64 when found", () => {
|
|
303
397
|
const stored = uploadAttachment("report.pdf", "application/pdf", "JVBER");
|
|
304
|
-
const result = getAttachmentById(stored.id);
|
|
398
|
+
const result = getAttachmentById(stored.id, { hydrateFileData: true });
|
|
305
399
|
|
|
306
400
|
expect(result).not.toBeNull();
|
|
307
401
|
expect(result!.id).toBe(stored.id);
|
|
@@ -334,6 +428,43 @@ describe("linkAttachmentToMessage + getAttachmentsForMessage", () => {
|
|
|
334
428
|
expect(linked[0].id).toBe(stored.id);
|
|
335
429
|
expect(linked[0].originalFilename).toBe("chart.png");
|
|
336
430
|
expect(linked[0].dataBase64).toBe("iVBORw0K");
|
|
431
|
+
expect(getFilePathForAttachment(stored.id)).toContain("/conversations/");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("uses timestamp-first conversation directory and does not recreate a legacy sibling", async () => {
|
|
435
|
+
const conv = createConversation();
|
|
436
|
+
const msg = await addMessage(conv.id, "assistant", "Disk view repaired");
|
|
437
|
+
const canonicalDir = getConversationDirPath(conv.id, conv.createdAt);
|
|
438
|
+
const legacyDir = getLegacyConversationDirPath(conv.id, conv.createdAt);
|
|
439
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
440
|
+
|
|
441
|
+
const stored = uploadAttachment("repaired.png", "image/png", "iVBORw0K");
|
|
442
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
443
|
+
|
|
444
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
445
|
+
expect(filePath).not.toBeNull();
|
|
446
|
+
expect(filePath!).toContain(join(canonicalDir, "attachments"));
|
|
447
|
+
expect(existsSync(filePath!)).toBe(true);
|
|
448
|
+
expect(existsSync(legacyDir)).toBe(false);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("reuses an existing legacy conversation directory when timestamp-first is absent", async () => {
|
|
452
|
+
const conv = createConversation();
|
|
453
|
+
const msg = await addMessage(conv.id, "assistant", "Legacy path");
|
|
454
|
+
const canonicalDir = getConversationDirPath(conv.id, conv.createdAt);
|
|
455
|
+
const legacyDir = getLegacyConversationDirPath(conv.id, conv.createdAt);
|
|
456
|
+
|
|
457
|
+
rmSync(canonicalDir, { recursive: true, force: true });
|
|
458
|
+
mkdirSync(legacyDir, { recursive: true });
|
|
459
|
+
|
|
460
|
+
const stored = uploadAttachment("legacy.png", "image/png", "iVBORw0K");
|
|
461
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
462
|
+
|
|
463
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
464
|
+
expect(filePath).not.toBeNull();
|
|
465
|
+
expect(filePath!).toContain(join(legacyDir, "attachments"));
|
|
466
|
+
expect(existsSync(filePath!)).toBe(true);
|
|
467
|
+
expect(existsSync(canonicalDir)).toBe(false);
|
|
337
468
|
});
|
|
338
469
|
|
|
339
470
|
test("returns attachments in position order", async () => {
|
|
@@ -375,6 +506,23 @@ describe("deleteOrphanAttachments", () => {
|
|
|
375
506
|
expect(removed).toBe(1);
|
|
376
507
|
});
|
|
377
508
|
|
|
509
|
+
test("cleans up on-disk files when removing orphaned materialized attachments", async () => {
|
|
510
|
+
const conv = createConversation();
|
|
511
|
+
const msg = await addMessage(conv.id, "assistant", "Orphan me");
|
|
512
|
+
const stored = uploadAttachment("orphan.txt", "text/plain", "ZGF0YQ==");
|
|
513
|
+
linkAttachmentToMessage(msg.id, stored.id, 0);
|
|
514
|
+
const filePath = getFilePathForAttachment(stored.id);
|
|
515
|
+
expect(existsSync(filePath!)).toBe(true);
|
|
516
|
+
|
|
517
|
+
rawRun(
|
|
518
|
+
"DELETE FROM message_attachments WHERE attachment_id = ?",
|
|
519
|
+
stored.id,
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
deleteOrphanAttachments([stored.id]);
|
|
523
|
+
expect(existsSync(filePath!)).toBe(false);
|
|
524
|
+
});
|
|
525
|
+
|
|
378
526
|
test("preserves attachments that are still linked", async () => {
|
|
379
527
|
const conv = createConversation();
|
|
380
528
|
const msg = await addMessage(conv.id, "assistant", "With attachment");
|
|
@@ -500,7 +648,7 @@ describe("validateAttachmentUpload", () => {
|
|
|
500
648
|
});
|
|
501
649
|
|
|
502
650
|
test("handles filenames without extensions", () => {
|
|
503
|
-
// No extension
|
|
651
|
+
// No extension -- only MIME check applies
|
|
504
652
|
expect(validateAttachmentUpload("Makefile", "text/plain").ok).toBe(true);
|
|
505
653
|
expect(validateAttachmentUpload("Makefile", "application/x-evil").ok).toBe(
|
|
506
654
|
false,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
attachmentsToContentBlocks,
|
|
5
|
+
enrichMessageWithSourcePaths,
|
|
6
|
+
} from "../agent/attachments.js";
|
|
4
7
|
import { createUserMessage } from "../agent/message-types.js";
|
|
5
8
|
|
|
6
9
|
// ---------------------------------------------------------------------------
|
|
@@ -141,3 +144,114 @@ describe("createUserMessage with image attachments", () => {
|
|
|
141
144
|
expect(imageBlock.source.type).toBe("base64");
|
|
142
145
|
});
|
|
143
146
|
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// enrichMessageWithSourcePaths
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe("enrichMessageWithSourcePaths", () => {
|
|
153
|
+
test("appends a source path annotation for images with filePath", () => {
|
|
154
|
+
const attachments = [
|
|
155
|
+
{
|
|
156
|
+
filename: "photo.jpg",
|
|
157
|
+
mimeType: "image/jpeg",
|
|
158
|
+
data: "base64img",
|
|
159
|
+
filePath: "/Users/me/Desktop/photo.jpg",
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
const original = createUserMessage("what is this?", attachments);
|
|
163
|
+
const enriched = enrichMessageWithSourcePaths(original, attachments);
|
|
164
|
+
|
|
165
|
+
expect(enriched).not.toBe(original);
|
|
166
|
+
// Original has text + image = 2 blocks; enriched adds 1 annotation = 3
|
|
167
|
+
expect(enriched.content).toHaveLength(3);
|
|
168
|
+
const annotation = enriched.content[2] as { type: "text"; text: string };
|
|
169
|
+
expect(annotation.type).toBe("text");
|
|
170
|
+
expect(annotation.text).toBe(
|
|
171
|
+
"[Attached image source: /Users/me/Desktop/photo.jpg]",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns the original message (same reference) when no images have filePath", () => {
|
|
176
|
+
const attachments = [
|
|
177
|
+
{
|
|
178
|
+
filename: "photo.jpg",
|
|
179
|
+
mimeType: "image/jpeg",
|
|
180
|
+
data: "base64img",
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
const original = createUserMessage("what is this?", attachments);
|
|
184
|
+
const result = enrichMessageWithSourcePaths(original, attachments);
|
|
185
|
+
|
|
186
|
+
expect(result).toBe(original);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("skips non-image attachments with filePath", () => {
|
|
190
|
+
const attachments = [
|
|
191
|
+
{
|
|
192
|
+
filename: "doc.pdf",
|
|
193
|
+
mimeType: "application/pdf",
|
|
194
|
+
data: "pdfdata",
|
|
195
|
+
filePath: "/Users/me/Documents/doc.pdf",
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
const original = createUserMessage("review this", attachments);
|
|
199
|
+
const result = enrichMessageWithSourcePaths(original, attachments);
|
|
200
|
+
|
|
201
|
+
// Non-image attachments are not annotated, so we get back the same ref
|
|
202
|
+
expect(result).toBe(original);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("handles multiple images with file paths", () => {
|
|
206
|
+
const attachments = [
|
|
207
|
+
{
|
|
208
|
+
filename: "a.jpg",
|
|
209
|
+
mimeType: "image/jpeg",
|
|
210
|
+
data: "img1",
|
|
211
|
+
filePath: "/path/to/a.jpg",
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
filename: "b.png",
|
|
215
|
+
mimeType: "image/png",
|
|
216
|
+
data: "img2",
|
|
217
|
+
filePath: "/path/to/b.png",
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
const original = createUserMessage("compare", attachments);
|
|
221
|
+
const enriched = enrichMessageWithSourcePaths(original, attachments);
|
|
222
|
+
|
|
223
|
+
expect(enriched).not.toBe(original);
|
|
224
|
+
// text + 2 images + 1 annotation = 4
|
|
225
|
+
expect(enriched.content).toHaveLength(4);
|
|
226
|
+
const annotation = enriched.content[3] as { type: "text"; text: string };
|
|
227
|
+
expect(annotation.type).toBe("text");
|
|
228
|
+
expect(annotation.text).toBe(
|
|
229
|
+
"[Attached image source: /path/to/a.jpg]\n[Attached image source: /path/to/b.png]",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("only annotates images that have filePath, skips those without", () => {
|
|
234
|
+
const attachments = [
|
|
235
|
+
{
|
|
236
|
+
filename: "a.jpg",
|
|
237
|
+
mimeType: "image/jpeg",
|
|
238
|
+
data: "img1",
|
|
239
|
+
filePath: "/path/to/a.jpg",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
filename: "b.png",
|
|
243
|
+
mimeType: "image/png",
|
|
244
|
+
data: "img2",
|
|
245
|
+
// no filePath — e.g. pasted screenshot
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
const original = createUserMessage("compare", attachments);
|
|
249
|
+
const enriched = enrichMessageWithSourcePaths(original, attachments);
|
|
250
|
+
|
|
251
|
+
expect(enriched).not.toBe(original);
|
|
252
|
+
// text + 2 images + 1 annotation (only for a.jpg) = 4
|
|
253
|
+
expect(enriched.content).toHaveLength(4);
|
|
254
|
+
const annotation = enriched.content[3] as { type: "text"; text: string };
|
|
255
|
+
expect(annotation.text).toBe("[Attached image source: /path/to/a.jpg]");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -348,6 +348,7 @@ describe("POST /v1/btw", () => {
|
|
|
348
348
|
|
|
349
349
|
// Options: tool_choice must be "none"
|
|
350
350
|
expect(options!.config!.tool_choice).toEqual({ type: "none" });
|
|
351
|
+
expect(options!.config!.modelIntent).toBe("latency-optimized");
|
|
351
352
|
});
|
|
352
353
|
|
|
353
354
|
// -- No persistence --
|
|
@@ -813,6 +813,44 @@ describe("canonical-guardian-store", () => {
|
|
|
813
813
|
);
|
|
814
814
|
});
|
|
815
815
|
|
|
816
|
+
test("expireAllPendingCanonicalRequests expires persistent kinds with past expiresAt", () => {
|
|
817
|
+
const expiredAccess = createCanonicalGuardianRequest({
|
|
818
|
+
kind: "access_request",
|
|
819
|
+
sourceType: "channel",
|
|
820
|
+
conversationId: "conv-bulk-persist-expired-1",
|
|
821
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
822
|
+
expiresAt: Date.now() - 10_000,
|
|
823
|
+
});
|
|
824
|
+
const expiredGrant = createCanonicalGuardianRequest({
|
|
825
|
+
kind: "tool_grant_request",
|
|
826
|
+
sourceType: "channel",
|
|
827
|
+
conversationId: "conv-bulk-persist-expired-2",
|
|
828
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
829
|
+
expiresAt: Date.now() - 10_000,
|
|
830
|
+
});
|
|
831
|
+
// Persistent kind with future expiresAt should NOT be expired
|
|
832
|
+
const futureAccess = createCanonicalGuardianRequest({
|
|
833
|
+
kind: "access_request",
|
|
834
|
+
sourceType: "channel",
|
|
835
|
+
conversationId: "conv-bulk-persist-expired-3",
|
|
836
|
+
guardianPrincipalId: TEST_PRINCIPAL,
|
|
837
|
+
expiresAt: Date.now() + 60_000,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const count = expireAllPendingCanonicalRequests();
|
|
841
|
+
expect(count).toBe(2);
|
|
842
|
+
|
|
843
|
+
expect(getCanonicalGuardianRequest(expiredAccess.id)!.status).toBe(
|
|
844
|
+
"expired",
|
|
845
|
+
);
|
|
846
|
+
expect(getCanonicalGuardianRequest(expiredGrant.id)!.status).toBe(
|
|
847
|
+
"expired",
|
|
848
|
+
);
|
|
849
|
+
expect(getCanonicalGuardianRequest(futureAccess.id)!.status).toBe(
|
|
850
|
+
"pending",
|
|
851
|
+
);
|
|
852
|
+
});
|
|
853
|
+
|
|
816
854
|
test("expireAllPendingCanonicalRequests does not affect already-resolved requests", () => {
|
|
817
855
|
const approved = createCanonicalGuardianRequest({
|
|
818
856
|
kind: "tool_approval",
|
|
@@ -365,6 +365,61 @@ describe("channel-reply-delivery", () => {
|
|
|
365
365
|
expect(deliveryCalls[0].payload.user).toBeUndefined();
|
|
366
366
|
});
|
|
367
367
|
|
|
368
|
+
it("suppresses delivery when the only text segment is <no_response/>", async () => {
|
|
369
|
+
await deliverRenderedReplyViaCallback({
|
|
370
|
+
callbackUrl: "http://gateway/deliver/slack",
|
|
371
|
+
chatId: "chat-silent",
|
|
372
|
+
textSegments: ["<no_response/>"],
|
|
373
|
+
fallbackText: "Fallback text",
|
|
374
|
+
interSegmentDelayMs: 0,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(deliveryCalls).toHaveLength(0);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("suppresses attachment delivery when <no_response/> is present", async () => {
|
|
381
|
+
await deliverRenderedReplyViaCallback({
|
|
382
|
+
callbackUrl: "http://gateway/deliver/slack",
|
|
383
|
+
chatId: "chat-silent-att",
|
|
384
|
+
textSegments: ["<no_response/>"],
|
|
385
|
+
attachments: [
|
|
386
|
+
{
|
|
387
|
+
id: "att-no-resp",
|
|
388
|
+
filename: "secret.txt",
|
|
389
|
+
mimeType: "text/plain",
|
|
390
|
+
sizeBytes: 10,
|
|
391
|
+
kind: "uploaded",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
interSegmentDelayMs: 0,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(deliveryCalls).toHaveLength(0);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("suppresses delivery for <no_response/> with surrounding whitespace", async () => {
|
|
401
|
+
await deliverRenderedReplyViaCallback({
|
|
402
|
+
callbackUrl: "http://gateway/deliver/slack",
|
|
403
|
+
chatId: "chat-silent-ws",
|
|
404
|
+
textSegments: [" <no_response/> "],
|
|
405
|
+
interSegmentDelayMs: 0,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(deliveryCalls).toHaveLength(0);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("delivers other segments when <no_response/> is mixed with real text", async () => {
|
|
412
|
+
await deliverRenderedReplyViaCallback({
|
|
413
|
+
callbackUrl: "http://gateway/deliver/slack",
|
|
414
|
+
chatId: "chat-mixed",
|
|
415
|
+
textSegments: ["<no_response/>", "Real response."],
|
|
416
|
+
interSegmentDelayMs: 0,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
expect(deliveryCalls).toHaveLength(1);
|
|
420
|
+
expect(deliveryCalls[0].payload.text).toBe("Real response.");
|
|
421
|
+
});
|
|
422
|
+
|
|
368
423
|
it("passes startFromSegment through deliverReplyViaCallback options", async () => {
|
|
369
424
|
conversationMessages.push(
|
|
370
425
|
{ id: "msg-u", role: "user", content: "hi" },
|
|
@@ -294,6 +294,28 @@ describe("Permission Checker", () => {
|
|
|
294
294
|
);
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
test("git --no-pager log is low risk (boolean global flag before subcommand)", async () => {
|
|
298
|
+
expect(
|
|
299
|
+
await classifyRisk("bash", { command: "git --no-pager log" }),
|
|
300
|
+
).toBe(RiskLevel.Low);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("git -C /some/path status is low risk (value-taking flag before subcommand)", async () => {
|
|
304
|
+
expect(
|
|
305
|
+
await classifyRisk("bash", {
|
|
306
|
+
command: "git -C /some/path status",
|
|
307
|
+
}),
|
|
308
|
+
).toBe(RiskLevel.Low);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("git -c core.editor=vim diff is low risk (value-taking -c flag before subcommand)", async () => {
|
|
312
|
+
expect(
|
|
313
|
+
await classifyRisk("bash", {
|
|
314
|
+
command: "git -c core.editor=vim diff",
|
|
315
|
+
}),
|
|
316
|
+
).toBe(RiskLevel.Low);
|
|
317
|
+
});
|
|
318
|
+
|
|
297
319
|
test("echo is low risk", async () => {
|
|
298
320
|
expect(await classifyRisk("bash", { command: "echo hello" })).toBe(
|
|
299
321
|
RiskLevel.Low,
|
|
@@ -381,6 +403,38 @@ describe("Permission Checker", () => {
|
|
|
381
403
|
).toBe(RiskLevel.Medium);
|
|
382
404
|
});
|
|
383
405
|
|
|
406
|
+
test("git -C status commit is medium risk (value-taking flag with dir named like a subcommand)", async () => {
|
|
407
|
+
expect(
|
|
408
|
+
await classifyRisk("bash", {
|
|
409
|
+
command: "git -C status commit",
|
|
410
|
+
}),
|
|
411
|
+
).toBe(RiskLevel.Medium);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("git -C /path push is medium risk (value-taking flag before mutating subcommand)", async () => {
|
|
415
|
+
expect(
|
|
416
|
+
await classifyRisk("bash", {
|
|
417
|
+
command: "git -C /path push",
|
|
418
|
+
}),
|
|
419
|
+
).toBe(RiskLevel.Medium);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("git --git-dir /path/to/.git push is medium risk", async () => {
|
|
423
|
+
expect(
|
|
424
|
+
await classifyRisk("bash", {
|
|
425
|
+
command: "git --git-dir /path/to/.git push",
|
|
426
|
+
}),
|
|
427
|
+
).toBe(RiskLevel.Medium);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("git --no-pager push is medium risk (boolean flag before mutating subcommand)", async () => {
|
|
431
|
+
expect(
|
|
432
|
+
await classifyRisk("bash", {
|
|
433
|
+
command: "git --no-pager push",
|
|
434
|
+
}),
|
|
435
|
+
).toBe(RiskLevel.Medium);
|
|
436
|
+
});
|
|
437
|
+
|
|
384
438
|
test("opaque construct (eval) is medium risk", async () => {
|
|
385
439
|
expect(await classifyRisk("bash", { command: 'eval "ls"' })).toBe(
|
|
386
440
|
RiskLevel.Medium,
|
|
@@ -56,6 +56,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
56
56
|
mock.module("../security/secure-keys.js", () => ({
|
|
57
57
|
getSecureKeyAsync: async (name: string) =>
|
|
58
58
|
name === "anthropic" ? "fake-anthropic-key" : null,
|
|
59
|
+
getProviderKeyAsync: async (provider: string) =>
|
|
60
|
+
provider === "anthropic" ? "fake-anthropic-key" : undefined,
|
|
59
61
|
}));
|
|
60
62
|
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
@@ -40,6 +40,8 @@ mock.module("../config/loader.js", () => ({
|
|
|
40
40
|
mock.module("../security/secure-keys.js", () => ({
|
|
41
41
|
getSecureKeyAsync: async (name: string) =>
|
|
42
42
|
name === "anthropic" ? "fake-anthropic-key" : null,
|
|
43
|
+
getProviderKeyAsync: async (provider: string) =>
|
|
44
|
+
provider === "anthropic" ? "fake-anthropic-key" : undefined,
|
|
43
45
|
}));
|
|
44
46
|
|
|
45
47
|
import { claudeCodeTool } from "../tools/claude-code/claude-code.js";
|
|
@@ -149,7 +149,8 @@ describe("Compaction benchmark", () => {
|
|
|
149
149
|
);
|
|
150
150
|
// Target is maxInputTokens * (targetBudgetRatio - summaryBudgetRatio)
|
|
151
151
|
const targetTokens = Math.floor(
|
|
152
|
-
config.maxInputTokens *
|
|
152
|
+
config.maxInputTokens *
|
|
153
|
+
(config.targetBudgetRatio - config.summaryBudgetRatio),
|
|
153
154
|
);
|
|
154
155
|
expect(result.estimatedInputTokens).toBeLessThanOrEqual(targetTokens);
|
|
155
156
|
});
|