akm-cli 0.9.5 → 0.9.7
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 +295 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/memory/memory-improve.js +2 -1
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +21 -10
- package/dist/commands/lint/base-linter.js +141 -18
- package/dist/commands/lint/index.js +21 -10
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/read/curate.js +47 -0
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks.js +12 -16
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/asset/asset-placement.js +13 -2
- package/dist/core/asset/frontmatter.js +116 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +73 -133
- package/dist/scripts/akm-migrate.js +73 -133
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +5 -10
- package/dist/tasks/source/task-to-v4.js +1 -4
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +5 -1
- package/docs/reference/configuration.md +1 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/indexer/search/semantic-status.js +0 -142
package/dist/setup/setup.js
CHANGED
|
@@ -29,11 +29,10 @@ import { ConfigError, UsageError } from "../core/errors.js";
|
|
|
29
29
|
import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
|
|
30
30
|
import { warn } from "../core/warn.js";
|
|
31
31
|
import { akmIndex } from "../indexer/indexer.js";
|
|
32
|
-
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/search/semantic-status.js";
|
|
33
32
|
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
|
|
34
33
|
import { defaultProfileName } from "../integrations/harnesses/index.js";
|
|
35
34
|
import { readLockfile } from "../integrations/lockfile.js";
|
|
36
|
-
import {
|
|
35
|
+
import { probeLlmReachable } from "../llm/client.js";
|
|
37
36
|
import { detectEnvironment, detectLMStudio, renderDetectionSummary, } from "./detect.js";
|
|
38
37
|
import { upsertDetectedAgentEngine, upsertDetectedLlmEngine, verifyOpenAiCompatibleEndpoint } from "./detected-engines.js";
|
|
39
38
|
import { readCurrentLlmEngine, writeAgentEngines, writeLlmEngine } from "./engine-config.js";
|
|
@@ -592,38 +591,14 @@ export async function runSetupWizard(opts) {
|
|
|
592
591
|
// Non-interactive setup paths never reach this interactive-only step.
|
|
593
592
|
p.log.step("Scheduled Tasks");
|
|
594
593
|
await stepScheduledTasks();
|
|
595
|
-
if (semanticSearchMode.mode === "off") {
|
|
596
|
-
clearSemanticStatus();
|
|
597
|
-
}
|
|
598
594
|
if (semanticSearchMode.mode === "auto") {
|
|
599
595
|
if (semanticSearchMode.prepareAssets) {
|
|
600
596
|
const ready = await prepareSemanticSearchAssets(savedConfig);
|
|
601
597
|
if (!ready.ok) {
|
|
602
|
-
writeSemanticStatus({
|
|
603
|
-
status: "blocked",
|
|
604
|
-
reason: ready.reason,
|
|
605
|
-
message: ready.message,
|
|
606
|
-
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
607
|
-
lastCheckedAt: new Date().toISOString(),
|
|
608
|
-
});
|
|
609
598
|
p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
|
|
610
599
|
}
|
|
611
|
-
else {
|
|
612
|
-
writeSemanticStatus({
|
|
613
|
-
status: "pending",
|
|
614
|
-
message: "Semantic prerequisites verified. Building the index to finish activation.",
|
|
615
|
-
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
616
|
-
lastCheckedAt: new Date().toISOString(),
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
600
|
}
|
|
620
601
|
else {
|
|
621
|
-
writeSemanticStatus({
|
|
622
|
-
status: "pending",
|
|
623
|
-
message: "Semantic search is enabled, but asset preparation was skipped.",
|
|
624
|
-
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
625
|
-
lastCheckedAt: new Date().toISOString(),
|
|
626
|
-
});
|
|
627
602
|
p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
|
|
628
603
|
}
|
|
629
604
|
}
|
|
@@ -649,15 +624,6 @@ export async function runSetupWizard(opts) {
|
|
|
649
624
|
catch (err) {
|
|
650
625
|
spin.stop("Indexing failed — you can run `akm index` manually later.");
|
|
651
626
|
p.log.warn(String(err));
|
|
652
|
-
if (savedConfig.semanticSearchMode === "auto") {
|
|
653
|
-
writeSemanticStatus({
|
|
654
|
-
status: "blocked",
|
|
655
|
-
reason: "index-failed",
|
|
656
|
-
message: String(err),
|
|
657
|
-
providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
|
|
658
|
-
lastCheckedAt: new Date().toISOString(),
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
627
|
}
|
|
662
628
|
// API key reminder
|
|
663
629
|
if (embedding?.apiKey === undefined && embedding?.provider !== "ollama") {
|
|
@@ -989,22 +955,19 @@ export async function runSetupFromConfig(opts) {
|
|
|
989
955
|
merged = ctx.config;
|
|
990
956
|
}
|
|
991
957
|
// Convert the private setup draft to the persisted bundle shape.
|
|
992
|
-
|
|
958
|
+
const finalizedMerged = finalizeSetupDraft(merged);
|
|
993
959
|
// Reject an invalid merged engine graph before probing or touching the stash.
|
|
994
960
|
validateCompleteConfig(finalizedMerged);
|
|
995
|
-
// Optional probe
|
|
961
|
+
// Optional connectivity probe — informational only, never blocks or
|
|
962
|
+
// mutates config. `chatCompletion` attempts structured output fresh on
|
|
963
|
+
// every call that needs it (see `llm/client.ts`), so there is nothing to
|
|
964
|
+
// probe or persist here beyond "did the endpoint answer".
|
|
996
965
|
const mergedLlm = readCurrentLlmEngine(finalizedMerged);
|
|
997
966
|
if (opts.probe && mergedLlm) {
|
|
998
967
|
try {
|
|
999
|
-
const
|
|
1000
|
-
if (
|
|
1001
|
-
|
|
1002
|
-
...finalizedMerged,
|
|
1003
|
-
...writeLlmEngine(finalizedMerged, {
|
|
1004
|
-
...mergedLlm,
|
|
1005
|
-
capabilities: { structuredOutput: caps.structuredOutput ?? false },
|
|
1006
|
-
}),
|
|
1007
|
-
};
|
|
968
|
+
const reach = await probeLlmReachable(mergedLlm);
|
|
969
|
+
if (!reach.reachable) {
|
|
970
|
+
warn(`[akm setup] LLM endpoint not reachable${reach.error ? `: ${reach.error}` : ""}.`);
|
|
1008
971
|
}
|
|
1009
972
|
}
|
|
1010
973
|
catch {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* each step keeps its own option assembly and branching.
|
|
10
10
|
*/
|
|
11
11
|
import * as p from "../../cli/clack.js";
|
|
12
|
-
import {
|
|
12
|
+
import { probeLlmReachable } from "../../llm/client.js";
|
|
13
13
|
import { prompt, promptOrBack } from "../prompt.js";
|
|
14
14
|
// ── Derive-config ───────────────────────────────────────────────────────────
|
|
15
15
|
/** Standard chat-connection config shared by every provider branch. */
|
|
@@ -97,21 +97,18 @@ export async function promptApiKeyEnvVarName() {
|
|
|
97
97
|
}
|
|
98
98
|
// ── Probe ───────────────────────────────────────────────────────────────────
|
|
99
99
|
/**
|
|
100
|
-
* Best-effort
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* Best-effort reachability probe — never blocks setup. Does NOT probe or
|
|
101
|
+
* cache structured-output support: `chatCompletion` now attempts
|
|
102
|
+
* `response_format: json_schema` fresh on every call that supplies a schema
|
|
103
|
+
* and falls back once per call on a 4xx (see `llm/client.ts`), so there is
|
|
104
|
+
* no verdict for setup to determine or persist here.
|
|
103
105
|
*/
|
|
104
106
|
export async function probeLlmConnection(llm) {
|
|
105
107
|
const probeSpin = p.spinner();
|
|
106
|
-
probeSpin.start("Probing LLM
|
|
107
|
-
const probe = await
|
|
108
|
-
if (probe.reachable
|
|
109
|
-
probeSpin.stop("LLM reachable
|
|
110
|
-
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
|
|
111
|
-
}
|
|
112
|
-
else if (probe.reachable) {
|
|
113
|
-
probeSpin.stop("LLM reachable but structured-output probe failed.");
|
|
114
|
-
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
|
|
108
|
+
probeSpin.start("Probing LLM connectivity...");
|
|
109
|
+
const probe = await probeLlmReachable(llm);
|
|
110
|
+
if (probe.reachable) {
|
|
111
|
+
probeSpin.stop("LLM reachable.");
|
|
115
112
|
}
|
|
116
113
|
else {
|
|
117
114
|
probeSpin.stop("LLM not reachable.");
|
|
@@ -23,6 +23,9 @@ const CACHE_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
23
23
|
const QUEUE_EXPANSION_FACTOR = 5;
|
|
24
24
|
const MAX_PAGES_DEFAULT = 50;
|
|
25
25
|
const MAX_DEPTH_DEFAULT = 3;
|
|
26
|
+
/** Byte cap for the `llms.txt` manifest itself — a curated link list, never a large file. */
|
|
27
|
+
const LLMS_TXT_BYTE_CAP = 512 * 1024;
|
|
28
|
+
const LLMS_TXT_BODY_TIMEOUT_MS = 15_000;
|
|
26
29
|
/**
|
|
27
30
|
* Per-page body cap for website scraping. HTML pages this large are
|
|
28
31
|
* almost never useful as agent knowledge sources and a runaway server
|
|
@@ -531,6 +534,38 @@ async function crawlWebsite(startUrl, options) {
|
|
|
531
534
|
? createAllowAllRobotsPolicy()
|
|
532
535
|
: createRobotsPolicy((robotsUrl) => loadRobotsTxt(robotsUrl, { allowPrivateHosts: options.allowPrivateHosts, signal: crawlSignal }));
|
|
533
536
|
await assertStartUrlAllowedByRobots(robots, start, options.rawStartUrl);
|
|
537
|
+
// llms.txt fast path: an increasing number of doc sites publish a curated,
|
|
538
|
+
// deduplicated link list at `/llms.txt` specifically for tools like this
|
|
539
|
+
// one. When present, use it as the crawl frontier instead of discovering
|
|
540
|
+
// links by parsing HTML — each linked page still goes through the exact
|
|
541
|
+
// same robots-compliant, host-guarded `fetchWebsitePage` call below, so
|
|
542
|
+
// ingested pages stay individually addressable. Gated to origin-root start
|
|
543
|
+
// URLs only (mirrors `extractGithubRepository`'s repo-root restriction):
|
|
544
|
+
// adding a specific page must fetch that page, not silently pull in the
|
|
545
|
+
// whole site's manifest.
|
|
546
|
+
if (isOriginRootUrl(start)) {
|
|
547
|
+
const manifest = await fetchLlmsManifest(start, robots, {
|
|
548
|
+
allowPrivateHosts: options.allowPrivateHosts,
|
|
549
|
+
signal: crawlSignal,
|
|
550
|
+
});
|
|
551
|
+
if (manifest) {
|
|
552
|
+
warn("[akm] Using llms.txt manifest from %s", manifest.manifestUrl);
|
|
553
|
+
queue.length = 0;
|
|
554
|
+
for (const link of manifest.links) {
|
|
555
|
+
// A manifest can name arbitrary hosts; only same-origin links are
|
|
556
|
+
// honored by default, same as links discovered mid-crawl below.
|
|
557
|
+
if (link.origin !== allowedOrigin)
|
|
558
|
+
continue;
|
|
559
|
+
const candidate = normalizeCrawlUrl(link.toString());
|
|
560
|
+
if (!candidate)
|
|
561
|
+
continue;
|
|
562
|
+
// depth = maxDepth: fetch each manifest page individually, but don't
|
|
563
|
+
// treat it as a fresh BFS seed — the manifest is already the
|
|
564
|
+
// author-curated set of pages worth ingesting.
|
|
565
|
+
queue.push({ url: candidate, rawUrl: link.toString(), depth: options.maxDepth, deferrals: 0 });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
534
569
|
// Counts actual `fetchWebsitePage` invocations (regardless of outcome) so
|
|
535
570
|
// Crawl-delay pacing skips the first fetch and never charges a delay slot
|
|
536
571
|
// to a URL that robots.txt skipped without ever being fetched (C-11).
|
|
@@ -870,6 +905,97 @@ function buildMarkdownSnapshot(page, slug, tags) {
|
|
|
870
905
|
"",
|
|
871
906
|
].join("\n");
|
|
872
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* True for a start URL that names an origin's root (no path, no query).
|
|
910
|
+
* Matches how `extractGithubRepository` restricts its own special-case match
|
|
911
|
+
* to repository-root URLs — the llms.txt probe must not fire for a
|
|
912
|
+
* user-supplied deep link, or `akm bundle add <site>/guides/foo` would
|
|
913
|
+
* silently ingest the whole site's manifest instead of the page requested.
|
|
914
|
+
*/
|
|
915
|
+
export function isOriginRootUrl(url) {
|
|
916
|
+
return url.pathname === "/" && !url.search;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Parses the `llms.txt` link-list format: list items shaped like
|
|
920
|
+
* `- [title](path) - description` (the description, and its separator, are
|
|
921
|
+
* ignored — only the link target is needed). Any line that isn't a markdown
|
|
922
|
+
* link list item — headings, the leading `# Title`/`> summary` lines, prose —
|
|
923
|
+
* is simply not a link line and is skipped.
|
|
924
|
+
*/
|
|
925
|
+
export function parseLlmsTxtLinks(text, baseUrl) {
|
|
926
|
+
const links = [];
|
|
927
|
+
const seen = new Set();
|
|
928
|
+
for (const line of text.split(/\r?\n/)) {
|
|
929
|
+
const match = line.trim().match(/^-\s*\[[^\]]*\]\(([^)\s]+)\)/);
|
|
930
|
+
const href = match?.[1];
|
|
931
|
+
if (!href)
|
|
932
|
+
continue;
|
|
933
|
+
let resolved;
|
|
934
|
+
try {
|
|
935
|
+
resolved = new URL(href, baseUrl);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:")
|
|
941
|
+
continue;
|
|
942
|
+
const key = resolved.toString();
|
|
943
|
+
if (seen.has(key))
|
|
944
|
+
continue;
|
|
945
|
+
seen.add(key);
|
|
946
|
+
links.push(resolved);
|
|
947
|
+
}
|
|
948
|
+
return links;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Probes `<origin>/llms.txt` and, if present, returns its parsed link list.
|
|
952
|
+
* Reuses `fetchWebsiteResponse` so the manifest fetch itself gets the exact
|
|
953
|
+
* same SSRF host guard, redirect handling, and (via `robots`) robots.txt
|
|
954
|
+
* compliance as any other page fetch — this is still a fetch against a
|
|
955
|
+
* user-supplied host, no different from the rest of the crawl.
|
|
956
|
+
*
|
|
957
|
+
* `llms-full.txt` (the single-file concatenation of every page) is
|
|
958
|
+
* deliberately NOT read here. Its `## <path>` separators are ambiguous — page
|
|
959
|
+
* content legitimately contains `##` headings too — so recovered page
|
|
960
|
+
* boundaries can't be trusted, whereas per-page fetches through the existing
|
|
961
|
+
* pipeline are cheap, bounded by this author-curated list, and produce
|
|
962
|
+
* cleanly addressable assets. See the issue's "alternatives considered".
|
|
963
|
+
*/
|
|
964
|
+
async function fetchLlmsManifest(start, robots, options) {
|
|
965
|
+
const manifestUrl = new URL("/llms.txt", start.origin).toString();
|
|
966
|
+
const decision = await resolveCrawlRobotsDecision(robots, manifestUrl);
|
|
967
|
+
if (!decision.allowed)
|
|
968
|
+
return null;
|
|
969
|
+
let fetched;
|
|
970
|
+
try {
|
|
971
|
+
fetched = await fetchWebsiteResponse(decision.fetchUrl, 0, {
|
|
972
|
+
allowPrivateHosts: options.allowPrivateHosts,
|
|
973
|
+
signal: options.signal,
|
|
974
|
+
robots,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
if (!fetched.response.ok) {
|
|
981
|
+
await fetched.response.body?.cancel().catch(() => undefined);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
let text;
|
|
985
|
+
try {
|
|
986
|
+
text = await readBodyWithByteCap(fetched.response, LLMS_TXT_BYTE_CAP, {
|
|
987
|
+
bodyTimeoutMs: LLMS_TXT_BODY_TIMEOUT_MS,
|
|
988
|
+
signal: options.signal,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const links = parseLlmsTxtLinks(text, fetched.finalUrl);
|
|
995
|
+
if (links.length === 0)
|
|
996
|
+
return null;
|
|
997
|
+
return { manifestUrl: fetched.finalUrl, links };
|
|
998
|
+
}
|
|
873
999
|
function normalizeCrawlUrl(rawUrl) {
|
|
874
1000
|
try {
|
|
875
1001
|
const parsed = new URL(rawUrl);
|
|
@@ -1,6 +1,25 @@
|
|
|
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
|
+
* Shared SQLite migration engine.
|
|
6
|
+
*
|
|
7
|
+
* SQLite schemas evolve through this transaction-per-migration runner backed
|
|
8
|
+
* by a `schema_migrations` ledger.
|
|
9
|
+
*
|
|
10
|
+
* This module factors that runner out once. Each caller supplies only its own
|
|
11
|
+
* `MIGRATIONS` array.
|
|
12
|
+
*
|
|
13
|
+
* Ledger/transaction contract:
|
|
14
|
+
* - `id` is permanent and must never be reused.
|
|
15
|
+
* - Applied IDs must be an exact ordered prefix of the registry.
|
|
16
|
+
* - Each `up` body and its ledger insert commit in the same transaction.
|
|
17
|
+
* - Migration bodies run only after `BEGIN IMMEDIATE` is observably active,
|
|
18
|
+
* and the transaction must remain active through the body's completion.
|
|
19
|
+
* - The caller owns semantic safety classification and any policy gate;
|
|
20
|
+
* this generic engine intentionally does not infer risk from SQL text.
|
|
21
|
+
*/
|
|
22
|
+
import { sleepSync } from "../../runtime.js";
|
|
4
23
|
export function assertMigrationRegistry(migrations) {
|
|
5
24
|
const seen = new Set();
|
|
6
25
|
for (const migration of migrations) {
|
|
@@ -152,7 +171,7 @@ function isRetryableImmediateBeginError(error) {
|
|
|
152
171
|
function sleepImmediateRetry(ms) {
|
|
153
172
|
if (ms <= 0)
|
|
154
173
|
return;
|
|
155
|
-
|
|
174
|
+
sleepSync(ms);
|
|
156
175
|
}
|
|
157
176
|
/**
|
|
158
177
|
* Run `fn` inside a `BEGIN IMMEDIATE` transaction.
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* import their opener from a sibling here instead of reaching up into the
|
|
11
11
|
* indexer — inverting the old storage→indexer arrow.
|
|
12
12
|
*/
|
|
13
|
+
import fs from "node:fs";
|
|
13
14
|
import { createRequire } from "node:module";
|
|
14
15
|
import { ConfigError } from "../../core/errors.js";
|
|
15
16
|
import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
|
|
16
17
|
import { getDbPath } from "../../core/paths.js";
|
|
18
|
+
import { warn } from "../../core/warn.js";
|
|
17
19
|
import { openDatabase } from "../database.js";
|
|
18
20
|
import { openManagedDatabase } from "../managed-db.js";
|
|
19
21
|
import { SQLITE_BUSY_TIMEOUT_MS } from "../sqlite-pragmas.js";
|
|
@@ -21,9 +23,23 @@ import { openSqliteReadSnapshot } from "../sqlite-read-snapshot.js";
|
|
|
21
23
|
import { isCanonicalIndexGeneration } from "./index-entry-schema.js";
|
|
22
24
|
import { ensureSchema } from "./index-schema.js";
|
|
23
25
|
import { loadVecExtension, warnIfVecMissing } from "./index-vec-repository.js";
|
|
26
|
+
/**
|
|
27
|
+
* Whether `error` is SQLite reporting on-disk corruption (`SQLITE_CORRUPT`,
|
|
28
|
+
* "database disk image is malformed") rather than a permission, lock, or
|
|
29
|
+
* schema problem. Matched on both `code` (bun:sqlite, better-sqlite3) and
|
|
30
|
+
* message text, since driver error shapes are not perfectly uniform.
|
|
31
|
+
*/
|
|
32
|
+
function isCorruptionError(error) {
|
|
33
|
+
const code = error?.code;
|
|
34
|
+
if (code === "SQLITE_CORRUPT")
|
|
35
|
+
return true;
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
return message.includes("database disk image is malformed") || message.includes("SQLITE_CORRUPT");
|
|
38
|
+
}
|
|
24
39
|
export function openIndexDatabase(dbPath, options) {
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
const resolvedPath = dbPath ?? getDbPath();
|
|
41
|
+
const spec = {
|
|
42
|
+
path: resolvedPath,
|
|
27
43
|
init: (db) => {
|
|
28
44
|
// Try to load sqlite-vec extension
|
|
29
45
|
loadVecExtension(db);
|
|
@@ -41,7 +57,33 @@ export function openIndexDatabase(dbPath, options) {
|
|
|
41
57
|
// Warn once at init if using JS fallback with many entries
|
|
42
58
|
warnIfVecMissing(db, { once: true });
|
|
43
59
|
},
|
|
44
|
-
}
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
return openManagedDatabase(spec);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// index.db is a derived cache, fully regenerable from the stash on disk
|
|
66
|
+
// (see src/core/state-db.ts's "Why a separate database from index.db"
|
|
67
|
+
// note) — so real on-disk corruption is recovered by deleting the file
|
|
68
|
+
// and rebuilding, not by surfacing a raw SQLITE_CORRUPT to the caller or
|
|
69
|
+
// quietly falling through to an unreadable index. This mirrors the
|
|
70
|
+
// existing stale-version-marker rebuild below, one layer further down
|
|
71
|
+
// (that path opens fine and rewrites tables in place; corruption prevents
|
|
72
|
+
// even opening, so the file itself has to go first).
|
|
73
|
+
if (!isCorruptionError(error))
|
|
74
|
+
throw error;
|
|
75
|
+
warn(`Index database is corrupt at ${resolvedPath} — rebuilding.`);
|
|
76
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
77
|
+
try {
|
|
78
|
+
fs.rmSync(`${resolvedPath}${suffix}`, { force: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Best-effort cleanup; the retried open below still fails loudly if
|
|
82
|
+
// the file could not actually be removed.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return openManagedDatabase(spec);
|
|
86
|
+
}
|
|
45
87
|
}
|
|
46
88
|
/**
|
|
47
89
|
* Read the operator-configured embedding dimension from the on-disk config.
|
|
@@ -354,9 +354,6 @@ function deleteEntryRows(db, rows, options = {}) {
|
|
|
354
354
|
}
|
|
355
355
|
return rows.map((row) => row.id);
|
|
356
356
|
}
|
|
357
|
-
export function deleteEntriesByDir(db, dirPath) {
|
|
358
|
-
db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath)))();
|
|
359
|
-
}
|
|
360
357
|
export function deleteEntriesByDirAndBundle(db, dirPath, bundleId, options = {}) {
|
|
361
358
|
return db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options))();
|
|
362
359
|
}
|
|
@@ -704,18 +701,6 @@ export function getEntryRefRowsForStashRoot(db, stashRoot) {
|
|
|
704
701
|
});
|
|
705
702
|
}
|
|
706
703
|
// ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
|
|
707
|
-
/**
|
|
708
|
-
* Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
|
|
709
|
-
* Not security-sensitive; used as an incremental fast-path skip key.
|
|
710
|
-
*/
|
|
711
|
-
export function computeSourceHash(content) {
|
|
712
|
-
let hash = 0x811c9dc5;
|
|
713
|
-
for (let i = 0; i < content.length; i++) {
|
|
714
|
-
hash ^= content[i];
|
|
715
|
-
hash = Math.imul(hash, 0x01000193);
|
|
716
|
-
}
|
|
717
|
-
return (hash >>> 0).toString(16);
|
|
718
|
-
}
|
|
719
704
|
/**
|
|
720
705
|
* Return distinct zero-result search queries from the `usage_events` table
|
|
721
706
|
* within the given lookback window.
|
|
@@ -31,7 +31,7 @@ import { getTaskLogDir } from "../../core/paths.js";
|
|
|
31
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
32
32
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
33
33
|
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
34
|
-
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
34
|
+
import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
35
35
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
36
36
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
37
37
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
@@ -105,7 +105,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
105
105
|
replaceCrontab(exec, existing, next);
|
|
106
106
|
},
|
|
107
107
|
list() {
|
|
108
|
-
return [...inspectCronState(readCrontab(exec)).installed];
|
|
108
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
|
|
109
109
|
},
|
|
110
110
|
listForRebind() {
|
|
111
111
|
const existing = readCrontab(exec);
|
|
@@ -123,14 +123,14 @@ export function CRON_BACKEND(options = {}) {
|
|
|
123
123
|
});
|
|
124
124
|
},
|
|
125
125
|
listNativeArtifacts() {
|
|
126
|
-
return [...inspectCronState(readCrontab(exec)).artifacts];
|
|
126
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
|
|
127
127
|
},
|
|
128
128
|
inspectBindings() {
|
|
129
|
-
return inspectCronState(readCrontab(exec));
|
|
129
|
+
return inspectCronState(readCrontab(exec), defaultContextPath);
|
|
130
130
|
},
|
|
131
131
|
snapshotBindings(ids) {
|
|
132
132
|
const crontab = readCrontab(exec);
|
|
133
|
-
const inspection = inspectCronState(crontab);
|
|
133
|
+
const inspection = inspectCronState(crontab, defaultContextPath);
|
|
134
134
|
const keys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
135
135
|
return Object.freeze({
|
|
136
136
|
kind: CRON_SNAPSHOT,
|
|
@@ -144,7 +144,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
144
144
|
throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
145
145
|
}
|
|
146
146
|
const existing = readCrontab(exec);
|
|
147
|
-
const current = inspectCronState(existing);
|
|
147
|
+
const current = inspectCronState(existing, defaultContextPath);
|
|
148
148
|
const safeNativeIds = [];
|
|
149
149
|
const errors = [];
|
|
150
150
|
if (expectedCurrent) {
|
|
@@ -192,7 +192,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
192
192
|
},
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
-
function inspectCronState(crontab) {
|
|
195
|
+
function inspectCronState(crontab, fallbackContextPath) {
|
|
196
196
|
const installed = [];
|
|
197
197
|
const artifacts = [];
|
|
198
198
|
for (const { id, body } of listBlocks(crontab)) {
|
|
@@ -207,7 +207,14 @@ function inspectCronState(crontab) {
|
|
|
207
207
|
signature: fingerprint,
|
|
208
208
|
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
209
209
|
binding: parsed.binding,
|
|
210
|
-
|
|
210
|
+
// A legacy (pre-`--scheduler-context`) row has no real descriptor
|
|
211
|
+
// path to report — `extractLegacyCronInvocation` leaves it "". Fall
|
|
212
|
+
// back to the current default so downstream consumers (context
|
|
213
|
+
// validation in `akm task prune`/`explain`, `sync`'s reuse of an
|
|
214
|
+
// existing binding's contextPath) see a real, resolvable descriptor
|
|
215
|
+
// rather than an empty path, since the row is about to be reconciled
|
|
216
|
+
// to a current one anyway (#881).
|
|
217
|
+
contextPath: parsed.contextPath || fallbackContextPath,
|
|
211
218
|
};
|
|
212
219
|
Object.defineProperty(ref, "nativeId", { value: id });
|
|
213
220
|
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
@@ -323,7 +330,40 @@ export function extractCronInvocation(body) {
|
|
|
323
330
|
const redirectIndex = fields.indexOf(">>", commandStart);
|
|
324
331
|
if (redirectIndex === -1)
|
|
325
332
|
return undefined;
|
|
326
|
-
|
|
333
|
+
const tail = fields.slice(commandStart, redirectIndex);
|
|
334
|
+
const parsed = parseScheduledBindingArgv(tail);
|
|
335
|
+
if (parsed)
|
|
336
|
+
return parsed;
|
|
337
|
+
// Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
|
|
338
|
+
// no context argument at all — just the akm argv immediately followed by
|
|
339
|
+
// the public `task run …` / `workflow run …` tail. `extractCronInvocation`
|
|
340
|
+
// only ever runs on a body already isolated between this backend's own
|
|
341
|
+
// `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
|
|
342
|
+
// this older shape here doesn't extend trust to any unmarked crontab
|
|
343
|
+
// line — it only lets sync see and reconcile a row akm already owns
|
|
344
|
+
// instead of treating it as absent and colliding with the still-present
|
|
345
|
+
// artifact (#881). Guarded on the marker's absence so a row that DOES
|
|
346
|
+
// carry `--scheduler-context` but fails to parse for some other reason
|
|
347
|
+
// is never silently reinterpreted as legacy.
|
|
348
|
+
if (tail.includes(SCHEDULER_CONTEXT_ARG))
|
|
349
|
+
return undefined;
|
|
350
|
+
return extractLegacyCronInvocation(tail);
|
|
351
|
+
}
|
|
352
|
+
function extractLegacyCronInvocation(tail) {
|
|
353
|
+
for (let index = 0; index < tail.length - 1; index += 1) {
|
|
354
|
+
if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
|
|
355
|
+
const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
|
|
356
|
+
if (!publicInvocation)
|
|
357
|
+
return undefined;
|
|
358
|
+
return {
|
|
359
|
+
binding: tail.slice(0, index),
|
|
360
|
+
contextPath: "",
|
|
361
|
+
invocation: publicInvocation.invocation,
|
|
362
|
+
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
327
367
|
}
|
|
328
368
|
/** Reverse {@link quoteForCron} for a single whitespace-free token. */
|
|
329
369
|
function splitCronShellWords(value) {
|
|
@@ -494,15 +494,20 @@ function assertUnrestorableLaunchdSnapshotUnchanged(entry, currentArtifacts) {
|
|
|
494
494
|
throw new ConfigError(`Loaded launchd artifact ${JSON.stringify(entry.id)} has no restorable plist and changed after its snapshot; refusing mutation.`, "INVALID_CONFIG_FILE");
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Enumerate the current launchd AKM service namespace (loaded domain members,
|
|
499
|
+
* print-disabled labels, and on-disk plists) in one pass.
|
|
500
|
+
*
|
|
501
|
+
* This used to run the enumeration twice and fail closed if the two passes
|
|
502
|
+
* disagreed, guarding against launchd state mutating between the two
|
|
503
|
+
* `launchctl` shell-outs (milliseconds apart, in-process). The only actor
|
|
504
|
+
* that could cause that is another concurrent akm process or the user
|
|
505
|
+
* running launchctl by hand at that exact instant; a subsequent `task sync`
|
|
506
|
+
* (idempotent by design) reconciles any such drift on its own, so the
|
|
507
|
+
* two-pass fencing added a TOCTOU-shaped guard around a race with a working
|
|
508
|
+
* self-heal already in place, not a way to avoid one.
|
|
509
|
+
*/
|
|
497
510
|
function inspectStableLaunchdNamespace(seedIds, context) {
|
|
498
|
-
const first = captureLaunchdNamespacePass(seedIds, context);
|
|
499
|
-
const second = captureLaunchdNamespacePass(seedIds, context);
|
|
500
|
-
if (first.stabilityKey !== second.stabilityKey) {
|
|
501
|
-
throw new ConfigError("The launchd AKM service namespace changed while scheduler state was being stabilized.", "INVALID_CONFIG_FILE");
|
|
502
|
-
}
|
|
503
|
-
return second.namespace;
|
|
504
|
-
}
|
|
505
|
-
function captureLaunchdNamespacePass(seedIds, context) {
|
|
506
511
|
const domain = context.exec.run(["launchctl", "print", `gui/${context.exec.uid()}`]);
|
|
507
512
|
if (domain.status !== 0) {
|
|
508
513
|
throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
@@ -532,9 +537,6 @@ function captureLaunchdNamespacePass(seedIds, context) {
|
|
|
532
537
|
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
533
538
|
for (const serviceLabel of akmDisabledLabels)
|
|
534
539
|
ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
|
|
535
|
-
if (ids.size > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES) {
|
|
536
|
-
throw new ConfigError(`launchd AKM scheduler inventory exceeds ${MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES} namespace entries.`, "INVALID_CONFIG_FILE");
|
|
537
|
-
}
|
|
538
540
|
const plistByNativeId = new Map(plistEntries);
|
|
539
541
|
const entries = [];
|
|
540
542
|
for (const nativeId of [...ids].sort()) {
|
|
@@ -566,15 +568,8 @@ function captureLaunchdNamespacePass(seedIds, context) {
|
|
|
566
568
|
}, parsed.invocation, entry.nativeId));
|
|
567
569
|
}
|
|
568
570
|
return Object.freeze({
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
entries: Object.freeze(entries),
|
|
572
|
-
}),
|
|
573
|
-
stabilityKey: JSON.stringify({
|
|
574
|
-
loadedLabels: [...loadedLabels].sort(),
|
|
575
|
-
disabledLabels: akmDisabledLabels,
|
|
576
|
-
plistEntries,
|
|
577
|
-
}),
|
|
571
|
+
inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
|
|
572
|
+
entries: Object.freeze(entries),
|
|
578
573
|
});
|
|
579
574
|
}
|
|
580
575
|
function restoreLaunchdBindingEntry(entry, context, errors) {
|
|
@@ -106,7 +106,7 @@ export function SCHTASKS_BACKEND(options = {}) {
|
|
|
106
106
|
assertSchedulerMutationArtifact(current?.status === 0 && finalNativeId ? schtasksArtifact(finalNativeId, current.stdout) : undefined, expected);
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
|
-
assertSchtasksArtifactUnchanged(exec, taskName(nativeId),
|
|
109
|
+
assertSchtasksArtifactUnchanged(exec, taskName(nativeId), nativeId, task);
|
|
110
110
|
}
|
|
111
111
|
try {
|
|
112
112
|
// /F forces overwrite if a task with the same name exists.
|
|
@@ -461,8 +461,23 @@ function parsePowerShellSingleQuotedArgs(script, start) {
|
|
|
461
461
|
}
|
|
462
462
|
return argv;
|
|
463
463
|
}
|
|
464
|
-
/**
|
|
465
|
-
|
|
464
|
+
/**
|
|
465
|
+
* Re-verify ownership of the native artifact immediately before `/Create /F`
|
|
466
|
+
* overwrites it (the read/prepare-to-/Create ownership race at the native
|
|
467
|
+
* boundary).
|
|
468
|
+
*
|
|
469
|
+
* This used to also compare the fresh query against the first read taken
|
|
470
|
+
* earlier in `install` and refuse the whole operation if anything about the
|
|
471
|
+
* XML had changed in between -- a freshness re-check on top of the identity
|
|
472
|
+
* re-check right below it. That extra comparison never caught anything the
|
|
473
|
+
* owner check didn't already cover (a foreign/changed owner still fails
|
|
474
|
+
* `assertSchedulerNativeArtifactOwner`), and `task sync` is idempotent, so a
|
|
475
|
+
* spurious refusal here only cost the user a rerun of a command they'd
|
|
476
|
+
* already asked for. Dropped; the owner re-check (the actual corruption/
|
|
477
|
+
* clobber guard -- no fallback exists if a foreign task got silently
|
|
478
|
+
* overwritten) stays.
|
|
479
|
+
*/
|
|
480
|
+
function assertSchtasksArtifactUnchanged(exec, nativeTaskName, nativeId, task) {
|
|
466
481
|
// This runs outside install's rollback region: no scheduler mutation has
|
|
467
482
|
// occurred, so a raced owner must remain untouched rather than be restored
|
|
468
483
|
// from the stale first read.
|
|
@@ -470,11 +485,6 @@ function assertSchtasksArtifactUnchanged(exec, nativeTaskName, previous, nativeI
|
|
|
470
485
|
isOk: (result) => result.status === 0 || isMissingTaskResult(result),
|
|
471
486
|
message: (result) => `schtasks /Query failed during final ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
472
487
|
});
|
|
473
|
-
const changed = current.status !== previous.status ||
|
|
474
|
-
(current.status === 0 && normalizeXmlForUtf16File(current.stdout) !== normalizeXmlForUtf16File(previous.stdout));
|
|
475
|
-
if (changed) {
|
|
476
|
-
throw new ConfigError(`schtasks task "${nativeTaskName}" changed while it was being prepared; refusing to replace an unverified owner.`, "INVALID_CONFIG_FILE");
|
|
477
|
-
}
|
|
478
488
|
if (current.status === 0) {
|
|
479
489
|
assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(current.stdout)?.invocation);
|
|
480
490
|
}
|