Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"version": "0.4.2",
"version": "0.4.3",
"category": "productivity",
"tags": ["notifications", "terminal", "warp"]
},
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Run PowerShell tests
shell: powershell
run: .\tests\test-hooks.ps1
- name: Verify ConPTY notification delivery
run: python tests/notification_conpty_test.py -v
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ plugins/orchestration/hooks/hooks.json
plugins/orchestration/scripts/ Oz parent-message listener, drain, and lifecycle scripts for POSIX shells and Windows PowerShell
plugins/orchestration/skills/ Oz orchestration skills
tests/test-hooks.sh Shell tests
tests/notification_conpty_test.py Isolated Windows ConPTY delivery tests
```
## Plugins
- `warp`: `SessionStart`, `Stop`, `PermissionRequest`, `UserPromptSubmit`, `PostToolUse` notifications for Warp.
Expand Down Expand Up @@ -40,8 +41,20 @@ The POSIX notifier uses `/dev/tty` when a controlling terminal is available.
Codex 0.155.0 and later detach hooks from that terminal, so the notifier falls
back to an ancestor's terminal device. Linux uses `/proc` and `readlink`; macOS
uses `ps`. Discovery is bounded, only terminal devices are used, and failure
produces a diagnostic without failing the hook. Notification payloads and the
Windows transport are unchanged.
produces a diagnostic without failing the hook. Notification payloads are unchanged.
On Windows, the notifier locates a live native Codex ancestor and checks whether
it shares that process's console. Detached hooks temporarily attach to the Codex
console while preserving their captured standard streams, then restore their
previous attachment when another live owner remains. Notifications never go to
hook stdout; an unavailable console produces a payload-free stderr diagnostic.
The Windows test suite checks actual OSC delivery on an isolated ConPTY, including
detached `cmd /C` launches, concurrent hooks, Unicode, and captured control output:
```sh
python tests/notification_conpty_test.py -v
```
Set `WARP_CONPTY_DLL` to Warp's bundled `conpty.dll` to test its duplex transport
instead of Windows' system ConPTY. Keep the DLL's companion assets adjacent and
available on `PATH`.
## One-off notifier testing in an Oz Docker task
Building the Codex CLI sidecar does not include this checkout. The CLI image
contains Codex itself; Warp normally installs this plugin at runtime.
Expand All @@ -60,7 +73,7 @@ This modifies only that container's installed copy. It does not publish the
plugin or make newly created tasks use local changes.
## Versioning
`plugins/warp/scripts/on-session-start.sh` emits `PLUGIN_VERSION`.
Current plugin version: `0.4.2`.
Current plugin version: `0.4.3`.
Keep it in sync with Warp's Codex plugin manager minimum version.
## Skills

Expand Down
2 changes: 1 addition & 1 deletion plugins/warp/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "warp",
"description": "Warp terminal integration for Codex - native notifications, and more to come",
"version": "0.4.2",
"version": "0.4.3",
"author": {
"name": "Warp",
"url": "https://warp.dev"
Expand Down
90 changes: 6 additions & 84 deletions plugins/warp/scripts/common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -122,86 +122,7 @@ function Initialize-WarpConsoleWriter {
return
}

Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

public static class WarpConsoleWriter
{
private const uint GenericWrite = 0x40000000;
private const uint FileShareRead = 0x00000001;
private const uint FileShareWrite = 0x00000002;
private const uint OpenExisting = 3;

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern IntPtr CreateFileW(
string fileName,
uint desiredAccess,
uint shareMode,
IntPtr securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
IntPtr templateFile);

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WriteConsoleW(
IntPtr consoleOutput,
string buffer,
uint charsToWrite,
out uint charsWritten,
IntPtr reserved);

[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);

public static bool Write(string message)
{
IntPtr handle = CreateFileW(
"CONOUT$",
GenericWrite,
FileShareRead | FileShareWrite,
IntPtr.Zero,
OpenExisting,
0,
IntPtr.Zero);

if (handle == new IntPtr(-1))
{
return false;
}

try
{
int offset = 0;
while (offset < message.Length)
{
int chunkLength = Math.Min(16384, message.Length - offset);
if (offset + chunkLength < message.Length
&& char.IsHighSurrogate(message[offset + chunkLength - 1]))
{
chunkLength--;
}

string chunk = message.Substring(offset, chunkLength);
uint written;
if (!WriteConsoleW(handle, chunk, (uint)chunk.Length, out written, IntPtr.Zero)
|| written == 0)
{
return false;
}

offset += (int)written;
}

return true;
}
finally
{
CloseHandle(handle);
}
}
}
"@
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.

}

function Send-WarpNotification {
Expand All @@ -216,10 +137,11 @@ function Send-WarpNotification {

try {
Initialize-WarpConsoleWriter
[void][WarpConsoleWriter]::Write($message)
if (-not [WarpConsoleWriter]::Write($message)) {
[Console]::Error.WriteLine("[codex-warp] notification delivery failed: $([WarpConsoleWriter]::LastError)")
}
} catch {
# Hook stdout is reserved for Codex hook control JSON. If there is no
# attached console device, drop the notification rather than emitting
# OSC text to captured stdout and making the hook fail.
# Hook stdout is reserved for Codex hook control JSON, not notification bytes.
[Console]::Error.WriteLine("[codex-warp] notification delivery failed: console interop unavailable")
}
}
Loading
Loading