@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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the memory v2 route handlers in `memory-v2-routes.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Currently focused on `memory_v2_list_concept_pages`:
|
|
5
|
+
* - empty workspace → returns no pages
|
|
6
|
+
* - populated workspace → surfaces slug, bodyBytes, edgeCount, updatedAtMs
|
|
7
|
+
* - corrupt page on disk → logged-and-skipped, does not poison listing
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import { writePage } from "../../../memory/v2/page-store.js";
|
|
17
|
+
import type { ConceptPage } from "../../../memory/v2/types.js";
|
|
18
|
+
import type { MemoryV2ListConceptPagesResult } from "../memory-v2-routes.js";
|
|
19
|
+
import { ROUTES } from "../memory-v2-routes.js";
|
|
20
|
+
import type { RouteDefinition } from "../types.js";
|
|
21
|
+
|
|
22
|
+
// ─── Setup ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
let workspaceDir: string;
|
|
25
|
+
let origWorkspaceDir: string | undefined;
|
|
26
|
+
|
|
27
|
+
function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
28
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
29
|
+
if (!route) throw new Error(`Route ${operationId} not found`);
|
|
30
|
+
return route.handler;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-memv2-list-"));
|
|
35
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
36
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
if (origWorkspaceDir === undefined) {
|
|
41
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
42
|
+
} else {
|
|
43
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
47
|
+
} catch {
|
|
48
|
+
// best-effort cleanup
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("memory_v2_list_concept_pages handler", () => {
|
|
55
|
+
test("returns empty list for an empty workspace", async () => {
|
|
56
|
+
const handler = findHandler("memory_v2_list_concept_pages");
|
|
57
|
+
const result = (await handler({
|
|
58
|
+
body: {},
|
|
59
|
+
})) as MemoryV2ListConceptPagesResult;
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({ pages: [] });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("returns slugs, body bytes, edge counts, and mtimes for populated workspace", async () => {
|
|
65
|
+
const before = Date.now();
|
|
66
|
+
|
|
67
|
+
const pages: ConceptPage[] = [
|
|
68
|
+
{
|
|
69
|
+
slug: "alice",
|
|
70
|
+
frontmatter: { edges: ["bob", "carol"], ref_files: [] },
|
|
71
|
+
body: "Alice prefers VS Code.\n",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
slug: "bob",
|
|
75
|
+
frontmatter: { edges: [], ref_files: [] },
|
|
76
|
+
body: "Bob ships at end of day.\nLikes async standups.\n",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
slug: "people/carol",
|
|
80
|
+
frontmatter: { edges: ["alice"], ref_files: [] },
|
|
81
|
+
body: "Carol leads the platform team.\n",
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
for (const page of pages) {
|
|
85
|
+
await writePage(workspaceDir, page);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handler = findHandler("memory_v2_list_concept_pages");
|
|
89
|
+
const result = (await handler({
|
|
90
|
+
body: {},
|
|
91
|
+
})) as MemoryV2ListConceptPagesResult;
|
|
92
|
+
|
|
93
|
+
expect(result.pages).toHaveLength(3);
|
|
94
|
+
|
|
95
|
+
const bySlug = new Map(result.pages.map((p) => [p.slug, p]));
|
|
96
|
+
|
|
97
|
+
const alice = bySlug.get("alice");
|
|
98
|
+
expect(alice).toBeDefined();
|
|
99
|
+
expect(alice!.bodyBytes).toBe(Buffer.byteLength(pages[0]!.body, "utf8"));
|
|
100
|
+
expect(alice!.edgeCount).toBe(2);
|
|
101
|
+
expect(alice!.updatedAtMs).toBeGreaterThanOrEqual(before);
|
|
102
|
+
// updatedAtMs must be an integer on the wire — Swift clients decode it as
|
|
103
|
+
// Int64 and a sub-millisecond float (which fs.Stats.mtimeMs returns by
|
|
104
|
+
// default) breaks JSONDecoder strict number parsing.
|
|
105
|
+
expect(Number.isInteger(alice!.updatedAtMs)).toBe(true);
|
|
106
|
+
|
|
107
|
+
const bob = bySlug.get("bob");
|
|
108
|
+
expect(bob).toBeDefined();
|
|
109
|
+
expect(bob!.bodyBytes).toBe(Buffer.byteLength(pages[1]!.body, "utf8"));
|
|
110
|
+
expect(bob!.edgeCount).toBe(0);
|
|
111
|
+
expect(bob!.updatedAtMs).toBeGreaterThanOrEqual(before);
|
|
112
|
+
expect(Number.isInteger(bob!.updatedAtMs)).toBe(true);
|
|
113
|
+
|
|
114
|
+
const carol = bySlug.get("people/carol");
|
|
115
|
+
expect(carol).toBeDefined();
|
|
116
|
+
expect(carol!.bodyBytes).toBe(Buffer.byteLength(pages[2]!.body, "utf8"));
|
|
117
|
+
expect(carol!.edgeCount).toBe(1);
|
|
118
|
+
expect(carol!.updatedAtMs).toBeGreaterThanOrEqual(before);
|
|
119
|
+
expect(Number.isInteger(carol!.updatedAtMs)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("tolerates a single corrupt page — returns valid pages and skips the broken one", async () => {
|
|
123
|
+
await writePage(workspaceDir, {
|
|
124
|
+
slug: "valid-page",
|
|
125
|
+
frontmatter: { edges: [], ref_files: [] },
|
|
126
|
+
body: "Body of the valid page.\n",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// A `.md` file with frontmatter that fails schema validation — `edges`
|
|
130
|
+
// must be a list of strings, not a single number — so `readPage` throws.
|
|
131
|
+
const conceptsDir = join(workspaceDir, "memory", "concepts");
|
|
132
|
+
await mkdir(conceptsDir, { recursive: true });
|
|
133
|
+
await writeFile(
|
|
134
|
+
join(conceptsDir, "broken.md"),
|
|
135
|
+
"---\nedges: 42\n---\nbroken body\n",
|
|
136
|
+
"utf-8",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const handler = findHandler("memory_v2_list_concept_pages");
|
|
140
|
+
const result = (await handler({
|
|
141
|
+
body: {},
|
|
142
|
+
})) as MemoryV2ListConceptPagesResult;
|
|
143
|
+
|
|
144
|
+
expect(result.pages).toHaveLength(1);
|
|
145
|
+
expect(result.pages.map((p) => p.slug)).toEqual(["valid-page"]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -62,8 +62,9 @@ function handleConfirm({ body }: RouteHandlerArgs) {
|
|
|
62
62
|
throw new BadRequestError("decision must resolve to allow or deny");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Validation passed —
|
|
66
|
-
|
|
65
|
+
// Validation passed. Use get() here — the prompter (or ACP directResolve path)
|
|
66
|
+
// owns deregistration via pendingInteractions.resolve().
|
|
67
|
+
const interaction = peeked;
|
|
67
68
|
|
|
68
69
|
log.info(
|
|
69
70
|
{
|
|
@@ -93,7 +94,9 @@ function handleConfirm({ body }: RouteHandlerArgs) {
|
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
// ACP permissions: resolve directly without a Conversation object.
|
|
97
|
+
// No PermissionPrompter involved, so the route owns deregistration.
|
|
96
98
|
if (interaction.directResolve) {
|
|
99
|
+
pendingInteractions.resolve(requestId);
|
|
97
100
|
interaction.directResolve(effectiveDecision as UserDecision);
|
|
98
101
|
return { accepted: true };
|
|
99
102
|
}
|
|
@@ -139,7 +142,8 @@ function handleSecret({ body }: RouteHandlerArgs) {
|
|
|
139
142
|
throw new BadRequestError('delivery must be "store" or "transient_send"');
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
// Use get() — SecretPrompter.resolveSecret() owns deregistration.
|
|
146
|
+
const interaction = pendingInteractions.get(requestId);
|
|
143
147
|
if (!interaction) {
|
|
144
148
|
throw new NotFoundError("No pending interaction found for this requestId");
|
|
145
149
|
}
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* only surface its config and provide an on-demand trigger for the Settings UI.
|
|
9
9
|
*
|
|
10
10
|
* `available` mirrors the filing route's `available` field: it reflects which
|
|
11
|
-
* background memory job is active for this instance. When
|
|
12
|
-
* is
|
|
11
|
+
* background memory job is active for this instance. When
|
|
12
|
+
* `config.memory.v2.enabled` is false, consolidation returns
|
|
13
|
+
* `available: false` and the UI hides the row.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import { z } from "zod";
|
|
16
17
|
|
|
17
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
18
18
|
import { getConfig } from "../../config/loader.js";
|
|
19
19
|
import { getMemoryCheckpoint } from "../../memory/checkpoints.js";
|
|
20
20
|
import {
|
|
@@ -26,7 +26,7 @@ import { BadRequestError } from "./errors.js";
|
|
|
26
26
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
27
27
|
|
|
28
28
|
function isConsolidationAvailable(): boolean {
|
|
29
|
-
return
|
|
29
|
+
return getConfig().memory.v2.enabled;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function consolidationIntervalMs(): number {
|
|
@@ -66,14 +66,13 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
66
66
|
success: z.boolean(),
|
|
67
67
|
}),
|
|
68
68
|
handler: async (_args: RouteHandlerArgs) => {
|
|
69
|
-
const
|
|
70
|
-
const v2Config = getConfig().memory.v2;
|
|
69
|
+
const enabled = getConfig().memory.v2.enabled;
|
|
71
70
|
const intervalMs = consolidationIntervalMs();
|
|
72
71
|
const lastRunAt = readLastRunAt();
|
|
73
72
|
const nextRunAt = lastRunAt != null ? lastRunAt + intervalMs : null;
|
|
74
73
|
return {
|
|
75
|
-
available,
|
|
76
|
-
enabled
|
|
74
|
+
available: enabled,
|
|
75
|
+
enabled,
|
|
77
76
|
intervalMs,
|
|
78
77
|
nextRunAt,
|
|
79
78
|
lastRunAt,
|
|
@@ -99,7 +98,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
99
98
|
handler: async (_args: RouteHandlerArgs) => {
|
|
100
99
|
if (!isConsolidationAvailable()) {
|
|
101
100
|
throw new BadRequestError(
|
|
102
|
-
"Consolidation is not available (memory
|
|
101
|
+
"Consolidation is not available (memory.v2.enabled is false)",
|
|
103
102
|
);
|
|
104
103
|
}
|
|
105
104
|
// Coalesce: don't pile up duplicate jobs if the worker hasn't picked up
|
|
@@ -22,7 +22,9 @@ import { z } from "zod";
|
|
|
22
22
|
|
|
23
23
|
import {
|
|
24
24
|
deepMergeOverwrite,
|
|
25
|
+
fillContextDefaultsForMissingKeys,
|
|
25
26
|
getConfig,
|
|
27
|
+
getDeploymentContextDefaults,
|
|
26
28
|
invalidateConfigCache,
|
|
27
29
|
loadRawConfig,
|
|
28
30
|
saveRawConfig,
|
|
@@ -312,9 +314,50 @@ async function handleSetEmbeddingConfig({ body }: RouteHandlerArgs) {
|
|
|
312
314
|
}
|
|
313
315
|
}
|
|
314
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Apply deployment-context defaults to a raw config payload before it goes
|
|
319
|
+
* out over the wire from `GET /v1/config`. The in-memory `loadConfig()`
|
|
320
|
+
* already layers these defaults for daemon-internal consumers; the GET
|
|
321
|
+
* response needs the same treatment so external clients (macOS, web, CLI)
|
|
322
|
+
* see the effective value rather than `undefined` when the daemon hasn't
|
|
323
|
+
* persisted an explicit choice yet. Without this, macOS's
|
|
324
|
+
* `loadServiceModes(config:)` short-circuits when `services.inference.mode`
|
|
325
|
+
* is missing and falls back to the SwiftUI `@Published` default of
|
|
326
|
+
* "your-own", which renders the wrong segment selection on freshly-hatched
|
|
327
|
+
* platform-managed assistants.
|
|
328
|
+
*
|
|
329
|
+
* Guards against `loadRawConfig()` handing us a value that is technically
|
|
330
|
+
* valid JSON but not a plain object (e.g. literal `null`, a number, or an
|
|
331
|
+
* array). `loadRawConfig` is typed `Record<string, unknown>` but `JSON.parse`
|
|
332
|
+
* itself doesn't enforce that — a malformed-but-parseable `config.json`
|
|
333
|
+
* would blow up `fillContextDefaultsForMissingKeys` on its `target[key]` /
|
|
334
|
+
* `fileConfig[key]` accesses, turning `GET /v1/config` into a 500 where it
|
|
335
|
+
* used to succeed (returning the malformed payload as-is). When `raw` is
|
|
336
|
+
* not a plain object, we return it unchanged.
|
|
337
|
+
*
|
|
338
|
+
* Exported for direct unit testing.
|
|
339
|
+
*/
|
|
340
|
+
export function applyContextDefaultsToRawConfig(raw: unknown): unknown {
|
|
341
|
+
const contextDefaults = getDeploymentContextDefaults();
|
|
342
|
+
if (
|
|
343
|
+
Object.keys(contextDefaults).length === 0 ||
|
|
344
|
+
raw === null ||
|
|
345
|
+
typeof raw !== "object" ||
|
|
346
|
+
Array.isArray(raw)
|
|
347
|
+
) {
|
|
348
|
+
return raw;
|
|
349
|
+
}
|
|
350
|
+
fillContextDefaultsForMissingKeys(
|
|
351
|
+
raw as Record<string, unknown>,
|
|
352
|
+
raw as Record<string, unknown>,
|
|
353
|
+
contextDefaults,
|
|
354
|
+
);
|
|
355
|
+
return raw;
|
|
356
|
+
}
|
|
357
|
+
|
|
315
358
|
function handleGetConfig() {
|
|
316
359
|
try {
|
|
317
|
-
return loadRawConfig();
|
|
360
|
+
return applyContextDefaultsToRawConfig(loadRawConfig());
|
|
318
361
|
} catch (err) {
|
|
319
362
|
const message = err instanceof Error ? err.message : String(err);
|
|
320
363
|
throw new InternalError(`Failed to read config: ${message}`);
|
|
@@ -15,6 +15,7 @@ import { spawn } from "node:child_process";
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
17
|
import { getIsContainerized } from "../../config/env-registry.js";
|
|
18
|
+
import { buildSanitizedEnv } from "../../tools/terminal/safe-env.js";
|
|
18
19
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
19
20
|
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
20
21
|
|
|
@@ -92,6 +93,7 @@ function handleDebugBash({ body }: RouteHandlerArgs): Promise<DebugBashResult> {
|
|
|
92
93
|
cwd: getWorkspaceDir(),
|
|
93
94
|
stdio: ["ignore", "pipe", "pipe"],
|
|
94
95
|
detached: true,
|
|
96
|
+
env: buildSanitizedEnv(),
|
|
95
97
|
});
|
|
96
98
|
|
|
97
99
|
const timer = setTimeout(() => {
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* Route handlers for filing management.
|
|
3
3
|
*
|
|
4
4
|
* `available` reflects whether the filing service is the active background
|
|
5
|
-
* memory job for this instance. When
|
|
5
|
+
* memory job for this instance. When `config.memory.v2.enabled` is true,
|
|
6
6
|
* filing yields to the consolidation job (see consolidation-routes.ts) and
|
|
7
7
|
* returns `available: false` so the UI can hide the row.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
13
12
|
import { getConfig } from "../../config/loader.js";
|
|
14
13
|
import { FilingService } from "../../filing/filing-service.js";
|
|
15
14
|
import { getLogger } from "../../util/logger.js";
|
|
@@ -19,7 +18,7 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
|
19
18
|
const log = getLogger("filing-routes");
|
|
20
19
|
|
|
21
20
|
function isFilingAvailable(): boolean {
|
|
22
|
-
return !
|
|
21
|
+
return !getConfig().memory.v2.enabled;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
// ---------------------------------------------------------------------------
|
|
@@ -174,9 +174,6 @@ export async function handleGuardianReplyIntercept(
|
|
|
174
174
|
eventId,
|
|
175
175
|
canonicalRouter: routerResult.type,
|
|
176
176
|
requestId: routerResult.requestId,
|
|
177
|
-
...(routerResult.activatedContact
|
|
178
|
-
? { activatedContact: routerResult.activatedContact }
|
|
179
|
-
: {}),
|
|
180
177
|
}),
|
|
181
178
|
skipApprovalInterception: false,
|
|
182
179
|
};
|
|
@@ -6,13 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
8
8
|
|
|
9
|
-
import { _setOverridesForTesting } from "../../config/assistant-feature-flags.js";
|
|
10
|
-
|
|
11
|
-
// This test exercises v1 memory CRUD routes. The `memory-v2-enabled` flag
|
|
12
|
-
// (registry default `true`) flips memory routing to v2 — disable it here so
|
|
13
|
-
// the v1 paths under test stay active.
|
|
14
|
-
_setOverridesForTesting({ "memory-v2-enabled": false });
|
|
15
|
-
|
|
16
9
|
mock.module("../../util/logger.js", () => ({
|
|
17
10
|
getLogger: () =>
|
|
18
11
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -20,7 +13,8 @@ mock.module("../../util/logger.js", () => ({
|
|
|
20
13
|
}),
|
|
21
14
|
}));
|
|
22
15
|
|
|
23
|
-
// Stub config loader —
|
|
16
|
+
// Stub config loader — return a config with memory.v2.enabled=false so the
|
|
17
|
+
// v1 paths under test stay active.
|
|
24
18
|
mock.module("../../config/loader.js", () => ({
|
|
25
19
|
loadConfig: () => mockConfig,
|
|
26
20
|
getConfig: () => mockConfig,
|
|
@@ -28,7 +22,7 @@ mock.module("../../config/loader.js", () => ({
|
|
|
28
22
|
}));
|
|
29
23
|
|
|
30
24
|
// ── Controllable mocks for semantic search ─────────────────────────────
|
|
31
|
-
const mockConfig: unknown = {};
|
|
25
|
+
const mockConfig: unknown = { memory: { v2: { enabled: false } } };
|
|
32
26
|
|
|
33
27
|
let mockBackendStatus: {
|
|
34
28
|
enabled: boolean;
|
|
@@ -25,7 +25,6 @@ import {
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
|
|
27
27
|
import { getConfig } from "../../config/loader.js";
|
|
28
|
-
import { isMemoryV2ReadActive } from "../../memory/context-search/sources/memory-v2.js";
|
|
29
28
|
import { getDb } from "../../memory/db-connection.js";
|
|
30
29
|
import {
|
|
31
30
|
embedWithBackend,
|
|
@@ -176,11 +175,11 @@ async function searchNodesSemantic(
|
|
|
176
175
|
): Promise<{ ids: string[]; total: number } | null> {
|
|
177
176
|
try {
|
|
178
177
|
const config = getConfig();
|
|
179
|
-
// v2 owns the read path when
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
if (
|
|
178
|
+
// v2 owns the read path when enabled. Fall back to SQL search (the
|
|
179
|
+
// caller's `null` branch) instead of querying the v1 collection, which
|
|
180
|
+
// is in active retirement and a corrupted sparse segment can OOM-crash
|
|
181
|
+
// the shared Qdrant process.
|
|
182
|
+
if (config.memory.v2.enabled) return null;
|
|
184
183
|
const backendStatus = await getMemoryBackendStatus(config);
|
|
185
184
|
if (!backendStatus.provider) return null;
|
|
186
185
|
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Migrated from `ipc/routes/memory-v2-backfill.ts` and
|
|
5
5
|
* `ipc/routes/memory-v2-validate.ts` into the shared ROUTES array.
|
|
6
6
|
*/
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
7
10
|
import { z } from "zod";
|
|
8
11
|
|
|
9
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
10
12
|
import { loadConfig } from "../../config/loader.js";
|
|
11
13
|
import {
|
|
12
14
|
applyCorrectionIfCalibrated,
|
|
@@ -32,6 +34,7 @@ import {
|
|
|
32
34
|
validateEdgeTargets,
|
|
33
35
|
} from "../../memory/v2/edge-index.js";
|
|
34
36
|
import {
|
|
37
|
+
getConceptsDir,
|
|
35
38
|
listPages,
|
|
36
39
|
readPage,
|
|
37
40
|
renderPageContent,
|
|
@@ -47,11 +50,37 @@ import {
|
|
|
47
50
|
getConceptPageCorpusStats,
|
|
48
51
|
rebuildConceptPageCorpusStats,
|
|
49
52
|
} from "../../memory/v2/sparse-bm25.js";
|
|
53
|
+
import { getLogger } from "../../util/logger.js";
|
|
50
54
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
51
55
|
import { RouteError } from "./errors.js";
|
|
52
56
|
import type { RouteDefinition } from "./types.js";
|
|
53
57
|
import type { RouteHandlerArgs } from "./types.js";
|
|
54
58
|
|
|
59
|
+
const log = getLogger("memory-v2-routes");
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Wire-format error code emitted when v2 routes reject a request because
|
|
63
|
+
* `memory.v2.enabled` is false. Exported so tests and the macOS client can
|
|
64
|
+
* reference the same string without drift.
|
|
65
|
+
*/
|
|
66
|
+
export const MEMORY_V2_DISABLED_CODE = "MEMORY_V2_DISABLED";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reject the request when memory v2 is not active. Returning 409 (rather
|
|
70
|
+
* than serving a partial response) keeps clients honest — the desktop
|
|
71
|
+
* Memories panel reads this code to render an explicit "disabled in
|
|
72
|
+
* config" empty state.
|
|
73
|
+
*/
|
|
74
|
+
function requireMemoryV2Enabled(): void {
|
|
75
|
+
if (!loadConfig().memory.v2.enabled) {
|
|
76
|
+
throw new RouteError(
|
|
77
|
+
"Memory v2 is not enabled — set memory.v2.enabled to true to use this command.",
|
|
78
|
+
MEMORY_V2_DISABLED_CODE,
|
|
79
|
+
409,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
// ── Backfill ────────────────────────────────────────────────────────────
|
|
56
85
|
|
|
57
86
|
const MemoryV2BackfillParams = z
|
|
@@ -76,6 +105,7 @@ const OP_TO_JOB_TYPE: Record<MemoryV2BackfillOp, MemoryJobType> = {
|
|
|
76
105
|
async function handleBackfill({
|
|
77
106
|
body = {},
|
|
78
107
|
}: RouteHandlerArgs): Promise<MemoryV2BackfillResult> {
|
|
108
|
+
requireMemoryV2Enabled();
|
|
79
109
|
const { op, force } = MemoryV2BackfillParams.parse(body);
|
|
80
110
|
const payload: Record<string, unknown> =
|
|
81
111
|
op === "migrate" && force === true ? { force: true } : {};
|
|
@@ -102,6 +132,7 @@ export type MemoryV2ValidateResult = {
|
|
|
102
132
|
async function handleValidate({
|
|
103
133
|
body = {},
|
|
104
134
|
}: RouteHandlerArgs): Promise<MemoryV2ValidateResult> {
|
|
135
|
+
requireMemoryV2Enabled();
|
|
105
136
|
MemoryV2ValidateParams.parse(body);
|
|
106
137
|
|
|
107
138
|
const workspaceDir = getWorkspaceDir();
|
|
@@ -158,6 +189,7 @@ export type MemoryV2GetConceptPageResult = {
|
|
|
158
189
|
async function handleGetConceptPage({
|
|
159
190
|
body = {},
|
|
160
191
|
}: RouteHandlerArgs): Promise<MemoryV2GetConceptPageResult> {
|
|
192
|
+
requireMemoryV2Enabled();
|
|
161
193
|
const { slug } = MemoryV2GetConceptPageParams.parse(body);
|
|
162
194
|
const workspaceDir = getWorkspaceDir();
|
|
163
195
|
let page;
|
|
@@ -180,6 +212,59 @@ async function handleGetConceptPage({
|
|
|
180
212
|
return { slug, rendered: renderPageContent(page) };
|
|
181
213
|
}
|
|
182
214
|
|
|
215
|
+
// ── List concept pages ──────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
const MemoryV2ListConceptPagesParams = z.object({}).strict();
|
|
218
|
+
|
|
219
|
+
export type MemoryV2ListConceptPagesResult = {
|
|
220
|
+
pages: Array<{
|
|
221
|
+
slug: string;
|
|
222
|
+
bodyBytes: number;
|
|
223
|
+
edgeCount: number;
|
|
224
|
+
updatedAtMs: number;
|
|
225
|
+
}>;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
async function handleListConceptPages({
|
|
229
|
+
body = {},
|
|
230
|
+
}: RouteHandlerArgs): Promise<MemoryV2ListConceptPagesResult> {
|
|
231
|
+
requireMemoryV2Enabled();
|
|
232
|
+
MemoryV2ListConceptPagesParams.parse(body);
|
|
233
|
+
|
|
234
|
+
const workspaceDir = getWorkspaceDir();
|
|
235
|
+
const conceptsDir = getConceptsDir(workspaceDir);
|
|
236
|
+
const slugs = await listPages(workspaceDir);
|
|
237
|
+
|
|
238
|
+
const settled = await Promise.all(
|
|
239
|
+
slugs.map(async (slug) => {
|
|
240
|
+
try {
|
|
241
|
+
const page = await readPage(workspaceDir, slug);
|
|
242
|
+
if (!page) return null;
|
|
243
|
+
const stats = await stat(join(conceptsDir, `${slug}.md`));
|
|
244
|
+
return {
|
|
245
|
+
slug,
|
|
246
|
+
bodyBytes: Buffer.byteLength(page.body, "utf8"),
|
|
247
|
+
edgeCount: page.frontmatter.edges.length,
|
|
248
|
+
updatedAtMs: Math.floor(stats.mtimeMs),
|
|
249
|
+
};
|
|
250
|
+
} catch (err) {
|
|
251
|
+
// A single corrupt page (bad YAML, schema mismatch, etc.) shouldn't
|
|
252
|
+
// poison the whole listing — the validate route is the place to
|
|
253
|
+
// surface those; this one is read-only and best-effort.
|
|
254
|
+
log.warn(
|
|
255
|
+
`Skipping concept page '${slug}' in list-concept-pages: ${err instanceof Error ? err.message : String(err)}`,
|
|
256
|
+
);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
const pages = settled.filter(
|
|
262
|
+
(p): p is MemoryV2ListConceptPagesResult["pages"][number] => p !== null,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
return { pages };
|
|
266
|
+
}
|
|
267
|
+
|
|
183
268
|
// ── Rebuild BM25 corpus stats ───────────────────────────────────────────
|
|
184
269
|
|
|
185
270
|
const MemoryV2RebuildCorpusStatsParams = z.object({}).strict();
|
|
@@ -194,6 +279,7 @@ export interface MemoryV2RebuildCorpusStatsResult {
|
|
|
194
279
|
async function handleRebuildCorpusStats({
|
|
195
280
|
body = {},
|
|
196
281
|
}: RouteHandlerArgs): Promise<MemoryV2RebuildCorpusStatsResult> {
|
|
282
|
+
requireMemoryV2Enabled();
|
|
197
283
|
MemoryV2RebuildCorpusStatsParams.parse(body);
|
|
198
284
|
const workspaceDir = getWorkspaceDir();
|
|
199
285
|
await rebuildConceptPageCorpusStats(workspaceDir);
|
|
@@ -225,23 +311,9 @@ export type MemoryV2ReembedSkillsResult = {
|
|
|
225
311
|
async function handleReembedSkills({
|
|
226
312
|
body = {},
|
|
227
313
|
}: RouteHandlerArgs): Promise<MemoryV2ReembedSkillsResult> {
|
|
314
|
+
requireMemoryV2Enabled();
|
|
228
315
|
MemoryV2ReembedSkillsParams.parse(body);
|
|
229
316
|
|
|
230
|
-
// Gate the route on both the feature flag and the per-workspace config
|
|
231
|
-
// toggle so the v2 skill collection never gets re-seeded against a
|
|
232
|
-
// workspace whose v2 subsystem is intentionally off.
|
|
233
|
-
const config = loadConfig();
|
|
234
|
-
if (
|
|
235
|
-
!isAssistantFeatureFlagEnabled("memory-v2-enabled", config) ||
|
|
236
|
-
!config.memory.v2.enabled
|
|
237
|
-
) {
|
|
238
|
-
throw new RouteError(
|
|
239
|
-
"Memory v2 is not enabled — flip both the memory-v2-enabled feature flag and memory.v2.enabled to use this command.",
|
|
240
|
-
"MEMORY_V2_DISABLED",
|
|
241
|
-
409,
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
317
|
// Unlike the queued backfill jobs above, this is a CLI-driven sync
|
|
246
318
|
// request: the operator wants the cache replaced before the next prompt
|
|
247
319
|
// assembly, so we await the seed inline rather than enqueueing it.
|
|
@@ -416,6 +488,7 @@ async function scoreChannel(
|
|
|
416
488
|
async function handleExplainSimilarity({
|
|
417
489
|
body = {},
|
|
418
490
|
}: RouteHandlerArgs): Promise<MemoryV2ExplainSimilarityResult> {
|
|
491
|
+
requireMemoryV2Enabled();
|
|
419
492
|
const params = MemoryV2ExplainSimilarityParams.parse(body);
|
|
420
493
|
const config = loadConfig();
|
|
421
494
|
const { dense_weight: denseWeight, sparse_weight: sparseWeight } =
|
|
@@ -475,6 +548,7 @@ const MemoryV2ConceptFrequencyParams = z
|
|
|
475
548
|
async function handleConceptFrequency({
|
|
476
549
|
body = {},
|
|
477
550
|
}: RouteHandlerArgs): Promise<ConceptFrequencyResponse> {
|
|
551
|
+
requireMemoryV2Enabled();
|
|
478
552
|
const { conversationId, sinceMs } =
|
|
479
553
|
MemoryV2ConceptFrequencyParams.parse(body);
|
|
480
554
|
const workspaceDir = getWorkspaceDir();
|
|
@@ -517,6 +591,7 @@ export interface MemoryV2FitAnisotropyResult {
|
|
|
517
591
|
async function handleFitAnisotropy({
|
|
518
592
|
body = {},
|
|
519
593
|
}: RouteHandlerArgs): Promise<MemoryV2FitAnisotropyResult> {
|
|
594
|
+
requireMemoryV2Enabled();
|
|
520
595
|
const { k, sample } = MemoryV2FitAnisotropyParams.parse(body);
|
|
521
596
|
const config = loadConfig();
|
|
522
597
|
|
|
@@ -603,6 +678,17 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
603
678
|
tags: ["memory"],
|
|
604
679
|
requestBody: MemoryV2GetConceptPageParams,
|
|
605
680
|
},
|
|
681
|
+
{
|
|
682
|
+
operationId: "memory_v2_list_concept_pages",
|
|
683
|
+
method: "POST",
|
|
684
|
+
endpoint: "memory/v2/list-concept-pages",
|
|
685
|
+
handler: handleListConceptPages,
|
|
686
|
+
summary: "List all memory v2 concept pages with metadata",
|
|
687
|
+
description:
|
|
688
|
+
"Returns slugs, body sizes, edge counts, and last-modified timestamps for every concept page on disk. Read-only; used by the desktop About → Memories surface to render a browse-able list.",
|
|
689
|
+
tags: ["memory"],
|
|
690
|
+
requestBody: MemoryV2ListConceptPagesParams,
|
|
691
|
+
},
|
|
606
692
|
{
|
|
607
693
|
operationId: "memory_v2_reembed_skills",
|
|
608
694
|
method: "POST",
|
|
@@ -610,7 +696,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
610
696
|
handler: handleReembedSkills,
|
|
611
697
|
summary: "Re-seed v2 skill entries from the current skill catalog",
|
|
612
698
|
description:
|
|
613
|
-
"Synchronously re-runs seedV2SkillEntries against the current skill catalog. Gated on
|
|
699
|
+
"Synchronously re-runs seedV2SkillEntries against the current skill catalog. Gated on config.memory.v2.enabled.",
|
|
614
700
|
tags: ["memory"],
|
|
615
701
|
requestBody: MemoryV2ReembedSkillsParams,
|
|
616
702
|
},
|