@thincoder/core 0.9.4 → 0.10.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,194 @@
1
+ /**
2
+ * declaration.mjs — 声明装载面(PROJECT-MANIFEST.json 三族 `codePaths` / `index.*` / `advisor.*`)
3
+ * · 自 `conventions.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786。
4
+ * 投影单源 = `manifest.mjs`(ONE reader / ONE root resolution——`readManifest` + `manifestFilePath`;
5
+ * name / shape / defaults = schema 面 `docs/core/design/MANIFEST.md` §2.2)。迁出块逐字;
6
+ * 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
7
+ */
8
+ import { existsSync } from "node:fs"
9
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
10
+ import { DEFAULT_MANIFEST, manifestFilePath, readManifest } from "./manifest.mjs"
11
+ import { logEvent } from "./log.mjs"
12
+
13
+ /** Retired declaration carrier (project-root relative). Its EXISTENCE is checked, its
14
+ * content is never read — the declaration surface = the manifest three families (KD-M1-34). */
15
+ const RETIRED_REL_PATH = ".thincoder/conventions.json"
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Declaration loading — the three families of PROJECT-MANIFEST.json, projected
19
+ // through manifest.mjs (ONE reader / ONE root resolution) and cached per DATA-FILE
20
+ // PATH (`clearDeclarationCache()` is the test seam; declarations change rarely).
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ function normalizeExtensions(v) {
24
+ if (!Array.isArray(v)) return []
25
+ const out = []
26
+ for (const e of v) {
27
+ if (typeof e !== "string") continue
28
+ const t = e.trim().toLowerCase()
29
+ if (!t) continue
30
+ const ext = t.startsWith(".") ? t : `.${t}`
31
+ if (!out.includes(ext)) out.push(ext)
32
+ }
33
+ return out
34
+ }
35
+
36
+ /** Declared code paths replace the default (replacement, not union — §3.1). An EMPTY
37
+ * array is a legal declaration ("no segment-based code face") — only a non-array value
38
+ * falls back to `null` (⇒ caller keeps the default). */
39
+ function normalizeCodePaths(v) {
40
+ if (!Array.isArray(v)) return null
41
+ const out = []
42
+ for (const e of v) {
43
+ if (typeof e !== "string") continue
44
+ const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
45
+ if (!s || out.includes(s)) continue
46
+ out.push(s)
47
+ }
48
+ return out
49
+ }
50
+
51
+ /** 路径列表归一(单源——`index.excludePaths` §6.14 ∥ `index.publicRepos` §6.15 同款):首部 `./` 剥离 · `\`→`/` · 去尾斜杠 · 空 ∕ 非串剔除 · 去重保序。 */
52
+ function normalizePathList(v) {
53
+ if (!Array.isArray(v)) return []
54
+ const out = []
55
+ for (const e of v) {
56
+ if (typeof e !== "string") continue
57
+ const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
58
+ if (!s || out.includes(s)) continue
59
+ out.push(s)
60
+ }
61
+ return out
62
+ }
63
+
64
+ function normalizeString(v) {
65
+ return typeof v === "string" && v.trim() ? v.trim() : ""
66
+ }
67
+
68
+ /** Project the manifest three families into the frozen declaration object.
69
+ * `declared` = VALUE comparison against the defaults — at least one family deviates
70
+ * (a key written with its default value still counts as NOT declared; the design-gate
71
+ * hint reads it to decide whether to point at the manifest, PORTABILITY §3.1).
72
+ * `root` = the project root this declaration belongs to (`dirname(manifestFilePath(cwd))` —
73
+ * the segment-matching face's anchor, D18); NOT a declared value (excluded from `declared`);
74
+ * `null` = unknown root (`DEFAULT_DECLARATION`). */
75
+ function buildDeclaration(m, root = null) {
76
+ const codePaths = normalizeCodePaths(m?.codePaths) ?? [...DEFAULT_MANIFEST.codePaths]
77
+ const codeExtensions = normalizeExtensions(m?.index?.codeExtensions)
78
+ const docExtensions = normalizeExtensions(m?.index?.docExtensions)
79
+ const excludePaths = normalizePathList(m?.index?.excludePaths)
80
+ const publicRepos = normalizePathList(m?.index?.publicRepos)
81
+ const docMap = normalizeString(m?.advisor?.docMap)
82
+ const standardsDoc = normalizeString(m?.advisor?.standardsDoc)
83
+ const defaults = normalizeCodePaths(DEFAULT_MANIFEST.codePaths) ?? []
84
+ const pathsDiffer = codePaths.length !== defaults.length || codePaths.some((p, i) => p !== defaults[i])
85
+ const declared = pathsDiffer || codeExtensions.length > 0 || docExtensions.length > 0 || excludePaths.length > 0 || publicRepos.length > 0 || Boolean(docMap) || Boolean(standardsDoc)
86
+ return Object.freeze({
87
+ declared,
88
+ codePaths: Object.freeze(codePaths),
89
+ index: Object.freeze({
90
+ codeExtensions: Object.freeze(codeExtensions),
91
+ docExtensions: Object.freeze(docExtensions),
92
+ excludePaths: Object.freeze(excludePaths),
93
+ publicRepos: Object.freeze(publicRepos),
94
+ }),
95
+ advisor: Object.freeze({ docMap, standardsDoc }),
96
+ root,
97
+ })
98
+ }
99
+
100
+ /** Full-default declaration (no manifest / unusable manifest) — the fallback every
101
+ * consumer gets; built from `DEFAULT_MANIFEST` so the default values stay single-source. */
102
+ export const DEFAULT_DECLARATION = buildDeclaration(null)
103
+
104
+ /** 排除前缀谓词(单源——§6.14 面①):`rel`(**`base` 相对**——缺省视为根相对)命中声明前缀(恰等
105
+ * ∨ 后随 `/`——`openclaw` 不吞 `openclaw-fork`)⇒ true;缺省 `[]` ⇒ 恒 false(零行为变化)。
106
+ * `base`(调用面 cwd/origin·#700)= `rel` 的相对基:`resolve` + `relative` 换算至 `decl.root` 根面
107
+ * 再比前缀(cwd = 项目根时与既有行为逐字等义);换算越出根面(`..` 头 ∥ 根外绝对形)⇒ **不命中**
108
+ * (保守不排除——沉默洞方向收窄,与 D17 aux 面「indeterminate ⇒ 不匹配」同向);`decl.root` 缺
109
+ * ⇒ `rel` 原样(根起步调用面等义)。 */
110
+ export function isExcludedRelPath(rel, decl, base = null) {
111
+ const list = decl?.index?.excludePaths
112
+ if (!Array.isArray(list) || list.length === 0) return false
113
+ let s = String(rel ?? "").replace(/\\/g, "/")
114
+ const root = decl?.root
115
+ if (base != null && root) {
116
+ const converted = relative(root, resolve(base, s)).replace(/\\/g, "/")
117
+ if (converted === ".." || converted.startsWith("../") || isAbsolute(converted)) return false
118
+ s = converted
119
+ }
120
+ return list.some((p) => p && (s === p || s.startsWith(`${p}/`)))
121
+ }
122
+
123
+ /**
124
+ * 声明公共仓**绝对根集**(§6.15 解析层——检索同步 ∥ 读面 origin 集 ∥ 引用解析三消费面共用的单一定义,
125
+ * 不得二写):值形层逐项 `resolve(decl.root, p)` ⇒ 绝对根集(去重保序);存在性判归消费面(本层不判)。
126
+ * 缺省 `[]` ⇒ `[]`(零行为)。
127
+ * @param {string} [cwd] 会话锚(项目根经 manifest.mjs 解析)
128
+ * @returns {string[]} 绝对路径(去重保序)
129
+ */
130
+ export function declaredPublicRoots(cwd) {
131
+ const decl = loadProjectDeclaration(cwd)
132
+ const list = decl?.index?.publicRepos
133
+ if (!Array.isArray(list) || list.length === 0) return []
134
+ const base = typeof decl.root === "string" && decl.root !== "" ? decl.root : resolve(cwd ?? ".")
135
+ const out = []
136
+ for (const p of list) {
137
+ const abs = resolve(base, p)
138
+ if (!out.includes(abs)) out.push(abs)
139
+ }
140
+ return out
141
+ }
142
+
143
+ const _cache = new Map()
144
+
145
+ /** Drop the per-data-file-path cache (test seam — declarations are read once per path). */
146
+ export function clearDeclarationCache() {
147
+ _cache.clear()
148
+ }
149
+
150
+ /** Retired-carrier warning (existence check only — zero content parsing, zero fallback). */
151
+ function warnRetiredCarrier(root) {
152
+ console.warn(`[declaration] ${RETIRED_REL_PATH} is retired and no longer read — move codePaths / index.*Extensions / advisor.{docMap,standardsDoc} into PROJECT-MANIFEST.json`)
153
+ logEvent("declaration:retired-file", { cwd: root })
154
+ }
155
+
156
+ /**
157
+ * Load (and cache) the normalized project declaration for a project.
158
+ * The data file is located through `manifestFilePath(cwd)` (ONE root resolution — a cwd
159
+ * deeper than the project root still reads the project's manifest) and read through
160
+ * `readManifest` (ONE reader — this module carries no second parse of the manifest).
161
+ * Missing manifest → defaults, silently (the returned `root` is still the project root —
162
+ * `dirname(manifestFilePath(cwd))`, the face's anchor). Unusable manifest (invalid shape /
163
+ * read error) → defaults + `console.warn` + a log event (visible degradation, never a throw).
164
+ * A retired `.thincoder/conventions.json` at the manifest's project root → one warning
165
+ * per cache miss (content zero-parsed, zero effect on the readings — KD-M1-34).
166
+ * @param {string} cwd — project dir / anchor (root resolved by manifest.mjs)
167
+ * @returns {Readonly<{declared: boolean, codePaths: readonly string[],
168
+ * index: {codeExtensions: string[], docExtensions: string[], excludePaths: string[], publicRepos: string[]},
169
+ * advisor: {docMap: string, standardsDoc: string}, root: string|null}>}
170
+ */
171
+ export function loadProjectDeclaration(cwd) {
172
+ const file = manifestFilePath(cwd) // data-file path (single source — manifest.mjs KD-M1-18)
173
+ const hit = _cache.get(file)
174
+ if (hit) return hit
175
+ const root = dirname(file) // project root the manifest belongs to (cwd may be deeper) — the face's anchor
176
+ if (existsSync(join(root, RETIRED_REL_PATH))) warnRetiredCarrier(root)
177
+ let decl = buildDeclaration(null, root) // missing / unusable manifest → the defaults, root still known
178
+ try {
179
+ const r = readManifest(cwd)
180
+ if (r.ok) {
181
+ decl = buildDeclaration(r.manifest, root)
182
+ } else if (r.reason !== "missing") {
183
+ // Usable-file expectation broken (invalid JSON / wrong shape) → defaults, visible.
184
+ console.warn(`[declaration] ${file} is not a usable declaration (${(r.errors ?? []).join("; ") || r.reason}) — falling back to defaults`)
185
+ logEvent("declaration:error", { path: file, err: `invalid: ${(r.errors ?? []).join("; ")}`.slice(0, 200) })
186
+ }
187
+ } catch (e) {
188
+ // Read error (EACCES …) — same visible degradation, never a throw.
189
+ console.warn(`[declaration] ${file} not readable (${e?.message ?? e}) — falling back to defaults`)
190
+ logEvent("declaration:error", { path: file, err: String(e?.message ?? e).slice(0, 200) })
191
+ }
192
+ _cache.set(file, decl)
193
+ return decl
194
+ }
package/embedding.mjs CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { RETRYABLE_STATUS } from "./provider/index.mjs"
9
+ import { resolveEnvRefs } from "./env-ref.mjs"
9
10
  const MAX_RETRIES = 3
10
11
  const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
11
12
 
@@ -16,7 +17,8 @@ export function createEmbedder(config) {
16
17
  if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
17
18
  return {
18
19
  baseURL: config.baseURL.replace(/\/+$/, ""),
19
- apiKey: config.apiKey,
20
+ // #57(provider-config-family 批):值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——embedder 持解析值,请求侧零改。
21
+ apiKey: resolveEnvRefs(config.apiKey),
20
22
  model: config.model,
21
23
  }
22
24
  }
package/env-ref.mjs ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * env-ref.mjs — 值位 `${env:NAME}` 引用的消费侧解析(单源叶档;机制 = `docs/core/design/CONFIG.md` §6.3)。
3
+ *
4
+ * 语义(零 import 纯函数):
5
+ * - 整值与内嵌同解(`sk-${env:K}` 式);同一值可含多引用;非字符串值原样通过。
6
+ * - 变量未设 ∕ 空串 ⇒ 抛错(点名变量;无「未设即缺省」回退——不静默字面透传)。
7
+ * - 畸形引用(`${env:` 起头不构成合法引用)同抛错(点名畸形片段)。
8
+ * - 解析单遍:替换产物不再二次扫描;解析产物永不回显、永不回写(调用方纪律——写盘链按磁盘原文)。
9
+ */
10
+ const REF_PREFIX = "${env:"
11
+ const REF_RE = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g
12
+
13
+ /** 读一个变量(未设 ∕ 空串 ⇒ 抛错,点名变量;错误句零含解析产物 ∕ 前置字面)。 */
14
+ function readVar(name) {
15
+ const v = process.env[name]
16
+ if (v === undefined || v === "") {
17
+ throw new Error(`env reference \${env:${name}}: variable "${name}" ${v === undefined ? "is not set" : "is empty"} — set the variable or replace the reference with a literal value`)
18
+ }
19
+ return v
20
+ }
21
+
22
+ /** 畸形引用抛错(快照自 `${env:` 起截——引用串之前的字面不进错误句)。 */
23
+ function badRef(value, idx) {
24
+ return new Error(`malformed env reference ${JSON.stringify(value.slice(idx, idx + 24))} — expected \${env:NAME} with NAME matching [A-Za-z_][A-Za-z0-9_]*`)
25
+ }
26
+
27
+ /** 字符串值解析:无引用 ⇒ 原串返回;逐引用替换(每次 `${env:` 出现必须构成合法引用)。 */
28
+ export function resolveEnvRefs(value) {
29
+ if (typeof value !== "string" || !value.includes(REF_PREFIX)) return value
30
+ const out = []
31
+ let pos = 0
32
+ while (pos < value.length) {
33
+ const idx = value.indexOf(REF_PREFIX, pos)
34
+ if (idx === -1) { out.push(value.slice(pos)); break }
35
+ REF_RE.lastIndex = idx
36
+ const m = REF_RE.exec(value)
37
+ if (!m || m.index !== idx) throw badRef(value, idx)
38
+ out.push(value.slice(pos, idx), readVar(m[1]))
39
+ pos = idx + m[0].length
40
+ }
41
+ return out.join("")
42
+ }
43
+
44
+ /** 键值对象解析(headers ∕ env 族):无引用 ⇒ 原对象返回(零拷贝);值非字符串原样。 */
45
+ export function resolveEnvRefMap(map) {
46
+ if (map === null || typeof map !== "object" || Array.isArray(map)) return map
47
+ let changed = false
48
+ const out = {}
49
+ for (const [k, v] of Object.entries(map)) {
50
+ const rv = resolveEnvRefs(v)
51
+ if (rv !== v) changed = true
52
+ out[k] = rv
53
+ }
54
+ return changed ? out : map
55
+ }
56
+
57
+ /** providers[] 条目适配:`apiKey` ∥ `headers.*`(无引用 ⇒ 原对象返回——零拷贝零写回)。 */
58
+ export function resolveProviderSecrets(provider) {
59
+ if (provider === null || typeof provider !== "object") return provider
60
+ const apiKey = resolveEnvRefs(provider.apiKey)
61
+ const headers = resolveEnvRefMap(provider.headers)
62
+ if (apiKey === provider.apiKey && headers === provider.headers) return provider
63
+ const out = { ...provider }
64
+ if (apiKey !== provider.apiKey) out.apiKey = apiKey
65
+ if (headers !== provider.headers) out.headers = headers
66
+ return out
67
+ }
68
+
69
+ /** mcp.servers[] 条目适配:`token` ∥ `headers.*` ∥ `env.*`(无引用 ⇒ 原对象返回)。 */
70
+ export function resolveMcpServerSecrets(config) {
71
+ if (config === null || typeof config !== "object") return config
72
+ const token = resolveEnvRefs(config.token)
73
+ const headers = resolveEnvRefMap(config.headers)
74
+ const env = resolveEnvRefMap(config.env)
75
+ if (token === config.token && headers === config.headers && env === config.env) return config
76
+ const out = { ...config }
77
+ if (token !== config.token) out.token = token
78
+ if (headers !== config.headers) out.headers = headers
79
+ if (env !== config.env) out.env = env
80
+ return out
81
+ }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * explore-distill.mjs — End-of-run exploration distillation (AGENT-LOOP §13 +
3
- * CONTEXT-COMPACTION §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
2
+ * explore-distill.mjs — End-of-run exploration distillation (CONTEXT-COMPACTION.md §6.9 H1, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
4
3
  * context.mjs (524 > 500 hard limit). The main agent's machine line is flooded by inline
5
4
  * step-by-step exploration (read/grep/...). At run end we distill THIS run's exploration
6
5
  * tool-results into one semantic summary note that replaces them in the machine line,
@@ -99,7 +98,7 @@ async function distillExplorations(history, start, provider, signal, agent, dept
99
98
  messages: buildCompressMessages(history, cut, extras?.systemPrompt, EXPLORE_SUMMARY_PROMPT),
100
99
  tools: extras?.tools,
101
100
  signal,
102
- // §18.6 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
101
+ // TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
103
102
  // depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
104
103
  logCtx: {
105
104
  stage: "distill", child: agent?._logId ?? null, kind: "distill",
package/file-links.mjs ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * file-links.mjs — 工具结果文本的**验存**文件链接抽取(上提核件 —— 处理流批 R2 · 2026-09-28)。
3
+ * 上提源 = VSC `thincoder-vscode/src/extension/file-links.mjs`(token 抽取 ∕ 去重 ∕ 封顶)× 桌面同档
4
+ * (相对径需项目根——无根 ⇒ 相对 token 零判据,不落 `process.cwd()` 第二解析基);本档 = 单源
5
+ * (消费面 = 桌面 `thincoder-desktop/src/main/file-links.mjs` · VSC `thincoder-vscode/src/extension/file-links.mjs`——已随
6
+ * parity-b4 迁移轮改指本档(2026-09-29,双写窗口收口))。
7
+ * 判据 = **盘上存在闸**(存在 ∧ 是文件):路径 token 只在真实存在时才成链接 —— URL ∕ 日志噪音 ∕
8
+ * 版本号恒不成链接(存在检查 = 最后一道闸)。**验存(`existsSync` ∕ `statSync`)转注入缝**:本档零
9
+ * `node:fs`——盘面探针由调用方注入(`probe = { existsSync, statSync }`);缺 ∕ 形违 ⇒ 抛(fail-loud——
10
+ * 存在闸未接线不得静默扮成「零链接」)。返回 `[{ raw, path, line }]`:`raw` = 含行后缀原文 · `path` =
11
+ * 盘上绝对路径 · `line` = 数 ∕ 缺 ⇒ `null`;零抛面 = fs 判据(探针逐条 try/catch:不可达 = 不成链接)。
12
+ */
13
+
14
+ import { isAbsolute, resolve } from "node:path"
15
+
16
+ /** 路径 token:可选 Windows 盘符 + 路径字符 + 点扩展名 + 可选 `:line` / `:line:col` 后缀;m[1] = 路径本体(供探针)、m[2] = 行号、m[0] = 渲染面包裹面 `raw`。 */
17
+ const PATH_TOKEN_RE = /((?:[A-Za-z]:)?[~./\\\w][\w./\\~-]*\.\w{1,10})(?::(\d+)(?::\d+)?)?/g
18
+
19
+ /** 逐结果封顶(值同源 = VSC 同档 `MAX_LINKS`)。 */
20
+ export const MAX_LINKS = 50
21
+
22
+ /**
23
+ * 工具结果文本 ⇒ 验存文件链接(去重 + 封顶)。`cwd` = 项目根(相对 token 的解析基);缺 ∕ 空 ⇒ 相对 token
24
+ * 一律不检(禁假链接);绝对 token 与 cwd 无关,照检。`probe` = 盘面探针注入缝(`{ existsSync(p), statSync(p) }`——
25
+ * `node:fs` 同名面;桌面端侧注入)。零文本(`""` ∕ 非串)先于探针校验早退(零判据面 —— 探针不检)。
26
+ */
27
+ export function extractFileLinks(cwd, text, probe) {
28
+ if (typeof text !== "string" || text === "") return []
29
+ if (typeof probe?.existsSync !== "function" || typeof probe?.statSync !== "function") {
30
+ throw new TypeError("extractFileLinks: probe seam missing — inject { existsSync, statSync } (the existence guard must not silently degrade)")
31
+ }
32
+ const base = typeof cwd === "string" && cwd !== "" ? cwd : null
33
+ const links = []
34
+ const seen = new Set()
35
+ for (const m of text.matchAll(PATH_TOKEN_RE)) {
36
+ const pathPart = m[1]
37
+ // 协议相对 URL 残体(`//example.com/a.png`)—— 恒非工作区路径。
38
+ if (pathPart.startsWith("//") || pathPart.startsWith("\\\\")) continue
39
+ const absolute = isAbsolute(pathPart)
40
+ if (!absolute && base === null) continue // 无项目根 ⇒ 相对 token 零判据(禁假链接)
41
+ const line = m[2] ? Number(m[2]) : null
42
+ const abs = absolute ? pathPart : resolve(base, pathPart)
43
+ const key = `${abs}:${line ?? ""}`
44
+ if (seen.has(key)) continue
45
+ try {
46
+ if (!probe.existsSync(abs) || !probe.statSync(abs).isFile()) continue
47
+ } catch { continue }
48
+ seen.add(key)
49
+ links.push({ raw: m[0], path: abs, line })
50
+ if (links.length >= MAX_LINKS) break
51
+ }
52
+ return links
53
+ }
package/file-refs.mjs ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * file-refs.mjs — @ 文件引用**注入 ∕ 剥离核件**(缺面族批补 #632 · 2026-09-29 上提)。
3
+ * 上提源 = VSC `thincoder-vscode/src/extension/file-refs.mjs` 自持档(逐字搬——regex ∕ 4000 截断 ∕
4
+ * `[File: …]` 围栏 ∕ 摘要块格式零语义改良);本档 = **单源**;两端各持薄壳(端侧 `node:fs` 探针注入):
5
+ * VSC `thincoder-vscode/src/extension/file-refs.mjs` ∥ 桌面 `thincoder-desktop/src/main/file-refs.mjs`。
6
+ * 消费面:VSC 注入 `panel-chat.mjs:187` ∕ 恢复剥离 `panel-session.mjs:185` ∕ 标题剥离
7
+ * `panel-session-write.mjs:139`;桌面注入缝 = `turn-driver.mjs`(起跑前单点,`turn-face.mjs`)∥
8
+ * 恢复剥离 = `session-slots.mjs` `pageHistory` ∥ 标题剥离 = 核 `generate-title.mjs:132`(读源处)。
9
+ * 文本形逐字冻结(`docs/vsc/design/WEBVIEW.md:154`「落线文本逐字不变」契约):上提不携任何行为面改动。
10
+ * 判据 = **盘面探针注入缝**(`probe = { readFileSync, existsSync, statSync }` —— `node:fs` 同名面;
11
+ * 缺 ∕ 形违 ⇒ 抛 —— fail-loud:盘面闸未接线不得静默扮成「零引用」,沿 `file-links.mjs:29-31` 先例)。
12
+ * `stripAtRefs` = 注入语法的逆变换:fail-closed 判据逐字依赖注入输出格式 —— 语法与其逆变换同住一处,
13
+ * 零第二实现(本档为唯一产者)。
14
+ */
15
+ import { join } from "node:path"
16
+
17
+ /** 探针三件(`node:fs` 同名面)——缺一即缝违。 */
18
+ const PROBE_KEYS = ["readFileSync", "existsSync", "statSync"]
19
+
20
+ /** 探针校验(fail-loud):三件皆须为函数 —— 缺 ∕ 形违 ⇒ 抛。 */
21
+ function probeOf(probe) {
22
+ if (PROBE_KEYS.some((key) => typeof probe?.[key] !== "function")) {
23
+ throw new TypeError("injectAtRefs: probe seam missing — inject { readFileSync, existsSync, statSync } (the disk probe must not silently degrade)")
24
+ }
25
+ return probe
26
+ }
27
+
28
+ /** Scan text for @path references and replace with inline file content.
29
+ * 零文本(非串 ∥ 空串)先于探针校验早退(零判据面——探针不检,同序沿核 `file-links.mjs:28`)。 */
30
+ export function injectAtRefs(text, cwd, probe) {
31
+ if (typeof text !== "string" || text === "") return text
32
+ const io = probeOf(probe)
33
+ const re = /@([^\s,;:()[\]{}"'`]+)/g
34
+ const refs = []
35
+ let m
36
+ while ((m = re.exec(text)) !== null) {
37
+ const raw = m[1]
38
+ // Skip things that aren't paths (emails, mentions like @user)
39
+ if (!/^[a-zA-Z0-9_./\\-]+$/.test(raw)) continue
40
+ if (raw.includes("@")) continue
41
+ // Resolve relative to cwd
42
+ const abs = join(cwd, raw)
43
+ if (!io.existsSync(abs)) continue
44
+ try {
45
+ const stat = io.statSync(abs)
46
+ if (!stat.isFile()) continue
47
+ let content = io.readFileSync(abs, "utf8")
48
+ if (content.length > 4000) content = content.slice(0, 4000) + "\n... (truncated)"
49
+ refs.push({ raw, abs, content })
50
+ } catch (e) {
51
+ console.error(`[file-refs] failed to read @${raw}:`, e.message)
52
+ }
53
+ }
54
+ if (refs.length === 0) return text
55
+
56
+ // Replace @refs in the text with inline content blocks
57
+ for (const ref of refs) {
58
+ const placeholder = `@${ref.raw}`
59
+ const injection = `[File: ${ref.raw}]\n\`\`\`\n${ref.content}\n\`\`\``
60
+ text = text.split(placeholder).join(injection)
61
+ }
62
+
63
+ // Append a summary of referenced files
64
+ const list = refs.map(r => ` - ${r.raw} (${r.content.length} chars)`).join("\n")
65
+ text += `\n\n[Referenced files:\n${list}\n]`
66
+
67
+ return text
68
+ }
69
+
70
+ /**
71
+ * 复原注入前的用户原文(F-W15 端侧显示边界剥离 · D-W17)。
72
+ *
73
+ * 契约:同一会话中用户消息的**恢复面**文本 ≡ 活面气泡文本(= 用户所打原文,`@路径` 保持简洁形)。
74
+ * 与产者同档(本档 = 注入语法的唯一产者——语法与其逆变换同住一处,零第二实现)。
75
+ *
76
+ * 剥离判据(fail-closed):仅当文本尾部为注入**摘要块**(`[Referenced files:` 头 +
77
+ * 逐行 ` - <raw> (<N> chars)` + 尾 `]`)时,按「`[File: <raw>]` 头 + 围栏 + 恰 N 字符正文 +
78
+ * 收尾围栏 ⇒ `@<raw>`」逐条还原并删摘要块;**任一条不吻合 ⇒ 整条原样返回**(用户手打的
79
+ * `[File: x]` / 形近文本零误伤)。落线 / 机读线(模型输入)零触碰——本函数只服务显示边界。
80
+ */
81
+ export function stripAtRefs(text) {
82
+ const s = typeof text === "string" ? text : ""
83
+ const tail = /(?:^|\n\n)\[Referenced files:\n([\s\S]*)\n\]$/.exec(s)
84
+ if (!tail) return s
85
+ const entries = []
86
+ for (const line of tail[1].split("\n")) {
87
+ const m = /^ {2}- (.*) \((\d+) chars\)$/.exec(line)
88
+ if (!m) return s // 摘要块形近(行不吻合)⇒ 原样返回
89
+ entries.push({ raw: m[1], len: Number(m[2]) })
90
+ }
91
+ if (entries.length === 0) return s
92
+ let head = s.slice(0, tail.index) // 删摘要块(含其前空行)
93
+ for (const { raw, len } of entries) {
94
+ const open = `[File: ${raw}]\n\`\`\`\n`
95
+ const at = head.indexOf(open)
96
+ if (at < 0) return s // 头部缺 / 不对应 ⇒ 原样返回
97
+ const bodyStart = at + open.length
98
+ const bodyEnd = bodyStart + len
99
+ if (head.slice(bodyEnd, bodyEnd + 4) !== "\n\`\`\`") return s // 字符数不符 ⇒ 原样返回
100
+ head = head.slice(0, at) + `@${raw}` + head.slice(bodyEnd + 4)
101
+ }
102
+ return head
103
+ }
@@ -9,9 +9,17 @@
9
9
  */
10
10
 
11
11
  import { proxyFetch } from "./proxy.mjs"
12
+ import { resolveProviderSecrets } from "./env-ref.mjs"
12
13
  // D7(2026-09-21 块标题行对齐批——标题链单源):首条真实 user 消息谓词收核单源
13
14
  // (`isRealUserMsg`——纯函数零依赖,无环)。
14
15
  import { isRealUserMsg } from "./history-window.mjs"
16
+ // #632(缺面族批补 · @ 文件引用对齐 · 2026-09-29):标题源剥离单源——注入形不得进标题请求(核件
17
+ // `file-refs.mjs`;CLI 无注入 ⇒ 零动作;VSC 同位转口 = `panel-session-write.mjs:139`)。
18
+ import { stripAtRefs } from "./file-refs.mjs"
19
+ // #363③(off 形族单源——MODEL-SPECS.md §16.2 / §16.4):禁思考形与「是否可禁」一律引核
20
+ // (`thinkOffShape` / `thinkOffPath`——本档零副本);spec 查表同单源(`specForModel`)。
21
+ import { specForModel } from "./model-specs.mjs"
22
+ import { thinkOffShape, thinkOffPath } from "./think-off.mjs"
15
23
 
16
24
  // Test seam (_-prefix, mirrors run.mjs seams): lets the proxy-branch regression
17
25
  // test swap the proxy fetch. The branch it exercises used to carry a dynamic
@@ -31,7 +39,16 @@ export async function generateTitle(userContent, provider) {
31
39
  if (typeof userText !== "string" || userText.length < 10) return null
32
40
  if (!provider?.apiKey || !provider?.baseURL || !provider?.model) return null
33
41
 
42
+ // #363③:禁思考形(三格式支共用)——off 取形与可宣称性引核单源:`thinkOffPath` 假
43
+ // (thinkAlwaysOn 族)或 `thinkOffShape` 为 `null`(effort 族——`null` 形是核载荷门的
44
+ // 约定,本档无门)⇒ 不发禁形(假标记:被服务端拒 / 静默失效)。
45
+ const spec = specForModel(provider.model)
46
+ const offShape = thinkOffPath(spec) ? thinkOffShape(spec) : null
47
+
34
48
  try {
49
+ // #57(provider-config-family 批):凭据值位 `${env:VAR}` 消费侧解析(CONFIG.md §6.3)——解析抛错
50
+ // 落本函数既有非致命兜底(下方 catch ⇒ 标题回落 null——不破坏回合,与「无 key 不生成标题」同形)。
51
+ provider = resolveProviderSecrets(provider)
35
52
  const system = "Generate a concise title (max 40 chars, no quotes) for this conversation. Reply ONLY with the title."
36
53
  const text = userText.slice(0, 200)
37
54
  let url, body, extract
@@ -43,7 +60,7 @@ export async function generateTitle(userContent, provider) {
43
60
  messages: [{ role: "user", content: text }],
44
61
  max_tokens: MAX_TITLE_TOKENS,
45
62
  stream: false,
46
- thinking: { type: "disabled" }, // IK9UZ8:不让 reasoning 吃掉输出预算
63
+ ...(offShape !== null ? { thinking: offShape } : {}), // IK9UZ8:不让 reasoning 吃掉输出预算
47
64
  })
48
65
  extract = (data) => data.content?.map((b) => b.text || "").join("")
49
66
  } else if (provider.format === "google") {
@@ -51,7 +68,11 @@ export async function generateTitle(userContent, provider) {
51
68
  body = JSON.stringify({
52
69
  systemInstruction: { parts: [{ text: system }] },
53
70
  contents: [{ role: "user", parts: [{ text }] }],
54
- generationConfig: { maxOutputTokens: MAX_TITLE_TOKENS, thinkingConfig: { thinkingLevel: "none" } }, // IK9UZ8
71
+ generationConfig: {
72
+ maxOutputTokens: MAX_TITLE_TOKENS,
73
+ // google 格式自身的关思考形(wire 原生——非族别取形副本);无有效 off 路径 ⇒ 零发
74
+ ...(offShape !== null ? { thinkingConfig: { thinkingLevel: "none" } } : {}),
75
+ }, // IK9UZ8
55
76
  })
56
77
  extract = (data) => data.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("")
57
78
  } else {
@@ -65,7 +86,9 @@ export async function generateTitle(userContent, provider) {
65
86
  // leave content empty (IK9UZ8). Providers that don't accept the field ignore it
66
87
  // (OpenAI-compatible convention). A 40-char title wants ~60–80 tokens, so 100 is
67
88
  // ~2.5x headroom (design decision — docs/core/design/SESSION.md §6.7).
68
- thinking: { type: "disabled" },
89
+ // #363③:形自核单源(`thinkOffShape`——type 族 = `{type:"disabled"}`;无有效 off 路径
90
+ // 族不发——见上方 offShape 取形)。
91
+ ...(offShape !== null ? { thinking: offShape } : {}),
69
92
  max_tokens: MAX_TITLE_TOKENS,
70
93
  stream: false,
71
94
  }
@@ -113,7 +136,8 @@ export async function ensureSessionTitle(agent) {
113
136
  let firstUser = agent._recordStore?.firstUserMessage?.() ?? null
114
137
  if (!firstUser) firstUser = (agent._fullHistory ?? agent.history).find(isRealUserMsg)
115
138
  if (firstUser) {
116
- const title = await generateTitle(firstUser.content, agent.provider)
139
+ // #632:读源处剥离(注入形不得入标题请求——与恢复面同读一函数,零第二实现)
140
+ const title = await generateTitle(stripAtRefs(firstUser.content), agent.provider)
117
141
  if (title) agent.title = title
118
142
  }
119
143
  } catch {
@@ -13,6 +13,10 @@ import { existsSync, readFileSync, statSync } from "node:fs"
13
13
  import { cp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"
14
14
  import { dirname, join } from "node:path"
15
15
  import { configDir } from "../config.mjs"
16
+ // normalizeCwd 单源(2026-09-25 批 ledger-key-normalize · 台账 #286):盘符大写契约归一住
17
+ // `session-slots.mjs`(session / checkpoint / traces / peers 同契约消费面);本档原私有副本删除,
18
+ // 跨端 cwdHash12(CHECKPOINT.md F5/T7)赖同一实现,不再各自持字面。
19
+ import { normalizeCwd } from "../session-slots.mjs"
16
20
 
17
21
  const CWD_HASH_LEN = 12
18
22
 
@@ -33,14 +37,6 @@ function git(cwd, args, { allowFail = false } = {}) {
33
37
  }
34
38
  }
35
39
 
36
- /** Normalize cwd for hashing: uppercase the Windows drive letter so the VS Code
37
- * extension's uri.fsPath (lowercased) produces the SAME cwdHash12 as the CLI's
38
- * process.cwd() — cross-end snapshot sharing (CHECKPOINT.md F5/T7) depends on this
39
- * contract. Same normalization as session storage (session-slots.mjs normalizeCwd). */
40
- function normalizeCwd(cwd) {
41
- return cwd.replace(/^([a-z]):/, (_, d) => d.toUpperCase() + ":")
42
- }
43
-
44
40
  function checkpointRoot(cwd) {
45
41
  const hash = createHash("sha1").update(normalizeCwd(cwd)).digest("hex").slice(0, CWD_HASH_LEN)
46
42
  return join(configDir, "checkpoints", hash)
@@ -9,6 +9,11 @@
9
9
  *
10
10
  * 承接:会话数据面的「人读线」窗口切分(分段恢复 / 逐页回看)——与磁盘会话档
11
11
  * 同源(`{hash}.json.N`),本档只读数组、不触盘。
12
+ *
13
+ * 留档记录直通(opt-in · 消化面留档批 · #719):`digest` ∕ `subagent` 两型记录
14
+ * 原样入窗(携 `idx`;字段零改名);默认关 ⇒ 输出与改前逐字等价(CLI ∕ VSC 读面
15
+ * 零改零破)——`kindOf` 认 `kind` 键;形 / 在场 / 判据单源 =
16
+ * `docs/desktop/design/RENDERER.md` §1.1「留档记录」条。
12
17
  */
13
18
 
14
19
  /** Real user message predicate (shared by the slot metadata digest and the window).
@@ -32,7 +37,7 @@ function tsOf(m) {
32
37
  }
33
38
 
34
39
  function kindOf(m) {
35
- return m?.type ?? m?.role
40
+ return m?.type ?? m?.role ?? m?.kind
36
41
  }
37
42
 
38
43
  function toolCallsOf(m) {
@@ -103,10 +108,16 @@ function pairToolEntries(history) {
103
108
  * takes the LAST page (first paint); otherwise the page [s, e) ending just before
104
109
  * `before`. Half-open [s, e) keeps loadOlder pages from re-rendering the boundary
105
110
  * message. `idx` values are GLOBAL history indexes — pagination never renumbers.
111
+ *
112
+ * `opts.records === true`(opt-in —— 留档批 · #719)⇒ `digest` ∕ `subagent` 两型
113
+ * 记录原样入窗(`{ ...record, idx }`,字段零改名)并占条目位(游标按条目推进);
114
+ * 记录不参与 turn ∕ 可见性谓词(`visible` 面语义不动)。**默认关** ⇒ 输出与改前
115
+ * 逐字等价(负控腿);缺 `opts` ∕ 非 `true` ⇒ 同默认径。
106
116
  */
107
- export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE) {
117
+ export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE, opts = {}) {
108
118
  const total = Array.isArray(history) ? history.length : 0
109
119
  if (total === 0) return { messages: [], hasOlder: false }
120
+ const records = opts?.records === true
110
121
  const { frameResults, ownerOf } = pairToolEntries(history)
111
122
  const end = before == null ? total : Math.max(0, Math.min(before, total))
112
123
  const start = Math.max(0, end - pageSize)
@@ -161,6 +172,10 @@ export function historyWindow(history, before, pageSize = HISTORY_PAGE_SIZE) {
161
172
  turnStart: prevVisibleKind === null || prevVisibleKind === "user",
162
173
  tools,
163
174
  })
175
+ } else if (records && (kind === "digest" || kind === "subagent")) {
176
+ // 留档记录直通(opt-in):人读线条目原样入窗 + 携位次(字段零改名 —— 形单源 =
177
+ // `docs/desktop/design/RENDERER.md` §1.1「留档记录」条)
178
+ messages.push({ ...m, idx: i })
164
179
  } else if (kind === "tool") {
165
180
  // A consumed tool entry produces no message of its own (rendered with its frame) —
166
181
  // only a true orphan falls back to the top level (rules 3/4)
package/i18n.mjs CHANGED
@@ -46,6 +46,8 @@ export const CORE_MESSAGES = Object.freeze({
46
46
  "status.rateLimited": { en: "Rate-limited 429, retry in ${s}s", zh: "限流 429,${s}s 后重试" },
47
47
  "status.overloaded": { en: "Server overloaded, retrying in ${s}s", zh: "服务过载,${s}s 后重试" },
48
48
  "status.quota": { en: "quota exhausted: ${msg}", zh: "配额耗尽:${msg}" },
49
+ // ── 停滞轻显形(在飞回合静默读数——TUI.md §7.7 · stall-indicator 批 2026-09-29)──
50
+ "status.quiet": { en: "Quiet ${s}s", zh: "已静默 ${s}s" },
49
51
  // ── 异步池状态词 ──
50
52
  "sub.running": { en: "running", zh: "运行中" },
51
53
  "sub.queued": { en: "queued", zh: "排队中" },