@vibe-forge/mcp 3.1.1 → 3.2.0

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.
@@ -1,875 +0,0 @@
1
- import process from 'node:process'
2
-
3
- import { loadInjectDefaultSystemPromptValue, mergeSystemPrompts } from '@vibe-forge/config'
4
- import { callHook } from '@vibe-forge/hooks'
5
- import { generateAdapterQueryOptions, run } from '@vibe-forge/task'
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'
17
- import { extractTextFromMessage } from '@vibe-forge/utils/chat-message'
18
-
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 = '权限规则已更新。请继续刚才被权限拦截的工作,并重试被阻止的操作。'
30
-
31
- interface ServerSyncState {
32
- sessionId: string
33
- lastEventIndex: number
34
- lastAssistantMessageId?: string
35
- seenMessageIds: Set<string>
36
- lastPollError?: string
37
- poller?: NodeJS.Timeout
38
- }
39
-
40
- export 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
-
51
- export interface TaskInfo {
52
- taskId: string
53
- adapter?: string
54
- model?: string
55
- description: string
56
- type?: 'default' | 'spec' | 'entity' | 'workspace'
57
- name?: string
58
- workspaceCwd?: string
59
- permissionMode?: SessionPermissionMode
60
- background?: boolean
61
- status: 'running' | 'waiting_input' | 'completed' | 'failed'
62
- exitCode?: number
63
- logs: string[]
64
- permissionState: SessionPermissionState
65
- queuedSteerMessages: string[]
66
- pendingInteraction?: PendingTaskInteraction
67
- lastError?: AdapterErrorData
68
- session?: ManagedTaskSession
69
- createdAt: number
70
- onStop?: () => void
71
- serverSync?: ServerSyncState
72
- }
73
-
74
- const appendTaskLog = (task: TaskInfo, message: string | undefined) => {
75
- const normalized = message?.trim()
76
- if (normalized == null || normalized === '') return
77
- if (task.logs.at(-1) === normalized) return
78
- task.logs.push(normalized)
79
- }
80
-
81
- const extractMessageLogText = (message: ChatMessage) => {
82
- const extracted = extractTextFromMessage(message)?.trim()
83
- if (extracted != null && extracted !== '') {
84
- return extracted
85
- }
86
-
87
- if (!Array.isArray(message.content)) {
88
- return undefined
89
- }
90
-
91
- for (const item of message.content) {
92
- if (item.type === 'tool_result') {
93
- if (typeof item.content === 'string' && item.content.trim() !== '') {
94
- return item.content.trim()
95
- }
96
- try {
97
- const serialized = JSON.stringify(item.content)
98
- return serialized === '""' ? undefined : serialized
99
- } catch {
100
- return undefined
101
- }
102
- }
103
- }
104
-
105
- return undefined
106
- }
107
-
108
- const formatInteractionLog = (payload: AskUserQuestionParams) => {
109
- const options = payload.options
110
- ?.map(option => option.value ?? option.label)
111
- .filter((value): value is string => value.trim() !== '')
112
- const optionsSuffix = options != null && options.length > 0
113
- ? ` Available responses: ${options.join(', ')}.`
114
- : ''
115
- return `Waiting for ${
116
- payload.kind === 'permission' ? 'permission' : 'user'
117
- } input: ${payload.question}${optionsSuffix}`
118
- }
119
-
120
- const formatPermissionErrorLog = (error: AdapterErrorData) => {
121
- if (error.code !== 'permission_required' || error.details == null || typeof error.details !== 'object') {
122
- return undefined
123
- }
124
-
125
- const details = error.details as {
126
- permissionDenials?: Array<{ message?: string; deniedTools?: string[] }>
127
- }
128
- const permissionDenials = Array.isArray(details.permissionDenials) ? details.permissionDenials : []
129
- const deniedTools = [
130
- ...new Set(permissionDenials.flatMap(item => Array.isArray(item.deniedTools) ? item.deniedTools : []))
131
- ]
132
- const reasons = permissionDenials
133
- .map(item => item.message?.trim())
134
- .filter((value): value is string => value != null && value !== '')
135
-
136
- const parts = []
137
- if (deniedTools.length > 0) {
138
- parts.push(`Denied tools: ${deniedTools.join(', ')}`)
139
- }
140
- if (reasons.length > 0) {
141
- parts.push(`Reasons: ${reasons.join(' | ')}`)
142
- }
143
-
144
- return parts.length > 0 ? parts.join(' | ') : undefined
145
- }
146
-
147
- const createTaskUserMessageEvent = (taskId: string, message: string) => ({
148
- type: 'message' as const,
149
- data: {
150
- id: `task-user:${taskId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
151
- role: 'user' as const,
152
- content: message,
153
- createdAt: Date.now()
154
- }
155
- })
156
-
157
- const createTaskSessionMessageEvent = (message: string, parentUuid?: string) => ({
158
- type: 'message' as const,
159
- content: [{
160
- type: 'text' as const,
161
- text: message
162
- }],
163
- ...(parentUuid != null ? { parentUuid } : {})
164
- })
165
-
166
- export class TaskManager {
167
- private tasks: Map<string, TaskInfo> = new Map()
168
- private permissionToolUseCache = new Map<string, Map<string, string>>()
169
-
170
- private getPermissionToolUseCache(taskId: string) {
171
- let cache = this.permissionToolUseCache.get(taskId)
172
- if (cache == null) {
173
- cache = new Map<string, string>()
174
- this.permissionToolUseCache.set(taskId, cache)
175
- }
176
- return cache
177
- }
178
-
179
- private trimPermissionToolUseCache(taskId: string, maxSize = 128) {
180
- const cache = this.permissionToolUseCache.get(taskId)
181
- if (cache == null) return
182
-
183
- while (cache.size > maxSize) {
184
- const firstKey = cache.keys().next().value as string | undefined
185
- if (firstKey == null) break
186
- cache.delete(firstKey)
187
- }
188
- }
189
-
190
- private rememberPermissionToolUses(taskId: string, message: ChatMessage) {
191
- if (!Array.isArray(message.content)) {
192
- return
193
- }
194
-
195
- const cache = this.getPermissionToolUseCache(taskId)
196
- for (const item of message.content) {
197
- if (
198
- item == null ||
199
- typeof item !== 'object' ||
200
- item.type !== 'tool_use' ||
201
- typeof item.id !== 'string' ||
202
- item.id.trim() === ''
203
- ) {
204
- continue
205
- }
206
-
207
- const rawName = typeof item.name === 'string' && item.name.trim() !== ''
208
- ? item.name.trim()
209
- : undefined
210
- const normalizedToolName = rawName?.startsWith('adapter:')
211
- ? rawName.split(':').at(-1)?.trim() ?? rawName
212
- : rawName
213
- const subject = normalizePermissionToolName(normalizedToolName ?? rawName)
214
- if (subject == null) {
215
- continue
216
- }
217
-
218
- cache.set(item.id.trim(), subject.key)
219
- }
220
-
221
- this.trimPermissionToolUseCache(taskId)
222
- }
223
-
224
- private resolvePermissionErrorContext(taskId: string, error: AdapterErrorData) {
225
- const context = extractPermissionErrorContext(error)
226
- if (context.subjectKeys.length > 0) {
227
- return context
228
- }
229
-
230
- const details = error.details != null && typeof error.details === 'object'
231
- ? error.details as Record<string, unknown>
232
- : {}
233
- const toolUseId = typeof details.toolUseId === 'string' && details.toolUseId.trim() !== ''
234
- ? details.toolUseId.trim()
235
- : undefined
236
- if (toolUseId == null) {
237
- return context
238
- }
239
-
240
- const cachedSubjectKey = this.permissionToolUseCache.get(taskId)?.get(toolUseId)
241
- if (cachedSubjectKey == null || cachedSubjectKey.trim() === '') {
242
- return context
243
- }
244
-
245
- return {
246
- subjectKeys: [...new Set([...context.subjectKeys, cachedSubjectKey])],
247
- deniedTools: [...new Set([...context.deniedTools, cachedSubjectKey])],
248
- reasons: context.reasons
249
- }
250
- }
251
-
252
- public async startTask(options: {
253
- taskId: string
254
- description: string
255
- type?: 'default' | 'spec' | 'entity' | 'workspace'
256
- name?: string
257
- permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
258
- adapter?: string
259
- model?: string
260
- background?: boolean
261
- enableServerSync?: boolean
262
- }): Promise<{ taskId: string; logs?: string[] }> {
263
- const {
264
- taskId,
265
- adapter,
266
- model,
267
- description,
268
- type,
269
- name,
270
- permissionMode,
271
- background = true,
272
- enableServerSync
273
- } = options
274
-
275
- // Initialize Task Info
276
- const taskInfo: TaskInfo = {
277
- taskId,
278
- adapter,
279
- model,
280
- description,
281
- type,
282
- name,
283
- permissionMode,
284
- background,
285
- status: 'running',
286
- logs: [],
287
- permissionState: createEmptySessionPermissionState(),
288
- queuedSteerMessages: [],
289
- createdAt: Date.now()
290
- }
291
- if (enableServerSync) {
292
- taskInfo.serverSync = {
293
- sessionId: taskId,
294
- lastEventIndex: 0,
295
- seenMessageIds: new Set()
296
- }
297
- }
298
- this.tasks.set(taskId, taskInfo)
299
- this.permissionToolUseCache.set(taskId, new Map())
300
-
301
- await this.launchTask(taskInfo, 'create')
302
-
303
- if (!background) {
304
- await new Promise<void>((resolve) => {
305
- const task = this.tasks.get(taskId)
306
- if (!task) {
307
- resolve()
308
- return
309
- }
310
- if (task.status !== 'running') {
311
- resolve()
312
- return
313
- }
314
- task.onStop = resolve
315
- })
316
- return { taskId, logs: taskInfo.logs }
317
- }
318
-
319
- return { taskId }
320
- }
321
-
322
- private async launchTask(
323
- task: TaskInfo,
324
- runType: 'create' | 'resume',
325
- resumeMessage?: string
326
- ) {
327
- try {
328
- const rootCwd = process.cwd()
329
- const promptType = task.type !== 'default' ? task.type : undefined
330
- const promptName = task.name
331
- const promptCWD = process.cwd()
332
- const [data, resolvedConfig] = await generateAdapterQueryOptions(
333
- promptType,
334
- promptName,
335
- promptCWD,
336
- {
337
- adapter: task.adapter,
338
- model: task.model
339
- }
340
- )
341
- const taskCwd = resolvedConfig.workspace?.cwd ?? promptCWD
342
- task.workspaceCwd = resolvedConfig.workspace?.cwd
343
- const env = {
344
- ...process.env,
345
- __VF_PROJECT_AI_CTX_ID__: task.taskId,
346
- __VF_PROJECT_WORKSPACE_FOLDER__: taskCwd,
347
- __VF_PROJECT_PRIMARY_WORKSPACE_FOLDER__: rootCwd
348
- }
349
- await callHook('GenerateSystemPrompt', {
350
- cwd: taskCwd,
351
- sessionId: task.taskId,
352
- type: promptType,
353
- name: promptName,
354
- data
355
- }, env)
356
-
357
- const injectDefaultSystemPrompt = await loadInjectDefaultSystemPromptValue(taskCwd)
358
- const ctxId = task.taskId
359
- const { session, resolvedAdapter } = await run({
360
- adapter: task.adapter,
361
- cwd: taskCwd,
362
- env: {
363
- ...env,
364
- __VF_PROJECT_AI_CTX_ID__: ctxId
365
- }
366
- }, {
367
- type: runType,
368
- runtime: 'mcp',
369
- mode: 'stream',
370
- sessionId: task.taskId,
371
- model: task.model,
372
- systemPrompt: mergeSystemPrompts({
373
- generatedSystemPrompt: resolvedConfig.systemPrompt,
374
- injectDefaultSystemPrompt
375
- }),
376
- permissionMode: task.permissionMode,
377
- tools: resolvedConfig.tools,
378
- skills: resolvedConfig.skills,
379
- mcpServers: resolvedConfig.mcpServers,
380
- promptAssetIds: resolvedConfig.promptAssetIds,
381
- assetBundle: resolvedConfig.assetBundle,
382
- onEvent: (event: AdapterOutputEvent) => {
383
- this.handleEvent(task.taskId, event)
384
- }
385
- })
386
-
387
- const current = this.tasks.get(task.taskId)
388
- if (current == null) {
389
- session.kill()
390
- return
391
- }
392
-
393
- current.adapter = resolvedAdapter ?? current.adapter
394
- current.session = session as ManagedTaskSession
395
- current.status = 'running'
396
- current.exitCode = undefined
397
- current.pendingInteraction = undefined
398
- current.lastError = undefined
399
- try {
400
- await syncTaskPermissionStateMirror({
401
- cwd: taskCwd,
402
- adapter: current.adapter,
403
- sessionId: current.taskId,
404
- permissionState: current.permissionState
405
- })
406
- } catch (error) {
407
- appendTaskLog(
408
- current,
409
- `Permission mirror sync failed: ${error instanceof Error ? error.message : String(error)}`
410
- )
411
- }
412
- this.startServerPolling(task.taskId)
413
- this.emitTaskUserMessage(
414
- current,
415
- current.session,
416
- resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
417
- )
418
- } catch (err) {
419
- const current = this.tasks.get(task.taskId)
420
- if (current) {
421
- current.status = 'failed'
422
- appendTaskLog(current, `Failed to start task: ${err instanceof Error ? err.message : String(err)}`)
423
- current.onStop?.()
424
- }
425
- throw err
426
- }
427
- }
428
-
429
- private handleEvent(taskId: string, event: AdapterOutputEvent) {
430
- const task = this.tasks.get(taskId)
431
- if (!task) return
432
-
433
- const shouldSyncEvent = !(task.status === 'failed' && (event.type === 'exit' || event.type === 'stop'))
434
- if (shouldSyncEvent) {
435
- void this.syncEvent(task, event)
436
- }
437
-
438
- switch (event.type) {
439
- case 'message': {
440
- const message = event.data as ChatMessage
441
- this.rememberPermissionToolUses(taskId, message)
442
- if (message?.id) {
443
- task.serverSync?.seenMessageIds.add(message.id)
444
- }
445
- if (message?.role === 'assistant' && message.id) {
446
- if (task.serverSync) {
447
- task.serverSync.lastAssistantMessageId = message.id
448
- }
449
- }
450
- appendTaskLog(task, extractMessageLogText(message))
451
- break
452
- }
453
- case 'interaction_request':
454
- task.status = 'waiting_input'
455
- task.pendingInteraction = {
456
- id: event.data.id,
457
- payload: event.data.payload,
458
- source: 'adapter'
459
- }
460
- appendTaskLog(task, formatInteractionLog(event.data.payload))
461
- task.onStop?.()
462
- break
463
- case 'error': {
464
- task.lastError = event.data
465
- appendTaskLog(task, event.data.message)
466
- appendTaskLog(task, formatPermissionErrorLog(event.data))
467
- if (event.data.code === 'permission_required') {
468
- const permissionContext = this.resolvePermissionErrorContext(taskId, event.data)
469
- const payload = buildPermissionRecoveryPayload({
470
- sessionId: task.taskId,
471
- adapter: task.adapter,
472
- currentMode: task.permissionMode,
473
- context: permissionContext
474
- })
475
- if (payload != null) {
476
- task.status = 'waiting_input'
477
- task.pendingInteraction = {
478
- id: `task-recovery:${task.taskId}:${Date.now()}`,
479
- payload,
480
- source: 'permission_recovery',
481
- subjectKeys: permissionContext.subjectKeys
482
- }
483
- appendTaskLog(task, formatInteractionLog(payload))
484
- this.stopServerPolling(taskId)
485
- void this.syncSyntheticInteraction(task)
486
- task.onStop?.()
487
- break
488
- }
489
- }
490
- if (event.data.fatal !== false) {
491
- task.status = 'failed'
492
- task.pendingInteraction = undefined
493
- this.stopServerPolling(taskId)
494
- task.onStop?.()
495
- }
496
- break
497
- }
498
- case 'stop': {
499
- task.session = undefined
500
- if (task.status === 'failed') {
501
- this.stopServerPolling(taskId)
502
- task.onStop?.()
503
- break
504
- }
505
- task.status = 'completed'
506
- task.pendingInteraction = undefined
507
- this.stopServerPolling(taskId)
508
- if (task.queuedSteerMessages.length > 0) {
509
- void this.dispatchQueuedSteerMessage(task).catch((error) => {
510
- task.status = 'failed'
511
- appendTaskLog(
512
- task,
513
- `Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
514
- )
515
- task.onStop?.()
516
- })
517
- break
518
- }
519
- task.onStop?.()
520
- break
521
- }
522
- case 'exit':
523
- task.session = undefined
524
- if (task.status === 'failed') {
525
- task.exitCode = event.data.exitCode ?? undefined
526
- this.stopServerPolling(taskId)
527
- task.onStop?.()
528
- break
529
- }
530
- if (task.status === 'waiting_input') {
531
- task.exitCode = event.data.exitCode ?? undefined
532
- this.stopServerPolling(taskId)
533
- task.onStop?.()
534
- break
535
- }
536
- task.status = event.data.exitCode === 0 ? 'completed' : 'failed'
537
- task.exitCode = event.data.exitCode ?? undefined
538
- task.pendingInteraction = undefined
539
- appendTaskLog(task, `Process exited with code ${event.data.exitCode}`)
540
- this.stopServerPolling(taskId)
541
- if (task.status === 'completed' && task.queuedSteerMessages.length > 0) {
542
- void this.dispatchQueuedSteerMessage(task).catch((error) => {
543
- task.status = 'failed'
544
- appendTaskLog(
545
- task,
546
- `Failed to resume steer-queued task: ${error instanceof Error ? error.message : String(error)}`
547
- )
548
- task.onStop?.()
549
- })
550
- break
551
- }
552
- task.onStop?.()
553
- break
554
- default:
555
- break
556
- }
557
- }
558
-
559
- private startServerPolling(taskId: string) {
560
- const task = this.tasks.get(taskId)
561
- if (!task?.serverSync) return
562
- if (task.serverSync.poller) return
563
-
564
- const poll = async () => {
565
- const current = this.tasks.get(taskId)
566
- if (!current?.serverSync || !current.session) return
567
- try {
568
- const events = await fetchSessionMessages(current.serverSync.sessionId)
569
- current.serverSync.lastPollError = undefined
570
- const startIndex = current.serverSync.lastEventIndex
571
- const newEvents = events.slice(startIndex)
572
- current.serverSync.lastEventIndex = events.length
573
-
574
- for (const ev of newEvents) {
575
- if (ev.type !== 'message') continue
576
- if (ev.message.role !== 'user') continue
577
- if (ev.message.id && current.serverSync.seenMessageIds.has(ev.message.id)) {
578
- continue
579
- }
580
- if (ev.message.id) {
581
- current.serverSync.seenMessageIds.add(ev.message.id)
582
- }
583
- const text = extractTextFromMessage(ev.message).trim()
584
- if (text === '') continue
585
- current.session.emit({
586
- type: 'message',
587
- content: [{ type: 'text', text }],
588
- parentUuid: current.serverSync.lastAssistantMessageId
589
- })
590
- }
591
- } catch (error) {
592
- const message = `Sync poll failed: ${error instanceof Error ? error.message : String(error)}`
593
- if (current.serverSync.lastPollError !== message) {
594
- current.serverSync.lastPollError = message
595
- appendTaskLog(current, message)
596
- }
597
- }
598
- }
599
-
600
- task.serverSync.poller = setInterval(() => {
601
- void poll()
602
- }, 1000)
603
- void poll()
604
- }
605
-
606
- private stopServerPolling(taskId: string) {
607
- const task = this.tasks.get(taskId)
608
- if (task?.serverSync?.poller) {
609
- clearInterval(task.serverSync.poller)
610
- task.serverSync.poller = undefined
611
- }
612
- }
613
-
614
- private async syncEvent(task: TaskInfo, event: AdapterOutputEvent) {
615
- if (!task.serverSync) return
616
- try {
617
- await postSessionEvent(task.serverSync.sessionId, event as unknown as Record<string, unknown>)
618
- } catch (err) {
619
- appendTaskLog(task, `Sync event failed: ${err instanceof Error ? err.message : String(err)}`)
620
- }
621
- }
622
-
623
- private async syncSyntheticInteraction(task: TaskInfo) {
624
- if (task.serverSync == null || task.pendingInteraction == null) return
625
- try {
626
- await postSessionEvent(task.serverSync.sessionId, {
627
- type: 'interaction_request',
628
- id: task.pendingInteraction.id,
629
- payload: task.pendingInteraction.payload
630
- })
631
- } catch (error) {
632
- appendTaskLog(task, `Sync interaction request failed: ${error instanceof Error ? error.message : String(error)}`)
633
- }
634
- }
635
-
636
- public getTask(taskId: string): TaskInfo | undefined {
637
- return this.tasks.get(taskId)
638
- }
639
-
640
- public getAllTasks(): TaskInfo[] {
641
- return Array.from(this.tasks.values())
642
- }
643
-
644
- private async syncTaskUserMessage(task: TaskInfo, message: string) {
645
- if (task.serverSync == null) {
646
- return
647
- }
648
-
649
- const event = createTaskUserMessageEvent(task.taskId, message)
650
- await postSessionEvent(task.serverSync.sessionId, event)
651
- task.serverSync.seenMessageIds.add(event.data.id)
652
- }
653
-
654
- private emitTaskUserMessage(task: TaskInfo, session: ManagedTaskSession, message: string) {
655
- session.emit(createTaskSessionMessageEvent(message, task.serverSync?.lastAssistantMessageId))
656
- }
657
-
658
- private async resumeTaskWithMessage(task: TaskInfo, message: string, logMessage: string) {
659
- await this.syncTaskUserMessage(task, message)
660
- task.status = 'running'
661
- appendTaskLog(task, logMessage)
662
- await this.launchTask(task, 'resume', message)
663
- }
664
-
665
- public async sendTaskMessage(params: {
666
- taskId: string
667
- message: string
668
- mode?: 'direct' | 'steer'
669
- }): Promise<void> {
670
- const task = this.tasks.get(params.taskId)
671
- if (task == null) {
672
- throw new Error(`Task ${params.taskId} not found.`)
673
- }
674
-
675
- const message = params.message.trim()
676
- const mode = params.mode ?? 'direct'
677
- if (message === '') {
678
- throw new Error('Task message cannot be empty.')
679
- }
680
-
681
- if (task.pendingInteraction != null || task.status === 'waiting_input') {
682
- throw new Error(`Task ${params.taskId} is waiting for input. Use SubmitTaskInput instead.`)
683
- }
684
-
685
- if (mode === 'steer') {
686
- if (task.status === 'completed' || task.status === 'failed') {
687
- await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
688
- return
689
- }
690
- task.queuedSteerMessages.push(message)
691
- appendTaskLog(task, `Queued task message (${mode}): ${message}`)
692
- return
693
- }
694
-
695
- if (task.status === 'completed' || task.status === 'failed') {
696
- await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
697
- return
698
- }
699
-
700
- if (task.status !== 'running' || task.session == null) {
701
- throw new Error(`Task ${params.taskId} is not running. Start a new task instead.`)
702
- }
703
-
704
- await this.syncTaskUserMessage(task, message)
705
- this.emitTaskUserMessage(task, task.session, message)
706
-
707
- appendTaskLog(task, `User message submitted (${mode}): ${message}`)
708
- }
709
-
710
- private async dispatchQueuedSteerMessage(task: TaskInfo) {
711
- const nextMessage = task.queuedSteerMessages.shift()
712
- if (nextMessage == null || nextMessage.trim() === '') {
713
- return false
714
- }
715
-
716
- await this.resumeTaskWithMessage(task, nextMessage, `Resuming task from steer queue: ${nextMessage}`)
717
- return true
718
- }
719
-
720
- public async submitTaskInput(params: {
721
- taskId: string
722
- interactionId?: string
723
- data: string | string[]
724
- }): Promise<void> {
725
- const task = this.tasks.get(params.taskId)
726
- if (task == null) {
727
- throw new Error(`Task ${params.taskId} not found.`)
728
- }
729
-
730
- const pendingInteraction = task.pendingInteraction
731
- if (pendingInteraction == null) {
732
- throw new Error(`Task ${params.taskId} does not have a pending interaction.`)
733
- }
734
-
735
- const interactionId = params.interactionId ?? pendingInteraction.id
736
- if (interactionId !== pendingInteraction.id) {
737
- throw new Error(`Interaction ${interactionId} is not pending for task ${params.taskId}.`)
738
- }
739
-
740
- if (pendingInteraction.source === 'adapter') {
741
- if (task.session?.respondInteraction == null) {
742
- throw new Error(`Task ${params.taskId} does not support interaction responses.`)
743
- }
744
- await task.session.respondInteraction(interactionId, params.data)
745
- await this.syncTaskInputResponse(task, interactionId, params.data)
746
- task.pendingInteraction = undefined
747
- task.status = 'running'
748
- const responseText = Array.isArray(params.data) ? params.data.join(', ') : params.data
749
- appendTaskLog(task, `Interaction response submitted: ${responseText}`)
750
- return
751
- }
752
-
753
- const decision = resolvePermissionInteractionDecision(params.data)
754
- if (decision == null) {
755
- throw new Error(`Task ${params.taskId} requires a permission decision response.`)
756
- }
757
-
758
- await this.syncTaskInputResponse(task, interactionId, params.data)
759
- if (decision === PERMISSION_DECISION_CANCEL) {
760
- task.pendingInteraction = undefined
761
- task.status = 'failed'
762
- appendTaskLog(task, 'Permission recovery cancelled. Task will not continue.')
763
- task.onStop?.()
764
- return
765
- }
766
-
767
- task.permissionState = await applyTaskPermissionDecision({
768
- cwd: process.cwd(),
769
- sessionId: task.taskId,
770
- adapter: task.adapter,
771
- permissionState: task.permissionState,
772
- subjectKeys: pendingInteraction.subjectKeys ?? [],
773
- action: decision as PermissionInteractionDecision
774
- })
775
-
776
- if (
777
- decision === 'deny_once' ||
778
- decision === 'deny_session' ||
779
- decision === 'deny_project'
780
- ) {
781
- task.pendingInteraction = undefined
782
- task.status = 'failed'
783
- appendTaskLog(task, `Permission decision applied: ${decision}. Task will not continue.`)
784
- task.onStop?.()
785
- return
786
- }
787
-
788
- task.pendingInteraction = undefined
789
- task.status = 'running'
790
- appendTaskLog(task, `Permission decision applied: ${decision}. Restarting task.`)
791
- await this.launchTask(task, 'resume')
792
- }
793
-
794
- public async respondToTaskInteraction(params: {
795
- taskId: string
796
- interactionId?: string
797
- data: string | string[]
798
- }): Promise<void> {
799
- await this.submitTaskInput(params)
800
- }
801
-
802
- private async syncTaskInputResponse(task: TaskInfo, interactionId: string, data: string | string[]) {
803
- if (!task.serverSync) return
804
- try {
805
- await postSessionEvent(task.serverSync.sessionId, {
806
- type: 'interaction_response',
807
- id: interactionId,
808
- data
809
- })
810
- } catch (error) {
811
- appendTaskLog(
812
- task,
813
- `Sync interaction response failed: ${error instanceof Error ? error.message : String(error)}`
814
- )
815
- }
816
- }
817
-
818
- private async syncStoppedTask(params: {
819
- task: TaskInfo
820
- pendingInteraction?: PendingTaskInteraction
821
- }) {
822
- const { task, pendingInteraction } = params
823
- if (!task.serverSync) return
824
-
825
- if (pendingInteraction != null) {
826
- try {
827
- await postSessionEvent(task.serverSync.sessionId, {
828
- type: 'interaction_response',
829
- id: pendingInteraction.id,
830
- data: PERMISSION_DECISION_CANCEL
831
- })
832
- } catch (error) {
833
- appendTaskLog(
834
- task,
835
- `Sync interaction cancellation failed: ${error instanceof Error ? error.message : String(error)}`
836
- )
837
- }
838
- }
839
-
840
- try {
841
- await postSessionEvent(task.serverSync.sessionId, {
842
- type: 'error',
843
- data: {
844
- message: 'Task stopped by user',
845
- fatal: true
846
- }
847
- })
848
- } catch (error) {
849
- appendTaskLog(
850
- task,
851
- `Sync stop event failed: ${error instanceof Error ? error.message : String(error)}`
852
- )
853
- }
854
- }
855
-
856
- public stopTask(taskId: string): boolean {
857
- const task = this.tasks.get(taskId)
858
- if (task && (task.session != null || task.pendingInteraction != null || task.status === 'waiting_input')) {
859
- const pendingInteraction = task.pendingInteraction
860
- task.session?.kill()
861
- task.session = undefined
862
- task.pendingInteraction = undefined
863
- appendTaskLog(task, 'Task stopped by user')
864
- task.status = 'failed' // or 'stopped' if we had that status
865
- this.stopServerPolling(taskId)
866
- void this.syncStoppedTask({
867
- task,
868
- pendingInteraction
869
- })
870
- if (task.onStop) task.onStop()
871
- return true
872
- }
873
- return false
874
- }
875
- }