@vellumai/assistant 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -161,6 +161,38 @@ mock.module("../skill-store.js", () => ({
|
|
|
161
161
|
listSkillEntries: () => Array.from(skillState.entries.values()),
|
|
162
162
|
}));
|
|
163
163
|
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// CLI-command-store mock
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
//
|
|
168
|
+
// Mirrors the skill-store mock. CLI subcommand synthetic entries flow through
|
|
169
|
+
// the unified pipeline under the `cli-commands/<name>` slug prefix and render
|
|
170
|
+
// under `### CLI Commands You Can Use`. Tests stage `cliCommandState.entries`
|
|
171
|
+
// and rely on `stageTurn` plumbing to land slugs in the candidate set.
|
|
172
|
+
|
|
173
|
+
interface CliCommandEntryStub {
|
|
174
|
+
id: string;
|
|
175
|
+
description: string;
|
|
176
|
+
content: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cliCommandState = {
|
|
180
|
+
entries: new Map<string, CliCommandEntryStub>(),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
mock.module("../cli-command-store.js", () => ({
|
|
184
|
+
getCliCommandCapability: (idOrSlug: string) => {
|
|
185
|
+
const id = idOrSlug.startsWith("cli-commands/")
|
|
186
|
+
? idOrSlug.slice("cli-commands/".length)
|
|
187
|
+
: idOrSlug;
|
|
188
|
+
return cliCommandState.entries.get(id) ?? null;
|
|
189
|
+
},
|
|
190
|
+
isCliCommandSlug: (slug: string) => slug.startsWith("cli-commands/"),
|
|
191
|
+
CLI_COMMAND_SLUG_PREFIX: "cli-commands/",
|
|
192
|
+
cliCommandSlugFor: (name: string) => `cli-commands/${name}`,
|
|
193
|
+
listCliCommandEntries: () => Array.from(cliCommandState.entries.values()),
|
|
194
|
+
}));
|
|
195
|
+
|
|
164
196
|
// ---------------------------------------------------------------------------
|
|
165
197
|
// Activation-log store mock
|
|
166
198
|
// ---------------------------------------------------------------------------
|
|
@@ -357,7 +389,7 @@ const { getSqliteFrom } = await import("../../db-connection.js");
|
|
|
357
389
|
const { migrateActivationState } =
|
|
358
390
|
await import("../../migrations/232-activation-state.js");
|
|
359
391
|
const schema = await import("../../schema.js");
|
|
360
|
-
const {
|
|
392
|
+
const { clearEverInjected, hydrate, save } =
|
|
361
393
|
await import("../activation-store.js");
|
|
362
394
|
const { injectMemoryV2Block } = await import("../injection.js");
|
|
363
395
|
const { _resetMemoryV2QdrantForTests } = await import("../qdrant.js");
|
|
@@ -484,6 +516,7 @@ function resetState(): void {
|
|
|
484
516
|
state.queryResponses.dense.length = 0;
|
|
485
517
|
state.queryResponses.sparse.length = 0;
|
|
486
518
|
skillState.entries.clear();
|
|
519
|
+
cliCommandState.entries.clear();
|
|
487
520
|
telemetryState.recordCalls.length = 0;
|
|
488
521
|
telemetryState.recordShouldThrow = false;
|
|
489
522
|
pageStoreState.failingSlugs.clear();
|
|
@@ -503,6 +536,13 @@ function stageSkills(entries: SkillEntry[]): void {
|
|
|
503
536
|
}
|
|
504
537
|
}
|
|
505
538
|
|
|
539
|
+
/** Stage cli-command-store cache entries for the upcoming render. */
|
|
540
|
+
function stageCliCommands(entries: CliCommandEntryStub[]): void {
|
|
541
|
+
for (const entry of entries) {
|
|
542
|
+
cliCommandState.entries.set(entry.id, entry);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
506
546
|
let db: DrizzleDb;
|
|
507
547
|
beforeEach(() => {
|
|
508
548
|
db = createTestDb();
|
|
@@ -653,10 +693,10 @@ describe("injectMemoryV2Block", () => {
|
|
|
653
693
|
config: makeConfig(),
|
|
654
694
|
});
|
|
655
695
|
|
|
656
|
-
// Simulate compaction:
|
|
696
|
+
// Simulate compaction: clear the entire everInjected list.
|
|
657
697
|
const beforeEvict = await hydrate(db, "conv-1");
|
|
658
698
|
expect(beforeEvict).not.toBeNull();
|
|
659
|
-
const afterEvict =
|
|
699
|
+
const afterEvict = clearEverInjected(beforeEvict!);
|
|
660
700
|
expect(afterEvict.everInjected).toEqual([]);
|
|
661
701
|
await save(db, "conv-1", afterEvict);
|
|
662
702
|
|
|
@@ -1060,6 +1100,153 @@ describe("injectMemoryV2Block", () => {
|
|
|
1060
1100
|
expect(result.block).toBeNull();
|
|
1061
1101
|
});
|
|
1062
1102
|
|
|
1103
|
+
// ---------------------------------------------------------------------------
|
|
1104
|
+
// CLI-command synthetic entries — same unified-pool plumbing as skills.
|
|
1105
|
+
// ---------------------------------------------------------------------------
|
|
1106
|
+
|
|
1107
|
+
test("renders a retrieved cli-commands/<name> slug under CLI Commands You Can Use", async () => {
|
|
1108
|
+
stageTurn([{ slug: "cli-commands/attachment", denseScore: 0.9 }]);
|
|
1109
|
+
stageCliCommands([
|
|
1110
|
+
{
|
|
1111
|
+
id: "attachment",
|
|
1112
|
+
description: "Manage file attachments for conversations",
|
|
1113
|
+
content: 'The "assistant attachment" CLI command is available...',
|
|
1114
|
+
},
|
|
1115
|
+
]);
|
|
1116
|
+
|
|
1117
|
+
const result = await injectMemoryV2Block({
|
|
1118
|
+
database: db,
|
|
1119
|
+
conversationId: "conv-1",
|
|
1120
|
+
currentTurn: 1,
|
|
1121
|
+
userMessage: "How do I register a video?",
|
|
1122
|
+
assistantMessage: "",
|
|
1123
|
+
nowText: "Now",
|
|
1124
|
+
messageId: "msg-1",
|
|
1125
|
+
config: makeConfig(),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
expect(result.toInject).toEqual(["cli-commands/attachment"]);
|
|
1129
|
+
expect(result.block).not.toBeNull();
|
|
1130
|
+
const headerIdx = result.block!.indexOf("### CLI Commands You Can Use");
|
|
1131
|
+
const lineIdx = result.block!.indexOf(
|
|
1132
|
+
"- `assistant attachment`: Manage file attachments for conversations",
|
|
1133
|
+
);
|
|
1134
|
+
expect(headerIdx).toBeGreaterThan(-1);
|
|
1135
|
+
expect(lineIdx).toBeGreaterThan(headerIdx);
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
test("renders concepts, skills, then cli-commands in that order in mixed blocks", async () => {
|
|
1139
|
+
stageTurn([
|
|
1140
|
+
{ slug: "alice-vscode", denseScore: 0.95 },
|
|
1141
|
+
{ slug: "skills/example-skill-a", denseScore: 0.85 },
|
|
1142
|
+
{ slug: "cli-commands/config", denseScore: 0.75 },
|
|
1143
|
+
]);
|
|
1144
|
+
stageSkills([
|
|
1145
|
+
{
|
|
1146
|
+
id: "example-skill-a",
|
|
1147
|
+
content:
|
|
1148
|
+
'The "Example Skill A" skill (example-skill-a) is available. Helps with examples.',
|
|
1149
|
+
},
|
|
1150
|
+
]);
|
|
1151
|
+
stageCliCommands([
|
|
1152
|
+
{
|
|
1153
|
+
id: "config",
|
|
1154
|
+
description: "Manage configuration",
|
|
1155
|
+
content: 'The "assistant config" CLI command is available...',
|
|
1156
|
+
},
|
|
1157
|
+
]);
|
|
1158
|
+
|
|
1159
|
+
const result = await injectMemoryV2Block({
|
|
1160
|
+
database: db,
|
|
1161
|
+
conversationId: "conv-1",
|
|
1162
|
+
currentTurn: 1,
|
|
1163
|
+
userMessage: "Help me",
|
|
1164
|
+
assistantMessage: "",
|
|
1165
|
+
nowText: "Now",
|
|
1166
|
+
messageId: "msg-1",
|
|
1167
|
+
config: makeConfig(),
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
expect(new Set(result.toInject)).toEqual(
|
|
1171
|
+
new Set([
|
|
1172
|
+
"alice-vscode",
|
|
1173
|
+
"skills/example-skill-a",
|
|
1174
|
+
"cli-commands/config",
|
|
1175
|
+
]),
|
|
1176
|
+
);
|
|
1177
|
+
const conceptIdx = result.block!.indexOf(
|
|
1178
|
+
"# memory/concepts/alice-vscode.md",
|
|
1179
|
+
);
|
|
1180
|
+
const skillsIdx = result.block!.indexOf("### Skills You Can Use");
|
|
1181
|
+
const cliIdx = result.block!.indexOf("### CLI Commands You Can Use");
|
|
1182
|
+
expect(conceptIdx).toBeGreaterThan(-1);
|
|
1183
|
+
expect(skillsIdx).toBeGreaterThan(conceptIdx);
|
|
1184
|
+
expect(cliIdx).toBeGreaterThan(skillsIdx);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("cli-command slugs whose entry is missing from the cache are dropped silently", async () => {
|
|
1188
|
+
stageTurn([{ slug: "cli-commands/missing-command", denseScore: 0.9 }]);
|
|
1189
|
+
|
|
1190
|
+
const result = await injectMemoryV2Block({
|
|
1191
|
+
database: db,
|
|
1192
|
+
conversationId: "conv-1",
|
|
1193
|
+
currentTurn: 1,
|
|
1194
|
+
userMessage: "anything",
|
|
1195
|
+
assistantMessage: "",
|
|
1196
|
+
nowText: "Now",
|
|
1197
|
+
messageId: "msg-1",
|
|
1198
|
+
config: makeConfig(),
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
expect(result.toInject).toEqual([]);
|
|
1202
|
+
expect(result.block).toBeNull();
|
|
1203
|
+
|
|
1204
|
+
const persisted = await hydrate(db, "conv-1");
|
|
1205
|
+
expect(persisted!.everInjected).toEqual([]);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
test("cli-commands participate in everInjected so they dedupe across turns", async () => {
|
|
1209
|
+
const entry = {
|
|
1210
|
+
id: "config",
|
|
1211
|
+
description: "Manage configuration",
|
|
1212
|
+
content: 'The "assistant config" CLI command is available...',
|
|
1213
|
+
};
|
|
1214
|
+
stageTurn([{ slug: "cli-commands/config", denseScore: 0.9 }]);
|
|
1215
|
+
stageCliCommands([entry]);
|
|
1216
|
+
const result1 = await injectMemoryV2Block({
|
|
1217
|
+
database: db,
|
|
1218
|
+
conversationId: "conv-1",
|
|
1219
|
+
currentTurn: 1,
|
|
1220
|
+
userMessage: "config",
|
|
1221
|
+
assistantMessage: "",
|
|
1222
|
+
nowText: "Now",
|
|
1223
|
+
messageId: "msg-1",
|
|
1224
|
+
config: makeConfig(),
|
|
1225
|
+
});
|
|
1226
|
+
expect(result1.toInject).toEqual(["cli-commands/config"]);
|
|
1227
|
+
expect(result1.block).toContain("### CLI Commands You Can Use");
|
|
1228
|
+
|
|
1229
|
+
stageTurn([{ slug: "cli-commands/config", denseScore: 0.9 }]);
|
|
1230
|
+
stageCliCommands([entry]);
|
|
1231
|
+
const result2 = await injectMemoryV2Block({
|
|
1232
|
+
database: db,
|
|
1233
|
+
conversationId: "conv-1",
|
|
1234
|
+
currentTurn: 2,
|
|
1235
|
+
userMessage: "more config",
|
|
1236
|
+
assistantMessage: "ok",
|
|
1237
|
+
nowText: "Now",
|
|
1238
|
+
messageId: "msg-2",
|
|
1239
|
+
config: makeConfig(),
|
|
1240
|
+
});
|
|
1241
|
+
expect(result2.toInject).toEqual([]);
|
|
1242
|
+
expect(result2.block).toBeNull();
|
|
1243
|
+
|
|
1244
|
+
const persisted = await hydrate(db, "conv-1");
|
|
1245
|
+
expect(persisted!.everInjected).toEqual([
|
|
1246
|
+
{ slug: "cli-commands/config", turn: 1 },
|
|
1247
|
+
]);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1063
1250
|
test("context-load mode renders topNow even when every slug was previously injected", async () => {
|
|
1064
1251
|
// Turn 1 (per-turn): seed alice as injected.
|
|
1065
1252
|
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
@@ -32,11 +32,15 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
let configMemoryV2Enabled = true;
|
|
35
|
+
let configMemoryEnabled = true;
|
|
35
36
|
|
|
36
37
|
mock.module("../../../config/loader.js", () => ({
|
|
37
38
|
getConfig: () => ({}),
|
|
38
39
|
loadConfig: () => ({
|
|
39
|
-
memory: {
|
|
40
|
+
memory: {
|
|
41
|
+
enabled: configMemoryEnabled,
|
|
42
|
+
v2: { enabled: configMemoryV2Enabled },
|
|
43
|
+
},
|
|
40
44
|
}),
|
|
41
45
|
loadRawConfig: () => ({}),
|
|
42
46
|
saveRawConfig: () => {},
|
|
@@ -71,6 +75,7 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
71
75
|
beforeEach(() => {
|
|
72
76
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
73
77
|
configMemoryV2Enabled = true;
|
|
78
|
+
configMemoryEnabled = true;
|
|
74
79
|
});
|
|
75
80
|
|
|
76
81
|
afterEach(() => {
|
|
@@ -83,6 +88,12 @@ describe("readMemoryV2StaticContent", () => {
|
|
|
83
88
|
expect(readMemoryV2StaticContent()).toBeNull();
|
|
84
89
|
});
|
|
85
90
|
|
|
91
|
+
test("returns null when config.memory.enabled is off even with v2 on", () => {
|
|
92
|
+
configMemoryEnabled = false;
|
|
93
|
+
for (const file of MEMORY_FILES) writeMemoryFile(file, `Content ${file}`);
|
|
94
|
+
expect(readMemoryV2StaticContent()).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
86
97
|
test("returns headed sections in canonical order when all files have content", () => {
|
|
87
98
|
writeMemoryFile("essentials.md", "Alice prefers dark mode.");
|
|
88
99
|
writeMemoryFile("threads.md", "Open thread: ship PR-123 review.");
|
|
@@ -11,11 +11,7 @@ import { eq } from "drizzle-orm";
|
|
|
11
11
|
|
|
12
12
|
import type { DrizzleDb } from "../db-connection.js";
|
|
13
13
|
import { activationState } from "../schema.js";
|
|
14
|
-
import {
|
|
15
|
-
type ActivationState,
|
|
16
|
-
ActivationStateSchema,
|
|
17
|
-
type EverInjectedEntry,
|
|
18
|
-
} from "./types.js";
|
|
14
|
+
import { type ActivationState, ActivationStateSchema } from "./types.js";
|
|
19
15
|
|
|
20
16
|
/**
|
|
21
17
|
* Load the activation state for a conversation, or `null` if no row exists.
|
|
@@ -123,16 +119,18 @@ export function forkActivationState(
|
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
122
|
+
* Clear all `everInjected` entries. Used after compaction: the cached
|
|
123
|
+
* `<memory>` attachments those slugs lived on are gone, so future turns
|
|
124
|
+
* should be free to re-inject them.
|
|
125
|
+
*
|
|
126
|
+
* Unconditionally empties the list rather than filtering by turn number.
|
|
127
|
+
* `everInjected` is persisted on every turn while the in-memory tracker's
|
|
128
|
+
* `currentTurn` is only snapshotted on graceful conversation dispose, so a
|
|
129
|
+
* non-graceful shutdown (SIGKILL, crash) followed by a reload can leave
|
|
130
|
+
* `everInjected` entries with `turn` values above the restored tracker's
|
|
131
|
+
* `currentTurn`. A turn-bounded filter misses those stale entries and they
|
|
132
|
+
* dedupe forever; a full clear is robust to that drift.
|
|
129
133
|
*/
|
|
130
|
-
export function
|
|
131
|
-
state:
|
|
132
|
-
upToTurn: number,
|
|
133
|
-
): ActivationState {
|
|
134
|
-
const everInjected: EverInjectedEntry[] = state.everInjected.filter(
|
|
135
|
-
(entry) => entry.turn > upToTurn,
|
|
136
|
-
);
|
|
137
|
-
return { ...state, everInjected };
|
|
134
|
+
export function clearEverInjected(state: ActivationState): ActivationState {
|
|
135
|
+
return { ...state, everInjected: [] };
|
|
138
136
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the prose-style capability statement embedded into the unified
|
|
3
|
+
* `memory_v2_concept_pages` Qdrant collection (under the `cli-commands/<name>`
|
|
4
|
+
* slug prefix). Mirrors `buildSkillContent` in shape — a short prose lead-in
|
|
5
|
+
* followed by the dense capability material — so activation scoring weighs
|
|
6
|
+
* both natural-language intent and structured help text.
|
|
7
|
+
*
|
|
8
|
+
* Intentionally uncapped: CLI `--help` output averages 1–2 KB and the longest
|
|
9
|
+
* (browser, oauth) hits ~3.4 KB. The embedding backend handles inputs of this
|
|
10
|
+
* size without trouble, and trimming would drop the very examples and flag
|
|
11
|
+
* descriptions that make commands semantically findable.
|
|
12
|
+
*/
|
|
13
|
+
export function buildCliCommandContent(
|
|
14
|
+
name: string,
|
|
15
|
+
description: string,
|
|
16
|
+
helpText: string,
|
|
17
|
+
): string {
|
|
18
|
+
return `The "assistant ${name}" CLI command is available. ${description}.\n\nFull help:\n${helpText}`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Memory v2 — `assistant` CLI subcommands → embedded capability entries
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
//
|
|
5
|
+
// Enumerate the top-level `assistant` CLI subcommands, render each as a prose
|
|
6
|
+
// capability statement that wraps the full `helpInformation()` output, embed
|
|
7
|
+
// dense + sparse, and upsert into `memory_v2_concept_pages` under the slug
|
|
8
|
+
// `cli-commands/<name>`. The router scores these alongside concept pages and
|
|
9
|
+
// skill entries; the injection layer surfaces hits under `### CLI Commands
|
|
10
|
+
// You Can Use` so the model can semantically discover a CLI capability it
|
|
11
|
+
// would not otherwise know to reach for.
|
|
12
|
+
//
|
|
13
|
+
// Mirrors `skill-store.ts` deliberately: same single-flight + generation
|
|
14
|
+
// coalescing, same dense + sparse + corpus-stats-aware sparse encoding, same
|
|
15
|
+
// payload-kind discriminator, same atomic cache replacement. Differences:
|
|
16
|
+
// - No remote catalog — the source of truth is the local Commander tree.
|
|
17
|
+
// - No per-entry feature-flag filter — flag gating already happens during
|
|
18
|
+
// `buildCliProgramTree` (e.g. email/plugins commands are conditionally
|
|
19
|
+
// registered).
|
|
20
|
+
// - No MCP-style augmentation — Commander's description is the canonical
|
|
21
|
+
// summary.
|
|
22
|
+
|
|
23
|
+
import { getConfig } from "../../config/loader.js";
|
|
24
|
+
import { getLogger } from "../../util/logger.js";
|
|
25
|
+
import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
26
|
+
import {
|
|
27
|
+
embedWithBackend,
|
|
28
|
+
generateSparseEmbedding,
|
|
29
|
+
} from "../embedding-backend.js";
|
|
30
|
+
import { buildCliCommandContent } from "./cli-command-content.js";
|
|
31
|
+
import { invalidatePageIndex } from "./page-index.js";
|
|
32
|
+
import {
|
|
33
|
+
backfillKindOnPointsWithPrefix,
|
|
34
|
+
pruneSlugsWithPrefixExcept,
|
|
35
|
+
upsertConceptPageEmbedding,
|
|
36
|
+
} from "./qdrant.js";
|
|
37
|
+
import {
|
|
38
|
+
generateBm25DocEmbedding,
|
|
39
|
+
getConceptPageCorpusStats,
|
|
40
|
+
} from "./sparse-bm25.js";
|
|
41
|
+
import type { CliCommandEntry } from "./types.js";
|
|
42
|
+
|
|
43
|
+
const log = getLogger("memory-v2-cli-command-store");
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Slug prefix under which CLI-command embeddings are indexed in
|
|
47
|
+
* `memory_v2_concept_pages`. Concept-page slugs must match
|
|
48
|
+
* `[a-z0-9][a-z0-9-]*(/...)*`, and `cli-commands` matches that pattern, so the
|
|
49
|
+
* prefix coexists with hand-authored concept pages without escape work.
|
|
50
|
+
*/
|
|
51
|
+
export const CLI_COMMAND_SLUG_PREFIX = "cli-commands/";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Payload discriminator written on every CLI-command-seeded Qdrant point.
|
|
55
|
+
* Keeps CLI rows distinguishable from user-authored concept pages and from
|
|
56
|
+
* skill rows that happen to live in adjacent namespaces, so prefix pruning
|
|
57
|
+
* never deletes a hand-authored page sitting under `cli-commands/...`.
|
|
58
|
+
*/
|
|
59
|
+
const CLI_COMMAND_PAYLOAD_KIND = "cli-command";
|
|
60
|
+
|
|
61
|
+
/** Compose the unified-collection slug for a CLI command name. */
|
|
62
|
+
export function cliCommandSlugFor(name: string): string {
|
|
63
|
+
return `${CLI_COMMAND_SLUG_PREFIX}${name}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Module-level cache of rendered CLI-command entries keyed by command name.
|
|
68
|
+
* `null` until the first successful seed run completes; replaced atomically
|
|
69
|
+
* on each successful re-seed so callers always see a consistent snapshot.
|
|
70
|
+
*/
|
|
71
|
+
let entries: Map<string, CliCommandEntry> | null = null;
|
|
72
|
+
let requestedSeedGeneration = 0;
|
|
73
|
+
let processedSeedGeneration = 0;
|
|
74
|
+
let activeSeedDrain: Promise<void> | null = null;
|
|
75
|
+
let lastSeedError: unknown = null;
|
|
76
|
+
const seedWaiters: Array<{ generation: number; resolve: () => void }> = [];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* In-process latch for the legacy `kind` backfill. New upserts always write
|
|
80
|
+
* `kind`, so once the latch is set there is no follow-up work to do this
|
|
81
|
+
* process.
|
|
82
|
+
*/
|
|
83
|
+
let legacyKindBackfillDone = false;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Seed (or re-seed) CLI-command embeddings into the unified concept-page
|
|
87
|
+
* collection. Idempotent. Best-effort for background callers (errors are
|
|
88
|
+
* logged but swallowed); pass `{ throwOnError: true }` from synchronous CLI
|
|
89
|
+
* paths that want failures surfaced.
|
|
90
|
+
*
|
|
91
|
+
* Single-flight + coalesced: at most one seed runs at a time. Requests made
|
|
92
|
+
* while a seed is in flight advance the requested generation; stale in-flight
|
|
93
|
+
* snapshots are skipped before they write embeddings or replace the cache.
|
|
94
|
+
*/
|
|
95
|
+
export async function seedV2CliCommandEntries(
|
|
96
|
+
opts: { throwOnError?: boolean } = {},
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const generation = ++requestedSeedGeneration;
|
|
99
|
+
const waiter = new Promise<void>((resolve) => {
|
|
100
|
+
seedWaiters.push({ generation, resolve });
|
|
101
|
+
});
|
|
102
|
+
startSeedDrainIfNeeded();
|
|
103
|
+
await waiter;
|
|
104
|
+
if (opts.throwOnError && lastSeedError) {
|
|
105
|
+
throw lastSeedError;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function startSeedDrainIfNeeded(): void {
|
|
110
|
+
if (activeSeedDrain) return;
|
|
111
|
+
if (processedSeedGeneration >= requestedSeedGeneration) return;
|
|
112
|
+
|
|
113
|
+
activeSeedDrain = drainSeedQueue().finally(() => {
|
|
114
|
+
activeSeedDrain = null;
|
|
115
|
+
startSeedDrainIfNeeded();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function drainSeedQueue(): Promise<void> {
|
|
120
|
+
while (processedSeedGeneration < requestedSeedGeneration) {
|
|
121
|
+
const generationToProcess = requestedSeedGeneration;
|
|
122
|
+
await runSeedV2CliCommandEntries(generationToProcess);
|
|
123
|
+
processedSeedGeneration = generationToProcess;
|
|
124
|
+
resolveSeedWaiters();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveSeedWaiters(): void {
|
|
129
|
+
for (let i = seedWaiters.length - 1; i >= 0; i -= 1) {
|
|
130
|
+
const waiter = seedWaiters[i]!;
|
|
131
|
+
if (waiter.generation > processedSeedGeneration) continue;
|
|
132
|
+
seedWaiters.splice(i, 1);
|
|
133
|
+
waiter.resolve();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
|
|
138
|
+
try {
|
|
139
|
+
const config = getConfig();
|
|
140
|
+
// Dynamic import so callers that only need `getCliCommandCapability` or
|
|
141
|
+
// `listCliCommandEntries` (e.g. the render path inside `injection.ts` and
|
|
142
|
+
// the `page-index.ts` dependency loader) never drag the full CLI command
|
|
143
|
+
// graph into their import tree. The CLI tree pulls in many provider and
|
|
144
|
+
// workspace modules whose presence has been a recurring source of test-
|
|
145
|
+
// mock cascades and circular-import surprises.
|
|
146
|
+
const { buildCliProgramTree } = await import("../../cli/program.js");
|
|
147
|
+
const program = buildCliProgramTree();
|
|
148
|
+
|
|
149
|
+
const seeds: CliCommandEntry[] = [];
|
|
150
|
+
for (const cmd of program.commands) {
|
|
151
|
+
const name = cmd.name();
|
|
152
|
+
// Skip the `help` builtin Commander adds automatically — it carries no
|
|
153
|
+
// capability information of its own and is uniform across commands.
|
|
154
|
+
if (name === "help") continue;
|
|
155
|
+
const description = cmd.description();
|
|
156
|
+
const content = buildCliCommandContent(
|
|
157
|
+
name,
|
|
158
|
+
description,
|
|
159
|
+
cmd.helpInformation(),
|
|
160
|
+
);
|
|
161
|
+
seeds.push({ id: name, description, content });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const nextEntries = new Map<string, CliCommandEntry>();
|
|
165
|
+
let denseVectors: number[][] = [];
|
|
166
|
+
let encodeSparse: (
|
|
167
|
+
input: string,
|
|
168
|
+
) => ReturnType<typeof generateSparseEmbedding> = generateSparseEmbedding;
|
|
169
|
+
if (seeds.length > 0) {
|
|
170
|
+
const embedded = await embedWithBackend(
|
|
171
|
+
config,
|
|
172
|
+
seeds.map((s) => s.content),
|
|
173
|
+
);
|
|
174
|
+
denseVectors = await Promise.all(
|
|
175
|
+
embedded.vectors.map((v) =>
|
|
176
|
+
applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// CLI commands share the concept-page Qdrant collection, so the sparse
|
|
181
|
+
// vector must use the same stemmed BM25 encoding as the concept-page
|
|
182
|
+
// documents. Fall back to the legacy TF encoder only during the cold-
|
|
183
|
+
// start window before corpus stats finish building — same rationale as
|
|
184
|
+
// the skill-store path.
|
|
185
|
+
const corpusStats = getConceptPageCorpusStats();
|
|
186
|
+
encodeSparse = (input: string) =>
|
|
187
|
+
corpusStats
|
|
188
|
+
? generateBm25DocEmbedding(input, corpusStats, {
|
|
189
|
+
k1: config.memory.v2.bm25_k1,
|
|
190
|
+
b: config.memory.v2.bm25_b,
|
|
191
|
+
})
|
|
192
|
+
: generateSparseEmbedding(input);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (generation !== requestedSeedGeneration) {
|
|
196
|
+
log.info(
|
|
197
|
+
{ generation, latestGeneration: requestedSeedGeneration },
|
|
198
|
+
"Skipping stale v2 CLI-command seed result",
|
|
199
|
+
);
|
|
200
|
+
lastSeedError = null;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (seeds.length > 0) {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
await Promise.all(
|
|
207
|
+
seeds.map((seed, i) =>
|
|
208
|
+
upsertConceptPageEmbedding({
|
|
209
|
+
slug: cliCommandSlugFor(seed.id),
|
|
210
|
+
dense: denseVectors[i],
|
|
211
|
+
sparse: encodeSparse(seed.content),
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
kind: CLI_COMMAND_PAYLOAD_KIND,
|
|
214
|
+
}),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
for (const seed of seeds) {
|
|
218
|
+
nextEntries.set(seed.id, seed);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The CLI tree is always available (no remote catalog), so pruning is
|
|
223
|
+
// unconditional. Run the legacy `kind` backfill once per process so
|
|
224
|
+
// pre-discriminator rows become prunable.
|
|
225
|
+
const knownIds = new Set(seeds.map((s) => s.id));
|
|
226
|
+
if (!legacyKindBackfillDone) {
|
|
227
|
+
try {
|
|
228
|
+
await backfillKindOnPointsWithPrefix(
|
|
229
|
+
CLI_COMMAND_SLUG_PREFIX,
|
|
230
|
+
CLI_COMMAND_PAYLOAD_KIND,
|
|
231
|
+
knownIds,
|
|
232
|
+
);
|
|
233
|
+
legacyKindBackfillDone = true;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
log.warn(
|
|
236
|
+
{ err },
|
|
237
|
+
"Failed to backfill kind on legacy CLI-command points — pruning may leave orphans this run",
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
await pruneSlugsWithPrefixExcept(
|
|
242
|
+
CLI_COMMAND_SLUG_PREFIX,
|
|
243
|
+
seeds.map((s) => s.id),
|
|
244
|
+
{ kind: CLI_COMMAND_PAYLOAD_KIND },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// Atomically replace the cache only after every step above succeeds.
|
|
248
|
+
entries = nextEntries;
|
|
249
|
+
invalidatePageIndex();
|
|
250
|
+
lastSeedError = null;
|
|
251
|
+
} catch (err) {
|
|
252
|
+
lastSeedError = err;
|
|
253
|
+
log.warn({ err }, "Failed to seed v2 CLI-command entries");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Synchronous lookup of a previously-seeded `CliCommandEntry` by command
|
|
259
|
+
* name. Returns `null` when the cache has not yet been populated, when the
|
|
260
|
+
* id is unknown, or when a prior seed run dropped the id.
|
|
261
|
+
*
|
|
262
|
+
* Accepts either a bare command name (`attachment`) or its unified-collection
|
|
263
|
+
* slug (`cli-commands/attachment`) so render-side callers can pass through
|
|
264
|
+
* what they have without a manual prefix strip.
|
|
265
|
+
*
|
|
266
|
+
* Returns a frozen copy so callers cannot mutate the underlying cache entry.
|
|
267
|
+
*/
|
|
268
|
+
export function getCliCommandCapability(
|
|
269
|
+
idOrSlug: string,
|
|
270
|
+
): CliCommandEntry | null {
|
|
271
|
+
const id = idOrSlug.startsWith(CLI_COMMAND_SLUG_PREFIX)
|
|
272
|
+
? idOrSlug.slice(CLI_COMMAND_SLUG_PREFIX.length)
|
|
273
|
+
: idOrSlug;
|
|
274
|
+
const entry = entries?.get(id);
|
|
275
|
+
return entry ? Object.freeze({ ...entry }) : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** True iff the slug refers to a CLI-command entry in the unified collection. */
|
|
279
|
+
export function isCliCommandSlug(slug: string): boolean {
|
|
280
|
+
return slug.startsWith(CLI_COMMAND_SLUG_PREFIX);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Snapshot of the in-process CLI-command cache, sorted by command name (ASCII
|
|
285
|
+
* order) for determinism. Returns a freshly allocated array of frozen entry
|
|
286
|
+
* copies on each call.
|
|
287
|
+
*/
|
|
288
|
+
export function listCliCommandEntries(): CliCommandEntry[] {
|
|
289
|
+
if (!entries) return [];
|
|
290
|
+
return [...entries.values()]
|
|
291
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
292
|
+
.map((entry) => Object.freeze({ ...entry }));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @internal Test-only: clear the module-level cache. */
|
|
296
|
+
export function _resetCliCommandStoreForTests(): void {
|
|
297
|
+
entries = null;
|
|
298
|
+
requestedSeedGeneration = 0;
|
|
299
|
+
processedSeedGeneration = 0;
|
|
300
|
+
activeSeedDrain = null;
|
|
301
|
+
seedWaiters.splice(0, seedWaiters.length);
|
|
302
|
+
lastSeedError = null;
|
|
303
|
+
legacyKindBackfillDone = false;
|
|
304
|
+
}
|
|
@@ -22,6 +22,7 @@ import { join } from "node:path";
|
|
|
22
22
|
|
|
23
23
|
import { parse as parseYaml } from "yaml";
|
|
24
24
|
|
|
25
|
+
import type { AssistantConfig } from "../../config/schema.js";
|
|
25
26
|
import { FRONTMATTER_REGEX } from "../../skills/frontmatter.js";
|
|
26
27
|
import { getLogger } from "../../util/logger.js";
|
|
27
28
|
import { listPages } from "./page-store.js";
|
|
@@ -32,11 +33,16 @@ const log = getLogger("memory-v2-frontmatter-sweep");
|
|
|
32
33
|
/**
|
|
33
34
|
* Validate every concept page's frontmatter against the strict schema and
|
|
34
35
|
* emit a `warn` per offender. Never throws — daemon startup must not block
|
|
35
|
-
* on this safety net.
|
|
36
|
+
* on this safety net. Self-gates on `config.memory.v2.enabled`: when v2
|
|
37
|
+
* is off, concept pages never enter a retrieval top-K so any warns here
|
|
38
|
+
* would be pure noise.
|
|
36
39
|
*/
|
|
37
40
|
export async function sweepConceptPageFrontmatter(
|
|
41
|
+
config: AssistantConfig,
|
|
38
42
|
workspaceDir: string,
|
|
39
43
|
): Promise<void> {
|
|
44
|
+
if (!config.memory.v2.enabled) return;
|
|
45
|
+
|
|
40
46
|
let slugs: string[];
|
|
41
47
|
try {
|
|
42
48
|
slugs = await listPages(workspaceDir);
|