martty 0.2.32 → 0.2.33
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/bin/martty.js +20 -7
- package/lib/acp-client-events.js +7 -1
- package/lib/acp-client.js +14 -2
- package/lib/acp-host.js +124 -1
- package/lib/acp-session-config.js +66 -31
- package/lib/acp-session-plan.js +39 -14
- package/lib/acp-session-stats.js +73 -32
- package/lib/acp-session-status.js +107 -54
- package/lib/cordis-protocol.js +1 -0
- package/lib/harnesses.js +16 -2
- package/lib/index.js +13 -3
- package/lib/mux.js +1 -0
- package/lib/runner.js +5 -1
- package/lib/tui-theme.js +7 -2
- package/package.json +1 -1
- package/vendor/darwin-arm64/martty +0 -0
- package/vendor/darwin-x64/martty +0 -0
- package/vendor/linux-arm64/martty +0 -0
- package/vendor/linux-x64/martty +0 -0
- package/vendor/win32-x64/martty.exe +0 -0
package/bin/martty.js
CHANGED
|
@@ -14,11 +14,19 @@ const vendorDir = path.join(__dirname, '..', 'vendor')
|
|
|
14
14
|
const binaryName = process.platform === 'win32' ? 'martty.exe' : 'martty'
|
|
15
15
|
const packaged = path.join(vendorDir, platformKey, binaryName)
|
|
16
16
|
const configuredBin = process.env.MARTTY_BIN || process.env.DSH_TUI_BIN
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
let binaryPath = packaged
|
|
18
|
+
if (typeof configuredBin === 'string' && configuredBin.length > 0) {
|
|
19
|
+
if (fs.existsSync(configuredBin)) {
|
|
20
|
+
binaryPath = configuredBin
|
|
21
|
+
} else {
|
|
22
|
+
// Never silently fall back: the user believes they are running their
|
|
23
|
+
// own build (devlocalinstall.sh has caught this confusion before).
|
|
24
|
+
console.error(
|
|
25
|
+
`martty: ${configuredBin} (from ${process.env.MARTTY_BIN ? 'MARTTY_BIN' : 'DSH_TUI_BIN'})`
|
|
26
|
+
+ ' does not exist — falling back to the bundled binary',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
const argv = process.argv.slice(2)
|
|
24
32
|
const wantDemoSkin = argv.includes('--demo-skin')
|
|
@@ -74,9 +82,14 @@ if (!fs.existsSync(binaryPath)) {
|
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
function exitFromSpawn(result) {
|
|
85
|
+
if (result.error) {
|
|
86
|
+
// A failed spawn (ENOENT, EACCES) sets status/signal to null; without
|
|
87
|
+
// a diagnostic the process would just exit 1 with no explanation.
|
|
88
|
+
console.error('martty: failed to launch the native binary: ' + result.error.message)
|
|
89
|
+
}
|
|
77
90
|
process.exit(
|
|
78
|
-
result.status
|
|
79
|
-
(result.signal === 'SIGINT' ? 130 : result.signal === 'SIGTERM' ? 143 : 1),
|
|
91
|
+
result.status
|
|
92
|
+
?? (result.signal === 'SIGINT' ? 130 : result.signal === 'SIGTERM' ? 143 : 1),
|
|
80
93
|
)
|
|
81
94
|
}
|
|
82
95
|
|
package/lib/acp-client-events.js
CHANGED
|
@@ -14,6 +14,7 @@ class AcpClientEventsService extends Service {
|
|
|
14
14
|
register(observer) { return this.core.register(this.ctx, observer) }
|
|
15
15
|
observeClient(message) { return this.core.observeClient(message) }
|
|
16
16
|
observeAgent(message) { return this.core.observeAgent(message) }
|
|
17
|
+
selectSession(sessionId) { return this.core.selectSession(sessionId) }
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function installAcpClientEvents(ctx) {
|
|
@@ -48,13 +49,18 @@ export function installAcpClientEvents(ctx) {
|
|
|
48
49
|
for (const observer of [...observers]) observer.observeAgent?.(message)
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
function selectSession(sessionId) {
|
|
53
|
+
for (const observer of [...observers]) observer.selectSession?.(sessionId)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const core = { register, observeClient, observeAgent, selectSession }
|
|
52
57
|
const service = typeof ctx.provide === 'function'
|
|
53
58
|
? new AcpClientEventsService(ctx, core)
|
|
54
59
|
: {
|
|
55
60
|
register(observer) { return register(ctx, observer) },
|
|
56
61
|
observeClient,
|
|
57
62
|
observeAgent,
|
|
63
|
+
selectSession,
|
|
58
64
|
}
|
|
59
65
|
if (typeof ctx.provide !== 'function') ctx.acpClientEvents = service
|
|
60
66
|
return service
|
package/lib/acp-client.js
CHANGED
|
@@ -23,6 +23,14 @@ export const inject = []
|
|
|
23
23
|
/** @type {null | { command: string, args: string[], child: import('node:child_process').ChildProcess, stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, kind: 'spawn' }} */
|
|
24
24
|
let liveAgent = null
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* One kill-on-exit hook per process, replaced (not accumulated) whenever a
|
|
28
|
+
* new agent is spawned. Registering `process.once('exit', …)` per apply()
|
|
29
|
+
* would stack listeners and kill every past child again on exit.
|
|
30
|
+
* @type {null | (() => void)}
|
|
31
|
+
*/
|
|
32
|
+
let liveAgentExitHook = null
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
35
|
* @typedef {{ command: string, args?: string[], env?: Record<string, string> }} AgentSpec
|
|
28
36
|
*/
|
|
@@ -115,13 +123,17 @@ export function apply(ctx, config = {}) {
|
|
|
115
123
|
}
|
|
116
124
|
liveAgent = service
|
|
117
125
|
provide(ctx, service)
|
|
118
|
-
|
|
126
|
+
if (liveAgentExitHook !== null) {
|
|
127
|
+
process.removeListener('exit', liveAgentExitHook)
|
|
128
|
+
}
|
|
129
|
+
liveAgentExitHook = () => {
|
|
119
130
|
try {
|
|
120
131
|
child.kill('SIGTERM')
|
|
121
132
|
} catch {
|
|
122
133
|
// already gone
|
|
123
134
|
}
|
|
124
|
-
}
|
|
135
|
+
}
|
|
136
|
+
process.once('exit', liveAgentExitHook)
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
function provide(ctx, service) {
|
package/lib/acp-host.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
|
|
6
6
|
export const name = 'dsh-tui-acp-host'
|
|
7
|
-
export const inject = ['loader']
|
|
7
|
+
export const inject = ['loader', 'userQuestions', 'permissionPresets']
|
|
8
8
|
|
|
9
9
|
function resolvedModule(ctx, specifier) {
|
|
10
10
|
for (const anchor of [ctx.baseUrl, import.meta.url]) {
|
|
@@ -18,7 +18,130 @@ function resolvedModule(ctx, specifier) {
|
|
|
18
18
|
return specifier
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function resolvedHostModule(ctx, specifier) {
|
|
22
|
+
if (typeof ctx.baseUrl !== 'string') return undefined
|
|
23
|
+
try {
|
|
24
|
+
return pathToFileURL(createRequire(ctx.baseUrl).resolve(specifier)).href
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* dsh 0.1.2 replaced the single user-question provider slot with a scoped
|
|
32
|
+
* waterfall. Keep the ACP dependency's older provider seam working while it
|
|
33
|
+
* remains protocol-compatible with both Host generations.
|
|
34
|
+
*/
|
|
35
|
+
export function installUserQuestionsCompatibility(ctx) {
|
|
36
|
+
const service = ctx.get?.('userQuestions')
|
|
37
|
+
if (service === undefined || typeof service.registerProvider === 'function') return false
|
|
38
|
+
const registerProvider = (provider) => {
|
|
39
|
+
if (provider === null || typeof provider !== 'object' || typeof provider.ask !== 'function') {
|
|
40
|
+
throw new TypeError('userQuestions.registerProvider: provider must expose ask()')
|
|
41
|
+
}
|
|
42
|
+
return ctx.on('user-questions/request', (request) => provider.ask(request))
|
|
43
|
+
}
|
|
44
|
+
Object.defineProperty(service, 'registerProvider', {
|
|
45
|
+
configurable: true,
|
|
46
|
+
value: registerProvider,
|
|
47
|
+
})
|
|
48
|
+
ctx.effect(() => () => {
|
|
49
|
+
if (service.registerProvider === registerProvider) delete service.registerProvider
|
|
50
|
+
}, 'dsh-tui.user-questions-compatibility')
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* dsh 0.1.2 replaced Session.events with snapshotEvents(). ACP 0.4.x only
|
|
56
|
+
* reads the old immutable snapshot, so restore that getter until ACP can
|
|
57
|
+
* require the new Host API directly.
|
|
58
|
+
*/
|
|
59
|
+
export function installSessionEventsCompatibility(Session) {
|
|
60
|
+
const prototype = Session?.prototype
|
|
61
|
+
if (prototype === undefined || 'events' in prototype) return false
|
|
62
|
+
if (typeof prototype.snapshotEvents !== 'function') return false
|
|
63
|
+
Object.defineProperty(prototype, 'events', {
|
|
64
|
+
configurable: true,
|
|
65
|
+
get() {
|
|
66
|
+
if (typeof this.snapshotEvents === 'function') return this.snapshotEvents()
|
|
67
|
+
// Cordis may hand an older plugin a remote Session reference whose
|
|
68
|
+
// declared surface predates snapshotEvents(). The 0.1.2 reference still
|
|
69
|
+
// exposes the scalar seq/eventAt pair, which yields the same snapshot.
|
|
70
|
+
if (typeof this.eventAt === 'function' && Number.isInteger(this.seq)) {
|
|
71
|
+
const events = []
|
|
72
|
+
for (let seq = 0; seq < this.seq; seq += 1) {
|
|
73
|
+
const event = this.eventAt(seq)
|
|
74
|
+
if (event !== undefined) events.push(event)
|
|
75
|
+
}
|
|
76
|
+
return Object.freeze(events)
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze([])
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Accept ACP 0.4.x's event-array read alongside dsh 0.1.2's Session read. */
|
|
85
|
+
export function installPermissionPresetsCompatibility(permissionPresets) {
|
|
86
|
+
if (permissionPresets === undefined) return false
|
|
87
|
+
const current = permissionPresets.current
|
|
88
|
+
if (typeof current !== 'function' || current.dshTuiAcceptsEventArrays === true) return false
|
|
89
|
+
const compatibleCurrent = function (target) {
|
|
90
|
+
if (!Array.isArray(target)) return current.call(this, target)
|
|
91
|
+
let state = { preset: null, sandbox: null, approval: null, seeded: false }
|
|
92
|
+
for (const event of target) {
|
|
93
|
+
switch (event?.type) {
|
|
94
|
+
case 'permission/preset':
|
|
95
|
+
state = { ...state, preset: event.data?.preset ?? null }
|
|
96
|
+
break
|
|
97
|
+
case 'sandbox/mode':
|
|
98
|
+
state = { ...state, sandbox: event.data?.mode ?? null }
|
|
99
|
+
break
|
|
100
|
+
case 'approval/policy':
|
|
101
|
+
state = { ...state, approval: event.data?.policy ?? null }
|
|
102
|
+
break
|
|
103
|
+
case 'session/end-seed':
|
|
104
|
+
state = { ...state, seeded: true }
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return this.derive(state)
|
|
109
|
+
}
|
|
110
|
+
Object.defineProperty(compatibleCurrent, 'dshTuiAcceptsEventArrays', { value: true })
|
|
111
|
+
Object.defineProperty(permissionPresets, 'current', {
|
|
112
|
+
configurable: true,
|
|
113
|
+
value: compatibleCurrent,
|
|
114
|
+
})
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function mountHostCompatibility(ctx) {
|
|
119
|
+
// The service can come from the active Host even when module resolution
|
|
120
|
+
// below lands on ACP's older peer copy, so adapt the live instance directly.
|
|
121
|
+
installPermissionPresetsCompatibility(ctx.permissionPresets ?? ctx.get?.('permissionPresets'))
|
|
122
|
+
|
|
123
|
+
const sessionModule = resolvedHostModule(ctx, '@deepseek-ai/dsh-session')
|
|
124
|
+
if (sessionModule !== undefined) {
|
|
125
|
+
const exports = await ctx.loader.import(sessionModule)
|
|
126
|
+
installSessionEventsCompatibility(exports.Session)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// New dsh presets opt the delegation tool into Host-owned model selection.
|
|
130
|
+
// Older hosts neither export nor require this service, so resolution is
|
|
131
|
+
// deliberately anchored only at the active Host profile.
|
|
132
|
+
if (ctx.get?.('subagentModelSelection') !== undefined) return
|
|
133
|
+
const resolved = resolvedHostModule(
|
|
134
|
+
ctx,
|
|
135
|
+
'@deepseek-ai/dsh-tool-subagent/model-selection-settings',
|
|
136
|
+
)
|
|
137
|
+
if (resolved === undefined) return
|
|
138
|
+
const exports = await ctx.loader.import(resolved)
|
|
139
|
+
await ctx.plugin(ctx.loader.unwrapExports(exports))
|
|
140
|
+
}
|
|
141
|
+
|
|
21
142
|
export async function apply(ctx, config) {
|
|
143
|
+
await mountHostCompatibility(ctx)
|
|
144
|
+
installUserQuestionsCompatibility(ctx)
|
|
22
145
|
const specifier = '@openma/deepseek-harness-acp/plugin'
|
|
23
146
|
const resolved = resolvedModule(ctx, specifier)
|
|
24
147
|
let exports
|
|
@@ -12,7 +12,7 @@ import { CORDIS_METHODS, CORDIS_PROTOCOL } from './cordis-protocol.js'
|
|
|
12
12
|
export const name = 'acp-session-config'
|
|
13
13
|
export const inject = []
|
|
14
14
|
|
|
15
|
-
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
15
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load', 'session/resume'])
|
|
16
16
|
|
|
17
17
|
class AcpSessionConfigService extends Service {
|
|
18
18
|
constructor(ctx, core) {
|
|
@@ -55,25 +55,35 @@ class AcpSessionConfigService extends Service {
|
|
|
55
55
|
observeAgent(message) {
|
|
56
56
|
return this.core.observeAgent(message)
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
selectSession(sessionId) {
|
|
60
|
+
return this.core.selectSession(sessionId)
|
|
61
|
+
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** Install the `ctx.acpSessionConfig` standard ACP state service. */
|
|
61
65
|
export function installAcpSessionConfig(ctx) {
|
|
62
66
|
let sessionId
|
|
63
|
-
let
|
|
67
|
+
let selectionKnown = false
|
|
68
|
+
const optionsBySession = new Map()
|
|
64
69
|
let requestTui
|
|
65
70
|
const pending = new Map()
|
|
66
71
|
const listeners = new Set()
|
|
67
72
|
|
|
73
|
+
function listFor(targetSessionId) {
|
|
74
|
+
return cloneJson(optionsBySession.get(targetSessionId) ?? [])
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
function list() {
|
|
69
|
-
return
|
|
78
|
+
return listFor(sessionId)
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
function current(id) {
|
|
73
82
|
if (typeof id !== 'string' || id.length === 0) {
|
|
74
83
|
throw new Error('acpSessionConfig.current: id must be a non-empty string')
|
|
75
84
|
}
|
|
76
|
-
const option =
|
|
85
|
+
const option = (optionsBySession.get(sessionId) ?? [])
|
|
86
|
+
.find((candidate) => candidate?.id === id)
|
|
77
87
|
return cloneJson(option?.currentValue ?? option?.current_value)
|
|
78
88
|
}
|
|
79
89
|
|
|
@@ -81,23 +91,36 @@ export function installAcpSessionConfig(ctx) {
|
|
|
81
91
|
if (typeof category !== 'string' || category.length === 0) {
|
|
82
92
|
throw new Error('acpSessionConfig.byCategory: category must be a non-empty string')
|
|
83
93
|
}
|
|
84
|
-
return cloneJson(
|
|
94
|
+
return cloneJson(
|
|
95
|
+
(optionsBySession.get(sessionId) ?? [])
|
|
96
|
+
.filter((candidate) => candidate?.category === category),
|
|
97
|
+
)
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
function snapshot() {
|
|
88
101
|
return { sessionId, options: list() }
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
function publish(
|
|
92
|
-
if (typeof nextSessionId === 'string' && nextSessionId.length > 0) {
|
|
93
|
-
sessionId = nextSessionId
|
|
94
|
-
}
|
|
95
|
-
if (!Array.isArray(nextOptions)) return
|
|
96
|
-
options = cloneJson(nextOptions)
|
|
104
|
+
function publish() {
|
|
97
105
|
const next = snapshot()
|
|
98
106
|
for (const listener of [...listeners]) listener(next)
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
function updateSession(targetSessionId, nextOptions) {
|
|
110
|
+
if (typeof targetSessionId !== 'string' || targetSessionId.length === 0
|
|
111
|
+
|| !Array.isArray(nextOptions)) return
|
|
112
|
+
optionsBySession.set(targetSessionId, cloneJson(nextOptions))
|
|
113
|
+
if (targetSessionId === sessionId) publish()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function selectSession(nextSessionId) {
|
|
117
|
+
selectionKnown = true
|
|
118
|
+
sessionId = typeof nextSessionId === 'string' && nextSessionId.length > 0
|
|
119
|
+
? nextSessionId
|
|
120
|
+
: undefined
|
|
121
|
+
publish()
|
|
122
|
+
}
|
|
123
|
+
|
|
101
124
|
function subscribe(effectCtx, listener) {
|
|
102
125
|
if (typeof listener !== 'function') {
|
|
103
126
|
throw new Error('acpSessionConfig.subscribe: listener must be a function')
|
|
@@ -120,7 +143,7 @@ export function installAcpSessionConfig(ctx) {
|
|
|
120
143
|
function observeClient(message) {
|
|
121
144
|
if (!isObject(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
122
145
|
if (SETUP_METHODS.has(message.method)) {
|
|
123
|
-
const requested = message.method
|
|
146
|
+
const requested = message.method !== 'session/new'
|
|
124
147
|
? readString(message.params, 'sessionId', 'session_id')
|
|
125
148
|
: undefined
|
|
126
149
|
pending.set(message.id, { kind: 'setup', sessionId: requested })
|
|
@@ -143,21 +166,21 @@ export function installAcpSessionConfig(ctx) {
|
|
|
143
166
|
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
144
167
|
const next = readOptions(message.result)
|
|
145
168
|
if (tracked.kind === 'setup') {
|
|
146
|
-
sessionId = bound
|
|
147
|
-
|
|
148
|
-
} else
|
|
149
|
-
|
|
169
|
+
if (!selectionKnown) sessionId = bound
|
|
170
|
+
updateSession(bound, next ?? [])
|
|
171
|
+
} else {
|
|
172
|
+
updateSession(bound, next)
|
|
150
173
|
}
|
|
151
174
|
return
|
|
152
175
|
}
|
|
153
176
|
if (message.method !== 'session/update' || !isObject(message.params)) return
|
|
154
177
|
const updatedSession = readString(message.params, 'sessionId', 'session_id')
|
|
155
|
-
if (sessionId !== undefined && updatedSession !== sessionId) return
|
|
156
178
|
const update = message.params.update
|
|
157
179
|
if (!isObject(update)) return
|
|
158
180
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
159
181
|
if (type !== 'config_option_update') return
|
|
160
|
-
|
|
182
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updatedSession
|
|
183
|
+
updateSession(updatedSession, readOptions(update))
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
function bindTransport(nextRequestTui) {
|
|
@@ -170,37 +193,47 @@ export function installAcpSessionConfig(ctx) {
|
|
|
170
193
|
}
|
|
171
194
|
}
|
|
172
195
|
|
|
173
|
-
async function
|
|
196
|
+
async function setForSession(targetSessionId, id, value) {
|
|
174
197
|
if (typeof id !== 'string' || id.length === 0) {
|
|
175
198
|
throw new Error('acpSessionConfig.set: id must be a non-empty string')
|
|
176
199
|
}
|
|
177
|
-
if (
|
|
200
|
+
if (targetSessionId === undefined) {
|
|
178
201
|
throw new Error('acpSessionConfig.set: no ACP Session is active')
|
|
179
202
|
}
|
|
180
203
|
if (typeof requestTui !== 'function') {
|
|
181
204
|
throw new Error('acpSessionConfig.set: native ACP transport is unavailable')
|
|
182
205
|
}
|
|
183
|
-
const option =
|
|
206
|
+
const option = (optionsBySession.get(targetSessionId) ?? [])
|
|
207
|
+
.find((candidate) => candidate?.id === id)
|
|
184
208
|
if (option === undefined) {
|
|
185
209
|
throw new Error(`acpSessionConfig.set: option "${id}" is not advertised by the current Session`)
|
|
186
210
|
}
|
|
187
211
|
validateValue(option, value)
|
|
188
212
|
const result = await requestTui(CORDIS_METHODS.sessionConfigSet, {
|
|
189
213
|
protocol: CORDIS_PROTOCOL,
|
|
190
|
-
sessionId,
|
|
214
|
+
sessionId: targetSessionId,
|
|
191
215
|
configId: id,
|
|
192
216
|
value,
|
|
193
217
|
})
|
|
194
218
|
if (!isObject(result)) {
|
|
195
219
|
throw new Error('acpSessionConfig.set: native ACP client returned an invalid response')
|
|
196
220
|
}
|
|
197
|
-
const resultSession = readString(result, 'sessionId', 'session_id') ??
|
|
198
|
-
|
|
199
|
-
return
|
|
221
|
+
const resultSession = readString(result, 'sessionId', 'session_id') ?? targetSessionId
|
|
222
|
+
updateSession(resultSession, readOptions(result))
|
|
223
|
+
return listFor(targetSessionId)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function set(id, value) {
|
|
227
|
+
return setForSession(sessionId, id, value)
|
|
200
228
|
}
|
|
201
229
|
|
|
202
230
|
function transaction(selector) {
|
|
203
|
-
const
|
|
231
|
+
const transactionSessionId = sessionId
|
|
232
|
+
if (transactionSessionId === undefined) {
|
|
233
|
+
throw new Error('acpSessionConfig.transaction: no ACP Session is active')
|
|
234
|
+
}
|
|
235
|
+
const transactionOptions = optionsBySession.get(transactionSessionId) ?? []
|
|
236
|
+
const option = resolveTransactionOption(selector, transactionOptions)
|
|
204
237
|
const original = cloneJson(option.currentValue ?? option.current_value)
|
|
205
238
|
if (original === undefined) {
|
|
206
239
|
throw new Error(
|
|
@@ -210,18 +243,18 @@ export function installAcpSessionConfig(ctx) {
|
|
|
210
243
|
|
|
211
244
|
let desired = cloneJson(original)
|
|
212
245
|
let applied = cloneJson(original)
|
|
213
|
-
let chain = Promise.resolve(
|
|
246
|
+
let chain = Promise.resolve(listFor(transactionSessionId))
|
|
214
247
|
let finalizing
|
|
215
248
|
let settled = false
|
|
216
249
|
|
|
217
250
|
const write = async (value) => {
|
|
218
|
-
const result = await
|
|
251
|
+
const result = await setForSession(transactionSessionId, option.id, value)
|
|
219
252
|
applied = cloneJson(value)
|
|
220
253
|
return result
|
|
221
254
|
}
|
|
222
255
|
|
|
223
256
|
const preview = (value) => {
|
|
224
|
-
if (settled) return Promise.resolve(
|
|
257
|
+
if (settled) return Promise.resolve(listFor(transactionSessionId))
|
|
225
258
|
if (finalizing) return finalizing
|
|
226
259
|
if (Object.is(value, desired)) return chain
|
|
227
260
|
desired = cloneJson(value)
|
|
@@ -232,13 +265,13 @@ export function installAcpSessionConfig(ctx) {
|
|
|
232
265
|
}
|
|
233
266
|
|
|
234
267
|
const finish = (value) => {
|
|
235
|
-
if (settled) return finalizing ?? Promise.resolve(
|
|
268
|
+
if (settled) return finalizing ?? Promise.resolve(listFor(transactionSessionId))
|
|
236
269
|
if (finalizing) return finalizing
|
|
237
270
|
desired = cloneJson(value)
|
|
238
271
|
const settle = async () => {
|
|
239
272
|
if (!Object.is(applied, desired)) await write(desired)
|
|
240
273
|
settled = true
|
|
241
|
-
return
|
|
274
|
+
return listFor(transactionSessionId)
|
|
242
275
|
}
|
|
243
276
|
finalizing = chain.then(settle, settle).catch((error) => {
|
|
244
277
|
finalizing = undefined
|
|
@@ -273,6 +306,7 @@ export function installAcpSessionConfig(ctx) {
|
|
|
273
306
|
bindTransport,
|
|
274
307
|
observeClient,
|
|
275
308
|
observeAgent,
|
|
309
|
+
selectSession,
|
|
276
310
|
}
|
|
277
311
|
const service = typeof ctx.provide === 'function'
|
|
278
312
|
? new AcpSessionConfigService(ctx, core)
|
|
@@ -288,6 +322,7 @@ export function installAcpSessionConfig(ctx) {
|
|
|
288
322
|
bindTransport,
|
|
289
323
|
observeClient,
|
|
290
324
|
observeAgent,
|
|
325
|
+
selectSession,
|
|
291
326
|
}
|
|
292
327
|
if (typeof ctx.provide !== 'function') ctx.acpSessionConfig = service
|
|
293
328
|
return service
|
package/lib/acp-session-plan.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Service } from '@deepseek-ai/cordis'
|
|
|
5
5
|
export const name = 'acp-session-plan'
|
|
6
6
|
export const inject = []
|
|
7
7
|
|
|
8
|
-
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
8
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load', 'session/resume'])
|
|
9
9
|
|
|
10
10
|
class AcpSessionPlanService extends Service {
|
|
11
11
|
constructor(ctx, core) {
|
|
@@ -32,21 +32,30 @@ class AcpSessionPlanService extends Service {
|
|
|
32
32
|
observeAgent(message) {
|
|
33
33
|
return this.core.observeAgent(message)
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
selectSession(sessionId) {
|
|
37
|
+
return this.core.selectSession(sessionId)
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** Install `ctx.acpSessionPlan`, backed only by standard ACP traffic. */
|
|
38
42
|
export function installAcpSessionPlan(ctx) {
|
|
39
43
|
let sessionId
|
|
40
|
-
|
|
44
|
+
let selectionKnown = false
|
|
45
|
+
const plansBySession = new Map()
|
|
41
46
|
const pending = new Map()
|
|
42
47
|
const listeners = new Set()
|
|
43
48
|
|
|
49
|
+
function activePlans() {
|
|
50
|
+
return plansBySession.get(sessionId) ?? new Map()
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
function list() {
|
|
45
|
-
return cloneJson([...
|
|
54
|
+
return cloneJson([...activePlans().values()])
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
function current() {
|
|
49
|
-
return cloneJson([...
|
|
58
|
+
return cloneJson([...activePlans().values()].at(-1) ?? null)
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
function snapshot() {
|
|
@@ -58,9 +67,17 @@ export function installAcpSessionPlan(ctx) {
|
|
|
58
67
|
for (const listener of [...listeners]) listener(next)
|
|
59
68
|
}
|
|
60
69
|
|
|
61
|
-
function reset(
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
function reset(targetSessionId) {
|
|
71
|
+
if (typeof targetSessionId !== 'string' || targetSessionId.length === 0) return
|
|
72
|
+
plansBySession.set(targetSessionId, new Map())
|
|
73
|
+
if (targetSessionId === sessionId) publish()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectSession(nextSessionId) {
|
|
77
|
+
selectionKnown = true
|
|
78
|
+
sessionId = typeof nextSessionId === 'string' && nextSessionId.length > 0
|
|
79
|
+
? nextSessionId
|
|
80
|
+
: undefined
|
|
64
81
|
publish()
|
|
65
82
|
}
|
|
66
83
|
|
|
@@ -86,7 +103,7 @@ export function installAcpSessionPlan(ctx) {
|
|
|
86
103
|
function observeClient(message) {
|
|
87
104
|
if (!isObject(message) || message.id === undefined || !SETUP_METHODS.has(message.method)) return
|
|
88
105
|
pending.set(message.id, {
|
|
89
|
-
sessionId: message.method
|
|
106
|
+
sessionId: message.method !== 'session/new'
|
|
90
107
|
? readString(message.params, 'sessionId', 'session_id')
|
|
91
108
|
: undefined,
|
|
92
109
|
})
|
|
@@ -98,13 +115,20 @@ export function installAcpSessionPlan(ctx) {
|
|
|
98
115
|
const tracked = pending.get(message.id)
|
|
99
116
|
pending.delete(message.id)
|
|
100
117
|
if (message.error !== undefined || !isObject(message.result)) return
|
|
101
|
-
|
|
118
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
119
|
+
if (!selectionKnown) sessionId = bound
|
|
120
|
+
reset(bound)
|
|
102
121
|
return
|
|
103
122
|
}
|
|
104
123
|
if (message.method !== 'session/update' || !isObject(message.params)) return
|
|
105
124
|
const updatedSession = readString(message.params, 'sessionId', 'session_id')
|
|
106
|
-
if (
|
|
107
|
-
if (
|
|
125
|
+
if (updatedSession === undefined) return
|
|
126
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updatedSession
|
|
127
|
+
let plans = plansBySession.get(updatedSession)
|
|
128
|
+
if (plans === undefined) {
|
|
129
|
+
plans = new Map()
|
|
130
|
+
plansBySession.set(updatedSession, plans)
|
|
131
|
+
}
|
|
108
132
|
const update = message.params.update
|
|
109
133
|
if (!isObject(update)) return
|
|
110
134
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
@@ -112,7 +136,7 @@ export function installAcpSessionPlan(ctx) {
|
|
|
112
136
|
const id = readString(update, 'planId', 'plan_id')
|
|
113
137
|
if (id === undefined) plans.clear()
|
|
114
138
|
else plans.delete(id)
|
|
115
|
-
publish()
|
|
139
|
+
if (updatedSession === sessionId) publish()
|
|
116
140
|
return
|
|
117
141
|
}
|
|
118
142
|
if (type !== 'plan' && type !== 'plan_update') return
|
|
@@ -124,10 +148,10 @@ export function installAcpSessionPlan(ctx) {
|
|
|
124
148
|
plans.delete(plan.id)
|
|
125
149
|
plans.set(plan.id, plan)
|
|
126
150
|
}
|
|
127
|
-
publish()
|
|
151
|
+
if (updatedSession === sessionId) publish()
|
|
128
152
|
}
|
|
129
153
|
|
|
130
|
-
const core = { list, current, subscribe, observeClient, observeAgent }
|
|
154
|
+
const core = { list, current, subscribe, observeClient, observeAgent, selectSession }
|
|
131
155
|
const service = typeof ctx.provide === 'function'
|
|
132
156
|
? new AcpSessionPlanService(ctx, core)
|
|
133
157
|
: {
|
|
@@ -138,6 +162,7 @@ export function installAcpSessionPlan(ctx) {
|
|
|
138
162
|
},
|
|
139
163
|
observeClient,
|
|
140
164
|
observeAgent,
|
|
165
|
+
selectSession,
|
|
141
166
|
}
|
|
142
167
|
if (typeof ctx.provide !== 'function') ctx.acpSessionPlan = service
|
|
143
168
|
return service
|
package/lib/acp-session-stats.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Service } from '@deepseek-ai/cordis'
|
|
|
5
5
|
export const name = 'acp-session-stats'
|
|
6
6
|
export const inject = []
|
|
7
7
|
|
|
8
|
-
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
8
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load', 'session/resume'])
|
|
9
9
|
|
|
10
10
|
class AcpSessionStatsService extends Service {
|
|
11
11
|
constructor(ctx, core) {
|
|
@@ -17,6 +17,7 @@ class AcpSessionStatsService extends Service {
|
|
|
17
17
|
subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
|
|
18
18
|
observeClient(message) { return this.core.observeClient(message) }
|
|
19
19
|
observeAgent(message) { return this.core.observeAgent(message) }
|
|
20
|
+
selectSession(sessionId) { return this.core.selectSession(sessionId) }
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
function zero(sessionId) {
|
|
@@ -44,20 +45,51 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
44
45
|
const pendingPrompts = new Map()
|
|
45
46
|
const activePrompts = new Map()
|
|
46
47
|
const toolStarts = new Map()
|
|
47
|
-
|
|
48
|
+
const values = new Map()
|
|
49
|
+
let sessionId
|
|
50
|
+
let selectionKnown = false
|
|
48
51
|
|
|
49
|
-
function
|
|
52
|
+
function stateFor(targetSessionId) {
|
|
53
|
+
if (typeof targetSessionId !== 'string' || targetSessionId.length === 0) {
|
|
54
|
+
return zero(undefined)
|
|
55
|
+
}
|
|
56
|
+
let value = values.get(targetSessionId)
|
|
57
|
+
if (value === undefined) {
|
|
58
|
+
value = zero(targetSessionId)
|
|
59
|
+
values.set(targetSessionId, value)
|
|
60
|
+
}
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function current() { return structuredClone(stateFor(sessionId)) }
|
|
50
65
|
|
|
51
66
|
function publish() {
|
|
52
67
|
const snapshot = current()
|
|
53
68
|
for (const listener of [...listeners]) listener(snapshot)
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
function publishIf(targetSessionId) {
|
|
72
|
+
if (targetSessionId === sessionId) publish()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function reset(targetSessionId) {
|
|
76
|
+
if (typeof targetSessionId !== 'string' || targetSessionId.length === 0) return
|
|
77
|
+
values.set(targetSessionId, zero(targetSessionId))
|
|
78
|
+
activePrompts.delete(targetSessionId)
|
|
79
|
+
for (const [id, prompt] of pendingPrompts) {
|
|
80
|
+
if (prompt.sessionId === targetSessionId) pendingPrompts.delete(id)
|
|
81
|
+
}
|
|
82
|
+
for (const key of toolStarts.keys()) {
|
|
83
|
+
if (key.startsWith(`${targetSessionId}\u0000`)) toolStarts.delete(key)
|
|
84
|
+
}
|
|
85
|
+
publishIf(targetSessionId)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function selectSession(nextSessionId) {
|
|
89
|
+
selectionKnown = true
|
|
90
|
+
sessionId = typeof nextSessionId === 'string' && nextSessionId.length > 0
|
|
91
|
+
? nextSessionId
|
|
92
|
+
: undefined
|
|
61
93
|
publish()
|
|
62
94
|
}
|
|
63
95
|
|
|
@@ -84,22 +116,26 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
84
116
|
if (!object(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
85
117
|
if (SETUP_METHODS.has(message.method)) {
|
|
86
118
|
pendingSetup.set(message.id, {
|
|
87
|
-
sessionId: message.method
|
|
119
|
+
sessionId: message.method !== 'session/new'
|
|
88
120
|
? readString(message.params, 'sessionId', 'session_id')
|
|
89
121
|
: undefined,
|
|
90
122
|
})
|
|
91
123
|
return
|
|
92
124
|
}
|
|
93
125
|
if (message.method !== 'session/prompt') return
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
const promptSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
127
|
+
if (promptSessionId === undefined) return
|
|
128
|
+
if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
|
|
129
|
+
const prompt = {
|
|
130
|
+
sessionId: promptSessionId,
|
|
131
|
+
started: now(),
|
|
132
|
+
firstToken: undefined,
|
|
133
|
+
toolMillis: 0,
|
|
134
|
+
}
|
|
99
135
|
pendingPrompts.set(message.id, prompt)
|
|
100
|
-
activePrompts.set(
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
activePrompts.set(promptSessionId, prompt)
|
|
137
|
+
stateFor(promptSessionId).stats.turns += 1
|
|
138
|
+
publishIf(promptSessionId)
|
|
103
139
|
}
|
|
104
140
|
|
|
105
141
|
function observeAgent(message) {
|
|
@@ -108,32 +144,36 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
108
144
|
const tracked = pendingSetup.get(message.id)
|
|
109
145
|
pendingSetup.delete(message.id)
|
|
110
146
|
if (message.error !== undefined || !object(message.result)) return
|
|
111
|
-
|
|
147
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
148
|
+
if (!selectionKnown) sessionId = bound
|
|
149
|
+
reset(bound)
|
|
112
150
|
return
|
|
113
151
|
}
|
|
114
152
|
if (message.id !== undefined && pendingPrompts.has(message.id)) {
|
|
115
153
|
const prompt = pendingPrompts.get(message.id)
|
|
116
154
|
pendingPrompts.delete(message.id)
|
|
117
155
|
activePrompts.delete(prompt.sessionId)
|
|
156
|
+
const value = stateFor(prompt.sessionId)
|
|
118
157
|
if (message.error === undefined && object(message.result)) {
|
|
119
|
-
addUsage(message.result.usage)
|
|
158
|
+
addUsage(value, message.result.usage)
|
|
120
159
|
}
|
|
121
160
|
const elapsed = Math.max(0, now() - prompt.started)
|
|
122
161
|
value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
123
|
-
|
|
162
|
+
publishIf(prompt.sessionId)
|
|
124
163
|
return
|
|
125
164
|
}
|
|
126
165
|
if (message.method !== 'session/update' || !object(message.params)) return
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
if (
|
|
166
|
+
const updateSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
167
|
+
if (updateSessionId === undefined) return
|
|
168
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updateSessionId
|
|
169
|
+
const value = stateFor(updateSessionId)
|
|
130
170
|
const update = message.params.update
|
|
131
171
|
if (!object(update)) return
|
|
132
172
|
|
|
133
173
|
const replay = update?._meta?.dsh
|
|
134
174
|
if (object(replay) && replay.event === 'prompt/usage') {
|
|
135
175
|
value.usage = usageOf(replay.usage)
|
|
136
|
-
|
|
176
|
+
publishIf(updateSessionId)
|
|
137
177
|
return
|
|
138
178
|
}
|
|
139
179
|
|
|
@@ -147,11 +187,11 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
147
187
|
const size = number(update.size)
|
|
148
188
|
if (size > 0) {
|
|
149
189
|
value.context = { used: number(update.used), size }
|
|
150
|
-
|
|
190
|
+
publishIf(updateSessionId)
|
|
151
191
|
}
|
|
152
192
|
return
|
|
153
193
|
}
|
|
154
|
-
const prompt =
|
|
194
|
+
const prompt = activePrompts.get(updateSessionId)
|
|
155
195
|
if (prompt !== undefined && prompt.firstToken === undefined
|
|
156
196
|
&& (type === 'agent_message_chunk' || type === 'agent_thought_chunk')
|
|
157
197
|
&& textOf(update.content).length > 0) {
|
|
@@ -162,28 +202,28 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
162
202
|
if (type === 'agent_message_chunk'
|
|
163
203
|
&& update?._meta?.dsh?.event === 'assistant_message') {
|
|
164
204
|
value.stats.steps += 1
|
|
165
|
-
|
|
205
|
+
publishIf(updateSessionId)
|
|
166
206
|
return
|
|
167
207
|
}
|
|
168
208
|
const callId = readString(update, 'toolCallId', 'tool_call_id')
|
|
169
209
|
if (type === 'tool_call' && callId !== undefined) {
|
|
170
|
-
toolStarts.set(`${
|
|
210
|
+
toolStarts.set(`${updateSessionId}\u0000${callId}`, now())
|
|
171
211
|
return
|
|
172
212
|
}
|
|
173
213
|
if (type === 'tool_call_update' && callId !== undefined
|
|
174
214
|
&& ['completed', 'failed'].includes(update.status)) {
|
|
175
|
-
const key = `${
|
|
215
|
+
const key = `${updateSessionId}\u0000${callId}`
|
|
176
216
|
const started = toolStarts.get(key)
|
|
177
217
|
toolStarts.delete(key)
|
|
178
218
|
if (started === undefined) return
|
|
179
219
|
const duration = Math.max(0, now() - started)
|
|
180
220
|
value.stats.toolMillis += duration
|
|
181
221
|
if (prompt !== undefined) prompt.toolMillis += duration
|
|
182
|
-
|
|
222
|
+
publishIf(updateSessionId)
|
|
183
223
|
}
|
|
184
224
|
}
|
|
185
225
|
|
|
186
|
-
function addUsage(usage) {
|
|
226
|
+
function addUsage(value, usage) {
|
|
187
227
|
const next = usageOf(usage)
|
|
188
228
|
value.usage.input += next.input
|
|
189
229
|
value.usage.output += next.output
|
|
@@ -193,7 +233,7 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
193
233
|
value.usage.reasoning += next.reasoning
|
|
194
234
|
}
|
|
195
235
|
|
|
196
|
-
const core = { current, subscribe, observeClient, observeAgent }
|
|
236
|
+
const core = { current, subscribe, observeClient, observeAgent, selectSession }
|
|
197
237
|
const service = typeof ctx.provide === 'function'
|
|
198
238
|
? new AcpSessionStatsService(ctx, core)
|
|
199
239
|
: {
|
|
@@ -201,6 +241,7 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
201
241
|
subscribe(listener) { return subscribe(ctx, listener) },
|
|
202
242
|
observeClient,
|
|
203
243
|
observeAgent,
|
|
244
|
+
selectSession,
|
|
204
245
|
}
|
|
205
246
|
if (typeof ctx.provide !== 'function') ctx.acpSessionStats = service
|
|
206
247
|
return service
|
|
@@ -14,7 +14,7 @@ export const name = 'acp-session-status'
|
|
|
14
14
|
export const inject = ['acpClientEvents', 'acpSessionConfig']
|
|
15
15
|
|
|
16
16
|
const AUTH_REQUIRED_CODE = -32000
|
|
17
|
-
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
17
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load', 'session/resume'])
|
|
18
18
|
const RUNNING_UPDATE_TYPES = new Set([
|
|
19
19
|
'user_message_chunk',
|
|
20
20
|
'agent_message_chunk',
|
|
@@ -37,20 +37,20 @@ class AcpSessionStatusService extends Service {
|
|
|
37
37
|
subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
|
|
38
38
|
observeClient(message) { return this.core.observeClient(message) }
|
|
39
39
|
observeAgent(message) { return this.core.observeAgent(message) }
|
|
40
|
+
selectSession(sessionId) { return this.core.selectSession(sessionId) }
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function
|
|
43
|
+
function zeroSession(sessionId, bound = sessionId !== undefined) {
|
|
43
44
|
return {
|
|
44
45
|
state: 'idle',
|
|
45
|
-
|
|
46
|
-
server: undefined,
|
|
47
|
-
auth: { status: undefined, method: undefined },
|
|
48
|
-
session: { sessionId: undefined, bound: false },
|
|
46
|
+
session: { sessionId, bound },
|
|
49
47
|
model: undefined,
|
|
50
48
|
effort: undefined,
|
|
51
49
|
permission: undefined,
|
|
52
50
|
plan: undefined,
|
|
53
51
|
agent: undefined,
|
|
52
|
+
permissionPreset: undefined,
|
|
53
|
+
sandboxMode: undefined,
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -60,21 +60,55 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
60
60
|
?? ctx.acpSessionConfig ?? ctx.get?.('acpSessionConfig')
|
|
61
61
|
|
|
62
62
|
const listeners = new Set()
|
|
63
|
-
|
|
63
|
+
const sessions = new Map()
|
|
64
|
+
const fallback = zeroSession(undefined, false)
|
|
65
|
+
let sessionId
|
|
66
|
+
let selectionKnown = false
|
|
67
|
+
let connection = 'connecting'
|
|
68
|
+
let server
|
|
69
|
+
const auth = { status: undefined, method: undefined }
|
|
64
70
|
let initializeId
|
|
65
|
-
|
|
71
|
+
const pendingAuthenticate = new Set()
|
|
66
72
|
const pendingSetup = new Map()
|
|
67
73
|
const pendingPrompts = new Map()
|
|
68
|
-
let permissionPreset
|
|
69
|
-
let sandboxMode
|
|
70
74
|
|
|
71
|
-
function
|
|
75
|
+
function stateFor(targetSessionId, bound = targetSessionId !== undefined) {
|
|
76
|
+
if (typeof targetSessionId !== 'string' || targetSessionId.length === 0) {
|
|
77
|
+
return fallback
|
|
78
|
+
}
|
|
79
|
+
let value = sessions.get(targetSessionId)
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
value = zeroSession(targetSessionId, bound)
|
|
82
|
+
sessions.set(targetSessionId, value)
|
|
83
|
+
} else if (bound) {
|
|
84
|
+
value.session.bound = true
|
|
85
|
+
}
|
|
86
|
+
return value
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function current() {
|
|
90
|
+
const value = stateFor(sessionId)
|
|
91
|
+
const { permissionPreset: _permissionPreset, sandboxMode: _sandboxMode, ...visible } = value
|
|
92
|
+
return structuredClone({ connection, server, auth, ...visible })
|
|
93
|
+
}
|
|
72
94
|
|
|
73
95
|
function publish() {
|
|
74
96
|
const snapshot = current()
|
|
75
97
|
for (const listener of [...listeners]) listener(snapshot)
|
|
76
98
|
}
|
|
77
99
|
|
|
100
|
+
function publishIf(targetSessionId) {
|
|
101
|
+
if (targetSessionId === sessionId) publish()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function selectSession(nextSessionId) {
|
|
105
|
+
selectionKnown = true
|
|
106
|
+
sessionId = typeof nextSessionId === 'string' && nextSessionId.length > 0
|
|
107
|
+
? nextSessionId
|
|
108
|
+
: undefined
|
|
109
|
+
publish()
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
function subscribe(effectCtx, listener) {
|
|
79
113
|
if (typeof listener !== 'function') {
|
|
80
114
|
throw new Error('acpSessionStatus.subscribe: listener must be a function')
|
|
@@ -102,27 +136,28 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
102
136
|
}
|
|
103
137
|
if (message.method === 'authenticate' && message.id !== undefined) {
|
|
104
138
|
pendingAuthenticate.add(message.id)
|
|
105
|
-
|
|
139
|
+
auth.status = 'signing in'
|
|
106
140
|
publish()
|
|
107
141
|
return
|
|
108
142
|
}
|
|
109
143
|
if (SETUP_METHODS.has(message.method) && message.id !== undefined) {
|
|
110
144
|
pendingSetup.set(
|
|
111
145
|
message.id,
|
|
112
|
-
message.method
|
|
146
|
+
message.method !== 'session/new'
|
|
113
147
|
? readString(message.params, 'sessionId', 'session_id')
|
|
114
148
|
: undefined,
|
|
115
149
|
)
|
|
116
150
|
return
|
|
117
151
|
}
|
|
118
152
|
if (message.method === 'session/prompt' && message.id !== undefined) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
153
|
+
const promptSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
154
|
+
if (promptSessionId === undefined) return
|
|
155
|
+
if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
|
|
156
|
+
pendingPrompts.set(message.id, promptSessionId)
|
|
157
|
+
const value = stateFor(promptSessionId)
|
|
123
158
|
if (value.state === 'idle') {
|
|
124
159
|
value.state = 'starting'
|
|
125
|
-
|
|
160
|
+
publishIf(promptSessionId)
|
|
126
161
|
}
|
|
127
162
|
}
|
|
128
163
|
}
|
|
@@ -131,14 +166,14 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
131
166
|
if (!object(message)) return
|
|
132
167
|
if (message.id !== undefined && message.id === initializeId) {
|
|
133
168
|
initializeId = undefined
|
|
134
|
-
|
|
169
|
+
connection = 'attached'
|
|
135
170
|
const result = object(message.result) ? message.result : undefined
|
|
136
|
-
|
|
171
|
+
server = readString(result?.agentInfo, 'name')
|
|
137
172
|
const methods = Array.isArray(result?.authMethods) ? result.authMethods : []
|
|
138
173
|
const method = methods.find((candidate) => object(candidate)
|
|
139
174
|
&& typeof candidate.id === 'string' && candidate.id.length > 0)
|
|
140
|
-
if (method !== undefined &&
|
|
141
|
-
|
|
175
|
+
if (method !== undefined && auth.status === undefined) {
|
|
176
|
+
auth.method = readString(method, 'name', 'label') ?? method.id
|
|
142
177
|
}
|
|
143
178
|
publish()
|
|
144
179
|
return
|
|
@@ -146,11 +181,11 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
146
181
|
if (message.id !== undefined && pendingAuthenticate.has(message.id)) {
|
|
147
182
|
pendingAuthenticate.delete(message.id)
|
|
148
183
|
if (message.error === undefined) {
|
|
149
|
-
|
|
184
|
+
auth.status = 'configured'
|
|
150
185
|
} else if (isAuthRequired(message.error)) {
|
|
151
|
-
|
|
186
|
+
auth.status = 'needs sign-in'
|
|
152
187
|
} else {
|
|
153
|
-
|
|
188
|
+
auth.status = undefined
|
|
154
189
|
}
|
|
155
190
|
publish()
|
|
156
191
|
return
|
|
@@ -159,82 +194,99 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
159
194
|
const requested = pendingSetup.get(message.id)
|
|
160
195
|
pendingSetup.delete(message.id)
|
|
161
196
|
if (message.error !== undefined) {
|
|
162
|
-
if (isAuthRequired(message.error))
|
|
163
|
-
|
|
197
|
+
if (isAuthRequired(message.error)) auth.status = 'needs sign-in'
|
|
198
|
+
if (requested !== undefined) {
|
|
199
|
+
stateFor(requested, false).session.bound = false
|
|
200
|
+
}
|
|
201
|
+
if (!selectionKnown) sessionId = requested
|
|
164
202
|
} else {
|
|
165
|
-
const
|
|
166
|
-
|
|
203
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? requested
|
|
204
|
+
if (bound !== undefined) stateFor(bound).session = { sessionId: bound, bound: true }
|
|
205
|
+
if (!selectionKnown) sessionId = bound
|
|
167
206
|
}
|
|
168
207
|
publish()
|
|
169
208
|
return
|
|
170
209
|
}
|
|
171
210
|
if (response(message) && pendingPrompts.has(message.id)) {
|
|
211
|
+
const promptSessionId = pendingPrompts.get(message.id)
|
|
172
212
|
pendingPrompts.delete(message.id)
|
|
173
213
|
let changed = false
|
|
174
214
|
if (message.error !== undefined && isAuthRequired(message.error)
|
|
175
|
-
&&
|
|
176
|
-
|
|
215
|
+
&& auth.status !== 'needs sign-in') {
|
|
216
|
+
auth.status = 'needs sign-in'
|
|
177
217
|
changed = true
|
|
178
218
|
}
|
|
179
|
-
|
|
219
|
+
const value = stateFor(promptSessionId)
|
|
220
|
+
if (!hasPendingPrompt(promptSessionId) && value.state !== 'idle') {
|
|
180
221
|
value.state = 'idle'
|
|
181
222
|
changed = true
|
|
182
223
|
}
|
|
183
|
-
if (changed) publish()
|
|
224
|
+
if (changed && (promptSessionId === sessionId || auth.status === 'needs sign-in')) publish()
|
|
184
225
|
return
|
|
185
226
|
}
|
|
186
227
|
if (message.error !== undefined && isAuthRequired(message.error)) {
|
|
187
|
-
|
|
228
|
+
auth.status = 'needs sign-in'
|
|
188
229
|
publish()
|
|
189
230
|
return
|
|
190
231
|
}
|
|
191
232
|
|
|
192
233
|
if (message.method === 'session.status' && object(message.params)) {
|
|
234
|
+
const statusSessionId = readString(message.params, 'sessionId', 'session_id') ?? sessionId
|
|
235
|
+
if (statusSessionId === undefined) return
|
|
236
|
+
if (!selectionKnown && sessionId === undefined) sessionId = statusSessionId
|
|
193
237
|
const status = readString(message.params, 'status')
|
|
194
|
-
|
|
238
|
+
const value = stateFor(statusSessionId)
|
|
239
|
+
if (status === 'running' || (status === 'idle' && !hasPendingPrompt(statusSessionId))) {
|
|
195
240
|
value.state = status
|
|
196
|
-
|
|
241
|
+
publishIf(statusSessionId)
|
|
197
242
|
}
|
|
198
243
|
return
|
|
199
244
|
}
|
|
200
245
|
if (message.method === 'session.event' && object(message.params)) {
|
|
246
|
+
const eventSessionId = readString(message.params, 'sessionId', 'session_id') ?? sessionId
|
|
247
|
+
if (eventSessionId === undefined) return
|
|
248
|
+
if (!selectionKnown && sessionId === undefined) sessionId = eventSessionId
|
|
249
|
+
const value = stateFor(eventSessionId)
|
|
201
250
|
const event = object(message.params.event) ? message.params.event : undefined
|
|
202
251
|
const type = readString(event, 'type')
|
|
203
252
|
const data = object(event?.data) ? event.data : undefined
|
|
204
253
|
if (type === 'permission/preset') {
|
|
205
254
|
const preset = readString(data, 'preset')
|
|
206
255
|
if (preset !== undefined) {
|
|
207
|
-
permissionPreset = preset
|
|
208
|
-
value.permission = permissionPreset ?? sandboxMode
|
|
209
|
-
|
|
256
|
+
value.permissionPreset = preset
|
|
257
|
+
value.permission = value.permissionPreset ?? value.sandboxMode
|
|
258
|
+
publishIf(eventSessionId)
|
|
210
259
|
}
|
|
211
260
|
} else if (type === 'sandbox/mode') {
|
|
212
261
|
const mode = readString(data, 'mode')
|
|
213
262
|
if (mode !== undefined) {
|
|
214
|
-
sandboxMode = mode
|
|
215
|
-
value.permission = permissionPreset ?? sandboxMode
|
|
216
|
-
|
|
263
|
+
value.sandboxMode = mode
|
|
264
|
+
value.permission = value.permissionPreset ?? value.sandboxMode
|
|
265
|
+
publishIf(eventSessionId)
|
|
217
266
|
}
|
|
218
267
|
} else if (type === 'plan/mode' && typeof data?.active === 'boolean') {
|
|
219
268
|
value.plan = data.active
|
|
220
|
-
|
|
269
|
+
publishIf(eventSessionId)
|
|
221
270
|
} else if (type === 'agent-preset/selected') {
|
|
222
271
|
const preset = readString(data, 'agentPreset')
|
|
223
272
|
if (preset !== undefined) {
|
|
224
273
|
value.agent = preset
|
|
225
|
-
|
|
274
|
+
publishIf(eventSessionId)
|
|
226
275
|
}
|
|
227
276
|
}
|
|
228
277
|
return
|
|
229
278
|
}
|
|
230
279
|
if (message.method !== 'session/update' || !object(message.params)) return
|
|
231
|
-
const
|
|
280
|
+
const updateSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
281
|
+
if (updateSessionId === undefined) return
|
|
282
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updateSessionId
|
|
232
283
|
const update = object(message.params.update) ? message.params.update : undefined
|
|
233
284
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
234
|
-
|
|
285
|
+
const value = stateFor(updateSessionId)
|
|
286
|
+
if (value.state !== 'running' && hasPendingPrompt(updateSessionId)
|
|
235
287
|
&& RUNNING_UPDATE_TYPES.has(type)) {
|
|
236
288
|
value.state = 'running'
|
|
237
|
-
|
|
289
|
+
publishIf(updateSessionId)
|
|
238
290
|
}
|
|
239
291
|
}
|
|
240
292
|
|
|
@@ -248,13 +300,13 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
248
300
|
|
|
249
301
|
function onConfigSnapshot(snapshot) {
|
|
250
302
|
if (snapshot === null || typeof snapshot !== 'object') return
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
303
|
+
const configSessionId = typeof snapshot.sessionId === 'string' && snapshot.sessionId.length > 0
|
|
304
|
+
? snapshot.sessionId
|
|
305
|
+
: sessionId
|
|
306
|
+
const value = stateFor(configSessionId)
|
|
255
307
|
value.model = optionValue(snapshot.options, 'model') ?? value.model
|
|
256
308
|
value.effort = optionValue(snapshot.options, 'effort') ?? value.effort
|
|
257
|
-
|
|
309
|
+
publishIf(configSessionId)
|
|
258
310
|
}
|
|
259
311
|
|
|
260
312
|
function optionValue(options, id) {
|
|
@@ -265,7 +317,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
265
317
|
return typeof raw === 'string' ? raw : undefined
|
|
266
318
|
}
|
|
267
319
|
|
|
268
|
-
const core = { current, subscribe, observeClient, observeAgent }
|
|
320
|
+
const core = { current, subscribe, observeClient, observeAgent, selectSession }
|
|
269
321
|
const service = typeof ctx.provide === 'function'
|
|
270
322
|
? new AcpSessionStatusService(ctx, core)
|
|
271
323
|
: {
|
|
@@ -273,6 +325,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
273
325
|
subscribe(listener) { return subscribe(ctx, listener) },
|
|
274
326
|
observeClient,
|
|
275
327
|
observeAgent,
|
|
328
|
+
selectSession,
|
|
276
329
|
}
|
|
277
330
|
if (typeof ctx.provide !== 'function') ctx.acpSessionStatus = service
|
|
278
331
|
|
|
@@ -286,7 +339,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
286
339
|
if (typeof events?.register === 'function') {
|
|
287
340
|
// register(observer) only: the service scopes the subscription to its
|
|
288
341
|
// own Context, so pass the folding object alone.
|
|
289
|
-
events.register({ observeClient, observeAgent })
|
|
342
|
+
events.register({ observeClient, observeAgent, selectSession })
|
|
290
343
|
}
|
|
291
344
|
|
|
292
345
|
return service
|
package/lib/cordis-protocol.js
CHANGED
|
@@ -37,6 +37,7 @@ export const CORDIS_METHODS = Object.freeze({
|
|
|
37
37
|
agentsUpdate: '_dsh/cordis/tui/agents/update',
|
|
38
38
|
agentsSelect: '_dsh/cordis/tui/agents/select',
|
|
39
39
|
agentsNavigate: '_dsh/cordis/tui/agents/navigate',
|
|
40
|
+
sessionActive: '_dsh/cordis/tui/session/active',
|
|
40
41
|
sessionConfigSet: '_dsh/cordis/tui/session-config/set',
|
|
41
42
|
})
|
|
42
43
|
|
package/lib/harnesses.js
CHANGED
|
@@ -28,14 +28,28 @@ function readSettings(settingsPath) {
|
|
|
28
28
|
try {
|
|
29
29
|
value = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
30
30
|
} catch (error) {
|
|
31
|
-
|
|
31
|
+
// A corrupt settings file must never block boot (boot.js: "must not
|
|
32
|
+
// block boot"). Park the unreadable file for diagnosis and start
|
|
33
|
+
// over — the same resilience tui-theme/tui-presets apply.
|
|
34
|
+
quarantineSettings(settingsPath, `invalid Martty settings: ${error.message}`)
|
|
35
|
+
return {}
|
|
32
36
|
}
|
|
33
37
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
-
|
|
38
|
+
quarantineSettings(settingsPath, 'invalid Martty settings: root must be an object')
|
|
39
|
+
return {}
|
|
35
40
|
}
|
|
36
41
|
return value
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function quarantineSettings(settingsPath, reason) {
|
|
45
|
+
process.stderr.write(`martty: ${reason} — moving it aside and starting fresh\n`)
|
|
46
|
+
try {
|
|
47
|
+
renameSync(settingsPath, `${settingsPath}.corrupt-${Date.now()}`)
|
|
48
|
+
} catch {
|
|
49
|
+
// Best effort only; the fresh settings write below still proceeds.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
function writeSettings(settingsPath, value) {
|
|
40
54
|
mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
41
55
|
const temporary = `${settingsPath}.${process.pid}.${Date.now()}.tmp`
|
package/lib/index.js
CHANGED
|
@@ -132,9 +132,10 @@ export async function applyShell(ctx, options = {}) {
|
|
|
132
132
|
}
|
|
133
133
|
const clientEvents = ctx.acpClientEvents ?? ctx.get?.('acpClientEvents')
|
|
134
134
|
if (clientEvents === undefined || typeof clientEvents.observeClient !== 'function'
|
|
135
|
-
|| typeof clientEvents.observeAgent !== 'function'
|
|
135
|
+
|| typeof clientEvents.observeAgent !== 'function'
|
|
136
|
+
|| typeof clientEvents.selectSession !== 'function') {
|
|
136
137
|
throw new Error(
|
|
137
|
-
'dsh-tui-shell: ctx.acpClientEvents must expose ACP observers',
|
|
138
|
+
'dsh-tui-shell: ctx.acpClientEvents must expose ACP observers and Session selection',
|
|
138
139
|
)
|
|
139
140
|
}
|
|
140
141
|
const queue = ctx.tuiQueue ?? ctx.get?.('tuiQueue')
|
|
@@ -180,7 +181,9 @@ export async function applyShell(ctx, options = {}) {
|
|
|
180
181
|
if (!shouldQuitOnPainterExit(signal)) return
|
|
181
182
|
shellState.refuseRespawn = true
|
|
182
183
|
if (shellState.livePainter?.child === child) shellState.livePainter = null
|
|
183
|
-
|
|
184
|
+
// Signal exits must not read as success: 130/143 match the
|
|
185
|
+
// bin/martty.js convention; any other signal is a failure.
|
|
186
|
+
process.exit(code ?? (signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 1))
|
|
184
187
|
})
|
|
185
188
|
child.on('error', (err) => {
|
|
186
189
|
console.error(`dsh-tui: ${err.message}`)
|
|
@@ -241,6 +244,13 @@ export async function applyShell(ctx, options = {}) {
|
|
|
241
244
|
if (message.method === CORDIS_METHODS.agentsUpdate) {
|
|
242
245
|
return { ok: agents.observe(message.params) }
|
|
243
246
|
}
|
|
247
|
+
if (message.method === CORDIS_METHODS.sessionActive) {
|
|
248
|
+
const active = message.params?.sessionId
|
|
249
|
+
clientEvents.selectSession(
|
|
250
|
+
typeof active === 'string' && active.length > 0 ? active : undefined,
|
|
251
|
+
)
|
|
252
|
+
return { ok: true }
|
|
253
|
+
}
|
|
244
254
|
if (message.method === CORDIS_METHODS.approvalRespond) {
|
|
245
255
|
return clientRunner.respondApproval(message.params)
|
|
246
256
|
}
|
package/lib/mux.js
CHANGED
|
@@ -24,6 +24,7 @@ const COMPOSITOR_METHODS = Object.freeze(new Set([
|
|
|
24
24
|
CORDIS_METHODS.agentsUpdate,
|
|
25
25
|
CORDIS_METHODS.agentsSelect,
|
|
26
26
|
CORDIS_METHODS.agentsNavigate,
|
|
27
|
+
CORDIS_METHODS.sessionActive,
|
|
27
28
|
CORDIS_METHODS.sessionConfigSet,
|
|
28
29
|
CORDIS_METHODS.approvalRespond,
|
|
29
30
|
CORDIS_METHODS.uiSelected,
|
package/lib/runner.js
CHANGED
|
@@ -79,7 +79,11 @@ export async function apply(ctx) {
|
|
|
79
79
|
void release()
|
|
80
80
|
})
|
|
81
81
|
child.once('exit', (code, signal) => {
|
|
82
|
-
|
|
82
|
+
// Profile patch watching can recompose the Host tree immediately after
|
|
83
|
+
// startup. In that path our disposer deliberately terminates this Client
|
|
84
|
+
// process; its resulting SIGTERM belongs to the old fiber and must not
|
|
85
|
+
// tear down the replacement tree through appExit(1).
|
|
86
|
+
if (!released) requestExit(code ?? (signal === 'SIGINT' ? 130 : 1))
|
|
83
87
|
void release()
|
|
84
88
|
})
|
|
85
89
|
process.once('exit', onProcessExit)
|
package/lib/tui-theme.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* flushed from `bindNotify`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
11
11
|
import { dirname, isAbsolute } from 'node:path'
|
|
12
12
|
import { CORDIS_METHODS } from './cordis-protocol.js'
|
|
13
13
|
|
|
@@ -92,7 +92,12 @@ function writePreferred(settingsPath, id) {
|
|
|
92
92
|
const settings = readSettings(settingsPath)
|
|
93
93
|
settings.theme = id
|
|
94
94
|
mkdirSync(dirname(settingsPath), { recursive: true })
|
|
95
|
-
|
|
95
|
+
// Atomic write (temp + rename), matching harnesses.writeSettings: a
|
|
96
|
+
// crash mid-write must never leave a truncated settings.json behind —
|
|
97
|
+
// every later launch would otherwise start from an unreadable file.
|
|
98
|
+
const temporary = `${settingsPath}.${process.pid}.${Date.now()}.tmp`
|
|
99
|
+
writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`)
|
|
100
|
+
renameSync(temporary, settingsPath)
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
package/package.json
CHANGED
|
Binary file
|
package/vendor/darwin-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|