@thincoder/core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/peer-domains.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peer-domains.mjs — R10 多实例协作 L3 文件写域登记 + 冲突检测(MULTI-INSTANCE-COLLAB
|
|
3
|
+
* §2a.5——评审修正 #8:独立文件,防 300 行超限)。
|
|
4
|
+
*
|
|
5
|
+
* 登记存储(D-L3a):~/.thincoder/peers/{sessionId}.json——每实例单文件、单写者(本
|
|
6
|
+
* 进程——end marker 同型模式);内容 {sessionId, pid, end, cwd, domains[绝对路径],
|
|
7
|
+
* updatedAt}。写点 = 回合级:写工具钩子累积"本回合实际写过的文件"(D-L3b 同一钩子——
|
|
8
|
+
* 检测+记录一次完成)→ 回合结束 flush 整写一次(finalizeAgentTurn);本回合无写入则
|
|
9
|
+
* 不写(文件按自身 updatedAt 自然过期——hot 5 分钟窗口语义不被空回合提前清掉)。
|
|
10
|
+
*
|
|
11
|
+
* 冲突检测(D-L3b):结构化写工具(PEER_WRITE_TOOLS)执行前查 conflicts——命中他实例
|
|
12
|
+
* hot 域 → 工具结果附软提示(决策⑥ A:不阻止)。缓存(评审修正 #2):聚合结果按 peers
|
|
13
|
+
* 目录 mtime 惰性缓存——目录未变零扫描;单次写工具调用新增开销上限 = 目录 stat 一次
|
|
14
|
+
* (N3 度量)。崩溃残留:聚合时惰性清理(死 pid 文件删除——batchAlive 一次批量);
|
|
15
|
+
* 损坏文件按缺失降级(end marker NF2 同型——不删不炸,属主下次 flush 覆盖)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { statSync, readdirSync, readFileSync, unlinkSync } from "node:fs"
|
|
19
|
+
import { join, resolve, sep } from "node:path"
|
|
20
|
+
import { configDir } from "./config.mjs"
|
|
21
|
+
import { normalizeCwd, getSessionId, END, writeSessionFile } from "./session-slots.mjs"
|
|
22
|
+
import { batchAlive } from "./peer-instances.mjs"
|
|
23
|
+
import { FILE_MUTATORS } from "./agent/helpers.mjs"
|
|
24
|
+
|
|
25
|
+
/** hot 窗口:登记(回合足迹 flush)后 5 分钟内视为 hot(决策⑤ A——"刚写过"语义) */
|
|
26
|
+
export const HOT_WINDOW_MS = 5 * 60 * 1000
|
|
27
|
+
|
|
28
|
+
/** L3 覆盖的结构化写工具(D-L3b 清单)——既有 FILE_MUTATORS(write/edit/insert_after/
|
|
29
|
+
* hashline_edit/apply_patch/delete)∪ file_ops。insert_after/hashline_edit 亦为结构化
|
|
30
|
+
* 写工具(同一文件写入面),file_ops 在 FILE_MUTATORS 之外故显式并入。 */
|
|
31
|
+
export const PEER_WRITE_TOOLS = new Set([...FILE_MUTATORS, "file_ops"])
|
|
32
|
+
|
|
33
|
+
export function peersDir() {
|
|
34
|
+
return join(configDir, "peers")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function peerFilePath(sessionId) {
|
|
38
|
+
return join(peersDir(), `${sessionId}.json`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 模块级测试注入缝(default null = 生产实现;测试注入 + finally 恢复)
|
|
42
|
+
let _testAliveFn = null
|
|
43
|
+
|
|
44
|
+
/** 注入聚合判活实现(批量判活计数/去真实 exec——T-L3b 死清理用例可用真死 pid 亦可注入)。 */
|
|
45
|
+
export function _setPeerDomainsTestImpl({ aliveFn = undefined } = {}) {
|
|
46
|
+
const prev = _testAliveFn
|
|
47
|
+
_testAliveFn = aliveFn ?? null
|
|
48
|
+
peersDirCache = null // 注入即环境变更——目录缓存失效(测试间不串)
|
|
49
|
+
return prev
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function _resetPeerDomainsTestImpl() {
|
|
53
|
+
_testAliveFn = null
|
|
54
|
+
peersDirCache = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// peers 目录 mtime 惰性缓存(评审修正 #2)——单全局项(peers 目录全局唯一)
|
|
58
|
+
let peersDirCache = null // { mtimeMs, peers: [...] }
|
|
59
|
+
|
|
60
|
+
function statDirMtimeMs(dir) {
|
|
61
|
+
try {
|
|
62
|
+
return statSync(dir).mtimeMs
|
|
63
|
+
} catch {
|
|
64
|
+
return null // 目录缺失
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 目标路径解析(工具 touchedPaths 优先——apply_patch/edit-batch 多文件;file_ops 无
|
|
69
|
+
* touchedPaths——source/dest 双算;其余 path 单参兜底)。相对路径按 cwd 解析为绝对
|
|
70
|
+
* 路径(resolve:绝对入参原样保留)。解析失败/畸形入参跳过(零目标 = 无检测无登记)。 */
|
|
71
|
+
export function peerWriteTargets(tool, args, cwd) {
|
|
72
|
+
const raw = tool?.touchedPaths
|
|
73
|
+
? tool.touchedPaths(args ?? {})
|
|
74
|
+
: tool?.name === "file_ops"
|
|
75
|
+
? [args?.source, args?.dest]
|
|
76
|
+
: [args?.path]
|
|
77
|
+
const out = []
|
|
78
|
+
for (const p of raw) {
|
|
79
|
+
if (typeof p !== "string" || p.length === 0) continue
|
|
80
|
+
try {
|
|
81
|
+
out.push(resolve(cwd, p))
|
|
82
|
+
} catch {
|
|
83
|
+
/* 畸形路径跳过——检测/登记尽力而为 */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 路径重叠:a 与 b 相等或互为目录包含(file_ops 目录级操作/delete 整目录语义——
|
|
90
|
+
* 区分大小写按平台(Windows 文件系统不区分);分隔符归一(resolve 产物为原生分隔符,
|
|
91
|
+
* 入参可能混用 / 与 \)。 */
|
|
92
|
+
export function pathsOverlap(a, b) {
|
|
93
|
+
const norm = (p) => {
|
|
94
|
+
const n = process.platform === "win32" ? p.toLowerCase() : p
|
|
95
|
+
return (process.platform === "win32" ? n.replace(/\//g, "\\") : n.replace(/\\/g, "/")).replace(/[\\/]+$/, "")
|
|
96
|
+
}
|
|
97
|
+
const pa = norm(a)
|
|
98
|
+
const pb = norm(b)
|
|
99
|
+
if (pa === pb) return true
|
|
100
|
+
const sepN = process.platform === "win32" ? "\\" : "/"
|
|
101
|
+
const prefix = (p, q) => p === q || p.startsWith(q + sepN)
|
|
102
|
+
return prefix(pa, pb) || prefix(pb, pa)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 扫描 peers 目录:解析每文件 + 一次批量判活全部 pid + 死登记惰性清理(unlink——
|
|
106
|
+
* 崩溃残留);损坏文件按缺失降级(不删——属主下次 flush 覆盖)。返回活登记
|
|
107
|
+
* [{ sessionId, pid, end, cwd, domains, updatedAt }]。目录缺失/不可读 → []。 */
|
|
108
|
+
function scanPeersDir() {
|
|
109
|
+
let files = []
|
|
110
|
+
try {
|
|
111
|
+
files = readdirSync(peersDir(), { withFileTypes: true })
|
|
112
|
+
.filter((e) => e.isFile() && e.name.endsWith(".json"))
|
|
113
|
+
.map((e) => join(peersDir(), e.name))
|
|
114
|
+
} catch {
|
|
115
|
+
return [] // 目录缺失(从未 flush)→ 无登记
|
|
116
|
+
}
|
|
117
|
+
const parsed = []
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
try {
|
|
120
|
+
const rec = JSON.parse(readFileSync(file, "utf8"))
|
|
121
|
+
if (rec && typeof rec === "object" && typeof rec.sessionId === "string" && rec.sessionId.length > 0
|
|
122
|
+
&& Number.isInteger(Number(rec.pid)) && Number(rec.pid) > 0
|
|
123
|
+
&& typeof rec.cwd === "string" && Array.isArray(rec.domains)) {
|
|
124
|
+
parsed.push({ file, rec })
|
|
125
|
+
}
|
|
126
|
+
// 结构非法 = 损坏 → 按缺失降级(不删——NF2 同型)
|
|
127
|
+
} catch {
|
|
128
|
+
/* 解析失败同损坏 */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (parsed.length === 0) return []
|
|
132
|
+
const aliveFn = _testAliveFn ?? batchAlive
|
|
133
|
+
const alive = aliveFn([...new Set(parsed.map((p) => Number(p.rec.pid)))])
|
|
134
|
+
// 判活探测失败(batchAlive → null)≠ 死:不得据此执行死清理(unlink 是破坏性副作用)
|
|
135
|
+
// ——返回 null 信号:调用方不缓存空聚合(下轮调用重试探测)
|
|
136
|
+
if (!(alive instanceof Set)) return null
|
|
137
|
+
const live = []
|
|
138
|
+
for (const { file, rec } of parsed) {
|
|
139
|
+
if (!alive.has(Number(rec.pid))) {
|
|
140
|
+
try {
|
|
141
|
+
// 死登记惰性清理(崩溃残留——聚合时一次批量判活顺带删除)
|
|
142
|
+
unlinkSync(file)
|
|
143
|
+
} catch { /* unlink 失败不影响聚合(下次再试) */ }
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
live.push({
|
|
147
|
+
sessionId: rec.sessionId,
|
|
148
|
+
pid: Number(rec.pid),
|
|
149
|
+
end: typeof rec.end === "string" ? rec.end : undefined,
|
|
150
|
+
cwd: rec.cwd,
|
|
151
|
+
domains: rec.domains.filter((d) => typeof d === "string"),
|
|
152
|
+
updatedAt: Number(rec.updatedAt) || 0,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
return live
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** peers 目录 mtime 惰性缓存聚合(评审修正 #2——目录未变不重扫)。返回当前目录下全部
|
|
159
|
+
* 活登记(含他 cwd 实例——死清理跨 cwd 一次批量;cwd 过滤在 peerDomains)。 */
|
|
160
|
+
function cachedScan() {
|
|
161
|
+
const dir = peersDir()
|
|
162
|
+
const mtime = statDirMtimeMs(dir)
|
|
163
|
+
if (mtime == null) {
|
|
164
|
+
peersDirCache = null
|
|
165
|
+
return []
|
|
166
|
+
}
|
|
167
|
+
if (peersDirCache && peersDirCache.mtimeMs === mtime) return peersDirCache.peers
|
|
168
|
+
const peers = scanPeersDir()
|
|
169
|
+
if (peers === null) {
|
|
170
|
+
peersDirCache = null // 探测失败——不缓存空聚合(下轮调用重试探测——失败 ≠ 无登记)
|
|
171
|
+
return []
|
|
172
|
+
}
|
|
173
|
+
// 死清理删文件会改目录 mtime——以清理后的 mtime 缓存(下次 stat 命中即不重扫)
|
|
174
|
+
peersDirCache = { mtimeMs: statDirMtimeMs(dir) ?? mtime, peers }
|
|
175
|
+
return peers
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 聚合(D-L3a):本 cwd(normalizeCwd 同构比较)的其他活实例登记(self 排除——
|
|
180
|
+
* sessionId === getSessionId())。返回 [{ sessionId, pid, end, cwd, domains, updatedAt }]。
|
|
181
|
+
* 纯只读;缓存命中 = 目录 stat 一次(N3)。
|
|
182
|
+
*/
|
|
183
|
+
export function peerDomains(cwd) {
|
|
184
|
+
const normCwd = normalizeCwd(cwd)
|
|
185
|
+
const myId = getSessionId()
|
|
186
|
+
return cachedScan().filter((p) => p.sessionId !== myId && normalizeCwd(p.cwd) === normCwd)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 冲突检测(D-L3b——写前查询):targets(绝对路径)命中他实例 hot 域(updatedAt 在
|
|
191
|
+
* HOT_WINDOW_MS 内且域含 target/互为包含)→ 返回 [{ target, by: [{end, pid, sessionId}] }]。
|
|
192
|
+
* 无冲突/无目标 → []。纯只读、不抛(内部全降级——失败按零冲突)。
|
|
193
|
+
*/
|
|
194
|
+
export function conflicts(cwd, targets, { now = Date.now() } = {}) {
|
|
195
|
+
if (!Array.isArray(targets) || targets.length === 0) return []
|
|
196
|
+
let peers = []
|
|
197
|
+
try {
|
|
198
|
+
peers = peerDomains(cwd)
|
|
199
|
+
} catch {
|
|
200
|
+
return []
|
|
201
|
+
}
|
|
202
|
+
const hot = peers.filter((p) => p.domains.length > 0 && now - p.updatedAt <= HOT_WINDOW_MS)
|
|
203
|
+
if (hot.length === 0) return []
|
|
204
|
+
const hits = []
|
|
205
|
+
for (const target of targets) {
|
|
206
|
+
const by = hot.filter((p) => p.domains.some((d) => pathsOverlap(target, d)))
|
|
207
|
+
if (by.length > 0) hits.push({ target, by: by.map((p) => ({ end: p.end, pid: p.pid, sessionId: p.sessionId })) })
|
|
208
|
+
}
|
|
209
|
+
return hits
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** dispatch 写工具钩子预检:命中他实例 hot 域 → 软提示文案(null = 无冲突/无目标/
|
|
213
|
+
* 降级——调用方零附加)。一次目录 stat(缓存命中零扫描——N3 度量)。 */
|
|
214
|
+
export function peerCollabNote(cwd, tool, args) {
|
|
215
|
+
try {
|
|
216
|
+
const targets = peerWriteTargets(tool, args, cwd)
|
|
217
|
+
if (targets.length === 0) return null
|
|
218
|
+
const hits = conflicts(cwd, targets)
|
|
219
|
+
if (hits.length === 0) return null
|
|
220
|
+
return hits.map((h) => {
|
|
221
|
+
const who = h.by.map((p) => (p.end ? `${p.end} pid=${p.pid}` : `pid=${p.pid}`)).join(", ")
|
|
222
|
+
return `[peer-collab] ${h.target} — another live instance (${who}) registered writing it within the last 5 minutes; concurrent edits may overwrite each other. Write not blocked — coordinate before proceeding.`
|
|
223
|
+
}).join("\n")
|
|
224
|
+
} catch {
|
|
225
|
+
return null // 感知失败绝不打扰工具执行
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 记录本回合写足迹(D-L3a 累积——D-L3b 同一钩子执行后调用;仅成功写计入"实际写过")。
|
|
230
|
+
* agent._peerWritten: Set<绝对路径>(回合级——finalizeAgentTurn flush 时整写并清空)。 */
|
|
231
|
+
export function recordPeerWrites(agent, tool, args) {
|
|
232
|
+
try {
|
|
233
|
+
const targets = peerWriteTargets(tool, args, agent.cwd)
|
|
234
|
+
if (targets.length === 0) return
|
|
235
|
+
agent._peerWritten ??= new Set()
|
|
236
|
+
for (const t of targets) agent._peerWritten.add(t)
|
|
237
|
+
} catch {
|
|
238
|
+
/* 记录失败不影响工具结果 */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 回合末登记 flush(D-L3a——finalizeAgentTurn 调用):本回合足迹整写一次本实例文件
|
|
244
|
+
* (单写者;.tmp+rename 原子——writeSessionFile)。本回合无写入 → 不写(文件按自身
|
|
245
|
+
* updatedAt 自然过期——hot 窗口不被空回合提前清掉;低频 N3)。写失败容忍(NF2——end
|
|
246
|
+
* marker 同型);成功 flush 后清空回合集合。不抛(调用方回合收尾零风险)。
|
|
247
|
+
*/
|
|
248
|
+
export function flushPeerDomains(agent) {
|
|
249
|
+
try {
|
|
250
|
+
const set = agent._peerWritten
|
|
251
|
+
if (!set || set.size === 0) return
|
|
252
|
+
const sessionId = getSessionId()
|
|
253
|
+
writeSessionFile(peerFilePath(sessionId), {
|
|
254
|
+
sessionId,
|
|
255
|
+
pid: process.pid,
|
|
256
|
+
end: END,
|
|
257
|
+
cwd: normalizeCwd(agent.cwd),
|
|
258
|
+
domains: [...set],
|
|
259
|
+
updatedAt: Date.now(),
|
|
260
|
+
})
|
|
261
|
+
set.clear()
|
|
262
|
+
} catch {
|
|
263
|
+
/* NF2:失败容忍——下次 flush 重写 */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peer-instances.mjs — R10 多实例协作感知面 L1/L2(MULTI-INSTANCE-COLLAB §3)。
|
|
3
|
+
*
|
|
4
|
+
* 目标:让同 cwd 的多个 thincoder 副本在 agent 层互相感知。数据源 = 既有 SESSION.md
|
|
5
|
+
* §6.10 基建(manifest slotSessions / getSessionId——不新建平行存储,N1),纯只读(N3
|
|
6
|
+
* ——不认领、不写 manifest——本模块结构化上没有任何 fs 写调用)。
|
|
7
|
+
*
|
|
8
|
+
* - 探测族(`batchAlive` / `probeCmdlines` / `classifyEnd`)住 `process-probe.mjs`
|
|
9
|
+
* (批 1 CORE-DEFECT-FIXES 外提——探测与身份判据单源,判据面 = §3.1 F-MI6 / V3);
|
|
10
|
+
* `batchAlive` 本档 **re-export**(既有 import 面零破——`peer-domains.mjs` 零改,
|
|
11
|
+
* 其域残留清理保持**纯存在性**语义)。
|
|
12
|
+
* - peerInstances(cwd):读 manifest slotSessions → 按 sessionId 去重分组 → 一次批量判活
|
|
13
|
+
* (batchAlive)→ **命令行身份复核**(`isProductProc`——pid 复用不再误报「另有活动实例」;
|
|
14
|
+
* 命令探测失败 / 该 pid 缺行 ⇒ **保守保留**,D-MI10)→ 端字段(决策③ A);
|
|
15
|
+
* self = sessionId === getSessionId()。
|
|
16
|
+
* - 惰性:manifest mtime **∨ 快照 TTL**缓存(命中即返——零 exec 零读;TUI 假死批 2026-09-18 收正:
|
|
17
|
+
* `saveSession` 每回合重写 manifest ⇒ 纯 mtime 判据**每回合自击穿**,探测面改 TTL 下限)。
|
|
18
|
+
* manifest 缺失 → 空清单(不缓存——stat 一次成本)。
|
|
19
|
+
* - 探测**异步化**(同批):读面用 `batchAliveAsync` / `probeCmdlinesAsync`(`execFile` promise——
|
|
20
|
+
* 不得用同步 exec 占住事件循环);清理面(`cleanDeadOwners`)继续同步版(非每回合面,D-MI14)。
|
|
21
|
+
* - 测试注入缝:_setPeerInstancesTestImpl({ aliveFn, cmdlineFn, nowFn })(前两缝均**批量**语义;
|
|
22
|
+
* `nowFn` = 时钟缝——TTL 两侧可**确定性**断言,免真实 ≥ 5 s 等待)。
|
|
23
|
+
* default null 生产行为不变;测试 restore in finally。
|
|
24
|
+
* - peerInstancesTool:L2 只读查询工具(schema description 逐字锚——评审修正 #7,
|
|
25
|
+
* 双端照抄);无参、去 self、字段白名单 {pid, end, sessionId, slots}(N4)。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { statSync } from "node:fs"
|
|
29
|
+
import { manifestPath, loadManifest, getSessionId, END } from "./session-slots.mjs"
|
|
30
|
+
import { batchAlive, batchAliveAsync, probeCmdlinesAsync, classifyEnd, isProductProc } from "./process-probe.mjs"
|
|
31
|
+
|
|
32
|
+
export { batchAlive } // 既有 import 面(peer-domains.mjs)零破——实现已外提 process-probe.mjs
|
|
33
|
+
|
|
34
|
+
const CACHE_MAX = 64
|
|
35
|
+
|
|
36
|
+
/** 快照最大存活(毫秒)——mtime 变了但距上次实探不足此值 ⇒ 仍命中缓存(§3.1)。
|
|
37
|
+
* 修因 = `saveSession` 每回合重写 manifest(`session.mjs`)⇒ 纯 mtime 判据每回合自击穿,
|
|
38
|
+
* 每回合重跑 `tasklist` + `Get-CimInstance`(1–3 s **同步 exec**)。TTL 只影响新鲜度
|
|
39
|
+
* (同伴来去在人类时标),不影响正确性。 */
|
|
40
|
+
export const PEER_PROBE_TTL_MS = 5000
|
|
41
|
+
|
|
42
|
+
// 模块级测试注入缝(default null = 生产实现;测试注入 + finally 恢复——见测试文件)
|
|
43
|
+
let _testImpl = null
|
|
44
|
+
|
|
45
|
+
/** 注入测试实现。aliveFn(pids) → Set<pid>;cmdlineFn(pids) → Map<pid,cmdline>|null;
|
|
46
|
+
* nowFn() → 毫秒时钟(缺省 `Date.now`——TTL 两侧可确定性断言)。三缝均**批量**语义。
|
|
47
|
+
* 返回前值便于测试保存恢复;**未传的槽置 null**(传 `{}` = 全槽同清,等价 reset)。 */
|
|
48
|
+
export function _setPeerInstancesTestImpl({ aliveFn = undefined, cmdlineFn = undefined, nowFn = undefined } = {}) {
|
|
49
|
+
const prev = _testImpl
|
|
50
|
+
_testImpl = { aliveFn: aliveFn ?? null, cmdlineFn: cmdlineFn ?? null, nowFn: nowFn ?? null }
|
|
51
|
+
peerInstanceCache.clear() // 注入即环境变更——缓存必须失效(测试间不串)
|
|
52
|
+
return prev
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function _resetPeerInstancesTestImpl() {
|
|
56
|
+
_testImpl = null
|
|
57
|
+
peerInstanceCache.clear()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** manifest mtime ∨ 快照 TTL 惰性缓存:cwd → { mtimeMs, probedAt, peers }。缓存有上限
|
|
61
|
+
* (CACHE_MAX——旧条目先出);条目是纯函数结果快照,无锁无句柄——陈旧只影响新鲜度不影响正确性。 */
|
|
62
|
+
const peerInstanceCache = new Map()
|
|
63
|
+
|
|
64
|
+
function statMtimeMs(p) {
|
|
65
|
+
try {
|
|
66
|
+
const st = statSync(p)
|
|
67
|
+
return st.mtimeMs
|
|
68
|
+
} catch {
|
|
69
|
+
return null // 文件缺失/不可读
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cachePut(cwd, entry) {
|
|
74
|
+
if (peerInstanceCache.size >= CACHE_MAX) {
|
|
75
|
+
const oldest = peerInstanceCache.keys().next().value
|
|
76
|
+
if (oldest !== undefined) peerInstanceCache.delete(oldest)
|
|
77
|
+
}
|
|
78
|
+
peerInstanceCache.set(cwd, entry)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** manifest slotSessions → 按 sessionId 去重分组 [{ sessionId, pid, slots }]。
|
|
82
|
+
* sessionId 形如 "{pid}-{ts}-{rand}"(进程级——可去重分组,MULTI-INSTANCE-COLLAB §3.1);pid 不可解析
|
|
83
|
+
* 的条目跳过(存量清理归 saveManifest 的 cleanDeadOwners——本模块纯只读不写)。 */
|
|
84
|
+
function groupSlotSessions(m) {
|
|
85
|
+
const byId = new Map()
|
|
86
|
+
for (const [slot, sessionId] of Object.entries(m.slotSessions ?? {})) {
|
|
87
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) continue
|
|
88
|
+
let g = byId.get(sessionId)
|
|
89
|
+
if (!g) {
|
|
90
|
+
const pid = Number.parseInt(sessionId.split("-")[0], 10)
|
|
91
|
+
g = { sessionId, pid: Number.isInteger(pid) ? pid : null, slots: [] }
|
|
92
|
+
byId.set(sessionId, g)
|
|
93
|
+
}
|
|
94
|
+
if (/^\d+$/.test(slot)) g.slots.push(Number(slot))
|
|
95
|
+
}
|
|
96
|
+
const groups = [...byId.values()]
|
|
97
|
+
for (const g of groups) g.slots.sort((a, b) => a - b)
|
|
98
|
+
return groups
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 汇总当前 cwd 的活 thincoder 实例(含 self——由调用方过滤;self = 本进程 sessionId)。
|
|
103
|
+
* 返回 [{ pid, sessionId, slots, self, end? }]——end 仅在批量 cmdline 探测成功时给出
|
|
104
|
+
* (N4 白名单键之外零附加字段)。纯只读(N3);manifest 缺失/损坏 → [](按缺失降级)。
|
|
105
|
+
*
|
|
106
|
+
* 身份复核(批 1 F-MI6):pid 存在性 × 命令行命中本产品标记族才算活同伴。命令**明确可得
|
|
107
|
+
* 且不命中** ⇒ 不列为同伴(pid 被他进程复用 ⇒ 消假阳性);探测失败(null)/ 缺行 ⇒
|
|
108
|
+
* 保守保留(D-MI10——探测失败 ≠ 死;只读面宁留噪音不隐藏真同伴)。
|
|
109
|
+
*
|
|
110
|
+
* **async**(TUI 假死批):内部经异步对偶探测(`execFile` promise)——每回合调用面不得同步 exec。
|
|
111
|
+
*/
|
|
112
|
+
export async function peerInstances(cwd) {
|
|
113
|
+
// 惰性:manifest mtime 未变 **∨ 快照年龄 < TTL** → 直接返回缓存快照(零 exec/零读——T-L1c)
|
|
114
|
+
const nowFn = _testImpl?.nowFn ?? Date.now
|
|
115
|
+
const mp = manifestPath(cwd)
|
|
116
|
+
const mtime = statMtimeMs(mp)
|
|
117
|
+
if (mtime == null) {
|
|
118
|
+
peerInstanceCache.delete(cwd) // manifest 尚未诞生/被删——不缓存空结果(stat 一次成本)
|
|
119
|
+
return []
|
|
120
|
+
}
|
|
121
|
+
const hit = peerInstanceCache.get(cwd)
|
|
122
|
+
if (hit && (hit.mtimeMs === mtime || nowFn() - hit.probedAt < PEER_PROBE_TTL_MS)) return hit.peers
|
|
123
|
+
|
|
124
|
+
const m = loadManifest(cwd) // 损坏 → {slots:{}, sessionId:null}——按缺失降级
|
|
125
|
+
const groups = groupSlotSessions(m)
|
|
126
|
+
const myId = getSessionId()
|
|
127
|
+
const others = groups.filter((g) => g.sessionId !== myId && g.pid != null)
|
|
128
|
+
// 活判定:其余实例的 pid 去重后一次批量(self 恒活不查;无同伴 → 零 exec)
|
|
129
|
+
// 探测失败(batchAliveAsync → null)→ 按"无活伴"降级——只读面不显示幽灵同伴
|
|
130
|
+
const aliveFn = _testImpl?.aliveFn ?? batchAliveAsync
|
|
131
|
+
const othersAlive = others.length > 0 ? await aliveFn([...new Set(others.map((g) => g.pid))]) : new Set()
|
|
132
|
+
const aliveSet = othersAlive instanceof Set ? othersAlive : new Set()
|
|
133
|
+
const liveOthers = others.filter((g) => aliveSet.has(g.pid))
|
|
134
|
+
// 身份复核 + 端字段:一次批量 cmdline 探测(仅活同伴——决策③ A;探测失败 → end 缺省)
|
|
135
|
+
let cmdlines = null
|
|
136
|
+
if (liveOthers.length > 0) {
|
|
137
|
+
const probe = _testImpl?.cmdlineFn ?? probeCmdlinesAsync
|
|
138
|
+
cmdlines = await probe([...new Set(liveOthers.map((g) => g.pid))])
|
|
139
|
+
}
|
|
140
|
+
const known = cmdlines instanceof Map
|
|
141
|
+
const peerGroups = liveOthers.filter((g) => {
|
|
142
|
+
const cmd = known ? cmdlines.get(g.pid) : undefined
|
|
143
|
+
return cmd == null || cmd === "" || isProductProc(cmd) // 缺行/探测失败 ⇒ 保守保留
|
|
144
|
+
})
|
|
145
|
+
const peers = []
|
|
146
|
+
const selfGroup = groups.find((g) => g.sessionId === myId)
|
|
147
|
+
if (selfGroup) {
|
|
148
|
+
peers.push({ pid: selfGroup.pid ?? process.pid, sessionId: myId, slots: selfGroup.slots, end: END, self: true })
|
|
149
|
+
}
|
|
150
|
+
for (const g of peerGroups) {
|
|
151
|
+
const end = known ? classifyEnd(cmdlines.get(g.pid)) : undefined
|
|
152
|
+
peers.push({ pid: g.pid, sessionId: g.sessionId, slots: g.slots, self: false, end })
|
|
153
|
+
}
|
|
154
|
+
peers.sort((a, b) => a.pid - b.pid || (a.self === b.self ? 0 : a.self ? -1 : 1))
|
|
155
|
+
cachePut(cwd, { mtimeMs: mtime, probedAt: nowFn(), peers })
|
|
156
|
+
return peers
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* L2 只读查询工具(MULTI-INSTANCE-COLLAB §3.3)——挂本模块导出。schema
|
|
161
|
+
* description 逐字锚(评审修正 #7——2026-09-06 定稿,双端照抄——禁止自行解释):
|
|
162
|
+
* "peer_instances — read-only: list other live ThinCoder instances sharing this
|
|
163
|
+
* workspace cwd. Returns [{ pid, end, sessionId, slots }]; never includes self;
|
|
164
|
+
* pure read — writes nothing."
|
|
165
|
+
*/
|
|
166
|
+
export const peerInstancesTool = {
|
|
167
|
+
name: "peer_instances",
|
|
168
|
+
description:
|
|
169
|
+
"peer_instances — read-only: list other live ThinCoder instances sharing this workspace cwd. Returns [{ pid, end, sessionId, slots }]; never includes self; pure read — writes nothing.",
|
|
170
|
+
parameters: { type: "object", properties: {} },
|
|
171
|
+
readonly: true,
|
|
172
|
+
async execute(args, ctx) {
|
|
173
|
+
const list = (await peerInstances(ctx.cwd))
|
|
174
|
+
.filter((p) => !p.self)
|
|
175
|
+
.map((p) => ({ pid: p.pid, end: p.end, sessionId: p.sessionId, slots: p.slots }))
|
|
176
|
+
return JSON.stringify(list, null, 2)
|
|
177
|
+
},
|
|
178
|
+
}
|
package/permission.mjs
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* permission.mjs — 权限闸(核内结构归位;CORE-UNIFICATION §2.5 #165「融合:权限闸按核内结构
|
|
3
|
+
* 归位 + 展示面按端注入」)。
|
|
4
|
+
*
|
|
5
|
+
* 来源:
|
|
6
|
+
* - `summarize` / `formatPermission` / `askPermission` 的**闸语义** = CLI
|
|
7
|
+
* `thincoder-cli/src/cli/permission.mjs`(逐字随迁;每工具定制化请示文案 + 非交互默认拒绝
|
|
8
|
+
* 的 safety-first 口径)。
|
|
9
|
+
* - **展示面(④ 端差)** = 按端注入:CLI 用本档默认的 TTY 文本问答;VSC 的面板卡片在装配层
|
|
10
|
+
* 传入自己的 `io.ask`(`askPermission` 的第三参),核内不出现端名分支(契约 5 / 10)。
|
|
11
|
+
*
|
|
12
|
+
* 闸语义(两端同,前提校验成立):`autoApprove` 每回合快照 + 中途 live 标志的判定在上层
|
|
13
|
+
* dispatch 内;本档只管「问一次 + 拿到布尔」。
|
|
14
|
+
*/
|
|
15
|
+
import { createInterface } from "node:readline"
|
|
16
|
+
|
|
17
|
+
/** CLI tool arg summary (truncated long JSON) */
|
|
18
|
+
export function summarize(toolArgs) {
|
|
19
|
+
const s = JSON.stringify(toolArgs)
|
|
20
|
+
return s.length > 120 ? s.slice(0, 120) + "..." : s
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Permission request key info (per-tool customized). name may include a sub-agent prefix
|
|
24
|
+
* ("coder/bash") — extract the basename for matching. */
|
|
25
|
+
export function formatPermission(name, args) {
|
|
26
|
+
const cap = (s, n = 1000) => (s.length > n ? `${s.slice(0, n)}…(共 ${s.length} 字符)` : s)
|
|
27
|
+
const base = name.includes("/") ? name.split("/").pop() : name
|
|
28
|
+
if (base === "bash") return cap(args.command ?? "")
|
|
29
|
+
if (base === "write") return `${args.path}(写入 ${(args.content ?? "").length} 字符)\n${cap(args.content ?? "", 1000)}`
|
|
30
|
+
if (base === "edit") {
|
|
31
|
+
const oldLines = cap(args.old_string ?? "", 500).split("\n").map((l) => `- ${l}`).join("\n")
|
|
32
|
+
const newLines = cap(args.new_string ?? "", 500).split("\n").map((l) => `+ ${l}`).join("\n")
|
|
33
|
+
return `${args.path}\n${oldLines}\n ↓\n${newLines}`
|
|
34
|
+
}
|
|
35
|
+
if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
|
|
36
|
+
if (base === "subagent") return cap(args.task ?? "", 500)
|
|
37
|
+
if (base === "memory") {
|
|
38
|
+
// §6 action-routed preview: put shows content, batch delete/clear show the gate args
|
|
39
|
+
const action = String(args.action ?? "")
|
|
40
|
+
if (action === "put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
|
|
41
|
+
if (action === "delete") return args.id ? `id=${args.id}${args.layer ? ` layer=${args.layer}` : ""}` : `batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
|
|
42
|
+
if (action === "clear") return `clear layer=${args.layer ?? ""} confirm=${args.confirm}`
|
|
43
|
+
return cap(summarize(args), 300)
|
|
44
|
+
}
|
|
45
|
+
return cap(summarize(args), 300)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Permission confirmation. Default channel = interactive y/n on the TTY; non-interactive
|
|
50
|
+
* defaults to deny (safety-first) — the CLI semantics, preserved verbatim.
|
|
51
|
+
*
|
|
52
|
+
* ④ 端差注入点:`io.ask({ name, toolArgs, text })` 返回 `Promise<boolean>`(或不返回 =
|
|
53
|
+
* 未处理 ⇒ 回退默认通道)。VSC 面板卡片在自己的装配层传入;核内零端名分支。
|
|
54
|
+
*
|
|
55
|
+
* @param {string} name
|
|
56
|
+
* @param {object} toolArgs
|
|
57
|
+
* @param {{ask?: Function}} [io]
|
|
58
|
+
* @returns {Promise<boolean>}
|
|
59
|
+
*/
|
|
60
|
+
export async function askPermission(name, toolArgs, io = {}) {
|
|
61
|
+
const text = formatPermission(name, toolArgs)
|
|
62
|
+
if (typeof io.ask === "function") {
|
|
63
|
+
const answer = await io.ask({ name, toolArgs, text })
|
|
64
|
+
if (answer !== undefined) return Boolean(answer)
|
|
65
|
+
}
|
|
66
|
+
if (!process.stdin.isTTY) {
|
|
67
|
+
console.error(`\n[deny] ${name} (non-interactive, side-effect tools require a TTY)`)
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr })
|
|
71
|
+
try {
|
|
72
|
+
const answer = await new Promise((resolve) => {
|
|
73
|
+
rl.question(`\n[allow?] ${name}\n${text}\n(y/N) `, resolve)
|
|
74
|
+
})
|
|
75
|
+
return answer.trim().toLowerCase() === "y"
|
|
76
|
+
} finally {
|
|
77
|
+
rl.close()
|
|
78
|
+
}
|
|
79
|
+
}
|