thincoder 0.12.62 → 0.12.64
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 +27 -0
- package/README.md +46 -49
- package/bin/thincoder.mjs +68 -34
- package/package.json +7 -6
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +257 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +60 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +35 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-plan.mjs +9 -0
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +18 -10
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +22 -59
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +15 -6
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +57 -23
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +6 -2
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
package/src/session.mjs
DELETED
|
@@ -1,492 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* session.mjs — session persistence (slot-based model)
|
|
3
|
-
* Each project (keyed by cwd hash) keeps unlimited session slots.
|
|
4
|
-
* Every session lives in a numbered slot; the manifest tracks the SHARED active pointer
|
|
5
|
-
* (2026-09-05 §10 D-6 修订:active = 旧版端/ACP 的恢复依据 + 无记录端的一次性继承源 +
|
|
6
|
-
* 列表回退高亮——本端恢复依据 = end marker `{manifest}.cli`,见 session-slots.mjs
|
|
7
|
-
* resumeSlot/readEndMarker,SESSION.md §1/§10)。
|
|
8
|
-
*
|
|
9
|
-
* File layout: {hash}.json.N (slots), {hash}.json.manifest (slot metadata + active pointer).
|
|
10
|
-
* Legacy {hash}.json is migrated to a slot on first access.
|
|
11
|
-
*
|
|
12
|
-
* 2026-08-31 advisor round1 🔴:slot/清单管理拆至 session-slots.mjs(本文件曾超 500 行
|
|
13
|
-
* 硬限);本文件只保留核心读写 + re-export 全部 slot 导出(既有 import 路径不变)。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
|
|
17
|
-
import { basename } from "node:path"
|
|
18
|
-
import { resolveCompactThreshold } from "./config.mjs"
|
|
19
|
-
import {
|
|
20
|
-
slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
|
|
21
|
-
activeSlot, getSessionId, isProcessAlive,
|
|
22
|
-
writeEndMarker, resumeSlot as slotsResumeSlot,
|
|
23
|
-
} from "./session-slots.mjs"
|
|
24
|
-
// F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
|
|
25
|
-
import { guardForeignSlotFile } from "./session-guard.mjs"
|
|
26
|
-
import { scheduleSessionGC } from "./session-gc.mjs"
|
|
27
|
-
import { engTokenSlotFields, restoreEngTokens } from "./token-ttl.mjs"
|
|
28
|
-
// TUI-OOM-ROOTCAUSE 批(SESSION.md §14):人读线记录存储(磁盘为准 + 内存窗口)。
|
|
29
|
-
// slimForDisplay/isLegacyTransient 迁入 store 模块(§14.5 文件表)——本文件 re-export 保持
|
|
30
|
-
// 既有 import 面(session-slots.mjs 引 isLegacyTransient)。依赖方向单向:store 零项目内依赖。
|
|
31
|
-
import {
|
|
32
|
-
bindRecordStore, unbindRecordStore, saveProjectedSlot, isLegacyTransient, slimForDisplay,
|
|
33
|
-
RECORD_WINDOW_MESSAGES,
|
|
34
|
-
} from "./session-store.mjs"
|
|
35
|
-
export { isLegacyTransient, slimForDisplay, bindRecordStore, unbindRecordStore }
|
|
36
|
-
|
|
37
|
-
// re-export slot 管理(保持既有 import session.mjs 的调用点不变;resumeSlot 下方本地包装导出)
|
|
38
|
-
export {
|
|
39
|
-
getSessionId, normalizeCwd, sessionPath, slotPath, manifestPath, activePath,
|
|
40
|
-
writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
|
|
41
|
-
deleteSlot, isProcessAlive, END, endMarkerPath,
|
|
42
|
-
readEndMarker, writeEndMarker, claimSlot, allocateFresh,
|
|
43
|
-
} from "./session-slots.mjs"
|
|
44
|
-
// §12.2.5 契约改使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§12.3 授权)
|
|
45
|
-
export { renameSlot } from "./session-rename.mjs"
|
|
46
|
-
|
|
47
|
-
/** 恢复决策包装(SESSION.md §12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
|
|
48
|
-
* scheduleSessionGC 内部 setImmediate 空闲执行 + 每进程每前缀去重,不阻塞启动路径(N4)。 */
|
|
49
|
-
export function resumeSlot(cwd) {
|
|
50
|
-
scheduleSessionGC(cwd)
|
|
51
|
-
return slotsResumeSlot(cwd)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ========== core read/write ==========
|
|
55
|
-
// (legacy transient 判定 + slimForDisplay 自本区迁至 session-store.mjs——§14.5 文件表;
|
|
56
|
-
// 上方 re-export 保持既有 import 面。)
|
|
57
|
-
|
|
58
|
-
/** 未绑定路径的人读线全量物化(模式 F——既有语义逐字保留)。 */
|
|
59
|
-
function legacyHistory(agent) {
|
|
60
|
-
return (agent._fullHistory ?? agent.history)
|
|
61
|
-
.filter((m) => !m.transient && !isLegacyTransient(m))
|
|
62
|
-
.map(slimForDisplay)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 恢复描述符(§14.3.6——两调用点统一形态):`{ history, total, base }`。
|
|
66
|
-
* 绑定态:history = 尾窗 + ±1 页沿头一条(跨页回合标签判定用——不渲染)、total = 绝对
|
|
67
|
-
* 总条数(「N messages」标签口径)、base = history[0] 的绝对序号;未绑定(模式 F):
|
|
68
|
-
* 回退全量数组(total = 数组长)。 */
|
|
69
|
-
export function sessionDescriptor(agent, data) {
|
|
70
|
-
const store = agent?._recordStore
|
|
71
|
-
if (store) {
|
|
72
|
-
const total = store.total()
|
|
73
|
-
const win = store.page(Math.max(0, total - RECORD_WINDOW_MESSAGES), total, { margin: 1 })
|
|
74
|
-
return { history: win.messages, total, base: win.base }
|
|
75
|
-
}
|
|
76
|
-
const full = Array.isArray(data?.history) ? data.history : []
|
|
77
|
-
return { history: full, total: full.length, base: 0 }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** 槽摘要(记录存储绑定路径——history 不物化):与 slotDigest(extractSlotMeta) 同形。 */
|
|
81
|
-
function digestFromStore(fields, counters) {
|
|
82
|
-
const meta = {
|
|
83
|
-
messageCount: counters.total,
|
|
84
|
-
turnCount: counters.userReal,
|
|
85
|
-
firstMessage: counters.firstMessage,
|
|
86
|
-
activeProvider: fields.activeProvider ?? "",
|
|
87
|
-
updatedAt: fields.updatedAt ?? Date.now(),
|
|
88
|
-
title: fields.title ?? "",
|
|
89
|
-
}
|
|
90
|
-
if (fields.activeModel) meta.activeModel = fields.activeModel
|
|
91
|
-
return { ts: Date.now(), ...meta }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Save agent state to the active slot file (atomic write). `display` (the old
|
|
95
|
-
* WYSIWYG render snapshot) is DEPRECATED — it drifted out of sync with history
|
|
96
|
-
* whenever VS Code wrote the slot, and the TUI resumed from a stale snapshot.
|
|
97
|
-
* Restore now always rebuilds from history (lazy, see startup.mjs).
|
|
98
|
-
* 2026-08-31 会诊 F1:slot 粘性——首次认领后缓存 agent._slot,永不重跑 ensureActive
|
|
99
|
-
* (原实现每次保存重推,manifest active 被并发方翻动时会话静默迁移)。
|
|
100
|
-
* 返回轮转的 .bak 路径或 null。 */
|
|
101
|
-
export function saveSession(agent) {
|
|
102
|
-
// _fullHistory is written at the source via pushReal — no flush needed here.
|
|
103
|
-
// history = FULL, never-compacted (human-readable; VS Code panel & CLI resume read this)
|
|
104
|
-
// contextHistory = machine context (possibly compacted) so CLI resume keeps the token savings
|
|
105
|
-
// Human line: transient machine injections never enter the readable record.
|
|
106
|
-
// TUI-OOM-ROOTCAUSE 批(SESSION.md §14.3.5):绑定记录存储后 `history` 字段由
|
|
107
|
-
// saveProjectedSlot 流式拼接段原文生成(磁盘为准——不物化全量数组);未绑定
|
|
108
|
-
// (模式 F:thincoder chat/测试/未覆盖路径)→ 既有全量物化路径逐字保留(D-R6)。
|
|
109
|
-
// Machine line (contextHistory): KEEP transient messages — resume must rebuild the
|
|
110
|
-
// machine line byte-identical to what the provider cache saw. Dropping them made every
|
|
111
|
-
// process restart diverge at the first injection position (git/OS/time reminders are
|
|
112
|
-
// re-injected with FRESH content) → whole-prefix cache miss on the CLI's very first
|
|
113
|
-
// request of each session (2026-08-16 cache-hit report; Kimi review).
|
|
114
|
-
const contextHistory = agent.history.filter((m) => !isLegacyTransient(m))
|
|
115
|
-
// fields = 槽 JSON 数据面(history 由投影插入——键序与既有 data 形态同序,history/contextHistory 居后)
|
|
116
|
-
const fields = {
|
|
117
|
-
version: 2,
|
|
118
|
-
cwd: agent.cwd,
|
|
119
|
-
title: agent.title ?? "",
|
|
120
|
-
// MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
|
|
121
|
-
// 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
|
|
122
|
-
activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
|
|
123
|
-
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
124
|
-
updatedAt: Date.now(),
|
|
125
|
-
tasks: agent.tasks ?? [],
|
|
126
|
-
planMode: agent.planMode ?? false,
|
|
127
|
-
autoApprove: agent.autoApprove ?? false,
|
|
128
|
-
engineering: agent.config?.agent?.engineering ?? false,
|
|
129
|
-
...engTokenSlotFields(agent),
|
|
130
|
-
goal: agent.goal ?? null,
|
|
131
|
-
advisor: agent.config?.advisor ?? null,
|
|
132
|
-
pendingReminders: agent._pendingReminders ?? [],
|
|
133
|
-
sessionStart: agent._sessionStart ?? null,
|
|
134
|
-
}
|
|
135
|
-
// 2026-09-05 §10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、“查看对方活槽 →
|
|
136
|
-
// 保存 fork 新槽”的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
|
|
137
|
-
// 的日常保存不写(F1:保存永不参与竞争)
|
|
138
|
-
const claimedNow = agent._slot == null
|
|
139
|
-
const slot = agent._slot ??= activeSlot(agent.cwd)
|
|
140
|
-
if (claimedNow) writeEndMarker(agent.cwd, slot)
|
|
141
|
-
const p = slotPath(agent.cwd, slot)
|
|
142
|
-
// 首保存补绑(§14.3.4 表末行——兜底所有未覆盖路径):未绑定且有槽 → 绑定记录存储
|
|
143
|
-
// (baseHistory = 当前 _fullHistory 全量;identity = 当前 _sessionStart)
|
|
144
|
-
if (!agent._recordStore) {
|
|
145
|
-
bindRecordStore(agent, { slotFile: p, identity: agent._sessionStart ?? null, baseHistory: agent._fullHistory ?? [] })
|
|
146
|
-
}
|
|
147
|
-
// 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
|
|
148
|
-
// 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
|
|
149
|
-
// 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
|
|
150
|
-
// SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
|
|
151
|
-
// saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
|
|
152
|
-
// mtime 缓存 _slotMtime:自写未变跳过全量解析)。
|
|
153
|
-
const rotated = guardForeignSlotFile(agent, p, slot)
|
|
154
|
-
// 绑定态 → 流式投影(段原文拼接——VSC 兼容面逐字同形);未绑定 → 既有全量物化写
|
|
155
|
-
if (agent._recordStore) saveProjectedSlot(agent, p, fields, contextHistory)
|
|
156
|
-
else writeSessionFile(p, { ...fields, history: legacyHistory(agent), contextHistory })
|
|
157
|
-
// 记录我们刚写的 mtime——下次保存跳过重复解析
|
|
158
|
-
try { agent._slotMtime = statSync(p).mtimeMs } catch {}
|
|
159
|
-
// Update slot metadata in manifest
|
|
160
|
-
try {
|
|
161
|
-
const m = loadManifest(agent.cwd)
|
|
162
|
-
m.slots[slot] = agent._recordStore
|
|
163
|
-
? digestFromStore(fields, agent._recordStore.counters())
|
|
164
|
-
: slotDigest({ ...fields, history: legacyHistory(agent) })
|
|
165
|
-
saveManifest(agent.cwd, m)
|
|
166
|
-
} catch (e) {
|
|
167
|
-
// Manifest update failure is non-fatal — data is safe, metadata will lazy-recover on next listSlots
|
|
168
|
-
console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
|
|
169
|
-
}
|
|
170
|
-
return rotated
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Load slot file with shared validation — 2026-08-31 会诊 deepseek 🟡 抽取:
|
|
174
|
-
* loadSession(active 槽)/switchToSlot/ACP session/load 共享。无认领副作用。
|
|
175
|
-
* version 1/2 + history 数组 + cwd 匹配(2026-09-01 会诊 kimi 🔵:cwd 先行——"别人的
|
|
176
|
-
* 文件不动"优先于结构校验,异 cwd + 坏 version 不得改名);结构不符改名 .unreadable、
|
|
177
|
-
* 解析失败 .tmp 回退成功后提升为正主(损坏主文件改名 .corrupted 保留)、主文件缺失
|
|
178
|
-
* 时恢复孤儿 .tmp(rename 前崩溃现场)。 */
|
|
179
|
-
export function loadSlotFile(cwd, slot) {
|
|
180
|
-
const p = slotPath(cwd, slot)
|
|
181
|
-
const tryLoad = (path) => {
|
|
182
|
-
try {
|
|
183
|
-
if (!existsSync(path)) return null
|
|
184
|
-
const data = JSON.parse(readFileSync(path, "utf8"))
|
|
185
|
-
if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null // 别人的文件,不动
|
|
186
|
-
if (data?.version !== 1 && data?.version !== 2) {
|
|
187
|
-
if (typeof data?.version === "number" && data.version > 2) return null // 新版 CLI 的文件,不动(F2 轮转兜底)
|
|
188
|
-
try { renameSync(path, `${path}.unreadable`) } catch {}
|
|
189
|
-
console.error(`[session] unsupported version ${data?.version} at ${path} — preserved as ${basename(path)}.unreadable`)
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
if (!Array.isArray(data.history)) {
|
|
193
|
-
try { renameSync(path, `${path}.unreadable`) } catch {}
|
|
194
|
-
console.error(`[session] slot file ${path}: history is not an array — preserved as ${basename(path)}.unreadable`)
|
|
195
|
-
return null
|
|
196
|
-
}
|
|
197
|
-
data.history = data.history.filter((m) => !isLegacyTransient(m))
|
|
198
|
-
return data
|
|
199
|
-
} catch (e) {
|
|
200
|
-
return { _error: e }
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
let result = tryLoad(p)
|
|
205
|
-
if (result && !result._error) return result
|
|
206
|
-
if (result?._error) {
|
|
207
|
-
console.error(`[session] failed to load slot ${slot}: ${result._error.message}. Trying .tmp fallback...`)
|
|
208
|
-
const tmpResult = tryLoad(`${p}.tmp`)
|
|
209
|
-
if (tmpResult && !tmpResult._error) {
|
|
210
|
-
console.error(`[session] recovered from .tmp fallback`)
|
|
211
|
-
// 2026-09-01 会诊 🟢:.tmp 提升为正主(损坏主文件改名 .corrupted 保留现场)——
|
|
212
|
-
// 否则主文件留在原地,每次加载都重复"恢复",且保存侧 F2 会持续轮转它。
|
|
213
|
-
try {
|
|
214
|
-
renameSync(p, `${p}.corrupted`)
|
|
215
|
-
renameSync(`${p}.tmp`, p)
|
|
216
|
-
} catch {}
|
|
217
|
-
return tmpResult
|
|
218
|
-
}
|
|
219
|
-
console.error(`[session] .tmp fallback also failed — session lost.`)
|
|
220
|
-
try { renameSync(p, `${p}.corrupted`) } catch {}
|
|
221
|
-
} else if (!result) {
|
|
222
|
-
// 2026-09-01 advisor 🔵:主文件缺失但孤儿 .tmp 存在(原子写 rename 前崩溃)——
|
|
223
|
-
// 恢复并提升为正主(SESSION.md §4 的 .tmp 回退语义应覆盖此场景)。
|
|
224
|
-
const orphan = tryLoad(`${p}.tmp`)
|
|
225
|
-
if (orphan && !orphan._error) {
|
|
226
|
-
console.error(`[session] slot ${slot} main file missing — recovered orphan .tmp`)
|
|
227
|
-
try { renameSync(`${p}.tmp`, p) } catch {}
|
|
228
|
-
return orphan
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return null
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Load session data for this cwd — 2026-09-05 §10 D-2:平移为 resumeSlot 的数据包装
|
|
235
|
-
* (签名不变——测试兼容;读槽校验/.tmp 回退/legacy 兜底语义原样保留在 resumeSlot 的
|
|
236
|
-
* data 层)。恢复目标选择按本端记录(end marker)——manifest active 仅作无记录端的
|
|
237
|
-
* 一次性继承源(D-6),不再作本端恢复第一依据。 */
|
|
238
|
-
export function loadSession(cwd) {
|
|
239
|
-
return resumeSlot(cwd).data
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
|
|
243
|
-
* v1 老文件回退播种机器线时置为 {}(合法空参数),防止半截 \\uXXXX 毒化发送载荷(会诊 F6)。 */
|
|
244
|
-
function stripTruncatedToolArgs(m) {
|
|
245
|
-
if (m?.role !== "assistant" || !Array.isArray(m.tool_calls)) return m
|
|
246
|
-
let changed = false
|
|
247
|
-
const tool_calls = m.tool_calls.map((tc) => {
|
|
248
|
-
const args = tc?.function?.arguments
|
|
249
|
-
if (typeof args === "string" && args.endsWith("…")) {
|
|
250
|
-
changed = true
|
|
251
|
-
return { ...tc, function: { ...tc.function, arguments: "{}" } }
|
|
252
|
-
}
|
|
253
|
-
return tc
|
|
254
|
-
})
|
|
255
|
-
return changed ? { ...m, tool_calls } : m
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Apply loaded session data onto an agent object; returns true if the runtime composite
|
|
259
|
-
* was switched. 2026-08-31 会诊 F1:清空 _slot/_slotMtime 缓存——切换后下次保存重新认领。
|
|
260
|
-
*
|
|
261
|
-
* MODEL-MERGE-SESSION(F-4)applySession 重写为两支 + 删旧支(评审 #7/#9 措辞——防
|
|
262
|
-
* "三支"误读):
|
|
263
|
-
* ① 槽 provider 存在 → provider/model 按槽值设(双字段恒非空——模型取 data.activeModel,
|
|
264
|
-
* legacy 槽 null/空串回该渠道默认模型 `providers[].model`(单值——2026-09-10 MODEL-SELECTION
|
|
265
|
-
* v2;老 models[] 首候选形态已退场))+ 重算
|
|
266
|
-
* compactThreshold(auto 时——按恢复后模型)——不看 config(defaultModel 只是新会话起点);
|
|
267
|
-
* ② 槽 provider 没了 → D-S3 保留——静默保持现状(config 有效则有效——两方都无效由调用侧
|
|
268
|
-
* 复验 validateProvider 弹重选)。
|
|
269
|
-
* 删旧支:"activeModel==null 清 stale override 回渠道默认"——前提 = 渠道默认字段——已随
|
|
270
|
-
* 三旧层删除消失——双字段恒非空故不可能触发(评审 #7)。 */
|
|
271
|
-
export function applySession(agent, data, opts = {}) {
|
|
272
|
-
// 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
|
|
273
|
-
// 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
|
|
274
|
-
// prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
|
|
275
|
-
// tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
|
|
276
|
-
// 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
|
|
277
|
-
agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
|
|
278
|
-
const full = Array.isArray(data.history) ? data.history : []
|
|
279
|
-
// SESSION.md §11.2(2026-09-08——F2/F3):恢复事件按当前 data 重定——载入历史非空 = 会话
|
|
280
|
-
// 恢复 → 下个 depth-0 回合 env-state resumed:yes 一次(pushEnvStateReminder 读即清——每次恢复
|
|
281
|
-
// 一次)。/new 与空历史槽切换不武装(显式清 false)——无恢复事件恒 no(F3 伪触发消除——不再靠
|
|
282
|
-
// _sessionStart != null 推断;已武装 agent 切到空历史槽也不残留上次恢复事件——评审 round2 🟡)。
|
|
283
|
-
// _processRestartPending 不在此清——由启动路径(bin/thincoder.mjs)设、prepareRun 发句清
|
|
284
|
-
// (N6——双信号独立:切槽不报进程重启)。
|
|
285
|
-
// 跨端异名互指(结构债批 5 N7——双端同语义各自独立实现、命名不统一是刻意——SESSION.md
|
|
286
|
-
// §11.2):VSC 端同机制载体异名 = agent._resumedPending(src/agent/setup.mjs hydrateRun——
|
|
287
|
-
// restore:true 工厂 + fullHistory 非空时武装——每槽恢复一次)+ 模块级 restartDetectionDone
|
|
288
|
-
// 一次性闸(src/agent/setup-reminders.mjs——process restarted 句的进程级信号)。
|
|
289
|
-
agent._envResumed = full.length > 0
|
|
290
|
-
const ch = data.contextHistory
|
|
291
|
-
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
292
|
-
// 绑定态(opts.slot 在场):人读线 = 记录存储尾窗(下方绑定块填充)——不复制全量 JSON 数组
|
|
293
|
-
agent._fullHistory = opts.slot != null ? [] : [...full]
|
|
294
|
-
agent.history = [...machine]
|
|
295
|
-
agent.title = data.title ?? ""
|
|
296
|
-
agent.tasks = data.tasks ?? []
|
|
297
|
-
agent.planMode = data.planMode ?? false
|
|
298
|
-
agent.autoApprove = data.autoApprove ?? false
|
|
299
|
-
agent.goal = data.goal ?? null
|
|
300
|
-
agent._pendingReminders = data.pendingReminders ?? []
|
|
301
|
-
agent._sessionStart = data.sessionStart ?? null
|
|
302
|
-
// R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
|
|
303
|
-
restoreEngTokens(agent, data)
|
|
304
|
-
// engineering is session-level (2026-08-29): the slot value is the CLI session's authority
|
|
305
|
-
// — config.json is only the initial default / cross-end mirror. A legacy slot without the
|
|
306
|
-
// field keeps whatever config.json seeded (unchanged behavior).
|
|
307
|
-
if (data.engineering !== undefined) {
|
|
308
|
-
agent.config.agent ??= {}
|
|
309
|
-
agent.config.agent.engineering = data.engineering === true
|
|
310
|
-
}
|
|
311
|
-
if (data.advisor) {
|
|
312
|
-
agent.config.advisor = { ...data.advisor }
|
|
313
|
-
}
|
|
314
|
-
// Reset stall/compaction state on session switch
|
|
315
|
-
agent._compressFailures = 0
|
|
316
|
-
agent._verifyRetries = 0
|
|
317
|
-
agent._verifyPassed = false
|
|
318
|
-
agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
|
|
319
|
-
agent._slotMtime = null
|
|
320
|
-
// ── 记录存储绑定(TUI-OOM-ROOTCAUSE——§14.3.4/§14.3.5)──
|
|
321
|
-
// opts.slot 在场(启动恢复 / 非占用的 /session 切换 / ACP 钉槽)→ 绑定(身份核验 + 对账 +
|
|
322
|
-
// 窗口)——人读线 = store.tail(200);未传(模式 F:被他人活进程占用的槽 / 测试 / ACP fork)
|
|
323
|
-
// → 解绑——人读线维持全量数组(D-R6 零回归)。
|
|
324
|
-
if (opts.slot != null) {
|
|
325
|
-
const store = bindRecordStore(agent, {
|
|
326
|
-
slotFile: slotPath(agent.cwd, opts.slot),
|
|
327
|
-
identity: data.sessionStart ?? null,
|
|
328
|
-
baseHistory: full,
|
|
329
|
-
})
|
|
330
|
-
agent._fullHistory = store.tail(RECORD_WINDOW_MESSAGES)
|
|
331
|
-
} else {
|
|
332
|
-
unbindRecordStore(agent)
|
|
333
|
-
}
|
|
334
|
-
// ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
|
|
335
|
-
const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
336
|
-
if (slotProvider) {
|
|
337
|
-
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
|
|
338
|
-
// null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
|
|
339
|
-
// F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
|
|
340
|
-
// 空槽 model 恒有值,provider.model 键不缺失)
|
|
341
|
-
const prevName = agent.activeProvider
|
|
342
|
-
const prevModel = agent.activeModel ?? null
|
|
343
|
-
const slotModel = data.activeModel || slotProvider.model
|
|
344
|
-
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
345
|
-
agent.activeProvider = slotProvider.name
|
|
346
|
-
agent.activeModel = slotModel
|
|
347
|
-
agent.provider = { ...slotProvider }
|
|
348
|
-
if (slotModel) agent.provider.model = slotModel
|
|
349
|
-
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
|
|
350
|
-
if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
|
|
351
|
-
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
352
|
-
}
|
|
353
|
-
return switched
|
|
354
|
-
}
|
|
355
|
-
// ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
|
|
356
|
-
// config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
|
|
357
|
-
return false
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Create a new session slot: allocate a free slot number,
|
|
362
|
-
* write an empty session, and mark it as the active slot.
|
|
363
|
-
* No limit on the number of sessions.
|
|
364
|
-
*/
|
|
365
|
-
export function newSession(cwd) {
|
|
366
|
-
const m = loadManifest(cwd)
|
|
367
|
-
|
|
368
|
-
// 2026-09-01 advisor 🔵:先清理死主条目(与 ensureActive 分支2 语义一致)——否则
|
|
369
|
-
// "死主 + 文件缺失"的空槽(m.slots 有条目、无文件、属主已死)永不复用,槽号持续
|
|
370
|
-
// 增长。死主且无文件 = 该会话从未落盘(进程死了没保存),条目可安全删除回收。
|
|
371
|
-
// 2026-09-01 会诊 deepseek/glm 🟡:清理必须经 deletions 显式落盘——saveManifest 条目级
|
|
372
|
-
// 合并会把磁盘死条目从 fresh 复活回写,仅传 m 等于没删(VS Code newSlot 已修,CLI 对称)。
|
|
373
|
-
const mySessionId = getSessionId()
|
|
374
|
-
const deadSlots = []
|
|
375
|
-
for (const [s, owner] of Object.entries(m.slotSessions ?? {})) {
|
|
376
|
-
if (owner && owner !== mySessionId) {
|
|
377
|
-
const pid = parseInt(owner.split("-")[0])
|
|
378
|
-
if (!pid || !isProcessAlive(pid)) {
|
|
379
|
-
delete m.slotSessions[s]
|
|
380
|
-
if (!existsSync(slotPath(cwd, Number(s)))) delete m.slots[s]
|
|
381
|
-
deadSlots.push(s)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Find next available slot number — 2026-08-31 会诊 deepseek 🟡:不能只看 manifest
|
|
387
|
-
// 条目(丢失更新可能让条目消失而文件仍在)——复用该号会直接覆写真实会话
|
|
388
|
-
// (F2 防护不覆盖 newSession 的空数据直写)。
|
|
389
|
-
// 2026-08-31 advisor round2 🟡:同时跳过"已被另一活进程认领但尚未落盘"的号
|
|
390
|
-
// (slotSessions 有条目、slots 无条目、文件不存在)——否则双进程会认领同一号。
|
|
391
|
-
const liveClaimed = (n) => {
|
|
392
|
-
const owner = m.slotSessions?.[n]
|
|
393
|
-
return !!(owner && owner !== mySessionId && isProcessAlive(parseInt(owner.split("-")[0])))
|
|
394
|
-
}
|
|
395
|
-
let slot = 1
|
|
396
|
-
while (m.slots[slot] || existsSync(slotPath(cwd, slot)) || liveClaimed(slot)) slot++
|
|
397
|
-
|
|
398
|
-
// Write empty session — 2026-09-01 advisor 🔵:补 contextHistory/planMode 字段与
|
|
399
|
-
// VS Code newSlot 对齐(SESSION.md §3 v2 格式双端一致;两端读侧均有兜底,功能等价)
|
|
400
|
-
const data = { version: 2, cwd, title: "", updatedAt: Date.now(), history: [], contextHistory: [], tasks: [], planMode: false, goal: null, autoApprove: false, advisor: null, pendingReminders: [], sessionStart: null }
|
|
401
|
-
writeSessionFile(slotPath(cwd, slot), data)
|
|
402
|
-
m.slots[slot] = slotDigest(data)
|
|
403
|
-
m.active = slot
|
|
404
|
-
// 2026-08-31 advisor round1 🟡:与 ensureActive 认领模型一致——立即记录新 slot 所有权,
|
|
405
|
-
// 否则 /new 后到首次保存之间并发方(VS Code/另一 CLI)会把新 active 槽当"空闲可恢复"
|
|
406
|
-
// 认领 → 双进程写同一槽(F2 轮转互旋)。
|
|
407
|
-
m.slotSessions ??= {}
|
|
408
|
-
m.slotSessions[slot] = mySessionId
|
|
409
|
-
// 2026-09-01 会诊三家 🟡:显式翻 active 的调用点传 setActive(saveManifest 默认保留
|
|
410
|
-
// 磁盘 fresh.active,避免把并发方刚翻的指针回滚)
|
|
411
|
-
// 2026-09-01 会诊 deepseek/glm 🟡:deletions 过滤掉本调用刚重新认领的槽(防删掉自己的
|
|
412
|
-
// 新属主)——与 ensureActive deadParam / VS Code newSlot 同型。
|
|
413
|
-
const deletions = deadSlots.length
|
|
414
|
-
? {
|
|
415
|
-
slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId),
|
|
416
|
-
slots: deadSlots.filter((s) => !m.slots[s]),
|
|
417
|
-
}
|
|
418
|
-
: null
|
|
419
|
-
saveManifest(cwd, m, deletions, { setActive: true })
|
|
420
|
-
// 2026-09-05 §10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
|
|
421
|
-
writeEndMarker(cwd, slot)
|
|
422
|
-
return slot
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/** 清空会话运行态(/new 用,2026-08-31 会诊 F3):_fullHistory/title/_sessionStart 等
|
|
426
|
-
* 全部会话级状态 + 一次性注入标志必须全清——否则新会话首次落盘把旧会话完整人类线 +
|
|
427
|
-
* 旧标题写进新槽(实锤 .19/.3 双副本);注入标志不清则 /new 后新会话永不注入
|
|
428
|
-
* OS/cwd reminder(2026-09-01 会诊 glm 🟡)。autoApprove 是用户偏好,跨会话保留(有意)。 */
|
|
429
|
-
export function resetSessionState(agent) {
|
|
430
|
-
agent._fullHistory = []
|
|
431
|
-
agent.history = []
|
|
432
|
-
agent.title = ""
|
|
433
|
-
agent.tasks = []
|
|
434
|
-
agent._sessionStart = null
|
|
435
|
-
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
|
|
436
|
-
agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
|
|
437
|
-
agent._compressFailures = 0
|
|
438
|
-
agent._verifyRetries = 0
|
|
439
|
-
agent._verifyPassed = undefined
|
|
440
|
-
agent._runStartHistoryLen = 0
|
|
441
|
-
agent._lastPromptTokens = null
|
|
442
|
-
agent._usageAtLen = null
|
|
443
|
-
agent.planMode = false
|
|
444
|
-
agent.goal = null
|
|
445
|
-
agent._pendingReminders = []
|
|
446
|
-
agent._slot = null
|
|
447
|
-
agent._slotMtime = null
|
|
448
|
-
agent._osReminderInjected = false
|
|
449
|
-
// §11.2(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
|
|
450
|
-
// _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
|
|
451
|
-
agent._envResumed = false
|
|
452
|
-
agent._processRestartPending = false
|
|
453
|
-
agent._lastEngState = false
|
|
454
|
-
// /new 或切换:记录存储解绑(新槽由 cmd-new / 首保存补绑重新挂载——§14.3.4)
|
|
455
|
-
unbindRecordStore(agent)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/** Switch the manifest active pointer to a slot. Returns the slot's session data
|
|
459
|
-
* (null if the slot doesn't exist / can't be read). 2026-08-31 会诊 deepseek 🔴:
|
|
460
|
-
* 原实现经 loadSession → activeSlot 有认领副作用——目标槽被活进程占用时 ensureActive
|
|
461
|
-
* 分支 3 会把 active 拨到新空槽并读回 null + 劫持对方指针。现改为 loadSlotFile 直接读
|
|
462
|
-
* (无认领副作用);目标槽空闲则一并认领、被另一活进程占用则不认领(slotOccupancy——
|
|
463
|
-
* 下次保存经 activeSlot 自然 fork 到新槽);只改 manifest 指针(setActive 意图)。
|
|
464
|
-
* 2026-09-01 会诊三家 🟡:saveManifest 条目级合并 + setActive(不把并发方刚翻的指针回滚)。 */
|
|
465
|
-
export function switchToSlot(cwd, slot) {
|
|
466
|
-
const m = loadManifest(cwd)
|
|
467
|
-
if (!m.slots[slot]) return null
|
|
468
|
-
const data = loadSlotFile(cwd, slot)
|
|
469
|
-
if (!data) return null
|
|
470
|
-
m.active = slot
|
|
471
|
-
const occ = slotOccupancy(cwd, slot)
|
|
472
|
-
if (!occ.occupied) {
|
|
473
|
-
m.slotSessions ??= {}
|
|
474
|
-
m.slotSessions[slot] = getSessionId()
|
|
475
|
-
}
|
|
476
|
-
saveManifest(cwd, m, null, { setActive: true })
|
|
477
|
-
// 2026-09-05 §10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
|
|
478
|
-
writeEndMarker(cwd, slot)
|
|
479
|
-
return data
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** 目标槽是否被另一活进程占用(2026-09-01 会诊/advisor 🟡):排除本进程属主——
|
|
483
|
-
* /session 重选当前槽不误报;同进程双会话防护由 ACP sameProcessPinned 承担。 */
|
|
484
|
-
export function slotOccupancy(cwd, slot) {
|
|
485
|
-
const m = loadManifest(cwd)
|
|
486
|
-
const owner = m.slotSessions?.[slot]
|
|
487
|
-
if (!owner) return { occupied: false }
|
|
488
|
-
if (owner === getSessionId()) return { occupied: false }
|
|
489
|
-
const pid = parseInt(owner.split("-")[0])
|
|
490
|
-
if (!pid || !isProcessAlive(pid)) return { occupied: false }
|
|
491
|
-
return { occupied: true, owner }
|
|
492
|
-
}
|
package/src/skills.mjs
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* skills.mjs — skill system
|
|
3
|
-
* Discovers .md skill files AND subdirectory SKILL.md from .thincoder/skills/ directory,
|
|
4
|
-
* injects into system prompt for agent to load on demand.
|
|
5
|
-
* Use the skill tool to activate a specific skill; content is written into conversation history wrapped in <skill-loaded>.
|
|
6
|
-
*
|
|
7
|
-
* Supported formats:
|
|
8
|
-
* .thincoder/skills/my-skill.md (flat, name = "my-skill")
|
|
9
|
-
* .thincoder/skills/my-skill/SKILL.md (subdirectory, name = "my-skill")
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readFile, readdir, stat } from "node:fs/promises"
|
|
13
|
-
import { join } from "node:path"
|
|
14
|
-
import { homedir } from "node:os"
|
|
15
|
-
|
|
16
|
-
/** Valid skill name pattern (alphanumeric + hyphens/underscores) */
|
|
17
|
-
const NAME_RE = /^[a-zA-Z0-9_-]+$/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Try to read a skill from a path, return { name, path, description } or null.
|
|
21
|
-
*/
|
|
22
|
-
async function tryReadSkill(dir, name, filePath) {
|
|
23
|
-
try {
|
|
24
|
-
const s = await stat(filePath)
|
|
25
|
-
if (!s.isFile()) return null
|
|
26
|
-
const head = await readFile(filePath, "utf8")
|
|
27
|
-
const body = head.slice(0, 400).split("\n")
|
|
28
|
-
let desc = ""
|
|
29
|
-
let inFrontmatter = false
|
|
30
|
-
for (const line of body) {
|
|
31
|
-
const t = line.trim()
|
|
32
|
-
if (t === "---") { inFrontmatter = !inFrontmatter; continue }
|
|
33
|
-
if (inFrontmatter) continue
|
|
34
|
-
if (t && !t.startsWith("#")) {
|
|
35
|
-
desc = t.slice(0, 120)
|
|
36
|
-
break
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return { name, path: filePath, description: desc || "(no description)" }
|
|
40
|
-
} catch {
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Scan a single skills directory, return skill list.
|
|
47
|
-
* Supports flat .md files and subdirectories with SKILL.md inside.
|
|
48
|
-
* Each skill: { name, path, description } — name derived from filename or directory.
|
|
49
|
-
* Returns empty array if directory is missing or empty.
|
|
50
|
-
*/
|
|
51
|
-
async function loadSkillsFromDir(dir) {
|
|
52
|
-
let entries
|
|
53
|
-
try {
|
|
54
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
55
|
-
// Deterministic order: readdir order is filesystem-dependent; an unsorted scan would
|
|
56
|
-
// reshuffle the skills listing → system prompt byte change with zero content change
|
|
57
|
-
// (2026-08-16 cache audit — that silently misses the provider prefix cache).
|
|
58
|
-
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
59
|
-
} catch {
|
|
60
|
-
return []
|
|
61
|
-
}
|
|
62
|
-
const skills = []
|
|
63
|
-
const added = new Set()
|
|
64
|
-
|
|
65
|
-
// Pass 1: subdirectories (higher priority — standard convention)
|
|
66
|
-
for (const entry of entries) {
|
|
67
|
-
if (!entry.isDirectory()) continue
|
|
68
|
-
if (!NAME_RE.test(entry.name)) continue
|
|
69
|
-
const skill = await tryReadSkill(dir, entry.name, join(dir, entry.name, "SKILL.md"))
|
|
70
|
-
if (skill) { skills.push(skill); added.add(entry.name) }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Pass 2: flat .md files (backward compat; skipped if subdirectory with same name exists)
|
|
74
|
-
for (const entry of entries) {
|
|
75
|
-
if (!entry.isFile()) continue
|
|
76
|
-
const m = entry.name.match(/^([a-zA-Z0-9_-]+)\.md$/)
|
|
77
|
-
if (!m) continue
|
|
78
|
-
const name = m[1]
|
|
79
|
-
if (added.has(name)) continue
|
|
80
|
-
const skill = await tryReadSkill(dir, name, join(dir, entry.name))
|
|
81
|
-
if (skill) { skills.push(skill); added.add(name) }
|
|
82
|
-
}
|
|
83
|
-
return skills
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Scan both project-level and user-level skills directories.
|
|
88
|
-
* Project-level skills (cwd/.thincoder/skills/) take priority over user-level (~/.thincoder/skills/).
|
|
89
|
-
* Returns merged skill list with project-level skills first.
|
|
90
|
-
*/
|
|
91
|
-
export async function loadSkills(cwd) {
|
|
92
|
-
const projectDir = join(cwd, ".thincoder", "skills")
|
|
93
|
-
const userDir = join(homedir(), ".thincoder", "skills")
|
|
94
|
-
|
|
95
|
-
// Load project-level skills first (higher priority)
|
|
96
|
-
const projectSkills = await loadSkillsFromDir(projectDir)
|
|
97
|
-
const added = new Set(projectSkills.map(s => s.name))
|
|
98
|
-
|
|
99
|
-
// Load user-level skills, skipping duplicates
|
|
100
|
-
const userSkills = await loadSkillsFromDir(userDir)
|
|
101
|
-
for (const skill of userSkills) {
|
|
102
|
-
if (!added.has(skill.name)) {
|
|
103
|
-
projectSkills.push(skill)
|
|
104
|
-
added.add(skill.name)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return projectSkills
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Generate skill listing text for system prompt injection.
|
|
113
|
-
* At most 3 (small footprint); overflow marked "... and N more".
|
|
114
|
-
* Prefixed with DISREGARD: when listing refreshes (skills added/removed), old listings are auto-invalidated without needing to delete history (inspired by kimi-code).
|
|
115
|
-
*/
|
|
116
|
-
export function formatSkillListing(skills) {
|
|
117
|
-
if (skills.length === 0) return ""
|
|
118
|
-
const listed = skills.slice(0, 3)
|
|
119
|
-
const lines = listed.map((s) => `- **${s.name}**: ${s.description}`)
|
|
120
|
-
if (skills.length > 3) lines.push(` ... and ${skills.length - 3} more`)
|
|
121
|
-
return "DISREGARD any earlier skill listings. Current available skills (use the skill tool to load one):\n" + lines.join("\n")
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Read the full content of a specific skill file.
|
|
126
|
-
* Tries project-level directory first, then user-level directory.
|
|
127
|
-
* For each directory, tries subdirectory format (name/SKILL.md) first, then flat format (name.md).
|
|
128
|
-
* Returns text, or null if not found.
|
|
129
|
-
*/
|
|
130
|
-
export async function readSkill(cwd, name) {
|
|
131
|
-
if (!NAME_RE.test(name)) return null
|
|
132
|
-
|
|
133
|
-
const dirs = [
|
|
134
|
-
join(cwd, ".thincoder", "skills"),
|
|
135
|
-
join(homedir(), ".thincoder", "skills")
|
|
136
|
-
]
|
|
137
|
-
|
|
138
|
-
for (const baseDir of dirs) {
|
|
139
|
-
// Try subdirectory format: name/SKILL.md
|
|
140
|
-
try {
|
|
141
|
-
const p = join(baseDir, name, "SKILL.md")
|
|
142
|
-
return await readFile(p, "utf8")
|
|
143
|
-
} catch { /* not found, try flat */ }
|
|
144
|
-
|
|
145
|
-
// Fallback to flat format: name.md
|
|
146
|
-
try {
|
|
147
|
-
const p = join(baseDir, `${name}.md`)
|
|
148
|
-
return await readFile(p, "utf8")
|
|
149
|
-
} catch { /* not found, try next directory */ }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null
|
|
153
|
-
}
|