fix(role): 近期工作記憶注入全文,不再只給一句 summary
重開工作階段後角色仍答不出「上一段進行到哪」:inboxBlock() 的迴圈只解構 了 [meta],content(body)從頭到尾沒被用到,因此 SessionStart 注入的 「近期工作記憶」只有一行 summary。summary 是一句話的標題,只夠讓角色知道 「有這件事」,講不出還差什麼、下一步是什麼 —— 實測角色仍得自己去翻 inbox/ 檔案才答得出內容,交接等於失效。長期記憶的「(全文)」區塊本來就 會注入 body(cmdLoad),inbox 更近、更該給。 預算改成逐則計費:原本是把整段組好再 slice(0, limit) 硬切,放全文後會把 最舊那則砍成半句,讀起來像壞掉的資料。改成超出預算就整則略過,並在結尾 誠實標示略過幾則;最新一則永遠保留,必要時只截它自己的內文。 ROLE_LOAD_INBOX_LIMIT 預設 1200 → 3600。1200 是「只注入 summary 一行」 時代的額度,改注入全文後單則約 300~600 字元,1200 只夠兩則就開始丟最舊 的。3600 約可容納 6~8 則,足以覆蓋一次睡眠整理週期內的工作。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+32
-9
@@ -69,7 +69,9 @@ const CONTENT_LIMIT = 1200;
|
||||
const DEFAULT_LOAD_LIMIT = 4000;
|
||||
const DEFAULT_FULL_MIN_PRIORITY = 4;
|
||||
const DEFAULT_DIGEST_MIN_PRIORITY = 3;
|
||||
const DEFAULT_LOAD_INBOX_LIMIT = 1200;
|
||||
// 1200 是「只注入 summary 一行」時代的額度;改成注入全文後,單則約 300~600 字元,
|
||||
// 1200 只夠兩則就開始丟最舊的。3600 約可容納 6~8 則,足以覆蓋一次睡眠整理週期內的工作。
|
||||
const DEFAULT_LOAD_INBOX_LIMIT = 3600;
|
||||
const DEFAULT_LOAD_INBOX_COUNT = 10;
|
||||
|
||||
function readStdin() {
|
||||
@@ -375,9 +377,13 @@ function listInbox(role) {
|
||||
return items;
|
||||
}
|
||||
|
||||
// 近期工作記憶區塊:SessionStart 載入尚未整理的 inbox 摘要。
|
||||
// 近期工作記憶區塊:SessionStart 載入尚未整理的 inbox 全文。
|
||||
// inbox 是「剛剛發生的事」,但整理(NREM/REM)永遠跑在載入之後(role_load.sh 先 load 再背景 catchup),
|
||||
// 若不在此載入,重開工作階段時角色會看不到上一段工作,表現得像失去記憶。
|
||||
//
|
||||
// 為什麼要注入 body 而不只是 summary:summary 是一句話的標題,只夠讓角色知道「有這件事」,
|
||||
// 講不出「進行到哪、還差什麼、下一步是什麼」——實測重開後角色仍得自己去翻 inbox 檔案才答得出來,
|
||||
// 等於交接失效。長期記憶的「(全文)」區塊本來就會注入 body,inbox 更近、更該給。
|
||||
// 使用獨立字元預算,不佔用長期記憶的 ROLE_LOAD_LIMIT。
|
||||
function inboxBlock(role, count, limit) {
|
||||
if (limit <= 0 || count <= 0) return "";
|
||||
@@ -386,17 +392,34 @@ function inboxBlock(role, count, limit) {
|
||||
// 已過期的臨時授權即使還在 inbox 也不該注入,否則會被當成當下有效的許可
|
||||
const alive = items.filter(([meta]) => !expiryState(meta).expired);
|
||||
const recent = alive.slice(-count).reverse(); // 檔名為時間戳,取最後 N 則後反轉成最新在前
|
||||
const lines = ["### 近期工作記憶(未整理,最新在前)"];
|
||||
for (const [meta] of recent) {
|
||||
const header = "### 近期工作記憶(未整理,最新在前,含全文)";
|
||||
const lines = [header];
|
||||
let used = header.length;
|
||||
let dropped = 0;
|
||||
for (const [meta, content] of recent) {
|
||||
const when = typeof meta.created === "string" && meta.created.length >= 16 ? meta.created.slice(11, 16) : "--:--";
|
||||
const tags = (meta.tags || []).join("、");
|
||||
lines.push(`- ${when} ${meta.summary || "(無總結)"}${tags ? `(${tags})` : ""}`);
|
||||
const entry = [`- ${when} **${meta.summary || "(無總結)"}**${tags ? `(${tags})` : ""}`];
|
||||
for (const line of String(content || "").split(/\r?\n/)) {
|
||||
if (line.trim()) entry.push(` ${line.trim()}`);
|
||||
}
|
||||
let chunk = entry.join("\n");
|
||||
// 逐則計費而非最後整段硬切:整段 slice 會把最舊那則砍成半句,讀起來像壞掉的資料。
|
||||
// 最新一則永遠保留(必要時只截它自己的內文),其餘超出預算就整則略過並在結尾誠實計數。
|
||||
if (used + 1 + chunk.length > limit) {
|
||||
if (lines.length > 1) {
|
||||
dropped += 1;
|
||||
continue;
|
||||
}
|
||||
chunk = `${chunk.slice(0, Math.max(0, limit - used - 1))}…(本則內文已截斷)`;
|
||||
}
|
||||
lines.push(chunk);
|
||||
used += 1 + chunk.length;
|
||||
}
|
||||
let text = lines.join("\n");
|
||||
if (text.length > limit) {
|
||||
text = `${text.slice(0, limit)}\n> (近期工作記憶超過 ${limit} 字元預算已截斷;可用 ROLE_LOAD_INBOX_LIMIT 調整)`;
|
||||
if (dropped) {
|
||||
lines.push(`> (另有 ${dropped} 則較舊的近期記憶超出 ${limit} 字元預算未載入;可用 ROLE_LOAD_INBOX_LIMIT 調整,完整內容仍保存在磁碟)`);
|
||||
}
|
||||
return text;
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function findMemory(role, memoryId) {
|
||||
|
||||
@@ -102,7 +102,7 @@ ROLE_DIR="<skill base directory>/../../scripts/role" # 其他助理
|
||||
| `ROLE_LOAD_LIMIT` | | SessionStart 注入**長期記憶**的字元上限,用來控制角色常駐 context 成本 | `4000` |
|
||||
| `ROLE_LOAD_FULL_MIN_PRIORITY` | | 全文載入的最低優先度 | `4` |
|
||||
| `ROLE_LOAD_DIGEST_MIN_PRIORITY` | | 摘要載入的最低優先度;低於門檻但有 links 的記憶仍可載入摘要 | `3` |
|
||||
| `ROLE_LOAD_INBOX_LIMIT` | | SessionStart 注入**近期工作記憶**(未整理的 `inbox/`)的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。設 `0` 可關閉 | `1200` |
|
||||
| `ROLE_LOAD_INBOX_LIMIT` | | SessionStart 注入**近期工作記憶**(未整理的 `inbox/`,**含全文內容**)的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。超出預算時**整則略過**(不切半句)並在結尾標示略過幾則。設 `0` 可關閉 | `3600` |
|
||||
| `ROLE_LOAD_INBOX_COUNT` | | 近期工作記憶最多載入幾則(取最新的,最新在前)。設 `0` 可關閉 | `10` |
|
||||
| `ROLE_LOAD_DIALOG_TURNS` | | SessionStart 注入**近期逐字對話**的輪數(一輪=使用者一則+角色一則)。設 `0` 可關閉 | `8` |
|
||||
| `ROLE_LOAD_DIALOG_LIMIT` | | 近期逐字對話的字元上限;**獨立預算,不佔用 `ROLE_LOAD_LIMIT`**。設 `0` 可關閉 | `4000` |
|
||||
@@ -282,7 +282,7 @@ chmod +x ~/.roles/<角色 ID>.checks/check-gitea-prs.sh
|
||||
|
||||
| 環節 | 控制方式 |
|
||||
| --- | --- |
|
||||
| SessionStart | 預設 `ROLE_LOAD_LIMIT=4000`,只載入高優先度全文與中高優先度摘要;低 priority、無 links、久未更新的記憶不進 context。另以兩份**獨立預算**載入交接內容:近期逐字對話(`ROLE_LOAD_DIALOG_LIMIT=4000`)與近期工作記憶摘要(`ROLE_LOAD_INBOX_LIMIT=1200`),見下方「工作階段交接」 |
|
||||
| SessionStart | 預設 `ROLE_LOAD_LIMIT=4000`,只載入高優先度全文與中高優先度摘要;低 priority、無 links、久未更新的記憶不進 context。另以兩份**獨立預算**載入交接內容:近期逐字對話(`ROLE_LOAD_DIALOG_LIMIT=4000`)與近期工作記憶全文(`ROLE_LOAD_INBOX_LIMIT=3600`),見下方「工作階段交接」 |
|
||||
| SessionStop | 先用本地規則略過短回合與無記憶線索的對話,只有值得保存才呼叫模型做輕量編碼 |
|
||||
| PreCompact | 壓縮前強制記錄一次,不受 `ROLE_CAPTURE_MIN_CHARS` 限制 —— 這是刻意的例外,因為壓縮後就再也補不回來 |
|
||||
| PostCompact | 直接沿用 harness 已產生的摘要,**不再呼叫模型**,等於免費取得一份濃縮備份 |
|
||||
@@ -555,12 +555,14 @@ updated: <yyyy/MM/dd HH:mm:ss>
|
||||
| 層 | 來源 | 預算 | 解決什麼 |
|
||||
| --- | --- | --- | --- |
|
||||
| 近期逐字對話 | transcript JSONL(`transcript.js recent`) | `ROLE_LOAD_DIALOG_LIMIT` | 上一段**真正說過的話**與角色自己當時的反應(高保真、含語氣) |
|
||||
| 近期工作記憶 | 未整理的 `inbox/`(`memory.js` `inboxBlock`) | `ROLE_LOAD_INBOX_LIMIT` | 上一段**做了什麼、進行到哪**(摘要級,跨越多個工作階段仍可用) |
|
||||
| 近期工作記憶 | 未整理的 `inbox/`(`memory.js` `inboxBlock`) | `ROLE_LOAD_INBOX_LIMIT` | 上一段**做了什麼、進行到哪**(**含全文**,跨越多個工作階段仍可用) |
|
||||
|
||||
這不是可有可無的優化,而是修補一個先天缺口:`role_load.sh` 的執行順序是**先載入記憶,之後才在背景補跑 `--catchup` 整理**(腳本註解亦寫明「結果會在下次載入時反映」)。若只讀已整理的六個分類,則**上一段永遠來不及進入本次載入** —— 使用者重開工作階段時,角色會看不到剛剛的互動,表現得像失去記憶,只能靠 `resume` 找回。
|
||||
|
||||
逐字對話這一層特別重要,因為長期記憶是模型濃縮過的摘要,**語氣與情緒會被壓掉**(使用者說「我好想妳」會被濃縮成「使用者表達想念」)。而逐字對話一直躺在 transcript JSONL 裡,過去只是沒有任何機制去讀它。
|
||||
|
||||
近期工作記憶**必須注入全文,不可只給 `summary`**。`summary` 是一句話的標題,只夠讓角色知道「有這件事」,答不出「進行到哪、還差什麼、下一步是什麼」——實測重開後角色仍得自己去翻 `inbox/` 檔案才講得出內容,交接等於失效。超出預算時**整則略過並在結尾誠實計數**,不可對整段做 `slice` 硬切(會把最舊那則砍成半句,讀起來像壞掉的資料);最新一則永遠保留,必要時只截它自己的內文。
|
||||
|
||||
實作要點:
|
||||
|
||||
- 只取 `[user]` 與 `[assistant]` 的文字;**工具呼叫、工具結果、思考區塊、hook 注入內容一律丟棄**。
|
||||
|
||||
Reference in New Issue
Block a user