@zhushanwen/pi-rename-session 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -21
- package/package.json +8 -2
- package/skills/rename-session-ext-config/SKILL.md +131 -0
- package/src/__tests__/commands.test.ts +73 -30
- package/src/__tests__/index.test.ts +343 -98
- package/src/__tests__/llm.test.ts +592 -39
- package/src/__tests__/pure.test.ts +490 -77
- package/src/commands.ts +17 -5
- package/src/index.ts +52 -25
- package/src/llm.ts +247 -59
- package/src/pure.ts +299 -69
- package/vitest.config.ts +5 -1
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @zhushanwen/pi-rename-session
|
|
2
2
|
|
|
3
|
-
Pi rename-session 扩展 — 新 session
|
|
3
|
+
Pi rename-session 扩展 — 新 session 首个成功 round 完成后,自动生成 slug 式会话标题并落库(`setSessionName`),让 session 列表摆脱默认的日期/序号占位,一眼可辨。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
- 新 session
|
|
8
|
-
-
|
|
7
|
+
- 新 session 的**首个成功 round 末**自动生成 slug 式标题(名词/动名词词组,非完整句子;英文小写 kebab-case;跟随对话语言)
|
|
8
|
+
- **触发时机**:只在 round 的最终 turn(`stopReason === "stop"`)评估——工具中间轮 / error / aborted / length 轮不评估,error 轮延迟到下一个成功轮命名
|
|
9
|
+
- **两段输入**:`[user(首条 prompt), assistant(最终回复)]` 两段信号(各截断 4000 码点),不含 toolCall/toolResult 过程数据,token 成本不随工具数增长
|
|
10
|
+
- **独立选模**:标题生成用独立的 `ModelSelector` 配置(仅支持 `ref` 精确指定 provider/model),不搭便车主 session 的昂贵模型
|
|
11
|
+
- **可靠性行为**:固定 30s 超时;落库前重查手动名(防覆盖 LLM 调用窗口内的竞态);任何失败静默跳过保留原 label,绝不阻断 agent 循环
|
|
9
12
|
- 标题直接 `setSessionName` 落库,不进 session history(不污染对话记录)
|
|
10
|
-
- fire-and-forget:任何失败(LLM 调用 / 提取 / auth / 读取)都静默跳过,保留原 label,绝不阻断 agent 循环
|
|
11
13
|
- **子 session 自动排除**:subagent 子进程 session 不触发 rename(避免给临时产物起名)
|
|
12
14
|
|
|
13
15
|
## 安装
|
|
@@ -16,25 +18,99 @@ Pi rename-session 扩展 — 新 session 首 turn 完成后,自动生成会话
|
|
|
16
18
|
pi install npm:@zhushanwen/pi-rename-session
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
##
|
|
21
|
+
## 配置
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
配置文件:`<agentDir>/config/rename-session-ext-config.json`(`<agentDir>` 默认 `~/.pi/agent`,`PI_CODING_AGENT_DIR` 可覆盖;xyz-agent 隔离环境为 `~/.xyz-agent/pi/agent`)。
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"model": { "type": "ref", "ref": "deepseek/deepseek-chat" },
|
|
29
|
+
"maxTitleLength": 50,
|
|
30
|
+
"thinkingLevel": "off"
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| 字段 | 类型 | 默认 | 说明 |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `enabled` | `boolean` | `false` | 自动重命名开关(受 flag 文件覆盖,见下) |
|
|
37
|
+
| `model` | `ModelSelector` | `{ "type": "ref", "ref": "" }` | 标题生成模型,仅支持精确指定 `{type:"ref", ref:"provider/modelId"}` |
|
|
38
|
+
| `maxTitleLength` | `number` | `50` | 标题最大长度(Unicode 码点数,须正整数) |
|
|
39
|
+
| `thinkingLevel` | `ModelThinkingLevel` | `"off"` | 标题 LLM 的 thinking 级别(`off` = 不传 reasoning,provider 默认) |
|
|
40
|
+
|
|
41
|
+
文件缺失/坏 JSON 返回默认值,不抛错。改完保存即生效(mtime 读时刷新,每个 `turn_end` 重新 load)。
|
|
42
|
+
|
|
43
|
+
## 开关优先级(重要)
|
|
44
|
+
|
|
45
|
+
`enabled` 有两层来源,优先级从高到低:
|
|
46
|
+
|
|
47
|
+
1. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):xyz-agent runtime 的开关契约——桌面端 SystemPage 开关、首启默认开启都写这个文件。**xyz-agent 用户请通过桌面端开关或 `/auto-rename` 命令管理,不要手改 JSON 的 `enabled`**(flag 存在时永远视为开,手改会被覆盖)。
|
|
48
|
+
2. **config 的 `enabled` 字段**(默认 false):flag 不存在时生效,是原生 pi CLI 用户的开关。
|
|
28
49
|
|
|
29
|
-
|
|
50
|
+
## 命令
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
/auto-rename # 查看当前状态
|
|
54
|
+
/auto-rename on # 开启(创建 flag 文件)
|
|
55
|
+
/auto-rename off # 关闭(写 config.enabled=false + 删 flag,双写同步)
|
|
56
|
+
```
|
|
30
57
|
|
|
31
58
|
## 工作原理
|
|
32
59
|
|
|
33
|
-
1. **监听 `turn_end
|
|
34
|
-
2. **开关 + subagent
|
|
35
|
-
3.
|
|
36
|
-
4.
|
|
37
|
-
5.
|
|
60
|
+
1. **监听 `turn_end`**:pi 每个 iteration 结束都发一次 turn_end(工具中间轮、最终轮、异常轮各一次)。
|
|
61
|
+
2. **开关 + subagent 过滤**:开关关闭(flag 不存在且 `enabled=false`)直接返回;session 路径含 `subagents` 段视为子进程 session,跳过。
|
|
62
|
+
3. **O(1) 快速路径**:只有 `stopReason === "stop"` 的 turn 才继续——**rename 一定在 round 末触发**(最终 turn 的 message 即最终 assistant 回复,final text 零遍历可得),不会在首个 iteration 中途命名。
|
|
63
|
+
4. **首 round 判定**:session entries 中成功(stop)assistant 回复数 === 1 才触发(后续 round 不重复 rename;error 轮的 assistant 回复不计数,延迟到下一个成功轮)。
|
|
64
|
+
5. **两段输入构造**:`[user(首条 prompt), assistant(最终回复文本), user(instruction)]`——任务意图 + 轮次结论恰好与标题语义对齐,不含 toolCall/toolResult 过程数据;两段文本各截断 4000 Unicode 码点(中文场景约 4k token/段,成本可控且不随工具数增长)。assistant 段为空(纯工具结束的 round)时降级为两条。
|
|
65
|
+
6. **LLM 生成 slug 标题**:独立精简 system prompt(<200 字符的 slug 词组约束,非整个 agent prompt)+ instruction(正反例 few-shot,作为追加 user message 发送)+ `tools: []` + `maxTokens: 64`,按 `config.model` 独立选模发起一次 LLM 调用;固定 30s 超时(超时归一为失败,走静默跳过)。
|
|
66
|
+
7. **落库**:cleanTitle 清洗(去首尾引号 / markdown 强调标记 / 句尾标点、空白归一、按码点截断)后 `setSessionName` 写入。**落库前重查** `pi.getSessionName()`——LLM 调用窗口(2-30s)内用户手动命名的竞态由此兜住,已有名则 skip 不覆盖。**不**写入 session history,对话记录不受影响。
|
|
67
|
+
|
|
68
|
+
### 可靠性行为
|
|
69
|
+
|
|
70
|
+
rename 是 best-effort 副作用,任何失败静默跳过、绝不阻断 agent 循环:
|
|
71
|
+
|
|
72
|
+
| 情形 | 行为 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| 中间 iteration(工具轮)的 turn_end | skip(stopReason=toolUse),round 末才评估 |
|
|
75
|
+
| error / aborted / length 轮 | skip(stopReason=<X>),error 上下文不用于命名,延迟到下一个成功轮 |
|
|
76
|
+
| 非 round-1(成功回复数 ≠ 1) | skip(count=N),一次性语义 |
|
|
77
|
+
| LLM 调用失败 / 超过 30s | 记录失败日志,保留原 label(不做重试;用户可手动命名) |
|
|
78
|
+
| 标题清洗后为空 | skip(title empty) |
|
|
79
|
+
| 落库前发现已有手动名 | skip(name exists),不覆盖 |
|
|
80
|
+
| 标题模型不可用 | 记日志静默跳过 |
|
|
81
|
+
|
|
82
|
+
### debug 证据链(`XYZ_AGENT_DEBUG=1`)
|
|
83
|
+
|
|
84
|
+
`console.warn` 输出,前缀 `[rename-session]`。下列 8 条 debug 日志的**文案字面值是 E2E 断言硬契约**(变更须同步 `e2e/` 场景脚本与单测):
|
|
85
|
+
|
|
86
|
+
| # | 日志 | 发出侧 | 含义 |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| 1 | `skip: stopReason=<r>` | handler(带 `turnIndex=<n>`) | 快速路径拦截(toolUse/error/aborted/length) |
|
|
89
|
+
| 2 | `skip: count=<n>` | handler(带 turnIndex) | 非首成功 round |
|
|
90
|
+
| 3 | `skip: name exists` | handler(带 turnIndex) | 落库前防覆盖命中 |
|
|
91
|
+
| 4 | `renamed to "<title>"` | handler(带 turnIndex) | 标题生成并落库成功(index.ts `.then()` 内 `setSessionName` 之后打出;竞态命中时只打 #3,无此条) |
|
|
92
|
+
| 5 | `skip: no user prompt` | llm | session 无 user message(理论不发生) |
|
|
93
|
+
| 6 | `skip: title empty` | llm | cleanTitle 清洗后为空 |
|
|
94
|
+
| 7 | `LLM request messages: <JSON>` | llm | 传给 callLLM 的 messages 内省(role + text 的 head 200 码点 + … + tail 100 码点预览,截断单位与 truncateForTitle 统一为 Unicode 码点),在请求发起前打出 |
|
|
95
|
+
| 8 | `rename with model <provider>/<id>` | llm | 成功路径模型记录(原常开日志;为避免污染 Pi 输入框改为 debug 输出,带 t=ISO 时间戳) |
|
|
96
|
+
|
|
97
|
+
另有两条**非 debug 常开**日志:`rename LLM call failed: <err>`(调用失败/超时;超时时 llm-shared callLLM 内部的 extractText 将空错误文本归一为 `unknown error`——extension 侧 `result.error ?? "unknown error"` 只兜 null/undefined,空串兜底发生在 llm-shared 层)、`model not available, skipping`(选模失败)。handler 侧日志带 `t=<ISO时间>` 与 `turnIndex`;llm 侧带 `t=<ISO时间>`、无 turnIndex。
|
|
98
|
+
|
|
99
|
+
## E2E 验收
|
|
100
|
+
|
|
101
|
+
E2E 是本地人工触发的验收资产(真实 pi 进程 + 真实模型,不进常规 CI),覆盖五个场景:A1 触发时机证据链(流序/内容匹配/负向/行序/结构五重——结构断言 = 仅一条 LLM request + [user,assistant,user] 三元组)、A2 slug 风格 ×3、A3 防覆盖(静态/竞态/一次性)、A4 error 轮两阶段(`--session` 续跑)、A5 超时兜底(hang provider)。
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd extensions/rename-session
|
|
105
|
+
node e2e/run-a1.mjs # 单场景独立可跑(run-a1 ~ run-a5)
|
|
106
|
+
node e2e/run-all.mjs # 顺序全跑:单场景失败不阻断后续,汇总表 + exit code(任一失败(含 KEBAB_NON_COMPLIANT)→ 1)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- 环境要求与探针结论(auth 迁移 / RPC 协议格式 / `--session` 续跑 / 坏 provider 与 stub socket 配置写法):`e2e/README.md`
|
|
110
|
+
- harness API 与断言纯函数(单测 `e2e/harness.test.mjs` 随 vitest 跑):`e2e/harness.mjs`
|
|
111
|
+
- A2 的标题记录与人工抽查表(词组形态/语义相关/语言跟随三列):`e2e/RESULTS.md`(run-a2 自动追加,人工填写)
|
|
112
|
+
- 保留现场调试:`E2E_KEEP_TMP=1 node e2e/run-all.mjs`
|
|
113
|
+
- 测试模型固定 `xiaomi-token-plan-cn/mimo-v2.5-pro`(项目规范,禁 kimi)
|
|
38
114
|
|
|
39
115
|
## 子 session 自动排除
|
|
40
116
|
|
|
@@ -48,9 +124,20 @@ rename-session/
|
|
|
48
124
|
├── package.json
|
|
49
125
|
├── vitest.config.ts
|
|
50
126
|
├── README.md
|
|
127
|
+
├── e2e/ # E2E 验收资产(本地人工触发,不进常规 CI)
|
|
128
|
+
│ ├── README.md # 探针结论 + 运行指南
|
|
129
|
+
│ ├── harness.mjs # pi 进程/RPC/交错时间轴/断言纯函数
|
|
130
|
+
│ ├── harness.test.mjs # 断言纯函数单测(随 vitest 跑)
|
|
131
|
+
│ ├── run-a1.mjs ~ run-a5.mjs # A1-A5 场景脚本
|
|
132
|
+
│ ├── run-all.mjs # 总结 runner(汇总 + exit code)
|
|
133
|
+
│ ├── scenarios.test.mjs # A1-A5 的 vitest 包装(仅 e2e config 收录,不进常规 CI)
|
|
134
|
+
│ ├── vitest.e2e.config.ts # E2E 专用 vitest 入口(--config 显式指定,include 含 scenarios.test.mjs)
|
|
135
|
+
│ └── RESULTS.md # A2 标题记录 + 人工抽查表
|
|
136
|
+
├── skills/rename-session-ext-config/SKILL.md # 配置指南(pi 内 agent 可发现)
|
|
51
137
|
└── src/
|
|
52
|
-
├── index.ts # 工厂入口(注册 turn_end handler
|
|
53
|
-
├──
|
|
54
|
-
├── llm.ts # callRenameLLM
|
|
55
|
-
|
|
138
|
+
├── index.ts # 工厂入口(注册 turn_end handler + /auto-rename 命令)
|
|
139
|
+
├── commands.ts # /auto-rename on|off|status 命令
|
|
140
|
+
├── llm.ts # callRenameLLM / 两段输入构造 / debug 内省 / 超时
|
|
141
|
+
├── pure.ts # 纯函数(配置 / 首轮计数 / cleanTitle)
|
|
142
|
+
└── __tests__/ # 单测(pure / commands / llm mock / index 集成)
|
|
56
143
|
```
|
package/package.json
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-rename-session",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"pi": {
|
|
7
7
|
"extensions": [
|
|
8
8
|
"./index.ts"
|
|
9
9
|
],
|
|
10
|
-
"skills": [
|
|
10
|
+
"skills": [
|
|
11
|
+
"./skills"
|
|
12
|
+
]
|
|
11
13
|
},
|
|
12
14
|
"keywords": [
|
|
13
15
|
"pi-package"
|
|
14
16
|
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@zhushanwen/pi-llm-shared": "0.3.0"
|
|
19
|
+
},
|
|
15
20
|
"devDependencies": {
|
|
16
21
|
"vitest": "^4.1.8"
|
|
17
22
|
},
|
|
18
23
|
"files": [
|
|
19
24
|
"index.ts",
|
|
20
25
|
"src/**/*.ts",
|
|
26
|
+
"skills/",
|
|
21
27
|
"vitest.config.ts"
|
|
22
28
|
],
|
|
23
29
|
"peerDependencies": {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rename-session-ext-config
|
|
3
|
+
description: "配置 @zhushanwen/pi-rename-session(会话自动重命名)时加载。含配置文件路径、RenameSessionConfig schema、ModelSelector ref 精确指定、触发时机(首 turn)、maxTitleLength 约束、默认值、示例、生效时机、开关优先级(flag 覆盖)。触发词:配置重命名、rename 配置、自动标题、rename-session config、auto-rename 设置、首 turn、触发时机、开关不生效。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rename-session 配置指南
|
|
7
|
+
|
|
8
|
+
> @zhushanwen/pi-rename-session:新 session 首个成功 round 完成后,用独立小模型生成会话标题(不搭便车主 session 的昂贵模型)。
|
|
9
|
+
|
|
10
|
+
## 配置文件位置
|
|
11
|
+
|
|
12
|
+
`<agentDir>/config/rename-session-ext-config.json`
|
|
13
|
+
|
|
14
|
+
- `<agentDir>` = pi agent 目录(`PI_CODING_AGENT_DIR` 覆盖,默认 `~/.pi/agent`;xyz-agent 隔离环境为 `~/.xyz-agent/pi/agent`)
|
|
15
|
+
- 走 llm-shared 泛型 config(config/ 子目录 + getAgentDir 派生 + mtime+size 缓存 + 原子写)
|
|
16
|
+
- 文件缺失/坏 JSON 返回默认值,不抛错
|
|
17
|
+
|
|
18
|
+
## 何时触发重命名(重要)
|
|
19
|
+
|
|
20
|
+
**仅在新 session 的首个成功 round 完成后触发一次**(判定条件:round 最终 turn 的 `stopReason === "stop"`,且 session 内成功(stop)assistant 回复数 === 1)。
|
|
21
|
+
|
|
22
|
+
- 已存在的多 turn session **不会回溯重命名**——开启 `enabled` 后只对之后新建的 session 生效
|
|
23
|
+
- 每个 session 最多重命名一次(首个成功 round 后不再触发)
|
|
24
|
+
- 工具中间轮(`stopReason === "toolUse"`)不评估;error/aborted/length 轮延迟到下一个成功轮再命名
|
|
25
|
+
- 若首个成功 round 时 LLM 调用失败,静默跳过保留原标题,不重试
|
|
26
|
+
|
|
27
|
+
> 改完配置「没看到 session 被重命名」的常见原因:当前 session 已过首个成功 round。新建一个 session 测试。
|
|
28
|
+
|
|
29
|
+
## Schema
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface RenameSessionConfig {
|
|
33
|
+
enabled: boolean; // 自动重命名开关,默认 false
|
|
34
|
+
model: ModelSelector; // 标题生成模型,默认 { type: "ref", ref: "" }(未配置则解析不到,跳过 rename)
|
|
35
|
+
maxTitleLength: number; // 标题最大长度(Unicode 码点),默认 50
|
|
36
|
+
thinkingLevel: ModelThinkingLevel; // 标题 LLM 的 thinking 级别,默认 "off"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### ModelSelector(仅支持 ref 精确指定)
|
|
41
|
+
|
|
42
|
+
| type | 形式 | 语义 |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `ref` | `{type:"ref", ref:"provider/modelId"}` | 精确指定(需配 auth) |
|
|
45
|
+
|
|
46
|
+
不再支持 `fallback` / `available` / `scoped`。需要自动选模时请在调用方(如 permission 的 `"auto"`)自行基于 `ctx.modelRegistry` 实现。
|
|
47
|
+
|
|
48
|
+
### maxTitleLength 约束
|
|
49
|
+
|
|
50
|
+
必须是**正整数**(`Number.isInteger && > 0`)。传小数(`50.5`)、0、负数、非数字都会回落默认值 50。截断按 Unicode 码点(不会截断多字节字符)。
|
|
51
|
+
|
|
52
|
+
### thinkingLevel 取值
|
|
53
|
+
|
|
54
|
+
标题 LLM 的 thinking 级别,枚举 `off` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(pi 的 `ModelThinkingLevel`)。默认 `"off"`:直接透传给 llm-shared,由 llm-shared 映射为不传 reasoning(provider 默认行为);`minimal`~`max` 透传给 reasoning(provider 不支持时静默忽略)。缺失或非法值回落 `"off"`。
|
|
55
|
+
|
|
56
|
+
## 默认值
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "enabled": false, "model": { "type": "ref", "ref": "" }, "maxTitleLength": 50, "thinkingLevel": "off" }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 环境变量覆盖(容器化部署/CI-CD)
|
|
63
|
+
|
|
64
|
+
支持通过环境变量覆盖配置,适用于容器化部署、CI/CD 等场景。环境变量优先级最高,覆盖配置文件和 flag 文件。
|
|
65
|
+
|
|
66
|
+
| 环境变量 | 说明 | 示例值 |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `PI_RENAME_ENABLED` | 自动重命名开关 | `true` / `false` |
|
|
69
|
+
| `PI_RENAME_MODEL` | 模型引用(`provider/model` 格式,映射为 `{type:"ref", ref:"provider/model"}`) | `deepseek/chat` |
|
|
70
|
+
| `PI_RENAME_MAX_TITLE_LENGTH` | 标题最大长度(正整数) | `30` |
|
|
71
|
+
| `PI_RENAME_THINKING_LEVEL` | thinking 级别 | `minimal` / `high` |
|
|
72
|
+
|
|
73
|
+
**注意事项:**
|
|
74
|
+
- 环境变量值无效时静默忽略,回落到配置文件或默认值
|
|
75
|
+
- 环境变量优先级最高,即使 flag 文件存在,`PI_RENAME_ENABLED=false` 也会禁用重命名
|
|
76
|
+
- 环境变量每次调用时 live 读取,修改后无需重启进程(下一个 `turn_end` 生效)
|
|
77
|
+
- 环境变量只支持简单 `provider/model` 覆盖;ModelSelector 本身仅支持 ref 精确指定
|
|
78
|
+
|
|
79
|
+
**使用示例:**
|
|
80
|
+
```bash
|
|
81
|
+
# 容器化部署:启用重命名 + 指定便宜模型
|
|
82
|
+
PI_RENAME_ENABLED=true PI_RENAME_MODEL=deepseek/chat node app.js
|
|
83
|
+
|
|
84
|
+
# CI/CD 禁用重命名
|
|
85
|
+
PI_RENAME_ENABLED=false npm test
|
|
86
|
+
|
|
87
|
+
# 开发环境:使用轻量 thinking
|
|
88
|
+
PI_RENAME_ENABLED=true PI_RENAME_THINKING_LEVEL=minimal npm run dev
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 配置示例
|
|
92
|
+
|
|
93
|
+
固定用便宜模型生成标题:
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"enabled": true,
|
|
97
|
+
"model": { "type": "ref", "ref": "deepseek/deepseek-chat" },
|
|
98
|
+
"maxTitleLength": 50,
|
|
99
|
+
"thinkingLevel": "off"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
> 必须精确指定 `ref`;未配置或 ref 为空时解析不到模型,rename 会静默跳过。
|
|
104
|
+
|
|
105
|
+
## 配置生效时机
|
|
106
|
+
|
|
107
|
+
配置走 mtime+size 读时刷新(每个 `turn_end` 都重新 load)。改完 JSON 保存后,**下一个新 session 的首个成功 round** 即按新配置触发(已过首个成功 round 的 session 不受影响)。
|
|
108
|
+
|
|
109
|
+
**环境变量生效时机:** 环境变量每次调用时 live 读取(`process.env`),修改后无需重启进程,下一个 `turn_end` 即按新环境变量生效。
|
|
110
|
+
|
|
111
|
+
## 排除项
|
|
112
|
+
|
|
113
|
+
subagent 子进程 session 不重命名(`isSubagentSession` 判定 session 目录)——子 session 是临时产物,重命名会产生噪音。如果你发现某个 session 没被重命名,先确认它不是 subagent session。
|
|
114
|
+
|
|
115
|
+
## 开关优先级(重要)
|
|
116
|
+
|
|
117
|
+
`enabled` 有三层来源,优先级从高到低:
|
|
118
|
+
|
|
119
|
+
1. **环境变量 `PI_RENAME_ENABLED`**(最高优先级):适用于容器化部署、CI/CD 等场景。`true`/`false` 字符串,live 读取。覆盖 flag 文件和配置文件。
|
|
120
|
+
2. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):这是 xyz-agent runtime 的开关契约(SystemPage 开关 / 首启默认开启都写这个文件,live 检查每次 turn_end 生效)。**xyz-agent 用户不要手改 JSON 里的 enabled**——桌面端的开关状态存在 flag 文件里,手改 JSON 会被 flag 覆盖(flag 存在时永远视为开)。
|
|
121
|
+
3. **config 的 `enabled` 字段**(默认 false):flag 不存在时生效,是原生 pi CLI 用户的开关(手改 JSON 或 `/auto-rename on|off` 命令)。
|
|
122
|
+
|
|
123
|
+
`/auto-rename on` 只创建 flag;`/auto-rename off` 写 config.enabled=false + 删 flag(双写同步)。旧版升级用户:旧 flag 文件保留不动,仍作为开关生效,无需任何迁移操作。
|
|
124
|
+
|
|
125
|
+
## LLM 调用特性
|
|
126
|
+
|
|
127
|
+
- 独立 model(不搭便车主 session 模型)
|
|
128
|
+
- 独立精简 system prompt(<200 字符的 slug 词组约束 + 正反例 few-shot,非整个 agent prompt)
|
|
129
|
+
- 不传 tools(纯文本标题生成)
|
|
130
|
+
- fire-and-forget(不阻塞 turn_end handler)
|
|
131
|
+
- model 不可用 → 静默跳过(日志 `[rename-session] model not available, skipping`),不阻断主对话
|
|
@@ -2,66 +2,89 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { afterEach, describe, expect, it
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { clearConfigCache } from "@zhushanwen/pi-llm-shared";
|
|
6
8
|
|
|
7
9
|
import { executeAutoRenameCommand } from "../commands.js";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
|
-
* executeAutoRenameCommand
|
|
11
|
-
*
|
|
12
|
+
* executeAutoRenameCommand 读写 `<agentDir>/config/rename-session-ext-config.json`(经 llm-shared loadConfig/saveConfig,
|
|
13
|
+
* 路径走 getAgentDir)。用 PI_CODING_AGENT_DIR 隔离到临时目录,避免读写真实 ~/.pi/agent。
|
|
14
|
+
* 每次写入后 clearConfigCache 确保读盘(不命中 mtime 缓存),验证真实落盘行为。
|
|
12
15
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
...actual,
|
|
18
|
-
CONFIG: { ...actual.CONFIG, switchFilePath: tmpFile },
|
|
19
|
-
isEnabled: (p: string) => fs.existsSync(p),
|
|
20
|
-
setSwitch: actual.setSwitch,
|
|
21
|
-
};
|
|
22
|
-
});
|
|
16
|
+
describe("executeAutoRenameCommand", () => {
|
|
17
|
+
let tmpAgentDir: string;
|
|
18
|
+
let origEnv: string | undefined;
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "rename-cmd-"));
|
|
22
|
+
origEnv = process.env.PI_CODING_AGENT_DIR;
|
|
23
|
+
process.env.PI_CODING_AGENT_DIR = tmpAgentDir;
|
|
24
|
+
clearConfigCache();
|
|
25
|
+
});
|
|
26
26
|
|
|
27
|
-
describe("executeAutoRenameCommand", () => {
|
|
28
27
|
afterEach(() => {
|
|
29
|
-
|
|
28
|
+
if (origEnv === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
29
|
+
else process.env.PI_CODING_AGENT_DIR = origEnv;
|
|
30
|
+
clearConfigCache();
|
|
31
|
+
fs.rmSync(tmpAgentDir, { recursive: true, force: true });
|
|
30
32
|
});
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
function configPath(): string {
|
|
35
|
+
return path.join(tmpAgentDir, "config", "rename-session-ext-config.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("无参数 → 显示当前状态(默认关闭)+ 用法", () => {
|
|
33
39
|
const msg = executeAutoRenameCommand("");
|
|
34
40
|
expect(msg).toContain("自动重命名会话");
|
|
41
|
+
expect(msg).toContain("已关闭");
|
|
35
42
|
expect(msg).toContain("用法");
|
|
36
43
|
});
|
|
37
44
|
|
|
38
45
|
it("status → 同无参数", () => {
|
|
39
|
-
|
|
40
|
-
expect(msg).toContain("自动重命名会话");
|
|
46
|
+
expect(executeAutoRenameCommand("status")).toContain("已关闭");
|
|
41
47
|
});
|
|
42
48
|
|
|
43
|
-
it("on →
|
|
49
|
+
it("on → 创建 flag 文件(不写 config,避免残留 enabled=true)", () => {
|
|
44
50
|
const msg = executeAutoRenameCommand("on");
|
|
45
51
|
expect(msg).toContain("已开启");
|
|
46
|
-
|
|
52
|
+
// 开启走 flag 契约(live 覆盖源),不落 config
|
|
53
|
+
expect(fs.existsSync(path.join(tmpAgentDir, "auto-rename-enabled"))).toBe(true);
|
|
54
|
+
expect(fs.existsSync(configPath())).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("on 后 status 显示已开启(缓存一致,无需 clearConfigCache)", () => {
|
|
58
|
+
executeAutoRenameCommand("on");
|
|
59
|
+
expect(executeAutoRenameCommand("status")).toContain("已开启");
|
|
47
60
|
});
|
|
48
61
|
|
|
49
|
-
it("off →
|
|
50
|
-
|
|
62
|
+
it("off → 写 config.enabled=false 落盘 + 删除 flag(双写同步)", () => {
|
|
63
|
+
executeAutoRenameCommand("on");
|
|
51
64
|
const msg = executeAutoRenameCommand("off");
|
|
52
65
|
expect(msg).toContain("已关闭");
|
|
53
|
-
|
|
66
|
+
clearConfigCache();
|
|
67
|
+
const raw = JSON.parse(fs.readFileSync(configPath(), "utf-8"));
|
|
68
|
+
expect(raw.enabled).toBe(false);
|
|
69
|
+
expect(fs.existsSync(path.join(tmpAgentDir, "auto-rename-enabled"))).toBe(false);
|
|
54
70
|
});
|
|
55
71
|
|
|
56
72
|
it("enable/disable 作为 on/off 别名", () => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
expect(executeAutoRenameCommand("
|
|
73
|
+
executeAutoRenameCommand("enable");
|
|
74
|
+
clearConfigCache();
|
|
75
|
+
expect(executeAutoRenameCommand("status")).toContain("已开启");
|
|
76
|
+
executeAutoRenameCommand("disable");
|
|
77
|
+
clearConfigCache();
|
|
78
|
+
expect(executeAutoRenameCommand("status")).toContain("已关闭");
|
|
60
79
|
});
|
|
61
80
|
|
|
62
81
|
it("大小写不敏感(ON / Off)", () => {
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
executeAutoRenameCommand("ON");
|
|
83
|
+
clearConfigCache();
|
|
84
|
+
expect(executeAutoRenameCommand("status")).toContain("已开启");
|
|
85
|
+
executeAutoRenameCommand("Off");
|
|
86
|
+
clearConfigCache();
|
|
87
|
+
expect(executeAutoRenameCommand("status")).toContain("已关闭");
|
|
65
88
|
});
|
|
66
89
|
|
|
67
90
|
it("未知参数 → 提示用法", () => {
|
|
@@ -69,4 +92,24 @@ describe("executeAutoRenameCommand", () => {
|
|
|
69
92
|
expect(msg).toContain("未知参数");
|
|
70
93
|
expect(msg).toContain("用法");
|
|
71
94
|
});
|
|
95
|
+
|
|
96
|
+
it("on 不动 config(手写 enabled/model/maxTitleLength 均保留)", () => {
|
|
97
|
+
// 预置含自定义 model + maxTitleLength 的配置
|
|
98
|
+
fs.mkdirSync(path.dirname(configPath()), { recursive: true });
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
configPath(),
|
|
101
|
+
JSON.stringify({ enabled: false, model: { type: "ref", ref: "a/b" }, maxTitleLength: 30 }),
|
|
102
|
+
);
|
|
103
|
+
clearConfigCache();
|
|
104
|
+
|
|
105
|
+
executeAutoRenameCommand("on");
|
|
106
|
+
clearConfigCache();
|
|
107
|
+
|
|
108
|
+
// on 只建 flag,config 原封不动(enabled 仍 false,但 loadRenameConfig 被 flag 覆盖为 true)
|
|
109
|
+
const raw = JSON.parse(fs.readFileSync(configPath(), "utf-8"));
|
|
110
|
+
expect(raw.enabled).toBe(false);
|
|
111
|
+
expect(raw.model).toEqual({ type: "ref", ref: "a/b" });
|
|
112
|
+
expect(raw.maxTitleLength).toBe(30);
|
|
113
|
+
expect(executeAutoRenameCommand("status")).toContain("已开启");
|
|
114
|
+
});
|
|
72
115
|
});
|