-
Notifications
You must be signed in to change notification settings - Fork 52
fix(omni): use --resume for respawn with JSONL-missing fallback #2489
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
Merged
namastex888
merged 12 commits into
automagik-dev:dev
from
rodriguess-caio:fix/omni-spawn-script-path
Jun 6, 2026
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3de24e6
chore(release): update manifests (dev) → v4.260523.1
3fc059c
chore(release): update manifests (dev) → v4.260523.2
461f357
chore(release): update manifests (dev) → v4.260523.3
9b9d69c
chore(release): update manifests (dev) → v4.260523.4
00548db
chore(release): update manifests (dev) → v4.260524.1
bf88efa
chore(release): update manifests (dev) → v4.260525.1
14c1cad
chore(release): update manifests (dev) → v4.260525.2
b76f1a1
fix: restore exec prefix in writeTmuxLaunchScript
rodriguess-caio 771e1c3
fix(omni): replace --resume with --session-id in tmux spawn scripts
rodriguess-caio 3087a64
Revert "fix(omni): replace --resume with --session-id in tmux spawn s…
rodriguess-caio eb9e60f
fix(omni): use --session-id instead of --resume for per-chat sessions
rodriguess-caio dc6177b
fix(omni): use --resume for respawn with JSONL-missing fallback
rodriguess-caio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,8 @@ | ||
| { | ||
| "schema_version": 1, | ||
| "channel": "dev", | ||
| "version": "4.260522.20", | ||
| "released_at": "2026-05-22T20:41:26Z", | ||
| "tarball_base": "https://github.com/automagik-dev/genie/releases/download/v4.260522.20", | ||
| "version": "4.260525.2", | ||
| "released_at": "2026-05-25T12:19:49Z", | ||
| "tarball_base": "https://github.com/automagik-dev/genie/releases/download/v4.260525.2", | ||
| "platforms": ["linux-x64-glibc", "linux-x64-musl", "linux-arm64", "darwin-arm64"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| #!/usr/bin/env bun | ||
| /** | ||
| * Smoke test for issue #2486 — Omni-Bridge spawn command corruption. | ||
| * | ||
| * Validates that the NEW script-based path (writeTmuxLaunchScript + source) | ||
| * works reliably with long commands containing backticks, emojis, parentheses, | ||
| * and nested quotes. | ||
| * | ||
| * Run with: | ||
| * bun run scripts/tests/omni-spawn-smoke.ts | ||
| */ | ||
|
|
||
| import { execSync } from 'node:child_process'; | ||
| import { readFileSync, unlinkSync } from 'node:fs'; | ||
| import { writeTmuxLaunchScript } from '../../src/lib/tmux-launch-script.js'; | ||
|
|
||
| const MARKER = 'OMNI_SPAWN_SUCCESS_2486'; | ||
| const RESULT_FILE = `/tmp/${MARKER}`; | ||
|
|
||
| // Build a deliberately nasty payload with all the problematic chars. | ||
| // We wrap everything in a single 'sh -c' so writeTmuxLaunchScript's | ||
| // leading 'exec' works correctly (the real Genie launches 'claude', | ||
| // a long-running process; here we use 'sleep' to keep the pane alive | ||
| // long enough for capture). | ||
| const nastyPayload = '👍 `backticks` (instance: alpha) (ALWAYS your last action)'; | ||
| const innerCommand = [ | ||
| `echo '${MARKER}'`, | ||
| `export TEST_VAR='${nastyPayload}'`, | ||
| `export JSON='{"emoji":"👍","nested":"(instance: alpha)"}'`, | ||
| `export LONG='${'A'.repeat(1800)}'`, | ||
| `touch ${RESULT_FILE}`, | ||
| 'sleep 1', | ||
| ].join(' && '); | ||
|
|
||
| // The full command that Genie would pass to writeTmuxLaunchScript | ||
| const nastyCommand = `sh -c '${innerCommand.replace(/'/g, "'\\''")}'`; | ||
|
|
||
| function killServer(socket: string) { | ||
| try { | ||
| execSync(`tmux -L ${socket} kill-server 2>/dev/null`, { stdio: 'ignore' }); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
|
|
||
| function cleanup(socket: string, scriptPath?: string) { | ||
| killServer(socket); | ||
| try { | ||
| unlinkSync(RESULT_FILE); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| if (scriptPath) { | ||
| try { | ||
| unlinkSync(scriptPath); | ||
| } catch { | ||
| // ignore | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function createPane(socket: string): string { | ||
| execSync(`tmux -L ${socket} new-session -d -e LC_ALL=C.UTF-8 -e LANG=C.UTF-8`, { stdio: 'ignore' }); | ||
| execSync('sleep 0.3'); | ||
| const paneId = execSync(`tmux -L ${socket} list-panes -F '#{pane_id}'`, { encoding: 'utf-8' }).trim(); | ||
| return paneId; | ||
| } | ||
|
|
||
| function capturePane(socket: string, paneId: string): string { | ||
| try { | ||
| return execSync(`tmux -L ${socket} capture-pane -p -t '${paneId}' -S -10`, { encoding: 'utf-8' }); | ||
| } catch { | ||
| return ''; | ||
| } | ||
| } | ||
|
|
||
| function waitForResult(socket: string, paneId: string, timeoutMs = 5000): boolean { | ||
| const start = Date.now(); | ||
| while (Date.now() - start < timeoutMs) { | ||
| const content = capturePane(socket, paneId); | ||
| if (content.includes(MARKER)) return true; | ||
| if (content.includes('parse error')) return false; | ||
| execSync('sleep 0.2'); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| console.log('=== Omni-Bridge Spawn Smoke Test (#2486) ===\n'); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // TEST 1: OLD inline path (replicates executeTmux -> send-keys without -l) | ||
| // --------------------------------------------------------------------------- | ||
| const socket1 = 'genie-smoke-old'; | ||
| console.log('TEST 1: Inline send-keys (old path)'); | ||
| cleanup(socket1); | ||
| const pane1 = createPane(socket1); | ||
|
|
||
| // Replicate the OLD Genie path exactly: | ||
| // executeTmux(`send-keys -t '${paneId}' ${shellQuote(cmd)} Enter`) | ||
| const quotedOld = `'${nastyCommand.replace(/'/g, "'\\''")}'`; | ||
| try { | ||
| execSync(`tmux -L ${socket1} send-keys -t '${pane1}' ${quotedOld} Enter`); | ||
| } catch (e) { | ||
| console.log(` tmux send-keys itself failed: ${e instanceof Error ? e.message : e}`); | ||
| } | ||
|
|
||
| const oldOk = waitForResult(socket1, pane1); | ||
| const oldContent = capturePane(socket1, pane1); | ||
| const oldHasParseError = oldContent.includes('parse error'); | ||
| console.log(` Parse error visible: ${oldHasParseError}`); | ||
| console.log(` Marker detected: ${oldOk}`); | ||
| console.log(` Result: ${oldOk ? 'PASS' : 'FAIL / FLAKY'}\n`); | ||
|
|
||
| killServer(socket1); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // TEST 2: NEW script-based path | ||
| // --------------------------------------------------------------------------- | ||
| const socket2 = 'genie-smoke-new'; | ||
| console.log('TEST 2: Script-based send-keys (writeTmuxLaunchScript + source)'); | ||
| cleanup(socket2); | ||
| const pane2 = createPane(socket2); | ||
|
|
||
| const scriptPath = writeTmuxLaunchScript('smoke-2486', nastyCommand); | ||
| // Replicate the NEW Genie path: | ||
| // executeTmux(`send-keys -t '${paneId}' "source ${scriptPath}" Enter`) | ||
| execSync(`tmux -L ${socket2} send-keys -t '${pane2}' "source ${scriptPath}" Enter`); | ||
|
|
||
| const newOk = waitForResult(socket2, pane2); | ||
| const newContent = capturePane(socket2, pane2); | ||
| const newHasParseError = newContent.includes('parse error'); | ||
| let markerOnDisk = false; | ||
| try { | ||
| markerOnDisk = readFileSync(RESULT_FILE).toString().trim() === ''; | ||
| } catch { | ||
| markerOnDisk = false; | ||
| } | ||
| console.log(` Parse error visible: ${newHasParseError}`); | ||
| console.log(` Marker in pane: ${newContent.includes(MARKER)}`); | ||
| console.log(` Marker file created: ${markerOnDisk}`); | ||
| console.log(` Result: ${newOk ? 'PASS' : 'FAIL'}\n`); | ||
|
|
||
| cleanup(socket2, scriptPath); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Summary | ||
| // --------------------------------------------------------------------------- | ||
| console.log('=== Summary ==='); | ||
| if (!oldOk && newOk) { | ||
| console.log('✅ Fix validated: inline path fails, script path is stable.'); | ||
| process.exit(0); | ||
| } else if (oldOk && newOk) { | ||
| console.log('⚠️ Both paths passed on this machine (tmux/zsh may be tolerant for this payload).'); | ||
| console.log(' The script path is still the safer architectural choice for 1968+ char payloads.'); | ||
| process.exit(0); | ||
| } else if (!newOk) { | ||
| console.log('❌ New script path failed — investigate.'); | ||
| process.exit(1); | ||
| } else { | ||
| console.log('❓ Unexpected state — review output above.'); | ||
| process.exit(1); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| /** | ||
| * Unit tests for tmux-launch-script.ts | ||
| */ | ||
|
|
||
| import { beforeEach, describe, expect, test } from 'bun:test'; | ||
| import { existsSync, readFileSync, rmSync } from 'node:fs'; | ||
| import { homedir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { writeTmuxLaunchScript } from '../tmux-launch-script.js'; | ||
|
|
||
| const SPAWN_DIR = join(homedir(), '.genie', 'spawn-scripts'); | ||
|
|
||
| describe('writeTmuxLaunchScript', () => { | ||
| beforeEach(() => { | ||
| // Clean up any leftover test scripts | ||
| try { | ||
| const files = require('node:fs').readdirSync(SPAWN_DIR); | ||
| for (const f of files) { | ||
| if (f.startsWith('test-') || f.startsWith('omni-')) { | ||
| rmSync(join(SPAWN_DIR, f)); | ||
| } | ||
| } | ||
| } catch { | ||
| // dir may not exist yet | ||
| } | ||
| }); | ||
|
|
||
| test('creates a script with shebang and command', () => { | ||
| const path = writeTmuxLaunchScript('test-worker', 'echo hello'); | ||
| expect(existsSync(path)).toBe(true); | ||
|
|
||
| const content = readFileSync(path, 'utf-8'); | ||
| expect(content).toStartWith('#!/bin/sh\n'); | ||
| expect(content).toInclude('echo hello\n'); | ||
| }); | ||
|
|
||
| test('sanitizes workerId in filename', () => { | ||
| const path = writeTmuxLaunchScript('worker/with:bad@chars', 'echo hello'); | ||
| const basename = path.split('/').pop()!; | ||
| expect(basename).toMatch(/^worker-with-bad-chars-/); | ||
| expect(basename).toEndWith('.sh'); | ||
| }); | ||
|
|
||
| test('creates script in ~/.genie/spawn-scripts', () => { | ||
| const path = writeTmuxLaunchScript('test-worker', 'echo hello'); | ||
| expect(path).toStartWith(SPAWN_DIR); | ||
| }); | ||
|
|
||
| test('sets executable permissions', () => { | ||
| const path = writeTmuxLaunchScript('test-worker', 'echo hello'); | ||
| const stats = require('node:fs').statSync(path); | ||
| // Check owner-execute bit | ||
| expect(stats.mode & 0o100).toBe(0o100); | ||
| }); | ||
|
|
||
| test('preserves complex commands with quotes and backticks', () => { | ||
| const cmd = `OMNI_API_KEY='sk-123' claude --permission-mode 'auto' --system-prompt 'Use \`git\` (👍) for (instance: x)'`; | ||
| const path = writeTmuxLaunchScript('omni-chat-123', cmd); | ||
| const content = readFileSync(path, 'utf-8'); | ||
| expect(content).toInclude(`${cmd}\n`); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * Tmux Launch Script — Write a temporary shell script for complex tmux spawns. | ||
| * | ||
| * Long commands with nested quotes, backticks, emojis, and JSON escapes corrupt | ||
| * when passed through `tmux send-keys`. Writing the command to a script file and | ||
| * sourcing it from the pane removes the escaping surface and keeps the launch | ||
| * stable. | ||
| */ | ||
|
|
||
| import { chmodSync, mkdirSync, writeFileSync } from 'node:fs'; | ||
| import { homedir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
|
|
||
| /** | ||
| * Write a temporary launch script for complex tmux spawns. | ||
| * | ||
| * @param workerId — identifier used in the filename (e.g. agent or chat id) | ||
| * @param fullCommand — the complete shell command to execute | ||
| * @returns absolute path to the written script | ||
| */ | ||
| export function writeTmuxLaunchScript(workerId: string, fullCommand: string): string { | ||
| const dir = join(homedir(), '.genie', 'spawn-scripts'); | ||
| mkdirSync(dir, { recursive: true }); | ||
| const safeId = workerId.replace(/[^a-zA-Z0-9._-]/g, '-'); | ||
| const scriptPath = join(dir, `${safeId}-${Date.now().toString(36)}.sh`); | ||
| writeFileSync(scriptPath, `#!/bin/sh\n${fullCommand}\n`, { mode: 0o700 }); | ||
| chmodSync(scriptPath, 0o700); | ||
| return scriptPath; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
If the test is run on a machine where the home directory path contains spaces, the unquoted
scriptPathwill cause thesourcecommand to fail in the tmux pane. Wrapping it in single quotes ensures the path is handled correctly.