@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 +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/opportunities/opportunity.cards.d.ts +16 -3
- package/dist/internal/opportunities/opportunity.cards.js +55 -17
- package/dist/internal/opportunities/opportunity.presentation.d.ts +3 -1
- package/dist/internal/opportunities/opportunity.presentation.js +8 -1
- package/dist/platform/database/capabilities.d.ts +2 -2
- package/dist/platform/database/entities.d.ts +20 -0
- package/dist/platform/database/member-queries.d.ts +7 -0
- package/dist/platform/database/opportunity-queries.d.ts +11 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
176
|
-
|
|
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`
|