@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,484 @@
1
+ /**
2
+ * Negotiation graph, stage 3: one negotiator turn.
3
+ */
4
+ import { requestContext } from "../../shared/observability/request-context.js";
5
+ import { allowedActionsFor, askUserAnswerWindowMs, configuredAskUserEnabled, fallbackActionFor, rejectActionFor } from "../domain/negotiation.protocol.js";
6
+ import { assessConsultationEligibility, consultationPromptFor, negotiationConsultationPolicyMode } from "../domain/negotiation.consultation-policy.js";
7
+ import { blocksNegotiationBeforeFirstTurn } from "./negotiation.screen.js";
8
+ import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "../domain/negotiation.deadlock.js";
9
+ import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId } from '../domain/negotiation.question-safety.js';
10
+ import { isNegotiationTurnCapReached } from "../domain/negotiation.turn-cap.js";
11
+ import { buildAttributedDialogue, hasPriorAskUser, memoryQueryText, retrieveMemory, turnLog, turnsFromMessages } from "./negotiation.graph.shared.js";
12
+ export async function turnNode(state, deps) {
13
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
14
+ // Local helper to emit events whose shape is wider than the declared
15
+ // `TraceEmitter` union. The chat agent already casts at its relay sink;
16
+ // here we localize the cast at the callsite so the rest of the body stays typed.
17
+ const emitWide = (event) => traceEmitter?.(event);
18
+ const agentName = "Index negotiator";
19
+ const agentStart = Date.now();
20
+ traceEmitter?.({ type: "agent_start", name: agentName });
21
+ try {
22
+ const history = turnsFromMessages(state.messages);
23
+ const isSource = state.currentSpeaker === "source";
24
+ const ownUser = isSource ? state.sourceUser : state.candidateUser;
25
+ const otherUser = isSource ? state.candidateUser : state.sourceUser;
26
+ const ownIntentId = isSource ? state.sourceIntentId : state.candidateIntentId;
27
+ // Determine if this is the system agent's final allowed turn.
28
+ const isFinalTurn = isNegotiationTurnCapReached(state.turnCount + 1, state.maxTurns);
29
+ // Seat attribution keys on initiatorUserId (rigid v2 stamp), never on
30
+ // parity or source/candidate position — under the conversation-scoped
31
+ // tie-break this run's source may hold the counterparty seat.
32
+ const version = state.protocolVersion ?? 'v1';
33
+ const seat = ownUser.id === (state.initiatorUserId ?? state.sourceUser.id)
34
+ ? 'initiator'
35
+ : 'counterparty';
36
+ // Legacy ask_user availability (P3.2): flag on, full pause loop wired
37
+ // (questioner + answer-window timer + an opportunity to resume
38
+ // against), v2 non-final non-opening turn, and this side's one client
39
+ // consultation not yet spent (rationing). Shadow is observational and
40
+ // must preserve this legacy path byte-for-byte except for telemetry.
41
+ const policyMode = negotiationConsultationPolicyMode();
42
+ const askUserAvailable = version === 'v2'
43
+ && !isFinalTurn
44
+ && configuredAskUserEnabled()
45
+ && !!deps.questionerEnqueue
46
+ && !!deps.timeoutQueue?.enqueueAskUserExpiry
47
+ && !!state.opportunityId
48
+ && !!ownIntentId
49
+ && !!state.indexContext.networkId
50
+ && !(state.turnCount === 0 && !state.isContinuation)
51
+ && !hasPriorAskUser(state.messages, ownUser.id);
52
+ // ─── Deadlock detection → persuasion→bargaining stance (IND-428) ──────
53
+ // Deterministic trailing-run inspection of the persisted history — no
54
+ // LLM in the decision. Gated on the strict default-off flag AND v2,
55
+ // checked alongside the protocol-version plumbing so v1 semantics stay
56
+ // untouched. Fail-open: any detection error means "no deadlock" and
57
+ // the legacy path proceeds byte-identically. The shift changes the
58
+ // system agent's drafting stance only — allowedActions, the dispatch
59
+ // payload, and all termination rules are untouched.
60
+ let deadlock = null;
61
+ if (version === 'v2' && configuredDeadlockShiftEnabled()) {
62
+ try {
63
+ deadlock = assessDeadlock(history, configuredDeadlockThreshold());
64
+ }
65
+ catch (err) {
66
+ turnLog.warn('Deadlock detection failed; proceeding without mode shift', {
67
+ taskId: state.taskId,
68
+ error: err instanceof Error ? err.message : String(err),
69
+ });
70
+ deadlock = null;
71
+ }
72
+ }
73
+ const bargainingMode = deadlock?.deadlocked === true;
74
+ // P5.3: the speaker's own negotiator memory (cached per side across
75
+ // turns). Injected into both the dispatch payload (the user's own
76
+ // agent — scope-correct) and the system-agent prompt.
77
+ const ownSide = isSource ? "source" : "candidate";
78
+ const ownMemory = state.memoryBySide?.[ownSide]
79
+ ?? (deps.memoryRetrieve
80
+ ? await retrieveMemory(deps, ownUser.id, otherUser.id, memoryQueryText(state, otherUser), "turn")
81
+ : []);
82
+ const payload = {
83
+ negotiationId: state.taskId,
84
+ ownUser,
85
+ otherUser,
86
+ indexContext: state.indexContext,
87
+ seedAssessment: state.seedAssessment,
88
+ history,
89
+ isFinalTurn,
90
+ isDiscoverer: isSource,
91
+ seat,
92
+ protocolVersion: version,
93
+ allowedActions: [...allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable })],
94
+ ...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
95
+ ...(ownMemory.length > 0 && { negotiatorMemory: ownMemory }),
96
+ ...(state.privateConsultation?.recipientUserId === ownUser.id
97
+ ? { privateConsultation: state.privateConsultation }
98
+ : {}),
99
+ ...(state.continuationExecution
100
+ ? {
101
+ timeoutContinuation: {
102
+ priorTaskId: state.continuationExecution.taskId,
103
+ settlementId: state.continuationExecution.settlementId,
104
+ successorTaskId: state.continuationExecution.successorTaskId,
105
+ token: state.continuationExecution.token,
106
+ fence: state.continuationExecution.fence,
107
+ },
108
+ }
109
+ : {}),
110
+ };
111
+ const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
112
+ const dispatchResult = await deps.dispatcher.dispatch(ownUser.id, scope, payload, { timeoutMs: state.timeoutMs });
113
+ let turn;
114
+ if (dispatchResult.handled) {
115
+ // Personal agent responded. Under v2, coerce out-of-seat actions to
116
+ // the conservative fallback — the polling/respond surfaces reject
117
+ // these with a 400, but locally-dispatched turns land here directly.
118
+ turn = dispatchResult.turn;
119
+ if (version === 'v2' && !allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable }).includes(turn.action)) {
120
+ turnLog.warn('Personal agent returned out-of-seat action, coercing to conservative fallback', {
121
+ action: turn.action, seat, isFinalTurn,
122
+ });
123
+ turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
124
+ }
125
+ }
126
+ else if (dispatchResult.reason === 'waiting') {
127
+ // Long timeout — graph suspends. Persist the full turn context so the
128
+ // polling agent (and MCP consumers via get_negotiation) reconstruct
129
+ // the same view the in-process system agent would see. The view is
130
+ // stored in absolute source/candidate terms; perspective is projected
131
+ // at pickup time using the claiming user's id.
132
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "waiting_for_agent" });
133
+ await deps.database.setTaskTurnContext(state.taskId, {
134
+ sourceUser: state.sourceUser,
135
+ candidateUser: state.candidateUser,
136
+ indexContext: state.indexContext,
137
+ seedAssessment: state.seedAssessment,
138
+ // Keep discoveryQuery speaker-scoped: include it only when the
139
+ // parked turn belongs to the discoverer (source). Persisting it on
140
+ // candidate-side turns would make the pickup prompt frame the
141
+ // search as "your user searched for X" for the wrong user.
142
+ ...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
143
+ ...(state.privateConsultation && state.privateConsultation.recipientUserId === ownUser.id
144
+ ? { privateConsultation: state.privateConsultation }
145
+ : {}),
146
+ }, state.continuationExecution);
147
+ await deps.database.updateTaskState(state.taskId, "waiting_for_agent", undefined, state.continuationExecution, dispatchResult.resumeToken);
148
+ return { status: 'waiting_for_agent' };
149
+ }
150
+ else {
151
+ // No personal agent or timeout — run system agent
152
+ const agentPriorDialogue = buildAttributedDialogue(state);
153
+ turn = await deps.systemAgent.invoke({
154
+ ownUser,
155
+ otherUser,
156
+ indexContext: state.indexContext,
157
+ seedAssessment: state.seedAssessment,
158
+ history,
159
+ isFinalTurn,
160
+ isDiscoverer: isSource,
161
+ seat,
162
+ protocolVersion: version,
163
+ ...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
164
+ isContinuation: state.isContinuation,
165
+ ...(agentPriorDialogue && { priorDialogue: agentPriorDialogue }),
166
+ ...(state.userAnswers.length > 0 && { userAnswers: state.userAnswers }),
167
+ ...(askUserAvailable && { canAskUser: true }),
168
+ ...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
169
+ ...(ownMemory.length > 0 && { memory: ownMemory }),
170
+ ...(state.privateConsultation?.recipientUserId === ownUser.id
171
+ ? { privateConsultation: state.privateConsultation }
172
+ : {}),
173
+ });
174
+ }
175
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `${turn.action}` });
176
+ // IND-564 / IND-611: the opening-withdraw guard runs BEFORE the turn-0
177
+ // opening force below. Order matters and used to be inverted: the force
178
+ // rewrote a turn-0 `withdraw` into `outreach` first, which (a) made this
179
+ // guard dead code for a v2 initiator on turn 0 and (b) sent an outreach
180
+ // whose surviving `reasoning` argued against the match to the
181
+ // counterparty. An honest turn-0 refusal is now allowed to stand and
182
+ // flows into the existing quiet `screened_out` path — no message is
183
+ // persisted, so the original guard's intent (never retract a message
184
+ // that was never made) is preserved rather than weakened.
185
+ //
186
+ // Exact ask_user resumes are exempt: the successor is the SAME logical
187
+ // negotiation resumed after the client answered, so post-consultation
188
+ // withdraw is legitimate.
189
+ if (turn.action === 'withdraw' && !state.outreachOpened && !state.continuationExecution) {
190
+ turnLog.info('negotiation_opening_withdraw_screened_out', {
191
+ taskId: state.taskId,
192
+ opportunityId: state.opportunityId || undefined,
193
+ seat,
194
+ turnCount: state.turnCount,
195
+ isContinuation: state.isContinuation,
196
+ });
197
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "screened_out: opening withdraw" });
198
+ return { lastTurn: turn, firstTurnScreenedOut: true };
199
+ }
200
+ // First turn must open the negotiation (unless continuing a prior
201
+ // conversation): v1 → "propose"; v2 initiator → "outreach". A v2 turn-0
202
+ // speaker holding the counterparty seat (tie-break inheritance) is left
203
+ // unforced — it is responding, not opening.
204
+ //
205
+ // A legitimate turn-0 refusal never reaches here: the opening-withdraw
206
+ // guard above already returned. What remains are genuinely malformed
207
+ // openings (a turn-0 `counter`/`question`), which are still coerced.
208
+ if (state.turnCount === 0 && !state.isContinuation) {
209
+ const openingAction = version === 'v2' ? 'outreach' : 'propose';
210
+ if ((version !== 'v2' || seat === 'initiator') && turn.action !== openingAction) {
211
+ turnLog.warn(`Agent returned unexpected action on turn 0, forcing to ${openingAction}`, { action: turn.action });
212
+ turn.action = openingAction;
213
+ }
214
+ }
215
+ // IND-508 deterministic admission is evaluated only after the opening
216
+ // guard but before any turn is persisted. It consumes action/role enums
217
+ // only; free-form reasoning, messages, profiles, and evaluator inputs
218
+ // are intentionally unavailable to the policy.
219
+ let consultationPolicyReason;
220
+ const policyEligibility = policyMode === 'off' ? { eligible: false } : assessConsultationEligibility({
221
+ protocolVersion: version,
222
+ seat,
223
+ isOpeningTurn: state.turnCount === 0 && !state.isContinuation,
224
+ isFinalTurn,
225
+ screenedOut: blocksNegotiationBeforeFirstTurn(state.screenDecision, state.turnCount),
226
+ action: turn.action,
227
+ ownSuggestedRole: turn.assessment?.suggestedRoles?.ownUser,
228
+ priorActions: history.map((prior) => prior.action),
229
+ previouslyConsulted: hasPriorAskUser(state.messages, ownUser.id),
230
+ hasExactResumeCoordinate: Boolean(configuredAskUserEnabled()
231
+ && deps.questionerEnqueue
232
+ && deps.timeoutQueue?.enqueueAskUserExpiry
233
+ && state.taskId
234
+ && state.opportunityId
235
+ && ownIntentId
236
+ && state.indexContext.networkId),
237
+ lifecycleValid: Boolean(state.taskId && state.opportunityId && ownIntentId && state.indexContext.networkId),
238
+ });
239
+ const emitConsultationTelemetry = (stage, reason) => {
240
+ turnLog.info('negotiation_consultation_policy', { stage, mode: policyMode, reason });
241
+ emitWide({ type: 'negotiation_consultation_policy', stage, mode: policyMode, reason });
242
+ };
243
+ if (policyEligibility.eligible && policyEligibility.reason) {
244
+ emitConsultationTelemetry('eligible', policyEligibility.reason);
245
+ if (policyMode === 'on') {
246
+ consultationPolicyReason = policyEligibility.reason;
247
+ turn = {
248
+ ...turn,
249
+ action: 'ask_user',
250
+ message: null,
251
+ assessment: {
252
+ reasoning: 'Client consultation required.',
253
+ suggestedRoles: turn.assessment.suggestedRoles,
254
+ },
255
+ askUser: { reason: consultationPolicyReason },
256
+ };
257
+ emitConsultationTelemetry('asked', consultationPolicyReason);
258
+ }
259
+ }
260
+ // Safety net: off/shadow retain legacy behavior. In on, a spontaneous
261
+ // ask_user is admissible only when the deterministic policy just
262
+ // authorized it, so no unbounded pause can enter shared history.
263
+ if (turn.action === 'ask_user' && (!askUserAvailable || (policyMode === 'on' && !consultationPolicyReason))) {
264
+ turnLog.warn('ask_user emitted while unavailable, coercing to conservative fallback', {
265
+ seat, isFinalTurn, taskId: state.taskId,
266
+ });
267
+ turn = { ...turn, action: fallbackActionFor(version, seat, isFinalTurn) };
268
+ }
269
+ // ─── Deadlock shift record (IND-428) ───────────────────────────────
270
+ // Applied-stance analytics: recorded once per session, on the first
271
+ // turn actually drafted in the bargaining stance (the system agent —
272
+ // externally dispatched turns never receive the stance). Internal
273
+ // metadata only: persisted to tasks.metadata.deadlockShift via the
274
+ // optional hook; negotiation API surfaces project specific fields and
275
+ // never return task metadata verbatim. Every step fails open.
276
+ const bargainingApplied = bargainingMode && !dispatchResult.handled;
277
+ let deadlockShiftRecord = null;
278
+ if (bargainingApplied && !state.deadlockShift) {
279
+ deadlockShiftRecord = {
280
+ reason: 'consecutive_non_convergent',
281
+ consecutiveNonConvergent: deadlock.consecutiveNonConvergent,
282
+ threshold: deadlock.threshold,
283
+ shiftedAtTurn: state.turnCount,
284
+ seat,
285
+ detectedAt: new Date().toISOString(),
286
+ };
287
+ await deps.database.setTaskDeadlockShift?.(state.taskId, deadlockShiftRecord, state.continuationExecution).catch((err) => {
288
+ turnLog.error('Failed to persist deadlock shift record', { taskId: state.taskId, error: err });
289
+ });
290
+ turnLog.info('negotiation_deadlock_shift', {
291
+ taskId: state.taskId,
292
+ opportunityId: state.opportunityId || undefined,
293
+ seat,
294
+ consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
295
+ threshold: deadlockShiftRecord.threshold,
296
+ turnIndex: state.turnCount,
297
+ });
298
+ if (state.opportunityId) {
299
+ emitWide({
300
+ type: 'negotiation_deadlock_shift',
301
+ opportunityId: state.opportunityId,
302
+ negotiationConversationId: state.conversationId,
303
+ turnIndex: state.turnCount,
304
+ actor: isSource ? 'source' : 'candidate',
305
+ consecutiveNonConvergent: deadlockShiftRecord.consecutiveNonConvergent,
306
+ threshold: deadlockShiftRecord.threshold,
307
+ });
308
+ }
309
+ }
310
+ const parts = [{ kind: "data", data: turn }];
311
+ const message = await deps.database.createMessage({
312
+ conversationId: state.conversationId,
313
+ senderId: `agent:${ownUser.id}`,
314
+ role: "agent",
315
+ parts,
316
+ taskId: state.taskId,
317
+ ...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
318
+ });
319
+ // ─── ask_user pause (P3.2) ────────────────────────────────────────────
320
+ // The negotiator consults its OWN client: persist the turn (done above),
321
+ // park the full turn context, arm the answer-window timer, enqueue the
322
+ // question through the negotiation_inflight preset, then suspend the
323
+ // task as input_required. The graph exits at this turn boundary exactly
324
+ // like the waiting_for_agent suspend; the answer (or window expiry)
325
+ // resumes via the run-existing continuation path.
326
+ if (turn.action === 'ask_user') {
327
+ const consultationReason = turn.askUser?.reason;
328
+ const safeAskUser = consultationReason
329
+ ? consultationPromptFor(consultationReason)
330
+ : null;
331
+ const settlementId = negotiationQuestionSettlementId(state.taskId);
332
+ const askUserBinding = await deps.database.captureNegotiationAskUserBinding({
333
+ taskId: state.taskId,
334
+ settlementId,
335
+ recipientUserId: ownUser.id,
336
+ recipientIntentId: ownIntentId,
337
+ opportunityId: state.opportunityId,
338
+ networkId: state.indexContext.networkId,
339
+ turnContext: {
340
+ sourceUser: state.sourceUser,
341
+ candidateUser: state.candidateUser,
342
+ indexContext: state.indexContext,
343
+ seedAssessment: state.seedAssessment,
344
+ ...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
345
+ ...(consultationReason && { consultationPolicyReason: consultationReason }),
346
+ },
347
+ ...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
348
+ });
349
+ // Arm the timer BEFORE flipping state: it is the durable recovery
350
+ // trigger even when generation enqueues no job or persists no row.
351
+ const windowMs = askUserAnswerWindowMs();
352
+ await deps.timeoutQueue.enqueueAskUserExpiry(state.taskId, {
353
+ settlementId,
354
+ opportunityId: state.opportunityId,
355
+ userId: ownUser.id,
356
+ recipientIntentId: ownIntentId,
357
+ networkId: state.indexContext.networkId,
358
+ intentFingerprint: askUserBinding.intentFingerprint,
359
+ opportunityStatus: askUserBinding.opportunityStatus,
360
+ opportunityUpdatedAt: askUserBinding.opportunityUpdatedAt,
361
+ counterpartyUserId: askUserBinding.counterpartyUserId,
362
+ counterpartyIntentId: askUserBinding.counterpartyIntentId,
363
+ }, windowMs);
364
+ // Persistence admission requires the exact task to be input_required.
365
+ // Flip before enqueue; if the structured ask_user fields fail the
366
+ // deterministic privacy gate, the timer alone closes the exact task.
367
+ await deps.database.updateTaskState(state.taskId, 'input_required', undefined, state.continuationExecution);
368
+ if (safeAskUser) {
369
+ const userContext = (await deps.database.getUserContext(ownUser.id, null).catch(() => null))?.text ?? '';
370
+ await deps.questionerEnqueue({
371
+ mode: 'negotiation_inflight',
372
+ purpose: 'inflight_consultation',
373
+ userId: ownUser.id,
374
+ sourceType: 'opportunity',
375
+ sourceId: state.opportunityId,
376
+ negotiation: {
377
+ purpose: 'inflight_consultation',
378
+ recipientUserId: ownUser.id,
379
+ recipientIntentId: ownIntentId,
380
+ opportunityId: state.opportunityId,
381
+ taskId: state.taskId,
382
+ networkId: state.indexContext.networkId,
383
+ },
384
+ context: {
385
+ negotiationId: state.taskId,
386
+ counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
387
+ indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
388
+ consultationPolicyReason: consultationReason,
389
+ ...(userContext && { userContext }),
390
+ },
391
+ }).catch((error) => {
392
+ turnLog.error('Failed to enqueue safe ask_user question; timeout recovery remains armed', {
393
+ taskId: state.taskId,
394
+ opportunityId: state.opportunityId,
395
+ error,
396
+ });
397
+ });
398
+ }
399
+ else {
400
+ turnLog.warn('Skipping unsafe or incomplete ask_user question generation', {
401
+ taskId: state.taskId,
402
+ opportunityId: state.opportunityId,
403
+ });
404
+ }
405
+ turnLog.info('negotiation_ask_user_pause', {
406
+ taskId: state.taskId,
407
+ opportunityId: state.opportunityId,
408
+ seat,
409
+ askingUserId: ownUser.id,
410
+ windowMs,
411
+ });
412
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: "ask_user" });
413
+ emitWide({
414
+ type: 'negotiation_ask_user',
415
+ opportunityId: state.opportunityId,
416
+ negotiationConversationId: state.conversationId,
417
+ turnIndex: state.turnCount,
418
+ actor: isSource ? 'source' : 'candidate',
419
+ questionGenerationSafe: Boolean(safeAskUser),
420
+ windowMs,
421
+ });
422
+ return {
423
+ messages: [{
424
+ id: message.id,
425
+ senderId: message.senderId,
426
+ role: "agent",
427
+ parts: message.parts,
428
+ createdAt: message.createdAt,
429
+ }],
430
+ turnCount: state.turnCount + 1,
431
+ lastTurn: turn,
432
+ status: 'input_required',
433
+ ...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
434
+ };
435
+ }
436
+ await deps.database.updateTaskState(state.taskId, "working", undefined, state.continuationExecution);
437
+ if (state.opportunityId) {
438
+ emitWide({
439
+ type: "negotiation_turn",
440
+ opportunityId: state.opportunityId,
441
+ negotiationConversationId: state.conversationId,
442
+ turnIndex: state.turnCount,
443
+ actor: isSource ? "source" : "candidate",
444
+ action: turn.action,
445
+ ...(turn.assessment?.reasoning && { reasoning: turn.assessment.reasoning }),
446
+ ...(turn.message && { message: turn.message }),
447
+ ...(turn.assessment?.suggestedRoles && { suggestedRoles: turn.assessment.suggestedRoles }),
448
+ durationMs: Date.now() - agentStart,
449
+ });
450
+ }
451
+ return {
452
+ messages: [{
453
+ id: message.id,
454
+ senderId: message.senderId,
455
+ role: "agent",
456
+ parts: message.parts,
457
+ createdAt: message.createdAt,
458
+ }],
459
+ turnCount: state.turnCount + 1,
460
+ currentSpeaker: (isSource ? "candidate" : "source"),
461
+ lastTurn: turn,
462
+ memoryBySide: { [ownSide]: ownMemory },
463
+ // Record the in-task outreach so a later `withdraw` is legal (IND-564).
464
+ ...(turn.action === 'outreach' && { outreachOpened: true }),
465
+ ...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
466
+ };
467
+ }
468
+ catch (err) {
469
+ const errMsg = err instanceof Error ? err.message : String(err);
470
+ turnLog.error("Agent invocation failed", { error: errMsg, stack: err instanceof Error ? err.stack : undefined, turnCount: state.turnCount });
471
+ traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `error: ${errMsg}` });
472
+ const errorSeat = (state.currentSpeaker === 'source' ? state.sourceUser.id : state.candidateUser.id) === (state.initiatorUserId ?? state.sourceUser.id)
473
+ ? 'initiator'
474
+ : 'counterparty';
475
+ return {
476
+ lastTurn: {
477
+ action: rejectActionFor(state.protocolVersion ?? 'v1', errorSeat),
478
+ assessment: { reasoning: `Agent error: ${errMsg}`, suggestedRoles: { ownUser: "peer", otherUser: "peer" } },
479
+ },
480
+ turnCount: state.turnCount + 1,
481
+ error: `Turn failed: ${errMsg}`,
482
+ };
483
+ }
484
+ }
@@ -450,7 +450,7 @@ export declare const NegotiationGraphState: import("@langchain/langgraph").Annot
450
450
  * negotiator's own client (answer or 24 h window expiry resumes it)
451
451
  * - `completed` — negotiation finalized (accept/reject/turn-cap/timeout)
452
452
  */
453
- status: import("@langchain/langgraph").BaseChannel<"waiting_for_agent" | "input_required" | "active" | "completed", "waiting_for_agent" | "input_required" | "active" | "completed" | import("@langchain/langgraph").OverwriteValue<"waiting_for_agent" | "input_required" | "active" | "completed">, unknown>;
453
+ status: import("@langchain/langgraph").BaseChannel<"waiting_for_agent" | "input_required" | "completed" | "active", "waiting_for_agent" | "input_required" | "completed" | "active" | import("@langchain/langgraph").OverwriteValue<"waiting_for_agent" | "input_required" | "completed" | "active">, unknown>;
454
454
  /** Number of turns present in the conversation before this session started. */
455
455
  priorTurnCount: import("@langchain/langgraph").BaseChannel<number, number | import("@langchain/langgraph").OverwriteValue<number>, unknown>;
456
456
  /** User answers collected by the questioner between negotiation sessions. */
@@ -149,7 +149,6 @@ export const NegotiationGraphState = Annotation.Root({
149
149
  * (attribution module imports NegotiationTurn from this module's shim);
150
150
  * callers cast to `SeededAttribution` via the negotiation.attribution module.
151
151
  */
152
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
152
  priorAttribution: Annotation({
154
153
  reducer: (curr, next) => next ?? curr,
155
154
  default: () => null,