akm-cli 0.9.0-beta.6 → 0.9.0-rc.0
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 +663 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -14
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +153 -147
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1083
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1313 -1278
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +243 -599
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +55 -4
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +160 -7
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +32 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +510 -2537
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +212 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +7 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -882
- package/dist/commands/read/curate.js +419 -103
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +29 -41
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +389 -58
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +21 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +142 -1091
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +356 -780
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +103 -24
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +106 -0
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +291 -310
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +57 -15
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2718 -2354
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { openDatabase } from "../../../storage/database.js";
|
|
7
8
|
import { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
|
|
8
9
|
function getOpenCodeBaseDir() {
|
|
9
10
|
if (process.platform === "darwin") {
|
|
@@ -12,20 +13,49 @@ function getOpenCodeBaseDir() {
|
|
|
12
13
|
return path.join(os.homedir(), ".local", "share", "opencode");
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
|
-
* Opencode storage
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Opencode storage layouts:
|
|
17
|
+
*
|
|
18
|
+
* SQLite (current, observed 2026-06): `<base>/opencode.db` — a Drizzle-managed
|
|
19
|
+
* database with `session` / `message` / `part` tables. Message text lives in
|
|
20
|
+
* `part` rows (`data` JSON, `type: "text"`); `message.data` holds role/timing.
|
|
21
|
+
* This is the layout current opencode builds write; it is preferred whenever
|
|
22
|
+
* `opencode.db` exists.
|
|
23
|
+
*
|
|
24
|
+
* JSON files (legacy, observed 2026-05): `<base>/storage/session/<projectId>/
|
|
25
|
+
* <sessionId>.json` (metadata) + `<base>/storage/message/<sessionId>/
|
|
26
|
+
* <messageId>.json` (one per message). Read only when `opencode.db` is absent.
|
|
18
27
|
*
|
|
19
28
|
* Older builds wrote logs directly into `<base>/log/` and `<base>/*.log`;
|
|
20
29
|
* those are still scanned by {@link OpenCodeProvider.readEvents} for
|
|
21
30
|
* backward compatibility with the existing failure-pattern aggregator.
|
|
22
31
|
*/
|
|
32
|
+
/** Filename of opencode's SQLite session store, relative to its base dir. */
|
|
33
|
+
const OPENCODE_DB_FILENAME = "opencode.db";
|
|
23
34
|
export class OpenCodeProvider {
|
|
24
35
|
name = "opencode";
|
|
25
36
|
#baseDir = getOpenCodeBaseDir();
|
|
26
37
|
isAvailable() {
|
|
27
38
|
return fs.existsSync(this.#baseDir);
|
|
28
39
|
}
|
|
40
|
+
/** Absolute path to opencode's SQLite store under `base`. */
|
|
41
|
+
#dbPath(base) {
|
|
42
|
+
return path.join(base, OPENCODE_DB_FILENAME);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Directories/files opencode writes session data under. Returns the base dir
|
|
46
|
+
* when the SQLite store (`opencode.db`) exists, the legacy JSON session root
|
|
47
|
+
* (`<base>/storage/session`) when present, or both during a migration overlap.
|
|
48
|
+
* Empty when neither exists. See {@link SessionLogHarness.watchRoots}.
|
|
49
|
+
*/
|
|
50
|
+
watchRoots() {
|
|
51
|
+
const roots = [];
|
|
52
|
+
if (fs.existsSync(this.#dbPath(this.#baseDir)))
|
|
53
|
+
roots.push(this.#baseDir);
|
|
54
|
+
const sessionRoot = path.join(this.#baseDir, "storage", "session");
|
|
55
|
+
if (fs.existsSync(sessionRoot))
|
|
56
|
+
roots.push(sessionRoot);
|
|
57
|
+
return roots;
|
|
58
|
+
}
|
|
29
59
|
*readEvents(input) {
|
|
30
60
|
// Legacy behavior: stream raw log lines from the top-level dir and `log/`
|
|
31
61
|
// subdirectory. Kept to keep `getExecutionLogCandidates` working without
|
|
@@ -82,6 +112,9 @@ export class OpenCodeProvider {
|
|
|
82
112
|
listSessions(input = {}) {
|
|
83
113
|
const base = input.location ?? this.#baseDir;
|
|
84
114
|
const sinceMs = input.sinceMs ?? 0;
|
|
115
|
+
const dbPath = this.#dbPath(base);
|
|
116
|
+
if (fs.existsSync(dbPath))
|
|
117
|
+
return this.#listSessionsFromDb(dbPath, sinceMs);
|
|
85
118
|
const sessionRoot = path.join(base, "storage", "session");
|
|
86
119
|
if (!fs.existsSync(sessionRoot))
|
|
87
120
|
return [];
|
|
@@ -142,6 +175,8 @@ export class OpenCodeProvider {
|
|
|
142
175
|
return summaries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
|
|
143
176
|
}
|
|
144
177
|
readSession(ref) {
|
|
178
|
+
if (path.basename(ref.filePath) === OPENCODE_DB_FILENAME)
|
|
179
|
+
return this.#readSessionFromDb(ref);
|
|
145
180
|
let meta = {};
|
|
146
181
|
try {
|
|
147
182
|
meta = JSON.parse(fs.readFileSync(ref.filePath, "utf8"));
|
|
@@ -199,6 +234,141 @@ export class OpenCodeProvider {
|
|
|
199
234
|
inlineRefs,
|
|
200
235
|
};
|
|
201
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* List sessions from the SQLite store. `filePath` on each summary is the
|
|
239
|
+
* `opencode.db` path so {@link readSession} can route back to the DB reader.
|
|
240
|
+
* Returns `[]` (never throws) when the DB is unreadable or lacks the expected
|
|
241
|
+
* schema — callers treat a missing harness as "no sessions".
|
|
242
|
+
*/
|
|
243
|
+
#listSessionsFromDb(dbPath, sinceMs) {
|
|
244
|
+
let db;
|
|
245
|
+
try {
|
|
246
|
+
db = openDatabase(dbPath, { readonly: true, create: false });
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const rows = db
|
|
253
|
+
.prepare("SELECT id, title, directory, time_created, time_updated FROM session WHERE time_updated >= ? ORDER BY time_updated DESC")
|
|
254
|
+
.all(sinceMs);
|
|
255
|
+
return rows.map((r) => {
|
|
256
|
+
const startedAt = typeof r.time_created === "number" ? r.time_created : undefined;
|
|
257
|
+
const endedAt = typeof r.time_updated === "number" ? r.time_updated : undefined;
|
|
258
|
+
const title = typeof r.title === "string" && r.title.length > 0 ? r.title : undefined;
|
|
259
|
+
const projectHint = typeof r.directory === "string" && r.directory.length > 0 ? r.directory : undefined;
|
|
260
|
+
return {
|
|
261
|
+
harness: this.name,
|
|
262
|
+
sessionId: r.id,
|
|
263
|
+
filePath: dbPath,
|
|
264
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
265
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
266
|
+
...(projectHint ? { projectHint } : {}),
|
|
267
|
+
...(title ? { title } : {}),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// Missing `session` table / unexpected schema — treat as no sessions.
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
db.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Read one session from the SQLite store. Message text lives in `part` rows
|
|
281
|
+
* (`type: "text"`); `message.data` carries role + timing. One event per
|
|
282
|
+
* message, text-parts concatenated in time order. Returns empty events
|
|
283
|
+
* (never throws) when the DB is unreadable.
|
|
284
|
+
*/
|
|
285
|
+
#readSessionFromDb(ref) {
|
|
286
|
+
const emptyRef = { harness: this.name, sessionId: ref.sessionId, filePath: ref.filePath };
|
|
287
|
+
let db;
|
|
288
|
+
try {
|
|
289
|
+
db = openDatabase(ref.filePath, { readonly: true, create: false });
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return { ref: emptyRef, events: [], inlineRefs: [] };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const meta = db
|
|
296
|
+
.prepare("SELECT title, directory, time_created, time_updated FROM session WHERE id = ?")
|
|
297
|
+
.get(ref.sessionId);
|
|
298
|
+
const startedAt = typeof meta?.time_created === "number" ? meta.time_created : undefined;
|
|
299
|
+
const endedAt = typeof meta?.time_updated === "number" ? meta.time_updated : undefined;
|
|
300
|
+
const title = typeof meta?.title === "string" && meta.title.length > 0 ? meta.title : undefined;
|
|
301
|
+
const projectHint = typeof meta?.directory === "string" && meta.directory.length > 0 ? meta.directory : undefined;
|
|
302
|
+
const messages = db
|
|
303
|
+
.prepare("SELECT id, data, time_created FROM message WHERE session_id = ? ORDER BY time_created ASC, id ASC")
|
|
304
|
+
.all(ref.sessionId);
|
|
305
|
+
const parts = db
|
|
306
|
+
.prepare("SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC")
|
|
307
|
+
.all(ref.sessionId);
|
|
308
|
+
// Group text-part bodies by their parent message.
|
|
309
|
+
const textByMessage = new Map();
|
|
310
|
+
for (const part of parts) {
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(part.data);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (parsed?.type !== "text")
|
|
319
|
+
continue;
|
|
320
|
+
const text = parsed.text;
|
|
321
|
+
if (typeof text !== "string" || text.length < 1)
|
|
322
|
+
continue;
|
|
323
|
+
const bucket = textByMessage.get(part.message_id) ?? [];
|
|
324
|
+
bucket.push(text);
|
|
325
|
+
textByMessage.set(part.message_id, bucket);
|
|
326
|
+
}
|
|
327
|
+
const events = [];
|
|
328
|
+
const inlineRefs = [];
|
|
329
|
+
for (const message of messages) {
|
|
330
|
+
let mdata = {};
|
|
331
|
+
try {
|
|
332
|
+
mdata = JSON.parse(message.data);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// role/timing unavailable — fall through with defaults
|
|
336
|
+
}
|
|
337
|
+
const role = typeof mdata.role === "string" ? mdata.role : "unknown";
|
|
338
|
+
const mtime = mdata.time?.created;
|
|
339
|
+
const ts = typeof mtime === "number"
|
|
340
|
+
? mtime
|
|
341
|
+
: typeof message.time_created === "number"
|
|
342
|
+
? message.time_created
|
|
343
|
+
: undefined;
|
|
344
|
+
const text = (textByMessage.get(message.id) ?? []).join("\n").trim();
|
|
345
|
+
if (text.length < 1)
|
|
346
|
+
continue;
|
|
347
|
+
events.push({ harness: this.name, text, ts, sessionId: ref.sessionId, role, filePath: ref.filePath });
|
|
348
|
+
inlineRefs.push(...extractInlineRefMentions(text, ts));
|
|
349
|
+
}
|
|
350
|
+
events.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
351
|
+
return {
|
|
352
|
+
ref: {
|
|
353
|
+
harness: this.name,
|
|
354
|
+
sessionId: ref.sessionId,
|
|
355
|
+
filePath: ref.filePath,
|
|
356
|
+
...(startedAt !== undefined ? { startedAt } : {}),
|
|
357
|
+
...(endedAt !== undefined ? { endedAt } : {}),
|
|
358
|
+
...(projectHint ? { projectHint } : {}),
|
|
359
|
+
...(title ? { title } : {}),
|
|
360
|
+
},
|
|
361
|
+
events,
|
|
362
|
+
inlineRefs,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return { ref: emptyRef, events: [], inlineRefs: [] };
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
db.close();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
202
372
|
/**
|
|
203
373
|
* Derive opencode base dir from a session metadata file path so a caller
|
|
204
374
|
* passing a custom `--location` can still find the message dir.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* OpenCode SDK harness (#564).
|
|
6
6
|
*
|
|
7
7
|
* Per-harness barrel for the SDK-mode dispatch path:
|
|
8
|
-
* - agent runner → ./sdk-runner.ts (runOpencodeSdk
|
|
8
|
+
* - agent runner → ./sdk-runner.ts (runOpencodeSdk)
|
|
9
9
|
*
|
|
10
10
|
* It also defines {@link OpencodeSdkHarness}, the {@link AkmHarness} descriptor
|
|
11
11
|
* that `HARNESS_REGISTRY` registers.
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* names. Canonical id is `'opencode-sdk'` with no alias.
|
|
18
18
|
*/
|
|
19
19
|
import { BaseHarness } from "../types.js";
|
|
20
|
-
export { closeServer,
|
|
20
|
+
export { closeServer, runOpencodeSdk } from "./sdk-runner.js";
|
|
21
21
|
function caps(c) {
|
|
22
22
|
return {
|
|
23
23
|
sessionLogs: false,
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { resolveSecret } from "../../../core/config/config.js";
|
|
16
16
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
|
|
17
|
+
import { resolveModel } from "../../agent/model-aliases.js";
|
|
17
18
|
// Singleton server — started once per process, reused across calls
|
|
18
19
|
let _server = null;
|
|
19
20
|
/**
|
|
@@ -85,16 +86,23 @@ function toolsToSdkAllowlist(tools) {
|
|
|
85
86
|
out[n] = true;
|
|
86
87
|
return out;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Assemble the OpenCode SDK server config from the profile + LLM fallback.
|
|
91
|
+
* Pure and exported for tests. `profile.model` is resolved through the model
|
|
92
|
+
* alias tables (platform key `"opencode-sdk"`) so config aliases like
|
|
93
|
+
* `"model": "fast"` work on the SDK path the same way they do for CLI
|
|
94
|
+
* builders. Note there is no built-in alias column for `opencode-sdk` —
|
|
95
|
+
* built-in opus/sonnet/haiku strings are CLI-provider-qualified and would
|
|
96
|
+
* collide with the `akm-custom/` provider prefixing below, so only profile
|
|
97
|
+
* and config-root alias tables apply here.
|
|
98
|
+
*/
|
|
99
|
+
export function buildSdkConfig(profile, llmConfig) {
|
|
94
100
|
// Resolve endpoint and model: profile fields take precedence over config.llm
|
|
95
101
|
const endpoint = profile.endpoint ?? llmConfig?.endpoint;
|
|
96
102
|
const apiKey = resolveSecret(profile.apiKey ?? llmConfig?.apiKey);
|
|
97
|
-
const model = profile.model
|
|
103
|
+
const model = profile.model
|
|
104
|
+
? resolveModel(profile.model, "opencode-sdk", profile.modelAliases, profile.globalModelAliases)
|
|
105
|
+
: undefined;
|
|
98
106
|
const sdkConfig = {};
|
|
99
107
|
if (model)
|
|
100
108
|
sdkConfig.model = model;
|
|
@@ -114,6 +122,15 @@ async function getOrStartServer(profile, llmConfig) {
|
|
|
114
122
|
sdkConfig.model = `akm-custom/${model}`;
|
|
115
123
|
}
|
|
116
124
|
}
|
|
125
|
+
return sdkConfig;
|
|
126
|
+
}
|
|
127
|
+
async function getOrStartServer(profile, llmConfig) {
|
|
128
|
+
if (_server)
|
|
129
|
+
return _server;
|
|
130
|
+
const { createOpencode } = await import("@opencode-ai/sdk").catch(() => {
|
|
131
|
+
throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
|
|
132
|
+
});
|
|
133
|
+
const sdkConfig = buildSdkConfig(profile, llmConfig);
|
|
117
134
|
_server = (await createOpencode(Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {}));
|
|
118
135
|
process.once("exit", () => {
|
|
119
136
|
closeServer();
|
|
@@ -122,8 +139,38 @@ async function getOrStartServer(profile, llmConfig) {
|
|
|
122
139
|
throw new Error("Failed to initialise OpenCode SDK server.");
|
|
123
140
|
return _server;
|
|
124
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Extract best-effort token usage from a prompt response. Only numeric
|
|
144
|
+
* fields the server actually reported are copied; returns undefined when
|
|
145
|
+
* nothing usable is present (older servers, test fakes).
|
|
146
|
+
*/
|
|
147
|
+
function extractUsage(info) {
|
|
148
|
+
const tokens = info?.tokens;
|
|
149
|
+
if (!tokens)
|
|
150
|
+
return undefined;
|
|
151
|
+
const usage = {};
|
|
152
|
+
if (typeof tokens.input === "number" && Number.isFinite(tokens.input))
|
|
153
|
+
usage.inputTokens = tokens.input;
|
|
154
|
+
if (typeof tokens.output === "number" && Number.isFinite(tokens.output))
|
|
155
|
+
usage.outputTokens = tokens.output;
|
|
156
|
+
if (typeof tokens.reasoning === "number" && Number.isFinite(tokens.reasoning)) {
|
|
157
|
+
usage.reasoningTokens = tokens.reasoning;
|
|
158
|
+
}
|
|
159
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
160
|
+
}
|
|
125
161
|
export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
126
162
|
const start = Date.now();
|
|
163
|
+
if (opts.signal?.aborted) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
stdout: "",
|
|
167
|
+
stderr: "",
|
|
168
|
+
durationMs: 0,
|
|
169
|
+
exitCode: null,
|
|
170
|
+
reason: "aborted",
|
|
171
|
+
error: `opencode-sdk agent "${profile.name}" not started: caller signal already aborted`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
127
174
|
let client;
|
|
128
175
|
try {
|
|
129
176
|
({ client } = await getOrStartServer(profile, llmConfig));
|
|
@@ -180,16 +227,42 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
180
227
|
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
181
228
|
let timer;
|
|
182
229
|
const TIMED_OUT = Symbol("opencode-sdk-timeout");
|
|
230
|
+
const ABORTED = Symbol("opencode-sdk-aborted");
|
|
231
|
+
// Cooperative cancel: there is no OS process to signal, so an abort simply
|
|
232
|
+
// wins the race below; the finally block reaps the in-flight session, same
|
|
233
|
+
// as the timeout path.
|
|
234
|
+
let onAbort;
|
|
235
|
+
const abortSignal = opts.signal;
|
|
183
236
|
try {
|
|
184
237
|
const promptPromise = client.session.prompt({ path: { id: sessionId }, body });
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
238
|
+
const racers = [promptPromise];
|
|
239
|
+
if (timeoutMs !== null) {
|
|
240
|
+
racers.push(new Promise((resolve) => {
|
|
241
|
+
timer = setTimeoutImpl(() => resolve(TIMED_OUT), timeoutMs);
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
if (abortSignal) {
|
|
245
|
+
racers.push(new Promise((resolve) => {
|
|
246
|
+
onAbort = () => resolve(ABORTED);
|
|
247
|
+
if (abortSignal.aborted)
|
|
248
|
+
onAbort();
|
|
249
|
+
else
|
|
250
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
const result = racers.length === 1 ? await promptPromise : await Promise.race(racers);
|
|
254
|
+
if (result === ABORTED) {
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
stdout: "",
|
|
258
|
+
stderr: "",
|
|
259
|
+
durationMs: Date.now() - start,
|
|
260
|
+
exitCode: null,
|
|
261
|
+
reason: "aborted",
|
|
262
|
+
error: `opencode-sdk agent "${profile.name}" aborted by caller signal`,
|
|
263
|
+
sessionId,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
193
266
|
if (result === TIMED_OUT) {
|
|
194
267
|
return {
|
|
195
268
|
ok: false,
|
|
@@ -199,17 +272,24 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
199
272
|
exitCode: null,
|
|
200
273
|
reason: "timeout",
|
|
201
274
|
error: `opencode-sdk agent "${profile.name}" timed out after ${timeoutMs}ms`,
|
|
275
|
+
sessionId,
|
|
202
276
|
};
|
|
203
277
|
}
|
|
204
278
|
const parts = result.data?.parts ?? [];
|
|
205
279
|
const textPart = parts.find((p) => p.type === "text");
|
|
206
280
|
const stdout = textPart?.text ?? "";
|
|
281
|
+
// Token accounting from the AssistantMessage (previously discarded) —
|
|
282
|
+
// the seam that makes workflow budget.maxTokens meterable on the
|
|
283
|
+
// default sdk runner.
|
|
284
|
+
const usage = extractUsage(result.data?.info);
|
|
207
285
|
return {
|
|
208
286
|
ok: true,
|
|
209
287
|
stdout,
|
|
210
288
|
stderr: "",
|
|
211
289
|
durationMs: Date.now() - start,
|
|
212
290
|
exitCode: 0,
|
|
291
|
+
sessionId,
|
|
292
|
+
...(usage ? { usage } : {}),
|
|
213
293
|
};
|
|
214
294
|
}
|
|
215
295
|
catch (e) {
|
|
@@ -221,14 +301,15 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
|
|
|
221
301
|
exitCode: 1,
|
|
222
302
|
reason: "non_zero_exit",
|
|
223
303
|
error: String(e),
|
|
304
|
+
sessionId,
|
|
224
305
|
};
|
|
225
306
|
}
|
|
226
307
|
finally {
|
|
227
308
|
if (timer !== undefined)
|
|
228
309
|
clearTimeoutImpl(timer);
|
|
310
|
+
if (abortSignal && onAbort)
|
|
311
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
229
312
|
// Clean up session to prevent disk accumulation in ~/.local/share/opencode/
|
|
230
313
|
await client.session.delete({ path: { id: sessionId } }).catch(() => { });
|
|
231
314
|
}
|
|
232
315
|
}
|
|
233
|
-
/** @deprecated Use {@link runOpencodeSdk} instead. */
|
|
234
|
-
export const runAgentSdk = runOpencodeSdk;
|
|
@@ -28,6 +28,22 @@ const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENO
|
|
|
28
28
|
export function getAvailableHarnesses() {
|
|
29
29
|
return HARNESSES.filter((harness) => harness.isAvailable());
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Map each available harness to its `{ harnessName, roots }` watch target,
|
|
33
|
+
* skipping harnesses that expose no roots (absent `watchRoots()` or an empty
|
|
34
|
+
* result). This is the one stable entry point the watcher uses so it never
|
|
35
|
+
* reaches into providers directly.
|
|
36
|
+
*/
|
|
37
|
+
export function getWatchTargets() {
|
|
38
|
+
const targets = [];
|
|
39
|
+
for (const harness of getAvailableHarnesses()) {
|
|
40
|
+
const roots = harness.watchRoots?.() ?? [];
|
|
41
|
+
if (roots.length === 0)
|
|
42
|
+
continue;
|
|
43
|
+
targets.push({ harnessName: harness.name, roots });
|
|
44
|
+
}
|
|
45
|
+
return targets;
|
|
46
|
+
}
|
|
31
47
|
export function normalizeSessionTopic(text) {
|
|
32
48
|
const normalized = text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
33
49
|
if (normalized.length < 10)
|
package/dist/llm/call-ai.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { getDefaultLlmConfig } from "../core/config/config.js";
|
|
5
5
|
import { warn } from "../core/warn.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveProfileFromConfig, runAgent } from "../integrations/agent/index.js";
|
|
7
7
|
import { chatCompletion } from "./client.js";
|
|
8
8
|
/**
|
|
9
9
|
* Unified AI call: prefers the default agent profile, falls back to the
|
|
@@ -14,7 +14,7 @@ export async function callAi(config, prompt, opts = {}) {
|
|
|
14
14
|
const defaultAgentName = config.defaults?.agent;
|
|
15
15
|
if (defaultAgentName) {
|
|
16
16
|
try {
|
|
17
|
-
const profile =
|
|
17
|
+
const profile = resolveProfileFromConfig(defaultAgentName, config);
|
|
18
18
|
if (!profile) {
|
|
19
19
|
return {
|
|
20
20
|
ok: false,
|
package/dist/llm/client.js
CHANGED
|
@@ -103,39 +103,81 @@ function retryBackoffMs() {
|
|
|
103
103
|
return RETRY_BACKOFF_MIN_MS + Math.random() * (RETRY_BACKOFF_MAX_MS - RETRY_BACKOFF_MIN_MS);
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
|
-
* Detect whether an error message indicates a context
|
|
107
|
-
*
|
|
108
|
-
*
|
|
106
|
+
* Detect whether an error message indicates a context size exceeded condition.
|
|
107
|
+
* Covers common patterns from OpenAI-compatible APIs (LM Studio, Ollama, etc).
|
|
108
|
+
*
|
|
109
|
+
* Requires BOTH a context keyword AND token-count/overflow evidence so that
|
|
110
|
+
* model prose merely mentioning "context size" / "context length" (e.g. gemma
|
|
111
|
+
* narrating about a document) does not get misclassified as a provider
|
|
112
|
+
* context-limit error (#496).
|
|
113
|
+
*
|
|
114
|
+
* Canonical home: `graph-extract.ts` re-exports this so the index-pass
|
|
115
|
+
* graph extractor and the retry classifier (`isRetryable`) share one
|
|
116
|
+
* definition — retrying a context overflow cannot shrink the input, so it
|
|
117
|
+
* must never be retried.
|
|
109
118
|
*/
|
|
110
|
-
function
|
|
119
|
+
export function isContextSizeError(message) {
|
|
111
120
|
const lower = message.toLowerCase();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
const contextKw = /context (size|length|window)|prompt too long|exceeds.*context/.test(lower);
|
|
122
|
+
if (!contextKw) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const evidence = /\b\d+\s*(token|tokens|tk)\b/.test(lower) ||
|
|
126
|
+
/max(imum)?\s+(context|token|input)/.test(lower) ||
|
|
127
|
+
/exceeded|over.*limit|too.*long/.test(lower);
|
|
128
|
+
return evidence;
|
|
118
129
|
}
|
|
119
130
|
/**
|
|
120
131
|
* Decide whether a first-attempt {@link LlmCallError} is eligible for a single
|
|
121
132
|
* retry. Retryable: HTTP 5xx (`provider_error` with statusCode >= 500) and
|
|
122
|
-
* `network_error` whose message looks like a transient connection
|
|
123
|
-
*
|
|
124
|
-
*
|
|
133
|
+
* `network_error` whose message looks like a transient connection drop.
|
|
134
|
+
* NOT retryable: 4xx, `rate_limited` (429), `timeout`, `parse_error`, and
|
|
135
|
+
* context-overflow-classified errors.
|
|
136
|
+
*
|
|
137
|
+
* The connection-drop heuristic covers the substrings emitted across runtimes
|
|
138
|
+
* for a mid-flight socket close:
|
|
139
|
+
* - `ECONNRESET` / `EPIPE` — Node/libuv socket reset codes
|
|
140
|
+
* - `fetch failed` — undici's generic wrapper message
|
|
141
|
+
* - `socket connection was closed` — Bun's message for a dropped connection
|
|
142
|
+
* (e.g. "The socket connection was closed unexpectedly.")
|
|
143
|
+
* - `terminated` / `other side closed` — undici's phrasings for the same
|
|
144
|
+
*
|
|
145
|
+
* These all describe a transient transport failure where a second attempt can
|
|
146
|
+
* legitimately succeed, which is exactly the case a single bounded retry is
|
|
147
|
+
* meant to absorb. Before this list was widened, Bun's "socket connection was
|
|
148
|
+
* closed unexpectedly" fell through unretried and surfaced as a recurring
|
|
149
|
+
* failure in the improve/reflect and capability-probe flows.
|
|
125
150
|
*/
|
|
126
151
|
function isRetryable(err) {
|
|
127
|
-
if (
|
|
152
|
+
if (isContextSizeError(err.message))
|
|
128
153
|
return false;
|
|
129
154
|
if (err.code === "provider_error") {
|
|
130
155
|
return typeof err.statusCode === "number" && err.statusCode >= 500;
|
|
131
156
|
}
|
|
132
157
|
if (err.code === "network_error") {
|
|
133
158
|
const lower = err.message.toLowerCase();
|
|
134
|
-
return lower.includes("econnreset") ||
|
|
159
|
+
return (lower.includes("econnreset") ||
|
|
160
|
+
lower.includes("epipe") ||
|
|
161
|
+
lower.includes("fetch failed") ||
|
|
162
|
+
lower.includes("socket connection was closed") ||
|
|
163
|
+
lower.includes("terminated") ||
|
|
164
|
+
lower.includes("other side closed"));
|
|
135
165
|
}
|
|
136
166
|
return false;
|
|
137
167
|
}
|
|
168
|
+
// ── Test seam ────────────────────────────────────────────────────────────────
|
|
169
|
+
// Swap-and-restore override. Inert in production; only tests call the setter.
|
|
170
|
+
let chatCompletionOverride;
|
|
171
|
+
/** TEST-ONLY. Swap the implementation of `chatCompletion`; pass undefined to restore. */
|
|
172
|
+
export function _setChatCompletionForTests(fake) {
|
|
173
|
+
chatCompletionOverride = fake;
|
|
174
|
+
}
|
|
138
175
|
export async function chatCompletion(config, messages, options) {
|
|
176
|
+
if (chatCompletionOverride)
|
|
177
|
+
return chatCompletionOverride(config, messages, options);
|
|
178
|
+
return chatCompletionReal(config, messages, options);
|
|
179
|
+
}
|
|
180
|
+
async function chatCompletionReal(config, messages, options) {
|
|
139
181
|
const effectiveTimeoutMs = options?.timeoutMs ?? config.timeoutMs ?? 120_000;
|
|
140
182
|
const started = Date.now();
|
|
141
183
|
try {
|