@keyou007/agent-bot 0.1.15-alpha.0 → 0.1.15-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 (56) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +23 -20
  3. package/README.zh.md +23 -20
  4. package/config.example.yaml +1 -0
  5. package/dist/cli/FeishuAppConfiguration.js +42 -3
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/feishu/CardRenderer.js +51 -8
  10. package/dist/feishu/CardRenderer.js.map +1 -1
  11. package/dist/feishu/FeishuConnector.js +17 -5
  12. package/dist/feishu/FeishuConnector.js.map +1 -1
  13. package/dist/feishu/FeishuMarkdown.js +58 -6
  14. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  15. package/dist/feishu/FeishuMessageClient.js +33 -4
  16. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  17. package/dist/feishu/FeishuTurnPresenter.js +7 -6
  18. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  19. package/dist/feishu/FinalResponseBranding.js +15 -0
  20. package/dist/feishu/FinalResponseBranding.js.map +1 -0
  21. package/dist/logging/DailyLogStream.js +73 -0
  22. package/dist/logging/DailyLogStream.js.map +1 -0
  23. package/dist/logging/logger.js +2 -1
  24. package/dist/logging/logger.js.map +1 -1
  25. package/dist/proxy/ProxySessionController.js +192 -36
  26. package/dist/proxy/ProxySessionController.js.map +1 -1
  27. package/dist/state/StateStore.js +126 -0
  28. package/dist/state/StateStore.js.map +1 -1
  29. package/dist/state/migrations.js +11 -0
  30. package/dist/state/migrations.js.map +1 -1
  31. package/dist/supervision/SupervisorDiagnostics.js +37 -21
  32. package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
  33. package/dist/supervisor.js +11 -4
  34. package/dist/supervisor.js.map +1 -1
  35. package/dist/utils/executeShellCommand.js +83 -16
  36. package/dist/utils/executeShellCommand.js.map +1 -1
  37. package/docs/technical-reference.md +9 -8
  38. package/docs/technical-reference.zh.md +9 -8
  39. package/npm-shrinkwrap.json +2 -2
  40. package/package.json +1 -1
  41. package/src/cli/FeishuAppConfiguration.ts +76 -3
  42. package/src/cli.ts +6 -1
  43. package/src/feishu/CardRenderer.ts +75 -9
  44. package/src/feishu/FeishuConnector.ts +28 -6
  45. package/src/feishu/FeishuMarkdown.ts +64 -5
  46. package/src/feishu/FeishuMessageClient.ts +41 -12
  47. package/src/feishu/FeishuTurnPresenter.ts +9 -6
  48. package/src/feishu/FinalResponseBranding.ts +18 -0
  49. package/src/logging/DailyLogStream.ts +88 -0
  50. package/src/logging/logger.ts +2 -1
  51. package/src/proxy/ProxySessionController.ts +208 -35
  52. package/src/state/StateStore.ts +180 -0
  53. package/src/state/migrations.ts +11 -0
  54. package/src/supervision/SupervisorDiagnostics.ts +41 -25
  55. package/src/supervisor.ts +10 -4
  56. package/src/utils/executeShellCommand.ts +103 -15
@@ -24,8 +24,10 @@ import {
24
24
  type ResetHistoryCardEntry,
25
25
  type SessionTaskCardEntry,
26
26
  type SessionTaskCardGroup,
27
+ type ShellCommandCardView,
27
28
  type TaskListCardAction,
28
29
  } from "../feishu/CardRenderer.js";
30
+ import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
29
31
  import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
30
32
  import { normalizeFeishuPostText } from "../feishu/InboundText.js";
31
33
  import { allowsFeishuUser } from "../feishu/ownerAccess.js";
@@ -54,10 +56,12 @@ import {
54
56
  type TurnAnchorRecord,
55
57
  } from "../state/StateStore.js";
56
58
  import { createId } from "../utils/id.js";
57
- import { asInlineCode, codeBlock, truncateMiddle, truncateText } from "../utils/markdown.js";
59
+ import { truncateMiddle, truncateText } from "../utils/markdown.js";
58
60
  import { normalizeTaskTitle } from "../utils/taskTitle.js";
59
61
  import {
60
62
  executeShellCommand,
63
+ type ShellCommandOptions,
64
+ type ShellCommandOutputSnapshot,
61
65
  type ShellCommandResult,
62
66
  } from "../utils/executeShellCommand.js";
63
67
 
@@ -98,6 +102,7 @@ const RESET_HISTORY_PAGE_SIZE = 10;
98
102
  const MAX_LLM_TURN_RETRIES = 3;
99
103
  const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
100
104
  const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
105
+ const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
101
106
  const FORWARD_ATTACHMENT_WINDOW_MS = 800;
102
107
  const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
103
108
  const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
@@ -366,7 +371,11 @@ export interface ProxyLifecycle {
366
371
  rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
367
372
  }
368
373
 
369
- export type ShellCommandExecutor = (command: string, cwd: string) => Promise<ShellCommandResult>;
374
+ export type ShellCommandExecutor = (
375
+ command: string,
376
+ cwd: string,
377
+ options?: ShellCommandOptions,
378
+ ) => Promise<ShellCommandResult>;
370
379
  export interface WindowsDriveInfo {
371
380
  root: string;
372
381
  label?: string;
@@ -686,7 +695,7 @@ export class ProxySessionController {
686
695
  if (isQueueIndependentCommand(command)) {
687
696
  await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
688
697
  try {
689
- if (command.type !== "forkgroup" && command.type !== "dismiss") await this.ensureThreadFork(message);
698
+ if (command.type === "prompt") await this.ensureThreadFork(message);
690
699
  await this.execute(
691
700
  message.contextKey,
692
701
  command,
@@ -709,7 +718,7 @@ export class ProxySessionController {
709
718
  const next = previous.catch(() => undefined).then(() =>
710
719
  this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
711
720
  try {
712
- if (command.type !== "forkgroup") await this.ensureThreadFork(message);
721
+ if (command.type === "prompt") await this.ensureThreadFork(message);
713
722
  await this.execute(
714
723
  message.contextKey,
715
724
  command,
@@ -1488,8 +1497,7 @@ export class ProxySessionController {
1488
1497
  if (!session.remoteSessionId || !this.isCodexSession(session)) {
1489
1498
  throw new Error("The task is not an App Server task that can be reset.");
1490
1499
  }
1491
- const contextKey = this.controlSessionContextKey(session);
1492
- const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
1500
+ const rows = this.store.listTaskTurnGraph(session.localSessionId);
1493
1501
  const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
1494
1502
  return {
1495
1503
  session,
@@ -1641,6 +1649,9 @@ export class ProxySessionController {
1641
1649
  incomingMessage?: IncomingMessage,
1642
1650
  ): Promise<void> {
1643
1651
  const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
1652
+ if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
1653
+ throw new Error(unboundThreadTaskMessage());
1654
+ }
1644
1655
  switch (command.type) {
1645
1656
  case "shell":
1646
1657
  await this.runShellCommand(contextKey, command.command);
@@ -3308,7 +3319,13 @@ export class ProxySessionController {
3308
3319
  const existing = runtime.getSession(record.localSessionId);
3309
3320
  if (existing) return { record, runtime, session: existing };
3310
3321
  const pending = this.sessionLoads.get(record.localSessionId);
3311
- if (pending) return pending;
3322
+ if (pending) {
3323
+ const loaded = await pending;
3324
+ return {
3325
+ ...loaded,
3326
+ record: { ...loaded.record, contextKey: record.contextKey },
3327
+ };
3328
+ }
3312
3329
 
3313
3330
  this.outbound.registerSession(
3314
3331
  record.localSessionId,
@@ -3377,7 +3394,11 @@ export class ProxySessionController {
3377
3394
  }
3378
3395
  this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
3379
3396
  const saved = this.store.getSession(record.localSessionId) ?? record;
3380
- return { record: saved, runtime, session };
3397
+ return {
3398
+ record: { ...saved, contextKey: record.contextKey },
3399
+ runtime,
3400
+ session,
3401
+ };
3381
3402
  })();
3382
3403
  this.sessionLoads.set(record.localSessionId, loading);
3383
3404
  try {
@@ -3476,6 +3497,7 @@ export class ProxySessionController {
3476
3497
  }
3477
3498
 
3478
3499
  if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
3500
+ await this.refreshGoalCards(event.sessionId);
3479
3501
  const terminalStatus = event.type === "turn_completed"
3480
3502
  ? "completed"
3481
3503
  : event.type === "turn_cancelled"
@@ -3883,8 +3905,10 @@ export class ProxySessionController {
3883
3905
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
3884
3906
  }
3885
3907
 
3886
- const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(turnId));
3887
- if (!snapshot || snapshot.sessionId !== current.localSessionId || snapshot.status !== "completed") {
3908
+ const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
3909
+ .find((turn) => turn.turnId === turnId);
3910
+ const snapshot = turnViewSnapshot(historyTurn?.snapshot);
3911
+ if (!snapshot || snapshot.status !== "completed") {
3888
3912
  throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
3889
3913
  }
3890
3914
  if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
@@ -3905,7 +3929,7 @@ export class ProxySessionController {
3905
3929
  }
3906
3930
 
3907
3931
  const origin = this.store.getTurnRuntimeOrigin(turnId);
3908
- if (origin && (origin.localSessionId !== current.localSessionId || origin.agentName !== current.agentName)) {
3932
+ if (origin && origin.agentName !== current.agentName) {
3909
3933
  throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
3910
3934
  }
3911
3935
  const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
@@ -4369,6 +4393,25 @@ export class ProxySessionController {
4369
4393
  notice?: string,
4370
4394
  record = this.currentSession(contextKey),
4371
4395
  ): Promise<void> {
4396
+ const card = this.renderGoalCard(goal, notice, record);
4397
+ if (!record) {
4398
+ await this.outbound.sendInteractiveCard(contextKey, card);
4399
+ return;
4400
+ }
4401
+ const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
4402
+ if (existing) {
4403
+ await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
4404
+ return;
4405
+ }
4406
+ const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
4407
+ if (messageId) this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
4408
+ }
4409
+
4410
+ private renderGoalCard(
4411
+ goal: RuntimeGoal | undefined,
4412
+ notice?: string,
4413
+ record?: SessionRecord,
4414
+ ): Record<string, unknown> {
4372
4415
  const sections: CardSection[] = [];
4373
4416
  if (notice) sections.push({ lines: [cardText(notice)] });
4374
4417
  sections.push(goal
@@ -4390,24 +4433,138 @@ export class ProxySessionController {
4390
4433
  "**/goal edit &#60;新目标&#62;** 修改  **/goal clear** 清除",
4391
4434
  ],
4392
4435
  });
4393
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Goal", sections));
4436
+ const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
4437
+ return this.cardRenderer.renderSectionsCard(title, sections);
4438
+ }
4439
+
4440
+ private async refreshGoalCards(localSessionId: string): Promise<void> {
4441
+ const deliveries = this.store.listGoalCardDeliveries(localSessionId);
4442
+ if (deliveries.length === 0) return;
4443
+ const record = this.store.getSession(localSessionId);
4444
+ if (!record) return;
4445
+ const runtime = this.runtimes.forAgent(record.agentName);
4446
+ if (runtime.kind !== "codex" || !runtime.getGoal) return;
4447
+
4448
+ try {
4449
+ const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
4450
+ const goal = await runtime.getGoal(loaded.localSessionId);
4451
+ const card = this.renderGoalCard(goal, undefined, record);
4452
+ await Promise.all(deliveries.map(async (delivery) => {
4453
+ try {
4454
+ await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
4455
+ } catch (error) {
4456
+ this.logger.warn(
4457
+ { error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId },
4458
+ "Failed to refresh an Agent Goal card.",
4459
+ );
4460
+ }
4461
+ }));
4462
+ } catch (error) {
4463
+ this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
4464
+ }
4394
4465
  }
4395
4466
 
4396
4467
  private async runShellCommand(contextKey: string, command: string): Promise<void> {
4397
4468
  const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
4398
- const result = await this.shellCommandExecutor(command, cwd);
4399
- const outputParts = [
4400
- result.stdout.trimEnd(),
4401
- result.stderr.trimEnd() ? `[stderr]\n${result.stderr.trimEnd()}` : "",
4402
- ].filter(Boolean);
4403
- const output = truncateMiddle(outputParts.join("\n"), 6_000);
4404
- const status = result.timedOut
4405
- ? "已超时(120s)"
4406
- : `退出码 ${result.exitCode ?? "未知"}`;
4407
- await this.outbound.sendMarkdown(contextKey, [
4408
- codeBlock(`$ ${command.trim()}\n${output || "(无输出)"}`, "text"),
4409
- `${asInlineCode(cwd)} · ${status}${result.outputTruncated ? " · 输出已截断" : ""}`,
4410
- ].join("\n"));
4469
+ const startedAt = Date.now();
4470
+ let latestOutput: ShellCommandOutputSnapshot = {
4471
+ stdout: "",
4472
+ stderr: "",
4473
+ outputTruncated: false,
4474
+ };
4475
+ let runningState: ShellCommandCardView = {
4476
+ command,
4477
+ cwd,
4478
+ ...latestOutput,
4479
+ status: "running",
4480
+ elapsedMs: 0,
4481
+ };
4482
+ const cardMessageId = await this.outbound.sendInteractiveCard(
4483
+ contextKey,
4484
+ this.cardRenderer.renderShellCommandCard(runningState),
4485
+ );
4486
+ const scheduler = cardMessageId
4487
+ ? new CardUpdateScheduler<ShellCommandCardView>({
4488
+ render: (state) => this.cardRenderer.renderShellCommandCard(state),
4489
+ write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
4490
+ onError: (error) => this.logger.warn(
4491
+ { error, contextKey, messageId: cardMessageId },
4492
+ "Failed to update a shell command card.",
4493
+ ),
4494
+ })
4495
+ : undefined;
4496
+ scheduler?.seed(runningState);
4497
+ const elapsedTimer = scheduler ? setInterval(() => {
4498
+ runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
4499
+ scheduler.update(runningState);
4500
+ }, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
4501
+ elapsedTimer?.unref?.();
4502
+
4503
+ const updateRunningOutput = (snapshot: ShellCommandOutputSnapshot): void => {
4504
+ latestOutput = snapshot;
4505
+ runningState = {
4506
+ ...runningState,
4507
+ ...snapshot,
4508
+ elapsedMs: Date.now() - startedAt,
4509
+ };
4510
+ scheduler?.update(runningState);
4511
+ };
4512
+
4513
+ let result: ShellCommandResult;
4514
+ try {
4515
+ result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
4516
+ } catch (error) {
4517
+ if (elapsedTimer) clearInterval(elapsedTimer);
4518
+ const failedCard = this.cardRenderer.renderShellCommandCard({
4519
+ command,
4520
+ cwd,
4521
+ ...latestOutput,
4522
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4523
+ status: "failed",
4524
+ elapsedMs: Date.now() - startedAt,
4525
+ });
4526
+ if (scheduler) {
4527
+ try {
4528
+ await scheduler.flush({
4529
+ ...runningState,
4530
+ stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
4531
+ status: "failed",
4532
+ elapsedMs: Date.now() - startedAt,
4533
+ });
4534
+ } catch (cardError) {
4535
+ this.logger.warn(
4536
+ { error: cardError, contextKey, messageId: cardMessageId },
4537
+ "Failed to finalize a shell command card.",
4538
+ );
4539
+ } finally {
4540
+ scheduler.dispose();
4541
+ }
4542
+ } else {
4543
+ await this.outbound.sendInteractiveCard(contextKey, failedCard);
4544
+ }
4545
+ throw error;
4546
+ }
4547
+
4548
+ if (elapsedTimer) clearInterval(elapsedTimer);
4549
+ const finalState: ShellCommandCardView = {
4550
+ command,
4551
+ cwd,
4552
+ stdout: result.stdout,
4553
+ stderr: result.stderr,
4554
+ status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
4555
+ exitCode: result.exitCode,
4556
+ elapsedMs: Date.now() - startedAt,
4557
+ outputTruncated: result.outputTruncated,
4558
+ };
4559
+ if (scheduler) {
4560
+ try {
4561
+ await scheduler.flush(finalState);
4562
+ } finally {
4563
+ scheduler.dispose();
4564
+ }
4565
+ } else {
4566
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
4567
+ }
4411
4568
  }
4412
4569
 
4413
4570
  private async setTitle(contextKey: string, title: string): Promise<void> {
@@ -4524,7 +4681,7 @@ export class ProxySessionController {
4524
4681
  if (!current.remoteSessionId || !this.isCodexSession(current)) {
4525
4682
  throw new Error("当前任务不是可 Reset 的 App Server 任务。");
4526
4683
  }
4527
- const allTurns = this.store.listCompletedTurnGraph(current.localSessionId, contextKey);
4684
+ const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
4528
4685
  const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
4529
4686
  turnId: turn.turnId,
4530
4687
  parentTurnId: turn.parentTurnId,
@@ -4666,7 +4823,6 @@ export class ProxySessionController {
4666
4823
  contextKey,
4667
4824
  },
4668
4825
  },
4669
- actions: directoryCreationActions(entry.path, contextKey),
4670
4826
  }
4671
4827
  : {
4672
4828
  name: entry.name,
@@ -4691,7 +4847,7 @@ export class ProxySessionController {
4691
4847
  footerLines: [
4692
4848
  `第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
4693
4849
  "",
4694
- "> 点击目录名称进入,点击文件名称发送到当前会话;**New** 在该目录创建任务,**NewGroup** 在该目录创建新群和任务。",
4850
+ "> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部的 **New** 和 **NewGroup** 使用当前目录。",
4695
4851
  ],
4696
4852
  });
4697
4853
  if (options.updateMessageId) {
@@ -4723,7 +4879,6 @@ export class ProxySessionController {
4723
4879
  contextKey,
4724
4880
  },
4725
4881
  },
4726
- actions: directoryCreationActions(drive.root, contextKey),
4727
4882
  }));
4728
4883
  const card = this.cardRenderer.renderDirectoryBrowserCard({
4729
4884
  directory: "此电脑",
@@ -4738,7 +4893,7 @@ export class ProxySessionController {
4738
4893
  footerLines: [
4739
4894
  `第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
4740
4895
  "",
4741
- "> 点击磁盘名称进入;**New** 在磁盘根目录创建任务,**NewGroup** 在磁盘根目录创建新群和任务。",
4896
+ "> 点击磁盘名称进入。",
4742
4897
  ],
4743
4898
  });
4744
4899
  if (options.updateMessageId) {
@@ -5420,7 +5575,9 @@ export class ProxySessionController {
5420
5575
  let activeTurnId: string | undefined;
5421
5576
  let queued = 0;
5422
5577
  if (!current) {
5423
- taskLines.push("无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5578
+ taskLines.push(isThreadContextKey(contextKey)
5579
+ ? unboundThreadTaskMessage()
5580
+ : "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
5424
5581
  } else {
5425
5582
  const agent = this.ensureAgent(current.agentName);
5426
5583
  const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
@@ -5696,7 +5853,11 @@ export class ProxySessionController {
5696
5853
 
5697
5854
  private requireCurrentSession(contextKey: string): SessionRecord {
5698
5855
  const record = this.currentSession(contextKey);
5699
- if (!record) throw new Error("当前没有任务,直接发送一条消息即可自动创建。");
5856
+ if (!record) {
5857
+ throw new Error(isThreadContextKey(contextKey)
5858
+ ? unboundThreadTaskMessage()
5859
+ : "当前没有任务,直接发送一条消息即可自动创建。");
5860
+ }
5700
5861
  return record;
5701
5862
  }
5702
5863
 
@@ -6213,6 +6374,19 @@ function isQueueIndependentCommand(command: Command): boolean {
6213
6374
  return false;
6214
6375
  }
6215
6376
 
6377
+ function commandRequiresCurrentSession(command: Command): boolean {
6378
+ if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
6379
+ return true;
6380
+ }
6381
+ if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
6382
+ if (command.type === "goal") return command.action !== "set";
6383
+ return false;
6384
+ }
6385
+
6386
+ function unboundThreadTaskMessage(): string {
6387
+ return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
6388
+ }
6389
+
6216
6390
  function parseSessionPage(value: unknown): number {
6217
6391
  const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
6218
6392
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
@@ -6554,14 +6728,13 @@ const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
6554
6728
 
6555
6729
  function formatGroupProjectDirectory(projectCwd: string): string {
6556
6730
  const value = abbreviateHomeDirectory(projectCwd);
6557
- if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
6558
-
6559
6731
  const levels = value
6560
6732
  .replace(/[\\/]+$/, "")
6561
6733
  .split(/[\\/]+/)
6562
6734
  .filter(Boolean)
6563
6735
  .slice(-2);
6564
- return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, path.sep)}]`;
6736
+ const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
6737
+ return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
6565
6738
  }
6566
6739
 
6567
6740
  function abbreviateHomeDirectory(value: string): string {
@@ -68,6 +68,13 @@ export interface AgentBotTurnMessageRecord extends TurnAnchorRecord {
68
68
  messageKind: "progress" | "final";
69
69
  }
70
70
 
71
+ export interface GoalCardDeliveryRecord {
72
+ localSessionId: string;
73
+ contextKey: string;
74
+ messageId: string;
75
+ updatedAt: string;
76
+ }
77
+
71
78
  export interface TurnRuntimeOriginRecord {
72
79
  turnId: string;
73
80
  localSessionId: string;
@@ -242,6 +249,46 @@ export class StateStore {
242
249
  `);
243
250
  }
244
251
 
252
+ saveGoalCardDelivery(localSessionId: string, contextKey: string, messageId: string): void {
253
+ const updatedAt = new Date().toISOString();
254
+ this.db.prepare(`
255
+ INSERT INTO goal_card_deliveries (local_session_id, context_key, message_id, updated_at)
256
+ VALUES (?, ?, ?, ?)
257
+ ON CONFLICT(local_session_id, context_key) DO UPDATE SET
258
+ message_id = excluded.message_id,
259
+ updated_at = excluded.updated_at
260
+ `).run(localSessionId, contextKey, messageId, updatedAt);
261
+ }
262
+
263
+ getGoalCardDelivery(localSessionId: string, contextKey: string): GoalCardDeliveryRecord | undefined {
264
+ const row = this.db.prepare(`
265
+ SELECT local_session_id, context_key, message_id, updated_at
266
+ FROM goal_card_deliveries
267
+ WHERE local_session_id = ? AND context_key = ?
268
+ `).get(localSessionId, contextKey) as {
269
+ local_session_id: string;
270
+ context_key: string;
271
+ message_id: string;
272
+ updated_at: string;
273
+ } | undefined;
274
+ return row ? goalCardDeliveryFromRow(row) : undefined;
275
+ }
276
+
277
+ listGoalCardDeliveries(localSessionId: string): GoalCardDeliveryRecord[] {
278
+ const rows = this.db.prepare(`
279
+ SELECT local_session_id, context_key, message_id, updated_at
280
+ FROM goal_card_deliveries
281
+ WHERE local_session_id = ?
282
+ ORDER BY updated_at ASC
283
+ `).all(localSessionId) as Array<{
284
+ local_session_id: string;
285
+ context_key: string;
286
+ message_id: string;
287
+ updated_at: string;
288
+ }>;
289
+ return rows.map(goalCardDeliveryFromRow);
290
+ }
291
+
245
292
  close(): void {
246
293
  this.db.close();
247
294
  }
@@ -1058,6 +1105,91 @@ export class StateStore {
1058
1105
  }));
1059
1106
  }
1060
1107
 
1108
+ listTaskTurnGraph(localSessionId: string): CompletedTurnSnapshotRecord[] {
1109
+ this.backfillTurnParents(localSessionId);
1110
+ const records = new Map(
1111
+ this.listCompletedTurnGraphRows(localSessionId).map((record) => [record.turnId, record]),
1112
+ );
1113
+ let ancestorTurnId = this.findForkSourceTurnId(localSessionId)
1114
+ ?? this.findCrossSessionParentTurnId(localSessionId);
1115
+ const visited = new Set<string>();
1116
+
1117
+ while (ancestorTurnId && !visited.has(ancestorTurnId)) {
1118
+ visited.add(ancestorTurnId);
1119
+ const ancestor = this.getCompletedTurnGraphRecord(ancestorTurnId);
1120
+ if (!ancestor) break;
1121
+ records.set(ancestor.turnId, ancestor);
1122
+ ancestorTurnId = ancestor.parentTurnId;
1123
+ }
1124
+
1125
+ return [...records.values()].sort(compareCompletedTurnSnapshots);
1126
+ }
1127
+
1128
+ private listCompletedTurnGraphRows(localSessionId: string): CompletedTurnSnapshotRecord[] {
1129
+ const rows = this.db.prepare(`
1130
+ SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
1131
+ FROM turn_snapshots ts
1132
+ LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
1133
+ WHERE ts.local_session_id = ?
1134
+ AND json_extract(ts.snapshot_json, '$.status') = 'completed'
1135
+ `).all(localSessionId) as Array<{
1136
+ turn_id: string;
1137
+ parent_turn_id: string | null;
1138
+ snapshot_json: string;
1139
+ updated_at: string;
1140
+ }>;
1141
+ return rows.map(mapCompletedTurnSnapshot);
1142
+ }
1143
+
1144
+ private getCompletedTurnGraphRecord(turnId: string): CompletedTurnSnapshotRecord | undefined {
1145
+ const owner = this.db.prepare(`
1146
+ SELECT local_session_id
1147
+ FROM turn_snapshots
1148
+ WHERE turn_id = ?
1149
+ AND json_extract(snapshot_json, '$.status') = 'completed'
1150
+ `).get(turnId) as { local_session_id: string } | undefined;
1151
+ if (!owner) return undefined;
1152
+ this.backfillTurnParents(owner.local_session_id);
1153
+ const row = this.db.prepare(`
1154
+ SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
1155
+ FROM turn_snapshots ts
1156
+ LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
1157
+ WHERE ts.turn_id = ?
1158
+ AND json_extract(ts.snapshot_json, '$.status') = 'completed'
1159
+ `).get(turnId) as {
1160
+ turn_id: string;
1161
+ parent_turn_id: string | null;
1162
+ snapshot_json: string;
1163
+ updated_at: string;
1164
+ } | undefined;
1165
+ return row ? mapCompletedTurnSnapshot(row) : undefined;
1166
+ }
1167
+
1168
+ private findForkSourceTurnId(localSessionId: string): string | undefined {
1169
+ const row = this.db.prepare(`
1170
+ SELECT json_extract(payload_json, '$.sourceTurnId') AS source_turn_id
1171
+ FROM audit_events
1172
+ WHERE event_type IN ('session_forked', 'thread_forked')
1173
+ AND json_extract(payload_json, '$.forkedLocalSessionId') = ?
1174
+ ORDER BY id DESC
1175
+ LIMIT 1
1176
+ `).get(localSessionId) as { source_turn_id: string | null } | undefined;
1177
+ return row?.source_turn_id ?? undefined;
1178
+ }
1179
+
1180
+ private findCrossSessionParentTurnId(localSessionId: string): string | undefined {
1181
+ const row = this.db.prepare(`
1182
+ SELECT links.parent_turn_id
1183
+ FROM turn_parent_links links
1184
+ JOIN turn_snapshots parent ON parent.turn_id = links.parent_turn_id
1185
+ WHERE links.local_session_id = ?
1186
+ AND parent.local_session_id <> ?
1187
+ ORDER BY links.created_at ASC
1188
+ LIMIT 1
1189
+ `).get(localSessionId, localSessionId) as { parent_turn_id: string } | undefined;
1190
+ return row?.parent_turn_id;
1191
+ }
1192
+
1061
1193
  private backfillTurnParents(localSessionId: string): void {
1062
1194
  const turns = this.db.prepare(`
1063
1195
  SELECT
@@ -1873,6 +2005,20 @@ function mapMessageReaction(row: MessageReactionRow): MessageReactionRecord {
1873
2005
  };
1874
2006
  }
1875
2007
 
2008
+ function goalCardDeliveryFromRow(row: {
2009
+ local_session_id: string;
2010
+ context_key: string;
2011
+ message_id: string;
2012
+ updated_at: string;
2013
+ }): GoalCardDeliveryRecord {
2014
+ return {
2015
+ localSessionId: row.local_session_id,
2016
+ contextKey: row.context_key,
2017
+ messageId: row.message_id,
2018
+ updatedAt: row.updated_at,
2019
+ };
2020
+ }
2021
+
1876
2022
  function mapQueuedPrompt(row: QueuedPromptRow): QueuedPromptRecord {
1877
2023
  const parsedPaths = JSON.parse(row.local_image_paths_json) as unknown;
1878
2024
  const localImagePaths = Array.isArray(parsedPaths)
@@ -1915,6 +2061,40 @@ function mapTurnAttempt(row: TurnAttemptRow): TurnAttemptRecord {
1915
2061
  };
1916
2062
  }
1917
2063
 
2064
+ function mapCompletedTurnSnapshot(row: {
2065
+ turn_id: string;
2066
+ parent_turn_id: string | null;
2067
+ snapshot_json: string;
2068
+ updated_at: string;
2069
+ }): CompletedTurnSnapshotRecord {
2070
+ return {
2071
+ turnId: row.turn_id,
2072
+ parentTurnId: row.parent_turn_id ?? undefined,
2073
+ snapshot: JSON.parse(row.snapshot_json),
2074
+ updatedAt: row.updated_at,
2075
+ };
2076
+ }
2077
+
2078
+ function compareCompletedTurnSnapshots(
2079
+ left: CompletedTurnSnapshotRecord,
2080
+ right: CompletedTurnSnapshotRecord,
2081
+ ): number {
2082
+ const timeDifference = completedTurnTimestamp(right) - completedTurnTimestamp(left);
2083
+ return timeDifference || right.updatedAt.localeCompare(left.updatedAt);
2084
+ }
2085
+
2086
+ function completedTurnTimestamp(record: CompletedTurnSnapshotRecord): number {
2087
+ const snapshot = record.snapshot && typeof record.snapshot === "object"
2088
+ ? record.snapshot as { completedAt?: unknown; startedAt?: unknown }
2089
+ : undefined;
2090
+ const timestamp = typeof snapshot?.completedAt === "number"
2091
+ ? snapshot.completedAt
2092
+ : typeof snapshot?.startedAt === "number"
2093
+ ? snapshot.startedAt
2094
+ : Date.parse(record.updatedAt);
2095
+ return Number.isFinite(timestamp) ? timestamp : 0;
2096
+ }
2097
+
1918
2098
  function isIncompleteTurnAttemptStatus(status: TurnAttemptStatus): boolean {
1919
2099
  return status === "accepted" || status === "running" || status === "recovering";
1920
2100
  }
@@ -173,4 +173,15 @@ export const migrations = [
173
173
  CREATE INDEX IF NOT EXISTS idx_turn_attempts_turn
174
174
  ON turn_attempts(turn_id);
175
175
  `,
176
+ `
177
+ CREATE TABLE IF NOT EXISTS goal_card_deliveries (
178
+ local_session_id TEXT NOT NULL,
179
+ context_key TEXT NOT NULL,
180
+ message_id TEXT NOT NULL,
181
+ updated_at TEXT NOT NULL,
182
+ PRIMARY KEY (local_session_id, context_key)
183
+ );
184
+ CREATE INDEX IF NOT EXISTS idx_goal_card_deliveries_session
185
+ ON goal_card_deliveries(local_session_id);
186
+ `,
176
187
  ] as const;