thincoder 0.12.59 → 0.12.61
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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/session.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session.mjs — session persistence (slot-based model)
|
|
3
3
|
* Each project (keyed by cwd hash) keeps unlimited session slots.
|
|
4
|
-
* Every session lives in a numbered slot; the manifest tracks
|
|
5
|
-
*
|
|
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)。
|
|
6
8
|
*
|
|
7
9
|
* File layout: {hash}.json.N (slots), {hash}.json.manifest (slot metadata + active pointer).
|
|
8
10
|
* Legacy {hash}.json is migrated to a slot on first access.
|
|
@@ -13,17 +15,33 @@
|
|
|
13
15
|
|
|
14
16
|
import { readFileSync, renameSync, existsSync, statSync } from "node:fs"
|
|
15
17
|
import { basename } from "node:path"
|
|
18
|
+
import { resolveCompactThreshold } from "./config.mjs"
|
|
16
19
|
import {
|
|
17
20
|
slotPath, writeSessionFile, loadManifest, saveManifest, slotDigest,
|
|
18
|
-
activeSlot,
|
|
21
|
+
activeSlot, getSessionId, isProcessAlive,
|
|
22
|
+
writeEndMarker, resumeSlot as slotsResumeSlot,
|
|
19
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"
|
|
20
28
|
|
|
21
|
-
// re-export slot 管理(保持既有 import session.mjs
|
|
29
|
+
// re-export slot 管理(保持既有 import session.mjs 的调用点不变;resumeSlot 下方本地包装导出)
|
|
22
30
|
export {
|
|
23
31
|
getSessionId, normalizeCwd, sessionPath, slotPath, manifestPath, activePath,
|
|
24
32
|
writeSessionFile, slotDigest, loadManifest, saveManifest, activeSlot, listSlots,
|
|
25
|
-
deleteSlot,
|
|
33
|
+
deleteSlot, isProcessAlive, END, endMarkerPath,
|
|
34
|
+
readEndMarker, writeEndMarker, claimSlot, allocateFresh,
|
|
26
35
|
} from "./session-slots.mjs"
|
|
36
|
+
// §12.2.5 契约改使 session-slots.mjs 超 500 行硬限 → renameSlot 拆至 session-rename.mjs(§12.3 授权)
|
|
37
|
+
export { renameSlot } from "./session-rename.mjs"
|
|
38
|
+
|
|
39
|
+
/** 恢复决策包装(SESSION.md §12 启动钩子,2026-09-06):本端恢复入口触发一次残留 GC——
|
|
40
|
+
* scheduleSessionGC 内部 setImmediate 空闲执行 + 每进程每前缀去重,不阻塞启动路径(N4)。 */
|
|
41
|
+
export function resumeSlot(cwd) {
|
|
42
|
+
scheduleSessionGC(cwd)
|
|
43
|
+
return slotsResumeSlot(cwd)
|
|
44
|
+
}
|
|
27
45
|
|
|
28
46
|
// ========== legacy transient prefix cleanup ==========
|
|
29
47
|
|
|
@@ -106,8 +124,10 @@ export function saveSession(agent) {
|
|
|
106
124
|
version: 2,
|
|
107
125
|
cwd: agent.cwd,
|
|
108
126
|
title: agent.title ?? "",
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
// MODEL-MERGE-SESSION 槽双字段恒非空(裁定 a):有 provider 即携带具体复合值——
|
|
128
|
+
// 恢复不看 config(F-4)——无渠道默认可回落 ""/null(全新未配态——读侧容忍)
|
|
129
|
+
activeProvider: agent.activeProvider ?? agent.provider?.name ?? "",
|
|
130
|
+
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
111
131
|
updatedAt: Date.now(),
|
|
112
132
|
history,
|
|
113
133
|
contextHistory,
|
|
@@ -115,60 +135,26 @@ export function saveSession(agent) {
|
|
|
115
135
|
planMode: agent.planMode ?? false,
|
|
116
136
|
autoApprove: agent.autoApprove ?? false,
|
|
117
137
|
engineering: agent.config?.agent?.engineering ?? false,
|
|
118
|
-
|
|
119
|
-
// Multi-design slots ride the same round-trip (2026-09-01 audit #1): Map → {designId: token}
|
|
120
|
-
// (JSON-safe). Empty/absent Map → undefined → the key is dropped by JSON.stringify, so a
|
|
121
|
-
// cleared session writes NO field instead of resurrecting slots from the previous save.
|
|
122
|
-
engDesignTokens: agent._engDesignTokens instanceof Map && agent._engDesignTokens.size > 0
|
|
123
|
-
? Object.fromEntries(agent._engDesignTokens)
|
|
124
|
-
: undefined,
|
|
138
|
+
...engTokenSlotFields(agent),
|
|
125
139
|
goal: agent.goal ?? null,
|
|
126
140
|
advisor: agent.config?.advisor ?? null,
|
|
127
141
|
pendingReminders: agent._pendingReminders ?? [],
|
|
128
142
|
sessionStart: agent._sessionStart ?? null,
|
|
129
143
|
}
|
|
144
|
+
// 2026-09-05 §10 D-4:首认领(_slot 为 null——如 /session 切换后首保存、"查看对方活槽 →
|
|
145
|
+
// 保存 fork 新槽"的落盘槽跟随)写本端记录——marker = 端内最后认领者;粘性 _slot 期间
|
|
146
|
+
// 的日常保存不写(F1:保存永不参与竞争)
|
|
147
|
+
const claimedNow = agent._slot == null
|
|
130
148
|
const slot = agent._slot ??= activeSlot(agent.cwd)
|
|
149
|
+
if (claimedNow) writeEndMarker(agent.cwd, slot)
|
|
131
150
|
const p = slotPath(agent.cwd, slot)
|
|
132
151
|
// 2026-08-31 会诊 F2 🔴:写前校验磁盘文件的 sessionStart——与本进程会话不符(另一
|
|
133
152
|
// 进程/会话的现场)→ 先轮转 .bak 保留再写(11311 条历史被新进程覆盖的实锤场景)。
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const st = statSync(p)
|
|
140
|
-
let disk = null
|
|
141
|
-
if (st.mtimeMs !== (agent._slotMtime ?? -1)) {
|
|
142
|
-
disk = JSON.parse(readFileSync(p, "utf8"))
|
|
143
|
-
// 2026-08-31 会诊 deepseek 🟡:version>2 的新版文件无论 sessionStart 一律轮转
|
|
144
|
-
// (loadSlotFile 对 v3 返回 null 不动文件——若其 sessionStart 为 null,旧版首次
|
|
145
|
-
// 保存会静默覆盖;轮转 .bak 保证新版文件保留)。
|
|
146
|
-
// 2026-09-01 advisor 🔵:磁盘文件 cwd 不匹配(异项目文件误落本路径)同样轮转——
|
|
147
|
-
// 与 loadSlotFile/legacy 读的"别人的文件不动"原则对齐(否则 sessionStart null +
|
|
148
|
-
// version≤2 的异项目文件被静默覆盖且无 .bak)。
|
|
149
|
-
const diskIsNewer = typeof disk?.version === "number" && disk.version > 2
|
|
150
|
-
const diskStart = disk?.sessionStart ?? null
|
|
151
|
-
const myStart = agent._sessionStart ?? null
|
|
152
|
-
const diskForeign = typeof disk?.cwd === "string" && disk.cwd.toLowerCase() !== agent.cwd.toLowerCase()
|
|
153
|
-
if (diskIsNewer || diskForeign || (diskStart && diskStart !== myStart)) {
|
|
154
|
-
const bak = `${p}.bak-${Date.now()}`
|
|
155
|
-
renameSync(p, bak)
|
|
156
|
-
rotated = bak
|
|
157
|
-
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)}`)
|
|
158
|
-
}
|
|
159
|
-
agent._slotMtime = st.mtimeMs
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
// 文件存在但不可读(损坏/半写):改名 .corrupted 保留现场(2026-09-01 advisor 🔵——
|
|
164
|
-
// 与自身 loadSlotFile 的 .corrupted 约定 + VS Code saveSessionToSlot 对齐;.bak 保留
|
|
165
|
-
// 给 F2 轮转路径,损坏现场不再混入轮转后缀,恢复/清理工具按后缀分类不误判)
|
|
166
|
-
if (existsSync(p)) {
|
|
167
|
-
try {
|
|
168
|
-
renameSync(p, `${p}.corrupted`)
|
|
169
|
-
} catch {}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
153
|
+
// 守卫自 2026-09-08 提取为 guardForeignSlotFile(session-guard.mjs——DESIGN-TOKEN-
|
|
154
|
+
// SETTLEMENT D1 二次拆分:先入 session-slots、再因 500 行硬限迁 session-guard)——
|
|
155
|
+
// saveSession 与 token-ttl persistEngTokens(settle 当场落盘)共用同一份(检查按
|
|
156
|
+
// mtime 缓存 _slotMtime:自写未变跳过全量解析)。
|
|
157
|
+
const rotated = guardForeignSlotFile(agent, p, slot)
|
|
172
158
|
writeSessionFile(p, data)
|
|
173
159
|
// 记录我们刚写的 mtime——下次保存跳过重复解析
|
|
174
160
|
try { agent._slotMtime = statSync(p).mtimeMs } catch {}
|
|
@@ -245,36 +231,12 @@ export function loadSlotFile(cwd, slot) {
|
|
|
245
231
|
return null
|
|
246
232
|
}
|
|
247
233
|
|
|
248
|
-
/** Load session data
|
|
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),不再作本端恢复第一依据。 */
|
|
249
238
|
export function loadSession(cwd) {
|
|
250
|
-
|
|
251
|
-
const slot = activeSlot(cwd)
|
|
252
|
-
let result = loadSlotFile(cwd, slot)
|
|
253
|
-
if (result) return result
|
|
254
|
-
|
|
255
|
-
// Fallback: try the legacy current file (pre-migration, or migration failed to clean up)
|
|
256
|
-
const legacy = sessionPath(cwd)
|
|
257
|
-
try {
|
|
258
|
-
if (existsSync(legacy)) {
|
|
259
|
-
const data = JSON.parse(readFileSync(legacy, "utf8"))
|
|
260
|
-
// cwd 不匹配是别人的文件——与 loadSlotFile 一致直接 return null 不改名
|
|
261
|
-
// ("别人的文件不动"原则,2026-08-31 advisor round2 🟡)
|
|
262
|
-
if (data.cwd && data.cwd.toLowerCase() !== cwd.toLowerCase()) return null
|
|
263
|
-
if ((data?.version === 1 || data?.version === 2) && Array.isArray(data.history)) {
|
|
264
|
-
data.history = data.history.filter((m) => !isLegacyTransient(m))
|
|
265
|
-
return data
|
|
266
|
-
}
|
|
267
|
-
// 结构不匹配:保留现场(version>2 的新版文件不动)
|
|
268
|
-
if (!(typeof data?.version === "number" && data.version > 2)) {
|
|
269
|
-
try { renameSync(legacy, `${legacy}.unreadable`) } catch {}
|
|
270
|
-
console.error(`[session] legacy file ${legacy}: invalid structure — preserved as ${basename(legacy)}.unreadable`)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
} catch (e) {
|
|
274
|
-
console.error(`[session] failed to load legacy ${legacy}: ${e.message}`)
|
|
275
|
-
try { renameSync(legacy, `${legacy}.corrupted`) } catch {}
|
|
276
|
-
}
|
|
277
|
-
return null
|
|
239
|
+
return resumeSlot(cwd).data
|
|
278
240
|
}
|
|
279
241
|
|
|
280
242
|
/** slimForDisplay 截断的 arguments 以 U+2026(…)结尾——不是合法 JSON 的完整值。
|
|
@@ -293,22 +255,38 @@ function stripTruncatedToolArgs(m) {
|
|
|
293
255
|
return changed ? { ...m, tool_calls } : m
|
|
294
256
|
}
|
|
295
257
|
|
|
296
|
-
/** Apply loaded session data onto an agent object; returns true if
|
|
297
|
-
* 2026-08-31 会诊 F1:清空 _slot/_slotMtime
|
|
298
|
-
*
|
|
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)。 */
|
|
299
271
|
export function applySession(agent, data) {
|
|
300
|
-
// data.history
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
// 后者会把已压缩的中间过程塞回上下文(实测 prompt 膨胀到 283%)。compactThresholdAuto 时
|
|
306
|
-
// 按恢复后的模型重新推导阈值(bin/thincoder.mjs)。v1 老文件(无 contextHistory)回退
|
|
307
|
-
// 播种时剥离被 slimForDisplay 截断的 tool_calls.arguments(以 … 结尾 → 置 {};会诊 F6——
|
|
308
|
-
// 截断可劈断 \\uXXXX 产生 400 毒载荷)。2026-09-01 会诊三家:length > 0 才当机读线
|
|
309
|
-
// (contextHistory: [] 是"无机读线"而非空机器线——空机器线会静默丢全部上下文)。
|
|
272
|
+
// 机读线语义(历史注释保留):data.history = FULL 未压缩记录(人读线);data.contextHistory =
|
|
273
|
+
// 压缩后机器线——恢复各从其源(保留压缩收益——从完整 history 重建会塞回中间过程——实测
|
|
274
|
+
// prompt 膨胀 283%)。v1 老文件(无 contextHistory)回退播种,剥离被 slimForDisplay 截断的
|
|
275
|
+
// tool_calls.arguments(… 结尾 → 置 {}——截断可劈断 \\uXXXX 产生 400 毒载荷)。length > 0 才
|
|
276
|
+
// 当机读线(contextHistory: [] 是"无机读线"而非空机器线——空机器线静默丢全部上下文)。
|
|
310
277
|
agent.config ??= {} // ACP test mocks may omit config; be defensive like the ??= below
|
|
311
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
|
|
312
290
|
const ch = data.contextHistory
|
|
313
291
|
const machine = (Array.isArray(ch) && ch.length > 0) ? ch : full.map(stripTruncatedToolArgs)
|
|
314
292
|
agent._fullHistory = [...full]
|
|
@@ -320,15 +298,8 @@ export function applySession(agent, data) {
|
|
|
320
298
|
agent.goal = data.goal ?? null
|
|
321
299
|
agent._pendingReminders = data.pendingReminders ?? []
|
|
322
300
|
agent._sessionStart = data.sessionStart ?? null
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// slot without the field restores NO Map (fresh state) — never resurrect slots the writer did
|
|
326
|
-
// not have. Expired tokens are rejected downstream by validateDesignToken (fail-closed, TTL).
|
|
327
|
-
if (data.engDesignTokens && typeof data.engDesignTokens === "object" && !Array.isArray(data.engDesignTokens)) {
|
|
328
|
-
agent._engDesignTokens = new Map(Object.entries(data.engDesignTokens))
|
|
329
|
-
} else {
|
|
330
|
-
delete agent._engDesignTokens
|
|
331
|
-
}
|
|
301
|
+
// R16 token 字段恢复(过期过滤——见 token-ttl.mjs restoreEngTokens)
|
|
302
|
+
restoreEngTokens(agent, data)
|
|
332
303
|
// engineering is session-level (2026-08-29): the slot value is the CLI session's authority
|
|
333
304
|
// — config.json is only the initial default / cross-end mirror. A legacy slot without the
|
|
334
305
|
// field keeps whatever config.json seeded (unchanged behavior).
|
|
@@ -345,27 +316,29 @@ export function applySession(agent, data) {
|
|
|
345
316
|
agent._verifyPassed = false
|
|
346
317
|
agent._slot = null // 粘性缓存清空——切换后重新认领(F1b)
|
|
347
318
|
agent._slotMtime = null
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
agent.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (agent.provider) {
|
|
365
|
-
|
|
366
|
-
if (p) agent.provider.model = p.model
|
|
319
|
+
// ── MODEL-MERGE-SESSION 恢复(F-4——两支)──
|
|
320
|
+
const slotProvider = data.activeProvider ? agent.providers?.find((pr) => pr.name === data.activeProvider) : null
|
|
321
|
+
if (slotProvider) {
|
|
322
|
+
// ① 槽 provider 存在 → 按槽值设(不看 config):双字段恒非空——legacy 槽 activeModel
|
|
323
|
+
// null/空串 = 无 override——回该渠道默认模型(`providers[].model` 单值——M3)。
|
|
324
|
+
// F-2d(MODEL-400-FIX):`||` 非 `??`——空串也兜(activeModel="" 的槽会让 `??` 不落链 →
|
|
325
|
+
// 空槽 model 恒有值,provider.model 键不缺失)
|
|
326
|
+
const prevName = agent.activeProvider
|
|
327
|
+
const prevModel = agent.activeModel ?? null
|
|
328
|
+
const slotModel = data.activeModel || slotProvider.model
|
|
329
|
+
const switched = prevName !== slotProvider.name || prevModel !== slotModel
|
|
330
|
+
agent.activeProvider = slotProvider.name
|
|
331
|
+
agent.activeModel = slotModel
|
|
332
|
+
agent.provider = { ...slotProvider }
|
|
333
|
+
if (slotModel) agent.provider.model = slotModel
|
|
334
|
+
// 重算 compactThreshold(auto 时——阈值跟模型走;原在 bin 的 switched 分支——收拢本处)
|
|
335
|
+
if (switched && agent.config?.agent?.compactThresholdAuto && agent.provider.model) {
|
|
336
|
+
agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
|
|
367
337
|
}
|
|
338
|
+
return switched
|
|
368
339
|
}
|
|
340
|
+
// ② 槽 provider 没了 → D-S3 保留——静默保持现状(不报错不纠正——config 有效则静默用
|
|
341
|
+
// config/defaultModel 运行时;两方都无效由调用侧复验 validateProvider 弹重选)
|
|
369
342
|
return false
|
|
370
343
|
}
|
|
371
344
|
|
|
@@ -429,6 +402,8 @@ export function newSession(cwd) {
|
|
|
429
402
|
}
|
|
430
403
|
: null
|
|
431
404
|
saveManifest(cwd, m, deletions, { setActive: true })
|
|
405
|
+
// 2026-09-05 §10 D-4:/new 落点写本端记录(显式切换跟随——T-M8)
|
|
406
|
+
writeEndMarker(cwd, slot)
|
|
432
407
|
return slot
|
|
433
408
|
}
|
|
434
409
|
|
|
@@ -442,7 +417,7 @@ export function resetSessionState(agent) {
|
|
|
442
417
|
agent.title = ""
|
|
443
418
|
agent.tasks = []
|
|
444
419
|
agent._sessionStart = null
|
|
445
|
-
|
|
420
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——不再维护
|
|
446
421
|
agent._engDesignTokens = new Map() // multi-design slots die with the session (2026-09-01 fix #2)
|
|
447
422
|
agent._compressFailures = 0
|
|
448
423
|
agent._verifyRetries = 0
|
|
@@ -456,7 +431,10 @@ export function resetSessionState(agent) {
|
|
|
456
431
|
agent._slot = null
|
|
457
432
|
agent._slotMtime = null
|
|
458
433
|
agent._osReminderInjected = false
|
|
459
|
-
|
|
434
|
+
// §11.2(2026-09-08):一次性注入标志换名——_restartReminderInjected 推断退役(F3——
|
|
435
|
+
// _sessionStart != null 伪触发);/new 清零新标记——恢复武装/启动句标志不跨会话复活
|
|
436
|
+
agent._envResumed = false
|
|
437
|
+
agent._processRestartPending = false
|
|
460
438
|
agent._lastEngState = false
|
|
461
439
|
}
|
|
462
440
|
|
|
@@ -479,6 +457,8 @@ export function switchToSlot(cwd, slot) {
|
|
|
479
457
|
m.slotSessions[slot] = getSessionId()
|
|
480
458
|
}
|
|
481
459
|
saveManifest(cwd, m, null, { setActive: true })
|
|
460
|
+
// 2026-09-05 §10 D-4:/session N 跟随"最后查看的槽"(成功切换才写)
|
|
461
|
+
writeEndMarker(cwd, slot)
|
|
482
462
|
return data
|
|
483
463
|
}
|
|
484
464
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* token-ttl.mjs — design-token TTL 语义 + 槽位清理 + 会话槽权威台账 I/O(R16——2026-09-06;
|
|
3
|
+
* DESIGN-TOKEN-SETTLEMENT D1-D3——2026-09-08)。
|
|
4
|
+
*
|
|
5
|
+
* 共享目标(D-R16b):过期判定语义在此定义一次,validateDesignToken
|
|
6
|
+
* (agent-tools/design-token.mjs)、恢复过滤(session.mjs applySession)、开模式清理
|
|
7
|
+
* (agent-tools/eng.mjs + tui/cmd-eng.mjs ON 路径)、spawn 门禁过期拒删槽
|
|
8
|
+
* (subagent-spawn.mjs)共引。
|
|
9
|
+
*
|
|
10
|
+
* token 格式 = `uuid:expiresAt`(无签名流程凭证——HMAC 防伪层已删——见
|
|
11
|
+
* ENGINEERING-MODE.md 2026-09-06 段)。过期判定只对**格式合法**的 token 判
|
|
12
|
+
* 过期:格式/畸形串不在此清理(恢复时读回由门禁格式拒、门禁拒时也不删槽——
|
|
13
|
+
* 防误删有效槽——F-R16b ①③)。
|
|
14
|
+
*
|
|
15
|
+
* DESIGN-TOKEN-SETTLEMENT(2026-09-08):
|
|
16
|
+
* - **D1**:persistEngTokens = settle 当场落盘的可复用函数(engTokenSlotFields 序列化 +
|
|
17
|
+
* session 安全写/轮转——guardForeignSlotFile 与 saveSession 共用同一份守卫)。
|
|
18
|
+
* - **D2/D3**:readEngTokensFromSlot / reconcileEngTokensFromSlot = 门禁 miss 回读权威
|
|
19
|
+
* 槽(spawn 门 D2 与 dispatch 写门 D3 同源 reconcile)。
|
|
20
|
+
* - **D3**:**单值镜像 `_engDesignToken` 退役**——AC3 零写 + 仅一次性迁移读。本模块
|
|
21
|
+
* 不再写镜像(engTokenSlotFields 只产多槽表);restoreEngTokens 是唯一迁移读点(旧
|
|
22
|
+
* slot 残留 engDesignToken → 迁入 Map,此后不写不读镜像)。
|
|
23
|
+
*
|
|
24
|
+
* 依赖方向:本模块只 import 底层槽 I/O(session-slots.mjs 原语 + session-guard.mjs 轮转
|
|
25
|
+
* 守卫)——session.mjs 同时 import 本模块(engTokenSlotFields/restoreEngTokens),形成
|
|
26
|
+
* 静态环,与既有的 session ↔ session-slots 环同构(函数声明实例化期已初始化、只函数体
|
|
27
|
+
* 内运行时使用——环安全)。
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
31
|
+
import {
|
|
32
|
+
slotPath, writeSessionFile, activeSlot, loadManifest, saveManifest, slotDigest,
|
|
33
|
+
writeEndMarker,
|
|
34
|
+
} from "./session-slots.mjs"
|
|
35
|
+
// F2 轮转守卫自 2026-09-08 迁至 session-guard.mjs(session-slots 再越 500 行硬限拆分)
|
|
36
|
+
import { guardForeignSlotFile } from "./session-guard.mjs"
|
|
37
|
+
|
|
38
|
+
const DESIGN_TOKEN_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
39
|
+
|
|
40
|
+
/** 格式校验 + 数值过期时刻:格式合法 → expiresAt 数值;格式/畸形 → null
|
|
41
|
+
* (与 validateDesignToken 的 fail-closed 判定同源——单一权威)。 */
|
|
42
|
+
export function tokenExpiryMs(token) {
|
|
43
|
+
if (!token || typeof token !== "string") return null
|
|
44
|
+
const parts = token.split(":")
|
|
45
|
+
if (parts.length !== 2) return null
|
|
46
|
+
const [uuid, expiresAt] = parts
|
|
47
|
+
if (!DESIGN_TOKEN_UUID_RE.test(uuid)) return null
|
|
48
|
+
if (!/^\d+$/.test(expiresAt)) return null
|
|
49
|
+
const expiry = parseInt(expiresAt, 10)
|
|
50
|
+
if (isNaN(expiry)) return null
|
|
51
|
+
return expiry
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 过期判定——仅格式合法的 token 可判过期(畸形 → false,由门禁格式拒)。 */
|
|
55
|
+
export function tokenExpired(token, now = Date.now()) {
|
|
56
|
+
const expiry = tokenExpiryMs(token)
|
|
57
|
+
return expiry !== null && now > expiry
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 删一个 designId 槽(R16 D-R16c ③——spawn 门禁过期拒的清理面)。
|
|
61
|
+
* 条件由调用方保证(槽在位且槽值 === token 且已过期):Map 无该 id / 值不符 →
|
|
62
|
+
* 不删(防误删有效槽);designId 缺省(单槽省略 designId 路径)→ 按 token 扫删。
|
|
63
|
+
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——不再同步清镜像。
|
|
64
|
+
* 返回是否删了槽。 */
|
|
65
|
+
export function removeDesignTokenSlot(agent, designId, token) {
|
|
66
|
+
const map = agent?._engDesignTokens
|
|
67
|
+
let slotRemoved = false
|
|
68
|
+
if (map instanceof Map && token) {
|
|
69
|
+
if (designId) {
|
|
70
|
+
if (map.get(designId) === token) {
|
|
71
|
+
map.delete(designId)
|
|
72
|
+
slotRemoved = true
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
for (const [id, t] of map) {
|
|
76
|
+
if (t === token) {
|
|
77
|
+
map.delete(id)
|
|
78
|
+
slotRemoved = true
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return slotRemoved
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 遍历删过期槽(R16 F-R16b ②——eng enter / cmd-eng ON 真转换路径):Map 只留
|
|
88
|
+
* 有效 token。DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像已退役——无
|
|
89
|
+
* legacy 镜像分支。返回清理个数。 */
|
|
90
|
+
export function purgeExpiredDesignTokens(agent) {
|
|
91
|
+
const map = agent?._engDesignTokens
|
|
92
|
+
let cleared = 0
|
|
93
|
+
if (map instanceof Map) {
|
|
94
|
+
for (const [id, t] of [...map]) {
|
|
95
|
+
if (typeof t === "string" && tokenExpired(t)) {
|
|
96
|
+
map.delete(id)
|
|
97
|
+
cleared++
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return cleared
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 会话槽序列化/恢复面(session.mjs saveSession/applySession 的 token 字段——
|
|
105
|
+
// 2026-09-06 R16:slot 持久化 = 跨重启/跨模式恢复的有意载体) ──
|
|
106
|
+
|
|
107
|
+
/** saveSession 数据面:token 的序列化形态。多槽 Map → {designId: token}
|
|
108
|
+
* (JSON-safe);空/缺 Map → undefined → JSON.stringify 丢 key——清过的会话写
|
|
109
|
+
* NO field,不从上次 save 复活槽。
|
|
110
|
+
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `engDesignToken` 退役——
|
|
111
|
+
* saveSession 不再写镜像字段(AC3 零写);只产多槽表 engDesignTokens。 */
|
|
112
|
+
export function engTokenSlotFields(agent) {
|
|
113
|
+
return {
|
|
114
|
+
engDesignTokens: agent._engDesignTokens instanceof Map && agent._engDesignTokens.size > 0
|
|
115
|
+
? Object.fromEntries(agent._engDesignTokens)
|
|
116
|
+
: undefined,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** applySession 恢复面(F-R16b ①——恢复 TTL 过滤):EXPIRED token 不读回(丢弃——
|
|
121
|
+
* 下次 save 自然清字段,清盘闭环);格式/畸形串读回(门禁拒——不主动删——恢复不
|
|
122
|
+
* 得销毁它无法判定的槽数据)。
|
|
123
|
+
* DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
|
|
124
|
+
* 本函数是**唯一镜像迁移读点**(AC3):旧 slot 文件可能残留 engDesignToken 单值
|
|
125
|
+
* (D3 前 saveSession 写的 legacy 字段)。当多槽表缺失/为空(Map 空)且残留镜像为
|
|
126
|
+
* **格式合法且未过期** token 时 → 一次性迁入 Map(标 legacy——以 token 自身 uuid
|
|
127
|
+
* 为 designId——确定性、可复现、不依赖曾回显的原 designId)。此后该字段随下次
|
|
128
|
+
* saveSession/persistEngTokens(不再写镜像)自然从槽文件消失,本函数不再读到
|
|
129
|
+
* 残留——一次性语义。镜像若已过期/畸形 → 不迁移(drop——与 Map 过期项同处理,
|
|
130
|
+
* 清盘闭环)。永不写 agent._engDesignToken。 */
|
|
131
|
+
export function restoreEngTokens(agent, data) {
|
|
132
|
+
const map = new Map()
|
|
133
|
+
if (data.engDesignTokens && typeof data.engDesignTokens === "object" && !Array.isArray(data.engDesignTokens)) {
|
|
134
|
+
for (const [id, t] of Object.entries(data.engDesignTokens)) {
|
|
135
|
+
if (!(typeof t === "string" && tokenExpired(t))) map.set(id, t)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// legacy 单值镜像一次性迁移读(唯一镜像读点——AC3):Map 空且残留为格式合法未过期 token
|
|
139
|
+
if (map.size === 0) {
|
|
140
|
+
const storedMirror = data.engDesignToken
|
|
141
|
+
if (typeof storedMirror === "string" && tokenExpiryMs(storedMirror) !== null && !tokenExpired(storedMirror)) {
|
|
142
|
+
map.set(storedMirror.split(":")[0], storedMirror)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (map.size > 0) agent._engDesignTokens = map
|
|
146
|
+
else delete agent._engDesignTokens
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── 会话槽权威台账 I/O(DESIGN-TOKEN-SETTLEMENT D1/D2/D3——槽文件 = 权威结算台账) ──
|
|
150
|
+
|
|
151
|
+
/** 当前会话槽号(读侧——无认领副作用):粘性 _slot 优先;未钉槽时读 manifest active
|
|
152
|
+
* (loadManifest 只读——不在门禁路径触发认领写)。无槽 → null。 */
|
|
153
|
+
function currentSlotNoReadOnly(agent) {
|
|
154
|
+
const slot = agent?._slot
|
|
155
|
+
if (Number.isInteger(slot)) return slot
|
|
156
|
+
const active = loadManifest(agent?.cwd)?.active
|
|
157
|
+
return Number.isInteger(active) ? active : null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 读当前会话槽文件的权威台账(engDesignTokens 多槽表——不做 TTL 过滤,判定由调用方)。
|
|
161
|
+
* 门禁侧轻读:JSON.parse 只读不改文件(.tmp 回退/改名等 loadSlotFile 副作用不引入)。
|
|
162
|
+
* 单值镜像 engDesignToken 字段已退役——此处不读(AC3——运行时零镜像读;唯一镜像
|
|
163
|
+
* 迁移读在 restoreEngTokens)。返回 {designId: token} 对象或 null(无槽/无文件/不可读)。 */
|
|
164
|
+
export function readEngTokensFromSlot(agent) {
|
|
165
|
+
try {
|
|
166
|
+
const slot = currentSlotNoReadOnly(agent)
|
|
167
|
+
if (slot == null) return null
|
|
168
|
+
const p = slotPath(agent.cwd, slot)
|
|
169
|
+
if (!existsSync(p)) return null
|
|
170
|
+
const data = JSON.parse(readFileSync(p, "utf8"))
|
|
171
|
+
return (data && typeof data === "object" && !Array.isArray(data.engDesignTokens)
|
|
172
|
+
&& data.engDesignTokens && typeof data.engDesignTokens === "object")
|
|
173
|
+
? data.engDesignTokens
|
|
174
|
+
: null
|
|
175
|
+
} catch { return null }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 内存 miss 时从权威槽 reconcile(D2 spawn 门 + D3 dispatch 写门同源):把槽文件中
|
|
179
|
+
* **未过期**项并进内存 Map(TTL 过滤保留——expired 不并入,fail-closed;格式/畸形串
|
|
180
|
+
* 与 restoreEngTokens 同语义读回——门禁格式拒);槽无活项 → 内存原样。返回判定用 Map。 */
|
|
181
|
+
export function reconcileEngTokensFromSlot(agent) {
|
|
182
|
+
const slots = agent?._engDesignTokens
|
|
183
|
+
const obj = readEngTokensFromSlot(agent)
|
|
184
|
+
if (!obj) return slots instanceof Map ? slots : new Map()
|
|
185
|
+
const live = new Map()
|
|
186
|
+
for (const [id, t] of Object.entries(obj)) {
|
|
187
|
+
if (typeof t === "string" && !tokenExpired(t)) live.set(id, t)
|
|
188
|
+
}
|
|
189
|
+
if (live.size === 0) return slots instanceof Map ? slots : new Map()
|
|
190
|
+
const merged = slots instanceof Map ? slots : new Map()
|
|
191
|
+
for (const [id, t] of live) merged.set(id, t)
|
|
192
|
+
agent._engDesignTokens = merged
|
|
193
|
+
return merged
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** 是否存在任一活槽(DESIGN-TOKEN-SETTLEMENT D3——dispatch 写门判定"任一活槽存在";
|
|
197
|
+
* AC4)。先问内存 Map(当前进程活缓存);miss 回读权威槽文件 reconcile(与 spawn 门
|
|
198
|
+
* D2 同源)。判定 fail-closed:格式合法且未过期才算活槽(畸形/过期不构成活槽——不
|
|
199
|
+
* 授权产品代码写)。 */
|
|
200
|
+
export function anyLiveDesignSlot(agent) {
|
|
201
|
+
const live = (m) => {
|
|
202
|
+
for (const t of m.values()) {
|
|
203
|
+
if (typeof t === "string" && tokenExpiryMs(t) !== null && !tokenExpired(t)) return true
|
|
204
|
+
}
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
const m = agent?._engDesignTokens
|
|
208
|
+
if (m instanceof Map && live(m)) return true
|
|
209
|
+
const rec = reconcileEngTokensFromSlot(agent)
|
|
210
|
+
return rec instanceof Map && live(rec)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* D1(DESIGN-TOKEN-SETTLEMENT,2026-09-08):settle 当场同步落盘——把当前内存多槽
|
|
215
|
+
* Map 写入会话槽文件的权威台账,消除 settle→回合尾 saveSession 间的重启丢 token 窗口。
|
|
216
|
+
* 复用 engTokenSlotFields 序列化 + session 安全写/轮转(guardForeignSlotFile =
|
|
217
|
+
* saveSession 同一份守卫——勿裸写文件)。槽文件缺失(本会话首次落盘)→ 写最小全新
|
|
218
|
+
* 记录(字段形状同 saveSession——loadSlotFile 可读);既有文件 → 读-改-写(只动 token
|
|
219
|
+
* 字段,历史等其他字段原样保留)。写失败(writeSessionFile/parse 抛错)→ 抛出/返回
|
|
220
|
+
* false——调用方(settle)失败即 settle 失败(token 不注册、可重评——评审 #1 语义)。
|
|
221
|
+
*/
|
|
222
|
+
export function persistEngTokens(agent) {
|
|
223
|
+
const claimedNow = agent._slot == null
|
|
224
|
+
const slot = agent._slot ??= activeSlot(agent.cwd)
|
|
225
|
+
if (claimedNow) writeEndMarker(agent.cwd, slot)
|
|
226
|
+
const p = slotPath(agent.cwd, slot)
|
|
227
|
+
guardForeignSlotFile(agent, p, slot)
|
|
228
|
+
let data = null
|
|
229
|
+
if (existsSync(p)) {
|
|
230
|
+
data = JSON.parse(readFileSync(p, "utf8"))
|
|
231
|
+
} else {
|
|
232
|
+
// 全新槽(claim 先行、首保存落盘前)——最小记录(VSC newSlotData 同构——字段形状
|
|
233
|
+
// 同 saveSession 数据面,loadSlotFile 校验可读;历史留空——本会话首保存尚未发生,
|
|
234
|
+
// 此前历史本就未落盘,token 台账先行落盘不制造任何额外损失)。
|
|
235
|
+
data = {
|
|
236
|
+
version: 2,
|
|
237
|
+
cwd: agent.cwd,
|
|
238
|
+
title: agent.title ?? "",
|
|
239
|
+
activeProvider: agent.activeProvider ?? agent.provider?.name,
|
|
240
|
+
// MODEL-MERGE-SESSION 恒非空形态:随 saveSession 同款回落链(无渠道仍容忍 null)
|
|
241
|
+
activeModel: agent.activeModel ?? agent.provider?.model ?? null,
|
|
242
|
+
history: [],
|
|
243
|
+
contextHistory: [],
|
|
244
|
+
tasks: agent.tasks ?? [],
|
|
245
|
+
planMode: agent.planMode ?? false,
|
|
246
|
+
autoApprove: agent.autoApprove ?? false,
|
|
247
|
+
engineering: agent.config?.agent?.engineering ?? false,
|
|
248
|
+
goal: agent.goal ?? null,
|
|
249
|
+
advisor: agent.config?.advisor ?? null,
|
|
250
|
+
pendingReminders: agent._pendingReminders ?? [],
|
|
251
|
+
sessionStart: agent._sessionStart ?? null,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const fields = engTokenSlotFields(agent)
|
|
255
|
+
if (fields.engDesignTokens !== undefined) data.engDesignTokens = fields.engDesignTokens
|
|
256
|
+
else delete data.engDesignTokens
|
|
257
|
+
// 单值镜像字段永不复写(D3 零写)——残留 engDesignToken 随本次写一并清(更快退役,
|
|
258
|
+
// 杜绝 stale 镜像 + 空 Map 组合在下次 restore 二次迁移复活死 token 的边角)。
|
|
259
|
+
delete data.engDesignToken
|
|
260
|
+
data.updatedAt = Date.now()
|
|
261
|
+
writeSessionFile(p, data)
|
|
262
|
+
// 记录我们刚写的 mtime——下次保存/守卫跳过重复解析(saveSession 同款)
|
|
263
|
+
try { agent._slotMtime = statSync(p).mtimeMs } catch {}
|
|
264
|
+
// Update slot metadata in manifest(saveSession 同款尾——非致命:数据已安全,
|
|
265
|
+
// metadata 下次 listSlots 惰性恢复)
|
|
266
|
+
try {
|
|
267
|
+
const m = loadManifest(agent.cwd)
|
|
268
|
+
m.slots[slot] = slotDigest(data)
|
|
269
|
+
saveManifest(agent.cwd, m)
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.error(`[session] manifest metadata update failed for slot ${slot}: ${e.message}`)
|
|
272
|
+
}
|
|
273
|
+
return true
|
|
274
|
+
}
|