akm-cli 0.9.0-beta.50 → 0.9.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/wiki/ingest-workflow-template.md +34 -12
  6. package/dist/assets/wiki/schema-template.md +4 -4
  7. package/dist/cli/parse-args.js +46 -1
  8. package/dist/cli.js +12 -6
  9. package/dist/commands/config-cli.js +18 -2
  10. package/dist/commands/env/child-env.js +47 -0
  11. package/dist/commands/env/env-cli.js +17 -2
  12. package/dist/commands/env/secret-cli.js +24 -2
  13. package/dist/commands/health/checks.js +1 -1
  14. package/dist/commands/improve/improve-auto-accept.js +30 -2
  15. package/dist/commands/improve/improve-cli.js +1 -1
  16. package/dist/commands/improve/improve-result-file.js +9 -2
  17. package/dist/commands/improve/preparation.js +10 -2
  18. package/dist/commands/improve/recombine.js +52 -15
  19. package/dist/commands/lint/env-key-rules.js +4 -0
  20. package/dist/commands/read/knowledge.js +5 -2
  21. package/dist/commands/read/search-cli.js +2 -4
  22. package/dist/commands/read/search.js +9 -6
  23. package/dist/commands/read/show.js +19 -5
  24. package/dist/commands/sources/init.js +13 -8
  25. package/dist/commands/sources/installed-stashes.js +6 -2
  26. package/dist/commands/sources/schema-repair.js +33 -47
  27. package/dist/commands/sources/source-add.js +7 -3
  28. package/dist/commands/tasks/tasks.js +38 -10
  29. package/dist/core/asset/asset-registry.js +1 -1
  30. package/dist/core/asset/asset-spec.js +4 -2
  31. package/dist/core/config/config-migration.js +12 -11
  32. package/dist/indexer/passes/memory-inference.js +3 -2
  33. package/dist/indexer/search/db-search.js +6 -4
  34. package/dist/indexer/search/search-source.js +15 -2
  35. package/dist/integrations/agent/prompts.js +1 -1
  36. package/dist/llm/memory-infer-impl.js +138 -0
  37. package/dist/llm/memory-infer.js +1 -135
  38. package/dist/migrate-storage-node.mjs +8 -0
  39. package/dist/output/renderers.js +1 -1
  40. package/dist/scripts/migrate-storage.js +463 -347
  41. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
  42. package/dist/sources/include.js +6 -2
  43. package/dist/sources/providers/git-install.js +10 -6
  44. package/dist/sources/providers/provider-utils.js +13 -7
  45. package/dist/sources/providers/website.js +8 -3
  46. package/dist/sources/website-ingest.js +136 -20
  47. package/dist/text-import-hook.mjs +0 -0
  48. package/dist/wiki/wiki.js +15 -11
  49. package/docs/data-and-telemetry.md +2 -2
  50. package/docs/migration/release-notes/0.9.0.md +39 -0
  51. package/package.json +8 -8
@@ -41,7 +41,7 @@ export const ACTION_BUILDERS = {
41
41
  lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
42
42
  memory: (ref) => `akm show ${ref} -> recall context`,
43
43
  workflow: (ref) => buildWorkflowAction(ref),
44
- env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (the agent-safe path values never reach stdout). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
44
+ env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
45
45
  secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
46
46
  wiki: (ref) => `akm show ${ref} -> read the wiki page`,
47
47
  task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
@@ -3,8 +3,10 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import path from "node:path";
5
5
  import { buildWorkflowAction } from "../../output/renderers.js";
6
- import { toPosix } from "../common.js";
7
6
  import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
7
+ function toPosix(input) {
8
+ return input.replace(/\\/g, "/");
9
+ }
8
10
  const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
9
11
  const markdownSpec = {
10
12
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
@@ -89,7 +91,7 @@ const ASSET_SPECS_INTERNAL = {
89
91
  return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
90
92
  },
91
93
  rendererName: "env-file",
92
- actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (values never reach stdout); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
94
+ actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
93
95
  },
94
96
  // Secrets — a single sensitive value used on its own for authentication (a
95
97
  // PEM key, API token, TLS cert). Unlike `env` (a group of related .env
@@ -169,7 +169,8 @@ function migrateProcessEntryToImprove(result, processName, legacy) {
169
169
  * (canonical string sentinel for this release) or when `configVersion` is a
170
170
  * number ≥ 2 AND the config carries no recognized legacy keys.
171
171
  */
172
- export function migrateConfigShape(raw) {
172
+ export function migrateConfigShape(raw, opts) {
173
+ const emitWarn = opts?.warn ?? warn;
173
174
  const hasLegacyKeys = Object.hasOwn(raw, "features") ||
174
175
  (isObj(raw.llm) && (Object.hasOwn(raw.llm, "endpoint") || Object.hasOwn(raw.llm, "features"))) ||
175
176
  isObj(raw.agent) ||
@@ -199,11 +200,11 @@ export function migrateConfigShape(raw) {
199
200
  if (Array.isArray(result.stashes)) {
200
201
  if (!Array.isArray(result.sources)) {
201
202
  result.sources = result.stashes;
202
- console.warn("[akm config-migrate] Legacy `stashes[]` config key renamed to `sources[]`. " +
203
+ emitWarn("[akm config-migrate] Legacy `stashes[]` config key renamed to `sources[]`. " +
203
204
  "Re-save your config to remove the deprecation notice.");
204
205
  }
205
206
  else {
206
- console.warn("[akm config-migrate] Both `stashes[]` and `sources[]` present; `stashes[]` dropped (sources takes precedence).");
207
+ emitWarn("[akm config-migrate] Both `stashes[]` and `sources[]` present; `stashes[]` dropped (sources takes precedence).");
207
208
  }
208
209
  delete result.stashes;
209
210
  changed = true;
@@ -218,7 +219,7 @@ export function migrateConfigShape(raw) {
218
219
  for (const entry of sources) {
219
220
  if (isObj(entry) && entry.type === "openviking") {
220
221
  const name = typeof entry.name === "string" && entry.name ? entry.name : "unnamed";
221
- console.warn(`[akm config-migrate] Source "${name}" (type: openviking) is no longer supported. ` +
222
+ emitWarn(`[akm config-migrate] Source "${name}" (type: openviking) is no longer supported. ` +
222
223
  "Remove it from your config, or replace with a `website`/`git` source. " +
223
224
  "Entry dropped from sources[].");
224
225
  renamed = true;
@@ -349,11 +350,11 @@ export function migrateConfigShape(raw) {
349
350
  const camelKey = toCamelCase(legacyKey);
350
351
  if (typeof legacyVal === "boolean" || isObj(legacyVal)) {
351
352
  migrateProcessEntryToImprove(result, camelKey, legacyVal);
352
- warn(`[akm config-migrate] Unknown features.improve.${legacyKey} migrated to ` +
353
+ emitWarn(`[akm config-migrate] Unknown features.improve.${legacyKey} migrated to ` +
353
354
  `profiles.improve.default.processes.${camelKey}. Please verify the new location.`);
354
355
  }
355
356
  else {
356
- warn(`[akm config-migrate] features.improve.${legacyKey} has an unrecognized value type ` +
357
+ emitWarn(`[akm config-migrate] features.improve.${legacyKey} has an unrecognized value type ` +
357
358
  `(${typeof legacyVal}); dropping. Please re-add it under profiles.improve.* manually.`);
358
359
  }
359
360
  }
@@ -404,11 +405,11 @@ export function migrateConfigShape(raw) {
404
405
  const camelKey = toCamelCase(legacyKey);
405
406
  const ok = migrateGenericGateToSection(result, "index", camelKey, legacyVal);
406
407
  if (ok) {
407
- warn(`[akm config-migrate] Unknown features.index.${legacyKey} migrated to ` +
408
+ emitWarn(`[akm config-migrate] Unknown features.index.${legacyKey} migrated to ` +
408
409
  `index.${camelKey}. Please verify the new location is correct.`);
409
410
  }
410
411
  else {
411
- warn(`[akm config-migrate] features.index.${legacyKey} has an unrecognized value shape; ` +
412
+ emitWarn(`[akm config-migrate] features.index.${legacyKey} has an unrecognized value shape; ` +
412
413
  `dropping. Please re-add it under index.${camelKey} manually if needed.`);
413
414
  }
414
415
  }
@@ -434,11 +435,11 @@ export function migrateConfigShape(raw) {
434
435
  const camelKey = toCamelCase(legacyKey);
435
436
  const ok = migrateGenericGateToSection(result, "search", camelKey, legacyVal);
436
437
  if (ok) {
437
- warn(`[akm config-migrate] Unknown features.search.${legacyKey} migrated to ` +
438
+ emitWarn(`[akm config-migrate] Unknown features.search.${legacyKey} migrated to ` +
438
439
  `search.${camelKey}. Please verify the new location is correct.`);
439
440
  }
440
441
  else {
441
- warn(`[akm config-migrate] features.search.${legacyKey} has an unrecognized value shape; ` +
442
+ emitWarn(`[akm config-migrate] features.search.${legacyKey} has an unrecognized value shape; ` +
442
443
  `dropping. Please re-add it under search.${camelKey} manually if needed.`);
443
444
  }
444
445
  }
@@ -555,7 +556,7 @@ export function migrateConfigShape(raw) {
555
556
  changed = true;
556
557
  }
557
558
  if (improveObj.preset !== undefined) {
558
- console.warn("[akm config-migrate] defaults.improve.preset is no longer supported. " +
559
+ emitWarn("[akm config-migrate] defaults.improve.preset is no longer supported. " +
559
560
  "Use `--profile <name>` (built-ins: default, quick, thorough, memory-focus) instead.");
560
561
  }
561
562
  delete defaultsRaw.improve;
@@ -72,6 +72,7 @@ const FM_CAPTURE_MODE = "captureMode";
72
72
  */
73
73
  export async function runMemoryInferencePass(ctx) {
74
74
  const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
75
+ const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
75
76
  const result = {
76
77
  considered: 0,
77
78
  cacheHits: 0,
@@ -172,14 +173,14 @@ export async function runMemoryInferencePass(ctx) {
172
173
  retryAttempts += 1;
173
174
  };
174
175
  const derived = db
175
- ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
+ ? await withLlmCache(db, record.filePath, record.body, reEnrich ?? false, () => compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
176
177
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
177
178
  }, inferTelemetry, onRetryAttempt), validate, undefined, "", {
178
179
  onCacheHit: () => {
179
180
  fromCache = true;
180
181
  },
181
182
  })
182
- : await memoryInfer.compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
+ : await compressMemoryToDerivedMemory(llmConfig, record.body, signal, config, (evt) => {
183
184
  warn(`[akm] LLM fallback for ${evt.feature}: ${evt.reason}`);
184
185
  }, inferTelemetry, onRetryAttempt);
185
186
  if (!derived) {
@@ -66,6 +66,8 @@ export async function searchLocal(input) {
66
66
  const beliefFilter = input.beliefFilter ?? "all";
67
67
  const restrictToSources = input.restrictToSources === true;
68
68
  const includeExcludedTypes = input.includeExcludedTypes === true;
69
+ const disableProjectContext = input.disableProjectContext === true;
70
+ const disableScopedUtility = input.disableScopedUtility === true;
69
71
  const rendererRegistry = input.rendererRegistry ?? defaultRendererRegistry;
70
72
  const allSourceDirs = sources.map((s) => s.path);
71
73
  const rawStatus = readSemanticStatus();
@@ -115,7 +117,7 @@ export async function searchLocal(input) {
115
117
  mode: "keyword",
116
118
  };
117
119
  }
118
- const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes);
120
+ const { hits, embedMs, rankMs } = await searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry, filters, includeProposed, beliefFilter, restrictToSources, includeExcludedTypes, disableProjectContext, disableScopedUtility);
119
121
  return {
120
122
  hits,
121
123
  tip: hits.length === 0
@@ -132,7 +134,7 @@ export async function searchLocal(input) {
132
134
  }
133
135
  }
134
136
  // ── Database search ─────────────────────────────────────────────────────────
135
- async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false) {
137
+ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceDirs, config, sources, rendererRegistry = defaultRendererRegistry, filters, includeProposed = false, beliefFilter = "all", restrictToSources = false, includeExcludedTypes = false, disableProjectContext = false, disableScopedUtility = false) {
136
138
  const hasSearchableTokens = query.length > 0 && sanitizeFtsQuery(query).length > 0;
137
139
  // #627 — resolve the default type-exclusion policy. It applies ONLY on the
138
140
  // untyped ('any') path and only when the caller did not opt back in via
@@ -247,7 +249,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
247
249
  // Resolve project-context tokens from the current working directory once
248
250
  // per search invocation. Returns null when running from home dir / /tmp,
249
251
  // or when the caller has set AKM_DISABLE_PROJECT_CONTEXT=1.
250
- const projectContext = process.env.AKM_DISABLE_PROJECT_CONTEXT === "1" ? null : resolveProjectContext(process.cwd());
252
+ const projectContext = disableProjectContext ? null : resolveProjectContext(process.cwd());
251
253
  // Phase 2A / Rec 5: resolve forgetting-curve config and skip the feedback
252
254
  // count query when the boost cannot make a difference (default ≤ 1.0 means
253
255
  // boost^count == 1 — zero overhead for the common case).
@@ -267,7 +269,7 @@ async function searchDatabase(db, query, searchType, limit, stashDir, allSourceD
267
269
  // AKM_DISABLE_SCOPED_UTILITY=1 opts out (e.g. for registry searches or tests).
268
270
  let scopeKey;
269
271
  try {
270
- scopeKey = process.env.AKM_DISABLE_SCOPED_UTILITY === "1" ? undefined : getCurrentWorkflowScopeKey();
272
+ scopeKey = disableScopedUtility ? undefined : getCurrentWorkflowScopeKey();
271
273
  }
272
274
  catch {
273
275
  // Non-fatal — ranking proceeds without scoped utility on any error.
@@ -5,7 +5,7 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { resolveStashDir } from "../../core/common.js";
7
7
  import { getSources, loadConfig } from "../../core/config/config.js";
8
- import { resolveSourceProviderFactory, resolveSourceProviders } from "../../sources/provider-factory.js";
8
+ import { resolveSourceProviderFactory } from "../../sources/provider-factory.js";
9
9
  // Eager side-effect imports so all built-in source providers self-register
10
10
  // before resolveEntryContentDir() runs.
11
11
  import "../../sources/providers/index.js";
@@ -264,7 +264,20 @@ export async function ensureSourceCaches(config, options) {
264
264
  // refreshes the same way — a bad source warns and is skipped without
265
265
  // aborting the others. The git content/-subdir layout convention stays in
266
266
  // resolveEntryContentDir.
267
- for (const provider of resolveSourceProviders(cfg)) {
267
+ for (const entry of getSources(cfg)) {
268
+ if (entry.enabled === false)
269
+ continue;
270
+ const factory = resolveSourceProviderFactory(entry.type);
271
+ if (!factory)
272
+ continue;
273
+ let provider;
274
+ try {
275
+ provider = factory(entry);
276
+ }
277
+ catch (err) {
278
+ warn(`Warning: failed to construct ${entry.type} source provider for "${entry.name ?? entry.url ?? entry.path}": ${err instanceof Error ? err.message : String(err)}`);
279
+ continue;
280
+ }
268
281
  if (!provider.sync)
269
282
  continue;
270
283
  try {
@@ -42,7 +42,7 @@ const TYPE_HINTS = {
42
42
  memory: "memory assets are short factual notes the user wants persisted across sessions. Frontmatter usually includes `description`.",
43
43
  workflow: "workflow assets are markdown describing a multi-step process. Include `# <Title>` and ordered `## Step N` sections.",
44
44
  script: "script assets are executable text files. Include a shebang and minimal usage comment.",
45
- env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd>` (the safe path values never reach stdout/your context); do NOT run `akm env export` and read its output, as that prints values. For a single sensitive value used on its own for authentication (token, key, cert) use a `secret` instead. Never echo values back to the user.",
45
+ env: "env assets are `.env` files holding a group of related CONFIGURATION for an app/service (KEY=VALUE pairs, `#` comments) — URLs, flags, and any credentials it needs. Values may or may not be sensitive; all are protected (key names discoverable, values stay on disk). Inject with `akm env run env:<name> -- <cmd>`; prefer `--clean` in agent contexts so the child starts from a minimal inherited environment. AKM itself does not print values, but the child command can print its environment, so do not run `env`, `printenv`, shell tracing, or similar diagnostics when secrets are in scope. For a single sensitive value used on its own for authentication (token, key, cert) use a `secret` instead. Never echo values back to the user.",
46
46
  wiki: "wiki assets are markdown reference pages with `# Title` and structured headings.",
47
47
  fact: "fact assets are durable stash-level facts (personal/team/project details, coding conventions, stash-meta). Frontmatter SHOULD include `description` and a `category` (personal|team|project|convention|meta); set `pinned: true` only for the small always-injected core. Keep each fact short, high-signal, and self-contained — it is durable context, not an episodic note.",
48
48
  };
@@ -0,0 +1,138 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * LLM helper for the `akm index` memory-inference pass (#201).
6
+ *
7
+ * Compresses a single memory body into one higher-signal derived memory. The
8
+ * pass itself (in `src/indexer/memory-inference.ts`) is responsible for
9
+ * deciding which memories are pending, persisting the derived memory with the
10
+ * correct frontmatter (`inferred: true`, `source: <parent-ref>`), and marking
11
+ * the parent as processed for idempotency.
12
+ *
13
+ * This module is intentionally tiny and stateless so tests can stub it via
14
+ * `mock.module("../src/llm/memory-infer", ...)` without hitting a network.
15
+ *
16
+ * Locked v1 contract (#208): the LLM connection always comes from the
17
+ * shared `akm.llm` block — never from a per-pass override. Callers obtain
18
+ * the connection via `resolveIndexPassLLM("memory", config)` and pass it
19
+ * straight through.
20
+ */
21
+ import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" };
22
+ import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" };
23
+ import { toErrorMessage } from "../core/common.js";
24
+ import { warn } from "../core/warn.js";
25
+ import { parseEmbeddedJsonResponse } from "./client.js";
26
+ import { callStructured } from "./structured-call.js";
27
+ /** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
28
+ const MAX_BODY_CHARS = 4000;
29
+ const SYSTEM_PROMPT = memoryInferSystemPrompt;
30
+ const USER_PROMPT_PREFIX = memoryInferUserPrompt;
31
+ /**
32
+ * Strict JSON Schema for the derived-memory payload. Sent to providers that
33
+ * opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
34
+ * silently drops the schema for providers that don't.
35
+ *
36
+ * Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to
37
+ * the memory-inference path. Mirrors the validation gate below
38
+ * (title/description/content + non-empty tags/searchHints) so a
39
+ * schema-compliant response is guaranteed to pass the downstream check
40
+ * — no more "incomplete derived memory payload from LLM; skipping memory"
41
+ * for shape-only failures.
42
+ */
43
+ const DERIVED_MEMORY_JSON_SCHEMA = {
44
+ type: "object",
45
+ properties: {
46
+ title: { type: "string", minLength: 1 },
47
+ description: { type: "string", minLength: 1 },
48
+ content: { type: "string", minLength: 1 },
49
+ tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
50
+ searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 },
51
+ },
52
+ required: ["title", "description", "content", "tags", "searchHints"],
53
+ additionalProperties: false,
54
+ };
55
+ /**
56
+ * Compress a single memory body into one derived memory via the configured LLM.
57
+ *
58
+ * Returns `undefined` on any failure (timeout, invalid JSON, empty response).
59
+ * Errors are logged via `warn()` but never thrown — a failed split for one memory
60
+ * must not abort the rest of the index pass.
61
+ *
62
+ * Routes through `callStructured({ feature: "memory_inference", ... })` so the
63
+ * feature gate, error classification, and onFallback hook are honoured uniformly
64
+ * (Fix C5).
65
+ */
66
+ export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt) {
67
+ const trimmedBody = body.trim();
68
+ if (!trimmedBody)
69
+ return undefined;
70
+ const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody.slice(0, MAX_BODY_CHARS)}`;
71
+ // Memory-inference is ALWAYS gated: no `akmConfig` ⇒ gate closed (no chat,
72
+ // `disabled` fallback), never the seam's ungated/propagate path (which is for
73
+ // direct callers like `enhanceMetadata`). This is the gate-closed branch
74
+ // `tryLlmFeature(_, undefined, _)` took before the migration.
75
+ if (!akmConfig) {
76
+ onFallback?.({ feature: "memory_inference", reason: "disabled" });
77
+ return undefined;
78
+ }
79
+ return callStructured({
80
+ feature: "memory_inference",
81
+ akmConfig,
82
+ config: llmConfig,
83
+ messages: [
84
+ { role: "system", content: SYSTEM_PROMPT },
85
+ { role: "user", content: userPrompt },
86
+ ],
87
+ request: {
88
+ temperature: 0.1,
89
+ timeoutMs: llmConfig.timeoutMs,
90
+ signal,
91
+ responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
92
+ onRetryAttempt,
93
+ },
94
+ parse: (raw) => {
95
+ if (!raw)
96
+ return undefined;
97
+ const parsed = parseEmbeddedJsonResponse(raw);
98
+ if (!parsed) {
99
+ warn("memory inference: invalid JSON response from LLM; skipping memory.");
100
+ return undefined;
101
+ }
102
+ const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
103
+ const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
104
+ const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
105
+ const tags = Array.isArray(parsed.tags)
106
+ ? parsed.tags
107
+ .filter((t) => typeof t === "string")
108
+ .map((t) => t.trim())
109
+ .filter(Boolean)
110
+ .slice(0, 8)
111
+ : [];
112
+ const searchHints = Array.isArray(parsed.searchHints)
113
+ ? parsed.searchHints
114
+ .filter((h) => typeof h === "string")
115
+ .map((h) => h.trim())
116
+ .filter(Boolean)
117
+ .slice(0, 6)
118
+ : [];
119
+ if (!title || !description || !content || tags.length === 0 || searchHints.length === 0) {
120
+ warn("memory inference: incomplete derived memory payload from LLM; skipping memory.");
121
+ return undefined;
122
+ }
123
+ return { title, description, tags, searchHints, content };
124
+ },
125
+ onError: (cls, err) => {
126
+ if (cls === "html") {
127
+ if (telemetry)
128
+ telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1;
129
+ warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`);
130
+ return undefined;
131
+ }
132
+ warn(`memory inference failed: ${toErrorMessage(err)}`);
133
+ return undefined;
134
+ },
135
+ fallback: undefined,
136
+ onFallback,
137
+ });
138
+ }
@@ -1,138 +1,4 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * LLM helper for the `akm index` memory-inference pass (#201).
6
- *
7
- * Compresses a single memory body into one higher-signal derived memory. The
8
- * pass itself (in `src/indexer/memory-inference.ts`) is responsible for
9
- * deciding which memories are pending, persisting the derived memory with the
10
- * correct frontmatter (`inferred: true`, `source: <parent-ref>`), and marking
11
- * the parent as processed for idempotency.
12
- *
13
- * This module is intentionally tiny and stateless so tests can stub it via
14
- * `mock.module("../src/llm/memory-infer", ...)` without hitting a network.
15
- *
16
- * Locked v1 contract (#208): the LLM connection always comes from the
17
- * shared `akm.llm` block — never from a per-pass override. Callers obtain
18
- * the connection via `resolveIndexPassLLM("memory", config)` and pass it
19
- * straight through.
20
- */
21
- import memoryInferSystemPrompt from "../assets/prompts/memory-infer-system.md" with { type: "text" };
22
- import memoryInferUserPrompt from "../assets/prompts/memory-infer-user.md" with { type: "text" };
23
- import { toErrorMessage } from "../core/common.js";
24
- import { warn } from "../core/warn.js";
25
- import { parseEmbeddedJsonResponse } from "./client.js";
26
- import { callStructured } from "./structured-call.js";
27
- /** Hard cap on body chars sent to the model — pragmatic and matches `runLlmEnrich`. */
28
- const MAX_BODY_CHARS = 4000;
29
- const SYSTEM_PROMPT = memoryInferSystemPrompt;
30
- const USER_PROMPT_PREFIX = memoryInferUserPrompt;
31
- /**
32
- * Strict JSON Schema for the derived-memory payload. Sent to providers that
33
- * opt in via `LlmConnectionConfig.supportsJsonSchema = true`; the client
34
- * silently drops the schema for providers that don't.
35
- *
36
- * Extends the responseSchema lift (PR 1, asset-writers-investigation §5) to
37
- * the memory-inference path. Mirrors the validation gate below
38
- * (title/description/content + non-empty tags/searchHints) so a
39
- * schema-compliant response is guaranteed to pass the downstream check
40
- * — no more "incomplete derived memory payload from LLM; skipping memory"
41
- * for shape-only failures.
42
- */
43
- const DERIVED_MEMORY_JSON_SCHEMA = {
44
- type: "object",
45
- properties: {
46
- title: { type: "string", minLength: 1 },
47
- description: { type: "string", minLength: 1 },
48
- content: { type: "string", minLength: 1 },
49
- tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 8 },
50
- searchHints: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 6 },
51
- },
52
- required: ["title", "description", "content", "tags", "searchHints"],
53
- additionalProperties: false,
54
- };
55
- /**
56
- * Compress a single memory body into one derived memory via the configured LLM.
57
- *
58
- * Returns `undefined` on any failure (timeout, invalid JSON, empty response).
59
- * Errors are logged via `warn()` but never thrown — a failed split for one memory
60
- * must not abort the rest of the index pass.
61
- *
62
- * Routes through `callStructured({ feature: "memory_inference", ... })` so the
63
- * feature gate, error classification, and onFallback hook are honoured uniformly
64
- * (Fix C5).
65
- */
66
- export async function compressMemoryToDerivedMemory(llmConfig, body, signal, akmConfig, onFallback, telemetry, onRetryAttempt) {
67
- const trimmedBody = body.trim();
68
- if (!trimmedBody)
69
- return undefined;
70
- const userPrompt = `${USER_PROMPT_PREFIX}${trimmedBody.slice(0, MAX_BODY_CHARS)}`;
71
- // Memory-inference is ALWAYS gated: no `akmConfig` ⇒ gate closed (no chat,
72
- // `disabled` fallback), never the seam's ungated/propagate path (which is for
73
- // direct callers like `enhanceMetadata`). This is the gate-closed branch
74
- // `tryLlmFeature(_, undefined, _)` took before the migration.
75
- if (!akmConfig) {
76
- onFallback?.({ feature: "memory_inference", reason: "disabled" });
77
- return undefined;
78
- }
79
- return callStructured({
80
- feature: "memory_inference",
81
- akmConfig,
82
- config: llmConfig,
83
- messages: [
84
- { role: "system", content: SYSTEM_PROMPT },
85
- { role: "user", content: userPrompt },
86
- ],
87
- request: {
88
- temperature: 0.1,
89
- timeoutMs: llmConfig.timeoutMs,
90
- signal,
91
- responseSchema: DERIVED_MEMORY_JSON_SCHEMA,
92
- onRetryAttempt,
93
- },
94
- parse: (raw) => {
95
- if (!raw)
96
- return undefined;
97
- const parsed = parseEmbeddedJsonResponse(raw);
98
- if (!parsed) {
99
- warn("memory inference: invalid JSON response from LLM; skipping memory.");
100
- return undefined;
101
- }
102
- const title = typeof parsed.title === "string" ? parsed.title.trim() : "";
103
- const description = typeof parsed.description === "string" ? parsed.description.trim() : "";
104
- const content = typeof parsed.content === "string" ? parsed.content.trim() : "";
105
- const tags = Array.isArray(parsed.tags)
106
- ? parsed.tags
107
- .filter((t) => typeof t === "string")
108
- .map((t) => t.trim())
109
- .filter(Boolean)
110
- .slice(0, 8)
111
- : [];
112
- const searchHints = Array.isArray(parsed.searchHints)
113
- ? parsed.searchHints
114
- .filter((h) => typeof h === "string")
115
- .map((h) => h.trim())
116
- .filter(Boolean)
117
- .slice(0, 6)
118
- : [];
119
- if (!title || !description || !content || tags.length === 0 || searchHints.length === 0) {
120
- warn("memory inference: incomplete derived memory payload from LLM; skipping memory.");
121
- return undefined;
122
- }
123
- return { title, description, tags, searchHints, content };
124
- },
125
- onError: (cls, err) => {
126
- if (cls === "html") {
127
- if (telemetry)
128
- telemetry.htmlErrorCount = (telemetry.htmlErrorCount ?? 0) + 1;
129
- warn(`memory inference: provider returned HTML instead of JSON; skipping memory: ${toErrorMessage(err)}`);
130
- return undefined;
131
- }
132
- warn(`memory inference failed: ${toErrorMessage(err)}`);
133
- return undefined;
134
- },
135
- fallback: undefined,
136
- onFallback,
137
- });
138
- }
4
+ export { compressMemoryToDerivedMemory, } from "./memory-infer-impl.js";
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // This Source Code Form is subject to the terms of the Mozilla Public
3
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+
6
+ const { main } = await import("./scripts/migrate-storage.js");
7
+
8
+ await main();
@@ -377,7 +377,7 @@ const envFileRenderer = {
377
377
  type: "env",
378
378
  name,
379
379
  path: ctx.absPath,
380
- action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected (the safe path values never reach stdout). `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
380
+ action: "Environment — keys + comments only. Use `akm env run <ref> -- <command>` to run with the whole .env injected; prefer `--clean` to minimize inherited parent env. AKM itself does not print values, but child stdout/stderr is not redacted. `akm env export <ref> --out <file>` writes a sourceable script to a file. Never `source` the raw file. Values stay on disk and are never written to akm's stdout.",
381
381
  description: comments.length > 0 ? comments.join("\n") : undefined,
382
382
  keys,
383
383
  comments,