@co0ontty/wand 4.94.0 → 4.95.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 +4 -1
  2. package/dist/ai-text-runner.d.ts +4 -0
  3. package/dist/ai-text-runner.js +44 -4
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli-api.d.ts +1 -0
  6. package/dist/cli-api.js +7 -2
  7. package/dist/cli.js +47 -0
  8. package/dist/conversation-execution-preference.d.ts +7 -0
  9. package/dist/conversation-execution-preference.js +26 -0
  10. package/dist/conversation-organize-contract.d.ts +70 -0
  11. package/dist/conversation-organize-contract.js +1 -0
  12. package/dist/conversation-organize-service.d.ts +46 -0
  13. package/dist/conversation-organize-service.js +505 -0
  14. package/dist/conversation-organize.d.ts +41 -0
  15. package/dist/conversation-organize.js +97 -0
  16. package/dist/conversation-service.d.ts +2 -1
  17. package/dist/conversation-service.js +10 -4
  18. package/dist/core-turn-tracker.d.ts +5 -1
  19. package/dist/core-turn-tracker.js +17 -1
  20. package/dist/decision-expert-service.d.ts +2 -0
  21. package/dist/decision-expert-service.js +17 -1
  22. package/dist/decision-skill.d.ts +0 -1
  23. package/dist/decision-skill.js +5 -64
  24. package/dist/deployment-lock.js +4 -1
  25. package/dist/employee-im-config.d.ts +17 -0
  26. package/dist/employee-im-config.js +85 -0
  27. package/dist/employee-im-lifecycle.d.ts +35 -0
  28. package/dist/employee-im-lifecycle.js +168 -0
  29. package/dist/employee-im-types.d.ts +80 -0
  30. package/dist/employee-im-types.js +32 -0
  31. package/dist/employee-im.d.ts +35 -0
  32. package/dist/employee-im.js +157 -0
  33. package/dist/employee-knowledge.js +3 -2
  34. package/dist/employee-text.d.ts +1 -0
  35. package/dist/employee-text.js +28 -13
  36. package/dist/git-quick-commit.js +21 -3
  37. package/dist/managed-skill.d.ts +6 -0
  38. package/dist/managed-skill.js +99 -0
  39. package/dist/model-auto-assign.js +1 -1
  40. package/dist/process-manager.js +1 -1
  41. package/dist/prompt-optimizer.js +1 -1
  42. package/dist/scheduled-task-activity.d.ts +16 -0
  43. package/dist/scheduled-task-activity.js +42 -0
  44. package/dist/scheduled-task-agenda.d.ts +20 -0
  45. package/dist/scheduled-task-agenda.js +82 -0
  46. package/dist/scheduled-task-calendar.d.ts +37 -0
  47. package/dist/scheduled-task-calendar.js +131 -0
  48. package/dist/scheduled-task-read-range.d.ts +7 -0
  49. package/dist/scheduled-task-read-range.js +35 -0
  50. package/dist/scheduled-task-service.d.ts +6 -1
  51. package/dist/scheduled-task-service.js +61 -1
  52. package/dist/scheduled-task-state.d.ts +1 -1
  53. package/dist/scheduled-task-types.d.ts +38 -0
  54. package/dist/server-asset-routes.js +3 -2
  55. package/dist/server-conversation-organize-routes.d.ts +4 -0
  56. package/dist/server-conversation-organize-routes.js +75 -0
  57. package/dist/server-conversation-routes.js +2 -1
  58. package/dist/server-employee-im-routes.d.ts +9 -0
  59. package/dist/server-employee-im-routes.js +40 -0
  60. package/dist/server-employee-routes.js +1 -1
  61. package/dist/server-scheduled-task-routes.js +24 -0
  62. package/dist/server-session-lifecycle-routes.js +8 -3
  63. package/dist/server-session-routes.js +18 -0
  64. package/dist/server-session-worktree-routes.js +3 -3
  65. package/dist/server-workspace-routes.d.ts +3 -0
  66. package/dist/server-workspace-routes.js +14 -1
  67. package/dist/server.js +39 -5
  68. package/dist/session-ai-context.d.ts +11 -1
  69. package/dist/session-ai-context.js +21 -2
  70. package/dist/session-metadata-service.js +1 -1
  71. package/dist/session-registry.js +6 -0
  72. package/dist/session-request.js +3 -0
  73. package/dist/session-topic.js +1 -1
  74. package/dist/silicon-employee-draft.js +1 -1
  75. package/dist/speech-polisher-service.d.ts +2 -0
  76. package/dist/speech-polisher-service.js +28 -6
  77. package/dist/storage/conversation-organization-repository.d.ts +13 -0
  78. package/dist/storage/conversation-organization-repository.js +33 -0
  79. package/dist/storage/migrations.js +10 -0
  80. package/dist/storage/secrets-repository.d.ts +7 -0
  81. package/dist/storage/secrets-repository.js +28 -1
  82. package/dist/storage/tasks-repository.js +4 -0
  83. package/dist/storage-schedules.d.ts +8 -1
  84. package/dist/storage-schedules.js +24 -0
  85. package/dist/storage-schema.js +9 -0
  86. package/dist/storage.d.ts +6 -1
  87. package/dist/storage.js +13 -0
  88. package/dist/structured-session-manager.d.ts +1 -0
  89. package/dist/structured-session-manager.js +18 -4
  90. package/dist/system-ai-call.d.ts +49 -0
  91. package/dist/system-ai-call.js +28 -0
  92. package/dist/system-ai-session-recorder.d.ts +30 -0
  93. package/dist/system-ai-session-recorder.js +239 -0
  94. package/dist/system-cli.d.ts +6 -0
  95. package/dist/system-cli.js +93 -0
  96. package/dist/system-skill.d.ts +5 -0
  97. package/dist/system-skill.js +19 -0
  98. package/dist/task-organize-contract.js +2 -2
  99. package/dist/task-title-organize-service.d.ts +1 -1
  100. package/dist/task-title-organize-service.js +9 -5
  101. package/dist/task-title-organize.js +13 -6
  102. package/dist/task-title-service.js +1 -1
  103. package/dist/task-title.js +1 -1
  104. package/dist/user-memory.js +1 -1
  105. package/dist/wand-task-sync.js +2 -1
  106. package/dist/web-ui/content/ai-teams.js +12 -12
  107. package/dist/web-ui/content/module-manifest.json +1 -1
  108. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js → 09842858f9d34a4e/library-bridge.js} +2 -2
  109. package/dist/web-ui/content/modules/1021ae40c5761e3f/sha256.js +1 -0
  110. package/dist/web-ui/content/modules/134f6120c0acaf57/host.js +1 -0
  111. package/dist/web-ui/content/modules/1d518ae6ed66fe5a/chunk.js +3 -0
  112. package/dist/web-ui/content/modules/1f93b5bd70381bb6/runtime.js +539 -0
  113. package/dist/web-ui/content/modules/26c68d4decdd18b4/markdown.js +1 -0
  114. package/dist/web-ui/content/modules/2ed8bafd5b4fecc5/workspace-window.js +1 -0
  115. package/dist/web-ui/content/modules/31ffc2f05f87652c/host.js +1 -0
  116. package/dist/web-ui/content/modules/35aaa85b9d0937af/chunk.js +20 -0
  117. package/dist/web-ui/content/modules/3664d0620f124292/chunk.js +3 -0
  118. package/dist/web-ui/content/modules/3cea480770cb184c/chunk.js +1 -0
  119. package/dist/web-ui/content/modules/3d12adf54647c1e2/host-bridge.js +1 -0
  120. package/dist/web-ui/content/modules/459cca2482a9a4ba/host.js +18 -0
  121. package/dist/web-ui/content/modules/474c9647130ef3e9/task-board-host.js +2 -0
  122. package/dist/web-ui/content/modules/4f74d2517f29134c/chunk.js +15 -0
  123. package/dist/web-ui/content/modules/52a218d74002a011/host.js +30 -0
  124. package/dist/web-ui/content/modules/54f2b10659ef8475/employee-card.js +1 -0
  125. package/dist/web-ui/content/modules/703fb1c670f7b199/chunk.js +1 -0
  126. package/dist/web-ui/content/modules/75511da23f051718/host.js +1 -0
  127. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-2IVIZ2YC.js → 799b19da948bec8d/host.js} +3 -2
  128. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-XNIR3NTJ.js → 7ef73b6c20d2504f/host.js} +3 -2
  129. package/dist/web-ui/content/modules/8f8f7a0a8874d29a/host.js +1 -0
  130. package/dist/web-ui/content/modules/90f2cd73cdf3a2cd/chat-render.js +25 -0
  131. package/dist/web-ui/content/modules/9510efce6caaa9ec/chunk.js +1 -0
  132. package/dist/web-ui/content/modules/9a0e65188739957f/chunk.js +67 -0
  133. package/dist/web-ui/content/modules/9eae1033f47e4526/host.js +1 -0
  134. package/dist/web-ui/content/modules/a2c5b246e57fa165/panel.js +1 -0
  135. package/dist/web-ui/content/modules/a50ce28f45bfeb2e/host.js +1 -0
  136. package/dist/web-ui/content/modules/adfc53789d5bedb2/chunk.js +1 -0
  137. package/dist/web-ui/content/modules/bcecb0cc14b7d2bd/chunk.js +1 -0
  138. package/dist/web-ui/content/modules/c4706f47c23f33e4/host.js +1 -0
  139. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-Q524TMSR.js → c7de873095c061ab/host.js} +4 -3
  140. package/dist/web-ui/content/modules/d789f84a3f8765ae/page.js +6 -0
  141. package/dist/web-ui/content/modules/db4e2dbc7aed6353/chunk.js +1 -0
  142. package/dist/web-ui/content/modules/e4fd9906521b4aa3/chunk.js +1 -0
  143. package/dist/web-ui/content/modules/eb95c3ee0d42827a/chunk.js +6 -0
  144. package/dist/web-ui/content/modules/{0051bd3d55f10ab7/host-PHHNGVNL.js → ec68d93a95c1d5d6/host.js} +2 -2
  145. package/dist/web-ui/content/modules/f33ed89d1ccd3a2d/chunk.js +1 -0
  146. package/dist/web-ui/content/modules/f6502dab121f9d91/chunk.js +1 -0
  147. package/dist/web-ui/content/modules/fac83e61c0a8c8a7/host.js +1 -0
  148. package/dist/web-ui/content/scripts.js +1 -1
  149. package/dist/web-ui/embedded-assets.d.ts +200 -27
  150. package/dist/web-ui/embedded-assets.js +5 -4
  151. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +5 -0
  152. package/dist/web-ui/react/workspaces/sidebar-manage.js +27 -3
  153. package/dist/web-ui/running-activity.d.ts +22 -9
  154. package/dist/web-ui/running-activity.js +58 -42
  155. package/dist/web-ui/running-current-action.d.ts +12 -0
  156. package/dist/web-ui/running-current-action.js +74 -0
  157. package/dist/web-ui/scripts.d.ts +4 -0
  158. package/dist/web-ui/scripts.js +17 -8
  159. package/package.json +4 -2
  160. package/scripts/recover-web-update.mjs +26 -19
  161. package/scripts/recover-web-update.sh +9 -6
  162. package/scripts/update-node.sh +3 -1
  163. package/skills/wand-system/SKILL.md +69 -0
  164. package/skills/wand-system/references/operations.md +100 -0
  165. package/skills/wand-system/scripts/wand.mjs +34 -0
  166. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chat-render-RY2I2CF3.js +0 -24
  167. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-2F6J2MKX.js +0 -1
  168. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-6C6A2RHG.js +0 -1
  169. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-7AFM245W.js +0 -1
  170. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-ECWYA3U5.js +0 -1
  171. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-MHX2QXIS.js +0 -111
  172. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RAAPETNH.js +0 -1
  173. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-RXKBHYUR.js +0 -38
  174. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-T35D57FW.js +0 -1
  175. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-U4EDP4TI.js +0 -1
  176. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-WHXQR7JT.js +0 -20
  177. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YOXXNXOX.js +0 -2
  178. package/dist/web-ui/content/modules/0051bd3d55f10ab7/chunk-YQJ6XYV7.js +0 -1
  179. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +0 -1
  180. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-2AQAIUNO.js +0 -1
  181. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-AZXNFCRW.js +0 -1
  182. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-EPEQX22R.js +0 -1
  183. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-FZYIMOLC.js +0 -1
  184. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-G6GL4SNL.js +0 -1
  185. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-PEILC5NU.js +0 -1
  186. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +0 -69
  187. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-WOQRK6NG.js +0 -1
  188. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-XRVOIEP6.js +0 -1
  189. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +0 -1
  190. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +0 -1
  191. package/dist/web-ui/content/modules/0051bd3d55f10ab7/page-PY4JUL4W.js +0 -3
  192. package/dist/web-ui/content/modules/0051bd3d55f10ab7/panel-BBQFC55Q.js +0 -1
  193. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +0 -974
  194. package/dist/web-ui/content/modules/0051bd3d55f10ab7/sha256-ANXV72UC.js +0 -1
  195. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +0 -2
  196. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +0 -1
  197. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-window-MNMKSOBG.js +0 -1
@@ -7,6 +7,30 @@ function failure(res, error) {
7
7
  /** Authentication and the sessions capability are enforced by the server route family. */
8
8
  export function registerScheduledTaskRoutes(app, service) {
9
9
  app.get("/api/scheduled-tasks", (req, res) => { res.json({ tasks: service.list(req.query.includeCancelled === "1") }); });
10
+ app.get("/api/scheduled-tasks/calendar", (req, res) => {
11
+ try {
12
+ res.json(service.calendar(req.query.from, req.query.to, req.query.timeZone));
13
+ }
14
+ catch (error) {
15
+ failure(res, error);
16
+ }
17
+ });
18
+ app.get("/api/scheduled-tasks/:id/activity", (req, res) => {
19
+ try {
20
+ res.json(service.activity(req.params.id, req.query.timeZone));
21
+ }
22
+ catch (error) {
23
+ failure(res, error);
24
+ }
25
+ });
26
+ app.get("/api/scheduled-tasks/:id/runs/:runId", (req, res) => {
27
+ try {
28
+ res.json({ run: service.getRun(req.params.id, req.params.runId) });
29
+ }
30
+ catch (error) {
31
+ failure(res, error);
32
+ }
33
+ });
10
34
  app.get("/api/scheduled-tasks/:id", (req, res) => {
11
35
  try {
12
36
  res.json(service.get(req.params.id));
@@ -11,6 +11,7 @@ import { isProviderSessionId } from "./resume-policy.js";
11
11
  import { sendRouteError } from "./server-request.js";
12
12
  import { isSessionProvider } from "./session-provider.js";
13
13
  import { selectEmployeeCandidate } from "./silicon-employee-dispatch.js";
14
+ import { parseConversationExecutionPreference, withConversationExecutionPreference } from "./conversation-execution-preference.js";
14
15
  import { projectCwdForSession, resolveWorkspaceIdForNewSession } from "./workspace-binding.js";
15
16
  import { badRequest, surfaceDomainError } from "./http-error.js";
16
17
  import { getInputDebugMeta, startResumedPtySession, restoreArchivedSession } from "./session-service.js";
@@ -36,7 +37,11 @@ export function registerSessionLifecycleRoutes(app, ctx) {
36
37
  if (employeeId && (!employee || employee.archivedAt))
37
38
  throw badRequest("硅基员工不存在或已归档。");
38
39
  const selectedEmployeeCandidate = employee ? selectEmployeeCandidate(employee) : null;
39
- const employeeAgent = selectedEmployeeCandidate?.agent;
40
+ const manualPreference = body.overrideCli === true ? parseConversationExecutionPreference({
41
+ provider: body.provider, engine: body.engine, model: body.model || "default", thinkingEffort: body.thinkingEffort || "off",
42
+ }) : undefined;
43
+ const executionEmployee = employee && manualPreference ? withConversationExecutionPreference(employee, manualPreference) : employee;
44
+ const employeeAgent = manualPreference && executionEmployee ? executionEmployee.agents[0] : selectedEmployeeCandidate?.agent;
40
45
  if (body.provider && !isSessionProvider(body.provider)) {
41
46
  res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder 或 Pi provider。" });
42
47
  return;
@@ -92,8 +97,8 @@ export function registerSessionLifecycleRoutes(app, ctx) {
92
97
  // 角色与规则走系统提示通道,不拼进 prompt(见 AGENTS.md「Session 输入契约」)。
93
98
  systemPrompt: role?.prompt ?? body.systemPrompt,
94
99
  ...sessionEmployeeFields(role),
95
- employeeCandidates: employee?.agents,
96
- employeeCandidateIndex: requestedProvider ? undefined : selectedEmployeeCandidate?.index,
100
+ employeeCandidates: executionEmployee?.agents,
101
+ employeeCandidateIndex: manualPreference ? 0 : selectedEmployeeCandidate?.index,
97
102
  ...(engineDecision ? { engine: engineDecision.engine } : {}),
98
103
  ...origin,
99
104
  });
@@ -1,3 +1,4 @@
1
+ import { isSystemAiCall, SYSTEM_AI_READ_ONLY } from "./system-ai-call.js";
1
2
  import { registerPiRecommendationRoute } from "./server-pi-recommendation-routes.js";
2
3
  export { parseExecutionMode, parseSessionCreationOrigin } from "./session-request.js";
3
4
  export * from "./session-service.js";
@@ -10,6 +11,23 @@ import { registerSessionWorktreeRoutes } from "./server-session-worktree-routes.
10
11
  import { createSessionRouteContext } from "./server-session-context.js";
11
12
  /** Registers all `/api/sessions` resource families. Order inside each family is preserved. */
12
13
  export function registerSessionRoutes(app, deps) {
14
+ // Historical system calls have no interactive runner. Guard every mutation family, not only Send.
15
+ for (const prefix of ["/api/sessions/:id", "/api/structured-sessions/:id"]) {
16
+ app.use(prefix, (req, res, next) => {
17
+ const session = deps.sessions.getSlim(req.params.id);
18
+ if (!isSystemAiCall(session) || req.method === "GET" || req.method === "HEAD") {
19
+ next();
20
+ return;
21
+ }
22
+ const allowed = req.method === "POST" && ["/archive", "/unarchive", "/completion/view"].includes(req.path)
23
+ || req.method === "DELETE" && req.path === "/" && session?.status !== "running";
24
+ if (allowed) {
25
+ next();
26
+ return;
27
+ }
28
+ res.status(409).json({ error: SYSTEM_AI_READ_ONLY, code: "system_call_read_only" });
29
+ });
30
+ }
13
31
  registerPiRecommendationRoute(app, { structured: deps.structured, sessions: deps.sessions, config: deps.config, decisions: deps.decisions });
14
32
  const ctx = createSessionRouteContext(deps);
15
33
  registerSessionListRoutes(app, ctx);
@@ -110,7 +110,7 @@ export function registerSessionWorktreeRoutes(app, ctx) {
110
110
  }
111
111
  const body = (req.body ?? {});
112
112
  try {
113
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
113
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free, { storage });
114
114
  // 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
115
115
  const context = await iterationCommitInput(storage, snapshot, body);
116
116
  const result = await runQuickCommitWithFallback({
@@ -194,7 +194,7 @@ export function registerSessionWorktreeRoutes(app, ctx) {
194
194
  }
195
195
  const body = (req.body ?? {});
196
196
  try {
197
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
197
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free, { storage });
198
198
  const context = await iterationCommitInput(storage, snapshot, body);
199
199
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
200
200
  ...ai,
@@ -231,7 +231,7 @@ export function registerSessionWorktreeRoutes(app, ctx) {
231
231
  }
232
232
  const body = (req.body ?? {});
233
233
  try {
234
- const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free);
234
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free, { storage });
235
235
  const result = await runTagHead({
236
236
  cwd: snapshot.cwd,
237
237
  language: config.language ?? "",
@@ -3,6 +3,7 @@ import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { WandStorage } from "./storage.js";
4
4
  import { type LayoutNode, type TaskWindowLayout } from "./types.js";
5
5
  import { type AutoTaskTitleOptions } from "./task-title-service.js";
6
+ import type { ConversationGroupingJudge } from "./conversation-organize.js";
6
7
  import { type TitleJudge } from "./task-title-organize.js";
7
8
  /** 校验并清洗前端提交的布局树;非法输入返回 null(空工作空间)。 */
8
9
  export declare function sanitizeLayout(value: unknown): LayoutNode | null;
@@ -17,4 +18,6 @@ export declare function sanitizeTaskLayout(value: unknown): TaskWindowLayout | n
17
18
  */
18
19
  export declare function registerWorkspaceRoutes(app: Express, storage: WandStorage, sessions?: SessionRegistry, titleOptions?: AutoTaskTitleOptions, organize?: {
19
20
  judge?: TitleJudge;
21
+ conversationJudge?: ConversationGroupingJudge;
22
+ summarize?: (text: string, index: number, total: number, signal: AbortSignal) => Promise<string>;
20
23
  }): void;
@@ -20,6 +20,8 @@ import { firstLayoutTabId, wrapLegacyTaskLayout } from "./layout-tree.js";
20
20
  import { parentTaskIdFrom } from "./task-agent-parsing.js";
21
21
  import { taskTitleService, refreshAutoBoardTaskTitles } from "./task-title-service.js";
22
22
  import { organizeSelectedByTitle, titleJudgeForStorage } from "./task-title-organize-service.js";
23
+ import { systemAiSessions } from "./system-ai-session-recorder.js";
24
+ import { registerConversationOrganizeRoutes } from "./server-conversation-organize-routes.js";
23
25
  import { TITLE_ORGANIZE_MAX_SESSIONS, TITLE_ORGANIZE_MAX_TASKS } from "./task-title-organize.js";
24
26
  import { provisionalTaskTitleFromDescription } from "./task-title.js";
25
27
  import { badRequest, surfaceDomainError } from "./http-error.js";
@@ -326,6 +328,12 @@ export function sanitizeTaskLayout(value) {
326
328
  */
327
329
  export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {}, organize = {}) {
328
330
  taskTitleService(storage, titleOptions);
331
+ registerConversationOrganizeRoutes(app, storage, {
332
+ titleOptions, judge: organize.conversationJudge, summarize: organize.summarize,
333
+ listSessions: () => sessions?.listSlim() ?? storage.loadSessionsSlim(),
334
+ getSession: id => sessions?.get(id) ?? storage.getSession(id),
335
+ refreshSession: id => sessions?.refreshSessionWorkspace(id),
336
+ });
329
337
  // 列出所有项目(按创建时间降序:新建在前,打开不会改位置)
330
338
  app.get("/api/workspaces", (req, res) => {
331
339
  const includeArchived = req.query.includeArchived === "1";
@@ -531,10 +539,15 @@ export function registerWorkspaceRoutes(app, storage, sessions, titleOptions = {
531
539
  res.status(400).json({ error: `一次最多归类 ${TITLE_ORGANIZE_MAX_TASKS} 个任务、${TITLE_ORGANIZE_MAX_SESSIONS} 个会话。` });
532
540
  return;
533
541
  }
542
+ const callSessionIds = [];
534
543
  const result = await organizeSelectedByTitle(storage, { taskIds, sessionIds, mode: body?.mode === "all" ? "all" : "auto" }, {
535
- judge: organize.judge ?? titleJudgeForStorage(storage, titleOptions),
544
+ judge: organize.judge ?? titleJudgeForStorage(storage, titleOptions, id => callSessionIds.push(id)),
536
545
  refreshSession: (sessionId) => sessions?.refreshSessionWorkspace(sessionId),
537
546
  });
547
+ for (const id of callSessionIds)
548
+ systemAiSessions(storage).appendOutcome(id, "实际任务归类结果", result);
549
+ res.setHeader("X-Wand-System-Call-Ids", callSessionIds.slice(0, 64).join(","));
550
+ res.setHeader("X-Wand-System-Call-Count", String(callSessionIds.length));
538
551
  if (!result.foundTasks && body?.target === undefined) {
539
552
  res.status(404).json({ error: "未找到所选任务。" });
540
553
  return;
package/dist/server.js CHANGED
@@ -23,6 +23,7 @@ import { coreHarnessAgentDir } from "./harness-engine.js";
23
23
  import { SessionRegistry } from "./session-registry.js";
24
24
  import { SessionCompletionTracker } from "./session-completion.js";
25
25
  import { resolveSystemAiContext } from "./session-ai-context.js";
26
+ import { systemAiSessions } from "./system-ai-session-recorder.js";
26
27
  import { StructuredSessionManager } from "./structured-session-manager.js";
27
28
  import { SettingsWebAccess, registerSettingsWebAccessRoute } from "./settings-web-access.js";
28
29
  import { DecisionService } from "./decision-service.js";
@@ -62,6 +63,9 @@ import { forwardConversationRelay, registerConversationRoutes } from "./server-c
62
63
  import { registerAiTeamRoutes } from "./server-ai-team-routes.js";
63
64
  import { registerTeamDispatchRoutes } from "./server-team-dispatch-routes.js";
64
65
  import { registerSiliconEmployeeRoutes } from "./server-employee-routes.js";
66
+ import { EmployeeImService } from "./employee-im.js";
67
+ import { EmployeeImLifecycle } from "./employee-im-lifecycle.js";
68
+ import { registerEmployeeImRoutes } from "./server-employee-im-routes.js";
65
69
  import { defaultRoleForCli } from "./default-employee.js";
66
70
  import { UserMemoryService } from "./user-memory.js";
67
71
  import { registerUserMemoryRoutes, userMemoryOperationLog } from "./server-user-memory.js";
@@ -107,7 +111,7 @@ const RUNTIME_ROOT_DIR = path.resolve(SERVER_MODULE_DIR, "..");
107
111
  const PKG_JSON = JSON.parse(readFileSync(path.join(RUNTIME_ROOT_DIR, "package.json"), "utf8"));
108
112
  const PKG_NAME = PKG_JSON.name;
109
113
  const PKG_VERSION = PKG_JSON.version;
110
- const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=24.21.0";
114
+ const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=22.19.0";
111
115
  const PKG_REPO_URL = "https://github.com/co0ontty/wand";
112
116
  // ── Update check cache ──
113
117
  const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
@@ -240,6 +244,8 @@ export async function startServer(config, configPath, options = {}) {
240
244
  // honoured for the configured proxy hops (see resolveTrustProxy).
241
245
  app.set("trust proxy", resolveTrustProxy(config.trustProxy));
242
246
  const storage = new WandStorage(resolveDatabasePath(configPath));
247
+ const systemCalls = systemAiSessions(storage);
248
+ systemCalls.recoverInterrupted();
243
249
  if (!storage.getConfigValue("migration:session-workspaces:v1")) {
244
250
  backfillSessionWorkspaces(storage);
245
251
  storage.setConfigValue("migration:session-workspaces:v1", "done");
@@ -248,12 +254,14 @@ export async function startServer(config, configPath, options = {}) {
248
254
  const runtimeConfig = new RuntimeConfigState(config);
249
255
  const openRouter = new OpenRouterFreeModelsService(storage, options.openRouterFetch);
250
256
  const speechPolisher = new SpeechPolisherService({ config, free: openRouter,
251
- employee: () => storage.getSystemSiliconEmployee(SPEECH_POLISHER_KEY) });
257
+ employee: () => storage.getSystemSiliconEmployee(SPEECH_POLISHER_KEY),
258
+ trace: employee => systemCalls.forEmployee(employee) });
252
259
  const authService = new AuthService(storage);
253
260
  const settingsAccess = new SettingsWebAccess();
254
261
  const decisions = new DecisionService(config.localDecision);
255
262
  const decisionExpert = new DecisionExpertService({ local: decisions, free: openRouter, config,
256
- employee: () => storage.getSystemSiliconEmployee(DECISION_EXPERT_KEY) });
263
+ employee: () => storage.getSystemSiliconEmployee(DECISION_EXPERT_KEY),
264
+ trace: employee => systemCalls.forEmployee(employee) });
257
265
  const getModelRefreshOptions = () => {
258
266
  const injected = options.modelRefreshOptions?.() ?? {};
259
267
  const currentDefaults = readProviderModelPreferences(config, storage);
@@ -317,6 +325,7 @@ export async function startServer(config, configPath, options = {}) {
317
325
  let autoAssignEvaluate;
318
326
  const processes = new ProcessManager(config, storage, configDir, terminalHost, { resolveDecisionEvaluate: () => autoAssignEvaluate, free: openRouter });
319
327
  const structuredLogger = new SessionLogger(configDir, config.shortcutLogMaxBytes);
328
+ systemCalls.attach({ logger: structuredLogger });
320
329
  // Production startup provides a daemon-backed host for structured CLI runs
321
330
  // even when Render owns every PTY. In-process hosts are for test injection.
322
331
  const legacyStructuredHost = ptyHosts.legacyHost
@@ -327,6 +336,16 @@ export async function startServer(config, configPath, options = {}) {
327
336
  // probes the in-process harness; CLI startup/settings must not depend on it.
328
337
  const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
329
338
  const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id));
339
+ let nextEmployeeImWarningAt = 0;
340
+ const employeeIm = new EmployeeImService(storage, { onFailure: () => {
341
+ if (Date.now() < nextEmployeeImWarningAt)
342
+ return;
343
+ nextEmployeeImWarningAt = Date.now() + 60_000;
344
+ wandWarn("员工 IM 通知发送未确认或队列已满;不会自动重发。");
345
+ } });
346
+ const employeeImLifecycle = new EmployeeImLifecycle({ im: employeeIm, getSession: id => sessionRegistry.get(id),
347
+ getRunSteps: id => storage.listAiTeamSteps(id) });
348
+ employeeImLifecycle.seed(sessionRegistry.listSlim(), storage.listAiTeamRuns());
330
349
  const missions = new Missions(storage, structuredSessions, sessionRegistry);
331
350
  // 任务保留的立即扫描也晚绑定:设置路由先注册,WebSocket 和会话表稍后才就绪。
332
351
  let runTaskRetentionSweep = () => ({
@@ -354,6 +373,7 @@ export async function startServer(config, configPath, options = {}) {
354
373
  selfAuthor: () => userAuthor(config.userProfile),
355
374
  notify: (run) => {
356
375
  conversations?.importRun(run);
376
+ employeeImLifecycle.ingestRun(run);
357
377
  notifyAiTeamRun({ kind: "ai-team-run", runId: run.id, taskId: run.taskId });
358
378
  },
359
379
  notifyLive: notifier.aiTeamLive,
@@ -543,6 +563,7 @@ export async function startServer(config, configPath, options = {}) {
543
563
  free: openRouter,
544
564
  notifyEmployeeChanged: (employeeId) => notifyAiTeamRun({ kind: "silicon-employee-definition", employeeId }),
545
565
  });
566
+ registerEmployeeImRoutes(app, { storage, im: employeeIm, requireAdmin });
546
567
  registerAttentionRoutes(app, { sessions: sessionRegistry, runner: aiTeams });
547
568
  registerConversationRoutes(app, conversationService);
548
569
  registerScheduledTaskRoutes(app, scheduledTasks);
@@ -596,7 +617,7 @@ export async function startServer(config, configPath, options = {}) {
596
617
  if (snapshot?.cwd)
597
618
  cwd = snapshot.cwd;
598
619
  if (snapshot)
599
- ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), openRouter);
620
+ ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), openRouter, { storage });
600
621
  }
601
622
  if (!ai) {
602
623
  const defaultSession = {
@@ -607,13 +628,19 @@ export async function startServer(config, configPath, options = {}) {
607
628
  selectedModel: null,
608
629
  thinkingEffort: config.defaultThinkingEffort,
609
630
  };
610
- ai = resolveSystemAiContext(defaultSession, config, storage.getSystemSiliconEmployee(), openRouter);
631
+ ai = resolveSystemAiContext(defaultSession, config, storage.getSystemSiliconEmployee(), openRouter, { storage });
611
632
  }
633
+ const callSessionIds = [];
634
+ ai.onCallSessionCreated = id => callSessionIds.push(id);
612
635
  try {
613
636
  const optimized = await optimizePrompt(text, config.language ?? "", cwd, ai);
637
+ res.setHeader("X-Wand-System-Call-Ids", callSessionIds.slice(0, 64).join(","));
638
+ res.setHeader("X-Wand-System-Call-Count", String(callSessionIds.length));
614
639
  res.json({ optimized });
615
640
  }
616
641
  catch (error) {
642
+ res.setHeader("X-Wand-System-Call-Ids", callSessionIds.slice(0, 64).join(","));
643
+ res.setHeader("X-Wand-System-Call-Count", String(callSessionIds.length));
617
644
  if (error instanceof PromptOptimizeError) {
618
645
  const status = error.code === "EMPTY_INPUT" || error.code === "INPUT_TOO_LONG" ? 400 : 500;
619
646
  res.status(status).json({ error: error.message, errorCode: error.code });
@@ -786,6 +813,7 @@ export async function startServer(config, configPath, options = {}) {
786
813
  sessionCompletions.ingest(event);
787
814
  missions.ingest(event);
788
815
  aiTeams.ingest(event);
816
+ employeeImLifecycle.ingest(event);
789
817
  wsManager.emitEvent(event);
790
818
  });
791
819
  structuredSessions.setEventEmitter((event) => {
@@ -795,9 +823,11 @@ export async function startServer(config, configPath, options = {}) {
795
823
  sessionCompletions.ingest(event);
796
824
  missions.ingest(event);
797
825
  aiTeams.ingest(event);
826
+ employeeImLifecycle.ingest(event);
798
827
  wsManager.emitEvent(event);
799
828
  });
800
829
  notifier.attach((event) => wsManager.emitEvent(event));
830
+ systemCalls.attach({ emit: event => wsManager.emitEvent(event), notifyConversation: notifier.conversationSession });
801
831
  // Re-attach structured CLI runs that kept going inside terminald while the
802
832
  // previous web process was down; fire-and-forget, failures are logged inside.
803
833
  void structuredSessions.recoverDetachedRuns();
@@ -875,6 +905,8 @@ export async function startServer(config, configPath, options = {}) {
875
905
  await new Promise((resolve, reject) => {
876
906
  const cleanupFailedListen = async () => {
877
907
  shuttingDown = true;
908
+ employeeImLifecycle.dispose();
909
+ employeeIm.dispose();
878
910
  await scheduledTasks.dispose();
879
911
  decisions.dispose();
880
912
  openRouter.dispose();
@@ -1135,6 +1167,8 @@ export async function startServer(config, configPath, options = {}) {
1135
1167
  return closePromise;
1136
1168
  closePromise = (async () => {
1137
1169
  shuttingDown = true;
1170
+ employeeImLifecycle.dispose();
1171
+ employeeIm.dispose();
1138
1172
  taskTitles.dispose();
1139
1173
  modelCatalog.dispose();
1140
1174
  const teamShutdown = aiTeams.dispose();
@@ -2,6 +2,8 @@ import type { EmployeeTextDeps } from "./employee-text.js";
2
2
  import type { ModelGroup } from "./model-groups.js";
3
3
  import type { SiliconEmployee } from "./ai-team-types.js";
4
4
  import type { AiCliCandidate, SessionProvider, SessionSnapshot, WandConfig } from "./types.js";
5
+ import type { WandStorage } from "./storage.js";
6
+ import type { AiCallTraceFactory, SystemAiPurpose } from "./system-ai-call.js";
5
7
  export interface SessionAiContext {
6
8
  provider: SessionProvider;
7
9
  model?: string;
@@ -15,6 +17,9 @@ export interface SessionAiContext {
15
17
  /** 系统应用必须经员工渠道;候选为空时禁止退回当前会话/默认 provider。 */
16
18
  employeeChannelOnly?: boolean;
17
19
  modelGroups?: ModelGroup[];
20
+ purpose?: SystemAiPurpose;
21
+ traceFactory?: AiCallTraceFactory;
22
+ onCallSessionCreated?(sessionId: string): void;
18
23
  }
19
24
  /**
20
25
  * Resolve the provider from every representation used by current and legacy
@@ -25,4 +30,9 @@ export declare function resolveSessionProvider(snapshot: Pick<SessionSnapshot, "
25
30
  /** Build the provider-specific settings used by session-adjacent AI actions. */
26
31
  export declare function resolveSessionAiContext(snapshot: Pick<SessionSnapshot, "provider" | "structuredState" | "runner" | "command" | "selectedModel" | "thinkingEffort">, config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultThinkingEffort" | "inheritEnv" | "modelGroups">): SessionAiContext;
27
32
  /** Build the source order for Wand-owned AI features such as titles. */
28
- export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0], config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAiCli" | "systemAiModel"> & Partial<Pick<WandConfig, "defaultCwd" | "harness">>, systemEmployee?: SiliconEmployee | null, free?: EmployeeTextDeps["free"]): SessionAiContext;
33
+ export declare function resolveSystemAiContext(snapshot: Parameters<typeof resolveSessionAiContext>[0] & {
34
+ id?: string;
35
+ }, config: Parameters<typeof resolveSessionAiContext>[1] & Pick<WandConfig, "systemAiCli" | "systemAiModel"> & Partial<Pick<WandConfig, "defaultCwd" | "defaultProvider" | "harness">>, systemEmployee?: SiliconEmployee | null, free?: EmployeeTextDeps["free"], recording?: {
36
+ storage: WandStorage;
37
+ purpose?: SystemAiPurpose;
38
+ }): SessionAiContext;
@@ -2,6 +2,7 @@ import { getDefaultModelForProvider } from "./config.js";
2
2
  import { providerCliInstalled, isSessionProvider } from "./session-provider.js";
3
3
  import { isSystemSiliconEmployee, systemEmployeeCandidates } from "./system-employee.js";
4
4
  import { inferProviderFromCommand, inferProviderFromRunner } from "./session-provider.js";
5
+ import { systemAiSessions } from "./system-ai-session-recorder.js";
5
6
  /**
6
7
  * Resolve the provider from every representation used by current and legacy
7
8
  * sessions. Older persisted sessions may not have the top-level provider, but
@@ -45,8 +46,7 @@ export function resolveSessionAiContext(snapshot, config) {
45
46
  };
46
47
  }
47
48
  /** Build the source order for Wand-owned AI features such as titles. */
48
- export function resolveSystemAiContext(snapshot, config, systemEmployee, free) {
49
- const sessionContext = resolveSessionAiContext(snapshot, config);
49
+ export function resolveSystemAiContext(snapshot, config, systemEmployee, free, recording) {
50
50
  // 候选里的「跟随默认模型」在这里就换成具体的 Wand 默认模型:降级到下一个 provider
51
51
  // 时不能拿上一个 provider 的模型,也不能把决定权交给 CLI 自己的默认值。
52
52
  // Keep the configured chain here; model groups are expanded once at execution.
@@ -54,12 +54,31 @@ export function resolveSystemAiContext(snapshot, config, systemEmployee, free) {
54
54
  ...candidate,
55
55
  model: candidate.model ?? normalizeModel(getDefaultModelForProvider(config, candidate.provider)),
56
56
  }));
57
+ // System work belongs to its configured role, not the source session's possibly removed provider/model.
58
+ const fallbackProvider = isSessionProvider(config.defaultProvider) ? config.defaultProvider
59
+ : isSessionProvider(snapshot.provider) ? snapshot.provider : "pi";
60
+ const sessionContext = systemEmployee ? {
61
+ provider: chain[0]?.provider ?? fallbackProvider,
62
+ model: chain[0]?.model,
63
+ thinkingEffort: config.defaultThinkingEffort,
64
+ inheritEnv: config.inheritEnv,
65
+ ...(config.modelGroups?.length ? { modelGroups: config.modelGroups } : {}),
66
+ } : resolveSessionAiContext(snapshot, config);
57
67
  const owned = {
58
68
  ...sessionContext,
59
69
  ...(chain.length ? { cliCandidates: chain } : {}),
60
70
  ...(chain.some(candidate => candidate.engine === "sdk") ? { employeeText: { config, ...(free ? { free } : {}) } } : {}),
61
71
  ...(systemEmployee ? { employeeChannelOnly: true } : {}),
62
72
  ...(isSystemSiliconEmployee(systemEmployee) ? { opsPersona: systemEmployee.prompt } : {}),
73
+ ...(recording ? {
74
+ employeeChannelOnly: true,
75
+ purpose: recording.purpose,
76
+ traceFactory: { start: (purpose, request, cwd) => {
77
+ if (!systemEmployee)
78
+ throw new Error("系统默认 AI 员工尚未初始化,未执行模型;请检查服务初始化。");
79
+ return systemAiSessions(recording.storage).forEmployee(systemEmployee, snapshot.id).start(purpose, request, cwd);
80
+ } },
81
+ } : {}),
63
82
  };
64
83
  // 内置员工可用时,Wand 自有调用一律按它的候选链:provider/model 取首个已安装的
65
84
  // 候选(两条路都一致),整条链留给运行期降级。
@@ -41,7 +41,7 @@ export class SessionMetadataService {
41
41
  input: prompt,
42
42
  cwd: snapshot.cwd,
43
43
  language: config.language,
44
- ai: resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free),
44
+ ai: resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee(), free, { storage }),
45
45
  readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(snapshot), snapshot.claudeSessionId, snapshot.cwd),
46
46
  hasNativeTitle,
47
47
  onGenerating: (generating) => {
@@ -1,4 +1,6 @@
1
1
  import { isArchiveBlocked } from "./archive-policy.js";
2
+ import { isSystemAiCall } from "./system-ai-call.js";
3
+ import { systemAiSessions } from "./system-ai-session-recorder.js";
2
4
  import { cleanupWorktreeSync } from "./git-worktree.js";
3
5
  import { inferProviderFromCommand } from "./session-provider.js";
4
6
  export function deleteProviderNativeHistory(deleter, provider, providerSessionId, cwd) {
@@ -230,6 +232,10 @@ export class SessionRegistry {
230
232
  const snapshot = this.get(id);
231
233
  if (!snapshot)
232
234
  return null;
235
+ if (isSystemAiCall(snapshot)) {
236
+ systemAiSessions(this.storage).remove(id);
237
+ return snapshot;
238
+ }
233
239
  // Best-effort: tear down the session's isolated worktree (directory +
234
240
  // branch) before removing the session record. Without this, deleting a
235
241
  // session that was never merged leaves an orphan under `.wand-worktrees/`
@@ -1,4 +1,5 @@
1
1
  import { isExecutionMode } from "./config.js";
2
+ import { isSystemAiCall } from "./system-ai-call.js";
2
3
  export function parseExecutionMode(value, fallback) {
3
4
  if (value === undefined)
4
5
  return fallback;
@@ -17,6 +18,8 @@ export function parseSessionCreationOrigin(body) {
17
18
  throw new Error("automationId 必须是非空字符串。");
18
19
  }
19
20
  const automationId = rawAutomationId?.trim();
21
+ if (isSystemAiCall({ automationId }))
22
+ throw new Error("automationId 使用了系统调用的保留命名空间。");
20
23
  if (rawAutomationId !== undefined && !automationId) {
21
24
  throw new Error("automationId 必须是非空字符串。");
22
25
  }
@@ -257,7 +257,7 @@ export async function generateSessionTopic(userMessages, cwd, language, ai = {})
257
257
  ].join("\n"),
258
258
  prompt: `按发送顺序排列的用户消息:\n${input}`,
259
259
  };
260
- return callConfiguredAiText(request, cwd ?? process.cwd(), language ?? "", ai, (raw) => {
260
+ return callConfiguredAiText(request, cwd ?? process.cwd(), language ?? "", { ...ai, purpose: "session-title" }, (raw) => {
261
261
  const topic = parseTopic(raw);
262
262
  if (!topic)
263
263
  throw new Error("模型返回的会话主题格式无效。");
@@ -217,7 +217,7 @@ export async function generateSiliconEmployeeDraft(expectation, ai = {}, options
217
217
  const providers = available.length ? available : [options.preferredProvider ?? "claude", ...SESSION_PROVIDERS];
218
218
  const request = buildEmployeeDraftPrompt(trimmed, providers, options.existingNames);
219
219
  try {
220
- return await callConfiguredAiText(request, options.cwd || process.cwd(), options.language ?? "", ai, (raw) => parseSiliconEmployeeDraft(raw, {
220
+ return await callConfiguredAiText(request, options.cwd || process.cwd(), options.language ?? "", { ...ai, purpose: "employee-draft" }, (raw) => parseSiliconEmployeeDraft(raw, {
221
221
  allowedProviders: providers, fallbackProvider: providers[0], existingNames: options.existingNames,
222
222
  }));
223
223
  }
@@ -1,6 +1,7 @@
1
1
  import type { SiliconEmployee } from "./ai-team-types.js";
2
2
  import type { OpenRouterFreeModelsService } from "./openrouter-free-models.js";
3
3
  import type { WandTaskAgent } from "./task-types.js";
4
+ import { type AiCallTraceFactory } from "./system-ai-call.js";
4
5
  import type { AiTextRequest, WandConfig } from "./types.js";
5
6
  export interface SpeechPolishResult {
6
7
  text: string;
@@ -11,6 +12,7 @@ export interface SpeechPolishResult {
11
12
  optimizationError?: string;
12
13
  }
13
14
  interface SpeechPolisherDeps {
15
+ trace?(employee: SiliconEmployee): AiCallTraceFactory;
14
16
  employee(): SiliconEmployee | null;
15
17
  config: WandConfig;
16
18
  free: Pick<OpenRouterFreeModelsService, "resolveForCall">;
@@ -1,5 +1,6 @@
1
1
  import { abortableEmployeeText, EmployeeCandidateRejected, employeeTextCandidates, prepareEmployeeTextCandidate } from "./employee-text.js";
2
2
  import { SPEECH_POLISHER_ID, SPEECH_POLISHER_KEY } from "./speech-polisher-identity.js";
3
+ import { AiCallTraceError } from "./system-ai-call.js";
3
4
  export function parseSpeechPolishOutput(raw) {
4
5
  if (raw.length > 32_768)
5
6
  throw new Error("口述整理结果过长。");
@@ -33,16 +34,22 @@ export class SpeechPolisherService {
33
34
  const combined = AbortSignal.any([this.lifetime.signal, deadline, ...(signal ? [signal] : [])]);
34
35
  const request = { system: `${employee.prompt}\n输出一个 JSON 对象:{"text":"整理后的完整文字"},不加其他字段或说明。`,
35
36
  prompt: JSON.stringify({ transcript: text }) };
37
+ let trace;
36
38
  try {
39
+ trace = this.deps.trace?.(employee).start("speech-polish", request, this.deps.config.defaultCwd);
37
40
  for (const candidate of employeeTextCandidates(employee.agents, this.deps.config)) {
38
41
  combined.throwIfAborted();
39
42
  const agent = candidate.agent;
40
43
  let generate;
44
+ trace?.candidateStarted(agent);
41
45
  // An unavailable candidate has accepted no user input: continue the configured order.
42
46
  try {
43
- generate = await this.prepareCandidate(agent, request, combined);
47
+ generate = await this.prepareCandidate(agent, request, combined, trace?.sessionId, usage => trace?.usageReported(usage));
44
48
  }
45
- catch {
49
+ catch (error) {
50
+ if (error instanceof AiCallTraceError)
51
+ throw error;
52
+ trace?.candidateFailed(agent, error, "availability");
46
53
  combined.throwIfAborted();
47
54
  continue;
48
55
  }
@@ -53,18 +60,33 @@ export class SpeechPolisherService {
53
60
  raw = await abortableEmployeeText(generate(), combined);
54
61
  }
55
62
  catch (error) {
63
+ if (error instanceof AiCallTraceError)
64
+ throw error;
65
+ trace?.candidateFailed(agent, error, "execution");
56
66
  combined.throwIfAborted();
57
67
  if (error instanceof EmployeeCandidateRejected && error.failure.retryable)
58
68
  continue;
59
69
  throw error;
60
70
  }
61
71
  combined.throwIfAborted();
62
- return { text: parseSpeechPolishOutput(raw), originalText: text, optimized: true, employeeId: employee.id,
72
+ let polished;
73
+ try {
74
+ polished = parseSpeechPolishOutput(raw);
75
+ }
76
+ catch (error) {
77
+ trace?.candidateFailed(agent, error, "result");
78
+ throw error;
79
+ }
80
+ trace?.candidateSucceeded(raw, polished);
81
+ trace?.complete();
82
+ return { text: polished, originalText: text, optimized: true, employeeId: employee.id,
63
83
  candidate: candidate.index };
64
84
  }
85
+ trace?.fail(new Error("NoAvailableCandidate"));
65
86
  return { ...original, optimizationError: "口述整理师的候选均不可用,已保留原始转写。" };
66
87
  }
67
- catch {
88
+ catch (error) {
89
+ trace?.fail(error, combined.aborted);
68
90
  signal?.throwIfAborted();
69
91
  this.lifetime.signal.throwIfAborted();
70
92
  return { ...original, optimizationError: "口述整理失败或超时,已保留原始转写。" };
@@ -73,12 +95,12 @@ export class SpeechPolisherService {
73
95
  this.active -= 1;
74
96
  }
75
97
  }
76
- async prepareCandidate(agent, request, signal) {
98
+ async prepareCandidate(agent, request, signal, callSessionId, onUsage) {
77
99
  const deadline = new AbortController();
78
100
  const timer = setTimeout(() => deadline.abort(new Error("候选可用性检查超时。")), 8000);
79
101
  const checkedSignal = AbortSignal.any([signal, deadline.signal]);
80
102
  try {
81
- return await abortableEmployeeText((this.deps.prepare ?? ((candidate, input, checked) => prepareEmployeeTextCandidate(this.deps, candidate, input, checked)))(agent, request, checkedSignal), checkedSignal);
103
+ return await abortableEmployeeText((this.deps.prepare ?? ((candidate, input, checked) => prepareEmployeeTextCandidate({ ...this.deps, callSessionId, onUsage }, candidate, input, checked)))(agent, request, checkedSignal), checkedSignal);
82
104
  }
83
105
  finally {
84
106
  clearTimeout(timer);
@@ -0,0 +1,13 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+ import type { ConversationOrganizationJob } from "../conversation-organize-contract.js";
3
+ /** Operation receipts, not a second source of tasks or membership. Task mutations share this transaction. */
4
+ export declare class ConversationOrganizationRepository {
5
+ private readonly db;
6
+ constructor(db: DatabaseSync);
7
+ saveConversationOrganizationJob(job: ConversationOrganizationJob, requestToken?: string, scopeKey?: string): void;
8
+ getConversationOrganizationJob(id: string): ConversationOrganizationJob | null;
9
+ getConversationOrganizationJobByToken(token: string): ConversationOrganizationJob | null;
10
+ listRunningConversationOrganizationJobs(): ConversationOrganizationJob[];
11
+ getConversationOrganizationFingerprint(scope: string): string | null;
12
+ saveConversationOrganizationFingerprint(scope: string, fingerprint: string): void;
13
+ }
@@ -0,0 +1,33 @@
1
+ /** Operation receipts, not a second source of tasks or membership. Task mutations share this transaction. */
2
+ export class ConversationOrganizationRepository {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ saveConversationOrganizationJob(job, requestToken, scopeKey) {
8
+ this.db.prepare(`INSERT INTO conversation_organization_jobs(id, request_token, scope_key, status, state_json, updated_at)
9
+ VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET status=excluded.status,
10
+ state_json=excluded.state_json, updated_at=excluded.updated_at`)
11
+ .run(job.id, requestToken ?? job.id, scopeKey ?? "", job.status, JSON.stringify(job), new Date().toISOString());
12
+ }
13
+ getConversationOrganizationJob(id) {
14
+ const row = this.db.prepare("SELECT state_json FROM conversation_organization_jobs WHERE id=?").get(id);
15
+ return row ? JSON.parse(row.state_json) : null;
16
+ }
17
+ getConversationOrganizationJobByToken(token) {
18
+ const row = this.db.prepare("SELECT state_json FROM conversation_organization_jobs WHERE request_token=?").get(token);
19
+ return row ? JSON.parse(row.state_json) : null;
20
+ }
21
+ listRunningConversationOrganizationJobs() {
22
+ return this.db.prepare("SELECT state_json FROM conversation_organization_jobs WHERE status='running'").all().map(row => JSON.parse(row.state_json));
23
+ }
24
+ getConversationOrganizationFingerprint(scope) {
25
+ const row = this.db.prepare("SELECT fingerprint FROM conversation_organization_cache WHERE scope_key=?").get(scope);
26
+ return row?.fingerprint ?? null;
27
+ }
28
+ saveConversationOrganizationFingerprint(scope, fingerprint) {
29
+ this.db.prepare(`INSERT INTO conversation_organization_cache(scope_key,fingerprint,updated_at) VALUES(?,?,?)
30
+ ON CONFLICT(scope_key) DO UPDATE SET fingerprint=excluded.fingerprint, updated_at=excluded.updated_at`)
31
+ .run(scope, fingerprint, new Date().toISOString());
32
+ }
33
+ }