@vellumai/assistant 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Memory v2 — Qdrant collection for skill autoinjection
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
//
|
|
5
|
-
// Owns a dedicated Qdrant collection, keyed by skill `id`, that holds dense +
|
|
6
|
-
// sparse embeddings of every enabled skill's `buildSkillContent` snippet. The
|
|
7
|
-
// collection is separate from `memory_v2_concept_pages` because skills are
|
|
8
|
-
// stateless (no `everInjected` dedup, no on-disk concept pages, no edges) and
|
|
9
|
-
// re-presented every turn — keeping them in their own collection means skill
|
|
10
|
-
// pruning and reseeding never touches concept-page state.
|
|
11
|
-
//
|
|
12
|
-
// Mirrors the dense + sparse named-vectors layout used by
|
|
13
|
-
// `ensureConceptPageCollection` in `qdrant.ts`. Connection settings — URL,
|
|
14
|
-
// vector size, on-disk storage — flow through the same env → config
|
|
15
|
-
// precedence as v1 via `resolveQdrantUrl` and `config.memory.qdrant.*`.
|
|
16
|
-
//
|
|
17
|
-
// Per-channel queries: `hybridQuerySkills` runs separate dense and sparse
|
|
18
|
-
// queries (no Qdrant-side RRF). Callers do their own weighted-sum fusion using
|
|
19
|
-
// `dense_weight` / `sparse_weight` from `config.memory.v2`, which RRF fusion
|
|
20
|
-
// would discard.
|
|
21
|
-
|
|
22
|
-
import { QdrantClient as QdrantRestClient } from "@qdrant/js-client-rest";
|
|
23
|
-
import { v5 as uuidv5 } from "uuid";
|
|
24
|
-
|
|
25
|
-
import { getConfig } from "../../config/loader.js";
|
|
26
|
-
import { getLogger } from "../../util/logger.js";
|
|
27
|
-
import type { SparseEmbedding } from "../embedding-types.js";
|
|
28
|
-
import { resolveQdrantUrl } from "../qdrant-client.js";
|
|
29
|
-
|
|
30
|
-
const log = getLogger("memory-v2-skill-qdrant");
|
|
31
|
-
|
|
32
|
-
/** Name of the dedicated Qdrant collection holding skill embeddings. */
|
|
33
|
-
export const MEMORY_V2_SKILLS_COLLECTION = "memory_v2_skills";
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Stable UUIDv5 namespace used to derive a deterministic Qdrant point ID from
|
|
37
|
-
* a skill id. The namespace is an arbitrary fixed UUID; what matters is that
|
|
38
|
-
* the same id always maps to the same point ID so upserts replace in place
|
|
39
|
-
* instead of accumulating duplicates. Distinct from `qdrant.ts`'s
|
|
40
|
-
* `SLUG_NAMESPACE` so a skill id that happens to collide with a concept-page
|
|
41
|
-
* slug still maps to a different point ID across the two collections.
|
|
42
|
-
*/
|
|
43
|
-
export const SKILL_NAMESPACE = "f1903e7f-1b9d-4c15-ac46-3540b8b0a9f6";
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Per-channel score for a single skill hit returned by hybrid query.
|
|
47
|
-
* Module-private — `sim.ts` consumes the fields by duck-typing rather than
|
|
48
|
-
* naming the type, so there is no benefit to exporting it.
|
|
49
|
-
*/
|
|
50
|
-
interface SkillQueryResult {
|
|
51
|
-
id: string;
|
|
52
|
-
/**
|
|
53
|
-
* Dense cosine similarity, when the id appeared in the dense top-`limit`.
|
|
54
|
-
* `undefined` if the id only appeared in the sparse channel.
|
|
55
|
-
*/
|
|
56
|
-
denseScore?: number;
|
|
57
|
-
/**
|
|
58
|
-
* Sparse score, when the id appeared in the sparse top-`limit`.
|
|
59
|
-
* `undefined` if the id only appeared in the dense channel. Lives on a
|
|
60
|
-
* different scale than `denseScore` — callers must normalize before fusing.
|
|
61
|
-
*/
|
|
62
|
-
sparseScore?: number;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
let _client: QdrantRestClient | null = null;
|
|
66
|
-
let _collectionReady = false;
|
|
67
|
-
|
|
68
|
-
/** Lazily create a Qdrant REST client bound to the resolved URL. */
|
|
69
|
-
function getClient(): QdrantRestClient {
|
|
70
|
-
if (_client) return _client;
|
|
71
|
-
const config = getConfig();
|
|
72
|
-
_client = new QdrantRestClient({
|
|
73
|
-
url: resolveQdrantUrl(config),
|
|
74
|
-
checkCompatibility: false,
|
|
75
|
-
});
|
|
76
|
-
return _client;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Create the v2 skills collection if it does not already exist.
|
|
81
|
-
* Idempotent: a no-op when the collection is already present.
|
|
82
|
-
*
|
|
83
|
-
* Vector layout mirrors `ensureConceptPageCollection` — named dense (cosine,
|
|
84
|
-
* configurable size + on-disk) and sparse vectors. The vector size and
|
|
85
|
-
* on-disk flag inherit from `config.memory.qdrant` so v2 stays aligned with
|
|
86
|
-
* the user's existing embedding backend without separate knobs.
|
|
87
|
-
*/
|
|
88
|
-
export async function ensureSkillCollection(): Promise<void> {
|
|
89
|
-
if (_collectionReady) return;
|
|
90
|
-
|
|
91
|
-
const client = getClient();
|
|
92
|
-
const config = getConfig();
|
|
93
|
-
const vectorSize = config.memory.qdrant.vectorSize;
|
|
94
|
-
const onDisk = config.memory.qdrant.onDisk;
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const exists = await client.collectionExists(MEMORY_V2_SKILLS_COLLECTION);
|
|
98
|
-
if (exists.exists) {
|
|
99
|
-
_collectionReady = true;
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
} catch (err) {
|
|
103
|
-
// Treat "not found"-shaped errors as "needs creation" and fall through.
|
|
104
|
-
if (!isCollectionMissing(err)) throw err;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
log.info(
|
|
108
|
-
{ collection: MEMORY_V2_SKILLS_COLLECTION, vectorSize },
|
|
109
|
-
"Creating Qdrant collection for memory v2 skills",
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await client.createCollection(MEMORY_V2_SKILLS_COLLECTION, {
|
|
114
|
-
vectors: {
|
|
115
|
-
dense: {
|
|
116
|
-
size: vectorSize,
|
|
117
|
-
distance: "Cosine",
|
|
118
|
-
on_disk: onDisk,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
sparse_vectors: {
|
|
122
|
-
sparse: {}, // Qdrant auto-infers sparse vector params
|
|
123
|
-
},
|
|
124
|
-
hnsw_config: {
|
|
125
|
-
on_disk: onDisk,
|
|
126
|
-
m: 16,
|
|
127
|
-
ef_construct: 100,
|
|
128
|
-
},
|
|
129
|
-
on_disk_payload: onDisk,
|
|
130
|
-
});
|
|
131
|
-
} catch (err) {
|
|
132
|
-
// 409 = a concurrent caller created the collection — that's fine.
|
|
133
|
-
if (
|
|
134
|
-
err instanceof Error &&
|
|
135
|
-
"status" in err &&
|
|
136
|
-
(err as { status: number }).status === 409
|
|
137
|
-
) {
|
|
138
|
-
_collectionReady = true;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
throw err;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Eagerly index `id` so future per-id filters (e.g. inspecting a single
|
|
145
|
-
// skill's stored payload) don't pay a one-time indexing cost. Mirrors the
|
|
146
|
-
// up-front `slug` index in `ensureConceptPageCollection`.
|
|
147
|
-
await client.createPayloadIndex(MEMORY_V2_SKILLS_COLLECTION, {
|
|
148
|
-
field_name: "id",
|
|
149
|
-
field_schema: "keyword",
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
_collectionReady = true;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Upsert a skill's dense + sparse embedding. The point ID is derived
|
|
157
|
-
* deterministically from the skill id so subsequent calls for the same id
|
|
158
|
-
* replace the prior point in place rather than accumulating duplicates.
|
|
159
|
-
*/
|
|
160
|
-
export async function upsertSkillEmbedding(params: {
|
|
161
|
-
id: string;
|
|
162
|
-
content: string;
|
|
163
|
-
dense: number[];
|
|
164
|
-
sparse: SparseEmbedding;
|
|
165
|
-
updatedAt: number;
|
|
166
|
-
}): Promise<void> {
|
|
167
|
-
await ensureSkillCollection();
|
|
168
|
-
|
|
169
|
-
const { id, content, dense, sparse, updatedAt } = params;
|
|
170
|
-
const client = getClient();
|
|
171
|
-
const pointId = pointIdForId(id);
|
|
172
|
-
|
|
173
|
-
const upsertOnce = () =>
|
|
174
|
-
client.upsert(MEMORY_V2_SKILLS_COLLECTION, {
|
|
175
|
-
wait: true,
|
|
176
|
-
points: [
|
|
177
|
-
{
|
|
178
|
-
id: pointId,
|
|
179
|
-
vector: { dense, sparse },
|
|
180
|
-
payload: {
|
|
181
|
-
id,
|
|
182
|
-
content,
|
|
183
|
-
updated_at: updatedAt,
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
],
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
await upsertOnce();
|
|
191
|
-
} catch (err) {
|
|
192
|
-
if (isCollectionMissing(err)) {
|
|
193
|
-
_collectionReady = false;
|
|
194
|
-
await ensureSkillCollection();
|
|
195
|
-
await upsertOnce();
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
throw err;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Remove every skill point whose `payload.id` is not in `activeIds`. Used by
|
|
204
|
-
* `seedV2SkillEntries` to drop stale points after a skill is uninstalled or
|
|
205
|
-
* disabled. Idempotent: when the live points already equal `activeIds`,
|
|
206
|
-
* the function performs a single scroll and no deletes.
|
|
207
|
-
*
|
|
208
|
-
* Implementation: paginate the collection with the Qdrant `scroll` API
|
|
209
|
-
* (payload-only, no vectors) and delete by point ID for any payload whose
|
|
210
|
-
* `id` is missing from the active set.
|
|
211
|
-
*/
|
|
212
|
-
export async function pruneSkillsExcept(
|
|
213
|
-
activeIds: readonly string[],
|
|
214
|
-
): Promise<void> {
|
|
215
|
-
await ensureSkillCollection();
|
|
216
|
-
|
|
217
|
-
const client = getClient();
|
|
218
|
-
const activeSet = new Set(activeIds);
|
|
219
|
-
|
|
220
|
-
const doPrune = async (): Promise<void> => {
|
|
221
|
-
const stalePointIds: Array<string | number> = [];
|
|
222
|
-
let offset: string | number | undefined = undefined;
|
|
223
|
-
// Guard against a pathological pagination loop.
|
|
224
|
-
const maxIterations = 10_000;
|
|
225
|
-
const batchSize = 256;
|
|
226
|
-
for (let i = 0; i < maxIterations; i++) {
|
|
227
|
-
const result = await client.scroll(MEMORY_V2_SKILLS_COLLECTION, {
|
|
228
|
-
limit: batchSize,
|
|
229
|
-
with_payload: true,
|
|
230
|
-
with_vector: false,
|
|
231
|
-
...(offset !== undefined ? { offset } : {}),
|
|
232
|
-
});
|
|
233
|
-
for (const point of result.points) {
|
|
234
|
-
const payloadId = (point.payload as { id?: unknown } | null)?.id;
|
|
235
|
-
if (typeof payloadId !== "string") continue;
|
|
236
|
-
if (!activeSet.has(payloadId)) {
|
|
237
|
-
stalePointIds.push(point.id);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const next = result.next_page_offset;
|
|
241
|
-
if (next == null) break;
|
|
242
|
-
offset = typeof next === "string" ? next : (next as number);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (stalePointIds.length === 0) return;
|
|
246
|
-
|
|
247
|
-
await client.delete(MEMORY_V2_SKILLS_COLLECTION, {
|
|
248
|
-
wait: true,
|
|
249
|
-
points: stalePointIds,
|
|
250
|
-
});
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
await doPrune();
|
|
255
|
-
} catch (err) {
|
|
256
|
-
if (isCollectionMissing(err)) {
|
|
257
|
-
_collectionReady = false;
|
|
258
|
-
await ensureSkillCollection();
|
|
259
|
-
await doPrune();
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
throw err;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Run separate dense and sparse queries against the skills collection and
|
|
268
|
-
* return per-channel scores per skill id. Callers fuse these — typically via
|
|
269
|
-
* a normalized weighted-sum — because RRF would discard the score magnitudes
|
|
270
|
-
* the activation formula needs.
|
|
271
|
-
*
|
|
272
|
-
* Each channel returns up to `limit` hits. A skill is included in the result
|
|
273
|
-
* if it appears in either channel; the missing channel's score is left
|
|
274
|
-
* `undefined` so callers can detect single-channel matches.
|
|
275
|
-
*
|
|
276
|
-
* `restrictToIds`, when provided, filters the search server-side to only
|
|
277
|
-
* those ids (Qdrant `id IN [...]` filter). Used by `simSkillBatch` when the
|
|
278
|
-
* candidate set is already known so the activation scorer gets scores for
|
|
279
|
-
* exactly those ids rather than Qdrant's global top-`limit`. An empty list
|
|
280
|
-
* short-circuits to no results — the caller is asking for "nothing", not
|
|
281
|
-
* "everything". Undefined queries the full collection.
|
|
282
|
-
*/
|
|
283
|
-
export async function hybridQuerySkills(
|
|
284
|
-
dense: number[],
|
|
285
|
-
sparse: SparseEmbedding,
|
|
286
|
-
limit: number,
|
|
287
|
-
restrictToIds?: readonly string[],
|
|
288
|
-
options?: { skipSparse?: boolean },
|
|
289
|
-
): Promise<SkillQueryResult[]> {
|
|
290
|
-
if (restrictToIds && restrictToIds.length === 0) {
|
|
291
|
-
// An empty restriction means "no candidates"; skip the round-trip.
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
await ensureSkillCollection();
|
|
296
|
-
|
|
297
|
-
const client = getClient();
|
|
298
|
-
const filter = restrictToIds
|
|
299
|
-
? { must: [{ key: "id", match: { any: [...restrictToIds] } }] }
|
|
300
|
-
: undefined;
|
|
301
|
-
|
|
302
|
-
// Same opt-in short-circuit as `hybridQueryConceptPages`: skip the sparse
|
|
303
|
-
// round-trip entirely so we sidestep the Qdrant 1.13.x sparse-index OOM
|
|
304
|
-
// crash when operators flip sparse off via `sparse_weight: 0`.
|
|
305
|
-
const skipSparse = options?.skipSparse ?? false;
|
|
306
|
-
|
|
307
|
-
const denseQuery = () =>
|
|
308
|
-
client.query(MEMORY_V2_SKILLS_COLLECTION, {
|
|
309
|
-
query: dense,
|
|
310
|
-
using: "dense",
|
|
311
|
-
limit,
|
|
312
|
-
with_payload: true,
|
|
313
|
-
filter,
|
|
314
|
-
});
|
|
315
|
-
const sparseQuery = () =>
|
|
316
|
-
client.query(MEMORY_V2_SKILLS_COLLECTION, {
|
|
317
|
-
query: sparse,
|
|
318
|
-
using: "sparse",
|
|
319
|
-
limit,
|
|
320
|
-
with_payload: true,
|
|
321
|
-
filter,
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
// Run both queries concurrently — they hit independent named vectors.
|
|
325
|
-
const emptyResult = {
|
|
326
|
-
points: [] as Array<{ payload?: unknown; score?: number }>,
|
|
327
|
-
};
|
|
328
|
-
const runQueries = async () =>
|
|
329
|
-
Promise.all([denseQuery(), skipSparse ? emptyResult : sparseQuery()]);
|
|
330
|
-
|
|
331
|
-
let denseResults;
|
|
332
|
-
let sparseResults;
|
|
333
|
-
try {
|
|
334
|
-
[denseResults, sparseResults] = await runQueries();
|
|
335
|
-
} catch (err) {
|
|
336
|
-
if (isCollectionMissing(err)) {
|
|
337
|
-
_collectionReady = false;
|
|
338
|
-
await ensureSkillCollection();
|
|
339
|
-
[denseResults, sparseResults] = await runQueries();
|
|
340
|
-
} else {
|
|
341
|
-
throw err;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Merge by id. Missing-side scores stay undefined so the fuser can tell
|
|
346
|
-
// "no match in this channel" apart from "match with score 0".
|
|
347
|
-
const merged = new Map<string, SkillQueryResult>();
|
|
348
|
-
for (const point of denseResults.points ?? []) {
|
|
349
|
-
const id = (point.payload as { id?: unknown } | null)?.id;
|
|
350
|
-
if (typeof id !== "string") continue;
|
|
351
|
-
merged.set(id, { id, denseScore: point.score ?? 0 });
|
|
352
|
-
}
|
|
353
|
-
for (const point of sparseResults.points ?? []) {
|
|
354
|
-
const id = (point.payload as { id?: unknown } | null)?.id;
|
|
355
|
-
if (typeof id !== "string") continue;
|
|
356
|
-
const existing = merged.get(id);
|
|
357
|
-
if (existing) {
|
|
358
|
-
existing.sparseScore = point.score ?? 0;
|
|
359
|
-
} else {
|
|
360
|
-
merged.set(id, { id, sparseScore: point.score ?? 0 });
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return Array.from(merged.values());
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Detect "collection not found" errors so callers can reset readiness and
|
|
369
|
-
* retry after an external deletion (e.g. workspace reset). Re-implemented
|
|
370
|
-
* locally rather than imported from `qdrant.ts` to keep this module
|
|
371
|
-
* self-contained — the helper is small and the duplication is cleaner than
|
|
372
|
-
* exporting an internal detail across files.
|
|
373
|
-
*/
|
|
374
|
-
function isCollectionMissing(err: unknown): boolean {
|
|
375
|
-
if (
|
|
376
|
-
err &&
|
|
377
|
-
typeof err === "object" &&
|
|
378
|
-
"status" in err &&
|
|
379
|
-
(err as { status: number }).status === 404
|
|
380
|
-
) {
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
384
|
-
return (
|
|
385
|
-
msg.includes("Not found") ||
|
|
386
|
-
msg.includes("doesn't exist") ||
|
|
387
|
-
msg.includes("not found")
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Derive the deterministic Qdrant point ID for a skill id. Qdrant requires
|
|
393
|
-
* UUID/integer IDs; UUIDv5 keeps the mapping stable across processes so
|
|
394
|
-
* upserts replace in place.
|
|
395
|
-
*/
|
|
396
|
-
function pointIdForId(id: string): string {
|
|
397
|
-
return uuidv5(id, SKILL_NAMESPACE);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/** @internal Test-only: reset module-level singletons. */
|
|
401
|
-
export function _resetMemoryV2SkillQdrantForTests(): void {
|
|
402
|
-
_client = null;
|
|
403
|
-
_collectionReady = false;
|
|
404
|
-
}
|
package/src/signals/bash.ts
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handle bash debug signals delivered via signal files from the CLI.
|
|
3
|
-
*
|
|
4
|
-
* Each invocation writes JSON to a unique `signals/bash.<requestId>` file.
|
|
5
|
-
* ConfigWatcher detects the new file and invokes {@link handleBashSignal},
|
|
6
|
-
* which reads the payload, spawns the command, and writes the result to
|
|
7
|
-
* `signals/bash.<requestId>.result` for the CLI to pick up.
|
|
8
|
-
*
|
|
9
|
-
* Per-request filenames avoid dropped commands when overlapping invocations
|
|
10
|
-
* race on the same signal file.
|
|
11
|
-
*
|
|
12
|
-
* **Security**: This handler is gated behind the `VELLUM_DEBUG` environment
|
|
13
|
-
* variable. When debug mode is off (the default), the daemon ignores bash
|
|
14
|
-
* signal files entirely. This prevents untrusted file-write operations
|
|
15
|
-
* (e.g. from prompt injection or a compromised skill) from bypassing the
|
|
16
|
-
* normal tool-approval flow for shell execution.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { spawn } from "node:child_process";
|
|
20
|
-
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
|
|
23
|
-
import { getIsContainerized } from "../config/env-registry.js";
|
|
24
|
-
import { getLogger } from "../util/logger.js";
|
|
25
|
-
import { getSignalsDir, getWorkspaceDir } from "../util/platform.js";
|
|
26
|
-
|
|
27
|
-
const log = getLogger("signal:bash");
|
|
28
|
-
|
|
29
|
-
function isDebugMode(): boolean {
|
|
30
|
-
return (
|
|
31
|
-
process.env.VELLUM_DEBUG === "1" || process.env.VELLUM_DEBUG === "true"
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
36
|
-
|
|
37
|
-
interface BashSignalPayload {
|
|
38
|
-
requestId: string;
|
|
39
|
-
command: string;
|
|
40
|
-
timeoutMs?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface BashSignalResult {
|
|
44
|
-
requestId: string;
|
|
45
|
-
stdout: string;
|
|
46
|
-
stderr: string;
|
|
47
|
-
exitCode: number | null;
|
|
48
|
-
timedOut: boolean;
|
|
49
|
-
error?: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function writeResult(requestId: string, result: BashSignalResult): void {
|
|
53
|
-
try {
|
|
54
|
-
writeFileSync(
|
|
55
|
-
join(getSignalsDir(), `bash.${requestId}.result`),
|
|
56
|
-
JSON.stringify(result),
|
|
57
|
-
);
|
|
58
|
-
} catch (err) {
|
|
59
|
-
log.error({ err, requestId }, "Failed to write bash signal result");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Read a `signals/bash.<requestId>` file, execute the command, and write
|
|
65
|
-
* the result to `signals/bash.<requestId>.result`. Called by ConfigWatcher
|
|
66
|
-
* when a matching signal file is created or modified.
|
|
67
|
-
*/
|
|
68
|
-
export function handleBashSignal(filename: string): void {
|
|
69
|
-
if (getIsContainerized()) return;
|
|
70
|
-
|
|
71
|
-
if (!isDebugMode()) {
|
|
72
|
-
log.warn(
|
|
73
|
-
{ filename },
|
|
74
|
-
"Bash signal ignored — debug mode is not enabled (set VELLUM_DEBUG=1)",
|
|
75
|
-
);
|
|
76
|
-
// Write an error result so the CLI gets a clear rejection instead of timing out.
|
|
77
|
-
const match = filename.match(/^bash\.(.+)$/);
|
|
78
|
-
if (match) {
|
|
79
|
-
writeResult(match[1], {
|
|
80
|
-
requestId: match[1],
|
|
81
|
-
stdout: "",
|
|
82
|
-
stderr: "",
|
|
83
|
-
exitCode: null,
|
|
84
|
-
timedOut: false,
|
|
85
|
-
error:
|
|
86
|
-
"Bash signals are disabled. The running assistant process must have been started with VELLUM_DEBUG=1 (setting it on the CLI command alone is not enough). Restart the assistant with: vellum sleep && VELLUM_DEBUG=1 vellum wake",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const signalPath = join(getSignalsDir(), filename);
|
|
93
|
-
let raw: string;
|
|
94
|
-
try {
|
|
95
|
-
raw = readFileSync(signalPath, "utf-8");
|
|
96
|
-
} catch {
|
|
97
|
-
// File may already be deleted (e.g. re-trigger from our own unlinkSync).
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let payload: BashSignalPayload;
|
|
102
|
-
try {
|
|
103
|
-
payload = JSON.parse(raw) as BashSignalPayload;
|
|
104
|
-
} catch (err) {
|
|
105
|
-
log.error({ err, filename }, "Failed to parse bash signal file");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
unlinkSync(signalPath);
|
|
111
|
-
} catch {
|
|
112
|
-
// Best-effort cleanup; the file may already be gone.
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const { requestId, command, timeoutMs } = payload;
|
|
116
|
-
|
|
117
|
-
if (!requestId || typeof requestId !== "string") {
|
|
118
|
-
log.warn("Bash signal missing requestId");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (!command || typeof command !== "string") {
|
|
122
|
-
log.warn({ requestId }, "Bash signal missing command");
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const effectiveTimeout =
|
|
127
|
-
typeof timeoutMs === "number" && timeoutMs > 0
|
|
128
|
-
? timeoutMs
|
|
129
|
-
: DEFAULT_TIMEOUT_MS;
|
|
130
|
-
|
|
131
|
-
log.info({ requestId, command }, "Executing bash signal command");
|
|
132
|
-
|
|
133
|
-
const stdoutChunks: Buffer[] = [];
|
|
134
|
-
const stderrChunks: Buffer[] = [];
|
|
135
|
-
let timedOut = false;
|
|
136
|
-
let resultWritten = false;
|
|
137
|
-
|
|
138
|
-
const child = spawn("bash", ["-c", command], {
|
|
139
|
-
cwd: getWorkspaceDir(),
|
|
140
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
141
|
-
detached: true,
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const timer = setTimeout(() => {
|
|
145
|
-
timedOut = true;
|
|
146
|
-
try {
|
|
147
|
-
process.kill(-child.pid!, "SIGKILL");
|
|
148
|
-
} catch {
|
|
149
|
-
// Process group may have already exited.
|
|
150
|
-
}
|
|
151
|
-
}, effectiveTimeout);
|
|
152
|
-
|
|
153
|
-
child.stdout.on("data", (data: Buffer) => {
|
|
154
|
-
stdoutChunks.push(data);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
child.stderr.on("data", (data: Buffer) => {
|
|
158
|
-
stderrChunks.push(data);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
child.on("close", (code) => {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
if (resultWritten) return;
|
|
164
|
-
resultWritten = true;
|
|
165
|
-
|
|
166
|
-
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
167
|
-
const stderr = Buffer.concat(stderrChunks).toString();
|
|
168
|
-
|
|
169
|
-
log.info(
|
|
170
|
-
{ requestId, exitCode: code, timedOut },
|
|
171
|
-
"Bash signal command completed",
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
writeResult(requestId, {
|
|
175
|
-
requestId,
|
|
176
|
-
stdout,
|
|
177
|
-
stderr,
|
|
178
|
-
exitCode: code,
|
|
179
|
-
timedOut,
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
child.on("error", (err) => {
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
if (resultWritten) return;
|
|
186
|
-
resultWritten = true;
|
|
187
|
-
|
|
188
|
-
log.error({ err, requestId }, "Failed to spawn bash signal command");
|
|
189
|
-
writeResult(requestId, {
|
|
190
|
-
requestId,
|
|
191
|
-
stdout: "",
|
|
192
|
-
stderr: "",
|
|
193
|
-
exitCode: null,
|
|
194
|
-
timedOut: false,
|
|
195
|
-
error: err.message,
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
}
|