-
Notifications
You must be signed in to change notification settings - Fork 0
Social Media Integration + Universal Handle System #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ok adapter - ISocialMediaProvider interface + SocialMediaTypes (platform-agnostic) - MoltbookProvider with full API coverage (posts, comments, voting, communities, search, profile) - SocialCredentialEntity for persistent credential storage per-persona per-platform - SocialCommandHelper with shared resolvePersonaId() and loadSocialContext() - SocialMediaProviderRegistry for multi-platform support - 5 commands: social/signup, social/post, social/feed, social/comment, social/notifications - Outreach room added to seed data - @continuum account registered and claimed on Moltbook - First post live with organic engagement (14 comments, 4+ upvotes) API fixes: nested author/submolt object handling, comment extraction from post detail endpoint, response wrapper unwrapping, correct endpoint paths per skill.md. Next: Universal handle system in continuum-core for async operations, then Rust social worker with persistent cache and demand-driven polling.
…hort ID resolution Handles are the universal primitive for async operations — social feeds, voice synthesis, AI inference, coding agents, proposals. Each handle persists in SQLite, survives restarts, and resolves by either full UUID or 6-char short form (#abc123). - Handle.ts: Type definitions (HandleStatus lifecycle, HandleRef, HandleRecord) - HandleEntity.ts: ORM entity with status transitions and TTL expiry - Handles.ts: Service layer (create, resolve short/long, status management, cleanup) - EntityRegistry: HandleEntity registered for schema creation - Constants: HANDLES collection added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a comprehensive social media integration system with a generic provider architecture and a universal handle system for managing async operations.
Changes:
- Adds generic
ISocialMediaProviderinterface withMoltbookProvideras the first implementation supporting full social media operations (signup, post, feed, comment, notifications) - Implements Universal Handle System for persistent async operation references with short ID (#abc123) and full UUID resolution, backed by SQLite with TTL-based expiry
- Creates 5 social commands (signup, post, feed, comment, notifications) following standard command generator patterns with full type safety
- Adds
SocialCredentialEntityfor storing per-persona platform credentials in their longterm.db - Seeds an "outreach" room for social media strategy coordination
Reviewed changes
Copilot reviewed 64 out of 66 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
src/debug/jtag/system/social/shared/SocialMediaTypes.ts |
Platform-agnostic type definitions for social media entities (posts, comments, notifications, profiles) |
src/debug/jtag/system/social/shared/SocialCredentialEntity.ts |
ORM entity for storing per-persona social credentials with composite unique index |
src/debug/jtag/system/social/shared/ISocialMediaProvider.ts |
Generic provider interface defining standard social media operations |
src/debug/jtag/system/social/server/providers/MoltbookProvider.ts |
Moltbook platform adapter with rate limiting, auth, and full API coverage |
src/debug/jtag/system/social/server/SocialMediaProviderRegistry.ts |
Factory registry for creating platform provider instances |
src/debug/jtag/system/social/server/SocialCommandHelper.ts |
Shared helper functions for credential loading and persona resolution |
src/debug/jtag/system/core/types/Handle.ts |
Type definitions for universal async operation handles |
src/debug/jtag/system/core/shared/Handles.ts |
Service implementation for handle CRUD, resolution, and expiry management |
src/debug/jtag/system/data/entities/HandleEntity.ts |
ORM entity for persisting handle state in SQLite |
src/debug/jtag/commands/social/*/ |
Five social commands with full command structure (shared, server, browser, tests, README) |
src/debug/jtag/system/shared/Constants.ts |
Added HANDLES collection constant |
src/debug/jtag/system/data/constants/RoomConstants.ts |
Added OUTREACH room constant |
src/debug/jtag/api/data-seed/RoomDataSeed.ts |
Seeds outreach room for social media coordination |
src/debug/jtag/daemons/data-daemon/server/EntityRegistry.ts |
Registers new entities (SocialCredentialEntity, HandleEntity) |
| Various generated files | Updated command registries and schemas |
Files not reviewed (1)
- src/debug/jtag/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const existingResult = await DataList.execute<SocialCredentialEntity>({ | ||
| dbHandle, | ||
| collection: SocialCredentialEntity.collection, | ||
| filter: { personaId, platformId: platform }, | ||
| limit: 1, | ||
| }); | ||
|
|
||
| if (existingResult.success && existingResult.items?.length) { | ||
| const existing = existingResult.items[0]; | ||
| return transformPayload(params, { | ||
| success: true, | ||
| message: `Already registered on ${platform} as @${existing.agentName}`, | ||
| apiKey: existing.apiKey, | ||
| agentName: existing.agentName, | ||
| profileUrl: existing.profileUrl, | ||
| claimUrl: existing.claimUrl, | ||
| }); | ||
| } | ||
|
|
||
| // Create provider (unauthenticated — signup doesn't need auth) | ||
| const provider = SocialMediaProviderRegistry.createProvider(platform); | ||
|
|
||
| // Register on the platform | ||
| const signupResult = await provider.signup({ agentName, description, metadata }); | ||
|
|
||
| if (!signupResult.success || !signupResult.apiKey) { | ||
| throw new Error(signupResult.error ?? `Signup failed on ${platform}`); | ||
| } | ||
|
|
||
| // Store credential in persona's longterm.db | ||
| const credential = new SocialCredentialEntity(); | ||
| credential.personaId = personaId; | ||
| credential.platformId = platform; | ||
| credential.apiKey = signupResult.apiKey; | ||
| credential.agentName = signupResult.agentName ?? agentName; | ||
| credential.profileUrl = signupResult.profileUrl; | ||
| credential.claimUrl = signupResult.claimUrl; | ||
| credential.claimStatus = 'pending'; | ||
| credential.registeredAt = new Date(); | ||
|
|
||
| await storeCredential(dbHandle, credential); |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SocialCredentialEntity has a composite unique index on (personaId, platformId) as defined in line 26-30, but the code in SocialSignupServerCommand checks for existing credentials using DataList.execute with a filter (lines 46-51). If the uniqueness is enforced at the database level, attempting to insert a duplicate would throw an error. Consider catching this error and providing a user-friendly message, or verify that the check-then-insert pattern is race-condition free.
| private readonly rateLimits: RateLimitTracker = { | ||
| requestTimestamps: [], | ||
| lastPostTimestamp: 0, | ||
| commentTimestamps: [], | ||
| }; |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rate limiting implementation uses in-memory arrays (requestTimestamps, commentTimestamps) that will be lost on provider instance recreation. Since the PR description mentions that "Each persona gets their own provider instance", these rate limits won't persist across restarts or if the provider instance is garbage collected. Consider documenting this limitation or implementing persistent rate limiting if needed.
| if (!this.requestedBy) { | ||
| return { success: false, error: 'Handle requestedBy is required' }; | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HandleEntity validation checks if 'requestedBy' exists (line 67) but doesn't validate that it's a properly formatted UUID. Consider adding UUID format validation to prevent invalid UUIDs from being stored.
|
|
||
| ```bash | ||
| # Run unit tests (no server required) | ||
| npx tsx commands/Social Signup/test/unit/SocialSignupCommand.test.ts |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The social command READMEs contain inconsistent command paths in the test examples. For instance, line 111 shows 'commands/Social Signup/test/unit/SocialSignupCommand.test.ts' with spaces in the path, but the actual file structure uses lowercase with hyphens 'commands/social/signup/test/unit/SocialSignupCommand.test.ts'. This will cause the test commands to fail if run as documented.
| private mapNotification(data: Record<string, unknown>): SocialNotification { | ||
| const typeStr = String(data.type ?? 'system'); | ||
| const validTypes = ['reply', 'mention', 'follow', 'vote', 'dm', 'system'] as const; | ||
| const type = validTypes.includes(typeStr as typeof validTypes[number]) | ||
| ? typeStr as typeof validTypes[number] | ||
| : 'system'; | ||
|
|
||
| return { | ||
| id: String(data.id ?? ''), | ||
| type, | ||
| content: String(data.content ?? data.message ?? data.text ?? ''), | ||
| authorName: data.author_name ? String(data.author_name) : (data.from ? String(data.from) : undefined), | ||
| postId: data.post_id ? String(data.post_id) : undefined, | ||
| postTitle: data.post_title ? String(data.post_title) : undefined, | ||
| commentId: data.comment_id ? String(data.comment_id) : undefined, | ||
| read: Boolean(data.read ?? data.is_read ?? false), | ||
| createdAt: String(data.created_at ?? data.createdAt ?? new Date().toISOString()), | ||
| }; | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mapNotification function is defined but never used in the MoltbookProvider. The getNotifications method at line 261 returns an empty array instead of using this mapper. This dead code should either be utilized or removed.
| validate(): { success: boolean; error?: string } { | ||
| const errors: string[] = []; | ||
|
|
||
| if (!this.personaId) errors.push('personaId is required'); | ||
| if (!this.platformId?.trim()) errors.push('platformId is required'); | ||
| if (!this.apiKey?.trim()) errors.push('apiKey is required'); | ||
| if (!this.agentName?.trim()) errors.push('agentName is required'); | ||
|
|
||
| const validStatuses: ClaimStatus[] = ['pending', 'claimed', 'unknown']; | ||
| if (!validStatuses.includes(this.claimStatus)) { | ||
| errors.push(`claimStatus must be one of: ${validStatuses.join(', ')}`); | ||
| } | ||
|
|
||
| if (errors.length > 0) { | ||
| return { success: false, error: errors.join(', ') }; | ||
| } | ||
| return { success: true }; | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SocialCredentialEntity validation (lines 90-107) checks that apiKey and agentName are not empty using trim(), but doesn't validate the format or length constraints for these fields. Consider adding validation for expected formats (e.g., apiKey should match a specific pattern if known, agentName should meet platform-specific requirements).
| constructor() { | ||
| super(); | ||
| this.status = 'pending'; | ||
| this.retryCount = 0; | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HandleEntity constructor initializes 'retryCount' to 0 in the constructor (line 56), but this property is not explicitly set in the constructor body. While this may work with TypeScript's property initialization, consider explicitly setting it in the constructor body for consistency with the other properties like 'status'.
| // Query all handles and filter by suffix | ||
| // The $regex operator matches UUIDs ending with the short ID | ||
| const result = await DataList.execute<HandleEntity>({ | ||
| collection: COLLECTIONS.HANDLES, | ||
| filter: { id: { $regex: `${shortId}$` } }, | ||
| limit: 2, // Get 2 to detect ambiguity | ||
| }); | ||
|
|
||
| if (!result.success || !result.items?.length) return null; | ||
|
|
||
| if (result.items.length > 1) { | ||
| log.warn(`Ambiguous short ID #${shortId} matched ${result.items.length} handles. Returning most recent.`); | ||
| } | ||
|
|
||
| return entityToRecord(result.items[0] as HandleEntity); |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Handles.resolve method uses a regex filter to find handles by short ID suffix (line 142), but this could match multiple handles if they share the same last 6 characters. While a warning is logged when ambiguous matches are found (line 149), the function returns the first match which could lead to incorrect handle resolution. Consider implementing a more robust collision handling strategy or enforcing uniqueness at the database level.
| if (status === 'failed') { | ||
| // Increment retry count on failure — read current, increment, write back | ||
| const current = await DataRead.execute<HandleEntity>({ | ||
| collection: COLLECTIONS.HANDLES, | ||
| id, | ||
| }); | ||
| if (current.found && current.data) { | ||
| updates.retryCount = ((current.data as HandleEntity).retryCount ?? 0) + 1; | ||
| } | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the _updateStatus method, when status is 'failed', the code reads the current retry count and increments it (lines 320-326). However, this creates a race condition if multiple concurrent failures occur for the same handle. The read-then-increment pattern should be replaced with an atomic increment operation or proper locking mechanism.
| if (!response.ok && response.status !== 404) { | ||
| const errorText = await response.text().catch(() => 'Unknown error'); | ||
| throw new Error(`Moltbook API error (${method} ${path}): ${response.status} ${errorText}`); | ||
| } |
Copilot
AI
Jan 31, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MoltbookProvider's rate limit check at line 433 only handles 404 status specially, but the comment at line 433 states "!response.ok && response.status !== 404". This means all other HTTP errors (400, 401, 403, 500, etc.) will throw errors. For 401 (Unauthorized), this is likely appropriate, but for 5xx server errors, retrying with exponential backoff might be more resilient than immediately throwing.
…al participation Three smart commands that let AIs be effective social media participants: - social/browse: Multi-mode exploration (discover communities, trending feeds, deep post reading with threaded comments, agent profiles). Returns AI-friendly summaries with actionable context. - social/engage: All interaction in one command (vote, follow/unfollow, subscribe/unsubscribe). Rate-limit aware. - social/search: Semantic search across platform with structured results. - Fix MoltbookProvider.getProfile() to unwrap API's 'agent' wrapper field. Tested live against Moltbook: discovered 100 communities, browsed trending feeds, read full post threads, voted on posts, searched for content.
…ndle fixes social/propose command — nominate actions, vote, auto-execute on threshold: - create/vote/list/view modes for democratic governance - configurable thresholds per action type (post=5, comment=4, follow=3, vote=2) - auto-execution when threshold met, rejection detection - visual vote progress bars in list mode social/engage — added delete action for posts and comments ISocialMediaProvider + MoltbookProvider — added deleteComment Handles.ts — fixed type narrowing bug with isValidUUID guard, extended _updateStatus to accept params in extra (needed for vote updates) RoomConstants + RoomDataSeed — added newsroom room for current events awareness
Analyzes external agents across 5 dimensions with probability scores: - spam: repetitive content, follow-spam patterns, template detection - authentic: content diversity, substance, community breadth - influence: karma, followers, engagement metrics - engagement: conversation quality, threaded depth - reliability: account age, posting consistency Produces trust score, expertise domains, labels, and actionable recommendations. Like an embedding space for AI personas on external social media platforms.
…community, downvote - SocialMediaRAGSource: Cache-only load() (<1ms) with background warmup loop that serializes credential resolution + API calls across 14 personas. Static singleton pattern ensures shared caches across all ChatRAGBuilder instances. Never blocks persona cognition for external API calls. - social/profile: View own/other agent profiles, update bio/description - social/trending: Discover hot/top/rising content with community filtering - social/community: Create, list, subscribe/unsubscribe communities - social/downvote: Downvote posts and comments - ChatRAGBuilder: Registers SocialMediaRAGSource, injects social awareness HUD into persona system prompts (hasSocialAwareness metadata flag) - SocialCommandHelper: Shared credential fallback for unclaimed accounts - Classify: Enhanced spam detection with karma velocity analysis
…reach/newsroom rooms SqliteQueryExecutor: row-level error isolation — one corrupted JSON field no longer kills an entire multi-row query. Corrupted rows are skipped with a warning instead of throwing. RoomMembershipDaemon: subscribe to room creation events in addition to user creation. When a room is created after daemon startup, all applicable users are automatically added. Fixes the race where seed script creates rooms after the daemon's catch-up logic, leaving personas unsubscribed. Outreach + Newsroom rooms: proper seed definitions with deterministic UUIDs, dedicated recipes, routing rules, and missing-room detection in seed script. Social HUD RAG source updated with directive language that drives personas to act rather than analyze.
Continuum personas are active on Moltbook (AI social network). Added link to platform table, social media mention, and contact section.
- Remove dead mapNotification() from MoltbookProvider (no API endpoint) - Fix malformed UUID examples in Handle.ts doc comments - Fix all 9 social command README test paths (spaces → lowercase slashes) - Fix Handles.expireStale() to paginate through all stale handles - Add UUID format validation to HandleEntity.requestedBy - Add orderBy to short ID resolution for deterministic collision handling - Document in-memory rate limiting as ephemeral by design - Document single-threaded safety for retryCount read-increment-write Skipped by design: credential check-then-insert race (single-persona, tiny window), apiKey format validation (unknown platform formats), 5xx retry logic (project policy: fail clearly, no fallbacks).
Summary
ISocialMediaProviderinterface supports any platform#abc123) or full UUID, backed by SQLite, survive restartsArchitecture
Social Media Provider (Generic)
Each persona gets their own provider instance with their own API key stored in
SocialCredentialEntity.Universal Handle System
Handles are the universal primitive for ALL async operations — social feeds, voice synthesis, AI inference, coding agents, proposals. Persists in SQLite, survives restarts, TTL-based expiry.
Next Steps (not in this PR)
Test plan
npm run build:ts)🤖 Generated with Claude Code