akm-cli 0.9.16 → 0.9.17-alpha.2
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 +504 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/graph-extract-user-prompt.md +5 -5
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/checks.js +54 -0
- package/dist/commands/health/improve-metrics.js +1 -5
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health.js +10 -0
- package/dist/commands/improve/consolidate/chunking.js +19 -35
- package/dist/commands/improve/consolidate/merge.js +6 -9
- package/dist/commands/improve/consolidate.js +104 -91
- package/dist/commands/improve/distill/promote-memory.js +40 -2
- package/dist/commands/improve/distill/quality-gate.js +186 -23
- package/dist/commands/improve/distill.js +42 -8
- package/dist/commands/improve/eligibility.js +13 -3
- package/dist/commands/improve/improve-cli.js +32 -9
- package/dist/commands/improve/improve-strategies.js +23 -1
- package/dist/commands/improve/improve.js +121 -84
- package/dist/commands/improve/loop-stages.js +241 -108
- package/dist/commands/improve/preparation.js +50 -17
- package/dist/commands/improve/reflect.js +16 -5
- package/dist/commands/improve/shared.js +0 -10
- package/dist/commands/proposal/drain.js +79 -10
- package/dist/commands/proposal/proposal-types.js +21 -0
- package/dist/commands/proposal/repository.js +108 -29
- package/dist/commands/tasks/tasks.js +19 -2
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/config/config.js +5 -2
- package/dist/core/config/retired-experimental-keys-shim.js +62 -0
- package/dist/core/config/schema/improve-processes.js +29 -2
- package/dist/core/improve-result.js +9 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/write-source.js +10 -2
- package/dist/indexer/ensure-index.js +52 -7
- package/dist/indexer/graph/graph-extraction.js +82 -8
- package/dist/indexer/passes/memory-inference.js +16 -1
- package/dist/llm/client.js +16 -2
- package/dist/llm/graph-extract.js +162 -18
- package/dist/scripts/akm-migrate-node.js +97 -36
- package/dist/scripts/akm-migrate.js +97 -36
- package/dist/storage/repositories/index-entries-repository.js +43 -0
- package/dist/storage/repositories/proposals-repository.js +4 -1
- package/dist/storage/state-db-integrity.js +123 -0
- package/dist/workflows/program/schema.js +1 -0
- package/docs/reference/cli.md +17 -7
- package/docs/reference/data-and-telemetry.md +1 -0
- package/package.json +1 -1
- package/schemas/akm-config.json +44 -0
- package/schemas/akm-workflow.json +1 -0
- package/dist/commands/improve/eval-cases.js +0 -52
|
@@ -21,11 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
|
+
import { hashContent } from "../core/adapter/adapters/shared.js";
|
|
24
25
|
import { placementSpecList } from "../core/asset/asset-placement.js";
|
|
25
26
|
import { classifyPathAccess } from "../core/path-access.js";
|
|
26
27
|
import { getDbPath } from "../core/paths.js";
|
|
28
|
+
import { warnVerbose } from "../core/warn.js";
|
|
27
29
|
import { assertIndexPathReadable, closeDatabase, openExistingDatabase } from "../storage/repositories/index-connection.js";
|
|
28
|
-
import { getEntryCount, getIndexedFilePaths } from "../storage/repositories/index-entries-repository.js";
|
|
30
|
+
import { getEntryCount, getIndexedFileHashes, getIndexedFilePaths, } from "../storage/repositories/index-entries-repository.js";
|
|
29
31
|
import { isCanonicalIndexGeneration } from "../storage/repositories/index-entry-schema.js";
|
|
30
32
|
import { getMeta } from "../storage/repositories/index-meta-repository.js";
|
|
31
33
|
import { warnOnBundleRenameDrift } from "./bundle-identity-guard.js";
|
|
@@ -77,11 +79,20 @@ function getIndexableFiles(root, spec) {
|
|
|
77
79
|
* millisecond-truncated), so the mtime test alone silently misses
|
|
78
80
|
* additions made within ~a millisecond of the previous build.
|
|
79
81
|
*
|
|
82
|
+
* A file whose mtime is newer than `builtAt` is NOT automatically stale
|
|
83
|
+
* (R6): `indexWrittenAssets` upserts a fresh `content_hash` without bumping
|
|
84
|
+
* `builtAt`, so a file `ensureIndex` itself just incrementally re-indexed
|
|
85
|
+
* (for example a proposal triage just promoted into `knowledge/`) would
|
|
86
|
+
* otherwise keep tripping this check on every subsequent call, forcing the
|
|
87
|
+
* full rescan the write-path fast path exists to avoid. Only files newer
|
|
88
|
+
* than `builtAt` are hashed here, so this stays cheap — the common case is
|
|
89
|
+
* zero or a handful of such files.
|
|
90
|
+
*
|
|
80
91
|
* `getIndexableFiles` applies each asset type's own relevance filter, so
|
|
81
92
|
* non-indexed companion files (e.g. `package.json` next to a knowledge doc) are
|
|
82
93
|
* never considered and do not produce false "new file" positives.
|
|
83
94
|
*/
|
|
84
|
-
function hasNewerIndexableFiles(stashDir, builtAt, indexedPaths) {
|
|
95
|
+
function hasNewerIndexableFiles(stashDir, builtAt, indexedPaths, indexedHashes) {
|
|
85
96
|
const builtAtMs = builtAt ? new Date(builtAt).getTime() : Number.NaN;
|
|
86
97
|
const builtAtUsable = Number.isFinite(builtAtMs);
|
|
87
98
|
for (const spec of placementSpecList()) {
|
|
@@ -92,13 +103,31 @@ function hasNewerIndexableFiles(stashDir, builtAt, indexedPaths) {
|
|
|
92
103
|
return true;
|
|
93
104
|
if (!builtAtUsable)
|
|
94
105
|
return true;
|
|
106
|
+
let mtimeMs;
|
|
95
107
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
mtimeMs = fs.statSync(file).mtimeMs;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (mtimeMs <= builtAtMs)
|
|
114
|
+
continue;
|
|
115
|
+
// Newer than the last full build — only stale if its current content
|
|
116
|
+
// actually differs from what is indexed. No stored hash means the row
|
|
117
|
+
// predates content-hash tracking (or a hash-less enrichment pass), so
|
|
118
|
+
// fall back to the conservative mtime-stale answer.
|
|
119
|
+
const indexedHash = indexedHashes.get(file);
|
|
120
|
+
if (indexedHash === undefined)
|
|
121
|
+
return true;
|
|
122
|
+
let currentHash;
|
|
123
|
+
try {
|
|
124
|
+
currentHash = hashContent(fs.readFileSync(file, "utf8"));
|
|
98
125
|
}
|
|
99
126
|
catch {
|
|
100
127
|
return true;
|
|
101
128
|
}
|
|
129
|
+
if (currentHash !== indexedHash)
|
|
130
|
+
return true;
|
|
102
131
|
}
|
|
103
132
|
}
|
|
104
133
|
return false;
|
|
@@ -125,7 +154,7 @@ export function isIndexStale(stashDir) {
|
|
|
125
154
|
if (entryCount === 0)
|
|
126
155
|
return true;
|
|
127
156
|
const builtAt = getMeta(db, "builtAt");
|
|
128
|
-
if (hasNewerIndexableFiles(stashDir, builtAt, getIndexedFilePaths(db)))
|
|
157
|
+
if (hasNewerIndexableFiles(stashDir, builtAt, getIndexedFilePaths(db), getIndexedFileHashes(db)))
|
|
129
158
|
return true;
|
|
130
159
|
const storedStashDir = getMeta(db, "stashDir");
|
|
131
160
|
if (storedStashDir !== stashDir) {
|
|
@@ -193,12 +222,24 @@ function indexCanServeStash(stashDir) {
|
|
|
193
222
|
}
|
|
194
223
|
async function runInlineReindex(stashDir, options = {}) {
|
|
195
224
|
const { akmIndex } = await import("./indexer.js");
|
|
196
|
-
|
|
225
|
+
const startedMs = Date.now();
|
|
226
|
+
const response = await akmIndex({
|
|
197
227
|
stashDir,
|
|
198
228
|
implicit: true,
|
|
199
229
|
...(options.signal ? { signal: options.signal } : {}),
|
|
200
230
|
...(options.hydrateSources === false ? { hydrateSources: false } : {}),
|
|
201
231
|
});
|
|
232
|
+
// R6: the implicit reindex's cost was previously discarded entirely
|
|
233
|
+
// (`await akmIndex(...)` and nothing else), making a 27-minute blocking
|
|
234
|
+
// rebuild invisible to both the operator and the improve result. Fall back
|
|
235
|
+
// to a wall-clock measurement when the response carries no `timing` block.
|
|
236
|
+
const durationMs = response.timing?.totalMs ?? Date.now() - startedMs;
|
|
237
|
+
const timing = response.timing;
|
|
238
|
+
warnVerbose(`[ensure-index] implicit reindex completed in ${durationMs}ms` +
|
|
239
|
+
(timing
|
|
240
|
+
? ` (walk=${timing.walkMs}ms llm=${timing.llmMs}ms embed=${timing.embedMs}ms finalize=${timing.finalizeMs}ms)`
|
|
241
|
+
: ""));
|
|
242
|
+
options.onReindexTiming?.({ durationMs, timing });
|
|
202
243
|
return true;
|
|
203
244
|
}
|
|
204
245
|
/**
|
|
@@ -226,7 +267,10 @@ export async function ensureIndex(stashDir, options = {}) {
|
|
|
226
267
|
// materialization point — hydrate cache-backed sources as usual.
|
|
227
268
|
if (!isIndexStale(stashDir))
|
|
228
269
|
return false;
|
|
229
|
-
return runInlineReindex(stashDir, {
|
|
270
|
+
return runInlineReindex(stashDir, {
|
|
271
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
272
|
+
...(options.onReindexTiming ? { onReindexTiming: options.onReindexTiming } : {}),
|
|
273
|
+
});
|
|
230
274
|
}
|
|
231
275
|
// Background = the READ path (`show` auto-index): query time must never clone/
|
|
232
276
|
// pull/fetch (spec §14.3 / D11). Build from already-materialized content only;
|
|
@@ -236,5 +280,6 @@ export async function ensureIndex(stashDir, options = {}) {
|
|
|
236
280
|
return runInlineReindex(stashDir, {
|
|
237
281
|
...(options.signal ? { signal: options.signal } : {}),
|
|
238
282
|
hydrateSources: false,
|
|
283
|
+
...(options.onReindexTiming ? { onReindexTiming: options.onReindexTiming } : {}),
|
|
239
284
|
});
|
|
240
285
|
}
|
|
@@ -128,7 +128,7 @@ function normalizeConfidence(raw) {
|
|
|
128
128
|
return undefined;
|
|
129
129
|
return Math.max(0, Math.min(1, raw));
|
|
130
130
|
}
|
|
131
|
-
function getGraphExtractorId(config) {
|
|
131
|
+
export function getGraphExtractorId(config) {
|
|
132
132
|
const fingerprint = computeBodyHash(JSON.stringify({
|
|
133
133
|
promptVersion: graphExtract.GRAPH_EXTRACT_PROMPT_VERSION,
|
|
134
134
|
model: config.model,
|
|
@@ -152,6 +152,36 @@ function buildLowQualityWarnings(quality, telemetry) {
|
|
|
152
152
|
}
|
|
153
153
|
return warnings;
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Failure-rate abort for the extraction run (R2), modelled on consolidate's
|
|
157
|
+
* chunk-level guard (`ABORT_MIN_CHUNKS`/`ABORT_FAILURE_RATE` in
|
|
158
|
+
* consolidate.ts, C-6/#392): rate-based over a minimum sample so a couple of
|
|
159
|
+
* transient per-file failures cannot abort a run that would otherwise
|
|
160
|
+
* recover, while a systemically dead provider stops burning through the
|
|
161
|
+
* rest of the eligible set. The existing "one failure must not abort the
|
|
162
|
+
* rest" behaviour for individual files is untouched — this only stops
|
|
163
|
+
* further model calls once the failure rate itself is the signal.
|
|
164
|
+
*/
|
|
165
|
+
const GRAPH_EXTRACTION_ABORT_MIN_ATTEMPTS = 4;
|
|
166
|
+
const GRAPH_EXTRACTION_ABORT_FAILURE_RATE = 0.5;
|
|
167
|
+
/** Records one attempted (non-cache-hit) model call and flips `aborted` once the failure-rate threshold is crossed. */
|
|
168
|
+
function recordGraphExtractionAttempt(state, failed) {
|
|
169
|
+
if (state.aborted)
|
|
170
|
+
return;
|
|
171
|
+
state.attempts += 1;
|
|
172
|
+
if (failed)
|
|
173
|
+
state.failures += 1;
|
|
174
|
+
if (state.attempts < GRAPH_EXTRACTION_ABORT_MIN_ATTEMPTS)
|
|
175
|
+
return;
|
|
176
|
+
const failureRate = state.failures / state.attempts;
|
|
177
|
+
if (failureRate < GRAPH_EXTRACTION_ABORT_FAILURE_RATE)
|
|
178
|
+
return;
|
|
179
|
+
state.aborted = true;
|
|
180
|
+
state.message =
|
|
181
|
+
`graph extraction aborted — failure rate ${(failureRate * 100).toFixed(0)}% over ${state.attempts} ` +
|
|
182
|
+
`attempt(s) (>= ${GRAPH_EXTRACTION_ABORT_FAILURE_RATE * 100}% threshold). LLM may be unavailable.`;
|
|
183
|
+
warn(state.message);
|
|
184
|
+
}
|
|
155
185
|
export function getGraphExtractionIncludeTypes(config) {
|
|
156
186
|
const configured = getIndexPassConfig(config.index, "graph")?.graphExtractionIncludeTypes;
|
|
157
187
|
if (!configured || configured.length === 0)
|
|
@@ -200,6 +230,20 @@ function validateGraphCacheShape(raw) {
|
|
|
200
230
|
...(typeof obj.reason === "string" ? { reason: obj.reason } : {}),
|
|
201
231
|
};
|
|
202
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A `"failed"` extraction (provider error, invalid JSON, context overflow —
|
|
235
|
+
* see {@link GraphExtractionStatus}) must never be reused as a cache hit or
|
|
236
|
+
* re-persisted as one. R2: a dead provider upserted ~30,900 rows shaped
|
|
237
|
+
* `{"entities":[],"relations":[],"status":"failed","reason":"llm_error"}`,
|
|
238
|
+
* and both hit paths (the `llm_enrichment_cache` lookup and `reuseGraphNode`
|
|
239
|
+
* over the previous graph) validated the shape without checking `status`, so
|
|
240
|
+
* 92% of the persisted graph became a permanent hit that never retried. A
|
|
241
|
+
* failed result becomes a miss naturally and is overwritten on the next
|
|
242
|
+
* successful extraction; existing failed rows are left on disk untouched.
|
|
243
|
+
*/
|
|
244
|
+
function isFailedExtractionStatus(status) {
|
|
245
|
+
return status === "failed";
|
|
246
|
+
}
|
|
203
247
|
function loadGraphFile(stashRoot, db) {
|
|
204
248
|
if (!db)
|
|
205
249
|
return { files: [] };
|
|
@@ -252,6 +296,8 @@ function reuseGraphNode(previousNodes, candidate, bodyHash) {
|
|
|
252
296
|
return undefined;
|
|
253
297
|
if (node.bodyHash !== bodyHash)
|
|
254
298
|
return undefined;
|
|
299
|
+
if (isFailedExtractionStatus(node.status))
|
|
300
|
+
return undefined;
|
|
255
301
|
const validated = validateGraphCacheShape({ entities: node.entities, relations: node.relations });
|
|
256
302
|
if (!validated)
|
|
257
303
|
return undefined;
|
|
@@ -276,8 +322,9 @@ function planEligibleGraphExtractions(args) {
|
|
|
276
322
|
if (entry?.bodyHash === bodyHash) {
|
|
277
323
|
try {
|
|
278
324
|
const cached = validateGraphCacheShape(JSON.parse(entry.resultJson));
|
|
279
|
-
if (cached)
|
|
325
|
+
if (cached && !isFailedExtractionStatus(cached.status)) {
|
|
280
326
|
return { kind: "cache-hit", candidate, bodyHash, cached, persistCache: false };
|
|
327
|
+
}
|
|
281
328
|
}
|
|
282
329
|
catch {
|
|
283
330
|
// Corrupt cache rows are immutable model plans for this pass.
|
|
@@ -305,7 +352,7 @@ function graphRecordFromCachePlan(plan) {
|
|
|
305
352
|
};
|
|
306
353
|
}
|
|
307
354
|
async function extractGraphBatches(args) {
|
|
308
|
-
const { plans, batchSize, signal, db, cacheVariant, telemetry, llmRunner, lease, featureConfig, onFallback, batchState, runtimeTelemetry, onNotices, reportProgress, } = args;
|
|
355
|
+
const { plans, batchSize, signal, db, cacheVariant, telemetry, llmRunner, lease, featureConfig, onFallback, abortState, batchState, runtimeTelemetry, onNotices, reportProgress, maxChunksPerAsset, } = args;
|
|
309
356
|
const results = new Array(plans.length).fill(undefined);
|
|
310
357
|
const chunkStarts = [];
|
|
311
358
|
for (let start = 0; start < plans.length; start += batchSize)
|
|
@@ -328,12 +375,12 @@ async function extractGraphBatches(args) {
|
|
|
328
375
|
continue;
|
|
329
376
|
telemetry.cacheHits += 1;
|
|
330
377
|
results[start + index] = graphRecordFromCachePlan(plan);
|
|
331
|
-
if (db && plan.persistCache) {
|
|
378
|
+
if (db && plan.persistCache && !isFailedExtractionStatus(plan.cached.status)) {
|
|
332
379
|
upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(plan.cached), cacheVariant);
|
|
333
380
|
}
|
|
334
381
|
}
|
|
335
382
|
const modelPlans = chunk.filter((plan) => plan.kind === "model");
|
|
336
|
-
if (modelPlans.length === 0) {
|
|
383
|
+
if (modelPlans.length === 0 || abortState.aborted) {
|
|
337
384
|
reportChunkProgress();
|
|
338
385
|
return;
|
|
339
386
|
}
|
|
@@ -345,6 +392,7 @@ async function extractGraphBatches(args) {
|
|
|
345
392
|
telemetry: runtimeTelemetry,
|
|
346
393
|
onNotices,
|
|
347
394
|
...(lease ? { lease } : {}),
|
|
395
|
+
...(maxChunksPerAsset != null ? { maxChunksPerAsset } : {}),
|
|
348
396
|
});
|
|
349
397
|
}
|
|
350
398
|
catch (error) {
|
|
@@ -355,6 +403,8 @@ async function extractGraphBatches(args) {
|
|
|
355
403
|
throw error;
|
|
356
404
|
}
|
|
357
405
|
let llmIndex = 0;
|
|
406
|
+
let dispatchHadResult = false;
|
|
407
|
+
let dispatchAllFailed = true;
|
|
358
408
|
for (let index = 0; index < chunk.length; index++) {
|
|
359
409
|
const plan = chunk[index];
|
|
360
410
|
if (!plan || plan.kind !== "model")
|
|
@@ -369,7 +419,10 @@ async function extractGraphBatches(args) {
|
|
|
369
419
|
...(extraction.status ? { status: extraction.status } : {}),
|
|
370
420
|
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
371
421
|
};
|
|
372
|
-
|
|
422
|
+
dispatchHadResult = true;
|
|
423
|
+
if (!isFailedExtractionStatus(cacheShape.status))
|
|
424
|
+
dispatchAllFailed = false;
|
|
425
|
+
if (db && !isFailedExtractionStatus(cacheShape.status)) {
|
|
373
426
|
upsertLlmCacheEntry(db, plan.candidate.absPath, plan.bodyHash, JSON.stringify(cacheShape), cacheVariant);
|
|
374
427
|
}
|
|
375
428
|
results[start + index] = {
|
|
@@ -379,6 +432,14 @@ async function extractGraphBatches(args) {
|
|
|
379
432
|
...cacheShape,
|
|
380
433
|
};
|
|
381
434
|
}
|
|
435
|
+
// One attempt per `extractGraphFromBodies` dispatch (this chunk's batch
|
|
436
|
+
// call), not one per file it covers — mirrors consolidate.ts's
|
|
437
|
+
// totalChunksProcessed++/totalChunksFailed, which count once per chunk
|
|
438
|
+
// regardless of how many memories are in it. Counting per file let a
|
|
439
|
+
// single batched provider_error satisfy GRAPH_EXTRACTION_ABORT_MIN_ATTEMPTS
|
|
440
|
+
// after one HTTP failure whenever graphExtractionBatchSize >= 4.
|
|
441
|
+
if (dispatchHadResult)
|
|
442
|
+
recordGraphExtractionAttempt(abortState, dispatchAllFailed);
|
|
382
443
|
reportChunkProgress();
|
|
383
444
|
}, llmRunner.connection.concurrency ?? 1);
|
|
384
445
|
return { results, ...(configFailure ? { configFailure } : {}) };
|
|
@@ -670,6 +731,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
670
731
|
batchingDisabled: false,
|
|
671
732
|
nonArrayBatchFailures: 0,
|
|
672
733
|
};
|
|
734
|
+
const abortState = { attempts: 0, failures: 0, aborted: false };
|
|
673
735
|
warnVerbose(`graph extraction: starting for ${considered} eligible file(s) under ${primary.path}; ` +
|
|
674
736
|
`includeTypes=${includeTypes.join(",")}, batchSize=${batchSize}, concurrency=${llmRunner.connection.concurrency ?? 1}, ` +
|
|
675
737
|
`reEnrich=${reEnrich === true}, candidateScoped=${options.candidatePaths ? "true" : "false"}.`);
|
|
@@ -690,11 +752,15 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
690
752
|
if (plan.kind === "cache-hit") {
|
|
691
753
|
telemetry.cacheHits += 1;
|
|
692
754
|
cached = plan.cached;
|
|
693
|
-
if (db && plan.persistCache) {
|
|
755
|
+
if (db && plan.persistCache && !isFailedExtractionStatus(cached.status)) {
|
|
694
756
|
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
695
757
|
}
|
|
696
758
|
}
|
|
697
759
|
else {
|
|
760
|
+
if (abortState.aborted) {
|
|
761
|
+
reportProgress(candidate.absPath, undefined);
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
698
764
|
telemetry.cacheMisses += 1;
|
|
699
765
|
let extraction;
|
|
700
766
|
try {
|
|
@@ -703,6 +769,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
703
769
|
telemetry: runtimeTelemetry,
|
|
704
770
|
onNotices,
|
|
705
771
|
...(dispatchLease ? { lease: dispatchLease } : {}),
|
|
772
|
+
...(options.maxChunksPerAsset != null ? { maxChunksPerAsset: options.maxChunksPerAsset } : {}),
|
|
706
773
|
});
|
|
707
774
|
}
|
|
708
775
|
catch (err) {
|
|
@@ -719,7 +786,8 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
719
786
|
...(extraction.status ? { status: extraction.status } : {}),
|
|
720
787
|
...(extraction.reason ? { reason: extraction.reason } : {}),
|
|
721
788
|
};
|
|
722
|
-
|
|
789
|
+
recordGraphExtractionAttempt(abortState, isFailedExtractionStatus(cached.status));
|
|
790
|
+
if (db && !isFailedExtractionStatus(cached.status)) {
|
|
723
791
|
upsertLlmCacheEntry(db, candidate.absPath, bodyHash, JSON.stringify(cached), cacheVariant);
|
|
724
792
|
}
|
|
725
793
|
}
|
|
@@ -754,8 +822,10 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
754
822
|
onFallback,
|
|
755
823
|
batchState,
|
|
756
824
|
runtimeTelemetry,
|
|
825
|
+
abortState,
|
|
757
826
|
onNotices,
|
|
758
827
|
reportProgress,
|
|
828
|
+
...(options.maxChunksPerAsset != null ? { maxChunksPerAsset: options.maxChunksPerAsset } : {}),
|
|
759
829
|
});
|
|
760
830
|
extractionResults = batch.results;
|
|
761
831
|
configFailure ??= batch.configFailure;
|
|
@@ -797,14 +867,18 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
797
867
|
const assetRefs = mergedNodes.map((node) => node.path);
|
|
798
868
|
const deduped = deduplicateGraph(mergedNodes.map((node) => ({ entities: node.entities, relations: node.relations })), assetRefs);
|
|
799
869
|
telemetry.truncationCount = runtimeTelemetry.truncationCount ?? 0;
|
|
870
|
+
telemetry.truncatedChunks = runtimeTelemetry.truncatedChunks ?? 0;
|
|
800
871
|
telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
|
|
801
872
|
telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
|
|
802
873
|
telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
|
|
803
874
|
telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
|
|
875
|
+
telemetry.aborted = abortState.aborted;
|
|
804
876
|
const qualityConsidered = mergedNodes.length;
|
|
805
877
|
const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
|
|
806
878
|
const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
807
879
|
const warnings = buildLowQualityWarnings(quality, telemetry);
|
|
880
|
+
if (abortState.message)
|
|
881
|
+
warnings.push(abortState.message);
|
|
808
882
|
for (const warning of warnings)
|
|
809
883
|
warnVerbose(`graph extraction quality: ${warning}`);
|
|
810
884
|
const graph = {
|
|
@@ -46,7 +46,7 @@ import { todayIso } from "../../core/common.js";
|
|
|
46
46
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
47
47
|
import { ConfigError } from "../../core/errors.js";
|
|
48
48
|
import { warn } from "../../core/warn.js";
|
|
49
|
-
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
49
|
+
import { beginWriteProvenance, recordWrittenPath } from "../../core/write-provenance.js";
|
|
50
50
|
import { writeAssetToSource } from "../../core/write-source.js";
|
|
51
51
|
import { disposeLoweredExecutionDispatchLease, } from "../../integrations/agent/execution-lowering.js";
|
|
52
52
|
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
@@ -188,6 +188,18 @@ async function inferPendingMemoryRecord(plan, ctx) {
|
|
|
188
188
|
* short-circuits to a no-op result.
|
|
189
189
|
*/
|
|
190
190
|
export async function runMemoryInferencePass(ctx) {
|
|
191
|
+
// R78 (tier1-0917): owns the write-provenance journal end-to-end so it closes on every
|
|
192
|
+
// exit path, including a throw out of the body below — an unclosed journal
|
|
193
|
+
// would keep reporting every later write in this process as this call's own.
|
|
194
|
+
const provenance = beginWriteProvenance();
|
|
195
|
+
try {
|
|
196
|
+
return await runMemoryInferencePassBody(ctx, provenance);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
provenance.end();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function runMemoryInferencePassBody(ctx, provenance) {
|
|
191
203
|
const { config, sources, signal, db, reEnrich, onProgress, options = {} } = ctx;
|
|
192
204
|
const invocationOwnsRunner = Object.hasOwn(ctx, "llmRunner");
|
|
193
205
|
const compressMemoryToDerivedMemory = options.compressMemoryToDerivedMemory ?? memoryInfer.compressMemoryToDerivedMemory;
|
|
@@ -202,6 +214,7 @@ export async function runMemoryInferencePass(ctx) {
|
|
|
202
214
|
skippedAborted: 0,
|
|
203
215
|
unaccounted: 0,
|
|
204
216
|
htmlErrorCount: 0,
|
|
217
|
+
writtenPaths: [],
|
|
205
218
|
};
|
|
206
219
|
// Mutable sink threaded into compressMemoryToDerivedMemory so the per-call
|
|
207
220
|
// HTML-error categorization (which is otherwise swallowed inside the feature
|
|
@@ -215,6 +228,8 @@ export async function runMemoryInferencePass(ctx) {
|
|
|
215
228
|
const completeResult = () => {
|
|
216
229
|
if (noticesByKey.size > 0)
|
|
217
230
|
result.notices = Object.freeze([...noticesByKey.values()]);
|
|
231
|
+
// Non-destructive read — the wrapper's `finally` owns closing the journal.
|
|
232
|
+
result.writtenPaths = provenance.writtenPaths();
|
|
218
233
|
return result;
|
|
219
234
|
};
|
|
220
235
|
// Gate 1 — feature gate via isProcessEnabled, which reads the 0.8.0 path
|
package/dist/llm/client.js
CHANGED
|
@@ -117,12 +117,24 @@ export function isContextSizeError(message) {
|
|
|
117
117
|
/exceeded|over.*limit|too.*long/.test(lower);
|
|
118
118
|
return evidence;
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Codes describing a failure to reach or get a usable response from the
|
|
122
|
+
* provider transport itself, as opposed to a malformed-but-received response
|
|
123
|
+
* (`parse_error`) or a request-shape rejection (`rate_limited`). Shared by
|
|
124
|
+
* {@link isRetryable} (which additionally requires evidence the failure is
|
|
125
|
+
* transient) and the batch graph-extraction storm guard in `graph-extract.ts`
|
|
126
|
+
* (which treats any of these as "the provider is down, stop retrying
|
|
127
|
+
* per-asset") so the two classifications cannot drift apart.
|
|
128
|
+
*/
|
|
129
|
+
export function isTransportFailure(err) {
|
|
130
|
+
return err.code === "provider_error" || err.code === "network_error" || err.code === "provider_html_error";
|
|
131
|
+
}
|
|
120
132
|
/**
|
|
121
133
|
* Decide whether a first-attempt {@link LlmCallError} is eligible for a single
|
|
122
134
|
* retry. Retryable: HTTP 5xx (`provider_error` with statusCode >= 500) and
|
|
123
135
|
* `network_error` whose message looks like a transient connection drop.
|
|
124
|
-
* NOT retryable: 4xx, `rate_limited` (429), `timeout`, `parse_error`,
|
|
125
|
-
* context-overflow-classified errors.
|
|
136
|
+
* NOT retryable: 4xx, `rate_limited` (429), `timeout`, `parse_error`,
|
|
137
|
+
* `provider_html_error`, and context-overflow-classified errors.
|
|
126
138
|
*
|
|
127
139
|
* The connection-drop heuristic covers the substrings emitted across runtimes
|
|
128
140
|
* for a mid-flight socket close:
|
|
@@ -141,6 +153,8 @@ export function isContextSizeError(message) {
|
|
|
141
153
|
function isRetryable(err) {
|
|
142
154
|
if (isContextSizeError(err.message))
|
|
143
155
|
return false;
|
|
156
|
+
if (!isTransportFailure(err))
|
|
157
|
+
return false;
|
|
144
158
|
if (err.code === "provider_error") {
|
|
145
159
|
return typeof err.statusCode === "number" && err.statusCode >= 500;
|
|
146
160
|
}
|