@keyou007/agent-bot 0.1.17 → 0.1.18-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.
@@ -1757,7 +1757,7 @@ export class ProxySessionController {
1757
1757
  }
1758
1758
  switch (command.type) {
1759
1759
  case "shell":
1760
- await this.runShellCommand(contextKey, command.command, messageId);
1760
+ await this.runShellCommand(contextKey, command.command, messageId, incomingMessage);
1761
1761
  return;
1762
1762
  case "dir":
1763
1763
  await this.openDirectoryBrowser(contextKey, command.directory);
@@ -2817,10 +2817,35 @@ export class ProxySessionController {
2817
2817
  if (!runtime.readRemoteSession && !source) {
2818
2818
  throw new Error("当前 App Server Agent 不支持读取指定任务。");
2819
2819
  }
2820
- remote ??= runtime.readRemoteSession
2821
- ? await runtime.readRemoteSession(remoteSessionId)
2822
- : undefined;
2823
- const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId;
2820
+ const persistedCompletedTurns = source ? this.localForkCompletedTurns(source) : [];
2821
+ const sourceLastSnapshot = turnViewSnapshot(
2822
+ source?.lastTurnId ? this.store.getTurnSnapshot(source.lastTurnId) : undefined,
2823
+ );
2824
+ const localCompletedTurns = persistedCompletedTurns.length > 0
2825
+ ? persistedCompletedTurns
2826
+ : source?.lastTurnId
2827
+ && (source.lastTurnStatus === "completed" || sourceLastSnapshot?.status === "completed")
2828
+ ? [{
2829
+ id: source.lastTurnId,
2830
+ prompt: latestUserPromptFromTurnView(sourceLastSnapshot) ?? source.title,
2831
+ startedAt: sourceLastSnapshot?.startedAt ?? parseIsoTimestamp(source.updatedAt),
2832
+ completedAt: sourceLastSnapshot?.completedAt
2833
+ ?? sourceLastSnapshot?.startedAt
2834
+ ?? parseIsoTimestamp(source.updatedAt),
2835
+ }]
2836
+ : [];
2837
+ if (!remote && runtime.readRemoteSession && (!source || persistedCompletedTurns.length === 0)) {
2838
+ try {
2839
+ remote = await runtime.readRemoteSession(remoteSessionId);
2840
+ } catch (error) {
2841
+ if (localCompletedTurns.length === 0) throw error;
2842
+ this.logger.warn(
2843
+ { error, contextKey, remoteSessionId, sourceTurnId: localCompletedTurns.at(-1)?.id },
2844
+ "Failed to read the complete source task before Fork; using the locally persisted completed Turn.",
2845
+ );
2846
+ }
2847
+ }
2848
+ const latestTurnId = remote?.lastTurnId ?? source?.lastTurnId ?? localCompletedTurns.at(-1)?.id;
2824
2849
  const latestSnapshot = turnViewSnapshot(latestTurnId ? this.store.getTurnSnapshot(latestTurnId) : undefined);
2825
2850
  const isRunning = remote
2826
2851
  ? isRemoteSessionActive(remote)
@@ -2829,7 +2854,9 @@ export class ProxySessionController {
2829
2854
  || source?.lastTurnStatus === "running"
2830
2855
  || isTurnStillRunning(latestSnapshot?.status),
2831
2856
  );
2832
- const lastTurnId = isRunning ? remote?.lastCompletedTurnId : latestTurnId;
2857
+ const lastTurnId = remote?.lastCompletedTurnId
2858
+ ?? (remote?.lastTurnStatus === "completed" ? remote.lastTurnId : undefined)
2859
+ ?? localCompletedTurns.at(-1)?.id;
2833
2860
  if (!lastTurnId) {
2834
2861
  if (isRunning) {
2835
2862
  throw new Error(`${sourceLabel}正在执行,且还没有已完成轮次可供 fork。请等待当前轮次完成后重试。`);
@@ -2837,7 +2864,9 @@ export class ProxySessionController {
2837
2864
  throw new Error(`${sourceLabel}还没有可供 fork 的轮次。请先完成至少一轮对话。`);
2838
2865
  }
2839
2866
  const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(lastTurnId));
2840
- const forkedFromHistoricalTurn = isRunning && lastTurnId !== latestTurnId;
2867
+ const forkedFromHistoricalTurn = lastTurnId !== latestTurnId;
2868
+ const completedTurn = (remote?.completedTurns ?? localCompletedTurns)
2869
+ .find((turn) => turn.id === lastTurnId);
2841
2870
 
2842
2871
  const cwd = remote?.cwd || source?.cwd;
2843
2872
  if (!cwd) throw new Error("指定的 App Server 任务没有可用的工作目录,暂时不能 fork。");
@@ -2849,6 +2878,7 @@ export class ProxySessionController {
2849
2878
  const permissionMode = source?.permissionMode ?? "auto";
2850
2879
  const sourceTurnPrompt = truncateText(
2851
2880
  latestUserPromptFromTurnView(snapshot)
2881
+ ?? completedTurn?.prompt
2852
2882
  ?? (lastTurnId === latestTurnId ? remote?.lastUserPrompt : undefined)
2853
2883
  ?? remote?.preview
2854
2884
  ?? source?.title
@@ -2858,6 +2888,8 @@ export class ProxySessionController {
2858
2888
  );
2859
2889
  const sourceTurnCompletedAt = snapshot?.completedAt
2860
2890
  ?? snapshot?.startedAt
2891
+ ?? completedTurn?.completedAt
2892
+ ?? completedTurn?.startedAt
2861
2893
  ?? (lastTurnId === latestTurnId
2862
2894
  ? latestRemoteTimestamp(remote?.recencyAt, remote?.updatedAt)
2863
2895
  : undefined)
@@ -2876,20 +2908,33 @@ export class ProxySessionController {
2876
2908
  model,
2877
2909
  reasoningEffort,
2878
2910
  permissionMode,
2879
- lastTurnStatus: forkedFromHistoricalTurn
2880
- ? "completed"
2881
- : mapRemoteTurnStatus(remote?.lastTurnStatus)
2882
- ?? forkedTurnStatus(snapshot?.status)
2883
- ?? source?.lastTurnStatus,
2911
+ lastTurnStatus: "completed",
2884
2912
  sourceTurnPrompt,
2885
2913
  sourceTurnStartedAt: snapshot?.startedAt ?? sourceTurnCompletedAt,
2886
2914
  sourceTurnCompletedAt,
2887
- sourceCompletedTurns: remote?.completedTurns ?? [],
2915
+ sourceCompletedTurns: remote?.completedTurns
2916
+ ?? (persistedCompletedTurns.length > 0 ? [] : localCompletedTurns),
2888
2917
  sourceWasRunning: isRunning,
2889
2918
  forkedFromHistoricalTurn,
2890
2919
  };
2891
2920
  }
2892
2921
 
2922
+ private localForkCompletedTurns(source: SessionRecord): RemoteCompletedTurnSummary[] {
2923
+ return this.store.listTaskTurnGraph(source.localSessionId)
2924
+ .slice()
2925
+ .reverse()
2926
+ .map((record) => {
2927
+ const snapshot = turnViewSnapshot(record.snapshot);
2928
+ const startedAt = snapshot?.startedAt ?? parseIsoTimestamp(record.updatedAt);
2929
+ return {
2930
+ id: record.turnId,
2931
+ prompt: latestUserPromptFromTurnView(snapshot),
2932
+ startedAt,
2933
+ completedAt: snapshot?.completedAt ?? startedAt,
2934
+ };
2935
+ });
2936
+ }
2937
+
2893
2938
  private async prepareForkGroupSession(
2894
2939
  contextKey: string,
2895
2940
  requestedTitle: string | undefined,
@@ -4963,8 +5008,9 @@ export class ProxySessionController {
4963
5008
  contextKey: string,
4964
5009
  command: string,
4965
5010
  sourceMessageId?: string,
5011
+ incomingMessage?: IncomingMessage,
4966
5012
  ): Promise<void> {
4967
- const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
5013
+ const cwd = await this.shellCommandCwd(contextKey, incomingMessage);
4968
5014
  const created = await this.shellCommandJobs.createJob({ contextKey, sourceMessageId, command, cwd });
4969
5015
  let cardMessageId: string | undefined;
4970
5016
  const initialState: ShellCommandCardView = {
@@ -5010,6 +5056,42 @@ export class ProxySessionController {
5010
5056
  }
5011
5057
  }
5012
5058
 
5059
+ private async shellCommandCwd(contextKey: string, incomingMessage?: IncomingMessage): Promise<string> {
5060
+ const current = this.currentSession(contextKey);
5061
+ if (current) return current.cwd;
5062
+
5063
+ if (incomingMessage?.threadContext) {
5064
+ const anchorMessageIds = threadForkAnchorMessageIds(incomingMessage);
5065
+ const anchor = anchorMessageIds
5066
+ .map((messageId) => this.store.findTurnAnchorByMessageId(messageId))
5067
+ .find((candidate) => candidate !== undefined);
5068
+ const source = anchor?.contextKey
5069
+ ? this.store.getSessionForContext(anchor.localSessionId, anchor.contextKey)
5070
+ : anchor
5071
+ ? this.store.getSession(anchor.localSessionId)
5072
+ : undefined;
5073
+ const sourceContextKey = anchor?.contextKey ?? source?.contextKey;
5074
+ if (source && sourceContextKey && baseChatContextKey(sourceContextKey) === baseChatContextKey(contextKey)) {
5075
+ return source.cwd;
5076
+ }
5077
+
5078
+ for (const messageId of anchorMessageIds) {
5079
+ const commandJob = await this.shellCommandJobs.findJobByCardMessageId(messageId);
5080
+ if (commandJob && baseChatContextKey(commandJob.contextKey) === baseChatContextKey(contextKey)) {
5081
+ return commandJob.cwd;
5082
+ }
5083
+ }
5084
+
5085
+ const baseContextKey = baseChatContextKey(contextKey);
5086
+ const baseSession = this.currentSession(baseContextKey);
5087
+ if (baseSession) return baseSession.cwd;
5088
+ const boundProjectCwd = this.store.getUserContext(baseContextKey)?.boundProjectCwd;
5089
+ if (boundProjectCwd) return boundProjectCwd;
5090
+ }
5091
+
5092
+ return this.config.defaults.cwd;
5093
+ }
5094
+
5013
5095
  private monitorShellCommandJob(
5014
5096
  job: ShellCommandJobSnapshot,
5015
5097
  seededState?: ShellCommandCardView,
@@ -52,6 +52,7 @@ export interface ShellCommandJobSnapshot extends ShellCommandJobSpec, Omit<Shell
52
52
  export interface ShellCommandJobManagerLike {
53
53
  createJob(input: Pick<ShellCommandJobSpec, "contextKey" | "sourceMessageId" | "command" | "cwd">): Promise<ShellCommandJobSnapshot>;
54
54
  bindCard(jobId: string, cardMessageId: string): Promise<ShellCommandJobSnapshot>;
55
+ findJobByCardMessageId(cardMessageId: string): Promise<ShellCommandJobSnapshot | undefined>;
55
56
  startJob(jobId: string): Promise<void>;
56
57
  failJob(jobId: string, error: string): Promise<void>;
57
58
  readJob(jobId: string): Promise<ShellCommandJobSnapshot>;
@@ -68,6 +69,7 @@ export interface ShellCommandJobManagerOptions {
68
69
  export class ShellCommandJobManager implements ShellCommandJobManagerLike {
69
70
  private readonly runnerEntry: string;
70
71
  private readonly spawnRunner: (entry: string, jobDirectory: string) => Promise<void>;
72
+ private readonly jobIdByCardMessageId = new Map<string, string>();
71
73
 
72
74
  constructor(
73
75
  private readonly jobsRoot: string,
@@ -102,9 +104,37 @@ export class ShellCommandJobManager implements ShellCommandJobManagerLike {
102
104
  const jobDirectory = this.jobDirectory(jobId);
103
105
  const spec = await readJson<ShellCommandJobSpec>(path.join(jobDirectory, JOB_SPEC_FILE));
104
106
  await writeJsonAtomic(path.join(jobDirectory, JOB_SPEC_FILE), { ...spec, cardMessageId });
107
+ this.jobIdByCardMessageId.set(cardMessageId, jobId);
105
108
  return this.readJob(jobId);
106
109
  }
107
110
 
111
+ async findJobByCardMessageId(cardMessageId: string): Promise<ShellCommandJobSnapshot | undefined> {
112
+ const cachedJobId = this.jobIdByCardMessageId.get(cardMessageId);
113
+ if (cachedJobId) {
114
+ try {
115
+ return await this.readJob(cachedJobId);
116
+ } catch {
117
+ this.jobIdByCardMessageId.delete(cardMessageId);
118
+ }
119
+ }
120
+
121
+ const entries = await fs.readdir(this.jobsRoot, { withFileTypes: true }).catch((error: NodeJS.ErrnoException) => {
122
+ if (error.code === "ENOENT") return [];
123
+ throw error;
124
+ });
125
+ for (const entry of entries) {
126
+ if (!entry.isDirectory()) continue;
127
+ try {
128
+ const spec = await readJson<ShellCommandJobSpec>(path.join(this.jobsRoot, entry.name, JOB_SPEC_FILE));
129
+ if (spec.cardMessageId) this.jobIdByCardMessageId.set(spec.cardMessageId, spec.id);
130
+ if (spec.cardMessageId === cardMessageId) return this.readJob(spec.id);
131
+ } catch {
132
+ // Ignore partially written or unrelated job directories while rebuilding the card index.
133
+ }
134
+ }
135
+ return undefined;
136
+ }
137
+
108
138
  async startJob(jobId: string): Promise<void> {
109
139
  const jobDirectory = this.jobDirectory(jobId);
110
140
  await this.spawnRunner(this.runnerEntry, jobDirectory);