dsh-codex-style-computer-use 2.2.6 → 2.2.7

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/README.md CHANGED
@@ -187,7 +187,7 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
187
187
  ## Safety model
188
188
 
189
189
  - **Shared input** — ordinary user activity is allowed while the agent is thinking, observing or preparing. During actual actuation, typing, scrolling and buttons yield immediately; pointer movement has a 16-pixel displacement tolerance. Continued input or a held key/button for 2 seconds enters automatic waiting. After release and 3 quiet seconds, the agent must inspect the current target and continue the original task, without blindly replaying partial writes. A gentle pale-yellow edge gradient shows human ownership.
190
- - **Release by desktop block, not by every call** — one cycle covers one continuous desktop block, not the whole task. When you leave that block for publish, upload, install, build, test, network, a file write, analysis or chat, call `computer_ctrl {action:"release"}` first. Keep the cycle only while the next call is still in the same block, including one quick readback you type straight back. An artifact in hand (token, clipboard text, value, path, screenshot) is a signal that the block may be ending, not an automatic release point. Do not hold "in case I need the desktop later"; reacquiring the lease is cheap, while a lit border and an exclusive lease during a long command are not. `hold` is only for position-sensitive readback inside the block, never across a build/publish/upload or long command; `free` then `release`.
190
+ - **Release by desktop block, not by every call** — one cycle covers one continuous desktop block, not the whole task. When you leave that block for publish, upload, install, build, test, network, a file write, analysis or chat, call `computer_ctrl {action:"release"}` first. Keep the cycle only while the next call is still in the same block, including one quick readback you type straight back. An artifact in hand (token, clipboard text, value, path, screenshot) is a signal that the block may be ending, not an automatic release point. Do not hold "in case I need the desktop later"; reacquiring the lease is cheap, while a lit border and an exclusive lease during a long command are not. `hold` is only for position-sensitive readback inside the block, never across a build/publish/upload or long command; `free` then `release`. A deterministic **100-second leave backstop** (`leaveDesktopGraceMs`; 0 disables) releases the lease if no `computer_*` call starts within the grace after a non-desktop tool; it is owner-scoped, cancelled by an actual computer start, never releases a newer cycle, and `hold` does not extend it.
191
191
  - **Manual pause** — only physical `Ctrl+Esc` raises a persistent input brake; `Ctrl+Alt+R` resumes. Ordinary Esc, typing, scrolling, pointer distance/frequency and the screen corner do not create this pause. Explicit question/diagnostic holds remain separate.
192
192
  - **Exit** — `Ctrl+Alt+Q` ends the session: the overlay goes away, its listeners are torn down, and
193
193
  further actuation is refused until a later turn opens a new one.
@@ -216,6 +216,7 @@ fields are detected through UI Automation's `IsPassword` and refused by default.
216
216
  | `actionFeedbackDetail` | `compact` | `compact`, `full` or `none`; a call's `feedback` overrides this default; uncached evidence falls back to full inline output |
217
217
  | `dryRun` | `false` | simulate actuations, log only |
218
218
  | `maxActionsPerMinute` | `60` | core-level actuation rate limit |
219
+ | `leaveDesktopGraceMs` | `100000` | grace after leaving the desktop for another tool; automatic lease expiry, `0` disables |
219
220
  | `annotateMarks` | `true` | draw Set-of-Marks boxes on screenshots |
220
221
  | `workerExe` | `""` | explicit worker path; empty = autodiscover or compile |
221
222
  | `snapshotDir` | `""` | screenshot directory; empty = `$DSH_HOME/data/computer-use/shots` |
package/README.zh.md CHANGED
@@ -178,7 +178,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
178
178
  ## 安全模型
179
179
 
180
180
  - **人机共用输入** —— 思考、观察和预告期间允许用户正常操作。实际操控时,键盘、滚轮和按钮立即触发让出;鼠标位移有 16 像素容差。持续输入或按住键/按钮 2 秒进入自动等待;全部松开并静默 3 秒后,agent 必须重新观察并接续原任务,不盲目重放部分写入。人操作时显示温和的淡黄色边缘渐变。
181
- - **按“桌面块”释放,不按每次调用** —— 一个 cycle 覆盖一段连续的桌面工作,不是整个任务。离开这一段去做发布、上传、安装、构建、测试、网络、写文件、分析或聊天时,先 `computer_ctrl {action:"release"}`;只有下一步仍是同一桌面块里的普通点击/输入,或一次很快的读数后马上输入回去,才保留 cycle。拿到的 token/值/路径/截图只是“这块可能快结束了”的信号,不是自动释放点;如果下一步是粘贴/回读/继续点,就保留。不要为了“等会儿可能还要用”而持有;重新获取租约很便宜,长命令期间亮着边框和独占锁并不便宜。`hold` 只用于块内对位置敏感的读数,不能跨 build/publish/upload 或长命令;`free` 后 release。暂停和待回答问题不会被自动清除。
181
+ - **按“桌面块”释放,不按每次调用** —— 一个 cycle 覆盖一段连续的桌面工作,不是整个任务。离开这一段去做发布、上传、安装、构建、测试、网络、写文件、分析或聊天时,先 `computer_ctrl {action:"release"}`;只有下一步仍是同一桌面块里的普通点击/输入,或一次很快的读数后马上输入回去,才保留 cycle。拿到的 token/值/路径/截图只是“这块可能快结束了”的信号,不是自动释放点;如果下一步是粘贴/回读/继续点,就保留。不要为了“等会儿可能还要用”而持有;重新获取租约很便宜,长命令期间亮着边框和独占锁并不便宜。`hold` 只用于块内对位置敏感的读数,不能跨 build/publish/upload 或长命令;`free` 后 release。插件另有 **100 秒离开计时兜底**(`leaveDesktopGraceMs`,0 可关):非桌面工具开始后若 100 秒内没有 `computer_*` 调用,插件会 owner-checked 地自动释放;它会在真实 computer 调用开始时取消、不会释放新 cycle,`hold` 也不延长它。暂停和待回答问题不会被自动清除。
182
182
  - **一个操作者** —— 会话/agent 所有权与跨 worker 独占锁覆盖整个控制周期,另一 agent 必须等当前操作者释放。
183
183
  - **人工暂停** —— 只有实体 `Ctrl+Esc` 触发持续输入刹车,`Ctrl+Alt+R` 恢复。普通 Esc、打字、滚动、鼠标距离/频率和屏幕左上角不再触发这种暂停;显式提问、诊断暂停保持独立。
184
184
  - **退出** —— `Ctrl+Alt+Q` 结束会话:覆盖层消失、监听全部拆除,后续注入被拒,直到之后某一轮重新开启。
@@ -197,6 +197,7 @@ ID**:派发输入前会核对窗口身份与采样像素,因此过期的 mar
197
197
  | `actionFeedbackDetail` | `compact` | `compact`、`full` 或 `none`;单次调用的 `feedback` 可覆盖默认值;无法缓存时完整内联返回证据 |
198
198
  | `dryRun` | `false` | 只模拟操作并记录 |
199
199
  | `maxActionsPerMinute` | `60` | 核心层操作限速 |
200
+ | `leaveDesktopGraceMs` | `100000` | 离开桌面去执行非桌面工具后的租约宽限;超时自动释放,`0` 关闭 |
200
201
  | `annotateMarks` | `true` | 在截图上绘制 Set-of-Marks 方框 |
201
202
  | `workerExe` | `""` | 显式指定 worker 路径;留空则自动发现或编译 |
202
203
  | `snapshotDir` | `""` | 截图目录;留空为 `$DSH_HOME/data/computer-use/shots` |
package/lib/cycle.js CHANGED
@@ -30,10 +30,12 @@
30
30
  // only on EVIDENCE that the identity is over (`forgetIdentity`, wired to agent/session disposal),
31
31
  // and a forgotten identity is marked DEAD so a late call from it is refused rather than trusted.
32
32
  //
33
- // NO TIME-BASED POLICY LIVES HERE EITHER. `openedAt`/`lastAt` are diagnostics only; a paused cycle is
34
- // never transferred to another session because time passed ("暂停只能由人的恢复/终结解除,时间不是授权").
33
+ // NO IDLE-TIME POLICY LIVES HERE. `openedAt`/`lastAt` are diagnostics only; a paused cycle is never
34
+ // transferred to another session because time passed ("暂停只能由人的恢复/终结解除,时间不是授权").
35
35
  // Ownership is released by FACTS: the owner's turn completing, its agent going idle, its identity
36
- // being disposed, or an exit.
36
+ // being disposed, or an exit. The separate leave timer (lib/leave-timer.js) is not an idle timeout:
37
+ // it is armed only when a non-computer tool starts while the cycle is open, cancelled by any actual
38
+ // computer_* start, retries refusals, and releases only the captured cycle id.
37
39
 
38
40
  /** Session identity without assuming the host's shape (id may be a string, or a field on an object). */
39
41
  export function sessionKeyOf (s) {
package/lib/index.js CHANGED
@@ -39,6 +39,7 @@ import { scopeOf } from '@deepseek-ai/dsh-scope'
39
39
  import { createCycleGate, agentKeyOf } from './cycle.js'
40
40
  import { recoveryProblem } from './recovery.js'
41
41
  import { createCycleLifecycle } from './cycle-lifecycle.js'
42
+ import { createLeaveTimer } from './leave-timer.js'
42
43
  import { createTerminalCleanup } from './terminal-cleanup.js'
43
44
  import fs from 'node:fs'
44
45
 
@@ -402,8 +403,11 @@ export const Config = Schema.object({
402
403
  workerExe: Schema.string().default('').description('explicit worker.exe path; empty = autodiscover/compile'),
403
404
  snapshotDir: Schema.string().default('').description('screenshot dir; empty = $DSH_HOME/data/computer-use/shots'),
404
405
  annotateMarks: Schema.boolean().default(true).description('draw SoM boxes on screenshots'),
405
- // Compatibility with saved settings only. Elapsed time never ends a computer-use cycle.
406
- calmAfterMs: Schema.number().default(0).description('legacy setting; ignored — computer-use cycles have no idle timeout'),
406
+ // Compatibility with saved settings only. Silence/elapsed time never ends a computer-use cycle.
407
+ calmAfterMs: Schema.number().default(0).description('legacy setting; ignored — silence never ends a cycle'),
408
+ // Narrow, explicit exception to the no-idle-timeout rule: armed by LEAVING the desktop for a
409
+ // non-computer tool, cancelled by any computer_* call; 0 disables it. See lib/leave-timer.js.
410
+ leaveDesktopGraceMs: Schema.number().default(100_000).description('release the desktop lease after this long away for another tool; 0 disables'),
407
411
  // Compatibility with saved settings; automatic completion has no question or timer.
408
412
  turnEndTimeoutAction: Schema.union([
409
413
  Schema.const('stop').description('legacy value; ignored'),
@@ -705,8 +709,16 @@ export function apply (ctx, config) {
705
709
  paused: pausedOrAsking, inFlight: () => inFlight,
706
710
  ownershipOf: exec => exec && typeof exec === 'object' ? callOwners.get(exec) : null,
707
711
  })
712
+ const leaveTimer = createLeaveTimer({
713
+ gate, claimOf, log,
714
+ notice: (...args) => notice(...args),
715
+ graceMs: config.leaveDesktopGraceMs,
716
+ setTimer: setTimeout, clearTimer: clearTimeout,
717
+ release: (exec, why, automatic, expected) => lifecycle.release(exec, why, automatic, expected),
718
+ })
708
719
  cu.on('handoff', () => {
709
720
  lifecycle.settle()?.catch(error => log.warn(`computer-use handback completion failed: ${error.message}`))
721
+ leaveTimer.retryNow()
710
722
  })
711
723
 
712
724
  const terminalCleanup = createTerminalCleanup({
@@ -727,9 +739,10 @@ export function apply (ctx, config) {
727
739
  }
728
740
  gate.assertOwnership(owner.claim, owner.cycleId)
729
741
  },
730
- releaseCycle: (exec, why) => lifecycle.release(exec, why),
742
+ releaseCycle: (exec, why) => { leaveTimer.clear(); return lifecycle.release(exec, why) },
731
743
  onStart: (exec) => {
732
744
  inFlight++
745
+ leaveTimer.cancel(exec)
733
746
  if (isCycleStateOnly(exec?.name, exec?.arguments)) {
734
747
  callOwners.set(exec, { claim: claimOf(exec), cycleId: gate.snapshot().id })
735
748
  }
@@ -737,6 +750,7 @@ export function apply (ctx, config) {
737
750
  onEnd: () => {
738
751
  inFlight = Math.max(0, inFlight - 1)
739
752
  lifecycle.settle()?.catch(error => log.warn(`computer-use deferred completion failed: ${error.message}`))
753
+ leaveTimer.retryNow()
740
754
  },
741
755
  // ---- THE WRAP-UP DIRECTIVE (2026-09-15) --------------------------------------------------------
742
756
  // WHAT WENT WRONG. A human brake (ESC, a mouse shake, the wheel, typing outside the host) engaged
@@ -1119,6 +1133,7 @@ export function apply (ctx, config) {
1119
1133
  const whoKey = agentKeyOf(payload && payload.agent)
1120
1134
  if (whoKey && gate.ownsAgent(whoKey)) {
1121
1135
  log.info(`owner agent disposed (${whoKey}) — releasing the machine for the next real call`)
1136
+ leaveTimer.onAgentDisposed(payload.agent)
1122
1137
  terminalCleanup.request('owner agent disposed')
1123
1138
  }
1124
1139
  } catch { /* never break the host */ }
@@ -1169,6 +1184,7 @@ export function apply (ctx, config) {
1169
1184
  log.info(`ignoring ${kind} turn/end from another session (${describeSession(session)})`)
1170
1185
  return
1171
1186
  }
1187
+ leaveTimer.onTurnEnd(session)
1172
1188
  if (kind === 'aborted' || kind === 'interrupted') {
1173
1189
  if (selfAbortUntil && Date.now() < selfAbortUntil) {
1174
1190
  selfAbortUntil = 0
@@ -1195,6 +1211,10 @@ export function apply (ctx, config) {
1195
1211
  // pure function; its {kind: allow|deny|ask} rides the tools/pre-execute
1196
1212
  // waterfall into the harness approval UI (fail-closed, one-shot grants).
1197
1213
  ctx.on('tools/pre-execute', (exec, next) => {
1214
+ // This hook is on the approval path. A timer bug must never deny a non-computer tool, so the
1215
+ // leave timer runs in its own fail-open guard; the policy try/catch below stays fail-closed for
1216
+ // computer_* calls only.
1217
+ try { leaveTimer.observe(exec) } catch (error) { log.warn(`computer-use leave timer observe failed: ${error.message}`) }
1198
1218
  try {
1199
1219
  if (!exec || typeof exec.name !== 'string' || !exec.name.startsWith('computer_')) return next()
1200
1220
  if (exec.name === ACTIVATION_TOOL) return next() // capability discovery does not open a control cycle
@@ -1275,6 +1295,7 @@ export function apply (ctx, config) {
1275
1295
  }
1276
1296
  })
1277
1297
 
1298
+ ctx.effect(() => () => leaveTimer.dispose())
1278
1299
  ctx.effect(() => {
1279
1300
  const disposer = () => { return cu.kill().catch(() => {}).finally(detachAudit) }
1280
1301
  return disposer
@@ -1285,5 +1306,5 @@ export function apply (ctx, config) {
1285
1306
  installToolExposure({ ctx, definitions, defineTool, scopeOf, log, enabled: config.progressiveTools !== false,
1286
1307
  skillPath: fileURLToPath(new URL('SKILL.md', SKILL_DIR_URL)),
1287
1308
  readSkill: () => fs.readFileSync(fileURLToPath(new URL('SKILL.md', SKILL_DIR_URL)), 'utf8') })
1288
- log.info(`ready (mode=${config.automationMode}${config.dryRun ? ' dry-run' : ''}; cycle idle timeout disabled)`)
1309
+ log.info(`ready (mode=${config.automationMode}${config.dryRun ? ' dry-run' : ''}; cycle idle timeout disabled${config.leaveDesktopGraceMs ? `; leave timer ${config.leaveDesktopGraceMs} ms` : ''})`)
1289
1310
  }
@@ -0,0 +1,201 @@
1
+ // lib/leave-timer.js — the deterministic backstop for a cycle that is left open.
2
+ //
3
+ // WHY THIS EXISTS (2026-11-08). The skill's cycle-boundary rule is a decision rule, not an
4
+ // enforcement mechanism: an agent can copy a desktop artifact and then run a long shell/network
5
+ // command without releasing. The desktop lease then stays open, the border stays lit and every
6
+ // other agent is blocked. The plugin already sees every top-level tool call, so when the agent
7
+ // stays away from the desktop for long enough we release the lease ourselves.
8
+ //
9
+ // WHAT THIS IS NOT. It is not an idle timeout: mere thinking never arms it, and a paused/asking
10
+ // cycle is never released by time. It is armed only when a tool that is not part of the desktop
11
+ // block starts, then cancelled by any later computer_* call. A quick read → compute → type flow
12
+ // therefore never releases, while a six-minute publish releases after the grace. Ownership is
13
+ // captured at arm time and passed to release as the expected cycle, so a late timer can never
14
+ // release a newer cycle. A refused release (pause/handoff/in-flight) is retried instead of being
15
+ // silently consumed.
16
+
17
+ /** Local bookkeeping / cheap inspection helpers that must not arm the leave timer. */
18
+ export const LEAVE_TOOL_EXEMPT = new Set([
19
+ 'todo_write',
20
+ 'read',
21
+ 'write',
22
+ 'edit',
23
+ 'glob',
24
+ 'grep',
25
+ 'run_code', // may wrap computer_* calls; the block-boundary skill covers that host shape
26
+ 'ask_user_question',
27
+ 'terminal_read',
28
+ 'terminal_list',
29
+ 'terminal_close',
30
+ ])
31
+
32
+ /** Desktop observation helpers: using one means the agent is still in the desktop block. */
33
+ const DESKTOP_HELPERS = new Set(['see_now', 'read_image'])
34
+
35
+ export function createLeaveTimer ({
36
+ gate,
37
+ claimOf,
38
+ release,
39
+ graceMs = 100_000,
40
+ retryMs = 10_000,
41
+ setTimer = setTimeout,
42
+ clearTimer = clearTimeout,
43
+ log = { info () {}, warn () {} },
44
+ notice = () => {},
45
+ } = {}) {
46
+ let timer = null
47
+ let retryTimer = null
48
+ let armed = null
49
+ let pending = null
50
+ let disposed = false
51
+
52
+ const unref = t => { try { t?.unref?.() } catch { /* host timer */ } }
53
+ const stop = t => { try { if (t) clearTimer(t) } catch { /* already gone */ } }
54
+ const isLive = () => !disposed && !!gate?.isLive?.()
55
+ const isOpen = () => isLive() && gate?.state?.() === 'open'
56
+ const owned = claim => !!claim && (
57
+ (!!claim.agentKey && !!gate?.ownsAgent?.(claim.agentKey)) ||
58
+ (!!claim.sessionKey && !!gate?.ownsSession?.(claim.sessionKey))
59
+ )
60
+ const dropTimer = () => { stop(timer); timer = null }
61
+ const dropRetry = () => { stop(retryTimer); retryTimer = null }
62
+
63
+ function clear () {
64
+ dropTimer()
65
+ dropRetry()
66
+ armed = null
67
+ pending = null
68
+ }
69
+
70
+ function cancel (exec) {
71
+ if (!armed && !pending && !timer && !retryTimer) return
72
+ if (exec) {
73
+ let claim
74
+ try { claim = claimOf(exec) } catch { return }
75
+ if (!owned(claim)) return // a foreign/denied computer call must not cancel the owner's timer
76
+ }
77
+ clear()
78
+ log.info?.('computer-use: leave timer cancelled by desktop activity')
79
+ }
80
+
81
+ function scheduleRetry () {
82
+ if (disposed || retryTimer || !pending || !isLive()) return
83
+ retryTimer = setTimer(() => {
84
+ retryTimer = null
85
+ attempt()
86
+ }, retryMs)
87
+ unref(retryTimer)
88
+ }
89
+
90
+ function attempt () {
91
+ if (disposed || !pending || !isLive()) return
92
+ const p = pending
93
+ let result
94
+ try {
95
+ result = release(p.exec, p.why, true, p.expected)
96
+ } catch (error) {
97
+ return failed(error)
98
+ }
99
+ if (result && typeof result.then === 'function') result.then(done, failed)
100
+ else done(result)
101
+ }
102
+
103
+ function done (result) {
104
+ if (disposed || !pending) return
105
+ if (result?.released) {
106
+ const finished = pending
107
+ pending = null
108
+ dropRetry()
109
+ log.info?.('computer-use: leave timer released the desktop lease')
110
+ try {
111
+ const out = notice(
112
+ 'computer-use: 桌面租约已自动释放(离开桌面超过 100 秒)。下一次操作会重新观察;旧 mark 已失效。',
113
+ 'computer-use: lease expired',
114
+ { agent: finished?.exec?.agent },
115
+ )
116
+ out?.catch?.(() => {})
117
+ } catch { /* a notice must never break the timer */ }
118
+ return
119
+ }
120
+ log.info?.(`computer-use: leave timer release refused (${result?.reason || 'no reason'})`)
121
+ scheduleRetry()
122
+ }
123
+
124
+ function failed (error) {
125
+ if (disposed) return
126
+ if (error?.code === 'COMPUTER_OWNER_CHANGED') {
127
+ // The captured cycle is gone or a newer one exists; this timer must never touch it.
128
+ pending = null
129
+ dropRetry()
130
+ log.info?.('computer-use: leave timer discarded (cycle/owner changed)')
131
+ return
132
+ }
133
+ log.warn?.(`computer-use: leave timer release failed (${error?.message || error})`)
134
+ scheduleRetry()
135
+ }
136
+
137
+ function fire () {
138
+ timer = null
139
+ if (disposed || !armed || !isLive()) { armed = null; return }
140
+ pending = armed
141
+ armed = null
142
+ attempt()
143
+ }
144
+
145
+ function arm (exec) {
146
+ if (disposed || armed || pending || timer || !graceMs || !isOpen()) return
147
+ let claim
148
+ try { claim = claimOf(exec) } catch { return }
149
+ if (!owned(claim)) return // global hook: only the owner's own leaving starts a timer
150
+ const cycle = gate?.snapshot?.()
151
+ if (!cycle || !Number.isInteger(cycle.id)) return
152
+ armed = {
153
+ exec,
154
+ expected: { claim, cycleId: cycle.id },
155
+ why: `left desktop for ${exec.name}`,
156
+ }
157
+ timer = setTimer(fire, graceMs)
158
+ unref(timer)
159
+ log.info?.(`computer-use: leave timer armed for ${graceMs} ms (${exec.name})`)
160
+ }
161
+
162
+ function observe (exec) {
163
+ if (disposed || !exec || typeof exec.name !== 'string') return
164
+ // A computer_* call — including one waiting in the approval card — means the agent is still on
165
+ // the desktop, so the leave grace must not fire underneath it. A denied call can cancel too;
166
+ // the next non-desktop top-level call arms a fresh grace. onStart() also cancels, idempotently,
167
+ // once the approved tool really begins.
168
+ if (exec.name.startsWith('computer_') || DESKTOP_HELPERS.has(exec.name)) {
169
+ cancel(exec)
170
+ return
171
+ }
172
+ if (LEAVE_TOOL_EXEMPT.has(exec.name)) return
173
+ arm(exec)
174
+ }
175
+
176
+ function retryNow () {
177
+ if (!pending || retryTimer || disposed || !isLive()) return
178
+ attempt()
179
+ }
180
+
181
+ function onTurnEnd (session) {
182
+ if (!isLive()) { clear(); return }
183
+ let claim
184
+ try { claim = claimOf({ session }) } catch { return }
185
+ if (owned(claim)) clear()
186
+ }
187
+
188
+ function onAgentDisposed (agent) {
189
+ if (!isLive()) { clear(); return }
190
+ let claim
191
+ try { claim = claimOf({ agent }) } catch { return }
192
+ if (owned(claim)) clear()
193
+ }
194
+
195
+ function dispose () {
196
+ disposed = true
197
+ clear()
198
+ }
199
+
200
+ return { observe, cancel, clear, retryNow, onTurnEnd, onAgentDisposed, dispose }
201
+ }
package/lib/tools.js CHANGED
@@ -798,7 +798,7 @@ export function registerTools ({ ctx, cu, config, dataDir, log, snapshotPath, de
798
798
  // ---------------- meta ----------------
799
799
  reg({
800
800
  name: 'computer_ctrl',
801
- description: 'hold requests stable page/pointer state between actions (breathing blue); free allows human use while observing (cyan). Neither blocks human takeover. After final verification, release ends desktop control while the conversation continues. stop raises a manual brake, released by human Ctrl+Alt+R; recover checks a temporary diagnostic pause; exit aborts. selftest/calibrate diagnose input; indicator controls the border; acknowledge accepts the skill receipt.',
801
+ description: 'hold requests stable page/pointer state between actions (breathing blue); free allows human use while observing (cyan). Neither blocks human takeover. After final verification, release ends desktop control while the conversation continues; leaving the desktop for another tool can also trigger an automatic lease expiry after the configured grace, so re-observe after it. stop raises a manual brake, released by human Ctrl+Alt+R; recover checks a temporary diagnostic pause; exit aborts. selftest/calibrate diagnose input; indicator controls the border; acknowledge accepts the skill receipt.',
802
802
  parameters: {
803
803
  action: P('string', 'hold | free | release | clear_cache | selftest | calibrate | stop | exit | indicator | acknowledge | recover', { required: true }),
804
804
  why: P('string', 'reason recorded with a stop'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-codex-style-computer-use",
3
- "version": "2.2.6",
3
+ "version": "2.2.7",
4
4
  "description": "Codex-style Computer Use for DeepSeek Harness: Windows desktop control, screenshots, UI Automation, human input handoff, approval gates and prebuilt native workers. Install and update from dsh-market without PowerShell 7 or build steps.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -37,7 +37,7 @@ const reviewed = new Set([
37
37
  'cycle-no-timeout', 'cycle-open', 'desktop-acceptance-io', 'desktop-ownership', 'drag-options', 'evidence-protocol',
38
38
  'exit-coherence', 'feedback-cache-integration', 'feedback-policy', 'glow-pump', 'glow-visual', 'handoff-continuation',
39
39
  'handoff-lock', 'handoff-runtime', 'handoff-watchdog', 'host-gate', 'human-handoff', 'indicator-hold', 'input-stamp', 'key-command',
40
- 'kickoff-focus', 'mcp-contracts', 'mcp-execution', 'modifier-vk', 'monitor-lifetime', 'native-hardening', 'native-respawn',
40
+ 'kickoff-focus', 'leave-timer', 'mcp-contracts', 'mcp-execution', 'modifier-vk', 'monitor-lifetime', 'native-hardening', 'native-respawn',
41
41
  'native-runtime', 'native-fault-lifecycle', 'observation-archive', 'observations', 'op-classification', 'pointer-lease', 'policy', 'preparation',
42
42
  'release-channel', 'renders', 'root-integration', 'session-identity', 'skill-gate', 'steer-message-shape',
43
43
  'task-rules', 'terminal-cleanup', 'timeout-branch', 'turnend-agent', 'turnend-rootagent',
@@ -28,6 +28,8 @@ An artifact in hand (token, clipboard text, a value, a path, a screenshot) means
28
28
 
29
29
  When in doubt, release: reacquiring the lease is cheap, while a lit border and an exclusive lease during long non-desktop work are not.
30
30
 
31
+ The plugin also has a deterministic backstop: after a non-desktop top-level tool starts, if no `computer_*` call starts within **100 seconds**, it releases the lease automatically (`leaveDesktopGraceMs`; 0 disables). `hold` does not extend this timer. A release is not an error: call `computer_state` again before driving; old marks and cached observations are stale. A `run_code`-only host is covered by this block rule, not by the timer; if you dispatch desktop work through `run_code`, release at the same boundary yourself.
32
+
31
33
  ## Start, act, verify
32
34
 
33
35
  1. Identify the app with `computer_state {windows:true}`, reusing a known window; activate its hwnd with `computer_window` and require `activated:true` before capturing or driving; bind the intended record/field/document too. Minimized: restore, then activate. An occlusion error means inspect what covers it.
@@ -21,6 +21,8 @@ The human's own words are the spec here; this section is what they mean.
21
21
 
22
22
  Call `computer_ctrl {action:"release"}` when the next step leaves this continuous desktop block for a different work item (publish, upload, install, build, test, network, a file write, analysis or chat), or when you cannot name the next desktop action. This closes the indicator/listeners and releases the exclusive desktop lease while the conversation continues. A quick readback you type straight back, a `run_code`/observation helper that itself continues the desktop action, or a single short helper needed for the next click is still inside the block; keep the cycle. An artifact in hand is a signal that the block may be ending, not an automatic release point. `hold` is only for position-sensitive readback inside the block, never across a long command; `free` then release. When in doubt, release; a later real tool call can open a new cycle in the same turn.
23
23
 
24
+ The plugin also enforces a deterministic backstop: after a non-desktop tool starts while the cycle is open, a leave timer releases the lease if no `computer_*` call starts within the configured grace (default 100 s; `leaveDesktopGraceMs: 0` disables it). It is owner-scoped, cancelled by an actual `computer_*` start, retries a release refused by a pause/question/handoff/in-flight call, and passes the cycle id captured when it was armed so it cannot release a newer cycle. `hold` does not extend this timer. After an automatic release, re-observe before driving; old marks and cached observations are stale.
25
+
24
26
  Release is owner-checked and distinct from `exit`: it does not cancel the conversation or write an EXITED latch. It cannot release an explicit human pause or a pending question. Closure is serialized with ownership acquisition so an old owner's delayed release cannot close a new owner's cycle. An explicit Ctrl+Alt+Q still prevents reopening in that same turn.
25
27
 
26
28
  Normal owner idle / turn completion releases automatically, without a question card, recommendation timer or a new agent turn. Only a genuine unresolved decision uses `computer_ask`. Keeping the chat alive is not a reason to keep the desktop cycle alive.