thincoder 0.12.61 → 0.12.63

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 (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
package/src/context.mjs DELETED
@@ -1,381 +0,0 @@
1
- /**
2
- * context.mjs — Context management and compaction
3
- * When no measured token count is available, use estimation as fallback (ASCII/4 + non-ASCII/1, no tokenizer dependency).
4
- * When a measured value exists (response usage.prompt_tokens), trust it — estimation underestimates CJK by 3-4x and relying solely on it may never trigger compaction.
5
- * Compaction strategy: summarize everything before the tail into one LLM note, keep the latest N messages verbatim.
6
- * NOTE: no dedicated head is kept (KEEP_HEAD = 0) — in multi-task sessions the earliest messages are
7
- * typically a COMPLETED earlier task; preserving them verbatim anchored the model's attention on stale
8
- * work after compaction. The earliest messages now go into the summary (which distinguishes completed
9
- * vs in-progress work), so the post-compaction context anchors on the current task (recent tail) only.
10
- */
11
-
12
- import { chat } from "./provider/index.mjs"
13
- import { estimateText } from "./provider/rate.mjs"
14
- import { providerSpec } from "./config.mjs"
15
-
16
- const IMAGE_TOKEN_ESTIMATE = 2000 // rough estimate for image content tokens (CLI legacy 256 underestimated real image costs, delaying compaction)
17
-
18
- /** Rough token count for a list of messages (body + reasoning + tool_calls params) */
19
- export function estimateTokens(messages) {
20
- let tokens = 0
21
- for (const m of messages) {
22
- if (typeof m.content === "string") tokens += estimateText(m.content)
23
- else if (Array.isArray(m.content)) {
24
- for (const part of m.content) {
25
- if (part.type === "text") tokens += estimateText(part.text)
26
- else if (part.type === "image_url") tokens += IMAGE_TOKEN_ESTIMATE
27
- }
28
- }
29
- if (typeof m.reasoning_content === "string") tokens += estimateText(m.reasoning_content)
30
- for (const tc of m.tool_calls ?? []) {
31
- tokens += estimateText(tc.function?.name ?? "") + estimateText(tc.function?.arguments ?? "")
32
- }
33
- }
34
- return tokens
35
- }
36
-
37
- const KEEP_HEAD = 0 // No dedicated head: earliest messages may be a COMPLETED earlier task in multi-task
38
- // sessions — keeping them verbatim anchored attention on stale work. Everything before the tail is
39
- // summarized (the summary itself distinguishes completed vs in-progress work; see SUMMARIZE_PROMPT).
40
- // Tail count formula (D4): window-adaptive (~30 msgs per 100K — old fixed 10 too thin on 1M), capped
41
- // at 40% of history; §9 D-T1/D-T2 make the count only a CANDIDATE — a token budget (TAIL_BUDGET_FRACTION
42
- // × window − SUMMARY_TOKEN_ESTIMATE ≈1K, §8) tightens it over pair-safe boundaries when compaction runs,
43
- // never below TAIL_FLOOR_MESSAGES; ordinary sessions never reach it (D-T4: trigger 0.6 untouched).
44
- const TAIL_BUDGET_FRACTION = 0.15
45
- const SUMMARY_TOKEN_ESTIMATE = 1000 // §8: summary output target ~1K tokens — reserved from the 15%
46
- const TAIL_FLOOR_MESSAGES = 10 // §9 D-T2: the tail keeps ≥10 verbatim messages — floor beats budget
47
- function keepTailSize(provider, historyLen) {
48
- // provider is guaranteed at every call site (runAgent always builds one); providerSpec
49
- // degrades to DEFAULT_SPEC (128K) only if provider is somehow absent — acceptable
50
- // because the 40% history cap still bounds the tail. providers[].context override
51
- // (K units) is honored here (PROVIDER.md §15 T-C2: tail formula follows the window).
52
- const ctxWindow = providerSpec(provider).context
53
- return Math.min(Math.max(10, Math.floor((ctxWindow / 100_000) * 30)), Math.floor(historyLen * 0.4))
54
- }
55
- // §9 D-T1 tail token budget: window×15% − summary ~1K — the compressed history segment (summary + placeholder + tail) lands ≈ 15% (B 口径 §9.5).
56
- function tailBudgetTokens(provider) {
57
- return Math.max(0, Math.floor(providerSpec(provider).context * TAIL_BUDGET_FRACTION) - SUMMARY_TOKEN_ESTIMATE)
58
- }
59
-
60
- export const SUMMARIZE_PROMPT = `You are a conversation compressor. Summarize the following agent work log into a compact summary for use as context in the ongoing conversation.
61
- Requirements:
62
- - Write in first person, present tense — these are "my" handover notes, continuing my own train of thought
63
- - Most important: preserve design decisions and their reasons — architecture choices, API contracts, naming conventions, trade-off rationale. These are the anchors the subsequent code must not deviate from
64
- - Distinguish COMPLETED vs IN-PROGRESS work: completed tasks get a ONE-LINE recap each (what was done, key outcome); spend the detail budget on unresolved issues, next steps, and the CURRENT task
65
- - The user's most recent request defines the current task — anchor on it. Earlier requests are likely already completed and only need the one-line recap; do NOT preserve them at full fidelity
66
- - Explicitly list FILES CHANGED: every modified file path plus a one-line "why" — so post-compaction work can re-locate what was edited and where
67
- - Explicitly list UNRESOLVED ISSUES / TODOs: anything still open plus the next steps — so post-compaction recovery knows where to resume
68
- - Drop: pleasantries, repetition, fine-grained tool output details
69
- - Honestly mark uncertain items: anything not actually verified must say "unverified"; do not present guesses as facts
70
- - Use bullet-point output. Stay under ~1K tokens (≈1000 Chinese chars / 4000 ASCII chars) — a hard target. An oversized summary wastes window and dilutes the tail; the old unbounded-length guidance is deprecated. When over budget, trim in this order: completed recaps to one line; FILES CHANGED why-notes to bare paths; in-progress prose tightened. NEVER cut design anchors or UNRESOLVED ISSUES/TODOs — recovery depends on them.
71
-
72
- Work log:
73
- `
74
-
75
- /** Context prefix after compaction, informing the agent what happened */
76
- const COMPACTION_PREFIX =
77
- "[Context was automatically compacted. Below is a summary of earlier work. " +
78
- "Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
79
- "but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
80
-
81
- /** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
82
- export const COMPRESS_FAILURE_LIMIT = 3
83
-
84
- /** Task re-injection reminder prefix (after compaction, clear old versions from history first for a single source of truth) */
85
- const TASK_REINJECT_PREFIX = "[System reminder: your current task list after compaction:"
86
-
87
- /** Truncation fallback note (used when the summary LLM fails repeatedly; no LLM call) */
88
- const FALLBACK_NOTE =
89
- "[Context was truncated after repeated summarization failures. " +
90
- "The middle portion of earlier work was dropped WITHOUT a summary. " +
91
- "Re-verify any state you need with tools before relying on it.]\n\n"
92
-
93
- /**
94
- * Split history into head / middle (to be summarized) / tail; return null if no middle to compress.
95
- * head is normally empty (KEEP_HEAD = 0 — earliest messages go into the summary); the tool_calls-extension logic below is defensive for future KEEP_HEAD > 0.
96
- * The tail boundary must include any assistant whose tool results are in the tail — if the assistant is in the middle, the summary swallows it, leaving orphan tool results → protocol 400.
97
- * `budgetTokens` (optional, §9 D-T1): when the candidate's estimate exceeds it, the boundary moves
98
- * forward until the tail fits — never below the D-T2 floor (10 msgs, or the candidate itself when
99
- * the 40% cap made it < 10 — short history).
100
- */
101
- function splitHistory(history, keepTail, budgetTokens = null) {
102
- if (history.length <= KEEP_HEAD + keepTail + 1) return null
103
- let headEnd = KEEP_HEAD
104
- // head must not end with dangling tool_calls: when assistant declares tool_calls, all its tool results must stay in head.
105
- // Parallel calls: one assistant followed by multiple tool messages — accepting only one still causes 400, must collect all
106
- if (history[headEnd - 1]?.role === "assistant" && history[headEnd - 1].tool_calls?.length) {
107
- while (headEnd < history.length && history[headEnd].role === "tool") headEnd++
108
- }
109
- const candidate = repairedTailStart(history, headEnd, history.length - keepTail)
110
- if (candidate <= headEnd) return null
111
- let tailStart = candidate
112
- // §9 D-T1: tighten only above the floor — a candidate ≤ 10 IS the floor (short history under the 40% cap must not tighten further, review #5); the floor is D5-repaired too.
113
- if (budgetTokens > 0 && keepTail > TAIL_FLOOR_MESSAGES) {
114
- const floor = repairedTailStart(history, headEnd, history.length - TAIL_FLOOR_MESSAGES)
115
- if (floor > candidate) tailStart = tightenTailByBudget(history, candidate, floor, budgetTokens)
116
- }
117
- return { headEnd, tailStart }
118
- }
119
-
120
- /**
121
- * D5 tail-side pairing repair for a raw cut at history.length − tailCount: pull into the tail any
122
- * assistant whose tool results are in the tail (the summary swallowing the owner leaves orphan tool
123
- * results → protocol 400), then skip orphan tool messages at the new boundary. Single-assistant
124
- * assumption (nearest owner only — a tail spans at most one assistant→tools cycle); bounds-guarded.
125
- */
126
- function repairedTailStart(history, headEnd, tailStart) {
127
- const tailToolIds = new Set()
128
- for (let i = tailStart; i < history.length; i++) {
129
- if (history[i].role === "tool") tailToolIds.add(history[i].tool_call_id)
130
- }
131
- for (let i = tailStart - 1; i > headEnd; i--) {
132
- const m = history[i]
133
- if (m.role === "assistant" && m.tool_calls?.some((tc) => tailToolIds.has(tc.id))) {
134
- tailStart = i
135
- break
136
- }
137
- }
138
- while (tailStart < history.length && tailStart > headEnd && history[tailStart].role === "tool") {
139
- tailStart++
140
- }
141
- return tailStart
142
- }
143
-
144
- /**
145
- * §9 D-T1 budget tightening (pair-safe, review #2): walk the boundary FORWARD (fewer tail messages —
146
- * the rest joins the summary) while the tail's estimated tokens exceed the budget. Only pair-safe
147
- * positions may stop the walk: a boundary ON a tool message would orphan its owner assistant into the
148
- * middle (D5); pairing is contiguous in the machine line (§6 note) — every non-tool boundary is safe.
149
- * No fit before the floor → keep the floor, accept the overrun.
150
- */
151
- function tightenTailByBudget(history, start, floorStart, budgetTokens) {
152
- const suffixTokens = new Array(history.length + 1)
153
- suffixTokens[history.length] = 0
154
- for (let i = history.length - 1; i >= 0; i--) suffixTokens[i] = suffixTokens[i + 1] + estimateTokens([history[i]])
155
- if (suffixTokens[start] <= budgetTokens) return start // already fits — ordinary sessions stay untouched (D-T2)
156
- for (let p = start + 1; p <= floorStart; p++) { // first fit keeps the most recent verbatim context
157
- if (history[p].role !== "tool" && suffixTokens[p] <= budgetTokens) return p
158
- }
159
- return floorStart
160
- }
161
-
162
- /**
163
- * pushReal — the single entry point for REAL conversation messages.
164
- * A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
165
- * agent.history — the machine context (compaction shrinks this)
166
- * agent._fullHistory — the NEVER-COMPACTED human-readable record (persistence source)
167
- * Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
168
- * are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
169
- * The two lines are written independently at the source — no after-the-fact delta sync.
170
- * Message timestamps (SESSION.md §9 D-S1): stamped HERE once at push time (epoch ms) — a single
171
- * point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
172
- * is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
173
- * ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
174
- */
175
- export function pushReal(agent, msg) {
176
- if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
177
- if (msg && msg.ts === undefined) msg.ts = Date.now()
178
- agent._fullHistory.push(msg)
179
- agent.history.push(msg)
180
- }
181
-
182
- /** Replace middle with a note, then re-inject task/plan state (shared by LLM summary and truncation fallback) */
183
- function applyCompression(agent, headEnd, tailStart, note) {
184
- // _fullHistory already holds every real message (written at the source via pushReal),
185
- // so compaction only shrinks the machine line — nothing to preserve here.
186
- // head is normally empty (KEEP_HEAD = 0) — the summary note becomes the first message,
187
- // which is exactly the intent: post-compaction context anchors on the current task, not on
188
- // possibly-completed earlier requests.
189
- const head = agent.history.slice(0, headEnd)
190
- const tail = agent.history.slice(tailStart)
191
- // SESSION.md §9 D-S1: compaction-injected messages (note + "Understood") carry a ts —
192
- // Date.now() at the compaction moment. They are machine-only (never in _fullHistory),
193
- // but the machine-line timeline stays consistent for any audit use.
194
- const now = Date.now()
195
- agent.history = [
196
- ...head,
197
- { role: "user", content: note, ts: now },
198
- { role: "assistant", content: "Understood. I'll continue from these notes, re-verifying anything transient.", ts: now },
199
- ...tail,
200
- ]
201
- // Compaction REBUILDS the machine line (head + note + "Understood" + tail), so the pre-compaction
202
- // _runStartHistoryLen index is stale — a longer array shrank beneath it, and end-of-run exploration
203
- // distillation would then silently skip or slice from the wrong offset. Reset the boundary to the
204
- // verbatim tail start (head.length + 2: the note and the "Understood" placeholder sit between head
205
- // and tail). Exploration before the tail was already covered by the compaction summary, so only the
206
- // still-raw tail needs distilling. `head` is empty today (KEEP_HEAD = 0) — the formula stays
207
- // correct if KEEP_HEAD ever grows. (shrinkOversized only truncates message bodies in place and
208
- // leaves the array length unchanged, so this boundary stays valid there — no reset needed.)
209
- agent._runStartHistoryLen = head.length + 2
210
- // Measured token baseline is invalidated along with old history (prompt_tokens were for pre-compaction context), fall back to estimation until next response
211
- agent._lastPromptTokens = null
212
- agent._usageAtLen = null
213
-
214
- // After compaction, re-inject the task list (the agent needs to know what it was doing).
215
- // Single source of truth: first remove any stale re-injections from the tail, then inject the latest version —
216
- // no longer embedded in the summary body (would duplicate and grow stale)
217
- agent.history = agent.history.filter(
218
- (m) => !(m.role === "user" && typeof m.content === "string" && m.content.startsWith(TASK_REINJECT_PREFIX))
219
- )
220
- if (agent.tasks.length > 0) {
221
- const taskSummary = agent.tasks.map((t) => `- [${t.status}] ${t.title}`).join("\n")
222
- agent.history.push({
223
- role: "user",
224
- content: `${TASK_REINJECT_PREFIX}\n${taskSummary}\nContinue from where you left off.]`,
225
- })
226
- }
227
-
228
- // Plan mode compaction: re-inject plan mode guidance
229
- if (agent.planMode) {
230
- agent.history.push({
231
- role: "user",
232
- content: "[System reminder: plan mode is active. Explore the codebase read-only, design your solution, then call plan with action='exit' to present it for user approval.]",
233
- })
234
- }
235
- }
236
-
237
- /**
238
- * If history exceeds threshold, compact it. Returns whether compaction happened.
239
- * Only called at safe points in the loop (history ends with user or tool message — a complete exchange boundary).
240
- * Automatically re-injects task list state after compaction.
241
- * @param {object} agent
242
- * @param {number} threshold - compaction threshold in tokens
243
- * @param {object} callbacks - { onToken, onReasoning, onCompress, onCompressStart } — summary
244
- * generation is SILENT (never forwards onToken/onReasoning: the compaction process is an
245
- * internal mechanism, not a model reply); onCompressStart fires right before the summary call
246
- * (§7 D-C1, compression lifecycle visibility — panel start state)
247
- * @param {object} extras - { systemPrompt?, tools? } — estimated overhead for the pure-estimation
248
- * path (no measured baseline); the measured path already includes system+tools in prompt_tokens.
249
- */
250
- export async function compressIfNeeded(agent, threshold, callbacks, extras = {}, signal) {
251
- const history = agent.history
252
- // Prefer the real baseline: the last response's prompt_tokens is the measured value for the full context (system+tools+history).
253
- // Subsequent appended messages use estimation as increment; when no measured value exists (first turn / after restore / right after compaction), fall back to pure estimation
254
- const overhead =
255
- (extras.systemPrompt ? estimateText(extras.systemPrompt) : 0) +
256
- (extras.tools ? estimateText(JSON.stringify(extras.tools)) : 0)
257
- const tokens =
258
- agent._lastPromptTokens != null
259
- ? agent._lastPromptTokens + estimateTokens(history.slice(agent._usageAtLen ?? history.length))
260
- : estimateTokens(history) + overhead
261
- if (tokens <= threshold) return false
262
-
263
- const keepTail = keepTailSize(agent.provider, history.length)
264
- const split = splitHistory(history, keepTail, tailBudgetTokens(agent.provider))
265
- if (!split) {
266
- // History is too short (≤KEEP_HEAD+keepTail+1 messages) to find a middle section, but tokens exceed threshold — typically a single giant message
267
- // (large paste / huge injection). When summarization has no room, degrade to deterministic shrinking to ensure context always reduces
268
- return shrinkOversized(agent)
269
- }
270
-
271
- const middle = history.slice(split.headEnd, split.tailStart)
272
- const serialized = middle
273
- .map((m) => {
274
- const toolNote = m.tool_calls ? ` [called tools: ${m.tool_calls.map((t) => t.function?.name).join(", ")}]` : ""
275
- // user messages get a wider cap (8000): cutting off a long user-pasted requirement loses original intent; tool/assistant capped at 2000 is enough
276
- const cap = m.role === "user" ? 8000 : 2000
277
- // Multimodal messages (array content): extract the TEXT parts — the image itself can't be
278
- // summarized, but any accompanying text (e.g. "看这张图" + image) must not be silently lost
279
- let text = ""
280
- if (typeof m.content === "string") text = m.content
281
- else if (Array.isArray(m.content)) text = m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join(" ")
282
- return `[${m.role}]${toolNote} ${text.slice(0, cap)}`
283
- })
284
- .join("\n")
285
-
286
- // The summary is a plain-text task, no reasoning needed — passing thinking to the compaction provider wastes tokens.
287
- // Silent by design (D11): no onToken/onReasoning — the compaction process must not stream to the frontend.
288
- // signal propagates user cancellation (Ctrl+C) to the in-flight summary call.
289
- // Compression visibility (CONTEXT-COMPACTION.md §7 D-C1/D-C2): the frontend learns the compression
290
- // STARTED right before the summary LLM call ("Compressing context… / summarizing N messages" panel) — only
291
- // the lifecycle is surfaced, never the summary body. N = the number of history messages being summarized.
292
- callbacks?.onCompressStart?.({ messages: middle.length })
293
- const startedAt = performance.now()
294
- const summary = await chat({ ...agent.provider, thinking: null, reasoningEffort: null }, {
295
- messages: [{ role: "user", content: SUMMARIZE_PROMPT + serialized }],
296
- signal,
297
- // §18.6 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
298
- // depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
299
- logCtx: {
300
- stage: "compress", child: agent._logId, kind: "compress",
301
- role: agent._role ?? null, depth: extras?.traceDepth ?? null,
302
- session: agent._sessionStart ?? null, cwd: agent.cwd,
303
- traces: agent.config?.traces?.enabled !== false,
304
- },
305
- })
306
-
307
- applyCompression(agent, split.headEnd, split.tailStart, COMPACTION_PREFIX + summary.content)
308
-
309
- // Completion info for the compression panel (D-C2): tokens freed = the pre-compression prompt
310
- // estimate (`tokens` — the value that tripped the threshold, incl. system/tools overhead on the
311
- // pure-estimation path) minus the post-compression estimate on the same basis. Elapsed = the
312
- // summary call + splice duration. agent.mjs forwards this to onCompress unchanged.
313
- agent._lastCompressInfo = {
314
- mode: "summary",
315
- tokensFreed: Math.max(0, Math.round(tokens - (estimateTokens(agent.history) + overhead))),
316
- elapsedMs: performance.now() - startedAt,
317
- }
318
- return true
319
- }
320
-
321
- /**
322
- * Deterministic truncation fallback: called when the summary LLM fails repeatedly, no network call.
323
- * Drops the middle so the task can continue. Returns whether truncation happened.
324
- */
325
- export function compressFallback(agent) {
326
- const keepTail = keepTailSize(agent.provider, agent.history.length)
327
- const split = splitHistory(agent.history, keepTail, tailBudgetTokens(agent.provider))
328
- if (!split) return false
329
- const tailMessages = agent.history.length - split.tailStart
330
- applyCompression(agent, split.headEnd, split.tailStart, FALLBACK_NOTE)
331
- // Fallback completion info (D-C2): mode marks the deterministic-truncation path — the panel
332
- // shows the degradation note ("truncated to N messages") ONLY after 3 consecutive failures.
333
- agent._lastCompressInfo = { mode: "fallback", tailMessages }
334
- return true
335
- }
336
-
337
- /** 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) */
338
- const OVERSIZE_CONTENT_LIMIT = 8_000
339
-
340
- /**
341
- * Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
342
- * Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
343
- * does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400 risk.
344
- * Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
345
- */
346
- function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
347
- let shrunk = false
348
- // Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
349
- // message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
350
- // line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
351
- // the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
352
- // already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
353
- const next = agent.history.map((m) => {
354
- if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
355
- if (m.content.length <= limit) return m
356
- // Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
357
- const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
358
- const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
359
- shrunk = true
360
- return {
361
- ...m,
362
- content:
363
- m.content.slice(0, keepHead) +
364
- `\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
365
- m.content.slice(-keepTail),
366
- }
367
- })
368
- if (shrunk) {
369
- agent.history = next
370
- // Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
371
- agent._lastPromptTokens = null
372
- agent._usageAtLen = null
373
- }
374
- return shrunk
375
- }
376
-
377
- // ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
378
- // 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
379
- // explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
380
-
381
- export { summarizeRunExplorations, EXPLORE_TOOLS, EXPLORE_SUMMARY_PROMPT } from "./explore-distill.mjs"
@@ -1,223 +0,0 @@
1
- /**
2
- * conventions.mjs — the single authority for code / doc / temp path classification,
3
- * plus the project convention declaration surface (`.thincoder/conventions.json`).
4
- *
5
- * Why one module: engineering-mode gates and guards (design gate, review-doc gate,
6
- * mutation accounting, verify fast path) each carried their own copy of the
7
- * "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
8
- * prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
9
- * 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.
12
- *
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).
17
- *
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.
22
- */
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"]
29
-
30
- /** Project declaration file, relative to the project root. */
31
- export const CONVENTIONS_REL_PATH = ".thincoder/conventions.json"
32
-
33
- /** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
34
- const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
35
-
36
- /** Temp/scratch predicate (moved here verbatim from advisor/repos.mjs). */
37
- const TEMP_FILE = /(?:^|[/\\])tmp-[^/\\]+$|\.(?:tmp|temp)$/i
38
-
39
- /** True when a path is a throwaway temp file (tmp-* name or .tmp/.temp extension). */
40
- export function isTempPath(p) {
41
- return TEMP_FILE.test(p ?? "")
42
- }
43
-
44
- /** Path → segments (both separators accepted; absolute and relative alike). */
45
- function segmentsOf(p) {
46
- return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
47
- }
48
-
49
- /**
50
- * True when the path contains a declared code segment sequence at any depth.
51
- * 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.
55
- */
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())
61
- if (want.length === 0) continue
62
- for (let i = 0; i + want.length <= parts.length; i++) {
63
- if (want.every((seg, j) => parts[i + j] === seg)) return true
64
- }
65
- }
66
- return false
67
- }
68
-
69
- /** "code" | "doc" | "temp" — the single classification decision.
70
- * Precedence: code segment first (src/** stays product code even when the name
71
- * looks scratch — the pre-existing unconditional-src rule), then temp, then a
72
- * documentation extension, else code (anything not doc/temp is product code). */
73
- export function classifyPath(p, conv) {
74
- const s = String(p ?? "")
75
- if (hasCodeSegment(s, conv)) return "code"
76
- if (TEMP_FILE.test(s)) return "temp"
77
- if (DOC_FILE.test(s)) return "doc"
78
- return "code"
79
- }
80
-
81
- /** True when the path is product code (see classifyPath for the precedence). */
82
- export function isCodePath(p, conv) {
83
- return classifyPath(p, conv) === "code"
84
- }
85
-
86
- /** True when the path is a documentation file — a doc extension that does NOT
87
- * live inside a declared code segment (src/prompts/*.md is product code). */
88
- export function isDocPath(p, conv) {
89
- const s = String(p ?? "")
90
- return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
91
- }
92
-
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
109
- }
110
-
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
- }