@keyou007/agent-bot 0.1.24-alpha.0 → 0.1.24-alpha.2

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 (48) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +24 -4
  3. package/README.zh.md +24 -4
  4. package/dist/cli/SelfUpdateRunner.js +2 -0
  5. package/dist/cli/SelfUpdateRunner.js.map +1 -1
  6. package/dist/codex/CodexEventMapper.js +90 -2
  7. package/dist/codex/CodexEventMapper.js.map +1 -1
  8. package/dist/codex/CodexRuntime.js +160 -12
  9. package/dist/codex/CodexRuntime.js.map +1 -1
  10. package/dist/commands/CommandRouter.js +14 -1
  11. package/dist/commands/CommandRouter.js.map +1 -1
  12. package/dist/feishu/CardRenderer.js +42 -11
  13. package/dist/feishu/CardRenderer.js.map +1 -1
  14. package/dist/feishu/FeishuMarkdown.js +70 -4
  15. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  16. package/dist/feishu/FeishuTurnPresenter.js +2 -1
  17. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  18. package/dist/index.js +5 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/local-files/TurnPreviewPage.js +16 -1
  21. package/dist/local-files/TurnPreviewPage.js.map +1 -1
  22. package/dist/presentation/TurnStateReducer.js +37 -7
  23. package/dist/presentation/TurnStateReducer.js.map +1 -1
  24. package/dist/proxy/ProxySessionController.js +58 -38
  25. package/dist/proxy/ProxySessionController.js.map +1 -1
  26. package/dist/supervision/SafeRestartNotifier.js +2 -0
  27. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  28. package/dist/supervision/SafeRestartScheduler.js +7 -0
  29. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  30. package/npm-shrinkwrap.json +2 -2
  31. package/package.json +1 -1
  32. package/skills/agent-bot/SKILL.md +12 -0
  33. package/src/cli/SelfUpdateRunner.ts +2 -0
  34. package/src/codex/CodexEventMapper.ts +97 -3
  35. package/src/codex/CodexRuntime.ts +163 -13
  36. package/src/commands/CommandRouter.ts +13 -2
  37. package/src/commands/commandTypes.ts +2 -2
  38. package/src/feishu/CardRenderer.ts +48 -12
  39. package/src/feishu/FeishuMarkdown.ts +69 -3
  40. package/src/feishu/FeishuTurnPresenter.ts +1 -0
  41. package/src/index.ts +4 -0
  42. package/src/local-files/TurnPreviewPage.ts +16 -1
  43. package/src/presentation/TurnStateReducer.ts +49 -9
  44. package/src/presentation/turnViewTypes.ts +4 -0
  45. package/src/proxy/ProxySessionController.ts +61 -40
  46. package/src/runtime/types.ts +13 -1
  47. package/src/supervision/SafeRestartNotifier.ts +2 -0
  48. package/src/supervision/SafeRestartScheduler.ts +7 -0
@@ -54,11 +54,25 @@ function includeLocalFileLineReferences(
54
54
  projectCwd?: string,
55
55
  localFileUrl?: LocalFileUrlResolver,
56
56
  ): string {
57
- return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
58
- if (imageMarker) return link;
57
+ const codeSpans = inlineCodeSpans(markdown);
58
+ return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string, offset: number) => {
59
+ if (imageMarker || codeSpans.some(([start, end]) => offset >= start && offset < end)) return link;
59
60
 
60
61
  const target = unwrapMarkdownTarget(rawTarget.trim());
61
- if (!isLocalFileTarget(target)) return link;
62
+ if (!isLocalFileTarget(target)) {
63
+ const relative = resolveRelativeFileTarget(target, projectCwd);
64
+ if (!relative || isLocalImageTarget(relative.filePath)) return link;
65
+ const viewerUrl = localFileUrl?.(relative.filePath, relative.reference);
66
+ if (!viewerUrl) {
67
+ const displayPath = displayAbsolutePath(relative.filePath) + (relative.reference ?? "");
68
+ return label ? `${label}(${inlineCode(displayPath)})` : inlineCode(displayPath);
69
+ }
70
+ const url = new URL(viewerUrl);
71
+ if (relative.hash) url.hash = relative.hash;
72
+ const normalizedLabel = relative.reference && !label.endsWith(relative.reference)
73
+ ? `${label}${relative.reference}` : label;
74
+ return `[${normalizedLabel}](${url.toString()})`;
75
+ }
62
76
  if (isLocalImageTarget(target)) return link;
63
77
 
64
78
  const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
@@ -84,6 +98,58 @@ function includeLocalFileLineReferences(
84
98
  });
85
99
  }
86
100
 
101
+ interface RelativeFileTarget {
102
+ filePath: string;
103
+ reference?: string;
104
+ hash?: string;
105
+ }
106
+
107
+ function resolveRelativeFileTarget(target: string, projectCwd?: string): RelativeFileTarget | undefined {
108
+ if (!projectCwd || !target || /^(?:#|\?|\/\/)/u.test(target)) return undefined;
109
+ const pathApi = usesWindowsPaths(projectCwd) ? path.win32 : path.posix;
110
+ if (!pathApi.isAbsolute(projectCwd)) return undefined;
111
+ const suffixStart = target.search(/[?#]/u);
112
+ const pathTarget = suffixStart < 0 ? target : target.slice(0, suffixStart);
113
+ const lineSuffix = LOCAL_FILE_LINE_REFERENCE.exec(pathTarget)?.[1];
114
+ // A basename with a line suffix (report.md:12) is a file, not a URI scheme.
115
+ const fileWithLine = /^[^:/\\]+\.[^:/\\]+:\d+(?::\d+)?$/u.test(pathTarget);
116
+ if (!fileWithLine && /^[a-z][a-z\d+.-]*:/iu.test(target)) return undefined;
117
+ const encodedPath = lineSuffix ? pathTarget.slice(0, -lineSuffix.length) : pathTarget;
118
+ try {
119
+ const decodedPath = decodeURIComponent(encodedPath);
120
+ if (!decodedPath || /[\x00-\x1f\x7f]/u.test(decodedPath)
121
+ || pathApi.isAbsolute(decodedPath) || /^[a-z]:/iu.test(decodedPath)) return undefined;
122
+ const filePath = pathApi.resolve(projectCwd, decodedPath);
123
+ const hashStart = target.indexOf("#");
124
+ const hash = hashStart < 0 ? undefined : new URL(target.slice(hashStart), "http://localhost/").hash;
125
+ const hashLine = /^#L(\d+)(?:C\d+)?(?:-L?\d+(?:C\d+)?)?$/u.exec(hash ?? "")?.[1];
126
+ const validHashLine = hashLine && Number.isSafeInteger(Number(hashLine)) && Number(hashLine) > 0;
127
+ return {
128
+ filePath,
129
+ reference: validHashLine ? `:${Number(hashLine)}` : hash ? undefined : lineSuffix,
130
+ hash: validHashLine ? `#L${Number(hashLine)}` : hash,
131
+ };
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ function inlineCodeSpans(markdown: string): Array<[number, number]> {
138
+ const runs = [...markdown.matchAll(/`+/gu)];
139
+ const spans: Array<[number, number]> = [];
140
+ for (let index = 0; index < runs.length; index += 1) {
141
+ const opener = runs[index]!;
142
+ const backslashes = /\\*$/u.exec(markdown.slice(0, opener.index))?.[0].length ?? 0;
143
+ if (backslashes % 2 !== 0) continue;
144
+ const closing = runs.findIndex((run, candidate) => candidate > index && run[0].length === opener[0].length);
145
+ if (closing < 0) continue;
146
+ const closer = runs[closing]!;
147
+ spans.push([opener.index, closer.index + closer[0].length]);
148
+ index = closing;
149
+ }
150
+ return spans;
151
+ }
152
+
87
153
  function projectFilePathLabel(
88
154
  target: string,
89
155
  projectCwd: string,
@@ -684,6 +684,7 @@ function eventPriority(event: AgentEvent): "normal" | "critical" {
684
684
  || event.type === "approval_resolved"
685
685
  || event.type === "tool_started"
686
686
  || event.type === "context_compaction"
687
+ || (event.type === "progress" && event.severity === "warning")
687
688
  ) {
688
689
  return "critical";
689
690
  }
package/src/index.ts CHANGED
@@ -192,6 +192,10 @@ if (feishuOutbound && config.updates?.enabled !== false) {
192
192
  const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
193
193
  supervised,
194
194
  restart: requestRestart,
195
+ forceSafeRestart: async (scheduleId) => {
196
+ if (shuttingDown || restartRequested) return false;
197
+ return safeRestart.forceScheduled(scheduleId);
198
+ },
195
199
  cancelAutomaticUpdate: async (action) => {
196
200
  if (!dailyUpdateMonitor) throw new Error("自动更新功能未启用。");
197
201
  await dailyUpdateMonitor.cancel(action);
@@ -46,6 +46,9 @@ const PREVIEW_LABELS = {
46
46
  model: "模型",
47
47
  tools: "个工具",
48
48
  turnTokens: "本轮",
49
+ totalTokens: "总计",
50
+ cachedTokens: "缓存命中",
51
+ nonCachedTokens: "非缓存",
49
52
  compactionTokens: "压缩",
50
53
  compactionAfterTokens: "压缩后",
51
54
  characters: "字符",
@@ -84,6 +87,9 @@ const PREVIEW_LABELS = {
84
87
  model: "Model",
85
88
  tools: "tools",
86
89
  turnTokens: "Turn",
90
+ totalTokens: "Total",
91
+ cachedTokens: "Cache hit",
92
+ nonCachedTokens: "Non-cached",
87
93
  compactionTokens: "Compaction",
88
94
  compactionAfterTokens: "After compaction",
89
95
  characters: "characters",
@@ -302,6 +308,7 @@ export function renderTurnPreviewSnapshot(
302
308
  : "",
303
309
  timeline ? `<section class="timeline">${timeline}</section>` : `<div class="empty">${escapeHtml(language === "zh" ? "正在等待 Agent 返回进度…" : "Waiting for Agent progress…")}</div>`,
304
310
  state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
311
+ state.approval ? `<section class="notice"><h2>${escapeHtml(state.approval.title)}</h2><div class="markdown">${renderText(state.approval.reason ?? "")}</div><p>${escapeHtml(language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.")}</p></section>` : "",
305
312
  state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
306
313
  state.finalResponse
307
314
  ? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
@@ -355,6 +362,9 @@ function renderActivity(
355
362
  ): string {
356
363
  switch (activity.kind) {
357
364
  case "assistant":
365
+ if (activity.id.startsWith("commentary:runtime-error:")) {
366
+ return `<article class="activity message runtime-error" data-activity="${escapeAttribute(activity.id)}"><pre>${escapeHtml(activity.text)}</pre></article>`;
367
+ }
358
368
  return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
359
369
  case "user":
360
370
  return renderMessageActivity(activity.id, activity.text, "user", renderText);
@@ -619,7 +629,11 @@ function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): st
619
629
  return [
620
630
  elapsed,
621
631
  state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
622
- state.totalTokens === undefined ? "" : `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`,
632
+ state.totalTokens === undefined ? "" : state.cachedInputTokens === undefined
633
+ ? `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`
634
+ : `<span title="${escapeAttribute(labels.nonCachedTokens)}: ${formatNumber(state.totalTokens)} tokens">${escapeHtml(labels.nonCachedTokens)} ${formatTokenCount(state.totalTokens)} tokens</span>`,
635
+ state.totalTokensIncludingCache === undefined ? "" : `<span title="${escapeAttribute(labels.totalTokens)}: ${formatNumber(state.totalTokensIncludingCache)} tokens">${escapeHtml(labels.totalTokens)} ${formatTokenCount(state.totalTokensIncludingCache)} tokens</span>`,
636
+ state.cachedInputTokens === undefined ? "" : `<span title="${escapeAttribute(labels.cachedTokens)}: ${formatNumber(state.cachedInputTokens)} tokens">${escapeHtml(labels.cachedTokens)} ${formatTokenCount(state.cachedInputTokens)} tokens</span>`,
623
637
  state.contextCompactionAfterTokens === undefined ? "" : state.contextCompactionBeforeTokens === undefined
624
638
  ? `<span title="${escapeAttribute(labels.compactionAfterTokens)}">${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`
625
639
  : `<span title="${escapeAttribute(labels.compactionTokens)}">${formatTokenCount(state.contextCompactionBeforeTokens)} → ${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`,
@@ -802,6 +816,7 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
802
816
  .tool-meta span { white-space: nowrap; }
803
817
  .tool-header-timing { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
804
818
  .tool-header-timing:empty, .tool-footer-timing:empty { display: none; }
819
+ .runtime-error pre { white-space: pre-wrap; overflow-wrap: anywhere; border-left: 2px solid var(--danger); }
805
820
  .error-output, .error-result h2 { color: var(--danger); }
806
821
  .muted, .empty { color: var(--muted); }
807
822
  .notice, .empty { padding: 10px 0; }
@@ -53,7 +53,14 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
53
53
  case "turn_started":
54
54
  return { ...state, status: "running", startedAt: event.startedAt };
55
55
  case "agent_text_delta":
56
- return { ...state, assistantText: bound(`${state.assistantText}${event.text}`) };
56
+ return {
57
+ ...state,
58
+ assistantText: bound(`${state.assistantText}${event.text}`),
59
+ ...(event.replacesActivityId ? {
60
+ activities: (state.activities ?? []).filter((activity) => activity.id !== event.replacesActivityId),
61
+ progressText: undefined,
62
+ } : {}),
63
+ };
57
64
  case "token_usage_updated": {
58
65
  const lastTokens = normalizeTokenCount(event.lastTokens);
59
66
  const cumulativeTokens = normalizeTokenCount(event.cumulativeTokens);
@@ -61,10 +68,22 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
61
68
  const delta = previousCumulative === undefined
62
69
  ? lastTokens
63
70
  : Math.max(0, cumulativeTokens - previousCumulative);
71
+ const total = accumulateReportedTokens(
72
+ event.lastTotalTokens, event.cumulativeTotalTokens,
73
+ state.totalTokensIncludingCache, state.tokenUsageTotalCumulative, previousCumulative !== undefined,
74
+ );
75
+ const cached = accumulateReportedTokens(
76
+ event.lastCachedTokens, event.cumulativeCachedTokens,
77
+ state.cachedInputTokens, state.tokenUsageCachedCumulative, previousCumulative !== undefined,
78
+ );
64
79
  return {
65
80
  ...state,
66
81
  totalTokens: (state.totalTokens ?? 0) + delta,
67
82
  tokenUsageCumulative: Math.max(previousCumulative ?? 0, cumulativeTokens),
83
+ totalTokensIncludingCache: total.value,
84
+ tokenUsageTotalCumulative: total.cumulative,
85
+ cachedInputTokens: cached.value,
86
+ tokenUsageCachedCumulative: cached.cumulative,
68
87
  latestContextTokens: event.contextTokens === undefined
69
88
  ? state.latestContextTokens
70
89
  : normalizeTokenCount(event.contextTokens),
@@ -155,11 +174,13 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
155
174
  };
156
175
  }
157
176
  case "progress": {
177
+ const activities = state.activities ?? [];
178
+ const warning = event.severity === "warning";
158
179
  const activityUpdate = upsertReasoningActivity(
159
- state.activities ?? [],
180
+ warning ? activities.filter((activity) => activity.id !== (event.activityId ?? "progress")) : activities,
160
181
  event.activityId ?? "progress",
161
- event.text,
162
- event.append === true,
182
+ warning && !event.activityId?.startsWith("commentary:runtime-error:") ? bound(event.text) : event.text,
183
+ !warning && event.append === true,
163
184
  event.activityId?.startsWith("commentary:") ? "assistant" : "reasoning",
164
185
  );
165
186
  return {
@@ -179,7 +200,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
179
200
  return {
180
201
  ...state,
181
202
  ...tracking,
182
- status: "tool_running",
203
+ status: state.approval ? "waiting_for_approval" : "tool_running",
183
204
  activeTool: bounded,
184
205
  activities: activityUpdate.activities,
185
206
  activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
@@ -193,6 +214,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
193
214
  case "approval_requested":
194
215
  return { ...state, status: "waiting_for_approval", approval: event.request };
195
216
  case "approval_resolved":
217
+ if (state.approval?.id !== event.requestId) return state;
196
218
  return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
197
219
  case "turn_completed": {
198
220
  const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
@@ -230,7 +252,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
230
252
  approval: undefined,
231
253
  completedAt,
232
254
  durationMs: Math.max(0, completedAt - state.startedAt),
233
- error: bound(event.message),
255
+ error: event.message,
234
256
  };
235
257
  }
236
258
  }
@@ -312,7 +334,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
312
334
  ...tracking,
313
335
  fullToolOutputs,
314
336
  fullToolErrors,
315
- status: "tool_running",
337
+ status: state.approval ? "waiting_for_approval" : "tool_running",
316
338
  activeTool: bounded,
317
339
  activities: activityUpdate.activities,
318
340
  activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
@@ -328,7 +350,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
328
350
  ...tracking,
329
351
  fullToolOutputs,
330
352
  fullToolErrors,
331
- status: activeTool ? "tool_running" : "running",
353
+ status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
332
354
  activeTool,
333
355
  activities: activityUpdate.activities,
334
356
  activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
@@ -343,7 +365,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
343
365
  ...tracking,
344
366
  fullToolOutputs,
345
367
  fullToolErrors,
346
- status: activeTool ? "tool_running" : "running",
368
+ status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
347
369
  activeTool,
348
370
  activities: activityUpdate.activities,
349
371
  activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
@@ -463,6 +485,24 @@ function bound(value: string): string {
463
485
  return `${value.slice(0, MAX_TEXT - 1)}…`;
464
486
  }
465
487
 
488
+ function accumulateReportedTokens(
489
+ last: number | undefined,
490
+ cumulative: number | undefined,
491
+ value: number | undefined,
492
+ previous: number | undefined,
493
+ hasPriorUsage: boolean,
494
+ ): { value?: number; cumulative?: number } {
495
+ if (last === undefined || cumulative === undefined || !Number.isFinite(last) || !Number.isFinite(cumulative)
496
+ || last < 0 || cumulative < 0) return {};
497
+ // A saved or partial turn without a baseline cannot acquire a complete breakdown later.
498
+ if (hasPriorUsage && (previous === undefined || value === undefined)) return {};
499
+ const normalizedCumulative = normalizeTokenCount(cumulative);
500
+ return {
501
+ value: (value ?? 0) + (previous === undefined ? normalizeTokenCount(last) : Math.max(0, normalizedCumulative - previous)),
502
+ cumulative: Math.max(previous ?? 0, normalizedCumulative),
503
+ };
504
+ }
505
+
466
506
  function normalizeTokenCount(value: number): number {
467
507
  return Math.max(0, Math.round(value));
468
508
  }
@@ -37,6 +37,10 @@ export interface TurnViewState {
37
37
  durationMs?: number;
38
38
  totalTokens?: number;
39
39
  tokenUsageCumulative?: number;
40
+ totalTokensIncludingCache?: number;
41
+ cachedInputTokens?: number;
42
+ tokenUsageTotalCumulative?: number;
43
+ tokenUsageCachedCumulative?: number;
40
44
  contextCompactionStatus?: "running" | "completed";
41
45
  contextCompactionCount?: number;
42
46
  contextCompactionId?: string;
@@ -61,6 +61,7 @@ import type {
61
61
  ApprovalDecision,
62
62
  ConversationTurn,
63
63
  ModelOption,
64
+ ModelListResult,
64
65
  PermissionMode,
65
66
  RemoteCompletedTurnSummary,
66
67
  RemoteSessionActivity,
@@ -234,13 +235,13 @@ const HELP_COMMAND_SECTIONS: Array<{
234
235
  commands: [
235
236
  {
236
237
  command: "/new",
237
- usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
238
- description: "使用默认 Agent 创建任务;--nodir 创建 Projectless 任务",
238
+ usage: "[title] [--agent &#60;name&#62;] [--dir &#60;cwd&#62; | --nodir]",
239
+ description: "使用默认或指定 Agent 创建任务;--nodir 创建 Projectless 任务",
239
240
  },
240
241
  {
241
242
  command: "/newgroup",
242
- usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
243
- description: "创建飞书群和新任务",
243
+ usage: "[title] [--agent &#60;name&#62;] [--dir &#60;cwd&#62; | --nodir]",
244
+ description: "使用默认或指定 Agent 创建飞书群和新任务",
244
245
  },
245
246
  {
246
247
  command: "/dir",
@@ -500,6 +501,7 @@ export interface ProxyLifecycle {
500
501
  supervised?: boolean;
501
502
  restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
502
503
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
504
+ forceSafeRestart?(scheduleId: number): Promise<boolean>;
503
505
  cancelAutomaticUpdate?(action: CardAction): Promise<void>;
504
506
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
505
507
  persistAgentExecutionDefaults?(
@@ -1049,6 +1051,18 @@ export class ProxySessionController {
1049
1051
  });
1050
1052
  } else if (kind === "queued_prompt_cancel") {
1051
1053
  await this.cancelQueuedPrompt(scopedAction);
1054
+ } else if (kind === "safe_restart_force") {
1055
+ const scheduleId = Number(scopedAction.value.scheduleId);
1056
+ if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0) {
1057
+ throw new Error("安全重启卡片无效,请使用最新的状态卡片。");
1058
+ }
1059
+ if (!this.lifecycle?.forceSafeRestart) {
1060
+ throw new Error("当前运行方式不支持立即重启。");
1061
+ }
1062
+ const restarted = await this.lifecycle.forceSafeRestart(scheduleId);
1063
+ if (!restarted) {
1064
+ await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
1065
+ }
1052
1066
  } else if (kind === "safe_restart_cancel") {
1053
1067
  const scheduleId = Number(scopedAction.value.scheduleId);
1054
1068
  if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0) {
@@ -1164,7 +1178,9 @@ export class ProxySessionController {
1164
1178
  await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
1165
1179
  await this.stopSessionReference(contextKey, sessionId);
1166
1180
  if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
1167
- else await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
1181
+ else if (scopedAction.value.cardView !== "safe_restart") {
1182
+ await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
1183
+ }
1168
1184
  } else if (kind === "session_status") {
1169
1185
  await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
1170
1186
  await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
@@ -2006,14 +2022,16 @@ export class ProxySessionController {
2006
2022
  case "file":
2007
2023
  await this.sendCurrentTaskFile(contextKey, command.filePath);
2008
2024
  return;
2009
- case "new":
2010
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
2025
+ case "new": {
2026
+ const agentName = command.agentName ?? context.defaultAgent;
2027
+ const agent = this.ensureAgent(agentName);
2028
+ if (command.projectless && agent.kind !== "app-server") {
2011
2029
  throw new Error("/new --nodir 仅支持 App Server Agent。");
2012
2030
  }
2013
2031
  await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
2014
2032
  await this.createSession(
2015
2033
  contextKey,
2016
- context.defaultAgent,
2034
+ agentName,
2017
2035
  command.projectless
2018
2036
  ? undefined
2019
2037
  : command.cwd === undefined
@@ -2024,11 +2042,14 @@ export class ProxySessionController {
2024
2042
  undefined,
2025
2043
  undefined,
2026
2044
  command.title,
2027
- this.inheritedExecutionSettings(contextKey, context.defaultAgent),
2045
+ this.inheritedExecutionSettings(contextKey, agentName),
2028
2046
  );
2029
2047
  return;
2030
- case "newgroup":
2031
- if (command.projectless && this.ensureAgent(context.defaultAgent).kind !== "app-server") {
2048
+ }
2049
+ case "newgroup": {
2050
+ const agentName = command.agentName ?? context.defaultAgent;
2051
+ const agent = this.ensureAgent(agentName);
2052
+ if (command.projectless && agent.kind !== "app-server") {
2032
2053
  throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
2033
2054
  }
2034
2055
  if (!userId?.startsWith("ou_")) {
@@ -2037,13 +2058,14 @@ export class ProxySessionController {
2037
2058
  await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
2038
2059
  await this.createFeishuGroup(
2039
2060
  contextKey,
2040
- context.defaultAgent,
2061
+ agentName,
2041
2062
  command.title,
2042
2063
  userId,
2043
2064
  command.cwd === undefined ? undefined : resolveUserPath(command.cwd),
2044
2065
  command.projectless === true,
2045
2066
  );
2046
2067
  return;
2068
+ }
2047
2069
  case "clone":
2048
2070
  case "clonegroup": {
2049
2071
  let source = this.currentSession(contextKey);
@@ -2136,7 +2158,7 @@ export class ProxySessionController {
2136
2158
  await this.openExecutionSettings(contextKey, "model");
2137
2159
  return;
2138
2160
  case "provider":
2139
- await this.openProviderSettings(contextKey);
2161
+ await this.openExecutionSettings(contextKey, "provider");
2140
2162
  return;
2141
2163
  case "thinking":
2142
2164
  await this.openExecutionSettings(contextKey, "thinking");
@@ -5110,7 +5132,7 @@ export class ProxySessionController {
5110
5132
  const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
5111
5133
  const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
5112
5134
  const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
5113
- const models = await loaded.runtime.listModels(currentProvider, loaded.session.model);
5135
+ const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider, loaded.session.model);
5114
5136
  const currentModel = models.find((model) => model.id === loaded.session.model)
5115
5137
  ?? models.find((model) => model.isDefault)
5116
5138
  ?? models[0];
@@ -5127,6 +5149,7 @@ export class ProxySessionController {
5127
5149
  providers,
5128
5150
  providerSupported,
5129
5151
  models,
5152
+ warning,
5130
5153
  reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
5131
5154
  };
5132
5155
  }
@@ -5156,21 +5179,6 @@ export class ProxySessionController {
5156
5179
  await this.openExecutionSettings(contextKey, "agent");
5157
5180
  }
5158
5181
 
5159
- private async openProviderSettings(contextKey: string): Promise<void> {
5160
- const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
5161
- const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
5162
- ? await this.modelProviderOptions(loaded)
5163
- : [];
5164
- const current = loaded.session.modelProvider?.trim()
5165
- || providers.find((provider) => provider.isDefault)?.id
5166
- || "运行时默认";
5167
- if (providers.length <= 1) {
5168
- await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
5169
- return;
5170
- }
5171
- await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
5172
- }
5173
-
5174
5182
  private async selectProvider(
5175
5183
  contextKey: string,
5176
5184
  modelProvider: string,
@@ -5210,13 +5218,14 @@ export class ProxySessionController {
5210
5218
  ): Promise<void> {
5211
5219
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
5212
5220
  await this.assertModelProvider(loaded, modelProvider);
5213
- const models = await loaded.runtime.listModels(modelProvider, loaded.session.model);
5221
+ const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
5214
5222
  const card = this.cardRenderer.renderModelSelector({
5215
5223
  sessionId,
5216
5224
  contextKey,
5217
5225
  currentModel: selectProviderModel(models, loaded.session.model)?.id,
5218
5226
  reasoningEffort: loaded.session.reasoningEffort,
5219
5227
  models,
5228
+ warning,
5220
5229
  modelProvider,
5221
5230
  permissionMode,
5222
5231
  unifiedSettings: true,
@@ -5234,7 +5243,7 @@ export class ProxySessionController {
5234
5243
  ): Promise<void> {
5235
5244
  const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
5236
5245
  await this.assertModelProvider(loaded, modelProvider);
5237
- const models = await loaded.runtime.listModels(modelProvider, model);
5246
+ const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
5238
5247
  const selected = models.find((candidate) => candidate.id === model);
5239
5248
  if (!selected) throw new Error(`未知模型:${model}`);
5240
5249
  const options = selected.supportedReasoningEfforts.length > 0
@@ -5252,6 +5261,7 @@ export class ProxySessionController {
5252
5261
  model,
5253
5262
  currentEffort,
5254
5263
  options,
5264
+ warning,
5255
5265
  permissionMode,
5256
5266
  unifiedSettings: true,
5257
5267
  });
@@ -5296,7 +5306,7 @@ export class ProxySessionController {
5296
5306
  await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
5297
5307
  }
5298
5308
  const loaded = await this.loadSession(record);
5299
- await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
5309
+ const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
5300
5310
  const session = await this.changeProviderSettings(loaded, {
5301
5311
  modelProvider: settings.provider,
5302
5312
  model: settings.model,
@@ -5317,7 +5327,7 @@ export class ProxySessionController {
5317
5327
  notice,
5318
5328
  });
5319
5329
  } else {
5320
- await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
5330
+ await this.outbound.sendText(contextKey, [notice.replaceAll("`", ""), warning].filter(Boolean).join("\n"));
5321
5331
  }
5322
5332
  }
5323
5333
 
@@ -5352,15 +5362,23 @@ export class ProxySessionController {
5352
5362
  modelProvider: string,
5353
5363
  model: string,
5354
5364
  effort: string,
5355
- ): Promise<void> {
5365
+ ): Promise<string | undefined> {
5356
5366
  await this.assertModelProvider(loaded, modelProvider);
5357
- const selected = (await loaded.runtime.listModels(modelProvider, model)).find((candidate) => candidate.id === model);
5367
+ const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
5368
+ const selected = models.find((candidate) => candidate.id === model);
5358
5369
  if (!selected) throw new Error(`未知模型:${model}`);
5359
5370
  if (selected.supportedReasoningEfforts.length > 0
5360
5371
  && !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
5361
5372
  const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
5362
5373
  throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
5363
5374
  }
5375
+ return warning;
5376
+ }
5377
+
5378
+ private async modelOptions(runtime: AgentRuntime, provider?: string, fallbackModel?: string): Promise<ModelListResult> {
5379
+ return runtime.listModelsWithStatus
5380
+ ? runtime.listModelsWithStatus(provider, fallbackModel)
5381
+ : { models: await runtime.listModels(provider, fallbackModel) };
5364
5382
  }
5365
5383
 
5366
5384
  private async assertModelProvider(loaded: LoadedSession, modelProvider: string): Promise<void> {
@@ -7027,10 +7045,12 @@ export class ProxySessionController {
7027
7045
  activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
7028
7046
  queued = this.store.countQueuedPrompts(current.localSessionId);
7029
7047
  snapshot = turnViewSnapshot(current.lastTurnId ? this.store.getTurnSnapshot(current.lastTurnId) : undefined);
7030
- const statusLabel = remoteActive && remote && localCurrent && !isBotOwnedActiveTurn(localCurrent, remote)
7031
- ? "外部执行中"
7032
- : sessionStatusLabel(current.status, activeTurnId);
7033
- const resultLabel = remoteActive
7048
+ const waitingForApproval = snapshot?.turnId === current.lastTurnId && snapshot?.status === "waiting_for_approval";
7049
+ const statusLabel = waitingForApproval ? "等待确认"
7050
+ : remoteActive && remote && localCurrent && !isBotOwnedActiveTurn(localCurrent, remote)
7051
+ ? "外部执行中"
7052
+ : sessionStatusLabel(current.status, activeTurnId);
7053
+ const resultLabel = waitingForApproval ? "等待确认" : remoteActive
7034
7054
  ? "执行中"
7035
7055
  : remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
7036
7056
  taskLines.push(
@@ -8259,7 +8279,8 @@ function executionDetailLines(
8259
8279
 
8260
8280
  function finalResultLines(snapshot?: TurnViewState, remote?: RemoteSessionSummary): string[] {
8261
8281
  const relevantSnapshot = !remote?.lastTurnId || snapshot?.turnId === remote.lastTurnId ? snapshot : undefined;
8262
- if (relevantSnapshot?.status === "running" || relevantSnapshot?.status === "tool_running" || relevantSnapshot?.status === "waiting_for_approval"
8282
+ if (relevantSnapshot?.status === "waiting_for_approval") return ["任务正在等待确认,尚无最终结果。"];
8283
+ if (relevantSnapshot?.status === "running" || relevantSnapshot?.status === "tool_running"
8263
8284
  || isRemoteSessionActive(remote)) {
8264
8285
  return ["任务仍在执行,尚无最终结果。"];
8265
8286
  }
@@ -58,6 +58,7 @@ export interface ToolState {
58
58
 
59
59
  export interface ApprovalRequest {
60
60
  id: string;
61
+ kind?: "mode_change";
61
62
  title: string;
62
63
  command?: string;
63
64
  reason?: string;
@@ -66,13 +67,17 @@ export interface ApprovalRequest {
66
67
 
67
68
  export type AgentEvent =
68
69
  | { type: "turn_started"; sessionId: string; turnId: string; startedAt: number }
69
- | { type: "agent_text_delta"; sessionId: string; turnId: string; text: string }
70
+ | { type: "agent_text_delta"; sessionId: string; turnId: string; text: string; replacesActivityId?: string }
70
71
  | {
71
72
  type: "token_usage_updated";
72
73
  sessionId: string;
73
74
  turnId: string;
74
75
  lastTokens: number;
75
76
  cumulativeTokens: number;
77
+ lastTotalTokens?: number;
78
+ cumulativeTotalTokens?: number;
79
+ lastCachedTokens?: number;
80
+ cumulativeCachedTokens?: number;
76
81
  contextTokens?: number;
77
82
  }
78
83
  | {
@@ -92,6 +97,7 @@ export type AgentEvent =
92
97
  text: string;
93
98
  activityId?: string;
94
99
  append?: boolean;
100
+ severity?: "warning";
95
101
  }
96
102
  | { type: "plan_updated"; sessionId: string; turnId: string; steps: PlanStep[] }
97
103
  | { type: "tool_started"; sessionId: string; turnId: string; tool: ToolState }
@@ -224,6 +230,11 @@ export interface ModelOption {
224
230
  defaultReasoningEffort?: string;
225
231
  }
226
232
 
233
+ export interface ModelListResult {
234
+ models: ModelOption[];
235
+ warning?: string;
236
+ }
237
+
227
238
  export interface ModelProviderOption {
228
239
  id: string;
229
240
  displayName?: string;
@@ -285,6 +296,7 @@ export interface AgentRuntime {
285
296
  ): Promise<RuntimeSession>;
286
297
  respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
287
298
  listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
299
+ listModelsWithStatus?(modelProvider?: string, fallbackModel?: string): Promise<ModelListResult>;
288
300
  listModelProviders?(): Promise<ModelProviderOption[]>;
289
301
  release?(options?: { force?: boolean }): Promise<RuntimeReleaseResult>;
290
302
  onEvent(listener: (event: RuntimeEvent) => void): () => void;
@@ -121,6 +121,8 @@ export class SafeRestartNotifier {
121
121
  .filter((session) => session.status === "running" || this.store.countQueuedPrompts(session.localSessionId) > 0)
122
122
  .map((session) => ({
123
123
  id: session.remoteSessionId ?? session.localSessionId,
124
+ localSessionId: session.localSessionId,
125
+ isRunning: session.status === "running",
124
126
  title: session.title,
125
127
  }));
126
128
  await Promise.all(targets.map(async ({ contextKey, replyMessageId, reason }) => {
@@ -93,6 +93,13 @@ export class SafeRestartScheduler {
93
93
  return this.cancelScheduled(this.scheduleId);
94
94
  }
95
95
 
96
+ async forceScheduled(expectedScheduleId: number): Promise<boolean> {
97
+ const reason = this.reason;
98
+ if (!reason || this.scheduleId !== expectedScheduleId) return false;
99
+ await this.restart(reason, expectedScheduleId, this.options.readActivity());
100
+ return true;
101
+ }
102
+
96
103
  async cancelScheduled(expectedScheduleId: number): Promise<boolean> {
97
104
  const reason = this.reason;
98
105
  if (!reason || this.scheduleId !== expectedScheduleId) return false;