Skip to content

fix(omni-bridge): harden session lifecycle + liveness check + collision-resistant window names - #1051

Merged
namastex888 merged 4 commits into
devfrom
omni-fix
Apr 5, 2026
Merged

namastex888 merged 4 commits into
devfrom
omni-fix

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

  • Harden omni-bridge session lifecycle — no silent drops, proper cleanup, concurrency guard
  • Fix chat ID collision in sanitizeWindowName via MD5 hash suffix (prevents window collisions for JIDs that sanitize to the same prefix)
  • Fix liveness check to detect crashed Claude processes via isPaneProcessRunning (walks pane pid tree 2 levels deep)
  • Fix JSON escaping in omni-reply script (jq preferred, bash fallback handles all JSON-special chars)
  • Add tests for sanitizeWindowName covering collision resistance, truncation, and edge cases

Changes

  • `src/services/omni-bridge.ts` — session lifecycle hardening
  • `src/services/executors/claude-code.ts` — hash-suffixed window names, export for testing
  • `src/lib/tmux.ts` — new `isPaneProcessRunning` helper
  • `src/services/executors/claude-code.test.ts` — new tests (7 cases)

Test plan

  • `bun run typecheck` passes clean
  • `bun test src/services/executors/claude-code.test.ts` — 7/7 pass
  • `bun test src/lib/tmux.test.ts` passes
  • Lint — 18 pre-existing warnings, none introduced by this PR

Test added 3 commits April 4, 2026 19:14
…cleanup, concurrency guard

- Log warning + send buffer-full reply when MAX_BUFFER_PER_CHAT exceeded (was silent drop)
- Shut down all active executor sessions in stop() before clearing (was resource leak)
- Count spawning entries in concurrency limit to prevent oversubscription
- Re-queue buffered messages on spawn failure instead of losing them

Closes #1035
The function is only used within the module — no need to export it.
Fixes dead-code check (knip) warning.
…roup 1 dependencies

- Re-export sanitizeWindowName (needed by test file)
- Add isPaneProcessRunning to tmux.ts (used by claude-code executor liveness check)
- Include claude-code.test.ts for sanitizeWindowName collision tests
@coderabbitai

coderabbitai Bot commented Apr 4, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6dc45abf-eaf1-4201-bc28-4f0cd0a3ac12

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch omni-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several improvements to the session management and process monitoring in the omni-bridge service. Key changes include the addition of a process-checking utility for tmux panes, an updated window name sanitization function with collision prevention, and enhanced session lifecycle management, including concurrent shutdown handling and buffer overflow protection. I have provided feedback regarding the use of synchronous process execution and suggested using Promise.all for more efficient session shutdowns.

Comment thread src/lib/tmux.ts
Comment on lines +584 to +590
const { execSync } = await import('node:child_process');
// Check direct children and grandchildren for the target process name
const output = execSync(
`pgrep -la -P ${panePid} 2>/dev/null; for cpid in $(pgrep -P ${panePid} 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true`,
{ encoding: 'utf-8', timeout: 5000 },
);
return output.toLowerCase().includes(processName.toLowerCase());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using execSync inside an asynchronous function blocks the Node.js event loop. This can significantly degrade the performance and responsiveness of the bridge, especially when managing multiple concurrent sessions or during the periodic liveness checks. It is highly recommended to use an asynchronous alternative like child_process.exec wrapped in a Promise. This implementation utilizes dynamic imports, which aligns with the repository guideline to use dynamic imports to prevent circular dependency cycles.

    const { exec } = await import('node:child_process');
    const { promisify } = await import('node:util');
    const execAsync = promisify(exec);
    // Check direct children and grandchildren for the target process name
    const { stdout } = await execAsync(
      'pgrep -la -P ' + panePid + ' 2>/dev/null; for cpid in $(pgrep -P ' + panePid + ' 2>/dev/null); do pgrep -la -P "$cpid" 2>/dev/null; done; true',
      { encoding: 'utf-8', timeout: 5000 },
    );
    return stdout.toLowerCase().includes(processName.toLowerCase());
References
  1. Use dynamic imports (require() or import()) to break circular dependency cycles that would otherwise occur at module load time.

Comment on lines +173 to 182
for (const [key, entry] of this.sessions) {
if (entry.idleTimer) clearTimeout(entry.idleTimer);
if (!entry.spawning && entry.session) {
try {
await this.executor.shutdown(entry.session);
} catch (err) {
console.warn(`[omni-bridge] Error shutting down session ${key}:`, err);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Shutting down sessions sequentially using await inside a loop can be inefficient and slow down the bridge's shutdown process, especially with a high number of active sessions. Using Promise.all to perform these shutdowns concurrently would be much more efficient.

Suggested change
for (const [key, entry] of this.sessions) {
if (entry.idleTimer) clearTimeout(entry.idleTimer);
if (!entry.spawning && entry.session) {
try {
await this.executor.shutdown(entry.session);
} catch (err) {
console.warn(`[omni-bridge] Error shutting down session ${key}:`, err);
}
}
}
const shutdownPromises: Promise<void>[] = [];
for (const [key, entry] of this.sessions) {
if (entry.idleTimer) clearTimeout(entry.idleTimer);
if (!entry.spawning && entry.session) {
shutdownPromises.push(
this.executor.shutdown(entry.session).catch((err) => {
console.warn('[omni-bridge] Error shutting down session ' + key + ':', err);
}),
);
}
}
await Promise.all(shutdownPromises);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7414d1c608

ℹ️ 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".

console.warn(
`[omni-bridge] Re-queuing ${lostMessages.length} buffered message(s) from failed spawn for ${key}`,
);
this.messageQueue.push(...lostMessages);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Drain requeued messages after spawn failure

When spawnSession fails, buffered messages are moved to messageQueue, but no retry path is triggered here. In this file, drainQueue() is only invoked from the idle-timeout callback in resetIdleTimer, so a failure during initial spawn (for example, transient tmux startup issues) can leave these requeued messages stuck indefinitely with no active timer event to flush them. This creates message loss-by-stall and unbounded queue growth under repeated spawn failures.

Useful? React with 👍 / 👎.

if (entry.idleTimer) clearTimeout(entry.idleTimer);
if (!entry.spawning && entry.session) {
try {
await this.executor.shutdown(entry.session);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unsubscribe before shutting down sessions

stop() now awaits executor.shutdown() for existing sessions while the NATS subscription is still active, and only unsubscribes afterward. During that wait, processSubscription() can still route new incoming messages and create new sessions; those entries are then dropped by this.sessions.clear() without a corresponding shutdown call, which can leave tmux windows/processes running after stop under live traffic.

Useful? React with 👍 / 👎.

@gitguardian

gitguardian Bot commented Apr 5, 2026 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 2 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
18760271 Triggered Generic Password a506587 src/lib/test-setup.ts View secret
18760271 Triggered Generic Password a506587 src/lib/test-setup.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

Tests now run against a dedicated pgserve spawned in --ram mode (native
postgres with data in /dev/shm) on port 20900+, isolated from the
production daemon at port 19642 and from ~/.genie/data/pgserve.

Eliminates PG deadlock flakiness in team.test.ts that was blocking the
pre-push hook. Root cause: tests and the live genie serve daemon shared
one PostgreSQL database, and their asymmetric catalog-lock orderings
tripped PG's deadlock detector under concurrent load. Full analysis in
.genie/wishes/test-pg-ram-isolation/TRACE.md.

Changes:
- bunfig.toml: [test] preload = ["./src/lib/test-setup.ts"]
- src/lib/test-setup.ts: spawns pgserve --ram on 20900-20999, reaps
  orphaned wrappers from previous runs at startup (bun test hard-exits
  without firing exit hooks).
- src/lib/db.ts: ensurePgserve() honors GENIE_TEST_PG_PORT, production
  paths unchanged when unset.
- src/lib/test-setup.test.ts: proves preload wiring and port binding.

Validation:
- 5 consecutive full-suite runs: 2052 pass, 0 fail, ~51s each.
- bun run check passes on first attempt in 61s.
- Orphan reaper keeps steady state of 1 pgserve between runs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant