From b5b09dd2f464e92996000888b7a1587efab83544 Mon Sep 17 00:00:00 2001 From: obviousbread Date: Fri, 14 Aug 2026 12:06:04 +0300 Subject: [PATCH 1/2] Add T3 thread titles to Codex notifications --- README.md | 1 + lib/code-notify/core/notifier.sh | 56 ++++++++++++++++++++++++++- tests/test-codex-notify.sh | 65 ++++++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9228790..0d3d232 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ Code-Notify uses the hook systems built into AI coding tools: For Codex, Code-Notify configures `notify = ["/absolute/path/to/notifier.sh", "codex"]` and reads the JSON payload Codex appends on completion. Codex currently exposes completion events through `notify`; approval and `request_permissions` prompts do not currently arrive through this hook. +When a Codex session runs through T3 Code, Code-Notify uses the payload's `thread-id` to resolve the matching title from T3 Code's local state. If T3 Code is unavailable or no matching thread exists, it falls back to the workspace name. For omp, Code-Notify writes a small managed extension module to `~/.omp/agent/extensions/code-notify.js`, because omp loads extension modules instead of reading hook commands from a config file. The extension forwards omp's `agent_end` event to the same `notifier.sh`, so sound, voice, Slack/Discord channels, click-through, the global mute switch, and rate limiting all work unchanged. Like Codex, omp currently exposes completion events; approval and idle prompts are not yet wired. diff --git a/lib/code-notify/core/notifier.sh b/lib/code-notify/core/notifier.sh index 7eae839..80b6d39 100755 --- a/lib/code-notify/core/notifier.sh +++ b/lib/code-notify/core/notifier.sh @@ -94,7 +94,17 @@ get_codex_hook_type() { } get_codex_project_name() { - local payload_cwd + local payload_cwd thread_id t3_thread_title + thread_id=$(json_extract_string "$HOOK_DATA" "thread-id") + + if [[ -n "$thread_id" ]]; then + t3_thread_title=$(get_t3_thread_title "$thread_id") + if [[ -n "$t3_thread_title" ]]; then + printf '%s\n' "$t3_thread_title" + return 0 + fi + fi + payload_cwd=$(json_extract_string "$HOOK_DATA" "cwd") if [[ -n "$payload_cwd" ]]; then @@ -104,6 +114,50 @@ get_codex_project_name() { fi } +# Resolve a Codex thread to its T3 Code title when T3 local state is available. +get_t3_thread_title() { + local thread_id="$1" + local state_db="$HOME/.t3/userdata/state.sqlite" + + [[ -n "$thread_id" ]] || return 1 + [[ -f "$state_db" ]] || return 1 + has_python3 || return 1 + + python3 - "$state_db" "$thread_id" <<'PY' 2>/dev/null +import json +import pathlib +import sqlite3 +import sys + +db_path = pathlib.Path(sys.argv[1]) +codex_thread_id = sys.argv[2] + +try: + with sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1) as conn: + rows = conn.execute( + """ + select t.title, r.resume_cursor_json + from provider_session_runtime r + join projection_threads t using (thread_id) + where r.provider_name = 'codex' + """ + ) + for title, resume_cursor_json in rows: + try: + resume_cursor = json.loads(resume_cursor_json or "{}") + except (TypeError, json.JSONDecodeError): + continue + + if resume_cursor.get("threadId") == codex_thread_id and isinstance(title, str): + title = " ".join(title.split()) + if title: + print(title, end="") + break +except (OSError, sqlite3.Error): + pass +PY +} + HOOK_DATA="" if [[ "$RAW_ARG1" == "codex" ]]; then TOOL_NAME="codex" diff --git a/tests/test-codex-notify.sh b/tests/test-codex-notify.sh index 9919911..1b31243 100644 --- a/tests/test-codex-notify.sh +++ b/tests/test-codex-notify.sh @@ -85,6 +85,53 @@ with sqlite3.connect(db_path) as conn: PY } +write_t3_thread_metadata() { + local codex_thread_id="$1" + local title="$2" + + python3 - "$HOME/.t3/userdata/state.sqlite" "$codex_thread_id" "$title" <<'PY' +import json +import pathlib +import sqlite3 +import sys + +db_path = pathlib.Path(sys.argv[1]) +codex_thread_id = sys.argv[2] +title = sys.argv[3] +db_path.parent.mkdir(parents=True, exist_ok=True) + +with sqlite3.connect(db_path) as conn: + conn.execute( + "create table projection_threads (thread_id text primary key, title text not null)" + ) + conn.execute( + """ + create table provider_session_runtime ( + thread_id text primary key, + provider_name text not null, + resume_cursor_json text + ) + """ + ) + conn.execute( + "insert into projection_threads values (?, ?)", + ("malformed-runtime", "Wrong thread"), + ) + conn.execute( + "insert into provider_session_runtime values (?, ?, ?)", + ("malformed-runtime", "codex", "not-json"), + ) + conn.execute( + "insert into projection_threads values (?, ?)", + ("matching-runtime", title), + ) + conn.execute( + "insert into provider_session_runtime values (?, ?, ?)", + ("matching-runtime", "codex", json.dumps({"threadId": codex_thread_id})), + ) +PY +} + test_dir="$(mktemp -d)" trap 'rm -rf "$test_dir"' EXIT @@ -143,14 +190,18 @@ run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"desk write_codex_thread_metadata "cli-thread" "Codex CLI" "shell" run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"cli-thread","cwd":"/tmp/demo","client":"codex-exec","last-assistant-message":"CLI event still notifies"}' -wait_for_lines "$notification_log" 3 || fail "expected three Codex notification deliveries" -wait_for_lines "$sound_log" 3 || fail "expected three Codex sound playbacks" -wait_for_lines "$HOME/.claude/logs/notifications.log" 3 || fail "expected three Codex notification log entries" +write_t3_thread_metadata "t3-codex-thread" "Investigate missing desktop notifications" +run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"t3-codex-thread","cwd":"/tmp/demo","client":"codex-exec","last-assistant-message":"T3 event"}' + +wait_for_lines "$notification_log" 4 || fail "expected four Codex notification deliveries" +wait_for_lines "$sound_log" 4 || fail "expected four Codex sound playbacks" +wait_for_lines "$HOME/.claude/logs/notifications.log" 4 || fail "expected four Codex notification log entries" grep -q "Task Complete - demo" "$notification_log" || fail "Codex completion payload did not map to a stop notification" grep -q "Input Required - demo" "$notification_log" || fail "Codex permission-like payload did not map to an input-required notification" -[[ $(wc -l < "$notification_log") -eq 3 ]] || fail "desktop-origin Codex events were not suppressed correctly" -[[ $(wc -l < "$sound_log") -eq 3 ]] || fail "desktop-origin Codex sound playback was not suppressed correctly" -[[ $(wc -l < "$HOME/.claude/logs/notifications.log") -eq 3 ]] || fail "desktop-origin Codex log entries were not suppressed correctly" +grep -q "Task Complete - Investigate missing desktop notifications" "$notification_log" || fail "T3 thread title was not used as Codex notification context" +[[ $(wc -l < "$notification_log") -eq 4 ]] || fail "desktop-origin Codex events were not suppressed correctly" +[[ $(wc -l < "$sound_log") -eq 4 ]] || fail "desktop-origin Codex sound playback was not suppressed correctly" +[[ $(wc -l < "$HOME/.claude/logs/notifications.log") -eq 4 ]] || fail "desktop-origin Codex log entries were not suppressed correctly" -pass "Codex notifies for CLI sessions while suppressing desktop-origin duplicate events" +pass "Codex resolves T3 titles while preserving CLI fallback and desktop suppression" From e14ff1f28cbb79142c5a0b4c450763792bc21332 Mon Sep 17 00:00:00 2001 From: obviousbread Date: Fri, 21 Aug 2026 13:53:30 +0300 Subject: [PATCH 2/2] fix: address T3 notification review --- README.md | 2 +- lib/code-notify/core/notifier.sh | 9 ++++++++- tests/test-codex-notify.sh | 27 +++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0d3d232..02ddecb 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ Code-Notify uses the hook systems built into AI coding tools: For Codex, Code-Notify configures `notify = ["/absolute/path/to/notifier.sh", "codex"]` and reads the JSON payload Codex appends on completion. Codex currently exposes completion events through `notify`; approval and `request_permissions` prompts do not currently arrive through this hook. -When a Codex session runs through T3 Code, Code-Notify uses the payload's `thread-id` to resolve the matching title from T3 Code's local state. If T3 Code is unavailable or no matching thread exists, it falls back to the workspace name. +On macOS, when a Codex session runs through T3 Code, Code-Notify uses the payload's `thread-id` to resolve the matching title from T3 Code's local state. If T3 Code is unavailable or no matching thread exists, it falls back to the workspace name. For omp, Code-Notify writes a small managed extension module to `~/.omp/agent/extensions/code-notify.js`, because omp loads extension modules instead of reading hook commands from a config file. The extension forwards omp's `agent_end` event to the same `notifier.sh`, so sound, voice, Slack/Discord channels, click-through, the global mute switch, and rate limiting all work unchanged. Like Codex, omp currently exposes completion events; approval and idle prompts are not yet wired. diff --git a/lib/code-notify/core/notifier.sh b/lib/code-notify/core/notifier.sh index 80b6d39..71a2990 100755 --- a/lib/code-notify/core/notifier.sh +++ b/lib/code-notify/core/notifier.sh @@ -148,6 +148,9 @@ try: except (TypeError, json.JSONDecodeError): continue + if not isinstance(resume_cursor, dict): + continue + if resume_cursor.get("threadId") == codex_thread_id and isinstance(title, str): title = " ".join(title.split()) if title: @@ -614,7 +617,11 @@ send_macos_notification() { else # osascript doesn't support click-to-activate, but we can use a workaround. # Keep this silent too so custom/default sound playback stays single-sourced. - osascript -e "display notification \"$MESSAGE\" with title \"$TITLE\" subtitle \"$SUBTITLE\"" 2>/dev/null + osascript - "$MESSAGE" "$TITLE" "$SUBTITLE" <<'APPLESCRIPT' 2>/dev/null +on run argv + display notification (item 1 of argv) with title (item 2 of argv) subtitle (item 3 of argv) +end run +APPLESCRIPT fi } diff --git a/tests/test-codex-notify.sh b/tests/test-codex-notify.sh index 1b31243..b2e5eb6 100644 --- a/tests/test-codex-notify.sh +++ b/tests/test-codex-notify.sh @@ -121,6 +121,14 @@ with sqlite3.connect(db_path) as conn: "insert into provider_session_runtime values (?, ?, ?)", ("malformed-runtime", "codex", "not-json"), ) + conn.execute( + "insert into projection_threads values (?, ?)", + ("non-object-runtime", "Wrong thread"), + ) + conn.execute( + "insert into provider_session_runtime values (?, ?, ?)", + ("non-object-runtime", "codex", json.dumps([])), + ) conn.execute( "insert into projection_threads values (?, ?)", ("matching-runtime", title), @@ -190,7 +198,7 @@ run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"desk write_codex_thread_metadata "cli-thread" "Codex CLI" "shell" run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"cli-thread","cwd":"/tmp/demo","client":"codex-exec","last-assistant-message":"CLI event still notifies"}' -write_t3_thread_metadata "t3-codex-thread" "Investigate missing desktop notifications" +write_t3_thread_metadata "t3-codex-thread" 'Investigate "missing" desktop notifications' run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"t3-codex-thread","cwd":"/tmp/demo","client":"codex-exec","last-assistant-message":"T3 event"}' wait_for_lines "$notification_log" 4 || fail "expected four Codex notification deliveries" @@ -199,9 +207,24 @@ wait_for_lines "$HOME/.claude/logs/notifications.log" 4 || fail "expected four C grep -q "Task Complete - demo" "$notification_log" || fail "Codex completion payload did not map to a stop notification" grep -q "Input Required - demo" "$notification_log" || fail "Codex permission-like payload did not map to an input-required notification" -grep -q "Task Complete - Investigate missing desktop notifications" "$notification_log" || fail "T3 thread title was not used as Codex notification context" +grep -Fq 'Task Complete - Investigate "missing" desktop notifications' "$notification_log" || fail "T3 thread title was not used as Codex notification context" [[ $(wc -l < "$notification_log") -eq 4 ]] || fail "desktop-origin Codex events were not suppressed correctly" [[ $(wc -l < "$sound_log") -eq 4 ]] || fail "desktop-origin Codex sound playback was not suppressed correctly" [[ $(wc -l < "$HOME/.claude/logs/notifications.log") -eq 4 ]] || fail "desktop-origin Codex log entries were not suppressed correctly" +if [[ "$(uname -s)" == "Darwin" ]]; then + osascript_log="$log_dir/osascript.log" + rm "$fake_bin/terminal-notifier" + cat > "$fake_bin/osascript" < "$osascript_log" +EOF + chmod +x "$fake_bin/osascript" + + run_codex_notifier "$fake_path" '{"type":"agent-turn-complete","thread-id":"t3-codex-thread","cwd":"/tmp/demo","client":"codex-exec","last-assistant-message":"T3 fallback event"}' + + wait_for_lines "$osascript_log" 4 || fail "osascript fallback did not receive notification arguments" + grep -Fqx 'Task Complete - Investigate "missing" desktop notifications' "$osascript_log" || fail "osascript fallback did not preserve quotes in the T3 thread title" +fi + pass "Codex resolves T3 titles while preserving CLI fallback and desktop suppression"