UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

876 lines (875 loc) • 30.9 kB
import { a as asOptionalRecord, c as isRecord } from "../../record-coerce-TIsquoVR.mjs"; import { b as redactToolPayloadText } from "../../redact-BV3wTtTv.mjs"; import { t as areDiagnosticsEnabledForProcess } from "../../diagnostic-events-DT4ejwoa.mjs"; import { t as createSubsystemLogger } from "../../subsystem-4NzR43uz.mjs"; import { s as isSubagentSessionKey } from "../../session-key-utils-CDyBC9Gn.mjs"; import { i as listAgentIds } from "../../agent-roster-DH6r_8f4.mjs"; import { n as validateJsonSchemaValue } from "../../schema-validator-fW6S83H8.mjs"; import "../../string-coerce-runtime-BbIJhy7G.mjs"; import "../../routing-DvCc0aVb.mjs"; import "../../agent-scope-runtime-OJ73CMLc.mjs"; import { S as prepareSessionCatalogSourceActorProjector, _ as readSessionTranscriptCatalogPage, v as readSessionTranscriptCatalogTitle, x as prepareSessionCatalogGitHubLinker } from "../../session-transcript-runtime-B9IYtIbl.mjs"; import { t as definePluginEntry } from "../../plugin-entry-8p_0SEiv.mjs"; import "../../json-schema-runtime-B4A5hjgp.mjs"; import "../../logging-core-ClC4WZ0f.mjs"; import "../../diagnostic-runtime-CL5p0vIf.mjs"; import "../../session-catalog-65hEMEu_.mjs"; import { n as sessionCatalogPaging, t as publishSessionCatalogHost } from "../../session-catalog-paging-Ca-qWw_C.mjs"; //#region extensions/session-share/src/config.ts function sessionShareConfig(config) { const value = config.plugins?.entries?.["session-share"]?.config; return isRecord(value) ? value : {}; } function sessionShareGroups(config) { const share = sessionShareConfig(config).share; if (!isRecord(share) || !Array.isArray(share.groups)) return []; return share.groups.filter((group) => typeof group === "string" && group.length > 0); } function sessionShareNodeBinding(config, nodeId) { const nodes = sessionShareConfig(config).nodes; const binding = isRecord(nodes) ? nodes[nodeId] : void 0; return { ...isRecord(binding) && typeof binding.owner === "string" ? { owner: binding.owner } : {}, linkGitHubIdentities: isRecord(binding) && binding.linkGitHubIdentities === true }; } //#endregion //#region extensions/session-share/src/node-commands.ts const SESSION_SHARE_LIST_COMMAND = "openclaw.sessions.list.v1"; const SESSION_SHARE_READ_COMMAND = "openclaw.sessions.read.v1"; const SESSION_SHARE_COMMANDS = [SESSION_SHARE_LIST_COMMAND, SESSION_SHARE_READ_COMMAND]; const parameterMessages = { listNotObject: "Session list parameters must be an object", unknownListParameter: (key) => `Unknown session list parameter: ${key}`, invalidSearchTerm: "searchTerm must be a non-empty string of at most 500 characters", readNotObject: "Session read parameters must be an object", unknownReadParameter: (key) => `Unknown session read parameter: ${key}`, invalidThreadId: "threadId must be a non-empty session key of at most 512 characters" }; function parseNodeParams(paramsJSON) { return paramsJSON ? JSON.parse(paramsJSON) : void 0; } function isSharedEntry(groups, { sessionKey, entry }) { return entry.category !== void 0 && groups.has(entry.category) && entry.incognito !== true && entry.visibility !== "draft" && !isSubagentSessionKey(sessionKey) && entry.createdVia !== "spawn" && !entry.spawnedBy?.trim() && !/^agent:[^:]+:catalog:/i.test(sessionKey); } function sharedEntries(api, selected) { const config = api.runtime.config.current(); const groups = new Set(sessionShareGroups(config)); if (groups.size === 0) return []; return listAgentIds(config).filter((agentId) => selected.some((entry) => entry.agentId === agentId)).toSorted().flatMap((agentId) => { const storePath = api.runtime.agent.session.resolveStorePath(config.session?.store, { agentId }); const sessionKeys = selected.filter((entry) => entry.agentId === agentId && entry.storePath === storePath).map((entry) => entry.sessionKey); if (sessionKeys.length === 0) return []; return api.runtime.agent.session.listSessionEntries({ agentId, storePath, readOnly: true, includeParticipants: false, sessionKeys }).map((session) => Object.assign({}, session, { agentId, storePath })); }).filter((session) => isSharedEntry(groups, session)); } function createSessionShareNodeCommands(api) { const source = { pluginId: "session-share", sourceDomain: "openclaw" }; let readersConfig; const readers = /* @__PURE__ */ new Map(); async function readSharedEntries() { const config = api.runtime.config.current(); if (config !== readersConfig) { readers.clear(); readersConfig = config; } const groups = new Set(sessionShareGroups(config)); const sessions = []; const active = /* @__PURE__ */ new Set(); for (const agentId of groups.size ? listAgentIds(config).toSorted() : []) { const storePath = api.runtime.agent.session.resolveStorePath(config.session?.store, { agentId }); const key = JSON.stringify([agentId, storePath]); active.add(key); let read = readers.get(key); if (!read) { read = api.runtime.agent.session.createSessionEntryListReader({ agentId, storePath }); readers.set(key, read); } const snapshot = await (await read)(); if (api.runtime.config.current() !== config) throw new Error("Session sharing configuration changed. Refresh the session catalog."); for (const session of snapshot.entries) if (isSharedEntry(groups, session)) sessions.push({ ...session, agentId, storePath, assertSourceCurrent: snapshot.assertCurrent }); } for (const key of readers.keys()) if (!active.has(key)) readers.delete(key); return sessions; } return [{ command: SESSION_SHARE_LIST_COMMAND, hasActiveWork: () => false, cap: "openclaw-sessions", dangerous: false, isAvailable: ({ config }) => sessionShareGroups(config).length > 0, async handle(paramsJSON) { const params = sessionCatalogPaging.parseListParams(parseNodeParams(paramsJSON), { searchMaxLength: 500, messages: parameterMessages }); const offset = sessionCatalogPaging.decodeCursor(params.cursor); const search = params.searchTerm?.toLowerCase(); const sessions = []; for (const { agentId, sessionKey, storePath, entry, assertSourceCurrent } of await readSharedEntries()) { const name = search ? readSessionTranscriptCatalogTitle({ agentId, sessionKey, storePath, entry }) : void 0; if (search && !name?.toLowerCase().includes(search) && !sessionKey.toLowerCase().includes(search)) continue; sessions.push({ agentId, storePath, threadId: sessionKey, name, entry, assertSourceCurrent, recencyAt: Math.max(entry.updatedAt, entry.lastInteractionAt ?? 0, entry.lastActivityAt ?? 0) }); } sessions.sort((left, right) => right.recencyAt - left.recencyAt || left.threadId.localeCompare(right.threadId)); const selected = sessions.slice(offset, offset + params.limit); if (!search) for (const session of selected) session.name = readSessionTranscriptCatalogTitle({ agentId: session.agentId, sessionKey: session.threadId, storePath: session.storePath, entry: session.entry }); const sourceAssertions = new Set(selected.map((session) => session.assertSourceCurrent)); sourceAssertions.forEach((assertCurrent) => assertCurrent()); const projectCreator = await prepareSessionCatalogSourceActorProjector({ ...source, actors: selected.map(({ entry }) => entry.createdActor) }); sourceAssertions.forEach((assertCurrent) => assertCurrent()); const current = sharedEntries(api, selected.map((session) => ({ agentId: session.agentId, storePath: session.storePath, sessionKey: session.threadId }))); if (selected.some((session) => !current.some(({ agentId, sessionKey, storePath, entry }) => agentId === session.agentId && sessionKey === session.threadId && storePath === session.storePath && entry.sessionId === session.entry.sessionId))) throw new Error("Session is no longer shared. Refresh the session catalog."); const page = selected.map(({ threadId, name, entry, recencyAt }) => { const archived = entry.archivedAt !== void 0; const cwd = entry.execCwd ?? entry.spawnedCwd ?? entry.spawnedWorkspaceDir ?? entry.worktree?.canonicalWorkspaceDir ?? entry.worktree?.repoRoot; return { threadId, name, color: entry.color, cwd: cwd ? redactToolPayloadText(cwd).slice(0, 6e3) : void 0, status: archived ? "archived" : "idle", createdAt: entry.createdAt, updatedAt: entry.updatedAt, recencyAt, gitBranch: entry.worktree?.branch ? redactToolPayloadText(entry.worktree.branch).slice(0, 6e3) : void 0, archived, canContinue: false, canArchive: false, canOpenTerminal: false, createdActor: projectCreator(entry.createdActor) }; }); return JSON.stringify({ sessions: page, ...offset + page.length < sessions.length ? { nextCursor: sessionCatalogPaging.encodeCursor(offset + page.length) } : {} }); } }, { command: SESSION_SHARE_READ_COMMAND, hasActiveWork: () => false, cap: "openclaw-sessions", dangerous: false, isAvailable: ({ config }) => sessionShareGroups(config).length > 0, async handle(paramsJSON) { const params = sessionCatalogPaging.parseReadParams(parseNodeParams(paramsJSON), { threadIdMaxLength: 512, threadIdPattern: /^[^\0\r\n]+$/, cursorMaxLength: 1200, messages: parameterMessages }); const session = (await readSharedEntries()).find(({ sessionKey }) => sessionKey === params.threadId); if (!session) throw new Error("Session is not shared. The source operator must select its group and keep it non-draft."); session.assertSourceCurrent(); const page = await readSessionTranscriptCatalogPage({ ...source, agentId: session.agentId, sessionKey: session.sessionKey, storePath: session.storePath, limit: params.limit, cursor: params.cursor }); session.assertSourceCurrent(); if (!sharedEntries(api, [session]).some(({ agentId, sessionKey, storePath, entry }) => agentId === session.agentId && sessionKey === session.sessionKey && storePath === session.storePath && entry.sessionId === session.entry.sessionId)) throw new Error("Session is no longer shared. Refresh the session catalog."); return JSON.stringify({ threadId: session.sessionKey, ...page }); } }]; } //#endregion //#region extensions/session-share/src/wire.ts const shortString = { type: "string", minLength: 1, maxLength: 512 }; const label = { type: "string", minLength: 1, maxLength: 200 }; const text = { type: "string", maxLength: 6e3 }; const cursor = { type: "string", minLength: 1, maxLength: 2048, pattern: "^[A-Za-z0-9_-]+$" }; const identity = { oneOf: [ { type: "object", additionalProperties: false, required: [ "type", "pluginId", "domain", "idKind", "id" ], properties: { type: { const: "remote" }, pluginId: shortString, domain: shortString, idKind: shortString, id: shortString } }, { type: "object", additionalProperties: false, required: [ "type", "pluginId", "accountId", "senderKind", "id" ], properties: { type: { const: "observation" }, pluginId: { anyOf: [shortString, { type: "null" }] }, accountId: { anyOf: [shortString, { type: "null" }] }, senderKind: { enum: [ "human", "bot", "unknown" ] }, id: shortString } }, { type: "object", additionalProperties: false, required: ["type", "id"], properties: { type: { const: "agent" }, id: shortString } }, { type: "object", additionalProperties: false, required: [ "type", "actorType", "source", "id" ], properties: { type: { const: "legacy" }, actorType: shortString, source: { anyOf: [shortString, { type: "null" }] }, id: text } } ] }; const participant = { type: "object", additionalProperties: false, required: ["identity"], properties: { identity, label, avatarUrl: shortString } }; const sessionPageSchema = { type: "object", additionalProperties: false, required: ["sessions"], properties: { nextCursor: cursor, sessions: { type: "array", maxItems: 100, items: { type: "object", additionalProperties: false, required: [ "threadId", "status", "archived", "canContinue", "canArchive" ], properties: { threadId: shortString, name: text, color: shortString, cwd: text, status: { enum: [ "live", "idle", "archived" ] }, createdAt: { type: "number" }, updatedAt: { type: "number" }, recencyAt: { type: "number" }, gitBranch: text, archived: { type: "boolean" }, createdActor: { type: "object", additionalProperties: false, required: ["type"], properties: { type: { enum: [ "human", "agent", "system" ] }, id: shortString, identity, label, avatarUrl: shortString } }, canContinue: { const: false }, canArchive: { const: false }, canOpenTerminal: { const: false } } } } } }; const transcriptPageSchema = { type: "object", additionalProperties: false, required: ["threadId", "items"], properties: { threadId: shortString, nextCursor: cursor, items: { type: "array", maxItems: 100, items: { type: "object", additionalProperties: false, required: ["type"], properties: { type: { enum: [ "userMessage", "agentMessage", "reasoning", "toolCall", "toolResult", "other" ] }, id: shortString, text, timestamp: shortString, model: shortString, sender: participant, truncated: { type: "boolean" } } } } } }; function isSessionPage(value) { return validateJsonSchemaValue({ schema: sessionPageSchema, cacheKey: "session-share.sessions.v1", value }).ok; } function isTranscriptPage(value) { return validateJsonSchemaValue({ schema: transcriptPageSchema, cacheKey: "session-share.transcript.v1", value }).ok; } function unwrapPayload(value) { if (isRecord(value) && typeof value.payloadJSON === "string") { if (value.payloadJSON.length > 2097152) throw new Error("Session catalog response exceeded the page limit"); return JSON.parse(value.payloadJSON); } return value; } function parseSessionSharePage(raw) { const value = unwrapPayload(raw); if (!isSessionPage(value) || value.nextCursor !== void 0 && !sessionCatalogPaging.isExactCursor(value.nextCursor)) throw new Error("Invalid OpenClaw session page from paired node"); return value; } function parseSessionShareTranscriptPage(raw, threadId) { const value = unwrapPayload(raw); if (!isTranscriptPage(value) || value.threadId !== threadId) throw new Error("Invalid OpenClaw transcript page from paired node"); return value; } //#endregion //#region extensions/session-share/src/session-catalog.ts const REFRESH_MS = 3e4; const MAX_STALENESS_MS = 6e4; const REFRESH_BUDGET_MS = 3e5; const MAX_SNAPSHOTS = 32; const MAX_REFRESHES = 4; const MAX_SNAPSHOT_ROWS = 1e4; const MAX_SNAPSHOT_BYTES = 16777216; var SnapshotLimitError = class extends Error { constructor() { super("Shared catalog exceeds the snapshot limit (10,000 sessions or 16 MiB). Reduce the shared groups on the source node."); } }; const log = createSubsystemLogger("gateway/session-catalog"); const nodeErrorCodes = /* @__PURE__ */ new Set([ "TIMEOUT", "NOT_CONNECTED", "PAIRING_CHANGED", "ROUTE_CHANGED", "ABORTED", "UNAVAILABLE", "POLICY_CHANGED", "APPROVAL_AUTHORITY_CLOSED" ]); async function observeCatalogRefresh(operation, warn) { if (!areDiagnosticsEnabledForProcess() || !log.isEnabled("warn")) return operation(); const started = performance.now(); const finish = (outcome, error) => { try { if (!areDiagnosticsEnabledForProcess() || !log.isEnabled("warn")) return; const elapsedMs = performance.now() - started; if (elapsedMs < 1e3 || !warn()) return; const details = asOptionalRecord(asOptionalRecord(error)?.details); const nodeError = asOptionalRecord(details?.nodeError); const nodeErrorCode = nodeError?.code; log.warn("slow Session Share catalog refresh", { elapsedMs: Math.round(elapsedMs), outcome, ...nodeError ? { nodeErrorCode: typeof nodeErrorCode === "string" && nodeErrorCodes.has(nodeErrorCode) ? nodeErrorCode : "unknown" } : {}, ...typeof details?.nodeCommandDispatched === "boolean" ? { nodeCommandDispatched: details.nodeCommandDispatched } : {} }); } catch {} }; try { const value = await operation(); finish("resolved"); return value; } catch (error) { finish("rejected", error); throw error; } } function namespaceIdentity(identity, hostId) { return identity.type === "remote" && identity.pluginId === "session-share" ? { ...identity, domain: hostId } : identity; } function nodeLabel(node) { return node.displayName?.trim() || node.remoteIp?.trim() || node.nodeId; } function isSessionHost(node) { return SESSION_SHARE_COMMANDS.every((command) => node.commands?.includes(command)); } function bindSession(session, hostId, owner, linkParticipant) { const sourceActor = session.createdActor; const portable = sourceActor?.type === "human" && (sourceActor.identity?.type === "remote" || sourceActor.identity?.type === "observation"); const actor = !portable && owner ? owner : sourceActor; if (!actor?.identity) return actor ? { ...session, createdActor: actor } : session; const participant = { identity: namespaceIdentity(actor.identity, hostId), label: actor.label, avatarUrl: actor.avatarUrl }; const linked = portable && linkParticipant ? linkParticipant(participant) : participant; return { ...session, createdActor: { ...actor, ...linked, ...linked.identity.type === "profile" ? { id: linked.identity.id } : {} } }; } function createSessionShareCatalog(api) { const snapshots = /* @__PURE__ */ new Map(); const publicationGuards = /* @__PURE__ */ new WeakMap(); let context; let nextWarningAt = 0; const workers = /* @__PURE__ */ new Set(); const queued = /* @__PURE__ */ new Map(); const bindingFor = (nodeId) => sessionShareNodeBinding(api.runtime.config.current(), nodeId); const current = (entry) => !entry.scope.signal.aborted && api.runtime.config.current() === entry.config && snapshots.get(entry.node.nodeId) === entry; const retire = (entry) => { snapshots.delete(entry.node.nodeId); entry.sessions = void 0; entry.scope.stop(); }; const sameConnection = (entry, node) => node?.connected && isSessionHost(node) && node.connectedAtMs === entry.node.connectedAtMs; function pump(owner) { if (context !== owner || owner.scheduler.signal.aborted) return; for (let worker = 0; worker < MAX_REFRESHES; worker++) { if (workers.has(worker) || !queued.size) continue; workers.add(worker); owner.scheduler.schedule({ id: `refresh:${worker}`, delayMs: 0, async run() { try { for (;;) { const next = queued.entries().next().value; if (!next) return; const [entry, run] = next; queued.delete(entry); if (current(entry)) await run(); else retire(entry); } } finally { workers.delete(worker); pump(owner); } } }); } } function refresh(entry, owner) { const completion = Promise.withResolvers(); entry.pending = completion.promise; const signal = entry.scope.signal; const release = () => { queued.delete(entry); completion.resolve(); }; signal.addEventListener("abort", release, { once: true }); queued.set(entry, async () => { try { const snapshot = await observeCatalogRefresh(async () => { const deadline = performance.now() + REFRESH_BUDGET_MS; const sessions = []; let cursor; let bytes = 0; do { if (!current(entry) || !owner.invokeNode) throw new Error("Session Share catalog owner retired"); const timeoutMs = Math.min(REFRESH_MS, Math.ceil(deadline - performance.now())); if (timeoutMs <= 0) throw new Error("Session Share catalog refresh timed out"); const page = parseSessionSharePage(await owner.invokeNode({ nodeId: entry.node.nodeId, command: SESSION_SHARE_LIST_COMMAND, params: { limit: 100, ...cursor ? { cursor } : {} }, timeoutMs, signal })); bytes += Buffer.byteLength(JSON.stringify(page)); if (sessions.length + page.sessions.length > MAX_SNAPSHOT_ROWS || bytes > MAX_SNAPSHOT_BYTES) throw new SnapshotLimitError(); sessions.push(...page.sessions); if (page.nextCursor !== void 0 && sessionCatalogPaging.decodeCursor(page.nextCursor) <= sessionCatalogPaging.decodeCursor(cursor)) throw new Error("Session Share catalog cursor did not advance"); cursor = page.nextCursor; } while (cursor); return sessions; }, () => { const now = performance.now(); if (now < nextWarningAt) return false; nextWarningAt = now + MAX_STALENESS_MS; return true; }); const connectedNode = (await api.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === entry.node.nodeId); if (!current(entry)) return; if (!sameConnection(entry, connectedNode)) { retire(entry); return; } entry.sessions = snapshot; entry.refreshedAt = performance.now(); entry.retryMs = REFRESH_MS; entry.error = void 0; } catch (error) { entry.retryMs = entry.error ? Math.min(entry.retryMs * 2, 3e5) : REFRESH_MS; entry.error = error instanceof SnapshotLimitError ? { code: "CATALOG_TOO_LARGE", message: error.message } : { code: "NODE_INVOKE_FAILED", message: "Cannot refresh OpenClaw sessions. Check the paired node's session-share configuration and connection." }; } finally { entry.pending = void 0; signal.removeEventListener("abort", release); completion.resolve(); if (current(entry)) entry.scope.schedule({ id: "next-refresh", delayMs: entry.retryMs, run: () => refresh(entry, owner) }); } }); pump(owner); } function reconcile(nodes) { const owner = context; if (!owner || owner.scheduler.signal.aborted) return; const config = api.runtime.config.current(); for (const entry of snapshots.values()) if (entry.config !== config || !sameConnection(entry, nodes.find((node) => node.nodeId === entry.node.nodeId))) retire(entry); } function admit(nodes) { const owner = context; if (!owner || owner.scheduler.signal.aborted) return; const config = api.runtime.config.current(); const selected = new Set(nodes.map((node) => node.nodeId)); for (const node of nodes.filter((candidate) => candidate.connected)) { const existing = snapshots.get(node.nodeId); if (existing) { snapshots.delete(node.nodeId); snapshots.set(node.nodeId, existing); continue; } if (snapshots.size >= MAX_SNAPSHOTS) { const victim = Array.from(snapshots.values()).find((candidate) => !candidate.pending && !selected.has(candidate.node.nodeId)); if (!victim) continue; retire(victim); } const entry = { node, config, scope: owner.scheduler.scope(), refreshedAt: 0, retryMs: REFRESH_MS }; snapshots.set(node.nodeId, entry); refresh(entry, owner); } } api.registerService({ id: "session-share-catalog", apiVersion: 2, start(ctx) { workers.clear(); context = ctx; ctx.scheduler.schedule({ id: "nodes", delayMs: 0, everyMs: REFRESH_MS, async run() { if (!snapshots.size) return; const { nodes } = await api.runtime.nodes.list(); if (context === ctx) reconcile(nodes); } }); }, stop() { context = void 0; snapshots.clear(); queued.clear(); } }); async function listNode(node, query) { const hostId = `node:${node.nodeId}`; const { onHost, waitUntil, signal, allowPartialResults } = query; const search = query.search?.trim().toLowerCase(); const cursor = query.cursors?.[hostId]; const limit = sessionCatalogPaging.boundedLimit(query.limitPerHost); const common = { hostId, label: nodeLabel(node), kind: "node", nodeId: node.nodeId, connected: node.connected === true }; const failed = (code, message) => ({ ...common, sessions: [], error: { code, message } }); if (!node.connected) return failed("NODE_OFFLINE", "Paired node is offline"); const entry = snapshots.get(node.nodeId); const project = async () => { if (!entry) return failed(context ? "CATALOG_LOADING" : "NODE_INVOKE_FAILED", "Session Share is unavailable or busy. Refresh the catalog."); if (!current(entry)) return failed("NODE_INVOKE_FAILED", "Session Share is unavailable. Refresh the catalog."); if (!entry.sessions || performance.now() - entry.refreshedAt >= MAX_STALENESS_MS) return entry.error ? { ...common, sessions: [], error: entry.error } : failed("CATALOG_LOADING", "Session Share catalog is still loading. Refresh the catalog."); const sessions = search ? entry.sessions.filter((session) => session.name?.toLowerCase().includes(search) || session.threadId.toLowerCase().includes(search)) : entry.sessions; const offset = sessionCatalogPaging.decodeCursor(cursor); const binding = bindingFor(node.nodeId); const selected = sessions.slice(offset, offset + limit); const linker = binding.owner || binding.linkGitHubIdentities ? await prepareSessionCatalogGitHubLinker({ owners: binding.owner ? [binding.owner] : [], participants: binding.linkGitHubIdentities ? selected.flatMap(({ createdActor }) => createdActor?.identity ? [{ identity: createdActor.identity }] : []) : [] }) : void 0; if (!current(entry) || signal?.aborted) return failed("NODE_INVOKE_FAILED", "Session Share is unavailable. Refresh the catalog."); const owner = binding.owner ? linker?.resolveOwner(binding.owner) : void 0; const linkParticipant = binding.linkGitHubIdentities ? linker?.linkParticipant : void 0; const host = { ...common, sessions: selected.map((session) => bindSession(session, hostId, owner, linkParticipant)), ...offset + limit < sessions.length ? { nextCursor: sessionCatalogPaging.encodeCursor(offset + limit) } : {} }; publicationGuards.set(host, () => { if (!current(entry) || signal?.aborted) throw new Error("Session Share is unavailable. Refresh the catalog."); linker?.assertCurrent(); }); return host; }; const host = await project(); if (entry && !current(entry) || signal?.aborted) return failed("NODE_INVOKE_FAILED", "Session Share is unavailable. Refresh the catalog."); publicationGuards.get(host)?.(); if (entry?.pending && allowPartialResults === true && onHost && waitUntil) { publishSessionCatalogHost({ waitUntil, onHost: (completedHost) => { if (completedHost && current(entry) && !signal?.aborted) { publicationGuards.get(completedHost)?.(); onHost(completedHost); } } }, entry.pending.then(() => signal?.aborted ? void 0 : project())); if (!host.error) { const pendingHost = { ...host, pending: true }; const guard = publicationGuards.get(host); if (guard) publicationGuards.set(pendingHost, guard); return pendingHost; } } onHost?.(host); return host; } return { id: "openclaw", label: "OpenClaw sessions", supportsProcessHomeIsolation: true, audience: "session-viewers", async list(query) { query.signal?.throwIfAborted(); const config = api.runtime.config.current(); let nodes; try { nodes = (await (query.listNodes?.() ?? api.runtime.nodes.list())).nodes; } catch (error) { query.signal?.throwIfAborted(); throw error; } query.signal?.throwIfAborted(); if (api.runtime.config.current() !== config) return []; reconcile(nodes); const requested = query.hostIds ? new Set(query.hostIds) : void 0; const eligible = nodes.filter((node) => isSessionHost(node) && (!requested || requested.has(`node:${node.nodeId}`))).toSorted((left, right) => nodeLabel(left).localeCompare(nodeLabel(right)) || left.nodeId.localeCompare(right.nodeId)).slice(0, MAX_SNAPSHOTS); admit(eligible); const owner = context; const hosts = await Promise.all(eligible.map((node) => { query.signal?.throwIfAborted(); return listNode(node, query); })); query.signal?.throwIfAborted(); if (context !== owner || api.runtime.config.current() !== config) return []; for (const host of hosts) publicationGuards.get(host)?.(); return hosts; }, async read(request) { if (!request.hostId.startsWith("node:") || !request.hostId.slice(5)) throw new Error("Select a paired node host to read an OpenClaw session"); const nodeId = request.hostId.slice(5); const node = (await api.runtime.nodes.list()).nodes.find((candidate) => candidate.nodeId === nodeId && candidate.connected && isSessionHost(candidate)); if (!node) throw new Error("OpenClaw session node is unavailable. Reconnect it and refresh the catalog."); const page = parseSessionShareTranscriptPage(await api.runtime.nodes.invoke({ nodeId, command: SESSION_SHARE_READ_COMMAND, timeoutMs: 3e4, scopes: ["operator.write"], params: { threadId: request.threadId, limit: sessionCatalogPaging.boundedLimit(request.limit), ...request.cursor !== void 0 ? { cursor: request.cursor } : {} } }), request.threadId); const config = api.runtime.config.current(); const linkParticipant = bindingFor(nodeId).linkGitHubIdentities ? (await prepareSessionCatalogGitHubLinker({ participants: page.items.flatMap((item) => item.sender ? [item.sender] : []) })).linkParticipant : void 0; if (api.runtime.config.current() !== config) throw new Error("Session Share configuration changed while preparing the page. Retry the request."); return { ...page, hostId: request.hostId, label: nodeLabel(node), items: page.items.map((item) => { if (!item.sender) return item; const sender = { ...item.sender, identity: namespaceIdentity(item.sender.identity, request.hostId) }; return Object.assign({}, item, { sender: linkParticipant?.(sender) ?? sender }); }) }; } }; } //#endregion //#region extensions/session-share/index.ts var session_share_default = definePluginEntry({ id: "session-share", name: "Session Share", description: "Read-only OpenClaw sessions on paired gateways", register(api) { api.registerSessionCatalog(createSessionShareCatalog(api)); for (const command of createSessionShareNodeCommands(api)) api.registerNodeHostCommand(command); api.registerNodeInvokePolicy({ commands: SESSION_SHARE_COMMANDS, defaultPlatforms: [ "macos", "linux", "windows" ], handle: (context) => context.invokeNode() }); } }); //#endregion export { session_share_default as default };