@indexnetwork/protocol 6.11.1-rc.402.1 → 6.12.1-rc.404.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/dist/negotiation/negotiation.agent.d.ts +10 -0
  3. package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
  4. package/dist/negotiation/negotiation.agent.js +26 -8
  5. package/dist/negotiation/negotiation.agent.js.map +1 -1
  6. package/dist/negotiation/negotiation.attribution.d.ts +92 -0
  7. package/dist/negotiation/negotiation.attribution.d.ts.map +1 -0
  8. package/dist/negotiation/negotiation.attribution.js +110 -0
  9. package/dist/negotiation/negotiation.attribution.js.map +1 -0
  10. package/dist/negotiation/negotiation.graph.d.ts +19 -0
  11. package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
  12. package/dist/negotiation/negotiation.graph.js +128 -6
  13. package/dist/negotiation/negotiation.graph.js.map +1 -1
  14. package/dist/negotiation/negotiation.screen.d.ts +24 -0
  15. package/dist/negotiation/negotiation.screen.d.ts.map +1 -1
  16. package/dist/negotiation/negotiation.screen.js +20 -1
  17. package/dist/negotiation/negotiation.screen.js.map +1 -1
  18. package/dist/negotiation/negotiation.state.d.ts +33 -0
  19. package/dist/negotiation/negotiation.state.d.ts.map +1 -1
  20. package/dist/negotiation/negotiation.state.js +33 -0
  21. package/dist/negotiation/negotiation.state.js.map +1 -1
  22. package/dist/opportunity/opportunity.evidence.d.ts.map +1 -1
  23. package/dist/opportunity/opportunity.evidence.js +8 -1
  24. package/dist/opportunity/opportunity.evidence.js.map +1 -1
  25. package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
  26. package/dist/opportunity/opportunity.graph.js +91 -4
  27. package/dist/opportunity/opportunity.graph.js.map +1 -1
  28. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +8 -0
  29. package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -1
  30. package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -1
  31. package/dist/shared/interfaces/database.interface.d.ts +27 -2
  32. package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
  33. package/dist/shared/interfaces/database.interface.js.map +1 -1
  34. package/dist/shared/schemas/question.schema.d.ts +37 -7
  35. package/dist/shared/schemas/question.schema.d.ts.map +1 -1
  36. package/dist/shared/schemas/question.schema.js +11 -1
  37. package/dist/shared/schemas/question.schema.js.map +1 -1
  38. package/package.json +1 -1
@@ -9,6 +9,7 @@ import { NegotiationScreener, configuredScreenMode } from "./negotiation.screen.
9
9
  import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "./negotiation.deadlock.js";
10
10
  import { protocolLogger } from "../shared/observability/protocol.logger.js";
11
11
  import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId, validateInflightAskUserFields } from './negotiation.question-safety.js';
12
+ import { attributedDialogueIsEmpty, buildSeededAttribution, combineAttributedDialogue } from './negotiation.attribution.js';
12
13
  const logger = protocolLogger("NegotiationGraph");
13
14
  const initLog = protocolLogger("NegotiationGraph:Init");
14
15
  const screenNodeLog = protocolLogger("NegotiationGraph:Screen");
@@ -100,6 +101,61 @@ export class NegotiationGraphFactory {
100
101
  return [];
101
102
  }
102
103
  };
104
+ /**
105
+ * IND-569: resolve a prior negotiation task's attribution metadata for the
106
+ * per-opportunity prior-dialogue labels. Never throws — any failure returns
107
+ * null so the turns degrade to the unattributed block instead of leaking
108
+ * into the current opportunity's context.
109
+ */
110
+ const resolveTaskAttribution = async (taskId) => {
111
+ try {
112
+ const task = await database.getTask(taskId);
113
+ if (!task)
114
+ return null;
115
+ const md = (task.metadata ?? {});
116
+ const opportunityId = typeof md.opportunityId === 'string' && md.opportunityId.length > 0 ? md.opportunityId : null;
117
+ const snapshots = Array.isArray(md.intentSnapshots) ? md.intentSnapshots : [];
118
+ const sourceIntentId = typeof md.sourceIntentId === 'string' ? md.sourceIntentId : null;
119
+ const snap = snapshots.find((s) => s && s.intentId === sourceIntentId) ?? snapshots[0];
120
+ const opportunityTitle = snap && typeof snap.title === 'string' && snap.title.trim().length > 0 ? snap.title : null;
121
+ let outcome = null;
122
+ try {
123
+ const artifacts = await database.getArtifactsForTask(taskId);
124
+ const outArtifact = artifacts.find((a) => a.name === 'negotiation-outcome');
125
+ if (outArtifact) {
126
+ const firstPart = Array.isArray(outArtifact.parts) ? outArtifact.parts[0] : undefined;
127
+ const data = firstPart?.data;
128
+ if (data && typeof data.hasOpportunity === 'boolean') {
129
+ outcome = data.hasOpportunity ? 'accepted' : (data.reason === 'screened_out' ? 'not pursued' : 'declined');
130
+ }
131
+ else if (typeof outArtifact.metadata?.continuationOutcome === 'string') {
132
+ outcome = outArtifact.metadata.continuationOutcome;
133
+ }
134
+ }
135
+ }
136
+ catch { /* outcome stays null; header degrades to "outcome unknown" */ }
137
+ const concludedAt = task.updatedAt instanceof Date
138
+ ? task.updatedAt.toISOString()
139
+ : (task.updatedAt ? new Date(task.updatedAt).toISOString() : null);
140
+ return { opportunityId, opportunityTitle, outcome, concludedAt };
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ };
146
+ /**
147
+ * IND-569: build the attributed prior dialogue passed to the screener and
148
+ * turn prompts. Combines the immutable seeded attribution (earlier + legacy
149
+ * unattributed blocks, resolved once in init) with this session's own turns
150
+ * (task-id-matched). Null when there is no seeded attribution.
151
+ */
152
+ const buildAttributedDialogue = (state) => {
153
+ if (!state.isContinuation || !state.priorAttribution)
154
+ return null;
155
+ const currentSessionTurns = turnsFromMessages(state.messages.filter((m) => m.taskId === state.taskId));
156
+ const dialogue = combineAttributedDialogue(state.priorAttribution, currentSessionTurns);
157
+ return attributedDialogueIsEmpty(dialogue) ? null : dialogue;
158
+ };
103
159
  /** Similarity query text: seed reasoning + counterparty context. */
104
160
  const memoryQueryText = (state, counterparty) => [
105
161
  state.discoveryQuery ? `Search: ${state.discoveryQuery}` : "",
@@ -301,14 +357,25 @@ export class NegotiationGraphFactory {
301
357
  return [];
302
358
  })
303
359
  : [];
304
- // Seed messages with prior turns (additive reducer appends new turns on top)
360
+ // Seed messages with prior turns (additive reducer appends new turns on top).
361
+ // taskId is preserved so the turn/screen nodes can separate this
362
+ // session's turns from seeded prior-task turns (IND-569).
305
363
  const seedMessages = isContinuation ? priorMessages.map((m) => ({
306
364
  id: m.id,
307
365
  senderId: m.senderId,
308
366
  role: 'agent',
309
367
  parts: m.parts,
310
368
  createdAt: m.createdAt,
369
+ taskId: m.taskId ?? null,
311
370
  })) : [];
371
+ // IND-569: attribute seeded prior turns to their originating opportunity
372
+ // once, up front. Earlier-opportunity and legacy unattributed blocks are
373
+ // immutable for the session; the current block is composed per turn.
374
+ const priorAttribution = isContinuation
375
+ ? await buildSeededAttribution(priorMessages
376
+ .map((m) => ({ taskId: m.taskId ?? null, turn: turnsFromMessages([m])[0] }))
377
+ .filter((e) => Boolean(e.turn)), state.opportunityId, resolveTaskAttribution)
378
+ : null;
312
379
  return {
313
380
  conversationId: conversation.id,
314
381
  taskId: task.id,
@@ -319,6 +386,7 @@ export class NegotiationGraphFactory {
319
386
  initiatorUserId,
320
387
  protocolVersion,
321
388
  priorTurnCount: priorTurns.length,
389
+ ...(priorAttribution && { priorAttribution }),
322
390
  ...(userAnswers.length > 0 && { userAnswers }),
323
391
  ...(exactContinuation?.execution.consultation
324
392
  ? { privateConsultation: exactContinuation.execution.consultation }
@@ -369,6 +437,14 @@ export class NegotiationGraphFactory {
369
437
  let decision;
370
438
  let failedOpen = false;
371
439
  let screenError;
440
+ // On continuations the seeded prior turns are the dialogue this pair
441
+ // already had. Pass them so the gate evaluates the NEW signal
442
+ // (discoveryQuery / seedAssessment) on its own merits with that context
443
+ // available — mirroring the continuation policy in negotiation.agent.ts
444
+ // ("if materially different, evaluate on its own merits").
445
+ const priorDialogue = state.isContinuation ? turnsFromMessages(state.messages) : [];
446
+ // IND-569: labeled per-opportunity attribution of that prior dialogue.
447
+ const attributedPrior = buildAttributedDialogue(state);
372
448
  try {
373
449
  const counterpartyContext = (await database.getUserContext(counterpartyUser.id, null).catch(() => null))?.text ?? "";
374
450
  decision = await screener.invoke({
@@ -381,6 +457,8 @@ export class NegotiationGraphFactory {
381
457
  ...(clientIsSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
382
458
  seedAssessment: state.seedAssessment,
383
459
  indexContext: state.indexContext,
460
+ ...(priorDialogue.length > 0 && { isContinuation: true, priorDialogue }),
461
+ ...(attributedPrior && { isContinuation: true, priorDialogueAttributed: attributedPrior }),
384
462
  });
385
463
  }
386
464
  catch (err) {
@@ -453,6 +531,9 @@ export class NegotiationGraphFactory {
453
531
  traceEmitter?.({ type: "agent_start", name: agentName });
454
532
  try {
455
533
  const history = turnsFromMessages(state.messages);
534
+ // IND-569: labeled per-opportunity attribution of prior dialogue on a
535
+ // continuation. Null on fresh runs (history renders flat, unchanged).
536
+ const attributedPrior = buildAttributedDialogue(state);
456
537
  const isSource = state.currentSpeaker === "source";
457
538
  const ownUser = isSource ? state.sourceUser : state.candidateUser;
458
539
  const otherUser = isSource ? state.candidateUser : state.sourceUser;
@@ -526,6 +607,7 @@ export class NegotiationGraphFactory {
526
607
  protocolVersion: version,
527
608
  allowedActions: [...allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable })],
528
609
  ...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
610
+ ...(attributedPrior && { priorDialogue: attributedPrior }),
529
611
  ...(ownMemory.length > 0 && { negotiatorMemory: ownMemory }),
530
612
  ...(state.privateConsultation?.recipientUserId === ownUser.id
531
613
  ? { privateConsultation: state.privateConsultation }
@@ -584,6 +666,7 @@ export class NegotiationGraphFactory {
584
666
  protocolVersion: version,
585
667
  ...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
586
668
  isContinuation: state.isContinuation,
669
+ ...(attributedPrior && { priorDialogue: attributedPrior }),
587
670
  ...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
588
671
  ...(askUserAvailable && { canAskUser: true }),
589
672
  ...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
@@ -700,6 +783,33 @@ export class NegotiationGraphFactory {
700
783
  });
701
784
  }
702
785
  }
786
+ // ─── IND-564: `withdraw` requires a prior in-task outreach ────────────
787
+ // `withdraw` semantically retracts an outreach the initiator made. In a
788
+ // continuation whose first initiator move is `withdraw` (or any withdraw
789
+ // before this task opened outreach), there is nothing to retract —
790
+ // persisting it would drop a spurious "connection withdrawn" message
791
+ // into the shared dm_pair thread as if an accepted connection were
792
+ // pulled. Seeded prior-task turns (state.messages) do NOT count as an
793
+ // in-task outreach. Map it to the quiet screen-out outcome instead: no
794
+ // message persisted, turnCount unchanged, opportunity quietly rejected
795
+ // in finalize. This is the backstop for whatever the screen gate (IND-
796
+ // 563) does not catch (screen off/shadow, or a fail-open reach_out).
797
+ //
798
+ // Exact ask_user resumes (continuationExecution) are exempt: the
799
+ // successor task is the SAME logical negotiation resumed after the
800
+ // client answered, so a post-consultation `withdraw` is a legitimate
801
+ // terminal decision, not an opening move.
802
+ if (turn.action === 'withdraw' && !state.outreachOpened && !state.continuationExecution) {
803
+ turnLog.info('negotiation_opening_withdraw_screened_out', {
804
+ taskId: state.taskId,
805
+ opportunityId: state.opportunityId || undefined,
806
+ seat,
807
+ turnCount: state.turnCount,
808
+ isContinuation: state.isContinuation,
809
+ });
810
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "screened_out: opening withdraw" });
811
+ return { lastTurn: turn, firstTurnScreenedOut: true };
812
+ }
703
813
  const parts = [{ kind: "data", data: turn }];
704
814
  const message = await database.createMessage({
705
815
  conversationId: state.conversationId,
@@ -838,6 +948,7 @@ export class NegotiationGraphFactory {
838
948
  role: "agent",
839
949
  parts: message.parts,
840
950
  createdAt: message.createdAt,
951
+ taskId: state.taskId,
841
952
  }],
842
953
  turnCount: state.turnCount + 1,
843
954
  lastTurn: turn,
@@ -867,11 +978,14 @@ export class NegotiationGraphFactory {
867
978
  role: "agent",
868
979
  parts: message.parts,
869
980
  createdAt: message.createdAt,
981
+ taskId: state.taskId,
870
982
  }],
871
983
  turnCount: state.turnCount + 1,
872
984
  currentSpeaker: (isSource ? "candidate" : "source"),
873
985
  lastTurn: turn,
874
986
  memoryBySide: { [ownSide]: ownMemory },
987
+ // Record the in-task outreach so a later `withdraw` is legal (IND-564).
988
+ ...(turn.action === 'outreach' && { outreachOpened: true }),
875
989
  ...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
876
990
  };
877
991
  }
@@ -964,7 +1078,10 @@ export class NegotiationGraphFactory {
964
1078
  const hasOpportunity = lastTurn?.action === "accept";
965
1079
  // P2.2: the client's own outreach gate declined before any turn — the
966
1080
  // negotiation never happened from the counterparty's perspective.
967
- const screenedOut = isScreenBlocked(state);
1081
+ // IND-564: an opening-move `withdraw` blocked before any message was
1082
+ // persisted is the same quiet screen-out outcome (no in-task outreach to
1083
+ // retract), reached from the turn node rather than the screen node.
1084
+ const screenedOut = isScreenBlocked(state) || state.firstTurnScreenedOut === true;
968
1085
  const atCap = !screenedOut && (state.maxTurns ?? 0) > 0 && state.turnCount >= state.maxTurns && !isTerminalAction(lastTurn?.action);
969
1086
  let agreedRoles = [];
970
1087
  if (hasOpportunity && history.length >= 2) {
@@ -983,7 +1100,7 @@ export class NegotiationGraphFactory {
983
1100
  hasOpportunity,
984
1101
  agreedRoles,
985
1102
  reasoning: screenedOut
986
- ? (state.screenDecision?.reasoning ?? "")
1103
+ ? (state.screenDecision?.reasoning ?? lastTurn?.assessment?.reasoning ?? "")
987
1104
  : (lastTurn?.assessment.reasoning ?? ""),
988
1105
  turnCount: state.turnCount,
989
1106
  ...(screenedOut
@@ -1169,9 +1286,14 @@ export class NegotiationGraphFactory {
1169
1286
  .addConditionalEdges("init", (state) => {
1170
1287
  if (state.error)
1171
1288
  return "finalize";
1172
- // Screen gate: fresh negotiations only (continuations already passed
1173
- // the gate when the dialogue opened); off disables the node entirely.
1174
- if (!state.isContinuation && configuredScreenMode() !== "off")
1289
+ // Screen gate (P2.1). Runs on fresh negotiations AND regular
1290
+ // continuations (IND-563): a new opportunity/intent reusing an existing
1291
+ // conversation must still pass the outreach gate before entering the
1292
+ // shared thread — a bad continuation match should be quietly screened
1293
+ // out, never dropped into the dm_pair. Exact ask_user resumes
1294
+ // (continuationExecution) are mid-flight and must never be re-screened.
1295
+ // `off` disables the node entirely.
1296
+ if (configuredScreenMode() !== "off" && !state.continuationExecution)
1175
1297
  return "screen";
1176
1298
  return "turn";
1177
1299
  }, { screen: "screen", turn: "turn", finalize: "finalize" })