@indexnetwork/protocol 4.1.2-rc.293.1 → 4.2.0-rc.295.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.
@@ -27,6 +27,12 @@ export type NegotiationContextDatabase = Pick<NegotiationGraphDatabase, 'getNego
27
27
  */
28
28
  export interface NegotiationContext {
29
29
  status: OpportunityStatus;
30
+ /**
31
+ * Conversation/task id of the A2A negotiation that produced this opportunity.
32
+ * Lets callers deep-link to the negotiation trace (e.g. `/chat/:conversationId`).
33
+ * Present whenever a negotiation task exists (i.e. context is non-null).
34
+ */
35
+ conversationId: string;
30
36
  turnCount: number;
31
37
  /** Max turns allowed for this negotiation (0 = unlimited). */
32
38
  turnCap: number;
@@ -1 +1 @@
1
- {"version":3,"file":"negotiation-context.loader.d.ts","sourceRoot":"/","sources":["opportunity/negotiation-context.loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAK/F;;;GAGG;AACH,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,wBAAwB,EACxB,kCAAkC,GAAG,4BAA4B,GAAG,qBAAqB,CAC1F,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,qDAAqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAKD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,0BAA0B,EAC9B,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,iBAAiB,GACnC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CA+BpC"}
1
+ {"version":3,"file":"negotiation-context.loader.d.ts","sourceRoot":"/","sources":["opportunity/negotiation-context.loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAK/F;;;GAGG;AACH,MAAM,MAAM,0BAA0B,GAAG,IAAI,CAC3C,wBAAwB,EACxB,kCAAkC,GAAG,4BAA4B,GAAG,qBAAqB,CAC1F,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,qDAAqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAKD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,0BAA0B,EAC9B,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,iBAAiB,GACnC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAgCpC"}
@@ -41,12 +41,13 @@ export async function loadNegotiationContext(db, opportunityId, opportunityStatu
41
41
  const turns = extractTurns(messages);
42
42
  const turnCount = turns.length;
43
43
  if (opportunityStatus === 'negotiating') {
44
- return { status: opportunityStatus, turnCount, turnCap };
44
+ return { status: opportunityStatus, conversationId: task.conversationId, turnCount, turnCap };
45
45
  }
46
46
  const artifacts = await db.getArtifactsForTask(task.id);
47
47
  const outcome = extractOutcome(artifacts);
48
48
  return {
49
49
  status: opportunityStatus,
50
+ conversationId: task.conversationId,
50
51
  turnCount,
51
52
  turnCap,
52
53
  ...(outcome ? { outcome } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"negotiation-context.loader.js","sourceRoot":"/","sources":["opportunity/negotiation-context.loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Negotiation context loader: given an opportunity, fetches the attached\n * negotiation task's transcript and outcome so the home-card presenter can\n * explain *why* the opportunity surfaced.\n *\n * For `draft`, `latent`, and `expired` opportunities, no negotiation has\n * happened (or no longer matters) so the loader returns null.\n *\n * For `negotiating` opportunities, only `turnCount` / `turnCap` are returned\n * — the presenter renders a templated chip without invoking the LLM.\n *\n * For `pending`, `stalled`, `accepted`, and `rejected` opportunities, the\n * full transcript and outcome are included so the prompt can ground its\n * explanation in concrete turn content.\n */\n\nimport type { NegotiationGraphDatabase, OpportunityStatus } from '../shared/interfaces/database.interface.js';\nimport type { NegotiationOutcome, NegotiationTurn } from '../negotiation/negotiation.state.js';\nimport { protocolLogger } from '../shared/observability/protocol.logger.js';\n\nconst logger = protocolLogger('NegotiationContextLoader');\n\n/**\n * Narrow slice of {@link NegotiationGraphDatabase} required by the loader. Kept\n * minimal so call sites can opt into a smaller surface.\n */\nexport type NegotiationContextDatabase = Pick<\n NegotiationGraphDatabase,\n 'getNegotiationTaskForOpportunity' | 'getMessagesForConversation' | 'getArtifactsForTask'\n>;\n\n/**\n * Snapshot of a negotiation surfaced to the presenter. `turns` and `outcome`\n * are only populated for post-negotiation statuses (pending/stalled/\n * accepted/rejected); `negotiating` gets only the counters.\n */\nexport interface NegotiationContext {\n status: OpportunityStatus;\n turnCount: number;\n /** Max turns allowed for this negotiation (0 = unlimited). */\n turnCap: number;\n /** Only present when status is not `negotiating`. */\n outcome?: NegotiationOutcome;\n /** Only present when status is not `negotiating`. */\n turns?: NegotiationTurn[];\n}\n\nconst STATUSES_WITH_NO_NEGOTIATION: ReadonlyArray<OpportunityStatus> = ['draft', 'latent', 'expired'];\nconst NEGOTIATION_OUTCOME_ARTIFACT_NAME = 'negotiation-outcome';\n\n/**\n * Loads the negotiation context for an opportunity.\n *\n * @param db - Narrow slice of NegotiationGraphDatabase.\n * @param opportunityId - Opportunity to load negotiation context for.\n * @param opportunityStatus - Current opportunity status. Used to gate loading\n * and to decide which fields to populate.\n * @returns NegotiationContext, or null when no meaningful negotiation exists\n * (draft/latent/expired) or when the task lookup fails.\n */\nexport async function loadNegotiationContext(\n db: NegotiationContextDatabase,\n opportunityId: string,\n opportunityStatus: OpportunityStatus,\n): Promise<NegotiationContext | null> {\n if (STATUSES_WITH_NO_NEGOTIATION.includes(opportunityStatus)) {\n return null;\n }\n\n const task = await db.getNegotiationTaskForOpportunity(opportunityId);\n if (!task) {\n logger.verbose('No negotiation task found for opportunity', { opportunityId, opportunityStatus });\n return null;\n }\n\n const turnCap = readNumber(task.metadata, 'maxTurns') ?? 0;\n\n const messages = await db.getMessagesForConversation(task.conversationId);\n const turns = extractTurns(messages);\n const turnCount = turns.length;\n\n if (opportunityStatus === 'negotiating') {\n return { status: opportunityStatus, turnCount, turnCap };\n }\n\n const artifacts = await db.getArtifactsForTask(task.id);\n const outcome = extractOutcome(artifacts);\n\n return {\n status: opportunityStatus,\n turnCount,\n turnCap,\n ...(outcome ? { outcome } : {}),\n turns,\n };\n}\n\nfunction readNumber(metadata: Record<string, unknown> | null, key: string): number | undefined {\n if (!metadata) return undefined;\n const value = metadata[key];\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction extractTurns(messages: Array<{ parts: unknown[] }>): NegotiationTurn[] {\n const turns: NegotiationTurn[] = [];\n for (const message of messages) {\n const dataPart = (message.parts as Array<{ kind?: string; data?: unknown }>).find((p) => p.kind === 'data');\n if (dataPart?.data) {\n turns.push(dataPart.data as NegotiationTurn);\n }\n }\n return turns;\n}\n\nfunction extractOutcome(\n artifacts: Array<{ name: string | null; parts: unknown[] }>,\n): NegotiationOutcome | undefined {\n const outcomeArtifact = artifacts.find((a) => a.name === NEGOTIATION_OUTCOME_ARTIFACT_NAME);\n if (!outcomeArtifact) return undefined;\n\n const dataPart = (outcomeArtifact.parts as Array<{ kind?: string; data?: unknown }>).find(\n (p) => p.kind === 'data',\n );\n return dataPart?.data as NegotiationOutcome | undefined;\n}\n"]}
1
+ {"version":3,"file":"negotiation-context.loader.js","sourceRoot":"/","sources":["opportunity/negotiation-context.loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Negotiation context loader: given an opportunity, fetches the attached\n * negotiation task's transcript and outcome so the home-card presenter can\n * explain *why* the opportunity surfaced.\n *\n * For `draft`, `latent`, and `expired` opportunities, no negotiation has\n * happened (or no longer matters) so the loader returns null.\n *\n * For `negotiating` opportunities, only `turnCount` / `turnCap` are returned\n * — the presenter renders a templated chip without invoking the LLM.\n *\n * For `pending`, `stalled`, `accepted`, and `rejected` opportunities, the\n * full transcript and outcome are included so the prompt can ground its\n * explanation in concrete turn content.\n */\n\nimport type { NegotiationGraphDatabase, OpportunityStatus } from '../shared/interfaces/database.interface.js';\nimport type { NegotiationOutcome, NegotiationTurn } from '../negotiation/negotiation.state.js';\nimport { protocolLogger } from '../shared/observability/protocol.logger.js';\n\nconst logger = protocolLogger('NegotiationContextLoader');\n\n/**\n * Narrow slice of {@link NegotiationGraphDatabase} required by the loader. Kept\n * minimal so call sites can opt into a smaller surface.\n */\nexport type NegotiationContextDatabase = Pick<\n NegotiationGraphDatabase,\n 'getNegotiationTaskForOpportunity' | 'getMessagesForConversation' | 'getArtifactsForTask'\n>;\n\n/**\n * Snapshot of a negotiation surfaced to the presenter. `turns` and `outcome`\n * are only populated for post-negotiation statuses (pending/stalled/\n * accepted/rejected); `negotiating` gets only the counters.\n */\nexport interface NegotiationContext {\n status: OpportunityStatus;\n /**\n * Conversation/task id of the A2A negotiation that produced this opportunity.\n * Lets callers deep-link to the negotiation trace (e.g. `/chat/:conversationId`).\n * Present whenever a negotiation task exists (i.e. context is non-null).\n */\n conversationId: string;\n turnCount: number;\n /** Max turns allowed for this negotiation (0 = unlimited). */\n turnCap: number;\n /** Only present when status is not `negotiating`. */\n outcome?: NegotiationOutcome;\n /** Only present when status is not `negotiating`. */\n turns?: NegotiationTurn[];\n}\n\nconst STATUSES_WITH_NO_NEGOTIATION: ReadonlyArray<OpportunityStatus> = ['draft', 'latent', 'expired'];\nconst NEGOTIATION_OUTCOME_ARTIFACT_NAME = 'negotiation-outcome';\n\n/**\n * Loads the negotiation context for an opportunity.\n *\n * @param db - Narrow slice of NegotiationGraphDatabase.\n * @param opportunityId - Opportunity to load negotiation context for.\n * @param opportunityStatus - Current opportunity status. Used to gate loading\n * and to decide which fields to populate.\n * @returns NegotiationContext, or null when no meaningful negotiation exists\n * (draft/latent/expired) or when the task lookup fails.\n */\nexport async function loadNegotiationContext(\n db: NegotiationContextDatabase,\n opportunityId: string,\n opportunityStatus: OpportunityStatus,\n): Promise<NegotiationContext | null> {\n if (STATUSES_WITH_NO_NEGOTIATION.includes(opportunityStatus)) {\n return null;\n }\n\n const task = await db.getNegotiationTaskForOpportunity(opportunityId);\n if (!task) {\n logger.verbose('No negotiation task found for opportunity', { opportunityId, opportunityStatus });\n return null;\n }\n\n const turnCap = readNumber(task.metadata, 'maxTurns') ?? 0;\n\n const messages = await db.getMessagesForConversation(task.conversationId);\n const turns = extractTurns(messages);\n const turnCount = turns.length;\n\n if (opportunityStatus === 'negotiating') {\n return { status: opportunityStatus, conversationId: task.conversationId, turnCount, turnCap };\n }\n\n const artifacts = await db.getArtifactsForTask(task.id);\n const outcome = extractOutcome(artifacts);\n\n return {\n status: opportunityStatus,\n conversationId: task.conversationId,\n turnCount,\n turnCap,\n ...(outcome ? { outcome } : {}),\n turns,\n };\n}\n\nfunction readNumber(metadata: Record<string, unknown> | null, key: string): number | undefined {\n if (!metadata) return undefined;\n const value = metadata[key];\n return typeof value === 'number' ? value : undefined;\n}\n\nfunction extractTurns(messages: Array<{ parts: unknown[] }>): NegotiationTurn[] {\n const turns: NegotiationTurn[] = [];\n for (const message of messages) {\n const dataPart = (message.parts as Array<{ kind?: string; data?: unknown }>).find((p) => p.kind === 'data');\n if (dataPart?.data) {\n turns.push(dataPart.data as NegotiationTurn);\n }\n }\n return turns;\n}\n\nfunction extractOutcome(\n artifacts: Array<{ name: string | null; parts: unknown[] }>,\n): NegotiationOutcome | undefined {\n const outcomeArtifact = artifacts.find((a) => a.name === NEGOTIATION_OUTCOME_ARTIFACT_NAME);\n if (!outcomeArtifact) return undefined;\n\n const dataPart = (outcomeArtifact.parts as Array<{ kind?: string; data?: unknown }>).find(\n (p) => p.kind === 'data',\n );\n return dataPart?.data as NegotiationOutcome | undefined;\n}\n"]}
@@ -30,8 +30,8 @@ import { renderNetworkContext } from '../shared/network/metadata.renderer.js';
30
30
  import { requestContext } from "../shared/observability/request-context.js";
31
31
  import { mergeOpportunityEvidence, withCandidateEvidence, withMatchedStrategies } from './opportunity.evidence.js';
32
32
  const logger = protocolLogger('OpportunityGraph');
33
- /** Time window for persist-node dedup. Parallel jobs arrive within seconds; 10 min catches those while allowing new opportunities for long-connected pairs. */
34
- const DEDUP_WINDOW_MS = 10 * 60 * 1000;
33
+ /** Time window for persist-node dedup. Suppresses a second opportunity with the same person while a recent one (within 30 days) is still in flight, so a person is not re-surfaced multiple times within a month (EDG-23). */
34
+ const DEDUP_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
35
35
  /** Default cap for source premises used by premise-to-premise discovery. Prevents BACKEND-5-style fan-out. */
36
36
  const DEFAULT_SOURCE_PREMISE_DISCOVERY_LIMIT = 40;
37
37
  /** Per-source cap for candidate premise matches. */
@@ -2678,7 +2678,7 @@ export class OpportunityGraphFactory {
2678
2678
  });
2679
2679
  continue;
2680
2680
  }
2681
- // Else: existing opportunity is old enough (>10 min), allow new opportunity creation
2681
+ // Else: existing opportunity is old enough (outside the 30-day dedup window), allow new opportunity creation
2682
2682
  logger.verbose('[Graph:Persist] Allowing new opportunity; existing is outside dedup window', {
2683
2683
  candidateUserId,
2684
2684
  existingStatus: existing.status,