Skip to main content

Listener Development

This guide covers listener-based stream integrations for command modules. The key design rule is separation of concerns: the runtime owns transport lifecycle and safety, while command modules own site-specific payload parsing and stream shaping.

Core invariant​

Keep runtime listener infrastructure generic and site-agnostic. Keep command adapter logic site-specific.

Before you start​

  • Familiarity with Command Authoring and the test(ctx, input, helpers) hook.
  • Understanding of the target site's network request patterns (URLs, MIME types, host).

Listener lifecycle​

listener.subscribe and listener.unsubscribe are terminal commands that return normal result or error envelopes. Stream data is emitted asynchronously as event frames with payload.type=listener_update, correlated by the original subscribe requestId.

network.http_intercept options​

OptionRequiredDescription
tabSessionIdYesManaged session target
siteYesSite scope for the listener
modeNonetwork | fetch | hybrid (default: network)
urlPatternsNoURL glob patterns to capture
requestHostAllowlistNoAllowlist of request hosts
includeBodyNoInclude response body in updates
includeHeadersNoInclude redacted request/response headers
maxBodyBytesNoMax response body bytes per update (default: 256000)
mimeTypesNoMIME type prefix allowlist
streamAdapterNoAdapter hint for command-side parsing
tip

Use --mode hybrid when the target site may use either fetch or XMLHttpRequest for the same API. Hybrid mode captures from both surfaces with transport-level deduplication.

Stream adapter guidance​

Adapter modules map raw transport payloads into shared-domain objects (for example chat.message, content.post):

  • Map raw payloads to shared domain types.
  • Attach originalEntity only when it is safe and operationally useful.
  • Keep objects compact to avoid cross-context serialization pressure during sustained streams.
  • Use streamAdapter hint in subscribe options to identify the adapter to use.

Deduplication​

Duplicate suppression is intentionally layered:

  1. Transport deduplication — runtime suppresses equivalent hybrid cross-surface response duplicates (same response captured via both fetch and network).
  2. Adapter deduplication — command adapters suppress semantic duplicates from replayed site payloads.

Do not skip either layer: transport duplicates and semantic duplicates are distinct failure modes.

Fallback strategy​

If a bounded stream probe cannot confirm traffic, return fallback metadata and use execute fallback helpers rather than leaving stream state ambiguous:

// In test() hook: probe failed
const bufferedResult = await helpers.execute(input);
return {
ready: false,
fallback: { strategy: 'command_poll', reason: 'intercept_probe_unavailable' },
bufferedResult
};

Verify success​

Use otto listener subscribe-network to validate raw network capture before debugging command stream plumbing:

otto listener subscribe-network \
--tab-session <tabSessionId> \
--site example.com \
--pattern 'https://api.example.com/events*' \
--mode network \
--max-body-bytes 200000

Expected output: streamed JSON listener_update events from the target API.

Next steps​