@co0ontty/wand 4.77.0 → 4.78.0-beta.ga1e4f59
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 +21 -46
- package/dist/agent-dispatch.js +3 -2
- package/dist/ai-team-live.d.ts +25 -0
- package/dist/ai-team-live.js +135 -0
- package/dist/ai-team-prompts.d.ts +10 -6
- package/dist/ai-team-prompts.js +74 -41
- package/dist/ai-team-runner.d.ts +85 -3
- package/dist/ai-team-runner.js +336 -33
- package/dist/ai-team-types.d.ts +57 -2
- package/dist/ai-team-types.js +4 -0
- package/dist/build-info.json +4 -4
- package/dist/commit-task-archive.js +14 -18
- package/dist/config.js +14 -6
- package/dist/git-quick-commit.js +1 -1
- package/dist/middleware/rate-limit.d.ts +18 -4
- package/dist/middleware/rate-limit.js +55 -23
- package/dist/missions.js +0 -3
- package/dist/models.d.ts +6 -1
- package/dist/models.js +35 -0
- package/dist/path-repair.d.ts +6 -5
- package/dist/path-repair.js +3 -3
- package/dist/render-protocol.d.ts +1 -1
- package/dist/render-protocol.js +1 -1
- package/dist/server-ai-team-routes.d.ts +1 -0
- package/dist/server-ai-team-routes.js +28 -14
- package/dist/server-local-preview-routes.js +1 -1
- package/dist/server-session-routes.js +0 -3
- package/dist/server-settings-routes.d.ts +1 -1
- package/dist/server-settings-routes.js +3 -2
- package/dist/server-task-routes.js +16 -32
- package/dist/server-workspace-routes.js +31 -24
- package/dist/server.js +60 -21
- package/dist/storage.d.ts +54 -21
- package/dist/storage.js +485 -122
- package/dist/types.d.ts +7 -0
- package/dist/upload-routes.d.ts +2 -2
- package/dist/upload-routes.js +10 -3
- package/dist/wand-task-sync.d.ts +2 -34
- package/dist/wand-task-sync.js +0 -206
- package/dist/web-ui/asset-version.d.ts +5 -0
- package/dist/web-ui/asset-version.js +5 -0
- package/dist/web-ui/content/ai-teams.js +17 -8
- package/dist/web-ui/content/scripts.js +67 -66
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +0 -21
- package/dist/web-ui/embedded-assets.js +8 -7
- package/dist/web-ui/index.js +9 -17
- package/dist/web-ui/scripts.d.ts +3 -1
- package/dist/web-ui/scripts.js +52 -25
- package/dist/web-ui/styles.d.ts +2 -0
- package/dist/web-ui/styles.js +12 -0
- package/package.json +13 -12
- package/scripts/install-render-binary.js +2 -2
- package/scripts/stage-render-binaries.js +1 -1
package/dist/storage.js
CHANGED
|
@@ -7,7 +7,8 @@ import { normalizeSessionDirectory } from "./session-directory-tree.js";
|
|
|
7
7
|
import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider, SESSION_PROVIDERS } from "./session-provider.js";
|
|
8
8
|
import { DEFAULT_ITERATION_NAME, DEFAULT_WAND_TASK_AGENT_KIND, DEFAULT_WAND_TASK_PRIORITY, isWandTaskAgentKind, normalizeWandTaskAgentMode } from "./task-types.js";
|
|
9
9
|
import { firstLayoutTabId } from "./layout-tree.js";
|
|
10
|
-
import {
|
|
10
|
+
import { isUnnamedWorkspaceTaskName } from "./wand-task-sync.js";
|
|
11
|
+
import { AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_TERMINAL_RUN_STATUSES, isTeamMemberRole, memberAgents } from "./ai-team-types.js";
|
|
11
12
|
import { isThinkingEffort } from "./structured-provider-common.js";
|
|
12
13
|
import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
|
|
13
14
|
function safeJsonParse(raw) {
|
|
@@ -996,6 +997,35 @@ export function ensureDatabaseFile(dbPath) {
|
|
|
996
997
|
chmodSync(dbPath, 0o600);
|
|
997
998
|
return created;
|
|
998
999
|
}
|
|
1000
|
+
const WAND_TASK_FIELDS = `id, identifier, workspace_id, workspace_task_id, parent_task_id,
|
|
1001
|
+
title, title_source, auto_title_signature, description, status, priority, labels_json,
|
|
1002
|
+
due_date, milestone_id, sort_order, agent_json, created_at, updated_at`;
|
|
1003
|
+
// Shared metadata comes from the latest explicitly linked card, including meaningful NULLs.
|
|
1004
|
+
// The old container columns remain readable for legacy rows until the startup migration.
|
|
1005
|
+
const WORKSPACE_TASK_PROJECTION = `SELECT wt.id,
|
|
1006
|
+
CASE WHEN card.id IS NULL THEN wt.workspace_id
|
|
1007
|
+
ELSE COALESCE(card.workspace_id, '${GLOBAL_WORKSPACE_ID}') END AS workspace_id,
|
|
1008
|
+
CASE WHEN card.id IS NULL THEN wt.name ELSE card.title END AS name,
|
|
1009
|
+
CASE WHEN card.id IS NULL THEN wt.status
|
|
1010
|
+
WHEN card.status IN ('done', 'archived') THEN 'done' ELSE 'active' END AS status,
|
|
1011
|
+
CASE WHEN card.id IS NULL THEN wt.milestone_id ELSE card.milestone_id END AS milestone_id,
|
|
1012
|
+
wt.worktree_json, wt.layout_json, wt.cwd, wt.created_at, wt.last_opened_at, wt.layout_revision,
|
|
1013
|
+
wt.rowid AS _created_order
|
|
1014
|
+
FROM workspace_tasks wt LEFT JOIN wand_tasks card ON card.id = (
|
|
1015
|
+
SELECT id FROM wand_tasks WHERE workspace_task_id = wt.id
|
|
1016
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1
|
|
1017
|
+
)`;
|
|
1018
|
+
function withoutSessionTab(node, sessionId) {
|
|
1019
|
+
if (node.type === "split")
|
|
1020
|
+
return {
|
|
1021
|
+
...node,
|
|
1022
|
+
children: [withoutSessionTab(node.children[0], sessionId), withoutSessionTab(node.children[1], sessionId)],
|
|
1023
|
+
};
|
|
1024
|
+
const activeId = node.tabs[node.active]?.id;
|
|
1025
|
+
const tabs = node.tabs.filter((tab) => tab.kind !== "session" || tab.sessionId !== sessionId);
|
|
1026
|
+
const active = tabs.findIndex((tab) => tab.id === activeId);
|
|
1027
|
+
return { ...node, tabs, active: active >= 0 ? active : Math.min(node.active, Math.max(0, tabs.length - 1)) };
|
|
1028
|
+
}
|
|
999
1029
|
export class WandStorage {
|
|
1000
1030
|
db;
|
|
1001
1031
|
dbPath;
|
|
@@ -1016,6 +1046,7 @@ export class WandStorage {
|
|
|
1016
1046
|
ensureAiTeamSchema(this.db);
|
|
1017
1047
|
ensureConnectorSchema(this.db);
|
|
1018
1048
|
this.ensureDefaultPasswordVault();
|
|
1049
|
+
this.migrateTaskRecords();
|
|
1019
1050
|
}
|
|
1020
1051
|
directory() {
|
|
1021
1052
|
return path.dirname(this.dbPath);
|
|
@@ -1233,20 +1264,40 @@ export class WandStorage {
|
|
|
1233
1264
|
this.db.prepare("UPDATE workspaces SET last_opened_at = ? WHERE id = ?").run(nowIso(), id);
|
|
1234
1265
|
}
|
|
1235
1266
|
deleteWorkspace(id, options = {}) {
|
|
1236
|
-
|
|
1237
|
-
this.db.prepare(`
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1267
|
+
this.transaction(() => {
|
|
1268
|
+
const cards = this.db.prepare(`SELECT ${WAND_TASK_FIELDS} FROM wand_tasks
|
|
1269
|
+
WHERE workspace_task_id IN (SELECT id FROM workspace_tasks WHERE workspace_id = ?)`)
|
|
1270
|
+
.all(id);
|
|
1271
|
+
if (options.cascade) {
|
|
1272
|
+
this.db.prepare(`DELETE FROM command_sessions WHERE workspace_id = ?
|
|
1273
|
+
OR workspace_task_id IN (SELECT id FROM workspace_tasks WHERE workspace_id = ?)`)
|
|
1274
|
+
.run(id, id);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
this.db.prepare(`UPDATE command_sessions SET workspace_id = NULL, workspace_task_id = NULL
|
|
1278
|
+
WHERE workspace_id = ? OR workspace_task_id IN
|
|
1279
|
+
(SELECT id FROM workspace_tasks WHERE workspace_id = ?)`)
|
|
1280
|
+
.run(id, id);
|
|
1281
|
+
}
|
|
1282
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_task_id = NULL
|
|
1283
|
+
WHERE workspace_task_id IN (SELECT id FROM workspace_tasks WHERE workspace_id = ?)`)
|
|
1284
|
+
.run(id);
|
|
1285
|
+
this.db.prepare("DELETE FROM workspaces WHERE id = ?").run(id);
|
|
1286
|
+
// Direct project removal detaches active cards; their next dispatch still uses the default cwd.
|
|
1287
|
+
// Route-level destructive removal archives cards first, so those cards never gain a container.
|
|
1288
|
+
for (const row of cards) {
|
|
1289
|
+
const card = this.mapWandTaskRow(row);
|
|
1290
|
+
if (card.status === "archived")
|
|
1291
|
+
continue;
|
|
1292
|
+
const global = this.ensureGlobalWorkspace();
|
|
1293
|
+
const containerId = this.insertWorkspaceTask({
|
|
1294
|
+
workspaceId: global.id, name: card.title, milestoneId: card.milestoneId,
|
|
1295
|
+
status: card.status === "done" ? "done" : "active", createdAt: card.createdAt,
|
|
1296
|
+
});
|
|
1297
|
+
this.writeWandTask(this.getWandTask(card.id), { workspaceTaskId: containerId });
|
|
1298
|
+
}
|
|
1299
|
+
this.forgetWorkspaceGroupOrder(id);
|
|
1300
|
+
});
|
|
1250
1301
|
}
|
|
1251
1302
|
listSessionsByWorkspace(workspaceId) {
|
|
1252
1303
|
const rows = this.db
|
|
@@ -1264,7 +1315,10 @@ export class WandStorage {
|
|
|
1264
1315
|
return row ? { workspaceId: row.workspace_id ?? undefined, workspaceTaskId: row.workspace_task_id ?? undefined } : null;
|
|
1265
1316
|
}
|
|
1266
1317
|
setSessionWorkspaceId(sessionId, workspaceId) {
|
|
1267
|
-
|
|
1318
|
+
const binding = this.getSessionWorkspace(sessionId);
|
|
1319
|
+
const task = binding?.workspaceTaskId ? this.getWorkspaceTask(binding.workspaceTaskId) : null;
|
|
1320
|
+
this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?")
|
|
1321
|
+
.run(task?.workspaceId ?? workspaceId, sessionId);
|
|
1268
1322
|
}
|
|
1269
1323
|
/** Lightweight count of persisted sessions grouped by workspace. */
|
|
1270
1324
|
countSessionsByWorkspace() {
|
|
@@ -1306,84 +1360,78 @@ export class WandStorage {
|
|
|
1306
1360
|
}));
|
|
1307
1361
|
}
|
|
1308
1362
|
// ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
|
|
1309
|
-
/**
|
|
1363
|
+
/** Both task DTOs read one set of task metadata; containers own only local layout/worktree. */
|
|
1310
1364
|
listWorkspaceTasks(workspaceId) {
|
|
1311
|
-
const rows = this.db
|
|
1312
|
-
|
|
1313
|
-
FROM workspace_tasks WHERE workspace_id = ?
|
|
1314
|
-
ORDER BY created_at DESC, rowid DESC`)
|
|
1315
|
-
.all(workspaceId);
|
|
1365
|
+
const rows = this.db.prepare(`SELECT * FROM (${WORKSPACE_TASK_PROJECTION}) WHERE workspace_id = ?
|
|
1366
|
+
ORDER BY created_at DESC, _created_order DESC`).all(workspaceId);
|
|
1316
1367
|
return rows.map(mapWorkspaceTaskRow);
|
|
1317
1368
|
}
|
|
1318
1369
|
getWorkspaceTask(id) {
|
|
1319
|
-
const row = this.db
|
|
1320
|
-
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, milestone_id, created_at, last_opened_at, layout_revision
|
|
1321
|
-
FROM workspace_tasks WHERE id = ?`)
|
|
1370
|
+
const row = this.db.prepare(`${WORKSPACE_TASK_PROJECTION} WHERE wt.id = ?`)
|
|
1322
1371
|
.get(id);
|
|
1323
1372
|
return row ? mapWorkspaceTaskRow(row) : null;
|
|
1324
1373
|
}
|
|
1325
|
-
|
|
1374
|
+
insertWorkspaceTask(input) {
|
|
1326
1375
|
const id = crypto.randomUUID();
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1376
|
+
this.db.prepare(`INSERT INTO workspace_tasks
|
|
1377
|
+
(id, workspace_id, name, worktree_json, layout_json, status, cwd, milestone_id, created_at, last_opened_at)
|
|
1378
|
+
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, NULL)`)
|
|
1379
|
+
.run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, input.status ?? "active", input.cwd?.trim() || null, input.milestoneId ?? null, input.createdAt ?? nowIso());
|
|
1380
|
+
return id;
|
|
1381
|
+
}
|
|
1382
|
+
taskMilestoneForWrite(id, workspaceId) {
|
|
1383
|
+
if (!id)
|
|
1384
|
+
return this.ensureDefaultWandMilestone().id;
|
|
1385
|
+
const milestone = this.getWandMilestone(id);
|
|
1386
|
+
if (!milestone)
|
|
1387
|
+
throw new Error("未找到该里程碑。");
|
|
1388
|
+
return workspaceId && milestone.workspaceId && milestone.workspaceId !== workspaceId
|
|
1389
|
+
? this.ensureDefaultWandMilestone().id : id;
|
|
1390
|
+
}
|
|
1391
|
+
createWorkspaceTask(input) {
|
|
1392
|
+
return this.transaction(() => {
|
|
1393
|
+
const workspace = this.getWorkspace(input.workspaceId);
|
|
1394
|
+
if (!workspace)
|
|
1395
|
+
throw new Error("任务所属工作区不存在。");
|
|
1396
|
+
const projectId = workspace.kind === "global" || workspace.id === GLOBAL_WORKSPACE_ID ? null : workspace.id;
|
|
1397
|
+
const milestoneId = this.taskMilestoneForWrite(input.milestoneId, projectId);
|
|
1398
|
+
const id = this.insertWorkspaceTask({ ...input, milestoneId });
|
|
1399
|
+
this.insertWandTask({
|
|
1400
|
+
...input.board, workspaceTaskId: id,
|
|
1401
|
+
workspaceId: projectId,
|
|
1402
|
+
title: input.name,
|
|
1403
|
+
titleSource: input.board?.titleSource ?? (isUnnamedWorkspaceTaskName(input.name) ? "auto" : "user"),
|
|
1404
|
+
status: input.status === "done" ? "done" : "todo", milestoneId,
|
|
1405
|
+
});
|
|
1406
|
+
return this.getWorkspaceTask(id);
|
|
1407
|
+
});
|
|
1347
1408
|
}
|
|
1348
1409
|
updateWorkspaceTask(id, patch) {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
}
|
|
1359
|
-
if (patch.status !== undefined) {
|
|
1360
|
-
assignments.push("status = ?");
|
|
1361
|
-
values.push(patch.status);
|
|
1362
|
-
}
|
|
1363
|
-
if (patch.worktree !== undefined) {
|
|
1364
|
-
assignments.push("worktree_json = ?");
|
|
1365
|
-
values.push(patch.worktree ? JSON.stringify(patch.worktree) : null);
|
|
1366
|
-
}
|
|
1367
|
-
if (patch.milestoneId !== undefined) {
|
|
1368
|
-
assignments.push("milestone_id = ?");
|
|
1369
|
-
values.push(patch.milestoneId || null);
|
|
1370
|
-
}
|
|
1371
|
-
if (assignments.length === 0)
|
|
1372
|
-
return;
|
|
1373
|
-
this.db.prepare(`UPDATE workspace_tasks SET ${assignments.join(", ")} WHERE id = ?`).run(...values, id);
|
|
1374
|
-
const card = this.getWandTaskByWorkspaceTaskId(id);
|
|
1375
|
-
if (card) {
|
|
1376
|
-
// Reverse projection uses direct SQL, so these writers cannot recurse.
|
|
1377
|
-
this.updateWandTask(card.id, {
|
|
1410
|
+
this.transaction(() => {
|
|
1411
|
+
const card = this.getWandTaskByWorkspaceTaskId(id);
|
|
1412
|
+
if (!card)
|
|
1413
|
+
throw new Error("未找到该任务。");
|
|
1414
|
+
if (patch.worktree !== undefined) {
|
|
1415
|
+
this.db.prepare("UPDATE workspace_tasks SET worktree_json = ? WHERE id = ?")
|
|
1416
|
+
.run(patch.worktree ? JSON.stringify(patch.worktree) : null, id);
|
|
1417
|
+
}
|
|
1418
|
+
this.writeWandTask(card, {
|
|
1378
1419
|
...(patch.name !== undefined ? { title: patch.name, titleSource: "user" } : {}),
|
|
1379
1420
|
...(patch.milestoneId !== undefined ? { milestoneId: patch.milestoneId } : {}),
|
|
1421
|
+
...(patch.workspaceId !== undefined ? {
|
|
1422
|
+
workspaceId: patch.workspaceId === GLOBAL_WORKSPACE_ID ? null : patch.workspaceId,
|
|
1423
|
+
} : {}),
|
|
1380
1424
|
...(patch.status !== undefined ? {
|
|
1381
1425
|
status: patch.status === "done"
|
|
1382
1426
|
? card.status === "archived" ? "archived" : "done"
|
|
1383
1427
|
: card.status === "todo" ? "todo" : "doing",
|
|
1384
1428
|
} : {}),
|
|
1385
1429
|
});
|
|
1386
|
-
}
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
archiveWorkspaceTask(id) {
|
|
1433
|
+
const card = this.getWandTaskByWorkspaceTaskId(id);
|
|
1434
|
+
return card ? this.updateWandTask(card.id, { status: "archived" }) : null;
|
|
1387
1435
|
}
|
|
1388
1436
|
saveWorkspaceTaskLayout(id, layout, expectedRevision) {
|
|
1389
1437
|
const current = this.getWorkspaceTask(id);
|
|
@@ -1403,13 +1451,17 @@ export class WandStorage {
|
|
|
1403
1451
|
this.db.prepare("UPDATE workspace_tasks SET last_opened_at = ? WHERE id = ?").run(nowIso(), id);
|
|
1404
1452
|
}
|
|
1405
1453
|
deleteWorkspaceTask(id, options = {}) {
|
|
1406
|
-
|
|
1407
|
-
this.db.prepare(
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1454
|
+
this.transaction(() => {
|
|
1455
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_task_id = NULL, status = 'archived', updated_at = ?
|
|
1456
|
+
WHERE workspace_task_id = ?`).run(nowIso(), id);
|
|
1457
|
+
if (options.cascade) {
|
|
1458
|
+
this.db.prepare("DELETE FROM command_sessions WHERE workspace_task_id = ?").run(id);
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
this.db.prepare("UPDATE command_sessions SET workspace_task_id = NULL WHERE workspace_task_id = ?").run(id);
|
|
1462
|
+
}
|
|
1463
|
+
this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(id);
|
|
1464
|
+
});
|
|
1413
1465
|
}
|
|
1414
1466
|
tasksAggregateFingerprint() {
|
|
1415
1467
|
const sessions = this.db.prepare(`SELECT COUNT(*) AS count,
|
|
@@ -1421,7 +1473,7 @@ export class WandStorage {
|
|
|
1421
1473
|
// timestamp/revision. Layout payloads are represented by their own revision.
|
|
1422
1474
|
const tasks = this.db.prepare(`SELECT id, workspace_id, name, status, cwd, worktree_json, milestone_id,
|
|
1423
1475
|
created_at, last_opened_at, layout_revision
|
|
1424
|
-
FROM
|
|
1476
|
+
FROM (${WORKSPACE_TASK_PROJECTION}) ORDER BY id`).all();
|
|
1425
1477
|
const workspaces = this.db.prepare(`SELECT COUNT(*) AS count, COALESCE(MAX(last_opened_at), '') AS opened FROM workspaces`).get();
|
|
1426
1478
|
// 名称也进指纹:改名(含工作区/项目名与目录自定义名)后客户端才肯重拉任务列表。
|
|
1427
1479
|
const names = this.db.prepare(`SELECT
|
|
@@ -1438,9 +1490,24 @@ export class WandStorage {
|
|
|
1438
1490
|
names,
|
|
1439
1491
|
});
|
|
1440
1492
|
}
|
|
1493
|
+
/**
|
|
1494
|
+
* 会话列表团队标记的廉价指纹:运行/步骤状态一变,`/api/tasks` 里的 `teamChat`/`teamStep`
|
|
1495
|
+
* 就跟着变,客户端必须肯重拉。只看聚合值和最近若干条状态,不逐行拼整张表。
|
|
1496
|
+
*/
|
|
1497
|
+
teamSessionFingerprint() {
|
|
1498
|
+
const runs = this.db.prepare(`SELECT COUNT(*) AS count,
|
|
1499
|
+
COALESCE((SELECT GROUP_CONCAT(status || ':' || COALESCE(chat_session_id, ''), '|')
|
|
1500
|
+
FROM (SELECT status, chat_session_id FROM ai_team_runs ORDER BY rowid DESC LIMIT 500)), '') AS statuses
|
|
1501
|
+
FROM ai_team_runs`).get();
|
|
1502
|
+
const steps = this.db.prepare(`SELECT COUNT(*) AS count,
|
|
1503
|
+
COALESCE((SELECT GROUP_CONCAT(status || ':' || COALESCE(session_id, ''), '|')
|
|
1504
|
+
FROM (SELECT status, session_id FROM ai_team_steps ORDER BY rowid DESC LIMIT 500)), '') AS statuses
|
|
1505
|
+
FROM ai_team_steps`).get();
|
|
1506
|
+
return JSON.stringify({ runs, steps });
|
|
1507
|
+
}
|
|
1441
1508
|
/** GET /api/wand-tasks 的顺序:新创建在前。客户端按此顺序渲染,不再本地排序。 */
|
|
1442
1509
|
listWandTasks(workspaceId) {
|
|
1443
|
-
const rows = this.db.prepare(`SELECT
|
|
1510
|
+
const rows = this.db.prepare(`SELECT ${WAND_TASK_FIELDS}
|
|
1444
1511
|
FROM wand_tasks ${workspaceId === undefined ? "" : "WHERE workspace_id IS ?"}
|
|
1445
1512
|
ORDER BY created_at DESC, rowid DESC`).all(...(workspaceId === undefined ? [] : [workspaceId]));
|
|
1446
1513
|
return rows.map((row) => this.mapWandTaskRow(row));
|
|
@@ -1468,42 +1535,181 @@ export class WandStorage {
|
|
|
1468
1535
|
};
|
|
1469
1536
|
}
|
|
1470
1537
|
getWandTask(id) {
|
|
1471
|
-
|
|
1538
|
+
const row = this.db.prepare(`SELECT ${WAND_TASK_FIELDS} FROM wand_tasks WHERE id = ?`)
|
|
1539
|
+
.get(id);
|
|
1540
|
+
return row ? this.mapWandTaskRow(row) : null;
|
|
1472
1541
|
}
|
|
1473
1542
|
getWandTaskByWorkspaceTaskId(workspaceTaskId) {
|
|
1474
|
-
const row = this.db.prepare(`SELECT
|
|
1475
|
-
|
|
1543
|
+
const row = this.db.prepare(`SELECT ${WAND_TASK_FIELDS} FROM wand_tasks
|
|
1544
|
+
WHERE workspace_task_id = ? ORDER BY updated_at DESC, rowid DESC LIMIT 1`)
|
|
1545
|
+
.get(workspaceTaskId);
|
|
1476
1546
|
return row ? this.mapWandTaskRow(row) : null;
|
|
1477
1547
|
}
|
|
1478
|
-
|
|
1548
|
+
insertWandTask(input, createdAt = nowIso()) {
|
|
1479
1549
|
const id = crypto.randomUUID();
|
|
1480
|
-
const now = nowIso();
|
|
1481
1550
|
const status = input.status ?? "todo";
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
this.db.prepare(`INSERT INTO wand_tasks (
|
|
1551
|
+
const max = this.db.prepare(`SELECT COALESCE(MAX(sort_order), -1) AS value FROM wand_tasks
|
|
1552
|
+
WHERE workspace_id IS ? AND status = ?`).get(input.workspaceId ?? null, status);
|
|
1553
|
+
const number = this.db.prepare(`SELECT COALESCE(MAX(CAST(substr(identifier, 6) AS INTEGER)), 0) AS value
|
|
1554
|
+
FROM wand_tasks WHERE identifier GLOB 'TASK-[0-9]*'`).get();
|
|
1555
|
+
this.db.prepare(`INSERT INTO wand_tasks (${WAND_TASK_FIELDS})
|
|
1556
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1557
|
+
.run(id, `TASK-${number.value + 1}`, input.workspaceId ?? null, input.workspaceTaskId ?? null, input.parentTaskId ?? null, input.title, input.titleSource ?? "user", null, input.description ?? "", status, input.priority ?? DEFAULT_WAND_TASK_PRIORITY, JSON.stringify(input.labels ?? []), input.dueDate ?? null, input.milestoneId ?? null, max.value + 1, input.agent ? JSON.stringify(input.agent) : null, createdAt, createdAt);
|
|
1487
1558
|
return this.getWandTask(id);
|
|
1488
1559
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1560
|
+
createWandTask(input) {
|
|
1561
|
+
return this.transaction(() => {
|
|
1562
|
+
const linked = input.workspaceTaskId ? this.getWorkspaceTask(input.workspaceTaskId) : null;
|
|
1563
|
+
const workspaceId = input.workspaceId === undefined && linked
|
|
1564
|
+
? linked.workspaceId === GLOBAL_WORKSPACE_ID ? null : linked.workspaceId
|
|
1565
|
+
: input.workspaceId ?? null;
|
|
1566
|
+
const workspace = workspaceId ? this.getWorkspace(workspaceId) : this.ensureGlobalWorkspace();
|
|
1567
|
+
if (!workspace)
|
|
1568
|
+
throw new Error("任务所属工作区不存在。");
|
|
1569
|
+
const milestoneId = this.taskMilestoneForWrite(input.milestoneId, workspaceId);
|
|
1570
|
+
if (input.workspaceTaskId) {
|
|
1571
|
+
const container = this.getWorkspaceTask(input.workspaceTaskId);
|
|
1572
|
+
if (!container)
|
|
1573
|
+
throw new Error("未找到该任务。");
|
|
1574
|
+
const existing = this.getWandTaskByWorkspaceTaskId(container.id);
|
|
1575
|
+
// Compatibility for callers which supplied a previously created container.
|
|
1576
|
+
if (existing)
|
|
1577
|
+
return this.writeWandTask(existing, { ...input, workspaceId, milestoneId });
|
|
1578
|
+
}
|
|
1579
|
+
const workspaceTaskId = input.workspaceTaskId ?? this.insertWorkspaceTask({
|
|
1580
|
+
workspaceId: workspace.id, name: input.title,
|
|
1581
|
+
status: input.status === "done" || input.status === "archived" ? "done" : "active",
|
|
1582
|
+
milestoneId,
|
|
1583
|
+
});
|
|
1584
|
+
return this.insertWandTask({ ...input, workspaceId, workspaceTaskId, milestoneId });
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
/** Private canonical writer; callers own the transaction, never a reverse writer. */
|
|
1588
|
+
writeWandTask(current, patch) {
|
|
1493
1589
|
const next = { ...current, ...patch, updatedAt: nowIso() };
|
|
1494
|
-
|
|
1495
|
-
|
|
1590
|
+
if (patch.milestoneId !== undefined || patch.workspaceId !== undefined) {
|
|
1591
|
+
next.milestoneId = this.taskMilestoneForWrite(next.milestoneId, next.workspaceId);
|
|
1592
|
+
}
|
|
1593
|
+
if (!next.workspaceTaskId && next.status !== "archived") {
|
|
1594
|
+
const workspace = next.workspaceId ? this.getWorkspace(next.workspaceId) : this.ensureGlobalWorkspace();
|
|
1595
|
+
if (!workspace)
|
|
1596
|
+
throw new Error("任务所属工作区不存在。");
|
|
1597
|
+
next.workspaceTaskId = this.insertWorkspaceTask({
|
|
1598
|
+
workspaceId: workspace.id, name: next.title, milestoneId: next.milestoneId,
|
|
1599
|
+
status: next.status === "done" ? "done" : "active", createdAt: next.createdAt,
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
this.db.prepare(`UPDATE wand_tasks SET workspace_id = ?, workspace_task_id = ?, parent_task_id = ?,
|
|
1603
|
+
title = ?, title_source = ?, auto_title_signature = ?, description = ?, status = ?, priority = ?,
|
|
1604
|
+
labels_json = ?, due_date = ?, milestone_id = ?, sort_order = ?, agent_json = ?, updated_at = ?
|
|
1605
|
+
WHERE id = ?`)
|
|
1606
|
+
.run(next.workspaceId, next.workspaceTaskId, next.parentTaskId, next.title, next.titleSource, next.autoTitleSignature ?? null, next.description, next.status, next.priority, JSON.stringify(next.labels), next.dueDate, next.milestoneId ?? null, next.sortOrder, next.agent ? JSON.stringify(next.agent) : null, next.updatedAt, next.id);
|
|
1607
|
+
if (patch.workspaceId !== undefined && next.workspaceTaskId) {
|
|
1496
1608
|
const workspaceId = next.workspaceId ?? this.ensureGlobalWorkspace().id;
|
|
1497
|
-
|
|
1498
|
-
|
|
1609
|
+
// The container FK/index and standalone session workspace bucket follow the canonical project.
|
|
1610
|
+
this.db.prepare("UPDATE workspace_tasks SET workspace_id = ? WHERE id = ?")
|
|
1611
|
+
.run(workspaceId, next.workspaceTaskId);
|
|
1499
1612
|
this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE workspace_task_id = ?")
|
|
1500
1613
|
.run(workspaceId, next.workspaceTaskId);
|
|
1501
1614
|
}
|
|
1502
1615
|
return next;
|
|
1503
1616
|
}
|
|
1617
|
+
updateWandTask(id, patch) {
|
|
1618
|
+
return this.transaction(() => {
|
|
1619
|
+
const current = this.getWandTask(id);
|
|
1620
|
+
if (!current)
|
|
1621
|
+
return null;
|
|
1622
|
+
return this.writeWandTask(current, patch);
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
/** Commit completion archives only the selected completed cards, atomically. */
|
|
1626
|
+
archiveCompletedWandTasks(ids) {
|
|
1627
|
+
return this.transaction(() => {
|
|
1628
|
+
const archived = [];
|
|
1629
|
+
for (const id of new Set(ids)) {
|
|
1630
|
+
const task = this.getWandTask(id);
|
|
1631
|
+
if (task?.status !== "done")
|
|
1632
|
+
continue;
|
|
1633
|
+
this.writeWandTask(task, { status: "archived" });
|
|
1634
|
+
archived.push(id);
|
|
1635
|
+
}
|
|
1636
|
+
return archived;
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1504
1639
|
deleteWandTask(id) {
|
|
1505
|
-
this.
|
|
1506
|
-
|
|
1640
|
+
this.transaction(() => {
|
|
1641
|
+
const card = this.getWandTask(id);
|
|
1642
|
+
if (!card)
|
|
1643
|
+
return;
|
|
1644
|
+
this.db.prepare("UPDATE wand_tasks SET parent_task_id = NULL WHERE parent_task_id = ?").run(id);
|
|
1645
|
+
if (card.workspaceTaskId) {
|
|
1646
|
+
this.db.prepare("UPDATE command_sessions SET workspace_task_id = NULL WHERE workspace_task_id = ?")
|
|
1647
|
+
.run(card.workspaceTaskId);
|
|
1648
|
+
this.db.prepare("DELETE FROM workspace_tasks WHERE id = ?").run(card.workspaceTaskId);
|
|
1649
|
+
}
|
|
1650
|
+
this.db.prepare("DELETE FROM wand_tasks WHERE id = ?").run(id);
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
/** One-time id-based reconciliation; normal reads never create tasks or repair membership. */
|
|
1654
|
+
migrateTaskRecords() {
|
|
1655
|
+
if (this.getPreference("pref:taskRecordsVersion", 0) === 1)
|
|
1656
|
+
return;
|
|
1657
|
+
this.transaction(() => {
|
|
1658
|
+
for (const workspace of this.listWorkspaces()) {
|
|
1659
|
+
for (const task of this.listWorkspaceTasks(workspace.id)) {
|
|
1660
|
+
if (this.getWandTaskByWorkspaceTaskId(task.id))
|
|
1661
|
+
continue;
|
|
1662
|
+
this.insertWandTask({
|
|
1663
|
+
workspaceId: workspace.kind === "global" || workspace.id === GLOBAL_WORKSPACE_ID ? null : workspace.id,
|
|
1664
|
+
workspaceTaskId: task.id, title: task.name,
|
|
1665
|
+
titleSource: isUnnamedWorkspaceTaskName(task.name) ? "auto" : "user",
|
|
1666
|
+
status: task.status === "done" ? "done" : "todo", milestoneId: task.milestoneId,
|
|
1667
|
+
}, task.createdAt);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
for (const card of this.listWandTasks()) {
|
|
1671
|
+
let container = card.workspaceTaskId ? this.getWorkspaceTask(card.workspaceTaskId) : null;
|
|
1672
|
+
if (!container && card.status !== "archived") {
|
|
1673
|
+
const workspace = card.workspaceId ? this.getWorkspace(card.workspaceId) : this.ensureGlobalWorkspace();
|
|
1674
|
+
if (!workspace)
|
|
1675
|
+
continue;
|
|
1676
|
+
const id = this.insertWorkspaceTask({
|
|
1677
|
+
workspaceId: workspace.id, name: card.title, milestoneId: card.milestoneId,
|
|
1678
|
+
status: card.status === "done" ? "done" : "active", createdAt: card.createdAt,
|
|
1679
|
+
});
|
|
1680
|
+
this.writeWandTask(card, { workspaceTaskId: id });
|
|
1681
|
+
container = this.getWorkspaceTask(id);
|
|
1682
|
+
}
|
|
1683
|
+
if (!container || this.getWandTaskByWorkspaceTaskId(container.id)?.id !== card.id)
|
|
1684
|
+
continue;
|
|
1685
|
+
this.db.prepare("UPDATE workspace_tasks SET workspace_id = ? WHERE id = ?")
|
|
1686
|
+
.run(container.workspaceId, container.id);
|
|
1687
|
+
const previouslyBound = new Set(this.legacyWandTaskSessionIds(card.id));
|
|
1688
|
+
for (const sessionId of previouslyBound) {
|
|
1689
|
+
const session = this.getSessionWorkspace(sessionId);
|
|
1690
|
+
if (!session || (session.workspaceTaskId && session.workspaceTaskId !== container.id))
|
|
1691
|
+
continue;
|
|
1692
|
+
this.db.prepare("UPDATE command_sessions SET workspace_task_id = ?, workspace_id = ? WHERE id = ?")
|
|
1693
|
+
.run(container.id, container.workspaceId, sessionId);
|
|
1694
|
+
}
|
|
1695
|
+
this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE workspace_task_id = ?")
|
|
1696
|
+
.run(container.workspaceId, container.id);
|
|
1697
|
+
const sessions = this.listSessionsByWorkspaceTaskSlim(container.id);
|
|
1698
|
+
for (const session of sessions) {
|
|
1699
|
+
this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)")
|
|
1700
|
+
.run(card.id, session.id, nowIso());
|
|
1701
|
+
}
|
|
1702
|
+
const agentSession = sessions.find((session) => isSessionProvider(session.provider));
|
|
1703
|
+
const newlyAssigned = sessions.some((session) => !previouslyBound.has(session.id));
|
|
1704
|
+
if (agentSession && (!card.agent || newlyAssigned)) {
|
|
1705
|
+
this.recordTaskSession(agentSession, newlyAssigned);
|
|
1706
|
+
}
|
|
1707
|
+
else if (newlyAssigned && sessions[0]) {
|
|
1708
|
+
this.recordTaskSession(sessions[0]);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
this.setPreference("pref:taskRecordsVersion", 1);
|
|
1712
|
+
});
|
|
1507
1713
|
}
|
|
1508
1714
|
// ── 里程碑(迭代,归属工作区;任务只存 milestoneId)──
|
|
1509
1715
|
/**
|
|
@@ -1664,28 +1870,109 @@ export class WandStorage {
|
|
|
1664
1870
|
return Number(result.changes) || 0;
|
|
1665
1871
|
}
|
|
1666
1872
|
// ============ 会话 → 任务 / 迭代 ============
|
|
1667
|
-
/**
|
|
1873
|
+
/** Live membership has a single owner: command_sessions.workspace_task_id. */
|
|
1668
1874
|
getWandTaskIdForSession(sessionId) {
|
|
1669
|
-
const
|
|
1670
|
-
|
|
1875
|
+
const binding = this.getSessionWorkspace(sessionId);
|
|
1876
|
+
if (binding?.workspaceTaskId)
|
|
1877
|
+
return this.getWandTaskByWorkspaceTaskId(binding.workspaceTaskId)?.id ?? null;
|
|
1878
|
+
const row = this.db.prepare(`SELECT links.task_id FROM wand_task_sessions links
|
|
1879
|
+
JOIN wand_tasks card ON card.id = links.task_id
|
|
1880
|
+
WHERE links.session_id = ? AND card.workspace_task_id IS NULL AND card.status = 'archived'
|
|
1881
|
+
ORDER BY links.rowid DESC LIMIT 1`).get(sessionId);
|
|
1882
|
+
return row?.task_id ?? null;
|
|
1883
|
+
}
|
|
1884
|
+
legacyWandTaskSessionIds(taskId) {
|
|
1885
|
+
const rows = this.db.prepare(`SELECT session_id FROM wand_task_sessions
|
|
1886
|
+
WHERE task_id = ? ORDER BY rowid ASC, session_id ASC`).all(taskId);
|
|
1887
|
+
return rows.map((row) => row.session_id);
|
|
1671
1888
|
}
|
|
1672
1889
|
listWandTaskSessionIds(taskId) {
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1890
|
+
const task = this.getWandTask(taskId);
|
|
1891
|
+
if (!task)
|
|
1892
|
+
return [];
|
|
1893
|
+
if (!task.workspaceTaskId)
|
|
1894
|
+
return task.status === "archived" ? this.legacyWandTaskSessionIds(taskId) : [];
|
|
1895
|
+
// Binding timestamps are ordering metadata only; stale membership cannot leak into this projection.
|
|
1896
|
+
const rows = this.db.prepare(`SELECT s.id FROM command_sessions s
|
|
1897
|
+
LEFT JOIN wand_task_sessions links ON links.session_id = s.id AND links.task_id = ?
|
|
1898
|
+
WHERE s.workspace_task_id = ? ORDER BY COALESCE(links.bound_at, s.started_at), s.rowid`)
|
|
1899
|
+
.all(task.id, task.workspaceTaskId);
|
|
1900
|
+
return rows.map((row) => row.id);
|
|
1675
1901
|
}
|
|
1676
1902
|
listBoundWandTaskSessionIds() {
|
|
1677
|
-
const rows = this.db.prepare(
|
|
1678
|
-
|
|
1903
|
+
const rows = this.db.prepare(`SELECT s.id FROM command_sessions s
|
|
1904
|
+
WHERE EXISTS (SELECT 1 FROM wand_tasks card WHERE card.workspace_task_id = s.workspace_task_id)
|
|
1905
|
+
ORDER BY s.rowid`).all();
|
|
1906
|
+
return rows.map((row) => row.id);
|
|
1679
1907
|
}
|
|
1680
1908
|
bindWandTaskSession(taskId, sessionId) {
|
|
1681
|
-
|
|
1909
|
+
const task = this.getWandTask(taskId);
|
|
1910
|
+
if (!task?.workspaceTaskId)
|
|
1682
1911
|
throw new Error("未找到该任务。");
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
this.
|
|
1912
|
+
this.moveSessionToWorkspaceTask(sessionId, task.workspaceTaskId);
|
|
1913
|
+
}
|
|
1914
|
+
unbindWandTaskSession(taskId, sessionId) {
|
|
1915
|
+
if (this.getWandTaskIdForSession(sessionId) === taskId)
|
|
1916
|
+
this.moveSessionToWorkspaceTask(sessionId, null);
|
|
1917
|
+
else
|
|
1918
|
+
this.db.prepare("DELETE FROM wand_task_sessions WHERE task_id = ? AND session_id = ?").run(taskId, sessionId);
|
|
1919
|
+
}
|
|
1920
|
+
/** Atomic exclusive ownership change; cwd, output and execution are untouched. */
|
|
1921
|
+
moveSessionToWorkspaceTask(sessionId, taskId) {
|
|
1922
|
+
this.transaction(() => {
|
|
1923
|
+
const session = this.getSessionWorkspace(sessionId);
|
|
1924
|
+
if (!session)
|
|
1925
|
+
throw new Error("未找到该会话。");
|
|
1926
|
+
const target = taskId ? this.getWorkspaceTask(taskId) : null;
|
|
1927
|
+
if (taskId && !target)
|
|
1928
|
+
throw new Error("未找到目标任务。");
|
|
1929
|
+
if (target && !this.getWorkspace(target.workspaceId))
|
|
1930
|
+
throw new Error("目标工作区不存在。");
|
|
1931
|
+
const source = session.workspaceTaskId ? this.getWorkspaceTask(session.workspaceTaskId) : null;
|
|
1932
|
+
if (source && source.id !== target?.id && source.layout) {
|
|
1933
|
+
const windows = source.layout.windows.map((window) => {
|
|
1934
|
+
const layout = withoutSessionTab(window.layout, sessionId);
|
|
1935
|
+
return { ...window, layout, activeTabId: firstLayoutTabId(layout) };
|
|
1936
|
+
});
|
|
1937
|
+
this.saveWorkspaceTaskLayout(source.id, { ...source.layout, windows });
|
|
1938
|
+
}
|
|
1939
|
+
const targetCard = target ? this.getWandTaskByWorkspaceTaskId(target.id) : null;
|
|
1940
|
+
if (target && !targetCard)
|
|
1941
|
+
throw new Error("未找到目标任务。");
|
|
1942
|
+
this.db.prepare("DELETE FROM wand_task_sessions WHERE session_id = ? AND (? IS NULL OR task_id != ?)")
|
|
1943
|
+
.run(sessionId, targetCard?.id ?? null, targetCard?.id ?? null);
|
|
1944
|
+
this.db.prepare("UPDATE command_sessions SET workspace_task_id = ?, workspace_id = ? WHERE id = ?")
|
|
1945
|
+
.run(target?.id ?? null, target?.workspaceId ?? session.workspaceId ?? null, sessionId);
|
|
1946
|
+
if (target) {
|
|
1947
|
+
this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)")
|
|
1948
|
+
.run(targetCard.id, sessionId, nowIso());
|
|
1949
|
+
if (source?.id !== target.id)
|
|
1950
|
+
this.recordTaskSession(this.getSessionSlim(sessionId));
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
/** Metadata promotion happens on session creation/move, never on reads or checkpoints. */
|
|
1955
|
+
recordTaskSession(session, newlyAssigned = true) {
|
|
1956
|
+
if (!session.workspaceTaskId)
|
|
1957
|
+
return;
|
|
1958
|
+
const card = this.getWandTaskByWorkspaceTaskId(session.workspaceTaskId);
|
|
1959
|
+
if (!card)
|
|
1960
|
+
return;
|
|
1961
|
+
this.db.prepare("INSERT OR IGNORE INTO wand_task_sessions (task_id, session_id, bound_at) VALUES (?, ?, ?)")
|
|
1962
|
+
.run(card.id, session.id, nowIso());
|
|
1963
|
+
const provider = session.provider;
|
|
1964
|
+
const agent = isSessionProvider(provider) ? {
|
|
1965
|
+
provider,
|
|
1966
|
+
model: session.selectedModel || session.structuredState?.model || "default",
|
|
1967
|
+
thinkingEffort: isThinkingEffort(session.thinkingEffort) ? session.thinkingEffort : "off",
|
|
1968
|
+
mode: normalizeWandTaskAgentMode(provider, session.mode),
|
|
1969
|
+
kind: session.sessionKind === "structured" ? "structured" : "pty",
|
|
1970
|
+
} : null;
|
|
1971
|
+
this.db.prepare(`UPDATE wand_tasks SET agent_json = COALESCE(agent_json, ?),
|
|
1972
|
+
status = CASE WHEN status = 'todo' AND ? THEN 'doing' ELSE status END,
|
|
1973
|
+
updated_at = ? WHERE id = ?`)
|
|
1974
|
+
.run(agent ? JSON.stringify(agent) : null, newlyAssigned ? 1 : 0, nowIso(), card.id);
|
|
1687
1975
|
}
|
|
1688
|
-
unbindWandTaskSession(taskId, sessionId) { this.db.prepare("DELETE FROM wand_task_sessions WHERE task_id = ? AND session_id = ?").run(taskId, sessionId); }
|
|
1689
1976
|
listGithubIssueBindings(owner, repo, issueNumber) {
|
|
1690
1977
|
const rows = this.db.prepare(`SELECT session_id, bound_at FROM github_issue_bindings
|
|
1691
1978
|
WHERE owner = ? AND repo = ? AND issue_number = ? ORDER BY bound_at ASC`).all(owner, repo, issueNumber);
|
|
@@ -1725,7 +2012,7 @@ export class WandStorage {
|
|
|
1725
2012
|
return rows.map((row) => this.mapSessionRow(row));
|
|
1726
2013
|
}
|
|
1727
2014
|
setSessionWorkspaceTaskId(sessionId, taskId) {
|
|
1728
|
-
this.
|
|
2015
|
+
this.moveSessionToWorkspaceTask(sessionId, taskId);
|
|
1729
2016
|
}
|
|
1730
2017
|
/** Get password from database */
|
|
1731
2018
|
getPassword() {
|
|
@@ -2014,8 +2301,9 @@ export class WandStorage {
|
|
|
2014
2301
|
* 一次查询建整张表,供 /api/tasks 每请求一次构建;不逐会话查询。
|
|
2015
2302
|
*/
|
|
2016
2303
|
listAiTeamRunChatMarkers() {
|
|
2017
|
-
const rows = this.db.prepare(`SELECT id, team_json, chat_session_id
|
|
2018
|
-
|
|
2304
|
+
const rows = this.db.prepare(`SELECT r.id, r.team_json, r.chat_session_id, t.name AS current_team_name
|
|
2305
|
+
FROM ai_team_runs r LEFT JOIN ai_teams t ON t.id = r.team_id
|
|
2306
|
+
WHERE r.chat_session_id IS NOT NULL AND r.chat_session_id <> '' ORDER BY r.created_at DESC`).all();
|
|
2019
2307
|
const markers = new Map();
|
|
2020
2308
|
for (const row of rows) {
|
|
2021
2309
|
const chatSessionId = typeof row.chat_session_id === "string" ? row.chat_session_id : "";
|
|
@@ -2024,12 +2312,59 @@ export class WandStorage {
|
|
|
2024
2312
|
const rawTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
|
|
2025
2313
|
markers.set(chatSessionId, {
|
|
2026
2314
|
runId: String(row.id),
|
|
2027
|
-
teamName: String(rawTeam?.name ?? ""),
|
|
2315
|
+
teamName: typeof row.current_team_name === "string" ? row.current_team_name : String(rawTeam?.name ?? ""),
|
|
2028
2316
|
memberCount: Array.isArray(rawTeam?.members) ? rawTeam.members.length : 0,
|
|
2029
2317
|
});
|
|
2030
2318
|
}
|
|
2031
2319
|
return markers;
|
|
2032
2320
|
}
|
|
2321
|
+
/**
|
|
2322
|
+
* 会话列表用的成员步骤标记:session_id → 这一步的身份。
|
|
2323
|
+
* 和 `listAiTeamRunChatMarkers()` 一样一次查询建整张表,供 /api/tasks 每请求一次构建。
|
|
2324
|
+
* 同一个会话被多个步骤复用时取 seq 最大的那一步(最近一次派发)。
|
|
2325
|
+
*/
|
|
2326
|
+
listAiTeamStepSessionMarkers() {
|
|
2327
|
+
const rows = this.db.prepare(`SELECT s.id, s.run_id, s.kind, s.seq, s.member_id, s.title, s.status, s.session_id,
|
|
2328
|
+
r.status AS run_status, r.team_json, t.name AS current_team_name, t.members_json AS current_members_json
|
|
2329
|
+
FROM ai_team_steps s
|
|
2330
|
+
JOIN ai_team_runs r ON r.id = s.run_id
|
|
2331
|
+
LEFT JOIN ai_teams t ON t.id = r.team_id
|
|
2332
|
+
WHERE s.session_id IS NOT NULL AND s.session_id <> ''
|
|
2333
|
+
ORDER BY s.seq DESC`).all();
|
|
2334
|
+
const markers = new Map();
|
|
2335
|
+
const currentTeams = new Map();
|
|
2336
|
+
for (const row of rows) {
|
|
2337
|
+
const sessionId = typeof row.session_id === "string" ? row.session_id : "";
|
|
2338
|
+
if (!sessionId || markers.has(sessionId))
|
|
2339
|
+
continue;
|
|
2340
|
+
const runTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
|
|
2341
|
+
const runId = String(row.run_id);
|
|
2342
|
+
const memberId = String(row.member_id ?? "");
|
|
2343
|
+
if (!currentTeams.has(runId)) {
|
|
2344
|
+
// ai_teams.members_json 存的是裸成员数组;team_json 存的是整份团队对象。
|
|
2345
|
+
currentTeams.set(runId, safeJsonParse(typeof row.current_members_json === "string" ? row.current_members_json : null));
|
|
2346
|
+
}
|
|
2347
|
+
const memberName = teamMemberName(currentTeams.get(runId), memberId)
|
|
2348
|
+
?? teamMemberName(runTeam?.members, memberId)
|
|
2349
|
+
?? memberId;
|
|
2350
|
+
const runStatus = String(row.run_status ?? "");
|
|
2351
|
+
markers.set(sessionId, {
|
|
2352
|
+
runId,
|
|
2353
|
+
stepId: String(row.id),
|
|
2354
|
+
kind: String(row.kind ?? "work") === "leader" ? "leader" : "work",
|
|
2355
|
+
title: String(row.title ?? ""),
|
|
2356
|
+
memberId,
|
|
2357
|
+
memberName,
|
|
2358
|
+
teamName: typeof row.current_team_name === "string" && row.current_team_name
|
|
2359
|
+
? row.current_team_name
|
|
2360
|
+
: String(runTeam?.name ?? ""),
|
|
2361
|
+
stepStatus: String(row.status ?? "queued"),
|
|
2362
|
+
runStatus,
|
|
2363
|
+
runFinished: AI_TEAM_TERMINAL_RUN_STATUSES.includes(runStatus),
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
return markers;
|
|
2367
|
+
}
|
|
2033
2368
|
getAiTeamRun(id) {
|
|
2034
2369
|
const row = this.db.prepare("SELECT * FROM ai_team_runs WHERE id = ?").get(id);
|
|
2035
2370
|
return row ? mapAiTeamRunRow(row) : null;
|
|
@@ -2178,6 +2513,7 @@ export class WandStorage {
|
|
|
2178
2513
|
this.db.prepare("UPDATE agent_activity SET read_at = ? WHERE read_at IS NULL").run(at);
|
|
2179
2514
|
}
|
|
2180
2515
|
saveSession(snapshot) {
|
|
2516
|
+
const isNew = !this.getSessionWorkspace(snapshot.id);
|
|
2181
2517
|
// A single SQLite statement is already atomic. Avoid BEGIN IMMEDIATE in
|
|
2182
2518
|
// this hot path so streaming checkpoints do not take an unnecessary write
|
|
2183
2519
|
// lock and saveSession can also participate in a caller-owned transaction.
|
|
@@ -2188,6 +2524,14 @@ export class WandStorage {
|
|
|
2188
2524
|
ON CONFLICT(id) DO UPDATE SET
|
|
2189
2525
|
${sessionPersistAssignments()}`)
|
|
2190
2526
|
.run(...sessionPersistValues(snapshot));
|
|
2527
|
+
if (isNew) {
|
|
2528
|
+
const task = snapshot.workspaceTaskId ? this.getWorkspaceTask(snapshot.workspaceTaskId) : null;
|
|
2529
|
+
if (task && task.workspaceId !== snapshot.workspaceId) {
|
|
2530
|
+
this.db.prepare("UPDATE command_sessions SET workspace_id = ? WHERE id = ?")
|
|
2531
|
+
.run(task.workspaceId, snapshot.id);
|
|
2532
|
+
}
|
|
2533
|
+
this.recordTaskSession(snapshot);
|
|
2534
|
+
}
|
|
2191
2535
|
}
|
|
2192
2536
|
/** Update runtime/scalar fields without serializing or rewriting messages/output. */
|
|
2193
2537
|
updateSessionRuntimeMetadata(snapshot) {
|
|
@@ -2336,6 +2680,25 @@ function normalizeAiTeamMembers(raw) {
|
|
|
2336
2680
|
}
|
|
2337
2681
|
return members;
|
|
2338
2682
|
}
|
|
2683
|
+
/**
|
|
2684
|
+
* 从裸成员 JSON(`ai_teams.members_json` 是数组、`team_json.members` 是数组)里按 id 取显示名。
|
|
2685
|
+
* 列表只要名字投影,不做读端归一,损坏行也不该把名字整个丢掉。
|
|
2686
|
+
*/
|
|
2687
|
+
function teamMemberName(members, memberId) {
|
|
2688
|
+
if (!Array.isArray(members))
|
|
2689
|
+
return undefined;
|
|
2690
|
+
for (const member of members) {
|
|
2691
|
+
if (!member || typeof member !== "object")
|
|
2692
|
+
continue;
|
|
2693
|
+
const record = member;
|
|
2694
|
+
if (String(record.id ?? "") !== memberId)
|
|
2695
|
+
continue;
|
|
2696
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
2697
|
+
if (name)
|
|
2698
|
+
return name;
|
|
2699
|
+
}
|
|
2700
|
+
return undefined;
|
|
2701
|
+
}
|
|
2339
2702
|
function mapAiTeamRunRow(row) {
|
|
2340
2703
|
const rawTeam = safeJsonParse(typeof row.team_json === "string" ? row.team_json : null);
|
|
2341
2704
|
const team = rawTeam ? {
|