@co0ontty/wand 4.89.6-beta.342.1.gb148c2c → 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 (93) hide show
  1. package/dist/agent-dispatch.js +4 -9
  2. package/dist/ai-team-employee-binding.js +10 -9
  3. package/dist/ai-text-runner.js +5 -5
  4. package/dist/avatar-input.d.ts +2 -0
  5. package/dist/avatar-input.js +19 -0
  6. package/dist/build-info.json +4 -4
  7. package/dist/conversation-service.d.ts +5 -1
  8. package/dist/conversation-service.js +69 -15
  9. package/dist/fixed-employee-avatar.d.ts +7 -0
  10. package/dist/fixed-employee-avatar.js +13 -0
  11. package/dist/git-quick-commit.d.ts +1 -1
  12. package/dist/git-quick-commit.js +4 -4
  13. package/dist/scheduled-task-service.js +8 -34
  14. package/dist/scheduled-task-state.d.ts +5 -0
  15. package/dist/scheduled-task-state.js +37 -0
  16. package/dist/server-ai-team-routes.js +3 -17
  17. package/dist/server-employee-routes.js +3 -25
  18. package/dist/server-request.d.ts +2 -0
  19. package/dist/server-request.js +9 -0
  20. package/dist/server-session-routes.d.ts +3 -3
  21. package/dist/server-session-routes.js +23 -25
  22. package/dist/server-task-routes.d.ts +4 -4
  23. package/dist/server-task-routes.js +13 -12
  24. package/dist/server-workspace-routes.js +2 -1
  25. package/dist/server.js +3 -7
  26. package/dist/session-model.d.ts +15 -0
  27. package/dist/session-model.js +23 -0
  28. package/dist/session-registry.d.ts +1 -1
  29. package/dist/session-transport.d.ts +3 -1
  30. package/dist/session-transport.js +11 -5
  31. package/dist/storage.d.ts +14 -7
  32. package/dist/storage.js +50 -25
  33. package/dist/structured-claude-adapter.js +12 -0
  34. package/dist/structured-claude-protocol.d.ts +4 -0
  35. package/dist/structured-claude-protocol.js +18 -4
  36. package/dist/structured-codex-adapter.js +11 -0
  37. package/dist/structured-failure.d.ts +21 -0
  38. package/dist/structured-failure.js +14 -0
  39. package/dist/structured-gemini-adapter.js +12 -2
  40. package/dist/structured-grok-adapter.js +12 -2
  41. package/dist/structured-opencode-adapter.js +8 -1
  42. package/dist/structured-qoder-adapter.js +14 -0
  43. package/dist/structured-session-manager.d.ts +10 -1
  44. package/dist/structured-session-manager.js +105 -25
  45. package/dist/web-ui/content/ai-teams.js +9 -9
  46. package/dist/web-ui/content/module-manifest.json +1 -1
  47. package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
  48. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-SJC4ZXP3.js → 5478be410b112154/chunk-2YZLCGYN.js} +1 -1
  49. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-26NLYQW5.js → 5478be410b112154/chunk-6CFHW3FA.js} +1 -1
  50. package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
  51. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-A7N5KYZL.js → 5478be410b112154/chunk-DDSO7OX7.js} +1 -1
  52. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-D2WG3LJN.js → 5478be410b112154/chunk-G3CXNMA2.js} +1 -1
  53. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-57XAPHNS.js → 5478be410b112154/chunk-LGVAPHRS.js} +1 -1
  54. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-BPXSNT4W.js → 5478be410b112154/chunk-PCMFGZKX.js} +1 -1
  55. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-5J3EQSWV.js → 5478be410b112154/chunk-QRNA4D7E.js} +1 -1
  56. package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-34NMKHUN.js → 5478be410b112154/chunk-WC7RK7LP.js} +1 -1
  57. package/dist/web-ui/content/modules/{5278975cb1b29070/employee-card-KB44OFX5.js → 5478be410b112154/employee-card-MDWKU4V3.js} +1 -1
  58. package/dist/web-ui/content/modules/{5278975cb1b29070/host-2X6ZCKPS.js → 5478be410b112154/host-56WPSON2.js} +1 -1
  59. package/dist/web-ui/content/modules/{5278975cb1b29070/host-ZGYIE4IZ.js → 5478be410b112154/host-AWIJ5L2K.js} +1 -1
  60. package/dist/web-ui/content/modules/{5278975cb1b29070/host-IROFJ4GI.js → 5478be410b112154/host-BOZLGIT5.js} +1 -1
  61. package/dist/web-ui/content/modules/{5278975cb1b29070/host-GF5KKLZR.js → 5478be410b112154/host-EZV22Q7Y.js} +1 -1
  62. package/dist/web-ui/content/modules/{5278975cb1b29070/host-ZH5TRUMM.js → 5478be410b112154/host-IW43Z5ZF.js} +1 -1
  63. package/dist/web-ui/content/modules/{5278975cb1b29070/host-AI64RHEZ.js → 5478be410b112154/host-JT7NIUQL.js} +1 -1
  64. package/dist/web-ui/content/modules/{5278975cb1b29070/host-TQRIXUV5.js → 5478be410b112154/host-JWEIDB55.js} +1 -1
  65. package/dist/web-ui/content/modules/{5278975cb1b29070/host-PBHWQEOX.js → 5478be410b112154/host-OU5TRYH6.js} +1 -1
  66. package/dist/web-ui/content/modules/{5278975cb1b29070/host-QWMGB6DH.js → 5478be410b112154/host-PRSTCPVE.js} +1 -1
  67. package/dist/web-ui/content/modules/{5278975cb1b29070/host-MR5EGBX4.js → 5478be410b112154/host-TWPJI3HE.js} +1 -1
  68. package/dist/web-ui/content/modules/{5278975cb1b29070/host-IIXD7IZL.js → 5478be410b112154/host-WZGJSNML.js} +1 -1
  69. package/dist/web-ui/content/modules/{5278975cb1b29070/host-FWZVWOP6.js → 5478be410b112154/host-X6KQVFD6.js} +1 -1
  70. package/dist/web-ui/content/modules/{5278975cb1b29070/host-DU3XW44J.js → 5478be410b112154/host-YKCQFMQR.js} +1 -1
  71. package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
  72. package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
  73. package/dist/web-ui/content/modules/{5278975cb1b29070/page-T54OOWBL.js → 5478be410b112154/page-T3VUCU2W.js} +1 -1
  74. package/dist/web-ui/content/modules/{5278975cb1b29070/panel-EW72RT2P.js → 5478be410b112154/panel-HJBGBBPY.js} +1 -1
  75. package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
  76. package/dist/web-ui/content/modules/{5278975cb1b29070/task-board-host-FZAVR2UH.js → 5478be410b112154/task-board-host-AVMDIHIV.js} +1 -1
  77. package/dist/web-ui/content/modules/{5278975cb1b29070/workspace-agent-picker-JFFDXA3L.js → 5478be410b112154/workspace-agent-picker-KI7QVSK3.js} +1 -1
  78. package/dist/web-ui/content/modules/{5278975cb1b29070/workspace-window-N4AZ2TM5.js → 5478be410b112154/workspace-window-L542FGP2.js} +1 -1
  79. package/dist/web-ui/content/styles.css +1 -1
  80. package/dist/web-ui/embedded-assets.d.ts +27 -27
  81. package/dist/web-ui/embedded-assets.js +4 -4
  82. package/package.json +1 -1
  83. package/dist/web-ui/content/modules/5278975cb1b29070/chat-render-ZQMRPFYI.js +0 -24
  84. package/dist/web-ui/content/modules/5278975cb1b29070/chunk-5MIXEH7P.js +0 -1
  85. package/dist/web-ui/content/modules/5278975cb1b29070/host-bridge-GAUAUH4R.js +0 -1
  86. package/dist/web-ui/content/modules/5278975cb1b29070/library-bridge-UEPAUWAR.js +0 -1
  87. package/dist/web-ui/content/modules/5278975cb1b29070/runtime.js +0 -794
  88. /package/dist/web-ui/content/modules/{5278975cb1b29070 → 5478be410b112154}/chunk-NURCNMRU.js +0 -0
  89. /package/dist/web-ui/content/modules/{5278975cb1b29070/chunk-XXSPOSVD.js → 5478be410b112154/chunk-TMRPHNJO.js} +0 -0
  90. /package/dist/web-ui/content/modules/{5278975cb1b29070 → 5478be410b112154}/chunk-YOXXNXOX.js +0 -0
  91. /package/dist/web-ui/content/modules/{5278975cb1b29070 → 5478be410b112154}/chunk-YX475LXO.js +0 -0
  92. /package/dist/web-ui/content/modules/{5278975cb1b29070 → 5478be410b112154}/host-5QOHKHNC.js +0 -0
  93. /package/dist/web-ui/content/modules/{5278975cb1b29070 → 5478be410b112154}/sha256-ANXV72UC.js +0 -0
@@ -1,6 +1,5 @@
1
- import { getDefaultModelForProvider } from "./config.js";
2
1
  import { isLocalDecisionModel } from "./decision-expert-identity.js";
3
- import { defaultModelGroupSelector } from "./model-groups.js";
2
+ import { fallbackProviderModel, sessionEmployeeFields } from "./session-model.js";
4
3
  import { defaultRoleForCli } from "./default-employee.js";
5
4
  import { providerCliCommand } from "./session-provider.js";
6
5
  import { agentKey } from "./ai-team-types.js";
@@ -46,7 +45,7 @@ export async function dispatchAgentForTask(deps, input) {
46
45
  const cwd = group.cwd;
47
46
  const provider = agent.provider;
48
47
  const model = agent.model === "default" ? "" : agent.model;
49
- const resolvedModel = model || defaultModelGroupSelector(config.modelGroups, provider, getDefaultModelForProvider(config, provider)) || getDefaultModelForProvider(config, provider) || undefined;
48
+ const resolvedModel = model || fallbackProviderModel(config, provider);
50
49
  if (isLocalDecisionModel(resolvedModel)) {
51
50
  throw new AgentDispatchPreflightError("LAYA 仅支持有界决策,不能执行聊天或任务;该候选尚未接受输入。");
52
51
  }
@@ -58,9 +57,7 @@ export async function dispatchAgentForTask(deps, input) {
58
57
  sessionSource: "automation",
59
58
  automationId: input.automationId,
60
59
  systemPrompt,
61
- employeeId: role?.id,
62
- employeeName: role?.name,
63
- employeeAvatar: role?.avatar,
60
+ ...sessionEmployeeFields(role),
64
61
  workspaceId: group.workspaceId,
65
62
  workspaceTaskId: group.workspaceTaskId,
66
63
  })
@@ -79,9 +76,7 @@ export async function dispatchAgentForTask(deps, input) {
79
76
  sessionSource: "automation",
80
77
  automationId: input.automationId,
81
78
  systemPrompt,
82
- employeeId: role?.id,
83
- employeeName: role?.name,
84
- employeeAvatar: role?.avatar,
79
+ ...sessionEmployeeFields(role),
85
80
  employeeCandidates: input.employee?.agents,
86
81
  employeeCandidateIndex: input.employeeCandidateIndex,
87
82
  workspaceId: group.workspaceId,
@@ -61,18 +61,19 @@ export function serializeExecutionTeam(team) {
61
61
  ...member, _employeeSnapshot: snapshots.get(member),
62
62
  })) });
63
63
  }
64
+ function projectBoundMember(member, source, includeAgents) {
65
+ const employee = member.employeeId ? source.getSiliconEmployee(member.employeeId) : null;
66
+ if (!employee)
67
+ return member;
68
+ return includeAgents
69
+ ? { ...member, name: employee.name, avatar: employee.avatar, agents: employee.agents, agent: employee.agents[0] }
70
+ : { ...member, name: employee.name, avatar: employee.avatar };
71
+ }
64
72
  /** Team definitions expose current employee-owned fields without storing another copy of the role. */
65
73
  export function projectTeamDefinition(team, source) {
66
- return { ...team, members: team.members.map((member) => {
67
- const employee = member.employeeId ? source.getSiliconEmployee(member.employeeId) : null;
68
- return employee ? { ...member, name: employee.name, avatar: employee.avatar,
69
- agents: employee.agents, agent: employee.agents[0] } : member;
70
- }) };
74
+ return { ...team, members: team.members.map((member) => projectBoundMember(member, source, true)) };
71
75
  }
72
76
  /** Read-only names/avatars follow the same employee, never a replacement at the same member id. */
73
77
  export function projectTeamEmployees(team, source) {
74
- return { ...team, members: team.members.map((member) => {
75
- const employee = member.employeeId ? source.getSiliconEmployee(member.employeeId) : null;
76
- return employee ? { ...member, name: employee.name, avatar: employee.avatar } : member;
77
- }) };
78
+ return { ...team, members: team.members.map((member) => projectBoundMember(member, source, false)) };
78
79
  }
@@ -1,18 +1,18 @@
1
- import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
1
+ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
5
- import { spawn } from "node:child_process";
6
6
  import { StringDecoder } from "node:string_decoder";
7
7
  import { appendDiagnosticTail, MAX_AI_TEXT_BYTES } from "./bounded-process-output.js";
8
+ import { QuickCommitError } from "./command-error.js";
8
9
  import { abortableEmployeeText, prepareEmployeeTextCandidate } from "./employee-text.js";
9
10
  import { buildChildEnv } from "./env-utils.js";
11
+ import { getGitErrorMessage } from "./git-utils.js";
10
12
  import { buildLanguageDirective } from "./language-prompt.js";
11
13
  import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
12
14
  import { asRecord } from "./structured-content.js";
13
- import { getGitErrorMessage } from "./git-utils.js";
14
- import { QuickCommitError } from "./command-error.js";
15
- import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel } from "./structured-provider-common.js";
15
+ import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
16
16
  export { QuickCommitError } from "./command-error.js";
17
17
  const CLAUDE_MESSAGE_TIMEOUT_MS = 60_000;
18
18
  const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
@@ -0,0 +1,2 @@
1
+ /** One write contract for employee and unbound team-member avatars. */
2
+ export declare function parseStoredAvatar(value: unknown, label: string, maxChars: number): string;
@@ -0,0 +1,19 @@
1
+ import { parsePlushAvatar, parsePlushCatAvatar, parsePlushDogAvatar } from "./plush-avatar.js";
2
+ import { text } from "./server-request.js";
3
+ /** One write contract for employee and unbound team-member avatars. */
4
+ export function parseStoredAvatar(value, label, maxChars) {
5
+ if (value !== undefined && value !== null && typeof value !== "string") {
6
+ throw new Error(`${label}的头像格式无效。`);
7
+ }
8
+ const avatar = text(value);
9
+ if (!avatar || /^cat:\d{1,2}$/.test(avatar))
10
+ return avatar;
11
+ if (parsePlushAvatar(avatar) || parsePlushCatAvatar(avatar) || parsePlushDogAvatar(avatar))
12
+ return avatar;
13
+ if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar)) {
14
+ throw new Error(`${label}的头像格式无效。`);
15
+ }
16
+ if (avatar.length > maxChars)
17
+ throw new Error(`${label}的头像太大。`);
18
+ return avatar;
19
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "b148c2c02754610cf75f7c9b533f123b2bdc0682",
3
- "builtAt": "2026-10-11T05:37:44.167Z",
4
- "version": "4.89.6-beta.342.1.gb148c2c",
5
- "channel": "beta"
2
+ "commit": "c19f50516469789583ace432959dd7519a952f19",
3
+ "builtAt": "2026-10-11T15:04:29.304Z",
4
+ "version": "4.90.0",
5
+ "channel": "stable"
6
6
  }
@@ -55,7 +55,11 @@ export declare class ConversationService {
55
55
  private selfName;
56
56
  private memberReasons;
57
57
  private turns;
58
- 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;
59
63
  list(): ConversationSummary[];
60
64
  updateListState(id: string, patch: {
61
65
  pinned?: boolean;
@@ -6,8 +6,8 @@ import { freezeTeamEmployees, projectTeamEmployees, requireTeamEmployee } from "
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
13
  import { isGlobalWorkspace, normalizeProjectCwd, resolveWorkspaceIdForNewSession } from "./workspace-binding.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,8 +248,7 @@ 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) ?? "";
@@ -203,7 +257,10 @@ export class ConversationService {
203
257
  // 私聊标题已说明对方身份;只保留自己的署名,群聊仍需区分发言人。
204
258
  const previewAuthor = last?.role === "user" ? `${this.selfName()}:`
205
259
  : instance.kind === "group" && last ? `${last.author?.name ?? "系统"}:` : "";
206
- return { ...instance, ...this.storage.conversationListState(instance.id), team: instance.team ? projectTeamEmployees(instance.team, this.storage) : null,
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,
207
264
  title, unavailableReason, memberUnavailableReasons: this.memberReasons(instance), tasks,
208
265
  preview: last ? `${previewAuthor}${lastText.slice(0, 160)}`
209
266
  : instance.kind === "group" ? "群已建立,尚未派任务" : "还没有消息", messageAt };
@@ -298,8 +355,9 @@ export class ConversationService {
298
355
  history(id) { return this.turns(this.requireInstance(id)); }
299
356
  detail(id) {
300
357
  const instance = this.requireInstance(id);
301
- const summary = this.summary(instance);
302
- 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
303
361
  ? { ...turn, sessionPreview: conversationSessionPreview(this.snapshot(turn.sessionLink.sessionId), turn.requestId ? this.receipt(turn.requestId) ?? undefined : undefined),
304
362
  // Older clients can still open the exact source session without understanding sessionLink.
305
363
  author: turn.author ? { ...turn.author, sessionId: turn.sessionLink.sessionId } : turn.author,
@@ -517,9 +575,7 @@ export class ConversationService {
517
575
  if (!session) {
518
576
  if (instance.communicationSessionId)
519
577
  instance.historySessionIds = [...(instance.historySessionIds ?? []), instance.communicationSessionId];
520
- const model = agent.model === "default"
521
- ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
522
- || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
578
+ const model = agent.model === "default" ? fallbackProviderModel(this.deps.config, agent.provider) : agent.model;
523
579
  session = this.deps.structured.createSession({ cwd, provider: agent.provider, mode: agent.mode, model,
524
580
  thinkingEffort: agent.thinkingEffort, worktreeEnabled: false, sessionSource: "interactive",
525
581
  automationId: `conversation-talk:${instance.id}`, employeeId: employee?.id,
@@ -628,9 +684,7 @@ export class ConversationService {
628
684
  const employee = this.storage.getSiliconEmployee(instance.peerEmployeeId);
629
685
  const selected = selectEmployeeCandidate(employee), agent = selected.agent;
630
686
  const cwd = this.cwd(explicitCwd);
631
- const model = agent.model === "default"
632
- ? defaultModelGroupSelector(this.deps.config.modelGroups, agent.provider, getDefaultModelForProvider(this.deps.config, agent.provider))
633
- || getDefaultModelForProvider(this.deps.config, agent.provider) || undefined : agent.model;
687
+ const model = agent.model === "default" ? fallbackProviderModel(this.deps.config, agent.provider) : agent.model;
634
688
  const title = shortText(executionTarget?.title, "会话标题", 200)
635
689
  || provisionalTaskTitleFromDescription(conversationMessageBody(input)) || "新会话";
636
690
  let session;
@@ -18,6 +18,13 @@ export declare function employeeAvatarValue(identity: {
18
18
  systemKey?: string | null;
19
19
  avatar?: string | null;
20
20
  }): string;
21
+ /** Uploaded photos are data URLs up to 60KB. Short tokens are the only avatar bytes lists should repeat. */
22
+ export declare function isUploadedAvatar(value: string | null | undefined): boolean;
23
+ /**
24
+ * List cards, nested session summaries and routine events keep plush/cat tokens.
25
+ * Uploaded bytes stay on the employee record, session detail and the first init packet.
26
+ */
27
+ export declare function compactTransportAvatar(value: string | null | undefined): string | undefined;
21
28
  /** The avatar exception does not change any other built-in employee's edit policy. */
22
29
  export declare function isFixedAvatarEmployee(identity: {
23
30
  id?: string | null;
@@ -11,6 +11,19 @@ export function fixedEmployeeAvatar(identity) {
11
11
  export function employeeAvatarValue(identity) {
12
12
  return fixedEmployeeAvatar(identity) ?? identity.avatar ?? "";
13
13
  }
14
+ /** Uploaded photos are data URLs up to 60KB. Short tokens are the only avatar bytes lists should repeat. */
15
+ export function isUploadedAvatar(value) {
16
+ return typeof value === "string" && value.startsWith("data:");
17
+ }
18
+ /**
19
+ * List cards, nested session summaries and routine events keep plush/cat tokens.
20
+ * Uploaded bytes stay on the employee record, session detail and the first init packet.
21
+ */
22
+ export function compactTransportAvatar(value) {
23
+ if (!value || isUploadedAvatar(value))
24
+ return undefined;
25
+ return value;
26
+ }
14
27
  /** The avatar exception does not change any other built-in employee's edit policy. */
15
28
  export function isFixedAvatarEmployee(identity) {
16
29
  return fixedEmployeeAvatar(identity) !== null;
@@ -1,7 +1,7 @@
1
1
  import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, SessionSnapshot, TagHeadResult } from "./types.js";
2
2
  /** provider 缺省值:无法识别时回落到 Claude。 */
3
3
  import { type QuickCommitAiOptions } from "./ai-text-runner.js";
4
- export { callCliAiText, callConfiguredAiText, assertGeminiTextCapability, withOpsPersona } from "./ai-text-runner.js";
4
+ export { assertGeminiTextCapability, callCliAiText, callConfiguredAiText, withOpsPersona } from "./ai-text-runner.js";
5
5
  export type { QuickCommitAiOptions } from "./ai-text-runner.js";
6
6
  export { QuickCommitError, type QuickCommitErrorCode } from "./command-error.js";
7
7
  /** Non-blocking status implementation used by HTTP routes. */
@@ -1,10 +1,10 @@
1
1
  import { existsSync } from "node:fs";
2
- import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
3
- import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, } from "./structured-provider-common.js";
2
+ import { getGitErrorMessage, runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, } from "./git-utils.js";
3
+ import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
4
4
  /** provider 缺省值:无法识别时回落到 Claude。 */
5
- import { defaultProvider, normalizeAiText, buildPiTextArgs, buildGrokTextArgs, buildGeminiTextArgs, buildQoderTextArgs, systemPromptArgsFrom, contentWithSystemPrompt, runCliText, callConfiguredAiText, withOpsPersona } from "./ai-text-runner.js";
5
+ import { buildGeminiTextArgs, buildGrokTextArgs, buildPiTextArgs, buildQoderTextArgs, callConfiguredAiText, contentWithSystemPrompt, defaultProvider, normalizeAiText, runCliText, systemPromptArgsFrom, withOpsPersona } from "./ai-text-runner.js";
6
6
  import { QuickCommitError } from "./command-error.js";
7
- export { callCliAiText, callConfiguredAiText, assertGeminiTextCapability, withOpsPersona } from "./ai-text-runner.js";
7
+ export { assertGeminiTextCapability, callCliAiText, callConfiguredAiText, withOpsPersona } from "./ai-text-runner.js";
8
8
  export { QuickCommitError } from "./command-error.js";
9
9
  const GIT_TIMEOUT_MS = 1500;
10
10
  const GIT_PUSH_TIMEOUT_MS = 30_000;
@@ -1,12 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sessionActive, scheduledExecutionStatus } from "./scheduled-task-state.js";
3
2
  import { isAbsolute } from "node:path";
3
+ import { reconcileScheduledRun, scheduledExecutionStatus } from "./scheduled-task-state.js";
4
4
  import { requireTeamEmployee } from "./ai-team-employee-binding.js";
5
5
  import { employeeConversationId } from "./conversation-types.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
+ import { nextScheduledTime, parseTaskSchedule } from "./scheduled-task-time.js";
7
8
  import { bodyObject } from "./server-request.js";
8
9
  import { resolveSessionCwd } from "./session-cwd.js";
9
- import { parseTaskSchedule, nextScheduledTime } from "./scheduled-task-time.js";
10
10
  import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
11
11
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
12
12
  import { isGlobalWorkspace } from "./workspace-binding.js";
@@ -186,42 +186,16 @@ export class ScheduledTaskService {
186
186
  return run;
187
187
  }
188
188
  const receipt = this.deps.conversations.receipt(run.requestId) ?? run.receipt;
189
- let updated = { ...run, receipt };
190
- if (run.state === "claimed") {
191
- updated = { ...updated, state: receipt?.state === "accepted" ? "accepted" : receipt?.state === "rejected" ? "rejected" : "unknown",
192
- error: receipt?.error ?? (receipt?.state === "accepted" ? null : "本次执行的接收状态无法确认,请核对执行记录;不会自动重发。") };
193
- }
194
189
  const session = receipt?.sessionId ? this.deps.getSession(receipt.sessionId) : null;
195
- if (session && session.endedAt && session.status !== "running" && !session.structuredState?.inFlight) {
196
- // A user may already have queued a follow-up. This ended turn still settles the scheduled occurrence.
197
- const failed = session.status === "failed" || session.status === "stopped" || Boolean(session.structuredState?.lastError)
198
- || receipt?.startup === "failed" || (session.exitCode !== null && session.exitCode !== 0);
199
- updated.executionOutcome = failed ? "failed" : "completed";
200
- updated.executionFinishedAt = session.endedAt;
201
- if (failed)
202
- updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
203
- }
204
- else if (session && !sessionActive(session) && (session.status === "failed" || session.status === "stopped"
205
- || session.structuredState?.lastError)) {
206
- updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
207
- updated.executionOutcome = "failed";
208
- updated.executionFinishedAt = now;
209
- }
210
- else if (updated.state === "rejected" || (!session && (updated.state === "unknown" || receipt?.state === "accepted"))
211
- || (receipt?.startup === "failed" && (!session || !sessionActive(session)))) {
212
- updated.executionOutcome = updated.state === "rejected" || receipt?.startup === "failed" ? "failed" : "unknown";
213
- updated.executionFinishedAt = now;
214
- }
215
- else if (recoverInactive && session && !sessionActive(session) && !session.endedAt && !this.liveAcceptedRuns.has(run.id)
216
- && (updated.state === "accepted" || updated.state === "unknown")) {
217
- // After restart an initial idle snapshot proves neither dispatch nor completion. Preserve its receipt without replay.
218
- updated = { ...updated, state: "unknown", executionFinishedAt: now, executionOutcome: "unknown",
219
- error: updated.error ?? "本次执行尚未确认启动或结束,请核对会话;不会自动重发。" };
220
- }
190
+ const updated = reconcileScheduledRun(run, receipt, session, now, {
191
+ recoverInactive, liveAccepted: this.liveAcceptedRuns.has(run.id),
192
+ });
221
193
  if (updated.executionFinishedAt)
222
194
  this.liveAcceptedRuns.delete(run.id);
223
- if (JSON.stringify(updated) !== JSON.stringify(run))
195
+ if (updated.state !== run.state || updated.error !== run.error || updated.executionOutcome !== run.executionOutcome
196
+ || updated.executionFinishedAt !== run.executionFinishedAt || JSON.stringify(updated.receipt) !== JSON.stringify(run.receipt)) {
224
197
  this.deps.storage.saveScheduledTaskRun(updated);
198
+ }
225
199
  return updated;
226
200
  }
227
201
  /** Capture the original turn's terminal fact before a user can start another turn in the same session. */
@@ -4,3 +4,8 @@ import type { SessionSnapshot } from "./types.js";
4
4
  export declare function sessionActive(session: SessionSnapshot): boolean;
5
5
  /** Pure read projection; it never settles or replays a scheduled occurrence. */
6
6
  export declare function scheduledExecutionStatus(run: ScheduledTaskRun, receipt: ConversationReceipt | null, session: SessionSnapshot | null): ScheduledTaskRunView["executionStatus"];
7
+ /** Settles only observed facts; missing/unknown acceptance never authorizes replay. */
8
+ export declare function reconcileScheduledRun(run: ScheduledTaskRun, receipt: ConversationReceipt | null, session: SessionSnapshot | null, now: string, options: {
9
+ recoverInactive: boolean;
10
+ liveAccepted: boolean;
11
+ }): ScheduledTaskRun;
@@ -26,3 +26,40 @@ export function scheduledExecutionStatus(run, receipt, session) {
26
26
  executionStatus = "pending";
27
27
  return executionStatus;
28
28
  }
29
+ /** Settles only observed facts; missing/unknown acceptance never authorizes replay. */
30
+ export function reconcileScheduledRun(run, receipt, session, now, options) {
31
+ if (run.executionFinishedAt)
32
+ return run;
33
+ let updated = { ...run, receipt };
34
+ if (run.state === "claimed") {
35
+ updated = { ...updated, state: receipt?.state === "accepted" ? "accepted" : receipt?.state === "rejected" ? "rejected" : "unknown",
36
+ error: receipt?.error ?? (receipt?.state === "accepted" ? null : "本次执行的接收状态无法确认,请核对执行记录;不会自动重发。") };
37
+ }
38
+ if (session && session.endedAt && session.status !== "running" && !session.structuredState?.inFlight) {
39
+ // A user may already have queued a follow-up. This ended turn still settles the scheduled occurrence.
40
+ const failed = session.status === "failed" || session.status === "stopped" || Boolean(session.structuredState?.lastError)
41
+ || receipt?.startup === "failed" || (session.exitCode !== null && session.exitCode !== 0);
42
+ updated.executionOutcome = failed ? "failed" : "completed";
43
+ updated.executionFinishedAt = session.endedAt;
44
+ if (failed)
45
+ updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
46
+ }
47
+ else if (session && !sessionActive(session) && (session.status === "failed" || session.status === "stopped"
48
+ || session.structuredState?.lastError)) {
49
+ updated.error = receipt?.error ?? session.structuredState?.lastError ?? updated.error;
50
+ updated.executionOutcome = "failed";
51
+ updated.executionFinishedAt = now;
52
+ }
53
+ else if (updated.state === "rejected" || (!session && (updated.state === "unknown" || receipt?.state === "accepted"))
54
+ || (receipt?.startup === "failed" && (!session || !sessionActive(session)))) {
55
+ updated.executionOutcome = updated.state === "rejected" || receipt?.startup === "failed" ? "failed" : "unknown";
56
+ updated.executionFinishedAt = now;
57
+ }
58
+ else if (options.recoverInactive && session && !sessionActive(session) && !session.endedAt && !options.liveAccepted
59
+ && (updated.state === "accepted" || updated.state === "unknown")) {
60
+ // After restart an initial idle snapshot proves neither dispatch nor completion. Preserve its receipt without replay.
61
+ updated = { ...updated, state: "unknown", executionFinishedAt: now, executionOutcome: "unknown",
62
+ error: updated.error ?? "本次执行尚未确认启动或结束,请核对会话;不会自动重发。" };
63
+ }
64
+ return updated;
65
+ }
@@ -5,7 +5,8 @@ import { requireTeamEmployee } from "./ai-team-employee-binding.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
7
  import { defaultMilestoneIdForWrite, scopedMilestoneId } from "./milestone-scope.js";
8
- import { bodyObject, sendRouteError, text } from "./server-request.js";
8
+ import { parseStoredAvatar } from "./avatar-input.js";
9
+ import { bodyObject, boundedText, sendRouteError, text } from "./server-request.js";
9
10
  import { parseTaskAgent } from "./server-task-routes.js";
10
11
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
11
12
  const NOT_FOUND = /不存在/;
@@ -20,23 +21,8 @@ function sendTeamError(res, error) {
20
21
  const status = error instanceof AiTeamConflictError ? 409 : NOT_FOUND.test(message) ? 404 : 400;
21
22
  sendRouteError(res, error, "团队操作失败。", status);
22
23
  }
23
- function boundedText(value, label, min, max) {
24
- const result = text(value);
25
- if (result.length < min)
26
- throw new Error(`${label}不能为空。`);
27
- if (result.length > max)
28
- throw new Error(`${label}不能超过 ${max} 个字符。`);
29
- return result;
30
- }
31
24
  function parseAvatar(value, name) {
32
- const avatar = text(value);
33
- if (!avatar || /^cat:\d{1,2}$/.test(avatar))
34
- return avatar;
35
- if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar))
36
- throw new Error(`成员「${name}」的头像格式无效。`);
37
- if (avatar.length > AI_TEAM_AVATAR_MAX_CHARS)
38
- throw new Error(`成员「${name}」的头像太大。`);
39
- return avatar;
25
+ return parseStoredAvatar(value, `成员「${name}」`, AI_TEAM_AVATAR_MAX_CHARS);
40
26
  }
41
27
  function parseMembers(value, existing, source) {
42
28
  if (!Array.isArray(value))
@@ -4,14 +4,14 @@ import { randomUUID } from "node:crypto";
4
4
  import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
5
5
  import { getErrorMessage } from "./error-utils.js";
6
6
  import { asyncRoute } from "./express-async.js";
7
- import { bodyObject, sendRouteError, text } from "./server-request.js";
7
+ import { parseStoredAvatar } from "./avatar-input.js";
8
+ import { bodyObject, boundedText, sendRouteError, text } from "./server-request.js";
8
9
  import { parseTaskAgent } from "./server-task-routes.js";
9
10
  import { resolveSystemAiContext } from "./session-ai-context.js";
10
11
  import { employeeCliAvailable } from "./silicon-employee-dispatch.js";
11
12
  import { EMPLOYEE_CREATION_DEFAULTS_PREF, resolveSiliconEmployeeDefaults, } from "./silicon-employee-defaults.js";
12
13
  import { availableEmployeeProviders, generateSiliconEmployeeDraft, SiliconEmployeeDraftError, validateStructuredEmployeeDraft, } from "./silicon-employee-draft.js";
13
14
  import { SESSION_PROVIDERS } from "./session-provider.js";
14
- import { parsePlushAvatar, parsePlushCatAvatar, parsePlushDogAvatar } from "./plush-avatar.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
  /** Recruitment uses HR’s own ordered candidates and persona, never the ops chain. */
@@ -54,30 +54,8 @@ function rejectSystemEmployeeMutation(existing, action) {
54
54
  return;
55
55
  throw new Error(`「${existing.name}」是 Wand 内置员工,不能${action}。`);
56
56
  }
57
- function boundedText(value, label, min, max) {
58
- const result = text(value);
59
- if (result.length < min)
60
- throw new Error(`${label}不能为空。`);
61
- if (result.length > max)
62
- throw new Error(`${label}不能超过 ${max} 个字符。`);
63
- return result;
64
- }
65
57
  function parseAvatar(value, name) {
66
- if (value !== undefined && value !== null && typeof value !== "string") {
67
- throw new Error(`员工「${name}」的头像格式无效。`);
68
- }
69
- const avatar = text(value);
70
- if (!avatar || /^cat:\d{1,2}$/.test(avatar))
71
- return avatar;
72
- if (parsePlushAvatar(avatar) || parsePlushCatAvatar(avatar) || parsePlushDogAvatar(avatar))
73
- return avatar;
74
- if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar)) {
75
- throw new Error(`员工「${name}」的头像格式无效。`);
76
- }
77
- if (avatar.length > SILICON_EMPLOYEE_AVATAR_MAX_CHARS) {
78
- throw new Error(`员工「${name}」的头像太大。`);
79
- }
80
- return avatar;
58
+ return parseStoredAvatar(value, `员工「${name}」`, SILICON_EMPLOYEE_AVATAR_MAX_CHARS);
81
59
  }
82
60
  /** Other built-ins keep profile locks; the two stable cat identities lock only avatar. */
83
61
  const SYSTEM_EMPLOYEE_LOCKED_FIELDS = [
@@ -7,6 +7,8 @@
7
7
  */
8
8
  /** 取字符串并 trim;非字符串一律当缺省处理。 */
9
9
  export declare function text(value: unknown, fallback?: string): string;
10
+ /** Shared length check for employee and team name fields. */
11
+ export declare function boundedText(value: unknown, label: string, min: number, max: number): string;
10
12
  /** Express query 值可能是数组,取第一个字符串。 */
11
13
  export declare function firstQueryStringValue(value: unknown): string | undefined;
12
14
  /** 请求头可能是数组(重复头),取第一个值。 */
@@ -10,6 +10,15 @@ import { getErrorMessage } from "./error-utils.js";
10
10
  export function text(value, fallback = "") {
11
11
  return typeof value === "string" ? value.trim() : fallback;
12
12
  }
13
+ /** Shared length check for employee and team name fields. */
14
+ export function boundedText(value, label, min, max) {
15
+ const result = text(value);
16
+ if (result.length < min)
17
+ throw new Error(`${label}不能为空。`);
18
+ if (result.length > max)
19
+ throw new Error(`${label}不能超过 ${max} 个字符。`);
20
+ return result;
21
+ }
13
22
  /** Express query 值可能是数组,取第一个字符串。 */
14
23
  export function firstQueryStringValue(value) {
15
24
  if (typeof value === "string")
@@ -1,13 +1,13 @@
1
1
  import type { Express } from "express";
2
+ import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
2
3
  import { ProcessManager } from "./process-manager.js";
3
- import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
+ import { StructuredSessionManager } from "./structured-session-manager.js";
5
6
  import { ExecutionMode, ProcessEvent, SessionSnapshot, WandConfig } from "./types.js";
6
- import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
7
7
  import { toSessionListItemDTO } from "./session-transport.js";
8
- import { type ProviderHistoryDeleter, SessionRegistry } from "./session-registry.js";
9
8
  import { type PiRecommendationRuntime } from "./server-pi-recommendation-routes.js";
10
9
  import { type SessionDirectoryNode } from "./session-directory-tree.js";
10
+ import { SessionRegistry, type ProviderHistoryDeleter } from "./session-registry.js";
11
11
  export { parseExecutionMode, parseSessionCreationOrigin } from "./session-request.js";
12
12
  type SessionDeletionProcesses = Pick<ProcessManager, "get" | "delete"> & ProviderHistoryDeleter;
13
13
  type SessionDeletionStructured = Pick<StructuredSessionManager, "get" | "delete">;