oh-my-opencode 5.0.0-beta.30 → 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.
- package/dist/cli/index.js +178 -37
- package/dist/cli-node/index.js +178 -37
- package/dist/config/schema/background-task.d.ts +1 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
- package/dist/features/background-agent/constants.d.ts +0 -7
- package/dist/features/background-agent/manager.d.ts +2 -1
- package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
- package/dist/features/background-agent/parent-wake-notifier-types.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-prompt-dispatch.d.ts +1 -1
- package/dist/features/background-agent/parent-wake-session-inspector.d.ts +1 -1
- package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
- package/dist/features/monitor/manager-internals.d.ts +1 -1
- package/dist/features/monitor/output-injector-types.d.ts +1 -1
- package/dist/features/task-toast-manager/manager.d.ts +0 -24
- package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +1 -0
- package/dist/features/team-mode/tools/messaging-runtime.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
- package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
- package/dist/hooks/anthropic-context-window-limit-recovery/tool-result-storage.d.ts +11 -0
- package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
- package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
- package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
- package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
- package/dist/hooks/keyword-detector/hook.d.ts +7 -0
- package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
- package/dist/hooks/model-fallback/hook.d.ts +1 -1
- package/dist/index.js +681 -273
- package/dist/oh-my-opencode.schema.json +5 -0
- package/dist/plugin-dispose.d.ts +3 -0
- package/dist/shared/agent-tool-restrictions.d.ts +0 -1
- package/dist/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +1 -0
- package/dist/tools/session-manager/directory-filter.d.ts +2 -0
- package/dist/tui.js +148 -9
- package/package.json +16 -15
- package/packages/git-bash-mcp/dist/cli.js +1 -0
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +47 -19
- package/packages/lsp-core/src/lsp/client-diagnostics-pull-timeout.test.ts +64 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
- package/packages/lsp-core/src/lsp/client.ts +6 -3
- package/packages/lsp-core/src/lsp/constants.ts +5 -0
- package/packages/lsp-core/src/lsp/controlled-clock-test-support.ts +61 -0
- package/packages/lsp-core/src/lsp/errors.ts +15 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +10 -3
- package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
- package/packages/lsp-core/src/lsp/manager.ts +106 -13
- package/packages/lsp-core/src/lsp/timer-provider.ts +13 -0
- package/packages/lsp-core/src/lsp/transport-request-timeout.test.ts +73 -0
- package/packages/lsp-core/src/lsp/transport.ts +7 -2
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +84 -1
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +30 -11
- package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
- package/packages/lsp-core/src/mcp.ts +3 -0
- package/packages/lsp-core/src/tools/parameters.test.ts +51 -0
- package/packages/lsp-daemon/dist/cli.js +155 -34
- package/packages/lsp-daemon/dist/client.js +155 -34
- package/packages/lsp-daemon/dist/index.js +155 -34
- package/packages/lsp-daemon/dist/proxy.js +3 -0
- package/packages/lsp-tools-mcp/dist/cli.js +155 -34
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +134 -28
- package/packages/lsp-tools-mcp/dist/mcp.js +155 -34
- package/packages/lsp-tools-mcp/dist/tools.js +154 -34
- package/packages/lsp-tools-mcp/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +17 -2
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +17 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +152 -34
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/AGENTS.md +5 -4
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +489 -4
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -2
- package/packages/omo-codex/plugin/components/ultrawork/scripts/sync-directive.mjs +30 -14
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +4 -0
- package/packages/omo-codex/plugin/components/ultrawork/src/directive.ts +5 -2
- package/packages/omo-codex/plugin/components/ultrawork/test/directive-source.test.ts +14 -8
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +12 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +5 -2
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +10 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +113 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +9 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +856 -347
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +27 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +14 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +17 -9
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +17 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +13 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +19 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-aggregate.js +53 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-artifacts.js +85 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +41 -102
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +185 -14
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +33 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +65 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +17 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +25 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +127 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +10 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +2 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +34 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +23 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +19 -9
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +18 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +27 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-aggregate.ts +57 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-artifacts.ts +103 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +54 -123
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +187 -12
- package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +36 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +95 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +107 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +92 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +183 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +35 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +105 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-aggregate.test.ts +101 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +46 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-senpi-surface.test.ts +131 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +5 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +318 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +9 -3
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +31 -2
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +17 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +11 -1
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +13 -0
- package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +5 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +9 -2
- package/dist/features/builtin-skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skill-file-loader.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/agent-browser-template.d.ts +0 -1
- package/dist/features/builtin-skills/skills/debugging.d.ts +0 -1
- package/dist/features/builtin-skills/skills/dev-browser.d.ts +0 -1
- package/dist/features/builtin-skills/skills/frontend.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-atomic-planning.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-context-analysis.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-execution-verification.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/commit-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/history-search-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/overview.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/quick-reference.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-sections/rebase-workflow.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master-skill-metadata.d.ts +0 -1
- package/dist/features/builtin-skills/skills/git-master.d.ts +0 -1
- package/dist/features/builtin-skills/skills/index.d.ts +0 -1
- package/dist/features/builtin-skills/skills/init-deep.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-cli.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright-mcp-skill.d.ts +0 -1
- package/dist/features/builtin-skills/skills/playwright.d.ts +0 -1
- package/dist/features/builtin-skills/skills/remove-ai-slops.d.ts +0 -1
- package/dist/features/builtin-skills/skills/review-work.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-research.d.ts +0 -1
- package/dist/features/builtin-skills/skills/security-review.d.ts +0 -1
- package/dist/features/builtin-skills/skills/team-mode.d.ts +0 -1
- package/dist/features/builtin-skills/skills/visual-qa.d.ts +0 -1
- package/dist/features/builtin-skills/skills.d.ts +0 -1
- package/dist/features/builtin-skills/types.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/agent-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/command-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-core.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery-paths.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/discovery.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/hook-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/index.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/install-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/installed-plugin-database.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loaded-plugin.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/mcp-server-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-key.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-manifest.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-path-resolver.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/plugin-settings.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/scope-filter.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/skill-loader.d.ts +0 -1
- package/dist/features/claude-code-plugin-loader/types.d.ts +0 -1
- package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
- package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
- package/dist/shared/prompt-async-gate/message-inspection-error.d.ts +0 -1
- package/dist/shared/prompt-async-gate/pending-tool-turn.d.ts +0 -1
- package/dist/shared/prompt-async-gate/prompt-message-state.d.ts +0 -1
- package/dist/shared/prompt-async-gate/queue.d.ts +0 -1
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +0 -1
- package/dist/shared/prompt-async-gate/reservations.d.ts +0 -1
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +0 -1
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +0 -1
- package/dist/shared/prompt-async-gate/timing.d.ts +0 -1
- package/dist/shared/prompt-async-gate/types.d.ts +0 -1
- package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
- package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +0 -483
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +0 -490
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { reportBestEffortCleanupError } from "./cleanup-errors.js";
|
|
2
2
|
import { LspClient } from "./client.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CLIENT_RESPAWN_COOLDOWN_MS,
|
|
5
|
+
CLIENT_RESPAWN_RETRY_LIMIT,
|
|
6
|
+
IDLE_TIMEOUT_MS,
|
|
7
|
+
INIT_TIMEOUT_MS,
|
|
8
|
+
MAX_RESIDENT_CLIENTS,
|
|
9
|
+
REAPER_INTERVAL_MS,
|
|
10
|
+
} from "./constants.js";
|
|
11
|
+
import { LspClientRespawnBudgetExceededError } from "./errors.js";
|
|
4
12
|
import { installProcessSignalCleanup } from "./process-signal-cleanup.js";
|
|
5
13
|
import type { ResolvedServer } from "./types.js";
|
|
6
14
|
|
|
@@ -14,6 +22,11 @@ interface ManagedClient {
|
|
|
14
22
|
initializingSince: number | null;
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
interface RespawnBudget {
|
|
26
|
+
deadGenerations: number;
|
|
27
|
+
exhaustedAt: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
export interface ClientSnapshot {
|
|
18
31
|
root: string;
|
|
19
32
|
serverId: string;
|
|
@@ -75,6 +88,9 @@ function awaitWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined
|
|
|
75
88
|
|
|
76
89
|
export class LspManager {
|
|
77
90
|
private readonly clients = new Map<string, ManagedClient>();
|
|
91
|
+
/** In-flight stops per key; getClient awaits these instead of spawning a duplicate client. */
|
|
92
|
+
private readonly pendingStops = new Map<string, Promise<void>>();
|
|
93
|
+
private readonly respawnBudgets = new Map<string, RespawnBudget>();
|
|
78
94
|
private reaperHandle: NodeJS.Timeout | null = null;
|
|
79
95
|
private signalDisposer: (() => void) | null = null;
|
|
80
96
|
private disposed = false;
|
|
@@ -112,6 +128,50 @@ export class LspManager {
|
|
|
112
128
|
return `${root}::${serverId}`;
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Deletes-then-stops with a tombstone: the pending stop is recorded so a concurrent getClient
|
|
133
|
+
* for the same key awaits it instead of spawning a duplicate client while the old one lingers
|
|
134
|
+
* (production stops reach ~21s). The tombstone removes itself once the stop settles.
|
|
135
|
+
*/
|
|
136
|
+
private tombstoneStop(key: string, client: LspClient): Promise<void> {
|
|
137
|
+
const stop = stopClientBestEffort(client);
|
|
138
|
+
this.pendingStops.set(key, stop);
|
|
139
|
+
void stop.finally(() => {
|
|
140
|
+
if (this.pendingStops.get(key) === stop) {
|
|
141
|
+
this.pendingStops.delete(key);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
return stop;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private recordDeadGeneration(key: string): void {
|
|
148
|
+
let budget = this.respawnBudgets.get(key);
|
|
149
|
+
if (budget === undefined) {
|
|
150
|
+
budget = { deadGenerations: 0, exhaustedAt: null };
|
|
151
|
+
this.respawnBudgets.set(key, budget);
|
|
152
|
+
}
|
|
153
|
+
budget.deadGenerations += 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private markHealthyGeneration(key: string): void {
|
|
157
|
+
this.respawnBudgets.delete(key);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Throws while the respawn budget is spent and the cooldown has not elapsed. */
|
|
161
|
+
private ensureRespawnAllowed(key: string, root: string, serverId: string): void {
|
|
162
|
+
const budget = this.respawnBudgets.get(key);
|
|
163
|
+
if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT) return;
|
|
164
|
+
if (budget.exhaustedAt === null) {
|
|
165
|
+
budget.exhaustedAt = this.now();
|
|
166
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
167
|
+
}
|
|
168
|
+
if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
|
|
169
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
170
|
+
}
|
|
171
|
+
budget.deadGenerations = 0;
|
|
172
|
+
budget.exhaustedAt = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
115
175
|
private reapStale(): void {
|
|
116
176
|
const t = this.now();
|
|
117
177
|
for (const [key, managed] of this.clients) {
|
|
@@ -120,8 +180,8 @@ export class LspManager {
|
|
|
120
180
|
managed.initializingSince !== null &&
|
|
121
181
|
t - managed.initializingSince > this.initTimeoutMs
|
|
122
182
|
) {
|
|
123
|
-
void stopClientBestEffort(managed.client);
|
|
124
183
|
this.clients.delete(key);
|
|
184
|
+
void this.tombstoneStop(key, managed.client);
|
|
125
185
|
continue;
|
|
126
186
|
}
|
|
127
187
|
|
|
@@ -131,8 +191,8 @@ export class LspManager {
|
|
|
131
191
|
managed.pendingWaiters === 0 &&
|
|
132
192
|
t - managed.lastUsedAt > this.idleTimeoutMs
|
|
133
193
|
) {
|
|
134
|
-
void stopClientBestEffort(managed.client);
|
|
135
194
|
this.clients.delete(key);
|
|
195
|
+
void this.tombstoneStop(key, managed.client);
|
|
136
196
|
}
|
|
137
197
|
}
|
|
138
198
|
}
|
|
@@ -148,7 +208,7 @@ export class LspManager {
|
|
|
148
208
|
const victim = this.leastRecentlyUsedIdleClient();
|
|
149
209
|
if (!victim) return;
|
|
150
210
|
this.clients.delete(victim.key);
|
|
151
|
-
void
|
|
211
|
+
void this.tombstoneStop(victim.key, victim.managed.client);
|
|
152
212
|
}
|
|
153
213
|
}
|
|
154
214
|
|
|
@@ -171,7 +231,7 @@ export class LspManager {
|
|
|
171
231
|
this.clients.get(key) === managed
|
|
172
232
|
) {
|
|
173
233
|
this.clients.delete(key);
|
|
174
|
-
await
|
|
234
|
+
await this.tombstoneStop(key, managed.client);
|
|
175
235
|
}
|
|
176
236
|
}
|
|
177
237
|
|
|
@@ -182,6 +242,16 @@ export class LspManager {
|
|
|
182
242
|
signal?.throwIfAborted();
|
|
183
243
|
|
|
184
244
|
const key = this.getKey(root, server.id);
|
|
245
|
+
for (;;) {
|
|
246
|
+
const pendingStop = this.pendingStops.get(key);
|
|
247
|
+
if (pendingStop === undefined) break;
|
|
248
|
+
await awaitWithSignal(pendingStop, signal);
|
|
249
|
+
signal?.throwIfAborted();
|
|
250
|
+
}
|
|
251
|
+
if (this.disposed) {
|
|
252
|
+
throw new Error("LspManager has been disposed");
|
|
253
|
+
}
|
|
254
|
+
|
|
185
255
|
let managed = this.clients.get(key);
|
|
186
256
|
|
|
187
257
|
if (managed) {
|
|
@@ -191,8 +261,8 @@ export class LspManager {
|
|
|
191
261
|
managed.initializingSince !== null &&
|
|
192
262
|
t - managed.initializingSince > this.initTimeoutMs
|
|
193
263
|
) {
|
|
194
|
-
await stopClientBestEffort(managed.client);
|
|
195
264
|
this.clients.delete(key);
|
|
265
|
+
await this.tombstoneStop(key, managed.client);
|
|
196
266
|
managed = undefined;
|
|
197
267
|
}
|
|
198
268
|
}
|
|
@@ -216,17 +286,22 @@ export class LspManager {
|
|
|
216
286
|
}
|
|
217
287
|
|
|
218
288
|
if (!managed.client.isAlive()) {
|
|
219
|
-
|
|
220
|
-
this.clients.
|
|
289
|
+
this.recordDeadGeneration(key);
|
|
290
|
+
if (this.clients.get(key) === managed) {
|
|
291
|
+
this.clients.delete(key);
|
|
292
|
+
}
|
|
293
|
+
await this.tombstoneStop(key, managed.client);
|
|
221
294
|
return this.getClient(root, server, signal);
|
|
222
295
|
}
|
|
223
296
|
|
|
297
|
+
this.markHealthyGeneration(key);
|
|
224
298
|
managed.refCount++;
|
|
225
299
|
managed.lastUsedAt = this.now();
|
|
226
300
|
return managed.client;
|
|
227
301
|
}
|
|
228
302
|
|
|
229
303
|
this.evictForAdmission();
|
|
304
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
230
305
|
|
|
231
306
|
const client = this.clientFactory(root, server);
|
|
232
307
|
const initStartedAt = this.now();
|
|
@@ -253,7 +328,7 @@ export class LspManager {
|
|
|
253
328
|
if (this.clients.get(key) === newManaged) {
|
|
254
329
|
this.clients.delete(key);
|
|
255
330
|
}
|
|
256
|
-
await
|
|
331
|
+
await this.tombstoneStop(key, client);
|
|
257
332
|
throw err;
|
|
258
333
|
}
|
|
259
334
|
|
|
@@ -267,6 +342,16 @@ export class LspManager {
|
|
|
267
342
|
signal.throwIfAborted();
|
|
268
343
|
}
|
|
269
344
|
|
|
345
|
+
if (!client.isAlive()) {
|
|
346
|
+
this.recordDeadGeneration(key);
|
|
347
|
+
if (this.clients.get(key) === newManaged) {
|
|
348
|
+
this.clients.delete(key);
|
|
349
|
+
}
|
|
350
|
+
await this.tombstoneStop(key, client);
|
|
351
|
+
return this.getClient(root, server, signal);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.markHealthyGeneration(key);
|
|
270
355
|
newManaged.refCount++;
|
|
271
356
|
newManaged.lastUsedAt = this.now();
|
|
272
357
|
return client;
|
|
@@ -287,15 +372,20 @@ export class LspManager {
|
|
|
287
372
|
if (!managed) return;
|
|
288
373
|
if (client && managed.client !== client) return;
|
|
289
374
|
this.clients.delete(key);
|
|
290
|
-
void
|
|
375
|
+
void this.tombstoneStop(key, managed.client);
|
|
291
376
|
}
|
|
292
377
|
|
|
293
378
|
warmupClient(root: string, server: ResolvedServer): void {
|
|
294
379
|
if (this.disposed) return;
|
|
295
380
|
const key = this.getKey(root, server.id);
|
|
296
|
-
if (this.clients.has(key)) return;
|
|
381
|
+
if (this.clients.has(key) || this.pendingStops.has(key)) return;
|
|
297
382
|
|
|
298
383
|
this.evictForAdmission();
|
|
384
|
+
try {
|
|
385
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
299
389
|
|
|
300
390
|
const client = this.clientFactory(root, server);
|
|
301
391
|
const initStartedAt = this.now();
|
|
@@ -326,7 +416,7 @@ export class LspManager {
|
|
|
326
416
|
if (this.clients.get(key) === managed) {
|
|
327
417
|
this.clients.delete(key);
|
|
328
418
|
}
|
|
329
|
-
void
|
|
419
|
+
void this.tombstoneStop(key, client);
|
|
330
420
|
},
|
|
331
421
|
);
|
|
332
422
|
}
|
|
@@ -379,8 +469,11 @@ export class LspManager {
|
|
|
379
469
|
for (const managed of this.clients.values()) {
|
|
380
470
|
stopPromises.push(stopClientBestEffort(managed.client));
|
|
381
471
|
}
|
|
472
|
+
const tombstonedStops = [...this.pendingStops.values()];
|
|
382
473
|
this.clients.clear();
|
|
383
|
-
|
|
474
|
+
this.respawnBudgets.clear();
|
|
475
|
+
await Promise.allSettled([...stopPromises, ...tombstonedStops]);
|
|
476
|
+
this.pendingStops.clear();
|
|
384
477
|
}
|
|
385
478
|
}
|
|
386
479
|
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { ControlledClock } from "./controlled-clock-test-support.js";
|
|
6
|
+
import { LspRequestTimeoutError } from "./errors.js";
|
|
7
|
+
import { JsonRpcConnection } from "./json-rpc-connection.js";
|
|
8
|
+
import { LspClientTransport } from "./transport.js";
|
|
9
|
+
import type { LspClientTimeoutOptions } from "./transport.js";
|
|
10
|
+
import type { ResolvedServer } from "./types.js";
|
|
11
|
+
|
|
12
|
+
const fakeServer: ResolvedServer = {
|
|
13
|
+
id: "controlled-clock-fake",
|
|
14
|
+
command: ["controlled-clock-fake"],
|
|
15
|
+
extensions: [".ts"],
|
|
16
|
+
priority: 0,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
class WiredTransport extends LspClientTransport {
|
|
20
|
+
constructor(timeouts: LspClientTimeoutOptions, connection: JsonRpcConnection) {
|
|
21
|
+
super("/tmp", fakeServer, timeouts);
|
|
22
|
+
this.connection = connection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
request<T>(method: string, params: unknown, options: { timeoutMs?: number }): Promise<T> {
|
|
26
|
+
return this.sendRequest<T>(method, params, options);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function encodeMessage(message: Record<string, unknown>): string {
|
|
31
|
+
const body = JSON.stringify(message);
|
|
32
|
+
return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeWiredTransport(clock: ControlledClock): { transport: WiredTransport; serverToClient: PassThrough } {
|
|
36
|
+
const serverToClient = new PassThrough();
|
|
37
|
+
const clientToServer = new PassThrough();
|
|
38
|
+
const connection = new JsonRpcConnection(serverToClient, clientToServer);
|
|
39
|
+
connection.listen();
|
|
40
|
+
return { transport: new WiredTransport({ timerProvider: clock }, connection), serverToClient };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("LspClientTransport request timeout via TimerProvider", () => {
|
|
44
|
+
it("#given a request the server never answers #when the injected clock passes the timeout #then the request rejects with LspRequestTimeoutError", async () => {
|
|
45
|
+
const clock = new ControlledClock();
|
|
46
|
+
const { transport } = makeWiredTransport(clock);
|
|
47
|
+
|
|
48
|
+
const pending = transport.request("textDocument/diagnostic", { textDocument: { uri: "file:///a.ts" } }, {
|
|
49
|
+
timeoutMs: 50,
|
|
50
|
+
});
|
|
51
|
+
await clock.waitForTimer(50);
|
|
52
|
+
expect(clock.scheduledDelays.at(-1)).toBe(50);
|
|
53
|
+
clock.advanceBy(50);
|
|
54
|
+
|
|
55
|
+
await expect(pending).rejects.toBeInstanceOf(LspRequestTimeoutError);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("#given a request answered before the deadline #when the injected clock later passes the timeout #then the settled result stands", async () => {
|
|
59
|
+
const clock = new ControlledClock();
|
|
60
|
+
const { transport, serverToClient } = makeWiredTransport(clock);
|
|
61
|
+
|
|
62
|
+
const pending = transport.request<{ items: unknown[] }>(
|
|
63
|
+
"textDocument/diagnostic",
|
|
64
|
+
{ textDocument: { uri: "file:///a.ts" } },
|
|
65
|
+
{ timeoutMs: 50 },
|
|
66
|
+
);
|
|
67
|
+
await clock.waitForTimer(50);
|
|
68
|
+
serverToClient.write(encodeMessage({ jsonrpc: "2.0", id: 1, result: { items: [] } }));
|
|
69
|
+
|
|
70
|
+
await expect(pending).resolves.toEqual({ items: [] });
|
|
71
|
+
clock.advanceBy(50);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { reportBestEffortCleanupError } from "./cleanup-errors.js";
|
|
2
|
+
import { realTimerProvider } from "./timer-provider.js";
|
|
3
|
+
import type { TimerProvider } from "./timer-provider.js";
|
|
2
4
|
import { INIT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, STOP_HARD_KILL_TIMEOUT_MS, STOP_SIGKILL_GRACE_MS } from "./constants.js";
|
|
3
5
|
import { LspConnectionClosedError, LspProcessExitedError, LspRequestTimeoutError } from "./errors.js";
|
|
4
6
|
import { JsonRpcConnection } from "./json-rpc-connection.js";
|
|
@@ -12,6 +14,7 @@ export { createLspSpawnEnv } from "./transport-protocol.js";
|
|
|
12
14
|
export interface LspClientTimeoutOptions {
|
|
13
15
|
requestTimeoutMs?: number;
|
|
14
16
|
initializeTimeoutMs?: number;
|
|
17
|
+
timerProvider?: TimerProvider;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
type WorkspaceApplyEditHandler = (params: unknown) => Promise<WorkspaceApplyEditResponse>;
|
|
@@ -35,6 +38,7 @@ export class LspClientTransport {
|
|
|
35
38
|
protected readonly diagnosticsStore = new Map<string, Diagnostic[]>();
|
|
36
39
|
protected readonly requestTimeoutMs: number;
|
|
37
40
|
protected readonly initializeTimeoutMs: number;
|
|
41
|
+
protected readonly timerProvider: TimerProvider;
|
|
38
42
|
private workspaceApplyEditHandler: WorkspaceApplyEditHandler | null = null;
|
|
39
43
|
private diagnosticPullSupported = false;
|
|
40
44
|
private documentFormattingSupported = false;
|
|
@@ -46,6 +50,7 @@ export class LspClientTransport {
|
|
|
46
50
|
) {
|
|
47
51
|
this.requestTimeoutMs = timeouts.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
48
52
|
this.initializeTimeoutMs = timeouts.initializeTimeoutMs ?? INIT_TIMEOUT_MS;
|
|
53
|
+
this.timerProvider = timeouts.timerProvider ?? realTimerProvider;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
pid(): number | undefined {
|
|
@@ -187,7 +192,7 @@ export class LspClientTransport {
|
|
|
187
192
|
const options = args[1];
|
|
188
193
|
const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
|
|
189
194
|
const timeoutController = new AbortController();
|
|
190
|
-
const timeoutHandle = setTimeout(() => {
|
|
195
|
+
const timeoutHandle = this.timerProvider.setTimeout(() => {
|
|
191
196
|
const stderrTail = this.stderrBuffer.slice(-5).join("\n");
|
|
192
197
|
timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
|
|
193
198
|
}, timeoutMs);
|
|
@@ -213,7 +218,7 @@ export class LspClientTransport {
|
|
|
213
218
|
}
|
|
214
219
|
throw error;
|
|
215
220
|
} finally {
|
|
216
|
-
clearTimeout(timeoutHandle);
|
|
221
|
+
this.timerProvider.clearTimeout(timeoutHandle);
|
|
217
222
|
combinedSignal.dispose();
|
|
218
223
|
}
|
|
219
224
|
}
|
|
@@ -1,8 +1,91 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
2
5
|
|
|
6
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import type { Diagnostic } from "./types.js";
|
|
3
9
|
import { WorkspaceDocumentState } from "./workspace-document-state.js";
|
|
4
10
|
import type { WorkspaceMutation } from "./workspace-edit.js";
|
|
5
11
|
|
|
12
|
+
const workspaces: string[] = [];
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
while (workspaces.length > 0) {
|
|
16
|
+
const workspace = workspaces.pop();
|
|
17
|
+
if (workspace) rmSync(workspace, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function makeDocuments(): WorkspaceDocumentState {
|
|
22
|
+
return new WorkspaceDocumentState(
|
|
23
|
+
async () => {},
|
|
24
|
+
() => {},
|
|
25
|
+
{ versionlessPublishQuiescenceMs: 0 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeSourceFile(directorySegment: string): string {
|
|
30
|
+
const workspace = realpathSync(mkdtempSync(join(tmpdir(), "lsp-document-uri-")));
|
|
31
|
+
workspaces.push(workspace);
|
|
32
|
+
const directory = join(workspace, directorySegment);
|
|
33
|
+
mkdirSync(directory, { recursive: true });
|
|
34
|
+
const source = join(directory, "page.ts");
|
|
35
|
+
writeFileSync(source, "const value: number = 'text';\n", "utf-8");
|
|
36
|
+
return source;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const diagnostic: Diagnostic = {
|
|
40
|
+
range: { start: { line: 0, character: 6 }, end: { line: 0, character: 11 } },
|
|
41
|
+
severity: 1,
|
|
42
|
+
message: "Type 'string' is not assignable to type 'number'.",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("WorkspaceDocumentState published diagnostics under a re-encoded uri", () => {
|
|
46
|
+
it("#given a server that percent-encodes a path segment #when it publishes #then the diagnostics reach the open document", async () => {
|
|
47
|
+
const documents = makeDocuments();
|
|
48
|
+
const source = makeSourceFile("(marketing)");
|
|
49
|
+
await documents.openFile(source);
|
|
50
|
+
const snapshot = documents.captureDiagnosticSnapshot(source);
|
|
51
|
+
expect(snapshot).not.toBeNull();
|
|
52
|
+
const publishedUri = snapshot!.uri.replaceAll("(", "%28").replaceAll(")", "%29");
|
|
53
|
+
expect(publishedUri).not.toBe(snapshot!.uri);
|
|
54
|
+
|
|
55
|
+
documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
|
|
56
|
+
|
|
57
|
+
expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
|
|
58
|
+
expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.skipIf(process.platform !== "win32")(
|
|
62
|
+
"#given a server that lowercases and percent-encodes the drive #when it publishes #then the diagnostics reach the open document",
|
|
63
|
+
async () => {
|
|
64
|
+
const documents = makeDocuments();
|
|
65
|
+
const source = makeSourceFile("routes");
|
|
66
|
+
await documents.openFile(source);
|
|
67
|
+
const snapshot = documents.captureDiagnosticSnapshot(source);
|
|
68
|
+
expect(snapshot).not.toBeNull();
|
|
69
|
+
const publishedUri = snapshot!.uri.replace(/^file:\/\/\/([A-Za-z]):/, (_match, drive: string) => `file:///${drive.toLowerCase()}%3A`);
|
|
70
|
+
expect(publishedUri).not.toBe(snapshot!.uri);
|
|
71
|
+
|
|
72
|
+
documents.recordPublishedDiagnostics({ uri: publishedUri, diagnostics: [diagnostic], version: snapshot!.version });
|
|
73
|
+
|
|
74
|
+
expect(documents.getStoredDiagnostics(snapshot!.uri)).toEqual([diagnostic]);
|
|
75
|
+
expect(documents.resolvePushDiagnostics(snapshot!)).toEqual({ status: "ready", diagnostics: [diagnostic] });
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
it("#given a document opened from a path the client already encodes #when the server echoes it verbatim #then the outgoing uri is unchanged", async () => {
|
|
80
|
+
const documents = makeDocuments();
|
|
81
|
+
const source = makeSourceFile("plain");
|
|
82
|
+
await documents.openFile(source);
|
|
83
|
+
const snapshot = documents.captureDiagnosticSnapshot(source);
|
|
84
|
+
|
|
85
|
+
expect(snapshot?.uri).toBe(pathToFileURL(source).href);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
6
89
|
describe("WorkspaceDocumentState watched-file bounds", () => {
|
|
7
90
|
it("#given more closed-file mutations than one batch #when synchronized #then every notification stays bounded", async () => {
|
|
8
91
|
const notifications: unknown[] = [];
|
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url";
|
|
|
5
5
|
import { effectiveExtension } from "./effective-extension.js";
|
|
6
6
|
import { getLanguageId } from "./language-mappings.js";
|
|
7
7
|
import type { Diagnostic } from "./types.js";
|
|
8
|
+
import type { TimerProvider } from "./timer-provider.js";
|
|
8
9
|
import type { PlannedWorkspaceOperation, WorkspaceMutation, WorkspaceMutationDelta } from "./workspace-edit-types.js";
|
|
9
10
|
|
|
10
11
|
const WATCHED_FILE_BATCH_SIZE = 128;
|
|
@@ -76,6 +77,7 @@ export type PushDiagnosticsResolution =
|
|
|
76
77
|
|
|
77
78
|
export interface WorkspaceDocumentStateOptions {
|
|
78
79
|
readonly now?: () => number;
|
|
80
|
+
readonly timerProvider?: TimerProvider;
|
|
79
81
|
readonly versionlessPublishQuiescenceMs?: number;
|
|
80
82
|
}
|
|
81
83
|
|
|
@@ -88,6 +90,17 @@ function canonicalPath(filePath: string): string {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
function normalizeDocumentUri(uri: string): string {
|
|
94
|
+
let decoded = uri;
|
|
95
|
+
try {
|
|
96
|
+
decoded = decodeURIComponent(uri);
|
|
97
|
+
} catch {
|
|
98
|
+
decoded = uri;
|
|
99
|
+
}
|
|
100
|
+
if (process.platform !== "win32") return decoded;
|
|
101
|
+
return decoded.replace(/^(file:\/\/\/)([a-z]):/, (_match, prefix: string, drive: string) => `${prefix}${drive.toUpperCase()}:`);
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
function isSameOrDescendant(candidate: string, parent: string): boolean {
|
|
92
105
|
const suffix = relative(parent, candidate);
|
|
93
106
|
return suffix === "" || (!suffix.startsWith("..") && suffix !== "..");
|
|
@@ -103,6 +116,7 @@ export class WorkspaceDocumentState {
|
|
|
103
116
|
private readonly openByUri = new Map<string, OpenDocumentState>();
|
|
104
117
|
private readonly openPromises = new Map<string, Promise<void>>();
|
|
105
118
|
private readonly now: () => number;
|
|
119
|
+
private readonly timerProvider: TimerProvider;
|
|
106
120
|
private readonly versionlessPublishQuiescenceMs: number;
|
|
107
121
|
|
|
108
122
|
constructor(
|
|
@@ -110,7 +124,12 @@ export class WorkspaceDocumentState {
|
|
|
110
124
|
private readonly clearDiagnostics: (uri: string) => void,
|
|
111
125
|
options: WorkspaceDocumentStateOptions = {},
|
|
112
126
|
) {
|
|
113
|
-
this.
|
|
127
|
+
this.timerProvider = options.timerProvider ?? {
|
|
128
|
+
now: options.now ?? (() => Date.now()),
|
|
129
|
+
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
130
|
+
clearTimeout: (handle) => clearTimeout(handle),
|
|
131
|
+
};
|
|
132
|
+
this.now = () => this.timerProvider.now();
|
|
114
133
|
this.versionlessPublishQuiescenceMs =
|
|
115
134
|
options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
|
|
116
135
|
}
|
|
@@ -135,7 +154,7 @@ export class WorkspaceDocumentState {
|
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
getStoredDiagnostics(uri: string): readonly Diagnostic[] {
|
|
138
|
-
const state = this.openByUri.get(uri);
|
|
157
|
+
const state = this.openByUri.get(normalizeDocumentUri(uri));
|
|
139
158
|
if (!state) return [];
|
|
140
159
|
return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
|
|
141
160
|
}
|
|
@@ -163,13 +182,13 @@ export class WorkspaceDocumentState {
|
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
getPullCache(snapshot: DiagnosticSnapshot): PullDiagnosticsCacheHit | null {
|
|
166
|
-
const state = this.openByUri.get(snapshot.uri);
|
|
185
|
+
const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
|
|
167
186
|
if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version) return null;
|
|
168
187
|
return state.pullCache;
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
recordPullDiagnostics(snapshot: DiagnosticSnapshot, report: PullDiagnosticsReport): void {
|
|
172
|
-
const state = this.openByUri.get(snapshot.uri);
|
|
191
|
+
const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
|
|
173
192
|
if (!state) return;
|
|
174
193
|
state.pullCache = {
|
|
175
194
|
documentVersion: snapshot.version,
|
|
@@ -183,7 +202,7 @@ export class WorkspaceDocumentState {
|
|
|
183
202
|
readonly diagnostics: readonly Diagnostic[];
|
|
184
203
|
readonly version?: number;
|
|
185
204
|
}): void {
|
|
186
|
-
const state = this.openByUri.get(params.uri);
|
|
205
|
+
const state = this.openByUri.get(normalizeDocumentUri(params.uri));
|
|
187
206
|
if (!state) return;
|
|
188
207
|
state.publishGeneration += 1;
|
|
189
208
|
state.lastPublish = {
|
|
@@ -197,7 +216,7 @@ export class WorkspaceDocumentState {
|
|
|
197
216
|
}
|
|
198
217
|
|
|
199
218
|
resolvePushDiagnostics(snapshot: DiagnosticSnapshot): PushDiagnosticsResolution {
|
|
200
|
-
const state = this.openByUri.get(snapshot.uri);
|
|
219
|
+
const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
|
|
201
220
|
if (!state?.lastPublish) return { status: "missing" };
|
|
202
221
|
const publish = state.lastPublish;
|
|
203
222
|
if (publish.version !== undefined) {
|
|
@@ -214,18 +233,18 @@ export class WorkspaceDocumentState {
|
|
|
214
233
|
}
|
|
215
234
|
|
|
216
235
|
waitForDiagnosticsActivity(snapshot: DiagnosticSnapshot, timeoutMs: number): Promise<void> {
|
|
217
|
-
const state = this.openByUri.get(snapshot.uri);
|
|
236
|
+
const state = this.openByUri.get(normalizeDocumentUri(snapshot.uri));
|
|
218
237
|
if (!state || timeoutMs <= 0) return Promise.resolve();
|
|
219
238
|
return new Promise((resolveActivity) => {
|
|
220
239
|
let settled = false;
|
|
221
240
|
const finish = () => {
|
|
222
241
|
if (settled) return;
|
|
223
242
|
settled = true;
|
|
224
|
-
clearTimeout(timer);
|
|
243
|
+
this.timerProvider.clearTimeout(timer);
|
|
225
244
|
state.waiters.delete(finish);
|
|
226
245
|
resolveActivity();
|
|
227
246
|
};
|
|
228
|
-
const timer = setTimeout(finish, timeoutMs);
|
|
247
|
+
const timer = this.timerProvider.setTimeout(finish, timeoutMs);
|
|
229
248
|
if (typeof timer.unref === "function") timer.unref();
|
|
230
249
|
state.waiters.add(finish);
|
|
231
250
|
});
|
|
@@ -329,7 +348,7 @@ export class WorkspaceDocumentState {
|
|
|
329
348
|
waiters: new Set(),
|
|
330
349
|
};
|
|
331
350
|
this.openDocuments.set(path, state);
|
|
332
|
-
this.openByUri.set(state.uri, state);
|
|
351
|
+
this.openByUri.set(normalizeDocumentUri(state.uri), state);
|
|
333
352
|
this.notifyWaiters(state);
|
|
334
353
|
await this.sendNotification("textDocument/didOpen", {
|
|
335
354
|
textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text },
|
|
@@ -356,7 +375,7 @@ export class WorkspaceDocumentState {
|
|
|
356
375
|
|
|
357
376
|
private async closeDocument(state: OpenDocumentState): Promise<void> {
|
|
358
377
|
this.openDocuments.delete(state.path);
|
|
359
|
-
this.openByUri.delete(state.uri);
|
|
378
|
+
this.openByUri.delete(normalizeDocumentUri(state.uri));
|
|
360
379
|
this.clearDiagnostics(state.uri);
|
|
361
380
|
this.notifyWaiters(state);
|
|
362
381
|
await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
|
|
@@ -1 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/** Repository marker: an ancestor containing this always wins over intermediate package markers. */
|
|
2
|
+
export const GIT_WORKSPACE_MARKER = ".git" as const;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Package/project markers. On their own they resolve to the NEAREST marked directory, but inside a
|
|
6
|
+
* request cwd they are only a fallback: a `.git` ancestor further up collapses nested package roots
|
|
7
|
+
* (one client per repository instead of one per monorepo package).
|
|
8
|
+
*/
|
|
9
|
+
export const PROJECT_WORKSPACE_MARKERS = [
|
|
10
|
+
"package.json",
|
|
11
|
+
"pyproject.toml",
|
|
12
|
+
"Cargo.toml",
|
|
13
|
+
"go.mod",
|
|
14
|
+
"pom.xml",
|
|
15
|
+
"build.gradle",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS] as const;
|
|
@@ -71,6 +71,9 @@ export async function runMcpStdioServer(
|
|
|
71
71
|
await runJsonRpcStdioServer({
|
|
72
72
|
input,
|
|
73
73
|
output,
|
|
74
|
+
// Idle stays disabled: hosts of this CLI (legacy opencode configs and
|
|
75
|
+
// standalone lsp-tools-mcp users) have no evidenced respawn for an exited
|
|
76
|
+
// stdio server; opencode in particular marks it failed without retry.
|
|
74
77
|
idleTimeoutMs: 0,
|
|
75
78
|
parentWatchdog: options.parentWatchdog ?? {},
|
|
76
79
|
handler: (request) => runWithRequestContext(requestContext, () => handleLspMcpRequest(request)),
|