Skip to content

Deliver Codex lifecycle notifications from detached Windows hooks - #7

Merged
joeywangzr merged 1 commit into
mainfrom
jopee/windows-codex-hook-console
Oct 14, 2026
Merged

joeywangzr merged 1 commit into
mainfrom
jopee/windows-codex-hook-console

Conversation

@joeywangzr

@joeywangzr joeywangzr commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Codex launches Windows hooks without the original console. The notifier can exit successfully without delivering SessionStart or Stop, leaving Warp without a native session ID or completion signal. This change reconnects the hook to the closest live Codex ancestor's console while keeping hook control streams captured.

Review guide

Start with plugins/warp/scripts/console-writer.cs. Focus on ancestor identity/liveness checks, handle lifetime, and standard-stream restoration. The delivery fixture checks received OSC payloads, not just successful Win32 API returns.

What changed

  • Add a bounded console reattachment path. It rejects stale or unrelated ancestors and never emits notification bytes on hook stdout. Delivery failures produce a payload-free stderr diagnostic without failing the hook.
  • Add isolated Windows ConPTY integration coverage and wire it into CI.
  • Synchronize the notification plugin manifests and version announcements to 0.4.3. The orchestration plugin remains 0.4.1.

This avoids a new IPC endpoint or an upstream Codex binary fork. Publish this plugin before shipping the companion Warp minimum-version update: warpdotdev/warp#16368.

Validation

A Windows validation agent ran all six delivery tests on both system ConPTY and Warp's bundled ConPTY from v0.2026.10.08.08.30.dev_00; both suites passed. Coverage includes detached/direct/cmd launches, spaces, Unicode, concurrent notifications, captured control streams, unrelated ancestors, and an exited closest Codex ancestor with an older live coordinator. The production writer/common files were checksum-matched to the local candidate, and the fixture's executable AST matched.

Locally, bash tests/test-hooks.sh passed 65 checks. PowerShell parsing, C# compilation, and whitespace checks passed. The existing POSIX notification suite timed out on this Mac, including on the unmodified baseline; it is not claimed passing.

CI passed both plugin test jobs. Windows CI executed the final PowerShell helper and all six delivery tests against this committed 0.4.3 candidate; it reported "PowerShell hook tests passed" and six passing tests. Linux CI also passed. Real staging E2E follow-ups remain pending after publication.

CHANGELOG-BUG-FIX: Fixed delivery of Codex lifecycle notifications from detached Windows hooks.

Conversation: https://staging.warp.dev/conversation/f70d81ec-85a4-4322-abaf-a1fb9713ce7e
Plan: https://staging.warp.dev/drive/notebook/sjtUyYQASbuCQEbfYS6AFA

Co-Authored-By: Warp agent@warp.dev

Recover the live Codex ancestor console while keeping hook control streams captured. Add isolated ConPTY delivery coverage and publish the candidate as plugin 0.4.3.

Co-Authored-By: Warp <agent@warp.dev>
@joeywangzr
joeywangzr marked this pull request as ready for review October 8, 2026 21:08
ulong childCreated;
if (!CreationTime(GetCurrentProcess(), out childCreated))
return IntPtr.Zero;
IntPtr snapshot = CreateToolhelp32Snapshot(SnapshotProcesses, 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How expensive is taking a snapshot? Seems potentially heavyweight for finding the codex process

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting up a benchmark to test this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agent output:

I benchmarked the exact implementation on Windows Server 2022 with 8 logical CPUs, 16 GiB RAM, and Windows PowerShell 5.1.

Operation Samples Median p95
Snapshot creation and close 200 0.43 ms 0.54 ms
Full successful ancestor lookup 200 0.86 ms 0.94 ms
Full lookup with 64 additional processes 200 1.23 ms 1.42 ms
Fresh PowerShell notification hook 25 243 ms 255 ms

The snapshot only captures process metadata, not process memory, heaps, or modules, but it does cover all processes. The full lookup measurements include enumeration, dictionary construction, ancestor validation, reflection overhead, and handle cleanup.

All 25 fresh hooks successfully delivered notifications through ConPTY and preserved the stdout/stderr control streams.

Given these results, I’d keep the supported snapshot API rather than introduce an internal API dependency. Discovery accounts for roughly 0.4–0.5% of fresh-hook runtime on this machine. These are single-VM measurements, though, not a guarantee for every customer machine.

if (process == IntPtr.Zero)
break;
ulong parentCreated;
if (WaitForSingleObject(process, 0) != WaitTimeout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this simply to check that the parent is alive (given that we wait with a 0ms timeout)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. WaitForSingleObject(process, 0) == WAIT_TIMEOUT means it hasn’t exited at that instant, since a process handle becomes signaled when the process exits.
At creation-time there is also a check which rejects a reused parent PID whose process started after the child.

uint[] original = ConsoleProcesses();
if (Array.IndexOf(original, targetId) < 0)
{
foreach (uint id in original)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks a lot like FindCodex - why do we need to loop again?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FindCodex walks process ancestry to locate the notification destination
This only checks the processes attached to the hook’s original console and retain a live peer so we can reattach to that console afterward

}
}

IntPtr console = CreateFileW("CONOUT$", 0x40000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're writing to this console object, do we need to modify stdin/out/err too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for the write. WriteConsoleW uses the CONOUT$ handle which is independent from stdin/stdout/stderr. The SetStdHandle calls will restore the original handles because AttachConsole can replace them with console handles. This will keep hook-control JSON on stdout and diagnostics on stderr instead of the codex console.

@joeywangzr
joeywangzr requested a review from bnavetta October 14, 2026 17:57

@captainsafia captainsafia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one comment about whether we might benefit from pre-compiling the script, especially if we are invoking it frequently whenever hooks are executed. We weren't doing this before so I'm leaving it as a non-blocking comment.


private static bool IsCodex(string executable)
{
return String.Equals(executable, "codex.exe", StringComparison.OrdinalIgnoreCase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC, do we expect the codex-*.exe search to grab versioning codex installs?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for the official versioned installs (they use codex.exe).
The codex-*.exe match is for target-suffixed release binaries, such as codex-x86_64-pc-windows-msvc.exe.

}
}
"@
Add-Type -Path (Join-Path $PSScriptRoot "console-writer.cs")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC, how often do we end up calling into this? Main reason is that this will re-compile this CSharp file everytime this hook is invoked so we'll end up recompiling the C# file to a DLL every time. The C# compiler is efficient enough that this might not be a problem but curious how often this hook is invoked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called from SessionStart, UserPromptSubmit, PostToolUse, PermissionRequest, and Stop.

  • PostToolUse is the frequent one, since it fires after each tool execution.

Full hook timings

Windows Server 2022, 8 vCPUs, 16 GB RAM. Each scenario used 30 fresh PowerShell processes after warmups, with randomized ordering.

Measurement PowerShell 5.1 median / p95 PowerShell 7.4 median / p95
Startup-only baseline 152 / 366 ms 216 / 362 ms
Enabled PostToolUse hook 370 / 591 ms 853 / 1,174 ms

The enabled hook timings include startup, compilation, and successful notification delivery. Across the delivery suite, all 340 enabled notifications were decoded from ConPTY and verified against their expected session IDs and events.

Base versus this PR

For a comparable successful-delivery path with an inherited console:

Revision PowerShell 5.1 median PowerShell 7.4 median
Base 325 ms 749 ms
This PR 345 ms 764 ms
Difference +20 ms +15 ms

A separate, interleaved compilation-only comparison measured:

Add-Type duration PowerShell 5.1 median PowerShell 7.4 median
Base, inline C# 87 ms 259 ms
This PR, C# file 95 ms 287 ms

These are runner-specific measurements with substantial variation, so the small differences shouldn't be treated as precise latency guarantees. But the recurring compilation cost isn't negligible, particularly for PostToolUse. A precompiled helper could avoid that cost; this PR doesn't introduce most of it.

@joeywangzr
joeywangzr merged commit 1da907c into main Oct 14, 2026
5 checks passed
joeywangzr added a commit to warpdotdev/warp that referenced this pull request Oct 15, 2026
## Description
Plugin 0.4.3 is available from the canonical Git marketplace.
warpdotdev/codex-warp#7 merged on October 14;
both the marketplace entry and plugin manifest on `main` advertise
0.4.3.

Raise the Codex notification-plugin floor to 0.4.3 so cached 0.4.2
installs get upgraded. The companion plugin restores lifecycle delivery
from detached Windows hooks; it prevents missing session IDs and
completion signals. The orchestration-plugin minimum stays at 0.4.1.

This PR contains only the client version requirement and test changes.
Update-behavior coverage remains; there is no constant-only getter test.
The transport implementation and Windows integration coverage live in
the companion PR.

## Linked Issue
- [ ] The linked issue is labeled `ready-to-spec` or
`ready-to-implement`. No dedicated issue is linked; the companion PR
carries the fix.
- [ ] Where appropriate, screenshots or a short video of the
implementation are included below. No GUI change; validation uses
isolated console fixtures.

## Testing
- [ ] I have manually tested my changes locally with `./script/run`. Not
run; this change only updates the plugin requirement.

Local validation on October 14:
- `cargo nextest run -p warp --lib --locked -E
'test(plugin_manager::codex::tests)'`: 38 passed. Nextest reported one
LEAK warning for `does_not_support_update_without_codex_plugin`; no
assertion failed.
- `cargo check -p warp --lib --locked`: passed.
- `cargo clippy -p warp --lib --tests --locked -- -D warnings`: passed.
- `cargo fmt -p warp` and `git diff --check`: passed.

Existing outdated-cache coverage now uses 0.4.2. The companion's six
Windows delivery tests passed on both system ConPTY and Warp's bundled
DLL. Post-publication staging E2E validation was not run in this
session. Full Warp CI attempt 3 was requested:
https://github.com/warpdotdev/warp/actions/runs/37846977663.

## Agent Mode
- [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode

CHANGELOG-NONE

Conversation:
https://staging.warp.dev/conversation/f70d81ec-85a4-4322-abaf-a1fb9713ce7e
Review follow-up:
https://staging.warp.dev/conversation/743855e1-c346-4782-8655-d4e082820056
Plan: https://staging.warp.dev/drive/notebook/sjtUyYQASbuCQEbfYS6AFA

Co-Authored-By: Warp <agent@warp.dev>

Co-authored-by: Warp <agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants