akm-cli 0.9.6 → 0.9.8-beta.1
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 +408 -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.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +9 -20
- package/dist/commands/improve/preparation.js +53 -37
- package/dist/commands/improve/reflect.js +14 -18
- package/dist/commands/lint/base-linter.js +182 -19
- package/dist/commands/lint/index.js +21 -9
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +51 -18
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +14 -5
- package/dist/core/asset/frontmatter.js +212 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +155 -251
- package/dist/scripts/akm-migrate.js +155 -251
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +7 -1
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -158,12 +158,6 @@ export function resolveEntryContentDir(entry) {
|
|
|
158
158
|
}
|
|
159
159
|
return dir;
|
|
160
160
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Convenience: returns just the directory paths, preserving priority order.
|
|
163
|
-
*/
|
|
164
|
-
export function resolveAllStashDirs(overrideStashDir) {
|
|
165
|
-
return resolveSourceEntries(overrideStashDir).map((s) => s.path);
|
|
166
|
-
}
|
|
167
161
|
/**
|
|
168
162
|
* Find which source a file path belongs to.
|
|
169
163
|
*
|
|
@@ -187,13 +181,6 @@ export function findSourceForPath(filePath, sources) {
|
|
|
187
181
|
}
|
|
188
182
|
return best;
|
|
189
183
|
}
|
|
190
|
-
/**
|
|
191
|
-
* Return the primary stash source (first entry in the list).
|
|
192
|
-
* This is the user's working stash and the default destination for clone.
|
|
193
|
-
*/
|
|
194
|
-
export function getPrimarySource(sources) {
|
|
195
|
-
return sources[0];
|
|
196
|
-
}
|
|
197
184
|
// ── Editability ─────────────────────────────────────────────────────────────
|
|
198
185
|
/**
|
|
199
186
|
* Determine whether AKM policy allows modifying this exact file in place.
|
|
@@ -1,6 +1,13 @@
|
|
|
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
|
+
* Usage event helpers for telemetry and utility-based re-ranking.
|
|
6
|
+
*
|
|
7
|
+
* Schema (created by ensureUsageEventsSchema):
|
|
8
|
+
* id, event_type, query, entry_id (nullable), entry_ref, signal, metadata, source, created_at
|
|
9
|
+
*/
|
|
10
|
+
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
4
11
|
const USAGE_EVENT_SOURCES = new Set(["user", "improve", "task", "audit", "unknown"]);
|
|
5
12
|
/**
|
|
6
13
|
* Resolve subprocess provenance without treating an invalid value as user
|
|
@@ -50,7 +57,8 @@ export function insertUsageEvent(db, event) {
|
|
|
50
57
|
db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source)
|
|
51
58
|
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown");
|
|
52
59
|
}
|
|
53
|
-
catch {
|
|
60
|
+
catch (error) {
|
|
61
|
+
rethrowIfTestIsolationError(error);
|
|
54
62
|
/* fire-and-forget: silently ignore errors */
|
|
55
63
|
}
|
|
56
64
|
}
|
|
@@ -167,13 +167,16 @@ function isInsideGitRepo(dir) {
|
|
|
167
167
|
return false;
|
|
168
168
|
}
|
|
169
169
|
/**
|
|
170
|
-
* Recursively
|
|
170
|
+
* Recursively collect every `.md` file under `root`.
|
|
171
171
|
*
|
|
172
|
-
* Shared by graph-extraction and memory-inference so the
|
|
173
|
-
*
|
|
174
|
-
* read
|
|
172
|
+
* Shared by graph-extraction and memory-inference so the walk logic lives in
|
|
173
|
+
* exactly one place. Mirrors the `complete` tracking of the other walkers in
|
|
174
|
+
* this file: a directory that cannot be read makes the result incomplete
|
|
175
|
+
* instead of silently looking like a clean, empty scan.
|
|
175
176
|
*/
|
|
176
|
-
export function
|
|
177
|
+
export function walkMarkdownFiles(root) {
|
|
178
|
+
const files = [];
|
|
179
|
+
let complete = true;
|
|
177
180
|
const stack = [root];
|
|
178
181
|
while (stack.length > 0) {
|
|
179
182
|
const current = stack.pop();
|
|
@@ -184,6 +187,7 @@ export function* walkMarkdownFiles(root) {
|
|
|
184
187
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
185
188
|
}
|
|
186
189
|
catch {
|
|
190
|
+
complete = false;
|
|
187
191
|
continue;
|
|
188
192
|
}
|
|
189
193
|
for (const entry of entries) {
|
|
@@ -194,10 +198,11 @@ export function* walkMarkdownFiles(root) {
|
|
|
194
198
|
stack.push(full);
|
|
195
199
|
}
|
|
196
200
|
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
197
|
-
|
|
201
|
+
files.push(full);
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
204
|
}
|
|
205
|
+
return { files, complete };
|
|
201
206
|
}
|
|
202
207
|
/** Manual walk for non-git directories. */
|
|
203
208
|
function walkStashManual(stashRoot, options) {
|
|
@@ -1,21 +1,36 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code CLI result extractor (Codex round-3 finding A; plan §"The adapter
|
|
6
|
+
* contract" step 3 / §"Structured-output normalization", tier "native-json").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes one raw {@link AgentRunResult} from a headless `claude -p` run into
|
|
9
|
+
* `{ text, sessionId? }` — the {@link AgentResultExtraction} seam. The engine's
|
|
10
|
+
* shared schema-validation / retry-until-valid loop runs *after* this; the
|
|
11
|
+
* extractor only strips transport framing.
|
|
12
|
+
*
|
|
13
|
+
* A schema-bearing unit is dispatched with `--output-format json` (see
|
|
14
|
+
* `./agent-builder.ts`), so stdout is Claude Code's documented RESULT ENVELOPE —
|
|
15
|
+
* a single JSON object:
|
|
16
|
+
*
|
|
17
|
+
* {"type":"result","subtype":"success","is_error":false,
|
|
18
|
+
* "result":"<final answer>","session_id":"<uuid>","total_cost_usd":…,"usage":…}
|
|
19
|
+
*
|
|
20
|
+
* The final answer the model produced lives in `result` (a string); the
|
|
21
|
+
* harness-native session id in `session_id`, stored opportunistically on the
|
|
22
|
+
* unit row for `claude --resume <id>` (akm never depends on it —
|
|
23
|
+
* `workflow_run_units` stays the source of truth).
|
|
24
|
+
*
|
|
25
|
+
* A SCHEMALESS unit is dispatched WITHOUT `--output-format json`, so stdout is
|
|
26
|
+
* plain text — passed through verbatim (untrimmed) so the schemaless dispatch
|
|
27
|
+
* path is byte-identical to the pre-extractor behaviour. Only a genuine result
|
|
28
|
+
* envelope (a `type: "result"` object, or one carrying both a string `result`
|
|
29
|
+
* and a `session_id`) is unwrapped; anything else degrades to raw pass-through,
|
|
30
|
+
* so a point-release envelope rename fails soft (the engine's embedded-JSON
|
|
31
|
+
* parsing still runs downstream for schema units) rather than hard.
|
|
32
|
+
*/
|
|
33
|
+
import { asNonEmptyString, isRecord, tryParseJson } from "../../../core/common.js";
|
|
19
34
|
/**
|
|
20
35
|
* Is this parsed object Claude Code's `--output-format json` RESULT ENVELOPE
|
|
21
36
|
* (rather than a bare JSON answer a schema unit produced without the flag)? A
|
|
@@ -1,12 +1,49 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* OpenAI Codex CLI result extractor (P2, plan §"The adapter contract" step 3 /
|
|
6
|
+
* §"Structured-output normalization").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes the raw {@link AgentRunResult} of a `codex exec --json` run into
|
|
9
|
+
* `{ text, sessionId? }` ({@link AgentResultExtraction}). Schema validation and
|
|
10
|
+
* the retry-until-valid loop stay in the engine, shared across harnesses —
|
|
11
|
+
* this module only strips transport framing.
|
|
12
|
+
*
|
|
13
|
+
* `--json` emits ONE JSON event per stdout line. Two documented event dialects
|
|
14
|
+
* exist across codex versions; the adapter contract localizes that churn here,
|
|
15
|
+
* so both are handled:
|
|
16
|
+
*
|
|
17
|
+
* Legacy protocol (envelope with an `msg` object):
|
|
18
|
+
* {"id":"0","msg":{"type":"session_configured","session_id":"<uuid>", ...}}
|
|
19
|
+
* {"id":"1","msg":{"type":"agent_message","message":"<text>"}}
|
|
20
|
+
* {"id":"1","msg":{"type":"task_complete","last_agent_message":"<text>"}}
|
|
21
|
+
*
|
|
22
|
+
* Newer experimental-JSON protocol (flat `type` field):
|
|
23
|
+
* {"type":"thread.started","thread_id":"<id>"}
|
|
24
|
+
* {"type":"item.completed","item":{"id":"item_1","type":"agent_message","text":"<text>"}}
|
|
25
|
+
* {"type":"turn.completed","usage":{...}}
|
|
26
|
+
*
|
|
27
|
+
* Extraction rules:
|
|
28
|
+
* - text: `task_complete.last_agent_message` wins when present (it is the
|
|
29
|
+
* harness's own "final answer" designation); otherwise the LAST
|
|
30
|
+
* agent-message event seen; otherwise the trimmed raw stdout (plain-text
|
|
31
|
+
* fallback for runs without `--json`, or unrecognized formats — the
|
|
32
|
+
* engine's `parseEmbeddedJsonResponse` tier still gets a fair input).
|
|
33
|
+
* - sessionId: `session_configured.session_id` / `thread.started.thread_id`,
|
|
34
|
+
* falling back to any sessionId the spawn layer already attached. Stored
|
|
35
|
+
* opportunistically on the unit row for `codex exec resume <id>`; akm
|
|
36
|
+
* never depends on it (plan §"Session, MCP, and identity").
|
|
37
|
+
*
|
|
38
|
+
* Non-event JSON lines (e.g. a bare JSON answer printed without framing) are
|
|
39
|
+
* ignored by the event scan and land in the raw-stdout fallback untouched.
|
|
40
|
+
*
|
|
41
|
+
* Registered: `codexResultExtractor` is `CodexHarness.resultExtractor`
|
|
42
|
+
* (`./index.ts`); the engine resolves it via `getHarness()` in
|
|
43
|
+
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
|
|
44
|
+
* schema validation.
|
|
45
|
+
*/
|
|
46
|
+
import { asNonEmptyString, isRecord } from "../../../core/common.js";
|
|
10
47
|
/** Interpret one parsed JSONL event in either codex event dialect. */
|
|
11
48
|
function interpretEvent(event) {
|
|
12
49
|
// Legacy protocol: {"id":..., "msg":{"type": ...}}
|
|
@@ -1,9 +1,45 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Copilot CLI result extractor (P2, plan §"The adapter contract"
|
|
6
|
+
* step 3 / §"Structured-output normalization", tier "native-json").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes one raw {@link AgentRunResult} from a headless `copilot` run
|
|
9
|
+
* into `{ text, sessionId? }` — the {@link AgentResultExtraction} seam. The
|
|
10
|
+
* engine's shared schema-validation / retry-until-valid loop runs *after*
|
|
11
|
+
* this; the extractor only strips transport framing.
|
|
12
|
+
*
|
|
13
|
+
* Copilot's stdout takes one of three shapes depending on flags/version
|
|
14
|
+
* (`--output-format json` per the capability matrix), all handled here:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Single JSON document** — a result envelope, e.g.
|
|
17
|
+
* `{"type":"result","session_id":"…","result":"<final answer>"}`.
|
|
18
|
+
* Pretty-printed multi-line JSON is included (whole-stdout parse is
|
|
19
|
+
* attempted first). Only objects carrying a transport marker (a `type`
|
|
20
|
+
* discriminator or a session-id field) are unwrapped; a bare JSON answer
|
|
21
|
+
* with no marker (a schema unit's `{"result":"ok"}`) is passed through
|
|
22
|
+
* raw so the engine's schema validator sees the whole object.
|
|
23
|
+
* 2. **JSONL event stream** — one JSON object per line
|
|
24
|
+
* (`session.start` / assistant `message` / `result` events); the LAST
|
|
25
|
+
* text-bearing event wins, the first session-id-bearing event supplies
|
|
26
|
+
* the session id. Non-JSON banner lines are skipped.
|
|
27
|
+
* 3. **Plain text** — no JSON anywhere; trimmed stdout passes through
|
|
28
|
+
* verbatim (the engine's embedded-JSON parsing still runs downstream for
|
|
29
|
+
* schema units).
|
|
30
|
+
*
|
|
31
|
+
* Key names are matched tolerantly (`result`/`response`/`text`/`output`/
|
|
32
|
+
* `content`/`message`, snake_case and camelCase session-id variants) so
|
|
33
|
+
* point-release renames in the CLI's envelope degrade to the plain-text
|
|
34
|
+
* fallback instead of hard-failing — version churn stays contained in this
|
|
35
|
+
* one file, per the adapter contract.
|
|
36
|
+
*
|
|
37
|
+
* Registered: `copilotResultExtractor` is `CopilotHarness.resultExtractor`
|
|
38
|
+
* (`./index.ts`); the engine resolves it via `getHarness()` in
|
|
39
|
+
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
|
|
40
|
+
* schema validation.
|
|
41
|
+
*/
|
|
42
|
+
import { isRecord, tryParseJson } from "../../../core/common.js";
|
|
7
43
|
/** Keys that may carry the final answer, in precedence order. */
|
|
8
44
|
const TEXT_KEYS = ["result", "response", "text", "output", "content", "message"];
|
|
9
45
|
/** Keys that may carry the harness-native session id, in precedence order. */
|
|
@@ -90,15 +126,6 @@ function isTransportEnvelope(value) {
|
|
|
90
126
|
return true;
|
|
91
127
|
return extractSessionId(value) !== undefined;
|
|
92
128
|
}
|
|
93
|
-
/** JSON.parse that returns undefined instead of throwing. */
|
|
94
|
-
function tryParseJson(raw) {
|
|
95
|
-
try {
|
|
96
|
-
return JSON.parse(raw);
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
129
|
/**
|
|
103
130
|
* Normalize a raw copilot run result into `{ text, sessionId? }`.
|
|
104
131
|
* See the module doc for the three stdout shapes handled.
|
|
@@ -1,9 +1,46 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Gemini CLI result extractor (P2, plan §"The adapter contract" step 3 /
|
|
6
|
+
* §"Structured-output normalization", tier "native-json").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes one raw {@link AgentRunResult} from a headless `gemini` run into
|
|
9
|
+
* `{ text, sessionId? }` — the {@link AgentResultExtraction} seam. The
|
|
10
|
+
* engine's shared schema-validation / retry-until-valid loop runs *after*
|
|
11
|
+
* this; the extractor only strips transport framing.
|
|
12
|
+
*
|
|
13
|
+
* Gemini's stdout takes one of three shapes depending on flags/version
|
|
14
|
+
* (`--output-format json` per the capability matrix), all handled here:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Single JSON document** — the documented `--output-format json`
|
|
17
|
+
* envelope, e.g. `{"response":"<final answer>","stats":{...}}` (an
|
|
18
|
+
* `error` variant carries `{error:{type,message,code}}` and no usable
|
|
19
|
+
* response — that degrades to the raw-stdout fallback so the engine's
|
|
20
|
+
* failure handling sees the whole envelope). Pretty-printed multi-line
|
|
21
|
+
* JSON is included (whole-stdout parse is attempted first).
|
|
22
|
+
* 2. **JSONL event stream** (`--output-format stream-json`) — one JSON
|
|
23
|
+
* object per line; the LAST text-bearing event wins, the first
|
|
24
|
+
* session-id-bearing event supplies the session id. Non-JSON banner
|
|
25
|
+
* lines ("Loaded cached credentials.", update notices) are skipped.
|
|
26
|
+
* 3. **Plain text** — no JSON anywhere; trimmed stdout passes through
|
|
27
|
+
* verbatim (the engine's embedded-JSON parsing still runs downstream for
|
|
28
|
+
* schema units).
|
|
29
|
+
*
|
|
30
|
+
* Key names are matched tolerantly (`response` first — Gemini's documented
|
|
31
|
+
* envelope key — then `result`/`text`/`output`/`content`/`message`;
|
|
32
|
+
* snake_case and camelCase session-id variants) so point-release renames in
|
|
33
|
+
* the CLI's envelope degrade to the plain-text fallback instead of
|
|
34
|
+
* hard-failing — version churn stays contained in this one file, per the
|
|
35
|
+
* adapter contract. The session id feeds `--resume <id>` opportunistically;
|
|
36
|
+
* `workflow_run_units` remains the durable source of truth.
|
|
37
|
+
*
|
|
38
|
+
* Registered: `geminiResultExtractor` is `GeminiHarness.resultExtractor`
|
|
39
|
+
* (`./index.ts`); the engine resolves it via `getHarness()` in
|
|
40
|
+
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
|
|
41
|
+
* schema validation.
|
|
42
|
+
*/
|
|
43
|
+
import { isRecord, tryParseJson } from "../../../core/common.js";
|
|
7
44
|
/**
|
|
8
45
|
* Keys that may carry the final answer, in precedence order. `response` is
|
|
9
46
|
* Gemini's documented `--output-format json` envelope key.
|
|
@@ -64,15 +101,6 @@ function extractSessionId(value) {
|
|
|
64
101
|
}
|
|
65
102
|
return undefined;
|
|
66
103
|
}
|
|
67
|
-
/** JSON.parse that returns undefined instead of throwing. */
|
|
68
|
-
function tryParseJson(raw) {
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(raw);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
104
|
/**
|
|
77
105
|
* Normalize a raw gemini run result into `{ text, sessionId? }`.
|
|
78
106
|
* See the module doc for the three stdout shapes handled.
|
|
@@ -96,10 +96,6 @@ export const VALID_HARNESS_IDS = Object.freeze(HARNESS_REGISTRY.map((h) => h.id)
|
|
|
96
96
|
export const SESSION_LOG_HARNESSES = HARNESS_REGISTRY.filter(isSessionLogHarness);
|
|
97
97
|
/** Harnesses that can be dispatched as an agent CLI / SDK. */
|
|
98
98
|
export const AGENT_DISPATCH_HARNESSES = HARNESS_REGISTRY.filter((h) => h.capabilities.agentDispatch);
|
|
99
|
-
/** Harnesses that can import an existing harness config into akm. */
|
|
100
|
-
export const CONFIG_IMPORTER_HARNESSES = HARNESS_REGISTRY.filter((h) => h.capabilities.configImport);
|
|
101
|
-
/** Harnesses that participate in `akm setup` detection. */
|
|
102
|
-
export const DETECTION_HARNESSES = HARNESS_REGISTRY.filter((h) => h.capabilities.detection);
|
|
103
99
|
/**
|
|
104
100
|
* Resolve an exact harness id to its descriptor, or `undefined` if unknown.
|
|
105
101
|
*/
|
|
@@ -1,15 +1,56 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* OpenHands CLI result extractor (P2, plan §"The adapter contract" step 3 /
|
|
6
|
+
* §"Structured-output normalization", tier "native-json").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes one raw {@link AgentRunResult} from a headless
|
|
9
|
+
* `openhands --headless --json` run into `{ text, sessionId? }` — the
|
|
10
|
+
* {@link AgentResultExtraction} seam. The engine's shared schema-validation /
|
|
11
|
+
* retry-until-valid loop runs *after* this; the extractor only strips
|
|
12
|
+
* transport framing.
|
|
13
|
+
*
|
|
14
|
+
* With `--json` OpenHands emits ONE JSON event per stdout line — its
|
|
15
|
+
* action/observation event stream, e.g.:
|
|
16
|
+
*
|
|
17
|
+
* {"id":0,"source":"user","action":"message","args":{"content":"<task>"},"message":"<task>"}
|
|
18
|
+
* {"id":1,"source":"agent","action":"run","args":{"command":"ls"},"message":"Running command: ls"}
|
|
19
|
+
* {"id":2,"source":"agent","observation":"run","content":"README.md","message":"Command `ls` executed."}
|
|
20
|
+
* {"id":3,"source":"agent","action":"message","args":{"content":"<answer>"},"message":"<answer>"}
|
|
21
|
+
* {"id":4,"source":"agent","action":"finish","args":{"final_thought":"<answer>","outputs":{}},"message":"..."}
|
|
22
|
+
*
|
|
23
|
+
* Extraction rules ("parse JSONL final message" per the capability matrix):
|
|
24
|
+
* - text: the LAST agent-sourced *message-bearing* event wins. Only two
|
|
25
|
+
* event kinds carry the agent's answer: `action:"message"`
|
|
26
|
+
* (`args.content`, falling back to the top-level `message` mirror) and
|
|
27
|
+
* `action:"finish"` (`args.final_thought`/`args.thought`, falling back to
|
|
28
|
+
* `message`). User echoes, observations, and tool actions (`run`, `edit`,
|
|
29
|
+
* …, whose `message` is progress noise like "Running command: ls") never
|
|
30
|
+
* contribute.
|
|
31
|
+
* - sessionId: the FIRST id-bearing event supplies it (`session_id`,
|
|
32
|
+
* `sessionId`, `sid`, `conversation_id`), falling back to any sessionId
|
|
33
|
+
* the spawn layer already attached. Stored opportunistically on the unit
|
|
34
|
+
* row; per the matrix OpenHands resume is workspace-state, so akm never
|
|
35
|
+
* depends on it (plan §"Session, MCP, and identity across harnesses").
|
|
36
|
+
*
|
|
37
|
+
* Also handled, so version churn stays contained in this one file (per the
|
|
38
|
+
* adapter contract):
|
|
39
|
+
* - a single whole-stdout JSON document (or a spawn-layer `result.parsed`
|
|
40
|
+
* value) is interpreted with the same rules;
|
|
41
|
+
* - plain text (no `--json`, or an unrecognized envelope) passes through
|
|
42
|
+
* trimmed — the engine's embedded-JSON parsing still runs downstream for
|
|
43
|
+
* schema units;
|
|
44
|
+
* - non-JSON banner lines interleaved in the stream are skipped.
|
|
45
|
+
*
|
|
46
|
+
* Registered: `openhandsResultExtractor` is `OpenhandsHarness.resultExtractor`
|
|
47
|
+
* (`./index.ts`); the engine resolves it via `getHarness()` in
|
|
48
|
+
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
|
|
49
|
+
* schema validation.
|
|
50
|
+
*/
|
|
51
|
+
import { asNonEmptyString, isRecord, tryParseJson } from "../../../core/common.js";
|
|
7
52
|
/** Keys that may carry the harness-native session id, in precedence order. */
|
|
8
53
|
const SESSION_KEYS = ["session_id", "sessionId", "sid", "conversation_id"];
|
|
9
|
-
/** Return a non-empty string, else undefined. */
|
|
10
|
-
function nonEmptyString(value) {
|
|
11
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
12
|
-
}
|
|
13
54
|
/**
|
|
14
55
|
* Extract the agent's answer text from one parsed OpenHands event, if the
|
|
15
56
|
* event is one of the two message-bearing kinds (see module doc). Everything
|
|
@@ -25,31 +66,22 @@ function extractAgentText(event) {
|
|
|
25
66
|
return undefined;
|
|
26
67
|
const args = isRecord(event.args) ? event.args : undefined;
|
|
27
68
|
if (event.action === "message") {
|
|
28
|
-
return
|
|
69
|
+
return asNonEmptyString(args?.content) ?? asNonEmptyString(event.message);
|
|
29
70
|
}
|
|
30
71
|
if (event.action === "finish") {
|
|
31
|
-
return
|
|
72
|
+
return asNonEmptyString(args?.final_thought) ?? asNonEmptyString(args?.thought) ?? asNonEmptyString(event.message);
|
|
32
73
|
}
|
|
33
74
|
return undefined;
|
|
34
75
|
}
|
|
35
76
|
/** Extract a harness-native session id from one parsed JSON value, if any. */
|
|
36
77
|
function extractSessionId(event) {
|
|
37
78
|
for (const key of SESSION_KEYS) {
|
|
38
|
-
const candidate =
|
|
79
|
+
const candidate = asNonEmptyString(event[key]);
|
|
39
80
|
if (candidate)
|
|
40
81
|
return candidate;
|
|
41
82
|
}
|
|
42
83
|
return undefined;
|
|
43
84
|
}
|
|
44
|
-
/** JSON.parse that returns undefined instead of throwing. */
|
|
45
|
-
function tryParseJson(raw) {
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(raw);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
85
|
/**
|
|
54
86
|
* Normalize a raw openhands run result into `{ text, sessionId? }`.
|
|
55
87
|
* See the module doc for the stdout shapes handled.
|
|
@@ -1,9 +1,51 @@
|
|
|
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
|
-
|
|
6
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Pi coding-agent CLI result extractor (P2, plan §"The adapter contract"
|
|
6
|
+
* step 3 / §"Structured-output normalization", tier "native-json").
|
|
7
|
+
*
|
|
8
|
+
* Normalizes one raw {@link AgentRunResult} from a headless `pi` run into
|
|
9
|
+
* `{ text, sessionId? }` — the {@link AgentResultExtraction} seam. The
|
|
10
|
+
* engine's shared schema-validation / retry-until-valid loop runs *after*
|
|
11
|
+
* this; the extractor only strips transport framing.
|
|
12
|
+
*
|
|
13
|
+
* With `--mode json` (the capability matrix's structured mode) Pi emits ONE
|
|
14
|
+
* JSON event per stdout line — the agent-session event stream, e.g.:
|
|
15
|
+
*
|
|
16
|
+
* {"type":"session_start","session_id":"<id>"}
|
|
17
|
+
* {"type":"agent_start"}
|
|
18
|
+
* {"type":"message_start","message":{"role":"assistant","content":[]}}
|
|
19
|
+
* {"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"<text>"}]}}
|
|
20
|
+
* {"type":"agent_end","messages":[{"role":"assistant","content":[...]}]}
|
|
21
|
+
*
|
|
22
|
+
* Extraction rules:
|
|
23
|
+
* - text: the LAST assistant text-bearing event wins (`message_end` bodies;
|
|
24
|
+
* an `agent_end` transcript's final assistant message; tolerant flat
|
|
25
|
+
* `role:"assistant"` events). User-role echoes / tool events never
|
|
26
|
+
* contribute. Assistant content blocks are flattened; non-text blocks
|
|
27
|
+
* (thinking, tool use) are skipped.
|
|
28
|
+
* - sessionId: the FIRST session-id-bearing event supplies it (snake_case /
|
|
29
|
+
* camelCase variants, plus `id` on `session*`-typed events), falling back
|
|
30
|
+
* to any sessionId the spawn layer already attached. Stored
|
|
31
|
+
* opportunistically on the unit row for `--session <id>` resume; akm
|
|
32
|
+
* never depends on it (plan §"Session, MCP, and identity").
|
|
33
|
+
*
|
|
34
|
+
* Also handled, so version churn stays contained in this one file (per the
|
|
35
|
+
* adapter contract):
|
|
36
|
+
* - a single whole-stdout JSON document (or a spawn-layer `result.parsed`
|
|
37
|
+
* value) is interpreted with the same rules;
|
|
38
|
+
* - plain text (no `--mode json`, or an unrecognized envelope) passes
|
|
39
|
+
* through trimmed — the engine's embedded-JSON parsing still runs
|
|
40
|
+
* downstream for schema units;
|
|
41
|
+
* - non-JSON banner lines interleaved in the stream are skipped.
|
|
42
|
+
*
|
|
43
|
+
* Registered: `piResultExtractor` is `PiHarness.resultExtractor`
|
|
44
|
+
* (`./index.ts`); the engine resolves it via `getHarness()` in
|
|
45
|
+
* `workflows/exec/native-executor.ts` to normalize agent-unit stdout before
|
|
46
|
+
* schema validation.
|
|
47
|
+
*/
|
|
48
|
+
import { isRecord, tryParseJson } from "../../../core/common.js";
|
|
7
49
|
/** Keys that may carry the harness-native session id, in precedence order. */
|
|
8
50
|
const SESSION_KEYS = ["session_id", "sessionId", "session"];
|
|
9
51
|
/**
|
|
@@ -73,15 +115,6 @@ function extractSessionId(event) {
|
|
|
73
115
|
}
|
|
74
116
|
return undefined;
|
|
75
117
|
}
|
|
76
|
-
/** JSON.parse that returns undefined instead of throwing. */
|
|
77
|
-
function tryParseJson(raw) {
|
|
78
|
-
try {
|
|
79
|
-
return JSON.parse(raw);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
118
|
/**
|
|
86
119
|
* Normalize a raw pi run result into `{ text, sessionId? }`.
|
|
87
120
|
* See the module doc for the stdout shapes handled.
|
|
@@ -186,20 +186,6 @@ export async function writeLockfile(entries) {
|
|
|
186
186
|
release();
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
/** Replace one exact parsed lock generation while the lockfile sentinel is held. */
|
|
190
|
-
export async function compareAndSwapLockfile(expected, desired) {
|
|
191
|
-
const release = await acquireLockSentinel();
|
|
192
|
-
try {
|
|
193
|
-
const current = readLockfileOrThrow();
|
|
194
|
-
if (JSON.stringify(current) !== JSON.stringify(expected))
|
|
195
|
-
return false;
|
|
196
|
-
writeLockfileUnlocked(desired);
|
|
197
|
-
return true;
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
release();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
189
|
/**
|
|
204
190
|
* Publish an update from one exact raw + parsed lockfile generation and return
|
|
205
191
|
* the exact bytes that were written. Formatting-only concurrent edits are a
|
|
@@ -39,7 +39,6 @@ for (const provider of HARNESSES) {
|
|
|
39
39
|
throw new Error(`[akm] session-log provider "${provider.name}" is not registered as a sessionLogs harness in HARNESS_REGISTRY (src/integrations/harnesses). Add it there.`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
|
|
43
42
|
/**
|
|
44
43
|
* Returns all available session log harnesses for the current machine.
|
|
45
44
|
* Add new harnesses to HARNESSES to support additional agent runtimes.
|
|
@@ -53,83 +52,3 @@ export function normalizeSessionTopic(text) {
|
|
|
53
52
|
return undefined;
|
|
54
53
|
return normalized.slice(0, 60);
|
|
55
54
|
}
|
|
56
|
-
export function aggregateSessionEvents(events) {
|
|
57
|
-
const counts = new Map();
|
|
58
|
-
for (const event of events) {
|
|
59
|
-
const topic = normalizeSessionTopic(event.text);
|
|
60
|
-
if (!topic)
|
|
61
|
-
continue;
|
|
62
|
-
const isFailurePattern = ERROR_PATTERNS.test(topic);
|
|
63
|
-
if (!isFailurePattern)
|
|
64
|
-
continue;
|
|
65
|
-
const existing = counts.get(topic) ?? {
|
|
66
|
-
count: 0,
|
|
67
|
-
isFailurePattern,
|
|
68
|
-
sources: new Set(),
|
|
69
|
-
topic,
|
|
70
|
-
};
|
|
71
|
-
existing.count += 1;
|
|
72
|
-
existing.isFailurePattern = existing.isFailurePattern || isFailurePattern;
|
|
73
|
-
existing.sources.add(event.harness);
|
|
74
|
-
counts.set(topic, existing);
|
|
75
|
-
}
|
|
76
|
-
return [...counts.values()]
|
|
77
|
-
.filter((entry) => entry.count >= 2)
|
|
78
|
-
.sort((a, b) => b.count - a.count || a.topic.localeCompare(b.topic))
|
|
79
|
-
.slice(0, 15)
|
|
80
|
-
.map((entry) => ({
|
|
81
|
-
topic: entry.topic,
|
|
82
|
-
frequency: entry.count,
|
|
83
|
-
source: [...entry.sources].sort().join(","),
|
|
84
|
-
isFailurePattern: entry.isFailurePattern,
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Collect normalized session events from a set of harnesses for the health
|
|
89
|
-
* candidate scan (#568).
|
|
90
|
-
*
|
|
91
|
-
* Pipeline selection per harness:
|
|
92
|
-
* - Drive the richer `listSessions()` + `readSession()` pipeline. `readSession` flattens
|
|
93
|
-
* structured content — tool calls, assistant content blocks, thinking,
|
|
94
|
-
* tool_result — into event text (e.g. ClaudeCodeProvider's `parseClaudeEvent`
|
|
95
|
-
* surfaces `[tool:*]` / `[tool_result]` blocks that the legacy flat
|
|
96
|
-
* `readEvents` scan drops entirely). This is what lets health advisories see
|
|
97
|
-
* repeated tool failures / long runs that the flat scan hid.
|
|
98
|
-
*
|
|
99
|
-
* Extracted as a pure function (harnesses injected) so it is unit-testable
|
|
100
|
-
* without touching the real on-disk session-log locations.
|
|
101
|
-
*
|
|
102
|
-
* `maxSessionsPerHarness` bounds the path: `readSession()` reads each
|
|
103
|
-
* session file in full. On a machine with a
|
|
104
|
-
* deep `~/.claude/projects` history a 30-day window can hold hundreds of
|
|
105
|
-
* multi-MB session files, and reading+parsing every one in full made the
|
|
106
|
-
* health command (`akm health`, which calls this synchronously) blow past its
|
|
107
|
-
* latency budget. `listSessions()` returns summaries sorted newest-first, so
|
|
108
|
-
* capping to the most-recent N sessions per harness keeps the richer signal
|
|
109
|
-
* for what actually matters (recent activity) while bounding cost.
|
|
110
|
-
*/
|
|
111
|
-
const DEFAULT_MAX_SESSIONS_PER_HARNESS = 50;
|
|
112
|
-
export function collectSessionEvents(harnesses, sinceMs, maxSessionsPerHarness = DEFAULT_MAX_SESSIONS_PER_HARNESS) {
|
|
113
|
-
const events = [];
|
|
114
|
-
for (const harness of harnesses) {
|
|
115
|
-
try {
|
|
116
|
-
// Enumerate sessions cheaply, then read each one's full structured event
|
|
117
|
-
// stream. There is no parallel flat-log parser.
|
|
118
|
-
const summaries = harness.listSessions({ sinceMs });
|
|
119
|
-
// summaries are newest-first; bound the full-file reads (see doc above).
|
|
120
|
-
for (const summary of summaries.slice(0, maxSessionsPerHarness)) {
|
|
121
|
-
try {
|
|
122
|
-
const session = harness.readSession(summary);
|
|
123
|
-
events.push(...session.events);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// a single unreadable session is non-fatal
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// individual harness failures are non-fatal
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return events;
|
|
135
|
-
}
|
package/dist/llm/client.js
CHANGED
|
@@ -423,18 +423,6 @@ async function chatCompletionAttemptOnce(config, messages, options, timeoutMs, i
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
// ── Availability check ──────────────────────────────────────────────────────
|
|
426
|
-
/**
|
|
427
|
-
* Check if the LLM endpoint is reachable.
|
|
428
|
-
*/
|
|
429
|
-
export async function isLlmAvailable(config) {
|
|
430
|
-
try {
|
|
431
|
-
const result = await chatCompletion(config, [{ role: "user", content: "Respond with just the word: ok" }]);
|
|
432
|
-
return result.length > 0;
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
return false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
426
|
// ── Reachability probe ──────────────────────────────────────────────────────
|
|
439
427
|
/**
|
|
440
428
|
* Best-effort reachability check with an error message, for setup's optional
|