@vibe-forge/mcp 2.0.2-alpha.1 → 3.0.0-alpha.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.
@@ -156,6 +156,47 @@ describe('taskManager fatal error scenarios', () => {
156
156
  )
157
157
  })
158
158
 
159
+ it('forwards task model overrides into query resolution and runtime startup', async () => {
160
+ const { TaskManager } = await import('#~/tools/task/manager.js')
161
+
162
+ mocks.run.mockResolvedValueOnce({
163
+ session: {
164
+ emit: vi.fn(),
165
+ kill: vi.fn()
166
+ }
167
+ })
168
+
169
+ const managedTaskManager = new TaskManager()
170
+ await managedTaskManager.startTask({
171
+ taskId: 'task-model-override',
172
+ description: 'trigger',
173
+ adapter: 'codex',
174
+ model: 'openai,gpt-5.4-mini'
175
+ })
176
+
177
+ expect(mocks.generateAdapterQueryOptions).toHaveBeenCalledWith(
178
+ undefined,
179
+ undefined,
180
+ expect.any(String),
181
+ expect.objectContaining({
182
+ adapter: 'codex',
183
+ model: 'openai,gpt-5.4-mini'
184
+ })
185
+ )
186
+ expect(mocks.run).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ adapter: 'codex'
189
+ }),
190
+ expect.objectContaining({
191
+ sessionId: 'task-model-override',
192
+ model: 'openai,gpt-5.4-mini'
193
+ })
194
+ )
195
+ expect(managedTaskManager.getTask('task-model-override')).toEqual(expect.objectContaining({
196
+ model: 'openai,gpt-5.4-mini'
197
+ }))
198
+ })
199
+
159
200
  it('responds to pending interactions and syncs the response', async () => {
160
201
  const { TaskManager } = await import('#~/tools/task/manager.js')
161
202
  const respondInteraction = vi.fn()
@@ -265,7 +306,13 @@ describe('taskManager fatal error scenarios', () => {
265
306
  })
266
307
 
267
308
  const task = managedTaskManager.getTask('task-send-synced')
268
- expect(emit).toHaveBeenCalledTimes(1)
309
+ expect(emit).toHaveBeenNthCalledWith(2, {
310
+ type: 'message',
311
+ content: [{
312
+ type: 'text',
313
+ text: 'keep going'
314
+ }]
315
+ })
269
316
  expect(mocks.postSessionEvent).toHaveBeenCalledWith('task-send-synced', {
270
317
  type: 'message',
271
318
  data: expect.objectContaining({
@@ -276,6 +323,84 @@ describe('taskManager fatal error scenarios', () => {
276
323
  expect(task?.logs).toContain('User message submitted (direct): keep going')
277
324
  })
278
325
 
326
+ it('does not replay stale synced messages after a failed resume attempt', async () => {
327
+ const { TaskManager } = await import('#~/tools/task/manager.js')
328
+ let onEvent: ((event: any) => void) | undefined
329
+ const resumedEmit = vi.fn()
330
+ const syncedEvents: Array<{ type: 'message'; message: Record<string, unknown> }> = []
331
+
332
+ mocks.postSessionEvent.mockImplementation(async (_sessionId: string, payload: any) => {
333
+ if (payload?.type === 'message' && payload.data != null) {
334
+ syncedEvents.push({
335
+ type: 'message',
336
+ message: payload.data
337
+ })
338
+ }
339
+ })
340
+ mocks.fetchSessionMessages.mockImplementation(async () => syncedEvents)
341
+
342
+ mocks.run
343
+ .mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
344
+ onEvent = adapterOptions.onEvent
345
+ return {
346
+ session: {
347
+ emit: vi.fn(),
348
+ kill: vi.fn()
349
+ }
350
+ }
351
+ })
352
+ .mockRejectedValueOnce(new Error('resume failed to start'))
353
+ .mockResolvedValueOnce({
354
+ session: {
355
+ emit: resumedEmit,
356
+ kill: vi.fn()
357
+ }
358
+ })
359
+
360
+ const managedTaskManager = new TaskManager()
361
+ await managedTaskManager.startTask({
362
+ taskId: 'task-send-synced-retry',
363
+ description: 'trigger',
364
+ enableServerSync: true
365
+ })
366
+
367
+ onEvent?.({
368
+ type: 'stop',
369
+ data: undefined
370
+ })
371
+
372
+ await expect(managedTaskManager.sendTaskMessage({
373
+ taskId: 'task-send-synced-retry',
374
+ message: 'first retry'
375
+ })).rejects.toThrow('resume failed to start')
376
+
377
+ const firstMessage = syncedEvents.at(-1)?.message
378
+ const taskAfterFailure = managedTaskManager.getTask('task-send-synced-retry')
379
+ expect(firstMessage).toEqual(expect.objectContaining({
380
+ id: expect.any(String),
381
+ role: 'user',
382
+ content: 'first retry'
383
+ }))
384
+ expect(taskAfterFailure?.serverSync?.seenMessageIds.has(String(firstMessage?.id))).toBe(true)
385
+
386
+ await managedTaskManager.sendTaskMessage({
387
+ taskId: 'task-send-synced-retry',
388
+ message: 'second retry'
389
+ })
390
+ await new Promise(resolve => setTimeout(resolve, 0))
391
+
392
+ expect(resumedEmit).toHaveBeenCalledTimes(1)
393
+ expect(resumedEmit).toHaveBeenCalledWith({
394
+ type: 'message',
395
+ content: [{
396
+ type: 'text',
397
+ text: 'second retry'
398
+ }]
399
+ })
400
+
401
+ managedTaskManager.stopTask('task-send-synced-retry')
402
+ })
403
+
279
404
  it('queues steer follow-up messages and resumes the same task after natural completion', async () => {
280
405
  const { TaskManager } = await import('#~/tools/task/manager.js')
281
406
  let onEvent: ((event: any) => void) | undefined
@@ -410,23 +535,81 @@ describe('taskManager fatal error scenarios', () => {
410
535
  })).rejects.toThrow('Task task-send-steer-blocked is waiting for input. Use SubmitTaskInput instead.')
411
536
  })
412
537
 
413
- it('rejects steer follow-up messages after a task has already completed', async () => {
538
+ it('resumes completed tasks when sending a direct follow-up message', async () => {
414
539
  const { TaskManager } = await import('#~/tools/task/manager.js')
415
540
  let onEvent: ((event: any) => void) | undefined
541
+ const resumedEmit = vi.fn()
416
542
 
417
- mocks.run.mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
418
- onEvent = adapterOptions.onEvent
419
- return {
543
+ mocks.run
544
+ .mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
545
+ onEvent = adapterOptions.onEvent
546
+ return {
547
+ session: {
548
+ emit: vi.fn(),
549
+ kill: vi.fn()
550
+ }
551
+ }
552
+ })
553
+ .mockResolvedValueOnce({
420
554
  session: {
421
- emit: vi.fn(),
555
+ emit: resumedEmit,
422
556
  kill: vi.fn()
423
557
  }
424
- }
558
+ })
559
+
560
+ const managedTaskManager = new TaskManager()
561
+ await managedTaskManager.startTask({
562
+ taskId: 'task-send-completed-direct',
563
+ description: 'trigger'
425
564
  })
426
565
 
566
+ onEvent?.({
567
+ type: 'stop',
568
+ data: undefined
569
+ })
570
+
571
+ await managedTaskManager.sendTaskMessage({
572
+ taskId: 'task-send-completed-direct',
573
+ message: 'continue from the final summary'
574
+ })
575
+
576
+ const task = managedTaskManager.getTask('task-send-completed-direct')
577
+ expect(mocks.run).toHaveBeenCalledTimes(2)
578
+ expect(resumedEmit).toHaveBeenCalledWith({
579
+ type: 'message',
580
+ content: [{
581
+ type: 'text',
582
+ text: 'continue from the final summary'
583
+ }]
584
+ })
585
+ expect(task?.logs).toContain('Resuming inactive task (direct): continue from the final summary')
586
+ })
587
+
588
+ it('resumes completed tasks when sending a steer follow-up message', async () => {
589
+ const { TaskManager } = await import('#~/tools/task/manager.js')
590
+ let onEvent: ((event: any) => void) | undefined
591
+ const resumedEmit = vi.fn()
592
+
593
+ mocks.run
594
+ .mockImplementationOnce(async (_options: unknown, adapterOptions: any) => {
595
+ onEvent = adapterOptions.onEvent
596
+ return {
597
+ session: {
598
+ emit: vi.fn(),
599
+ kill: vi.fn()
600
+ }
601
+ }
602
+ })
603
+ .mockResolvedValueOnce({
604
+ session: {
605
+ emit: resumedEmit,
606
+ kill: vi.fn()
607
+ }
608
+ })
609
+
427
610
  const managedTaskManager = new TaskManager()
428
611
  await managedTaskManager.startTask({
429
- taskId: 'task-send-completed',
612
+ taskId: 'task-send-completed-steer',
430
613
  description: 'trigger'
431
614
  })
432
615
 
@@ -435,11 +618,22 @@ describe('taskManager fatal error scenarios', () => {
435
618
  data: undefined
436
619
  })
437
620
 
438
- await expect(managedTaskManager.sendTaskMessage({
439
- taskId: 'task-send-completed',
621
+ await managedTaskManager.sendTaskMessage({
622
+ taskId: 'task-send-completed-steer',
440
623
  message: 'queue this for later',
441
624
  mode: 'steer'
442
- })).rejects.toThrow('Task task-send-completed is not active. Start a new task instead.')
625
+ })
626
+
627
+ const task = managedTaskManager.getTask('task-send-completed-steer')
628
+ expect(mocks.run).toHaveBeenCalledTimes(2)
629
+ expect(resumedEmit).toHaveBeenCalledWith({
630
+ type: 'message',
631
+ content: [{
632
+ type: 'text',
633
+ text: 'queue this for later'
634
+ }]
635
+ })
636
+ expect(task?.logs).toContain('Resuming inactive task (steer): queue this for later')
443
637
  })
444
638
 
445
639
  it('builds synthetic permission recovery for claude-code and resumes after SubmitTaskInput', async () => {
@@ -119,6 +119,35 @@ describe('task tool integration', () => {
119
119
  }))
120
120
  })
121
121
 
122
+ it('passes explicit task model overrides to the hook and task manager', async () => {
123
+ const { createTaskRegister } = await import('#~/tools/task/index.js')
124
+
125
+ const tester = createToolTester()
126
+ createTaskRegister()(tester.mockRegister)
127
+
128
+ await tester.callTool('StartTasks', {
129
+ tasks: [{
130
+ description: 'investigate flaky tests',
131
+ type: 'default',
132
+ model: 'gpt-5.4-mini'
133
+ }]
134
+ })
135
+
136
+ expect(mocks.callHook).toHaveBeenCalledWith(
137
+ 'StartTasks',
138
+ expect.objectContaining({
139
+ tasks: [expect.objectContaining({
140
+ taskId: 'task-1',
141
+ model: 'gpt-5.4-mini'
142
+ })]
143
+ })
144
+ )
145
+ expect(mocks.startTask).toHaveBeenCalledWith(expect.objectContaining({
146
+ taskId: 'task-1',
147
+ model: 'gpt-5.4-mini'
148
+ }))
149
+ })
150
+
122
151
  it('inherits the parent permission mode when the task does not specify one', async () => {
123
152
  process.env.__VF_PROJECT_AI_PERMISSION_MODE__ = 'dontAsk'
124
153
  mocks.getParentSessionId.mockReturnValue('parent-session')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-forge/mcp",
3
- "version": "2.0.2-alpha.1",
3
+ "version": "3.0.0-alpha.0",
4
4
  "description": "Vibe Forge MCP server",
5
5
  "imports": {
6
6
  "#~/*.js": {
@@ -31,15 +31,15 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "^1.25.3",
34
- "@vibe-forge/config": "^2.0.2",
35
34
  "commander": "^12.1.0",
36
35
  "zod": "^3.24.1",
37
- "@vibe-forge/hooks": "2.0.2-alpha.0",
38
- "@vibe-forge/cli-helper": "^2.0.0",
39
- "@vibe-forge/task": "2.0.1-alpha.3",
40
- "@vibe-forge/register": "^2.0.0",
41
- "@vibe-forge/types": "^2.0.2",
42
- "@vibe-forge/utils": "2.0.4-alpha.1"
36
+ "@vibe-forge/hooks": "3.0.0-alpha.0",
37
+ "@vibe-forge/types": "3.0.0-alpha.0",
38
+ "@vibe-forge/utils": "3.0.0-alpha.0",
39
+ "@vibe-forge/register": "3.0.0-alpha.0",
40
+ "@vibe-forge/cli-helper": "3.0.0-alpha.0",
41
+ "@vibe-forge/config": "3.0.0-alpha.0",
42
+ "@vibe-forge/task": "3.0.0-alpha.0"
43
43
  },
44
44
  "scripts": {
45
45
  "test": "pnpm -C ../.. exec vitest run --workspace vitest.workspace.ts --project bundler packages/mcp/__tests__"
@@ -12,6 +12,7 @@ import {
12
12
  SESSION_PERMISSION_MODES,
13
13
  START_TASKS_DESCRIPTION,
14
14
  TASK_BACKGROUND_DESCRIPTION,
15
+ TASK_MODEL_DESCRIPTION,
15
16
  TASK_PERMISSION_MODE_DESCRIPTION,
16
17
  resolveInheritedPermissionMode,
17
18
  serializeTaskInfo
@@ -50,6 +51,10 @@ export const createTaskRegister = () => {
50
51
  .string()
51
52
  .describe('The adapter to use for the task (e.g. claude-code)')
52
53
  .optional(),
54
+ model: z
55
+ .string()
56
+ .describe(TASK_MODEL_DESCRIPTION)
57
+ .optional(),
53
58
  permissionMode: z
54
59
  .enum(SESSION_PERMISSION_MODES)
55
60
  .describe(TASK_PERMISSION_MODE_DESCRIPTION)
@@ -51,6 +51,7 @@ type ManagedTaskSession = McpTaskSession & {
51
51
  export interface TaskInfo {
52
52
  taskId: string
53
53
  adapter?: string
54
+ model?: string
54
55
  description: string
55
56
  type?: 'default' | 'spec' | 'entity' | 'workspace'
56
57
  name?: string
@@ -143,6 +144,25 @@ const formatPermissionErrorLog = (error: AdapterErrorData) => {
143
144
  return parts.length > 0 ? parts.join(' | ') : undefined
144
145
  }
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
+
146
166
  export class TaskManager {
147
167
  private tasks: Map<string, TaskInfo> = new Map()
148
168
  private permissionToolUseCache = new Map<string, Map<string, string>>()
@@ -236,15 +256,27 @@ export class TaskManager {
236
256
  name?: string
237
257
  permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
238
258
  adapter?: string
259
+ model?: string
239
260
  background?: boolean
240
261
  enableServerSync?: boolean
241
262
  }): Promise<{ taskId: string; logs?: string[] }> {
242
- const { taskId, adapter, description, type, name, permissionMode, background = true, enableServerSync } = options
263
+ const {
264
+ taskId,
265
+ adapter,
266
+ model,
267
+ description,
268
+ type,
269
+ name,
270
+ permissionMode,
271
+ background = true,
272
+ enableServerSync
273
+ } = options
243
274
 
244
275
  // Initialize Task Info
245
276
  const taskInfo: TaskInfo = {
246
277
  taskId,
247
278
  adapter,
279
+ model,
248
280
  description,
249
281
  type,
250
282
  name,
@@ -287,7 +319,11 @@ export class TaskManager {
287
319
  return { taskId }
288
320
  }
289
321
 
290
- private async launchTask(task: TaskInfo, runType: 'create' | 'resume', resumeMessage?: string) {
322
+ private async launchTask(
323
+ task: TaskInfo,
324
+ runType: 'create' | 'resume',
325
+ resumeMessage?: string
326
+ ) {
291
327
  try {
292
328
  const rootCwd = process.cwd()
293
329
  const promptType = task.type !== 'default' ? task.type : undefined
@@ -298,7 +334,8 @@ export class TaskManager {
298
334
  promptName,
299
335
  promptCWD,
300
336
  {
301
- adapter: task.adapter
337
+ adapter: task.adapter,
338
+ model: task.model
302
339
  }
303
340
  )
304
341
  const taskCwd = resolvedConfig.workspace?.cwd ?? promptCWD
@@ -331,6 +368,7 @@ export class TaskManager {
331
368
  runtime: 'mcp',
332
369
  mode: 'stream',
333
370
  sessionId: task.taskId,
371
+ model: task.model,
334
372
  systemPrompt: mergeSystemPrompts({
335
373
  generatedSystemPrompt: resolvedConfig.systemPrompt,
336
374
  injectDefaultSystemPrompt
@@ -372,13 +410,11 @@ export class TaskManager {
372
410
  )
373
411
  }
374
412
  this.startServerPolling(task.taskId)
375
- session.emit({
376
- type: 'message',
377
- content: [{
378
- type: 'text',
379
- text: resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
380
- }]
381
- })
413
+ this.emitTaskUserMessage(
414
+ current,
415
+ current.session,
416
+ resumeMessage ?? (runType === 'resume' ? TASK_PERMISSION_CONTINUE_PROMPT : current.description)
417
+ )
382
418
  } catch (err) {
383
419
  const current = this.tasks.get(task.taskId)
384
420
  if (current) {
@@ -605,6 +641,27 @@ export class TaskManager {
605
641
  return Array.from(this.tasks.values())
606
642
  }
607
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
+
608
665
  public async sendTaskMessage(params: {
609
666
  taskId: string
610
667
  message: string
@@ -627,36 +684,25 @@ export class TaskManager {
627
684
 
628
685
  if (mode === 'steer') {
629
686
  if (task.status === 'completed' || task.status === 'failed') {
630
- throw new Error(`Task ${params.taskId} is not active. Start a new task instead.`)
687
+ await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
688
+ return
631
689
  }
632
690
  task.queuedSteerMessages.push(message)
633
691
  appendTaskLog(task, `Queued task message (${mode}): ${message}`)
634
692
  return
635
693
  }
636
694
 
695
+ if (task.status === 'completed' || task.status === 'failed') {
696
+ await this.resumeTaskWithMessage(task, message, `Resuming inactive task (${mode}): ${message}`)
697
+ return
698
+ }
699
+
637
700
  if (task.status !== 'running' || task.session == null) {
638
701
  throw new Error(`Task ${params.taskId} is not running. Start a new task instead.`)
639
702
  }
640
703
 
641
- if (task.serverSync != null) {
642
- await postSessionEvent(task.serverSync.sessionId, {
643
- type: 'message',
644
- data: {
645
- id: `task-user:${task.taskId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
646
- role: 'user',
647
- content: message,
648
- createdAt: Date.now()
649
- }
650
- })
651
- } else {
652
- task.session.emit({
653
- type: 'message',
654
- content: [{
655
- type: 'text',
656
- text: message
657
- }]
658
- })
659
- }
704
+ await this.syncTaskUserMessage(task, message)
705
+ this.emitTaskUserMessage(task, task.session, message)
660
706
 
661
707
  appendTaskLog(task, `User message submitted (${mode}): ${message}`)
662
708
  }
@@ -667,9 +713,7 @@ export class TaskManager {
667
713
  return false
668
714
  }
669
715
 
670
- task.status = 'running'
671
- appendTaskLog(task, `Resuming task from steer queue: ${nextMessage}`)
672
- await this.launchTask(task, 'resume', nextMessage)
716
+ await this.resumeTaskWithMessage(task, nextMessage, `Resuming task from steer queue: ${nextMessage}`)
673
717
  return true
674
718
  }
675
719
 
@@ -10,13 +10,13 @@ export const DEFAULT_TASK_LOG_LIMIT = 10
10
10
  export type TaskLogsOrder = typeof TASK_LOG_ORDERS[number]
11
11
 
12
12
  export const START_TASKS_DESCRIPTION =
13
- 'Start multiple tasks in background or foreground. Use type "workspace" plus name to run in a configured workspace. If a task stalls, fails, or asks for permission/input, call GetTaskInfo. GetTaskInfo returns the 10 most recent logs by default in descending order, so newer log lines appear earlier in the logs array. If you need to add another instruction to a task that is still running, use SendTaskMessage instead of starting a replacement task. If GetTaskInfo returns pendingInput or pendingInteraction, resolve it with SubmitTaskInput. If logs show permission_required, you can answer the recovery prompt with SubmitTaskInput instead of restarting manually.'
13
+ 'Start multiple tasks in background or foreground. Use type "workspace" plus name to run in a configured workspace. If a task stalls, fails, or asks for permission/input, call GetTaskInfo. GetTaskInfo returns the 10 most recent logs by default in descending order, so newer log lines appear earlier in the logs array. If you need to add another instruction to a task, use SendTaskMessage: running tasks continue immediately, while completed or failed tasks resume the same conversation instead of forcing a replacement task. If GetTaskInfo returns pendingInput or pendingInteraction, resolve it with SubmitTaskInput. If logs show permission_required, you can answer the recovery prompt with SubmitTaskInput instead of restarting manually.'
14
14
 
15
15
  export const GET_TASK_INFO_DESCRIPTION =
16
- 'Get the detailed status, logs, pendingInput, pendingInteraction, lastError, and guidance for a task. By default this returns the 10 most recent logs in descending order, so newer log lines appear earlier in the logs array. Use logLimit to inspect a different number of recent logs, and set logOrder to "asc" when you want the selected log window in oldest-to-newest order. If the task is still active and you need to continue it, call SendTaskMessage with mode "direct" or "steer" depending on timing. If pendingInput is present, answer it with SubmitTaskInput.'
16
+ 'Get the detailed status, logs, pendingInput, pendingInteraction, lastError, and guidance for a task. By default this returns the 10 most recent logs in descending order, so newer log lines appear earlier in the logs array. Use logLimit to inspect a different number of recent logs, and set logOrder to "asc" when you want the selected log window in oldest-to-newest order. If you need to continue the task, call SendTaskMessage with mode "direct" or "steer": active tasks continue immediately, while completed or failed tasks resume the same conversation. If pendingInput is present, answer it with SubmitTaskInput.'
17
17
 
18
18
  export const SEND_TASK_MESSAGE_DESCRIPTION =
19
- 'Send a follow-up user message to a managed task. Use mode "direct" (default) to continue the current running task immediately. Use mode "steer" to queue a follow-up that should run after the current task finishes naturally. Do not use this to answer pendingInput or pendingInteraction; use SubmitTaskInput for that. If the task already completed or failed, start a new task instead.'
19
+ 'Send a follow-up user message to a managed task. Use mode "direct" (default) to continue the current running task immediately. Use mode "steer" to queue a follow-up that should run after the current task finishes naturally. If the task already completed or failed and you still want to keep working in that same conversation, SendTaskMessage resumes the same task session instead of starting a replacement task. Do not use this to answer pendingInput or pendingInteraction; use SubmitTaskInput for that.'
20
20
 
21
21
  export const SUBMIT_TASK_INPUT_DESCRIPTION =
22
22
  'Submit input for a task that is blocked waiting for permission or user input. First call GetTaskInfo or ListTasks, then use taskId plus one of pendingInput.payload.options[].value when available. Do not use this for ordinary follow-up instructions to a running task or queued steer messages; use SendTaskMessage instead. Common permission answers are allow_once, allow_session, allow_project, deny_once, deny_session, or deny_project.'
@@ -33,6 +33,9 @@ export const LIST_TASKS_DESCRIPTION =
33
33
  export const TASK_PERMISSION_MODE_DESCRIPTION =
34
34
  'Permission mode for the task. If omitted, inherits the parent session. Raise it only when the task is blocked by permission errors.'
35
35
 
36
+ export const TASK_MODEL_DESCRIPTION =
37
+ 'Model override for the task. Uses the same model selector format as a normal session model setting.'
38
+
36
39
  export const TASK_BACKGROUND_DESCRIPTION =
37
40
  'Whether to run in background (default: true). If false, waits until the task completes, fails, or becomes blocked waiting for input, then returns the current logs.'
38
41
 
@@ -75,7 +78,7 @@ const buildTaskGuidance = (task: {
75
78
  }
76
79
 
77
80
  if (task.status === 'failed' && hints.length === 0) {
78
- hints.push('Task failed. Inspect logs and lastError, then restart the task if needed.')
81
+ hints.push('Task failed. Inspect logs and lastError, then use SendTaskMessage to resume it or StartTasks to replace it if needed.')
79
82
  }
80
83
 
81
84
  return hints