akm-cli 0.10.261016-beta → 0.10.261018-beta

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 (113) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +8 -10
  3. package/SECURITY.md +7 -99
  4. package/STABILITY.md +57 -98
  5. package/dist/assets/opencode/akm-plugin.js +103 -1
  6. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
  7. package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
  8. package/dist/cli.js +32 -41
  9. package/dist/commands/feedback-cli.js +5 -4
  10. package/dist/commands/health.js +15 -16
  11. package/dist/commands/proposal/proposal-cli.js +1 -1
  12. package/dist/commands/read/registry-search.js +4 -1
  13. package/dist/commands/read/search.js +6 -6
  14. package/dist/commands/sources/self-update.js +9 -5
  15. package/dist/commands/tasks/tasks.js +12 -13
  16. package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
  17. package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
  18. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  19. package/dist/core/adapter/adapters/claude-adapter.js +1 -1
  20. package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
  21. package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
  22. package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
  23. package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
  24. package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
  25. package/dist/core/bundle-rename.js +17 -7
  26. package/dist/core/config/convert-engine-references.js +23 -7
  27. package/dist/core/config/convert-engines.js +94 -28
  28. package/dist/core/config/convert-models-overlay.js +14 -2
  29. package/dist/indexer/bundle-identity-guard.js +6 -6
  30. package/dist/indexer/index-written-assets.js +3 -3
  31. package/dist/indexer/indexer.js +12 -8
  32. package/dist/indexer/materialize-embeddings.js +12 -2
  33. package/dist/indexer/passes/metadata.js +103 -46
  34. package/dist/indexer/search/ranking.js +7 -3
  35. package/dist/indexer/search/search-fields.js +1 -1
  36. package/dist/indexer/usage/usage-events.js +30 -8
  37. package/dist/indexer/walk/walker.js +1 -1
  38. package/dist/integrations/agent/spawn.js +7 -2
  39. package/dist/integrations/harnesses/opencode/agent-builder.js +10 -1
  40. package/dist/integrations/harnesses/opencode/runtime/binary.js +6 -3
  41. package/dist/integrations/harnesses/opencode/runtime/index.js +18 -9
  42. package/dist/integrations/harnesses/opencode/session-log-v2.js +11 -1
  43. package/dist/output/shapes/helpers.js +3 -3
  44. package/dist/output/text/command-format.js +5 -6
  45. package/dist/scripts/akm-migrate-node.js +122 -36
  46. package/dist/scripts/akm-migrate.js +122 -36
  47. package/dist/sources/providers/git-provider.js +2 -4
  48. package/dist/sources/providers/provider-utils.js +8 -13
  49. package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
  50. package/dist/storage/repositories/events-repository.js +15 -0
  51. package/dist/storage/repositories/improve-ledger-repository.js +13 -0
  52. package/dist/storage/repositories/index-entries-repository.js +43 -5
  53. package/dist/storage/repositories/outcome-repository.js +12 -0
  54. package/dist/storage/repositories/proposals-repository.js +5 -5
  55. package/dist/storage/repositories/salience-repository.js +13 -0
  56. package/dist/storage/repositories/task-history-repository.js +8 -7
  57. package/dist/storage/repositories/workflow-runs-repository.js +10 -8
  58. package/docs/README.md +24 -60
  59. package/docs/agents/README.md +3 -8
  60. package/docs/architecture/README.md +18 -20
  61. package/docs/architecture/adapters.md +40 -43
  62. package/docs/architecture/architecture.md +180 -154
  63. package/docs/architecture/improvement.md +95 -91
  64. package/docs/architecture/internals/classification.md +101 -119
  65. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
  66. package/docs/architecture/internals/health-advisories.md +59 -46
  67. package/docs/architecture/internals/improve-workflow.md +166 -693
  68. package/docs/architecture/internals/indexing.md +65 -465
  69. package/docs/architecture/internals/storage-locations.md +301 -848
  70. package/docs/architecture/persisted-data-compat.md +6 -6
  71. package/docs/architecture/specs/okf-support.md +67 -95
  72. package/docs/architecture/specs/ref.md +67 -125
  73. package/docs/architecture/testing/testing-workflow.md +108 -168
  74. package/docs/architecture/workflow-engine.md +34 -33
  75. package/docs/guides/README.md +5 -5
  76. package/docs/guides/author-bundles.md +72 -98
  77. package/docs/guides/author-workflows.md +11 -18
  78. package/docs/guides/bundles.md +91 -123
  79. package/docs/guides/capture-knowledge.md +10 -9
  80. package/docs/guides/concepts.md +45 -30
  81. package/docs/guides/discover-and-load.md +39 -32
  82. package/docs/guides/getting-started.md +24 -24
  83. package/docs/guides/improve-the-library.md +98 -80
  84. package/docs/guides/recipes/headless-install.md +4 -4
  85. package/docs/guides/recipes/website-source.md +9 -5
  86. package/docs/guides/run-workflows.md +10 -11
  87. package/docs/guides/scheduling.md +11 -17
  88. package/docs/guides/use-with-any-agent.md +15 -14
  89. package/docs/guides/wikis.md +6 -9
  90. package/docs/integration/bundling-akm.md +21 -26
  91. package/docs/maintainers/README.md +2 -2
  92. package/docs/maintainers/local-development.md +3 -16
  93. package/docs/migration/README.md +2 -1
  94. package/docs/migration/release-notes/0.10.0.md +196 -23
  95. package/docs/migration/release-notes/README.md +2 -2
  96. package/docs/migration/v0.9-to-v0.10.md +103 -12
  97. package/docs/product-surface.md +10 -8
  98. package/docs/reference/README.md +26 -15
  99. package/docs/reference/asset-types.md +11 -12
  100. package/docs/reference/bundle-types.md +6 -5
  101. package/docs/reference/cli.md +52 -28
  102. package/docs/reference/configuration.md +91 -73
  103. package/docs/reference/data-and-telemetry.md +204 -421
  104. package/docs/reference/env-and-secrets.md +80 -83
  105. package/docs/reference/memory.md +1 -1
  106. package/docs/reference/refs.md +23 -36
  107. package/docs/reference/registry.md +138 -348
  108. package/docs/reference/supported-formats.md +8 -8
  109. package/docs/reference/tasks.md +13 -19
  110. package/docs/reference/website-sources.md +23 -15
  111. package/docs/reference/workflow-schema.md +28 -47
  112. package/docs/reference/workflows.md +33 -37
  113. package/package.json +1 -1
@@ -6,7 +6,9 @@
6
6
  * bundle's key. A bundle id is a mass identity prefix: every durable ref this
7
7
  * tool minted (`entries.item_ref`/`bundle_id`, `proposals.ref`, a pending
8
8
  * proposal's `proposedTarget.source`, a workflow task_history row's
9
- * `target_ref`, a `workflow_runs.workflow_ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
9
+ * `target_ref`, a `workflow_runs.workflow_ref`, a `usage_events.entry_ref`, an
10
+ * `events.ref`, an `asset_salience`/`asset_outcome` `asset_ref`, an
11
+ * `improve_ledger.ref`, `scheduler.enabled[].ref`) carries it, and a hand-edited
10
12
  * `bundles` key strands all of it (see `src/indexer/bundle-identity-guard.ts`
11
13
  * §11.5). This module is the rekey transaction that guard's docstring points
12
14
  * to.
@@ -20,17 +22,17 @@
20
22
  * `akm mv`/editing territory, not a config-identity rename. `--dry-run`
21
23
  * (and every real run) lists the indexed files that still spell the old
22
24
  * `<old>//` prefix so the operator can follow up.
23
- *
24
- * Left behind on purpose: `usage_events`/`asset_salience`/`asset_outcome`
25
- * keyed by the old bundle prefix (learned utility/outcome history) — outside
26
- * this item's scope; `warnOnBundleRenameDrift` already documents this class
27
- * of gap and `akm index --full` re-mints under the new id.
28
25
  */
29
26
  import fs from "node:fs";
27
+ import { renameUsageEventRefs } from "../indexer/usage/usage-events.js";
30
28
  import { readLockfile, renameLockEntry } from "../integrations/lockfile.js";
29
+ import { renameEventRefs } from "../storage/repositories/events-repository.js";
30
+ import { renameImproveLedgerRefs } from "../storage/repositories/improve-ledger-repository.js";
31
31
  import { closeDatabase, openIndexDatabase, openReadonlyExistingDatabase, } from "../storage/repositories/index-connection.js";
32
32
  import { getFilePathsByBundle, renameEntriesBundleId } from "../storage/repositories/index-entries-repository.js";
33
+ import { renameAssetOutcomeRefs } from "../storage/repositories/outcome-repository.js";
33
34
  import { countProposalsForBundleRename, renameProposalsBundleRef } from "../storage/repositories/proposals-repository.js";
35
+ import { renameAssetSalienceRefs } from "../storage/repositories/salience-repository.js";
34
36
  import { countTaskHistoryTargetRefs, renameTaskHistoryTargetRefs, } from "../storage/repositories/task-history-repository.js";
35
37
  import { countWorkflowRunRefsForRename, renameWorkflowRunRefs } from "../storage/repositories/workflow-runs-repository.js";
36
38
  import { selectBackend } from "../tasks/backends/index.js";
@@ -262,12 +264,20 @@ export async function renameBundle(oldId, newId, options = {}, deps) {
262
264
  }
263
265
  }
264
266
  // State: proposals.ref, a pending proposal's proposedTarget.source,
265
- // workflow task_history.target_ref, and workflow_runs.workflow_ref.
267
+ // workflow task_history.target_ref, workflow_runs.workflow_ref, and the
268
+ // learned history keyed by item ref: usage_events.entry_ref (search ranking
269
+ // counts feedback by it), events.ref and improve_ledger.ref (improve reads
270
+ // feedback and its own attempts by them), and asset_salience/asset_outcome.asset_ref.
266
271
  if (fs.existsSync(getStateDbPath())) {
267
272
  withStateDb((db) => {
268
273
  renameProposalsBundleRef(db, oldId, newId);
269
274
  renameTaskHistoryTargetRefs(db, oldId, newId);
270
275
  renameWorkflowRunRefs(db, oldId, newId);
276
+ renameUsageEventRefs(db, oldId, newId);
277
+ renameEventRefs(db, oldId, newId);
278
+ renameImproveLedgerRefs(db, oldId, newId);
279
+ renameAssetSalienceRefs(db, oldId, newId);
280
+ renameAssetOutcomeRefs(db, oldId, newId);
271
281
  });
272
282
  }
273
283
  // Scheduler: remove native rows still naming `<old>//` (see
@@ -85,14 +85,30 @@ export function improveStrategyDefault(out, defaults, improve) {
85
85
  out.improve = { ...(isRecord(improve) ? improve : {}) };
86
86
  out.improve.strategy ??= defaults.improveStrategy;
87
87
  }
88
- /** `workflow.judgeEngine`: a gate's judge is the default connection or harness now. */
89
- export function dropJudgeEngine(out, conversion) {
88
+ /**
89
+ * `workflow.judgeEngine`, else `defaults.engine`, is where 0.9 ran a gate's judge. It is model work now and runs where
90
+ * model work does, `judge`: the default connection, else the default harness. `workflow.judgeEngine` is dropped, and
91
+ * a note says whether the judge moves.
92
+ */
93
+ export function dropJudgeEngine(out, conversion, target, defaultEngine, judge) {
90
94
  const workflow = out.workflow;
91
- if (!isRecord(workflow) || !("judgeEngine" in workflow))
92
- return;
93
- delete workflow.judgeEngine;
94
- conversion.changed = true;
95
- conversion.notes.push("workflow.judgeEngine: a gate's judge is the default connection or harness now; it is dropped.");
95
+ const named = isRecord(workflow) && "judgeEngine" in workflow;
96
+ const engine = named ? str(workflow.judgeEngine) : defaultEngine;
97
+ if (named) {
98
+ delete workflow.judgeEngine;
99
+ conversion.changed = true;
100
+ }
101
+ const became = engine === undefined ? undefined : target(engine);
102
+ const stays = became !== undefined && judge !== undefined && isDeepStrictEqual(became, judge);
103
+ const kind = judge === undefined ? undefined : "connection" in judge ? "connection" : "harness";
104
+ if (named) {
105
+ conversion.notes.push(stays
106
+ ? `workflow.judgeEngine: "${engine}" is the default ${kind}, where a gate's judge runs now; the setting is dropped.`
107
+ : `workflow.judgeEngine${engine === undefined ? "" : ` "${engine}"`}: dropped. A gate's judge runs on the default connection, else the default harness, so a judge that ran on another engine has moved; to keep it, make that engine the default.`);
108
+ }
109
+ else if (became !== undefined && judge !== undefined && !stays) {
110
+ conversion.notes.push(`workflow: with no workflow.judgeEngine, a gate's judge ran on the default engine "${engine}"; it is model work now and runs on the default ${kind}, "${"connection" in judge ? judge.connection : judge.harness}".`);
111
+ }
96
112
  }
97
113
  /** `index.defaults.*` and `index.<pass>.engine`: `index.*` and `connection`; a harness cannot run an index pass. */
98
114
  export function convertIndexEngines(out, conversion, retarget) {
@@ -55,17 +55,32 @@ function implicitKeyVariables(name, defaults) {
55
55
  return name === str(defaults.llmEngine) ? [own, "AKM_LLM_API_KEY"] : [own];
56
56
  }
57
57
  // ── engines → connections / harnesses ────────────────────────────────────────
58
- /** `engines.<name>` with `kind: "agent"` and `platform: "opencode-sdk"`: akm's own runtime has no entry of its own. */
59
- function runtimeEngine(name, engine, converted, conversion) {
60
- converted.runtimes.set(name, str(engine.llmEngine));
61
- conversion.notes.push(`engines.${name}: opencode-sdk is akm's own runtime now, with no entry of its own; its model, timeout and workspace are dropped.`);
58
+ /**
59
+ * `engines.<name>` with `kind: "agent"` and `platform: "opencode-sdk"` over an `llmEngine`: OpenCode ran over
60
+ * akm's own connection, which is what akm's own runtime is now, so it has no entry of its own.
61
+ */
62
+ function runtimeEngine(name, engine, llmEngine, converted, conversion) {
63
+ converted.runtimes.set(name, llmEngine);
64
+ const dropped = ["model", "timeoutMs", "workspace", "bin", "agent"].filter((key) => engine[key] !== undefined);
65
+ const gone = dropped.length === 0 ? "" : `; its ${dropped.join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped`;
66
+ conversion.notes.push(`engines.${name}: opencode-sdk over llmEngine "${llmEngine}" is akm's own runtime over connection "${llmEngine}" now, with no entry of its own${gone}.`);
62
67
  }
63
- /** `engines.<name>` with `kind: "agent"`, any other platform: `harnesses.<name>`. */
64
- function agentEngineToHarness(name, engine, converted) {
65
- const harness = { platform: engine.platform };
68
+ /**
69
+ * `engines.<name>` with `kind: "agent"`: `harnesses.<name>`. An `opencode-sdk` engine with no `llmEngine` ran the
70
+ * user's own OpenCode (provider, model and login from its configuration), which the `opencode` harness runs now.
71
+ */
72
+ function agentEngineToHarness(name, engine, converted, conversion) {
73
+ const sdk = engine.platform === "opencode-sdk";
74
+ const harness = { platform: sdk ? "opencode" : engine.platform };
75
+ if (sdk)
76
+ converted.sdkHarnesses.add(name);
77
+ // 0.9 started `opencode serve` without an engine's `args`; the harness would pass them in place of `run`.
66
78
  for (const key of ["bin", "args", "workspace", "agent", "timeoutMs"])
67
- if (engine[key] !== undefined)
79
+ if (engine[key] !== undefined && !(sdk && key === "args"))
68
80
  harness[key] = engine[key];
81
+ if (sdk && engine.args !== undefined) {
82
+ conversion.notes.push(`engines.${name}: args is dropped. 0.9 did not pass it to OpenCode, and the opencode harness would pass it in place of \`run\`.`);
83
+ }
69
84
  const model = str(engine.model);
70
85
  if (model !== undefined)
71
86
  converted.ownModelOf.push({ kind: "harnesses", engine: name, model });
@@ -123,6 +138,7 @@ function convertEngines(engines, defaults, env, conversion) {
123
138
  connections: {},
124
139
  harnesses: {},
125
140
  runtimes: new Map(),
141
+ sdkHarnesses: new Set(),
126
142
  keyHolders: [],
127
143
  ownModelOf: [],
128
144
  };
@@ -132,12 +148,13 @@ function convertEngines(engines, defaults, env, conversion) {
132
148
  const unknown = Object.keys(engine).filter((k) => !(engine.kind === "agent" ? AGENT_ENGINE_KEYS : LLM_ENGINE_KEYS).has(k));
133
149
  if (unknown.length > 0)
134
150
  conversion.notes.push(`engines.${name}: dropped ${unknown.join(", ")}, which akm does not read.`);
151
+ const llmEngine = str(engine.llmEngine);
135
152
  if (engine.kind !== "agent")
136
153
  llmEngineToConnection(name, engine, defaults, env, converted, conversion);
137
- else if (engine.platform === "opencode-sdk")
138
- runtimeEngine(name, engine, converted, conversion);
154
+ else if (engine.platform === "opencode-sdk" && llmEngine !== undefined)
155
+ runtimeEngine(name, engine, llmEngine, converted, conversion);
139
156
  else
140
- agentEngineToHarness(name, engine, converted);
157
+ agentEngineToHarness(name, engine, converted, conversion);
141
158
  }
142
159
  return converted;
143
160
  }
@@ -161,12 +178,37 @@ function renameMap(names, taken, from) {
161
178
  }
162
179
  return (name) => moved.get(name) ?? name;
163
180
  }
181
+ /** The entry the loader reads as the default: the one called `default`, else the only one. */
182
+ const defaultName = (entries) => "default" in entries ? "default" : Object.keys(entries).length === 1 ? Object.keys(entries)[0] : undefined;
183
+ /**
184
+ * The engine 0.9 ran agent work on when `defaults.engine` named none and OpenCode was installed: the one called
185
+ * `opencode-sdk`, else a bare OpenCode of the user's own.
186
+ */
187
+ const IMPLICIT_AGENT_ENGINE = "opencode-sdk";
188
+ /**
189
+ * Where agent work ran when `defaults.engine` named no harness, if that is not where it runs now. A runtime was akm's
190
+ * own; with no `defaults.engine` it was the user's OpenCode, whenever it was installed.
191
+ */
192
+ function agentWorkMoved(defaultEngine, runtimes, harness) {
193
+ if (defaultEngine !== undefined) {
194
+ return runtimes.has(defaultEngine) && harness !== undefined
195
+ ? `Agent work ran on akm's own runtime (engines.${defaultEngine}); with the harness "${harness}" configured it now runs there. Remove that harness to keep agent work on akm's runtime.`
196
+ : undefined;
197
+ }
198
+ const ran = "Without defaults.engine, agent work ran on your own OpenCode (the implicit fallback), when it was installed";
199
+ return harness !== undefined
200
+ ? `${ran}; with the harness "${harness}" configured it now runs there.`
201
+ : `${ran}; it now runs on akm's own runtime. To keep it on your OpenCode, enable the harness: \`akm setup --harness opencode\`.`;
202
+ }
164
203
  /** `defaults.llmEngine`, else `defaults.engine`: that connection is `default`; `defaults.engine` naming one of several harnesses: that harness is. */
165
204
  function nameDefaults(defaults, converted, conversion, written) {
166
- const { connections, harnesses, runtimes, keyHolders } = converted;
167
- const defaultEngine = str(defaults.engine);
205
+ const { connections, harnesses, runtimes, sdkHarnesses, keyHolders } = converted;
206
+ const implicit = sdkHarnesses.has(IMPLICIT_AGENT_ENGINE) || runtimes.has(IMPLICIT_AGENT_ENGINE);
207
+ const defaultEngine = str(defaults.engine) ?? (implicit ? IMPLICIT_AGENT_ENGINE : undefined);
208
+ const modelWorkEngine = str(defaults.llmEngine);
168
209
  const runtimeLlm = defaultEngine === undefined ? undefined : runtimes.get(defaultEngine);
169
- const defaultConnection = [str(defaults.llmEngine), defaultEngine, runtimeLlm].find((n) => n !== undefined && n in connections);
210
+ const modelWorkRuntimeLlm = modelWorkEngine === undefined ? undefined : runtimes.get(modelWorkEngine);
211
+ const defaultConnection = [modelWorkEngine, modelWorkRuntimeLlm, defaultEngine, runtimeLlm].find((n) => n !== undefined && n in connections);
170
212
  const defaultHarness = defaultEngine !== undefined && defaultEngine in harnesses && Object.keys(harnesses).length > 1
171
213
  ? defaultEngine
172
214
  : undefined;
@@ -177,13 +219,18 @@ function nameDefaults(defaults, converted, conversion, written) {
177
219
  connections: renamed(connections, connectionName),
178
220
  harnesses: renamed(harnesses, harnessName),
179
221
  };
180
- const defaultHarnessName = "default" in named.harnesses
181
- ? "default"
182
- : Object.keys(named.harnesses).length === 1
183
- ? Object.keys(named.harnesses)[0]
184
- : undefined;
185
- if (defaultHarnessName !== undefined && (defaultEngine === undefined || runtimes.has(defaultEngine))) {
186
- conversion.notes.push(`Agent work ran on akm's own runtime${defaultEngine === undefined ? "" : ` (engines.${defaultEngine})`}; with the harness "${defaultHarnessName}" configured it now runs there. Remove that harness to keep agent work on akm's runtime.`);
222
+ const defaultHarnessName = defaultName(named.harnesses);
223
+ const hasEngines = Object.keys(connections).length + Object.keys(harnesses).length + runtimes.size > 0;
224
+ const moved = hasEngines ? agentWorkMoved(defaultEngine, runtimes, defaultHarnessName) : undefined;
225
+ if (moved !== undefined)
226
+ conversion.notes.push(moved);
227
+ // `defaults.llmEngine` could name an agent engine, which then ran akm's model work; model work runs on the default
228
+ // connection, else the default harness, whichever engine that was.
229
+ if (modelWorkEngine !== undefined && modelWorkEngine in harnesses) {
230
+ const now = harnessName(modelWorkEngine);
231
+ if (defaultName({ ...named.connections, ...written.connections }) !== undefined || defaultHarnessName !== now) {
232
+ conversion.notes.push(`defaults.llmEngine "${modelWorkEngine}" named an agent engine, which ran akm's model work. Model work now runs on the default connection, else the default harness; to keep it on the harness "${now}", set improve.harness to it.`);
233
+ }
187
234
  }
188
235
  for (const [kind, rename] of [
189
236
  ["connections", connectionName],
@@ -195,10 +242,9 @@ function nameDefaults(defaults, converted, conversion, written) {
195
242
  conversion.notes.push(`engines.default is now ${kind}.${aside}, and "default" names another engine: an engine: default key is rewritten by \`akm migrate apply\` where it can be, but a script or scheduled command that passes --engine default now reaches the new "default"; change it to the name ${aside}.`);
196
243
  }
197
244
  const target = (engine) => {
198
- if (runtimes.has(engine)) {
199
- const runtime = runtimes.get(engine);
200
- return runtime !== undefined && runtime in connections ? { connection: connectionName(runtime) } : undefined;
201
- }
245
+ const runtime = runtimes.get(engine);
246
+ if (runtime !== undefined)
247
+ return runtime in connections ? { connection: connectionName(runtime) } : undefined;
202
248
  if (engine in harnesses)
203
249
  return { harness: harnessName(engine) };
204
250
  if (engine in connections)
@@ -210,7 +256,7 @@ function nameDefaults(defaults, converted, conversion, written) {
210
256
  connection: connectionName(engine),
211
257
  field,
212
258
  }));
213
- return { defaultConnection, defaultHarness, connectionName, harnessName, ...named, keys, target };
259
+ return { defaultEngine, defaultConnection, defaultHarness, connectionName, harnessName, ...named, keys, target };
214
260
  }
215
261
  /** What the file writes under `connections` / `harnesses` itself, read with the converted entries: the written entry wins field by field. */
216
262
  function mergeWritten(out, naming) {
@@ -230,6 +276,24 @@ function ownModelsOf(converted, naming) {
230
276
  }
231
277
  return ownModels;
232
278
  }
279
+ /**
280
+ * Where a gate's judge and akm's other model work run now when nothing names a place: the default connection, else
281
+ * the default harness. With no `defaults.llmEngine`, 0.9 ran none of that work unless a setting named an engine, so
282
+ * a default harness that takes it now gets a note saying so and how to turn it off.
283
+ */
284
+ function modelWorkTarget(defaults, naming, merged, inherited, conversion) {
285
+ const connection = defaultName({ ...inherited?.connections, ...merged.connections });
286
+ if (connection !== undefined)
287
+ return { connection };
288
+ const harness = defaultName({ ...inherited?.harnesses, ...merged.harnesses });
289
+ const entry = harness === undefined ? undefined : merged.harnesses[harness];
290
+ if (harness === undefined || !isRecord(entry))
291
+ return undefined;
292
+ if (str(defaults.llmEngine) === undefined && harness in naming.harnesses && entry.modelWork === undefined) {
293
+ conversion.notes.push(`harnesses.${harness}: akm's own model work (improve, index passes, extraction, judges) now runs on this harness, the default for it with no connection. Without defaults.llmEngine, 0.9 ran none of it unless a setting named an engine (a workflow gate's judge ran on defaults.engine). To keep the 0.9 behaviour, set \`modelWork: false\` on this harness: a call that names it still runs there, but a workflow gate's judge then needs a connection (\`akm setup --endpoint URL\`).`);
294
+ }
295
+ return entry.modelWork === false ? undefined : { harness };
296
+ }
233
297
  // ── the whole conversion ─────────────────────────────────────────────────────
234
298
  export function convertEngineConfig(raw, options = {}) {
235
299
  const out = structuredClone(raw);
@@ -254,8 +318,9 @@ export function convertEngineConfig(raw, options = {}) {
254
318
  inherited: { connections: options.inherited?.connections ?? {}, harnesses: options.inherited?.harnesses ?? {} },
255
319
  renamed: {
256
320
  runtime: [...converted.runtimes.values()]
257
- .filter((name) => name !== undefined && name in converted.connections)
321
+ .filter((name) => name in converted.connections)
258
322
  .map((name) => naming.connectionName(name)),
323
+ sdkHarness: [...converted.sdkHarnesses].map((name) => naming.harnessName(name)),
259
324
  connection: (engine) => (engine in converted.connections ? naming.connectionName(engine) : undefined),
260
325
  },
261
326
  });
@@ -265,7 +330,8 @@ export function convertEngineConfig(raw, options = {}) {
265
330
  if (isRecord(improve))
266
331
  moveStrategyExecution(improve, conversion, retarget);
267
332
  improveStrategyDefault(out, defaults, improve);
268
- dropJudgeEngine(out, conversion);
333
+ const judge = modelWorkTarget(defaults, naming, merged, options.inherited, conversion);
334
+ dropJudgeEngine(out, conversion, naming.target, naming.defaultEngine, judge);
269
335
  convertIndexEngines(out, conversion, retarget);
270
336
  // Assemble, keeping the file's key order.
271
337
  const config = {};
@@ -44,6 +44,7 @@ function overlayRows(modelsOverlay) {
44
44
  /**
45
45
  * `models.json`: an alias is set by, nearest first, the config's own `models`, models.json's column for that
46
46
  * connection or harness, models.json's `*` column (and the flat shorthand, which is that), the engine's own model.
47
+ * The exception is a harness engine's own model: it ran when a request named none, so it stands over every column.
47
48
  */
48
49
  export function foldModelsOverlay(modelsOverlay, fold) {
49
50
  const { conversion, merged, written, inherited, ownModels, renamed } = fold;
@@ -57,6 +58,10 @@ export function foldModelsOverlay(modelsOverlay, fold) {
57
58
  /** Whether the config itself, the file or its base, already names a model for the alias. */
58
59
  const configNames = (own, base, name, alias) => hasModel(own[name], alias) || hasModel(base[name], alias);
59
60
  const platformOf = (name) => String((mergedHarnesses[name] ?? inheritedHarnesses[name])?.platform ?? name);
61
+ /** The column of models.json that named a harness: an `opencode-sdk` engine's was `opencode-sdk`, not its platform's. */
62
+ const columnOf = (name) => (renamed.sdkHarness.includes(name) ? "opencode-sdk" : platformOf(name));
63
+ /** Harnesses whose engine had a `model` of its own, which names every alias and so stands over the column (noted once each). */
64
+ const notedOwn = new Set();
60
65
  let folded = false;
61
66
  for (const { alias, column, profile } of overlayRows(modelsOverlay)) {
62
67
  const p = typeof profile === "string" ? { model: profile } : isRecord(profile) ? profile : {};
@@ -76,7 +81,7 @@ export function foldModelsOverlay(modelsOverlay, fold) {
76
81
  : column in writtenConnections || column in inheritedConnections
77
82
  ? [column]
78
83
  : [];
79
- const harnessNamed = [...new Set([...Object.keys(mergedHarnesses), ...Object.keys(inheritedHarnesses)])].filter((name) => platformOf(name) === column);
84
+ const harnessNamed = [...new Set([...Object.keys(mergedHarnesses), ...Object.keys(inheritedHarnesses)])].filter((name) => columnOf(name) === column);
80
85
  if (candidates.length === 0 && harnessNamed.length === 0) {
81
86
  notes.push(`${where}: names no configured connection or harness, so it is not carried over.`);
82
87
  continue;
@@ -84,7 +89,14 @@ export function foldModelsOverlay(modelsOverlay, fold) {
84
89
  // The config's own choice stands, and `*` leaves what an explicit column already set.
85
90
  const connectionNames = candidates.filter((name) => !configNames(writtenConnections, inheritedConnections, name, alias) &&
86
91
  !(column === "*" && hasModel(mergedConnections[name], alias)));
87
- const harnessNames = harnessNamed.filter((name) => !configNames(writtenHarnesses, inheritedHarnesses, name, alias));
92
+ const unnamed = harnessNamed.filter((name) => !configNames(writtenHarnesses, inheritedHarnesses, name, alias));
93
+ // The model a harness engine named is what ran when a request named none, and a harness runs its first model then,
94
+ // so it stands over the column, which would put the column's models first.
95
+ const harnessNames = unnamed.filter((name) => !ownModels.harnesses.has(name));
96
+ for (const name of unnamed.filter((n) => ownModels.harnesses.has(n) && !notedOwn.has(`${column}:${n}`))) {
97
+ notedOwn.add(`${column}:${name}`);
98
+ notes.push(`models.json (${column === "*" ? "all" : column}): harness "${name}" has a model of its own, which names every alias, so the column is not carried over.`);
99
+ }
88
100
  if (connectionNames.length === 0 && harnessNames.length === 0) {
89
101
  // Only the config's own choice is worth a note; an explicit column that came first is the overlay's own.
90
102
  const keptByConfig = candidates.some((name) => configNames(writtenConnections, inheritedConnections, name, alias)) ||
@@ -2,7 +2,7 @@
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
4
  /**
5
- * §11.5 bundle-rename startup guard.
5
+ * Bundle-rename startup guard.
6
6
  *
7
7
  * Renaming a workspace bundle id is a mass identity migration (every ref in the
8
8
  * bundle changes without any file moving) and MUST go through the
@@ -10,13 +10,13 @@
10
10
  * every ref-keyed state table atomically. If a user instead hand-edits the
11
11
  * `bundles` KEY in config.json, the durable index/state rows still carry the OLD
12
12
  * bundle prefix while the config names the NEW id. Silently re-minting fresh
13
- * state under the new id would strand the old rows; §11.5 requires we detect this
14
- * and refuse-or-warn.
13
+ * state under the new id would strand the old rows, so this guard detects it
14
+ * and warns.
15
15
  *
16
16
  * This guard implements the cheap heuristic form (WARN, never silently re-mint):
17
17
  * it compares the configured bundle ids against the DISTINCT `bundle_id`
18
- * (item_ref bundle prefix) values already persisted in the index — and, by
19
- * cutover construction, the same prefixes key the state.db usage/salience rows.
18
+ * (item_ref bundle prefix) values already persisted in the index — the same
19
+ * prefixes key the state.db usage/salience rows.
20
20
  * The hand-rename signature is: a configured bundle id with NO indexed rows
21
21
  * co-existing with indexed rows under a bundle id that is no longer configured.
22
22
  *
@@ -70,7 +70,7 @@ function indexedBundlePaths(dbPath) {
70
70
  }
71
71
  /**
72
72
  * Warn (once per process) when the config's bundle ids and the index's bundle
73
- * prefixes show the hand-rename signature (§11.5). No-op for an old-shape config
73
+ * prefixes show the hand-rename signature. No-op for an old-shape config
74
74
  * (no `bundles`), an absent/empty index, or when the guard already settled.
75
75
  */
76
76
  export function warnOnBundleRenameDrift(config = loadConfig()) {
@@ -31,7 +31,7 @@ import { warn, warnVerbose } from "../core/warn.js";
31
31
  import { closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
32
32
  import { deleteEntriesByIds, getEntryCount, upsertEntry } from "../storage/repositories/index-entries-repository.js";
33
33
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
34
- import { generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
34
+ import { embeddingInputCap, generateEmbeddingsForDb, publishTargetedEmbeddingMeta } from "./materialize-embeddings.js";
35
35
  import { withFileSize } from "./passes/metadata.js";
36
36
  import { drainDirDocuments } from "./scan/drain-dir.js";
37
37
  import { buildFileContext } from "./walk/file-context.js";
@@ -153,6 +153,7 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
153
153
  db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
154
154
  if (getEntryCount(db) === 0)
155
155
  return true;
156
+ const config = loadConfig();
156
157
  const targetEntryIds = new Set();
157
158
  let mutated = false;
158
159
  db.transaction(() => {
@@ -194,12 +195,11 @@ export async function indexWrittenAssets(stashDir, filePaths, options = {}) {
194
195
  .prepare("SELECT id FROM entries WHERE file_path = ? AND item_ref <> ?")
195
196
  .all(file, provenance.itemRef);
196
197
  deleteEntriesByIds(db, supersededIds.map((row) => row.id));
197
- targetEntryIds.add(upsertEntry(db, file, entryWithSize, provenance, contentHash));
198
+ targetEntryIds.add(upsertEntry(db, file, entryWithSize, provenance, contentHash, embeddingInputCap(config)));
198
199
  mutated = true;
199
200
  }
200
201
  })();
201
202
  if (mutated) {
202
- const config = loadConfig();
203
203
  await generateEmbeddingsForDb(db, config, () => { }, undefined, [...targetEntryIds]);
204
204
  publishTargetedEmbeddingMeta(db, config);
205
205
  }
@@ -28,9 +28,9 @@ import { assertIndexedWorkflowSourceIdentity, WorkflowSourceIdentityError } from
28
28
  import { reclassifyIndexDbContention } from "./index-db-contention.js";
29
29
  import { deriveEntryProvenance, deriveInstallations } from "./installations.js";
30
30
  import { indexedPathMatchesOwner, resolveConceptOwner } from "./lookup/adapter-concept-owner.js";
31
- import { generateEmbeddingsForDb } from "./materialize-embeddings.js";
31
+ import { embeddingInputCap, generateEmbeddingsForDb } from "./materialize-embeddings.js";
32
32
  import { canUseIncrementalSkip, computeDirFingerprint, getCachedDirState, getDirIndexState, inferZeroRowReason, } from "./passes/dir-staleness.js";
33
- import { isWorkflowSkipWarning, withFileSize } from "./passes/metadata.js";
33
+ import { CONTENT_PROJECTION_VERSION, isWorkflowSkipWarning, withFileSize, } from "./passes/metadata.js";
34
34
  import { drainDirDocuments } from "./scan/drain-dir.js";
35
35
  import { purgeOldUsageEvents, USAGE_EVENT_RETENTION_DAYS } from "./usage/usage-events.js";
36
36
  import { SEMANTIC_SEARCH_META_KEY, } from "./walk/index-context.js";
@@ -446,7 +446,7 @@ async function akmIndexReal(options) {
446
446
  // …then write it in one transaction: `item_ref = <bundle>//<conceptId>`
447
447
  // plus canonical component/adapter provenance per source root.
448
448
  const bundleByRoot = buildIndexedSourceOwners(sources);
449
- persistDirRecords(db, dirRecords, warnings, bundleByRoot);
449
+ persistDirRecords(db, dirRecords, warnings, bundleByRoot, embeddingInputCap(config));
450
450
  onProgress({
451
451
  phase: "scan",
452
452
  message: `Scanned ${scannedDirs} ${scannedDirs === 1 ? "directory" : "directories"} and skipped ${skippedDirs}.`,
@@ -819,7 +819,9 @@ async function scanSourceDirs(db, allSourceEntries, isIncremental, builtAtMs, ha
819
819
  warn(`Skipping component "${component.id}": unknown adapter id "${component.adapter}".`);
820
820
  continue;
821
821
  }
822
- const indexVariant = `${adapter.id}@${adapter.version}`;
822
+ // The content projection's version is part of the variant: a change to what Markdown bodies index re-projects
823
+ // every directory once instead of keeping the old text for unchanged files.
824
+ const indexVariant = `${adapter.id}@${adapter.version}+content${CONTENT_PROJECTION_VERSION}`;
823
825
  for (const removal of removals) {
824
826
  dirRecords.push(removal);
825
827
  scannedDirs++;
@@ -938,7 +940,7 @@ function requiresWorkflowSourcePreflight(ctxs) {
938
940
  });
939
941
  }
940
942
  /** Phase 2 (sync): write all pre-generated scan records inside a single transaction. */
941
- function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
943
+ function persistDirRecords(db, dirRecords, warnings, bundleByRoot, maxInputTokens) {
942
944
  const deletedUsageEntryIds = new Set();
943
945
  const findPersisted = db.prepare("SELECT id, content_hash, file_path, adapter_id FROM entries WHERE item_ref = ?");
944
946
  const insertTransaction = db.transaction(() => {
@@ -1038,7 +1040,7 @@ function persistDirRecords(db, dirRecords, warnings, bundleByRoot) {
1038
1040
  if (unchanged) {
1039
1041
  continue;
1040
1042
  }
1041
- upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash);
1043
+ upsertEntry(db, entryPath, withFileSize(entry, entryPath), provenance, contentHash, maxInputTokens);
1042
1044
  }
1043
1045
  }
1044
1046
  // Prune the departed rows: everything under this dir NOT re-upserted above
@@ -1332,8 +1334,10 @@ export function recomputeUtilityScores(db, stateDb, onlyEntryId) {
1332
1334
  // (Chunk-8 WI-8.3); its table is created by state migration 020.
1333
1335
  if (!scoped)
1334
1336
  purgeOldUsageEvents(stateDb, USAGE_EVENT_RETENTION_DAYS);
1335
- // Time-proportional decay: apply one round of EMA per elapsed day so
1336
- // indexing frequency doesn't affect how fast scores decay.
1337
+ // Decay is raised to the days since the last run, floored at one day. Every
1338
+ // run therefore applies at least one full round of EMA decay (so runs less
1339
+ // than a day apart each decay a full round), and a longer gap applies
1340
+ // proportionally more rounds. The first run counts as one day.
1337
1341
  const lastComputedAt = getMeta(db, "last_utility_computed_at");
1338
1342
  let elapsedDays = 1; // default for first run
1339
1343
  if (lastComputedAt) {
@@ -32,6 +32,13 @@ export function deriveSemanticProviderFingerprint(embedding) {
32
32
  }
33
33
  return `local:${embedding?.localModel ?? DEFAULT_LOCAL_MODEL}${template}`;
34
34
  }
35
+ /**
36
+ * The most estimated tokens of an entry's search text that are embedded
37
+ * (`embedding.maxInputTokens`); the rest of the text never reaches the embedder.
38
+ */
39
+ export function embeddingInputCap(config) {
40
+ return effectiveEmbedding(config)?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
41
+ }
35
42
  /**
36
43
  * The heartbeat text emitted every 15s while a provider request is in
37
44
  * flight, and by default (not `--verbose`-only) since silence indistinguishable
@@ -103,7 +110,7 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
103
110
  let targetEntryIds = entryIds;
104
111
  /** Set only when every entry is (re)embedded, so the up-front "Re-embedding N entries" line names why. */
105
112
  let rebuildReason;
106
- const maxInputTokens = embedding?.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS;
113
+ const maxInputTokens = embeddingInputCap(config);
107
114
  if (opts?.forceReembed) {
108
115
  // The explicit operator override: purge and re-embed everything. The model
109
116
  // is recorded in the SAME transaction as the purge, before any embedding
@@ -171,9 +178,12 @@ export async function generateEmbeddingsForDb(db, config, onProgress, signal, en
171
178
  const message = `[embed] Re-embedding ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"} because ${rebuildReason}`;
172
179
  onProgress({ phase: "embeddings", message });
173
180
  }
181
+ // Names the size of the work before the first request, so a first index after an upgrade that
182
+ // re-embeds part of the corpus is not mistaken for a hang.
183
+ const unchangedCount = Math.max(0, getEmbeddableEntryCount(db) - pendingEntries.length);
174
184
  onProgress({
175
185
  phase: "embeddings",
176
- message: `Generating embeddings for ${pendingEntries.length} entr${pendingEntries.length === 1 ? "y" : "ies"}.`,
186
+ message: `Embedding ${pendingEntries.length.toLocaleString()} changed entr${pendingEntries.length === 1 ? "y" : "ies"} (${unchangedCount.toLocaleString()} unchanged)`,
177
187
  });
178
188
  // Per-document sizes; the provider's actual request grouping is reported
179
189
  // per batch below (and each skipped document is listed at the end).