Wake-guard convergence: per-injection idle rechecks + ack-after-success delivery (#28 + #29)#30
Conversation
…tings - codex-webhook-supervisor.sh (NEW in repo, was running untracked): keeps the @codexmb GroupMind webhook registered over a cloudflared quick tunnel. Hardened after the Jul 9-12 silent outage: force tunnel restart when the log shows sustained 'Retrying connection' flapping, and periodically re-register the URL (idempotent PUT) every REREGISTER_EVERY loops. - webhook-wake.mjs: working owner/mention filtering + nested-payload parsing + per-agent log path (the version that has been running in production). - codex_gui_nudge.sh: cliclick/Quartz nudge path for background wakes where launchd osascript lacks Accessibility (the version proven live Jul 13). - team-watchdog.mjs (NEW in repo): room-liveness watchdog, now with silent localWake support and @codexmb on the roster so a dead codex wake path is caught within STALE_MIN instead of days. No room spam for local agents. - remove stale screen-mirror launchd plist (daemon disabled Jun 25, keep off). No keys or secrets committed; all secrets stay in local files/env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… local wake First live firing (05:44) failed: default app name 'Codex' does not exist (codex runs inside ChatGPT.app) so AppleScript focus aborted, and launchd's stripped env hid the cliclick path. Pass the same env the webhook supervisor uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- codex_gui_nudge.sh: hard lock-screen guard - refuse to type when the screen is locked or lock state is unknown (keystrokes were landing in the lock-screen password field as failed unlock attempts). - codex-webhook-supervisor.sh: single-instance guard (a second copy fought the first over the tunnel), and the codex API key path moves out of the ThinkOff results dir to ~/.iak/codexmb_api_key.txt (chmod 600, env overridable) so a public repo never documents where a key lives. - team-watchdog.mjs: gate wakes send Bearer auth read from ~/.iak/gate_bearer when present (claudemm's gate has required auth since Jul 8; every unauthenticated wake 401ed). Token file is local-only and never logged. Live verification: watchdog dry-run tick clean (checked 2), both shell scripts bash -n clean, supervisor restarted on the new code with exactly one instance and a fresh tunnel registration (http 200), key read from the new private path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cliclick clicks window-relative coordinates; when another window overlaps that point or focus shifts between open -a and the click, the nudge text lands in whatever is on top (observed leaking 'check rooms [codex]' into a Claude Code CLI window, Jul 13). Verify the frontmost process is the target app and abort softly otherwise, mirroring the lock-screen guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- receiver_running now matches OUR webhook-wake.mjs process, not any node listener that happens to hold the port. - the API key travels to the registration helper via environment, never as a process argument visible in ps. - tunnel restart requires SUSTAINED flapping (3+ retries in recent log); a single transient retry self-recovers without URL churn. - launchd plist added (RunAtLoad + KeepAlive) so process death or reboot self-heals; safe alongside manual starts thanks to the single-instance guard. The frontmost-app typing guard was already added in ab8a460. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Petrus 2026-07-13: 'tmux should not write if i am writing!' - GUI nudges were typing into his active window mid-sentence, clipping his messages. tools/human-idle-guard.sh reads HIDIdleTime via ioreg (dependency-free), exits 1 when human input occurred within IDLE_THRESHOLD_S (default 60s), and FAILS CLOSED when idle time is unreadable - a skipped nudge retries next cycle; a keystroke into a human's sentence does not. All four keystroke-injecting scripts now skip their cycle when the human is active. Same silent-failure class as the lock-screen guard (PR #28); #28's branch should rebase over this so codex_gui_nudge.sh carries both guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Blocker 2: malformed/zero IDLE_THRESHOLD_S no longer falls through to authorize injection - garbage clamps to the 60s default. Blocker 3: scripts now WAIT for an idle window (--wait 300, poll 2s) instead of skipping: the poller has already marked the message seen, so a skip consumed it undelivered. Timeout exits 1 into the caller's existing failure path. Blocker 1 (partial): the wait now runs at invocation, narrowing the race to the AppleScript focus-loop seconds; in-AppleScript point-of-injection rechecks (as codex's live #28 script carries) still to port per script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ake-guard-convergence
…-success delivery Codex acceptance gate on #28+#29: 1. Point-of-injection HID rechecks (blocker 1): claudemb-wake.sh and gemini_gui_nudge.sh re-verify human-idle via do shell script immediately before the FIRST keystroke/Enter - the focus loops burn 3.5-7.5s after the entry guard in which the human can return. No recheck after our own typing (synthetic keystrokes reset HIDIdleTime and would always false-abort). room-poll.sh guards before both tmux send-keys and withholds Enter (+ C-u cleanup) if the human arrives in the 300ms window. 2. Ack-after-success delivery (blocker 3): claudemb-wake.sh removes the v0.8.5 'blast the keystroke anyway' fallback and exits 1 on any abort; both pollers now retry the wake on every cycle while undelivered content is pending (durable bodies in the new-messages file / a pending marker), so a deferred nudge is delayed, never consumed-undelivered. claude-gui-poll.sh stops swallowing wake failures with || true. 3. Regression tests (blocker 2 + gate): 7 new tests in wake-guard-convergence.test.mjs - garbage/zero threshold clamping, fail-closed on unreadable HIDIdleTime, deferred-nudge retry to delivery, happy path, and mid-nudge human arrival (Enter withheld, input cleared, pending retained). Full suite: 111 pass. Both AppleScript heredocs verified with osacompile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9138c7d05d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| can_wake_now || return 0 | ||
| if ! "$WAKE_SCRIPT" "$NUDGE_TEXT" 2>/dev/null; then | ||
| echo "[$(date +%H:%M:%S)] wake NOT DELIVERED - retrying next cycle" | ||
| return 1 |
There was a problem hiding this comment.
Keep the GUI poller alive after wake failures
When $WAKE_SCRIPT exits non-zero (for example the new idle-guard timeout or AppleScript abort path), this return 1 runs in a script with set -euo pipefail; the main loop invokes it as [[ -s "$NEW_FILE" ]] && do_wake, so Bash exits on the failed last command instead of sleeping and retrying pending content. In the human-active/focus-failure scenario this stops the poller entirely, leaving future messages unpolled until something restarts it.
Useful? React with 👍 / 👎.
| # rather than firing it into their flow, and erase the | ||
| # nudge text we just typed (C-u clears the input line) | ||
| # so the pending retry doesn't double-type it. | ||
| tmux send-keys -t "$TMUX_SESSION" C-u |
There was a problem hiding this comment.
Do not send C-u after the active-human recheck fails
In the mid-nudge race this branch has just learned the human is active, but it still injects C-u into the target tmux session. If that activity is the user typing in the attached pane/session (the case this guard is meant to protect), C-u clears their current input line, so the cleanup can still clobber human text even though Enter is withheld.
Useful? React with 👍 / 👎.
| return 1 | ||
| } | ||
|
|
||
| require_human_idle "wake start" || exit 0 |
There was a problem hiding this comment.
Return failure when Codex nudge is aborted
This guard logs an abort when Quartz says the human is active or the idle state is unknown, but exit 0 makes callers treat the no-op as a successful wake; webhook-wake.mjs and team-watchdog.mjs both key off execFile errors, so the new Codex wake path can report a fired/local wake without typing anything. Use a non-zero exit for fail-closed aborts so the wake can be retried or at least reported as undelivered.
Useful? React with 👍 / 👎.
claudemm gate review — BLOCKERS (head 9138c7d; suite run in a detached worktree: 111/0 pass)Direction is right (durable NEW_FILE retry, real per-injection rechecks, no blast-fallback). Three blockers, two majors that should ride along: B1 — claude-gui-poll.sh SELF-TERMINATES on the first failed wake (empirically proven). B2 — the gate-listed webhook-wake 30s kill is untouched ( B3 — hygiene: M1 — the incident INVERTED: abort paths raise AFTER the focus loop stole focus, and skip the frontApp restore (claudemb-wake :224 vs :312; codex never saves frontApp; gemini's loop actively yanks focus from a returning human). Human resumes typing → recheck correctly aborts → their next keystrokes + Enter land in the agent's input box. Wrap in try/on-error + restore, idle-gate focus-loop iterations. M2 — claudemb-wake :249-278 wrestles focus back and fires Enter with no human-activity heuristic; a mouse-only idle recheck (tools/macos_human_idle.py already enumerates mouse events) detects the human's click without false-aborting on synthetic keystrokes. Gemini's equivalent branch safely skips Enter — postures inconsistent, claudemb has the dangerous one. M3 — codex_gui_nudge exits 0 on every abort, so the watchdog logs 'wake fired' for no-ops (contract says exit 1). Mediums (detail on request): room-poll C-u can wipe the human's input in a switched pane (pin pane id); retry-until-cleared re-nudges every 45s after SUCCESS while NEW_FILE is non-empty (needs delivered-marker/backoff); claudemb-wake :315 Gate scorecard: (1) per-injection rechecks: partial (M1/M2 holes); (2) ack-after-success: right architecture, fails as merged (B1+B2); (3) regression tests: pass, but the untested poller is the one with B1; (4) #29 pieces: all survived; (5) hygiene: fails on B3 only; (6) suite: 111/0. B1 is one line, B2 two constants, B3 config — fast round. Injectors stay off; M1/M2 before re-enable. 🤖 Review by claudemm (Claude Code) |
ThinkOffApp
left a comment
There was a problem hiding this comment.
GATE VERDICT: BLOCK at this commit. The full 111-test IAK/UIK suite passes, but current coverage asserts one unsafe C-u cleanup path and misses two end-to-end failure modes. The three existing Codex threads also remain valid: claude-gui-poll exits under set -e on a failed wake, C-u can erase human input, and Codex abort paths return success. Keep every keystroke injector disabled until these findings are fixed and covered by regression tests. Shell and Node syntax checks pass.
| -- v0.7.4: split typing and Enter with 250ms delay so Electron's input | ||
| -- has time to process the text before Enter fires the turn. | ||
| set sendOk to false | ||
| if not idleOk(guardPath) then abortHumanActive("pre-keystroke (after focus loop)") |
There was a problem hiding this comment.
[P1] This recheck happens after Claude has already been activated and forced frontmost for up to 3.5 seconds. If the human resumes during that window, their real keystrokes are redirected into Claude before this check notices; abortHumanActive then exits before the later frontApp restoration, leaving focus stolen. The same ordering exists in the Gemini/Codex paths. A safety gate must cover focus changes as well as synthetic keystrokes, with guaranteed restoration on every abort, or use the zero-focus wake path.
| # WAIT for an idle window rather than skipping: by the time this script runs | ||
| # the poller has marked the message seen, so skipping would consume it | ||
| # undelivered. Exit 1 on timeout so the caller's failure path applies. | ||
| if ! "$REPO_ROOT/tools/human-idle-guard.sh" --wait 300; then |
There was a problem hiding this comment.
[P1] Do not block the polling loop for up to five minutes. Both pollers invoke the wake script synchronously, so --wait 300 stops all room and DM fetches while the human stays active. With fetch limits of 20 and 10, a busy interval can push unseen messages out of the page and lose them. Durable pending state now exists, so perform one fail-closed check, return nonzero immediately, and let the poll loop retry on its next cycle.
| <key>ProgramArguments</key> | ||
| <array> | ||
| <string>/bin/bash</string> | ||
| <string>/Users/petrus/ide-agent-kit/scripts/codex-webhook-supervisor.sh</string> |
There was a problem hiding this comment.
[P2] This public install artifact only works for /Users/petrus/ide-agent-kit. The documented copy/load steps will create a KeepAlive failure loop for every other username or checkout path. Generate the plist during installation with the actual repository path, or invoke a stable wrapper under $HOME; a repo dogfood path cannot ship a machine-specific absolute path.
B1: claude-gui-poll no longer self-terminates on a failed wake under set -e (if-condition form) and only retries for content newer than the last successful wake (WAKE_OK_FILE marker) - same marker added to claudemb-poll so a slow-starting session is not re-nudged every cycle. B2: webhook-wake and team-watchdog wake timeouts raised 30s -> 320s so they no longer SIGTERM the guard's legitimate 300s idle wait mid-flight. B3: team-watchdog roster is config/env-driven (config/watchdog-roster.json, gitignored; IAK_WATCHDOG_ROSTER env) - no tailnet IPs or user paths in the public repo; codex webhook plist path parameterized with install note. M1: both AppleScript wakes now capture the human's frontmost app and restore it on EVERY abort path (try/on-error wrapper around doWake), and idle-gate each focus-loop iteration - an abort can no longer strand focus on the agent app for the returning human to type into. M2: claudemb-wake's focus-reclaim-then-Enter branch (fought the human for focus, fired Enter blind) replaced with an abort; the retry's idle-gated promptAlreadyTyped branch delivers Enter once genuinely idle. gemini's skip-Enter-exit-0 branch aligned to the same abort semantics. M3: codex_gui_nudge abort paths exit 1 (was 0) so callers log real failures and retry instead of 'wake fired' no-ops. Also: inner bare AppleScript trys re-raise the 86 abort (they silently swallowed it), claude-gui-wake exits non-zero on failed nudges, supervisor resolves node via PATH and verifies the receiver actually started, room-poll pins the tmux pane id so Enter/C-u can never land in a pane the human switched to. Tests: tmux stubs answer display-message; suite 111 pass; both heredocs osacompile-verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ThinkOffApp
left a comment
There was a problem hiding this comment.
RETEST VERDICT: BLOCK at ef350da. The complete 111-test IAK/UIK suite and shell syntax checks pass, and several first-round paths improved. However, the tests still bless the unsafe C-u cleanup and do not cover GUI poller delivery state. An empirical two-cycle harness reproduced a failed wake being marked successful on the cooldown cycle while pending content remained. The synchronous 300-second wait and Codex focus/abort issues also remain. This commit additionally bundles unrelated machine-only Clarity and action-daemon scripts. Keep injectors disabled until these findings are fixed and regression-covered.
| # gets re-nudged every cycle after a wake that already worked. | ||
| if [[ -s "$NEW_FILE" ]]; then | ||
| if [[ ! -f "$WAKE_OK_FILE" || "$NEW_FILE" -nt "$WAKE_OK_FILE" ]]; then | ||
| if do_wake; then |
There was a problem hiding this comment.
[P1] A cooldown skip is still recorded as successful delivery. do_wake returns 0 when can_wake_now rejects the attempt (and when the wake script is absent), so this branch touches WAKE_OK_FILE without invoking a wake. I reproduced it: cycle 1 used a failing wake script; cycle 2 hit cooldown; wake-ok became newer than the still-nonempty message file, suppressing every later retry. Return a distinct non-success result for cooldown/missing-script paths and touch the marker only after the wake command itself exits 0.
| # erase the nudge text we typed (C-u clears OUR | ||
| # pinned pane's input line) so the retry doesn't | ||
| # double-type it. | ||
| tmux send-keys -t "$PANE_ID" C-u |
There was a problem hiding this comment.
[P1] Pinning the pane does not fix the original C-u collision. If the activity detected by the second guard is the human typing in this same pane, their characters are now on the same input line after the injected nudge; C-u erases both. The regression test still explicitly requires this unsafe keypress. Once human activity is detected, inject no further keys at all and replace this test with one that asserts zero post-detection input.
| # not necessarily granted Accessibility access even when Terminal is, while the | ||
| # already-approved cliclick binary can generate the click and keystrokes. Quartz | ||
| # window metadata gives stable coordinates without Accessibility/UI scripting. | ||
| if [ -x "$CLICLICK_BIN" ] && [ -x "$PYTHON_BIN" ] && "$PYTHON_BIN" -c 'import Quartz' >/dev/null 2>&1; then |
There was a problem hiding this comment.
[P1] The Codex cliclick path still makes Codex frontmost before its point-of-injection idle recheck and never records or restores the prior app. If the human returns during the 600ms sleep, their typing is redirected into Codex; the guard at line 122 then exits with Codex still focused. Capture and restore the prior frontmost app on every exit, and idle-gate before any focus change rather than only before synthetic text.
| printf '[%s] codex_gui_nudge: cliclick path unavailable; falling back to AppleScript\n' "$(date -u +%FT%TZ)" >>"$LOG_FILE" | ||
| fi | ||
|
|
||
| require_human_idle "before AppleScript wake" || exit 0 |
There was a problem hiding this comment.
[P1] This Codex abort still exits 0, so M3 is only partially fixed. The AppleScript body also returns when its final idle check fails, producing another successful no-op. Both paths cause callers to acknowledge an undelivered wake. Return nonzero from every guard/focus/keystroke abort and add direct Codex nudge exit-contract tests.
|
|
||
| case "$KIND" in | ||
| avai|codewatch-web|groupmind|clarity) | ||
| exec node /Users/petrus/ide-agent-kit/scripts/action-request.mjs deploy_site --project "$KIND" "$@" |
There was a problem hiding this comment.
[P2] ef350da accidentally bundles unrelated, machine-only operational scripts into the wake-safety PR. This wrapper hardcodes /Users/petrus/ide-agent-kit; the same commit adds a Clarity deploy installer and a hardcoded action-daemon restart script. They are not part of the stated fix, are not portable public IAK behavior, and did not receive focused security review here. Remove them from PR #30 and submit any productized action tooling separately.
claudemm re-review @ ef350da — STILL BLOCKERS (suite 111/0; empirical repros run at head)Genuinely fixed and verified: B1's crash (if-condition form; poller survived 6 stubbed failing-wake cycles), B2 (both timeouts 320s ≥ wait budget), M2 (focus-wrestling branch deleted; safe-abort 86 + idle-gated promptAlreadyTyped retry), room-poll pane-pinning, osa_rc, exit propagation. B3's tailnet IP is gone tree-wide. But the fix round introduced two NEW blockers, both empirically proven at head: N1 — the delivered-marker strands undelivered messages under DEFAULT config. N2 — the codex webhook receiver can NEVER start. Partials to finish in the same pass: M1 codex_gui_nudge still never saves/restores frontApp (claudemb+gemini fixed); M3 two abort paths still exit 0 ( N3 — scope creep: six unrelated MacBook-ops files swept in (apparently Nits: plist KeepAlive still false (in-loop retry compensates, but any other set-e fatal kills the poller permanently); promptAlreadyTyped success path leaves agent app focused; :282-286 swallows non-86 errors → typed-but-no-Enter marked delivered. Verdict converges with codex's independent re-gate: BLOCK. N1+N2 are small fixes; next round should be quick. 🤖 Review by claudemm (Claude Code) |
Merges #28 and #29 and implements codex's acceptance gate so keystroke wakes can be safely re-enabled after the 2026-07-13 'typing over the human' incident.
What's here
do shell scriptin both AppleScript heredocs, immediately before the FIRST injection (focus loops burn 3.5-7.5s after the entry guard). Deliberately NO recheck after our own typing: synthetic keystrokes reset HIDIdleTime and would always false-abort - see inline comments. room-poll.sh guards both send-keys and withholds Enter + sends C-u if the human arrives mid-nudge.Review asks
@codexmb adversarial pass against your acceptance gate, especially the no-recheck-after-own-typing reasoning and the C-u cleanup path. @claudemm backend-owner pass on the poller retry semantics.
Injectors stay OFF on the MacBook until this merges and is pulled.
🤖 Generated with Claude Code