oh-my-opencode 5.0.0-beta.31 → 5.0.0-beta.32

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 (227) hide show
  1. package/dist/cli/index.js +136 -44
  2. package/dist/cli-node/index.js +136 -44
  3. package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
  4. package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
  5. package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
  6. package/dist/features/monitor/manager-internals.d.ts +1 -1
  7. package/dist/features/monitor/output-injector-types.d.ts +1 -1
  8. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
  9. package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
  10. package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
  11. package/dist/index.js +286 -99
  12. package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  13. package/dist/tools/session-manager/directory-filter.d.ts +2 -0
  14. package/dist/tui.js +109 -23
  15. package/package.json +16 -15
  16. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
  17. package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
  18. package/packages/lsp-core/src/lsp/client.ts +6 -3
  19. package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
  20. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
  21. package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
  22. package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
  23. package/packages/lsp-core/src/lsp/transport.ts +7 -2
  24. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
  25. package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
  26. package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
  27. package/packages/lsp-daemon/dist/cli.js +41 -16
  28. package/packages/lsp-daemon/dist/client.js +41 -16
  29. package/packages/lsp-daemon/dist/index.js +41 -16
  30. package/packages/lsp-tools-mcp/dist/cli.js +41 -16
  31. package/packages/lsp-tools-mcp/dist/lsp/manager.js +41 -16
  32. package/packages/lsp-tools-mcp/dist/mcp.js +41 -16
  33. package/packages/lsp-tools-mcp/dist/tools.js +41 -16
  34. package/packages/lsp-tools-mcp/package.json +1 -1
  35. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  36. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +3 -17
  39. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +3 -17
  40. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  44. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  46. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  48. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +39 -16
  49. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  52. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  53. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  57. package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
  58. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
  59. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  60. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
  61. package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
  62. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
  63. package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
  64. package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
  65. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
  66. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  67. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +4 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
  72. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
  73. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +7 -5
  76. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +775 -312
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +18 -0
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
  88. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
  89. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
  91. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
  92. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  93. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -99
  94. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
  95. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +184 -14
  96. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
  97. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +3 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +15 -0
  100. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  101. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  102. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
  103. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
  104. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
  105. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
  106. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +8 -12
  107. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
  108. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
  109. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  110. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +18 -0
  111. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
  112. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
  113. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
  114. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
  115. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
  116. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
  117. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
  118. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +50 -116
  119. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
  120. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +186 -12
  121. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +22 -0
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +66 -3
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
  125. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
  126. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +13 -3
  127. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
  128. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
  129. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +16 -15
  130. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
  131. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
  132. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +300 -3
  133. package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
  134. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  135. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  136. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  137. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  138. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  139. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  140. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  141. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  142. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  143. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  144. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  145. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  146. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  147. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  148. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  149. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  150. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  151. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  152. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  153. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  154. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  155. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  156. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  157. package/packages/omo-codex/plugin/package-lock.json +13 -13
  158. package/packages/omo-codex/plugin/package.json +1 -1
  159. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
  160. package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  161. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
  162. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
  163. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
  164. package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
  165. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  166. package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
  167. package/dist/features/builtin-skills/index.d.ts +0 -1
  168. package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
  169. package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
  170. package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
  171. package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
  172. package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
  173. package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
  174. package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
  175. package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
  176. package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
  177. package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
  178. package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
  179. package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
  180. package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
  181. package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
  182. package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
  183. package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
  184. package/dist/features/builtin-skills/skills/index.d.ts +0 -1
  185. package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
  186. package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
  187. package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
  188. package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
  189. package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
  190. package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
  191. package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
  192. package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
  193. package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
  194. package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
  195. package/dist/features/builtin-skills/skills.d.ts +0 -1
  196. package/dist/features/builtin-skills/types.d.ts +0 -1
  197. package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
  198. package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
  199. package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
  200. package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
  201. package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
  202. package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
  203. package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
  204. package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
  205. package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
  206. package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
  207. package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
  208. package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
  209. package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
  210. package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
  211. package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
  212. package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
  213. package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
  214. package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
  215. package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
  216. package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
  217. package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
  218. package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
  219. package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
  220. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
  221. package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
  222. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
  223. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
  224. package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
  225. package/dist/shared/prompt-async-gate/types.d.ts +0 -1
  226. package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
  227. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
@@ -16,7 +16,8 @@
16
16
  1. WebFetch, 즉흥 curl, 수동 헤더 조합 **시도 금지**
17
17
  2. 즉시 다음을 실행:
18
18
  ```bash
19
- python3 -m engine "<URL>" [--selector "<CSS>"] [--device auto|desktop|mobile] [--trace]
19
+ python3 -m engine "<URL>" [--selector "<CSS>"] [--device auto|desktop|mobile] [--trace] \
20
+ [--no-retry] [--no-extract]
20
21
  ```
21
22
  3. 종료코드 0(ok) 또는 1(fail) 받은 뒤 판단. trace를 먼저 읽고 재시도 결정.
22
23
  4. 실패 시에만 `--trace --json`으로 재호출해서 원인 진단 후 `--device` 또는 `user_hint` 조정.
@@ -135,7 +136,13 @@ result = fetch(
135
136
 
136
137
  if result.ok:
137
138
  print(result.verdict) # strong_ok | weak_ok
138
- html = result.content
139
+ html = result.content # raw body — 단, content-rescue가 발동한 경우 구조 텍스트
140
+ # v0.10.0 content-rescue: PDF 응답은 pypdf 추출 텍스트, visible text가 얇은
141
+ # SPA 셸은 JSON-LD articleBody / 렌더된 innerText로 대체될 수 있다.
142
+ # result.extraction_source로 판별: "raw"(원문 그대로) | pdf | json_ld | *+inner_text.
143
+ # 일반 HTML 성공은 항상 raw. 끄기: enable_extraction=False / --no-extract.
144
+ # 429/502/503/504는 probe에서 백오프 재시도(Retry-After 반영, 총 10초 캡);
145
+ # 끄기: enable_retry=False / --no-retry.
139
146
  else:
140
147
  # Phase 3 수동 개입 (Playwright MCP) 필요 — result.trace로 원인 진단
141
148
  pass
@@ -0,0 +1,2 @@
1
+ export declare function normalizeSessionDirectory(directory: string): string;
2
+ export declare function sessionDirectoriesMatch(stored: string, filter: string): boolean;
package/dist/tui.js CHANGED
@@ -17180,11 +17180,7 @@ var init_memory = __esm(() => {
17180
17180
  }).strict();
17181
17181
  OmoMemoryRecallSchema = object({
17182
17182
  enabled: boolean2().default(true),
17183
- max_items: number2().int().min(1).max(5).default(2),
17184
- budget_tokens: number2().int().positive().default(600),
17185
- excerpt_chars: number2().int().positive().default(200),
17186
- min_score: number2().optional(),
17187
- exclude: array(string2()).default([])
17183
+ max_items: number2().int().min(1).max(5).default(2)
17188
17184
  }).strict();
17189
17185
  OmoMemoryNudgeSchema = object({
17190
17186
  enabled: boolean2().default(true),
@@ -17234,11 +17230,7 @@ var init_memory = __esm(() => {
17234
17230
  }).strict();
17235
17231
  OmoMemoryRecallLayerSchema = object({
17236
17232
  enabled: boolean2().optional(),
17237
- max_items: number2().int().min(1).max(5).optional(),
17238
- budget_tokens: number2().int().positive().optional(),
17239
- excerpt_chars: number2().int().positive().optional(),
17240
- min_score: number2().optional(),
17241
- exclude: array(string2()).optional()
17233
+ max_items: number2().int().min(1).max(5).optional()
17242
17234
  }).strict();
17243
17235
  OmoMemoryNudgeLayerSchema = object({
17244
17236
  enabled: boolean2().optional(),
@@ -17309,13 +17301,7 @@ var init_memory = __esm(() => {
17309
17301
  write_notice: OmoMemoryWriteNoticeSchema.default({ enabled: true }),
17310
17302
  sync: OmoMemorySyncSchema.default({ enabled: true }),
17311
17303
  search: OmoMemorySearchSchema.default({ enabled: true }),
17312
- recall: OmoMemoryRecallSchema.default({
17313
- enabled: true,
17314
- max_items: 2,
17315
- budget_tokens: 600,
17316
- excerpt_chars: 200,
17317
- exclude: []
17318
- }),
17304
+ recall: OmoMemoryRecallSchema.default({ enabled: true, max_items: 2 }),
17319
17305
  compile_warn_tokens: number2().int().positive().default(30000),
17320
17306
  agents: record(string2(), OmoMemoryAgentOverridesSchema).default({})
17321
17307
  }).strict();
@@ -20048,6 +20034,111 @@ var init_supplemental_entries = __esm(() => {
20048
20034
  input: 272000,
20049
20035
  output: 128000
20050
20036
  }
20037
+ },
20038
+ "grok-4.5": {
20039
+ id: "grok-4.5",
20040
+ family: "grok",
20041
+ reasoning: false,
20042
+ temperature: true,
20043
+ toolCall: true,
20044
+ modalities: {
20045
+ input: ["text", "image"],
20046
+ output: ["text"]
20047
+ },
20048
+ limit: {
20049
+ context: 500000,
20050
+ output: 32768
20051
+ }
20052
+ },
20053
+ "xai/grok-4.5": {
20054
+ id: "xai/grok-4.5",
20055
+ family: "grok",
20056
+ reasoning: false,
20057
+ temperature: true,
20058
+ toolCall: true,
20059
+ modalities: {
20060
+ input: ["text", "image"],
20061
+ output: ["text"]
20062
+ },
20063
+ limit: {
20064
+ context: 500000,
20065
+ output: 32768
20066
+ }
20067
+ },
20068
+ "x-ai/grok-4.5": {
20069
+ id: "x-ai/grok-4.5",
20070
+ family: "grok",
20071
+ reasoning: false,
20072
+ temperature: true,
20073
+ toolCall: true,
20074
+ modalities: {
20075
+ input: ["text", "image"],
20076
+ output: ["text"]
20077
+ },
20078
+ limit: {
20079
+ context: 500000,
20080
+ output: 32768
20081
+ }
20082
+ },
20083
+ "grok-4.6": {
20084
+ id: "grok-4.6",
20085
+ family: "grok",
20086
+ reasoning: false,
20087
+ temperature: true,
20088
+ toolCall: true,
20089
+ modalities: {
20090
+ input: ["text", "image"],
20091
+ output: ["text"]
20092
+ },
20093
+ limit: {
20094
+ context: 500000,
20095
+ output: 32768
20096
+ }
20097
+ },
20098
+ "xai/grok-4.6": {
20099
+ id: "xai/grok-4.6",
20100
+ family: "grok",
20101
+ reasoning: false,
20102
+ temperature: true,
20103
+ toolCall: true,
20104
+ modalities: {
20105
+ input: ["text", "image"],
20106
+ output: ["text"]
20107
+ },
20108
+ limit: {
20109
+ context: 500000,
20110
+ output: 32768
20111
+ }
20112
+ },
20113
+ "xai/grok-build-0.1": {
20114
+ id: "xai/grok-build-0.1",
20115
+ family: "grok",
20116
+ reasoning: false,
20117
+ temperature: true,
20118
+ toolCall: true,
20119
+ modalities: {
20120
+ input: ["text", "image"],
20121
+ output: ["text"]
20122
+ },
20123
+ limit: {
20124
+ context: 256000,
20125
+ output: 32768
20126
+ }
20127
+ },
20128
+ "x-ai/grok-build-0.1": {
20129
+ id: "x-ai/grok-build-0.1",
20130
+ family: "grok",
20131
+ reasoning: false,
20132
+ temperature: true,
20133
+ toolCall: true,
20134
+ modalities: {
20135
+ input: ["text", "image"],
20136
+ output: ["text"]
20137
+ },
20138
+ limit: {
20139
+ context: 256000,
20140
+ output: 32768
20141
+ }
20051
20142
  }
20052
20143
  };
20053
20144
  });
@@ -78539,14 +78630,9 @@ var init_claude_code_plugin_loader = __esm(() => {
78539
78630
  init_hook_loader();
78540
78631
  });
78541
78632
 
78542
- // packages/omo-opencode/src/features/claude-code-plugin-loader/index.ts
78543
- var init_claude_code_plugin_loader2 = __esm(() => {
78544
- init_claude_code_plugin_loader();
78545
- });
78546
-
78547
78633
  // packages/omo-opencode/src/shared/plugin-command-discovery.ts
78548
78634
  var init_plugin_command_discovery = __esm(() => {
78549
- init_claude_code_plugin_loader2();
78635
+ init_claude_code_plugin_loader();
78550
78636
  });
78551
78637
 
78552
78638
  // packages/omo-opencode/src/shared/session-category-registry.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "5.0.0-beta.31",
3
+ "version": "5.0.0-beta.32",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -113,6 +113,7 @@
113
113
  "install:codex-dev": "bun run script/build-codex-install.ts && bun run script/install-codex-dev.ts",
114
114
  "build:codex-plugin": "npm --prefix packages/omo-codex/plugin ci && bun run --cwd packages/omo-codex/plugin build",
115
115
  "build:senpi-plugin": "bun run build:lsp-daemon && bun run build:ast-grep-mcp && bun run build:senpi-plugin:stage",
116
+ "build:senpi-plugin:native": "node packages/omo-senpi/plugin/scripts/stage-lsp-daemon-runtime.mjs && node packages/omo-senpi/plugin/scripts/stage-ast-grep-mcp-runtime.mjs && node packages/omo-senpi/plugin/scripts/stage-agent-toolkit.mjs && node packages/omo-senpi/plugin/scripts/build-extension.mjs && node packages/omo-senpi/plugin/scripts/sync-skills.mjs && node packages/omo-senpi/plugin/scripts/embed-directive.mjs --check && node packages/omo-senpi/plugin/scripts/build-install.mjs",
116
117
  "build:senpi-plugin:stage": "bun run build:materialize-frontend && node packages/omo-senpi/plugin/scripts/stage-lsp-daemon-runtime.mjs && node packages/omo-senpi/plugin/scripts/stage-ast-grep-mcp-runtime.mjs && node packages/omo-senpi/plugin/scripts/stage-agent-toolkit.mjs && node packages/omo-senpi/plugin/scripts/build-extension.mjs && node packages/omo-senpi/plugin/scripts/sync-skills.mjs && node packages/omo-senpi/plugin/scripts/embed-directive.mjs --check && node packages/omo-senpi/plugin/scripts/build-install.mjs",
117
118
  "build:materialize-frontend": "node packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs --strict",
118
119
  "build:shared-skills-assets": "bun run build:materialize-frontend && rm -rf dist/skills && cp -R packages/shared-skills/skills dist/skills",
@@ -181,7 +182,7 @@
181
182
  "zod": "^4.4.3"
182
183
  },
183
184
  "devDependencies": {
184
- "@code-yeongyu/senpi": "2026.8.31",
185
+ "@code-yeongyu/senpi": "2026.9.2",
185
186
  "@oh-my-opencode/agents-md-core": "workspace:*",
186
187
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
187
188
  "@oh-my-opencode/boulder-state": "workspace:*",
@@ -222,18 +223,18 @@
222
223
  "typescript": "^7.0.2"
223
224
  },
224
225
  "optionalDependencies": {
225
- "oh-my-opencode-darwin-arm64": "5.0.0-beta.31",
226
- "oh-my-opencode-darwin-x64": "5.0.0-beta.31",
227
- "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.31",
228
- "oh-my-opencode-linux-arm64": "5.0.0-beta.31",
229
- "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.31",
230
- "oh-my-opencode-linux-x64": "5.0.0-beta.31",
231
- "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.31",
232
- "oh-my-opencode-linux-x64-musl": "5.0.0-beta.31",
233
- "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.31",
234
- "oh-my-opencode-windows-arm64": "5.0.0-beta.31",
235
- "oh-my-opencode-windows-x64": "5.0.0-beta.31",
236
- "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.31"
226
+ "oh-my-opencode-darwin-arm64": "5.0.0-beta.32",
227
+ "oh-my-opencode-darwin-x64": "5.0.0-beta.32",
228
+ "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.32",
229
+ "oh-my-opencode-linux-arm64": "5.0.0-beta.32",
230
+ "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.32",
231
+ "oh-my-opencode-linux-x64": "5.0.0-beta.32",
232
+ "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.32",
233
+ "oh-my-opencode-linux-x64-musl": "5.0.0-beta.32",
234
+ "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.32",
235
+ "oh-my-opencode-windows-arm64": "5.0.0-beta.32",
236
+ "oh-my-opencode-windows-x64": "5.0.0-beta.32",
237
+ "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.32"
237
238
  },
238
239
  "overrides": {
239
240
  "@earendil-works/pi-agent-core": "0.84.2",
@@ -251,6 +252,6 @@
251
252
  "@code-yeongyu/comment-checker"
252
253
  ],
253
254
  "patchedDependencies": {
254
- "@code-yeongyu/senpi@2026.8.31": "patches/@code-yeongyu%2Fsenpi@2026.8.31.patch"
255
+ "@code-yeongyu/senpi@2026.9.2": "patches/@code-yeongyu%2Fsenpi@2026.9.2.patch"
255
256
  }
256
257
  }
@@ -2,6 +2,8 @@ import { writeFileSync } from "node:fs";
2
2
 
3
3
  import { afterEach, describe, expect, it } from "bun:test";
4
4
 
5
+ import { ControlledClock } from "./controlled-clock-test-support.js";
6
+
5
7
  import {
6
8
  createWorkspaceEditTestHarness,
7
9
  diagnostic,
@@ -17,6 +19,7 @@ afterEach(async () => {
17
19
 
18
20
  describe("LspClient diagnostics freshness", () => {
19
21
  it("#given a versionless publish that arrives after the current change #when no newer eligible publish arrives before quiescence #then diagnostics wait for that quiescence window and return the versionless payload", async () => {
22
+ const clock = new ControlledClock();
20
23
  const context = await harness.makeClient(
21
24
  {
22
25
  publishDiagnostics: [
@@ -27,7 +30,7 @@ describe("LspClient diagnostics freshness", () => {
27
30
  },
28
31
  ],
29
32
  },
30
- { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 20 },
33
+ { diagnosticsFreshnessTimeoutMs: 80, versionlessPublishQuiescenceMs: 20, timerProvider: clock },
31
34
  );
32
35
  await context.client.openFile(context.source);
33
36
  const versionlessDelivery = waitForEventCount(
@@ -35,42 +38,59 @@ describe("LspClient diagnostics freshness", () => {
35
38
  (event) => event.type === "clientResponse" && event.method === "workspace/configuration",
36
39
  1,
37
40
  );
38
- const startedAt = Date.now();
39
41
  writeFileSync(context.source, "const after = 1;\n", "utf-8");
40
42
  await context.client.openFile(context.source);
41
43
  expect(await versionlessDelivery).toHaveLength(1);
42
44
 
43
- const result = await context.client.diagnostics(context.source);
44
- const elapsedMs = Date.now() - startedAt;
45
+ const pending = context.client.diagnostics(context.source);
46
+ await clock.waitForTimer(20);
47
+ expect(clock.scheduledDelays.at(-1)).toBe(20);
48
+ clock.advanceBy(20);
49
+ const result = await pending;
45
50
 
46
51
  expect(result.items).toEqual([diagnostic("post-generation-versionless")]);
47
- expect(elapsedMs).toBeGreaterThanOrEqual(15);
48
52
  });
49
53
 
50
54
  it("#given only a pre-generation versionless publish #when a newer local version asks for diagnostics #then the stale versionless publish is ignored and the request does not resolve clean", async () => {
55
+ const clock = new ControlledClock();
51
56
  const context = await harness.makeClient(
52
57
  {
53
58
  publishDiagnostics: [
54
59
  {
55
60
  trigger: "didOpen",
56
61
  diagnostics: [diagnostic("pre-generation-versionless")],
62
+ awaitClientDelivery: true,
57
63
  },
58
64
  ],
59
65
  },
60
- { diagnosticsFreshnessTimeoutMs: 300, versionlessPublishQuiescenceMs: 5 },
66
+ { diagnosticsFreshnessTimeoutMs: 300, versionlessPublishQuiescenceMs: 5, timerProvider: clock },
61
67
  );
62
68
  await context.client.openFile(context.source);
63
- expect((await context.client.diagnostics(context.source)).items).toEqual([diagnostic("pre-generation-versionless")]);
69
+ await waitForEventCount(
70
+ context.events,
71
+ (event) => event.type === "clientResponse" && event.method === "workspace/configuration",
72
+ 1,
73
+ );
74
+ const initial = context.client.diagnostics(context.source);
75
+ await clock.waitForTimer(5);
76
+ expect(clock.scheduledDelays.at(-1)).toBe(5);
77
+ clock.advanceBy(5);
78
+ expect((await initial).items).toEqual([diagnostic("pre-generation-versionless")]);
64
79
  writeFileSync(context.source, "const after = 1;\n", "utf-8");
65
80
  await context.client.openFile(context.source);
66
81
 
67
- const result = await context.client.diagnostics(context.source);
82
+ const pending = context.client.diagnostics(context.source);
83
+ await clock.waitForTimer(300);
84
+ expect(clock.scheduledDelays.at(-1)).toBe(300);
85
+ clock.advanceBy(300);
86
+ const result = await pending;
68
87
 
69
88
  expect(result.items).toEqual([]);
70
89
  expect(result.transientError?.kind).toBe("freshness_timeout");
71
90
  });
72
91
 
73
92
  it("#given stale and future publish versions #when diagnostics target the current version #then neither stale nor future diagnostics satisfy the request", async () => {
93
+ const staleClock = new ControlledClock();
74
94
  const stale = await harness.makeClient(
75
95
  {
76
96
  publishDiagnostics: [
@@ -82,7 +102,7 @@ describe("LspClient diagnostics freshness", () => {
82
102
  },
83
103
  ],
84
104
  },
85
- { diagnosticsFreshnessTimeoutMs: 100, versionlessPublishQuiescenceMs: 5 },
105
+ { diagnosticsFreshnessTimeoutMs: 100, versionlessPublishQuiescenceMs: 5, timerProvider: staleClock },
86
106
  );
87
107
  await stale.client.openFile(stale.source);
88
108
  const staleDelivery = waitForEventCount(
@@ -94,13 +114,17 @@ describe("LspClient diagnostics freshness", () => {
94
114
  await stale.client.openFile(stale.source);
95
115
  expect(await staleDelivery).toHaveLength(1);
96
116
 
97
- const staleResult = await stale.client.diagnostics(stale.source);
117
+ const stalePending = stale.client.diagnostics(stale.source);
118
+ await staleClock.waitForTimer(100);
119
+ staleClock.advanceBy(100);
120
+ const staleResult = await stalePending;
98
121
 
99
122
  expect(staleResult.items).toEqual([]);
100
123
  expect(staleResult.transientError?.kind).toBe("freshness_timeout");
101
124
 
102
125
  await harness.cleanup();
103
126
 
127
+ const futureClock = new ControlledClock();
104
128
  const future = await harness.makeClient(
105
129
  {
106
130
  publishDiagnostics: [
@@ -112,7 +136,7 @@ describe("LspClient diagnostics freshness", () => {
112
136
  },
113
137
  ],
114
138
  },
115
- { diagnosticsFreshnessTimeoutMs: 100, versionlessPublishQuiescenceMs: 5 },
139
+ { diagnosticsFreshnessTimeoutMs: 100, versionlessPublishQuiescenceMs: 5, timerProvider: futureClock },
116
140
  );
117
141
  await future.client.openFile(future.source);
118
142
  const futureDelivery = waitForEventCount(
@@ -124,7 +148,10 @@ describe("LspClient diagnostics freshness", () => {
124
148
  await future.client.openFile(future.source);
125
149
  expect(await futureDelivery).toHaveLength(1);
126
150
 
127
- const futureResult = await future.client.diagnostics(future.source);
151
+ const futurePending = future.client.diagnostics(future.source);
152
+ await futureClock.waitForTimer(100);
153
+ futureClock.advanceBy(100);
154
+ const futureResult = await futurePending;
128
155
 
129
156
  expect(futureResult.items).toEqual([]);
130
157
  expect(futureResult.transientError?.kind).toBe("freshness_timeout");
@@ -136,7 +163,7 @@ describe("LspClient diagnostics freshness", () => {
136
163
  capabilities: { diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false } },
137
164
  diagnosticResponses: [
138
165
  {
139
- delayMs: 200,
166
+ releaseOnDidChange: true,
140
167
  report: { kind: "full", resultId: "v1", items: [diagnostic("pull-stale")] },
141
168
  },
142
169
  {
@@ -248,19 +275,20 @@ describe("LspClient diagnostics freshness", () => {
248
275
  });
249
276
 
250
277
  it("#given a server without pull support that never publishes diagnostics #when diagnostics run on a clean file #then the request resolves clean after the freshness window instead of reporting a timeout", async () => {
278
+ const clock = new ControlledClock();
251
279
  const context = await harness.makeClient(
252
280
  {},
253
- { diagnosticsFreshnessTimeoutMs: 60, versionlessPublishQuiescenceMs: 5 },
281
+ { diagnosticsFreshnessTimeoutMs: 60, versionlessPublishQuiescenceMs: 5, timerProvider: clock },
254
282
  );
255
283
 
256
- const startedAt = Date.now();
257
- const result = await context.client.diagnostics(context.source);
258
- const elapsedMs = Date.now() - startedAt;
284
+ const pending = context.client.diagnostics(context.source);
285
+ await clock.waitForTimer(60);
286
+ expect(clock.scheduledDelays.at(-1)).toBe(60);
287
+ clock.advanceBy(60);
288
+ const result = await pending;
259
289
 
260
290
  expect(result.transientError).toBeUndefined();
261
291
  expect(result.items).toEqual([]);
262
- // The full freshness window is still honored so a slow publisher can win.
263
- expect(elapsedMs).toBeGreaterThanOrEqual(45);
264
292
  });
265
293
 
266
294
  it("#given a pull-supported server that cached diagnostics for an older document version #when the file changes and a later pull is rejected as unsupported without any publish #then the fallback resolves empty instead of returning the stale cached diagnostics", async () => {
@@ -0,0 +1,64 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, it } from "bun:test";
6
+
7
+ import { ControlledClock } from "./controlled-clock-test-support.js";
8
+ import { LspClient } from "./client.js";
9
+ import { LspRequestTimeoutError } from "./errors.js";
10
+ import type { ResolvedServer } from "./types.js";
11
+
12
+ const fakeServer: ResolvedServer = {
13
+ id: "pull-timeout-fake",
14
+ command: ["pull-timeout-fake"],
15
+ extensions: [".ts"],
16
+ priority: 0,
17
+ };
18
+
19
+ class HangingPullClient extends LspClient {
20
+ readonly grantedPullTimeouts: number[] = [];
21
+
22
+ constructor(root: string, private readonly clock: ControlledClock) {
23
+ super(root, fakeServer, { diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5, timerProvider: clock });
24
+ this.setDiagnosticPullSupported(true);
25
+ }
26
+
27
+ protected override async sendNotification(): Promise<void> {}
28
+
29
+ protected override async sendRequest<T>(
30
+ method: string,
31
+ ...args: [] | [unknown] | [unknown, { timeoutMs?: number; signal?: AbortSignal }]
32
+ ): Promise<T> {
33
+ if (method !== "textDocument/diagnostic") {
34
+ throw new Error(`unexpected request in hanging-pull fixture: ${method}`);
35
+ }
36
+ const timeoutMs = args[1]?.timeoutMs ?? 0;
37
+ this.grantedPullTimeouts.push(timeoutMs);
38
+ this.clock.advanceBy(timeoutMs);
39
+ throw new LspRequestTimeoutError(method);
40
+ }
41
+ }
42
+
43
+ describe("LspClient diagnostics pull timeout conversion", () => {
44
+ let root: string | null = null;
45
+
46
+ afterEach(() => {
47
+ if (root !== null) rmSync(root, { recursive: true, force: true });
48
+ root = null;
49
+ });
50
+
51
+ it("#given a pull that exhausts the freshness window #when diagnostics settle #then the timeout converts to freshness_timeout instead of rejecting", async () => {
52
+ root = mkdtempSync(join(tmpdir(), "lsp-pull-timeout-"));
53
+ const source = join(root, "sample.ts");
54
+ writeFileSync(source, "const value = 1;\n", "utf-8");
55
+ const clock = new ControlledClock();
56
+ const client = new HangingPullClient(root, clock);
57
+
58
+ const result = await client.diagnostics(source);
59
+
60
+ expect(client.grantedPullTimeouts).toEqual([50]);
61
+ expect(result.items).toEqual([]);
62
+ expect(result.transientError?.kind).toBe("freshness_timeout");
63
+ });
64
+ });
@@ -19,6 +19,7 @@ import type {
19
19
  } from "./types.js";
20
20
  import { LspRequestTimeoutError } from "./errors.js";
21
21
  import { WorkspaceDocumentState } from "./workspace-document-state.js";
22
+ import type { TimerProvider } from "./timer-provider.js";
22
23
  import type { LspRenameResult, WorkspaceEditCommitIo } from "./workspace-edit-types.js";
23
24
  import { WorkspaceMutationController } from "./workspace-mutation-controller.js";
24
25
 
@@ -36,6 +37,7 @@ export interface LspDiagnosticsResult {
36
37
  export interface LspClientOptions extends LspClientTimeoutOptions {
37
38
  readonly diagnosticsFreshnessTimeoutMs?: number;
38
39
  readonly versionlessPublishQuiescenceMs?: number;
40
+ readonly timerProvider?: TimerProvider;
39
41
  }
40
42
 
41
43
  export class LspClient extends LspClientConnection {
@@ -52,6 +54,7 @@ export class LspClient extends LspClientConnection {
52
54
  (method, params) => this.sendNotification(method, params),
53
55
  (uri) => this.diagnosticsStore.delete(uri),
54
56
  {
57
+ timerProvider: this.timerProvider,
55
58
  versionlessPublishQuiescenceMs:
56
59
  options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS,
57
60
  },
@@ -207,7 +210,7 @@ export class LspClient extends LspClientConnection {
207
210
  const absPath = this.resolveWorkspacePath(filePath);
208
211
  const uri = pathToFileURL(absPath).href;
209
212
  await this.openFile(absPath);
210
- const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
213
+ const deadlineAt = this.timerProvider.now() + this.diagnosticsFreshnessTimeoutMs;
211
214
 
212
215
  for (;;) {
213
216
  signal?.throwIfAborted();
@@ -220,7 +223,7 @@ export class LspClient extends LspClientConnection {
220
223
  if (!pushFallbackOnly) {
221
224
  const cached = this.documents.getPullCache(snapshot);
222
225
  try {
223
- const remainingMs = deadlineAt - Date.now();
226
+ const remainingMs = deadlineAt - this.timerProvider.now();
224
227
  if (remainingMs <= 0) return this.freshnessTimeout(absPath);
225
228
  const result = await this.sendRequest<{ items?: Diagnostic[]; kind?: string; resultId?: string }>(
226
229
  "textDocument/diagnostic",
@@ -262,7 +265,7 @@ export class LspClient extends LspClientConnection {
262
265
 
263
266
  if (!pushFallbackOnly) continue;
264
267
 
265
- const remainingMs = deadlineAt - Date.now();
268
+ const remainingMs = deadlineAt - this.timerProvider.now();
266
269
  if (remainingMs <= 0) {
267
270
  // A server that never advertised pull diagnostics (or rejected the pull
268
271
  // method) and has never published for this document stays silent for a
@@ -0,0 +1,61 @@
1
+ import type { TimerProvider } from "./timer-provider.js";
2
+
3
+ interface ControlledTimer {
4
+ readonly at: number;
5
+ readonly callback: () => void;
6
+ cancelled: boolean;
7
+ }
8
+
9
+ interface TimerWaiter {
10
+ readonly delayMs: number | undefined;
11
+ readonly resolve: () => void;
12
+ }
13
+
14
+ /**
15
+ * Deterministic TimerProvider for tests: timers fire only through advanceBy(),
16
+ * and waitForTimer(delayMs) resolves once a timer with exactly that delay is
17
+ * scheduled, so tests order themselves behind the timer they intend to fire.
18
+ */
19
+ export class ControlledClock implements TimerProvider {
20
+ private readonly timers: ControlledTimer[] = [];
21
+ private currentTime = 0;
22
+ private readonly timerWaiters = new Set<TimerWaiter>();
23
+ readonly scheduledDelays: number[] = [];
24
+
25
+ readonly now = (): number => this.currentTime;
26
+ readonly setTimeout = (callback: () => void, delayMs: number): ReturnType<typeof setTimeout> => {
27
+ const timer: ControlledTimer = { at: this.currentTime + delayMs, callback, cancelled: false };
28
+ this.scheduledDelays.push(delayMs);
29
+ this.timers.push(timer);
30
+ for (const waiter of this.timerWaiters) {
31
+ if (waiter.delayMs === undefined || waiter.delayMs === delayMs) {
32
+ this.timerWaiters.delete(waiter);
33
+ waiter.resolve();
34
+ }
35
+ }
36
+ return timer as unknown as ReturnType<typeof setTimeout>;
37
+ };
38
+ readonly clearTimeout = (handle: ReturnType<typeof setTimeout>): void => {
39
+ (handle as unknown as { cancelled: boolean }).cancelled = true;
40
+ };
41
+
42
+ waitForTimer(delayMs?: number): Promise<void> {
43
+ const hasTimer = this.timers.some(
44
+ (timer) => !timer.cancelled && (delayMs === undefined || timer.at === this.currentTime + delayMs),
45
+ );
46
+ if (hasTimer) return Promise.resolve();
47
+ return new Promise((resolve) => {
48
+ this.timerWaiters.add({ delayMs, resolve });
49
+ });
50
+ }
51
+
52
+ advanceBy(delayMs: number): void {
53
+ this.currentTime += delayMs;
54
+ for (;;) {
55
+ const index = this.timers.findIndex((timer) => timer.at <= this.currentTime);
56
+ if (index < 0) return;
57
+ const timer = this.timers.splice(index, 1)[0];
58
+ if (timer && !timer.cancelled) timer.callback();
59
+ }
60
+ }
61
+ }
@@ -8,6 +8,7 @@ let nextServerRequestId = 10_000;
8
8
  let renameIndex = 0;
9
9
  let diagnosticIndex = 0;
10
10
  const pendingServerRequests = new Map();
11
+ const releasedDiagnosticResponses = [];
11
12
 
12
13
  function record(event) {
13
14
  appendFileSync(eventsPath, `${JSON.stringify(event)}\n`, "utf-8");
@@ -146,8 +147,7 @@ function handleRequest(message) {
146
147
  setTimeout(() => process.exit(0), delay);
147
148
  return;
148
149
  }
149
- const delay = Number(step?.delayMs ?? 0);
150
- setTimeout(() => {
150
+ const sendDiagnosticResponse = () => {
151
151
  if (step?.error) {
152
152
  record({ type: "serverError", method: "textDocument/diagnostic", error: step.error });
153
153
  sendError(message.id, step.error.code, step.error.message);
@@ -156,7 +156,13 @@ function handleRequest(message) {
156
156
  const result = step?.report ?? { items: scenario.diagnostics ?? [] };
157
157
  record({ type: "serverResponse", method: "textDocument/diagnostic", result });
158
158
  sendResponse(message.id, result);
159
- }, delay);
159
+ };
160
+ if (step?.releaseOnDidChange === true) {
161
+ releasedDiagnosticResponses.push(sendDiagnosticResponse);
162
+ } else {
163
+ const delay = Number(step?.delayMs ?? 0);
164
+ setTimeout(sendDiagnosticResponse, delay);
165
+ }
160
166
  return;
161
167
  }
162
168
  if (message.method === "shutdown") {
@@ -190,6 +196,7 @@ function handleNotification(message) {
190
196
  }
191
197
  if (message.method === "textDocument/didChange") {
192
198
  applyPublishSteps("didChange", message.params.textDocument.uri, message.params.textDocument.version);
199
+ for (const release of releasedDiagnosticResponses.splice(0)) release();
193
200
  }
194
201
  if (message.method === "exit") process.exit(0);
195
202
  }
@@ -0,0 +1,13 @@
1
+ export type TimerHandle = ReturnType<typeof setTimeout>;
2
+
3
+ export interface TimerProvider {
4
+ readonly now: () => number;
5
+ readonly setTimeout: (callback: () => void, delayMs: number) => TimerHandle;
6
+ readonly clearTimeout: (handle: TimerHandle) => void;
7
+ }
8
+
9
+ export const realTimerProvider: TimerProvider = {
10
+ now: () => Date.now(),
11
+ setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
12
+ clearTimeout: (handle) => clearTimeout(handle),
13
+ };