martty 0.2.32 → 0.2.34
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 +42 -14
- package/lib/acp-session-stats.js +81 -34
- 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 +2 -2
- 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,10 +103,11 @@ 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
|
})
|
|
110
|
+
if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
function observeAgent(message) {
|
|
@@ -98,13 +116,22 @@ export function installAcpSessionPlan(ctx) {
|
|
|
98
116
|
const tracked = pending.get(message.id)
|
|
99
117
|
pending.delete(message.id)
|
|
100
118
|
if (message.error !== undefined || !isObject(message.result)) return
|
|
101
|
-
|
|
119
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
120
|
+
if (!selectionKnown) sessionId = bound
|
|
121
|
+
// Replay notifications precede the setup response. Preserve their state.
|
|
122
|
+
if (!plansBySession.has(bound)) reset(bound)
|
|
123
|
+
else if (bound === sessionId) publish()
|
|
102
124
|
return
|
|
103
125
|
}
|
|
104
126
|
if (message.method !== 'session/update' || !isObject(message.params)) return
|
|
105
127
|
const updatedSession = readString(message.params, 'sessionId', 'session_id')
|
|
106
|
-
if (
|
|
107
|
-
if (
|
|
128
|
+
if (updatedSession === undefined) return
|
|
129
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updatedSession
|
|
130
|
+
let plans = plansBySession.get(updatedSession)
|
|
131
|
+
if (plans === undefined) {
|
|
132
|
+
plans = new Map()
|
|
133
|
+
plansBySession.set(updatedSession, plans)
|
|
134
|
+
}
|
|
108
135
|
const update = message.params.update
|
|
109
136
|
if (!isObject(update)) return
|
|
110
137
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
@@ -112,7 +139,7 @@ export function installAcpSessionPlan(ctx) {
|
|
|
112
139
|
const id = readString(update, 'planId', 'plan_id')
|
|
113
140
|
if (id === undefined) plans.clear()
|
|
114
141
|
else plans.delete(id)
|
|
115
|
-
publish()
|
|
142
|
+
if (updatedSession === sessionId) publish()
|
|
116
143
|
return
|
|
117
144
|
}
|
|
118
145
|
if (type !== 'plan' && type !== 'plan_update') return
|
|
@@ -124,10 +151,10 @@ export function installAcpSessionPlan(ctx) {
|
|
|
124
151
|
plans.delete(plan.id)
|
|
125
152
|
plans.set(plan.id, plan)
|
|
126
153
|
}
|
|
127
|
-
publish()
|
|
154
|
+
if (updatedSession === sessionId) publish()
|
|
128
155
|
}
|
|
129
156
|
|
|
130
|
-
const core = { list, current, subscribe, observeClient, observeAgent }
|
|
157
|
+
const core = { list, current, subscribe, observeClient, observeAgent, selectSession }
|
|
131
158
|
const service = typeof ctx.provide === 'function'
|
|
132
159
|
? new AcpSessionPlanService(ctx, core)
|
|
133
160
|
: {
|
|
@@ -138,6 +165,7 @@ export function installAcpSessionPlan(ctx) {
|
|
|
138
165
|
},
|
|
139
166
|
observeClient,
|
|
140
167
|
observeAgent,
|
|
168
|
+
selectSession,
|
|
141
169
|
}
|
|
142
170
|
if (typeof ctx.provide !== 'function') ctx.acpSessionPlan = service
|
|
143
171
|
return service
|