Include all credentials and runtime config
Remove secret exclusions from .gitignore (local-only repo). Add openclaw runtime state: credentials, identity, devices, hooks, telegram, secrets, agent configs. Exclude noisy/binary data: sessions, sqlite, media, temp files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: boot-md
|
||||
description: "Run BOOT.md on gateway startup"
|
||||
homepage: https://docs.openclaw.ai/automation/hooks#boot-md
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "🚀",
|
||||
"events": ["gateway:startup"],
|
||||
"requires": { "config": ["workspace.dir"] },
|
||||
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Boot Checklist Hook
|
||||
|
||||
Runs `BOOT.md` at gateway startup for each configured agent scope, if the file exists in that
|
||||
agent's resolved workspace.
|
||||
@@ -0,0 +1,221 @@
|
||||
import { c as resolveAgentWorkspaceDir, r as listAgentIds } from "../../run-with-concurrency-Cuc1THN9.js";
|
||||
import "../../paths-hfkBoC7i.js";
|
||||
import { a as defaultRuntime, t as createSubsystemLogger } from "../../subsystem-C-Cf_MFK.js";
|
||||
import { B as resolveAgentIdFromSessionKey } from "../../workspace-CaW79EXh.js";
|
||||
import "../../logger-BW8uLq6f.js";
|
||||
import "../../model-selection-BU6wl1le.js";
|
||||
import "../../github-copilot-token-CQmATy5E.js";
|
||||
import { a as isGatewayStartupEvent } from "../../legacy-names-BAf61_0I.js";
|
||||
import "../../thinking-B5B36ffe.js";
|
||||
import { n as SILENT_REPLY_TOKEN } from "../../tokens-CT3nywWU.js";
|
||||
import { o as agentCommand, s as createDefaultDeps } from "../../pi-embedded-C6ITuRXf.js";
|
||||
import "../../plugins-BZr8LJrk.js";
|
||||
import "../../accounts-D4KOSoV2.js";
|
||||
import "../../send-BLQvMYTW.js";
|
||||
import "../../send-DyQ6zcob.js";
|
||||
import "../../deliver-ClGktCjk.js";
|
||||
import "../../diagnostic-B9sgiG77.js";
|
||||
import "../../accounts-cJqOTvBI.js";
|
||||
import "../../image-ops-D4vlUR_L.js";
|
||||
import "../../send-D4CMR9ev.js";
|
||||
import "../../pi-model-discovery--C0FuY_K.js";
|
||||
import { Dt as resolveAgentMainSessionKey, W as loadSessionStore, Y as updateSessionStore, kt as resolveMainSessionKey } from "../../pi-embedded-helpers-CkWXaNFn.js";
|
||||
import "../../chrome-u1QjWgKY.js";
|
||||
import "../../frontmatter-CZF6xkL3.js";
|
||||
import "../../skills-B24U0XQQ.js";
|
||||
import "../../path-alias-guards-CouH80Zp.js";
|
||||
import "../../redact-DSv8X-3F.js";
|
||||
import "../../errors-_LEe37ld.js";
|
||||
import "../../fs-safe-DOYVoR6M.js";
|
||||
import "../../proxy-env-BZseFuIl.js";
|
||||
import "../../store-BteyapSQ.js";
|
||||
import { s as resolveStorePath } from "../../paths-Co-u8IhA.js";
|
||||
import "../../tool-images-C0W994KU.js";
|
||||
import "../../image-fMgabouP.js";
|
||||
import "../../audio-transcription-runner-DfRfzdqH.js";
|
||||
import "../../fetch-JzejSI-7.js";
|
||||
import "../../fetch-guard-C3LWD6FT.js";
|
||||
import "../../api-key-rotation-CLI6TxVv.js";
|
||||
import "../../proxy-fetch-CbII9--S.js";
|
||||
import "../../ir-D_UJzvhu.js";
|
||||
import "../../render-7C7EDC8_.js";
|
||||
import "../../target-errors-C8xePsI5.js";
|
||||
import "../../commands-registry-DJWLO-6B.js";
|
||||
import "../../skill-commands-B6iXy7Nx.js";
|
||||
import "../../fetch-CONQGbzL.js";
|
||||
import "../../channel-activity-CVe33Aey.js";
|
||||
import "../../tables-DushlpuO.js";
|
||||
import "../../send-CHthYes-.js";
|
||||
import "../../outbound-attachment-3soL6fn0.js";
|
||||
import "../../send-DYCEGbmH.js";
|
||||
import "../../proxy-BzwL4n0W.js";
|
||||
import "../../manager-DS9FBMMG.js";
|
||||
import "../../query-expansion-DUWWrH-g.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
//#region src/gateway/boot.ts
|
||||
function generateBootSessionId() {
|
||||
return `boot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").replace("T", "_").replace("Z", "")}-${crypto.randomUUID().slice(0, 8)}`;
|
||||
}
|
||||
const log$1 = createSubsystemLogger("gateway/boot");
|
||||
const BOOT_FILENAME = "BOOT.md";
|
||||
function buildBootPrompt(content) {
|
||||
return [
|
||||
"You are running a boot check. Follow BOOT.md instructions exactly.",
|
||||
"",
|
||||
"BOOT.md:",
|
||||
content,
|
||||
"",
|
||||
"If BOOT.md asks you to send a message, use the message tool (action=send with channel + target).",
|
||||
"Use the `target` field (not `to`) for message tool destinations.",
|
||||
`After sending with the message tool, reply with ONLY: ${SILENT_REPLY_TOKEN}.`,
|
||||
`If nothing needs attention, reply with ONLY: ${SILENT_REPLY_TOKEN}.`
|
||||
].join("\n");
|
||||
}
|
||||
async function loadBootFile(workspaceDir) {
|
||||
const bootPath = path.join(workspaceDir, BOOT_FILENAME);
|
||||
try {
|
||||
const trimmed = (await fs.readFile(bootPath, "utf-8")).trim();
|
||||
if (!trimmed) return { status: "empty" };
|
||||
return {
|
||||
status: "ok",
|
||||
content: trimmed
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") return { status: "missing" };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
function snapshotMainSessionMapping(params) {
|
||||
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId });
|
||||
try {
|
||||
const entry = loadSessionStore(storePath, { skipCache: true })[params.sessionKey];
|
||||
if (!entry) return {
|
||||
storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
canRestore: true,
|
||||
hadEntry: false
|
||||
};
|
||||
return {
|
||||
storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
canRestore: true,
|
||||
hadEntry: true,
|
||||
entry: structuredClone(entry)
|
||||
};
|
||||
} catch (err) {
|
||||
log$1.debug("boot: could not snapshot main session mapping", {
|
||||
sessionKey: params.sessionKey,
|
||||
error: String(err)
|
||||
});
|
||||
return {
|
||||
storePath,
|
||||
sessionKey: params.sessionKey,
|
||||
canRestore: false,
|
||||
hadEntry: false
|
||||
};
|
||||
}
|
||||
}
|
||||
async function restoreMainSessionMapping(snapshot) {
|
||||
if (!snapshot.canRestore) return;
|
||||
try {
|
||||
await updateSessionStore(snapshot.storePath, (store) => {
|
||||
if (snapshot.hadEntry && snapshot.entry) {
|
||||
store[snapshot.sessionKey] = snapshot.entry;
|
||||
return;
|
||||
}
|
||||
delete store[snapshot.sessionKey];
|
||||
}, { activeSessionKey: snapshot.sessionKey });
|
||||
return;
|
||||
} catch (err) {
|
||||
return err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
async function runBootOnce(params) {
|
||||
const bootRuntime = {
|
||||
log: () => {},
|
||||
error: (message) => log$1.error(String(message)),
|
||||
exit: defaultRuntime.exit
|
||||
};
|
||||
let result;
|
||||
try {
|
||||
result = await loadBootFile(params.workspaceDir);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log$1.error(`boot: failed to read ${BOOT_FILENAME}: ${message}`);
|
||||
return {
|
||||
status: "failed",
|
||||
reason: message
|
||||
};
|
||||
}
|
||||
if (result.status === "missing" || result.status === "empty") return {
|
||||
status: "skipped",
|
||||
reason: result.status
|
||||
};
|
||||
const sessionKey = params.agentId ? resolveAgentMainSessionKey({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId
|
||||
}) : resolveMainSessionKey(params.cfg);
|
||||
const message = buildBootPrompt(result.content ?? "");
|
||||
const sessionId = generateBootSessionId();
|
||||
const mappingSnapshot = snapshotMainSessionMapping({
|
||||
cfg: params.cfg,
|
||||
sessionKey
|
||||
});
|
||||
let agentFailure;
|
||||
try {
|
||||
await agentCommand({
|
||||
message,
|
||||
sessionKey,
|
||||
sessionId,
|
||||
deliver: false,
|
||||
senderIsOwner: true
|
||||
}, bootRuntime, params.deps);
|
||||
} catch (err) {
|
||||
agentFailure = err instanceof Error ? err.message : String(err);
|
||||
log$1.error(`boot: agent run failed: ${agentFailure}`);
|
||||
}
|
||||
const mappingRestoreFailure = await restoreMainSessionMapping(mappingSnapshot);
|
||||
if (mappingRestoreFailure) log$1.error(`boot: failed to restore main session mapping: ${mappingRestoreFailure}`);
|
||||
if (!agentFailure && !mappingRestoreFailure) return { status: "ran" };
|
||||
return {
|
||||
status: "failed",
|
||||
reason: [agentFailure ? `agent run failed: ${agentFailure}` : void 0, mappingRestoreFailure ? `mapping restore failed: ${mappingRestoreFailure}` : void 0].filter((part) => Boolean(part)).join("; ")
|
||||
};
|
||||
}
|
||||
//#endregion
|
||||
//#region src/hooks/bundled/boot-md/handler.ts
|
||||
const log = createSubsystemLogger("hooks/boot-md");
|
||||
const runBootChecklist = async (event) => {
|
||||
if (!isGatewayStartupEvent(event)) return;
|
||||
if (!event.context.cfg) return;
|
||||
const cfg = event.context.cfg;
|
||||
const deps = event.context.deps ?? createDefaultDeps();
|
||||
const agentIds = listAgentIds(cfg);
|
||||
for (const agentId of agentIds) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const result = await runBootOnce({
|
||||
cfg,
|
||||
deps,
|
||||
workspaceDir,
|
||||
agentId
|
||||
});
|
||||
if (result.status === "failed") {
|
||||
log.warn("boot-md failed for agent startup run", {
|
||||
agentId,
|
||||
workspaceDir,
|
||||
reason: result.reason
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (result.status === "skipped") log.debug("boot-md skipped for agent startup run", {
|
||||
agentId,
|
||||
workspaceDir,
|
||||
reason: result.reason
|
||||
});
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { runBootChecklist as default };
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: bootstrap-extra-files
|
||||
description: "Inject additional workspace bootstrap files via glob/path patterns"
|
||||
homepage: https://docs.openclaw.ai/automation/hooks#bootstrap-extra-files
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "📎",
|
||||
"events": ["agent:bootstrap"],
|
||||
"requires": { "config": ["workspace.dir"] },
|
||||
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Bootstrap Extra Files Hook
|
||||
|
||||
Loads additional bootstrap files into `Project Context` during `agent:bootstrap`.
|
||||
|
||||
## Why
|
||||
|
||||
Use this when your workspace has multiple context roots (for example monorepos) and
|
||||
you want to include extra `AGENTS.md`/`TOOLS.md`-class files without changing the
|
||||
workspace root.
|
||||
|
||||
## Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"internal": {
|
||||
"enabled": true,
|
||||
"entries": {
|
||||
"bootstrap-extra-files": {
|
||||
"enabled": true,
|
||||
"paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
- `paths` (string[]): preferred list of glob/path patterns.
|
||||
- `patterns` (string[]): alias of `paths`.
|
||||
- `files` (string[]): alias of `paths`.
|
||||
|
||||
All paths are resolved from the workspace and must stay inside it (including realpath checks).
|
||||
Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`,
|
||||
`IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`, `memory.md`).
|
||||
@@ -0,0 +1,45 @@
|
||||
import "../../paths-hfkBoC7i.js";
|
||||
import { t as createSubsystemLogger } from "../../subsystem-C-Cf_MFK.js";
|
||||
import { d as loadExtraBootstrapFilesWithDiagnostics, u as filterBootstrapFilesForSession } from "../../workspace-CaW79EXh.js";
|
||||
import "../../logger-BW8uLq6f.js";
|
||||
import { i as isAgentBootstrapEvent } from "../../legacy-names-BAf61_0I.js";
|
||||
import "../../frontmatter-CZF6xkL3.js";
|
||||
import { t as resolveHookConfig } from "../../config-Bs6iYHRw.js";
|
||||
//#region src/hooks/bundled/bootstrap-extra-files/handler.ts
|
||||
const HOOK_KEY = "bootstrap-extra-files";
|
||||
const log = createSubsystemLogger("bootstrap-extra-files");
|
||||
function normalizeStringArray(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((v) => typeof v === "string" ? v.trim() : "").filter(Boolean);
|
||||
}
|
||||
function resolveExtraBootstrapPatterns(hookConfig) {
|
||||
const fromPaths = normalizeStringArray(hookConfig.paths);
|
||||
if (fromPaths.length > 0) return fromPaths;
|
||||
const fromPatterns = normalizeStringArray(hookConfig.patterns);
|
||||
if (fromPatterns.length > 0) return fromPatterns;
|
||||
return normalizeStringArray(hookConfig.files);
|
||||
}
|
||||
const bootstrapExtraFilesHook = async (event) => {
|
||||
if (!isAgentBootstrapEvent(event)) return;
|
||||
const context = event.context;
|
||||
const hookConfig = resolveHookConfig(context.cfg, HOOK_KEY);
|
||||
if (!hookConfig || hookConfig.enabled === false) return;
|
||||
const patterns = resolveExtraBootstrapPatterns(hookConfig);
|
||||
if (patterns.length === 0) return;
|
||||
try {
|
||||
const { files: extras, diagnostics } = await loadExtraBootstrapFilesWithDiagnostics(context.workspaceDir, patterns);
|
||||
if (diagnostics.length > 0) log.debug("skipped extra bootstrap candidates", {
|
||||
skipped: diagnostics.length,
|
||||
reasons: diagnostics.reduce((counts, item) => {
|
||||
counts[item.reason] = (counts[item.reason] ?? 0) + 1;
|
||||
return counts;
|
||||
}, {})
|
||||
});
|
||||
if (extras.length === 0) return;
|
||||
context.bootstrapFiles = filterBootstrapFilesForSession([...context.bootstrapFiles, ...extras], context.sessionKey);
|
||||
} catch (err) {
|
||||
log.warn(`failed: ${String(err)}`);
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { bootstrapExtraFilesHook as default };
|
||||
Executable
+122
@@ -0,0 +1,122 @@
|
||||
---
|
||||
name: command-logger
|
||||
description: "Log all command events to a centralized audit file"
|
||||
homepage: https://docs.openclaw.ai/automation/hooks#command-logger
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "📝",
|
||||
"events": ["command"],
|
||||
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Command Logger Hook
|
||||
|
||||
Logs all command events (`/new`, `/reset`, `/stop`, etc.) to a centralized audit log file for debugging and monitoring purposes.
|
||||
|
||||
## What It Does
|
||||
|
||||
Every time you issue a command to the agent:
|
||||
|
||||
1. **Captures event details** - Command action, timestamp, session key, sender ID, source
|
||||
2. **Appends to log file** - Writes a JSON line to `~/.openclaw/logs/commands.log`
|
||||
3. **Silent operation** - Runs in the background without user notifications
|
||||
|
||||
## Output Format
|
||||
|
||||
Log entries are written in JSONL (JSON Lines) format:
|
||||
|
||||
```json
|
||||
{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"}
|
||||
{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"user@example.com","source":"whatsapp"}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Debugging**: Track when commands were issued and from which source
|
||||
- **Auditing**: Monitor command usage across different channels
|
||||
- **Analytics**: Analyze command patterns and frequency
|
||||
- **Troubleshooting**: Investigate issues by reviewing command history
|
||||
|
||||
## Log File Location
|
||||
|
||||
`~/.openclaw/logs/commands.log`
|
||||
|
||||
## Requirements
|
||||
|
||||
No requirements - this hook works out of the box on all platforms.
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration needed. The hook automatically:
|
||||
|
||||
- Creates the log directory if it doesn't exist
|
||||
- Appends to the log file (doesn't overwrite)
|
||||
- Handles errors silently without disrupting command execution
|
||||
|
||||
## Disabling
|
||||
|
||||
To disable this hook:
|
||||
|
||||
```bash
|
||||
openclaw hooks disable command-logger
|
||||
```
|
||||
|
||||
Or via config:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"internal": {
|
||||
"entries": {
|
||||
"command-logger": { "enabled": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Log Rotation
|
||||
|
||||
The hook does not automatically rotate logs. To manage log size, you can:
|
||||
|
||||
1. **Manual rotation**:
|
||||
|
||||
```bash
|
||||
mv ~/.openclaw/logs/commands.log ~/.openclaw/logs/commands.log.old
|
||||
```
|
||||
|
||||
2. **Use logrotate** (Linux):
|
||||
Create `/etc/logrotate.d/openclaw`:
|
||||
```
|
||||
/home/username/.openclaw/logs/commands.log {
|
||||
weekly
|
||||
rotate 4
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
}
|
||||
```
|
||||
|
||||
## Viewing Logs
|
||||
|
||||
View recent commands:
|
||||
|
||||
```bash
|
||||
tail -n 20 ~/.openclaw/logs/commands.log
|
||||
```
|
||||
|
||||
Pretty-print with jq:
|
||||
|
||||
```bash
|
||||
cat ~/.openclaw/logs/commands.log | jq .
|
||||
```
|
||||
|
||||
Filter by action:
|
||||
|
||||
```bash
|
||||
grep '"action":"new"' ~/.openclaw/logs/commands.log | jq .
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
import { c as resolveStateDir } from "../../paths-hfkBoC7i.js";
|
||||
import { t as createSubsystemLogger } from "../../subsystem-C-Cf_MFK.js";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
//#region src/hooks/bundled/command-logger/handler.ts
|
||||
/**
|
||||
* Example hook handler: Log all commands to a file
|
||||
*
|
||||
* This handler demonstrates how to create a hook that logs all command events
|
||||
* to a centralized log file for audit/debugging purposes.
|
||||
*
|
||||
* To enable this handler, add it to your config:
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "hooks": {
|
||||
* "internal": {
|
||||
* "enabled": true,
|
||||
* "handlers": [
|
||||
* {
|
||||
* "event": "command",
|
||||
* "module": "./hooks/handlers/command-logger.ts"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const log = createSubsystemLogger("command-logger");
|
||||
/**
|
||||
* Log all command events to a file
|
||||
*/
|
||||
const logCommand = async (event) => {
|
||||
if (event.type !== "command") return;
|
||||
try {
|
||||
const stateDir = resolveStateDir(process.env, os.homedir);
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
const logFile = path.join(logDir, "commands.log");
|
||||
const logLine = JSON.stringify({
|
||||
timestamp: event.timestamp.toISOString(),
|
||||
action: event.action,
|
||||
sessionKey: event.sessionKey,
|
||||
senderId: event.context.senderId ?? "unknown",
|
||||
source: event.context.commandSource ?? "unknown"
|
||||
}) + "\n";
|
||||
await fs.appendFile(logFile, logLine, "utf-8");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
log.error(`Failed to log command: ${message}`);
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { logCommand as default };
|
||||
+1
@@ -0,0 +1 @@
|
||||
/home/openclaw/.openclaw/workspace/hooks/model-skill-injector
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: session-memory
|
||||
description: "Save session context to memory when /new or /reset command is issued"
|
||||
homepage: https://docs.openclaw.ai/automation/hooks#session-memory
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "💾",
|
||||
"events": ["command:new", "command:reset"],
|
||||
"requires": { "config": ["workspace.dir"] },
|
||||
"install": [{ "id": "bundled", "kind": "bundled", "label": "Bundled with OpenClaw" }],
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Session Memory Hook
|
||||
|
||||
Automatically saves session context to your workspace memory when you issue `/new` or `/reset`.
|
||||
|
||||
## What It Does
|
||||
|
||||
When you run `/new` or `/reset` to start a fresh session:
|
||||
|
||||
1. **Finds the previous session** - Uses the pre-reset session entry to locate the correct transcript
|
||||
2. **Extracts conversation** - Reads the last N user/assistant messages from the session (default: 15, configurable)
|
||||
3. **Generates descriptive slug** - Uses LLM to create a meaningful filename slug based on conversation content
|
||||
4. **Saves to memory** - Creates a new file at `<workspace>/memory/YYYY-MM-DD-slug.md`
|
||||
5. **Sends confirmation** - Notifies you with the file path
|
||||
|
||||
## Output Format
|
||||
|
||||
Memory files are created with the following format:
|
||||
|
||||
```markdown
|
||||
# Session: 2026-01-16 14:30:00 UTC
|
||||
|
||||
- **Session Key**: agent:main:main
|
||||
- **Session ID**: abc123def456
|
||||
- **Source**: telegram
|
||||
```
|
||||
|
||||
## Filename Examples
|
||||
|
||||
The LLM generates descriptive slugs based on your conversation:
|
||||
|
||||
- `2026-01-16-vendor-pitch.md` - Discussion about vendor evaluation
|
||||
- `2026-01-16-api-design.md` - API architecture planning
|
||||
- `2026-01-16-bug-fix.md` - Debugging session
|
||||
- `2026-01-16-1430.md` - Fallback timestamp if slug generation fails
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Config**: `workspace.dir` must be set (automatically configured during onboarding)
|
||||
|
||||
The hook uses your configured LLM provider to generate slugs, so it works with any provider (Anthropic, OpenAI, etc.).
|
||||
|
||||
## Configuration
|
||||
|
||||
The hook supports optional configuration:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ---------- | ------ | ------- | --------------------------------------------------------------- |
|
||||
| `messages` | number | 15 | Number of user/assistant messages to include in the memory file |
|
||||
|
||||
Example configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"internal": {
|
||||
"entries": {
|
||||
"session-memory": {
|
||||
"enabled": true,
|
||||
"messages": 25
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The hook automatically:
|
||||
|
||||
- Uses your workspace directory (`~/.openclaw/workspace` by default)
|
||||
- Uses your configured LLM for slug generation
|
||||
- Falls back to timestamp slugs if LLM is unavailable
|
||||
|
||||
## Disabling
|
||||
|
||||
To disable this hook:
|
||||
|
||||
```bash
|
||||
openclaw hooks disable session-memory
|
||||
```
|
||||
|
||||
Or remove it from your config:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"internal": {
|
||||
"entries": {
|
||||
"session-memory": { "enabled": false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,238 @@
|
||||
import { c as resolveAgentWorkspaceDir } from "../../run-with-concurrency-Cuc1THN9.js";
|
||||
import { c as resolveStateDir } from "../../paths-hfkBoC7i.js";
|
||||
import { t as createSubsystemLogger } from "../../subsystem-C-Cf_MFK.js";
|
||||
import { B as resolveAgentIdFromSessionKey } from "../../workspace-CaW79EXh.js";
|
||||
import "../../logger-BW8uLq6f.js";
|
||||
import "../../model-selection-BU6wl1le.js";
|
||||
import "../../github-copilot-token-CQmATy5E.js";
|
||||
import "../../legacy-names-BAf61_0I.js";
|
||||
import "../../thinking-B5B36ffe.js";
|
||||
import "../../tokens-CT3nywWU.js";
|
||||
import "../../pi-embedded-C6ITuRXf.js";
|
||||
import "../../plugins-BZr8LJrk.js";
|
||||
import "../../accounts-D4KOSoV2.js";
|
||||
import "../../send-BLQvMYTW.js";
|
||||
import "../../send-DyQ6zcob.js";
|
||||
import "../../deliver-ClGktCjk.js";
|
||||
import "../../diagnostic-B9sgiG77.js";
|
||||
import "../../accounts-cJqOTvBI.js";
|
||||
import "../../image-ops-D4vlUR_L.js";
|
||||
import "../../send-D4CMR9ev.js";
|
||||
import "../../pi-model-discovery--C0FuY_K.js";
|
||||
import { pt as hasInterSessionUserProvenance } from "../../pi-embedded-helpers-CkWXaNFn.js";
|
||||
import "../../chrome-u1QjWgKY.js";
|
||||
import "../../frontmatter-CZF6xkL3.js";
|
||||
import "../../skills-B24U0XQQ.js";
|
||||
import "../../path-alias-guards-CouH80Zp.js";
|
||||
import "../../redact-DSv8X-3F.js";
|
||||
import "../../errors-_LEe37ld.js";
|
||||
import { c as writeFileWithinRoot } from "../../fs-safe-DOYVoR6M.js";
|
||||
import "../../proxy-env-BZseFuIl.js";
|
||||
import "../../store-BteyapSQ.js";
|
||||
import "../../paths-Co-u8IhA.js";
|
||||
import "../../tool-images-C0W994KU.js";
|
||||
import "../../image-fMgabouP.js";
|
||||
import "../../audio-transcription-runner-DfRfzdqH.js";
|
||||
import "../../fetch-JzejSI-7.js";
|
||||
import "../../fetch-guard-C3LWD6FT.js";
|
||||
import "../../api-key-rotation-CLI6TxVv.js";
|
||||
import "../../proxy-fetch-CbII9--S.js";
|
||||
import "../../ir-D_UJzvhu.js";
|
||||
import "../../render-7C7EDC8_.js";
|
||||
import "../../target-errors-C8xePsI5.js";
|
||||
import "../../commands-registry-DJWLO-6B.js";
|
||||
import "../../skill-commands-B6iXy7Nx.js";
|
||||
import "../../fetch-CONQGbzL.js";
|
||||
import "../../channel-activity-CVe33Aey.js";
|
||||
import "../../tables-DushlpuO.js";
|
||||
import "../../send-CHthYes-.js";
|
||||
import "../../outbound-attachment-3soL6fn0.js";
|
||||
import "../../send-DYCEGbmH.js";
|
||||
import "../../proxy-BzwL4n0W.js";
|
||||
import "../../manager-DS9FBMMG.js";
|
||||
import "../../query-expansion-DUWWrH-g.js";
|
||||
import { generateSlugViaLLM } from "../../llm-slug-generator.js";
|
||||
import { t as resolveHookConfig } from "../../config-Bs6iYHRw.js";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
//#region src/hooks/bundled/session-memory/handler.ts
|
||||
/**
|
||||
* Session memory hook handler
|
||||
*
|
||||
* Saves session context to memory when /new or /reset command is triggered
|
||||
* Creates a new dated memory file with LLM-generated slug
|
||||
*/
|
||||
const log = createSubsystemLogger("hooks/session-memory");
|
||||
/**
|
||||
* Read recent messages from session file for slug generation
|
||||
*/
|
||||
async function getRecentSessionContent(sessionFilePath, messageCount = 15) {
|
||||
try {
|
||||
const lines = (await fs.readFile(sessionFilePath, "utf-8")).trim().split("\n");
|
||||
const allMessages = [];
|
||||
for (const line of lines) try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.type === "message" && entry.message) {
|
||||
const msg = entry.message;
|
||||
const role = msg.role;
|
||||
if ((role === "user" || role === "assistant") && msg.content) {
|
||||
if (role === "user" && hasInterSessionUserProvenance(msg)) continue;
|
||||
const text = Array.isArray(msg.content) ? msg.content.find((c) => c.type === "text")?.text : msg.content;
|
||||
if (text && !text.startsWith("/")) allMessages.push(`${role}: ${text}`);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return allMessages.slice(-messageCount).join("\n");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Try the active transcript first; if /new already rotated it,
|
||||
* fallback to the latest .jsonl.reset.* sibling.
|
||||
*/
|
||||
async function getRecentSessionContentWithResetFallback(sessionFilePath, messageCount = 15) {
|
||||
const primary = await getRecentSessionContent(sessionFilePath, messageCount);
|
||||
if (primary) return primary;
|
||||
try {
|
||||
const dir = path.dirname(sessionFilePath);
|
||||
const resetPrefix = `${path.basename(sessionFilePath)}.reset.`;
|
||||
const resetCandidates = (await fs.readdir(dir)).filter((name) => name.startsWith(resetPrefix)).toSorted();
|
||||
if (resetCandidates.length === 0) return primary;
|
||||
const latestResetPath = path.join(dir, resetCandidates[resetCandidates.length - 1]);
|
||||
const fallback = await getRecentSessionContent(latestResetPath, messageCount);
|
||||
if (fallback) log.debug("Loaded session content from reset fallback", {
|
||||
sessionFilePath,
|
||||
latestResetPath
|
||||
});
|
||||
return fallback || primary;
|
||||
} catch {
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
function stripResetSuffix(fileName) {
|
||||
const resetIndex = fileName.indexOf(".reset.");
|
||||
return resetIndex === -1 ? fileName : fileName.slice(0, resetIndex);
|
||||
}
|
||||
async function findPreviousSessionFile(params) {
|
||||
try {
|
||||
const files = await fs.readdir(params.sessionsDir);
|
||||
const fileSet = new Set(files);
|
||||
const baseFromReset = params.currentSessionFile ? stripResetSuffix(path.basename(params.currentSessionFile)) : void 0;
|
||||
if (baseFromReset && fileSet.has(baseFromReset)) return path.join(params.sessionsDir, baseFromReset);
|
||||
const trimmedSessionId = params.sessionId?.trim();
|
||||
if (trimmedSessionId) {
|
||||
const canonicalFile = `${trimmedSessionId}.jsonl`;
|
||||
if (fileSet.has(canonicalFile)) return path.join(params.sessionsDir, canonicalFile);
|
||||
const topicVariants = files.filter((name) => name.startsWith(`${trimmedSessionId}-topic-`) && name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed();
|
||||
if (topicVariants.length > 0) return path.join(params.sessionsDir, topicVariants[0]);
|
||||
}
|
||||
if (!params.currentSessionFile) return;
|
||||
const nonResetJsonl = files.filter((name) => name.endsWith(".jsonl") && !name.includes(".reset.")).toSorted().toReversed();
|
||||
if (nonResetJsonl.length > 0) return path.join(params.sessionsDir, nonResetJsonl[0]);
|
||||
} catch {}
|
||||
}
|
||||
/**
|
||||
* Save session context to memory when /new or /reset command is triggered
|
||||
*/
|
||||
const saveSessionToMemory = async (event) => {
|
||||
const isResetCommand = event.action === "new" || event.action === "reset";
|
||||
if (event.type !== "command" || !isResetCommand) return;
|
||||
try {
|
||||
log.debug("Hook triggered for reset/new command", { action: event.action });
|
||||
const context = event.context || {};
|
||||
const cfg = context.cfg;
|
||||
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
|
||||
const workspaceDir = cfg ? resolveAgentWorkspaceDir(cfg, agentId) : path.join(resolveStateDir(process.env, os.homedir), "workspace");
|
||||
const memoryDir = path.join(workspaceDir, "memory");
|
||||
await fs.mkdir(memoryDir, { recursive: true });
|
||||
const now = new Date(event.timestamp);
|
||||
const dateStr = now.toISOString().split("T")[0];
|
||||
const sessionEntry = context.previousSessionEntry || context.sessionEntry || {};
|
||||
const currentSessionId = sessionEntry.sessionId;
|
||||
let currentSessionFile = sessionEntry.sessionFile || void 0;
|
||||
if (!currentSessionFile || currentSessionFile.includes(".reset.")) {
|
||||
const sessionsDirs = /* @__PURE__ */ new Set();
|
||||
if (currentSessionFile) sessionsDirs.add(path.dirname(currentSessionFile));
|
||||
sessionsDirs.add(path.join(workspaceDir, "sessions"));
|
||||
for (const sessionsDir of sessionsDirs) {
|
||||
const recoveredSessionFile = await findPreviousSessionFile({
|
||||
sessionsDir,
|
||||
currentSessionFile,
|
||||
sessionId: currentSessionId
|
||||
});
|
||||
if (!recoveredSessionFile) continue;
|
||||
currentSessionFile = recoveredSessionFile;
|
||||
log.debug("Found previous session file", { file: currentSessionFile });
|
||||
break;
|
||||
}
|
||||
}
|
||||
log.debug("Session context resolved", {
|
||||
sessionId: currentSessionId,
|
||||
sessionFile: currentSessionFile,
|
||||
hasCfg: Boolean(cfg)
|
||||
});
|
||||
const sessionFile = currentSessionFile || void 0;
|
||||
const hookConfig = resolveHookConfig(cfg, "session-memory");
|
||||
const messageCount = typeof hookConfig?.messages === "number" && hookConfig.messages > 0 ? hookConfig.messages : 15;
|
||||
let slug = null;
|
||||
let sessionContent = null;
|
||||
if (sessionFile) {
|
||||
sessionContent = await getRecentSessionContentWithResetFallback(sessionFile, messageCount);
|
||||
log.debug("Session content loaded", {
|
||||
length: sessionContent?.length ?? 0,
|
||||
messageCount
|
||||
});
|
||||
const allowLlmSlug = !(process.env.OPENCLAW_TEST_FAST === "1" || process.env.VITEST === "true" || process.env.VITEST === "1" || false) && hookConfig?.llmSlug !== false;
|
||||
if (sessionContent && cfg && allowLlmSlug) {
|
||||
log.debug("Calling generateSlugViaLLM...");
|
||||
slug = await generateSlugViaLLM({
|
||||
sessionContent,
|
||||
cfg
|
||||
});
|
||||
log.debug("Generated slug", { slug });
|
||||
}
|
||||
}
|
||||
if (!slug) {
|
||||
slug = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, "").slice(0, 4);
|
||||
log.debug("Using fallback timestamp slug", { slug });
|
||||
}
|
||||
const filename = `${dateStr}-${slug}.md`;
|
||||
const memoryFilePath = path.join(memoryDir, filename);
|
||||
log.debug("Memory file path resolved", {
|
||||
filename,
|
||||
path: memoryFilePath.replace(os.homedir(), "~")
|
||||
});
|
||||
const timeStr = now.toISOString().split("T")[1].split(".")[0];
|
||||
const sessionId = sessionEntry.sessionId || "unknown";
|
||||
const source = context.commandSource || "unknown";
|
||||
const entryParts = [
|
||||
`# Session: ${dateStr} ${timeStr} UTC`,
|
||||
"",
|
||||
`- **Session Key**: ${event.sessionKey}`,
|
||||
`- **Session ID**: ${sessionId}`,
|
||||
`- **Source**: ${source}`,
|
||||
""
|
||||
];
|
||||
if (sessionContent) entryParts.push("## Conversation Summary", "", sessionContent, "");
|
||||
await writeFileWithinRoot({
|
||||
rootDir: memoryDir,
|
||||
relativePath: filename,
|
||||
data: entryParts.join("\n"),
|
||||
encoding: "utf-8"
|
||||
});
|
||||
log.debug("Memory file written successfully");
|
||||
const relPath = memoryFilePath.replace(os.homedir(), "~");
|
||||
log.info(`Session context saved to ${relPath}`);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) log.error("Failed to save session memory", {
|
||||
errorName: err.name,
|
||||
errorMessage: err.message,
|
||||
stack: err.stack
|
||||
});
|
||||
else log.error("Failed to save session memory", { error: String(err) });
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
export { saveSessionToMemory as default };
|
||||
Reference in New Issue
Block a user