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/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,30 @@ 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
|
})
|
|
123
|
+
if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
|
|
91
124
|
return
|
|
92
125
|
}
|
|
93
126
|
if (message.method !== 'session/prompt') return
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
127
|
+
const promptSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
128
|
+
if (promptSessionId === undefined) return
|
|
129
|
+
if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
|
|
130
|
+
const prompt = {
|
|
131
|
+
sessionId: promptSessionId,
|
|
132
|
+
primary: !activePrompts.has(promptSessionId),
|
|
133
|
+
started: now(),
|
|
134
|
+
firstToken: undefined,
|
|
135
|
+
toolMillis: 0,
|
|
136
|
+
}
|
|
99
137
|
pendingPrompts.set(message.id, prompt)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
138
|
+
if (prompt.primary) {
|
|
139
|
+
activePrompts.set(promptSessionId, prompt)
|
|
140
|
+
stateFor(promptSessionId).stats.turns += 1
|
|
141
|
+
}
|
|
142
|
+
publishIf(promptSessionId)
|
|
103
143
|
}
|
|
104
144
|
|
|
105
145
|
function observeAgent(message) {
|
|
@@ -108,32 +148,38 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
108
148
|
const tracked = pendingSetup.get(message.id)
|
|
109
149
|
pendingSetup.delete(message.id)
|
|
110
150
|
if (message.error !== undefined || !object(message.result)) return
|
|
111
|
-
|
|
151
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
152
|
+
if (!selectionKnown) sessionId = bound
|
|
153
|
+
// Replay notifications precede the setup response. Preserve their state.
|
|
154
|
+
if (!values.has(bound)) reset(bound)
|
|
155
|
+
else if (bound === sessionId) publish()
|
|
112
156
|
return
|
|
113
157
|
}
|
|
114
158
|
if (message.id !== undefined && pendingPrompts.has(message.id)) {
|
|
115
159
|
const prompt = pendingPrompts.get(message.id)
|
|
116
160
|
pendingPrompts.delete(message.id)
|
|
117
|
-
activePrompts.delete(prompt.sessionId)
|
|
161
|
+
if (activePrompts.get(prompt.sessionId) === prompt) activePrompts.delete(prompt.sessionId)
|
|
162
|
+
const value = stateFor(prompt.sessionId)
|
|
118
163
|
if (message.error === undefined && object(message.result)) {
|
|
119
|
-
addUsage(message.result.usage)
|
|
164
|
+
addUsage(value, message.result.usage)
|
|
120
165
|
}
|
|
121
166
|
const elapsed = Math.max(0, now() - prompt.started)
|
|
122
|
-
value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
123
|
-
|
|
167
|
+
if (prompt.primary) value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
168
|
+
publishIf(prompt.sessionId)
|
|
124
169
|
return
|
|
125
170
|
}
|
|
126
171
|
if (message.method !== 'session/update' || !object(message.params)) return
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
if (
|
|
172
|
+
const updateSessionId = readString(message.params, 'sessionId', 'session_id')
|
|
173
|
+
if (updateSessionId === undefined) return
|
|
174
|
+
if (!selectionKnown && sessionId === undefined) sessionId = updateSessionId
|
|
175
|
+
const value = stateFor(updateSessionId)
|
|
130
176
|
const update = message.params.update
|
|
131
177
|
if (!object(update)) return
|
|
132
178
|
|
|
133
179
|
const replay = update?._meta?.dsh
|
|
134
180
|
if (object(replay) && replay.event === 'prompt/usage') {
|
|
135
181
|
value.usage = usageOf(replay.usage)
|
|
136
|
-
|
|
182
|
+
publishIf(updateSessionId)
|
|
137
183
|
return
|
|
138
184
|
}
|
|
139
185
|
|
|
@@ -147,11 +193,11 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
147
193
|
const size = number(update.size)
|
|
148
194
|
if (size > 0) {
|
|
149
195
|
value.context = { used: number(update.used), size }
|
|
150
|
-
|
|
196
|
+
publishIf(updateSessionId)
|
|
151
197
|
}
|
|
152
198
|
return
|
|
153
199
|
}
|
|
154
|
-
const prompt =
|
|
200
|
+
const prompt = activePrompts.get(updateSessionId)
|
|
155
201
|
if (prompt !== undefined && prompt.firstToken === undefined
|
|
156
202
|
&& (type === 'agent_message_chunk' || type === 'agent_thought_chunk')
|
|
157
203
|
&& textOf(update.content).length > 0) {
|
|
@@ -162,28 +208,28 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
162
208
|
if (type === 'agent_message_chunk'
|
|
163
209
|
&& update?._meta?.dsh?.event === 'assistant_message') {
|
|
164
210
|
value.stats.steps += 1
|
|
165
|
-
|
|
211
|
+
publishIf(updateSessionId)
|
|
166
212
|
return
|
|
167
213
|
}
|
|
168
214
|
const callId = readString(update, 'toolCallId', 'tool_call_id')
|
|
169
215
|
if (type === 'tool_call' && callId !== undefined) {
|
|
170
|
-
toolStarts.set(`${
|
|
216
|
+
toolStarts.set(`${updateSessionId}\u0000${callId}`, now())
|
|
171
217
|
return
|
|
172
218
|
}
|
|
173
219
|
if (type === 'tool_call_update' && callId !== undefined
|
|
174
220
|
&& ['completed', 'failed'].includes(update.status)) {
|
|
175
|
-
const key = `${
|
|
221
|
+
const key = `${updateSessionId}\u0000${callId}`
|
|
176
222
|
const started = toolStarts.get(key)
|
|
177
223
|
toolStarts.delete(key)
|
|
178
224
|
if (started === undefined) return
|
|
179
225
|
const duration = Math.max(0, now() - started)
|
|
180
226
|
value.stats.toolMillis += duration
|
|
181
227
|
if (prompt !== undefined) prompt.toolMillis += duration
|
|
182
|
-
|
|
228
|
+
publishIf(updateSessionId)
|
|
183
229
|
}
|
|
184
230
|
}
|
|
185
231
|
|
|
186
|
-
function addUsage(usage) {
|
|
232
|
+
function addUsage(value, usage) {
|
|
187
233
|
const next = usageOf(usage)
|
|
188
234
|
value.usage.input += next.input
|
|
189
235
|
value.usage.output += next.output
|
|
@@ -193,7 +239,7 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
193
239
|
value.usage.reasoning += next.reasoning
|
|
194
240
|
}
|
|
195
241
|
|
|
196
|
-
const core = { current, subscribe, observeClient, observeAgent }
|
|
242
|
+
const core = { current, subscribe, observeClient, observeAgent, selectSession }
|
|
197
243
|
const service = typeof ctx.provide === 'function'
|
|
198
244
|
? new AcpSessionStatsService(ctx, core)
|
|
199
245
|
: {
|
|
@@ -201,6 +247,7 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
201
247
|
subscribe(listener) { return subscribe(ctx, listener) },
|
|
202
248
|
observeClient,
|
|
203
249
|
observeAgent,
|
|
250
|
+
selectSession,
|
|
204
251
|
}
|
|
205
252
|
if (typeof ctx.provide !== 'function') ctx.acpSessionStats = service
|
|
206
253
|
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)
|