@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
* {@link captionImagesInMessages} (which also persists the original and
|
|
18
18
|
* caches captions across turns), sweeping top-level blocks and images
|
|
19
19
|
* nested in `tool_result` blocks alike.
|
|
20
|
+
* 4. Posts a transcript system card when an image the user attached to this
|
|
21
|
+
* turn reached step 3 with no vision profile configured, since that image
|
|
22
|
+
* is information the model never receives in any form.
|
|
23
|
+
*
|
|
24
|
+
* The card covers only images attached to the message this turn submitted.
|
|
25
|
+
* The sweep also walks earlier turns (persisted rows keep their raw images, so
|
|
26
|
+
* every turn re-encounters them) and tool-returned media, and describing those
|
|
27
|
+
* again would post a card per turn for images the user already heard about.
|
|
20
28
|
*
|
|
21
29
|
* The companion `post-tool-use` hook applies the same substitution to images a
|
|
22
30
|
* tool returns (e.g. a browser screenshot), and `post-compact` re-sweeps the
|
|
@@ -25,6 +33,7 @@
|
|
|
25
33
|
|
|
26
34
|
import {
|
|
27
35
|
type HookFunction,
|
|
36
|
+
persistSystemCard,
|
|
28
37
|
type UserPromptSubmitContext,
|
|
29
38
|
} from "@vellumai/plugin-api";
|
|
30
39
|
|
|
@@ -34,6 +43,29 @@ import {
|
|
|
34
43
|
} from "../src/caption-blocks.js";
|
|
35
44
|
import { findVisionProfile } from "../src/vision-caption.js";
|
|
36
45
|
|
|
46
|
+
/**
|
|
47
|
+
* How many images the user attached to the message this turn submitted, read
|
|
48
|
+
* off the pre-hook history snapshot: its tail is the submitted row as the user
|
|
49
|
+
* sent it, before any earlier hook's injected blocks (memory context, runtime
|
|
50
|
+
* context) reach the working history. Counting there keeps the tally to the
|
|
51
|
+
* user's own attachments and needs no object identity, which injection breaks
|
|
52
|
+
* by rebuilding the tail message around its added blocks.
|
|
53
|
+
*/
|
|
54
|
+
function submittedImageCount(ctx: UserPromptSubmitContext): number {
|
|
55
|
+
const submitted = ctx.originalMessages[ctx.originalMessages.length - 1];
|
|
56
|
+
if (submitted == null || submitted.role !== "user") {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
return submitted.content.filter((block) => block.type === "image").length;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Transcript copy for images this turn could not send to the model. */
|
|
63
|
+
function droppedImageCardText(count: number): string {
|
|
64
|
+
return count === 1
|
|
65
|
+
? "The image you attached was not sent to the model: no vision-capable model is configured to describe it. Configure one to use images in this conversation."
|
|
66
|
+
: `The ${count} images you attached were not sent to the model: no vision-capable model is configured to describe them. Configure one to use images in this conversation.`;
|
|
67
|
+
}
|
|
68
|
+
|
|
37
69
|
const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
38
70
|
// If the turn's model already supports vision, nothing to do.
|
|
39
71
|
if (!needsImageFallback(ctx.modelProfileKey)) {
|
|
@@ -43,6 +75,12 @@ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
|
43
75
|
// Find a vision-capable profile for captioning.
|
|
44
76
|
const visionProfileKey = findVisionProfile();
|
|
45
77
|
|
|
78
|
+
// Images the user attached to this turn, tallied before the sweep replaces
|
|
79
|
+
// them: with no vision profile every one of them is dropped, so this is the
|
|
80
|
+
// count the card reports.
|
|
81
|
+
const droppedFromSubmission =
|
|
82
|
+
visionProfileKey == null ? submittedImageCount(ctx) : 0;
|
|
83
|
+
|
|
46
84
|
// Scan all messages for image blocks and replace them with captions.
|
|
47
85
|
const imageCount = await captionImagesInMessages(
|
|
48
86
|
ctx.latestMessages,
|
|
@@ -57,6 +95,26 @@ const userPromptSubmit: HookFunction<UserPromptSubmitContext> = async (ctx) => {
|
|
|
57
95
|
"Replaced image blocks with text captions for text-only model",
|
|
58
96
|
);
|
|
59
97
|
}
|
|
98
|
+
|
|
99
|
+
if (droppedFromSubmission > 0) {
|
|
100
|
+
try {
|
|
101
|
+
await persistSystemCard({
|
|
102
|
+
conversationId: ctx.conversationId,
|
|
103
|
+
text: droppedImageCardText(droppedFromSubmission),
|
|
104
|
+
metadata: {
|
|
105
|
+
plugin: "image-fallback",
|
|
106
|
+
droppedImageCount: droppedFromSubmission,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// The turn proceeds with the placeholder text either way; the notice is
|
|
111
|
+
// the only thing lost.
|
|
112
|
+
ctx.logger.warn(
|
|
113
|
+
{ plugin: "image-fallback", err },
|
|
114
|
+
"Failed to post dropped-image notice",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
60
118
|
};
|
|
61
119
|
|
|
62
120
|
export default userPromptSubmit;
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* original image to a known location, caption it via a vision-capable
|
|
12
12
|
* profile, and swap in a `[Image …]` text block — and the message-level deep
|
|
13
13
|
* sweep ({@link captionImagesInMessages}) that reaches images nested inside
|
|
14
|
-
* `tool_result` blocks as well as top-level ones.
|
|
14
|
+
* `tool_result` blocks as well as top-level ones. The message-level sweeps
|
|
15
|
+
* close by merging text-only user content into a single text block
|
|
16
|
+
* ({@link flattenTextOnlyBlocks}), the shape providers serialize as a plain
|
|
17
|
+
* string.
|
|
15
18
|
*
|
|
16
19
|
* The substitution mutates the blocks in place, but the hook pipeline hands
|
|
17
20
|
* each hook a deep clone of its context, so the caption reaches only the
|
|
@@ -62,8 +65,7 @@ export function needsImageFallback(modelProfileKey: string): boolean {
|
|
|
62
65
|
|
|
63
66
|
/**
|
|
64
67
|
* Replace every `image` block in `blocks` (in place) with a text caption so a
|
|
65
|
-
* text-only model can still reason about the image's content.
|
|
66
|
-
* number of image blocks replaced.
|
|
68
|
+
* text-only model can still reason about the image's content.
|
|
67
69
|
*
|
|
68
70
|
* @param blocks Content-block array to scan and mutate in place.
|
|
69
71
|
* @param conversationId Conversation the blocks belong to, recorded on the
|
|
@@ -80,7 +82,7 @@ export async function captionImageBlocks(
|
|
|
80
82
|
visionProfileKey: string | null,
|
|
81
83
|
logger: PluginLogger,
|
|
82
84
|
): Promise<number> {
|
|
83
|
-
let
|
|
85
|
+
let replaced = 0;
|
|
84
86
|
|
|
85
87
|
for (let i = 0; i < blocks.length; i++) {
|
|
86
88
|
const block = blocks[i];
|
|
@@ -88,7 +90,7 @@ export async function captionImageBlocks(
|
|
|
88
90
|
continue;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
|
|
93
|
+
replaced++;
|
|
92
94
|
const image = block as ImageContent;
|
|
93
95
|
|
|
94
96
|
// Persist the original to a known, content-hash-deduped location so it
|
|
@@ -114,7 +116,7 @@ export async function captionImageBlocks(
|
|
|
114
116
|
: `[Image: auto-description failed (text-only model)]`,
|
|
115
117
|
};
|
|
116
118
|
} else {
|
|
117
|
-
// No vision profile configured at all
|
|
119
|
+
// No vision profile configured at all: fail-open placeholder.
|
|
118
120
|
blocks[i] = {
|
|
119
121
|
type: "text",
|
|
120
122
|
text: `[Image: no vision-capable model configured to describe it]`,
|
|
@@ -122,12 +124,12 @@ export async function captionImageBlocks(
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
return
|
|
127
|
+
return replaced;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
/**
|
|
129
131
|
* Replace image blocks nested in a message's `tool_result` blocks' rich
|
|
130
|
-
* `contentBlocks` (in place) with text captions.
|
|
132
|
+
* `contentBlocks` (in place) with text captions.
|
|
131
133
|
*/
|
|
132
134
|
async function captionToolResultMedia(
|
|
133
135
|
message: Message,
|
|
@@ -135,10 +137,10 @@ async function captionToolResultMedia(
|
|
|
135
137
|
visionProfileKey: string | null,
|
|
136
138
|
logger: PluginLogger,
|
|
137
139
|
): Promise<number> {
|
|
138
|
-
let
|
|
140
|
+
let replaced = 0;
|
|
139
141
|
for (const block of message.content) {
|
|
140
142
|
if (block.type === "tool_result" && block.contentBlocks != null) {
|
|
141
|
-
|
|
143
|
+
replaced += await captionImageBlocks(
|
|
142
144
|
block.contentBlocks,
|
|
143
145
|
conversationId,
|
|
144
146
|
visionProfileKey,
|
|
@@ -146,7 +148,42 @@ async function captionToolResultMedia(
|
|
|
146
148
|
);
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
|
-
return
|
|
151
|
+
return replaced;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Merge a user message's text blocks (in place) into a single text block,
|
|
156
|
+
* joined by a blank line, for every message whose content is more than one
|
|
157
|
+
* block and entirely text. Returns how many messages were merged.
|
|
158
|
+
*
|
|
159
|
+
* A text-only turn's user content is text after image substitution, and a
|
|
160
|
+
* single text block is the shape providers serialize as a plain string:
|
|
161
|
+
* OpenAI-compatible endpoints that accept only `messages[].content` as a
|
|
162
|
+
* string (rejecting an array of content parts with
|
|
163
|
+
* `body/messages/N/content must be string`) can then take the turn, and the
|
|
164
|
+
* request costs fewer tokens than the equivalent array of parts. The blank
|
|
165
|
+
* line keeps the boundaries between the blocks a user message carries (the
|
|
166
|
+
* turn's runtime-injected context blocks plus the user's own text) legible.
|
|
167
|
+
*
|
|
168
|
+
* Content holding any non-text block (audio, tool results, an image no
|
|
169
|
+
* fallback replaced) keeps its array shape, since merging would drop it.
|
|
170
|
+
*/
|
|
171
|
+
export function flattenTextOnlyBlocks(messages: Message[]): number {
|
|
172
|
+
let flattened = 0;
|
|
173
|
+
for (const message of messages) {
|
|
174
|
+
if (message.role !== "user" || message.content.length <= 1) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (!message.content.every((block) => block.type === "text")) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const text = message.content
|
|
181
|
+
.map((block) => (block.type === "text" ? block.text : ""))
|
|
182
|
+
.join("\n\n");
|
|
183
|
+
message.content = [{ type: "text", text }];
|
|
184
|
+
flattened++;
|
|
185
|
+
}
|
|
186
|
+
return flattened;
|
|
150
187
|
}
|
|
151
188
|
|
|
152
189
|
/**
|
|
@@ -154,8 +191,9 @@ async function captionToolResultMedia(
|
|
|
154
191
|
* a text caption via {@link captionImageBlocks}. Covers both top-level image
|
|
155
192
|
* blocks (user-attached images, the compactor's retained-image message) and
|
|
156
193
|
* images nested in a `tool_result` block's rich `contentBlocks` (tool results
|
|
157
|
-
* restored from persistence carry their raw images there).
|
|
158
|
-
*
|
|
194
|
+
* restored from persistence carry their raw images there).
|
|
195
|
+
*
|
|
196
|
+
* Text-only user content is then merged via {@link flattenTextOnlyBlocks}.
|
|
159
197
|
*/
|
|
160
198
|
export async function captionImagesInMessages(
|
|
161
199
|
messages: Message[],
|
|
@@ -163,22 +201,23 @@ export async function captionImagesInMessages(
|
|
|
163
201
|
visionProfileKey: string | null,
|
|
164
202
|
logger: PluginLogger,
|
|
165
203
|
): Promise<number> {
|
|
166
|
-
let
|
|
204
|
+
let replaced = 0;
|
|
167
205
|
for (const message of messages) {
|
|
168
|
-
|
|
206
|
+
replaced += await captionImageBlocks(
|
|
169
207
|
message.content,
|
|
170
208
|
conversationId,
|
|
171
209
|
visionProfileKey,
|
|
172
210
|
logger,
|
|
173
211
|
);
|
|
174
|
-
|
|
212
|
+
replaced += await captionToolResultMedia(
|
|
175
213
|
message,
|
|
176
214
|
conversationId,
|
|
177
215
|
visionProfileKey,
|
|
178
216
|
logger,
|
|
179
217
|
);
|
|
180
218
|
}
|
|
181
|
-
|
|
219
|
+
flattenTextOnlyBlocks(messages);
|
|
220
|
+
return replaced;
|
|
182
221
|
}
|
|
183
222
|
|
|
184
223
|
/**
|
|
@@ -188,8 +227,9 @@ export async function captionImagesInMessages(
|
|
|
188
227
|
* message ({@link lastToolResultUserMessageIndex}, the one the sanitizer keeps
|
|
189
228
|
* intact). Older tool_result media is left raw so the sanitizer replaces it
|
|
190
229
|
* with its compact removed-media marker on the retry rather than a full
|
|
191
|
-
* caption
|
|
192
|
-
*
|
|
230
|
+
* caption: captioning it would waste vision calls and balloon context.
|
|
231
|
+
*
|
|
232
|
+
* Text-only user content is then merged via {@link flattenTextOnlyBlocks}.
|
|
193
233
|
*/
|
|
194
234
|
export async function captionOutboundImagesInMessages(
|
|
195
235
|
messages: Message[],
|
|
@@ -198,16 +238,16 @@ export async function captionOutboundImagesInMessages(
|
|
|
198
238
|
logger: PluginLogger,
|
|
199
239
|
): Promise<number> {
|
|
200
240
|
const currentTurnIdx = lastToolResultUserMessageIndex(messages);
|
|
201
|
-
let
|
|
241
|
+
let replaced = 0;
|
|
202
242
|
for (let i = 0; i < messages.length; i++) {
|
|
203
|
-
|
|
243
|
+
replaced += await captionImageBlocks(
|
|
204
244
|
messages[i].content,
|
|
205
245
|
conversationId,
|
|
206
246
|
visionProfileKey,
|
|
207
247
|
logger,
|
|
208
248
|
);
|
|
209
249
|
if (i === currentTurnIdx) {
|
|
210
|
-
|
|
250
|
+
replaced += await captionToolResultMedia(
|
|
211
251
|
messages[i],
|
|
212
252
|
conversationId,
|
|
213
253
|
visionProfileKey,
|
|
@@ -215,5 +255,6 @@ export async function captionOutboundImagesInMessages(
|
|
|
215
255
|
);
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
|
-
|
|
258
|
+
flattenTextOnlyBlocks(messages);
|
|
259
|
+
return replaced;
|
|
219
260
|
}
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* classification matters because retrying with the same image is futile — the
|
|
14
14
|
* recovery path must resize, relabel, or note it instead.
|
|
15
15
|
*
|
|
16
|
+
* OpenAI-compatible providers reject the same class of input in their own
|
|
17
|
+
* wording ("The image data you provided does not represent a valid image.
|
|
18
|
+
* Please check your input and try again with one of the supported image
|
|
19
|
+
* formats: [image/jpeg, image/png, image/gif, image/webp]"), so the
|
|
20
|
+
* unsupported-format patterns carry that shape. Without them an OpenAI-family
|
|
21
|
+
* 400 falls through to the generic provider-error copy and takes the whole
|
|
22
|
+
* turn down with no hint that an attachment is what broke it.
|
|
23
|
+
*
|
|
16
24
|
* Exported as the single source of truth: the image-recovery hook reads these
|
|
17
25
|
* to recognize the rejections it can recover, and `daemon/conversation-error`
|
|
18
26
|
* imports them so the user-facing classification stays in lockstep with what
|
|
@@ -32,6 +40,11 @@ export const IMAGE_MEDIA_TYPE_MISMATCH_PATTERNS: readonly RegExp[] = [
|
|
|
32
40
|
/image does not match the provided media type/i,
|
|
33
41
|
];
|
|
34
42
|
|
|
43
|
+
export const IMAGE_UNSUPPORTED_FORMAT_PATTERNS: readonly RegExp[] = [
|
|
44
|
+
/does not represent a valid image/i,
|
|
45
|
+
/supported image formats?:/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
35
48
|
/** Whether an error message indicates an image-input dimension/payload failure. */
|
|
36
49
|
export function isImageDimensionsTooLargeError(message: string): boolean {
|
|
37
50
|
return IMAGE_DIMENSIONS_TOO_LARGE_PATTERNS.some((p) => p.test(message));
|
|
@@ -53,11 +66,21 @@ export function isImageMediaTypeMismatchError(message: string): boolean {
|
|
|
53
66
|
return IMAGE_MEDIA_TYPE_MISMATCH_PATTERNS.some((p) => p.test(message));
|
|
54
67
|
}
|
|
55
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Whether an error message indicates the image's bytes are not in a format the
|
|
71
|
+
* provider accepts at all, rather than merely mislabeled: an unsupported
|
|
72
|
+
* codec (HEIC, AVIF, BMP), or a truncated or corrupt payload.
|
|
73
|
+
*/
|
|
74
|
+
export function isImageUnsupportedFormatError(message: string): boolean {
|
|
75
|
+
return IMAGE_UNSUPPORTED_FORMAT_PATTERNS.some((p) => p.test(message));
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
/** Any image-input rejection the recovery hook knows how to act on. */
|
|
57
79
|
export function isRecoverableImageError(message: string): boolean {
|
|
58
80
|
return (
|
|
59
81
|
isImageDimensionsTooLargeError(message) ||
|
|
60
82
|
isImageUnprocessableError(message) ||
|
|
61
|
-
isImageMediaTypeMismatchError(message)
|
|
83
|
+
isImageMediaTypeMismatchError(message) ||
|
|
84
|
+
isImageUnsupportedFormatError(message)
|
|
62
85
|
);
|
|
63
86
|
}
|
|
@@ -29,6 +29,7 @@ const DEFAULTS_DIR = resolveBundledDir(import.meta.dir, ".", "default-plugins");
|
|
|
29
29
|
const DEFAULT_PLUGIN_NAMESPACE_PREFIX = "default-";
|
|
30
30
|
|
|
31
31
|
let cachedNames: readonly string[] | null = null;
|
|
32
|
+
let cachedNameSet: ReadonlySet<string> | null = null;
|
|
32
33
|
let cachedDirToManifest: ReadonlyMap<string, string> | null = null;
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -58,10 +59,24 @@ export function getAllDefaultPluginNames(): readonly string[] {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
cachedNames = names.sort();
|
|
62
|
+
cachedNameSet = new Set(cachedNames);
|
|
61
63
|
}
|
|
62
64
|
return cachedNames;
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Whether `name` is one of the first-party default plugins that ship in
|
|
69
|
+
* `plugins/defaults/` (e.g. `default-memory`). Used by hook collection so
|
|
70
|
+
* those plugins' hooks are not dropped by a per-chat allowlist that only
|
|
71
|
+
* names user-installed plugins.
|
|
72
|
+
*/
|
|
73
|
+
export function isFirstPartyDefaultPlugin(name: string): boolean {
|
|
74
|
+
if (cachedNameSet === null) {
|
|
75
|
+
getAllDefaultPluginNames();
|
|
76
|
+
}
|
|
77
|
+
return cachedNameSet !== null && cachedNameSet.has(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
/**
|
|
66
81
|
* Manifest name (e.g. `default-platform-hosted`) for a default plugin's
|
|
67
82
|
* *directory* name (e.g. `platform-hosted`), or `null` when `<dirName>` is not
|
|
@@ -38,11 +38,16 @@ them. Everything not listed as a tier directory is spine.
|
|
|
38
38
|
|
|
39
39
|
| Directory | What lives there | Gate predicate |
|
|
40
40
|
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
41
|
-
| `substrate/` | The shared concept-page substrate (v2 + v3): `page-store`, `page-index`, `edge-index`, `qdrant`, `sim`, `sparse-bm25`, `spread`, `skill-store`, `cli-command-store`, `skill-content`, `cli-command-content`, `injected-block-slugs`, `static-context`, `boot-maintenance`, `frontmatter-sweep`, `consolidation-lock`, `consolidation-job`, `ingest`, `sweep-job`, `reembed-job`, `tuning`, `types`, `
|
|
41
|
+
| `substrate/` | The shared concept-page substrate (v2 + v3): `page-store`, `page-index`, `edge-index`, `qdrant`, `sim`, `sparse-bm25`, `spread`, `skill-store`, `cli-command-store`, `skill-content`, `cli-command-content`, `injected-block-slugs`, `static-context`, `boot-maintenance`, `frontmatter-sweep`, `consolidation-lock`, `consolidation-job`, `ingest`, `sweep-job`, `reembed-job`, `tuning`, `types`, `page-links`, `prompts/`. | `usesConceptPageMemory()` |
|
|
42
42
|
| `v1/` | The legacy PKB/graph engine: `pkb/`, `graph/` (retriever, triggers, scoring, serendipity, extraction, extraction-job, decay, consolidation, pattern-scan, narrative, bootstrap, injection, and the hybrid node-search half of `graph-search`), `jobs/embed-pkb-file.ts`, `job-handlers/{backfill,embedding,index-maintenance}.ts`, `pkb-schedule.ts`, `filing-jobs.ts`, `semantic-search.ts`, `identity-context.ts`, `README.md`. | `isMemoryV1Active()` |
|
|
43
43
|
| `v2/` | The v2 activation/router injection engine: `activation`, `activation-store`, `activation-log-store`, `concept-frequency`, `injection`, `injection-events`, `router`, `reranker`, `rerank-local`, `migration`, `now-text`, `backfill-jobs` (migrate + activation-recompute), `harness/`, `prompts/router`. | `isV2InjectionEngineActive()` |
|
|
44
44
|
| `v3/`, `v3-eval/` | The v3 lanes, orchestrator, injectors, maintain job, and the eval siblings. | `isMemoryV3Live()` |
|
|
45
45
|
|
|
46
|
+
`substrate/page-links` is the one owner of `links:`-entry and `[[wikilink]]`
|
|
47
|
+
parsing; every reader in any tier imports it (the same single-matcher rule
|
|
48
|
+
`buffer-format` follows for buffer entries, below). Do not add a second
|
|
49
|
+
wikilink matcher or `links:` splitter anywhere.
|
|
50
|
+
|
|
46
51
|
Everything else under the plugin root is **spine**:
|
|
47
52
|
|
|
48
53
|
- `graph/` — the **all-tier** legacy-graph store plus the tier dispatcher:
|
|
@@ -708,8 +713,11 @@ not deleted**. Today the test-only hits are:
|
|
|
708
713
|
client-coordinated change.
|
|
709
714
|
- `cli/commands/memory/memory-v2.ts` — follows its routes rather than being
|
|
710
715
|
deleted outright. `memory v2 reembed` (which posts the surviving `reembed`
|
|
711
|
-
backfill op)
|
|
712
|
-
|
|
716
|
+
backfill op) and `memory v2 reembed-skills` keep working and move with
|
|
717
|
+
the substrate routes; `memory v2 validate` and the top-level
|
|
718
|
+
`memory validate` (`cli/commands/memory/memory-validate.ts`) share
|
|
719
|
+
`runMemoryValidate`, which lives in `memory-v2.ts` and moves with the
|
|
720
|
+
survivors; `memory v2 activation` (the
|
|
713
721
|
`activation-recompute` op), `memory v2 ema`, `memory v2 simulate`, and
|
|
714
722
|
`memory v2 compare` go with the engine. There is no `migrate` subcommand —
|
|
715
723
|
the route is that job's only enqueue path.
|
|
@@ -151,7 +151,6 @@ const HOST_TIER_IMPORT_ALLOWLIST: readonly HostTierImportExemption[] = [
|
|
|
151
151
|
{ path: "src/daemon/embedding-reconcile.ts", tiers: ["substrate", "v3"] },
|
|
152
152
|
{ path: "src/daemon/skill-memory-refresh.ts", tiers: ["substrate"] },
|
|
153
153
|
{ path: "src/daemon/tool-side-effects.ts", tiers: ["substrate"] },
|
|
154
|
-
{ path: "src/daemon/trust-context.ts", tiers: ["substrate"] },
|
|
155
154
|
{ path: "src/persistence/steps.ts", tiers: ["v1"] },
|
|
156
155
|
{ path: "src/runtime/routes/consolidation-routes.ts", tiers: ["substrate"] },
|
|
157
156
|
{
|
|
@@ -22,6 +22,11 @@ import {
|
|
|
22
22
|
formatRememberEntry,
|
|
23
23
|
matchBufferEntryStart,
|
|
24
24
|
} from "../buffer-format.js";
|
|
25
|
+
import {
|
|
26
|
+
extractWikilinkTargets,
|
|
27
|
+
WIKILINK_REGEX,
|
|
28
|
+
wikilinkTarget,
|
|
29
|
+
} from "../substrate/page-links.js";
|
|
25
30
|
import type { MemoryGraphEdge, MemoryGraphNode } from "./types.js";
|
|
26
31
|
|
|
27
32
|
/** Node id prefix marking a pending buffer entry (id = prefix + text hash). */
|
|
@@ -43,9 +48,6 @@ const PENDING_LABEL_MAX_CHARS = 60;
|
|
|
43
48
|
* timestamped entry seen yet still counts as an entry of its own. */
|
|
44
49
|
const PLAIN_BULLET_REGEX = /^-\s+(.+)$/;
|
|
45
50
|
|
|
46
|
-
/** Matches `[[slug]]` / `[[slug|label]]` wikilinks inside an entry. */
|
|
47
|
-
const WIKILINK_REGEX = /\[\[([^[\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
48
|
-
|
|
49
51
|
export interface PendingBufferEntry {
|
|
50
52
|
/** Stable node id (`buffer:` + content hash, deduped within one parse). */
|
|
51
53
|
id: string;
|
|
@@ -116,13 +118,7 @@ export function parseBufferEntries(content: string): PendingBufferEntry[] {
|
|
|
116
118
|
flush();
|
|
117
119
|
|
|
118
120
|
for (const text of texts) {
|
|
119
|
-
const slugs
|
|
120
|
-
for (const link of text.matchAll(WIKILINK_REGEX)) {
|
|
121
|
-
const slug = link[1]!.trim();
|
|
122
|
-
if (slug && !slugs.includes(slug)) {
|
|
123
|
-
slugs.push(slug);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
121
|
+
const slugs = [...new Set(extractWikilinkTargets(text))];
|
|
126
122
|
|
|
127
123
|
const hash = hashText(text);
|
|
128
124
|
const count = (seen.get(hash) ?? 0) + 1;
|
|
@@ -144,8 +140,9 @@ export function parseBufferEntries(content: string): PendingBufferEntry[] {
|
|
|
144
140
|
function pendingLabel(text: string): string {
|
|
145
141
|
const firstLine = text.split("\n", 1)[0] ?? text;
|
|
146
142
|
const plain = firstLine
|
|
147
|
-
.replace(WIKILINK_REGEX, (_match,
|
|
148
|
-
|
|
143
|
+
.replace(WIKILINK_REGEX, (_match, inner: string) => {
|
|
144
|
+
const slug = wikilinkTarget(inner);
|
|
145
|
+
return slug.split("/").pop() || slug;
|
|
149
146
|
})
|
|
150
147
|
.trim();
|
|
151
148
|
if (plain.length <= PENDING_LABEL_MAX_CHARS) {
|
|
@@ -223,7 +223,7 @@ If your review window contains a PROCEDURE you actually carried out — a sequen
|
|
|
223
223
|
|
|
224
224
|
When you do capture a procedure:
|
|
225
225
|
|
|
226
|
-
1. Deduplicate against existing skills first. Call \`find_similar_skills\` with
|
|
226
|
+
1. Deduplicate against existing skills first. Call \`find_similar_skills\` with the procedure's goal as its \`goal\` argument. Each hit carries a \`source\` (bundled, managed, plugin, workspace, or extra), and a managed hit also carries \`author\` (\`"assistant"\` if you authored it, \`"user"\` if a person did, omitted if untagged). You may only overwrite or refine a skill YOU authored — a hit with \`source: "managed"\` AND \`author: "assistant"\`. ANY other hit means the procedure is ALREADY COVERED: a non-managed source (bundled, plugin, workspace, or extra), OR a managed skill that is NOT \`author: "assistant"\` (a person wrote it, or it is untagged). For an ALREADY COVERED hit do not \`overwrite\` it, do not shadow it by creating a skill with its \`skill_id\`, and do not create a near-duplicate — skip it. Only when a returned skill is one of your own (\`source: "managed"\`, \`author: "assistant"\`) and is the SAME procedure, UPDATE it: call \`scaffold_managed_skill\` with that \`skill_id\` and \`overwrite: true\`, rewriting \`body_markdown\` from what you actually observed in the trace. Only CREATE a new skill (fresh \`skill_id\`) when no existing skill of any source covers the procedure. Bias strongly toward reusing or refining your own skills over spawning near-duplicates.
|
|
227
227
|
|
|
228
228
|
2. Capture procedure-scoped knowledge alongside the body. Failure modes, gotchas, and cached values you observed in the trace (error signatures and how you recovered, preconditions, IDs/paths/endpoints that held steady) belong in companion files passed via \`scaffold_managed_skill\`'s \`files\` input (for example \`references/failure-modes.md\`), and the SKILL.md body should reference them so a future load surfaces them.
|
|
229
229
|
|
|
@@ -22,12 +22,9 @@ import type { RouteDefinition } from "../../../../runtime/routes/types.js";
|
|
|
22
22
|
import type { RouteHandlerArgs } from "../../../../runtime/routes/types.js";
|
|
23
23
|
import { getLogger } from "../logging.js";
|
|
24
24
|
import { getWorkspaceDir } from "../paths.js";
|
|
25
|
-
import {
|
|
26
|
-
getEdgeIndex,
|
|
27
|
-
totalEdgeCount,
|
|
28
|
-
validateEdgeTargets,
|
|
29
|
-
} from "../substrate/edge-index.js";
|
|
25
|
+
import { getEdgeIndex, totalEdgeCount } from "../substrate/edge-index.js";
|
|
30
26
|
import { getPageIndex } from "../substrate/page-index.js";
|
|
27
|
+
import type { DanglingLink } from "../substrate/page-links.js";
|
|
31
28
|
import {
|
|
32
29
|
getConceptsDir,
|
|
33
30
|
listPages,
|
|
@@ -111,14 +108,17 @@ async function handleBackfill({
|
|
|
111
108
|
|
|
112
109
|
const MemoryV2ValidateParams = z.object({}).strict();
|
|
113
110
|
|
|
114
|
-
type MissingEdgeEndpoint = { from: string; to: string };
|
|
115
111
|
type OversizedPage = { slug: string; chars: number };
|
|
116
112
|
type ParseFailure = { slug: string; error: string };
|
|
117
113
|
|
|
118
114
|
export type MemoryV2ValidateResult = {
|
|
119
115
|
pageCount: number;
|
|
120
116
|
edgeCount: number;
|
|
121
|
-
|
|
117
|
+
/**
|
|
118
|
+
* `links:`, `[[wikilink]]`, and `edges:` references whose target has no
|
|
119
|
+
* page (`PageIndex.danglingLinks`).
|
|
120
|
+
*/
|
|
121
|
+
danglingLinks: DanglingLink[];
|
|
122
122
|
oversizedPages: OversizedPage[];
|
|
123
123
|
parseFailures: ParseFailure[];
|
|
124
124
|
};
|
|
@@ -162,13 +162,15 @@ async function handleValidate({
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
const edgeIndex = await
|
|
166
|
-
|
|
165
|
+
const [edgeIndex, pageIndex] = await Promise.all([
|
|
166
|
+
getEdgeIndex(workspaceDir),
|
|
167
|
+
getPageIndex(workspaceDir),
|
|
168
|
+
]);
|
|
167
169
|
|
|
168
170
|
return {
|
|
169
171
|
pageCount: knownSlugs.size,
|
|
170
172
|
edgeCount: totalEdgeCount(edgeIndex),
|
|
171
|
-
|
|
173
|
+
danglingLinks: pageIndex.danglingLinks,
|
|
172
174
|
oversizedPages,
|
|
173
175
|
parseFailures,
|
|
174
176
|
};
|
|
@@ -615,6 +615,32 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
615
615
|
}
|
|
616
616
|
});
|
|
617
617
|
|
|
618
|
+
test("threads page-index dangling links into the prompt's repair step", async () => {
|
|
619
|
+
const conceptsDir = join(tmpWorkspace, "memory", "concepts");
|
|
620
|
+
const sourcePath = join(conceptsDir, "atl-1290.md");
|
|
621
|
+
// A `links:` entry naming a page that does not exist (the LUM-3316 shape).
|
|
622
|
+
writeFileSync(
|
|
623
|
+
sourcePath,
|
|
624
|
+
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n\nSee [[atl-1291]].\n",
|
|
625
|
+
);
|
|
626
|
+
invalidatePageIndex();
|
|
627
|
+
try {
|
|
628
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
629
|
+
expect(result.kind).toBe("invoked");
|
|
630
|
+
const prompt = runnerLastArgs?.prompt as string;
|
|
631
|
+
expect(prompt).toContain("resolve dangling links");
|
|
632
|
+
expect(prompt).toContain(
|
|
633
|
+
"- `memory/concepts/atl-1290.md` → `atl-1291` (frontmatter `links:` entry)",
|
|
634
|
+
);
|
|
635
|
+
expect(prompt).toContain(
|
|
636
|
+
"- `memory/concepts/atl-1290.md` → `atl-1291` (inline `[[wikilink]]` in the body)",
|
|
637
|
+
);
|
|
638
|
+
} finally {
|
|
639
|
+
rmSync(sourcePath, { force: true });
|
|
640
|
+
invalidatePageIndex();
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
|
|
618
644
|
test("dispatches with skipPromptIndexing so the kickoff prompt is not indexed", async () => {
|
|
619
645
|
// The prompt is a static instruction manual — indexing it would write
|
|
620
646
|
// near-identical memory segments, embeddings, and a lexical entry on
|
|
@@ -880,6 +906,94 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
880
906
|
expect(enqueuedJobs).toHaveLength(0);
|
|
881
907
|
});
|
|
882
908
|
|
|
909
|
+
test("a run that leaves dangling links reports the count and still enqueues follow-ups", async () => {
|
|
910
|
+
const conceptsDir = join(tmpWorkspace, "memory", "concepts");
|
|
911
|
+
runnerImpl = async () => {
|
|
912
|
+
// The agent files the buffer into a page whose links: names a page it
|
|
913
|
+
// never spawned. Writing through the file tools would invalidate the
|
|
914
|
+
// index via the tool hook; the stub does so by hand.
|
|
915
|
+
writeFileSync(
|
|
916
|
+
join(conceptsDir, "atl-1290.md"),
|
|
917
|
+
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
918
|
+
);
|
|
919
|
+
writeFileSync(bufferPath(), "");
|
|
920
|
+
invalidatePageIndex();
|
|
921
|
+
return { conversationId: "conv-1", ok: true };
|
|
922
|
+
};
|
|
923
|
+
try {
|
|
924
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
925
|
+
|
|
926
|
+
expect(result.kind).toBe("invoked");
|
|
927
|
+
if (result.kind !== "invoked") {
|
|
928
|
+
throw new Error("unreachable");
|
|
929
|
+
}
|
|
930
|
+
expect(result.noProgress).toBe(false);
|
|
931
|
+
expect(result.danglingLinks).toBe(1);
|
|
932
|
+
// Surfaced, not gating: the page that was written still gets indexed.
|
|
933
|
+
expect(result.followUpJobIds).toEqual(["job-1"]);
|
|
934
|
+
expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
|
|
935
|
+
} finally {
|
|
936
|
+
rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
|
|
937
|
+
invalidatePageIndex();
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test("a run that spawns the missing target clears the pre-run dangling link", async () => {
|
|
942
|
+
const conceptsDir = join(tmpWorkspace, "memory", "concepts");
|
|
943
|
+
writeFileSync(
|
|
944
|
+
join(conceptsDir, "atl-1290.md"),
|
|
945
|
+
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
946
|
+
);
|
|
947
|
+
invalidatePageIndex();
|
|
948
|
+
runnerImpl = async () => {
|
|
949
|
+
writeFileSync(join(conceptsDir, "atl-1291.md"), "# ATL-1291\n");
|
|
950
|
+
writeFileSync(bufferPath(), "");
|
|
951
|
+
invalidatePageIndex();
|
|
952
|
+
return { conversationId: "conv-1", ok: true };
|
|
953
|
+
};
|
|
954
|
+
try {
|
|
955
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
956
|
+
|
|
957
|
+
expect(result.kind).toBe("invoked");
|
|
958
|
+
if (result.kind !== "invoked") {
|
|
959
|
+
throw new Error("unreachable");
|
|
960
|
+
}
|
|
961
|
+
// The pre-run index fed the repair step...
|
|
962
|
+
expect(runnerLastArgs?.prompt as string).toContain(
|
|
963
|
+
"resolve dangling links",
|
|
964
|
+
);
|
|
965
|
+
// ...and the post-run count reflects the repair.
|
|
966
|
+
expect(result.danglingLinks).toBe(0);
|
|
967
|
+
} finally {
|
|
968
|
+
rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
|
|
969
|
+
rmSync(join(conceptsDir, "atl-1291.md"), { force: true });
|
|
970
|
+
invalidatePageIndex();
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
test("a no-progress run still reports its dangling-link count", async () => {
|
|
975
|
+
const conceptsDir = join(tmpWorkspace, "memory", "concepts");
|
|
976
|
+
writeFileSync(
|
|
977
|
+
join(conceptsDir, "atl-1290.md"),
|
|
978
|
+
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
979
|
+
);
|
|
980
|
+
invalidatePageIndex();
|
|
981
|
+
runnerImpl = async () => ({ conversationId: "conv-1", ok: true });
|
|
982
|
+
try {
|
|
983
|
+
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
984
|
+
|
|
985
|
+
expect(result.kind).toBe("invoked");
|
|
986
|
+
if (result.kind !== "invoked") {
|
|
987
|
+
throw new Error("unreachable");
|
|
988
|
+
}
|
|
989
|
+
expect(result.noProgress).toBe(true);
|
|
990
|
+
expect(result.danglingLinks).toBe(1);
|
|
991
|
+
} finally {
|
|
992
|
+
rmSync(join(conceptsDir, "atl-1290.md"), { force: true });
|
|
993
|
+
invalidatePageIndex();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
|
|
883
997
|
test("a pending reembed suppresses its duplicate; v3 maintain still enqueues", async () => {
|
|
884
998
|
v3FlagOn = true;
|
|
885
999
|
pendingJobTypes.add("memory_v2_reembed");
|