@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
package/session-gc.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* F1 残留 GC(自动):进程启动时对当前 cwd hash 前缀做一次轻量清理——.corrupted /
|
|
5
5
|
* .unreadable / .manifest.corrupted / .bak-* 保留 30 天,孤儿 .tmp 保留 7 天
|
|
6
|
-
*(mtime < now − retention 才删,等于保留期保留——older-than
|
|
6
|
+
*(mtime < now − retention 才删,等于保留期保留——older-than 边界语义);**孤儿 sidecar
|
|
7
|
+
* 记录目录 `.d`**(主文件 `{prefix}.{N}` 不存在)保留 30 天(数据现场族口径)⇒ **回收**
|
|
8
|
+
*(rename 进 `sessions-trash`——目录承载记录存储本体,不可直删;D-SE36 同语义)。
|
|
7
9
|
* 安全(N1):活跃槽(主文件在 + manifest slotSessions[N] 属主活)的现场一律保留;
|
|
8
10
|
* manifest 主文件 / end marker / 数据主文件永不进入候选(后缀预过滤只匹配残留后缀)。
|
|
9
11
|
*
|
|
@@ -35,6 +37,9 @@ import { configDir } from "./config.mjs"
|
|
|
35
37
|
import { sessionPath, ownerPid } from "./session-slots.mjs"
|
|
36
38
|
import { probeOwnersAsync, ownerState } from "./process-probe.mjs"
|
|
37
39
|
import { listStaleCwds, deleteStaleCwd, recycleGroup, sweepStale, trashRootFor } from "./session-stale.mjs"
|
|
40
|
+
// sidecar 记录目录后缀常量(单源 = `session-segments.mjs:13`——本档经该常量判后缀形态;
|
|
41
|
+
// 两处名形匹配式(`classifyResidue` / `sweepOrphanRecordDirs`)按 `{prefix}.{N}` + 后缀内联,不构成第二单源)。
|
|
42
|
+
import { RECORD_DIR_SUFFIX } from "./session-segments.mjs"
|
|
38
43
|
|
|
39
44
|
/** 保留期(§6.12):损坏现场(.corrupted/.unreadable/.manifest.corrupted)与并发轮转
|
|
40
45
|
* 备份(.bak-*)30 天;孤儿 .tmp 7 天(崩溃现场恢复窗口)。 */
|
|
@@ -46,8 +51,9 @@ export const COLD_CWD_RETENTION_MS = 90 * 24 * 3600 * 1000
|
|
|
46
51
|
function sessionsDir() { return join(configDir, "sessions") }
|
|
47
52
|
|
|
48
53
|
/** 残留分类(§6.12 后缀表):name 须以 `${prefix}.` 开头(prefix = `${hash}.json`)。
|
|
49
|
-
* 返回 { retention, slot, tmp } 或 null(主文件/end marker/他前缀——不动)。
|
|
50
|
-
* slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted
|
|
54
|
+
* 返回 { retention, slot, tmp, dir } 或 null(主文件/end marker/他前缀——不动)。
|
|
55
|
+
* slot = 关联槽号(`.N.` 段),无则 null(如 .manifest.corrupted);`dir` = sidecar 记录目录
|
|
56
|
+
* 形(`{prefix}.{N}.d`——孤儿判据反向:**主文件在 ⇒ 跳过**)。 */
|
|
51
57
|
function classifyResidue(name, prefix) {
|
|
52
58
|
if (!name.startsWith(prefix + ".")) return null
|
|
53
59
|
const slotMatch = name.slice(prefix.length).match(/^\.(\d+)\./)
|
|
@@ -55,6 +61,7 @@ function classifyResidue(name, prefix) {
|
|
|
55
61
|
if (name.endsWith(".corrupted") || name.endsWith(".unreadable")) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
56
62
|
if (/\.bak-\d+$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false }
|
|
57
63
|
if (name.endsWith(".tmp")) return { retention: ORPHAN_TMP_RETENTION_MS, slot, tmp: true }
|
|
64
|
+
if (name.endsWith(RECORD_DIR_SUFFIX) && /\.\d+\.d$/.test(name)) return { retention: RESIDUE_RETENTION_MS, slot, tmp: false, dir: true }
|
|
58
65
|
return null
|
|
59
66
|
}
|
|
60
67
|
|
|
@@ -105,17 +112,60 @@ export async function gcResidue({ dir = sessionsDir(), prefix, now = Date.now(),
|
|
|
105
112
|
if (c.slot !== null && active.has(c.slot)) continue // N1:活跃槽现场一律保留(T3)
|
|
106
113
|
const p = join(dir, c.name)
|
|
107
114
|
if (c.tmp && existsSync(p.slice(0, -".tmp".length))) continue // 非孤儿 .tmp(主文件在——写中/回退候选)
|
|
115
|
+
if (c.dir && existsSync(p.slice(0, -RECORD_DIR_SUFFIX.length))) continue // 非孤儿 sidecar(主文件在 ⇒ 记录存储本体——不动;仅孤儿目录入候选)
|
|
108
116
|
let st
|
|
109
117
|
try { st = await stat(p) } catch { continue }
|
|
110
118
|
if (st.mtimeMs >= now - c.retention) continue // 边界:older-than 才删,等于保留期保留(§6.12)
|
|
111
119
|
result.candidates.push(c.name)
|
|
112
|
-
if (
|
|
113
|
-
|
|
120
|
+
if (dryRun) continue
|
|
121
|
+
if (c.dir) {
|
|
122
|
+
// 目录承载记录存储本体 ⇒ **回收**(rename 进 `sessions-trash/<批次>/`——不 unlink / 不同步 fs;
|
|
123
|
+
// 失败 ⇒ 跳过并计数(candidates − deleted)+ 原目录零删除)
|
|
124
|
+
const r = await recycleGroup({ prefix, files: [c.name] }, { dir, now })
|
|
125
|
+
if (r.moved.length) result.deleted.push(c.name)
|
|
126
|
+
continue
|
|
114
127
|
}
|
|
128
|
+
try { await unlink(p); result.deleted.push(c.name) } catch { /* 占用/竞态——跳过 */ }
|
|
115
129
|
}
|
|
116
130
|
return result
|
|
117
131
|
}
|
|
118
132
|
|
|
133
|
+
/** ② 显式面清运腿(§6.12 / §6.17 补充——存量通路):全目录孤儿 sidecar 记录目录
|
|
134
|
+
*(`{prefix}.{N}.d`——主文件不存在 ∧ 该槽非活跃)⇒ 逐条**回收**(rename 进 `sessions-trash/<批次>/`,同 ① 语义)。
|
|
135
|
+
* 与 ① 两面之差:① = 自动面(单前缀 / 当前 cwd / **带 30 天保留期**——只治本前缀未来新增);
|
|
136
|
+
* 本函数 = **用户显式面**(跳前缀遍历全目录 / **不设保留期**——存量孤儿跨多前缀且多为近期孤儿,
|
|
137
|
+
* 自动面望不到;进程入口 = `--dry-run` 先列 / `--confirm --all` 才动,且回收可回退)。
|
|
138
|
+
* `dryRun` ⇒ 只列不删;目录不可读 ⇒ 空集。失败 ⇒ 跳过(不 unlink 兜底——原目录零删除)。 */
|
|
139
|
+
export async function sweepOrphanRecordDirs({ dir = sessionsDir(), now = Date.now(), dryRun = false, probeFn = probeOwnersAsync, entries = null } = {}) {
|
|
140
|
+
const out = { candidates: [], recycled: [] }
|
|
141
|
+
let names = entries
|
|
142
|
+
if (!names) {
|
|
143
|
+
try { names = await readdir(dir) } catch { return out }
|
|
144
|
+
}
|
|
145
|
+
const present = new Set(names)
|
|
146
|
+
const byPrefix = new Map() // prefix → [{ name, slot }]
|
|
147
|
+
for (const name of names) {
|
|
148
|
+
const m = /^(.+\.json)\.(\d+)\.d$/.exec(name)
|
|
149
|
+
if (!m) continue
|
|
150
|
+
if (present.has(`${m[1]}.${m[2]}`)) continue // 主文件在 ⇒ 非孤儿(记录存储本体——不动)
|
|
151
|
+
if (!byPrefix.has(m[1])) byPrefix.set(m[1], [])
|
|
152
|
+
byPrefix.get(m[1]).push({ name, slot: Number(m[2]) })
|
|
153
|
+
}
|
|
154
|
+
for (const [prefix, items] of byPrefix) {
|
|
155
|
+
const active = await liveSlots(dir, prefix, probeFn) // N1:活跃槽现场一律保留
|
|
156
|
+
for (const it of items) {
|
|
157
|
+
if (active.has(it.slot)) continue
|
|
158
|
+
let st
|
|
159
|
+
try { st = await stat(join(dir, it.name)) } catch { continue }
|
|
160
|
+
if (st) out.candidates.push(it.name)
|
|
161
|
+
if (dryRun) continue
|
|
162
|
+
const r = await recycleGroup({ prefix, files: [it.name] }, { dir, now })
|
|
163
|
+
if (r.moved.length) out.recycled.push(it.name)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
119
169
|
/** 启动**窗外延迟拍** pass(§6.17 编排——点火见 `scheduleSessionGC`):① 一次**异步目录快照**
|
|
120
170
|
* (readdir 恰一次——两面共享)② 残留面(gcResidue 既有判据)③ 存量面(总评估有界
|
|
121
171
|
* ≤ STALE_SWEEP_LIMIT(含 ① 面 manifest 读——D-SE40)+ 超期回收批清运)。
|
|
@@ -265,6 +315,10 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
|
|
|
265
315
|
const residue = p ? await gcResidue({ dir, prefix: p, now, dryRun: true, probeFn }) : { candidates: [] }
|
|
266
316
|
out(`Residue candidates for current project (${p ?? "unknown"}): ${residue.candidates.length}`)
|
|
267
317
|
for (const name of residue.candidates) out(` ${name}`)
|
|
318
|
+
// 孤儿 sidecar 记录目录(② 存量面——全目录;主文件不在 + 该槽非活跃——不设保留期)
|
|
319
|
+
const orphanDirs = await sweepOrphanRecordDirs({ dir, now, dryRun: true, probeFn })
|
|
320
|
+
out(`Orphan record-dir (.d) candidates (main file gone; explicit face — no retention): ${orphanDirs.candidates.length}`)
|
|
321
|
+
for (const name of orphanDirs.candidates) out(` ${name}`)
|
|
268
322
|
out(`Cold/stale project candidates (cold = manifest idle > 90 days; stale = no live owner + cwd unreachable/empty + 7-day window): ${candidates.length}`)
|
|
269
323
|
for (const c of candidates) out(` ${c.hash} reason ${c.reason} files ${c.files.length}`)
|
|
270
324
|
if (candidates.length >= GC_PROGRESS_MIN) out(`Estimate: ${fmtGcEstimate(candidates.length)} to scan ${candidates.length} candidates (measured ≈${GC_MS_PER_CANDIDATE}ms/candidate).`)
|
|
@@ -279,7 +333,9 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
|
|
|
279
333
|
try { loopEntries = await readdir(dir) } catch { loopEntries = null }
|
|
280
334
|
}
|
|
281
335
|
const targets = confirmTarget === "--all" ? candidates : candidates.filter((c) => c.hash === confirmTarget)
|
|
282
|
-
|
|
336
|
+
// ② 面候选预列(孤儿 sidecar 记录目录——存量通路;只列不删——实回收在组循环后)
|
|
337
|
+
const orphanDirs = confirmTarget === "--all" ? await sweepOrphanRecordDirs({ dir, now, dryRun: true, probeFn, entries: loopEntries }) : { candidates: [] }
|
|
338
|
+
if (!targets.length && !orphanDirs.candidates.length) {
|
|
283
339
|
err(`Refused: ${confirmTarget} is not a cold/stale project (active, recent, or unknown) — nothing deleted.`)
|
|
284
340
|
return 1
|
|
285
341
|
}
|
|
@@ -299,5 +355,10 @@ export async function runSessionGc(args, { dir = sessionsDir(), prefix = null, c
|
|
|
299
355
|
}
|
|
300
356
|
out(`Moved ${r.deleted.length} files for ${t.hash} into the recycle bin (${r.batch})${r.skipped?.length ? ` — skipped ${r.skipped.length} (locked/racing, kept in place)` : ""}.`)
|
|
301
357
|
}
|
|
358
|
+
// ② 显式面清运腿(孤儿 sidecar 记录目录——存量通路;与 ① 同语义:rename 进回收批)
|
|
359
|
+
if (confirmTarget === "--all" && orphanDirs.candidates.length) {
|
|
360
|
+
const moved = await sweepOrphanRecordDirs({ dir, now, probeFn, entries: loopEntries })
|
|
361
|
+
out(`Orphan record dirs (.d): moved ${moved.recycled.length}/${moved.candidates.length} into the recycle bin (${trashRootFor(dir)}).`)
|
|
362
|
+
}
|
|
302
363
|
return 0
|
|
303
364
|
}
|
package/session-guard.mjs
CHANGED
|
@@ -4,20 +4,29 @@
|
|
|
4
4
|
* 行随迁,verbatim 零语义变):saveSession 与 token-ttl persistEngTokens 共用同一份。
|
|
5
5
|
* 写前校验磁盘槽文件是否本进程会话的现场——磁盘文件 sessionStart 与本进程会话不符
|
|
6
6
|
* (另一进程/会话的现场)/版本更新/异 cwd → 先轮转 .bak 保留再写。检查按 mtime 缓存
|
|
7
|
-
* (agent._slotMtime
|
|
7
|
+
* (agent._slotMtime = `{p, mtimeMs}`——**按槽文件路径**命中,见 #386 复核):文件自上次检查/自写未变就跳过全量解析——每次保存都
|
|
8
8
|
* readFileSync+JSON.parse 多 MB 会话 → O(n²) 退化。文件存在但不可读(损坏/半写)→
|
|
9
9
|
* 改名 .corrupted 保留现场。返回轮转的 .bak 路径或 null。
|
|
10
|
+
*
|
|
11
|
+
* 创建端取值链(SLOT-END-PARAM 批 §6.20 判据句 4):本守卫是全量解析盘面的唯一写前窗口
|
|
12
|
+
* ⇒ 创建端**顺带解析**(副作用回写 `agent._slotCreatedBy`,供 saveSession 在守卫**之后**
|
|
13
|
+
* 注入 fields——分支:文件不在盘 / 轮转 / 损坏改名 ⇒ `sessionEnd()`(本端将首物化这份文件);
|
|
14
|
+
* 解析通过未轮转 ⇒ `disk.createdBy ?? null`(透传盘上键;老槽无键 ⇒ null ⇒ 禁回填);
|
|
15
|
+
* mtime 命中 ⇒ 本函数未解析盘面 ⇒ 值不动)。
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
|
|
13
19
|
import { basename } from "node:path"
|
|
14
|
-
// §
|
|
20
|
+
// SESSION.md §6.12①(修正轮 #1):.bak 轮转对**非本会话活动绑定面**联动改名 sidecar(`{p}.d → {bak}.d`)。
|
|
15
21
|
import { RECORD_DIR_SUFFIX } from "./session-store.mjs"
|
|
22
|
+
// 本进程端名(创建端取值链缺省值)——静态环:本档 ↔ session-slots.mjs(经 session.mjs,同
|
|
23
|
+
// 两档头注:函数体内运行时使用、求值期零顶层调用 ⇒ 环安全)。
|
|
24
|
+
import { sessionEnd } from "./session-slots.mjs"
|
|
16
25
|
|
|
17
26
|
/** 轮转时随迁记录存储(.bak 路径联动)——本会话活动绑定面跳过(防拔掉活动存储)。 */
|
|
18
27
|
function moveSidecar(src, dst, agent) {
|
|
19
28
|
const dir = src + RECORD_DIR_SUFFIX
|
|
20
|
-
if (agent?._recordStore?.dir === dir) return //
|
|
29
|
+
if (agent?._recordStore?.dir === dir) return // 本会话活动绑定面——原地保留(SESSION.md §6.12①)
|
|
21
30
|
try {
|
|
22
31
|
if (existsSync(dir)) renameSync(dir, dst + RECORD_DIR_SUFFIX)
|
|
23
32
|
} catch { /* 随迁失败不阻断轮转(现场原地保留——读侧身份核验兜底) */ }
|
|
@@ -25,9 +34,17 @@ function moveSidecar(src, dst, agent) {
|
|
|
25
34
|
|
|
26
35
|
export function guardForeignSlotFile(agent, p, slot) {
|
|
27
36
|
try {
|
|
28
|
-
if (!existsSync(p))
|
|
37
|
+
if (!existsSync(p)) {
|
|
38
|
+
agent._slotCreatedBy = sessionEnd() // 文件不在盘 ⇒ 本端即将首物化(= 创建端)
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
29
41
|
const st = statSync(p)
|
|
30
|
-
|
|
42
|
+
// mtime 命中(**同路径** ∧ 同 mtime):未解析盘面 ⇒ 值不动。缓存带路径标签(#386 复核)——
|
|
43
|
+
// 单 agent 换槽复用(`_slot` 直钉新槽,未经 applySession / resetSessionState 清缓存)时,
|
|
44
|
+
// 旧槽 mtime 不得命中新槽(同 mtime 巧合 ⇒ 漏轮转 / 漏 createdBy 透传);「换槽即清缓存」
|
|
45
|
+
// 不再是被依赖的隐式前提。
|
|
46
|
+
const hit = agent._slotMtime
|
|
47
|
+
if (hit && hit.p === p && st.mtimeMs === hit.mtimeMs) return null
|
|
31
48
|
const disk = JSON.parse(readFileSync(p, "utf8"))
|
|
32
49
|
// version>2 的新版文件无论 sessionStart 一律轮转(loadSlotFile 对 v3 返回 null 不动
|
|
33
50
|
// 文件——若其 sessionStart 为 null,旧版首次保存会静默覆盖;轮转 .bak 保证新版文件
|
|
@@ -42,10 +59,12 @@ export function guardForeignSlotFile(agent, p, slot) {
|
|
|
42
59
|
renameSync(p, bak)
|
|
43
60
|
moveSidecar(p, bak, agent)
|
|
44
61
|
console.error(`[session] slot ${slot} holds ${diskIsNewer ? `a newer-version file (v${disk.version})` : diskForeign ? `a foreign-cwd file (${disk.cwd})` : `another session (start ${diskStart}, ours ${myStart})`} — preserved as ${basename(bak)}`)
|
|
45
|
-
agent._slotMtime = st.mtimeMs
|
|
62
|
+
agent._slotMtime = { p, mtimeMs: st.mtimeMs }
|
|
63
|
+
agent._slotCreatedBy = sessionEnd() // 轮转:现场已让位 ⇒ 本端将首物化新文件
|
|
46
64
|
return bak
|
|
47
65
|
}
|
|
48
|
-
agent._slotMtime = st.mtimeMs
|
|
66
|
+
agent._slotMtime = { p, mtimeMs: st.mtimeMs }
|
|
67
|
+
agent._slotCreatedBy = disk?.createdBy ?? null // 解析通过:透传盘上键(无键 ⇒ null ⇒ 禁回填)
|
|
49
68
|
return null
|
|
50
69
|
} catch {
|
|
51
70
|
// 文件存在但不可读(损坏/半写):改名 .corrupted 保留现场(2026-09-01 advisor 🔵——
|
|
@@ -54,6 +73,7 @@ export function guardForeignSlotFile(agent, p, slot) {
|
|
|
54
73
|
if (existsSync(p)) {
|
|
55
74
|
try { renameSync(p, `${p}.corrupted`) } catch {}
|
|
56
75
|
}
|
|
76
|
+
agent._slotCreatedBy = sessionEnd() // 损坏改名:现场已让位 ⇒ 本端将首物化新文件
|
|
57
77
|
return null
|
|
58
78
|
}
|
|
59
79
|
}
|
package/session-lifecycle.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* 硬限;各函数语义原样外提,SESSION.md §6.10 D-2 / §6.12 / §6.11 注记随迁):
|
|
4
4
|
* 本端恢复入口包装(resumeSlot——残留 GC 钩子)/ 槽数据应用(applySession)/ 新建槽
|
|
5
5
|
* (newSession)/ 运行态清空(resetSessionState)/ 槽切换(switchToSlot)/ 占用查询
|
|
6
|
-
* (slotOccupancy)/
|
|
6
|
+
* (slotOccupancy)/ 打开态读数(sessionReading——§6.24 只读纯投影,桌面残余批落)/
|
|
7
|
+
* v1 老文件清扫(stripTruncatedToolArgs)。
|
|
7
8
|
*
|
|
8
9
|
* 静态环(与 session-slots.mjs ↔ session.mjs 同形):本档引 `session.mjs` 的
|
|
9
10
|
* `loadSlotFile`(data 层读留 session.mjs——2026-09-05 §6.10 D-2 拆分点),session.mjs re-export
|
|
@@ -20,24 +21,37 @@ import { existsSync } from "node:fs"
|
|
|
20
21
|
import { resolveCompactThreshold } from "./config.mjs"
|
|
21
22
|
import {
|
|
22
23
|
slotPath, loadManifest, saveManifest, slotDigest, writeSessionFile, getSessionId,
|
|
23
|
-
writeEndMarker, ownerPid, ownerPids,
|
|
24
|
+
writeEndMarker, sessionEnd, ownerPid, ownerPids,
|
|
24
25
|
resumeSlot as slotsResumeSlot,
|
|
25
26
|
} from "./session-slots.mjs"
|
|
26
27
|
import { probeOwnersSync, probeOwnersAsync, ownerState } from "./process-probe.mjs"
|
|
28
|
+
// 落点 A 顺手补写(§6.25 判据句 3 · LEDGER-RELIABILITY 批)——判据单源住清单档(与 resumeSlot 同函数)。
|
|
29
|
+
import { healDigest, slotMtime } from "./session-slots-manifest.mjs"
|
|
27
30
|
// 环 import:见头注(loadSlotFile 属 session.mjs 的 data 层读面)。
|
|
28
31
|
import { loadSlotFile } from "./session.mjs"
|
|
32
|
+
// 全新槽首物化构造器单源(SLOT-END-PARAM 批 §6.20 判据句 4 写面:「`newSession` 并入」——
|
|
33
|
+
// 与 VSC `newSlot` / `loadSlotForWrite` 全新分支同一构造器;环 import 同上,函数体内运行时使用)。
|
|
34
|
+
import { newSlotData } from "./session-slot-write.mjs"
|
|
29
35
|
import { mergeAdjacentAssistantEchoes } from "./context.mjs"
|
|
36
|
+
// 打开态读数公式(§6.24 判据句 2——与 CLI 状态行 / VSC 同式;零新公式,只组合)。
|
|
37
|
+
import { historyPercent } from "./token-window.mjs"
|
|
30
38
|
import { scheduleSessionGC } from "./session-gc.mjs"
|
|
31
39
|
import { restoreEngTokens } from "./token-ttl.mjs"
|
|
32
40
|
import { clearPlanMode } from "./agent-tools/plan.mjs"
|
|
33
41
|
import { bindRecordStore, unbindRecordStore, RECORD_WINDOW_MESSAGES } from "./session-store.mjs"
|
|
42
|
+
// 会话级档位(§6.21 判据句 4)判据单源:off 形族(`think-off.mjs`)+ 逐模型枚举
|
|
43
|
+
// (`model-specs.mjs`)——两叶档(零 import)⇒ 不新增环,核内零第二份族别表。
|
|
44
|
+
import { specForModel } from "./model-specs.mjs"
|
|
45
|
+
import { thinkOffShape, thinkOffPath } from "./think-off.mjs"
|
|
34
46
|
|
|
35
47
|
/** 恢复决策包装(SESSION.md §6.12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
|
|
36
48
|
* scheduleSessionGC 内部**启动窗外延迟拍**(`GC_PASS_DELAY_MS` = 3s)+ 每进程每前缀去重,
|
|
37
|
-
* **异步非阻塞**启动路径(N4;§6.17 D-SE39);**async**(F-MI7——调用面必须 await)。
|
|
38
|
-
|
|
49
|
+
* **异步非阻塞**启动路径(N4;§6.17 D-SE39);**async**(F-MI7——调用面必须 await)。
|
|
50
|
+
* `opts.end`(§6.20 判据句 2——显式端参 > 进程端名):本端记录端名透传(端壳经绑定转口
|
|
51
|
+
* 传本端常量;缺省 = 进程端名 ⇒ CLI 全调用点零改)。 */
|
|
52
|
+
export async function resumeSlot(cwd, { end = sessionEnd() } = {}) {
|
|
39
53
|
scheduleSessionGC(cwd)
|
|
40
|
-
return slotsResumeSlot(cwd)
|
|
54
|
+
return slotsResumeSlot(cwd, { end })
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
|
|
@@ -61,9 +75,9 @@ function stripTruncatedToolArgs(m) {
|
|
|
61
75
|
*
|
|
62
76
|
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
63
77
|
* "三支"误读):
|
|
64
|
-
* ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel
|
|
65
|
-
*
|
|
66
|
-
* v2;老 models[]
|
|
78
|
+
* ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel(**串值**;
|
|
79
|
+
* 非串 ∕ null ∕ 空串 = 未登记 ⇒ 回该渠道默认模型 `providers[].model` 单值——2026-09-10 MODEL-SELECTION
|
|
80
|
+
* v2;老 models[] 首候选形态已退场)+ 重算
|
|
67
81
|
* compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
|
|
68
82
|
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
69
83
|
* 复验 validateProvider 弹重选)。
|
|
@@ -126,6 +140,10 @@ export function applySession(agent, data, opts = {}) {
|
|
|
126
140
|
agent._verifyPassed = false
|
|
127
141
|
agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
|
|
128
142
|
agent._slotMtime = null
|
|
143
|
+
// 会话级档位携带(§6.21 判据句 5——保存面的一半):值 = 本槽 `effort` **原样**携带(表外 /
|
|
144
|
+
// 不可 off 的原值不静默改写——写面归一只在 `setSlotPrefs`);缺键(老槽)⇒ `undefined` ⇒
|
|
145
|
+
// `saveSession` 不落该键(禁回填——同 `createdBy` 先例;键在而值为 `null` ⇒ 原样携 `null`)。
|
|
146
|
+
agent._slotEffort = Object.hasOwn(data, "effort") ? (data.effort ?? null) : undefined
|
|
129
147
|
// ── 记录存储绑定(TUI-OOM-ROOTCAUSE · SESSION.md §6.14 绑定与对账 / 追加时点)──
|
|
130
148
|
// opts.slot 在场(启动恢复 / 非占用的 /session 切换 / ACP 钉槽)→ 绑定(身份核验 + 对账 +
|
|
131
149
|
// 窗口)——人读线 = store.tail(200);未传(模式 F:被他人活进程占用的槽 / 测试 / ACP fork)
|
|
@@ -149,16 +167,37 @@ export function applySession(agent, data, opts = {}) {
|
|
|
149
167
|
// 空槽 model 恒有值,provider.model 键不缺失)
|
|
150
168
|
const prevName = agent.activeProvider
|
|
151
169
|
const prevModel = agent.activeModel ?? null
|
|
152
|
-
|
|
170
|
+
// #638(adoption 边界归一):非串 `data.activeModel`(外端脏载 / 手工档)= 未登记 ⇒ 回退渠道
|
|
171
|
+
// 模型——类型脏值不入两状态字段(镜下方阈值重算门 ∕ 档位块 ∕ VSC `panel-turn-stages.mjs:53` 同形门)。
|
|
172
|
+
const slotModel = (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : slotProvider.model
|
|
153
173
|
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
154
174
|
agent.activeProvider = slotProvider.name
|
|
155
175
|
agent.activeModel = slotModel
|
|
156
176
|
agent.provider = { ...slotProvider }
|
|
157
177
|
if (slotModel) agent.provider.model = slotModel
|
|
158
|
-
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched
|
|
159
|
-
|
|
178
|
+
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)。
|
|
179
|
+
// 非串 model(外端脏载 / 手工档)⇒ 不重算(#638 后查表族恒 total,本门保留为同形防御——与
|
|
180
|
+
// 下方档位块同口径);类型脏载不中断恢复。
|
|
181
|
+
if (switched && agent.config?.agent?.compactThresholdAuto && typeof agent.provider.model === "string" && agent.provider.model) {
|
|
160
182
|
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
161
183
|
}
|
|
184
|
+
// 会话级档位施加(§6.21 判据句 4——在模型合并支之后,以**合并后**的 `provider.model` 判 spec;
|
|
185
|
+
// 三端共用本施加面):`"off"` 记号 ⇒ 该模型有 off 路径才设关思考形(effort 族该形为 `null`
|
|
186
|
+
// ⇒ 另置 `reasoningEffort: "none"`——§16.4 载荷门,单源 = `think-off.mjs`);枚举字面 ⇒ 直置;
|
|
187
|
+
// `null` / 缺键 / 表外串 ⇒ 不动(边界表第 1–3 行:老槽零行为变更,表外不静默改写)。
|
|
188
|
+
if (data.effort != null) {
|
|
189
|
+
// 非串 model(外端脏载 / 手工档)按未登记归一——本块不因类型脏载抛(同 `resolveEffortPatch` 口径)
|
|
190
|
+
const spec = specForModel(typeof agent.provider.model === "string" ? agent.provider.model : "")
|
|
191
|
+
if (data.effort === "off") {
|
|
192
|
+
if (thinkOffPath(spec)) {
|
|
193
|
+
const shape = thinkOffShape(spec)
|
|
194
|
+
agent.provider.thinking = shape
|
|
195
|
+
if (shape === null) agent.provider.reasoningEffort = "none"
|
|
196
|
+
}
|
|
197
|
+
} else if (spec?.reasoningEffortEnum?.includes(data.effort)) {
|
|
198
|
+
agent.provider.reasoningEffort = data.effort
|
|
199
|
+
}
|
|
200
|
+
}
|
|
162
201
|
return switched
|
|
163
202
|
}
|
|
164
203
|
// ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
|
|
@@ -211,10 +250,13 @@ export async function newSession(cwd, opts = {}) {
|
|
|
211
250
|
while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
|
|
212
251
|
|
|
213
252
|
// Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
|
|
214
|
-
// VS Code newSlot 对齐(SESSION.md §6.3 v2
|
|
215
|
-
|
|
253
|
+
// VS Code newSlot 对齐(SESSION.md §6.3 v2 格式双端一致;两端读侧均有兜底,功能等价)。
|
|
254
|
+
// SLOT-END-PARAM 批(§6.20 判据句 4 写面):改用首物化构造器单源 `newSlotData(cwd)`
|
|
255
|
+
// (原字面逐字同形——并入后随其带创建端 = 本端名)。
|
|
256
|
+
const data = newSlotData(cwd)
|
|
216
257
|
writeSessionFile(slotPath(cwd, slot), data)
|
|
217
|
-
|
|
258
|
+
// 摘要 `ts` 地板入参(#503 同族随动——保存面写后取 mtime;`slotMtime` 读失败 ⇒ null ⇒ 裸墙钟)
|
|
259
|
+
m.slots[slot] = slotDigest(data, slotMtime(cwd, slot))
|
|
218
260
|
m.active = slot
|
|
219
261
|
// 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
|
|
220
262
|
// 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
|
|
@@ -262,6 +304,7 @@ export function resetSessionState(agent) {
|
|
|
262
304
|
agent._pendingReminders = []
|
|
263
305
|
agent._slot = null
|
|
264
306
|
agent._slotMtime = null
|
|
307
|
+
agent._slotEffort = null // 新会话无档位(§6.21 判据句 5:全新槽规范形 = `effort: null`)
|
|
265
308
|
agent._osReminderInjected = false
|
|
266
309
|
// §6.11(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
|
|
267
310
|
// _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
|
|
@@ -279,11 +322,16 @@ export function resetSessionState(agent) {
|
|
|
279
322
|
* (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
|
|
280
323
|
* 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
|
|
281
324
|
* 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
|
|
282
|
-
export function switchToSlot(cwd, slot) {
|
|
325
|
+
export function switchToSlot(cwd, slot, { end = sessionEnd() } = {}) {
|
|
283
326
|
const m = loadManifest(cwd)
|
|
284
|
-
|
|
327
|
+
// §6.22 判据句 4(SESSION-LIST-DISK 批):准入 = 盘面文件存在 ∨ manifest 有条目——盘上独有槽
|
|
328
|
+
// 可切;旧分支逐字保留(有条目无文件 ⇒ 读槽返 null 同今日)。
|
|
329
|
+
if (!m.slots[slot] && !existsSync(slotPath(cwd, slot))) return null
|
|
285
330
|
const data = loadSlotFile(cwd, slot)
|
|
286
331
|
if (!data) return null
|
|
332
|
+
// 落点 A(§6.25 判据句 3):数据已在手 ⇒ 摘要顺手补写——随本次 saveManifest 落(**零额外读 /
|
|
333
|
+
// 零额外写**;新鲜条目不写——幂等,§6.25 谓词单源 `healNeeded`)。
|
|
334
|
+
healDigest(m, slot, data, slotMtime(cwd, slot))
|
|
287
335
|
m.active = slot
|
|
288
336
|
const occ = slotOccupancy(cwd, slot)
|
|
289
337
|
if (!occ.occupied) {
|
|
@@ -294,8 +342,9 @@ export function switchToSlot(cwd, slot) {
|
|
|
294
342
|
// (不认领目标 + 旧认领一并释放——下次保存经 `allocateFresh` fork,既有语义)。核受占 =
|
|
295
343
|
// 切换成立(D-SE33:指针 / 记录按 D-6 / D-4 落点——fork 面依赖指针翻至目标槽)。
|
|
296
344
|
saveManifest(cwd, m, null, { setActive: true, release: occ.occupied ? [] : [slot] })
|
|
297
|
-
// 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"
|
|
298
|
-
|
|
345
|
+
// 2026-09-05 §6.10 D-4:/session N 跟随"最后查看的槽"(成功切换才写);§6.20 判据句 2:
|
|
346
|
+
// 写点端名随动(显式端参 > 进程端名——端壳经绑定转口传本端常量)
|
|
347
|
+
writeEndMarker(cwd, slot, end)
|
|
299
348
|
return data
|
|
300
349
|
}
|
|
301
350
|
|
|
@@ -316,3 +365,26 @@ export function slotOccupancy(cwd, slot) {
|
|
|
316
365
|
if (st === "unknown") return { occupied: true, unknown: true, owner }
|
|
317
366
|
return { occupied: false }
|
|
318
367
|
}
|
|
368
|
+
|
|
369
|
+
/** 打开态读数(SESSION.md §6.24 · 桌面残余批——恢复态播种的读数**单源**):槽数据 ⇒ 状态行上下文
|
|
370
|
+
* 读数百分数(整数)。与 `applySession` 后读数**同源同式**(四事同判——组合既有件,零新公式):
|
|
371
|
+
* ① 线选 = `contextHistory` 非空取之,否则 `history` 经 `stripTruncatedToolArgs` 回退(同 applySession);
|
|
372
|
+
* ② 回声归并 = `mergeAdjacentAssistantEchoes`(同 applySession 装线前一步);
|
|
373
|
+
* ③ 渠道合并 = `providers` 命中 `data.activeProvider` ⇒ 串值 `activeModel` 覆盖 `entry.model`(非串 ∕
|
|
374
|
+
* 空串 = 未登记 ⇒ 回退 `entry.model`——模型合并支 ① 同归一);未命中 ⇒ `fallback`(支 ②「静默保持现状」的装配口径 = `loadConfig().provider`);
|
|
375
|
+
* ④ 公式 = `historyPercent`(`thincoder-core/token-window.mjs`——与 CLI 状态行 / VSC 同式)。
|
|
376
|
+
* **零副作用**:`data` / `providers` / `fallback` 皆入参(不读盘、不写盘、不改任何进程态);
|
|
377
|
+
* `data` 非对象 ⇒ `0`(与空历史同值——「非正 ⇒ 零节点」显示门归端侧,沿 `historyPercent` 空历史口径)。 */
|
|
378
|
+
export function sessionReading(data, { providers, fallback } = {}) {
|
|
379
|
+
if (data === null || typeof data !== "object") return 0
|
|
380
|
+
const full = Array.isArray(data.history) ? data.history : []
|
|
381
|
+
const ch = data.contextHistory
|
|
382
|
+
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
383
|
+
const line = mergeAdjacentAssistantEchoes(machine)
|
|
384
|
+
const entry = data.activeProvider ? providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
385
|
+
// #638:非串 `activeModel` 归一(与 applySession 模型合并支 ① 同判)——脏值不达 `historyPercent`。
|
|
386
|
+
const provider = entry
|
|
387
|
+
? { ...entry, model: (typeof data.activeModel === "string" && data.activeModel) ? data.activeModel : entry.model }
|
|
388
|
+
: fallback
|
|
389
|
+
return historyPercent(line, provider)
|
|
390
|
+
}
|
package/session-rename.mjs
CHANGED
|
@@ -29,9 +29,13 @@ export function renameSlot(cwd, slot, title, _stat = statSync) {
|
|
|
29
29
|
// 读与写之间文件被并发方改过 → 放弃(保留并发内容,重命名下次重试)
|
|
30
30
|
if (_stat(p).mtimeMs !== t0) return { ok: false, reason: "mtime-conflict" }
|
|
31
31
|
writeSessionFile(p, data)
|
|
32
|
+
// 写后取 mtime(标题写也是一次落盘)——摘要 `ts` 地板入参(#503 同族随动;读失败 ⇒ 缺省回溯墙钟。
|
|
33
|
+
// `statSync` 是真实 stat——`_stat` 只为 mtime-conflict 判据注入,地板不共用该缝)。
|
|
34
|
+
let wroteMtimeMs
|
|
35
|
+
try { wroteMtimeMs = statSync(p).mtimeMs } catch { /* 不可得 ⇒ 裸墙钟 */ }
|
|
32
36
|
const m = loadManifest(cwd)
|
|
33
37
|
if (m.slots[n]) {
|
|
34
|
-
m.slots[n] = slotDigest(data)
|
|
38
|
+
m.slots[n] = slotDigest(data, wroteMtimeMs)
|
|
35
39
|
saveManifest(cwd, m)
|
|
36
40
|
}
|
|
37
41
|
return { ok: true }
|