feat: persist session origin metadata across connectors

This commit is contained in:
Peter Steinberger
2026-01-18 02:41:06 +00:00
parent 0c93b9b7bb
commit 34590d2144
30 changed files with 246 additions and 66 deletions

View File

@@ -38,6 +38,7 @@ Docs: https://docs.clawd.bot
### Changes ### Changes
- Memory: add OpenAI Batch API indexing for embeddings when configured. - Memory: add OpenAI Batch API indexing for embeddings when configured.
- Memory: enable OpenAI batch indexing by default for OpenAI embeddings. - Memory: enable OpenAI batch indexing by default for OpenAI embeddings.
- Sessions: persist origin metadata across connectors for generic session explainers.
### Fixes ### Fixes
- Memory: retry transient 5xx errors (Cloudflare) during embedding indexing. - Memory: retry transient 5xx errors (Cloudflare) during embedding indexing.

View File

@@ -25,6 +25,7 @@ All session state is **owned by the gateway** (the “master” Clawdbot). UI cl
- Transcripts: `~/.clawdbot/agents/<agentId>/sessions/<SessionId>.jsonl` (Telegram topic sessions use `.../<SessionId>-topic-<threadId>.jsonl`). - Transcripts: `~/.clawdbot/agents/<agentId>/sessions/<SessionId>.jsonl` (Telegram topic sessions use `.../<SessionId>-topic-<threadId>.jsonl`).
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand. - The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand.
- Group entries may include `displayName`, `channel`, `subject`, `room`, and `space` to label sessions in UIs. - Group entries may include `displayName`, `channel`, `subject`, `room`, and `space` to label sessions in UIs.
- Session entries include `origin` metadata (label + routing hints) so UIs can explain where a session came from.
- Clawdbot does **not** read legacy Pi/Tau session folders. - Clawdbot does **not** read legacy Pi/Tau session folders.
## Session pruning ## Session pruning
@@ -113,3 +114,11 @@ Send these as standalone messages so they register.
## Tips ## Tips
- Keep the primary key dedicated to 1:1 traffic; let groups keep their own keys. - Keep the primary key dedicated to 1:1 traffic; let groups keep their own keys.
- When automating cleanup, delete individual keys instead of the whole store to preserve context elsewhere. - When automating cleanup, delete individual keys instead of the whole store to preserve context elsewhere.
## Session origin metadata
Each session entry records where it came from (best-effort) in `origin`:
- `label`: human label (resolved from conversation label + group subject/channel)
- `provider`: normalized channel id (including extensions)
- `from`/`to`: raw routing ids from the inbound envelope
- `accountId`: provider account id (when multi-account)
- `threadId`: thread/topic id when the channel supports it

View File

@@ -18,7 +18,11 @@ import type { ReplyPayload } from "../../../../../src/auto-reply/types.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../../../src/channels/command-gating.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../../../src/channels/command-gating.js";
import { formatAllowlistMatchMeta } from "../../../../../src/channels/plugins/allowlist-match.js"; import { formatAllowlistMatchMeta } from "../../../../../src/channels/plugins/allowlist-match.js";
import { loadConfig } from "../../../../../src/config/config.js"; import { loadConfig } from "../../../../../src/config/config.js";
import { resolveStorePath, updateLastRoute } from "../../../../../src/config/sessions.js"; import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../../../../../src/config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../../../src/globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../../../src/globals.js";
import { enqueueSystemEvent } from "../../../../../src/infra/system-events.js"; import { enqueueSystemEvent } from "../../../../../src/infra/system-events.js";
import { getChildLogger } from "../../../../../src/logging.js"; import { getChildLogger } from "../../../../../src/logging.js";
@@ -494,7 +498,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
}); });
const groupSystemPrompt = roomConfigInfo.config?.systemPrompt?.trim() || undefined; const groupSystemPrompt = roomConfigInfo.config?.systemPrompt?.trim() || undefined;
const ctxPayload = finalizeInboundContext({ const ctxPayload = finalizeInboundContext({
Body: body, Body: body,
RawBody: bodyText, RawBody: bodyText,
CommandBody: bodyText, CommandBody: bodyText,
@@ -526,10 +530,21 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
OriginatingTo: `room:${roomId}`, OriginatingTo: `room:${roomId}`,
}); });
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logger.warn(
{ error: String(err), storePath, sessionKey: ctxPayload.SessionKey ?? route.sessionKey },
"failed updating session meta",
);
});
if (isDirectMessage) { if (isDirectMessage) {
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
await updateLastRoute({ await updateLastRoute({
storePath, storePath,
sessionKey: route.mainSessionKey, sessionKey: route.mainSessionKey,

View File

@@ -18,6 +18,7 @@ import { resolveCommandAuthorizedFromAuthorizers } from "../../../../src/channel
import { formatAllowlistMatchMeta } from "../../../../src/channels/plugins/allowlist-match.js"; import { formatAllowlistMatchMeta } from "../../../../src/channels/plugins/allowlist-match.js";
import { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../../../src/globals.js";
import { enqueueSystemEvent } from "../../../../src/infra/system-events.js"; import { enqueueSystemEvent } from "../../../../src/infra/system-events.js";
import { recordSessionMetaFromInbound, resolveStorePath } from "../../../../src/config/sessions.js";
import { import {
readChannelAllowFromStore, readChannelAllowFromStore,
upsertChannelPairingRequest, upsertChannelPairingRequest,
@@ -459,6 +460,17 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
...mediaPayload, ...mediaPayload,
}); });
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logVerbose(`msteams: failed updating session meta: ${String(err)}`);
});
if (shouldLogVerbose()) { if (shouldLogVerbose()) {
logVerbose(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`); logVerbose(`msteams inbound: from=${ctxPayload.From} preview="${preview}"`);
} }

View File

@@ -7,6 +7,7 @@ import {
} from "../../../src/auto-reply/command-detection.js"; } from "../../../src/auto-reply/command-detection.js";
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
import { recordSessionMetaFromInbound, resolveStorePath } from "../../../src/config/sessions.js";
import { import {
ZaloApiError, ZaloApiError,
deleteWebhook, deleteWebhook,
@@ -552,6 +553,17 @@ async function processMessageWithPipeline(params: {
OriginatingTo: `zalo:${chatId}`, OriginatingTo: `zalo:${chatId}`,
}); });
const storePath = resolveStorePath(config.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
runtime.error?.(`zalo: failed updating session meta: ${String(err)}`);
});
await deps.dispatchReplyWithBufferedBlockDispatcher({ await deps.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload, ctx: ctxPayload,
cfg: config, cfg: config,

View File

@@ -8,6 +8,7 @@ import {
import { mergeAllowlist, summarizeMapping } from "../../../src/channels/allowlists/resolve-utils.js"; import { mergeAllowlist, summarizeMapping } from "../../../src/channels/allowlists/resolve-utils.js";
import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../../src/auto-reply/reply/inbound-context.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js"; import { resolveCommandAuthorizedFromAuthorizers } from "../../../src/channels/command-gating.js";
import { recordSessionMetaFromInbound, resolveStorePath } from "../../../src/config/sessions.js";
import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js"; import { loadCoreChannelDeps, type CoreChannelDeps } from "./core-bridge.js";
import { sendMessageZalouser } from "./send.js"; import { sendMessageZalouser } from "./send.js";
import type { import type {
@@ -299,6 +300,17 @@ async function processMessage(
OriginatingTo: `zalouser:${chatId}`, OriginatingTo: `zalouser:${chatId}`,
}); });
const storePath = resolveStorePath(config.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
runtime.error?.(`zalouser: failed updating session meta: ${String(err)}`);
});
await deps.dispatchReplyWithBufferedBlockDispatcher({ await deps.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload, ctx: ctxPayload,
cfg: config, cfg: config,

View File

@@ -39,6 +39,7 @@ vi.mock("../config/sessions.js", () => ({
resolveAgentIdFromSessionKey: () => "main", resolveAgentIdFromSessionKey: () => "main",
resolveStorePath: () => "/tmp/sessions.json", resolveStorePath: () => "/tmp/sessions.json",
resolveMainSessionKey: () => "agent:main:main", resolveMainSessionKey: () => "agent:main:main",
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("./pi-embedded.js", () => embeddedRunMock); vi.mock("./pi-embedded.js", () => embeddedRunMock);

View File

@@ -4,13 +4,11 @@ import path from "node:path";
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import { import {
buildGroupDisplayName,
DEFAULT_IDLE_MINUTES, DEFAULT_IDLE_MINUTES,
DEFAULT_RESET_TRIGGERS, DEFAULT_RESET_TRIGGERS,
deriveSessionMetaPatch,
type GroupKeyResolution, type GroupKeyResolution,
loadSessionStore, loadSessionStore,
resolveGroupSessionKey, resolveGroupSessionKey,
@@ -237,39 +235,16 @@ export async function initSessionState(params: {
lastTo, lastTo,
lastAccountId, lastAccountId,
}; };
if (groupResolution?.channel) { const metaPatch = deriveSessionMetaPatch({
const channel = groupResolution.channel; ctx: sessionCtxForState,
const subject = ctx.GroupSubject?.trim(); sessionKey,
const space = ctx.GroupSpace?.trim(); existing: sessionEntry,
const explicitChannel = ctx.GroupChannel?.trim(); groupResolution,
const normalizedChannel = normalizeChannelId(channel); });
const isChannelProvider = Boolean( if (metaPatch) {
normalizedChannel && sessionEntry = { ...sessionEntry, ...metaPatch };
getChannelDock(normalizedChannel)?.capabilities.chatTypes.includes("channel"), }
); if (!sessionEntry.chatType) {
const nextGroupChannel =
explicitChannel ??
((groupResolution.chatType === "channel" || isChannelProvider) &&
subject &&
subject.startsWith("#")
? subject
: undefined);
const nextSubject = nextGroupChannel ? undefined : subject;
sessionEntry.chatType = groupResolution.chatType ?? "group";
sessionEntry.channel = channel;
sessionEntry.groupId = groupResolution.id;
if (nextSubject) sessionEntry.subject = nextSubject;
if (nextGroupChannel) sessionEntry.groupChannel = nextGroupChannel;
if (space) sessionEntry.space = space;
sessionEntry.displayName = buildGroupDisplayName({
provider: sessionEntry.channel,
subject: sessionEntry.subject,
groupChannel: sessionEntry.groupChannel,
space: sessionEntry.space,
id: groupResolution.id,
key: sessionKey,
});
} else if (!sessionEntry.chatType) {
sessionEntry.chatType = "direct"; sessionEntry.chatType = "direct";
} }
const threadLabel = ctx.ThreadLabel?.trim(); const threadLabel = ctx.ThreadLabel?.trim();

View File

@@ -21,6 +21,7 @@ vi.mock("../config/config.js", async (importOriginal) => {
vi.mock("../config/sessions.js", () => ({ vi.mock("../config/sessions.js", () => ({
resolveStorePath: () => "/tmp/sessions.json", resolveStorePath: () => "/tmp/sessions.json",
loadSessionStore: () => testStore, loadSessionStore: () => testStore,
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("../web/auth-store.js", () => ({ vi.mock("../web/auth-store.js", () => ({

View File

@@ -100,6 +100,7 @@ vi.mock("../config/sessions.js", () => ({
loadSessionStore: mocks.loadSessionStore, loadSessionStore: mocks.loadSessionStore,
resolveMainSessionKey: mocks.resolveMainSessionKey, resolveMainSessionKey: mocks.resolveMainSessionKey,
resolveStorePath: mocks.resolveStorePath, resolveStorePath: mocks.resolveStorePath,
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("../channels/plugins/index.js", () => ({ vi.mock("../channels/plugins/index.js", () => ({
listChannelPlugins: () => listChannelPlugins: () =>

View File

@@ -1,4 +1,5 @@
export * from "./sessions/group.js"; export * from "./sessions/group.js";
export * from "./sessions/metadata.js";
export * from "./sessions/main-session.js"; export * from "./sessions/main-session.js";
export * from "./sessions/paths.js"; export * from "./sessions/paths.js";
export * from "./sessions/session-key.js"; export * from "./sessions/session-key.js";

View File

@@ -11,6 +11,8 @@ import {
normalizeSessionDeliveryFields, normalizeSessionDeliveryFields,
type DeliveryContext, type DeliveryContext,
} from "../../utils/delivery-context.js"; } from "../../utils/delivery-context.js";
import type { MsgContext } from "../../auto-reply/templating.js";
import { deriveSessionMetaPatch } from "./metadata.js";
import { mergeSessionEntry, type SessionEntry } from "./types.js"; import { mergeSessionEntry, type SessionEntry } from "./types.js";
// ============================================================================ // ============================================================================
@@ -334,6 +336,31 @@ export async function updateSessionStoreEntry(params: {
}); });
} }
export async function recordSessionMetaFromInbound(params: {
storePath: string;
sessionKey: string;
ctx: MsgContext;
groupResolution?: import("./types.js").GroupKeyResolution | null;
createIfMissing?: boolean;
}): Promise<SessionEntry | null> {
const { storePath, sessionKey, ctx } = params;
const createIfMissing = params.createIfMissing ?? true;
return await updateSessionStore(storePath, (store) => {
const existing = store[sessionKey];
const patch = deriveSessionMetaPatch({
ctx,
sessionKey,
existing,
groupResolution: params.groupResolution,
});
if (!patch) return existing ?? null;
if (!existing && !createIfMissing) return null;
const next = mergeSessionEntry(existing, patch);
store[sessionKey] = next;
return next;
});
}
export async function updateLastRoute(params: { export async function updateLastRoute(params: {
storePath: string; storePath: string;
sessionKey: string; sessionKey: string;

View File

@@ -11,6 +11,17 @@ export type SessionChannelId = ChannelId | "webchat";
export type SessionChatType = NormalizedChatType; export type SessionChatType = NormalizedChatType;
export type SessionOrigin = {
label?: string;
provider?: string;
surface?: string;
chatType?: SessionChatType;
from?: string;
to?: string;
accountId?: string;
threadId?: string | number;
};
export type SessionEntry = { export type SessionEntry = {
/** /**
* Last delivered heartbeat payload (used to suppress duplicate heartbeat notifications). * Last delivered heartbeat payload (used to suppress duplicate heartbeat notifications).
@@ -69,6 +80,7 @@ export type SessionEntry = {
subject?: string; subject?: string;
groupChannel?: string; groupChannel?: string;
space?: string; space?: string;
origin?: SessionOrigin;
deliveryContext?: DeliveryContext; deliveryContext?: DeliveryContext;
lastChannel?: SessionChannelId; lastChannel?: SessionChannelId;
lastTo?: string; lastTo?: string;

View File

@@ -17,7 +17,11 @@ import {
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
import type { ReplyPayload } from "../../auto-reply/types.js"; import type { ReplyPayload } from "../../auto-reply/types.js";
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js"; import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
import { buildAgentSessionKey } from "../../routing/resolve-route.js"; import { buildAgentSessionKey } from "../../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../../routing/session-key.js"; import { resolveThreadSessionKeys } from "../../routing/session-key.js";
@@ -264,11 +268,18 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget, OriginatingTo: autoThreadContext?.OriginatingTo ?? replyTarget,
}); });
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logVerbose(`discord: failed updating session meta: ${String(err)}`);
});
if (isDirectMessage) { if (isDirectMessage) {
const sessionCfg = cfg.session;
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,
});
await updateLastRoute({ await updateLastRoute({
storePath, storePath,
sessionKey: route.mainSessionKey, sessionKey: route.mainSessionKey,

View File

@@ -9,6 +9,7 @@ import {
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { import {
resolveMainSessionKeyFromConfig, resolveMainSessionKeyFromConfig,
snapshotSessionOrigin,
type SessionEntry, type SessionEntry,
updateSessionStore, updateSessionStore,
} from "../config/sessions.js"; } from "../config/sessions.js";
@@ -205,6 +206,7 @@ export const handleSessionsBridgeMethods: BridgeMethodHandler = async (
contextTokens: entry?.contextTokens, contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
label: entry?.label, label: entry?.label,
origin: snapshotSessionOrigin(entry),
displayName: entry?.displayName, displayName: entry?.displayName,
chatType: entry?.chatType, chatType: entry?.chatType,
channel: entry?.channel, channel: entry?.channel,

View File

@@ -6,6 +6,7 @@ import { stopSubagentsForRequester } from "../../auto-reply/reply/abort.js";
import { clearSessionQueues } from "../../auto-reply/reply/queue.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue.js";
import { loadConfig } from "../../config/config.js"; import { loadConfig } from "../../config/config.js";
import { import {
snapshotSessionOrigin,
resolveMainSessionKey, resolveMainSessionKey,
type SessionEntry, type SessionEntry,
updateSessionStore, updateSessionStore,
@@ -173,6 +174,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
contextTokens: entry?.contextTokens, contextTokens: entry?.contextTokens,
sendPolicy: entry?.sendPolicy, sendPolicy: entry?.sendPolicy,
label: entry?.label, label: entry?.label,
origin: snapshotSessionOrigin(entry),
lastChannel: entry?.lastChannel, lastChannel: entry?.lastChannel,
lastTo: entry?.lastTo, lastTo: entry?.lastTo,
skillsSnapshot: entry?.skillsSnapshot, skillsSnapshot: entry?.skillsSnapshot,

View File

@@ -381,6 +381,8 @@ export function listSessionsFromStore(params: {
const groupChannel = entry?.groupChannel; const groupChannel = entry?.groupChannel;
const space = entry?.space; const space = entry?.space;
const id = parsed?.id; const id = parsed?.id;
const origin = entry?.origin;
const originLabel = origin?.label;
const displayName = const displayName =
entry?.displayName ?? entry?.displayName ??
(channel (channel
@@ -393,7 +395,8 @@ export function listSessionsFromStore(params: {
key, key,
}) })
: undefined) ?? : undefined) ??
entry?.label; entry?.label ??
originLabel;
const deliveryFields = normalizeSessionDeliveryFields(entry); const deliveryFields = normalizeSessionDeliveryFields(entry);
return { return {
key, key,
@@ -405,6 +408,7 @@ export function listSessionsFromStore(params: {
groupChannel, groupChannel,
space, space,
chatType: entry?.chatType, chatType: entry?.chatType,
origin,
updatedAt, updatedAt,
sessionId: entry?.sessionId, sessionId: entry?.sessionId,
systemSent: entry?.systemSent, systemSent: entry?.systemSent,

View File

@@ -18,6 +18,7 @@ export type GatewaySessionRow = {
groupChannel?: string; groupChannel?: string;
space?: string; space?: string;
chatType?: NormalizedChatType; chatType?: NormalizedChatType;
origin?: SessionEntry["origin"];
updatedAt: number | null; updatedAt: number | null;
sessionId?: string; sessionId?: string;
systemSent?: boolean; systemSent?: boolean;

View File

@@ -38,6 +38,7 @@ vi.mock("../pairing/pairing-store.js", () => ({
vi.mock("../config/sessions.js", () => ({ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("./client.js", () => ({ vi.mock("./client.js", () => ({

View File

@@ -38,6 +38,7 @@ vi.mock("../pairing/pairing-store.js", () => ({
vi.mock("../config/sessions.js", () => ({ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("./client.js", () => ({ vi.mock("./client.js", () => ({

View File

@@ -32,7 +32,11 @@ import {
resolveChannelGroupPolicy, resolveChannelGroupPolicy,
resolveChannelGroupRequireMention, resolveChannelGroupRequireMention,
} from "../../config/group-policy.js"; } from "../../config/group-policy.js";
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js"; import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
import { waitForTransportReady } from "../../infra/transport-ready.js"; import { waitForTransportReady } from "../../infra/transport-ready.js";
import { mediaKindFromMime } from "../../media/constants.js"; import { mediaKindFromMime } from "../../media/constants.js";
@@ -449,11 +453,18 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
OriginatingTo: imessageTo, OriginatingTo: imessageTo,
}); });
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logVerbose(`imessage: failed updating session meta: ${String(err)}`);
});
if (!isGroup) { if (!isGroup) {
const sessionCfg = cfg.session;
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,
});
const to = (isGroup ? chatTarget : undefined) || sender; const to = (isGroup ? chatTarget : undefined) || sender;
if (to) { if (to) {
await updateLastRoute({ await updateLastRoute({

View File

@@ -35,6 +35,7 @@ vi.mock("../pairing/pairing-store.js", () => ({
vi.mock("../config/sessions.js", () => ({ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
const streamMock = vi.fn(); const streamMock = vi.fn();

View File

@@ -39,6 +39,7 @@ vi.mock("../pairing/pairing-store.js", () => ({
vi.mock("../config/sessions.js", () => ({ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
const streamMock = vi.fn(); const streamMock = vi.fn();

View File

@@ -20,7 +20,11 @@ import {
} from "../../auto-reply/reply/history.js"; } from "../../auto-reply/reply/history.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js"; import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js"; import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../../config/sessions.js";
import { danger, logVerbose, shouldLogVerbose } from "../../globals.js"; import { danger, logVerbose, shouldLogVerbose } from "../../globals.js";
import { enqueueSystemEvent } from "../../infra/system-events.js"; import { enqueueSystemEvent } from "../../infra/system-events.js";
import { mediaKindFromMime } from "../../media/constants.js"; import { mediaKindFromMime } from "../../media/constants.js";
@@ -140,11 +144,18 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
OriginatingTo: signalTo, OriginatingTo: signalTo,
}); });
const storePath = resolveStorePath(deps.cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logVerbose(`signal: failed updating session meta: ${String(err)}`);
});
if (!entry.isGroup) { if (!entry.isGroup) {
const sessionCfg = deps.cfg.session;
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,
});
await updateLastRoute({ await updateLastRoute({
storePath, storePath,
sessionKey: route.mainSessionKey, sessionKey: route.mainSessionKey,

View File

@@ -54,6 +54,7 @@ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
resolveSessionKey: vi.fn(), resolveSessionKey: vi.fn(),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("@slack/bolt", () => { vi.mock("@slack/bolt", () => {

View File

@@ -56,6 +56,7 @@ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
resolveSessionKey: vi.fn(), resolveSessionKey: vi.fn(),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("@slack/bolt", () => { vi.mock("@slack/bolt", () => {

View File

@@ -54,6 +54,7 @@ vi.mock("../config/sessions.js", () => ({
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"), resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
resolveSessionKey: vi.fn(), resolveSessionKey: vi.fn(),
recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined),
})); }));
vi.mock("@slack/bolt", () => { vi.mock("@slack/bolt", () => {

View File

@@ -21,6 +21,7 @@ import { resolveThreadSessionKeys } from "../../../routing/session-key.js";
import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js"; import { resolveMentionGatingWithBypass } from "../../../channels/mention-gating.js";
import { resolveConversationLabel } from "../../../channels/conversation-label.js"; import { resolveConversationLabel } from "../../../channels/conversation-label.js";
import { resolveControlCommandGate } from "../../../channels/command-gating.js"; import { resolveControlCommandGate } from "../../../channels/command-gating.js";
import { recordSessionMetaFromInbound, resolveStorePath } from "../../../config/sessions.js";
import type { ResolvedSlackAccount } from "../../accounts.js"; import type { ResolvedSlackAccount } from "../../accounts.js";
import { reactSlackMessage } from "../../actions.js"; import { reactSlackMessage } from "../../actions.js";
@@ -471,6 +472,24 @@ export async function prepareSlackMessage(params: {
OriginatingTo: slackTo, OriginatingTo: slackTo,
}) satisfies FinalizedMsgContext; }) satisfies FinalizedMsgContext;
const storePath = resolveStorePath(ctx.cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: sessionKey,
ctx: ctxPayload,
}).catch((err) => {
ctx.logger.warn(
{
error: String(err),
storePath,
sessionKey,
},
"failed updating session meta",
);
});
const replyTarget = ctxPayload.To ?? undefined; const replyTarget = ctxPayload.To ?? undefined;
if (!replyTarget) return null; if (!replyTarget) return null;

View File

@@ -12,7 +12,11 @@ import {
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js"; import { buildMentionRegexes, matchesMentionPatterns } from "../auto-reply/reply/mentions.js";
import { formatLocationText, toLocationContext } from "../channels/location.js"; import { formatLocationText, toLocationContext } from "../channels/location.js";
import { resolveStorePath, updateLastRoute } from "../config/sessions.js"; import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../config/sessions.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
import { logVerbose, shouldLogVerbose } from "../globals.js"; import { logVerbose, shouldLogVerbose } from "../globals.js";
@@ -500,6 +504,17 @@ export const buildTelegramMessageContext = async ({
OriginatingTo: `telegram:${chatId}`, OriginatingTo: `telegram:${chatId}`,
}); });
const storePath = resolveStorePath(cfg.session?.store, {
agentId: route.agentId,
});
void recordSessionMetaFromInbound({
storePath,
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
logVerbose(`telegram: failed updating session meta: ${String(err)}`);
});
if (replyTarget && shouldLogVerbose()) { if (replyTarget && shouldLogVerbose()) {
const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120); const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120);
logVerbose( logVerbose(
@@ -514,10 +529,6 @@ export const buildTelegramMessageContext = async ({
} }
if (!isGroup) { if (!isGroup) {
const sessionCfg = cfg.session;
const storePath = resolveStorePath(sessionCfg?.store, {
agentId: route.agentId,
});
await updateLastRoute({ await updateLastRoute({
storePath, storePath,
sessionKey: route.mainSessionKey, sessionKey: route.mainSessionKey,

View File

@@ -20,6 +20,7 @@ import { shouldComputeCommandAuthorized } from "../../../auto-reply/command-dete
import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js"; import { finalizeInboundContext } from "../../../auto-reply/reply/inbound-context.js";
import { toLocationContext } from "../../../channels/location.js"; import { toLocationContext } from "../../../channels/location.js";
import type { loadConfig } from "../../../config/config.js"; import type { loadConfig } from "../../../config/config.js";
import { recordSessionMetaFromInbound, resolveStorePath } from "../../../config/sessions.js";
import { logVerbose, shouldLogVerbose } from "../../../globals.js"; import { logVerbose, shouldLogVerbose } from "../../../globals.js";
import type { getChildLogger } from "../../../logging.js"; import type { getChildLogger } from "../../../logging.js";
import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js"; import { readChannelAllowFromStore } from "../../../pairing/pairing-store.js";
@@ -33,7 +34,7 @@ import type { WebInboundMsg } from "../types.js";
import { elide } from "../util.js"; import { elide } from "../util.js";
import { maybeSendAckReaction } from "./ack-reaction.js"; import { maybeSendAckReaction } from "./ack-reaction.js";
import { formatGroupMembers } from "./group-members.js"; import { formatGroupMembers } from "./group-members.js";
import { updateLastRouteInBackground } from "./last-route.js"; import { trackBackgroundTask, updateLastRouteInBackground } from "./last-route.js";
import { buildInboundLine } from "./message-line.js"; import { buildInboundLine } from "./message-line.js";
export type GroupHistoryEntry = { export type GroupHistoryEntry = {
@@ -249,8 +250,7 @@ export async function processMessage(params: {
identityName: resolveIdentityName(params.cfg, params.route.agentId), identityName: resolveIdentityName(params.cfg, params.route.agentId),
}; };
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({ const ctxPayload = finalizeInboundContext({
ctx: finalizeInboundContext({
Body: combinedBody, Body: combinedBody,
RawBody: params.msg.body, RawBody: params.msg.body,
CommandBody: params.msg.body, CommandBody: params.msg.body,
@@ -283,7 +283,29 @@ export async function processMessage(params: {
Surface: "whatsapp", Surface: "whatsapp",
OriginatingChannel: "whatsapp", OriginatingChannel: "whatsapp",
OriginatingTo: params.msg.from, OriginatingTo: params.msg.from,
}), });
const storePath = resolveStorePath(params.cfg.session?.store, {
agentId: params.route.agentId,
});
const metaTask = recordSessionMetaFromInbound({
storePath,
sessionKey: params.route.sessionKey,
ctx: ctxPayload,
}).catch((err) => {
params.replyLogger.warn(
{
error: formatError(err),
storePath,
sessionKey: params.route.sessionKey,
},
"failed updating session meta",
);
});
trackBackgroundTask(params.backgroundTasks, metaTask);
const { queuedFinal } = await dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg: params.cfg, cfg: params.cfg,
replyResolver: params.replyResolver, replyResolver: params.replyResolver,
dispatcherOptions: { dispatcherOptions: {