@thincoder/core 0.9.4 → 0.10.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 +63 -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 +14 -14
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +23 -13
- 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 +55 -294
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +63 -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 +19 -15
- 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 +90 -30
- 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 +11 -11
- package/agent-tools/advisor-settle.mjs +17 -17
- package/agent-tools/advisor.mjs +38 -43
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +23 -20
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +65 -34
- package/agent-tools/batch-paths.mjs +186 -0
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +22 -119
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +58 -58
- 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 +18 -13
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +3 -3
- 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 +29 -273
- package/agent-tools/subagent-async.mjs +36 -31
- package/agent-tools/subagent-panel.mjs +38 -9
- package/agent-tools/subagent-run.mjs +44 -29
- package/agent-tools/subagent-scheduler.mjs +29 -28
- package/agent-tools/subagent-spawn.mjs +57 -25
- package/agent-tools/subagent.mjs +46 -66
- 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 +20 -351
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +12 -7
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- 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/git/checkpoint.mjs +4 -8
- 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 +35 -7
- package/ledger-migrate.mjs +299 -0
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +11 -5
- 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-specs.mjs +131 -66
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +100 -71
- 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 +19 -7
- 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 +1 -2
- package/prompts/persona-engineering.md +41 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +10 -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-gc.mjs +67 -6
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- 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 -236
- 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/question.md +1 -0
- 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 +10 -11
- package/tools/edit-diff.mjs +33 -25
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +11 -12
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/glob-dialect.mjs +3 -3
- 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/search.mjs +2 -2
- package/tools/shared.mjs +9 -7
- 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,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-slot-claims.mjs — 认领面族(2026-09-28 拆分批 · R3——自 `session-slots-manifest.mjs`
|
|
3
|
+
* (421 > 300)外提认领面:`staleClaims` ∕ owner 三件(`ownerPid` / `ownerPids` /
|
|
4
|
+
* `ownerStateOf`)∕ `cleanDeadOwners` ∕ `ensureActive` ∕ `allocateFresh` ∕ `claimSlot` ∕
|
|
5
|
+
* `_releaseStats` ∕ `releaseClaimsAll` ∕ `activeSlot`;**逐字迁移**、零行为改——台账 #484
|
|
6
|
+
* 「认领面族拆分」顺延项落形)。
|
|
7
|
+
* 原档(`session-slots-manifest.mjs`)保留摘要 / 清单读写 / 属主健康面,并 re-export 本档
|
|
8
|
+
* 导出保既有 import 面(消费档路径与名面不动)。
|
|
9
|
+
*
|
|
10
|
+
* 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的存储
|
|
11
|
+
* 原语(`getSessionId` / `manifestPath` / `slotPath`)与 `session-slots-manifest.mjs` 的清单
|
|
12
|
+
* 读写(`loadManifest` / `saveManifest`),后者反向 re-export 本档导出——函数声明在实例化期
|
|
13
|
+
* 已初始化、双方只在函数体内运行时使用(模块求值期零顶层调用 ⇒ 环安全)。
|
|
14
|
+
* 探测纪律(init-block 批 · F-MI7 / SESSION.md §6.2):本档**零自有 exec、零逐 pid 探测**——
|
|
15
|
+
* 只消费调用面传入的**探测束**(`bundle`);属主三态判据单源 = `process-probe.mjs`,
|
|
16
|
+
* `unknown`(探测失败 / 缺行)⇒ 不认领 / 不判死 / 不删(D-MI10)。
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from "node:fs"
|
|
19
|
+
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据面与探测面分离)。
|
|
20
|
+
import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
|
|
21
|
+
// 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
|
|
22
|
+
import { getSessionId, manifestPath, slotPath } from "./session-slots.mjs"
|
|
23
|
+
// 清单读写(落盘单源住原档——静态环见头注;反向 re-export 保 import 面)。
|
|
24
|
+
import { loadManifest, saveManifest } from "./session-slots-manifest.mjs"
|
|
25
|
+
|
|
26
|
+
/** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
|
|
27
|
+
* 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
|
|
28
|
+
* 其余活绑定槽(单绑定落点 ⇒ `{落点槽}`;被占落点 ⇒ 空集)。返回槽号(**字符串键**形态)。 */
|
|
29
|
+
export function staleClaims(src, keep = []) {
|
|
30
|
+
const keepSet = new Set((keep ?? []).filter((s) => s != null).map(Number))
|
|
31
|
+
const mySessionId = getSessionId()
|
|
32
|
+
return Object.entries(src?.slotSessions ?? {})
|
|
33
|
+
.filter(([slot, owner]) => owner === mySessionId && !keepSet.has(Number(slot)))
|
|
34
|
+
.map(([slot]) => slot)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 属主串 → pid(`"pid-ts-rand"`——`owner.split("-")[0]` 格式判据单源:session-slots ↔
|
|
38
|
+
* session-lifecycle 共用,防两处漂移)。不可解析 ⇒ NaN——判据层按 dead 处理
|
|
39
|
+
* (见 process-probe `ownerState`)。 */
|
|
40
|
+
export function ownerPid(owner) { return parseInt(owner.split("-")[0]) }
|
|
41
|
+
|
|
42
|
+
/** manifest 属主 pid 全量清单(**非本进程**——束入参:一次探测覆盖所有待判 pid;
|
|
43
|
+
* 空清单 ⇒ 探测面零 exec 早退)。`exclude` 缺省 = 本进程 sessionId。 */
|
|
44
|
+
export function ownerPids(m, exclude = getSessionId()) {
|
|
45
|
+
const pids = []
|
|
46
|
+
for (const owner of Object.values(m.slotSessions ?? {})) {
|
|
47
|
+
if (!owner || owner === exclude) continue
|
|
48
|
+
const pid = ownerPid(owner)
|
|
49
|
+
if (pid) pids.push(pid)
|
|
50
|
+
}
|
|
51
|
+
return pids
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 束 → 属主三态(判据单源 = process-probe `ownerState`;束缺失 / 探测失败 ⇒ "unknown"——
|
|
55
|
+
* 保守保留,**不得**当全死)。 */
|
|
56
|
+
export function ownerStateOf(owner, bundle) {
|
|
57
|
+
const pid = ownerPid(owner)
|
|
58
|
+
return ownerState(pid, { aliveSet: bundle?.aliveSet ?? null, cmds: bundle?.cmds ?? null })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 束 → `filterDeadOwners` 入参(清理面 API 保面——三态 `alive`:无束 / 失败 ⇒ undefined = 未知)。 */
|
|
62
|
+
function ownerArgs(owner, bundle) {
|
|
63
|
+
const pid = ownerPid(owner)
|
|
64
|
+
const aliveSet = bundle?.aliveSet ?? null
|
|
65
|
+
return [pid, { alive: aliveSet ? aliveSet.has(pid) : undefined, cmdline: bundle?.cmds?.get?.(pid) }]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
|
|
69
|
+
* ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
|
|
70
|
+
* 判据(不能以"文件缺失"短路——活进程在"认领 → 首次保存"窗口文件暂缺,误删会
|
|
71
|
+
* 致双进程同槽),但**不自行探测**:判活 / 命令行来自调用面同一探测束(F-MI7——
|
|
72
|
+
* 零逐 pid exec)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
|
|
73
|
+
* 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
|
|
74
|
+
* 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。
|
|
75
|
+
*
|
|
76
|
+
* 身份复核(批 1 · F-MI6 / D-MI11——判据单源 `filterDeadOwners`):pid 活 + 命令行**明确
|
|
77
|
+
* 可得且非本产品** ⇒ 同判可删(pid 复用即此场景);探测失败 / 缺行 ⇒ 保守保留(D-MI10)。 */
|
|
78
|
+
export function cleanDeadOwners(m, bundle) {
|
|
79
|
+
const mySessionId = getSessionId()
|
|
80
|
+
const entries = Object.entries(m.slotSessions).filter(([, o]) => o && o !== mySessionId)
|
|
81
|
+
const deadSlots = []
|
|
82
|
+
for (const [slot, owner] of entries) {
|
|
83
|
+
const [pid, args] = ownerArgs(owner, bundle)
|
|
84
|
+
if (filterDeadOwners(pid, args)) {
|
|
85
|
+
delete m.slotSessions[slot]
|
|
86
|
+
deadSlots.push(slot)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Claim a slot for this process and set it as active. Idempotent.
|
|
94
|
+
* Preference order:
|
|
95
|
+
* 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
|
|
96
|
+
* 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
|
|
97
|
+
* 3. A brand-new slot when all are owned by live processes. → allocateFresh
|
|
98
|
+
* The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
|
|
99
|
+
* see which slots are taken and avoid them.
|
|
100
|
+
*
|
|
101
|
+
* F-MI7(同步例外收窄):粘性早退(已拥有 active)**零探测**(无束、无 exec);
|
|
102
|
+
* 冷路径 = 一次有界同步束(`probeOwnersSync`)——清理 / 占用 / 空闲全部查表。
|
|
103
|
+
*/
|
|
104
|
+
function ensureActive(cwd, m) {
|
|
105
|
+
const mySessionId = getSessionId()
|
|
106
|
+
if (!m.slotSessions) m.slotSessions = {}
|
|
107
|
+
|
|
108
|
+
// Already own the active slot — nothing to do. 粘性早退:**零探测**(死项清理交给
|
|
109
|
+
// 非粘性路径——resumeSlot 每次进程入口的异步束 / 冷路径同步束都会清并落盘)。
|
|
110
|
+
if (m.active && m.slotSessions[m.active] === mySessionId) return
|
|
111
|
+
|
|
112
|
+
// 冷路径:入口一次有界同步束(每 exec ≤ SYNC_PROBE_MS)覆盖全量属主 pid。
|
|
113
|
+
const bundle = probeOwnersSync(ownerPids(m))
|
|
114
|
+
const deadParam = cleanDeadOwners(m, bundle)
|
|
115
|
+
|
|
116
|
+
const isFree = (slot) => {
|
|
117
|
+
const owner = m.slotSessions[slot]
|
|
118
|
+
if (!owner || owner === mySessionId) return true
|
|
119
|
+
// 未知(探测失败 / 缺行)⇒ 不认为是空闲(不认领——D-MI10)
|
|
120
|
+
return ownerStateOf(owner, bundle) === "dead"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
|
|
124
|
+
if (m.active && m.slots[m.active] && isFree(m.active)) {
|
|
125
|
+
m.slotSessions[m.active] = mySessionId
|
|
126
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §6.10 D-2——resumeSlot 全新分配复用)。
|
|
131
|
+
allocateFresh(cwd, m, deadParam, bundle)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §6.10 D-2 抽取——resumeSlot
|
|
136
|
+
* 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
|
|
137
|
+
* 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
|
|
138
|
+
* 3. A brand-new slot when none is free / all numbers are taken.
|
|
139
|
+
* 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
|
|
140
|
+
* F-MI7:占用 / 空闲判据全部查**调用面束**(`bundle`——零自有探测);缺省 null ⇒
|
|
141
|
+
* 全部属主按 unknown 论(只可能取全新号——保守,不认领陌生人)。
|
|
142
|
+
*/
|
|
143
|
+
export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
|
|
144
|
+
const mySessionId = getSessionId()
|
|
145
|
+
if (!m.slotSessions) m.slotSessions = {}
|
|
146
|
+
const isFree = (slot) => {
|
|
147
|
+
const owner = m.slotSessions[slot]
|
|
148
|
+
if (!owner || owner === mySessionId) return true
|
|
149
|
+
return ownerStateOf(owner, bundle) === "dead" // 未知 ⇒ 非空闲(不认领——D-MI10)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
|
|
153
|
+
// 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
|
|
154
|
+
// 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
|
|
155
|
+
const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
|
|
156
|
+
for (const slot of allSlots) {
|
|
157
|
+
if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
|
|
158
|
+
m.active = slot
|
|
159
|
+
m.slotSessions[slot] = mySessionId
|
|
160
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
161
|
+
return slot
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 3. All slots owned by live processes — allocate a new one (no limit).
|
|
166
|
+
// 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
|
|
167
|
+
// 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
|
|
168
|
+
// 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
|
|
169
|
+
// 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
|
|
170
|
+
// 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
|
|
171
|
+
// 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
|
|
172
|
+
// spread 有 RangeError 风险)。
|
|
173
|
+
const liveClaimed = (n) => {
|
|
174
|
+
const owner = m.slotSessions?.[n]
|
|
175
|
+
if (!owner || owner === mySessionId) return false
|
|
176
|
+
return ownerStateOf(owner, bundle) !== "dead" // 活 / 未知 ⇒ 视为已占(跳号——保守)
|
|
177
|
+
}
|
|
178
|
+
let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
|
|
179
|
+
while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
|
|
180
|
+
m.active = newSlot
|
|
181
|
+
m.slotSessions[newSlot] = mySessionId
|
|
182
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
183
|
+
return newSlot
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 认领指定槽为本进程所有并置为 active(2026-09-05 §6.10 D-2 resumeSlot 认领路径——调用方
|
|
187
|
+
* 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
|
|
188
|
+
* 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
|
|
189
|
+
* deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
|
|
190
|
+
* deadParam 同型过滤)。
|
|
191
|
+
* F-CR1(2026-09-21 · SESSION-CLAIM 批):认领落点释放本进程残留认领(保留集 = {认领槽}——
|
|
192
|
+
* §6.2 公式代入);本函数 = 启动恢复 / 本端恢复落点(核 `resumeSlot` 与 VSC 端壳
|
|
193
|
+
* `resumeSlot` 共用),落盘判据(fresh 快照 / 值条件删除 / 内存移除)见 `saveManifest`。 */
|
|
194
|
+
export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
|
|
195
|
+
m.slotSessions ??= {}
|
|
196
|
+
m.slotSessions[slot] = getSessionId()
|
|
197
|
+
m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
|
|
198
|
+
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true, release: [slot] })
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** 观测计数(测试缝——`_storeStats` / `_staleHooks` 同惯例):`releaseClaimsAll` 调用计数。
|
|
202
|
+
* 端壳「工作区转空 ⇒ 旧 cwd 释放恰一次」类断言以此为桩计数(生产零消费)。 */
|
|
203
|
+
export const _releaseStats = { calls: 0 }
|
|
204
|
+
|
|
205
|
+
/** 退出全释放(EXIT-CLAIM-RELEASE 批 · F-XR1 · SESSION.md §6.18 D-SE41):优雅退出前释放
|
|
206
|
+
* 本进程**全部**槽认领(保留集空——`staleClaims` 释放谓词单源);落盘判据 = `saveManifest`
|
|
207
|
+
* `opts.release` 三条自动继承(fresh 同次快照取释放集 / 值条件删除——他人窗口内新认领不误删 /
|
|
208
|
+
* 内存认领表同步移除)。**零触碰 marker 面**(F-XR2 路标保留——下次启动经 `usableSlot`
|
|
209
|
+
* 无属主短路零探测直达恢复);崩溃路径不经此(信号无 JS 钩子——F-XR3)。
|
|
210
|
+
* 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
|
|
211
|
+
* 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
|
|
212
|
+
* (true = 有释放且落盘成功;false = 早退 / 失败 / **写面拒写**——`saveManifest` 返回值透传,
|
|
213
|
+
* §6.23 判据句 3——断言面);写失败盘面 = 认领残留 →
|
|
214
|
+
* 恢复走既有探测面(现状形态,数据零险)。 */
|
|
215
|
+
export function releaseClaimsAll(cwd) {
|
|
216
|
+
_releaseStats.calls += 1
|
|
217
|
+
try {
|
|
218
|
+
if (!existsSync(manifestPath(cwd))) return false // 无 manifest ⇒ 零写(不造盘面)
|
|
219
|
+
const m = loadManifest(cwd)
|
|
220
|
+
if (staleClaims(m, []).length === 0) return false // 无本进程认领 ⇒ 零写(免白刷共享 sessionId)
|
|
221
|
+
return saveManifest(cwd, m, null, { release: [] }) !== false // 保留集空 = 全释放;不 setActive;返回值透传(§6.23 判据句 3——拒写 ⇒ false)
|
|
222
|
+
} catch { return false } // F-XR1:退出恒达(写失败 = 认领残留 → 恢复走探测面)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Return the active slot number for this process, claiming one if necessary */
|
|
226
|
+
export function activeSlot(cwd) {
|
|
227
|
+
const m = loadManifest(cwd)
|
|
228
|
+
ensureActive(cwd, m)
|
|
229
|
+
return m.active
|
|
230
|
+
}
|
|
@@ -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
|
+
}
|