@keyou007/agent-bot 0.1.10 → 0.1.12

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.
Files changed (121) hide show
  1. package/.env.example +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +55 -43
  4. package/README.zh.md +55 -43
  5. package/assets/agent-bot-logo.png +0 -0
  6. package/config.example.yaml +11 -1
  7. package/dist/acp/AcpSessionManager.js +78 -46
  8. package/dist/acp/AcpSessionManager.js.map +1 -1
  9. package/dist/cli/FeishuAppConfiguration.js +1 -1
  10. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  11. package/dist/cli/FeishuAppRegistration.js +2 -2
  12. package/dist/cli/FeishuAppRegistration.js.map +1 -1
  13. package/dist/cli/Initializer.js +1 -1
  14. package/dist/cli/Initializer.js.map +1 -1
  15. package/dist/cli/ServerStarter.js +7 -2
  16. package/dist/cli/ServerStarter.js.map +1 -1
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +20 -16
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +95 -0
  21. package/dist/cli/taskGroupOptions.js.map +1 -0
  22. package/dist/cli.js +74 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/codex/AppServerConnection.js +15 -1
  25. package/dist/codex/AppServerConnection.js.map +1 -1
  26. package/dist/codex/CodexLocalActivityDetector.js +4 -1
  27. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  28. package/dist/codex/CodexProcessManager.js +1 -0
  29. package/dist/codex/CodexProcessManager.js.map +1 -1
  30. package/dist/codex/CodexRuntime.js +112 -11
  31. package/dist/codex/CodexRuntime.js.map +1 -1
  32. package/dist/commands/CommandRouter.js +64 -49
  33. package/dist/commands/CommandRouter.js.map +1 -1
  34. package/dist/config/loadConfig.js +11 -1
  35. package/dist/config/loadConfig.js.map +1 -1
  36. package/dist/config/schema.js +6 -1
  37. package/dist/config/schema.js.map +1 -1
  38. package/dist/console/ConsoleConnector.js +1 -1
  39. package/dist/console/ConsoleConnector.js.map +1 -1
  40. package/dist/console/ConsoleTurnPresenter.js +15 -8
  41. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  42. package/dist/deprecated-cli.js +5 -0
  43. package/dist/deprecated-cli.js.map +1 -0
  44. package/dist/feishu/CardRenderer.js +350 -15
  45. package/dist/feishu/CardRenderer.js.map +1 -1
  46. package/dist/feishu/FeishuMessageClient.js +51 -5
  47. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  48. package/dist/feishu/FeishuTurnPresenter.js +17 -8
  49. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  50. package/dist/feishu/transport.js +1 -1
  51. package/dist/feishu/transport.js.map +1 -1
  52. package/dist/index.js +38 -19
  53. package/dist/index.js.map +1 -1
  54. package/dist/presentation/OutboundRouter.js +2 -2
  55. package/dist/presentation/OutboundRouter.js.map +1 -1
  56. package/dist/presentation/TurnStateReducer.js +2 -1
  57. package/dist/presentation/TurnStateReducer.js.map +1 -1
  58. package/dist/proxy/ProxySessionController.js +665 -225
  59. package/dist/proxy/ProxySessionController.js.map +1 -1
  60. package/dist/runtime/AcpRuntimeAdapter.js +1 -0
  61. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  62. package/dist/runtime/AgentRuntimeRegistry.js +7 -4
  63. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
  64. package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
  65. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
  66. package/dist/startup/SessionMetadataHydrator.js +2 -2
  67. package/dist/startup/SessionMetadataHydrator.js.map +1 -1
  68. package/dist/startup/StartupNotifier.js +1 -0
  69. package/dist/startup/StartupNotifier.js.map +1 -1
  70. package/dist/state/StateStore.js +21 -15
  71. package/dist/state/StateStore.js.map +1 -1
  72. package/dist/supervision/SafeRestartScheduler.js +5 -0
  73. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  74. package/dist/supervision/systemEnvironment.js +129 -0
  75. package/dist/supervision/systemEnvironment.js.map +1 -0
  76. package/dist/supervisor.js +13 -1
  77. package/dist/supervisor.js.map +1 -1
  78. package/docs/technical-reference.md +35 -21
  79. package/docs/technical-reference.zh.md +35 -21
  80. package/npm-shrinkwrap.json +4 -3
  81. package/package.json +4 -2
  82. package/skills/agent-bot/SKILL.md +73 -30
  83. package/src/acp/AcpSessionManager.ts +73 -54
  84. package/src/cli/FeishuAppConfiguration.ts +2 -2
  85. package/src/cli/FeishuAppRegistration.ts +4 -4
  86. package/src/cli/Initializer.ts +2 -2
  87. package/src/cli/ServerStarter.ts +8 -3
  88. package/src/cli/controlProtocol.ts +21 -1
  89. package/src/cli/help.ts +20 -16
  90. package/src/cli/taskGroupOptions.ts +149 -0
  91. package/src/cli.ts +98 -4
  92. package/src/codex/AppServerConnection.ts +21 -1
  93. package/src/codex/CodexLocalActivityDetector.ts +8 -1
  94. package/src/codex/CodexProcessManager.ts +1 -0
  95. package/src/codex/CodexRuntime.ts +137 -15
  96. package/src/commands/CommandRouter.ts +71 -49
  97. package/src/commands/commandTypes.ts +6 -11
  98. package/src/config/loadConfig.ts +11 -1
  99. package/src/config/schema.ts +7 -1
  100. package/src/console/ConsoleConnector.ts +1 -1
  101. package/src/console/ConsoleTurnPresenter.ts +21 -8
  102. package/src/deprecated-cli.ts +9 -0
  103. package/src/feishu/CardRenderer.ts +423 -15
  104. package/src/feishu/FeishuMessageClient.ts +66 -3
  105. package/src/feishu/FeishuTurnPresenter.ts +22 -3
  106. package/src/feishu/transport.ts +1 -1
  107. package/src/index.ts +54 -24
  108. package/src/presentation/OutboundRouter.ts +15 -3
  109. package/src/presentation/TurnStateReducer.ts +2 -0
  110. package/src/presentation/turnViewTypes.ts +1 -0
  111. package/src/proxy/ProxySessionController.ts +870 -228
  112. package/src/runtime/AcpRuntimeAdapter.ts +1 -0
  113. package/src/runtime/AgentRuntimeRegistry.ts +7 -6
  114. package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
  115. package/src/runtime/types.ts +18 -0
  116. package/src/startup/SessionMetadataHydrator.ts +2 -2
  117. package/src/startup/StartupNotifier.ts +1 -0
  118. package/src/state/StateStore.ts +29 -15
  119. package/src/supervision/SafeRestartScheduler.ts +5 -0
  120. package/src/supervision/systemEnvironment.ts +174 -0
  121. package/src/supervisor.ts +12 -1
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
3
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
4
+ import { resolveUserPath } from "../config/paths.js";
4
5
  import { CommandRouter } from "../commands/CommandRouter.js";
5
6
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
6
- import { CardRenderer } from "../feishu/CardRenderer.js";
7
+ import { CardRenderer, } from "../feishu/CardRenderer.js";
7
8
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
8
9
  import { createId } from "../utils/id.js";
9
10
  import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
@@ -14,6 +15,7 @@ const MESSAGE_COMPLETED_REACTION = "DONE";
14
15
  const MESSAGE_FAILED_REACTION = "ERROR";
15
16
  const MESSAGE_CANCELLED_REACTION = "CrossMark";
16
17
  const SESSION_PAGE_SIZE = 5;
18
+ const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
17
19
  export class ProxySessionController {
18
20
  config;
19
21
  store;
@@ -46,8 +48,8 @@ export class ProxySessionController {
46
48
  if (interruptedAcpSessions.length > 0) {
47
49
  this.logger.warn({ sessionIds: interruptedAcpSessions.map((session) => session.localSessionId) }, "Marked interrupted ACP sessions as failed after process restart.");
48
50
  }
49
- for (const kind of ["acp", "codex"]) {
50
- this.unsubscribe.push(this.runtimes.get(kind).onEvent((event) => {
51
+ for (const [, runtime] of this.runtimes.entries()) {
52
+ this.unsubscribe.push(runtime.onEvent((event) => {
51
53
  void this.handleRuntimeEvent(event).catch((error) => {
52
54
  this.logger.warn({ error, event }, "Failed to present runtime event.");
53
55
  });
@@ -102,7 +104,7 @@ export class ProxySessionController {
102
104
  let command;
103
105
  try {
104
106
  command = imageCount > 0 && !message.text.trimStart().startsWith("/")
105
- ? { type: "prompt", text: message.text.trim() || "请分析这张图片。" }
107
+ ? { type: "prompt", text: message.text.trim() || "请查看这张图片" }
106
108
  : this.router.parse(message.text);
107
109
  }
108
110
  catch (error) {
@@ -169,7 +171,7 @@ export class ProxySessionController {
169
171
  else if (kind === "activity_history") {
170
172
  const requestedPage = String(scopedAction.value.page ?? "0");
171
173
  const numericPage = Number(requestedPage);
172
- await this.outbound.showActivityPage(contextKey, String(scopedAction.value.turnId ?? ""), requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0, scopedAction.messageId);
174
+ await this.outbound.showActivityPage(contextKey, String(scopedAction.value.turnId ?? ""), requestedPage === "latest" ? "latest" : Number.isFinite(numericPage) ? numericPage : 0, requiredCardMessageId(scopedAction.messageId));
173
175
  }
174
176
  else if (kind === "turn_cancel") {
175
177
  const sessionId = String(scopedAction.value.sessionId ?? "");
@@ -236,6 +238,43 @@ export class ProxySessionController {
236
238
  else if (kind === "session_status_refresh") {
237
239
  await this.refreshStatusCardFromAction(scopedAction);
238
240
  }
241
+ else if (kind === "settings_tab_open") {
242
+ await this.openExecutionSettings(contextKey, executionSettingsTabValue(scopedAction.value.tab), {
243
+ sessionId: String(scopedAction.value.sessionId ?? ""),
244
+ updateMessageId: scopedAction.messageId,
245
+ });
246
+ }
247
+ else if (kind === "settings_agent_select") {
248
+ await this.setDefaultAgent(contextKey, String(scopedAction.value.agent ?? ""), {
249
+ sessionId: String(scopedAction.value.sessionId ?? ""),
250
+ updateMessageId: scopedAction.messageId,
251
+ });
252
+ }
253
+ else if (kind === "settings_provider_select") {
254
+ await this.selectProvider(contextKey, String(scopedAction.value.provider ?? ""), {
255
+ sessionId: String(scopedAction.value.sessionId ?? ""),
256
+ updateMessageId: scopedAction.messageId,
257
+ });
258
+ }
259
+ else if (kind === "settings_model_select") {
260
+ await this.model(contextKey, String(scopedAction.value.model ?? ""), {
261
+ sessionId: String(scopedAction.value.sessionId ?? ""),
262
+ updateMessageId: scopedAction.messageId,
263
+ });
264
+ }
265
+ else if (kind === "settings_thinking_select") {
266
+ await this.thinking(contextKey, String(scopedAction.value.effort ?? ""), {
267
+ sessionId: String(scopedAction.value.sessionId ?? ""),
268
+ updateMessageId: scopedAction.messageId,
269
+ expectedModel: String(scopedAction.value.model ?? ""),
270
+ });
271
+ }
272
+ else if (kind === "settings_permission_select") {
273
+ await this.permissions(contextKey, permissionModeValue(scopedAction.value.permissionMode), {
274
+ sessionId: String(scopedAction.value.sessionId ?? ""),
275
+ updateMessageId: scopedAction.messageId,
276
+ });
277
+ }
239
278
  else if (kind === "model_select") {
240
279
  const sessionId = String(scopedAction.value.sessionId ?? "");
241
280
  const model = String(scopedAction.value.model ?? "");
@@ -261,6 +300,32 @@ export class ProxySessionController {
261
300
  expectedModel: model,
262
301
  });
263
302
  }
303
+ else if (kind === "provider_open") {
304
+ await this.openExecutionSettings(contextKey, "provider", {
305
+ sessionId: String(scopedAction.value.sessionId ?? ""),
306
+ updateMessageId: scopedAction.messageId,
307
+ });
308
+ }
309
+ else if (kind === "provider_select" || kind === "provider_model_open") {
310
+ await this.openProviderModelSelector(contextKey, String(scopedAction.value.provider ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
311
+ }
312
+ else if (kind === "provider_model_select" || kind === "provider_reasoning_open") {
313
+ await this.openProviderReasoningSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
314
+ }
315
+ else if (kind === "provider_reasoning_select") {
316
+ await this.openProviderPermissionSelector(contextKey, String(scopedAction.value.provider ?? ""), String(scopedAction.value.model ?? ""), String(scopedAction.value.effort ?? ""), permissionModeValue(scopedAction.value.permissionMode), String(scopedAction.value.sessionId ?? ""), requiredCardMessageId(scopedAction.messageId));
317
+ }
318
+ else if (kind === "provider_permission_select") {
319
+ await this.applyProviderSettings(contextKey, {
320
+ provider: String(scopedAction.value.provider ?? ""),
321
+ model: String(scopedAction.value.model ?? ""),
322
+ effort: String(scopedAction.value.effort ?? ""),
323
+ mode: permissionModeValue(scopedAction.value.permissionMode),
324
+ }, {
325
+ sessionId: String(scopedAction.value.sessionId ?? ""),
326
+ updateMessageId: scopedAction.messageId,
327
+ });
328
+ }
264
329
  else if (kind === "approval") {
265
330
  await this.resolveApproval(scopedAction);
266
331
  }
@@ -304,7 +369,7 @@ export class ProxySessionController {
304
369
  throw new Error(`Task not found: ${localSessionId}`);
305
370
  let remote;
306
371
  if (record.remoteSessionId) {
307
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
372
+ const runtime = this.runtimes.forAgent(record.agentName);
308
373
  if (runtime.readRemoteSession) {
309
374
  try {
310
375
  remote = await runtime.readRemoteSession(record.remoteSessionId);
@@ -348,7 +413,7 @@ export class ProxySessionController {
348
413
  const record = this.store.getSession(localSessionId);
349
414
  if (!record || record.status === "closed")
350
415
  throw new Error(`Task not found: ${localSessionId}`);
351
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
416
+ const runtime = this.runtimes.forAgent(record.agentName);
352
417
  const activeTurnId = runtime.getSession(localSessionId)?.activeTurnId;
353
418
  const routedContextKey = this.outbound.getSessionContextKey(localSessionId);
354
419
  const responseContextKey = (activeTurnId ? this.store.getTurnContextKey(activeTurnId) ?? routedContextKey : undefined)
@@ -373,7 +438,7 @@ export class ProxySessionController {
373
438
  ?? { ...record, contextKey: responseContextKey };
374
439
  const previous = this.messageQueues.get(responseContextKey) ?? Promise.resolve();
375
440
  const next = previous.catch(() => undefined).then(async () => {
376
- this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd);
441
+ this.outbound.registerSession(scopedRecord.localSessionId, responseContextKey, scopedRecord.title, scopedRecord.cwd, this.agentLabel(scopedRecord.agentName));
377
442
  const promptMessageId = await this.outbound.withReplyTarget(responseContextKey, existingReplyTarget, () => this.outbound.sendText(responseContextKey, promptText));
378
443
  const turnReplyTarget = isThreadContextKey(responseContextKey)
379
444
  ? promptMessageId
@@ -396,13 +461,64 @@ export class ProxySessionController {
396
461
  }
397
462
  return `The Prompt was posted to the original chat and submitted to the task: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
398
463
  }
464
+ async controlCreateTaskGroup(localSessionId, requestedTitle, userOpenId, requestedProjectCwd, forceProjectless = false, requestedAgentName) {
465
+ const source = this.store.getSession(localSessionId);
466
+ if (!source || source.status === "closed")
467
+ throw new Error(`Task not found: ${localSessionId}`);
468
+ const agentName = requestedAgentName?.trim() || source.agentName;
469
+ const agent = this.ensureAgent(agentName);
470
+ if (forceProjectless && agent.kind !== "app-server") {
471
+ throw new Error("task newgroup --nodir is only available for Codex agents.");
472
+ }
473
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
474
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
475
+ const boundProjectCwd = forceProjectless
476
+ ? undefined
477
+ : requestedProjectCwd === undefined
478
+ ? detectProjectlessWorkspace(source.cwd) ? undefined : source.cwd
479
+ : resolveUserPath(requestedProjectCwd);
480
+ const created = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () => this.createFeishuGroupWithTask(sourceContextKey, agentName, requestedTitle, userOpenId, boundProjectCwd, source.agentName === agentName
481
+ ? {
482
+ modelProvider: source.modelProvider,
483
+ model: source.model,
484
+ reasoningEffort: source.reasoningEffort,
485
+ permissionMode: source.permissionMode,
486
+ }
487
+ : {}));
488
+ return {
489
+ sourceLocalSessionId: source.localSessionId,
490
+ group: created.group,
491
+ task: created.task,
492
+ };
493
+ }
494
+ async controlForkTaskGroup(localSessionId, requestedTitle, userOpenId) {
495
+ const source = this.store.getSession(localSessionId);
496
+ if (!source || source.status === "closed")
497
+ throw new Error(`Task not found: ${localSessionId}`);
498
+ const sourceContextKey = this.outbound.getSessionContextKey(localSessionId) ?? source.contextKey;
499
+ const replyTarget = this.outbound.getSessionReplyTarget(localSessionId);
500
+ const plan = await this.prepareForkSession(sourceContextKey, localSessionId, requestedTitle);
501
+ const prepared = {
502
+ plan,
503
+ sourceDescription: plan.forkedFromHistoricalTurn
504
+ ? "当前任务最近已完成轮次"
505
+ : "当前任务最新轮次",
506
+ };
507
+ const forked = await this.outbound.withReplyTarget(sourceContextKey, replyTarget, () => this.forkPreparedSessionToFeishuGroup(sourceContextKey, prepared, userOpenId ?? "", "当前任务"));
508
+ return {
509
+ sourceLocalSessionId: source.localSessionId,
510
+ sourceTurnId: plan.lastTurnId,
511
+ group: forked.group,
512
+ task: forked.task,
513
+ };
514
+ }
399
515
  cardActionContextKey(action) {
400
516
  const explicit = typeof action.value.contextKey === "string" ? action.value.contextKey : undefined;
401
517
  if (explicit && baseChatContextKey(explicit) === baseChatContextKey(action.contextKey))
402
518
  return explicit;
403
519
  const sessionReference = typeof action.value.sessionId === "string" ? action.value.sessionId : undefined;
404
520
  const session = sessionReference
405
- ? this.store.getSession(sessionReference) ?? this.store.findSessionByRemoteSessionId(sessionReference)
521
+ ? this.store.getSession(sessionReference) ?? this.findStoredSessionByReference(sessionReference)
406
522
  : undefined;
407
523
  if (session && baseChatContextKey(session.contextKey) === baseChatContextKey(action.contextKey)) {
408
524
  return session.contextKey;
@@ -421,13 +537,20 @@ export class ProxySessionController {
421
537
  await this.runShellCommand(contextKey, command.command);
422
538
  return;
423
539
  case "new":
424
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "codex") {
425
- throw new Error("/new --nodir 仅支持 Codex Agent。");
540
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
541
+ throw new Error("/new --nodir 仅支持 App Server Agent。");
426
542
  }
427
- await this.createSession(contextKey, context.defaultAgent, command.projectless ? undefined : command.cwd ?? this.inheritedNewTaskCwd(contextKey), true, false, undefined, undefined, command.title);
543
+ await this.createSession(contextKey, context.defaultAgent, command.projectless
544
+ ? undefined
545
+ : command.cwd === undefined
546
+ ? this.inheritedNewTaskCwd(contextKey)
547
+ : resolveUserPath(command.cwd), true, false, undefined, undefined, command.title, this.inheritedExecutionSettings(contextKey, context.defaultAgent));
428
548
  return;
429
549
  case "newgroup":
430
- await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId);
550
+ if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
551
+ throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
552
+ }
553
+ await this.createFeishuGroup(contextKey, context.defaultAgent, command.title, userId, command.cwd === undefined ? undefined : resolveUserPath(command.cwd), command.projectless === true);
431
554
  return;
432
555
  case "forkgroup":
433
556
  await this.forkCurrentSessionToFeishuGroup(contextKey, command.title, userId, incomingMessage);
@@ -438,7 +561,6 @@ export class ProxySessionController {
438
561
  case "title":
439
562
  await this.setTitle(contextKey, command.title);
440
563
  return;
441
- case "ask":
442
564
  case "prompt":
443
565
  await this.prompt(contextKey, command.text, messageId, replyTarget, localImagePaths);
444
566
  return;
@@ -455,11 +577,7 @@ export class ProxySessionController {
455
577
  if (command.agent)
456
578
  await this.setDefaultAgent(contextKey, command.agent);
457
579
  else
458
- await this.listAgents(contextKey);
459
- return;
460
- case "use":
461
- await this.setDefaultAgent(contextKey, command.agent);
462
- await this.createSession(contextKey, command.agent, command.cwd, true, false);
580
+ await this.openAgentSettings(contextKey);
463
581
  return;
464
582
  case "status":
465
583
  await this.status(contextKey, command.sessionId);
@@ -470,20 +588,19 @@ export class ProxySessionController {
470
588
  case "restart":
471
589
  if (!this.lifecycle)
472
590
  throw new Error("当前运行方式不支持自动重启。");
473
- await this.lifecycle.restart(contextKey);
591
+ await this.lifecycle.restart(contextKey, command.force === true);
474
592
  return;
475
593
  case "model":
476
- await this.model(contextKey, command.model);
594
+ await this.openExecutionSettings(contextKey, "model");
595
+ return;
596
+ case "provider":
597
+ await this.openProviderSettings(contextKey);
477
598
  return;
478
599
  case "thinking":
479
- await this.thinking(contextKey, command.effort);
600
+ await this.openExecutionSettings(contextKey, "thinking");
480
601
  return;
481
602
  case "permissions":
482
- await this.permissions(contextKey, command.mode);
483
- return;
484
- case "modes":
485
- case "mode":
486
- await this.outbound.sendText(contextKey, "当前统一运行时不再暴露 ACP modes;Codex 请使用 /model 和 /permissions。");
603
+ await this.openExecutionSettings(contextKey, "permission");
487
604
  return;
488
605
  case "help":
489
606
  await this.help(contextKey);
@@ -503,7 +620,7 @@ export class ProxySessionController {
503
620
  const turnContextKey = session.lastTurnId
504
621
  ? this.store.getTurnContextKey(session.lastTurnId) ?? context.contextKey
505
622
  : context.contextKey;
506
- this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd);
623
+ this.outbound.registerSession(session.localSessionId, turnContextKey, session.title, session.cwd, this.agentLabel(session.agentName));
507
624
  if (!session.lastTurnId)
508
625
  continue;
509
626
  void this.outbound.resumeDelivery(session.localSessionId, turnContextKey, session.lastTurnId).catch((error) => {
@@ -546,9 +663,9 @@ export class ProxySessionController {
546
663
  await this.promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths);
547
664
  }
548
665
  async promptSession(record, contextKey, text, messageId, replyTarget, localImagePaths) {
549
- const configuredRuntime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
666
+ const configuredRuntime = this.runtimes.forAgent(record.agentName);
550
667
  if (!configuredRuntime.getSession(record.localSessionId)) {
551
- this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd);
668
+ this.outbound.registerSession(record.localSessionId, contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
552
669
  await this.outbound.startPendingTurn(record.localSessionId, contextKey, record.title, replyTarget, text);
553
670
  }
554
671
  let loaded;
@@ -760,7 +877,7 @@ export class ProxySessionController {
760
877
  async forkThreadSession(message) {
761
878
  const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
762
879
  const agent = this.ensureAgent(source.agentName);
763
- const runtime = this.runtimes.forAgent(agent);
880
+ const runtime = this.runtimes.forAgent(source.agentName);
764
881
  if (runtime.kind !== "codex" || !runtime.forkSession) {
765
882
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
766
883
  }
@@ -779,12 +896,13 @@ export class ProxySessionController {
779
896
  this.store.updateRuntimeSession(localSessionId, {
780
897
  runtimeKind: "codex",
781
898
  title: forkTitle,
899
+ modelProvider: source.modelProvider,
782
900
  model: source.model,
783
901
  reasoningEffort: source.reasoningEffort,
784
902
  permissionMode: source.permissionMode ?? "auto",
785
903
  });
786
904
  this.store.setCurrentSession(message.contextKey, localSessionId);
787
- this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd);
905
+ this.outbound.registerSession(localSessionId, message.contextKey, forkTitle, source.cwd, this.agentLabel(source.agentName));
788
906
  try {
789
907
  const forked = await runtime.forkSession({
790
908
  localSessionId,
@@ -793,6 +911,7 @@ export class ProxySessionController {
793
911
  agentName: source.agentName,
794
912
  cwd: source.cwd,
795
913
  title: forkTitle,
914
+ modelProvider: source.modelProvider,
796
915
  model: source.model,
797
916
  reasoningEffort: source.reasoningEffort,
798
917
  permissionMode: source.permissionMode ?? "auto",
@@ -869,7 +988,7 @@ export class ProxySessionController {
869
988
  if (direct?.status === "closed") {
870
989
  throw new Error(`找不到任务:${taskId}`);
871
990
  }
872
- const global = direct ?? this.store.findSessionByRemoteSessionId(taskId);
991
+ const global = direct ?? this.findStoredSessionByReference(taskId);
873
992
  source = global ? { ...global, contextKey } : undefined;
874
993
  }
875
994
  if (source && (!source.remoteSessionId || !this.isCodexSession(source))) {
@@ -877,27 +996,29 @@ export class ProxySessionController {
877
996
  }
878
997
  let runtime;
879
998
  let agentName;
999
+ let remote;
880
1000
  if (source) {
881
1001
  agentName = source.agentName;
882
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1002
+ runtime = this.runtimes.forAgent(agentName);
883
1003
  }
884
1004
  else {
885
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
886
- if (!agentEntry)
887
- throw new Error("未配置 Codex Agent。");
888
- [agentName] = agentEntry;
889
- runtime = this.runtimes.get("codex");
1005
+ if (!taskId)
1006
+ throw new Error("缺少要 fork 的 Codex 任务 ID。");
1007
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1008
+ agentName = resolved.agentName;
1009
+ runtime = resolved.runtime;
1010
+ remote = resolved.remote;
890
1011
  }
891
1012
  if (runtime.kind !== "codex" || !runtime.forkSession) {
892
1013
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
893
1014
  }
894
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1015
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
895
1016
  if (!remoteSessionId)
896
1017
  throw new Error("当前任务尚未创建 Codex 任务 ID,暂时不能 fork。");
897
1018
  if (!runtime.readRemoteSession && !source) {
898
1019
  throw new Error("当前 Codex 运行时不支持读取指定任务。");
899
1020
  }
900
- const remote = runtime.readRemoteSession
1021
+ remote ??= runtime.readRemoteSession
901
1022
  ? await runtime.readRemoteSession(remoteSessionId)
902
1023
  : undefined;
903
1024
  const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
@@ -921,7 +1042,8 @@ export class ProxySessionController {
921
1042
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能 fork。");
922
1043
  const sourceTitle = remote?.title ?? source?.title ?? remote?.preview;
923
1044
  const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(sourceTitle);
924
- const model = source?.model;
1045
+ const modelProvider = remote?.modelProvider ?? source?.modelProvider;
1046
+ const model = remote?.model ?? source?.model;
925
1047
  const reasoningEffort = source?.reasoningEffort;
926
1048
  const permissionMode = source?.permissionMode ?? "auto";
927
1049
  return {
@@ -933,6 +1055,7 @@ export class ProxySessionController {
933
1055
  lastTurnId,
934
1056
  cwd,
935
1057
  forkTitle,
1058
+ modelProvider,
936
1059
  model,
937
1060
  reasoningEffort,
938
1061
  permissionMode,
@@ -975,7 +1098,7 @@ export class ProxySessionController {
975
1098
  if (!source.remoteSessionId || !this.isCodexSession(source)) {
976
1099
  throw new Error(`${sourceLabel}不是可 fork 的 Codex 任务。`);
977
1100
  }
978
- const runtime = this.runtimes.forAgent(this.ensureAgent(source.agentName));
1101
+ const runtime = this.runtimes.forAgent(source.agentName);
979
1102
  if (runtime.kind !== "codex" || !runtime.forkSession) {
980
1103
  throw new Error("当前 Codex 运行时不支持 fork 任务。");
981
1104
  }
@@ -996,6 +1119,7 @@ export class ProxySessionController {
996
1119
  lastTurnId,
997
1120
  cwd: source.cwd,
998
1121
  forkTitle,
1122
+ modelProvider: source.modelProvider,
999
1123
  model: source.model,
1000
1124
  reasoningEffort: source.reasoningEffort,
1001
1125
  permissionMode: source.permissionMode ?? "auto",
@@ -1018,6 +1142,7 @@ export class ProxySessionController {
1018
1142
  this.store.updateRuntimeSession(localSessionId, {
1019
1143
  runtimeKind: "codex",
1020
1144
  title: plan.forkTitle,
1145
+ modelProvider: plan.modelProvider,
1021
1146
  model: plan.model,
1022
1147
  reasoningEffort: plan.reasoningEffort,
1023
1148
  permissionMode: plan.permissionMode,
@@ -1030,6 +1155,7 @@ export class ProxySessionController {
1030
1155
  agentName: plan.agentName,
1031
1156
  cwd: plan.cwd,
1032
1157
  title: plan.forkTitle,
1158
+ modelProvider: plan.modelProvider,
1033
1159
  model: plan.model,
1034
1160
  reasoningEffort: plan.reasoningEffort,
1035
1161
  permissionMode: plan.permissionMode,
@@ -1040,7 +1166,7 @@ export class ProxySessionController {
1040
1166
  lastTurnStatus: plan.lastTurnStatus,
1041
1167
  });
1042
1168
  this.store.setCurrentSession(contextKey, localSessionId);
1043
- this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd);
1169
+ this.outbound.registerSession(localSessionId, contextKey, forked.title ?? plan.forkTitle, plan.cwd, this.agentLabel(plan.agentName));
1044
1170
  this.store.audit(contextKey, "session_forked", {
1045
1171
  sourceLocalSessionId: plan.source?.localSessionId,
1046
1172
  sourceRemoteSessionId: plan.remoteSessionId,
@@ -1077,26 +1203,25 @@ export class ProxySessionController {
1077
1203
  if (direct?.status === "closed") {
1078
1204
  throw new Error(`找不到任务:${taskId}`);
1079
1205
  }
1080
- const source = direct ?? this.store.findSessionByRemoteSessionId(taskId);
1206
+ const source = direct ?? this.findStoredSessionByReference(taskId);
1081
1207
  if (source && !this.isCodexSession(source)) {
1082
1208
  throw new Error("指定任务不是 Codex 任务,暂时不能按项目创建新任务。");
1083
1209
  }
1084
1210
  let agentName;
1085
1211
  let runtime;
1212
+ let remote;
1086
1213
  if (source) {
1087
1214
  agentName = source.agentName;
1088
- runtime = this.runtimes.forAgent(this.ensureAgent(agentName));
1215
+ runtime = this.runtimes.forAgent(agentName);
1089
1216
  }
1090
1217
  else {
1091
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
1092
- if (!agentEntry)
1093
- throw new Error("未配置 Codex Agent。");
1094
- [agentName] = agentEntry;
1095
- runtime = this.runtimes.get("codex");
1218
+ const resolved = await this.resolveRemoteCodexSession(taskId);
1219
+ agentName = resolved.agentName;
1220
+ runtime = resolved.runtime;
1221
+ remote = resolved.remote;
1096
1222
  }
1097
- const remoteSessionId = source?.remoteSessionId ?? taskId;
1098
- let remote;
1099
- if (runtime.readRemoteSession && remoteSessionId) {
1223
+ const remoteSessionId = source?.remoteSessionId ?? remote?.id ?? taskId;
1224
+ if (!remote && runtime.readRemoteSession && remoteSessionId) {
1100
1225
  try {
1101
1226
  remote = await runtime.readRemoteSession(remoteSessionId);
1102
1227
  }
@@ -1111,6 +1236,7 @@ export class ProxySessionController {
1111
1236
  throw new Error("指定的 Codex 任务没有可用的工作目录,暂时不能按项目创建新任务。");
1112
1237
  }
1113
1238
  const executionSettings = {
1239
+ modelProvider: remote?.modelProvider ?? source?.modelProvider,
1114
1240
  model: remote?.model ?? source?.model,
1115
1241
  reasoningEffort: remote?.reasoningEffort ?? source?.reasoningEffort,
1116
1242
  permissionMode: remote?.permissionMode ?? source?.permissionMode ?? "auto",
@@ -1152,21 +1278,22 @@ export class ProxySessionController {
1152
1278
  const agent = this.ensureAgent(agentName);
1153
1279
  const localSessionId = createId("sess");
1154
1280
  const initialTitle = normalizeTaskTitle(requestedTitle ?? prompt ?? "");
1155
- const sessionCwd = cwd === undefined && agent.kind === "codex"
1281
+ const sessionCwd = cwd === undefined && agent.kind === "app-server"
1156
1282
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
1157
1283
  : path.resolve(cwd ?? this.config.defaults.cwd);
1158
1284
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
1159
- if (initialTitle || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1285
+ if (initialTitle || executionSettings.modelProvider || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
1160
1286
  this.store.updateRuntimeSession(localSessionId, {
1161
1287
  title: initialTitle,
1288
+ modelProvider: executionSettings.modelProvider,
1162
1289
  model: executionSettings.model,
1163
1290
  reasoningEffort: executionSettings.reasoningEffort,
1164
1291
  permissionMode: executionSettings.permissionMode,
1165
1292
  });
1166
1293
  }
1167
1294
  this.store.setCurrentSession(contextKey, localSessionId);
1168
- this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd);
1169
- const runtime = this.runtimes.forAgent(agent);
1295
+ this.outbound.registerSession(localSessionId, contextKey, initialTitle, sessionCwd, this.agentLabel(agentName));
1296
+ const runtime = this.runtimes.forAgent(agentName);
1170
1297
  try {
1171
1298
  if (prepareTurn) {
1172
1299
  await this.outbound.startPendingTurn(localSessionId, contextKey, initialTitle, replyTarget, prompt);
@@ -1176,6 +1303,7 @@ export class ProxySessionController {
1176
1303
  agentName,
1177
1304
  cwd: sessionCwd,
1178
1305
  title: initialTitle,
1306
+ modelProvider: executionSettings.modelProvider,
1179
1307
  model: executionSettings.model,
1180
1308
  reasoningEffort: executionSettings.reasoningEffort,
1181
1309
  permissionMode: executionSettings.permissionMode ?? "auto",
@@ -1196,11 +1324,14 @@ export class ProxySessionController {
1196
1324
  throw error;
1197
1325
  }
1198
1326
  }
1199
- async createFeishuGroup(sourceContextKey, agentName, requestedTitle, userId) {
1327
+ async createFeishuGroup(sourceContextKey, agentName, requestedTitle, userId, requestedProjectCwd, forceProjectless = false) {
1200
1328
  const source = this.currentSession(sourceContextKey);
1201
- const boundProjectCwd = this.currentProjectCwd(sourceContextKey);
1329
+ const boundProjectCwd = forceProjectless
1330
+ ? undefined
1331
+ : requestedProjectCwd ?? this.currentProjectCwd(sourceContextKey);
1202
1332
  const executionSettings = source?.agentName === agentName
1203
1333
  ? {
1334
+ modelProvider: source.modelProvider,
1204
1335
  model: source.model,
1205
1336
  reasoningEffort: source.reasoningEffort,
1206
1337
  permissionMode: source.permissionMode,
@@ -1215,8 +1346,8 @@ export class ProxySessionController {
1215
1346
  }
1216
1347
  async createFeishuGroupWithTask(sourceContextKey, agentName, requestedTitle, userId, boundProjectCwd, executionSettings) {
1217
1348
  const explicitTitle = normalizeTaskTitle(requestedTitle);
1218
- const taskTitle = explicitTitle ?? "新任务";
1219
- const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup", !explicitTitle);
1349
+ const taskTitle = explicitTitle ?? `新任务 (${formatGroupDateSuffix(new Date())})`;
1350
+ const group = await this.createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, "/newgroup", false);
1220
1351
  let task;
1221
1352
  try {
1222
1353
  task = await this.createSession(group.contextKey, agentName, boundProjectCwd, false, false, undefined, undefined, taskTitle, executionSettings);
@@ -1232,10 +1363,13 @@ export class ProxySessionController {
1232
1363
  "群和新任务已创建。",
1233
1364
  `当前任务:${taskDescription}`,
1234
1365
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1366
+ `当前 Provider:${task.modelProvider ?? "Codex 默认"}`,
1235
1367
  `当前模型:${task.model ?? "默认"}`,
1236
1368
  `思考强度:${task.reasoningEffort ?? "自动"}`,
1369
+ `权限类型:${task.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1237
1370
  ].join("\n"));
1238
1371
  await this.outbound.sendText(sourceContextKey, `已创建飞书群:${group.name},并创建新任务 ${taskDescription}。`);
1372
+ return { group, task };
1239
1373
  }
1240
1374
  async forkCurrentSessionToFeishuGroup(sourceContextKey, requestedTitle, userId, incomingMessage) {
1241
1375
  if (!userId?.startsWith("ou_")) {
@@ -1275,11 +1409,13 @@ export class ProxySessionController {
1275
1409
  `已从${prepared.sourceDescription}创建分支。`,
1276
1410
  `当前任务:${taskDescription}`,
1277
1411
  `当前 Project 目录:${boundProjectCwd ?? "未绑定(Projectless)"}`,
1412
+ `当前 Provider:${forked.session.modelProvider ?? "Codex 默认"}`,
1278
1413
  `当前模型:${forked.session.model ?? "默认"}`,
1279
1414
  `思考强度:${forked.session.reasoningEffort ?? "自动"}`,
1280
1415
  `权限类型:${forked.session.permissionMode === "confirm" ? "执行前确认" : "自动执行"}`,
1281
1416
  ].join("\n"));
1282
1417
  await this.outbound.sendText(sourceContextKey, `已将${sourceSummary} Fork 到飞书群:${group.name};新群当前任务为 ${taskDescription}。`);
1418
+ return { group, task: forked.record };
1283
1419
  }
1284
1420
  async createFeishuGroupContext(sourceContextKey, agentName, taskTitle, userId, boundProjectCwd, commandName, includeTimestamp) {
1285
1421
  if (!userId?.startsWith("ou_")) {
@@ -1296,18 +1432,18 @@ export class ProxySessionController {
1296
1432
  this.store.getOrCreateUserContext(groupContextKey, agentName);
1297
1433
  if (boundProjectCwd)
1298
1434
  this.store.setBoundProjectCwd(groupContextKey, boundProjectCwd);
1299
- return { contextKey: groupContextKey, name: group.name };
1435
+ return { chatId: group.chatId, contextKey: groupContextKey, name: group.name };
1300
1436
  }
1301
1437
  async loadSession(record) {
1302
1438
  const agent = this.ensureAgent(record.agentName);
1303
- const runtime = this.runtimes.forAgent(agent);
1439
+ const runtime = this.runtimes.forAgent(record.agentName);
1304
1440
  const existing = runtime.getSession(record.localSessionId);
1305
1441
  if (existing)
1306
1442
  return { record, runtime, session: existing };
1307
1443
  const pending = this.sessionLoads.get(record.localSessionId);
1308
1444
  if (pending)
1309
1445
  return pending;
1310
- this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd);
1446
+ this.outbound.registerSession(record.localSessionId, record.contextKey, record.title, record.cwd, this.agentLabel(record.agentName));
1311
1447
  const loading = (async () => {
1312
1448
  if (record.lastTurnId) {
1313
1449
  try {
@@ -1329,10 +1465,11 @@ export class ProxySessionController {
1329
1465
  agentName: record.agentName,
1330
1466
  cwd: record.cwd,
1331
1467
  title: record.title,
1468
+ modelProvider: record.modelProvider,
1332
1469
  model: record.model,
1333
1470
  reasoningEffort: record.reasoningEffort,
1334
1471
  permissionMode,
1335
- activeTurnId: agent.kind === "codex" && record.status === "running" && record.lastTurnStatus === "running"
1472
+ activeTurnId: agent.kind === "app-server" && record.status === "running" && record.lastTurnStatus === "running"
1336
1473
  ? record.lastTurnId
1337
1474
  : undefined,
1338
1475
  lastTurnId: record.lastTurnId,
@@ -1340,7 +1477,7 @@ export class ProxySessionController {
1340
1477
  });
1341
1478
  }
1342
1479
  catch (error) {
1343
- if (!(agent.kind === "codex" && !record.lastTurnId && isMissingRolloutError(error)))
1480
+ if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
1344
1481
  throw error;
1345
1482
  this.logger.warn({ error, sessionId: record.localSessionId }, "Codex task has no rollout; creating a replacement task.");
1346
1483
  session = await runtime.createSession({
@@ -1348,6 +1485,7 @@ export class ProxySessionController {
1348
1485
  agentName: record.agentName,
1349
1486
  cwd: record.cwd,
1350
1487
  title: record.title,
1488
+ modelProvider: record.modelProvider,
1351
1489
  model: record.model,
1352
1490
  reasoningEffort: record.reasoningEffort,
1353
1491
  permissionMode,
@@ -1360,6 +1498,7 @@ export class ProxySessionController {
1360
1498
  agentName: record.agentName,
1361
1499
  cwd: record.cwd,
1362
1500
  title: record.title,
1501
+ modelProvider: record.modelProvider,
1363
1502
  model: record.model,
1364
1503
  reasoningEffort: record.reasoningEffort,
1365
1504
  permissionMode,
@@ -1382,6 +1521,7 @@ export class ProxySessionController {
1382
1521
  runtimeKind: session.runtimeKind,
1383
1522
  remoteSessionId: session.remoteSessionId,
1384
1523
  title: session.title,
1524
+ modelProvider: session.modelProvider,
1385
1525
  model: session.model,
1386
1526
  reasoningEffort: session.reasoningEffort,
1387
1527
  permissionMode: session.permissionMode,
@@ -1407,38 +1547,36 @@ export class ProxySessionController {
1407
1547
  lastTurnStatus: event.type === "turn_completed" ? "completed" : event.type === "turn_cancelled" ? "cancelled" : "failed",
1408
1548
  });
1409
1549
  }
1550
+ let presentationError;
1410
1551
  try {
1411
1552
  await this.outbound.onEvent(event);
1412
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1413
- const terminalStatus = event.type === "turn_completed"
1414
- ? "completed"
1415
- : event.type === "turn_cancelled"
1416
- ? "cancelled"
1417
- : "failed";
1418
- const latest = this.store.getSession(event.sessionId);
1419
- if (latest?.lastTurnId === event.turnId && latest.lastTurnStatus === terminalStatus) {
1420
- await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1421
- }
1422
- }
1423
1553
  }
1424
- finally {
1425
- if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1426
- const latest = this.store.getSession(event.sessionId);
1427
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1428
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1429
- if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1430
- this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1431
- }
1432
- }
1554
+ catch (error) {
1555
+ presentationError = error;
1433
1556
  }
1434
1557
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
1558
+ const terminalStatus = event.type === "turn_completed"
1559
+ ? "completed"
1560
+ : event.type === "turn_cancelled"
1561
+ ? "cancelled"
1562
+ : "failed";
1563
+ const reactionSession = this.store.getSession(event.sessionId);
1564
+ if (!presentationError
1565
+ && reactionSession?.lastTurnId === event.turnId
1566
+ && reactionSession.lastTurnStatus === terminalStatus) {
1567
+ await this.finalizeTurnMessageReactions(event.turnId, terminalStatus);
1568
+ }
1435
1569
  const latest = this.store.getSession(event.sessionId);
1436
- const agent = latest ? this.ensureAgent(latest.agentName) : undefined;
1437
- const activeTurnId = agent ? this.runtimes.forAgent(agent).getSession(event.sessionId)?.activeTurnId : undefined;
1438
- if (latest?.lastTurnId !== event.turnId || activeTurnId)
1439
- return;
1440
- await this.scheduleNextQueuedPrompt(event.sessionId);
1570
+ const activeTurnId = latest
1571
+ ? this.runtimes.forAgent(latest.agentName).getSession(event.sessionId)?.activeTurnId
1572
+ : undefined;
1573
+ if (latest?.lastTurnId === event.turnId && !activeTurnId) {
1574
+ this.store.updateSession(event.sessionId, { status: event.type === "turn_failed" ? "failed" : "ready" });
1575
+ await this.scheduleNextQueuedPrompt(event.sessionId);
1576
+ }
1441
1577
  }
1578
+ if (presentationError)
1579
+ throw presentationError;
1442
1580
  }
1443
1581
  scheduleNextQueuedPrompt(sessionId) {
1444
1582
  const previous = this.queuedPromptStarts.get(sessionId) ?? Promise.resolve();
@@ -1533,7 +1671,7 @@ export class ProxySessionController {
1533
1671
  await this.cancelSession(record);
1534
1672
  }
1535
1673
  async cancelSession(record) {
1536
- const runtime = this.runtimes.forAgent(this.ensureAgent(record.agentName));
1674
+ const runtime = this.runtimes.forAgent(record.agentName);
1537
1675
  if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
1538
1676
  if (runtime.getGoal && runtime.setGoal) {
1539
1677
  try {
@@ -1590,29 +1728,237 @@ export class ProxySessionController {
1590
1728
  const loaded = await this.loadSession(this.requireSession(action.contextKey, sessionId));
1591
1729
  await loaded.runtime.respondToApproval(sessionId, requestId, decision);
1592
1730
  }
1593
- async model(contextKey, model, options = {}) {
1731
+ async openExecutionSettings(contextKey, activeTab, options = {}) {
1732
+ const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1733
+ const record = options.sessionId
1734
+ ? this.requireSession(contextKey, options.sessionId)
1735
+ : this.currentSession(contextKey);
1736
+ if (!record && activeTab !== "agent")
1737
+ this.requireCurrentSession(contextKey);
1738
+ const agents = Object.entries(this.config.agents).map(([name, agent]) => ({
1739
+ name,
1740
+ title: agent.title,
1741
+ }));
1742
+ let runtimeSettings = {
1743
+ currentPermissionMode: "auto",
1744
+ providers: [],
1745
+ providerSupported: false,
1746
+ models: [],
1747
+ reasoningOptions: [],
1748
+ };
1749
+ if (record) {
1750
+ const loaded = await this.loadSession(record);
1751
+ const models = await loaded.runtime.listModels();
1752
+ const currentModel = models.find((model) => model.id === loaded.session.model)
1753
+ ?? models.find((model) => model.isDefault);
1754
+ const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
1755
+ const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
1756
+ const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
1757
+ const currentEffort = currentModel?.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
1758
+ ? loaded.session.reasoningEffort
1759
+ : currentModel?.defaultReasoningEffort ?? loaded.session.reasoningEffort;
1760
+ runtimeSettings = {
1761
+ currentProvider,
1762
+ currentModel: currentModel?.id ?? loaded.session.model,
1763
+ currentEffort,
1764
+ currentPermissionMode: loaded.session.permissionMode,
1765
+ providers,
1766
+ providerSupported,
1767
+ models,
1768
+ reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
1769
+ };
1770
+ }
1771
+ const card = this.cardRenderer.renderExecutionSettings({
1772
+ ...(record ? { sessionId: record.localSessionId, taskAgent: record.agentName } : {}),
1773
+ contextKey,
1774
+ activeTab,
1775
+ currentAgent: context.defaultAgent,
1776
+ agents,
1777
+ runtimeSettingsAvailable: Boolean(record),
1778
+ ...runtimeSettings,
1779
+ notice: options.notice,
1780
+ });
1781
+ if (options.updateMessageId) {
1782
+ await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1783
+ }
1784
+ else {
1785
+ await this.outbound.sendInteractiveCard(contextKey, card);
1786
+ }
1787
+ }
1788
+ async openAgentSettings(contextKey) {
1789
+ const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1790
+ if (Object.keys(this.config.agents).length <= 1) {
1791
+ await this.outbound.sendText(contextKey, `当前 Agent:${current}\n当前没有其他 Agent 可以切换。`);
1792
+ return;
1793
+ }
1794
+ await this.openExecutionSettings(contextKey, "agent");
1795
+ }
1796
+ async openProviderSettings(contextKey) {
1797
+ const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
1798
+ const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
1799
+ ? await this.modelProviderOptions(loaded)
1800
+ : [];
1801
+ const current = loaded.session.modelProvider?.trim()
1802
+ || providers.find((provider) => provider.isDefault)?.id
1803
+ || "运行时默认";
1804
+ if (providers.length <= 1) {
1805
+ await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
1806
+ return;
1807
+ }
1808
+ await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
1809
+ }
1810
+ async selectProvider(contextKey, modelProvider, options = {}) {
1594
1811
  const record = options.sessionId
1595
1812
  ? this.requireSession(contextKey, options.sessionId)
1596
1813
  : this.requireCurrentSession(contextKey);
1597
1814
  const loaded = await this.loadSession(record);
1815
+ await this.assertModelProvider(loaded, modelProvider);
1598
1816
  const models = await loaded.runtime.listModels();
1599
- if (!model) {
1600
- const currentModel = loaded.session.model ?? models.find((item) => item.isDefault)?.id;
1601
- const card = this.cardRenderer.renderModelSelector({
1817
+ const model = models.find((candidate) => candidate.id === loaded.session.model)
1818
+ ?? models.find((candidate) => candidate.isDefault);
1819
+ if (!model)
1820
+ throw new Error("当前运行时没有可用于 Provider 切换的模型。");
1821
+ const effort = model.supportedReasoningEfforts.some((candidate) => candidate.value === loaded.session.reasoningEffort)
1822
+ ? loaded.session.reasoningEffort
1823
+ : model.defaultReasoningEffort ?? model.supportedReasoningEfforts[0]?.value;
1824
+ if (!effort)
1825
+ throw new Error(`模型 ${model.id} 没有可用于 Provider 切换的思考强度。`);
1826
+ await this.applyProviderSettings(contextKey, {
1827
+ provider: modelProvider,
1828
+ model: model.id,
1829
+ effort,
1830
+ mode: loaded.session.permissionMode,
1831
+ }, options);
1832
+ }
1833
+ async openProviderSelector(contextKey, options = {}) {
1834
+ await this.openExecutionSettings(contextKey, "provider", options);
1835
+ }
1836
+ async openProviderModelSelector(contextKey, modelProvider, permissionMode, sessionId, updateMessageId) {
1837
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1838
+ await this.assertModelProvider(loaded, modelProvider);
1839
+ const models = await loaded.runtime.listModels();
1840
+ const card = this.cardRenderer.renderModelSelector({
1841
+ sessionId,
1842
+ contextKey,
1843
+ currentModel: loaded.session.model ?? models.find((model) => model.isDefault)?.id,
1844
+ reasoningEffort: loaded.session.reasoningEffort,
1845
+ models,
1846
+ modelProvider,
1847
+ permissionMode,
1848
+ unifiedSettings: true,
1849
+ });
1850
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
1851
+ }
1852
+ async openProviderReasoningSelector(contextKey, modelProvider, model, permissionMode, sessionId, updateMessageId) {
1853
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1854
+ await this.assertModelProvider(loaded, modelProvider);
1855
+ const models = await loaded.runtime.listModels();
1856
+ const selected = models.find((candidate) => candidate.id === model);
1857
+ if (!selected)
1858
+ throw new Error(`未知模型:${model}`);
1859
+ const currentEffort = selected.supportedReasoningEfforts.some((option) => option.value === loaded.session.reasoningEffort)
1860
+ ? loaded.session.reasoningEffort
1861
+ : selected.defaultReasoningEffort;
1862
+ const card = this.cardRenderer.renderReasoningSelector({
1863
+ sessionId,
1864
+ contextKey,
1865
+ modelProvider,
1866
+ model,
1867
+ currentEffort,
1868
+ options: selected.supportedReasoningEfforts,
1869
+ permissionMode,
1870
+ unifiedSettings: true,
1871
+ });
1872
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, card);
1873
+ }
1874
+ async openProviderPermissionSelector(contextKey, modelProvider, model, effort, permissionMode, sessionId, updateMessageId) {
1875
+ const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
1876
+ await this.assertProviderModelSettings(loaded, modelProvider, model, effort);
1877
+ await this.outbound.updateInteractiveCard(contextKey, updateMessageId, this.cardRenderer.renderPermissionSelector({
1878
+ sessionId,
1879
+ contextKey,
1880
+ modelProvider,
1881
+ model,
1882
+ reasoningEffort: effort,
1883
+ currentMode: permissionMode,
1884
+ }));
1885
+ }
1886
+ async applyProviderSettings(contextKey, settings, options = {}) {
1887
+ const record = options.sessionId
1888
+ ? this.requireSession(contextKey, options.sessionId)
1889
+ : this.requireCurrentSession(contextKey);
1890
+ const loaded = await this.loadSession(record);
1891
+ await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
1892
+ if (!loaded.runtime.setExecutionSettings) {
1893
+ throw new Error("当前运行时不支持 Provider 设置。");
1894
+ }
1895
+ const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, {
1896
+ modelProvider: settings.provider,
1897
+ model: settings.model,
1898
+ reasoningEffort: settings.effort,
1899
+ permissionMode: settings.mode,
1900
+ });
1901
+ this.store.updateRuntimeSession(loaded.record.localSessionId, {
1902
+ modelProvider: session.modelProvider ?? settings.provider,
1903
+ model: session.model ?? settings.model,
1904
+ reasoningEffort: session.reasoningEffort ?? settings.effort,
1905
+ permissionMode: session.permissionMode,
1906
+ });
1907
+ const notice = [
1908
+ `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
1909
+ `模型 ${cardCode(session.model ?? settings.model)}`,
1910
+ `思考强度 ${cardCode(session.reasoningEffort ?? settings.effort)}`,
1911
+ `权限 ${cardCode(session.permissionMode)}`,
1912
+ "从下一次请求生效。",
1913
+ ].join(",");
1914
+ if (options.updateMessageId) {
1915
+ await this.openProviderSelector(contextKey, {
1602
1916
  sessionId: loaded.record.localSessionId,
1603
- contextKey,
1604
- currentModel,
1605
- reasoningEffort: loaded.session.reasoningEffort,
1606
- models,
1917
+ updateMessageId: options.updateMessageId,
1918
+ notice,
1607
1919
  });
1608
- if (options.updateMessageId) {
1609
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1610
- }
1611
- else {
1612
- await this.outbound.sendInteractiveCard(contextKey, card);
1613
- }
1920
+ }
1921
+ else {
1922
+ await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
1923
+ }
1924
+ }
1925
+ async assertProviderModelSettings(loaded, modelProvider, model, effort) {
1926
+ await this.assertModelProvider(loaded, modelProvider);
1927
+ const selected = (await loaded.runtime.listModels()).find((candidate) => candidate.id === model);
1928
+ if (!selected)
1929
+ throw new Error(`未知模型:${model}`);
1930
+ if (!selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
1931
+ const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
1932
+ throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
1933
+ }
1934
+ }
1935
+ async assertModelProvider(loaded, modelProvider) {
1936
+ const providers = await this.modelProviderOptions(loaded);
1937
+ if (!providers.some((provider) => provider.id === modelProvider)) {
1938
+ throw new Error(`未知 Provider:${modelProvider}`);
1939
+ }
1940
+ }
1941
+ async modelProviderOptions(loaded) {
1942
+ if (loaded.runtime.kind !== "codex" || !loaded.runtime.listModelProviders) {
1943
+ throw new Error("当前任务不支持 Provider 设置。");
1944
+ }
1945
+ const providers = await loaded.runtime.listModelProviders();
1946
+ const current = loaded.session.modelProvider?.trim();
1947
+ if (current && !providers.some((provider) => provider.id === current)) {
1948
+ providers.unshift({ id: current });
1949
+ }
1950
+ return providers;
1951
+ }
1952
+ async model(contextKey, model, options = {}) {
1953
+ if (!model) {
1954
+ await this.openExecutionSettings(contextKey, "model", options);
1614
1955
  return;
1615
1956
  }
1957
+ const record = options.sessionId
1958
+ ? this.requireSession(contextKey, options.sessionId)
1959
+ : this.requireCurrentSession(contextKey);
1960
+ const loaded = await this.loadSession(record);
1961
+ const models = await loaded.runtime.listModels();
1616
1962
  const selected = models.find((item) => item.id === model);
1617
1963
  if (!selected)
1618
1964
  throw new Error(`未知模型:${model}`);
@@ -1630,14 +1976,11 @@ export class ProxySessionController {
1630
1976
  ? `,思考强度已自动调整为 ${nextEffort}`
1631
1977
  : "";
1632
1978
  if (options.updateMessageId) {
1633
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
1979
+ await this.openExecutionSettings(contextKey, "model", {
1634
1980
  sessionId: loaded.record.localSessionId,
1635
- contextKey,
1636
- model,
1637
- currentEffort: nextEffort,
1638
- options: selected.supportedReasoningEfforts,
1639
- notice: `模型已切换为 ${cardCode(model)},请选择思考模式。`,
1640
- }));
1981
+ updateMessageId: options.updateMessageId,
1982
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
1983
+ });
1641
1984
  return;
1642
1985
  }
1643
1986
  await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
@@ -1653,8 +1996,8 @@ export class ProxySessionController {
1653
1996
  }
1654
1997
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1655
1998
  const agent = this.ensureAgent(context.defaultAgent);
1656
- if (agent.kind !== "codex")
1657
- throw new Error("Goal 模式仅支持 Codex App Server 任务。");
1999
+ if (agent.kind !== "app-server")
2000
+ throw new Error("Goal 模式仅支持 App Server 任务。");
1658
2001
  record = await this.createSession(contextKey, context.defaultAgent, this.inheritedNewTaskCwd(contextKey), false, false, objective);
1659
2002
  }
1660
2003
  const loaded = await this.loadSession(record);
@@ -1785,6 +2128,10 @@ export class ProxySessionController {
1785
2128
  });
1786
2129
  }
1787
2130
  async thinking(contextKey, effort, options = {}) {
2131
+ if (!effort) {
2132
+ await this.openExecutionSettings(contextKey, "thinking", options);
2133
+ return;
2134
+ }
1788
2135
  const record = options.sessionId
1789
2136
  ? this.requireSession(contextKey, options.sessionId)
1790
2137
  : this.requireCurrentSession(contextKey);
@@ -1798,22 +2145,6 @@ export class ProxySessionController {
1798
2145
  throw new Error("模型已发生变化,请重新打开 /model。");
1799
2146
  }
1800
2147
  const supported = currentModel.supportedReasoningEfforts;
1801
- if (!effort) {
1802
- const card = this.cardRenderer.renderReasoningSelector({
1803
- sessionId: loaded.record.localSessionId,
1804
- contextKey,
1805
- model: currentModel.id,
1806
- currentEffort: loaded.session.reasoningEffort ?? currentModel.defaultReasoningEffort,
1807
- options: supported,
1808
- });
1809
- if (options.updateMessageId) {
1810
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, card);
1811
- }
1812
- else {
1813
- await this.outbound.sendInteractiveCard(contextKey, card);
1814
- }
1815
- return;
1816
- }
1817
2148
  if (!supported.some((option) => option.value === effort)) {
1818
2149
  const options = supported.map((option) => option.value).join("、") || "无";
1819
2150
  throw new Error(`不支持的思考强度:${effort}。支持的强度:${options}`);
@@ -1821,79 +2152,85 @@ export class ProxySessionController {
1821
2152
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
1822
2153
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
1823
2154
  if (options.updateMessageId) {
1824
- await this.outbound.updateInteractiveCard(contextKey, options.updateMessageId, this.cardRenderer.renderReasoningSelector({
2155
+ await this.openExecutionSettings(contextKey, "thinking", {
1825
2156
  sessionId: loaded.record.localSessionId,
1826
- contextKey,
1827
- model: currentModel.id,
1828
- currentEffort: effort,
1829
- options: supported,
1830
- notice: `思考模式已切换为 ${cardCode(effort)},从下一次请求生效。`,
1831
- }));
2157
+ updateMessageId: options.updateMessageId,
2158
+ notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
2159
+ });
1832
2160
  return;
1833
2161
  }
1834
2162
  await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
1835
2163
  }
1836
- async permissions(contextKey, mode) {
1837
- const record = this.requireCurrentSession(contextKey);
2164
+ async permissions(contextKey, mode, options = {}) {
1838
2165
  if (!mode) {
1839
- await this.outbound.sendText(contextKey, `当前权限模式:${record.permissionMode ?? "auto"}`);
2166
+ await this.openExecutionSettings(contextKey, "permission", options);
1840
2167
  return;
1841
2168
  }
2169
+ const record = options.sessionId
2170
+ ? this.requireSession(contextKey, options.sessionId)
2171
+ : this.requireCurrentSession(contextKey);
1842
2172
  const loaded = await this.loadSession(record);
1843
2173
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
2174
+ loaded.session.permissionMode = mode;
1844
2175
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
2176
+ if (options.updateMessageId) {
2177
+ await this.openExecutionSettings(contextKey, "permission", {
2178
+ sessionId: loaded.record.localSessionId,
2179
+ updateMessageId: options.updateMessageId,
2180
+ notice: mode === "auto"
2181
+ ? "已切换为自动执行模式,从下一次请求生效。"
2182
+ : "已切换为执行前确认模式,从下一次请求生效。",
2183
+ });
2184
+ return;
2185
+ }
1845
2186
  await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
1846
2187
  }
1847
- async listAgents(contextKey) {
1848
- const current = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent).defaultAgent;
1849
- const lines = Object.entries(this.config.agents).map(([name, agent]) => {
1850
- const selected = name === current;
1851
- return `- ${selected ? "✅ " : ""}${asInlineCode(name)}:${agent.title}${selected ? "(当前)" : ""}`;
1852
- });
1853
- await this.outbound.sendMarkdown(contextKey, [
1854
- `当前 Agent:${asInlineCode(current)}`,
1855
- "可用 Agent:",
1856
- lines.join("\n") || "无",
1857
- ].join("\n"));
1858
- }
1859
2188
  async listSessions(contextKey, searchTerm, options = {}) {
1860
2189
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
1861
2190
  const normalizedSearch = searchTerm?.trim().toLowerCase();
1862
2191
  const visibleCount = options.visibleCount ?? SESSION_PAGE_SIZE;
1863
2192
  const localSessions = this.store.listSessions(contextKey).filter((session) => session.status !== "closed" && this.isCodexSession(session));
1864
- let remoteSessions = [];
1865
- let remoteHint;
2193
+ const remoteSessions = [];
2194
+ const remoteErrors = [];
1866
2195
  let remoteHasMore = false;
1867
- const runtime = this.runtimes.get("codex");
1868
- if (runtime.listRemoteSessions) {
2196
+ await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
2197
+ if (!runtime.listRemoteSessions)
2198
+ return;
1869
2199
  try {
1870
2200
  const page = await runtime.listRemoteSessions({ searchTerm, limit: visibleCount });
1871
- remoteSessions = page.sessions;
1872
- remoteHasMore = Boolean(page.nextCursor);
2201
+ remoteSessions.push(...page.sessions.map((session) => ({ agentName, session })));
2202
+ remoteHasMore ||= Boolean(page.nextCursor);
1873
2203
  }
1874
2204
  catch (error) {
1875
- this.logger.warn({ error, contextKey }, "Failed to list Codex sessions.");
1876
- remoteHint = `读取 Codex 任务失败:${error instanceof Error ? error.message : String(error)}`;
2205
+ this.logger.warn({ error, contextKey, agentName }, "Failed to list Codex sessions for an Agent.");
2206
+ remoteErrors.push(`${agentName}: ${error instanceof Error ? error.message : String(error)}`);
1877
2207
  }
1878
- }
2208
+ }));
2209
+ const remoteHint = remoteErrors.length > 0
2210
+ ? `部分 Agent 的任务读取失败:${remoteErrors.join(";")}`
2211
+ : undefined;
1879
2212
  const entries = mergeTaskList(localSessions, remoteSessions, context.currentSessionId)
1880
- .filter((entry) => !normalizedSearch || [entry.id, entry.title, entry.cwd].some((value) => value.toLowerCase().includes(normalizedSearch)))
2213
+ .filter((entry) => !normalizedSearch
2214
+ || [entry.id, entry.title, entry.cwd, entry.agentName]
2215
+ .some((value) => value.toLowerCase().includes(normalizedSearch)))
1881
2216
  .sort((left, right) => Number(right.current) - Number(left.current)
1882
2217
  || Number(right.active) - Number(left.active)
1883
2218
  || right.updatedAt - left.updatedAt);
1884
2219
  const activeCount = entries.filter((entry) => entry.active).length;
1885
2220
  const visibleEntries = entries.slice(0, visibleCount);
1886
2221
  const hasMore = remoteHasMore || entries.length > visibleCount;
1887
- this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.id));
2222
+ this.lastSessionListings.set(contextKey, visibleEntries.map((entry) => entry.reference));
1888
2223
  const cardEntries = visibleEntries.map((entry, index) => {
1889
2224
  const marker = entry.current ? "✅" : entry.active ? "🟢 **活跃**" : "•";
1890
- const showStop = entry.status === "外部执行中" && entry.id !== options.forceSwitchTaskId;
2225
+ const showStop = entry.status === "外部执行中"
2226
+ && entry.reference !== options.forceSwitchTaskId
2227
+ && entry.id !== options.forceSwitchTaskId;
1891
2228
  const actions = entry.current ? [] : [{
1892
2229
  text: showStop ? "Stop" : "Switch",
1893
2230
  type: showStop ? "danger" : "default",
1894
2231
  value: {
1895
2232
  action: showStop ? "session_stop" : "session_switch",
1896
- sessionId: entry.id,
2233
+ sessionId: entry.reference,
1897
2234
  ...(searchTerm ? { searchTerm } : {}),
1898
2235
  visibleCount: String(visibleCount),
1899
2236
  contextKey,
@@ -1903,7 +2240,7 @@ export class ProxySessionController {
1903
2240
  text: "New",
1904
2241
  value: {
1905
2242
  action: "session_new",
1906
- sessionId: entry.id,
2243
+ sessionId: entry.reference,
1907
2244
  ...(searchTerm ? { searchTerm } : {}),
1908
2245
  visibleCount: String(visibleCount),
1909
2246
  contextKey,
@@ -1913,7 +2250,7 @@ export class ProxySessionController {
1913
2250
  text: "NewGroup",
1914
2251
  value: {
1915
2252
  action: "session_new_group",
1916
- sessionId: entry.id,
2253
+ sessionId: entry.reference,
1917
2254
  ...(searchTerm ? { searchTerm } : {}),
1918
2255
  visibleCount: String(visibleCount),
1919
2256
  contextKey,
@@ -1923,7 +2260,7 @@ export class ProxySessionController {
1923
2260
  text: "Fork",
1924
2261
  value: {
1925
2262
  action: "session_fork",
1926
- sessionId: entry.id,
2263
+ sessionId: entry.reference,
1927
2264
  ...(searchTerm ? { searchTerm } : {}),
1928
2265
  visibleCount: String(visibleCount),
1929
2266
  contextKey,
@@ -1933,7 +2270,7 @@ export class ProxySessionController {
1933
2270
  text: "ForkGroup",
1934
2271
  value: {
1935
2272
  action: "session_fork_group",
1936
- sessionId: entry.id,
2273
+ sessionId: entry.reference,
1937
2274
  ...(searchTerm ? { searchTerm } : {}),
1938
2275
  visibleCount: String(visibleCount),
1939
2276
  contextKey,
@@ -1943,14 +2280,14 @@ export class ProxySessionController {
1943
2280
  text: "Status",
1944
2281
  value: {
1945
2282
  action: "session_status",
1946
- sessionId: entry.id,
2283
+ sessionId: entry.reference,
1947
2284
  contextKey,
1948
2285
  },
1949
2286
  });
1950
2287
  return {
1951
2288
  lines: [
1952
2289
  `**${index + 1}.** ${marker} **${cardText(entry.title)}** ${cardText(entry.id)}`,
1953
- `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.cwd || "目录未知")}`,
2290
+ `${entry.status} · ${entry.updatedLabel} · ${cardText(entry.agentName)} · ${cardText(entry.cwd || "目录未知")}`,
1954
2291
  ],
1955
2292
  actions,
1956
2293
  };
@@ -2003,6 +2340,45 @@ export class ProxySessionController {
2003
2340
  forceSwitchTaskId,
2004
2341
  });
2005
2342
  }
2343
+ findStoredSessionByReference(reference, contextKey) {
2344
+ const scoped = parseRemoteSessionReference(reference);
2345
+ return scoped
2346
+ ? this.store.findSessionByRemoteSessionId(scoped.remoteSessionId, contextKey, scoped.agentName)
2347
+ : this.store.findSessionByRemoteSessionId(reference, contextKey);
2348
+ }
2349
+ async resolveRemoteCodexSession(reference) {
2350
+ const scoped = parseRemoteSessionReference(reference);
2351
+ const candidates = scoped
2352
+ ? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)]]
2353
+ : this.runtimes.entries("codex");
2354
+ if (candidates.length === 0)
2355
+ throw new Error("未配置 Codex Agent。");
2356
+ const reads = await Promise.allSettled(candidates.map(async ([agentName, runtime]) => {
2357
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
2358
+ throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
2359
+ }
2360
+ return {
2361
+ agentName,
2362
+ runtime,
2363
+ remote: await runtime.readRemoteSession(scoped?.remoteSessionId ?? reference),
2364
+ };
2365
+ }));
2366
+ const matches = reads
2367
+ .filter((result) => result.status === "fulfilled")
2368
+ .map((result) => result.value);
2369
+ if (matches.length === 1)
2370
+ return matches[0];
2371
+ if (matches.length > 1) {
2372
+ throw new Error(`多个 Agent 中存在相同任务 ID:${reference}。请先使用 /sessions,再通过序号选择任务。`);
2373
+ }
2374
+ const details = reads
2375
+ .map((result, index) => result.status === "rejected"
2376
+ ? `${candidates[index]?.[0] ?? "unknown"}: ${runtimeErrorMessage(result.reason)}`
2377
+ : undefined)
2378
+ .filter((detail) => Boolean(detail))
2379
+ .join(";");
2380
+ throw new Error(`找不到 Codex 任务:${scoped?.remoteSessionId ?? reference}${details ? `(${details})` : ""}`);
2381
+ }
2006
2382
  async switchSession(contextKey, reference) {
2007
2383
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2008
2384
  const taskId = this.resolveSessionReference(contextKey, reference);
@@ -2010,29 +2386,22 @@ export class ProxySessionController {
2010
2386
  if (direct?.status === "closed") {
2011
2387
  throw new Error(`找不到任务:${taskId}`);
2012
2388
  }
2013
- const existing = direct ?? this.store.findSessionByRemoteSessionId(taskId);
2389
+ const existing = direct ?? this.findStoredSessionByReference(taskId);
2014
2390
  if (existing) {
2015
- const runtime = this.runtimes.forAgent(this.ensureAgent(existing.agentName));
2391
+ const runtime = this.runtimes.forAgent(existing.agentName);
2016
2392
  await this.assertSessionTurnOwnership(existing, runtime);
2017
2393
  this.store.attachSessionToContext(contextKey, existing.localSessionId);
2018
2394
  this.store.setCurrentSession(contextKey, existing.localSessionId);
2019
- this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd);
2395
+ this.outbound.registerSession(existing.localSessionId, contextKey, existing.title, existing.cwd, this.agentLabel(existing.agentName));
2020
2396
  await this.outbound.sendText(contextKey, `已切换到任务:${existing.title ?? existing.remoteSessionId ?? taskId}`);
2021
2397
  return;
2022
2398
  }
2023
- const runtime = this.runtimes.get("codex");
2024
- if (!runtime.readRemoteSession)
2025
- throw new Error("当前 Codex 运行时不支持读取任务。");
2026
- const remote = await runtime.readRemoteSession(taskId);
2399
+ const { agentName, remote } = await this.resolveRemoteCodexSession(taskId);
2027
2400
  if (remote.status === "active" || remote.lastTurnStatus === "inProgress") {
2028
2401
  throw new Error(`这个任务正在外部 Codex 中执行,当前不会切换。可使用 /status ${taskId} 查看进度。`);
2029
2402
  }
2030
2403
  if (!remote.cwd)
2031
2404
  throw new Error("这个 Codex 任务没有可用的工作目录,暂时无法切换。");
2032
- const agentEntry = Object.entries(this.config.agents).find(([, agent]) => agent.kind === "codex");
2033
- if (!agentEntry)
2034
- throw new Error("未配置 Codex Agent。");
2035
- const [agentName] = agentEntry;
2036
2405
  const localSessionId = createId("sess");
2037
2406
  this.store.createSession({
2038
2407
  localSessionId,
@@ -2050,7 +2419,7 @@ export class ProxySessionController {
2050
2419
  lastTurnStatus: mapRemoteTurnStatus(remote.lastTurnStatus),
2051
2420
  });
2052
2421
  this.store.setCurrentSession(contextKey, localSessionId);
2053
- this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd);
2422
+ this.outbound.registerSession(localSessionId, contextKey, remote.title ?? remote.preview, remote.cwd, this.agentLabel(agentName));
2054
2423
  await this.outbound.sendText(contextKey, `已切换到任务:${remote.title ?? remote.preview ?? remote.id}。历史消息不会重新发送。`);
2055
2424
  }
2056
2425
  async stopSessionReference(contextKey, taskId) {
@@ -2061,16 +2430,15 @@ export class ProxySessionController {
2061
2430
  await this.cancelSession({ ...direct, contextKey });
2062
2431
  return;
2063
2432
  }
2064
- const existing = this.store.findSessionByRemoteSessionId(taskId);
2433
+ const existing = this.findStoredSessionByReference(taskId);
2065
2434
  if (existing) {
2066
2435
  await this.cancelSession({ ...existing, contextKey });
2067
2436
  return;
2068
2437
  }
2069
- const runtime = this.runtimes.get("codex");
2438
+ const { runtime, remote } = await this.resolveRemoteCodexSession(taskId);
2070
2439
  if (!runtime.readRemoteSession || !runtime.interruptRemoteTurn) {
2071
2440
  throw new Error("当前 Codex 运行时不支持停止外部任务。");
2072
2441
  }
2073
- const remote = await runtime.readRemoteSession(taskId);
2074
2442
  const turnId = remote.status === "active" || remote.lastTurnStatus === "inProgress"
2075
2443
  ? remote.lastTurnId
2076
2444
  : undefined;
@@ -2147,11 +2515,18 @@ export class ProxySessionController {
2147
2515
  }
2148
2516
  return undefined;
2149
2517
  }
2150
- async setDefaultAgent(contextKey, agentName) {
2518
+ async setDefaultAgent(contextKey, agentName, options = {}) {
2151
2519
  this.ensureAgent(agentName);
2152
2520
  this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
2153
2521
  this.store.setDefaultAgent(contextKey, agentName);
2154
- await this.outbound.sendText(contextKey, `默认 agent 已切换为:${agentName}`);
2522
+ if (Object.keys(this.config.agents).length <= 1) {
2523
+ await this.outbound.sendText(contextKey, `当前 Agent:${agentName}\n当前没有其他 Agent 可以切换。`);
2524
+ return;
2525
+ }
2526
+ await this.openExecutionSettings(contextKey, "agent", {
2527
+ ...options,
2528
+ notice: `默认 Agent 已切换为 ${cardCode(agentName)},从下一次新建任务生效。`,
2529
+ });
2155
2530
  }
2156
2531
  async status(contextKey, sessionId, options = {}) {
2157
2532
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
@@ -2161,7 +2536,7 @@ export class ProxySessionController {
2161
2536
  const direct = this.store.getSession(targetSessionId);
2162
2537
  current = direct
2163
2538
  ? { ...direct, contextKey }
2164
- : this.store.findSessionByRemoteSessionId(targetSessionId, contextKey);
2539
+ : this.findStoredSessionByReference(targetSessionId, contextKey);
2165
2540
  if (!current) {
2166
2541
  await this.statusForCodexTask(contextKey, targetSessionId, options);
2167
2542
  return;
@@ -2172,7 +2547,7 @@ export class ProxySessionController {
2172
2547
  }
2173
2548
  if (current && current.status === "running") {
2174
2549
  try {
2175
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2550
+ const runtime = this.runtimes.forAgent(current.agentName);
2176
2551
  if (runtime.getSession(current.localSessionId)) {
2177
2552
  await runtime.synchronizeSession(current.localSessionId);
2178
2553
  current = this.store.getSession(current.localSessionId) ?? current;
@@ -2186,7 +2561,7 @@ export class ProxySessionController {
2186
2561
  let remote;
2187
2562
  let goal;
2188
2563
  if (current?.remoteSessionId) {
2189
- const runtime = this.runtimes.forAgent(this.ensureAgent(current.agentName));
2564
+ const runtime = this.runtimes.forAgent(current.agentName);
2190
2565
  if (runtime.readRemoteSession) {
2191
2566
  try {
2192
2567
  remote = await runtime.readRemoteSession(current.remoteSessionId);
@@ -2216,7 +2591,7 @@ export class ProxySessionController {
2216
2591
  }
2217
2592
  else {
2218
2593
  const agent = this.ensureAgent(current.agentName);
2219
- const runtimeSession = this.runtimes.forAgent(agent).getSession(current.localSessionId);
2594
+ const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
2220
2595
  activeTurnId = runtimeSession?.activeTurnId;
2221
2596
  const remoteActive = isRemoteSessionActive(remote);
2222
2597
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
@@ -2228,7 +2603,7 @@ export class ProxySessionController {
2228
2603
  const resultLabel = remoteActive
2229
2604
  ? "执行中"
2230
2605
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
2231
- taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**模型 / 思考强度**:${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**Codex 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
2606
+ taskLines.push(`**标题**:${cardCode(current.title ?? "未命名任务")}`, `**工作目录**:${cardCode(current.cwd)}`, `**Provider / 模型 / 思考强度**:${cardCode(current.modelProvider ?? "Codex 默认")} / ${cardCode(current.model ?? "默认")} / ${cardCode(current.reasoningEffort ?? "自动")}`, `**状态 / 最近结果**:${statusLabel} / ${resultLabel}`, `**Agent**:${cardCode(agent.title)}`, `**权限 / 任务范围**:${current.permissionMode === "confirm" ? "执行前确认" : "自动执行"} / ${detectProjectlessWorkspace(current.cwd) ? "未指定项目" : "指定项目"}`, `**Codex 任务 ID**:${cardCode(current.remoteSessionId ?? "尚未创建")}`, `**当前执行 / 排队消息**:${activeTurnId ? cardCode(activeTurnId) : "无"} / ${queued} 条`, `**创建时间 / 最近活动**:${formatStatusTime(current.createdAt)} / ${formatStatusTime(current.updatedAt)}`);
2232
2607
  }
2233
2608
  const sections = [
2234
2609
  { title: targetSessionId ? "指定任务" : "当前任务", lines: taskLines },
@@ -2257,12 +2632,15 @@ export class ProxySessionController {
2257
2632
  const title = targetSessionId && current
2258
2633
  ? `Codex 状态:${truncateText((current.title ?? current.remoteSessionId ?? current.localSessionId).replace(/\s+/g, " "), 40)}`
2259
2634
  : "Codex 状态";
2260
- const taskId = current?.remoteSessionId ?? current?.localSessionId;
2635
+ const taskId = current?.remoteSessionId
2636
+ ? remoteSessionReference(current.agentName, current.remoteSessionId)
2637
+ : current?.localSessionId;
2261
2638
  const isCurrent = Boolean(current && current.localSessionId === context.currentSessionId);
2262
2639
  const remoteActive = isRemoteSessionActive(remote);
2263
2640
  const botOwnsActiveTurn = Boolean(localCurrent && remote && isBotOwnedActiveTurn(localCurrent, remote));
2264
2641
  const active = Boolean(activeTurnId || current?.status === "running" || remoteActive);
2265
- const forceSwitch = Boolean(taskId && options.forceSwitchTaskId === taskId);
2642
+ const forceSwitch = Boolean(taskId && (options.forceSwitchTaskId === taskId
2643
+ || options.forceSwitchTaskId === current?.remoteSessionId));
2266
2644
  const taskActions = !taskId ? []
2267
2645
  : remoteActive && !botOwnsActiveTurn && !forceSwitch
2268
2646
  ? [statusCardAction("Stop", "danger", "session_stop", taskId, contextKey)]
@@ -2281,11 +2659,9 @@ export class ProxySessionController {
2281
2659
  else
2282
2660
  await this.outbound.sendInteractiveCard(contextKey, card);
2283
2661
  }
2284
- async statusForCodexTask(contextKey, remoteSessionId, options = {}) {
2285
- const runtime = this.runtimes.get("codex");
2286
- if (!runtime.readRemoteSession)
2287
- throw new Error("当前 Codex 运行时不支持读取外部任务状态。");
2288
- const remote = await runtime.readRemoteSession(remoteSessionId);
2662
+ async statusForCodexTask(contextKey, reference, options = {}) {
2663
+ const { agentName, remote } = await this.resolveRemoteCodexSession(reference);
2664
+ const actionReference = remoteSessionReference(agentName, remote.id);
2289
2665
  const sections = [
2290
2666
  {
2291
2667
  title: "指定任务",
@@ -2293,6 +2669,7 @@ export class ProxySessionController {
2293
2669
  `**标题**:${cardCode(remote.title ?? remote.preview ?? "未命名任务")}`,
2294
2670
  `**工作目录**:${cardCode(remote.cwd || "目录未知")}`,
2295
2671
  `**状态 / 当前任务**:${remoteSessionDetailStatus(remote)} / 未切换`,
2672
+ `**Agent**:${cardCode(agentName)}`,
2296
2673
  `**Codex 任务 ID**:${cardCode(remote.id)}`,
2297
2674
  `**最近回合**:${cardCode(remote.lastTurnId ?? "无")} ${remoteTurnStatusLabel(remote.lastTurnStatus)}`,
2298
2675
  `**创建时间 / 最近活动**:${formatRemoteTime(remote.createdAt)} / ${formatRemoteTime(latestRemoteTimestamp(remote.recencyAt, remote.updatedAt))}`,
@@ -2312,10 +2689,12 @@ export class ProxySessionController {
2312
2689
  },
2313
2690
  ];
2314
2691
  const title = `Codex 状态:${truncateText((remote.title ?? remote.preview ?? remote.id).replace(/\s+/g, " "), 40)}`;
2315
- const showStop = isRemoteSessionActive(remote) && options.forceSwitchTaskId !== remote.id;
2692
+ const showStop = isRemoteSessionActive(remote)
2693
+ && options.forceSwitchTaskId !== actionReference
2694
+ && options.forceSwitchTaskId !== remote.id;
2316
2695
  const actions = [
2317
- statusRefreshAction(remote.id, contextKey),
2318
- statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", remote.id, contextKey),
2696
+ statusRefreshAction(actionReference, contextKey),
2697
+ statusCardAction(showStop ? "Stop" : "Switch", showStop ? "danger" : "default", showStop ? "session_stop" : "session_switch", actionReference, contextKey),
2319
2698
  ];
2320
2699
  const card = this.cardRenderer.renderSectionsCard(title, sections, actions);
2321
2700
  if (options.updateMessageId)
@@ -2338,7 +2717,7 @@ export class ProxySessionController {
2338
2717
  title: "任务管理",
2339
2718
  lines: [
2340
2719
  "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
2341
- "**/newgroup [title]** 创建飞书群和新任务;继承当前项目与执行设置",
2720
+ "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
2342
2721
  "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
2343
2722
  "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
2344
2723
  "**/title &#60;新标题&#62;** 修改当前任务的标题",
@@ -2353,22 +2732,23 @@ export class ProxySessionController {
2353
2732
  "**/stop** 停止当前执行",
2354
2733
  "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
2355
2734
  "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
2356
- "**/model [name]** 查看或切换模型",
2357
- "**/thinking [level]** 查看或设置思考强度",
2358
- "**/permissions [auto|confirm]** 查看或切换确认模式",
2735
+ "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
2736
+ "**/model** 选择当前任务使用的模型",
2737
+ "**/thinking** 设置模型的思考强度",
2738
+ "**/permissions** 设置执行工具前是否需要确认",
2359
2739
  ],
2360
2740
  },
2361
2741
  {
2362
2742
  title: "Agent",
2363
2743
  lines: [
2364
- "**/agent [name]** 查看或设置新任务的默认 Agent",
2744
+ "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
2365
2745
  ],
2366
2746
  },
2367
2747
  {
2368
2748
  title: "系统",
2369
2749
  lines: [
2370
2750
  "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
2371
- "**/restart** 重启 Agent Bot 并恢复会话",
2751
+ "**/restart [--force]** 默认安全重启;--force 立即重启",
2372
2752
  "**/help** 显示本帮助",
2373
2753
  ],
2374
2754
  },
@@ -2381,10 +2761,13 @@ export class ProxySessionController {
2381
2761
  throw new Error(`未知 agent:${agentName}`);
2382
2762
  return agent;
2383
2763
  }
2764
+ agentLabel(agentName) {
2765
+ return this.ensureAgent(agentName).title;
2766
+ }
2384
2767
  isCodexSession(session) {
2385
2768
  if (session.runtimeKind)
2386
2769
  return session.runtimeKind === "codex";
2387
- return this.config.agents[session.agentName]?.kind === "codex";
2770
+ return this.config.agents[session.agentName]?.kind === "app-server";
2388
2771
  }
2389
2772
  currentSession(contextKey) {
2390
2773
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent);
@@ -2400,6 +2783,17 @@ export class ProxySessionController {
2400
2783
  ? createProjectlessWorkspace().cwd
2401
2784
  : current.cwd;
2402
2785
  }
2786
+ inheritedExecutionSettings(contextKey, agentName) {
2787
+ const current = this.currentSession(contextKey);
2788
+ if (!current || current.agentName !== agentName)
2789
+ return {};
2790
+ return {
2791
+ modelProvider: current.modelProvider,
2792
+ model: current.model,
2793
+ reasoningEffort: current.reasoningEffort,
2794
+ permissionMode: current.permissionMode,
2795
+ };
2796
+ }
2403
2797
  currentProjectCwd(contextKey) {
2404
2798
  const current = this.currentSession(contextKey);
2405
2799
  return current && !detectProjectlessWorkspace(current.cwd) ? current.cwd : undefined;
@@ -2454,10 +2848,10 @@ function statusRefreshAction(sessionId, contextKey) {
2454
2848
  function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2455
2849
  const localByRemoteId = new Map(localSessions
2456
2850
  .filter((session) => session.runtimeKind === "codex" && session.remoteSessionId)
2457
- .map((session) => [session.remoteSessionId, session]));
2851
+ .map((session) => [agentRemoteKey(session.agentName, session.remoteSessionId), session]));
2458
2852
  const representedLocalIds = new Set();
2459
- const entries = remoteSessions.map((remote) => {
2460
- const local = localByRemoteId.get(remote.id);
2853
+ const entries = remoteSessions.map(({ agentName, session: remote }) => {
2854
+ const local = localByRemoteId.get(agentRemoteKey(agentName, remote.id));
2461
2855
  if (local)
2462
2856
  representedLocalIds.add(local.localSessionId);
2463
2857
  const active = remote.status === "active" || remote.lastTurnStatus === "inProgress";
@@ -2466,7 +2860,9 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2466
2860
  : remoteSessionStatusLabel(remote.status);
2467
2861
  const recencyAt = remote.recencyAt ?? remote.updatedAt;
2468
2862
  return {
2863
+ reference: remoteSessionReference(agentName, remote.id),
2469
2864
  id: remote.id,
2865
+ agentName,
2470
2866
  title: remote.title ?? remote.preview ?? local?.title ?? "未命名任务",
2471
2867
  cwd: remote.cwd || local?.cwd || "",
2472
2868
  status,
@@ -2481,7 +2877,11 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2481
2877
  continue;
2482
2878
  const updatedAt = Date.parse(local.updatedAt);
2483
2879
  entries.push({
2880
+ reference: local.remoteSessionId
2881
+ ? remoteSessionReference(local.agentName, local.remoteSessionId)
2882
+ : local.localSessionId,
2484
2883
  id: local.remoteSessionId ?? local.localSessionId,
2884
+ agentName: local.agentName,
2485
2885
  title: local.title ?? "未命名任务",
2486
2886
  cwd: local.cwd,
2487
2887
  status: sessionStatusLabel(local.status),
@@ -2493,6 +2893,31 @@ function mergeTaskList(localSessions, remoteSessions, currentLocalSessionId) {
2493
2893
  }
2494
2894
  return entries;
2495
2895
  }
2896
+ function agentRemoteKey(agentName, remoteSessionId) {
2897
+ return `${agentName}\u0000${remoteSessionId}`;
2898
+ }
2899
+ function remoteSessionReference(agentName, remoteSessionId) {
2900
+ return `${REMOTE_SESSION_REFERENCE_PREFIX}${encodeURIComponent(agentName)}:${encodeURIComponent(remoteSessionId)}`;
2901
+ }
2902
+ function parseRemoteSessionReference(reference) {
2903
+ if (!reference.startsWith(REMOTE_SESSION_REFERENCE_PREFIX))
2904
+ return undefined;
2905
+ const value = reference.slice(REMOTE_SESSION_REFERENCE_PREFIX.length);
2906
+ const separator = value.indexOf(":");
2907
+ if (separator <= 0 || separator === value.length - 1)
2908
+ return undefined;
2909
+ try {
2910
+ const agentName = decodeURIComponent(value.slice(0, separator));
2911
+ const remoteSessionId = decodeURIComponent(value.slice(separator + 1));
2912
+ return agentName && remoteSessionId ? { agentName, remoteSessionId } : undefined;
2913
+ }
2914
+ catch {
2915
+ return undefined;
2916
+ }
2917
+ }
2918
+ function runtimeErrorMessage(error) {
2919
+ return error instanceof Error ? error.message : String(error);
2920
+ }
2496
2921
  function isMissingRolloutError(error) {
2497
2922
  const message = error instanceof Error ? error.message : String(error);
2498
2923
  return /no rollout found|rollout[^\n]*(?:not found|missing)|thread\/resume failed/i.test(message);
@@ -2671,6 +3096,21 @@ function cardText(value) {
2671
3096
  function cardCode(value) {
2672
3097
  return `\`${value.replaceAll("`", "'")}\``;
2673
3098
  }
3099
+ function permissionModeValue(value) {
3100
+ if (value === "auto" || value === "confirm")
3101
+ return value;
3102
+ throw new Error("权限模式只能是 auto 或 confirm。");
3103
+ }
3104
+ function executionSettingsTabValue(value) {
3105
+ if (value === "agent" || value === "provider" || value === "model" || value === "thinking" || value === "permission")
3106
+ return value;
3107
+ throw new Error("设置卡片的 tab 无效,请重新发送设置命令。");
3108
+ }
3109
+ function requiredCardMessageId(value) {
3110
+ if (value)
3111
+ return value;
3112
+ throw new Error("无法更新设置卡片,请重新发送设置命令。");
3113
+ }
2674
3114
  function remoteSessionStatusLabel(status) {
2675
3115
  const labels = {
2676
3116
  active: "执行中",
@@ -2707,6 +3147,10 @@ function mergeRemoteTaskStatus(record, remote) {
2707
3147
  ...record,
2708
3148
  title: remote.title ?? record.title,
2709
3149
  cwd: remote.cwd || record.cwd,
3150
+ modelProvider: remote.modelProvider ?? record.modelProvider,
3151
+ model: remote.model ?? record.model,
3152
+ reasoningEffort: remote.reasoningEffort ?? record.reasoningEffort,
3153
+ permissionMode: remote.permissionMode ?? record.permissionMode,
2710
3154
  status: isRemoteSessionActive(remote)
2711
3155
  ? "running"
2712
3156
  : remote.lastTurnStatus
@@ -2758,12 +3202,8 @@ function isQueueIndependentCommand(command) {
2758
3202
  return true;
2759
3203
  if (command.type === "agent")
2760
3204
  return command.agent === undefined;
2761
- if (command.type === "model")
2762
- return command.model === undefined;
2763
- if (command.type === "thinking")
2764
- return command.effort === undefined;
2765
- if (command.type === "permissions")
2766
- return command.mode === undefined;
3205
+ if (["model", "provider", "thinking", "permissions"].includes(command.type))
3206
+ return true;
2767
3207
  return false;
2768
3208
  }
2769
3209
  function parseSessionVisibleCount(value) {
@@ -2771,7 +3211,7 @@ function parseSessionVisibleCount(value) {
2771
3211
  return Number.isSafeInteger(parsed) && parsed >= SESSION_PAGE_SIZE ? parsed : SESSION_PAGE_SIZE;
2772
3212
  }
2773
3213
  function isPromptCommand(command) {
2774
- return command.type === "ask" || command.type === "prompt" || command.type === "nosteer";
3214
+ return command.type === "prompt" || command.type === "nosteer";
2775
3215
  }
2776
3216
  function runtimePrompt(text, localImagePaths) {
2777
3217
  return localImagePaths?.length ? { text, localImagePaths } : text;
@@ -2783,8 +3223,8 @@ function formatGroupDateSuffix(date) {
2783
3223
  }
2784
3224
  function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimestamp) {
2785
3225
  const prefix = `[${agentName}] `;
2786
- const project = formatGroupProjectDirectory(projectCwd);
2787
- const titlePrefix = `${prefix}${project} `;
3226
+ const projectPrefix = projectCwd ? `${formatGroupProjectDirectory(projectCwd)} ` : "";
3227
+ const titlePrefix = `${prefix}${projectPrefix}`;
2788
3228
  const timestampSuffix = includeTimestamp ? ` (${formatGroupDateSuffix(date)})` : "";
2789
3229
  const availableTitleLength = FEISHU_GROUP_NAME_MAX_LENGTH
2790
3230
  - Array.from(titlePrefix).length
@@ -2795,7 +3235,7 @@ function formatNewGroupName(agentName, projectCwd, taskTitle, date, includeTimes
2795
3235
  const FEISHU_GROUP_NAME_MAX_LENGTH = 60;
2796
3236
  const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
2797
3237
  function formatGroupProjectDirectory(projectCwd) {
2798
- const value = projectCwd ? abbreviateHomeDirectory(projectCwd) : "Projectless";
3238
+ const value = abbreviateHomeDirectory(projectCwd);
2799
3239
  if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH)
2800
3240
  return `[${value}]`;
2801
3241
  const levels = value