MINARA

工作區

基於 Markdown 的 Agent 身份與記憶,以及承載持久化對話內容的產物與文件存儲

Minara 將其身份、角色、精選記憶及會話間狀態,以純 Markdown 文件的形式存儲在工作區目錄(默認為 ~/.minara/workspace/)。

工作區是 Agent 的人工精選事實來源。當推斷出的用戶偏好、已學習的方法論或場景劇本與 MEMORY.md / SOUL.md / AGENTS.md 的內容衝突時,工作區內容優先。Agent 循環按該順序組裝系統提示詞;動態塊與緩存塊的組合方式詳見 Agent 循環

磁盤佈局與 OpenClaw 兼容:文件名、章節及 frontmatter 均符合 OpenClaw 的 AGENTS.default 規範,因此為任一工具編寫的工作區均可直接移植到另一工具(見下方 OpenClaw 兼容性)。

文件集

文件作用生命週期
SOUL.md身份、語氣、邊界長期保留;編輯後向用戶觸發一次性披露
AGENTS.md會話啟動規則、安全默認值長期保留
IDENTITY.md顯示名稱、Emoji、風格(UI 界面)長期保留
USER.md操作者檔案(時區、關注點、風險偏好、觀察列表)長期保留
MEMORY.md精選事實 / 偏好 / 決策長期保留;dreaming 任務追加 ## Dreamed YYYY-MM-DD 章節(命中 PII 過濾時改為隔離源日誌,不寫入 MEMORY.md)
BOOTSTRAP.md兩階段首次運行引導劇本Agent 輸出 BOOTSTRAP_DONE 後歸檔為 .done.<timestamp>
TOOLS.md環境專屬工具 / 技能說明長期保留;操作者維護
HEARTBEAT.md會話間備忘錄(last_seen、待處理循環、日程)Agent 在每輪結束時改寫狀態章節;用戶擁有 ## Schedule
memory/YYYY-MM-DD-<session>.md每次會話的日記每 N 輪追加一次;由 dreaming 任務合併

HEARTBEAT.md 是唯一由 Agent 在每輪結束時改寫的文件。其餘文件由操作者編輯(或僅由 dreaming 任務提議新增)。

啟動時自動初始化

createApp() 在加載工作區前會調用 seedWorkspaceIfMissing(workspaceDir),將 apps/agent/src/workspace/templates/ 中缺失的模板複製到運行時目錄。初始化操作是冪等的:操作者已編輯的文件(或此前已存在的文件)不會被覆蓋。新安裝無需手動運行 minara setup,即可獲得完整文件集。

HEARTBEAT.md 有意排除在可初始化集合之外。Agent 會在第一輪結束時寫入全新內容;若用過期狀態初始化,會對會話連續性造成誤導。模板文件僅作為 Web UI 的 restore-template 來源和規範參考,存放於 apps/agent/src/workspace/templates/HEARTBEAT.md

編輯工作區

有三種方式可編輯工作區文件:

  1. 直接在磁盤上編輯:在 Agent 會話間隙,用任意編輯器打開文件。下次會話將讀取保存的內容。
  2. REPL 斜槓命令:/soul/identity/heartbeat 以及 /workspace [soul|agents|identity|user|memory|heartbeat|bootstrap] 可打印當前文件內容。完整命令集見斜槓命令參考
  3. Web UI 設置 → 工作區:從白名單中選擇文件,在文本框中編輯後手動保存。保存操作通過網關進行,採用 sha256 樂觀併發控制:每次 PUT 攜帶編輯器加載時的 sha256 值;若網關返回 409,則彈出"覆蓋或重新加載"對話框,確保跨瀏覽器標籤頁或跨會話的併發編輯不會靜默覆蓋彼此的內容。

Web UI 編輯器不直接操作文件系統,所有操作均通過 /v1/workspace/files* 路由(見 HTTP API 參考)。

記憶的三個層次

工作區與 SQLite 記憶存儲協同工作(見記憶系統),分為三個時間維度:

  • 短期:每日日誌。每隔 WORKSPACE_DAILY_LOG_INTERVAL 輪,寫入器按 SQLite chat_turns 水位批量寫入全部未落盤 turn;日誌保留真實 session、surface 和來源,不再把 correlation ID 當 session ID。每次會話獨立成文件,可避免 REPL 與 HTTP 網關之間的追加競爭。
  • 長期:人工精選MEMORY.md 包含三個子章節:## Facts## Preferences## Decisions,由操作者維護。dreaming 任務以僅追加的方式提議新增 ## Dreamed YYYY-MM-DD 章節。當 LLM 輸出命中 PII 過濾時,源日誌改為隔離,MEMORY.md 不被改動(詳見 PII 隔離)。
  • 會話間備忘錄:心跳HEARTBEAT.md 記錄 last_seensession_idsurfaceturn_count、近期 open_loops 以及用戶所有的 ## Schedule。寫入器在每次寫入時完整保留 ## Schedule 章節(schedule_raw 往返同步),操作者註釋、空行分組和未知字段均不丟失。

Dreaming 合併

Dreaming 是 Agent 的"夜間圖書管理員"。自動來源 turn 仍保留在日誌中供審計,但在調用 LLM 前會被確定性移除;Autopilot、Strategy Studio、workflow、cron 或來源不明的 perps 執行不會被提升為人工偏好、決策或案例。其餘持久信號以 ## Dreamed YYYY-MM-DD 章節追加到 MEMORY.md

流程

每次 tick 按以下閘門順序執行;任一閘門提前返回則跳過其餘步驟:

  1. 搶鎖 <workspace>/.dreaming.lock(O_EXCL)。鎖被活進程持有時跳過本次 tick。
  2. 讀狀態:從 .dreamed-state.json 加載 mtime 水位線,從 .dreamed-quarantine.json 加載已隔離的源文件名集合。
  3. 選取:mtime 大於水位線、不在隔離集合內、在 windowDays 窗口內的每日日誌。按"最新優先連續後綴"挑選,使總字節數不超過 WORKSPACE_DREAM_TOTAL_INPUT_BYTES(默認 256 KB)。每份日誌在拼入提示詞前先尾部截斷到 64 KB。
  4. 讀取:當前 MEMORY.md(同樣尾部截斷到 64 KB),讓 LLM 在提示詞內對已有內容去重。
  5. 調用 LLM,使用 curator 提示詞:抽取持久信號而非噪聲、不重複已有事實、不要包含任何看似敏感的內容。
  6. 若返回(trim 後)嚴格等於 NOTHING_TO_PROMOTE,推進水位線後退出,下次 tick 不再將同一批日誌傳給 LLM。
  7. PII 閘門:將響應跑過密鑰 / 憑據正則集合。命中則將源日誌文件名追加到 .dreamed-quarantine.json,按 error 級別記錄日誌後退出。水位線推進(詳見 PII 隔離)。
  8. 追加 ## Dreamed YYYY-MM-DD 章節到 MEMORY.md(僅追加,操作者在它之上的編輯永遠不會被覆蓋)。
  9. 推進水位線並釋放鎖。

整個流程包在 try/finally 中,所有退出路徑(包括 LLM 報錯與 PII 隔離)均會釋放鎖。

狀態文件

工作區根目錄下三個 sibling JSON 文件承載 dreaming 任務的狀態。三者均可在 tick 間隙用編輯器查看或修改:

文件作用生命週期
.dreaming.lock多進程互斥鎖,包含 pidhostname、ISO 格式 started_at、不透明 token搶到鎖時創建,釋放時刪除。超過 WORKSPACE_DREAM_LOCK_TTL_MS 或同主機進程已死時視為陳舊
.dreamed-state.jsonmtime 水位線 last_processed_mtime成功 append 或 NOTHING_TO_PROMOTE 後推進。命中 PII 時推進
.dreamed-quarantine.json{ quarantined_at, pattern, log_filenames, max_mtime } 條目數組PII 閘門追加。操作者手動刪除條目(或整文件)以重新啟用

多進程安全

REPL(npm run dev)與 HTTP 網關(npm run serve)各自啟動一個調度器,跑在同一份工作區上。若不做串行化,兩個進程的 tick 窗口可能撞在一起,將同一批每日日誌重複 promote 到 MEMORY.md.dreaming.lock 防止此情況:每次 dream pass 持鎖直到 LLM 調用結束,所有退出路徑均釋放鎖。

兩條接管陳舊鎖的路徑處理崩潰的 peer:

  • 超時陳舊:若 Date.now() - started_at > WORKSPACE_DREAM_LOCK_TTL_MS(默認 30 分鐘,與 tick 間隔解耦),鎖可被接管。
  • PID 陳舊(僅同主機):若鎖的 hostname 與本機匹配,且 kill -0 pid 報告該進程已死,鎖可被接管。

接管前會重新 stat 鎖文件並重讀 token,確認仍是同一把鎖。若另一進程在"判定陳舊"和"unlink"之間剛好刷新了鎖,本進程會讓出本次 tick 而不刪除新鎖(關閉 read-then-unlink 的 TOCTOU 競態)。

NFS 不被支持:O_EXCL 在部分 NFS 客戶端下不保證原子性。共享存儲掛載的工作區請將 dreaming 限制在單一主機上啟用。

預算與選取

長窗口的密集每日日誌會撐爆模型上下文。兩層上限做防護:

  • 每文件:每份每日日誌尾部截斷到 64 KB(可通過 dreaming 任務的 maxFileBytes 依賴配置)。日誌是僅追加的,尾部(最近的輪次內容)正是 curator 提示詞關心的部分;早期的頭部截斷會丟掉最新內容。被截斷的文件前綴加 [...truncated head],讓 LLM 知道這是片段。
  • 總量:WORKSPACE_DREAM_TOTAL_INPUT_BYTES(默認 256 KB)封頂所有選中日誌的合併字節數。選取算法是最新優先連續後綴:從最新往最舊累計字節數,下一份會讓總數超預算時停止。最新一份日誌一定保留(單獨超過總預算時截斷到 64 KB)。LLM 不會看到"day 1 + day 30 中間空一片"的非連續時間線。

MEMORY.md 在傳給 LLM 做提示詞內去重時也尾部截斷到 64 KB,因為最新的 ## Dreamed 章節(最可能被重複提議的內容)位於文件末尾。

NOTHING_TO_PROMOTE 嚴格匹配

當 curator 提示詞判定日誌中沒有值得 promote 的內容時,它返回單獨一行字面量 NOTHING_TO_PROMOTE。Dreaming 任務只接受嚴格相等(trim 後):響應 ### Facts\n- discussed the NOTHING_TO_PROMOTE marker behaviour 會被正常追加,因為它是合法輸出,只是順便提到了該 marker。早期版本的 substring 檢查會靜默吞掉任何含此字符串的響應。

PII 隔離

正則後置過濾在 append 之前對 LLM 響應跑一次。命中的 pattern 都是高置信度的密鑰 / 憑據,絕不能落到 MEMORY.md(它會被注入每個未來會話的系統提示詞,洩漏會持續放大):

類別Pattern 形態
anthropic_keysk-ant-...(必須排在 openai_key 之前)
openai_keysk-...(20+ 字母數字)
stripe_keysk_live_...rk_live_...
github_patghp_github_pat_gho_ghu_ghs_ghr_
google_api_keyAIza + 35 個 url-safe 字符
slack_tokenxoxb-xoxp-xoxa-xoxr-xoxs-
aws_access_keyAKIA + 16 個大寫字母 / 數字
evm_address0x + 40 hex
btc_bech32bc1...
btc_legacy13 開頭的 base58
pem_private-----BEGIN ... PRIVATE KEY-----

命中時:

  1. 響應追加到 MEMORY.md
  2. 涉及的源文件名追加到 .dreamed-quarantine.json,記錄命中的 pattern 名。
  3. 一條結構化 error 級日誌包含 pattern、文件名、工作區路徑。
  4. mtime 水位線推進。推進等於將安全事件靜默歸檔;改用按文件名過濾的方式,讓這批日誌在操作者清理前不再進入選取。

Solana base58(32-44 字符)、BIP-39 助記詞、裸 64-hex 字符串均被故意排除在 pattern 列表之外。它們對哈希、交易簽名、自然語言段落的誤報率太高,不適合做硬閘門。提示詞層面的指令("不要包含任何看似敏感的內容")仍要求 LLM 自行抑制這些類別。

清理隔離條目:編輯 <workspace>/.dreamed-quarantine.json 刪除條目,或直接刪除整個文件。下個 tick 會重新考慮那些每日日誌。若洩漏本身在源日誌中,需先編輯對應的 memory/YYYY-MM-DD-<session>.md

操作者調試入口

常見場景與查看順序:

  • "昨天 dream 跑了嗎?".dreamed-state.jsonlast_run_at。在 Agent 日誌中查 module: "workspace/dreaming-task" 的行:appended / nothing_to_promote / pii_detected_quarantine / lock_held_skip / llm_error
  • "為什麼這條事實沒被 promote?" 源日誌的 mtime 是否大於 last_processed_mtime?源文件名是否在 .dreamed-quarantine.json 中?LLM 是否返回了 NOTHING_TO_PROMOTE?結構化日誌行說明每種原因。
  • "兩個進程同時跑 dream 但沒看到重複章節。" 鎖機制在工作。失敗方記錄 lock_held_skip 後乾淨退出。
  • "PII 隔離觸發了。" 檢查 .dreamed-quarantine.json 中的 pattern 名與源文件名。打開源日誌判斷:是真實洩漏(脫敏後清理條目),還是文本對話誤報(直接刪條目讓下次 tick 重試)。
  • "我的 LLM 調用經常 45 分鐘。"WORKSPACE_DREAM_LOCK_TTL_MS 調到大於最壞情況的牆鍾,避免另一進程將仍在工作的 dream 當成陳舊鎖接管。

配置

五個環境變量控制 dreaming 行為。完整參考連同默認值與影響見環境變量:工作區:

  • WORKSPACE_DREAM_ENABLED:總開關(默認關閉)。
  • WORKSPACE_DREAM_INTERVAL_HOURS:tick 間隔(默認 24 小時)。
  • WORKSPACE_DREAM_TOTAL_INPUT_BYTES:提示詞總字節預算(默認 256 KB)。
  • WORKSPACE_DREAM_LOCK_TTL_MS:鎖陳舊 TTL(默認 30 分鐘)。
  • WORKSPACE_DAILY_LOG_INTERVAL:每日日誌寫入間隔(dreaming 的輸入來源)。

SOUL.md 變更披露

運行時在啟動時對 SOUL.md 計算哈希,並將結果持久化到 <workspaceDir>/.soul-state.json。若兩次會話之間哈希發生變化,Agent 循環將收到一次性提示詞級通知,要求在下次回覆中向操作者確認該變更。單次確認完成後,變更不再被提及;檢測器在每次讀取時重新建立基準。

BOOTSTRAP.md 兩階段引導

全新工作區附帶 BOOTSTRAP.md。流程如下:

  1. 第一輪:Agent 讀取 BOOTSTRAP.md,向操作者提出引導問題(姓名、時區、關注點、風險承受度、默認交易場所)。此輪輸出 BOOTSTRAP_DONE
  2. 第二輪:操作者回答後,Agent 使用 write_file 填充 USER.md,簡要確認寫入內容,然後在回覆末尾單獨一行輸出字面 token BOOTSTRAP_DONE

bootstrap 處理鉤子(輪次結束時)檢測到該 token 後,將 BOOTSTRAP.md 重命名為 BOOTSTRAP.md.done.<timestamp>(保留審計跟蹤)。下一輪時文件已不存在,動態提示詞塊將自動去掉引導指令。

工作區作為事實來源(優先級)

Agent 循環組裝系統提示詞時,工作區 Markdown 位於最前面,優先於派生層:

  1. identity 塊(緩存):SOUL.md + AGENTS.md
  2. memorySnapshot(動態,位於動態塊最前):USER.mdMEMORY.mdHEARTBEAT.md、近期日記條目。
  3. bootstrapInstructions(動態,僅當 BOOTSTRAP.md 存在時):首次運行劇本。
  4. 技能目錄、場景劇本、個性化、方法論提示及其他派生層位於其後。

該順序是有意為之:若 MEMORY.md 中寫明"用戶在 BTC 上偏好現貨而非永續合約",而個性化層推斷出"用戶偏好永續合約",則 MEMORY.md 優先。這一優先級規則在 apps/agent/src/core/prompt-builder.ts 中強制執行,並在 AGENTS.mdSoul 章節中明文規定。

配置

默認值經過調整,確保新安裝即可直接使用:

  • 心跳:開啟(WORKSPACE_HEARTBEAT_ENABLED=1)
  • 每日日誌:關閉(WORKSPACE_DAILY_LOG_ENABLED=0)
  • Dreaming:關閉(WORKSPACE_DREAM_ENABLED=0)

完整參數列表(含默認值與說明)見環境變量參考

產物與文件

工作區的 Markdown 是持久化的狀態。另有兩個並列存儲保存持久化的內容產物(Agent 在某輪中生成的圖表、電子表格和報告)和文件(用戶上傳的二進制內容)。兩者與工作區和沙盒並列,但契約各不相同:沙盒是 Agent 在單輪中可隨意覆寫的臨時空間,而產物與文件必須跨會話留存、帶有穩定 id,且不會被偶發的 write_file 改動。它們與沙盒從不共享路徑,因此寫入臨時輸出的工具無法覆蓋已存儲的圖表或用戶上傳文件。

產物存儲

產物存儲在 chat_artifacts SQLite 表中,由 artifacts/artifact-store.ts 擁有。三種類型共享同一行結構:

類型id 前綴data payload構建器
chartx-{ charts: [...] } ECharts 選項chart-builder.ts
spreadsheetx-{ csvContent, title?, description? }CSV,物化為 .csv
reportr-完整 HTML / markdown 報告深度研究

每個構建器都走同一條狀態路徑:insert()running)→ markCompleted(data)completed)或 markError(msg)error)。REPL 渲染 (running) 佔位符並就地更新;審計日誌記錄每次狀態轉換。

模型通過 URI 引用已完成的產物,絕不臆造 id:chart://x-…report://r-…spreadsheet://x-…。臆造的 id 會解析為結構化的 ArtifactNotFoundError,模型看到後自行糾正。輪次結束時,gateway/render/artifact-materializer.ts 掃描最終消息中的這些 URI,將可瀏覽文件(自包含 HTML 查看器、原始 JSON,以及每張圖表的 2 倍 PNG;每個電子表格一個 .csv)寫入 $dataDir/artifacts/<id>/。PNG 渲染為盡力而為:若未安裝 Playwright Chromium,物化器記錄 chart_png_skipped 並繼續。

文件存儲

文件是 Agent 存儲並引用、但不解析的不透明字節,由 files/file-store.ts 擁有。默認的 LocalFileStore 寫入 $dataDir/uploads/,鍵為 chat/files/<userId>/<unix_ms>-<random_id>.<ext>unix_ms 前綴讓上傳文件天然按時間排序)。

StoredFile 攜帶 key(寫入聊天消息的穩定 id)和 url(LLM 提供方為多模態消息抓取的地址)。上傳(POST /v1/files)經 fileStore.put(...) 處理,將 key 附加到待處理消息,並強制默認 20 MB 上限,與各提供方的多模態上限一致。files/message-translator.ts 在進入循環時將附件 key 映射為各提供方偏好的多模態形態,在寫回存儲時再映射回 key。消息文本存於 sessions 行;二進制內容存於文件存儲;翻譯器在兩者之間橋接。sessions 中從不存儲原始二進制。上傳的電子表格由 files/spreadsheet-parser.ts 轉換為結構化文本,使模型無需完整附件往返即可對其內容進行推理。

安全態勢

  • 文件從不執行。該存儲是 blob 緩存,沒有任何代碼路徑會 eval 上傳內容。
  • key 與產物 id 是穩定引用,而非機密。將 URL 或 chart:// id 視為持有即可訪問的憑據;網關在其上疊加會話級鑑權。
  • 刪除是顯式的。LocalFileStore.delete(key) 從磁盤移除文件;GDPR 式清除請通過它進行,使移除有意且可追溯。

返回這些內容的 HTTP 端點見 API → /files 及產物抓取路由。

OpenClaw 兼容性

工作區格式與 OpenClaw 的 AGENTS.default 規範在字節層面兼容:文件名、章節標題、frontmatter 結構完全一致。為任一工具編寫的工作區均可直接移植,無需轉換;僅有各工具各自獨立維護的運行時產物在本質上有所不同。

將現有 OpenClaw 工作區導入 Minara 的三種方式:

  • 啟動時傳入 --workspace ~/.openclaw/workspace
  • 在環境中設置 MINARA_WORKSPACE_DIR=~/.openclaw/workspace
  • 運行 OpenClawWorkspaceImporter,將文件複製到 Minara 的默認位置。

BOOTSTRAP.mdHEARTBEAT.md 有意被導入器跳過:前者是一次性引導劇本(Minara 附帶自己的初始模板);後者由 Agent 在每輪結束時改寫,導入過期心跳會對會話連續性造成誤導。

本頁目錄