@co0ontty/wand 4.97.0 → 4.98.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 (160) hide show
  1. package/README.md +0 -4
  2. package/dist/agent-dispatch.d.ts +1 -0
  3. package/dist/agent-dispatch.js +13 -2
  4. package/dist/ai-team-types.d.ts +1 -1
  5. package/dist/ai-team-types.js +1 -1
  6. package/dist/build-info.json +3 -3
  7. package/dist/cli.js +2 -0
  8. package/dist/config.d.ts +1 -1
  9. package/dist/config.js +2 -2
  10. package/dist/core-runner.d.ts +0 -4
  11. package/dist/core-runner.js +41 -3
  12. package/dist/decision-expert-employee.js +1 -0
  13. package/dist/distribution-manager.d.ts +29 -4
  14. package/dist/distribution-manager.js +147 -57
  15. package/dist/employee-types.d.ts +2 -1
  16. package/dist/employee-types.js +2 -4
  17. package/dist/feedback-feature-map.d.ts +0 -1
  18. package/dist/feedback-feature-map.js +0 -1
  19. package/dist/feedback.d.ts +0 -1
  20. package/dist/feedback.js +1 -1
  21. package/dist/git-quick-commit.js +13 -52
  22. package/dist/git-tag-generation.d.ts +11 -0
  23. package/dist/git-tag-generation.js +47 -0
  24. package/dist/hr-employee.js +1 -0
  25. package/dist/local-model-setup.d.ts +1 -9
  26. package/dist/local-model-setup.js +52 -166
  27. package/dist/local-model-types.d.ts +2 -5
  28. package/dist/managed-temp.d.ts +1 -1
  29. package/dist/managed-temp.js +1 -1
  30. package/dist/middleware/api-scope.js +2 -4
  31. package/dist/migration-backup-retention.d.ts +17 -0
  32. package/dist/migration-backup-retention.js +91 -0
  33. package/dist/pi-tmux-host.d.ts +28 -0
  34. package/dist/pi-tmux-host.js +189 -0
  35. package/dist/server-employee-routes.d.ts +1 -0
  36. package/dist/server-employee-routes.js +17 -4
  37. package/dist/server-local-model-routes.js +2 -15
  38. package/dist/server-session-input-routes.js +3 -3
  39. package/dist/server-session-lifecycle-routes.js +94 -18
  40. package/dist/server-task-routes.js +18 -13
  41. package/dist/server-update-routes.d.ts +11 -1
  42. package/dist/server-update-routes.js +25 -12
  43. package/dist/server-workspace-routes.js +11 -5
  44. package/dist/server.js +29 -129
  45. package/dist/storage/conversations-repository.d.ts +2 -0
  46. package/dist/storage/conversations-repository.js +4 -0
  47. package/dist/storage/employees-repository.d.ts +2 -2
  48. package/dist/storage/employees-repository.js +12 -15
  49. package/dist/storage/internal.js +2 -2
  50. package/dist/storage/migrations.js +20 -0
  51. package/dist/storage/readonly.js +3 -0
  52. package/dist/storage/sessions-repository.d.ts +2 -0
  53. package/dist/storage/sessions-repository.js +13 -0
  54. package/dist/storage/tasks-repository.d.ts +22 -0
  55. package/dist/storage/tasks-repository.js +52 -3
  56. package/dist/storage/workspaces-repository.d.ts +1 -0
  57. package/dist/storage/workspaces-repository.js +3 -1
  58. package/dist/storage-cleanup.d.ts +37 -2
  59. package/dist/storage-cleanup.js +62 -2
  60. package/dist/storage-schema.js +9 -0
  61. package/dist/structured-claude-adapter.js +5 -1
  62. package/dist/structured-claude-protocol.d.ts +1 -1
  63. package/dist/structured-claude-protocol.js +12 -1
  64. package/dist/structured-runner.d.ts +2 -0
  65. package/dist/structured-session-manager.d.ts +14 -2
  66. package/dist/structured-session-manager.js +87 -14
  67. package/dist/system-ai-call.d.ts +5 -1
  68. package/dist/system-ai-call.js +32 -1
  69. package/dist/system-ai-session-recorder.d.ts +2 -0
  70. package/dist/system-ai-session-recorder.js +35 -3
  71. package/dist/system-cli.js +6 -1
  72. package/dist/system-employee.js +1 -0
  73. package/dist/task-comment-dispatch.d.ts +18 -0
  74. package/dist/task-comment-dispatch.js +121 -0
  75. package/dist/task-types.d.ts +14 -0
  76. package/dist/version-utils.d.ts +31 -0
  77. package/dist/version-utils.js +35 -0
  78. package/dist/web-ui/content/ai-teams.js +10 -10
  79. package/dist/web-ui/content/module-manifest.json +1 -1
  80. package/dist/web-ui/content/modules/02b1ec89c69dc0dd/host.js +30 -0
  81. package/dist/web-ui/content/modules/{e8f0cda6b9ac8a70 → 0434a45abd2c1d15}/chunk.js +1 -1
  82. package/dist/web-ui/content/modules/{e9a01a25ec573294 → 0afa04591224bc23}/library-bridge.js +1 -1
  83. package/dist/web-ui/content/modules/{b33c567946044d83 → 0bf4a78c8be1bdc9}/host.js +1 -1
  84. package/dist/web-ui/content/modules/17f72e0fb014a302/chat-render.js +75 -0
  85. package/dist/web-ui/content/modules/{5903104a2e0e3888 → 183db4802a7d99ed}/host.js +1 -1
  86. package/dist/web-ui/content/modules/23d2006fcfcf3c0c/host-bridge.js +1 -0
  87. package/dist/web-ui/content/modules/{0c96341a022aa360 → 32f151da1a006041}/host.js +1 -1
  88. package/dist/web-ui/content/modules/{3172fe0b46d51f8d → 34a7674efc329383}/chunk.js +1 -1
  89. package/dist/web-ui/content/modules/3541ba4cb2886e29/chunk.js +1 -0
  90. package/dist/web-ui/content/modules/{801e7f686ce7eb28 → 395257bd20cd0098}/chunk.js +1 -1
  91. package/dist/web-ui/content/modules/{3ddcbd8505bfcdf4 → 3fd75af0a6891549}/chunk.js +1 -1
  92. package/dist/web-ui/content/modules/{c41d8ffa27816766 → 428352e422932c40}/host.js +1 -1
  93. package/dist/web-ui/content/modules/485a72b2c83e7680/workspace-window.js +1 -0
  94. package/dist/web-ui/content/modules/489dfe1f7441ef2c/markdown.js +1 -0
  95. package/dist/web-ui/content/modules/4b42b4336458b7b5/runtime.js +550 -0
  96. package/dist/web-ui/content/modules/{de00ab5329b5e05a → 528c5d97e8f9d582}/chunk.js +1 -1
  97. package/dist/web-ui/content/modules/{3086df78444998b3 → 535aad05d34079d2}/host.js +1 -1
  98. package/dist/web-ui/content/modules/62028b9bbf8ffc88/page.js +6 -0
  99. package/dist/web-ui/content/modules/6868f75a5981ad08/host.js +69 -0
  100. package/dist/web-ui/content/modules/6c72c8c166e34de7/host.js +1 -0
  101. package/dist/web-ui/content/modules/6ff6ce370ef26753/host.js +1 -0
  102. package/dist/web-ui/content/modules/{87379801875d1921 → 72bef6889fdafefe}/host.js +1 -1
  103. package/dist/web-ui/content/modules/{5e5eabd9a9366a56 → 763c79f5527d762a}/panel.js +1 -1
  104. package/dist/web-ui/content/modules/{d30ac9af6dc7d555 → 77ea205020273767}/chunk.js +1 -1
  105. package/dist/web-ui/content/modules/{e01e1e25b7fe37e9 → 7fb02c44fd6f877b}/host.js +1 -1
  106. package/dist/web-ui/content/modules/{349ee129eef5a182 → 818d87c1651fc6a1}/chunk.js +1 -1
  107. package/dist/web-ui/content/modules/{66f36cd2c38cfb07 → 8cb5d84a2731789e}/chunk.js +1 -1
  108. package/dist/web-ui/content/modules/{f5b3fb6fedd9b05d → 96e78c7f2cd9acd5}/chunk.js +1 -1
  109. package/dist/web-ui/content/modules/{64c1b0217f5377aa → a60a07ba1abcdc3f}/chunk.js +1 -1
  110. package/dist/web-ui/content/modules/{ee12d3c4421a5c12 → b4bcb9bf5cfb0b3a}/host.js +1 -1
  111. package/dist/web-ui/content/modules/b71cda413318d71f/employee-card.js +1 -0
  112. package/dist/web-ui/content/modules/bb0c299cf006e157/task-board-host.js +17 -0
  113. package/dist/web-ui/content/modules/{42ae495da5903b07 → c8c6adee81d4bd98}/sha256.js +1 -1
  114. package/dist/web-ui/content/modules/{854b0f6dc887923b → cd0cec0ba1887ffd}/chunk.js +1 -1
  115. package/dist/web-ui/content/modules/{3338b55c7767bacb → d673a76becc7d1f5}/chunk.js +1 -1
  116. package/dist/web-ui/content/modules/{2bce1137f402bd70 → d99ea7e921488348}/chunk.js +1 -1
  117. package/dist/web-ui/content/modules/{a9f15bd03bc590e7 → e862ccabc908eb0c}/host.js +1 -1
  118. package/dist/web-ui/content/modules/{9984f6bb05ac55bb → eb46bf62b1ae1bea}/host.js +1 -1
  119. package/dist/web-ui/content/plush-avatar.js +166 -166
  120. package/dist/web-ui/content/styles.css +1 -1
  121. package/dist/web-ui/embedded-assets.d.ts +177 -177
  122. package/dist/web-ui/embedded-assets.js +6 -6
  123. package/dist/web-ui/page-route.d.ts +24 -0
  124. package/dist/web-ui/page-route.js +130 -0
  125. package/dist/web-ui/pty-input-state.d.ts +2 -0
  126. package/dist/web-ui/pty-input-state.js +2 -2
  127. package/dist/web-ui/react/workspaces/types.d.ts +5 -0
  128. package/package.json +1 -3
  129. package/dist/server-speech-routes.d.ts +0 -10
  130. package/dist/server-speech-routes.js +0 -93
  131. package/dist/speech-models.d.ts +0 -24
  132. package/dist/speech-models.js +0 -16
  133. package/dist/speech-polisher-employee.d.ts +0 -6
  134. package/dist/speech-polisher-employee.js +0 -23
  135. package/dist/speech-polisher-identity.d.ts +0 -4
  136. package/dist/speech-polisher-identity.js +0 -4
  137. package/dist/speech-polisher-service.d.ts +0 -33
  138. package/dist/speech-polisher-service.js +0 -110
  139. package/dist/speech-service.d.ts +0 -59
  140. package/dist/speech-service.js +0 -420
  141. package/dist/speech-support.d.ts +0 -6
  142. package/dist/speech-support.js +0 -24
  143. package/dist/speech-types.d.ts +0 -52
  144. package/dist/speech-types.js +0 -3
  145. package/dist/web-ui/content/modules/15e32e85e086f6b4/page.js +0 -6
  146. package/dist/web-ui/content/modules/1e43259bddbc6805/markdown.js +0 -1
  147. package/dist/web-ui/content/modules/271d488d262909c2/host.js +0 -30
  148. package/dist/web-ui/content/modules/43746e7237d61da9/task-board-host.js +0 -17
  149. package/dist/web-ui/content/modules/4485e6c1f0459aa8/chunk.js +0 -1
  150. package/dist/web-ui/content/modules/5b541e0f296a3592/employee-card.js +0 -1
  151. package/dist/web-ui/content/modules/6ed52e6604661678/chat-render.js +0 -75
  152. package/dist/web-ui/content/modules/89888da0972e5033/host.js +0 -50
  153. package/dist/web-ui/content/modules/c11165f843d818f4/host-bridge.js +0 -1
  154. package/dist/web-ui/content/modules/c1829693f91fd25c/host.js +0 -1
  155. package/dist/web-ui/content/modules/cdb882ddab12e325/host.js +0 -1
  156. package/dist/web-ui/content/modules/ea67fd4e96c9f45a/workspace-window.js +0 -1
  157. package/dist/web-ui/content/modules/fd767282c29173b5/runtime.js +0 -539
  158. package/dist/web-ui/speech-audio.d.ts +0 -2
  159. package/dist/web-ui/speech-audio.js +0 -41
  160. package/scripts/install-speech-runtime.js +0 -82
@@ -26,11 +26,21 @@ export interface PublicUpdateRoutesDependencies {
26
26
  resolveAndroidDownload(channel: "stable" | "beta"): Promise<DownloadAsset | null>;
27
27
  /** GitHub 来源由服务端代拉,避免手机直连 CDN。测试 stub 可不实现。 */
28
28
  resolveGitHubApkDownload?(): Promise<GitHubApkDownload | null>;
29
- /** Beta 包下发时清理分发目录,只保留最新一个 APK(可选,测试 stub 可不实现)。 */
29
+ /** Beta 包下发时清理分发目录,只保留最新一套产物(可选,测试 stub 可不实现)。 */
30
30
  pruneAndroidApkDirectory?(keepFileName?: string): Promise<{
31
31
  deleted: string[];
32
32
  freedBytes: number;
33
33
  }>;
34
+ /** macOS Beta 包下发时清理分发目录,保留最新一整套 zip/dmg/update.json。 */
35
+ pruneMacosBetaDirectory?(keepFileName?: string): Promise<{
36
+ deleted: string[];
37
+ freedBytes: number;
38
+ }>;
39
+ /** iOS IPA 下发时清理分发目录,保留最新一个包。 */
40
+ pruneIosIpaDirectory?(keepFileName?: string): Promise<{
41
+ deleted: string[];
42
+ freedBytes: number;
43
+ }>;
34
44
  /** 计算本地分发文件的 SHA-256(hex);失败返回 null,下载响应不带哈希头。 */
35
45
  computeAssetSha256(asset: DownloadAsset): Promise<string | null>;
36
46
  resolveLatestDmg(): Promise<ResolvedUpdateAsset | null>;
@@ -13,6 +13,24 @@ import { compareApkInstallOrder, compareSemver, compareWandInstallOrder } from "
13
13
  import { HttpError } from "./http-error.js";
14
14
  import { canUseDetachedUpdateHelper, readDetachedUpdateStatus, readLatestDetachedUpdate, startDetachedUpdateHelper } from "./update-helper.js";
15
15
  const GITHUB_PROXY_TIMEOUT_MS = 5 * 60 * 1000;
16
+ /**
17
+ * 分发目录清理的共用执行器:清理失败绝不能阻断本次下载或更新检查。
18
+ * 只在真正开始下发时调用,避免定时扫描去删一个没人要的文件。
19
+ */
20
+ async function pruneQuietly(prune, keepFileName, label) {
21
+ if (!prune)
22
+ return;
23
+ try {
24
+ const pruned = await prune(keepFileName);
25
+ if (pruned.deleted.length > 0) {
26
+ process.stdout.write(`[wand] 已清理旧版${label}:${pruned.deleted.join(", ")}`
27
+ + `(释放 ${(pruned.freedBytes / 1024 / 1024).toFixed(1)} MB)\n`);
28
+ }
29
+ }
30
+ catch (error) {
31
+ process.stdout.write(`[wand] 清理旧版${label}失败:${getErrorMessage(error)}\n`);
32
+ }
33
+ }
16
34
  async function proxyGitHubApk(_req, res, asset, fetchImpl = fetch) {
17
35
  let remote;
18
36
  try {
@@ -113,18 +131,8 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
113
131
  // Beta 包下发时顺手清理分发目录:debug 构建每次都带独立时间戳版本号,
114
132
  // 不清理会按构建次数无限堆积。只保留最新一个;正在下发的文件与
115
133
  // currentApkFile 固定指向的文件不会被删。清理失败不阻断本次下载。
116
- if (channel === "beta" && deps.pruneAndroidApkDirectory) {
117
- try {
118
- const pruned = await deps.pruneAndroidApkDirectory(asset.fileName);
119
- if (pruned.deleted.length > 0) {
120
- process.stdout.write(`[wand] 已清理旧版安装包:${pruned.deleted.join(", ")}`
121
- + `(释放 ${(pruned.freedBytes / 1024 / 1024).toFixed(1)} MB)\n`);
122
- }
123
- }
124
- catch (error) {
125
- process.stdout.write(`[wand] 清理旧版安装包失败:${getErrorMessage(error)}\n`);
126
- }
127
- }
134
+ if (channel === "beta")
135
+ await pruneQuietly(deps.pruneAndroidApkDirectory, asset.fileName, "安装包");
128
136
  // 响应头携带实际发送字节的 SHA-256:check 与 download 之间 APK 目录若被
129
137
  // 重新构建/覆盖,客户端以响应头为准,避免用过期 check 快照误报校验失败。
130
138
  const headers = {};
@@ -175,6 +183,9 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
175
183
  res.status(503).json({ error: "无法校验 macOS Beta 更新包。" });
176
184
  return;
177
185
  }
186
+ // 开始下发后再清理:macOS 每次 debug 构建产出独立的 zip+dmg+update.json,
187
+ // 不清理会按构建次数堆积。清理保留最新一整套,不影响本次传输。
188
+ await pruneQuietly(deps.pruneMacosBetaDirectory, asset.fileName, " macOS 构建包");
178
189
  streamFileWithRange(req, res, {
179
190
  filePath: asset.filePath,
180
191
  size: asset.size,
@@ -294,6 +305,8 @@ export function registerPublicUpdateRoutes(app, deps, publicOrigin) {
294
305
  res.status(404).json({ error: "当前没有可下载的 IPA 文件。" });
295
306
  return;
296
307
  }
308
+ // iOS 每次 debug 构建产出独立时间戳的 IPA,不清理会按构建次数堆积。
309
+ await pruneQuietly(deps.pruneIosIpaDirectory, asset.fileName, " iOS 构建包");
297
310
  streamFileWithRange(req, res, {
298
311
  filePath: asset.filePath,
299
312
  size: asset.size,
@@ -21,6 +21,7 @@ import { parentTaskIdFrom } from "./task-agent-parsing.js";
21
21
  import { taskTitleService, refreshAutoBoardTaskTitles } from "./task-title-service.js";
22
22
  import { organizeSelectedByTitle, titleJudgeForStorage } from "./task-title-organize-service.js";
23
23
  import { systemAiSessions } from "./system-ai-session-recorder.js";
24
+ import { isSystemAiCall } from "./system-ai-call.js";
24
25
  import { registerConversationOrganizeRoutes } from "./server-conversation-organize-routes.js";
25
26
  import { TITLE_ORGANIZE_MAX_SESSIONS, TITLE_ORGANIZE_MAX_TASKS } from "./task-title-organize.js";
26
27
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
@@ -223,7 +224,7 @@ function workspaceWithCounts(storage, workspace, sessionCounts, includeArchived
223
224
  const worktreeCount = storage.listWorkspaceTasks(workspace.id)
224
225
  .filter((task) => (includeArchived || !task.archived) && task.worktree !== null)
225
226
  .length;
226
- const sessionCount = (sessionCounts ?? storage.countSessionsByWorkspace({ includeArchived })).get(workspace.id) ?? 0;
227
+ const sessionCount = (sessionCounts ?? storage.countSessionsByWorkspace({ includeArchived, excludeSystemAiCalls: true })).get(workspace.id) ?? 0;
227
228
  return { ...workspace, worktreeCount, sessionCount };
228
229
  }
229
230
  // ── Layout validation / sanitization(前端 PUT 与测试共用)──
@@ -337,7 +338,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
337
338
  // 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
338
339
  app.get("/api/workspaces", (req, res) => {
339
340
  const includeArchived = req.query.includeArchived === "1";
340
- const sessionCounts = storage.countSessionsByWorkspace({ includeArchived });
341
+ const sessionCounts = storage.countSessionsByWorkspace({ includeArchived, excludeSystemAiCalls: true });
341
342
  res.json(storage.listWorkspaces()
342
343
  .filter((workspace) => !isGlobalWorkspace(workspace))
343
344
  .map((workspace) => workspaceWithCounts(storage, workspace, sessionCounts, includeArchived)));
@@ -376,7 +377,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
376
377
  const archivedTaskIds = new Set(storage.listWorkspaceTasks(workspace.id)
377
378
  .filter((task) => task.archived).map((task) => task.id));
378
379
  const teamMarkers = teamSessionMarkers(storage);
379
- const visibleSessions = storage.listSessionsByWorkspaceSlim(workspace.id).map((session) => ({
380
+ const visibleSessions = storage.listSessionsByWorkspaceSlim(workspace.id).filter((session) => !isSystemAiCall(session)).map((session) => ({
380
381
  ...workspaceSessionSummary(session, { workspaceName: workspace.name }, sessions, teamMarkersFor(teamMarkers, session.id)),
381
382
  workspaceTaskId: session.workspaceTaskId,
382
383
  })).filter((session) => includeArchived || (!session.archived
@@ -590,6 +591,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
590
591
  const snapshots = sessions?.listSlim();
591
592
  const revision = crypto.createHash("sha256").update(JSON.stringify({
592
593
  storage: storage.projectionRevision(),
594
+ projection: "workspace-work-sessions-v1",
593
595
  query: { includeArchived, workspaceFilter, taskLimit, sessionLimit },
594
596
  runtime: snapshots,
595
597
  })).digest("base64url");
@@ -599,7 +601,11 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
599
601
  }
600
602
  const model = new TaskReadModel(storage, sessions, snapshots);
601
603
  const allSessions = model.sessions;
602
- const sessionVisible = (session) => includeArchived || !session.archived;
604
+ // Server-owned one-shot feature logs are not Agent work. Do not filter by employee,
605
+ // sessionSource=automation or generic automation IDs: those also carry dispatch,
606
+ // communication and scheduled work. Keep storage and employee audit history intact.
607
+ const sessionVisible = (session) => !isSystemAiCall(session)
608
+ && (includeArchived || !session.archived);
603
609
  const workspaces = [...model.workspaces.values()];
604
610
  // 群聊与成员派发标记一次查全:列表里每条会话只做 Map 命中,不逐个查库。
605
611
  const teamMarkers = teamSessionMarkers(storage);
@@ -933,7 +939,7 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
933
939
  ...task,
934
940
  ...resolvedMilestoneFields(storage, task.milestoneId),
935
941
  cwd: taskRuntimeCwd(task, workspace),
936
- sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).map((session) => ({
942
+ sessions: storage.listSessionsByWorkspaceTaskSlim(task.id).filter((session) => !isSystemAiCall(session)).map((session) => ({
937
943
  ...workspaceSessionSummary(session, {
938
944
  taskName: task.name,
939
945
  workspaceName: workspace?.name,
package/dist/server.js CHANGED
@@ -16,9 +16,9 @@ import { authenticateBearerAppToken, createRequireAuth, getEffectivePassword, re
16
16
  import { createApiScopeMiddleware } from "./middleware/api-scope.js";
17
17
  import { createExtensionCors } from "./middleware/cors.js";
18
18
  import { ensureCertificates } from "./cert.js";
19
- import { getDefaultModelForProvider, getProviderDefaultModels, isExecutionMode, resolveConfigDir, writePreferenceToStorage, } from "./config.js";
19
+ import { getDefaultModelForProvider, getProviderDefaultModels, resolveConfigDir, writePreferenceToStorage, } from "./config.js";
20
20
  import { ModelCatalogService } from "./models.js";
21
- import { fallbackProviderModel, readProviderModelPreferences, sessionEmployeeFields } from "./session-model.js";
21
+ import { readProviderModelPreferences } from "./session-model.js";
22
22
  import { ProcessManager } from "./process-manager.js";
23
23
  import { SessionLogger } from "./session-logger.js";
24
24
  import { coreHarnessAgentDir } from "./harness-engine.js";
@@ -34,21 +34,17 @@ import { registerFileRoutes } from "./server-file-routes.js";
34
34
  import { recordRecentPath } from "./recent-paths.js";
35
35
  import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
36
36
  import { registerSettingsRoutes } from "./server-settings-routes.js";
37
- import { registerSpeechRoutes } from "./server-speech-routes.js";
38
- import { SpeechPolisherService } from "./speech-polisher-service.js";
39
- import { SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
40
37
  import { registerLocalModelRoutes } from "./server-local-model-routes.js";
41
- import { createStorageCleanup } from "./storage-cleanup.js";
38
+ import { createStorageCleanup, STORAGE_CLEANUP_RETENTION_DAYS } from "./storage-cleanup.js";
39
+ import { pruneMigrationBackups } from "./migration-backup-retention.js";
42
40
  import { registerStorageCleanupRoutes } from "./server-storage-cleanup-routes.js";
43
41
  import { LocalModelSetupService } from "./local-model-setup.js";
44
42
  import { DecisionExpertService } from "./decision-expert-service.js";
45
43
  import { DECISION_EXPERT_KEY } from "./decision-expert-identity.js";
46
- import { SpeechService } from "./speech-service.js";
47
44
  import { buildStructuredChatPersonaPayload, resolveAppConnectCode, } from "./server-app-connect.js";
48
45
  import { resolveTrustProxy, sendRouteError } from "./server-request.js";
49
46
  import { registerAuthRoutes } from "./server-auth-routes.js";
50
47
  import { WebLoginTickets } from "./web-login-tickets.js";
51
- import { inferProviderFromCommand, isSessionProvider, providerCliCommand } from "./session-provider.js";
52
48
  import { registerVaultRoutes } from "./server-vault-routes.js";
53
49
  import { registerGithubRoutes } from "./server-github-routes.js";
54
50
  import { registerTaskRoutes } from "./server-task-routes.js";
@@ -69,16 +65,13 @@ import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
69
65
  import { EmployeeImService } from "./employee-im.js";
70
66
  import { EmployeeImLifecycle } from "./employee-im-lifecycle.js";
71
67
  import { registerEmployeeImRoutes } from "./server-employee-im-routes.js";
72
- import { defaultRoleForCli } from "./default-employee.js";
73
68
  import { UserMemoryService } from "./user-memory.js";
74
69
  import { registerUserMemoryRoutes, userMemoryOperationLog } from "./server-user-memory.js";
75
70
  import { registerAttentionRoutes } from "./server-attention-routes.js";
76
71
  import { refreshProviderCliUpdateState, registerAdminUpdateRoutes, launchServerUpdate, registerPublicUpdateRoutes, ServerUpdateState, } from "./server-update-routes.js";
77
72
  import { registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
78
- import { parseSessionCreationOrigin } from "./session-request.js";
79
73
  import { registerWorkspaceRoutes } from "./server-workspace-routes.js";
80
- import { resolveSessionCwd } from "./session-cwd.js";
81
- import { backfillSessionWorkspaces, resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
74
+ import { backfillSessionWorkspaces } from "./workspace-binding.js";
82
75
  import { getErrorMessage } from "./error-utils.js";
83
76
  import { asyncRoute, jsonErrorHandler } from "./express-async.js";
84
77
  import { FeedbackReceiver, FeedbackReporter } from "./feedback.js";
@@ -89,9 +82,6 @@ import { computeRelaunch } from "./relaunch.js";
89
82
  import { createRetentionSweep, startRetentionTimer } from "./retention.js";
90
83
  import { RuntimeConfigState } from "./runtime-config.js";
91
84
  import { safeServiceInstalled } from "./tui/runtime-utils.js";
92
- import { toSessionDetailDTO } from "./session-transport.js";
93
- import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
94
- import { enrichStructuredMessages } from "./structured-client-protocol.js";
95
85
  import { registerUploadRoutes, invalidateUploadUsage } from "./upload-routes.js";
96
86
  import { uploadReferenceGuard } from "./upload-reference-guard.js";
97
87
  import { configTemporaryRoot } from "./managed-temp.js";
@@ -162,15 +152,6 @@ function readBuildInfo() {
162
152
  const BUILD_INFO = readBuildInfo();
163
153
  const DISPLAY_VERSION = BUILD_INFO.version || PKG_VERSION;
164
154
  const SERVER_INSTANCE_ID = crypto.randomUUID();
165
- /**
166
- * 解析 PTY 会话的 provider:显式 `provider` 字段优先,其次按命令前缀识别;
167
- * 都识别不出时回落到 Claude,与旧版客户端行为保持一致。
168
- */
169
- function resolveProviderForCommand(provider, command) {
170
- if (isSessionProvider(provider))
171
- return provider;
172
- return inferProviderFromCommand(command) ?? "claude";
173
- }
174
155
  // ── Startup error handling ──
175
156
  let activeFeedback = null;
176
157
  process.on("uncaughtException", (err) => {
@@ -263,7 +244,8 @@ export async function startServer(config, configPath, options = {}) {
263
244
  // Explicit and bounded: login/bearer limits key on req.ip, so forwarded headers are only
264
245
  // honoured for the configured proxy hops (see resolveTrustProxy).
265
246
  app.set("trust proxy", resolveTrustProxy(config.trustProxy));
266
- const storage = new WandStorage(resolveDatabasePath(configPath));
247
+ const databasePath = resolveDatabasePath(configPath);
248
+ const storage = new WandStorage(databasePath);
267
249
  const systemCalls = systemAiSessions(storage);
268
250
  systemCalls.recoverInterrupted();
269
251
  if (!storage.getConfigValue("migration:session-workspaces:v1")) {
@@ -273,9 +255,6 @@ export async function startServer(config, configPath, options = {}) {
273
255
  const taskTitles = taskTitleService(storage, { config });
274
256
  const runtimeConfig = new RuntimeConfigState(config);
275
257
  const openRouter = new OpenRouterFreeModelsService(storage, options.openRouterFetch);
276
- const speechPolisher = new SpeechPolisherService({ config, free: openRouter,
277
- employee: () => storage.getSystemSiliconEmployee(SPEECH_POLISHER_KEY),
278
- trace: employee => systemCalls.forEmployee(employee) });
279
258
  const authService = new AuthService(storage);
280
259
  const webLoginTickets = new WebLoginTickets();
281
260
  const settingsAccess = new SettingsWebAccess();
@@ -345,9 +324,27 @@ export async function startServer(config, configPath, options = {}) {
345
324
  }
346
325
  const storageCleanup = createStorageCleanup(configDir, { uploads: storage.uploads,
347
326
  uploadsActive: () => sessionRegistry.listSlim().some(session => (session.sessionKind === "structured" && session.status === "running") || (session.queuedMessages?.length ?? 0) > 0),
348
- onUploadRemoved: invalidateUploadUsage });
349
- const speech = new SpeechService(storage, configDir);
350
- const localModels = new LocalModelSetupService({ configDir, speech, decisions,
327
+ onUploadRemoved: invalidateUploadUsage,
328
+ // 历史构建包也会持续堆积(每次 debug 构建一个新版本号),跟着同一轮定时回收走。
329
+ pruneDistributionArtifacts: async () => {
330
+ const results = await Promise.all([
331
+ distributionManager.pruneAndroidApkDirectory(),
332
+ distributionManager.pruneMacosBetaDirectory(),
333
+ distributionManager.pruneIosIpaDirectory(),
334
+ ]);
335
+ return {
336
+ deleted: results.flatMap((result) => result.deleted),
337
+ freedBytes: results.reduce((total, result) => total + result.freedBytes, 0),
338
+ };
339
+ },
340
+ // Pi 原生回合目录(pi-native/)按回合数增长,同样交给同一轮定时回收。
341
+ collectNativeArtifacts: () => structuredSessions.collectNativeGarbage({
342
+ liveSessionIds: new Set(sessionRegistry.listSlim().map((session) => session.id)),
343
+ retentionMs: STORAGE_CLEANUP_RETENTION_DAYS * 24 * 60 * 60 * 1000,
344
+ }),
345
+ // 旧版本遗留的迁移前快照:无上限堆积。迁移成功路径已在 storage-schema 修剪。
346
+ pruneMigrationSnapshots: () => pruneMigrationBackups(databasePath, { dryRun: false }) });
347
+ const localModels = new LocalModelSetupService({ configDir, decisions,
351
348
  decisionConfig: () => config.localDecision ?? { enabled: false, pythonPath: "", modelPath: "" },
352
349
  configureDecision(next) {
353
350
  decisions.assertConfigurable();
@@ -676,7 +673,6 @@ export async function startServer(config, configPath, options = {}) {
676
673
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config, free: openRouter });
677
674
  registerMissionRoutes(app, missions);
678
675
  registerUploadRoutes(app, sessionRegistry, { uploads: storage.uploads, temporaryRoot: configTemporaryRoot(configDir) });
679
- registerSpeechRoutes(app, { speech, requireSessions, requireAdmin, polisher: speechPolisher });
680
676
  registerLocalModelRoutes(app, { models: localModels, requireSessions, requireAdmin });
681
677
  registerStorageCleanupRoutes(app, storageCleanup, requireAdmin);
682
678
  app.post("/api/optimize-prompt", asyncRoute(async (req, res) => {
@@ -723,99 +719,6 @@ export async function startServer(config, configPath, options = {}) {
723
719
  }));
724
720
  registerFileRoutes(app, { storage, defaultCwd: config.defaultCwd });
725
721
  // ── Session control ──
726
- app.post("/api/commands", asyncRoute(async (req, res) => {
727
- const body = req.body;
728
- if (body.employeeId !== undefined || body.teamId !== undefined
729
- || (body.subject !== undefined && body.subject?.type !== "cli")) {
730
- res.status(400).json({ error: "PTY 只能选择 CLI 工具。" });
731
- return;
732
- }
733
- if (body.provider !== undefined && !isSessionProvider(body.provider)) {
734
- res.status(400).json({ error: "请选择有效的执行工具。" });
735
- return;
736
- }
737
- const interactiveShell = body.shell === true;
738
- // 连接应用只能启动 Provider CLI 会话(provider 字段 + 该 CLI 的标准可执行名)。
739
- // 任意自定义命令字符串需要 admin。shell:true 是原生客户端“空白终端”(服务端配置的
740
- // 登录 shell,不接受外部命令):为保持现有客户端可用而保留,见 AGENTS 与报告。
741
- if (!principalHasScope(req.principal, "admin") && !interactiveShell) {
742
- // Provider CLI only: the executable must be the provider's own CLI name (old clients send
743
- // the provider id as the command, so an absent provider is inferred from it).
744
- const rawCommand = typeof body.command === "string" ? body.command.trim() : "";
745
- const provider = isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand);
746
- const isProviderCli = !!provider && (!rawCommand || rawCommand === provider || rawCommand === providerCliCommand(provider));
747
- if (!isProviderCli) {
748
- res.status(403).json({ error: "当前连接只能启动已配置的 CLI 工具,不能执行自定义命令。", code: "forbidden" });
749
- return;
750
- }
751
- }
752
- if (!interactiveShell && !body.command?.trim()) {
753
- res.status(400).json({ error: "请输入要执行的命令。" });
754
- return;
755
- }
756
- if (body.mode !== undefined && !isExecutionMode(body.mode)) {
757
- res.status(400).json({ error: `无效执行模式: ${String(body.mode)}` });
758
- return;
759
- }
760
- const initialInput = body.initialInput?.trim();
761
- try {
762
- const origin = parseSessionCreationOrigin(body);
763
- const rawModel = typeof body.model === "string" ? body.model.trim() : "";
764
- const rawCommand = body.command?.trim() ?? "";
765
- const provider = interactiveShell
766
- ? undefined
767
- : resolveProviderForCommand(body.provider, rawCommand);
768
- // Older clients used the provider id as the PTY command. Qoder's executable
769
- // is named qodercli, so keep those clients working while preserving custom commands.
770
- const command = provider === "qoder" && rawCommand === "qoder"
771
- ? "qodercli"
772
- : rawCommand;
773
- const effectiveModel = provider ? rawModel || fallbackProviderModel(config, provider) : undefined;
774
- const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
775
- const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
776
- const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
777
- const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
778
- const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
779
- && (isSessionProvider(body.provider) || inferProviderFromCommand(command))
780
- ? defaultRoleForCli(storage, provider) : null;
781
- const snapshot = await (interactiveShell
782
- ? processes.startShell(sessionCwd, body.mode ?? "default", {
783
- worktreeEnabled: body.worktreeEnabled === true,
784
- cols: reqCols,
785
- rows: reqRows,
786
- workspaceId,
787
- workspaceTaskId: body.workspaceTaskId,
788
- ...origin,
789
- })
790
- : processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
791
- worktreeEnabled: body.worktreeEnabled === true,
792
- provider,
793
- model: effectiveModel,
794
- cols: reqCols,
795
- rows: reqRows,
796
- thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
797
- systemPrompt: body.systemPrompt?.trim() || role?.prompt,
798
- ...sessionEmployeeFields(role),
799
- workspaceId,
800
- workspaceTaskId: body.workspaceTaskId,
801
- ...origin,
802
- }));
803
- recordRecentPath(storage, snapshot.cwd);
804
- const compactTools = req.get("X-Wand-Tool-Projection") === "compact"
805
- || req.query.compactTools === "1";
806
- if (compactTools) {
807
- const messages = compactToolMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id));
808
- const windowed = windowMessagesForTransport(messages, config.cardDefaults ?? {});
809
- res.status(201).json(toSessionDetailDTO(snapshot, windowed));
810
- }
811
- else {
812
- res.status(201).json(toSessionDetailDTO(snapshot));
813
- }
814
- }
815
- catch (error) {
816
- sendRouteError(res, error, "无法启动命令。请检查命令是否安装。");
817
- }
818
- }));
819
722
  // ── WebSocket broadcast layer ──
820
723
  let activeSslCertPath = null;
821
724
  const server = useHttps
@@ -891,6 +794,7 @@ export async function startServer(config, configPath, options = {}) {
891
794
  });
892
795
  structuredSessions.setEventEmitter((event) => {
893
796
  taskTitles.ingest(event);
797
+ storage.recordTaskCommentSessionEvent(event);
894
798
  conversationService.ingestSessionEvent(event);
895
799
  scheduledTasks.ingestSessionEvent(event);
896
800
  sessionCompletions.ingest(event);
@@ -990,8 +894,6 @@ export async function startServer(config, configPath, options = {}) {
990
894
  catch { /* noop */ }
991
895
  conversationService.dispose();
992
896
  localModels.dispose();
993
- speech.dispose();
994
- speechPolisher.dispose();
995
897
  try {
996
898
  structuredSessions.dispose();
997
899
  }
@@ -1322,8 +1224,6 @@ export async function startServer(config, configPath, options = {}) {
1322
1224
  catch { /* best-effort shutdown */ }
1323
1225
  conversationService.dispose();
1324
1226
  localModels.dispose();
1325
- speech.dispose();
1326
- speechPolisher.dispose();
1327
1227
  try {
1328
1228
  structuredSessions.dispose();
1329
1229
  }
@@ -42,6 +42,8 @@ export declare class ConversationRepository {
42
42
  acceptConversationSession(instance: ConversationInstance, createSession: () => string, title: string, input: string, request: ConversationRequest): void;
43
43
  /** Attach an independent interactive session, without copying its transcript or executing it. */
44
44
  linkEmployeeSession(instance: ConversationInstance, session: SessionSnapshot): void;
45
+ /** Remove only the canonical system/employee link, never a user's chat message. */
46
+ unlinkEmployeeSession(sessionId: string): void;
45
47
  employeeConversationForSession(sessionId: string): string | null;
46
48
  conversationSessionIds(id: string): string[];
47
49
  conversationTaskStartup(taskId: string): ConversationRequest["receipt"] | null;
@@ -163,6 +163,10 @@ export class ConversationRepository {
163
163
  .run(messageId, instance.id, JSON.stringify(turn), session.startedAt);
164
164
  });
165
165
  }
166
+ /** Remove only the canonical system/employee link, never a user's chat message. */
167
+ unlinkEmployeeSession(sessionId) {
168
+ this.db.prepare("DELETE FROM conversation_messages WHERE id = ?").run(`employee-session:${sessionId}`);
169
+ }
166
170
  employeeConversationForSession(sessionId) {
167
171
  const row = this.db.prepare("SELECT conversation_id FROM conversation_messages WHERE id = ?")
168
172
  .get(`employee-session:${sessionId}`);
@@ -26,8 +26,8 @@ export declare class EmployeeRepository {
26
26
  ensureSystemSiliconEmployee(seed?: SystemEmployeeSeed): SiliconEmployee;
27
27
  /** Stable built-in decision employee; seed once and retain the user's ordered call chain. */
28
28
  ensureDecisionExpertEmployee(): SiliconEmployee;
29
- /** Seed the SDK free group once, then preserve independent user choices. */
30
- ensureSpeechPolisherEmployee(): SiliconEmployee;
29
+ /** The speech polisher built-in was removed; archive (never delete) a leftover row so history stays readable. */
30
+ retireLegacySpeechPolisherEmployee(): void;
31
31
  /** HR owns recruitment independently; seed from configured system candidates once. */
32
32
  ensureHrEmployee(seedAgents?: WandTaskAgent[]): SiliconEmployee;
33
33
  saveSiliconEmployee(employee: SiliconEmployee): void;
@@ -8,8 +8,6 @@ import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "../employee-knowledge-types.js";
8
8
  import { FIXED_EMPLOYEE_AVATARS, employeeAvatarValue, fixedEmployeeAvatar, isFixedAvatarEmployee } from "../fixed-employee-avatar.js";
9
9
  import { HR_EMPLOYEE_KEY } from "../hr-employee-identity.js";
10
10
  import { hrEmployeeDefinition } from "../hr-employee.js";
11
- import { speechPolisherDefinition } from "../speech-polisher-employee.js";
12
- import { SPEECH_POLISHER_KEY } from "../speech-polisher-identity.js";
13
11
  import { SYSTEM_EMPLOYEE_KEY, systemEmployeeDefinition, systemEmployeeDefaultAgents, systemEmployeeSeedAgents } from "../system-employee.js";
14
12
  import { LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS, mapSiliconEmployeeRow } from "./internal.js";
15
13
  /** Employee storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
@@ -87,16 +85,10 @@ export class EmployeeRepository {
87
85
  }
88
86
  return existing;
89
87
  }
90
- /** Seed the SDK free group once, then preserve independent user choices. */
91
- ensureSpeechPolisherEmployee() {
92
- const existing = this.getSystemSiliconEmployee(SPEECH_POLISHER_KEY);
93
- const definition = speechPolisherDefinition(new Date().toISOString(), existing);
94
- if (!existing || existing.name !== definition.name || existing.duty !== definition.duty || existing.prompt !== definition.prompt
95
- || existing.avatar !== definition.avatar || existing.archivedAt || !existing.agents.length) {
96
- this.saveSiliconEmployee(definition);
97
- return definition;
98
- }
99
- return existing;
88
+ /** The speech polisher built-in was removed; archive (never delete) a leftover row so history stays readable. */
89
+ retireLegacySpeechPolisherEmployee() {
90
+ const now = new Date().toISOString();
91
+ this.db.prepare("UPDATE silicon_employees SET archived_at = ?, updated_at = ? WHERE system_key = 'wand-speech-polisher' AND archived_at IS NULL").run(now, now);
100
92
  }
101
93
  /** HR owns recruitment independently; seed from configured system candidates once. */
102
94
  ensureHrEmployee(seedAgents) {
@@ -141,9 +133,13 @@ export class EmployeeRepository {
141
133
  const tags = isBuiltinSiliconEmployee(employee)
142
134
  ? siliconEmployeeTags(employee)
143
135
  : parseSiliconEmployeeTags(employee.tags ?? []);
136
+ const systemSessionLimit = employee.systemSessionLimit === undefined ? existing?.systemSessionLimit ?? null : employee.systemSessionLimit;
137
+ if (systemSessionLimit !== null && (!Number.isInteger(systemSessionLimit) || systemSessionLimit < 0 || systemSessionLimit > 10_000)) {
138
+ throw new Error("系统调用历史保留上限必须是 0 到 10000 的整数,或 null(不限)。");
139
+ }
144
140
  this.db.prepare(`INSERT INTO silicon_employees (
145
- id, name, duty, prompt, avatar, agents_json, system_key, archived_at, created_at, updated_at, tags_json
146
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
141
+ id, name, duty, prompt, avatar, agents_json, system_key, archived_at, created_at, updated_at, tags_json, system_session_limit
142
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
147
143
  ON CONFLICT(id) DO UPDATE SET
148
144
  name = excluded.name,
149
145
  duty = excluded.duty,
@@ -151,11 +147,12 @@ export class EmployeeRepository {
151
147
  avatar = excluded.avatar,
152
148
  agents_json = excluded.agents_json,
153
149
  tags_json = excluded.tags_json,
150
+ system_session_limit = excluded.system_session_limit,
154
151
  system_key = excluded.system_key,
155
152
  archived_at = excluded.archived_at,
156
153
  updated_at = excluded.updated_at`).run(employee.id, employee.name, employee.duty, employee.prompt,
157
154
  // Accept a canonical read echo without persisting a display projection over a legacy value.
158
- target && existing ? existing.avatar : employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags));
155
+ target && existing ? existing.avatar : employee.avatar, JSON.stringify(employee.agents), employee.systemKey ?? null, employee.archivedAt ?? null, employee.createdAt, employee.updatedAt, JSON.stringify(tags), systemSessionLimit);
159
156
  }
160
157
  /** Explicit, atomic migration; not called from GET, config load, seeds, or ordinary saves.
161
158
  * Compare all expected avatar values first; change only avatar, including preserving timestamps.
@@ -4,14 +4,13 @@ import { AI_TEAM_DEFAULT_MAX_STEPS, isTeamMemberRole, memberAgents, siliconEmplo
4
4
  import { decisionExpertDefinition } from "../decision-expert-employee.js";
5
5
  import { hrEmployeeDefinition } from "../hr-employee.js";
6
6
  import { isSessionProvider } from "../session-provider.js";
7
- import { speechPolisherDefinition } from "../speech-polisher-employee.js";
8
7
  import { isWandTaskAgentKind } from "../task-types.js";
9
8
  import { GLOBAL_WORKSPACE_ID } from "../types.js";
10
9
  import { safeJsonParse } from "./mappers.js";
11
10
  import { parseWandTaskAgent } from "./mappers.js";
12
11
  // Shared module-level helpers for the storage repositories (moved verbatim from storage.ts).
13
12
  // These system profiles are code-owned; candidates and timestamps are not part of the lock.
14
- export const LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS = [decisionExpertDefinition(""), speechPolisherDefinition(""), hrEmployeeDefinition("")];
13
+ export const LOCKED_SYSTEM_EMPLOYEE_DEFINITIONS = [decisionExpertDefinition(""), hrEmployeeDefinition("")];
15
14
  export const DEFAULT_DB_FILE = "wand.db";
16
15
  export function resolveDatabasePath(configPath) {
17
16
  return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
@@ -64,6 +63,7 @@ export function mapSiliconEmployeeRow(row) {
64
63
  agents,
65
64
  systemKey,
66
65
  tags: siliconEmployeeTags({ systemKey, tags }),
66
+ systemSessionLimit: typeof row.system_session_limit === "number" ? row.system_session_limit : null,
67
67
  archivedAt: typeof row.archived_at === "string" && row.archived_at ? row.archived_at : undefined,
68
68
  createdAt: String(row.created_at),
69
69
  updatedAt: String(row.updated_at),
@@ -272,4 +272,24 @@ export const STORAGE_MIGRATIONS = [
272
272
  if (!columns.some(column => column.name === "acceptance_json"))
273
273
  db.exec("ALTER TABLE wand_tasks ADD COLUMN acceptance_json TEXT");
274
274
  } },
275
+ { version: 11, name: "task-comment-dispatch", up: db => db.exec(`
276
+ CREATE TABLE IF NOT EXISTS wand_task_comment_dispatches (
277
+ comment_id TEXT PRIMARY KEY REFERENCES wand_task_comments(id) ON DELETE CASCADE,
278
+ task_id TEXT NOT NULL REFERENCES wand_tasks(id) ON DELETE CASCADE,
279
+ request_id TEXT NOT NULL, signature TEXT NOT NULL, source_comment_id TEXT,
280
+ employee_id TEXT, employee_name TEXT, instruction TEXT NOT NULL,
281
+ task_version TEXT NOT NULL, state TEXT NOT NULL, session_id TEXT, error TEXT, execution_status TEXT,
282
+ UNIQUE(task_id, request_id)
283
+ );
284
+ CREATE INDEX IF NOT EXISTS idx_comment_dispatch_session ON wand_task_comment_dispatches(session_id);
285
+ `) },
286
+ { version: 12, name: "employee-system-session-limit", up: db => {
287
+ const columns = db.prepare("PRAGMA table_info(silicon_employees)").all();
288
+ if (!columns.some(column => column.name === "system_session_limit")) {
289
+ db.exec("ALTER TABLE silicon_employees ADD COLUMN system_session_limit INTEGER");
290
+ }
291
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_system_ai_employee_history
292
+ ON command_sessions(json_extract(session_options, '$.employeeId'), started_at DESC)
293
+ WHERE automation_id GLOB 'system-ai:*'`);
294
+ } },
275
295
  ];
@@ -3,6 +3,9 @@ import { decryptVaultSecretTyped } from "../password-manager.js";
3
3
  import { readVaultKey } from "./vault-keys.js";
4
4
  export function openStorageReadonly(dbPath) {
5
5
  const db = new DatabaseSync(dbPath, { readOnly: true });
6
+ // Local CLI readers share the file with the Server writer. Configure only this connection;
7
+ // a transient exclusive/schema lock is not proof of a missing/revoked capability.
8
+ db.exec("PRAGMA busy_timeout = 5000");
6
9
  const readConfig = (key) => {
7
10
  const value = db.prepare("SELECT value FROM app_config WHERE key = ?").get(key)?.value;
8
11
  return typeof value === "string" ? value : null;
@@ -54,5 +54,7 @@ export declare class SessionRepository {
54
54
  /** List durable metadata without reading or parsing output/messages payloads. */
55
55
  loadSessionsSlim(): SessionSnapshot[];
56
56
  mapSessionRow(row: SessionRow): SessionSnapshot;
57
+ /** Metadata-only indexed scan. Running or ambiguous records never count toward the history cap. */
58
+ excessSystemAiSessionIds(employeeId: string, keep: number): string[];
57
59
  deleteSession(id: string): void;
58
60
  }
@@ -2,6 +2,8 @@ import { sessionPersistValues } from "./mappers.js";
2
2
  import { sessionRuntimeMetadataValues } from "./mappers.js";
3
3
  import { nowIso } from "../password-manager.js";
4
4
  import { sessionSelectFields, sessionPersistFields, sessionPersistAssignments, sessionRuntimeMetadataAssignments, mapSessionCore, sessionRowQuery } from "./mappers.js";
5
+ import { SYSTEM_AI_AUXILIARY_PURPOSES, SYSTEM_AI_CALL_PREFIX } from "../system-ai-call.js";
6
+ const AUXILIARY_HISTORY_PATTERNS = SYSTEM_AI_AUXILIARY_PURPOSES.map(purpose => `${SYSTEM_AI_CALL_PREFIX}${purpose}:?*`);
5
7
  /** Session storage: one domain of the WandStorage facade. Shares the connection and the facade's transaction. */
6
8
  export class SessionRepository {
7
9
  db;
@@ -216,6 +218,17 @@ export class SessionRepository {
216
218
  mapSessionRow(row) {
217
219
  return mapSessionCore(row);
218
220
  }
221
+ /** Metadata-only indexed scan. Running or ambiguous records never count toward the history cap. */
222
+ excessSystemAiSessionIds(employeeId, keep) {
223
+ if (!Number.isInteger(keep) || keep < 0 || keep > 10_000)
224
+ return [];
225
+ const rows = this.db.prepare(`SELECT id FROM command_sessions
226
+ WHERE (${AUXILIARY_HISTORY_PATTERNS.map(() => "automation_id GLOB ?").join(" OR ")})
227
+ AND json_extract(session_options, '$.employeeId') = ?
228
+ AND status IN ('exited', 'failed', 'stopped') AND ended_at IS NOT NULL
229
+ ORDER BY started_at DESC, rowid DESC LIMIT -1 OFFSET ?`).all(...AUXILIARY_HISTORY_PATTERNS, employeeId, keep);
230
+ return rows.map(row => row.id);
231
+ }
219
232
  deleteSession(id) {
220
233
  this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
221
234
  }