@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `assistant/src/memory/v3/selector.ts`.
|
|
3
|
-
*
|
|
4
|
-
* Coverage matrix:
|
|
5
|
-
* - Returned IDs map to the right member slugs by 1-based index, with
|
|
6
|
-
* `pinned` driven by `pinned_ids`.
|
|
7
|
-
* - Omitted `ids` → ALL members of the leaf (recall-safe; bounded to one
|
|
8
|
-
* leaf, so this stays a select-all unlike the L1 router).
|
|
9
|
-
* - Explicit `ids: []` → no pages (deliberate abstention).
|
|
10
|
-
* - No provider / missing tool_use / schema mismatch / throw → no pages
|
|
11
|
-
* (degrade to the deterministic lanes), the last three after a re-prompt
|
|
12
|
-
* retry; a malformed response that recovers on retry returns its pages.
|
|
13
|
-
* - The per-leaf `<pages>` prefix is byte-identical across two calls with
|
|
14
|
-
* different turns (the cache invariant).
|
|
15
|
-
* - `selectAcrossLeaves` flattens per-leaf results and never exceeds the
|
|
16
|
-
* configured concurrency.
|
|
17
|
-
*
|
|
18
|
-
* The provider is stubbed so no network calls fire.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
22
|
-
|
|
23
|
-
import type {
|
|
24
|
-
Message,
|
|
25
|
-
Provider,
|
|
26
|
-
ProviderResponse,
|
|
27
|
-
SendMessageOptions,
|
|
28
|
-
ToolUseContent,
|
|
29
|
-
} from "../../../../providers/types.js";
|
|
30
|
-
import type {
|
|
31
|
-
LeafNode,
|
|
32
|
-
LeafPath,
|
|
33
|
-
LeafTree,
|
|
34
|
-
MemoryRoutingTurn,
|
|
35
|
-
Slug,
|
|
36
|
-
} from "../types.js";
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Mocks installed BEFORE the selector import so the module observes them at
|
|
40
|
-
// load time.
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
let providerStub: Provider | null = null;
|
|
44
|
-
|
|
45
|
-
interface ProviderCall {
|
|
46
|
-
messages: Message[];
|
|
47
|
-
options: SendMessageOptions | undefined;
|
|
48
|
-
}
|
|
49
|
-
const providerCalls: ProviderCall[] = [];
|
|
50
|
-
|
|
51
|
-
mock.module("../../../../providers/provider-send-message.js", () => ({
|
|
52
|
-
getConfiguredProvider: async () => providerStub,
|
|
53
|
-
extractToolUse: (response: ProviderResponse) =>
|
|
54
|
-
response.content.find((b): b is ToolUseContent => b.type === "tool_use"),
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
mock.module("../../../../util/logger.js", () => ({
|
|
58
|
-
getLogger: () =>
|
|
59
|
-
new Proxy({} as Record<string, unknown>, {
|
|
60
|
-
get: (_t, prop) => (prop === "child" ? () => ({}) : () => {}),
|
|
61
|
-
}),
|
|
62
|
-
}));
|
|
63
|
-
|
|
64
|
-
const { selectFromLeaf, selectAcrossLeaves } = await import("../selector.js");
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Helpers.
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
function makeProvider(response: ProviderResponse): Provider {
|
|
71
|
-
return {
|
|
72
|
-
name: "stub",
|
|
73
|
-
sendMessage: async (messages, options) => {
|
|
74
|
-
providerCalls.push({ messages, options });
|
|
75
|
-
return response;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
|
|
81
|
-
return {
|
|
82
|
-
model: "stub-model",
|
|
83
|
-
stopReason: "tool_use",
|
|
84
|
-
usage: { inputTokens: 0, outputTokens: 0 },
|
|
85
|
-
content: [{ type: "tool_use", id: "tu-1", name: "select_pages", input }],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** A 200 response that carries no tool_use — the malformed-but-successful case
|
|
90
|
-
* the re-prompt retry exists to recover from. */
|
|
91
|
-
function noToolResponse(): ProviderResponse {
|
|
92
|
-
return {
|
|
93
|
-
model: "stub-model",
|
|
94
|
-
stopReason: "end_turn",
|
|
95
|
-
usage: { inputTokens: 0, outputTokens: 0 },
|
|
96
|
-
content: [{ type: "text", text: "no tool call" }],
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Provider returning a different response per call (the i-th call returns
|
|
101
|
-
* responses[i], or the last entry once exhausted), recording each call so a
|
|
102
|
-
* test can assert how many attempts were made. */
|
|
103
|
-
function makeSequenceProvider(responses: ProviderResponse[]): Provider {
|
|
104
|
-
let i = 0;
|
|
105
|
-
return {
|
|
106
|
-
name: "sequence",
|
|
107
|
-
sendMessage: async (messages, options) => {
|
|
108
|
-
providerCalls.push({ messages, options });
|
|
109
|
-
const response = responses[Math.min(i, responses.length - 1)];
|
|
110
|
-
i += 1;
|
|
111
|
-
return response;
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Provider that records each call and then throws — for the throw-after-retries
|
|
117
|
-
* path (the provider's own RetryProvider has already exhausted its backoff). */
|
|
118
|
-
function makeThrowingProvider(): Provider {
|
|
119
|
-
return {
|
|
120
|
-
name: "throwing",
|
|
121
|
-
sendMessage: async (messages, options) => {
|
|
122
|
-
providerCalls.push({ messages, options });
|
|
123
|
-
throw new Error("boom");
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function makeLeaf(path: LeafPath, members: Slug[]): LeafNode {
|
|
129
|
-
return {
|
|
130
|
-
path,
|
|
131
|
-
frontmatter: { path, in_core: false },
|
|
132
|
-
description: `description for ${path}`,
|
|
133
|
-
members,
|
|
134
|
-
domain: path.split("/")[0],
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Tree with two leaves; `people/alice` has three member pages. */
|
|
139
|
-
function makeTree(): LeafTree {
|
|
140
|
-
const leaves: LeafNode[] = [
|
|
141
|
-
makeLeaf("people/alice", ["alice-bio", "alice-1on1", "alice-feedback"]),
|
|
142
|
-
makeLeaf("projects/atlas", ["atlas-roadmap", "atlas-status"]),
|
|
143
|
-
];
|
|
144
|
-
const byPage = new Map<Slug, LeafPath[]>();
|
|
145
|
-
for (const leaf of leaves) {
|
|
146
|
-
for (const slug of leaf.members) {
|
|
147
|
-
byPage.set(slug, [...(byPage.get(slug) ?? []), leaf.path]);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
leaves: new Map(leaves.map((n) => [n.path, n])),
|
|
152
|
-
byPage,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const ALICE_MEMBERS = ["alice-bio", "alice-1on1", "alice-feedback"];
|
|
157
|
-
|
|
158
|
-
function makeTurn(currentMessage: string): MemoryRoutingTurn {
|
|
159
|
-
return {
|
|
160
|
-
conversationId: "conv-xyz",
|
|
161
|
-
turnNumber: 1,
|
|
162
|
-
currentMessage,
|
|
163
|
-
recentContext: "earlier we talked about the timeline",
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Deterministic summary stub so the rendered prefix is reproducible. */
|
|
168
|
-
const summaryOf = async (slug: Slug): Promise<string> => `summary of ${slug}`;
|
|
169
|
-
|
|
170
|
-
beforeEach(() => {
|
|
171
|
-
providerStub = null;
|
|
172
|
-
providerCalls.length = 0;
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// selectFromLeaf — id mapping.
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
describe("selectFromLeaf — id mapping", () => {
|
|
180
|
-
test("returned IDs map to member slugs, pinned driven by pinned_ids", async () => {
|
|
181
|
-
providerStub = makeProvider(
|
|
182
|
-
toolUseResponse({ ids: [3, 1], pinned_ids: [1] }),
|
|
183
|
-
);
|
|
184
|
-
const result = await selectFromLeaf(
|
|
185
|
-
"people/alice",
|
|
186
|
-
makeTurn("how's alice?"),
|
|
187
|
-
makeTree(),
|
|
188
|
-
summaryOf,
|
|
189
|
-
);
|
|
190
|
-
expect(result).toEqual([
|
|
191
|
-
{ slug: "alice-feedback", pinned: false },
|
|
192
|
-
{ slug: "alice-bio", pinned: true },
|
|
193
|
-
]);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("omitted ids selects ALL members (recall-safe)", async () => {
|
|
197
|
-
providerStub = makeProvider(toolUseResponse({}));
|
|
198
|
-
const result = await selectFromLeaf(
|
|
199
|
-
"people/alice",
|
|
200
|
-
makeTurn("anything"),
|
|
201
|
-
makeTree(),
|
|
202
|
-
summaryOf,
|
|
203
|
-
);
|
|
204
|
-
expect(result).toEqual(
|
|
205
|
-
ALICE_MEMBERS.map((slug) => ({ slug, pinned: false })),
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("explicit empty ids selects no pages (abstention)", async () => {
|
|
210
|
-
providerStub = makeProvider(toolUseResponse({ ids: [] }));
|
|
211
|
-
const result = await selectFromLeaf(
|
|
212
|
-
"people/alice",
|
|
213
|
-
makeTurn("nothing relevant"),
|
|
214
|
-
makeTree(),
|
|
215
|
-
summaryOf,
|
|
216
|
-
);
|
|
217
|
-
expect(result).toEqual([]);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("out-of-range and duplicate IDs are ignored without throwing", async () => {
|
|
221
|
-
providerStub = makeProvider(toolUseResponse({ ids: [2, 99, 0, -1, 2] }));
|
|
222
|
-
const result = await selectFromLeaf(
|
|
223
|
-
"people/alice",
|
|
224
|
-
makeTurn("the 1:1"),
|
|
225
|
-
makeTree(),
|
|
226
|
-
summaryOf,
|
|
227
|
-
);
|
|
228
|
-
expect(result).toEqual([{ slug: "alice-1on1", pinned: false }]);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test("empty leaf returns no pages and never calls the provider", async () => {
|
|
232
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
233
|
-
const result = await selectFromLeaf(
|
|
234
|
-
"people/empty",
|
|
235
|
-
makeTurn("hi"),
|
|
236
|
-
{
|
|
237
|
-
leaves: new Map([["people/empty", makeLeaf("people/empty", [])]]),
|
|
238
|
-
byPage: new Map(),
|
|
239
|
-
},
|
|
240
|
-
summaryOf,
|
|
241
|
-
);
|
|
242
|
-
expect(result).toEqual([]);
|
|
243
|
-
expect(providerCalls).toHaveLength(0);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// selectFromLeaf — recall-safe fallbacks.
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
describe("selectFromLeaf — degradation on failure", () => {
|
|
252
|
-
test("no provider → no pages, without calling the provider", async () => {
|
|
253
|
-
providerStub = null;
|
|
254
|
-
const result = await selectFromLeaf(
|
|
255
|
-
"people/alice",
|
|
256
|
-
makeTurn("x"),
|
|
257
|
-
makeTree(),
|
|
258
|
-
summaryOf,
|
|
259
|
-
);
|
|
260
|
-
expect(result).toEqual([]);
|
|
261
|
-
expect(providerCalls).toHaveLength(0);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("missing tool_use → no pages after retrying", async () => {
|
|
265
|
-
providerStub = makeProvider(noToolResponse());
|
|
266
|
-
const result = await selectFromLeaf(
|
|
267
|
-
"people/alice",
|
|
268
|
-
makeTurn("x"),
|
|
269
|
-
makeTree(),
|
|
270
|
-
summaryOf,
|
|
271
|
-
);
|
|
272
|
-
expect(result).toEqual([]);
|
|
273
|
-
expect(providerCalls).toHaveLength(3);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("schema mismatch → no pages after retrying", async () => {
|
|
277
|
-
providerStub = makeProvider(toolUseResponse({ ids: "not-an-array" }));
|
|
278
|
-
const result = await selectFromLeaf(
|
|
279
|
-
"people/alice",
|
|
280
|
-
makeTurn("x"),
|
|
281
|
-
makeTree(),
|
|
282
|
-
summaryOf,
|
|
283
|
-
);
|
|
284
|
-
expect(result).toEqual([]);
|
|
285
|
-
expect(providerCalls).toHaveLength(3);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test("provider throw → no pages after retrying", async () => {
|
|
289
|
-
providerStub = makeThrowingProvider();
|
|
290
|
-
const result = await selectFromLeaf(
|
|
291
|
-
"people/alice",
|
|
292
|
-
makeTurn("x"),
|
|
293
|
-
makeTree(),
|
|
294
|
-
summaryOf,
|
|
295
|
-
);
|
|
296
|
-
expect(result).toEqual([]);
|
|
297
|
-
expect(providerCalls).toHaveLength(3);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("a malformed response that recovers on retry returns its pages", async () => {
|
|
301
|
-
providerStub = makeSequenceProvider([
|
|
302
|
-
noToolResponse(),
|
|
303
|
-
toolUseResponse({ ids: [2] }),
|
|
304
|
-
]);
|
|
305
|
-
const result = await selectFromLeaf(
|
|
306
|
-
"people/alice",
|
|
307
|
-
makeTurn("the 1:1"),
|
|
308
|
-
makeTree(),
|
|
309
|
-
summaryOf,
|
|
310
|
-
);
|
|
311
|
-
expect(result).toEqual([{ slug: "alice-1on1", pinned: false }]);
|
|
312
|
-
expect(providerCalls).toHaveLength(2);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// ---------------------------------------------------------------------------
|
|
317
|
-
// selectFromLeaf — request shape and cache invariant.
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
|
|
320
|
-
describe("selectFromLeaf — request shape", () => {
|
|
321
|
-
test("forces tool_choice to select_pages with the v3 L2 call site", async () => {
|
|
322
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
323
|
-
await selectFromLeaf(
|
|
324
|
-
"people/alice",
|
|
325
|
-
makeTurn("alice?"),
|
|
326
|
-
makeTree(),
|
|
327
|
-
summaryOf,
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
expect(providerCalls).toHaveLength(1);
|
|
331
|
-
const [call] = providerCalls;
|
|
332
|
-
const cfg = call.options?.config as Record<string, unknown>;
|
|
333
|
-
expect(cfg?.callSite).toBe("memoryV3SelectL2");
|
|
334
|
-
expect(cfg?.tool_choice).toEqual({ type: "tool", name: "select_pages" });
|
|
335
|
-
expect(call.options?.tools?.[0]?.name).toBe("select_pages");
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("pages block is the first content block with an ephemeral cache breakpoint", async () => {
|
|
339
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
340
|
-
await selectFromLeaf(
|
|
341
|
-
"people/alice",
|
|
342
|
-
makeTurn("alice?"),
|
|
343
|
-
makeTree(),
|
|
344
|
-
summaryOf,
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const [blockA, blockB] = providerCalls[0].messages[0].content as Array<{
|
|
348
|
-
type: string;
|
|
349
|
-
text: string;
|
|
350
|
-
cache_control?: { type: string; ttl?: string };
|
|
351
|
-
}>;
|
|
352
|
-
expect(blockA.type).toBe("text");
|
|
353
|
-
expect(blockA.text).toContain("<leaf>people/alice</leaf>");
|
|
354
|
-
expect(blockA.text).toContain("<pages>");
|
|
355
|
-
expect(blockA.text).toContain("[1] alice-bio — summary of alice-bio");
|
|
356
|
-
expect(blockA.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
357
|
-
|
|
358
|
-
expect(blockB.type).toBe("text");
|
|
359
|
-
expect(blockB.text).toContain("<current_message>alice?</current_message>");
|
|
360
|
-
expect(blockB.text).toContain("<recent_context>");
|
|
361
|
-
expect(blockB.cache_control).toBeUndefined();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("situational context renders in the per-turn block when present", async () => {
|
|
365
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
366
|
-
await selectFromLeaf(
|
|
367
|
-
"people/alice",
|
|
368
|
-
{
|
|
369
|
-
...makeTurn("alice?"),
|
|
370
|
-
situationalContext: "Today is Saturday. Alice's anniversary is today.",
|
|
371
|
-
},
|
|
372
|
-
makeTree(),
|
|
373
|
-
summaryOf,
|
|
374
|
-
);
|
|
375
|
-
const blockB = providerCalls[0].messages[0].content[1] as { text: string };
|
|
376
|
-
expect(blockB.text).toContain(
|
|
377
|
-
"<situation>Today is Saturday. Alice's anniversary is today.</situation>",
|
|
378
|
-
);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("system prompt mentions pinned (locks the pinning commitment)", async () => {
|
|
382
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
383
|
-
await selectFromLeaf("people/alice", makeTurn("x"), makeTree(), summaryOf);
|
|
384
|
-
expect(providerCalls[0].options?.systemPrompt).toMatch(/pinned/);
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
describe("selectFromLeaf — per-leaf cache invariant", () => {
|
|
389
|
-
test("the pages prefix is byte-identical across two calls with different turns", async () => {
|
|
390
|
-
const tree = makeTree();
|
|
391
|
-
providerStub = makeProvider(toolUseResponse({ ids: [1] }));
|
|
392
|
-
|
|
393
|
-
await selectFromLeaf(
|
|
394
|
-
"people/alice",
|
|
395
|
-
makeTurn("first query"),
|
|
396
|
-
tree,
|
|
397
|
-
summaryOf,
|
|
398
|
-
);
|
|
399
|
-
await selectFromLeaf(
|
|
400
|
-
"people/alice",
|
|
401
|
-
makeTurn("totally different second query"),
|
|
402
|
-
tree,
|
|
403
|
-
summaryOf,
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
const prefixA = (
|
|
407
|
-
providerCalls[0].messages[0].content[0] as { text: string }
|
|
408
|
-
).text;
|
|
409
|
-
const prefixB = (
|
|
410
|
-
providerCalls[1].messages[0].content[0] as { text: string }
|
|
411
|
-
).text;
|
|
412
|
-
expect(prefixA).toBe(prefixB);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
// selectAcrossLeaves — fan-out and bounded concurrency.
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
|
|
420
|
-
describe("selectAcrossLeaves", () => {
|
|
421
|
-
test("flattens per-leaf selections", async () => {
|
|
422
|
-
// The selector forces the tool, so omitted ids → all members per leaf.
|
|
423
|
-
providerStub = makeProvider(toolUseResponse({}));
|
|
424
|
-
const result = await selectAcrossLeaves(
|
|
425
|
-
["people/alice", "projects/atlas"],
|
|
426
|
-
makeTurn("x"),
|
|
427
|
-
makeTree(),
|
|
428
|
-
summaryOf,
|
|
429
|
-
);
|
|
430
|
-
expect(result.map((p) => p.slug)).toEqual([
|
|
431
|
-
"alice-bio",
|
|
432
|
-
"alice-1on1",
|
|
433
|
-
"alice-feedback",
|
|
434
|
-
"atlas-roadmap",
|
|
435
|
-
"atlas-status",
|
|
436
|
-
]);
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
test("never exceeds the configured concurrency", async () => {
|
|
440
|
-
let inFlight = 0;
|
|
441
|
-
let maxInFlight = 0;
|
|
442
|
-
let calls = 0;
|
|
443
|
-
providerStub = {
|
|
444
|
-
name: "tracking",
|
|
445
|
-
sendMessage: async () => {
|
|
446
|
-
calls++;
|
|
447
|
-
inFlight++;
|
|
448
|
-
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
449
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
450
|
-
inFlight--;
|
|
451
|
-
return toolUseResponse({ ids: [] });
|
|
452
|
-
},
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
// Build a tree with eight single-member leaves so there are eight calls.
|
|
456
|
-
const leaves: LeafNode[] = Array.from({ length: 8 }, (_, i) =>
|
|
457
|
-
makeLeaf(`misc/leaf-${i}`, [`page-${i}`]),
|
|
458
|
-
);
|
|
459
|
-
const tree: LeafTree = {
|
|
460
|
-
leaves: new Map(leaves.map((n) => [n.path, n])),
|
|
461
|
-
byPage: new Map(),
|
|
462
|
-
};
|
|
463
|
-
const paths = leaves.map((l) => l.path);
|
|
464
|
-
|
|
465
|
-
await selectAcrossLeaves(paths, makeTurn("x"), tree, summaryOf, 3);
|
|
466
|
-
expect(maxInFlight).toBeLessThanOrEqual(3);
|
|
467
|
-
expect(maxInFlight).toBeGreaterThan(1);
|
|
468
|
-
expect(calls).toBe(8);
|
|
469
|
-
});
|
|
470
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
-
|
|
6
|
-
import { restoreDataDir, snapshotDataDir } from "../snapshot.js";
|
|
7
|
-
|
|
8
|
-
let tmpRoot: string;
|
|
9
|
-
let dataDir: string;
|
|
10
|
-
|
|
11
|
-
async function readJson(file: string): Promise<unknown> {
|
|
12
|
-
return JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function listLeaves(dir: string): Promise<Record<string, string>> {
|
|
16
|
-
const leavesDir = path.join(dir, "leaves");
|
|
17
|
-
const out: Record<string, string> = {};
|
|
18
|
-
async function walk(current: string, prefix: string): Promise<void> {
|
|
19
|
-
let entries: import("node:fs").Dirent[];
|
|
20
|
-
try {
|
|
21
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
22
|
-
} catch {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
27
|
-
const abs = path.join(current, entry.name);
|
|
28
|
-
if (entry.isDirectory()) {
|
|
29
|
-
await walk(abs, rel);
|
|
30
|
-
} else if (entry.isFile()) {
|
|
31
|
-
out[rel] = await fs.readFile(abs, "utf8");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
await walk(leavesDir, "");
|
|
36
|
-
return out;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function seedDataDir(): Promise<void> {
|
|
40
|
-
await fs.mkdir(path.join(dataDir, "leaves", "alpha"), { recursive: true });
|
|
41
|
-
await fs.writeFile(
|
|
42
|
-
path.join(dataDir, "leaves", "alpha", "leaf-1.md"),
|
|
43
|
-
"leaf one body",
|
|
44
|
-
);
|
|
45
|
-
await fs.writeFile(
|
|
46
|
-
path.join(dataDir, "leaves", "leaf-2.md"),
|
|
47
|
-
"leaf two body",
|
|
48
|
-
);
|
|
49
|
-
await fs.writeFile(
|
|
50
|
-
path.join(dataDir, "assignments.json"),
|
|
51
|
-
JSON.stringify({ "leaf-1": ["topic-a"] }),
|
|
52
|
-
);
|
|
53
|
-
await fs.writeFile(
|
|
54
|
-
path.join(dataDir, "core.json"),
|
|
55
|
-
JSON.stringify({ version: 1, root: "topic-a" }),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
beforeEach(async () => {
|
|
60
|
-
tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "v3-snapshot-"));
|
|
61
|
-
dataDir = path.join(tmpRoot, "data");
|
|
62
|
-
await fs.mkdir(dataDir, { recursive: true });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
afterEach(async () => {
|
|
66
|
-
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe("snapshotDataDir / restoreDataDir", () => {
|
|
70
|
-
test("roundtrips leaves, assignments, and core", async () => {
|
|
71
|
-
await seedDataDir();
|
|
72
|
-
const before = {
|
|
73
|
-
leaves: await listLeaves(dataDir),
|
|
74
|
-
assignments: await readJson(path.join(dataDir, "assignments.json")),
|
|
75
|
-
core: await readJson(path.join(dataDir, "core.json")),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const snapshotPath = await snapshotDataDir(dataDir, { label: "snap-1" });
|
|
79
|
-
|
|
80
|
-
// Mutate the live data dir after snapshotting.
|
|
81
|
-
await fs.writeFile(
|
|
82
|
-
path.join(dataDir, "leaves", "alpha", "leaf-1.md"),
|
|
83
|
-
"CORRUPTED",
|
|
84
|
-
);
|
|
85
|
-
await fs.rm(path.join(dataDir, "leaves", "leaf-2.md"));
|
|
86
|
-
await fs.writeFile(
|
|
87
|
-
path.join(dataDir, "assignments.json"),
|
|
88
|
-
JSON.stringify({ broken: true }),
|
|
89
|
-
);
|
|
90
|
-
await fs.writeFile(
|
|
91
|
-
path.join(dataDir, "core.json"),
|
|
92
|
-
JSON.stringify({ broken: true }),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
await restoreDataDir(snapshotPath, dataDir);
|
|
96
|
-
|
|
97
|
-
expect(await listLeaves(dataDir)).toEqual(before.leaves);
|
|
98
|
-
expect(await readJson(path.join(dataDir, "assignments.json"))).toEqual(
|
|
99
|
-
before.assignments,
|
|
100
|
-
);
|
|
101
|
-
expect(await readJson(path.join(dataDir, "core.json"))).toEqual(
|
|
102
|
-
before.core,
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("roundtrips the pageRefs diff", async () => {
|
|
107
|
-
await seedDataDir();
|
|
108
|
-
const pageRefs = new Map<string, string[]>([
|
|
109
|
-
["slug-one", ["leaf-1", "leaf-2"]],
|
|
110
|
-
["slug-two", ["leaf-3"]],
|
|
111
|
-
]);
|
|
112
|
-
|
|
113
|
-
const snapshotPath = await snapshotDataDir(dataDir, {
|
|
114
|
-
label: "snap-refs",
|
|
115
|
-
pageRefs,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const { pageRefs: restored } = await restoreDataDir(snapshotPath, dataDir);
|
|
119
|
-
expect(restored).toBeDefined();
|
|
120
|
-
expect(restored).toEqual(pageRefs);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("returns no pageRefs when none were captured", async () => {
|
|
124
|
-
await seedDataDir();
|
|
125
|
-
const snapshotPath = await snapshotDataDir(dataDir, { label: "snap-none" });
|
|
126
|
-
const result = await restoreDataDir(snapshotPath, dataDir);
|
|
127
|
-
expect(result.pageRefs).toBeUndefined();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("restoring removes leaves added after the snapshot", async () => {
|
|
131
|
-
await seedDataDir();
|
|
132
|
-
const snapshotPath = await snapshotDataDir(dataDir, { label: "snap-add" });
|
|
133
|
-
|
|
134
|
-
await fs.writeFile(
|
|
135
|
-
path.join(dataDir, "leaves", "leaf-extra.md"),
|
|
136
|
-
"added later",
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
await restoreDataDir(snapshotPath, dataDir);
|
|
140
|
-
const leaves = await listLeaves(dataDir);
|
|
141
|
-
expect(leaves["leaf-extra.md"]).toBeUndefined();
|
|
142
|
-
expect(leaves["leaf-2.md"]).toBe("leaf two body");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("retention is bounded to the last N snapshots", async () => {
|
|
146
|
-
await seedDataDir();
|
|
147
|
-
// Zero-padded labels sort chronologically.
|
|
148
|
-
const labels = Array.from(
|
|
149
|
-
{ length: 8 },
|
|
150
|
-
(_, i) => `snap-${String(i).padStart(2, "0")}`,
|
|
151
|
-
);
|
|
152
|
-
for (const label of labels) {
|
|
153
|
-
await snapshotDataDir(dataDir, { label });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const snapshotsRoot = path.join(tmpRoot, "v3-snapshots");
|
|
157
|
-
const remaining = (await fs.readdir(snapshotsRoot)).sort();
|
|
158
|
-
expect(remaining.length).toBe(5);
|
|
159
|
-
// Oldest three pruned, newest five kept.
|
|
160
|
-
expect(remaining).toEqual([
|
|
161
|
-
"snap-03",
|
|
162
|
-
"snap-04",
|
|
163
|
-
"snap-05",
|
|
164
|
-
"snap-06",
|
|
165
|
-
"snap-07",
|
|
166
|
-
]);
|
|
167
|
-
});
|
|
168
|
-
});
|