Introduces:
- src/shared/process-stream-reader.ts: Buffer-concat stream reader compatible with both Bun and Node ChildProcess stdout (replaces Web Response API usage)
- src/tools/shared/search-process-output.ts: structured subprocess output collector with timeout, kill, and rejection cleanup
- bun-spawn-shim hardened: Node path forces windowsHide: true; spawn errors no longer escape as unhandledRejection
Foundation for #3919 fix.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
OpenCode Desktop's Electron sidecar runtime can omit Bun's ctx.$ helper.
The sender previously called ctx.$ unconditionally, throwing
TypeError: ctx.$ is not a function as unhandledRejection and crashing
the sidecar with exit code 1.
Add a runtime guard at every call site, falling back to Node.js
child_process.execFile (with windowsHide: true) when ctx.$ is missing.
The Bun ctx.$ path remains preferred when available. Every notification
path is wrapped in try/catch so no failure escapes as unhandledRejection.
Fixes#4128Fixes#4061
The migration log message previously pointed users to docs/reference/configuration.md for the new LSP config location, but that doc section still showed the obsolete plugin-level 'lsp' block. A user following the guidance would re-add the same 'lsp' key, see it stripped again on next startup, and never reach a usable config.\n\nFix both sides: rewrite the log message so it is self-contained (states the new path .opencode/lsp.json and the consumer directly) and rewrite the LSP section in docs/reference/configuration.md to describe the actual current architecture (LSP served by the 'lsp' MCP server, reading server map from .opencode/lsp.json via LSP_TOOLS_MCP_PROJECT_CONFIG, schema lives in packages/lsp-tools-mcp).\n\nVerification: bun test src/shared/migration/ -> 26/26 pass. bun run typecheck -> exit 0. Manual probe -> migration still strips lsp from both in-memory and persisted file.
Issue 1: hasNewCommentsOnly() now returns false when oldString and newString
both contain comment syntax and the new lines are a subset of old lines —
preventing the hook from firing on comment-only modifications.
Issue 2: Per-session deduplication via sessionLastWarning Map with a 30s
window (DEDUP_WINDOW_MS). At most one warning fires per session per
response turn, breaking the deadloop on consecutive edits.
The lsp config block was removed from OhMyOpenCodeConfigSchema when LSP tools were migrated from native plugin tools to the lsp tier-1 MCP server (packages/lsp-tools-mcp). Zod v4 strips unknown keys silently on safeParse, so a v3-era oh-my-opencode.jsonc with 'lsp': { typescript: { command: ... } } continues to live in the file unchanged while doing absolutely nothing. The user reporting #4225 saw their custom LSP servers stop working with zero indication that the configuration site moved.\n\nAdd a migrator that removes the orphan lsp key during migrateConfigFile, mirroring the existing omo_agent -> sisyphus_agent migration immediately above and the 'Removed obsolete hooks from disabled_hooks' precedent below. A single log line records the configPath and the list of dropped server keys so the user has a paper trail in oh-my-opencode.log, and needsWrite is flipped so the cleanup persists to disk (with a timestamped backup) the next time the plugin loads.\n\nReproduction (clean upstream/dev, BEFORE fix):\n needsWrite=false\n inMemory.lsp=<original block, kept>\n persisted.lsp=<original block, kept>\n\nVerification (AFTER fix):\n needsWrite=true\n inMemory.lsp=undefined\n persisted.lsp=undefined\n\nbun test src/shared/migration/ -> 26/26 pass (24/24 pre-existing + 2 new regression tests). bun run typecheck -> exit 0.
When AGENT_MODEL_REQUIREMENTS gates an agent (sisyphus / hephaestus / atlas / general agents) and resolution returns no model -- or the requiresModel / requiresAnyModel / requiresProvider constraint is not satisfied by any connected provider -- the agent was being dropped from the registered set with no log, no warning, no doctor signal. Users upgrading from v3 to v4 saw their configured agents disappear from 'opencode agent list' with zero indication of why, and the only debugging path was reading the source.\n\nAdd a log() call at every silent-drop point in the four wrapper files so the user-facing 'oh-my-opencode.log' shows exactly which agent was skipped and which model / provider requirement failed. Follows the existing precedent in src/tools/delegate-task/categories.ts:44 ([resolveCategoryConfig] Category X requires Y but not available) and the in-file precedent in general-agents.ts:86 ([agent-registration] User-configured model not resolved). Zero behavior change -- agents that were skipped before are still skipped now, the only difference is they are no longer silent.\n\nVerification: bun test src/agents/builtin-agents/ -> 27/27 pass. bun test src/agents/ -> 405/405 pass. bun run typecheck -> exit 0. LSP diagnostics clean on all four touched files.
The dist-side bug reported in #4220 (`path.endsWith(\"dist/cli.js\")` failing on
Windows backslash paths) has already been fixed in source by routing through
`hasCliSuffix` in `src/mcp/ast-grep.ts` and `src/mcp/lsp.ts`. The `cli-suffix.test.ts`
covered the lsp-tools-mcp shape but not the ast_grep shape.
Adds a regression case that asserts a Windows-style absolute path containing
`...\\packages\\ast-grep-mcp\\dist\\cli.js` matches both `\"dist/cli.js\"` and the
fully-qualified `\"packages/ast-grep-mcp/dist/cli.js\"` suffix — closing the
exact symptom from the bug report against future regressions.
`openai/gpt-5.3-codex` is the codex-series powerhouse still recommended
in docs/guide/agent-model-matching.md and listed in the default
fallback chain in docs/reference/configuration.md, not a deprecated
alias for `gpt-5.4`. The migration entry silently rewrote any user
config that picked `gpt-5.3-codex` for its token efficiency, sending
agents to a non-codex model on every startup.
Drop the bogus mapping from MODEL_VERSION_MAP and add a regression
test that explicit `gpt-5.3-codex` selections (including in nested
fallback_models) survive `migrateModelVersions`. Users already
auto-migrated previously can revert to `gpt-5.3-codex` by hand and it
will now stick on subsequent loads regardless of the sidecar history.
default-mode (system-transform):
- e5463e2db introduced auto-activation of ultrawork+ralph-loop, and
dc2e082ac then skipped the ultrawork system prompt whenever ralph_loop
was also enabled. Net effect: the keyword-detector still showed
'Default ultrawork mode enabled' to the user, but the first turn had
none of the ultrawork behavior. Loop continuation kept the ultrawork
prefix, so the contract was honored only on later iterations.
- Drop the skip so the initial turn matches what the toast advertises.
New matrix test pins all four (ultrawork, ralph_loop) combinations.
multimodal-looker:
- Prompt claimed 'read' and 'call_omo_agent' were available, but the
look_at invocation runtime explicitly disables both via READ_ENABLED
and createAgentToolAllowlist([]). Small VL models trusted the prompt
and looped on rejected tool calls (#4116).
- Rewrite the agent prompt to describe direct-attachment analysis and
forbid tool/agent calls. Add a consistency test that extracts the
prompt's 'available tools' claim and compares it against the
configured allowlist.
delegate-task (skill-resolver):
- 088693697 filtered per-agent restricted skills at the skill tool and
builtin agent prompt layers, but delegate-task itself happily injected
whatever skill name a caller passed. A target agent could be force-fed
a skill marked agent: oracle just by listing it in load_skills.
- Thread the target agent through resolveSkills and silently filter
skills whose definition.agent does not include it. Public skills with
no restriction are unaffected. Regression test pins the bypass.
notepad-write-guard:
- The hook was created by create-tool-guard-hooks but tool-execute-before
never invoked it, so the guard was inert.
- It also only matched .sisyphus/notepads, missing the current
.omo/notepads layout introduced by the workspace migration.
- Add the dispatch call alongside writeExistingFileGuard, and extend
NOTEPAD_ROOTS to cover both paths via normalize() + sep. New
integration test pins the wire and the .omo block; the existing unit
test now asserts both paths.
start-work session-plan-affinity:
- PLAN_PATH_PATTERN only matched .sisyphus/plans, so sessions referring
to plans under .omo/plans returned null and start-work missed the
current session's own plan.
- Extend the regex to .(sisyphus|omo)/plans and add findPrometheusPlans
in packages/boulder-state to scan both directories during the
transition. New regression test pins .omo/plans matching; legacy
.sisyphus/plans coverage preserved.
6ffea1bc3 added i18n with en/zh locales and plugin config support, but
the initI18n() call lived in the original src/index.ts. When src/index.ts
became an 18-line wrapper that delegates to
src/testing/create-plugin-module.ts createPluginModule(), the call site
was dropped on the floor. Result: i18n.locale config and LANG env both
ignored at runtime, every toast stayed English regardless of user
setting.
Inject initI18n as a managed dependency and call it in
createPluginModule() immediately after loadPluginConfig(), passing
pluginConfig.i18n?.locale through. Add an integration test that boots
the plugin with i18n.locale='zh' and asserts getLocale() returns 'zh'
and t('toast.task_completed') returns the Chinese string. Regression
locked - subsequent moves of the startup path will fail loudly.
- prompt-async-gate.test.ts: refactor ced36bffc removed
promptAsyncAfterSessionIdle in favor of the unified
dispatchInternalPrompt({ mode: 'async', ... }). One call site at
line 1441 was left behind. Replace it with the current API and pass
the explicit dispatchTimeoutMs so the status-timeout semantics are
preserved. Also switch the surrounding tests to the third-argument
timeout form so Bun's typings stay happy.
- runtime-model-readers.test.ts: implementation moved to
packages/model-core during the layering refactor; the orphaned test
still pointed at './runtime-model-readers'. Switch to the package
export via getModelCapabilities and keep the modality-reader
coverage by deriving keys through the package API.
mock.restore() in afterAll is global in Bun, so the previous attempt to
satisfy mock-module-lifecycle-audit by pairing mock.module('./logger')
with afterAll(() => mock.restore()) tore down mocks owned by other
tests. Concretely, running this file before src/hooks/runtime-fallback
produced 42 cascading fallback test failures.
The logger mock was decorative - tests do not assert log calls, and the
real logger only writes to oh-my-opencode.log in the OS temp dir. Drop
the mock entirely so the audit has nothing to score and other tests
keep their mocks intact.
- prometheus-prompt.test.ts: close missing }) on the OpenSpec expanded
commands describe block (introduced by d66b6bcbf, parse error).
- agent-sort-shim/agent-config-integration/continuation-injection/
unstable-agent-babysitter/subagent-resolver/sync-executor/
resolve-caller-team-lead tests: expect 'Sisyphus - ultraworker'
(lowercase) to match production after cd39f8858, which lowercased the
display name to dodge a TUI ZWSP rendering glitch. Legacy uppercase
inputs that exercise the normalization path are preserved.
- sync-executor.ts + resolve-caller-team-lead.ts: route legacy display
name inputs through normalizeAgentForPrompt so prompt agent names and
caller team lead lookups produce the canonical lowercase form.
PR #4146 fixed the async resolvers (resolveSkillContentAsync, resolveMultipleSkillsAsync) to support unambiguous short-name skill lookups by routing through matchSkillByName. But the public getSkillByName API (exported from src/features/opencode-skill-loader/) was left on the old skills.find(s => s.name === name) lookup, so any external caller asking for 'systematic-debugging' on a 'superpowers/systematic-debugging' skill silently gets undefined.
Wire getSkillByName through the same matchSkillByName helper so the public API matches the async resolution semantics:
- Exact full-name lookup (case-insensitive) is preferred.
- Unambiguous short-name lookup falls back to the unique namespaced match.
- Ambiguous short-name (>=2 namespaces share the same short tail) returns undefined, mirroring matchSkillByName.
Regression tests added to src/features/opencode-skill-loader/loader.test.ts under a new 'getSkillByName' describe block:
- exact full-name returns the skill
- namespaced skill resolves by its unique short name
- ambiguous short name returns undefined
Verification: 3/3 new cases pass, 129/129 full src/features/opencode-skill-loader tests pass (was 126/126 pre-fix), bun run typecheck clean.
The ZAI (Zhipu) provider emits 'Weekly/Monthly Limit Exhausted. Your limit will reset at YYYY-MM-DD HH:MM:SS' when the coding-plan subscription quota is hit. None of the existing quota regex patterns (/quota.?exceeded/, /usage\s+limit/, /exhausted\s+your\s+capacity/, /credit\s+balance.*too\s+low/, etc.) match the 'Limit Exhausted' phrasing, so the runtime-fallback never fires and the user is stuck on the dead model.
Add /limit\s+exhausted/i to both pattern lists that gate fallback dispatch:
- RETRYABLE_ERROR_PATTERNS in constants.ts (text-pattern path used by extractStatusCode + retryable scan)
- classifyErrorType quota_exceeded branch in error-classifier.ts (typed classification path used by isRetryableError)
The pattern is intentionally narrow: it requires the literal token 'Limit' followed by whitespace then 'Exhausted'. It matches the ZAI weekly, monthly, and combined Weekly/Monthly variants but does not collide with unrelated phrases such as 'context limit' or 'rate limit' that already have their own dedicated patterns.
Regression coverage added to quota-error-classifier.regression.test.ts:
- 'Weekly/Monthly Limit Exhausted. Your limit will reset at 2026-05-20 15:43:27' -> quota_exceeded + retryable=true
- 'Weekly Limit Exhausted. Your limit will reset at 2026-05-28 10:30:00' -> quota_exceeded + retryable=true
Verification: 11/11 quota-error-classifier.regression.test.ts pass (was 9 pass + 2 fail pre-fix). Broader runtime-fallback suite goes from 135/196 pass to 137/198 pass (the 61 pre-existing failures are unrelated to this change and reproduce on a clean upstream/dev checkout). bun run typecheck clean.
src/hooks/comment-checker/apply-patch-edits.ts was already a pure re-export over @oh-my-opencode/comment-checker-core after the core extraction landed. Every importer now reaches into the package directly, so the shim has no remaining call sites and can be removed.
Verified: rg "comment-checker/apply-patch-edits" src/ packages/ returns no matches.
Promote the project-rule constants (PROJECT_MARKERS, PROJECT_RULE_SUBDIRS, PROJECT_RULE_FILES, OPENCODE_USER_RULE_DIRS, USER_RULE_DIR, GITHUB_INSTRUCTIONS_PATTERN, RULE_EXTENSIONS, AGENTS_FILENAME, etc.) and the findAgentsMdUp walk-up helper out of the agents-md-core and rules-injector adapters and into @oh-my-opencode/rules-engine, the single owner of rule discovery.
- packages/agents-md-core/ drops the findAgentsMdUp/AgentsMdDiscoveryInput wrappers (now sourced directly from rules-engine) and its constants module re-exports AGENTS_FILENAME from rules-engine instead of duplicating it.
- src/hooks/directory-agents-injector/finder.ts pulls findAgentsMdUp from rules-engine directly while still re-exporting resolveFilePath from agents-md-core.
- src/hooks/rules-injector/constants.ts becomes a pure re-export shim over the rules-engine constants.
Add packages/agents-md-core/src/injector.test.ts to lock the root-skipping AGENTS.md injection order so future changes to findAgentsMdUp cannot silently regress the [Directory Context: ...] block format the injector emits.
Tests: bun test packages/agents-md-core packages/rules-engine src/hooks/directory-agents-injector src/hooks/rules-injector
Move three pure helpers from src/shared/ into @oh-my-opencode/model-core so the package can stand alone without depending on plugin internals:
- buildModelCapabilitiesSnapshotFromModelsDev + fetchModelCapabilitiesSnapshot (models.dev normalization)
- parseModelSuggestion (cross-provider ProviderModelNotFoundError suggestion extraction)
- resolveActualContextLimit (Anthropic GA 1M context override)
Split provider-model-id-transform into two variants exposed by model-core:
- transformModelForProvider keeps the runtime dash to dot Anthropic rewrite used by the SDK
- transformModelForProviderDisplay preserves hyphenated Anthropic IDs so the installer writes registry-compatible model strings, fixing the ProviderModelNotFoundError fresh installs hit when the dotted form leaks into the config
src/shared/* and src/cli/provider-model-id-transform.ts collapse to re-export shims that point at the new core modules. Stale src/shared/{known-variants,model-capability-aliases,model-capability-guardrails,model-capability-heuristics}.ts re-export files plus the duplicated context-limit-resolver test are removed in favor of the canonical model-core copies.
Tests: bun test packages/model-core src/shared/model-capabilities-cache.test.ts src/cli/provider-model-id-transform.test.ts
Move the hash-anchored edit core (hash computation, validation, edit operations, text normalization, chunk formatter, diff utilities, and a runtime-aware xxHash32 binding) into a new @oh-my-opencode/hashline-core workspace package.
The src/tools/hashline-edit/ surface becomes a set of thin re-export shims, so existing import paths in the plugin keep working while the pure logic lives behind a stable package boundary that has no opencode runtime dependencies.
Tests: bun test packages/hashline-core src/tools/hashline-edit
Treat resolved abort responses with a non-null error payload the same as rejected aborts. This prevents stale-timeout cancellation bookkeeping from reporting success when the child session was not actually aborted.
Plan: .omo/plans/subagent-timeout-active-output.md
The model emits tool names like mcp_background_output but the runtime
registry has them as background_output. While transformToolName already
handles the prefix for display purposes, the tool dispatch path in
tool-execute-before was not stripping it, causing 'unavailable tool' errors.
This adds mcp_ prefix stripping at the earliest point in the tool
execution pipeline, fixing background_output, background_cancel, and
all nocturne-memory_* tools.
Closes#2697
Convert OpenCode v2 session.next stream events into the existing message part activity path so child sessions that are still producing text, reasoning, or tool output refresh lastUpdate before stale polling runs.
This keeps the timeout poller from cancelling active subagents and preserves tool-call progress for session.next.tool.called events.
Plan: .omo/plans/subagent-timeout-active-output.md
The hyperplan trigger \b(hyperplan|hpp)\b/i matched 'hpp' inside common C++ header references like 'check interface.hpp' or 'open buffer.hpp'. The leading '.' is a non-word character, so \b is already satisfied and the false positive fires the hyperplan-mode prompt on routine code questions.
Split the alternation so 'hpp' additionally requires that the preceding character is neither a word character nor a '.'. This preserves every existing trigger ('hpp do this', '/hpp ...', mid-sentence usage, mixed case) while rejecting filename uses of the .hpp extension. The longer 'hyperplan' keyword keeps the original \b boundary semantics.
Reproduction (added regression tests):
- 'please help to check interface.hpp' must NOT fire
- 'open src/include/audio/buffer.hpp and fix the leak' must NOT fire
All 14 cases in hyperplan.test.ts pass (12 existing + 2 new), broader keyword-detector suite stays green (92 pass), typecheck clean.