@indexnetwork/protocol 66.0.0-rc.641.1 → 66.0.0-rc.643.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,9 +4,15 @@
4
4
 
5
5
  ### Added
6
6
 
7
+ - `OpportunityCard` gains `priorOpportunity` (an earlier accepted opportunity, or a pending one the viewer already said yes to) and `peerChat` (the viewer's existing direct chat with the counterpart), two separate facts. The card presenter gets the prior opportunity and no longer words a known contact as new. Types `PriorOpportunity` and `PeerChat` are exported.
8
+ - `OpportunityCardsDatabase` and `OpportunityControllerDatabase` require `findPriorOpportunities(viewerId, peerIds, excludeOpportunityIds?)` and `findPeerChats(viewerId, peerIds)`, both batched and read-only.
7
9
  - `OpportunityDetection.source` gains `'introducer'`, stamped on opportunities a third party opens.
8
10
  - Protocol agent requests to OpenRouter carry `HTTP-Referer` and `X-Title` headers, so their spend is attributed apart from the hosted agent's, and a `trace` and `metadata` (`service: "protocol"`, `operation`) named by the agent key (for example `intentInferrer` or `chat`).
9
11
 
12
+ ### Fixed
13
+
14
+ - `listOpportunityCards` no longer drops accepted opportunities that rank beyond the read window.
15
+
10
16
  ## 66.0.0
11
17
 
12
18
  ### Breaking changes
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { requestContext, setRequestContextStore } from "./internal/shared/observ
6
6
  export { setLoggerFactory } from "./internal/shared/observability/log.js";
7
7
  export { setTimingWrapper } from "./internal/shared/observability/performance.js";
8
8
  export type { Cache, CacheOptions, OpportunityCache } from "./platform/discovery/cache.js";
9
- export type { CompositeDatabase, UserDatabase, SystemDatabase, OpportunityDatabase, OpportunityControllerDatabase, OpportunityCardsDatabase, IntentGraphDatabase, Opportunity, OpportunityActor, OpportunityStatus, AssignmentNetworkMembership, IntentNetworkFinalAssignmentResult, CreateOpportunityData, IntentLifecycleStatus, TransitionLifecycleResult, NegotiationContextDatabase, NegotiationContextRecord, NegotiationContextOutcome, NegotiationContextTurn, } from "./platform/database.js";
9
+ export type { CompositeDatabase, UserDatabase, SystemDatabase, OpportunityDatabase, OpportunityControllerDatabase, OpportunityCardsDatabase, IntentGraphDatabase, Opportunity, OpportunityActor, OpportunityStatus, PriorOpportunity, PeerChat, AssignmentNetworkMembership, IntentNetworkFinalAssignmentResult, CreateOpportunityData, IntentLifecycleStatus, TransitionLifecycleResult, NegotiationContextDatabase, NegotiationContextRecord, NegotiationContextOutcome, NegotiationContextTurn, } from "./platform/database.js";
10
10
  export type { Embedder, VectorStoreOption, VectorSearchResult } from "./platform/discovery/embedder.js";
11
11
  export type { IntentFollowUp } from "./platform/runtime/follow-up.js";
12
12
  export type { Scraper } from "./platform/discovery/scraper.js";
@@ -3,7 +3,7 @@
3
3
  * a time. The list loads, filters, and dedupes the viewer's opportunities,
4
4
  * serves cached cards, and presents the misses through OpportunityPresenter.
5
5
  */
6
- import type { Opportunity, OpportunityCardsDatabase, OpportunityStatus } from '../../platform/database.js';
6
+ import type { Opportunity, OpportunityCardsDatabase, OpportunityStatus, PeerChat, PriorOpportunity } from '../../platform/database.js';
7
7
  import type { OpportunityCache } from '../../platform/discovery/cache.js';
8
8
  import { OpportunityPresenter } from './opportunity.presentation.js';
9
9
  /** One opportunity with its presenter-driven display contract. */
@@ -36,6 +36,17 @@ export interface OpportunityCard {
36
36
  * empty because the presenter was skipped. Never cached.
37
37
  */
38
38
  presentationPending?: boolean;
39
+ /** An earlier opportunity with this person. Stamped live on every read, never cached. */
40
+ priorOpportunity?: PriorOpportunity | null;
41
+ /** The direct-message chat with this person. Stamped live on every read, never cached. */
42
+ peerChat?: PeerChat | null;
43
+ /** Which prior opportunity the cached copy was written against, so a changed one regenerates it. */
44
+ presentedPrior?: string | null;
45
+ }
46
+ /** What the viewer already has with one counterpart, besides the opportunity being shown. */
47
+ interface PeerContext {
48
+ priorOpportunity: PriorOpportunity | null;
49
+ peerChat: PeerChat | null;
39
50
  }
40
51
  /** Everything card presentation reaches for. */
41
52
  export interface OpportunityCardsDeps {
@@ -49,7 +60,7 @@ export interface OpportunityCardsDeps {
49
60
  * @param deps - Database, cache, and presenter.
50
61
  * @param opportunity - The opportunity to present.
51
62
  * @param viewerId - The viewing user.
52
- * @param options - `intentId` focuses viewer context on one intent; `skeleton` skips the presenter; `noCache` skips the stored-card read.
63
+ * @param options - `intentId` focuses viewer context on one intent; `skeleton` skips the presenter; `noCache` skips the stored-card read; `peerContext` is the prior opportunity and chat when the caller already loaded them.
53
64
  * @returns The card, or null when the counterpart's name cannot be resolved.
54
65
  * @throws When the presenter fails.
55
66
  */
@@ -57,6 +68,7 @@ export declare function presentOpportunityCard(deps: OpportunityCardsDeps, oppor
57
68
  intentId?: string;
58
69
  skeleton?: boolean;
59
70
  noCache?: boolean;
71
+ peerContext?: PeerContext;
60
72
  }): Promise<OpportunityCard | null>;
61
73
  /**
62
74
  * List the viewer's opportunities in the given statuses as presented cards,
@@ -67,7 +79,7 @@ export declare function presentOpportunityCard(deps: OpportunityCardsDeps, oppor
67
79
  * one. Cards whose presentation fails are dropped.
68
80
  *
69
81
  * @param deps - Database, cache, and presenter.
70
- * @param input - Viewer, statuses, optional network/intent scope, limit, cache bypass, skeleton mode.
82
+ * @param input - Viewer, statuses, optional network/intent scope, limit, cache bypass, skeleton mode. Accepted rows are read without the recency window.
71
83
  * @returns The cards and the number of opportunities selected.
72
84
  */
73
85
  export declare function listOpportunityCards(deps: OpportunityCardsDeps, input: {
@@ -82,3 +94,4 @@ export declare function listOpportunityCards(deps: OpportunityCardsDeps, input:
82
94
  cards: OpportunityCard[];
83
95
  totalOpportunities: number;
84
96
  }>;
97
+ export {};
@@ -10,6 +10,7 @@ import { canUserSeeOpportunity } from './opportunity.utils.js';
10
10
  import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from './opportunity.labels.js';
11
11
  const logger = protocolLogger('OpportunityCards');
12
12
  const PRESENTATION_CONCURRENCY = 50;
13
+ const priorKey = (prior) => prior ? `${prior.opportunityId}:${prior.status}` : null;
13
14
  const ROLE_PRIORITY = new Map([
14
15
  ['patient', 0],
15
16
  ['party', 1],
@@ -50,13 +51,32 @@ function counterpartSlotRank(opportunity, viewerId) {
50
51
  function stampViewerCommitted(card, viewerCommitted) {
51
52
  return { ...card, viewerCommitted };
52
53
  }
54
+ function stampPeerContext(card, context) {
55
+ return { ...card, ...context };
56
+ }
57
+ /** One batched read of prior opportunities and chats for the counterparts of `opportunities`. */
58
+ async function loadPeerContext(database, viewerId, opportunities) {
59
+ const peerOf = new Map(opportunities.map((opp) => [opp.id, pickDisplayCounterpartActor(opp, viewerId)?.userId]));
60
+ const peerIds = [...new Set([...peerOf.values()].filter((id) => !!id))];
61
+ const [priors, chats] = await Promise.all([
62
+ database.findPriorOpportunities(viewerId, peerIds, opportunities.map((opp) => opp.id)),
63
+ database.findPeerChats(viewerId, peerIds),
64
+ ]).catch((error) => {
65
+ logger.warn('peer context read failed, presenting without it', { error });
66
+ return [new Map(), new Map()];
67
+ });
68
+ return new Map(opportunities.map((opp) => {
69
+ const peerId = peerOf.get(opp.id);
70
+ return [opp.id, { priorOpportunity: (peerId && priors.get(peerId)) || null, peerChat: (peerId && chats.get(peerId)) || null }];
71
+ }));
72
+ }
53
73
  /**
54
74
  * Present one opportunity as a card for the viewer.
55
75
  *
56
76
  * @param deps - Database, cache, and presenter.
57
77
  * @param opportunity - The opportunity to present.
58
78
  * @param viewerId - The viewing user.
59
- * @param options - `intentId` focuses viewer context on one intent; `skeleton` skips the presenter; `noCache` skips the stored-card read.
79
+ * @param options - `intentId` focuses viewer context on one intent; `skeleton` skips the presenter; `noCache` skips the stored-card read; `peerContext` is the prior opportunity and chat when the caller already loaded them.
60
80
  * @returns The card, or null when the counterpart's name cannot be resolved.
61
81
  * @throws When the presenter fails.
62
82
  */
@@ -67,13 +87,16 @@ export async function presentOpportunityCard(deps, opportunity, viewerId, option
67
87
  if (!counterpart)
68
88
  return null;
69
89
  const viewerCommitted = viewerHasCommitted(opportunity, viewerId);
90
+ const peerContext = options.peerContext
91
+ ?? (await loadPeerContext(database, viewerId, [opportunity])).get(opportunity.id);
70
92
  const cacheable = opportunity.status !== 'negotiating';
71
93
  const cacheKey = buildOpportunityCardCacheKey(opportunity.id, viewerId, options.intentId);
72
94
  if (cacheable && !options.skeleton && !options.noCache) {
73
95
  try {
74
96
  const hit = await deps.cache.get(cacheKey);
75
- if (hit)
76
- return stampViewerCommitted({ ...hit, status: opportunity.status }, viewerCommitted);
97
+ if (hit && (hit.presentedPrior ?? null) === priorKey(peerContext.priorOpportunity)) {
98
+ return stampPeerContext(stampViewerCommitted({ ...hit, status: opportunity.status }, viewerCommitted), peerContext);
99
+ }
77
100
  }
78
101
  catch (error) {
79
102
  logger.warn('card cache read failed, presenting', { opportunityId: opportunity.id, error });
@@ -99,7 +122,7 @@ export async function presentOpportunityCard(deps, opportunity, viewerId, option
99
122
  viewerRole,
100
123
  };
101
124
  if (options.skeleton) {
102
- return stampViewerCommitted({ ...identity, mainText: '', cta: '', mutualIntentsLabel: 'Shared interests', presentationPending: true }, viewerCommitted);
125
+ return stampPeerContext(stampViewerCommitted({ ...identity, mainText: '', cta: '', mutualIntentsLabel: 'Shared interests', presentationPending: true }, viewerCommitted), peerContext);
103
126
  }
104
127
  const [context, negotiationContext] = await Promise.all([
105
128
  gatherPresenterContext(database, opportunity, viewerId, counterpart.userId, options.intentId),
@@ -109,6 +132,7 @@ export async function presentOpportunityCard(deps, opportunity, viewerId, option
109
132
  ...context,
110
133
  opportunityStatus: opportunity.status,
111
134
  ...(negotiationContext ? { negotiationContext } : {}),
135
+ ...(peerContext.priorOpportunity ? { priorOpportunity: peerContext.priorOpportunity } : {}),
112
136
  });
113
137
  const card = stampViewerCommitted({
114
138
  ...identity,
@@ -117,13 +141,14 @@ export async function presentOpportunityCard(deps, opportunity, viewerId, option
117
141
  headline: presentation.headline,
118
142
  mutualIntentsLabel: presentation.mutualIntentsLabel,
119
143
  narratorChip: { name: 'Index', text: presentation.narratorRemark },
144
+ presentedPrior: priorKey(peerContext.priorOpportunity),
120
145
  }, viewerCommitted);
121
146
  if (cacheable) {
122
147
  await deps.cache.set(cacheKey, card).catch((error) => {
123
148
  logger.warn('card cache write failed', { opportunityId: opportunity.id, error });
124
149
  });
125
150
  }
126
- return card;
151
+ return stampPeerContext(card, peerContext);
127
152
  }
128
153
  /**
129
154
  * List the viewer's opportunities in the given statuses as presented cards,
@@ -134,17 +159,31 @@ export async function presentOpportunityCard(deps, opportunity, viewerId, option
134
159
  * one. Cards whose presentation fails are dropped.
135
160
  *
136
161
  * @param deps - Database, cache, and presenter.
137
- * @param input - Viewer, statuses, optional network/intent scope, limit, cache bypass, skeleton mode.
162
+ * @param input - Viewer, statuses, optional network/intent scope, limit, cache bypass, skeleton mode. Accepted rows are read without the recency window.
138
163
  * @returns The cards and the number of opportunities selected.
139
164
  */
140
165
  export async function listOpportunityCards(deps, input) {
141
166
  const { viewerId, intentId } = input;
142
- const raw = await deps.database.getOpportunitiesForUser(viewerId, {
143
- limit: Math.min(150, Math.max(50, input.limit * 3)),
144
- statuses: input.statuses,
167
+ const scope = {
145
168
  ...(input.networkId ? { networkId: input.networkId } : {}),
146
169
  ...(intentId ? { scopeType: 'intent', scopeId: intentId } : {}),
147
- });
170
+ };
171
+ // Accepted rows are few and always win a person's slot, so they are read apart,
172
+ // not left to fall outside the newest-rows window the other statuses share.
173
+ const others = input.statuses.filter((status) => status !== 'accepted');
174
+ const [accepted, rest] = await Promise.all([
175
+ input.statuses.includes('accepted')
176
+ ? deps.database.getOpportunitiesForUser(viewerId, { statuses: ['accepted'], ...scope })
177
+ : [],
178
+ others.length
179
+ ? deps.database.getOpportunitiesForUser(viewerId, {
180
+ limit: Math.min(150, Math.max(50, input.limit * 3)),
181
+ statuses: others,
182
+ ...scope,
183
+ })
184
+ : [],
185
+ ]);
186
+ const raw = [...accepted, ...rest];
148
187
  const requested = new Set(input.statuses);
149
188
  const visible = raw
150
189
  .filter((opp) => requested.has(opp.status) && canUserSeeOpportunity(opp.actors, opp.status, viewerId))
@@ -163,6 +202,7 @@ export async function listOpportunityCards(deps, input) {
163
202
  seen.add(id);
164
203
  return true;
165
204
  }).slice(0, input.limit);
205
+ const peerContexts = await loadPeerContext(deps.database, viewerId, opportunities);
166
206
  const cacheable = (opp) => opp.status !== 'negotiating';
167
207
  const keyFor = (opp) => buildOpportunityCardCacheKey(opp.id, viewerId, intentId);
168
208
  const cacheableOpps = input.noCache ? [] : opportunities.filter(cacheable);
@@ -172,8 +212,10 @@ export async function listOpportunityCards(deps, input) {
172
212
  const hits = await deps.cache.mget(cacheableOpps.map(keyFor));
173
213
  cacheableOpps.forEach((opp, i) => {
174
214
  const hit = hits[i];
175
- if (hit)
176
- cached.set(opp.id, stampViewerCommitted({ ...hit, status: opp.status }, viewerHasCommitted(opp, viewerId)));
215
+ const peerContext = peerContexts.get(opp.id);
216
+ if (hit && (hit.presentedPrior ?? null) === priorKey(peerContext.priorOpportunity)) {
217
+ cached.set(opp.id, stampPeerContext(stampViewerCommitted({ ...hit, status: opp.status }, viewerHasCommitted(opp, viewerId)), peerContext));
218
+ }
177
219
  });
178
220
  }
179
221
  catch (error) {
@@ -192,12 +234,8 @@ export async function listOpportunityCards(deps, input) {
192
234
  intentId,
193
235
  skeleton: input.skeleton,
194
236
  noCache: input.noCache,
237
+ peerContext: peerContexts.get(opp.id),
195
238
  });
196
- if (card && !card.presentationPending && cacheable(opp)) {
197
- await deps.cache.set(keyFor(opp), card).catch((error) => {
198
- logger.warn('card cache write failed', { opportunityId: opp.id, error });
199
- });
200
- }
201
239
  return card;
202
240
  }
203
241
  catch (error) {
@@ -10,7 +10,7 @@
10
10
  * 2. OpportunityPresenter (LLM card and chat copy)
11
11
  */
12
12
  import { z } from "zod";
13
- import type { Opportunity } from "../../platform/database.js";
13
+ import type { Opportunity, PriorOpportunity } from "../../platform/database.js";
14
14
  import type { CompositeDatabase } from "../../platform/database.js";
15
15
  import type { NegotiationContext } from "./negotiation-context.loader.js";
16
16
  export interface UserInfo {
@@ -63,6 +63,8 @@ export interface CardPresenterInput extends PresenterInput {
63
63
  * transcript and outcome ground the LLM's explanation.
64
64
  */
65
65
  negotiationContext?: NegotiationContext;
66
+ /** An earlier opportunity with the same person: already connected, or the viewer already said yes. */
67
+ priorOpportunity?: Pick<PriorOpportunity, "status" | "at" | "topic">;
66
68
  }
67
69
  /** LLM-generated fields for card presentation (buttons are hardcoded by callers, not LLM-generated). */
68
70
  export declare const CardLLMSchema: z.ZodObject<{
@@ -242,8 +242,15 @@ Produce headline, personalizedSummary (2-3 sentences in "you" language), suggest
242
242
  const negotiationDirective = negotiationBlock
243
243
  ? `\nIMPORTANT: This opportunity surfaced because the agents negotiated and converged. Your personalizedSummary MUST reference at least one specific intent from the NEGOTIATION CONTEXT block below — what concern was raised, what was confirmed, what the agents agreed on. It must communicate *why this specific match* surfaced now (the negotiation that led to it), not a generic skill-complementarity line. Do not produce the generic summary every card looked like before this negotiation happened.\n`
244
244
  : "";
245
+ const prior = input.priorOpportunity;
246
+ const priorDate = prior ? new Date(prior.at).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }) : "";
247
+ const priorBlock = prior
248
+ ? `\nEARLIER CONTACT: ${prior.status === "accepted"
249
+ ? `The viewer and the other party already connected on ${priorDate}${prior.topic ? `, about: ${prior.topic}` : ""}. Say so plainly, then say what is new here.`
250
+ : `The viewer already said yes to the other party on ${priorDate}${prior.topic ? `, about: ${prior.topic}` : ""}, and they have not answered. They are not connected yet, so do not say they are. Say the viewer already said yes, then what is new here.`} Never use the words intro, intent or opportunity.\n`
251
+ : "";
245
252
  const humanContent = `
246
- ${negotiationBlock}${negotiationDirective}
253
+ ${negotiationBlock}${negotiationDirective}${priorBlock}
247
254
  VIEWER (the person seeing this opportunity):
248
255
  ${input.viewerContext}
249
256
 
@@ -19,7 +19,7 @@ export type CompositeDatabase = Pick<Database, 'getProfile' | 'openCounterpartie
19
19
  * Access layer: SystemDatabase (cross-user opportunity operations)
20
20
  */
21
21
  export type OpportunityDatabase = Pick<Database, 'getProfile' | 'getOpportunity' | 'getOpportunitiesForUser' | 'getNetwork' | 'getUser' | 'isNetworkMember' | 'isNetworkOwner' | 'getOrCreateDM' | 'updateOpportunityStatus'>;
22
- export type OpportunityControllerDatabase = Pick<Database, 'getOpportunity' | 'getOpportunitiesByIds' | 'findEnrichedReplacementOpportunities' | 'getOpportunitiesForUser' | 'getOpportunitiesForNetwork' | 'resolveOpportunityId' | 'updateOpportunityStatus' | 'createOpportunity' | 'createOpportunityAndExpireIds' | 'opportunityExistsBetweenActors' | 'findOpportunitiesByActors' | 'isNetworkOwner' | 'isNetworkMember' | 'getUser' | 'getNetwork' | 'getNetworkMemberships' | 'getProfile' | 'getActiveIntents' | 'getOrCreateDM' | 'unhideConversation'>;
22
+ export type OpportunityControllerDatabase = Pick<Database, 'getOpportunity' | 'getOpportunitiesByIds' | 'findEnrichedReplacementOpportunities' | 'getOpportunitiesForUser' | 'getOpportunitiesForNetwork' | 'resolveOpportunityId' | 'updateOpportunityStatus' | 'createOpportunity' | 'createOpportunityAndExpireIds' | 'opportunityExistsBetweenActors' | 'findOpportunitiesByActors' | 'isNetworkOwner' | 'isNetworkMember' | 'getUser' | 'getNetwork' | 'getNetworkMemberships' | 'getProfile' | 'getActiveIntents' | 'getOrCreateDM' | 'unhideConversation' | 'findPriorOpportunities' | 'findPeerChats'>;
23
23
  /**
24
24
  * Database interface narrowed for Intent Graph operations.
25
25
  * Provides state population (getActiveIntents), action execution (create/update/archive),
@@ -56,4 +56,4 @@ export type NetworkMembershipGraphDatabase = Pick<Database, 'isNetworkMember' |
56
56
  *
57
57
  * Access layer: UserDatabase (own opportunities and profile)
58
58
  */
59
- export type OpportunityCardsDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser'> & NegotiationContextDatabase;
59
+ export type OpportunityCardsDatabase = Pick<Database, 'getOpportunitiesForUser' | 'getOpportunity' | 'getProfile' | 'getActiveIntents' | 'getNetwork' | 'getUser' | 'findPriorOpportunities' | 'findPeerChats'> & NegotiationContextDatabase;
@@ -420,6 +420,26 @@ export interface UpdateNetworkSettingsData {
420
420
  joinPolicy?: 'anyone' | 'invite_only';
421
421
  }
422
422
  export type OpportunityStatus = 'negotiating' | 'pending' | 'accepted' | 'rejected' | 'expired';
423
+ /**
424
+ * The strongest other opportunity between the viewer and one peer: an accepted
425
+ * one, else a pending one the viewer committed to. `topic` is what its
426
+ * negotiation settled on, truncated; never a transcript.
427
+ */
428
+ export interface PriorOpportunity {
429
+ opportunityId: string;
430
+ status: 'accepted' | 'pending';
431
+ /** When it became accepted, or when the viewer said yes (ISO). */
432
+ at: string;
433
+ topic?: string;
434
+ }
435
+ /** The direct-message chat between the viewer and one peer. Never carries message text. */
436
+ export interface PeerChat {
437
+ conversationId: string;
438
+ lastMessage?: {
439
+ fromUserId: string;
440
+ at: string;
441
+ };
442
+ }
423
443
  /**
424
444
  * Minimal opportunity lifecycle evidence used to narrate an agent negotiation.
425
445
  * `acceptedByOwner` is true only when the authenticated owner is the persisted
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Database operations for user contexts.
3
3
  */
4
+ import type { PeerChat } from './entities.js';
4
5
  /** User-context operations. */
5
6
  export interface DatabaseMemberQueries {
6
7
  /**
@@ -13,6 +14,12 @@ export interface DatabaseMemberQueries {
13
14
  getOrCreateDM(userA: string, userB: string, participantType?: 'user' | 'agent'): Promise<{
14
15
  id: string;
15
16
  }>;
17
+ /**
18
+ * Read-only lookup of the DM chat between the viewer and each peer, with the
19
+ * latest message's sender and time. Never creates a conversation. Peers with
20
+ * no chat are absent.
21
+ */
22
+ findPeerChats(viewerId: string, peerIds: string[]): Promise<Map<string, PeerChat>>;
16
23
  /**
17
24
  * Clears hiddenAt for a user on a conversation, making it visible in their
18
25
  * conversation list again. Called by startChat when reusing an existing DM
@@ -2,7 +2,7 @@ import type { NegotiationOpening, NegotiationOpeningDecision } from '../../proto
2
2
  /**
3
3
  * Database operations for the opportunity lifecycle.
4
4
  */
5
- import type { CreateIntentCounterpartyData, CreateOpportunityData, IntentScopedOpportunityPersistenceResult, OpenedNegotiation, Opportunity, OpportunityActor, OpportunityNetworkEligibility, OpportunityQueryOptions, OpportunityStatus } from './entities.js';
5
+ import type { CreateIntentCounterpartyData, CreateOpportunityData, IntentScopedOpportunityPersistenceResult, OpenedNegotiation, Opportunity, OpportunityActor, OpportunityNetworkEligibility, OpportunityQueryOptions, OpportunityStatus, PriorOpportunity } from './entities.js';
6
6
  /** Opportunity persistence operations. */
7
7
  export interface DatabaseOpportunityQueries {
8
8
  /**
@@ -160,6 +160,16 @@ export interface DatabaseOpportunityQueries {
160
160
  statuses?: OpportunityStatus[];
161
161
  excludeStatuses?: OpportunityStatus[];
162
162
  }): Promise<Opportunity[]>;
163
+ /**
164
+ * For each peer, the strongest other opportunity with the viewer: an accepted
165
+ * one, else a pending one the viewer committed to. Peers with none are absent.
166
+ *
167
+ * @param viewerId - The viewing user
168
+ * @param peerIds - Counterpart user IDs
169
+ * @param excludeOpportunityIds - Opportunities being rendered, so one is never its own prior
170
+ * @returns Map of peer user ID to their prior opportunity
171
+ */
172
+ findPriorOpportunities(viewerId: string, peerIds: string[], excludeOpportunityIds?: string[]): Promise<Map<string, PriorOpportunity>>;
163
173
  /**
164
174
  * IND-567 Rejection cool-down: returns the subset of `candidateUserIds` that
165
175
  * have at least one non-draft opportunity with `discovererId` whose `updatedAt`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indexnetwork/protocol",
3
- "version": "66.0.0-rc.641.1",
3
+ "version": "66.0.0-rc.643.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",