@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.
- package/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- 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
|
-
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
1442
|
-
|
|
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
|
|
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
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
58
|
-
* pi
|
|
53
|
+
* 判据本体在 `src/subagent-dispatch.ts`:与 Web 前端共用同一份实现,
|
|
54
|
+
* pi 的 `subagent` 以「有没有 `action`」区分管理/控制与真派发,不靠枚举参数形状。
|
|
55
|
+
* 服务端盖的这份章就是 Android / iOS / macOS 的唯一判据来源,各端只消费不重判。
|
|
59
56
|
*/
|
|
60
57
|
function deriveSubagentMeta(block) {
|
|
61
|
-
|
|
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(
|
|
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
|
}
|