akm-cli 0.9.0-beta.9 → 0.9.0-rc.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.
Files changed (381) hide show
  1. package/CHANGELOG.md +715 -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/prompts/workflow-unit-preamble.md +26 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  34. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  35. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  36. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  37. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  38. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  39. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  40. package/dist/assets/templates/html/health.html +281 -111
  41. package/dist/assets/wiki/ingest-workflow-template.md +45 -16
  42. package/dist/assets/wiki/schema-template.md +4 -4
  43. package/dist/cli/clack.js +56 -0
  44. package/dist/cli/config-migrate.js +7 -1
  45. package/dist/cli/confirm.js +1 -1
  46. package/dist/cli/parse-args.js +46 -1
  47. package/dist/cli/shared.js +28 -0
  48. package/dist/cli.js +25 -21
  49. package/dist/commands/agent/agent-dispatch.js +3 -2
  50. package/dist/commands/agent/agent-support.js +0 -7
  51. package/dist/commands/agent/contribute-cli.js +26 -7
  52. package/dist/commands/config-cli.js +26 -13
  53. package/dist/commands/env/child-env.js +47 -0
  54. package/dist/commands/env/env-binding.js +95 -0
  55. package/dist/commands/env/env-cli.js +228 -292
  56. package/dist/commands/env/env.js +14 -67
  57. package/dist/commands/env/secret-cli.js +140 -138
  58. package/dist/commands/feedback-cli.js +156 -155
  59. package/dist/commands/graph/graph-cli.js +5 -13
  60. package/dist/commands/graph/graph.js +3 -3
  61. package/dist/commands/health/advisories.js +151 -0
  62. package/dist/commands/health/checks.js +103 -16
  63. package/dist/commands/health/html-report.js +447 -81
  64. package/dist/commands/health/improve-metrics.js +771 -0
  65. package/dist/commands/health/llm-usage.js +65 -0
  66. package/dist/commands/health/md-report.js +103 -0
  67. package/dist/commands/health/metrics.js +278 -0
  68. package/dist/commands/health/stash-exposure.js +46 -0
  69. package/dist/commands/health/surfaces.js +216 -0
  70. package/dist/commands/health/task-runs.js +135 -0
  71. package/dist/commands/health/types.js +26 -0
  72. package/dist/commands/health/windows.js +195 -0
  73. package/dist/commands/health.js +91 -1091
  74. package/dist/commands/improve/anti-collapse.js +170 -0
  75. package/dist/commands/improve/calibration.js +161 -0
  76. package/dist/commands/improve/collapse-detector.js +421 -0
  77. package/dist/commands/improve/consolidate/chunking.js +141 -0
  78. package/dist/commands/improve/consolidate/eligibility.js +64 -0
  79. package/dist/commands/improve/consolidate/merge.js +145 -0
  80. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  81. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  82. package/dist/commands/improve/consolidate.js +1295 -1277
  83. package/dist/commands/improve/dedup.js +482 -0
  84. package/dist/commands/improve/distill/content-repair.js +202 -0
  85. package/dist/commands/improve/distill/promote-memory.js +229 -0
  86. package/dist/commands/improve/distill/quality-gate.js +236 -0
  87. package/dist/commands/improve/distill-guards.js +127 -0
  88. package/dist/commands/improve/distill-promotion-policy.js +826 -167
  89. package/dist/commands/improve/distill.js +228 -605
  90. package/dist/commands/improve/eligibility.js +434 -0
  91. package/dist/commands/improve/encoding-salience.js +205 -0
  92. package/dist/commands/improve/extract-cli.js +179 -59
  93. package/dist/commands/improve/extract-prompt.js +54 -3
  94. package/dist/commands/improve/extract-watch.js +140 -0
  95. package/dist/commands/improve/extract.js +409 -43
  96. package/dist/commands/improve/feedback-valence.js +54 -0
  97. package/dist/commands/improve/hot-probation.js +45 -0
  98. package/dist/commands/improve/improve-auto-accept.js +157 -10
  99. package/dist/commands/improve/improve-cli.js +115 -73
  100. package/dist/commands/improve/improve-profiles.js +28 -8
  101. package/dist/commands/improve/improve-result-file.js +15 -25
  102. package/dist/commands/improve/improve-session.js +58 -0
  103. package/dist/commands/improve/improve.js +485 -2764
  104. package/dist/commands/improve/locks.js +154 -0
  105. package/dist/commands/improve/loop-stages.js +1100 -0
  106. package/dist/commands/improve/memory/memory-belief.js +14 -15
  107. package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
  108. package/dist/commands/improve/memory/memory-improve.js +27 -27
  109. package/dist/commands/improve/outcome-loop.js +270 -0
  110. package/dist/commands/improve/preparation.js +2002 -0
  111. package/dist/commands/improve/proactive-maintenance.js +37 -35
  112. package/dist/commands/improve/procedural.js +398 -0
  113. package/dist/commands/improve/recombine.js +818 -0
  114. package/dist/commands/improve/reflect-noise.js +0 -0
  115. package/dist/commands/improve/reflect.js +206 -45
  116. package/dist/commands/improve/salience.js +455 -0
  117. package/dist/commands/improve/schema-similarity-gate.js +168 -0
  118. package/dist/commands/improve/shared.js +51 -0
  119. package/dist/commands/improve/triage.js +93 -0
  120. package/dist/commands/lint/agent-linter.js +19 -24
  121. package/dist/commands/lint/base-linter.js +173 -60
  122. package/dist/commands/lint/command-linter.js +19 -24
  123. package/dist/commands/lint/env-key-rules.js +38 -1
  124. package/dist/commands/lint/fact-linter.js +39 -0
  125. package/dist/commands/lint/index.js +31 -13
  126. package/dist/commands/lint/memory-linter.js +1 -1
  127. package/dist/commands/lint/registry.js +7 -2
  128. package/dist/commands/lint/task-linter.js +3 -3
  129. package/dist/commands/lint/workflow-linter.js +26 -1
  130. package/dist/commands/observability-cli.js +4 -4
  131. package/dist/commands/proposal/drain-policies.js +13 -4
  132. package/dist/commands/proposal/drain.js +45 -51
  133. package/dist/commands/proposal/legacy-import.js +115 -0
  134. package/dist/commands/proposal/proposal-cli.js +24 -34
  135. package/dist/commands/proposal/proposal.js +2 -1
  136. package/dist/commands/proposal/propose.js +8 -3
  137. package/dist/commands/proposal/repository.js +829 -0
  138. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  139. package/dist/commands/proposal/validators/proposals.js +93 -895
  140. package/dist/commands/read/curate.js +410 -111
  141. package/dist/commands/read/knowledge.js +10 -3
  142. package/dist/commands/read/remember-cli.js +133 -138
  143. package/dist/commands/read/search-cli.js +15 -8
  144. package/dist/commands/read/search.js +22 -11
  145. package/dist/commands/read/show.js +106 -14
  146. package/dist/commands/registry-cli.js +76 -87
  147. package/dist/commands/remember.js +11 -12
  148. package/dist/commands/sources/add-cli.js +91 -95
  149. package/dist/commands/sources/history.js +1 -1
  150. package/dist/commands/sources/init.js +66 -18
  151. package/dist/commands/sources/installed-stashes.js +11 -3
  152. package/dist/commands/sources/migration-help.js +7 -4
  153. package/dist/commands/sources/schema-repair.js +44 -46
  154. package/dist/commands/sources/self-update.js +2 -2
  155. package/dist/commands/sources/source-add.js +7 -3
  156. package/dist/commands/sources/sources-cli.js +3 -3
  157. package/dist/commands/sources/stash-cli.js +19 -39
  158. package/dist/commands/sources/stash-skeleton.js +57 -8
  159. package/dist/commands/tasks/default-tasks.js +15 -2
  160. package/dist/commands/tasks/tasks-cli.js +20 -29
  161. package/dist/commands/tasks/tasks.js +39 -11
  162. package/dist/commands/wiki-cli.js +23 -38
  163. package/dist/commands/workflow-cli.js +291 -13
  164. package/dist/core/asset/asset-registry.js +3 -1
  165. package/dist/core/asset/asset-spec.js +79 -5
  166. package/dist/core/asset/frontmatter.js +188 -167
  167. package/dist/core/asset/markdown.js +8 -0
  168. package/dist/core/authoring-rules.js +92 -0
  169. package/dist/core/common.js +4 -23
  170. package/dist/core/concurrent.js +10 -1
  171. package/dist/core/config/config-io.js +10 -1
  172. package/dist/core/config/config-migration.js +18 -40
  173. package/dist/core/config/config-schema.js +403 -62
  174. package/dist/core/config/config-types.js +3 -3
  175. package/dist/core/config/config.js +67 -22
  176. package/dist/core/deep-merge.js +38 -0
  177. package/dist/core/errors.js +1 -0
  178. package/dist/core/eval/rank-metrics.js +113 -0
  179. package/dist/core/events.js +4 -7
  180. package/dist/core/improve-types.js +47 -8
  181. package/dist/core/json-schema.js +142 -0
  182. package/dist/core/logs-db.js +14 -75
  183. package/dist/core/parse.js +36 -16
  184. package/dist/core/paths.js +18 -18
  185. package/dist/core/standards/resolve-standards-context.js +87 -0
  186. package/dist/core/standards/resolve-stash-standards.js +99 -0
  187. package/dist/core/standards/resolve-type-conventions.js +66 -0
  188. package/dist/core/state/migrations.js +770 -0
  189. package/dist/core/state-db.js +132 -1126
  190. package/dist/core/structured.js +69 -0
  191. package/dist/core/time.js +53 -0
  192. package/dist/core/warn.js +21 -0
  193. package/dist/core/write-source.js +37 -0
  194. package/dist/indexer/db/db.js +261 -770
  195. package/dist/indexer/db/entry-mapper.js +41 -0
  196. package/dist/indexer/db/graph-db.js +129 -86
  197. package/dist/indexer/db/llm-cache.js +2 -2
  198. package/dist/indexer/db/schema.js +516 -0
  199. package/dist/indexer/ensure-index.js +36 -92
  200. package/dist/indexer/feedback/utility-policy.js +75 -0
  201. package/dist/indexer/graph/graph-boost.js +51 -41
  202. package/dist/indexer/graph/graph-extraction.js +207 -4
  203. package/dist/indexer/index-writer-lock.js +18 -11
  204. package/dist/indexer/index-written-assets.js +105 -0
  205. package/dist/indexer/indexer.js +182 -204
  206. package/dist/indexer/passes/dir-staleness.js +114 -0
  207. package/dist/indexer/passes/memory-inference.js +13 -5
  208. package/dist/indexer/passes/metadata.js +20 -0
  209. package/dist/indexer/read-preflight.js +23 -0
  210. package/dist/indexer/search/db-search.js +89 -13
  211. package/dist/indexer/search/fts-query.js +51 -0
  212. package/dist/indexer/search/ranking-contributors.js +95 -9
  213. package/dist/indexer/search/ranking.js +79 -3
  214. package/dist/indexer/search/search-fields.js +6 -0
  215. package/dist/indexer/search/search-source.js +32 -21
  216. package/dist/indexer/search/semantic-status.js +4 -0
  217. package/dist/indexer/walk/matchers.js +48 -0
  218. package/dist/indexer/walk/walker.js +21 -13
  219. package/dist/integrations/agent/builders.js +41 -13
  220. package/dist/integrations/agent/config.js +20 -59
  221. package/dist/integrations/agent/detect.js +9 -0
  222. package/dist/integrations/agent/index.js +3 -19
  223. package/dist/integrations/agent/model-aliases.js +16 -2
  224. package/dist/integrations/agent/profiles.js +79 -6
  225. package/dist/integrations/agent/prompts.js +75 -9
  226. package/dist/integrations/agent/runner-dispatch.js +83 -0
  227. package/dist/integrations/agent/runner.js +13 -9
  228. package/dist/integrations/agent/spawn.js +206 -81
  229. package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
  230. package/dist/integrations/harnesses/aider/index.js +58 -0
  231. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  232. package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
  233. package/dist/integrations/harnesses/amazonq/index.js +59 -0
  234. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  235. package/dist/integrations/harnesses/claude/agent-builder.js +46 -7
  236. package/dist/integrations/harnesses/claude/index.js +27 -23
  237. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  238. package/dist/integrations/harnesses/claude/session-log.js +10 -0
  239. package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
  240. package/dist/integrations/harnesses/codex/index.js +63 -0
  241. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  242. package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
  243. package/dist/integrations/harnesses/copilot/index.js +60 -0
  244. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  245. package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
  246. package/dist/integrations/harnesses/gemini/index.js +60 -0
  247. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  248. package/dist/integrations/harnesses/index.js +28 -7
  249. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
  250. package/dist/integrations/harnesses/opencode/index.js +17 -16
  251. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  252. package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
  253. package/dist/integrations/harnesses/opencode-sdk/index.js +10 -34
  254. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +642 -71
  255. package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
  256. package/dist/integrations/harnesses/openhands/index.js +58 -0
  257. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  258. package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
  259. package/dist/integrations/harnesses/pi/index.js +58 -0
  260. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  261. package/dist/integrations/harnesses/types.js +8 -0
  262. package/dist/integrations/session-logs/index.js +40 -11
  263. package/dist/llm/call-ai.js +2 -2
  264. package/dist/llm/client.js +34 -11
  265. package/dist/llm/embedder.js +67 -4
  266. package/dist/llm/embedders/cache.js +3 -1
  267. package/dist/llm/embedders/deterministic.js +66 -0
  268. package/dist/llm/embedders/local.js +73 -3
  269. package/dist/llm/feature-gate.js +16 -15
  270. package/dist/llm/graph-extract.js +67 -44
  271. package/dist/llm/memory-infer-impl.js +138 -0
  272. package/dist/llm/memory-infer.js +1 -127
  273. package/dist/llm/metadata-enhance.js +44 -31
  274. package/dist/llm/structured-call.js +49 -0
  275. package/dist/migrate-storage-node.mjs +8 -0
  276. package/dist/output/context.js +5 -5
  277. package/dist/output/renderers.js +87 -15
  278. package/dist/output/shapes/curate.js +14 -2
  279. package/dist/output/shapes/helpers.js +0 -3
  280. package/dist/output/shapes/passthrough.js +6 -1
  281. package/dist/output/text/helpers.js +241 -2
  282. package/dist/output/text/workflow.js +4 -1
  283. package/dist/registry/providers/skills-sh.js +21 -147
  284. package/dist/registry/providers/static-index.js +15 -157
  285. package/dist/registry/resolve.js +27 -9
  286. package/dist/runtime.js +25 -1
  287. package/dist/schemas/akm-config.json +14225 -0
  288. package/dist/schemas/akm-workflow.json +328 -0
  289. package/dist/scripts/migrate-storage.js +2743 -8390
  290. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +1652 -607
  291. package/dist/setup/detect.js +9 -0
  292. package/dist/setup/legacy-config.js +106 -0
  293. package/dist/setup/prompt.js +57 -0
  294. package/dist/setup/providers.js +14 -0
  295. package/dist/setup/registry-stash-loader.js +12 -0
  296. package/dist/setup/semantic-assets.js +124 -0
  297. package/dist/setup/setup.js +52 -1614
  298. package/dist/setup/steps/connection.js +734 -0
  299. package/dist/setup/steps/output.js +31 -0
  300. package/dist/setup/steps/platforms.js +124 -0
  301. package/dist/setup/steps/semantic.js +27 -0
  302. package/dist/setup/steps/sources.js +222 -0
  303. package/dist/setup/steps/stashdir.js +42 -0
  304. package/dist/setup/steps/tasks.js +152 -0
  305. package/dist/sources/include.js +6 -2
  306. package/dist/sources/providers/filesystem.js +0 -1
  307. package/dist/sources/providers/git-install.js +210 -0
  308. package/dist/sources/providers/git-provider.js +234 -0
  309. package/dist/sources/providers/git-stash.js +248 -0
  310. package/dist/sources/providers/git.js +10 -661
  311. package/dist/sources/providers/npm.js +2 -6
  312. package/dist/sources/providers/provider-utils.js +13 -7
  313. package/dist/sources/providers/sync-from-ref.js +9 -1
  314. package/dist/sources/providers/website.js +9 -5
  315. package/dist/sources/website-ingest.js +187 -29
  316. package/dist/sources/wiki-fetchers/registry.js +53 -0
  317. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  318. package/dist/storage/database.js +45 -10
  319. package/dist/storage/managed-db.js +82 -0
  320. package/dist/storage/repositories/canaries-repository.js +107 -0
  321. package/dist/storage/repositories/consolidation-repository.js +38 -0
  322. package/dist/storage/repositories/embeddings-repository.js +72 -0
  323. package/dist/storage/repositories/events-repository.js +187 -0
  324. package/dist/storage/repositories/extract-sessions-repository.js +96 -0
  325. package/dist/storage/repositories/improve-runs-repository.js +146 -0
  326. package/dist/storage/repositories/index-db.js +14 -8
  327. package/dist/storage/repositories/proposals-repository.js +220 -0
  328. package/dist/storage/repositories/recombine-repository.js +213 -0
  329. package/dist/storage/repositories/registry-cache.js +93 -0
  330. package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
  331. package/dist/storage/repositories/task-history-repository.js +93 -0
  332. package/dist/storage/repositories/workflow-runs-repository.js +189 -1
  333. package/dist/storage/sqlite-pragmas.js +146 -0
  334. package/dist/tasks/backends/cron.js +1 -1
  335. package/dist/tasks/backends/index.js +9 -0
  336. package/dist/tasks/backends/launchd.js +1 -1
  337. package/dist/tasks/backends/schtasks.js +1 -1
  338. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  339. package/dist/tasks/runner.js +15 -13
  340. package/dist/text-import-hook.mjs +1 -1
  341. package/dist/wiki/wiki.js +52 -11
  342. package/dist/workflows/authoring/authoring.js +123 -10
  343. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  344. package/dist/workflows/cli.js +5 -0
  345. package/dist/workflows/db.js +138 -4
  346. package/dist/workflows/exec/brief.js +484 -0
  347. package/dist/workflows/exec/native-executor.js +975 -0
  348. package/dist/workflows/exec/param-secrets.js +115 -0
  349. package/dist/workflows/exec/report.js +1295 -0
  350. package/dist/workflows/exec/run-workflow.js +596 -0
  351. package/dist/workflows/exec/scheduler.js +100 -0
  352. package/dist/workflows/exec/step-work.js +1156 -0
  353. package/dist/workflows/exec/unit-writer.js +23 -0
  354. package/dist/workflows/exec/watch.js +116 -0
  355. package/dist/workflows/exec/worktree.js +171 -0
  356. package/dist/workflows/ir/compile.js +388 -0
  357. package/dist/workflows/ir/params.js +54 -0
  358. package/dist/workflows/ir/plan-hash.js +33 -0
  359. package/dist/workflows/ir/schema.js +4 -0
  360. package/dist/workflows/parser.js +3 -1
  361. package/dist/workflows/program/expressions.js +369 -0
  362. package/dist/workflows/program/parser.js +760 -0
  363. package/dist/workflows/program/project.js +105 -0
  364. package/dist/workflows/program/schema.js +54 -0
  365. package/dist/workflows/renderer.js +82 -5
  366. package/dist/workflows/runtime/agent-identity.js +59 -14
  367. package/dist/workflows/runtime/runs.js +248 -153
  368. package/dist/workflows/runtime/unit-checkin.js +45 -0
  369. package/dist/workflows/runtime/workflow-asset-loader.js +188 -0
  370. package/dist/workflows/validate-summary.js +26 -10
  371. package/dist/workflows/validator.js +1 -1
  372. package/docs/README.md +69 -18
  373. package/docs/data-and-telemetry.md +7 -5
  374. package/docs/migration/release-notes/0.7.0.md +1 -1
  375. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  376. package/docs/migration/release-notes/0.9.0.md +39 -0
  377. package/package.json +10 -10
  378. package/dist/assets/tasks/core/update-stashes.yml +0 -4
  379. package/dist/commands/db-cli.js +0 -23
  380. package/dist/indexer/db/db-backup.js +0 -376
  381. package/dist/indexer/passes/staleness-detect.js +0 -488
@@ -11,11 +11,109 @@
11
11
  * This is the runtime surface of the {@link OpencodeSdkHarness} (`id =
12
12
  * 'opencode-sdk'`). It is the dispatch path for `sdkMode` profiles; it exposes
13
13
  * no native session logs of its own (`capabilities.sessionLogs = false`).
14
+ *
15
+ * ## Per-call cwd and env (redesign addendum R2, open seam decision 1)
16
+ *
17
+ * The plan left one decision open: per-call cwd/env forwarding vs a server
18
+ * keyed by `(cwd, envKeysHash)`. Reading the SDK settled it as a SPLIT — the
19
+ * two halves have different API realities (verified against
20
+ * `@opencode-ai/sdk` 1.2.20):
21
+ *
22
+ * - **cwd is PER-CALL.** `session.create` / `session.prompt` /
23
+ * `session.delete` all accept a `query.directory` parameter that scopes
24
+ * the session's working directory, so a single server can host sessions
25
+ * in any number of working directories. {@link RunAgentOptions.cwd} is
26
+ * forwarded as `query: { directory }` on every session call — no
27
+ * per-cwd server processes, no server-key explosion for worktree
28
+ * isolation (which mints a fresh directory per unit attempt).
29
+ *
30
+ * - **env is PER-SERVER (keyed registry).** The SDK exposes NO per-call or
31
+ * per-session env surface; the only way env reaches tool child processes
32
+ * is the `opencode serve` process environment, which
33
+ * `createOpencodeServer` copies from `process.env` **synchronously**
34
+ * (its `spawn` call runs before its first `await`, so the snapshot is
35
+ * taken inside our call frame). {@link getOrStartServer} therefore keys
36
+ * servers by a hash of the FULL env binding entries (keys AND values —
37
+ * two bindings that share keys but differ in values must not share a
38
+ * server), overlays the bindings onto `process.env` for exactly the
39
+ * synchronous prefix of the `createOpencode` call, and restores the
40
+ * previous values before awaiting. JavaScript's single-threaded event
41
+ * loop makes that overlay window atomic: no concurrently-running akm
42
+ * code can observe the mutated environment. Units with the same
43
+ * bindings share one server; units with none share the default server
44
+ * (byte-identical to the pre-R2 singleton behavior).
45
+ *
46
+ * This is what removed the workflow engine's `env_unsupported` hard-fail for
47
+ * the sdk runner: injection genuinely reaches the child, because tool
48
+ * subprocesses (bash etc.) inherit the server process environment.
49
+ *
50
+ * Registry hygiene (peer-review fixes):
51
+ *
52
+ * - **Ports.** `createOpencodeServer` binds a FIXED default port (4096),
53
+ * so coexisting registry entries would contend for the same bind. The
54
+ * default key keeps the SDK default; every env-keyed entry is started on
55
+ * its own OS-assigned free port (see {@link startServer}).
56
+ * - **Shutdown.** {@link closeServer} closes resolved servers
57
+ * SYNCHRONOUSLY — it runs from `process.once('exit')`, where Bun never
58
+ * drains microtasks, so a `.then()`-based close would orphan every
59
+ * `opencode serve` child.
60
+ *
61
+ * Process-lifecycle note (owner finding 4): a cached `opencode serve` child is
62
+ * a live OS handle that keeps Bun's event loop OPEN, so a one-shot CLI never
63
+ * becomes idle and `process.once('exit')` never fires — the exit hook alone
64
+ * cannot free a process the child is keeping alive (a deadlock that hangs the
65
+ * caller after an otherwise-successful run). The registry is therefore drained
66
+ * PROACTIVELY at the end of a dispatching command: the workflow engine calls
67
+ * `disposeDispatchResources()` (→ {@link closeServer}) in its run `finally`.
68
+ * The `process.once('exit')` hook stays as the last-resort backstop for paths
69
+ * that never reach that drain.
70
+ *
71
+ * ## Managed server spawn (owner finding 4, live-harness follow-up)
72
+ *
73
+ * Draining the registry is necessary but NOT sufficient with the SDK's own
74
+ * `createOpencodeServer`: its `close()` merely sends SIGTERM and it never
75
+ * `unref()`s the child or its stdio pipes, so akm's event loop stays pinned
76
+ * until the child ACTUALLY exits — and a real `opencode serve` (a live HTTP
77
+ * server with provider children) can outlive SIGTERM long enough to hang the
78
+ * caller indefinitely. {@link createManagedOpencode} therefore owns the spawn
79
+ * (the SDK package is used only for `createOpencodeClient`):
80
+ *
81
+ * - after the URL handshake, the child and its stdio are `unref()`ed /
82
+ * destroyed, so the handle can never hold akm open;
83
+ * - `close()` sends SIGTERM and arms a bounded grace timer
84
+ * ({@link SERVER_KILL_GRACE_MS}) that escalates to SIGKILL and is cleared
85
+ * on cooperative exit, so stubborn children cannot survive parent exit and
86
+ * stale timers cannot signal a reused PID;
87
+ * - the spawn (and its `process.env` snapshot) stays in the SYNCHRONOUS
88
+ * prefix of the factory call, preserving the env-overlay contract above.
14
89
  */
90
+ import { spawn } from "node:child_process";
91
+ import { createHash } from "node:crypto";
15
92
  import { resolveSecret } from "../../../core/config/config.js";
16
93
  import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
17
- // Singleton server started once per process, reused across calls
18
- let _server = null;
94
+ import { resolveModel } from "../../agent/model-aliases.js";
95
+ // Server registry — one server per env-binding signature, started lazily and
96
+ // reused across calls. The default (no env bindings) key is "" and behaves
97
+ // exactly like the pre-R2 process-wide singleton.
98
+ const _servers = new Map();
99
+ // Resolved servers by registry key, mirrored from `_servers` as each start
100
+ // promise settles. This exists so closeServer() can close started servers
101
+ // SYNCHRONOUSLY: it is wired to `process.once('exit')`, and Bun does not
102
+ // drain microtasks scheduled inside 'exit' handlers, so a `.then()`-based
103
+ // close never runs there and would orphan every `opencode serve` child.
104
+ const _resolvedServers = new Map();
105
+ // Listen ports handed to non-default registry entries (see startServer) —
106
+ // tracked so two coexisting servers in this process can never be assigned
107
+ // the same port.
108
+ const _serverPorts = new Map();
109
+ /** The port `createOpencodeServer` binds when none is passed (SDK 1.2.20). */
110
+ const DEFAULT_SDK_PORT = 4096;
111
+ // Test override: when set, every call uses this server (all keys) and no real
112
+ // server is ever started.
113
+ let _testServer = null;
114
+ // Test seam replacing the real `createOpencode` import (see __setServerFactory).
115
+ let _serverFactory = null;
116
+ let _exitHookInstalled = false;
19
117
  /**
20
118
  * Test-only seam: inject a fake {@link SdkServer} so `runOpencodeSdk` can be
21
119
  * exercised without the real `@opencode-ai/sdk` (which would spin up a server).
@@ -24,20 +122,67 @@ let _server = null;
24
122
  * leading underscores mark it as internal.
25
123
  */
26
124
  export function __setTestServer(server) {
27
- _server = server;
125
+ _testServer = server;
28
126
  }
29
127
  /**
30
- * Close the singleton OpenCode SDK server and reset the handle.
31
- * Primarily for use in tests to ensure clean teardown between test runs.
128
+ * Test-only seam: replace the `createOpencode` factory so the env-keyed
129
+ * server registry (module doc, *Per-call cwd and env*) can be exercised
130
+ * without the real SDK. The fake MUST read whatever `process.env` state it
131
+ * cares about in its SYNCHRONOUS prefix — exactly like the real
132
+ * `createOpencodeServer`, whose `spawn` snapshot happens before its first
133
+ * await — because the runner restores the env overlay as soon as the factory
134
+ * call returns its promise. Pass `null` to clear.
135
+ */
136
+ export function __setServerFactory(factory) {
137
+ _serverFactory = factory;
138
+ }
139
+ /**
140
+ * Close every started OpenCode SDK server and reset the registry (and any
141
+ * injected test server). Used by tests for clean teardown between runs and
142
+ * wired to `process.once('exit')` — which is why resolved servers MUST be
143
+ * closed synchronously here: Bun never drains microtasks scheduled inside
144
+ * 'exit' handlers, so a promise-based close would silently orphan the
145
+ * `opencode serve` children (leaking processes AND keeping their ports
146
+ * bound for the next invocation).
32
147
  */
33
148
  export function closeServer() {
149
+ for (const [key, pending] of _servers) {
150
+ const resolved = _resolvedServers.get(key);
151
+ if (resolved) {
152
+ // Synchronous close — safe from the 'exit' hook.
153
+ try {
154
+ resolved.server.close();
155
+ }
156
+ catch {
157
+ /* ignore */
158
+ }
159
+ }
160
+ else {
161
+ // Still starting: close on arrival. This branch can never complete
162
+ // inside the 'exit' hook (no microtasks there), but it keeps
163
+ // mid-start test teardown leak-free.
164
+ pending
165
+ .then((s) => {
166
+ try {
167
+ s.server.close();
168
+ }
169
+ catch {
170
+ /* ignore */
171
+ }
172
+ })
173
+ .catch(() => { });
174
+ }
175
+ }
176
+ _servers.clear();
177
+ _resolvedServers.clear();
178
+ _serverPorts.clear();
34
179
  try {
35
- _server?.server.close();
180
+ _testServer?.server.close();
36
181
  }
37
182
  catch {
38
183
  /* ignore */
39
184
  }
40
- _server = null;
185
+ _testServer = null;
41
186
  }
42
187
  /**
43
188
  * Convert an `AgentDispatchRequest.tools` policy into the SDK's tool-allowlist
@@ -85,16 +230,23 @@ function toolsToSdkAllowlist(tools) {
85
230
  out[n] = true;
86
231
  return out;
87
232
  }
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
- });
233
+ /**
234
+ * Assemble the OpenCode SDK server config from the profile + LLM fallback.
235
+ * Pure and exported for tests. `profile.model` is resolved through the model
236
+ * alias tables (platform key `"opencode-sdk"`) so config aliases like
237
+ * `"model": "fast"` work on the SDK path the same way they do for CLI
238
+ * builders. Note there is no built-in alias column for `opencode-sdk` —
239
+ * built-in opus/sonnet/haiku strings are CLI-provider-qualified and would
240
+ * collide with the `akm-custom/` provider prefixing below, so only profile
241
+ * and config-root alias tables apply here.
242
+ */
243
+ export function buildSdkConfig(profile, llmConfig) {
94
244
  // Resolve endpoint and model: profile fields take precedence over config.llm
95
245
  const endpoint = profile.endpoint ?? llmConfig?.endpoint;
96
246
  const apiKey = resolveSecret(profile.apiKey ?? llmConfig?.apiKey);
97
- const model = profile.model;
247
+ const model = profile.model
248
+ ? resolveModel(profile.model, "opencode-sdk", profile.modelAliases, profile.globalModelAliases)
249
+ : undefined;
98
250
  const sdkConfig = {};
99
251
  if (model)
100
252
  sdkConfig.model = model;
@@ -114,19 +266,375 @@ async function getOrStartServer(profile, llmConfig) {
114
266
  sdkConfig.model = `akm-custom/${model}`;
115
267
  }
116
268
  }
117
- _server = (await createOpencode(Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {}));
118
- process.once("exit", () => {
119
- closeServer();
269
+ return sdkConfig;
270
+ }
271
+ /**
272
+ * Stable key for the env-keyed server registry: sha256 over the SORTED
273
+ * binding entries (keys AND values — see module doc), "" when no bindings.
274
+ */
275
+ function envServerKey(env) {
276
+ if (!env || Object.keys(env).length === 0)
277
+ return "";
278
+ const entries = Object.entries(env).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
279
+ return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
280
+ }
281
+ /**
282
+ * Overlay `env` onto `process.env`, returning a restore function. The
283
+ * overlay is intended to live only for the SYNCHRONOUS prefix of the server
284
+ * factory call (module doc): mutation → factory() → restore happens in one
285
+ * uninterruptible event-loop turn, so no other code observes it.
286
+ */
287
+ function overlayProcessEnv(env) {
288
+ const previous = new Map();
289
+ for (const [key, value] of Object.entries(env)) {
290
+ previous.set(key, process.env[key]);
291
+ process.env[key] = value;
292
+ }
293
+ return () => {
294
+ for (const [key, prior] of previous) {
295
+ if (prior === undefined)
296
+ delete process.env[key];
297
+ else
298
+ process.env[key] = prior;
299
+ }
300
+ };
301
+ }
302
+ /**
303
+ * Ask the OS for a currently-free localhost port (bind :0, read the assigned
304
+ * port, release it). Skips the SDK's fixed default port and any port already
305
+ * handed to another registry entry in this process, so coexisting servers
306
+ * never contend. The probe-then-use gap is the standard free-port race —
307
+ * acceptable here because the failure mode is a clean `spawn_failed` on the
308
+ * next dispatch, not corruption.
309
+ */
310
+ async function allocateFreePort() {
311
+ const { createServer } = await import("node:net");
312
+ const taken = new Set(_serverPorts.values());
313
+ for (let attempt = 0; attempt < 10; attempt++) {
314
+ const port = await new Promise((resolve, reject) => {
315
+ const probe = createServer();
316
+ probe.unref();
317
+ probe.on("error", reject);
318
+ probe.listen(0, "127.0.0.1", () => {
319
+ const address = probe.address();
320
+ probe.close(() => {
321
+ if (address && typeof address === "object")
322
+ resolve(address.port);
323
+ else
324
+ reject(new Error("could not read the probe socket's port"));
325
+ });
326
+ });
327
+ });
328
+ if (port !== DEFAULT_SDK_PORT && !taken.has(port))
329
+ return port;
330
+ }
331
+ throw new Error("could not allocate a free port for the OpenCode SDK server");
332
+ }
333
+ /** Grace between SIGTERM and SIGKILL when closing a managed server child. */
334
+ const SERVER_KILL_GRACE_MS = 2_000;
335
+ /** How long the managed spawn waits for the server's listening handshake. */
336
+ const SERVER_START_TIMEOUT_MS = 5_000;
337
+ // Test seam: override the argv used to spawn the server child ("opencode"
338
+ // plus serve flags by default) so the managed-spawn lifecycle (handshake,
339
+ // unref, SIGTERM→SIGKILL escalation) is testable without the real binary.
340
+ let _serveCommand = null;
341
+ /** Test-only seam: replace the `opencode serve` argv. Pass `null` to clear. */
342
+ export function __setServeCommand(argv) {
343
+ _serveCommand = argv;
344
+ }
345
+ /**
346
+ * Spawn-owning replacement for the SDK's `createOpencode` (module doc,
347
+ * *Managed server spawn*). Mirrors `createOpencodeServer`'s contract — the
348
+ * `spawn` (and its `process.env` snapshot) happens in the SYNCHRONOUS prefix,
349
+ * `OPENCODE_CONFIG_CONTENT` carries the config, the handshake parses the
350
+ * "opencode server listening on <url>" line — but manages the child so its
351
+ * handle can never pin akm's event loop:
352
+ *
353
+ * - handshake success → stdio destroyed, listeners dropped, `proc.unref()`;
354
+ * - `close()` → SIGTERM now, SIGKILL after an unref'ed grace timer;
355
+ * - handshake failure → the child is killed and unref'ed before rejecting.
356
+ */
357
+ async function createManagedOpencode(options) {
358
+ const { createOpencodeClient } = (await import("@opencode-ai/sdk").catch(() => {
359
+ throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
360
+ }));
361
+ const port = options.port ?? DEFAULT_SDK_PORT;
362
+ const argv = _serveCommand ?? ["opencode", "serve", "--hostname=127.0.0.1", `--port=${port}`];
363
+ // Synchronous prefix: the env snapshot (incl. any binding overlay in the
364
+ // caller's frame) is taken HERE, before the first await below.
365
+ const proc = spawn(argv[0], argv.slice(1), {
366
+ env: { ...process.env, OPENCODE_CONFIG_CONTENT: JSON.stringify(options.config ?? {}) },
367
+ stdio: ["ignore", "pipe", "pipe"],
120
368
  });
121
- if (!_server)
369
+ let closeStarted = false;
370
+ let closeEscalation;
371
+ const childExited = () => proc.exitCode !== null || proc.signalCode !== null;
372
+ const clearCloseEscalation = () => {
373
+ if (closeEscalation !== undefined) {
374
+ clearTimeout(closeEscalation);
375
+ closeEscalation = undefined;
376
+ }
377
+ };
378
+ const closeManaged = () => {
379
+ if (closeStarted)
380
+ return;
381
+ closeStarted = true;
382
+ if (childExited())
383
+ return;
384
+ proc.once("exit", clearCloseEscalation);
385
+ try {
386
+ proc.kill("SIGTERM");
387
+ }
388
+ catch {
389
+ proc.off("exit", clearCloseEscalation);
390
+ return;
391
+ }
392
+ if (childExited())
393
+ return;
394
+ closeEscalation = setTimeout(() => {
395
+ closeEscalation = undefined;
396
+ if (childExited())
397
+ return;
398
+ try {
399
+ proc.kill("SIGKILL");
400
+ }
401
+ catch {
402
+ /* already dead */
403
+ }
404
+ }, SERVER_KILL_GRACE_MS);
405
+ };
406
+ const url = await new Promise((resolve, reject) => {
407
+ let output = "";
408
+ let timer;
409
+ let settled = false;
410
+ const cleanupStartup = () => {
411
+ if (timer !== undefined) {
412
+ clearTimeout(timer);
413
+ timer = undefined;
414
+ }
415
+ proc.stdout?.off("data", onStdoutData);
416
+ proc.stderr?.off("data", onStderrData);
417
+ proc.off("exit", onExit);
418
+ proc.off("error", onError);
419
+ };
420
+ const fail = (err) => {
421
+ if (settled)
422
+ return;
423
+ settled = true;
424
+ cleanupStartup();
425
+ closeManaged();
426
+ proc.stdout?.destroy();
427
+ proc.stderr?.destroy();
428
+ proc.unref();
429
+ reject(err);
430
+ };
431
+ const succeed = (serverUrl) => {
432
+ if (settled)
433
+ return;
434
+ settled = true;
435
+ cleanupStartup();
436
+ resolve(serverUrl);
437
+ };
438
+ const onStdoutData = (chunk) => {
439
+ output += chunk.toString();
440
+ for (const line of output.split("\n")) {
441
+ if (line.startsWith("opencode server listening")) {
442
+ const match = line.match(/on\s+(https?:\/\/\S+)/);
443
+ if (!match?.[1]) {
444
+ fail(new Error(`Failed to parse the OpenCode server url from: ${line}`));
445
+ return;
446
+ }
447
+ succeed(match[1]);
448
+ return;
449
+ }
450
+ }
451
+ };
452
+ const onStderrData = (chunk) => {
453
+ output += chunk.toString();
454
+ };
455
+ const onExit = (code, signal) => {
456
+ const status = code !== null ? `code ${code}` : `signal ${signal ?? "unknown"}`;
457
+ fail(new Error(`OpenCode server exited with ${status}${output.trim() ? `\nServer output: ${output}` : ""}`));
458
+ };
459
+ const onError = (err) => {
460
+ fail(err instanceof Error ? err : new Error(String(err)));
461
+ };
462
+ timer = setTimeout(() => {
463
+ fail(new Error(`Timeout waiting for the OpenCode server to start after ${SERVER_START_TIMEOUT_MS}ms`));
464
+ }, SERVER_START_TIMEOUT_MS);
465
+ proc.stdout?.on("data", onStdoutData);
466
+ proc.stderr?.on("data", onStderrData);
467
+ proc.on("exit", onExit);
468
+ proc.on("error", onError);
469
+ });
470
+ // Handshake done: from here on the child must never hold akm open. Its
471
+ // lifetime is managed explicitly (closeServer → closeManaged), not by the
472
+ // event loop. Destroying the pipes also releases their loop handles.
473
+ proc.stdout?.destroy();
474
+ proc.stderr?.destroy();
475
+ proc.unref();
476
+ return { client: createOpencodeClient({ baseUrl: url }), server: { close: closeManaged } };
477
+ }
478
+ async function startServer(profile, llmConfig, env, registryKey) {
479
+ const factory = _serverFactory ?? createManagedOpencode;
480
+ const sdkConfig = buildSdkConfig(profile, llmConfig);
481
+ const options = Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {};
482
+ // Port discipline: `createOpencodeServer` defaults to a FIXED port (4096),
483
+ // so two coexisting servers — the default one plus any env-keyed one —
484
+ // would contend for the same bind and the second start would fail. The
485
+ // default key keeps the SDK default (byte-identical to the pre-R2
486
+ // singleton); every other registry entry gets its own OS-assigned free
487
+ // port, allocated BEFORE the env overlay below (allocation awaits).
488
+ if (registryKey !== "") {
489
+ const port = await allocateFreePort();
490
+ _serverPorts.set(registryKey, port);
491
+ options.port = port;
492
+ }
493
+ // Env injection (module doc): the SDK's createOpencodeServer snapshots
494
+ // process.env synchronously (its spawn precedes its first await), so the
495
+ // overlay only needs to survive the factory's synchronous prefix. Restore
496
+ // BEFORE awaiting, so nothing else ever runs under the mutated env.
497
+ let pending;
498
+ if (env && Object.keys(env).length > 0) {
499
+ const restore = overlayProcessEnv(env);
500
+ try {
501
+ pending = factory(options);
502
+ }
503
+ finally {
504
+ restore();
505
+ }
506
+ }
507
+ else {
508
+ pending = factory(options);
509
+ }
510
+ const server = await pending;
511
+ if (!server)
122
512
  throw new Error("Failed to initialise OpenCode SDK server.");
123
- return _server;
513
+ if (!_exitHookInstalled) {
514
+ _exitHookInstalled = true;
515
+ process.once("exit", () => {
516
+ closeServer();
517
+ });
518
+ }
519
+ return server;
520
+ }
521
+ /**
522
+ * Get (or lazily start) the server for this call's env bindings. Servers are
523
+ * keyed by {@link envServerKey}; concurrent callers of the same key share one
524
+ * start (the registry stores the in-flight promise). A failed start is
525
+ * evicted so the next call can retry instead of caching the error forever.
526
+ */
527
+ async function getOrStartServer(profile, llmConfig, env) {
528
+ if (_testServer)
529
+ return _testServer;
530
+ const key = envServerKey(env);
531
+ let pending = _servers.get(key);
532
+ if (!pending) {
533
+ pending = startServer(profile, llmConfig, env, key);
534
+ _servers.set(key, pending);
535
+ pending.then((server) => {
536
+ // Mirror into the synchronously-closable registry (see closeServer)
537
+ // — but only while this start is still the live entry (closeServer
538
+ // may have cleared the registry mid-start).
539
+ if (_servers.get(key) === pending)
540
+ _resolvedServers.set(key, server);
541
+ }, () => {
542
+ if (_servers.get(key) === pending) {
543
+ _servers.delete(key);
544
+ _serverPorts.delete(key);
545
+ }
546
+ });
547
+ }
548
+ return pending;
549
+ }
550
+ /**
551
+ * Extract best-effort token usage from a prompt response. Only numeric
552
+ * fields the server actually reported are copied; returns undefined when
553
+ * nothing usable is present (older servers, test fakes).
554
+ */
555
+ function extractUsage(info) {
556
+ const tokens = info?.tokens;
557
+ if (!tokens)
558
+ return undefined;
559
+ const usage = {};
560
+ if (typeof tokens.input === "number" && Number.isFinite(tokens.input))
561
+ usage.inputTokens = tokens.input;
562
+ if (typeof tokens.output === "number" && Number.isFinite(tokens.output))
563
+ usage.outputTokens = tokens.output;
564
+ if (typeof tokens.reasoning === "number" && Number.isFinite(tokens.reasoning)) {
565
+ usage.reasoningTokens = tokens.reasoning;
566
+ }
567
+ return Object.keys(usage).length > 0 ? usage : undefined;
568
+ }
569
+ const SDK_OPERATION_TIMED_OUT = Symbol("opencode-sdk-operation-timeout");
570
+ const SDK_OPERATION_ABORTED = Symbol("opencode-sdk-operation-aborted");
571
+ const SDK_SESSION_DELETE_TIMEOUT_MS = 5_000;
572
+ async function raceSdkOperation(operation, opts) {
573
+ let timer;
574
+ let onAbort;
575
+ const racers = [operation];
576
+ if (opts.timeoutMs !== null) {
577
+ racers.push(new Promise((resolve) => {
578
+ timer = opts.setTimeoutFn(() => resolve(SDK_OPERATION_TIMED_OUT), opts.timeoutMs ?? 0);
579
+ }));
580
+ }
581
+ if (opts.signal) {
582
+ racers.push(new Promise((resolve) => {
583
+ onAbort = () => resolve(SDK_OPERATION_ABORTED);
584
+ if (opts.signal?.aborted)
585
+ onAbort();
586
+ else
587
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
588
+ }));
589
+ }
590
+ try {
591
+ return racers.length === 1 ? await operation : await Promise.race(racers);
592
+ }
593
+ finally {
594
+ if (timer !== undefined)
595
+ opts.clearTimeoutFn(timer);
596
+ if (opts.signal && onAbort)
597
+ opts.signal.removeEventListener("abort", onAbort);
598
+ }
599
+ }
600
+ function errorText(err) {
601
+ return err instanceof Error ? err.message : String(err);
602
+ }
603
+ function appendStderr(stderr, message) {
604
+ return stderr ? `${stderr}\n${message}` : message;
605
+ }
606
+ async function deleteSessionBestEffort(client, sessionId, query, setTimeoutFn, clearTimeoutFn) {
607
+ try {
608
+ const deleted = await raceSdkOperation(client.session.delete({ path: { id: sessionId }, ...(query ? { query } : {}) }), {
609
+ timeoutMs: SDK_SESSION_DELETE_TIMEOUT_MS,
610
+ setTimeoutFn,
611
+ clearTimeoutFn,
612
+ });
613
+ if (deleted === SDK_OPERATION_TIMED_OUT) {
614
+ return `OpenCode session cleanup timed out after ${SDK_SESSION_DELETE_TIMEOUT_MS}ms`;
615
+ }
616
+ return undefined;
617
+ }
618
+ catch (err) {
619
+ return `OpenCode session cleanup failed: ${errorText(err)}`;
620
+ }
124
621
  }
125
622
  export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
126
623
  const start = Date.now();
624
+ if (opts.signal?.aborted) {
625
+ return {
626
+ ok: false,
627
+ stdout: "",
628
+ stderr: "",
629
+ durationMs: 0,
630
+ exitCode: null,
631
+ reason: "aborted",
632
+ error: `opencode-sdk agent "${profile.name}" not started: caller signal already aborted`,
633
+ };
634
+ }
127
635
  let client;
128
636
  try {
129
- ({ client } = await getOrStartServer(profile, llmConfig));
637
+ ({ client } = await getOrStartServer(profile, llmConfig, opts.env));
130
638
  }
131
639
  catch (e) {
132
640
  return {
@@ -139,9 +647,69 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
139
647
  error: String(e),
140
648
  };
141
649
  }
142
- // One session per call do NOT reuse (history accumulates, token costs grow)
143
- const sessionRes = await client.session.create({ body: { title: "akm" } });
144
- const sessionId = sessionRes.data?.id;
650
+ // #564 bug fix (3): enforce a hard timeout like the CLI path (runAgent).
651
+ // Previously runOpencodeSdk() awaited SDK calls with no timeout, so a stalled
652
+ // local-model endpoint or wedged server could block the caller indefinitely.
653
+ // We resolve the same budget runAgent uses (opts.timeoutMs override →
654
+ // profile.timeoutMs → DEFAULT_AGENT_TIMEOUT_MS) and race both session.create
655
+ // and session.prompt against it. null disables the timer (parity with
656
+ // runAgent's "no timeout" contract). Session cleanup is separately bounded
657
+ // by a short best-effort timer so timeout/abort results cannot be pinned in
658
+ // the finally path by a hung delete call.
659
+ const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : (profile.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS);
660
+ const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
661
+ const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
662
+ // Per-call working directory (module doc): forwarded as the SDK's
663
+ // `query.directory` on every session call, so worktree-isolated units run
664
+ // in their own checkout without a per-cwd server.
665
+ const query = opts.cwd ? { directory: opts.cwd } : undefined;
666
+ // One session per call — do NOT reuse (history accumulates, token costs grow).
667
+ // Session creation is startup plumbing, so failures map to spawn_failed rather
668
+ // than bubbling out as a generic workflow dispatch exception.
669
+ const abortSignal = opts.signal;
670
+ let sessionId;
671
+ try {
672
+ const created = await raceSdkOperation(client.session.create({ body: { title: "akm" }, ...(query ? { query } : {}) }), {
673
+ timeoutMs,
674
+ setTimeoutFn: setTimeoutImpl,
675
+ clearTimeoutFn: clearTimeoutImpl,
676
+ signal: abortSignal,
677
+ });
678
+ if (created === SDK_OPERATION_ABORTED) {
679
+ return {
680
+ ok: false,
681
+ stdout: "",
682
+ stderr: "",
683
+ durationMs: Date.now() - start,
684
+ exitCode: null,
685
+ reason: "aborted",
686
+ error: `opencode-sdk agent "${profile.name}" aborted by caller signal`,
687
+ };
688
+ }
689
+ if (created === SDK_OPERATION_TIMED_OUT) {
690
+ return {
691
+ ok: false,
692
+ stdout: "",
693
+ stderr: "",
694
+ durationMs: Date.now() - start,
695
+ exitCode: null,
696
+ reason: "timeout",
697
+ error: `opencode-sdk agent "${profile.name}" timed out creating a session after ${timeoutMs}ms`,
698
+ };
699
+ }
700
+ sessionId = created.data?.id;
701
+ }
702
+ catch (err) {
703
+ return {
704
+ ok: false,
705
+ stdout: "",
706
+ stderr: errorText(err),
707
+ durationMs: Date.now() - start,
708
+ exitCode: 1,
709
+ reason: "spawn_failed",
710
+ error: errorText(err),
711
+ };
712
+ }
145
713
  if (!sessionId) {
146
714
  return {
147
715
  ok: false,
@@ -165,33 +733,28 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
165
733
  body.system = system;
166
734
  if (tools)
167
735
  body.tools = tools;
168
- // #564 bug fix (3): enforce a hard timeout like the CLI path (runAgent).
169
- // Previously runOpencodeSdk() awaited session.prompt() with no timeout, so a
170
- // hung SDK call (e.g. a stalled local-model endpoint) blocked the caller
171
- // indefinitely while the CLI path would have killed the process. We resolve
172
- // the same budget runAgent uses (opts.timeoutMs override → profile.timeoutMs
173
- // → DEFAULT_AGENT_TIMEOUT_MS) and race the prompt against it. null disables
174
- // the timer (parity with runAgent's "no timeout" contract). There is no
175
- // OS process to SIGTERM/SIGKILL here, so on timeout we best-effort delete the
176
- // session (the SDK's equivalent of reaping the in-flight work) and return a
177
- // structured `timeout` failure with the same reason vocabulary as the CLI.
178
- const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : (profile.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS);
179
- const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
180
- const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
181
- let timer;
182
- const TIMED_OUT = Symbol("opencode-sdk-timeout");
736
+ let result;
183
737
  try {
184
- 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
- ]);
193
- if (result === TIMED_OUT) {
194
- return {
738
+ const prompted = await raceSdkOperation(client.session.prompt({ path: { id: sessionId }, body, ...(query ? { query } : {}) }), {
739
+ timeoutMs,
740
+ setTimeoutFn: setTimeoutImpl,
741
+ clearTimeoutFn: clearTimeoutImpl,
742
+ signal: abortSignal,
743
+ });
744
+ if (prompted === SDK_OPERATION_ABORTED) {
745
+ result = {
746
+ ok: false,
747
+ stdout: "",
748
+ stderr: "",
749
+ durationMs: Date.now() - start,
750
+ exitCode: null,
751
+ reason: "aborted",
752
+ error: `opencode-sdk agent "${profile.name}" aborted by caller signal`,
753
+ sessionId,
754
+ };
755
+ }
756
+ else if (prompted === SDK_OPERATION_TIMED_OUT) {
757
+ result = {
195
758
  ok: false,
196
759
  stdout: "",
197
760
  stderr: "",
@@ -199,36 +762,44 @@ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
199
762
  exitCode: null,
200
763
  reason: "timeout",
201
764
  error: `opencode-sdk agent "${profile.name}" timed out after ${timeoutMs}ms`,
765
+ sessionId,
766
+ };
767
+ }
768
+ else {
769
+ const parts = prompted.data?.parts ?? [];
770
+ const textPart = parts.find((p) => p.type === "text");
771
+ const stdout = textPart?.text ?? "";
772
+ // Token accounting from the AssistantMessage (previously discarded) —
773
+ // the seam that makes workflow budget.maxTokens meterable on the
774
+ // default sdk runner.
775
+ const usage = extractUsage(prompted.data?.info);
776
+ result = {
777
+ ok: true,
778
+ stdout,
779
+ stderr: "",
780
+ durationMs: Date.now() - start,
781
+ exitCode: 0,
782
+ sessionId,
783
+ ...(usage ? { usage } : {}),
202
784
  };
203
785
  }
204
- const parts = result.data?.parts ?? [];
205
- const textPart = parts.find((p) => p.type === "text");
206
- const stdout = textPart?.text ?? "";
207
- return {
208
- ok: true,
209
- stdout,
210
- stderr: "",
211
- durationMs: Date.now() - start,
212
- exitCode: 0,
213
- };
214
786
  }
215
- catch (e) {
216
- return {
787
+ catch (err) {
788
+ result = {
217
789
  ok: false,
218
790
  stdout: "",
219
- stderr: String(e),
791
+ stderr: errorText(err),
220
792
  durationMs: Date.now() - start,
221
793
  exitCode: 1,
222
794
  reason: "non_zero_exit",
223
- error: String(e),
795
+ error: errorText(err),
796
+ sessionId,
224
797
  };
225
798
  }
226
- finally {
227
- if (timer !== undefined)
228
- clearTimeoutImpl(timer);
229
- // Clean up session to prevent disk accumulation in ~/.local/share/opencode/
230
- await client.session.delete({ path: { id: sessionId } }).catch(() => { });
231
- }
799
+ // Clean up session to prevent disk accumulation in ~/.local/share/opencode/.
800
+ // Failures are non-fatal to the agent result but must not be invisible.
801
+ const cleanupWarning = await deleteSessionBestEffort(client, sessionId, query, setTimeoutImpl, clearTimeoutImpl);
802
+ if (cleanupWarning)
803
+ result.stderr = appendStderr(result.stderr, cleanupWarning);
804
+ return result;
232
805
  }
233
- /** @deprecated Use {@link runOpencodeSdk} instead. */
234
- export const runAgentSdk = runOpencodeSdk;