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
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
5
5
|
|
|
6
6
|
import { createStandaloneMcpRequestContext, runWithRequestContext } from "../request-context.js";
|
|
7
|
+
import type { LspClient } from "./client.js";
|
|
7
8
|
import { findWorkspaceRoot, resolveReadablePathInsideContext, withLspClient } from "./client-wrapper.js";
|
|
8
9
|
import { LspInvalidPathError } from "./errors.js";
|
|
9
10
|
import { LspManager } from "./manager.js";
|
|
@@ -22,6 +23,102 @@ function tempRoot(prefix: string): string {
|
|
|
22
23
|
return root;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
describe("LSP workspace root collapse in monorepos", () => {
|
|
27
|
+
it("#given a git repository containing four package.json packages #when resolving workspace roots for files in each package #then all collapse to the repository root", () => {
|
|
28
|
+
const root = tempRoot("lsp-client-wrapper-monorepo-");
|
|
29
|
+
mkdirSync(join(root, ".git"), { recursive: true });
|
|
30
|
+
const packageDirs = ["alpha", "beta", "gamma", "delta"].map((name) => join(root, "packages", name));
|
|
31
|
+
for (const packageDir of packageDirs) {
|
|
32
|
+
mkdirSync(join(packageDir, "src"), { recursive: true });
|
|
33
|
+
writeFileSync(join(packageDir, "package.json"), "{}\n");
|
|
34
|
+
writeFileSync(join(packageDir, "src", "file.ts"), "export const value = 1;\n");
|
|
35
|
+
}
|
|
36
|
+
const context = createStandaloneMcpRequestContext({ cwd: root });
|
|
37
|
+
|
|
38
|
+
const roots = packageDirs.map((packageDir) =>
|
|
39
|
+
runWithRequestContext(context, () => findWorkspaceRoot(join(packageDir, "src", "file.ts"))),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(new Set(roots).size).toBe(1);
|
|
43
|
+
expect(roots[0]).toBe(realpathSync(root));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("#given nested package.json markers without any .git #when resolving workspace #then keeps the nearest package root", () => {
|
|
47
|
+
const root = tempRoot("lsp-client-wrapper-nested-");
|
|
48
|
+
const nested = join(root, "parent", "nested");
|
|
49
|
+
mkdirSync(join(nested, "src"), { recursive: true });
|
|
50
|
+
writeFileSync(join(root, "parent", "package.json"), "{}\n");
|
|
51
|
+
writeFileSync(join(nested, "package.json"), "{}\n");
|
|
52
|
+
writeFileSync(join(nested, "src", "file.ts"), "export const value = 1;\n");
|
|
53
|
+
|
|
54
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
55
|
+
findWorkspaceRoot(join(nested, "src", "file.ts")),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(workspace).toBe(realpathSync(nested));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("#given four monorepo package files under one .git root #when acquiring clients #then one client serves the whole repository", async () => {
|
|
62
|
+
const root = tempRoot("lsp-client-wrapper-oneclient-");
|
|
63
|
+
mkdirSync(join(root, ".git"), { recursive: true });
|
|
64
|
+
const userConfig = join(root, "user-lsp.json");
|
|
65
|
+
writeFileSync(
|
|
66
|
+
userConfig,
|
|
67
|
+
JSON.stringify({ lsp: { monorepoProbe: { command: [process.execPath], extensions: [".monoroot"] } } }),
|
|
68
|
+
);
|
|
69
|
+
const filePaths = ["alpha", "beta", "gamma", "delta"].map((name) => {
|
|
70
|
+
const packageDir = join(root, "packages", name);
|
|
71
|
+
mkdirSync(join(packageDir, "src"), { recursive: true });
|
|
72
|
+
writeFileSync(join(packageDir, "package.json"), "{}\n");
|
|
73
|
+
const filePath = join(packageDir, "src", "probe.monoroot");
|
|
74
|
+
writeFileSync(filePath, "const value = 1;\n");
|
|
75
|
+
return filePath;
|
|
76
|
+
});
|
|
77
|
+
const factoryRoots: string[] = [];
|
|
78
|
+
const manager = new LspManager({
|
|
79
|
+
clientFactory: (workspaceRoot) => {
|
|
80
|
+
factoryRoots.push(workspaceRoot);
|
|
81
|
+
return new StubLspClient() as unknown as LspClient;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const context = createStandaloneMcpRequestContext({
|
|
87
|
+
cwd: root,
|
|
88
|
+
env: { LSP_TOOLS_MCP_USER_CONFIG: userConfig },
|
|
89
|
+
});
|
|
90
|
+
await Promise.all(
|
|
91
|
+
filePaths.map((filePath) =>
|
|
92
|
+
runWithRequestContext(context, () =>
|
|
93
|
+
withLspClient(filePath, async () => undefined, "diagnostics", { manager }),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(factoryRoots).toEqual([realpathSync(root)]);
|
|
99
|
+
expect(manager.clientCount()).toBe(1);
|
|
100
|
+
} finally {
|
|
101
|
+
await manager.stopAll();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
class StubLspClient {
|
|
107
|
+
async start(): Promise<void> {}
|
|
108
|
+
|
|
109
|
+
async initialize(): Promise<void> {}
|
|
110
|
+
|
|
111
|
+
isAlive(): boolean {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
command(): string[] {
|
|
116
|
+
return [process.execPath];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async stop(): Promise<void> {}
|
|
120
|
+
}
|
|
121
|
+
|
|
25
122
|
describe("LSP client path confinement", () => {
|
|
26
123
|
it("#given a relative file inside context cwd #when resolving workspace #then marker search stays inside cwd", () => {
|
|
27
124
|
const root = tempRoot("lsp-client-wrapper-root-");
|
|
@@ -21,7 +21,7 @@ import { LSP_LOCAL_INSTALL_HINTS } from "./server-definitions.js";
|
|
|
21
21
|
import { loadInstallDecision } from "./server-install-state.js";
|
|
22
22
|
import { findServerForExtension } from "./server-resolution.js";
|
|
23
23
|
import type { ServerLookupResult } from "./types.js";
|
|
24
|
-
import {
|
|
24
|
+
import { GIT_WORKSPACE_MARKER, PROJECT_WORKSPACE_MARKERS } from "./workspace-markers.js";
|
|
25
25
|
|
|
26
26
|
export function isDirectoryPath(filePath: string): boolean {
|
|
27
27
|
try {
|
|
@@ -31,6 +31,15 @@ export function isDirectoryPath(filePath: string): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the workspace root for a file inside the request cwd.
|
|
36
|
+
*
|
|
37
|
+
* A `.git`-containing ancestor always wins over nearer package markers, so all packages of a
|
|
38
|
+
* monorepo share one workspace root (one resident client per repository instead of one per
|
|
39
|
+
* package, which multiplies language-server processes by the package count). The nearest
|
|
40
|
+
* package-marked directory is remembered while walking and used only when no `.git` ancestor
|
|
41
|
+
* exists within the request cwd.
|
|
42
|
+
*/
|
|
34
43
|
export function findWorkspaceRoot(filePath: string): string {
|
|
35
44
|
const cwd = contextCwd();
|
|
36
45
|
const abs = resolveReadablePathInsideContext(filePath);
|
|
@@ -43,20 +52,26 @@ export function findWorkspaceRoot(filePath: string): string {
|
|
|
43
52
|
if (!isPathInside(cwd, abs)) return findWorkspaceRootOutsideContext(dir);
|
|
44
53
|
|
|
45
54
|
const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
|
|
55
|
+
let nearestPackageRoot: string | undefined;
|
|
46
56
|
while (isPathInside(cwd, dir)) {
|
|
47
57
|
const canonicalDir = existingDirectoryInsideContext(dir, cwd);
|
|
48
58
|
if (canonicalDir !== undefined) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
if (existsSync(join(dir, GIT_WORKSPACE_MARKER))) {
|
|
60
|
+
return canonicalDir;
|
|
61
|
+
}
|
|
62
|
+
if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
|
|
63
|
+
nearestPackageRoot = canonicalDir;
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
if (dir === cwd) break;
|
|
56
67
|
dir = dirname(dir);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
return fallbackRoot;
|
|
70
|
+
return nearestPackageRoot ?? fallbackRoot;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasProjectWorkspaceMarker(directory: string): boolean {
|
|
74
|
+
return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync(join(directory, marker)));
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
export function resolveReadablePathInsideContext(filePath: string): string {
|
|
@@ -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 =
|
|
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 -
|
|
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 -
|
|
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
|
|
@@ -10,3 +10,8 @@ export const MAX_RESIDENT_CLIENTS = 6;
|
|
|
10
10
|
export const REAPER_INTERVAL_MS = 60_000;
|
|
11
11
|
export const STOP_HARD_KILL_TIMEOUT_MS = 5_000;
|
|
12
12
|
export const STOP_SIGKILL_GRACE_MS = 1_000;
|
|
13
|
+
|
|
14
|
+
/** Max consecutive dead client generations respawned for one key before admission is blocked. */
|
|
15
|
+
export const CLIENT_RESPAWN_RETRY_LIMIT = 2;
|
|
16
|
+
/** How long a spent respawn budget blocks admission before a fresh attempt window opens. */
|
|
17
|
+
export const CLIENT_RESPAWN_COOLDOWN_MS = 60_000;
|
|
@@ -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
|
+
}
|
|
@@ -69,6 +69,21 @@ export class LspProcessSpawnError extends Error {
|
|
|
69
69
|
override readonly name = "LspProcessSpawnError";
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export class LspClientRespawnBudgetExceededError extends Error {
|
|
73
|
+
override readonly name = "LspClientRespawnBudgetExceededError";
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
readonly serverId: string,
|
|
77
|
+
readonly root: string,
|
|
78
|
+
readonly retryLimit: number,
|
|
79
|
+
) {
|
|
80
|
+
super(
|
|
81
|
+
`LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` +
|
|
82
|
+
`(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
export function isLspDeadConnectionError(err: unknown): err is LspConnectionClosedError | LspProcessExitedError {
|
|
73
88
|
return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
|
|
74
89
|
}
|
|
@@ -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
|
|
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
|
-
}
|
|
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,262 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { LspClient } from "./client.js";
|
|
4
|
+
import { LspManager } from "./manager.js";
|
|
5
|
+
import type { ResolvedServer } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const SERVER: ResolvedServer = {
|
|
8
|
+
id: "typescript",
|
|
9
|
+
command: ["typescript-language-server", "--stdio"],
|
|
10
|
+
extensions: [".ts"],
|
|
11
|
+
priority: 1,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Simulates a stop that lingers (production stops reach ~21s), which is the behavior under test. */
|
|
15
|
+
const SLOW_STOP_MS = 50;
|
|
16
|
+
|
|
17
|
+
interface RecordingClientOptions {
|
|
18
|
+
readonly stopDelayMs: number;
|
|
19
|
+
readonly initializeError?: Error;
|
|
20
|
+
readonly alive: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class RecordingFakeClient {
|
|
24
|
+
alive: boolean;
|
|
25
|
+
stopCallCount = 0;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
readonly root: string,
|
|
29
|
+
private readonly log: EventLog,
|
|
30
|
+
private readonly options: RecordingClientOptions,
|
|
31
|
+
) {
|
|
32
|
+
this.alive = options.alive;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async start(): Promise<void> {
|
|
36
|
+
this.log.push(`spawn:${this.root}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initialize(): Promise<void> {
|
|
40
|
+
if (this.options.initializeError !== undefined) {
|
|
41
|
+
throw this.options.initializeError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isAlive(): boolean {
|
|
46
|
+
return this.alive;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
command(): string[] {
|
|
50
|
+
return [...SERVER.command];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async stop(): Promise<void> {
|
|
54
|
+
this.stopCallCount += 1;
|
|
55
|
+
this.log.push(`stop-start:${this.root}`);
|
|
56
|
+
await delay(this.options.stopDelayMs);
|
|
57
|
+
this.alive = false;
|
|
58
|
+
this.log.push(`stop-end:${this.root}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class EventLog {
|
|
63
|
+
readonly entries: string[] = [];
|
|
64
|
+
private waiters: Array<{ readonly event: string; readonly resolve: () => void }> = [];
|
|
65
|
+
|
|
66
|
+
push(event: string): void {
|
|
67
|
+
this.entries.push(event);
|
|
68
|
+
const due = this.waiters.filter((waiter) => waiter.event === event);
|
|
69
|
+
if (due.length === 0) return;
|
|
70
|
+
this.waiters = this.waiters.filter((waiter) => waiter.event !== event);
|
|
71
|
+
for (const waiter of due) waiter.resolve();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
waitFor(event: string): Promise<void> {
|
|
75
|
+
if (this.entries.includes(event)) return Promise.resolve();
|
|
76
|
+
return new Promise<void>((resolve) => {
|
|
77
|
+
this.waiters.push({ event, resolve });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function delay(ms: number): Promise<void> {
|
|
83
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface HarnessOptions {
|
|
87
|
+
readonly maxResidentClients?: number;
|
|
88
|
+
readonly stopDelayMs?: number;
|
|
89
|
+
/** Leading generations whose initialize() rejects; later generations start cleanly. */
|
|
90
|
+
readonly initializeFailures?: number;
|
|
91
|
+
/** Generations at or above this index report alive; earlier ones emulate a crash-looping server. */
|
|
92
|
+
readonly aliveFromGeneration?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function managerHarness(options: HarnessOptions = {}): {
|
|
96
|
+
readonly manager: LspManager;
|
|
97
|
+
readonly clients: RecordingFakeClient[];
|
|
98
|
+
readonly log: EventLog;
|
|
99
|
+
readonly clock: { value: number };
|
|
100
|
+
} {
|
|
101
|
+
const clients: RecordingFakeClient[] = [];
|
|
102
|
+
const log = new EventLog();
|
|
103
|
+
const clock = { value: 1_000 };
|
|
104
|
+
const generations = new Map<string, number>();
|
|
105
|
+
const aliveFrom = options.aliveFromGeneration ?? 1;
|
|
106
|
+
const stopDelayMs = options.stopDelayMs ?? 0;
|
|
107
|
+
const initializeFailures = options.initializeFailures ?? 0;
|
|
108
|
+
const manager = new LspManager({
|
|
109
|
+
maxResidentClients: options.maxResidentClients ?? 6,
|
|
110
|
+
reaperIntervalMs: 60_000,
|
|
111
|
+
idleTimeoutMs: 60_000_000,
|
|
112
|
+
now: () => clock.value,
|
|
113
|
+
clientFactory: (root) => {
|
|
114
|
+
const generation = (generations.get(root) ?? 0) + 1;
|
|
115
|
+
generations.set(root, generation);
|
|
116
|
+
const client = new RecordingFakeClient(root, log, {
|
|
117
|
+
stopDelayMs,
|
|
118
|
+
alive: generation >= aliveFrom,
|
|
119
|
+
...(generation <= initializeFailures ? { initializeError: new Error("initialize boom") } : {}),
|
|
120
|
+
});
|
|
121
|
+
clients.push(client);
|
|
122
|
+
return client as unknown as LspClient;
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return { manager, clients, log, clock };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
describe("LspManager pending-stop tombstones", () => {
|
|
129
|
+
it("#given a slow-stopping client evicted at the resident cap #when the evicted root is re-acquired while the stop is pending #then the replacement spawns only after the old client stopped", async () => {
|
|
130
|
+
// given
|
|
131
|
+
const { manager, log } = managerHarness({ maxResidentClients: 1, stopDelayMs: SLOW_STOP_MS });
|
|
132
|
+
try {
|
|
133
|
+
await manager.getClient("/root-a", SERVER);
|
|
134
|
+
manager.releaseClient("/root-a", SERVER.id);
|
|
135
|
+
|
|
136
|
+
// when: admitting root-b synchronously evicts idle root-a (void stop), and root-a is
|
|
137
|
+
// re-acquired in the same window while the evicted client's stop is still pending.
|
|
138
|
+
const rootBPromise = manager.getClient("/root-b", SERVER);
|
|
139
|
+
const rootAPromise = manager.getClient("/root-a", SERVER);
|
|
140
|
+
await Promise.all([rootBPromise, rootAPromise]);
|
|
141
|
+
manager.releaseClient("/root-b", SERVER.id);
|
|
142
|
+
manager.releaseClient("/root-a", SERVER.id);
|
|
143
|
+
|
|
144
|
+
// then
|
|
145
|
+
expect(log.entries.filter((event) => event.endsWith("/root-a"))).toEqual([
|
|
146
|
+
"spawn:/root-a",
|
|
147
|
+
"stop-start:/root-a",
|
|
148
|
+
"stop-end:/root-a",
|
|
149
|
+
"spawn:/root-a",
|
|
150
|
+
]);
|
|
151
|
+
} finally {
|
|
152
|
+
await manager.stopAll();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("#given an invalidated client whose stop is still pending #when the same root is re-acquired #then the replacement spawns only after the invalidated client stopped", async () => {
|
|
157
|
+
// given
|
|
158
|
+
const { manager, log } = managerHarness({ stopDelayMs: SLOW_STOP_MS });
|
|
159
|
+
try {
|
|
160
|
+
await manager.getClient("/root-a", SERVER);
|
|
161
|
+
manager.releaseClient("/root-a", SERVER.id);
|
|
162
|
+
|
|
163
|
+
// when
|
|
164
|
+
manager.invalidateClient("/root-a", SERVER.id);
|
|
165
|
+
await manager.getClient("/root-a", SERVER);
|
|
166
|
+
|
|
167
|
+
// then
|
|
168
|
+
expect(log.entries).toEqual([
|
|
169
|
+
"spawn:/root-a",
|
|
170
|
+
"stop-start:/root-a",
|
|
171
|
+
"stop-end:/root-a",
|
|
172
|
+
"spawn:/root-a",
|
|
173
|
+
]);
|
|
174
|
+
} finally {
|
|
175
|
+
await manager.stopAll();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("#given a client whose initialization failed while its stop is pending #when another getClient races the stop #then the replacement spawns only after the failed client stopped", async () => {
|
|
180
|
+
// given
|
|
181
|
+
const { manager, log } = managerHarness({
|
|
182
|
+
stopDelayMs: SLOW_STOP_MS,
|
|
183
|
+
initializeFailures: 1,
|
|
184
|
+
});
|
|
185
|
+
try {
|
|
186
|
+
const failing = manager.getClient("/root-fail", SERVER);
|
|
187
|
+
|
|
188
|
+
// when: the second acquisition starts once the first one's stop is provably in flight.
|
|
189
|
+
await log.waitFor("stop-start:/root-fail");
|
|
190
|
+
const racing = manager.getClient("/root-fail", SERVER);
|
|
191
|
+
await expect(failing).rejects.toThrow("initialize boom");
|
|
192
|
+
await racing;
|
|
193
|
+
manager.releaseClient("/root-fail", SERVER.id);
|
|
194
|
+
|
|
195
|
+
// then
|
|
196
|
+
expect(log.entries).toEqual([
|
|
197
|
+
"spawn:/root-fail",
|
|
198
|
+
"stop-start:/root-fail",
|
|
199
|
+
"stop-end:/root-fail",
|
|
200
|
+
"spawn:/root-fail",
|
|
201
|
+
]);
|
|
202
|
+
} finally {
|
|
203
|
+
await manager.stopAll();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("LspManager dead-client respawn budget", () => {
|
|
209
|
+
it("#given a crash-looping client that is dead on every acquisition #when getClient is called repeatedly #then respawn attempts stay within the retry budget", async () => {
|
|
210
|
+
// given
|
|
211
|
+
const { manager, clients } = managerHarness({ aliveFromGeneration: Number.POSITIVE_INFINITY });
|
|
212
|
+
try {
|
|
213
|
+
// when
|
|
214
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
215
|
+
await manager
|
|
216
|
+
.getClient("/root-crash", SERVER)
|
|
217
|
+
.then(
|
|
218
|
+
() => manager.releaseClient("/root-crash", SERVER.id),
|
|
219
|
+
() => undefined,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// then: one initial generation plus at most two budgeted respawns.
|
|
224
|
+
expect(clients.length).toBeLessThanOrEqual(3);
|
|
225
|
+
} finally {
|
|
226
|
+
await manager.stopAll();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("#given the respawn budget is exhausted #when getClient is called again #then it rejects with a budget error instead of respawning", async () => {
|
|
231
|
+
// given
|
|
232
|
+
const { manager } = managerHarness({ aliveFromGeneration: Number.POSITIVE_INFINITY });
|
|
233
|
+
try {
|
|
234
|
+
await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
|
|
235
|
+
|
|
236
|
+
// when / then
|
|
237
|
+
await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
|
|
238
|
+
} finally {
|
|
239
|
+
await manager.stopAll();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("#given the budget was exhausted but the cooldown elapsed and the server recovered #when getClient is called #then it spawns again and resets the budget on a healthy generation", async () => {
|
|
244
|
+
// given: generations one and two crash, the third is healthy.
|
|
245
|
+
const { manager, clients, clock } = managerHarness({ aliveFromGeneration: 3 });
|
|
246
|
+
try {
|
|
247
|
+
await expect(manager.getClient("/root-crash", SERVER)).rejects.toThrow(/respawn budget/);
|
|
248
|
+
|
|
249
|
+
// when: cooldown elapses (60s default) and the server stays alive again.
|
|
250
|
+
clock.value += 60_001;
|
|
251
|
+
const recovered = await manager.getClient("/root-crash", SERVER);
|
|
252
|
+
manager.releaseClient("/root-crash", SERVER.id);
|
|
253
|
+
|
|
254
|
+
// then
|
|
255
|
+
expect(clients.length).toBe(3);
|
|
256
|
+
await expect(manager.getClient("/root-crash", SERVER)).resolves.toBe(recovered);
|
|
257
|
+
manager.releaseClient("/root-crash", SERVER.id);
|
|
258
|
+
} finally {
|
|
259
|
+
await manager.stopAll();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|