@pi-claudian/auto-save-to-markdown 0.1.1 → 0.1.2

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
@@ -105,6 +105,16 @@ kept in a collapsible `<details>` block) and summarizes each tool call and
105
105
  result in one line, so the file stays readable while still showing what the
106
106
  agent did.
107
107
 
108
+ ### Fragmented thinking repair
109
+
110
+ Some upstream reasoning streams (observed with z-ai/GLM via OpenRouter) store
111
+ thinking with every word — or every CJK character — on its own line: the
112
+ original spaces collapse into leading spaces of one-word fragments joined by
113
+ runs of newlines. The extension detects this corruption (lines starting with a
114
+ single leading space, or a majority of 1–2-character fragment lines) and
115
+ re-joins the fragments into flowing text, so saved thinking reads normally
116
+ instead of one word per line. Clean thinking blocks are written untouched.
117
+
108
118
  `cost` and the token fields cover the whole saved branch and include cached
109
119
  tokens (priced at the provider's cache rates), so the totals are comparable
110
120
  with provider-side accounting (e.g. OpenRouter activity). Requests that never
package/README.zh.md CHANGED
@@ -94,6 +94,14 @@ auth 重构之后登录页一直重定向死循环……
94
94
  `<details>` 块中),每个工具调用和结果各压缩成一行摘要,既可读又能看出
95
95
  agent 做了什么。
96
96
 
97
+ ### 碎片化 thinking 修复
98
+
99
+ 部分上游推理流(在 z-ai/GLM 经 OpenRouter 的场景中观察到)会把 thinking
100
+ 存成一词一行、甚至一字一行:原始空格塌缩成碎片行开头的单个空格,碎片之间
101
+ 被成串的换行拼接。扩展会检测这种损坏(依据带单个前导空格的行、或大量
102
+ 1–2 字符碎片行),把碎片重新接回通顺的文本,保存的 thinking 不再一行
103
+ 一词。正常的 thinking 块原样保存,不做任何改动。
104
+
97
105
  `cost` 和 token 字段统计整条已保存分支,且包含缓存 token(按供应商缓存
98
106
  价格计费),因此总计可与供应商侧账单(如 OpenRouter Activity)对照。未
99
107
  进入会话树的请求(失败重试、共用同一 API key 的其他会话)不在其中。
package/index.ts CHANGED
@@ -32,6 +32,9 @@
32
32
  * - Compaction: files archive the ORIGINAL messages (getBranch() returns the
33
33
  * raw tree path, not the compaction-aware context), so a compacted session
34
34
  * still exports its complete history.
35
+ * - Thinking repair: reasoning blocks stored with the upstream
36
+ * newline-fragmentation corruption (one word per line) are detected and
37
+ * re-joined into flowing text before saving; clean thinking is untouched.
35
38
  *
36
39
  * Manual command: `/save-conversation` saves the current branch immediately
37
40
  * and reports the file path.
@@ -122,6 +125,59 @@ interface BranchMeta {
122
125
  projectRoot: string;
123
126
  }
124
127
 
128
+ // ---------- thinking fragmentation repair ----------
129
+
130
+ /**
131
+ * Some upstream reasoning streams (observed with z-ai/GLM via OpenRouter) store
132
+ * thinking as one word — or one CJK character — per line: the stream splits
133
+ * tokens into fragments joined by runs of newlines, and the original spaces
134
+ * survive only as leading spaces of the fragments. The saved markdown then has
135
+ * every token on its own line, which is miserable to read and bloats storage.
136
+ *
137
+ * Detection uses two signatures validated against ~520 real thinking blocks:
138
+ * lines starting with exactly one space (a survived word separator; blank-ish
139
+ * " " lines included), and an excess of 1–2-char non-list-marker lines (CJK
140
+ * fragments carry no leading space). Clean thinking never matches either.
141
+ *
142
+ * Repair strips all newlines — run length carries no recoverable meaning (the
143
+ * same paragraph boundary appears as 1, 2 or 3 newlines, while 4–7 can sit
144
+ * mid-sentence) — and collapses the doubled spaces left by lone-space
145
+ * fragments. Clean blocks pass through untouched.
146
+ */
147
+
148
+ /** Line whose single leading space is a survived word separator. */
149
+ function isThinkingSigLine(line: string): boolean {
150
+ return line === " " || /^ [^ *+\-\d]/.test(line);
151
+ }
152
+
153
+ /** Non-blank line of 1–2 chars that is not a standalone list marker. */
154
+ function isThinkingShortFragment(line: string): boolean {
155
+ const s = line.trim();
156
+ if (s.length === 0 || s.length > 2) return false;
157
+ return !/^([-*+]|\d+[.)])$/.test(s);
158
+ }
159
+
160
+ /** Whether a thinking block shows the newline-fragmentation corruption. */
161
+ function isFragmentedThinking(s: string): boolean {
162
+ const lines = s.split("\n");
163
+ const nonBlank = lines.filter((l) => l.trim().length > 0);
164
+ if (nonBlank.length === 0) return false;
165
+ const sig = lines.filter(isThinkingSigLine).length;
166
+ if (nonBlank.length < 8) return nonBlank.length >= 3 && sig >= 3;
167
+ if (sig / lines.length >= 0.12) return true;
168
+ return nonBlank.filter(isThinkingShortFragment).length / nonBlank.length >= 0.4;
169
+ }
170
+
171
+ /** Repair newline-fragmented thinking; clean thinking is returned unchanged. */
172
+ function repairThinking(s: string): string {
173
+ if (!isFragmentedThinking(s)) return s;
174
+ debug("repairing fragmented thinking block:", s.length, "chars");
175
+ return s
176
+ .replace(/\n+/g, "")
177
+ .replace(/[ \t]{2,}/g, " ")
178
+ .trim();
179
+ }
180
+
125
181
  export default function (pi: ExtensionAPI) {
126
182
  /**
127
183
  * Resolve the target directory. The env var may hold a relative folder name
@@ -242,7 +298,7 @@ export default function (pi: ExtensionAPI) {
242
298
  flushThinking();
243
299
  parts.push(b.text);
244
300
  } else if (b.type === "thinking") {
245
- thinkings.push(b.thinking);
301
+ thinkings.push(repairThinking(b.thinking));
246
302
  } else if (b.type === "toolCall") {
247
303
  flushThinking();
248
304
  calls.push(`- \`${b.name}\` — ${previewArgs(b.arguments)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-claudian/auto-save-to-markdown",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Pi extension that automatically saves each completed conversation turn as a markdown file with YAML frontmatter, one file per session-tree branch.",
5
5
  "type": "module",
6
6
  "license": "MIT",