@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
@@ -0,0 +1,91 @@
1
+ import { readdirSync, lstatSync, unlinkSync } from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * 迁移前 SQLite 快照(`wand.db.pre-v<N>-<时间戳>-<随机>.bak`)的有界保留。
5
+ *
6
+ * 每次 schema 升级都会 `VACUUM INTO` 一份完整副本,而快照体积等于整库大小,
7
+ * 没有上限时旧备份会无限累积。这里只做「有界保留」,不做数据清理:
8
+ *
9
+ * - 迁移**失败**时绝不调用(调用方保证只在迁移成功且健康检查通过后调用);
10
+ * - 默认保留最近 `keep` 份,外加调用方明确要求保留的那一份(刚成功这次迁移的回滚点);
11
+ * - 只认迁移备份的命名,不碰 `wand.db`、`backups/` 里的人工恢复点或配置快照;
12
+ * - 删除失败不影响迁移结果。
13
+ */
14
+ /** 迁移前快照的命名:`<db>.pre-v12-2026-10-18T09-58-41-937Z-ca8636.bak`。 */
15
+ const MIGRATION_BACKUP_PATTERN = /\.pre-v\d+-\d{4}-\d{2}-\d{2}T[\d-]+Z-[0-9a-f]{6}\.bak$/;
16
+ /** 人工标记的迁移前快照,例如 `pre-organization-baseline-repair-<时间戳>.bak`。 */
17
+ const MANUAL_BACKUP_PATTERN = /\.pre-(?!v\d+-)[A-Za-z0-9-]+-\d{4}-\d{2}-\d{2}T[\d-]+Z\.bak$/;
18
+ export const DEFAULT_MIGRATION_BACKUP_KEEP = 2;
19
+ /**
20
+ * 保留最近若干份迁移快照。`keepPath` 是本次刚生成、必须保留的回滚点。
21
+ *
22
+ * `keep` 统计的是「自动迁移快照」,人工命名的快照不计入额度,单独保留最近一份,
23
+ * 避免把开发者手工留的基线修复点一起清掉。
24
+ */
25
+ export function pruneMigrationBackups(dbPath, options = {}) {
26
+ const result = { deleted: [], freedBytes: 0, kept: [] };
27
+ const keepCount = Math.max(1, options.keep ?? DEFAULT_MIGRATION_BACKUP_KEEP);
28
+ const directory = path.dirname(dbPath);
29
+ const base = path.basename(dbPath);
30
+ let entries;
31
+ try {
32
+ entries = readdirSync(directory, { withFileTypes: true });
33
+ }
34
+ catch {
35
+ return result;
36
+ }
37
+ const automatic = [];
38
+ const manual = [];
39
+ for (const entry of entries) {
40
+ if (!entry.isFile() || !entry.name.startsWith(`${base}.pre-`))
41
+ continue;
42
+ const filePath = path.join(directory, entry.name);
43
+ let stat;
44
+ try {
45
+ stat = lstatSync(filePath);
46
+ // 符号链接一律不碰:备份文件必须是普通文件。
47
+ if (!stat.isFile() || stat.isSymbolicLink())
48
+ continue;
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ const record = { name: entry.name, filePath, mtimeMs: stat.mtimeMs, size: stat.size };
54
+ if (MIGRATION_BACKUP_PATTERN.test(entry.name))
55
+ automatic.push(record);
56
+ else if (MANUAL_BACKUP_PATTERN.test(entry.name))
57
+ manual.push(record);
58
+ }
59
+ const keepName = options.keepPath ? path.basename(options.keepPath) : null;
60
+ const newestFirst = (a, b) => b.mtimeMs - a.mtimeMs;
61
+ automatic.sort(newestFirst);
62
+ manual.sort(newestFirst);
63
+ const keep = new Set();
64
+ if (keepName)
65
+ keep.add(keepName);
66
+ for (const record of automatic.slice(0, keepCount))
67
+ keep.add(record.name);
68
+ // 人工快照只留最近一份,其余按同一规则回收。
69
+ if (manual.length > 0)
70
+ keep.add(manual[0].name);
71
+ for (const record of [...automatic, ...manual]) {
72
+ if (keep.has(record.name)) {
73
+ result.kept.push(record.name);
74
+ continue;
75
+ }
76
+ if (options.dryRun) {
77
+ result.deleted.push(record.name);
78
+ result.freedBytes += record.size;
79
+ continue;
80
+ }
81
+ try {
82
+ unlinkSync(record.filePath);
83
+ result.deleted.push(record.name);
84
+ result.freedBytes += record.size;
85
+ }
86
+ catch {
87
+ // 删不掉(权限/占用)就留着,下次扫描再试。
88
+ }
89
+ }
90
+ return result;
91
+ }
@@ -40,5 +40,33 @@ export declare class PiTmuxHost implements StructuredExecHost {
40
40
  listRuns(): Promise<StructuredRunState[]>;
41
41
  kill(job: NativeJob): Promise<void>;
42
42
  forgetRun(runId: string): void;
43
+ /**
44
+ * 回收 pi-native 下已经没用的执行产物。每次 Pi 原生回合都会在 slot 里
45
+ * `mkdtemp` 一个新 turn 目录(events.jsonl / stderr.log / state.json 等),
46
+ * 而 forgetRun 只写 ack 标记、从不删目录,所以会按回合数无限堆积。
47
+ *
48
+ * 只回收三类确定无主的东西:
49
+ * - slot 已 ack 且 runId 不再属于任何存活会话:整个 slot 无主;
50
+ * - slot 无主的,整槽删除;
51
+ * - slot 仍有主的,只删非 current.json 引用、已 ack 且进程已退出、超保留期的 turn。
52
+ *
53
+ * 保留 current.json 指向的 turn:`listRuns` / `adoptRun` 仍靠它的 events.jsonl
54
+ * 重放恢复;未 ack 的 slot 可能是尚未收尾的回合,一律不动。
55
+ */
56
+ collectGarbage(options: {
57
+ /** 仍在数据库里的会话 id;不在其中的 slot 视为无主。 */
58
+ liveSessionIds: ReadonlySet<string>;
59
+ retentionMs: number;
60
+ now?: number;
61
+ }): PiNativeGcReport;
62
+ private readJob;
63
+ /** 无主 slot 是否已整体静默:所有 turn 都不再是运行中,且最新写入超过保留期。 */
64
+ private slotIsIdle;
65
+ }
66
+ /** pi-native 回收结果:删了几个 slot / 几个旧 turn / 释放多少字节。 */
67
+ export interface PiNativeGcReport {
68
+ removedSlots: number;
69
+ removedTurns: number;
70
+ freedBytes: number;
43
71
  }
44
72
  export {};
@@ -382,4 +382,193 @@ export class PiTmuxHost {
382
382
  writeFileSync(job.ack, "consumed\n", { mode: 0o600 });
383
383
  this.observed.delete(runId);
384
384
  }
385
+ /**
386
+ * 回收 pi-native 下已经没用的执行产物。每次 Pi 原生回合都会在 slot 里
387
+ * `mkdtemp` 一个新 turn 目录(events.jsonl / stderr.log / state.json 等),
388
+ * 而 forgetRun 只写 ack 标记、从不删目录,所以会按回合数无限堆积。
389
+ *
390
+ * 只回收三类确定无主的东西:
391
+ * - slot 已 ack 且 runId 不再属于任何存活会话:整个 slot 无主;
392
+ * - slot 无主的,整槽删除;
393
+ * - slot 仍有主的,只删非 current.json 引用、已 ack 且进程已退出、超保留期的 turn。
394
+ *
395
+ * 保留 current.json 指向的 turn:`listRuns` / `adoptRun` 仍靠它的 events.jsonl
396
+ * 重放恢复;未 ack 的 slot 可能是尚未收尾的回合,一律不动。
397
+ */
398
+ collectGarbage(options) {
399
+ const report = { removedSlots: 0, removedTurns: 0, freedBytes: 0 };
400
+ const now = options.now ?? Date.now();
401
+ let entries;
402
+ try {
403
+ entries = readdirSync(this.root, { withFileTypes: true });
404
+ }
405
+ catch {
406
+ return report;
407
+ }
408
+ for (const entry of entries) {
409
+ if (!entry.isDirectory())
410
+ continue;
411
+ const slot = path.join(this.root, entry.name);
412
+ const job = this.readJob(slot);
413
+ if (!job)
414
+ continue;
415
+ const acked = existsSync(job.ack);
416
+ const sessionId = job.runId.replace(/^structured:/, "");
417
+ const orphan = !options.liveSessionIds.has(sessionId);
418
+ if (orphan) {
419
+ // 无主 slot 也得等 ack 完成:未 ack 说明回合可能还在收尾。
420
+ if (!acked)
421
+ continue;
422
+ if (!this.slotIsIdle(slot, job, now, options.retentionMs))
423
+ continue;
424
+ report.freedBytes += directoryBytes(slot);
425
+ if (removeTree(slot))
426
+ report.removedSlots += 1;
427
+ else
428
+ report.freedBytes -= directoryBytes(slot);
429
+ continue;
430
+ }
431
+ // 有主 slot:只删不被 current.json 引用的旧 turn。
432
+ const current = path.dirname(job.events);
433
+ let turns;
434
+ try {
435
+ turns = readdirSync(slot, { withFileTypes: true });
436
+ }
437
+ catch {
438
+ continue;
439
+ }
440
+ for (const turn of turns) {
441
+ if (!turn.isDirectory() || !turn.name.startsWith("turn-"))
442
+ continue;
443
+ const directory = path.join(slot, turn.name);
444
+ if (directory === current)
445
+ continue;
446
+ if (!existsSync(path.join(directory, "ack")))
447
+ continue;
448
+ const state = readJson(path.join(directory, "state.json"));
449
+ // 进程还活着的回合不能被当成垃圾。state 写 running 但 PID 已消失的
450
+ // (异常退出/被强杀)按已退出处理,否则这种回合会永久堆积。
451
+ if (!state || stateAlive(state))
452
+ continue;
453
+ let latest;
454
+ try {
455
+ latest = directoryLatestMtime(directory);
456
+ }
457
+ catch {
458
+ continue;
459
+ }
460
+ if (now - latest < options.retentionMs)
461
+ continue;
462
+ const bytes = directoryBytes(directory);
463
+ if (!removeTree(directory))
464
+ continue;
465
+ report.removedTurns += 1;
466
+ report.freedBytes += bytes;
467
+ }
468
+ }
469
+ return report;
470
+ }
471
+ readJob(slot) {
472
+ try {
473
+ return JSON.parse(readFileSync(path.join(slot, "current.json"), "utf8"));
474
+ }
475
+ catch {
476
+ return null;
477
+ }
478
+ }
479
+ /** 无主 slot 是否已整体静默:所有 turn 都不再是运行中,且最新写入超过保留期。 */
480
+ slotIsIdle(slot, job, now, retentionMs) {
481
+ const current = path.dirname(job.events);
482
+ const state = readJson(job.state);
483
+ if (state && stateAlive(state))
484
+ return false;
485
+ let latest;
486
+ try {
487
+ latest = directoryLatestMtime(slot);
488
+ }
489
+ catch {
490
+ return false;
491
+ }
492
+ if (now - latest < retentionMs)
493
+ return false;
494
+ // 非 current 的 turn 里如果还有活进程,整槽不能删。
495
+ let turns;
496
+ try {
497
+ turns = readdirSync(slot, { withFileTypes: true });
498
+ }
499
+ catch {
500
+ return false;
501
+ }
502
+ for (const turn of turns) {
503
+ if (!turn.isDirectory() || !turn.name.startsWith("turn-"))
504
+ continue;
505
+ const directory = path.join(slot, turn.name);
506
+ if (directory === current)
507
+ continue;
508
+ const other = readJson(path.join(directory, "state.json"));
509
+ if (other && stateAlive(other))
510
+ return false;
511
+ }
512
+ return true;
513
+ }
514
+ }
515
+ function readJson(file) {
516
+ try {
517
+ return JSON.parse(readFileSync(file, "utf8"));
518
+ }
519
+ catch {
520
+ return null;
521
+ }
522
+ }
523
+ /** 回合是否仍活着:state 声称未退出且 PID 真实存在。 */
524
+ function stateAlive(state) {
525
+ return state.status !== "exited" && pidAlive(state.pid);
526
+ }
527
+ function pidAlive(pid) {
528
+ if (!pid || pid <= 0)
529
+ return false;
530
+ try {
531
+ process.kill(pid, 0);
532
+ return true;
533
+ }
534
+ catch (error) {
535
+ // EPERM means the PID exists but belongs to another user; treat as alive.
536
+ return error.code === "EPERM";
537
+ }
538
+ }
539
+ function directoryLatestMtime(directory) {
540
+ let latest = lstatSync(directory).mtimeMs;
541
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
542
+ const child = path.join(directory, entry.name);
543
+ const stat = lstatSync(child);
544
+ if (stat.isDirectory())
545
+ latest = Math.max(latest, directoryLatestMtime(child));
546
+ // 只看 mtime:ctime 无法回拨,且文件被任何元数据操作碰一下就会刷新,
547
+ // 用它做保留期会把「早已结束的回合」永远当成刚活动过。
548
+ else
549
+ latest = Math.max(latest, stat.mtimeMs);
550
+ }
551
+ return latest;
552
+ }
553
+ function directoryBytes(directory) {
554
+ let total = 0;
555
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
556
+ const child = path.join(directory, entry.name);
557
+ const stat = lstatSync(child);
558
+ if (stat.isDirectory())
559
+ total += directoryBytes(child);
560
+ else
561
+ total += stat.size;
562
+ }
563
+ return total;
564
+ }
565
+ /** 只删目录,不跟随符号链接。 */
566
+ function removeTree(directory) {
567
+ try {
568
+ rmSync(directory, { recursive: true, force: false });
569
+ return true;
570
+ }
571
+ catch {
572
+ return false;
573
+ }
385
574
  }
@@ -5,6 +5,7 @@ import type { QuickCommitAiOptions } from "./ai-text-runner.js";
5
5
  import type { WandStorage } from "./storage.js";
6
6
  import type { WandTaskAgent } from "./task-types.js";
7
7
  import type { WandConfig } from "./types.js";
8
+ export declare function parseSystemSessionLimit(value: unknown, existing?: SiliconEmployee | null): number | null;
8
9
  /** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
9
10
  export declare function employeeDraftAiOptions(storage: WandStorage, config?: WandConfig, free?: Pick<OpenRouterFreeModelsService, "resolveForCall">): QuickCommitAiOptions;
10
11
  export interface EmployeeDraftRequest {
@@ -15,6 +15,17 @@ import { SESSION_PROVIDERS } from "./session-provider.js";
15
15
  import { DEFAULT_WAND_TASK_AGENT_KIND } from "./task-types.js";
16
16
  import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
17
17
  import { badRequest } from "./http-error.js";
18
+ import { systemAiSessions } from "./system-ai-session-recorder.js";
19
+ export function parseSystemSessionLimit(value, existing) {
20
+ if (value === undefined)
21
+ return existing?.systemSessionLimit ?? null;
22
+ if (value === null)
23
+ return null;
24
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 10_000) {
25
+ throw badRequest("系统调用历史保留上限必须是 0 到 10000 的整数,或 null(不限)。");
26
+ }
27
+ return value;
28
+ }
18
29
  /** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
19
30
  export function employeeDraftAiOptions(storage, config, free) {
20
31
  if (!config)
@@ -90,7 +101,7 @@ export function parseSystemEmployeeAgents(value, existing) {
90
101
  if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
91
102
  throw badRequest("该员工是内置的,身份不可修改。");
92
103
  }
93
- return isFixedAvatarEmployee(existing) && body.agents === undefined ? existing.agents : parseEmployeeAgents(body.agents);
104
+ return body.agents === undefined ? existing.agents : parseEmployeeAgents(body.agents);
94
105
  }
95
106
  /** Partial edits preserve omitted fields and the raw saved prompt, not a generated read projection. */
96
107
  export function parseFixedAvatarEmployeeInput(value, existing, now) {
@@ -103,7 +114,7 @@ export function parseFixedAvatarEmployeeInput(value, existing, now) {
103
114
  throw badRequest("员工职责不能超过 2000 个字符。");
104
115
  if (prompt.length > 20_000)
105
116
  throw badRequest("员工设定 Prompt 不能超过 20000 个字符。");
106
- return { ...existing, name, duty, prompt, agents, updatedAt: now };
117
+ return { ...existing, name, duty, prompt, agents, systemSessionLimit: parseSystemSessionLimit(body.systemSessionLimit, existing), updatedAt: now };
107
118
  }
108
119
  /** One validation owner for every employee candidate list, including legacy single agents. */
109
120
  function parseEmployeeAgents(value) {
@@ -148,6 +159,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
148
159
  avatar,
149
160
  agents,
150
161
  tags,
162
+ systemSessionLimit: parseSystemSessionLimit(body.systemSessionLimit, existing),
151
163
  archivedAt: existing?.archivedAt,
152
164
  createdAt: existing ? existing.createdAt : now,
153
165
  updatedAt: now,
@@ -337,9 +349,10 @@ export function registerSiliconEmployeeRoutes(app, deps) {
337
349
  const employee = isFixedAvatarEmployee(definition)
338
350
  ? parseFixedAvatarEmployeeInput(input, definition, now)
339
351
  : isBuiltinSiliconEmployee(existing)
340
- ? { ...existing, agents: parseSystemEmployeeAgents(input, existing), updatedAt: now }
341
- : parseSiliconEmployeeInput(input, existing, now);
352
+ ? { ...existing, agents: parseSystemEmployeeAgents(input, existing), systemSessionLimit: parseSystemSessionLimit(input.systemSessionLimit, existing), updatedAt: now }
353
+ : parseSiliconEmployeeInput({ ...definition, ...input }, definition, now);
342
354
  storage.saveSiliconEmployee(employee);
355
+ systemAiSessions(storage).enforceRetention(employee.id);
343
356
  notifyEmployee(employee.id);
344
357
  res.json(storage.getSiliconEmployee(employee.id));
345
358
  }
@@ -1,10 +1,9 @@
1
1
  import { DecisionError } from "./decision-types.js";
2
- import { SpeechError } from "./speech-service.js";
3
2
  /** Behind generic /api authentication; inference-only capabilities cannot install dependencies. */
4
3
  export function registerLocalModelRoutes(app, deps) {
5
4
  app.use("/api/local-models", deps.requireSessions, (_req, res, next) => { res.set("Cache-Control", "no-store"); next(); });
6
5
  const kind = (req, res, next) => {
7
- if (req.params.kind !== "laya" && req.params.kind !== "speech") {
6
+ if (req.params.kind !== "laya") {
8
7
  res.status(404).json({ error: "未知本地模型。" });
9
8
  return;
10
9
  }
@@ -51,20 +50,8 @@ export function registerLocalModelRoutes(app, deps) {
51
50
  next(error);
52
51
  }
53
52
  });
54
- app.patch("/api/local-models/speech/settings", deps.requireAdmin, async (req, res, next) => {
55
- try {
56
- if (!req.body || Array.isArray(req.body) || Object.keys(req.body).length !== 1 || typeof req.body.enabled !== "boolean") {
57
- throw new DecisionError("INVALID_REQUEST", "仅允许修改语音启用状态。");
58
- }
59
- await deps.models.setSpeechEnabled(req.body.enabled);
60
- res.status(202).json(await deps.models.status());
61
- }
62
- catch (error) {
63
- next(error);
64
- }
65
- });
66
53
  const errors = (error, _req, res, _next) => {
67
- if (error instanceof DecisionError || error instanceof SpeechError) {
54
+ if (error instanceof DecisionError) {
68
55
  res.status(error.status).json({ error: error.message, code: error.code });
69
56
  return;
70
57
  }
@@ -68,11 +68,11 @@ export function registerSessionInputRoutes(app, ctx) {
68
68
  return;
69
69
  }
70
70
  try {
71
- const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)));
71
+ const snapshot = structured.reorderQueuedMessages(req.params.id, rawOrder.map((v) => Number(v)), req.body?.expectedQueue);
72
72
  res.json(sessionResponseDTO(snapshot, req));
73
73
  }
74
74
  catch (error) {
75
- sendRouteError(res, surfaceDomainError(error, /^(排序长度与当前队列不一致,请刷新后重试。|排序参数无效。)$/) ?? error, "无法调整排队顺序。");
75
+ sendRouteError(res, surfaceDomainError(error, /^(排序长度与当前队列不一致,请刷新后重试。|排序参数无效。|排队消息已变化,请刷新后重试。)$/) ?? error, "无法调整排队顺序。");
76
76
  }
77
77
  });
78
78
  app.patch("/api/structured-sessions/:id/queued/:index", (req, res) => {
@@ -126,7 +126,7 @@ export function registerSessionInputRoutes(app, ctx) {
126
126
  }));
127
127
  app.delete("/api/structured-sessions/:id/queued", (req, res) => {
128
128
  try {
129
- const snapshot = structured.clearQueuedMessages(req.params.id);
129
+ const snapshot = structured.clearQueuedMessages(req.params.id, req.body?.expectedQueue);
130
130
  res.json(sessionResponseDTO(snapshot, req));
131
131
  }
132
132
  catch (error) {
@@ -4,6 +4,12 @@ import { defaultRoleForCli } from "./default-employee.js";
4
4
  import { fallbackProviderModel, resolveExplicitOrFallbackModel, sessionEmployeeFields } from "./session-model.js";
5
5
  import { resolveSessionCwd } from "./session-cwd.js";
6
6
  import { asyncRoute } from "./express-async.js";
7
+ import { principalHasScope } from "./auth.js";
8
+ import { inferProviderFromCommand, providerCliCommand } from "./session-provider.js";
9
+ import { recordRecentPath } from "./recent-paths.js";
10
+ import { toSessionDetailDTO } from "./session-transport.js";
11
+ import { compactToolMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
12
+ import { enrichStructuredMessages } from "./structured-client-protocol.js";
7
13
  import { installedPiExtensions } from "./pi-extension-resources.js";
8
14
  import { discoverPiResources, resolvePiResourceSelection } from "./pi-resource-catalog.js";
9
15
  import { effectivePiSessionSettings, patchPiSessionSettings } from "./pi-session-settings.js";
@@ -18,9 +24,86 @@ import { getInputDebugMeta, startResumedPtySession, restoreArchivedSession } fro
18
24
  /** Session creation, metadata/provider/model settings, resume, archive and deletion routes. */
19
25
  export function registerSessionLifecycleRoutes(app, ctx) {
20
26
  const { processes, structured, storage, defaultMode, config, sessions, onSessionCreated, decisions, wantsCompactTools, sessionResponseDTO, piSettingsResponse, requirePiSession, } = ctx;
21
- app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
27
+ app.post("/api/sessions", asyncRoute(async (req, res) => {
22
28
  const body = req.body;
23
29
  try {
30
+ if (body.kind !== "structured" && body.kind !== "pty")
31
+ throw badRequest("kind 必须是 structured 或 pty。");
32
+ if (body.prompt !== undefined && typeof body.prompt !== "string")
33
+ throw badRequest("prompt 必须是字符串。");
34
+ if (body.initialInput !== undefined && typeof body.initialInput !== "string")
35
+ throw badRequest("initialInput 必须是字符串。");
36
+ const prompt = body.prompt?.trim();
37
+ if (body.kind === "pty") {
38
+ if (prompt !== undefined)
39
+ throw badRequest("PTY 会话不接受结构化 prompt,请使用 initialInput。");
40
+ if ((body.engine !== undefined && body.engine !== "cli") || body.runner !== undefined
41
+ || body.employeeId !== undefined || body.teamId !== undefined
42
+ || (body.subject !== undefined && body.subject?.type !== "cli")) {
43
+ throw badRequest("PTY 不支持 SDK engine、runner 或员工/团队身份。");
44
+ }
45
+ if (body.provider !== undefined && !isSessionProvider(body.provider))
46
+ throw badRequest("请选择有效的执行工具。");
47
+ const interactiveShell = body.shell === true;
48
+ if (interactiveShell && (body.command !== undefined || body.provider !== undefined)) {
49
+ throw badRequest("shell:true 不能同时指定 command 或 provider。");
50
+ }
51
+ if (!req.principal || !principalHasScope(req.principal, "admin")) {
52
+ if (!interactiveShell) {
53
+ const rawCommand = typeof body.command === "string" ? body.command.trim() : "";
54
+ const provider = isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand);
55
+ const isProviderCli = !!provider && (!rawCommand || rawCommand === provider || rawCommand === providerCliCommand(provider));
56
+ if (!isProviderCli) {
57
+ res.status(403).json({ error: "当前连接只能启动已配置的 CLI 工具,不能执行自定义命令。", code: "forbidden" });
58
+ return;
59
+ }
60
+ }
61
+ }
62
+ if (!interactiveShell && !body.command?.trim())
63
+ throw badRequest("请输入要执行的命令。");
64
+ if (body.mode !== undefined && !isExecutionMode(body.mode))
65
+ throw badRequest(`无效执行模式: ${String(body.mode)}`);
66
+ const initialInput = body.initialInput?.trim();
67
+ const origin = parseSessionCreationOrigin(body);
68
+ const rawModel = typeof body.model === "string" ? body.model.trim() : "";
69
+ const rawCommand = body.command?.trim() ?? "";
70
+ const provider = interactiveShell
71
+ ? undefined
72
+ : (isSessionProvider(body.provider) ? body.provider : inferProviderFromCommand(rawCommand) ?? "claude");
73
+ const command = provider === "qoder" && rawCommand === "qoder" ? "qodercli" : rawCommand;
74
+ const effectiveModel = provider ? rawModel || fallbackProviderModel(config, provider) : undefined;
75
+ const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
76
+ const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
77
+ const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
78
+ const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
79
+ const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
80
+ && (isSessionProvider(body.provider) || inferProviderFromCommand(command))
81
+ ? defaultRoleForCli(storage, provider) : null;
82
+ const snapshot = await (interactiveShell
83
+ ? processes.startShell(sessionCwd, body.mode ?? "default", {
84
+ worktreeEnabled: body.worktreeEnabled === true, cols: reqCols, rows: reqRows, workspaceId,
85
+ workspaceTaskId: body.workspaceTaskId, ...origin,
86
+ })
87
+ : processes.start(command, sessionCwd, body.mode ?? config.defaultMode, initialInput || undefined, {
88
+ worktreeEnabled: body.worktreeEnabled === true, provider, model: effectiveModel, cols: reqCols, rows: reqRows,
89
+ thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
90
+ systemPrompt: body.systemPrompt?.trim() || role?.prompt, ...sessionEmployeeFields(role),
91
+ workspaceId, workspaceTaskId: body.workspaceTaskId, ...origin,
92
+ }));
93
+ recordRecentPath(storage, snapshot.cwd);
94
+ const messages = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
95
+ const windowed = windowMessagesForTransport(wantsCompactTools(req) ? compactToolMessagesForTransport(messages) : messages, config.cardDefaults ?? {});
96
+ res.status(201).json(toSessionDetailDTO(snapshot, {
97
+ messages: windowed.messages, messageOffset: windowed.messageOffset, messageTotal: windowed.messageTotal,
98
+ }));
99
+ return;
100
+ }
101
+ if (body.shell !== undefined || body.command !== undefined || body.initialInput !== undefined
102
+ || body.kind !== "structured") {
103
+ throw badRequest("结构化会话不接受 PTY 参数。");
104
+ }
105
+ if (body.engine !== undefined && body.engine !== "cli" && body.engine !== "sdk")
106
+ throw badRequest("请选择有效的执行引擎。");
24
107
  if (body.teamId !== undefined || body.subject?.type === "team") {
25
108
  throw badRequest("AI 团队请通过团队开工入口创建群聊。");
26
109
  }
@@ -30,7 +113,7 @@ export function registerSessionLifecycleRoutes(app, ctx) {
30
113
  if (employeeId === null || (body.employeeId !== undefined && !employeeId)) {
31
114
  throw badRequest("employeeId 必须是员工 ID。");
32
115
  }
33
- if (employeeId && (body.kind === "pty" || body.runner === "pty")) {
116
+ if (employeeId && body.runner === "pty") {
34
117
  throw badRequest("硅基员工只支持结构化会话。");
35
118
  }
36
119
  const employee = employeeId ? storage.getSiliconEmployee(employeeId) : null;
@@ -103,30 +186,23 @@ export function registerSessionLifecycleRoutes(app, ctx) {
103
186
  ...origin,
104
187
  });
105
188
  onSessionCreated?.(snapshot.cwd);
106
- const prompt = body.prompt?.trim();
107
189
  if (prompt) {
108
- if (body.respondImmediately === true) {
109
- // 只创建、不等首轮:整轮可能是几分钟,调用方(新建任务对话框、worktree 合并)
110
- // 等在 HTTP 上会把用户锁在「正在创建…」且关闭按钮被禁用。与
111
- // /api/sessions/:id/input 的 respondImmediately、/api/wand-tasks/:id/dispatch
112
- // 一致:先回会话快照,首轮进度继续走结构化事件。
113
- structured.sendMessage(snapshot.id, prompt).catch((error) => {
190
+ try {
191
+ Promise.resolve(structured.sendMessage(snapshot.id, prompt)).catch((error) => {
114
192
  console.error("[wand] Accepted structured prompt later failed", {
115
193
  sessionId: snapshot.id,
116
194
  error: getInputDebugMeta(error),
117
195
  });
118
196
  });
119
- const accepted = structured.get(snapshot.id);
120
- if (!accepted)
121
- throw badRequest("未找到该结构化会话。");
122
- res.status(201).json(sessionResponseDTO(accepted, req));
123
- return;
124
197
  }
125
- const finished = await structured.sendMessage(snapshot.id, prompt);
126
- res.status(201).json(sessionResponseDTO(finished, req));
127
- return;
198
+ catch (error) {
199
+ console.error("[wand] Accepted structured prompt later failed", {
200
+ sessionId: snapshot.id,
201
+ error: getInputDebugMeta(error),
202
+ });
203
+ }
128
204
  }
129
- res.status(201).json(sessionResponseDTO(snapshot, req));
205
+ res.status(201).json(sessionResponseDTO(structured.get(snapshot.id) ?? snapshot, req));
130
206
  }
131
207
  catch (error) {
132
208
  sendRouteError(res, surfaceDomainError(error, /Wand Agent|harness|core/i) ?? error, "无法启动结构化会话。");
@@ -1,5 +1,7 @@
1
1
  import { taskDispatchReceipt } from "./task-dispatch-receipt.js";
2
- import { dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
2
+ import { registerTaskCommentRoutes, submitTaskComment } from "./task-comment-dispatch.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { AgentDispatchPreflightError, dispatchAgentForTask, resolveTaskDispatchTarget } from "./agent-dispatch.js";
3
5
  import { memberAgents } from "./ai-team-types.js";
4
6
  import { defaultRoleForCli } from "./default-employee.js";
5
7
  import { asyncRoute } from "./express-async.js";
@@ -200,13 +202,7 @@ export function registerTaskRoutes(app, deps) {
200
202
  res.json(model.tasks.filter(task => workspaceId === undefined || task.workspaceId === workspaceId)
201
203
  .map(task => taskDto(deps, task, model)));
202
204
  });
203
- app.get("/api/wand-tasks/:id/comments", (req, res) => {
204
- if (!storage.getWandTask(req.params.id)) {
205
- res.status(404).json({ error: "未找到该任务。" });
206
- return;
207
- }
208
- res.set("Cache-Control", "no-store").json({ comments: storage.listWandTaskComments(req.params.id) });
209
- });
205
+ registerTaskCommentRoutes(app, deps);
210
206
  app.get("/api/wand-task-agent-defaults", (_req, res) => {
211
207
  res.json(readTaskBoardLastAgent(storage));
212
208
  });
@@ -644,7 +640,6 @@ export function registerTaskRoutes(app, deps) {
644
640
  throw badRequest("所选工具不支持该运行模式,请重新选择。");
645
641
  if (employee && body.agent !== undefined && !override)
646
642
  throw badRequest("请选择有效的本次工具配置。");
647
- const executionEmployee = employee && override ? { ...employee, agents: [override] } : employee;
648
643
  const agent = override ?? selected?.agent ?? (body.agent === undefined
649
644
  ? cliDefault ?? task.agent ?? fallbackAgent ?? parseWandTaskAgent(JSON.stringify(body))
650
645
  : parseTaskAgent(body.agent, task.agent?.mode, task.agent?.kind));
@@ -666,9 +661,19 @@ export function registerTaskRoutes(app, deps) {
666
661
  assertTaskWorkspaceMove(storage, task.id, workspaceId);
667
662
  task = storage.updateWandTask(task.id, { workspaceId }) ?? task;
668
663
  }
669
- const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}`,
670
- systemPrompt: employee?.prompt, employee: executionEmployee ?? undefined,
671
- employeeCandidateIndex: override ? 0 : selected?.index });
664
+ // Compatibility creation/drag clients also enter the same durable comment path for employees.
665
+ // No implicit description fallback: a first dispatch needs an explicit round instruction too.
666
+ if (employee) {
667
+ const result = await submitTaskComment(deps, task.id, { requestId: text(body.requestId) || randomUUID(), body: requestedPrompt, employeeId: employee.id }, override ?? undefined);
668
+ const session = result.comment.dispatch?.sessionId ? sessions?.getSlim(result.comment.dispatch.sessionId)
669
+ ?? storage.getSessionSlim(result.comment.dispatch.sessionId) : null;
670
+ if (session)
671
+ storage.updateWandTask(task.id, { agent, executionSubject: { type: "employee", id: employee.id, ...(override ? { agentOverride: override } : {}) }, status: task.status === "todo" ? "doing" : task.status });
672
+ res.status(202).json({ ok: true, taskId: task.id, subject: { type: "employee", id: employee.id }, comment: result.comment,
673
+ session: session ? { ...taskSessionSummary(session), employeeCandidateIndex: session.employeeCandidateIndex } : null });
674
+ return;
675
+ }
676
+ const { session, cwd, workspaceId } = await dispatchAgentForTask({ storage, config, structured: structured ?? null, processes: processes ?? null }, { task, agent, prompt, automationId: `wand-task:${task.id}` });
672
677
  task = storage.getWandTask(task.id);
673
678
  const usedSubject = subject?.type === "employee" ? { type: "employee", id: subject.id, ...(override ? { agentOverride: override } : {}) } : subject ?? { type: "cli", id: agent.provider };
674
679
  storage.updateWandTask(task.id, {
@@ -709,7 +714,7 @@ export function registerTaskRoutes(app, deps) {
709
714
  });
710
715
  }
711
716
  catch (error) {
712
- sendRouteError(res, surfaceDomainError(error, /^(任务所属项目已被删除,请重新指定。|任务容器不存在,请恢复任务后再派发。|绑定员工只支持结构化会话。|员工执行候选与启动快照不一致。|当前服务未启用结构化会话,无法派发 Agent。|当前服务未启用终端会话,无法派发 Agent。|LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。)$/) ?? error, "无法派发 Agent。");
717
+ sendRouteError(res, error instanceof AgentDispatchPreflightError ? badRequest(error.message) : surfaceDomainError(error, /^(任务所属项目已被删除,请重新指定。|任务容器不存在,请恢复任务后再派发。|绑定员工只支持结构化会话。|员工执行候选与启动快照不一致。|当前服务未启用结构化会话,无法派发 Agent。|当前服务未启用终端会话,无法派发 Agent。|LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。)$/) ?? error, "无法派发 Agent。");
713
718
  }
714
719
  }));
715
720
  app.get("/api/wand-tasks/:id", asyncRoute(async (req, res) => {