@co0ontty/wand 4.82.0 → 4.83.1

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 (91) hide show
  1. package/README.md +47 -9
  2. package/dist/agent-dispatch.d.ts +3 -2
  3. package/dist/agent-dispatch.js +19 -3
  4. package/dist/ai-team-delivery-types.d.ts +40 -0
  5. package/dist/ai-team-delivery-types.js +1 -0
  6. package/dist/ai-team-delivery.d.ts +13 -0
  7. package/dist/ai-team-delivery.js +89 -0
  8. package/dist/ai-team-employee-binding.d.ts +16 -0
  9. package/dist/ai-team-employee-binding.js +77 -0
  10. package/dist/ai-team-prompts.js +5 -0
  11. package/dist/ai-team-runner.d.ts +9 -2
  12. package/dist/ai-team-runner.js +52 -7
  13. package/dist/ai-team-types.d.ts +17 -3
  14. package/dist/ai-team-types.js +43 -3
  15. package/dist/build-info.json +3 -3
  16. package/dist/cli.js +36 -0
  17. package/dist/config.d.ts +18 -10
  18. package/dist/config.js +63 -105
  19. package/dist/decision-client.d.ts +4 -0
  20. package/dist/decision-client.js +91 -0
  21. package/dist/decision-runner.d.ts +9 -0
  22. package/dist/decision-runner.js +36 -0
  23. package/dist/decision-service.d.ts +46 -0
  24. package/dist/decision-service.js +230 -0
  25. package/dist/decision-skill.d.ts +6 -0
  26. package/dist/decision-skill.js +70 -0
  27. package/dist/decision-tool.d.ts +21 -0
  28. package/dist/decision-tool.js +346 -0
  29. package/dist/decision-types.d.ts +36 -0
  30. package/dist/decision-types.js +123 -0
  31. package/dist/decision-worker.py +86 -0
  32. package/dist/default-employee.d.ts +4 -1
  33. package/dist/default-employee.js +11 -1
  34. package/dist/env-utils.js +3 -0
  35. package/dist/git-quick-commit.js +7 -1
  36. package/dist/message-truncator.d.ts +1 -1
  37. package/dist/message-truncator.js +29 -22
  38. package/dist/models.d.ts +1 -0
  39. package/dist/models.js +31 -4
  40. package/dist/process-manager.d.ts +39 -1
  41. package/dist/process-manager.js +112 -8
  42. package/dist/provider-catalog.d.ts +47 -0
  43. package/dist/provider-catalog.js +138 -0
  44. package/dist/provider-cli-updater.d.ts +1 -1
  45. package/dist/pty-foreground.d.ts +37 -0
  46. package/dist/pty-foreground.js +56 -0
  47. package/dist/pty-turn-activity.d.ts +6 -5
  48. package/dist/pty-turn-activity.js +6 -5
  49. package/dist/retention.d.ts +29 -0
  50. package/dist/retention.js +142 -0
  51. package/dist/server-ai-team-routes.d.ts +2 -1
  52. package/dist/server-ai-team-routes.js +27 -9
  53. package/dist/server-decision-routes.d.ts +10 -0
  54. package/dist/server-decision-routes.js +52 -0
  55. package/dist/server-employee-routes.js +9 -1
  56. package/dist/server-session-routes.d.ts +2 -2
  57. package/dist/server-session-routes.js +97 -1
  58. package/dist/server-settings-routes.js +10 -36
  59. package/dist/server-update-routes.js +2 -1
  60. package/dist/server-workspace-routes.js +8 -0
  61. package/dist/server.js +37 -5
  62. package/dist/session-completion-state.d.ts +26 -0
  63. package/dist/session-completion-state.js +30 -0
  64. package/dist/session-completion.d.ts +12 -0
  65. package/dist/session-completion.js +62 -0
  66. package/dist/session-provider.d.ts +1 -15
  67. package/dist/session-provider.js +4 -54
  68. package/dist/session-registry.d.ts +5 -0
  69. package/dist/session-registry.js +19 -1
  70. package/dist/session-transport.js +2 -0
  71. package/dist/storage.d.ts +12 -0
  72. package/dist/storage.js +119 -16
  73. package/dist/structured-client-protocol.js +36 -1
  74. package/dist/structured-provider-common.js +3 -3
  75. package/dist/structured-session-manager.d.ts +13 -1
  76. package/dist/structured-session-manager.js +196 -27
  77. package/dist/system-employee.js +1 -0
  78. package/dist/task-types.d.ts +4 -2
  79. package/dist/tool-preview.d.ts +8 -0
  80. package/dist/tool-preview.js +158 -0
  81. package/dist/types.d.ts +35 -2
  82. package/dist/web-ui/content/ai-teams.js +13 -13
  83. package/dist/web-ui/content/scripts.js +66 -64
  84. package/dist/web-ui/content/styles.css +1 -1
  85. package/dist/web-ui/content/tailwind.css +3 -0
  86. package/dist/web-ui/embedded-assets.js +3 -3
  87. package/dist/web-ui/provider-identity.d.ts +7 -13
  88. package/dist/web-ui/provider-identity.js +8 -71
  89. package/package.json +3 -2
  90. package/skills/wand-decision/SKILL.md +61 -0
  91. package/skills/wand-decision/scripts/decide.mjs +16 -0
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, } from "./ai-team-types.js";
2
+ import { AI_TEAM_MAX_CANDIDATES, SILICON_EMPLOYEE_AVATAR_MAX_CHARS, agentKey, isBuiltinSiliconEmployee, parseSiliconEmployeeTags, siliconEmployeeTags, } from "./ai-team-types.js";
3
3
  import { getErrorMessage } from "./error-utils.js";
4
4
  import { asyncRoute } from "./express-async.js";
5
5
  import { bodyObject, sendRouteError, text } from "./server-request.js";
@@ -75,6 +75,12 @@ export function parseSystemEmployeeAgents(value, existing) {
75
75
  throw new Error(`该员工是内置的,${field.label}不可修改。`);
76
76
  }
77
77
  }
78
+ if (body.tags !== undefined && JSON.stringify(body.tags) !== JSON.stringify(siliconEmployeeTags(existing))) {
79
+ throw new Error("该员工是内置的,标签不可修改。");
80
+ }
81
+ if (body.systemKey !== undefined && body.systemKey !== existing.systemKey) {
82
+ throw new Error("该员工是内置的,身份不可修改。");
83
+ }
78
84
  if (!Array.isArray(body.agents))
79
85
  throw new Error("请提供执行候选数组。");
80
86
  if (body.agents.length === 0)
@@ -139,6 +145,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
139
145
  throw new Error("执行候选存在重复。");
140
146
  }
141
147
  const avatar = parseAvatar(body.avatar, name);
148
+ const tags = parseSiliconEmployeeTags(body.tags === undefined ? existing?.tags ?? [] : body.tags);
142
149
  const id = existing?.id ?? `e_${randomUUID().replace(/-/g, "")}`;
143
150
  return {
144
151
  id,
@@ -147,6 +154,7 @@ export function parseSiliconEmployeeInput(value, existing, now) {
147
154
  prompt,
148
155
  avatar,
149
156
  agents,
157
+ tags,
150
158
  archivedAt: existing?.archivedAt,
151
159
  createdAt: existing ? existing.createdAt : now,
152
160
  updatedAt: now,
@@ -2,7 +2,7 @@ import type { Express } from "express";
2
2
  import { ProcessManager } from "./process-manager.js";
3
3
  import { StructuredSessionManager } from "./structured-session-manager.js";
4
4
  import { WandStorage } from "./storage.js";
5
- import { ExecutionMode, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
5
+ import { ExecutionMode, ProcessEvent, SessionSnapshot, SessionSource, WandConfig } from "./types.js";
6
6
  import { toSessionListItemDTO } from "./session-transport.js";
7
7
  import { SessionRegistry } from "./session-registry.js";
8
8
  import { type SessionDirectoryNode } from "./session-directory-tree.js";
@@ -45,6 +45,6 @@ export interface SessionDirectoryTreeResponse {
45
45
  }
46
46
  export declare function buildSessionListPage(sessions: SessionSnapshot[], offset: number, limit: number): SessionListPage;
47
47
  export declare function buildSessionDirectoryTree(sessions: SessionSnapshot[], customNames?: ReadonlyMap<string, string>): SessionDirectoryTreeResponse;
48
- export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void): void;
48
+ export declare function registerSessionRoutes(app: Express, processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage, defaultMode: ExecutionMode, config: WandConfig, sessions: SessionRegistry, onSessionCreated?: (cwd: string | undefined | null) => void, onCompletionViewed?: (event: ProcessEvent) => void): void;
49
49
  export declare function registerClaudeHistoryRoutes(app: Express, processes: ProcessManager, storage: WandStorage): void;
50
50
  export {};
@@ -181,6 +181,8 @@ function sessionListRevision(entries, directoryNames = []) {
181
181
  entry.sortTimestamp,
182
182
  entry.session.title,
183
183
  entry.session.status,
184
+ entry.session.completionRevision ?? 0,
185
+ entry.session.viewedCompletionRevision ?? 0,
184
186
  entry.session.ptyBusy === true,
185
187
  entry.session.structuredState?.inFlight === true,
186
188
  ]);
@@ -347,7 +349,7 @@ function canMergeSession(snapshot) {
347
349
  function isMergeActionAllowed(snapshot) {
348
350
  return snapshot.status !== "running";
349
351
  }
350
- export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
352
+ export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated, onCompletionViewed) {
351
353
  const wantsCompactTools = (req) => req.get("X-Wand-Tool-Projection") === "compact" || req.query.compactTools === "1";
352
354
  const sessionResponseDTO = (snapshot, req) => {
353
355
  const enriched = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
@@ -528,6 +530,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
528
530
  sendRouteError(res, error, "无法启动结构化会话。");
529
531
  }
530
532
  }));
533
+ app.post("/api/sessions/:id/provider", (req, res) => {
534
+ const provider = req.body?.provider;
535
+ if (!isSessionProvider(provider)) {
536
+ res.status(400).json({ error: "请选择有效的 CLI 工具。" });
537
+ return;
538
+ }
539
+ const id = req.params.id;
540
+ const owner = sessions.ownerOf(id);
541
+ if (!owner) {
542
+ res.status(404).json({ error: "未找到该会话。" });
543
+ return;
544
+ }
545
+ if (owner !== "structured") {
546
+ res.status(400).json({ error: "只有新建空白结构化对话可以更换工具。" });
547
+ return;
548
+ }
549
+ try {
550
+ res.json(sessionResponseDTO(structured.setSessionProvider(id, provider), req));
551
+ }
552
+ catch (error) {
553
+ sendRouteError(res, error, "切换工具失败。");
554
+ }
555
+ });
531
556
  app.post("/api/sessions/:id/model", (req, res) => {
532
557
  const body = req.body;
533
558
  const rawModel = typeof body?.model === "string" ? body.model.trim() : null;
@@ -1129,6 +1154,24 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1129
1154
  }
1130
1155
  res.json(page);
1131
1156
  }));
1157
+ // Explicit display acknowledgement: reads/prefetch/resync never consume completion state.
1158
+ app.post("/api/sessions/:id/completion/view", (req, res) => {
1159
+ const snapshot = sessions.get(req.params.id);
1160
+ if (!snapshot) {
1161
+ res.status(404).json({ error: "未找到该会话。" });
1162
+ return;
1163
+ }
1164
+ const revision = req.body?.completionRevision;
1165
+ if (!Number.isSafeInteger(revision) || revision <= 0) {
1166
+ res.status(400).json({ error: "完成代次无效。" });
1167
+ return;
1168
+ }
1169
+ const changed = storage.markSessionCompletionViewed(snapshot.id, revision);
1170
+ const completion = storage.getSessionCompletion(snapshot.id);
1171
+ if (changed)
1172
+ onCompletionViewed?.({ type: "status", sessionId: snapshot.id, data: completion });
1173
+ res.json({ ok: true, ...completion });
1174
+ });
1132
1175
  app.get("/api/sessions/:id", (req, res) => {
1133
1176
  const snapshot = sessions.get(req.params.id);
1134
1177
  if (!snapshot) {
@@ -1494,6 +1537,59 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1494
1537
  sendRouteError(res, error, "无法停止会话。");
1495
1538
  }
1496
1539
  });
1540
+ // 归档 / 取消归档会话:只写标记,不杀终端、不删历史;保留期扫描再按 archivedAt 清理。
1541
+ app.post("/api/sessions/:id/archive", (req, res) => {
1542
+ try {
1543
+ const updated = sessions.setArchived(req.params.id, true);
1544
+ if (!updated) {
1545
+ res.status(404).json({ error: "未找到该会话。" });
1546
+ return;
1547
+ }
1548
+ res.json(sessionResponseDTO(updated, req));
1549
+ }
1550
+ catch (error) {
1551
+ sendRouteError(res, error, "无法归档会话。");
1552
+ }
1553
+ });
1554
+ app.post("/api/sessions/:id/unarchive", (req, res) => {
1555
+ try {
1556
+ const updated = sessions.setArchived(req.params.id, false);
1557
+ if (!updated) {
1558
+ res.status(404).json({ error: "未找到该会话。" });
1559
+ return;
1560
+ }
1561
+ res.json(sessionResponseDTO(updated, req));
1562
+ }
1563
+ catch (error) {
1564
+ sendRouteError(res, error, "无法恢复会话。");
1565
+ }
1566
+ });
1567
+ // 批量归档 / 恢复:与 /api/sessions/batch-delete 对称,返回失败项让客户端保留选择。
1568
+ app.post("/api/sessions/batch-archive", (req, res) => {
1569
+ const body = req.body;
1570
+ const ids = Array.isArray(body?.sessionIds)
1571
+ ? body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
1572
+ : [];
1573
+ if (ids.length === 0) {
1574
+ res.status(400).json({ error: "至少提供一个会话 ID。" });
1575
+ return;
1576
+ }
1577
+ const archived = body?.archived !== false;
1578
+ const failed = [];
1579
+ let changed = 0;
1580
+ for (const id of ids) {
1581
+ try {
1582
+ if (sessions.setArchived(id, archived))
1583
+ changed += 1;
1584
+ else
1585
+ failed.push(id);
1586
+ }
1587
+ catch {
1588
+ failed.push(id);
1589
+ }
1590
+ }
1591
+ res.json({ ok: true, archived: changed, ...(failed.length > 0 ? { failed } : {}) });
1592
+ });
1497
1593
  app.delete("/api/sessions/:id", (req, res) => {
1498
1594
  try {
1499
1595
  sessions.deleteWithProviderHistory(req.params.id);
@@ -4,7 +4,8 @@ import { buildInfoPayload } from "./build-info.js";
4
4
  import { buildChildEnv } from "./env-utils.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
6
  import { sendRouteError } from "./server-request.js";
7
- import { getProviderDefaultModels, PREFERENCE_KEYS, saveConfig, writePreferenceToStorage, } from "./config.js";
7
+ import { getProviderDefaultModels, PREFERENCE_KEYS, PROVIDER_MODEL_PREFERENCE_FIELDS, providerModelFields, saveConfig, writePreferenceToStorage, } from "./config.js";
8
+ import { SESSION_PROVIDERS } from "./session-provider.js";
8
9
  import { withConfiguredDefaultModelLabels } from "./models.js";
9
10
  import { DEPLOYMENT_CONFIG_KEYS } from "./runtime-config.js";
10
11
  function publicConfig(config) {
@@ -12,13 +13,7 @@ function publicConfig(config) {
12
13
  const defaultModels = getProviderDefaultModels(config);
13
14
  return {
14
15
  ...safe,
15
- defaultModel: defaultModels.claude,
16
- defaultCodexModel: defaultModels.codex,
17
- defaultOpenCodeModel: defaultModels.opencode,
18
- defaultGrokModel: defaultModels.grok,
19
- defaultQoderModel: defaultModels.qoder,
20
- defaultPiModel: defaultModels.pi,
21
- defaultGeminiModel: defaultModels.gemini,
16
+ ...providerModelFields(defaultModels),
22
17
  defaultModels,
23
18
  };
24
19
  }
@@ -160,20 +155,11 @@ export function registerSettingsRoutes(app, deps) {
160
155
  if (!body.defaultModels || typeof body.defaultModels !== "object" || Array.isArray(body.defaultModels)) {
161
156
  throw new Error("defaultModels 必须是对象。");
162
157
  }
163
- if (Object.hasOwn(body.defaultModels, "claude"))
164
- stagePreference("defaultModel", body.defaultModels.claude);
165
- if (Object.hasOwn(body.defaultModels, "codex"))
166
- stagePreference("defaultCodexModel", body.defaultModels.codex);
167
- if (Object.hasOwn(body.defaultModels, "opencode"))
168
- stagePreference("defaultOpenCodeModel", body.defaultModels.opencode);
169
- if (Object.hasOwn(body.defaultModels, "grok"))
170
- stagePreference("defaultGrokModel", body.defaultModels.grok);
171
- if (Object.hasOwn(body.defaultModels, "qoder"))
172
- stagePreference("defaultQoderModel", body.defaultModels.qoder);
173
- if (Object.hasOwn(body.defaultModels, "pi"))
174
- stagePreference("defaultPiModel", body.defaultModels.pi);
175
- if (Object.hasOwn(body.defaultModels, "gemini"))
176
- stagePreference("defaultGeminiModel", body.defaultModels.gemini);
158
+ for (const provider of SESSION_PROVIDERS) {
159
+ if (Object.hasOwn(body.defaultModels, provider)) {
160
+ stagePreference(PROVIDER_MODEL_PREFERENCE_FIELDS[provider], body.defaultModels[provider]);
161
+ }
162
+ }
177
163
  }
178
164
  for (const field of PREFERENCE_KEYS) {
179
165
  const value = body[field];
@@ -229,13 +215,7 @@ export function registerSettingsRoutes(app, deps) {
229
215
  const defaults = getProviderDefaultModels(config);
230
216
  res.json({
231
217
  ...withConfiguredDefaultModelLabels(cached, defaults),
232
- defaultModel: defaults.claude,
233
- defaultCodexModel: defaults.codex,
234
- defaultOpenCodeModel: defaults.opencode,
235
- defaultGrokModel: defaults.grok,
236
- defaultQoderModel: defaults.qoder,
237
- defaultPiModel: defaults.pi,
238
- defaultGeminiModel: defaults.gemini,
218
+ ...providerModelFields(defaults),
239
219
  defaultModels: defaults,
240
220
  });
241
221
  });
@@ -245,13 +225,7 @@ export function registerSettingsRoutes(app, deps) {
245
225
  const defaults = getProviderDefaultModels(config);
246
226
  res.json({
247
227
  ...withConfiguredDefaultModelLabels(refreshed, defaults),
248
- defaultModel: defaults.claude,
249
- defaultCodexModel: defaults.codex,
250
- defaultOpenCodeModel: defaults.opencode,
251
- defaultGrokModel: defaults.grok,
252
- defaultQoderModel: defaults.qoder,
253
- defaultPiModel: defaults.pi,
254
- defaultGeminiModel: defaults.gemini,
228
+ ...providerModelFields(defaults),
255
229
  defaultModels: defaults,
256
230
  });
257
231
  }
@@ -4,6 +4,7 @@ import { buildInfoPayload } from "./build-info.js";
4
4
  import { getErrorMessage } from "./error-utils.js";
5
5
  import { asyncRoute } from "./express-async.js";
6
6
  import { sendRouteError, text } from "./server-request.js";
7
+ import { isSessionProvider } from "./session-provider.js";
7
8
  import { checkProviderCliUpdates, updateProviderClis, verifyProviderCliUpdateResults, } from "./provider-cli-updater.js";
8
9
  import { absoluteUrl, buildIosInstallPage, buildIosOtaManifest, buildItmsServicesUrl, collectIosOtaBlockers, formatIosAssetSize, inspectIpa, normalizePublicOrigin, publicOriginFromRequest, } from "./ios-ota.js";
9
10
  import { streamFileWithRange } from "./server-file-routes.js";
@@ -405,7 +406,7 @@ export function registerAdminUpdateRoutes(app, deps) {
405
406
  return;
406
407
  }
407
408
  const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
408
- const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "grok" || value === "qoder" || value === "pi" || value === "gemini");
409
+ const ids = rawIds.filter((value) => isSessionProvider(value));
409
410
  state.providerCliUpdateInFlight = true;
410
411
  try {
411
412
  const before = await refreshProviderCliUpdateState(state, config);
@@ -35,8 +35,13 @@ function workspaceSessionSummary(session, names = {}, registry, team = {}) {
35
35
  command: live.command,
36
36
  title: team.chat?.chatTitle ?? workspaceSessionTitle(live, names),
37
37
  status: live.status,
38
+ completionRevision: live.completionRevision ?? 0,
39
+ viewedCompletionRevision: live.viewedCompletionRevision ?? 0,
38
40
  ptyBusy: live.ptyBusy === true,
41
+ archived: live.archived === true,
42
+ archivedAt: live.archivedAt ?? null,
39
43
  providerCliActive: live.providerCliActive,
44
+ providerCliExitCode: live.providerCliExitCode,
40
45
  inFlight: live.structuredState?.inFlight === true,
41
46
  cwd: live.cwd,
42
47
  startedAt: live.startedAt,
@@ -72,6 +77,9 @@ function cheapTasksRevision(storage, registry) {
72
77
  ...(registry?.listSlim() ?? []).map((session) => [
73
78
  session.id,
74
79
  session.status,
80
+ session.completionRevision ?? 0,
81
+ session.viewedCompletionRevision ?? 0,
82
+ session.archived === true ? 1 : 0,
75
83
  session.ptyBusy === true ? 1 : 0,
76
84
  session.providerCliActive ? 1 : 0,
77
85
  session.structuredState?.inFlight === true ? 1 : 0,
package/dist/server.js CHANGED
@@ -16,8 +16,11 @@ import { ModelCatalogService } from "./models.js";
16
16
  import { ProcessManager } from "./process-manager.js";
17
17
  import { SessionLogger } from "./session-logger.js";
18
18
  import { SessionRegistry } from "./session-registry.js";
19
+ import { SessionCompletionTracker } from "./session-completion.js";
19
20
  import { resolveSystemAiContext } from "./session-ai-context.js";
20
21
  import { StructuredSessionManager } from "./structured-session-manager.js";
22
+ import { DecisionService } from "./decision-service.js";
23
+ import { registerDecisionRoutes } from "./server-decision-routes.js";
21
24
  import { recordRecentPath, registerFileRoutes } from "./server-file-routes.js";
22
25
  import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
23
26
  import { registerSettingsRoutes } from "./server-settings-routes.js";
@@ -47,6 +50,7 @@ import { asyncRoute, jsonErrorHandler } from "./express-async.js";
47
50
  import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateChannel, resolveGlobalWandCli, } from "./npm-update-utils.js";
48
51
  import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
49
52
  import { computeRelaunch } from "./relaunch.js";
53
+ import { startRetentionTimer } from "./retention.js";
50
54
  import { RuntimeConfigState } from "./runtime-config.js";
51
55
  import { safeServiceInstalled } from "./tui/runtime-utils.js";
52
56
  import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
@@ -296,6 +300,7 @@ export async function startServer(config, configPath, options = {}) {
296
300
  const storage = new WandStorage(resolveDatabasePath(configPath));
297
301
  const runtimeConfig = new RuntimeConfigState(config);
298
302
  const authService = new AuthService(storage);
303
+ const decisions = new DecisionService(config.localDecision);
299
304
  // 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
300
305
  const getCurrentDefaultModels = () => ({
301
306
  claude: storage.getPreference("pref:defaultModel", config.defaultModel ?? ""),
@@ -313,6 +318,7 @@ export async function startServer(config, configPath, options = {}) {
313
318
  storage,
314
319
  inheritEnv: config.inheritEnv !== false,
315
320
  apiKey: process.env.ANTHROPIC_API_KEY,
321
+ baseUrl: process.env.ANTHROPIC_BASE_URL,
316
322
  ...injected,
317
323
  piEndpointDiscovery: injected.piEndpointDiscovery ?? { enabled: !testMode },
318
324
  configuredClaudeModels: [
@@ -352,8 +358,10 @@ export async function startServer(config, configPath, options = {}) {
352
358
  const legacyStructuredHost = ptyHosts.legacyHost
353
359
  ?? (terminalHost instanceof TerminalDaemonClient ? terminalHost : null);
354
360
  const structuredHosts = await createUpgradeAwareStructuredHost(configPath, legacyStructuredHost, config.structured?.processHost);
355
- const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host);
361
+ let decisionRuntime = null;
362
+ const structuredSessions = new StructuredSessionManager(storage, config, structuredLogger, {}, structuredHosts.host, () => decisionRuntime);
356
363
  const sessionRegistry = new SessionRegistry(processes, structuredSessions, storage);
364
+ const sessionCompletions = new SessionCompletionTracker(storage, (id) => sessionRegistry.get(id));
357
365
  const missions = new Missions(storage, structuredSessions, sessionRegistry);
358
366
  // wsManager 在后面才建:团队运行的变更通知经由这个转发口,接好之前静默丢弃。
359
367
  let notifyAiTeamRun = (_data) => { };
@@ -399,6 +407,7 @@ export async function startServer(config, configPath, options = {}) {
399
407
  registerLocalPreviewRoutes(app, { requireAuth, requireFiles });
400
408
  // Route-specific parsers must run before the global parser. Once body-parser
401
409
  // has consumed a request, a later express.json() cannot tighten or widen it.
410
+ app.use("/api/decisions", express.json({ limit: "32kb" }));
402
411
  app.use("/api/optimize-prompt", express.json({ limit: "256kb" }));
403
412
  app.use("/api/file-write", express.json({ limit: "2mb" }));
404
413
  app.use(express.json({ limit: "1mb" }));
@@ -593,6 +602,7 @@ export async function startServer(config, configPath, options = {}) {
593
602
  app.get("/api/session-check", (req, res) => {
594
603
  res.json({ authed: authService.validateSession(readSessionCookie(req, useHttps)) });
595
604
  });
605
+ registerDecisionRoutes(app, { storage, decisions, requireAuth, requireSessions });
596
606
  app.use("/api", requireAuth);
597
607
  // Connected apps receive only the route families used by native clients and
598
608
  // the browser extension. Browser-admin sessions implicitly satisfy all scopes.
@@ -754,7 +764,7 @@ export async function startServer(config, configPath, options = {}) {
754
764
  }
755
765
  registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode, config, sessionRegistry, (cwd) => {
756
766
  recordRecentPath(storage, cwd);
757
- });
767
+ }, (event) => wsManager.emitEvent(event));
758
768
  registerClaudeHistoryRoutes(app, processes, storage);
759
769
  registerWorkspaceRoutes(app, storage, sessionRegistry, { config });
760
770
  registerMissionRoutes(app, missions);
@@ -833,6 +843,9 @@ export async function startServer(config, configPath, options = {}) {
833
843
  const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
834
844
  const sessionCwd = resolveSessionCwd(body.cwd, config.defaultCwd);
835
845
  const workspaceId = resolveWorkspaceIdForNewSession(storage, sessionCwd, body.workspaceId);
846
+ const role = !interactiveShell && !body.systemPrompt?.trim() && origin.sessionSource === "interactive"
847
+ && (isSessionProvider(body.provider) || inferProviderFromCommand(command))
848
+ ? defaultRoleForCli(storage, provider) : null;
836
849
  const snapshot = await (interactiveShell
837
850
  ? processes.startShell(sessionCwd, body.mode ?? "default", {
838
851
  worktreeEnabled: body.worktreeEnabled === true,
@@ -849,9 +862,10 @@ export async function startServer(config, configPath, options = {}) {
849
862
  cols: reqCols,
850
863
  rows: reqRows,
851
864
  thinkingEffort: body.thinkingEffort ?? config.defaultThinkingEffort,
852
- systemPrompt: body.systemPrompt?.trim() || (origin.sessionSource === "interactive"
853
- && (isSessionProvider(body.provider) || inferProviderFromCommand(command))
854
- ? defaultRoleForCli(storage, provider).prompt : undefined),
865
+ systemPrompt: body.systemPrompt?.trim() || role?.prompt,
866
+ employeeId: role?.id,
867
+ employeeName: role?.name,
868
+ employeeAvatar: role?.avatar,
855
869
  workspaceId,
856
870
  workspaceTaskId: body.workspaceTaskId,
857
871
  ...origin,
@@ -937,11 +951,13 @@ export async function startServer(config, configPath, options = {}) {
937
951
  });
938
952
  // Wire process events to WebSocket broadcast
939
953
  processes.on("process", (event) => {
954
+ sessionCompletions.ingest(event);
940
955
  missions.ingest(event);
941
956
  aiTeams.ingest(event);
942
957
  wsManager.emitEvent(event);
943
958
  });
944
959
  structuredSessions.setEventEmitter((event) => {
960
+ sessionCompletions.ingest(event);
945
961
  missions.ingest(event);
946
962
  aiTeams.ingest(event);
947
963
  wsManager.emitEvent(event);
@@ -1002,6 +1018,7 @@ export async function startServer(config, configPath, options = {}) {
1002
1018
  await new Promise((resolve, reject) => {
1003
1019
  const cleanupFailedListen = () => {
1004
1020
  shuttingDown = true;
1021
+ decisions.dispose();
1005
1022
  try {
1006
1023
  processes.dispose();
1007
1024
  }
@@ -1052,6 +1069,11 @@ export async function startServer(config, configPath, options = {}) {
1052
1069
  const address = server.address();
1053
1070
  const actualPort = typeof address === "object" && address ? address.port : config.port;
1054
1071
  bindAddr = `${config.host}:${actualPort}`;
1072
+ if (config.localDecision?.enabled) {
1073
+ const localHost = config.host === "::1" ? "[::1]" : config.host === "0.0.0.0" || config.host === "::" ? "127.0.0.1" : config.host;
1074
+ decisionRuntime = { url: `${protocol}://${localHost}:${actualPort}`,
1075
+ ...(activeSslCertPath ? { caPath: activeSslCertPath } : {}) };
1076
+ }
1055
1077
  const scheme = useHttps ? "HTTPS" : "HTTP";
1056
1078
  // 主 URL:本机回环;若绑定 0.0.0.0 再补一个对外提示。
1057
1079
  collectedUrls.push({ url: `${protocol}://127.0.0.1:${actualPort}`, scheme });
@@ -1095,6 +1117,11 @@ export async function startServer(config, configPath, options = {}) {
1095
1117
  }, MODEL_CATALOG_AUTO_REFRESH_INTERVAL_MS);
1096
1118
  modelCatalogRefreshTimer.unref();
1097
1119
  }
1120
+ // 保留期扫描:7 天没活动的会话/任务自动归档,归档 7 天后自动清理。
1121
+ let retentionTimer = null;
1122
+ if (!testMode) {
1123
+ retentionTimer = startRetentionTimer({ storage, sessions: sessionRegistry });
1124
+ }
1098
1125
  // Express 4 does not forward rejected route promises automatically. Every
1099
1126
  // async route above is wrapped with asyncRoute, and this final middleware
1100
1127
  // keeps parser, synchronous middleware, and async failures JSON-shaped.
@@ -1241,6 +1268,7 @@ export async function startServer(config, configPath, options = {}) {
1241
1268
  return closePromise;
1242
1269
  closePromise = (async () => {
1243
1270
  shuttingDown = true;
1271
+ decisions.dispose();
1244
1272
  await userMemory.dispose();
1245
1273
  if (updateCheckTimer) {
1246
1274
  clearInterval(updateCheckTimer);
@@ -1254,6 +1282,10 @@ export async function startServer(config, configPath, options = {}) {
1254
1282
  clearInterval(modelCatalogRefreshTimer);
1255
1283
  modelCatalogRefreshTimer = null;
1256
1284
  }
1285
+ if (retentionTimer) {
1286
+ clearInterval(retentionTimer);
1287
+ retentionTimer = null;
1288
+ }
1257
1289
  // Stop accepting requests first. Existing requests get a short grace
1258
1290
  // period while managers flush and active runners are cancelled.
1259
1291
  const serverClosed = new Promise((resolve) => {
@@ -0,0 +1,26 @@
1
+ export interface SessionCompletionState {
2
+ /** Monotonic successful-completion generation, owned by storage rather than runner checkpoints. */
3
+ completionRevision?: number;
4
+ viewedCompletionRevision?: number;
5
+ }
6
+ /** Explicit navigation, not a list refresh choosing its preferred/default session. */
7
+ export declare function createSessionCompletionViewIntent(): {
8
+ open(id: string): void;
9
+ clear(): void;
10
+ isOpen(id: string): boolean;
11
+ };
12
+ /** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
13
+ export declare function mergeSessionCompletionState(current: SessionCompletionState | null | undefined, incoming: SessionCompletionState): SessionCompletionState;
14
+ /** A presentation state only: never change the runner's lifecycle or input eligibility. */
15
+ export declare function isSessionJustCompleted(session: SessionCompletionState & {
16
+ status?: string;
17
+ archived?: boolean;
18
+ ptyBusy?: boolean;
19
+ inFlight?: boolean;
20
+ structuredState?: {
21
+ inFlight?: boolean;
22
+ } | null;
23
+ permissionBlocked?: boolean;
24
+ pendingEscalation?: unknown;
25
+ providerCliExitCode?: number | null;
26
+ }): boolean;
@@ -0,0 +1,30 @@
1
+ /** Explicit navigation, not a list refresh choosing its preferred/default session. */
2
+ export function createSessionCompletionViewIntent() {
3
+ let openedId = null;
4
+ return {
5
+ open(id) { openedId = id; },
6
+ clear() { openedId = null; },
7
+ isOpen(id) { return openedId === id; },
8
+ };
9
+ }
10
+ /** Delayed HTTP/WS projections may advance generations, but must never move them backwards. */
11
+ export function mergeSessionCompletionState(current, incoming) {
12
+ if (incoming.completionRevision === undefined && incoming.viewedCompletionRevision === undefined)
13
+ return {};
14
+ return {
15
+ completionRevision: Math.max(current?.completionRevision ?? 0, incoming.completionRevision ?? 0),
16
+ viewedCompletionRevision: Math.max(current?.viewedCompletionRevision ?? 0, incoming.viewedCompletionRevision ?? 0),
17
+ };
18
+ }
19
+ /** A presentation state only: never change the runner's lifecycle or input eligibility. */
20
+ export function isSessionJustCompleted(session) {
21
+ return (session.completionRevision ?? 0) > (session.viewedCompletionRevision ?? 0)
22
+ && !session.archived
23
+ && !session.ptyBusy
24
+ && !session.inFlight
25
+ && !session.structuredState?.inFlight
26
+ && !session.permissionBlocked
27
+ && !session.pendingEscalation
28
+ && !(typeof session.providerCliExitCode === "number" && session.providerCliExitCode !== 0)
29
+ && ["idle", "running", "exited"].includes(session.status ?? "");
30
+ }
@@ -0,0 +1,12 @@
1
+ import type { WandStorage } from "./storage.js";
2
+ import type { ProcessEvent, SessionSnapshot } from "./types.js";
3
+ /** Observe completion facts without combining PTY and structured execution implementations. */
4
+ export declare class SessionCompletionTracker {
5
+ private readonly storage;
6
+ private readonly getSession;
7
+ private readonly busyPty;
8
+ private readonly completedPty;
9
+ private readonly ended;
10
+ constructor(storage: WandStorage, getSession: (id: string) => SessionSnapshot | null);
11
+ ingest(event: ProcessEvent): void;
12
+ }
@@ -0,0 +1,62 @@
1
+ /** Observe completion facts without combining PTY and structured execution implementations. */
2
+ export class SessionCompletionTracker {
3
+ storage;
4
+ getSession;
5
+ busyPty = new Set();
6
+ completedPty = new Set();
7
+ ended = new Map();
8
+ constructor(storage, getSession) {
9
+ this.storage = storage;
10
+ this.getSession = getSession;
11
+ }
12
+ ingest(event) {
13
+ if (event.type === "started") {
14
+ this.busyPty.delete(event.sessionId);
15
+ this.completedPty.delete(event.sessionId);
16
+ this.ended.delete(event.sessionId);
17
+ return;
18
+ }
19
+ if (event.type !== "status" && event.type !== "ended")
20
+ return;
21
+ const data = event.data;
22
+ if (!data)
23
+ return;
24
+ const snapshot = this.getSession(event.sessionId);
25
+ if (!snapshot)
26
+ return;
27
+ let completed = false;
28
+ if ((snapshot.sessionKind ?? "pty") === "structured") {
29
+ if (event.type === "ended" && snapshot.status === "idle" && snapshot.exitCode === 0
30
+ && !snapshot.structuredState?.inFlight && !snapshot.structuredState?.lastError) {
31
+ const key = snapshot.endedAt ?? "";
32
+ completed = !!key && this.ended.get(snapshot.id) !== key;
33
+ if (completed)
34
+ this.ended.set(snapshot.id, key);
35
+ }
36
+ }
37
+ else {
38
+ if (data.ptyBusy === true) {
39
+ this.busyPty.add(snapshot.id);
40
+ this.completedPty.delete(snapshot.id);
41
+ }
42
+ else if (data.ptyBusy === false && this.busyPty.delete(snapshot.id)) {
43
+ completed = !snapshot.permissionBlocked && !snapshot.pendingEscalation
44
+ && snapshot.status === "running"
45
+ && !(typeof snapshot.providerCliExitCode === "number" && snapshot.providerCliExitCode !== 0);
46
+ }
47
+ if ((data.providerCliActive === false && data.providerCliExitCode === 0)
48
+ || (event.type === "ended" && snapshot.status === "exited" && snapshot.exitCode === 0)) {
49
+ completed ||= !this.completedPty.has(snapshot.id);
50
+ }
51
+ if (completed)
52
+ this.completedPty.add(snapshot.id);
53
+ if (event.type === "ended")
54
+ this.busyPty.delete(snapshot.id);
55
+ }
56
+ if (completed) {
57
+ const completion = this.storage.recordSessionCompletion(snapshot.id);
58
+ if (completion)
59
+ Object.assign(data, completion);
60
+ }
61
+ }
62
+ }
@@ -1,21 +1,7 @@
1
1
  import type { SessionProvider } from "./types.js";
2
- /** 全部受支持的会话 provider;顺序与 UI / 派发选择器一致。 */
3
- export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
4
- /** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
5
- export declare function isSessionProvider(value: unknown): value is SessionProvider;
6
- /** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
7
- export declare function providerCliCommand(provider: SessionProvider): string;
2
+ export { SESSION_PROVIDERS, isSessionProvider, providerCliCommand, providerDisplayName, inferProviderFromRunner, inferProviderFromCommand, normalizeProviderId, } from "./provider-catalog.js";
8
3
  /**
9
4
  * 不启动 shell 也能判断某个 provider 的 CLI 是否在 PATH 上(与子进程 PATH 一致)。
10
5
  * 用于多候选降级时跳过没安装的工具,避免白等一次 spawn 失败。
11
6
  */
12
7
  export declare function providerCliInstalled(provider: SessionProvider, pathValue?: string): boolean;
13
- /** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
14
- export declare function inferProviderFromRunner(runner: unknown): SessionProvider | undefined;
15
- /**
16
- * 从启动命令推断 provider;无法识别时返回 undefined,由调用方决定默认值。
17
- *
18
- * 识别的是命令行的第一个 token:`claude`、`npx claude`、任意路径下的 `claude`
19
- * 都算 Claude,其余 provider 要求命令以其 CLI 名开头。
20
- */
21
- export declare function inferProviderFromCommand(command: string): SessionProvider | undefined;