@indexnetwork/protocol 14.3.2-rc.477.1 → 17.0.0-rc.479.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 (184) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/IMPLEMENTATION.md +50 -52
  3. package/STABILITY.md +3 -3
  4. package/dist/chat/chat.agent.js +2 -3
  5. package/dist/chat/chat.graph.d.ts +41 -1
  6. package/dist/chat/chat.graph.js +108 -127
  7. package/dist/chat/negotiator.persona.d.ts +1 -1
  8. package/dist/chat/negotiator.persona.js +1 -4
  9. package/dist/chat/negotiator.prompt.js +1 -2
  10. package/dist/chat/onboarding.persona.js +0 -1
  11. package/dist/chat/reporter.persona.js +0 -1
  12. package/dist/chat/signal.persona.js +0 -1
  13. package/dist/contacts/application/contact.tools.d.ts +6 -9
  14. package/dist/contacts/application/contact.tools.js +11 -92
  15. package/dist/contacts/application/index.d.ts +2 -14
  16. package/dist/contacts/application/index.js +2 -13
  17. package/dist/contacts/domain/contact.types.d.ts +0 -23
  18. package/dist/contacts/domain/index.d.ts +2 -6
  19. package/dist/contacts/index.d.ts +1 -1
  20. package/dist/contacts/index.js +1 -1
  21. package/dist/contacts/ports/contact.repository.port.d.ts +5 -12
  22. package/dist/contacts/ports/contact.repository.port.js +4 -6
  23. package/dist/contacts/ports/contact.tools.port.d.ts +1 -3
  24. package/dist/contacts/ports/contact.tools.port.js +1 -2
  25. package/dist/contacts/ports/index.d.ts +1 -1
  26. package/dist/contacts/ports/index.js +1 -1
  27. package/dist/discovery/hyde.frame.d.ts +4 -4
  28. package/dist/discovery/hyde.graph.d.ts +71 -12
  29. package/dist/discovery/hyde.graph.js +337 -335
  30. package/dist/discovery/lens.inferrer.d.ts +6 -6
  31. package/dist/enrichment/enrichment.graph.d.ts +127 -9
  32. package/dist/enrichment/enrichment.graph.js +633 -655
  33. package/dist/enrichment/enrichment.state.d.ts +1 -1
  34. package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
  35. package/dist/enrichment/enrichment.tools.context-read.js +353 -0
  36. package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
  37. package/dist/enrichment/enrichment.tools.context-write.js +417 -0
  38. package/dist/enrichment/enrichment.tools.d.ts +11 -1
  39. package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
  40. package/dist/enrichment/enrichment.tools.helpers.js +234 -0
  41. package/dist/enrichment/enrichment.tools.js +16 -975
  42. package/dist/index.d.ts +27 -50
  43. package/dist/index.js +17 -28
  44. package/dist/intents/application/intent.graph.d.ts +71 -67
  45. package/dist/intents/application/intent.graph.execute.d.ts +59 -0
  46. package/dist/intents/application/intent.graph.execute.js +301 -0
  47. package/dist/intents/application/intent.graph.infer.d.ts +44 -0
  48. package/dist/intents/application/intent.graph.infer.js +97 -0
  49. package/dist/intents/application/intent.graph.js +96 -888
  50. package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
  51. package/dist/intents/application/intent.graph.reconcile.js +274 -0
  52. package/dist/intents/application/intent.graph.shared.d.ts +70 -0
  53. package/dist/intents/application/intent.graph.shared.js +153 -0
  54. package/dist/intents/domain/intent.state.d.ts +1 -1
  55. package/dist/maintenance/maintenance.graph.d.ts +66 -3
  56. package/dist/maintenance/maintenance.graph.js +155 -156
  57. package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
  58. package/dist/mcp/mcp.authorization-policy.js +14 -34
  59. package/dist/mcp/mcp.server.d.ts +3 -3
  60. package/dist/mcp/mcp.server.js +14 -20
  61. package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
  62. package/dist/negotiations/application/negotiation.candidates.js +162 -0
  63. package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
  64. package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
  65. package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
  66. package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
  67. package/dist/negotiations/application/negotiation.graph.init.js +227 -0
  68. package/dist/negotiations/application/negotiation.graph.js +69 -1388
  69. package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
  70. package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
  71. package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
  72. package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
  73. package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
  74. package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
  75. package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
  76. package/dist/negotiations/domain/negotiation.state.js +0 -1
  77. package/dist/networks/application/indexer.graph.d.ts +165 -7
  78. package/dist/networks/application/indexer.graph.js +339 -388
  79. package/dist/networks/application/indexer.state.d.ts +1 -1
  80. package/dist/networks/application/membership.graph.d.ts +83 -5
  81. package/dist/networks/application/membership.graph.js +177 -207
  82. package/dist/networks/application/network.graph.d.ts +148 -5
  83. package/dist/networks/application/network.graph.js +249 -278
  84. package/dist/networks/domain/membership.state.d.ts +1 -1
  85. package/dist/networks/domain/network.state.d.ts +1 -1
  86. package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
  87. package/dist/opportunities/application/delivery-card.cache.js +2 -2
  88. package/dist/opportunities/application/index.d.ts +2 -2
  89. package/dist/opportunities/application/index.js +2 -2
  90. package/dist/opportunities/application/opportunity.evaluator.js +1 -1
  91. package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
  92. package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
  93. package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
  94. package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
  95. package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
  96. package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
  97. package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
  98. package/dist/opportunities/application/opportunity.graph.js +120 -3690
  99. package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
  100. package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
  101. package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
  102. package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
  103. package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
  104. package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
  105. package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
  106. package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
  107. package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
  108. package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
  109. package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
  110. package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
  111. package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
  112. package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
  113. package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
  114. package/dist/opportunities/application/opportunity.tools.js +28 -711
  115. package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
  116. package/dist/opportunities/application/opportunity.tools.list.js +493 -0
  117. package/dist/opportunities/domain/index.d.ts +3 -3
  118. package/dist/opportunities/domain/index.js +3 -3
  119. package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
  120. package/dist/opportunities/index.d.ts +6 -6
  121. package/dist/opportunities/index.js +4 -4
  122. package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
  123. package/dist/opportunities/radar/radar.graph.d.ts +57 -13
  124. package/dist/opportunities/radar/radar.graph.js +470 -471
  125. package/dist/premises/premise.graph.d.ts +88 -20
  126. package/dist/premises/premise.graph.js +207 -218
  127. package/dist/questions/domain/question.schema.d.ts +40 -40
  128. package/dist/shared/agent/tool.factory.js +0 -8
  129. package/dist/shared/agent/tool.helpers.d.ts +21 -45
  130. package/dist/shared/agent/tool.helpers.js +1 -2
  131. package/dist/shared/agent/tool.registry.d.ts +4 -3
  132. package/dist/shared/agent/tool.registry.js +2 -4
  133. package/dist/shared/agent/tool.runtime.js +0 -2
  134. package/dist/shared/agent/utility.tools.js +9 -13
  135. package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
  136. package/dist/shared/interfaces/database.capabilities.js +7 -0
  137. package/dist/shared/interfaces/database.entities.d.ts +533 -0
  138. package/dist/shared/interfaces/database.entities.js +10 -0
  139. package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
  140. package/dist/shared/interfaces/database.identity-queries.js +4 -0
  141. package/dist/shared/interfaces/database.interface.d.ts +15 -2277
  142. package/dist/shared/interfaces/database.interface.js +8 -0
  143. package/dist/shared/interfaces/database.member-queries.d.ts +221 -0
  144. package/dist/shared/interfaces/database.member-queries.js +4 -0
  145. package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
  146. package/dist/shared/interfaces/database.negotiation.js +26 -0
  147. package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
  148. package/dist/shared/interfaces/database.network-queries.js +4 -0
  149. package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
  150. package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
  151. package/dist/shared/interfaces/database.port.d.ts +322 -0
  152. package/dist/shared/interfaces/database.port.js +29 -0
  153. package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
  154. package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
  155. package/package.json +2 -15
  156. package/dist/contacts/application/contact.inviter.d.ts +0 -49
  157. package/dist/contacts/application/contact.inviter.js +0 -64
  158. package/dist/integrations/application/index.d.ts +0 -14
  159. package/dist/integrations/application/index.js +0 -14
  160. package/dist/integrations/application/integration.tools.d.ts +0 -27
  161. package/dist/integrations/application/integration.tools.js +0 -98
  162. package/dist/integrations/domain/index.d.ts +0 -16
  163. package/dist/integrations/domain/index.js +0 -1
  164. package/dist/integrations/domain/integration.types.d.ts +0 -51
  165. package/dist/integrations/domain/integration.types.js +0 -9
  166. package/dist/integrations/index.d.ts +0 -10
  167. package/dist/integrations/index.js +0 -8
  168. package/dist/integrations/ports/index.d.ts +0 -25
  169. package/dist/integrations/ports/index.js +0 -23
  170. package/dist/integrations/ports/integration.adapter.port.d.ts +0 -62
  171. package/dist/integrations/ports/integration.adapter.port.js +0 -15
  172. package/dist/integrations/ports/integration.importer.port.d.ts +0 -31
  173. package/dist/integrations/ports/integration.importer.port.js +0 -15
  174. package/dist/integrations/ports/integration.tools.port.d.ts +0 -25
  175. package/dist/integrations/ports/integration.tools.port.js +0 -18
  176. package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
  177. package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
  178. package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
  179. package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
  180. package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
  181. package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
  182. package/dist/opportunities/domain/opportunity.presentation.js +0 -516
  183. package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
  184. package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Negotiation graph, stage 4: persist the outcome and fan out follow-ups.
3
+ */
4
+ import { requestContext } from "../../shared/observability/request-context.js";
5
+ import { isRejectLikeAction, isTerminalAction } from "../domain/negotiation.protocol.js";
6
+ import { blocksNegotiationBeforeFirstTurn } from "./negotiation.screen.js";
7
+ import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK } from '../domain/negotiation.question-safety.js';
8
+ import { isNegotiationTurnCapReached } from "../domain/negotiation.turn-cap.js";
9
+ import { finalizeLog, turnsFromMessages } from "./negotiation.graph.shared.js";
10
+ export async function finalizeNode(state, deps) {
11
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
12
+ const emitWide = (event) => traceEmitter?.(event);
13
+ const pauseReceipt = (outcome) => state.continuationExecution
14
+ ? {
15
+ continuationReceipt: {
16
+ priorTaskId: state.continuationExecution.taskId,
17
+ settlementId: state.continuationExecution.settlementId,
18
+ successorTaskId: state.continuationExecution.successorTaskId,
19
+ fence: state.continuationExecution.fence,
20
+ outcome,
21
+ },
22
+ }
23
+ : undefined;
24
+ if (state.status === 'waiting_for_agent') {
25
+ if (state.opportunityId) {
26
+ emitWide({
27
+ type: "negotiation_outcome",
28
+ opportunityId: state.opportunityId,
29
+ outcome: "waiting_for_agent",
30
+ turnCount: state.turnCount,
31
+ isContinuation: state.isContinuation,
32
+ });
33
+ }
34
+ return pauseReceipt('waiting_for_agent') ?? {};
35
+ }
36
+ // ask_user pause: no outcome, no completed state — the task stays
37
+ // input_required until the client answers or the window expires.
38
+ if (state.status === 'input_required') {
39
+ if (state.opportunityId) {
40
+ emitWide({
41
+ type: "negotiation_outcome",
42
+ opportunityId: state.opportunityId,
43
+ outcome: "input_required",
44
+ turnCount: state.turnCount,
45
+ isContinuation: state.isContinuation,
46
+ });
47
+ }
48
+ return pauseReceipt('input_required') ?? {};
49
+ }
50
+ // Init can fail closed before an attempt owns a task. Such a rejection is
51
+ // not a completed negotiation and must not write through an empty task ID,
52
+ // create an artifact, or advance the opportunity lifecycle.
53
+ if (!state.taskId) {
54
+ finalizeLog.info('Skipping outcome persistence because no negotiation task was claimed', {
55
+ opportunityId: state.opportunityId || undefined,
56
+ error: state.error || undefined,
57
+ });
58
+ return {};
59
+ }
60
+ const history = turnsFromMessages(state.messages);
61
+ const lastTurn = state.lastTurn;
62
+ const hasOpportunity = lastTurn?.action === "accept";
63
+ // P2.2: the client's own outreach gate declined before any turn — the
64
+ // negotiation never happened from the counterparty's perspective.
65
+ // IND-564: an opening-move `withdraw` blocked before any message was
66
+ // persisted is the same quiet screen-out outcome (no in-task outreach to
67
+ // retract), reached from the turn node rather than the screen node.
68
+ // Two DISTINCT routes reach the same quiet `screened_out` outcome, and
69
+ // they disagree about who authored the decision:
70
+ // - the screen node blocked before any turn was drafted → the screen
71
+ // decision is the reason;
72
+ // - the acting agent refused on its opening turn (IND-611) → the
73
+ // withdrawing TURN is the reason.
74
+ // They are collapsed into `screenedOut` for status/lifecycle purposes but
75
+ // must stay separate when attributing `outcome.reasoning` below.
76
+ const blockedByScreenNode = blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount);
77
+ const refusedAtOpeningTurn = state.firstTurnScreenedOut === true;
78
+ const screenedOut = blockedByScreenNode || refusedAtOpeningTurn;
79
+ const atCap = !screenedOut && isNegotiationTurnCapReached(state.turnCount, state.maxTurns) && !isTerminalAction(lastTurn?.action);
80
+ let agreedRoles = [];
81
+ if (hasOpportunity && history.length >= 2) {
82
+ const acceptTurn = history[history.length - 1];
83
+ const precedingTurn = history[history.length - 2];
84
+ const accepterIsSource = state.currentSpeaker === "candidate";
85
+ const [sourceRole, candidateRole] = accepterIsSource
86
+ ? [acceptTurn.assessment.suggestedRoles.ownUser, precedingTurn.assessment.suggestedRoles.ownUser]
87
+ : [precedingTurn.assessment.suggestedRoles.ownUser, acceptTurn.assessment.suggestedRoles.ownUser];
88
+ agreedRoles = [
89
+ { userId: state.sourceUser.id, role: sourceRole },
90
+ { userId: state.candidateUser.id, role: candidateRole },
91
+ ];
92
+ }
93
+ const outcome = {
94
+ hasOpportunity,
95
+ agreedRoles,
96
+ // IND-611: attribute the reasoning to whoever actually made the
97
+ // decision. Before the turn-0 refusal path existed, `screenedOut`
98
+ // implied the screen node, so preferring `screenDecision.reasoning`
99
+ // was always right. It is now wrong for an opening-turn refusal taken
100
+ // while the screen said `reach_out`: that record argues FOR the match,
101
+ // and surfacing it as the reason the agent did NOT reach out is exactly
102
+ // the dishonesty this work removes (IND-610 renders this string in the
103
+ // owner-only gate-decision card). The screen-node branch is unchanged.
104
+ reasoning: blockedByScreenNode
105
+ ? (state.screenDecision?.reasoning ?? lastTurn?.assessment?.reasoning ?? "")
106
+ : refusedAtOpeningTurn
107
+ ? (lastTurn?.assessment?.reasoning ?? state.screenDecision?.reasoning ?? "")
108
+ : (lastTurn?.assessment.reasoning ?? ""),
109
+ turnCount: state.turnCount,
110
+ ...(screenedOut
111
+ ? { reason: "screened_out" }
112
+ : atCap
113
+ ? { reason: "turn_cap" }
114
+ : {}),
115
+ };
116
+ try {
117
+ await deps.database.updateTaskState(state.taskId, "completed", undefined, state.continuationExecution);
118
+ await deps.database.createArtifact({
119
+ taskId: state.taskId,
120
+ name: "negotiation-outcome",
121
+ parts: [{ kind: "data", data: outcome }],
122
+ metadata: {
123
+ hasOpportunity,
124
+ turnCount: state.turnCount,
125
+ ...(state.continuationExecution ? {
126
+ continuationOutcome: hasOpportunity
127
+ ? 'accepted'
128
+ : (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
129
+ } : {}),
130
+ },
131
+ ...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
132
+ });
133
+ finalizeLog.info('Session complete', {
134
+ conversationId: state.conversationId,
135
+ taskId: state.taskId,
136
+ isContinuation: state.isContinuation,
137
+ turnsAdded: state.turnCount,
138
+ priorTurnCount: state.priorTurnCount,
139
+ outcome: hasOpportunity ? 'accepted' : screenedOut ? 'screened_out' : (atCap ? 'turn_cap' : (lastTurn?.action ?? 'unknown')),
140
+ opportunityId: state.opportunityId || undefined,
141
+ });
142
+ if (state.opportunityId) {
143
+ // screened_out → 'rejected': quiet terminal status (hidden from
144
+ // default lists), never 'stalled' — with zero turns the generic
145
+ // mapping would misfile the client's own gate decision.
146
+ const nextStatus = lastTurn?.action === 'accept'
147
+ ? 'pending'
148
+ : (screenedOut || isRejectLikeAction(lastTurn?.action))
149
+ ? 'rejected'
150
+ : 'stalled';
151
+ await deps.database.updateOpportunityStatus(state.opportunityId, nextStatus, undefined, state.continuationExecution).catch((err) => {
152
+ finalizeLog.error("Failed to update opportunity status", { opportunityId: state.opportunityId, nextStatus, error: err });
153
+ });
154
+ }
155
+ }
156
+ catch (err) {
157
+ finalizeLog.error("Failed to persist outcome", { error: err });
158
+ }
159
+ if (state.opportunityId) {
160
+ const emittedOutcome = hasOpportunity
161
+ ? "accepted"
162
+ : screenedOut
163
+ ? "screened_out"
164
+ : atCap
165
+ ? "turn_cap"
166
+ : state.error && /timeout/i.test(state.error)
167
+ ? "timed_out"
168
+ : "rejected_stalled";
169
+ emitWide({
170
+ type: "negotiation_outcome",
171
+ opportunityId: state.opportunityId,
172
+ outcome: emittedOutcome,
173
+ turnCount: state.turnCount,
174
+ isContinuation: state.isContinuation,
175
+ turnsAdded: state.turnCount,
176
+ priorTurnCount: state.priorTurnCount,
177
+ ...(outcome.reasoning && { reasoning: outcome.reasoning }),
178
+ ...(hasOpportunity && agreedRoles.length >= 2 && {
179
+ agreedRoles: {
180
+ ownUser: agreedRoles[0]?.role,
181
+ otherUser: agreedRoles[1]?.role,
182
+ },
183
+ }),
184
+ });
185
+ }
186
+ if (state.consultationPolicyReason) {
187
+ // Stable, content-free terminal funnel telemetry. This executes only
188
+ // after a fenced continuation has reached its terminal outcome.
189
+ finalizeLog.info('negotiation_consultation_policy', {
190
+ stage: 'terminal_outcome',
191
+ reason: state.consultationPolicyReason,
192
+ outcome: hasOpportunity ? 'accepted' : (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
193
+ });
194
+ emitWide({
195
+ type: 'negotiation_consultation_policy',
196
+ stage: 'terminal_outcome',
197
+ reason: state.consultationPolicyReason,
198
+ outcome: hasOpportunity ? 'accepted' : (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
199
+ });
200
+ }
201
+ // Enqueue post-negotiation reflection (P5.2 memory write path) — fire
202
+ // and forget: a reflection failure must never affect the outcome. Only
203
+ // sessions that actually exchanged turns teach anything; init/turn
204
+ // errors with turnCount 0 are skipped.
205
+ if (deps.reflectEnqueue && state.turnCount > 0 && !state.continuationExecution) {
206
+ deps.reflectEnqueue({
207
+ negotiationId: state.taskId,
208
+ conversationId: state.conversationId,
209
+ ...(state.opportunityId && { opportunityId: state.opportunityId }),
210
+ sourceUser: {
211
+ id: state.sourceUser.id,
212
+ ...(state.sourceUser.profile.name && { name: state.sourceUser.profile.name }),
213
+ ...(state.sourceUser.profile.bio && { bio: state.sourceUser.profile.bio }),
214
+ },
215
+ candidateUser: {
216
+ id: state.candidateUser.id,
217
+ ...(state.candidateUser.profile.name && { name: state.candidateUser.profile.name }),
218
+ ...(state.candidateUser.profile.bio && { bio: state.candidateUser.profile.bio }),
219
+ },
220
+ initiatorUserId: state.initiatorUserId ?? state.sourceUser.id,
221
+ outcome: { hasOpportunity, reasoning: outcome.reasoning, turnCount: state.turnCount },
222
+ }).catch((err) => finalizeLog.error('Failed to enqueue negotiation reflection', {
223
+ taskId: state.taskId,
224
+ error: err,
225
+ }));
226
+ }
227
+ // Enqueue question generation for stalled/capped negotiations (not accepted or explicitly rejected).
228
+ // Require turnCount > 0 so early init/turn errors don't enqueue with empty context.
229
+ if (!hasOpportunity && !isRejectLikeAction(lastTurn?.action) && state.turnCount > 0 && state.opportunityId && state.sourceIntentId && state.indexContext.networkId && deps.questionerEnqueue && !state.continuationExecution) {
230
+ const stallReason = atCap
231
+ ? 'turn_cap'
232
+ : (state.error && /timeout/i.test(state.error))
233
+ ? 'timeout'
234
+ : 'stalled';
235
+ const userContext = (await deps.database.getUserContext(state.sourceUser.id, null))?.text ?? '';
236
+ const sourceIntent = state.sourceUser.intents.find((intent) => intent.id === state.sourceIntentId);
237
+ deps.questionerEnqueue({
238
+ mode: 'negotiation',
239
+ purpose: 'stalled_followup',
240
+ userId: state.sourceUser.id,
241
+ sourceType: 'opportunity',
242
+ sourceId: state.opportunityId,
243
+ negotiation: {
244
+ purpose: 'stalled_followup',
245
+ recipientUserId: state.sourceUser.id,
246
+ recipientIntentId: state.sourceIntentId,
247
+ opportunityId: state.opportunityId,
248
+ taskId: state.taskId,
249
+ networkId: state.indexContext.networkId,
250
+ },
251
+ context: {
252
+ negotiationId: state.taskId,
253
+ counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
254
+ indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
255
+ outcomeReason: stallReason,
256
+ recipientIntent: sourceIntent
257
+ ? `${sourceIntent.title}: ${sourceIntent.description}`
258
+ : 'the signal attached to this match',
259
+ userContext,
260
+ },
261
+ }).catch((err) => finalizeLog.error('Failed to enqueue negotiation question generation', {
262
+ opportunityId: state.opportunityId,
263
+ error: err,
264
+ }));
265
+ }
266
+ const terminalReceipt = state.continuationExecution
267
+ ? {
268
+ continuationReceipt: {
269
+ priorTaskId: state.continuationExecution.taskId,
270
+ settlementId: state.continuationExecution.settlementId,
271
+ successorTaskId: state.continuationExecution.successorTaskId,
272
+ fence: state.continuationExecution.fence,
273
+ outcome: hasOpportunity
274
+ ? 'accepted'
275
+ : (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
276
+ },
277
+ }
278
+ : undefined;
279
+ return { outcome, status: 'completed', ...terminalReceipt };
280
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Negotiation graph, stage 1: claim the conversation, task and seat.
3
+ */
4
+ import { type NegotiationConsultationReason } from "../domain/negotiation.consultation-policy.js";
5
+ import type { NegotiationProtocolVersion } from "../../shared/schemas/negotiation-state.schema.js";
6
+ import type { NegotiationGraphDeps, NegotiationState } from "./negotiation.graph.shared.js";
7
+ export declare function initNode(state: NegotiationState, deps: NegotiationGraphDeps): Promise<{
8
+ error: string;
9
+ } | {
10
+ messages?: {
11
+ id: string;
12
+ senderId: string;
13
+ role: "agent";
14
+ parts: unknown[];
15
+ createdAt: Date;
16
+ taskId: string | null;
17
+ }[] | undefined;
18
+ privateConsultation?: import("../../shared/interfaces/database.negotiation.js").NegotiationPrivateConsultation | undefined;
19
+ userAnswers?: import("../../shared/interfaces/database.negotiation.js").NegotiationUserAnswer[] | undefined;
20
+ priorAttribution?: import("../negotiation.attribution.js").SeededAttribution | undefined;
21
+ conversationId: string;
22
+ taskId: string;
23
+ currentSpeaker: "source" | "candidate";
24
+ turnCount: number;
25
+ maxTurns: number;
26
+ isContinuation: boolean;
27
+ initiatorUserId: string;
28
+ protocolVersion: NegotiationProtocolVersion;
29
+ priorTurnCount: number;
30
+ error?: undefined;
31
+ } | {
32
+ messages?: {
33
+ id: string;
34
+ senderId: string;
35
+ role: "agent";
36
+ parts: unknown[];
37
+ createdAt: Date;
38
+ taskId: string | null;
39
+ }[] | undefined;
40
+ consultationPolicyReason: NegotiationConsultationReason;
41
+ privateConsultation?: import("../../shared/interfaces/database.negotiation.js").NegotiationPrivateConsultation | undefined;
42
+ userAnswers?: import("../../shared/interfaces/database.negotiation.js").NegotiationUserAnswer[] | undefined;
43
+ priorAttribution?: import("../negotiation.attribution.js").SeededAttribution | undefined;
44
+ conversationId: string;
45
+ taskId: string;
46
+ currentSpeaker: "source" | "candidate";
47
+ turnCount: number;
48
+ maxTurns: number;
49
+ isContinuation: boolean;
50
+ initiatorUserId: string;
51
+ protocolVersion: NegotiationProtocolVersion;
52
+ priorTurnCount: number;
53
+ error?: undefined;
54
+ }>;
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Negotiation graph, stage 1: claim the conversation, task and seat.
3
+ */
4
+ import { configuredProtocolVersion, readProtocolVersion } from "../domain/negotiation.protocol.js";
5
+ import { buildIntentSnapshots } from "../domain/negotiation.intent-snapshot-provenance.js";
6
+ import { holdsNegotiationConversationLock } from "../domain/negotiation.task-lock-policy.js";
7
+ import { expectedNegotiationSpeaker } from "../domain/negotiation.expected-speaker.js";
8
+ import { buildSeededAttribution } from '../negotiation.attribution.js';
9
+ import { initLog, resolveTaskAttribution, turnsFromMessages } from "./negotiation.graph.shared.js";
10
+ export async function initNode(state, deps) {
11
+ try {
12
+ // Exact continuations reuse the prior conversation and preclaimed
13
+ // successor; they must not create any mutable state before the fence.
14
+ const agentIdA = `agent:${state.sourceUser.id}`;
15
+ const agentIdB = `agent:${state.candidateUser.id}`;
16
+ const execution = state.continuationExecution;
17
+ const conversation = execution
18
+ ? { id: execution.conversationId }
19
+ : await deps.database.getOrCreateDM(agentIdA, agentIdB, 'agent');
20
+ // --- Lock gate: check for an active task on this conversation ---
21
+ const priorMessages = await deps.database.getMessagesForConversation(conversation.id);
22
+ if (Boolean(state.resumeFromTaskId) !== Boolean(state.continuationSettlementId)
23
+ || Boolean(state.resumeFromTaskId) !== Boolean(execution))
24
+ return { error: 'invalid continuation correlation' };
25
+ const exactContinuation = state.resumeFromTaskId && state.continuationSettlementId && execution
26
+ ? { taskId: state.resumeFromTaskId, settlementId: state.continuationSettlementId, execution }
27
+ : null;
28
+ const priorTask = exactContinuation
29
+ ? await deps.database.getTask(exactContinuation.taskId)
30
+ : state.opportunityId
31
+ ? await deps.database.getNegotiationTaskForOpportunity(state.opportunityId)
32
+ : null;
33
+ const claimedSuccessor = exactContinuation
34
+ ? await deps.database.getTask(exactContinuation.execution.successorTaskId)
35
+ : null;
36
+ if (exactContinuation) {
37
+ const settlement = priorTask?.metadata?.questionSettlement;
38
+ const storedExecution = claimedSuccessor?.metadata?.continuationExecution;
39
+ if (!priorTask
40
+ || priorTask.conversationId !== conversation.id
41
+ || priorTask.state !== 'canceled'
42
+ || priorTask.metadata?.opportunityId !== state.opportunityId
43
+ || settlement?.settlementId !== exactContinuation.settlementId
44
+ || settlement?.taskId !== exactContinuation.taskId
45
+ || !claimedSuccessor
46
+ || claimedSuccessor.conversationId !== conversation.id
47
+ || storedExecution?.token !== exactContinuation.execution.token
48
+ || storedExecution?.fence !== exactContinuation.execution.fence
49
+ || storedExecution?.status !== 'claimed')
50
+ return { error: 'invalid exact continuation task' };
51
+ }
52
+ const isLocked = !exactContinuation && !!priorTask && holdsNegotiationConversationLock(priorTask);
53
+ if (isLocked) {
54
+ initLog.info('Conversation locked by active task, returning busy', {
55
+ conversationId: conversation.id,
56
+ opportunityId: state.opportunityId,
57
+ });
58
+ return { error: 'busy' };
59
+ }
60
+ // --- Load prior messages and determine continuation ---
61
+ const priorTurns = turnsFromMessages(priorMessages);
62
+ const isContinuation = priorTurns.length > 0;
63
+ const expectedSpeaker = expectedNegotiationSpeaker({
64
+ sourceUserId: state.sourceUser.id,
65
+ candidateUserId: state.candidateUser.id,
66
+ }, priorMessages);
67
+ if (!expectedSpeaker)
68
+ return { error: 'invalid negotiation participants' };
69
+ const currentSpeaker = expectedSpeaker === state.sourceUser.id
70
+ ? 'source'
71
+ : 'candidate';
72
+ // Determine scenario-based maxTurns
73
+ const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
74
+ const [sourceHasAgent, candidateHasAgent] = await Promise.all([
75
+ deps.dispatcher.hasExternalAgent(state.sourceUser.id, scope),
76
+ deps.dispatcher.hasExternalAgent(state.candidateUser.id, scope),
77
+ ]);
78
+ const ambientMax = Number(process.env.NEGOTIATION_MAX_TURNS_AMBIENT) || 6;
79
+ let maxTurns = state.maxTurns;
80
+ if (maxTurns == null) {
81
+ maxTurns = (sourceHasAgent && candidateHasAgent) ? 0 : ambientMax;
82
+ }
83
+ // --- Initiator seat resolution (v2: rigid per match, stamped at discovery) ---
84
+ // 1. Continuations inherit from the prior task for the same opportunity —
85
+ // never re-derive, so the seat cannot flip between sessions.
86
+ // 2. Conversation-scoped tie-break: if another negotiation on this DM is
87
+ // active and fresh (symmetric concurrent start under a different
88
+ // opportunityId — the opportunity-scoped lock above cannot see it),
89
+ // the first created task keeps the seat; this run inherits its stamp.
90
+ // 3. Otherwise: explicit stamp from the caller, falling back to the
91
+ // session's sourceUser (pre-stamp heuristic behavior, unchanged).
92
+ const readInitiator = (metadata) => {
93
+ const v = metadata?.initiatorUserId;
94
+ return typeof v === 'string' && v.length > 0 ? v : null;
95
+ };
96
+ let initiatorUserId = readInitiator(priorTask?.metadata) ?? state.initiatorUserId ?? state.sourceUser.id;
97
+ // Conversation-scoped prior task: used only for the initiator tie-break
98
+ // (and only when active+fresh).
99
+ const convTask = !exactContinuation && !readInitiator(priorTask?.metadata)
100
+ ? await deps.database.getLatestNegotiationTaskForConversation?.(conversation.id).catch(() => null)
101
+ : null;
102
+ if (!readInitiator(priorTask?.metadata)) {
103
+ if (convTask && convTask.id !== priorTask?.id && holdsNegotiationConversationLock(convTask)) {
104
+ const convInitiator = readInitiator(convTask.metadata);
105
+ if (convInitiator) {
106
+ initLog.info('Conversation-scoped tie-break: inheriting initiator seat from concurrent task', {
107
+ conversationId: conversation.id,
108
+ winningTaskId: convTask.id,
109
+ initiatorUserId: convInitiator,
110
+ });
111
+ initiatorUserId = convInitiator;
112
+ }
113
+ }
114
+ }
115
+ // --- Protocol version: pinned per negotiation, re-stamped per match ---
116
+ // A prior task for this same negotiation (exact continuation resume or
117
+ // a re-run of the same opportunity) pins the version, so one
118
+ // negotiation never flips semantics mid-flight (absent field on a
119
+ // genuine prior = pre-v2 task = v1). Everything else — including
120
+ // continuations of older conversations between the same pair — stamps
121
+ // fresh from NEGOTIATION_PROTOCOL_VERSION, so a version cutover
122
+ // reaches existing pairs on their next new match instead of being
123
+ // pinned to v1 forever by conversation history.
124
+ const protocolVersion = priorTask
125
+ ? (readProtocolVersion(priorTask.metadata) ?? 'v1')
126
+ : configuredProtocolVersion();
127
+ const taskMetadata = {
128
+ type: 'negotiation',
129
+ sourceUserId: state.sourceUser.id,
130
+ initiatorUserId,
131
+ protocolVersion,
132
+ candidateUserId: state.candidateUser.id,
133
+ networkId: state.indexContext.networkId,
134
+ sourceIntentId: state.sourceIntentId,
135
+ candidateIntentId: state.candidateIntentId,
136
+ participantBindings: [
137
+ ...(state.sourceIntentId ? [{ userId: state.sourceUser.id, intentId: state.sourceIntentId, networkId: state.indexContext.networkId }] : []),
138
+ ...(state.candidateIntentId ? [{ userId: state.candidateUser.id, intentId: state.candidateIntentId, networkId: state.indexContext.networkId }] : []),
139
+ ],
140
+ intentSnapshots: buildIntentSnapshots(state.sourceUser, state.candidateUser),
141
+ ...(state.opportunityId && { opportunityId: state.opportunityId }),
142
+ maxTurns,
143
+ isContinuation,
144
+ priorTurnCount: priorTurns.length,
145
+ ...(exactContinuation ? {
146
+ resumeFromTaskId: exactContinuation.taskId,
147
+ continuationSettlementId: exactContinuation.settlementId,
148
+ } : {}),
149
+ };
150
+ if (state.opportunityId && Boolean(state.opportunityStatus) !== Boolean(state.opportunityUpdatedAt)) {
151
+ throw new Error('Negotiation attempt requires both opportunity status and updatedAt');
152
+ }
153
+ const task = exactContinuation
154
+ ? claimedSuccessor
155
+ : state.opportunityId && state.opportunityStatus && state.opportunityUpdatedAt
156
+ ? await deps.database.createNegotiationTaskForAttempt({
157
+ conversationId: conversation.id,
158
+ opportunityId: state.opportunityId,
159
+ expectedStatus: state.opportunityStatus,
160
+ expectedUpdatedAt: state.opportunityUpdatedAt,
161
+ metadata: taskMetadata,
162
+ })
163
+ : await deps.database.createTask(conversation.id, taskMetadata);
164
+ if (!task) {
165
+ throw new Error('Negotiation attempt is stale or already claimed');
166
+ }
167
+ // Attempt-bound discovery atomically promoted the exact persisted state
168
+ // to `negotiating` while inserting the task. Legacy/direct invocations
169
+ // with only an opportunity ID retain the prior best-effort status update.
170
+ if (state.opportunityId && !state.opportunityUpdatedAt) {
171
+ await deps.database.updateOpportunityStatus(state.opportunityId, 'negotiating').catch((err) => {
172
+ initLog.error('Failed to set opportunity status to negotiating', { opportunityId: state.opportunityId, error: err });
173
+ });
174
+ }
175
+ // Load user answers collected by the questioner between sessions
176
+ const userAnswers = (isContinuation && state.opportunityId)
177
+ ? await deps.database.getOpportunityUserAnswers(state.opportunityId).catch((err) => {
178
+ initLog.error('Failed to load user answers', { opportunityId: state.opportunityId, error: err });
179
+ return [];
180
+ })
181
+ : [];
182
+ // Seed messages with prior turns (additive reducer appends new turns on top).
183
+ // taskId is preserved so the turn/screen nodes can separate this
184
+ // session's turns from seeded prior-task turns (IND-569).
185
+ const seedMessages = isContinuation ? priorMessages.map((m) => ({
186
+ id: m.id,
187
+ senderId: m.senderId,
188
+ role: 'agent',
189
+ parts: m.parts,
190
+ createdAt: m.createdAt,
191
+ taskId: m.taskId ?? null,
192
+ })) : [];
193
+ // IND-569: attribute seeded prior turns to their originating opportunity
194
+ // once, up front. Earlier-opportunity and legacy unattributed blocks are
195
+ // immutable for the session; the current block is composed per turn.
196
+ const priorAttribution = isContinuation
197
+ ? await buildSeededAttribution(priorMessages
198
+ .map((m) => ({ taskId: m.taskId ?? null, turn: turnsFromMessages([m])[0] }))
199
+ .filter((e) => Boolean(e.turn)), state.opportunityId, (taskId) => resolveTaskAttribution(deps, taskId))
200
+ : null;
201
+ return {
202
+ conversationId: conversation.id,
203
+ taskId: task.id,
204
+ currentSpeaker,
205
+ turnCount: 0,
206
+ maxTurns,
207
+ isContinuation,
208
+ initiatorUserId,
209
+ protocolVersion,
210
+ priorTurnCount: priorTurns.length,
211
+ ...(priorAttribution && { priorAttribution }),
212
+ ...(userAnswers.length > 0 && { userAnswers }),
213
+ ...(exactContinuation?.execution.consultation
214
+ ? { privateConsultation: exactContinuation.execution.consultation }
215
+ : {}),
216
+ ...(exactContinuation && (() => {
217
+ const turnContext = priorTask?.metadata?.turnContext;
218
+ const reason = turnContext?.consultationPolicyReason;
219
+ return typeof reason === 'string' ? { consultationPolicyReason: reason } : {};
220
+ })()),
221
+ ...(seedMessages.length > 0 && { messages: seedMessages }),
222
+ };
223
+ }
224
+ catch (err) {
225
+ return { error: `Init failed: ${err instanceof Error ? err.message : String(err)}` };
226
+ }
227
+ }