diff --git a/memory/project_mode_sop.md b/memory/project_mode_sop.md index e4494e4b..86cc9f7f 100644 --- a/memory/project_mode_sop.md +++ b/memory/project_mode_sop.md @@ -7,9 +7,14 @@ Project Mode = 跨会话保持项目认知的工作模式 ## 进入 -用户表示「进入/切换到 <项目名> 项目」时(下文路径一律以 cwd 为基准,cwd 即 GA 的 temp 目录;禁写 `temp/xxx` 前缀,会嵌套出 temp/temp): +锚 = `./.active_project.<宿主pid>`,只对当前 GA 进程有效:GA 关闭即自动失活;多开 GA 各自激活不同项目,互不干扰。(下文路径一律以 cwd 为基准,cwd 即 GA 的 temp 目录;禁写 `temp/xxx` 前缀,会嵌套出 temp/temp) + +- 用户只说「进入项目模式」未指明项目:列出 `./projects/` 下各项目(名字 + memory 行数 + 最后修改时间),ask_user 让用户选定后再继续 +- 用户明确说「进入/切换到 <项目名> 项目」:视为已确认,直接执行: + 1. 建目录 `./projects/<项目名>/`,无则创建 `project_memory.md`(空文件即可) -2. 写文件锚:把 `<项目名>` 写入 `./.active_project` +2. 写文件锚,必须用 code_run(锚文件名含宿主 pid,ppid 即 GA 宿主进程): + `import os; open(f'./.active_project.{os.getppid()}', 'w').write('<项目名>')` 3. 回读 `project_memory.md` 全文,向用户复述项目现状 ## 期间纪律 @@ -20,5 +25,6 @@ Project Mode = 跨会话保持项目认知的工作模式 ## 离开 -用户表示「离开项目模式」时:清空 `./.active_project`(仅关闭激活态,项目目录与 `project_memory.md` 原样保留) +用户表示「离开项目模式」时:删除 `./.active_project.<宿主pid>`(仅关闭激活态,项目目录与 `project_memory.md` 原样保留) 切换到另一项目时无需先离开,直接按「进入」覆盖文件锚即可 +GA 关闭不需任何操作:锚随进程消亡自动失效,残留文件由插件下次启动时清扫 diff --git a/plugins/project_mode.py b/plugins/project_mode.py index c14d7e38..809b6e19 100644 --- a/plugins/project_mode.py +++ b/plugins/project_mode.py @@ -6,10 +6,11 @@ 由模型按 L1 中的指针与线索(行数/大小)自行判断是否用 file 工具读取。 利用 messages 是 list 引用的事实,直接 mutate 即反映到真正发给 LLM 的内容。 -激活态载体 = 文件锚 temp/.active_project(存当前项目名)。文件驱动,跨轮天生持久。 - - 文件存在且非空 → 项目模式激活 - - 进入:agent 读 project_mode_sop 后写该文件(SOP 指示) - - 退出:清空/删除该文件 +激活态载体 = 文件锚 temp/.active_project.<宿主pid>(存当前项目名)。PID 键控: + - 锚只对写它的那个 GA 进程有效 → 多开 GA 各自激活不同项目,互不可见 + - GA 关闭即自动失活(重启后 pid 变,旧锚作废);重新激活需经用户确认(SOP 指示) + - 进入:agent 读 project_mode_sop,经用户确认后写锚(code_run 中 os.getppid() 即宿主 pid) + - 退出:删除该文件。插件加载时清扫旧版无后缀锚与自己 pid 的前世残留(不碰他进程的锚) 目录约定: temp/projects/<项目名>/project_memory.md 单文件全文注入的项目记忆 @@ -20,7 +21,24 @@ import plugins.hooks as hooks _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) _TEMP = os.path.join(_PROJECT_ROOT, 'temp') -_ANCHOR = os.path.join(_TEMP, '.active_project') +_ANCHOR = os.path.join(_TEMP, f'.active_project.{os.getpid()}') + + +def _cleanup_stale_anchors(): + """清扫无主锚:仅删两类能纯靠文件名判定的——旧版无后缀的、自己 pid 的(刚启动 + 不可能写过锚,必是 pid 复用的前世残留)。他进程的锚一律不碰:故意不做存活探测, + os.kill(pid, 0) 在 Windows 上语义是终止进程而非探活,任何平台分支都不值得为 + 删除无害残留文件引入杀进程风险(激活判定只认自己 pid 的锚,残留不影响行为)。""" + import glob + for path in glob.glob(os.path.join(_TEMP, '.active_project*')): + pid = path.rsplit('.', 1)[-1] + if path != _ANCHOR and pid.isdigit(): + continue # 他进程的锚(含已死进程的),不碰 + try: os.remove(path) + except OSError: pass + + +_cleanup_stale_anchors() def _active_project():