feat: add channel — event subscription protocol#422
Open
4ier wants to merge 7 commits intojackwener:mainfrom
Open
feat: add channel — event subscription protocol#4224ier wants to merge 7 commits intojackwener:mainfrom
4ier wants to merge 7 commits intojackwener:mainfrom
Conversation
Implements opencli channel with consumer-side subscription model:
- Sources poll platforms (GitHub via gh api), track cursors, dedup events
- Consumers subscribe to origins they care about
- Daemon delivers events to subscribers via sinks (stdout, webhook)
CLI: opencli channel {sources,subscribe,unsubscribe,subscriptions,start,stop,status,poll}
GitHub source: repo events, issue/PR comments, pulls, issues
Docs: docs/channel.md with architecture, usage, extension guide
Ref: jackwener#369
When multiple origins poll simultaneously, concurrent save() calls could race on the temp file. Add a simple promise chain to serialize writes, and use PID-stamped tmp filenames for extra safety.
- CRITICAL: Replace execSync with execFileSync to prevent command injection via malicious origin strings (e.g. $(whoami) in owner/repo) - HIGH: Fix cursor-store mutex — use promise chain instead of broken check-then-act pattern; use unique tmp filenames with PID+timestamp - HIGH: Registry save now uses unique tmp filenames - HIGH: Webhook sink validates URL scheme (http/https only) - HIGH: Daemon start -d checks for existing running daemon before spawning - MEDIUM: Cursor only advances when at least one sink succeeds - LOW: Validate --interval (reject NaN), require --webhook-url with --sink webhook, remove unused import
- Sink per-subscription: replace shared singleton sinks with SinkFactory pattern — each subscription gets a dedicated sink instance, fixing the webhook URL overwrite bug when multiple subscriptions use different URLs - Issue comments cursor: use updated_at (matches GitHub's since= semantics) instead of created_at; detect edited comments via issue_comment.updated type; include updated_at in dedup ID to catch edits - eventsDelivered: cumulative counter instead of resetting each poll - Graceful shutdown: flush cursor store before process.exit - Dedup: O(1) circular buffer replacing O(n) Array.shift() - Remove unused _args parameter from ghJson
- Clean up PID file on foreground daemon shutdown (SIGINT/SIGTERM) - Guard against spawn failure (child.pid undefined) - Reuse validated intervalMs variable instead of re-parsing - Consistent URL construction in pollIssues (no ?& artifact)
- Cursor only advances when ALL sinks succeed (not any-one), preventing a failing sink from permanently missing events - Webhook fetch: add 30s AbortController timeout to prevent hung connections from blocking the poll loop indefinitely - Registry dedup: include sinkConfig in dedup key, so two webhook subscriptions with different URLs are correctly stored as separate entries. Update interval on re-subscribe instead of silently ignoring.
- async: replace execFileSync with async execFile (promisified) in GitHub source — no longer blocks the event loop, multiple origins poll concurrently - file locking: SubscriptionRegistry.withLock() uses O_CREAT|O_EXCL lockfile with stale detection (10s timeout). subscribe/unsubscribe use withLock to prevent concurrent CLI invocations from clobbering each other's changes - all poll methods now async, awaiting gh api calls
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Implements
opencli channel— an event subscription system that lets consumers (humans or AI agents) subscribe to platform events and receive them.RFC: #369
Design: Consumer-Side Subscription
Instead of Channel routing events to sessions (complex), consumers subscribe to sources they care about (simple):
Channel does exactly 3 things:
No routing logic, no session lifecycle management, no dispatcher.
What's Included
Core (
src/channel/)types.tscursor-store.ts~/.opencli/channel/cursors.json)dedup.tsscheduler.tsregistry.ts~/.opencli/channel/subscriptions.json)GitHub Source (
src/channel/sources/github.ts)gh apiCLI (inherits auth, proxy, host config)github:owner/repo,github:owner/repo#42,github:owner/repo/pulls,github:owner/repo/issuesSinks
stdout— JSON lines, pipe-friendlywebhook— POST to any URLCLI Commands
Documentation
docs/channel.md— full guide with architecture, usage examples, extension guideConstraints
node:*built-ins)gh api), not raw HTTPtsc --noEmitcleanVerified
npm run buildpassesnpx tsc --noEmitcleannpm test— 282 tests passopencli channel --helpregisters correctlyopencli channel sourceslists GitHubopencli channel subscribecreates subscriptionopencli channel poll github:jackwener/opencli#369fetches real events