deskbuddy 0.2.0-beta.21 → 0.2.0-beta.23
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/dist/chunk-UKA3BUTV.js +5062 -0
- package/dist/cli.js +1374 -5770
- package/dist/enable-A2TXGR7H.js +15 -0
- package/dist/hook.cjs +24 -1
- package/dist/opencode.mjs +381 -8
- package/hook/hook-debug.log +68 -0
- package/hook/hook.cjs +24 -1
- package/hook/opencode.d.mts +22 -1
- package/hook/opencode.mjs +381 -8
- package/package.json +1 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
defaultShimEnvForInstall,
|
|
4
|
+
installShims,
|
|
5
|
+
installTools,
|
|
6
|
+
parseTools,
|
|
7
|
+
runEnable
|
|
8
|
+
} from "./chunk-UKA3BUTV.js";
|
|
9
|
+
export {
|
|
10
|
+
defaultShimEnvForInstall,
|
|
11
|
+
installShims,
|
|
12
|
+
installTools,
|
|
13
|
+
parseTools,
|
|
14
|
+
runEnable
|
|
15
|
+
};
|
package/dist/hook.cjs
CHANGED
|
@@ -258,6 +258,24 @@ function loadRegistry() {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* WorkBuddy 会话所在的配置目录:transcript_path(`<配置目录>/projects/<编码>/<sid>.jsonl`)优先,
|
|
263
|
+
* 没有就看进程环境的 CODEBUDDY_CONFIG_DIR。都没有返回 ''(daemon 回落 ~/.workbuddy)。
|
|
264
|
+
* 与 src/core/codebuddyDirs.ts 的 configDirFromTranscript 同构
|
|
265
|
+
*/
|
|
266
|
+
function configDirFromTranscript(p) {
|
|
267
|
+
if (typeof p !== 'string' || !p) return ''
|
|
268
|
+
const i = p.replace(/\\/g, '/').lastIndexOf('/projects/')
|
|
269
|
+
return i > 0 ? p.slice(0, i) : ''
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function workbuddyConfigDir(payload, env) {
|
|
273
|
+
const fromTranscript = configDirFromTranscript(payload && payload.transcript_path)
|
|
274
|
+
if (fromTranscript) return fromTranscript
|
|
275
|
+
const v = env && typeof env.CODEBUDDY_CONFIG_DIR === 'string' ? env.CODEBUDDY_CONFIG_DIR.trim() : ''
|
|
276
|
+
return v
|
|
277
|
+
}
|
|
278
|
+
|
|
261
279
|
function mapEvent(registry, tool, event, payload) {
|
|
262
280
|
const t = registry && registry.tools ? registry.tools[tool] : null
|
|
263
281
|
if (!t) return 'ignore'
|
|
@@ -2146,7 +2164,8 @@ async function main(argv) {
|
|
|
2146
2164
|
if (rec) {
|
|
2147
2165
|
const p0 = readState(sessionId)
|
|
2148
2166
|
const agentPid = p0 && p0.pid && pidExists(p0.pid) ? p0.pid : resolveAgentPid(process.pid, agentNames(registry, tool))
|
|
2149
|
-
|
|
2167
|
+
// 🔴 Bug 215:窗口跑的是别家工具(变量被继承进了别的智能体,如独立版 codebuddy:进程名认不出、pid 判不了)就不认
|
|
2168
|
+
if ((!rec.tool || rec.tool === tool) && windowOwns(rec, agentPid || undefined)) windowId = envWin
|
|
2150
2169
|
}
|
|
2151
2170
|
if (windowId) noteWindow(windowId, sessionId, event, Date.now())
|
|
2152
2171
|
}
|
|
@@ -2275,6 +2294,8 @@ async function main(argv) {
|
|
|
2275
2294
|
// transcript 路径(Claude Code 的 hook 输入带):daemon 据此认 Esc 打断(Bug 表 160)。🔴 只落本机、不上报
|
|
2276
2295
|
transcriptPath:
|
|
2277
2296
|
typeof payload.transcript_path === 'string' && payload.transcript_path ? payload.transcript_path : (prev && prev.transcriptPath) || undefined,
|
|
2297
|
+
// WorkBuddy:会话在 ~/.workbuddy 还是 ~/.codebuddy(spec 2026-10-08 codebuddy-default-dir),续写按它选目录。🔴 只落本机、不上报
|
|
2298
|
+
configDir: tool === 'workbuddy' ? workbuddyConfigDir(payload, process.env) || (prev && prev.configDir) || undefined : undefined,
|
|
2278
2299
|
}
|
|
2279
2300
|
|
|
2280
2301
|
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
@@ -2330,6 +2351,8 @@ if (require.main === module) {
|
|
|
2330
2351
|
|
|
2331
2352
|
// 仅供单测使用(被 require 时才会用到,作为 hook 跑时没有任何开销)
|
|
2332
2353
|
module.exports = {
|
|
2354
|
+
configDirFromTranscript,
|
|
2355
|
+
workbuddyConfigDir,
|
|
2333
2356
|
DEFAULT_TITLE_SOURCES,
|
|
2334
2357
|
APP_ID,
|
|
2335
2358
|
APP_SECRET,
|
package/dist/opencode.mjs
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
// 🔴 窗口模式反过来:任何失败都不表态,授权框照常由人处理。
|
|
22
22
|
|
|
23
23
|
import { spawn } from 'node:child_process'
|
|
24
|
-
import {
|
|
24
|
+
import { randomBytes } from 'node:crypto'
|
|
25
|
+
import { appendFileSync, chmodSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
25
26
|
import { dirname, join } from 'node:path'
|
|
26
27
|
import { fileURLToPath } from 'node:url'
|
|
27
28
|
import { alwaysSuggestions, askToCc, isPlainQuestion, ocReplyOf, questionToCc, rememberTitle, supportsPermissionReply, toCcTool } from './ocTranslate.mjs'
|
|
@@ -40,6 +41,11 @@ const APPROVED_CALL_TTL_MS = 60 * 1000
|
|
|
40
41
|
const CALL_TTL_MS = 10 * 60 * 1000
|
|
41
42
|
/** hook.cjs 自己有 2 秒上报超时,这里再兜一层:一次卡死不能堵住后面所有状态 */
|
|
42
43
|
const HOOK_KILL_MS = 10 * 1000
|
|
44
|
+
/**
|
|
45
|
+
* Bug 219:远程关卡(gate.cjs --run)挂着超过这么久 = 已经推到设备在等人答,这时补报 PermissionRequest(ask_approval),
|
|
46
|
+
* 服务端才会把待确认的 approvalId 挂到会话上。自动放行(只读工具等)在这之前就回来了,不会闪成待确认
|
|
47
|
+
*/
|
|
48
|
+
export const OC_ASK_REPORT_MS = 1000
|
|
43
49
|
/** chat.message 里同步取「窗口外追加」的上限:它卡着用户这句话发出去 */
|
|
44
50
|
const APPENDED_KILL_MS = 3 * 1000
|
|
45
51
|
|
|
@@ -52,6 +58,329 @@ export const OC_GATED_KEYS = [
|
|
|
52
58
|
'external_directory', 'todowrite', 'todoread', 'lsp', 'skill', 'doom_loop', 'patch', 'apply_patch',
|
|
53
59
|
]
|
|
54
60
|
|
|
61
|
+
// ---- 窗口回复信箱(spec 2026-10-08 opencode-window-reply):与 src/core/ocMailbox.ts / dshMailbox.ts 的约定一致,改一边必须改另一边 ----
|
|
62
|
+
const OC_HEARTBEAT_MS = 1000
|
|
63
|
+
const OC_INBOX_POLL_MS = 500
|
|
64
|
+
/** 会话列表多久刷一次(session.list 读盘,不必每秒) */
|
|
65
|
+
const OC_LIST_MS = 10 * 1000
|
|
66
|
+
/** 只列最近这么久更新过的顶层会话、最多这么多个 */
|
|
67
|
+
const OC_LIST_MAX_AGE_MS = 7 * 24 * 3600 * 1000
|
|
68
|
+
const OC_LIST_MAX = 200
|
|
69
|
+
const OC_ACTIVE_MAX = 20
|
|
70
|
+
/** 与 src/core/dshMailbox.ts 的 DSH_STALE_MS 一致:daemon 早已按超时回报过的信不再投 */
|
|
71
|
+
export const OC_STALE_LETTER_MS = 12 * 1000
|
|
72
|
+
/** 判忙闲只决定回报 started 还是 queued,最多等这么久 */
|
|
73
|
+
const OC_STATUS_WAIT_MS = 1000
|
|
74
|
+
/** promptAsync 是立即返回的(204),最多等这么久 */
|
|
75
|
+
const OC_PROMPT_WAIT_MS = 3 * 1000
|
|
76
|
+
/**
|
|
77
|
+
* 回执最晚在信龄这么久时写出:daemon 8 秒没回执去收回、发现已被认领再宽限 3 秒(= 11 秒报 ack_timeout),
|
|
78
|
+
* 这里留 1 秒余量。同一会话的信串行发,排在后面的信剩余时间不够发一次就不发、回 backlog(确定没发出去)
|
|
79
|
+
*/
|
|
80
|
+
export const OC_ACK_BUDGET_MS = 10 * 1000
|
|
81
|
+
/** 剩余时间少于这个就不再发(至少够 promptAsync 等 1 秒 + 写回执) */
|
|
82
|
+
const OC_MIN_SEND_MS = 1500
|
|
83
|
+
const OC_ID_RE = /^[A-Za-z0-9_-]{1,128}$/
|
|
84
|
+
export const OC_LETTER_RE = /^([A-Za-z0-9_-]{1,128})\.([A-Za-z0-9_-]{1,128})\.json$/
|
|
85
|
+
/** 同一进程里插件会被加载两次(实测 1.18.34):信箱只起一份,挂在进程全局上 */
|
|
86
|
+
export const OC_MAILBOX_KEY = Symbol.for('deskbuddy.opencode.mailbox')
|
|
87
|
+
|
|
88
|
+
/** 不是 TUI 的子命令:这些进程里不起信箱(serve / web / acp 等由别的前端驱动,界面不归我们管;attach 的插件跑在它连的 server 里) */
|
|
89
|
+
const OC_NON_TUI_COMMANDS = new Set([
|
|
90
|
+
'completion', 'acp', 'mcp', 'attach', 'run', 'debug', 'providers', 'auth', 'agent', 'upgrade', 'uninstall',
|
|
91
|
+
'serve', 'web', 'models', 'stats', 'export', 'import', 'github', 'session', 'plugin', 'plug', 'db',
|
|
92
|
+
])
|
|
93
|
+
/** 带值的全局 / TUI 选项(`--port 4096` 的 4096 不是子命令)。`--x=v` 写法不占下一个参数 */
|
|
94
|
+
const OC_VALUE_FLAGS = new Set(['--log-level', '--port', '--hostname', '--mdns-domain', '--cors', '-m', '--model', '-s', '--session', '--prompt', '--agent', '--replay-limit'])
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 是不是 TUI 形态(`opencode`、`opencode <项目目录>`、`opencode pr <n>`):`--` 之前第一个位置参数不是非 TUI 子命令。
|
|
98
|
+
* 编译版 argv = [bun, 入口, …]
|
|
99
|
+
*/
|
|
100
|
+
export function isTuiProcess(argv = process.argv) {
|
|
101
|
+
const args = argv.slice(2)
|
|
102
|
+
const end = args.indexOf('--')
|
|
103
|
+
const head = end >= 0 ? args.slice(0, end) : args
|
|
104
|
+
for (let i = 0; i < head.length; i++) {
|
|
105
|
+
const a = String(head[i])
|
|
106
|
+
if (a.startsWith('-')) {
|
|
107
|
+
if (OC_VALUE_FLAGS.has(a)) i++
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
return !OC_NON_TUI_COMMANDS.has(a)
|
|
111
|
+
}
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 同一类失败一分钟只记一行(心跳 / 轮询每秒都在跑,失败别刷屏) */
|
|
116
|
+
export function throttledLog(write, windowMs = 60 * 1000, now = Date.now) {
|
|
117
|
+
const last = new Map()
|
|
118
|
+
return (line) => {
|
|
119
|
+
const key = String(line).replace(/[::].*$/, '')
|
|
120
|
+
const t = now()
|
|
121
|
+
if (last.has(key) && t - last.get(key) < windowMs) return
|
|
122
|
+
last.set(key, t)
|
|
123
|
+
try {
|
|
124
|
+
write(line)
|
|
125
|
+
} catch (_) {
|
|
126
|
+
/* 日志写不了不影响 */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ocDebugLog(line) {
|
|
132
|
+
appendFileSync(join(HERE, 'hook-debug.log'), `${new Date().toISOString()} [opencode] ${line}\n`, 'utf8')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ocMailboxHome(env = process.env) {
|
|
136
|
+
return (env && env.DESKBUDDY_HOME) || HERE
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** session.list 的结果 → 能续写的顶层会话(新的在前),顺带记下各自目录 */
|
|
140
|
+
export function listableSessions(list, now, dirs) {
|
|
141
|
+
if (!Array.isArray(list)) return []
|
|
142
|
+
const out = []
|
|
143
|
+
for (const s of list) {
|
|
144
|
+
if (!s || typeof s.id !== 'string' || !OC_ID_RE.test(s.id) || s.parentID) continue
|
|
145
|
+
const t = s.time && typeof s.time.updated === 'number' ? s.time.updated : 0
|
|
146
|
+
if (now - t > OC_LIST_MAX_AGE_MS) continue
|
|
147
|
+
if (dirs && typeof s.directory === 'string' && s.directory) dirs.set(s.id, s.directory)
|
|
148
|
+
out.push({ id: s.id, t })
|
|
149
|
+
}
|
|
150
|
+
return out.sort((a, b) => b.t - a.t).slice(0, OC_LIST_MAX).map((x) => x.id)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** SDK 调用的结果判定:没抛、没有 error、HTTP < 400 才算成 */
|
|
154
|
+
function sdkOk(r) {
|
|
155
|
+
if (!r || r.error) return false
|
|
156
|
+
const st = r.response && typeof r.response.status === 'number' ? r.response.status : 200
|
|
157
|
+
return st < 400
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 心跳:每秒写 alive/<pid>.json;取信:≤1 秒扫一次 inbox,地址是本进程、会话是自己列着的才 rename 认领
|
|
162
|
+
* (认领名带本实例随机串;与 daemon 收回、同进程另一份插件实例互斥)→ 读 → 删 → promptAsync → 写 ack。
|
|
163
|
+
* opts:client(opencode SDK)、directory、home、pid、now、log。返回句柄(测试用;进程退出时自动清心跳)
|
|
164
|
+
*/
|
|
165
|
+
export function startOcMailbox(opts) {
|
|
166
|
+
const client = opts.client
|
|
167
|
+
const home = opts.home || ocMailboxHome()
|
|
168
|
+
const pid = opts.pid || process.pid
|
|
169
|
+
const now = opts.now || Date.now
|
|
170
|
+
const log = opts.log || (() => {})
|
|
171
|
+
const directory = opts.directory || process.cwd()
|
|
172
|
+
const token = randomBytes(4).toString('hex')
|
|
173
|
+
const dir = (name) => join(home, 'opencode', name)
|
|
174
|
+
const alivePath = join(dir('alive'), `${pid}.json`)
|
|
175
|
+
const ensure = (p) => {
|
|
176
|
+
mkdirSync(p, { recursive: true, mode: 0o700 })
|
|
177
|
+
try {
|
|
178
|
+
chmodSync(p, 0o700)
|
|
179
|
+
} catch (_) {
|
|
180
|
+
/* 改不了权限也不挡 */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const writeJson = (path, value) => {
|
|
184
|
+
const tmp = `${path}.${pid}.${token}.tmp`
|
|
185
|
+
writeFileSync(tmp, JSON.stringify(value), { encoding: 'utf8', mode: 0o600 })
|
|
186
|
+
renameSync(tmp, path)
|
|
187
|
+
}
|
|
188
|
+
let listed = []
|
|
189
|
+
const dirs = new Map() // sessionID → 目录
|
|
190
|
+
const active = [] // 新的在前
|
|
191
|
+
const children = new Set()
|
|
192
|
+
const done = new Set() // 处理过的 instructionId:同一封信绝不发第二次
|
|
193
|
+
const known = (sid) => listed.includes(sid) || active.includes(sid)
|
|
194
|
+
|
|
195
|
+
const noteActive = (sid, directory2) => {
|
|
196
|
+
if (typeof sid !== 'string' || !OC_ID_RE.test(sid) || children.has(sid)) return
|
|
197
|
+
if (typeof directory2 === 'string' && directory2) dirs.set(sid, directory2)
|
|
198
|
+
const i = active.indexOf(sid)
|
|
199
|
+
if (i >= 0) active.splice(i, 1)
|
|
200
|
+
active.unshift(sid)
|
|
201
|
+
if (active.length > OC_ACTIVE_MAX) active.length = OC_ACTIVE_MAX
|
|
202
|
+
}
|
|
203
|
+
const noteChild = (sid) => {
|
|
204
|
+
if (typeof sid !== 'string') return
|
|
205
|
+
children.add(sid)
|
|
206
|
+
const i = active.indexOf(sid)
|
|
207
|
+
if (i >= 0) active.splice(i, 1)
|
|
208
|
+
}
|
|
209
|
+
let listing = false
|
|
210
|
+
const refresh = async () => {
|
|
211
|
+
if (listing) return // 上一次还没回来(读盘慢):不叠请求
|
|
212
|
+
listing = true
|
|
213
|
+
try {
|
|
214
|
+
const r = await client.session.list({ query: { directory } })
|
|
215
|
+
if (sdkOk(r)) listed = listableSessions(r.data, now(), dirs)
|
|
216
|
+
else log(`读会话列表失败:status=${(r && r.response && r.response.status) || '?'}`)
|
|
217
|
+
} catch (e) {
|
|
218
|
+
log(`读会话列表失败:${(e && e.message) || e}`)
|
|
219
|
+
} finally {
|
|
220
|
+
listing = false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const beat = () => {
|
|
224
|
+
try {
|
|
225
|
+
ensure(dir('alive'))
|
|
226
|
+
const sessions = [...new Set([...active, ...listed])]
|
|
227
|
+
writeJson(alivePath, { pid, at: now(), sessions, active: [...active] })
|
|
228
|
+
} catch (e) {
|
|
229
|
+
log(`写心跳失败:${(e && e.message) || e}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const writeAck = (iid, ack) => {
|
|
233
|
+
try {
|
|
234
|
+
ensure(dir('ack'))
|
|
235
|
+
writeJson(join(dir('ack'), `${iid}.json`), ack)
|
|
236
|
+
} catch (e) {
|
|
237
|
+
log(`写回执失败:${(e && e.message) || e}`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const withTimeout = (p, ms) => {
|
|
241
|
+
let timer = null
|
|
242
|
+
return Promise.race([p, new Promise((x) => (timer = setTimeout(() => x('timeout'), ms)))]).finally(() => timer && clearTimeout(timer))
|
|
243
|
+
}
|
|
244
|
+
/** deadline:这封信的回执最晚要在这时写出(信龄 OC_ACK_BUDGET_MS) */
|
|
245
|
+
const deliver = async (sid, text, deadline) => {
|
|
246
|
+
if (!text.trim()) return { ok: false, reason: 'empty' }
|
|
247
|
+
const d = dirs.get(sid) || directory
|
|
248
|
+
const left = () => deadline - now()
|
|
249
|
+
let busy = false
|
|
250
|
+
// 判忙闲只影响 started / queued:时间不宽裕就不判,按闲报
|
|
251
|
+
if (left() >= OC_STATUS_WAIT_MS + OC_PROMPT_WAIT_MS + 500) {
|
|
252
|
+
try {
|
|
253
|
+
const st = await withTimeout(client.session.status({ query: { directory: d } }), OC_STATUS_WAIT_MS)
|
|
254
|
+
const t = st && st !== 'timeout' && st.data && st.data[sid] && st.data[sid].type
|
|
255
|
+
busy = t === 'busy' || t === 'retry'
|
|
256
|
+
} catch (_) {
|
|
257
|
+
busy = false
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 剩余时间不够发一次:不发(确定没发出去),daemon 那头按失败回报
|
|
261
|
+
if (left() < OC_MIN_SEND_MS) return { ok: false, reason: 'backlog' }
|
|
262
|
+
try {
|
|
263
|
+
const r = await withTimeout(
|
|
264
|
+
client.session.promptAsync({ path: { id: sid }, query: { directory: d }, body: { parts: [{ type: 'text', text }] } }),
|
|
265
|
+
Math.min(OC_PROMPT_WAIT_MS, left() - 500)
|
|
266
|
+
)
|
|
267
|
+
if (r === 'timeout') return { ok: false, reason: 'prompt_timeout' }
|
|
268
|
+
if (!sdkOk(r)) {
|
|
269
|
+
// 404 = 这个进程里没有这个会话,确定没发出去,daemon 可以改走后台续跑
|
|
270
|
+
const st = r && r.response && r.response.status
|
|
271
|
+
return { ok: false, reason: st === 404 ? 'not_live' : 'prompt_failed' }
|
|
272
|
+
}
|
|
273
|
+
noteActive(sid)
|
|
274
|
+
return { ok: true, busy }
|
|
275
|
+
} catch (e) {
|
|
276
|
+
log(`promptAsync 失败:${(e && e.message) || e}`)
|
|
277
|
+
return { ok: false, reason: 'prompt_failed' }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** 会话 → 这个会话上一封信的处理链:同一会话的信串行发,前一封 promptAsync 返回了才发下一封(保证先后) */
|
|
281
|
+
const chains = new Map()
|
|
282
|
+
const enqueue = (sid, iid, text, at) => {
|
|
283
|
+
const prev = chains.get(sid) || Promise.resolve()
|
|
284
|
+
const next = prev
|
|
285
|
+
.then(() => deliver(sid, text, at + OC_ACK_BUDGET_MS))
|
|
286
|
+
.then(
|
|
287
|
+
(ack) => writeAck(iid, ack),
|
|
288
|
+
() => writeAck(iid, { ok: false, reason: 'prompt_failed' })
|
|
289
|
+
)
|
|
290
|
+
chains.set(sid, next)
|
|
291
|
+
next.finally(() => {
|
|
292
|
+
if (chains.get(sid) === next) chains.delete(sid)
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
const poll = () => {
|
|
296
|
+
let names
|
|
297
|
+
try {
|
|
298
|
+
names = readdirSync(dir('inbox'))
|
|
299
|
+
} catch (_) {
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
const got = [] // 这一轮认领到的信,按信里的 at 排序后再入链
|
|
303
|
+
for (const n of names.sort()) {
|
|
304
|
+
const m = OC_LETTER_RE.exec(n)
|
|
305
|
+
if (!m || !known(m[1])) continue
|
|
306
|
+
const sid = m[1]
|
|
307
|
+
const src = join(dir('inbox'), n)
|
|
308
|
+
// 先看地址:不是给本进程的不认领(同一会话可能被同项目的另一个 TUI 也列着)
|
|
309
|
+
let peek = null
|
|
310
|
+
try {
|
|
311
|
+
peek = JSON.parse(readFileSync(src, 'utf8'))
|
|
312
|
+
} catch (_) {
|
|
313
|
+
continue // 刚被收回 / 别人认领了 / 还没写完(原子写不会出现)
|
|
314
|
+
}
|
|
315
|
+
if (!peek || (peek.pid !== undefined && peek.pid !== pid)) continue
|
|
316
|
+
const claim = `${src}.${pid}.${token}.claim`
|
|
317
|
+
try {
|
|
318
|
+
renameSync(src, claim)
|
|
319
|
+
} catch (_) {
|
|
320
|
+
continue // daemon 收回了 / 另一份实例认领了
|
|
321
|
+
}
|
|
322
|
+
let letter = null
|
|
323
|
+
try {
|
|
324
|
+
letter = JSON.parse(readFileSync(claim, 'utf8'))
|
|
325
|
+
} catch (_) {
|
|
326
|
+
letter = null
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
unlinkSync(claim)
|
|
330
|
+
} catch (_) {
|
|
331
|
+
/* 删不掉也不影响 */
|
|
332
|
+
}
|
|
333
|
+
if (!letter || letter.instructionId !== m[2] || !OC_ID_RE.test(letter.instructionId)) continue
|
|
334
|
+
const iid = letter.instructionId
|
|
335
|
+
if (done.has(iid)) continue
|
|
336
|
+
done.add(iid)
|
|
337
|
+
if (done.size > 500) done.delete(done.values().next().value)
|
|
338
|
+
const at = typeof letter.at === 'number' ? letter.at : 0
|
|
339
|
+
if (now() - at > OC_STALE_LETTER_MS) {
|
|
340
|
+
writeAck(iid, { ok: false, reason: 'stale' })
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
got.push({ sid, iid, at, text: typeof letter.text === 'string' ? letter.text : '' })
|
|
344
|
+
}
|
|
345
|
+
got.sort((a, b) => a.at - b.at)
|
|
346
|
+
for (const x of got) enqueue(x.sid, x.iid, x.text, x.at)
|
|
347
|
+
}
|
|
348
|
+
let t1 = null
|
|
349
|
+
let t2 = null
|
|
350
|
+
let t3 = null
|
|
351
|
+
const stop = () => {
|
|
352
|
+
if (t1) clearInterval(t1)
|
|
353
|
+
if (t2) clearInterval(t2)
|
|
354
|
+
if (t3) clearInterval(t3)
|
|
355
|
+
t1 = t2 = t3 = null
|
|
356
|
+
try {
|
|
357
|
+
unlinkSync(alivePath)
|
|
358
|
+
} catch (_) {
|
|
359
|
+
/* 已经没了 */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const handle = { noteActive, noteChild, refresh, beat, poll, stop, pid, token }
|
|
363
|
+
if (opts.timers !== false) {
|
|
364
|
+
refresh().finally(beat)
|
|
365
|
+
t1 = setInterval(beat, OC_HEARTBEAT_MS)
|
|
366
|
+
t2 = setInterval(poll, OC_INBOX_POLL_MS)
|
|
367
|
+
t3 = setInterval(() => void refresh(), OC_LIST_MS)
|
|
368
|
+
for (const t of [t1, t2, t3]) if (t.unref) t.unref()
|
|
369
|
+
process.once('exit', stop)
|
|
370
|
+
}
|
|
371
|
+
return handle
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 进程里只起一份信箱(插件实测会被加载两次);已有就复用。能力不够(老 SDK 没有 promptAsync)返回 null */
|
|
375
|
+
export function ensureOcMailbox(ctx, g = globalThis, log = throttledLog(ocDebugLog)) {
|
|
376
|
+
const client = ctx && ctx.client
|
|
377
|
+
if (!client || !client.session || typeof client.session.promptAsync !== 'function' || typeof client.session.list !== 'function') return null
|
|
378
|
+
if (g[OC_MAILBOX_KEY]) return g[OC_MAILBOX_KEY]
|
|
379
|
+
const h = startOcMailbox({ client, log, directory: (ctx && (ctx.directory || ctx.worktree)) || process.cwd() })
|
|
380
|
+
g[OC_MAILBOX_KEY] = h
|
|
381
|
+
return h
|
|
382
|
+
}
|
|
383
|
+
|
|
55
384
|
function runIdFromEnv() {
|
|
56
385
|
const v = process.env.DESKBUDDY_GATE_RUN || ''
|
|
57
386
|
return /^[0-9a-zA-Z_-]{1,64}$/.test(v) ? v : ''
|
|
@@ -193,6 +522,16 @@ export function make(opts = {}) {
|
|
|
193
522
|
const cwd = (ctx && (ctx.directory || ctx.worktree)) || process.cwd()
|
|
194
523
|
const runId = runIdFromEnv()
|
|
195
524
|
const headless = isHeadless()
|
|
525
|
+
// 窗口回复信箱:只在交互进程里起(远程 run / 一次性 run 里不收信)
|
|
526
|
+
// 只在 TUI 形态里起:serve / web / acp 等由别的前端驱动,不归我们(评审 2026-10-08 第 3 条)
|
|
527
|
+
const mailbox = runId || headless || opts.mailbox === false || !isTuiProcess(opts.argv || process.argv) ? null : ensureOcMailbox(ctx)
|
|
528
|
+
const touch = (sid, d) => {
|
|
529
|
+
try {
|
|
530
|
+
if (mailbox) mailbox.noteActive(sid, d)
|
|
531
|
+
} catch (_) {
|
|
532
|
+
/* 不影响别的 */
|
|
533
|
+
}
|
|
534
|
+
}
|
|
196
535
|
|
|
197
536
|
// ---- 状态上报:串行喂 hook.cjs,保证 PreToolUse / PostToolUse / Stop 的先后 ----
|
|
198
537
|
let queue = Promise.resolve()
|
|
@@ -242,6 +581,7 @@ export function make(opts = {}) {
|
|
|
242
581
|
const call = callID ? calls.get(callID) : null
|
|
243
582
|
const cc = call ? call.cc : askToCc(p)
|
|
244
583
|
report('PermissionRequest', sessionID, cc)
|
|
584
|
+
askedReq.set(id, sessionID)
|
|
245
585
|
if (runId || headless || !client) return
|
|
246
586
|
// 同一调用的后续授权框直接代答:只在设备那张卡显示的就是真实参数时(approvedCalls 只在那时写入)
|
|
247
587
|
const at = callID && approvedCalls.get(callID)
|
|
@@ -380,7 +720,16 @@ export function make(opts = {}) {
|
|
|
380
720
|
}
|
|
381
721
|
}
|
|
382
722
|
|
|
723
|
+
const askedReq = new Map() // requestID → sessionID:答完报回 watch 用
|
|
383
724
|
const onReplied = (p) => {
|
|
725
|
+
// Bug 219:授权框答完(窗口里 / 设备上都会有 replied)报回 watch,不然一直停在待确认,直到工具跑完的 PostToolUse。
|
|
726
|
+
// 🔴 不带 tool_name / tool_input:replied 在「拒绝」时也触发,带了工具信息会进 recent 生成指纹,
|
|
727
|
+
// coordinator 推断电脑上的决定时会把它当成工具跑完 → PC_ALLOW(评审必修)。不带工具信息只改状态
|
|
728
|
+
const sid = p && askedReq.get(p.requestID)
|
|
729
|
+
if (sid) {
|
|
730
|
+
askedReq.delete(p.requestID)
|
|
731
|
+
report('PostToolUse', sid, {})
|
|
732
|
+
}
|
|
384
733
|
// 人先在窗口里答了:结束 gate 子进程,daemon 随之撤掉设备上的卡片
|
|
385
734
|
const child = p && pending.get(p.requestID)
|
|
386
735
|
if (child) {
|
|
@@ -401,6 +750,7 @@ export function make(opts = {}) {
|
|
|
401
750
|
'chat.message': async (input, output) => {
|
|
402
751
|
// prompt 给 hook 的 first-prompt 标题来源用(只记首条,见 hook.cjs)
|
|
403
752
|
report('UserPromptSubmit', input && input.sessionID, { prompt: userText(output) })
|
|
753
|
+
touch(input && input.sessionID)
|
|
404
754
|
// 切片 C:设备在这个窗口之外追加、daemon 后台跑完的轮次,同步取出补进这条消息(常规 report 是异步的,输出不读)
|
|
405
755
|
// daemon 起的远程 run(runId)不取:那是留给用户窗口的(hook 侧还有 DESKBUDDY_HEADLESS 兜一层)
|
|
406
756
|
if (runId || !(input && input.sessionID)) return
|
|
@@ -421,12 +771,29 @@ export function make(opts = {}) {
|
|
|
421
771
|
report('PreToolUse', input && input.sessionID, cc)
|
|
422
772
|
if (!runId) return
|
|
423
773
|
// 远程关卡:等设备。任何失败都拒(见顶部)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
774
|
+
// Bug 219:opencode run 不发 permission.asked(ask 由关卡在 tool.execute.before 里等),状态一直是 watch,
|
|
775
|
+
// 服务端不计待确认。挂着超过 OC_ASK_REPORT_MS 就补报 PermissionRequest,回来后报回 watch
|
|
776
|
+
const sid = (input && input.sessionID) || ''
|
|
777
|
+
let asked = false
|
|
778
|
+
const askTimer = sid
|
|
779
|
+
? setTimeout(() => {
|
|
780
|
+
asked = true
|
|
781
|
+
report('PermissionRequest', sid, cc)
|
|
782
|
+
}, opts.askReportMs ?? OC_ASK_REPORT_MS)
|
|
783
|
+
: null
|
|
784
|
+
let r
|
|
785
|
+
try {
|
|
786
|
+
r = await runChild(node, GATE, ['--run', runId], {
|
|
787
|
+
hook_event_name: 'PreToolUse',
|
|
788
|
+
session_id: sid,
|
|
789
|
+
cwd,
|
|
790
|
+
...cc,
|
|
791
|
+
})
|
|
792
|
+
} finally {
|
|
793
|
+
if (askTimer) clearTimeout(askTimer)
|
|
794
|
+
// 同上:只改回 watch,不带工具信息(关卡可能是拒绝返回的);工具真跑完时 tool.execute.after 会再报带工具信息的那条
|
|
795
|
+
if (asked) report('PostToolUse', sid, {})
|
|
796
|
+
}
|
|
430
797
|
let decision = ''
|
|
431
798
|
try {
|
|
432
799
|
decision = JSON.parse(r.out).hookSpecificOutput.permissionDecision
|
|
@@ -463,10 +830,16 @@ export function make(opts = {}) {
|
|
|
463
830
|
case 'session.idle':
|
|
464
831
|
report('Stop', p.sessionID)
|
|
465
832
|
break
|
|
833
|
+
case 'session.status':
|
|
834
|
+
if (p.status && p.status.type === 'busy' && !children.has(p.sessionID)) touch(p.sessionID)
|
|
835
|
+
break
|
|
466
836
|
case 'session.created':
|
|
467
837
|
case 'session.updated':
|
|
468
838
|
if (event.type === 'session.updated') rememberTitle(titles, p.info)
|
|
469
|
-
if (p.info && typeof p.info.id === 'string' && p.info.parentID)
|
|
839
|
+
if (p.info && typeof p.info.id === 'string' && p.info.parentID) {
|
|
840
|
+
children.add(p.info.id)
|
|
841
|
+
if (mailbox) mailbox.noteChild(p.info.id)
|
|
842
|
+
}
|
|
470
843
|
break
|
|
471
844
|
case 'session.deleted':
|
|
472
845
|
children.delete((p.info && p.info.id) || p.sessionID)
|
package/hook/hook-debug.log
CHANGED
|
@@ -66,3 +66,71 @@
|
|
|
66
66
|
2026-10-05T02:23:41.812Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
67
67
|
2026-10-05T02:23:41.898Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
68
68
|
2026-10-05T02:23:41.898Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
69
|
+
2026-10-08T04:16:45.340Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
70
|
+
2026-10-08T04:16:45.767Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
71
|
+
2026-10-08T04:16:45.767Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
72
|
+
2026-10-08T04:16:45.863Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
73
|
+
2026-10-08T04:16:45.863Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
74
|
+
2026-10-08T05:57:20.374Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
75
|
+
2026-10-08T05:57:20.845Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
76
|
+
2026-10-08T05:57:20.845Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
77
|
+
2026-10-08T05:57:21.097Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
78
|
+
2026-10-08T05:57:21.097Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
79
|
+
2026-10-08T07:39:08.169Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
80
|
+
2026-10-08T07:39:08.637Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
81
|
+
2026-10-08T07:39:08.637Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
82
|
+
2026-10-08T07:39:08.761Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
83
|
+
2026-10-08T07:39:08.761Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
84
|
+
2026-10-08T08:54:25.494Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
85
|
+
2026-10-08T08:54:25.991Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
86
|
+
2026-10-08T08:54:26.067Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
87
|
+
2026-10-08T09:04:51.854Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
88
|
+
2026-10-08T09:04:52.293Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
89
|
+
2026-10-08T09:04:52.293Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
90
|
+
2026-10-08T09:04:52.382Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
91
|
+
2026-10-08T09:04:52.382Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
92
|
+
2026-10-08T09:04:58.029Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
93
|
+
2026-10-08T09:04:58.479Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
94
|
+
2026-10-08T09:04:58.479Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
95
|
+
2026-10-08T09:04:58.567Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
96
|
+
2026-10-08T09:04:58.567Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
97
|
+
2026-10-08T09:05:03.573Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
98
|
+
2026-10-08T09:05:04.009Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
99
|
+
2026-10-08T09:05:04.017Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
100
|
+
2026-10-08T09:05:04.111Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
101
|
+
2026-10-08T09:05:04.111Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
102
|
+
2026-10-08T09:05:17.071Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
103
|
+
2026-10-08T09:05:17.476Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
104
|
+
2026-10-08T09:05:17.476Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
105
|
+
2026-10-08T09:05:17.598Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
106
|
+
2026-10-08T09:05:17.598Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
107
|
+
2026-10-08T10:41:31.608Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
108
|
+
2026-10-08T10:41:32.095Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
109
|
+
2026-10-08T10:41:32.095Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
110
|
+
2026-10-08T10:41:32.246Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
111
|
+
2026-10-08T10:41:32.246Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
112
|
+
2026-10-08T10:48:03.752Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
113
|
+
2026-10-08T10:48:04.172Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
114
|
+
2026-10-08T10:48:04.173Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
115
|
+
2026-10-08T10:48:04.297Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
116
|
+
2026-10-08T10:48:04.297Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
117
|
+
2026-10-08T10:50:27.264Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
118
|
+
2026-10-08T10:50:27.729Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
119
|
+
2026-10-08T10:50:27.730Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
120
|
+
2026-10-08T10:50:27.847Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
121
|
+
2026-10-08T10:50:27.848Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
122
|
+
2026-10-08T10:51:37.456Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
123
|
+
2026-10-08T10:51:37.856Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
124
|
+
2026-10-08T10:51:37.856Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
125
|
+
2026-10-08T10:51:37.970Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
126
|
+
2026-10-08T10:51:37.970Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
127
|
+
2026-10-08T10:58:08.360Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
128
|
+
2026-10-08T10:58:08.926Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
129
|
+
2026-10-08T10:58:08.926Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
130
|
+
2026-10-08T10:58:09.074Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
131
|
+
2026-10-08T10:58:09.074Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
132
|
+
2026-10-08T10:58:37.599Z [opencode] 回填选择题失败:/question/{requestID}/reply status=404
|
|
133
|
+
2026-10-08T10:58:38.220Z [opencode] opencode 版本 1.4.3:带说明的拒绝不走新路由
|
|
134
|
+
2026-10-08T10:58:38.220Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
|
135
|
+
2026-10-08T10:58:38.367Z [opencode] opencode 版本 读不出:带说明的拒绝不走新路由
|
|
136
|
+
2026-10-08T10:58:38.367Z [opencode] opencode 版本不确定支持带说明的拒绝,设备上的反馈没有带给模型
|
package/hook/hook.cjs
CHANGED
|
@@ -258,6 +258,24 @@ function loadRegistry() {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/**
|
|
262
|
+
* WorkBuddy 会话所在的配置目录:transcript_path(`<配置目录>/projects/<编码>/<sid>.jsonl`)优先,
|
|
263
|
+
* 没有就看进程环境的 CODEBUDDY_CONFIG_DIR。都没有返回 ''(daemon 回落 ~/.workbuddy)。
|
|
264
|
+
* 与 src/core/codebuddyDirs.ts 的 configDirFromTranscript 同构
|
|
265
|
+
*/
|
|
266
|
+
function configDirFromTranscript(p) {
|
|
267
|
+
if (typeof p !== 'string' || !p) return ''
|
|
268
|
+
const i = p.replace(/\\/g, '/').lastIndexOf('/projects/')
|
|
269
|
+
return i > 0 ? p.slice(0, i) : ''
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function workbuddyConfigDir(payload, env) {
|
|
273
|
+
const fromTranscript = configDirFromTranscript(payload && payload.transcript_path)
|
|
274
|
+
if (fromTranscript) return fromTranscript
|
|
275
|
+
const v = env && typeof env.CODEBUDDY_CONFIG_DIR === 'string' ? env.CODEBUDDY_CONFIG_DIR.trim() : ''
|
|
276
|
+
return v
|
|
277
|
+
}
|
|
278
|
+
|
|
261
279
|
function mapEvent(registry, tool, event, payload) {
|
|
262
280
|
const t = registry && registry.tools ? registry.tools[tool] : null
|
|
263
281
|
if (!t) return 'ignore'
|
|
@@ -2146,7 +2164,8 @@ async function main(argv) {
|
|
|
2146
2164
|
if (rec) {
|
|
2147
2165
|
const p0 = readState(sessionId)
|
|
2148
2166
|
const agentPid = p0 && p0.pid && pidExists(p0.pid) ? p0.pid : resolveAgentPid(process.pid, agentNames(registry, tool))
|
|
2149
|
-
|
|
2167
|
+
// 🔴 Bug 215:窗口跑的是别家工具(变量被继承进了别的智能体,如独立版 codebuddy:进程名认不出、pid 判不了)就不认
|
|
2168
|
+
if ((!rec.tool || rec.tool === tool) && windowOwns(rec, agentPid || undefined)) windowId = envWin
|
|
2150
2169
|
}
|
|
2151
2170
|
if (windowId) noteWindow(windowId, sessionId, event, Date.now())
|
|
2152
2171
|
}
|
|
@@ -2275,6 +2294,8 @@ async function main(argv) {
|
|
|
2275
2294
|
// transcript 路径(Claude Code 的 hook 输入带):daemon 据此认 Esc 打断(Bug 表 160)。🔴 只落本机、不上报
|
|
2276
2295
|
transcriptPath:
|
|
2277
2296
|
typeof payload.transcript_path === 'string' && payload.transcript_path ? payload.transcript_path : (prev && prev.transcriptPath) || undefined,
|
|
2297
|
+
// WorkBuddy:会话在 ~/.workbuddy 还是 ~/.codebuddy(spec 2026-10-08 codebuddy-default-dir),续写按它选目录。🔴 只落本机、不上报
|
|
2298
|
+
configDir: tool === 'workbuddy' ? workbuddyConfigDir(payload, process.env) || (prev && prev.configDir) || undefined : undefined,
|
|
2278
2299
|
}
|
|
2279
2300
|
|
|
2280
2301
|
// 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
|
|
@@ -2330,6 +2351,8 @@ if (require.main === module) {
|
|
|
2330
2351
|
|
|
2331
2352
|
// 仅供单测使用(被 require 时才会用到,作为 hook 跑时没有任何开销)
|
|
2332
2353
|
module.exports = {
|
|
2354
|
+
configDirFromTranscript,
|
|
2355
|
+
workbuddyConfigDir,
|
|
2333
2356
|
DEFAULT_TITLE_SOURCES,
|
|
2334
2357
|
APP_ID,
|
|
2335
2358
|
APP_SECRET,
|