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.
Files changed (326) hide show
  1. package/CHANGELOG.md +663 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/help/help-improve.md +9 -6
  6. package/dist/assets/hints/cli-hints-full.md +6 -5
  7. package/dist/assets/profiles/default.json +9 -4
  8. package/dist/assets/profiles/frequent.json +1 -1
  9. package/dist/assets/profiles/memory-focus.json +1 -1
  10. package/dist/assets/profiles/proactive-maintenance.json +25 -0
  11. package/dist/assets/profiles/quick.json +1 -1
  12. package/dist/assets/profiles/recombine-only.json +21 -0
  13. package/dist/assets/profiles/reflect-distill.json +30 -0
  14. package/dist/assets/profiles/synthesize.json +15 -0
  15. package/dist/assets/profiles/thorough.json +1 -1
  16. package/dist/assets/prompts/consolidate-system.md +23 -0
  17. package/dist/assets/prompts/contradiction-judge.md +33 -0
  18. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  19. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  20. package/dist/assets/prompts/extract-session.md +11 -3
  21. package/dist/assets/prompts/graph-extract-system.md +1 -0
  22. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  23. package/dist/assets/prompts/memory-infer-system.md +1 -0
  24. package/dist/assets/prompts/memory-infer-user.md +5 -0
  25. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  26. package/dist/assets/prompts/procedural-system.md +44 -0
  27. package/dist/assets/prompts/recombine-system.md +40 -0
  28. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  29. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  39. package/dist/assets/templates/html/health.html +281 -111
  40. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  41. package/dist/assets/wiki/schema-template.md +4 -4
  42. package/dist/cli/clack.js +56 -0
  43. package/dist/cli/config-migrate.js +7 -1
  44. package/dist/cli/confirm.js +1 -1
  45. package/dist/cli/parse-args.js +46 -1
  46. package/dist/cli/shared.js +28 -0
  47. package/dist/cli.js +25 -14
  48. package/dist/commands/agent/agent-dispatch.js +3 -2
  49. package/dist/commands/agent/agent-support.js +0 -7
  50. package/dist/commands/agent/contribute-cli.js +26 -7
  51. package/dist/commands/config-cli.js +26 -13
  52. package/dist/commands/env/child-env.js +47 -0
  53. package/dist/commands/env/env-cli.js +220 -227
  54. package/dist/commands/env/env.js +14 -67
  55. package/dist/commands/env/secret-cli.js +140 -138
  56. package/dist/commands/feedback-cli.js +153 -147
  57. package/dist/commands/graph/graph-cli.js +5 -13
  58. package/dist/commands/graph/graph.js +76 -72
  59. package/dist/commands/health/advisories.js +151 -0
  60. package/dist/commands/health/checks.js +103 -16
  61. package/dist/commands/health/html-report.js +447 -81
  62. package/dist/commands/health/improve-metrics.js +771 -0
  63. package/dist/commands/health/llm-usage.js +65 -0
  64. package/dist/commands/health/md-report.js +103 -0
  65. package/dist/commands/health/metrics.js +278 -0
  66. package/dist/commands/health/stash-exposure.js +46 -0
  67. package/dist/commands/health/surfaces.js +216 -0
  68. package/dist/commands/health/task-runs.js +135 -0
  69. package/dist/commands/health/types.js +26 -0
  70. package/dist/commands/health/windows.js +195 -0
  71. package/dist/commands/health.js +91 -1083
  72. package/dist/commands/improve/anti-collapse.js +170 -0
  73. package/dist/commands/improve/calibration.js +161 -0
  74. package/dist/commands/improve/collapse-detector.js +421 -0
  75. package/dist/commands/improve/consolidate/chunking.js +141 -0
  76. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  77. package/dist/commands/improve/consolidate/merge.js +145 -0
  78. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  79. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  80. package/dist/commands/improve/consolidate.js +1313 -1278
  81. package/dist/commands/improve/dedup.js +482 -0
  82. package/dist/commands/improve/distill/content-repair.js +202 -0
  83. package/dist/commands/improve/distill/promote-memory.js +229 -0
  84. package/dist/commands/improve/distill/quality-gate.js +236 -0
  85. package/dist/commands/improve/distill-guards.js +127 -0
  86. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  87. package/dist/commands/improve/distill.js +243 -599
  88. package/dist/commands/improve/eligibility.js +434 -0
  89. package/dist/commands/improve/encoding-salience.js +205 -0
  90. package/dist/commands/improve/extract-cli.js +179 -59
  91. package/dist/commands/improve/extract-prompt.js +55 -4
  92. package/dist/commands/improve/extract-watch.js +140 -0
  93. package/dist/commands/improve/extract.js +409 -43
  94. package/dist/commands/improve/feedback-valence.js +54 -0
  95. package/dist/commands/improve/hot-probation.js +45 -0
  96. package/dist/commands/improve/improve-auto-accept.js +160 -7
  97. package/dist/commands/improve/improve-cli.js +115 -73
  98. package/dist/commands/improve/improve-profiles.js +32 -8
  99. package/dist/commands/improve/improve-result-file.js +15 -25
  100. package/dist/commands/improve/improve-session.js +58 -0
  101. package/dist/commands/improve/improve.js +510 -2537
  102. package/dist/commands/improve/locks.js +154 -0
  103. package/dist/commands/improve/loop-stages.js +1100 -0
  104. package/dist/commands/improve/memory/memory-belief.js +14 -15
  105. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  106. package/dist/commands/improve/memory/memory-improve.js +27 -27
  107. package/dist/commands/improve/outcome-loop.js +270 -0
  108. package/dist/commands/improve/preparation.js +2002 -0
  109. package/dist/commands/improve/proactive-maintenance.js +115 -0
  110. package/dist/commands/improve/procedural.js +398 -0
  111. package/dist/commands/improve/recombine.js +818 -0
  112. package/dist/commands/improve/reflect-noise.js +0 -0
  113. package/dist/commands/improve/reflect.js +212 -45
  114. package/dist/commands/improve/salience.js +455 -0
  115. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  116. package/dist/commands/improve/shared.js +51 -0
  117. package/dist/commands/improve/triage.js +93 -0
  118. package/dist/commands/lint/agent-linter.js +19 -24
  119. package/dist/commands/lint/base-linter.js +173 -60
  120. package/dist/commands/lint/command-linter.js +19 -24
  121. package/dist/commands/lint/env-key-rules.js +38 -1
  122. package/dist/commands/lint/fact-linter.js +39 -0
  123. package/dist/commands/lint/index.js +31 -13
  124. package/dist/commands/lint/memory-linter.js +1 -1
  125. package/dist/commands/lint/registry.js +7 -2
  126. package/dist/commands/lint/task-linter.js +3 -3
  127. package/dist/commands/lint/workflow-linter.js +26 -1
  128. package/dist/commands/observability-cli.js +4 -4
  129. package/dist/commands/proposal/drain-policies.js +13 -4
  130. package/dist/commands/proposal/drain.js +45 -51
  131. package/dist/commands/proposal/legacy-import.js +115 -0
  132. package/dist/commands/proposal/proposal-cli.js +24 -34
  133. package/dist/commands/proposal/proposal.js +7 -1
  134. package/dist/commands/proposal/propose.js +8 -3
  135. package/dist/commands/proposal/repository.js +829 -0
  136. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  137. package/dist/commands/proposal/validators/proposals.js +93 -882
  138. package/dist/commands/read/curate.js +419 -103
  139. package/dist/commands/read/knowledge.js +10 -3
  140. package/dist/commands/read/remember-cli.js +133 -138
  141. package/dist/commands/read/search-cli.js +15 -8
  142. package/dist/commands/read/search.js +22 -11
  143. package/dist/commands/read/show.js +106 -14
  144. package/dist/commands/registry-cli.js +76 -87
  145. package/dist/commands/remember.js +11 -12
  146. package/dist/commands/sources/add-cli.js +91 -95
  147. package/dist/commands/sources/history.js +1 -1
  148. package/dist/commands/sources/init.js +66 -18
  149. package/dist/commands/sources/installed-stashes.js +11 -3
  150. package/dist/commands/sources/schema-repair.js +44 -46
  151. package/dist/commands/sources/self-update.js +2 -2
  152. package/dist/commands/sources/source-add.js +7 -3
  153. package/dist/commands/sources/sources-cli.js +3 -3
  154. package/dist/commands/sources/stash-cli.js +29 -41
  155. package/dist/commands/sources/stash-skeleton.js +57 -8
  156. package/dist/commands/tasks/default-tasks.js +15 -2
  157. package/dist/commands/tasks/tasks-cli.js +20 -29
  158. package/dist/commands/tasks/tasks.js +39 -11
  159. package/dist/commands/wiki-cli.js +23 -38
  160. package/dist/commands/workflow-cli.js +15 -1
  161. package/dist/core/asset/asset-registry.js +3 -1
  162. package/dist/core/asset/asset-spec.js +21 -4
  163. package/dist/core/asset/frontmatter.js +188 -167
  164. package/dist/core/asset/markdown.js +8 -0
  165. package/dist/core/authoring-rules.js +92 -0
  166. package/dist/core/common.js +4 -23
  167. package/dist/core/concurrent.js +10 -1
  168. package/dist/core/config/config-io.js +10 -1
  169. package/dist/core/config/config-migration.js +18 -40
  170. package/dist/core/config/config-schema.js +389 -58
  171. package/dist/core/config/config-types.js +3 -3
  172. package/dist/core/config/config.js +67 -22
  173. package/dist/core/deep-merge.js +38 -0
  174. package/dist/core/errors.js +1 -0
  175. package/dist/core/eval/rank-metrics.js +113 -0
  176. package/dist/core/events.js +4 -7
  177. package/dist/core/improve-types.js +47 -8
  178. package/dist/core/logs-db.js +14 -75
  179. package/dist/core/parse.js +36 -16
  180. package/dist/core/paths.js +21 -18
  181. package/dist/core/standards/resolve-standards-context.js +87 -0
  182. package/dist/core/standards/resolve-stash-standards.js +99 -0
  183. package/dist/core/standards/resolve-type-conventions.js +66 -0
  184. package/dist/core/state/migrations.js +770 -0
  185. package/dist/core/state-db.js +142 -1091
  186. package/dist/core/structured.js +69 -0
  187. package/dist/core/time.js +53 -0
  188. package/dist/core/warn.js +21 -0
  189. package/dist/core/write-source.js +37 -0
  190. package/dist/indexer/db/db.js +356 -780
  191. package/dist/indexer/db/entry-mapper.js +41 -0
  192. package/dist/indexer/db/graph-db.js +129 -86
  193. package/dist/indexer/db/llm-cache.js +2 -2
  194. package/dist/indexer/db/schema.js +516 -0
  195. package/dist/indexer/ensure-index.js +103 -24
  196. package/dist/indexer/feedback/utility-policy.js +75 -0
  197. package/dist/indexer/graph/graph-boost.js +51 -41
  198. package/dist/indexer/graph/graph-extraction.js +207 -4
  199. package/dist/indexer/index-writer-lock.js +106 -0
  200. package/dist/indexer/index-written-assets.js +105 -0
  201. package/dist/indexer/indexer.js +291 -310
  202. package/dist/indexer/passes/dir-staleness.js +114 -0
  203. package/dist/indexer/passes/memory-inference.js +13 -5
  204. package/dist/indexer/passes/metadata.js +20 -0
  205. package/dist/indexer/read-preflight.js +23 -0
  206. package/dist/indexer/search/db-search.js +89 -13
  207. package/dist/indexer/search/fts-query.js +51 -0
  208. package/dist/indexer/search/ranking-contributors.js +95 -9
  209. package/dist/indexer/search/ranking.js +79 -3
  210. package/dist/indexer/search/search-fields.js +6 -0
  211. package/dist/indexer/search/search-source.js +32 -21
  212. package/dist/indexer/search/semantic-status.js +4 -0
  213. package/dist/indexer/walk/matchers.js +9 -0
  214. package/dist/indexer/walk/walker.js +21 -13
  215. package/dist/integrations/agent/builders.js +39 -13
  216. package/dist/integrations/agent/config.js +20 -59
  217. package/dist/integrations/agent/detect.js +9 -0
  218. package/dist/integrations/agent/index.js +3 -19
  219. package/dist/integrations/agent/model-aliases.js +7 -2
  220. package/dist/integrations/agent/profiles.js +7 -1
  221. package/dist/integrations/agent/prompts.js +75 -9
  222. package/dist/integrations/agent/runner-dispatch.js +59 -0
  223. package/dist/integrations/agent/runner.js +13 -9
  224. package/dist/integrations/agent/spawn.js +69 -67
  225. package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
  226. package/dist/integrations/harnesses/claude/index.js +2 -0
  227. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  228. package/dist/integrations/harnesses/index.js +2 -3
  229. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  230. package/dist/integrations/harnesses/opencode/index.js +2 -0
  231. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  232. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  233. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
  234. package/dist/integrations/harnesses/types.js +1 -0
  235. package/dist/integrations/session-logs/index.js +16 -0
  236. package/dist/llm/call-ai.js +2 -2
  237. package/dist/llm/client.js +57 -15
  238. package/dist/llm/embedder.js +67 -4
  239. package/dist/llm/embedders/cache.js +3 -1
  240. package/dist/llm/embedders/deterministic.js +66 -0
  241. package/dist/llm/embedders/local.js +73 -3
  242. package/dist/llm/feature-gate.js +16 -15
  243. package/dist/llm/graph-extract.js +67 -44
  244. package/dist/llm/memory-infer-impl.js +138 -0
  245. package/dist/llm/memory-infer.js +1 -127
  246. package/dist/llm/metadata-enhance.js +44 -31
  247. package/dist/llm/structured-call.js +49 -0
  248. package/dist/migrate-storage-node.mjs +8 -0
  249. package/dist/output/context.js +5 -5
  250. package/dist/output/renderers.js +85 -14
  251. package/dist/output/shapes/curate.js +14 -2
  252. package/dist/output/shapes/helpers.js +0 -3
  253. package/dist/output/shapes/passthrough.js +2 -1
  254. package/dist/output/text/helpers.js +29 -1
  255. package/dist/output/text/workflow.js +1 -0
  256. package/dist/registry/providers/skills-sh.js +21 -147
  257. package/dist/registry/providers/static-index.js +15 -157
  258. package/dist/registry/resolve.js +27 -9
  259. package/dist/runtime.js +25 -1
  260. package/dist/scripts/migrate-storage.js +2718 -2354
  261. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +891 -597
  262. package/dist/setup/detect.js +9 -0
  263. package/dist/setup/legacy-config.js +106 -0
  264. package/dist/setup/prompt.js +57 -0
  265. package/dist/setup/providers.js +14 -0
  266. package/dist/setup/registry-stash-loader.js +12 -0
  267. package/dist/setup/semantic-assets.js +124 -0
  268. package/dist/setup/setup.js +52 -1614
  269. package/dist/setup/steps/connection.js +734 -0
  270. package/dist/setup/steps/output.js +31 -0
  271. package/dist/setup/steps/platforms.js +124 -0
  272. package/dist/setup/steps/semantic.js +27 -0
  273. package/dist/setup/steps/sources.js +222 -0
  274. package/dist/setup/steps/stashdir.js +42 -0
  275. package/dist/setup/steps/tasks.js +152 -0
  276. package/dist/sources/include.js +6 -2
  277. package/dist/sources/providers/filesystem.js +0 -1
  278. package/dist/sources/providers/git-install.js +210 -0
  279. package/dist/sources/providers/git-provider.js +234 -0
  280. package/dist/sources/providers/git-stash.js +248 -0
  281. package/dist/sources/providers/git.js +10 -661
  282. package/dist/sources/providers/npm.js +2 -6
  283. package/dist/sources/providers/provider-utils.js +13 -7
  284. package/dist/sources/providers/sync-from-ref.js +9 -1
  285. package/dist/sources/providers/tar-utils.js +16 -8
  286. package/dist/sources/providers/website.js +9 -5
  287. package/dist/sources/website-ingest.js +187 -29
  288. package/dist/sources/wiki-fetchers/registry.js +53 -0
  289. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  290. package/dist/storage/database.js +45 -10
  291. package/dist/storage/managed-db.js +82 -0
  292. package/dist/storage/repositories/canaries-repository.js +107 -0
  293. package/dist/storage/repositories/consolidation-repository.js +38 -0
  294. package/dist/storage/repositories/embeddings-repository.js +72 -0
  295. package/dist/storage/repositories/events-repository.js +187 -0
  296. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  297. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  298. package/dist/storage/repositories/index-db.js +14 -8
  299. package/dist/storage/repositories/proposals-repository.js +220 -0
  300. package/dist/storage/repositories/recombine-repository.js +213 -0
  301. package/dist/storage/repositories/registry-cache.js +93 -0
  302. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  303. package/dist/storage/repositories/task-history-repository.js +93 -0
  304. package/dist/storage/sqlite-pragmas.js +146 -0
  305. package/dist/tasks/backends/cron.js +1 -1
  306. package/dist/tasks/backends/index.js +9 -0
  307. package/dist/tasks/backends/launchd.js +1 -1
  308. package/dist/tasks/backends/schtasks.js +1 -1
  309. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  310. package/dist/tasks/runner.js +15 -13
  311. package/dist/text-import-hook.mjs +0 -0
  312. package/dist/wiki/wiki.js +52 -11
  313. package/dist/workflows/cli.js +1 -0
  314. package/dist/workflows/db.js +3 -4
  315. package/dist/workflows/runtime/runs.js +43 -118
  316. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  317. package/dist/workflows/validate-summary.js +2 -7
  318. package/docs/README.md +69 -18
  319. package/docs/data-and-telemetry.md +5 -4
  320. package/docs/migration/release-notes/0.7.0.md +1 -1
  321. package/docs/migration/release-notes/0.9.0.md +39 -0
  322. package/package.json +10 -10
  323. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  324. package/dist/commands/db-cli.js +0 -23
  325. package/dist/indexer/db/db-backup.js +0 -376
  326. 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 layout (observed 2026-05):
16
- * <base>/storage/session/<projectId>/<sessionId>.json — metadata
17
- * <base>/storage/message/<sessionId>/<messageId>.json one per message
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 / runAgentSdk)
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, runAgentSdk, runOpencodeSdk } from "./sdk-runner.js";
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
- async function getOrStartServer(profile, llmConfig) {
89
- if (_server)
90
- return _server;
91
- const { createOpencode } = await import("@opencode-ai/sdk").catch(() => {
92
- throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
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 result = timeoutMs === null
186
- ? await promptPromise
187
- : await Promise.race([
188
- promptPromise,
189
- new Promise((resolve) => {
190
- timer = setTimeoutImpl(() => resolve(TIMED_OUT), timeoutMs);
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;
@@ -13,6 +13,7 @@
13
13
  export class BaseHarness {
14
14
  runtimeId;
15
15
  setupDetectionDir;
16
+ agentBuilder;
16
17
  /**
17
18
  * Lowercase prefixes that a decorated v1 profile name may start with and
18
19
  * still belong to this harness (e.g. `["opencode-sdk"]`). The canonical id
@@ -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)
@@ -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 { resolveAgentProfile, runAgent } from "../integrations/agent/index.js";
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 = resolveAgentProfile(defaultAgentName, config.profiles?.agent?.[defaultAgentName]);
17
+ const profile = resolveProfileFromConfig(defaultAgentName, config);
18
18
  if (!profile) {
19
19
  return {
20
20
  ok: false,
@@ -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-size-exceeded condition.
107
- * Mirrors the heuristic in `graph-extract.ts` retrying a context overflow
108
- * cannot shrink the input, so it must not be retried.
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 looksLikeContextOverflow(message) {
119
+ export function isContextSizeError(message) {
111
120
  const lower = message.toLowerCase();
112
- return (lower.includes("context") &&
113
- (lower.includes("context size") ||
114
- lower.includes("context length") ||
115
- lower.includes("context_window") ||
116
- lower.includes("prompt too long") ||
117
- lower.includes("exceeds")));
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 reset
123
- * (ECONNRESET / EPIPE / "fetch failed"). NOT retryable: 4xx, `rate_limited`
124
- * (429), `timeout`, `parse_error`, and context-overflow-classified errors.
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 (looksLikeContextOverflow(err.message))
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") || lower.includes("epipe") || lower.includes("fetch failed");
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 {