Skip to content

feat: inbox-driven session management — watcher, routing headers, daemon integration (#574) - #771

Merged
namastex888 merged 5 commits into
devfrom
inbox-sessions
Mar 25, 2026
Merged

namastex888 merged 5 commits into
devfrom
inbox-sessions

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Restores and extends the inbox-driven session management system:

  1. Inbox watcher — restored from 040b667 (PR feat: team-lead liveness checks, inbox watcher daemon, CI coverage gate #645). Polls native inboxes every 30s, auto-spawns offline team-leads for teams with unread messages. DI pattern for testability, backoff after 3 failures.
  2. Routing header parser — new routing-header.ts. Parses [channel:telegram instance:xxx chat:123 ...] headers. Resolves deterministic session keys per sender/chat/thread.
  3. Daemon integration — inbox watcher starts/stops with genie daemon start/stop. genie inbox watch for foreground mode.
  4. listTeamsWithUnreadInbox — restored in claude-native-teams.ts.

Test plan

  • All tests pass
  • genie inbox watch polls and logs activity
  • genie daemon start starts both scheduler + inbox watcher

Fixes #574

Genie added 5 commits March 24, 2026 21:19
Parse bracket-delimited routing headers from Omni-relayed messages
and resolve deterministic session keys for per-user/per-chat routing.
Implements Group 2 of inbox-driven-sessions wish (#574).
- Re-apply src/lib/inbox-watcher.ts (143 lines) with DI pattern and backoff logic
- Re-apply src/lib/inbox-watcher.test.ts (184 lines, 8 tests)
- Add listTeamsWithUnreadInbox() to claude-native-teams.ts (scans native inboxes)
- Export isTeamActive from team-auto-spawn.ts for inbox-watcher consumption

Cherry-picked from commit 040b667, adapted to current API signatures.
Ref: #574
- Add listTeamsWithUnreadInbox() to claude-native-teams.ts for inbox scanning
- Export isTeamActive from team-auto-spawn.ts for inbox-watcher consumption
Exports will be consumed by daemon wiring (Group 3).
- Import and start/stop inbox watcher in scheduler-daemon alongside
  existing scheduler, heartbeat, and orphan timers
- Respect GENIE_INBOX_POLL_MS=0 to disable inbox watching in daemon
- Add inbox watcher state to daemon status output
- Convert genie inbox to command group with list (default) and watch
- genie inbox watch runs inbox watcher in foreground with Ctrl+C
- Integrate routing header parser into inbox watcher for session key
  resolution (falls back to team name when no header present)
- Add firstUnreadText to listTeamsWithUnreadInbox return type
- Add tests for routing header integration and fallback behavior
@coderabbitai

coderabbitai Bot commented Mar 25, 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: 7c04b4df-910e-4cb9-bc6a-41761d2c79fd

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 inbox-sessions

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

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the system's inbox-driven session management capabilities. It re-establishes the automatic spawning of team-leads when new messages arrive in their inboxes, ensuring timely responses. A new, robust mechanism for parsing message routing headers is introduced, which allows for consistent and deterministic session key resolution. These features are seamlessly integrated into the daemon, providing a more autonomous and efficient operational flow for managing team interactions.

Highlights

  • Inbox Watcher: Restored and extended the inbox watcher, which polls native inboxes every 30 seconds and automatically spawns offline team-leads for teams with unread messages. It includes a dependency injection pattern for testability and implements backoff after 3 failures.
  • Routing Header Parser: Introduced a new routing-header.ts module to parse structured routing headers (e.g., [channel:telegram instance:xxx chat:123 ...]) and resolve deterministic session keys per sender/chat/thread.
  • Daemon Integration: Integrated the inbox watcher with the daemon, allowing it to start and stop automatically with genie daemon start/stop. A new genie inbox watch command is also available for foreground operation.
  • listTeamsWithUnreadInbox Function: Restored the listTeamsWithUnreadInbox function in claude-native-teams.ts to identify teams with unread messages in their team-lead inbox.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@namastex888
namastex888 merged commit b9db38b into dev Mar 25, 2026
6 checks passed
@namastex888
namastex888 deleted the inbox-sessions branch March 25, 2026 00:38

@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 an "Inbox Watcher" feature, designed to automatically spawn team-leads for inactive teams that have unread messages in their inboxes. Key changes include new modules for listing teams with unread messages (claude-native-teams.ts), the core inbox watcher logic (inbox-watcher.ts), and a utility for parsing routing headers (routing-header.ts), all accompanied by dedicated test files. The main scheduler daemon (scheduler-daemon.ts) has been updated to integrate and manage the lifecycle of this new watcher. Additionally, new CLI commands (genie inbox list and genie inbox watch) have been added to facilitate interaction and monitoring of the inbox watcher. Feedback suggests improving the performance of team inbox scanning by processing teams in parallel, simplifying the stripRoutingHeader function, and optimizing dependency imports and logging consistency within the inbox watch command.

Comment on lines +394 to +439
const results: Array<{
teamName: string;
unreadCount: number;
workingDir: string | null;
firstUnreadText: string | null;
}> = [];

for (const name of teamDirs) {
// Read inbox messages
const inboxFile = join(base, name, 'inboxes', 'team-lead.json');
let messages: NativeInboxMessage[];
try {
const content = await readFile(inboxFile, 'utf-8');
messages = JSON.parse(content);
} catch {
continue; // No inbox or invalid JSON
}

if (!Array.isArray(messages)) continue;

const unread = messages.filter((m) => m.read === false);
if (unread.length === 0) continue;

// Get workingDir from config.json → members → team-lead → cwd
let workingDir: string | null = null;
try {
const cfgContent = await readFile(join(base, name, 'config.json'), 'utf-8');
const config: NativeTeamConfig = JSON.parse(cfgContent);
const leadMember = config.members.find((m) => m.name === 'team-lead' || m.agentId.startsWith('team-lead@'));
if (leadMember?.cwd) {
workingDir = leadMember.cwd;
}
} catch {
// Config missing or malformed — workingDir stays null
}

results.push({
teamName: name,
unreadCount: unread.length,
workingDir,
firstUnreadText: unread[0]?.text ?? null,
});
}

return results;
}

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

The current implementation processes each team directory sequentially using a for...of loop with await inside. This can be inefficient if there are many teams, as each team's file I/O operations will block the processing of the next. To improve performance, you can process teams in parallel using Promise.all.

  const teamPromises = teamDirs.map(async (name) => {
    // Read inbox messages
    const inboxFile = join(base, name, 'inboxes', 'team-lead.json');
    let messages: NativeInboxMessage[];
    try {
      const content = await readFile(inboxFile, 'utf-8');
      messages = JSON.parse(content);
    } catch {
      return null; // No inbox or invalid JSON
    }

    if (!Array.isArray(messages)) return null;

    const unread = messages.filter((m) => m.read === false);
    if (unread.length === 0) return null;

    // Get workingDir from config.json → members → team-lead → cwd
    let workingDir: string | null = null;
    try {
      const cfgContent = await readFile(join(base, name, 'config.json'), 'utf-8');
      const config: NativeTeamConfig = JSON.parse(cfgContent);
      const leadMember = config.members.find((m) => m.name === 'team-lead' || m.agentId.startsWith('team-lead@'));
      if (leadMember?.cwd) {
        workingDir = leadMember.cwd;
      }
    } catch {
      // Config missing or malformed — workingDir stays null
    }

    return {
      teamName: name,
      unreadCount: unread.length,
      workingDir,
      firstUnreadText: unread[0]?.text ?? null,
    };
  });

  const resultsWithNulls = await Promise.all(teamPromises);
  return resultsWithNulls.filter((r): r is NonNullable<typeof r> => r !== null);
}

Comment thread src/lib/routing-header.ts
Comment on lines +117 to +133
export function stripRoutingHeader(text: string): string {
if (!text) return text;

const firstNewline = text.indexOf('\n');
if (firstNewline === -1) {
// Single line — if it's a header, body is empty
const firstLine = text.trim();
return firstLine.match(/^\[(.+)\]$/) ? '' : text;
}

const firstLine = text.slice(0, firstNewline).trim();
if (firstLine.match(/^\[(.+)\]$/)) {
return text.slice(firstNewline + 1);
}

return text;
}

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

The logic in stripRoutingHeader is a bit repetitive. You can simplify it by first identifying the first line of the text and then checking if it's a routing header, which consolidates the logic for single-line and multi-line messages.

export function stripRoutingHeader(text: string): string {
  if (!text) return text;

  const firstNewline = text.indexOf('\n');
  const firstLine = (firstNewline === -1 ? text : text.slice(0, firstNewline)).trim();

  if (firstLine.match(/^\[(.+)\]$/)) {
    return firstNewline === -1 ? '' : text.slice(firstNewline + 1);
  }

  return text;
}

Comment thread src/term-commands/msg.ts
Comment on lines +529 to +562
const { checkInboxes, getInboxPollIntervalMs, startInboxWatcher, stopInboxWatcher } = await import(
'../lib/inbox-watcher.js'
);

const pollMs = getInboxPollIntervalMs();
if (pollMs === 0) {
console.log('Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)');
process.exit(0);
}

console.log(`Inbox watcher starting (poll every ${pollMs / 1000}s)`);
console.log('Press Ctrl+C to stop.\n');

// Run an initial check immediately
const initial = await checkInboxes();
if (initial.length > 0) {
console.log(`[inbox-watcher] Spawned team-leads for: ${initial.join(', ')}`);
}

// Start the polling loop with visible logging
const handle = startInboxWatcher({
listTeamsWithUnreadInbox: (await import('../lib/claude-native-teams.js')).listTeamsWithUnreadInbox,
isTeamActive: async (teamName) => {
const { isTeamActive } = await import('../lib/team-auto-spawn.js');
return isTeamActive(teamName);
},
ensureTeamLead: async (teamName, workingDir) => {
const { ensureTeamLead } = await import('../lib/team-auto-spawn.js');
const result = await ensureTeamLead(teamName, workingDir);
console.log(`[inbox-watcher] Spawned team-lead for "${teamName}" in ${workingDir}`);
return result;
},
warn: (msg) => console.log(msg),
});

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

The inbox watch command can be improved in a few ways:

  1. Dependency Imports: The dynamic import() calls inside the dependency functions (isTeamActive, ensureTeamLead) are inefficient as they re-import the module on every invocation. It's better to import them once at the top of the action handler.
  2. Logging Consistency: The initial checkInboxes() call uses default dependencies, while the startInboxWatcher uses custom ones with different logging behavior. This leads to inconsistent output. You should define a single deps object with console logging and use it for both the initial check and the watcher loop.

This refactoring will make the code cleaner, more efficient, and ensure consistent logging.

      const { checkInboxes, getInboxPollIntervalMs, startInboxWatcher, stopInboxWatcher } = await import(
        '../lib/inbox-watcher.js'
      );
      const { listTeamsWithUnreadInbox } = await import('../lib/claude-native-teams.js');
      const autoSpawn = await import('../lib/team-auto-spawn.js');

      const pollMs = getInboxPollIntervalMs();
      if (pollMs === 0) {
        console.log('Inbox watcher is disabled (GENIE_INBOX_POLL_MS=0)');
        process.exit(0);
      }

      console.log(`Inbox watcher starting (poll every ${pollMs / 1000}s)`);
      console.log('Press Ctrl+C to stop.\n');

      // Define dependencies for consistent logging
      const deps = {
        listTeamsWithUnreadInbox,
        isTeamActive: autoSpawn.isTeamActive,
        ensureTeamLead: async (teamName: string, workingDir: string) => {
          const result = await autoSpawn.ensureTeamLead(teamName, workingDir);
          console.log(`[inbox-watcher] Spawned team-lead for "${teamName}" in ${workingDir}`);
          return result;
        },
        warn: (msg: string) => console.log(msg),
      };

      // Run an initial check immediately
      await checkInboxes(deps);

      // Start the polling loop
      const handle = startInboxWatcher(deps);

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