@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.
Files changed (169) hide show
  1. package/ARCHITECTURE.md +29 -28
  2. package/Dockerfile +1 -0
  3. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  4. package/bun.lock +3 -0
  5. package/knip.json +1 -0
  6. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  7. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  8. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  9. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  11. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  12. package/openapi.yaml +22 -4
  13. package/package.json +3 -1
  14. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  15. package/src/__tests__/approval-cascade.test.ts +8 -16
  16. package/src/__tests__/approval-routes-http.test.ts +6 -0
  17. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  18. package/src/__tests__/call-constants.test.ts +10 -1
  19. package/src/__tests__/call-controller.test.ts +127 -0
  20. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
  21. package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
  22. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  23. package/src/__tests__/context-search-pkb-source.test.ts +12 -6
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  27. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  28. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  29. package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -6
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  32. package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
  33. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  34. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  35. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  37. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  38. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  39. package/src/__tests__/filing-service.test.ts +2 -19
  40. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
  41. package/src/__tests__/injector-chain.test.ts +24 -16
  42. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  43. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
  44. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  45. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  46. package/src/__tests__/oauth-cli.test.ts +121 -0
  47. package/src/__tests__/relay-server.test.ts +46 -2
  48. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  49. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  50. package/src/__tests__/secret-response-routing.test.ts +7 -5
  51. package/src/__tests__/server-history-render.test.ts +82 -0
  52. package/src/__tests__/skill-include-graph.test.ts +31 -0
  53. package/src/__tests__/skill-load-tool.test.ts +44 -16
  54. package/src/__tests__/skills.test.ts +39 -0
  55. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  56. package/src/__tests__/tool-executor.test.ts +155 -0
  57. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  58. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
  59. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  60. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  61. package/src/agent/loop.ts +11 -0
  62. package/src/approvals/guardian-decision-primitive.ts +0 -13
  63. package/src/approvals/guardian-request-resolvers.ts +4 -32
  64. package/src/calls/call-constants.ts +5 -8
  65. package/src/calls/call-controller.ts +130 -67
  66. package/src/calls/relay-server.ts +7 -1
  67. package/src/calls/voice-session-bridge.ts +1 -1
  68. package/src/cli/commands/memory-v2.ts +7 -7
  69. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
  70. package/src/cli/commands/oauth/connect.ts +10 -52
  71. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  72. package/src/config/feature-flag-registry.json +1 -17
  73. package/src/config/loader.ts +72 -19
  74. package/src/config/schemas/memory-v2.ts +1 -1
  75. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
  76. package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
  77. package/src/daemon/conversation-agent-loop.ts +13 -10
  78. package/src/daemon/conversation-lifecycle.ts +22 -8
  79. package/src/daemon/conversation-surfaces.ts +16 -14
  80. package/src/daemon/conversation-tool-setup.ts +9 -5
  81. package/src/daemon/conversation.ts +1 -1
  82. package/src/daemon/handlers/shared.ts +26 -0
  83. package/src/daemon/host-bash-proxy.ts +1 -1
  84. package/src/daemon/host-browser-proxy.ts +1 -1
  85. package/src/daemon/host-cu-proxy.ts +1 -1
  86. package/src/daemon/host-file-proxy.ts +1 -1
  87. package/src/daemon/host-transfer-proxy.ts +2 -2
  88. package/src/daemon/lifecycle.ts +88 -73
  89. package/src/daemon/memory-v2-startup.ts +55 -14
  90. package/src/daemon/message-types/messages.ts +19 -1
  91. package/src/documents/document-store.ts +35 -1
  92. package/src/filing/filing-service.ts +2 -3
  93. package/src/heartbeat/heartbeat-service.ts +1 -1
  94. package/src/ipc/assistant-server.ts +93 -36
  95. package/src/ipc/skill-server.ts +99 -42
  96. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  97. package/src/memory/context-search/sources/memory-v2.ts +1 -17
  98. package/src/memory/context-search/sources/memory.ts +2 -2
  99. package/src/memory/context-search/sources/pkb.ts +2 -3
  100. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  101. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  102. package/src/memory/graph/conversation-graph-memory.ts +32 -9
  103. package/src/memory/graph/graph-search.test.ts +6 -5
  104. package/src/memory/graph/graph-search.ts +3 -4
  105. package/src/memory/graph/retriever.test.ts +12 -7
  106. package/src/memory/graph/retriever.ts +4 -5
  107. package/src/memory/graph/tool-handlers.ts +3 -4
  108. package/src/memory/graph/tools.ts +4 -4
  109. package/src/memory/indexer.ts +1 -2
  110. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
  111. package/src/memory/jobs/embed-concept-page.ts +223 -87
  112. package/src/memory/jobs-worker.ts +8 -4
  113. package/src/memory/pkb/pkb-search.test.ts +6 -5
  114. package/src/memory/pkb/pkb-search.ts +4 -5
  115. package/src/memory/qdrant-client.ts +3 -0
  116. package/src/memory/search/semantic.ts +4 -5
  117. package/src/memory/v2/__tests__/activation.test.ts +35 -5
  118. package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
  119. package/src/memory/v2/__tests__/injection.test.ts +140 -23
  120. package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
  121. package/src/memory/v2/__tests__/sim.test.ts +118 -7
  122. package/src/memory/v2/__tests__/static-context.test.ts +1 -13
  123. package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
  124. package/src/memory/v2/consolidation-job.ts +7 -8
  125. package/src/memory/v2/injection.ts +32 -12
  126. package/src/memory/v2/page-store.ts +39 -0
  127. package/src/memory/v2/prompts/consolidation.ts +5 -0
  128. package/src/memory/v2/qdrant.ts +209 -48
  129. package/src/memory/v2/sim.ts +67 -26
  130. package/src/memory/v2/static-context.ts +4 -8
  131. package/src/memory/v2/sweep-job.ts +5 -6
  132. package/src/memory/v2/types.ts +7 -0
  133. package/src/notifications/copy-composer.ts +46 -12
  134. package/src/notifications/decision-engine.ts +46 -0
  135. package/src/permissions/gateway-threshold-reader.ts +116 -8
  136. package/src/permissions/prompter.ts +86 -96
  137. package/src/permissions/secret-prompter.ts +31 -31
  138. package/src/plugins/defaults/injectors.ts +1 -2
  139. package/src/proactive-artifact/job.test.ts +51 -4
  140. package/src/proactive-artifact/job.ts +16 -2
  141. package/src/proactive-artifact/message-copy.ts +18 -1
  142. package/src/prompts/templates/SOUL.md +13 -28
  143. package/src/runtime/auth/route-policy.ts +1 -0
  144. package/src/runtime/channel-approvals.ts +3 -2
  145. package/src/runtime/guardian-reply-router.ts +0 -10
  146. package/src/runtime/pending-interactions.ts +19 -15
  147. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  148. package/src/runtime/routes/approval-routes.ts +7 -3
  149. package/src/runtime/routes/consolidation-routes.ts +8 -9
  150. package/src/runtime/routes/conversation-query-routes.ts +44 -1
  151. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  152. package/src/runtime/routes/filing-routes.ts +2 -3
  153. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
  154. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  155. package/src/runtime/routes/memory-item-routes.ts +5 -6
  156. package/src/runtime/routes/memory-v2-routes.ts +103 -17
  157. package/src/skills/include-graph.ts +35 -13
  158. package/src/tools/document/document-tool.ts +20 -0
  159. package/src/tools/executor.ts +18 -2
  160. package/src/tools/memory/register.test.ts +7 -5
  161. package/src/tools/permission-checker.ts +15 -0
  162. package/src/tools/skills/load.ts +24 -20
  163. package/src/tools/tool-name-aliases.ts +19 -0
  164. package/src/tools/types.ts +19 -1
  165. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  166. package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
  167. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  168. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  169. package/src/workspace/migrations/registry.ts +6 -0
@@ -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 { maybeSeedMemoryV2Skills } from "./memory-v2-startup.js";
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
- // subsequent sync isAssistantFeatureFlagEnabled() calls have data.
323
- // Fired non-blocking so a slow or unreachable gateway doesn't delay
324
- // daemon startup (the IPC call has a 3s connect + 5s call timeout
325
- // that would otherwise stall the critical path).
326
- //
327
- // On resolve, retry the v2 skill seed: the synchronous gate at the
328
- // skill-seed call site below evaluates the memory-v2-enabled flag
329
- // before the gateway has populated overrides, so a cold-boot race
330
- // can leave the v2 skill collection unseeded for the lifetime of
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
- try {
745
- const embeddingSelection = await selectEmbeddingBackend(config);
746
- // Sentinel only encodes the dense provider+model identity; sparse
747
- // encoder changes never require collection recreation, so they
748
- // intentionally do not contribute to the v1 collection identity.
749
- const embeddingModel = embeddingSelection.backend
750
- ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}`
751
- : undefined;
752
- const qdrantClient = initQdrantClient({
753
- url: qdrantUrl,
754
- collection: config.memory.qdrant.collection,
755
- vectorSize: config.memory.qdrant.vectorSize,
756
- onDisk: config.memory.qdrant.onDisk,
757
- quantization: config.memory.qdrant.quantization,
758
- embeddingModel,
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
- // Eagerly ensure the collection exists so we detect migrations
762
- // (unnamed→named vectors, dimension/model changes) at startup.
763
- // If a destructive migration occurred, enqueue a rebuild_index job
764
- // to re-embed all memory items from the SQLite cache.
765
- const { migrated } = await qdrantClient.ensureCollection();
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
- log.info("Qdrant vector store initialized");
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
- "Qdrant client initialization failed memory features will be degraded",
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. Kept
782
- // inside the `qdrantStarted` guard so we don't call
783
- // `getQdrantClient()` (which throws "not initialized") on every
784
- // startup when Qdrant is unavailable. Fire-and-forget so enqueued
785
- // re-index jobs drain in the background and first-turn latency
786
- // stays unaffected.
787
- void (async () => {
788
- try {
789
- const { reconcilePkbIndex } =
790
- await import("../memory/pkb/pkb-reconcile.js");
791
- const { PKB_WORKSPACE_SCOPE } =
792
- await import("../memory/pkb/types.js");
793
- const pkbRoot = join(getWorkspaceDir(), "pkb");
794
- await reconcilePkbIndex(pkbRoot, PKB_WORKSPACE_SCOPE);
795
- } catch (err) {
796
- log.warn(
797
- { err },
798
- "PKB index reconciliation failed — continuing startup",
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 the flag is on
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 = isAssistantFeatureFlagEnabled(
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. Gated on both the `memory-v2-enabled` feature flag and the
20
- * workspace-level `config.memory.v2.enabled` switch so v2 modules stay out of
21
- * the v1 startup path when v2 is off.
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
- /** Scope options ladder for the rule editor modal (narrowest to broadest). */
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 (isAssistantFeatureFlagEnabled("memory-v2-enabled", fullConfig)) {
119
- log.info("Filing service disabled — memory v2 flag is set");
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, mkdirSync, unlinkSync } from "node:fs";
31
+ import { existsSync, unlinkSync } from "node:fs";
32
32
  import { createServer, type Server, type Socket } from "node:net";
33
- import { dirname } from "node:path";
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
- const socketDir = dirname(this.socketPath);
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 = createServer((socket) => {
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,