@thincoder/core 0.9.5 → 0.10.2
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 +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-slot-scan.mjs — 会话槽盘面扫描(SESSION.md §6.22 · SESSION-LIST-DISK 批 · 2026-09-28)。
|
|
3
|
+
*
|
|
4
|
+
* 条目集单源 = 盘面实读(判据句 1):sessions 根下 `^<hash>\.json\.(\d+)$` 的**普通文件**——
|
|
5
|
+
* 逐条出槽号 N;manifest `m.slots` 不参与条目集(既不筛也不补),只作判据句 2 取数链的
|
|
6
|
+
* **摘要缓存**(叠加源)。后缀闭集(`.manifest` / `.manifest.<端名>` / `.tmp` / `.corrupted` /
|
|
7
|
+
* `.unreadable` / `.bak-<ts>` / `.d` 目录 / `.stale-<ts>` / 遗留单档 `<hash>.json`)由
|
|
8
|
+
* 「数字槽号全锚 + 普通文件」两个判据一并排除——零伪槽号、零目录误收。
|
|
9
|
+
*
|
|
10
|
+
* 取数链(判据句 2 · 免费先行):① 摘要快路(摘要在场且 **`ts`(摘要落盘时刻)≥ 文件 mtime** ⇒
|
|
11
|
+
* 整条由摘要供给、**槽文件零字节读**——设计档 §6.22 判据句 2 快路比较口径**已收正为 `ts` 单源**;
|
|
12
|
+
* `updatedAt`(会话逻辑时,取于写槽文件之前)恒早于 mtime ⇒ 按之快路生产永不命中)② 小档全读
|
|
13
|
+
* (`size ≤ SCAN_FULL_MAX` = 256 KiB ⇒ JSON 全解析、全字段)③ 大档早键截读(读头 `min(size,
|
|
14
|
+
* SCAN_HEAD_BYTES)` = 64 KiB——**结构感知扫描**:键值边界由结构定界、小值解码走 `JSON.parse`
|
|
15
|
+
* 切片,零正则、零整档物化;遇顶层键 `history` 即停,其后的键不读)④ stat 兜底(`updatedAt` =
|
|
16
|
+
* mtime)。字段优先级 = **盘面实读值 > 摘要值(任意新鲜度)> 缺省**(`""` / `null` / mtime);单次
|
|
17
|
+
* 调用槽文件读 ≤ `SCAN_BUDGET_BYTES`(4 MiB)——预算按 mtime 降序耗用(最上面 = 用户最可能看
|
|
18
|
+
* 的行),越预算条目退 ④。计数两字段**不可得 = `null`**(真 0 与未知可分——§6.25 判据句 4)。
|
|
19
|
+
*
|
|
20
|
+
* 降级阶梯(判据句 3 · 入列不筛):坏 JSON / 半写 / `version > 2` / 异 cwd 内容档一律入列
|
|
21
|
+
* (存在性 ≠ 可读性)——内容面不可用 ⇒ 该档退摘要 ∪ 缺省、日期退 mtime;点开走既有失败面。
|
|
22
|
+
*
|
|
23
|
+
* 静态环(与 session-slots.mjs ↔ session-slots-manifest.mjs 同形):本档引 `sessionPath`
|
|
24
|
+
* (前缀 / 目录单源——先短哈希迁移、后枚举)与 `extractSlotMeta`(② 全读面摘要单源);两侧只在
|
|
25
|
+
* 函数体内运行时使用(模块求值期零顶层调用 ⇒ 环安全)。`SCAN_*` 三常量单源住本档。
|
|
26
|
+
*/
|
|
27
|
+
import { closeSync, openSync, readSync, readFileSync, readdirSync, statSync } from "node:fs"
|
|
28
|
+
import { basename, dirname, join } from "node:path"
|
|
29
|
+
// 环 import:见头注(前缀 = `sessionPath` 的 basename——与 `slotPath` 同源)。
|
|
30
|
+
import { sessionPath } from "./session-slots.mjs"
|
|
31
|
+
// ② 全读面摘要单源(计数 / 首条真实用户消息 / 字段缺省口径——与槽摘要写面同函数)。
|
|
32
|
+
import { extractSlotMeta } from "./session-slots-manifest.mjs"
|
|
33
|
+
// 「真实用户消息」谓词单源(人读线窗口 / 槽摘要 / ③ 早键截读三面同判据)。
|
|
34
|
+
import { isRealUserMsg } from "./history-window.mjs"
|
|
35
|
+
|
|
36
|
+
/** ② 小档全读上界(判据句 2)。 */
|
|
37
|
+
export const SCAN_FULL_MAX = 256 * 1024
|
|
38
|
+
/** ③ 大档早键截读窗口(判据句 2)。 */
|
|
39
|
+
export const SCAN_HEAD_BYTES = 64 * 1024
|
|
40
|
+
/** 单次扫描的槽文件读预算(判据句 2——按 mtime 降序耗用,越预算退 ④)。 */
|
|
41
|
+
export const SCAN_BUDGET_BYTES = 4 * 1024 * 1024
|
|
42
|
+
|
|
43
|
+
/** 观测计数(测试缝——`_storeStats` / `_releaseStats` 同惯例):读放大上界的断言语料。
|
|
44
|
+
* `bytes` = 本进程槽文件读累计字节;`reads` = 逐次读 `{ slot, bytes, window }`(window =
|
|
45
|
+
* `"full"` ② / `"head"` ③);`fastPath` = ① 命中次数。生产零消费。 */
|
|
46
|
+
export const _scanStats = { calls: 0, bytes: 0, fastPath: 0, reads: [] }
|
|
47
|
+
/** 测试缝复位(跨用例隔离——`_resetSessionsDirForTest` 同惯例)。 */
|
|
48
|
+
export function _resetScanStats() {
|
|
49
|
+
_scanStats.calls = 0; _scanStats.bytes = 0; _scanStats.fastPath = 0; _scanStats.reads.length = 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SLOT_SUFFIX = /^\d+$/
|
|
53
|
+
const HEAD_KEYS = new Set(["title", "activeProvider", "activeModel", "createdBy", "cwd", "updatedAt", "version"])
|
|
54
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v)
|
|
55
|
+
const isStr = (v) => typeof v === "string"
|
|
56
|
+
const isWs = (c) => c === " " || c === "\n" || c === "\r" || c === "\t"
|
|
57
|
+
|
|
58
|
+
/** JSON 切片解码(结构定界的**小值**:键 / 短串 / 数字);不可解析 ⇒ `undefined`。 */
|
|
59
|
+
function parseSlice(text, start, end) { try { return JSON.parse(text.slice(start, end)) } catch { return undefined } }
|
|
60
|
+
|
|
61
|
+
/** JSON 字符串跳过(不建值——大串零分配)。返回闭引号后一位置;窗截断 ⇒ `null`。 */
|
|
62
|
+
function skipJsonString(text, i) {
|
|
63
|
+
const n = text.length
|
|
64
|
+
let j = i + 1
|
|
65
|
+
while (j < n) {
|
|
66
|
+
const c = text[j]
|
|
67
|
+
if (c === "\\") { j += 2; continue }
|
|
68
|
+
if (c === '"') return j + 1
|
|
69
|
+
j++
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 结构化跳过任意 JSON 值(对象 / 数组按深度配对,字符串走 `skipJsonString`)。窗截断 ⇒ `null`。 */
|
|
75
|
+
function skipJsonValue(text, i) {
|
|
76
|
+
const n = text.length
|
|
77
|
+
const c = text[i]
|
|
78
|
+
if (c === '"') return skipJsonString(text, i)
|
|
79
|
+
if (c === "{" || c === "[") {
|
|
80
|
+
let depth = 0
|
|
81
|
+
let j = i
|
|
82
|
+
while (j < n) {
|
|
83
|
+
const ch = text[j]
|
|
84
|
+
if (ch === '"') {
|
|
85
|
+
const end = skipJsonString(text, j)
|
|
86
|
+
if (end === null) return null
|
|
87
|
+
j = end
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (ch === "{" || ch === "[") depth++
|
|
91
|
+
else if (ch === "}" || ch === "]") {
|
|
92
|
+
depth--
|
|
93
|
+
if (depth === 0) return j + 1
|
|
94
|
+
}
|
|
95
|
+
j++
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
let j = i
|
|
100
|
+
while (j < n && !isWs(text[j]) && text[j] !== "," && text[j] !== "}" && text[j] !== "]") j++
|
|
101
|
+
return j === i ? null : j
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** ③ 供给面:窗内首个真实用户消息(谓词单源 = history-window.mjs)。`i` = `history` 数组起点。
|
|
105
|
+
* 逐条结构定界 + `JSON.parse` 切片(谓词拿整条对象);窗截断 ⇒ 停(已得部分照给)。 */
|
|
106
|
+
function firstUserMessage(text, i) {
|
|
107
|
+
const n = text.length
|
|
108
|
+
if (text[i] !== "[") return null
|
|
109
|
+
let j = i + 1
|
|
110
|
+
while (j < n) {
|
|
111
|
+
const c = text[j]
|
|
112
|
+
if (isWs(c) || c === ",") { j++; continue }
|
|
113
|
+
if (c !== "{") return null // 数组尾 / 非对象条 ⇒ 不再走(保守)
|
|
114
|
+
const end = skipJsonValue(text, j)
|
|
115
|
+
if (end === null) return null
|
|
116
|
+
const m = parseSlice(text, j, end)
|
|
117
|
+
if (isRealUserMsg(m)) return m.content.slice(0, 80)
|
|
118
|
+
j = end
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** ③ 早键截读(判据句 2):自首字节走 JSON **顶层键**——供给面键取值、遇 `history` 置
|
|
124
|
+
* `historySeen` 并取首条真实用户消息后**即停**(其后的键不读)。窗口截断 / 结构不符 ⇒ 已得
|
|
125
|
+
* 部分照给(缺键由摘要补位);值跳过一律结构定界(非正则)。 */
|
|
126
|
+
function scanHeadKeys(text) {
|
|
127
|
+
const out = {}
|
|
128
|
+
const n = text.length
|
|
129
|
+
let i = 0
|
|
130
|
+
while (i < n && isWs(text[i])) i++
|
|
131
|
+
if (text[i] !== "{") return out
|
|
132
|
+
i++
|
|
133
|
+
while (i < n) {
|
|
134
|
+
while (i < n && isWs(text[i])) i++
|
|
135
|
+
const c = text[i]
|
|
136
|
+
if (c === "}" || c === undefined) break
|
|
137
|
+
if (c === ",") { i++; continue }
|
|
138
|
+
if (c !== '"') break
|
|
139
|
+
const keyEnd = skipJsonString(text, i)
|
|
140
|
+
if (keyEnd === null) break
|
|
141
|
+
const key = parseSlice(text, i, keyEnd)
|
|
142
|
+
if (!isStr(key)) break
|
|
143
|
+
i = keyEnd
|
|
144
|
+
while (i < n && isWs(text[i])) i++
|
|
145
|
+
if (text[i] !== ":") break
|
|
146
|
+
i++
|
|
147
|
+
while (i < n && isWs(text[i])) i++
|
|
148
|
+
if (key === "history") {
|
|
149
|
+
out.historySeen = text[i] === "[" // 数组起点 = ③ 面「history 数组」判据(② 面 = Array.isArray)
|
|
150
|
+
const first = firstUserMessage(text, i)
|
|
151
|
+
if (first !== null) out.firstMessage = first
|
|
152
|
+
break // 判据:遇 history 即停(其后的键不读——大数组不跳跃、不物化)
|
|
153
|
+
}
|
|
154
|
+
const valEnd = skipJsonValue(text, i)
|
|
155
|
+
if (valEnd === null) break // 窗截断 ⇒ 停(已得键照给)
|
|
156
|
+
if (HEAD_KEYS.has(key)) {
|
|
157
|
+
const v = parseSlice(text, i, valEnd)
|
|
158
|
+
if (isStr(v) || isNum(v)) out[key] = v
|
|
159
|
+
}
|
|
160
|
+
i = valEnd
|
|
161
|
+
}
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 内容面可用性(判据句 3 降级阶梯):`history` **数组**已见(② = `Array.isArray` · ③ = 值以 `[`
|
|
166
|
+
* 起)∧ version ∈ {1,2} ∧ cwd 一致(缺键 = 本档)。坏 JSON / 半写 / `version > 2` / 异 cwd ⇒
|
|
167
|
+
* 内容面不可用(该档退摘要 ∪ 缺省、日期退 mtime)。 */
|
|
168
|
+
function contentUsable(version, fileCwd, hasHistory, cwd) {
|
|
169
|
+
if (!hasHistory) return false
|
|
170
|
+
if (version !== 1 && version !== 2) return false
|
|
171
|
+
return !(isStr(fileCwd) && fileCwd.toLowerCase() !== cwd.toLowerCase())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** ② 小档全读:JSON 全解析 ⇒ 全字段(② 供给面含计数两字段)。 */
|
|
175
|
+
function fullDisk(text, cwd) {
|
|
176
|
+
let data
|
|
177
|
+
try { data = JSON.parse(text) } catch { return null }
|
|
178
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return null
|
|
179
|
+
if (!contentUsable(data.version, data.cwd, Array.isArray(data.history), cwd)) return null
|
|
180
|
+
const meta = extractSlotMeta(data.history, data.activeProvider, data.updatedAt, isStr(data.title) ? data.title : "")
|
|
181
|
+
if (isNum(data.updatedAt)) meta.updatedAt = data.updatedAt
|
|
182
|
+
else delete meta.updatedAt // 缺键 ⇒ 退摘要 / mtime(缺省链;extractSlotMeta 的 now() 兜底在此不适用)
|
|
183
|
+
if (data.activeModel) meta.activeModel = data.activeModel
|
|
184
|
+
if (data.createdBy) meta.createdBy = data.createdBy
|
|
185
|
+
return meta
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** ③ 头窗供给面 → 盘面元数据(门不过 ⇒ `null`;`historySeen` 是内部标记,不入供给面)。 */
|
|
189
|
+
function headDisk(head, cwd) {
|
|
190
|
+
if (!contentUsable(head.version, head.cwd, head.historySeen === true, cwd)) return null
|
|
191
|
+
delete head.version
|
|
192
|
+
delete head.cwd
|
|
193
|
+
delete head.historySeen
|
|
194
|
+
return head
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 盘面枚举(判据句 1):普通文件 + 数字槽号全锚;目录不存在 / 不可读 ⇒ `[]`。
|
|
198
|
+
* `sessionPath` 先行 = 短哈希迁移随该调用先行(先迁移、后枚举)。 */
|
|
199
|
+
function enumerateSlots(cwd) {
|
|
200
|
+
const base = sessionPath(cwd)
|
|
201
|
+
const dir = dirname(base)
|
|
202
|
+
const prefix = `${basename(base)}.`
|
|
203
|
+
let entries
|
|
204
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return [] }
|
|
205
|
+
const found = []
|
|
206
|
+
for (const e of entries) {
|
|
207
|
+
if (!e.isFile() || !e.name.startsWith(prefix)) continue
|
|
208
|
+
const suffix = e.name.slice(prefix.length)
|
|
209
|
+
if (!SLOT_SUFFIX.test(suffix)) continue
|
|
210
|
+
const path = join(dir, e.name)
|
|
211
|
+
try {
|
|
212
|
+
const st = statSync(path)
|
|
213
|
+
found.push({ slot: Number(suffix), path, size: st.size, mtime: st.mtimeMs })
|
|
214
|
+
} catch { /* 枚举与 stat 之间被删 / 不可达 ⇒ 视同不在盘 */ }
|
|
215
|
+
}
|
|
216
|
+
return found
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** ③ 头窗读(只读 `min(size, SCAN_HEAD_BYTES)` 字节——大档绝不整档读)。 */
|
|
220
|
+
function readHeadText(path) {
|
|
221
|
+
const fd = openSync(path, "r")
|
|
222
|
+
try {
|
|
223
|
+
const buf = Buffer.allocUnsafe(SCAN_HEAD_BYTES)
|
|
224
|
+
const size = readSync(fd, buf, 0, SCAN_HEAD_BYTES, 0)
|
|
225
|
+
return buf.subarray(0, size).toString("utf8")
|
|
226
|
+
} finally {
|
|
227
|
+
try { closeSync(fd) } catch { /* 关闭失败不影响读结果 */ }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** 字段装配(判据句 2 优先级):盘面实读值 > 摘要值(任意新鲜度)> 缺省(`""` / `null` / mtime);计数两字段
|
|
232
|
+
* **不可得 = `null`**(非 `0`——真 0 与「不知道」可分;§6.25 判据句 4 / D-SE62)。`disk` = ②/③ 盘面供给面;
|
|
233
|
+
* `digest` = manifest 摘要;`dateFromStat` = 日期取 stat mtime(判据句 2 ④ 与判据句 3 降级档)。 */
|
|
234
|
+
function mergeFields(disk, digest, mtime, tsBase, dateFromStat) {
|
|
235
|
+
const d = disk ?? {}
|
|
236
|
+
const g = digest ?? {}
|
|
237
|
+
const num = (a, b, fallback) => isNum(a) ? a : (isNum(b) ? b : fallback)
|
|
238
|
+
const str = (a, b, fallback) => isStr(a) ? a : (isStr(b) ? b : fallback)
|
|
239
|
+
const meta = {
|
|
240
|
+
ts: tsBase,
|
|
241
|
+
messageCount: num(d.messageCount, g.messageCount, null),
|
|
242
|
+
turnCount: num(d.turnCount, g.turnCount, null),
|
|
243
|
+
firstMessage: str(d.firstMessage, g.firstMessage, ""),
|
|
244
|
+
activeProvider: str(d.activeProvider, g.activeProvider, ""),
|
|
245
|
+
updatedAt: dateFromStat ? mtime : num(d.updatedAt, g.updatedAt, mtime),
|
|
246
|
+
title: str(d.title, g.title, ""),
|
|
247
|
+
createdBy: str(d.createdBy, g.createdBy, ""),
|
|
248
|
+
}
|
|
249
|
+
const model = str(d.activeModel, g.activeModel, null)
|
|
250
|
+
if (model !== null) meta.activeModel = model
|
|
251
|
+
return meta
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** 行时间标基准:摘要 `ts` > 数字形旧条目值 > mtime(判据句 2 缺省链)。 */
|
|
255
|
+
function tsBaseOf(entry, mtime) {
|
|
256
|
+
if (isNum(entry?.ts)) return entry.ts
|
|
257
|
+
if (isNum(entry)) return entry
|
|
258
|
+
return mtime
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** 单档取数(判据句 2 四级的装配点):返回 `{ meta, spent }`——`spent` = 本次占用的预算字节。 */
|
|
262
|
+
function readSlot(f, entry, budget, cwd) {
|
|
263
|
+
const mtime = f.mtime
|
|
264
|
+
const digest = entry && typeof entry === "object" ? entry : null
|
|
265
|
+
const tsBase = tsBaseOf(entry, mtime)
|
|
266
|
+
// ① 摘要快路:摘要在场且 `ts`(摘要落盘时刻)≥ 文件 mtime ⇒ 整条由摘要供给、槽文件零字节读
|
|
267
|
+
if (digest && isNum(digest.ts) && digest.ts >= mtime) {
|
|
268
|
+
_scanStats.fastPath++
|
|
269
|
+
return { meta: mergeFields(null, digest, mtime, tsBase, false), spent: 0 }
|
|
270
|
+
}
|
|
271
|
+
// ②/③:预算内读盘;越预算 / 读失败 / 内容不可用 ⇒ 盘面缺位(日期退 mtime——判据句 2 ④ / 判据句 3)
|
|
272
|
+
const full = f.size <= SCAN_FULL_MAX
|
|
273
|
+
const cost = full ? f.size : SCAN_HEAD_BYTES
|
|
274
|
+
if (cost > budget) return { meta: mergeFields(null, digest, mtime, tsBase, true), spent: 0 }
|
|
275
|
+
let disk = null
|
|
276
|
+
try {
|
|
277
|
+
const text = full ? readFileSync(f.path, "utf8") : readHeadText(f.path)
|
|
278
|
+
_scanStats.bytes += cost
|
|
279
|
+
_scanStats.reads.push({ slot: f.slot, bytes: cost, window: full ? "full" : "head" })
|
|
280
|
+
disk = full ? fullDisk(text, cwd) : headDisk(scanHeadKeys(text), cwd)
|
|
281
|
+
} catch { /* 并发删除 / EPERM ⇒ 盘面缺位 */ }
|
|
282
|
+
return { meta: mergeFields(disk, digest, mtime, tsBase, disk === null), spent: cost }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 扫描本 cwd 的槽文件并装配每档元数据(判据句 1 + 2):返回 `[{ slot, meta, mtime }]`(序 = 枚举序——
|
|
286
|
+
* 行序归调用面投影;`mtime` = 文件 mtime,判定链 / 核实面新鲜度判据入参)。`digests` = manifest `m.slots`。 */
|
|
287
|
+
export function scanSlotMetas(cwd, digests = {}) {
|
|
288
|
+
_scanStats.calls++
|
|
289
|
+
const found = enumerateSlots(cwd)
|
|
290
|
+
found.sort((a, b) => b.mtime - a.mtime) // 预算耗用序 = mtime 降序
|
|
291
|
+
let budget = SCAN_BUDGET_BYTES
|
|
292
|
+
const out = []
|
|
293
|
+
for (const f of found) {
|
|
294
|
+
const { meta, spent } = readSlot(f, digests[f.slot], budget, cwd)
|
|
295
|
+
budget -= spent
|
|
296
|
+
out.push({ slot: f.slot, meta, mtime: f.mtime })
|
|
297
|
+
}
|
|
298
|
+
return out
|
|
299
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-slot-verify.mjs — 读面懒核实面(SESSION.md §6.25 判据句 3 · LEDGER-RELIABILITY 批 · 2026-09-28)。
|
|
3
|
+
* 触发 = 读面交来的**不可信档清单**(谓词单源 `needsVerify`)。**调度 = 纯内存登记 + 启动窗外延迟拍**(`VERIFY_TICK_DELAY_MS` = 3s,`setTimeout` + `unref`——承 D-SE39:`setImmediate` 点火与启动链同循环)
|
|
4
|
+
* ⇒ 零 I/O / 零同步阻塞(首答 F-SL1 50 ms 不被拖慢)。核实 = 单档整档**分块流式**(计数只能全读获得——
|
|
5
|
+
* D-SE56):1 MiB 分块 + 逐块宏任务让出(零 ≥50 ms 连续同步段);**不整档物化**——走顶层键 + 逐元素结构
|
|
6
|
+
* 定界计数 / 切片解析(`jump` 每字符只扫一次;跨块续读、零正则)。预算 = 每轮 ≤ `SCAN_BUDGET_BYTES`(4 MiB
|
|
7
|
+
* ——单源住 `session-slot-scan.mjs`)∨ **超预算大档该轮至多一档**;未覆盖者留待下次列表调用;**不做全量扫**
|
|
8
|
+
* (D-SE67)。不重复三条:① 核完即回写摘要(`ts` 地板 = 该档 mtime——平台时间戳可领先 `Date.now()`,见回写行注释)⇒ 回快路;② **单飞**;③ **负缓存**(同 mtime 不重试)。
|
|
9
|
+
* 回写 = 尾部**一次合并写**(仅摘要条目);校验门不过(坏 JSON / 异 cwd / `version > 2`)⇒ **不写回**。
|
|
10
|
+
* 缝:`_verifyState` · `_verifyIdle()`(**禁时间等待**)· `_setVerifyDelayForTest` · `_resetVerifyStateForTest`。
|
|
11
|
+
* 在飞轮去向:目录切换 ⇒ **弃轮**(零跨根写);进程退出 ⇒ 拍 `unref` 不持活、在飞轮随终止丢弃;静态环:核内四引求值期零顶层调用 ⇒ 环安全。
|
|
12
|
+
*/
|
|
13
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs"
|
|
14
|
+
import { SCAN_BUDGET_BYTES } from "./session-slot-scan.mjs" // 预算常量单源
|
|
15
|
+
import { sessionPath, slotPath } from "./session-slots.mjs" // 槽前缀 / 路径单源
|
|
16
|
+
import { loadManifest, saveManifest } from "./session-slots-manifest.mjs" // 清单面单源
|
|
17
|
+
import { isRealUserMsg } from "./history-window.mjs" // 「真实用户消息」谓词单源(turnCount / firstMessage)
|
|
18
|
+
/** 启动窗外延迟拍(D-SE64 · 承 D-SE39):自调度点起 3s ⇒ 核实起点落于启动窗外。 */
|
|
19
|
+
export const VERIFY_TICK_DELAY_MS = 3000
|
|
20
|
+
/** 分块粒度(判据句 3):1 MiB + 逐块让出。 */
|
|
21
|
+
export const VERIFY_CHUNK_BYTES = 1024 * 1024
|
|
22
|
+
const HEAD_KEYS = new Set(["title", "activeProvider", "activeModel", "createdBy", "updatedAt", "version", "cwd"])
|
|
23
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v)
|
|
24
|
+
const isStr = (v) => typeof v === "string"
|
|
25
|
+
const isWs = (c) => c === " " || c === "\n" || c === "\r" || c === "\t"
|
|
26
|
+
const isDelim = (c) => c === undefined || isWs(c) || c === "," || c === "}" || c === "]"
|
|
27
|
+
const isBlank = (s) => { for (let i = 0; i < s.length; i++) if (!isWs(s[i])) return false; return true } // 尾随只许空白
|
|
28
|
+
const yieldToLoop = () => new Promise((resolve) => setImmediate(resolve)) // 逐块让出(唯一 setImmediate——非点火)
|
|
29
|
+
/** 观测计数(`_scanStats` 同惯例——生产零消费):`ticks` / `verified` 成功回写档数 / `failed` 门不过档数 /
|
|
30
|
+
* `skipped` 跳过档数(不在盘 / 负缓存 / 余量不足)/ `chunks` 分块读次数 / `bytes` 分块读字节。 */
|
|
31
|
+
export const _verifyState = { ticks: 0, verified: 0, failed: 0, skipped: 0, chunks: 0, bytes: 0 }
|
|
32
|
+
let verifyDelayMs = VERIFY_TICK_DELAY_MS
|
|
33
|
+
/** 延迟拍缝(置 0 点火勿真等;还原 = 置回 `VERIFY_TICK_DELAY_MS`)。 */
|
|
34
|
+
export function _setVerifyDelayForTest(ms) { verifyDelayMs = ms }
|
|
35
|
+
/** 不可信谓词(判据句 3 · **单源**):缺失 ∨ `ts < mtime`(陈旧)∨ 计数两字段任一非数(不可得);真 0
|
|
36
|
+
* 不触发(数值 0 = 可信);`mtime` 非数(stat 失败)⇒ 不妄断陈旧。 */
|
|
37
|
+
export function needsVerify(entry, mtime) {
|
|
38
|
+
if (!entry || typeof entry !== "object") return true
|
|
39
|
+
if (!isNum(entry.messageCount) || !isNum(entry.turnCount)) return true
|
|
40
|
+
if (!isNum(entry.ts)) return true
|
|
41
|
+
return isNum(mtime) ? entry.ts < mtime : false
|
|
42
|
+
}
|
|
43
|
+
const pending = new Map() // cwd → Set(slot):待核(拍在途——纯内存登记)
|
|
44
|
+
const timers = new Map() // cwd → Timeout:拍句柄(去重 / 复位)
|
|
45
|
+
const running = new Set() // cwd:在飞轮(单飞)
|
|
46
|
+
const failedCache = new Map() // cwd → Map(slot → mtime):负缓存
|
|
47
|
+
|
|
48
|
+
/** 读面交付点(`listSlots` 同步返回前调用——纯内存登记 + 延迟拍:零 I/O / 零同步阻塞):`scanned` = 扫描行
|
|
49
|
+
* `[{ slot, meta, mtime }]`;不可信档按 **mtime 降序**入队(用户最可能看的行先核);同档不重复入队;
|
|
50
|
+
* 已有拍 ⇒ 并入待核集(不重复点火)。 */
|
|
51
|
+
export function scheduleVerify(cwd, scanned, digests = {}) {
|
|
52
|
+
const targets = scanned.filter((s) => needsVerify(digests?.[s.slot], s.mtime)).sort((a, b) => b.mtime - a.mtime)
|
|
53
|
+
if (targets.length === 0) return
|
|
54
|
+
let prefix
|
|
55
|
+
try { prefix = sessionPath(cwd) } catch { return } // 前缀不可解析 ⇒ 不排
|
|
56
|
+
const list = pending.get(cwd) ?? new Set()
|
|
57
|
+
for (const s of targets) list.add(s.slot)
|
|
58
|
+
pending.set(cwd, list)
|
|
59
|
+
if (timers.has(cwd)) return
|
|
60
|
+
const t = setTimeout(() => { timers.delete(cwd); runPass(cwd, prefix).catch(() => { /* 静默:下轮重核 */ }) }, verifyDelayMs)
|
|
61
|
+
t.unref?.() // 不持活(进程退出随终止)
|
|
62
|
+
timers.set(cwd, t)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 一轮核实(拍点火):捕获前缀校验(目录切换 ⇒ 弃轮——零跨根写)→ 逐档(mtime 降序)分块流式核实 →
|
|
66
|
+
* 尾部一次合并写(仅摘要条目)。 */
|
|
67
|
+
async function runPass(cwd, prefix) {
|
|
68
|
+
const slots = pending.get(cwd)
|
|
69
|
+
pending.delete(cwd)
|
|
70
|
+
if (!slots || running.has(cwd)) return // 单飞:在飞即返(余档留待下次列表调用)
|
|
71
|
+
if (sessionPath(cwd) !== prefix) return
|
|
72
|
+
running.add(cwd)
|
|
73
|
+
_verifyState.ticks += 1
|
|
74
|
+
try {
|
|
75
|
+
const results = new Map() // slot → { meta, mtime }(meta 不含 ts——ts 写回时打点)
|
|
76
|
+
let budget = SCAN_BUDGET_BYTES
|
|
77
|
+
let oversizeTaken = false
|
|
78
|
+
for (const slot of slots) {
|
|
79
|
+
let st
|
|
80
|
+
try { st = statSync(slotPath(cwd, slot)) } catch { _verifyState.skipped += 1; continue } // 不在盘 ⇒ 跳
|
|
81
|
+
if (failedCache.get(cwd)?.get(slot) === st.mtimeMs) { _verifyState.skipped += 1; continue } // 负缓存
|
|
82
|
+
if (st.size > SCAN_BUDGET_BYTES) {
|
|
83
|
+
if (oversizeTaken) continue // 超预算大档该轮至多一档
|
|
84
|
+
oversizeTaken = true
|
|
85
|
+
} else if (st.size > budget) continue // 余量不足 ⇒ 留待下次列表调用
|
|
86
|
+
const meta = await scanOne(slotPath(cwd, slot), st.size, cwd)
|
|
87
|
+
budget = Math.max(0, budget - st.size)
|
|
88
|
+
if (meta) { results.set(slot, { meta, mtime: st.mtimeMs }); continue }
|
|
89
|
+
_verifyState.failed += 1 // 门不过 ⇒ 负缓存(不发明摘要)
|
|
90
|
+
const neg = failedCache.get(cwd) ?? new Map()
|
|
91
|
+
neg.set(slot, st.mtimeMs)
|
|
92
|
+
failedCache.set(cwd, neg)
|
|
93
|
+
}
|
|
94
|
+
// 回写:尾部一次合并写——仅摘要条目;写回前再校验捕获前缀(零跨根写)
|
|
95
|
+
if (results.size > 0 && sessionPath(cwd) === prefix) {
|
|
96
|
+
const m = loadManifest(cwd)
|
|
97
|
+
m.slots ??= {}
|
|
98
|
+
// `ts` 地板 = 该档 mtime(平台文件时间戳可领先 `Date.now()` 数 ms——延迟盖章):无地板则刚核完的档被判「陈旧」再读;判据比较(`ts ≥ mtime`)零改。
|
|
99
|
+
for (const [slot, r] of results) m.slots[slot] = { ts: Math.max(Date.now(), r.mtime), ...r.meta }
|
|
100
|
+
if (saveManifest(cwd, m) !== false) _verifyState.verified += results.size // 拒写(边界 ⑩)不计
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
running.delete(cwd)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 可等待句柄(判据句 3 缝):「无 pending 拍 ∧ 无在飞轮」即决。 */
|
|
108
|
+
export async function _verifyIdle() {
|
|
109
|
+
while (pending.size > 0 || running.size > 0) await yieldToLoop()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 复位(用例跨例隔离):清 pending 拍 + 在飞登记 + 负缓存 + 观测计数。 */
|
|
113
|
+
export function _resetVerifyStateForTest() {
|
|
114
|
+
for (const t of timers.values()) clearTimeout(t)
|
|
115
|
+
timers.clear(); pending.clear(); running.clear(); failedCache.clear()
|
|
116
|
+
for (const k of Object.keys(_verifyState)) _verifyState[k] = 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 单档分块流式核实:1 MiB 分块读 + 逐块让出;返回摘要字段(不含 `ts`)或 `null`(门不过)。 */
|
|
120
|
+
async function scanOne(path, size, cwd) {
|
|
121
|
+
const buf = Buffer.allocUnsafe(VERIFY_CHUNK_BYTES)
|
|
122
|
+
const dec = new TextDecoder("utf-8") // 流式解码:多字节字符跨块不劈
|
|
123
|
+
const scan = new StreamScan()
|
|
124
|
+
let fd
|
|
125
|
+
try { fd = openSync(path, "r") } catch { return null } // 打开失败(并发删除等)⇒ 门不过(负缓存)
|
|
126
|
+
try {
|
|
127
|
+
let pos = 0
|
|
128
|
+
while (pos < size) { // 读到文件末(`done` 后仍读——尾随非空白等于坏 JSON,见 `feed` / `result`)
|
|
129
|
+
const n = readSync(fd, buf, 0, Math.min(VERIFY_CHUNK_BYTES, size - pos), pos)
|
|
130
|
+
if (n <= 0) break // 截断(读不到声明长度)⇒ 门不过
|
|
131
|
+
pos += n
|
|
132
|
+
_verifyState.chunks += 1
|
|
133
|
+
_verifyState.bytes += n
|
|
134
|
+
scan.feed(dec.decode(buf.subarray(0, n), { stream: true }))
|
|
135
|
+
await yieldToLoop() // 逐块宏任务让出(零 ≥50 ms 连续同步段)
|
|
136
|
+
}
|
|
137
|
+
// EOF 尾段冲洗(#499):流式解码器在文件末滞留的**不完整 UTF-8 序列**(≤3 字节)不冲洗就
|
|
138
|
+
// 丢失——坏档尾段会逃过「尾随非空白 = 坏 JSON」门;无参 `decode()` 收尾 ⇒ 残缺字节出 U+FFFD
|
|
139
|
+
// ⇒ 按坏档门不过(完整多字节字符照常解码,零影响)。
|
|
140
|
+
scan.feed(dec.decode())
|
|
141
|
+
} catch {
|
|
142
|
+
return null // 读失败 ⇒ 门不过(负缓存)
|
|
143
|
+
} finally {
|
|
144
|
+
try { closeSync(fd) } catch { /* 关闭失败不影响读结果 */ }
|
|
145
|
+
}
|
|
146
|
+
return scan.result(cwd)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 跨块流式结构扫描器(判据句 3):顶层键取值 + `history` 逐元素定界计数;**消费段即时丢弃**(内存 =
|
|
150
|
+
* 未完成 token);零正则、零整档物化。**严格 JSON**(容器分隔态 `sep`;结构违规 ⇒ `corrupt()`)。
|
|
151
|
+
* st:key → value → after ∨ item(history 内);sep:start 首元素位 / need 刚吃 `,` / after 刚完一元素;
|
|
152
|
+
* k:跳跃态(1 串 / 2 结构 / 3 标量 / null 无)、p 跳跃位置(buf 内)、d 深度、inStr / esc 串与转义态
|
|
153
|
+
* (跨块保留——续扫不重扫);cursor = 已消费位置(在飞值时 = 值起点);open = 顶层 `{` 未取。 */
|
|
154
|
+
class StreamScan {
|
|
155
|
+
constructor() {
|
|
156
|
+
this.buf = ""; this.cursor = 0; this.st = "key"; this.open = true; this.sep = "start"; this.key = null
|
|
157
|
+
this.k = null; this.p = 0; this.d = 0; this.inStr = false; this.esc = false
|
|
158
|
+
this.count = 0; this.turns = 0; this.first = null; this.head = {}
|
|
159
|
+
this.historySeen = false; this.done = false; this.bad = false
|
|
160
|
+
}
|
|
161
|
+
/** 喂入一块文本(终结后只许空白——尾随非空白 = 坏 JSON,判据句 3 门面)。 */
|
|
162
|
+
feed(text) {
|
|
163
|
+
if (this.done) { if (!isBlank(text)) this.bad = true; return }
|
|
164
|
+
if (this.cursor > 0) {
|
|
165
|
+
this.buf = this.buf.slice(this.cursor)
|
|
166
|
+
if (this.k !== null) this.p -= this.cursor
|
|
167
|
+
this.cursor = 0
|
|
168
|
+
}
|
|
169
|
+
this.buf += text
|
|
170
|
+
while (this.done === false) {
|
|
171
|
+
const next = this.step(this.buf, this.cursor)
|
|
172
|
+
if (next === null) break // 请示更多输入(不完整 token——含坏档停滞)
|
|
173
|
+
this.cursor = next
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** 单步(返回新位置 / `null` = 请示更多输入);结构违规 ⇒ `corrupt()`(门不过)。 */
|
|
177
|
+
step(b, i) {
|
|
178
|
+
const n = b.length
|
|
179
|
+
while (i < n && isWs(b[i])) i++
|
|
180
|
+
if (this.st === "key") {
|
|
181
|
+
if (i >= n) return null
|
|
182
|
+
const c = b[i]
|
|
183
|
+
if (this.open) { // 顶层 `{`(首字符)
|
|
184
|
+
if (c !== "{") return this.corrupt()
|
|
185
|
+
this.open = false
|
|
186
|
+
return i + 1
|
|
187
|
+
}
|
|
188
|
+
if (c === "}") { if (this.sep === "need") return this.corrupt(); this.done = true; return i + 1 } // 尾逗号 ⇒ 违规
|
|
189
|
+
if (c !== '"') return this.corrupt() // 空键位 / 双逗号 / 非串键
|
|
190
|
+
const end = this.jump(b, i)
|
|
191
|
+
if (end === null) return null
|
|
192
|
+
try { this.key = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
|
|
193
|
+
if (!isStr(this.key)) return this.corrupt()
|
|
194
|
+
let k = end
|
|
195
|
+
while (k < n && isWs(b[k])) k++
|
|
196
|
+
if (k >= n) return null // 冒号未到(键重扫——键短,零成本)
|
|
197
|
+
if (b[k] !== ":") return this.corrupt()
|
|
198
|
+
this.st = "value"
|
|
199
|
+
return k + 1
|
|
200
|
+
}
|
|
201
|
+
if (this.st === "value") {
|
|
202
|
+
if (i >= n) return null
|
|
203
|
+
const c = b[i]
|
|
204
|
+
if (this.key === "history") { // history 非数组 ⇒ 内容面不可用(门不过)
|
|
205
|
+
if (c !== "[") return this.corrupt()
|
|
206
|
+
this.historySeen = true
|
|
207
|
+
this.sep = "start"
|
|
208
|
+
this.st = "item"
|
|
209
|
+
return i + 1
|
|
210
|
+
}
|
|
211
|
+
const end = this.jump(b, i)
|
|
212
|
+
if (end === null) return null
|
|
213
|
+
if (end <= i) return this.corrupt() // 空值(无进展)⇒ 结构违规(防死循环)
|
|
214
|
+
if (HEAD_KEYS.has(this.key) && c !== "{" && c !== "[") {
|
|
215
|
+
try { this.head[this.key] = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
|
|
216
|
+
}
|
|
217
|
+
this.st = "after"
|
|
218
|
+
return end
|
|
219
|
+
}
|
|
220
|
+
if (this.st === "after") { // 顶层:刚完一值 ⇒ 须是 `,` 或 `}`
|
|
221
|
+
if (i >= n) return null
|
|
222
|
+
if (b[i] === ",") { this.sep = "need"; this.st = "key"; return i + 1 }
|
|
223
|
+
if (b[i] === "}") { this.done = true; return i + 1 }
|
|
224
|
+
return this.corrupt()
|
|
225
|
+
}
|
|
226
|
+
// item:history 数组内(元素结构定界——逐元素切片解析)
|
|
227
|
+
if (i >= n) return null
|
|
228
|
+
const c = b[i]
|
|
229
|
+
if (c === "]") { if (this.sep === "need") return this.corrupt(); this.st = "after"; return i + 1 } // 尾逗号
|
|
230
|
+
if (c === ",") { if (this.sep !== "after") return this.corrupt(); this.sep = "need"; return i + 1 }
|
|
231
|
+
if (this.sep === "after") return this.corrupt() // 缺分隔符
|
|
232
|
+
const end = this.jump(b, i) // 值闭界(不完整 ⇒ 留值文本于 buf,续扫)
|
|
233
|
+
if (end === null) return null
|
|
234
|
+
if (end <= i) return this.corrupt() // 空值元素(无进展)⇒ 结构违规(防死循环)
|
|
235
|
+
this.sep = "after"
|
|
236
|
+
this.count += 1 // messageCount = history.length(逐元素计数——含非对象元素)
|
|
237
|
+
if (c === "{") {
|
|
238
|
+
let m
|
|
239
|
+
try { m = JSON.parse(b.slice(i, end)) } catch { return this.corrupt() }
|
|
240
|
+
if (isRealUserMsg(m)) {
|
|
241
|
+
this.turns += 1 // turnCount = 真实用户消息数(谓词单源 = history-window.mjs)
|
|
242
|
+
if (this.first === null) this.first = m.content.slice(0, 80)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return end
|
|
246
|
+
}
|
|
247
|
+
/** 跨块可续的值跳跃(「字符串 / 转义跨块续读」):自 `i` 跳到值末(回值后一位);输入不足 ⇒ `null`
|
|
248
|
+
* (**状态保留**——下次自 `this.p` 续扫,每字符只扫一次);标量**不吃分隔符**(切片 `[i, end)`)。 */
|
|
249
|
+
jump(b, i) {
|
|
250
|
+
if (this.k === null) {
|
|
251
|
+
const c = b[i]
|
|
252
|
+
if (c === undefined) return null
|
|
253
|
+
if (c === '"') { this.k = 1; this.p = i + 1; this.esc = false }
|
|
254
|
+
else if (c === "{" || c === "[") { this.k = 2; this.p = i + 1; this.d = 1; this.inStr = false; this.esc = false }
|
|
255
|
+
else { this.k = 3; this.p = i }
|
|
256
|
+
}
|
|
257
|
+
let j = this.p
|
|
258
|
+
if (this.k === 1) {
|
|
259
|
+
while (j < b.length) {
|
|
260
|
+
const c = b[j]
|
|
261
|
+
if (this.esc) { this.esc = false; j++; continue }
|
|
262
|
+
if (c === "\\") { this.esc = true; j++; continue }
|
|
263
|
+
if (c === '"') { this.k = null; return j + 1 }
|
|
264
|
+
j++
|
|
265
|
+
}
|
|
266
|
+
} else if (this.k === 2) {
|
|
267
|
+
while (j < b.length) {
|
|
268
|
+
const c = b[j]
|
|
269
|
+
if (this.inStr) {
|
|
270
|
+
if (this.esc) this.esc = false
|
|
271
|
+
else if (c === "\\") this.esc = true
|
|
272
|
+
else if (c === '"') this.inStr = false
|
|
273
|
+
} else if (c === '"') this.inStr = true
|
|
274
|
+
else if (c === "{" || c === "[") this.d += 1
|
|
275
|
+
else if ((c === "}" || c === "]") && --this.d === 0) { this.k = null; return j + 1 }
|
|
276
|
+
j++
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
while (j < b.length && !isDelim(b[j])) j++
|
|
280
|
+
if (j < b.length) { this.k = null; return j } // 标量末(不吃分隔符)
|
|
281
|
+
}
|
|
282
|
+
this.p = j // 输入不足 ⇒ 记位置续扫(触底 = 可能截断)
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
corrupt() { this.bad = true; this.done = true; return null }
|
|
286
|
+
/** 校验门 + 摘要字段:结构完整(顶层配平 ∧ 见 `history` 数组)∧ `version ∈ {1,2}` ∧ cwd 一致(缺键 =
|
|
287
|
+
* 本档)⇒ 字段(不含 `ts`);否则 `null`(不发明摘要)。字段缺省与 `slotDigest` / `extractSlotMeta`
|
|
288
|
+
* 逐条同口径(单源判据 = 用例强制逐字段相等)。 */
|
|
289
|
+
result(cwd) {
|
|
290
|
+
if (this.bad || !this.done || !this.historySeen) return null
|
|
291
|
+
if (!isBlank(this.buf.slice(this.cursor))) return null // 顶层配平后非空白残留(未过 `feed` 的那段)⇒ 门不过
|
|
292
|
+
if (this.head.version !== 1 && this.head.version !== 2) return null
|
|
293
|
+
const fc = this.head.cwd
|
|
294
|
+
if (isStr(fc) && fc.toLowerCase() !== cwd.toLowerCase()) return null
|
|
295
|
+
const meta = {
|
|
296
|
+
messageCount: this.count, turnCount: this.turns, firstMessage: this.first ?? "",
|
|
297
|
+
activeProvider: this.head.activeProvider ?? "", updatedAt: this.head.updatedAt ?? Date.now(),
|
|
298
|
+
title: this.head.title ?? "",
|
|
299
|
+
}
|
|
300
|
+
if (this.head.activeModel) meta.activeModel = this.head.activeModel
|
|
301
|
+
if (this.head.createdBy) meta.createdBy = this.head.createdBy
|
|
302
|
+
return meta
|
|
303
|
+
}
|
|
304
|
+
}
|