@zhushanwen/pi-rename-session 0.9.1 → 0.9.3

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 CHANGED
@@ -4,7 +4,7 @@ Pi rename-session 扩展 — 三种触发模式下自动/自主为会话生成 s
4
4
 
5
5
  ## 功能
6
6
 
7
- - **三种触发模式**(`mode` 配置,默认 `first-stop` 零行为迁移):
7
+ - **三种触发模式**(`mode` 配置,默认 `first-stop`):
8
8
  - `first-stop`(默认):新 session 的**首个成功 round 末**自动生成 slug 式标题
9
9
  - `first-prompt`:**首条 user 消息发出即命名**(不等回复)——标题只基于 prompt 本身;pi 事件链 await handler,rename LLM 请求构造性早于首条 assistant 回复开始
10
10
  - `agent-tool`:**不自动生成**——注册 `rename_session` 工具,由 agent 在对话中自主改名(零额外 rename LLM 调用);工具改名允许覆盖任何既有名(agent 显式调用语义等同手动 rename)
@@ -23,7 +23,7 @@ pi install npm:@zhushanwen/pi-rename-session
23
23
 
24
24
  ## 配置
25
25
 
26
- 配置文件:`<agentDir>/config/rename-session-ext-config.json`(`<agentDir>` 默认 `~/.pi/agent`,`PI_CODING_AGENT_DIR` 可覆盖;xyz-agent 隔离环境为 `~/.xyz-agent/pi/agent`)。
26
+ 配置文件:`<agentDir>/config/rename-session-ext-config.json`(`<agentDir>` 默认 `~/.pi/agent`,`PI_CODING_AGENT_DIR` 可覆盖;taiji 隔离环境为 `~/.taiji/agent`)。
27
27
 
28
28
  ```json
29
29
  {
@@ -39,7 +39,7 @@ pi install npm:@zhushanwen/pi-rename-session
39
39
  |---|---|---|---|
40
40
  | `enabled` | `boolean` | `false` | 自动重命名开关(受 flag 文件覆盖,见下) |
41
41
  | `model` | `ModelSelector` | `{ "type": "ref", "ref": "" }` | 标题生成模型,仅支持精确指定 `{type:"ref", ref:"provider/modelId"}`;**空 ref 跟随会话主模型**(开箱即用),非空但解析失败(配错)才静默跳过 |
42
- | `mode` | `"first-prompt" \| "first-stop" \| "agent-tool"` | `"first-stop"` | 触发模式(三值互斥):首条请求即命名 / 首个成功 round 末命名(现状)/ agent 自主经 `rename_session` 工具命名。事件面每次事件 live 读(GUI 切换对活跃 session 的自动命名即时生效);工具注册面只在 pi 进程启动加载 extension 时求值一次(切换后已存活 session 的工具清单不回溯,残留工具由 execute 内 live mode 守卫拒绝并给恢复指引) |
42
+ | `mode` | `"first-prompt" \| "first-stop" \| "agent-tool"` | `"first-stop"` | 触发模式(三值互斥):首条请求即命名 / 首个成功 round 末命名(现状)/ agent 自主经 `rename_session` 工具命名。事件面每次事件 live 读(GUI 切换对活跃 session 的自动命名即时生效);工具注册面在 extension 工厂执行时求值(每 session 创建一次——taiji 每 session 一个 pi 进程;原生 pi CLI 同进程 /fork、/session 新建 session 亦重新求值),session 存活期内不回溯(pi 无 unregisterTool),切换后已存活 session 的残留工具由 execute 内 live mode 守卫拒绝并给恢复指引 |
43
43
  | `maxTitleLength` | `number` | `50` | 标题最大长度(Unicode 码点数,须正整数) |
44
44
  | `thinkingLevel` | `ModelThinkingLevel` | `"off"` | 标题 LLM 的 thinking 级别(`off` = 不传 reasoning,provider 默认) |
45
45
 
@@ -49,25 +49,23 @@ pi install npm:@zhushanwen/pi-rename-session
49
49
 
50
50
  `enabled` 有三层来源,优先级从高到低(`src/pure.ts` `loadRenameConfig`):
51
51
 
52
- 1. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):xyz-agent runtime 的开关契约——桌面端 SystemPage 开关、首启默认开启都写这个文件。**xyz-agent 用户请通过桌面端开关或 `/auto-rename` 命令管理,不要手改 JSON 的 `enabled`**(flag 存在时视为开,手改会被覆盖)。
52
+ 1. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):taiji runtime 的开关契约——桌面端 SystemPage 开关、首启默认开启都写这个文件。**taiji 用户请通过桌面端开关或 `/auto-rename` 命令管理,不要手改 JSON 的 `enabled`**(flag 存在时视为开,手改会被覆盖)。
53
53
  2. **config 的 `enabled` 字段**(默认 false):flag 不存在时生效,是原生 pi CLI 用户的开关。
54
54
  3. **默认值**(false):以上两层均未设置时。
55
55
 
56
- > [HISTORICAL] 原最高优先级的 `PI_RENAME_*` 环境变量覆盖层已删(全仓 0 生产 setter,4 键中 3 键从未被用过)——预置该前缀的环境变量不再有任何效果。rename-session 相关精简项裁决见原设计文档 rename-session-three-modes.md 附录 A(已删除,git 可追溯)。
57
-
58
56
  ## 命令
59
57
 
60
58
  ```
61
59
  /auto-rename # 查看当前状态
62
- /auto-rename on # 开启(创建 flag 文件)
63
- /auto-rename off # 关闭(写 config.enabled=false + 删 flag,双写同步)
60
+ /auto-rename on # 开启(创建 flag 文件;enable 别名等效)
61
+ /auto-rename off # 关闭(写 config.enabled=false + 删 flag,双写同步;disable 别名等效)
64
62
  ```
65
63
 
66
64
  ## 工作原理
67
65
 
68
66
  三个入口共用同一条落库管道(`callRenameLLM` → 防覆盖重查 → `setSessionName`),按 `mode` 分派:
69
67
 
70
- 1. **入口分派(事件面 live 读 mode)**:`message_end` 入口(first-prompt)过滤 `role === "user"`;`turn_end` 入口(first-stop)按下方流程;`agent-tool` 模式不激活自动命名逻辑(两 handler 常驻注册、命中即静默返回),改为 extension load 时注册 `rename_session` 工具。
68
+ 1. **入口分派(事件面 live 读 mode)**:`message_end` 入口(first-prompt)过滤 `role === "user"`;`turn_end` 入口(first-stop)按下方流程;`agent-tool` 模式不激活自动命名逻辑(两 handler 常驻注册、命中即静默返回),改为 extension 工厂执行时(每 session 创建一次)注册 `rename_session` 工具。
71
69
  2. **开关 + subagent 过滤**(自动路径共用):开关关闭(flag 不存在且 `enabled=false`)直接返回;session 路径含 `subagents` 段视为子进程 session,跳过。
72
70
  3. **O(1) 快速路径(first-stop)**:只有 `stopReason === "stop"` 的 turn 才继续——**rename 一定在 round 末触发**(最终 turn 的 message 即最终 assistant 回复,final text 零遍历可得),不会在首个 iteration 中途命名。first-prompt 入口的对应守卫:首条 user 判定 = handler 执行时 `getEntries()` 中 user message 计数 === 0(pi 的 extension handler 先于该条 message 的 entries append 执行,本条即 session 首条 user;steering/follow-up 消息到达时首条已入 entries,天然不重复触发)。
73
71
  4. **首 round 判定(first-stop)**:session entries 中成功(stop)assistant 回复数 === 1 才触发(后续 round 不重复 rename;error 轮的 assistant 回复不计数,延迟到下一个成功轮)。
@@ -97,10 +95,10 @@ rename 是 best-effort 副作用,任何失败静默跳过、绝不阻断 agent
97
95
 
98
96
  所有日志经 `@zhushanwen/pi-extension-logger` 输出,两个通道:
99
97
 
100
- - **appendEntry(session entry,常开)**:`logger.warn` / `logger.error` 写入 session JSONL 的 custom entry(`type: "custom"`、`customType: "rename-session:log"`),不进 LLM 上下文、不显 TUI。`data.message` 由 logger 自动补 `[rename-session]` 前缀,格式为 `msg + 结构化 data`(error 等详情在 entry `data.data` 字段,不冒号拼接进 message)。
101
- - **文件日志(`XYZ_AGENT_DEBUG=1` 时)**:`<agentDir>/logs/rename-session-<date>.log`。
98
+ - **appendEntry(session entry,常开)**:`logger.warn` / `logger.error` 写入 session JSONL 的 custom entry(`type: "custom"`、`customType: "rename-session:log"`),不进 LLM 上下文、不显 TUI。`data.message` 由 logger 自动补 `[rename-session]` 前缀,格式为 `msg + 结构化 data`(error 等详情在 entry `data.data` 字段,不冒号拼接进 message)。extension-logger 对同 key 重复文案有 60s 窗口 ×10 条限流(超限抑制,窗口过期后补一条聚合摘要;文案含变量值如 `count=<n>` 时 key 不同,不触及)。
99
+ - **文件日志(`TAIJI_AGENT_DEBUG=1` 时)**:`<agentDir>/logs/rename-session-<date>.log`。
102
100
 
103
- 下列 debug 日志(仅 `XYZ_AGENT_DEBUG=1` 时经 `logger.warn` 发出)的**文案字面值是 E2E 断言硬契约**(断言对象 = appendEntry entry 的 message 内容;变更须同步 `e2e/` 场景脚本与单测):
101
+ 下列 debug 日志(仅 `TAIJI_AGENT_DEBUG=1` 时经 `logger.warn` 发出)的**文案字面值是 E2E 断言硬契约**(断言对象 = appendEntry entry 的 message 内容;变更须同步 `e2e/` 场景脚本与单测):
104
102
 
105
103
  | # | 日志 | 发出侧 | 含义 |
106
104
  |---|---|---|---|
@@ -159,6 +157,7 @@ rename-session/
159
157
  ├── skills/rename-session-ext-config/SKILL.md # 配置指南(pi 内 agent 可发现)
160
158
  └── src/
161
159
  ├── index.ts # 工厂入口(message_end/turn_end handler 按模式分派 + rename_session 工具注册 + /auto-rename 命令)
160
+ ├── landing.ts # landTitle:两入口共用落库管道(起 LLM → 防覆盖重查 → setSessionName → renamed to 日志 + usage 落账)
162
161
  ├── commands.ts # /auto-rename on|off|status 命令(enable/disable 别名)
163
162
  ├── llm.ts # callRenameLLM / 两段输入构造 / debug 内省 / 超时
164
163
  ├── pure.ts # 纯函数(配置 / 首轮计数 / cleanTitle)
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-rename-session",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
- "xyz-agent": {
6
+ "taiji": {
7
7
  "role": "universal",
8
8
  "startupConfig": [
9
9
  {
@@ -33,9 +33,9 @@
33
33
  "pi-package"
34
34
  ],
35
35
  "dependencies": {
36
- "@zhushanwen/pi-ext-guards": "0.4.0",
37
- "@zhushanwen/pi-extension-logger": "0.6.0",
38
- "@zhushanwen/pi-llm-shared": "0.8.0"
36
+ "@zhushanwen/pi-ext-guards": "0.4.1",
37
+ "@zhushanwen/pi-extension-logger": "0.6.1",
38
+ "@zhushanwen/pi-llm-shared": "0.8.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@vitest/coverage-v8": "^4.1.9",
@@ -11,7 +11,7 @@ description: "配置 @zhushanwen/pi-rename-session(会话自动重命名)时
11
11
 
12
12
  `<agentDir>/config/rename-session-ext-config.json`
13
13
 
14
- - `<agentDir>` = pi agent 目录(`PI_CODING_AGENT_DIR` 覆盖,默认 `~/.pi/agent`;xyz-agent 隔离环境为 `~/.xyz-agent/pi/agent`)
14
+ - `<agentDir>` = pi agent 目录(`PI_CODING_AGENT_DIR` 覆盖,默认 `~/.pi/agent`;taiji 隔离环境为 `~/.taiji/agent`)
15
15
  - 走 llm-shared 泛型 config(config/ 子目录 + getAgentDir 派生 + mtime+size 缓存 + 原子写)
16
16
  - 文件缺失/坏 JSON 返回默认值,不抛错
17
17
 
@@ -116,7 +116,7 @@ subagent 子进程 session 不重命名(`isSubagentSession` 判定 session 目
116
116
 
117
117
  `enabled` 有三层来源,优先级从高到低(`src/pure.ts` `loadRenameConfig`):
118
118
 
119
- 1. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):这是 xyz-agent runtime 的开关契约(SystemPage 开关 / 首启默认开启都写这个文件,live 检查每次事件生效)。**xyz-agent 用户不要手改 JSON 里的 enabled**——桌面端的开关状态存在 flag 文件里,手改 JSON 会被 flag 覆盖(flag 存在即视为开)。
119
+ 1. **`<agentDir>/auto-rename-enabled` flag 文件**(存在 = 开):这是 taiji runtime 的开关契约(SystemPage 开关 / 首启默认开启都写这个文件,live 检查每次事件生效)。**taiji 用户不要手改 JSON 里的 enabled**——桌面端的开关状态存在 flag 文件里,手改 JSON 会被 flag 覆盖(flag 存在即视为开)。
120
120
  2. **config 的 `enabled` 字段**(默认 false):flag 不存在时生效,是原生 pi CLI 用户的开关(手改 JSON 或 `/auto-rename on|off` 命令)。
121
121
  3. **默认值**(false):以上两层均未设置时。
122
122
 
@@ -88,7 +88,7 @@ describe("executeAutoRenameCommand", () => {
88
88
  });
89
89
 
90
90
  it("未知参数 → 提示用法", () => {
91
- const msg = executeAutoRenameCommand("xyz");
91
+ const msg = executeAutoRenameCommand("foo");
92
92
  expect(msg).toContain("未知参数");
93
93
  expect(msg).toContain("用法");
94
94
  });
@@ -36,7 +36,7 @@ import { callLLM, resolveModel } from "@zhushanwen/pi-llm-shared";
36
36
  import renameSessionExtension from "../index";
37
37
  import { type RenameSessionConfig, loadRenameConfig } from "../pure.js";
38
38
 
39
- // 每用例收尾统一还原:logger mock 恢复 + stub 的 XYZ_AGENT_DEBUG 还原,
39
+ // 每用例收尾统一还原:logger mock 恢复 + stub 的 TAIJI_AGENT_DEBUG 还原,
40
40
  // 防泄漏到后续用例(debug 开关 live 读 process.env,依赖 stubEnv/unstubAllEnvs 成对)
41
41
  afterEach(() => {
42
42
  vi.restoreAllMocks();
@@ -50,12 +50,12 @@ interface MockSetup {
50
50
  setSessionNameMock: ReturnType<typeof vi.fn>;
51
51
  /** 防覆盖检查读 pi.getSessionName(),非 ctx——默认未命名(undefined)。 */
52
52
  getSessionNameMock: ReturnType<typeof vi.fn>;
53
- /** usage 落账入口:index.ts 注入回调体内调 pi.appendEntry,接线断言用(时点与 catch 归属契约见 llm.ts)。 */
53
+ /** usage 落账入口:landing.ts 注入回调体内调 pi.appendEntry,接线断言用(时点与 catch 归属契约见 llm.ts)。 */
54
54
  appendEntryMock: ReturnType<typeof vi.fn>;
55
- /** registerTool mock(设计 rename-session-three-modes.md D3 agent-tool 注册面接线断言用)。 */
55
+ /** registerTool mockagent-tool 注册面接线断言用)。 */
56
56
  registerToolMock: ReturnType<typeof vi.fn>;
57
57
  turnEndHandler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>;
58
- /** message_end handler(first-prompt 入口,设计 rename-session-three-modes.md D2)。 */
58
+ /** message_end handler(first-prompt 入口)。 */
59
59
  messageEndHandler: (event: unknown, ctx: ExtensionContext) => void | Promise<void>;
60
60
  /** 工厂注册的 rename_session 工具定义(registerTool 捕获)。 */
61
61
  registeredTool: unknown;
@@ -176,7 +176,7 @@ function fire(setup: MockSetup, ctx: ExtensionContext, message?: unknown): Promi
176
176
  }
177
177
 
178
178
  /**
179
- * 触发 message_end handler(first-prompt 入口,设计 rename-session-three-modes.md D2)。message 默认为首条 user prompt
179
+ * 触发 message_end handler(first-prompt 入口)。message 默认为首条 user prompt
180
180
  * (text blocks 形态——探针 P1 实测 rpc 模式 user 载荷 content 为 blocks 数组)。
181
181
  */
182
182
  function fireMessageEnd(setup: MockSetup, ctx: ExtensionContext, message?: unknown): Promise<void> {
@@ -191,7 +191,7 @@ function fireMessageEnd(setup: MockSetup, ctx: ExtensionContext, message?: unkno
191
191
 
192
192
  /** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
193
193
  function debugWarnSpy(): ReturnType<typeof vi.fn> {
194
- vi.stubEnv("XYZ_AGENT_DEBUG", "1");
194
+ vi.stubEnv("TAIJI_AGENT_DEBUG", "1");
195
195
  loggerMock.warn.mockClear();
196
196
  return loggerMock.warn;
197
197
  }
@@ -218,7 +218,7 @@ describe("renameSessionExtension", () => {
218
218
  vi.mocked(loadRenameConfig).mockReset();
219
219
  vi.mocked(resolveModel).mockReset();
220
220
  vi.mocked(callLLM).mockReset();
221
- // 工厂注册面在 load 时读一次 config(工具注册判定,设计 rename-session-three-modes.md D1)——工厂调用前必须有可用返回值
221
+ // 工厂注册面在 load 时读一次 config(工具注册判定)——工厂调用前必须有可用返回值
222
222
  vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
223
223
  setup = createMockPi();
224
224
  renameSessionExtension(setup.pi);
@@ -230,7 +230,7 @@ describe("renameSessionExtension", () => {
230
230
  expect(setup.pi.on).toHaveBeenCalledWith("message_end", expect.any(Function));
231
231
  expect(setup.turnEndHandler).toBeTypeOf("function");
232
232
  expect(setup.messageEndHandler).toBeTypeOf("function");
233
- // first-stop(ENABLED_CONFIG)非 agent-tool → 工具不注册(设计 rename-session-three-modes.md D1 求值时点)
233
+ // first-stop(ENABLED_CONFIG)非 agent-tool → 工具不注册(注册面 load 求值时点)
234
234
  expect(setup.registerToolMock).not.toHaveBeenCalled();
235
235
  });
236
236
 
@@ -265,7 +265,7 @@ describe("renameSessionExtension", () => {
265
265
  });
266
266
 
267
267
  // ────────────────────────────────────────────────────
268
- // message_end 入口三模式分派(设计 rename-session-three-modes.md D1 live 读 mode / D2 first-prompt 守卫链)
268
+ // message_end 入口三模式分派(live 读 mode / first-prompt 守卫链)
269
269
  // ────────────────────────────────────────────────────
270
270
 
271
271
  it("TC-M1: first-prompt 模式首条 user message_end → callLLM 发起,prompt 从 event 载荷取(entries 空,finalText 空走两条降级)", async () => {
@@ -280,7 +280,7 @@ describe("renameSessionExtension", () => {
280
280
  const opts = vi.mocked(callLLM).mock.calls[0][1] as {
281
281
  messages: { role: string; content: { type: string; text: string }[] }[];
282
282
  };
283
- // 不等回复(设计 rename-session-three-modes.md D2):finalText 空 → 两条 [user(promptText), user(instruction)],无 assistant 条目
283
+ // 不等回复:finalText 空 → 两条 [user(promptText), user(instruction)],无 assistant 条目
284
284
  expect(opts.messages.map((m) => m.role)).toEqual(["user", "user"]);
285
285
  expect(opts.messages[0].content[0].text).toBe("帮我修复登录超时,并补上单测");
286
286
  // 落库走同一管道(防覆盖重查 → setSessionName → renamed to)
@@ -398,7 +398,7 @@ describe("renameSessionExtension", () => {
398
398
  });
399
399
 
400
400
  // ────────────────────────────────────────────────────
401
- // turn_end 入口 mode 分派(设计 rename-session-three-modes.md D1 live 读:first-stop 以外的 mode 静默返回)
401
+ // turn_end 入口 mode 分派(live 读:first-stop 以外的 mode 静默返回)
402
402
  // ────────────────────────────────────────────────────
403
403
 
404
404
  it("TC-M10: first-prompt 模式 turn_end(stop + count=1 触发态)→ skip: mode=first-prompt,不发起 LLM 调用", async () => {
@@ -640,9 +640,9 @@ describe("renameSessionExtension", () => {
640
640
  // TC9 debug 关闭态零输出(默认生产环境零噪音)
641
641
  // ────────────────────────────────────────────────────
642
642
 
643
- it("TC9: XYZ_AGENT_DEBUG 未设 → 全流程 7 条 debug 契约文案零输出(既有 A1 日志除外)", async () => {
644
- // 显式清除(防宿主环境泄漏 XYZ_AGENT_DEBUG 影响判定)
645
- vi.stubEnv("XYZ_AGENT_DEBUG", undefined);
643
+ it("TC9: TAIJI_AGENT_DEBUG 未设 → 全流程 7 条 debug 契约文案零输出(既有 A1 日志除外)", async () => {
644
+ // 显式清除(防宿主环境泄漏 TAIJI_AGENT_DEBUG 影响判定)
645
+ vi.stubEnv("TAIJI_AGENT_DEBUG", undefined);
646
646
  loggerMock.warn.mockClear();
647
647
  const warnSpy = loggerMock.warn;
648
648
  vi.mocked(loadRenameConfig).mockReturnValue(ENABLED_CONFIG);
@@ -769,7 +769,7 @@ describe("usage 落账接线(appendUsageEntry 回调注入)", () => {
769
769
  });
770
770
 
771
771
  // ────────────────────────────────────────────────────
772
- // rename_session 工具注册与 execute 守卫(设计 rename-session-three-modes.md D1 求值时点 / D3 agent-tool 模式)
772
+ // rename_session 工具注册与 execute 守卫(load 求值时点 / agent-tool 模式)
773
773
  // ────────────────────────────────────────────────────
774
774
 
775
775
  /** 注册到的工具定义形态(registerTool 捕获后测试侧调用 execute 用)。 */
@@ -788,7 +788,7 @@ interface RegisteredRenameTool {
788
788
  describe("rename_session 工具注册与 execute 守卫(D3)", () => {
789
789
  let setup: MockSetup;
790
790
 
791
- /** 显式设置 load 时 mode 后重建工厂(注册面只在 load 求值一次,设计 rename-session-three-modes.md D1)。 */
791
+ /** 显式设置 load 时 mode 后重建工厂(注册面只在 load 求值一次)。 */
792
792
  function setupWithMode(config: RenameSessionConfig): void {
793
793
  vi.mocked(loadRenameConfig).mockReset();
794
794
  vi.mocked(loadRenameConfig).mockReturnValue(config);
@@ -891,7 +891,7 @@ describe("rename_session 工具注册与 execute 守卫(D3)", () => {
891
891
  });
892
892
 
893
893
  // ────────────────────────────────────────────────────
894
- // first-prompt 在途去重(设计 rename-session-three-modes.md D2:工厂闭包级——pi extensionCache 缓存 factory,
894
+ // first-prompt 在途去重(工厂闭包级——pi extensionCache 缓存 factory,
895
895
  // 每次 session 创建重执行工厂,闭包变量 = per-session 生命周期)
896
896
  // ────────────────────────────────────────────────────
897
897
 
@@ -43,7 +43,7 @@ import {
43
43
  } from "../llm.js";
44
44
  import { cleanTitle, type RenameSessionConfig } from "../pure.js";
45
45
 
46
- // 每用例收尾统一还原:logger mock 恢复 + stub 的 XYZ_AGENT_DEBUG 还原,
46
+ // 每用例收尾统一还原:logger mock 恢复 + stub 的 TAIJI_AGENT_DEBUG 还原,
47
47
  // 防泄漏到后续用例(debug 开关 live 读 process.env,依赖 stubEnv/unstubAllEnvs 成对)
48
48
  afterEach(() => {
49
49
  vi.restoreAllMocks();
@@ -259,7 +259,7 @@ describe("RENAME_SYSTEM_PROMPT / RENAME_INSTRUCTION", () => {
259
259
  });
260
260
 
261
261
  // ────────────────────────────────────────────────────
262
- // 设计 rename-session-three-modes.md D5 空 ref fallback(空 ref → ctx.model 跟随会话主模型;非空无效 ref → 静默跳过 + warn)
262
+ // 空 ref fallback(空 ref → ctx.model 跟随会话主模型;非空无效 ref → 静默跳过 + warn)
263
263
  // ────────────────────────────────────────────────────
264
264
 
265
265
  describe("callRenameLLM D5 空 ref fallback(跟随会话主模型)", () => {
@@ -317,7 +317,7 @@ describe("callRenameLLM D5 空 ref fallback(跟随会话主模型)", () => {
317
317
  });
318
318
 
319
319
  // ────────────────────────────────────────────────────
320
- // promptText 选项(设计 rename-session-three-modes.md D2 first-prompt:文本从 message_end 载荷取,不走 entries)
320
+ // promptText 选项(first-prompt:文本从 message_end 载荷取,不走 entries)
321
321
  // ────────────────────────────────────────────────────
322
322
 
323
323
  describe("callRenameLLM promptText 选项(first-prompt 载荷取文本)", () => {
@@ -363,7 +363,7 @@ describe("callRenameLLM promptText 选项(first-prompt 载荷取文本)", ()
363
363
 
364
364
  /** 开 debug 开关 + 清空 warn mock,返回 loggerMock.warn 供日志断言(还原由顶层 afterEach 统一负责)。 */
365
365
  function debugWarnSpy(): ReturnType<typeof vi.fn> {
366
- vi.stubEnv("XYZ_AGENT_DEBUG", "1");
366
+ vi.stubEnv("TAIJI_AGENT_DEBUG", "1");
367
367
  loggerMock.warn.mockClear();
368
368
  return loggerMock.warn;
369
369
  }
@@ -557,7 +557,7 @@ describe("callRenameLLM", () => {
557
557
  expect(logAtCallTime).toContain(head);
558
558
  expect(logAtCallTime).toContain(tail);
559
559
  expect(logAtCallTime).not.toContain("乙");
560
- // 「renamed to」日志已移位到 index.ts handler 侧(setSessionName 之后)——
560
+ // 「renamed to」日志已移位到 landing.ts landTitle(setSessionName 之后)——
561
561
  // callRenameLLM 全流程(含成功路径)不再打出该日志
562
562
  const logAfter = warnLines(warnSpy).join("\n");
563
563
  expect(logAfter).not.toContain("renamed to");
@@ -725,14 +725,14 @@ describe("callRenameLLM usage 落账回调(appendUsageEntry 注入)", () =>
725
725
  expect(appendUsageEntry).not.toHaveBeenCalled();
726
726
  });
727
727
 
728
- it("回调内部抛错被回调实现 catch(契约:catch 位于回调实现内部,模拟 index.ts 注入的真实回调)→ 不阻断 cleanTitle 流程,标题照常返回", async () => {
728
+ it("回调内部抛错被回调实现 catch(契约:catch 位于回调实现内部,模拟 landing.ts 注入的真实回调)→ 不阻断 cleanTitle 流程,标题照常返回", async () => {
729
729
  vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
730
730
  vi.mocked(callLLM).mockResolvedValue({ ok: true, content: "修复登录bug", usage: STUB_USAGE });
731
731
  const appendUsageEntry = vi.fn((_model: string, _usage: Usage) => {
732
732
  try {
733
733
  throw new Error("session switched"); // 模拟 pi.appendEntry 抛错(session 已切换等)
734
734
  } catch {
735
- // 回调体内 catch(catch 必须位于回调实现内部)——index.ts 注入实现同款
735
+ // 回调体内 catch(catch 必须位于回调实现内部)——landing.ts 注入实现同款
736
736
  }
737
737
  });
738
738
 
@@ -744,7 +744,7 @@ describe("callRenameLLM usage 落账回调(appendUsageEntry 注入)", () =>
744
744
  expect(appendUsageEntry).toHaveBeenCalledTimes(1);
745
745
  });
746
746
 
747
- it("违约回调(实现未自吞错直接抛出)→ callRenameLLM reject(llm.ts 不吞错——catch 归属钉死回调实现内部,防双重 catch 漂移;真实接线的兜底由 index.ts 回调体内 catch + 外层 .catch 覆盖)", async () => {
747
+ it("违约回调(实现未自吞错直接抛出)→ callRenameLLM reject(llm.ts 不吞错——catch 归属钉死回调实现内部,防双重 catch 漂移;真实接线的兜底由 landing.ts 回调体内 catch + 外层 .catch 覆盖)", async () => {
748
748
  vi.mocked(resolveModel).mockReturnValue(STUB_MODEL);
749
749
  vi.mocked(callLLM).mockResolvedValue({ ok: true, content: "修复登录bug", usage: STUB_USAGE });
750
750
  const appendUsageEntry = vi.fn(() => {
@@ -72,7 +72,7 @@ describe("countSuccessfulAssistantReplies", () => {
72
72
  });
73
73
 
74
74
  // ────────────────────────────────────────────────────
75
- // countUserMessages(设计 rename-session-three-modes.md D2:first-prompt 模式首条判定——message_end(user) handler 内
75
+ // countUserMessagesfirst-prompt 模式首条判定——message_end(user) handler 内
76
76
  // entries 计数 === 0 ⇔ 本条即 session 首条 user,探针 P1 实测该时点 entries 不含本条)
77
77
  // ────────────────────────────────────────────────────
78
78
 
@@ -242,7 +242,7 @@ describe("normalizeRenameConfig", () => {
242
242
  expect(normalizeRenameConfig(cfg)).toEqual(cfg);
243
243
  });
244
244
 
245
- // ── mode 三值枚举(设计 rename-session-three-modes.md D1:normalize 逐字段校验回默认) ──
245
+ // ── mode 三值枚举(normalize 逐字段校验回默认) ──
246
246
 
247
247
  it("mode 三个合法值(first-prompt/first-stop/agent-tool)→ 原样保留", () => {
248
248
  for (const mode of ["first-prompt", "first-stop", "agent-tool"] as const) {
@@ -333,7 +333,7 @@ describe("normalizeRenameConfig", () => {
333
333
  });
334
334
 
335
335
  // ────────────────────────────────────────────────────
336
- // env 覆盖层删除负面(设计 rename-session-three-modes.md D6 / V8:PI_RENAME_* 四键已删,预置变量不得有任何幽灵效果)
336
+ // env 覆盖层删除负面(PI_RENAME_* 四键已删,预置变量不得有任何幽灵效果)
337
337
  // ────────────────────────────────────────────────────
338
338
 
339
339
  describe("env 覆盖层删除负面(PI_RENAME_* 无效果)", () => {
@@ -461,7 +461,7 @@ describe("loadRenameConfig / saveRenameConfig", () => {
461
461
  expect(loadRenameConfig().enabled).toBe(false);
462
462
  });
463
463
 
464
- // ── xyz-agent runtime 开关契约(flag live 覆盖源,见 pure.ts [COMPAT] 注释)──
464
+ // ── taiji runtime 开关契约(flag live 覆盖源,见 pure.ts [COMPAT] 注释)──
465
465
 
466
466
  it("TC5: flag 文件存在 → enabled 强制 true(不写 config、不删 flag,live 检查)", () => {
467
467
  const flagPath = path.join(tmpAgentDir, "auto-rename-enabled");
@@ -490,7 +490,7 @@ describe("loadRenameConfig / saveRenameConfig", () => {
490
490
  expect(loadRenameConfig().enabled).toBe(true);
491
491
  });
492
492
 
493
- it("TC6c: flag 删除后(xyz-agent SystemPage 关闭路径)→ 回落 config,且默认 false", () => {
493
+ it("TC6c: flag 删除后(taiji SystemPage 关闭路径)→ 回落 config,且默认 false", () => {
494
494
  // 预置 flag(模拟 rt 默认开启)+ 无 config
495
495
  const flagPath = path.join(tmpAgentDir, "auto-rename-enabled");
496
496
  fs.writeFileSync(flagPath, "");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * startupConfig 声明守护测试:断言 package.json `xyz-agent.startupConfig` 声明的
2
+ * startupConfig 声明守护测试:断言 package.json `taiji.startupConfig` 声明的
3
3
  * content 与代码 DEFAULT_RENAME_CONFIG 常量深相等,防止两侧任一改动未同步(漂移)。
4
4
  * 机制:runtime 启动序列统一 ensure(extension-startup-config.ts),声明即首建内容。
5
5
  *
@@ -13,7 +13,7 @@ import { describe, expect, it } from "vitest";
13
13
 
14
14
  import { DEFAULT_RENAME_CONFIG } from "../pure.js";
15
15
 
16
- /** package.json `xyz-agent.startupConfig` 声明条目的最小形状(守护断言用)。 */
16
+ /** package.json `taiji.startupConfig` 声明条目的最小形状(守护断言用)。 */
17
17
  interface StartupConfigEntry {
18
18
  path: string;
19
19
  content: unknown;
@@ -21,11 +21,11 @@ interface StartupConfigEntry {
21
21
 
22
22
  const pkg = JSON.parse(
23
23
  readFileSync(join(fileURLToPath(import.meta.url), "../../../package.json"), "utf-8"),
24
- ) as { "xyz-agent"?: { startupConfig?: StartupConfigEntry[] } };
24
+ ) as { "taiji"?: { startupConfig?: StartupConfigEntry[] } };
25
25
 
26
26
  describe("startupConfig 声明守护", () => {
27
27
  it("声明 content 与代码 DEFAULT_RENAME_CONFIG 深相等", () => {
28
- const entry = pkg["xyz-agent"]?.startupConfig?.find(
28
+ const entry = pkg["taiji"]?.startupConfig?.find(
29
29
  (e: StartupConfigEntry) => e.path === "config/rename-session-ext-config.json",
30
30
  );
31
31
  expect(entry).toBeDefined();
package/src/commands.ts CHANGED
@@ -6,7 +6,7 @@ import { loadRenameConfig, saveRenameConfig, setAutoRenameSwitch } from "./pure.
6
6
  * 解析 /auto-rename 参数并执行开关操作。
7
7
  *
8
8
  * 开关双机制(见 pure.ts [COMPAT] 契约注释):
9
- * - `<agentDir>/auto-rename-enabled` flag 文件 = xyz-agent runtime 开关契约(live 覆盖源,存在即生效)
9
+ * - `<agentDir>/auto-rename-enabled` flag 文件 = taiji runtime 开关契约(live 覆盖源,存在即生效)
10
10
  * - `config/rename-session-ext-config.json` 的 `enabled` = pi CLI 用户开关(flag 不存在时生效)
11
11
  *
12
12
  * 写入策略:
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@ import { getLogger, setPiHandle } from "@zhushanwen/pi-extension-logger";
7
7
  import { Type } from "typebox";
8
8
 
9
9
  import { registerAutoRenameCommand } from "./commands.js";
10
- import { callRenameLLM, debugLog as llmDebugLog, extractMessageText, isSubagentSession } from "./llm.js";
10
+ import { landTitle } from "./landing.js";
11
+ import { debugLog as llmDebugLog, extractMessageText, isSubagentSession } from "./llm.js";
11
12
  import {
12
13
  cleanTitle,
13
14
  countSuccessfulAssistantReplies,
@@ -32,7 +33,7 @@ interface TurnEndLikeEvent {
32
33
  }
33
34
 
34
35
  /**
35
- * message_end 事件的宽松类型(设计 rename-session-three-modes.md D2 first-prompt 入口)。同 TurnEndLikeEvent 的
36
+ * message_end 事件的宽松类型(first-prompt 入口)。同 TurnEndLikeEvent 的
36
37
  * `& object` 逆变技巧;message 只读 role(分派过滤)与 content(载荷取文本)。
37
38
  */
38
39
  interface MessageEndLikeEvent {
@@ -41,14 +42,14 @@ interface MessageEndLikeEvent {
41
42
  }
42
43
 
43
44
  /**
44
- * mode 切走后残留 rename_session 工具被调用的 execute 守卫文案(设计 rename-session-three-modes.md D3,isError 经 throw 产生)。
45
+ * mode 切走后残留 rename_session 工具被调用的 execute 守卫文案(isError 经 throw 产生)。
45
46
  * 与失败路径表「守卫拒绝」行 / V10 通过标准三处对齐:含恢复动作指引。
46
47
  */
47
48
  const RENAME_TOOL_MODE_GUARD_MESSAGE =
48
49
  "模式已切换,rename_session 仅在 agent-tool 模式可用;本会话可切回 agent-tool 立即恢复(live 读 mode),或手动改名(GUI rename / pi 原生 /name)。";
49
50
 
50
51
  /**
51
- * subagent session 的 rename_session 工具 execute 守卫文案(设计 rename-session-three-modes.md D7 守卫闭环,C-ext-21 三入口
52
+ * subagent session 的 rename_session 工具 execute 守卫文案(守卫闭环,C-ext-21 三入口
52
53
  * 复用;isError 经 throw 产生)。不暴露内部路径细节——subagents 目录形态是跨包实现耦合,
53
54
  * 面向 agent 的报错只说结论与指引。
54
55
  */
@@ -56,10 +57,10 @@ const RENAME_TOOL_SUBAGENT_GUARD_MESSAGE =
56
57
  "当前会话是 subagent 子任务会话(临时产物,不参与会话命名体系),不支持重命名;请继续当前任务,无需重试。";
57
58
 
58
59
  /**
59
- * 注册 rename_session 工具(设计 rename-session-three-modes.md D3 agent-tool 模式)。
60
+ * 注册 rename_session 工具(agent-tool 模式)。
60
61
  *
61
62
  * 关键语义:
62
- * - subagent session 守卫(设计 rename-session-three-modes.md D7):isSubagentSession 命中即 throw isError——subagent
63
+ * - subagent session 守卫:isSubagentSession 命中即 throw isError——subagent
63
64
  * 子进程同样加载本 extension(全局 mode=agent-tool 时 subagent 也注册本工具),
64
65
  * 子会话是临时产物不参与 rename 体系,与 message_end / turn_end 两入口同语义。
65
66
  * - execute 内 live 读 config 守卫:mode !== "agent-tool"(mode 切走后本工具残留在
@@ -112,7 +113,7 @@ function registerRenameSessionTool(pi: ExtensionAPI): void {
112
113
 
113
114
  /**
114
115
  * pi-rename-session extension 工厂函数。
115
- * 三种触发模式(设计 rename-session-three-modes.md D1,默认 first-stop)共用同一落库管道(callRenameLLM → 防覆盖重查 →
116
+ * 三种触发模式(默认 first-stop)共用同一落库管道(callRenameLLM → 防覆盖重查 →
116
117
  * setSessionName):
117
118
  * - first-stop(默认):首个成功 round 末触发(turn_end 入口,现状行为)
118
119
  * - first-prompt:首条 user 消息发出即触发(message_end 入口,不等回复,标题只基于 prompt)
@@ -120,25 +121,25 @@ function registerRenameSessionTool(pi: ExtensionAPI): void {
120
121
  */
121
122
  export default function renameSessionExtension(pi: ExtensionAPI): void {
122
123
  // 日志通道注入(extension-logger 两阶段初始化:工厂拿 pi → setPiHandle,最早期调用)。
123
- // 不注入则 logger.warn/error 只有文件日志通道(XYZ_AGENT_DEBUG=1),appendEntry
124
+ // 不注入则 logger.warn/error 只有文件日志通道(TAIJI_AGENT_DEBUG=1),appendEntry
124
125
  // (session entry,README「debug 证据链」的 E2E 断言数据源)不生效。
125
126
  setPiHandle(pi);
126
127
 
127
128
  registerAutoRenameCommand(pi);
128
129
 
129
- // 工具注册面(设计 rename-session-three-modes.md D1 求值时点边界):只在 extension load 时求值一次——pi 无 unregisterTool,
130
+ // 工具注册面(求值时点边界):只在 extension load 时求值一次——pi 无 unregisterTool,
130
131
  // 切 mode 后已存活 session 的工具清单不回溯(切到 agent-tool 当前 session 无工具、切走则
131
132
  // 工具残留),残留工具由 execute 内 live mode 守卫兜底(RENAME_TOOL_MODE_GUARD_MESSAGE)。
132
133
  if (loadRenameConfig().mode === "agent-tool") {
133
134
  registerRenameSessionTool(pi);
134
135
  }
135
136
 
136
- // 在途去重标志(设计 rename-session-three-modes.md D2):工厂闭包级而非模块级——pi 的 extensionCache 缓存 factory、每次
137
+ // 在途去重标志:工厂闭包级而非模块级——pi 的 extensionCache 缓存 factory、每次
137
138
  // session 创建重执行工厂函数,闭包变量 = per-session 生命周期;模块顶层变量进程级存活,
138
139
  // 进程内 /fork、/session 切换 session 时会跨 session 污染、吞掉新 session 的唯一命名机会。
139
140
  let firstPromptInFlight = false;
140
141
 
141
- // message_end 入口(first-prompt 模式,设计 rename-session-three-modes.md D2):首条 user 消息发出即命名,不等回复完成。
142
+ // message_end 入口(first-prompt 模式):首条 user 消息发出即命名,不等回复完成。
142
143
  pi.on("message_end", async (event: MessageEndLikeEvent, ctx: ExtensionContext) => {
143
144
  // handler 侧 debug 日志(C3 同款契约):firstPrompt 前缀区分触发入口(与 turn_end 路径
144
145
  // 共享 skip: name exists / renamed to 契约文案,harness 断言可归因到本入口)
@@ -153,7 +154,7 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
153
154
  const config = loadRenameConfig();
154
155
  if (!config.enabled) return;
155
156
 
156
- // 3. mode 分派(事件面 live 读,设计 rename-session-three-modes.md D1:GUI 切 mode 对活跃 session 的自动命名即时生效——
157
+ // 3. mode 分派(事件面 live 读:GUI 切 mode 对活跃 session 的自动命名即时生效——
157
158
  // first-stop / agent-tool 模式下本入口静默返回,V10 A 段「切走即停」)
158
159
  if (config.mode !== "first-prompt") {
159
160
  debugLog(`skip: mode=${config.mode}`);
@@ -163,7 +164,7 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
163
164
  // 4. 排除 subagent 子进程 session(守卫链复用,C-ext-21 覆盖面含本新入口)
164
165
  if (isSubagentSession(ctx.sessionManager.getSessionDir())) return;
165
166
 
166
- // 5. 首条 user 判定(设计 rename-session-three-modes.md D2):extension handler 先于本条 message 的 entries append 执行
167
+ // 5. 首条 user 判定:extension handler 先于本条 message 的 entries append 执行
167
168
  // (探针 P1 实测:user message_end 时 getEntries() 的 user 计数为 0,assistant
168
169
  // message_end 时已变 1),计数 === 0 ⇔ 本条即 session 首条 user——steering/follow-up
169
170
  // 队列消息到达时首条已入 entries(计数 ≥1),天然不重复触发
@@ -173,7 +174,7 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
173
174
  return;
174
175
  }
175
176
 
176
- // 6. prompt 文本从 event 载荷取(设计 rename-session-three-modes.md D2:entries 此时不含本条,只能从载荷取;探针 P1 实测
177
+ // 6. prompt 文本从 event 载荷取(entries 此时不含本条,只能从载荷取;探针 P1 实测
177
178
  // content 为 text blocks 数组,extractMessageText → joinTextBlocks 拼出完整 prompt)
178
179
  const promptText = extractMessageText(event.message);
179
180
  if (promptText === "") {
@@ -181,42 +182,17 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
181
182
  return;
182
183
  }
183
184
 
184
- // 7. 在途去重(设计 rename-session-three-modes.md D2):by construction 第二条 user 到达时首条已 append(步 5 拦截),本
185
+ // 7. 在途去重:by construction 第二条 user 到达时首条已 append(步 5 拦截),本
185
186
  // 标志兜住极端交错双发;settle 后释放(首条窗口已消费,释放后新到达靠步 5 拦截)
186
187
  if (firstPromptInFlight) return;
187
188
  firstPromptInFlight = true;
188
189
 
189
190
  // 8. fire-and-forget(同 turn_end 契约):handler 立即 resolve,LLM 调用与 setSessionName
190
191
  // 在后台异步完成。finalMessage 传空 content——first-prompt 语义即「不等回复」,
191
- // finalText 为空串走 buildTitleMessages 两条降级(标题只基于 prompt,设计 rename-session-three-modes.md D2 语义)
192
- void callRenameLLM(ctx, config, { content: [] }, {
193
- promptText,
194
- // usage 落账回调注入(与 turn_end 路径同款契约):调用时点 ok:true && usage 后立即、
195
- // cleanTitle 前;catch 位于回调实现内部(回调自吞错),appendEntry 抛错只记日志
196
- appendUsageEntry: (model, usage) => {
197
- try {
198
- pi.appendEntry("rename-session", { model, usage });
199
- } catch (e) {
200
- logger.error("failed to append usage entry", { error: String(e) });
201
- }
202
- },
203
- })
204
- .then((title) => {
205
- if (!title) return;
206
- // 防覆盖(同 turn_end 路径):落库前重查——LLM 调用窗口(2-30s)内语义名/
207
- // 手动命名的竞态由此兜住(skip 文案是 E2E 硬契约)
208
- if (pi.getSessionName()) {
209
- debugLog("skip: name exists");
210
- return;
211
- }
212
- pi.setSessionName(title);
213
- // 落库成功才打「renamed to」(防覆盖 return 在前,竞态命中时本日志不出现)
214
- debugLog(`renamed to "${title}"`);
215
- })
216
- .catch((e) => logger.error("rename LLM failed", { error: String(e) }))
217
- .finally(() => {
218
- firstPromptInFlight = false;
219
- });
192
+ // finalText 为空串走 buildTitleMessages 两条降级(标题只基于 prompt
193
+ void landTitle(pi, ctx, config, { content: [] }, { debugLog, promptText }).finally(() => {
194
+ firstPromptInFlight = false;
195
+ });
220
196
  // rename 是 best-effort,任何 LLM 失败(网络/提取/auth/model 不可用)都静默跳过保留原 label,
221
197
  // 不进 session history。
222
198
  } catch (e) {
@@ -239,7 +215,7 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
239
215
  const config = loadRenameConfig();
240
216
  if (!config.enabled) return;
241
217
 
242
- // 2. mode 分派(事件面 live 读,设计 rename-session-three-modes.md D1):first-stop 才走 turn_end 自动命名——
218
+ // 2. mode 分派(事件面 live 读):first-stop 才走 turn_end 自动命名——
243
219
  // first-prompt / agent-tool 模式下本入口静默返回(V10 A 段「切走即停」;
244
220
  // 切回 first-stop 后 live 恢复分派,但已跑过成功 round 的 session 受一次性窗口约束)
245
221
  if (config.mode !== "first-stop") {
@@ -269,35 +245,9 @@ export default function renameSessionExtension(pi: ExtensionAPI): void {
269
245
  // 6. LLM 生成标题并落库。pi 运行时的事件链是 await 的(runner.emit → await handler),
270
246
  // 若 await callRenameLLM 会阻塞 agent 进入下一次迭代。这里用 detached promise 脱离 await 链,
271
247
  // 真正实现 fire-and-forget:handler 立即 resolve,LLM 调用与 setSessionName 在后台异步完成。
272
- void callRenameLLM(ctx, config, event.message, {
273
- // usage 落账回调注入:闭包捕获 pi,把 rename LLM 调用的 usage
274
- // custom entry 落盘(pi.appendEntry {type:"custom",customType:"rename-session",
275
- // data:{model,usage},timestamp},不进对话流不进 LLM 上下文)。调用时点
276
- // (ok:true && usage 后立即、cleanTitle 前)由 llm.ts 统一规定。
277
- appendUsageEntry: (model, usage) => {
278
- // catch 必须位于回调实现内部——appendEntry 抛错(session 已切换等)只记
279
- // 日志,不影响回调返回与后续 cleanTitle/setSessionName(「标题照常落库」)。
280
- try {
281
- pi.appendEntry("rename-session", { model, usage });
282
- } catch (e) {
283
- logger.error("failed to append usage entry", { error: String(e) });
284
- }
285
- },
286
- })
287
- .then((title) => {
288
- if (!title) return;
289
- // 防覆盖:落库前重查——LLM 调用窗口(2-30s)内用户手动命名的竞态由此兜住
290
- // (发起前查没有意义,那时查不能防竞态;skip 文案是 E2E 硬契约)
291
- if (pi.getSessionName()) {
292
- debugLog("skip: name exists");
293
- return;
294
- }
295
- pi.setSessionName(title);
296
- // 落库成功才打「renamed to」(移位自 llm.ts:日志必须晚于 setSessionName——
297
- // 防覆盖 return 在前,竞态命中时本日志不出现,避免「日志称 renamed 但未落库」)
298
- debugLog(`renamed to "${title}"`);
299
- })
300
- .catch((e) => logger.error("rename LLM failed", { error: String(e) }));
248
+ // 落库管道(起 LLM → 防覆盖重查 → setSessionName → renamed to 日志)见 landing.ts:
249
+ // 与 first-prompt 入口共用同一段,usage 落账回调 / skip / renamed to 契约由该处单点保证
250
+ void landTitle(pi, ctx, config, event.message, { debugLog });
301
251
  // rename 是 best-effort,任何 LLM 失败(网络/提取/auth/model 不可用)都静默跳过保留原 label,
302
252
  // 不进 session history。
303
253
  } catch (e) {
package/src/landing.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
3
+
4
+ import { callRenameLLM } from "./llm.js";
5
+ import type { RenameSessionConfig } from "./pure.js";
6
+
7
+ const logger = getLogger("rename-session");
8
+
9
+ /**
10
+ * landTitle 注入项。
11
+ *
12
+ * llm.ts 刻意不依赖 pi 句柄(ExtensionAPI,见该模块 CallRenameLLMOptions 注释)——
13
+ * 自动命名的 pi 副作用(getSessionName 防覆盖重查 / setSessionName 落库 / appendEntry
14
+ * 落账)与本包唯一的自动落库管道收口在本模块,两触发入口不再各自复写这段。
15
+ */
16
+ export interface LandTitleOptions {
17
+ /**
18
+ * handler 侧 debug 日志(C3 契约,前缀区分触发入口):
19
+ * - first-prompt 入口传 `firstPrompt <message>`
20
+ * - first-stop 入口传 `turnIndex=<n> <message>`
21
+ * skip/renamed to 文案本身由本模块统一产出(两入口共享硬契约文案)。
22
+ */
23
+ debugLog: (message: string) => void;
24
+ /**
25
+ * 显式 prompt 文本(first-prompt 模式):文本从 message_end(role=user) 的 event
26
+ * 载荷取(handler 先于 entries append,getEntries() 此时不含本条,探针 P1 实测),
27
+ * 调用方负责提取并保证非空(空文本在 handler 侧先 skip)。
28
+ * 未提供时走 extractUserPromptText 从 session entries 取(first-stop 现状路径)。
29
+ */
30
+ promptText?: string;
31
+ }
32
+
33
+ /**
34
+ * 三种触发模式的共用落库管道:起 LLM → 防覆盖重查 → setSessionName → `renamed to` 日志。
35
+ *
36
+ * 关键契约(两触发入口一度逐字重复,收口于此——改一处即两入口同步):
37
+ * - usage 落账:闭包捕获 pi,把 rename LLM 调用的 usage 以 custom entry 落盘
38
+ * (pi.appendEntry → {type:"custom",customType:"rename-session",data:{model,usage},timestamp},
39
+ * 不进对话流不进 LLM 上下文)。调用时点(ok:true && usage 后立即、cleanTitle 前)由
40
+ * llm.ts 统一规定;catch 必须位于回调实现内部——appendEntry 抛错(session 已切换等)
41
+ * 只记日志,不影响回调返回与后续 cleanTitle/setSessionName(「标题照常落库」)。
42
+ * - 防覆盖:落库前重查——LLM 调用窗口(2-30s)内语义名/手动命名的竞态由此兜住
43
+ * (发起前查没有意义,那时查不能防竞态;skip 文案是 E2E 硬契约)。
44
+ * - 日志时点:`renamed to` 必须晚于 setSessionName——防覆盖 return 在前,竞态命中时本
45
+ * 日志不出现,避免「日志称 renamed 但未落库」。
46
+ * - 错误传播:整条链路 catch 自吞(logger.error「rename LLM failed」),返回的 promise
47
+ * 永不 reject;调用方按 fire-and-forget 处理(`void landTitle(...)`)。rename 是
48
+ * best-effort,任何 LLM 失败(网络/提取/auth/model 不可用)都静默跳过保留原 label,
49
+ * 不进 session history。
50
+ *
51
+ * finalMessage:触发 turn 的 event.message(first-stop);first-prompt 入口传
52
+ * `{ content: [] }`——语义即「不等回复」,finalText 为空串走 buildTitleMessages 两条降级。
53
+ */
54
+ export function landTitle(
55
+ pi: ExtensionAPI,
56
+ ctx: ExtensionContext,
57
+ config: RenameSessionConfig,
58
+ finalMessage: unknown,
59
+ opts: LandTitleOptions,
60
+ ): Promise<void> {
61
+ return callRenameLLM(ctx, config, finalMessage, {
62
+ promptText: opts.promptText,
63
+ appendUsageEntry: (model, usage) => {
64
+ try {
65
+ pi.appendEntry("rename-session", { model, usage });
66
+ } catch (e) {
67
+ logger.error("failed to append usage entry", { error: String(e) });
68
+ }
69
+ },
70
+ })
71
+ .then((title) => {
72
+ // 无候选标题(LLM 失败 / model 不可用 / 清洗后为空)→ 静默跳过保留原 label
73
+ if (!title) return;
74
+ if (pi.getSessionName()) {
75
+ opts.debugLog("skip: name exists");
76
+ return;
77
+ }
78
+ pi.setSessionName(title);
79
+ // 落库成功才打「renamed to」(防覆盖 return 在前,竞态命中时本日志不出现)
80
+ opts.debugLog(`renamed to "${title}"`);
81
+ })
82
+ .catch((e) => logger.error("rename LLM failed", { error: String(e) }));
83
+ }
package/src/llm.ts CHANGED
@@ -36,10 +36,10 @@ export const RENAME_INSTRUCTION =
36
36
  /**
37
37
  * sessionDir 路径含 subagents 段 → 是 subagent 子进程 session,跳过 rename。
38
38
  *
39
- * 跨包路径耦合(constraints.json C-ext-21):`subagents` 目录布局由 xyz-agent
39
+ * 跨包路径耦合(constraints.json C-ext-21):`subagents` 目录布局由 taiji
40
40
  * `packages/subagent-core/src/execution/assembly/path-encoding.ts`(getSubagentSessionDir /
41
41
  * encodeCwd)定义并写入。本包 role=universal,不 import subagent-core
42
- * (import 会把 universal 包绑死在 xyz-agent 体系上),只按路径形态守卫——
42
+ * (import 会把 universal 包绑死在 taiji 体系上),只按路径形态守卫——
43
43
  * path-encoding.ts 侧有对向注释互指。改 subagent 目录布局必须双侧同步改。
44
44
  * 三模式的新入口(message_end / rename_session 工具)同样复用本守卫。
45
45
  */
@@ -89,7 +89,7 @@ export function extractFinalText(message: unknown): string {
89
89
  }
90
90
 
91
91
  /**
92
- * 取 message 载荷的文本(设计 rename-session-three-modes.md D2 first-prompt:message_end(role=user) 的 event.message)。
92
+ * 取 message 载荷的文本(first-prompt:message_end(role=user) 的 event.message)。
93
93
  * content 为 string 直接返回;blocks 数组拼接 text blocks——与 extractUserPromptText 的
94
94
  * 单条 message 拼装逻辑同构(探针 P1 实测:rpc 模式 user message_end 载荷 content 为
95
95
  * text blocks 数组)。非对象 / 异常形态返回 ''(调用方按无文本 skip)。
@@ -160,7 +160,7 @@ const RENAME_TIMEOUT_MS = 30_000;
160
160
 
161
161
  /** debug 开关 live 读(每次调用查 process.env,非模块加载时读——vi.stubEnv 可测 + 运行时可切换)。 */
162
162
  export function isRenameDebugEnabled(): boolean {
163
- return process.env.XYZ_AGENT_DEBUG === "1";
163
+ return process.env.TAIJI_AGENT_DEBUG === "1";
164
164
  }
165
165
 
166
166
  /**
@@ -218,7 +218,7 @@ export interface CallRenameLLMOptions {
218
218
  */
219
219
  appendUsageEntry?: (model: string, usage: Usage) => void;
220
220
  /**
221
- * 显式 prompt 文本(设计 rename-session-three-modes.md D2 first-prompt 模式):文本从 message_end(role=user) 的
221
+ * 显式 prompt 文本(first-prompt 模式):文本从 message_end(role=user) 的
222
222
  * event 载荷取(handler 先于 entries append,getEntries() 此时不含本条,探针 P1 实测),
223
223
  * 调用方(index.ts)负责提取并保证非空(空文本在 handler 侧先 skip)。
224
224
  * 未提供时走 extractUserPromptText 从 session entries 取(first-stop 现状路径)。
@@ -233,7 +233,7 @@ export interface CallRenameLLMOptions {
233
233
  * assistant message,final text 零遍历可得)。
234
234
  *
235
235
  * 收口要点(对比旧版搭便车逻辑):
236
- * - model:空 ref → `ctx.model` 跟随会话主模型(设计 rename-session-three-modes.md D5,消灭「默认配置静默不工作」);
236
+ * - model:空 ref → `ctx.model` 跟随会话主模型(消灭「默认配置静默不工作」);
237
237
  * 非空 ref → `resolveModel(ctx, config.model)` 独立选模(解析失败静默跳过 + warn)
238
238
  * - systemPrompt:`RENAME_SYSTEM_PROMPT` 精简版(旧版 `ctx.getSystemPrompt()` 整个 agent prompt)
239
239
  * - messages:两段信号 [user(prompt), assistant(finalText), user(instruction)]
@@ -256,7 +256,7 @@ export async function callRenameLLM(
256
256
  ): Promise<string | null> {
257
257
  // 内部顺序不可调换(E2E 竞态断言依赖「内省日志在请求发起前打出」):
258
258
  // resolveModel(含空 ref fallback)→ extract prompt → extract finalText → truncate ×2 → build → debug 内省 → callLLM
259
- // 模型解析(设计 rename-session-three-modes.md D5):空 ref = 未配置语义 → 跟随会话主模型(ctx.model,探针 P2 实测首轮可用;
259
+ // 模型解析:空 ref = 未配置语义 → 跟随会话主模型(ctx.model,探针 P2 实测首轮可用;
260
260
  // undefined 时无 fallback 走下方静默跳过);非空 ref 解析失败 = 显式配错 → 同一守卫静默跳过 + warn。
261
261
  const model =
262
262
  config.model.ref === "" ? ctx.model : resolveModel(ctx, config.model);
@@ -320,7 +320,7 @@ export async function callRenameLLM(
320
320
  }
321
321
 
322
322
  // A1 成功路径日志(B2+B3 修正):默认不输出,避免常开 console.warn 污染 Pi 输入框;
323
- // 需要排查时设 XYZ_AGENT_DEBUG=1,经 debugLog 输出(带时间戳)。
323
+ // 需要排查时设 TAIJI_AGENT_DEBUG=1,经 debugLog 输出(带时间戳)。
324
324
  // - B2 位置:原在 callLLM 调用前打出,失败时会误导(日志已落但 rename 未发生);移到 result.ok 确认后
325
325
  // - B3 文案:补 provider 前缀,区分两个 provider 同名 model
326
326
  debugLog(`rename with model ${model.provider}/${model.id}`);
package/src/pure.ts CHANGED
@@ -15,19 +15,19 @@ import {
15
15
 
16
16
  // ──────────────────────── 配置 ────────────────────────
17
17
 
18
- /** 触发模式三值枚举(设计 rename-session-three-modes.md D1):互斥,默认 "first-stop"(零行为迁移)。 */
18
+ /** 触发模式三值枚举:互斥,默认 "first-stop"(零行为迁移)。 */
19
19
  export type RenameMode = "first-prompt" | "first-stop" | "agent-tool";
20
20
 
21
21
  /**
22
22
  * rename-session 配置 schema(落盘到 `<agentDir>/config/rename-session-ext-config.json`,路径由 llm-shared 推导)。
23
23
  *
24
24
  * 收口自旧版 pure.ts 的 `RenameConfig`(switchFilePath/maxTitleLength/renameInstruction 硬编码常量):
25
- * - 开关双机制:`enabled` 字段(pi CLI 用户主开关,默认 false)+ xyz-agent runtime 的
25
+ * - 开关双机制:`enabled` 字段(pi CLI 用户主开关,默认 false)+ taiji runtime 的
26
26
  * auto-rename-enabled flag 文件(live 覆盖源,存在即开,见下方 [COMPAT] 契约)
27
27
  * - model 从「搭便车 ctx.model」改为独立 `ModelSelector`(仅支持 ref 精确指定;空 ref 跟随会话主模型,见 llm.ts 的空 ref fallback)
28
28
  * - maxTitleLength 保留(默认 50)
29
29
  * - renameInstruction 不进配置(i18n 留未来),由代码常量 RENAME_INSTRUCTION 承载
30
- * - mode(设计 rename-session-three-modes.md D1):first-prompt 首条 user 消息触发 / first-stop 首个成功 round 末触发(默认,现状)/
30
+ * - modefirst-prompt 首条 user 消息触发 / first-stop 首个成功 round 末触发(默认,现状)/
31
31
  * agent-tool 不自动生成、注册 rename_session 工具由 agent 自主改名
32
32
  */
33
33
  export interface RenameSessionConfig {
@@ -72,20 +72,20 @@ export const DEFAULT_RENAME_CONFIG: RenameSessionConfig = {
72
72
  /** llm-shared loadConfig/saveConfig 的包名(决定文件名 rename-session-ext-config.json,llm-shared getConfigPath 追加 -ext-config.json 后缀)。 */
73
73
  const CONFIG_PKG = "rename-session";
74
74
 
75
- // ──────────────────────── xyz-agent runtime 开关契约(live 覆盖源) ────────────────────────
75
+ // ──────────────────────── taiji runtime 开关契约(live 覆盖源) ────────────────────────
76
76
 
77
77
  /**
78
- * [COMPAT] xyz-agent runtime 开关契约文件:<agentDir>/auto-rename-enabled(存在=开,不存在=关)。
78
+ * [COMPAT] taiji runtime 开关契约文件:<agentDir>/auto-rename-enabled(存在=开,不存在=关)。
79
79
  * Added in v0.4.0. Remove after v1.0.0(旧 runtime 版本淘汰后随 flag 契约一并移除)。
80
80
  *
81
- * 背景:已发布的 xyz-agent runtime(worktree-config-helper.ts)只认这个 flag 文件——
81
+ * 背景:已发布的 taiji runtime(worktree-config-helper.ts)只认这个 flag 文件——
82
82
  * SystemPage 开关读写它、首启 ensureAutoRenameDefault 默认创建它,且这部分代码随桌面 app
83
83
  * 发布、不随本 extension 升级。若本扩展单方面改为只读 config JSON 并在迁移时删除 flag,
84
84
  * 则 mandatory 自动升级后所有未更新桌面 app 的用户:UI 显示 OFF 而 extension 实际 ON,
85
85
  * 且 SystemPage toggle 永久失效(旧 runtime 只写 flag,新 extension 不再读)。
86
86
  *
87
87
  * 契约语义(loadRenameConfig 每次调用 live 检查,非一次性迁移):
88
- * - flag 存在 → enabled 强制 true(xyz-agent runtime 的开关打开,覆盖 config.enabled)
88
+ * - flag 存在 → enabled 强制 true(taiji runtime 的开关打开,覆盖 config.enabled)
89
89
  * - flag 不存在 → 回落 config.enabled(pi CLI 用户的主开关机制,见 config skill)
90
90
  * - 扩展永不删除/创建该文件,除非用户通过 /auto-rename on|off 显式操作(commands.ts 双写同步)
91
91
  */
@@ -97,7 +97,7 @@ function getAutoRenameFlagPath(): string {
97
97
  }
98
98
 
99
99
  /**
100
- * 设置 xyz-agent runtime 开关契约 flag(/auto-rename on|off 命令调用,与 config 双写同步)。
100
+ * 设置 taiji runtime 开关契约 flag(/auto-rename on|off 命令调用,与 config 双写同步)。
101
101
  * enabled=true 创建空 flag 文件;enabled=false 删除(不存在时视为成功)。best-effort 不抛错。
102
102
  */
103
103
  export function setAutoRenameSwitch(enabled: boolean): void {
@@ -131,7 +131,7 @@ export function normalizeRenameConfig(raw: unknown): RenameSessionConfig {
131
131
 
132
132
  const enabled = typeof obj.enabled === "boolean" ? obj.enabled : DEFAULT_RENAME_CONFIG.enabled;
133
133
 
134
- // mode 逐字段校验(设计 rename-session-three-modes.md D1):旧 config 无字段 / 非法值 → 默认 first-stop(零迁移,现状行为)
134
+ // mode 逐字段校验:旧 config 无字段 / 非法值 → 默认 first-stop(零迁移,现状行为)
135
135
  const mode = isRenameMode(obj.mode) ? obj.mode : DEFAULT_RENAME_CONFIG.mode;
136
136
 
137
137
  const maxTitleLength =
@@ -155,19 +155,19 @@ export function normalizeRenameConfig(raw: unknown): RenameSessionConfig {
155
155
  * 加载配置(mtime+size 缓存;文件缺失/损坏返回默认,不抛错)。
156
156
  *
157
157
  * 配置值优先级(从高到低):
158
- * 1. xyz-agent runtime 开关契约(flag 文件存在 → enabled=true)
158
+ * 1. taiji runtime 开关契约(flag 文件存在 → enabled=true)
159
159
  * 2. 配置文件(<agentDir>/config/rename-session-ext-config.json)
160
160
  * 3. 默认值
161
161
  *
162
162
  * [HISTORICAL] 原 env 覆盖层(`PI_` + 包名前缀四键,最高优先级)已删(设计 rename-session-three-modes.md D6,吸收 ext-simplify-15 D1):
163
163
  * 全仓 0 生产 setter、4 键中 3 键从未被用过,唯一用法是 1 个历史验收场景。预置该前缀的
164
- * 环境变量不再有任何效果(幽灵负面场景见设计 rename-session-three-modes.md V8,负面用例在 pure.test.ts)。
164
+ * 环境变量不再有任何效果(负面用例在 pure.test.ts)。
165
165
  */
166
166
  export function loadRenameConfig(): RenameSessionConfig {
167
167
  // 1. 从配置文件加载基础配置(带 mtime+size 缓存)
168
168
  const fileConfig = loadConfig(CONFIG_PKG, DEFAULT_RENAME_CONFIG, normalizeRenameConfig);
169
169
 
170
- // 2. xyz-agent runtime 开关契约(flag 文件存在 → enabled 强制 true,覆盖 config)
170
+ // 2. taiji runtime 开关契约(flag 文件存在 → enabled 强制 true,覆盖 config)
171
171
  if (existsSync(getAutoRenameFlagPath())) {
172
172
  return { ...fileConfig, enabled: true };
173
173
  }
@@ -185,7 +185,7 @@ export function saveRenameConfig(
185
185
  // ──────────────────────── 首 turn / 首 prompt 判定 ────────────────────────
186
186
 
187
187
  /**
188
- * 数 session entries 中的 user message 条数(first-prompt 模式首条判定用,设计 rename-session-three-modes.md D2)。
188
+ * 数 session entries 中的 user message 条数(first-prompt 模式首条判定用)。
189
189
  *
190
190
  * 调用时点契约:pi 的 extension handler 先于该条 message 的 entries append 执行
191
191
  * (agent-session.js `_emitExtensionEvent` 先于 `appendMessage`,探针 P1 已实测),
package/vitest.config.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { defineConfig } from "vitest/config";
1
+ import { taijiTestConfig } from "../../../test-guard/factory.ts"
2
2
 
3
- export default defineConfig({
3
+ export default taijiTestConfig({
4
4
  test: {
5
- reporters: ["default", "junit"],
6
- outputFile: { junit: "./test-results/vitest-junit.xml" },
7
5
  // e2e/harness.test.mjs:E2E harness 断言纯函数单测(无网络/无进程 spawn,随单测跑)。
8
6
  // 场景脚本 run-aN.mjs 与 vitest 包装 scenarios.test.mjs(真实 pi + 真实模型,2-15 分钟,
9
7
  // 依赖 ~/.pi/agent/auth.json)必须留在常规 CI 之外——显式跑用