Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 16 additions & 47 deletions src/card/streaming-card-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
*/

import { readFile } from 'node:fs/promises';
import { resolveDefaultAgentId } from 'openclaw/plugin-sdk/agent-runtime';

import type { ReplyPayload } from 'openclaw/plugin-sdk';
import { SILENT_REPLY_TOKEN } from 'openclaw/plugin-sdk/reply-runtime';
import { extractLarkApiCode } from '../core/api-error';
Expand Down Expand Up @@ -141,13 +141,13 @@ export class StreamingCardController {
const runtime = LarkClient.runtime as {
agent?: {
session?: {
resolveStorePath?: (storePath?: string) => string;
resolveStorePath?: (storePath?: string, opts?: { agentId?: string }) => string;
loadSessionStore?: (storePath: string) => Record<string, Record<string, unknown>>;
};
};
channel?: {
session?: {
resolveStorePath?: (storePath?: string) => string;
resolveStorePath?: (storePath?: string, opts?: { agentId?: string }) => string;
};
};
} | null;
Expand All @@ -156,41 +156,20 @@ export class StreamingCardController {
const cfgWithSession = this.deps.cfg as { sessions?: { store?: string }; session?: { store?: string } };
const sessionStorePath = cfgWithSession.sessions?.store ?? cfgWithSession.session?.store;
const key = this.deps.sessionKey.trim().toLowerCase();

// WORKAROUND: SDK session key round-trip bug.
// The SDK's toAgentRequestSessionKey() strips the agent scope from keys
// like "agent:hr:main" → "main", then toAgentStoreSessionKey() rebuilds
// using the default agent ID → "agent:main:main". This means metrics
// written by the SDK always land under "agent:<defaultAgentId>:…"
// regardless of the account-scoped agent ID the plugin routing generated.
// Fallback: when the primary key misses, try replacing the agent-id
// segment with the resolved default agent ID.
// TODO: remove once the SDK preserves the original agent ID during the
// request→store key round-trip.
const defaultAgentId = resolveDefaultAgentId(this.deps.cfg as Record<string, unknown>);
const fallbackKey = key.replace(/^(agent):[^:]+:/, `$1:${defaultAgentId}:`);
const candidateKeys = fallbackKey !== key ? [key, fallbackKey] : [key];
// Extract agentId from session key (format: "agent:<agentId>:feishu:...")
// to route the store lookup to the correct per-agent session file.
const agentId = key.match(/^agent:([^:]+):/)?.[1];

const sessionApi = runtime.agent?.session;
if (sessionApi?.resolveStorePath && sessionApi?.loadSessionStore) {
const storePath = sessionApi.resolveStorePath(sessionStorePath);
const storePath = sessionApi.resolveStorePath(sessionStorePath, { agentId });
const store = sessionApi.loadSessionStore(storePath);

let entry: Record<string, unknown> | undefined;
let matchedKey: string | undefined;
for (const candidate of candidateKeys) {
const val = store[candidate];
if (val && typeof val === 'object') {
entry = val as Record<string, unknown>;
matchedKey = candidate;
break;
}
}

if (!entry) {
const entry = store[key];
if (!entry || typeof entry !== 'object') {
log.debug('footer metrics lookup: session entry missing', {
sessionKey: this.deps.sessionKey,
candidateKeys,
key,
storePath,
source: 'runtime.agent.session',
});
Expand All @@ -209,7 +188,7 @@ export class StreamingCardController {
};
log.debug('footer metrics lookup: session entry found', {
sessionKey: this.deps.sessionKey,
matchedKey,
key,
storePath,
source: 'runtime.agent.session',
});
Expand All @@ -221,29 +200,19 @@ export class StreamingCardController {
return undefined;
}

const storePath = channelSession.resolveStorePath(sessionStorePath);
const storePath = channelSession.resolveStorePath(sessionStorePath, { agentId });
const raw = await readFile(storePath, 'utf8');
const parsed: unknown = JSON.parse(raw);
const store =
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as Record<string, Record<string, unknown>>)
: {};

let entry: Record<string, unknown> | undefined;
let matchedKey: string | undefined;
for (const candidate of candidateKeys) {
const val = store[candidate];
if (val && typeof val === 'object') {
entry = val as Record<string, unknown>;
matchedKey = candidate;
break;
}
}

if (!entry) {
const entry = store[key];
if (!entry || typeof entry !== 'object') {
log.debug('footer metrics lookup: session entry missing', {
sessionKey: this.deps.sessionKey,
candidateKeys,
key,
storePath,
source: 'channel.session.file',
});
Expand All @@ -262,7 +231,7 @@ export class StreamingCardController {
};
log.debug('footer metrics lookup: session entry found', {
sessionKey: this.deps.sessionKey,
matchedKey,
key,
storePath,
source: 'channel.session.file',
});
Expand Down