@vibe-forge/mcp 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/sync.spec.ts +23 -0
- package/__tests__/task-manager.spec.ts +319 -1
- package/__tests__/task-tool.spec.ts +130 -0
- package/package.json +6 -6
- package/src/sync.ts +7 -1
- package/src/tools/task/index.ts +22 -78
- package/src/tools/task/manager.ts +493 -69
- package/src/tools/task/permission-recovery.ts +172 -0
- package/src/tools/task/permission-state.ts +200 -0
- package/src/tools/task/presentation.ts +95 -0
- package/src/tools/task/register-task-runtime-tools.ts +145 -0
|
@@ -3,19 +3,51 @@ import process from 'node:process'
|
|
|
3
3
|
import { loadInjectDefaultSystemPromptValue, mergeSystemPrompts } from '@vibe-forge/config'
|
|
4
4
|
import { callHook } from '@vibe-forge/hooks'
|
|
5
5
|
import { generateAdapterQueryOptions, run } from '@vibe-forge/task'
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
AdapterErrorData,
|
|
8
|
+
AdapterOutputEvent,
|
|
9
|
+
AskUserQuestionParams,
|
|
10
|
+
ChatMessage,
|
|
11
|
+
McpTaskSession,
|
|
12
|
+
PermissionInteractionDecision,
|
|
13
|
+
SessionPermissionMode
|
|
14
|
+
} from '@vibe-forge/types'
|
|
15
|
+
import { createEmptySessionPermissionState, normalizePermissionToolName } from '@vibe-forge/utils'
|
|
16
|
+
import type { SessionPermissionState } from '@vibe-forge/utils'
|
|
7
17
|
import { extractTextFromMessage } from '@vibe-forge/utils/chat-message'
|
|
8
18
|
|
|
9
19
|
import { fetchSessionMessages, postSessionEvent } from '#~/sync.js'
|
|
20
|
+
import {
|
|
21
|
+
PERMISSION_DECISION_CANCEL,
|
|
22
|
+
applyTaskPermissionDecision,
|
|
23
|
+
buildPermissionRecoveryPayload,
|
|
24
|
+
extractPermissionErrorContext,
|
|
25
|
+
resolvePermissionInteractionDecision,
|
|
26
|
+
syncTaskPermissionStateMirror
|
|
27
|
+
} from './permission-recovery'
|
|
28
|
+
|
|
29
|
+
const TASK_PERMISSION_CONTINUE_PROMPT = '权限规则已更新。请继续刚才被权限拦截的工作,并重试被阻止的操作。'
|
|
10
30
|
|
|
11
31
|
interface ServerSyncState {
|
|
12
32
|
sessionId: string
|
|
13
33
|
lastEventIndex: number
|
|
14
34
|
lastAssistantMessageId?: string
|
|
15
35
|
seenMessageIds: Set<string>
|
|
36
|
+
lastPollError?: string
|
|
16
37
|
poller?: NodeJS.Timeout
|
|
17
38
|
}
|
|
18
39
|
|
|
40
|
+
interface PendingTaskInteraction {
|
|
41
|
+
id: string
|
|
42
|
+
payload: AskUserQuestionParams
|
|
43
|
+
source: 'adapter' | 'permission_recovery'
|
|
44
|
+
subjectKeys?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ManagedTaskSession = McpTaskSession & {
|
|
48
|
+
respondInteraction?: (interactionId: string, data: string | string[]) => void | Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
19
51
|
export interface TaskInfo {
|
|
20
52
|
taskId: string
|
|
21
53
|
adapter?: string
|
|
@@ -24,17 +56,176 @@ export interface TaskInfo {
|
|
|
24
56
|
name?: string
|
|
25
57
|
permissionMode?: SessionPermissionMode
|
|
26
58
|
background?: boolean
|
|
27
|
-
status: 'running' | 'completed' | 'failed'
|
|
59
|
+
status: 'running' | 'waiting_input' | 'completed' | 'failed'
|
|
28
60
|
exitCode?: number
|
|
29
61
|
logs: string[]
|
|
30
|
-
|
|
62
|
+
permissionState: SessionPermissionState
|
|
63
|
+
pendingInteraction?: PendingTaskInteraction
|
|
64
|
+
lastError?: AdapterErrorData
|
|
65
|
+
session?: ManagedTaskSession
|
|
31
66
|
createdAt: number
|
|
32
67
|
onStop?: () => void
|
|
33
68
|
serverSync?: ServerSyncState
|
|
34
69
|
}
|
|
35
70
|
|
|
71
|
+
const appendTaskLog = (task: TaskInfo, message: string | undefined) => {
|
|
72
|
+
const normalized = message?.trim()
|
|
73
|
+
if (normalized == null || normalized === '') return
|
|
74
|
+
if (task.logs.at(-1) === normalized) return
|
|
75
|
+
task.logs.push(normalized)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const extractMessageLogText = (message: ChatMessage) => {
|
|
79
|
+
const extracted = extractTextFromMessage(message)?.trim()
|
|
80
|
+
if (extracted != null && extracted !== '') {
|
|
81
|
+
return extracted
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!Array.isArray(message.content)) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const item of message.content) {
|
|
89
|
+
if (item.type === 'tool_result') {
|
|
90
|
+
if (typeof item.content === 'string' && item.content.trim() !== '') {
|
|
91
|
+
return item.content.trim()
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const serialized = JSON.stringify(item.content)
|
|
95
|
+
return serialized === '""' ? undefined : serialized
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const formatInteractionLog = (payload: AskUserQuestionParams) => {
|
|
106
|
+
const options = payload.options
|
|
107
|
+
?.map(option => option.value ?? option.label)
|
|
108
|
+
.filter((value): value is string => value.trim() !== '')
|
|
109
|
+
const optionsSuffix = options != null && options.length > 0
|
|
110
|
+
? ` Available responses: ${options.join(', ')}.`
|
|
111
|
+
: ''
|
|
112
|
+
return `Waiting for ${
|
|
113
|
+
payload.kind === 'permission' ? 'permission' : 'user'
|
|
114
|
+
} input: ${payload.question}${optionsSuffix}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const formatPermissionErrorLog = (error: AdapterErrorData) => {
|
|
118
|
+
if (error.code !== 'permission_required' || error.details == null || typeof error.details !== 'object') {
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const details = error.details as {
|
|
123
|
+
permissionDenials?: Array<{ message?: string; deniedTools?: string[] }>
|
|
124
|
+
}
|
|
125
|
+
const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
|
|
126
|
+
const deniedTools = [
|
|
127
|
+
...new Set(permissionDenials.flatMap(item => Array.isArray(item.deniedTools) ? item.deniedTools : []))
|
|
128
|
+
]
|
|
129
|
+
const reasons = permissionDenials
|
|
130
|
+
.map(item => item.message?.trim())
|
|
131
|
+
.filter((value): value is string => value != null && value !== '')
|
|
132
|
+
|
|
133
|
+
const parts = []
|
|
134
|
+
if (deniedTools.length > 0) {
|
|
135
|
+
parts.push(`Denied tools: ${deniedTools.join(', ')}`)
|
|
136
|
+
}
|
|
137
|
+
if (reasons.length > 0) {
|
|
138
|
+
parts.push(`Reasons: ${reasons.join(' | ')}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return parts.length > 0 ? parts.join(' | ') : undefined
|
|
142
|
+
}
|
|
143
|
+
|
|
36
144
|
export class TaskManager {
|
|
37
145
|
private tasks: Map<string, TaskInfo> = new Map()
|
|
146
|
+
private permissionToolUseCache = new Map<string, Map<string, string>>()
|
|
147
|
+
|
|
148
|
+
private getPermissionToolUseCache(taskId: string) {
|
|
149
|
+
let cache = this.permissionToolUseCache.get(taskId)
|
|
150
|
+
if (cache == null) {
|
|
151
|
+
cache = new Map<string, string>()
|
|
152
|
+
this.permissionToolUseCache.set(taskId, cache)
|
|
153
|
+
}
|
|
154
|
+
return cache
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private trimPermissionToolUseCache(taskId: string, maxSize = 128) {
|
|
158
|
+
const cache = this.permissionToolUseCache.get(taskId)
|
|
159
|
+
if (cache == null) return
|
|
160
|
+
|
|
161
|
+
while (cache.size > maxSize) {
|
|
162
|
+
const firstKey = cache.keys().next().value as string | undefined
|
|
163
|
+
if (firstKey == null) break
|
|
164
|
+
cache.delete(firstKey)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private rememberPermissionToolUses(taskId: string, message: ChatMessage) {
|
|
169
|
+
if (!Array.isArray(message.content)) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const cache = this.getPermissionToolUseCache(taskId)
|
|
174
|
+
for (const item of message.content) {
|
|
175
|
+
if (
|
|
176
|
+
item == null ||
|
|
177
|
+
typeof item !== 'object' ||
|
|
178
|
+
item.type !== 'tool_use' ||
|
|
179
|
+
typeof item.id !== 'string' ||
|
|
180
|
+
item.id.trim() === ''
|
|
181
|
+
) {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rawName = typeof item.name === 'string' && item.name.trim() !== ''
|
|
186
|
+
? item.name.trim()
|
|
187
|
+
: undefined
|
|
188
|
+
const normalizedToolName = rawName?.startsWith('adapter:')
|
|
189
|
+
? rawName.split(':').at(-1)?.trim() ?? rawName
|
|
190
|
+
: rawName
|
|
191
|
+
const subject = normalizePermissionToolName(normalizedToolName ?? rawName)
|
|
192
|
+
if (subject == null) {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cache.set(item.id.trim(), subject.key)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.trimPermissionToolUseCache(taskId)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private resolvePermissionErrorContext(taskId: string, error: AdapterErrorData) {
|
|
203
|
+
const context = extractPermissionErrorContext(error)
|
|
204
|
+
if (context.subjectKeys.length > 0) {
|
|
205
|
+
return context
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const details = error.details != null && typeof error.details === 'object'
|
|
209
|
+
? error.details as Record<string, unknown>
|
|
210
|
+
: {}
|
|
211
|
+
const toolUseId = typeof details.toolUseId === 'string' && details.toolUseId.trim() !== ''
|
|
212
|
+
? details.toolUseId.trim()
|
|
213
|
+
: undefined
|
|
214
|
+
if (toolUseId == null) {
|
|
215
|
+
return context
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const cachedSubjectKey = this.permissionToolUseCache.get(taskId)?.get(toolUseId)
|
|
219
|
+
if (cachedSubjectKey == null || cachedSubjectKey.trim() === '') {
|
|
220
|
+
return context
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
subjectKeys: [...new Set([...context.subjectKeys, cachedSubjectKey])],
|
|
225
|
+
deniedTools: [...new Set([...context.deniedTools, cachedSubjectKey])],
|
|
226
|
+
reasons: context.reasons
|
|
227
|
+
}
|
|
228
|
+
}
|
|
38
229
|
|
|
39
230
|
public async startTask(options: {
|
|
40
231
|
taskId: string
|
|
@@ -59,6 +250,7 @@ export class TaskManager {
|
|
|
59
250
|
background,
|
|
60
251
|
status: 'running',
|
|
61
252
|
logs: [],
|
|
253
|
+
permissionState: createEmptySessionPermissionState(),
|
|
62
254
|
createdAt: Date.now()
|
|
63
255
|
}
|
|
64
256
|
if (enableServerSync) {
|
|
@@ -69,114 +261,140 @@ export class TaskManager {
|
|
|
69
261
|
}
|
|
70
262
|
}
|
|
71
263
|
this.tasks.set(taskId, taskInfo)
|
|
264
|
+
this.permissionToolUseCache.set(taskId, new Map())
|
|
72
265
|
|
|
266
|
+
await this.launchTask(taskInfo, 'create')
|
|
267
|
+
|
|
268
|
+
if (!background) {
|
|
269
|
+
await new Promise<void>((resolve) => {
|
|
270
|
+
const task = this.tasks.get(taskId)
|
|
271
|
+
if (!task) {
|
|
272
|
+
resolve()
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (task.status !== 'running') {
|
|
276
|
+
resolve()
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
task.onStop = resolve
|
|
280
|
+
})
|
|
281
|
+
return { taskId, logs: taskInfo.logs }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { taskId }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async launchTask(task: TaskInfo, runType: 'create' | 'resume') {
|
|
73
288
|
try {
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const promptName = name
|
|
289
|
+
const promptType = task.type !== 'default' ? task.type : undefined
|
|
290
|
+
const promptName = task.name
|
|
77
291
|
const promptCWD = process.cwd()
|
|
78
292
|
const [data, resolvedConfig] = await generateAdapterQueryOptions(
|
|
79
293
|
promptType,
|
|
80
294
|
promptName,
|
|
81
295
|
promptCWD,
|
|
82
296
|
{
|
|
83
|
-
adapter
|
|
297
|
+
adapter: task.adapter
|
|
84
298
|
}
|
|
85
299
|
)
|
|
86
300
|
const env = {
|
|
87
301
|
...process.env,
|
|
88
|
-
__VF_PROJECT_AI_CTX_ID__: process.env.__VF_PROJECT_AI_CTX_ID__ ?? taskId
|
|
302
|
+
__VF_PROJECT_AI_CTX_ID__: process.env.__VF_PROJECT_AI_CTX_ID__ ?? task.taskId
|
|
89
303
|
}
|
|
90
304
|
await callHook('GenerateSystemPrompt', {
|
|
91
305
|
cwd: promptCWD,
|
|
92
|
-
sessionId: taskId,
|
|
306
|
+
sessionId: task.taskId,
|
|
93
307
|
type: promptType,
|
|
94
308
|
name: promptName,
|
|
95
309
|
data
|
|
96
310
|
}, env)
|
|
97
311
|
|
|
98
312
|
const injectDefaultSystemPrompt = await loadInjectDefaultSystemPromptValue(promptCWD)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const { session } = await run({
|
|
103
|
-
adapter,
|
|
313
|
+
const ctxId = process.env.__VF_PROJECT_AI_CTX_ID__ ?? task.taskId
|
|
314
|
+
const { session, resolvedAdapter } = await run({
|
|
315
|
+
adapter: task.adapter,
|
|
104
316
|
cwd: process.cwd(),
|
|
105
317
|
env: {
|
|
106
318
|
...process.env,
|
|
107
319
|
__VF_PROJECT_AI_CTX_ID__: ctxId
|
|
108
320
|
}
|
|
109
321
|
}, {
|
|
110
|
-
type:
|
|
322
|
+
type: runType,
|
|
111
323
|
runtime: 'mcp',
|
|
112
324
|
mode: 'stream',
|
|
113
|
-
sessionId: taskId,
|
|
325
|
+
sessionId: task.taskId,
|
|
114
326
|
systemPrompt: mergeSystemPrompts({
|
|
115
327
|
generatedSystemPrompt: resolvedConfig.systemPrompt,
|
|
116
328
|
injectDefaultSystemPrompt
|
|
117
329
|
}),
|
|
118
|
-
permissionMode,
|
|
330
|
+
permissionMode: task.permissionMode,
|
|
119
331
|
tools: resolvedConfig.tools,
|
|
120
332
|
skills: resolvedConfig.skills,
|
|
121
333
|
mcpServers: resolvedConfig.mcpServers,
|
|
122
334
|
promptAssetIds: resolvedConfig.promptAssetIds,
|
|
123
335
|
assetBundle: resolvedConfig.assetBundle,
|
|
124
336
|
onEvent: (event: AdapterOutputEvent) => {
|
|
125
|
-
this.handleEvent(taskId, event)
|
|
337
|
+
this.handleEvent(task.taskId, event)
|
|
126
338
|
}
|
|
127
339
|
})
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
session.emit({
|
|
134
|
-
type: 'message',
|
|
135
|
-
content: [{ type: 'text', text: description }]
|
|
136
|
-
})
|
|
137
|
-
this.startServerPolling(taskId)
|
|
340
|
+
|
|
341
|
+
const current = this.tasks.get(task.taskId)
|
|
342
|
+
if (current == null) {
|
|
343
|
+
session.kill()
|
|
344
|
+
return
|
|
138
345
|
}
|
|
139
346
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
// Register callback
|
|
154
|
-
task.onStop = resolve
|
|
347
|
+
current.adapter = resolvedAdapter ?? current.adapter
|
|
348
|
+
current.session = session as ManagedTaskSession
|
|
349
|
+
current.status = 'running'
|
|
350
|
+
current.exitCode = undefined
|
|
351
|
+
current.pendingInteraction = undefined
|
|
352
|
+
current.lastError = undefined
|
|
353
|
+
try {
|
|
354
|
+
await syncTaskPermissionStateMirror({
|
|
355
|
+
cwd: process.cwd(),
|
|
356
|
+
adapter: current.adapter,
|
|
357
|
+
sessionId: current.taskId,
|
|
358
|
+
permissionState: current.permissionState
|
|
155
359
|
})
|
|
156
|
-
|
|
360
|
+
} catch (error) {
|
|
361
|
+
appendTaskLog(
|
|
362
|
+
current,
|
|
363
|
+
`Permission mirror sync failed: ${error instanceof Error ? error.message : String(error)}`
|
|
364
|
+
)
|
|
157
365
|
}
|
|
366
|
+
this.startServerPolling(task.taskId)
|
|
367
|
+
session.emit({
|
|
368
|
+
type: 'message',
|
|
369
|
+
content: [{
|
|
370
|
+
type: 'text',
|
|
371
|
+
text: runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description
|
|
372
|
+
}]
|
|
373
|
+
})
|
|
158
374
|
} catch (err) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
375
|
+
const current = this.tasks.get(task.taskId)
|
|
376
|
+
if (current) {
|
|
377
|
+
current.status = 'failed'
|
|
378
|
+
appendTaskLog(current, `Failed to start task: ${err instanceof Error ? err.message : String(err)}`)
|
|
379
|
+
current.onStop?.()
|
|
164
380
|
}
|
|
165
381
|
throw err
|
|
166
382
|
}
|
|
167
|
-
|
|
168
|
-
return { taskId }
|
|
169
383
|
}
|
|
170
384
|
|
|
171
385
|
private handleEvent(taskId: string, event: AdapterOutputEvent) {
|
|
172
386
|
const task = this.tasks.get(taskId)
|
|
173
387
|
if (!task) return
|
|
174
388
|
|
|
175
|
-
|
|
389
|
+
const shouldSyncEvent = !(task.status === 'failed' && (event.type === 'exit' || event.type === 'stop'))
|
|
390
|
+
if (shouldSyncEvent) {
|
|
391
|
+
void this.syncEvent(task, event)
|
|
392
|
+
}
|
|
176
393
|
|
|
177
394
|
switch (event.type) {
|
|
178
395
|
case 'message': {
|
|
179
396
|
const message = event.data as ChatMessage
|
|
397
|
+
this.rememberPermissionToolUses(taskId, message)
|
|
180
398
|
if (message?.id) {
|
|
181
399
|
task.serverSync?.seenMessageIds.add(message.id)
|
|
182
400
|
}
|
|
@@ -185,42 +403,85 @@ export class TaskManager {
|
|
|
185
403
|
task.serverSync.lastAssistantMessageId = message.id
|
|
186
404
|
}
|
|
187
405
|
}
|
|
188
|
-
|
|
189
|
-
let text = ''
|
|
190
|
-
if (typeof content === 'string') {
|
|
191
|
-
text = content
|
|
192
|
-
} else if (Array.isArray(content)) {
|
|
193
|
-
text = content.map(c => c.type === 'text' ? c.text : '').join('')
|
|
194
|
-
}
|
|
195
|
-
if (text) {
|
|
196
|
-
task.logs.push(text)
|
|
197
|
-
}
|
|
406
|
+
appendTaskLog(task, extractMessageLogText(message))
|
|
198
407
|
break
|
|
199
408
|
}
|
|
409
|
+
case 'interaction_request':
|
|
410
|
+
task.status = 'waiting_input'
|
|
411
|
+
task.pendingInteraction = {
|
|
412
|
+
id: event.data.id,
|
|
413
|
+
payload: event.data.payload,
|
|
414
|
+
source: 'adapter'
|
|
415
|
+
}
|
|
416
|
+
appendTaskLog(task, formatInteractionLog(event.data.payload))
|
|
417
|
+
task.onStop?.()
|
|
418
|
+
break
|
|
200
419
|
case 'error': {
|
|
201
|
-
task.
|
|
420
|
+
task.lastError = event.data
|
|
421
|
+
appendTaskLog(task, event.data.message)
|
|
422
|
+
appendTaskLog(task, formatPermissionErrorLog(event.data))
|
|
423
|
+
if (event.data.code === 'permission_required') {
|
|
424
|
+
const permissionContext = this.resolvePermissionErrorContext(taskId, event.data)
|
|
425
|
+
const payload = buildPermissionRecoveryPayload({
|
|
426
|
+
sessionId: task.taskId,
|
|
427
|
+
adapter: task.adapter,
|
|
428
|
+
currentMode: task.permissionMode,
|
|
429
|
+
context: permissionContext
|
|
430
|
+
})
|
|
431
|
+
if (payload != null) {
|
|
432
|
+
task.status = 'waiting_input'
|
|
433
|
+
task.pendingInteraction = {
|
|
434
|
+
id: `task-recovery:${task.taskId}:${Date.now()}`,
|
|
435
|
+
payload,
|
|
436
|
+
source: 'permission_recovery',
|
|
437
|
+
subjectKeys: permissionContext.subjectKeys
|
|
438
|
+
}
|
|
439
|
+
appendTaskLog(task, formatInteractionLog(payload))
|
|
440
|
+
this.stopServerPolling(taskId)
|
|
441
|
+
void this.syncSyntheticInteraction(task)
|
|
442
|
+
task.onStop?.()
|
|
443
|
+
break
|
|
444
|
+
}
|
|
445
|
+
}
|
|
202
446
|
if (event.data.fatal !== false) {
|
|
203
447
|
task.status = 'failed'
|
|
448
|
+
task.pendingInteraction = undefined
|
|
204
449
|
this.stopServerPolling(taskId)
|
|
205
450
|
task.onStop?.()
|
|
206
451
|
}
|
|
207
452
|
break
|
|
208
453
|
}
|
|
209
454
|
case 'stop': {
|
|
455
|
+
task.session = undefined
|
|
210
456
|
if (task.status === 'failed') {
|
|
211
457
|
this.stopServerPolling(taskId)
|
|
212
458
|
task.onStop?.()
|
|
213
459
|
break
|
|
214
460
|
}
|
|
215
461
|
task.status = 'completed'
|
|
462
|
+
task.pendingInteraction = undefined
|
|
216
463
|
this.stopServerPolling(taskId)
|
|
217
464
|
task.onStop?.()
|
|
218
465
|
break
|
|
219
466
|
}
|
|
220
467
|
case 'exit':
|
|
468
|
+
task.session = undefined
|
|
469
|
+
if (task.status === 'failed') {
|
|
470
|
+
task.exitCode = event.data.exitCode ?? undefined
|
|
471
|
+
this.stopServerPolling(taskId)
|
|
472
|
+
task.onStop?.()
|
|
473
|
+
break
|
|
474
|
+
}
|
|
475
|
+
if (task.status === 'waiting_input') {
|
|
476
|
+
task.exitCode = event.data.exitCode ?? undefined
|
|
477
|
+
this.stopServerPolling(taskId)
|
|
478
|
+
task.onStop?.()
|
|
479
|
+
break
|
|
480
|
+
}
|
|
221
481
|
task.status = event.data.exitCode === 0 ? 'completed' : 'failed'
|
|
222
482
|
task.exitCode = event.data.exitCode ?? undefined
|
|
223
|
-
task.
|
|
483
|
+
task.pendingInteraction = undefined
|
|
484
|
+
appendTaskLog(task, `Process exited with code ${event.data.exitCode}`)
|
|
224
485
|
this.stopServerPolling(taskId)
|
|
225
486
|
task.onStop?.()
|
|
226
487
|
break
|
|
@@ -239,6 +500,7 @@ export class TaskManager {
|
|
|
239
500
|
if (!current?.serverSync || !current.session) return
|
|
240
501
|
try {
|
|
241
502
|
const events = await fetchSessionMessages(current.serverSync.sessionId)
|
|
503
|
+
current.serverSync.lastPollError = undefined
|
|
242
504
|
const startIndex = current.serverSync.lastEventIndex
|
|
243
505
|
const newEvents = events.slice(startIndex)
|
|
244
506
|
current.serverSync.lastEventIndex = events.length
|
|
@@ -260,7 +522,13 @@ export class TaskManager {
|
|
|
260
522
|
parentUuid: current.serverSync.lastAssistantMessageId
|
|
261
523
|
})
|
|
262
524
|
}
|
|
263
|
-
} catch {
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const message = `Sync poll failed: ${error instanceof Error ? error.message : String(error)}`
|
|
527
|
+
if (current.serverSync.lastPollError !== message) {
|
|
528
|
+
current.serverSync.lastPollError = message
|
|
529
|
+
appendTaskLog(current, message)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
264
532
|
}
|
|
265
533
|
|
|
266
534
|
task.serverSync.poller = setInterval(() => {
|
|
@@ -282,7 +550,20 @@ export class TaskManager {
|
|
|
282
550
|
try {
|
|
283
551
|
await postSessionEvent(task.serverSync.sessionId, event as unknown as Record<string, unknown>)
|
|
284
552
|
} catch (err) {
|
|
285
|
-
task
|
|
553
|
+
appendTaskLog(task, `Sync event failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private async syncSyntheticInteraction(task: TaskInfo) {
|
|
558
|
+
if (task.serverSync == null || task.pendingInteraction == null) return
|
|
559
|
+
try {
|
|
560
|
+
await postSessionEvent(task.serverSync.sessionId, {
|
|
561
|
+
type: 'interaction_request',
|
|
562
|
+
id: task.pendingInteraction.id,
|
|
563
|
+
payload: task.pendingInteraction.payload
|
|
564
|
+
})
|
|
565
|
+
} catch (error) {
|
|
566
|
+
appendTaskLog(task, `Sync interaction request failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
286
567
|
}
|
|
287
568
|
}
|
|
288
569
|
|
|
@@ -294,13 +575,156 @@ export class TaskManager {
|
|
|
294
575
|
return Array.from(this.tasks.values())
|
|
295
576
|
}
|
|
296
577
|
|
|
578
|
+
public async submitTaskInput(params: {
|
|
579
|
+
taskId: string
|
|
580
|
+
interactionId?: string
|
|
581
|
+
data: string | string[]
|
|
582
|
+
}): Promise<void> {
|
|
583
|
+
const task = this.tasks.get(params.taskId)
|
|
584
|
+
if (task == null) {
|
|
585
|
+
throw new Error(`Task ${params.taskId} not found.`)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const pendingInteraction = task.pendingInteraction
|
|
589
|
+
if (pendingInteraction == null) {
|
|
590
|
+
throw new Error(`Task ${params.taskId} does not have a pending interaction.`)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const interactionId = params.interactionId ?? pendingInteraction.id
|
|
594
|
+
if (interactionId !== pendingInteraction.id) {
|
|
595
|
+
throw new Error(`Interaction ${interactionId} is not pending for task ${params.taskId}.`)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (pendingInteraction.source === 'adapter') {
|
|
599
|
+
if (task.session?.respondInteraction == null) {
|
|
600
|
+
throw new Error(`Task ${params.taskId} does not support interaction responses.`)
|
|
601
|
+
}
|
|
602
|
+
await task.session.respondInteraction(interactionId, params.data)
|
|
603
|
+
await this.syncTaskInputResponse(task, interactionId, params.data)
|
|
604
|
+
task.pendingInteraction = undefined
|
|
605
|
+
task.status = 'running'
|
|
606
|
+
const responseText = Array.isArray(params.data) ? params.data.join(', ') : params.data
|
|
607
|
+
appendTaskLog(task, `Interaction response submitted: ${responseText}`)
|
|
608
|
+
return
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const decision = resolvePermissionInteractionDecision(params.data)
|
|
612
|
+
if (decision == null) {
|
|
613
|
+
throw new Error(`Task ${params.taskId} requires a permission decision response.`)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
await this.syncTaskInputResponse(task, interactionId, params.data)
|
|
617
|
+
if (decision === PERMISSION_DECISION_CANCEL) {
|
|
618
|
+
task.pendingInteraction = undefined
|
|
619
|
+
task.status = 'failed'
|
|
620
|
+
appendTaskLog(task, 'Permission recovery cancelled. Task will not continue.')
|
|
621
|
+
task.onStop?.()
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
task.permissionState = await applyTaskPermissionDecision({
|
|
626
|
+
cwd: process.cwd(),
|
|
627
|
+
sessionId: task.taskId,
|
|
628
|
+
adapter: task.adapter,
|
|
629
|
+
permissionState: task.permissionState,
|
|
630
|
+
subjectKeys: pendingInteraction.subjectKeys ?? [],
|
|
631
|
+
action: decision as PermissionInteractionDecision
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
if (
|
|
635
|
+
decision === 'deny_once' ||
|
|
636
|
+
decision === 'deny_session' ||
|
|
637
|
+
decision === 'deny_project'
|
|
638
|
+
) {
|
|
639
|
+
task.pendingInteraction = undefined
|
|
640
|
+
task.status = 'failed'
|
|
641
|
+
appendTaskLog(task, `Permission decision applied: ${decision}. Task will not continue.`)
|
|
642
|
+
task.onStop?.()
|
|
643
|
+
return
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
task.pendingInteraction = undefined
|
|
647
|
+
task.status = 'running'
|
|
648
|
+
appendTaskLog(task, `Permission decision applied: ${decision}. Restarting task.`)
|
|
649
|
+
await this.launchTask(task, 'resume')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
public async respondToTaskInteraction(params: {
|
|
653
|
+
taskId: string
|
|
654
|
+
interactionId?: string
|
|
655
|
+
data: string | string[]
|
|
656
|
+
}): Promise<void> {
|
|
657
|
+
await this.submitTaskInput(params)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private async syncTaskInputResponse(task: TaskInfo, interactionId: string, data: string | string[]) {
|
|
661
|
+
if (!task.serverSync) return
|
|
662
|
+
try {
|
|
663
|
+
await postSessionEvent(task.serverSync.sessionId, {
|
|
664
|
+
type: 'interaction_response',
|
|
665
|
+
id: interactionId,
|
|
666
|
+
data
|
|
667
|
+
})
|
|
668
|
+
} catch (error) {
|
|
669
|
+
appendTaskLog(
|
|
670
|
+
task,
|
|
671
|
+
`Sync interaction response failed: ${error instanceof Error ? error.message : String(error)}`
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private async syncStoppedTask(params: {
|
|
677
|
+
task: TaskInfo
|
|
678
|
+
pendingInteraction?: PendingTaskInteraction
|
|
679
|
+
}) {
|
|
680
|
+
const { task, pendingInteraction } = params
|
|
681
|
+
if (!task.serverSync) return
|
|
682
|
+
|
|
683
|
+
if (pendingInteraction != null) {
|
|
684
|
+
try {
|
|
685
|
+
await postSessionEvent(task.serverSync.sessionId, {
|
|
686
|
+
type: 'interaction_response',
|
|
687
|
+
id: pendingInteraction.id,
|
|
688
|
+
data: PERMISSION_DECISION_CANCEL
|
|
689
|
+
})
|
|
690
|
+
} catch (error) {
|
|
691
|
+
appendTaskLog(
|
|
692
|
+
task,
|
|
693
|
+
`Sync interaction cancellation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
await postSessionEvent(task.serverSync.sessionId, {
|
|
700
|
+
type: 'error',
|
|
701
|
+
data: {
|
|
702
|
+
message: 'Task stopped by user',
|
|
703
|
+
fatal: true
|
|
704
|
+
}
|
|
705
|
+
})
|
|
706
|
+
} catch (error) {
|
|
707
|
+
appendTaskLog(
|
|
708
|
+
task,
|
|
709
|
+
`Sync stop event failed: ${error instanceof Error ? error.message : String(error)}`
|
|
710
|
+
)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
297
714
|
public stopTask(taskId: string): boolean {
|
|
298
715
|
const task = this.tasks.get(taskId)
|
|
299
|
-
if (task && task.session) {
|
|
300
|
-
task.
|
|
301
|
-
task.
|
|
716
|
+
if (task && (task.session != null || task.pendingInteraction != null || task.status === 'waiting_input')) {
|
|
717
|
+
const pendingInteraction = task.pendingInteraction
|
|
718
|
+
task.session?.kill()
|
|
719
|
+
task.session = undefined
|
|
720
|
+
task.pendingInteraction = undefined
|
|
721
|
+
appendTaskLog(task, 'Task stopped by user')
|
|
302
722
|
task.status = 'failed' // or 'stopped' if we had that status
|
|
303
723
|
this.stopServerPolling(taskId)
|
|
724
|
+
void this.syncStoppedTask({
|
|
725
|
+
task,
|
|
726
|
+
pendingInteraction
|
|
727
|
+
})
|
|
304
728
|
if (task.onStop) task.onStop()
|
|
305
729
|
return true
|
|
306
730
|
}
|