deskbuddy 0.2.0-beta.2 → 0.2.0-beta.4

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/hook/hook.cjs CHANGED
@@ -26,7 +26,7 @@ const MAX_SESSIONS = 20
26
26
  * 快照整包带的能力声明(前置 spec 2026-09-29 §4.4,服务端写 deskbuddy:caps:{clientId})。
27
27
  * 与 src/core/snapshot.ts 的 CAPS 一致
28
28
  */
29
- const CAPS = ['cmd_result', 'window_inject']
29
+ const CAPS = ['cmd_result', 'window_inject', 'resume_ended']
30
30
  /** 状态文件里保留的最近事件条数,与 src/core/state.ts 的 RECENT_MAX 一致 */
31
31
  const RECENT_MAX = 8
32
32
  const REPORT_TIMEOUT_MS = 2000
@@ -556,6 +556,40 @@ function writeState(s) {
556
556
  fs.renameSync(tmp, statePath(s.sessionId))
557
557
  }
558
558
 
559
+ // ---------- 已结束记录(与 src/core/ended.ts 同构,续跑方案 spec 2026-09-29 §5.1) ----------
560
+
561
+ const ENDED_TTL_MS = 7 * 24 * 60 * 60 * 1000
562
+
563
+ function writeEnded(s, now) {
564
+ const sid = String((s && s.sessionId) || '')
565
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(sid)) return
566
+ try {
567
+ const dir = path.join(deskbuddyHome(), 'ended')
568
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
569
+ const t = now || Date.now()
570
+ const rec = { sessionId: sid, tool: s.tool || '', cwd: s.cwd || '', interactive: s.interactive === true, endedAt: t }
571
+ const file = path.join(dir, sid + '.json')
572
+ fs.writeFileSync(file + '.tmp', JSON.stringify(rec), { encoding: 'utf8', mode: 0o600 })
573
+ fs.renameSync(file + '.tmp', file)
574
+ for (const n of fs.readdirSync(dir)) {
575
+ const f = path.join(dir, n)
576
+ try {
577
+ if (t - fs.statSync(f).mtimeMs > ENDED_TTL_MS) fs.rmSync(f, { force: true })
578
+ } catch (e) {
579
+ /* ignore */
580
+ }
581
+ }
582
+ } catch (e) {
583
+ /* 记不下:只是这个会话结束后不能从设备续跑 */
584
+ }
585
+ }
586
+
587
+ /** 会话结束:先写已结束记录,再删状态(删状态的地方都走这里,/clear 除外) */
588
+ function retireState(s, now) {
589
+ writeEnded(s, now)
590
+ removeState(s.sessionId)
591
+ }
592
+
559
593
  function removeState(sessionId) {
560
594
  try {
561
595
  fs.rmSync(statePath(sessionId), { force: true })
@@ -591,7 +625,7 @@ function readAliveStates(tool) {
591
625
  }
592
626
  if (!s || s.tool !== tool) continue
593
627
  if (s.pid && !isAlive(s.pid, s.pidName)) {
594
- removeState(s.sessionId) // 窗口已关:剔除并自我清理,state 目录不再无限增长
628
+ retireState(s) // 窗口已关:记下已结束、剔除并自我清理,state 目录不再无限增长
595
629
  continue
596
630
  }
597
631
  alive.push(s)
@@ -764,8 +798,35 @@ async function reportIfAuthorized(tool, nowMs) {
764
798
  // clientId 为空说明还没 setup 过(服务端靠 clientId ↔ userId 绑定关系认人):
765
799
  // 绝不能在这里现编一个,那等于凭空造出一台没人认领的机器
766
800
  if (!cfg.clientId || cfg.enabledTools.indexOf(tool) === -1) return
801
+ // Codex:以 daemon 连着的 app-server 清单判 remoteWindow(切片 B);Claude Code:状态文件 + 窗口唤醒标记
767
802
  const remote = tool === 'codex' ? readCodexRemote() || new Set() : null
768
- await postReport(cfg, buildSnapshot(readAliveStates(tool), tool, cfg, nowMs, remote))
803
+ await postReport(cfg, buildSnapshot(withWake(readAliveStates(tool)), tool, cfg, nowMs, remote))
804
+ }
805
+
806
+ // ---------- 窗口唤醒(asyncRewake,与 src/core/wake.ts 同构) ----------
807
+
808
+ /**
809
+ * daemon 写的 `wake/marks/<sessionId>.json`:这个会话开着的窗口里有等待者,设备指令能直接唤醒窗口执行。
810
+ * 标记里的智能体进程还在才算(kill 0,不查进程名:每次上报都要调)
811
+ */
812
+ function wakeMarked(sessionId) {
813
+ const safe = String(sessionId).replace(/[^A-Za-z0-9_-]/g, '')
814
+ if (!safe) return false
815
+ try {
816
+ const m = JSON.parse(fs.readFileSync(path.join(deskbuddyHome(), 'wake', 'marks', safe + '.json'), 'utf8'))
817
+ // 写标记的 daemon 死了就不算(daemon 退出来不及清标记时,不能一直误报 remoteWindow)
818
+ const daemonPid = Number(m && m.daemonPid) || 0
819
+ if (!daemonPid || !pidExists(daemonPid)) return false
820
+ const pid = Number(m && m.agentPid) || 0
821
+ return pid > 0 ? pidExists(pid) : true
822
+ } catch (e) {
823
+ return false
824
+ }
825
+ }
826
+
827
+ /** 上报前叠加唤醒标记:能被唤醒的会话带 remoteWindow=true(服务端据此放行 8348) */
828
+ function withWake(states) {
829
+ return states.map((s) => (s.remoteWindow === true || !wakeMarked(s.sessionId) ? s : Object.assign({}, s, { remoteWindow: true })))
769
830
  }
770
831
 
771
832
  /**
@@ -1063,8 +1124,13 @@ async function main(argv) {
1063
1124
  if (event === 'SessionEnd' && payload.reason === 'clear') {
1064
1125
  const old = readState(sessionId)
1065
1126
  if (old && old.pid) markClear(old.pid, sessionId)
1127
+ // /clear 引起的结束交给别名改绑,不写已结束记录
1128
+ removeState(sessionId)
1129
+ } else {
1130
+ // 先记下已结束(设备再回复时续跑要知道 cwd),再删状态。状态文件没有就用这次事件里的 cwd
1131
+ const old = readState(sessionId)
1132
+ retireState(old || { sessionId: sessionId, tool: tool, cwd: payload.cwd || '', interactive: false })
1066
1133
  }
1067
- removeState(sessionId)
1068
1134
  // 删完补发一次剩余会话,否则这条会一直挂在屏上等服务端失联 TTL。
1069
1135
  // 关掉最后一个窗口时发的是空列表——服务端放行空快照,该机器该工具随即从屏上消失
1070
1136
  await reportIfAuthorized(tool, Date.now())
@@ -1165,12 +1231,15 @@ module.exports = {
1165
1231
  main,
1166
1232
  mapEvent,
1167
1233
  processName,
1234
+ parentPid,
1168
1235
  readAiTitle,
1169
1236
  readAliveStates,
1170
1237
  readCodexThreadName,
1171
1238
  readGitBranch,
1172
1239
  readState,
1173
1240
  removeState,
1241
+ retireState,
1242
+ writeEnded,
1174
1243
  resolveAgentPid,
1175
1244
  resolveTitle,
1176
1245
  takeNotice,
@@ -1192,4 +1261,8 @@ module.exports = {
1192
1261
  noteWindow,
1193
1262
  readWindow,
1194
1263
  windowOwns,
1264
+ wakeMarked,
1265
+ withWake,
1266
+ detectInteractive,
1267
+ pidExists,
1195
1268
  }