@zhushanwen/pi-rename-session 0.9.0 → 0.9.1

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
@@ -53,7 +53,7 @@ pi install npm:@zhushanwen/pi-rename-session
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 相关精简项裁决见本仓 `docs/design/rename-session-three-modes.md` 附录 A
56
+ > [HISTORICAL] 原最高优先级的 `PI_RENAME_*` 环境变量覆盖层已删(全仓 0 生产 setter,4 键中 3 键从未被用过)——预置该前缀的环境变量不再有任何效果。rename-session 相关精简项裁决见原设计文档 rename-session-three-modes.md 附录 A(已删除,git 可追溯)。
57
57
 
58
58
  ## 命令
59
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-rename-session",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "xyz-agent": {
@@ -33,8 +33,9 @@
33
33
  "pi-package"
34
34
  ],
35
35
  "dependencies": {
36
+ "@zhushanwen/pi-ext-guards": "0.4.0",
36
37
  "@zhushanwen/pi-extension-logger": "0.6.0",
37
- "@zhushanwen/pi-llm-shared": "0.7.0"
38
+ "@zhushanwen/pi-llm-shared": "0.8.0"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@vitest/coverage-v8": "^4.1.9",
@@ -102,7 +102,7 @@ interface RenameSessionConfig {
102
102
  }
103
103
  ```
104
104
 
105
- > [HISTORICAL] 原最高优先级的 `PI_RENAME_*` 环境变量覆盖层已删(全仓 0 生产 setter)——预置该前缀的环境变量不再有任何效果。rename-session 相关精简项裁决见本仓 `docs/design/rename-session-three-modes.md` 附录 A
105
+ > [HISTORICAL] 原最高优先级的 `PI_RENAME_*` 环境变量覆盖层已删(全仓 0 生产 setter)——预置该前缀的环境变量不再有任何效果。rename-session 相关精简项裁决见原设计文档 rename-session-three-modes.md 附录 A(已删除,git 可追溯)。
106
106
 
107
107
  ## 配置生效时机
108
108
 
package/src/llm.ts CHANGED
@@ -2,10 +2,11 @@ import path from "node:path";
2
2
 
3
3
  import type { AssistantMessage, Message, Usage } from "@earendil-works/pi-ai";
4
4
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
5
+ import { isRecord } from "@zhushanwen/pi-ext-guards";
5
6
  import { getLogger } from "@zhushanwen/pi-extension-logger";
6
- import { callLLM, resolveModel } from "@zhushanwen/pi-llm-shared";
7
+ import { callLLM, joinTextBlocks, resolveModel } from "@zhushanwen/pi-llm-shared";
7
8
 
8
- import { type RenameSessionConfig, cleanTitle } from "./pure.js";
9
+ import { type RenameSessionConfig, cleanTitle, truncateCodePoints } from "./pure.js";
9
10
 
10
11
  const logger = getLogger("rename-session");
11
12
 
@@ -36,7 +37,7 @@ export const RENAME_INSTRUCTION =
36
37
  * sessionDir 路径含 subagents 段 → 是 subagent 子进程 session,跳过 rename。
37
38
  *
38
39
  * 跨包路径耦合(constraints.json C-ext-21):`subagents` 目录布局由 xyz-agent 的
39
- * `packages/subagent-core/src/execution/path-encoding.ts`(getSubagentSessionDir /
40
+ * `packages/subagent-core/src/execution/assembly/path-encoding.ts`(getSubagentSessionDir /
40
41
  * encodeCwd)定义并写入。本包 role=universal,不 import subagent-core
41
42
  * (import 会把 universal 包绑死在 xyz-agent 体系上),只按路径形态守卫——
42
43
  * path-encoding.ts 侧有对向注释互指。改 subagent 目录布局必须双侧同步改。
@@ -53,23 +54,10 @@ interface EntryLike {
53
54
  }
54
55
 
55
56
  // ──────────────────────── 标题输入构造(两段信号:首条 user prompt / 触发 turn 最终回复) ────────────────────────
56
-
57
- /** unknown Record 的运行时守卫:extractUserPromptText 逐字段消费 session entries 的宽松数据,字段存在性与类型由守卫核实(不使用 as any)。 */
58
- function isRecord(value: unknown): value is Record<string, unknown> {
59
- return typeof value === "object" && value !== null;
60
- }
61
-
62
- /** 拼接 content 内 type==="text" blocks 的文本(join(" "),跳过非 text block 与非 string text)。 */
63
- function joinTextBlocks(content: unknown): string {
64
- if (!Array.isArray(content)) return "";
65
- const texts: string[] = [];
66
- for (const block of content) {
67
- if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
68
- texts.push(block.text);
69
- }
70
- }
71
- return texts.join(" ");
72
- }
57
+ //
58
+ // isRecord / joinTextBlocks 均为共享导出(ext-simplify-17 D3/D7):isRecord ext-guards
59
+ // 排数组严版(数组 message 输入下三消费点 continue/return "" 殊途同归,等价论证见设计
60
+ // 文档 §3.1 D3);joinTextBlocks llm-shared unknown 安全内核(不 trim,trim 是调用方策略)。
73
61
 
74
62
  /**
75
63
  * 取 session entries 中首条 user message 的 prompt 文本(标题输入信号之一)。
@@ -116,13 +104,11 @@ const MAX_TITLE_INPUT_CODE_POINTS = 4000;
116
104
 
117
105
  /**
118
106
  * 按 Unicode 码点截断标题输入段(两段信号各 4000 码点,中文场景约 4k token/段,成本可控)。
119
- * Array.from 按码点切分,星面字符(emoji 等,占 2 UTF-16 码元)不会被劈成半个代理对;
120
- * 超长才追加 '…' 后缀,≤ 上限(含恰好等于)原样返回。
107
+ * 截断内核见 pure.ts truncateCodePoints(ext-simplify-17 D13 收口);超长才追加 '…' 后缀,
108
+ * 上限(含恰好等于)原样返回。
121
109
  */
122
110
  export function truncateForTitle(text: string): string {
123
- const chars = Array.from(text);
124
- if (chars.length <= MAX_TITLE_INPUT_CODE_POINTS) return text;
125
- return chars.slice(0, MAX_TITLE_INPUT_CODE_POINTS).join("") + "…";
111
+ return truncateCodePoints(text, MAX_TITLE_INPUT_CODE_POINTS, "…");
126
112
  }
127
113
 
128
114
  /**
@@ -195,16 +181,17 @@ const PREVIEW_TAIL_CODE_POINTS = 100;
195
181
 
196
182
  /**
197
183
  * debug 日志的文本预览(≤300 码点直接全文;超长输出 head 200 码点 + 字面 … + tail 100 码点
198
- * (head/tail 双段支撑 E2E 对长 prompt 首尾片段的断言)。按码点截断(与 truncateForTitle 同单位,
199
- * Array.from 切分,代理对/emoji 不被劈开);e2e/harness.mjs rebuildPreview 是同构实现,两处必须同步改。
184
+ * (head/tail 双段支撑 E2E 对长 prompt 首尾片段的断言)。截断内核见 pure.ts truncateCodePoints
185
+ * (ext-simplify-17 D13 收口,双段形态经 head/keepTail 参数表达——300/200/100 15 号 §6.4
186
+ * D4 裁决的三个独立契约数字);e2e/harness.mjs 的 rebuildPreview 是同构实现,两处必须同步改。
200
187
  */
201
188
  function previewText(text: string): string {
202
- const chars = Array.from(text);
203
- if (chars.length <= PREVIEW_MAX_CODE_POINTS) return text;
204
- return (
205
- chars.slice(0, PREVIEW_HEAD_CODE_POINTS).join("") +
206
- "…" +
207
- chars.slice(-PREVIEW_TAIL_CODE_POINTS).join("")
189
+ return truncateCodePoints(
190
+ text,
191
+ PREVIEW_MAX_CODE_POINTS,
192
+ "",
193
+ PREVIEW_HEAD_CODE_POINTS,
194
+ PREVIEW_TAIL_CODE_POINTS,
208
195
  );
209
196
  }
210
197
 
package/src/pure.ts CHANGED
@@ -4,8 +4,11 @@ import { dirname, join } from "node:path";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import type { ModelThinkingLevel } from "@earendil-works/pi-ai";
6
6
 
7
+ import { isEnoentError } from "@zhushanwen/pi-ext-guards";
7
8
  import {
9
+ isThinkingLevel,
8
10
  loadConfig,
11
+ normalizeModelSelector,
9
12
  saveConfig,
10
13
  type ModelSelector,
11
14
  } from "@zhushanwen/pi-llm-shared";
@@ -44,17 +47,6 @@ export interface RenameSessionConfig {
44
47
  thinkingLevel: ModelThinkingLevel;
45
48
  }
46
49
 
47
- /** 合法 thinking 级别清单(与 pi-ai ModelThinkingLevel 一致;normalize 校验用)。Set 免 as 断言。 */
48
- const THINKING_LEVELS: ReadonlySet<string> = new Set([
49
- "off",
50
- "minimal",
51
- "low",
52
- "medium",
53
- "high",
54
- "xhigh",
55
- "max",
56
- ]);
57
-
58
50
  // ──────────────────────── 枚举校验 ────────────────────────
59
51
 
60
52
  /** 合法触发模式清单(与 RenameMode 一致;normalize 校验用)。Set 免 as 断言。 */
@@ -68,14 +60,6 @@ function isRenameMode(raw: unknown): raw is RenameMode {
68
60
  return typeof raw === "string" && RENAME_MODES.has(raw);
69
61
  }
70
62
 
71
- /**
72
- * 类型谓词:unknown 是否为合法 thinking 级别(normalizeRenameConfig 校验用,单点断言)。
73
- * Set.has 运行时兜底 + 类型收窄,调用方无需再断言。
74
- */
75
- function isThinkingLevel(raw: unknown): raw is ModelThinkingLevel {
76
- return typeof raw === "string" && THINKING_LEVELS.has(raw);
77
- }
78
-
79
63
  /** 默认配置:关闭、空 ref(跟随会话主模型,见 llm.ts 的空 ref fallback)、first-stop 触发、标题上限 50、不启用 thinking。 */
80
64
  export const DEFAULT_RENAME_CONFIG: RenameSessionConfig = {
81
65
  enabled: false,
@@ -128,8 +112,7 @@ export function setAutoRenameSwitch(enabled: boolean): void {
128
112
  rmSync(flagPath);
129
113
  } catch (e: unknown) {
130
114
  // flag 不存在视为已关(吞 ENOENT);其他错误(如权限)如实抛出,不静默
131
- const code = (e as NodeJS.ErrnoException).code;
132
- if (code !== "ENOENT") throw e;
115
+ if (!isEnoentError(e)) throw e;
133
116
  }
134
117
  }
135
118
  }
@@ -158,6 +141,7 @@ export function normalizeRenameConfig(raw: unknown): RenameSessionConfig {
158
141
  ? obj.maxTitleLength
159
142
  : DEFAULT_RENAME_CONFIG.maxTitleLength;
160
143
 
144
+ // normalizeModelSelector:llm-shared 导出(ext-simplify-17 D6 收口)
161
145
  const model = normalizeModelSelector(obj.model) ?? DEFAULT_RENAME_CONFIG.model;
162
146
 
163
147
  const thinkingLevel = isThinkingLevel(obj.thinkingLevel)
@@ -167,16 +151,6 @@ export function normalizeRenameConfig(raw: unknown): RenameSessionConfig {
167
151
  return { enabled, model, mode, maxTitleLength, thinkingLevel };
168
152
  }
169
153
 
170
- /** 校验 ModelSelector:只支持 ref 精确指定,其余形式非法返回 null。 */
171
- function normalizeModelSelector(raw: unknown): ModelSelector | null {
172
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
173
- const obj = raw as Record<string, unknown>;
174
- if (obj.type === "ref" && typeof obj.ref === "string") {
175
- return { type: "ref", ref: obj.ref };
176
- }
177
- return null;
178
- }
179
-
180
154
  /**
181
155
  * 加载配置(mtime+size 缓存;文件缺失/损坏返回默认,不抛错)。
182
156
  *
@@ -258,6 +232,36 @@ export function countSuccessfulAssistantReplies(entries: ReadonlyArray<EntryLike
258
232
 
259
233
  // ──────────────────────── 标题清洗 ────────────────────────
260
234
 
235
+ /**
236
+ * 按 Unicode 码点截断(ext-simplify-17 D13:收口包内三处同构 `Array.from` 骨架——
237
+ * cleanTitle / truncateForTitle / previewText,三处差异全部参数化)。
238
+ *
239
+ * Array.from 按码点切分,星面字符(emoji 等,占 2 个 UTF-16 码元)不会被劈成半个代理对。
240
+ * 码点数 ≤ max(含恰好等于)原样返回;超长返回「前 head 码点 + tail + 原文最后 keepTail 码点」。
241
+ *
242
+ * @param max 判定阈值:码点数 ≤ max 原样返回(与 head 独立——previewText 的 300/200/100 是
243
+ * 15 号 §6.4 D4 裁决的三个独立契约数字,不得由 head+keepTail 推导)
244
+ * @param tail 超长时追加在 head 段后的字面中缀(如 "…";单段形态传 "")
245
+ * @param head 超长时保留的头部码点数(默认 = max,单段形态下阈值即截断长度)
246
+ * @param keepTail 超长时保留的原文尾部码点数(默认 0 = 单段形态;双段形态的调用方
247
+ * 自负 head/keepTail 不重叠——本包内 previewText 的 300/200/100 在超长区间恒不重叠)
248
+ */
249
+ export function truncateCodePoints(
250
+ text: string,
251
+ max: number,
252
+ tail: string,
253
+ head = max,
254
+ keepTail = 0,
255
+ ): string {
256
+ const chars = Array.from(text);
257
+ if (chars.length <= max) return text;
258
+ return (
259
+ chars.slice(0, head).join("") +
260
+ tail +
261
+ (keepTail > 0 ? chars.slice(-keepTail).join("") : "")
262
+ );
263
+ }
264
+
261
265
  /**
262
266
  * rename 专属后处理:去首尾成对引号(单/双/中文)+ markdown 强调标记(* ** ` _)+ 尾部标点,按 Unicode 码点截断。
263
267
  *
@@ -281,8 +285,6 @@ export function cleanTitle(content: string, maxLength: number): string {
281
285
  .trim();
282
286
  if (!cleaned) return "";
283
287
 
284
- // 按 Unicode 码点截断(避免截断多字节字符)
285
- const chars = Array.from(cleaned);
286
- if (chars.length <= maxLength) return cleaned;
287
- return chars.slice(0, maxLength).join("");
288
+ // 按 Unicode 码点截断(避免截断多字节字符;无尾部后缀的单段形态)
289
+ return truncateCodePoints(cleaned, maxLength, "");
288
290
  }