@indexnetwork/protocol 4.5.0-rc.323.1 → 4.5.0-rc.325.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/dist/agent/agent.tools.js +1 -1
- package/dist/agent/agent.tools.js.map +1 -1
- package/dist/chat/chat.interrupt.classifier.js +1 -1
- package/dist/chat/chat.interrupt.classifier.js.map +1 -1
- package/dist/chat/chat.suggester.js +3 -3
- package/dist/chat/chat.suggester.js.map +1 -1
- package/dist/chat/chat.title.generator.js +2 -2
- package/dist/chat/chat.title.generator.js.map +1 -1
- package/dist/contact/contact.tools.js +1 -1
- package/dist/contact/contact.tools.js.map +1 -1
- package/dist/enrichment/enrichment.graph.d.ts.map +1 -1
- package/dist/enrichment/enrichment.graph.js +9 -3
- package/dist/enrichment/enrichment.graph.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/integration.tools.js +1 -1
- package/dist/integration/integration.tools.js.map +1 -1
- package/dist/intent/intent.graph.d.ts.map +1 -1
- package/dist/intent/intent.graph.js +12 -10
- package/dist/intent/intent.graph.js.map +1 -1
- package/dist/intent/intent.indexer.d.ts.map +1 -1
- package/dist/intent/intent.indexer.js +4 -3
- package/dist/intent/intent.indexer.js.map +1 -1
- package/dist/intent/intent.inferrer.d.ts.map +1 -1
- package/dist/intent/intent.inferrer.js +2 -1
- package/dist/intent/intent.inferrer.js.map +1 -1
- package/dist/intent/intent.reconciler.d.ts.map +1 -1
- package/dist/intent/intent.reconciler.js +5 -4
- package/dist/intent/intent.reconciler.js.map +1 -1
- package/dist/intent/intent.verifier.d.ts.map +1 -1
- package/dist/intent/intent.verifier.js +4 -3
- package/dist/intent/intent.verifier.js.map +1 -1
- package/dist/maintenance/maintenance.graph.d.ts.map +1 -1
- package/dist/maintenance/maintenance.graph.js +27 -5
- package/dist/maintenance/maintenance.graph.js.map +1 -1
- package/dist/mcp/mcp.server.d.ts.map +1 -1
- package/dist/mcp/mcp.server.js +5 -4
- package/dist/mcp/mcp.server.js.map +1 -1
- package/dist/negotiation/insight.generator.js +2 -2
- package/dist/negotiation/insight.generator.js.map +1 -1
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +15 -11
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.tools.js +1 -1
- package/dist/negotiation/negotiation.tools.js.map +1 -1
- package/dist/network/network.recommender.d.ts.map +1 -1
- package/dist/network/network.recommender.js +5 -4
- package/dist/network/network.recommender.js.map +1 -1
- package/dist/network/network.tools.d.ts.map +1 -1
- package/dist/network/network.tools.js +4 -2
- package/dist/network/network.tools.js.map +1 -1
- package/dist/opportunity/feed/feed.graph.d.ts +17 -1
- package/dist/opportunity/feed/feed.graph.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.graph.js +131 -27
- package/dist/opportunity/feed/feed.graph.js.map +1 -1
- package/dist/opportunity/feed/feed.state.d.ts +16 -0
- package/dist/opportunity/feed/feed.state.d.ts.map +1 -1
- package/dist/opportunity/feed/feed.state.js +10 -0
- package/dist/opportunity/feed/feed.state.js.map +1 -1
- package/dist/opportunity/opportunity.discover.d.ts +1 -1
- package/dist/opportunity/opportunity.discover.d.ts.map +1 -1
- package/dist/opportunity/opportunity.discover.js +12 -19
- package/dist/opportunity/opportunity.discover.js.map +1 -1
- package/dist/opportunity/opportunity.enricher.js +2 -2
- package/dist/opportunity/opportunity.enricher.js.map +1 -1
- package/dist/opportunity/opportunity.evaluator.d.ts.map +1 -1
- package/dist/opportunity/opportunity.evaluator.js +9 -7
- package/dist/opportunity/opportunity.evaluator.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +123 -105
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/opportunity/opportunity.introducer.js +5 -5
- package/dist/opportunity/opportunity.introducer.js.map +1 -1
- package/dist/opportunity/opportunity.persist.js +2 -2
- package/dist/opportunity/opportunity.persist.js.map +1 -1
- package/dist/opportunity/opportunity.presenter.d.ts.map +1 -1
- package/dist/opportunity/opportunity.presenter.js +4 -2
- package/dist/opportunity/opportunity.presenter.js.map +1 -1
- package/dist/opportunity/opportunity.tools.d.ts.map +1 -1
- package/dist/opportunity/opportunity.tools.js +7 -5
- package/dist/opportunity/opportunity.tools.js.map +1 -1
- package/dist/opportunity/opportunity.utils.d.ts +4 -1
- package/dist/opportunity/opportunity.utils.d.ts.map +1 -1
- package/dist/opportunity/opportunity.utils.js +42 -11
- package/dist/opportunity/opportunity.utils.js.map +1 -1
- package/dist/premise/premise.analyzer.d.ts.map +1 -1
- package/dist/premise/premise.analyzer.js +3 -2
- package/dist/premise/premise.analyzer.js.map +1 -1
- package/dist/premise/premise.decomposer.d.ts.map +1 -1
- package/dist/premise/premise.decomposer.js +12 -3
- package/dist/premise/premise.decomposer.js.map +1 -1
- package/dist/premise/premise.graph.d.ts.map +1 -1
- package/dist/premise/premise.graph.js +19 -9
- package/dist/premise/premise.graph.js.map +1 -1
- package/dist/premise/premise.indexer.d.ts.map +1 -1
- package/dist/premise/premise.indexer.js +2 -1
- package/dist/premise/premise.indexer.js.map +1 -1
- package/dist/premise/premise.tools.d.ts.map +1 -1
- package/dist/premise/premise.tools.js +8 -4
- package/dist/premise/premise.tools.js.map +1 -1
- package/dist/questioner/questioner.ask.tool.d.ts.map +1 -1
- package/dist/questioner/questioner.ask.tool.js +2 -10
- package/dist/questioner/questioner.ask.tool.js.map +1 -1
- package/dist/questioner/questioner.env.d.ts +50 -0
- package/dist/questioner/questioner.env.d.ts.map +1 -0
- package/dist/questioner/questioner.env.js +76 -0
- package/dist/questioner/questioner.env.js.map +1 -0
- package/dist/questioner/questioner.types.d.ts +7 -0
- package/dist/questioner/questioner.types.d.ts.map +1 -1
- package/dist/questioner/questioner.types.js.map +1 -1
- package/dist/shared/agent/tool.factory.d.ts.map +1 -1
- package/dist/shared/agent/tool.factory.js +4 -2
- package/dist/shared/agent/tool.factory.js.map +1 -1
- package/dist/shared/agent/tool.registry.d.ts.map +1 -1
- package/dist/shared/agent/tool.registry.js +6 -4
- package/dist/shared/agent/tool.registry.js.map +1 -1
- package/dist/shared/agent/tool.runtime.js +1 -1
- package/dist/shared/agent/tool.runtime.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/dist/shared/observability/log.d.ts +3 -2
- package/dist/shared/observability/log.d.ts.map +1 -1
- package/dist/shared/observability/log.js +72 -9
- package/dist/shared/observability/log.js.map +1 -1
- package/dist/shared/observability/protocol.logger.d.ts.map +1 -1
- package/dist/shared/observability/protocol.logger.js +5 -3
- package/dist/shared/observability/protocol.logger.js.map +1 -1
- package/package.json +1 -1
|
@@ -15,13 +15,20 @@ import { OpportunityPresenter, gatherPresenterContext } from '../opportunity.pre
|
|
|
15
15
|
import { loadNegotiationContext } from '../negotiation-context.loader.js';
|
|
16
16
|
import { HomeCategorizerAgent } from './feed.categorizer.js';
|
|
17
17
|
import { canUserSeeOpportunity, isActionableForViewer, selectByComposition } from '../opportunity.utils.js';
|
|
18
|
-
import { resolveHomeSectionIcon } from '../../shared/ui/lucide.icon-catalog.js';
|
|
18
|
+
import { resolveHomeSectionIcon, DEFAULT_HOME_SECTION_ICON } from '../../shared/ui/lucide.icon-catalog.js';
|
|
19
19
|
import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from '../opportunity.labels.js';
|
|
20
20
|
import { safeFallbackSummary } from '../opportunity.safe-presentation.js';
|
|
21
21
|
import { protocolLogger } from '../../shared/observability/protocol.logger.js';
|
|
22
22
|
import { timed } from '../../shared/observability/performance.js';
|
|
23
23
|
import { requestContext } from "../../shared/observability/request-context.js";
|
|
24
24
|
const logger = protocolLogger('HomeGraph');
|
|
25
|
+
const checkCategorizerCacheLog = protocolLogger('HomeGraph:checkCategorizerCache');
|
|
26
|
+
const cacheCategorizerResultsLog = protocolLogger('HomeGraph:cacheCategorizerResults');
|
|
27
|
+
const checkPresenterCacheLog = protocolLogger('HomeGraph:checkPresenterCache');
|
|
28
|
+
const cachePresenterResultsLog = protocolLogger('HomeGraph:cachePresenterResults');
|
|
29
|
+
const categorizeDynamicallyLog = protocolLogger('HomeGraph:categorizeDynamically');
|
|
30
|
+
const normalizeAndSortLog = protocolLogger('HomeGraph:normalizeAndSort');
|
|
31
|
+
const generateCardTextLog = protocolLogger('HomeGraph:generateCardText');
|
|
25
32
|
/** Default home-feed statuses: the lifecycle stages a viewer can act on today. */
|
|
26
33
|
export const DEFAULT_HOME_STATUSES = ['latent', 'pending'];
|
|
27
34
|
// Exhaustive registry — keys must cover every OpportunityStatus union member.
|
|
@@ -170,7 +177,42 @@ export class HomeGraphFactory {
|
|
|
170
177
|
// Do not pass conversationId: home view excludes draft opportunities (chat-only drafts).
|
|
171
178
|
const raw = await this.database.getOpportunitiesForUser(state.userId, options);
|
|
172
179
|
const visible = raw.filter((opp) => canUserSeeOpportunity(opp.actors, opp.status, state.userId));
|
|
173
|
-
|
|
180
|
+
// Actionability only gates the live statuses a viewer could act on:
|
|
181
|
+
// latent/pending cards the viewer cannot act on are noise, but
|
|
182
|
+
// terminal/internal statuses (accepted, rejected, expired, negotiating,
|
|
183
|
+
// stalled, draft) are deliberate history — when a caller explicitly
|
|
184
|
+
// requests them via `statuses`, they must pass through (they are never
|
|
185
|
+
// actionable by rule 5, so filtering them here would return nothing).
|
|
186
|
+
// The requested-status membership check is defense-in-depth: rows
|
|
187
|
+
// outside the requested set are dropped even if the adapter drifts.
|
|
188
|
+
const requestedStatuses = new Set(statuses);
|
|
189
|
+
const visibleForFeed = visible.filter((opp) => {
|
|
190
|
+
if (!requestedStatuses.has(opp.status))
|
|
191
|
+
return false;
|
|
192
|
+
if (opp.status === 'latent' || opp.status === 'pending') {
|
|
193
|
+
return isActionableForViewer(opp.actors, opp.status, state.userId);
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
const explicitStatuses = (state.statuses?.length ?? 0) > 0;
|
|
198
|
+
if (explicitStatuses) {
|
|
199
|
+
// Lifecycle view (e.g. intent radar): newest-first so counterpart
|
|
200
|
+
// dedup keeps each person's most recent state (an accepted
|
|
201
|
+
// opportunity supersedes an older pending one), no composition
|
|
202
|
+
// capping — the caller wants the full pipeline up to `limit`.
|
|
203
|
+
const newestFirst = [...visibleForFeed].sort((a, b) => safeParseDate(b.updatedAt) - safeParseDate(a.updatedAt));
|
|
204
|
+
const seenIds = new Set();
|
|
205
|
+
const dedupedByCounterpart = newestFirst.filter((opp) => {
|
|
206
|
+
const counterpartIds = getUniqueCounterpartUserIds(opp, state.userId);
|
|
207
|
+
const hasOverlap = [...counterpartIds].some((id) => seenIds.has(id));
|
|
208
|
+
if (hasOverlap)
|
|
209
|
+
return false;
|
|
210
|
+
for (const id of counterpartIds)
|
|
211
|
+
seenIds.add(id);
|
|
212
|
+
return true;
|
|
213
|
+
});
|
|
214
|
+
return { opportunities: dedupedByCounterpart.slice(0, state.limit) };
|
|
215
|
+
}
|
|
174
216
|
const sorted = [...visibleForFeed].sort((a, b) => {
|
|
175
217
|
// Connections before connector-flow so dedup claims counterpart IDs
|
|
176
218
|
// for direct connections first — prevents introducer cards from
|
|
@@ -213,7 +255,7 @@ export class HomeGraphFactory {
|
|
|
213
255
|
return { cachedCards: new Map(), uncachedOpportunities: [] };
|
|
214
256
|
}
|
|
215
257
|
if (state.noCache) {
|
|
216
|
-
|
|
258
|
+
checkPresenterCacheLog.verbose('noCache=true, skipping cache');
|
|
217
259
|
return { cachedCards: new Map(), uncachedOpportunities: opportunities };
|
|
218
260
|
}
|
|
219
261
|
try {
|
|
@@ -234,13 +276,15 @@ export class HomeGraphFactory {
|
|
|
234
276
|
const cached = results[i];
|
|
235
277
|
if (cached) {
|
|
236
278
|
const originalIndex = opportunities.indexOf(cacheable[i]);
|
|
237
|
-
|
|
279
|
+
// Stamp the live status: pre-status cache entries lack the field,
|
|
280
|
+
// and the key already guarantees it matches the current status.
|
|
281
|
+
cachedCards.set(cacheable[i].id, { ...cached, status: cacheable[i].status, _cardIndex: originalIndex });
|
|
238
282
|
}
|
|
239
283
|
else {
|
|
240
284
|
uncachedOpportunities.push(cacheable[i]);
|
|
241
285
|
}
|
|
242
286
|
}
|
|
243
|
-
|
|
287
|
+
checkPresenterCacheLog.verbose('', {
|
|
244
288
|
total: opportunities.length,
|
|
245
289
|
cacheHits: cachedCards.size,
|
|
246
290
|
cacheMisses: uncachedOpportunities.length,
|
|
@@ -248,7 +292,7 @@ export class HomeGraphFactory {
|
|
|
248
292
|
return { cachedCards, uncachedOpportunities };
|
|
249
293
|
}
|
|
250
294
|
catch (e) {
|
|
251
|
-
|
|
295
|
+
checkPresenterCacheLog.warn('cache unavailable, skipping', { error: e });
|
|
252
296
|
return { cachedCards: new Map(), uncachedOpportunities: opportunities };
|
|
253
297
|
}
|
|
254
298
|
});
|
|
@@ -257,7 +301,7 @@ export class HomeGraphFactory {
|
|
|
257
301
|
if (state.uncachedOpportunities.length > 0) {
|
|
258
302
|
return 'generate';
|
|
259
303
|
}
|
|
260
|
-
logger.verbose('
|
|
304
|
+
logger.verbose('All presenter results cached, skipping generation');
|
|
261
305
|
return 'skip';
|
|
262
306
|
};
|
|
263
307
|
const generateCardTextNode = async (state) => {
|
|
@@ -265,9 +309,9 @@ export class HomeGraphFactory {
|
|
|
265
309
|
const opportunities = state.uncachedOpportunities.length > 0
|
|
266
310
|
? state.uncachedOpportunities
|
|
267
311
|
: state.opportunities;
|
|
268
|
-
|
|
312
|
+
generateCardTextLog.verbose('entry', { opportunitiesLength: opportunities.length, userId: state.userId });
|
|
269
313
|
if (opportunities.length === 0) {
|
|
270
|
-
|
|
314
|
+
generateCardTextLog.verbose('exit', { totalOpportunities: 0, totalSections: 0 });
|
|
271
315
|
return { cards: [], agentTimings: [], meta: { totalOpportunities: 0, totalSections: 0 } };
|
|
272
316
|
}
|
|
273
317
|
const db = this.database;
|
|
@@ -329,13 +373,26 @@ export class HomeGraphFactory {
|
|
|
329
373
|
// Fallback to profile identity name when users.name is missing (e.g. profile has display name, users row does not)
|
|
330
374
|
if ((userName === 'Unknown' || !userName?.trim()) && otherActor?.userId && db.getProfile) {
|
|
331
375
|
const profile = await db.getProfile(otherActor.userId).catch((err) => {
|
|
332
|
-
logger.debug('
|
|
376
|
+
logger.debug('getProfile fallback failed', { otherActorUserId: otherActor.userId, error: err });
|
|
333
377
|
return null;
|
|
334
378
|
});
|
|
335
379
|
const profileName = profile?.identity?.name?.trim();
|
|
336
380
|
if (profileName)
|
|
337
381
|
userName = profileName;
|
|
338
382
|
}
|
|
383
|
+
// Unresolvable display counterpart (deleted user: no users row, no
|
|
384
|
+
// profile fallback). Drop the card entirely instead of rendering an
|
|
385
|
+
// "Unknown" placeholder: such cards are unusable, excluded from the
|
|
386
|
+
// presenter cache (see cachePresenterResults), and would otherwise
|
|
387
|
+
// trigger a fresh presenter LLM call on every request — a permanent
|
|
388
|
+
// cache miss that keeps the whole feed slow (~9s per load).
|
|
389
|
+
if (userName === 'Unknown' || !userName?.trim()) {
|
|
390
|
+
logger.verbose('[HomeGraph:generateCardText] dropping card with unresolvable counterpart', {
|
|
391
|
+
opportunityId: opportunity.id,
|
|
392
|
+
otherActorUserId: otherActor?.userId,
|
|
393
|
+
});
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
339
396
|
const userAvatar = otherUser?.avatar ?? null;
|
|
340
397
|
// Shared sanitization standard (UUID strip, viewer-centric rewrite,
|
|
341
398
|
// boundary truncation) — raw reasoning must never render verbatim.
|
|
@@ -360,9 +417,33 @@ export class HomeGraphFactory {
|
|
|
360
417
|
}
|
|
361
418
|
}
|
|
362
419
|
const isCounterpartGhost = otherUser?.isGhost ?? false;
|
|
420
|
+
// Skeleton presentation: return an identity-only card without the
|
|
421
|
+
// presenter LLM or negotiation-context load. Name resolution and
|
|
422
|
+
// the unresolvable-counterpart drop above still apply, so the card
|
|
423
|
+
// set matches what the follow-up full request will return.
|
|
424
|
+
if (state.presentation === 'skeleton') {
|
|
425
|
+
return {
|
|
426
|
+
opportunityId: opportunity.id,
|
|
427
|
+
status: opportunity.status,
|
|
428
|
+
userId: otherActor?.userId ?? '',
|
|
429
|
+
name: userName,
|
|
430
|
+
avatar: userAvatar,
|
|
431
|
+
mainText: '',
|
|
432
|
+
cta: '',
|
|
433
|
+
primaryActionLabel: getPrimaryActionLabel(viewerRole),
|
|
434
|
+
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
435
|
+
mutualIntentsLabel: isIntroducer ? 'Connector match' : 'Shared interests',
|
|
436
|
+
viewerRole,
|
|
437
|
+
isGhost: isCounterpartGhost,
|
|
438
|
+
...(secondPartyData ? { secondParty: secondPartyData } : {}),
|
|
439
|
+
presentationPending: true,
|
|
440
|
+
_cardIndex: cardIndex,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
363
443
|
const isPendingIntroducerFallback = isIntroducer && opportunity.status !== 'latent';
|
|
364
444
|
const fallbackCard = () => ({
|
|
365
445
|
opportunityId: opportunity.id,
|
|
446
|
+
status: opportunity.status,
|
|
366
447
|
userId: otherActor?.userId ?? '',
|
|
367
448
|
name: userName,
|
|
368
449
|
avatar: userAvatar,
|
|
@@ -421,6 +502,7 @@ export class HomeGraphFactory {
|
|
|
421
502
|
}
|
|
422
503
|
return {
|
|
423
504
|
opportunityId: opportunity.id,
|
|
505
|
+
status: opportunity.status,
|
|
424
506
|
userId: otherActor?.userId ?? '',
|
|
425
507
|
name: userName,
|
|
426
508
|
avatar: userAvatar,
|
|
@@ -442,9 +524,9 @@ export class HomeGraphFactory {
|
|
|
442
524
|
return fallbackCard();
|
|
443
525
|
}
|
|
444
526
|
}));
|
|
445
|
-
cards.push(...chunkCards);
|
|
527
|
+
cards.push(...chunkCards.filter((c) => c !== null));
|
|
446
528
|
}
|
|
447
|
-
|
|
529
|
+
generateCardTextLog.verbose('exit', { totalOpportunities: state.opportunities.length, totalSections: 0 });
|
|
448
530
|
return {
|
|
449
531
|
cards,
|
|
450
532
|
agentTimings: agentTimingsAccum,
|
|
@@ -464,6 +546,10 @@ export class HomeGraphFactory {
|
|
|
464
546
|
// Skip persisting negotiating cards — see read-side note.
|
|
465
547
|
if (!status || status === 'negotiating')
|
|
466
548
|
return Promise.resolve();
|
|
549
|
+
// Never cache skeleton cards — they carry no presenter text and
|
|
550
|
+
// would otherwise be served as "complete" for the full TTL.
|
|
551
|
+
if (card.presentationPending)
|
|
552
|
+
return Promise.resolve();
|
|
467
553
|
// Skip caching cards with unresolved names — transient DB failures
|
|
468
554
|
// would persist "Unknown" placeholders for the full TTL.
|
|
469
555
|
if (!card.name || card.name === 'Unknown')
|
|
@@ -472,7 +558,7 @@ export class HomeGraphFactory {
|
|
|
472
558
|
}));
|
|
473
559
|
}
|
|
474
560
|
catch (e) {
|
|
475
|
-
|
|
561
|
+
cachePresenterResultsLog.warn('cache write failed, continuing', { error: e });
|
|
476
562
|
}
|
|
477
563
|
// Merge cached cards into full card list
|
|
478
564
|
const allCards = [...cards];
|
|
@@ -483,7 +569,7 @@ export class HomeGraphFactory {
|
|
|
483
569
|
}
|
|
484
570
|
// Re-sort by _cardIndex to maintain original ordering
|
|
485
571
|
allCards.sort((a, b) => a._cardIndex - b._cardIndex);
|
|
486
|
-
|
|
572
|
+
cachePresenterResultsLog.verbose('', {
|
|
487
573
|
newlyCached: newCards.length,
|
|
488
574
|
totalCards: allCards.length,
|
|
489
575
|
});
|
|
@@ -498,37 +584,55 @@ export class HomeGraphFactory {
|
|
|
498
584
|
if (state.cards.length === 0) {
|
|
499
585
|
return { categoryCacheHit: false };
|
|
500
586
|
}
|
|
587
|
+
// Skeleton runs never categorize (some cards have no text to categorize
|
|
588
|
+
// and the response must stay LLM-free): emit flat sections and mark the
|
|
589
|
+
// categorizer as "hit" so the generate node and the cache write are both
|
|
590
|
+
// skipped. Chunked by MAX_ITEMS_PER_SECTION because normalizeAndSort
|
|
591
|
+
// caps every section — one giant section would silently drop cards that
|
|
592
|
+
// the follow-up full request (multiple sections) does return.
|
|
593
|
+
if (state.presentation === 'skeleton') {
|
|
594
|
+
const sectionProposals = [];
|
|
595
|
+
for (let start = 0; start < state.cards.length; start += MAX_ITEMS_PER_SECTION) {
|
|
596
|
+
sectionProposals.push({
|
|
597
|
+
id: `all-${sectionProposals.length + 1}`,
|
|
598
|
+
title: 'All matches',
|
|
599
|
+
iconName: DEFAULT_HOME_SECTION_ICON,
|
|
600
|
+
itemIndices: state.cards.slice(start, start + MAX_ITEMS_PER_SECTION).map((_, i) => start + i),
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return { sectionProposals, categoryCacheHit: true };
|
|
604
|
+
}
|
|
501
605
|
if (state.noCache) {
|
|
502
|
-
|
|
606
|
+
checkCategorizerCacheLog.verbose('noCache=true, skipping cache');
|
|
503
607
|
return { categoryCacheHit: false };
|
|
504
608
|
}
|
|
505
609
|
try {
|
|
506
610
|
const key = buildCategorizerCacheKey(state.userId, state.cards);
|
|
507
611
|
const cached = await this.cache.get(key);
|
|
508
612
|
if (cached) {
|
|
509
|
-
|
|
613
|
+
checkCategorizerCacheLog.verbose('cache hit');
|
|
510
614
|
return { sectionProposals: cached, categoryCacheHit: true };
|
|
511
615
|
}
|
|
512
|
-
|
|
616
|
+
checkCategorizerCacheLog.verbose('cache miss');
|
|
513
617
|
}
|
|
514
618
|
catch (e) {
|
|
515
|
-
|
|
619
|
+
checkCategorizerCacheLog.warn('cache unavailable, skipping', { error: e });
|
|
516
620
|
}
|
|
517
621
|
return { categoryCacheHit: false };
|
|
518
622
|
});
|
|
519
623
|
};
|
|
520
624
|
const shouldCategorize = (state) => {
|
|
521
625
|
if (state.categoryCacheHit) {
|
|
522
|
-
logger.verbose('
|
|
626
|
+
logger.verbose('Categorizer results cached, skipping');
|
|
523
627
|
return 'skip';
|
|
524
628
|
}
|
|
525
629
|
return 'categorize';
|
|
526
630
|
};
|
|
527
631
|
const categorizeDynamicallyNode = async (state) => {
|
|
528
632
|
return timed("HomeGraph.categorizeDynamically", async () => {
|
|
529
|
-
|
|
633
|
+
categorizeDynamicallyLog.verbose('entry', { cardsLength: state.cards.length });
|
|
530
634
|
if (state.cards.length === 0) {
|
|
531
|
-
|
|
635
|
+
categorizeDynamicallyLog.verbose('exit', { sectionProposalsCount: 0 });
|
|
532
636
|
return { sectionProposals: [], agentTimings: [] };
|
|
533
637
|
}
|
|
534
638
|
const agentTimingsAccum = [];
|
|
@@ -551,7 +655,7 @@ export class HomeGraphFactory {
|
|
|
551
655
|
...s,
|
|
552
656
|
itemIndices: s.itemIndices.filter((i) => i >= 0 && i < state.cards.length),
|
|
553
657
|
}));
|
|
554
|
-
|
|
658
|
+
categorizeDynamicallyLog.verbose('exit', { sectionProposalsCount: proposals.length });
|
|
555
659
|
return { sectionProposals: proposals, agentTimings: agentTimingsAccum };
|
|
556
660
|
});
|
|
557
661
|
};
|
|
@@ -563,12 +667,12 @@ export class HomeGraphFactory {
|
|
|
563
667
|
try {
|
|
564
668
|
const key = buildCategorizerCacheKey(state.userId, state.cards);
|
|
565
669
|
await this.cache.set(key, state.sectionProposals, { ttl: HOME_CACHE_TTL });
|
|
566
|
-
|
|
670
|
+
cacheCategorizerResultsLog.verbose('cached', {
|
|
567
671
|
sectionCount: state.sectionProposals.length,
|
|
568
672
|
});
|
|
569
673
|
}
|
|
570
674
|
catch (e) {
|
|
571
|
-
|
|
675
|
+
cacheCategorizerResultsLog.warn('cache write failed, continuing', { error: e });
|
|
572
676
|
}
|
|
573
677
|
return {};
|
|
574
678
|
});
|
|
@@ -577,9 +681,9 @@ export class HomeGraphFactory {
|
|
|
577
681
|
return timed("HomeGraph.normalizeAndSort", async () => {
|
|
578
682
|
const cards = state.cards;
|
|
579
683
|
const proposals = state.sectionProposals;
|
|
580
|
-
|
|
684
|
+
normalizeAndSortLog.verbose('entry', { cardsLength: cards.length, proposalsLength: proposals.length });
|
|
581
685
|
if (cards.length === 0) {
|
|
582
|
-
|
|
686
|
+
normalizeAndSortLog.verbose('exit', { totalOpportunities: 0, totalSections: 0 });
|
|
583
687
|
return { sections: [], meta: { totalOpportunities: 0, totalSections: 0 } };
|
|
584
688
|
}
|
|
585
689
|
const usedIndices = new Set();
|
|
@@ -619,7 +723,7 @@ export class HomeGraphFactory {
|
|
|
619
723
|
totalOpportunities: state.opportunities.length,
|
|
620
724
|
totalSections: sections.length,
|
|
621
725
|
};
|
|
622
|
-
|
|
726
|
+
normalizeAndSortLog.verbose('exit', { totalOpportunities: meta.totalOpportunities, totalSections: meta.totalSections });
|
|
623
727
|
return { sections, meta };
|
|
624
728
|
});
|
|
625
729
|
};
|