@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.
- package/CHANGELOG.md +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
package/provider/errors.mjs
CHANGED
|
@@ -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
|
+
}
|
package/provider/google.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
226
|
-
idleError =
|
|
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
|
-
},
|
|
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")
|
package/provider/list-models.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 ~
|
|
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) /
|
|
73
|
+
return Math.ceil((s.length - nonAscii) / ASCII_CHARS_PER_TOKEN) + nonAscii
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/** Estimated prompt tokens for this request */
|
package/provider/responses.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
234
|
-
const headers = { ...(provider.headers
|
|
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, {
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
},
|
|
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
|
+
}
|
package/session-lifecycle.mjs
CHANGED
|
@@ -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
|
-
* ④ 公式 = `
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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 ?? "")
|