oh-my-codex 0.11.12 → 0.11.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +5 -5
- package/Cargo.toml +1 -1
- package/README.md +23 -0
- package/README.vi.md +144 -185
- package/crates/omx-runtime-core/src/engine.rs +122 -4
- package/crates/omx-runtime-core/src/lib.rs +17 -0
- package/dist/cli/__tests__/autoresearch.test.js +11 -0
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/cleanup.test.js +117 -4
- package/dist/cli/__tests__/cleanup.test.js.map +1 -1
- package/dist/cli/__tests__/error-handling-warnings.test.js +13 -0
- package/dist/cli/__tests__/error-handling-warnings.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +6 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +94 -1
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +3 -0
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +10 -0
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/packaged-script-resolution.test.js +4 -3
- package/dist/cli/__tests__/packaged-script-resolution.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +6 -0
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +29 -12
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/star-prompt.test.js +16 -0
- package/dist/cli/__tests__/star-prompt.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +112 -1
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +2 -0
- package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +1 -0
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -0
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -0
- package/dist/cli/cleanup.d.ts +2 -0
- package/dist/cli/cleanup.d.ts.map +1 -1
- package/dist/cli/cleanup.js +26 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +161 -50
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +15 -14
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/star-prompt.d.ts.map +1 -1
- package/dist/cli/star-prompt.js +1 -0
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +5 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +26 -0
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +1 -0
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +4 -4
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/mcp-registry.test.js +13 -16
- package/dist/config/__tests__/mcp-registry.test.js.map +1 -1
- package/dist/config/mcp-registry.d.ts +1 -0
- package/dist/config/mcp-registry.d.ts.map +1 -1
- package/dist/config/mcp-registry.js +4 -4
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +1 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +39 -1
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +12 -1
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +499 -17
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +140 -14
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js +5 -0
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +2 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +597 -0
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +15 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +73 -53
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +193 -2
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +183 -0
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +255 -97
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +46 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +1 -0
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +48 -0
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1 -0
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +70 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +10 -37
- package/dist/hud/state.js.map +1 -1
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +5 -0
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +46 -0
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +4 -0
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +2 -0
- package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +1 -0
- package/dist/notifications/__tests__/custom-alias-enablement.test.js +84 -0
- package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +1 -0
- package/dist/notifications/__tests__/idle-cooldown.test.js +55 -0
- package/dist/notifications/__tests__/idle-cooldown.test.js.map +1 -1
- package/dist/notifications/idle-cooldown.d.ts +8 -6
- package/dist/notifications/idle-cooldown.d.ts.map +1 -1
- package/dist/notifications/idle-cooldown.js +53 -22
- package/dist/notifications/idle-cooldown.js.map +1 -1
- package/dist/notifications/notifier.js +1 -1
- package/dist/notifications/notifier.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +1 -0
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/openclaw/config.js +2 -2
- package/dist/openclaw/config.js.map +1 -1
- package/dist/runtime/bridge.d.ts +1 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +2 -6
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +97 -59
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +72 -238
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +19 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -0
- package/dist/scripts/notify-hook/managed-tmux.js +320 -0
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -0
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +22 -0
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -0
- package/dist/scripts/notify-hook/ralph-session-resume.js +277 -0
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -0
- package/dist/scripts/notify-hook/state-io.d.ts +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +2 -10
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +60 -59
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +13 -5
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +1 -19
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +4 -4
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +102 -35
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +144 -20
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts +1 -0
- package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
- package/dist/scripts/tmux-hook-engine.js +3 -0
- package/dist/scripts/tmux-hook-engine.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +96 -4
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/leader-activity.test.js +107 -2
- package/dist/team/__tests__/leader-activity.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +32 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +148 -0
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/shutdown-fallback.test.js +13 -0
- package/dist/team/__tests__/shutdown-fallback.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +11 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +16 -5
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +460 -2
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +34 -7
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.d.ts +60 -0
- package/dist/team/commit-hygiene.d.ts.map +1 -0
- package/dist/team/commit-hygiene.js +232 -0
- package/dist/team/commit-hygiene.js.map +1 -0
- package/dist/team/leader-activity.d.ts.map +1 -1
- package/dist/team/leader-activity.js +17 -35
- package/dist/team/leader-activity.js.map +1 -1
- package/dist/team/runtime-cli.d.ts +9 -1
- package/dist/team/runtime-cli.d.ts.map +1 -1
- package/dist/team/runtime-cli.js +15 -6
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +7 -2
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +391 -63
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/state/dispatch.js +1 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/mailbox.d.ts +1 -0
- package/dist/team/state/mailbox.d.ts.map +1 -1
- package/dist/team/state/mailbox.js +54 -8
- package/dist/team/state/mailbox.js.map +1 -1
- package/dist/team/state-root.d.ts +1 -1
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +8 -3
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +66 -3
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +69 -27
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/utils/__tests__/platform-command.test.js +101 -2
- package/dist/utils/__tests__/platform-command.test.js.map +1 -1
- package/dist/utils/git-layout.d.ts +8 -0
- package/dist/utils/git-layout.d.ts.map +1 -0
- package/dist/utils/git-layout.js +58 -0
- package/dist/utils/git-layout.js.map +1 -0
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +32 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/package.json +6 -6
- package/src/scripts/notify-fallback-watcher.ts +96 -58
- package/src/scripts/notify-hook/auto-nudge.ts +75 -230
- package/src/scripts/notify-hook/managed-tmux.ts +324 -0
- package/src/scripts/notify-hook/ralph-session-resume.ts +337 -0
- package/src/scripts/notify-hook/state-io.ts +2 -10
- package/src/scripts/notify-hook/team-dispatch.ts +70 -54
- package/src/scripts/notify-hook/team-leader-nudge.ts +19 -5
- package/src/scripts/notify-hook/team-tmux-guard.ts +0 -20
- package/src/scripts/notify-hook/team-worker.ts +4 -4
- package/src/scripts/notify-hook/tmux-injection.ts +103 -33
- package/src/scripts/notify-hook.ts +150 -21
- package/src/scripts/tmux-hook-engine.ts +4 -0
|
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { spawn, spawnSync } from 'node:child_process';
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { initTeamState, enqueueDispatchRequest, readDispatchRequest } from '../../team/state.js';
|
|
10
|
+
import { buildWindowsMsysBackgroundHelperBootstrapScript } from '../../cli/index.js';
|
|
10
11
|
import { writeSessionStart } from '../session.js';
|
|
11
12
|
async function appendLine(path, line) {
|
|
12
13
|
const prev = await readFile(path, 'utf-8');
|
|
@@ -111,7 +112,7 @@ if [[ "$cmd" == "display-message" ]]; then
|
|
|
111
112
|
exit 0
|
|
112
113
|
fi
|
|
113
114
|
if [[ "$fmt" == "#S" ]]; then
|
|
114
|
-
echo "session-test"
|
|
115
|
+
echo "\${OMX_TEST_TMUX_SESSION_NAME:-session-test}"
|
|
115
116
|
exit 0
|
|
116
117
|
fi
|
|
117
118
|
exit 0
|
|
@@ -129,6 +130,20 @@ if [[ "$cmd" == "send-keys" ]]; then
|
|
|
129
130
|
exit 0
|
|
130
131
|
fi
|
|
131
132
|
if [[ "$cmd" == "list-panes" ]]; then
|
|
133
|
+
target=""
|
|
134
|
+
while [[ "$#" -gt 0 ]]; do
|
|
135
|
+
case "$1" in
|
|
136
|
+
-t)
|
|
137
|
+
shift
|
|
138
|
+
target="$1"
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
141
|
+
shift || true
|
|
142
|
+
done
|
|
143
|
+
if [[ -n "$target" ]]; then
|
|
144
|
+
printf "%%42\tcodex\tcodex\n"
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
132
147
|
echo "%42 1"
|
|
133
148
|
exit 0
|
|
134
149
|
fi
|
|
@@ -372,6 +387,71 @@ describe('notify-fallback watcher', () => {
|
|
|
372
387
|
await rm(wd, { recursive: true, force: true });
|
|
373
388
|
}
|
|
374
389
|
});
|
|
390
|
+
it('suppresses idle no-op lifecycle and control-plane logs during authority-only one-shot ticks', async () => {
|
|
391
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-authority-noop-'));
|
|
392
|
+
try {
|
|
393
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
394
|
+
await mkdir(join(wd, '.omx', 'state'), { recursive: true });
|
|
395
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
396
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
397
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
|
|
398
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
399
|
+
const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
|
|
400
|
+
const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
|
|
401
|
+
assert.equal(watcherState.authority_only, true);
|
|
402
|
+
assert.equal(watcherState.dispatch_drain?.run_count, 1);
|
|
403
|
+
assert.equal(watcherState.dispatch_drain?.last_result?.processed ?? 0, 0);
|
|
404
|
+
assert.equal(watcherState.leader_nudge?.run_count, 1);
|
|
405
|
+
assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
|
|
406
|
+
assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'hud_state_missing');
|
|
407
|
+
const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
408
|
+
const logContent = await readFile(logPath, 'utf-8').catch(() => '');
|
|
409
|
+
assert.equal(logContent.trim(), '');
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
await rm(wd, { recursive: true, force: true });
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
it('suppresses authority-only control-plane ticks when only skill-active-state carries the deep-interview input lock', async () => {
|
|
416
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-authority-skill-lock-'));
|
|
417
|
+
try {
|
|
418
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
419
|
+
await mkdir(join(wd, '.omx', 'state'), { recursive: true });
|
|
420
|
+
await writeFile(join(wd, '.omx', 'state', 'skill-active-state.json'), JSON.stringify({
|
|
421
|
+
version: 1,
|
|
422
|
+
active: true,
|
|
423
|
+
skill: 'deep-interview',
|
|
424
|
+
keyword: 'deep interview',
|
|
425
|
+
phase: 'planning',
|
|
426
|
+
activated_at: '2026-02-25T00:00:00.000Z',
|
|
427
|
+
updated_at: '2026-02-25T00:00:00.000Z',
|
|
428
|
+
source: 'keyword-detector',
|
|
429
|
+
input_lock: {
|
|
430
|
+
active: true,
|
|
431
|
+
scope: 'deep-interview-auto-approval',
|
|
432
|
+
acquired_at: '2026-02-25T00:00:00.000Z',
|
|
433
|
+
blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
|
|
434
|
+
message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
|
|
435
|
+
},
|
|
436
|
+
}, null, 2));
|
|
437
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
438
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
439
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
|
|
440
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
441
|
+
const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
|
|
442
|
+
const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
|
|
443
|
+
assert.equal(watcherState.authority_only, true);
|
|
444
|
+
assert.equal(watcherState.dispatch_drain?.run_count, 1);
|
|
445
|
+
assert.equal(watcherState.leader_nudge?.run_count, 0);
|
|
446
|
+
assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'init');
|
|
447
|
+
const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
448
|
+
const logContent = await readFile(logPath, 'utf-8').catch(() => '');
|
|
449
|
+
assert.equal(logContent.trim(), '');
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
await rm(wd, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
375
455
|
it('disables fallback watcher nudges when deep-interview state is active', async () => {
|
|
376
456
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-deep-interview-suppressed-'));
|
|
377
457
|
const fakeBinDir = join(wd, 'fake-bin');
|
|
@@ -423,6 +503,69 @@ describe('notify-fallback watcher', () => {
|
|
|
423
503
|
await rm(wd, { recursive: true, force: true });
|
|
424
504
|
}
|
|
425
505
|
});
|
|
506
|
+
it('disables fallback watcher nudges when only skill-active-state carries the deep-interview input lock', async () => {
|
|
507
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-deep-interview-skill-lock-'));
|
|
508
|
+
const fakeBinDir = join(wd, 'fake-bin');
|
|
509
|
+
const tmuxLogPath = join(wd, 'tmux.log');
|
|
510
|
+
try {
|
|
511
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
512
|
+
await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team'), { recursive: true });
|
|
513
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
514
|
+
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
515
|
+
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
516
|
+
await writeFile(join(wd, '.omx', 'state', 'skill-active-state.json'), JSON.stringify({
|
|
517
|
+
version: 1,
|
|
518
|
+
active: true,
|
|
519
|
+
skill: 'deep-interview',
|
|
520
|
+
keyword: 'deep interview',
|
|
521
|
+
phase: 'planning',
|
|
522
|
+
activated_at: '2026-02-25T00:00:00.000Z',
|
|
523
|
+
updated_at: '2026-02-25T00:00:00.000Z',
|
|
524
|
+
source: 'keyword-detector',
|
|
525
|
+
input_lock: {
|
|
526
|
+
active: true,
|
|
527
|
+
scope: 'deep-interview-auto-approval',
|
|
528
|
+
acquired_at: '2026-02-25T00:00:00.000Z',
|
|
529
|
+
blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
|
|
530
|
+
message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
|
|
531
|
+
},
|
|
532
|
+
}, null, 2));
|
|
533
|
+
await writeFile(join(wd, '.omx', 'state', 'ralph-state.json'), JSON.stringify({
|
|
534
|
+
active: true,
|
|
535
|
+
current_phase: 'executing',
|
|
536
|
+
tmux_pane_id: '%42',
|
|
537
|
+
}, null, 2));
|
|
538
|
+
await writeFile(join(wd, '.omx', 'state', 'team-state.json'), JSON.stringify({
|
|
539
|
+
active: true,
|
|
540
|
+
team_name: 'dispatch-team',
|
|
541
|
+
current_phase: 'team-exec',
|
|
542
|
+
}, null, 2));
|
|
543
|
+
await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
544
|
+
last_turn_at: new Date(Date.now() - 300_000).toISOString(),
|
|
545
|
+
turn_count: 3,
|
|
546
|
+
last_agent_output: 'Would you like me to continue?',
|
|
547
|
+
}, null, 2));
|
|
548
|
+
await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
|
|
549
|
+
name: 'dispatch-team',
|
|
550
|
+
tmux_session: 'omx-team-dispatch-team',
|
|
551
|
+
leader_pane_id: '%42',
|
|
552
|
+
}, null, 2));
|
|
553
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
554
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
555
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
|
|
556
|
+
encoding: 'utf-8',
|
|
557
|
+
env: buildCleanNotifyEnv({ PATH: `${fakeBinDir}:${process.env.PATH || ''}` }),
|
|
558
|
+
});
|
|
559
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
560
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
|
|
561
|
+
assert.doesNotMatch(tmuxLog, /Ralph loop active continue/);
|
|
562
|
+
assert.doesNotMatch(tmuxLog, /Team dispatch-team:/);
|
|
563
|
+
assert.doesNotMatch(tmuxLog, /yes, proceed \[OMX_TMUX_INJECT\]/);
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
await rm(wd, { recursive: true, force: true });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
426
569
|
it('runs leader nudge checks from the fallback watcher so stale alerts do not wait for a leader turn', async () => {
|
|
427
570
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-leader-nudge-'));
|
|
428
571
|
const fakeBinDir = join(wd, 'fake-bin');
|
|
@@ -532,9 +675,181 @@ describe('notify-fallback watcher', () => {
|
|
|
532
675
|
const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
533
676
|
const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
534
677
|
const nudgeEvent = logEntries.find((entry) => entry.type === 'leader_nudge_tick');
|
|
535
|
-
assert.
|
|
536
|
-
|
|
537
|
-
|
|
678
|
+
assert.equal(nudgeEvent, undefined);
|
|
679
|
+
}
|
|
680
|
+
finally {
|
|
681
|
+
await rm(wd, { recursive: true, force: true });
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
it('runs stalled-worker leader nudges from the fallback watcher even when the leader is not stale', async () => {
|
|
685
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-worker-stall-nudge-'));
|
|
686
|
+
const fakeBinDir = join(wd, 'fake-bin');
|
|
687
|
+
const tmuxLogPath = join(wd, 'tmux.log');
|
|
688
|
+
try {
|
|
689
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
690
|
+
await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1'), { recursive: true });
|
|
691
|
+
await mkdir(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'tasks'), { recursive: true });
|
|
692
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
693
|
+
const tmuxScript = `#!/usr/bin/env bash
|
|
694
|
+
set -eu
|
|
695
|
+
echo "$@" >> "${tmuxLogPath}"
|
|
696
|
+
cmd="$1"
|
|
697
|
+
shift || true
|
|
698
|
+
if [[ "$cmd" == "display-message" ]]; then
|
|
699
|
+
target=""
|
|
700
|
+
fmt=""
|
|
701
|
+
while [[ "$#" -gt 0 ]]; do
|
|
702
|
+
case "$1" in
|
|
703
|
+
-t)
|
|
704
|
+
shift
|
|
705
|
+
target="$1"
|
|
706
|
+
;;
|
|
707
|
+
*)
|
|
708
|
+
fmt="$1"
|
|
709
|
+
;;
|
|
710
|
+
esac
|
|
711
|
+
shift || true
|
|
712
|
+
done
|
|
713
|
+
if [[ "$fmt" == "#{pane_in_mode}" ]]; then
|
|
714
|
+
echo "0"
|
|
715
|
+
exit 0
|
|
716
|
+
fi
|
|
717
|
+
if [[ "$fmt" == "#{pane_id}" ]]; then
|
|
718
|
+
echo "\${target:-%42}"
|
|
719
|
+
exit 0
|
|
720
|
+
fi
|
|
721
|
+
if [[ "$fmt" == "#{pane_current_path}" ]]; then
|
|
722
|
+
dirname "${tmuxLogPath}"
|
|
723
|
+
exit 0
|
|
724
|
+
fi
|
|
725
|
+
if [[ "$fmt" == "#{pane_current_command}" ]]; then
|
|
726
|
+
echo "codex"
|
|
727
|
+
exit 0
|
|
728
|
+
fi
|
|
729
|
+
if [[ "$fmt" == "#S" ]]; then
|
|
730
|
+
echo "omx-team-dispatch-team"
|
|
731
|
+
exit 0
|
|
732
|
+
fi
|
|
733
|
+
exit 0
|
|
734
|
+
fi
|
|
735
|
+
if [[ "$cmd" == "send-keys" ]]; then
|
|
736
|
+
exit 0
|
|
737
|
+
fi
|
|
738
|
+
if [[ "$cmd" == "list-panes" ]]; then
|
|
739
|
+
target=""
|
|
740
|
+
while [[ "$#" -gt 0 ]]; do
|
|
741
|
+
case "$1" in
|
|
742
|
+
-t)
|
|
743
|
+
shift
|
|
744
|
+
target="$1"
|
|
745
|
+
;;
|
|
746
|
+
esac
|
|
747
|
+
shift || true
|
|
748
|
+
done
|
|
749
|
+
if [[ -n "$target" ]]; then
|
|
750
|
+
printf "%%42 12345\n%%10 12346\n%%11 12347\n"
|
|
751
|
+
exit 0
|
|
752
|
+
fi
|
|
753
|
+
echo "%42 1"
|
|
754
|
+
exit 0
|
|
755
|
+
fi
|
|
756
|
+
exit 0
|
|
757
|
+
`;
|
|
758
|
+
await writeFile(join(fakeBinDir, 'tmux'), tmuxScript);
|
|
759
|
+
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
760
|
+
const now = Date.now();
|
|
761
|
+
await writeFile(join(wd, '.omx', 'state', 'team-state.json'), JSON.stringify({
|
|
762
|
+
active: true,
|
|
763
|
+
team_name: 'dispatch-team',
|
|
764
|
+
current_phase: 'team-exec',
|
|
765
|
+
}, null, 2));
|
|
766
|
+
await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
767
|
+
last_turn_at: new Date().toISOString(),
|
|
768
|
+
turn_count: 3,
|
|
769
|
+
}, null, 2));
|
|
770
|
+
await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
|
|
771
|
+
name: 'dispatch-team',
|
|
772
|
+
tmux_session: 'omx-team-dispatch-team',
|
|
773
|
+
leader_pane_id: '%42',
|
|
774
|
+
workers: [
|
|
775
|
+
{ name: 'worker-1', index: 1, pane_id: '%10' },
|
|
776
|
+
{ name: 'worker-2', index: 2, pane_id: '%11' },
|
|
777
|
+
],
|
|
778
|
+
}, null, 2));
|
|
779
|
+
await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'tasks', 'task-1.json'), JSON.stringify({
|
|
780
|
+
id: '1',
|
|
781
|
+
subject: 'Pending work',
|
|
782
|
+
description: 'Needs attention',
|
|
783
|
+
status: 'pending',
|
|
784
|
+
created_at: new Date().toISOString(),
|
|
785
|
+
}, null, 2));
|
|
786
|
+
await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'status.json'), JSON.stringify({
|
|
787
|
+
state: 'working',
|
|
788
|
+
current_task_id: '1',
|
|
789
|
+
updated_at: new Date(now - 180_000).toISOString(),
|
|
790
|
+
}, null, 2));
|
|
791
|
+
await writeFile(join(wd, '.omx', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'heartbeat.json'), JSON.stringify({
|
|
792
|
+
alive: true,
|
|
793
|
+
pid: 101,
|
|
794
|
+
turn_count: 2,
|
|
795
|
+
last_turn_at: new Date(now - 180_000).toISOString(),
|
|
796
|
+
}, null, 2));
|
|
797
|
+
await writeFile(join(wd, '.omx', 'state', 'team-leader-nudge.json'), JSON.stringify({
|
|
798
|
+
last_nudged_by_team: {
|
|
799
|
+
'dispatch-team': {
|
|
800
|
+
at: new Date(now - 5_000).toISOString(),
|
|
801
|
+
last_message_id: '',
|
|
802
|
+
reason: 'new_mailbox_message',
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
progress_by_team: {
|
|
806
|
+
'dispatch-team': {
|
|
807
|
+
signature: JSON.stringify({
|
|
808
|
+
tasks: [{ id: '1', owner: '', status: 'pending' }],
|
|
809
|
+
workers: [
|
|
810
|
+
{
|
|
811
|
+
worker: 'worker-1',
|
|
812
|
+
state: 'working',
|
|
813
|
+
current_task_id: '1',
|
|
814
|
+
status_missing: false,
|
|
815
|
+
turn_count: 2,
|
|
816
|
+
heartbeat_missing: false,
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
worker: 'worker-2',
|
|
820
|
+
state: 'unknown',
|
|
821
|
+
current_task_id: '',
|
|
822
|
+
status_missing: true,
|
|
823
|
+
turn_count: null,
|
|
824
|
+
heartbeat_missing: true,
|
|
825
|
+
},
|
|
826
|
+
],
|
|
827
|
+
}),
|
|
828
|
+
last_progress_at: new Date(now - 180_000).toISOString(),
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
}, null, 2));
|
|
832
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
833
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
834
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
|
|
835
|
+
encoding: 'utf-8',
|
|
836
|
+
env: buildCleanNotifyEnv({
|
|
837
|
+
PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
|
|
838
|
+
OMX_TEAM_PROGRESS_STALL_MS: '60000',
|
|
839
|
+
OMX_TEAM_LEADER_NUDGE_MS: '30000',
|
|
840
|
+
OMX_TEAM_LEADER_STALE_MS: '60000',
|
|
841
|
+
}),
|
|
842
|
+
});
|
|
843
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
844
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf8');
|
|
845
|
+
assert.match(tmuxLog, /send-keys -t %42 -l Team dispatch-team: worker panes stalled, no progress 3m/);
|
|
846
|
+
assert.doesNotMatch(tmuxLog, /leader stale/);
|
|
847
|
+
const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
|
|
848
|
+
const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
|
|
849
|
+
assert.equal(watcherState.leader_nudge?.enabled, true);
|
|
850
|
+
assert.equal(watcherState.leader_nudge?.leader_only, true);
|
|
851
|
+
assert.equal(watcherState.leader_nudge?.run_count, 1);
|
|
852
|
+
assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
|
|
538
853
|
}
|
|
539
854
|
finally {
|
|
540
855
|
await rm(wd, { recursive: true, force: true });
|
|
@@ -569,6 +884,7 @@ describe('notify-fallback watcher', () => {
|
|
|
569
884
|
PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
|
|
570
885
|
CODEX_HOME: codexHome,
|
|
571
886
|
OMX_SESSION_ID: 'sess-managed-fallback',
|
|
887
|
+
OMX_TEST_TMUX_SESSION_NAME: 'omx-fallback-auto-nudge-stalled-managed',
|
|
572
888
|
TMUX: '1',
|
|
573
889
|
TMUX_PANE: '%42',
|
|
574
890
|
OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
|
|
@@ -587,6 +903,98 @@ describe('notify-fallback watcher', () => {
|
|
|
587
903
|
await rm(wd, { recursive: true, force: true });
|
|
588
904
|
}
|
|
589
905
|
});
|
|
906
|
+
it('respects `.omx/tmux-hook.json` enabled:false for fallback auto-nudge', async () => {
|
|
907
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-disabled-'));
|
|
908
|
+
const fakeBinDir = join(wd, 'fake-bin');
|
|
909
|
+
const tmuxLogPath = join(wd, 'tmux.log');
|
|
910
|
+
const codexHome = join(wd, 'codex-home');
|
|
911
|
+
try {
|
|
912
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
913
|
+
await mkdir(join(wd, '.omx', 'state'), { recursive: true });
|
|
914
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
915
|
+
await mkdir(codexHome, { recursive: true });
|
|
916
|
+
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
917
|
+
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
918
|
+
await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
|
|
919
|
+
autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
|
|
920
|
+
}, null, 2));
|
|
921
|
+
await writeFile(join(wd, '.omx', 'tmux-hook.json'), JSON.stringify({
|
|
922
|
+
enabled: false,
|
|
923
|
+
target: { type: 'pane', value: '%42' },
|
|
924
|
+
}, null, 2));
|
|
925
|
+
await writeSessionStart(wd, 'sess-managed-fallback');
|
|
926
|
+
await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
927
|
+
last_turn_at: new Date(Date.now() - 6_000).toISOString(),
|
|
928
|
+
turn_count: 7,
|
|
929
|
+
last_agent_output: 'If you want, I can keep going from here.',
|
|
930
|
+
}, null, 2));
|
|
931
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
932
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
933
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
|
|
934
|
+
encoding: 'utf-8',
|
|
935
|
+
env: buildCleanNotifyEnv({
|
|
936
|
+
PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
|
|
937
|
+
CODEX_HOME: codexHome,
|
|
938
|
+
OMX_SESSION_ID: 'sess-managed-fallback',
|
|
939
|
+
TMUX: '1',
|
|
940
|
+
TMUX_PANE: '%42',
|
|
941
|
+
OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
|
|
942
|
+
}),
|
|
943
|
+
});
|
|
944
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
945
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
|
|
946
|
+
assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
|
|
947
|
+
}
|
|
948
|
+
finally {
|
|
949
|
+
await rm(wd, { recursive: true, force: true });
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
it('suppresses fallback unmanaged-session auto-nudge skip logs while idle', async () => {
|
|
953
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-unmanaged-'));
|
|
954
|
+
const fakeBinDir = join(wd, 'fake-bin');
|
|
955
|
+
const tmuxLogPath = join(wd, 'tmux.log');
|
|
956
|
+
const codexHome = join(wd, 'codex-home');
|
|
957
|
+
try {
|
|
958
|
+
await mkdir(join(wd, '.omx', 'logs'), { recursive: true });
|
|
959
|
+
await mkdir(join(wd, '.omx', 'state'), { recursive: true });
|
|
960
|
+
await mkdir(fakeBinDir, { recursive: true });
|
|
961
|
+
await mkdir(codexHome, { recursive: true });
|
|
962
|
+
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
963
|
+
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
964
|
+
await writeFile(join(codexHome, '.omx-config.json'), JSON.stringify({
|
|
965
|
+
autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
|
|
966
|
+
}, null, 2));
|
|
967
|
+
await writeFile(join(wd, '.omx', 'state', 'hud-state.json'), JSON.stringify({
|
|
968
|
+
last_turn_at: new Date(Date.now() - 6_000).toISOString(),
|
|
969
|
+
turn_count: 9,
|
|
970
|
+
last_agent_output: 'If you want, I can keep going from here.',
|
|
971
|
+
}, null, 2));
|
|
972
|
+
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
973
|
+
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
974
|
+
const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
|
|
975
|
+
encoding: 'utf-8',
|
|
976
|
+
env: buildCleanNotifyEnv({
|
|
977
|
+
PATH: `${fakeBinDir}:${process.env.PATH || ''}`,
|
|
978
|
+
CODEX_HOME: codexHome,
|
|
979
|
+
TMUX: '1',
|
|
980
|
+
TMUX_PANE: '%42',
|
|
981
|
+
OMX_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
|
|
982
|
+
}),
|
|
983
|
+
});
|
|
984
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
985
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
|
|
986
|
+
assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l yes, proceed \[OMX_TMUX_INJECT\]/);
|
|
987
|
+
const watcherStatePath = join(wd, '.omx', 'state', 'notify-fallback-state.json');
|
|
988
|
+
const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
|
|
989
|
+
assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'eligible_but_not_sent');
|
|
990
|
+
const tmuxHookLogPath = join(wd, '.omx', 'logs', `tmux-hook-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
991
|
+
const tmuxHookLog = await readFile(tmuxHookLogPath, 'utf-8').catch(() => '');
|
|
992
|
+
assert.equal(tmuxHookLog.trim(), '');
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
await rm(wd, { recursive: true, force: true });
|
|
996
|
+
}
|
|
997
|
+
});
|
|
590
998
|
it('does not auto-nudge stalled-like output when the latest turn is still fresh', async () => {
|
|
591
999
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-auto-nudge-fresh-'));
|
|
592
1000
|
const fakeBinDir = join(wd, 'fake-bin');
|
|
@@ -737,7 +1145,9 @@ describe('notify-fallback watcher', () => {
|
|
|
737
1145
|
});
|
|
738
1146
|
it('runs bounded non-turn team dispatch drain tick in leader context', async () => {
|
|
739
1147
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-dispatch-'));
|
|
1148
|
+
const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
740
1149
|
try {
|
|
1150
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
741
1151
|
await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
|
|
742
1152
|
const queued = await enqueueDispatchRequest('dispatch-team', {
|
|
743
1153
|
kind: 'inbox',
|
|
@@ -754,12 +1164,18 @@ describe('notify-fallback watcher', () => {
|
|
|
754
1164
|
assert.notEqual(request?.status, 'pending');
|
|
755
1165
|
}
|
|
756
1166
|
finally {
|
|
1167
|
+
if (typeof previousRuntimeBridge === 'string')
|
|
1168
|
+
process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
|
|
1169
|
+
else
|
|
1170
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
757
1171
|
await rm(wd, { recursive: true, force: true });
|
|
758
1172
|
}
|
|
759
1173
|
});
|
|
760
1174
|
it('skips dispatch drain in worker context (leader-only guard)', async () => {
|
|
761
1175
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-dispatch-worker-'));
|
|
1176
|
+
const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
762
1177
|
try {
|
|
1178
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
763
1179
|
await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
|
|
764
1180
|
const queued = await enqueueDispatchRequest('dispatch-team', {
|
|
765
1181
|
kind: 'inbox',
|
|
@@ -781,11 +1197,13 @@ describe('notify-fallback watcher', () => {
|
|
|
781
1197
|
const logPath = join(wd, '.omx', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
|
|
782
1198
|
const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
783
1199
|
const drainEvent = logEntries.find((entry) => entry.type === 'dispatch_drain_tick');
|
|
784
|
-
assert.
|
|
785
|
-
assert.equal(drainEvent.leader_only, false);
|
|
786
|
-
assert.equal(drainEvent.reason, 'worker_context');
|
|
1200
|
+
assert.equal(drainEvent, undefined);
|
|
787
1201
|
}
|
|
788
1202
|
finally {
|
|
1203
|
+
if (typeof previousRuntimeBridge === 'string')
|
|
1204
|
+
process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
|
|
1205
|
+
else
|
|
1206
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
789
1207
|
await rm(wd, { recursive: true, force: true });
|
|
790
1208
|
}
|
|
791
1209
|
});
|
|
@@ -794,18 +1212,20 @@ describe('notify-fallback watcher', () => {
|
|
|
794
1212
|
const fakeBinDir = join(wd, 'fake-bin');
|
|
795
1213
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
796
1214
|
const captureFile = join(wd, 'capture.txt');
|
|
1215
|
+
const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
797
1216
|
try {
|
|
1217
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
798
1218
|
await mkdir(fakeBinDir, { recursive: true });
|
|
799
1219
|
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
800
1220
|
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
801
|
-
await writeFile(captureFile, '
|
|
1221
|
+
await writeFile(captureFile, '... ping ...');
|
|
802
1222
|
await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
|
|
803
1223
|
const queued = await enqueueDispatchRequest('dispatch-team', {
|
|
804
1224
|
kind: 'inbox',
|
|
805
1225
|
to_worker: 'worker-1',
|
|
806
1226
|
worker_index: 1,
|
|
807
1227
|
pane_id: '%42',
|
|
808
|
-
trigger_message: '
|
|
1228
|
+
trigger_message: 'ping',
|
|
809
1229
|
}, wd);
|
|
810
1230
|
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
811
1231
|
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
@@ -819,14 +1239,21 @@ describe('notify-fallback watcher', () => {
|
|
|
819
1239
|
const second = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env });
|
|
820
1240
|
assert.equal(second.status, 0, second.stderr || second.stdout);
|
|
821
1241
|
const tmuxLog = await readFile(tmuxLogPath, 'utf8');
|
|
822
|
-
const typeMatches = tmuxLog.match(/send-keys -t %42 -l
|
|
823
|
-
assert.equal(typeMatches.length, 1, '
|
|
1242
|
+
const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
|
|
1243
|
+
assert.equal(typeMatches.length, 1, 'fresh attempt should type once; retries with draft should be submit-only');
|
|
1244
|
+
const cmMatches = tmuxLog.match(/send-keys -t %42 C-m/g) || [];
|
|
1245
|
+
assert.ok(cmMatches.length > 0, 'submit should use C-m');
|
|
824
1246
|
assert.ok(!/send-keys[^\n]*-l[^\n]*C-m/.test(tmuxLog), 'must keep -l payload and C-m submits isolated');
|
|
825
1247
|
const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
|
|
826
1248
|
assert.equal(request?.status, 'pending');
|
|
1249
|
+
assert.equal(request?.attempt_count, 2);
|
|
827
1250
|
assert.equal(request?.last_reason, 'tmux_send_keys_unconfirmed');
|
|
828
1251
|
}
|
|
829
1252
|
finally {
|
|
1253
|
+
if (typeof previousRuntimeBridge === 'string')
|
|
1254
|
+
process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
|
|
1255
|
+
else
|
|
1256
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
830
1257
|
await rm(wd, { recursive: true, force: true });
|
|
831
1258
|
}
|
|
832
1259
|
});
|
|
@@ -989,6 +1416,7 @@ describe('notify-fallback watcher', () => {
|
|
|
989
1416
|
}, null, 2));
|
|
990
1417
|
await writeFile(statePath, JSON.stringify({
|
|
991
1418
|
ralph_continue_steer: {
|
|
1419
|
+
pane_id: '%7',
|
|
992
1420
|
last_sent_at: new Date(Date.now() - 61_000).toISOString(),
|
|
993
1421
|
},
|
|
994
1422
|
}, null, 2));
|
|
@@ -1002,6 +1430,7 @@ describe('notify-fallback watcher', () => {
|
|
|
1002
1430
|
assert.equal(missingRun.status, 0, missingRun.stderr || missingRun.stdout);
|
|
1003
1431
|
let watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
|
|
1004
1432
|
assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_missing');
|
|
1433
|
+
assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
|
|
1005
1434
|
await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
|
|
1006
1435
|
last_progress_at: 'not-a-date',
|
|
1007
1436
|
}, null, 2));
|
|
@@ -1009,6 +1438,7 @@ describe('notify-fallback watcher', () => {
|
|
|
1009
1438
|
assert.equal(invalidRun.status, 0, invalidRun.stderr || invalidRun.stdout);
|
|
1010
1439
|
watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
|
|
1011
1440
|
assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_invalid');
|
|
1441
|
+
assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
|
|
1012
1442
|
const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
|
|
1013
1443
|
const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[OMX_TMUX_INJECT\]/g) || [];
|
|
1014
1444
|
assert.equal(sends.length, 0, 'missing or invalid progress should fail closed without sending steer');
|
|
@@ -1260,7 +1690,9 @@ describe('notify-fallback watcher', () => {
|
|
|
1260
1690
|
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-control-plane-split-'));
|
|
1261
1691
|
const fakeBinDir = join(wd, 'fake-bin');
|
|
1262
1692
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
1693
|
+
const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
1263
1694
|
try {
|
|
1695
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
1264
1696
|
await mkdir(join(wd, '.omx', 'state'), { recursive: true });
|
|
1265
1697
|
await mkdir(fakeBinDir, { recursive: true });
|
|
1266
1698
|
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath, {
|
|
@@ -1313,6 +1745,10 @@ describe('notify-fallback watcher', () => {
|
|
|
1313
1745
|
assert.ok(ralphFailureEvent, 'expected Ralph failure to be logged without aborting team control-plane pumping');
|
|
1314
1746
|
}
|
|
1315
1747
|
finally {
|
|
1748
|
+
if (typeof previousRuntimeBridge === 'string')
|
|
1749
|
+
process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
|
|
1750
|
+
else
|
|
1751
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
1316
1752
|
await rm(wd, { recursive: true, force: true });
|
|
1317
1753
|
}
|
|
1318
1754
|
});
|
|
@@ -1322,7 +1758,9 @@ describe('notify-fallback watcher', () => {
|
|
|
1322
1758
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
1323
1759
|
const captureSeqFile = join(wd, 'capture-seq.txt');
|
|
1324
1760
|
const captureCounterFile = join(wd, 'capture-seq.idx');
|
|
1761
|
+
const previousRuntimeBridge = process.env.OMX_RUNTIME_BRIDGE;
|
|
1325
1762
|
try {
|
|
1763
|
+
process.env.OMX_RUNTIME_BRIDGE = '0';
|
|
1326
1764
|
await mkdir(fakeBinDir, { recursive: true });
|
|
1327
1765
|
await writeFile(join(fakeBinDir, 'tmux'), buildFakeTmux(tmuxLogPath));
|
|
1328
1766
|
await chmod(join(fakeBinDir, 'tmux'), 0o755);
|
|
@@ -1331,11 +1769,11 @@ describe('notify-fallback watcher', () => {
|
|
|
1331
1769
|
// (no trigger) so the request is retyped on every retry.
|
|
1332
1770
|
await writeFile(captureSeqFile, [
|
|
1333
1771
|
// Run 1 (attempt 0): 1 shared preflight + 3 verify rounds × 2 captures = 7
|
|
1334
|
-
'ready', '
|
|
1772
|
+
'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
|
|
1335
1773
|
// Run 2 (attempt 1): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
|
|
1336
|
-
'ready', 'ready', '
|
|
1774
|
+
'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
|
|
1337
1775
|
// Run 3 (attempt 2): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
|
|
1338
|
-
'ready', 'ready', '
|
|
1776
|
+
'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
|
|
1339
1777
|
].join('\n'));
|
|
1340
1778
|
await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
|
|
1341
1779
|
const queued = await enqueueDispatchRequest('dispatch-team', {
|
|
@@ -1343,7 +1781,7 @@ describe('notify-fallback watcher', () => {
|
|
|
1343
1781
|
to_worker: 'worker-1',
|
|
1344
1782
|
worker_index: 1,
|
|
1345
1783
|
pane_id: '%42',
|
|
1346
|
-
trigger_message: '
|
|
1784
|
+
trigger_message: 'ping',
|
|
1347
1785
|
}, wd);
|
|
1348
1786
|
const watcherScript = new URL('../../../dist/scripts/notify-fallback-watcher.js', import.meta.url).pathname;
|
|
1349
1787
|
const notifyHook = new URL('../../../dist/scripts/notify-hook.js', import.meta.url).pathname;
|
|
@@ -1358,13 +1796,17 @@ describe('notify-fallback watcher', () => {
|
|
|
1358
1796
|
assert.equal(run.status, 0, run.stderr || run.stdout);
|
|
1359
1797
|
}
|
|
1360
1798
|
const tmuxLog = await readFile(tmuxLogPath, 'utf8');
|
|
1361
|
-
const typeMatches = tmuxLog.match(/send-keys -t %42 -l
|
|
1362
|
-
assert.equal(typeMatches.length, 3, '
|
|
1799
|
+
const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
|
|
1800
|
+
assert.equal(typeMatches.length, 3, 'should retype on every retry when trigger not in narrow capture (fresh + 2 retries)');
|
|
1363
1801
|
const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
|
|
1364
1802
|
assert.equal(request?.status, 'failed');
|
|
1365
1803
|
assert.equal(request?.last_reason, 'unconfirmed_after_max_retries');
|
|
1366
1804
|
}
|
|
1367
1805
|
finally {
|
|
1806
|
+
if (typeof previousRuntimeBridge === 'string')
|
|
1807
|
+
process.env.OMX_RUNTIME_BRIDGE = previousRuntimeBridge;
|
|
1808
|
+
else
|
|
1809
|
+
delete process.env.OMX_RUNTIME_BRIDGE;
|
|
1368
1810
|
await rm(wd, { recursive: true, force: true });
|
|
1369
1811
|
}
|
|
1370
1812
|
});
|
|
@@ -1860,5 +2302,45 @@ describe('notify-fallback watcher', () => {
|
|
|
1860
2302
|
await rm(tempHome, { recursive: true, force: true });
|
|
1861
2303
|
}
|
|
1862
2304
|
});
|
|
2305
|
+
it('keeps the detached helper alive after the hidden bootstrap exits', async () => {
|
|
2306
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-fallback-bootstrap-survival-'));
|
|
2307
|
+
const readyPath = join(wd, 'helper-ready.json');
|
|
2308
|
+
const helperScriptPath = join(wd, 'helper-survival.js');
|
|
2309
|
+
try {
|
|
2310
|
+
await writeFile(helperScriptPath, `
|
|
2311
|
+
const fs = require('node:fs');
|
|
2312
|
+
const readyPath = process.argv[2];
|
|
2313
|
+
fs.writeFileSync(readyPath, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
|
|
2314
|
+
setInterval(() => {}, 1000);
|
|
2315
|
+
`);
|
|
2316
|
+
const bootstrap = spawnSync(process.execPath, [
|
|
2317
|
+
'-e',
|
|
2318
|
+
buildWindowsMsysBackgroundHelperBootstrapScript([helperScriptPath, readyPath], wd),
|
|
2319
|
+
], {
|
|
2320
|
+
cwd: wd,
|
|
2321
|
+
encoding: 'utf-8',
|
|
2322
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
2323
|
+
windowsHide: true,
|
|
2324
|
+
});
|
|
2325
|
+
assert.equal(bootstrap.status, 0, bootstrap.stderr || bootstrap.stdout);
|
|
2326
|
+
const helperPid = Number.parseInt((bootstrap.stdout || '').trim(), 10);
|
|
2327
|
+
assert.ok(Number.isFinite(helperPid) && helperPid > 0, 'expected detached helper pid from bootstrap');
|
|
2328
|
+
await waitFor(async () => {
|
|
2329
|
+
try {
|
|
2330
|
+
const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
|
|
2331
|
+
return ready.pid === helperPid;
|
|
2332
|
+
}
|
|
2333
|
+
catch {
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
2336
|
+
}, 4000, 50);
|
|
2337
|
+
assert.ok(isPidAlive(helperPid), 'expected detached helper to survive after bootstrap exit');
|
|
2338
|
+
process.kill(helperPid, 'SIGTERM');
|
|
2339
|
+
await waitFor(async () => !isPidAlive(helperPid), 4000, 50);
|
|
2340
|
+
}
|
|
2341
|
+
finally {
|
|
2342
|
+
await rm(wd, { recursive: true, force: true });
|
|
2343
|
+
}
|
|
2344
|
+
});
|
|
1863
2345
|
});
|
|
1864
2346
|
//# sourceMappingURL=notify-fallback-watcher.test.js.map
|