Handle OpenCode session events that carry the session ID under properties.info.id or properties.info.sessionID so background tasks and continuation hooks do not miss idle/error/delete events.
Add regression coverage for nested session.idle events completing background tasks and waking continuation hooks.
Only clear recentAnyIdles when the stored marker matches the synthetic idle timestamp for the same session, preventing accidental clobbering of newer idle markers. Add a regression test to verify other sessions keep their dedup state during this bypass path.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
When session.status(idle) is converted to synthetic session.idle and recorded in recentAnyIdles, a real session.idle arriving within 500ms was being dropped by the dedup logic. recentSyntheticIdles was cleared but recentAnyIdles persisted, causing TODO-DIAG to red-alert with 'no todossession.idle event'.
Fix: when real session.idle arrives, also clear recentAnyIdles entry so dedup does not drop it. Test renamed and expected dispatchCalls updated 1 → 2.
Fixes#2667
Uses new Function() to prevent the static ESM loader from seeing
the bun: protocol import at parse time. In Electron/Node.js, the
import() call is evaluated at runtime and gracefully returns null
when bun:sqlite is unavailable.
Fixes#3829, likely fixes#3762
Adds keyword_detector.disabled_keywords config so users can opt out of
specific keyword detectors individually without disabling the entire
keyword-detector hook. Allowed values: 'ultrawork', 'search', 'analyze',
'team'. Default empty/missing -> all four detectors active (no behavior
change for existing configs).
Motivation: an audit revealed search and analyze patterns trigger on
~30-60% of normal conversational user messages (e.g. 'how to', 'why is',
'show me', '왜', '어떻게'). The disable list is the immediate kill switch
while the patterns themselves are tightened in a separate PR.
Schema follows the existing per-feature config block convention shared
by team_mode, ralph_loop, runtime_fallback, and comment_checker. The
KeywordType enum (z.enum) lives next to the config schema and is
re-imported by the detector to keep the union type in lockstep with the
schema.
Threading:
pluginConfig.keyword_detector
-> create-transform-hooks.ts (factory wiring)
-> createKeywordDetectorHook(config)
-> detectKeywordsWithType(text, agent, model, disabledKeywords)
-> Set-based filter at the source-of-truth detector
Adds 8 regression tests covering per-keyword disable, multi-keyword
disable, partial disable (one keyword off, another still firing),
ultrawork toast suppression, undefined config, and empty array.
Top-level `import { Database } from 'bun:sqlite'` caused Node/Electron's
ESM loader to reject the plugin at module-graph resolution time because
the `bun:` protocol is not in Node's allowed scheme list. This prevented
the OpenCode desktop app from loading the plugin at all.
Fix:
- Remove top-level static import of `bun:sqlite`
- Use a lazy importer (`_bunSqliteImporter`) that calls
`import('bun:sqlite').catch(() => null)` at runtime
- If the import returns null (non-Bun environment), log a warning and
return early — no DB override attempted, plugin loads normally
- Expose `__setBunSqliteImporterForTesting` / `__resetBunSqliteImporterForTesting`
test seams to verify the Node/Electron fallback path
Closes#3795
Previously each transform hook was awaited sequentially without per-hook
error handling. If contextInjectorMessagesTransform or thinkingBlockValidator
threw, toolPairValidator was silently skipped, leaving orphaned tool_use
blocks in the post-compaction API payload and producing
"messages.N: tool_use ids were found without tool_result blocks immediately
after" 400s from Anthropic.
Wraps each hook in runHookSafely so an upstream throw is logged but the
chain continues. Adds regression tests covering the isolation contract and
the consecutive-assistants compaction tail case (ses_22bd806).
The sort shim from the previous commit enforces canonical core ordering at runtime, so ZWSP prefixes are no longer needed. Removing them eliminates the Bun.stringWidth vs terminal-width drift that broke the TUI status bar (#3259).
Drop AGENT_LIST_SORT_PREFIXES and getAgentRuntimeName from agent-display-names; switch all call sites to getAgentDisplayName. getAgentListDisplayName stays as a thin alias for external importers.
Keep stripInvisibleAgentCharacters and the ZWSP regex paths so legacy session state and configs from v3.14.0-v3.16.0 still resolve.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>