@xfxstudio/claworld 0.1.3 → 0.1.5

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.
@@ -517,7 +517,7 @@ export class ClaworldRelayClient extends EventEmitter {
517
517
  config,
518
518
  agentId,
519
519
  credential = null,
520
- clientVersion = 'claworld-plugin/0.1.3',
520
+ clientVersion = 'claworld-plugin/0.1.5',
521
521
  sessionTarget,
522
522
  fallbackTarget,
523
523
  } = {}) {
@@ -422,6 +422,7 @@ function normalizeCandidate(candidate = {}, index = 0) {
422
422
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
423
423
  targetAgentId: normalizeText(candidate.targetAgentId, null),
424
424
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
425
+ online: candidate.online === true,
425
426
  targetAgentId,
426
427
  requestChat,
427
428
  profileSummary: normalizeCandidateProfileSummary(candidate.profileSummary),
@@ -457,7 +458,7 @@ function normalizeCandidateFeedResponse(payload = {}, { worldId = null, agentId
457
458
  candidateDelivery: payload.candidateDelivery && typeof payload.candidateDelivery === 'object'
458
459
  ? payload.candidateDelivery
459
460
  : null,
460
- candidateSource: normalizeText(payload.candidateSource, 'active_memberships'),
461
+ candidateSource: normalizeText(payload.candidateSource, 'active_memberships_online'),
461
462
  candidateModel: payload.candidateModel && typeof payload.candidateModel === 'object' ? payload.candidateModel : {},
462
463
  strategy: payload.strategy && typeof payload.strategy === 'object' ? payload.strategy : {},
463
464
  limit: normalizeInteger(payload.limit, candidates.length),
@@ -230,6 +230,7 @@ function projectToolCandidateDeliverySummary(
230
230
  sourceMembershipId: normalizeText(summary.sourceMembershipId, null),
231
231
  displayName: normalizeText(summary.displayName, null),
232
232
  headline: normalizeText(summary.headline, null),
233
+ online: summary.online === true,
233
234
  rank: normalizeOptionalInteger(summary.rank, null),
234
235
  score: normalizeOptionalInteger(summary.score, null),
235
236
  targetAgentId: normalizeText(summary.targetAgentId, summary.requestChat?.targetAgentId || null),
@@ -383,6 +384,7 @@ function projectToolCandidateSummary(summary = {}, index = 0) {
383
384
  targetAgentId: normalizeText(summary.targetAgentId, null),
384
385
  displayName: normalizeText(summary.displayName, `Candidate ${index + 1}`),
385
386
  headline: normalizeText(summary.headline, null),
387
+ online: summary.online === true,
386
388
  rank: normalizeInteger(summary.rank, 0) || null,
387
389
  score: normalizeInteger(summary.score, 0) || null,
388
390
  summary: normalizeText(summary.summary, null),
@@ -405,6 +407,7 @@ function projectToolCandidateFeed(joinResult = {}) {
405
407
  targetAgentId: candidate.targetAgentId,
406
408
  displayName: candidate.profileSummary?.displayName,
407
409
  headline: candidate.profileSummary?.headline,
410
+ online: candidate.online === true,
408
411
  rank: candidate.rank,
409
412
  score: candidate.score,
410
413
  summary: normalizeText(candidate.deliveryReason?.summary, null),
@@ -783,6 +786,12 @@ function projectChatRequestKickoff(kickoff = {}) {
783
786
  return {
784
787
  status: normalizeText(kickoff.status, 'skipped'),
785
788
  deliveredAt: normalizeText(kickoff.deliveredAt, null),
789
+ relaySessionPreparedAt: normalizeText(kickoff.relaySessionPreparedAt, null),
790
+ acceptedRoundPreparedAt: normalizeText(kickoff.acceptedRoundPreparedAt, null),
791
+ senderKickoffDeliveredAt: normalizeText(kickoff.senderKickoffDeliveredAt, normalizeText(kickoff.deliveredAt, null)),
792
+ openerAcceptedAt: normalizeText(kickoff.openerAcceptedAt, null),
793
+ openerDeliveredAt: normalizeText(kickoff.openerDeliveredAt, null),
794
+ liveChatEstablishedAt: normalizeText(kickoff.liveChatEstablishedAt, null),
786
795
  reason: normalizeText(kickoff.reason, null),
787
796
  };
788
797
  }
@@ -883,8 +892,10 @@ export function projectToolChatRequestMutationResponse(result = {}, { accountId
883
892
  nextAction: normalizeText(
884
893
  result.nextAction,
885
894
  normalizedStatus === 'accepted'
886
- ? kickoff?.status === 'sent'
895
+ ? kickoff?.status === 'established'
887
896
  ? 'runtime_owns_live_conversation'
897
+ : kickoff?.status === 'sent'
898
+ ? 'wait_for_sender_opener_delivery'
888
899
  : kickoff?.status === 'failed'
889
900
  ? 'backend_kickoff_failed'
890
901
  : 'chat_request_accepted_without_opening_message'
@@ -1,27 +1,16 @@
1
1
  export const CLAWORLD_TOOL_CONTRACT_VERSION = 'v1';
2
2
 
3
- export const CLAWORLD_FRIEND_REQUEST_TOOL_NAMES = Object.freeze([
4
- 'claworld_send_friend_request',
5
- 'claworld_list_friend_requests',
6
- 'claworld_accept_friend_request',
7
- 'claworld_reject_friend_request',
8
- ]);
9
-
10
3
  export const CLAWORLD_CHAT_REQUEST_TOOL_NAMES = Object.freeze([
11
4
  'claworld_request_chat',
12
5
  'claworld_list_chat_requests',
13
6
  'claworld_accept_chat_request',
14
7
  ]);
15
8
 
16
- export const CLAWORLD_REQUEST_TOOL_NAMES = Object.freeze([
17
- ...CLAWORLD_FRIEND_REQUEST_TOOL_NAMES,
18
- ...CLAWORLD_CHAT_REQUEST_TOOL_NAMES,
9
+ export const CLAWORLD_BOOTSTRAP_TOOL_NAMES = Object.freeze([
10
+ 'claworld_pair_agent',
19
11
  ]);
20
12
 
21
- export const CLAWORLD_CORE_TOOL_NAMES = Object.freeze([
22
- ...CLAWORLD_REQUEST_TOOL_NAMES,
23
- 'claworld_pair_agent',
24
- 'claworld_resolve_agent',
13
+ export const CLAWORLD_FEEDBACK_TOOL_NAMES = Object.freeze([
25
14
  'claworld_submit_feedback',
26
15
  ]);
27
16
 
@@ -29,27 +18,38 @@ export const CLAWORLD_WORLD_TOOL_NAMES = Object.freeze([
29
18
  'claworld_list_worlds',
30
19
  'claworld_get_world_detail',
31
20
  'claworld_join_world',
32
- 'claworld_broadcast_world',
33
21
  ]);
34
22
 
35
- export const CLAWORLD_OPTIONAL_WORLD_HELPER_TOOL_NAMES = Object.freeze([
23
+ export const CLAWORLD_WORLD_ADMIN_PUBLIC_TOOL_NAMES = Object.freeze([
24
+ 'claworld_create_world',
25
+ ]);
26
+
27
+ export const CLAWORLD_COMPATIBILITY_TOOL_NAMES = Object.freeze([
36
28
  'claworld_prepare_world_join',
37
29
  'claworld_search_world',
38
30
  ]);
39
31
 
40
- export const CLAWORLD_ADVANCED_TOOL_NAMES = Object.freeze([
41
- 'claworld_create_world',
32
+ export const CLAWORLD_RETIRED_PUBLIC_TOOL_NAMES = Object.freeze([
33
+ 'claworld_send_friend_request',
34
+ 'claworld_list_friend_requests',
35
+ 'claworld_accept_friend_request',
36
+ 'claworld_reject_friend_request',
37
+ 'claworld_broadcast_world',
42
38
  'claworld_list_owned_worlds',
43
39
  'claworld_manage_world',
40
+ 'claworld_resolve_agent',
44
41
  ]);
45
42
 
46
- export const CLAWORLD_PUBLIC_TOOL_NAMES = Object.freeze([
43
+ export const CLAWORLD_REGISTERED_TOOL_NAMES = Object.freeze([
44
+ ...CLAWORLD_BOOTSTRAP_TOOL_NAMES,
47
45
  ...CLAWORLD_WORLD_TOOL_NAMES,
48
- ...CLAWORLD_REQUEST_TOOL_NAMES,
49
- 'claworld_submit_feedback',
50
- ...CLAWORLD_ADVANCED_TOOL_NAMES,
51
- 'claworld_pair_agent',
52
- 'claworld_resolve_agent',
46
+ ...CLAWORLD_WORLD_ADMIN_PUBLIC_TOOL_NAMES,
47
+ ...CLAWORLD_CHAT_REQUEST_TOOL_NAMES,
48
+ ...CLAWORLD_FEEDBACK_TOOL_NAMES,
49
+ ]);
50
+
51
+ export const CLAWORLD_PUBLIC_TOOL_NAMES = Object.freeze([
52
+ ...CLAWORLD_REGISTERED_TOOL_NAMES,
53
53
  ]);
54
54
 
55
55
  export const CLAWORLD_MINIMAL_OPENCLAW_TOOL_NAMES = Object.freeze([
@@ -65,7 +65,11 @@ export const CLAWORLD_READ_ONLY_OPENCLAW_TOOL_NAMES = Object.freeze([
65
65
  ]);
66
66
 
67
67
  export const CLAWORLD_PLUGIN_SMOKE_REQUIRED_TOOL_NAMES = Object.freeze([
68
+ ...CLAWORLD_BOOTSTRAP_TOOL_NAMES,
68
69
  ...CLAWORLD_WORLD_TOOL_NAMES,
70
+ ...CLAWORLD_WORLD_ADMIN_PUBLIC_TOOL_NAMES,
71
+ ...CLAWORLD_CHAT_REQUEST_TOOL_NAMES,
72
+ ...CLAWORLD_FEEDBACK_TOOL_NAMES,
69
73
  ]);
70
74
 
71
75
  export const CLAWORLD_TOOL_PROFILES = Object.freeze({
@@ -83,8 +83,8 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
83
83
  mode: 'scored_push',
84
84
  cadence: 'periodic',
85
85
  strategySummary:
86
- 'Score active memberships by intent, location, and interests, deliver candidate summaries first, and route live contact through request_chat after review.',
87
- candidateSources: ['active_memberships'],
86
+ 'Score active online memberships by intent, location, and interests, deliver candidate summaries first, and route live contact through request_chat after review.',
87
+ candidateSources: ['active_memberships_online'],
88
88
  },
89
89
  sessionTemplate: {
90
90
  mode: 'a2a',
@@ -179,8 +179,8 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
179
179
  mode: 'intent_filter',
180
180
  cadence: 'on_demand',
181
181
  strategySummary:
182
- 'Filter by capability overlap, deliver candidate summaries first, and let members request_chat before negotiating fit in a short session.',
183
- candidateSources: ['world_members'],
182
+ 'Filter active online world members by capability overlap, deliver candidate summaries first, and let members request_chat before negotiating fit in a short session.',
183
+ candidateSources: ['active_memberships_online'],
184
184
  },
185
185
  sessionTemplate: {
186
186
  mode: 'a2a',
@@ -273,8 +273,8 @@ export const DEFAULT_WORLD_MANIFESTS = Object.freeze([
273
273
  mode: 'profile_overlap',
274
274
  cadence: 'periodic',
275
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.',
277
- candidateSources: ['world_members', 'search_results'],
276
+ 'Use active online memberships plus 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.',
277
+ candidateSources: ['active_memberships_online'],
278
278
  },
279
279
  sessionTemplate: {
280
280
  mode: 'a2a',
@@ -7,6 +7,7 @@ export const CANDIDATE_OBJECT_FIELDS = Object.freeze([
7
7
  'worldId',
8
8
  'targetAgentId',
9
9
  'sourceMembershipId',
10
+ 'online',
10
11
  'requestChat',
11
12
  'profileSummary',
12
13
  'compatibilitySignals',
@@ -180,7 +181,7 @@ function buildCompatibilitySignals(world, viewerProfile = {}, candidateProfile =
180
181
  type: 'world_ready',
181
182
  fieldIds: [],
182
183
  score: 0.05,
183
- summary: 'Candidate has an active world membership and is ready for agent review before live session handoff.',
184
+ summary: 'Candidate is online with an active world membership and is ready for agent review before live session handoff.',
184
185
  }),
185
186
  ];
186
187
  }
@@ -192,7 +193,7 @@ function buildDeliveryReason(signals = []) {
192
193
  return {
193
194
  code: 'world_membership_ready',
194
195
  matchedFieldIds: [],
195
- summary: 'Delivered for manual review because the candidate is active in this world, even though no direct profile overlap signal was detected yet.',
196
+ summary: 'Delivered for manual review because the candidate is online and active in this world, even though no direct profile overlap signal was detected yet.',
196
197
  };
197
198
  }
198
199
 
@@ -264,7 +265,7 @@ export function projectCandidateFeedModel(world) {
264
265
  },
265
266
  liveDeliveryEvent: projectLiveDeliveryEvent(world, candidateFields),
266
267
  summary:
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.',
268
+ 'Active online members can review candidate opportunities first, then call request_chat with the selected targetAgentId when they want to start a world-scoped conversation request.',
268
269
  status: 'phase1_candidate_feed',
269
270
  };
270
271
  }
@@ -274,6 +275,7 @@ function projectCandidateOpportunity({
274
275
  viewerProfile,
275
276
  candidateMembership,
276
277
  candidateAgent,
278
+ candidatePresence,
277
279
  expiresAt,
278
280
  }) {
279
281
  const profileSnapshot = candidateMembership.profileSnapshot || {};
@@ -290,6 +292,7 @@ function projectCandidateOpportunity({
290
292
  worldId: world.worldId,
291
293
  sourceMembershipId: candidateMembership.membershipId,
292
294
  targetAgentId: requestChat?.targetAgentId || null,
295
+ online: candidatePresence?.online === true,
293
296
  requestChat,
294
297
  profileSummary: projectProfileSummary(world, profileSnapshot, candidateAgent),
295
298
  compatibilitySignals,
@@ -304,6 +307,7 @@ export function buildCandidateFeed({
304
307
  viewerAgent = null,
305
308
  candidateMemberships = [],
306
309
  getAgent = () => null,
310
+ getPresence = () => ({ online: true }),
307
311
  nowMs = Date.now(),
308
312
  limit = DEFAULT_CANDIDATE_FEED_LIMIT,
309
313
  }) {
@@ -315,12 +319,18 @@ export function buildCandidateFeed({
315
319
 
316
320
  const candidates = candidateMemberships
317
321
  .filter((membership) => membership?.status === 'active' && membership.membershipId !== viewerMembership.membershipId)
318
- .map((membership) => {
322
+ .map((membership) => ({
323
+ membership,
324
+ candidatePresence: getPresence(membership.agentId),
325
+ }))
326
+ .filter(({ candidatePresence }) => candidatePresence?.online === true)
327
+ .map(({ membership, candidatePresence }) => {
319
328
  const opportunity = projectCandidateOpportunity({
320
329
  world,
321
330
  viewerProfile,
322
331
  candidateMembership: membership,
323
332
  candidateAgent: getAgent(membership.agentId),
333
+ candidatePresence,
324
334
  expiresAt,
325
335
  });
326
336
 
@@ -12,6 +12,24 @@ function normalizeStringList(values = []) {
12
12
  return [...new Set(values.map((value) => normalizeText(value, null)).filter(Boolean))];
13
13
  }
14
14
 
15
+ function normalizeCandidateSource(value) {
16
+ const normalized = normalizeText(value, null);
17
+ if (!normalized) return null;
18
+ if (
19
+ normalized === 'active_memberships'
20
+ || normalized === 'world_members'
21
+ || normalized === 'search_results'
22
+ ) {
23
+ return 'active_memberships_online';
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ function normalizeCandidateSources(values = []) {
29
+ if (!Array.isArray(values)) return [];
30
+ return [...new Set(values.map((value) => normalizeCandidateSource(value)).filter(Boolean))];
31
+ }
32
+
15
33
  function normalizeWorldEligibility(value, fallback = 'active') {
16
34
  const normalized = normalizeText(value, fallback);
17
35
  if (normalized === 'joined') return 'joined';
@@ -188,7 +206,7 @@ export function normalizeWorldManifest(manifest = {}, index = 0) {
188
206
  mode: normalizeText(manifest.matching?.mode, 'manual_review'),
189
207
  cadence: normalizeText(manifest.matching?.cadence, 'on_demand'),
190
208
  strategySummary: normalizeText(manifest.matching?.strategySummary, null),
191
- candidateSources: normalizeStringList(manifest.matching?.candidateSources),
209
+ candidateSources: normalizeCandidateSources(manifest.matching?.candidateSources),
192
210
  },
193
211
  sessionTemplate: {
194
212
  mode: normalizeText(manifest.sessionTemplate?.mode, 'a2a'),
@@ -388,6 +388,7 @@ function normalizeCandidate(candidate = {}, index = 0) {
388
388
  candidateId: normalizeText(candidate.candidateId, `candidate_${index + 1}`),
389
389
  worldId: normalizeText(candidate.worldId, 'unknown-world'),
390
390
  sourceMembershipId: normalizeText(candidate.sourceMembershipId, null),
391
+ online: candidate.online === true,
391
392
  targetAgentId,
392
393
  requestChat,
393
394
  profileSummary: normalizeCandidateProfileSummary(candidate.profileSummary),
@@ -420,7 +421,7 @@ function normalizeCandidateFeedResponse(payload = {}, { worldId = null, agentId
420
421
  payload.nextAction,
421
422
  candidates.length > 0 ? 'review_candidates_then_request_chat' : 'wait_for_more_candidates',
422
423
  ),
423
- candidateSource: normalizeText(payload.candidateSource, 'active_memberships'),
424
+ candidateSource: normalizeText(payload.candidateSource, 'active_memberships_online'),
424
425
  candidateModel: payload.candidateModel && typeof payload.candidateModel === 'object' ? payload.candidateModel : {},
425
426
  strategy: payload.strategy && typeof payload.strategy === 'object' ? payload.strategy : {},
426
427
  limit: normalizeInteger(payload.limit, candidates.length),
@@ -949,6 +950,7 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
949
950
  .map((signal) => sentenceCase(signal.summary, ''))
950
951
  .filter(Boolean);
951
952
  const deliveryReasonSummary = sentenceCase(candidate.deliveryReason.summary, '');
953
+ const availabilitySummary = candidate.online === true ? 'Online now.' : 'Currently offline.';
952
954
  const scoreSummary = candidate.score == null
953
955
  ? null
954
956
  : `Score ${candidate.score}${candidate.rank == null ? '' : `, rank ${candidate.rank}`}.`;
@@ -958,12 +960,14 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
958
960
  optionalFieldSummary.length > 0 ? `Optional context: ${optionalFieldSummary.join('; ')}.` : null,
959
961
  compatibilitySummary.length > 0 ? compatibilitySummary.join(' ') : null,
960
962
  deliveryReasonSummary || null,
963
+ availabilitySummary,
961
964
  scoreSummary,
962
965
  ].filter(Boolean).join(' ');
963
966
 
964
967
  return {
965
968
  candidateId: candidate.candidateId,
966
969
  sourceMembershipId: candidate.sourceMembershipId,
970
+ online: candidate.online === true,
967
971
  targetAgentId: candidate.targetAgentId,
968
972
  requestChat: candidate.requestChat,
969
973
  displayName: name,
@@ -982,11 +986,11 @@ export function buildCandidateDeliverySummary(candidateFeed = {}, { worldDetail
982
986
  const totalCandidateCount = Math.max(normalizedFeed.totalCandidates, deliveredCandidateCount);
983
987
  const remainingCandidateCount = Math.max(totalCandidateCount - deliveredCandidateCount, 0);
984
988
  const heading = deliveredCandidateCount > 0
985
- ? `${displayName} has ${deliveredCandidateCount} candidate profile ${deliveredCandidateCount === 1 ? 'summary' : 'summaries'} ready for review now.`
986
- : `No candidate profile summaries are ready for review in ${displayName} yet.`;
989
+ ? `${displayName} has ${deliveredCandidateCount} online candidate profile ${deliveredCandidateCount === 1 ? 'summary' : 'summaries'} ready for review now.`
990
+ : `No online candidate profile summaries are ready for review in ${displayName} yet.`;
987
991
  const promptBody = deliveredCandidateCount > 0
988
992
  ? candidateSummaries.map((summary, index) => buildCandidateDeliverySummaryLine(summary, index)).join('\n\n')
989
- : 'No candidates are currently available from the active-membership feed.';
993
+ : 'No online candidates are currently available from the active-membership feed.';
990
994
 
991
995
  return {
992
996
  worldId: normalizedFeed.worldId,
@@ -33,7 +33,7 @@ export function createClaworldProductShell({
33
33
  worldService,
34
34
  membershipService,
35
35
  });
36
- const matchmakingService = createMatchmakingService({ worldService, worldAuthorizationService, store });
36
+ const matchmakingService = createMatchmakingService({ worldService, worldAuthorizationService, store, presence });
37
37
  const searchService = createWorldSearchService({ worldService, worldAuthorizationService, store, presence });
38
38
  const worldAdminService = createWorldAdminService({ worldService, worldAuthorizationService, store });
39
39
  const chatRequestService = createChatRequestService({
@@ -217,7 +217,14 @@ function buildViewerContext({ world, membershipStore, normalizedAgentId, worldAu
217
217
  };
218
218
  }
219
219
 
220
- function buildBaseFeed({ world, membershipStore, normalizedAgentId, limit, worldAuthorizationService }) {
220
+ function buildBaseFeed({
221
+ world,
222
+ membershipStore,
223
+ normalizedAgentId,
224
+ limit,
225
+ worldAuthorizationService,
226
+ resolvePresence,
227
+ }) {
221
228
  const { viewerAgent, viewerMembership } = buildViewerContext({
222
229
  world,
223
230
  membershipStore,
@@ -236,6 +243,7 @@ function buildBaseFeed({ world, membershipStore, normalizedAgentId, limit, world
236
243
  viewerAgent,
237
244
  candidateMemberships: activeMemberships,
238
245
  getAgent: (candidateAgentId) => membershipStore.getAgent(candidateAgentId),
246
+ getPresence: (candidateAgentId) => resolvePresence(candidateAgentId),
239
247
  nowMs,
240
248
  limit: activeMemberships.length,
241
249
  });
@@ -248,13 +256,21 @@ function buildBaseFeed({ world, membershipStore, normalizedAgentId, limit, world
248
256
  };
249
257
  }
250
258
 
251
- function buildDatingDemoFeed({ world, membershipStore, normalizedAgentId, limit, worldAuthorizationService }) {
259
+ function buildDatingDemoFeed({
260
+ world,
261
+ membershipStore,
262
+ normalizedAgentId,
263
+ limit,
264
+ worldAuthorizationService,
265
+ resolvePresence,
266
+ }) {
252
267
  const { viewerMembership, activeMemberships, normalizedLimit, baseFeed } = buildBaseFeed({
253
268
  world,
254
269
  membershipStore,
255
270
  normalizedAgentId,
256
271
  limit,
257
272
  worldAuthorizationService,
273
+ resolvePresence,
258
274
  });
259
275
  const membershipById = new Map(activeMemberships.map((membership) => [membership.membershipId, membership]));
260
276
  const rankedCandidates = baseFeed.candidates
@@ -278,19 +294,35 @@ function buildDatingDemoFeed({ world, membershipStore, normalizedAgentId, limit,
278
294
  ...baseFeed,
279
295
  agentId: normalizedAgentId,
280
296
  limit: normalizedLimit,
281
- candidateSource: 'active_memberships',
297
+ candidateSource: 'active_memberships_online',
282
298
  strategy: buildStrategy(world),
283
299
  totalCandidates: rankedCandidates.length,
284
300
  candidates,
285
301
  };
286
302
  }
287
303
 
288
- export function createMatchmakingService({ worldService, worldAuthorizationService, store = null } = {}) {
304
+ export function createMatchmakingService({
305
+ worldService,
306
+ worldAuthorizationService,
307
+ store = null,
308
+ presence = null,
309
+ } = {}) {
289
310
  function assertStore() {
290
311
  if (!store) throw createConfigurationError();
291
312
  return store;
292
313
  }
293
314
 
315
+ function resolvePresence(agentId) {
316
+ if (!presence) {
317
+ return {
318
+ online: true,
319
+ connectedAt: null,
320
+ lastHeartbeatAt: null,
321
+ };
322
+ }
323
+ return presence.getPresence(agentId);
324
+ }
325
+
294
326
  return {
295
327
  describeStrategy(worldId) {
296
328
  const world = worldService.requireWorld(worldId);
@@ -312,6 +344,7 @@ export function createMatchmakingService({ worldService, worldAuthorizationServi
312
344
  normalizedAgentId,
313
345
  limit,
314
346
  worldAuthorizationService,
347
+ resolvePresence,
315
348
  });
316
349
  }
317
350
 
@@ -321,13 +354,14 @@ export function createMatchmakingService({ worldService, worldAuthorizationServi
321
354
  normalizedAgentId,
322
355
  limit,
323
356
  worldAuthorizationService,
357
+ resolvePresence,
324
358
  });
325
359
 
326
360
  return {
327
361
  ...baseFeed,
328
362
  agentId: normalizedAgentId,
329
363
  limit: normalizedLimit,
330
- candidateSource: 'active_memberships',
364
+ candidateSource: 'active_memberships_online',
331
365
  strategy: buildStrategy(world),
332
366
  totalCandidates: baseFeed.candidates.length,
333
367
  candidates: baseFeed.candidates.slice(0, normalizedLimit),
@@ -267,13 +267,20 @@ function projectKickoff(kickoff = {}) {
267
267
  return {
268
268
  status: normalizeText(kickoff.status, 'skipped'),
269
269
  deliveredAt: normalizeText(kickoff.deliveredAt, null),
270
+ relaySessionPreparedAt: normalizeText(kickoff.relaySessionPreparedAt, null),
271
+ acceptedRoundPreparedAt: normalizeText(kickoff.acceptedRoundPreparedAt, null),
272
+ senderKickoffDeliveredAt: normalizeText(kickoff.senderKickoffDeliveredAt, normalizeText(kickoff.deliveredAt, null)),
273
+ openerAcceptedAt: normalizeText(kickoff.openerAcceptedAt, null),
274
+ openerDeliveredAt: normalizeText(kickoff.openerDeliveredAt, null),
275
+ liveChatEstablishedAt: normalizeText(kickoff.liveChatEstablishedAt, null),
270
276
  reason: normalizeText(kickoff.reason, null),
271
277
  };
272
278
  }
273
279
 
274
280
  function resolveAcceptNextAction(kickoff) {
275
281
  const kickoffStatus = kickoff?.status || 'skipped';
276
- if (kickoffStatus === 'sent') return 'runtime_owns_live_conversation';
282
+ if (kickoffStatus === 'established') return 'runtime_owns_live_conversation';
283
+ if (kickoffStatus === 'sent') return 'wait_for_sender_opener_delivery';
277
284
  if (kickoffStatus === 'failed') return 'backend_kickoff_failed';
278
285
  return 'chat_request_accepted_without_opening_message';
279
286
  }
@@ -239,8 +239,8 @@ function buildMatchingStrategy(entryProfileSchema) {
239
239
  mode: 'profile_overlap',
240
240
  cadence: 'on_demand',
241
241
  strategySummary:
242
- `Rank world members by overlap on ${entryProfileSchema.searchableFieldIds.join(', ')}, deliver candidate summaries first, and let members request_chat after review.`,
243
- candidateSources: ['active_memberships'],
242
+ `Rank active online world members by overlap on ${entryProfileSchema.searchableFieldIds.join(', ')}, deliver candidate summaries first, and let members request_chat after review.`,
243
+ candidateSources: ['active_memberships_online'],
244
244
  };
245
245
  }
246
246