thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
package/src/agent.mjs CHANGED
@@ -3,43 +3,43 @@
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
5
  import { chat } from "./provider/index.mjs"
6
- import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT, pushReal, summarizeRunExplorations } from "./context.mjs"
6
+ import { abortError, annotateAbort } from "./abort-provenance.mjs"
7
+ import { pushReal, summarizeRunExplorations } from "./context.mjs"
7
8
  import { specForModel } from "./config.mjs"
8
- import { readFileSync } from "node:fs"
9
- import { join, dirname } from "node:path"
10
- import { fileURLToPath } from "node:url"
9
+ import { resolve } from "node:path"
11
10
  import { executeToolCalls } from "./agent/dispatch.mjs"
12
11
  import { recordToolResults } from "./agent/record-results.mjs"
13
12
  import { FILE_MUTATORS } from "./agent/helpers.mjs"
14
13
  import { prepareRun } from "./agent/setup.mjs"
15
14
  import { injectPostTurn } from "./agent/post-turn.mjs"
16
15
  import { handleCompletion } from "./agent/completion.mjs"
17
- import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
18
- import { logEvent } from "./log.mjs"
16
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
17
+ // (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
18
+ // 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
19
+ // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
20
+ import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
19
21
  import {
20
- escapeXml, repairHistory, listWorkDir, ensureAutoReminder,
22
+ escapeXml, repairHistory, listWorkDir,
21
23
  readonlyToolNames, collectGitContext, loadProjectInstructions,
22
24
  ContinueError,
25
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 §19.3)
23
26
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
24
27
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
28
+ AUTO_TURN_DIGEST_DOMAIN,
25
29
  } from "./agent/helpers.mjs"
30
+ // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
31
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
32
+ // (与子代理角色常量同源——单一权威锚 D1);本文件 re-export 保 import 面。
33
+ export {
34
+ PERSONA_ENGINEERING, PERSONA_NORMAL, COMMON,
35
+ DISCIPLINE_ENGINEERING, DISCIPLINE_NORMAL,
36
+ CONSULT_BASE,
37
+ } from "./prompt-overlays.mjs"
38
+ export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
26
39
 
27
- // Prompt files (byte-stable, loaded once)
28
- const __dirname = dirname(fileURLToPath(import.meta.url))
29
- const SYSTEM_PROMPT = readFileSync(join(__dirname, "prompts", "system.md"), "utf8")
30
- const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
31
- const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
32
- let _EXPLORE, _CODER, _PLAN, _ENG_CODER, _CONSULT_BASE
33
- try { _EXPLORE = readFileSync(join(__dirname, "prompts", "explore.md"), "utf8") } catch { _EXPLORE = "" }
34
- try { _CODER = readFileSync(join(__dirname, "prompts", "coder.md"), "utf8") } catch { _CODER = "" }
35
- try { _PLAN = readFileSync(join(__dirname, "prompts", "plan.md"), "utf8") } catch { _PLAN = "" }
36
- try { _ENG_CODER = readFileSync(join(__dirname, "prompts", "eng-coder.md"), "utf8") } catch { _ENG_CODER = "" }
37
- try { _CONSULT_BASE = readFileSync(join(__dirname, "prompts", "consult-base.md"), "utf8") } catch { _CONSULT_BASE = "" }
38
- export const EXPLORE_OVERLAY = _EXPLORE
39
- export const CODER_OVERLAY = _CODER
40
- export const PLAN_OVERLAY = _PLAN
41
- export const ENG_CODER_OVERLAY = _ENG_CODER
42
- export const CONSULT_BASE = _CONSULT_BASE
40
+ // Role persona slot mapping note(施工② G3): 子代理人格由 assemblePrompt 场景表按
41
+ // role 承载(persona-explore/coder/plan/eng-coder.md——D1 表 = 蓝图 §3.2 1:1);
42
+ // spawn overlay 概念已退役(prompt-overlays.mjs 不再导出 OVERLAY 别名常量)。
43
43
 
44
44
  // exported for consumption by agent-tools.mjs
45
45
  export {
@@ -50,40 +50,9 @@ export {
50
50
  }
51
51
 
52
52
 
53
- // Engineering mode reminder — shared with eng.mjs tool
54
- export const ENG_ON_REMINDER =
55
- "[System reminder: engineering mode is ON — design-before-code enforced. " +
56
- "Workflow: Requirements doc → Design doc → advisor(type='design') → " +
57
- "user approval → eng-coder implementation. Code changes go through eng-coder " +
58
- "subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
59
- "flow nodes or when the user asks.]"
60
-
61
53
  // Re-exported for API compatibility (single source of truth: advisor/repos.mjs)
62
54
  export { hasCodeMutations } from "./advisor/repos.mjs"
63
55
 
64
- /** Engineering mode OFF reminder — shared with the eng tool and the injector. */
65
- export const ENG_OFF_REMINDER =
66
- "[System reminder: engineering mode is now OFF — standard discipline applies. " +
67
- "Changes go through the normal workflow: you may edit files directly, advisor/verify " +
68
- "guards apply per config.]"
69
-
70
- /** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
71
- * Injected per manual auto-turn run — writes/execute/spawns/questions are also
72
- * mechanically denied (no permission handler + spawn gate); this steers first. */
73
- const AUTO_TURN_DIGEST_DOMAIN =
74
- "[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) update the task list with the task tool (allowed) to mark finished work done; 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
75
-
76
- /** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
77
- * OFF is announced too — the model must know the gates lifted; silence after /eng-off
78
- * left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
79
- function injectEngineeringReminder(agent) {
80
- const eng = agent.config?.agent?.engineering ?? false
81
- if (eng !== agent._lastEngState) {
82
- agent.history.push({ role: "user", content: eng ? ENG_ON_REMINDER : ENG_OFF_REMINDER, transient: true })
83
- }
84
- agent._lastEngState = eng
85
- }
86
-
87
56
  /** Create a new agent state object with all fields initialized to defaults */
88
57
  export function createAgent({
89
58
  provider, tools, config, cwd, memory, overlay, role,
@@ -97,8 +66,12 @@ export function createAgent({
97
66
  planMode, autoApprove, goal,
98
67
  _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
99
68
  _engDesignReviewed: false, // eng-coder: design review gate passed (hard gate in dispatch.mjs)
100
- _engDesignToken: null, // issued by advisor(type="design"); required to spawn eng-coder
69
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): single-value `_engDesignToken` mirror retired
70
+ // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
71
+ // authoritative ledger (hydrated by restoreEngTokens / written by settle).
101
72
  _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
73
+ _advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
74
+ _mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
102
75
  _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
103
76
  _lastEngState: false,
104
77
  _pendingReminders: [],
@@ -114,7 +87,7 @@ export function createAgent({
114
87
  }
115
88
 
116
89
  /** Run the agent loop: LLM ↔ tool-call cycle until task completion or turn limit. Returns final text content. */
117
- export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, suspDriven = false } = {}) {
90
+ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, suspDriven = false, consumeInjected = null } = {}) {
118
91
  // Previous run's async exploration distillation must settle before this run pushes
119
92
  // input (SEND-STALL-DISTILL §2.2 N1) — await first, or its history replace wipes it.
120
93
  if (agent._pendingDistill) {
@@ -125,15 +98,25 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
125
98
  // §17 D-S3: suspension-settled async results inject before EVERY run's prepareRun
126
99
  // (user + auto-turn); spliced = consumed. collectSettledAsync owns a different
127
100
  // container, so no double-inject across the two consumption points.
101
+ // ASYNC-RESULT-CONTAINER.md D2 (2026-09-08):pending 单容器 `_pendingAsyncResults`
102
+ // +role——四族(subagent/advisor/escalate/consult)统一停靠;注入器按 role 分发
103
+ // (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 §25 D-R17a/b)
104
+ // ——单容器一处清,不再逐族三段。
128
105
  const pendingAsync = agent._pendingAsyncResults
129
106
  if (pendingAsync?.length) {
130
107
  const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
131
- for (const e of pendingAsync.splice(0)) await injectAsyncResult(agent, e)
108
+ const { injectConsultResult } = await import("./agent-tools/consult.mjs")
109
+ for (const e of pendingAsync.splice(0)) {
110
+ if (e.role === "consult") await injectConsultResult(agent, e)
111
+ else await injectAsyncResult(agent, e)
112
+ }
132
113
  }
133
114
  agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
134
115
  const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
116
+ // G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
117
+ // 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
135
118
  agent, input, callbacks,
136
- { depth, signal, overrideTurns, resume: resume || autoTurn, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
119
+ { depth, signal, overrideTurns, resume: resume || autoTurn },
137
120
  )
138
121
 
139
122
  // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
@@ -143,6 +126,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
143
126
  // Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
144
127
  // mutation/guard continuity and the convergence budget must survive a continuation.
145
128
  if (!resume) {
129
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):链内累计编号
130
+ // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
131
+ // turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
132
+ agent._turnSeq = 0
146
133
  // §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
147
134
  // reset) so auto-turn changes never escape the guard silently.
148
135
  const g = agent._inheritedGuard
@@ -160,18 +147,19 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
160
147
  agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
161
148
  agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
162
149
  agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
163
- agent._asyncCheckLastN = 0 // check action read counter is per-run (§15 D-A2): a fresh user turn restarts from 1
164
150
  }
165
151
  }
166
152
  // §17 D-S6 manual tier: digest action-domain reminder (system-driven turn — organize only).
167
153
  if (autoTurn && !agent.autoApprove) {
168
154
  agent.history.push({ role: "user", content: AUTO_TURN_DIGEST_DOMAIN, transient: true })
169
155
  }
170
- // eng-coder authorization is set by subagent.mjs AFTER token validation but BEFORE
171
- // runAgent only reset for the top-level agent (depth 0); child runs keep theirs
172
- if (depth === 0) agent._engDesignReviewed = false
173
- // _engDesignToken survives across turns (design review → approval → eng-coder spawn);
174
- // lifecycle: invalidated on failed re-review (advisor.mjs), issued on a passing one.
156
+ // eng-coder authorization (_engDesignReviewed) is eng-coder-only: set by subagent-spawn.mjs
157
+ // (spawn gate) / design-token.mjs (design review pass) BEFORE the child runAgent the
158
+ // depth-0 parent never reads or writes it (the parent gate reads anyLiveDesignSlot; the
159
+ // depth-0 per-turn reset was removed 2026-09-08, ENG-SESSION-PROVIDER-CLEANUP D1.3).
160
+ // Design slots (_engDesignTokens Map) survive across turns (design review approval
161
+ // eng-coder spawn) — persisted to the session slot at settle time (DESIGN-TOKEN-
162
+ // SETTLEMENT D1); lifecycle: issued on a passing review, consumed by consume-design / TTL.
175
163
  let guardPushbacks = 0
176
164
  let advisorPushbacks = 0
177
165
  let honestReminderInjected = false
@@ -194,64 +182,37 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
194
182
  let thrownError = null
195
183
  try {
196
184
  for (let turn = 0; turn < maxTurns; turn++) {
197
- // Update turn counter for status bar display
198
- agent._currentTurn = turn + 1
199
- agent._maxTurns = maxTurns
185
+ // 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):编号帧——
186
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
187
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
188
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
189
+ // turn / maxTurns——N6)。
190
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
191
+ agent._currentTurn = frame.turn
192
+ agent._maxTurns = frame.maxTurns
200
193
  // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
201
194
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
202
- // the onToolCall/onToolResult relay — no token for those).
195
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
196
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
203
197
  if (depth > 0 && callbacks.onToken) {
204
- callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
198
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
205
199
  }
206
200
 
201
+ // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
202
+ // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
203
+ // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
204
+ // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
205
+ consumeInjected?.(agent)
206
+
207
207
  const lastRole = agent.history.at(-1)?.role
208
208
  if (lastRole === "user" || lastRole === "tool") {
209
- try {
210
- if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
211
- agent._compressFailures = 0
212
- agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
213
- recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
214
- // Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
215
- // the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
216
- callbacks.onCompress?.(agent._lastCompressInfo ?? {})
217
- ensureAutoReminder(agent)
218
- }
219
- } catch (compressError) {
220
- // AbortError must not be swallowed: user cancellation must propagate
221
- if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
222
- agent._compressFailures = (agent._compressFailures ?? 0) + 1
223
- // Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
224
- // COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
225
- callbacks?.onCompressFail?.(compressError)
226
- if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
227
- agent._compressFailures = 0
228
- if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
229
- }
230
- }
231
- }
232
-
233
- // Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
234
- // (sparse every 2 turns, full every 5 / on new user message) so the restriction never fades.
235
- if (agent.planMode) {
236
- const lastMsg = agent.history.at(-1)
237
- const realUserMsg = lastMsg?.role === "user"
238
- && typeof lastMsg.content === "string"
239
- && !lastMsg.content.startsWith("[System reminder:")
240
- && !lastMsg.content.startsWith("[User interrupt:")
241
- const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
242
- const { planReminderForTurn } = await import("./agent-tools/plan.mjs")
243
- const reminder = planReminderForTurn(agent, newUserSince)
244
- if (reminder) {
245
- agent._planReminderAtLen = agent.history.length + 1
246
- agent.history.push({ role: "user", content: reminder, transient: true })
247
- }
209
+ // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
210
+ // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
211
+ await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
248
212
  }
249
213
 
250
- // Engineering-mode status injection on every new user message (design-before-code
251
- // vs standard discipline) — see injectEngineeringReminder.
252
- if (depth === 0) {
253
- injectEngineeringReminder(agent)
254
- }
214
+ // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
215
+ await injectTurnReminders(agent, { depth })
255
216
 
256
217
  const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
257
218
  let response
@@ -327,15 +288,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
327
288
  continue
328
289
  }
329
290
 
330
- // Stream rule warnings (action: "warn"): stream completed; inject de-duplicated
331
- // warnings so the model sees them before its next response.
332
- if (response._warnings?.length) {
333
- const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
334
- agent.history.push({
335
- role: "user",
336
- content: `[System reminder — stream rule warnings from your last response:\n${deDuplicated.map(w => `- ${w.name || w.pattern}: ${w.message}`).join("\n")}]`,
337
- })
338
- }
291
+ // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
292
+ // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
293
+ injectResponseReminders(agent, response)
339
294
 
340
295
  // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
341
296
  // message, then signal the outer loop to recreate the controller and resume.
@@ -347,7 +302,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
347
302
  role: "user",
348
303
  content: `[User interrupt: ${response.interruptMessage}]`,
349
304
  })
350
- throw Object.assign(new Error("User interrupted"), { name: "AbortError" })
305
+ // §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
306
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
351
307
  }
352
308
 
353
309
  if (response.usage) {
@@ -359,18 +315,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
359
315
  }
360
316
 
361
317
  // Warn on abnormal finish reasons — the response may be incomplete/truncated.
362
- if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
363
- const reasonMap = {
364
- length: "output token limit reached after exhausting continuations",
365
- insufficient_system_resource: "provider inference resources exhausted — consider retrying or switching models",
366
- content_filter: "response blocked by provider content filtering",
367
- }
368
- const detail = reasonMap[response.finishReason] || `unknown reason "${response.finishReason}"`
369
- agent.history.push({
370
- role: "user",
371
- content: `[System reminder: the previous turn ended abnormally — ${detail}. The assistant response that follows may be incomplete.]`,
372
- })
373
- }
318
+ // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
374
319
 
375
320
  if (response.toolCalls.length === 0) {
376
321
  const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
@@ -390,7 +335,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
390
335
  }
391
336
 
392
337
  // abort after chat completes, before committing history: don't commit a half-finished turn
393
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
338
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
394
339
 
395
340
  pushReal(agent, {
396
341
  role: "assistant",
@@ -412,6 +357,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
412
357
  // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
413
358
  // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
414
359
  // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
360
+ // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
361
+ // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
362
+ // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
415
363
  for (const { toolCall, ok } of results) {
416
364
  const tool = toolByName.get(toolCall.name)
417
365
  if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
@@ -423,7 +371,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
423
371
  const args = JSON.parse(toolCall.arguments)
424
372
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
425
373
  for (const p of paths) {
426
- const abs = join(agent.cwd, p)
374
+ const abs = resolve(agent.cwd, p)
427
375
  if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
428
376
  }
429
377
  } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
@@ -457,73 +405,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
457
405
  thrownError = e
458
406
  throw e
459
407
  } finally {
460
- // Turn-end cleanup: abort any leftover consultation children (consult_start spawns
461
- // fire-and-forget runners; a completed turn must not let them keep burning tokens).
462
- cleanupConsultSessions(agent)
463
- // Async subagent turn-end handling (AGENT-LOOP.md §15 D-A3 + §17 D-S1). Lifecycle:
464
- // - Ctrl+C (plain abort): children were aborted with the parent signal — clear
465
- // WITHOUT injecting stale errors (user explicitly stopped). Ctrl+I (interrupt)
466
- // keeps the pool: the turn resumes with the interrupt message, children stay
467
- // tracked (in a suspension session children hold agent._sessionSignal and a
468
- // digest's own Ctrl+I must not orphan them).
469
- // - ContinueError (turn cap): no wait, no injection — children keep running and
470
- // the RESUME run's turn-end collection takes over.
471
- // - anything else: inject the SETTLED entries only; running/queued stay in the
472
- // pool for the suspension session (D-S1 — no allSettled turn-end wait).
473
- if (signal?.aborted && !signal?.reason?.interrupt) {
474
- if (agent._asyncSubagents?.size > 0) {
475
- logEvent("ev:stopped", { poolN: agent._asyncSubagents?.size ?? 0, where: "turn-end-abort" })
476
- }
477
- agent._asyncSubagents?.clear()
478
- agent._asyncQueue = []
479
- agent._asyncCheckLastN = 0
480
- } else if (thrownError instanceof ContinueError) {
481
- // keep _asyncSubagents + the check counter — the resumed run continues them
482
- } else {
483
- await collectSettledAsync(agent, { suspDriven })
484
- agent._asyncCheckLastN = 0
485
- }
486
- agent._inAutoTurn = false
487
- // §17 D-S6: auto-turn guard marks survive into the next USER run (restored at its
488
- // !resume reset above). Normal ends only — abort discards; ContinueError lets the
489
- // auto-resumed run snapshot at its own end.
490
- if (autoTurn && !(signal?.aborted && !signal?.reason?.interrupt) && !(thrownError instanceof ContinueError)) {
491
- agent._inheritedGuard = {
492
- _mutatedThisRun: agent._mutatedThisRun, _verifiedThisRun: agent._verifiedThisRun,
493
- _verifyPassed: agent._verifyPassed, _calledAdvisorThisRun: agent._calledAdvisorThisRun,
494
- _touchedFiles: agent._touchedFiles, _verifyRetries: agent._verifyRetries,
495
- _advisorRound: agent._advisorRound,
496
- }
497
- }
498
- }
499
- }
500
-
501
- /**
502
- * Turn-end async subagent collection (AGENT-LOOP.md §17 D-S1 + §17.5 supersede):
503
- * two modes, selected by the caller's driver context (17.5.2/17.5.4 #2):
504
- * - suspDriven=false (fallback — headless/direct runAgent callers without a
505
- * suspension driver): inject every entry that SETTLED during this run
506
- * (XML-escaped report/error — child reports may carry file/webpage content;
507
- * >64K offloaded with preview + path) and remove it from the pool. Running/
508
- * queued STAY — no allSettled wait. Results never lost without a session.
509
- * - suspDriven=true (the interaction layer runs suspensionSession after this
510
- * run): NO direct inject — settled entries STAY pooled (settled not consumed)
511
- * for the session's first sweepSettledToPending → digest turn (§17.5 — done
512
- * 条目留池等消化轮注入;check/status 在 sweep 前仍从池读——17.5.2 不变面)。
513
- * maybeRefillAsync runs in both modes (starts queued heads whose slot freed).
514
- * Single ownership: entries settled inside a suspension session were moved to
515
- * _pendingAsyncResults by the settle callback, so this only sees user-turn
516
- * settles (no double inject — D-S3 points ①/②). The ⟦ev⟧done freeze is NOT
517
- * emitted here — each settle callback emits it (§15 D-A3). */
518
- async function collectSettledAsync(agent, { suspDriven = false } = {}) {
519
- const map = agent._asyncSubagents
520
- if (!map || map.size === 0) return
521
- const { maybeRefillAsync, injectAsyncResult } = await import("./agent-tools/subagent.mjs")
522
- maybeRefillAsync(agent) // start queued heads now that slots may have freed — no waiting
523
- if (suspDriven) return // §17.5: settled stays pooled — the suspension session digests it
524
- for (const e of [...map.values()]) {
525
- if (!e.done) continue // still running — stays in the pool (D-S1)
526
- await injectAsyncResult(agent, e)
527
- map.delete(String(e.id))
408
+ // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
409
+ // + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
410
+ // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
411
+ await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
528
412
  }
529
413
  }
@@ -1,4 +1,4 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig, configPath } from "../config.mjs"
3
3
  import { createMemory } from "../memory.mjs"
4
4
  import { teamConfig, gitAuthor } from "./make-agent.mjs"
@@ -10,7 +10,7 @@ function noKeyMessage() {
10
10
  return `还没有配置 API key。运行 thincoder 进入 TUI,用 /provider add 和 /provider key 配置;或直接编辑 ${configPath}`
11
11
  }
12
12
 
13
- /** thincoder distill <transcript-file> [--yes] [--scope=...]
13
+ /** thincoder distill <transcript-file> [--yes] [--layer=...]
14
14
  * Returns exit code: 0=success, 1=error */
15
15
  export async function distillCommand(args, exitSoon) {
16
16
  const flags = {}
@@ -20,9 +20,15 @@ export async function distillCommand(args, exitSoon) {
20
20
  if (m) flags[m[1]] = m[2] ?? true
21
21
  else positional.push(a)
22
22
  }
23
+ // AC2(MEMORY.md §6.5):显式拦截旧 flag——`--scope=X` 与 `--scope X` 两形态落入 flags.scope,
24
+ // 报错防静默吞参(解析器本不校验未知 flag——旧 --scope=project 静默落 personal 最危险)。
25
+ if (flags.scope !== undefined) {
26
+ console.error("distill: --scope renamed to --layer — update your invocation")
27
+ return 1
28
+ }
23
29
  const file = positional[0]
24
30
  if (!file) {
25
- console.error("Usage: thincoder distill <transcript-file> [--yes] [--scope=personal|project|team]")
31
+ console.error("Usage: thincoder distill <transcript-file> [--yes] [--layer=personal|project|team]")
26
32
  return 1
27
33
  }
28
34
  const { readFile } = await import("node:fs/promises")
@@ -58,15 +64,15 @@ export async function distillCommand(args, exitSoon) {
58
64
  }
59
65
 
60
66
  const opts = {
61
- projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
67
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
62
68
  team,
63
69
  author: gitAuthor(),
64
70
  }
65
71
  let saved = 0
66
72
  for (const c of candidates) {
67
- if (flags.scope) c.scope = flags.scope
73
+ if (flags.layer) c.layer = flags.layer
68
74
  console.log(`\n--- candidate ---`)
69
- console.log(`[${c.type}] ${c.title} (scope: ${c.scope})`)
75
+ console.log(`[${c.type}] ${c.title} (layer: ${c.layer})`)
70
76
  console.log(c.content)
71
77
  if (c.type === "rule") {
72
78
  console.log("(rule 类知识通常建议手动撰写;确认提取吗?)")
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process"
2
- import { join } from "node:path"
2
+ import { isAbsolute, join } from "node:path"
3
3
  import { createAgent } from "../agent.mjs"
4
4
  import { loadConfig, configDir } from "../config.mjs"
5
5
  import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
@@ -7,9 +7,21 @@ import { settingsTool } from "../agent-tools/settings.mjs"
7
7
  import { repoOutlineTool } from "../tools/repomap.mjs"
8
8
  import { builtinTools } from "../tools/index.mjs"
9
9
  import { discoverRules } from "../rules.mjs"
10
+ // R10 L2(MULTI-INSTANCE-COLLAB §2a.4 D-L2b):peer_instances 只读工具——挂感知模块导出
11
+ import { peerInstancesTool } from "../peer-instances.mjs"
12
+
13
+ /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
+ * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
15
+ * ACP 通道经 `assembleAgent({ excludeTools })` 传入(acp.mjs `ACP_EXCLUDED_TOOLS`)。 */
16
+ export function applyToolExclusions(tools, excludeTools = []) {
17
+ const excluded = new Set(excludeTools ?? [])
18
+ if (excluded.size === 0) return tools
19
+ const kept = tools.filter((t) => !excluded.has(t?.name))
20
+ return kept.length === tools.length ? tools : kept
21
+ }
10
22
 
11
23
  /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
12
- export async function assembleAgent() {
24
+ export async function assembleAgent({ excludeTools = [] } = {}) {
13
25
  const config = loadConfig()
14
26
  const provider = config.provider
15
27
  const providers = config.providersList
@@ -18,7 +30,7 @@ export async function assembleAgent() {
18
30
  const { injectProxy } = await import("../proxy.mjs")
19
31
  injectProxy(providers, config)
20
32
  // config.provider 是 loadConfig 里的独立拷贝,同步注入结果
21
- provider.proxyUri = providers.find((p) => p.name === config.activeProvider)?.proxyUri
33
+ if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
22
34
 
23
35
  const memory = createMemory({ dbPath: config.memory.dbPath })
24
36
  // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
@@ -39,7 +51,7 @@ export async function assembleAgent() {
39
51
  memory.codeOrigin = cwd
40
52
  // Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
41
53
  if (config.memory.projectDir) {
42
- memory.projectOrigin = join(cwd, config.memory.projectDir)
54
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir)
43
55
  await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
44
56
  }
45
57
  // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
@@ -49,7 +61,7 @@ export async function assembleAgent() {
49
61
  await ensureClone(team)
50
62
  await syncDir(memory, { layer: "team", dir: team.dir })
51
63
  }
52
- const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd), settingsTool()]
64
+ const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd), settingsTool(), peerInstancesTool]
53
65
 
54
66
  // MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
55
67
  const mcpServers = config.mcp?.servers ?? []
@@ -99,18 +111,24 @@ export async function assembleAgent() {
99
111
 
100
112
  const agent = createAgent({
101
113
  provider,
102
- tools: [...baseTools, ...mcpTools],
114
+ tools: applyToolExclusions([...baseTools, ...mcpTools], excludeTools),
103
115
  config,
104
116
  cwd,
105
117
  memory,
106
118
  })
107
119
  agent.providers = providers
108
- agent.activeProvider = config.activeProvider
109
- agent.activeModel = config.activeModel ?? null
120
+ // MODEL-MERGE-SESSION:config 无 active*——会话运行时起点 = defaultModel 复合解析
121
+ // (provider 对象带解析后 model——API/spec 消费点零改);无效 → {} + 原因走 D-S1 标记
122
+ agent.activeProvider = provider.name ?? ""
123
+ agent.activeModel = provider.model ?? null
110
124
  agent._mcpWarnings = mcpWarnings
111
125
  // SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
112
126
  // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
113
127
  validateProvider(agent)
128
+ // defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
129
+ if (agent._providerInvalid && config.providerInvalidReason) {
130
+ agent._providerInvalidReason = config.providerInvalidReason
131
+ }
114
132
  return agent
115
133
  }
116
134
 
@@ -1,7 +1,8 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig } from "../config.mjs"
3
3
  import { teamConfig } from "./make-agent.mjs"
4
- import { put, search, list, deleteByUid } from "../memory/core.mjs"
4
+ import { put, search, list } from "../memory/core.mjs"
5
+ import { deleteByUid } from "../memory/delete.mjs"
5
6
 
6
7
  /** thincoder memory <list|search|put|remove> subcommands.
7
8
  * opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
@@ -66,7 +67,7 @@ export async function memoryCommand(memory, args, opts = {}) {
66
67
  function cliDirs() {
67
68
  const config = loadConfig()
68
69
  const dirs = { project: null, team: null }
69
- if (config.memory?.projectDir) dirs.project = join(process.cwd(), config.memory.projectDir)
70
+ if (config.memory?.projectDir) dirs.project = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
70
71
  const team = teamConfig(config)
71
72
  if (team) dirs.team = team.dir
72
73
  return dirs
@@ -23,8 +23,8 @@ export function formatPermission(name, args) {
23
23
  // §6 action-routed preview: put shows content, batch delete/clear show the gate args
24
24
  const action = String(args.action ?? "")
25
25
  if (action === "put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
26
- if (action === "delete") return args.id ? `id=${args.id} scope=${args.scope}` : `batch delete scope=${args.scope} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
27
- if (action === "clear") return `clear scope=${args.scope} confirm=${args.confirm}`
26
+ if (action === "delete") return args.id ? `id=${args.id}${args.layer ? ` layer=${args.layer}` : ""}` : `batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
27
+ if (action === "clear") return `clear layer=${args.layer ?? ""} confirm=${args.confirm}`
28
28
  return cap(summarize(args), 300)
29
29
  }
30
30
  return cap(summarize(args), 300)