@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
package/context.mjs CHANGED
@@ -13,7 +13,9 @@
13
13
 
14
14
  import { chat } from "./provider/index.mjs"
15
15
  import { buildCompressMessages } from "./compress-form.mjs"
16
- import { contextUsage, estimateTokens, collectStaleToolOutputs, keepTailSize, splitHistory, tailBudgetTokens } from "./token-window.mjs"
16
+ import { contextUsage, estimateTokens, keepTailSize, splitHistory, tailBudgetTokens } from "./token-window.mjs"
17
+ import { COMPACTION_PLACEHOLDER, withPlaceholderPrefix } from "./context-echo.mjs"
18
+ import { shrinkOversized } from "./context-degrade.mjs"
17
19
 
18
20
  export const SUMMARIZE_PROMPT = `The conversation above is our work log so far — summarize it into a compact summary for use as context in the ongoing conversation.
19
21
  Requirements:
@@ -34,9 +36,6 @@ const COMPACTION_PREFIX =
34
36
  "Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
35
37
  "but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
36
38
 
37
- /** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
38
- const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
39
-
40
39
  /** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
41
40
  export const COMPRESS_FAILURE_LIMIT = 3
42
41
 
@@ -71,115 +70,6 @@ function anchorText(agent) {
71
70
  return lines.length > 0 ? lines.join("\n") : null
72
71
  }
73
72
 
74
- /**
75
- * pushReal — the single entry point for REAL conversation messages.
76
- * A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
77
- * agent.history — the machine context (compaction shrinks this)
78
- * agent._fullHistory — the human-readable record (persistence source)
79
- * Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
80
- * are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
81
- * The two lines are written independently at the source — no after-the-fact delta sync.
82
- * Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
83
- * point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
84
- * is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
85
- * ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
86
- *
87
- * TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
88
- * ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
89
- * ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
90
- * 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
91
- * 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
92
- */
93
- export function pushReal(agent, msg) {
94
- if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
95
- if (msg && msg.ts === undefined) msg.ts = Date.now()
96
- agent._fullHistory.push(msg)
97
- try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
98
- const win = agent._historyWindow
99
- if (win > 0 && agent._fullHistory.length > win) {
100
- agent._fullHistory.splice(0, agent._fullHistory.length - win)
101
- }
102
- agent.history.push(msg)
103
- }
104
-
105
- /**
106
- * Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
107
- * string → text + blank line + original; multimodal array → text part prepended;
108
- * empty string / null / undefined / other → text alone.
109
- */
110
- function prefixContent(content, text) {
111
- if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
112
- if (Array.isArray(content)) return [{ type: "text", text }, ...content]
113
- return text
114
- }
115
-
116
- /**
117
- * D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
118
- * Thin wrapper over prefixContent (behavioral semantics unchanged).
119
- */
120
- function withPlaceholderPrefix(content) {
121
- return prefixContent(content, COMPACTION_PLACEHOLDER)
122
- }
123
-
124
- /**
125
- * D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
126
- * is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
127
- * WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
128
- * mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
129
- * only at restore time, never prompts the user, never rewrites the session file. Merge direction
130
- * matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
131
- * tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
132
- * line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
133
- * shared with other lines, so the merged message is always a NEW object; clean input returns
134
- * the SAME array reference (zero copy).
135
- */
136
-
137
- /** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
138
- * directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
139
- * safety, F-3.) */
140
- export function isAssistantEchoPair(prev, next) {
141
- return prev?.role === "assistant"
142
- && next?.role === "assistant"
143
- && !prev.reasoning_content
144
- && !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
145
- }
146
-
147
- /** Text of a message content in any supported shape (string / parts array / null-ish). */
148
- function contentTextOf(content) {
149
- if (typeof content === "string") return content
150
- if (Array.isArray(content)) {
151
- return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
152
- }
153
- return ""
154
- }
155
-
156
- /** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
157
- function absorbEchoContent(prevContent, nextContent) {
158
- const text = contentTextOf(prevContent)
159
- if (text.length === 0) return nextContent
160
- return prefixContent(nextContent, text)
161
- }
162
-
163
- export function mergeAdjacentAssistantEchoes(history) {
164
- if (!Array.isArray(history)) return history
165
- let src = history
166
- for (;;) {
167
- let changed = false
168
- const next = []
169
- for (let i = 0; i < src.length; i++) {
170
- if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
171
- next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
172
- i += 1
173
- changed = true
174
- } else {
175
- next.push(src[i])
176
- }
177
- }
178
- if (!changed) return src === history ? history : src
179
- src = next
180
- }
181
- }
182
-
183
73
  /** Replace middle with a note, then re-inject task/plan state (shared by LLM summary and truncation fallback) */
184
74
  function applyCompression(agent, headEnd, tailStart, note) {
185
75
  // _fullHistory already holds every real message (written at the source via pushReal),
@@ -314,7 +204,7 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
314
204
  messages,
315
205
  tools: extras?.tools,
316
206
  signal,
317
- // §18.6 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
207
+ // TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
318
208
  // depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
319
209
  logCtx: {
320
210
  stage: "compress", child: agent._logId, kind: "compress",
@@ -342,37 +232,6 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
342
232
  return true
343
233
  }
344
234
 
345
- /** prune stub 逐字(§6.16.3):只给「已清理 + 原长度 + 重跑路径」——prune 是**删**不是摘要,不声称可复原。 */
346
- const pruneStub = (chars) => `[pruned: stale tool output dropped (${chars} chars) — re-run the tool if you need it again.]`
347
-
348
- /**
349
- * 陈旧工具输出清理(F-CC3 · §6.16.3):合格集 = `token-window.mjs` 单源(保护尾外 ∧ `role:"tool"` ∧ ≥ 门槛);
350
- * 命中项**原位换「内容」**(`history[i] = { ...m, content: stub }`——数组引用 / 长度 / 索引 / `tool_call_id` 全不变
351
- * ⇒ 配对**结构上不可能被拆**)。记录面零改(copy-on-write:消息对象与人读线共享);基线失效同 `shrinkOversized` 先例。
352
- * @returns {{pruned:number, freed:number, candidates:number, tailKept:number, belowMin:number}}
353
- */
354
- export function pruneStaleToolOutputs(agent) {
355
- const history = agent.history
356
- const stale = collectStaleToolOutputs(history, agent.provider)
357
- const counts = {
358
- pruned: stale.indexes.length,
359
- freed: stale.tokens,
360
- candidates: stale.candidates,
361
- tailKept: stale.tailKept,
362
- belowMin: stale.belowMin,
363
- }
364
- if (counts.pruned === 0) return counts
365
- for (const i of stale.indexes) {
366
- const m = history[i]
367
- // 多模态 tool 结果(content 数组)整体替换为 stub ⇒ 图像 part 丢弃(不可再取——prune 是删;回执只给重跑路径)
368
- const chars = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content ?? "").length
369
- history[i] = { ...m, content: pruneStub(chars) }
370
- }
371
- agent._lastPromptTokens = null
372
- agent._usageAtLen = null
373
- return counts
374
- }
375
-
376
235
  /**
377
236
  * Deterministic truncation fallback: called when the summary LLM fails repeatedly, no network call.
378
237
  * Drops the middle so the task can continue. Returns whether truncation happened.
@@ -389,50 +248,12 @@ export function compressFallback(agent) {
389
248
  return true
390
249
  }
391
250
 
392
- /** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
393
- const OVERSIZE_CONTENT_LIMIT = 8_000
394
-
395
- /**
396
- * Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
397
- * Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
398
- * does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
399
- * risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
400
- * committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
401
- * Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
402
- */
403
- function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
404
- let shrunk = false
405
- // Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
406
- // message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
407
- // line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
408
- // the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
409
- // already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
410
- const next = agent.history.map((m) => {
411
- if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
412
- if (m.content.length <= limit) return m
413
- // Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
414
- const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
415
- const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
416
- shrunk = true
417
- return {
418
- ...m,
419
- content:
420
- m.content.slice(0, keepHead) +
421
- `\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
422
- m.content.slice(-keepTail),
423
- }
424
- })
425
- if (shrunk) {
426
- agent.history = next
427
- // Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
428
- agent._lastPromptTokens = null
429
- agent._usageAtLen = null
430
- }
431
- return shrunk
432
- }
433
-
434
251
  // ─── 迁出面(import 面保持:TUI / verify-compress / VSC 对拍经本档取——先例 = 下方 explore-distill 再导出)──
435
252
  export { estimateTokens }
253
+ // ─── 拆分转口(2026-10-01 拆分批 · #755 ∥ #786):写缝 ∥ 回声 ∥ 降级三面外提——消费面 / 批内件 import 面零改 ───
254
+ export { pushReal, pushRecord } from "./context-push.mjs"
255
+ export { isAssistantEchoPair, mergeAdjacentAssistantEchoes } from "./context-echo.mjs"
256
+ export { pruneStaleToolOutputs } from "./context-degrade.mjs"
436
257
  // ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
437
258
  // 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
438
259
  // explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
package/conventions.mjs CHANGED
@@ -1,34 +1,48 @@
1
1
  /**
2
- * conventions.mjs — the single authority for code / doc / temp path classification,
3
- * plus the project convention declaration surface (`.thincoder/conventions.json`).
2
+ * conventions.mjs — the single authority for code / doc / temp / aux path classification,
3
+ * plus the project declaration surface (the three declaration families of the project's
4
+ * `PROJECT-MANIFEST.json`: `codePaths` / `index.*` / `advisor.*`).
4
5
  *
5
- * Why one module: engineering-mode gates and guards (design gate, review-doc gate,
6
+ * Why one module: the write-domain gates and guards (design gate, review-doc gate,
6
7
  * mutation accounting, verify fast path) each carried their own copy of the
7
8
  * "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
8
9
  * prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
9
10
  * repository's layout: a project whose code lives outside `src/` slipped through
10
- * the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one
11
- * declaration file = one truth.
11
+ * the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one declared
12
+ * carrier = one truth. It is a **shared** classification surface: the gates are consumers,
13
+ * not owners (consumer faces: advisor / agent-tools / both ends).
12
14
  *
13
- * Defaults are DATA (`DEFAULT_CODE_PATHS`) — overridable per project through the
14
- * declaration file (§4.1 schema). Missing file → pure defaults (no noise);
15
- * corrupt/unreadable file → defaults + console.warn + a log event (never crash,
16
- * never swallow — PORTABILITY FR10).
15
+ * Declaration (2026-09-27 · `.thincoder/conventions.json` retired — single carrier):
16
+ * the three families live in the project manifest and are projected through `manifest.mjs`
17
+ * → `declaration.mjs` (declaration loading split out of this file 2026-10-01 — this file keeps the
18
+ * classification adjudication; ONE reader / ONE root resolution — `readManifest` + `manifestFilePath`;
19
+ * name / shape / defaults are schema-owned by `docs/core/design/MANIFEST.md` §2.2).
20
+ * Missing manifest → pure defaults (silent); unusable manifest (invalid / read error) →
21
+ * defaults + console.warn + a log event (never crash, never swallow — PORTABILITY FR10/D16).
22
+ * A retired `.thincoder/conventions.json` is never read: while present it yields ONE
23
+ * visible warning per load (existence check only — zero content parsing — KD-M1-34).
17
24
  *
18
- * Classification vocabulary (PORTABILITY design §3.1):
19
- * code — inside a declared code segment (default: the path segment `src`), or
20
- * not a documentation extension; doc — documentation extension outside
21
- * any code segment; temp — tmp-* name or .tmp/.temp extension.
25
+ * Classification vocabulary (PORTABILITY design §3.2):
26
+ * code — inside a declared code segment (default: the path segment `src`), or the
27
+ * fallback; doc — documentation extension outside any code segment and not
28
+ * scratch; temp — tmp-* name or .tmp/.temp extension; aux — a default
29
+ * auxiliary-path segment sequence (test / tests / scripts / .thincoder/tmp —
30
+ * F9) that matched no code segment and neither doc nor temp.
31
+ * Precedence: code segment → temp → doc → aux → fallback code.
32
+ *
33
+ * Segment matching runs on the PROJECT-ROOT-RELATIVE FACE of a path (D17 · 2026-09-28): the
34
+ * machine layout ABOVE the project root never participates. An absolute path outside the
35
+ * root / an unknown root / a `.`-`..` jump segment ⇒ the face is indeterminate: code segments
36
+ * fall back to the whole path (the interception surface does not shrink), aux sequences do
37
+ * NOT match (the former fail-open edge — an ancestor segment exempted a whole subtree).
22
38
  */
23
- import { readFileSync } from "node:fs"
24
- import { join, resolve } from "node:path"
25
- import { logEvent } from "./log.mjs"
26
-
27
- /** Default code-path segments (data, not logic — a project may replace them). */
28
- export const DEFAULT_CODE_PATHS = ["src"]
39
+ import { DEFAULT_DECLARATION } from "./declaration.mjs"
29
40
 
30
- /** Project declaration file, relative to the project root. */
31
- export const CONVENTIONS_REL_PATH = ".thincoder/conventions.json"
41
+ /** Default auxiliary-path segment sequences (data, not logic — F9): the engineering-mode
42
+ * parent gate's default exemption surface — `test` / `tests` / `scripts` at any depth,
43
+ * plus the `.thincoder/tmp` scratch space (two-segment sequence). Not declarable — a
44
+ * project reclaims a sequence by listing it in `codePaths` (code segment wins). */
45
+ export const DEFAULT_AUX_PATHS = ["test", "tests", "scripts", ".thincoder/tmp"]
32
46
 
33
47
  /** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
34
48
  const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
@@ -46,18 +60,44 @@ function segmentsOf(p) {
46
60
  return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
47
61
  }
48
62
 
63
+ const lowerSeg = (s) => s.toLowerCase()
64
+ const isJumpSeg = (seg) => seg === "." || seg === ".."
65
+
66
+ /**
67
+ * The segment-matching face of a path — its position INSIDE the project (PORTABILITY design
68
+ * §3.2 · D17「段匹配面 = 项目根相对面」):
69
+ * · absolute form under the root (separators normalized + per-segment case-insensitive
70
+ * comparison) ⇒ the REMAINING segments (an EMPTY list when the path IS the root);
71
+ * · relative form ⇒ its own segments;
72
+ * · anything else — outside the root / root unknown / a `.` / `..` jump segment ⇒
73
+ * `null` = indeterminate.
74
+ */
75
+ function faceOf(p, root) {
76
+ const s = String(p ?? "").replace(/\\/g, "/")
77
+ const segs = segmentsOf(s)
78
+ if (!/^([A-Za-z]:|\/)/.test(s)) return segs.some(isJumpSeg) ? null : segs.map(lowerSeg)
79
+ if (typeof root !== "string" || root === "") return null
80
+ const rootSegs = segmentsOf(root).map(lowerSeg)
81
+ if (segs.length < rootSegs.length) return null
82
+ for (let i = 0; i < rootSegs.length; i++) {
83
+ if (lowerSeg(segs[i]) !== rootSegs[i]) return null
84
+ }
85
+ const rest = segs.slice(rootSegs.length)
86
+ if (rest.length === 0) return []
87
+ return rest.some(isJumpSeg) ? null : rest.map(lowerSeg)
88
+ }
89
+
49
90
  /**
50
- * True when the path contains a declared code segment sequence at any depth.
91
+ * True when `segments` contain any of the given segment sequences at any depth.
51
92
  * Segment matching (not a prefix anchor) is what closes the nested-layout hole:
52
- * `packages/foo/src/x.md` is product code, not a document. Comparison is
53
- * case-insensitive — on case-insensitive filesystems `Src/x.mjs` is the same
54
- * directory, and the gate must not be bypassable by casing.
93
+ * `packages/foo/src/x.md` matches the sequence ["src"] — product code, not a
94
+ * document. Comparison is case-insensitive — on case-insensitive filesystems
95
+ * `Src/x.mjs` is the same directory, and the gate must not be bypassable by casing.
55
96
  */
56
- function hasCodeSegment(p, conv) {
57
- const parts = segmentsOf(p).map((s) => s.toLowerCase())
58
- const wanted = conv?.codePaths ?? DEFAULT_CODE_PATHS
59
- for (const entry of wanted) {
60
- const want = segmentsOf(entry).map((s) => s.toLowerCase())
97
+ function hasSequenceIn(segments, sequences) {
98
+ const parts = segments.map(lowerSeg)
99
+ for (const entry of sequences) {
100
+ const want = segmentsOf(entry).map(lowerSeg)
61
101
  if (want.length === 0) continue
62
102
  for (let i = 0; i + want.length <= parts.length; i++) {
63
103
  if (want.every((seg, j) => parts[i + j] === seg)) return true
@@ -66,15 +106,40 @@ function hasCodeSegment(p, conv) {
66
106
  return false
67
107
  }
68
108
 
69
- /** "code" | "doc" | "temp" — the single classification decision.
109
+ /**
110
+ * Segment matching on the project-root-relative face (D17). An indeterminate face splits the
111
+ * two callers by design: code segments fall back to the WHOLE path (`fallbackAll` — the
112
+ * interception surface does not shrink), while aux sequences do NOT match (the former
113
+ * fail-open edge: an ancestor segment above the root used to exempt a whole subtree).
114
+ */
115
+ function hasSegmentSequence(p, sequences, root, fallbackAll) {
116
+ const face = faceOf(p, root)
117
+ if (face === null) return fallbackAll ? hasSequenceIn(segmentsOf(p), sequences) : false
118
+ return hasSequenceIn(face, sequences)
119
+ }
120
+
121
+ /** True when the path contains a declared code segment sequence (on the face — D17). */
122
+ function hasCodeSegment(p, conv) {
123
+ return hasSegmentSequence(p, conv?.codePaths ?? DEFAULT_DECLARATION.codePaths, conv?.root, true)
124
+ }
125
+
126
+ /** True when the path contains a default auxiliary-path segment sequence (F9 · face-bound). */
127
+ function hasAuxSegment(p, conv) {
128
+ return hasSegmentSequence(p, DEFAULT_AUX_PATHS, conv?.root, false)
129
+ }
130
+
131
+ /** "code" | "doc" | "temp" | "aux" — the single classification decision.
70
132
  * Precedence: code segment first (src/** stays product code even when the name
71
133
  * looks scratch — the pre-existing unconditional-src rule), then temp, then a
72
- * documentation extension, else code (anything not doc/temp is product code). */
134
+ * documentation extension, then an auxiliary-path segment sequence (test / tests
135
+ * / scripts / .thincoder/tmp — F9), else code (anything not doc/temp/aux is product
136
+ * code). Both segment checks run on the project-root-relative face (D17 — `faceOf`). */
73
137
  export function classifyPath(p, conv) {
74
138
  const s = String(p ?? "")
75
139
  if (hasCodeSegment(s, conv)) return "code"
76
140
  if (TEMP_FILE.test(s)) return "temp"
77
141
  if (DOC_FILE.test(s)) return "doc"
142
+ if (hasAuxSegment(s, conv)) return "aux"
78
143
  return "code"
79
144
  }
80
145
 
@@ -90,134 +155,18 @@ export function isDocPath(p, conv) {
90
155
  return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
91
156
  }
92
157
 
93
- // ─────────────────────────────────────────────────────────────────────────────
94
- // Declaration loading (cached per project root — `clearConventionsCache()` is
95
- // the test seam; declaration files change rarely and only at session scope).
96
- // ─────────────────────────────────────────────────────────────────────────────
97
-
98
- function normalizeExtensions(v) {
99
- if (!Array.isArray(v)) return []
100
- const out = []
101
- for (const e of v) {
102
- if (typeof e !== "string") continue
103
- const t = e.trim().toLowerCase()
104
- if (!t) continue
105
- const ext = t.startsWith(".") ? t : `.${t}`
106
- if (!out.includes(ext)) out.push(ext)
107
- }
108
- return out
158
+ /** True when the path is an auxiliary path (`classifyPath === "aux"`) — the engineering-mode
159
+ * parent gate's default exemption surface (test / tests / scripts at any depth ·
160
+ * .thincoder/tmp/** — matched on the project-root-relative face only, D17). Not declarable:
161
+ * a project reclaims one by listing its sequence in `codePaths`, which then wins (code
162
+ * segment first). Production faces read aux through classifyPath / isCodePath; this
163
+ * predicate is the readable form for the test face. */
164
+ export function isAuxPath(p, conv) {
165
+ return classifyPath(p, conv) === "aux"
109
166
  }
110
167
 
111
- /** Declared code paths replace the default (replacement, not union — §4.1). */
112
- function normalizeCodePaths(v) {
113
- if (!Array.isArray(v)) return null
114
- const out = []
115
- for (const e of v) {
116
- if (typeof e !== "string") continue
117
- const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
118
- if (!s || out.includes(s)) continue
119
- out.push(s)
120
- }
121
- return out.length > 0 ? out : null
122
- }
123
-
124
- function normalizeString(v) {
125
- return typeof v === "string" && v.trim() ? v.trim() : ""
126
- }
127
-
128
- /** Per-key type check for recognized keys (present but wrong type). §3.2/§4.1: a
129
- * type error degrades WITH a warning — never silently (the fallback semantics stay
130
- * per-key; only the visibility is added here). */
131
- function typeErrorsOf(raw) {
132
- const isObj = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v)
133
- const strArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string")
134
- const errs = []
135
- if (raw.codePaths !== undefined && !strArray(raw.codePaths)) errs.push("codePaths must be an array of strings")
136
- const idx = raw.index
137
- if (idx !== undefined && !isObj(idx)) errs.push("index must be an object")
138
- else if (isObj(idx)) {
139
- for (const k of ["codeExtensions", "docExtensions"]) {
140
- if (idx[k] !== undefined && !strArray(idx[k])) errs.push(`index.${k} must be an array of strings`)
141
- }
142
- }
143
- const adv = raw.advisor
144
- if (adv !== undefined && !isObj(adv)) errs.push("advisor must be an object")
145
- else if (isObj(adv)) {
146
- for (const k of ["docMap", "standardsDoc"]) {
147
- if (adv[k] !== undefined && typeof adv[k] !== "string") errs.push(`advisor.${k} must be a string`)
148
- }
149
- }
150
- return errs
151
- }
152
-
153
- function buildConventions(raw) {
154
- const codePaths = normalizeCodePaths(raw?.codePaths)
155
- const codeExtensions = normalizeExtensions(raw?.index?.codeExtensions)
156
- const docExtensions = normalizeExtensions(raw?.index?.docExtensions)
157
- const docMap = normalizeString(raw?.advisor?.docMap)
158
- const standardsDoc = normalizeString(raw?.advisor?.standardsDoc)
159
- // `declared` = the declaration actually took effect (at least one recognized key
160
- // honored) — the design-gate hint reads it to decide whether to point at the
161
- // declaration file ("declare project conventions … to adjust").
162
- const declared = Boolean(codePaths || codeExtensions.length || docExtensions.length || docMap || standardsDoc)
163
- return Object.freeze({
164
- declared,
165
- codePaths: Object.freeze(codePaths ?? [...DEFAULT_CODE_PATHS]),
166
- index: Object.freeze({
167
- codeExtensions: Object.freeze(codeExtensions),
168
- docExtensions: Object.freeze(docExtensions),
169
- }),
170
- advisor: Object.freeze({ docMap, standardsDoc }),
171
- })
172
- }
173
-
174
- /** Full-default conventions (no declaration) — the fallback every consumer gets. */
175
- export const DEFAULT_CONVENTIONS = buildConventions(null)
176
-
177
- const _cache = new Map()
178
-
179
- /** Drop the per-root cache (test seam — declaration files are read once per root). */
180
- export function clearConventionsCache() {
181
- _cache.clear()
182
- }
183
-
184
- /**
185
- * Load (and cache) the normalized conventions for a project root.
186
- * @param {string} cwd — project root (declaration lives at .thincoder/conventions.json)
187
- * @returns {Readonly<{declared: boolean, codePaths: readonly string[],
188
- * index: {codeExtensions: string[], docExtensions: string[]},
189
- * advisor: {docMap: string, standardsDoc: string}}>}
190
- */
191
- export function loadConventions(cwd) {
192
- const root = resolve(cwd ?? process.cwd())
193
- const hit = _cache.get(root)
194
- if (hit) return hit
195
- let conv = DEFAULT_CONVENTIONS
196
- try {
197
- const text = readFileSync(join(root, CONVENTIONS_REL_PATH), "utf8")
198
- try {
199
- const raw = JSON.parse(text)
200
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("top level must be a JSON object")
201
- conv = buildConventions(raw)
202
- const typeErrs = typeErrorsOf(raw)
203
- if (typeErrs.length > 0) {
204
- // Wrong-typed keys fall back per-key — but the user must SEE that their
205
- // declaration did not take effect (never silently swallowed).
206
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} has invalid value types (${typeErrs.join("; ")}) — those keys fall back to defaults`)
207
- logEvent("conventions:error", { cwd: root, err: `type errors: ${typeErrs.join("; ").slice(0, 160)}` })
208
- }
209
- } catch (e) {
210
- // Corrupt file / wrong shape → defaults, visible: warn + event (never silent).
211
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} unreadable (${e?.message ?? e}) — falling back to defaults`)
212
- logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
213
- }
214
- } catch (e) {
215
- if (e?.code !== "ENOENT") {
216
- // File exists but cannot be read (EACCES etc.) — same visible degradation.
217
- console.warn(`[conventions] ${CONVENTIONS_REL_PATH} not readable (${e?.message ?? e}) — falling back to defaults`)
218
- logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
219
- }
220
- }
221
- _cache.set(root, conv)
222
- return conv
223
- }
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // 迁出面(2026-10-01 拆分批 · #755 ∥ #786):声明装载面外提 `declaration.mjs`——本档经
170
+ // 转口保名(消费面 / 批内件 import 面零改;`DEFAULT_DECLARATION` 另供本档分类裁判缺省取用)。
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ export { loadProjectDeclaration, DEFAULT_DECLARATION, isExcludedRelPath, declaredPublicRoots, clearDeclarationCache } from "./declaration.mjs"