deskbuddy 0.2.0-beta.5 → 0.2.0-beta.6

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/hook.cjs CHANGED
@@ -1077,6 +1077,19 @@ function codexRemoteHas(sessionId) {
1077
1077
  return !!r && r.has(sessionId)
1078
1078
  }
1079
1079
 
1080
+ /** 并发 hook 的先后差不会超过一次 hook 的执行时长;超出这个差值说明是时钟回拨,不能当作「更晚的事件」 */
1081
+ const SUPERSEDE_MAX_MS = 2 * 60 * 1000
1082
+
1083
+ /**
1084
+ * 状态里已是比这次事件更晚发生的事件写的(hook 并发、先发生的后写完),这次不能再覆盖。
1085
+ * 🔴 差值带上限(评审):开机后校时往回拨时,状态里的 updatedAt 落在「未来」,不设上限的话在墙钟追上之前该会话的事件全被丢掉
1086
+ */
1087
+ function superseded(cur, eventAt) {
1088
+ if (!cur || typeof cur.updatedAt !== 'number') return false
1089
+ const ahead = cur.updatedAt - eventAt
1090
+ return ahead > 0 && ahead < SUPERSEDE_MAX_MS
1091
+ }
1092
+
1080
1093
  async function main(argv) {
1081
1094
  // 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
1082
1095
  const tool = (argv && argv[2]) || 'claude_code'
@@ -1084,6 +1097,10 @@ async function main(argv) {
1084
1097
  const event = payload.hook_event_name || ''
1085
1098
  const sessionId = payload.session_id || ''
1086
1099
  if (!sessionId) return
1100
+ // 🔴 事件时间取在任何慢操作之前:Codex 的 hook 是 async(不等跑完),一轮很短时 UserPromptSubmit 与 Stop
1101
+ // 的 hook 并发跑;Windows 上新会话首个 hook 要起 PowerShell 查进程,常比 Stop 晚写完,
1102
+ // 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
1103
+ const eventAt = Date.now()
1087
1104
 
1088
1105
  if (NOTICE_EVENTS[event]) {
1089
1106
  const notice = takeNotice(sessionId)
@@ -1140,6 +1157,7 @@ async function main(argv) {
1140
1157
  const prev = readState(sessionId)
1141
1158
  const now = Date.now()
1142
1159
  if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
1160
+ if (superseded(prev, eventAt)) return
1143
1161
  // 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
1144
1162
  // 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
1145
1163
  const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
@@ -1158,8 +1176,9 @@ async function main(argv) {
1158
1176
  // ⚠️ 只落本机,绝不上报:快照里发出去的仍然只有 basename。
1159
1177
  cwd: payload.cwd || (prev && prev.cwd) || '',
1160
1178
  state: action,
1161
- startedAt: (prev && prev.startedAt) || now,
1162
- updatedAt: now,
1179
+ startedAt: (prev && prev.startedAt) || eventAt,
1180
+ // 记事件发生时间而不是写完时间,后来的事件才比得过先发生、却后写完的那个
1181
+ updatedAt: eventAt,
1163
1182
  pid: pid,
1164
1183
  pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
1165
1184
  // 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
@@ -1181,7 +1200,11 @@ async function main(argv) {
1181
1200
 
1182
1201
  // 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
1183
1202
  const windowChanged = !!(prev && prev.remoteWindow) !== !!next.remoteWindow
1184
- if (!windowChanged && !shouldReport(prev, next, now)) {
1203
+ // 跃迁要跟磁盘上的最新状态比,不能只跟开头读到的 prev 比(评审):上一轮停在 done 时 Stop 读到的 prev 是旧 done,
1204
+ // 并发的 UserPromptSubmit 已把 watch 写盘并上报了,Stop 若按 done→done 静默落盘,设备收到的最后一份就是 watch
1205
+ const latest = readState(sessionId)
1206
+ if (superseded(latest, eventAt)) return
1207
+ if (!windowChanged && !shouldReport(latest || prev, next, now)) {
1185
1208
  writeState(next)
1186
1209
  return
1187
1210
  }
@@ -1197,6 +1220,8 @@ async function main(argv) {
1197
1220
  next.title ||
1198
1221
  ''
1199
1222
  next.lastReportAt = now
1223
+ // 取标题 / 查进程期间可能已有更晚发生的事件写完了:那份更新,这次既不落盘也不上报
1224
+ if (superseded(readState(sessionId), eventAt)) return
1200
1225
  writeState(next)
1201
1226
 
1202
1227
  await reportIfAuthorized(tool, now)
@@ -1248,6 +1273,7 @@ module.exports = {
1248
1273
  markClear,
1249
1274
  pairClear,
1250
1275
  shouldReport,
1276
+ superseded,
1251
1277
  sign,
1252
1278
  titleSources,
1253
1279
  writeState,
package/hook/hook.cjs CHANGED
@@ -1077,6 +1077,19 @@ function codexRemoteHas(sessionId) {
1077
1077
  return !!r && r.has(sessionId)
1078
1078
  }
1079
1079
 
1080
+ /** 并发 hook 的先后差不会超过一次 hook 的执行时长;超出这个差值说明是时钟回拨,不能当作「更晚的事件」 */
1081
+ const SUPERSEDE_MAX_MS = 2 * 60 * 1000
1082
+
1083
+ /**
1084
+ * 状态里已是比这次事件更晚发生的事件写的(hook 并发、先发生的后写完),这次不能再覆盖。
1085
+ * 🔴 差值带上限(评审):开机后校时往回拨时,状态里的 updatedAt 落在「未来」,不设上限的话在墙钟追上之前该会话的事件全被丢掉
1086
+ */
1087
+ function superseded(cur, eventAt) {
1088
+ if (!cur || typeof cur.updatedAt !== 'number') return false
1089
+ const ahead = cur.updatedAt - eventAt
1090
+ return ahead > 0 && ahead < SUPERSEDE_MAX_MS
1091
+ }
1092
+
1080
1093
  async function main(argv) {
1081
1094
  // 同一份 hook 被 Claude Code 与 Codex 复用,靠命令行参数区分是哪个工具
1082
1095
  const tool = (argv && argv[2]) || 'claude_code'
@@ -1084,6 +1097,10 @@ async function main(argv) {
1084
1097
  const event = payload.hook_event_name || ''
1085
1098
  const sessionId = payload.session_id || ''
1086
1099
  if (!sessionId) return
1100
+ // 🔴 事件时间取在任何慢操作之前:Codex 的 hook 是 async(不等跑完),一轮很短时 UserPromptSubmit 与 Stop
1101
+ // 的 hook 并发跑;Windows 上新会话首个 hook 要起 PowerShell 查进程,常比 Stop 晚写完,
1102
+ // 按写完先后落盘就会把 done 覆盖回 watch,屏上一直「进行中」直到窗口关掉(10-01 WIN-LVT5M8H5670 实测)
1103
+ const eventAt = Date.now()
1087
1104
 
1088
1105
  if (NOTICE_EVENTS[event]) {
1089
1106
  const notice = takeNotice(sessionId)
@@ -1140,6 +1157,7 @@ async function main(argv) {
1140
1157
  const prev = readState(sessionId)
1141
1158
  const now = Date.now()
1142
1159
  if (event === 'UserPromptSubmit') recordDir(payload.cwd, now)
1160
+ if (superseded(prev, eventAt)) return
1143
1161
  // 状态里记的进程还活着才沿用(评审):先远程 -p 跑过、后又在窗口 resume 同一会话时,
1144
1162
  // 不能拿死掉的 -p 进程的 interactive=false 挡不住窗口
1145
1163
  const prevAlive = !!(prev && prev.pid && pidExists(prev.pid))
@@ -1158,8 +1176,9 @@ async function main(argv) {
1158
1176
  // ⚠️ 只落本机,绝不上报:快照里发出去的仍然只有 basename。
1159
1177
  cwd: payload.cwd || (prev && prev.cwd) || '',
1160
1178
  state: action,
1161
- startedAt: (prev && prev.startedAt) || now,
1162
- updatedAt: now,
1179
+ startedAt: (prev && prev.startedAt) || eventAt,
1180
+ // 记事件发生时间而不是写完时间,后来的事件才比得过先发生、却后写完的那个
1181
+ updatedAt: eventAt,
1163
1182
  pid: pid,
1164
1183
  pidName: pid ? (prevAlive && prev.pidName) || processName(pid) || undefined : undefined,
1165
1184
  // 二期 P1:窗口开着的会话,设备不能再往里下指令(两个进程会互相吞掉对话)。每个会话只判一次
@@ -1181,7 +1200,11 @@ async function main(argv) {
1181
1200
 
1182
1201
  // 进 / 出 DeskBuddy 窗口不一定伴随状态跃迁(窗口里 --resume 一个已完成的会话),要立刻报,否则设备最长 2 分钟仍被 8348 挡
1183
1202
  const windowChanged = !!(prev && prev.remoteWindow) !== !!next.remoteWindow
1184
- if (!windowChanged && !shouldReport(prev, next, now)) {
1203
+ // 跃迁要跟磁盘上的最新状态比,不能只跟开头读到的 prev 比(评审):上一轮停在 done 时 Stop 读到的 prev 是旧 done,
1204
+ // 并发的 UserPromptSubmit 已把 watch 写盘并上报了,Stop 若按 done→done 静默落盘,设备收到的最后一份就是 watch
1205
+ const latest = readState(sessionId)
1206
+ if (superseded(latest, eventAt)) return
1207
+ if (!windowChanged && !shouldReport(latest || prev, next, now)) {
1185
1208
  writeState(next)
1186
1209
  return
1187
1210
  }
@@ -1197,6 +1220,8 @@ async function main(argv) {
1197
1220
  next.title ||
1198
1221
  ''
1199
1222
  next.lastReportAt = now
1223
+ // 取标题 / 查进程期间可能已有更晚发生的事件写完了:那份更新,这次既不落盘也不上报
1224
+ if (superseded(readState(sessionId), eventAt)) return
1200
1225
  writeState(next)
1201
1226
 
1202
1227
  await reportIfAuthorized(tool, now)
@@ -1248,6 +1273,7 @@ module.exports = {
1248
1273
  markClear,
1249
1274
  pairClear,
1250
1275
  shouldReport,
1276
+ superseded,
1251
1277
  sign,
1252
1278
  titleSources,
1253
1279
  writeState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskbuddy",
3
- "version": "0.2.0-beta.5",
3
+ "version": "0.2.0-beta.6",
4
4
  "description": "把 Claude Code、Codex 等 AI 编码智能体每个会话窗口的状态,实时同步到桌面硬件屏幕",
5
5
  "keywords": [
6
6
  "ai",