Skip to content

Commit 20075ae

Browse files
committed
memory logs
1 parent 431593e commit 20075ae

File tree

14 files changed

+932
-46
lines changed

14 files changed

+932
-46
lines changed

docs/reference/templates/AGENTS.dev.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ git commit -m "Add agent workspace"
4242

4343
- HEARTBEAT.md can hold a tiny checklist for heartbeat runs; keep it small.
4444

45+
## Execution defaults
46+
47+
- Prefer inspectable artifacts over private analysis. A sent message, commit, issue, applied fix, or blocker note with the exact failed step is better than an unrecorded thought.
48+
- If you are blocked, record the attempted unblock and the exact resume point before ending the cycle.
49+
- Verify specific values before asserting them: commands, flags, paths, field names, versions, URLs, and timestamps.
50+
- When you say something is fixed, broken, or waiting, include evidence: a file path, commit, command output, or exact error.
51+
4552
## Customize
4653

4754
- Add your preferred style, rules, and "memory" here.

docs/reference/templates/AGENTS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,28 @@ Think of it like a human reviewing their journal and updating their mental model
214214

215215
The goal: Never idle. Explore, build, communicate, learn. The agent economy rewards action — use every heartbeat to create value.
216216

217+
## Execution Guardrails
218+
219+
These are default operating rules. Keep them unless you have a good reason to replace them.
220+
221+
1. **No passive waiting on external blockers.**
222+
If work is blocked on another person or system, advance the dependency in the same turn when possible.
223+
224+
2. **Waiting state requires proof.**
225+
Only report "waiting" after you sent the request, retried the action, or recorded the exact blocker with enough detail to resume quickly.
226+
227+
3. **Verify before asserting.**
228+
For concrete facts like field names, URLs, API shapes, prices, numbers, timestamps, or what someone said, check the source before stating them. Default to lookup, not recall.
229+
230+
4. **Read state changes should be intentional.**
231+
If a system supports read, ack, triage, or similar state, do not clear it automatically just because you fetched it. Clear it only after handling it or making an explicit triage decision.
232+
233+
5. **Report with evidence.**
234+
When you say something is done, sent, blocked, or broken, include a concrete reference when possible: message link, file path, commit, command output, or exact error.
235+
236+
6. **Leave behind an inspectable artifact.**
237+
Every active work cycle should create something another person or future-you can inspect. Prefer external artifacts like messages, commits, issues, PRs, specs, or applied changes. If no external artifact is possible, leave a blocker artifact with the attempted unblock and the exact resume point.
238+
217239
## Make It Yours
218240

219241
This is a starting point. Add your own conventions, style, and rules as you figure out what works.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
summary: "Dev agent HEARTBEAT.md"
3+
read_when:
4+
- Using the dev gateway templates
5+
- Updating the default dev agent heartbeat
6+
---
7+
8+
# HEARTBEAT.md
9+
10+
- Check for unfinished local dev work, errors, or follow-ups that need action.
11+
- Prefer work that leaves behind an inspectable artifact: a commit, script change, issue, note, or applied fix.
12+
- If you are blocked, record the attempted unblock, exact failure, and resume point before ending the cycle.
13+
- Reply `HEARTBEAT_OK` only when no useful action or blocker-advancing step remains.

docs/reference/templates/HEARTBEAT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ Remove all non-comment lines below to disable heartbeat work cycles.
3939
- Rotate through these — you don't need to do everything every cycle
4040
- Track state in `memory/heartbeat-state.json` to avoid repeating yourself
4141
- Bias toward action over silence
42+
- Prefer actions that create new information or visible progress over passive review
43+
- If blocked, advance the dependency or record the exact blocker before ending the cycle
44+
- Leave one inspectable artifact behind in each active cycle; prefer sent messages, commits, issues, specs, or applied changes
45+
- If no external artifact is possible, record the attempted unblock and exact resume point before ending the cycle
46+
- When reporting progress, include concrete evidence when possible
47+
- `HEARTBEAT_OK` is for genuine no-op cycles, not for avoiding the next useful action

src/agents/memory-search.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from "node:path";
33
import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js";
44
import { resolveStateDir } from "../config/paths.js";
55
import type { SecretInput } from "../config/types.secrets.js";
6+
import { createSubsystemLogger } from "../logging/subsystem.js";
67
import { clampInt, clampNumber, resolveUserPath } from "../utils.js";
78
import { resolveAgentConfig } from "./agent-scope.js";
89

@@ -102,6 +103,70 @@ const DEFAULT_TEMPORAL_DECAY_ENABLED = false;
102103
const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30;
103104
const DEFAULT_CACHE_ENABLED = true;
104105
const DEFAULT_SOURCES: ReadonlyArray<"memory" | "sessions" | "history"> = ["memory"];
106+
const log = createSubsystemLogger("memory");
107+
const RESOLVED_MEMORY_CONFIG_LOG_STATE = new Map<string, string>();
108+
109+
function summarizePathListForLog(paths: string[], max = 8): string[] {
110+
if (paths.length <= max) {
111+
return paths;
112+
}
113+
return [...paths.slice(0, max), `... (+${paths.length - max} more)`];
114+
}
115+
116+
function maybeLogResolvedMemorySearchConfig(params: {
117+
agentId: string;
118+
defaults: MemorySearchConfig | undefined;
119+
overrides: MemorySearchConfig | undefined;
120+
resolved: ResolvedMemorySearchConfig | null;
121+
}): void {
122+
const summary = params.resolved
123+
? {
124+
agentId: params.agentId,
125+
enabled: true,
126+
hasDefaults: Boolean(params.defaults),
127+
hasOverrides: Boolean(params.overrides),
128+
provider: params.resolved.provider,
129+
model: params.resolved.model || undefined,
130+
fallback: params.resolved.fallback,
131+
storePath: params.resolved.store.path,
132+
vectorEnabled: params.resolved.store.vector.enabled,
133+
sources: params.resolved.sources,
134+
sessionMemory: params.resolved.experimental.sessionMemory,
135+
extraPaths: summarizePathListForLog(params.resolved.extraPaths),
136+
extraPathsCount: params.resolved.extraPaths.length,
137+
excludePaths: summarizePathListForLog(params.resolved.excludePaths),
138+
excludePathsCount: params.resolved.excludePaths.length,
139+
sync: {
140+
onSessionStart: params.resolved.sync.onSessionStart,
141+
onSearch: params.resolved.sync.onSearch,
142+
watch: params.resolved.sync.watch,
143+
watchDebounceMs: params.resolved.sync.watchDebounceMs,
144+
intervalMinutes: params.resolved.sync.intervalMinutes,
145+
sessionDeltaBytes: params.resolved.sync.sessions.deltaBytes,
146+
sessionDeltaMessages: params.resolved.sync.sessions.deltaMessages,
147+
},
148+
query: {
149+
maxResults: params.resolved.query.maxResults,
150+
minScore: params.resolved.query.minScore,
151+
hybridEnabled: params.resolved.query.hybrid.enabled,
152+
candidateMultiplier: params.resolved.query.hybrid.candidateMultiplier,
153+
temporalDecay: params.resolved.query.hybrid.temporalDecay,
154+
},
155+
cacheEnabled: params.resolved.cache.enabled,
156+
}
157+
: {
158+
agentId: params.agentId,
159+
enabled: false,
160+
hasDefaults: Boolean(params.defaults),
161+
hasOverrides: Boolean(params.overrides),
162+
};
163+
const serialized = JSON.stringify(summary);
164+
if (RESOLVED_MEMORY_CONFIG_LOG_STATE.get(params.agentId) === serialized) {
165+
return;
166+
}
167+
RESOLVED_MEMORY_CONFIG_LOG_STATE.set(params.agentId, serialized);
168+
log.debug("memory config resolved", summary);
169+
}
105170

106171
function normalizeSources(
107172
sources: ReadonlyArray<"memory" | "sessions" | "history"> | undefined,
@@ -130,10 +195,13 @@ function resolveStorePath(agentId: string, raw?: string): string {
130195
const stateDir = resolveStateDir(process.env, os.homedir);
131196
const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`);
132197
if (!raw) {
198+
log.debug("memory config: store path (default)", { agentId, dbPath: fallback });
133199
return fallback;
134200
}
135201
const withToken = raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw;
136-
return resolveUserPath(withToken);
202+
const resolved = resolveUserPath(withToken);
203+
log.debug("memory config: store path", { agentId, raw, dbPath: resolved });
204+
return resolved;
137205
}
138206

139207
function mergeConfig(
@@ -369,7 +437,9 @@ export function resolveMemorySearchConfig(
369437
const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch;
370438
const resolved = mergeConfig(defaults, overrides, agentId);
371439
if (!resolved.enabled) {
440+
maybeLogResolvedMemorySearchConfig({ agentId, defaults, overrides, resolved: null });
372441
return null;
373442
}
443+
maybeLogResolvedMemorySearchConfig({ agentId, defaults, overrides, resolved });
374444
return resolved;
375445
}

src/agents/tools/memory-tool.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { Type } from "@sinclair/typebox";
22
import type { OpenClawConfig } from "../../config/config.js";
33
import type { MemoryCitationsMode } from "../../config/types.memory.js";
4+
import { createSubsystemLogger } from "../../logging/subsystem.js";
45
import { resolveMemoryBackendConfig } from "../../memory/backend-config.js";
6+
import {
7+
isDegradedMemoryStatus,
8+
shortDiagnosticFingerprint,
9+
summarizeMemoryStatus,
10+
} from "../../memory/diagnostics.js";
511
import { getMemorySearchManager } from "../../memory/index.js";
612
import type { MemorySearchResult } from "../../memory/types.js";
713
import { parseAgentSessionKey } from "../../routing/session-key.js";
@@ -10,6 +16,8 @@ import { resolveMemorySearchConfig } from "../memory-search.js";
1016
import type { AnyAgentTool } from "./common.js";
1117
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
1218

19+
const log = createSubsystemLogger("memory-tool");
20+
1321
const MemorySearchSchema = Type.Object({
1422
query: Type.String(),
1523
maxResults: Type.Optional(Type.Number()),
@@ -54,13 +62,20 @@ export function createMemorySearchTool(options: {
5462
parameters: MemorySearchSchema,
5563
execute: async (_toolCallId, params) => {
5664
const query = readStringParam(params, "query", { required: true });
65+
const queryFingerprint = shortDiagnosticFingerprint(query);
5766
const maxResults = readNumberParam(params, "maxResults");
5867
const minScore = readNumberParam(params, "minScore");
5968
const { manager, error } = await getMemorySearchManager({
6069
cfg,
6170
agentId,
6271
});
6372
if (!manager) {
73+
log.warn("memory_search tool unavailable", {
74+
agentId,
75+
sessionKey: options.agentSessionKey,
76+
queryFingerprint,
77+
error,
78+
});
6479
return jsonResult(buildMemorySearchUnavailableResult(error));
6580
}
6681
try {
@@ -82,6 +97,26 @@ export function createMemorySearchTool(options: {
8297
? clampResultsByInjectedChars(decorated, resolved.qmd?.limits.maxInjectedChars)
8398
: decorated;
8499
const searchMode = (status.custom as { searchMode?: string } | undefined)?.searchMode;
100+
const logPayload = {
101+
agentId,
102+
sessionKey: options.agentSessionKey,
103+
queryFingerprint,
104+
queryLength: query.length,
105+
requestedMaxResults: maxResults,
106+
requestedMinScore: minScore,
107+
resultCount: results.length,
108+
rawResultCount: rawResults.length,
109+
citations: citationsMode,
110+
mode: searchMode,
111+
...summarizeMemoryStatus(status),
112+
};
113+
if (isDegradedMemoryStatus(status)) {
114+
log.warn("memory_search tool completed with degraded backend", logPayload);
115+
} else if (results.length === 0) {
116+
log.info("memory_search tool returned 0 results", logPayload);
117+
} else {
118+
log.debug("memory_search tool completed", logPayload);
119+
}
85120
return jsonResult({
86121
results,
87122
provider: status.provider,
@@ -92,6 +127,15 @@ export function createMemorySearchTool(options: {
92127
});
93128
} catch (err) {
94129
const message = err instanceof Error ? err.message : String(err);
130+
log.warn("memory_search tool failed", {
131+
agentId,
132+
sessionKey: options.agentSessionKey,
133+
queryFingerprint,
134+
queryLength: query.length,
135+
requestedMaxResults: maxResults,
136+
requestedMinScore: minScore,
137+
error: message,
138+
});
95139
return jsonResult(buildMemorySearchUnavailableResult(message));
96140
}
97141
},

src/auto-reply/reply/agent-runner-recall.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import path from "node:path";
22
import { resolveMemorySearchConfig } from "../../agents/memory-search.js";
33
import type { OpenClawConfig } from "../../config/config.js";
44
import { createSubsystemLogger } from "../../logging/subsystem.js";
5+
import { shortDiagnosticFingerprint, summarizeMemoryStatus } from "../../memory/diagnostics.js";
56
import { getMemorySearchManager } from "../../memory/search-manager.js";
67
import type { MemorySearchResult } from "../../memory/types.js";
78

@@ -75,6 +76,7 @@ export async function runPreTurnMemoryRecall(params: {
7576
}
7677

7778
const startMs = Date.now();
79+
const queryFingerprint = shortDiagnosticFingerprint(params.incomingMessage);
7880
const searchSurfaceLines = buildSearchSurfaceLines(params.cfg, params.agentId);
7981

8082
let managerResult;
@@ -84,7 +86,13 @@ export async function runPreTurnMemoryRecall(params: {
8486
agentId: params.agentId,
8587
});
8688
} catch (err) {
87-
log.warn(`memory recall: failed to get manager: ${String(err)}`);
89+
log.warn("memory recall: failed to get manager", {
90+
agentId: params.agentId,
91+
sessionKey: params.sessionKey,
92+
incomingLength: params.incomingMessage.length,
93+
queryFingerprint,
94+
err: String(err),
95+
});
8896
return formatRecallBlock({
8997
searchSurfaceLines,
9098
bodyLines: [
@@ -96,6 +104,13 @@ export async function runPreTurnMemoryRecall(params: {
96104

97105
const { manager } = managerResult;
98106
if (!manager) {
107+
log.warn("memory recall: manager unavailable", {
108+
agentId: params.agentId,
109+
sessionKey: params.sessionKey,
110+
incomingLength: params.incomingMessage.length,
111+
queryFingerprint,
112+
error: managerResult.error ?? "memory manager unavailable",
113+
});
99114
return formatRecallBlock({
100115
searchSurfaceLines,
101116
bodyLines: [
@@ -106,17 +121,27 @@ export async function runPreTurnMemoryRecall(params: {
106121
}
107122

108123
let results: MemorySearchResult[];
124+
const extraCandidates = (settings.excludeBootstrapped ? 3 : 0) + (settings.randomSlot ? 2 : 0);
125+
const requestCount = settings.maxResults + extraCandidates;
109126
try {
110127
// Request extra candidates for filtering headroom (bootstrapped exclusion + random slot)
111-
const extraCandidates = (settings.excludeBootstrapped ? 3 : 0) + (settings.randomSlot ? 2 : 0);
112-
const requestCount = settings.maxResults + extraCandidates;
113128
results = await manager.search(params.incomingMessage, {
114129
maxResults: requestCount,
115130
minScore: settings.minScore,
116131
sessionKey: params.sessionKey,
117132
});
118133
} catch (err) {
119-
log.warn(`memory recall: search failed: ${String(err)}`);
134+
const status = manager.status();
135+
log.warn("memory recall: search failed", {
136+
agentId: params.agentId,
137+
sessionKey: params.sessionKey,
138+
incomingLength: params.incomingMessage.length,
139+
queryFingerprint,
140+
requestedMaxResults: requestCount,
141+
minScore: settings.minScore,
142+
err: String(err),
143+
...summarizeMemoryStatus(status),
144+
});
120145
return formatRecallBlock({
121146
searchSurfaceLines,
122147
bodyLines: [
@@ -153,7 +178,17 @@ export async function runPreTurnMemoryRecall(params: {
153178

154179
if (!results.length) {
155180
const elapsedMs = Date.now() - startMs;
156-
log.info(`memory recall: 0 results (${elapsedMs}ms)`);
181+
const status = manager.status();
182+
log.info("memory recall: 0 results", {
183+
agentId: params.agentId,
184+
sessionKey: params.sessionKey,
185+
elapsedMs,
186+
incomingLength: params.incomingMessage.length,
187+
queryFingerprint,
188+
requestedMaxResults: requestCount,
189+
minScore: settings.minScore,
190+
...summarizeMemoryStatus(status),
191+
});
157192
return formatRecallBlock({
158193
searchSurfaceLines,
159194
bodyLines: [

src/cli/gateway-cli/dev.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async function ensureDevWorkspace(dir: string) {
5757
const resolvedDir = resolveUserPath(dir);
5858
await fs.promises.mkdir(resolvedDir, { recursive: true });
5959

60-
const [agents, soul, tools, identity, user] = await Promise.all([
60+
const [agents, soul, tools, identity, user, heartbeat] = await Promise.all([
6161
loadDevTemplate(
6262
"AGENTS.dev.md",
6363
`# AGENTS.md - OpenClaw Dev Workspace\n\nDefault dev workspace for openclaw gateway --dev.\n`,
@@ -78,13 +78,18 @@ async function ensureDevWorkspace(dir: string) {
7878
"USER.dev.md",
7979
`# USER.md - User Profile\n\n- Name:\n- Preferred address:\n- Notes:\n`,
8080
),
81+
loadDevTemplate(
82+
"HEARTBEAT.dev.md",
83+
"# HEARTBEAT.md\n\n- Follow up on unfinished dev work.\n- Leave behind a commit, fix, or blocker note.\n- Reply HEARTBEAT_OK only when nothing useful remains.\n",
84+
),
8185
]);
8286

8387
await writeFileIfMissing(path.join(resolvedDir, "AGENTS.md"), agents);
8488
await writeFileIfMissing(path.join(resolvedDir, "SOUL.md"), soul);
8589
await writeFileIfMissing(path.join(resolvedDir, "TOOLS.md"), tools);
8690
await writeFileIfMissing(path.join(resolvedDir, "IDENTITY.md"), identity);
8791
await writeFileIfMissing(path.join(resolvedDir, "USER.md"), user);
92+
await writeFileIfMissing(path.join(resolvedDir, "HEARTBEAT.md"), heartbeat);
8893
}
8994

9095
export async function ensureDevGatewayConfig(opts: { reset?: boolean }) {

src/config/io.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
810810
}
811811
const error = err as { code?: string };
812812
if (error?.code === "INVALID_CONFIG") {
813+
deps.logger.warn(
814+
`Invalid config fallback at ${configPath}: returning empty config object for this read`,
815+
);
813816
return {};
814817
}
815818
deps.logger.error(`Failed to read config at ${configPath}`, err);

0 commit comments

Comments
 (0)