Skip to content

listen: glob patterns for source names, and sessions that pick up newly matching sources #467

Description

@leggetter

Problem

  1. Patterns aren't supported. hookdeck listen 4000 'deepgram-*' fails with a raw API error:

    error: name with value deepgram-* fails to match the required pattern: /^[A-z0-9-_]+$/
    

    Only the literal * is special (pkg/listen/source.go#L33). Any other token is sent as GET /sources?name=<token>.

  2. A running listen never picks up sources added after it starts, even with '*'. A glob like 'deepgram-*' suggests you'll get events from any matching source that's connected to the CLI, including ones created later. Today you don't: the source list is resolved once, at startup.

Current behavior

Observed with the CLI at 98cc142 and API version 2026-09-01.

  • Resolved once: listen resolves its source argument once at startup. * lists all sources at that moment, and each name is looked up with GET /sources?name=<name>.

  • Session holds connection IDs only: it creates its session with:

    POST /cli-sessions
    { "webhook_ids": ["web_a", "web_b"], "filters": { ... } }

    That's the connection IDs only, with no record of what was asked for ('*', names or a pattern).

  • Same list on every reconnect: each websocket connect and reconnect sends X-Webhook-Ids: web_a,web_b.

  • New connections aren't routed: a request to a source whose CLI connection isn't in the session's list isn't delivered to the running listen. The request is stored and verified, but gets an ignored event with cause CLI_DISCONNECTED (GET /requests/{id}/ignored_events). That happens even if the new connection uses the same CLI destination as one the session already serves, and even if listen was started with '*'.

To reproduce:

  1. Create source A and a connection from A to a CLI destination D.
  2. Run hookdeck listen 4000 A.
  3. Create source B and a connection from B to D (or to a new CLI destination E).
  4. Send a request to B. Nothing reaches listen, and the request is ignored as CLI_DISCONNECTED.
  5. Restart listen (with A,B or '*'), and requests to B are delivered.

Use case

The MCP Events bridge gives local agents a public MCP Events webhook endpoint through Event Gateway and the CLI:

  • Setup: each subscription gets its own MCP_EVENTS source, because the spec makes the webhook secret per subscription and a source holds one secret. Each source has a connection to the agent's CLI destination.

  • The goal: an agent adds subscriptions while running, and we'd like hookdeck listen 4300 'agent-laptop-*' to keep receiving events for new ones.

  • Workaround today: restart listen whenever a source is added, then replay the requests that arrived during the restart:

    POST /requests/{id}/retry
    { "webhook_ids": ["<connection id>"] }

Proposal

1. Glob patterns for source names

Support glob patterns, e.g. listen 4000 'deepgram-*' or 'deepgram-*,stripe'.

  • Why:
    • Projects often group sources by a prefix per service or demo (deepgram-tts, deepgram-stt).
    • A pattern avoids listing every name and the 10-name limit (pkg/listen/listen.go#L258).
  • How: GET /sources supports name[contains], a case-insensitive substring match. Send the longest literal run as name[contains], then match client-side with path.Match.
  • Behavior:
    • Treat a pattern as multi-source mode, so --path is disallowed.
    • Error if nothing matches.
    • Never auto-create a source from a pattern.

2. Sessions follow newly matching sources

Make the selector part of the session, so new connections that match it are added while listen runs.

  • Session creation carries the selector. The raw source argument and any connection filter are sent alongside the resolved IDs:

    POST /cli-sessions
    {
      "webhook_ids": ["web_a", "web_b"],
      "filters": { ... },
      "selector": { "sources": ["deepgram-*", "stripe"], "connection": null }
    }
  • Matching connections join live sessions. When a connection with a CLI destination is created or updated (POST/PUT /connections), and its source name matches the selector of an active session in the same project:

    • The server adds the connection to that session before responding, so a request sent straight afterwards isn't ignored as CLI_DISCONNECTED.

    • The server pushes a websocket message to the session:

      {
        "event": "session_updated",
        "body": {
          "added": [{ "webhook_id": "web_c", "source": { "id": "src_c", "name": "deepgram-tts" }, "cli_path": "/webhooks" }],
          "removed": []
        }
      }
    • removed covers connections that are deleted, disabled or changed so they no longer match.

  • The CLI applies the update:

    • It adds the connection to the list it sends as X-Webhook-Ids on reconnect, so the addition survives reconnects and session expiry.
    • It prints e.g. Now also listening on deepgram-tts → http://localhost:4000/webhooks.
  • Scope: only matching sessions.

    • The selector is the opt-in: '*' matches every source, a pattern matches its sources, and an explicit name matches only that source.
    • Other sessions in the project, such as a teammate's listen on different sources, are unaffected.
  • Compatibility with older CLIs:

    • They log unknown websocket message types at debug level and carry on.
    • They forward any attempt they receive.
    • They resend their original X-Webhook-Ids on reconnect, so the server should add that list to the session's, not replace it.

Smaller fix, worth doing regardless

Validate source names client-side against ^[A-Za-z0-9_-]+$ and print a helpful error, e.g. Invalid source name "deepgram-*": use letters, digits, - or _ (or '*' for all sources), instead of the raw 422. This also covers typos such as src.a.

Related: #305

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions