import type { ScopeMembership } from '../agent/tool.scope.js'; import type { UserIdentity } from '../schemas/identity.schema.js'; import type { NetworkAssignmentMetadata } from '../schemas/network-assignment.schema.js'; /** Branded string ID for type-safe entity references (keyed by Drizzle table name). */ export type Id = string & { readonly __table?: T; }; export interface OnboardingProfileSeed { source: 'experiment_signup' | 'experiment_csv_import'; networkId: string; capturedAt: string; name?: string; bio?: string; location?: string; socials?: { label: string; value: string; }[]; } export interface NetworkAssignmentContext { networkId: string; indexPrompt: string | null; memberPrompt: string | null; } export interface AssignmentNetworkMembership extends ScopeMembership { networkId: string; isPersonal: boolean; } /** Final-authority result for an existing intent-to-network assignment. */ export type IntentNetworkFinalAssignmentResult = { kind: 'assigned'; } | { kind: 'already_assigned'; } | { kind: 'membership_required'; } | { kind: 'intent_not_owned_or_not_found'; }; /** Onboarding flow state stored as JSON on the user record. */ export interface OnboardingState { completedAt?: string; profileConfirmedAt?: string; firstSignalIntentId?: string; flow?: 1 | 2 | 3; currentStep?: 'profile' | 'summary' | 'connections' | 'create_network' | 'invite_members' | 'join_networks' | 'first_signal' | 'complete'; networkId?: string; invitationCode?: string; profileSeeds?: OnboardingProfileSeed[]; } /** Single social-link row from the user_socials table. */ export interface UserSocial { id: string; userId: string; label: string; value: string; } /** Detection metadata recorded when an opportunity is created. */ export interface OpportunityDetection { source: 'opportunity_graph' | 'chat' | 'manual' | 'cron' | 'member_added' | 'enrichment' | 'introducer_discovery'; createdBy?: Id<'users'> | string; createdByName?: string; triggeredBy?: Id<'intents'>; timestamp: string; enrichedFrom?: string[]; } /** A participant (user + network) involved in an opportunity. */ export interface OpportunityActor { networkId: Id<'networks'>; userId: Id<'users'>; intent?: Id<'intents'>; /** Which premise grounded this match (set when discoverySource is 'premise-similarity'). */ premise?: Id<'premises'>; role: string; /** Only set on role === 'introducer'. false until the introducer explicitly approves; true after approval. */ approved?: boolean; /** * ISO-8601 timestamp set the first time this actor advanced the opportunity's * state (patient sending, agent accepting, peer "accepting" on draft = sending * under the hood, peer accepting on pending, introducer sending). Once set, * this actor has committed and cannot be the one to subsequently `accept` the * same opportunity — enforced by the self-accept guard in `updateNode`. */ actedAt?: string; } /** Individual signal contributing to an opportunity score. */ export interface OpportunitySignal { type: string; weight: number; detail?: string; /** Optional source question for reversible pool-preference provenance. */ questionId?: string; /** Recipient provenance for pool-discriminator signals. */ recipientUserId?: string; /** Intent-pool provenance for pool-discriminator signals. */ intentId?: string; } /** LLM-generated interpretation of an opportunity's category and confidence. */ export interface OpportunityInterpretation { category: string; reasoning: string; confidence: number; signals?: OpportunitySignal[]; } /** Optional scoping context (network / conversation) for an opportunity. */ export interface OpportunityContext { networkId?: Id<'networks'>; conversationId?: Id<'conversations'>; } /** User record returned by getUser (minimal fields plus optional profile fields). */ export interface UserRecord { id: string; name: string; email: string; intro?: string | null; avatar?: string | null; location?: string | null; socials: UserSocial[]; onboarding?: OnboardingState | null; isGhost?: boolean; deletedAt?: Date | null; } /** * Minimal intent representation used for graph state population. * Contains only the fields needed for reconciliation logic. */ export interface ActiveIntent { /** Unique identifier of the intent */ id: string; /** Full intent description/payload */ payload: string; /** Short summary of the intent (may be null if not generated) */ summary: string | null; /** When the intent was created */ createdAt: Date; /** Relevancy score for this intent in its index context (0.0–1.0, null if not scored) */ relevancyScore?: number | null; } /** * Input data for creating a new intent. * Supports the full intent pipeline including embedding and index association. */ export interface CreateIntentData { /** The user who owns this intent */ userId: string; /** Full intent description/payload */ payload: string; /** Pre-computed summary (optional, will be generated if not provided) */ summary?: string | null; /** Pre-computed embedding vector (optional, will be generated if not provided) */ embedding?: number[]; /** Whether the intent should be hidden from public views */ isIncognito?: boolean; /** Network IDs to associate with (optional, uses dynamic scoping if empty) */ networkIds?: string[]; /** Source type for provenance tracking */ sourceType?: 'file' | 'integration' | 'link' | 'discovery_form' | 'enrichment'; /** Source ID for provenance tracking */ sourceId?: string; /** Confidence score from inference (0-1, required) */ confidence: number; /** How the intent was inferred */ inferenceType: 'explicit' | 'implicit'; /** Semantic entropy from verifier (0 specific -> 1 vague) */ semanticEntropy?: number | null; /** Referential anchor extracted by verifier (if any) */ referentialAnchor?: string | null; /** Felicity authority score from verifier (0-100) */ felicityAuthority?: number | null; /** Felicity sincerity score from verifier (0-100) */ felicitySincerity?: number | null; /** Felicity clarity score from verifier (0-100) */ felicityClarity?: number | null; /** Donnellan intent mode */ intentMode?: 'REFERENTIAL' | 'ATTRIBUTIVE' | null; /** Speech act category used by protocol enum */ speechActType?: 'COMMISSIVE' | 'DIRECTIVE' | null; } /** * Input data for updating an existing intent. * All fields are optional - only provided fields will be updated. */ export interface UpdateIntentData { /** Updated intent description/payload */ payload?: string; /** Updated summary */ summary?: string | null; /** Updated embedding vector */ embedding?: number[]; /** Updated incognito status */ isIncognito?: boolean; /** Updated index associations (replaces existing) */ networkIds?: string[]; /** Semantic entropy from verifier (0 specific -> 1 vague) */ semanticEntropy?: number | null; /** Referential anchor extracted by verifier (if any) */ referentialAnchor?: string | null; /** Felicity authority score from verifier (0-100) */ felicityAuthority?: number | null; /** Felicity sincerity score from verifier (0-100) */ felicitySincerity?: number | null; /** Felicity clarity score from verifier (0-100) */ felicityClarity?: number | null; /** Donnellan intent mode */ intentMode?: 'REFERENTIAL' | 'ATTRIBUTIVE' | null; /** Speech act category used by protocol enum */ speechActType?: 'COMMISSIVE' | 'DIRECTIVE' | null; /** * Optional compare-and-set guard for recovery-answer writes. Implementations * must compare this value with the material payload+summary fingerprint while * holding the final intent row lock. Omitted for ordinary intent updates. */ expectedIntentFingerprint?: string; /** Expected owner paired with the recovery-answer fingerprint guard. */ expectedIntentUserId?: string; } /** * The result of a successful intent creation. * Contains the core fields needed for immediate use. */ export interface CreatedIntent { /** Unique identifier of the created intent */ id: string; /** Full intent description/payload */ payload: string; /** Generated or provided summary */ summary: string | null; /** Incognito status */ isIncognito: boolean; /** Creation timestamp */ createdAt: Date; /** Last update timestamp */ updatedAt: Date; /** Owner user ID */ userId: string; } /** * Full intent record with all fields (for detailed queries). */ export interface IntentRecord extends CreatedIntent { /** Archival timestamp (null if active) */ archivedAt: Date | null; /** Embedding vector (may be null) */ embedding?: number[] | null; /** Source type for provenance */ sourceType?: string | null; /** Source ID for provenance */ sourceId?: string | null; /** Lifecycle admission state; null is a legacy ACTIVE row. */ status?: 'ACTIVE' | 'PAUSED' | 'FULFILLED' | 'EXPIRED' | null; } /** * Intent with similarity score from vector search. */ export interface SimilarIntent extends IntentRecord { /** Cosine similarity score (0-1) */ similarity: number; } /** * Result of an archive operation. */ export interface ArchiveResult { /** Whether the operation succeeded */ success: boolean; /** Error message if failed */ error?: string; } /** * Options for vector similarity search. */ export interface SimilarIntentSearchOptions { /** Maximum number of results to return (default: 10) */ limit?: number; /** Minimum similarity threshold (default: 0.7) */ threshold?: number; } /** * Represents a user's membership in an index with full details. * Used for displaying network memberships in chat (index_query). */ export interface ActiveNetworkMembershipPair { userId: string; networkId: string; } export interface NetworkMembership { /** Unique identifier of the index */ networkId: string; /** Display title of the index */ networkTitle: string; /** Index description/prompt (what the community is about) */ indexPrompt: string | null; /** Member's permissions in this network */ permissions: string[]; /** Member's custom prompt (overrides network prompt for their intents) */ memberPrompt: string | null; /** Whether new intents are auto-assigned to this network */ autoAssign: boolean; /** Whether this is the user's personal network ("My Network") */ isPersonal: boolean; /** When the user joined the network */ joinedAt: Date; } export interface PremiseAssertion { text: string; tier: 'assertive' | 'contextual'; summary?: string; } export interface PremiseProvenance { source: 'explicit' | 'enrichment' | 'integration' | 'onboarding'; sourceId?: string; confidence: number; timestamp: string; } export interface PremiseAnalysis { speechActType: 'DECLARATIVE' | 'ASSERTIVE'; felicityAuthority: number; felicitySincerity: number; felicityClarity: number; semanticEntropy: number; } export interface PremiseValidity { validFrom?: string; validUntil?: string; volatile: boolean; } export interface PremiseRecord { id: string; userId: string; assertion: PremiseAssertion; provenance: PremiseProvenance; analysis: PremiseAnalysis | null; validity: PremiseValidity; embedding: number[] | null; status: 'ACTIVE' | 'RETRACTED' | 'EXPIRED'; createdAt: Date; updatedAt: Date; retractedAt: Date | null; } /** * Represents an index owned by the user with full details. */ export interface OwnedIndex { /** Network ID */ id: string; /** Display title */ title: string; /** Index purpose/scope prompt */ prompt: string | null; /** Cover image URL */ imageUrl: string | null; /** Permission settings */ permissions: { joinPolicy: 'anyone' | 'invite_only'; invitationLink: { code: string; } | null; }; /** Whether this is a personal network */ isPersonal: boolean; /** When the index was created */ createdAt: Date; /** When the index was last updated */ updatedAt: Date; /** Member count */ memberCount: number; /** Total intents indexed */ intentCount: number; /** Owner summary */ user: { id: string; name: string; avatar: string | null; }; /** Aggregate counts for frontend compatibility */ _count: { members: number; }; } /** * Member details visible to network owners (and optionally to members with privacy rules). */ export interface IndexMemberDetails { /** User ID */ userId: string; /** User's display name */ name: string; /** User's avatar URL */ avatar: string | null; /** User's email; only present when viewer is owner/admin or the member themselves (privacy-safe) */ email?: string | null; /** Member's permissions in this network */ permissions: string[]; /** Member's custom prompt */ memberPrompt: string | null; /** Whether auto-assign is enabled */ autoAssign: boolean; /** When they joined */ joinedAt: Date; /** Count of their intents in this network */ intentCount: number; /** Whether this user is a ghost (not yet onboarded) */ isGhost?: boolean; } /** * Intent details visible to network owners. */ export interface IndexedIntentDetails { /** Intent ID */ id: string; /** Intent payload/description */ payload: string; /** Intent summary */ summary: string | null; /** Owner's user ID */ userId: string; /** Owner's name */ userName: string; /** When the intent was created */ createdAt: Date; /** Relevancy score for this intent in its index context (0.0–1.0, null if not scored) */ relevancyScore?: number | null; } /** * Options for updating index settings. */ export interface UpdateIndexSettingsData { /** New title (optional) */ title?: string; /** New prompt (optional) */ prompt?: string | null; /** New image URL (optional) */ imageUrl?: string | null; /** New join policy (optional) */ joinPolicy?: 'anyone' | 'invite_only'; } export type HydeSourceType = 'intent' | 'query' | 'context'; export interface HydeDocument { id: string; sourceType: HydeSourceType; sourceId: string | null; sourceText: string | null; strategy: string; targetCorpus: string; hydeText: string; hydeEmbedding: number[]; context: Record | null; createdAt: Date; expiresAt: Date | null; } export interface CreateHydeDocumentData { sourceType: HydeSourceType; sourceId?: string; sourceText?: string; strategy: string; targetCorpus: string; hydeText: string; hydeEmbedding: number[]; context?: Record; expiresAt?: Date; } export type OpportunityStatus = 'latent' | 'draft' | 'negotiating' | 'pending' | 'stalled' | 'accepted' | 'rejected' | 'expired'; /** * Minimal opportunity lifecycle evidence used to narrate an agent negotiation. * `acceptedByOwner` is true only when the authenticated owner is the persisted * human acceptor; other terminal states do not imply an owner action. */ export interface NegotiationOpportunityLifecycle { status: OpportunityStatus; acceptedByOwner: boolean; } export interface Opportunity { id: string; detection: OpportunityDetection; actors: OpportunityActor[]; interpretation: OpportunityInterpretation; context: OpportunityContext; confidence: string; status: OpportunityStatus; createdAt: Date; updatedAt: Date; expiresAt: Date | null; metadata?: Record | null; } export interface OpportunityNetworkEligibility { /** User whose active memberships define the discovery boundary. */ ownerUserId: string; /** Request/intent-authorized networks after the latest graph-side recomputation. */ allowedNetworkIds: string[]; /** When present, each actor network must remain assigned to this intent through commit. */ triggerIntentId?: string; } export type OpportunityDedupConflictReason = 'same_trigger_recent_duplicate' | 'pair_active_negotiation'; export interface OpportunityDedupConflict { reason: OpportunityDedupConflictReason; existingOpportunityId: string; existingTriggerIntentId?: string; existingStatus: OpportunityStatus; existingCreatedAt: Date; } export type IntentScopedOpportunityPersistenceResult = { created: Opportunity; expired: Opportunity[]; } | { conflict: OpportunityDedupConflict; }; export interface CreateOpportunityData { detection: OpportunityDetection; actors: OpportunityActor[]; interpretation: OpportunityInterpretation; context: OpportunityContext; confidence: string; status?: OpportunityStatus; expiresAt?: Date; metadata?: Record | null; } export interface OpportunityQueryOptions { status?: OpportunityStatus; /** When set, filter to opportunities whose status is in this list. Orthogonal to `status` (single) — callers pick one. */ statuses?: OpportunityStatus[]; networkId?: string; /** Optional selected-intent scope. When `scopeType === 'intent'`, `scopeId` is the selected intent id. */ scopeType?: 'intent'; scopeId?: string; role?: string; limit?: number; offset?: number; /** When set, include draft opportunities for this chat session. When unset, exclude all draft opportunities (e.g. radar view, API). */ conversationId?: string; } /** * Abstract database interface for performing specific domain operations. * Decouples the protocol layer from the infrastructure layer. */ export interface Database { /** * Retrieves a user profile by userId. * @param userId - The unique identifier of the user * @returns The user's profile or null if not found */ getProfile(userId: string): Promise; /** * Creates or updates a user profile. * @param userId - The unique identifier of the user * @param profile - The profile data to save */ saveProfile(userId: string, profile: UserIdentity): Promise; /** * Retrieves basic user information (name, email, socials) by userId. * @param userId - The unique identifier of the user * @returns The user record or null if not found */ getUser(userId: string): Promise; /** * Updates user account fields (name, location, socials). * Merges socials with existing values (does not overwrite the whole object). * Used by create_user_context tool to persist user-provided info before * invoking the Profile Graph in generate mode. * * @param userId - The unique identifier of the user * @param data - Partial user fields to update * @returns The updated user record or null if not found */ updateUser(userId: string, data: { name?: string; intro?: string; location?: string; onboarding?: OnboardingState; }): Promise; getUserSocials(userId: string): Promise; setUserSocials(userId: string, socials: { label: string; value: string; }[]): Promise; /** * Soft-delete a ghost user and all their contact memberships. * Used when enrichment determines the entity is not a real person. * @param userId - The ghost user to soft-delete * @returns true if the user was soft-deleted */ softDeleteGhost(userId: string): Promise; /** * Find an existing user that matches the given social handles. * Checks LinkedIn, GitHub, and Twitter/X handles (case-insensitive, exact match). * Excludes the given userId and soft-deleted users. * Prefers real users over ghosts; among ghosts, returns the oldest. * @param userId - The ghost user being enriched (excluded from results) * @param socials - Enriched social handles to match against * @returns The matching user's id, or null if no match */ findDuplicateUser(userId: string, socials: UserSocial[]): Promise<{ id: string; } | null>; /** * Merge a ghost user (source) into a target user. * Re-points all data (intents, opportunities, memberships, etc.) from source to target, * deletes ghost-only records (profile, sessions, etc.), and soft-deletes the source user. * Runs in a single transaction. * @param sourceId - The ghost user to merge away * @param targetId - The user to merge into */ mergeGhostUser(sourceId: string, targetId: string): Promise; /** * Retrieves all active (non-archived) intents for a user. * Used to populate the `activeIntents` field in the Intent Graph state * before graph execution. * * @param userId - The unique identifier of the user * @returns Array of active intents with minimal fields needed for reconciliation * * @example * ```typescript * const activeIntents = await db.getActiveIntents(userId); * const formattedIntents = activeIntents * .map(i => `ID: ${i.id}, Description: ${i.payload}, Summary: ${i.summary || 'N/A'}`) * .join('\n'); * ``` */ getActiveIntents(userId: string): Promise; /** * Get active intents that belong to the user and are assigned to a specific index. * Caller must be a member of that index; only the user's own intents are returned. * * @param userId - The user requesting (must be a member of the index) * @param indexNameOrId - Network UUID or display name (e.g. "Commons") * @returns Array of active intents in that index for the user, or empty if not a member / no match */ getIntentsInIndexForMember(userId: string, indexNameOrId: string): Promise; /** * Creates a new intent with full processing pipeline. * Handles summarization, embedding generation, and index association. * * Called when the reconciler outputs a "create" action. * * @param data - The intent creation data * @returns The created intent with generated fields * * @example * ```typescript * // After graph outputs CREATE action * const newIntent = await db.createIntent({ * userId, * payload: action.payload, * confidence: action.score / 100, * inferenceType: 'explicit', * sourceType: 'discovery_form' * }); * ``` */ createIntent(data: CreateIntentData): Promise; /** * Updates an existing intent. * Re-generates summary and embedding if payload changes. * * Called when the reconciler outputs an "update" action. * * @param intentId - The unique identifier of the intent to update * @param data - The fields to update * @returns The updated intent or null if not found * @throws Error if the intent exists but user doesn't have access * * @example * ```typescript * // After graph outputs UPDATE action * const updated = await db.updateIntent(action.id, { * payload: action.payload * }); * ``` */ updateIntent(intentId: string, data: UpdateIntentData): Promise; /** * Archives (soft-deletes) an intent. * Sets the archivedAt timestamp rather than hard deleting. * * Called when the reconciler outputs an "expire" action. * * @param intentId - The unique identifier of the intent to archive * @returns Result object indicating success or failure with error message * * @example * ```typescript * // After graph outputs EXPIRE action * const result = await db.archiveIntent(action.id); * if (!result.success) { * console.error('Failed to archive intent', { error: result.error }); * } * ``` */ archiveIntent(intentId: string): Promise; /** * Retrieves a single intent by ID. * * @param intentId - The unique identifier of the intent * @returns The full intent record or null if not found */ getIntent(intentId: string): Promise; /** * Retrieves an intent with ownership verification. * Ensures the requesting user owns the intent before returning. * * Used for processing operations (refine, suggestions) that require ownership. * * @param intentId - The unique identifier of the intent * @param userId - The user requesting access * @returns The intent if found and owned by user, null if not found * @throws Error with message 'Access denied' if intent exists but is not owned by user * * @example * ```typescript * try { * const intent = await db.getIntentWithOwnership(intentId, userId); * if (!intent) return res.status(404).json({ error: 'Not found' }); * // Process intent... * } catch (e) { * if (e.message === 'Access denied') { * return res.status(403).json({ error: 'Forbidden' }); * } * throw e; * } * ``` */ getIntentWithOwnership(intentId: string, userId: string): Promise; /** * Gets Network IDs where the user has auto-assign membership enabled. * Used for determining which indexes to associate new intents with. * * @param userId - The unique identifier of the user * @returns Array of network IDs * * @example * ```typescript * const networkIds = await db.getUserIndexIds(userId); * if (networkIds.length > 0) { * await db.associateIntentWithNetworks(intentId, networkIds); * } * ``` */ getUserIndexIds(userId: string): Promise; /** * Retrieves all networks the user is a member of with full details. * Used for displaying network memberships in chat (index_query). * * @param userId - The unique identifier of the user * @returns Array of network memberships with details */ getNetworkMemberships(userId: string): Promise; /** * Get a single network membership by index and user. * Used when the preloaded memberships list may not contain this network (e.g. after isNetworkMember check). * * @param networkId - The network ID * @param userId - The user ID * @returns The membership or null if not found */ getNetworkMembership(networkId: string, userId: string): Promise; /** * Return only requested user/network pairs backed by a live membership row * and a non-deleted network. Permissions are intentionally not filtered: * personal-network contacts are valid discovery participants. */ getActiveNetworkMembershipPairs(pairs: ActiveNetworkMembershipPair[]): Promise; /** * Get index by ID with core fields. Used for opportunity presentation and context rendering. */ getNetwork(networkId: string): Promise<{ id: string; title: string; prompt?: string | null; type?: string; metadata?: Record | null; permissions?: Record | null; } | null>; /** * Get index by ID with permissions (e.g. joinPolicy). Used by chat tools for create_index_membership. */ getNetworkWithPermissions(networkId: string): Promise<{ id: string; title: string; permissions: { joinPolicy: 'anyone' | 'invite_only'; }; } | null>; /** * Associates an intent with one or more networks. * Creates entries in the intentNetworks join table. * * @param intentId - The intent to associate * @param networkIds - Array of network IDs to associate with * * @example * ```typescript * await db.associateIntentWithNetworks(intentId, ['idx_1', 'idx_2']); * ``` */ associateIntentWithNetworks(intentId: string, networkIds: string[]): Promise; /** * Finds semantically similar intents using vector search. * Used for deduplication during intent creation and discovery. * * Privacy scoping: Results are always filtered by userId to ensure * users only see their own intents. * * @param embedding - The query embedding vector * @param userId - The user ID for privacy scoping (required) * @param options - Search options (limit, threshold) * @returns Array of intents with similarity scores, sorted by similarity * * @example * ```typescript * // Check for duplicates before creating * const embedding = await embedder.generate(payload); * const similar = await db.findSimilarIntents(embedding, userId, { * limit: 5, * threshold: 0.85 * }); * if (similar.length > 0 && similar[0].similarity > 0.95) { * // Likely duplicate - consider updating instead * } * ``` */ findSimilarIntents(embedding: number[], userId: string, options?: SimilarIntentSearchOptions): Promise; /** * Intent fields needed for index appropriateness evaluation. */ getIntentForIndexing(intentId: string): Promise<{ id: string; payload: string; userId: string; sourceType: string | null; sourceId: string | null; } | null>; /** * Index + member prompts for a user in an index (only when member has autoAssign). * Returns null if user is not a member or autoAssign is false. */ getNetworkMemberContext(networkId: string, userId: string): Promise; /** * Network memberships that should be considered for assignment policy. Unlike * getUserIndexIds, this is not gated by network_members.autoAssign and carries * personal-index metadata so scoped writes can include the user's personal network. */ getAssignmentNetworkMembershipsForUser(userId: string): Promise; /** * Network IDs that should be considered for assignment policy. Unlike * getUserIndexIds, this is not gated by network_members.autoAssign. * @deprecated Prefer getAssignmentNetworkMembershipsForUser for scope-aware assignment. */ getAssignmentNetworkIdsForUser(userId: string): Promise; /** * Prompt context for assignment policy. Unlike getNetworkMemberContext, this is * not gated by network_members.autoAssign. */ getNetworkAssignmentContext(networkId: string, userId: string): Promise; /** * Whether the intent is currently assigned to the index. */ isIntentAssignedToIndex(intentId: string, networkId: string): Promise; /** * Assigns an intent to an index (inserts intent_indexes row). */ assignIntentToNetwork(intentId: string, networkId: string, relevancyScore?: number, assignmentMetadata?: NetworkAssignmentMetadata): Promise; /** * Atomically assign an owned, non-archived intent only while the exact * accepted network membership and network remain active. Implementations * hold intent, network, and membership row locks through the insert. */ assignIntentToNetworkIfMember(userId: string, intentId: string, networkId: string, relevancyScore?: number, assignmentMetadata?: NetworkAssignmentMetadata): Promise; /** * Returns per-index relevancy scores for an intent's index assignments. */ getIntentIndexScores(intentId: string): Promise>; /** * Removes an intent from an index (deletes intent_indexes row). */ unassignIntentFromIndex(intentId: string, networkId: string): Promise; /** * Returns all network IDs that an intent is registered to. */ getNetworkIdsForIntent(intentId: string): Promise; /** * Get indexes where the user has owner permissions. * Returns full index details with member and intent counts. * * @param userId - The user ID to check ownership for * @returns Array of owned indexes with counts */ getOwnedIndexes(userId: string): Promise; /** * Get public networks (joinPolicy 'anyone') that the user has not joined. * Used for discovering communities available to join. * * @param userId - The user ID to check memberships against * @returns Object containing array of public networks with owner info */ getPublicIndexesNotJoined(userId: string): Promise<{ networks: Array<{ id: string; title: string; prompt: string | null; memberCount: number; owner: { id: string; name: string; avatar: string | null; } | null; }>; }>; /** * Check if user is an owner of a specific index. * * @param networkId - The index to check * @param userId - The user to verify ownership for * @returns True if user is an owner */ isIndexOwner(networkId: string, userId: string): Promise; /** * Check if user is a member of a specific index. * * @param networkId - The index to check * @param userId - The user to verify membership for * @returns True if user is a member */ isNetworkMember(networkId: string, userId: string): Promise; /** * Get all members of an index with their details. * **OWNER ONLY** - throws if user is not an owner. * * @param networkId - The index to get members for * @param requestingUserId - The user requesting (must be owner) * @returns Array of member details with intent counts * @throws Error if requestingUserId is not an owner */ getNetworkMembersForOwner(networkId: string, requestingUserId: string): Promise; /** * Get all members of an index with their details. * **MEMBER ONLY** - any member of the index can list members (not just owners). * Returns same shape as getNetworkMembersForOwner; email may be omitted for privacy. * * @param networkId - The index to get members for * @param requestingUserId - The user requesting (must be a member of the index) * @returns Array of member details with intent counts * @throws Error if requestingUserId is not a member of the index */ getNetworkMembersForMember(networkId: string, requestingUserId: string): Promise; /** * Get all members from every network the user is a member of (deduplicated). * Used for mentionable-users: anyone who shares at least one index with the requesting user. * * @param userId - The signed-in user * @returns Array of member summaries (id, name, avatar only; no email) */ getMembersFromUserIndexes(userId: Id<'users'>): Promise<{ userId: Id<'users'>; name: string; avatar: string | null; }[]>; /** * Get all indexed intents for an index. * **OWNER ONLY** - throws if user is not an owner. * * @param networkId - The index to get intents for * @param requestingUserId - The user requesting (must be owner) * @param options - Pagination options * @returns Array of intent details with owner info * @throws Error if requestingUserId is not an owner */ getNetworkIntentsForOwner(networkId: string, requestingUserId: string, options?: { limit?: number; offset?: number; }): Promise; /** * Get all indexed intents for an index. * **MEMBER ONLY** - any member of the index can list intents (not just owners). * * @param networkId - The index to get intents for * @param requestingUserId - The user requesting (must be a member of the index) * @param options - Pagination options * @returns Array of intent details with owner info * @throws Error if requestingUserId is not a member of the index */ getNetworkIntentsForMember(networkId: string, requestingUserId: string, options?: { limit?: number; offset?: number; }): Promise; /** * Get the caller's own active intents across a set of indexes. * Returns intents owned by `userId` that are linked (via intent_networks) * to at least one of `indexIds`. Used by network-scoped agents to honor * indexScope without falling back to global getActiveIntents (which would * include intents in indexes outside scope). * * @param userId - The intent owner (always the caller). * @param indexIds - The set of network IDs to filter on. Empty → empty result. * @returns Active intents owned by userId in any of indexIds, deduped by intent id. */ getActiveIntentsAcrossIndexes(userId: string, indexIds: string[]): Promise; /** * Update index settings. * **OWNER ONLY** - throws if user is not an owner. * * @param networkId - The index to update * @param requestingUserId - The user requesting (must be owner) * @param data - The settings to update * @returns The updated index * @throws Error if requestingUserId is not an owner */ updateIndexSettings(networkId: string, requestingUserId: string, data: UpdateIndexSettingsData): Promise; /** * Soft-delete a network (set deletedAt). * Caller must ensure network is not personal and has no other members. * * @param networkId - The network to soft-delete */ softDeleteNetwork(networkId: string): Promise; /** * Delete a user's profile (removes profile row). * Used after confirmation in chat tools. * * @param userId - User whose profile to delete */ deleteProfile(userId: string): Promise; /** * Get a user's profile including its row id (for update_user_context validation). * * @param userId - The user whose profile to fetch * @returns Profile with id, or null if not found */ getProfileByUserId(userId: string): Promise<(UserIdentity & { id: string; }) | null>; /** * Create a new index and return its record. * * @param data - Title, optional prompt, optional imageUrl, optional joinPolicy * @returns The created network with id, title, prompt, imageUrl, permissions */ createNetwork(data: { title: string; prompt?: string | null; imageUrl?: string | null; joinPolicy?: 'anyone' | 'invite_only'; }): Promise<{ id: string; title: string; prompt: string | null; imageUrl: string | null; permissions: { joinPolicy: 'anyone' | 'invite_only'; invitationLink: { code: string; } | null; }; }>; /** * Count members in an index (for delete guard). * * @param networkId - The index to count * @returns Number of members */ getNetworkMemberCount(networkId: string): Promise; /** * Add a user as a member of a network. * * @param networkId - The network to add to * @param userId - The user to add * @param role - owner | member * @returns success and optionally alreadyMember if they were already in the network */ addMemberToNetwork(networkId: string, userId: string, role: 'owner' | 'member'): Promise<{ success: boolean; alreadyMember?: boolean; }>; /** * Removes a user from an index. * Only the network owner can remove members. Cannot remove the owner. * * @param networkId - The index to remove from * @param userId - The user to remove * @returns success, or wasOwner/notMember if removal failed */ removeMemberFromIndex(networkId: string, userId: string): Promise<{ success: boolean; wasOwner?: boolean; notMember?: boolean; }>; /** * Get a HyDE document by source and strategy/lens hash. * Returns the first matching document when multiple target corpuses exist. * * @param sourceType - 'intent' | 'query' * @param sourceId - Source entity ID (e.g. intent ID, user ID) * @param strategy - Lens hash (SHA-256 of lens label) or legacy strategy name * @returns The HyDE document or null if not found */ getHydeDocument(sourceType: HydeSourceType, sourceId: string, strategy: string): Promise; /** * Get all HyDE documents for a source (all strategies). * * @param sourceType - 'intent' | 'query' * @param sourceId - Source entity ID * @returns Array of HyDE documents for that source */ getHydeDocumentsForSource(sourceType: HydeSourceType, sourceId: string): Promise; /** * Save a HyDE document (upsert by sourceType + sourceId + strategy/lensHash + targetCorpus). * * @param data - HyDE document data * @returns The saved HyDE document */ saveHydeDocument(data: CreateHydeDocumentData): Promise; /** * Delete all HyDE documents for a source (e.g. when intent archived). * * @param sourceType - 'intent' | 'query' * @param sourceId - Source entity ID * @returns Number of documents deleted */ deleteHydeDocumentsForSource(sourceType: HydeSourceType, sourceId: string): Promise; /** * Delete expired HyDE documents (expires_at <= now). Used by maintenance jobs. * * @returns Number of documents deleted */ deleteExpiredHydeDocuments(): Promise; /** * Get stale HyDE documents for refresh (e.g. createdAt < threshold). * * @param threshold - Date threshold; documents created before this are considered stale * @returns Array of stale HyDE documents */ getStaleHydeDocuments(threshold: Date): Promise; /** * Create a new opportunity. * * @param data - Opportunity creation data * @returns The created opportunity */ createOpportunity(data: CreateOpportunityData): Promise; /** * Atomically create only while every actor still has an active membership on * the actor's network. Implementations lock the membership rows through the * insert commit so concurrent removal cannot race opportunity creation. */ createOpportunityIfNetworkEligible?(data: CreateOpportunityData, eligibility: OpportunityNetworkEligibility): Promise; /** * Intent-scoped discovery persistence boundary. Implementations serialize on * normalized participant pair + trigger intent, re-check same-trigger recent * duplicates and pair-global active negotiations, then create/expire while * the existing network eligibility locks remain held. */ persistIntentScopedOpportunityIfNetworkEligible?(data: CreateOpportunityData, expireIds: string[], eligibility: OpportunityNetworkEligibility & { triggerIntentId: string; }, dedupWindowMs: number): Promise; /** * Atomically update status only while the supplied participant anchors remain * active and, when supplied, the opportunity still has `expectedStatus`. * Used for discovery dedup reactivation races. * * @param id - Opportunity ID * @param status - Target lifecycle status * @param actors - Participant anchors that must remain network-eligible * @param eligibility - Authoritative owner/network/intent scope * @param expectedStatus - Optional compare-and-set source status * @returns The updated opportunity, or null after eligibility/status drift */ updateOpportunityStatusIfNetworkEligible?(id: string, status: OpportunityStatus, actors: OpportunityActor[], eligibility: OpportunityNetworkEligibility, expectedStatus?: OpportunityStatus): Promise; /** * Get a single opportunity by ID. * * @param id - Opportunity ID * @returns The opportunity or null if not found */ getOpportunity(id: string): Promise; /** * Get multiple opportunities by ID in a single batched query. * * Returns rows in arbitrary order; callers should index by `id`. * Missing IDs are silently dropped (no error). * * @param ids - Opportunity IDs (deduplicated by the caller is fine but not required) * @returns Opportunities found */ getOpportunitiesByIds(ids: string[]): Promise; /** * Find opportunities that superseded a previous opportunity through enrichment. * Uses the existing JSONB `detection.enrichedFrom` array, so no schema-level relation is required. * Results are newest-first so callers can choose the newest visible replacement. * * @param opportunityId - Superseded opportunity ID * @returns Replacement opportunities, newest first */ findEnrichedReplacementOpportunities(opportunityId: string): Promise; /** * Resolve an opportunity identifier (full UUID or short prefix) to a full UUID. * @param idOrPrefix - Full UUID or short hex prefix * @param userId - The user ID (for visibility scoping) * @returns Resolved ID, ambiguous marker, or null if not found */ resolveOpportunityId(idOrPrefix: string, userId: string): Promise<{ id: string; } | { ambiguous: true; } | null>; /** * Get opportunities for a user (as any actor role). * * @param userId - User ID (actor userId) * @param options - Optional filters and pagination * @returns Array of opportunities */ getOpportunitiesForUser(userId: string, options?: OpportunityQueryOptions): Promise; /** * Get the live candidate pool created exactly by one intent and visible to * its recipient. Unlike selected-intent reads, this never falls back to an * actor.intent match. */ getLivePoolOpportunitiesForIntent(recipientUserId: string, intentId: string): Promise; /** * Get opportunities in an index (for index admins). * * @param networkId - Network ID * @param options - Optional filters and pagination * @returns Array of opportunities */ getOpportunitiesForNetwork(networkId: string, options?: OpportunityQueryOptions): Promise; /** * Update an opportunity's status. * * @param id - Opportunity ID * @param status - New status * @param acceptedBy - Required when `status === 'accepted'` * @param outbox - Optional IND-434 atomic outcome-capture (same-txn insert) * @returns The updated opportunity or null if not found */ updateOpportunityStatus(id: string, status: OpportunityStatus, acceptedBy?: string, outbox?: OutcomeOutbox): Promise; /** * Atomically restores a taskless negotiation attempt to its pre-negotiation status. * Serializes with exact-attempt task creation, then transitions only the exact * still-current `negotiating` version when no qualifying negotiation task exists. * * @param id - Opportunity ID * @param expectedUpdatedAt - Persistence boundary for this negotiation attempt * @param fallbackStatus - Status restored when the guarded transition succeeds * @returns The compensated opportunity, or null on a status, version, or task race */ compensateTasklessNegotiatingOpportunity(id: string, expectedUpdatedAt: Date, fallbackStatus: 'latent' | 'draft'): Promise; /** * Stamp `actedAt` on the actor matching `actorUserId` and update the * opportunity's status atomically (row-lock + JSONB merge in one txn). * * Used by `sendNode` (status → 'pending') and `updateNode` (status → * 'accepted'). The self-accept guard is enforced in the caller, not here — * this method blindly stamps. Callers must pre-check `actor.actedAt` before * invocation when the semantics require it (i.e. accepting). * * @param id - Opportunity ID * @param actorUserId - The user whose actor entry should be stamped * @param status - New opportunity status * @param acceptedBy - Required when `status === 'accepted'` * @param outbox - Optional IND-434 atomic outcome-capture (same-txn insert) * @returns The updated opportunity, or null if not found */ stampOpportunityActorAction(id: string, actorUserId: string, status: OpportunityStatus, acceptedBy?: string, outbox?: OutcomeOutbox): Promise; /** * Update the `approved` field on an opportunity's introducer actor. * Fetches the opportunity, patches the matching actor in JS, and writes * the updated actors JSONB back. Returns the updated opportunity or null. */ updateOpportunityActorApproval(id: string, introducerUserId: string, approved: boolean): Promise; /** * Create one opportunity and expire others in a single transaction. * Atomic: insert then update status to 'expired' for each id in expireIds. * Used when enriching replaces overlapping opportunities so subscribers see consistent state. * * @param data - Opportunity creation data (caller may set status when enriched) * @param expireIds - Opportunity IDs to set status to 'expired' * @returns The created opportunity and the list of opportunities that were expired */ createOpportunityAndExpireIds(data: CreateOpportunityData, expireIds: string[]): Promise<{ created: Opportunity; expired: Opportunity[]; }>; /** Eligibility-locked variant of create+expire for discovery persistence. */ createOpportunityAndExpireIdsIfNetworkEligible?(data: CreateOpportunityData, expireIds: string[], eligibility: OpportunityNetworkEligibility): Promise<{ created: Opportunity; expired: Opportunity[]; } | null>; /** * Check if an opportunity already exists between the given actors in the index (deduplication). * * @param actorIds - Array of user IDs that would be actors * @param networkId - Network ID * @returns True if a non-expired opportunity exists with exactly these actors in this network */ opportunityExistsBetweenActors(actorIds: string[], networkId: string): Promise; /** * Find opportunities whose actors contain all the given user IDs. * * The `includeIntroducers` flag controls actor matching: when false (default), matching * is restricted to non-introducer roles; when true, any role in `actors` counts. * * Index-agnostic. Ordered by updatedAt desc. * * @param actorIds - User IDs that must all appear in each returned opportunity's actors * @param options - includeIntroducers (default false), statuses (include filter), excludeStatuses (exclude filter) * @returns Matching opportunities, newest first */ findOpportunitiesByActors(actorIds: string[], options?: { includeIntroducers?: boolean; statuses?: OpportunityStatus[]; excludeStatuses?: OpportunityStatus[]; }): Promise; /** * IND-567 Rejection cool-down: returns the subset of `candidateUserIds` that * have at least one non-draft opportunity with `discovererId` whose `updatedAt` * falls within the last `windowMs` milliseconds AND whose status is `rejected` * or `stalled`. Used by the evaluation node to apply a score penalty before * sending candidates to the LLM, suppressing cross-query re-surfacing of * recently-rejected pairs. * * Optional — adapters that do not implement it return `undefined`; the graph * degrades gracefully (no penalty applied, dedup persist-node guard still fires). * * @param discovererId - User running discovery * @param candidateUserIds - Candidate user IDs to check (may be empty — return []) * @param windowMs - Look-back window in milliseconds * @returns Candidate user IDs (subset of input) with a recent rejected/stalled opp */ getRecentlyRejectedOpportunityCounterparties?(discovererId: string, candidateUserIds: string[], windowMs: number): Promise; /** * Expire opportunities referencing an intent (e.g. when intent is archived). * * @param intentId - Intent ID to match in opportunity actors * @returns Number of opportunities updated to expired */ expireOpportunitiesByIntent(intentId: string): Promise; /** * Expire opportunities for a user removed from an index. * * @param networkId - Network ID * @param userId - User ID that was removed * @returns Number of opportunities updated to expired */ expireOpportunitiesForRemovedMember(networkId: string, userId: string): Promise; /** * Expire opportunities whose expires_at <= now. Used by maintenance cron. * * @returns Number of opportunities updated to expired */ expireStaleOpportunities(): Promise; /** * Accept all sibling opportunities between the same actor pair in one transaction. * Selects opportunities where both userId and counterpartUserId are actors and status * is not accepted/expired/rejected, excludes excludeOpportunityId, then bulk-updates status to accepted. * Rolls back on any failure. * * @param userId - First actor user ID * @param counterpartUserId - Second actor user ID * @param excludeOpportunityId - Opportunity ID to exclude (the one already being accepted) * @returns IDs of opportunities that were updated to accepted */ acceptSiblingOpportunities(userId: string, counterpartUserId: string, excludeOpportunityId: string): Promise; /** Create a ghost user (unregistered contact) with empty profile. */ createGhostUser(data: { name: string; email: string; }): Promise<{ id: string; }>; /** Upsert a contact membership in the owner's personal network (index_members with permissions=['contact']). */ upsertContactMembership(ownerId: string, contactUserId: string, options?: { restore?: boolean; }): Promise; /** * Finds an existing DM conversation between two users, or creates one. * Uses a unique `dmPair` column (sorted user IDs joined by ':') to * prevent duplicate DMs under concurrency. Used by the Start Chat flow * (Plan B Task 8) to atomically surface the h2h conversation when * accepting an opportunity. */ getOrCreateDM(userA: string, userB: string, participantType?: 'user' | 'agent'): Promise<{ id: string; }>; /** * Clears hiddenAt for a user on a conversation, making it visible in their * conversation list again. Called by startChat when reusing an existing DM * that the user had previously hidden. */ unhideConversation(userId: string, conversationId: string): Promise; /** Hard-delete a contact membership from the owner's personal network. */ hardDeleteContactMembership(ownerId: string, contactUserId: string): Promise; /** Get all contact members from the owner's personal network with user details. */ getContactMembers(ownerId: string): Promise>; /** Clear a reverse opt-out (reactivate soft-deleted contact membership in another user's personal network). */ clearReverseOptOut(ownerId: string, otherUserId: string): Promise; /** * Returns the IDs of personal networks where the given user is a contact member. * Used for auto-assigning new intents to personal networks of contacts who imported this user. * * @param userId - The user whose contact memberships to look up * @returns Array of personal network IDs */ getPersonalIndexesForContact(userId: string): Promise<{ networkId: string; }[]>; /** Find a user by email. */ getUserByEmail(email: string): Promise<{ id: string; name: string; email: string; isGhost: boolean; } | null>; createPremise(input: { userId: string; assertion: PremiseAssertion; provenance: PremiseProvenance; analysis?: PremiseAnalysis; validity: PremiseValidity; embedding?: number[]; }): Promise; getPremise(premiseId: string): Promise; getPremisesForUser(userId: string, status?: 'ACTIVE' | 'RETRACTED' | 'EXPIRED'): Promise; /** * Retrieve a user's premises assigned to one of the provided networks. * Optional for older/test adapters; OpportunityGraph falls back to capped * getPremisesForUser results when unavailable. */ getPremisesForUserInNetworks?(userId: string, networkIds: string[], status?: 'ACTIVE' | 'RETRACTED' | 'EXPIRED', limit?: number): Promise; updatePremise(premiseId: string, updates: { assertion?: PremiseAssertion; analysis?: PremiseAnalysis; validity?: PremiseValidity; embedding?: number[]; status?: 'ACTIVE' | 'RETRACTED' | 'EXPIRED'; retractedAt?: Date; }): Promise; assignPremiseToNetwork(premiseId: string, networkId: string, relevancyScore: number, assignmentMetadata?: NetworkAssignmentMetadata): Promise; getPremiseNetworks(premiseId: string): Promise>; /** * Cosine similarity search against premise embeddings, scoped to shared networks. * Used by the opportunity graph's premise discovery path (path D). */ searchPremisesBySimilarity(params: { embedding: number[]; networkIds: string[]; excludeUserId: string; limit: number; minScore?: number; }): Promise>; /** * Cosine similarity search against user_context embeddings, scoped to shared networks. * Matches only per-network context rows (the global networkId-null row is never a * candidate), excluding the discovering user. Optional — lightweight-mode * context-to-context discovery no-ops when the adapter omits it. */ searchUserContextsBySimilarity?(params: { embedding: number[]; networkIds: string[]; excludeUserId: string; limit: number; minScore?: number; }): Promise>; /** * Batched version of premise similarity search. Executes one bounded DB call * for all selected source premises instead of one query per source premise. * Optional for older/test adapters; OpportunityGraph falls back to the * single-source method when unavailable. */ searchPremisesBySimilarityBatch?(params: { sources: Array<{ premiseId: string; embedding: number[]; }>; networkIds: string[]; excludeUserId: string; limitPerSource: number; minScore?: number; }): Promise>; /** * Find the single most-similar ACTIVE premise belonging to the SAME user whose * cosine similarity to `embedding` meets or exceeds `threshold`. Used by the * premise graph to skip near-duplicate premises on create. Returns null when no * active premise clears the threshold (or the user has none with an embedding). * Optional so older/test adapters can omit it — the premise graph skips dedup * when it is unavailable. */ findSimilarActivePremise?(params: { userId: string; embedding: number[]; threshold: number; }): Promise<{ premiseId: string; assertionText: string; similarity: number; } | null>; /** * Upsert a user context. Pass a concrete `networkId` for a per-network row, or * `null` for the user's single global (profile-replacing) context row. * Creates or updates the synthesized context paragraph + embedding. */ upsertUserContext(params: { userId: string; networkId: string | null; text: string; embedding: number[]; premiseHash: string; }): Promise<{ id: string; }>; /** * Get the user context for a specific user+network pair, or the global row when * `networkId` is `null`. */ getUserContext(userId: string, networkId: string | null): Promise<{ id: string; text: string; embedding: number[]; premiseHash: string; generatedAt: Date; } | null>; /** * Get user contexts for a user across all their networks. Includes the global * row (`networkId: null`) when present. */ getUserContexts(userId: string): Promise>; /** * Cosine similarity search against intent embeddings using a context embedding. * Restores the profile→intent cross-search deleted when Path B was removed. */ searchIntentsByContextEmbedding(params: { embedding: number[]; networkIds: string[]; excludeUserId: string; limit: number; minScore?: number; }): Promise>; } /** * Context-bound database for accessing the authenticated user's own resources. * Created with authUserId bound at construction; no userId parameter needed on methods. * * **NOT network-scoped**: Returns ALL of the user's own resources regardless of index. * This is critical for the IntentReconciler which needs the full picture for deduplication. * * Use via `createUserDatabase(db, authUserId)` factory function. */ export interface AgentActivitySummary { /** The requested reporting window, in hours. */ sinceHours: number; /** Number of the user's own non-archived ACTIVE intents. */ liveSignalsWatched: number; /** Opportunities created in the window and linked to one of the user's intents. */ opportunitiesSurfaced: number; /** Opportunity counts grouped by the user's own signal. */ opportunitiesBySignal: Array<{ intentId: string; title: string; count: number; }>; /** Current, non-expired questions waiting for the user, grouped by affected mode (QuestionMode values). Meta-network. */ pendingQuestionsByMode: Record; /** Questions answered by the user during the window, grouped by affected mode (QuestionMode values). Meta-network. */ answeredQuestionsByMode: Record; /** Distinct opportunity negotiations started during the window. */ negotiationsStarted: number; /** Distinct opportunity negotiations completed during the window. */ negotiationsCompleted: number; } export interface UserDatabase { /** The bound authenticated user ID */ readonly authUserId: string; /** Get the authenticated user's profile. */ getProfile(): Promise; /** Get the authenticated user's profile with row ID. */ getProfileByUserId(): Promise<(UserIdentity & { id: string; }) | null>; /** Save/update the authenticated user's profile. */ saveProfile(profile: UserIdentity): Promise; /** Delete the authenticated user's profile. */ deleteProfile(): Promise; /** Get the authenticated user's basic record (name, email, socials). */ getUser(): Promise; /** Update the authenticated user's account fields. */ updateUser(data: { name?: string; intro?: string; location?: string; onboarding?: OnboardingState; }): Promise; getUserSocials(): Promise; setUserSocials(socials: { label: string; value: string; }[]): Promise; /** Get ALL active intents for the authenticated user (not index-filtered). */ getActiveIntents(): Promise; /** * Case-insensitive substring search over the authenticated user's own * active intents. Matches against `payload` and `summary`. Most recent first. */ searchOwnIntents(q: string, limit: number): Promise>; /** Get a single intent by ID (ownership enforced). */ getIntent(intentId: string): Promise; /** Create a new intent for the authenticated user. */ createIntent(data: Omit): Promise; /** Update an intent owned by the authenticated user. */ updateIntent(intentId: string, data: UpdateIntentData): Promise; /** Archive an intent owned by the authenticated user. */ archiveIntent(intentId: string): Promise; /** Find similar intents among the user's own intents (for deduplication). */ findSimilarIntents(embedding: number[], options?: SimilarIntentSearchOptions): Promise; /** Get intent fields for indexing (own intent). */ getIntentForIndexing(intentId: string): Promise<{ id: string; payload: string; userId: string; sourceType: string | null; sourceId: string | null; } | null>; /** Associate an intent with networks. */ associateIntentWithNetworks(intentId: string, networkIds: string[]): Promise; /** Assign an intent to an index. */ assignIntentToNetwork(intentId: string, networkId: string, relevancyScore?: number, assignmentMetadata?: NetworkAssignmentMetadata): Promise; /** Unassign an intent from an index. */ unassignIntentFromIndex(intentId: string, networkId: string): Promise; /** Get network IDs for an intent. */ getNetworkIdsForIntent(intentId: string): Promise; /** Check if intent is assigned to index. */ isIntentAssignedToIndex(intentId: string, networkId: string): Promise; /** Get all network memberships for the authenticated user. */ getNetworkMemberships(): Promise; /** Get network IDs with auto-assign enabled for the authenticated user. */ getUserIndexIds(): Promise; /** Get indexes owned by the authenticated user. */ getOwnedIndexes(): Promise; /** Get a specific network membership for the authenticated user. */ getNetworkMembership(networkId: string): Promise; /** Get index + member context for the authenticated user (for auto-assign). */ getNetworkMemberContext(networkId: string): Promise; /** Get index + member context for the authenticated user without auto-assign gating. */ getNetworkAssignmentContext?(networkId: string): Promise; /** Create a new index (user becomes owner). */ createNetwork(data: { title: string; prompt?: string | null; imageUrl?: string | null; joinPolicy?: 'anyone' | 'invite_only'; }): Promise<{ id: string; title: string; prompt: string | null; imageUrl: string | null; permissions: { joinPolicy: 'anyone' | 'invite_only'; invitationLink: { code: string; } | null; }; }>; /** Update index settings (owner only). */ updateIndexSettings(networkId: string, data: UpdateIndexSettingsData): Promise; /** Soft-delete a network (owner only). */ softDeleteNetwork(networkId: string): Promise; /** Get public networks (joinPolicy 'anyone') that the user has not joined. */ getPublicIndexesNotJoined(): Promise<{ networks: Array<{ id: string; title: string; prompt: string | null; memberCount: number; owner: { id: string; name: string; avatar: string | null; } | null; }>; }>; /** Join a public network (validates joinPolicy === 'anyone'). */ joinPublicNetwork(networkId: string): Promise<{ success: boolean; alreadyMember?: boolean; }>; /** * Summarize the authenticated user's own agent activity without counterparty rows. * When `networkId` is present (a network agent's bound community), the * network-bound aggregates (opportunity and negotiation counts) are narrowed * to that community inside the query; own-signal and question aggregates are * meta-network and stay global. */ getAgentActivitySummary(input: { sinceHours: number; networkId?: string; }): Promise; /** Get opportunities where the authenticated user is an actor. */ getOpportunitiesForUser(options?: OpportunityQueryOptions): Promise; /** Get a specific opportunity (if user is an actor). */ getOpportunity(id: string): Promise; /** Update an opportunity's status (if user is an actor). acceptedBy is derived from the auth context. */ updateOpportunityStatus(id: string, status: OpportunityStatus): Promise; /** Accept sibling opportunities between the authenticated user and another actor. */ acceptSiblingOpportunities(counterpartUserId: string, excludeOpportunityId: string): Promise; /** Get a HyDE document for the user's own source. */ getHydeDocument(sourceType: HydeSourceType, sourceId: string, strategy: string): Promise; /** Get all HyDE documents for the user's own source. */ getHydeDocumentsForSource(sourceType: HydeSourceType, sourceId: string): Promise; /** Save a HyDE document for the user's own source. */ saveHydeDocument(data: CreateHydeDocumentData): Promise; /** Delete HyDE documents for the user's own source. */ deleteHydeDocumentsForSource(sourceType: HydeSourceType, sourceId: string): Promise; } /** * Context-bound database for LLM/system operations that access cross-user resources. * Created with authUserId + indexScope[]; validates membership before access. * * **Network-scoped**: All cross-user operations are restricted to users/resources * within the bound indexScope[]. This prevents the LLM from accessing arbitrary users' data. * * Use via `createSystemDatabase(db, authUserId, indexScope)` factory function. */ export interface SystemDatabase { /** The bound authenticated user ID */ readonly authUserId: string; /** The indexes the authenticated user has access to (determines cross-user scope) */ readonly indexScope: string[]; /** Get a user's profile (requires shared network membership). */ getProfile(userId: string): Promise; /** Get a user's basic record (requires shared network membership). */ getUser(userId: string): Promise; /** Get all intents in an index (cross-user, requires membership). */ getIntentsInIndex(networkId: string, options?: { limit?: number; offset?: number; }): Promise; /** Get a specific user's intents in an index (requires shared membership). */ getUserIntentsInIndex(userId: string, networkId: string): Promise; /** * Get the caller's own active intents across a set of indexes. * Returns intents owned by `userId` that are linked (via intent_networks) * to at least one of `indexIds`. Used by network-scoped agents to honor * indexScope without falling back to global getActiveIntents (which would * include intents in indexes outside scope). * * @param userId - The intent owner (always the caller). * @param indexIds - The set of network IDs to filter on. Empty → empty result. * @returns Active intents owned by userId in any of indexIds, deduped by intent id. */ getActiveIntentsAcrossIndexes(userId: string, indexIds: string[]): Promise; /** Get a single intent by ID (if in scope). */ getIntent(intentId: string): Promise; /** Find similar intents across users within the network scope. */ findSimilarIntentsInScope(embedding: number[], options?: SimilarIntentSearchOptions): Promise; /** Check if a user is a member of an index. */ isNetworkMember(networkId: string, userId: string): Promise; /** Check if a user is an owner of an index. */ isIndexOwner(networkId: string, userId: string): Promise; /** Get all members of an index (requires membership). */ getNetworkMembers(networkId: string): Promise; /** Get all members across all networks in scope (deduplicated). */ getMembersFromScope(): Promise<{ userId: Id<'users'>; name: string; avatar: string | null; }[]>; /** Add a user to an index (requires ownership or 'anyone' policy). */ addMemberToNetwork(networkId: string, userId: string, role: 'owner' | 'member'): Promise<{ success: boolean; alreadyMember?: boolean; }>; /** Remove a user from an index (requires ownership). Cannot remove the owner. */ removeMemberFromIndex(networkId: string, userId: string): Promise<{ success: boolean; wasOwner?: boolean; notMember?: boolean; }>; /** Get index info by ID with core fields (requires scope). */ getNetwork(networkId: string): Promise<{ id: string; title: string; prompt?: string | null; type?: string; metadata?: Record | null; permissions?: Record | null; } | null>; /** Get index with permissions (requires scope). */ getNetworkWithPermissions(networkId: string): Promise<{ id: string; title: string; permissions: { joinPolicy: 'anyone' | 'invite_only'; }; } | null>; /** Get member count for an index (requires scope). */ getNetworkMemberCount(networkId: string): Promise; /** Create an opportunity (cross-user). */ createOpportunity(data: CreateOpportunityData): Promise; /** Create opportunity and expire overlapping ones atomically. */ createOpportunityAndExpireIds(data: CreateOpportunityData, expireIds: string[]): Promise<{ created: Opportunity; expired: Opportunity[]; }>; /** Get an opportunity by ID (for system processing). */ getOpportunity(id: string): Promise; /** Get opportunities for an index (requires membership). */ getOpportunitiesForNetwork(networkId: string, options?: OpportunityQueryOptions): Promise; /** Update an opportunity's status (system-level). */ updateOpportunityStatus(id: string, status: OpportunityStatus, acceptedBy?: string): Promise; /** Stamp actor `actedAt` + update status atomically (system-level). */ stampOpportunityActorAction(id: string, actorUserId: string, status: OpportunityStatus, acceptedBy?: string): Promise; /** Check if opportunity exists between actors in an index. */ opportunityExistsBetweenActors(actorIds: string[], networkId: string): Promise; /** Find opportunities by actor IDs with optional include/exclude status filters. */ findOpportunitiesByActors(actorIds: string[], options?: { includeIntroducers?: boolean; statuses?: OpportunityStatus[]; excludeStatuses?: OpportunityStatus[]; }): Promise; /** Expire opportunities referencing an intent. */ expireOpportunitiesByIntent(intentId: string): Promise; /** Expire opportunities for a removed member. */ expireOpportunitiesForRemovedMember(networkId: string, userId: string): Promise; /** Expire stale opportunities (maintenance). */ expireStaleOpportunities(): Promise; /** Get a HyDE document (cross-user for matching). */ getHydeDocument(sourceType: HydeSourceType, sourceId: string, strategy: string): Promise; /** Get all HyDE documents for a source (cross-user). */ getHydeDocumentsForSource(sourceType: HydeSourceType, sourceId: string): Promise; /** Save a HyDE document (system-level). */ saveHydeDocument(data: CreateHydeDocumentData): Promise; /** Delete expired HyDE documents (maintenance). */ deleteExpiredHydeDocuments(): Promise; /** Get stale HyDE documents for refresh (maintenance). */ getStaleHydeDocuments(threshold: Date): Promise; } /** * Database interface narrowed for Profile Graph operations. * Provides full profile lifecycle: read, write, and query mode. * * Access layer: Primarily UserDatabase (user's own profile) */ export type EnrichmentGraphDatabase = Pick & { /** * Optional premise retraction support. When present, write-mode input that * disavows existing premises ("remove X", "I have nothing to do with Y") * retracts them during decomposition. Adapters without it (e.g. the scraped * enrichment adapter) skip retraction — scraped content never disavows. */ updatePremise?: Database['updatePremise']; }; /** * Database interface narrowed for Premise Graph operations. * Provides premise lifecycle: create, read, update, and network assignment. * * Access layer: UserDatabase (user's own premises) */ export type PremiseGraphDatabase = Pick; /** * Composite database interface for Chat Graph. * Includes direct ChatGraph operations plus all methods needed by * internally composed subgraphs (ProfileGraph, OpportunityGraph, IntentGraph, NetworkGraph). * * Use this type when ChatGraph orchestrates subgraphs internally. * * Access layer: Both UserDatabase + SystemDatabase (orchestrates all operations) */ export type ChatGraphCompositeDatabase = Pick & Pick; /** * Database interface for Opportunity Graph operations. * Includes prep/scope (network membership, intents, index details), persist (create, dedupe), * and CRUD operations (read, update status, send). * * Access layer: SystemDatabase (cross-user opportunity operations) */ export type OpportunityGraphDatabase = Pick & Pick; /** * Negotiation-specific query operations not covered by generic * conversation/task primitives. */ /** A user's ordinary follow-up answer stored on established shared opportunity metadata. */ export interface NegotiationUserAnswer { questionId: string; selectedOptions: string[]; freeText?: string; answeredAt: string; } export interface NegotiationPrivateConsultation { recipientUserId: string; recipientIntentId: string; kind: 'answer' | 'dismiss' | 'timeout'; selectedOptions: string[]; freeText?: string; } export interface NegotiationContinuationExecution { taskId: string; settlementId: string; opportunityId: string; userId: string; recipientIntentId: string; networkId: string; intentFingerprint: string; opportunityStatus: string; opportunityUpdatedAt: string; counterpartyUserId: string; counterpartyIntentId: string; successorTaskId: string; conversationId: string; token: string; fence: number; leaseExpiresAt: string; consultation: NegotiationPrivateConsultation; } export interface NegotiationContinuationReceipt { priorTaskId: string; settlementId: string; successorTaskId: string; fence: number; outcome: 'accepted' | 'rejected' | 'stalled' | 'waiting_for_agent' | 'input_required'; } export interface NegotiationQueries { /** Capture canonical material binding before arming an ask-user timeout. */ captureNegotiationAskUserBinding(input: { taskId: string; turnContext: Record; settlementId: string; recipientUserId: string; recipientIntentId: string; opportunityId: string; networkId: string; continuationExecution?: NegotiationContinuationExecution; }): Promise<{ version: 2; settlementId: string; recipientUserId: string; recipientIntentId: string; opportunityId: string; networkId: string; intentFingerprint: string; opportunityStatus: string; opportunityUpdatedAt: string; counterpartyUserId: string; counterpartyIntentId: string; }>; /** * Persists the full negotiation turn context (source/candidate user contexts, * seed assessment, index context, discovery query) onto the task metadata so * that polling agents can reconstruct the same context the system agent sees * in-process. Merges into `metadata.turnContext`, leaving other keys intact. * @param taskId - Task whose metadata to enrich * @param turnContext - Absolute (source/candidate) view of the negotiation context */ setTaskTurnContext(taskId: string, turnContext: Record, continuationExecution?: NegotiationContinuationExecution): Promise; /** * Merges a screen-gate decision (P2.1 shadow mode) into * `metadata.screenDecision`, leaving other metadata keys intact. Optional so * existing fakes/wireups remain valid; when absent the screen node logs the * decision and proceeds without persisting. * @param taskId - Task whose metadata to enrich * @param screenDecision - ScreenDecisionRecord (decision, evidence, mode, timing) */ setTaskScreenDecision?(taskId: string, screenDecision: Record, continuationExecution?: NegotiationContinuationExecution): Promise; /** * Merges an applied deadlock→bargaining shift record (IND-428) into * `metadata.deadlockShift`, leaving other metadata keys intact. Internal * analytics only — API surfaces must never project this key. Optional so * existing fakes/wireups remain valid; when absent the turn node logs the * shift and proceeds without persisting. * @param taskId - Task whose metadata to enrich * @param deadlockShift - DeadlockShiftRecord (run length, threshold, turn, seat, timing) */ setTaskDeadlockShift?(taskId: string, deadlockShift: Record, continuationExecution?: NegotiationContinuationExecution): Promise; /** * Returns the most-recently-created task whose metadata carries * `type: 'negotiation'` and `opportunityId: `. Returns null if no * negotiation has been started for that opportunity yet. */ getNegotiationTaskForOpportunity(opportunityId: string): Promise<{ id: string; conversationId: string; state: string; metadata: Record | null; createdAt: Date; updatedAt: Date; } | null>; /** * Returns the most-recently-created task whose metadata carries * `type: 'negotiation'` on the given conversation, regardless of * opportunityId or direction. Used by the init node's conversation-scoped * tie-break: symmetric concurrent starts carry different opportunityIds, so * the opportunity-scoped lookup above cannot see the competing task. * Optional so existing fakes/wireups remain valid; when absent the * tie-break is skipped (pre-stamp behavior). */ getLatestNegotiationTaskForConversation?(conversationId: string): Promise<{ id: string; conversationId: string; state: string; metadata: Record | null; createdAt: Date; updatedAt: Date; } | null>; /** * Returns user answers collected by the questioner system for a given * opportunity. Reads `metadata.userAnswers` from the opportunities table. * Used by the negotiation graph to inject between-session context into * continuation prompts. */ getOpportunityUserAnswers(opportunityId: string): Promise; } /** * Database dependency for the negotiation graph (A2A conversation/task/artifact * persistence). Composes generic conversation ops with negotiation-specific queries. * * Access layer: ConversationDatabaseAdapter */ export type NegotiationGraphDatabase = Pick & NegotiationQueries & { /** * Update the status of an opportunity. Called from the negotiation graph to * advance the opportunity lifecycle (negotiating -> pending/rejected/stalled). * Returns only the narrow { id, status } needed by the graph, not the full Opportunity. */ updateOpportunityStatus(id: string, status: OpportunityStatus, acceptedBy?: string, continuationExecution?: NegotiationContinuationExecution): Promise<{ id: string; status: OpportunityStatus; } | null>; /** Persists a negotiation turn message within a conversation. */ createMessage(data: { conversationId: string; senderId: string; role: 'user' | 'agent'; parts: unknown[]; taskId?: string; metadata?: Record | null; continuationExecution?: NegotiationContinuationExecution; }): Promise<{ id: string; senderId: string; role: 'user' | 'agent'; parts: unknown; createdAt: Date; }>; /** * Atomically claims an exact persisted opportunity attempt, promotes it to * negotiating, and creates its task. Returns null when the status/version is * stale or another qualifying task already owns the attempt. */ createNegotiationTaskForAttempt(input: { conversationId: string; opportunityId: string; expectedStatus: OpportunityStatus; expectedUpdatedAt: Date; metadata: Record; }): Promise<{ id: string; conversationId: string; state: string; } | null>; /** Creates a generic task to track a non-attempt-bound lifecycle. */ createTask(conversationId: string, metadata?: Record): Promise<{ id: string; conversationId: string; state: string; }>; /** * Under a deterministic settlement lock, validate the exact canceled ask_user * task and return its existing successor or create one. Never consults a * latest-task lookup. */ getOrCreateNegotiationContinuationTask(input: { priorTaskId: string; settlementId: string; conversationId: string; opportunityId: string; metadata: Record; }): Promise<{ id: string; conversationId: string; state: string; created: boolean; } | null>; /** Transitions a task to a new state (e.g. working, completed, failed). */ updateTaskState(taskId: string, state: string, statusMessage?: unknown, continuationExecution?: NegotiationContinuationExecution, parkGeneration?: string): Promise<{ id: string; conversationId: string; state: string; }>; /** Persists a negotiation outcome artifact attached to a task. */ createArtifact(data: { taskId: string; name?: string; parts: unknown[]; metadata?: Record | null; continuationExecution?: NegotiationContinuationExecution; }): Promise<{ id: string; }>; /** Lists negotiation tasks where the given user is source or candidate. */ getTasksForUser(userId: string, options?: { state?: string; }): Promise | null; createdAt: Date; updatedAt: Date; }>>; /** * Resolves each opportunity to the intent carried by the given user's actor. * Missing opportunities or actor intents are returned as null so callers can * enforce fail-closed scope filtering for legacy task metadata. */ getIntentIdsForOpportunities(opportunityIds: string[], userId: string): Promise>; /** * Batch-loads current opportunity lifecycle evidence for negotiation * narration. Implementations must omit opportunities that do not contain the * authenticated owner actor. Optional for backward-compatible hosts; callers * must treat a missing implementation as unavailable evidence, never as acceptance. */ getOpportunityLifecyclesForNegotiations?(opportunityIds: string[], ownerUserId: string): Promise>; /** Gets a specific task by ID. */ getTask(taskId: string): Promise<{ id: string; conversationId: string; state: string; metadata: Record | null; createdAt: Date; updatedAt: Date; } | null>; /** * Gets all messages for a conversation, ordered by creation time. * * `taskId` is the originating negotiation task (IND-569). Optional so legacy * hosts remain valid; when omitted, prior negotiation turns cannot be * attributed to their opportunity and degrade to the unattributed * prior-dialogue block rather than being mixed into the current opportunity. */ getMessagesForConversation(conversationId: string): Promise>; /** Gets artifacts for a task (e.g. negotiation outcome). */ getArtifactsForTask(taskId: string): Promise | null; }>>; }; /** * Database interface for opportunity controller (API). * * Access layer: Both UserDatabase + SystemDatabase (API handles auth) */ /** * Optional atomic outbox for Lens B outcome capture (IND-434). Passed to a * winning owner-action transition so the append-only outcome event is written * in the SAME transaction as the status change: * - a rolled-back action leaves NO event; * - a committed eligible action produces EXACTLY one event; * - `result.inserted` is set to true by the adapter only when a NEW row was * written (idempotent retries / duplicates set it false), so the caller can * gate post-commit mining on a genuine first insert. * * `event` is typed `unknown` (the api-side outcome-event insert row, cast by the * adapter) to keep the protocol layer free of database-schema imports. The * actor-resolution mode is a transaction-time precondition: selected-intent * captures require that exact actor intent, while unscoped captures require the * recipient to still have one unambiguous actor-intent scope. */ export interface OutcomeOutbox { event: unknown; actorResolution: 'selected_intent' | 'unique_owned_scope'; result: { inserted: boolean; }; } export type OpportunityControllerDatabase = Pick; /** * Database interface narrowed for Intent Graph operations. * Provides state population (getActiveIntents), action execution (create/update/archive), * and read operations (query intents; getIntentsInIndexForMember for network-scoped reads). * * Access layer: UserDatabase (mutations on own intents) + SystemDatabase (network-scoped reads) */ export type IntentGraphDatabase = Pick; /** * Database interface narrowed for Network Graph CRUD operations. * Handles create, read, update, delete of indexes (communities). * * Access layer: UserDatabase (CRUD on own networks and memberships) */ export type NetworkGraphDatabase = Pick; /** * Database interface narrowed for Intent Index Graph operations. * Provides intent/index context and assignment for intent–index evaluation. * (Migrated from the old NetworkGraphDatabase.) * * Access layer: UserDatabase (own intent assignment) + SystemDatabase (index context) */ export type IntentNetworkGraphDatabase = Pick; /** * Database interface narrowed for Network Membership Graph operations. * Handles CRUD for network memberships (add, list, remove members). * * Access layer: SystemDatabase (cross-user membership operations) */ export type NetworkMembershipGraphDatabase = Pick; /** * Database interface narrowed for HyDE Graph operations. * Provides HyDE document CRUD and intent lookup for refresh. * * Access layer: UserDatabase (own HyDE) + SystemDatabase (cross-user matching) */ export type HydeGraphDatabase = Pick; /** * Database interface for Radar Graph (opportunity radar view). * Load opportunities, enrich with profile/index, and support presenter context. * * Access layer: UserDatabase (own opportunities and profile) */ export type RadarGraphDatabase = Pick & Pick;