@co0ontty/wand 4.89.5 → 4.90.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 (197) hide show
  1. package/README.md +5 -3
  2. package/dist/agent-dispatch.js +4 -9
  3. package/dist/ai-team-employee-binding.js +10 -9
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +38 -2
  6. package/dist/ai-team-types.js +3 -2
  7. package/dist/ai-text-runner.d.ts +61 -0
  8. package/dist/ai-text-runner.js +653 -0
  9. package/dist/avatar-input.d.ts +2 -0
  10. package/dist/avatar-input.js +19 -0
  11. package/dist/bounded-process-output.d.ts +5 -0
  12. package/dist/bounded-process-output.js +9 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/command-error.d.ts +6 -0
  15. package/dist/command-error.js +8 -0
  16. package/dist/config.js +11 -42
  17. package/dist/conversation-service.d.ts +12 -2
  18. package/dist/conversation-service.js +104 -25
  19. package/dist/conversation-session-preview.d.ts +2 -0
  20. package/dist/conversation-session-preview.js +4 -0
  21. package/dist/decision-client.js +22 -6
  22. package/dist/decision-expert-employee.js +2 -2
  23. package/dist/decision-expert-identity.d.ts +1 -1
  24. package/dist/decision-expert-identity.js +1 -1
  25. package/dist/decision-expert-service.d.ts +1 -0
  26. package/dist/decision-expert-service.js +82 -76
  27. package/dist/decision-hardware.js +4 -3
  28. package/dist/decision-types.d.ts +4 -0
  29. package/dist/decision-types.js +4 -0
  30. package/dist/deployment-lock.d.ts +13 -0
  31. package/dist/deployment-lock.js +189 -0
  32. package/dist/employee-text.js +1 -1
  33. package/dist/fixed-employee-avatar.d.ts +7 -0
  34. package/dist/fixed-employee-avatar.js +13 -0
  35. package/dist/git-quick-commit.d.ts +5 -40
  36. package/dist/git-quick-commit.js +8 -636
  37. package/dist/hr-employee-identity.d.ts +5 -0
  38. package/dist/hr-employee-identity.js +5 -0
  39. package/dist/hr-employee.d.ts +5 -0
  40. package/dist/hr-employee.js +21 -0
  41. package/dist/middleware/path-safety.js +1 -1
  42. package/dist/milestone-scope.js +1 -1
  43. package/dist/model-auto-assign.d.ts +1 -1
  44. package/dist/model-auto-assign.js +1 -1
  45. package/dist/models.d.ts +4 -0
  46. package/dist/models.js +21 -1
  47. package/dist/npm-update-utils.js +5 -44
  48. package/dist/pi-auto-resources.js +6 -3
  49. package/dist/plush-avatar.d.ts +10 -1
  50. package/dist/plush-avatar.js +13 -1
  51. package/dist/plush-cat-appearance.d.ts +97 -0
  52. package/dist/plush-cat-appearance.js +118 -0
  53. package/dist/plush-cat-whiskers.d.ts +6 -0
  54. package/dist/plush-cat-whiskers.js +42 -0
  55. package/dist/process-manager.d.ts +22 -21
  56. package/dist/process-manager.js +49 -83
  57. package/dist/prompt-optimizer.d.ts +1 -1
  58. package/dist/prompt-optimizer.js +1 -1
  59. package/dist/render-host.js +1 -19
  60. package/dist/scheduled-task-service.d.ts +41 -0
  61. package/dist/scheduled-task-service.js +294 -0
  62. package/dist/scheduled-task-state.d.ts +11 -0
  63. package/dist/scheduled-task-state.js +65 -0
  64. package/dist/scheduled-task-time.d.ts +4 -0
  65. package/dist/scheduled-task-time.js +82 -0
  66. package/dist/scheduled-task-types.d.ts +71 -0
  67. package/dist/scheduled-task-types.js +1 -0
  68. package/dist/server-ai-team-routes.js +3 -17
  69. package/dist/server-employee-routes.d.ts +3 -0
  70. package/dist/server-employee-routes.js +20 -38
  71. package/dist/server-file-routes.js +44 -1
  72. package/dist/server-pi-recommendation-routes.js +13 -4
  73. package/dist/server-request.d.ts +2 -0
  74. package/dist/server-request.js +9 -0
  75. package/dist/server-resume-routes.js +1 -1
  76. package/dist/server-scheduled-task-routes.d.ts +4 -0
  77. package/dist/server-scheduled-task-routes.js +34 -0
  78. package/dist/server-session-routes.d.ts +5 -13
  79. package/dist/server-session-routes.js +35 -70
  80. package/dist/server-task-routes.d.ts +6 -21
  81. package/dist/server-task-routes.js +25 -120
  82. package/dist/server-team-dispatch-routes.js +12 -2
  83. package/dist/server-workspace-routes.d.ts +1 -1
  84. package/dist/server-workspace-routes.js +33 -78
  85. package/dist/server.js +55 -20
  86. package/dist/session-model.d.ts +15 -0
  87. package/dist/session-model.js +23 -0
  88. package/dist/session-registry.d.ts +5 -1
  89. package/dist/session-registry.js +20 -11
  90. package/dist/session-request.d.ts +9 -0
  91. package/dist/session-request.js +27 -0
  92. package/dist/session-topic.d.ts +1 -1
  93. package/dist/session-topic.js +1 -1
  94. package/dist/session-transport.d.ts +3 -1
  95. package/dist/session-transport.js +11 -5
  96. package/dist/silicon-employee-draft.d.ts +2 -2
  97. package/dist/silicon-employee-draft.js +3 -3
  98. package/dist/socket-probe.d.ts +2 -0
  99. package/dist/socket-probe.js +20 -0
  100. package/dist/storage-schedules.d.ts +20 -0
  101. package/dist/storage-schedules.js +78 -0
  102. package/dist/storage-schema.d.ts +3 -0
  103. package/dist/storage-schema.js +702 -0
  104. package/dist/storage.d.ts +42 -8
  105. package/dist/storage.js +182 -715
  106. package/dist/structured-claude-adapter.js +12 -0
  107. package/dist/structured-claude-protocol.d.ts +4 -0
  108. package/dist/structured-claude-protocol.js +18 -4
  109. package/dist/structured-codex-adapter.js +11 -0
  110. package/dist/structured-exec-pump.js +79 -38
  111. package/dist/structured-failure.d.ts +21 -0
  112. package/dist/structured-failure.js +14 -0
  113. package/dist/structured-gemini-adapter.js +12 -2
  114. package/dist/structured-grok-adapter.js +12 -2
  115. package/dist/structured-opencode-adapter.js +8 -1
  116. package/dist/structured-qoder-adapter.js +14 -0
  117. package/dist/structured-session-manager.d.ts +13 -18
  118. package/dist/structured-session-manager.js +147 -517
  119. package/dist/system-employee.d.ts +1 -1
  120. package/dist/system-employee.js +1 -1
  121. package/dist/task-archive-policy.js +1 -1
  122. package/dist/task-read-model.d.ts +27 -0
  123. package/dist/task-read-model.js +47 -0
  124. package/dist/task-retention.d.ts +1 -1
  125. package/dist/task-retention.js +1 -1
  126. package/dist/task-title-service.d.ts +30 -0
  127. package/dist/task-title-service.js +122 -0
  128. package/dist/task-title.d.ts +1 -1
  129. package/dist/task-title.js +1 -1
  130. package/dist/terminal-daemon-client.js +1 -18
  131. package/dist/user-memory.js +1 -1
  132. package/dist/web-ui/asset-runtime.d.ts +12 -0
  133. package/dist/web-ui/asset-runtime.js +6 -0
  134. package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
  135. package/dist/web-ui/composer-avatar-activity.js +29 -0
  136. package/dist/web-ui/content/ai-teams.js +13 -13
  137. package/dist/web-ui/content/module-manifest.json +1 -0
  138. package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
  139. package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
  140. package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
  141. package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
  142. package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
  143. package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
  144. package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
  145. package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
  146. package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
  147. package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
  148. package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
  149. package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
  150. package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
  151. package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
  152. package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
  153. package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
  154. package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
  155. package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
  156. package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
  157. package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
  158. package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
  159. package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
  160. package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
  161. package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
  162. package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
  163. package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
  164. package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
  165. package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
  166. package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
  167. package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
  168. package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
  169. package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
  170. package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
  171. package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
  172. package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
  173. package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
  174. package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
  175. package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
  176. package/dist/web-ui/content/plush-avatar.js +177 -177
  177. package/dist/web-ui/content/scripts.js +1 -994
  178. package/dist/web-ui/content/styles.css +1 -1
  179. package/dist/web-ui/content/tailwind.css +1 -1
  180. package/dist/web-ui/embedded-assets.d.ts +32 -0
  181. package/dist/web-ui/embedded-assets.js +10 -8
  182. package/dist/web-ui/markdown.d.ts +3 -2
  183. package/dist/web-ui/markdown.js +61 -48
  184. package/dist/web-ui/message-file-target.d.ts +13 -0
  185. package/dist/web-ui/message-file-target.js +85 -0
  186. package/dist/web-ui/provider-identity.d.ts +1 -1
  187. package/dist/web-ui/provider-identity.js +3 -4
  188. package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
  189. package/dist/web-ui/react/local-preview/controller.js +12 -0
  190. package/dist/web-ui/scripts.d.ts +10 -0
  191. package/dist/web-ui/scripts.js +42 -5
  192. package/package.json +6 -3
  193. package/browser-extension/icons/wand-128.png +0 -0
  194. package/browser-extension/icons/wand-16.png +0 -0
  195. package/browser-extension/icons/wand-32.png +0 -0
  196. package/browser-extension/icons/wand-48.png +0 -0
  197. package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
@@ -0,0 +1,5 @@
1
+ /** Diagnostic tails are not protocol payloads; only diagnostics may be truncated. */
2
+ export declare const MAX_DIAGNOSTIC_CHARS: number;
3
+ export declare const MAX_PROTOCOL_LINE_BYTES: number;
4
+ export declare const MAX_AI_TEXT_BYTES: number;
5
+ export declare function appendDiagnosticTail(previous: string, text: string): string;
@@ -0,0 +1,9 @@
1
+ /** Diagnostic tails are not protocol payloads; only diagnostics may be truncated. */
2
+ export const MAX_DIAGNOSTIC_CHARS = 64 * 1024;
3
+ export const MAX_PROTOCOL_LINE_BYTES = 8 * 1024 * 1024;
4
+ export const MAX_AI_TEXT_BYTES = 4 * 1024 * 1024;
5
+ export function appendDiagnosticTail(previous, text) {
6
+ const tail = (previous + text).slice(-MAX_DIAGNOSTIC_CHARS);
7
+ const first = tail.charCodeAt(0);
8
+ return first >= 0xdc00 && first <= 0xdfff ? tail.slice(1) : tail;
9
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "ec13fefa558fb2d1236d377d46e6c3152e4eb79b",
3
- "builtAt": "2026-10-10T07:01:42.094Z",
4
- "version": "4.89.5",
2
+ "commit": "c19f50516469789583ace432959dd7519a952f19",
3
+ "builtAt": "2026-10-11T15:04:29.304Z",
4
+ "version": "4.90.0",
5
5
  "channel": "stable"
6
6
  }
@@ -0,0 +1,6 @@
1
+ /** Shared command failure shape; public legacy names/codes stay compatible. */
2
+ export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
3
+ export declare class QuickCommitError extends Error {
4
+ readonly code: QuickCommitErrorCode;
5
+ constructor(message: string, code: QuickCommitErrorCode);
6
+ }
@@ -0,0 +1,8 @@
1
+ export class QuickCommitError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = "QuickCommitError";
7
+ }
8
+ }
package/dist/config.js CHANGED
@@ -61,9 +61,10 @@ export const PROVIDER_MODEL_PREFERENCE_FIELDS = {
61
61
  gemini: "defaultGeminiModel",
62
62
  };
63
63
  const PROVIDER_MODEL_FIELDS = Object.values(PROVIDER_MODEL_PREFERENCE_FIELDS);
64
- const PROVIDER_MODEL_FIELD_SET = new Set(PROVIDER_MODEL_FIELDS);
65
- function isProviderModelField(key) {
66
- return PROVIDER_MODEL_FIELD_SET.has(key);
64
+ const TRIMMED_STRING_FIELDS = [...PROVIDER_MODEL_FIELDS, "commitModel", "systemAiModel", "language"];
65
+ const TRIMMED_STRING_FIELD_SET = new Set(TRIMMED_STRING_FIELDS);
66
+ function isTrimmedStringField(key) {
67
+ return TRIMMED_STRING_FIELD_SET.has(key);
67
68
  }
68
69
  const PREFERENCE_KEY_SET = new Set(PREFERENCE_KEYS);
69
70
  function preferenceStorageKey(key) {
@@ -244,8 +245,8 @@ export async function loadConfigWithStorage(configPath, storage) {
244
245
  reconcileAppSecret(config, storage);
245
246
  // password: DB 优先映射到 runtime config,让 config:show 与 server.ts 都看到真值
246
247
  applyStoragePassword(config, storage);
247
- // 内置「系统运维」员工:Wand 自有 AI 调用(commit message / 标题 / 提示词优化 /
248
- // 员工起草)都由它执行。这里只做幂等补齐——首次按用户已有的系统 AI 工具与默认
248
+ // 内置「系统运维」员工负责 commit message / 标题 / 提示词优化;招聘由 HR 负责。
249
+ // 这里只做幂等补齐——首次按用户已有的系统 AI 工具与默认
249
250
  // provider 建首条候选,之后候选由用户在员工设置里维护,不再回写 config。
250
251
  storage.ensureSystemSiliconEmployee({
251
252
  cli: config.systemAiCli,
@@ -256,6 +257,7 @@ export async function loadConfigWithStorage(configPath, storage) {
256
257
  storage.ensureDefaultSiliconEmployee(config.defaultProvider);
257
258
  storage.ensureDecisionExpertEmployee();
258
259
  storage.ensureSpeechPolisherEmployee();
260
+ storage.ensureHrEmployee(storage.getSystemSiliconEmployee()?.agents);
259
261
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
260
262
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
261
263
  const hasLegacySecrets = "password" in rawInput || "appSecret" in rawInput;
@@ -372,7 +374,7 @@ export function applyStoragePreferences(config, storage) {
372
374
  if (typeof v === "string" && v.trim())
373
375
  config.defaultCwd = v;
374
376
  }
375
- for (const field of PROVIDER_MODEL_FIELDS) {
377
+ for (const field of TRIMMED_STRING_FIELDS) {
376
378
  const dbKey = preferenceStorageKey(field);
377
379
  if (!storage.hasPreference(dbKey))
378
380
  continue;
@@ -388,31 +390,16 @@ export function applyStoragePreferences(config, storage) {
388
390
  if (v === "claude" || v === "codex" || v === "opencode")
389
391
  config.commitCli = v;
390
392
  }
391
- if (storage.hasPreference(preferenceStorageKey("commitModel"))) {
392
- const v = storage.getPreference(preferenceStorageKey("commitModel"), defaults.commitModel ?? "");
393
- if (typeof v === "string")
394
- config.commitModel = v.trim();
395
- }
396
393
  if (storage.hasPreference(preferenceStorageKey("systemAiCli"))) {
397
394
  const v = storage.getPreference(preferenceStorageKey("systemAiCli"), defaults.systemAiCli);
398
395
  if (isSessionProvider(v))
399
396
  config.systemAiCli = v;
400
397
  }
401
- if (storage.hasPreference(preferenceStorageKey("systemAiModel"))) {
402
- const v = storage.getPreference(preferenceStorageKey("systemAiModel"), defaults.systemAiModel);
403
- if (typeof v === "string")
404
- config.systemAiModel = v.trim();
405
- }
406
398
  if (storage.hasPreference(preferenceStorageKey("defaultThinkingEffort"))) {
407
399
  const v = storage.getPreference(preferenceStorageKey("defaultThinkingEffort"), defaults.defaultThinkingEffort ?? "off");
408
400
  if (isThinkingEffort(v))
409
401
  config.defaultThinkingEffort = v;
410
402
  }
411
- if (storage.hasPreference(preferenceStorageKey("language"))) {
412
- const v = storage.getPreference(preferenceStorageKey("language"), defaults.language ?? "");
413
- if (typeof v === "string")
414
- config.language = v.trim();
415
- }
416
403
  if (storage.hasPreference(preferenceStorageKey("cardDefaults"))) {
417
404
  const v = storage.getPreference(preferenceStorageKey("cardDefaults"), defaults.cardDefaults);
418
405
  config.cardDefaults = normalizeCardDefaults(v);
@@ -440,9 +427,9 @@ export function applyStoragePreferences(config, storage) {
440
427
  /** Write a single preference value to DB and (in-place) update the live config object. */
441
428
  export function writePreferenceToStorage(config, storage, key, value) {
442
429
  const dbKey = preferenceStorageKey(key);
443
- // Default model fields share one shape (trimmed string) across every provider;
444
- // handling them before the switch keeps the switch free of seven copies.
445
- if (isProviderModelField(key)) {
430
+ // Identical string policies share one typed field set for both read and write.
431
+ // Strict enums/booleans and legacy read tolerance deliberately remain distinct.
432
+ if (isTrimmedStringField(key)) {
446
433
  const v = typeof value === "string" ? value.trim() : "";
447
434
  storage.setPreference(dbKey, v);
448
435
  config[key] = v;
@@ -509,12 +496,6 @@ export function writePreferenceToStorage(config, storage, key, value) {
509
496
  config.commitCli = value;
510
497
  break;
511
498
  }
512
- case "commitModel": {
513
- const v = typeof value === "string" ? value.trim() : "";
514
- storage.setPreference(dbKey, v);
515
- config.commitModel = v;
516
- break;
517
- }
518
499
  case "systemAiCli": {
519
500
  if (!isSessionProvider(value))
520
501
  throw new Error(`无效系统 AI CLI: ${String(value)}`);
@@ -522,12 +503,6 @@ export function writePreferenceToStorage(config, storage, key, value) {
522
503
  config.systemAiCli = value;
523
504
  break;
524
505
  }
525
- case "systemAiModel": {
526
- const v = typeof value === "string" ? value.trim() : "";
527
- storage.setPreference(dbKey, v);
528
- config.systemAiModel = v;
529
- break;
530
- }
531
506
  case "defaultThinkingEffort": {
532
507
  if (!isThinkingEffort(value))
533
508
  throw new Error(`无效思考深度: ${String(value)}`);
@@ -536,12 +511,6 @@ export function writePreferenceToStorage(config, storage, key, value) {
536
511
  config.defaultThinkingEffort = v;
537
512
  break;
538
513
  }
539
- case "language": {
540
- const v = typeof value === "string" ? value.trim() : "";
541
- storage.setPreference(dbKey, v);
542
- config.language = v;
543
- break;
544
- }
545
514
  case "cardDefaults": {
546
515
  const normalized = normalizeCardDefaults(value);
547
516
  storage.setPreference(dbKey, normalized);
@@ -20,6 +20,12 @@ interface ConversationDispatchInput {
20
20
  memberVersion?: number;
21
21
  continueTaskId?: string;
22
22
  }
23
+ /** Internal execution context; HTTP conversation input does not expose this override. */
24
+ interface ConversationExecutionTarget {
25
+ workspaceId?: string;
26
+ title?: string;
27
+ scheduled?: true;
28
+ }
23
29
  /** Business coordination only; all execution remains in StructuredSessionManager / AiTeamRunner. */
24
30
  export declare class ConversationService {
25
31
  private readonly deps;
@@ -49,7 +55,11 @@ export declare class ConversationService {
49
55
  private selfName;
50
56
  private memberReasons;
51
57
  private turns;
52
- summary(instance: ConversationInstance): ConversationSummary;
58
+ /** Same ordering as turns(), so a source tail is enough when timestamps are append-only. */
59
+ private laterTurn;
60
+ private tailTurn;
61
+ private previewTurn;
62
+ summary(instance: ConversationInstance, knownTurns?: ConversationTurn[]): ConversationSummary;
53
63
  list(): ConversationSummary[];
54
64
  updateListState(id: string, patch: {
55
65
  pinned?: boolean;
@@ -75,7 +85,7 @@ export declare class ConversationService {
75
85
  /** Explicit resource action may prepare a channel; it never sends or starts a model. */
76
86
  prepare(id: string, requestId: string, explicitCwd?: string): Promise<ConversationReceipt>;
77
87
  private communicationChannel;
78
- send(id: string, requestId: string, input: string, target: ConversationTarget, explicitCwd?: string, echo?: boolean): Promise<ConversationReceipt>;
88
+ send(id: string, requestId: string, input: string, target: ConversationTarget, explicitCwd?: string, echo?: boolean, executionTarget?: ConversationExecutionTarget): Promise<ConversationReceipt>;
79
89
  /** One IM message is one fresh model context. No task, team, relay or previous DM history. */
80
90
  private startMessageSession;
81
91
  private requireTarget;
@@ -1,16 +1,16 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { statSync, rmSync, existsSync, realpathSync } from "node:fs";
3
- import { resolve, sep } from "node:path";
3
+ import { isAbsolute, resolve, sep } from "node:path";
4
4
  import { AI_TEAM_ACTIVE_RUN_STATUSES, AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_MAX_MEMBERS, memberAgents } from "./ai-team-types.js";
5
5
  import { freezeTeamEmployees, projectTeamEmployees, requireTeamEmployee } from "./ai-team-employee-binding.js";
6
6
  import { AiTeamConflictError } from "./ai-team-runner.js";
7
7
  import { CONVERSATION_OWNER, CONVERSATION_RELAY_PREFIX, employeeConversationId } from "./conversation-types.js";
8
8
  import { conversationLeaderMention, conversationMessageBody } from "./conversation-mentions.js";
9
- import { getDefaultModelForProvider } from "./config.js";
10
- import { defaultModelGroupSelector } from "./model-groups.js";
9
+ import { compactTransportAvatar } from "./fixed-employee-avatar.js";
10
+ import { fallbackProviderModel } from "./session-model.js";
11
11
  import { getErrorMessage } from "./error-utils.js";
12
12
  import { resolveSessionCwd } from "./session-cwd.js";
13
- import { resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
13
+ import { isGlobalWorkspace, normalizeProjectCwd, resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
14
14
  import { CONVERSATION_SESSION_PREFIX, conversationSessionPreview } from "./conversation-session-preview.js";
15
15
  import { conversationTaskLiveText } from "./conversation-task-preview.js";
16
16
  import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
@@ -162,7 +162,7 @@ export class ConversationService {
162
162
  ...(turn.role === "assistant" && !turn.author ? { author: {
163
163
  id: channel?.employeeId ?? instance.team?.members.find((m) => m.isLeader)?.id ?? "leader",
164
164
  name: channel?.employeeName ?? instance.team?.members.find((m) => m.isLeader)?.name ?? instance.name,
165
- avatar: channel?.employeeAvatar, sessionId: id,
165
+ avatar: compactTransportAvatar(channel?.employeeAvatar), sessionId: id,
166
166
  leader: instance.kind === "group" || undefined,
167
167
  } } : {}),
168
168
  }));
@@ -174,7 +174,62 @@ export class ConversationService {
174
174
  return [...base, ...this.storage.conversationEvents(instance.id), ...communication].map(withSelf)
175
175
  .sort((a, b) => (a.createdAt ?? "").localeCompare(b.createdAt ?? "") || (instance.kind === "dm" ? 0 : (a.role === "user" ? -1 : 0) - (b.role === "user" ? -1 : 0)));
176
176
  }
177
- summary(instance) {
177
+ /** Same ordering as turns(), so a source tail is enough when timestamps are append-only. */
178
+ laterTurn(instance, left, right) {
179
+ const time = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
180
+ if (time)
181
+ return time > 0 ? left : right;
182
+ if (instance.kind === "dm" || left.role === right.role)
183
+ return right;
184
+ return left.role === "user" ? right : left;
185
+ }
186
+ tailTurn(id, role) {
187
+ const live = this.deps.structured.get(id);
188
+ if (live) {
189
+ const messages = live.messages ?? [];
190
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
191
+ const turn = messages[index];
192
+ if (turn && (!role || turn.role === role))
193
+ return turn;
194
+ }
195
+ return undefined;
196
+ }
197
+ return this.storage.lastSessionTurn(id, role) ?? undefined;
198
+ }
199
+ previewTurn(instance) {
200
+ const candidates = [];
201
+ if (instance.kind !== "dm" && instance.sessionId) {
202
+ const relay = this.tailTurn(instance.sessionId);
203
+ if (relay)
204
+ candidates.push({ ...relay, messageId: relay.messageId ?? `${instance.sessionId}:0`, conversationId: instance.id });
205
+ }
206
+ const event = this.storage.lastConversationEvent(instance.id);
207
+ if (event)
208
+ candidates.push(event);
209
+ const channelIds = [...(instance.historySessionIds ?? []), instance.communicationSessionId]
210
+ .filter((id) => !!id && (instance.kind === "dm" || id !== instance.sessionId));
211
+ for (const id of new Set(channelIds)) {
212
+ const turn = this.tailTurn(id, "assistant");
213
+ if (!turn)
214
+ continue;
215
+ const channel = this.deps.structured.get(id) ?? this.storage.getSessionSlim(id);
216
+ candidates.push({
217
+ ...turn,
218
+ messageId: turn.messageId ?? `${id}:preview`,
219
+ conversationId: instance.id,
220
+ conversationTarget: null,
221
+ ...(turn.author ? {} : { author: {
222
+ id: channel?.employeeId ?? instance.team?.members.find((member) => member.isLeader)?.id ?? "leader",
223
+ name: channel?.employeeName ?? instance.team?.members.find((member) => member.isLeader)?.name ?? instance.name,
224
+ avatar: compactTransportAvatar(channel?.employeeAvatar),
225
+ sessionId: id,
226
+ leader: instance.kind === "group" || undefined,
227
+ } }),
228
+ });
229
+ }
230
+ return candidates.reduce((latest, turn) => latest ? this.laterTurn(instance, latest, turn) : turn, undefined);
231
+ }
232
+ summary(instance, knownTurns) {
178
233
  const tasks = this.storage.conversationTasks(instance.id).flatMap((link) => {
179
234
  const task = this.storage.getWandTask(link.taskId);
180
235
  const receipt = task ? this.storage.conversationTaskStartup(task.id) : null;
@@ -193,16 +248,21 @@ export class ConversationService {
193
248
  unavailableReason = getErrorMessage(error);
194
249
  }
195
250
  }
196
- const messages = this.turns(instance);
197
- const last = messages.at(-1);
251
+ const last = knownTurns ? knownTurns.at(-1) : this.previewTurn(instance);
198
252
  const lastText = last?.sessionLink ? conversationSessionPreview(this.snapshot(last.sessionLink.sessionId), last.requestId ? this.receipt(last.requestId) ?? undefined : undefined).text
199
253
  : last?.content.map((b) => b.type === "text" ? b.text : "").join(" ") ?? "";
200
254
  const messageAt = [instance.updatedAt, last?.createdAt ?? "", ...tasks.flatMap((t) => t.runs.map((r) => r.updatedAt))].sort().at(-1) ?? "";
201
255
  const title = instance.kind === "dm" ? employee?.name ?? instance.name
202
256
  : instance.nameSource === "custom" ? instance.name : tasks.at(-1)?.task.title ?? instance.name;
203
- return { ...instance, ...this.storage.conversationListState(instance.id), team: instance.team ? projectTeamEmployees(instance.team, this.storage) : null,
257
+ // 私聊标题已说明对方身份;只保留自己的署名,群聊仍需区分发言人。
258
+ const previewAuthor = last?.role === "user" ? `${this.selfName()}:`
259
+ : instance.kind === "group" && last ? `${last.author?.name ?? "系统"}:` : "";
260
+ const team = instance.team ? projectTeamEmployees(instance.team, this.storage) : null;
261
+ return { ...instance, ...lifecycle, team: team ? { ...team, members: team.members.map((member) => ({
262
+ ...member, avatar: compactTransportAvatar(member.avatar) ?? "",
263
+ })) } : null,
204
264
  title, unavailableReason, memberUnavailableReasons: this.memberReasons(instance), tasks,
205
- preview: last ? `${last.role === "user" ? this.selfName() : last.author?.name ?? "系统"}:${lastText.slice(0, 160)}`
265
+ preview: last ? `${previewAuthor}${lastText.slice(0, 160)}`
206
266
  : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
207
267
  }
208
268
  list() {
@@ -295,8 +355,9 @@ export class ConversationService {
295
355
  history(id) { return this.turns(this.requireInstance(id)); }
296
356
  detail(id) {
297
357
  const instance = this.requireInstance(id);
298
- const summary = this.summary(instance);
299
- return { ...summary, messages: this.turns(instance).map(turn => turn.sessionLink
358
+ const turns = this.turns(instance);
359
+ const summary = this.summary(instance, turns);
360
+ return { ...summary, messages: turns.map(turn => turn.sessionLink
300
361
  ? { ...turn, sessionPreview: conversationSessionPreview(this.snapshot(turn.sessionLink.sessionId), turn.requestId ? this.receipt(turn.requestId) ?? undefined : undefined),
301
362
  // Older clients can still open the exact source session without understanding sessionLink.
302
363
  author: turn.author ? { ...turn.author, sessionId: turn.sessionLink.sessionId } : turn.author,
@@ -514,9 +575,7 @@ export class ConversationService {
514
575
  if (!session) {
515
576
  if (instance.communicationSessionId)
516
577
  instance.historySessionIds = [...(instance.historySessionIds ?? []), instance.communicationSessionId];
517
- const model = agent.model === "default"
518
- ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
519
- || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
578
+ const model = agent.model === "default" ? fallbackProviderModel(this.deps.config, agent.provider) : agent.model;
520
579
  session = this.deps.structured.createSession({ cwd, provider: agent.provider, mode: agent.mode, model,
521
580
  thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: "interactive",
522
581
  automationId: `conversation-talk:${instance.id}`, employeeId: employee?.id,
@@ -533,10 +592,31 @@ export class ConversationService {
533
592
  this.storage.saveConversation(instance);
534
593
  return session;
535
594
  }
536
- send(id, requestId, input, target, explicitCwd, echo = true) {
537
- return this.request(requestId, "send", { input, target, explicitCwd, echo }, id, async (receipt, commit) => {
595
+ send(id, requestId, input, target, explicitCwd, echo = true, executionTarget) {
596
+ return this.request(requestId, "send", { input, target, explicitCwd, echo,
597
+ ...(executionTarget?.workspaceId ? { workspaceId: executionTarget.workspaceId } : {}),
598
+ ...(executionTarget?.title !== undefined ? { title: executionTarget.title } : {}),
599
+ ...(executionTarget?.scheduled ? { scheduled: true } : {}) }, id, async (receipt, commit) => {
538
600
  const text = shortText(input, "消息", 256_000, true);
539
601
  const instance = this.requireInstance(id);
602
+ let workspaceId;
603
+ if (executionTarget) {
604
+ if (instance.kind !== "dm" || target)
605
+ throw new Error("指定工作项目仅用于员工执行。");
606
+ }
607
+ if (executionTarget?.workspaceId !== undefined) {
608
+ workspaceId = shortText(executionTarget.workspaceId, "执行项目", 160, true);
609
+ const workspace = this.storage.getWorkspace(workspaceId);
610
+ if (!workspace || isGlobalWorkspace(workspace))
611
+ throw new Error("执行项目不存在或不可用,请重新选择项目。");
612
+ if (!explicitCwd?.trim() || !workspace.cwd.trim() || !isAbsolute(workspace.cwd.trim())) {
613
+ throw new Error("指定工作项目需要有效的绝对执行目录。");
614
+ }
615
+ const cwd = this.cwd(explicitCwd);
616
+ const workspaceCwd = resolveSessionCwd(workspace.cwd, undefined);
617
+ if (normalizeProjectCwd(cwd) !== normalizeProjectCwd(workspaceCwd))
618
+ throw new Error("执行项目目录已变更,请核对执行位置。");
619
+ }
540
620
  if (target) {
541
621
  const run = this.requireTarget(instance, target);
542
622
  if (!AI_TEAM_ACTIVE_RUN_STATUSES.includes(run.status))
@@ -555,7 +635,7 @@ export class ConversationService {
555
635
  return;
556
636
  }
557
637
  if (instance.kind === "dm") {
558
- this.startMessageSession(instance, requestId, text, explicitCwd, receipt, commit);
638
+ this.startMessageSession(instance, requestId, text, explicitCwd, receipt, commit, executionTarget ? { ...executionTarget, workspaceId } : undefined);
559
639
  return;
560
640
  }
561
641
  if (instance.kind === "group" && instance.team) {
@@ -599,21 +679,20 @@ export class ConversationService {
599
679
  });
600
680
  }
601
681
  /** One IM message is one fresh model context. No task, team, relay or previous DM history. */
602
- startMessageSession(instance, requestId, input, explicitCwd, receipt, commit) {
682
+ startMessageSession(instance, requestId, input, explicitCwd, receipt, commit, executionTarget) {
603
683
  requireTeamEmployee(this.storage, instance.peerEmployeeId ?? "");
604
684
  const employee = this.storage.getSiliconEmployee(instance.peerEmployeeId);
605
685
  const selected = selectEmployeeCandidate(employee), agent = selected.agent;
606
686
  const cwd = this.cwd(explicitCwd);
607
- const model = agent.model === "default"
608
- ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
609
- || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
610
- const title = provisionalTaskTitleFromDescription(conversationMessageBody(input)) || "新会话";
687
+ const model = agent.model === "default" ? fallbackProviderModel(this.deps.config, agent.provider) : agent.model;
688
+ const title = shortText(executionTarget?.title, "会话标题", 200)
689
+ || provisionalTaskTitleFromDescription(conversationMessageBody(input)) || "新会话";
611
690
  let session;
612
691
  try {
613
692
  this.storage.acceptConversationSession(instance, () => {
614
693
  session = this.deps.structured.createSession({ title, cwd, provider: agent.provider, mode: agent.mode, model,
615
- thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: "interactive",
616
- workspaceId: resolveWorkspaceIdForNewSession(this.storage, cwd),
694
+ thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: executionTarget?.scheduled ? "automation" : "interactive",
695
+ workspaceId: resolveWorkspaceIdForNewSession(this.storage, cwd, executionTarget?.workspaceId),
617
696
  automationId: `${CONVERSATION_SESSION_PREFIX}${requestId}`, employeeId: employee.id,
618
697
  employeeName: employee.name, employeeAvatar: employee.avatar, employeeCandidates: employee.agents,
619
698
  employeeCandidateIndex: selected.index, systemPrompt: employee.prompt });
@@ -1,5 +1,7 @@
1
1
  import type { ConversationSessionPreview, SessionSnapshot } from "./types.js";
2
2
  export declare const CONVERSATION_SESSION_PREFIX = "conversation-message:";
3
+ /** Scheduled occurrences already have a plan title. Identify them from durable metadata, never prompt text. */
4
+ export declare function isScheduledConversationSession(session: Pick<SessionSnapshot, "sessionSource" | "automationId">): boolean;
3
5
  export declare const CONVERSATION_PREVIEW_LIMIT = 4000;
4
6
  export declare const conversationSessionPreviewLabels: Record<ConversationSessionPreview["status"], string>;
5
7
  /** The session owns the transcript. IM only reads a bounded tail; never copies it into message history. */
@@ -1,4 +1,8 @@
1
1
  export const CONVERSATION_SESSION_PREFIX = "conversation-message:";
2
+ /** Scheduled occurrences already have a plan title. Identify them from durable metadata, never prompt text. */
3
+ export function isScheduledConversationSession(session) {
4
+ return session.sessionSource === "automation" && session.automationId?.startsWith(`${CONVERSATION_SESSION_PREFIX}scheduled:`) === true;
5
+ }
2
6
  export const CONVERSATION_PREVIEW_LIMIT = 4000;
3
7
  export const conversationSessionPreviewLabels = {
4
8
  starting: "正在启动", running: "正在回复", waiting_user: "等待你确认", done: "已完成",
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { request as httpRequest } from "node:http";
3
3
  import { request as httpsRequest } from "node:https";
4
- import { DECISION_MAX_BYTES, DecisionError, parseDecisionRequest, parseDecisionResult } from "./decision-types.js";
4
+ import { DECISION_CHAIN_WAIT_MS, DECISION_MAX_BYTES, DecisionError, parseDecisionRequest, parseDecisionResult } from "./decision-types.js";
5
5
  /** No admin/password lookup, redirects, or implicit remote fallback. Credentials stay in headers. */
6
6
  export async function callDecisionService(request, env = process.env) {
7
7
  const endpoint = env.WAND_DECISION_URL;
@@ -49,15 +49,31 @@ export async function callDecisionService(request, env = process.env) {
49
49
  const code = typeof value?.code === "string" && /^[A-Z_]{1,40}$/.test(value.code) ? value.code : "HTTP_ERROR";
50
50
  reject(new DecisionError(code, `决策调用失败 (${code}, HTTP ${res.statusCode})。`, res.statusCode));
51
51
  }
52
- else
53
- resolve(request ? parseDecisionResult(value, request) : value);
52
+ else if (!request)
53
+ resolve(value);
54
+ else {
55
+ // A configured backup is a text evaluator, not LAYA (and can have output tokens).
56
+ if (typeof value?.model !== "string" || !value.model || value.model.length > 512
57
+ || !["laya-mlx", "decision-expert"].includes(value.runtime))
58
+ throw new Error("identity");
59
+ const result = parseDecisionResult(value, request, { model: value.model, runtime: value.runtime });
60
+ if (value.executor !== undefined) {
61
+ const executor = value.executor;
62
+ if (!executor || typeof executor.employeeId !== "string" || executor.employeeId.length > 128
63
+ || !Number.isSafeInteger(executor.candidate) || executor.candidate < 0 || executor.candidate > 3
64
+ || !["local", "free-group", "cli", "sdk"].includes(executor.source))
65
+ throw new Error("executor");
66
+ result.executor = { employeeId: executor.employeeId, candidate: executor.candidate, source: executor.source };
67
+ }
68
+ resolve(result);
69
+ }
54
70
  }
55
- catch {
56
- reject(new DecisionError("INVALID_RESULT", "决策服务返回了无效结果。", 502));
71
+ catch (error) {
72
+ reject(error instanceof DecisionError ? error : new DecisionError("INVALID_RESULT", "决策服务返回了无效结果。", 502));
57
73
  }
58
74
  });
59
75
  });
60
- const deadline = setTimeout(() => req.destroy(new Error("deadline")), 50_000);
76
+ const deadline = setTimeout(() => req.destroy(new Error("deadline")), request ? DECISION_CHAIN_WAIT_MS : 50_000);
61
77
  req.once("close", () => clearTimeout(deadline));
62
78
  req.once("error", () => reject(new DecisionError("CONNECTION_FAILED", "连接决策服务失败或超时。", 502)));
63
79
  req.end(data);
@@ -3,11 +3,11 @@ import { DECISION_EXPERT_ID, DECISION_EXPERT_KEY, DECISION_EXPERT_NAME, WAND_LOC
3
3
  import { OPENROUTER_FREE_SELECTOR } from "./openrouter-free-selection.js";
4
4
  export const DECISION_EXPERT_DUTY = "Wand 决策专家:根据最少必要证据做有界选择、评分与是非判断;按已配置工具与模型顺序调用;本地决策先核对本机能力。";
5
5
  export const DECISION_EXPERT_PROMPT = [
6
- "你是 Wand 内置系统员工「决策专家」。你的职责是基于用户提供的证据,做有界候选选择、评分与是非判断。",
6
+ `你是 Wand 内置系统员工「${DECISION_EXPERT_NAME}」,担任决策专家。你的职责是基于用户提供的证据,做有界候选选择、评分与是非判断。`,
7
7
  "按员工调用链的配置顺序调用工具与模型;初始候选为 LAYA 和 Wand 免费分组。LAYA 不生成开放式文本,不伪装成聊天模型。",
8
8
  "先核对机器的 CPU、内存、Apple Silicon / Metal 和运行时健康。性能/平台不足时明确说明原因,建议配置你的调用链,不能捏造可以运行。",
9
9
  "只使用输入中已授权的最少证据,不读取别人的知识/私聊。概率不是正确性保证,更不是权限、删除、发布或付款批准。",
10
- "严格输出任务要求的格式;不编造选项、解释、执行结果或事实。失败有明确原因,不盲目重复已执行/未知送达的工作。",
10
+ "严格输出任务要求的格式;不编造选项、解释、执行结果或事实。任一决策候选失败时按已配置顺序尝试后续候选,直到成功、全部失败或用户取消;这仅适用于无工具判断,不重放实际任务或操作。",
11
11
  "免费分组在每次请求前重新核对价格;免费候选不可用时依调用链处理,不把免费选择器替换成付费模型。",
12
12
  ].join("\n");
13
13
  export function decisionExpertSeedAgents() {
@@ -1,7 +1,7 @@
1
1
  /** Pure identity shared by server and clients; LAYA is a bounded decision source, never a chat model. */
2
2
  export declare const DECISION_EXPERT_KEY = "wand-decision-expert";
3
3
  export declare const DECISION_EXPERT_ID = "e_wand_decision_expert";
4
- export declare const DECISION_EXPERT_NAME = "\u51B3\u7B56\u4E13\u5BB6";
4
+ export declare const DECISION_EXPERT_NAME = "\u77F3\u4E00";
5
5
  export declare const WAND_LOCAL_DECISION_MODEL = "wand-decision/laya";
6
6
  export declare const WAND_LOCAL_DECISION_LABEL = "Wand \u672C\u5730\u51B3\u7B56\u6A21\u578B \u00B7 LAYA";
7
7
  export declare function isLocalDecisionModel(value: string | null | undefined): boolean;
@@ -1,7 +1,7 @@
1
1
  /** Pure identity shared by server and clients; LAYA is a bounded decision source, never a chat model. */
2
2
  export const DECISION_EXPERT_KEY = "wand-decision-expert";
3
3
  export const DECISION_EXPERT_ID = "e_wand_decision_expert";
4
- export const DECISION_EXPERT_NAME = "决策专家";
4
+ export const DECISION_EXPERT_NAME = "石一";
5
5
  export const WAND_LOCAL_DECISION_MODEL = "wand-decision/laya";
6
6
  export const WAND_LOCAL_DECISION_LABEL = "Wand 本地决策模型 · LAYA";
7
7
  export function isLocalDecisionModel(value) { return value === WAND_LOCAL_DECISION_MODEL; }
@@ -11,6 +11,7 @@ interface DecisionExpertDeps {
11
11
  free: Pick<OpenRouterFreeModelsService, "status" | "resolveForCall">;
12
12
  config: WandConfig;
13
13
  hardware?: () => DecisionHardwareAssessment;
14
+ /** Budget per saved slot; expanded group members share it, not later slots. */
14
15
  timeoutMs?: number;
15
16
  /** A prepared adapter has accepted no input yet. Tests never call real models. */
16
17
  prepare?: (agent: WandTaskAgent, request: AiTextRequest, signal: AbortSignal) => Promise<() => Promise<string>>;