@vellumai/assistant 0.11.4 → 0.11.5-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +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__/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__/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/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/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/feature-flag-registry.json +35 -19
- 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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
CONSOLIDATION_PROMPT_V3,
|
|
27
27
|
CORE_PAGES_CONSOLIDATION_SECTION,
|
|
28
28
|
renderConsolidationPrompt,
|
|
29
|
+
renderDanglingLinksSection,
|
|
29
30
|
renderParseFailuresSection,
|
|
30
31
|
} from "../prompts/consolidation.js";
|
|
31
32
|
|
|
@@ -59,6 +60,13 @@ const GATED_SECTIONS: Array<{
|
|
|
59
60
|
]),
|
|
60
61
|
marker: "repair unreadable pages",
|
|
61
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: "dangling-link repair step (gate: index-reported dangling links)",
|
|
65
|
+
section: renderDanglingLinksSection([
|
|
66
|
+
{ from: "example-page", to: "example-target", kind: "links" },
|
|
67
|
+
]),
|
|
68
|
+
marker: "resolve dangling links",
|
|
69
|
+
},
|
|
62
70
|
];
|
|
63
71
|
|
|
64
72
|
/** All `ConsolidationPromptOptions` gates forced off — the v2-only rendering. */
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
getReachable,
|
|
16
16
|
invalidateEdgeIndex,
|
|
17
17
|
totalEdgeCount,
|
|
18
|
-
validateEdgeTargets,
|
|
19
18
|
} from "../edge-index.js";
|
|
20
19
|
import { deletePage, writePage } from "../page-store.js";
|
|
21
20
|
import type { ConceptPage } from "../types.js";
|
|
@@ -228,35 +227,6 @@ describe("getReachable", () => {
|
|
|
228
227
|
});
|
|
229
228
|
});
|
|
230
229
|
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
// validateEdgeTargets
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
describe("validateEdgeTargets", () => {
|
|
236
|
-
test("ok=true when every outgoing target has a known slug", async () => {
|
|
237
|
-
await writePage(workspaceDir, makePage("alice", ["bob"]));
|
|
238
|
-
await writePage(workspaceDir, makePage("bob"));
|
|
239
|
-
|
|
240
|
-
const idx = await getEdgeIndex(workspaceDir);
|
|
241
|
-
const result = validateEdgeTargets(idx, new Set(["alice", "bob"]));
|
|
242
|
-
expect(result).toEqual({ ok: true, missing: [] });
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("flags outgoing targets that don't correspond to a known slug", async () => {
|
|
246
|
-
await writePage(workspaceDir, makePage("alice", ["ghost", "bob"]));
|
|
247
|
-
await writePage(workspaceDir, makePage("bob", ["phantom"]));
|
|
248
|
-
|
|
249
|
-
const idx = await getEdgeIndex(workspaceDir);
|
|
250
|
-
const result = validateEdgeTargets(idx, new Set(["alice", "bob"]));
|
|
251
|
-
|
|
252
|
-
expect(result.ok).toBe(false);
|
|
253
|
-
expect(result.missing).toEqual([
|
|
254
|
-
{ from: "alice", to: "ghost" },
|
|
255
|
-
{ from: "bob", to: "phantom" },
|
|
256
|
-
]);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
230
|
// ---------------------------------------------------------------------------
|
|
261
231
|
// totalEdgeCount
|
|
262
232
|
// ---------------------------------------------------------------------------
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* - Batch over the cap: RangeError.
|
|
13
13
|
* - In-batch duplicate slug: later occurrence `invalid`.
|
|
14
14
|
* - Non-blocking warnings: missing `source`, unparseable `origin_date`,
|
|
15
|
-
* frontmatter `slug` differing from the storage slug
|
|
15
|
+
* frontmatter `slug` differing from the storage slug, link targets
|
|
16
|
+
* neither on disk nor in the batch.
|
|
16
17
|
* - Under-lock collision snapshot: a page committed between validation and
|
|
17
18
|
* the write phase is skipped, and `listPages` runs after `tryAcquireLock`.
|
|
18
19
|
* - Partial write failure: the reindex fan-out still runs for committed
|
|
@@ -363,6 +364,77 @@ describe("ingestPages", () => {
|
|
|
363
364
|
expect(await listPages(workspace)).toEqual(["people/alice", "people/bob"]);
|
|
364
365
|
});
|
|
365
366
|
|
|
367
|
+
test("link targets neither on disk nor in the batch warn without blocking the write", async () => {
|
|
368
|
+
// `people/bob` already exists on disk; `projects/garden` arrives in the
|
|
369
|
+
// same batch; `atl-1291` and `ghost` exist nowhere.
|
|
370
|
+
await ingestPages(workspace, [
|
|
371
|
+
{ slug: "people/bob", content: validPage("Bob.") },
|
|
372
|
+
]);
|
|
373
|
+
enqueuedJobs.length = 0;
|
|
374
|
+
|
|
375
|
+
const summary = await ingestPages(workspace, [
|
|
376
|
+
{
|
|
377
|
+
slug: "people/alice",
|
|
378
|
+
content:
|
|
379
|
+
"---\nsource: import:chatgpt\norigin_date: 2025-03-14\n" +
|
|
380
|
+
"links:\n - people/bob\n - projects/garden\n - atl-1291\n" +
|
|
381
|
+
"edges: [ghost]\n---\n" +
|
|
382
|
+
"Alice mentions [[projects/garden]] and [[people/bob]] and [[skills/deploy]].\n",
|
|
383
|
+
},
|
|
384
|
+
{ slug: "projects/garden", content: validPage("Garden.") },
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
expect(summary.written).toBe(2);
|
|
388
|
+
expect(summary.invalid).toBe(0);
|
|
389
|
+
const [alice, garden] = summary.results;
|
|
390
|
+
expect(alice.action).toBe("written");
|
|
391
|
+
expect(alice.warnings).toHaveLength(1);
|
|
392
|
+
// Every unresolved target on one line, sorted; targets that resolve on
|
|
393
|
+
// disk, in the batch, or under a synthetic prefix stay silent.
|
|
394
|
+
expect(alice.warnings[0]).toContain(
|
|
395
|
+
"link targets not on disk or in this batch: atl-1291, ghost",
|
|
396
|
+
);
|
|
397
|
+
expect(alice.warnings[0]).not.toContain("people/bob");
|
|
398
|
+
expect(alice.warnings[0]).not.toContain("projects/garden");
|
|
399
|
+
expect(alice.warnings[0]).not.toContain("skills/deploy");
|
|
400
|
+
expect(garden.warnings).toEqual([]);
|
|
401
|
+
// Written regardless, and reindexed.
|
|
402
|
+
expect(await readPage(workspace, "people/alice")).not.toBeNull();
|
|
403
|
+
expect(enqueuedJobs.map((j) => j.type).sort()).toEqual([
|
|
404
|
+
"memory_v2_reembed",
|
|
405
|
+
"memory_v3_maintain",
|
|
406
|
+
]);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("dangling-link warnings are reported on dry-run and skipped for pages the batch will not write", async () => {
|
|
410
|
+
await ingestPages(workspace, [
|
|
411
|
+
{ slug: "people/bob", content: validPage("Bob.") },
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
const summary = await ingestPages(
|
|
415
|
+
workspace,
|
|
416
|
+
[
|
|
417
|
+
{
|
|
418
|
+
slug: "people/alice",
|
|
419
|
+
content: validPage("Alice mentions [[nowhere]]."),
|
|
420
|
+
},
|
|
421
|
+
// Exists on disk and overwrite is off, so this content is never
|
|
422
|
+
// written; its dangling link is not this batch's business.
|
|
423
|
+
{
|
|
424
|
+
slug: "people/bob",
|
|
425
|
+
content: validPage("Bob mentions [[also-nowhere]]."),
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
{ dryRun: true },
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(summary.results[0].action).toBe("written");
|
|
432
|
+
expect(summary.results[0].warnings.join(" ")).toContain("nowhere");
|
|
433
|
+
expect(summary.results[1].action).toBe("skipped_exists");
|
|
434
|
+
expect(summary.results[1].warnings).toEqual([]);
|
|
435
|
+
expect(await readPage(workspace, "people/alice")).toBeNull();
|
|
436
|
+
});
|
|
437
|
+
|
|
366
438
|
test("a pending reindex job of the same type suppresses its duplicate enqueue", async () => {
|
|
367
439
|
pendingJobTypes = new Set(["memory_v2_reembed"]);
|
|
368
440
|
await ingestPages(workspace, [
|
|
@@ -56,6 +56,7 @@ const {
|
|
|
56
56
|
} = await import("../page-index.js");
|
|
57
57
|
const { writePage } = await import("../page-store.js");
|
|
58
58
|
const { invalidateEdgeIndex } = await import("../edge-index.js");
|
|
59
|
+
const { LINK_SEPARATOR } = await import("../page-links.js");
|
|
59
60
|
|
|
60
61
|
let workspaceDir: string;
|
|
61
62
|
|
|
@@ -272,6 +273,42 @@ describe("getPageIndex", () => {
|
|
|
272
273
|
const alice = idx.bySlug.get("alice")!;
|
|
273
274
|
const bob = idx.bySlug.get("bob")!;
|
|
274
275
|
expect(alice.edges).toEqual([bob.id]);
|
|
276
|
+
// Dropped from the numeric edges, reported on the index.
|
|
277
|
+
expect(idx.danglingLinks).toEqual([
|
|
278
|
+
{ from: "alice", to: "ghost", kind: "edges" },
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("danglingLinks covers links:, body wikilinks, and edges:, resolved against pages and synthetic rows", async () => {
|
|
283
|
+
skillState.entries = [{ id: "browser", content: "Drive a browser." }];
|
|
284
|
+
const alice = makePage("alice", {
|
|
285
|
+
summary: "A",
|
|
286
|
+
edges: ["bob"],
|
|
287
|
+
body: "Alice uses [[skills/browser]] and [[bob|Bob]] but also [[nobody#sec]].",
|
|
288
|
+
});
|
|
289
|
+
alice.frontmatter.links = [
|
|
290
|
+
`bob${LINK_SEPARATOR}peer`,
|
|
291
|
+
`atl-1291${LINK_SEPARATOR}ticket`,
|
|
292
|
+
];
|
|
293
|
+
await writePage(workspaceDir, alice);
|
|
294
|
+
await writePage(workspaceDir, makePage("bob", { summary: "B" }));
|
|
295
|
+
|
|
296
|
+
const idx = await getPageIndex(workspaceDir);
|
|
297
|
+
expect(idx.danglingLinks).toEqual([
|
|
298
|
+
{ from: "alice", to: "atl-1291", kind: "links" },
|
|
299
|
+
{ from: "alice", to: "nobody", kind: "wikilink" },
|
|
300
|
+
]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("danglingLinks is empty when every reference resolves", async () => {
|
|
304
|
+
await writePage(
|
|
305
|
+
workspaceDir,
|
|
306
|
+
makePage("alice", { summary: "A", edges: ["bob"], body: "See [[bob]]." }),
|
|
307
|
+
);
|
|
308
|
+
await writePage(workspaceDir, makePage("bob", { summary: "B" }));
|
|
309
|
+
|
|
310
|
+
const idx = await getPageIndex(workspaceDir);
|
|
311
|
+
expect(idx.danglingLinks).toEqual([]);
|
|
275
312
|
});
|
|
276
313
|
|
|
277
314
|
test("exposes frontmatter leaves in the index entry", async () => {
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `substrate/page-links.ts`: the single owner of `links:` entry and
|
|
3
|
+
* `[[wikilink]]` parsing, plus the dangling-link finder every write-side
|
|
4
|
+
* report uses.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
extractWikilinkTargets,
|
|
11
|
+
findDanglingLinks,
|
|
12
|
+
LINK_SEPARATOR,
|
|
13
|
+
parseLinkEntry,
|
|
14
|
+
WIKILINK_REGEX,
|
|
15
|
+
wikilinkTarget,
|
|
16
|
+
} from "../page-links.js";
|
|
17
|
+
import { isValidSlug } from "../page-store.js";
|
|
18
|
+
|
|
19
|
+
/** Build a `"<slug> <sep> <why>"` links entry without spelling the separator. */
|
|
20
|
+
const link = (target: string, why?: string): string =>
|
|
21
|
+
why === undefined ? target : `${target}${LINK_SEPARATOR}${why}`;
|
|
22
|
+
|
|
23
|
+
describe("LINK_SEPARATOR", () => {
|
|
24
|
+
test("is space, U+2014 EM DASH, space", () => {
|
|
25
|
+
expect(LINK_SEPARATOR).toHaveLength(3);
|
|
26
|
+
expect(LINK_SEPARATOR.codePointAt(1)).toBe(0x2014);
|
|
27
|
+
expect(LINK_SEPARATOR[0]).toBe(" ");
|
|
28
|
+
expect(LINK_SEPARATOR[2]).toBe(" ");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("parseLinkEntry", () => {
|
|
33
|
+
test("splits target and description on the first separator", () => {
|
|
34
|
+
expect(parseLinkEntry(link("alice", "the principal"))).toEqual({
|
|
35
|
+
target: "alice",
|
|
36
|
+
description: "the principal",
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("keeps later separators inside the description", () => {
|
|
41
|
+
const why = `why${LINK_SEPARATOR}with a second dash`;
|
|
42
|
+
expect(parseLinkEntry(link("alice", why))).toEqual({
|
|
43
|
+
target: "alice",
|
|
44
|
+
description: why,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a bare slug carries no description; whitespace is trimmed", () => {
|
|
49
|
+
expect(parseLinkEntry(" alice ")).toEqual({
|
|
50
|
+
target: "alice",
|
|
51
|
+
description: undefined,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("an empty annotation after the separator reads as no description", () => {
|
|
56
|
+
expect(parseLinkEntry(`alice${LINK_SEPARATOR} `)).toEqual({
|
|
57
|
+
target: "alice",
|
|
58
|
+
description: undefined,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("wikilinkTarget / extractWikilinkTargets", () => {
|
|
64
|
+
test("strips |display and #anchor suffixes and trims", () => {
|
|
65
|
+
expect(wikilinkTarget("alice")).toBe("alice");
|
|
66
|
+
expect(wikilinkTarget("alice|Alice B.")).toBe("alice");
|
|
67
|
+
expect(wikilinkTarget("alice#role")).toBe("alice");
|
|
68
|
+
expect(wikilinkTarget(" tools/vs-code#setup|editor ")).toBe(
|
|
69
|
+
"tools/vs-code",
|
|
70
|
+
);
|
|
71
|
+
expect(wikilinkTarget("|label")).toBe("");
|
|
72
|
+
expect(wikilinkTarget("#anchor")).toBe("");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("extracts every target in order, dropping empty ones, keeping duplicates", () => {
|
|
76
|
+
const body =
|
|
77
|
+
"See [[alice]] and [[bob|Bob]] then [[alice#role]] but not [[|x]] or [[#y]].";
|
|
78
|
+
expect(extractWikilinkTargets(body)).toEqual(["alice", "bob", "alice"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("the regex is global and captures the raw inner text", () => {
|
|
82
|
+
const matches = [..."[[a|A]] [[b#c]]".matchAll(WIKILINK_REGEX)].map(
|
|
83
|
+
(m) => m[1],
|
|
84
|
+
);
|
|
85
|
+
expect(matches).toEqual(["a|A", "b#c"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("findDanglingLinks", () => {
|
|
90
|
+
const known = new Set(["alice", "bob", "hub", "skills/deploy"]);
|
|
91
|
+
|
|
92
|
+
test("reports links:, wikilink, and edges: targets absent from the known set", () => {
|
|
93
|
+
const result = findDanglingLinks(
|
|
94
|
+
[
|
|
95
|
+
{
|
|
96
|
+
slug: "hub",
|
|
97
|
+
frontmatter: {
|
|
98
|
+
links: [link("alice", "principal"), link("atl-1291", "ticket")],
|
|
99
|
+
edges: ["bob", "ghost-edge"],
|
|
100
|
+
},
|
|
101
|
+
body: "Lead mentions [[alice]] and [[ghost-wiki|Ghost]].",
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
known,
|
|
105
|
+
isValidSlug,
|
|
106
|
+
);
|
|
107
|
+
expect(result).toEqual([
|
|
108
|
+
{ from: "hub", to: "atl-1291", kind: "links" },
|
|
109
|
+
{ from: "hub", to: "ghost-edge", kind: "edges" },
|
|
110
|
+
{ from: "hub", to: "ghost-wiki", kind: "wikilink" },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("resolves against synthetic slugs too when the caller counts them as known", () => {
|
|
115
|
+
expect(
|
|
116
|
+
findDanglingLinks(
|
|
117
|
+
[
|
|
118
|
+
{
|
|
119
|
+
slug: "alice",
|
|
120
|
+
frontmatter: { links: [link("skills/deploy")] },
|
|
121
|
+
body: "",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
known,
|
|
125
|
+
isValidSlug,
|
|
126
|
+
),
|
|
127
|
+
).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("ignores self-references and empty targets, dedupes per (from, to, kind)", () => {
|
|
131
|
+
const result = findDanglingLinks(
|
|
132
|
+
[
|
|
133
|
+
{
|
|
134
|
+
slug: "alice",
|
|
135
|
+
frontmatter: {
|
|
136
|
+
links: [
|
|
137
|
+
link("alice", "self"),
|
|
138
|
+
link("ghost"),
|
|
139
|
+
link("ghost", "twice"),
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
body: "[[alice]] [[ghost]] [[ghost#sec]] [[|nothing]]",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
known,
|
|
146
|
+
isValidSlug,
|
|
147
|
+
);
|
|
148
|
+
expect(result).toEqual([
|
|
149
|
+
{ from: "alice", to: "ghost", kind: "links" },
|
|
150
|
+
{ from: "alice", to: "ghost", kind: "wikilink" },
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("output is sorted by (from, to, kind) regardless of page order", () => {
|
|
155
|
+
const pages = [
|
|
156
|
+
{ slug: "zed", frontmatter: { links: [link("nope")] }, body: "[[nope]]" },
|
|
157
|
+
{ slug: "amy", frontmatter: {}, body: "[[zzz]] [[aaa]]" },
|
|
158
|
+
];
|
|
159
|
+
const forward = findDanglingLinks(pages, known, isValidSlug);
|
|
160
|
+
const reversed = findDanglingLinks(
|
|
161
|
+
[...pages].reverse(),
|
|
162
|
+
known,
|
|
163
|
+
isValidSlug,
|
|
164
|
+
);
|
|
165
|
+
expect(forward).toEqual(reversed);
|
|
166
|
+
expect(forward.map((d) => `${d.from}>${d.to}>${d.kind}`)).toEqual([
|
|
167
|
+
"amy>aaa>wikilink",
|
|
168
|
+
"amy>zzz>wikilink",
|
|
169
|
+
"zed>nope>links",
|
|
170
|
+
"zed>nope>wikilink",
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("targets that are not slug-shaped are not references at all", () => {
|
|
175
|
+
// Shell tests, code, and prose in double brackets must not be reported
|
|
176
|
+
// (and so never reach the repair step that would unwrap them).
|
|
177
|
+
expect(
|
|
178
|
+
findDanglingLinks(
|
|
179
|
+
[
|
|
180
|
+
{
|
|
181
|
+
slug: "procs/deploy",
|
|
182
|
+
frontmatter: { links: ["Not A Slug"] },
|
|
183
|
+
body: "Run `if [[ -f foo ]]; then` and [[ Some Prose ]] then [[real-missing]].",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
known,
|
|
187
|
+
isValidSlug,
|
|
188
|
+
),
|
|
189
|
+
).toEqual([{ from: "procs/deploy", to: "real-missing", kind: "wikilink" }]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a fully resolved corpus yields an empty list", () => {
|
|
193
|
+
expect(
|
|
194
|
+
findDanglingLinks(
|
|
195
|
+
[
|
|
196
|
+
{
|
|
197
|
+
slug: "alice",
|
|
198
|
+
frontmatter: { links: [link("bob", "peer")] },
|
|
199
|
+
body: "[[hub]]",
|
|
200
|
+
},
|
|
201
|
+
{ slug: "bob", frontmatter: { edges: ["alice"] }, body: "" },
|
|
202
|
+
],
|
|
203
|
+
known,
|
|
204
|
+
isValidSlug,
|
|
205
|
+
),
|
|
206
|
+
).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -66,8 +66,10 @@ const {
|
|
|
66
66
|
CORE_PAGES_CONSOLIDATION_SECTION,
|
|
67
67
|
CORE_PAGES_PLACEHOLDER,
|
|
68
68
|
CUTOFF_PLACEHOLDER,
|
|
69
|
+
DANGLING_LINKS_PLACEHOLDER,
|
|
69
70
|
PARSE_FAILURES_PLACEHOLDER,
|
|
70
71
|
renderConsolidationPrompt,
|
|
72
|
+
renderDanglingLinksSection,
|
|
71
73
|
renderParseFailuresSection,
|
|
72
74
|
resolveConsolidationPrompt,
|
|
73
75
|
} = await import("../prompts/consolidation.js");
|
|
@@ -107,7 +109,15 @@ const bundledPrompt = (includeCorePagesSection = false): string =>
|
|
|
107
109
|
? (CORE_PAGES_CONSOLIDATION_SECTION as string)
|
|
108
110
|
: "",
|
|
109
111
|
)
|
|
110
|
-
.replaceAll(PARSE_FAILURES_PLACEHOLDER, "")
|
|
112
|
+
.replaceAll(PARSE_FAILURES_PLACEHOLDER, "")
|
|
113
|
+
.replaceAll(DANGLING_LINKS_PLACEHOLDER, "");
|
|
114
|
+
|
|
115
|
+
/** Sample dangling links for the repair-section tests. */
|
|
116
|
+
const SAMPLE_DANGLING = [
|
|
117
|
+
{ from: "atl-1290", to: "atl-1291", kind: "links" as const },
|
|
118
|
+
{ from: "people/alice", to: "ghost-page", kind: "wikilink" as const },
|
|
119
|
+
{ from: "arcs/day", to: "old-slug", kind: "edges" as const },
|
|
120
|
+
];
|
|
111
121
|
|
|
112
122
|
beforeEach(() => {
|
|
113
123
|
warnCalls.length = 0;
|
|
@@ -316,6 +326,153 @@ describe("resolveConsolidationPrompt — parse-failures repair section", () => {
|
|
|
316
326
|
});
|
|
317
327
|
});
|
|
318
328
|
|
|
329
|
+
describe("resolveConsolidationPrompt: dangling-links repair section", () => {
|
|
330
|
+
test("no dangling links: no section and no placeholder residue, both article shapes", () => {
|
|
331
|
+
expect(renderDanglingLinksSection([])).toBe("");
|
|
332
|
+
for (const articleShape of ["v2", "v3"] as const) {
|
|
333
|
+
const result = renderConsolidationPrompt(CUTOFF, {
|
|
334
|
+
includeCorePagesSection: false,
|
|
335
|
+
articleShape,
|
|
336
|
+
});
|
|
337
|
+
expect(result).not.toContain("resolve dangling links");
|
|
338
|
+
expect(result).not.toContain(DANGLING_LINKS_PLACEHOLDER);
|
|
339
|
+
expect(result).toContain("# The work\n\n## 1. Read the buffer");
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("with dangling links: section renders once, before step 1, one line per reference with its kind", () => {
|
|
344
|
+
const result = renderConsolidationPrompt(CUTOFF, {
|
|
345
|
+
...NO_CORE,
|
|
346
|
+
danglingLinks: SAMPLE_DANGLING,
|
|
347
|
+
});
|
|
348
|
+
expect(result.split("resolve dangling links").length - 1).toBe(1);
|
|
349
|
+
const sectionAt = result.indexOf("## 0. FIRST: resolve dangling links");
|
|
350
|
+
expect(sectionAt).toBeGreaterThan(result.indexOf("# The work"));
|
|
351
|
+
expect(sectionAt).toBeLessThan(result.indexOf("## 1. Read the buffer"));
|
|
352
|
+
expect(result).toContain(
|
|
353
|
+
"- `memory/concepts/atl-1290.md` → `atl-1291` (frontmatter `links:` entry)",
|
|
354
|
+
);
|
|
355
|
+
expect(result).toContain(
|
|
356
|
+
"- `memory/concepts/people/alice.md` → `ghost-page` (inline `[[wikilink]]` in the body)",
|
|
357
|
+
);
|
|
358
|
+
expect(result).toContain(
|
|
359
|
+
"- `memory/concepts/arcs/day.md` → `old-slug` (frontmatter `edges:` entry)",
|
|
360
|
+
);
|
|
361
|
+
// The three resolutions the agent must choose between.
|
|
362
|
+
expect(result).toContain("spawn it, a stub is fine, and keep the link");
|
|
363
|
+
expect(result).toContain("repoint the reference at the surviving slug");
|
|
364
|
+
expect(result).toContain("Prose stays prose");
|
|
365
|
+
expect(result).not.toContain("more, reported next pass");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("renders after the parse-failures section when both are present", () => {
|
|
369
|
+
const result = renderConsolidationPrompt(CUTOFF, {
|
|
370
|
+
...NO_CORE,
|
|
371
|
+
parseFailures: SAMPLE_FAILURES,
|
|
372
|
+
danglingLinks: SAMPLE_DANGLING,
|
|
373
|
+
});
|
|
374
|
+
expect(result.indexOf("repair unreadable pages")).toBeLessThan(
|
|
375
|
+
result.indexOf("resolve dangling links"),
|
|
376
|
+
);
|
|
377
|
+
expect(result.indexOf("resolve dangling links")).toBeLessThan(
|
|
378
|
+
result.indexOf("## 1. Read the buffer"),
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("caps the rendered list at 40 and counts the remainder", () => {
|
|
383
|
+
const many = Array.from({ length: 45 }, (_, i) => ({
|
|
384
|
+
from: `page-${String(i).padStart(2, "0")}`,
|
|
385
|
+
to: `missing-${i}`,
|
|
386
|
+
kind: "links" as const,
|
|
387
|
+
}));
|
|
388
|
+
const section = renderDanglingLinksSection(many);
|
|
389
|
+
const listLines = section
|
|
390
|
+
.split("\n")
|
|
391
|
+
.filter((line) => line.startsWith("- `memory/concepts/"));
|
|
392
|
+
expect(listLines).toHaveLength(40);
|
|
393
|
+
expect(section).toContain("`memory/concepts/page-39.md`");
|
|
394
|
+
expect(section).not.toContain("`memory/concepts/page-40.md`");
|
|
395
|
+
expect(section).toContain("...and 5 more, reported next pass");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("sanitizes injection-shaped slugs and targets: newlines, backticks, oversize", () => {
|
|
399
|
+
// Both ends of a dangling link are page content: the source is a
|
|
400
|
+
// filename, the target is whatever the page wrote inside `[[...]]` or
|
|
401
|
+
// `links:`. Raw, either could break out of the list item.
|
|
402
|
+
const section = renderDanglingLinksSection([
|
|
403
|
+
{
|
|
404
|
+
from: "evil\n\n## Injected heading\n- `nested`",
|
|
405
|
+
to: "target`code`\n## also injected",
|
|
406
|
+
kind: "wikilink",
|
|
407
|
+
},
|
|
408
|
+
{ from: "s".repeat(500), to: "t".repeat(500), kind: "links" },
|
|
409
|
+
]);
|
|
410
|
+
const listLines = section
|
|
411
|
+
.split("\n")
|
|
412
|
+
.filter((line) => line.startsWith("- `memory/concepts/"));
|
|
413
|
+
expect(listLines).toHaveLength(2);
|
|
414
|
+
for (const line of listLines) {
|
|
415
|
+
// Source path, target, and the kind label's own inline-code span:
|
|
416
|
+
// six template backticks, no injected ones.
|
|
417
|
+
expect((line.match(/`/g) ?? []).length).toBe(6);
|
|
418
|
+
}
|
|
419
|
+
const headingLines = section
|
|
420
|
+
.split("\n")
|
|
421
|
+
.filter((line) => line.startsWith("## "));
|
|
422
|
+
expect(headingLines).toHaveLength(1);
|
|
423
|
+
expect(section).toContain("memory/concepts/evil ");
|
|
424
|
+
expect(section).toContain(`${"s".repeat(199)}…`);
|
|
425
|
+
expect(section).toContain(`${"t".repeat(199)}…`);
|
|
426
|
+
expect(section).not.toContain("s".repeat(200));
|
|
427
|
+
expect(section).not.toContain("t".repeat(200));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("override containing the placeholder: substituted in place", () => {
|
|
431
|
+
const path = join(tmpWorkspace, "custom-prompt.md");
|
|
432
|
+
writeFileSync(path, "Top\n{{DANGLING_LINKS_SECTION}}Bottom\n");
|
|
433
|
+
|
|
434
|
+
const result = resolveConsolidationPrompt(path, CUTOFF, {
|
|
435
|
+
...NO_CORE,
|
|
436
|
+
danglingLinks: SAMPLE_DANGLING,
|
|
437
|
+
});
|
|
438
|
+
expect(result).toContain("## 0. FIRST: resolve dangling links");
|
|
439
|
+
expect(result.indexOf("resolve dangling links")).toBeLessThan(
|
|
440
|
+
result.indexOf("Bottom"),
|
|
441
|
+
);
|
|
442
|
+
expect(result).not.toContain(DANGLING_LINKS_PLACEHOLDER);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test("override without either placeholder: both non-empty repair sections are appended, in order", () => {
|
|
446
|
+
const path = join(tmpWorkspace, "no-placeholder.md");
|
|
447
|
+
writeFileSync(path, "My custom prompt {{CUTOFF}}\n");
|
|
448
|
+
|
|
449
|
+
const result = resolveConsolidationPrompt(path, CUTOFF, {
|
|
450
|
+
...NO_CORE,
|
|
451
|
+
parseFailures: SAMPLE_FAILURES,
|
|
452
|
+
danglingLinks: SAMPLE_DANGLING,
|
|
453
|
+
});
|
|
454
|
+
expect(result.startsWith(`My custom prompt ${CUTOFF}`)).toBe(true);
|
|
455
|
+
const parseAt = result.indexOf("repair unreadable pages");
|
|
456
|
+
const danglingAt = result.indexOf("resolve dangling links");
|
|
457
|
+
expect(parseAt).toBeGreaterThan(0);
|
|
458
|
+
expect(danglingAt).toBeGreaterThan(parseAt);
|
|
459
|
+
expect(result).toContain("`memory/concepts/atl-1290.md`");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("override with only the parse placeholder + dangling links: dangling section appended", () => {
|
|
463
|
+
const path = join(tmpWorkspace, "custom-prompt.md");
|
|
464
|
+
writeFileSync(path, "Top\n{{PARSE_FAILURES_SECTION}}Bottom\n");
|
|
465
|
+
|
|
466
|
+
const result = resolveConsolidationPrompt(path, CUTOFF, {
|
|
467
|
+
...NO_CORE,
|
|
468
|
+
danglingLinks: SAMPLE_DANGLING,
|
|
469
|
+
});
|
|
470
|
+
expect(result.indexOf("Bottom")).toBeLessThan(
|
|
471
|
+
result.indexOf("resolve dangling links"),
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
319
476
|
describe("resolveConsolidationPrompt — with override", () => {
|
|
320
477
|
test("loads an absolute path verbatim and substitutes {{CUTOFF}}", () => {
|
|
321
478
|
const path = join(tmpWorkspace, "custom-prompt.md");
|
|
@@ -50,8 +50,7 @@ mock.module("../../config.js", () => ({
|
|
|
50
50
|
}),
|
|
51
51
|
}));
|
|
52
52
|
|
|
53
|
-
const { readMemoryV2StaticContent
|
|
54
|
-
await import("../static-context.js");
|
|
53
|
+
const { readMemoryV2StaticContent } = await import("../static-context.js");
|
|
55
54
|
|
|
56
55
|
const MEMORY_FILES = [
|
|
57
56
|
"essentials.md",
|
|
@@ -361,62 +360,3 @@ describe("readMemoryV2StaticContent Buffer cap", () => {
|
|
|
361
360
|
expect(lines.some((line) => line.endsWith("entry-0"))).toBe(false);
|
|
362
361
|
});
|
|
363
362
|
});
|
|
364
|
-
|
|
365
|
-
describe("shouldExposePersonalMemory", () => {
|
|
366
|
-
test("allows guardian-trusted local conversations", () => {
|
|
367
|
-
expect(
|
|
368
|
-
shouldExposePersonalMemory({
|
|
369
|
-
sourceChannel: "vellum",
|
|
370
|
-
isTrustedActor: true,
|
|
371
|
-
}),
|
|
372
|
-
).toBe(true);
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("allows local-channel conversations even when trust class is unknown (analyze runs, dev)", () => {
|
|
376
|
-
expect(
|
|
377
|
-
shouldExposePersonalMemory({
|
|
378
|
-
sourceChannel: "vellum",
|
|
379
|
-
isTrustedActor: false,
|
|
380
|
-
}),
|
|
381
|
-
).toBe(true);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
test("allows turns with no trust context (work-item task runs, internal background)", () => {
|
|
385
|
-
expect(
|
|
386
|
-
shouldExposePersonalMemory({
|
|
387
|
-
sourceChannel: undefined,
|
|
388
|
-
isTrustedActor: false,
|
|
389
|
-
}),
|
|
390
|
-
).toBe(true);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const REMOTE_CHANNELS = [
|
|
394
|
-
"phone",
|
|
395
|
-
"slack",
|
|
396
|
-
"telegram",
|
|
397
|
-
"whatsapp",
|
|
398
|
-
"email",
|
|
399
|
-
] as const;
|
|
400
|
-
|
|
401
|
-
test("allows guardian-trusted remote channels (user's own phone/Slack)", () => {
|
|
402
|
-
for (const channel of REMOTE_CHANNELS) {
|
|
403
|
-
expect(
|
|
404
|
-
shouldExposePersonalMemory({
|
|
405
|
-
sourceChannel: channel,
|
|
406
|
-
isTrustedActor: true,
|
|
407
|
-
}),
|
|
408
|
-
).toBe(true);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("blocks non-guardian remote-channel actors (the leak this gate exists to prevent)", () => {
|
|
413
|
-
for (const channel of REMOTE_CHANNELS) {
|
|
414
|
-
expect(
|
|
415
|
-
shouldExposePersonalMemory({
|
|
416
|
-
sourceChannel: channel,
|
|
417
|
-
isTrustedActor: false,
|
|
418
|
-
}),
|
|
419
|
-
).toBe(false);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
});
|