code-yeongyu / code-yeongyu/lazycodex
Windows hook health: OMO SessionStart exceeds 5s and doctor misses incompatible hook output
- Dominant language
- TypeScript
- Stars
- 3.5k
- Forks
- 216
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
On Windows 11 with Codex CLI 0.144.3 and the OMO 4.16.0 marketplace plugin, a normal CLI session showed two unattributed `SessionStart` failures with `hook timed out after 5s`, followed by repeated `UserPromptSubmit` and `Stop` invalid-JSON failures.
The investigation separated two causes:
1. OMO owns a confirmed `SessionStart` timeout risk: three Windows handlers still have a 5-second limit. Isolated probes showed telemetry at 9,128 ms and auto-update at 6,077 ms even with their optional work disabled. The historical CLI output did not identify which handlers produced its two timeout notices.
2. A co-installed global hook is a confirmed incompatible JSON emitter and is sufficient to reproduce the same Codex classification; current OMO handlers passed the isolated output checks. The original CLI notices were not toggled end to end, so their exact emitter remains unverified. LazyCodex can make this actionable by having `doctor` identify incompatible co-installed hooks and report their source path, elapsed time, and sanitized output shape.
## Environment
- OS: Windows 11 Pro 10.0.26200, AMD64
- Windows PowerShell: 5.1.26100.8655
- Node.js: 22.15.0
- Codex CLI: 0.144.3, installed through global npm
- Installed OMO marketplace plugin: 4.16.0
- Latest LazyCodex source checked: `3d7416bff3e6c80ebf5542b4dd12f5c76298d46d` (OMO 4.17.1)
- Latest upstream Codex source checked: `2e156cbe31212ccfa9173cb2679c1a201a940b18`
- Relevant configuration: OMO enabled through the `sisyphuslabs` marketplace; global `UserPromptSubmit` and `Stop` hook groups also enabled
## Repository Decision
- Target repository: `code-yeongyu/lazycodex`
- Why this belongs here: the confirmed timeout is defined by OMO hook manifests and persists in current LazyCodex 4.17.1 source. The second actionable gap is LazyCodex hook-health diagnosis: the generic CLI error does not identify which co-installed handler emitted incompatible JSON.
- LazyCodex evidence:
- Telemetry uses a [5-second SessionStart timeout](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/hooks/session-start-recording-session-telemetry.json#L3-L12).
- Auto-update uses a [5-second SessionStart timeout](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/hooks/session-start-checking-auto-update.json#L3-L15).
- CodeGraph uses a [5-second SessionStart timeout](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/hooks/session-start-checking-codegraph-bootstrap.json#L3-L12).
- Windows launches each through [`node-dispatch.ps1`](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/components/bootstrap/scripts/node-dispatch.ps1#L1-L75), adding a PowerShell process and Node-resolution work before the target handler starts.
- Upstream Codex evidence:
- Codex reports the handler's configured limit from [`command_runner.rs`](https://github.com/openai/codex/blob/2e156cbe31212ccfa9173cb2679c1a201a940b18/codex-rs/hooks/src/engine/command_runner.rs#L101-L135).
- `UserPromptSubmit` and `Stop` outputs use strict schemas with unknown fields denied in [`schema.rs`](https://github.com/openai/codex/blob/2e156cbe31212ccfa9173cb2679c1a201a940b18/codex-rs/hooks/src/schema.rs#L421-L459).
- Empty stdout is accepted as a no-op, while JSON-looking incompatible output is reported as invalid in [`user_prompt_submit.rs`](https://github.com/openai/codex/blob/2e156cbe31212ccfa9173cb2679c1a201a940b18/codex-rs/hooks/src/events/user_prompt_submit.rs#L133-L215) and [`stop.rs`](https://github.com/openai/codex/blob/2e156cbe31212ccfa9173cb2679c1a201a940b18/codex-rs/hooks/src/events/stop.rs#L202-L301).
## Reproduction
1. On Windows, enable OMO 4.16.0 in Codex and start a normal CLI session. In the reported session, Codex displayed two unattributed `SessionStart hook (failed): hook timed out after 5s` notices.
2. Reproduce the OMO timeout risk independently with this sanitized probe. Replace `` with the installed plugin directory:
```powershell
$PluginRoot = ''
$Dispatch = Join-Path $PluginRoot 'components\bootstrap\scripts\node-dispatch.ps1'
$env:CODEX_HOME = Join-Path $env:USERPROFILE '.codex'
$env:PLUGIN_ROOT = $PluginRoot
$env:PLUGIN_DATA = Join-Path $env:CODEX_HOME 'plugins\data\sisyphuslabs\omo'
$env:OMO_CODEX_DISABLE_POSTHOG = '1'
$env:LAZYCODEX_AUTO_UPDATE_DISABLED = '1'
$env:CODEX_CODEGRAPH_ENABLED = '0'
$Payload = '{"session_id":"probe-session","transcript_path":null,"cwd":"C:\\temp","hook_event_name":"SessionStart","model":"gpt-5.5","permission_mode":"never","source":"startup"}'
$Targets = @(
@{ Name = 'telemetry'; File = 'components\telemetry\dist\cli.js' },
@{ Name = 'auto-update'; File = 'scripts\auto-update.mjs' },
@{ Name = 'codegraph'; File = 'components\codegraph\dist\cli.js' }
)
foreach ($Target in $Targets) {
1..5 | ForEach-Object {
$Elapsed = Measure-Command {
$Payload | powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Dispatch (Join-Path $PluginRoot $Target.File) hook session-start
}
[pscustomobject]@{ Handler = $Target.Name; Run = $_; ElapsedMs = [math]::Round($Elapsed.TotalMilliseconds) }
}
}
```
These controls are present in current source: [telemetry opt-out](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/components/telemetry/README.md#L51-L60), [auto-update disable](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/scripts/auto-update.mjs#L75-L84), and the [Codex CodeGraph disable case](https://github.com/code-yeongyu/lazycodex/blob/3d7416bff3e6c80ebf5542b4dd12f5c76298d46d/plugins/omo/components/codegraph/test/hook.test.ts#L189-L208). Elapsed time includes the nested `powershell.exe` launcher because that is the real `commandWindows` path Codex executes.
3. Treat the first invocation in a fresh parent shell as the cold sample and immediate repeats as warm samples. The timeout risk is reproduced when any handler takes more than its 5,000 ms manifest limit.
4. Reproduce the separate attribution gap in a disposable project. This creates a hook that drains stdin, exits 0, and writes only incompatible application status JSON:
```powershell
$ProbeRoot = Join-Path $env:TEMP 'codex-invalid-hook-output-probe'
$DotCodex = Join-Path $ProbeRoot '.codex'
$HookScript = Join-Path $ProbeRoot 'invalid-hook.mjs'
New-Item -ItemType Directory -Force -Path $DotCodex | Out-Null
@'
process.stdin.resume();
process.stdin.on("end", () => {
process.stdout.write(`${JSON.stringify({ ok: true, skipped: "clean_event" })}\n`);
});
'@ | Set-Content -Encoding ascii -Path $HookScript
$Command = 'node "' + $HookScript + '"'
$Handler = @{ type = 'command'; command = $Command; commandWindows = $Command; timeout = 5 }
$Hooks = @{
hooks = @{
UserPromptSubmit = @(@{ hooks = @($Handler) })
Stop = @(@{ hooks = @($Handler) })
}
}
$Hooks | ConvertTo-Json -Depth 8 | Set-Content -Encoding ascii -Path (Join-Path $DotCodex 'hooks.json')
```
5. Run `codex -C $ProbeRoot`, approve the two disposable project hooks when prompted, submit `hi`, and let the turn finish. Codex reports `hook returned invalid user prompt submit JSON output` and `hook returned invalid stop hook JSON output`, but the visible failures do not identify the emitting file.
6. Remove the disposable reproduction after the check:
```powershell
Remove-Item -LiteralPath $ProbeRoot -Recurse -Force
```
## Expected Behavior
- OMO `SessionStart` handlers should reliably finish within their configured timeout on a cold Windows start, including the PowerShell launcher overhead.
- LazyCodex diagnostics should identify the exact active hook source that exceeds its timeout or emits incompatible JSON, without exposing the prompt or raw hook payload.
- A no-op capture/telemetry hook should either emit no stdout or emit a valid event-specific Codex hook output shape.
## Actual Behavior
- The reported CLI session showed two unattributed `SessionStart` timeouts. Independent OMO probes proved that the telemetry and auto-update handlers can each exceed their configured 5-second limit; they do not prove which handlers emitted the historical notices.
- A separate global hook was confirmed to emit incompatible status JSON for both events and reproduces the same generic invalid-JSON classification. End-to-end attribution of the original notices remains unverified.
- The visible error did not name the handler, so OMO and non-OMO hooks initially appeared to be one failure.
## Evidence
Safe synthetic Windows probes used the installed OMO 4.16.0 `commandWindows` launcher while disabling optional external work:
| Handler | Cold elapsed | Configured timeout | Result |
|---|---:|---:|---|
| Telemetry | 9,128 ms | 5,000 ms | Would time out |
| Auto-update | 6,077 ms | 5,000 ms | Would time out |
| CodeGraph | 4,454 ms | 5,000 ms | Passed with little headroom |
A warm second pass through the same launcher completed in 2,651-2,735 ms, while the first direct telemetry process in a separate pass still took 5,827 ms. The failure is therefore cold-start sensitive.
Synthetic output checks found:
- OMO rules, ultrawork, ulw-loop, and start-work-continuation no-op paths: exit 0 with empty stdout, accepted by current Codex.
- OMO triggered output builders: valid event-specific JSON shapes in source and tests.
- Co-installed global capture hook: exit 0 with `ok,event,skipped` status JSON for both `UserPromptSubmit` and `Stop`; rejected because those fields are outside the Codex hook output schemas.
- Co-installed final-handoff hook: valid `decision,reason` output; not the invalid `Stop` emitter.
No exact open LazyCodex duplicate was found. Closed [#57](https://github.com/code-yeongyu/lazycodex/issues/57) is the closest JSON-output precedent but addressed a prior CodeGraph `SessionStart` output defect. Closed #45, #80, and #92 cover missing hook payloads or earlier doctor/layout defects rather than this timeout headroom and handler-attribution case.
## Root Cause
Insufficient OMO timeout headroom on Windows is confirmed independently of the historical handler attribution. The manifests allow 5 seconds, but cold PowerShell + Node startup and handler initialization can exceed 5 seconds before optional work matters. Current LazyCodex 4.17.1 retains the same limits. The exact two handlers behind the original CLI notices remain unidentified because the visible output did not include handler names.
An incompatible JSON emitter is also confirmed, but isolated current OMO handlers did not emit it. A co-installed global hook prints internal status JSON to stdout, and that output is sufficient to reproduce Codex's invalid-output classification because its top-level fields are outside the hook schema. The original notices were not verified with a disable/re-enable toggle. The LazyCodex-owned gap is diagnosis and attribution, not the external hook's output implementation.
## Proposed Fix
1. As the minimal immediate fix, raise the telemetry, auto-update, and CodeGraph `SessionStart` manifest limits from 5 to 15 seconds in their hook fragments, then add deterministic tests that assert the generated marketplace manifests retain those values.
2. Add an opt-in Windows smoke check that runs each real `commandWindows` path from a fresh shell and records cold and warm elapsed times. Keep wall-clock thresholds out of the deterministic unit suite.
3. Extend `lazycodex-ai doctor --platform=codex` to inventory global plus plugin hooks and run redacted synthetic payload probes. Report handler source, exit code, elapsed time versus configured timeout, stdout byte count, and parsed top-level keys only.
4. Validate synthetic stdout against the event-specific Codex hook schema. Recommend empty stdout for no-op handlers and explain incompatible keys without printing raw payloads.
5. Add a regression fixture where a co-installed hook emits `{"ok":true}` so doctor attributes the invalid output to that file instead of implying OMO emitted it. Moving nonessential startup work behind a detached path can be evaluated separately from the minimal timeout correction.
## Verification Plan
- On a cold Windows test host, run all OMO `SessionStart` `commandWindows` handlers repeatedly and confirm no run reaches its configured timeout.
- Run `doctor` with a synthetic incompatible global hook and confirm it names that hook, reports only sanitized output metadata, and returns a clear remediation.
- Confirm OMO no-op and triggered outputs pass current Codex `UserPromptSubmit`, `Stop`, and `SessionStart` schemas.
- Start Codex with OMO plus the fixed global hook and confirm startup, prompt submission, and stop complete without hook failure notices.
---
This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the three OMO SessionStart hook manifests and plugins/omo/components/bootstrap/scripts/node-dispatch.ps1, then review plugins/omo/components/codegraph/test/hook.test.ts and the linked Codex hook schema and runner. Check how the lazycodex-ai doctor --platform=codex entry point discovers hooks and runs probes. Done means manifest limits and deterministic assertions are updated, Windows behavior is covered separately, and doctor reports redacted source, timing, exit status, stdout size, and top-level keys.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, powershell, typescript
- Domain
- cli, testing, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 42/100