feat(plugin): record non-wakeup messages as session history (no model)
Previously a non-wakeup message returned immediately and was fully discarded — the agent kept zero record of it, so when later woken in a discuss/work channel it replied without the conversation context. Now non-wakeup messages are ingested into the agent's OpenClaw session via recordInboundSession (createIfMissing) WITHOUT dispatch: the real model is not invoked and nothing is sent back to Fabric. This is correct for the turn engine — only the woken speaker emits a normal message or /no-reply; non-woken agents stay silent — while still giving the agent full channel context whenever it IS woken. Verified live: report-channel (all recipients wakeup=false) message logs 'recorded (no wakeup, history only)' with 0 dispatch/deliver/ posted; wakeup path unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
dist/fabric/src/inbound.js
vendored
38
dist/fabric/src/inbound.js
vendored
@@ -162,12 +162,6 @@ export class FabricInbound {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
async dispatch(agentId, guild, channelId, m, session) {
|
async dispatch(agentId, guild, channelId, m, session) {
|
||||||
// wakeup === false -> drop (Fabric already decided this agent is silent)
|
|
||||||
if (m.wakeup !== true) {
|
|
||||||
this.log.info(`fabric: drop (no wakeup) agent=${agentId} channel=${channelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.log.info(`fabric: dispatch agent=${agentId} channel=${channelId}`);
|
|
||||||
const core = this.core;
|
const core = this.core;
|
||||||
const cfg = this.cfg;
|
const cfg = this.cfg;
|
||||||
try {
|
try {
|
||||||
@@ -180,11 +174,7 @@ export class FabricInbound {
|
|||||||
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
|
||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
});
|
});
|
||||||
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
const baseCtx = {
|
||||||
// Fetch any uploaded files for the agent: download to a temp dir and
|
|
||||||
// hand openclaw local MediaPaths (+types) so the model receives them.
|
|
||||||
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
|
||||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
||||||
Body: m.content,
|
Body: m.content,
|
||||||
BodyForAgent: m.content,
|
BodyForAgent: m.content,
|
||||||
RawBody: m.content,
|
RawBody: m.content,
|
||||||
@@ -202,6 +192,32 @@ export class FabricInbound {
|
|||||||
Timestamp: m.createdAt ? Date.parse(m.createdAt) : Date.now(),
|
Timestamp: m.createdAt ? Date.parse(m.createdAt) : Date.now(),
|
||||||
OriginatingChannel: 'fabric',
|
OriginatingChannel: 'fabric',
|
||||||
OriginatingTo: `fabric:${channelId}`,
|
OriginatingTo: `fabric:${channelId}`,
|
||||||
|
};
|
||||||
|
// Non-wakeup: Fabric has already decided this agent is NOT the speaker
|
||||||
|
// this round. Do NOT run the model and do NOT send anything back — the
|
||||||
|
// discuss/work turn engine expects silence from non-woken agents (only
|
||||||
|
// the woken speaker emits a normal message or /no-reply). We still
|
||||||
|
// record the message into the agent's session so it has the full
|
||||||
|
// channel conversation as context whenever it IS later woken.
|
||||||
|
if (m.wakeup !== true) {
|
||||||
|
const ctxPayload = core.channel.reply.finalizeInboundContext(baseCtx);
|
||||||
|
await core.channel.session.recordInboundSession({
|
||||||
|
storePath,
|
||||||
|
sessionKey: route.sessionKey,
|
||||||
|
ctx: ctxPayload,
|
||||||
|
createIfMissing: true,
|
||||||
|
onRecordError: (err) => this.log.warn(`fabric: history record failed agent=${agentId}: ${String(err)}`),
|
||||||
|
});
|
||||||
|
this.log.info(`fabric: recorded (no wakeup, history only) agent=${agentId} channel=${channelId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.log.info(`fabric: dispatch agent=${agentId} channel=${channelId}`);
|
||||||
|
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
||||||
|
// Fetch any uploaded files for the agent: download to a temp dir and
|
||||||
|
// hand openclaw local MediaPaths (+types) so the model receives them.
|
||||||
|
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
||||||
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||||
|
...baseCtx,
|
||||||
// Provide ONLY local paths. The guild file URL is on a private host
|
// Provide ONLY local paths. The guild file URL is on a private host
|
||||||
// (e.g. localhost); openclaw's SSRF guard blocks re-fetching it, so
|
// (e.g. localhost); openclaw's SSRF guard blocks re-fetching it, so
|
||||||
// passing MediaUrls is both redundant (we already downloaded the
|
// passing MediaUrls is both redundant (we already downloaded the
|
||||||
|
|||||||
@@ -16,7 +16,16 @@ import type { IdentityRegistry } from './identity.js';
|
|||||||
type Core = {
|
type Core = {
|
||||||
channel: {
|
channel: {
|
||||||
routing: { resolveAgentRoute: (p: unknown) => { agentId: string; sessionKey: string; accountId?: string } };
|
routing: { resolveAgentRoute: (p: unknown) => { agentId: string; sessionKey: string; accountId?: string } };
|
||||||
session: { resolveStorePath: (store: unknown, o: { agentId: string }) => string };
|
session: {
|
||||||
|
resolveStorePath: (store: unknown, o: { agentId: string }) => string;
|
||||||
|
recordInboundSession: (p: {
|
||||||
|
storePath: string;
|
||||||
|
sessionKey: string;
|
||||||
|
ctx: unknown;
|
||||||
|
createIfMissing?: boolean;
|
||||||
|
onRecordError: (e: unknown) => void;
|
||||||
|
}) => Promise<unknown>;
|
||||||
|
};
|
||||||
reply: { finalizeInboundContext: (p: Record<string, unknown>) => unknown };
|
reply: { finalizeInboundContext: (p: Record<string, unknown>) => unknown };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -202,13 +211,6 @@ export class FabricInbound {
|
|||||||
m: FabricMessage,
|
m: FabricMessage,
|
||||||
session: FabricSession,
|
session: FabricSession,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// wakeup === false -> drop (Fabric already decided this agent is silent)
|
|
||||||
if (m.wakeup !== true) {
|
|
||||||
this.log.info(`fabric: drop (no wakeup) agent=${agentId} channel=${channelId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.log.info(`fabric: dispatch agent=${agentId} channel=${channelId}`);
|
|
||||||
|
|
||||||
const core = this.core as Core & Record<string, unknown>;
|
const core = this.core as Core & Record<string, unknown>;
|
||||||
const cfg = this.cfg as { session?: { store?: unknown } };
|
const cfg = this.cfg as { session?: { store?: unknown } };
|
||||||
try {
|
try {
|
||||||
@@ -222,13 +224,7 @@ export class FabricInbound {
|
|||||||
agentId: route.agentId,
|
agentId: route.agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
const baseCtx: Record<string, unknown> = {
|
||||||
|
|
||||||
// Fetch any uploaded files for the agent: download to a temp dir and
|
|
||||||
// hand openclaw local MediaPaths (+types) so the model receives them.
|
|
||||||
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
|
||||||
|
|
||||||
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
||||||
Body: m.content,
|
Body: m.content,
|
||||||
BodyForAgent: m.content,
|
BodyForAgent: m.content,
|
||||||
RawBody: m.content,
|
RawBody: m.content,
|
||||||
@@ -246,6 +242,39 @@ export class FabricInbound {
|
|||||||
Timestamp: m.createdAt ? Date.parse(m.createdAt) : Date.now(),
|
Timestamp: m.createdAt ? Date.parse(m.createdAt) : Date.now(),
|
||||||
OriginatingChannel: 'fabric',
|
OriginatingChannel: 'fabric',
|
||||||
OriginatingTo: `fabric:${channelId}`,
|
OriginatingTo: `fabric:${channelId}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Non-wakeup: Fabric has already decided this agent is NOT the speaker
|
||||||
|
// this round. Do NOT run the model and do NOT send anything back — the
|
||||||
|
// discuss/work turn engine expects silence from non-woken agents (only
|
||||||
|
// the woken speaker emits a normal message or /no-reply). We still
|
||||||
|
// record the message into the agent's session so it has the full
|
||||||
|
// channel conversation as context whenever it IS later woken.
|
||||||
|
if (m.wakeup !== true) {
|
||||||
|
const ctxPayload = core.channel.reply.finalizeInboundContext(baseCtx);
|
||||||
|
await core.channel.session.recordInboundSession({
|
||||||
|
storePath,
|
||||||
|
sessionKey: route.sessionKey,
|
||||||
|
ctx: ctxPayload,
|
||||||
|
createIfMissing: true,
|
||||||
|
onRecordError: (err: unknown) =>
|
||||||
|
this.log.warn(`fabric: history record failed agent=${agentId}: ${String(err)}`),
|
||||||
|
});
|
||||||
|
this.log.info(
|
||||||
|
`fabric: recorded (no wakeup, history only) agent=${agentId} channel=${channelId}`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log.info(`fabric: dispatch agent=${agentId} channel=${channelId}`);
|
||||||
|
const gt = await this.freshGuildToken(agentId, guild.nodeId, session);
|
||||||
|
|
||||||
|
// Fetch any uploaded files for the agent: download to a temp dir and
|
||||||
|
// hand openclaw local MediaPaths (+types) so the model receives them.
|
||||||
|
const media = await this.fetchAttachments(agentId, guild.endpoint, gt, m);
|
||||||
|
|
||||||
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
||||||
|
...baseCtx,
|
||||||
// Provide ONLY local paths. The guild file URL is on a private host
|
// Provide ONLY local paths. The guild file URL is on a private host
|
||||||
// (e.g. localhost); openclaw's SSRF guard blocks re-fetching it, so
|
// (e.g. localhost); openclaw's SSRF guard blocks re-fetching it, so
|
||||||
// passing MediaUrls is both redundant (we already downloaded the
|
// passing MediaUrls is both redundant (we already downloaded the
|
||||||
|
|||||||
Reference in New Issue
Block a user