@keyou007/agent-bot 0.1.16-alpha.0 → 0.1.16-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.
@@ -58,7 +58,7 @@ export interface InitializationWelcomeView {
58
58
  export interface SafeRestartStatusView {
59
59
  scheduleId: number;
60
60
  reason: string;
61
- phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled";
61
+ phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
62
62
  remainingMs?: number;
63
63
  pendingFinalDeliveries: number;
64
64
  waitingTasks: Array<{
@@ -70,6 +70,7 @@ export interface SafeRestartStatusView {
70
70
  export interface PromptQueueCardView {
71
71
  sessionId: string;
72
72
  contextKey: string;
73
+ phase?: "active" | "superseded";
73
74
  prompts: Array<{
74
75
  id: string;
75
76
  text: string;
@@ -665,7 +666,8 @@ export class CardRenderer {
665
666
  }
666
667
 
667
668
  renderPromptQueue(view: PromptQueueCardView): Record<string, unknown> {
668
- const elements = view.prompts.length === 0
669
+ const superseded = view.phase === "superseded";
670
+ const elements: Record<string, unknown>[] = view.prompts.length === 0
669
671
  ? [markdown("队列为空")]
670
672
  : view.prompts.map((prompt, index) => ({
671
673
  tag: "column_set",
@@ -688,7 +690,7 @@ export class CardRenderer {
688
690
  tag: "column",
689
691
  width: "auto",
690
692
  vertical_align: "center",
691
- elements: [{
693
+ elements: superseded ? [] : [{
692
694
  tag: "button",
693
695
  text: { tag: "plain_text", content: "Cancel" },
694
696
  type: "default",
@@ -706,6 +708,9 @@ export class CardRenderer {
706
708
  },
707
709
  ],
708
710
  }));
711
+ if (superseded) {
712
+ elements.push({ tag: "hr" }, markdown("此卡片已由新的排队卡片替代。"));
713
+ }
709
714
  return {
710
715
  schema: "2.0",
711
716
  config: {
@@ -714,7 +719,10 @@ export class CardRenderer {
714
719
  },
715
720
  header: {
716
721
  template: "grey",
717
- title: { tag: "plain_text", content: `排队 Prompt · ${view.prompts.length}` },
722
+ title: {
723
+ tag: "plain_text",
724
+ content: superseded ? "排队 Prompt · 已停止" : `排队 Prompt · ${view.prompts.length}`,
725
+ },
718
726
  padding: "8px 12px 8px 12px",
719
727
  },
720
728
  body: {
@@ -733,6 +741,8 @@ export class CardRenderer {
733
741
  ? "0s"
734
742
  : view.phase === "cancelled"
735
743
  ? "已取消"
744
+ : view.phase === "superseded"
745
+ ? "已停止"
736
746
  : "等待阻塞项清空后开始";
737
747
  const status = view.phase === "waiting_tasks"
738
748
  ? "🟠 等待任务完成"
@@ -742,7 +752,9 @@ export class CardRenderer {
742
752
  ? "🟡 空闲确认中"
743
753
  : view.phase === "restarting"
744
754
  ? "🔄 正在重启"
745
- : "⚪ 已取消";
755
+ : view.phase === "cancelled"
756
+ ? "⚪ 已取消"
757
+ : "⚪ 已停止";
746
758
  const lines = [
747
759
  `**状态**:${status}`,
748
760
  `**重启原因**:${inlineCode(view.reason)}`,
@@ -761,7 +773,7 @@ export class CardRenderer {
761
773
  } else {
762
774
  elements.push({ tag: "hr" }, markdown("**当前等待的任务**:无"));
763
775
  }
764
- if (view.phase !== "restarting" && view.phase !== "cancelled") {
776
+ if (view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded") {
765
777
  elements.push(
766
778
  { tag: "hr" },
767
779
  taskActionRow([{
@@ -775,7 +787,7 @@ export class CardRenderer {
775
787
  }
776
788
  const template = view.phase === "restarting"
777
789
  ? "blue"
778
- : view.phase === "cancelled"
790
+ : view.phase === "cancelled" || view.phase === "superseded"
779
791
  ? "grey"
780
792
  : "orange";
781
793
  return sectionCard("Agent Bot 安全重启", elements, template);
@@ -2503,17 +2503,28 @@ export class ProxySessionController {
2503
2503
 
2504
2504
  private async presentPromptQueueCard(localSessionId: string, contextKey: string): Promise<void> {
2505
2505
  await this.serializePromptQueueCardWrite(localSessionId, async () => {
2506
- const card = this.renderPromptQueueCard(localSessionId, contextKey);
2507
- const existing = this.queuedPromptCards.get(localSessionId)?.get(contextKey);
2506
+ const cards = this.queuedPromptCards.get(localSessionId);
2507
+ const existing = cards?.get(contextKey);
2508
2508
  if (existing) {
2509
2509
  try {
2510
- await this.outbound.updateInteractiveCard(contextKey, existing, card);
2511
- return;
2510
+ await this.outbound.updateInteractiveCard(
2511
+ contextKey,
2512
+ existing,
2513
+ this.renderPromptQueueCard(localSessionId, contextKey, "superseded"),
2514
+ );
2512
2515
  } catch (error) {
2513
- this.logger.warn({ error, localSessionId, contextKey, messageId: existing }, "Failed to update prompt queue card; sending a replacement.");
2516
+ this.logger.warn(
2517
+ { error, localSessionId, contextKey, messageId: existing },
2518
+ "Failed to stop the previous prompt queue card; sending a replacement.",
2519
+ );
2514
2520
  }
2521
+ cards?.delete(contextKey);
2522
+ if (cards?.size === 0) this.queuedPromptCards.delete(localSessionId);
2515
2523
  }
2516
- const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
2524
+ const messageId = await this.outbound.sendInteractiveCard(
2525
+ contextKey,
2526
+ this.renderPromptQueueCard(localSessionId, contextKey),
2527
+ );
2517
2528
  if (messageId) this.rememberPromptQueueCard(localSessionId, contextKey, messageId);
2518
2529
  });
2519
2530
  }
@@ -2536,10 +2547,15 @@ export class ProxySessionController {
2536
2547
  });
2537
2548
  }
2538
2549
 
2539
- private renderPromptQueueCard(localSessionId: string, contextKey: string): Record<string, unknown> {
2550
+ private renderPromptQueueCard(
2551
+ localSessionId: string,
2552
+ contextKey: string,
2553
+ phase: "active" | "superseded" = "active",
2554
+ ): Record<string, unknown> {
2540
2555
  return this.cardRenderer.renderPromptQueue({
2541
2556
  sessionId: localSessionId,
2542
2557
  contextKey,
2558
+ phase,
2543
2559
  prompts: this.store.listQueuedPrompts(localSessionId).map((prompt) => ({
2544
2560
  id: prompt.promptId,
2545
2561
  text: prompt.displayPrompt ?? prompt.text,
@@ -17,6 +17,7 @@ export class SafeRestartNotifier {
17
17
  private readonly notificationTargets = new Map<string, RestartNotificationTarget>();
18
18
  private activeScheduleId?: number;
19
19
  private terminalScheduleId?: number;
20
+ private lastStatus?: SafeRestartStatus;
20
21
  private pendingInitialStatus?: SafeRestartStatus;
21
22
  private initialCardTimer?: NodeJS.Timeout;
22
23
  private queue = Promise.resolve();
@@ -40,12 +41,24 @@ export class SafeRestartNotifier {
40
41
  this.terminalScheduleId = Math.max(this.terminalScheduleId ?? 0, status.scheduleId);
41
42
  }
42
43
  if (this.activeScheduleId !== status.scheduleId) {
44
+ const previousStatus = this.lastStatus ?? this.pendingInitialStatus;
45
+ const previousMessageIds = [...this.messageIds];
46
+ const previousTargets = new Map(this.notificationTargets);
47
+ if (
48
+ previousStatus
49
+ && previousStatus.phase !== "cancelled"
50
+ && previousStatus.phase !== "restarting"
51
+ && previousMessageIds.length > 0
52
+ ) {
53
+ this.enqueueSupersededCards(previousStatus, previousMessageIds, previousTargets);
54
+ }
43
55
  this.activeScheduleId = status.scheduleId;
44
56
  this.messageIds.clear();
45
57
  this.cardHashes.clear();
46
58
  this.notificationTargets.clear();
47
59
  this.clearInitialCardTimer();
48
60
  this.pendingInitialStatus = status;
61
+ this.lastStatus = status;
49
62
  this.collectNotificationTargets(status);
50
63
  const delayMs = status.phase === "cancelled"
51
64
  ? 0
@@ -59,6 +72,7 @@ export class SafeRestartNotifier {
59
72
  }, delayMs);
60
73
  return Promise.resolve();
61
74
  }
75
+ this.lastStatus = status;
62
76
  this.collectNotificationTargets(status);
63
77
  if (this.pendingInitialStatus) {
64
78
  this.pendingInitialStatus = status;
@@ -149,9 +163,13 @@ export class SafeRestartNotifier {
149
163
  } else {
150
164
  created = await this.outbound.sendInteractiveCard(contextKey, card);
151
165
  }
152
- if (created && this.activeScheduleId === status.scheduleId) {
153
- this.messageIds.set(contextKey, created);
154
- this.cardHashes.set(contextKey, cardHash);
166
+ if (created) {
167
+ if (this.activeScheduleId === status.scheduleId) {
168
+ this.messageIds.set(contextKey, created);
169
+ this.cardHashes.set(contextKey, cardHash);
170
+ } else {
171
+ await this.stopCard(created, status, reason ?? status.reason);
172
+ }
155
173
  }
156
174
  } catch (error) {
157
175
  this.logger.warn({ error, contextKey }, "Failed to send safe restart status card.");
@@ -159,6 +177,37 @@ export class SafeRestartNotifier {
159
177
  }));
160
178
  }
161
179
 
180
+ private enqueueSupersededCards(
181
+ status: SafeRestartStatus,
182
+ messageIds: Array<[string, string]>,
183
+ targets: Map<string, RestartNotificationTarget>,
184
+ ): void {
185
+ const queued = this.queue.catch(() => undefined).then(async () => {
186
+ await Promise.all(messageIds.map(async ([contextKey, messageId]) => {
187
+ const reason = targets.get(contextKey)?.reason ?? status.reason;
188
+ await this.stopCard(messageId, status, reason);
189
+ }));
190
+ });
191
+ this.queue = queued;
192
+ }
193
+
194
+ private async stopCard(messageId: string, status: SafeRestartStatus, reason: string): Promise<void> {
195
+ try {
196
+ await this.outbound.updateInteractiveCard(messageId, this.renderer.renderSafeRestartStatus({
197
+ scheduleId: status.scheduleId,
198
+ reason,
199
+ phase: "superseded",
200
+ pendingFinalDeliveries: status.activity.pendingFinalDeliveries,
201
+ waitingTasks: [],
202
+ }));
203
+ } catch (error) {
204
+ this.logger.warn(
205
+ { error, messageId, scheduleId: status.scheduleId },
206
+ "Failed to stop a superseded safe restart status card.",
207
+ );
208
+ }
209
+ }
210
+
162
211
  private collectNotificationTargets(status: SafeRestartStatus): void {
163
212
  for (const target of status.notificationTargets ?? []) {
164
213
  const contextKey = target.contextKey.trim();
@@ -58,9 +58,9 @@ export class SafeRestartScheduler {
58
58
  schedule(reason: string, notificationTarget?: RestartNotificationTarget): boolean {
59
59
  const newlyScheduled = !this.reason;
60
60
  if (newlyScheduled) {
61
- this.scheduleId += 1;
62
61
  this.notificationTargets = [];
63
62
  }
63
+ this.scheduleId += 1;
64
64
  this.addNotificationTarget(notificationTarget, reason);
65
65
  this.reason = reason;
66
66
  this.idleSince = undefined;
@@ -109,12 +109,13 @@ export class SafeRestartScheduler {
109
109
  private async poll(): Promise<void> {
110
110
  const reason = this.reason;
111
111
  if (!reason) return;
112
+ const scheduleId = this.scheduleId;
112
113
  const state = this.options.readActivity();
113
114
  if (state.runningSessions > 0 || state.pendingFinalDeliveries > 0) {
114
115
  this.idleSince = undefined;
115
116
  this.idleInboundAt = undefined;
116
117
  await this.emitStatus({
117
- scheduleId: this.scheduleId,
118
+ scheduleId,
118
119
  reason,
119
120
  notificationTargets: this.notificationTargetsSnapshot(),
120
121
  phase: state.runningSessions > 0 ? "waiting_tasks" : "waiting_delivery",
@@ -128,7 +129,7 @@ export class SafeRestartScheduler {
128
129
  this.idleSince = Date.now();
129
130
  this.idleInboundAt = latestInboundAt;
130
131
  await this.emitStatus({
131
- scheduleId: this.scheduleId,
132
+ scheduleId,
132
133
  reason,
133
134
  notificationTargets: this.notificationTargetsSnapshot(),
134
135
  phase: "countdown",
@@ -140,7 +141,7 @@ export class SafeRestartScheduler {
140
141
  const remainingMs = Math.max(0, quietPeriodMs - (Date.now() - this.idleSince));
141
142
  if (remainingMs > 0) {
142
143
  await this.emitStatus({
143
- scheduleId: this.scheduleId,
144
+ scheduleId,
144
145
  reason,
145
146
  notificationTargets: this.notificationTargetsSnapshot(),
146
147
  phase: "countdown",
@@ -159,7 +160,7 @@ export class SafeRestartScheduler {
159
160
  this.idleInboundAt = undefined;
160
161
  if (confirmed.runningSessions > 0 || confirmed.pendingFinalDeliveries > 0) {
161
162
  await this.emitStatus({
162
- scheduleId: this.scheduleId,
163
+ scheduleId,
163
164
  reason,
164
165
  notificationTargets: this.notificationTargetsSnapshot(),
165
166
  phase: confirmed.runningSessions > 0 ? "waiting_tasks" : "waiting_delivery",
@@ -167,7 +168,7 @@ export class SafeRestartScheduler {
167
168
  });
168
169
  } else {
169
170
  await this.emitStatus({
170
- scheduleId: this.scheduleId,
171
+ scheduleId,
171
172
  reason,
172
173
  notificationTargets: this.notificationTargetsSnapshot(),
173
174
  phase: "countdown",
@@ -177,7 +178,6 @@ export class SafeRestartScheduler {
177
178
  }
178
179
  return;
179
180
  }
180
- const scheduleId = this.scheduleId;
181
181
  const notificationTargets = this.notificationTargetsSnapshot();
182
182
  this.clear();
183
183
  await this.emitStatus({
@@ -230,7 +230,7 @@ export class SafeRestartScheduler {
230
230
  this.notificationTargets[existingIndex] = {
231
231
  ...existing,
232
232
  reason: target.reason,
233
- ...(!existing.replyMessageId && target.replyMessageId
233
+ ...(target.replyMessageId
234
234
  ? { replyMessageId: target.replyMessageId }
235
235
  : {}),
236
236
  };