akm-cli 0.9.15-beta.3 → 0.9.15-beta.5
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/CHANGELOG.md +109 -3
- package/dist/commands/health/checks.js +2 -7
- package/dist/commands/improve/improve-cli.js +107 -6
- package/dist/commands/improve/improve.js +47 -3
- package/dist/commands/improve/locks.js +9 -2
- package/dist/commands/improve/loop-stages.js +21 -1
- package/dist/commands/improve/reflect.js +159 -64
- package/dist/commands/models-cli.js +6 -1
- package/dist/commands/read/curate.js +34 -1
- package/dist/core/config/schema/search.js +32 -1
- package/dist/core/errors.js +2 -0
- package/dist/core/maintenance-barrier.js +35 -6
- package/dist/indexer/index-db-contention.js +56 -0
- package/dist/indexer/indexer.js +10 -43
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/integrations/agent/model-map.js +39 -2
- package/dist/llm/client.js +18 -0
- package/dist/llm/feature-gate.js +4 -0
- package/dist/llm/rerank-client.js +113 -0
- package/dist/llm/usage-persist.js +7 -1
- package/dist/scripts/akm-migrate-node.js +59 -7
- package/dist/scripts/akm-migrate.js +59 -7
- package/dist/tasks/run/task-log.js +59 -0
- package/docs/migration/release-notes/0.9.15.md +81 -0
- package/docs/reference/cli.md +34 -2
- package/docs/reference/configuration.md +51 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +56 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [0.9.15-beta.
|
|
7
|
+
## [0.9.15-beta.5] - 2026-09-10
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -159,6 +159,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
159
159
|
(#951).** These are akm's resolved data/config/cache/state directories, so a
|
|
160
160
|
script can read `akm info --format json | jq -r .dataDir` instead of hardcoding
|
|
161
161
|
a path that differs between a host install and a container.
|
|
162
|
+
- **A real `curate_rerank` implementation: `search.curateRerank`, a cross-
|
|
163
|
+
encoder rerank pass over `akm curate`'s already-selected candidates (#951).**
|
|
164
|
+
`curate_rerank` was a dead `llm.features.*` key removed in 0.8.0 with no wire
|
|
165
|
+
call ever implemented under it. `search.curateRerank.{enabled, endpoint,
|
|
166
|
+
model, apiKey, timeoutMs, topN}` configures a standalone `/rerank`-style
|
|
167
|
+
endpoint (`src/llm/rerank-client.ts`, `POST {model, query, documents}` →
|
|
168
|
+
`{results: [{index, relevance_score}]}`); when enabled, curate sends its top
|
|
169
|
+
`topN` (default 8) already-ranked candidates and reorders them by the
|
|
170
|
+
endpoint's scores. Disabled by default, and best-effort like every other
|
|
171
|
+
bounded LLM feature: a misconfigured endpoint, network failure, timeout, or
|
|
172
|
+
malformed response falls back to curate's own ranking unchanged rather than
|
|
173
|
+
failing the command. Deliberately its own config arm rather than a third
|
|
174
|
+
`engines` kind alongside `"llm"`/`"agent"` — that union's kinds are
|
|
175
|
+
load-bearing through execution-lowering, runner dispatch, and the harness
|
|
176
|
+
model map, none of which a reranker touches.
|
|
177
|
+
- **Per-run task log files are purged past the retention window (#951).**
|
|
178
|
+
`tasks/logs/<taskId>/<timestamp>.log` (the per-run human-readable tail;
|
|
179
|
+
`logs.db` is the durable, already-purged record) had file separation but no
|
|
180
|
+
cleanup, so old run files accumulated forever. `akm improve`'s existing
|
|
181
|
+
retention pass now also deletes `.log` files under `getTaskLogDir()` older
|
|
182
|
+
than the same `improve.eventRetentionDays` window (default 90d, `0` disables
|
|
183
|
+
it) it already uses for `task_logs`/events/`improve_runs` — bounded to that
|
|
184
|
+
one directory, one level of `<taskId>` subdirectories, `.log` files only.
|
|
185
|
+
Path-agnostic bundled scripts and the `akm show env/<name>`/`akm task list`
|
|
186
|
+
items from the same review were previously confirmed shipped and are
|
|
187
|
+
unchanged here.
|
|
162
188
|
- **`akm index --reembed` forces a full re-embed (#955).** Bypasses the
|
|
163
189
|
compatibility check above entirely and purges + regenerates every stored
|
|
164
190
|
embedding, for the rare case where the check's verdict should not be trusted. A
|
|
@@ -250,8 +276,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
250
276
|
`embedding.contextLength` is Ollama's `num_ctx` only now — it used to also
|
|
251
277
|
silently set the per-request token budget (`embedding.maxTokens`), so
|
|
252
278
|
setting it for the server's context window changed request batching too.
|
|
253
|
-
The request budget is `embedding.maxTokens` (default
|
|
254
|
-
carries about
|
|
279
|
+
The request budget is `embedding.maxTokens` (default 6000, see #954
|
|
280
|
+
below), so a request carries about 11 documents alongside the new
|
|
281
|
+
per-document cap by default.
|
|
255
282
|
- **`akm index` reports where its embedding credential came from, before the
|
|
256
283
|
first provider request (#953).** A field report suspected a gateway was
|
|
257
284
|
receiving unauthenticated embedding requests despite `embedding.apiKey`
|
|
@@ -279,6 +306,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
279
306
|
it differs from the running CLI, pointing at `akm task sync` as the
|
|
280
307
|
remedy. `unknown` when not probed, no task is installed, or the recorded
|
|
281
308
|
binary cannot be executed.
|
|
309
|
+
- **`akm improve <ref> --show-prompt` prints the composed reflect prompt for
|
|
310
|
+
one asset and exits (#952).** A beta.3 field round confirmed the #952 prompt
|
|
311
|
+
fix by reading source, but no live `akm improve` completed across three
|
|
312
|
+
attempts, leaving no cheap way to see the prompt in practice. `--show-prompt`
|
|
313
|
+
reuses every read-only step `akm improve`'s live reflect step already
|
|
314
|
+
performs (source resolution, runner selection, feedback/schema-hint/
|
|
315
|
+
related-lesson/rejected-proposal gathering) and stops before the dispatch
|
|
316
|
+
lease reflect would otherwise acquire — no lock, index write, or engine call,
|
|
317
|
+
same as `--dry-run`. Requires a fully-qualified asset ref as the scope; JSON/
|
|
318
|
+
yaml output carries the prompt as a `prompt` field, text output prints it
|
|
319
|
+
directly so the #952 framing (feedback shown as an unverified report, and
|
|
320
|
+
the instruction never to emit the truncation marker or out-of-asset content)
|
|
321
|
+
can be checked by eye.
|
|
282
322
|
|
|
283
323
|
### Changed
|
|
284
324
|
|
|
@@ -463,6 +503,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
463
503
|
|
|
464
504
|
### Fixed
|
|
465
505
|
|
|
506
|
+
- **An alias-level `engine` binding in `models.json` is honoured on every
|
|
507
|
+
platform column (#946).** The per-platform nested form
|
|
508
|
+
(`"fast": {"opencode": {"engine": "local-fast"}}`) already worked, but the
|
|
509
|
+
flat shorthand from this issue's own acceptance criteria
|
|
510
|
+
(`"fast": {"engine": "local-fast"}`) did not: `parseModelMapLayer` treated
|
|
511
|
+
every key under an alias as a platform-column name, so `engine` was parsed
|
|
512
|
+
as a fourth platform holding the model string, the three real columns kept
|
|
513
|
+
their hardcoded cloud defaults, and `akm agent --model fast` silently
|
|
514
|
+
dispatched to a cloud model instead of the configured local engine. The
|
|
515
|
+
flat `model`/`inference`/`engine` keys are now a wildcard default merged
|
|
516
|
+
onto every column, a same-alias per-platform entry still overrides it, and
|
|
517
|
+
an alias naming an unknown engine fails with a `ConfigError` rather than
|
|
518
|
+
falling back.
|
|
519
|
+
- **Two `improve` runs colliding on the lock is transient, not a broken
|
|
520
|
+
config (#948).** The lock-held path threw a `ConfigError`, surfacing as
|
|
521
|
+
exit 78 (`INVALID_CONFIG_FILE`) and telling a supervisor to stop retrying
|
|
522
|
+
an ordinary collision. It is now a `TransientError`/`IMPROVE_LOCK_HELD` at
|
|
523
|
+
exit 75, matching the `MAINTENANCE_BARRIER_BUSY`/`INDEX_DB_CONTENDED`
|
|
524
|
+
treatment the index rebuild lock already had.
|
|
525
|
+
- **The embedding path no longer leaks a raw `database is locked` (#956).**
|
|
526
|
+
`reclassifyIndexDbContention` moved out of `indexer.ts` into a shared
|
|
527
|
+
module and `materialize-embeddings.ts`'s embedding-generation catch now
|
|
528
|
+
routes through it, so contention produces the same "index database is
|
|
529
|
+
busy" wording as every other path instead of the raw driver string.
|
|
530
|
+
Control flow is unchanged: this was always non-fatal.
|
|
531
|
+
|
|
466
532
|
- **Starting a workflow ref that already has an active run in a different scope
|
|
467
533
|
now warns instead of silently duplicating it (#942).** `akm workflow run
|
|
468
534
|
<ref>`'s per-scope concurrency guard is unchanged by design — two unrelated
|
|
@@ -610,6 +676,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
610
676
|
precedent (#948) for state.db; the original driver text survives as
|
|
611
677
|
`cause`. `--skip-if-locked` is unaffected — it already skips gracefully
|
|
612
678
|
before ever attempting the write.
|
|
679
|
+
- **A concurrent plain `akm index` (no `--skip-if-locked`) could still exit
|
|
680
|
+
78 instead of 75, a 2026-09-10 field re-test found (#956).** Two `akm
|
|
681
|
+
index` runs colliding on the short internal barrier that registers the
|
|
682
|
+
opt-in rebuild lock (shared with every other akm lock/lease) threw
|
|
683
|
+
`ConfigError("INVALID_CONFIG_FILE")` — a config-error exit that tells a
|
|
684
|
+
supervisor to stop retrying, when this is ordinary contention between two
|
|
685
|
+
legitimate runs. The barrier is meant to be held only milliseconds, so it
|
|
686
|
+
now retries briefly (a bounded, jittered backoff) before giving up, letting
|
|
687
|
+
an ordinary collision succeed instead of erroring at all; if it is still
|
|
688
|
+
busy after that, it raises `TransientError` with a dedicated
|
|
689
|
+
`MAINTENANCE_BARRIER_BUSY` code (exit 75) instead of the config error. The
|
|
690
|
+
rebuild lock itself is unaffected and still never blocks (#872).
|
|
613
691
|
- **The fingerprint-rename canary embeds the exact text the stored vector was
|
|
614
692
|
generated from (#955).** `sampleEmbeddedEntriesForCanary` handed the canary
|
|
615
693
|
the entry's raw `search_text`, while the main embedding pass caps it to
|
|
@@ -621,6 +699,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
621
699
|
purge and rebuild on a same-model rename. The canary now caps each sampled
|
|
622
700
|
entry's search text the same way, through the same `capEmbeddingText`
|
|
623
701
|
helper, before requesting its vector.
|
|
702
|
+
- **`akm improve --require-engines` now probes reachability instead of only
|
|
703
|
+
checking config/credentials, and a dead engine can no longer hang a run
|
|
704
|
+
past its timeout or a signal (#957).** Field, beta.3: engines pointed at a
|
|
705
|
+
dead endpoint, then `akm improve --require-engines` ran ~4 minutes with
|
|
706
|
+
zero output, ignoring an external `timeout 30` (SIGTERM) and akm's own
|
|
707
|
+
`--timeout-ms` — the documented exit-78 "required engines unavailable"
|
|
708
|
+
path could never be observed, because `--require-engines` only checked
|
|
709
|
+
that an engine was configured and credentialed, never whether it actually
|
|
710
|
+
answered. It now also runs the same bounded reachability probe `akm
|
|
711
|
+
health`'s `default-llm-engine`/`configured-engines` checks already use
|
|
712
|
+
(one `/models` request per distinct endpoint), before any lock, log, or
|
|
713
|
+
index side effect, and aborts at exit 78 naming the unreachable engine and
|
|
714
|
+
endpoint. Separately, a live run now prints one default-level line if it
|
|
715
|
+
has waited more than a few seconds on its first engine response, so a
|
|
716
|
+
scheduled run's log is never silently empty while an engine is slow or
|
|
717
|
+
dead — `--timeout-ms` and an engine's own configured timeout already
|
|
718
|
+
aborted the in-flight request correctly (confirmed by this investigation,
|
|
719
|
+
not changed), and SIGTERM/SIGINT already ended the process within its
|
|
720
|
+
documented grace period.
|
|
721
|
+
- **`akm improve --show-prompt` now includes `avoidPatterns` when a live
|
|
722
|
+
improve loop has set them (#952).** The preview built its own second copy
|
|
723
|
+
of reflect's prompt-source gathering and `ReflectPromptInput` assembly,
|
|
724
|
+
which had already drifted from the real dispatch path: it never read
|
|
725
|
+
`avoidPatterns` (recent-error context from earlier assets in the same
|
|
726
|
+
run), so the preview was not always the prompt a live iteration would
|
|
727
|
+
actually send. Both the preview and the real dispatch path (`akmReflect`,
|
|
728
|
+
`runReflectRefineIterations`) now gather sources and assemble the prompt
|
|
729
|
+
input through the same two shared helpers, so this cannot drift again.
|
|
624
730
|
|
|
625
731
|
## [0.9.14] - 2026-09-04
|
|
626
732
|
|
|
@@ -12,6 +12,7 @@ import { withEngineFallback } from "../../integrations/agent/engine-fallback.js"
|
|
|
12
12
|
import { isLlmCredentialAvailable, resolveEngine, } from "../../integrations/agent/engine-resolution.js";
|
|
13
13
|
import { executionEngineDefinitionsFromConfig } from "../../integrations/agent/execution-definitions.js";
|
|
14
14
|
import { loadModelMap, mergeModelMapLayers, parseModelMapLayer, readInstalledModelMapText, resolveModelMapAlias, userModelMapPath, } from "../../integrations/agent/model-map.js";
|
|
15
|
+
import { probeEndpointOnce } from "../../llm/client.js";
|
|
15
16
|
import { listKeys } from "../env/env.js";
|
|
16
17
|
import { resolveImprovePlan } from "../improve/improve-strategies.js";
|
|
17
18
|
import { ENGINE_LAST_USED_LOOKBACK_DAYS } from "./engine-usage.js";
|
|
@@ -20,13 +21,7 @@ import { ACTIVE_RUN_WARN_MS, TASK_FAIL_RATE_WARN, } from "./types.js";
|
|
|
20
21
|
function probeConnectionReachable(connection, deps, cache) {
|
|
21
22
|
if (!deps.probeReachable)
|
|
22
23
|
return Promise.resolve(undefined);
|
|
23
|
-
|
|
24
|
-
let pending = cache.get(key);
|
|
25
|
-
if (!pending) {
|
|
26
|
-
pending = deps.probeReachable(connection);
|
|
27
|
-
cache.set(key, pending);
|
|
28
|
-
}
|
|
29
|
-
return pending;
|
|
24
|
+
return probeEndpointOnce(connection, cache, deps.probeReachable);
|
|
30
25
|
}
|
|
31
26
|
function reachabilityEvidence(reach) {
|
|
32
27
|
return reach
|
|
@@ -15,12 +15,16 @@ import { redactSensitiveText } from "../../core/redaction.js";
|
|
|
15
15
|
import { clearLogFile, setLogFile, warn } from "../../core/warn.js";
|
|
16
16
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
17
17
|
import { collectEngineCredentialValues } from "../../integrations/agent/engine-resolution.js";
|
|
18
|
-
import {
|
|
18
|
+
import { probeEndpointOnce, probeLlmEndpoint } from "../../llm/client.js";
|
|
19
|
+
import { getOutputMode } from "../../output/context.js";
|
|
20
|
+
import { deliverRendered } from "../../output/html-render.js";
|
|
21
|
+
import { akmImprove, resolveImproveReadSource } from "./improve.js";
|
|
19
22
|
import { runImproveReportQuery } from "./improve-report.js";
|
|
20
23
|
import { buildImproveRunId, recordImproveRunResult, recordTerminatedImproveRun, } from "./improve-result-file.js";
|
|
21
24
|
import { runImproveSession } from "./improve-session.js";
|
|
22
|
-
import { resolveImprovePlan } from "./improve-strategies.js";
|
|
25
|
+
import { resolveImprovePlan, } from "./improve-strategies.js";
|
|
23
26
|
import { formatUsageReportTable } from "./improve-usage-report.js";
|
|
27
|
+
import { renderReflectPromptPreview } from "./reflect.js";
|
|
24
28
|
let akmImproveForRun = akmImprove;
|
|
25
29
|
/** Swap the CLI's improve work implementation in deterministic subprocess tests. */
|
|
26
30
|
export function _setAkmImproveForTests(fake) {
|
|
@@ -93,6 +97,84 @@ function assertRequiredEnginesAvailable(plan) {
|
|
|
93
97
|
const lines = plan.engineUnavailable.map((item) => ` - ${item.process} (${item.configKey}): ${item.reason}`);
|
|
94
98
|
throw new ConfigError(`--require-engines: ${plan.engineUnavailable.length} improve process${plan.engineUnavailable.length === 1 ? "" : "es"} cannot run because ${plan.engineUnavailable.length === 1 ? "its" : "their"} engine is unavailable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Every distinct `kind: "llm"` connection the active strategy's plan would
|
|
102
|
+
* actually dispatch against — the main per-process runners plus triage's own
|
|
103
|
+
* judgment engine, which is resolved separately (#957).
|
|
104
|
+
*/
|
|
105
|
+
function collectRequiredEngineTargets(plan) {
|
|
106
|
+
const targets = [];
|
|
107
|
+
for (const [processName, process] of Object.entries(plan.processes)) {
|
|
108
|
+
if (process.runner) {
|
|
109
|
+
targets.push({ process: processName, engine: process.runner.engine, connection: process.runner.connection });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (plan.triageJudgment?.kind === "llm") {
|
|
113
|
+
targets.push({
|
|
114
|
+
process: "triage.judgment",
|
|
115
|
+
engine: plan.triageJudgment.engine,
|
|
116
|
+
connection: plan.triageJudgment.connection,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return targets;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `--require-engines` field re-test (#957): the static check above only
|
|
123
|
+
* proves an engine is configured and credentialed — it cannot see a dead
|
|
124
|
+
* endpoint. A field run against an unreachable engine sat silent for
|
|
125
|
+
* minutes instead of hitting the documented exit-78 path. Reuse the SAME
|
|
126
|
+
* bounded reachability probe `akm health`'s `default-llm-engine` /
|
|
127
|
+
* `configured-engines` checks already run (`probeLlmEndpoint`, a single
|
|
128
|
+
* `/models` GET bounded by its own default timeout) once per distinct
|
|
129
|
+
* endpoint (via the shared `probeEndpointOnce` memoization health/checks.ts
|
|
130
|
+
* also uses), so a dead engine is caught here instead of during dispatch.
|
|
131
|
+
*/
|
|
132
|
+
async function assertRequiredEnginesReachable(plan, probeReachable = probeLlmEndpoint) {
|
|
133
|
+
const targets = collectRequiredEngineTargets(plan);
|
|
134
|
+
if (targets.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
const probesByEndpoint = new Map();
|
|
137
|
+
const probed = await Promise.all(targets.map(async (target) => ({
|
|
138
|
+
...target,
|
|
139
|
+
reach: await probeEndpointOnce(target.connection, probesByEndpoint, probeReachable),
|
|
140
|
+
})));
|
|
141
|
+
const unreachable = probed.filter((item) => !item.reach.reachable);
|
|
142
|
+
if (unreachable.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const lines = unreachable.map((item) => ` - ${item.process} (engine "${item.engine}", ${item.connection.endpoint}): ${item.reach.error ?? "did not respond"}`);
|
|
145
|
+
throw new ConfigError(`--require-engines: ${unreachable.length} improve process${unreachable.length === 1 ? "" : "es"} cannot run because ${unreachable.length === 1 ? "its" : "their"} engine endpoint is not reachable:\n${lines.join("\n")}`, "LLM_NOT_CONFIGURED");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* `--show-prompt` (#952): render the composed reflect prompt for one asset ref
|
|
149
|
+
* and exit, before any lock, log, index write, or engine dispatch — the field
|
|
150
|
+
* had no cheap way to confirm the #952 prompt fix (unverified-feedback framing,
|
|
151
|
+
* no-truncation-marker instruction) without running a full improve cycle.
|
|
152
|
+
* Reuses `renderReflectPromptPreview` (reflect.ts), which stops before the
|
|
153
|
+
* dispatch lease reflect would otherwise acquire, so this never calls an engine.
|
|
154
|
+
*/
|
|
155
|
+
async function runShowPromptCli(refArg, parsedRef, taskArg, targetArg, resolvedPlan) {
|
|
156
|
+
const readSource = resolveImproveReadSource(resolvedPlan.config, parsedRef, targetArg);
|
|
157
|
+
const preview = await renderReflectPromptPreview({
|
|
158
|
+
ref: refArg,
|
|
159
|
+
...(taskArg ? { task: taskArg } : {}),
|
|
160
|
+
improveProfile: resolvedPlan.strategy.config,
|
|
161
|
+
config: resolvedPlan.config,
|
|
162
|
+
stashDir: readSource.source.path,
|
|
163
|
+
});
|
|
164
|
+
const outputMode = getOutputMode();
|
|
165
|
+
if (outputMode.format === "text") {
|
|
166
|
+
deliverRendered(preview.prompt, outputMode.outputPath);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
output("improve", {
|
|
170
|
+
schemaVersion: 2,
|
|
171
|
+
ok: true,
|
|
172
|
+
ref: preview.ref,
|
|
173
|
+
engine: preview.engine,
|
|
174
|
+
engineKind: preview.engineKind,
|
|
175
|
+
prompt: preview.prompt,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
96
178
|
/**
|
|
97
179
|
* `akm improve report` (#944): a scope value that dispatches to the per-run
|
|
98
180
|
* LLM usage/routing report instead of a real improve run — "report" is not,
|
|
@@ -172,7 +254,12 @@ export const improveCommand = defineCommand({
|
|
|
172
254
|
},
|
|
173
255
|
"require-engines": {
|
|
174
256
|
type: "boolean",
|
|
175
|
-
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment. Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
257
|
+
description: "Abort before any indexing, lock, or log side effect (exit 78) if the active strategy would enable a process whose engine or credential cannot be resolved in this process's environment, OR whose endpoint fails a bounded reachability probe (the same probe akm health runs). Without this flag, improve degrades gracefully instead: it skips the affected processes and reports them in the result's skippedProcesses. Recommended alongside --skip-if-locked for scheduled runs.",
|
|
258
|
+
default: false,
|
|
259
|
+
},
|
|
260
|
+
"show-prompt": {
|
|
261
|
+
type: "boolean",
|
|
262
|
+
description: "Print the composed reflect prompt for one asset ref and exit — no lock, index write, or engine dispatch (#952). Requires a fully-qualified asset ref as the scope positional (e.g. `akm improve lessons/my-lesson --show-prompt`). JSON/yaml format carries the prompt as a `prompt` field; text format prints it directly.",
|
|
176
263
|
default: false,
|
|
177
264
|
},
|
|
178
265
|
run: {
|
|
@@ -216,8 +303,10 @@ export const improveCommand = defineCommand({
|
|
|
216
303
|
const targetArg = getStringArg(args, "bundle");
|
|
217
304
|
const taskArg = getStringArg(args, "task");
|
|
218
305
|
// #947 — `--plan` is a zero-logic discoverability alias for `--dry-run`;
|
|
219
|
-
// it must never fork the computation, only set the same flag.
|
|
220
|
-
|
|
306
|
+
// it must never fork the computation, only set the same flag. #952 —
|
|
307
|
+
// `--show-prompt` implies the same read-only posture (it never reaches
|
|
308
|
+
// akmImprove at all, but keeps writeTarget/resolvedPlan unset the same way).
|
|
309
|
+
const dryRun = args["dry-run"] || args.plan || args["show-prompt"];
|
|
221
310
|
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
222
311
|
const timeoutMs = parsePositiveIntFlag(args["timeout-ms"], "--timeout-ms");
|
|
223
312
|
const requireFeedbackSignal = args["require-feedback-signal"];
|
|
@@ -239,8 +328,20 @@ export const improveCommand = defineCommand({
|
|
|
239
328
|
// is disabled purely by an unreachable credential; a live run keeps
|
|
240
329
|
// throwing (allowAllDisabled unset).
|
|
241
330
|
const resolvedPlan = resolveImprovePlan(strategyArg, effectiveConfig, { allowAllDisabled: Boolean(dryRun) });
|
|
242
|
-
|
|
331
|
+
// #952 — same interception point as the `report` scope above: before any
|
|
332
|
+
// lock, log, or index side effect. Requires a single fully-qualified
|
|
333
|
+
// asset ref (not a type or whole-bundle scope).
|
|
334
|
+
if (args["show-prompt"]) {
|
|
335
|
+
if (!scopeArg || !scopeRef) {
|
|
336
|
+
throw new UsageError("`--show-prompt` requires a fully-qualified asset ref as the scope (e.g. `akm improve lessons/my-lesson --show-prompt`).", "INVALID_FLAG_VALUE");
|
|
337
|
+
}
|
|
338
|
+
await runShowPromptCli(scopeArg, scopeRef, taskArg, targetArg, resolvedPlan);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (args["require-engines"]) {
|
|
243
342
|
assertRequiredEnginesAvailable(resolvedPlan);
|
|
343
|
+
await assertRequiredEnginesReachable(resolvedPlan);
|
|
344
|
+
}
|
|
244
345
|
const selectedStrategyName = resolvedPlan.strategy.name;
|
|
245
346
|
const sensitiveValues = collectEngineCredentialValues(effectiveConfig);
|
|
246
347
|
// Only set the keys the user actually passed (citty leaves the flag
|
|
@@ -65,6 +65,19 @@ export function renderSyncCommitMessage(template, result, nowMs) {
|
|
|
65
65
|
};
|
|
66
66
|
return template.replace(/\{(\w+)\}/g, (match, key) => tokens[key] ?? match);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* How long the improve loop waits for its FIRST engine response (success or
|
|
70
|
+
* error — any terminal record proves the run is not silent) before printing
|
|
71
|
+
* one default-level line. The timer is armed once the triage/index prepass
|
|
72
|
+
* finishes and the loop is about to start dispatching engine requests — not
|
|
73
|
+
* at run start — so it measures engine latency, not prepass time. Field
|
|
74
|
+
* re-test (#957): an engine pointed at a dead endpoint produced zero output
|
|
75
|
+
* for minutes, so a genuine hang looked identical to a normal-but-slow run.
|
|
76
|
+
* A few seconds is short enough that an operator watching a scheduled run's
|
|
77
|
+
* live log sees something promptly, long enough that an ordinary fast
|
|
78
|
+
* response never prints it.
|
|
79
|
+
*/
|
|
80
|
+
export const FIRST_ENGINE_RESPONSE_HEARTBEAT_MS = 5_000;
|
|
68
81
|
export function armBudgetWatchdog(budgetMs, controller, deps) {
|
|
69
82
|
const setTimeoutFn = deps?.setTimeoutFn ?? setTimeout;
|
|
70
83
|
const clearTimeoutFn = deps?.clearTimeoutFn ?? clearTimeout;
|
|
@@ -119,6 +132,12 @@ export async function akmImprove(options = {}) {
|
|
|
119
132
|
options = setup.options;
|
|
120
133
|
const { budgetMs, budgetAbortController, scope, selectedStrategy, syncRepoDir, resolvedStateDbPath, resolvedLockPath, } = setup;
|
|
121
134
|
let clearBudgetTimer = () => { };
|
|
135
|
+
let clearFirstResponseHeartbeat = () => { };
|
|
136
|
+
// #957: set by the usage sink's onRecord callback the moment any engine
|
|
137
|
+
// call terminates (success or error), including one issued by the prepass
|
|
138
|
+
// itself — makes arming the heartbeat below a no-op when the run is
|
|
139
|
+
// already known not to be silent.
|
|
140
|
+
let firstEngineResponseSeen = false;
|
|
122
141
|
let initialGitPaths = new Set();
|
|
123
142
|
const runJournal = createRunWriteJournal();
|
|
124
143
|
const preEnsureCleanupWarnings = [];
|
|
@@ -175,7 +194,10 @@ export async function akmImprove(options = {}) {
|
|
|
175
194
|
return buildLockSkippedResult(selectedStrategy.name, scope, options.runId);
|
|
176
195
|
}
|
|
177
196
|
improveLockOwnership = acquisition.ownership;
|
|
178
|
-
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx)
|
|
197
|
+
disposeLlmUsageSink = installLlmUsagePersistence(() => eventsCtx, () => {
|
|
198
|
+
firstEngineResponseSeen = true;
|
|
199
|
+
clearFirstResponseHeartbeat();
|
|
200
|
+
});
|
|
179
201
|
exitBackstop = releaseRunLock;
|
|
180
202
|
process.on("exit", exitBackstop);
|
|
181
203
|
initialGitPaths =
|
|
@@ -215,6 +237,7 @@ export async function akmImprove(options = {}) {
|
|
|
215
237
|
// If the live prepass fails, emit its summary and clear the owning sink
|
|
216
238
|
// before any run teardown. The disposer is idempotent with the main finalizer.
|
|
217
239
|
disposeLlmUsageSink();
|
|
240
|
+
clearFirstResponseHeartbeat();
|
|
218
241
|
clearBudgetTimer();
|
|
219
242
|
if (exitBackstop) {
|
|
220
243
|
process.removeListener("exit", exitBackstop);
|
|
@@ -233,6 +256,20 @@ export async function akmImprove(options = {}) {
|
|
|
233
256
|
// are all in hand. See buildImproveRunContext for exactly which
|
|
234
257
|
// already-resolved values back each field.
|
|
235
258
|
const ctx = buildImproveRunContext(setup, eventsCtx);
|
|
259
|
+
// #957: arm the heartbeat here, immediately before the improve loop
|
|
260
|
+
// starts dispatching engine requests — not at run start, where its timer
|
|
261
|
+
// would measure the triage/index prepass instead of engine latency. A
|
|
262
|
+
// no-op when the prepass already produced a terminal LLM record (the
|
|
263
|
+
// onRecord callback above already saw it). Cleared the moment any call
|
|
264
|
+
// terminates (success or error) — never rearmed, so this prints at most
|
|
265
|
+
// once per run.
|
|
266
|
+
if (!firstEngineResponseSeen) {
|
|
267
|
+
const firstResponseTimer = setTimeout(() => {
|
|
268
|
+
warn("[improve] Still waiting for the first engine response...");
|
|
269
|
+
}, FIRST_ENGINE_RESPONSE_HEARTBEAT_MS);
|
|
270
|
+
firstResponseTimer.unref?.();
|
|
271
|
+
clearFirstResponseHeartbeat = () => clearTimeout(firstResponseTimer);
|
|
272
|
+
}
|
|
236
273
|
const seq = await runImproveStageSequence({
|
|
237
274
|
run: setup,
|
|
238
275
|
strategyFilteredRefs,
|
|
@@ -292,6 +329,8 @@ export async function akmImprove(options = {}) {
|
|
|
292
329
|
// #576: clear the per-run LLM usage sink BEFORE closing `eventsDb` below, so
|
|
293
330
|
// no late sink invocation can write through a closed handle.
|
|
294
331
|
disposeLlmUsageSink();
|
|
332
|
+
// #957: never leave the first-response heartbeat timer pending past the run.
|
|
333
|
+
clearFirstResponseHeartbeat();
|
|
295
334
|
// O-1 (#364): Clear the budget abort timer so it does not keep the event
|
|
296
335
|
// loop alive after the run completes.
|
|
297
336
|
clearBudgetTimer();
|
|
@@ -345,8 +384,13 @@ function describeRunWrittenPaths(setup, writtenPaths) {
|
|
|
345
384
|
}
|
|
346
385
|
return [...described].sort();
|
|
347
386
|
}
|
|
348
|
-
/**
|
|
349
|
-
|
|
387
|
+
/**
|
|
388
|
+
* Resolve a dry-run inspection source without adapting it into a write target.
|
|
389
|
+
* Exported so `improve-cli.ts`'s `--show-prompt` (#952) can resolve the same
|
|
390
|
+
* read-only bundle a plain `--dry-run` would, without duplicating this
|
|
391
|
+
* selector/target/fallback precedence.
|
|
392
|
+
*/
|
|
393
|
+
export function resolveImproveReadSource(config, scopedRef, explicitTarget, fallbackStashDir) {
|
|
350
394
|
if (scopedRef?.origin && explicitTarget && scopedRef.origin !== explicitTarget) {
|
|
351
395
|
throw new UsageError(`Qualified ref bundle "${scopedRef.origin}" conflicts with --target "${explicitTarget}".`, "INVALID_FLAG_VALUE", `Drop --target or use --target ${scopedRef.origin}.`);
|
|
352
396
|
}
|
|
@@ -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
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { TransientError } from "../../core/errors.js";
|
|
6
6
|
import { appendEvent } from "../../core/events.js";
|
|
7
7
|
import { releaseLock } from "../../core/file-lock.js";
|
|
8
8
|
import { tryWithMaintenanceStartBarrier, withMaintenanceStartBarrier } from "../../core/maintenance-barrier.js";
|
|
@@ -65,7 +65,14 @@ function tryAcquireImproveLockUnlocked(lockPath, skipIfLocked, onRecovered) {
|
|
|
65
65
|
warn(`[improve] another improve run holds the lock (PID ${pid}, started ${startedAt}); skipping (--skip-if-locked)`);
|
|
66
66
|
return { state: "skipped" };
|
|
67
67
|
}
|
|
68
|
-
|
|
68
|
+
// #948 field follow-up: two legitimate `improve` invocations colliding on
|
|
69
|
+
// this lock is ordinary, retryable contention — not a broken config file.
|
|
70
|
+
// A `ConfigError` here surfaced as exit 78 (INVALID_CONFIG_FILE) and told
|
|
71
|
+
// a supervisor to stop retrying a normal lock collision. Reclassified to
|
|
72
|
+
// `TransientError`/`IMPROVE_LOCK_HELD` (exit 75), mirroring the
|
|
73
|
+
// `MAINTENANCE_BARRIER_BUSY`/`INDEX_DB_CONTENDED` treatment #956 already
|
|
74
|
+
// applied to the index rebuild lock and the maintenance-start barrier.
|
|
75
|
+
throw new TransientError(`akm improve is already running (PID ${pid}, started ${startedAt}). Delete ${lockPath} to force.`, "IMPROVE_LOCK_HELD");
|
|
69
76
|
}
|
|
70
77
|
export function releaseImproveLock(ownership) {
|
|
71
78
|
releaseLock(ownership);
|
|
@@ -9,7 +9,7 @@ import { DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE, loadConfig } from "../../core/conf
|
|
|
9
9
|
import { UsageError } from "../../core/errors.js";
|
|
10
10
|
import { appendEvent } from "../../core/events.js";
|
|
11
11
|
import { openLogsDatabase, purgeOldTaskLogs } from "../../core/logs-db.js";
|
|
12
|
-
import { getDbPath } from "../../core/paths.js";
|
|
12
|
+
import { getDbPath, getTaskLogDir } from "../../core/paths.js";
|
|
13
13
|
import { withStateDb } from "../../core/state-db.js";
|
|
14
14
|
import { info } from "../../core/warn.js";
|
|
15
15
|
import { DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES, runGraphExtractionPass, } from "../../indexer/graph/graph-extraction.js";
|
|
@@ -25,6 +25,7 @@ import { closeDatabase, openIndexDatabase } from "../../storage/repositories/ind
|
|
|
25
25
|
import { getEntryByRef } from "../../storage/repositories/index-entries-repository.js";
|
|
26
26
|
import { clearAssetOutcomeMissing, countAssetOutcomeMissing, deleteAssetOutcomeMissingBefore, listAssetOutcomeMissingState, stampAssetOutcomeMissing, } from "../../storage/repositories/outcome-repository.js";
|
|
27
27
|
import { clearAssetSalienceMissing, countAssetSalienceMissing, deleteAssetSalienceMissingBefore, listAssetSalienceMissingState, stampAssetSalienceMissing, } from "../../storage/repositories/salience-repository.js";
|
|
28
|
+
import { purgeOldTaskLogFiles } from "../../tasks/run/task-log.js";
|
|
28
29
|
import { expireStaleProposals, listProposals, purgeOrphanProposals } from "../proposal/repository.js";
|
|
29
30
|
import { checkDeadUrls } from "../url-checker.js";
|
|
30
31
|
import { DEFAULT_RETENTION_DAYS as CYCLE_METRICS_RETENTION_DAYS, runCollapseDetector } from "./collapse-detector.js";
|
|
@@ -1124,6 +1125,25 @@ export function runRetentionPurgePass(ctx) {
|
|
|
1124
1125
|
}
|
|
1125
1126
|
}
|
|
1126
1127
|
}
|
|
1128
|
+
// Per-run flat log files under getTaskLogDir() (#951): logs.db above is
|
|
1129
|
+
// the durable record and already retention-purged, so the transitional
|
|
1130
|
+
// `<taskId>/<timestamp>.log` tail files can be deleted on the same
|
|
1131
|
+
// window without losing anything. A separate try/catch — a filesystem
|
|
1132
|
+
// problem here must not block the DB purges above.
|
|
1133
|
+
try {
|
|
1134
|
+
const taskLogFilesPurged = purgeOldTaskLogFiles(undefined, retentionDays);
|
|
1135
|
+
if (taskLogFilesPurged > 0) {
|
|
1136
|
+
info(`[improve] task log files purge: ${taskLogFilesPurged} file(s) older than ${retentionDays}d removed from ${getTaskLogDir()}`);
|
|
1137
|
+
}
|
|
1138
|
+
appendEvent({
|
|
1139
|
+
eventType: "task_log_files_purged",
|
|
1140
|
+
ref: "task_log_files/_purge",
|
|
1141
|
+
metadata: { purgedCount: taskLogFilesPurged, retentionDays },
|
|
1142
|
+
}, eventsCtx);
|
|
1143
|
+
}
|
|
1144
|
+
catch (err) {
|
|
1145
|
+
warnings.push(`task log files purge failed: ${errMessage(err)}`);
|
|
1146
|
+
}
|
|
1127
1147
|
}
|
|
1128
1148
|
return { warnings };
|
|
1129
1149
|
}
|