-
Notifications
You must be signed in to change notification settings - Fork 3
poller: exactly one wake path per agent, as a config fact (#90 item 4) #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -176,6 +176,12 @@ export async function startRoomPoller({ rooms, apiKey, handle, interval, config, | |
| const session = sessionOpt || config?.tmux?.ide_session || config?.tmux?.default_session || 'claude'; | ||
| const nudgeText = config?.tmux?.nudge_text || 'check rooms'; | ||
| const nudgeMode = config?.poller?.nudge_mode || 'tmux'; | ||
| // Exactly one wake path per agent (issue #90 item 4). On 2026-09-01 the | ||
| // codexmb poller nudged on top of a webhook receiver and the app's own | ||
| // scheduled check, and every mention got two or three answers. The path | ||
| // is a config fact: this poller nudges only when it owns the wake. | ||
| const wakePath = String(config?.poller?.wake_path || (nudgeMode === 'none' ? 'none' : 'nudge')).toLowerCase(); | ||
| const pollerOwnsWake = wakePath === 'nudge'; | ||
|
Comment on lines
+183
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Useful? React with 👍 / 👎. |
||
| const nudgeCommandText = config?.poller?.nudge_command || ''; | ||
| const pollInterval = parsePositiveInt(interval || config?.poller?.interval_sec, 30); | ||
| const selfHandle = normalizeHandle(resolveSelfHandle({ explicit: handle, config })); | ||
|
|
@@ -191,6 +197,7 @@ export async function startRoomPoller({ rooms, apiKey, handle, interval, config, | |
| let lastNudgeAt = 0; | ||
| function nudgeGate(hasPriority) { | ||
| if (!hasPriority) return { fire: false, why: 'no owner/mention in batch' }; | ||
| if (!pollerOwnsWake) return { fire: false, why: `wake path is ${wakePath}, not this poller` }; | ||
| const now = Date.now(); | ||
| if (now - lastNudgeAt < nudgeCooldownSec * 1000) { | ||
| return { fire: false, why: `cooldown ${nudgeCooldownSec}s` }; | ||
|
|
@@ -214,6 +221,11 @@ export async function startRoomPoller({ rooms, apiKey, handle, interval, config, | |
| console.log(` interval: ${pollInterval}s`); | ||
| console.log(` notification file: ${notifyFile}`); | ||
| console.log(` nudge mode: ${nudgeMode}`); | ||
| console.log(` wake path: ${wakePath}${pollerOwnsWake ? '' : ' (this poller will not nudge)'}`); | ||
| if (!pollerOwnsWake && nudgeMode !== 'none') { | ||
| console.error(` WARNING: poller.nudge_mode is '${nudgeMode}' but poller.wake_path is '${wakePath}': ` + | ||
| 'the wake belongs to another path, so nudges from this poller are refused. Set nudge_mode to none to silence this.'); | ||
| } | ||
| if (nudgeMode === 'tmux') { | ||
| console.log(` tmux session: ${session} (optional)`); | ||
| } else if (nudgeMode === 'command') { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| // Issue #90 item 4: exactly one wake path. With wake_path set to another | ||
| // path the poller delivers the notification but never nudges; with the | ||
| // default it nudges (positive control, same stubs). | ||
| import assert from 'node:assert/strict'; | ||
| import { mkdtempSync, rmSync, writeFileSync, chmodSync, mkdirSync, existsSync, readFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import path from 'node:path'; | ||
| import test from 'node:test'; | ||
| import { startRoomPoller } from '../src/team-relay/room-poller.mjs'; | ||
|
|
||
| async function runPoll(dir, pollerOverrides) { | ||
| const stubDir = path.join(dir, 'bin'); mkdirSync(stubDir, { recursive: true }); | ||
| const stub = path.join(stubDir, 'curl'); | ||
| writeFileSync(stub, `#!/bin/sh | ||
| case "$*" in *limit=50*) echo "[]";; *) echo '[{"id":"o1","from":"petrus","body":"claudemm are you there","created_at":"2026-09-02T00:00:01Z"}]';; esac | ||
| `); | ||
| chmodSync(stub, 0o755); | ||
| const marker = path.join(dir, 'nudged'); | ||
| const savedPath = process.env.PATH; process.env.PATH = `${stubDir}:${savedPath}`; | ||
| const logs = []; const origLog = console.log; const origErr = console.error; | ||
| console.log = (...a) => logs.push(a.join(' ')); console.error = (...a) => logs.push(a.join(' ')); | ||
| let timers; | ||
| try { | ||
| timers = await startRoomPoller({ | ||
| rooms: ['r'], apiKey: 'k', handle: '@t', interval: 3600, | ||
| config: { poller: { seen_file: path.join(dir, 'seen'), notification_file: path.join(dir, 'notify'), heartbeat_file: path.join(dir, 'hb'), | ||
| nudge_mode: 'command', nudge_command: `touch ${marker}`, ...pollerOverrides }, queue: { path: path.join(dir, 'q.jsonl') } } | ||
| }); | ||
| } finally { | ||
| if (timers?.roomTimer) clearInterval(timers.roomTimer); | ||
| if (timers?.dmTimer) clearInterval(timers.dmTimer); | ||
| console.log = origLog; console.error = origErr; process.env.PATH = savedPath; | ||
| } | ||
| return { nudged: existsSync(marker), notified: existsSync(path.join(dir, 'notify')) && /petrus: claudemm are you there/.test(readFileSync(path.join(dir, 'notify'), 'utf8')), log: logs.join('\n') }; | ||
| } | ||
|
|
||
| test('wake_path webhook: notification delivered, nudge refused with the reason, warning at start', async () => { | ||
| const dir = mkdtempSync(path.join(tmpdir(), 'iak-wake-')); | ||
| try { | ||
| const r = await runPoll(dir, { wake_path: 'webhook' }); | ||
| assert.equal(r.notified, true); | ||
| assert.equal(r.nudged, false, 'poller must not nudge when the webhook owns the wake'); | ||
| assert.match(r.log, /wake path is webhook, not this poller/); | ||
| assert.match(r.log, /WARNING: poller.nudge_mode is 'command' but poller.wake_path is 'webhook'/); | ||
| } finally { rmSync(dir, { recursive: true, force: true }); } | ||
| }); | ||
|
|
||
| test('default wake_path: the same owner message nudges (positive control)', async () => { | ||
| const dir = mkdtempSync(path.join(tmpdir(), 'iak-wake-')); | ||
| try { | ||
| const r = await runPoll(dir, {}); | ||
| assert.equal(r.notified, true); | ||
| assert.equal(r.nudged, true, 'control: with the poller owning the wake, the nudge command runs'); | ||
| assert.match(r.log, /wake path: nudge/); | ||
| } finally { rmSync(dir, { recursive: true, force: true }); } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user follows the README's Codex Desktop setup and copies this advertised ready-to-copy example, these settings disable the included
codex_gui_nudge.shcommand, but the example does not configure or startscripts/codex-webhook-supervisor.sh. Consequently, room and DM messages are written to the notification file without waking the desktop app. Keep this example on command-mode nudging, or include the required webhook receiver setup before selectingwebhook.Useful? React with 👍 / 👎.