@vellumai/assistant 0.7.3 → 0.8.0
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 +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -7,10 +7,7 @@ import { reconcileCallsOnStartup } from "../calls/call-recovery.js";
|
|
|
7
7
|
import { setRelayBroadcast } from "../calls/relay-server.js";
|
|
8
8
|
import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
|
|
9
9
|
import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
|
|
10
|
-
import {
|
|
11
|
-
initFeatureFlagOverrides,
|
|
12
|
-
isAssistantFeatureFlagEnabled,
|
|
13
|
-
} from "../config/assistant-feature-flags.js";
|
|
10
|
+
import { initFeatureFlagOverrides } from "../config/assistant-feature-flags.js";
|
|
14
11
|
import {
|
|
15
12
|
getPlatformAssistantId,
|
|
16
13
|
getRuntimeHttpHost,
|
|
@@ -117,7 +114,10 @@ import {
|
|
|
117
114
|
} from "./guardian-action-generators.js";
|
|
118
115
|
import { backfillSlackInjectionTemplates } from "./handlers/config-slack-channel.js";
|
|
119
116
|
import { installAssistantSymlink } from "./install-symlink.js";
|
|
120
|
-
import {
|
|
117
|
+
import {
|
|
118
|
+
maybeRebuildMemoryV2Concepts,
|
|
119
|
+
maybeSeedMemoryV2Skills,
|
|
120
|
+
} from "./memory-v2-startup.js";
|
|
121
121
|
import { processMessage } from "./process-message.js";
|
|
122
122
|
import { runProfilerSweep } from "./profiler-run-store.js";
|
|
123
123
|
import {
|
|
@@ -318,21 +318,16 @@ export async function runDaemon(): Promise<void> {
|
|
|
318
318
|
const signingKey = resolveSigningKey();
|
|
319
319
|
initAuthSigningKey(signingKey);
|
|
320
320
|
|
|
321
|
-
// Pre-populate
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
// the daemon. seedV2SkillEntries is idempotent, so re-running after
|
|
332
|
-
// overrides land is safe.
|
|
333
|
-
void initFeatureFlagOverrides()
|
|
334
|
-
.then(() => maybeSeedMemoryV2Skills(loadConfig()))
|
|
335
|
-
.catch((err) => log.warn({ err }, "Background feature flag init failed"));
|
|
321
|
+
// Pre-populate feature flag overrides so subsequent sync
|
|
322
|
+
// isAssistantFeatureFlagEnabled() calls have data. Fired non-blocking
|
|
323
|
+
// so a slow or unreachable gateway doesn't delay daemon startup (the
|
|
324
|
+
// IPC call has a 3s connect + 5s call timeout that would otherwise
|
|
325
|
+
// stall the critical path).
|
|
326
|
+
void initFeatureFlagOverrides().catch((err) =>
|
|
327
|
+
log.warn({ err }, "Background feature flag init failed"),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
maybeSeedMemoryV2Skills(loadConfig());
|
|
336
331
|
|
|
337
332
|
seedInterfaceFiles();
|
|
338
333
|
|
|
@@ -741,64 +736,87 @@ export async function runDaemon(): Promise<void> {
|
|
|
741
736
|
}
|
|
742
737
|
|
|
743
738
|
if (qdrantStarted) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
collection
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
739
|
+
// Skip the v1 Qdrant collection lifecycle when memory v2 is active —
|
|
740
|
+
// the v1 collection has no writers (handleRemember returns early) or
|
|
741
|
+
// readers (graph search is bypassed) under v2, so ensuring/migrating
|
|
742
|
+
// it just maintains a dead-on-arrival collection. Existing on-disk
|
|
743
|
+
// collections are left intact so flipping v2 off restores v1 cleanly.
|
|
744
|
+
if (!config.memory.v2.enabled) {
|
|
745
|
+
try {
|
|
746
|
+
const embeddingSelection = await selectEmbeddingBackend(config);
|
|
747
|
+
// Sentinel only encodes the dense provider+model identity; sparse
|
|
748
|
+
// encoder changes never require collection recreation, so they
|
|
749
|
+
// intentionally do not contribute to the v1 collection identity.
|
|
750
|
+
const embeddingModel = embeddingSelection.backend
|
|
751
|
+
? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}`
|
|
752
|
+
: undefined;
|
|
753
|
+
const qdrantClient = initQdrantClient({
|
|
754
|
+
url: qdrantUrl,
|
|
755
|
+
collection: config.memory.qdrant.collection,
|
|
756
|
+
vectorSize: config.memory.qdrant.vectorSize,
|
|
757
|
+
onDisk: config.memory.qdrant.onDisk,
|
|
758
|
+
quantization: config.memory.qdrant.quantization,
|
|
759
|
+
embeddingModel,
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
// Eagerly ensure the collection exists so we detect migrations
|
|
763
|
+
// (unnamed→named vectors, dimension/model changes) at startup.
|
|
764
|
+
// If a destructive migration occurred, enqueue a rebuild_index job
|
|
765
|
+
// to re-embed all memory items from the SQLite cache.
|
|
766
|
+
const { migrated } = await qdrantClient.ensureCollection();
|
|
767
|
+
if (migrated) {
|
|
768
|
+
enqueueMemoryJob("rebuild_index", {});
|
|
769
|
+
log.info(
|
|
770
|
+
"Qdrant collection was migrated — enqueued rebuild_index job",
|
|
771
|
+
);
|
|
772
|
+
}
|
|
760
773
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (migrated) {
|
|
767
|
-
enqueueMemoryJob("rebuild_index", {});
|
|
768
|
-
log.info(
|
|
769
|
-
"Qdrant collection was migrated — enqueued rebuild_index job",
|
|
774
|
+
log.info("Qdrant vector store initialized");
|
|
775
|
+
} catch (err) {
|
|
776
|
+
log.warn(
|
|
777
|
+
{ err },
|
|
778
|
+
"Qdrant client initialization failed — memory features will be degraded",
|
|
770
779
|
);
|
|
771
780
|
}
|
|
781
|
+
}
|
|
772
782
|
|
|
773
|
-
|
|
783
|
+
// Detect schema drift on the v2 concept-page collection (e.g.
|
|
784
|
+
// pre-#29823 collections lacking summary_dense / summary_sparse) and
|
|
785
|
+
// recreate + enqueue a reembed when needed. Awaited inline so the
|
|
786
|
+
// reembed enqueue happens before the memory worker drains its first
|
|
787
|
+
// batch; the call's own try/catch keeps any v2-side failure from
|
|
788
|
+
// blocking the v1 PKB reconcile or BM25 build below.
|
|
789
|
+
try {
|
|
790
|
+
await maybeRebuildMemoryV2Concepts(config);
|
|
774
791
|
} catch (err) {
|
|
775
792
|
log.warn(
|
|
776
793
|
{ err },
|
|
777
|
-
"
|
|
794
|
+
"Memory v2 collection schema check threw — continuing startup",
|
|
778
795
|
);
|
|
779
796
|
}
|
|
780
797
|
|
|
781
|
-
// Reconcile the PKB Qdrant index against the on-disk tree.
|
|
782
|
-
//
|
|
783
|
-
// `getQdrantClient()`
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
798
|
+
// Reconcile the PKB Qdrant index against the on-disk tree. Gated on
|
|
799
|
+
// !v2 because PKB is the v1 storage layer; under v2 the v1 collection
|
|
800
|
+
// is not initialized, so calling `getQdrantClient()` here would throw.
|
|
801
|
+
// Fire-and-forget so enqueued re-index jobs drain in the background
|
|
802
|
+
// and first-turn latency stays unaffected.
|
|
803
|
+
if (!config.memory.v2.enabled) {
|
|
804
|
+
void (async () => {
|
|
805
|
+
try {
|
|
806
|
+
const { reconcilePkbIndex } =
|
|
807
|
+
await import("../memory/pkb/pkb-reconcile.js");
|
|
808
|
+
const { PKB_WORKSPACE_SCOPE } =
|
|
809
|
+
await import("../memory/pkb/types.js");
|
|
810
|
+
const pkbRoot = join(getWorkspaceDir(), "pkb");
|
|
811
|
+
await reconcilePkbIndex(pkbRoot, PKB_WORKSPACE_SCOPE);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
log.warn(
|
|
814
|
+
{ err },
|
|
815
|
+
"PKB index reconciliation failed — continuing startup",
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
})();
|
|
819
|
+
}
|
|
802
820
|
|
|
803
821
|
// Build the BM25 corpus stats (per-token document frequencies and
|
|
804
822
|
// average document length) used by the v2 sparse channel. Without
|
|
@@ -1283,14 +1301,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
1283
1301
|
log.warn({ err }, "Proactive artifact backfill failed");
|
|
1284
1302
|
}
|
|
1285
1303
|
|
|
1286
|
-
// Filing yields to the memory v2 consolidation job when
|
|
1304
|
+
// Filing yields to the memory v2 consolidation job when v2 is enabled —
|
|
1287
1305
|
// both serve the same role (periodic background memory processing) and
|
|
1288
1306
|
// running both is redundant. The consolidation job runs through the
|
|
1289
1307
|
// memory jobs worker (see `maybeEnqueueGraphMaintenanceJobs`).
|
|
1290
|
-
const memoryV2Enabled =
|
|
1291
|
-
"memory-v2-enabled",
|
|
1292
|
-
config,
|
|
1293
|
-
);
|
|
1308
|
+
const memoryV2Enabled = config.memory.v2.enabled;
|
|
1294
1309
|
let filing: FilingService | null = null;
|
|
1295
1310
|
if (!memoryV2Enabled) {
|
|
1296
1311
|
const filingConfig = config.filing;
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// startup work invoked from `lifecycle.ts`. Lives in its own file so the unit
|
|
7
7
|
// test for the gate does not have to mount the entire lifecycle import graph.
|
|
8
8
|
|
|
9
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
10
9
|
import type { AssistantConfig } from "../config/schema.js";
|
|
11
10
|
import { getLogger } from "../util/logger.js";
|
|
11
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
12
12
|
|
|
13
13
|
const log = getLogger("memory-v2-startup");
|
|
14
14
|
|
|
@@ -16,21 +16,12 @@ const log = getLogger("memory-v2-startup");
|
|
|
16
16
|
* Fire-and-forget seed of the v2 skill entries (now indexed alongside concept
|
|
17
17
|
* pages in `memory_v2_concept_pages` under the `skills/<id>` slug prefix), and
|
|
18
18
|
* a one-shot best-effort cleanup of the legacy `memory_v2_skills` Qdrant
|
|
19
|
-
* collection.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* Uses a dynamic import so v2 code does not load unless the gate passes.
|
|
24
|
-
* Never awaits — startup must not block on this (see `assistant/CLAUDE.md`
|
|
25
|
-
* daemon startup philosophy).
|
|
19
|
+
* collection. Uses a dynamic import so v2 code does not load unless the gate
|
|
20
|
+
* passes. Never awaits — startup must not block on this (see
|
|
21
|
+
* `assistant/CLAUDE.md` daemon startup philosophy).
|
|
26
22
|
*/
|
|
27
23
|
export function maybeSeedMemoryV2Skills(config: AssistantConfig): void {
|
|
28
|
-
if (
|
|
29
|
-
!isAssistantFeatureFlagEnabled("memory-v2-enabled", config) ||
|
|
30
|
-
!config.memory.v2.enabled
|
|
31
|
-
) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
24
|
+
if (!config.memory.v2.enabled) return;
|
|
34
25
|
void import("../memory/v2/skill-store.js")
|
|
35
26
|
.then(({ seedV2SkillEntries }) => seedV2SkillEntries())
|
|
36
27
|
.catch((err) => log.warn({ err }, "Failed to seed v2 skill entries"));
|
|
@@ -43,3 +34,53 @@ export function maybeSeedMemoryV2Skills(config: AssistantConfig): void {
|
|
|
43
34
|
),
|
|
44
35
|
);
|
|
45
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reconcile the v2 concept-page Qdrant collection with the expected schema
|
|
40
|
+
* and enqueue `memory_v2_reembed` when the collection is missing data.
|
|
41
|
+
* Triggers reembed in two cases:
|
|
42
|
+
* - Drift: `ensureConceptPageCollection` returned `{ migrated: true }`
|
|
43
|
+
* after destructively recreating the collection (e.g. pre-#29823
|
|
44
|
+
* schemas lacking `summary_*` named vectors).
|
|
45
|
+
* - Empty-after-create: the collection has zero points but pages exist on
|
|
46
|
+
* disk — covers crash-mid-rebuild and external Qdrant wipes.
|
|
47
|
+
*
|
|
48
|
+
* Awaited inline by `lifecycle.ts` so the enqueue happens before the memory
|
|
49
|
+
* worker drains its first batch; the body is wrapped in try/catch so a v2
|
|
50
|
+
* failure never blocks startup.
|
|
51
|
+
*/
|
|
52
|
+
export async function maybeRebuildMemoryV2Concepts(
|
|
53
|
+
config: AssistantConfig,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
if (!config.memory.v2.enabled) return;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const { ensureConceptPageCollection, countConceptPagePoints } =
|
|
59
|
+
await import("../memory/v2/qdrant.js");
|
|
60
|
+
const { hasConceptPages } = await import("../memory/v2/page-store.js");
|
|
61
|
+
const { enqueueMemoryJob } = await import("../memory/jobs-store.js");
|
|
62
|
+
|
|
63
|
+
const { migrated } = await ensureConceptPageCollection();
|
|
64
|
+
|
|
65
|
+
let shouldReembed = migrated;
|
|
66
|
+
if (!shouldReembed) {
|
|
67
|
+
const points = await countConceptPagePoints();
|
|
68
|
+
if (points === 0 && (await hasConceptPages(getWorkspaceDir()))) {
|
|
69
|
+
shouldReembed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (shouldReembed) {
|
|
74
|
+
const jobId = enqueueMemoryJob("memory_v2_reembed", {});
|
|
75
|
+
log.info(
|
|
76
|
+
{ jobId, collectionMigrated: migrated },
|
|
77
|
+
"Memory v2 collection rebuild required — enqueued reembed job",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.warn(
|
|
82
|
+
{ err },
|
|
83
|
+
"Memory v2 collection schema check failed — continuing startup; v2 retrieval may be degraded",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -147,8 +147,26 @@ export interface ToolResult {
|
|
|
147
147
|
matchedTrustRuleId?: string;
|
|
148
148
|
/** Whether the daemon is running in a containerized (Docker) environment. */
|
|
149
149
|
isContainerized?: boolean;
|
|
150
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Display-only ladder of scope option labels for the rule editor
|
|
152
|
+
* (narrowest to broadest). The `pattern` here is regex-style and is
|
|
153
|
+
* NOT a valid trust rule pattern. Clients must use
|
|
154
|
+
* `riskAllowlistOptions` for the pattern that gets saved.
|
|
155
|
+
*/
|
|
151
156
|
riskScopeOptions?: Array<{ pattern: string; label: string }>;
|
|
157
|
+
/**
|
|
158
|
+
* Allowlist options for the rule editor save path (narrowest to
|
|
159
|
+
* broadest). Each `pattern` is a Minimatch-glob compatible string —
|
|
160
|
+
* what the gateway actually matches against. Mirrors the
|
|
161
|
+
* `allowlistOptions` field on `ConfirmationRequest`. May be absent
|
|
162
|
+
* for tools whose classifier does not produce an allowlist (e.g.
|
|
163
|
+
* web-risk classifier, MCP tools without classifier coverage).
|
|
164
|
+
*/
|
|
165
|
+
riskAllowlistOptions?: Array<{
|
|
166
|
+
label: string;
|
|
167
|
+
description: string;
|
|
168
|
+
pattern: string;
|
|
169
|
+
}>;
|
|
152
170
|
/** Directory scope ladder for the rule editor modal (narrowest to broadest). */
|
|
153
171
|
riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
|
|
154
172
|
/** How the approval decision was reached: prompted, auto, blocked, or unknown (legacy). */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* background jobs (e.g. proactive artifact generation) can persist documents
|
|
6
6
|
* without going through the HTTP layer.
|
|
7
7
|
*/
|
|
8
|
-
import { rawRun } from "../memory/raw-query.js";
|
|
8
|
+
import { rawGet, rawRun } from "../memory/raw-query.js";
|
|
9
9
|
import { getLogger } from "../util/logger.js";
|
|
10
10
|
|
|
11
11
|
const log = getLogger("document-store");
|
|
@@ -83,3 +83,37 @@ export function saveDocument(params: {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/** Update persisted document content (append or replace). */
|
|
88
|
+
export function updateDocumentContent(
|
|
89
|
+
surfaceId: string,
|
|
90
|
+
markdown: string,
|
|
91
|
+
mode: string,
|
|
92
|
+
): void {
|
|
93
|
+
try {
|
|
94
|
+
const existing = rawGet<{ content: string }>(
|
|
95
|
+
/*sql*/ `SELECT content FROM documents WHERE surface_id = ?`,
|
|
96
|
+
surfaceId,
|
|
97
|
+
);
|
|
98
|
+
if (!existing) {
|
|
99
|
+
log.info({ surfaceId }, "No persisted document to update");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const sep = mode === "append" && existing.content.length > 0 ? "\n\n" : "";
|
|
103
|
+
const newContent =
|
|
104
|
+
mode === "append" ? existing.content + sep + markdown : markdown;
|
|
105
|
+
const wordCount = newContent
|
|
106
|
+
.split(/\s+/)
|
|
107
|
+
.filter((w) => w.length > 0).length;
|
|
108
|
+
rawRun(
|
|
109
|
+
/*sql*/ `UPDATE documents SET content = ?, word_count = ?, updated_at = ? WHERE surface_id = ?`,
|
|
110
|
+
newContent,
|
|
111
|
+
wordCount,
|
|
112
|
+
Date.now(),
|
|
113
|
+
surfaceId,
|
|
114
|
+
);
|
|
115
|
+
log.info({ surfaceId, mode }, "Updated document content");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
log.error({ err: error, surfaceId }, "Document content update error");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
5
4
|
import { getConfig } from "../config/loader.js";
|
|
6
5
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
7
6
|
import {
|
|
@@ -115,8 +114,8 @@ export class FilingService {
|
|
|
115
114
|
|
|
116
115
|
start(): void {
|
|
117
116
|
const fullConfig = getConfig();
|
|
118
|
-
if (
|
|
119
|
-
log.info("Filing service disabled — memory v2
|
|
117
|
+
if (fullConfig.memory.v2.enabled) {
|
|
118
|
+
log.info("Filing service disabled — memory v2 is active");
|
|
120
119
|
this._nextRunAt = null;
|
|
121
120
|
this._nextCompactionAt = null;
|
|
122
121
|
return;
|
|
@@ -882,7 +882,7 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
|
|
|
882
882
|
prompt += `\n\n<heartbeat-disposition>
|
|
883
883
|
This heartbeat runs frequently. Do not manufacture a report just because it ran.
|
|
884
884
|
If there is nothing genuinely useful, actionable, or interesting to surface, keep the response brief and end with HEARTBEAT_OK.
|
|
885
|
-
If there is something worth interrupting the guardian for, write a concise guardian-facing note first: what happened, why it matters, and the recommended next step. Then end with HEARTBEAT_ALERT. That note may be used as notification copy.
|
|
885
|
+
If there is something worth interrupting the guardian for, write a concise guardian-facing note first: what happened, why it matters, and the recommended next step. Address the guardian directly as "you"; do not write instructions to yourself or another intermediary. Then end with HEARTBEAT_ALERT. That note may be used as notification copy.
|
|
886
886
|
After completing your review, end your response with one of:
|
|
887
887
|
- HEARTBEAT_OK — if everything looks good, no action needed
|
|
888
888
|
- HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
|
|
@@ -28,9 +28,13 @@
|
|
|
28
28
|
* back to a shorter deterministic path so CLI commands can still connect.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { existsSync,
|
|
31
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
32
32
|
import { createServer, type Server, type Socket } from "node:net";
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
ensureSocketDir,
|
|
36
|
+
SocketWatchdog,
|
|
37
|
+
} from "@vellumai/ipc-server-utils";
|
|
34
38
|
|
|
35
39
|
import { findLocalGuardianPrincipalId } from "../runtime/local-actor-identity.js";
|
|
36
40
|
import { RouteError } from "../runtime/routes/errors.js";
|
|
@@ -130,13 +134,29 @@ function isIpcBinaryResponse(value: unknown): value is IpcBinaryResponse {
|
|
|
130
134
|
// Server
|
|
131
135
|
// ---------------------------------------------------------------------------
|
|
132
136
|
|
|
137
|
+
/** Optional configuration for {@link AssistantIpcServer}. */
|
|
138
|
+
export interface AssistantIpcServerOptions {
|
|
139
|
+
/**
|
|
140
|
+
* How often the socket-file watchdog stats the listening socket path.
|
|
141
|
+
* Set to `0` to disable. Defaults to {@link SocketWatchdog}'s 5000ms.
|
|
142
|
+
*/
|
|
143
|
+
watchdogIntervalMs?: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
export class AssistantIpcServer {
|
|
134
147
|
private server: Server | null = null;
|
|
135
148
|
private clients = new Set<Socket>();
|
|
136
149
|
private methods = new Map<string, RouteDefinition["handler"]>();
|
|
137
150
|
private socketPath: string;
|
|
151
|
+
private watchdog: SocketWatchdog;
|
|
152
|
+
/**
|
|
153
|
+
* Servers whose listener path has been replaced by a re-bind. Kept around
|
|
154
|
+
* so already-connected sockets continue to work; closed gracefully once
|
|
155
|
+
* their accept loops drain.
|
|
156
|
+
*/
|
|
157
|
+
private legacyServers = new Set<Server>();
|
|
138
158
|
|
|
139
|
-
constructor() {
|
|
159
|
+
constructor(options?: AssistantIpcServerOptions) {
|
|
140
160
|
const resolution = resolveIpcSocketPath("assistant");
|
|
141
161
|
this.socketPath = resolution.path;
|
|
142
162
|
log.info(
|
|
@@ -154,62 +174,55 @@ export class AssistantIpcServer {
|
|
|
154
174
|
this.methods.set("db_proxy", (params) =>
|
|
155
175
|
handleDbProxy(params as unknown as DbProxyParams),
|
|
156
176
|
);
|
|
177
|
+
|
|
178
|
+
this.watchdog = new SocketWatchdog({
|
|
179
|
+
socketPath: this.socketPath,
|
|
180
|
+
intervalMs: options?.watchdogIntervalMs,
|
|
181
|
+
getServer: () => this.server,
|
|
182
|
+
createServer: () => this.createListeningServer(),
|
|
183
|
+
onRebind: (newServer, oldServer) => {
|
|
184
|
+
this.server = newServer;
|
|
185
|
+
this.legacyServers.add(oldServer);
|
|
186
|
+
oldServer.close(() => {
|
|
187
|
+
this.legacyServers.delete(oldServer);
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
log,
|
|
191
|
+
});
|
|
157
192
|
}
|
|
158
193
|
|
|
159
194
|
/** Start listening on the Unix domain socket. */
|
|
160
195
|
async start(): Promise<void> {
|
|
161
196
|
// Ensure the parent directory exists before listening.
|
|
162
|
-
|
|
163
|
-
if (!existsSync(socketDir)) {
|
|
164
|
-
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
165
|
-
}
|
|
197
|
+
ensureSocketDir(this.socketPath);
|
|
166
198
|
|
|
167
199
|
// Probe before unlink so a second daemon can't silently orphan an active
|
|
168
200
|
// listener (Unix lets you unlink a still-bound socket file). See
|
|
169
201
|
// `ensureSocketPathFree` for the behavior matrix.
|
|
170
202
|
await ensureSocketPathFree(this.socketPath);
|
|
171
203
|
|
|
172
|
-
this.server =
|
|
173
|
-
this.clients.add(socket);
|
|
174
|
-
log.debug("IPC client connected");
|
|
175
|
-
|
|
176
|
-
const reader = new IpcFrameReader(
|
|
177
|
-
(envelope, binary) =>
|
|
178
|
-
this.handleEnvelope(socket, reader, envelope, binary),
|
|
179
|
-
(err) => log.warn({ err }, "IPC frame read error"),
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
socket.on("data", (chunk) => {
|
|
183
|
-
reader.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
socket.on("close", () => {
|
|
187
|
-
this.clients.delete(socket);
|
|
188
|
-
log.debug("IPC client disconnected");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
socket.on("error", (err) => {
|
|
192
|
-
log.warn({ err }, "IPC client socket error");
|
|
193
|
-
this.clients.delete(socket);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
this.server.on("error", (err) => {
|
|
198
|
-
log.error({ err }, "Assistant IPC server error");
|
|
199
|
-
});
|
|
200
|
-
|
|
204
|
+
this.server = this.createListeningServer();
|
|
201
205
|
this.server.listen(this.socketPath, () => {
|
|
202
206
|
log.info({ path: this.socketPath }, "Assistant IPC server listening");
|
|
203
207
|
});
|
|
208
|
+
|
|
209
|
+
this.watchdog.start();
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
/** Stop the server and disconnect all clients. */
|
|
207
213
|
stop(): void {
|
|
214
|
+
this.watchdog.stop();
|
|
215
|
+
|
|
208
216
|
for (const client of this.clients) {
|
|
209
217
|
if (!client.destroyed) client.destroy();
|
|
210
218
|
}
|
|
211
219
|
this.clients.clear();
|
|
212
220
|
|
|
221
|
+
for (const legacy of this.legacyServers) {
|
|
222
|
+
legacy.close();
|
|
223
|
+
}
|
|
224
|
+
this.legacyServers.clear();
|
|
225
|
+
|
|
213
226
|
if (this.server) {
|
|
214
227
|
this.server.close();
|
|
215
228
|
this.server = null;
|
|
@@ -229,8 +242,52 @@ export class AssistantIpcServer {
|
|
|
229
242
|
return this.socketPath;
|
|
230
243
|
}
|
|
231
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Re-bind the listening socket if its path entry is missing on disk.
|
|
247
|
+
*
|
|
248
|
+
* Public for tests so the watchdog can be exercised deterministically
|
|
249
|
+
* without waiting for the interval. Returns `true` when a re-bind was
|
|
250
|
+
* performed, `false` otherwise.
|
|
251
|
+
*/
|
|
252
|
+
async rebindIfMissing(): Promise<boolean> {
|
|
253
|
+
return this.watchdog.rebindIfMissing();
|
|
254
|
+
}
|
|
255
|
+
|
|
232
256
|
// ── Internal ──────────────────────────────────────────────────────────
|
|
233
257
|
|
|
258
|
+
private createListeningServer(): Server {
|
|
259
|
+
const server = createServer((socket) => {
|
|
260
|
+
this.clients.add(socket);
|
|
261
|
+
log.debug("IPC client connected");
|
|
262
|
+
|
|
263
|
+
const reader = new IpcFrameReader(
|
|
264
|
+
(envelope, binary) =>
|
|
265
|
+
this.handleEnvelope(socket, reader, envelope, binary),
|
|
266
|
+
(err) => log.warn({ err }, "IPC frame read error"),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
socket.on("data", (chunk) => {
|
|
270
|
+
reader.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
socket.on("close", () => {
|
|
274
|
+
this.clients.delete(socket);
|
|
275
|
+
log.debug("IPC client disconnected");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
socket.on("error", (err) => {
|
|
279
|
+
log.warn({ err }, "IPC client socket error");
|
|
280
|
+
this.clients.delete(socket);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
server.on("error", (err) => {
|
|
285
|
+
log.error({ err }, "Assistant IPC server error");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return server;
|
|
289
|
+
}
|
|
290
|
+
|
|
234
291
|
private handleEnvelope(
|
|
235
292
|
socket: Socket,
|
|
236
293
|
reader: IpcFrameReader,
|