@thincoder/core 0.9.4 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +14 -14
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +23 -13
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +55 -294
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +63 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +19 -15
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +90 -30
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +11 -11
- package/agent-tools/advisor-settle.mjs +17 -17
- package/agent-tools/advisor.mjs +38 -43
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +23 -20
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +65 -34
- package/agent-tools/batch-paths.mjs +186 -0
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +22 -119
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +58 -58
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +18 -13
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +3 -3
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +29 -273
- package/agent-tools/subagent-async.mjs +36 -31
- package/agent-tools/subagent-panel.mjs +38 -9
- package/agent-tools/subagent-run.mjs +44 -29
- package/agent-tools/subagent-scheduler.mjs +29 -28
- package/agent-tools/subagent-spawn.mjs +57 -25
- package/agent-tools/subagent.mjs +46 -66
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +20 -351
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +12 -7
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/git/checkpoint.mjs +4 -8
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +35 -7
- package/ledger-migrate.mjs +299 -0
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +11 -5
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +131 -66
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +100 -71
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +19 -7
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +1 -2
- package/prompts/persona-engineering.md +41 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +10 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-gc.mjs +67 -6
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -236
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/question.md +1 -0
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +10 -11
- package/tools/edit-diff.mjs +33 -25
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +11 -12
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/glob-dialect.mjs +3 -3
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/search.mjs +2 -2
- package/tools/shared.mjs +9 -7
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/peer-claims.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peer-claims.mjs — R10 多实例协作 L3 **意图认领面**(claims · TTL 租约——MULTI-INSTANCE-COLLAB
|
|
3
|
+
* §4.4 / D-MI17–D-MI21)。
|
|
4
|
+
*
|
|
5
|
+
* 语义(§4.4.1–§4.4.3):结构化写工具执行成功 ⇒ 本实例登记认领——同 `peers/{sessionId}.json`
|
|
6
|
+
* 加 `claims` / `claimsUpdatedAt` 两字段,与足迹面 `domains` / `updatedAt` **分字段分存**
|
|
7
|
+
* (落盘 = 字段级合并写,互不改写,未知字段逐字保留);租约 `CLAIM_TTL_MS`,同域再写续约
|
|
8
|
+
* (`claimedAt` 保持首次;覆盖去冗 = 方向性 `covers` 三分支,集内无被覆盖项);落盘 = 新目标
|
|
9
|
+
* 即刻 + 续约节流 `CLAIM_RENEW_FLUSH_MS`(届内零 IO);原子写经 `writeSessionFile`
|
|
10
|
+
* (.tmp + rename——rename 翻 peers 目录 mtime ⇒ 对端聚合缓存失效,认领回合内可见)。
|
|
11
|
+
* 释放 = 租约到期 / 属主进程死亡 / 记录被死清理——**无显式释放面**(D-MI19)。
|
|
12
|
+
*
|
|
13
|
+
* 读面(§4.4.4):按 `expiresAt > now` 过滤 + 本实例落盘时剪除(不 unlink——过期不是崩溃
|
|
14
|
+
* 残留);属主死亡 ⇒ 整条记录不可见并随既有死清理删除。命中 = 目标 ∩ 他实例未过期认领
|
|
15
|
+
* (`claimHits`——调用面 = 既有写前预检同点、同一聚合扫描);软提示文案 `claimNoteText`
|
|
16
|
+
* (双端逐字一致;`age` / `left` / `who` 规则同 §4.4.4);去重集 `agent._peerNoted`
|
|
17
|
+
* (每(目标 × 认领属主)每 run 至多一行——`markClaimNoted` 在提示真正附加时落标记)。
|
|
18
|
+
*
|
|
19
|
+
* 本档另归口 peers 目录路径与路径谓词(`pathsOverlap` / `covers`——`peer-domains.mjs`
|
|
20
|
+
* re-export 保既有 import 面)。测试缝:`_setPeersDirForTest` / `_resetPeersDirForTest`
|
|
21
|
+
* (沙箱)+ `_setPeerClaimsTestImpl({ nowFn })`(时钟——续约 / 过期两侧零真实等待)。
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
24
|
+
import { join } from "node:path"
|
|
25
|
+
import { configDir } from "./config.mjs"
|
|
26
|
+
import { normalizeCwd, getSessionId, sessionEnd, manifestPath, writeSessionFile } from "./session-slots.mjs"
|
|
27
|
+
|
|
28
|
+
/** 认领租约(§4.4.2):30 min——同目标再写 / 目标被已有认领覆盖 ⇒ `expiresAt = now + 本值`。 */
|
|
29
|
+
export const CLAIM_TTL_MS = 30 * 60 * 1000
|
|
30
|
+
|
|
31
|
+
/** 续约落盘节流(§4.4.3):新目标即刻落盘;纯续约距上次落盘 ≥ 本窗才落盘,其余零 IO。 */
|
|
32
|
+
export const CLAIM_RENEW_FLUSH_MS = 60 * 1000
|
|
33
|
+
|
|
34
|
+
// ─── peers 目录(测试注入缝——形态先例 = 端侧同名缝)────────────────────────
|
|
35
|
+
let peersDirOverride = null
|
|
36
|
+
export function _setPeersDirForTest(dir) { peersDirOverride = dir }
|
|
37
|
+
export function _resetPeersDirForTest() { peersDirOverride = null }
|
|
38
|
+
|
|
39
|
+
export function peersDir() {
|
|
40
|
+
return peersDirOverride ?? join(configDir, "peers")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function peerFilePath(sessionId) {
|
|
44
|
+
return join(peersDir(), `${sessionId}.json`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── 时钟缝(续约 / 过期两侧确定性断言——零真实等待)──────────────────────
|
|
48
|
+
let _testNowFn = null
|
|
49
|
+
|
|
50
|
+
export function _setPeerClaimsTestImpl({ nowFn = undefined } = {}) {
|
|
51
|
+
const prev = _testNowFn
|
|
52
|
+
_testNowFn = nowFn ?? null
|
|
53
|
+
return prev
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function _resetPeerClaimsTestImpl() {
|
|
57
|
+
_testNowFn = null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 认领面时钟(读面命中判据与落盘同一时钟——时钟缝注入时两侧确定一致;缺省 `Date.now`)。 */
|
|
61
|
+
export function claimsNow() {
|
|
62
|
+
return _testNowFn ? _testNowFn() : Date.now()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── 路径谓词(自 peer-domains.mjs 归口——单源)────────────────────────────
|
|
66
|
+
|
|
67
|
+
function normPath(p) {
|
|
68
|
+
const n = process.platform === "win32" ? String(p).toLowerCase() : String(p)
|
|
69
|
+
return (process.platform === "win32" ? n.replace(/\//g, "\\") : n.replace(/\\/g, "/")).replace(/[\\/]+$/, "")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 路径重叠:a 与 b 相等或互为目录包含(file_ops 目录级操作 / delete 整目录语义——既有
|
|
73
|
+
* 语义原样:区分大小写按平台;分隔符归一)。本地规范式 = `normPath` 单源。 */
|
|
74
|
+
export function pathsOverlap(a, b) {
|
|
75
|
+
const pa = normPath(a)
|
|
76
|
+
const pb = normPath(b)
|
|
77
|
+
if (pa === pb) return true
|
|
78
|
+
const sepN = process.platform === "win32" ? "\\" : "/"
|
|
79
|
+
const prefix = (p, q) => p === q || p.startsWith(q + sepN)
|
|
80
|
+
return prefix(pa, pb) || prefix(pb, pa)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 方向性包含(覆盖去冗判据 §4.4.1):`coarse` 覆盖 `fine` = 相等或 coarse 为 fine 的目录
|
|
84
|
+
* **前缀**(单方向——对称谓词 `pathsOverlap` 不作包含判据)。归一 / 大小写同款。 */
|
|
85
|
+
export function covers(coarse, fine) {
|
|
86
|
+
const pc = normPath(coarse)
|
|
87
|
+
const pf = normPath(fine)
|
|
88
|
+
if (pc === pf) return true
|
|
89
|
+
return pf.startsWith(pc + (process.platform === "win32" ? "\\" : "/"))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── 认领登记 / 落盘(写成功钩子——§4.4.3)────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** 认领状态载体 = `agent._peerClaims`(Map<target, {target, claimedAt, expiresAt}>,与足迹面
|
|
95
|
+
* `agent._peerWritten` 同型);落盘账 = `agent._peerClaimsFlushed`(Set<target>)+
|
|
96
|
+
* `agent._peerClaimsFlushedAt`(上次落盘时刻——续约节流锚)。 */
|
|
97
|
+
|
|
98
|
+
function pendingClaims(agent) {
|
|
99
|
+
return agent._peerClaims ?? (agent._peerClaims = new Map())
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 过期剪除(§4.4.2——读面过滤之外的落盘面剪除):租约已结束的认领不参与覆盖判定,
|
|
103
|
+
* 也不占据续约对象;剪除同时清落盘账(该目标在盘上已失效 ⇒ 再认领视同新目标)。 */
|
|
104
|
+
function pruneClaims(agent, now) {
|
|
105
|
+
const set = pendingClaims(agent)
|
|
106
|
+
const flushed = agent._peerClaimsFlushed
|
|
107
|
+
for (const [target, claim] of [...set]) {
|
|
108
|
+
if (claim.expiresAt > now) continue
|
|
109
|
+
set.delete(target)
|
|
110
|
+
flushed?.delete(target)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 认领登记 + 落盘决策(§4.4.3——写工具成功钩子调用;零新手工动作)。三分支(§4.4.1):
|
|
116
|
+
* 已有认领 `covers` 新目标 ⇒ 只续约;新目标 `covers` 已有认领项 ⇒ 替换(被覆盖项出集);
|
|
117
|
+
* 无包含关系 ⇒ 新目标入集。落盘:含未落盘目标 ⇒ 即刻整写;否则距上次落盘 ≥
|
|
118
|
+
* `CLAIM_RENEW_FLUSH_MS` ⇒ 续约落盘;其余零 IO。失败容忍(NF2——下次写重试)。
|
|
119
|
+
*/
|
|
120
|
+
export function recordPeerClaims(agent, targets) {
|
|
121
|
+
try {
|
|
122
|
+
const now = claimsNow()
|
|
123
|
+
pruneClaims(agent, now)
|
|
124
|
+
const set = pendingClaims(agent)
|
|
125
|
+
for (const target of targets) {
|
|
126
|
+
const covering = [...set.values()].find((c) => covers(c.target, target))
|
|
127
|
+
if (covering) {
|
|
128
|
+
covering.expiresAt = now + CLAIM_TTL_MS // 续约(claimedAt 保持首次)
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
for (const [t, c] of [...set]) {
|
|
132
|
+
if (covers(target, c.target)) {
|
|
133
|
+
set.delete(t)
|
|
134
|
+
agent._peerClaimsFlushed?.delete(t)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
set.set(target, { target, claimedAt: now, expiresAt: now + CLAIM_TTL_MS })
|
|
138
|
+
}
|
|
139
|
+
if (set.size === 0) return
|
|
140
|
+
const flushed = agent._peerClaimsFlushed ?? (agent._peerClaimsFlushed = new Set())
|
|
141
|
+
const hasUnflushed = [...set.keys()].some((t) => !flushed.has(t))
|
|
142
|
+
const due = now - (agent._peerClaimsFlushedAt ?? 0) >= CLAIM_RENEW_FLUSH_MS
|
|
143
|
+
if (hasUnflushed || due) flushPeerClaims(agent, now)
|
|
144
|
+
} catch {
|
|
145
|
+
/* 登记失败不影响工具结果(工具主流程永不受认领面影响) */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 读既有实例文件(字段级合并写基线):缺失 / 损坏 / 结构非法 ⇒ `{}`(按缺失降级——
|
|
150
|
+
* 损坏件不删,本次落盘覆盖)。 */
|
|
151
|
+
export function readPeerRecord(file) {
|
|
152
|
+
try {
|
|
153
|
+
const rec = JSON.parse(readFileSync(file, "utf8"))
|
|
154
|
+
return rec && typeof rec === "object" && !Array.isArray(rec) ? rec : {}
|
|
155
|
+
} catch {
|
|
156
|
+
return {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 认领落盘(§4.4.1 字段级合并写):只改 `claims` / `claimsUpdatedAt`——其余字段(含足迹面
|
|
161
|
+
* `domains` / `updatedAt`、身份字段、未知字段)逐字保留;身份字段仅在缺失时补齐(本面
|
|
162
|
+
* 不写他面字段)。原子写 = `writeSessionFile`(.tmp + rename)。落盘门控 = 本 cwd 会话
|
|
163
|
+
* manifest 在场(测试卫生——无头 / 未绑定会话不向真实 peers 目录写任何东西;端侧既有
|
|
164
|
+
* L3 门同法)。过期条目在落盘时剪除。 */
|
|
165
|
+
export function flushPeerClaims(agent, now = claimsNow()) {
|
|
166
|
+
try {
|
|
167
|
+
const cwd = agent?.cwd
|
|
168
|
+
if (!cwd || !existsSync(manifestPath(cwd))) return
|
|
169
|
+
const sessionId = getSessionId()
|
|
170
|
+
const base = readPeerRecord(peerFilePath(sessionId))
|
|
171
|
+
const claims = [...pendingClaims(agent).values()]
|
|
172
|
+
.filter((c) => c.expiresAt > now)
|
|
173
|
+
.map((c) => ({ target: c.target, claimedAt: c.claimedAt, expiresAt: c.expiresAt }))
|
|
174
|
+
.sort((a, b) => (a.target < b.target ? -1 : a.target > b.target ? 1 : 0))
|
|
175
|
+
const payload = { ...base }
|
|
176
|
+
if (payload.sessionId === undefined) payload.sessionId = sessionId
|
|
177
|
+
if (payload.pid === undefined) payload.pid = process.pid
|
|
178
|
+
if (payload.end === undefined) payload.end = sessionEnd()
|
|
179
|
+
if (payload.cwd === undefined) payload.cwd = normalizeCwd(cwd)
|
|
180
|
+
payload.claims = claims
|
|
181
|
+
payload.claimsUpdatedAt = now
|
|
182
|
+
writeSessionFile(peerFilePath(sessionId), payload)
|
|
183
|
+
agent._peerClaimsFlushed = new Set(claims.map((c) => c.target))
|
|
184
|
+
agent._peerClaimsFlushedAt = now
|
|
185
|
+
} catch {
|
|
186
|
+
/* NF2:失败容忍——下次写重试(落盘账不推进) */
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── 命中判据 / 文案 / 去重(§4.4.4)──────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/** 聚合载荷中解析出的认领项形态(`scanPeersDir` 消费——结构非法按缺失)。 */
|
|
193
|
+
export function parseClaims(raw) {
|
|
194
|
+
if (!Array.isArray(raw)) return []
|
|
195
|
+
const out = []
|
|
196
|
+
for (const c of raw) {
|
|
197
|
+
if (!c || typeof c !== "object") continue
|
|
198
|
+
if (typeof c.target !== "string" || c.target.length === 0) continue
|
|
199
|
+
const claimedAt = Number(c.claimedAt) || 0
|
|
200
|
+
const expiresAt = Number(c.expiresAt) || 0
|
|
201
|
+
if (expiresAt <= 0) continue
|
|
202
|
+
out.push({ target: c.target, claimedAt, expiresAt })
|
|
203
|
+
}
|
|
204
|
+
return out
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 命中判据(§4.4.4):`peers`(聚合载荷——同 cwd / self 已排除 / 属主已判活)中各实例的
|
|
208
|
+
* **未过期**认领 ∩ `target` ⇒ `[{end, pid, sessionId, claimedAt, expiresAt}]`(同属主同
|
|
209
|
+
* 目标至多一条)。纯只读、不抛。 */
|
|
210
|
+
export function claimHits(peers, target, now) {
|
|
211
|
+
const out = []
|
|
212
|
+
for (const p of peers ?? []) {
|
|
213
|
+
const hit = (p.claims ?? []).find((c) => c.expiresAt > now && pathsOverlap(target, c.target))
|
|
214
|
+
if (hit) out.push({ end: p.end, pid: p.pid, sessionId: p.sessionId, claimedAt: hit.claimedAt, expiresAt: hit.expiresAt })
|
|
215
|
+
}
|
|
216
|
+
return out
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** 认领时长(§4.4.4):`just now`(< 1 min)/ `${n} min ago`。 */
|
|
220
|
+
export function claimAge(claimedAt, now) {
|
|
221
|
+
const min = Math.floor((now - claimedAt) / 60000)
|
|
222
|
+
return min < 1 ? "just now" : `${min} min ago`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 剩余租约(§4.4.4):`max(1, ceil((expiresAt − now) / 60000))`(分钟)。 */
|
|
226
|
+
export function claimLeft(expiresAt, now) {
|
|
227
|
+
return Math.max(1, Math.ceil((expiresAt - now) / 60000))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** `who` 串(§4.4.4 逐字规则):认领属主 `${end} pid=${pid}`(无 end 则 `pid=${pid}`);
|
|
231
|
+
* 仅足迹命中的属主追加 ` (recent write)`;以 ", " 连接。 */
|
|
232
|
+
export function claimWho(owners) {
|
|
233
|
+
return owners
|
|
234
|
+
.map((o) => `${o.end ? `${o.end} ` : ""}pid=${o.pid}${o.recentWrite ? " (recent write)" : ""}`)
|
|
235
|
+
.join(", ")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 认领软提示文案(逐字契约 §4.4.4——双端一致;禁自行解释)。 */
|
|
239
|
+
export function claimNoteText(target, who, age, left) {
|
|
240
|
+
return `[peer-collab] ${target} — another live instance (${who}) holds a live intent claim on it (claimed ${age}, lease ${left} min left); concurrent edits may overwrite each other. Write not blocked — coordinate before proceeding.`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** 去重键(§4.4.4):同一(目标 × 认领属主)每 run 至多一行——键 = `${target}\\u0000${sessionId}`。
|
|
244
|
+
* `||`(非 `??`)兜底:缺 `sessionId` 的载荷不得与空串共键(端侧聚合允许空串)。 */
|
|
245
|
+
export function claimNoteKey(target, owner) {
|
|
246
|
+
return `${target}\u0000${owner.sessionId || owner.pid}`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function claimNoted(agent, key) {
|
|
250
|
+
return agent?._peerNoted instanceof Set && agent._peerNoted.has(key)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 落去重标记(调用方**在提示真正附加时**调用——写失败路径不消耗额度)。 */
|
|
254
|
+
export function markClaimNoted(agent, keys) {
|
|
255
|
+
if (!agent || !Array.isArray(keys) || keys.length === 0) return
|
|
256
|
+
const set = agent._peerNoted instanceof Set ? agent._peerNoted : (agent._peerNoted = new Set())
|
|
257
|
+
for (const k of keys) set.add(k)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 去重集清空(§4.4.4——落点 = `flushPeerDomains` 首步:先于「无写入即返回」早退)。 */
|
|
261
|
+
export function clearClaimNoted(agent) {
|
|
262
|
+
if (agent?._peerNoted instanceof Set) agent._peerNoted.clear()
|
|
263
|
+
}
|
package/peer-domains.mjs
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 登记存储(D-L3a):~/.thincoder/peers/{sessionId}.json——每实例单文件、单写者(本
|
|
6
6
|
* 进程——end marker 同型模式);内容 {sessionId, pid, end, cwd, domains[绝对路径],
|
|
7
|
-
* updatedAt}
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* updatedAt} + 意图认领面 claims / claimsUpdatedAt(§4.4.1——两字段属 `peer-claims.mjs`,
|
|
8
|
+
* 本档只做字段级合并写)。写点 = 回合级:写工具钩子累积"本回合实际写过的文件"(D-L3b
|
|
9
|
+
* 同一钩子——检测+记录一次完成)→ 回合结束 flush 整写一次(finalizeAgentTurn);本回合
|
|
10
|
+
* 无写入则不写(文件按自身 updatedAt 自然过期——hot 5 分钟窗口语义不被空回合提前清掉)。
|
|
11
|
+
*
|
|
12
|
+
* 意图认领面接线(§4.4 / D-MI17–D-MI21):认领登记 / 落盘 / 命中判据 / 文案住 `peer-claims.mjs`
|
|
13
|
+
* ——本档接线 = `recordPeerWrites` 调认领登记 · `peerCollabNote` 组合认领行 · `flushPeerDomains` 首步清空去重集 + 字段级合并写 · 聚合载荷增 `claims`。
|
|
10
14
|
*
|
|
11
15
|
* 冲突检测(D-L3b):结构化写工具(PEER_WRITE_TOOLS)执行前查 conflicts——命中他实例
|
|
12
16
|
* hot 域 → 工具结果附软提示(决策⑥ A:不阻止)。缓存(评审修正 #2):聚合结果按 peers
|
|
@@ -16,11 +20,18 @@
|
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { statSync, readdirSync, readFileSync, unlinkSync } from "node:fs"
|
|
19
|
-
import { join, resolve
|
|
20
|
-
import {
|
|
21
|
-
import { normalizeCwd, getSessionId, END, writeSessionFile } from "./session-slots.mjs"
|
|
23
|
+
import { join, resolve } from "node:path"
|
|
24
|
+
import { normalizeCwd, getSessionId, sessionEnd, writeSessionFile } from "./session-slots.mjs"
|
|
22
25
|
import { batchAlive } from "./peer-instances.mjs"
|
|
23
|
-
import { FILE_MUTATORS } from "./agent/helpers.mjs"
|
|
26
|
+
import { FILE_MUTATORS, toolTouchPaths } from "./agent/helpers.mjs"
|
|
27
|
+
// 认领面(§4.4)归口 `peer-claims.mjs`;re-export 路径与谓词保既有 import 面(单源不复制)。
|
|
28
|
+
import {
|
|
29
|
+
peersDir, peerFilePath, pathsOverlap, claimHits, claimNoteText, claimAge, claimLeft,
|
|
30
|
+
claimWho, claimNoteKey, claimNoted, markClaimNoted, clearClaimNoted, claimsNow,
|
|
31
|
+
recordPeerClaims, readPeerRecord, parseClaims,
|
|
32
|
+
} from "./peer-claims.mjs"
|
|
33
|
+
|
|
34
|
+
export { peersDir, peerFilePath, pathsOverlap, markClaimNoted }
|
|
24
35
|
|
|
25
36
|
/** hot 窗口:登记(回合足迹 flush)后 5 分钟内视为 hot(决策⑤ A——"刚写过"语义) */
|
|
26
37
|
export const HOT_WINDOW_MS = 5 * 60 * 1000
|
|
@@ -30,50 +41,43 @@ export const HOT_WINDOW_MS = 5 * 60 * 1000
|
|
|
30
41
|
* 写工具(同一文件写入面),file_ops 在 FILE_MUTATORS 之外故显式并入。 */
|
|
31
42
|
export const PEER_WRITE_TOOLS = new Set([...FILE_MUTATORS, "file_ops"])
|
|
32
43
|
|
|
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
44
|
// 模块级测试注入缝(default null = 生产实现;测试注入 + finally 恢复)
|
|
42
45
|
let _testAliveFn = null
|
|
46
|
+
let _testStatFn = null
|
|
43
47
|
|
|
44
|
-
/** 注入聚合判活实现(批量判活计数/去真实 exec——T-L3b 死清理用例可用真死 pid
|
|
45
|
-
|
|
48
|
+
/** 注入聚合判活实现(批量判活计数/去真实 exec——T-L3b 死清理用例可用真死 pid 亦可注入)+
|
|
49
|
+
* 目录 stat 实现(N-MI7 成本断言——「目录 stat ≤ 1 / 缓存命中零扫描」,AC-IC10 计数面)。 */
|
|
50
|
+
export function _setPeerDomainsTestImpl({ aliveFn = undefined, statFn = undefined } = {}) {
|
|
46
51
|
const prev = _testAliveFn
|
|
47
52
|
_testAliveFn = aliveFn ?? null
|
|
53
|
+
_testStatFn = statFn ?? null
|
|
48
54
|
peersDirCache = null // 注入即环境变更——目录缓存失效(测试间不串)
|
|
49
55
|
return prev
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
export function _resetPeerDomainsTestImpl() {
|
|
53
59
|
_testAliveFn = null
|
|
60
|
+
_testStatFn = null
|
|
54
61
|
peersDirCache = null
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
// peers 目录 mtime 惰性缓存(评审修正 #2
|
|
58
|
-
|
|
64
|
+
// peers 目录 mtime 惰性缓存(评审修正 #2)——单全局项(缓存键含目录路径:沙箱换目录即
|
|
65
|
+
// 自然失效,`_setPeersDirForTest` 自足)
|
|
66
|
+
let peersDirCache = null // { dir, mtimeMs, peers: [...] }
|
|
59
67
|
|
|
60
68
|
function statDirMtimeMs(dir) {
|
|
61
69
|
try {
|
|
62
|
-
return statSync(dir).mtimeMs
|
|
70
|
+
return (_testStatFn ?? statSync)(dir).mtimeMs
|
|
63
71
|
} catch {
|
|
64
72
|
return null // 目录缺失
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
76
|
+
/** 目标路径解析(#327 单源谓词 `toolTouchPaths`——`docs/core/design/TOOLS.md` §6.17:钩子裁决 /
|
|
77
|
+
* `file_ops` 无钩子 ⇒ 动作感知(`copy` ⇒ 只 `dest`;`move` / `rename` / 缺 `action` ⇒ source+dest);
|
|
78
|
+
* 其余单参兜底)。相对路径按 cwd 解析为绝对(resolve:绝对入参原样保留);畸形入参跳过。 */
|
|
71
79
|
export function peerWriteTargets(tool, args, cwd) {
|
|
72
|
-
const raw = tool
|
|
73
|
-
? tool.touchedPaths(args ?? {})
|
|
74
|
-
: tool?.name === "file_ops"
|
|
75
|
-
? [args?.source, args?.dest]
|
|
76
|
-
: [args?.path]
|
|
80
|
+
const raw = toolTouchPaths(tool, args) // 单源谓词(恒数组 · 恒零抛——原裸抛面收口)
|
|
77
81
|
const out = []
|
|
78
82
|
for (const p of raw) {
|
|
79
83
|
if (typeof p !== "string" || p.length === 0) continue
|
|
@@ -86,25 +90,9 @@ export function peerWriteTargets(tool, args, cwd) {
|
|
|
86
90
|
return out
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
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 }]。目录缺失/不可读 → []。 */
|
|
93
|
+
/** 扫描 peers 目录:解析每文件 + 一次批量判活全部 pid + 死登记惰性清理(unlink——崩溃残留);
|
|
94
|
+
* 损坏文件按缺失降级(不删——属主下次 flush 覆盖)。返回活登记
|
|
95
|
+
* [{ sessionId, pid, end, cwd, domains, updatedAt, claims, claimsUpdatedAt }](认领面同载荷)。 */
|
|
108
96
|
function scanPeersDir() {
|
|
109
97
|
let files = []
|
|
110
98
|
try {
|
|
@@ -120,7 +108,8 @@ function scanPeersDir() {
|
|
|
120
108
|
const rec = JSON.parse(readFileSync(file, "utf8"))
|
|
121
109
|
if (rec && typeof rec === "object" && typeof rec.sessionId === "string" && rec.sessionId.length > 0
|
|
122
110
|
&& Number.isInteger(Number(rec.pid)) && Number(rec.pid) > 0
|
|
123
|
-
&& typeof rec.cwd === "string"
|
|
111
|
+
&& typeof rec.cwd === "string"
|
|
112
|
+
&& (Array.isArray(rec.domains) || Array.isArray(rec.claims))) { // 认领面先落盘的记录(尚无 domains)同样可解析
|
|
124
113
|
parsed.push({ file, rec })
|
|
125
114
|
}
|
|
126
115
|
// 结构非法 = 损坏 → 按缺失降级(不删——NF2 同型)
|
|
@@ -148,8 +137,11 @@ function scanPeersDir() {
|
|
|
148
137
|
pid: Number(rec.pid),
|
|
149
138
|
end: typeof rec.end === "string" ? rec.end : undefined,
|
|
150
139
|
cwd: rec.cwd,
|
|
151
|
-
domains: rec.domains.filter((d) => typeof d === "string"),
|
|
140
|
+
domains: (rec.domains ?? []).filter((d) => typeof d === "string"),
|
|
152
141
|
updatedAt: Number(rec.updatedAt) || 0,
|
|
142
|
+
// 认领面(§4.4.4——同一聚合扫描供两面;旧记录无 claims ⇒ 按缺失降级)
|
|
143
|
+
claims: parseClaims(rec.claims),
|
|
144
|
+
claimsUpdatedAt: Number(rec.claimsUpdatedAt) || 0,
|
|
153
145
|
})
|
|
154
146
|
}
|
|
155
147
|
return live
|
|
@@ -164,14 +156,14 @@ function cachedScan() {
|
|
|
164
156
|
peersDirCache = null
|
|
165
157
|
return []
|
|
166
158
|
}
|
|
167
|
-
if (peersDirCache && peersDirCache.mtimeMs === mtime) return peersDirCache.peers
|
|
159
|
+
if (peersDirCache && peersDirCache.dir === dir && peersDirCache.mtimeMs === mtime) return peersDirCache.peers
|
|
168
160
|
const peers = scanPeersDir()
|
|
169
161
|
if (peers === null) {
|
|
170
162
|
peersDirCache = null // 探测失败——不缓存空聚合(下轮调用重试探测——失败 ≠ 无登记)
|
|
171
163
|
return []
|
|
172
164
|
}
|
|
173
165
|
// 死清理删文件会改目录 mtime——以清理后的 mtime 缓存(下次 stat 命中即不重扫)
|
|
174
|
-
peersDirCache = { mtimeMs: statDirMtimeMs(dir) ?? mtime, peers }
|
|
166
|
+
peersDirCache = { dir, mtimeMs: statDirMtimeMs(dir) ?? mtime, peers }
|
|
175
167
|
return peers
|
|
176
168
|
}
|
|
177
169
|
|
|
@@ -199,41 +191,73 @@ export function conflicts(cwd, targets, { now = Date.now() } = {}) {
|
|
|
199
191
|
} catch {
|
|
200
192
|
return []
|
|
201
193
|
}
|
|
202
|
-
const hot = peers.filter((p) => p.domains.length > 0 && now - p.updatedAt <= HOT_WINDOW_MS)
|
|
203
|
-
if (hot.length === 0) return []
|
|
204
194
|
const hits = []
|
|
205
195
|
for (const target of targets) {
|
|
206
|
-
const by =
|
|
196
|
+
const by = footHitsFor(peers, target, now)
|
|
207
197
|
if (by.length > 0) hits.push({ target, by: by.map((p) => ({ end: p.end, pid: p.pid, sessionId: p.sessionId })) })
|
|
208
198
|
}
|
|
209
199
|
return hits
|
|
210
200
|
}
|
|
211
201
|
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
/** 足迹命中(D-L3b 判据单源——`conflicts` 与认领面同源同载荷,零二次扫描)。 */
|
|
203
|
+
function footHitsFor(peers, target, now) {
|
|
204
|
+
return peers.filter((p) => p.domains.length > 0 && now - p.updatedAt <= HOT_WINDOW_MS
|
|
205
|
+
&& p.domains.some((d) => pathsOverlap(target, d)))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* dispatch 写工具钩子预检(§4.4.4——一次聚合扫描供两面):认领命中(未过期)⇒ 出认领行并
|
|
210
|
+
* **抑制该目标足迹行**;仅足迹命中 ⇒ 既有足迹文案逐字零变;去重 = 同一(目标 × 认领属主)
|
|
211
|
+
* 每 run 至多一行。返回 `{ text, keys }`(`keys` = 本次新提示的去重键——调用方在**提示真正
|
|
212
|
+
* 附加**时落标记,写失败路径不消耗额度;无提示 / 降级 ⇒ null)。一次目录 stat。
|
|
213
|
+
*/
|
|
214
|
+
export function peerCollabNote(agent, tool, args) {
|
|
215
215
|
try {
|
|
216
|
-
const targets = peerWriteTargets(tool, args, cwd)
|
|
216
|
+
const targets = peerWriteTargets(tool, args, agent.cwd)
|
|
217
217
|
if (targets.length === 0) return null
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
218
|
+
const peers = peerDomains(agent.cwd)
|
|
219
|
+
if (peers.length === 0) return null
|
|
220
|
+
const now = Date.now() // 足迹面时钟(hot 窗口——D-MI5 语义零变)
|
|
221
|
+
const cnow = claimsNow() // 认领面时钟(与落盘同源——时钟缝注入侧确定一致)
|
|
222
|
+
const lines = []
|
|
223
|
+
const keys = []
|
|
224
|
+
for (const target of targets) {
|
|
225
|
+
const claims = claimHits(peers, target, cnow)
|
|
226
|
+
const foot = footHitsFor(peers, target, now)
|
|
227
|
+
const fresh = claims.filter((o) => !claimNoted(agent, claimNoteKey(target, o)))
|
|
228
|
+
if (fresh.length > 0) {
|
|
229
|
+
// 认领行:`who` = 新提示的认领属主 + 仅足迹命中的属主(` (recent write)` 后缀)
|
|
230
|
+
const footOnly = foot.filter((p) => !claims.some((c) => c.sessionId === p.sessionId))
|
|
231
|
+
const who = claimWho([
|
|
232
|
+
...fresh.map((o) => ({ end: o.end, pid: o.pid })),
|
|
233
|
+
...footOnly.map((p) => ({ end: p.end, pid: p.pid, recentWrite: true })),
|
|
234
|
+
])
|
|
235
|
+
// 代表 = 集内最早认领(multi-owner 时 `age` / `left` 的确定性取样)
|
|
236
|
+
const rep = fresh.reduce((a, b) => (a.claimedAt <= b.claimedAt ? a : b))
|
|
237
|
+
lines.push(claimNoteText(target, who, claimAge(rep.claimedAt, cnow), claimLeft(rep.expiresAt, cnow)))
|
|
238
|
+
for (const o of fresh) keys.push(claimNoteKey(target, o))
|
|
239
|
+
} else if (claims.length === 0 && foot.length > 0) {
|
|
240
|
+
const who = foot.map((p) => (p.end ? `${p.end} pid=${p.pid}` : `pid=${p.pid}`)).join(", ")
|
|
241
|
+
lines.push(`[peer-collab] ${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.`)
|
|
242
|
+
}
|
|
243
|
+
// claims.length > 0 && fresh.length === 0 ⇒ 该目标本轮已提示:抑制足迹行、不出行
|
|
244
|
+
}
|
|
245
|
+
if (lines.length === 0) return null
|
|
246
|
+
return { text: lines.join("\n"), keys }
|
|
224
247
|
} catch {
|
|
225
248
|
return null // 感知失败绝不打扰工具执行
|
|
226
249
|
}
|
|
227
250
|
}
|
|
228
251
|
|
|
229
|
-
/** 记录本回合写足迹(D-L3a 累积——D-L3b 同一钩子执行后调用;仅成功写计入"实际写过"
|
|
230
|
-
*
|
|
252
|
+
/** 记录本回合写足迹(D-L3a 累积——D-L3b 同一钩子执行后调用;仅成功写计入"实际写过")+
|
|
253
|
+
* 意图认领登记(§4.4.3——写成功即登记:新目标即刻落盘 / 续约节流;载体 agent._peerWritten)。 */
|
|
231
254
|
export function recordPeerWrites(agent, tool, args) {
|
|
232
255
|
try {
|
|
233
256
|
const targets = peerWriteTargets(tool, args, agent.cwd)
|
|
234
257
|
if (targets.length === 0) return
|
|
235
258
|
agent._peerWritten ??= new Set()
|
|
236
259
|
for (const t of targets) agent._peerWritten.add(t)
|
|
260
|
+
recordPeerClaims(agent, targets) // 认领面:登记 + 落盘决策(§4.4.3——零新手工动作)
|
|
237
261
|
} catch {
|
|
238
262
|
/* 记录失败不影响工具结果 */
|
|
239
263
|
}
|
|
@@ -241,19 +265,24 @@ export function recordPeerWrites(agent, tool, args) {
|
|
|
241
265
|
|
|
242
266
|
/**
|
|
243
267
|
* 回合末登记 flush(D-L3a——finalizeAgentTurn 调用):本回合足迹整写一次本实例文件
|
|
244
|
-
* (单写者;.tmp+rename 原子——writeSessionFile
|
|
245
|
-
*
|
|
246
|
-
*
|
|
268
|
+
* (单写者;.tmp+rename 原子——writeSessionFile)。**字段级合并写**(§4.4.1):只改足迹面
|
|
269
|
+
* `domains` / `updatedAt`——认领字段与未知字段逐字保留。首步清空认领去重集(§4.4.4——先于
|
|
270
|
+
* 「无写入即返回」早退:去重集不随无写回合泄漏)。本回合无写入 → 不写(hot 窗口不被空回合
|
|
271
|
+
* 提前清掉);写失败容忍(NF2);成功 flush 后清空回合集合。不抛(回合收尾零风险)。
|
|
247
272
|
*/
|
|
248
273
|
export function flushPeerDomains(agent) {
|
|
274
|
+
clearClaimNoted(agent) // §4.4.4 去重集清空(不抛——纯内存操作)
|
|
249
275
|
try {
|
|
250
276
|
const set = agent._peerWritten
|
|
251
277
|
if (!set || set.size === 0) return
|
|
252
278
|
const sessionId = getSessionId()
|
|
253
|
-
|
|
279
|
+
const file = peerFilePath(sessionId)
|
|
280
|
+
const base = readPeerRecord(file) // 认领字段 / 未知字段逐字保留(字段级合并写)
|
|
281
|
+
writeSessionFile(file, {
|
|
282
|
+
...base,
|
|
254
283
|
sessionId,
|
|
255
284
|
pid: process.pid,
|
|
256
|
-
end:
|
|
285
|
+
end: sessionEnd(),
|
|
257
286
|
cwd: normalizeCwd(agent.cwd),
|
|
258
287
|
domains: [...set],
|
|
259
288
|
updatedAt: Date.now(),
|
package/peer-instances.mjs
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { statSync } from "node:fs"
|
|
29
|
-
import { manifestPath, loadManifest, getSessionId,
|
|
29
|
+
import { manifestPath, loadManifest, getSessionId, sessionEnd } from "./session-slots.mjs"
|
|
30
30
|
import { batchAlive, batchAliveAsync, probeCmdlinesAsync, classifyEnd, isProductProc } from "./process-probe.mjs"
|
|
31
|
+
import { DESC } from "./tools/shared.mjs" // #15 描述外置:文本单点 = tool-docs/peer_instances.md
|
|
31
32
|
|
|
32
33
|
export { batchAlive } // 既有 import 面(peer-domains.mjs)零破——实现已外提 process-probe.mjs
|
|
33
34
|
|
|
@@ -78,10 +79,11 @@ function cachePut(cwd, entry) {
|
|
|
78
79
|
peerInstanceCache.set(cwd, entry)
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
/** manifest slotSessions → 按 sessionId 去重分组 [{ sessionId, pid, slots }]
|
|
82
|
+
/** manifest slotSessions → 按 sessionId 去重分组 [{ sessionId, pid, slots }](纯函数·零 IO)。
|
|
82
83
|
* sessionId 形如 "{pid}-{ts}-{rand}"(进程级——可去重分组,MULTI-INSTANCE-COLLAB §3.1);pid 不可解析
|
|
83
|
-
* 的条目跳过(存量清理归 saveManifest 的 cleanDeadOwners——本模块纯只读不写)。
|
|
84
|
-
|
|
84
|
+
* 的条目跳过(存量清理归 saveManifest 的 cleanDeadOwners——本模块纯只读不写)。
|
|
85
|
+
* **具名导出**(§3.1「聚合半段单源」):VSC 端引本件分组半段(端侧零本地副本)。 */
|
|
86
|
+
export function groupSlotSessions(m) {
|
|
85
87
|
const byId = new Map()
|
|
86
88
|
for (const [slot, sessionId] of Object.entries(m.slotSessions ?? {})) {
|
|
87
89
|
if (typeof sessionId !== "string" || sessionId.length === 0) continue
|
|
@@ -145,7 +147,7 @@ export async function peerInstances(cwd) {
|
|
|
145
147
|
const peers = []
|
|
146
148
|
const selfGroup = groups.find((g) => g.sessionId === myId)
|
|
147
149
|
if (selfGroup) {
|
|
148
|
-
peers.push({ pid: selfGroup.pid ?? process.pid, sessionId: myId, slots: selfGroup.slots, end:
|
|
150
|
+
peers.push({ pid: selfGroup.pid ?? process.pid, sessionId: myId, slots: selfGroup.slots, end: sessionEnd(), self: true })
|
|
149
151
|
}
|
|
150
152
|
for (const g of peerGroups) {
|
|
151
153
|
const end = known ? classifyEnd(cmdlines.get(g.pid)) : undefined
|
|
@@ -159,14 +161,11 @@ export async function peerInstances(cwd) {
|
|
|
159
161
|
/**
|
|
160
162
|
* L2 只读查询工具(MULTI-INSTANCE-COLLAB §3.3)——挂本模块导出。schema
|
|
161
163
|
* description 逐字锚(评审修正 #7——2026-09-06 定稿,双端照抄——禁止自行解释):
|
|
162
|
-
*
|
|
163
|
-
* workspace cwd. Returns [{ pid, end, sessionId, slots }]; never includes self;
|
|
164
|
-
* pure read — writes nothing."
|
|
164
|
+
* 文本本体 = `tool-docs/peer_instances.md`(DESC() 加载;#15 外置——文本逐字零改)。
|
|
165
165
|
*/
|
|
166
166
|
export const peerInstancesTool = {
|
|
167
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.",
|
|
168
|
+
description: DESC("peer_instances"), // #15 外置:文本单点 = tool-docs/peer_instances.md(逐字锚)
|
|
170
169
|
parameters: { type: "object", properties: {} },
|
|
171
170
|
readonly: true,
|
|
172
171
|
async execute(args, ctx) {
|