dsh-codex-style-computer-use 1.2.3 → 1.3.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/README.md +27 -14
- package/README.zh.md +28 -14
- package/icon.svg +29 -0
- package/lib/batch.js +21 -5
- package/lib/bin/attention-worker.exe +0 -0
- package/lib/bin/attention-worker.exe.json +1 -1
- package/lib/continuation.js +14 -0
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/compile-worker.ps1 +5 -1
- package/lib/core/index.js +170 -28
- package/lib/core/native-runtime.js +6 -1
- package/lib/core/winmd/Windows.winmd +0 -0
- package/lib/core/worker.cs +1116 -369
- package/lib/cycle-lifecycle.js +66 -0
- package/lib/cycle.js +99 -9
- package/lib/index.js +68 -618
- package/lib/policy.js +24 -3
- package/lib/skill-gate.js +1 -1
- package/lib/terminal-cleanup.js +57 -0
- package/lib/tools.js +58 -6
- package/mcp/src/execution.js +22 -0
- package/mcp/src/index.js +36 -13
- package/package.json +4 -2
- package/scripts/render-promo.py +81 -0
- package/skills/computer-use/SKILL.md +23 -3
- package/skills/computer-use/references/lifecycle.md +23 -12
- package/skills/computer-use/references/zero-uia-apps.md +54 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Finishing desktop work releases the machine without cancelling the conversation.
|
|
2
|
+
export function createCycleLifecycle ({ gate, cu, claimOf, owner, paused, inFlight, ownershipOf = () => null }) {
|
|
3
|
+
const refusal = reason => ({ released: false, reason, conversationContinues: true })
|
|
4
|
+
let pendingCompletion = null
|
|
5
|
+
|
|
6
|
+
async function release (exec, why = 'desktop work finished', automatic = false, expected) {
|
|
7
|
+
if (!gate.isLive()) return { released: false, alreadyReleased: true, conversationContinues: true }
|
|
8
|
+
const ownership = expected || ownershipOf(exec) || { claim: claimOf(exec), cycleId: gate.snapshot().id }
|
|
9
|
+
const { claim, cycleId } = ownership
|
|
10
|
+
gate.assertOwnership(claim, cycleId)
|
|
11
|
+
if (paused() || cu.exited || cu.cooperation?.active) return refusal('a manual pause, question, exit, or active human handoff still owns this cycle')
|
|
12
|
+
if (inFlight() > (automatic ? 0 : 1)) return refusal('computer-use calls are still in flight')
|
|
13
|
+
const token = gate.beginCompletion(claim, cycleId)
|
|
14
|
+
try {
|
|
15
|
+
const result = await cu.calm({ beforeDispatch: () => {
|
|
16
|
+
const current = gate.snapshot()
|
|
17
|
+
if (current.id !== token.cycleId || current.state !== 'releasing' || current.ownerIdent !== token.ident ||
|
|
18
|
+
current.exitVersion !== token.exitVersion) {
|
|
19
|
+
throw Object.assign(new Error('COMPUTER_OWNER_CHANGED: this cleanup no longer owns the releasing cycle; no cleanup was dispatched.'),
|
|
20
|
+
{ code: 'COMPUTER_OWNER_CHANGED', outcome: 'not-dispatched' })
|
|
21
|
+
}
|
|
22
|
+
} })
|
|
23
|
+
if (result?.released === false || paused() || cu.exited) {
|
|
24
|
+
gate.cancelCompletion(token)
|
|
25
|
+
return refusal(result?.reason || 'a newer pause or exit superseded completion')
|
|
26
|
+
}
|
|
27
|
+
const released = gate.finishCompletion(token)
|
|
28
|
+
return { released, ...(released ? {} : { reason: 'a newer cycle state superseded completion' }),
|
|
29
|
+
conversationContinues: true, cycleEnded: released,
|
|
30
|
+
message: released ? 'Computer-use control and monitoring released. Continue the conversation normally; a later required desktop call may acquire a fresh cycle.' : 'The current cycle state remains authoritative.' }
|
|
31
|
+
} catch (error) {
|
|
32
|
+
gate.cancelCompletion(token)
|
|
33
|
+
throw error
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function settle () {
|
|
38
|
+
if (!pendingCompletion || inFlight() !== 0) return null
|
|
39
|
+
const pending = pendingCompletion
|
|
40
|
+
try { gate.assertOwnership(pending.ownership.claim, pending.ownership.cycleId) }
|
|
41
|
+
catch { pendingCompletion = null; return null } // Old events cannot close a newer turn or cycle.
|
|
42
|
+
if (paused() || cu.exited) { pendingCompletion = null; return null }
|
|
43
|
+
if (cu.cooperation?.active) return null // Handback will drain this same captured completion.
|
|
44
|
+
pendingCompletion = null
|
|
45
|
+
return release(pending.exec, pending.why, true, pending.ownership)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function requestCompletion (exec, why) {
|
|
49
|
+
if (!exec?.agent || paused() || !gate.isLive() || gate.state() !== 'open') return null
|
|
50
|
+
pendingCompletion = { exec, why, ownership: { claim: claimOf(exec), cycleId: gate.snapshot().id } }
|
|
51
|
+
return settle()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function completeTurn (session, kind = 'completed') {
|
|
55
|
+
if (kind === 'aborted' || kind === 'interrupted' || !gate.ownsSession(claimOf({ session }).sessionKey)) return null
|
|
56
|
+
const exec = owner()
|
|
57
|
+
return requestCompletion(exec, `owner turn ${kind}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function idle (agent) {
|
|
61
|
+
if (!agent || !gate.ownsAgent(claimOf({ agent }).agentKey)) return null
|
|
62
|
+
return requestCompletion(owner(), 'owner finished desktop work')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { release, completeTurn, idle, settle }
|
|
66
|
+
}
|
package/lib/cycle.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
// cannot open a cycle at all. The attempt itself is ONE shared settled-once promise per
|
|
17
17
|
// qualification, so concurrent calls in a turn cannot slip an op into the worker's stdin ahead
|
|
18
18
|
// of the `resume` that opens the cycle (build/test-cycle-gate.mjs measures that ordering).
|
|
19
|
-
// * `ended` is terminal for that id. A new cycle needs a NEW qualification
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// * `ended` is terminal for that id. A new cycle needs a NEW qualification and gets a NEW id.
|
|
20
|
+
// A later turn qualifies naturally; explicit successful owner completion can also requalify
|
|
21
|
+
// that owner in the same turn. Ctrl+Alt+Q stays terminal for its turn. Neither a mouse event
|
|
22
|
+
// nor a late callback can revive an ended cycle.
|
|
22
23
|
//
|
|
23
24
|
// NO MEMORY BOUND LIVES IN THIS FILE, AND THAT IS DELIBERATE (#5, 2026-09-13). A cap that evicts a
|
|
24
25
|
// consumed qualification is not a memory policy, it is a RE-AUTHORIZATION channel: after 64
|
|
@@ -55,7 +56,7 @@ export function createCycleGate ({
|
|
|
55
56
|
} = {}) {
|
|
56
57
|
let nextId = 0
|
|
57
58
|
let exitVersion = 0 // bumped by every `ended`: a pending open compares it before/after its await
|
|
58
|
-
/** @type {{id:number, ownerIdent:string|null, ownerLabel:string, ownerSessionKey:string|null, ownerAgentKey:string|null, state:'open'|'paused'|'asking'|'ended', openedAt:number, lastAt:number, endedWhy:string}|null} */
|
|
59
|
+
/** @type {{id:number, ownerIdent:string|null, ownerLabel:string, ownerSessionKey:string|null, ownerAgentKey:string|null, state:'open'|'paused'|'asking'|'releasing'|'ended', openedAt:number, lastAt:number, endedWhy:string}|null} */
|
|
59
60
|
let cycle = null
|
|
60
61
|
/** qualification PER IDENTITY: ident -> {turn, promise, dead}. One entry per identity, never evicted. */
|
|
61
62
|
const idents = new Map()
|
|
@@ -64,18 +65,29 @@ export function createCycleGate ({
|
|
|
64
65
|
/** ident -> the turn generation in which an exit was seen: that turn may not open a cycle */
|
|
65
66
|
const endedTurns = new Map()
|
|
66
67
|
const events = [] // bounded transition log for the guards and diagnostics
|
|
68
|
+
const objectIds = new WeakMap()
|
|
69
|
+
let nextObjectId = 0
|
|
70
|
+
const identityKey = (value, preferred, kind) => {
|
|
71
|
+
const key = preferred(value)
|
|
72
|
+
if (key) return String(key)
|
|
73
|
+
if (value && (typeof value === 'object' || typeof value === 'function')) {
|
|
74
|
+
if (!objectIds.has(value)) objectIds.set(value, `${kind}-object-${++nextObjectId}`)
|
|
75
|
+
return objectIds.get(value)
|
|
76
|
+
}
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
67
79
|
|
|
68
80
|
const note = (what, detail) => {
|
|
69
81
|
events.push({ at: now(), what, ...(detail || {}) })
|
|
70
82
|
if (events.length > 96) events.shift()
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
const identOf = (sessionKeyVal, agentKeyVal) =>
|
|
85
|
+
const identOf = (sessionKeyVal, agentKeyVal) => JSON.stringify([sessionKeyVal, agentKeyVal])
|
|
74
86
|
|
|
75
87
|
/** A claim is what the CALL brings: whose call it is, and which of that session's turns it is in. */
|
|
76
88
|
function claim ({ session, agent } = {}) {
|
|
77
|
-
const sk =
|
|
78
|
-
const ak =
|
|
89
|
+
const sk = identityKey(session, sessionKey, 'session')
|
|
90
|
+
const ak = identityKey(agent, agentKey, 'agent')
|
|
79
91
|
const turn = (sk && turns.get(sk)) || 0
|
|
80
92
|
const ident = identOf(sk, ak)
|
|
81
93
|
return {
|
|
@@ -91,7 +103,7 @@ export function createCycleGate ({
|
|
|
91
103
|
|
|
92
104
|
/** Renewed by THAT session's turn/start only. Another session's event must not touch it (B). */
|
|
93
105
|
function noteTurnStart (session) {
|
|
94
|
-
const sk =
|
|
106
|
+
const sk = identityKey(session, sessionKey, 'session')
|
|
95
107
|
if (!sk) return 0
|
|
96
108
|
const n = (turns.get(sk) || 0) + 1
|
|
97
109
|
turns.set(sk, n)
|
|
@@ -131,6 +143,13 @@ export function createCycleGate ({
|
|
|
131
143
|
* not a warning — lib/tools.js rethrows it so the call fails loudly instead of driving anyway.
|
|
132
144
|
*/
|
|
133
145
|
function open (cl, openFn) {
|
|
146
|
+
if (!cl.sessionKey && !cl.agentKey) {
|
|
147
|
+
const err = new Error('COMPUTER_OWNER_REQUIRED: computer-use requires a stable session or agent identity; no operation was dispatched.')
|
|
148
|
+
err.dshCycleRefusal = true
|
|
149
|
+
err.code = 'COMPUTER_OWNER_REQUIRED'
|
|
150
|
+
err.outcome = 'not-dispatched'
|
|
151
|
+
throw err
|
|
152
|
+
}
|
|
134
153
|
// A live cycle with NO owner is a pause/ask that arrived before any call claimed the machine.
|
|
135
154
|
// Ownership is free: the next real call adopts it. Only a cycle owned by a DIFFERENT session is
|
|
136
155
|
// a refusal.
|
|
@@ -145,11 +164,16 @@ export function createCycleGate ({
|
|
|
145
164
|
const err = new Error(
|
|
146
165
|
`this machine is owned by another computer-use cycle (owner ${cycle.ownerLabel}, cycle ` +
|
|
147
166
|
`#${cycle.id}, state ${cycle.state}) — a session that is not the owner must not drive it. ` +
|
|
148
|
-
'
|
|
167
|
+
'Keep this task pending until the owner finishes or explicitly releases computer use, then retry.')
|
|
149
168
|
err.dshCycleRefusal = true
|
|
150
169
|
note('refused-foreign', { owner: cycle.ownerLabel, id: cycle.id, by: cl.label })
|
|
151
170
|
throw err
|
|
152
171
|
}
|
|
172
|
+
if (isLive(cycle) && (cycle.state === 'releasing' || cycle.terminating)) {
|
|
173
|
+
const err = new Error('COMPUTER_CYCLE_RELEASING: the owning agent is releasing computer use. Wait for release to finish before starting a fresh cycle.')
|
|
174
|
+
err.dshCycleRefusal = true; err.code = 'COMPUTER_CYCLE_RELEASING'; err.outcome = 'not-dispatched'
|
|
175
|
+
throw err
|
|
176
|
+
}
|
|
153
177
|
|
|
154
178
|
// ONE qualification per (session, agent, turn), kept per identity and NEVER evicted.
|
|
155
179
|
let e = idents.get(cl.ident)
|
|
@@ -278,6 +302,72 @@ export function createCycleGate ({
|
|
|
278
302
|
claim,
|
|
279
303
|
noteTurnStart,
|
|
280
304
|
open,
|
|
305
|
+
// Recheck the original call's claim after every async wait. Re-deriving it
|
|
306
|
+
// would silently give an old tool the identity of a newer turn.
|
|
307
|
+
assertOwnership (cl, expectedCycleId) {
|
|
308
|
+
const currentTurn = (cl.sessionKey && turns.get(cl.sessionKey)) || 0
|
|
309
|
+
if (!isLive(cycle) || cycle.state === 'releasing' || cycle.terminating || !ownedBy(cycle, cl) || cycle.id !== expectedCycleId ||
|
|
310
|
+
cl.turn !== currentTurn || idents.get(cl.ident)?.dead) {
|
|
311
|
+
const err = new Error('COMPUTER_OWNER_CHANGED: this call no longer owns the active computer-use cycle. No further input was dispatched; only the current owner may continue.')
|
|
312
|
+
err.dshCycleRefusal = true
|
|
313
|
+
err.code = 'COMPUTER_OWNER_CHANGED'
|
|
314
|
+
err.outcome = 'not-dispatched'
|
|
315
|
+
throw err
|
|
316
|
+
}
|
|
317
|
+
return true
|
|
318
|
+
},
|
|
319
|
+
// Complete the machine cycle without ending the chat. The intermediate state
|
|
320
|
+
// closes the async gap while the native worker releases its lease.
|
|
321
|
+
beginCompletion (cl, expectedCycleId) {
|
|
322
|
+
this.assertOwnership(cl, expectedCycleId)
|
|
323
|
+
if (cycle.state !== 'open') {
|
|
324
|
+
const err = new Error('COMPUTER_CYCLE_PAUSED: a human pause or active question keeps computer-use ownership; resume it before voluntarily releasing the cycle.')
|
|
325
|
+
err.dshCycleRefusal = true; err.code = 'COMPUTER_CYCLE_PAUSED'; err.outcome = 'not-dispatched'
|
|
326
|
+
throw err
|
|
327
|
+
}
|
|
328
|
+
const token = { cycleId: cycle.id, ident: cl.ident, turn: cl.turn, exitVersion }
|
|
329
|
+
cycle.completion = token
|
|
330
|
+
cycle.state = 'releasing'
|
|
331
|
+
note('completion-started', { id: cycle.id, owner: cl.label })
|
|
332
|
+
return token
|
|
333
|
+
},
|
|
334
|
+
// Host cancellation/disposal ends this turn's right to input immediately,
|
|
335
|
+
// but holds ownership until native cleanup finishes. A manual pause keeps its
|
|
336
|
+
// latch; after R the deferred terminal cleanup runs without waking that task.
|
|
337
|
+
beginTermination (cl, expectedCycleId, why, hold = false) {
|
|
338
|
+
if (!isLive(cycle) || !ownedBy(cycle, cl) || cycle.id !== expectedCycleId) return null
|
|
339
|
+
cycle.terminating = true
|
|
340
|
+
rememberEndedTurn(cl)
|
|
341
|
+
if (hold || cycle.state === 'paused' || cycle.state === 'asking') return { deferred: true }
|
|
342
|
+
if (cycle.state === 'releasing' && cycle.completion) {
|
|
343
|
+
cycle.completion.terminal = true; cycle.completion.why = why
|
|
344
|
+
return { joined: true, token: cycle.completion }
|
|
345
|
+
}
|
|
346
|
+
const token = { cycleId: cycle.id, ident: cl.ident, turn: cl.turn, exitVersion, terminal: true, why }
|
|
347
|
+
cycle.completion = token; cycle.state = 'releasing'
|
|
348
|
+
note('termination-started', { id: cycle.id, owner: cl.label, why })
|
|
349
|
+
return { token }
|
|
350
|
+
},
|
|
351
|
+
finishCompletion (token) {
|
|
352
|
+
if (!cycle || cycle.completion !== token || cycle.state !== 'releasing' ||
|
|
353
|
+
cycle.id !== token.cycleId || exitVersion !== token.exitVersion) return false
|
|
354
|
+
cycle.state = 'ended'
|
|
355
|
+
cycle.endedWhy = token.terminal ? token.why : 'owner completed computer use; chat remains active'
|
|
356
|
+
cycle.lastAt = now()
|
|
357
|
+
cycle.completion = null
|
|
358
|
+
exitVersion++
|
|
359
|
+
const e = idents.get(token.ident)
|
|
360
|
+
if (!token.terminal && e && !e.dead && e.turn === token.turn) e.promise = null
|
|
361
|
+
note('completed', { id: cycle.id, owner: cycle.ownerLabel })
|
|
362
|
+
return true
|
|
363
|
+
},
|
|
364
|
+
cancelCompletion (token) {
|
|
365
|
+
if (!cycle || cycle.completion !== token || cycle.state !== 'releasing' ||
|
|
366
|
+
cycle.id !== token.cycleId || exitVersion !== token.exitVersion) return false
|
|
367
|
+
cycle.state = 'open'; cycle.completion = null
|
|
368
|
+
note('completion-failed', { id: cycle.id })
|
|
369
|
+
return true
|
|
370
|
+
},
|
|
281
371
|
mark,
|
|
282
372
|
forgetIdentity,
|
|
283
373
|
snapshot: () => ({
|