@keyou007/agent-bot 0.1.16 → 0.1.17-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +4 -2
  3. package/README.zh.md +4 -2
  4. package/config.example.yaml +8 -0
  5. package/dist/cli/help.js +4 -4
  6. package/dist/cli/taskTarget.js +7 -0
  7. package/dist/cli/taskTarget.js.map +1 -1
  8. package/dist/cli.js +12 -12
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexProcessManager.js +2 -1
  11. package/dist/codex/CodexProcessManager.js.map +1 -1
  12. package/dist/codex/CodexRuntime.js +62 -14
  13. package/dist/codex/CodexRuntime.js.map +1 -1
  14. package/dist/config/AgentExecutionDefaultsStore.js +71 -0
  15. package/dist/config/AgentExecutionDefaultsStore.js.map +1 -0
  16. package/dist/config/loadConfig.js +8 -0
  17. package/dist/config/loadConfig.js.map +1 -1
  18. package/dist/config/schema.js +7 -0
  19. package/dist/config/schema.js.map +1 -1
  20. package/dist/feishu/CardRenderer.js +29 -27
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +61 -5
  23. package/dist/feishu/FeishuConnector.js.map +1 -1
  24. package/dist/feishu/FeishuTurnPresenter.js +2 -2
  25. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  26. package/dist/index.js +17 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/proxy/ProxySessionController.js +255 -30
  29. package/dist/proxy/ProxySessionController.js.map +1 -1
  30. package/dist/state/StateStore.js +62 -9
  31. package/dist/state/StateStore.js.map +1 -1
  32. package/dist/supervision/restartNotificationTarget.js +9 -0
  33. package/dist/supervision/restartNotificationTarget.js.map +1 -0
  34. package/dist/utils/spawnCommand.js +2 -1
  35. package/dist/utils/spawnCommand.js.map +1 -1
  36. package/docs/technical-reference.md +7 -3
  37. package/docs/technical-reference.zh.md +7 -3
  38. package/npm-shrinkwrap.json +9 -9
  39. package/package.json +2 -2
  40. package/skills/agent-bot/SKILL.md +3 -1
  41. package/src/cli/help.ts +4 -4
  42. package/src/cli/taskTarget.ts +11 -0
  43. package/src/cli.ts +16 -20
  44. package/src/codex/CodexProcessManager.ts +3 -1
  45. package/src/codex/CodexRuntime.ts +63 -12
  46. package/src/config/AgentExecutionDefaultsStore.ts +71 -0
  47. package/src/config/loadConfig.ts +8 -0
  48. package/src/config/schema.ts +13 -1
  49. package/src/feishu/CardRenderer.ts +30 -22
  50. package/src/feishu/FeishuConnector.ts +70 -5
  51. package/src/feishu/FeishuTurnPresenter.ts +2 -2
  52. package/src/index.ts +17 -2
  53. package/src/proxy/ProxySessionController.ts +330 -40
  54. package/src/runtime/types.ts +8 -0
  55. package/src/state/StateStore.ts +100 -9
  56. package/src/supervision/restartNotificationTarget.ts +12 -0
  57. package/src/utils/spawnCommand.ts +2 -0
@@ -5,7 +5,11 @@ import path from "node:path";
5
5
  import type { Logger } from "pino";
6
6
  import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
7
7
  import { resolveUserPath } from "../config/paths.js";
8
- import { DEFAULT_GROUP_NAME_FORMAT, type AppConfig } from "../config/schema.js";
8
+ import {
9
+ DEFAULT_GROUP_NAME_FORMAT,
10
+ type AgentExecutionDefaults,
11
+ type AppConfig,
12
+ } from "../config/schema.js";
9
13
  import { CommandRouter } from "../commands/CommandRouter.js";
10
14
  import type { Command } from "../commands/commandTypes.js";
11
15
  import { baseChatContextKey, isThreadContextKey } from "../feishu/contextKey.js";
@@ -45,6 +49,7 @@ import type {
45
49
  AgentRuntime,
46
50
  ApprovalDecision,
47
51
  PermissionMode,
52
+ RemoteCompletedTurnSummary,
48
53
  RemoteSessionActivity,
49
54
  RemoteSessionSummary,
50
55
  RuntimeGoal,
@@ -323,12 +328,7 @@ interface ExecutionSettingsCardOptions extends ModelCardOptions {
323
328
  notice?: string;
324
329
  }
325
330
 
326
- interface SessionExecutionSettings {
327
- modelProvider?: string;
328
- model?: string;
329
- reasoningEffort?: string;
330
- permissionMode?: PermissionMode;
331
- }
331
+ type SessionExecutionSettings = AgentExecutionDefaults;
332
332
 
333
333
  interface ProjectSessionReference {
334
334
  source?: SessionRecord;
@@ -352,6 +352,10 @@ interface ForkSessionPlan {
352
352
  reasoningEffort?: string;
353
353
  permissionMode: PermissionMode;
354
354
  lastTurnStatus?: SessionRecord["lastTurnStatus"];
355
+ sourceTurnPrompt?: string;
356
+ sourceTurnStartedAt?: number;
357
+ sourceTurnCompletedAt?: number;
358
+ sourceCompletedTurns?: RemoteCompletedTurnSummary[];
355
359
  sourceWasRunning: boolean;
356
360
  forkedFromHistoricalTurn: boolean;
357
361
  }
@@ -395,6 +399,10 @@ export interface ProxyLifecycle {
395
399
  restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
396
400
  cancelSafeRestart?(scheduleId: number): Promise<boolean>;
397
401
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
402
+ persistAgentExecutionDefaults?(
403
+ agentName: string,
404
+ defaults: AgentExecutionDefaults,
405
+ ): Promise<void> | void;
398
406
  }
399
407
 
400
408
  export type ShellCommandExecutor = (
@@ -419,6 +427,7 @@ export class ProxySessionController {
419
427
  private readonly queuedPromptCards = new Map<string, Map<string, string>>();
420
428
  private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
421
429
  private readonly sessionResets = new Map<string, Promise<void>>();
430
+ private readonly resetHistoryOperations = new Map<string, string>();
422
431
  private readonly lastSessionListings = new Map<string, string[]>();
423
432
  private readonly threadInitializations = new Map<string, Promise<void>>();
424
433
  private readonly llmRetryingSessions = new Set<string>();
@@ -824,17 +833,17 @@ export class ProxySessionController {
824
833
  );
825
834
  } else if (kind === "turn_reset") {
826
835
  const sessionId = String(scopedAction.value.sessionId ?? "");
827
- await this.resetCurrentSessionToTurn(
828
- contextKey,
829
- sessionId,
830
- String(scopedAction.value.turnId ?? ""),
831
- );
836
+ const turnId = String(scopedAction.value.turnId ?? "");
832
837
  if (scopedAction.value.cardView === "reset_history") {
833
- await this.openResetHistory(contextKey, {
834
- expectedSessionId: sessionId,
835
- updateMessageId: requiredCardMessageId(scopedAction.messageId),
836
- page: resetHistoryPageValue(scopedAction.value.page),
837
- });
838
+ await this.resetFromHistoryCard(
839
+ contextKey,
840
+ sessionId,
841
+ turnId,
842
+ requiredCardMessageId(scopedAction.messageId),
843
+ resetHistoryPageValue(scopedAction.value.page),
844
+ );
845
+ } else {
846
+ await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
838
847
  }
839
848
  } else if (kind === "turn_reset_page") {
840
849
  await this.openResetHistory(contextKey, {
@@ -1086,6 +1095,7 @@ export class ProxySessionController {
1086
1095
  this.queuedPromptCards.clear();
1087
1096
  this.queuedPromptCardWrites.clear();
1088
1097
  this.sessionResets.clear();
1098
+ this.resetHistoryOperations.clear();
1089
1099
  for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
1090
1100
  this.pendingMergedForwards.clear();
1091
1101
  for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
@@ -1506,6 +1516,7 @@ export class ProxySessionController {
1506
1516
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: nextValue });
1507
1517
  }
1508
1518
  const session = this.store.getSession(record.localSessionId) ?? record;
1519
+ if (setting) await this.persistAgentExecutionDefaults(session.localSessionId);
1509
1520
  const models = await loaded.runtime.listModels();
1510
1521
  const currentModel = models.find((candidate) => candidate.id === session.model)
1511
1522
  ?? models.find((candidate) => candidate.isDefault);
@@ -2827,6 +2838,21 @@ export class ProxySessionController {
2827
2838
  const model = remote?.model ?? source?.model;
2828
2839
  const reasoningEffort = source?.reasoningEffort;
2829
2840
  const permissionMode = source?.permissionMode ?? "auto";
2841
+ const sourceTurnPrompt = truncateText(
2842
+ latestUserPromptFromTurnView(snapshot)
2843
+ ?? (lastTurnId === latestTurnId ? remote?.lastUserPrompt : undefined)
2844
+ ?? remote?.preview
2845
+ ?? source?.title
2846
+ ?? remote?.title
2847
+ ?? "Fork 来源轮次",
2848
+ 1_000,
2849
+ );
2850
+ const sourceTurnCompletedAt = snapshot?.completedAt
2851
+ ?? snapshot?.startedAt
2852
+ ?? (lastTurnId === latestTurnId
2853
+ ? latestRemoteTimestamp(remote?.recencyAt, remote?.updatedAt)
2854
+ : undefined)
2855
+ ?? parseIsoTimestamp(source?.updatedAt);
2830
2856
 
2831
2857
  return {
2832
2858
  source,
@@ -2846,6 +2872,10 @@ export class ProxySessionController {
2846
2872
  : mapRemoteTurnStatus(remote?.lastTurnStatus)
2847
2873
  ?? forkedTurnStatus(snapshot?.status)
2848
2874
  ?? source?.lastTurnStatus,
2875
+ sourceTurnPrompt,
2876
+ sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2877
+ sourceTurnCompletedAt,
2878
+ sourceCompletedTurns: remote?.completedTurns ?? [],
2849
2879
  sourceWasRunning: isRunning,
2850
2880
  forkedFromHistoricalTurn,
2851
2881
  };
@@ -2918,6 +2948,9 @@ export class ProxySessionController {
2918
2948
  || source.status === "running",
2919
2949
  );
2920
2950
  const forkTitle = normalizeTaskTitle(requestedTitle) ?? this.store.nextForkTitle(source.title);
2951
+ const sourceTurnCompletedAt = snapshot?.completedAt
2952
+ ?? snapshot?.startedAt
2953
+ ?? parseIsoTimestamp(source.updatedAt);
2921
2954
 
2922
2955
  return {
2923
2956
  source,
@@ -2935,6 +2968,12 @@ export class ProxySessionController {
2935
2968
  lastTurnStatus: forkedTurnStatus(snapshot?.status)
2936
2969
  ?? (source.lastTurnId === lastTurnId ? source.lastTurnStatus : undefined)
2937
2970
  ?? "completed",
2971
+ sourceTurnPrompt: truncateText(
2972
+ latestUserPromptFromTurnView(snapshot) ?? source.title ?? "Fork 来源轮次",
2973
+ 1_000,
2974
+ ),
2975
+ sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2976
+ sourceTurnCompletedAt,
2938
2977
  sourceWasRunning,
2939
2978
  forkedFromHistoricalTurn: sourceWasRunning && source.lastTurnId !== lastTurnId,
2940
2979
  };
@@ -2944,6 +2983,7 @@ export class ProxySessionController {
2944
2983
  contextKey: string,
2945
2984
  plan: ForkSessionPlan,
2946
2985
  ): Promise<ForkSessionResult> {
2986
+ const sourceCompletedTurns = await this.resolveForkSourceCompletedTurns(plan);
2947
2987
  const localSessionId = createId("sess");
2948
2988
  const record = this.store.createSession({
2949
2989
  localSessionId,
@@ -2979,7 +3019,6 @@ export class ProxySessionController {
2979
3019
  lastTurnId: plan.lastTurnId,
2980
3020
  lastTurnStatus: plan.lastTurnStatus,
2981
3021
  });
2982
- this.store.setCurrentSession(contextKey, localSessionId);
2983
3022
  this.outbound.registerSession(
2984
3023
  localSessionId,
2985
3024
  contextKey,
@@ -2987,14 +3026,25 @@ export class ProxySessionController {
2987
3026
  plan.cwd,
2988
3027
  this.agentLabel(plan.agentName),
2989
3028
  );
3029
+ const sourceTurnHistoryCount = this.persistForkSourceHistory(
3030
+ contextKey,
3031
+ localSessionId,
3032
+ plan,
3033
+ sourceCompletedTurns,
3034
+ );
2990
3035
  this.store.audit(contextKey, "session_forked", {
2991
3036
  sourceLocalSessionId: plan.source?.localSessionId,
2992
3037
  sourceRemoteSessionId: plan.remoteSessionId,
2993
3038
  sourceTurnId: plan.lastTurnId,
3039
+ sourceTurnPrompt: plan.sourceTurnPrompt,
3040
+ sourceTurnStartedAt: plan.sourceTurnStartedAt,
3041
+ sourceTurnCompletedAt: plan.sourceTurnCompletedAt,
3042
+ sourceTurnHistoryCount,
2994
3043
  sourceWasRunning: plan.sourceWasRunning,
2995
3044
  forkedLocalSessionId: localSessionId,
2996
3045
  forkedRemoteSessionId: forked.remoteSessionId,
2997
3046
  });
3047
+ this.store.setCurrentSession(contextKey, localSessionId);
2998
3048
  return {
2999
3049
  record: this.store.getSession(localSessionId) ?? record,
3000
3050
  session: forked,
@@ -3005,6 +3055,146 @@ export class ProxySessionController {
3005
3055
  }
3006
3056
  }
3007
3057
 
3058
+ private async resolveForkSourceCompletedTurns(
3059
+ plan: ForkSessionPlan,
3060
+ ): Promise<RemoteCompletedTurnSummary[]> {
3061
+ let completedTurns = plan.sourceCompletedTurns;
3062
+ if (completedTurns === undefined && plan.runtime.readRemoteSession) {
3063
+ try {
3064
+ completedTurns = (await plan.runtime.readRemoteSession(plan.remoteSessionId)).completedTurns ?? [];
3065
+ } catch (error) {
3066
+ this.logger.warn(
3067
+ { error, remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
3068
+ "Failed to read the source task Turn history before Fork; keeping the Fork anchor fallback.",
3069
+ );
3070
+ }
3071
+ }
3072
+ if (!completedTurns?.length) return [];
3073
+ const anchorIndex = completedTurns.findIndex((turn) => turn.id === plan.lastTurnId);
3074
+ if (anchorIndex < 0) {
3075
+ this.logger.warn(
3076
+ { remoteSessionId: plan.remoteSessionId, sourceTurnId: plan.lastTurnId },
3077
+ "The source task Turn history did not include the Fork anchor; keeping the Fork anchor fallback.",
3078
+ );
3079
+ return [];
3080
+ }
3081
+ return completedTurns.slice(0, anchorIndex + 1);
3082
+ }
3083
+
3084
+ private persistForkSourceHistory(
3085
+ contextKey: string,
3086
+ forkedLocalSessionId: string,
3087
+ plan: ForkSessionPlan,
3088
+ completedTurns: RemoteCompletedTurnSummary[],
3089
+ ): number {
3090
+ if (completedTurns.length === 0) return 0;
3091
+ const localSessionId = plan.source?.localSessionId ?? forkedLocalSessionId;
3092
+ const historyContextKey = plan.source?.contextKey ?? contextKey;
3093
+ const fallbackStart = plan.sourceTurnStartedAt
3094
+ ?? plan.sourceTurnCompletedAt
3095
+ ?? Date.now() - completedTurns.length;
3096
+ return this.persistCompletedTurnHistory({
3097
+ localSessionId,
3098
+ contextKey: historyContextKey,
3099
+ agentName: plan.agentName,
3100
+ remoteSessionId: plan.remoteSessionId,
3101
+ completedTurns,
3102
+ fallbackStart,
3103
+ });
3104
+ }
3105
+
3106
+ private persistCompletedTurnHistory(input: {
3107
+ localSessionId: string;
3108
+ contextKey: string;
3109
+ agentName: string;
3110
+ remoteSessionId: string;
3111
+ completedTurns: RemoteCompletedTurnSummary[];
3112
+ fallbackStart: number;
3113
+ }): number {
3114
+ try {
3115
+ this.store.importCompletedTurnHistory({
3116
+ localSessionId: input.localSessionId,
3117
+ contextKey: input.contextKey,
3118
+ agentName: input.agentName,
3119
+ remoteSessionId: input.remoteSessionId,
3120
+ turns: input.completedTurns.map((turn, index) => {
3121
+ const startedAt = turn.startedAt ?? input.fallbackStart + index;
3122
+ const completedAt = turn.completedAt ?? startedAt;
3123
+ return {
3124
+ turnId: turn.id,
3125
+ snapshot: {
3126
+ sessionId: input.localSessionId,
3127
+ turnId: turn.id,
3128
+ prompt: turn.prompt ?? "未记录对话内容",
3129
+ status: "completed",
3130
+ startedAt,
3131
+ completedAt,
3132
+ assistantText: "",
3133
+ plan: [],
3134
+ activities: [],
3135
+ completedTools: [],
3136
+ failedTools: [],
3137
+ fileSummary: [],
3138
+ } satisfies TurnViewState,
3139
+ updatedAt: new Date(completedAt).toISOString(),
3140
+ };
3141
+ }),
3142
+ });
3143
+ return input.completedTurns.length;
3144
+ } catch (error) {
3145
+ this.logger.warn(
3146
+ { error, remoteSessionId: input.remoteSessionId },
3147
+ "Failed to persist inherited Turn history after Fork; keeping the Fork anchor fallback.",
3148
+ );
3149
+ return 0;
3150
+ }
3151
+ }
3152
+
3153
+ private async hydrateLegacyForkTurnHistory(
3154
+ contextKey: string,
3155
+ current: SessionRecord,
3156
+ ): Promise<void> {
3157
+ const source = this.store.getForkHistorySource(current.localSessionId);
3158
+ if (!source || source.sourceTurnHistoryCount !== undefined
3159
+ || this.store.hasImportedForkTurnHistory(current.localSessionId)) return;
3160
+ if (!source.sourceRemoteSessionId) return;
3161
+ const runtime = this.runtimes.forAgent(current.agentName);
3162
+ if (runtime.kind !== "codex" || !runtime.readRemoteSession) return;
3163
+ try {
3164
+ const remote = await runtime.readRemoteSession(source.sourceRemoteSessionId);
3165
+ const completedTurns = remote.completedTurns ?? [];
3166
+ const anchorIndex = completedTurns.findIndex((turn) => turn.id === source.sourceTurnId);
3167
+ if (anchorIndex < 0) return;
3168
+ const inheritedTurns = completedTurns.slice(0, anchorIndex + 1);
3169
+ const sourceSession = source.sourceLocalSessionId
3170
+ ? this.store.getSession(source.sourceLocalSessionId)
3171
+ : undefined;
3172
+ const imported = this.persistCompletedTurnHistory({
3173
+ localSessionId: sourceSession?.localSessionId ?? current.localSessionId,
3174
+ contextKey: sourceSession?.contextKey ?? contextKey,
3175
+ agentName: current.agentName,
3176
+ remoteSessionId: source.sourceRemoteSessionId,
3177
+ completedTurns: inheritedTurns,
3178
+ fallbackStart: source.sourceTurnStartedAt
3179
+ ?? source.sourceTurnCompletedAt
3180
+ ?? Date.now() - inheritedTurns.length,
3181
+ });
3182
+ if (imported > 0) {
3183
+ this.store.audit(contextKey, "fork_turn_history_imported", {
3184
+ forkedLocalSessionId: current.localSessionId,
3185
+ sourceRemoteSessionId: source.sourceRemoteSessionId,
3186
+ sourceTurnId: source.sourceTurnId,
3187
+ importedTurnCount: imported,
3188
+ });
3189
+ }
3190
+ } catch (error) {
3191
+ this.logger.warn(
3192
+ { error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId },
3193
+ "Failed to hydrate legacy Fork Turn history; rendering the locally available history.",
3194
+ );
3195
+ }
3196
+ }
3197
+
3008
3198
  private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
3009
3199
  const resolved = await this.resolveProjectSessionReference(contextKey, reference);
3010
3200
  const created = await this.createSession(
@@ -3220,19 +3410,21 @@ export class ProxySessionController {
3220
3410
  executionSettings: SessionExecutionSettings = {},
3221
3411
  ): Promise<SessionRecord> {
3222
3412
  const agent = this.ensureAgent(agentName);
3413
+ const resolvedExecutionSettings = mergeExecutionSettings(agent.defaults ?? {}, executionSettings);
3223
3414
  const localSessionId = createId("sess");
3224
3415
  const initialTitle = normalizeTaskTitle(requestedTitle ?? prompt ?? "");
3225
3416
  const sessionCwd = cwd === undefined && agent.kind === "app-server"
3226
3417
  ? createProjectlessWorkspace({ prompt: initialTitle }).cwd
3227
3418
  : path.resolve(cwd ?? this.config.defaults.cwd);
3228
3419
  const record = this.store.createSession({ localSessionId, contextKey, agentName, cwd: sessionCwd, status: "starting" });
3229
- if (initialTitle || executionSettings.modelProvider || executionSettings.model || executionSettings.reasoningEffort || executionSettings.permissionMode) {
3420
+ if (initialTitle || resolvedExecutionSettings.modelProvider || resolvedExecutionSettings.model
3421
+ || resolvedExecutionSettings.reasoningEffort || resolvedExecutionSettings.permissionMode) {
3230
3422
  this.store.updateRuntimeSession(localSessionId, {
3231
3423
  title: initialTitle,
3232
- modelProvider: executionSettings.modelProvider,
3233
- model: executionSettings.model,
3234
- reasoningEffort: executionSettings.reasoningEffort,
3235
- permissionMode: executionSettings.permissionMode,
3424
+ modelProvider: resolvedExecutionSettings.modelProvider,
3425
+ model: resolvedExecutionSettings.model,
3426
+ reasoningEffort: resolvedExecutionSettings.reasoningEffort,
3427
+ permissionMode: resolvedExecutionSettings.permissionMode,
3236
3428
  });
3237
3429
  }
3238
3430
  this.store.setCurrentSession(contextKey, localSessionId);
@@ -3253,10 +3445,10 @@ export class ProxySessionController {
3253
3445
  agentName,
3254
3446
  cwd: sessionCwd,
3255
3447
  title: initialTitle,
3256
- modelProvider: executionSettings.modelProvider,
3257
- model: executionSettings.model,
3258
- reasoningEffort: executionSettings.reasoningEffort,
3259
- permissionMode: executionSettings.permissionMode ?? "auto",
3448
+ modelProvider: resolvedExecutionSettings.modelProvider,
3449
+ model: resolvedExecutionSettings.model,
3450
+ reasoningEffort: resolvedExecutionSettings.reasoningEffort,
3451
+ permissionMode: resolvedExecutionSettings.permissionMode ?? "auto",
3260
3452
  });
3261
3453
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3262
3454
  const saved = this.store.getSession(localSessionId) ?? record;
@@ -4091,6 +4283,53 @@ export class ProxySessionController {
4091
4283
  }
4092
4284
  }
4093
4285
 
4286
+ private async resetFromHistoryCard(
4287
+ contextKey: string,
4288
+ sessionId: string,
4289
+ turnId: string,
4290
+ updateMessageId: string,
4291
+ page: number,
4292
+ ): Promise<void> {
4293
+ if (!sessionId || !turnId) throw new Error("无效的 Reset 请求。请使用最新的历史轮次卡片重试。");
4294
+ const existingTurnId = this.resetHistoryOperations.get(sessionId);
4295
+ if (existingTurnId) {
4296
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
4297
+ throw new Error("当前已有 Reset 正在执行,请稍候。");
4298
+ }
4299
+
4300
+ this.resetHistoryOperations.set(sessionId, turnId);
4301
+ try {
4302
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
4303
+ } catch (error) {
4304
+ if (this.resetHistoryOperations.get(sessionId) === turnId) {
4305
+ this.resetHistoryOperations.delete(sessionId);
4306
+ }
4307
+ throw error;
4308
+ }
4309
+
4310
+ try {
4311
+ await this.resetCurrentSessionToTurn(contextKey, sessionId, turnId);
4312
+ } catch (error) {
4313
+ if (this.resetHistoryOperations.get(sessionId) === turnId) {
4314
+ this.resetHistoryOperations.delete(sessionId);
4315
+ }
4316
+ try {
4317
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
4318
+ } catch (refreshError) {
4319
+ this.logger.warn(
4320
+ { error: refreshError, contextKey, sessionId, turnId, updateMessageId },
4321
+ "Failed to restore the Turns card after Reset failed.",
4322
+ );
4323
+ }
4324
+ throw error;
4325
+ }
4326
+
4327
+ if (this.resetHistoryOperations.get(sessionId) === turnId) {
4328
+ this.resetHistoryOperations.delete(sessionId);
4329
+ }
4330
+ await this.openResetHistory(contextKey, { expectedSessionId: sessionId, updateMessageId, page });
4331
+ }
4332
+
4094
4333
  private async performCurrentSessionReset(
4095
4334
  contextKey: string,
4096
4335
  sessionId: string,
@@ -4430,12 +4669,13 @@ export class ProxySessionController {
4430
4669
  reasoningEffort: session.reasoningEffort ?? settings.effort,
4431
4670
  permissionMode: session.permissionMode,
4432
4671
  });
4672
+ await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
4433
4673
  const notice = [
4434
4674
  `Provider 已切换为 ${cardCode(session.modelProvider ?? settings.provider)}`,
4435
4675
  `模型 ${cardCode(session.model ?? settings.model)}`,
4436
4676
  `思考强度 ${cardCode(session.reasoningEffort ?? settings.effort)}`,
4437
4677
  `权限 ${cardCode(session.permissionMode)}`,
4438
- "从下一次请求生效。",
4678
+ `并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
4439
4679
  ].join(",");
4440
4680
  if (options.updateMessageId) {
4441
4681
  await this.openProviderSelector(contextKey, {
@@ -4505,6 +4745,7 @@ export class ProxySessionController {
4505
4745
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, nextEffort);
4506
4746
  }
4507
4747
  this.store.updateRuntimeSession(loaded.record.localSessionId, { model, reasoningEffort: nextEffort });
4748
+ await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
4508
4749
  const effortMessage = nextEffort && nextEffort !== currentEffort
4509
4750
  ? `,思考强度已自动调整为 ${nextEffort}`
4510
4751
  : "";
@@ -4512,11 +4753,14 @@ export class ProxySessionController {
4512
4753
  await this.openExecutionSettings(contextKey, "model", {
4513
4754
  sessionId: loaded.record.localSessionId,
4514
4755
  updateMessageId: options.updateMessageId,
4515
- notice: `模型已切换为 ${cardCode(model)}${effortMessage},从下一次请求生效。`,
4756
+ notice: `模型已切换为 ${cardCode(model)}${effortMessage},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
4516
4757
  });
4517
4758
  return;
4518
4759
  }
4519
- await this.outbound.sendText(contextKey, `模型已切换为 ${model}${effortMessage},从下一次请求生效。`);
4760
+ await this.outbound.sendText(
4761
+ contextKey,
4762
+ `模型已切换为 ${model}${effortMessage},并已保存为 ${loaded.record.agentName} Agent 的默认设置,从下一次请求生效。`,
4763
+ );
4520
4764
  }
4521
4765
 
4522
4766
  private async goal(contextKey: string, command: Extract<Command, { type: "goal" }>): Promise<void> {
@@ -4899,15 +5143,19 @@ export class ProxySessionController {
4899
5143
  }
4900
5144
  await loaded.runtime.setReasoningEffort(loaded.record.localSessionId, effort);
4901
5145
  this.store.updateRuntimeSession(loaded.record.localSessionId, { reasoningEffort: effort });
5146
+ await this.persistAgentExecutionDefaults(loaded.record.localSessionId);
4902
5147
  if (options.updateMessageId) {
4903
5148
  await this.openExecutionSettings(contextKey, "thinking", {
4904
5149
  sessionId: loaded.record.localSessionId,
4905
5150
  updateMessageId: options.updateMessageId,
4906
- notice: `思考强度已切换为 ${cardCode(effort)},从下一次请求生效。`,
5151
+ notice: `思考强度已切换为 ${cardCode(effort)},并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
4907
5152
  });
4908
5153
  return;
4909
5154
  }
4910
- await this.outbound.sendText(contextKey, `思考强度已切换为 ${effort},从下一次请求生效。`);
5155
+ await this.outbound.sendText(
5156
+ contextKey,
5157
+ `思考强度已切换为 ${effort},并已保存为 ${loaded.record.agentName} Agent 的默认设置,从下一次请求生效。`,
5158
+ );
4911
5159
  }
4912
5160
 
4913
5161
  private async permissions(
@@ -4926,17 +5174,23 @@ export class ProxySessionController {
4926
5174
  await loaded.runtime.setPermissionMode(record.localSessionId, mode);
4927
5175
  loaded.session.permissionMode = mode;
4928
5176
  this.store.updateRuntimeSession(record.localSessionId, { permissionMode: mode });
5177
+ await this.persistAgentExecutionDefaults(record.localSessionId);
4929
5178
  if (options.updateMessageId) {
4930
5179
  await this.openExecutionSettings(contextKey, "permission", {
4931
5180
  sessionId: loaded.record.localSessionId,
4932
5181
  updateMessageId: options.updateMessageId,
4933
5182
  notice: mode === "auto"
4934
- ? "已切换为自动执行模式,从下一次请求生效。"
4935
- : "已切换为执行前确认模式,从下一次请求生效。",
5183
+ ? `已切换为自动执行模式,并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`
5184
+ : `已切换为执行前确认模式,并已保存为 ${cardCode(loaded.record.agentName)} Agent 的默认设置,从下一次请求生效。`,
4936
5185
  });
4937
5186
  return;
4938
5187
  }
4939
- await this.outbound.sendText(contextKey, mode === "auto" ? "已切换为自动执行模式。" : "已切换为执行前确认模式。");
5188
+ await this.outbound.sendText(
5189
+ contextKey,
5190
+ mode === "auto"
5191
+ ? `已切换为自动执行模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`
5192
+ : `已切换为执行前确认模式,并已保存为 ${loaded.record.agentName} Agent 的默认设置。`,
5193
+ );
4940
5194
  }
4941
5195
 
4942
5196
  private async openResetHistory(
@@ -4950,6 +5204,7 @@ export class ProxySessionController {
4950
5204
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4951
5205
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4952
5206
  }
5207
+ await this.hydrateLegacyForkTurnHistory(contextKey, current);
4953
5208
  const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
4954
5209
  const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
4955
5210
  const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
@@ -4996,6 +5251,7 @@ export class ProxySessionController {
4996
5251
  const page = Math.max(0, Math.min(Math.trunc(options.page ?? 0), totalPages - 1));
4997
5252
  const offset = page * RESET_HISTORY_PAGE_SIZE;
4998
5253
  const turns = allTurns.slice(offset, offset + RESET_HISTORY_PAGE_SIZE);
5254
+ const resettingTurnId = this.resetHistoryOperations.get(current.localSessionId);
4999
5255
  const entries: ResetHistoryCardEntry[] = turns.flatMap((turn, index) => {
5000
5256
  const snapshot = turnViewSnapshot(turn.snapshot);
5001
5257
  if (!snapshot) return [];
@@ -5006,17 +5262,17 @@ export class ProxySessionController {
5006
5262
  );
5007
5263
  const isCurrent = current.lastTurnId === turn.turnId && current.lastTurnStatus === "completed";
5008
5264
  const isRunning = runningTurnId === turn.turnId;
5265
+ const isResetting = resettingTurnId === turn.turnId;
5009
5266
  return [{
5010
5267
  sequence: graph.sequence,
5011
5268
  graphNodeLine: graph.nodeLine,
5012
5269
  graphConnectorLine: graph.connectorLine,
5013
- lines: [
5014
- cardText(summary || "未记录对话内容"),
5015
- `${formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt)} · ${cardText(turn.turnId)}`,
5016
- ],
5270
+ lines: [cardText(summary || "未记录对话内容")],
5271
+ timestamp: formatResetTurnTime(snapshot.completedAt ?? snapshot.startedAt),
5017
5272
  current: isCurrent,
5018
5273
  running: isRunning,
5019
- actions: isCurrent || isRunning ? undefined : [{
5274
+ resetting: isResetting,
5275
+ actions: isCurrent || isRunning || resettingTurnId ? undefined : [{
5020
5276
  text: "Reset",
5021
5277
  value: {
5022
5278
  action: "turn_reset",
@@ -5055,6 +5311,7 @@ export class ProxySessionController {
5055
5311
  runningTurn
5056
5312
  ? `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个 turn(${completedTurns.length} 个已完成,1 个运行中)`
5057
5313
  : `第 ${page + 1}/${totalPages} 页 · 共 ${total} 个已完成 turn`,
5314
+ ...(resettingTurnId ? ["正在 Reset 到所选轮次,请稍候…"] : []),
5058
5315
  ],
5059
5316
  pageActions,
5060
5317
  });
@@ -6179,6 +6436,21 @@ export class ProxySessionController {
6179
6436
  return agent;
6180
6437
  }
6181
6438
 
6439
+ private async persistAgentExecutionDefaults(localSessionId: string): Promise<void> {
6440
+ const session = this.store.getSession(localSessionId);
6441
+ if (!session) throw new Error(`找不到任务:${localSessionId}`);
6442
+ const agent = this.ensureAgent(session.agentName);
6443
+ const observed: AgentExecutionDefaults = {
6444
+ ...(session.modelProvider?.trim() ? { modelProvider: session.modelProvider } : {}),
6445
+ ...(session.model?.trim() ? { model: session.model } : {}),
6446
+ ...(session.reasoningEffort?.trim() ? { reasoningEffort: session.reasoningEffort } : {}),
6447
+ ...(session.permissionMode ? { permissionMode: session.permissionMode } : {}),
6448
+ };
6449
+ const defaults = { ...(agent.defaults ?? {}), ...observed };
6450
+ await this.lifecycle?.persistAgentExecutionDefaults?.(session.agentName, defaults);
6451
+ agent.defaults = defaults;
6452
+ }
6453
+
6182
6454
  private agentLabel(agentName: string): string {
6183
6455
  return this.ensureAgent(agentName).title;
6184
6456
  }
@@ -6246,6 +6518,18 @@ function threadForkAnchorMessageIds(message: IncomingMessage): string[] {
6246
6518
  .filter((messageId): messageId is string => Boolean(messageId && messageId !== message.messageId)))];
6247
6519
  }
6248
6520
 
6521
+ function mergeExecutionSettings(
6522
+ defaults: AgentExecutionDefaults,
6523
+ overrides: SessionExecutionSettings,
6524
+ ): SessionExecutionSettings {
6525
+ return {
6526
+ modelProvider: overrides.modelProvider ?? defaults.modelProvider,
6527
+ model: overrides.model ?? defaults.model,
6528
+ reasoningEffort: overrides.reasoningEffort ?? defaults.reasoningEffort,
6529
+ permissionMode: overrides.permissionMode ?? defaults.permissionMode,
6530
+ };
6531
+ }
6532
+
6249
6533
  function sessionStatusLabel(status: SessionRecord["status"], activeTurnId?: string): string {
6250
6534
  if (activeTurnId || status === "running") return "执行中";
6251
6535
  const labels: Record<SessionRecord["status"], string> = {
@@ -6737,6 +7021,12 @@ function normalizeRemoteTimestamp(value: number): number {
6737
7021
  return value >= 10_000_000_000 ? value : value * 1_000;
6738
7022
  }
6739
7023
 
7024
+ function parseIsoTimestamp(value: string | undefined): number | undefined {
7025
+ if (!value) return undefined;
7026
+ const timestamp = Date.parse(value);
7027
+ return Number.isFinite(timestamp) ? timestamp : undefined;
7028
+ }
7029
+
6740
7030
  function latestRemoteTimestamp(...values: Array<number | undefined>): number | undefined {
6741
7031
  const timestamps = values
6742
7032
  .filter((value): value is number => value !== undefined)
@@ -106,6 +106,13 @@ export interface RuntimeSessionMetadata {
106
106
 
107
107
  export type RemoteSessionStatus = "active" | "idle" | "not_loaded" | "error";
108
108
 
109
+ export interface RemoteCompletedTurnSummary {
110
+ id: string;
111
+ prompt?: string;
112
+ startedAt?: number;
113
+ completedAt?: number;
114
+ }
115
+
109
116
  export interface RemoteSessionSummary {
110
117
  id: string;
111
118
  title?: string;
@@ -131,6 +138,7 @@ export interface RemoteSessionSummary {
131
138
  lastTurnCompletedToolCount?: number;
132
139
  lastTurnFailedToolCount?: number;
133
140
  lastTurnRunningToolCount?: number;
141
+ completedTurns?: RemoteCompletedTurnSummary[];
134
142
  }
135
143
 
136
144
  export interface RemoteSessionPage {