本章总览
先看职责:memory-extraction 负责在回合结束后做“后台补记忆”,把 transcript 中可长期复用的信息落到 memdir 文件系统。它通过 stop hook 触发、scan 清单预热、forked agent 写入与互斥跳过机制,和主 agent 的即时写入形成互补。
学完本章你应该能
- 描述 extractMemories 触发时机(stopHooks)
- 理解 runForkedAgent 与 cache-safe params
- 说明 hasMemoryWritesSince 跳过逻辑
- 知道 scanMemoryFiles 如何避免 extract agent ls 浪费 turn
- 列举 extract 可用 tool 白名单思路
- 理解 stash/trailing extraction 并发场景
核心概念(先读懂这些)
主 agent 与 extract agent 分工
paths.ts 注释:主 agent prompt 始终含完整 save 指令;主 agent 写了 memory 则 extract 跳过该 uuid 区间;主 agent 没写则 extract 补漏。不是二选一 feature,而是互补双轨。
memoryScan 破 cycle
memoryScan.ts 从 findRelevantMemories 拆出,避免 extractMemories import findRelevantMemories → sideQuery → memdir 循环 (#25372)。scan 仅 fs + frontmatter,无 API client。
Closure-scoped state
initExtractMemories 返回 { executeExtractMemories, drainPendingExtraction, ... },内部 mutable lastExtractUuid、inProgress flag。与 confidenceRating 同模式,避免 module-level 测试污染。
建议学习步骤
- 阅读 extractMemories.ts 文件头注释
- 跟踪 executeExtractMemories 主流程
- 阅读 hasMemoryWritesSince 与 FileWrite 检测
- 查看 prompts buildExtractCombinedPrompt
- 阅读 stopHooks handleStopHooks 分支
- 查看 utils/messages createMemorySavedMessage
常见误区
注意
feature EXTRACT_MEMORIES 必须 call site if feature() 直接判断
注意
isExtractModeActive 不含 feature check
注意
extract subagent 不 record transcript
注意
team memory 路径 feature TEAMMEM require
extractMemories 架构
services/extractMemories/extractMemories.ts:
stopHooks / print exit
→ isAutoMemoryEnabled && feature(EXTRACT_MEMORIES) && isExtractModeActive
→ executeExtractMemories(messages, sinceUuid, ...)
→ hasMemoryWritesSince? skip
→ scanMemoryFiles + formatMemoryManifest 注入 prompt
→ runForkedAgent(extract prompt, limited tools)
→ writtenPaths → createMemorySavedMessage / logEvent
使用 ENTRYPOINT_NAME、getAutoMemPath、isAutoMemPath 来自 memdir/。
源码引用: src/services/extractMemories/extractMemories.ts · 第 1–30 行(共 616 行)
1| /**
2| * Extracts durable memories from the current session transcript
3| * and writes them to the auto-memory directory (~/.claude/projects/<path>/memory/).
4| *
5| * It runs once at the end of each complete query loop (when the model produces
6| * a final response with no tool calls) via handleStopHooks in stopHooks.ts.
7| *
8| * Uses the forked agent pattern (runForkedAgent) — a perfect fork of the main
9| * conversation that shares the parent's prompt cache.
10| *
11| * State is closure-scoped inside initExtractMemories() rather than module-level,
12| * following the same pattern as confidenceRating.ts. Tests call
13| * initExtractMemories() in beforeEach to get a fresh closure.
14| */
15|
16| import { feature } from 'bun:bundle'
17| import { basename } from 'path'
18| import { getIsRemoteMode } from '../../bootstrap/state.js'
19| import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
20| import { ENTRYPOINT_NAME } from '../../memdir/memdir.js'
21| import {
22| formatMemoryManifest,
23| scanMemoryFiles,
24| } from '../../memdir/memoryScan.js'
25| import {
26| getAutoMemPath,
27| isAutoMemoryEnabled,
28| isAutoMemPath,
29| } from '../../memdir/paths.js'
30| import type { Tool } from '../../Tool.js'
源码引用: src/services/extractMemories/extractMemories.ts · 第 330–400 行(共 616 行)
330| context,
331| appendSystemMessage,
332| isTrailingRun,
333| }: {
334| context: REPLHookContext
335| appendSystemMessage?: AppendSystemMessageFn
336| isTrailingRun?: boolean
337| }): Promise<void> {
338| const { messages } = context
339| const memoryDir = getAutoMemPath()
340| const newMessageCount = countModelVisibleMessagesSince(
341| messages,
342| lastMemoryMessageUuid,
343| )
344|
345| // Mutual exclusion: when the main agent wrote memories, skip the
346| // forked agent and advance the cursor past this range so the next
347| // extraction only considers messages after the main agent's write.
348| if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
349| logForDebugging(
350| '[extractMemories] skipping — conversation already wrote to memory files',
351| )
352| const lastMessage = messages.at(-1)
353| if (lastMessage?.uuid) {
354| lastMemoryMessageUuid = lastMessage.uuid
355| }
356| logEvent('tengu_extract_memories_skipped_direct_write', {
357| message_count: newMessageCount,
358| })
359| return
360| }
361|
362| const teamMemoryEnabled = feature('TEAMMEM')
363| ? teamMemPaths!.isTeamMemoryEnabled()
364| : false
365|
366| const skipIndex = getFeatureValue_CACHED_MAY_BE_STALE(
367| 'tengu_moth_copse',
368| false,
369| )
370|
371| const canUseTool = createAutoMemCanUseTool(memoryDir)
372| const cacheSafeParams = createCacheSafeParams(context)
373|
374| // Only run extraction every N eligible turns (tengu_bramble_lintel, default 1).
375| // Trailing extractions (from stashed contexts) skip this check since they
376| // process already-committed work that should not be throttled.
377| if (!isTrailingRun) {
378| turnsSinceLastExtraction++
379| if (
380| turnsSinceLastExtraction <
381| (getFeatureValue_CACHED_MAY_BE_STALE('tengu_bramble_lintel', null) ?? 1)
382| ) {
383| return
384| }
385| }
386| turnsSinceLastExtraction = 0
387|
388| inProgress = true
389| const startTime = Date.now()
390| try {
391| logForDebugging(
392| `[extractMemories] starting — ${newMessageCount} new messages, memoryDir=${memoryDir}`,
393| )
394|
395| // Pre-inject the memory directory manifest so the agent doesn't spend
396| // a turn on `ls`. Reuses findRelevantMemories' frontmatter scan.
397| // Placed after the throttle gate so skipped turns don't pay the scan cost.
398| const existingMemories = formatMemoryManifest(
399| await scanMemoryFiles(memoryDir, createAbortController().signal),
400| )
stopHooks 触发点
query/stopHooks.ts 在 model 产出 final response(无 tool calls)后运行 stop hooks,并 void executeExtractMemories(...)。
条件链:isExtractModeActive()、feature gate、非 abort。传入 newMessageCount、memoryDir、appendSystemMessage 回调。
与 compact、confidence rating 等 stop hook 并列——extract 是 fire-and-forget async。
源码引用: src/query/stopHooks.ts · 第 1–50 行(共 474 行)
1| import { feature } from 'bun:bundle'
2| import { getShortcutDisplay } from '../keybindings/shortcutFormat.js'
3| import { isExtractModeActive } from '../memdir/paths.js'
4| import {
5| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
6| logEvent,
7| } from '../services/analytics/index.js'
8| import type { ToolUseContext } from '../Tool.js'
9| import type { HookProgress } from '../types/hooks.js'
10| import type {
11| AssistantMessage,
12| Message,
13| RequestStartEvent,
14| StopHookInfo,
15| StreamEvent,
16| TombstoneMessage,
17| ToolUseSummaryMessage,
18| } from '../types/message.js'
19| import { createAttachmentMessage } from '../utils/attachments.js'
20| import { logForDebugging } from '../utils/debug.js'
21| import { errorMessage } from '../utils/errors.js'
22| import type { REPLHookContext } from '../utils/hooks/postSamplingHooks.js'
23| import {
24| executeStopHooks,
25| executeTaskCompletedHooks,
26| executeTeammateIdleHooks,
27| getStopHookMessage,
28| getTaskCompletedHookMessage,
29| getTeammateIdleHookMessage,
30| } from '../utils/hooks.js'
31| import {
32| createStopHookSummaryMessage,
33| createSystemMessage,
34| createUserInterruptionMessage,
35| createUserMessage,
36| } from '../utils/messages.js'
37| import type { SystemPrompt } from '../utils/systemPromptType.js'
38| import { getTaskListId, listTasks } from '../utils/tasks.js'
39| import { getAgentName, getTeamName, isTeammate } from '../utils/teammate.js'
40|
41| /* eslint-disable @typescript-eslint/no-require-imports */
42| const extractMemoriesModule = feature('EXTRACT_MEMORIES')
43| ? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
44| : null
45| const jobClassifierModule = feature('TEMPLATES')
46| ? (require('../jobs/classifier.js') as typeof import('../jobs/classifier.js'))
47| : null
48|
49| /* eslint-enable @typescript-eslint/no-require-imports */
50|
源码引用: src/query/stopHooks.ts · 第 140–160 行(共 474 行)
140| }
141| if (
142| feature('EXTRACT_MEMORIES') &&
143| !toolUseContext.agentId &&
144| isExtractModeActive()
145| ) {
146| // Fire-and-forget in both interactive and non-interactive. For -p/SDK,
147| // print.ts drains the in-flight promise after flushing the response
148| // but before gracefulShutdownSync (see drainPendingExtraction).
149| void extractMemoriesModule!.executeExtractMemories(
150| stopHookContext,
151| toolUseContext.appendSystemMessage,
152| )
153| }
154| if (!toolUseContext.agentId) {
155| void executeAutoDream(stopHookContext, toolUseContext.appendSystemMessage)
156| }
157| }
158|
159| // chicago MCP: auto-unhide + lock release at turn end.
160| // Main thread only — the CU lock is a process-wide module-level variable,
hasMemoryWritesSince 互斥
检测 transcript 中主 agent 是否已用 Write/Edit 写 isAutoMemPath 下文件。若 true,log skipping 并 return——避免 duplicate 记忆。
prompts.ts 注释:extract 跳过已有 memory writes 的 turn;主 agent prompt 仍鼓励实时 save。
Tool 名常量:FILE_WRITE、FILE_EDIT、BASH 等来自各 Tool prompt.js。
源码引用: src/services/extractMemories/extractMemories.ts · 第 160–220 行(共 616 行)
160| behavior: 'deny' as const,
161| message: reason,
162| decisionReason: { type: 'other' as const, reason },
163| }
164| }
165|
166| /**
167| * Creates a canUseTool function that allows Read/Grep/Glob (unrestricted),
168| * read-only Bash commands, and Edit/Write only for paths within the
169| * auto-memory directory. Shared by extractMemories and autoDream.
170| */
171| export function createAutoMemCanUseTool(memoryDir: string): CanUseToolFn {
172| return async (tool: Tool, input: Record<string, unknown>) => {
173| // Allow REPL — when REPL mode is enabled (ant-default), primitive tools
174| // are hidden from the tool list so the forked agent calls REPL instead.
175| // REPL's VM context re-invokes this canUseTool for each inner primitive
176| // (toolWrappers.ts createToolWrapper), so the Read/Bash/Edit/Write checks
177| // below still gate the actual file and shell operations. Giving the fork a
178| // different tool list would break prompt cache sharing (tools are part of
179| // the cache key — see CacheSafeParams in forkedAgent.ts).
180| if (tool.name === REPL_TOOL_NAME) {
181| return { behavior: 'allow' as const, updatedInput: input }
182| }
183|
184| // Allow Read/Grep/Glob unrestricted — all inherently read-only
185| if (
186| tool.name === FILE_READ_TOOL_NAME ||
187| tool.name === GREP_TOOL_NAME ||
188| tool.name === GLOB_TOOL_NAME
189| ) {
190| return { behavior: 'allow' as const, updatedInput: input }
191| }
192|
193| // Allow Bash only for commands that pass BashTool.isReadOnly.
194| // `tool` IS BashTool here — no static import needed.
195| if (tool.name === BASH_TOOL_NAME) {
196| const parsed = tool.inputSchema.safeParse(input)
197| if (parsed.success && tool.isReadOnly(parsed.data)) {
198| return { behavior: 'allow' as const, updatedInput: input }
199| }
200| return denyAutoMemTool(
201| tool,
202| 'Only read-only shell commands are permitted in this context (ls, find, grep, cat, stat, wc, head, tail, and similar)',
203| )
204| }
205|
206| if (
207| (tool.name === FILE_EDIT_TOOL_NAME ||
208| tool.name === FILE_WRITE_TOOL_NAME) &&
209| 'file_path' in input
210| ) {
211| const filePath = input.file_path
212| if (typeof filePath === 'string' && isAutoMemPath(filePath)) {
213| return { behavior: 'allow' as const, updatedInput: input }
214| }
215| }
216|
217| return denyAutoMemTool(
218| tool,
219| `only ${FILE_READ_TOOL_NAME}, ${GREP_TOOL_NAME}, ${GLOB_TOOL_NAME}, read-only ${BASH_TOOL_NAME}, and ${FILE_EDIT_TOOL_NAME}/${FILE_WRITE_TOOL_NAME} within ${memoryDir} are allowed`,
220| )
源码引用: src/services/extractMemories/prompts.ts · 第 1–30 行(共 155 行)
1| /**
2| * Prompt templates for the background memory extraction agent.
3| *
4| * The extraction agent runs as a perfect fork of the main conversation — same
5| * system prompt, same message prefix. The main agent's system prompt always
6| * has full save instructions; when the main agent writes memories itself,
7| * extractMemories.ts skips that turn (hasMemoryWritesSince). This prompt
8| * fires only when the main agent didn't write, so the save-criteria here
9| * overlap the system prompt's harmlessly.
10| */
11|
12| import { feature } from 'bun:bundle'
13| import {
14| MEMORY_FRONTMATTER_EXAMPLE,
15| TYPES_SECTION_COMBINED,
16| TYPES_SECTION_INDIVIDUAL,
17| WHAT_NOT_TO_SAVE_SECTION,
18| } from '../../memdir/memoryTypes.js'
19| import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js'
20| import { FILE_EDIT_TOOL_NAME } from '../../tools/FileEditTool/constants.js'
21| import { FILE_READ_TOOL_NAME } from '../../tools/FileReadTool/prompt.js'
22| import { FILE_WRITE_TOOL_NAME } from '../../tools/FileWriteTool/prompt.js'
23| import { GLOB_TOOL_NAME } from '../../tools/GlobTool/prompt.js'
24| import { GREP_TOOL_NAME } from '../../tools/GrepTool/prompt.js'
25|
26| /**
27| * Shared opener for both extract-prompt variants.
28| */
29| function opener(newMessageCount: number, existingMemories: string): string {
30| const manifest =
memoryScan 原语
scanMemoryFiles(memoryDir, signal):
- readdir recursive,filter .md 排除 MEMORY.md
- readFileInRange 前 30 行 frontmatter
- parseMemoryType、description
- sort mtime desc,cap MAX_MEMORY_FILES=200
formatMemoryManifest 一行一文件供 extract prompt 预加载。
extractMemories 与 findRelevantMemories 共用 scan,不共用 sideQuery 选文件逻辑。
源码引用: src/memdir/memoryScan.ts · 第 21–77 行(共 95 行)
21| const MAX_MEMORY_FILES = 200
22| const FRONTMATTER_MAX_LINES = 30
23|
24| /**
25| * Scan a memory directory for .md files, read their frontmatter, and return
26| * a header list sorted newest-first (capped at MAX_MEMORY_FILES). Shared by
27| * findRelevantMemories (query-time recall) and extractMemories (pre-injects
28| * the listing so the extraction agent doesn't spend a turn on `ls`).
29| *
30| * Single-pass: readFileInRange stats internally and returns mtimeMs, so we
31| * read-then-sort rather than stat-sort-read. For the common case (N ≤ 200)
32| * this halves syscalls vs a separate stat round; for large N we read a few
33| * extra small files but still avoid the double-stat on the surviving 200.
34| */
35| export async function scanMemoryFiles(
36| memoryDir: string,
37| signal: AbortSignal,
38| ): Promise<MemoryHeader[]> {
39| try {
40| const entries = await readdir(memoryDir, { recursive: true })
41| const mdFiles = entries.filter(
42| f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
43| )
44|
45| const headerResults = await Promise.allSettled(
46| mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
47| const filePath = join(memoryDir, relativePath)
48| const { content, mtimeMs } = await readFileInRange(
49| filePath,
50| 0,
51| FRONTMATTER_MAX_LINES,
52| undefined,
53| signal,
54| )
55| const { frontmatter } = parseFrontmatter(content, filePath)
56| return {
57| filename: relativePath,
58| filePath,
59| mtimeMs,
60| description: frontmatter.description || null,
61| type: parseMemoryType(frontmatter.type),
62| }
63| }),
64| )
65|
66| return headerResults
67| .filter(
68| (r): r is PromiseFulfilledResult<MemoryHeader> =>
69| r.status === 'fulfilled',
70| )
71| .map(r => r.value)
72| .sort((a, b) => b.mtimeMs - a.mtimeMs)
73| .slice(0, MAX_MEMORY_FILES)
74| } catch {
75| return []
76| }
77| }
源码引用: src/memdir/memoryScan.ts · 第 79–95 行(共 95 行)
79| /**
80| * Format memory headers as a text manifest: one line per file with
81| * [type] filename (timestamp): description. Used by both the recall
82| * selector prompt and the extraction-agent prompt.
83| */
84| export function formatMemoryManifest(memories: MemoryHeader[]): string {
85| return memories
86| .map(m => {
87| const tag = m.type ? `[${m.type}] ` : ''
88| const ts = new Date(m.mtimeMs).toISOString()
89| return m.description
90| ? `- ${tag}${m.filename} (${ts}): ${m.description}`
91| : `- ${tag}${m.filename} (${ts})`
92| })
93| .join('\n')
94| }
95|
SessionMemory 系统消息
提取或主 agent 写入成功后,createMemorySavedMessage(utils/messages.ts)生成 SystemMemorySavedMessage 进 transcript,UI 显示 MemoryUpdateNotification。
appendSystemMessage 回调由 REPL 传入 extract 闭包,将消息 insert 为 isMeta 或 system subtype memory_saved。
用户可见「记忆已保存」与相对路径 getRelativeMemoryPath。
源码引用: src/utils/messages.ts · 第 200–250 行(共 5513 行)
200| export function deriveShortMessageId(uuid: string): string {
201| // Take first 10 hex chars from the UUID (skipping dashes)
202| const hex = uuid.replace(/-/g, '').slice(0, 10)
203| // Convert to base36 for shorter representation, take 6 chars
204| return parseInt(hex, 16).toString(36).slice(0, 6)
205| }
206|
207| export const INTERRUPT_MESSAGE = '[Request interrupted by user]'
208| export const INTERRUPT_MESSAGE_FOR_TOOL_USE =
209| '[Request interrupted by user for tool use]'
210| export const CANCEL_MESSAGE =
211| "The user doesn't want to take this action right now. STOP what you are doing and wait for the user to tell you how to proceed."
212| export const REJECT_MESSAGE =
213| "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed."
214| export const REJECT_MESSAGE_WITH_REASON_PREFIX =
215| "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\n"
216| export const SUBAGENT_REJECT_MESSAGE =
217| 'Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.'
218| export const SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX =
219| 'Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). The user said:\n'
220| export const PLAN_REJECTION_PREFIX =
221| 'The agent proposed a plan that was rejected by the user. The user chose to stay in plan mode rather than proceed with implementation.\n\nRejected plan:\n'
222|
223| /**
224| * Shared guidance for permission denials, instructing the model on appropriate workarounds.
225| */
226| export const DENIAL_WORKAROUND_GUIDANCE =
227| `IMPORTANT: You *may* attempt to accomplish this action using other tools that might naturally be used to accomplish this goal, ` +
228| `e.g. using head instead of cat. But you *should not* attempt to work around this denial in malicious ways, ` +
229| `e.g. do not use your ability to run tests to execute non-test actions. ` +
230| `You should only try to work around this restriction in reasonable ways that do not attempt to bypass the intent behind this denial. ` +
231| `If you believe this capability is essential to complete the user's request, STOP and explain to the user ` +
232| `what you were trying to do and why you need this permission. Let the user decide how to proceed.`
233|
234| export function AUTO_REJECT_MESSAGE(toolName: string): string {
235| return `Permission to use ${toolName} has been denied. ${DENIAL_WORKAROUND_GUIDANCE}`
236| }
237| export function DONT_ASK_REJECT_MESSAGE(toolName: string): string {
238| return `Permission to use ${toolName} has been denied because Claude Code is running in don't ask mode. ${DENIAL_WORKAROUND_GUIDANCE}`
239| }
240| export const NO_RESPONSE_REQUESTED = 'No response requested.'
241|
242| // Synthetic tool_result content inserted by ensureToolResultPairing when a
243| // tool_use block has no matching tool_result. Exported so HFI submission can
244| // reject any payload containing it — placeholder satisfies pairing structurally
245| // but the content is fake, which poisons training data if submitted.
246| export const SYNTHETIC_TOOL_RESULT_PLACEHOLDER =
247| '[Tool result missing due to internal error]'
248|
249| // Prefix used by UI to detect classifier denials and render them concisely
250| const AUTO_MODE_REJECTION_PREFIX =
源码引用: src/types/message.ts · 第 61–62 行(共 135 行)
61| export type SystemMemorySavedMessage = SystemMessage
62| export type SystemStopHookSummaryMessage = SystemMessage
源码引用: src/services/extractMemories/extractMemories.ts · 第 450–500 行(共 616 行)
450| : '0.0'
451| logForDebugging(
452| `[extractMemories] finished — ${writtenPaths.length} files written, cache: read=${result.totalUsage.cache_read_input_tokens} create=${result.totalUsage.cache_creation_input_tokens} input=${result.totalUsage.input_tokens} (${hitPct}% hit)`,
453| )
454|
455| if (writtenPaths.length > 0) {
456| logForDebugging(
457| `[extractMemories] memories saved: ${writtenPaths.join(', ')}`,
458| )
459| } else {
460| logForDebugging('[extractMemories] no memories saved this run')
461| }
462|
463| // Index file updates are mechanical — the agent touches MEMORY.md to add
464| // a topic link, but the user-visible "memory" is the topic file itself.
465| const memoryPaths = writtenPaths.filter(
466| p => basename(p) !== ENTRYPOINT_NAME,
467| )
468| const teamCount = feature('TEAMMEM')
469| ? count(memoryPaths, teamMemPaths!.isTeamMemPath)
470| : 0
471|
472| // Log extraction event with usage from the forked agent
473| logEvent('tengu_extract_memories_extraction', {
474| input_tokens: result.totalUsage.input_tokens,
475| output_tokens: result.totalUsage.output_tokens,
476| cache_read_input_tokens: result.totalUsage.cache_read_input_tokens,
477| cache_creation_input_tokens:
478| result.totalUsage.cache_creation_input_tokens,
479| message_count: newMessageCount,
480| turn_count: turnCount,
481| files_written: writtenPaths.length,
482| memories_saved: memoryPaths.length,
483| team_memories_saved: teamCount,
484| duration_ms: Date.now() - startTime,
485| })
486|
487| logForDebugging(
488| `[extractMemories] writtenPaths=${writtenPaths.length} memoryPaths=${memoryPaths.length} appendSystemMessage defined=${appendSystemMessage != null}`,
489| )
490| if (memoryPaths.length > 0) {
491| const msg = createMemorySavedMessage(memoryPaths)
492| if (feature('TEAMMEM')) {
493| msg.teamCount = teamCount
494| }
495| appendSystemMessage?.(msg)
496| }
497| } catch (error) {
498| // Extraction is best-effort — log but don't notify on error
499| logForDebugging(`[extractMemories] error: ${error}`)
500| logEvent('tengu_extract_memories_error', {
并发 stash 与 drain
extract 进行中又来 stop hook → stash context for trailing run(log extraction in progress — stashing)。
drainPendingExtraction 在 print.ts CLI exit 调用,确保 trailing extraction 完成。
initExtractMemories closure 维护 inProgress、stashedMessages 状态机。
源码引用: src/services/extractMemories/extractMemories.ts · 第 510–570 行(共 616 行)
510| const trailing = pendingContext
511| pendingContext = undefined
512| if (trailing) {
513| logForDebugging(
514| '[extractMemories] running trailing extraction for stashed context',
515| )
516| await runExtraction({
517| context: trailing.context,
518| appendSystemMessage: trailing.appendSystemMessage,
519| isTrailingRun: true,
520| })
521| }
522| }
523| }
524|
525| // --- Public entry point (captured by extractor) ---
526|
527| async function executeExtractMemoriesImpl(
528| context: REPLHookContext,
529| appendSystemMessage?: AppendSystemMessageFn,
530| ): Promise<void> {
531| // Only run for the main agent, not subagents
532| if (context.toolUseContext.agentId) {
533| return
534| }
535|
536| if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
537| if (process.env.USER_TYPE === 'ant' && !hasLoggedGateFailure) {
538| hasLoggedGateFailure = true
539| logEvent('tengu_extract_memories_gate_disabled', {})
540| }
541| return
542| }
543|
544| // Check auto-memory is enabled
545| if (!isAutoMemoryEnabled()) {
546| return
547| }
548|
549| // Skip in remote mode
550| if (getIsRemoteMode()) {
551| return
552| }
553|
554| // If an extraction is already in progress, stash this context for a
555| // trailing run (overwrites any previously stashed context — only the
556| // latest matters since it has the most messages).
557| if (inProgress) {
558| logForDebugging(
559| '[extractMemories] extraction in progress — stashing for trailing run',
560| )
561| logEvent('tengu_extract_memories_coalesced', {})
562| pendingContext = { context, appendSystemMessage }
563| return
564| }
565|
566| await runExtraction({ context, appendSystemMessage })
567| }
568|
569| extractor = async (context, appendSystemMessage) => {
570| const p = executeExtractMemoriesImpl(context, appendSystemMessage)
源码引用: src/cli/print.ts · 第 354–375 行(共 5595 行)
354| import { isExtractModeActive } from '../memdir/paths.js'
355|
356| // Dead code elimination: conditional imports
357| /* eslint-disable @typescript-eslint/no-require-imports */
358| const coordinatorModeModule = feature('COORDINATOR_MODE')
359| ? (require('../coordinator/coordinatorMode.js') as typeof import('../coordinator/coordinatorMode.js'))
360| : null
361| const proactiveModule =
362| feature('PROACTIVE') || feature('KAIROS')
363| ? (require('../proactive/index.js') as typeof import('../proactive/index.js'))
364| : null
365| const cronSchedulerModule = feature('AGENT_TRIGGERS')
366| ? (require('../utils/cronScheduler.js') as typeof import('../utils/cronScheduler.js'))
367| : null
368| const cronJitterConfigModule = feature('AGENT_TRIGGERS')
369| ? (require('../utils/cronJitterConfig.js') as typeof import('../utils/cronJitterConfig.js'))
370| : null
371| const cronGate = feature('AGENT_TRIGGERS')
372| ? (require('../tools/ScheduleCronTool/prompt.js') as typeof import('../tools/ScheduleCronTool/prompt.js'))
373| : null
374| const extractMemoriesModule = feature('EXTRACT_MEMORIES')
375| ? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
源码引用: src/cli/print.ts · 第 960–975 行(共 5595 行)
960| logHeadlessProfilerTurn()
961|
962| // Drain any in-flight memory extraction before shutdown. The response is
963| // already flushed above, so this adds no user-visible latency — it just
964| // delays process exit so gracefulShutdownSync's 5s failsafe doesn't kill
965| // the forked agent mid-flight. Gated by isExtractModeActive so the
966| // tengu_slate_thimble flag controls non-interactive extraction end-to-end.
967| if (feature('EXTRACT_MEMORIES') && isExtractModeActive()) {
968| await extractMemoriesModule!.drainPendingExtraction()
969| }
970|
971| gracefulShutdownSync(
972| lastMessage?.type === 'result' && lastMessage?.is_error ? 1 : 0,
973| )
974| }
975|
findRelevantMemories 对比
query-time 召回(findRelevantMemories.ts):用户发消息时 sideQuery Sonnet 从 scan 列表选 ≤5 文件,exclude alreadySurfaced,thread mtimeMs。
与 extract 写入 正交:recall 读,extract 写。MEMORY_SHAPE_TELEMETRY feature 记录 recall 形状。
memdir 模块 9 文件中 findRelevantMemories + memoryScan 服务两条读路径。
源码引用: src/memdir/findRelevantMemories.ts · 第 18–45 行(共 142 行)
18| const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be useful to Claude Code as it processes a user's query. You will be given the user's query and a list of available memory files with their filenames and descriptions.
19|
20| Return a list of filenames for the memories that will clearly be useful to Claude Code as it processes the user's query (up to 5). Only include memories that you are certain will be helpful based on their name and description.
21| - If you are unsure if a memory will be useful in processing the user's query, then do not include it in your list. Be selective and discerning.
22| - If there are no memories in the list that would clearly be useful, feel free to return an empty list.
23| - If a list of recently-used tools is provided, do not select memories that are usage reference or API documentation for those tools (Claude Code is already exercising them). DO still select memories containing warnings, gotchas, or known issues about those tools — active use is exactly when those matter.
24| `
25|
26| /**
27| * Find memory files relevant to a query by scanning memory file headers
28| * and asking Sonnet to select the most relevant ones.
29| *
30| * Returns absolute file paths + mtime of the most relevant memories
31| * (up to 5). Excludes MEMORY.md (already loaded in system prompt).
32| * mtime is threaded through so callers can surface freshness to the
33| * main model without a second stat.
34| *
35| * `alreadySurfaced` filters paths shown in prior turns before the
36| * Sonnet call, so the selector spends its 5-slot budget on fresh
37| * candidates instead of re-picking files the caller will discard.
38| */
39| export async function findRelevantMemories(
40| query: string,
41| memoryDir: string,
42| signal: AbortSignal,
43| recentTools: readonly string[] = [],
44| alreadySurfaced: ReadonlySet<string> = new Set(),
45| ): Promise<RelevantMemory[]> {
源码引用: src/memdir/memoryShapeTelemetry.ts · 第 1–2 行(共 2 行)
1| export function recordMemoryShapeTelemetry(): void {}
2|
与 SessionMemory 的边界
extractMemories 与 services/SessionMemory 都是后台提取,但目标完全不同。extractMemories 写 auto-memory topic 文件,面向跨会话长期知识,依赖 getAutoMemPath、memoryScan、buildExtractAutoOnlyPrompt/CombinedPrompt,并在写入后用 createMemorySavedMessage 生成 memory_saved 系统消息。SessionMemory 写当前会话的摘要 markdown,面向 compact、awaySummary、skillify 等“本会话上下文压缩”场景,阈值来自 SessionMemoryConfig,状态如 lastSummarizedMessageId、extractionStartedAt、tokensAtLastExtraction 存在 SessionMemory/sessionMemoryUtils 中。
边界上有两个常见误区。第一,主 agent 直接 Write/Edit 到 auto-memory 时,extractMemories 会通过 hasMemoryWritesSince 跳过该区间,避免重复保存;SessionMemory 不参与这条互斥。第二,extractMemories 的 forked agent skipTranscript=true,因为它只是维护磁盘记忆,不应把后台分析过程插回主对话;SessionMemory 则会在 compact 前等待提取完成,以便 compact 可以用最新摘要替换旧消息。理解这两个差异后,看到 stopHooks、print drain、compact sessionMemoryCompact、/remember skill 时,就能判断它们是在维护长期 memdir,还是在维护当前会话摘要。
并发处理也体现了后台任务的产品取舍。extractMemories 正在运行时,新一轮 stop hook 不会启动第二个 forked agent,而是覆盖 pendingContext,只保留最新消息视图;当前 run 完成后再做 trailing extraction。这避免多个后台 agent 同时写 MEMORY.md 或同一 topic 文件,也避免频繁 stop hook 把 token 花在重复扫描上。print.ts 在 CLI 退出前 drain pending extraction,是为了让非交互模式的最后一轮也有机会落盘。
源码引用: src/services/extractMemories/extractMemories.ts · 第 345–360 行(共 616 行)
345| // Mutual exclusion: when the main agent wrote memories, skip the
346| // forked agent and advance the cursor past this range so the next
347| // extraction only considers messages after the main agent's write.
348| if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
349| logForDebugging(
350| '[extractMemories] skipping — conversation already wrote to memory files',
351| )
352| const lastMessage = messages.at(-1)
353| if (lastMessage?.uuid) {
354| lastMemoryMessageUuid = lastMessage.uuid
355| }
356| logEvent('tengu_extract_memories_skipped_direct_write', {
357| message_count: newMessageCount,
358| })
359| return
360| }
源码引用: src/services/extractMemories/extractMemories.ts · 第 415–427 行(共 616 行)
415| const result = await runForkedAgent({
416| promptMessages: [createUserMessage({ content: userPrompt })],
417| cacheSafeParams,
418| canUseTool,
419| querySource: 'extract_memories',
420| forkLabel: 'extract_memories',
421| // The extractMemories subagent does not need to record to transcript.
422| // Doing so can create race conditions with the main thread.
423| skipTranscript: true,
424| // Well-behaved extractions complete in 2-4 turns (read → write).
425| // A hard cap prevents verification rabbit-holes from burning turns.
426| maxTurns: 5,
427| })
源码引用: src/services/SessionMemory/sessionMemoryUtils.ts · 第 18–53 行(共 208 行)
18| export type SessionMemoryConfig = {
19| /** Minimum context window tokens before initializing session memory.
20| * Uses the same token counting as autocompact (input + output + cache tokens)
21| * to ensure consistent behavior between the two features. */
22| minimumMessageTokensToInit: number
23| /** Minimum context window growth (in tokens) between session memory updates.
24| * Uses the same token counting as autocompact (tokenCountWithEstimation)
25| * to measure actual context growth, not cumulative API usage. */
26| minimumTokensBetweenUpdate: number
27| /** Number of tool calls between session memory updates */
28| toolCallsBetweenUpdates: number
29| }
30|
31| // Default configuration values
32| export const DEFAULT_SESSION_MEMORY_CONFIG: SessionMemoryConfig = {
33| minimumMessageTokensToInit: 10000,
34| minimumTokensBetweenUpdate: 5000,
35| toolCallsBetweenUpdates: 3,
36| }
37|
38| // Current session memory configuration
39| let sessionMemoryConfig: SessionMemoryConfig = {
40| ...DEFAULT_SESSION_MEMORY_CONFIG,
41| }
42|
43| // Track the last summarized message ID (shared state)
44| let lastSummarizedMessageId: string | undefined
45|
46| // Track extraction state with timestamp (set by sessionMemory.ts)
47| let extractionStartedAt: number | undefined
48|
49| // Track context size at last memory extraction (for minimumTokensBetweenUpdate)
50| let tokensAtLastExtraction = 0
51|
52| // Track whether session memory has been initialized (met minimumMessageTokensToInit)
53| let sessionMemoryInitialized = false
源码引用: src/services/compact/sessionMemoryCompact.ts · 第 526–531 行(共 631 行)
526| // Wait for any in-progress session memory extraction to complete (with timeout)
527| await waitForSessionMemoryExtraction()
528|
529| const lastSummarizedMessageId = getLastSummarizedMessageId()
530| const sessionMemory = await getSessionMemoryContent()
531|
失败与降级策略(避免主流程受阻)
extractMemories 的实现强调“后台尽力而为,不阻塞主对话”。触发侧通常使用 void executeExtractMemories,说明即使提取失败,主 query loop 也不会回滚;执行侧在关键节点记录日志与 telemetry,而不是把异常上抛给用户输入路径。再加上 inProgress + pendingContext 机制,系统会优先完成当前提取,再处理最新一轮上下文,避免并发 fork 污染同一记忆目录。
这种降级策略与 memdir 的产品定位一致:记忆提取是增强能力,不应成为可用性单点。排障时应先确认触发条件与门控,再看互斥跳过和后台执行日志,最后才追查写盘细节。
源码引用: src/query/stopHooks.ts · 第 140–160 行(共 474 行)
140| }
141| if (
142| feature('EXTRACT_MEMORIES') &&
143| !toolUseContext.agentId &&
144| isExtractModeActive()
145| ) {
146| // Fire-and-forget in both interactive and non-interactive. For -p/SDK,
147| // print.ts drains the in-flight promise after flushing the response
148| // but before gracefulShutdownSync (see drainPendingExtraction).
149| void extractMemoriesModule!.executeExtractMemories(
150| stopHookContext,
151| toolUseContext.appendSystemMessage,
152| )
153| }
154| if (!toolUseContext.agentId) {
155| void executeAutoDream(stopHookContext, toolUseContext.appendSystemMessage)
156| }
157| }
158|
159| // chicago MCP: auto-unhide + lock release at turn end.
160| // Main thread only — the CU lock is a process-wide module-level variable,
源码引用: src/services/extractMemories/extractMemories.ts · 第 510–570 行(共 616 行)
510| const trailing = pendingContext
511| pendingContext = undefined
512| if (trailing) {
513| logForDebugging(
514| '[extractMemories] running trailing extraction for stashed context',
515| )
516| await runExtraction({
517| context: trailing.context,
518| appendSystemMessage: trailing.appendSystemMessage,
519| isTrailingRun: true,
520| })
521| }
522| }
523| }
524|
525| // --- Public entry point (captured by extractor) ---
526|
527| async function executeExtractMemoriesImpl(
528| context: REPLHookContext,
529| appendSystemMessage?: AppendSystemMessageFn,
530| ): Promise<void> {
531| // Only run for the main agent, not subagents
532| if (context.toolUseContext.agentId) {
533| return
534| }
535|
536| if (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
537| if (process.env.USER_TYPE === 'ant' && !hasLoggedGateFailure) {
538| hasLoggedGateFailure = true
539| logEvent('tengu_extract_memories_gate_disabled', {})
540| }
541| return
542| }
543|
544| // Check auto-memory is enabled
545| if (!isAutoMemoryEnabled()) {
546| return
547| }
548|
549| // Skip in remote mode
550| if (getIsRemoteMode()) {
551| return
552| }
553|
554| // If an extraction is already in progress, stash this context for a
555| // trailing run (overwrites any previously stashed context — only the
556| // latest matters since it has the most messages).
557| if (inProgress) {
558| logForDebugging(
559| '[extractMemories] extraction in progress — stashing for trailing run',
560| )
561| logEvent('tengu_extract_memories_coalesced', {})
562| pendingContext = { context, appendSystemMessage }
563| return
564| }
565|
566| await runExtraction({ context, appendSystemMessage })
567| }
568|
569| extractor = async (context, appendSystemMessage) => {
570| const p = executeExtractMemoriesImpl(context, appendSystemMessage)
源码引用: src/cli/print.ts · 第 960–975 行(共 5595 行)
960| logHeadlessProfilerTurn()
961|
962| // Drain any in-flight memory extraction before shutdown. The response is
963| // already flushed above, so this adds no user-visible latency — it just
964| // delays process exit so gracefulShutdownSync's 5s failsafe doesn't kill
965| // the forked agent mid-flight. Gated by isExtractModeActive so the
966| // tengu_slate_thimble flag controls non-interactive extraction end-to-end.
967| if (feature('EXTRACT_MEMORIES') && isExtractModeActive()) {
968| await extractMemoriesModule!.drainPendingExtraction()
969| }
970|
971| gracefulShutdownSync(
972| lastMessage?.type === 'result' && lastMessage?.is_error ? 1 : 0,
973| )
974| }
975|
提取提示为何要先注入 memory 清单
extract agent 并不是直接拿整段 transcript 自由发挥,它在提示里先看到当前 memory 文件清单与摘要信息。这样做的目的有两个:第一,减少 agent 为了“看看已有记忆”而额外调用读取工具的回合浪费;第二,降低重复写入概率,让模型在决定新增/更新时有明确参照。memoryScan 提供 frontmatter 级元信息,formatMemoryManifest 再转为紧凑文本片段,正好兼顾成本与可读性。
这一步也体现了 extraction 与 recall 的分工:recall 关心给当前回答找最相关的少量文件,提取关心维护全局记忆目录的一致性。二者都基于 scanMemoryFiles,但提示目标和后续动作不同。
源码引用: src/memdir/memoryScan.ts · 第 21–95 行(共 95 行)
21| const MAX_MEMORY_FILES = 200
22| const FRONTMATTER_MAX_LINES = 30
23|
24| /**
25| * Scan a memory directory for .md files, read their frontmatter, and return
26| * a header list sorted newest-first (capped at MAX_MEMORY_FILES). Shared by
27| * findRelevantMemories (query-time recall) and extractMemories (pre-injects
28| * the listing so the extraction agent doesn't spend a turn on `ls`).
29| *
30| * Single-pass: readFileInRange stats internally and returns mtimeMs, so we
31| * read-then-sort rather than stat-sort-read. For the common case (N ≤ 200)
32| * this halves syscalls vs a separate stat round; for large N we read a few
33| * extra small files but still avoid the double-stat on the surviving 200.
34| */
35| export async function scanMemoryFiles(
36| memoryDir: string,
37| signal: AbortSignal,
38| ): Promise<MemoryHeader[]> {
39| try {
40| const entries = await readdir(memoryDir, { recursive: true })
41| const mdFiles = entries.filter(
42| f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
43| )
44|
45| const headerResults = await Promise.allSettled(
46| mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
47| const filePath = join(memoryDir, relativePath)
48| const { content, mtimeMs } = await readFileInRange(
49| filePath,
50| 0,
51| FRONTMATTER_MAX_LINES,
52| undefined,
53| signal,
54| )
55| const { frontmatter } = parseFrontmatter(content, filePath)
56| return {
57| filename: relativePath,
58| filePath,
59| mtimeMs,
60| description: frontmatter.description || null,
61| type: parseMemoryType(frontmatter.type),
62| }
63| }),
64| )
65|
66| return headerResults
67| .filter(
68| (r): r is PromiseFulfilledResult<MemoryHeader> =>
69| r.status === 'fulfilled',
70| )
71| .map(r => r.value)
72| .sort((a, b) => b.mtimeMs - a.mtimeMs)
73| .slice(0, MAX_MEMORY_FILES)
74| } catch {
75| return []
76| }
77| }
78|
79| /**
80| * Format memory headers as a text manifest: one line per file with
81| * [type] filename (timestamp): description. Used by both the recall
82| * selector prompt and the extraction-agent prompt.
83| */
84| export function formatMemoryManifest(memories: MemoryHeader[]): string {
85| return memories
86| .map(m => {
87| const tag = m.type ? `[${m.type}] ` : ''
88| const ts = new Date(m.mtimeMs).toISOString()
89| return m.description
90| ? `- ${tag}${m.filename} (${ts}): ${m.description}`
91| : `- ${tag}${m.filename} (${ts})`
92| })
93| .join('\n')
94| }
95|
源码引用: src/services/extractMemories/extractMemories.ts · 第 367–410 行(共 616 行)
367| 'tengu_moth_copse',
368| false,
369| )
370|
371| const canUseTool = createAutoMemCanUseTool(memoryDir)
372| const cacheSafeParams = createCacheSafeParams(context)
373|
374| // Only run extraction every N eligible turns (tengu_bramble_lintel, default 1).
375| // Trailing extractions (from stashed contexts) skip this check since they
376| // process already-committed work that should not be throttled.
377| if (!isTrailingRun) {
378| turnsSinceLastExtraction++
379| if (
380| turnsSinceLastExtraction <
381| (getFeatureValue_CACHED_MAY_BE_STALE('tengu_bramble_lintel', null) ?? 1)
382| ) {
383| return
384| }
385| }
386| turnsSinceLastExtraction = 0
387|
388| inProgress = true
389| const startTime = Date.now()
390| try {
391| logForDebugging(
392| `[extractMemories] starting — ${newMessageCount} new messages, memoryDir=${memoryDir}`,
393| )
394|
395| // Pre-inject the memory directory manifest so the agent doesn't spend
396| // a turn on `ls`. Reuses findRelevantMemories' frontmatter scan.
397| // Placed after the throttle gate so skipped turns don't pay the scan cost.
398| const existingMemories = formatMemoryManifest(
399| await scanMemoryFiles(memoryDir, createAbortController().signal),
400| )
401|
402| const userPrompt =
403| feature('TEAMMEM') && teamMemoryEnabled
404| ? buildExtractCombinedPrompt(
405| newMessageCount,
406| existingMemories,
407| skipIndex,
408| )
409| : buildExtractAutoOnlyPrompt(
410| newMessageCount,
源码引用: src/services/extractMemories/prompts.ts · 第 31–90 行(共 155 行)
31| existingMemories.length > 0
32| ? `\n\n## Existing memory files\n\n${existingMemories}\n\nCheck this list before writing — update an existing file rather than creating a duplicate.`
33| : ''
34| return [
35| `You are now acting as the memory extraction subagent. Analyze the most recent ~${newMessageCount} messages above and use them to update your persistent memory systems.`,
36| '',
37| `Available tools: ${FILE_READ_TOOL_NAME}, ${GREP_TOOL_NAME}, ${GLOB_TOOL_NAME}, read-only ${BASH_TOOL_NAME} (ls/find/cat/stat/wc/head/tail and similar), and ${FILE_EDIT_TOOL_NAME}/${FILE_WRITE_TOOL_NAME} for paths inside the memory directory only. ${BASH_TOOL_NAME} rm is not permitted. All other tools — MCP, Agent, write-capable ${BASH_TOOL_NAME}, etc — will be denied.`,
38| '',
39| `You have a limited turn budget. ${FILE_EDIT_TOOL_NAME} requires a prior ${FILE_READ_TOOL_NAME} of the same file, so the efficient strategy is: turn 1 — issue all ${FILE_READ_TOOL_NAME} calls in parallel for every file you might update; turn 2 — issue all ${FILE_WRITE_TOOL_NAME}/${FILE_EDIT_TOOL_NAME} calls in parallel. Do not interleave reads and writes across multiple turns.`,
40| '',
41| `You MUST only use content from the last ~${newMessageCount} messages to update your persistent memories. Do not waste any turns attempting to investigate or verify that content further — no grepping source files, no reading code to confirm a pattern exists, no git commands.` +
42| manifest,
43| ].join('\n')
44| }
45|
46| /**
47| * Build the extraction prompt for auto-only memory (no team memory).
48| * Four-type taxonomy, no scope guidance (single directory).
49| */
50| export function buildExtractAutoOnlyPrompt(
51| newMessageCount: number,
52| existingMemories: string,
53| skipIndex = false,
54| ): string {
55| const howToSave = skipIndex
56| ? [
57| '## How to save memories',
58| '',
59| 'Write each memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
60| '',
61| ...MEMORY_FRONTMATTER_EXAMPLE,
62| '',
63| '- Organize memory semantically by topic, not chronologically',
64| '- Update or remove memories that turn out to be wrong or outdated',
65| '- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
66| ]
67| : [
68| '## How to save memories',
69| '',
70| 'Saving a memory is a two-step process:',
71| '',
72| '**Step 1** — write the memory to its own file (e.g., `user_role.md`, `feedback_testing.md`) using this frontmatter format:',
73| '',
74| ...MEMORY_FRONTMATTER_EXAMPLE,
75| '',
76| '**Step 2** — add a pointer to that file in `MEMORY.md`. `MEMORY.md` is an index, not a memory — each entry should be one line, under ~150 characters: `- [Title](file.md) — one-line hook`. It has no frontmatter. Never write memory content directly into `MEMORY.md`.',
77| '',
78| '- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep the index concise',
79| '- Organize memory semantically by topic, not chronologically',
80| '- Update or remove memories that turn out to be wrong or outdated',
81| '- Do not write duplicate memories. First check if there is an existing memory you can update before writing a new one.',
82| ]
83|
84| return [
85| opener(newMessageCount, existingMemories),
86| '',
87| 'If the user explicitly asks you to remember something, save it immediately as whichever type fits best. If they ask you to forget something, find and remove the relevant entry.',
88| '',
89| ...TYPES_SECTION_INDIVIDUAL,
90| ...WHAT_NOT_TO_SAVE_SECTION,
本章小结与延伸
memory-extraction = 回合末 fork agent 写盘。下一章 memdir-commands 读 /memory 与 /remember UI。 继续学习: