akm docs

Migration notes for akm v0.9.15

RUN_LEASE_HELD (a workflow run-lease refusal) and the new STATE_DB_CONTENDED (state.db write contention) now exit 75 (EX_TEMPFAIL) instead of exit 2 and exit 70 respectively. Both are unrelated to a bad command line — they mean "another akm process is using this database or lease right now, retry shortly." If a script or scheduler wrapper special-cases exit 2 to detect a held lease, switch it to exit 75, or read the JSON envelope's code field instead.

A concurrent akm index now also exits 75 instead of exit 78 or exit 70. A 2026-09-10 field report found a second akm index (no --skip-if-locked) colliding on the short internal barrier that registers the opt-in rebuild lock could fail with {"code":"INVALID_CONFIG_FILE"} at exit 78 — a config-error exit that told a supervisor to stop retrying ordinary contention between two legitimate runs. That barrier now retries briefly before giving up, and a busy barrier is reclassified as TransientError code MAINTENANCE_BARRIER_BUSY (exit 75) instead. Contention on index.db itself was already reclassified from a raw database is locked error (exit 70) to TransientError code INDEX_DB_CONTENDED (exit 75). Either way, the fix for a scheduled or opportunistic index run is the same: akm index --skip-if-locked steps aside (exit 0) instead of contending at all.

Two more contention paths collided on the same "config error instead of transient" anti-pattern (#948 field follow-up, 2026-09-10). akm improve's whole-run lock (only one improve runs at a time) used to fail a losing contender with {"code":"INVALID_CONFIG_FILE"} at exit 78 when no --skip-if-locked was passed — a config-error exit for ordinary contention between two legitimate improve invocations. It is now TransientError code IMPROVE_LOCK_HELD at exit 75, naming the current holder's pid and start time; --skip-if-locked is unchanged (still exit 0). Separately, the index rebuild's embedding-verification step ([index:verify] Semantic search verification failed: ...) could still surface a raw, unclassified database is locked message even though the acquisition path was already fixed — that message is now built through the same INDEX_DB_CONTENDED reclassification as the rest of the index path. This step does not throw, so it does not change akm index's own exit code; only the message text changed.

The six shipped scheduled improve task templates now run with --require-engines, which aborts (exit 78) before any index work when a process's engine or credential cannot be resolved in the task's own environment, instead of silently skipping that process. This only affects new installs and new akm setup task seeding — an existing install's already-materialized task files are not rewritten. To get the same protection on an existing scheduled task, add --require-engines to its run: command yourself, then run akm task sync.

--require-engines now also runs a bounded reachability probe against each distinct engine endpoint (the same probe akm health already uses), not only a config/credential check. A field re-test found the flag let a run through to a fully dead endpoint, which then sat silent for minutes making no progress and no exit — the flag's exit-78 abort now catches that case up front, naming the unreachable engine and endpoint, before any index work. If a scheduled --require-engines run starts failing at exit 78 after upgrading, check that the engine's endpoint actually answers — this is the flag doing its documented job on a condition it previously missed, not a new failure mode. Separately, --timeout-ms and an engine's own configured timeout already aborted an in-flight request correctly, and SIGTERM/SIGINT already ended a run within its documented grace period — both confirmed, not changed, by this investigation. A live run now also prints one default-level line if it waits more than a few seconds on its first engine response, so a slow-but-alive run and a dead one are never indistinguishable from silence alone.

akm health --no-probe now also skips the cli-version update check (a GitHub release lookup), alongside the engine-reachability checks it already skipped. An air-gapped or offline host's existing --no-probe habit now suppresses both network calls with no config change needed.

Thinking-control wire forms (chat_template_kwargs.enable_thinking and enable_thinking) are now sent for every provider whenever enableThinking resolves to a value, not only when provider: "vllm" is set. If your engine sits behind Bifrost and the gateway does not honor either form, also set reasoningEffort: "none" on that engine. If your engine talks to a strict hosted API that rejects unknown request keys, leave enableThinking unset on that engine so neither wire form is sent.

An embedding.model rename no longer forces a full re-embed by itself: akm index re-embeds a small sample first and keeps the existing vectors when they still verify against the endpoint. akm index --reembed forces a full re-embed when you don't trust that verdict. akm index --skip-if-locked lets a scheduled or opportunistic run step aside instead of contending with a rebuild already in progress; the shipped index-refresh scheduled task already passes it.

embedding.maxTokens's default (the per-request token budget) is now 6000, down from 8000: a field report on an 8192-token llama.cpp embedder showed the 4-chars-per-token estimator undercounts dense technical text by 7-55%, so the old default regularly overshot the endpoint's real context window. If you already set embedding.maxTokens explicitly, this default change does not affect you — your configured value is unchanged. akm index also now recovers automatically within a run: on the first request rejected for exceeding the endpoint's context window, it lowers its effective budget for the rest of that run (reported with one line) rather than continuing to hit the same wall on every following batch.

embedding.concurrency (positive integer, 1-16) overrides the number of embedding requests kept in flight at once, which otherwise defaults to 1 for a loopback endpoint and 2 for a remote one. Set it only for an endpoint that genuinely serves parallel requests — a local model server started with a multi-slot flag (llama.cpp's --parallel N, vLLM) — since the default already protects an ordinary single-slot server from reload-thrash. Embedding throughput is still tuned first by embedding.batchSize (documents per request) and embedding.maxTokens (token budget per request); the concurrency override is a second lever for a server that can actually use it.

embedding.timeoutMs bounds each embedding request (default 120s, up from a prior fixed 30s that cut off a slow local model server mid-response). It is the budget for a request at the full token budget — a smaller request gets a proportionally smaller timeout, so a dead endpoint is still detected in seconds on the common case of small documents. A request TIMEOUT no longer drops its batch immediately: akm now backs off (5s, doubling, capped at 60s) and retries the same request once, since field evidence showed the endpoint keeps computing an abandoned request regardless of the client giving up; a second timeout splits the batch in half and retries each half the same way, down to individual documents, and a single document that still times out is finally skipped. After 3 consecutive failures at single-document size (timeout or network error), or 3 consecutive network errors at any size — never a batch rejected only for exceeding the endpoint's context window — akm index's embedding phase stops dispatching further requests and reports failure instead of grinding through every remaining batch against a dead endpoint — batches already committed are kept, and a rerun picks up where it left off.

akm bundle update now durably commits its embedding pass instead of nesting it inside its own transaction: earlier releases ran the embedding phase inside the same transaction as content/lock/index/state, so every per-batch commit landed as an unobservable SAVEPOINT and a SIGKILL mid-run lost every embedding of the update, not just the one in flight. The embedding phase now runs on its own connection after the update's own commit; a failing pass (provider down) still leaves the update itself successful, with the new index.semanticStatus field on akm bundle update's response the only sign semantic search fell behind.

The published akm/akm-migrate launchers now forward SIGTERM/SIGINT/ SIGHUP to their child and exit alongside it, instead of leaving the child running as an orphan when only the launcher is signaled. No action needed — this is a drop-in fix for anyone running akm under a scheduler, supervisor, or hook that can time out or kill the launcher process directly.

embedding.maxInputTokens (default 512) now caps how much of a single document's text is sent to the embedding provider, truncating to the head instead of ever failing a whole batch over one oversized document.

Which token knob fixed the original 8k-context overflow. A 0.9.15-beta field report described documents estimated under the request budget that still tokenized to 8.5k-12.4k real tokens against an 8192-token endpoint, because the 4-chars-per-token estimator undercounts dense technical text. maxInputTokens, maxTokens, and contextLength are easy to confuse, and only one of them makes that overflow structurally unreachable:

The field's exact 0.9.15-beta config — contextLength: 8192 and maxTokens: 8000 — produces no 400s on 0.9.15. maxTokens now defaults lower anyway (6000), but that is not why the overflow stopped: every document is truncated to maxInputTokens (512 tokens) before it is counted toward any request, so the 8.5k-12.4k-token documents that used to overflow an 8192-token endpoint can no longer reach the request budget in the first place. Set embedding.maxInputTokens higher only if you need documents longer than ~2000 characters embedded in full — for a corpus with such documents, size embedding.maxTokens to still fit the worst case, or the overflow risk returns.

akm index --full and an index-generation bump no longer re-embed unchanged content: vectors about to be discarded are salvaged and handed back to unchanged entries at the start of the next embedding pass instead of every upgrade re-embedding the whole corpus once. No action needed — this is automatic; akm index --reembed still forces a full re-embed when you don't trust the salvaged vectors.

A field report suspected akm index was sending embedding requests with no Authorization header despite embedding.apiKey being set to a secret:// reference. Auditing every path that builds an embedding request found all of them already resolve secret:// through the same store lookup, now pinned by integration and contract tests — this was not a bug in the code as it stands. akm index now prints one line before its first provider request naming the endpoint, model, and credential SOURCE (never the value), e.g. [embed] endpoint http://.../v1/embeddings, model nomic-embed; credential: secret://lab-api-key (store). If you run an embedding gateway that enforces auth and still see unauthenticated requests after upgrading, compare this line's endpoint and credential source against what the gateway's own request log shows for the same request — a mismatch there (not in this line) is the next place to look.

A config file can now inherit a shared base via extends: <path|bundle//path>, deep-merging under the local file so local keys always win. akm config diff <path|bundle//path> prints every leaf that differs between this instance's effective config and another config file or bundle-relative file. akm config unset now refuses to unset a key whose value comes only from an extends-inherited base, naming the source, since there would be nothing local to remove.

The scheduler runs the binary path akm task sync recorded at sync time, not whichever akm your shell now resolves to. After upgrading akm through a different installer than the one active at your last task sync (e.g. npm-global to a standalone download), run akm task sync again so the schedule points at the new binary; akm health --probe now warns via a new scheduler-binary advisory when the two diverge.