@thincoder/core 0.10.12 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/advisor/messages.mjs +4 -2
  3. package/advisor/notice.mjs +1 -1
  4. package/advisor/project-context.mjs +121 -11
  5. package/agent/completion.mjs +13 -1
  6. package/agent/digest-account.mjs +73 -46
  7. package/agent/dispatch-run.mjs +1 -1
  8. package/agent/dispatch.mjs +29 -21
  9. package/agent/family-tools.mjs +9 -5
  10. package/agent/helpers.mjs +1 -94
  11. package/agent/offload.mjs +88 -0
  12. package/agent/record-results.mjs +1 -6
  13. package/agent/run-stages.mjs +74 -49
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +6 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +35 -3
  18. package/agent/write-face.mjs +99 -0
  19. package/agent/write-gate.mjs +354 -5
  20. package/agent-tools/advisor.mjs +127 -11
  21. package/agent-tools/async-settle.mjs +2 -2
  22. package/agent-tools/bash-async.mjs +8 -2
  23. package/agent-tools/batch-closeout.mjs +206 -0
  24. package/agent-tools/batch-gate.mjs +287 -73
  25. package/agent-tools/batch-ledger-bind.mjs +183 -0
  26. package/agent-tools/batch-ledger-link.mjs +73 -0
  27. package/agent-tools/batch-lifecycle.mjs +113 -354
  28. package/agent-tools/batch-paths.mjs +45 -40
  29. package/agent-tools/batch-read.mjs +1 -1
  30. package/agent-tools/batch-reopen.mjs +142 -0
  31. package/agent-tools/batch-skeleton.mjs +283 -80
  32. package/agent-tools/batch-unit-test-gate.mjs +107 -0
  33. package/agent-tools/batch.mjs +243 -50
  34. package/agent-tools/context.mjs +6 -3
  35. package/agent-tools/design-slots.mjs +24 -24
  36. package/agent-tools/design-token.mjs +96 -15
  37. package/agent-tools/doc-check.mjs +18 -2
  38. package/agent-tools/eng.mjs +1 -1
  39. package/agent-tools/escalate-async.mjs +3 -3
  40. package/agent-tools/manifest.mjs +8 -5
  41. package/agent-tools/read-history.mjs +5 -5
  42. package/agent-tools/review-facts.mjs +31 -13
  43. package/agent-tools/spawn-gates.mjs +10 -14
  44. package/agent-tools/ssh-session.mjs +120 -111
  45. package/agent-tools/ssh-transfer.mjs +210 -0
  46. package/agent-tools/ssh-transport.mjs +277 -46
  47. package/agent-tools/subagent-actions-query.mjs +1 -1
  48. package/agent-tools/subagent-actions.mjs +1 -1
  49. package/agent-tools/subagent-async.mjs +3 -2
  50. package/agent-tools/subagent-scheduler.mjs +43 -9
  51. package/agent-tools/subagent-spawn.mjs +68 -9
  52. package/agent-tools/subagent.mjs +16 -14
  53. package/agent.mjs +1 -0
  54. package/client-identity.mjs +70 -0
  55. package/config-presets.mjs +5 -3
  56. package/config.mjs +19 -13
  57. package/context.mjs +39 -8
  58. package/declaration.mjs +2 -2
  59. package/doc-check/anchors.mjs +128 -28
  60. package/doc-check/index.mjs +93 -27
  61. package/doc-check/line-refs.mjs +112 -0
  62. package/doc-check/paragraph.mjs +24 -8
  63. package/embedding.mjs +2 -0
  64. package/explore-distill.mjs +1 -1
  65. package/generate-title.mjs +3 -2
  66. package/i18n.mjs +1 -1
  67. package/index-discover.mjs +4 -1
  68. package/ledger-close-proof.mjs +62 -0
  69. package/ledger-cmd.mjs +79 -194
  70. package/ledger-db.mjs +29 -28
  71. package/ledger-migrate.mjs +4 -4
  72. package/ledger-read.mjs +10 -8
  73. package/ledger-surface.mjs +1 -1
  74. package/ledger-tools.mjs +10 -10
  75. package/ledger-variant-notice.mjs +1 -1
  76. package/ledger-write-ops.mjs +373 -0
  77. package/ledger.mjs +2 -2
  78. package/manifest-discovery.mjs +2 -2
  79. package/manifest-schema.mjs +4 -0
  80. package/memory/memory-tool.mjs +5 -2
  81. package/memory/schema.mjs +5 -26
  82. package/model-specs.mjs +6 -5
  83. package/package.json +1 -1
  84. package/path-resolve.mjs +217 -0
  85. package/permission.mjs +4 -0
  86. package/prompt-overlays.mjs +5 -0
  87. package/prompts/advisor-design.md +9 -6
  88. package/prompts/advisor-round1.md +4 -3
  89. package/prompts/advisor-round2.md +6 -5
  90. package/prompts/advisor-round3.md +6 -5
  91. package/prompts/common.md +23 -10
  92. package/prompts/consult-base.md +2 -1
  93. package/prompts/discipline-engineering.md +29 -48
  94. package/prompts/discipline-normal.md +10 -5
  95. package/prompts/persona-eng-coder.md +8 -0
  96. package/prompts/persona-eng-designer.md +22 -12
  97. package/prompts/persona-engineering.md +81 -66
  98. package/prompts/persona-plan.md +2 -1
  99. package/prompts/persona-tester.md +55 -0
  100. package/provider/anthropic.mjs +12 -7
  101. package/provider/core.mjs +21 -5
  102. package/provider/errors.mjs +41 -0
  103. package/provider/google.mjs +25 -9
  104. package/provider/list-models.mjs +4 -3
  105. package/provider/rate.mjs +11 -3
  106. package/provider/responses.mjs +17 -7
  107. package/provider/sse.mjs +15 -4
  108. package/session-identity.mjs +21 -0
  109. package/session-lifecycle.mjs +12 -4
  110. package/session-slots.mjs +9 -14
  111. package/session.mjs +5 -0
  112. package/skip-dirs.mjs +48 -0
  113. package/team-ledger.mjs +4 -4
  114. package/text-budget.mjs +4 -0
  115. package/token-window.mjs +107 -10
  116. package/tool-docs/advisor.md +2 -2
  117. package/tool-docs/apply_patch.md +2 -0
  118. package/tool-docs/bash.md +4 -0
  119. package/tool-docs/batch.md +85 -1
  120. package/tool-docs/browser.md +3 -1
  121. package/tool-docs/code_search.md +3 -1
  122. package/tool-docs/consult_start.md +6 -2
  123. package/tool-docs/consult_stop.md +3 -1
  124. package/tool-docs/delete.md +3 -0
  125. package/tool-docs/doc_check.md +43 -8
  126. package/tool-docs/doc_search.md +5 -1
  127. package/tool-docs/edit.md +22 -1
  128. package/tool-docs/execute.md +7 -1
  129. package/tool-docs/fetch.md +3 -1
  130. package/tool-docs/file_ops.md +3 -1
  131. package/tool-docs/git.md +9 -1
  132. package/tool-docs/goal.md +2 -0
  133. package/tool-docs/grep.md +1 -1
  134. package/tool-docs/hashline_edit.md +2 -0
  135. package/tool-docs/insert_after.md +3 -1
  136. package/tool-docs/ledger.md +5 -5
  137. package/tool-docs/lint.md +1 -0
  138. package/tool-docs/manifest-read.md +2 -1
  139. package/tool-docs/manifest.md +9 -2
  140. package/tool-docs/memory.md +11 -1
  141. package/tool-docs/notify_parent.md +1 -1
  142. package/tool-docs/plan.md +2 -0
  143. package/tool-docs/question.md +1 -1
  144. package/tool-docs/read_history.md +15 -3
  145. package/tool-docs/read_image.md +3 -1
  146. package/tool-docs/recent_changes.md +3 -1
  147. package/tool-docs/repo_outline.md +3 -1
  148. package/tool-docs/settings.md +10 -2
  149. package/tool-docs/skill.md +2 -0
  150. package/tool-docs/ssh.md +30 -17
  151. package/tool-docs/subagent.md +50 -13
  152. package/tool-docs/task.md +2 -0
  153. package/tool-docs/team_ledger.md +1 -1
  154. package/tool-docs/timer.md +7 -0
  155. package/tool-docs/verify.md +13 -1
  156. package/tool-docs/websearch.md +3 -1
  157. package/tool-docs/write.md +2 -0
  158. package/tools/bash.mjs +12 -2
  159. package/tools/edit-batch.mjs +40 -33
  160. package/tools/edit-diff.mjs +69 -1
  161. package/tools/execute.mjs +10 -4
  162. package/tools/file.mjs +17 -27
  163. package/tools/git.mjs +27 -1
  164. package/tools/ops.mjs +2 -1
  165. package/tools/patch.mjs +12 -3
  166. package/tools/process-doc-mark.mjs +139 -0
  167. package/tools/search.mjs +15 -6
  168. package/tools/shared.mjs +24 -1
  169. package/tools/ssh.mjs +48 -7
  170. package/tools/tree.mjs +4 -1
  171. package/tools/write-path.mjs +64 -3
  172. package/traces/trace-store.mjs +4 -3
  173. package/agent/light-round.mjs +0 -63
  174. package/agent/unbooked-pen.mjs +0 -62
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { RETRYABLE_STATUS, RATE_LIMIT_BACKOFF_MS } from "./rate.mjs"
11
+ import { requestBudget } from "../config.mjs"
11
12
 
12
13
  /** Parse Retry-After: 秒数 or HTTP-date;上限 300s(会诊 #11)— 异常头不得让 CLI 睡数小时。
13
14
  * header 缺失/非法时退回指数退避表(rateLimitHits 计数取档)。 */
@@ -132,3 +133,43 @@ export function classifyResponsesErrorCode(code) {
132
133
  return null
133
134
  }
134
135
  }
136
+
137
+ /** 越窗报文特征(§6.17 HTTP 400 分类——三个渠道的逐字形态;词表就这一处)。 */
138
+ const CONTEXT_OVERFLOW_PHRASES = ["maximum context length", "context_length_exceeded", "prompt is too long"]
139
+
140
+ /** 越窗 typed 错误(§6.17):三实数逐字带出(prompt / max_tokens / 窗口,另附 limit);kind / retryable
141
+ * 复用同族既有词表(`classifyResponsesErrorCode("context_length_exceeded")`),零新词。 */
142
+ export function contextOverflowError(provider, prompt, budget, remedy) {
143
+ const cls = classifyResponsesErrorCode("context_length_exceeded")
144
+ const err = new Error(
145
+ `context overflow: prompt ~${prompt} tokens · max_tokens ${budget.maxTokens ?? "not sent"}` +
146
+ ` · window ${budget.window} (limit ${budget.limit}) — ${remedy} [model ${provider?.model ?? "unknown"}]`,
147
+ )
148
+ err.kind = cls.kind
149
+ err.retryable = cls.retryable
150
+ return err
151
+ }
152
+
153
+ /** 发送面净判(§6.17 第二道,四个装配面同形一次调用):`estPrompt` = 输入侧估算
154
+ * (= `estimateRequestTokens(X) − (body.max_tokens ?? 0)`,四面同一定义——不减会把输出预算白扣一份)。
155
+ * 内部阶段豁免:`compress` / `distill` / 续写本身就是越线后用来把上下文降下来的那次调用,
156
+ * 判它 = 判掉止血动作本身(标签已存在,零新增机制)。 */
157
+ export function assertPromptFits(provider, estPrompt, logCtx) {
158
+ const stage = logCtx?.stage
159
+ if (stage === "compress" || stage === "distill" || logCtx?.isContinuation === true) return
160
+ const budget = requestBudget(provider)
161
+ if (!(estPrompt > budget.limit)) return
162
+ throw contextOverflowError(provider, estPrompt, budget, "the request was not sent; reduce the conversation (/compact or prune) before retrying")
163
+ }
164
+
165
+ /** HTTP 400 越窗报文 ⇒ typed 错误(§6.17);未命中 ⇒ 原样返回入参错误(不发明分类)。 */
166
+ export function markContextOverflow(error, status, text) {
167
+ if (status !== 400) return error
168
+ const lower = String(text ?? "").toLowerCase()
169
+ if (!CONTEXT_OVERFLOW_PHRASES.some((p) => lower.includes(p))) return error
170
+ const cls = classifyResponsesErrorCode("context_length_exceeded")
171
+ error.kind = cls.kind
172
+ error.retryable = cls.retryable
173
+ if (cls.hint) error.message += `(${cls.hint})`
174
+ return error
175
+ }
@@ -5,12 +5,14 @@
5
5
  */
6
6
 
7
7
  import { proxyFetch } from "../proxy.mjs"
8
+ import { withUserAgent } from "../client-identity.mjs"
8
9
  import { requestWithRetry } from "./retry.mjs"
9
10
  import { effectiveFetchTimeoutMs, effectiveBodyIdleMs } from "./core.mjs"
10
11
  import { abortError, timeoutError } from "../abort-provenance.mjs"
11
12
  import { terminateBody } from "../stream-destroy.mjs"
12
13
  import { requestBudget } from "../config.mjs"
13
14
  import { DEFAULT_BODY_IDLE_MS } from "../config-defaults.mjs"
15
+ import { assertPromptFits } from "./errors.mjs"
14
16
 
15
17
  /** OpenAI 语义 tool_choice → Gemini FunctionCallingConfig(2026-08-31 能力层)。 */
16
18
  function mapFunctionCallingConfig(choice) {
@@ -77,7 +79,7 @@ export function convertMessages(messages) {
77
79
 
78
80
  /** Build and send a Gemini chat request. Returns the same shape as core.mjs chat.
79
81
  * 2026-08-31 会诊 #6:接入 rateGate/recordRate(原实现完全绕过 TPM/RPM 闸门)。 */
80
- export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice }) {
82
+ export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, logCtx }) {
81
83
  const systemMessages = messages.filter((m) => m.role === "system")
82
84
  const contents = convertMessages(messages)
83
85
 
@@ -118,7 +120,10 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
118
120
 
119
121
  // 会诊 #6:TPM/RPM 闸门 + 记账
120
122
  const { rateGate, recordRate, estimateRequestTokens } = await import("./rate.mjs")
121
- const estimated = estimateRequestTokens({ messages })
123
+ // §6.17 发送面净判:估准面补 `tools`(原只投影 messages);body 用 `maxOutputTokens`、无 `max_tokens` 键 ⇒ 减项恒 0
124
+ const estimated = estimateRequestTokens({ messages, tools })
125
+ const estPrompt = estimated - (body.max_tokens ?? 0)
126
+ assertPromptFits(provider, estPrompt, logCtx)
122
127
  await rateGate(provider, estimated, onWait, signal)
123
128
 
124
129
  // 2026-08-31:5xx/网络与 OpenAI 格式统一退避重试链(原完全无重试——Gemini 高峰
@@ -126,7 +131,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
126
131
  const response = await requestWithRetry(
127
132
  () => proxyFetch(url, {
128
133
  method: "POST",
129
- headers: { ...(provider.headers ?? {}), "Content-Type": "application/json" }, // 定制头展开(PROVIDER.md §21)——定制头在前、内置头在后:内置头胜出
134
+ headers: { ...withUserAgent(provider.headers), "Content-Type": "application/json" }, // 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17);内置头在后故同名胜出
130
135
  body: JSON.stringify(body),
131
136
  // 2026-09-01:同 core.mjs——绝对墙钟废除;响应头阶段 fetchTimeoutMs(600s 默认),body 阶段读侧 idle 管
132
137
  signal,
@@ -136,7 +141,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
136
141
  { signal, onWait, buildMessage: (status, text) => `Gemini API error ${status}: ${text}` },
137
142
  )
138
143
 
139
- const result = await parseGeminiStream(response, { onToken, onReasoning, signal, idleMs: effectiveBodyIdleMs(provider) })
144
+ const result = await parseGeminiStream(response, { onToken, onReasoning, signal, idleMs: effectiveBodyIdleMs(provider), firstChunkMs: effectiveFetchTimeoutMs(provider) })
140
145
  recordRate(provider, estimated, result.usage)
141
146
 
142
147
  const usage = result.usage
@@ -145,6 +150,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
145
150
  content: result.content,
146
151
  reasoning: result.reasoning,
147
152
  finishReason: result.finishReason ?? null, // §6.31.7 F-DA5(原三 transport 恒 null——字段值收正)
153
+ estimated: estPrompt, // §6.17 自校准采样(den 与净判同口径)
148
154
  usage: {
149
155
  prompt_tokens: usage.prompt_tokens ?? 0,
150
156
  completion_tokens: usage.completion_tokens ?? 0,
@@ -154,7 +160,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
154
160
  }
155
161
  }
156
162
 
157
- return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, toolCalls: result.toolCalls }
163
+ return { content: result.content, reasoning: result.reasoning, finishReason: result.finishReason ?? null, estimated: estPrompt, toolCalls: result.toolCalls }
158
164
  }
159
165
 
160
166
  /** Gemini `finishReason` ⇒ 核统一 `finishReason`(§6.31.7 F-DA5——归一只喂既有分类;
@@ -169,7 +175,7 @@ function mapGeminiFinishReason(reason) {
169
175
  * Parse Gemini SSE stream.
170
176
  * Format: data: {...}\n\n (each line is a complete JSON object)
171
177
  */
172
- async function parseGeminiStream(response, { onToken, onReasoning, signal, idleMs = DEFAULT_BODY_IDLE_MS }) {
178
+ export async function parseGeminiStream(response, { onToken, onReasoning, signal, idleMs = DEFAULT_BODY_IDLE_MS, firstChunkMs }) {
173
179
  const result = { content: "", reasoning: "", toolCalls: [], usage: null, finishReason: null }
174
180
  const decoder = new TextDecoder()
175
181
  let buffer = ""
@@ -216,21 +222,31 @@ async function parseGeminiStream(response, { onToken, onReasoning, signal, idleM
216
222
 
217
223
  if (!response.body) throw new Error("No stream response body")
218
224
  // 2026-09-01 读侧 idle 超时(同 sse.mjs):body 有数据流动即不超时;连续 `idleMs` 无新 chunk 判死
225
+ // (首 chunk 之前改用首 chunk 窗,见下)
226
+ // 首 chunk 两态臂(TIMEOUT-DIGEST.md §2.4):首个 body 字节前用 `firstChunkMs`,之后每次重挂用 `idleMs`
219
227
  let idleTimer = null
220
228
  let idleError = null
229
+ let gotFirstChunk = false
221
230
  const armIdle = () => {
222
231
  if (idleTimer) clearTimeout(idleTimer)
232
+ // 同 sse.mjs:首挂窗缺省取 `idleMs`,相位文案按态取(未传者仍报既有 `google-sse-idle`)
233
+ const firstArm = !gotFirstChunk
234
+ const ms = firstArm ? (firstChunkMs ?? idleMs) : idleMs
235
+ const firstChunkPhase = firstArm && firstChunkMs != null
223
236
  idleTimer = setTimeout(() => {
224
237
  // #878:断流单点分流——直连 fetch(web 流)走 `IDLE_ABORT` 内部 abort 通道;
225
- // proxy 形回落 destroyBody(原契约)。相位串 = 本分支专属 google-sse-idle。
226
- idleError = timeoutError(`SSE idle timeout: no data for ${idleMs / 1000}s`, "provider", "google-sse-idle")
238
+ // proxy 形回落 destroyBody(原契约)。
239
+ idleError = firstChunkPhase
240
+ ? timeoutError(`SSE first-chunk timeout: no data for ${ms / 1000}s`, "provider", "google-sse-first-chunk")
241
+ : timeoutError(`SSE idle timeout: no data for ${ms / 1000}s`, "provider", "google-sse-idle")
227
242
  try { terminateBody(response, idleError) } catch { /* already gone */ }
228
- }, idleMs)
243
+ }, ms)
229
244
  idleTimer.unref?.()
230
245
  }
231
246
  armIdle()
232
247
  try {
233
248
  for await (const chunk of response.body) {
249
+ gotFirstChunk = true
234
250
  armIdle()
235
251
  if (signal?.aborted) {
236
252
  throw abortError(signal, "provider", "transport-google")
@@ -14,6 +14,7 @@
14
14
  * 解析保持防御性(字段缺失即跳过该项)。候选不过滤非对话模型(embedding 等——§16.6 #14)。
15
15
  */
16
16
  import { proxyFetch } from "../proxy.mjs"
17
+ import { withUserAgent } from "../client-identity.mjs"
17
18
  import { resolveProviderSecrets } from "../env-ref.mjs"
18
19
  import { positiveInt, recordListedContext } from "../model-context.mjs"
19
20
 
@@ -74,7 +75,7 @@ function listedWindowOf(row) {
74
75
 
75
76
  async function listOpenai(provider, signal) {
76
77
  const url = `${provider.baseURL}/models`
77
- const data = await fetchJson(provider, url, { ...(provider.headers ?? {}), Authorization: `Bearer ${provider.apiKey ?? ""}` }, signal)
78
+ const data = await fetchJson(provider, url, { ...withUserAgent(provider.headers), Authorization: `Bearer ${provider.apiKey ?? ""}` }, signal)
78
79
  // L3 写面的收集段(PROVIDER.md §6.15):解析 `data[]` 时顺带收 `id → 窗口`——三字段按序取
79
80
  // 第一个**合法**值(R25 ∥ R26),非法相当于该字段缺席;三字段都不可得 = 该模型列表未报(不进表)。
80
81
  const ids = []
@@ -92,7 +93,7 @@ async function listOpenai(provider, signal) {
92
93
  }
93
94
 
94
95
  async function listAnthropic(provider, signal) {
95
- const headers = { ...(provider.headers ?? {}), "x-api-key": provider.apiKey ?? "", "anthropic-version": ANTHROPIC_VERSION }
96
+ const headers = { ...withUserAgent(provider.headers), "x-api-key": provider.apiKey ?? "", "anthropic-version": ANTHROPIC_VERSION }
96
97
  const base = `${provider.baseURL}/models?limit=1000`
97
98
  const out = []
98
99
  let afterId = null
@@ -108,7 +109,7 @@ async function listAnthropic(provider, signal) {
108
109
  }
109
110
 
110
111
  async function listGoogle(provider, signal) {
111
- const headers = { ...(provider.headers ?? {}) }
112
+ const headers = { ...withUserAgent(provider.headers) }
112
113
  const base = `${provider.baseURL}/models?key=${encodeURIComponent(provider.apiKey ?? "")}&pageSize=1000`
113
114
  const out = []
114
115
  let pageToken = null
package/provider/rate.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /**
1
+ /**
2
2
  * provider/rate.mjs — TPM/RPM proactive throttling gate
3
3
  * Sliding-window accounting; pre-check budget before sending requests; sleep until window frees space when over budget.
4
4
  */
@@ -42,8 +42,16 @@ function rateKey(provider) {
42
42
  return `${base}|${provider.apiKey ?? ""}`
43
43
  }
44
44
 
45
+ /** 估算口径(CONTEXT-COMPACTION.md §6.17.1 链一,**单源**):ASCII `ASCII_CHARS_PER_TOKEN` 字符/token;
46
+ * 非 ASCII(CJK/emoji)1 字/token(保守方向,不动)。取 3 的依据 = 失败现场服务端报 723,272 in messages
47
+ * ÷ 同刻状态栏 491k ≈ 2.7 字符/token ⇒ 旧值 `/4` 低报约 1.47 倍(余下由链二按模型自校准吸收)。 */
48
+ export const ASCII_CHARS_PER_TOKEN = 3
49
+ /** 估算口径版本位(§6.17.1 链二):落槽的校准样本只在同版本内复用——改公式即自动弃旧样本。 */
50
+ export const ESTIMATOR_VERSION = 1
51
+
45
52
  /** Rough estimate of text token count.
46
- * ASCII ~4 chars/token; non-ASCII (CJK/emoji) ~1 char/token (conservative; measured BPE is 1.5-2.5 chars/token).
53
+ * ASCII ~3 chars/token (`ASCII_CHARS_PER_TOKEN`); non-ASCII (CJK/emoji) ~1 char/token
54
+ * (conservative; measured BPE is 1.5-2.5 chars/token).
47
55
  * §2.5 #114 并入(token 估算取并集):数组内容按 part 分派——text 走单串估算,
48
56
  * image_url part 计 85 tokens(VSC 侧口径)。 */
49
57
  export function estimateText(s) {
@@ -62,7 +70,7 @@ export function estimateText(s) {
62
70
  function _estimateSingle(s) {
63
71
  let nonAscii = 0
64
72
  for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) nonAscii++
65
- return Math.ceil((s.length - nonAscii) / 4) + nonAscii
73
+ return Math.ceil((s.length - nonAscii) / ASCII_CHARS_PER_TOKEN) + nonAscii
66
74
  }
67
75
 
68
76
  /** Estimated prompt tokens for this request */
@@ -13,10 +13,11 @@
13
13
  */
14
14
  import { effectiveFetchTimeoutMs, effectiveBodyIdleMs } from "./core.mjs"
15
15
  import { proxyFetch } from "../proxy.mjs"
16
+ import { withUserAgent } from "../client-identity.mjs"
16
17
  import { requestWithRetry } from "./retry.mjs"
17
18
  import { rateGate, recordRate, estimateRequestTokens } from "./rate.mjs"
18
19
  import { buildBody, normalizeUsage } from "./responses-request.mjs"
19
- import { classifyResponsesErrorCode } from "./errors.mjs"
20
+ import { classifyResponsesErrorCode, assertPromptFits } from "./errors.mjs"
20
21
  // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):请求构造面出档 `provider/responses-request.mjs`;
21
22
  // `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名既有导出经本档转口——消费者 import 面零改。
22
23
  export { buildBody, isStoreRequiredHost, builtinToolsFor } from "./responses-request.mjs"
@@ -223,15 +224,18 @@ function handleEvent(ev, h) {
223
224
  }
224
225
 
225
226
  /** 主入口:请求 + 链状态推进(与 core.mjs 的 chat 同形返回)。 */
226
- export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, stateful }) {
227
+ export async function chat(provider, { messages, tools, onToken, onReasoning, onWait, signal, toolChoice, stateful, logCtx }) {
227
228
  const { body: reqBody, previousResponseId, warnings, newChain } = buildBody(provider, messages, tools, { toolChoice, stateful })
228
229
  const body = { ...reqBody, ...(previousResponseId ? { previous_response_id: previousResponseId } : {}) }
229
230
 
230
231
  // rateGate/recordRate 对齐 core(responses body 无 messages 键——按本地全量 messages 估算)
231
- const estimated = estimateRequestTokens({ messages })
232
+ // §6.17 发送面净判:估准面补 `tools`(原只投影 messages);body 用 `max_output_tokens`、无 `max_tokens` 键 ⇒ 减项恒 0
233
+ const estimated = estimateRequestTokens({ messages, tools })
234
+ const estPrompt = estimated - (body.max_tokens ?? 0)
235
+ assertPromptFits(provider, estPrompt, logCtx)
232
236
  await rateGate(provider, estimated, onWait, signal)
233
- // 定制头展开(PROVIDER.md §21):provider.headers 在前、内置头在后——同名内置头胜出(三处 fetch 共用)
234
- const headers = { ...(provider.headers ?? {}), "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` }
237
+ // 定制头 + 产品标识头(UA 不可被渠道覆盖——PROVIDER.md §6.17):定制头在前、内置头在后——同名内置头胜出(三处 fetch 共用)
238
+ const headers = { ...withUserAgent(provider.headers), "Content-Type": "application/json", Authorization: `Bearer ${provider.apiKey}` }
235
239
 
236
240
  // round2 复验 #1(2026-08-31):retry 层对 4xx 非可重试是 throw 而非返回——
237
241
  // requestWithRetry 从不返回 400/404 响应 → 下方 isChainInvalidError 分支原来不可达(D6 死代码)。
@@ -287,11 +291,17 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
287
291
  { signal, onWait, buildMessage: (status, text) => `Responses API error ${status}: ${text}` },
288
292
  )
289
293
  }
290
- return finish(provider, response, { onToken, onReasoning, signal, newChain, warnings, estimated })
294
+ return finish(provider, response, {
295
+ onToken, onReasoning, signal, newChain, warnings, estimated,
296
+ // §6.17 自校准口径:链式(`previous_response_id`)请求的 body 只带增量(`responses-request.mjs`
297
+ // `body.input = newOutputs`),而分母是本地全量估算 ⇒ 两者口径不同,本轮不采样。
298
+ estPrompt: previousResponseId ? undefined : estPrompt,
299
+ })
291
300
  }
292
301
 
293
- async function finish(provider, response, { onToken, onReasoning, signal, newChain, warnings, estimated }) {
302
+ async function finish(provider, response, { onToken, onReasoning, signal, newChain, warnings, estimated, estPrompt }) {
294
303
  const result = await parseStream(response, { onToken, onReasoning, signal })
304
+ result.estimated = estPrompt // §6.17 自校准采样(den 与净判同口径)
295
305
  recordRate(provider, estimated, result.usage)
296
306
  // 链状态推进:completed 事件里 response.id 供同一 turn 后续增量;
297
307
  // 截断/失败/无 id(部分端点不回传)→ 链作废(后续全量,正确性优先)。
package/provider/sse.mjs CHANGED
@@ -90,7 +90,7 @@ function finalizeToolCalls(result) {
90
90
  }
91
91
  }
92
92
 
93
- export async function readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns: sharedFired, idleMs = DEFAULT_BODY_IDLE_MS }) {
93
+ export async function readSSE(response, { onToken, onReasoning, rules, signal, firedPatterns: sharedFired, idleMs = DEFAULT_BODY_IDLE_MS, firstChunkMs }) {
94
94
  // Early intercept: non-SSE responses — either error bodies (HTTP >= 400) or
95
95
  // valid single-chunk JSON completions (some APIs return JSON despite stream:true).
96
96
  const contentType = response.headers.get("content-type") || ""
@@ -198,23 +198,34 @@ export async function readSSE(response, { onToken, onReasoning, rules, signal, f
198
198
  // 2026-09-01 读侧 idle 超时(根因修复):原实现靠 fetch 层的 600s 绝对墙钟兜底——长上下文子代理
199
199
  // 单次生成(或上游排队)超 10 分钟即被腰斩("The operation was aborted due to timeout" 直透)。
200
200
  // 现改为:读侧空闲超时——body 只要有数据流动就永不超时,连续 `idleMs` 无新 chunk 才判死
201
- // (`idleMs` 由 provider/core.mjs 的 effectiveBodyIdleMs 算好传入,缺省 DEFAULT_BODY_IDLE_MS)。
201
+ // (首 chunk 之前改用首 chunk 窗,见下;`idleMs` 由 provider/core.mjs 的 effectiveBodyIdleMs 算好传入)。
202
202
  // 直连 fetch 与 proxyFetch 统一走这里(proxy 的 _bodyIdleMs 语义与之等价,双保险)。
203
203
  let idleTimer = null
204
204
  let idleError = null
205
+ // 首 chunk 两态臂(TIMEOUT-DIGEST.md §2.4):首次挂用 `firstChunkMs`,收到第一个 body 字节后
206
+ // 每次重挂改用 `idleMs`——翻转不可逆(流中途不会退回「等首 chunk」态)。
207
+ let gotFirstChunk = false
205
208
  const armIdle = () => {
206
209
  if (idleTimer) clearTimeout(idleTimer)
210
+ // 窗与文案同源(`N` = 该相位实际生效的窗值):首挂窗缺省取 `idleMs`(入参缺省 = 老行为),
211
+ // 相位文案按态取——真传了首 chunk 窗才是分窗使用者(未传者仍报既有 `sse-idle`,逐字零变)。
212
+ const firstArm = !gotFirstChunk
213
+ const ms = firstArm ? (firstChunkMs ?? idleMs) : idleMs
214
+ const firstChunkPhase = firstArm && firstChunkMs != null
207
215
  idleTimer = setTimeout(() => {
208
216
  // #878:断流单点分流——直连 fetch(web 流)走 `IDLE_ABORT` 内部 abort 通道,
209
217
  // proxy 形回落 destroyBody(原契约)。
210
- idleError = timeoutError(`SSE idle timeout: no data for ${idleMs / 1000}s`, "provider", "sse-idle")
218
+ idleError = firstChunkPhase
219
+ ? timeoutError(`SSE first-chunk timeout: no data for ${ms / 1000}s`, "provider", "sse-first-chunk")
220
+ : timeoutError(`SSE idle timeout: no data for ${ms / 1000}s`, "provider", "sse-idle")
211
221
  try { terminateBody(response, idleError) } catch { /* already gone */ }
212
- }, idleMs)
222
+ }, ms)
213
223
  idleTimer.unref?.()
214
224
  }
215
225
  armIdle()
216
226
  try {
217
227
  for await (const chunk of response.body) {
228
+ gotFirstChunk = true
218
229
  armIdle()
219
230
  if (signal?.aborted) {
220
231
  throw abortError(signal, "provider", "stream-read")
@@ -0,0 +1,21 @@
1
+ /**
2
+ * session-identity.mjs — 进程会话 id 单源(零 import 叶档)——PROVIDER.md §6.17「会话标识头」。
3
+ *
4
+ * 为什么单立一叶:请求装配面 `client-identity.mjs` 是零依赖叶子,而每个 transport 都 import 它;
5
+ * 会话 id 又要「每次请求现取」。若从 `session-slots.mjs` 取,会把它的依赖链(config 读盘 ∥ 会话
6
+ * 存储 ∥ 进程探测共 8 档)连带拉进每个 transport 的 import 图。故本体住本档,`session-slots.mjs`
7
+ * 反向 re-export——既有 import 面零改。
8
+ *
9
+ * 语义:进程级惰性生成、零 IO、进程内稳定;形 `{pid}-{ts}-{rand6}`。它区分的是「哪一次进程会话」,
10
+ * 不是「槽位里的哪一份对话」(按对话槽分桶属另批穿参改造)。
11
+ */
12
+
13
+ let currentSessionId = null
14
+
15
+ /** Generate unique session ID for this process */
16
+ export function getSessionId() {
17
+ if (!currentSessionId) {
18
+ currentSessionId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
19
+ }
20
+ return currentSessionId
21
+ }
@@ -36,8 +36,8 @@ import { loadSlotFile } from "./session.mjs"
36
36
  // 与 VSC `newSlot` / `loadSlotForWrite` 全新分支同一构造器;环 import 同上,函数体内运行时使用)。
37
37
  import { newSlotData } from "./session-slot-write.mjs"
38
38
  import { mergeAdjacentAssistantEchoes } from "./context.mjs"
39
- // 打开态读数公式(§6.24 判据句 2——与 CLI 状态行 / VSC 同式;零新公式,只组合)。
40
- import { historyPercent } from "./token-window.mjs"
39
+ // 打开态读数公式(§6.24 判据句 2——与 CLI 状态行 / VSC 同一实现点的投影;零新公式,只组合)。
40
+ import { historyPercent, restoreTokenSample, sampleRatio } from "./token-window.mjs"
41
41
  import { scheduleSessionGC } from "./session-gc.mjs"
42
42
  import { restoreEngTokens, tokenExpired } from "./token-ttl.mjs"
43
43
  // 槽事件(docs/core/design/SESSION-OBSERVABILITY.md §3.1):`slot:new` 走核日志单一通道。
@@ -210,10 +210,15 @@ export function applySession(agent, data, opts = {}) {
210
210
  agent.provider.reasoningEffort = data.effort
211
211
  }
212
212
  }
213
+ // §6.17.1 链二读面:估算校准样本随槽恢复——上行刚把 `provider` 按槽值落定,key 同源校验才有意义
214
+ // (四条校验在 `restoreTokenSample`:形状 / est 版本 / key 同源 / 下界 1;不合格 ⇒ 未校准档)。
215
+ restoreTokenSample(agent, data.estimateSample, agent.provider)
213
216
  return { switched, droppedDesignTokens: restored.droppedDesignTokens }
214
217
  }
215
218
  // ② 槽无 key ∥ 不在册 → D-S3 保留——静默保持现状(落 config 链:config 统一解析产物已是
216
219
  // `agent.provider`;不报错不纠正——两方都无效由调用侧复验 validateProvider 弹重选)
220
+ // §6.17.1 链二读面(B 支出口同 A 支):样本照旧随槽恢复(provider = config 链产物,key 同源校验照做)
221
+ restoreTokenSample(agent, data.estimateSample, agent.provider)
217
222
  return { switched: false, droppedDesignTokens: restored.droppedDesignTokens }
218
223
  }
219
224
 
@@ -322,6 +327,9 @@ export function resetSessionState(agent) {
322
327
  agent._runStartHistoryLen = 0
323
328
  agent._lastPromptTokens = null
324
329
  agent._usageAtLen = null
330
+ // §6.17 估算自校准:新会话无样本(比 = 1)——跨会话不继承旧模型的估算偏差
331
+ agent._tok = null
332
+ agent._tokRatio = 1
325
333
  agent.planMode = false
326
334
  agent.goal = null
327
335
  agent._pendingReminders = []
@@ -398,7 +406,7 @@ export function slotOccupancy(cwd, slot) {
398
406
  * ⇒ 落链)∥ 无槽模型 ⇒ 现行模型链回落(`defaultModel` 属本渠道 ⇒ 其模型段;两档皆无 ⇒ `null`——
399
407
  * `resolveChannelModel`,2026-10-09 清除批:渠道单值模型退场,零静默回落);未命中 ⇒ `fallback`
400
408
  * (支 ②「静默保持现状」的装配口径 = `loadConfig().provider`);
401
- * ④ 公式 = `historyPercent`(`thincoder-core/token-window.mjs`——与 CLI 状态行 / VSC 同式)。
409
+ * ④ 公式 = 核 `historyReading`(与 CLI ∥ VSC 同一实现点的投影);第三参 = 槽样本经 `sampleRatio` 取的校准比。
402
410
  * **零副作用**:`data` / `providers` / `fallback` / `defaultModel` 皆入参(不读盘、不写盘、不改任何进程态);
403
411
  * `data` 非对象 ⇒ `0`(与空历史同值——「非正 ⇒ 零节点」显示门归端侧,沿 `historyPercent` 空历史口径)。 */
404
412
  export function sessionReading(data, { providers, fallback, defaultModel } = {}) {
@@ -414,5 +422,5 @@ export function sessionReading(data, { providers, fallback, defaultModel } = {})
414
422
  const provider = entry
415
423
  ? { ...entry, model: slotModel ?? resolveChannelModel(entry, defaultModel ?? null) }
416
424
  : fallback
417
- return historyPercent(line, provider)
425
+ return historyPercent(line, provider, sampleRatio(data.estimateSample, provider))
418
426
  }
package/session-slots.mjs CHANGED
@@ -6,10 +6,10 @@
6
6
  * init-block 批 · F-MI7 二次拆分:探测束接线后本档实读 517 行(破 490 判定线并越 500 硬限)
7
7
  * ⇒ 按 `CORE-UNIFICATION.md` §2.8.1 子表原行 3(已随 2026-10-08 批清账)**随批**外提清单 / 认领 / 属主面至
8
8
  * `session-slots-manifest.mjs`(语义原样迁移;本档 re-export 该档导出保既有 import 面——
9
- * 消费档 import 路径与名面不动)。本档保留 = 存储原语(cwd 哈希 / 路径 / 原子写 / 进程
10
- * sessionId)· 端记录(end marker)· 列表面(listSlots——条目集 = 盘面实读;扫描面住
11
- * `session-slot-scan.mjs`)· 删槽 · 恢复决策面
12
- * (usableSlot / loadLegacyFile / resumeSlot)。
9
+ * 消费档 import 路径与名面不动)。本档保留 = 存储原语(cwd 哈希 / 路径 / 原子写)· 端记录
10
+ * (end marker)· 列表面(listSlots——条目集 = 盘面实读;扫描面住 `session-slot-scan.mjs`)·
11
+ * 删槽 · 恢复决策面(usableSlot / loadLegacyFile / resumeSlot)。进程 sessionId 的本体已迁
12
+ * 零 import 叶 `session-identity.mjs`(PROVIDER.md §6.17),本档 re-export 保名面。
13
13
  *
14
14
  * 端名缝(SESSION.md §6.20 D-SE50 · 2026-09-25 SLOT-END-PARAM 批):端名 = **进程内的单值
15
15
  * 声明**(`setSessionEnd` / `sessionEnd`;`END` 仅模块级初值——CLI 零声明即得「cli」)。
@@ -53,20 +53,15 @@ import {
53
53
  import { scanSlotMetas } from "./session-slot-scan.mjs"
54
54
  // 读面懒核实面(§6.25 判据句 3 落点 B——不可信清单调度:纯内存登记 + 启动窗外延迟拍);静态环同形。
55
55
  import { scheduleVerify } from "./session-slot-verify.mjs"
56
+ // 进程会话 id(PROVIDER.md §6.17 会话标识头):本体住零 import 叶 `session-identity.mjs`——本档的
57
+ // 依赖链会经请求装配面被拉进每个 transport 的 import 图,故迁出。本档 import 取用(usableSlot ∥
58
+ // resumeSlot 的属主判据)+ 下行 re-export 保既有 import 面零改(消费档路径与名面不动)。
59
+ import { getSessionId } from "./session-identity.mjs"
56
60
  export {
57
61
  slotDigest, loadManifest, saveManifest, ownerPid, ownerPids,
58
62
  allocateFresh, claimSlot, activeSlot, ledgerHealth,
59
63
  } from "./session-slots-manifest.mjs"
60
-
61
- let currentSessionId = null
62
-
63
- /** Generate unique session ID for this process */
64
- export function getSessionId() {
65
- if (!currentSessionId) {
66
- currentSessionId = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
67
- }
68
- return currentSessionId
69
- }
64
+ export { getSessionId }
70
65
 
71
66
  /** Normalize cwd for hashing: uppercase Windows drive letter so both ends
72
67
  * (CLI's process.cwd() vs VS Code's uri.fsPath, which lowercases it) agree. */
package/session.mjs CHANGED
@@ -27,6 +27,8 @@ import { digestTs } from "./session-slots-manifest.mjs"
27
27
  // F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
28
28
  import { guardForeignSlotFile } from "./session-guard.mjs"
29
29
  import { engTokenSlotFields } from "./token-ttl.mjs"
30
+ // §6.17.1 链二写面(估算校准样本随会话槽走):本档只取写出值,读面在 `session-lifecycle.mjs`(applySession)。
31
+ import { tokenSampleOf } from "./token-window.mjs"
30
32
  // TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14):人读线记录存储(磁盘为准 + 内存窗口)。
31
33
  // slimForDisplay/isLegacyTransient 迁入 store 模块(§6.14 记录存储)——本文件 re-export 保持
32
34
  // 既有 import 面(session-slots.mjs 引 isLegacyTransient)。依赖方向单向:store 零项目内依赖。
@@ -143,6 +145,9 @@ export function saveSession(agent, opts = {}) {
143
145
  advisor: agent.config?.advisor ?? null,
144
146
  pendingReminders: agent._pendingReminders ?? [],
145
147
  sessionStart: agent._sessionStart ?? null,
148
+ // §6.17.1 链二写面:估算校准样本 `{key,num,den,est}`——空样本落 null(不造空对象);
149
+ // 只随回合尾保存走,不新增写路径、不逐样本落盘。
150
+ estimateSample: tokenSampleOf(agent),
146
151
  }
147
152
  // 2026-09-05 §6.10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、“查看对方活槽 →
148
153
  // 保存 fork 新槽”的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
package/skip-dirs.mjs ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * skip-dirs.mjs — 目录排除名单**零依赖叶档**(本批 E12:四份名单合一份)。
3
+ *
4
+ * 四份并存曾是漂移源:`index-discover.mjs:28`(8 项)· `tools/shared.mjs:27`(6 项,名 `IGNORED_DIRS`)·
5
+ * `tools/tree.mjs:13`(13 项)· `memory/schema.mjs:48-69`(39 项 + 折叠集 + 前缀族)。同一个问题
6
+ * (「哪些目录不是人要读的东西」)四个答案;索引面与检索面各剪各的,同一批文件在两个面上一进一出。
7
+ * 单源 = 本档,四个消费面只 import 名单,不各自持有副本。
8
+ *
9
+ * **名单只此一份**:`SKIP_DIRS`(42 项)答「索引 / 遍历 / 搜索时跳过什么」——剪掉的是噪音。
10
+ * 面判不读任何排除名单:面只有两值(文档面放行 ∥ 其余产品面拦),「什么算产物」不参与面判定。
11
+ *
12
+ * 三条处置(理由写在项旁,防后人「顺手补回去」):
13
+ * - `bin` **不列**:`thincoder-cli/bin/` 有 2 个 `.mjs` 是 CLI 入口、产品代码;列进名单会把入口
14
+ * 漏出索引与检索面。
15
+ * - `turbo` 并入 `.turbo`:`tools/tree.mjs` 旧名单里的 `turbo`(无点)是笔误,另三份都是 `.turbo`,
16
+ * 工作区递归查 `*turbo*` 零命中该目录。
17
+ * - `.thincoder` / `.vs` / `.idea` 本批补入:工具临时目录与 IDE 配置——进索引是噪音。
18
+ *
19
+ * 零依赖:本档只出常量,无 import——四处引它不成环、不加重量(`memory/schema.mjs` 引 `node:sqlite`、
20
+ * `conventions.mjs` 的依赖链被 `declaration.mjs` → `manifest.mjs` 拖着走,两条都不适合当单源落点)。
21
+ */
22
+
23
+ /** 索引 / 遍历 / 搜索的剪枝全集(42 项)。精确段匹配(`memory/file-walk.mjs` 另叠折叠与前缀口径)。 */
24
+ export const SKIP_DIRS = new Set([
25
+ // 产物 / 依赖(19)
26
+ "node_modules", ".git", "dist", "build", ".turbo", "coverage",
27
+ "__pycache__", ".venv", "venv", "target", ".next", ".nuxt", ".svelte-kit",
28
+ "vendor", "Pods", "bower_components", "third_party", "obj", "out",
29
+ // Windows 用户目录(14——永不含项目代码)
30
+ "AppData", "Application Data", "Desktop", "Documents", "Downloads",
31
+ "Music", "Pictures", "Videos", "OneDrive", "Contacts", "Favorites",
32
+ "Links", "Saved Games", "Searches",
33
+ // 其他非代码目录(6——Library = macOS 用户目录;go = Go workspace;三项 Windows 系统目录)
34
+ "Library", "go", "Program Files", "Program Files (x86)", "Windows", "$Recycle.Bin",
35
+ // 本批补(3——工具临时目录 / IDE 配置;见头注)
36
+ ".thincoder", ".vs", ".idea",
37
+ ])
38
+
39
+ /** win32 大小写折叠子集(18 项——那台文件系统不区分大小写,`Node_Modules` 仍要剪)。
40
+ * `.git` 不在:它由 `memory/file-walk.mjs` 的点段规则覆盖,不属本族。 */
41
+ export const SKIP_DIRS_FOLD = new Set([
42
+ "node_modules", "dist", "build", ".turbo", "coverage",
43
+ "__pycache__", ".venv", "venv", "target", ".next", ".nuxt", ".svelte-kit",
44
+ "vendor", "Pods", "bower_components", "third_party", "obj", "out",
45
+ ])
46
+
47
+ /** 目录名前缀族(`bazel-<workspace>` 无法枚举——workspace 名任意)。 */
48
+ export const SKIP_DIR_PREFIXES = ["bazel-"]
package/team-ledger.mjs CHANGED
@@ -26,8 +26,8 @@ const READ_ACTIONS = ["query", "count"]
26
26
  /** 六态(服务端 CHECK 同集——ledger/LEDGER.md §3)。 */
27
27
  const STATUSES = ["待讨论", "待设计", "在途", "待核销", "已核销", "已废弃"]
28
28
 
29
- /** 工具 cwd 供值面(缺省 → 当前项目根)。 */
30
- const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
29
+ /** 工具 cwd 供值面(零进程缺省:`ctx.agent.cwd` > `ctx.cwd`;两者皆无 ⇒ 当场失败,不落 `process.cwd()`)。 */
30
+ const cwdOf = (ctx) => ctx?.agent?.cwd ?? ctx?.cwd
31
31
 
32
32
  // ── 仓标记派生(§2.2) ───────────────────────────────────────────────────────
33
33
 
@@ -174,7 +174,7 @@ const ACTION_SPECS = {
174
174
  properties: {
175
175
  id: { type: "number", description: "条目编号(全局)" },
176
176
  title: { type: "string", description: "标题(缺省 = 不变)" },
177
- detail: { type: "string", description: "正文(缺省 = 不变;追认核销前须非空)" },
177
+ detail: { type: "string", description: "正文(缺省 = 不变)" },
178
178
  owner: { type: "string", description: "负责人(交接 = 换负责人;缺省 = 不变)" },
179
179
  status: { type: "string", enum: STATUSES, description: "目标状态(迁移表合法后继——非法由服务端 400)" },
180
180
  },
@@ -201,7 +201,7 @@ const UNION_PARAMETERS = {
201
201
  status: { type: "string", enum: STATUSES, description: "状态(query ∥ count 过滤——六态之一;update 目标态——迁移表合法后继;close ∈ {已核销, 已废弃})" },
202
202
  owner: { type: "string", description: "负责人(query ∥ count 过滤——用户名精确匹配;add ∥ update 设值)" },
203
203
  title: { type: "string", description: "标题(add 必填 ∥ update 可改——trim 非空 ∥ ≤200)" },
204
- detail: { type: "string", description: "正文(add ∥ update——可空 ∥ ≤4000;追认核销须非空)" },
204
+ detail: { type: "string", description: "正文(add ∥ update——可空 ∥ ≤4000)" },
205
205
  id: { type: "number", description: "条目编号(update ∥ close 必填——全局编号)" },
206
206
  },
207
207
  additionalProperties: false,
package/text-budget.mjs CHANGED
@@ -51,6 +51,10 @@ export function appendCappedText(prev, add, { hard, head = 0, tail = 0, marker =
51
51
  * straddling the boundary leaves a LONE high surrogate (U+D800-DBFF) that strict UTF-16
52
52
  * decoders reject. When the cut point lands on a high surrogate, step back one code unit
53
53
  * (the pair is dropped whole).
54
+ *
55
+ * 核内唯一定义(D2):`thincoder-core/agent/offload.mjs`(双端 preview 头切片)与
56
+ * `thincoder-core/agent/setup.mjs`(doc_search 预览)经 import 复用本函数——核内不再有私有复本。
57
+ * 入参先归一(`String(text ?? "")`):非字符串入参归一后按同一规则切片,不抛(既有导出契约)。
54
58
  */
55
59
  export function safeSliceUTF16(text, max) {
56
60
  const t = String(text ?? "")