akm-cli 0.9.0 → 0.9.1-beta.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 +724 -0
- package/README.md +28 -63
- package/STABILITY.md +4 -2
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/feedback-cli.js +7 -1
- package/dist/commands/health/llm-usage.js +2 -1
- package/dist/commands/health/surfaces.js +4 -77
- package/dist/commands/health.js +65 -11
- package/dist/commands/improve/distill/quality-gate.js +6 -1
- package/dist/commands/improve/eligibility.js +7 -1
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/improve.js +126 -10
- package/dist/commands/improve/locks.js +7 -0
- package/dist/commands/improve/memory/memory-improve.js +9 -0
- package/dist/commands/improve/run-context.js +5 -0
- package/dist/commands/improve/session-asset.js +4 -0
- package/dist/commands/lint/base-linter.js +31 -7
- package/dist/commands/lint/index.js +205 -51
- package/dist/commands/lint/types.js +22 -1
- package/dist/commands/proposal/repository.js +17 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/info.js +12 -2
- package/dist/commands/sources/installed-stashes.js +6 -1
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/commands/tasks/tasks.js +8 -2
- package/dist/commands/workflow-cli.js +17 -11
- package/dist/core/abort-deadline.js +28 -0
- package/dist/core/adapter/adapters/agent-skills-adapter.js +83 -5
- package/dist/core/adapter/adapters/akm-adapter.js +13 -10
- package/dist/core/adapter/adapters/akm-lint.js +78 -22
- package/dist/core/adapter/adapters/akm-task-adapter.js +43 -20
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +5 -3
- package/dist/core/asset/frontmatter.js +10 -1
- package/dist/core/common.js +147 -9
- package/dist/core/concurrent.js +32 -0
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/config/schema/workflow.js +11 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/events.js +30 -24
- package/dist/core/extra-params.js +11 -0
- package/dist/core/file-lock.js +7 -1
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/improve-result.js +5 -0
- package/dist/core/json-schema.js +344 -9
- package/dist/core/loopback.js +89 -0
- package/dist/core/migration-operation.js +17 -2
- package/dist/core/path-access.js +107 -0
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +86 -18
- package/dist/core/spawn-env.js +234 -0
- package/dist/core/state-db-scope.js +134 -0
- package/dist/core/state-db.js +1 -0
- package/dist/core/subprocess.js +181 -37
- package/dist/core/write-provenance.js +85 -0
- package/dist/core/write-source.js +33 -2
- package/dist/indexer/db/graph-db.js +17 -6
- package/dist/indexer/ensure-index.js +10 -3
- package/dist/indexer/index-written-assets.js +17 -2
- package/dist/indexer/indexer.js +86 -21
- package/dist/indexer/passes/memory-inference.js +4 -0
- package/dist/indexer/search/db-search.js +25 -17
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/agent/engine-resolution.js +24 -11
- package/dist/integrations/agent/model-aliases.js +1 -1
- package/dist/integrations/agent/profiles.js +9 -1
- package/dist/integrations/agent/spawn.js +15 -87
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +55 -2
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/output/text/lint-format.js +17 -4
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +1714 -836
- package/dist/scripts/akm-migrate.js +1682 -804
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/providers/git-stash.js +19 -0
- package/dist/sources/providers/git.js +1 -1
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -20
- package/dist/storage/database.js +71 -7
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/managed-db.js +19 -0
- package/dist/storage/repositories/index-connection.js +39 -4
- package/dist/storage/repositories/index-entries-repository.js +6 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/repositories/workflow-runs-repository.js +66 -13
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/log-redaction.js +156 -0
- package/dist/tasks/parser.js +82 -5
- package/dist/tasks/runner.js +222 -17
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +86 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/concurrency-policy.js +95 -1
- package/dist/workflows/exec/dispatch-redaction.js +114 -0
- package/dist/workflows/exec/exec-unit.js +542 -0
- package/dist/workflows/exec/frozen-judge.js +114 -42
- package/dist/workflows/exec/native-executor.js +465 -238
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +424 -219
- package/dist/workflows/exec/step-work.js +506 -167
- package/dist/workflows/exec/unit-dispatch.js +31 -1
- package/dist/workflows/exec/unit-writer.js +53 -13
- package/dist/workflows/exec/worktree.js +454 -41
- package/dist/workflows/ir/compile.js +26 -2
- package/dist/workflows/ir/freeze.js +82 -15
- package/dist/workflows/ir/schema.js +105 -20
- package/dist/workflows/parser.js +242 -19
- package/dist/workflows/program/schema.js +24 -0
- package/dist/workflows/renderer.js +32 -4
- package/dist/workflows/resource-limits.js +182 -0
- package/dist/workflows/runtime/runs.js +146 -6
- package/dist/workflows/validate-summary.js +17 -2
- package/docs/README.md +74 -32
- package/docs/migration/release-notes/0.9.0.md +2 -1
- package/docs/migration/v0.7-to-v0.8.md +2 -1
- package/docs/migration/v0.8-to-v0.9.md +3 -1
- package/docs/reference/README.md +11 -4
- package/docs/reference/bundle-types.md +19 -0
- package/docs/reference/cli.md +105 -16
- package/docs/reference/configuration.md +15 -2
- package/docs/reference/data-and-telemetry.md +30 -10
- package/docs/reference/supported-formats.md +50 -0
- package/docs/reference/workflow-schema.md +1014 -0
- package/docs/reference/workflows.md +37 -633
- package/package.json +13 -6
- package/schemas/akm-config.json +18 -5
- package/schemas/akm-task.json +27 -5
- package/schemas/akm-workflow.json +92 -13
package/dist/indexer/indexer.js
CHANGED
|
@@ -7,6 +7,8 @@ import { detectAdapterId } from "../core/adapter/detect-adapter.js";
|
|
|
7
7
|
import { adapterForId } from "../core/adapter/registry.js";
|
|
8
8
|
import { isHttpUrl, toErrorMessage } from "../core/common.js";
|
|
9
9
|
import { concurrentMap } from "../core/concurrent.js";
|
|
10
|
+
import { isLoopbackEndpoint } from "../core/loopback.js";
|
|
11
|
+
import { classifyPathAccess, describeInaccessiblePath } from "../core/path-access.js";
|
|
10
12
|
import { getDbPath } from "../core/paths.js";
|
|
11
13
|
import { SCRIPT_EXTENSIONS } from "../core/recognition-util.js";
|
|
12
14
|
import { withStateDb } from "../core/state-db.js";
|
|
@@ -17,7 +19,7 @@ import { closeDatabase, openExistingDatabase, openIndexDatabase } from "../stora
|
|
|
17
19
|
import { deleteEntriesByDirAndStash, deleteEntriesByDirExceptKeys, deleteEntriesByIds, deleteEntriesByStashDir, deleteUsageEventsByEntryIds, findEntryIdByRef, getAllEntries, getEmbeddableEntryCount, getEntryCount, getIndexedDirPathsByStashDir, getIndexedStashDirsByDir, relinkUsageEvents, upsertEntry, upsertWorkflowDocument, } from "../storage/repositories/index-entries-repository.js";
|
|
18
20
|
import { rebuildFts } from "../storage/repositories/index-fts-repository.js";
|
|
19
21
|
import { clearStaleCacheEntries } from "../storage/repositories/index-llm-cache-repository.js";
|
|
20
|
-
import { deleteIndexDirState, deleteIndexDirStatesByStashDir, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
22
|
+
import { deleteIndexDirState, deleteIndexDirStatesByStashDir, deleteMeta, getMeta, setMeta, upsertIndexDirState, } from "../storage/repositories/index-meta-repository.js";
|
|
21
23
|
import { upsertUtilityScore } from "../storage/repositories/index-utility-repository.js";
|
|
22
24
|
import { getAllEntriesForEmbedding, getEmbeddingCount, isVecAvailable, isVecFastPathReady, purgeEmbeddings, setVecFastPathReady, upsertEmbedding, warnIfVecMissing, } from "../storage/repositories/index-vec-repository.js";
|
|
23
25
|
import { takeWorkflowDocument } from "../workflows/runtime/document-cache.js";
|
|
@@ -39,23 +41,15 @@ function throwIfAborted(signal) {
|
|
|
39
41
|
export function getDefaultLlmConcurrency(llmConfig) {
|
|
40
42
|
if (typeof llmConfig?.concurrency === "number")
|
|
41
43
|
return llmConfig.concurrency;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// comparison actually matches.
|
|
48
|
-
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
49
|
-
if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost"))
|
|
50
|
-
return 1;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
44
|
+
// Local model servers stay at 1 (single loaded model; parallel requests
|
|
45
|
+
// trigger reload thrash); an absent or unparseable endpoint fails safe as
|
|
46
|
+
// local. ONE classifier decides what "local" means (`core/loopback.ts`,
|
|
47
|
+
// shared with the workflow engine's frozen concurrency default).
|
|
48
|
+
if (isLoopbackEndpoint(llmConfig?.endpoint))
|
|
53
49
|
return 1;
|
|
54
|
-
}
|
|
55
50
|
// Remote endpoints default to a modest 2-wide pool (owner ruling 2026-07-21):
|
|
56
51
|
// enough to overlap request latency without hammering rate-limited APIs.
|
|
57
|
-
//
|
|
58
|
-
// trigger reload thrash). The explicit-override branch above only fires for
|
|
52
|
+
// The explicit-override branch above only fires for
|
|
59
53
|
// callers that put `concurrency` on the connection themselves —
|
|
60
54
|
// `engines.<name>.concurrency` is a valid schema field but `resolveLlmEngineUse`
|
|
61
55
|
// does NOT copy it into the resolved connection, so on the enrichment path the
|
|
@@ -164,7 +158,12 @@ async function runWalkPhase(ctx) {
|
|
|
164
158
|
async function runEmbeddingPhase(ctx) {
|
|
165
159
|
const { db, config, signal, onProgress } = ctx;
|
|
166
160
|
throwIfAborted(signal);
|
|
167
|
-
|
|
161
|
+
// Forward the signal. Without it generateEmbeddingsForDb's abort machinery was
|
|
162
|
+
// inert — its throwIfAborted checks and the signal it threads into embedBatch
|
|
163
|
+
// (which RemoteEmbedder passes to every fetch and LocalEmbedder honours between
|
|
164
|
+
// chunks) never saw a controller. Ctrl-C and the improve budget abort could not
|
|
165
|
+
// stop the embedding phase, the longest phase of an index run.
|
|
166
|
+
ctx.embeddingResult = await generateEmbeddingsForDb(db, config, onProgress, signal);
|
|
168
167
|
ctx.timing.tEmbedEnd = Date.now();
|
|
169
168
|
}
|
|
170
169
|
/**
|
|
@@ -290,12 +289,33 @@ export function reconcileBodyOpeningIndexState(db, flagEnabled, isFullWalk) {
|
|
|
290
289
|
*
|
|
291
290
|
* Only rows with a non-empty `file_path` are checked — remote/virtual entries
|
|
292
291
|
* that have no local path are always skipped.
|
|
292
|
+
*
|
|
293
|
+
* "No longer exists" means ABSENT, never merely unreadable (#791). This pass
|
|
294
|
+
* DELETES rows, and `fs.existsSync` reported `false` for a file akm lacked
|
|
295
|
+
* permission to look at exactly as for one that had been removed — so a
|
|
296
|
+
* bundle temporarily mounted read-restricted (a uid mismatch, a tightened
|
|
297
|
+
* parent directory) had its whole index wiped, and the run reported the
|
|
298
|
+
* deletions as a clean success. Unreadable files keep their rows and are
|
|
299
|
+
* reported instead.
|
|
293
300
|
*/
|
|
294
301
|
function runCleanPass(db, dryRun) {
|
|
295
302
|
const allEntries = db.prepare("SELECT id, entry_key AS ref, file_path AS path FROM entries").all();
|
|
296
303
|
// Only check entries that have a non-empty local path (skip remote/virtual).
|
|
297
304
|
const localEntries = allEntries.filter((e) => typeof e.path === "string" && e.path.trim() !== "");
|
|
298
|
-
const missing =
|
|
305
|
+
const missing = [];
|
|
306
|
+
const unreadable = [];
|
|
307
|
+
for (const entry of localEntries) {
|
|
308
|
+
const { access, code } = classifyPathAccess(entry.path);
|
|
309
|
+
if (access === "absent")
|
|
310
|
+
missing.push(entry);
|
|
311
|
+
else if (access === "inaccessible")
|
|
312
|
+
unreadable.push({ path: entry.path, ...(code ? { code } : {}) });
|
|
313
|
+
}
|
|
314
|
+
if (unreadable.length > 0) {
|
|
315
|
+
const shown = unreadable.slice(0, 5).map((u) => describeInaccessiblePath(u.path, u.code));
|
|
316
|
+
warn(`Index clean pass kept ${unreadable.length} entr${unreadable.length === 1 ? "y" : "ies"} whose file akm cannot ` +
|
|
317
|
+
`read (unreadable is not deleted): ${shown.join("; ")}${unreadable.length > shown.length ? "; …" : ""}`);
|
|
318
|
+
}
|
|
299
319
|
if (!dryRun && missing.length > 0) {
|
|
300
320
|
deleteEntriesByIds(db, missing.map((e) => e.id));
|
|
301
321
|
}
|
|
@@ -319,6 +339,24 @@ export async function akmIndex(options) {
|
|
|
319
339
|
return akmIndexOverride(options);
|
|
320
340
|
return akmIndexReal(options);
|
|
321
341
|
}
|
|
342
|
+
let indexTransactionHookForTests;
|
|
343
|
+
/**
|
|
344
|
+
* TEST-ONLY. Observe the in-flight reindex transaction; `undefined` restores.
|
|
345
|
+
*
|
|
346
|
+
* Exists because the delete-then-reinsert atomicity guarantee is, by
|
|
347
|
+
* construction, invisible from outside the transaction: by the time
|
|
348
|
+
* `akmIndex()` resolves, the commit has already collapsed both generations
|
|
349
|
+
* into one observable state. Concurrency tests install a hook that opens a
|
|
350
|
+
* SECOND connection at these points and asserts it still sees the previous
|
|
351
|
+
* complete generation. Inert in production (one `undefined?.()` per reindex).
|
|
352
|
+
*/
|
|
353
|
+
export function _setIndexTransactionHookForTests(hook) {
|
|
354
|
+
indexTransactionHookForTests = hook;
|
|
355
|
+
}
|
|
356
|
+
/** Fire a named in-transaction observation point (no-op outside tests). */
|
|
357
|
+
function indexTransactionHook(point) {
|
|
358
|
+
indexTransactionHookForTests?.(point);
|
|
359
|
+
}
|
|
322
360
|
/**
|
|
323
361
|
* Detect an adapter for every resolvable source that does not declare one, and
|
|
324
362
|
* persist each detection into `config.json`.
|
|
@@ -976,6 +1014,10 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
976
1014
|
// (cross-DB) nulls entry_ids that no longer resolve to a rebuilt entry and
|
|
977
1015
|
// re-resolves the rest by entry_ref — subsuming the old detach.
|
|
978
1016
|
db.exec("DELETE FROM entries");
|
|
1017
|
+
// Atomicity observation point: inside the transaction the tables are now
|
|
1018
|
+
// empty, but no other connection may observe that. See
|
|
1019
|
+
// tests/integration/indexer/reindex-generation-atomicity.test.ts.
|
|
1020
|
+
indexTransactionHook("full-delete-applied");
|
|
979
1021
|
}
|
|
980
1022
|
for (const { dirPath, currentStashDir, files, stash, skip, reason, hashByFile, conceptIdByFile, indexVariant, remove, pruneMissing, } of dirRecords) {
|
|
981
1023
|
if (remove) {
|
|
@@ -1089,6 +1131,9 @@ function persistDirRecords(db, dirRecords, doFullDelete, warnings, sourceRoots,
|
|
|
1089
1131
|
}
|
|
1090
1132
|
}
|
|
1091
1133
|
}
|
|
1134
|
+
// Atomicity observation point: the new generation is fully written but
|
|
1135
|
+
// uncommitted, so it must still be invisible to other connections.
|
|
1136
|
+
indexTransactionHook("records-persisted");
|
|
1092
1137
|
});
|
|
1093
1138
|
insertTransaction();
|
|
1094
1139
|
deleteUsageEventsByEntryIds([...deletedUsageEntryIds]);
|
|
@@ -1317,9 +1362,20 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
|
1317
1362
|
const currentFingerprint = deriveSemanticProviderFingerprint(config.embedding);
|
|
1318
1363
|
const storedFingerprint = getMeta(db, "embeddingFingerprint");
|
|
1319
1364
|
if (storedFingerprint && storedFingerprint !== currentFingerprint) {
|
|
1320
|
-
// Model/provider changed → stored vectors are incompatible. Clear them
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1365
|
+
// Model/provider changed → stored vectors are incompatible. Clear them;
|
|
1366
|
+
// re-embedded by this index run.
|
|
1367
|
+
//
|
|
1368
|
+
// The vec table goes too. "Same dimension, so keep the vec table" only held
|
|
1369
|
+
// for a same-width model swap: entries_vec is a vec0 virtual table declared
|
|
1370
|
+
// at a FIXED width, so after a dimension-changing model change every insert
|
|
1371
|
+
// failed against the old width, and ensureSchema's dim-change rebuild never
|
|
1372
|
+
// fired because it only runs for callers that pass an explicit
|
|
1373
|
+
// embeddingDim. The stale table survived `--full` — the exact remedy the
|
|
1374
|
+
// warning recommended. Clearing the stored dim lets the next ensureSchema
|
|
1375
|
+
// materialize it at the new width; until then the fast-path flag reads
|
|
1376
|
+
// false (no table) and search uses the complete BLOB table.
|
|
1377
|
+
purgeEmbeddings(db, { dropVecTable: true });
|
|
1378
|
+
deleteMeta(db, "embeddingDim");
|
|
1323
1379
|
}
|
|
1324
1380
|
try {
|
|
1325
1381
|
const { embedBatch } = await import("../llm/embedder.js");
|
|
@@ -1364,6 +1420,7 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
|
1364
1420
|
let storedCount = 0;
|
|
1365
1421
|
let skippedCount = 0;
|
|
1366
1422
|
let vecFailedCount = 0;
|
|
1423
|
+
let vecUnavailableCount = 0;
|
|
1367
1424
|
db.transaction(() => {
|
|
1368
1425
|
for (let i = 0; i < allEntries.length; i++) {
|
|
1369
1426
|
const res = upsertEmbedding(db, allEntries[i].id, embeddings[i]);
|
|
@@ -1375,6 +1432,8 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
|
1375
1432
|
}
|
|
1376
1433
|
if (res.vec === "failed")
|
|
1377
1434
|
vecFailedCount++;
|
|
1435
|
+
if (res.vec === "unavailable")
|
|
1436
|
+
vecUnavailableCount++;
|
|
1378
1437
|
}
|
|
1379
1438
|
})();
|
|
1380
1439
|
if (skippedCount > 0) {
|
|
@@ -1384,7 +1443,13 @@ async function generateEmbeddingsForDb(db, config, onProgress, signal) {
|
|
|
1384
1443
|
// instead of inferring readiness from stored-BLOB counts. Any failure
|
|
1385
1444
|
// marks the fast path degraded, routing search to the JS-cosine fallback
|
|
1386
1445
|
// over the (complete) BLOB table — honest degradation, not a hard failure.
|
|
1387
|
-
|
|
1446
|
+
//
|
|
1447
|
+
// 'unavailable' has to degrade the flag too. It means no vec row was
|
|
1448
|
+
// written at all, so marking the fast path ready left a later open (a
|
|
1449
|
+
// different runtime, or sqlite-vec installed afterwards) trusting an
|
|
1450
|
+
// empty entries_vec and returning zero semantic hits against a fully
|
|
1451
|
+
// populated BLOB table.
|
|
1452
|
+
setVecFastPathReady(db, vecFailedCount === 0 && vecUnavailableCount === 0);
|
|
1388
1453
|
if (vecFailedCount > 0) {
|
|
1389
1454
|
warn(`[embed] ${vecFailedCount} sqlite-vec fast-path insert${vecFailedCount === 1 ? "" : "s"} failed — ` +
|
|
1390
1455
|
"semantic search will use the slower JS-cosine fallback over stored embeddings. " +
|
|
@@ -43,6 +43,7 @@ import { conceptIdFromTypeName, parseRefInput } from "../../core/asset/resolve-r
|
|
|
43
43
|
import { todayIso } from "../../core/common.js";
|
|
44
44
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
45
45
|
import { warn } from "../../core/warn.js";
|
|
46
|
+
import { recordWrittenPath } from "../../core/write-provenance.js";
|
|
46
47
|
import { writeAssetToSource } from "../../core/write-source.js";
|
|
47
48
|
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
48
49
|
import { resolveIndexPassLLM } from "../../llm/index-passes.js";
|
|
@@ -449,6 +450,9 @@ function markParentProcessed(parent) {
|
|
|
449
450
|
const next = assembleAsset(updatedFm, block.content);
|
|
450
451
|
try {
|
|
451
452
|
fs.writeFileSync(parent.filePath, next, "utf8");
|
|
453
|
+
// #652: the parent's `inference_processed` stamp is a real asset mutation
|
|
454
|
+
// (the documented writeAssetToSource exception above) — journal it.
|
|
455
|
+
recordWrittenPath(parent.filePath);
|
|
452
456
|
}
|
|
453
457
|
catch (err) {
|
|
454
458
|
warn(`memory inference: failed to mark parent processed ${parent.filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Database-backed (SQLite + FTS5/vector) source search implementation.
|
|
6
|
-
*
|
|
7
|
-
* Extracted from source-search.ts to break the circular import:
|
|
8
|
-
* source-search.ts → sources/providers/filesystem.ts → db-search.ts (no cycle)
|
|
9
|
-
*
|
|
10
|
-
* source-search.ts imports this module for the `searchLocal` export.
|
|
11
|
-
* sources/providers/filesystem.ts also imports `searchLocal` from here.
|
|
12
|
-
*
|
|
13
|
-
* Renamed from `local-search.ts` to signal that this is the DB-layer search
|
|
14
|
-
* implementation, not a "local vs. remote" distinction.
|
|
15
|
-
*/
|
|
16
|
-
import fs from "node:fs";
|
|
17
4
|
import path from "node:path";
|
|
18
5
|
import { buildActionFromContributors, defaultActionContributors } from "../../core/action-contributors.js";
|
|
19
6
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
20
7
|
import { displayRef } from "../../core/asset/resolve-ref.js";
|
|
8
|
+
import { classifyPathAccess } from "../../core/path-access.js";
|
|
21
9
|
import { getDbPath } from "../../core/paths.js";
|
|
22
10
|
import { defaultRendererRegistry } from "../../core/type-presentation.js";
|
|
23
11
|
import { warn } from "../../core/warn.js";
|
|
24
|
-
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
12
|
+
import { assertIndexPathReadable, closeDatabase, openExistingDatabase, } from "../../storage/repositories/index-connection.js";
|
|
25
13
|
import { getAllEntries, getBaseBeliefStatesForDerivedTwins, getEntryById, getEntryCount, getPositiveFeedbackCountsByIds, } from "../../storage/repositories/index-entries-repository.js";
|
|
26
14
|
import { searchFts } from "../../storage/repositories/index-fts-repository.js";
|
|
27
15
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
@@ -98,6 +86,21 @@ export function shouldQueryPositiveFeedbackCounts(utilityDecayRaw) {
|
|
|
98
86
|
return boost > 1.0;
|
|
99
87
|
}
|
|
100
88
|
// ── Main search entrypoint ───────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Whether an embedding provider is actually configured.
|
|
91
|
+
*
|
|
92
|
+
* A remote provider needs BOTH endpoint and model. A LOCAL provider needs
|
|
93
|
+
* neither — `embedding.localModel` selects a transformers model that runs in
|
|
94
|
+
* process. Checking only the remote pair told every local-provider user that
|
|
95
|
+
* "no embedding provider is configured" and pointed them at
|
|
96
|
+
* `akm config set embedding '{"endpoint":...}'`, which is the wrong remedy and
|
|
97
|
+
* hid the real diagnostic recorded in the semantic status.
|
|
98
|
+
*/
|
|
99
|
+
function hasConfiguredEmbeddingProvider(config) {
|
|
100
|
+
if (config.embedding?.localModel)
|
|
101
|
+
return true;
|
|
102
|
+
return Boolean(config.embedding?.endpoint && config.embedding?.model);
|
|
103
|
+
}
|
|
101
104
|
export async function searchLocal(input) {
|
|
102
105
|
const { query, searchType, limit, stashDir, sources, config } = input;
|
|
103
106
|
const filters = input.filters;
|
|
@@ -117,7 +120,7 @@ export async function searchLocal(input) {
|
|
|
117
120
|
if (rawStatus && rawStatus.providerFingerprint !== currentFingerprint) {
|
|
118
121
|
warnings.push("Embedding config changed. Run 'akm index --full' to rebuild the semantic index with the new provider.");
|
|
119
122
|
}
|
|
120
|
-
else if (!config
|
|
123
|
+
else if (!hasConfiguredEmbeddingProvider(config)) {
|
|
121
124
|
// #480: when semantic mode is `auto` but no embedding provider is
|
|
122
125
|
// configured (e.g. `akm setup --yes` ran without picking one), telling
|
|
123
126
|
// the user to "run akm setup" is misleading — they just did. Surface
|
|
@@ -132,7 +135,7 @@ export async function searchLocal(input) {
|
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
if (config.semanticSearchMode === "auto" && semanticStatus === "blocked") {
|
|
135
|
-
if (!config
|
|
138
|
+
if (!hasConfiguredEmbeddingProvider(config)) {
|
|
136
139
|
// F7/A2: same predicate as the `pending` branch above (#480) — a
|
|
137
140
|
// `blocked` status can outlive the provider config that produced it
|
|
138
141
|
// (e.g. the embedding config was later unset). This is not a fault;
|
|
@@ -158,7 +161,12 @@ export async function searchLocal(input) {
|
|
|
158
161
|
// reads serve the existing index as-is.
|
|
159
162
|
await ensureIndex(stashDir);
|
|
160
163
|
const dbPath = getDbPath();
|
|
161
|
-
|
|
164
|
+
// An index we cannot READ is not an index that does not exist (#791). Saying
|
|
165
|
+
// "No search index available" for a populated index the caller merely lacks
|
|
166
|
+
// permission on is a lie at exit 0 — and an agent consuming this JSON has no
|
|
167
|
+
// way to tell it from a genuine empty result, so it relays the lie onward.
|
|
168
|
+
assertIndexPathReadable(dbPath);
|
|
169
|
+
if (classifyPathAccess(dbPath).access === "absent") {
|
|
162
170
|
return {
|
|
163
171
|
hits: [],
|
|
164
172
|
tip: "No search index available. Run 'akm index' to build one.",
|
|
@@ -123,7 +123,12 @@ function walkStashGit(stashRoot, options) {
|
|
|
123
123
|
for (const relFile of files) {
|
|
124
124
|
const absPath = path.join(stashRoot, relFile);
|
|
125
125
|
try {
|
|
126
|
-
|
|
126
|
+
// lstat, not stat: a tracked symlink must not be dereferenced. statSync
|
|
127
|
+
// follows the link, so a target outside stashRoot would be read and
|
|
128
|
+
// indexed. The manual walk below already skips symlinks for exactly this
|
|
129
|
+
// reason; the two walkers have to agree regardless of whether the stash
|
|
130
|
+
// happens to sit inside a git repo.
|
|
131
|
+
if (fs.lstatSync(absPath).isFile()) {
|
|
127
132
|
results.push(buildFileContext(stashRoot, absPath));
|
|
128
133
|
}
|
|
129
134
|
}
|
|
@@ -74,6 +74,16 @@ const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
|
|
|
74
74
|
* on win32 or when the env supplies PATHEXT (which is also the seam tests use
|
|
75
75
|
* to cover Windows resolution from a POSIX runner).
|
|
76
76
|
*/
|
|
77
|
+
/**
|
|
78
|
+
* Script shims that `child_process.spawn` cannot execute directly on Windows.
|
|
79
|
+
*
|
|
80
|
+
* Node refuses to spawn `.bat`/`.cmd` without a shell (the CVE-2024-27980
|
|
81
|
+
* hardening), so a detection that returns one of these reports an agent as
|
|
82
|
+
* available that dispatch then fails to launch. They stay in the candidate
|
|
83
|
+
* list — a shim is better than reporting the CLI as missing — but they sort
|
|
84
|
+
* last, so a real `.exe` next to `claude.cmd` wins.
|
|
85
|
+
*/
|
|
86
|
+
const NON_SPAWNABLE_SUFFIXES = new Set([".cmd", ".bat"]);
|
|
77
87
|
function executableSuffixes(envSource) {
|
|
78
88
|
const pathext = envSource.PATHEXT ?? envSource.Pathext ?? envSource.pathext;
|
|
79
89
|
if (process.platform !== "win32" && !pathext)
|
|
@@ -83,7 +93,9 @@ function executableSuffixes(envSource) {
|
|
|
83
93
|
.map((ext) => ext.trim())
|
|
84
94
|
.filter(Boolean)
|
|
85
95
|
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`));
|
|
86
|
-
|
|
96
|
+
const directlySpawnable = exts.filter((ext) => !NON_SPAWNABLE_SUFFIXES.has(ext.toLowerCase()));
|
|
97
|
+
const shims = exts.filter((ext) => NON_SPAWNABLE_SUFFIXES.has(ext.toLowerCase()));
|
|
98
|
+
return ["", ...directlySpawnable, ...shims];
|
|
87
99
|
}
|
|
88
100
|
let detectOverrides;
|
|
89
101
|
/** TEST-ONLY. Swap the detection implementations; pass undefined to restore. */
|
|
@@ -43,6 +43,29 @@ function resolveCredential(name, engine, config) {
|
|
|
43
43
|
? { names: [specific, "AKM_LLM_API_KEY"], required: false }
|
|
44
44
|
: { names: [specific], required: false };
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Read a credential descriptor's value out of `process.env`: the FIRST
|
|
48
|
+
* non-empty trimmed value across `names`, in declared order. A `required`
|
|
49
|
+
* descriptor that resolves to nothing is a config error naming its PRIMARY
|
|
50
|
+
* variable — the one an operator is told to set.
|
|
51
|
+
*
|
|
52
|
+
* The ONE env-credential seam. The live-config dispatch boundary
|
|
53
|
+
* ({@link materializeLlmConnection}) and the FROZEN workflow dispatch boundary
|
|
54
|
+
* (`materializeFrozenLlm` in `workflows/exec/unit-dispatch.ts`, whose frozen
|
|
55
|
+
* snapshots carry a structurally identical descriptor) both read through it, so
|
|
56
|
+
* lookup order and the failure message cannot drift between them.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCredentialFromEnv(credential) {
|
|
59
|
+
for (const name of credential?.names ?? []) {
|
|
60
|
+
const candidate = process.env[name]?.trim();
|
|
61
|
+
if (candidate)
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
if (credential?.required) {
|
|
65
|
+
throw new ConfigError(`Required engine credential ${credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
46
69
|
/** Collect materialized engine credentials for output and persistence redaction. */
|
|
47
70
|
export function collectEngineCredentialValues(config, envSource = process.env) {
|
|
48
71
|
const values = new Set();
|
|
@@ -112,17 +135,7 @@ export function materializeLlmConnection(resolved) {
|
|
|
112
135
|
throw new ConfigError(formatExtraParamsIssue(`Engine "${resolved.engine}" extraParams`, issue), "INVALID_CONFIG_FILE");
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
for (const name of resolved.credential?.names ?? []) {
|
|
117
|
-
const candidate = process.env[name]?.trim();
|
|
118
|
-
if (candidate) {
|
|
119
|
-
apiKey = candidate;
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (resolved.credential?.required && !apiKey) {
|
|
124
|
-
throw new ConfigError(`Required engine credential ${resolved.credential.names[0]} is not set.`, "INVALID_CONFIG_FILE");
|
|
125
|
-
}
|
|
138
|
+
const apiKey = resolveCredentialFromEnv(resolved.credential);
|
|
126
139
|
return {
|
|
127
140
|
...resolved.connection,
|
|
128
141
|
...(apiKey ? { apiKey } : {}),
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
const BUILTIN_ALIASES = [
|
|
14
14
|
{
|
|
15
15
|
// Anthropic's Mythos-class tier above Opus — the recommended resolution
|
|
16
|
-
// target for the `deep` workflow tier (see docs/reference/
|
|
16
|
+
// target for the `deep` workflow tier (see docs/reference/workflow-schema.md).
|
|
17
17
|
alias: "fable",
|
|
18
18
|
platforms: {
|
|
19
19
|
claude: "claude-fable-5",
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Built-in profile registry for external agent CLIs (v1 spec §12.1).
|
|
6
|
+
*
|
|
7
|
+
* A `AgentProfile` is the minimum metadata required to shell-out to a
|
|
8
|
+
* coding-agent CLI. Named engines lower canonical harness metadata into this
|
|
9
|
+
* intentionally small internal shape. The wrapper is in `./spawn.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import { COMMON_SPAWN_ENV_PASSTHROUGH } from "../../core/spawn-env.js";
|
|
4
12
|
// AKM_EVENT_SOURCE carries usage-event provenance (improve/task) so that akm
|
|
5
13
|
// invocations a spawned agent makes are recorded as machine traffic, not user
|
|
6
14
|
// demand (DRIFT-6). Without it in the passthrough whitelist, buildChildEnv drops
|
|
7
15
|
// the stamp at the agent boundary — e.g. `akm wiki ingest` spawns an agent whose
|
|
8
16
|
// `akm curate/show/search` tool-calls then log source='user', silently inflating
|
|
9
17
|
// every lane's read-back (GRR). It is a provenance tag, never a secret.
|
|
10
|
-
const COMMON_PASSTHROUGH =
|
|
18
|
+
const COMMON_PASSTHROUGH = COMMON_SPAWN_ENV_PASSTHROUGH;
|
|
11
19
|
/**
|
|
12
20
|
* Built-in profiles for the agent CLIs akm knows out of the box: the five the
|
|
13
21
|
* v1 spec calls out explicitly, plus the P2 harness adapters (copilot, pi,
|
|
@@ -14,90 +14,21 @@
|
|
|
14
14
|
* NEVER imports an LLM SDK. Agents are reachable only via shell-out;
|
|
15
15
|
* this is a pre-emptive guarantee against the #222 invariant.
|
|
16
16
|
*/
|
|
17
|
-
import fs from "node:fs";
|
|
18
|
-
import os from "node:os";
|
|
19
|
-
import path from "node:path";
|
|
20
17
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
21
|
-
import {
|
|
18
|
+
import { collectAllowlistedEnv } from "../../core/spawn-env.js";
|
|
19
|
+
import { runManagedSubprocess, streamCaptureFailure, } from "../../core/subprocess.js";
|
|
22
20
|
import { getCommandBuilder } from "./builders.js";
|
|
23
21
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config.js";
|
|
24
|
-
/**
|
|
25
|
-
* Supplement `existingPath` with well-known user binary directories when
|
|
26
|
-
* running in a scheduler context (cron/launchd) where PATH is stripped.
|
|
27
|
-
*
|
|
28
|
-
* Detection heuristic: if the current PATH does not contain the user's home
|
|
29
|
-
* directory, we are likely in a stripped scheduler env. In an interactive
|
|
30
|
-
* shell the user's home almost always appears (e.g. ~/.bun/bin, ~/.cargo/bin).
|
|
31
|
-
*
|
|
32
|
-
* Only directories that actually exist on disk are prepended, and only if
|
|
33
|
-
* they are not already present, so interactive-shell PATH ordering is never
|
|
34
|
-
* disturbed.
|
|
35
|
-
*/
|
|
36
|
-
export function supplementPathForSchedulerContext(existingPath) {
|
|
37
|
-
const home = os.homedir();
|
|
38
|
-
// If PATH already contains the home directory, we are in an interactive
|
|
39
|
-
// shell — skip supplementation entirely.
|
|
40
|
-
if (existingPath.split(path.delimiter).some((d) => d.startsWith(home))) {
|
|
41
|
-
return existingPath;
|
|
42
|
-
}
|
|
43
|
-
const candidates = pathCandidatesForCurrentPlatform(home);
|
|
44
|
-
const existing = new Set(existingPath.split(path.delimiter).filter(Boolean));
|
|
45
|
-
const toAdd = candidates.filter((d) => !existing.has(d) && fs.existsSync(d));
|
|
46
|
-
if (toAdd.length === 0)
|
|
47
|
-
return existingPath;
|
|
48
|
-
return [...toAdd, existingPath].filter(Boolean).join(path.delimiter);
|
|
49
|
-
}
|
|
50
|
-
function pathCandidatesForCurrentPlatform(home) {
|
|
51
|
-
if (process.platform === "win32") {
|
|
52
|
-
// Windows: Bun + Cargo + Scoop + Chocolatey + system tools. Order favors
|
|
53
|
-
// user-local installs over machine-global so the user's chosen toolchain
|
|
54
|
-
// wins. These paths are commonly stripped from Task Scheduler / service
|
|
55
|
-
// environments, mirroring the cron/launchd problem on POSIX.
|
|
56
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local");
|
|
57
|
-
const userProfile = process.env.USERPROFILE ?? home;
|
|
58
|
-
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
|
|
59
|
-
return [
|
|
60
|
-
path.join(userProfile, ".bun", "bin"),
|
|
61
|
-
path.join(localAppData, "Programs", "bun"),
|
|
62
|
-
path.join(userProfile, ".cargo", "bin"),
|
|
63
|
-
path.join(localAppData, "Programs", "Git", "cmd"),
|
|
64
|
-
path.join(userProfile, "scoop", "shims"),
|
|
65
|
-
path.join(programFiles, "Git", "cmd"),
|
|
66
|
-
"C:\\ProgramData\\chocolatey\\bin",
|
|
67
|
-
];
|
|
68
|
-
}
|
|
69
|
-
return [
|
|
70
|
-
path.join(home, ".bun", "bin"),
|
|
71
|
-
path.join(home, ".cargo", "bin"),
|
|
72
|
-
path.join(home, ".local", "bin"),
|
|
73
|
-
"/opt/homebrew/bin",
|
|
74
|
-
"/opt/homebrew/sbin",
|
|
75
|
-
"/usr/local/bin",
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
22
|
/**
|
|
79
23
|
* Build the child env. Starts empty and copies through:
|
|
80
|
-
* • Every name in `profile.envPassthrough
|
|
24
|
+
* • Every name in `profile.envPassthrough` (via the shared
|
|
25
|
+
* {@link collectAllowlistedEnv}, which also supplements PATH for
|
|
26
|
+
* scheduler contexts where the inherited PATH is stripped).
|
|
81
27
|
* • Every entry in `profile.env`.
|
|
82
28
|
* • Every entry in `options.env` (highest precedence).
|
|
83
|
-
*
|
|
84
|
-
* PATH is supplemented with well-known user binary directories when running
|
|
85
|
-
* in a scheduler context (cron/launchd) where the inherited PATH is stripped.
|
|
86
|
-
* See {@link supplementPathForSchedulerContext}.
|
|
87
29
|
*/
|
|
88
30
|
function buildChildEnv(profile, options) {
|
|
89
|
-
const
|
|
90
|
-
const env = {};
|
|
91
|
-
for (const name of profile.envPassthrough) {
|
|
92
|
-
const value = source[name];
|
|
93
|
-
if (value !== undefined)
|
|
94
|
-
env[name] = value;
|
|
95
|
-
}
|
|
96
|
-
// Supplement PATH after passthrough so the scheduler-context fix applies to
|
|
97
|
-
// the value actually coming from the environment source.
|
|
98
|
-
if (env.PATH !== undefined) {
|
|
99
|
-
env.PATH = supplementPathForSchedulerContext(env.PATH);
|
|
100
|
-
}
|
|
31
|
+
const env = collectAllowlistedEnv(profile.envPassthrough, options.envSource ?? process.env);
|
|
101
32
|
if (profile.env) {
|
|
102
33
|
for (const [k, v] of Object.entries(profile.env))
|
|
103
34
|
env[k] = v;
|
|
@@ -108,19 +39,16 @@ function buildChildEnv(profile, options) {
|
|
|
108
39
|
}
|
|
109
40
|
return env;
|
|
110
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* This path's phrasing of the SHARED incomplete-capture verdict
|
|
44
|
+
* ({@link streamCaptureFailure} in `core/subprocess.ts`). The classification
|
|
45
|
+
* lives in the primitive so the agent path and the workflow `exec` path cannot
|
|
46
|
+
* drift apart on what "the capture did not complete" means; only the sentence
|
|
47
|
+
* naming the profile is local. Message text is unchanged from the inlined copy.
|
|
48
|
+
*/
|
|
111
49
|
function streamFailureMessage(profileName, stdout, stderr) {
|
|
112
|
-
const failures =
|
|
113
|
-
|
|
114
|
-
failures.push(`stdout read failed: ${stdout.error instanceof Error ? stdout.error.message : String(stdout.error)}`);
|
|
115
|
-
if (stderr.error)
|
|
116
|
-
failures.push(`stderr read failed: ${stderr.error instanceof Error ? stderr.error.message : String(stderr.error)}`);
|
|
117
|
-
if (stdout.timedOut)
|
|
118
|
-
failures.push("stdout drain timed out");
|
|
119
|
-
if (stderr.timedOut)
|
|
120
|
-
failures.push("stderr drain timed out");
|
|
121
|
-
if (failures.length === 0)
|
|
122
|
-
return undefined;
|
|
123
|
-
return `agent CLI "${profileName}" output capture failed: ${failures.join("; ")}`;
|
|
50
|
+
const failures = streamCaptureFailure(stdout, stderr);
|
|
51
|
+
return failures === undefined ? undefined : `agent CLI "${profileName}" output capture failed: ${failures}`;
|
|
124
52
|
}
|
|
125
53
|
/**
|
|
126
54
|
* Spawn the agent CLI described by `profile` with `prompt` (forwarded as
|
|
@@ -118,6 +118,12 @@ let _testServer = null;
|
|
|
118
118
|
// Test seam replacing the real `createOpencode` import (see __setServerFactory).
|
|
119
119
|
let _serverFactory = null;
|
|
120
120
|
let _exitHookInstalled = false;
|
|
121
|
+
/**
|
|
122
|
+
* True once `process.on("exit")` has fired. Close paths consult it because an
|
|
123
|
+
* exit handler runs after the event loop has stopped: no timer scheduled there
|
|
124
|
+
* will ever fire, so any cleanup that needs to happen must happen inline.
|
|
125
|
+
*/
|
|
126
|
+
let _processExiting = false;
|
|
121
127
|
/**
|
|
122
128
|
* Test-only seam: inject a fake {@link SdkServer} so `runOpencodeSdk` can be
|
|
123
129
|
* exercised without the real `@opencode-ai/sdk` (which would spin up a server).
|
|
@@ -424,6 +430,20 @@ async function createManagedOpencode(options) {
|
|
|
424
430
|
}
|
|
425
431
|
if (childExited())
|
|
426
432
|
return;
|
|
433
|
+
if (_processExiting) {
|
|
434
|
+
// During `process.on("exit")` the event loop is finished, so a timer
|
|
435
|
+
// scheduled here would never fire and the SIGKILL escalation could not
|
|
436
|
+
// happen at all — a server child that ignores SIGTERM outlived akm.
|
|
437
|
+
// Exit handlers must be synchronous, so escalate immediately instead of
|
|
438
|
+
// waiting out a grace period we have no way to wait for.
|
|
439
|
+
try {
|
|
440
|
+
proc.kill("SIGKILL");
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
/* already dead */
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
427
447
|
closeEscalation = setTimeout(() => {
|
|
428
448
|
closeEscalation = undefined;
|
|
429
449
|
if (childExited())
|
|
@@ -525,6 +545,7 @@ async function startServer(profile, sdkConfig, env, registryKey, startupSignal)
|
|
|
525
545
|
if (!_exitHookInstalled) {
|
|
526
546
|
_exitHookInstalled = true;
|
|
527
547
|
process.once("exit", () => {
|
|
548
|
+
_processExiting = true;
|
|
528
549
|
void closeServer();
|
|
529
550
|
});
|
|
530
551
|
}
|