@co0ontty/wand 4.83.1 → 4.84.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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
package/dist/storage.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import crypto from "node:crypto";
2
+ import { CONVERSATION_OWNER } from "./conversation-types.js";
2
3
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { DatabaseSync } from "node:sqlite";
5
6
  import { GLOBAL_WORKSPACE_ID } from "./types.js";
6
7
  import { normalizeSessionDirectory } from "./session-directory-tree.js";
8
+ import { defaultPiCliSessionSettings, defaultPiSessionSettings, patchPiSessionSettings } from "./pi-session-settings.js";
9
+ import { parseHarnessExtensionState } from "./core-extension-host.js";
7
10
  import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider, SESSION_PROVIDERS } from "./session-provider.js";
8
- import { DEFAULT_ITERATION_NAME, DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, normalizeWandTaskAgentMode } from "./task-types.js";
11
+ import { DEFAULT_ITERATION_NAME, DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentEngine, isWandTaskAgentKind, normalizeWandTaskAgentMode } from "./task-types.js";
9
12
  import { firstLayoutTabId } from "./layout-tree.js";
10
13
  import { isUnnamedWorkspaceTaskName } from "./wand-task-sync.js";
11
14
  import { AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_TERMINAL_RUN_STATUSES, aiTeamChatTitle, isBuiltinSiliconEmployee, isTeamMemberRole, memberAgents, parseSiliconEmployeeTags, siliconEmployeeTags } from "./ai-team-types.js";
@@ -54,7 +57,12 @@ export function parseWandTaskAgent(raw) {
54
57
  const mode = normalizeWandTaskAgentMode(provider, value.mode);
55
58
  // kind 同样是后加字段:老数据 / 老客户端没带时按结构化会话读取。
56
59
  const kind = isWandTaskAgentKind(value.kind) ? value.kind : DEFAULT_WAND_TASK_AGENT_KIND;
57
- return { provider, model: model.trim(), thinkingEffort, mode, kind };
60
+ const engine = value.engine === undefined || value.engine === null || value.engine === ""
61
+ ? undefined
62
+ : isWandTaskAgentEngine(value.engine) ? value.engine : null;
63
+ if (engine === null || (engine === "sdk" && (provider !== "pi" || kind !== "structured")))
64
+ return null;
65
+ return { provider, model: model.trim(), thinkingEffort, mode, kind, ...(engine === "sdk" ? { engine } : {}) };
58
66
  }
59
67
  /** `wand_milestones` 行 → 领域对象;名字必须非空,脏行直接跳过。 */
60
68
  function mapWandMilestoneRow(row) {
@@ -190,6 +198,9 @@ function serializeSessionOptions(snapshot) {
190
198
  employeeAvatar: snapshot.employeeAvatar,
191
199
  employeeCandidates: snapshot.employeeCandidates,
192
200
  employeeCandidateIndex: snapshot.employeeCandidateIndex,
201
+ harnessContext: snapshot.harnessContext,
202
+ piSettings: snapshot.piSettings,
203
+ harnessExtensionState: snapshot.harnessExtensionState,
193
204
  };
194
205
  return JSON.stringify(options);
195
206
  }
@@ -267,8 +278,40 @@ function parseSessionOptions(raw) {
267
278
  if (Number.isSafeInteger(parsed.employeeCandidateIndex) && parsed.employeeCandidateIndex >= 0) {
268
279
  options.employeeCandidateIndex = parsed.employeeCandidateIndex;
269
280
  }
281
+ const harnessContext = parseHarnessContext(parsed.harnessContext);
282
+ if (harnessContext)
283
+ options.harnessContext = harnessContext;
284
+ if (parsed.piSettings !== undefined) {
285
+ try {
286
+ options.piSettings = patchPiSessionSettings(defaultPiSessionSettings(), parsed.piSettings);
287
+ }
288
+ catch { /* Invalid old settings do not enable optional capabilities. */ }
289
+ }
290
+ const extensions = parseHarnessExtensionState(parsed.harnessExtensionState);
291
+ if (extensions)
292
+ options.harnessExtensionState = extensions;
270
293
  return options;
271
294
  }
295
+ /** core 引擎压缩状态:形状不对就当没有压缩过,不让旧数据把会话读坏。 */
296
+ const HARNESS_SUMMARY_MAX_CHARS = 20_000;
297
+ function parseHarnessContext(raw) {
298
+ if (!isRecord(raw))
299
+ return undefined;
300
+ const { summary, fromTurnIndex, tokensBefore, compactions } = raw;
301
+ if (typeof summary !== "string" || summary.length === 0)
302
+ return undefined;
303
+ const boundedFrom = typeof fromTurnIndex === "number" && Number.isSafeInteger(fromTurnIndex) && fromTurnIndex > 0
304
+ ? fromTurnIndex
305
+ : undefined;
306
+ if (boundedFrom === undefined)
307
+ return undefined;
308
+ return {
309
+ summary: summary.slice(0, HARNESS_SUMMARY_MAX_CHARS),
310
+ fromTurnIndex: boundedFrom,
311
+ tokensBefore: typeof tokensBefore === "number" && Number.isFinite(tokensBefore) && tokensBefore >= 0 ? Math.floor(tokensBefore) : 0,
312
+ compactions: typeof compactions === "number" && Number.isSafeInteger(compactions) && compactions > 0 ? compactions : 1,
313
+ };
314
+ }
272
315
  function parseQueuedMessages(raw) {
273
316
  const parsed = safeJsonParse(raw);
274
317
  return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
@@ -356,6 +399,7 @@ function mapWorkspaceTaskRow(row) {
356
399
  milestoneId: typeof row.milestone_id === "string" && row.milestone_id ? row.milestone_id : null,
357
400
  layout: mapWorkspaceTaskLayout(row.layout_json),
358
401
  status: (row.status === "done" ? "done" : "active"),
402
+ ...(row.card_status === "archived" ? { archived: true } : {}),
359
403
  createdAt: row.created_at,
360
404
  lastOpenedAt: row.last_opened_at,
361
405
  layoutRevision: Number(row.layout_revision ?? 0) || 0,
@@ -851,6 +895,16 @@ const INIT_SQL = `
851
895
  );
852
896
  CREATE INDEX IF NOT EXISTS idx_decision_access_expiry ON decision_access(expires_at);
853
897
 
898
+ CREATE TABLE IF NOT EXISTS openrouter_free_access (
899
+ token_hash TEXT PRIMARY KEY,
900
+ session_id TEXT NOT NULL REFERENCES command_sessions(id) ON DELETE CASCADE,
901
+ selector TEXT NOT NULL,
902
+ requirements_json TEXT NOT NULL,
903
+ credential_hash TEXT NOT NULL,
904
+ expires_at INTEGER NOT NULL
905
+ );
906
+ CREATE INDEX IF NOT EXISTS idx_openrouter_free_access_expiry ON openrouter_free_access(expires_at);
907
+
854
908
  CREATE TABLE IF NOT EXISTS user_memory_events (
855
909
  id INTEGER PRIMARY KEY AUTOINCREMENT,
856
910
  feature TEXT NOT NULL,
@@ -977,7 +1031,7 @@ function ensureWandTaskSchema(db) {
977
1031
  // 里程碑:只加列,历史行保持 NULL;里程碑本体在 wand_milestones(INIT_SQL 建表)。
978
1032
  if (columns.length > 0 && !names.has("milestone_id"))
979
1033
  db.exec("ALTER TABLE wand_tasks ADD COLUMN milestone_id TEXT");
980
- // 归档时间(自动/手动归档都写它):保留期清理按它起算 7 天,历史归档行保持 NULL。
1034
+ // 归档时间(自动/手动归档都写它):任务自动删除按它起算;历史归档行保持 NULL。
981
1035
  if (columns.length > 0 && !names.has("archived_at"))
982
1036
  db.exec("ALTER TABLE wand_tasks ADD COLUMN archived_at TEXT");
983
1037
  if (columns.length > 0) {
@@ -1079,6 +1133,9 @@ export function ensureDatabaseFile(dbPath) {
1079
1133
  chmodSync(dir, 0o700);
1080
1134
  const created = !existsSync(dbPath);
1081
1135
  const db = new DatabaseSync(dbPath);
1136
+ // DDL below takes the write lock; without this a concurrent CLI/Server writer
1137
+ // aborts startup with SQLITE_BUSY instead of waiting its turn.
1138
+ db.exec("PRAGMA busy_timeout = 5000");
1082
1139
  db.exec(INIT_SQL);
1083
1140
  ensureAuthSessionSchema(db);
1084
1141
  ensureCommandSessionSchema(db);
@@ -1115,7 +1172,7 @@ const WORKSPACE_TASK_PROJECTION = `SELECT wt.id,
1115
1172
  WHEN card.status IN ('done', 'archived') THEN 'done' ELSE 'active' END AS status,
1116
1173
  CASE WHEN card.id IS NULL THEN wt.milestone_id ELSE card.milestone_id END AS milestone_id,
1117
1174
  wt.worktree_json, wt.layout_json, wt.cwd, wt.created_at, wt.last_opened_at, wt.layout_revision,
1118
- wt.rowid AS _created_order
1175
+ card.status AS card_status, wt.rowid AS _created_order
1119
1176
  FROM workspace_tasks wt LEFT JOIN wand_tasks card ON card.id = (
1120
1177
  SELECT id FROM wand_tasks WHERE workspace_task_id = wt.id
1121
1178
  ORDER BY updated_at DESC, rowid DESC LIMIT 1
@@ -1151,6 +1208,44 @@ export class WandStorage {
1151
1208
  ensureWandMilestoneSchema(this.db);
1152
1209
  ensureIterationPromptSchema(this.db);
1153
1210
  ensureAiTeamSchema(this.db);
1211
+ this.db.exec(`
1212
+ CREATE TABLE IF NOT EXISTS conversation_list_state (
1213
+ conversation_id TEXT PRIMARY KEY,
1214
+ pinned_at TEXT,
1215
+ hidden_at TEXT, dissolved_at TEXT, dissolved_by TEXT, deleting INTEGER NOT NULL DEFAULT 0
1216
+ );
1217
+ CREATE TABLE IF NOT EXISTS conversations (
1218
+ id TEXT PRIMARY KEY, owner TEXT NOT NULL, kind TEXT NOT NULL,
1219
+ peer_employee_id TEXT, session_id TEXT, instance_json TEXT NOT NULL
1220
+ );
1221
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_conversation_peer ON conversations(owner, peer_employee_id)
1222
+ WHERE kind = 'dm';
1223
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_conversation_session ON conversations(session_id)
1224
+ WHERE session_id IS NOT NULL;
1225
+ CREATE TABLE IF NOT EXISTS conversation_tasks (
1226
+ conversation_id TEXT NOT NULL, task_id TEXT NOT NULL UNIQUE, linked_at TEXT NOT NULL,
1227
+ PRIMARY KEY(conversation_id, task_id)
1228
+ );
1229
+ CREATE TABLE IF NOT EXISTS conversation_messages (
1230
+ id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, turn_json TEXT NOT NULL, created_at TEXT NOT NULL
1231
+ );
1232
+ CREATE INDEX IF NOT EXISTS idx_conversation_messages ON conversation_messages(conversation_id, created_at);
1233
+ CREATE TABLE IF NOT EXISTS conversation_requests (
1234
+ id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, receipt_json TEXT NOT NULL, updated_at TEXT NOT NULL
1235
+ );
1236
+ `);
1237
+ const conversationListColumns = new Set(this.db.prepare("PRAGMA table_info(conversation_list_state)").all().map(column => column.name));
1238
+ for (const [name, type] of [["dissolved_at", "TEXT"], ["dissolved_by", "TEXT"], ["deleting", "INTEGER NOT NULL DEFAULT 0"]]) {
1239
+ if (!conversationListColumns.has(name))
1240
+ this.db.exec(`ALTER TABLE conversation_list_state ADD COLUMN ${name} ${type}`);
1241
+ }
1242
+ const conversationRunColumns = new Set(this.db.prepare("PRAGMA table_info(ai_team_runs)").all().map((column) => column.name));
1243
+ if (!conversationRunColumns.has("conversation_id"))
1244
+ this.db.exec("ALTER TABLE ai_team_runs ADD COLUMN conversation_id TEXT");
1245
+ if (!conversationRunColumns.has("member_version"))
1246
+ this.db.exec("ALTER TABLE ai_team_runs ADD COLUMN member_version INTEGER");
1247
+ if (!conversationRunColumns.has("round_number"))
1248
+ this.db.exec("ALTER TABLE ai_team_runs ADD COLUMN round_number INTEGER");
1154
1249
  ensureConnectorSchema(this.db);
1155
1250
  this.ensureDefaultPasswordVault();
1156
1251
  this.migrateTaskRecords();
@@ -1185,6 +1280,157 @@ export class WandStorage {
1185
1280
  throw error;
1186
1281
  }
1187
1282
  }
1283
+ // ============ Stable conversations (no read-side repair) ============
1284
+ conversationListState(id) {
1285
+ const row = this.db.prepare("SELECT pinned_at, dissolved_at, dissolved_by, deleting FROM conversation_list_state WHERE conversation_id = ?")
1286
+ .get(id);
1287
+ return { pinnedAt: row?.pinned_at ?? null, dissolvedAt: row?.dissolved_at ?? null, dissolvedBy: row?.dissolved_by ?? null, deleting: !!row?.deleting };
1288
+ }
1289
+ /** Separate from channel snapshots, so late run saves cannot overwrite lifecycle state. */
1290
+ updateConversationListState(id, patch) {
1291
+ this.transaction(() => {
1292
+ const previous = this.conversationListState(id);
1293
+ const pinnedAt = patch.pinned === false ? null : patch.pinned === true ? previous.pinnedAt ?? nowIso() : previous.pinnedAt;
1294
+ const dissolvedAt = patch.dissolved === true ? previous.dissolvedAt ?? nowIso() : patch.dissolved === false ? null : previous.dissolvedAt;
1295
+ // 解散人是当时那一版署名,保留真实归属;只有没传资料时才回落到默认的「我」。
1296
+ const dissolvedBy = dissolvedAt ? previous.dissolvedBy ?? patch.dissolvedBy ?? "我" : null;
1297
+ this.db.prepare(`INSERT INTO conversation_list_state(conversation_id, pinned_at, dissolved_at, dissolved_by, deleting) VALUES (?, ?, ?, ?, ?)
1298
+ ON CONFLICT(conversation_id) DO UPDATE SET pinned_at = excluded.pinned_at, dissolved_at = excluded.dissolved_at, dissolved_by = excluded.dissolved_by, deleting = excluded.deleting`)
1299
+ .run(id, pinnedAt, dissolvedAt, dissolvedBy, Number(patch.deleting ?? previous.deleting));
1300
+ });
1301
+ }
1302
+ deleteConversation(id) {
1303
+ this.transaction(() => {
1304
+ this.db.prepare("DELETE FROM conversation_messages WHERE conversation_id = ?").run(id);
1305
+ this.db.prepare("DELETE FROM conversation_tasks WHERE conversation_id = ?").run(id);
1306
+ this.db.prepare("DELETE FROM conversations WHERE id = ? AND owner = ?").run(id, CONVERSATION_OWNER);
1307
+ this.db.prepare("DELETE FROM conversation_list_state WHERE conversation_id = ?").run(id);
1308
+ // Keep the content-free request ledger: retrying an old accepted request must never recreate deleted work.
1309
+ });
1310
+ }
1311
+ listConversations() {
1312
+ return this.db.prepare("SELECT instance_json FROM conversations WHERE owner = ?").all(CONVERSATION_OWNER)
1313
+ .map((row) => JSON.parse(row.instance_json));
1314
+ }
1315
+ getConversation(id) {
1316
+ const row = this.db.prepare("SELECT instance_json FROM conversations WHERE id = ? AND owner = ?")
1317
+ .get(id, CONVERSATION_OWNER);
1318
+ return row ? JSON.parse(row.instance_json) : null;
1319
+ }
1320
+ getConversationBySession(sessionId) {
1321
+ const row = this.db.prepare("SELECT instance_json FROM conversations WHERE session_id = ? AND owner = ?")
1322
+ .get(sessionId, CONVERSATION_OWNER);
1323
+ return row ? JSON.parse(row.instance_json) : null;
1324
+ }
1325
+ saveConversation(instance) {
1326
+ this.db.prepare(`INSERT INTO conversations(id, owner, kind, peer_employee_id, session_id, instance_json)
1327
+ VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
1328
+ session_id = excluded.session_id, instance_json = excluded.instance_json`)
1329
+ .run(instance.id, instance.owner, instance.kind, instance.peerEmployeeId, instance.sessionId, JSON.stringify(instance));
1330
+ }
1331
+ conversationTasks(id) {
1332
+ return this.db.prepare("SELECT task_id, linked_at FROM conversation_tasks WHERE conversation_id = ? ORDER BY linked_at, rowid")
1333
+ .all(id).map((row) => ({ taskId: row.task_id, linkedAt: row.linked_at }));
1334
+ }
1335
+ linkConversationTask(conversationId, taskId, linkedAt) {
1336
+ this.db.prepare("INSERT OR IGNORE INTO conversation_tasks(conversation_id, task_id, linked_at) VALUES (?, ?, ?)")
1337
+ .run(conversationId, taskId, linkedAt);
1338
+ }
1339
+ /** One canonical task/workspace/instance association commit, before any model input. */
1340
+ createConversationTask(instance, input, accept) {
1341
+ return this.transaction(() => {
1342
+ const workspace = input.workspaceId ? this.getWorkspace(input.workspaceId) : null;
1343
+ if (!workspace || workspace.kind === "global")
1344
+ throw new Error("请选择非全局工作项目。");
1345
+ const milestoneId = this.taskMilestoneForWrite(input.milestoneId, workspace.id);
1346
+ const workspaceTaskId = this.insertWorkspaceTask({ workspaceId: workspace.id, name: input.title, milestoneId });
1347
+ const task = this.insertWandTask({ ...input, workspaceId: workspace.id, workspaceTaskId, milestoneId });
1348
+ this.saveConversation(instance);
1349
+ this.linkConversationTask(instance.id, task.id, task.createdAt);
1350
+ accept?.(task);
1351
+ return task;
1352
+ });
1353
+ }
1354
+ /** Commit the first accepted fact, identifiers and DM link together, before any execution I/O. */
1355
+ acceptConversationDispatch(instance, input, request, source) {
1356
+ const accept = (task) => {
1357
+ this.saveConversation(instance);
1358
+ this.linkConversationTask(instance.id, task.id, task.createdAt);
1359
+ request.receipt = { ...request.receipt, state: "accepted", conversationId: instance.id, taskId: task.id, startup: "pending" };
1360
+ if (source) {
1361
+ source.createdAt ||= nowIso();
1362
+ source.updatedAt = nowIso();
1363
+ this.saveConversation(source);
1364
+ this.appendConversationEvent(source.id, { role: "user", messageId: request.id, requestId: request.id,
1365
+ conversationTarget: null, content: [{ type: "text", text: "description" in input ? input.description ?? task.description : task.description }] });
1366
+ request.receipt.messageId = request.id;
1367
+ const employee = source.peerEmployeeId ? this.getSiliconEmployee(source.peerEmployeeId) : null;
1368
+ this.appendConversationEvent(source.id, { role: "assistant", notice: true,
1369
+ author: { id: source.peerEmployeeId ?? source.id, name: employee?.name ?? source.name, avatar: employee?.avatar },
1370
+ messageId: `${request.id}:task-link`, requestId: request.id,
1371
+ conversationLink: { conversationId: instance.id, taskId: task.id, title: task.title },
1372
+ content: [{ type: "text", text: `我正在处理「${task.title}」,进展在任务群实时同步。` }] });
1373
+ }
1374
+ this.saveConversationRequest({ ...request, updatedAt: nowIso() });
1375
+ return task;
1376
+ };
1377
+ if (!("continueTaskId" in input))
1378
+ return this.createConversationTask(instance, input, accept);
1379
+ return this.transaction(() => {
1380
+ const task = this.getWandTask(input.continueTaskId);
1381
+ if (!task)
1382
+ throw new Error("任务不存在。");
1383
+ return accept(task);
1384
+ });
1385
+ }
1386
+ /** Message + reply + exact session association + receipt are committed before any model input. */
1387
+ acceptConversationSession(instance, createSession, title, input, request) {
1388
+ this.transaction(() => {
1389
+ const sessionId = createSession();
1390
+ instance.createdAt ||= nowIso();
1391
+ instance.updatedAt = nowIso();
1392
+ this.saveConversation(instance);
1393
+ this.appendConversationEvent(instance.id, { role: "user", messageId: request.id, requestId: request.id,
1394
+ conversationTarget: null, content: [{ type: "text", text: input }] });
1395
+ const employee = instance.peerEmployeeId ? this.getSiliconEmployee(instance.peerEmployeeId) : null;
1396
+ this.appendConversationEvent(instance.id, { role: "assistant", messageId: `${request.id}:session`, requestId: request.id,
1397
+ author: { id: instance.peerEmployeeId ?? instance.id, name: employee?.name ?? instance.name, avatar: employee?.avatar },
1398
+ sessionLink: { sessionId, title }, content: [] });
1399
+ this.saveConversationRequest({ ...request, updatedAt: nowIso(), receipt: { ...request.receipt,
1400
+ state: "accepted", conversationId: instance.id, sessionId, messageId: request.id, startup: "pending" } });
1401
+ });
1402
+ }
1403
+ conversationSessionIds(id) {
1404
+ const rows = this.db.prepare(`SELECT json_extract(turn_json, '$.sessionLink.sessionId') AS session_id
1405
+ FROM conversation_messages WHERE conversation_id = ? AND json_extract(turn_json, '$.sessionLink.sessionId') IS NOT NULL`)
1406
+ .all(id);
1407
+ return [...new Set(rows.map(row => row.session_id))];
1408
+ }
1409
+ conversationTaskStartup(taskId) {
1410
+ const row = this.db.prepare(`SELECT receipt_json FROM conversation_requests
1411
+ WHERE json_extract(receipt_json, '$.taskId') = ? AND json_extract(receipt_json, '$.startup') IS NOT NULL
1412
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1`).get(taskId);
1413
+ return row ? JSON.parse(row.receipt_json) : null;
1414
+ }
1415
+ appendConversationEvent(id, turn) {
1416
+ const createdAt = turn.createdAt ?? nowIso();
1417
+ const messageId = turn.messageId ?? crypto.randomUUID();
1418
+ this.db.prepare("INSERT INTO conversation_messages(id, conversation_id, turn_json, created_at) VALUES (?, ?, ?, ?)")
1419
+ .run(messageId, id, JSON.stringify({ ...turn, messageId, conversationId: id, createdAt }), createdAt);
1420
+ }
1421
+ conversationEvents(id) {
1422
+ return this.db.prepare("SELECT turn_json FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid")
1423
+ .all(id).map((row) => JSON.parse(row.turn_json));
1424
+ }
1425
+ getConversationRequest(id) {
1426
+ const row = this.db.prepare("SELECT * FROM conversation_requests WHERE id = ?").get(id);
1427
+ return row ? { id: row.id, fingerprint: row.fingerprint, receipt: JSON.parse(row.receipt_json), updatedAt: row.updated_at } : null;
1428
+ }
1429
+ saveConversationRequest(request) {
1430
+ this.db.prepare(`INSERT INTO conversation_requests(id, fingerprint, receipt_json, updated_at) VALUES (?, ?, ?, ?)
1431
+ ON CONFLICT(id) DO UPDATE SET receipt_json = excluded.receipt_json, updated_at = excluded.updated_at`)
1432
+ .run(request.id, request.fingerprint, JSON.stringify(request.receipt), request.updatedAt);
1433
+ }
1188
1434
  // ============ Config Methods ============
1189
1435
  /** Get a config value from database */
1190
1436
  getConfigValue(key) {
@@ -1233,6 +1479,28 @@ export class WandStorage {
1233
1479
  hasPreference(key) {
1234
1480
  return this.getConfigValue(key) !== null;
1235
1481
  }
1482
+ // ============ Pi 会话默认设置 ============
1483
+ /**
1484
+ * Pi CLI 会话的「上次设置」:用户在任意 Pi 会话里改过设置后,新建会话以它起步,
1485
+ * 不必每开一个会话都重配自动选择 / CodeMode。返回 null 表示沿用出厂默认
1486
+ * (跟随 Pi 自身配置),不是「设置全关」。只存设置字段本身,不含路径、命令或凭据。
1487
+ */
1488
+ getPiSessionDefaults() {
1489
+ const raw = this.getPreference(PI_SESSION_DEFAULTS_KEY, null);
1490
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1491
+ return null;
1492
+ try {
1493
+ return patchPiSessionSettings(defaultPiCliSessionSettings(), raw);
1494
+ }
1495
+ catch {
1496
+ // 非法/过期的默认设置不能静默开启任何能力,退回出厂默认。
1497
+ return null;
1498
+ }
1499
+ }
1500
+ /** 写入 Pi 会话默认设置;null / undefined 清除,回到出厂默认。 */
1501
+ setPiSessionDefaults(settings) {
1502
+ this.setPreference(PI_SESSION_DEFAULTS_KEY, settings ?? null);
1503
+ }
1236
1504
  // ============ 首页目录组顺序 ============
1237
1505
  /**
1238
1506
  * 用户在首页拖动排序后的目录组顺序(服务端偏好,所有客户端共用)。
@@ -1432,14 +1700,18 @@ export class WandStorage {
1432
1700
  this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?")
1433
1701
  .run(task?.workspaceId ?? workspaceId, sessionId);
1434
1702
  }
1435
- /** Lightweight count of persisted sessions grouped by workspace. */
1436
- countSessionsByWorkspace() {
1703
+ /** Normal workspace counts exclude archived sessions and sessions in archived tasks. */
1704
+ countSessionsByWorkspace(options = {}) {
1437
1705
  const rows = this.db
1438
- .prepare(`SELECT workspace_id AS id, COUNT(*) AS n
1439
- FROM command_sessions
1440
- WHERE workspace_id IS NOT NULL AND workspace_id != ''
1441
- GROUP BY workspace_id`)
1442
- .all();
1706
+ .prepare(`SELECT session.workspace_id AS id, COUNT(*) AS n
1707
+ FROM command_sessions session
1708
+ WHERE session.workspace_id IS NOT NULL AND session.workspace_id != ''
1709
+ AND (? = 1 OR (session.archived = 0 AND COALESCE((
1710
+ SELECT status FROM wand_tasks WHERE workspace_task_id = session.workspace_task_id
1711
+ ORDER BY updated_at DESC, rowid DESC LIMIT 1
1712
+ ), '') <> 'archived'))
1713
+ GROUP BY session.workspace_id`)
1714
+ .all(options.includeArchived === true ? 1 : 0);
1443
1715
  return new Map(rows.map((row) => [row.id, Number(row.n) || 0]));
1444
1716
  }
1445
1717
  /** Count user-initiated CLI launches (one per session), including archived sessions. */
@@ -1755,11 +2027,20 @@ export class WandStorage {
1755
2027
  }
1756
2028
  /** Commit completion archives only the selected completed cards, atomically. */
1757
2029
  archiveCompletedWandTasks(ids) {
2030
+ return this.archiveWandTasks(ids, { completedOnly: true });
2031
+ }
2032
+ /**
2033
+ * Archive the given cards. Already-archived ids are skipped.
2034
+ * `completedOnly` keeps the historical commit helper from closing in-progress work.
2035
+ */
2036
+ archiveWandTasks(ids, options = {}) {
1758
2037
  return this.transaction(() => {
1759
2038
  const archived = [];
1760
2039
  for (const id of new Set(ids)) {
1761
2040
  const task = this.getWandTask(id);
1762
- if (task?.status !== "done")
2041
+ if (!task || task.status === "archived")
2042
+ continue;
2043
+ if (options.completedOnly && task.status !== "done")
1763
2044
  continue;
1764
2045
  this.writeWandTask(task, { status: "archived" });
1765
2046
  archived.push(id);
@@ -1768,19 +2049,36 @@ export class WandStorage {
1768
2049
  });
1769
2050
  }
1770
2051
  deleteWandTask(id) {
1771
- this.transaction(() => {
1772
- const card = this.getWandTask(id);
1773
- if (!card)
1774
- return;
1775
- this.db.prepare("UPDATE wand_tasks SET parent_task_id = NULL WHERE parent_task_id = ?").run(id);
1776
- if (card.workspaceTaskId) {
1777
- this.db.prepare("UPDATE command_sessions SET workspace_task_id = NULL WHERE workspace_task_id = ?")
1778
- .run(card.workspaceTaskId);
1779
- this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(card.workspaceTaskId);
2052
+ this.transaction(() => this.removeWandTaskRow(id));
2053
+ }
2054
+ /**
2055
+ * 批量硬删除卡片:一次事务,返回实际删除的 id(不存在或已消失的跳过)。
2056
+ * 归档目录的「清空」和保留期扫描都走这里,调用方自己决定哪些 id 可以删。
2057
+ */
2058
+ deleteWandTasks(ids) {
2059
+ return this.transaction(() => {
2060
+ const deleted = [];
2061
+ for (const id of new Set(ids)) {
2062
+ if (this.removeWandTaskRow(id))
2063
+ deleted.push(id);
1780
2064
  }
1781
- this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id);
2065
+ return deleted;
1782
2066
  });
1783
2067
  }
2068
+ /** Single-card hard delete body; callers own the transaction (SQLite forbids nesting). */
2069
+ removeWandTaskRow(id) {
2070
+ const card = this.getWandTask(id);
2071
+ if (!card)
2072
+ return false;
2073
+ this.db.prepare("UPDATE wand_tasks SET parent_task_id = NULL WHERE parent_task_id = ?").run(id);
2074
+ if (card.workspaceTaskId) {
2075
+ this.db.prepare("UPDATE command_sessions SET workspace_task_id = NULL WHERE workspace_task_id = ?")
2076
+ .run(card.workspaceTaskId);
2077
+ this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(card.workspaceTaskId);
2078
+ }
2079
+ this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id);
2080
+ return true;
2081
+ }
1784
2082
  /** One-time id-based reconciliation; normal reads never create tasks or repair membership. */
1785
2083
  migrateTaskRecords() {
1786
2084
  if (this.getPreference("pref:taskRecordsVersion", 0) === 1)
@@ -2588,6 +2886,24 @@ export class WandStorage {
2588
2886
  this.db.prepare("DELETE FROM decision_access WHERE token_hash = ?")
2589
2887
  .run(crypto.createHash("sha256").update(token).digest("hex"));
2590
2888
  }
2889
+ /** CLI-only free inference scope, durable across Server restarts but not reusable by another run. */
2890
+ issueOpenRouterFreeAccess(sessionId, selector, credentialHash, requirements, now = Date.now()) {
2891
+ const session = this.getSessionSlim(sessionId);
2892
+ if (session?.provider !== "pi" || session.sessionKind !== "structured"
2893
+ || !selector.startsWith("wand-openrouter-free/") || !/^[a-f0-9]{64}$/.test(credentialHash)) {
2894
+ throw new Error("免费模型调用所属会话或凭据无效。");
2895
+ }
2896
+ this.db.prepare("DELETE FROM openrouter_free_access WHERE expires_at <= ? OR session_id = ?").run(now, sessionId);
2897
+ const token = `wf_${crypto.randomBytes(32).toString("base64url")}`;
2898
+ this.db.prepare(`INSERT INTO openrouter_free_access
2899
+ (token_hash, session_id, selector, requirements_json, credential_hash, expires_at) VALUES (?, ?, ?, ?, ?, ?)`)
2900
+ .run(crypto.createHash("sha256").update(token).digest("hex"), sessionId, selector, JSON.stringify(requirements), credentialHash, now + 6 * 60 * 60 * 1000);
2901
+ return token;
2902
+ }
2903
+ revokeOpenRouterFreeAccess(token) {
2904
+ this.db.prepare("DELETE FROM openrouter_free_access WHERE token_hash = ?")
2905
+ .run(crypto.createHash("sha256").update(token).digest("hex"));
2906
+ }
2591
2907
  // ============ User short-term memory ============
2592
2908
  /** No IO on input hot paths; queued observations still CAS against the database before writing. */
2593
2909
  userMemoryCaptureState() {
@@ -2725,19 +3041,29 @@ export class WandStorage {
2725
3041
  deleteAiTeam(id) {
2726
3042
  this.db.prepare("DELETE FROM ai_teams WHERE id = ?").run(id);
2727
3043
  }
3044
+ /** 保留期清理:删掉一次运行及其步骤。关联会话由调用方按运行所有权删除。 */
3045
+ deleteAiTeamRun(id) {
3046
+ this.transaction(() => {
3047
+ this.db.prepare("DELETE FROM ai_team_steps WHERE run_id = ?").run(id);
3048
+ this.db.prepare("DELETE FROM ai_team_runs WHERE id = ?").run(id);
3049
+ });
3050
+ }
2728
3051
  saveAiTeamRun(run) {
2729
3052
  this.db.prepare(`INSERT INTO ai_team_runs (
2730
3053
  id, team_id, task_id, team_json, objective, cwd, status, status_detail,
2731
3054
  steps_used, step_limit, format_retries, plan_approved, chat_session_id, pending_notes_json,
2732
- created_at, updated_at
2733
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3055
+ created_at, updated_at, conversation_id, member_version, round_number
3056
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2734
3057
  ON CONFLICT(id) DO UPDATE SET
2735
3058
  team_json = excluded.team_json,
2736
3059
  status = excluded.status, status_detail = excluded.status_detail,
2737
3060
  steps_used = excluded.steps_used, step_limit = excluded.step_limit,
2738
3061
  format_retries = excluded.format_retries, plan_approved = excluded.plan_approved,
2739
3062
  chat_session_id = excluded.chat_session_id, pending_notes_json = excluded.pending_notes_json,
2740
- updated_at = excluded.updated_at`).run(run.id, run.teamId, run.taskId, serializeExecutionTeam(run.team), run.objective, run.cwd, run.status, run.statusDetail, run.stepsUsed, run.stepLimit, run.formatRetries, run.planApproved ? 1 : 0, run.chatSessionId, JSON.stringify(run.pendingNotes), run.createdAt, run.updatedAt);
3063
+ conversation_id = COALESCE(excluded.conversation_id, ai_team_runs.conversation_id),
3064
+ member_version = COALESCE(excluded.member_version, ai_team_runs.member_version),
3065
+ round_number = COALESCE(excluded.round_number, ai_team_runs.round_number),
3066
+ updated_at = excluded.updated_at`).run(run.id, run.teamId, run.taskId, serializeExecutionTeam(run.team), run.objective, run.cwd, run.status, run.statusDetail, run.stepsUsed, run.stepLimit, run.formatRetries, run.planApproved ? 1 : 0, run.chatSessionId, JSON.stringify(run.pendingNotes), run.createdAt, run.updatedAt, run.conversationId ?? null, run.memberVersion ?? null, run.roundNumber ?? null);
2741
3067
  }
2742
3068
  /** 群聊会话对应的最近一次运行(同一个群聊可以接着开新运行)。 */
2743
3069
  getLatestAiTeamRunByChat(sessionId) {
@@ -3172,6 +3498,10 @@ function normalizeAiTeamMember(raw) {
3172
3498
  member.avatar = value.avatar;
3173
3499
  if (isTeamMemberRole(value.role))
3174
3500
  member.role = value.role;
3501
+ if (typeof value.legacyTemplateId === "string")
3502
+ member.legacyTemplateId = value.legacyTemplateId;
3503
+ if (typeof value.legacyMemberId === "string")
3504
+ member.legacyMemberId = value.legacyMemberId;
3175
3505
  if (typeof value.employeeId === "string" && value.employeeId.trim()) {
3176
3506
  member.employeeId = value.employeeId.trim();
3177
3507
  const raw = value._employeeSnapshot;
@@ -3226,6 +3556,7 @@ function mapAiTeamRunRow(row) {
3226
3556
  description: String(rawTeam.description ?? ""),
3227
3557
  instructions: String(rawTeam.instructions ?? ""),
3228
3558
  members: normalizeAiTeamMembers(rawTeam.members),
3559
+ ...(rawTeam.allowLeaderWork === true ? { allowLeaderWork: true } : {}),
3229
3560
  requirePlanApproval: rawTeam.requirePlanApproval === undefined ? true : rawTeam.requirePlanApproval !== false,
3230
3561
  maxSteps: Number(rawTeam.maxSteps) || AI_TEAM_DEFAULT_MAX_STEPS,
3231
3562
  createdAt: String(rawTeam.createdAt ?? row.created_at),
@@ -3248,6 +3579,9 @@ function mapAiTeamRunRow(row) {
3248
3579
  formatRetries: Number(row.format_retries) || 0,
3249
3580
  planApproved: Number(row.plan_approved) !== 0,
3250
3581
  chatSessionId: typeof row.chat_session_id === "string" && row.chat_session_id ? row.chat_session_id : null,
3582
+ ...(typeof row.conversation_id === "string" ? {
3583
+ conversationId: row.conversation_id, memberVersion: Number(row.member_version), roundNumber: Number(row.round_number),
3584
+ } : {}),
3251
3585
  pendingNotes: (safeJsonParse(typeof row.pending_notes_json === "string" ? row.pending_notes_json : null) ?? [])
3252
3586
  .filter((note) => typeof note === "string"),
3253
3587
  createdAt: String(row.created_at),
@@ -3256,7 +3590,7 @@ function mapAiTeamRunRow(row) {
3256
3590
  }
3257
3591
  /** 候选失败类别的读侧白名单;真源是 `ai-team-types.ts` 的 `CandidateFailureKind`。 */
3258
3592
  const CANDIDATE_FAILURE_KINDS = [
3259
- "spawn-missing", "host-disabled", "model-unknown", "startup-timeout",
3593
+ "spawn-missing", "input-rejected", "host-disabled", "model-unknown", "startup-timeout",
3260
3594
  "runtime-failure", "format-error", "user-stop",
3261
3595
  ];
3262
3596
  function isCandidateFailureKind(value) {
@@ -3492,6 +3826,8 @@ const SCHEMA_MIGRATIONS = [
3492
3826
  ];
3493
3827
  /** 首页目录组顺序的偏好键(app_config 表,与其它 UI 偏好同一套读写)。 */
3494
3828
  const WORKSPACE_GROUP_ORDER_KEY = "workspaceGroupOrder";
3829
+ /** Pi 会话默认设置的偏好键,与其它 UI 偏好同一套读写,不出现在 config.json。 */
3830
+ const PI_SESSION_DEFAULTS_KEY = "pref:piSessionDefaults";
3495
3831
  const AUTH_SESSION_MIGRATIONS = [
3496
3832
  ["kind", "ALTER TABLE auth_sessions ADD COLUMN kind TEXT NOT NULL DEFAULT 'browser-admin'"],
3497
3833
  ["scopes", `ALTER TABLE auth_sessions ADD COLUMN scopes TEXT NOT NULL DEFAULT '["admin"]'`],
@@ -246,7 +246,7 @@ export class ClaudeCliProtocolReducer {
246
246
  else {
247
247
  const duplicate = block.type === "text"
248
248
  ? merged.some((entry) => entry.type === "text" && entry.text === block.text)
249
- : merged.some((entry) => entry.type === "thinking" && entry.thinking === block.thinking);
249
+ : block.type === "thinking" && merged.some((entry) => entry.type === "thinking" && entry.thinking === block.thinking);
250
250
  if (!duplicate)
251
251
  merged.push(block);
252
252
  }
@@ -1,5 +1,6 @@
1
1
  import { withToolPreview } from "./tool-preview.js";
2
2
  import { asRecord, isStructuredImagePart } from "./structured-content.js";
3
+ import { deriveSubagentDispatchMeta, PI_TODO_TOOL_NAME } from "./subagent-dispatch.js";
3
4
  import { isDecisionToolCall, decisionCardSummary } from "./decision-tool.js";
4
5
  export const WAND_PROTOCOL_VERSION = 2;
5
6
  function text(value) {
@@ -45,44 +46,16 @@ function questionsFromInput(input) {
45
46
  }
46
47
  return questions;
47
48
  }
48
- /** 派发子 Agent 的工具名:Claude Code 的 Task/Agent,以及 Wand pi 扩展的 subagent。 */
49
- const SUBAGENT_TOOL_NAMES = new Set(["Task", "Agent"]);
50
- const PI_SUBAGENT_TOOL_NAME = "Pi/subagent";
51
- /** pi 的待办扩展工具,经 `piToolName` 映射后带 `Pi/` 前缀。 */
52
- const PI_TODO_TOOL_NAME = "Pi/todo";
53
49
  /**
54
50
  * 历史 turn / 非 Claude provider 的子 Agent 工具调用没有 `__subagent` 盖章,
55
51
  * 按调用参数现场补一份,让「子 Agent」面板在所有端都拿得到同一份分组。
56
52
  *
57
- * 只认一次真正的派发:Claude 的 Task/Agent(`subagent_type` 可省),
58
- * pi 扩展的 `Pi/subagent` 必须带 `agent`,避免把管理类调用误当子任务。
53
+ * 判据本体在 `src/subagent-dispatch.ts`:与 Web 前端共用同一份实现,
54
+ * pi 的 `subagent` 以「有没有 `action`」区分管理/控制与真派发,不靠枚举参数形状。
55
+ * 服务端盖的这份章就是 Android / iOS / macOS 的唯一判据来源,各端只消费不重判。
59
56
  */
60
57
  function deriveSubagentMeta(block) {
61
- const existing = block.__subagent;
62
- if (existing?.taskId)
63
- return existing;
64
- const input = asRecord(block.input) ?? {};
65
- if (block.name === PI_SUBAGENT_TOOL_NAME) {
66
- const agent = text(input.agent);
67
- const task = text(input.task);
68
- if (!agent && !task)
69
- return null;
70
- return {
71
- taskId: block.id,
72
- ...(agent ? { agentType: agent } : {}),
73
- ...(task ? { taskDescription: task } : {}),
74
- };
75
- }
76
- const agentType = text(input.subagent_type);
77
- // Claude 的 Task/Agent 允许省 `subagent_type`;其他工具只有在真给了 `subagent_type` 时才算派发。
78
- if (!SUBAGENT_TOOL_NAMES.has(block.name) && !agentType)
79
- return null;
80
- const description = text(input.description);
81
- return {
82
- taskId: block.id,
83
- ...(agentType ? { agentType } : {}),
84
- ...(description ? { taskDescription: description } : {}),
85
- };
58
+ return deriveSubagentDispatchMeta(block);
86
59
  }
87
60
  /**
88
61
  * 给没有盖章的子 Agent 调用打上 `__subagent`:派发用的 tool_use 自己带一份,
@@ -282,7 +255,12 @@ export function enrichStructuredMessages(messages, sessionId) {
282
255
  };
283
256
  const enriched = stampDerivedSubagents(messages).map((turn) => ({
284
257
  ...turn,
285
- content: turn.content.map(withToolPreview).map((block) => {
258
+ content: turn.content.map((block) => {
259
+ if (block.type !== "tool_use" || !block.piExecutionRef)
260
+ return block;
261
+ const { piExecutionRef: _hostReference, ...publicBlock } = block;
262
+ return publicBlock;
263
+ }).map(withToolPreview).map((block) => {
286
264
  if (block.type === "tool_use" && decisionIds.has(block.id)) {
287
265
  return { ...block, semantic: decisionSemantic(block.id), activity: undefined };
288
266
  }