@co0ontty/wand 4.83.1 → 4.84.0

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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -264,6 +264,9 @@ export class ProviderHistoryScanner {
264
264
  codexSessionsDir;
265
265
  openCodeDatabasePath;
266
266
  qoderProjectsDirs;
267
+ piSessionsDir;
268
+ grokSessionsDir;
269
+ geminiHome;
267
270
  claudeIndex = new Map();
268
271
  codexIndex = new Map();
269
272
  qoderIndex = new Map();
@@ -280,6 +283,9 @@ export class ProviderHistoryScanner {
280
283
  path.join(os.homedir(), ".qoder", "projects"),
281
284
  path.join(os.homedir(), ".qoder-cn", "projects"),
282
285
  ];
286
+ this.piSessionsDir = options.piSessionsDir ?? path.join(os.homedir(), ".pi", "agent", "sessions");
287
+ this.grokSessionsDir = options.grokSessionsDir ?? path.join(os.homedir(), ".grok", "sessions");
288
+ this.geminiHome = options.geminiHome ?? path.join(os.homedir(), ".gemini");
283
289
  }
284
290
  getDiagnostics() {
285
291
  return {
@@ -611,11 +617,25 @@ export class ProviderHistoryScanner {
611
617
  let database = null;
612
618
  try {
613
619
  database = new DatabaseSync(this.openCodeDatabasePath);
620
+ // This is OpenCode's own database, shared with every live `opencode run`. Default
621
+ // busy_timeout is 0, so one concurrent writer turned a delete into an instant
622
+ // SQLITE_BUSY that we swallowed as "0 deleted".
623
+ database.exec("PRAGMA busy_timeout = 5000");
614
624
  database.exec("PRAGMA foreign_keys = ON");
625
+ // `session` has no foreign key to `event_sequence`, so deleting a session row alone
626
+ // orphans every event row it ever wrote (measured: 425MB across 28 deleted sessions).
627
+ // Removing the sequence row first cascades to `event` and actually reclaims the space.
628
+ // The event tables only exist on newer schemas, so their absence must not fail the delete.
629
+ const hasEventSequence = database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'event_sequence'").get() !== undefined;
630
+ const dropSequence = hasEventSequence
631
+ ? database.prepare("DELETE FROM event_sequence WHERE aggregate_id = ?")
632
+ : null;
615
633
  const remove = database.prepare("DELETE FROM session WHERE id = ?");
616
634
  let deleted = 0;
617
- for (const id of ids)
635
+ for (const id of ids) {
636
+ dropSequence?.run(id);
618
637
  deleted += Number(remove.run(id).changes ?? 0);
638
+ }
619
639
  this.invalidate("opencode");
620
640
  return deleted;
621
641
  }
@@ -647,4 +667,90 @@ export class ProviderHistoryScanner {
647
667
  }
648
668
  return deleted;
649
669
  }
670
+ deletePiHistoryFiles(sessionIds) {
671
+ const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
672
+ if (ids.size === 0 || !existsSync(this.piSessionsDir))
673
+ return 0;
674
+ let deleted = 0;
675
+ try {
676
+ const dirs = readdirSync(this.piSessionsDir, { withFileTypes: true });
677
+ for (const dir of dirs) {
678
+ if (!dir.isDirectory())
679
+ continue;
680
+ const projectDir = path.join(this.piSessionsDir, dir.name);
681
+ try {
682
+ const files = readdirSync(projectDir, { withFileTypes: true });
683
+ for (const file of files) {
684
+ if (!file.isFile() || !file.name.endsWith(".jsonl"))
685
+ continue;
686
+ const matched = Array.from(ids).some((id) => file.name.endsWith(`_${id}.jsonl`) || file.name === `${id}.jsonl`);
687
+ if (matched) {
688
+ try {
689
+ unlinkSync(path.join(projectDir, file.name));
690
+ deleted += 1;
691
+ }
692
+ catch { /* already absent */ }
693
+ }
694
+ }
695
+ }
696
+ catch { /* ignore per-project read error */ }
697
+ }
698
+ }
699
+ catch {
700
+ return 0;
701
+ }
702
+ return deleted;
703
+ }
704
+ deleteGrokHistoryFiles(sessionIds) {
705
+ const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
706
+ if (ids.size === 0 || !existsSync(this.grokSessionsDir))
707
+ return 0;
708
+ let deleted = 0;
709
+ try {
710
+ const dirs = readdirSync(this.grokSessionsDir, { withFileTypes: true });
711
+ for (const dir of dirs) {
712
+ if (!dir.isDirectory())
713
+ continue;
714
+ const projectDir = path.join(this.grokSessionsDir, dir.name);
715
+ for (const id of ids) {
716
+ const targetDir = path.join(projectDir, id);
717
+ if (existsSync(targetDir)) {
718
+ try {
719
+ rmSync(targetDir, { recursive: true, force: true });
720
+ deleted += 1;
721
+ }
722
+ catch { /* best effort */ }
723
+ }
724
+ }
725
+ }
726
+ }
727
+ catch {
728
+ return 0;
729
+ }
730
+ return deleted;
731
+ }
732
+ deleteGeminiHistoryFiles(sessionIds) {
733
+ const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
734
+ if (ids.size === 0 || !existsSync(this.geminiHome))
735
+ return 0;
736
+ let deleted = 0;
737
+ for (const id of ids) {
738
+ const dbPath = path.join(this.geminiHome, "antigravity-cli", "conversations", `${id}.db`);
739
+ if (existsSync(dbPath)) {
740
+ try {
741
+ unlinkSync(dbPath);
742
+ deleted += 1;
743
+ }
744
+ catch { /* already absent */ }
745
+ }
746
+ const lockPath = path.join(this.geminiHome, "antigravity-cli", "presence", `${id}.lock`);
747
+ if (existsSync(lockPath)) {
748
+ try {
749
+ unlinkSync(lockPath);
750
+ }
751
+ catch { /* best effort */ }
752
+ }
753
+ }
754
+ return deleted;
755
+ }
650
756
  }
@@ -1,29 +1,41 @@
1
1
  import type { SessionRegistry } from "./session-registry.js";
2
2
  import type { WandStorage } from "./storage.js";
3
+ import { type TaskRetentionSettings } from "./task-retention.js";
4
+ import type { SessionSnapshot } from "./types.js";
3
5
  /**
4
- * 统一保留期策略(会话与看板任务同一套):
5
- * 1. 超过 IDLE 没有活动的会话 / 任务 → 自动归档;
6
- * 2. 归档后经过 ARCHIVED 仍没被恢复 → 自动清理(删除)。
7
- *
8
- * 会话的自动归档此前散在两个 runner 里(24h),这里收敛成同一份常量,避免
9
- * 「会话 24h、任务 7 天」两套口径。运行中的会话/任务永不归档或清理。
6
+ * 看板任务、会话和已结束的团队运行共用 `taskRetention`。
7
+ * 没配过时默认空闲 7 天归档,再过 7 天清理。
8
+ * 结构化会话看运行状态;PTY 只有正在回复或等待确认才跳过,空闲壳不算在处理。
10
9
  */
11
10
  export declare const RETENTION_IDLE_MS: number;
12
11
  export declare const RETENTION_ARCHIVED_MS: number;
13
12
  export interface RetentionDeps {
14
13
  storage: WandStorage;
15
14
  sessions: SessionRegistry;
15
+ /** 每次扫描读取。传入函数才能在设置保存后立刻用新窗口,而不是启动时的快照。 */
16
+ taskRetention?: TaskRetentionSettings | (() => TaskRetentionSettings | undefined);
16
17
  }
17
18
  export interface RetentionResult {
18
19
  archivedSessions: number;
19
20
  purgedSessions: number;
20
21
  archivedTasks: number;
21
22
  purgedTasks: number;
23
+ purgedTeamRuns: number;
22
24
  }
25
+ /**
26
+ * 保留期里的「还在处理」:结构化会话看运行状态;PTY 的 `running` 只表示壳进程还在,
27
+ * 空闲 zsh 不算。真正挡住清理的是正在回复、或停在权限/确认上。
28
+ */
29
+ export declare function isRetentionBusy(session: SessionSnapshot): boolean;
23
30
  /**
24
31
  * 跑一遍保留期清理。返回本轮的实际动作计数,便于日志与测试断言。
25
32
  * 纯函数式编排:只依赖 storage / registry,方便在单测里注入固定时间。
26
33
  */
27
34
  export declare function runRetentionSweep(deps: RetentionDeps, now?: number): RetentionResult;
35
+ /**
36
+ * 可重复调用的扫描。同一时刻只跑一轮;进行中又被请求时,本轮结束后再跑一轮,
37
+ * 这样保存设置时不会和定时扫描互相覆盖。
38
+ */
39
+ export declare function createRetentionSweep(deps: RetentionDeps): () => RetentionResult;
28
40
  /** 每小时的保留期扫描;返回清理定时器,调用方负责 clearInterval。 */
29
- export declare function startRetentionTimer(deps: RetentionDeps, intervalMs?: number): NodeJS.Timeout;
41
+ export declare function startRetentionTimer(deps: RetentionDeps, intervalMs?: number, sweep?: () => RetentionResult): NodeJS.Timeout;
package/dist/retention.js CHANGED
@@ -1,14 +1,29 @@
1
1
  import { cleanupWorktreeSync } from "./git-worktree.js";
2
+ import { taskRetentionWindows } from "./task-retention.js";
2
3
  /**
3
- * 统一保留期策略(会话与看板任务同一套):
4
- * 1. 超过 IDLE 没有活动的会话 / 任务 → 自动归档;
5
- * 2. 归档后经过 ARCHIVED 仍没被恢复 → 自动清理(删除)。
6
- *
7
- * 会话的自动归档此前散在两个 runner 里(24h),这里收敛成同一份常量,避免
8
- * 「会话 24h、任务 7 天」两套口径。运行中的会话/任务永不归档或清理。
4
+ * 看板任务、会话和已结束的团队运行共用 `taskRetention`。
5
+ * 没配过时默认空闲 7 天归档,再过 7 天清理。
6
+ * 结构化会话看运行状态;PTY 只有正在回复或等待确认才跳过,空闲壳不算在处理。
9
7
  */
10
8
  export const RETENTION_IDLE_MS = 7 * 24 * 60 * 60 * 1000;
11
9
  export const RETENTION_ARCHIVED_MS = 7 * 24 * 60 * 60 * 1000;
10
+ /**
11
+ * 保留期里的「还在处理」:结构化会话看运行状态;PTY 的 `running` 只表示壳进程还在,
12
+ * 空闲 zsh 不算。真正挡住清理的是正在回复、或停在权限/确认上。
13
+ */
14
+ export function isRetentionBusy(session) {
15
+ if (session.ptyBusy === true || session.structuredState?.inFlight === true)
16
+ return true;
17
+ if (session.permissionBlocked || session.pendingEscalation)
18
+ return true;
19
+ if ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty")
20
+ return false;
21
+ return session.status === "running";
22
+ }
23
+ function isIdleLivePty(session) {
24
+ return session.status === "running" && !isRetentionBusy(session)
25
+ && ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty");
26
+ }
12
27
  function timestamp(value) {
13
28
  if (!value)
14
29
  return null;
@@ -40,23 +55,28 @@ function taskLastActivity(storage, task, bound) {
40
55
  * 纯函数式编排:只依赖 storage / registry,方便在单测里注入固定时间。
41
56
  */
42
57
  export function runRetentionSweep(deps, now = Date.now()) {
43
- const result = { archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0 };
58
+ const result = {
59
+ archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0, purgedTeamRuns: 0,
60
+ };
61
+ const taskWindows = taskRetentionWindows(typeof deps.taskRetention === "function" ? deps.taskRetention() : deps.taskRetention);
44
62
  const snapshots = deps.sessions.listSlim();
45
63
  for (const session of snapshots) {
46
- // 运行中的会话既不属于「没动」,也不能被杀掉。
47
- if (session.status === "running")
64
+ // 真正还在处理的会话不能归档或杀掉。空闲 PTY 壳不算。
65
+ if (isRetentionBusy(session))
48
66
  continue;
49
67
  if (session.archived) {
68
+ if (taskWindows.archivedMs === null)
69
+ continue;
50
70
  const archivedAt = timestamp(session.archivedAt);
51
71
  if (archivedAt === null) {
52
- // 历史归档行没有时间戳:先补上,让 7 天窗口从这一刻起算,而不是立刻删除。
72
+ // 历史归档行没有时间戳:先补上,让删除窗口从这一刻起算,而不是立刻删除。
53
73
  try {
54
74
  deps.sessions.setArchived(session.id, true);
55
75
  }
56
76
  catch { /* already gone */ }
57
77
  continue;
58
78
  }
59
- if (now - archivedAt < RETENTION_ARCHIVED_MS)
79
+ if (now - archivedAt < taskWindows.archivedMs)
60
80
  continue;
61
81
  try {
62
82
  deps.sessions.delete(session.id);
@@ -65,9 +85,17 @@ export function runRetentionSweep(deps, now = Date.now()) {
65
85
  catch { /* best-effort: a session may disappear between the read and the purge */ }
66
86
  continue;
67
87
  }
88
+ if (taskWindows.idleMs === null)
89
+ continue;
68
90
  const reference = timestamp(session.endedAt ?? session.startedAt);
69
- if (reference === null || now - reference < RETENTION_IDLE_MS)
91
+ if (reference === null || now - reference < taskWindows.idleMs)
70
92
  continue;
93
+ if (isIdleLivePty(session)) {
94
+ try {
95
+ deps.sessions.stop(session.id);
96
+ }
97
+ catch { /* shell may already be gone */ }
98
+ }
71
99
  try {
72
100
  deps.sessions.setArchived(session.id, true);
73
101
  result.archivedSessions += 1;
@@ -87,9 +115,9 @@ export function runRetentionSweep(deps, now = Date.now()) {
87
115
  }
88
116
  for (const task of deps.storage.listWandTasks()) {
89
117
  const bound = task.workspaceTaskId ? sessionsByTask.get(task.workspaceTaskId) ?? [] : [];
90
- const running = bound.some((session) => session.status === "running");
118
+ const running = bound.some((session) => isRetentionBusy(session));
91
119
  if (task.status === "archived") {
92
- if (running)
120
+ if (running || taskWindows.archivedMs === null)
93
121
  continue;
94
122
  const archivedAt = timestamp(task.archivedAt);
95
123
  if (archivedAt === null) {
@@ -97,7 +125,7 @@ export function runRetentionSweep(deps, now = Date.now()) {
97
125
  deps.storage.updateWandTask(task.id, { status: "archived" });
98
126
  continue;
99
127
  }
100
- if (now - archivedAt < RETENTION_ARCHIVED_MS)
128
+ if (now - archivedAt < taskWindows.archivedMs)
101
129
  continue;
102
130
  const container = task.workspaceTaskId ? deps.storage.getWorkspaceTask(task.workspaceTaskId) : null;
103
131
  if (container?.worktree) {
@@ -110,33 +138,148 @@ export function runRetentionSweep(deps, now = Date.now()) {
110
138
  result.purgedTasks += 1;
111
139
  continue;
112
140
  }
113
- if (running)
141
+ if (running || taskWindows.idleMs === null)
114
142
  continue;
115
143
  const lastActivity = taskLastActivity(deps.storage, task, bound);
116
- if (lastActivity === null || now - lastActivity < RETENTION_IDLE_MS)
144
+ if (lastActivity === null || now - lastActivity < taskWindows.idleMs)
117
145
  continue;
118
146
  deps.storage.updateWandTask(task.id, { status: "archived" });
119
147
  result.archivedTasks += 1;
120
148
  }
149
+ sweepTeamRuns(deps, taskWindows, snapshots, now, result);
121
150
  return result;
122
151
  }
123
- /** 每小时的保留期扫描;返回清理定时器,调用方负责 clearInterval。 */
124
- export function startRetentionTimer(deps, intervalMs = 60 * 60 * 1000) {
125
- const sweep = () => {
126
- try {
127
- const result = runRetentionSweep(deps);
128
- const acted = result.archivedSessions + result.purgedSessions + result.archivedTasks + result.purgedTasks;
129
- if (acted > 0) {
130
- console.log(`[retention] archived sessions=${result.archivedSessions} tasks=${result.archivedTasks}; `
131
- + `purged sessions=${result.purgedSessions} tasks=${result.purgedTasks}`);
152
+ function sweepTeamRuns(deps, windows, snapshots, now, result) {
153
+ const sessionById = new Map(snapshots.map((session) => [session.id, session]));
154
+ for (const run of deps.storage.listAiTeamRuns()) {
155
+ if (run.status === "running")
156
+ continue;
157
+ const linked = teamRunSessionIds(deps.storage, run.id, run.chatSessionId);
158
+ if (linked.some((id) => {
159
+ const session = sessionById.get(id);
160
+ return session ? isRetentionBusy(session) : false;
161
+ }))
162
+ continue;
163
+ const activity = teamRunActivity(run.updatedAt, linked, sessionById);
164
+ if (activity === null)
165
+ continue;
166
+ if (windows.archivedMs !== null && now - activity >= windows.archivedMs) {
167
+ for (const id of linked) {
168
+ const session = sessionById.get(id);
169
+ if (!session || isRetentionBusy(session))
170
+ continue;
171
+ if (isIdleLivePty(session)) {
172
+ try {
173
+ deps.sessions.stop(id);
174
+ }
175
+ catch { /* already gone */ }
176
+ }
177
+ try {
178
+ deps.sessions.delete(id);
179
+ result.purgedSessions += 1;
180
+ sessionById.delete(id);
181
+ }
182
+ catch { /* already gone */ }
132
183
  }
184
+ deps.storage.deleteAiTeamRun(run.id);
185
+ result.purgedTeamRuns += 1;
186
+ continue;
187
+ }
188
+ if (windows.idleMs === null || now - activity < windows.idleMs)
189
+ continue;
190
+ for (const id of linked) {
191
+ const session = sessionById.get(id);
192
+ if (!session || session.archived || isRetentionBusy(session))
193
+ continue;
194
+ if (isIdleLivePty(session)) {
195
+ try {
196
+ deps.sessions.stop(id);
197
+ }
198
+ catch { /* already gone */ }
199
+ }
200
+ try {
201
+ deps.sessions.setArchived(id, true);
202
+ session.archived = true;
203
+ result.archivedSessions += 1;
204
+ }
205
+ catch { /* best-effort */ }
206
+ }
207
+ }
208
+ }
209
+ function teamRunSessionIds(storage, runId, chatSessionId) {
210
+ const ids = new Set();
211
+ if (chatSessionId)
212
+ ids.add(chatSessionId);
213
+ for (const step of storage.listAiTeamSteps(runId)) {
214
+ if (step.sessionId)
215
+ ids.add(step.sessionId);
216
+ }
217
+ return [...ids];
218
+ }
219
+ function teamRunActivity(updatedAt, sessionIds, sessionById) {
220
+ const times = [];
221
+ const updated = timestamp(updatedAt);
222
+ if (updated !== null)
223
+ times.push(updated);
224
+ for (const id of sessionIds) {
225
+ const session = sessionById.get(id);
226
+ const reference = timestamp(session?.endedAt ?? session?.startedAt);
227
+ if (reference !== null)
228
+ times.push(reference);
229
+ }
230
+ return times.length > 0 ? Math.max(...times) : null;
231
+ }
232
+ function logRetentionSweep(result) {
233
+ const acted = result.archivedSessions + result.purgedSessions + result.archivedTasks + result.purgedTasks + result.purgedTeamRuns;
234
+ if (acted > 0) {
235
+ console.log(`[retention] archived sessions=${result.archivedSessions} tasks=${result.archivedTasks}; `
236
+ + `purged sessions=${result.purgedSessions} tasks=${result.purgedTasks} teamRuns=${result.purgedTeamRuns}`);
237
+ }
238
+ }
239
+ /**
240
+ * 可重复调用的扫描。同一时刻只跑一轮;进行中又被请求时,本轮结束后再跑一轮,
241
+ * 这样保存设置时不会和定时扫描互相覆盖。
242
+ */
243
+ export function createRetentionSweep(deps) {
244
+ const empty = {
245
+ archivedSessions: 0, purgedSessions: 0, archivedTasks: 0, purgedTasks: 0, purgedTeamRuns: 0,
246
+ };
247
+ let running = false;
248
+ let again = false;
249
+ let last = empty;
250
+ return () => {
251
+ if (running) {
252
+ again = true;
253
+ return last;
254
+ }
255
+ running = true;
256
+ try {
257
+ do {
258
+ again = false;
259
+ last = runRetentionSweep(deps);
260
+ logRetentionSweep(last);
261
+ } while (again);
262
+ return last;
133
263
  }
134
264
  catch (error) {
135
265
  console.error(`[retention] sweep failed: ${error instanceof Error ? error.message : String(error)}`);
266
+ throw error;
267
+ }
268
+ finally {
269
+ running = false;
270
+ }
271
+ };
272
+ }
273
+ /** 每小时的保留期扫描;返回清理定时器,调用方负责 clearInterval。 */
274
+ export function startRetentionTimer(deps, intervalMs = 60 * 60 * 1000, sweep = createRetentionSweep(deps)) {
275
+ const run = () => {
276
+ try {
277
+ sweep();
136
278
  }
279
+ catch { /* createRetentionSweep 已记录 */ }
137
280
  };
138
- sweep();
139
- const timer = setInterval(sweep, intervalMs);
281
+ run();
282
+ const timer = setInterval(run, intervalMs);
140
283
  timer.unref?.();
141
284
  return timer;
142
285
  }
@@ -290,10 +290,14 @@ export function registerAiTeamRoutes(app, deps) {
290
290
  sendTeamError(res, error);
291
291
  }
292
292
  });
293
- /** 运行中步骤的 live 文本(§4.9):Web 走 ai-team-step-live 推送,移动端轻量轮询这个端点。 */
293
+ /**
294
+ * 运行中步骤的 live 文本(§4.9.1):Web 走 ai-team-step-live 推送,移动端轻量轮询这个端点。
295
+ * 除步骤快照外还回 run 级活动时间(startedAt / lastActivityAt / observedAt),轮询端不必
296
+ * 再拉一次 detail 才知道「还在跑、只是这一轮安静」。
297
+ */
294
298
  app.get("/api/ai-team-runs/:id/live", (req, res) => {
295
299
  try {
296
- res.json({ runId: req.params.id, steps: runner.live(req.params.id) });
300
+ res.json(runner.liveSnapshot(req.params.id));
297
301
  }
298
302
  catch (error) {
299
303
  sendTeamError(res, error);
@@ -0,0 +1,5 @@
1
+ import type { Express } from "express";
2
+ import type { ConversationService } from "./conversation-service.js";
3
+ export declare function registerConversationRoutes(app: Express, service: ConversationService): void;
4
+ /** Compatibility relay inputs are ordinary talk; never infer the newest task or approve a short phrase. */
5
+ export declare function forwardConversationRelay(service: ConversationService, sessionId: string, input: string): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AiTeamConflictError } from "./ai-team-runner.js";
3
+ import { asyncRoute } from "./express-async.js";
4
+ import { bodyObject, sendRouteError, text } from "./server-request.js";
5
+ function targetOf(value) {
6
+ if (value === null || value === undefined)
7
+ return null;
8
+ const body = bodyObject(value);
9
+ if (!text(body.taskId) || !text(body.runId))
10
+ throw new Error("发送目标需要明确 taskId 和 runId。");
11
+ return { taskId: text(body.taskId), runId: text(body.runId) };
12
+ }
13
+ function result(res, receipt) {
14
+ if (receipt.state === "rejected")
15
+ res.status(400).json({ error: receipt.error ?? "请求未接受。", receipt });
16
+ else
17
+ res.status(202).json(receipt);
18
+ }
19
+ export function registerConversationRoutes(app, service) {
20
+ app.get("/api/conversations", (req, res) => res.json({ conversations: service.list() }));
21
+ app.patch("/api/conversations/:id/list-state", (req, res) => {
22
+ try {
23
+ const body = bodyObject(req.body);
24
+ if ((body.pinned !== undefined && typeof body.pinned !== "boolean") || (body.dissolved !== undefined && typeof body.dissolved !== "boolean")
25
+ || (body.pinned === undefined && body.dissolved === undefined))
26
+ throw new Error("请提供有效的置顶或列表显示状态。");
27
+ res.json(service.updateListState(req.params.id, { pinned: body.pinned, dissolved: body.dissolved }));
28
+ }
29
+ catch (error) {
30
+ sendRouteError(res, error, "对话列表更新失败。", 400);
31
+ }
32
+ });
33
+ app.delete("/api/conversations/:id", asyncRoute(async (req, res) => {
34
+ try {
35
+ await service.remove(req.params.id);
36
+ res.json({ ok: true });
37
+ }
38
+ catch (error) {
39
+ sendRouteError(res, error, "删除对话失败。", error instanceof AiTeamConflictError ? 409 : 400);
40
+ }
41
+ }));
42
+ app.get("/api/conversations/requests/:requestId", (req, res) => {
43
+ const receipt = service.receipt(req.params.requestId);
44
+ if (!receipt)
45
+ res.status(404).json({ error: "未找到回执;不能据此断言请求未接受。" });
46
+ else
47
+ res.json(receipt);
48
+ });
49
+ app.get("/api/conversations/:id", (req, res) => {
50
+ try {
51
+ res.json(service.detail(req.params.id));
52
+ }
53
+ catch (error) {
54
+ sendRouteError(res, error, "对话读取失败。", 404);
55
+ }
56
+ });
57
+ const post = (path, operation) => {
58
+ app.post(path, asyncRoute(async (req, res) => {
59
+ try {
60
+ result(res, await operation(req.params.id ?? "", bodyObject(req.body)));
61
+ }
62
+ catch (error) {
63
+ sendRouteError(res, error, "对话操作失败。", error instanceof AiTeamConflictError ? 409 : 400);
64
+ }
65
+ }));
66
+ };
67
+ post("/api/conversations", (_id, body) => service.createGroup(text(body.requestId), bodyObject(body.group)));
68
+ post("/api/conversations/:id/invitations", (id, body) => service.invite(id, text(body.requestId), Number(body.memberVersion), bodyObject(body.group)));
69
+ post("/api/conversations/:id/channel", (id, body) => service.prepare(id, text(body.requestId), text(body.cwd) || undefined));
70
+ post("/api/conversations/:id/messages", (id, body) => service.send(id, text(body.requestId), text(body.input), targetOf(body.target), text(body.cwd) || undefined));
71
+ post("/api/conversations/:id/tasks", (id, body) => service.dispatch(id, text(body.requestId), {
72
+ title: text(body.title), description: text(body.description), workspaceId: text(body.workspaceId), cwd: text(body.cwd) || undefined,
73
+ memberVersion: body.memberVersion === undefined ? undefined : Number(body.memberVersion),
74
+ continueTaskId: text(body.continueTaskId) || undefined,
75
+ }));
76
+ post("/api/conversations/:id/actions", (id, body) => {
77
+ const target = targetOf(body.target);
78
+ if (!target)
79
+ throw new Error("运行操作需要明确任务与轮次。");
80
+ return service.act(id, text(body.requestId), target, text(body.action), body.extraSteps === undefined ? undefined : Number(body.extraSteps));
81
+ });
82
+ }
83
+ /** Compatibility relay inputs are ordinary talk; never infer the newest task or approve a short phrase. */
84
+ export async function forwardConversationRelay(service, sessionId, input) {
85
+ const group = service.list().find((item) => item.sessionId === sessionId);
86
+ if (!group)
87
+ throw new Error("群实例不存在,请从对话列表核对。");
88
+ const receipt = await service.send(group.id, randomUUID(), input, null, undefined, false);
89
+ if (receipt.error)
90
+ throw new Error(receipt.error);
91
+ }
@@ -19,4 +19,6 @@ export declare function registerSiliconEmployeeRoutes(app: Express, deps: {
19
19
  config?: WandConfig;
20
20
  /** 测试注入点:默认按系统 AI 配置真实调用模型。 */
21
21
  generateDraft?: (request: EmployeeDraftRequest) => Promise<SiliconEmployeeDraft>;
22
+ /** 测试注入点:CLI 可用性探测。 */
23
+ isProviderAvailable?: (agent: WandTaskAgent) => boolean;
22
24
  }): void;