@xfxstudio/claworld 2026.6.9-testing.1 → 2026.6.10-testing.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.
@@ -336,6 +336,25 @@ function inferExistingAgentId(config = {}, accountId = DEFAULT_CLAWORLD_ACCOUNT_
336
336
  return DEFAULT_CLAWORLD_AGENT_ID;
337
337
  }
338
338
 
339
+ const MANAGED_LEGACY_BUNDLED_SKILL_NAMES = Object.freeze([
340
+ 'claworld-main-session',
341
+ 'claworld-manage-worlds',
342
+ 'claworld-help',
343
+ ]);
344
+
345
+ function hasOnlyManagedBundledSkills(value) {
346
+ if (!Array.isArray(value) || value.length === 0) return false;
347
+ return value.every((skillName) => MANAGED_LEGACY_BUNDLED_SKILL_NAMES.includes(skillName));
348
+ }
349
+
350
+ function buildManagedAgentEntry(options = {}) {
351
+ return {
352
+ id: options.agentId,
353
+ workspace: options.workspace,
354
+ ...(options.agentDirExplicit && options.agentDir ? { agentDir: options.agentDir } : {}),
355
+ };
356
+ }
357
+
339
358
  function buildManagedRoutingEntry(options = {}, existingRouting = {}) {
340
359
  return {
341
360
  ...ensureObject(existingRouting),
@@ -488,6 +507,60 @@ export function resolveToolNames({ toolProfile = DEFAULT_CLAWORLD_TOOL_PROFILE }
488
507
  return [...baseProfile];
489
508
  }
490
509
 
510
+ const MANAGED_BUNDLED_SKILL_NAMES = Object.freeze([
511
+ 'claworld-main-session',
512
+ 'claworld-manage-worlds',
513
+ 'claworld-help',
514
+ ]);
515
+
516
+ export function resolveManagedAgentSkills({ toolProfile = DEFAULT_CLAWORLD_TOOL_PROFILE } = {}) {
517
+ const normalizedProfile = normalizeClaworldToolProfile(toolProfile);
518
+ return normalizedProfile === 'full' ? undefined : [...MANAGED_BUNDLED_SKILL_NAMES];
519
+ }
520
+
521
+ function describeToolAllowEntries(toolNames = []) {
522
+ if (toolNames.length === 1) {
523
+ return toolNames[0] === '*' ? '1 entry (includes *)' : '1 entry';
524
+ }
525
+ return toolNames.includes('*')
526
+ ? `${toolNames.length} entries (includes *)`
527
+ : `${toolNames.length} entries`;
528
+ }
529
+
530
+ export function buildWorkspaceAgentsContent({
531
+ agentId,
532
+ accountId,
533
+ registrationDisplayName,
534
+ appToken = null,
535
+ defaultTargetAgentId = null,
536
+ } = {}) {
537
+ const identityLine = appToken
538
+ ? '- relay binding is resolved from the configured appToken at runtime'
539
+ : registrationDisplayName
540
+ ? '- relay binding is created during runtime bootstrap and persists as backend-issued credentials'
541
+ : '- activation is pending until the user completes Claworld public identity setup';
542
+
543
+ return `# Claworld Channel Agent
544
+
545
+ This workspace is dedicated to the OpenClaw \`claworld\` channel.
546
+
547
+ Routing contract:
548
+
549
+ - local OpenClaw agent id: \`${agentId}\`
550
+ - claworld account id: \`${accountId}\`
551
+ ${registrationDisplayName ? `- bootstrap display name: \`${registrationDisplayName}\`` : (appToken ? '- credential mode: appToken/manual binding' : '- credential mode: activation pending')}
552
+ ${identityLine}
553
+ ${defaultTargetAgentId ? `- default outbound target agentId: \`${defaultTargetAgentId}\`` : '- outbound sends require explicit target agentId inputs'}
554
+
555
+ Operating rules:
556
+
557
+ - keep this workspace focused on Claworld relay and world interactions
558
+ - use explicit \`claworld_*\` tools when they are available
559
+ - do not treat this workspace as the user's general-purpose main agent
560
+ - do not treat this as a separately bootstrapped OpenClaw persona
561
+ `;
562
+ }
563
+
491
564
  function buildBoundAgentEntry(existingAgent = {}, agentId) {
492
565
  const nextAgent = {
493
566
  ...ensureObject(existingAgent),
@@ -497,8 +570,41 @@ function buildBoundAgentEntry(existingAgent = {}, agentId) {
497
570
  return nextAgent;
498
571
  }
499
572
 
573
+ export function buildWorkspaceMemoryContent({
574
+ agentId,
575
+ accountId,
576
+ registrationDisplayName,
577
+ appToken = null,
578
+ defaultTargetAgentId = null,
579
+ } = {}) {
580
+ const identityLine = appToken
581
+ ? '- relay binding: resolved from appToken at runtime'
582
+ : registrationDisplayName
583
+ ? '- relay binding: assigned during runtime bootstrap'
584
+ : '- relay binding: pending until public identity setup completes';
585
+
586
+ return `# Claworld Memory
587
+
588
+ - workspace owner: \`${agentId}\`
589
+ - claworld account: \`${accountId}\`
590
+ ${registrationDisplayName ? `- bootstrap display name: \`${registrationDisplayName}\`` : ''}
591
+ ${identityLine}
592
+ ${defaultTargetAgentId ? `- default outbound target agentId: \`${defaultTargetAgentId}\`` : ''}
593
+
594
+ Use this file for durable Claworld-specific notes only.
595
+
596
+ - keep world rules, pairing context, and recurring counterpart preferences here when they help future Claworld conversations
597
+ - do not duplicate a full standalone OpenClaw bootstrap/persona here
598
+ - prefer channel/world-specific memory over general-purpose assistant memory
599
+ `;
600
+ }
601
+
602
+ export function resolveDefaultManagedWorkspace(agentId = DEFAULT_CLAWORLD_AGENT_ID) {
603
+ return `~/.openclaw/workspace-${agentId}`;
604
+ }
605
+
500
606
  export function resolveDefaultManagedDisplayName(accountId = DEFAULT_CLAWORLD_ACCOUNT_ID) {
501
- return `${titleCase(accountId)} Channel`;
607
+ return `${titleCase(accountId)} Channel Agent`;
502
608
  }
503
609
 
504
610
  export function resolveClaworldManagedRuntimeOptions({
@@ -509,6 +615,7 @@ export function resolveClaworldManagedRuntimeOptions({
509
615
  installerState = null,
510
616
  } = {}) {
511
617
  const resolvedAccountId = normalizeText(accountId, DEFAULT_CLAWORLD_ACCOUNT_ID);
618
+ const attachToExistingAgent = overrides.attachToExistingAgent !== false;
512
619
  const existingBackup = findClaworldManagedRuntimeBackup(installerState, resolvedAccountId);
513
620
  const inferredAgentId = inferExistingAgentId(cfg, resolvedAccountId)
514
621
  || normalizeText(existingBackup.agentId, null);
@@ -516,7 +623,20 @@ export function resolveClaworldManagedRuntimeOptions({
516
623
  const existingAgent = findAgentEntry(cfg, agentId);
517
624
  const existingAccount = findManagedAccountEntry(cfg, resolvedAccountId);
518
625
  const replaceManagedRuntime = overrides.replaceManagedRuntime !== false;
519
- const workspace = normalizeText(existingAgent?.workspace, normalizeText(existingBackup.workspace, null));
626
+ const explicitWorkspace = normalizeText(overrides.workspace, null);
627
+ const manageAgentEntry = overrides.manageAgentEntry === true
628
+ || overrides.agentDirExplicit === true
629
+ || attachToExistingAgent !== true;
630
+ const manageWorkspace = overrides.manageWorkspace === true
631
+ || Boolean(explicitWorkspace)
632
+ || attachToExistingAgent !== true;
633
+ const defaultWorkspace = manageWorkspace ? resolveDefaultManagedWorkspace(agentId) : null;
634
+ const workspace = normalizeText(
635
+ explicitWorkspace,
636
+ replaceManagedRuntime
637
+ ? normalizeText(existingAgent?.workspace, normalizeText(existingBackup.workspace, defaultWorkspace))
638
+ : normalizeText(existingAgent?.workspace, normalizeText(existingBackup.workspace, defaultWorkspace)),
639
+ );
520
640
  const serverUrl = normalizeText(
521
641
  overrides.serverUrl,
522
642
  normalizeText(input.httpUrl, normalizeText(input.url, normalizeText(existingBackup.serverUrl, DEFAULT_CLAWORLD_SERVER_URL))),
@@ -560,9 +680,14 @@ export function resolveClaworldManagedRuntimeOptions({
560
680
 
561
681
  return {
562
682
  repoRoot: normalizeText(overrides.repoRoot, null),
683
+ attachToExistingAgent,
563
684
  agentId,
564
685
  accountId: resolvedAccountId,
565
686
  workspace,
687
+ manageAgentEntry,
688
+ manageWorkspace,
689
+ agentDir: normalizeText(overrides.agentDir, null),
690
+ agentDirExplicit: overrides.agentDirExplicit === true,
566
691
  serverUrl,
567
692
  apiKey,
568
693
  appToken,
@@ -590,6 +715,7 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
590
715
  const replaceManagedRuntime = options.replaceManagedRuntime !== false;
591
716
  const preserveDefaultAccount = options.preserveDefaultAccount === true;
592
717
  const sessionDmScope = normalizeText(options.sessionDmScope, DEFAULT_CLAWORLD_DM_SCOPE);
718
+ const manageAgentEntry = options.manageAgentEntry === true;
593
719
 
594
720
  const removedManagedToolNames = new Set(CLAWORLD_PUBLIC_TOOL_NAMES);
595
721
  if (inputConfig?.tools && typeof inputConfig.tools === 'object') {
@@ -628,18 +754,72 @@ export function applyClaworldManagedRuntimeConfig(inputConfig = {}, options = {}
628
754
 
629
755
  config.agents = ensureObject(config.agents);
630
756
  const existingAgentList = Array.isArray(config.agents.list) ? [...config.agents.list] : [];
631
- const agentIndex = findAgentIndex(existingAgentList, options.agentId);
632
- if (agentIndex >= 0) {
633
- const existingAgent = ensureObject(existingAgentList[agentIndex]);
634
- existingAgentList[agentIndex] = buildBoundAgentEntry(existingAgent, options.agentId);
635
- config.agents.list = existingAgentList;
636
- summary.push(`updated bound agent tool exposure ${options.agentId}`);
757
+ if (manageAgentEntry) {
758
+ const managedAgentEntry = buildManagedAgentEntry(options);
759
+ if (replaceManagedRuntime) {
760
+ const removedAgentEntries = existingAgentList.filter((item) => ensureObject(item).id === options.agentId).length;
761
+ config.agents.list = [
762
+ ...existingAgentList.filter((item) => ensureObject(item).id !== options.agentId),
763
+ {
764
+ ...managedAgentEntry,
765
+ tools: mergeManagedPluginToolExposure(managedAgentEntry.tools),
766
+ },
767
+ ];
768
+ summary.push(
769
+ removedAgentEntries > 0
770
+ ? `replaced managed agent entry ${options.agentId}`
771
+ : `added workspace-scoped agent entry ${options.agentId}`,
772
+ );
773
+ } else {
774
+ const agentIndex = findAgentIndex(existingAgentList, options.agentId);
775
+ if (agentIndex >= 0) {
776
+ const existingAgent = ensureObject(existingAgentList[agentIndex]);
777
+ const existingAgentDir = normalizeText(existingAgent.agentDir, null);
778
+ const keepExistingAgentDir = Boolean(
779
+ existingAgentDir
780
+ && (
781
+ options.agentDirExplicit
782
+ || existingAgentDir !== normalizeText(options.agentDir, null)
783
+ ),
784
+ );
785
+ const nextAgentEntry = {
786
+ ...existingAgent,
787
+ id: options.agentId,
788
+ workspace: normalizeText(existingAgent.workspace, options.workspace),
789
+ ...(keepExistingAgentDir ? { agentDir: existingAgentDir } : {}),
790
+ };
791
+ if (!keepExistingAgentDir) {
792
+ delete nextAgentEntry.agentDir;
793
+ }
794
+ if (hasOnlyManagedBundledSkills(existingAgent.skills)) {
795
+ delete nextAgentEntry.skills;
796
+ }
797
+ nextAgentEntry.tools = mergeManagedPluginToolExposure(existingAgent.tools);
798
+ existingAgentList[agentIndex] = nextAgentEntry;
799
+ summary.push(`updated existing agent entry ${options.agentId}`);
800
+ } else {
801
+ existingAgentList.push({
802
+ ...managedAgentEntry,
803
+ tools: mergeManagedPluginToolExposure(managedAgentEntry.tools),
804
+ });
805
+ summary.push(`added workspace-scoped agent entry ${options.agentId}`);
806
+ }
807
+ config.agents.list = existingAgentList;
808
+ }
637
809
  } else {
638
- existingAgentList.push(buildBoundAgentEntry({}, options.agentId));
639
- config.agents.list = existingAgentList;
640
- summary.push(`added bound agent entry ${options.agentId}`);
810
+ const agentIndex = findAgentIndex(existingAgentList, options.agentId);
811
+ if (agentIndex >= 0) {
812
+ const existingAgent = ensureObject(existingAgentList[agentIndex]);
813
+ existingAgentList[agentIndex] = buildBoundAgentEntry(existingAgent, options.agentId);
814
+ config.agents.list = existingAgentList;
815
+ summary.push(`updated bound agent tool exposure ${options.agentId}`);
816
+ } else {
817
+ existingAgentList.push(buildBoundAgentEntry({}, options.agentId));
818
+ config.agents.list = existingAgentList;
819
+ summary.push(`added bound agent entry ${options.agentId}`);
820
+ }
821
+ summary.push(`attached claworld account ${options.accountId} to existing local agent ${options.agentId}`);
641
822
  }
642
- summary.push(`attached claworld account ${options.accountId} to local agent ${options.agentId}`);
643
823
 
644
824
  config.channels = ensureObject(config.channels);
645
825
  const existingClaworldRoot = ensureObject(config.channels.claworld);
@@ -133,6 +133,7 @@ export function inspectManagedClaworldInstall({
133
133
  const setupReady = Boolean(
134
134
  managedAccountPresent
135
135
  && managedBindingPresent
136
+ && (managedOptions.manageAgentEntry !== true || managedAgentPresent)
136
137
  );
137
138
 
138
139
  let statusLabel = 'needs setup';
@@ -247,9 +248,7 @@ async function applyManagedOnboardingConfig({
247
248
  managedOptions.appToken
248
249
  ? 'Activation state: ready via configured appToken'
249
250
  : 'Activation state: pending until claworld_manage_account(action=activate_account) runs',
250
- 'Workspace memory: runtime prompt bootstrap maintains .claworld/ in the active host workspace',
251
- 'This flow refreshes plugin-side config and binds claworld onto the selected local agent.',
252
- 'Setup lifecycle: OpenClaw host-native setup.',
251
+ 'This flow refreshes plugin-side config and binds claworld onto the selected local agent. It does not run installer commands or start a backend service.',
253
252
  ];
254
253
  await prompter.note(
255
254
  noteLines.join('\n'),
@@ -20,6 +20,7 @@ import {
20
20
  } from '../runtime/backend-error-context.js';
21
21
 
22
22
  export const INTERNAL_REQUESTER_SESSION_KEY_PARAM = '__claworldRequesterSessionKey';
23
+ export const INTERNAL_WORKSPACE_ROOT_PARAM = '__claworldWorkspaceRoot';
23
24
 
24
25
  export function normalizeText(value, fallback = null) {
25
26
  if (value == null) return fallback;
@@ -240,9 +241,10 @@ export async function resolveToolContext(
240
241
  runtime: api?.runtime || null,
241
242
  accountId,
242
243
  runtimeConfig,
243
- agentId: normalizeText(params.agentId, runtimeConfig.relay?.agentId || null),
244
- requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
245
- });
244
+ agentId: normalizeText(params.agentId, runtimeConfig.relay?.agentId || null),
245
+ requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
246
+ workspaceRoot: normalizeText(params[INTERNAL_WORKSPACE_ROOT_PARAM], null),
247
+ });
246
248
  if (
247
249
  requiredPublicIdentityCapability
248
250
  && (
@@ -282,6 +284,7 @@ export async function resolveToolContext(
282
284
  runtimeConfig,
283
285
  agentId,
284
286
  requesterSessionKey: normalizeText(params[INTERNAL_REQUESTER_SESSION_KEY_PARAM], null),
287
+ workspaceRoot: normalizeText(params[INTERNAL_WORKSPACE_ROOT_PARAM], null),
285
288
  };
286
289
  }
287
290
 
@@ -2,6 +2,10 @@ import {
2
2
  createClaworldChannelPlugin,
3
3
  recordClaworldRuntimeAssistantOutput,
4
4
  } from './claworld-channel-plugin.js';
5
+ import {
6
+ attachConversationViewerToPayload,
7
+ buildConversationViewerRoute,
8
+ } from './conversation-viewer.js';
5
9
  import {
6
10
  projectToolChatRequestMutationResponse,
7
11
  projectToolCreateWorldResponse,
@@ -41,6 +45,7 @@ import {
41
45
  inferChatInboxAction,
42
46
  inferManageWorldAction,
43
47
  INTERNAL_REQUESTER_SESSION_KEY_PARAM,
48
+ INTERNAL_WORKSPACE_ROOT_PARAM,
44
49
  integerParam,
45
50
  loadCurrentConfig,
46
51
  MANAGE_WORLD_ACTIONS,
@@ -267,6 +272,24 @@ function buildTerminalActionResult({ tool, action, payload = {}, status = null }
267
272
  });
268
273
  }
269
274
 
275
+ async function attachLocalConversationViewer(payload, {
276
+ context = {},
277
+ params = {},
278
+ role = null,
279
+ source = null,
280
+ logger = console,
281
+ } = {}) {
282
+ return await attachConversationViewerToPayload(payload, {
283
+ accountId: context.accountId || null,
284
+ viewerAgentId: context.agentId || null,
285
+ role,
286
+ source,
287
+ cfg: context.cfg || {},
288
+ workspaceRoot: context.workspaceRoot || params[INTERNAL_WORKSPACE_ROOT_PARAM] || null,
289
+ logger,
290
+ });
291
+ }
292
+
270
293
  function normalizeChatInboxListFiltersInput(params = {}) {
271
294
  const source = normalizeObject(params.filters, {}) || {};
272
295
  const normalized = {
@@ -1093,10 +1116,17 @@ function createTerminalToolAdapters(api, plugin, internalTools) {
1093
1116
  conversationKey,
1094
1117
  localSessionKey,
1095
1118
  });
1119
+ const payloadWithViewer = await attachLocalConversationViewer(payload, {
1120
+ context,
1121
+ params,
1122
+ role: 'participant',
1123
+ source: 'close_conversation',
1124
+ logger: getHookLogger(api),
1125
+ });
1096
1126
  return buildTerminalActionResult({
1097
1127
  tool: manageConversationsTool,
1098
1128
  action,
1099
- payload,
1129
+ payload: payloadWithViewer,
1100
1130
  });
1101
1131
  }
1102
1132
  requireManageWorldField('action', `action must be one of ${TERMINAL_CONVERSATION_ACTIONS.join(', ')}`);
@@ -1691,6 +1721,7 @@ function buildRegisteredTools(api, plugin) {
1691
1721
  'Do not use this tool for replying inside an already-open Claworld chat or for runtime live turns.',
1692
1722
  'After creation, use claworld_chat_inbox to inspect pending, expired, rejected, opening, ending, active, silent, or ended status, or wait for the peer to accept.',
1693
1723
  'Once accepted, the runtime owns the live conversation loop.',
1724
+ 'If the result includes conversationViewer.url, include that local viewer link in the owner-facing response so the human can watch the chat live.',
1694
1725
  ],
1695
1726
  examples: [
1696
1727
  {
@@ -1759,7 +1790,14 @@ function buildRegisteredTools(api, plugin) {
1759
1790
  openingMessage: params.openingMessage || null,
1760
1791
  worldId: params.worldId || null,
1761
1792
  });
1762
- return buildToolResult(projectToolChatRequestMutationResponse(payload, { accountId: context.accountId }));
1793
+ const payloadWithViewer = await attachLocalConversationViewer(payload, {
1794
+ context,
1795
+ params,
1796
+ role: 'requester',
1797
+ source: 'request_chat',
1798
+ logger: getHookLogger(api),
1799
+ });
1800
+ return buildToolResult(projectToolChatRequestMutationResponse(payloadWithViewer, { accountId: context.accountId }));
1763
1801
  },
1764
1802
  },
1765
1803
  {
@@ -1778,6 +1816,7 @@ function buildRegisteredTools(api, plugin) {
1778
1816
  'For user requests to contact, PK, continue, or re-engage a Claworld peer, use claworld_manage_conversations(action=request) with the intended direct or world scope.',
1779
1817
  'Peer-facing opener/reply/final content is delivered by the Conversation Session and backend conversation runtime. Main Session must not use sessions_send to write peer-facing content into a local conversation session.',
1780
1818
  'Prefer Claworld conversation state, reports, and concise summaries before inspecting raw local transcript details.',
1819
+ 'If an accept result includes conversationViewer.url, include that local viewer link in the owner-facing response or report.',
1781
1820
  'Global counts stay visible even when filters are applied; filtered counts describe the current narrowed result set.',
1782
1821
  'After action=accept or action=reject, call action=list again to refresh the inbox view.',
1783
1822
  ],
@@ -1868,7 +1907,16 @@ function buildRegisteredTools(api, plugin) {
1868
1907
  ...context,
1869
1908
  chatRequestId,
1870
1909
  });
1871
- return buildToolResult(projectToolChatInboxActionResponse(payload, {
1910
+ const payloadWithViewer = action === 'accept'
1911
+ ? await attachLocalConversationViewer(payload, {
1912
+ context,
1913
+ params,
1914
+ role: 'recipient',
1915
+ source: 'accept_chat_request',
1916
+ logger: getHookLogger(api),
1917
+ })
1918
+ : payload;
1919
+ return buildToolResult(projectToolChatInboxActionResponse(payloadWithViewer, {
1872
1920
  accountId: context.accountId,
1873
1921
  action,
1874
1922
  }));
@@ -2148,7 +2196,7 @@ function buildRegisteredTools(api, plugin) {
2148
2196
  }));
2149
2197
  }
2150
2198
 
2151
- export function registerClaworldPluginFull(api, plugin) {
2199
+ export function registerClaworldPluginFull(api, plugin, { fetchImpl = globalThis.fetch } = {}) {
2152
2200
  if (!plugin) {
2153
2201
  throw new Error('registerClaworldPluginFull requires a plugin instance');
2154
2202
  }
@@ -2217,17 +2265,16 @@ export function registerClaworldPluginFull(api, plugin) {
2217
2265
  ? event.params
2218
2266
  : {};
2219
2267
  const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
2220
- if (
2221
- toolName !== 'claworld_manage_conversations'
2222
- || normalizeTerminalConversationAction(params.action, null) !== 'request'
2223
- || !requesterSessionKey
2224
- ) {
2225
- return;
2226
- }
2268
+ if (toolName !== 'claworld_manage_conversations') return;
2227
2269
  const logger = getHookLogger(api);
2270
+ let workspaceRoot = null;
2228
2271
  try {
2229
- const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
2230
- if (workspaceRoot) {
2272
+ workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
2273
+ if (
2274
+ workspaceRoot
2275
+ && requesterSessionKey
2276
+ && normalizeTerminalConversationAction(params.action, null) === 'request'
2277
+ ) {
2231
2278
  await updateClaworldSessionDirectory(
2232
2279
  workspaceRoot,
2233
2280
  {
@@ -2248,10 +2295,12 @@ export function registerClaworldPluginFull(api, plugin) {
2248
2295
  } catch (error) {
2249
2296
  logger?.warn?.('[claworld:working-memory] unable to update requester session directory', error);
2250
2297
  }
2298
+ if (!requesterSessionKey && !workspaceRoot) return;
2251
2299
  return {
2252
2300
  params: {
2253
2301
  ...params,
2254
- [INTERNAL_REQUESTER_SESSION_KEY_PARAM]: requesterSessionKey,
2302
+ ...(requesterSessionKey ? { [INTERNAL_REQUESTER_SESSION_KEY_PARAM]: requesterSessionKey } : {}),
2303
+ ...(workspaceRoot ? { [INTERNAL_WORKSPACE_ROOT_PARAM]: workspaceRoot } : {}),
2255
2304
  },
2256
2305
  };
2257
2306
  });
@@ -2280,6 +2329,11 @@ export function registerClaworldPluginFull(api, plugin) {
2280
2329
  }
2281
2330
  if (typeof api.registerHttpRoute === 'function') {
2282
2331
  api.registerHttpRoute(buildClaworldStatusRoute(plugin));
2332
+ api.registerHttpRoute(buildConversationViewerRoute(plugin, {
2333
+ api,
2334
+ fetchImpl,
2335
+ logger: getHookLogger(api),
2336
+ }));
2283
2337
  }
2284
2338
  if (typeof api.registerTool === 'function') {
2285
2339
  const internalTools = new Map(
@@ -2310,7 +2364,7 @@ export function registerClaworldPlugin(api, options = {}) {
2310
2364
  } = options;
2311
2365
  const plugin = existingPlugin || createClaworldChannelPlugin(pluginOptions);
2312
2366
  api.registerChannel({ plugin });
2313
- registerClaworldPluginFull(api, plugin);
2367
+ registerClaworldPluginFull(api, plugin, pluginOptions);
2314
2368
  return plugin;
2315
2369
  }
2316
2370
 
@@ -641,6 +641,35 @@ function projectChatRequestOrigin(origin = {}) {
641
641
  };
642
642
  }
643
643
 
644
+ function projectConversationViewer(viewer = null) {
645
+ if (!viewer || typeof viewer !== 'object' || Array.isArray(viewer)) return null;
646
+ const id = normalizeText(viewer.id, normalizeText(viewer.viewerId, null));
647
+ const url = normalizeText(viewer.url, null);
648
+ if (!id && !url && normalizeText(viewer.status, null) !== 'unavailable') return null;
649
+ return {
650
+ id,
651
+ status: normalizeText(viewer.status, 'ready'),
652
+ url,
653
+ localFile: normalizeText(viewer.localFile, null),
654
+ fileUrl: normalizeText(viewer.fileUrl, null),
655
+ chatRequestId: normalizeText(viewer.chatRequestId, null),
656
+ conversationKey: normalizeText(viewer.conversationKey, null),
657
+ localSessionKey: normalizeText(viewer.localSessionKey, null),
658
+ replay: viewer.replay && typeof viewer.replay === 'object' && !Array.isArray(viewer.replay)
659
+ ? {
660
+ firstOpenReplaysEndedConversation: typeof viewer.replay.firstOpenReplaysEndedConversation === 'boolean'
661
+ ? viewer.replay.firstOpenReplaysEndedConversation
662
+ : null,
663
+ secondOpenShowsFullTranscript: typeof viewer.replay.secondOpenShowsFullTranscript === 'boolean'
664
+ ? viewer.replay.secondOpenShowsFullTranscript
665
+ : null,
666
+ }
667
+ : null,
668
+ note: normalizeText(viewer.note, null),
669
+ reason: normalizeText(viewer.reason, null),
670
+ };
671
+ }
672
+
644
673
  function projectChatRequestItem(request = {}) {
645
674
  if (!request || typeof request !== 'object') return null;
646
675
  const requestContext = request.requestContext && typeof request.requestContext === 'object' && !Array.isArray(request.requestContext)
@@ -656,6 +685,7 @@ function projectChatRequestItem(request = {}) {
656
685
  ? request.conversation
657
686
  : requestContext.conversation,
658
687
  );
688
+ const conversationViewer = projectConversationViewer(request.conversationViewer);
659
689
  return {
660
690
  chatRequestId: normalizeText(request.chatRequestId || request.requestId, null),
661
691
  status: normalizeText(request.status, 'pending'),
@@ -681,11 +711,13 @@ function projectChatRequestItem(request = {}) {
681
711
  toAgent: projectToolAgentSummary(request.toAgent),
682
712
  counterparty: projectToolAgentSummary(request.counterparty),
683
713
  conversation,
714
+ ...(conversationViewer ? { conversationViewer } : {}),
684
715
  };
685
716
  }
686
717
 
687
718
  function projectChatInboxChatItem(chat = {}) {
688
719
  if (!chat || typeof chat !== 'object' || Array.isArray(chat)) return null;
720
+ const conversationViewer = projectConversationViewer(chat.conversationViewer);
689
721
  return {
690
722
  chatRequestId: normalizeText(chat.chatRequestId, null),
691
723
  status: normalizeText(chat.status, null),
@@ -699,6 +731,7 @@ function projectChatInboxChatItem(chat = {}) {
699
731
  counterparty: projectToolAgentSummary(chat.counterparty),
700
732
  conversation: normalizeConversationScopeDetails(chat.conversation),
701
733
  feedbackSummary: projectConversationFeedbackSummary(chat.feedbackSummary),
734
+ ...(conversationViewer ? { conversationViewer } : {}),
702
735
  };
703
736
  }
704
737
 
@@ -764,12 +797,19 @@ export function projectToolChatRequestMutationResponse(result = {}, { accountId
764
797
  const projectedRequest = projectChatRequestItem(request);
765
798
  const kickoff = projectChatRequestKickoff(result.kickoff || result.request?.kickoff);
766
799
  const projectedChat = projectChatInboxChatItem(result.chat);
800
+ const conversationViewer = projectConversationViewer(
801
+ result.conversationViewer
802
+ || request?.conversationViewer
803
+ || result.request?.conversationViewer
804
+ || result.chat?.conversationViewer,
805
+ );
767
806
  const normalizedStatus = normalizeText(result.status, projectedRequest?.status || 'pending');
768
807
  return {
769
808
  status: normalizedStatus,
770
809
  accountId: normalizeText(accountId, null),
771
810
  chatRequest: projectedRequest,
772
811
  ...(projectedChat ? { chat: projectedChat } : {}),
812
+ ...(conversationViewer ? { conversationViewer } : {}),
773
813
  kickoff,
774
814
  nextAction: normalizeText(
775
815
  result.nextAction,