@xfxstudio/claworld 0.1.1 → 0.1.2

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.
@@ -72,7 +72,8 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
72
72
  searchSchema: {
73
73
  mode: 'profile_overlap_search',
74
74
  inputFieldIds: ['intent', 'location', 'interests'],
75
- summary: 'Search active online members by intent, location, and shared interests before opening an A2A chat.',
75
+ summary:
76
+ 'Compatibility-only manual search over active online members by intent, location, and shared interests. Candidate feed review is the canonical path before request_chat.',
76
77
  hints: [
77
78
  'Search defaults to the viewer membership profile when no explicit query is provided.',
78
79
  'Only online members with an active world membership are returned.',
@@ -81,7 +82,8 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
81
82
  matching: {
82
83
  mode: 'scored_push',
83
84
  cadence: 'periodic',
84
- strategySummary: 'Score active memberships by intent, location, and interests; then inject world rules for a bounded A2A conversation.',
85
+ strategySummary:
86
+ 'Score active memberships by intent, location, and interests, deliver candidate summaries first, and route live contact through request_chat after review.',
85
87
  candidateSources: ['active_memberships'],
86
88
  },
87
89
  sessionTemplate: {
@@ -170,12 +172,14 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
170
172
  searchSchema: {
171
173
  mode: 'capability_overlap_search',
172
174
  inputFieldIds: ['capabilities', 'budgetBand'],
173
- summary: 'Search active online members by capability overlap and optional budget fit before creating a conversation.',
175
+ summary:
176
+ 'Compatibility-only manual search over active online members by capability overlap and optional budget fit. Candidate feed review is the canonical path before request_chat.',
174
177
  },
175
178
  matching: {
176
179
  mode: 'intent_filter',
177
180
  cadence: 'on_demand',
178
- strategySummary: 'Filter by capability overlap and then let agents negotiate fit in a short session.',
181
+ strategySummary:
182
+ 'Filter by capability overlap, deliver candidate summaries first, and let members request_chat before negotiating fit in a short session.',
179
183
  candidateSources: ['world_members'],
180
184
  },
181
185
  sessionTemplate: {
@@ -262,12 +266,14 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
262
266
  searchSchema: {
263
267
  mode: 'profile_overlap_search',
264
268
  inputFieldIds: ['targetRole', 'location', 'workMode'],
265
- summary: 'Search active online members by role fit, location, and work mode before starting screening.',
269
+ summary:
270
+ 'Compatibility-only manual search over active online members by role fit, location, and work mode. Candidate feed review is the canonical path before request_chat.',
266
271
  },
267
272
  matching: {
268
273
  mode: 'profile_overlap',
269
274
  cadence: 'periodic',
270
- strategySummary: 'Use target role, experience summary, and location/work mode as the first-pass scoring surface.',
275
+ strategySummary:
276
+ 'Use target role, experience summary, and location/work mode as the first-pass scoring surface, deliver candidate summaries, and route contact through request_chat after review.',
271
277
  candidateSources: ['world_members', 'search_results'],
272
278
  },
273
279
  sessionTemplate: {
@@ -5,7 +5,9 @@ const DEFAULT_CANDIDATE_FEED_LIMIT = 10;
5
5
  export const CANDIDATE_OBJECT_FIELDS = Object.freeze([
6
6
  'candidateId',
7
7
  'worldId',
8
+ 'targetAgentId',
8
9
  'sourceMembershipId',
10
+ 'requestChat',
9
11
  'profileSummary',
10
12
  'compatibilitySignals',
11
13
  'deliveryReason',
@@ -79,6 +81,16 @@ function normalizeValue(value, field = {}) {
79
81
  return normalized;
80
82
  }
81
83
 
84
+ function buildRequestChatPayload(world, candidateMembership) {
85
+ const targetAgentId = normalizeText(candidateMembership?.agentId, null);
86
+ if (!targetAgentId) return null;
87
+
88
+ return {
89
+ worldId: world.worldId,
90
+ targetAgentId,
91
+ };
92
+ }
93
+
82
94
  function normalizeComparableArray(value) {
83
95
  if (!Array.isArray(value)) return [];
84
96
  return [...new Set(value.map((entry) => normalizeText(entry, null)?.toLowerCase()).filter(Boolean))];
@@ -244,9 +256,15 @@ export function projectCandidateFeedModel(world) {
244
256
  profileSummaryFieldShape: PROFILE_SUMMARY_FIELD_FIELDS,
245
257
  compatibilitySignalFields: COMPATIBILITY_SIGNAL_FIELDS,
246
258
  deliveryReasonFields: DELIVERY_REASON_FIELDS,
259
+ requestChatAction: {
260
+ action: 'request_chat',
261
+ requiredFields: ['worldId', 'targetAgentId'],
262
+ summary:
263
+ 'After reviewing a candidate, create a world-scoped chat request with worldId and targetAgentId.',
264
+ },
247
265
  liveDeliveryEvent: projectLiveDeliveryEvent(world, candidateFields),
248
266
  summary:
249
- 'Active members can review candidate opportunities first, then decide whether a delivery or live session should happen.',
267
+ 'Active members can review candidate opportunities first, then call request_chat with the selected targetAgentId when they want to start a world-scoped conversation request.',
250
268
  status: 'phase1_candidate_feed',
251
269
  };
252
270
  }
@@ -265,11 +283,14 @@ function projectCandidateOpportunity({
265
283
  profileSnapshot,
266
284
  candidateMembership.membershipId,
267
285
  );
286
+ const requestChat = buildRequestChatPayload(world, candidateMembership);
268
287
 
269
288
  return {
270
289
  candidateId: `cand_${candidateMembership.membershipId}`,
271
290
  worldId: world.worldId,
272
291
  sourceMembershipId: candidateMembership.membershipId,
292
+ targetAgentId: requestChat?.targetAgentId || null,
293
+ requestChat,
273
294
  profileSummary: projectProfileSummary(world, profileSnapshot, candidateAgent),
274
295
  compatibilitySignals,
275
296
  deliveryReason: buildDeliveryReason(compatibilitySignals),
@@ -322,7 +343,7 @@ export function buildCandidateFeed({
322
343
  generatedAt,
323
344
  expiresAt,
324
345
  deliveryMode: 'agent_review_before_live_session',
325
- nextAction: 'review_candidates_before_requesting_live_session',
346
+ nextAction: 'review_candidates_then_request_chat',
326
347
  candidateModel: projectCandidateFeedModel(world),
327
348
  candidates,
328
349
  status: 'feed_ready',
@@ -133,7 +133,7 @@ function normalizeSearchSchema(searchSchema = {}, joinSchema = {}) {
133
133
  : 10,
134
134
  summary: normalizeText(
135
135
  searchSchema.summary,
136
- 'Search active online world members by world-profile overlap before opening a conversation.',
136
+ 'Compatibility-only manual search over active online world members. Candidate feed review is the canonical path before request_chat.',
137
137
  ),
138
138
  hints: normalizeStringList(searchSchema.hints),
139
139
  };
@@ -259,7 +259,7 @@ export function projectJoinPlan(world) {
259
259
  requiredFields: world.joinSchema.requiredFields,
260
260
  optionalFields: world.joinSchema.optionalFields,
261
261
  hints: world.joinSchema.hints,
262
- nextAction: 'collect_profile_fields_then_call_join',
262
+ nextAction: 'call_join_world',
263
263
  };
264
264
  }
265
265
 
@@ -290,7 +290,7 @@ export function projectSearchModel(world) {
290
290
  defaultLimit: world.searchSchema.defaultLimit,
291
291
  summary: world.searchSchema.summary,
292
292
  hints: world.searchSchema.hints,
293
- status: 'phase1_world_search',
293
+ status: 'compatibility_world_search',
294
294
  };
295
295
  }
296
296
 
@@ -190,7 +190,7 @@ function normalizeWorldDetail(payload = {}) {
190
190
  requiredFields,
191
191
  optionalFields,
192
192
  hints: normalizeStringList(payload.hints),
193
- nextAction: normalizeText(payload.nextAction, 'collect_profile_fields_then_call_join'),
193
+ nextAction: normalizeText(payload.nextAction, 'call_join_world'),
194
194
  sessionOverview: payload.sessionOverview && typeof payload.sessionOverview === 'object' ? payload.sessionOverview : {},
195
195
  matchingOverview: payload.matchingOverview && typeof payload.matchingOverview === 'object' ? payload.matchingOverview : {},
196
196
  searchSchema: normalizeSearchSchema(payload.searchSchema || {}, {
@@ -247,7 +247,7 @@ function normalizeWorldDetail(payload = {}) {
247
247
  requiredFields,
248
248
  optionalFields,
249
249
  hints: normalizeStringList(payload.hints || joinSchema.hints),
250
- nextAction: normalizeText(payload.nextAction || joinSchema.nextAction, 'collect_profile_fields_then_call_join'),
250
+ nextAction: normalizeText(payload.nextAction || joinSchema.nextAction, 'call_join_world'),
251
251
  sessionOverview,
252
252
  matchingOverview,
253
253
  searchSchema: normalizeSearchSchema(searchOverview, {
@@ -301,7 +301,7 @@ function normalizeJoinCheckResponse(payload = {}, { worldId = null, profile = {}
301
301
  ),
302
302
  nextAction: normalizeText(
303
303
  payload.nextAction,
304
- accepted ? 'join_world_when_membership_persistence_exists' : 'collect_missing_profile_fields',
304
+ accepted ? 'call_join_world' : 'retry_join_world_after_profile_update',
305
305
  ),
306
306
  };
307
307
  }
@@ -373,11 +373,23 @@ function normalizeCandidateScoreBreakdown(entries = []) {
373
373
 
374
374
  function normalizeCandidate(candidate = {}, index = 0) {
375
375
  const normalizedRank = normalizeNumber(candidate.rank, null);
376
+ const targetAgentId = normalizeText(
377
+ candidate.targetAgentId || candidate.requestChat?.targetAgentId,
378
+ null,
379
+ );
380
+ const requestChat = targetAgentId
381
+ ? {
382
+ worldId: normalizeText(candidate.requestChat?.worldId, normalizeText(candidate.worldId, 'unknown-world')),
383
+ targetAgentId,
384
+ }
385
+ : null;
376
386
 
377
387
  return {
378
388
  candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
379
389
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
380
390
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
391
+ targetAgentId,
392
+ requestChat,
381
393
  profileSummary: normalizeCandidateProfileSummary(candidate.profileSummary),
382
394
  compatibilitySignals: Array.isArray(candidate.compatibilitySignals)
383
395
  ? candidate.compatibilitySignals.map((signal, signalIndex) => normalizeCompatibilitySignal(signal, signalIndex))
@@ -406,7 +418,7 @@ function normalizeCandidateFeedResponse(payload = {}, { worldId = null, agentId
406
418
  deliveryMode: normalizeText(payload.deliveryMode, 'agent_review_before_live_session'),
407
419
  nextAction: normalizeText(
408
420
  payload.nextAction,
409
- candidates.length > 0 ? 'review_candidates_before_requesting_live_session' : 'wait_for_more_candidates',
421
+ candidates.length > 0 ? 'review_candidates_then_request_chat' : 'wait_for_more_candidates',
410
422
  ),
411
423
  candidateSource: normalizeText(payload.candidateSource, 'active_memberships'),
412
424
  candidateModel: payload.candidateModel && typeof payload.candidateModel === 'object' ? payload.candidateModel : {},
@@ -443,12 +455,10 @@ function formatSessionOverview(detail = {}) {
443
455
  : {};
444
456
  const mode = normalizeText(detail.sessionMode || sessionOverview.mode, null);
445
457
  const maxTurns = normalizeInteger(sessionOverview.maxTurns, null);
446
- const turnTimeoutMs = normalizeInteger(sessionOverview.turnTimeoutMs, null);
447
458
  const parts = [];
448
459
 
449
460
  if (mode) parts.push(`${mode} mode`);
450
461
  if (maxTurns != null) parts.push(`max ${maxTurns} turns`);
451
- if (turnTimeoutMs != null) parts.push(`${turnTimeoutMs}ms turn timeout`);
452
462
 
453
463
  return parts.length > 0 ? parts.join(', ') : null;
454
464
  }
@@ -490,6 +500,7 @@ export function buildWorldSessionStartupText(detail = {}) {
490
500
  summary ? `Summary: ${summary}` : null,
491
501
  sessionSummary ? `Session overview: ${sessionSummary}` : null,
492
502
  raiseHandSummary ? `Completion rule: ${raiseHandSummary}` : null,
503
+ 'Interruption handling: prefer reconnect/resume. Temporary silence or reconnect churn is not the normal way to close a round.',
493
504
  openingText ? `Opening focus: ${openingText}` : null,
494
505
  interactionRules ? `Interaction rules: ${interactionRules}` : null,
495
506
  prohibitedRules ? `Prohibited rules: ${prohibitedRules}` : null,
@@ -544,7 +555,7 @@ function buildSelectionRetryContract(status, selection, items = [], matches = []
544
555
  stage: 'post_setup_world_selection_retry',
545
556
  system: 'The world choice matched multiple worlds. Show the narrowed list and ask the user to pick one exact world ID or display name.',
546
557
  user: `The choice ${choiceLabel} is ambiguous. Matching worlds: ${joinAsNaturalLanguage(retrySummary)}. Ask the user to choose one exact world ID or display name.`,
547
- followUp: 'Once the user confirms one exact world, fetch its detail and explain the required fields before join-check.',
558
+ followUp: 'Once the user confirms one exact world, fetch its detail, explain the required fields, and use join_world when enough profile data is available.',
548
559
  },
549
560
  };
550
561
  }
@@ -564,7 +575,7 @@ function buildSelectionRetryContract(status, selection, items = [], matches = []
564
575
  user: retrySummary.length > 0
565
576
  ? `I could not match ${choiceLabel} to an available world. Available worlds: ${joinAsNaturalLanguage(retrySummary)}. Ask the user to choose one by world ID or display name.`
566
577
  : 'No worlds are currently available. Tell the user setup is complete but world selection cannot continue yet.',
567
- followUp: 'Once the user chooses a valid world, confirm it, fetch the world detail, and explain the required fields before join-check.',
578
+ followUp: 'Once the user chooses a valid world, confirm it, fetch the world detail, and explain the required fields before calling join_world.',
568
579
  },
569
580
  };
570
581
  }
@@ -588,7 +599,7 @@ export function buildWorldSelectionPrompt(worldDirectory = {}) {
588
599
  ? `Available worlds:\n${worldLines.join('\n')}\nAsk the user which world they want to join next.`
589
600
  : 'No worlds are currently available. Tell the user setup is complete but no worlds can be selected yet.',
590
601
  followUp:
591
- 'After the user chooses a world, confirm the selection, fetch that world detail, explain the required fields, and then use join-check for that world.',
602
+ 'After the user chooses a world, confirm the selection, fetch that world detail, explain the required fields, and then use join_world for that world.',
592
603
  };
593
604
  }
594
605
 
@@ -653,7 +664,7 @@ export function resolveWorldSelection(worldDirectory = {}, selection = null) {
653
664
  system: 'Confirm the resolved world choice before fetching detail and explaining the required fields.',
654
665
  user: `I matched the user choice to ${selectedWorld.displayName} [${selectedWorld.worldId}]. Confirm that this is the world we will use next.`,
655
666
  confirmation: `Confirmed world: ${selectedWorld.displayName} [${selectedWorld.worldId}].`,
656
- followUp: 'Fetch the selected world detail and explain the required fields one by one before join-check.',
667
+ followUp: 'Fetch the selected world detail, explain the required fields, and use join_world once enough profile data is available.',
657
668
  },
658
669
  };
659
670
  }
@@ -678,7 +689,7 @@ export function buildRequiredFieldExplanation(worldDetail = {}) {
678
689
  const optionalFieldLabels = optionalFields.map((field) => field.label);
679
690
  const summary = requiredFields.length > 0
680
691
  ? `To join ${detail.displayName}, I need ${requiredFields.length} required field${requiredFields.length === 1 ? '' : 's'}: ${joinAsNaturalLanguage(requiredFieldLabels)}.`
681
- : `${detail.displayName} does not require any mandatory profile fields before join-check.`;
692
+ : `${detail.displayName} does not require any mandatory profile fields before join_world.`;
682
693
  const steps = requiredFields.map((field, index) => ({
683
694
  step: index + 1,
684
695
  fieldId: field.fieldId,
@@ -691,7 +702,7 @@ export function buildRequiredFieldExplanation(worldDetail = {}) {
691
702
  const optionalContextSummary = optionalFieldLabels.length > 0
692
703
  ? `Optional context you can add later: ${joinAsNaturalLanguage(optionalFieldLabels)}.`
693
704
  : null;
694
- const nextInstruction = steps[0]?.prompt || 'All required fields are already explained. You can move to join-check.';
705
+ const nextInstruction = steps[0]?.prompt || 'All required fields are already explained. You can move to join_world.';
695
706
 
696
707
  return {
697
708
  status: 'ready',
@@ -707,14 +718,14 @@ export function buildRequiredFieldExplanation(worldDetail = {}) {
707
718
  nextAction: detail.nextAction,
708
719
  orchestration: {
709
720
  stage: 'post_setup_world_requirements',
710
- system: 'Confirm the selected world, explain its required fields in plain language, and collect them one at a time before join-check.',
721
+ system: 'Confirm the selected world, explain its required fields in plain language, and use join_world once the available profile data is enough for the backend to validate.',
711
722
  confirmation: `Confirmed world: ${detail.displayName} [${detail.worldId}].`,
712
723
  user: [summary, nextInstruction, optionalContextSummary].filter(Boolean).join('\n\n'),
713
724
  followUp: requiredFields.length > 1
714
- ? `After the user answers ${steps[0].label}, continue with the remaining required fields in order, then call join-check.`
725
+ ? `After the user answers ${steps[0].label}, continue with the remaining required fields in order, then call join_world.`
715
726
  : (requiredFields.length === 1
716
- ? 'After the user answers the required field, call join-check.'
717
- : 'No required fields remain. Call join-check now.'),
727
+ ? 'After the user answers the required field, call join_world.'
728
+ : 'No required fields remain. Call join_world now.'),
718
729
  },
719
730
  };
720
731
  }
@@ -778,10 +789,10 @@ function buildProfileCollectionFollowUp(promptFields = []) {
778
789
 
779
790
  const labels = promptFields.map((field) => field.label);
780
791
  if (promptFields.length === 1) {
781
- return `After the user answers ${labels[0]}, merge it into the saved profile draft and re-run join-check before asking anything else.`;
792
+ return `After the user answers ${labels[0]}, merge it into the saved profile draft and retry join_world before asking anything else.`;
782
793
  }
783
794
 
784
- return `After the user answers ${joinAsNaturalLanguage(labels)}, merge those fields into the saved profile draft and re-run join-check before asking anything else.`;
795
+ return `After the user answers ${joinAsNaturalLanguage(labels)}, merge those fields into the saved profile draft and retry join_world before asking anything else.`;
785
796
  }
786
797
 
787
798
  export function buildWorldProfileCollectionFlow({
@@ -848,7 +859,7 @@ export function buildWorldProfileCollectionFlow({
848
859
  orchestration: {
849
860
  stage: 'post_setup_world_profile_collection',
850
861
  system:
851
- 'Use backend join-check guidance as the source of truth for the next required field prompt. After every user reply or the current batch checkpoint, merge the updates into the saved profile draft and re-run join-check.',
862
+ 'Use the backend join_world unmet-requirement guidance as the source of truth for the next required field prompt. After every user reply or the current batch checkpoint, merge the updates into the saved profile draft and retry join_world.',
852
863
  confirmation: `Confirmed world: ${detail.displayName} [${detail.worldId}].`,
853
864
  user: [summary, savedSummary, promptSummary, promptBody, optionalContextSummary].filter(Boolean).join('\n\n'),
854
865
  followUp: buildProfileCollectionFollowUp(promptFields),
@@ -870,13 +881,13 @@ export function buildWorldJoinOutcomeOrchestration({ worldDetail = {}, joinResul
870
881
  stage: 'world_join_result',
871
882
  status: membershipStatus,
872
883
  system:
873
- 'The backend already resolved the world join result. Reflect the authoritative membership outcome before any search, candidate review, or live-session follow-up.',
884
+ 'The backend already resolved the world join result. Reflect the authoritative membership outcome before any candidate-feed review, request_chat, or live-session follow-up.',
874
885
  confirmation: `World membership in ${detail.displayName} [${detail.worldId}] is ${membershipStatus}.`,
875
886
  user: membershipStatus === 'active'
876
887
  ? ['Joined ' + detail.displayName + ' successfully. World membership is active.', joinSummary].filter(Boolean).join(' ')
877
888
  : `The join result for ${detail.displayName} is ${membershipStatus}.`,
878
889
  followUp: membershipStatus === 'active'
879
- ? 'Use backend-authored next-stage summary and candidate-delivery payloads for the next world step.'
890
+ ? 'Use the backend-authored candidate-feed and candidate-delivery payloads for the next world step, and keep request_chat as the canonical conversation-start action.'
880
891
  : 'Do not continue to world-member-only follow-up until membership becomes active.',
881
892
  };
882
893
  }
@@ -923,6 +934,13 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
923
934
  normalizeInteger(limit, normalizedFeed.candidates.length || normalizedFeed.totalCandidates || 1),
924
935
  );
925
936
  const displayName = detail?.displayName || normalizedFeed.worldId || 'the selected world';
937
+ const requestChatAction = {
938
+ action: 'request_chat',
939
+ worldId: normalizedFeed.worldId,
940
+ requiredFields: ['worldId', 'targetAgentId'],
941
+ summary:
942
+ 'After the user chooses a candidate, request_chat with this worldId and the candidate targetAgentId.',
943
+ };
926
944
  const candidateSummaries = normalizedFeed.candidates.slice(0, summaryLimit).map((candidate, index) => {
927
945
  const name = candidate.profileSummary.displayName || `Candidate ${index + 1}`;
928
946
  const requiredFieldSummary = summarizeProfileFields(candidate.profileSummary.requiredFields);
@@ -946,6 +964,8 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
946
964
  return {
947
965
  candidateId: candidate.candidateId,
948
966
  sourceMembershipId: candidate.sourceMembershipId,
967
+ targetAgentId: candidate.targetAgentId,
968
+ requestChat: candidate.requestChat,
949
969
  displayName: name,
950
970
  headline: candidate.profileSummary.headline,
951
971
  rank: candidate.rank,
@@ -969,10 +989,13 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
969
989
  : 'No candidates are currently available from the active-membership feed.';
970
990
 
971
991
  return {
992
+ worldId: normalizedFeed.worldId,
993
+ agentId: normalizedFeed.agentId,
972
994
  status: deliveredCandidateCount > 0 ? 'candidate_summary_ready' : 'candidate_summary_pending',
973
995
  deliveredCandidateCount,
974
996
  totalCandidateCount,
975
997
  remainingCandidateCount,
998
+ requestChatAction,
976
999
  candidateSummaries,
977
1000
  nextAction: deliveredCandidateCount > 0
978
1001
  ? normalizedFeed.nextAction
@@ -980,13 +1003,13 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
980
1003
  orchestration: {
981
1004
  stage: 'post_join_candidate_delivery',
982
1005
  system:
983
- 'Use the backend-authored candidate summaries already attached to this payload. Do not invent additional candidate-delivery business text locally.',
1006
+ 'Use the backend-authored candidate summaries already attached to this payload. Candidate requestChat payloads are the canonical follow-up inputs for world-scoped contact establishment.',
984
1007
  confirmation: `Candidate review payload for ${displayName} [${normalizedFeed.worldId}].`,
985
1008
  user: [heading, promptBody].filter(Boolean).join('\n\n'),
986
1009
  followUp: deliveredCandidateCount > 0
987
1010
  ? (remainingCandidateCount > 0
988
- ? `Share these ${deliveredCandidateCount} candidate summaries first. If the user wants more, continue with the remaining ${remainingCandidateCount} candidate${remainingCandidateCount === 1 ? '' : 's'} from the same feed before requesting a live session.`
989
- : 'Share these candidate summaries and ask whether the user wants to review them further or continue toward a live session later.')
1011
+ ? `Share these ${deliveredCandidateCount} candidate summaries first. If the user chooses someone now, continue with request_chat using that candidate's {worldId, targetAgentId}. If they want more options first, continue with the remaining ${remainingCandidateCount} candidate${remainingCandidateCount === 1 ? '' : 's'} from the same feed.`
1012
+ : 'Share these candidate summaries and, if the user chooses one, continue with request_chat using the attached {worldId, targetAgentId} payload for that candidate.')
990
1013
  : 'Tell the user candidate delivery can be retried later through the same backend-authored world flow.',
991
1014
  },
992
1015
  };
@@ -69,15 +69,14 @@ function createJoinNotEligibleError(joinCheck) {
69
69
  error.responseBody = {
70
70
  error: error.code,
71
71
  message: 'profile does not satisfy world join requirements',
72
+ status: 'needs_profile',
72
73
  membershipStatus: 'inactive',
73
74
  worldId: joinCheck.worldId,
75
+ normalizedProfile: joinCheck.normalizedProfile,
74
76
  missingFields: joinCheck.missingFields,
75
77
  nextMissingField: joinCheck.nextMissingField,
76
78
  missingFieldGuidance: joinCheck.missingFieldGuidance,
77
- normalizedProfile: joinCheck.normalizedProfile,
78
- nextAction: joinCheck.nextAction,
79
- joinCheck,
80
- profileCollectionFlow: joinCheck.profileCollectionFlow || null,
79
+ nextAction: 'retry_join_world_after_profile_update',
81
80
  };
82
81
  return error;
83
82
  }
@@ -98,12 +97,14 @@ function normalizeProfileSnapshot(...candidates) {
98
97
 
99
98
  function buildNextStageSummary(world) {
100
99
  const matchingSummary = world.matching.strategySummary
101
- || `The world uses ${world.matching.mode} matching before session handoff.`;
100
+ || `The world uses ${world.matching.mode} matching to deliver candidate summaries before request_chat.`;
101
+ const reviewSummary =
102
+ 'Review the backend-authored candidate feed, choose a candidate, and create a world-scoped chat request.';
102
103
  const sessionSummary = `Matched agents then enter a ${world.sessionTemplate.mode} session with up to ${world.sessionTemplate.maxTurns} turns.`;
103
104
 
104
105
  return {
105
106
  stage: 'matchmaking',
106
- summary: `${matchingSummary} ${sessionSummary}`.trim(),
107
+ summary: `${matchingSummary} ${reviewSummary} ${sessionSummary}`.trim(),
107
108
  matchingMode: world.matching.mode,
108
109
  matchingCadence: world.matching.cadence,
109
110
  sessionMode: world.sessionTemplate.mode,
@@ -143,7 +144,7 @@ export function createMembershipService({ worldService, store = null } = {}) {
143
144
  },
144
145
  normalizedProfile,
145
146
  nextAction:
146
- missingFields.length === 0 ? 'join_world_when_membership_persistence_exists' : 'collect_missing_profile_fields',
147
+ missingFields.length === 0 ? 'call_join_world' : 'retry_join_world_after_profile_update',
147
148
  };
148
149
 
149
150
  const worldDetail = worldService.describeWorldDetail(world.worldId);
@@ -242,15 +243,21 @@ export function createMembershipService({ worldService, store = null } = {}) {
242
243
  });
243
244
 
244
245
  return {
246
+ status: 'joined',
247
+ worldId: world.worldId,
245
248
  membership,
246
249
  membershipStatus: membership.status,
247
250
  created: !existingMembership,
251
+ normalizedProfile: joinCheck.normalizedProfile,
252
+ nextAction: 'review_candidate_feed',
248
253
  nextStageSummary: buildNextStageSummary(world),
249
254
  orchestration: buildWorldJoinOutcomeOrchestration({
250
255
  worldDetail: worldService.describeWorldDetail(world.worldId),
251
256
  joinResult: {
252
257
  membershipStatus: membership.status,
253
258
  membership,
259
+ normalizedProfile: joinCheck.normalizedProfile,
260
+ nextAction: 'review_candidate_feed',
254
261
  nextStageSummary: buildNextStageSummary(world),
255
262
  },
256
263
  }),
@@ -348,7 +348,7 @@ export function createWorldSearchService({
348
348
  limit: normalizedLimit,
349
349
  totalMatches: orderedItems.length,
350
350
  items: orderedItems.slice(0, normalizedLimit),
351
- nextAction: orderedItems.length > 0 ? 'select_player_and_start_conversation' : 'broaden_search_or_wait',
351
+ nextAction: orderedItems.length > 0 ? 'request_chat_with_selected_candidate' : 'broaden_search_or_wait',
352
352
  status: orderedItems.length > 0 ? 'search_ready' : 'no_matches',
353
353
  };
354
354
  },
@@ -227,7 +227,8 @@ function buildSearchSchema(entryProfileSchema) {
227
227
  return {
228
228
  mode: 'profile_overlap_search',
229
229
  inputFieldIds: entryProfileSchema.searchableFieldIds,
230
- summary: 'Search active online members by overlap on the world profile fields chosen by the world creator.',
230
+ summary:
231
+ 'Compatibility-only manual search over active online members. Candidate feed review is the canonical path before request_chat.',
231
232
  onlineOnly: true,
232
233
  defaultLimit: 10,
233
234
  };
@@ -238,7 +239,7 @@ function buildMatchingStrategy(entryProfileSchema) {
238
239
  mode: 'profile_overlap',
239
240
  cadence: 'on_demand',
240
241
  strategySummary:
241
- `Rank world members by overlap on ${entryProfileSchema.searchableFieldIds.join(', ')} before opening a conversation.`,
242
+ `Rank world members by overlap on ${entryProfileSchema.searchableFieldIds.join(', ')}, deliver candidate summaries first, and let members request_chat after review.`,
242
243
  candidateSources: ['active_memberships'],
243
244
  };
244
245
  }
@@ -2,6 +2,7 @@ import { authenticateAppTokenRequest, resolveAuthenticatedAgentId } from '../../
2
2
  import {
3
3
  buildCandidateDeliverySummary,
4
4
  buildRequiredFieldExplanation,
5
+ buildResolvedWorldJoinOrchestration,
5
6
  buildWorldSelectionPrompt,
6
7
  } from '../contracts/world-orchestration.js';
7
8
 
@@ -295,12 +296,33 @@ export function registerWorldRoutes(
295
296
  profileSnapshot: req.body?.profileSnapshot,
296
297
  maxFieldsPerStep: req.body?.maxFieldsPerStep,
297
298
  });
299
+ const worldDetail = worldService.describeWorldDetail(req.params.worldId);
300
+ const candidateFeed = matchmakingService.listCandidateFeed({
301
+ worldId: req.params.worldId,
302
+ agentId,
303
+ limit: req.body?.candidateLimit || null,
304
+ });
305
+ const candidateDelivery = buildCandidateDeliverySummary(candidateFeed, {
306
+ worldDetail,
307
+ limit: req.body?.candidateLimit || candidateFeed.limit || null,
308
+ });
298
309
  res.status(result.created ? 201 : 200).json({
310
+ status: result.status,
299
311
  worldId: req.params.worldId,
300
312
  membershipStatus: result.membershipStatus,
313
+ normalizedProfile: result.normalizedProfile,
301
314
  membership: result.membership,
315
+ nextAction: result.nextAction,
302
316
  nextStageSummary: result.nextStageSummary,
303
- orchestration: result.orchestration || null,
317
+ candidateFeed: {
318
+ ...candidateFeed,
319
+ candidateDelivery,
320
+ },
321
+ candidateDelivery,
322
+ orchestration: buildResolvedWorldJoinOrchestration({
323
+ joinResult: result,
324
+ candidateDelivery,
325
+ }) || result.orchestration || null,
304
326
  });
305
327
  } catch (error) {
306
328
  sendWorldError(res, error);