@thincoder/core 0.9.5 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/session-slot-write.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-slot-write.mjs — 会话槽「元数据开关」写面(Parnas 边界:开关写决策组——
|
|
3
|
-
* autoApprove / planMode / engineering / advisor guard
|
|
3
|
+
* autoApprove / planMode / engineering / advisor guard / 会话级偏好三键
|
|
4
|
+
* (provider / model / effort——§6.21),外加全新槽默认记录与落盘单点)。
|
|
4
5
|
*
|
|
5
6
|
* 归属:会话数据面(同上位 `session.mjs` / `session-slots.mjs` / `session-store.mjs`)。
|
|
6
7
|
* 本档 = **(cwd, slot)形态**的槽写入面(读-改-写);另一形态 = `session.mjs` 的
|
|
@@ -23,10 +24,14 @@
|
|
|
23
24
|
|
|
24
25
|
import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
|
|
25
26
|
|
|
26
|
-
import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile } from "./session-slots.mjs"
|
|
27
|
-
import { loadSlotFile } from "./session.mjs"
|
|
27
|
+
import { getSessionId, loadManifest, saveManifest, slotDigest, slotPath, writeSessionFile, sessionEnd } from "./session-slots.mjs"
|
|
28
|
+
import { loadSlotFile, isLegacyTransient } from "./session.mjs"
|
|
28
29
|
// token 形态单源(格式校验 + 到期时刻)——本档的保存面合并规则复用同一判定。
|
|
29
30
|
import { tokenExpiryMs } from "./token-ttl.mjs"
|
|
31
|
+
// 会话级偏好写面(§6.21 判据句 3)的判据单源——档位值域(`specForModel(...).reasoningEffortEnum`)
|
|
32
|
+
// 与 off 可达性(`thinkOffPath`);两叶档(零 import)⇒ 不涉环,核内零第二份族别 / 枚举表。
|
|
33
|
+
import { specForModel } from "./model-specs.mjs"
|
|
34
|
+
import { thinkOffPath } from "./think-off.mjs"
|
|
30
35
|
|
|
31
36
|
/** mtime 缓存(键 = 槽文件路径 → 上次所见 mtimeMs):同一槽连写时跳过重复解析。
|
|
32
37
|
* 测试复位缝(与 `_setSessionsDirForTest` 同款):缓存为进程级单例,用例间需可复位
|
|
@@ -34,13 +39,17 @@ import { tokenExpiryMs } from "./token-ttl.mjs"
|
|
|
34
39
|
const slotMtimeCache = new Map()
|
|
35
40
|
export function _resetSlotMtimeCacheForTest() { slotMtimeCache.clear() }
|
|
36
41
|
|
|
37
|
-
/** Fresh slot data default:全新空槽的规范结构(`newSession`
|
|
42
|
+
/** Fresh slot data default:全新空槽的规范结构(`newSession` 写盘面**同源**——SLOT-END-PARAM
|
|
43
|
+
* 批起 `newSession` 与本构造器单源;`effort: null` = 会话级档位未设态——§6.21 判据句 5 规范
|
|
44
|
+
* 结构同源;老槽无该键由读侧按 `null` 容忍)。首物化即记创建端(§6.20 判据句 4 写面:本端
|
|
45
|
+
* 物化者 = 进程端名;「禁回填」不适用——本构造器只产全新槽)。 */
|
|
38
46
|
export function newSlotData(cwd) {
|
|
39
47
|
return {
|
|
40
48
|
version: 2, cwd, title: "", updatedAt: Date.now(),
|
|
41
49
|
history: [], contextHistory: [], tasks: [],
|
|
42
|
-
planMode: false, goal: null, autoApprove: false, advisor: null,
|
|
50
|
+
planMode: false, goal: null, autoApprove: false, advisor: null, effort: null,
|
|
43
51
|
pendingReminders: [], sessionStart: null,
|
|
52
|
+
createdBy: sessionEnd(),
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -62,8 +71,11 @@ function rotateIfForeign(p, slot, data) {
|
|
|
62
71
|
disk.cwd.toLowerCase() !== data.cwd.toLowerCase()
|
|
63
72
|
const diskStart = disk?.sessionStart ?? null
|
|
64
73
|
const myStart = data?.sessionStart ?? null
|
|
74
|
+
// 盘面长度与快照**同口径**(#441①):快照 `data` 来自读槽回放(`loadSlotFile` 已按
|
|
75
|
+
// `isLegacyTransient` 过滤,`session.mjs` 读面)⇒ 盘侧同滤——否则坏档里的 legacy 注入
|
|
76
|
+
// 条目会凭空把盘面算长,无并发追加也误判轮转(.bak 白产 + 误报 concurrent append)。
|
|
65
77
|
const diskLonger = Array.isArray(disk?.history) && Array.isArray(data?.history) &&
|
|
66
|
-
disk.history.length > data.history.length
|
|
78
|
+
disk.history.filter((m) => !isLegacyTransient(m)).length > data.history.length
|
|
67
79
|
if (!(diskIsNewer || diskForeign || (diskStart && diskStart !== myStart) || diskLonger)) return null
|
|
68
80
|
const bak = `${p}.bak-${Date.now()}`
|
|
69
81
|
renameSync(p, bak)
|
|
@@ -84,16 +96,24 @@ function rotateIfForeign(p, slot, data) {
|
|
|
84
96
|
/**
|
|
85
97
|
* 槽落盘单点(读-改-写面共用):轮转判定 → 写槽文件 → 回写 manifest 摘要。
|
|
86
98
|
* 返回轮转出的 `.bak` 路径或 null(调用方按需透出)。
|
|
99
|
+
*
|
|
100
|
+
* 创建端(SLOT-END-PARAM 批 §6.20 判据句 4 写面):入参无 `createdBy` **且**槽文件不在盘
|
|
101
|
+
* (**轮转后**判据——`rotateIfForeign` 已让位现场)⇒ 本端即将首物化 ⇒ 落进程端名;
|
|
102
|
+
* 文件在盘(含老槽无键)⇒ 不动(禁回填)。
|
|
87
103
|
*/
|
|
88
104
|
export function saveSlotData(cwd, slot, data) {
|
|
89
105
|
data.updatedAt = Date.now()
|
|
90
106
|
const p = slotPath(cwd, slot)
|
|
91
107
|
const rotated = rotateIfForeign(p, slot, data)
|
|
108
|
+
if (data.createdBy === undefined && !existsSync(p)) data.createdBy = sessionEnd()
|
|
92
109
|
writeSessionFile(p, data)
|
|
93
|
-
|
|
110
|
+
// 写后取 mtime 单次采集(读失败 ⇒ `0` = 不可得):缓存(本档路径键)+ 摘要 `ts` 地板入参
|
|
111
|
+
// (#503——保存面写后取 mtime,同族先例 `session-slot-verify.mjs:99`)。
|
|
112
|
+
const wroteMtime = (() => { try { return statSync(p).mtimeMs } catch { return 0 } })()
|
|
113
|
+
slotMtimeCache.set(p, wroteMtime)
|
|
94
114
|
try {
|
|
95
115
|
const m = loadManifest(cwd)
|
|
96
|
-
m.slots[slot] = slotDigest(data)
|
|
116
|
+
m.slots[slot] = slotDigest(data, wroteMtime)
|
|
97
117
|
saveManifest(cwd, m)
|
|
98
118
|
} catch { /* 摘要失败非致命——数据已安全,元数据下次 listSlots 惰性恢复 */ }
|
|
99
119
|
return rotated
|
|
@@ -166,3 +186,49 @@ export function mergeEngTokensForSave(incoming, existing) {
|
|
|
166
186
|
}
|
|
167
187
|
return out
|
|
168
188
|
}
|
|
189
|
+
|
|
190
|
+
/** 会话级偏好(§6.21)键闭集——表外键 = 整 patch 拒(写未发生)。 */
|
|
191
|
+
const SLOT_PREF_KEYS = new Set(["provider", "model", "effort"])
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 会话级档位归一(§6.21 判据句 3——纯函数、不抛):`level` → 槽 `effort` 值。
|
|
195
|
+
* - `null` / `undefined` / `"auto"` ⇒ `null`(未设 ⇒ 回落配置面 / 渠道默认);
|
|
196
|
+
* - `"off"` ⇒ 该模型**有 off 路径**(判据单源 = `thinkOffPath`,勿在本档重写族别判定)
|
|
197
|
+
* ⇒ `"off"`、否则 `null`;
|
|
198
|
+
* - 其余 ⇒ 逐模型枚举(单源 = `specForModel(model).reasoningEffortEnum`)含之 ⇒ 原字面、
|
|
199
|
+
* 不含 / 无枚举 ⇒ `null`。
|
|
200
|
+
* `model` 非串(外端脏载 / 手工档)⇒ 按**未登记**归一(`DEFAULT_SPEC` 径)——「不抛」契约。
|
|
201
|
+
* 写面归一只此一处;读侧另容忍表外串(applySession 边界表——不设,不静默改写)。
|
|
202
|
+
*/
|
|
203
|
+
export function resolveEffortPatch(level, model) {
|
|
204
|
+
if (level == null || level === "auto") return null
|
|
205
|
+
const spec = specForModel(typeof model === "string" ? model : "")
|
|
206
|
+
if (level === "off") return thinkOffPath(spec) ? "off" : null
|
|
207
|
+
return (spec?.reasoningEffortEnum ?? []).includes(level) ? level : null
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 会话级偏好写出口(§6.21 判据句 2——沿 `setSlotAutoApprove` 同形,复用 `writeFlag` 单点):
|
|
212
|
+
* `patch` 键闭集 = `provider` / `model` / `effort`,**至少一键**(零键 / 表外键 ⇒ `false`,写未
|
|
213
|
+
* 发生);槽不可读 ⇒ `false`。`provider` / `model` 键入参映射既有槽字段 `activeProvider` /
|
|
214
|
+
* `activeModel`(不新增字段;`null` = 缺键语义——`provider` / `model` 无「未设」态);`effort` 经
|
|
215
|
+
* `resolveEffortPatch` 归一(`model` 取 `patch.model ?? 槽现值 activeModel`),归一 `null` = 未设
|
|
216
|
+
* (回落配置面 / 渠道默认)——**值置 `null` 不删键**(与 `newSlotData` 规范结构同源)。
|
|
217
|
+
* 只写盘:不碰当前内存态(施加面唯一 = `applySession`——§6.21 判据句 4 / 边界表第 5 行)。
|
|
218
|
+
*
|
|
219
|
+
* **值域面(#441② 明文——核侧有意不校)**:本出口只校**键闭集**,**不校值域**——`provider` /
|
|
220
|
+
* `model` 传 `null`(或表外字面)按**缺键语义**零改(不报错;`null` 仅对 `effort` 有「未设」
|
|
221
|
+
* 语义);值域名(`null` 仅 `effort` + `provider` / `model` 非空)由**端侧契约**把守
|
|
222
|
+
* (桌面 IPC「会话级偏好注」;端侧拒例 = `thincoder-desktop/src/main/agent-host.mjs`
|
|
223
|
+
* `prefsPatchFailure`)——读侧容忍为有意(跨端 / 手工改写档零行为变更),核内不立第二份值域校验。
|
|
224
|
+
*/
|
|
225
|
+
export function setSlotPrefs(cwd, slot, patch) {
|
|
226
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) return false
|
|
227
|
+
const keys = Object.keys(patch)
|
|
228
|
+
if (keys.length === 0 || keys.some((k) => !SLOT_PREF_KEYS.has(k))) return false
|
|
229
|
+
return writeFlag(cwd, slot, (d) => {
|
|
230
|
+
if (patch.provider != null) d.activeProvider = patch.provider
|
|
231
|
+
if (patch.model != null) d.activeModel = patch.model
|
|
232
|
+
if ("effort" in patch) d.effort = resolveEffortPatch(patch.effort, patch.model ?? d.activeModel)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
@@ -5,7 +5,18 @@
|
|
|
5
5
|
* 摘要面(`extractSlotMeta` / `slotDigest`)· 清单读写(`loadManifest` / `saveManifest`)·
|
|
6
6
|
* 属主面(`ownerPid` / `ownerPids` / `ownerStateOf` / `ownerArgs` / `cleanDeadOwners`)·
|
|
7
7
|
* 认领面(`ensureActive` / `allocateFresh` / `claimSlot` / `activeSlot` / **认领释放集
|
|
8
|
-
* `staleClaims`**——F-CR1 · SESSION-CLAIM 批:判据单源,`saveManifest` 落盘 +
|
|
8
|
+
* `staleClaims`**——F-CR1 · SESSION-CLAIM 批:判据单源,`saveManifest` 落盘 + 各落点共用;
|
|
9
|
+
* **属主 ∕ 认领两族已随 2026-09-28 拆分迁出 `session-slot-claims.mjs`——见下**)。
|
|
10
|
+
*
|
|
11
|
+
* LEDGER-RELIABILITY 批(2026-09-28 · SESSION.md §6.25):补摘要谓词 `healNeeded` + 落点 A 顺手补写
|
|
12
|
+
* `healDigest` / `slotMtime`(判据句 3)· 账本面写安全(判据句 5)· 账本健康出口 `ledgerHealth`(判据句 4)。
|
|
13
|
+
*
|
|
14
|
+
* 2026-09-28 拆分批(R3——本档 421 > 300):**认领面族**(`staleClaims` ∕ owner 三件 ∕
|
|
15
|
+
* `cleanDeadOwners` ∕ `ensureActive` ∕ `allocateFresh` ∕ `claimSlot` ∕ `_releaseStats` ∕
|
|
16
|
+
* `releaseClaimsAll` ∕ `activeSlot`)外提 `./session-slot-claims.mjs`(verbatim——台账 #484
|
|
17
|
+
* 顺延项落形);本档保留摘要 / 清单读写 / 属主健康面,并 re-export 该档导出保既有 import 面
|
|
18
|
+
* (消费档路径与名面不动)。新增静态环:本档 ↔ `session-slot-claims.mjs`(`loadManifest` ∕
|
|
19
|
+
* `saveManifest` ↔ 认领面族——环安全同下)。
|
|
9
20
|
*
|
|
10
21
|
* 静态环(与既有的 session.mjs ↔ session-slots.mjs 同形):本档引 `session-slots.mjs` 的
|
|
11
22
|
* 存储原语(`getSessionId` / `manifestPath` / `slotPath` / `writeSessionFile`),
|
|
@@ -18,14 +29,14 @@
|
|
|
18
29
|
* 属主三态(`ownerState`)判据单源 = `process-probe.mjs`。`unknown`(探测失败 / 缺行)⇒
|
|
19
30
|
* 不认领 / 不判死 / 不删(方向不对称见 process-probe.mjs 头注 · D-MI10)。
|
|
20
31
|
*/
|
|
21
|
-
|
|
22
|
-
import { existsSync, readFileSync, renameSync } from "node:fs"
|
|
32
|
+
import { existsSync, readFileSync, renameSync, statSync } from "node:fs"
|
|
23
33
|
// 「真实用户消息」谓词单源(`history-window.mjs`——人读线窗口与槽摘要同判据)。
|
|
24
34
|
import { isRealUserMsg } from "./history-window.mjs"
|
|
25
|
-
// 探测束(判活)+ 属主三态判据 / 死主判定(本档零自有 exec——判据面与探测面分离)。
|
|
26
|
-
import { probeOwnersSync, ownerState, filterDeadOwners } from "./process-probe.mjs"
|
|
27
35
|
// 存储原语(路径 / 原子写 / 进程 sessionId)——静态环见头注。
|
|
28
36
|
import { getSessionId, manifestPath, slotPath, writeSessionFile } from "./session-slots.mjs"
|
|
37
|
+
// 认领面(2026-09-28 拆分外提——见头注):本档 import `staleClaims` 供 dropStaleClaims 取用,
|
|
38
|
+
// 并在下方 re-export 全族保既有 import 面。
|
|
39
|
+
import { staleClaims } from "./session-slot-claims.mjs"
|
|
29
40
|
|
|
30
41
|
/** Extract slot metadata from history (shared by slotDigest and loadSlotMeta) */
|
|
31
42
|
export function extractSlotMeta(history, activeProvider, updatedAt, title = "") {
|
|
@@ -42,11 +53,49 @@ export function extractSlotMeta(history, activeProvider, updatedAt, title = "")
|
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/** Extract preview summary from session data for manifest storage (with current timestamp)
|
|
45
|
-
* MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。
|
|
46
|
-
|
|
56
|
+
* MODEL-MERGE-SESSION:摘要升级带 activeModel——listSlots 合成显 "p:m"(双端同规则)。
|
|
57
|
+
* SLOT-END-PARAM 批(SESSION.md §6.20 判据句 4 读面):创建端**有值才带**(老槽无键 ⇒
|
|
58
|
+
* 键缺席 = 未知——禁回填、禁以占用端冒充)。
|
|
59
|
+
* #503:`ts` 经打点单源 `digestTs(mtimeMs)`——`mtimeMs`(可选)= 该档保存面**写后**取的 mtime
|
|
60
|
+
* (地板);缺省 / 不可得 ⇒ 裸墙钟回溯(旧调用点零改)。 */
|
|
61
|
+
export function slotDigest(data, mtimeMs) {
|
|
47
62
|
const meta = extractSlotMeta(data.history ?? [], data.activeProvider, data.updatedAt, data.title ?? "")
|
|
48
63
|
if (data.activeModel) meta.activeModel = data.activeModel
|
|
49
|
-
|
|
64
|
+
if (data.createdBy) meta.createdBy = data.createdBy
|
|
65
|
+
return { ts: digestTs(mtimeMs), ...meta }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isNum = (v) => typeof v === "number" && Number.isFinite(v)
|
|
69
|
+
|
|
70
|
+
/** 摘要 `ts` 打点(**单源** · #503 地板):`max(墙钟, mtimeMs)`;`mtimeMs` 不可得(缺省 / 非数)⇒
|
|
71
|
+
* 裸墙钟回溯。沿先例 `session-slot-verify.mjs:99`(回写面地板):**平台文件时间戳可领先
|
|
72
|
+
* `Date.now()` 数 ms**——无地板则保存面刚写的摘要会被 `needsVerify`(`ts < mtime`)判「陈旧」
|
|
73
|
+
* 一次(有界自纠,非数据面);地板后 `ts ≥ mtime` 恒成立(同一 stat 值即比较对象)。 */
|
|
74
|
+
export function digestTs(mtimeMs) {
|
|
75
|
+
return isNum(mtimeMs) ? Math.max(Date.now(), mtimeMs) : Date.now()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 摘要补写谓词(§6.25 判据句 3 · **单源**):缺失 ∨ 陈旧(`ts < mtime`)⇒ 补写;新鲜 ⇒ 不写(幂等)。 */
|
|
79
|
+
export function healNeeded(entry, mtime) {
|
|
80
|
+
if (!entry || typeof entry !== "object") return true
|
|
81
|
+
if (!isNum(entry.ts)) return true
|
|
82
|
+
return isNum(mtime) ? entry.ts < mtime : false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 落点 A 顺手补写(§6.25 判据句 3 · 打开 / 切槽):数据**已在手** ⇒ 置入调用方 manifest 对象,随其
|
|
86
|
+
* 既有那次 `saveManifest` 落盘(**零额外槽文件读 + 零额外写**);`mtime` 不可得 ⇒ 不妄断陈旧。
|
|
87
|
+
* `ts` 携同一 `mtime` 地板(#503——否则 mtime 领先墙钟时刚补写的条目又判陈旧)。 */
|
|
88
|
+
export function healDigest(m, slot, data, mtime) {
|
|
89
|
+
if (!data || typeof data !== "object") return false
|
|
90
|
+
if (!healNeeded(m.slots?.[slot], mtime)) return false
|
|
91
|
+
m.slots ??= {}
|
|
92
|
+
m.slots[slot] = slotDigest(data, mtime)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 槽文件 mtime(`healDigest` 新鲜度判据入参——读失败 ⇒ `null` = 不可得)。 */
|
|
97
|
+
export function slotMtime(cwd, slot) {
|
|
98
|
+
try { return statSync(slotPath(cwd, slot)).mtimeMs } catch { return null }
|
|
50
99
|
}
|
|
51
100
|
|
|
52
101
|
export function loadManifest(cwd) {
|
|
@@ -60,6 +109,62 @@ export function loadManifest(cwd) {
|
|
|
60
109
|
} catch { return { slots: {}, sessionId: null } }
|
|
61
110
|
}
|
|
62
111
|
|
|
112
|
+
/** 可信合并基座判据(§6.23 判据句 1 · ②):顶层非 null 非数组对象 ∧(`slots` / `slotSessions`
|
|
113
|
+
* 缺席或为对象)。**假值(null / `0` / `""` / `false`)按缺席归一**——判据线单源 = `loadManifest`
|
|
114
|
+
* 的 `!m.slots → {}`(同向:空基座可合并、不拒写;展开后自然落 `{}`);真值非对象(`42` / `"x"`)
|
|
115
|
+
* 与数组 ⇒ 不可信。形态非法 ⇒ 不写盘(判据句 2(c))。 */
|
|
116
|
+
function isTrustedBase(fresh) {
|
|
117
|
+
if (!fresh || typeof fresh !== "object" || Array.isArray(fresh)) return false
|
|
118
|
+
const mapField = (v) => !v || (typeof v === "object" && !Array.isArray(v))
|
|
119
|
+
return mapField(fresh.slots) && mapField(fresh.slotSessions)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** (b)(c) 现场保全:原字节改名 `{manifest 路径}.corrupted`(与槽文件面同形)——同名已存在 ⇒
|
|
123
|
+
* 覆盖(单槽位 · last-wins);改名失败被吞、**不阻断拒写**(§6.23 判据句 2)。返回值为**改名目标
|
|
124
|
+
* 路径**(拒写 loud 行的 `preserved=` 即取此)——非存在性保证:改名失败面 = `.corrupted` 缺席自证。 */
|
|
125
|
+
function preserveScene(p) {
|
|
126
|
+
const dst = `${p}.corrupted`
|
|
127
|
+
try { renameSync(p, dst) } catch {}
|
|
128
|
+
return dst
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 账本健康计数(§6.25 判据句 4——**本进程累计**):拒写(§6.23)+ 写后读回失败次数与最近一条。 */
|
|
132
|
+
const ledgerRefusals = { refused: 0, lastReason: null, lastPath: null, lastAt: null }
|
|
133
|
+
const noteRefusal = (reason, p) => Object.assign(ledgerRefusals, { refused: ledgerRefusals.refused + 1, lastReason: reason, lastPath: p, lastAt: Date.now() })
|
|
134
|
+
|
|
135
|
+
/** 账本健康出口(§6.25 判据句 4 · **判据单源**):`refused` = 本进程累计(§6.23 拒写 + 判据句 5 读回
|
|
136
|
+
* 失败)+ `lastReason` ∈ 四类(read-failed / parse-failed / shape-invalid / readback-failed);`scene` =
|
|
137
|
+
* 该 cwd 的 `{manifest}.corrupted` 在盘(损坏现场——随 30 天 GC / 手工清除而止)。 */
|
|
138
|
+
export function ledgerHealth(cwd) {
|
|
139
|
+
let scene = false
|
|
140
|
+
try { scene = existsSync(`${manifestPath(cwd)}.corrupted`) } catch { /* cwd 不可解析 ⇒ 无现场 */ }
|
|
141
|
+
return { ...ledgerRefusals, scene }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 拒写(§6.23 判据句 2/3):不调 `writeSessionFile`(本次调用对盘面零字节写),stderr 一行 loud,返回 `false`。 */
|
|
145
|
+
function refuseManifestWrite(p, reason, preserved = null) {
|
|
146
|
+
noteRefusal(reason, p)
|
|
147
|
+
console.error(`[session] saveManifest: write refused (reason=${reason}) path=${p}${preserved ? ` preserved=${preserved}` : ""}`)
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 写后钩子缝(§6.25 判据句 5:写后 / 读回前注入;生产零消费):`fn({ path, tmp })`。 */
|
|
152
|
+
let manifestWriteHook = null
|
|
153
|
+
export function _setManifestWriteHookForTest(fn) { manifestWriteHook = fn }
|
|
154
|
+
|
|
155
|
+
/** 账本面写(两路共用 · D-SE65):独占临时名 + **写后结构读回**(判据单源 = `isTrustedBase`);不过 ⇒
|
|
156
|
+
* loud 一行 + 现场改名 `.corrupted`(解封下一写)+ `false`。**非逐字节相等**(并发合法后写会假红——明裁)。 */
|
|
157
|
+
function writeManifestBase(p, m) {
|
|
158
|
+
const tmp = writeSessionFile(p, m, { tmpUnique: true })
|
|
159
|
+
manifestWriteHook?.({ path: p, tmp })
|
|
160
|
+
let trusted = false
|
|
161
|
+
try { trusted = isTrustedBase(JSON.parse(readFileSync(p, "utf8"))) } catch { trusted = false }
|
|
162
|
+
if (trusted) return true
|
|
163
|
+
noteRefusal("readback-failed", p)
|
|
164
|
+
console.error(`[session] saveManifest: readback-failed path=${p} preserved=${preserveScene(p)}`)
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
|
|
63
168
|
export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
64
169
|
// 2026-08-31 会诊 kimi/deepseek 🟡:写前重读并按"条目级"合并——原实现把"读时快照"
|
|
65
170
|
// 整对象写回,另一进程在窗口内对 slots/slotSessions/active 的变更被覆盖抹除(被抹
|
|
@@ -74,49 +179,52 @@ export function saveManifest(cwd, m, deletions = null, opts = {}) {
|
|
|
74
179
|
// (落点槽 ∪ 本进程其余活绑定槽;被占落点 ⇒ 空集)。③ 内存认领表先移除残留条目(否则下方
|
|
75
180
|
// 条目级合并会把它从内存复活回写)——先于 try(fresh 不可读时亦须生效)。
|
|
76
181
|
if (opts.release) dropStaleClaims(m, opts.release)
|
|
182
|
+
// MANIFEST-WRITE-GUARD 批(2026-09-28 · SESSION.md §6.23 判据句 1–3 / D-SE59):写前分类——只有
|
|
183
|
+
// 「① 合法创建(ENOENT)」与「② 可信合并基座」两路通向 writeSessionFile;文件在盘而基座不可信
|
|
184
|
+
// (读失败 / 解析失败 / 形态非法)⇒ **拒绝写**(不整档替换——#476 实证:读不可信窗口内一次保存把
|
|
185
|
+
// manifest 永久缩水且零告警)。拒写 = stderr 一行 + 返回 false(唯一消费点 = releaseClaimsAll 透传;
|
|
186
|
+
// 其余调用点忽略返回值——零行为变化);拒写不回滚调用方内存态(认领等内存变更归调用方所有,
|
|
187
|
+
// 下一次保存重试——与 F-XR1 同向的失败容忍形态)。
|
|
188
|
+
const p = manifestPath(cwd)
|
|
189
|
+
let fresh
|
|
77
190
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (m.sessionId) merged.sessionId = m.sessionId
|
|
85
|
-
// ① 释放集按**本次 fresh 快照**取值(不得以陈旧内存 manifest 构 deletions);② **值条件删除**
|
|
86
|
-
// (仅当 fresh 属主仍为本进程——窗口内他人的新认领不在集内、不被误删)⇒ 并入本次 deletions。
|
|
87
|
-
if (opts.release) {
|
|
88
|
-
const released = staleClaims(fresh, opts.release)
|
|
89
|
-
if (released.length > 0) {
|
|
90
|
-
deletions = { ...(deletions ?? {}), slotSessions: [...(deletions?.slotSessions ?? []), ...released] }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (deletions) {
|
|
94
|
-
for (const [section, keys] of Object.entries(deletions)) {
|
|
95
|
-
for (const k of keys) delete merged[section]?.[k]
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
m = merged
|
|
191
|
+
fresh = JSON.parse(readFileSync(p, "utf8"))
|
|
192
|
+
} catch (err) {
|
|
193
|
+
// ① 合法创建:路径不存在(首建)⇒ 以调用方对象建新档(现行为保留)
|
|
194
|
+
if (err?.code === "ENOENT") {
|
|
195
|
+
m.sessionId = getSessionId()
|
|
196
|
+
return writeManifestBase(p, m)
|
|
99
197
|
}
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
try { renameSync(manifestPath(cwd), `${manifestPath(cwd)}.corrupted`) } catch {}
|
|
198
|
+
// (a) 读失败(非 ENOENT:EISDIR / EPERM / EBUSY 等)⇒ 拒写 · 不改名(读不到 ≠ 损坏——现场
|
|
199
|
+
// 原样保留;该窗口正是 #476 缩水损伤的成因窗口)
|
|
200
|
+
if (!(err instanceof SyntaxError)) return refuseManifestWrite(p, "read-failed")
|
|
201
|
+
// (b) 解析失败 ⇒ 拒写 · 保底改名 .corrupted(现场保全 + 解封下一写——原路径不重建)
|
|
202
|
+
return refuseManifestWrite(p, "parse-failed", preserveScene(p))
|
|
106
203
|
}
|
|
204
|
+
// (c) 形态非法(顶层非 null 非数组对象 / `slots` 非对象 / `slotSessions` 非对象)⇒ 同 (b)
|
|
205
|
+
if (!isTrustedBase(fresh)) return refuseManifestWrite(p, "shape-invalid", preserveScene(p))
|
|
206
|
+
// ② 可信合并基座:读-合并-写(条目级合并 / deletions / setActive / opts.release 四判据零改)
|
|
207
|
+
const merged = { ...fresh }
|
|
208
|
+
if (opts.setActive) merged.active = m.active
|
|
209
|
+
merged.slots = { ...fresh.slots, ...(m.slots ?? {}) }
|
|
210
|
+
merged.slotSessions = { ...(fresh.slotSessions ?? {}), ...(m.slotSessions ?? {}) }
|
|
211
|
+
if (m.sessionId) merged.sessionId = m.sessionId
|
|
212
|
+
// ① 释放集按**本次 fresh 快照**取值(不得以陈旧内存 manifest 构 deletions);② **值条件删除**
|
|
213
|
+
// (仅当 fresh 属主仍为本进程——窗口内他人的新认领不在集内、不被误删)⇒ 并入本次 deletions。
|
|
214
|
+
if (opts.release) {
|
|
215
|
+
const released = staleClaims(fresh, opts.release)
|
|
216
|
+
if (released.length > 0) {
|
|
217
|
+
deletions = { ...(deletions ?? {}), slotSessions: [...(deletions?.slotSessions ?? []), ...released] }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (deletions) {
|
|
221
|
+
for (const [section, keys] of Object.entries(deletions)) {
|
|
222
|
+
for (const k of keys) delete merged[section]?.[k]
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
m = merged
|
|
107
226
|
m.sessionId = getSessionId()
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** 本进程**残留认领集**(F-CR1 释放谓词 —— **判据单源** · SESSION.md §6.2 / §6.16):
|
|
112
|
-
* 「释放 A ⟺ `slotSessions[A]` 为本进程 ∧ `A ∉ 保留集`」。`keep` = 保留集 = 落点槽 ∪ 本进程
|
|
113
|
-
* 其余活绑定槽(单绑定落点 ⇒ `{落点槽}`;被占落点 ⇒ 空集)。返回槽号(**字符串键**形态)。 */
|
|
114
|
-
export function staleClaims(src, keep = []) {
|
|
115
|
-
const keepSet = new Set((keep ?? []).filter((s) => s != null).map(Number))
|
|
116
|
-
const mySessionId = getSessionId()
|
|
117
|
-
return Object.entries(src?.slotSessions ?? {})
|
|
118
|
-
.filter(([slot, owner]) => owner === mySessionId && !keepSet.has(Number(slot)))
|
|
119
|
-
.map(([slot]) => slot)
|
|
227
|
+
return writeManifestBase(p, m)
|
|
120
228
|
}
|
|
121
229
|
|
|
122
230
|
/** 从**内存**认领表移除本进程残留条目(判据③——与写盘同一时机;见 `saveManifest`)。 */
|
|
@@ -125,197 +233,11 @@ function dropStaleClaims(m, keep) {
|
|
|
125
233
|
for (const slot of staleClaims(m, keep)) delete m.slotSessions[slot]
|
|
126
234
|
}
|
|
127
235
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const pids = []
|
|
137
|
-
for (const owner of Object.values(m.slotSessions ?? {})) {
|
|
138
|
-
if (!owner || owner === exclude) continue
|
|
139
|
-
const pid = ownerPid(owner)
|
|
140
|
-
if (pid) pids.push(pid)
|
|
141
|
-
}
|
|
142
|
-
return pids
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** 束 → 属主三态(判据单源 = process-probe `ownerState`;束缺失 / 探测失败 ⇒ "unknown"——
|
|
146
|
-
* 保守保留,**不得**当全死)。 */
|
|
147
|
-
export function ownerStateOf(owner, bundle) {
|
|
148
|
-
const pid = ownerPid(owner)
|
|
149
|
-
return ownerState(pid, { aliveSet: bundle?.aliveSet ?? null, cmds: bundle?.cmds ?? null })
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/** 束 → `filterDeadOwners` 入参(清理面 API 保面——三态 `alive`:无束 / 失败 ⇒ undefined = 未知)。 */
|
|
153
|
-
function ownerArgs(owner, bundle) {
|
|
154
|
-
const pid = ownerPid(owner)
|
|
155
|
-
const aliveSet = bundle?.aliveSet ?? null
|
|
156
|
-
return [pid, { alive: aliveSet ? aliveSet.has(pid) : undefined, cmdline: bundle?.cmds?.get?.(pid) }]
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** 死主条目清理(2026-08-31 会诊 F4 + 2026-09-01 会诊 deepseek/kimi 🔴 语义原样抽取——
|
|
160
|
-
* ensureActive/resumeSlot 共用):删除 owner 进程已死的 slotSessions 条目。死主判定必须跑
|
|
161
|
-
* 判据(不能以"文件缺失"短路——活进程在"认领 → 首次保存"窗口文件暂缺,误删会
|
|
162
|
-
* 致双进程同槽),但**不自行探测**:判活 / 命令行来自调用面同一探测束(F-MI7——
|
|
163
|
-
* 零逐 pid exec)。返回 deletions 计算函数(调用时按 m 当前状态过滤刚重新认领的槽——防删
|
|
164
|
-
* 掉自己的新属主),无清理返回 null。删除须经 saveManifest 的 deletions 显式落盘——条目级
|
|
165
|
-
* 合并会把磁盘死条目从 fresh 复活回写(N1 + 会诊 🔴)。
|
|
166
|
-
*
|
|
167
|
-
* 身份复核(批 1 · F-MI6 / D-MI11——判据单源 `filterDeadOwners`):pid 活 + 命令行**明确
|
|
168
|
-
* 可得且非本产品** ⇒ 同判可删(pid 复用即此场景);探测失败 / 缺行 ⇒ 保守保留(D-MI10)。 */
|
|
169
|
-
export function cleanDeadOwners(m, bundle) {
|
|
170
|
-
const mySessionId = getSessionId()
|
|
171
|
-
const entries = Object.entries(m.slotSessions).filter(([, o]) => o && o !== mySessionId)
|
|
172
|
-
const deadSlots = []
|
|
173
|
-
for (const [slot, owner] of entries) {
|
|
174
|
-
const [pid, args] = ownerArgs(owner, bundle)
|
|
175
|
-
if (filterDeadOwners(pid, args)) {
|
|
176
|
-
delete m.slotSessions[slot]
|
|
177
|
-
deadSlots.push(slot)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return deadSlots.length === 0 ? null : () => ({ slotSessions: deadSlots.filter((s) => m.slotSessions[s] !== mySessionId) })
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Claim a slot for this process and set it as active. Idempotent.
|
|
185
|
-
* Preference order:
|
|
186
|
-
* 1. The current active slot, if it is unowned / ours / its owner is dead — reuse it.
|
|
187
|
-
* 2. Any slot that is unowned or owned by a dead process (reclaim). → allocateFresh
|
|
188
|
-
* 3. A brand-new slot when all are owned by live processes. → allocateFresh
|
|
189
|
-
* The owner is recorded in m.slotSessions so other processes (CLI ↔ VS Code) can
|
|
190
|
-
* see which slots are taken and avoid them.
|
|
191
|
-
*
|
|
192
|
-
* F-MI7(同步例外收窄):粘性早退(已拥有 active)**零探测**(无束、无 exec);
|
|
193
|
-
* 冷路径 = 一次有界同步束(`probeOwnersSync`)——清理 / 占用 / 空闲全部查表。
|
|
194
|
-
*/
|
|
195
|
-
function ensureActive(cwd, m) {
|
|
196
|
-
const mySessionId = getSessionId()
|
|
197
|
-
if (!m.slotSessions) m.slotSessions = {}
|
|
198
|
-
|
|
199
|
-
// Already own the active slot — nothing to do. 粘性早退:**零探测**(死项清理交给
|
|
200
|
-
// 非粘性路径——resumeSlot 每次进程入口的异步束 / 冷路径同步束都会清并落盘)。
|
|
201
|
-
if (m.active && m.slotSessions[m.active] === mySessionId) return
|
|
202
|
-
|
|
203
|
-
// 冷路径:入口一次有界同步束(每 exec ≤ SYNC_PROBE_MS)覆盖全量属主 pid。
|
|
204
|
-
const bundle = probeOwnersSync(ownerPids(m))
|
|
205
|
-
const deadParam = cleanDeadOwners(m, bundle)
|
|
206
|
-
|
|
207
|
-
const isFree = (slot) => {
|
|
208
|
-
const owner = m.slotSessions[slot]
|
|
209
|
-
if (!owner || owner === mySessionId) return true
|
|
210
|
-
// 未知(探测失败 / 缺行)⇒ 不认为是空闲(不认领——D-MI10)
|
|
211
|
-
return ownerStateOf(owner, bundle) === "dead"
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// 1. Prefer the current active slot if we can take it (preserves "resume where you left off").
|
|
215
|
-
if (m.active && m.slots[m.active] && isFree(m.active)) {
|
|
216
|
-
m.slotSessions[m.active] = mySessionId
|
|
217
|
-
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 2/3. 分支 2/3 已抽取为 allocateFresh(2026-09-05 §6.10 D-2——resumeSlot 全新分配复用)。
|
|
222
|
-
allocateFresh(cwd, m, deadParam, bundle)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Allocate a slot with ensureActive 分支 2/3 语义(2026-09-05 §6.10 D-2 抽取——resumeSlot
|
|
227
|
-
* 步骤③"其余一切 → 全新分配"复用;与 newSession 选号语义对齐):
|
|
228
|
-
* 2. Reclaim a manifest slot whose FILE does not exist (never held a session);
|
|
229
|
-
* 3. A brand-new slot when none is free / all numbers are taken.
|
|
230
|
-
* 记录所有权 + active 指针(setActive)并落盘。返回分配的槽号。
|
|
231
|
-
* F-MI7:占用 / 空闲判据全部查**调用面束**(`bundle`——零自有探测);缺省 null ⇒
|
|
232
|
-
* 全部属主按 unknown 论(只可能取全新号——保守,不认领陌生人)。
|
|
233
|
-
*/
|
|
234
|
-
export function allocateFresh(cwd, m, deadParam = null, bundle = null) {
|
|
235
|
-
const mySessionId = getSessionId()
|
|
236
|
-
if (!m.slotSessions) m.slotSessions = {}
|
|
237
|
-
const isFree = (slot) => {
|
|
238
|
-
const owner = m.slotSessions[slot]
|
|
239
|
-
if (!owner || owner === mySessionId) return true
|
|
240
|
-
return ownerStateOf(owner, bundle) === "dead" // 未知 ⇒ 非空闲(不认领——D-MI10)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 2. Reclaim a slot whose FILE does not exist (never held a session). 2026-08-31 会诊 F4:
|
|
244
|
-
// 原实现认领"编号最小的空闲 slot"——死主的旧 slot 文件仍在,新进程会 resume 进
|
|
245
|
-
// 陌生会话("会话乱了"实锤)且退出时覆盖它。只有文件缺失的空 slot 才允许回收。
|
|
246
|
-
const allSlots = Object.keys(m.slots).filter((n) => /^\d+$/.test(n)).map(Number).sort((a, b) => a - b)
|
|
247
|
-
for (const slot of allSlots) {
|
|
248
|
-
if (isFree(slot) && !existsSync(slotPath(cwd, slot))) {
|
|
249
|
-
m.active = slot
|
|
250
|
-
m.slotSessions[slot] = mySessionId
|
|
251
|
-
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
252
|
-
return slot
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// 3. All slots owned by live processes — allocate a new one (no limit).
|
|
257
|
-
// 2026-08-31 advisor round2 🟡:新号从 max+1 起逐号跳过"已被活进程认领但尚未落盘"
|
|
258
|
-
// 的号(认领→首次保存窗口:slotSessions 有条目、m.slots 无条目、文件不存在——
|
|
259
|
-
// 仅凭 m.slots/existsSync 查不到 → 双进程认领同一号 → 同槽双写/互旋)。
|
|
260
|
-
// 2026-09-01 会诊 kimi 🟡:同时跳过文件仍存在的号(与 newSession 对齐)——manifest
|
|
261
|
-
// 条目丢失/损坏时 max+1 会撞上孤儿槽文件 → F2 把真会话轮转成不可见的 .bak。
|
|
262
|
-
// 另:allSlots 已升序,取 max 用 allSlots[allSlots.length-1](数万槽位时 Math.max
|
|
263
|
-
// spread 有 RangeError 风险)。
|
|
264
|
-
const liveClaimed = (n) => {
|
|
265
|
-
const owner = m.slotSessions?.[n]
|
|
266
|
-
if (!owner || owner === mySessionId) return false
|
|
267
|
-
return ownerStateOf(owner, bundle) !== "dead" // 活 / 未知 ⇒ 视为已占(跳号——保守)
|
|
268
|
-
}
|
|
269
|
-
let newSlot = allSlots.length > 0 ? allSlots[allSlots.length - 1] + 1 : 1
|
|
270
|
-
while (liveClaimed(newSlot) || existsSync(slotPath(cwd, newSlot))) newSlot++
|
|
271
|
-
m.active = newSlot
|
|
272
|
-
m.slotSessions[newSlot] = mySessionId
|
|
273
|
-
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true })
|
|
274
|
-
return newSlot
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** 认领指定槽为本进程所有并置为 active(2026-09-05 §6.10 D-2 resumeSlot 认领路径——调用方
|
|
278
|
-
* 已按"属主 空/死/本进程"判据校验可用性;幂等)。manifest active 保留为共享指针(D-6:
|
|
279
|
-
* 旧版端/ACP 恢复依据 + 无记录端一次性继承源 + 列表回退高亮——不再作本端恢复第一依据)。
|
|
280
|
-
* deadParam 在写入所有权之后求值(防 deletions 删掉本调用刚认领的槽——ensureActive
|
|
281
|
-
* deadParam 同型过滤)。
|
|
282
|
-
* F-CR1(2026-09-21 · SESSION-CLAIM 批):认领落点释放本进程残留认领(保留集 = {认领槽}——
|
|
283
|
-
* §6.2 公式代入);本函数 = 启动恢复 / 本端恢复落点(核 `resumeSlot` 与 VSC 端壳
|
|
284
|
-
* `resumeSlot` 共用),落盘判据(fresh 快照 / 值条件删除 / 内存移除)见 `saveManifest`。 */
|
|
285
|
-
export function claimSlot(cwd, slot, m = loadManifest(cwd), deadParam = null) {
|
|
286
|
-
m.slotSessions ??= {}
|
|
287
|
-
m.slotSessions[slot] = getSessionId()
|
|
288
|
-
m.active = slot // 认领即翻共享指针(setActive 写 m.active——不更新则落快照旧值)
|
|
289
|
-
saveManifest(cwd, m, deadParam?.() ?? null, { setActive: true, release: [slot] })
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** 观测计数(测试缝——`_storeStats` / `_staleHooks` 同惯例):`releaseClaimsAll` 调用计数。
|
|
293
|
-
* 端壳「工作区转空 ⇒ 旧 cwd 释放恰一次」类断言以此为桩计数(生产零消费)。 */
|
|
294
|
-
export const _releaseStats = { calls: 0 }
|
|
295
|
-
|
|
296
|
-
/** 退出全释放(EXIT-CLAIM-RELEASE 批 · F-XR1 · SESSION.md §6.18 D-SE41):优雅退出前释放
|
|
297
|
-
* 本进程**全部**槽认领(保留集空——`staleClaims` 释放谓词单源);落盘判据 = `saveManifest`
|
|
298
|
-
* `opts.release` 三条自动继承(fresh 同次快照取释放集 / 值条件删除——他人窗口内新认领不误删 /
|
|
299
|
-
* 内存认领表同步移除)。**零触碰 marker 面**(F-XR2 路标保留——下次启动经 `usableSlot`
|
|
300
|
-
* 无属主短路零探测直达恢复);崩溃路径不经此(信号无 JS 钩子——F-XR3)。
|
|
301
|
-
* 早退面:磁盘无 manifest ⇒ `existsSync` 单条目早退**零写**(不造盘面);本进程无认领 ⇒
|
|
302
|
-
* 零写返回 false。失败容忍(F-XR1「退出恒达」):整体 try/catch **永不抛出**,返回 boolean
|
|
303
|
-
* (true = 有释放且落盘成功;false = 早退 / 失败——断言面);写失败盘面 = 认领残留 →
|
|
304
|
-
* 恢复走既有探测面(现状形态,数据零险)。 */
|
|
305
|
-
export function releaseClaimsAll(cwd) {
|
|
306
|
-
_releaseStats.calls += 1
|
|
307
|
-
try {
|
|
308
|
-
if (!existsSync(manifestPath(cwd))) return false // 无 manifest ⇒ 零写(不造盘面)
|
|
309
|
-
const m = loadManifest(cwd)
|
|
310
|
-
if (staleClaims(m, []).length === 0) return false // 无本进程认领 ⇒ 零写(免白刷共享 sessionId)
|
|
311
|
-
saveManifest(cwd, m, null, { release: [] }) // 保留集空 = 全释放;不 setActive(active 共享指针不动)
|
|
312
|
-
return true
|
|
313
|
-
} catch { return false } // F-XR1:退出恒达(写失败 = 认领残留 → 恢复走探测面)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** Return the active slot number for this process, claiming one if necessary */
|
|
317
|
-
export function activeSlot(cwd) {
|
|
318
|
-
const m = loadManifest(cwd)
|
|
319
|
-
ensureActive(cwd, m)
|
|
320
|
-
return m.active
|
|
321
|
-
}
|
|
236
|
+
// ========== 认领面(2026-09-28 拆分外提至 ./session-slot-claims.mjs——见头注)==========
|
|
237
|
+
// 认领面族(staleClaims / ownerPid / ownerPids / ownerStateOf / cleanDeadOwners / ensureActive /
|
|
238
|
+
// allocateFresh / claimSlot / _releaseStats / releaseClaimsAll / activeSlot)语义原样迁至
|
|
239
|
+
// `session-slot-claims.mjs`;本档上方 import 取用(dropStaleClaims)、re-export 保既有 import 面。
|
|
240
|
+
export {
|
|
241
|
+
staleClaims, ownerPid, ownerPids, ownerStateOf, cleanDeadOwners,
|
|
242
|
+
allocateFresh, claimSlot, _releaseStats, releaseClaimsAll, activeSlot,
|
|
243
|
+
} from "./session-slot-claims.mjs"
|