@xfxstudio/claworld 0.1.4 → 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.
@@ -107,12 +107,21 @@ function findManagedAccountEntry(config = {}, accountId) {
107
107
  return {};
108
108
  }
109
109
 
110
+ const MANAGED_LEGACY_BUNDLED_SKILL_NAMES = Object.freeze([
111
+ 'claworld-join-and-chat',
112
+ 'claworld-manage-worlds',
113
+ 'claworld-help',
114
+ ]);
115
+
116
+ function hasOnlyManagedBundledSkills(value) {
117
+ if (!Array.isArray(value) || value.length === 0) return false;
118
+ return value.every((skillName) => MANAGED_LEGACY_BUNDLED_SKILL_NAMES.includes(skillName));
119
+ }
120
+
110
121
  function buildManagedAgentEntry(options = {}) {
111
- const managedSkills = resolveManagedAgentSkills({ toolProfile: options.toolProfile });
112
122
  return {
113
123
  id: options.agentId,
114
124
  workspace: options.workspace,
115
- ...(managedSkills === undefined ? {} : { skills: managedSkills }),
116
125
  ...(options.agentDirExplicit && options.agentDir ? { agentDir: options.agentDir } : {}),
117
126
  };
118
127
  }
@@ -123,7 +132,6 @@ function buildManagedAccountEntry(options = {}) {
123
132
  serverUrl: options.serverUrl,
124
133
  apiKey: options.apiKey,
125
134
  accountId: options.accountId,
126
- toolProfile: options.toolProfile,
127
135
  name: normalizeText(options.name, normalizeText(options.displayName, null)),
128
136
  approval: {
129
137
  mode: normalizeChatRequestApprovalMode(options.approvalMode, DEFAULT_CLAWORLD_APPROVAL_MODE),
@@ -162,7 +170,6 @@ function buildMergedAccountEntry(existingAccount = {}, options = {}) {
162
170
  serverUrl: options.serverUrl,
163
171
  apiKey: options.apiKey,
164
172
  accountId: options.accountId,
165
- toolProfile: options.toolProfile,
166
173
  name: normalizeText(options.name, normalizeText(existingAccount.name, normalizeText(options.displayName, null))),
167
174
  approval: {
168
175
  ...existingApproval,
@@ -179,6 +186,7 @@ function buildMergedAccountEntry(existingAccount = {}, options = {}) {
179
186
  ? { relay: existingRelay }
180
187
  : {}),
181
188
  };
189
+ delete merged.toolProfile;
182
190
 
183
191
  if (options.appToken) {
184
192
  return {
@@ -421,10 +429,6 @@ export function resolveClaworldManagedRuntimeOptions({
421
429
  normalizeText(input.name, resolveDefaultManagedDisplayName(resolvedAccountId)),
422
430
  );
423
431
  const name = normalizeText(overrides.name, displayName);
424
- const explicitToolProfile = normalizeText(
425
- overrides.toolProfile,
426
- normalizeText(input.toolProfile, null),
427
- );
428
432
  const existingRegistrationAgentCode = normalizeRegistrationAgentCode(
429
433
  existingAccount?.registration?.agentCode,
430
434
  normalizeRegistrationAgentCode(existingAccount?.localAgent?.agentCode, null),
@@ -461,11 +465,6 @@ export function resolveClaworldManagedRuntimeOptions({
461
465
  displayName,
462
466
  name,
463
467
  defaultToAddress: normalizeText(overrides.defaultToAddress, null),
464
- toolProfile: resolveManagedToolProfile({
465
- cfg,
466
- existingAccount,
467
- explicitToolProfile,
468
- }),
469
468
  approvalMode,
470
469
  sessionDmScope: normalizeText(
471
470
  overrides.sessionDmScope,
@@ -481,8 +480,6 @@ export function resolveClaworldManagedRuntimeOptions({
481
480
 
482
481
  export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}) {
483
482
  const config = JSON.parse(JSON.stringify(ensureObject(inputConfig)));
484
- const toolProfile = normalizeClaworldToolProfile(options.toolProfile);
485
- const toolNames = resolveToolNames({ toolProfile });
486
483
  const summary = [];
487
484
  const replaceManagedRuntime = options.replaceManagedRuntime !== false;
488
485
  const preserveDefaultAccount = options.preserveDefaultAccount === true;
@@ -492,21 +489,28 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
492
489
  throw new Error('claworld registration agentCode is required when appToken is absent');
493
490
  }
494
491
 
495
- config.tools = ensureObject(config.tools);
496
492
  const removedManagedToolNames = new Set([
493
+ ...CLAWORLD_PUBLIC_TOOL_NAMES,
497
494
  ...CLAWORLD_COMPATIBILITY_TOOL_NAMES,
498
495
  ...CLAWORLD_RETIRED_PUBLIC_TOOL_NAMES,
499
496
  ]);
500
- const existingAllow = asStringArray(config.tools.allow);
501
- const removedHelperTools = existingAllow.filter((toolName) => removedManagedToolNames.has(toolName));
502
- config.tools.allow = uniqueStrings([
503
- ...existingAllow.filter((toolName) => !removedManagedToolNames.has(toolName)),
504
- ...toolNames,
505
- ]);
506
- if (removedHelperTools.length > 0) {
507
- summary.push(`tools.allow removed optional helper entries (${removedHelperTools.join(',')})`);
497
+ if (inputConfig?.tools && typeof inputConfig.tools === 'object') {
498
+ config.tools = ensureObject(config.tools);
499
+ const existingAllow = asStringArray(config.tools.allow);
500
+ const filteredAllow = existingAllow.filter((toolName) => !removedManagedToolNames.has(toolName));
501
+ const removedManagedTools = existingAllow.filter((toolName) => removedManagedToolNames.has(toolName));
502
+ if (removedManagedTools.length > 0) {
503
+ if (filteredAllow.length > 0) {
504
+ config.tools.allow = uniqueStrings(filteredAllow);
505
+ } else {
506
+ delete config.tools.allow;
507
+ }
508
+ summary.push(`tools.allow removed managed claworld entries (${removedManagedTools.join(',')})`);
509
+ }
510
+ if (Object.keys(config.tools).length === 0) {
511
+ delete config.tools;
512
+ }
508
513
  }
509
- summary.push(`tools.allow updated for ${toolProfile} profile (${describeToolAllowEntries(toolNames)})`);
510
514
 
511
515
  config.session = ensureObject(config.session);
512
516
  if (!Object.prototype.hasOwnProperty.call(config.session, 'dmScope')) {
@@ -533,7 +537,6 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
533
537
  if (agentIndex >= 0) {
534
538
  const existingAgent = ensureObject(existingAgentList[agentIndex]);
535
539
  const existingAgentDir = normalizeText(existingAgent.agentDir, null);
536
- const managedSkills = resolveManagedAgentSkills({ toolProfile: options.toolProfile });
537
540
  const keepExistingAgentDir = Boolean(
538
541
  existingAgentDir
539
542
  && (
@@ -546,12 +549,11 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
546
549
  id: options.agentId,
547
550
  workspace: normalizeText(existingAgent.workspace, options.workspace),
548
551
  ...(keepExistingAgentDir ? { agentDir: existingAgentDir } : {}),
549
- ...(managedSkills === undefined ? {} : { skills: managedSkills }),
550
552
  };
551
553
  if (!keepExistingAgentDir) {
552
554
  delete nextAgentEntry.agentDir;
553
555
  }
554
- if (managedSkills === undefined) {
556
+ if (hasOnlyManagedBundledSkills(existingAgent.skills)) {
555
557
  delete nextAgentEntry.skills;
556
558
  }
557
559
  existingAgentList[agentIndex] = nextAgentEntry;
@@ -619,7 +621,6 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
619
621
 
620
622
  return {
621
623
  config,
622
- toolNames,
623
624
  summary,
624
625
  canonicalAgentCode: canonicalRelayAgentCode(
625
626
  options.registrationAgentCode,
@@ -401,13 +401,13 @@ function buildRegisteredTools(api, plugin) {
401
401
  {
402
402
  name: 'claworld_join_world',
403
403
  label: 'Claworld Join World',
404
- description: 'Canonical world-entry tool. Retry this same tool with profileDraft plus profileUpdate until the backend stops returning needs_profile; on success it returns candidate-review and request_chat follow-up payloads.',
404
+ description: 'Canonical world-entry tool. Retry this same tool with profileDraft plus profileUpdate until the backend stops returning needs_profile; on success it returns online candidate-review and request_chat follow-up payloads.',
405
405
  metadata: buildToolMetadata({
406
406
  category: 'world_join',
407
407
  usageNotes: [
408
408
  'This is the only public join entrypoint for the default flow.',
409
409
  'When status is needs_profile, merge the user reply into profileDraft and retry this same tool.',
410
- 'When status is joined, use candidateDelivery/requestChatAction and move to claworld_request_chat.',
410
+ 'When status is joined, read candidateFeed/candidateDelivery online state, then use requestChatAction and move to claworld_request_chat.',
411
411
  ],
412
412
  examples: [
413
413
  {
@@ -435,7 +435,7 @@ function buildRegisteredTools(api, plugin) {
435
435
  interests: ['running', 'climbing'],
436
436
  },
437
437
  },
438
- outcome: 'Returns joined plus candidateDelivery and requestChatAction.',
438
+ outcome: 'Returns joined plus online candidateDelivery and requestChatAction.',
439
439
  },
440
440
  ],
441
441
  }),
@@ -1195,7 +1195,7 @@ export function registerClaworldPluginFull(api, plugin) {
1195
1195
  }
1196
1196
  if (typeof api.registerTool === 'function') {
1197
1197
  for (const tool of buildRegisteredTools(api, plugin)) {
1198
- api.registerTool(tool, { optional: true });
1198
+ api.registerTool(tool);
1199
1199
  }
1200
1200
  }
1201
1201
  return plugin;
@@ -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.4',
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),
@@ -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),
@@ -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