用两次命令,证明留下来的不是旧对话
用户要求记住这个项目的报告习惯:“使用中文,先给核对结论,再列是否执行、退出码和证据。”思想篇已经区分了稳定偏好与本次结果。现在先保存偏好,再启动一个全新的检查会话,看它怎样重新进入请求。
正式实现位于公开仓库 examples/s09-memory,Python3.10+ 标准库即可运行默认路径:
python3 examples/s09-memory/user_code/practice.py remember --approve
python3 examples/s09-memory/user_code/practice.py check --decision allow
python3 examples/s09-memory/user_code/practice.py check --decision deny第一条命令读取可查看的 report-memory.json 候选,宿主用 --approve 确认保存;没有这个参数就拒绝写入。第二、三条命令各自启动新进程,计划、历史和许可重新建立;它们共享的是 workspace/.memory/memory.json,没有恢复第一次检查的执行位置。
允许时,真实程序输出42,报告先给结论,再列执行证据;拒绝时明确“未执行,不能核对”。主模型和子模型都是固定响应替身,格式变化由教学脚本展示,不证明真实模型一定理解或遵循偏好。文件、记忆保存、新进程加载和获准后的程序均真实执行。
save_memory:先准备整份候选,最后才改正式文件
保存入口接收宿主选定的目录、候选记录和明确布尔确认。候选含 name、description、keywords、body、source 五个字段;name 是稳定标识,source 保留这条偏好的来历。读取上限和字段校验由同源 helper 完成,不能从来源文字推断宿主已经批准。
本课把元数据和正文存在同一个版本化 JSON 中,目录在启动读取后生成。这样,更正一条记忆只涉及一个正式文件,不会出现“正文写了,第二份索引还没更新”的半提交。
真实执行轨迹回放;写入Worker临时虚拟文件,替换失败情形注入公开故障接口。
def save_memory(directory, candidate, approved=False):
"""宿主审核后整份替换单一文件;单写者教学实现,不让模型直接写目录。"""
item = validate_memory(candidate)
if approved is not True:
raise ValueError('宿主未确认保存,原文件不变。')
records = load_memory(directory)
replacement = [record for record in records if record['name'] != item['name']] + [item]
replacement = validate_records(replacement)
payload = json.dumps({'version': 1, 'records': replacement}, ensure_ascii=False, indent=2)
if len(payload.encode('utf-8')) > 64_000:
raise ValueError('新记忆文件超过64000字节。')
directory = Path(directory)
directory.mkdir(parents=True, exist_ok=True)
temporary = None
try:
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8', dir=directory, prefix='.memory-', delete=False) as stream:
temporary = Path(stream.name)
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, directory / 'memory.json')
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
return {'name': item['name'], 'count': len(replacement)}点击行号设置断点,再点“调试”;也可以直接运行。
教学准备与调用入口
上方源码保持原样。以下准备输入和调用入口,在单独文件名中执行;断点只回放上方源码。
from copy import deepcopy
import json
import os
from pathlib import Path
import re
import tempfile
from types import SimpleNamespace
scenario='new'
_temp=tempfile.TemporaryDirectory(prefix="lesson9-")
directory=Path(_temp.name)
report={"name":"report-format","description":"程序报告偏好","keywords":["核对","报告"],"body":"报告使用中文,先给核对结论,再列是否执行、退出码与证据。","source":"用户明确要求,宿主审核"}
sql={**report,"name":"sql-style","description":"SQL书写偏好","keywords":["SQL"],"body":"SQL关键字使用大写。"}
initial=[] if scenario=="new" else [report,sql]
(directory/"memory.json").write_text(json.dumps({"version":1,"records":initial},ensure_ascii=False),encoding="utf-8")
before_file=(directory/"memory.json").read_bytes()
def validate_memory(item):
fields = {'name', 'description', 'keywords', 'body', 'source'}
if type(item) is not dict or set(item) != fields:
raise ValueError('记忆必须含名称、描述、关键词、正文与来源。')
if type(item['name']) is not str or not re.fullmatch(r'[a-z][a-z0-9-]{0,39}', item['name']):
raise ValueError('名称必须是1到40位小写标识。')
for field, limit in [('description', 120), ('body', 1200), ('source', 120)]:
if type(item[field]) is not str or not item[field].strip() or len(item[field]) > limit:
raise ValueError(field + '为空或超过字符上限。')
keywords = item['keywords']
if type(keywords) is not list or not 1 <= len(keywords) <= 6 or any(type(k) is not str or not 2 <= len(k.strip()) <= 24 for k in keywords):
raise ValueError('需要1到6个长度为2到24的关键词。')
if len({k.strip().casefold() for k in keywords}) != len(keywords):
raise ValueError('关键词不能重复。')
return deepcopy(item)
def validate_records(records):
if type(records) is not list or len(records) > 20:
raise ValueError('项目最多保存20条记忆。')
checked = [validate_memory(item) for item in records]
if len({item['name'] for item in checked}) != len(checked):
raise ValueError('记忆名称不能重复。')
return sorted(checked, key=lambda item: item['name'])
def load_memory(directory):
target = Path(directory) / 'memory.json'
try:
with target.open('rb') as stream:
raw = stream.read(64_001)
except FileNotFoundError:
return []
if len(raw) > 64_000:
raise ValueError('记忆文件超过64000字节。')
data = json.loads(raw.decode('utf-8'))
if type(data) is not dict or set(data) != {'version', 'records'} or type(data['version']) is not int or data['version'] != 1:
raise ValueError('记忆文件版本或格式不支持。')
return validate_records(data['records'])
def memory_catalog(records):
return [{key: item[key] for key in ('name', 'description', 'keywords')}
for item in validate_records(records)]
def checked_catalog(catalog):
if type(catalog) is not list or len(catalog) > 20:
raise ValueError('目录格式或数量不支持。')
records = []
for item in catalog:
if type(item) is not dict or set(item) != {'name', 'description', 'keywords'}:
raise ValueError('目录只允许名称、描述和关键词。')
records.append(validate_memory({**item, 'body': '-', 'source': '-'}))
return memory_catalog(records)
records=load_memory(directory)
catalog=memory_catalog(records)
candidate={**report,"body":"更新后的报告偏好:先说明是否执行。"}
approved=scenario!="unapproved"
if scenario=="invalid":candidate["body"]=""
if scenario=="replace_error":
_fsync=os.fsync
def fail_replace(source,target):raise OSError("教学替换失败,旧文件保留")
os=SimpleNamespace(fsync=_fsync,replace=fail_replace)
question="核对输出并报告"
if scenario=="none":question="介绍天气"
if scenario=="limit":question="核对并检查SQL"
if scenario=="current":question="本次用一句话报告"
if scenario=="invalid" and 'save_memory'=="select_memories":catalog[0]["body"]="目录不应带正文"
selected=["report-format"]
if scenario=="none":selected=[]
if scenario=="unknown":selected.append("missing")
messages=[{"role":"user","content":question},{"role":"assistant","content":[{"type":"tool_use","id":"run_1","name":"run_main","input":{}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"run_1","content":"本次拒绝,未执行。","is_error":True}]}]
before=deepcopy(messages)
# 调用入口
try:
observed=save_memory(directory,candidate,approved)
print("返回:",observed)
except (ValueError,OSError) as error:
observed={"error":str(error)}
print(str(error))
assert messages==before
stored=load_memory(directory)
print("当前保存:",[item["name"] for item in stored])
- 校验稳定偏好、来源与宿主确认。
- 临时文件写完,原子替换正式JSON。
- 失败保留旧文件;成功才返回保存收据。
选择“更正同名”,先看 replacement:旧同名记录从候选列表移除,新条目放进同一个位置集合,数量不因为更正而增加。“未确认保存”会在读取旧文件之前停下;“无效候选”连临时文件都不会写。
在 os.replace(...) 设断点。停住时,临时文件已写完并关闭,正式 memory.json 仍是旧内容;下一步才尝试替换。选择“替换失败”,公开故障接口会抛出异常,清理临时文件,旧文件保持不变,没有保存收据。
实现最多接受20条记录,单条正文1200字符,整个文件64000个 UTF-8 字节。写临时文件后执行 flush/fsync,再做同目录替换;这是单写者教学实现,没有并发更新锁,也没有保证断电后的持久性。目录由可信宿主配置,不交给模型选择。
候选的结构合法,不代表内容适合长期保存。是否准确、是否已经过时、是否包含不该留下的信息,仍需要审核;本课没有自动从响应或摘要中提取记忆。要更正偏好,可以用 --candidate 提供审核后的 JSON,同名覆盖;默认没有注册模型可直接写记忆的工具。
select_memories:先让“相关”变成可检查的规则
新 check 启动时,load_memory 有界读取文件并检查版本、条目数量与字段;损坏文件直接报错,不当成空库继续。memory_catalog 再从记录生成只含名称、描述、关键词的目录。小 JSON 的正文此时已在宿主内存里,但还没有全部发送给模型。
select_memories 接收这个目录、当前问题和选择上限。它按名称顺序检查关键词是否出现在问题中,不区分大小写,默认最多两条;描述帮助人阅读目录,本例没有对描述做语义评分。
真实执行轨迹回放;启动文件仅在Worker临时虚拟目录,同源helper和输入可展开查看。
def select_memories(catalog, question, max_items=2):
"""只看元数据做可复现关键词匹配;不宣称语义检索或模型推理。"""
entries = checked_catalog(catalog)
if type(question) is not str or not question.strip() or type(max_items) is not int or not 1 <= max_items <= 3:
raise ValueError('需要当前问题和1到3条选择上限。')
query = question.casefold()
selected = []
for item in entries:
matched = any(keyword.strip().casefold() in query for keyword in item['keywords'])
if matched:
selected.append(item['name'])
if len(selected) == max_items:
break
return selected点击行号设置断点,再点“调试”;也可以直接运行。
教学准备与调用入口
上方源码保持原样。以下准备输入和调用入口,在单独文件名中执行;断点只回放上方源码。
from copy import deepcopy
import json
import os
from pathlib import Path
import re
import tempfile
from types import SimpleNamespace
scenario='match'
_temp=tempfile.TemporaryDirectory(prefix="lesson9-")
directory=Path(_temp.name)
report={"name":"report-format","description":"程序报告偏好","keywords":["核对","报告"],"body":"报告使用中文,先给核对结论,再列是否执行、退出码与证据。","source":"用户明确要求,宿主审核"}
sql={**report,"name":"sql-style","description":"SQL书写偏好","keywords":["SQL"],"body":"SQL关键字使用大写。"}
initial=[] if scenario=="new" else [report,sql]
(directory/"memory.json").write_text(json.dumps({"version":1,"records":initial},ensure_ascii=False),encoding="utf-8")
before_file=(directory/"memory.json").read_bytes()
def validate_memory(item):
fields = {'name', 'description', 'keywords', 'body', 'source'}
if type(item) is not dict or set(item) != fields:
raise ValueError('记忆必须含名称、描述、关键词、正文与来源。')
if type(item['name']) is not str or not re.fullmatch(r'[a-z][a-z0-9-]{0,39}', item['name']):
raise ValueError('名称必须是1到40位小写标识。')
for field, limit in [('description', 120), ('body', 1200), ('source', 120)]:
if type(item[field]) is not str or not item[field].strip() or len(item[field]) > limit:
raise ValueError(field + '为空或超过字符上限。')
keywords = item['keywords']
if type(keywords) is not list or not 1 <= len(keywords) <= 6 or any(type(k) is not str or not 2 <= len(k.strip()) <= 24 for k in keywords):
raise ValueError('需要1到6个长度为2到24的关键词。')
if len({k.strip().casefold() for k in keywords}) != len(keywords):
raise ValueError('关键词不能重复。')
return deepcopy(item)
def validate_records(records):
if type(records) is not list or len(records) > 20:
raise ValueError('项目最多保存20条记忆。')
checked = [validate_memory(item) for item in records]
if len({item['name'] for item in checked}) != len(checked):
raise ValueError('记忆名称不能重复。')
return sorted(checked, key=lambda item: item['name'])
def load_memory(directory):
target = Path(directory) / 'memory.json'
try:
with target.open('rb') as stream:
raw = stream.read(64_001)
except FileNotFoundError:
return []
if len(raw) > 64_000:
raise ValueError('记忆文件超过64000字节。')
data = json.loads(raw.decode('utf-8'))
if type(data) is not dict or set(data) != {'version', 'records'} or type(data['version']) is not int or data['version'] != 1:
raise ValueError('记忆文件版本或格式不支持。')
return validate_records(data['records'])
def memory_catalog(records):
return [{key: item[key] for key in ('name', 'description', 'keywords')}
for item in validate_records(records)]
def checked_catalog(catalog):
if type(catalog) is not list or len(catalog) > 20:
raise ValueError('目录格式或数量不支持。')
records = []
for item in catalog:
if type(item) is not dict or set(item) != {'name', 'description', 'keywords'}:
raise ValueError('目录只允许名称、描述和关键词。')
records.append(validate_memory({**item, 'body': '-', 'source': '-'}))
return memory_catalog(records)
records=load_memory(directory)
catalog=memory_catalog(records)
candidate={**report,"body":"更新后的报告偏好:先说明是否执行。"}
approved=scenario!="unapproved"
if scenario=="invalid":candidate["body"]=""
if scenario=="replace_error":
_fsync=os.fsync
def fail_replace(source,target):raise OSError("教学替换失败,旧文件保留")
os=SimpleNamespace(fsync=_fsync,replace=fail_replace)
question="核对输出并报告"
if scenario=="none":question="介绍天气"
if scenario=="limit":question="核对并检查SQL"
if scenario=="current":question="本次用一句话报告"
if scenario=="invalid" and 'select_memories'=="select_memories":catalog[0]["body"]="目录不应带正文"
selected=["report-format"]
if scenario=="none":selected=[]
if scenario=="unknown":selected.append("missing")
messages=[{"role":"user","content":question},{"role":"assistant","content":[{"type":"tool_use","id":"run_1","name":"run_main","input":{}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"run_1","content":"本次拒绝,未执行。","is_error":True}]}]
before=deepcopy(messages)
# 调用入口
try:
observed=select_memories(catalog,question,max_items=1 if scenario=="limit" else 2)
print("返回:",observed)
except (ValueError,OSError) as error:
observed={"error":str(error)}
print(str(error))
assert messages==before
- 只接收元数据目录与当前问题。
- 关键词命中后选择名称,达到上限停止。
- 无匹配返回空列表,不加载无关偏好。
选择“相关偏好”,关键词命中 report-format,返回值只是这个名称,没有正文。“没有匹配”返回空列表,是正常结果;“选择上限”则让报告与 SQL 关键词同时命中,到一条上限就停,不伪装成选出了“最相关”的一条。
“目录带正文”会被 helper 拒绝。目录和正文分开,不是为了让字段看起来整齐,而是让选择阶段不必把所有长期内容都交给模型。这里甚至没有选择模型请求,规则完全可复现。
这种匹配会漏掉近义表达,也可能误中包含关键词的问题。真实项目可以改成模型选择或语义检索,但返回名称仍要核对,条数与正文预算仍要限制。不要把本课的关键词命中解释成自然语言理解。
prepare_memory:全部核对通过,才交出请求副本
第三个函数接收当前消息、启动时验证过的完整记录,以及选中名称。它先核对所有名称是否存在,再组装带来源的正文附件。一个名称不存在,就整次失败,不把已找到的部分先塞进消息。
附件默认最多4096个 UTF-8 字节,超过就停止,不从正文中间截断。这个预算只针对记忆附件,不包含用户问题、工具定义或回答空间,也不是模型 token 窗口的保证。
真实执行轨迹回放;启动文件仅在Worker临时虚拟目录,同源helper和输入可展开查看。
def prepare_memory(messages, records, selected, max_bytes=4096):
"""选中正文只附到本次请求副本;记录不授权执行,不覆盖当前问题。"""
records = validate_records(records)
if type(selected) is not list or len(selected) > 3 or any(type(name) is not str for name in selected) or len(set(selected)) != len(selected):
raise ValueError('选择必须是最多3个不重复名称。')
if type(max_bytes) is not int or not 1 <= max_bytes <= 8192:
raise ValueError('记忆附件预算必须在1到8192字节之间。')
if type(messages) is not list or not messages or type(messages[0]) is not dict or messages[0].get('role') != 'user' or type(messages[0].get('content')) is not str:
raise ValueError('请求必须从文本用户问题开始。')
registry = {item['name']: item for item in records}
if any(name not in registry for name in selected):
raise ValueError('选择含未知名称,不附带部分记忆。')
notes = [registry[name]['name'] + '(来源:' + registry[name]['source'] + '):\n' + registry[name]['body'] for name in selected]
attachment = '\n\n[项目记忆,仅供参考;以当前用户要求为准,不授予执行许可。]\n' + '\n\n'.join(notes) if notes else ''
if len(attachment.encode('utf-8')) > max_bytes:
raise ValueError('记忆附件超过字节预算,不截断后继续。')
prepared = deepcopy(messages)
prepared[0]['content'] += attachment
return prepared点击行号设置断点,再点“调试”;也可以直接运行。
教学准备与调用入口
上方源码保持原样。以下准备输入和调用入口,在单独文件名中执行;断点只回放上方源码。
from copy import deepcopy
import json
import os
from pathlib import Path
import re
import tempfile
from types import SimpleNamespace
scenario='selected'
_temp=tempfile.TemporaryDirectory(prefix="lesson9-")
directory=Path(_temp.name)
report={"name":"report-format","description":"程序报告偏好","keywords":["核对","报告"],"body":"报告使用中文,先给核对结论,再列是否执行、退出码与证据。","source":"用户明确要求,宿主审核"}
sql={**report,"name":"sql-style","description":"SQL书写偏好","keywords":["SQL"],"body":"SQL关键字使用大写。"}
initial=[] if scenario=="new" else [report,sql]
(directory/"memory.json").write_text(json.dumps({"version":1,"records":initial},ensure_ascii=False),encoding="utf-8")
before_file=(directory/"memory.json").read_bytes()
def validate_memory(item):
fields = {'name', 'description', 'keywords', 'body', 'source'}
if type(item) is not dict or set(item) != fields:
raise ValueError('记忆必须含名称、描述、关键词、正文与来源。')
if type(item['name']) is not str or not re.fullmatch(r'[a-z][a-z0-9-]{0,39}', item['name']):
raise ValueError('名称必须是1到40位小写标识。')
for field, limit in [('description', 120), ('body', 1200), ('source', 120)]:
if type(item[field]) is not str or not item[field].strip() or len(item[field]) > limit:
raise ValueError(field + '为空或超过字符上限。')
keywords = item['keywords']
if type(keywords) is not list or not 1 <= len(keywords) <= 6 or any(type(k) is not str or not 2 <= len(k.strip()) <= 24 for k in keywords):
raise ValueError('需要1到6个长度为2到24的关键词。')
if len({k.strip().casefold() for k in keywords}) != len(keywords):
raise ValueError('关键词不能重复。')
return deepcopy(item)
def validate_records(records):
if type(records) is not list or len(records) > 20:
raise ValueError('项目最多保存20条记忆。')
checked = [validate_memory(item) for item in records]
if len({item['name'] for item in checked}) != len(checked):
raise ValueError('记忆名称不能重复。')
return sorted(checked, key=lambda item: item['name'])
def load_memory(directory):
target = Path(directory) / 'memory.json'
try:
with target.open('rb') as stream:
raw = stream.read(64_001)
except FileNotFoundError:
return []
if len(raw) > 64_000:
raise ValueError('记忆文件超过64000字节。')
data = json.loads(raw.decode('utf-8'))
if type(data) is not dict or set(data) != {'version', 'records'} or type(data['version']) is not int or data['version'] != 1:
raise ValueError('记忆文件版本或格式不支持。')
return validate_records(data['records'])
def memory_catalog(records):
return [{key: item[key] for key in ('name', 'description', 'keywords')}
for item in validate_records(records)]
def checked_catalog(catalog):
if type(catalog) is not list or len(catalog) > 20:
raise ValueError('目录格式或数量不支持。')
records = []
for item in catalog:
if type(item) is not dict or set(item) != {'name', 'description', 'keywords'}:
raise ValueError('目录只允许名称、描述和关键词。')
records.append(validate_memory({**item, 'body': '-', 'source': '-'}))
return memory_catalog(records)
records=load_memory(directory)
catalog=memory_catalog(records)
candidate={**report,"body":"更新后的报告偏好:先说明是否执行。"}
approved=scenario!="unapproved"
if scenario=="invalid":candidate["body"]=""
if scenario=="replace_error":
_fsync=os.fsync
def fail_replace(source,target):raise OSError("教学替换失败,旧文件保留")
os=SimpleNamespace(fsync=_fsync,replace=fail_replace)
question="核对输出并报告"
if scenario=="none":question="介绍天气"
if scenario=="limit":question="核对并检查SQL"
if scenario=="current":question="本次用一句话报告"
if scenario=="invalid" and 'prepare_memory'=="select_memories":catalog[0]["body"]="目录不应带正文"
selected=["report-format"]
if scenario=="none":selected=[]
if scenario=="unknown":selected.append("missing")
messages=[{"role":"user","content":question},{"role":"assistant","content":[{"type":"tool_use","id":"run_1","name":"run_main","input":{}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"run_1","content":"本次拒绝,未执行。","is_error":True}]}]
before=deepcopy(messages)
# 调用入口
try:
observed=prepare_memory(messages,records,selected,max_bytes=10 if scenario=="budget" else 4096)
print("本次请求副本已准备;原历史保持不变。")
except (ValueError,OSError) as error:
observed={"error":str(error)}
print(str(error))
assert messages==before
- 核对全部选择与附件字节预算。
- 只把选中正文附到本次请求副本。
- 原问题、工具交接与宿主许可保持独立。
在 prepared[0]['content'] += attachment 停住:prepared 已是深副本,当前用户问题仍在前面;下一步才附上相关记忆。第二条以后的工具请求和结果不改,tool_use_id 与 is_error 也保持原样。返回以后,调用者才获得本次可发送的副本。
“无需记忆”返回没有附件的副本,不额外写一段“没有相关记忆”的提示。“含未知名称”和“附件超限”都不交出部分结果。“当前要求不同”则让问题明确写着本次用一句话,旧偏好仍标为参考,没有替换用户要求。
正文里的优先约定需要模型理解,校验器不判断语义冲突。运行许可可以更硬一些:即使旧记忆声称以前已经允许,实际 run_main 仍经过第三课的宿主入口。测试用真实程序标记确认,拒绝时文件不会被创建。
装到第八课以后,为什么不会再被压缩掉?
项目记忆在启动时由宿主读入,保存在当前会话的独立快照中。第八课的 prepare_context 只整理活跃历史;每次 ask 再生成带当前计划与记忆的请求副本:
# prepare_context 仍在原 run_loop 的 prepare 入口执行。
# 这里是 ask 内的装配顺序,三者不互相修改原始状态。
prepared = prepare_messages(history, plan_state)
prepared = prepare_memory(prepared, records, selected)
response = ask_model(prepared)因此,摘要没有复述报告偏好,也不影响下一次重新附带它。活跃 messages 没有保存这份附件,连续请求就不会变成一份、两份、三份相同正文。原循环、工具分发、Hook 和权限入口均未改写,也没有复制另一个 Memory 专用循环。
system 只追加项目记忆元数据目录,正文只进父请求副本;子任务仍使用独立系统说明和历史,不自动继承父记忆。当前计划仍来自第五课 state;记忆文件不把任务标成完成。
这里还要区分两种预算:第八课的3200字节只计活跃历史,第九课的4096字节只计记忆附件。最终模型请求还包括 system、工具与计划,不能把两个教学阈值当成真实 API 的容量保证。
新会话、旧偏好,分别怎么验证?
可以换一个问题,观察当前要求怎样覆盖默认格式:
python3 examples/s09-memory/user_code/practice.py check --decision allow --question '检查并核对,本次用一句话报告,运行前问我。'
python3 examples/s09-memory/user_code/verify_practice.py
python3 examples/s09-memory/user_code/verify_fixtures.py固定替身会改成一句话,仍须取得这次运行许可。若把程序改成输出43,即使记忆写着“以前输出42”,当前完整输出也会被判为不一致;输出多一行,同样不能只取最后的42就判断一致。
公开20组测试还验证了另一个 Python 进程重新加载、同名更正、项目目录隔离、损坏文件不覆盖、替换失败保留旧文件、选择错误不改原历史,以及压缩后重新附带正文。14组教学输入原样执行三个函数,浏览器文件只在 Worker 临时虚拟目录里,不访问宿主项目。
固定 Anthropic SDK 1.13.0 的模拟 HTTP 对照允许/拒绝各8次请求,父5次、子3次;断言目录没有正文、父请求只带相关正文、子任务没有自动继承,以及原历史不累积附件。没有访问在线模型。显式 --live 才接主/子模型,以及容量压力下可能需要的摘要模型;本例没有额外的记忆选择或提取模型请求。
文件在新 check 才重新加载,当前会话不会热更新。要改变长期偏好,应审核同名新候选;一次临时换格式不会自动保存。课程还讨论自动提取与合并,本课先把“谁确认、保存了什么、这次加载了什么”做成可追踪的路径,没有让摘要或模型响应自行改写长期记录。
资料核对于2026-10-19:课程 Memory与Claude Code 官方记忆说明。官方自动记忆的文件组织和加载能力与本例不同;这里的 JSON、人工确认、稳定关键词顺序与上限均是本册独立设计。