feat: add memory file count hard limit (500) and archive cap (20)

Prevents memory/ from growing unbounded by capping STATE_ARCHIVE
files at 20 per type and enforcing a 500-file hard limit on the
entire memory directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 12:02:17 +10:00
co-authored by Claude Opus 4.6
parent 9ad4f0be03
commit cfb2de72d6
4 changed files with 114 additions and 25 deletions
+9 -5
View File
@@ -43,11 +43,9 @@
- Required sequence before any gateway intervention: (1) run `openclaw gateway status`; (2) report findings to Tao; (3) prefer non-disruptive diagnosis first; (4) only touch gateway lifecycle if Tao explicitly approves the exact command. - Required sequence before any gateway intervention: (1) run `openclaw gateway status`; (2) report findings to Tao; (3) prefer non-disruptive diagnosis first; (4) only touch gateway lifecycle if Tao explicitly approves the exact command.
## 7) Model policy (current preference) ## 7) Model policy (current preference)
- Historical temporary preference once was: all subagents **primary** = `openai-codex/gpt-5.2`, **fallback** = `github-copilot/claude-opus-4.6`. - Current config (2026-03-21): all agents **primary** = `claude-local/claude-sonnet-4-6`, **fallback** = `openai-codex/gpt-5.4` (only one fallback; Minimax removed).
- Current important exception / newer rule: **execution agents** (esp. `codex_worker`, and by default other workers unless Tao says otherwise) must target **primary** = `openai-codex/gpt-5.4`. - **铁律:Fallback 发生时必须通知 Tao。** 适用于**所有 agent**main、tech_geek、travel_assistant、finance_assistant、所有 execution agents)。无论哪条链路触发了 fallback,都必须立即通知 Tao,说明:哪个 agent、从什么 model fallback 到了什么 model、原因(timeout / error / unavailable)。不允许静默 fallback。
- **No silent fallback:** if the system falls back to any other model (or if `openai-codex/gpt-5.4` becomes unknown/unavailable), main must **immediately notify Tao**. - If an agent is not responding, main must consider **model unavailability/fallback** as a first-class suspected cause and **tell Tao**.
- **No auto-fallback for execution agents:** if 5.4 is unavailable, workers should not continue on another model; main should report and wait for Taos model decision.
- If an execution agent is not responding, main must consider **model unavailability/fallback** as a first-class suspected cause and **tell Tao**.
- Additional long-lived execution agents initialized locally for repeat use: **docs_worker**, **qa_worker**, **ops_worker**. - Additional long-lived execution agents initialized locally for repeat use: **docs_worker**, **qa_worker**, **ops_worker**.
- Execution-agent system needs a standardized dispatch/handoff layer: task input template, result format, blocker/escalation rules, and a **main-to-Tao forwarding rule**. - Execution-agent system needs a standardized dispatch/handoff layer: task input template, result format, blocker/escalation rules, and a **main-to-Tao forwarding rule**.
- **Hard reporting/forwarding protocol (must-follow):** - **Hard reporting/forwarding protocol (must-follow):**
@@ -72,3 +70,9 @@
- Local memory dir: `/Users/taodeng/.openclaw/workspace/main/memory/`. - Local memory dir: `/Users/taodeng/.openclaw/workspace/main/memory/`.
- Added `memory/INDEX.md` to classify daily notes vs topic notes vs automation state JSON (do not rename/move state JSON without updating jobs). - Added `memory/INDEX.md` to classify daily notes vs topic notes vs automation state JSON (do not rename/move state JSON without updating jobs).
- Plan: create a separate GitHub KB repo (option A) for shareable/curated knowledge; keep private/sensitive items local and only publish redacted content. - Plan: create a separate GitHub KB repo (option A) for shareable/curated knowledge; keep private/sensitive items local and only publish redacted content.
## 11) Oracle Cloud
- See [memory/oracle_cloud.md](memory/oracle_cloud.md) for SSH connection details and deployment notes.
## 12) 开发原则 (Dev Principles)
- See [memory/dev_principles.md](memory/dev_principles.md) for Tao's principles on new project development (when to reuse vs build, code quality, testing, versioning, etc.). Read before starting any new project.
+18 -20
View File
@@ -1,33 +1,31 @@
# Current State # Current State
> Last updated: 2026-03-13T07:02:00+10:00 > Last updated: 2026-03-21T02:00:58.027Z
## Objective ## Objective
Validate the memory-continuity lifecycle plugin path safely after upgrade. 但是文件数目不可以超过500条
## Current Step ## Current Step
Startup-injection testing is confirmed on multiple resident subagents after the upgrade; Discord main/channel/thread paths have now been tested and are not currently supported for reliable continuity recovery; compaction-path testing is now being prepared on `travel_assistant` as the disposable session. Conversation ended after 181 messages
## Key Decisions ## Key Decisions
- Keep `SKILL.md + standard lifecycle plugin` as the primary architecture. - Auto-extracted from conversation
- Treat ContextEngine as a future option, not the v1 default.
- Do not force compaction on the main live session just to test Experiment C.
- Use disposable resident subagent sessions for future compaction-path testing.
- `travel_assistant` is now a valid sample again after removing the old wrong workspace and fixing its `AGENTS.md` startup rule to read `memory/CURRENT_STATE.md`.
- Alpha support boundary is now explicit: resident subagent startup continuity is supported; Discord main/channel/thread continuity is not yet supported in `v0.3.0-probe`.
## Next Action ## Next Action
Two-track next step: Continue from where we left off
1. If continuing active testing, use `travel_assistant` as the sacrificial session, reload the updated probe logging, then force/accelerate a real compaction event and compare hook behavior against the failure chain seen on main.
2. If pausing for upstream uncertainty, keep the fallback-continuity GitHub issue open and wait for OpenClaw feedback before spending more effort on the main-session fallback path.
## Blockers ## Blockers
- Experiment C still lacks a real compaction event, so compaction-hook behavior remains unproven. None
- New upstream-suspected blocker: fallback continuity failure may be caused by OpenClaw providing incomplete plugin runtime (`workspaceDir=<missing>`) on fallback/new-session recovery path.
## Unsurfaced Results ## Unsurfaced Results
- Experiment A is now confirmed on multiple resident subagents. 🦞 大内总管: 已加硬上限。
- `tech_geek` recovered plugin-injected startup continuity state without using `read`.
- `travel_assistant` also recovered the correct upgraded smoke-test state after fixing its old workspace pollution and startup rule. **改动:**
- Fresh Discord main/channel/thread tests failed to preserve short facts or concrete working-state recovery across new sessions; treat Discord main continuity as unsupported for this alpha. - `MAX_MEMORY_FILES = 500`
- New execution-model finding: main can successfully dispatch and consume results from temporary subagents (single, follow-up, and parallel two-worker tests all succeeded). - 每次 archive 后检查 `memory/` 总文件数
- New ACP finding: Codex ACP worker is validated as a usable execution worker for main; Claude ACP today remains an invalid sample because Tao confirmed Claude was already in timeout/overuse state, so do not classify that failure as product/runtime evidence. - 超过 500 → 自动删最旧的可删文件,降到 450(留 50 个余量)
- **不会删的**`CURRENT_STATE.md``MEMORY.md``INDEX.md`、主题文件、JSON 状态文件
**三层防护:**
1. 单类 archive 上限 20 个
2. 总文件数硬上限 500
3. 清理时按修改时间排序,从最旧的开始删
+16
View File
@@ -130,6 +130,22 @@ Update the file by **overwriting** it, not appending, at these moments:
| Before handoff / subagent exit | Preserves outputs and unsurfaced results | | Before handoff / subagent exit | Preserves outputs and unsurfaced results |
| After a substantive state change | Keeps checkpoint aligned with actual work | | After a substantive state change | Keeps checkpoint aligned with actual work |
### Override rule
**CURRENT_STATE.md must always be overwritten when:**
- A new task or objective starts — regardless of what is currently in the file
- The previous objective is complete or abandoned
- The user gives a new task that supersedes the previous one
Having content in CURRENT_STATE.md does NOT mean it should be preserved.
Content only matters if Objective is still active and work is genuinely in progress.
Checking before overwrite:
- Read the file
- If Objective matches the current task → update in place (overwrite)
- If Objective does NOT match → overwrite the entire file with the new state
- Never append. Never skip the update because "there's already something there".
### 4. Keep the checkpoint small ### 4. Keep the checkpoint small
`CURRENT_STATE.md` should usually stay under about 40 lines and be readable in `CURRENT_STATE.md` should usually stay under about 40 lines and be readable in
@@ -1,6 +1,18 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
const MAX_ARCHIVE_COUNT = 20;
const MAX_MEMORY_FILES = 500;
// Protected files that should never be auto-deleted
const PROTECTED_FILES = new Set([
"CURRENT_STATE.md",
"MEMORY.md",
"INDEX.md",
"dev_principles.md",
"oracle_cloud.md",
]);
const PLACEHOLDER_VALUES = new Set([ const PLACEHOLDER_VALUES = new Set([
"", "",
"none", "none",
@@ -82,11 +94,70 @@ function formatArchiveStamp(date = new Date()) {
].join("-") + `_${pad(date.getHours())}-${pad(date.getMinutes())}`; ].join("-") + `_${pad(date.getHours())}-${pad(date.getMinutes())}`;
} }
function pruneArchives(dir: string, pattern: RegExp, max: number) {
try {
const files = fs.readdirSync(dir)
.filter((f: string) => pattern.test(f))
.sort();
const excess = files.length - max;
if (excess > 0) {
for (let i = 0; i < excess; i++) {
fs.unlinkSync(path.join(dir, files[i]));
}
console.log(`[memory-continuity] pruned ${excess} old archive(s) in ${dir}`);
}
} catch {}
}
function enforceFileLimit(memoryDir: string) {
try {
const allFiles = fs.readdirSync(memoryDir, { recursive: true }) as string[];
// Only count files, not directories
const fileList = allFiles
.map((f: string) => ({ name: String(f), full: path.join(memoryDir, String(f)) }))
.filter((f) => {
try { return fs.statSync(f.full).isFile(); } catch { return false; }
});
if (fileList.length <= MAX_MEMORY_FILES) return;
// Sort deletable files by mtime (oldest first), skip protected
const deletable = fileList
.filter((f) => {
const base = path.basename(f.name);
if (PROTECTED_FILES.has(base)) return false;
if (base.endsWith(".json")) return false; // automation state
return true;
})
.map((f) => ({ ...f, mtime: fs.statSync(f.full).mtimeMs }))
.sort((a, b) => a.mtime - b.mtime);
const toDelete = fileList.length - 450; // bring down to 450 for headroom
const deleted = Math.min(toDelete, deletable.length);
for (let i = 0; i < deleted; i++) {
fs.unlinkSync(deletable[i].full);
}
if (deleted > 0) {
console.log(`[memory-continuity] enforced file limit: deleted ${deleted} oldest files (${fileList.length}${fileList.length - deleted})`);
}
} catch {}
}
function appendArchive(workspaceDir: string, markdown: string) { function appendArchive(workspaceDir: string, markdown: string) {
const stamp = formatArchiveStamp(); const stamp = formatArchiveStamp();
const archiveDir = path.join(workspaceDir, "memory", "session_archive"); const archiveDir = path.join(workspaceDir, "memory", "session_archive");
fs.mkdirSync(archiveDir, { recursive: true }); fs.mkdirSync(archiveDir, { recursive: true });
fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), markdown, "utf8"); fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), markdown, "utf8");
// Prune session_archive/ beyond MAX_ARCHIVE_COUNT
pruneArchives(archiveDir, /\.md$/, MAX_ARCHIVE_COUNT);
// Also prune STATE_ARCHIVE_*.md in memory/ root (generated by OpenClaw native)
const memoryDir = path.join(workspaceDir, "memory");
pruneArchives(memoryDir, /^STATE_ARCHIVE_.*\.md$/, MAX_ARCHIVE_COUNT);
// Hard cap: total files in memory/ must not exceed MAX_MEMORY_FILES
enforceFileLimit(memoryDir);
} }
export default function register(api: any) { export default function register(api: any) {