@vellumai/assistant 0.6.4 → 0.6.5
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/.prettierignore +5 -0
- package/ARCHITECTURE.md +32 -36
- package/Dockerfile +12 -0
- package/README.md +3 -4
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +1 -20
- package/docs/architecture/security.md +16 -16
- package/docs/error-handling.md +111 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/openapi.yaml +123 -11
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/approval-cascade.test.ts +31 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +27 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +428 -501
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +427 -114
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +530 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
- package/src/__tests__/conversation-agent-loop.test.ts +412 -82
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +6 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
- package/src/__tests__/conversation-process-callsite.test.ts +306 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
- package/src/__tests__/conversation-queue.test.ts +41 -26
- package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
- package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +34 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
- package/src/__tests__/conversation-speed-override.test.ts +30 -11
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +3 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
- package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +26 -7
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/intent-routing.test.ts +1 -40
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +684 -0
- package/src/__tests__/model-intents.test.ts +9 -83
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/oauth-store.test.ts +10 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +50 -3
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +560 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +160 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor.test.ts +60 -94
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +1 -13
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +209 -17
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +14 -1
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +300 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +23 -4
- package/src/cli.ts +0 -37
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -175
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +30 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +318 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +53 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skills.ts +2 -2
- package/src/context/__tests__/compact-prompt.test.ts +45 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +12 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/window-manager.ts +229 -25
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
- package/src/daemon/conversation-agent-loop.ts +462 -80
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-lifecycle.ts +30 -6
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-process.ts +10 -4
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +760 -29
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +10 -5
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +118 -30
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +54 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +2 -2
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +76 -14
- package/src/daemon/message-types/conversations.ts +14 -0
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +117 -9
- package/src/daemon/tool-side-effects.ts +0 -9
- package/src/daemon/watch-handler.ts +4 -4
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/heartbeat-service.ts +76 -28
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +4 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +56 -2
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +73 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +4 -4
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +61 -0
- package/src/ipc/routes/browser.ts +96 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/index.ts +17 -1
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +100 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +103 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-title-service.ts +7 -4
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +173 -51
- package/src/memory/graph/graph-search.test.ts +92 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +230 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-index.test.ts +368 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +438 -0
- package/src/memory/pkb/pkb-search.ts +137 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.ts +122 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
- package/src/messaging/providers/slack/render-transcript.ts +443 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/oauth/oauth-store.ts +1 -0
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/seed-providers.ts +4 -2
- package/src/permissions/approval-policy.test.ts +948 -0
- package/src/permissions/approval-policy.ts +257 -0
- package/src/permissions/bash-risk-classifier.test.ts +1208 -0
- package/src/permissions/bash-risk-classifier.ts +707 -0
- package/src/permissions/checker.ts +217 -708
- package/src/permissions/command-registry.test.ts +535 -0
- package/src/permissions/command-registry.ts +825 -0
- package/src/permissions/defaults.ts +26 -78
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/risk-types.ts +205 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +23 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +1 -16
- package/src/platform/client.ts +19 -1
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +501 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +76 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
- package/src/providers/speech-to-text/xai-realtime.ts +796 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +52 -1
- package/src/runtime/http-types.ts +23 -1
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
- package/src/runtime/routes/approval-routes.ts +12 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +133 -27
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +912 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/migration-routes.ts +720 -124
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +28 -6
- package/src/schedule/scheduler.ts +8 -0
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +26 -7
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +45 -2
- package/src/tools/browser/browser-execution.ts +65 -38
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +4 -0
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +36 -15
- package/src/tools/policy-context.ts +25 -8
- package/src/tools/registry.ts +55 -3
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +12 -3
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +2 -2
- package/src/util/pricing.ts +15 -5
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
mock.module("../../util/logger.js", () => ({
|
|
7
|
+
getLogger: () =>
|
|
8
|
+
new Proxy({} as Record<string, unknown>, {
|
|
9
|
+
get: () => () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Capture calls to embedAndUpsert so we can assert on targetType + payload.
|
|
14
|
+
const embedAndUpsertCalls: Array<{
|
|
15
|
+
config: unknown;
|
|
16
|
+
targetType: string;
|
|
17
|
+
targetId: string;
|
|
18
|
+
input: unknown;
|
|
19
|
+
extraPayload: unknown;
|
|
20
|
+
}> = [];
|
|
21
|
+
|
|
22
|
+
mock.module("../job-utils.js", () => ({
|
|
23
|
+
embedAndUpsert: async (
|
|
24
|
+
config: unknown,
|
|
25
|
+
targetType: string,
|
|
26
|
+
targetId: string,
|
|
27
|
+
input: unknown,
|
|
28
|
+
extraPayload: unknown,
|
|
29
|
+
) => {
|
|
30
|
+
embedAndUpsertCalls.push({
|
|
31
|
+
config,
|
|
32
|
+
targetType,
|
|
33
|
+
targetId,
|
|
34
|
+
input,
|
|
35
|
+
extraPayload,
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Minimal stub for getConfig — indexPkbFile forwards it opaquely to the
|
|
41
|
+
// mocked embedAndUpsert, so any sentinel value works.
|
|
42
|
+
mock.module("../../config/loader.js", () => ({
|
|
43
|
+
getConfig: () => ({ __stub: true }),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// Track Qdrant deletes by capturing the filter the client sends.
|
|
47
|
+
const qdrantDeleteCalls: Array<{
|
|
48
|
+
targetType: string;
|
|
49
|
+
path: string;
|
|
50
|
+
memoryScopeId: string;
|
|
51
|
+
}> = [];
|
|
52
|
+
|
|
53
|
+
// Track per-target deletes (used by write-then-cleanup in indexPkbFile).
|
|
54
|
+
const qdrantDeleteByTargetCalls: Array<{
|
|
55
|
+
targetType: string;
|
|
56
|
+
targetId: string;
|
|
57
|
+
}> = [];
|
|
58
|
+
|
|
59
|
+
// Points the mocked scroll will return on the next call. Tests mutate this
|
|
60
|
+
// to simulate pre-existing PKB chunks on disk.
|
|
61
|
+
let scrollReturnPoints: Array<{
|
|
62
|
+
id: string;
|
|
63
|
+
payload: Record<string, unknown>;
|
|
64
|
+
}> = [];
|
|
65
|
+
const qdrantScrollCalls: Array<{
|
|
66
|
+
targetType: string;
|
|
67
|
+
memoryScopeId?: string;
|
|
68
|
+
path?: string;
|
|
69
|
+
}> = [];
|
|
70
|
+
|
|
71
|
+
mock.module("../qdrant-client.js", () => ({
|
|
72
|
+
getQdrantClient: () => ({
|
|
73
|
+
deleteByTargetTypeAndPath: async (
|
|
74
|
+
targetType: string,
|
|
75
|
+
path: string,
|
|
76
|
+
memoryScopeId: string,
|
|
77
|
+
) => {
|
|
78
|
+
qdrantDeleteCalls.push({ targetType, path, memoryScopeId });
|
|
79
|
+
},
|
|
80
|
+
deleteByTarget: async (targetType: string, targetId: string) => {
|
|
81
|
+
qdrantDeleteByTargetCalls.push({ targetType, targetId });
|
|
82
|
+
},
|
|
83
|
+
scrollByTargetType: async (
|
|
84
|
+
targetType: string,
|
|
85
|
+
options?: {
|
|
86
|
+
memoryScopeId?: string;
|
|
87
|
+
path?: string;
|
|
88
|
+
batchSize?: number;
|
|
89
|
+
},
|
|
90
|
+
) => {
|
|
91
|
+
qdrantScrollCalls.push({
|
|
92
|
+
targetType,
|
|
93
|
+
memoryScopeId: options?.memoryScopeId,
|
|
94
|
+
path: options?.path,
|
|
95
|
+
});
|
|
96
|
+
return scrollReturnPoints;
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
// The circuit breaker is a thin wrapper; just call the function through.
|
|
102
|
+
mock.module("../qdrant-circuit-breaker.js", () => ({
|
|
103
|
+
withQdrantBreaker: async <T,>(fn: () => Promise<T>) => fn(),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
import {
|
|
107
|
+
chunkPkbFile,
|
|
108
|
+
deletePkbFilePoints,
|
|
109
|
+
indexPkbFile,
|
|
110
|
+
scanPkbFiles,
|
|
111
|
+
} from "./pkb-index.js";
|
|
112
|
+
|
|
113
|
+
describe("chunkPkbFile", () => {
|
|
114
|
+
test("returns whole-file for small inputs", () => {
|
|
115
|
+
const small = "a".repeat(500);
|
|
116
|
+
const chunks = chunkPkbFile(small);
|
|
117
|
+
expect(chunks).toEqual([small]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("splits on ## headings with lossless concatenation", () => {
|
|
121
|
+
const sectionA = "## Section A\n" + "a".repeat(5990) + "\n";
|
|
122
|
+
const sectionB = "## Section B\n" + "b".repeat(6010);
|
|
123
|
+
const content = sectionA + sectionB;
|
|
124
|
+
expect(content.length).toBeGreaterThanOrEqual(12000);
|
|
125
|
+
|
|
126
|
+
const chunks = chunkPkbFile(content);
|
|
127
|
+
expect(chunks).toHaveLength(2);
|
|
128
|
+
expect(chunks.join("")).toBe(content);
|
|
129
|
+
expect(chunks[0].startsWith("## Section A")).toBe(true);
|
|
130
|
+
expect(chunks[1].startsWith("## Section B")).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("falls back to char-window chunks when no ## headings exist", () => {
|
|
134
|
+
const content = "x".repeat(12000);
|
|
135
|
+
const chunks = chunkPkbFile(content);
|
|
136
|
+
// 12000 / 4000 = 3 windows.
|
|
137
|
+
expect(chunks).toHaveLength(3);
|
|
138
|
+
expect(chunks.join("")).toBe(content);
|
|
139
|
+
expect(chunks[0].length).toBe(4000);
|
|
140
|
+
expect(chunks[1].length).toBe(4000);
|
|
141
|
+
expect(chunks[2].length).toBe(4000);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("scanPkbFiles", () => {
|
|
146
|
+
test("returns entries for each .md file and ignores non-markdown", async () => {
|
|
147
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-scan-"));
|
|
148
|
+
await writeFile(join(root, "a.md"), "# A\nalpha content");
|
|
149
|
+
await writeFile(join(root, "b.md"), "# B\nbeta content");
|
|
150
|
+
await writeFile(join(root, "notes.txt"), "plain text");
|
|
151
|
+
|
|
152
|
+
// Set deterministic mtimes so we can assert them.
|
|
153
|
+
const mtimeA = new Date(1_700_000_000_000);
|
|
154
|
+
const mtimeB = new Date(1_700_000_001_000);
|
|
155
|
+
await utimes(join(root, "a.md"), mtimeA, mtimeA);
|
|
156
|
+
await utimes(join(root, "b.md"), mtimeB, mtimeB);
|
|
157
|
+
|
|
158
|
+
const entries = await scanPkbFiles(root);
|
|
159
|
+
expect(entries).not.toBeNull();
|
|
160
|
+
const byPath = new Map(entries!.map((e) => [e.path, e]));
|
|
161
|
+
|
|
162
|
+
expect(byPath.size).toBe(2);
|
|
163
|
+
expect(byPath.has("a.md")).toBe(true);
|
|
164
|
+
expect(byPath.has("b.md")).toBe(true);
|
|
165
|
+
expect(byPath.has("notes.txt")).toBe(false);
|
|
166
|
+
|
|
167
|
+
const a = byPath.get("a.md")!;
|
|
168
|
+
expect(a.mtimeMs).toBe(mtimeA.getTime());
|
|
169
|
+
expect(a.chunkIndex).toBe(0);
|
|
170
|
+
expect(a.contentHash).toHaveLength(16);
|
|
171
|
+
|
|
172
|
+
// Hash is stable across scans.
|
|
173
|
+
const entriesAgain = await scanPkbFiles(root);
|
|
174
|
+
const aAgain = entriesAgain!.find((e) => e.path === "a.md")!;
|
|
175
|
+
expect(aAgain.contentHash).toBe(a.contentHash);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("walks nested directories", async () => {
|
|
179
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-scan-nested-"));
|
|
180
|
+
const sub = join(root, "sub");
|
|
181
|
+
await mkdir(sub);
|
|
182
|
+
await writeFile(join(sub, "nested.md"), "# nested");
|
|
183
|
+
|
|
184
|
+
const entries = await scanPkbFiles(root);
|
|
185
|
+
expect(entries).toHaveLength(1);
|
|
186
|
+
expect(entries![0].path).toBe(join("sub", "nested.md"));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("returns null when pkbRoot does not exist", async () => {
|
|
190
|
+
const parent = await mkdtemp(join(tmpdir(), "pkb-scan-missing-"));
|
|
191
|
+
const missing = join(parent, "does-not-exist");
|
|
192
|
+
const entries = await scanPkbFiles(missing);
|
|
193
|
+
expect(entries).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("returns null when pkbRoot existed then was removed", async () => {
|
|
197
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-scan-removed-"));
|
|
198
|
+
await writeFile(join(root, "a.md"), "# A");
|
|
199
|
+
await rm(root, { recursive: true, force: true });
|
|
200
|
+
|
|
201
|
+
const entries = await scanPkbFiles(root);
|
|
202
|
+
expect(entries).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("returns [] (not null) when pkbRoot exists but is empty", async () => {
|
|
206
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-scan-empty-"));
|
|
207
|
+
const entries = await scanPkbFiles(root);
|
|
208
|
+
expect(entries).not.toBeNull();
|
|
209
|
+
expect(entries).toEqual([]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("returns null when pkbRoot points at a file instead of a directory", async () => {
|
|
213
|
+
const parent = await mkdtemp(join(tmpdir(), "pkb-scan-file-"));
|
|
214
|
+
const filePath = join(parent, "not-a-dir");
|
|
215
|
+
await writeFile(filePath, "just a file");
|
|
216
|
+
const entries = await scanPkbFiles(filePath);
|
|
217
|
+
expect(entries).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("indexPkbFile", () => {
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
embedAndUpsertCalls.length = 0;
|
|
224
|
+
qdrantDeleteCalls.length = 0;
|
|
225
|
+
qdrantDeleteByTargetCalls.length = 0;
|
|
226
|
+
qdrantScrollCalls.length = 0;
|
|
227
|
+
scrollReturnPoints = [];
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("invokes embedAndUpsert once per chunk with pkb_file target_type", async () => {
|
|
231
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-"));
|
|
232
|
+
const filePath = join(root, "doc.md");
|
|
233
|
+
await writeFile(filePath, "# hello\nworld");
|
|
234
|
+
|
|
235
|
+
await indexPkbFile(root, filePath, "scope-xyz");
|
|
236
|
+
|
|
237
|
+
expect(embedAndUpsertCalls).toHaveLength(1);
|
|
238
|
+
const call = embedAndUpsertCalls[0];
|
|
239
|
+
expect(call.targetType).toBe("pkb_file");
|
|
240
|
+
expect(call.targetId).toBe("scope-xyz:doc.md#0");
|
|
241
|
+
expect(call.input).toEqual({ type: "text", text: "# hello\nworld" });
|
|
242
|
+
const payload = call.extraPayload as Record<string, unknown>;
|
|
243
|
+
expect(payload.path).toBe("doc.md");
|
|
244
|
+
expect(payload.chunk_index).toBe(0);
|
|
245
|
+
expect(payload.memory_scope_id).toBe("scope-xyz");
|
|
246
|
+
expect(typeof payload.mtime_ms).toBe("number");
|
|
247
|
+
expect(typeof payload.content_hash).toBe("string");
|
|
248
|
+
expect((payload.content_hash as string).length).toBe(16);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("emits one embedAndUpsert call per chunk for a large file", async () => {
|
|
252
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-large-"));
|
|
253
|
+
const filePath = join(root, "big.md");
|
|
254
|
+
const content =
|
|
255
|
+
"## Section A\n" +
|
|
256
|
+
"a".repeat(5990) +
|
|
257
|
+
"\n## Section B\n" +
|
|
258
|
+
"b".repeat(5990);
|
|
259
|
+
await writeFile(filePath, content);
|
|
260
|
+
|
|
261
|
+
await indexPkbFile(root, filePath, "scope-1");
|
|
262
|
+
|
|
263
|
+
expect(embedAndUpsertCalls).toHaveLength(2);
|
|
264
|
+
expect(embedAndUpsertCalls[0].targetId).toBe("scope-1:big.md#0");
|
|
265
|
+
expect(embedAndUpsertCalls[1].targetId).toBe("scope-1:big.md#1");
|
|
266
|
+
expect(embedAndUpsertCalls.every((c) => c.targetType === "pkb_file")).toBe(
|
|
267
|
+
true,
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("scope-namespaces target ids so two scopes indexing the same path do not collide", async () => {
|
|
272
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-scope-"));
|
|
273
|
+
const filePath = join(root, "shared.md");
|
|
274
|
+
await writeFile(filePath, "# shared");
|
|
275
|
+
|
|
276
|
+
await indexPkbFile(root, filePath, "alpha");
|
|
277
|
+
await indexPkbFile(root, filePath, "beta");
|
|
278
|
+
|
|
279
|
+
expect(embedAndUpsertCalls).toHaveLength(2);
|
|
280
|
+
const ids = embedAndUpsertCalls.map((c) => c.targetId);
|
|
281
|
+
expect(ids).toEqual(["alpha:shared.md#0", "beta:shared.md#0"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("scrolls existing chunks scoped to (target_type, scope, path) before upserting", async () => {
|
|
285
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-scroll-"));
|
|
286
|
+
const filePath = join(root, "noted.md");
|
|
287
|
+
await writeFile(filePath, "# one");
|
|
288
|
+
|
|
289
|
+
await indexPkbFile(root, filePath, "scope-xyz");
|
|
290
|
+
|
|
291
|
+
expect(qdrantScrollCalls).toHaveLength(1);
|
|
292
|
+
expect(qdrantScrollCalls[0]).toEqual({
|
|
293
|
+
targetType: "pkb_file",
|
|
294
|
+
memoryScopeId: "scope-xyz",
|
|
295
|
+
path: "noted.md",
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("deletes only stale chunks after upserting (write-then-cleanup)", async () => {
|
|
300
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-shrink-"));
|
|
301
|
+
const filePath = join(root, "shrinking.md");
|
|
302
|
+
// New content produces a single chunk (index #0). Pre-existing chunks
|
|
303
|
+
// #0..#2 simulate a prior run over a larger file.
|
|
304
|
+
await writeFile(filePath, "# just one");
|
|
305
|
+
scrollReturnPoints = [
|
|
306
|
+
{
|
|
307
|
+
id: "point-0",
|
|
308
|
+
payload: { target_id: "scope-xyz:shrinking.md#0" },
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: "point-1",
|
|
312
|
+
payload: { target_id: "scope-xyz:shrinking.md#1" },
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "point-2",
|
|
316
|
+
payload: { target_id: "scope-xyz:shrinking.md#2" },
|
|
317
|
+
},
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
await indexPkbFile(root, filePath, "scope-xyz");
|
|
321
|
+
|
|
322
|
+
// Exactly one upsert for the surviving chunk.
|
|
323
|
+
expect(embedAndUpsertCalls).toHaveLength(1);
|
|
324
|
+
expect(embedAndUpsertCalls[0].targetId).toBe("scope-xyz:shrinking.md#0");
|
|
325
|
+
|
|
326
|
+
// The pre-delete is gone; only the two stale chunks are removed.
|
|
327
|
+
expect(qdrantDeleteCalls).toHaveLength(0);
|
|
328
|
+
const staleTargetIds = qdrantDeleteByTargetCalls.map((c) => c.targetId);
|
|
329
|
+
expect(staleTargetIds.sort()).toEqual([
|
|
330
|
+
"scope-xyz:shrinking.md#1",
|
|
331
|
+
"scope-xyz:shrinking.md#2",
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("does not delete points whose target_id is regenerated", async () => {
|
|
336
|
+
const root = await mkdtemp(join(tmpdir(), "pkb-index-stable-"));
|
|
337
|
+
const filePath = join(root, "stable.md");
|
|
338
|
+
await writeFile(filePath, "# same");
|
|
339
|
+
scrollReturnPoints = [
|
|
340
|
+
{
|
|
341
|
+
id: "point-0",
|
|
342
|
+
payload: { target_id: "scope-xyz:stable.md#0" },
|
|
343
|
+
},
|
|
344
|
+
];
|
|
345
|
+
|
|
346
|
+
await indexPkbFile(root, filePath, "scope-xyz");
|
|
347
|
+
|
|
348
|
+
expect(embedAndUpsertCalls).toHaveLength(1);
|
|
349
|
+
expect(qdrantDeleteByTargetCalls).toHaveLength(0);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("deletePkbFilePoints", () => {
|
|
354
|
+
beforeEach(() => {
|
|
355
|
+
qdrantDeleteCalls.length = 0;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("sends a filter with target_type, path, and memory_scope_id predicates", async () => {
|
|
359
|
+
await deletePkbFilePoints("notes/todo.md", "scope-xyz");
|
|
360
|
+
|
|
361
|
+
expect(qdrantDeleteCalls).toHaveLength(1);
|
|
362
|
+
expect(qdrantDeleteCalls[0]).toEqual({
|
|
363
|
+
targetType: "pkb_file",
|
|
364
|
+
path: "notes/todo.md",
|
|
365
|
+
memoryScopeId: "scope-xyz",
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PKB (Personal Knowledge Base) filesystem indexing primitives.
|
|
3
|
+
*
|
|
4
|
+
* Provides the low-level building blocks used by the PKB job handler and
|
|
5
|
+
* startup reconciliation:
|
|
6
|
+
* - `scanPkbFiles`: recursively walk a PKB root and emit one entry per chunk.
|
|
7
|
+
* - `chunkPkbFile`: split a markdown file into retrieval-friendly chunks.
|
|
8
|
+
* - `indexPkbFile`: embed each chunk and upsert it to Qdrant.
|
|
9
|
+
* - `deletePkbFilePoints`: remove every Qdrant point for a given file.
|
|
10
|
+
*
|
|
11
|
+
* Consumers (job queue wiring, startup scan) land in later PRs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
16
|
+
import { join, relative } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { getConfig } from "../../config/loader.js";
|
|
19
|
+
import { embedAndUpsert } from "../job-utils.js";
|
|
20
|
+
import { withQdrantBreaker } from "../qdrant-circuit-breaker.js";
|
|
21
|
+
import { getQdrantClient } from "../qdrant-client.js";
|
|
22
|
+
import type { PkbIndexEntry } from "./types.js";
|
|
23
|
+
import { PKB_TARGET_TYPE } from "./types.js";
|
|
24
|
+
|
|
25
|
+
/** Files larger than this are split into chunks for retrieval. */
|
|
26
|
+
const WHOLE_FILE_THRESHOLD = 8000;
|
|
27
|
+
|
|
28
|
+
/** Character-window size when falling back for unstructured content. */
|
|
29
|
+
const CHAR_WINDOW_SIZE = 4000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively walk `pkbRoot` and return one `PkbIndexEntry` per chunk of
|
|
33
|
+
* every `*.md` file found. Paths in the returned entries are relative to
|
|
34
|
+
* `pkbRoot`; mtime is read from the filesystem and `contentHash` is the
|
|
35
|
+
* first 16 hex chars of the sha256 of the file's contents.
|
|
36
|
+
*
|
|
37
|
+
* Returns `null` if `pkbRoot` cannot be confirmed as an existing directory
|
|
38
|
+
* (missing, not a directory, or stat failed for any reason — ENOENT, EACCES,
|
|
39
|
+
* EIO, etc.). This is distinct from returning `[]` for a directory that
|
|
40
|
+
* exists but has no `*.md` files — callers that run destructive
|
|
41
|
+
* reconciliation against the result (e.g. `reconcilePkbIndex`) use the
|
|
42
|
+
* sentinel to avoid interpreting an unreadable or transiently missing root
|
|
43
|
+
* as "delete every indexed point".
|
|
44
|
+
*/
|
|
45
|
+
export async function scanPkbFiles(
|
|
46
|
+
pkbRoot: string,
|
|
47
|
+
): Promise<PkbIndexEntry[] | null> {
|
|
48
|
+
const entries: PkbIndexEntry[] = [];
|
|
49
|
+
|
|
50
|
+
// Verify the root exists up front. Any failure to confirm the root is a
|
|
51
|
+
// directory — ENOENT, EACCES, EIO, or a path that exists but isn't a
|
|
52
|
+
// directory — returns the missing sentinel so callers that run destructive
|
|
53
|
+
// reconciliation don't interpret "couldn't read the tree" as "disk is
|
|
54
|
+
// empty, delete everything indexed".
|
|
55
|
+
try {
|
|
56
|
+
const rootStat = await stat(pkbRoot);
|
|
57
|
+
if (!rootStat.isDirectory()) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function walk(dir: string): Promise<void> {
|
|
65
|
+
let dirents;
|
|
66
|
+
try {
|
|
67
|
+
dirents = await readdir(dir, { withFileTypes: true });
|
|
68
|
+
} catch {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const dirent of dirents) {
|
|
73
|
+
const absPath = join(dir, dirent.name);
|
|
74
|
+
if (dirent.isDirectory()) {
|
|
75
|
+
await walk(absPath);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!dirent.isFile()) continue;
|
|
79
|
+
if (!dirent.name.toLowerCase().endsWith(".md")) continue;
|
|
80
|
+
|
|
81
|
+
let content: string;
|
|
82
|
+
let mtimeMs: number;
|
|
83
|
+
try {
|
|
84
|
+
content = await readFile(absPath, "utf8");
|
|
85
|
+
const st = await stat(absPath);
|
|
86
|
+
mtimeMs = st.mtimeMs;
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const contentHash = hashContent(content);
|
|
92
|
+
const relPath = relative(pkbRoot, absPath);
|
|
93
|
+
const chunks = chunkPkbFile(content);
|
|
94
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
95
|
+
entries.push({
|
|
96
|
+
path: relPath,
|
|
97
|
+
mtimeMs,
|
|
98
|
+
contentHash,
|
|
99
|
+
chunkIndex,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await walk(pkbRoot);
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Split markdown content into retrieval chunks.
|
|
111
|
+
*
|
|
112
|
+
* Strategy:
|
|
113
|
+
* - If the file is small (< WHOLE_FILE_THRESHOLD chars), return the whole
|
|
114
|
+
* file as a single chunk.
|
|
115
|
+
* - Otherwise split on lines starting with `## `, keeping each heading
|
|
116
|
+
* with the body of its section. Concatenation of the returned chunks is
|
|
117
|
+
* lossless — no content is dropped or duplicated.
|
|
118
|
+
* - If no `## ` headings are present, fall back to fixed-size character
|
|
119
|
+
* windows.
|
|
120
|
+
*/
|
|
121
|
+
export function chunkPkbFile(content: string): string[] {
|
|
122
|
+
if (content.length < WHOLE_FILE_THRESHOLD) {
|
|
123
|
+
return [content];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const headingIndices: number[] = [];
|
|
127
|
+
let cursor = 0;
|
|
128
|
+
while (cursor < content.length) {
|
|
129
|
+
// Find the next line that starts with "## " (either at the very beginning
|
|
130
|
+
// of the file or immediately after a newline).
|
|
131
|
+
const atStart = cursor === 0 && content.startsWith("## ");
|
|
132
|
+
if (atStart) {
|
|
133
|
+
headingIndices.push(0);
|
|
134
|
+
cursor = 1; // advance past the match so the `indexOf` below keeps moving
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const nextNewline = content.indexOf("\n## ", cursor);
|
|
138
|
+
if (nextNewline === -1) break;
|
|
139
|
+
headingIndices.push(nextNewline + 1);
|
|
140
|
+
cursor = nextNewline + 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (headingIndices.length === 0) {
|
|
144
|
+
// Fallback: fixed-size char windows.
|
|
145
|
+
const chunks: string[] = [];
|
|
146
|
+
for (let i = 0; i < content.length; i += CHAR_WINDOW_SIZE) {
|
|
147
|
+
chunks.push(content.slice(i, i + CHAR_WINDOW_SIZE));
|
|
148
|
+
}
|
|
149
|
+
return chunks;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build chunks from heading boundaries. Preserve any preamble before the
|
|
153
|
+
// first heading so concatenation stays lossless.
|
|
154
|
+
const chunks: string[] = [];
|
|
155
|
+
if (headingIndices[0] > 0) {
|
|
156
|
+
chunks.push(content.slice(0, headingIndices[0]));
|
|
157
|
+
}
|
|
158
|
+
for (let i = 0; i < headingIndices.length; i++) {
|
|
159
|
+
const start = headingIndices[i];
|
|
160
|
+
const end = i + 1 < headingIndices.length ? headingIndices[i + 1] : content.length;
|
|
161
|
+
chunks.push(content.slice(start, end));
|
|
162
|
+
}
|
|
163
|
+
return chunks;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Read a PKB file, chunk it, and upsert each chunk to Qdrant via the shared
|
|
168
|
+
* embedding pipeline. `relPath` in the payload is computed relative to
|
|
169
|
+
* `pkbRoot`.
|
|
170
|
+
*
|
|
171
|
+
* Write-then-cleanup: enumerate the existing chunk target_ids for this
|
|
172
|
+
* (scope, path), upsert every new chunk, then delete only the stale
|
|
173
|
+
* target_ids (those the fresh content no longer produces). Upsert dedupes on
|
|
174
|
+
* (target_type, target_id), so matching chunk indexes are replaced in place
|
|
175
|
+
* and the prior index stays queryable if any embed/upsert call throws. A
|
|
176
|
+
* pre-delete would instead leave the file unsearchable on transient failure
|
|
177
|
+
* until a successful retry.
|
|
178
|
+
*/
|
|
179
|
+
export async function indexPkbFile(
|
|
180
|
+
pkbRoot: string,
|
|
181
|
+
absPath: string,
|
|
182
|
+
memoryScopeId: string,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const content = await readFile(absPath, "utf8");
|
|
185
|
+
const st = await stat(absPath);
|
|
186
|
+
const mtimeMs = st.mtimeMs;
|
|
187
|
+
const contentHash = hashContent(content);
|
|
188
|
+
const relPath = relative(pkbRoot, absPath);
|
|
189
|
+
const chunks = chunkPkbFile(content);
|
|
190
|
+
|
|
191
|
+
const config = getConfig();
|
|
192
|
+
|
|
193
|
+
const qdrant = getQdrantClient();
|
|
194
|
+
const existing = await withQdrantBreaker(() =>
|
|
195
|
+
qdrant.scrollByTargetType(PKB_TARGET_TYPE, {
|
|
196
|
+
memoryScopeId,
|
|
197
|
+
path: relPath,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const newTargetIds = new Set<string>();
|
|
202
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
|
|
203
|
+
const chunk = chunks[chunkIndex];
|
|
204
|
+
// Scope-namespace the target_id so `qdrant.upsert` — which dedupes on
|
|
205
|
+
// (target_type, target_id) — cannot collapse distinct scopes' chunks of
|
|
206
|
+
// the same relpath into a single point. Without the scope prefix, the
|
|
207
|
+
// second scope to index a shared path would overwrite the first's vectors.
|
|
208
|
+
const targetId = `${memoryScopeId}:${relPath}#${chunkIndex}`;
|
|
209
|
+
newTargetIds.add(targetId);
|
|
210
|
+
await embedAndUpsert(
|
|
211
|
+
config,
|
|
212
|
+
PKB_TARGET_TYPE,
|
|
213
|
+
targetId,
|
|
214
|
+
{ type: "text", text: chunk },
|
|
215
|
+
{
|
|
216
|
+
path: relPath,
|
|
217
|
+
mtime_ms: mtimeMs,
|
|
218
|
+
chunk_index: chunkIndex,
|
|
219
|
+
content_hash: contentHash,
|
|
220
|
+
memory_scope_id: memoryScopeId,
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// All upserts succeeded — safe to remove stale chunks that the new content
|
|
226
|
+
// did not regenerate (e.g. the file shrunk from 4 chunks to 2).
|
|
227
|
+
for (const point of existing) {
|
|
228
|
+
const staleTargetId = point.payload.target_id;
|
|
229
|
+
if (typeof staleTargetId !== "string") continue;
|
|
230
|
+
if (newTargetIds.has(staleTargetId)) continue;
|
|
231
|
+
await withQdrantBreaker(() =>
|
|
232
|
+
qdrant.deleteByTarget(PKB_TARGET_TYPE, staleTargetId),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Remove every Qdrant point belonging to a given PKB file (all chunks) within
|
|
239
|
+
* a single memory scope. `relPath` must match the `path` payload written by
|
|
240
|
+
* `indexPkbFile`. The `memoryScopeId` filter is required — omitting it would
|
|
241
|
+
* wipe that relpath's chunks across every scope that indexes the same file.
|
|
242
|
+
*/
|
|
243
|
+
export async function deletePkbFilePoints(
|
|
244
|
+
relPath: string,
|
|
245
|
+
memoryScopeId: string,
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
const qdrant = getQdrantClient();
|
|
248
|
+
await withQdrantBreaker(() =>
|
|
249
|
+
qdrant.deleteByTargetTypeAndPath(PKB_TARGET_TYPE, relPath, memoryScopeId),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function hashContent(content: string): string {
|
|
254
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
255
|
+
}
|