@indexnetwork/protocol 6.8.0-rc.395.1 → 6.9.0-rc.396.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/negotiation/negotiation.agent.d.ts +3 -1
- package/dist/negotiation/negotiation.agent.d.ts.map +1 -1
- package/dist/negotiation/negotiation.agent.js +4 -1
- package/dist/negotiation/negotiation.agent.js.map +1 -1
- package/dist/negotiation/negotiation.graph.d.ts +53 -22
- package/dist/negotiation/negotiation.graph.d.ts.map +1 -1
- package/dist/negotiation/negotiation.graph.js +235 -71
- package/dist/negotiation/negotiation.graph.js.map +1 -1
- package/dist/negotiation/negotiation.question-safety.d.ts +26 -0
- package/dist/negotiation/negotiation.question-safety.d.ts.map +1 -0
- package/dist/negotiation/negotiation.question-safety.js +56 -0
- package/dist/negotiation/negotiation.question-safety.js.map +1 -0
- package/dist/negotiation/negotiation.state.d.ts +22 -2
- package/dist/negotiation/negotiation.state.d.ts.map +1 -1
- package/dist/negotiation/negotiation.state.js +30 -0
- package/dist/negotiation/negotiation.state.js.map +1 -1
- package/dist/opportunity/opportunity.graph.d.ts +12 -1
- package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
- package/dist/opportunity/opportunity.graph.js +43 -5
- package/dist/opportunity/opportunity.graph.js.map +1 -1
- package/dist/opportunity/opportunity.state.d.ts +5 -2
- package/dist/opportunity/opportunity.state.d.ts.map +1 -1
- package/dist/opportunity/opportunity.state.js +5 -0
- package/dist/opportunity/opportunity.state.js.map +1 -1
- package/dist/questioner/questioner.agent.d.ts +1 -1
- package/dist/questioner/questioner.agent.d.ts.map +1 -1
- package/dist/questioner/questioner.agent.js +5 -0
- package/dist/questioner/questioner.agent.js.map +1 -1
- package/dist/questioner/questioner.presets.d.ts.map +1 -1
- package/dist/questioner/questioner.presets.js +9 -13
- package/dist/questioner/questioner.presets.js.map +1 -1
- package/dist/questioner/questioner.types.d.ts +44 -10
- package/dist/questioner/questioner.types.d.ts.map +1 -1
- package/dist/questioner/questioner.types.js +56 -1
- package/dist/questioner/questioner.types.js.map +1 -1
- package/dist/shared/agent/tool.factory.d.ts.map +1 -1
- package/dist/shared/agent/tool.factory.js +1 -0
- package/dist/shared/agent/tool.factory.js.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts +3 -0
- package/dist/shared/interfaces/agent-dispatcher.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/agent-dispatcher.interface.js.map +1 -1
- package/dist/shared/interfaces/database.interface.d.ts +81 -6
- package/dist/shared/interfaces/database.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/database.interface.js.map +1 -1
- package/dist/shared/interfaces/negotiation-events.interface.d.ts +14 -2
- package/dist/shared/interfaces/negotiation-events.interface.d.ts.map +1 -1
- package/dist/shared/interfaces/negotiation-events.interface.js.map +1 -1
- package/dist/shared/schemas/question.schema.d.ts +347 -28
- package/dist/shared/schemas/question.schema.d.ts.map +1 -1
- package/dist/shared/schemas/question.schema.js +132 -2
- package/dist/shared/schemas/question.schema.js.map +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { ASK_USER_LOCK_SLACK_MS, allowedActionsFor, askUserAnswerWindowMs, confi
|
|
|
7
7
|
import { NegotiationScreener, configuredScreenMode } from "./negotiation.screen.js";
|
|
8
8
|
import { assessDeadlock, configuredDeadlockShiftEnabled, configuredDeadlockThreshold } from "./negotiation.deadlock.js";
|
|
9
9
|
import { protocolLogger } from "../shared/observability/protocol.logger.js";
|
|
10
|
+
import { NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY, NEGOTIATION_QUESTION_GENERIC_NETWORK, negotiationQuestionSettlementId, validateInflightAskUserFields } from './negotiation.question-safety.js';
|
|
10
11
|
const logger = protocolLogger("NegotiationGraph");
|
|
11
12
|
const initLog = protocolLogger("NegotiationGraph:Init");
|
|
12
13
|
const screenNodeLog = protocolLogger("NegotiationGraph:Screen");
|
|
@@ -108,10 +109,14 @@ export class NegotiationGraphFactory {
|
|
|
108
109
|
].filter(Boolean).join("\n");
|
|
109
110
|
const initNode = async (state) => {
|
|
110
111
|
try {
|
|
111
|
-
//
|
|
112
|
+
// Exact continuations reuse the prior conversation and preclaimed
|
|
113
|
+
// successor; they must not create any mutable state before the fence.
|
|
112
114
|
const agentIdA = `agent:${state.sourceUser.id}`;
|
|
113
115
|
const agentIdB = `agent:${state.candidateUser.id}`;
|
|
114
|
-
const
|
|
116
|
+
const execution = state.continuationExecution;
|
|
117
|
+
const conversation = execution
|
|
118
|
+
? { id: execution.conversationId }
|
|
119
|
+
: await database.getOrCreateDM(agentIdA, agentIdB, 'agent');
|
|
115
120
|
// --- Lock gate: check for an active task on this conversation ---
|
|
116
121
|
const priorMessages = await database.getMessagesForConversation(conversation.id);
|
|
117
122
|
const activeStates = ['submitted', 'working', 'input_required', 'waiting_for_agent', 'claimed'];
|
|
@@ -128,10 +133,37 @@ export class NegotiationGraphFactory {
|
|
|
128
133
|
: 5 * 60 * 1000;
|
|
129
134
|
return (Date.now() - new Date(t.updatedAt).getTime()) < freshnessMs;
|
|
130
135
|
};
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
if (Boolean(state.resumeFromTaskId) !== Boolean(state.continuationSettlementId)
|
|
137
|
+
|| Boolean(state.resumeFromTaskId) !== Boolean(execution))
|
|
138
|
+
return { error: 'invalid continuation correlation' };
|
|
139
|
+
const exactContinuation = state.resumeFromTaskId && state.continuationSettlementId && execution
|
|
140
|
+
? { taskId: state.resumeFromTaskId, settlementId: state.continuationSettlementId, execution }
|
|
133
141
|
: null;
|
|
134
|
-
const
|
|
142
|
+
const priorTask = exactContinuation
|
|
143
|
+
? await database.getTask(exactContinuation.taskId)
|
|
144
|
+
: state.opportunityId
|
|
145
|
+
? await database.getNegotiationTaskForOpportunity(state.opportunityId)
|
|
146
|
+
: null;
|
|
147
|
+
const claimedSuccessor = exactContinuation
|
|
148
|
+
? await database.getTask(exactContinuation.execution.successorTaskId)
|
|
149
|
+
: null;
|
|
150
|
+
if (exactContinuation) {
|
|
151
|
+
const settlement = priorTask?.metadata?.questionSettlement;
|
|
152
|
+
const storedExecution = claimedSuccessor?.metadata?.continuationExecution;
|
|
153
|
+
if (!priorTask
|
|
154
|
+
|| priorTask.conversationId !== conversation.id
|
|
155
|
+
|| priorTask.state !== 'canceled'
|
|
156
|
+
|| priorTask.metadata?.opportunityId !== state.opportunityId
|
|
157
|
+
|| settlement?.settlementId !== exactContinuation.settlementId
|
|
158
|
+
|| settlement?.taskId !== exactContinuation.taskId
|
|
159
|
+
|| !claimedSuccessor
|
|
160
|
+
|| claimedSuccessor.conversationId !== conversation.id
|
|
161
|
+
|| storedExecution?.token !== exactContinuation.execution.token
|
|
162
|
+
|| storedExecution?.fence !== exactContinuation.execution.fence
|
|
163
|
+
|| storedExecution?.status !== 'claimed')
|
|
164
|
+
return { error: 'invalid exact continuation task' };
|
|
165
|
+
}
|
|
166
|
+
const isLocked = !exactContinuation && !!priorTask && isActiveAndFresh(priorTask);
|
|
135
167
|
if (isLocked) {
|
|
136
168
|
initLog.info('Conversation locked by active task, returning busy', {
|
|
137
169
|
conversationId: conversation.id,
|
|
@@ -186,7 +218,7 @@ export class NegotiationGraphFactory {
|
|
|
186
218
|
// Conversation-scoped prior task: reused for both the initiator tie-break
|
|
187
219
|
// (only when active+fresh) and protocol-version inheritance (any prior
|
|
188
220
|
// task on the conversation pins the version).
|
|
189
|
-
const convTask = (!readInitiator(priorTask?.metadata) || !readProtocolVersion(priorTask?.metadata))
|
|
221
|
+
const convTask = !exactContinuation && (!readInitiator(priorTask?.metadata) || !readProtocolVersion(priorTask?.metadata))
|
|
190
222
|
? await database.getLatestNegotiationTaskForConversation?.(conversation.id).catch(() => null)
|
|
191
223
|
: null;
|
|
192
224
|
if (!readInitiator(priorTask?.metadata)) {
|
|
@@ -227,24 +259,36 @@ export class NegotiationGraphFactory {
|
|
|
227
259
|
protocolVersion,
|
|
228
260
|
candidateUserId: state.candidateUser.id,
|
|
229
261
|
networkId: state.indexContext.networkId,
|
|
262
|
+
sourceIntentId: state.sourceIntentId,
|
|
263
|
+
candidateIntentId: state.candidateIntentId,
|
|
264
|
+
participantBindings: [
|
|
265
|
+
...(state.sourceIntentId ? [{ userId: state.sourceUser.id, intentId: state.sourceIntentId, networkId: state.indexContext.networkId }] : []),
|
|
266
|
+
...(state.candidateIntentId ? [{ userId: state.candidateUser.id, intentId: state.candidateIntentId, networkId: state.indexContext.networkId }] : []),
|
|
267
|
+
],
|
|
230
268
|
intentSnapshots: buildIntentSnapshots(state.sourceUser, state.candidateUser),
|
|
231
269
|
...(state.opportunityId && { opportunityId: state.opportunityId }),
|
|
232
270
|
maxTurns,
|
|
233
271
|
isContinuation,
|
|
234
272
|
priorTurnCount: priorTurns.length,
|
|
273
|
+
...(exactContinuation ? {
|
|
274
|
+
resumeFromTaskId: exactContinuation.taskId,
|
|
275
|
+
continuationSettlementId: exactContinuation.settlementId,
|
|
276
|
+
} : {}),
|
|
235
277
|
};
|
|
236
278
|
if (state.opportunityId && Boolean(state.opportunityStatus) !== Boolean(state.opportunityUpdatedAt)) {
|
|
237
279
|
throw new Error('Negotiation attempt requires both opportunity status and updatedAt');
|
|
238
280
|
}
|
|
239
|
-
const task =
|
|
240
|
-
?
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
281
|
+
const task = exactContinuation
|
|
282
|
+
? claimedSuccessor
|
|
283
|
+
: state.opportunityId && state.opportunityStatus && state.opportunityUpdatedAt
|
|
284
|
+
? await database.createNegotiationTaskForAttempt({
|
|
285
|
+
conversationId: conversation.id,
|
|
286
|
+
opportunityId: state.opportunityId,
|
|
287
|
+
expectedStatus: state.opportunityStatus,
|
|
288
|
+
expectedUpdatedAt: state.opportunityUpdatedAt,
|
|
289
|
+
metadata: taskMetadata,
|
|
290
|
+
})
|
|
291
|
+
: await database.createTask(conversation.id, taskMetadata);
|
|
248
292
|
if (!task) {
|
|
249
293
|
throw new Error('Negotiation attempt is stale or already claimed');
|
|
250
294
|
}
|
|
@@ -282,6 +326,9 @@ export class NegotiationGraphFactory {
|
|
|
282
326
|
protocolVersion,
|
|
283
327
|
priorTurnCount: priorTurns.length,
|
|
284
328
|
...(userAnswers.length > 0 && { userAnswers }),
|
|
329
|
+
...(exactContinuation?.execution.consultation
|
|
330
|
+
? { privateConsultation: exactContinuation.execution.consultation }
|
|
331
|
+
: {}),
|
|
285
332
|
...(seedMessages.length > 0 && { messages: seedMessages }),
|
|
286
333
|
};
|
|
287
334
|
}
|
|
@@ -360,7 +407,7 @@ export class NegotiationGraphFactory {
|
|
|
360
407
|
screenedAt: new Date().toISOString(),
|
|
361
408
|
durationMs,
|
|
362
409
|
};
|
|
363
|
-
await database.setTaskScreenDecision?.(state.taskId, record).catch((err) => {
|
|
410
|
+
await database.setTaskScreenDecision?.(state.taskId, record, state.continuationExecution).catch((err) => {
|
|
364
411
|
screenNodeLog.error("Failed to persist screen decision", { taskId: state.taskId, error: err });
|
|
365
412
|
});
|
|
366
413
|
screenNodeLog.info("negotiation_screen", {
|
|
@@ -410,6 +457,7 @@ export class NegotiationGraphFactory {
|
|
|
410
457
|
const isSource = state.currentSpeaker === "source";
|
|
411
458
|
const ownUser = isSource ? state.sourceUser : state.candidateUser;
|
|
412
459
|
const otherUser = isSource ? state.candidateUser : state.sourceUser;
|
|
460
|
+
const ownIntentId = isSource ? state.sourceIntentId : state.candidateIntentId;
|
|
413
461
|
// Determine if this is the system agent's final allowed turn
|
|
414
462
|
const maxTurns = state.maxTurns ?? 0;
|
|
415
463
|
const isFinalTurn = maxTurns > 0 && (state.turnCount + 1) >= maxTurns;
|
|
@@ -433,6 +481,8 @@ export class NegotiationGraphFactory {
|
|
|
433
481
|
&& !!questionerEnqueue
|
|
434
482
|
&& !!timeoutQueue?.enqueueAskUserExpiry
|
|
435
483
|
&& !!state.opportunityId
|
|
484
|
+
&& !!ownIntentId
|
|
485
|
+
&& !!state.indexContext.networkId
|
|
436
486
|
&& !(state.turnCount === 0 && !state.isContinuation)
|
|
437
487
|
&& !hasPriorAskUser(state.messages, ownUser.id);
|
|
438
488
|
// ─── Deadlock detection → persuasion→bargaining stance (IND-428) ──────
|
|
@@ -479,6 +529,9 @@ export class NegotiationGraphFactory {
|
|
|
479
529
|
allowedActions: [...allowedActionsFor(version, seat, isFinalTurn, { askUser: askUserAvailable })],
|
|
480
530
|
...(state.discoveryQuery && isSource && { discoveryQuery: state.discoveryQuery }),
|
|
481
531
|
...(ownMemory.length > 0 && { negotiatorMemory: ownMemory }),
|
|
532
|
+
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
533
|
+
? { privateConsultation: state.privateConsultation }
|
|
534
|
+
: {}),
|
|
482
535
|
};
|
|
483
536
|
const scope = { action: 'manage:negotiations', scopeType: 'network', scopeId: state.indexContext.networkId };
|
|
484
537
|
const dispatchResult = await dispatcher.dispatch(ownUser.id, scope, payload, { timeoutMs: state.timeoutMs });
|
|
@@ -512,8 +565,11 @@ export class NegotiationGraphFactory {
|
|
|
512
565
|
// candidate-side turns would make the pickup prompt frame the
|
|
513
566
|
// search as "your user searched for X" for the wrong user.
|
|
514
567
|
...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
515
|
-
|
|
516
|
-
|
|
568
|
+
...(state.privateConsultation && state.privateConsultation.recipientUserId === ownUser.id
|
|
569
|
+
? { privateConsultation: state.privateConsultation }
|
|
570
|
+
: {}),
|
|
571
|
+
}, state.continuationExecution);
|
|
572
|
+
await database.updateTaskState(state.taskId, "waiting_for_agent", undefined, state.continuationExecution);
|
|
517
573
|
return { status: 'waiting_for_agent' };
|
|
518
574
|
}
|
|
519
575
|
else {
|
|
@@ -534,6 +590,9 @@ export class NegotiationGraphFactory {
|
|
|
534
590
|
...(askUserAvailable && { canAskUser: true }),
|
|
535
591
|
...(bargainingMode && { bargaining: { consecutiveNonConvergent: deadlock.consecutiveNonConvergent } }),
|
|
536
592
|
...(ownMemory.length > 0 && { memory: ownMemory }),
|
|
593
|
+
...(state.privateConsultation?.recipientUserId === ownUser.id
|
|
594
|
+
? { privateConsultation: state.privateConsultation }
|
|
595
|
+
: {}),
|
|
537
596
|
});
|
|
538
597
|
}
|
|
539
598
|
traceEmitter?.({ type: "agent_end", name: agentName, durationMs: Date.now() - agentStart, summary: `${turn.action}` });
|
|
@@ -576,7 +635,7 @@ export class NegotiationGraphFactory {
|
|
|
576
635
|
seat,
|
|
577
636
|
detectedAt: new Date().toISOString(),
|
|
578
637
|
};
|
|
579
|
-
await database.setTaskDeadlockShift?.(state.taskId, deadlockShiftRecord).catch((err) => {
|
|
638
|
+
await database.setTaskDeadlockShift?.(state.taskId, deadlockShiftRecord, state.continuationExecution).catch((err) => {
|
|
580
639
|
turnLog.error('Failed to persist deadlock shift record', { taskId: state.taskId, error: err });
|
|
581
640
|
});
|
|
582
641
|
turnLog.info('negotiation_deadlock_shift', {
|
|
@@ -606,6 +665,7 @@ export class NegotiationGraphFactory {
|
|
|
606
665
|
role: "agent",
|
|
607
666
|
parts,
|
|
608
667
|
taskId: state.taskId,
|
|
668
|
+
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
609
669
|
});
|
|
610
670
|
// ─── ask_user pause (P3.2) ────────────────────────────────────────────
|
|
611
671
|
// The negotiator consults its OWN client: persist the turn (done above),
|
|
@@ -615,50 +675,101 @@ export class NegotiationGraphFactory {
|
|
|
615
675
|
// like the waiting_for_agent suspend; the answer (or window expiry)
|
|
616
676
|
// resumes via the run-existing continuation path.
|
|
617
677
|
if (turn.action === 'ask_user') {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
678
|
+
const counterparty = isSource ? state.candidateUser : state.sourceUser;
|
|
679
|
+
const safeAskUser = validateInflightAskUserFields({
|
|
680
|
+
disclosureSubject: turn.askUser?.disclosureSubject,
|
|
681
|
+
draftQuestion: turn.askUser?.draftQuestion,
|
|
682
|
+
forbiddenIdentifiers: [
|
|
683
|
+
counterparty.id,
|
|
684
|
+
counterparty.profile.name ?? '',
|
|
685
|
+
state.opportunityId,
|
|
686
|
+
state.taskId,
|
|
687
|
+
state.indexContext.networkId,
|
|
688
|
+
isSource ? state.candidateIntentId ?? '' : state.sourceIntentId ?? '',
|
|
689
|
+
],
|
|
690
|
+
forbiddenSourceText: [
|
|
691
|
+
counterparty.profile.bio ?? '',
|
|
692
|
+
counterparty.profile.location ?? '',
|
|
693
|
+
...counterparty.intents.flatMap((intent) => [intent.title, intent.description]),
|
|
694
|
+
state.seedAssessment.reasoning,
|
|
695
|
+
state.indexContext.prompt,
|
|
696
|
+
state.discoveryQuery ?? '',
|
|
697
|
+
],
|
|
628
698
|
});
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
699
|
+
const settlementId = negotiationQuestionSettlementId(state.taskId);
|
|
700
|
+
const askUserBinding = await database.captureNegotiationAskUserBinding({
|
|
701
|
+
taskId: state.taskId,
|
|
702
|
+
settlementId,
|
|
703
|
+
recipientUserId: ownUser.id,
|
|
704
|
+
recipientIntentId: ownIntentId,
|
|
705
|
+
opportunityId: state.opportunityId,
|
|
706
|
+
networkId: state.indexContext.networkId,
|
|
707
|
+
turnContext: {
|
|
708
|
+
sourceUser: state.sourceUser,
|
|
709
|
+
candidateUser: state.candidateUser,
|
|
710
|
+
indexContext: state.indexContext,
|
|
711
|
+
seedAssessment: state.seedAssessment,
|
|
712
|
+
...(isSource && state.discoveryQuery && { discoveryQuery: state.discoveryQuery }),
|
|
713
|
+
},
|
|
714
|
+
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
715
|
+
});
|
|
716
|
+
// Arm the timer BEFORE flipping state: it is the durable recovery
|
|
717
|
+
// trigger even when generation enqueues no job or persists no row.
|
|
633
718
|
const windowMs = askUserAnswerWindowMs();
|
|
634
719
|
await timeoutQueue.enqueueAskUserExpiry(state.taskId, {
|
|
720
|
+
settlementId,
|
|
635
721
|
opportunityId: state.opportunityId,
|
|
636
722
|
userId: ownUser.id,
|
|
637
|
-
|
|
723
|
+
recipientIntentId: ownIntentId,
|
|
724
|
+
networkId: state.indexContext.networkId,
|
|
725
|
+
intentFingerprint: askUserBinding.intentFingerprint,
|
|
726
|
+
opportunityStatus: askUserBinding.opportunityStatus,
|
|
727
|
+
opportunityUpdatedAt: askUserBinding.opportunityUpdatedAt,
|
|
728
|
+
counterpartyUserId: askUserBinding.counterpartyUserId,
|
|
729
|
+
counterpartyIntentId: askUserBinding.counterpartyIntentId,
|
|
638
730
|
}, windowMs);
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
731
|
+
// Persistence admission requires the exact task to be input_required.
|
|
732
|
+
// Flip before enqueue; if the structured ask_user fields fail the
|
|
733
|
+
// deterministic privacy gate, the timer alone closes the exact task.
|
|
734
|
+
await database.updateTaskState(state.taskId, 'input_required', undefined, state.continuationExecution);
|
|
735
|
+
if (safeAskUser) {
|
|
736
|
+
const userContext = (await database.getUserContext(ownUser.id, null).catch(() => null))?.text ?? '';
|
|
737
|
+
await questionerEnqueue({
|
|
738
|
+
mode: 'negotiation_inflight',
|
|
739
|
+
purpose: 'inflight_consultation',
|
|
740
|
+
userId: ownUser.id,
|
|
741
|
+
sourceType: 'opportunity',
|
|
742
|
+
sourceId: state.opportunityId,
|
|
743
|
+
negotiation: {
|
|
744
|
+
purpose: 'inflight_consultation',
|
|
745
|
+
recipientUserId: ownUser.id,
|
|
746
|
+
recipientIntentId: ownIntentId,
|
|
747
|
+
opportunityId: state.opportunityId,
|
|
748
|
+
taskId: state.taskId,
|
|
749
|
+
networkId: state.indexContext.networkId,
|
|
750
|
+
},
|
|
751
|
+
context: {
|
|
752
|
+
negotiationId: state.taskId,
|
|
753
|
+
counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
|
|
754
|
+
disclosureSubject: safeAskUser.disclosureSubject,
|
|
755
|
+
...(safeAskUser.draftQuestion && { draftQuestion: safeAskUser.draftQuestion }),
|
|
756
|
+
indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
|
|
757
|
+
...(userContext && { userContext }),
|
|
758
|
+
},
|
|
759
|
+
}).catch((error) => {
|
|
760
|
+
turnLog.error('Failed to enqueue safe ask_user question; timeout recovery remains armed', {
|
|
761
|
+
taskId: state.taskId,
|
|
762
|
+
opportunityId: state.opportunityId,
|
|
763
|
+
error,
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
turnLog.warn('Skipping unsafe or incomplete ask_user question generation', {
|
|
769
|
+
taskId: state.taskId,
|
|
770
|
+
opportunityId: state.opportunityId,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
662
773
|
turnLog.info('negotiation_ask_user_pause', {
|
|
663
774
|
taskId: state.taskId,
|
|
664
775
|
opportunityId: state.opportunityId,
|
|
@@ -673,7 +784,7 @@ export class NegotiationGraphFactory {
|
|
|
673
784
|
negotiationConversationId: state.conversationId,
|
|
674
785
|
turnIndex: state.turnCount,
|
|
675
786
|
actor: isSource ? 'source' : 'candidate',
|
|
676
|
-
|
|
787
|
+
questionGenerationSafe: Boolean(safeAskUser),
|
|
677
788
|
windowMs,
|
|
678
789
|
});
|
|
679
790
|
return {
|
|
@@ -690,7 +801,7 @@ export class NegotiationGraphFactory {
|
|
|
690
801
|
...(deadlockShiftRecord && { deadlockShift: deadlockShiftRecord }),
|
|
691
802
|
};
|
|
692
803
|
}
|
|
693
|
-
await database.updateTaskState(state.taskId, "working");
|
|
804
|
+
await database.updateTaskState(state.taskId, "working", undefined, state.continuationExecution);
|
|
694
805
|
if (state.opportunityId) {
|
|
695
806
|
emitWide({
|
|
696
807
|
type: "negotiation_turn",
|
|
@@ -757,6 +868,17 @@ export class NegotiationGraphFactory {
|
|
|
757
868
|
const finalizeNode = async (state) => {
|
|
758
869
|
const traceEmitter = requestContext.getStore()?.traceEmitter;
|
|
759
870
|
const emitWide = (event) => traceEmitter?.(event);
|
|
871
|
+
const pauseReceipt = (outcome) => state.continuationExecution
|
|
872
|
+
? {
|
|
873
|
+
continuationReceipt: {
|
|
874
|
+
priorTaskId: state.continuationExecution.taskId,
|
|
875
|
+
settlementId: state.continuationExecution.settlementId,
|
|
876
|
+
successorTaskId: state.continuationExecution.successorTaskId,
|
|
877
|
+
fence: state.continuationExecution.fence,
|
|
878
|
+
outcome,
|
|
879
|
+
},
|
|
880
|
+
}
|
|
881
|
+
: undefined;
|
|
760
882
|
if (state.status === 'waiting_for_agent') {
|
|
761
883
|
if (state.opportunityId) {
|
|
762
884
|
emitWide({
|
|
@@ -767,7 +889,7 @@ export class NegotiationGraphFactory {
|
|
|
767
889
|
isContinuation: state.isContinuation,
|
|
768
890
|
});
|
|
769
891
|
}
|
|
770
|
-
return {};
|
|
892
|
+
return pauseReceipt('waiting_for_agent') ?? {};
|
|
771
893
|
}
|
|
772
894
|
// ask_user pause: no outcome, no completed state — the task stays
|
|
773
895
|
// input_required until the client answers or the window expires.
|
|
@@ -781,7 +903,7 @@ export class NegotiationGraphFactory {
|
|
|
781
903
|
isContinuation: state.isContinuation,
|
|
782
904
|
});
|
|
783
905
|
}
|
|
784
|
-
return {};
|
|
906
|
+
return pauseReceipt('input_required') ?? {};
|
|
785
907
|
}
|
|
786
908
|
// Init can fail closed before an attempt owns a task. Such a rejection is
|
|
787
909
|
// not a completed negotiation and must not write through an empty task ID,
|
|
@@ -827,12 +949,21 @@ export class NegotiationGraphFactory {
|
|
|
827
949
|
: {}),
|
|
828
950
|
};
|
|
829
951
|
try {
|
|
830
|
-
await database.updateTaskState(state.taskId, "completed");
|
|
952
|
+
await database.updateTaskState(state.taskId, "completed", undefined, state.continuationExecution);
|
|
831
953
|
await database.createArtifact({
|
|
832
954
|
taskId: state.taskId,
|
|
833
955
|
name: "negotiation-outcome",
|
|
834
956
|
parts: [{ kind: "data", data: outcome }],
|
|
835
|
-
metadata: {
|
|
957
|
+
metadata: {
|
|
958
|
+
hasOpportunity,
|
|
959
|
+
turnCount: state.turnCount,
|
|
960
|
+
...(state.continuationExecution ? {
|
|
961
|
+
continuationOutcome: hasOpportunity
|
|
962
|
+
? 'accepted'
|
|
963
|
+
: (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
|
|
964
|
+
} : {}),
|
|
965
|
+
},
|
|
966
|
+
...(state.continuationExecution ? { continuationExecution: state.continuationExecution } : {}),
|
|
836
967
|
});
|
|
837
968
|
finalizeLog.info('Session complete', {
|
|
838
969
|
conversationId: state.conversationId,
|
|
@@ -852,7 +983,7 @@ export class NegotiationGraphFactory {
|
|
|
852
983
|
: (screenedOut || isRejectLikeAction(lastTurn?.action))
|
|
853
984
|
? 'rejected'
|
|
854
985
|
: 'stalled';
|
|
855
|
-
await database.updateOpportunityStatus(state.opportunityId, nextStatus).catch((err) => {
|
|
986
|
+
await database.updateOpportunityStatus(state.opportunityId, nextStatus, undefined, state.continuationExecution).catch((err) => {
|
|
856
987
|
finalizeLog.error("Failed to update opportunity status", { opportunityId: state.opportunityId, nextStatus, error: err });
|
|
857
988
|
});
|
|
858
989
|
}
|
|
@@ -891,7 +1022,7 @@ export class NegotiationGraphFactory {
|
|
|
891
1022
|
// and forget: a reflection failure must never affect the outcome. Only
|
|
892
1023
|
// sessions that actually exchanged turns teach anything; init/turn
|
|
893
1024
|
// errors with turnCount 0 are skipped.
|
|
894
|
-
if (reflectEnqueue && state.turnCount > 0) {
|
|
1025
|
+
if (reflectEnqueue && state.turnCount > 0 && !state.continuationExecution) {
|
|
895
1026
|
reflectEnqueue({
|
|
896
1027
|
negotiationId: state.taskId,
|
|
897
1028
|
conversationId: state.conversationId,
|
|
@@ -915,24 +1046,36 @@ export class NegotiationGraphFactory {
|
|
|
915
1046
|
}
|
|
916
1047
|
// Enqueue question generation for stalled/capped negotiations (not accepted or explicitly rejected).
|
|
917
1048
|
// Require turnCount > 0 so early init/turn errors don't enqueue with empty context.
|
|
918
|
-
if (!hasOpportunity && !isRejectLikeAction(lastTurn?.action) && state.turnCount > 0 && state.opportunityId && questionerEnqueue) {
|
|
1049
|
+
if (!hasOpportunity && !isRejectLikeAction(lastTurn?.action) && state.turnCount > 0 && state.opportunityId && state.sourceIntentId && state.indexContext.networkId && questionerEnqueue && !state.continuationExecution) {
|
|
919
1050
|
const stallReason = atCap
|
|
920
1051
|
? 'turn_cap'
|
|
921
1052
|
: (state.error && /timeout/i.test(state.error))
|
|
922
1053
|
? 'timeout'
|
|
923
1054
|
: 'stalled';
|
|
924
1055
|
const userContext = (await database.getUserContext(state.sourceUser.id, null))?.text ?? '';
|
|
1056
|
+
const sourceIntent = state.sourceUser.intents.find((intent) => intent.id === state.sourceIntentId);
|
|
925
1057
|
questionerEnqueue({
|
|
926
1058
|
mode: 'negotiation',
|
|
1059
|
+
purpose: 'stalled_followup',
|
|
927
1060
|
userId: state.sourceUser.id,
|
|
928
1061
|
sourceType: 'opportunity',
|
|
929
1062
|
sourceId: state.opportunityId,
|
|
1063
|
+
negotiation: {
|
|
1064
|
+
purpose: 'stalled_followup',
|
|
1065
|
+
recipientUserId: state.sourceUser.id,
|
|
1066
|
+
recipientIntentId: state.sourceIntentId,
|
|
1067
|
+
opportunityId: state.opportunityId,
|
|
1068
|
+
taskId: state.taskId,
|
|
1069
|
+
networkId: state.indexContext.networkId,
|
|
1070
|
+
},
|
|
930
1071
|
context: {
|
|
931
1072
|
negotiationId: state.taskId,
|
|
932
|
-
counterpartyHint:
|
|
933
|
-
indexContext:
|
|
1073
|
+
counterpartyHint: NEGOTIATION_QUESTION_GENERIC_COUNTERPARTY,
|
|
1074
|
+
indexContext: NEGOTIATION_QUESTION_GENERIC_NETWORK,
|
|
934
1075
|
outcomeReason: stallReason,
|
|
935
|
-
|
|
1076
|
+
recipientIntent: sourceIntent
|
|
1077
|
+
? `${sourceIntent.title}: ${sourceIntent.description}`
|
|
1078
|
+
: 'the signal attached to this match',
|
|
936
1079
|
userContext,
|
|
937
1080
|
},
|
|
938
1081
|
}).catch((err) => finalizeLog.error('Failed to enqueue negotiation question generation', {
|
|
@@ -940,7 +1083,20 @@ export class NegotiationGraphFactory {
|
|
|
940
1083
|
error: err,
|
|
941
1084
|
}));
|
|
942
1085
|
}
|
|
943
|
-
|
|
1086
|
+
const terminalReceipt = state.continuationExecution
|
|
1087
|
+
? {
|
|
1088
|
+
continuationReceipt: {
|
|
1089
|
+
priorTaskId: state.continuationExecution.taskId,
|
|
1090
|
+
settlementId: state.continuationExecution.settlementId,
|
|
1091
|
+
successorTaskId: state.continuationExecution.successorTaskId,
|
|
1092
|
+
fence: state.continuationExecution.fence,
|
|
1093
|
+
outcome: hasOpportunity
|
|
1094
|
+
? 'accepted'
|
|
1095
|
+
: (screenedOut || isRejectLikeAction(lastTurn?.action)) ? 'rejected' : 'stalled',
|
|
1096
|
+
},
|
|
1097
|
+
}
|
|
1098
|
+
: undefined;
|
|
1099
|
+
return { outcome, status: 'completed', ...terminalReceipt };
|
|
944
1100
|
};
|
|
945
1101
|
const workflow = new StateGraph(NegotiationGraphState)
|
|
946
1102
|
.addNode("init", initNode)
|
|
@@ -972,7 +1128,7 @@ export class NegotiationGraphFactory {
|
|
|
972
1128
|
* @returns Only candidates that produced an opportunity
|
|
973
1129
|
*/
|
|
974
1130
|
export async function negotiateCandidates(negotiationGraph, sourceUser, candidates, indexContext, opts) {
|
|
975
|
-
const { maxTurns, traceEmitter, indexContextOverrides, timeoutMs, onCandidateResolved, trigger, initiatorUserId } = opts ?? {};
|
|
1131
|
+
const { maxTurns, traceEmitter, indexContextOverrides, timeoutMs, onCandidateResolved, trigger, initiatorUserId, resumeFromTaskId, continuationSettlementId, continuationExecution, } = opts ?? {};
|
|
976
1132
|
// Local helper to emit events whose shape is wider than the declared
|
|
977
1133
|
// `TraceEmitter` union (mirrors the cast used in chat.agent at the relay sink
|
|
978
1134
|
// and inside turn/finalize nodes above).
|
|
@@ -1001,6 +1157,8 @@ export async function negotiateCandidates(negotiationGraph, sourceUser, candidat
|
|
|
1001
1157
|
const result = await invokeWithAbortSignal(negotiationGraph, {
|
|
1002
1158
|
sourceUser,
|
|
1003
1159
|
candidateUser: candidate.candidateUser,
|
|
1160
|
+
...(candidate.sourceIntentId && { sourceIntentId: candidate.sourceIntentId }),
|
|
1161
|
+
...(candidate.candidateIntentId && { candidateIntentId: candidate.candidateIntentId }),
|
|
1004
1162
|
indexContext: candidateIndexContext,
|
|
1005
1163
|
seedAssessment: {
|
|
1006
1164
|
reasoning: candidate.reasoning,
|
|
@@ -1011,6 +1169,11 @@ export async function negotiateCandidates(negotiationGraph, sourceUser, candidat
|
|
|
1011
1169
|
...(candidate.opportunityStatus && { opportunityStatus: candidate.opportunityStatus }),
|
|
1012
1170
|
...(candidate.opportunityUpdatedAt && { opportunityUpdatedAt: candidate.opportunityUpdatedAt }),
|
|
1013
1171
|
...(initiatorUserId && { initiatorUserId }),
|
|
1172
|
+
...(resumeFromTaskId && continuationSettlementId && continuationExecution ? {
|
|
1173
|
+
resumeFromTaskId,
|
|
1174
|
+
continuationSettlementId,
|
|
1175
|
+
continuationExecution,
|
|
1176
|
+
} : {}),
|
|
1014
1177
|
...(maxTurns !== undefined && { maxTurns }),
|
|
1015
1178
|
...(timeoutMs !== undefined && { timeoutMs }),
|
|
1016
1179
|
});
|
|
@@ -1064,6 +1227,7 @@ export async function negotiateCandidates(negotiationGraph, sourceUser, candidat
|
|
|
1064
1227
|
accepted,
|
|
1065
1228
|
turns: turnHistory,
|
|
1066
1229
|
outcome: resolvedOutcome,
|
|
1230
|
+
...(result.continuationReceipt ? { continuationReceipt: result.continuationReceipt } : {}),
|
|
1067
1231
|
});
|
|
1068
1232
|
}
|
|
1069
1233
|
catch (hookErr) {
|