Skip to content

Commit 7067745

Browse files
authored
fix(openclaw): stop silently skipping retention on default agent:main:main sessions (#1276)
The default dynamicBankGranularity is ["agent","channel","user"] in deriveBankId, but getIdentitySkipReason defaulted to false when the field was unset, causing agent:main:main sessions to be silently skipped from retention and recall. Align both paths: default agentBanking to true (matching the runtime default), normalise dynamicBankGranularity at config-validation time, and extract a shared DEFAULT_DYNAMIC_BANK_GRANULARITY constant. Also adds throttled info-level logging for identity skip events so operators can discover silent skips without enabling debug mode. Closes #1215
1 parent ee2d8f7 commit 7067745

2 files changed

Lines changed: 72 additions & 12 deletions

File tree

hindsight-integrations/openclaw/src/index.test.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -974,12 +974,10 @@ describe("session identity helpers", () => {
974974
expect(bankId).toBe("main::direct%3A12345::12345");
975975
});
976976

977-
it("marks operational main sessions as skippable", () => {
977+
it("allows agent:*:main sessions by default (default granularity includes 'agent')", () => {
978978
const result = getIdentitySkipReason({ sessionKey: "agent:main:main" });
979-
expect(result.reason).toEqual({
980-
kind: "final",
981-
detail: "internal main session agent:main:main",
982-
});
979+
expect(result.reason).toBeUndefined();
980+
expect(result.resolvedCtx?.senderId).toBe("agent-user:main");
983981
});
984982

985983
it.each([
@@ -1079,19 +1077,38 @@ describe("session identity helpers", () => {
10791077
expect(result.resolvedCtx?.senderId).toBe("agent-user:main");
10801078
});
10811079

1082-
it("does not broaden the carve-out when dynamicBankId is false but bankId is missing", () => {
1080+
it("allows agent:*:main when dynamicBankId is false but bankId is missing (default granularity includes 'agent')", () => {
10831081
const result = getIdentitySkipReason(
10841082
{ sessionKey: "agent:main:main" },
10851083
{ dynamicBankId: false }
10861084
);
1085+
// Default agentBanking is true (default granularity includes 'agent'),
1086+
// so the session is allowed even without an explicit bankId.
1087+
expect(result.reason).toBeUndefined();
1088+
});
1089+
1090+
it("does not broaden the carve-out when granularity excludes 'agent' and bankId is missing", () => {
1091+
const result = getIdentitySkipReason(
1092+
{ sessionKey: "agent:main:main" },
1093+
{ dynamicBankId: false, dynamicBankGranularity: ["channel", "user"] }
1094+
);
10871095
expect(result.reason).toEqual({
10881096
kind: "final",
10891097
detail: "internal main session agent:main:main",
10901098
});
10911099
});
10921100

1093-
it("preserves default skip behavior when agent banking is not enabled", () => {
1101+
it("allows agent:*:main sessions with empty config (default granularity includes 'agent')", () => {
10941102
const result = getIdentitySkipReason({ sessionKey: "agent:main:main" }, {});
1103+
expect(result.reason).toBeUndefined();
1104+
expect(result.resolvedCtx?.senderId).toBe("agent-user:main");
1105+
});
1106+
1107+
it("skips agent:*:main sessions when granularity explicitly excludes 'agent'", () => {
1108+
const result = getIdentitySkipReason(
1109+
{ sessionKey: "agent:main:main" },
1110+
{ dynamicBankGranularity: ["channel", "user"] }
1111+
);
10951112
expect(result.reason).toEqual({
10961113
kind: "final",
10971114
detail: "internal main session agent:main:main",

hindsight-integrations/openclaw/src/index.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,20 @@ const __dirname = dirname(__filename);
390390
// Default bank name (fallback when channel context not available)
391391
const DEFAULT_BANK_NAME = "openclaw";
392392

393+
// Default granularity fields used by deriveBankId when not explicitly configured.
394+
// This constant is shared between getPluginConfig (normalisation) and deriveBankId
395+
// (fallback) so the skip-reason check and the bank-routing logic always agree.
396+
const DEFAULT_DYNAMIC_BANK_GRANULARITY: Array<"agent" | "provider" | "channel" | "user"> = [
397+
"agent",
398+
"channel",
399+
"user",
400+
];
401+
402+
// Throttle set: log an info-level skip message at most once per (sessionKey) per
403+
// process lifetime so operators can discover silent retention/recall skips without
404+
// flooding the log on every turn.
405+
const loggedSkipSessions = new Set<string>();
406+
393407
function getConfiguredBankId(pluginConfig: PluginConfig): string | undefined {
394408
if (typeof pluginConfig.bankId !== "string") {
395409
return undefined;
@@ -794,6 +808,27 @@ function isRetryableIdentitySkipReason(reason: IdentitySkipReason | undefined):
794808
return reason?.kind === "retryable";
795809
}
796810

811+
/**
812+
* Log an identity-skip event at info level, throttled to once per session key
813+
* per process lifetime. This makes silent skips visible to operators without
814+
* flooding the log on every turn.
815+
*/
816+
function logSkipOnce(
817+
operation: "recall" | "retain" | "dispatch" | "agent_start",
818+
sessionKey: string | undefined,
819+
reason: IdentitySkipReason
820+
): void {
821+
if (!sessionKey) return;
822+
const cacheKey = `${operation}:${sessionKey}`;
823+
if (loggedSkipSessions.has(cacheKey)) return;
824+
loggedSkipSessions.add(cacheKey);
825+
const hint =
826+
reason.kind === "final"
827+
? ". If unexpected, set dynamicBankGranularity to ['agent','channel','user'] or use static banking (dynamicBankId: false + bankId: '<name>')"
828+
: "";
829+
log.info(`Skipping ${operation} on session '${sessionKey}': ${reason.detail}${hint}`);
830+
}
831+
797832
function cacheSessionIdentity(
798833
sessionKey: string | undefined,
799834
resolvedCtx: PluginHookAgentContext | undefined
@@ -886,11 +921,13 @@ export function getIdentitySkipReason(
886921
// exist to keep the default multi-tenant bank from being polluted by CLI/main
887922
// sessions that lack a stable identity. They should NOT fire when the user has
888923
// explicitly opted into a routing scheme that expects those sessions:
889-
// - dynamicBankGranularity includes 'agent' → each agent (including 'main')
890-
// gets its own bank
924+
// - dynamicBankGranularity includes 'agent' (the default) → each agent
925+
// (including 'main') gets its own bank
891926
// - dynamicBankId === false with a configured bankId → user pinned a single
892927
// named bank and wants every session retained into it
893-
const agentBanking = pluginConfig?.dynamicBankGranularity?.includes("agent") ?? false;
928+
// When dynamicBankGranularity is unset, the default is ["agent","channel","user"]
929+
// which includes "agent", so agentBanking defaults to true to match deriveBankId.
930+
const agentBanking = pluginConfig?.dynamicBankGranularity?.includes("agent") ?? true;
894931
const staticBanking =
895932
pluginConfig?.dynamicBankId === false &&
896933
typeof pluginConfig?.bankId === "string" &&
@@ -994,7 +1031,7 @@ export function deriveBankId(
9941031
const resolvedCtx = resolveSessionIdentity(ctx);
9951032
const fields = pluginConfig.dynamicBankGranularity?.length
9961033
? pluginConfig.dynamicBankGranularity
997-
: ["agent", "channel", "user"];
1034+
: DEFAULT_DYNAMIC_BANK_GRANULARITY;
9981035

9991036
// Validate field names at runtime — typos silently produce 'unknown' segments
10001037
const validFields = new Set(["agent", "channel", "user", "provider"]);
@@ -1321,7 +1358,7 @@ function getPluginConfig(api: MoltbotPluginAPI): PluginConfig {
13211358
autoRecall: config.autoRecall !== false, // Default: true (on) — backward compatible
13221359
dynamicBankGranularity: Array.isArray(config.dynamicBankGranularity)
13231360
? config.dynamicBankGranularity
1324-
: undefined,
1361+
: DEFAULT_DYNAMIC_BANK_GRANULARITY,
13251362
autoRetain: config.autoRetain !== false, // Default: true
13261363
retainRoles: Array.isArray(config.retainRoles) ? config.retainRoles : undefined,
13271364
retainFormat: config.retainFormat === "text" ? "text" : "json",
@@ -1802,6 +1839,7 @@ export default function (api: MoltbotPluginAPI) {
18021839
debug(
18031840
`[Hindsight] before_dispatch marked session ${sessionKey} to skip this turn: ${formatIdentitySkipReason(skipReason)}`
18041841
);
1842+
logSkipOnce("dispatch", sessionKey, skipReason);
18051843
return;
18061844
}
18071845
if (!resolvedCtx?.senderId || typeof resolvedCtx.senderId !== "string") {
@@ -1830,6 +1868,7 @@ export default function (api: MoltbotPluginAPI) {
18301868
debug(
18311869
`[Hindsight] before_agent_start skipping session ${sessionKey}: ${formatIdentitySkipReason(skipReason)}`
18321870
);
1871+
logSkipOnce("agent_start", sessionKey, skipReason);
18331872
return;
18341873
}
18351874
if (!resolvedCtx) {
@@ -1895,6 +1934,7 @@ export default function (api: MoltbotPluginAPI) {
18951934
debug(
18961935
`[Hindsight] Skipping recall for session ${sessionKeyForCache}: ${formatIdentitySkipReason(skipTurnReason)}`
18971936
);
1937+
logSkipOnce("recall", sessionKeyForCache, skipTurnReason);
18981938
return;
18991939
}
19001940

@@ -1912,6 +1952,7 @@ export default function (api: MoltbotPluginAPI) {
19121952
debug(
19131953
`[Hindsight] Skipping recall for session ${sessionKeyForCache}: ${formatIdentitySkipReason(identitySkipReason)}`
19141954
);
1955+
logSkipOnce("recall", sessionKeyForCache, identitySkipReason);
19151956
return;
19161957
}
19171958

@@ -2124,6 +2165,7 @@ ${memoriesFormatted}
21242165
debug(
21252166
`[Hindsight Hook] Skipping retain for session ${sessionKeyForLookup}: ${formatIdentitySkipReason(skipTurnReason)}`
21262167
);
2168+
logSkipOnce("retain", sessionKeyForLookup, skipTurnReason);
21272169
if (sessionKeyForLookup) {
21282170
skipHindsightTurnBySession.delete(sessionKeyForLookup);
21292171
}
@@ -2144,6 +2186,7 @@ ${memoriesFormatted}
21442186
debug(
21452187
`[Hindsight Hook] Skipping retain for session ${sessionKeyForLookup}: ${formatIdentitySkipReason(identitySkipReason)}`
21462188
);
2189+
logSkipOnce("retain", sessionKeyForLookup, identitySkipReason);
21472190
if (sessionKeyForLookup) {
21482191
skipHindsightTurnBySession.delete(sessionKeyForLookup);
21492192
}

0 commit comments

Comments
 (0)