@co0ontty/wand 4.81.0 → 4.82.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 (64) hide show
  1. package/dist/agent-dispatch.js +7 -4
  2. package/dist/ai-team-prompts.d.ts +1 -1
  3. package/dist/ai-team-prompts.js +2 -2
  4. package/dist/ai-team-runner.d.ts +2 -0
  5. package/dist/ai-team-runner.js +60 -7
  6. package/dist/ai-team-types.d.ts +6 -0
  7. package/dist/ai-team-types.js +11 -0
  8. package/dist/build-info.json +3 -3
  9. package/dist/cli.js +11 -0
  10. package/dist/config.js +2 -0
  11. package/dist/default-employee.d.ts +11 -0
  12. package/dist/default-employee.js +33 -0
  13. package/dist/employee-knowledge-cli.d.ts +2 -0
  14. package/dist/employee-knowledge-cli.js +52 -0
  15. package/dist/employee-knowledge-content.d.ts +4 -0
  16. package/dist/employee-knowledge-content.js +23 -0
  17. package/dist/employee-knowledge-types.d.ts +15 -0
  18. package/dist/employee-knowledge-types.js +3 -0
  19. package/dist/employee-knowledge.d.ts +6 -0
  20. package/dist/employee-knowledge.js +84 -0
  21. package/dist/git-quick-commit.d.ts +2 -0
  22. package/dist/git-quick-commit.js +121 -58
  23. package/dist/iteration-log.d.ts +5 -0
  24. package/dist/iteration-log.js +12 -1
  25. package/dist/message-truncator.js +13 -9
  26. package/dist/models.d.ts +4 -0
  27. package/dist/models.js +38 -10
  28. package/dist/pi-model-discovery.d.ts +12 -0
  29. package/dist/pi-model-discovery.js +281 -0
  30. package/dist/process-manager.d.ts +23 -1
  31. package/dist/process-manager.js +68 -1
  32. package/dist/prompt-optimizer.js +10 -11
  33. package/dist/pty-turn-activity.d.ts +27 -0
  34. package/dist/pty-turn-activity.js +31 -0
  35. package/dist/server-employee-routes.js +52 -6
  36. package/dist/server-session-routes.js +11 -5
  37. package/dist/server-task-routes.js +6 -1
  38. package/dist/server-user-memory.d.ts +9 -0
  39. package/dist/server-user-memory.js +96 -0
  40. package/dist/server.js +22 -2
  41. package/dist/session-topic.js +6 -5
  42. package/dist/silicon-employee-draft.js +1 -3
  43. package/dist/storage.d.ts +38 -0
  44. package/dist/storage.js +276 -3
  45. package/dist/structured-claude-adapter.d.ts +1 -1
  46. package/dist/structured-claude-adapter.js +2 -0
  47. package/dist/structured-provider-common.d.ts +2 -2
  48. package/dist/structured-provider-common.js +6 -6
  49. package/dist/structured-session-manager.js +5 -4
  50. package/dist/task-title.js +8 -7
  51. package/dist/team-report-file-link.d.ts +6 -0
  52. package/dist/team-report-file-link.js +54 -0
  53. package/dist/team-report-preview.d.ts +3 -0
  54. package/dist/team-report-preview.js +70 -0
  55. package/dist/types.d.ts +15 -0
  56. package/dist/user-memory-types.d.ts +42 -0
  57. package/dist/user-memory-types.js +6 -0
  58. package/dist/user-memory.d.ts +30 -0
  59. package/dist/user-memory.js +190 -0
  60. package/dist/web-ui/content/ai-teams.js +13 -13
  61. package/dist/web-ui/content/scripts.js +59 -61
  62. package/dist/web-ui/content/styles.css +1 -1
  63. package/dist/web-ui/embedded-assets.js +3 -3
  64. package/package.json +1 -1
package/dist/storage.js CHANGED
@@ -12,6 +12,11 @@ import { AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_TERMINAL_RUN_STATUSES, aiTeamChatTit
12
12
  import { isTaskExecutionSubject } from "./task-types.js";
13
13
  import { SYSTEM_EMPLOYEE_KEY, systemEmployeeDefinition, systemEmployeeSeedAgents, } from "./system-employee.js";
14
14
  import { isThinkingEffort } from "./structured-provider-common.js";
15
+ import { DEFAULT_EMPLOYEE_KEY } from "./ai-team-types.js";
16
+ import { defaultEmployeeDefinition } from "./default-employee.js";
17
+ import { normalizeEmployeeKnowledge } from "./employee-knowledge-content.js";
18
+ import { EMPLOYEE_KNOWLEDGE_MAX_ENTRIES } from "./employee-knowledge-types.js";
19
+ import { USER_MEMORY_MAX_EVENTS, USER_MEMORY_RETENTION_MS, } from "./user-memory-types.js";
15
20
  import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
16
21
  function safeJsonParse(raw) {
17
22
  if (!raw)
@@ -817,6 +822,41 @@ const INIT_SQL = `
817
822
  );
818
823
  CREATE INDEX IF NOT EXISTS idx_silicon_employees_archived ON silicon_employees(archived_at);
819
824
 
825
+ CREATE TABLE IF NOT EXISTS employee_knowledge (
826
+ id TEXT PRIMARY KEY,
827
+ employee_id TEXT NOT NULL REFERENCES silicon_employees(id) ON DELETE CASCADE,
828
+ content TEXT NOT NULL,
829
+ content_hash TEXT NOT NULL,
830
+ created_at TEXT NOT NULL,
831
+ UNIQUE (employee_id, content_hash)
832
+ );
833
+ CREATE INDEX IF NOT EXISTS idx_employee_knowledge_owner ON employee_knowledge(employee_id, created_at);
834
+ CREATE TABLE IF NOT EXISTS employee_knowledge_access (
835
+ token_hash TEXT PRIMARY KEY,
836
+ employee_id TEXT NOT NULL REFERENCES silicon_employees(id) ON DELETE CASCADE,
837
+ session_id TEXT NOT NULL REFERENCES command_sessions(id) ON DELETE CASCADE,
838
+ expires_at INTEGER NOT NULL
839
+ );
840
+ CREATE INDEX IF NOT EXISTS idx_employee_knowledge_access_expiry ON employee_knowledge_access(expires_at);
841
+
842
+ CREATE TABLE IF NOT EXISTS user_memory_events (
843
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
844
+ feature TEXT NOT NULL,
845
+ text TEXT NOT NULL DEFAULT '',
846
+ dedup_key TEXT NOT NULL,
847
+ created_at INTEGER NOT NULL
848
+ );
849
+ CREATE INDEX IF NOT EXISTS idx_user_memory_events_created ON user_memory_events(created_at);
850
+ CREATE INDEX IF NOT EXISTS idx_user_memory_events_dedup ON user_memory_events(dedup_key, created_at);
851
+ CREATE TABLE IF NOT EXISTS user_memory_state (
852
+ id INTEGER PRIMARY KEY CHECK (id = 1),
853
+ enabled INTEGER NOT NULL DEFAULT 1,
854
+ revision INTEGER NOT NULL DEFAULT 0,
855
+ last_attempt_at INTEGER NOT NULL DEFAULT 0,
856
+ source_id INTEGER NOT NULL DEFAULT 0,
857
+ profile_json TEXT
858
+ );
859
+ INSERT OR IGNORE INTO user_memory_state (id) VALUES (1);
820
860
 
821
861
  CREATE TABLE IF NOT EXISTS ai_team_runs (
822
862
  id TEXT PRIMARY KEY,
@@ -1076,12 +1116,14 @@ function withoutSessionTab(node, sessionId) {
1076
1116
  export class WandStorage {
1077
1117
  db;
1078
1118
  dbPath;
1119
+ memoryCapture = { enabled: true, revision: 0 };
1079
1120
  constructor(dbPath) {
1080
1121
  const dir = path.dirname(dbPath);
1081
1122
  mkdirSync(dir, { recursive: true, mode: 0o700 });
1082
1123
  chmodSync(dir, 0o700);
1083
1124
  this.dbPath = dbPath;
1084
1125
  this.db = new DatabaseSync(dbPath);
1126
+ this.db.exec("PRAGMA busy_timeout = 5000");
1085
1127
  chmodSync(dbPath, 0o600);
1086
1128
  this.db.exec(INIT_SQL);
1087
1129
  ensureAuthSessionSchema(this.db);
@@ -1094,10 +1136,15 @@ export class WandStorage {
1094
1136
  ensureConnectorSchema(this.db);
1095
1137
  this.ensureDefaultPasswordVault();
1096
1138
  this.migrateTaskRecords();
1139
+ const memory = this.getUserMemoryState();
1140
+ this.memoryCapture = { enabled: memory.enabled, revision: memory.revision };
1097
1141
  }
1098
1142
  directory() {
1099
1143
  return path.dirname(this.dbPath);
1100
1144
  }
1145
+ databasePath() {
1146
+ return this.dbPath;
1147
+ }
1101
1148
  close() {
1102
1149
  this.db.close();
1103
1150
  }
@@ -2311,11 +2358,34 @@ export class WandStorage {
2311
2358
  ? "SELECT * FROM silicon_employees ORDER BY (system_key IS NULL), created_at ASC"
2312
2359
  : "SELECT * FROM silicon_employees WHERE archived_at IS NULL ORDER BY (system_key IS NULL), created_at ASC";
2313
2360
  const rows = this.db.prepare(sql).all();
2314
- return rows.map(mapSiliconEmployeeRow);
2361
+ return rows.map((row) => this.projectDefaultEmployee(mapSiliconEmployeeRow(row)));
2362
+ }
2363
+ projectDefaultEmployee(employee) {
2364
+ if (employee.systemKey !== DEFAULT_EMPLOYEE_KEY)
2365
+ return employee;
2366
+ const state = this.getUserMemoryState();
2367
+ return defaultEmployeeDefinition(employee.agents, employee.updatedAt, employee, state.enabled ? state.profile : null);
2315
2368
  }
2316
2369
  getSiliconEmployee(id) {
2317
2370
  const row = this.db.prepare("SELECT * FROM silicon_employees WHERE id = ?").get(id);
2318
- return row ? mapSiliconEmployeeRow(row) : null;
2371
+ return row ? this.projectDefaultEmployee(mapSiliconEmployeeRow(row)) : null;
2372
+ }
2373
+ getDefaultSiliconEmployee() {
2374
+ const employee = this.getSystemSiliconEmployee(DEFAULT_EMPLOYEE_KEY);
2375
+ return employee ? this.projectDefaultEmployee(employee) : null;
2376
+ }
2377
+ /** Seed once, preserve execution choices, and never reset learned preferences on restart. */
2378
+ ensureDefaultSiliconEmployee(provider) {
2379
+ const existing = this.getSystemSiliconEmployee(DEFAULT_EMPLOYEE_KEY);
2380
+ const state = this.getUserMemoryState();
2381
+ const definition = defaultEmployeeDefinition(existing?.agents ?? systemEmployeeSeedAgents({ provider }), new Date().toISOString(), existing, state.enabled ? state.profile : null);
2382
+ if (!existing || existing.name !== definition.name || existing.duty !== definition.duty
2383
+ || existing.prompt !== definition.prompt || existing.avatar !== definition.avatar
2384
+ || existing.archivedAt || !existing.agents.length) {
2385
+ this.saveSiliconEmployee(definition);
2386
+ return definition;
2387
+ }
2388
+ return existing;
2319
2389
  }
2320
2390
  /** 内置「系统运维」员工;未创建(或被删除过)时返回 null。 */
2321
2391
  getSystemSiliconEmployee(key = SYSTEM_EMPLOYEE_KEY) {
@@ -2368,6 +2438,209 @@ export class WandStorage {
2368
2438
  deleteSiliconEmployee(id) {
2369
2439
  this.db.prepare("DELETE FROM silicon_employees WHERE id = ?").run(id);
2370
2440
  }
2441
+ // ============ Employee-owned explicit knowledge ============
2442
+ requireKnowledgeEmployee(employeeId) {
2443
+ if (!this.db.prepare("SELECT id FROM silicon_employees WHERE id = ?").get(employeeId)) {
2444
+ throw new Error("员工已删除或不存在,不能改存到其他员工的知识库。");
2445
+ }
2446
+ }
2447
+ listEmployeeKnowledge(employeeId, query = "", limit = 20) {
2448
+ this.requireKnowledgeEmployee(employeeId);
2449
+ const normalizedQuery = query.trim().slice(0, 200).toLowerCase();
2450
+ const boundedLimit = Math.max(1, Math.min(Math.floor(limit), EMPLOYEE_KNOWLEDGE_MAX_ENTRIES));
2451
+ // SQLite lower() is ASCII-only. Load at most this employee's fixed quota,
2452
+ // perform Unicode case folding, and apply the result limit after filtering.
2453
+ const rows = this.db.prepare(`SELECT id, employee_id, content, created_at FROM employee_knowledge
2454
+ WHERE employee_id = ? ORDER BY created_at DESC, rowid DESC LIMIT ?`)
2455
+ .all(employeeId, EMPLOYEE_KNOWLEDGE_MAX_ENTRIES);
2456
+ return rows.filter((row) => row.content.toLowerCase().includes(normalizedQuery))
2457
+ .slice(0, boundedLimit).map((row) => ({
2458
+ id: row.id, employeeId: row.employee_id, content: row.content, createdAt: row.created_at,
2459
+ }));
2460
+ }
2461
+ countEmployeeKnowledge(employeeId) {
2462
+ this.requireKnowledgeEmployee(employeeId);
2463
+ return Number(this.db.prepare("SELECT COUNT(*) AS count FROM employee_knowledge WHERE employee_id = ?")
2464
+ .get(employeeId)?.count ?? 0);
2465
+ }
2466
+ rememberEmployeeKnowledge(employeeId, value, accessToken) {
2467
+ const content = normalizeEmployeeKnowledge(value);
2468
+ const hash = crypto.createHash("sha256").update(content).digest("hex");
2469
+ return this.transaction(() => {
2470
+ this.requireKnowledgeEmployee(employeeId);
2471
+ if (accessToken && this.resolveEmployeeKnowledgeAccess(accessToken) !== employeeId) {
2472
+ throw new Error("知识库访问已失效,请等待新的会话执行。");
2473
+ }
2474
+ const existing = this.db.prepare("SELECT id, content, created_at FROM employee_knowledge WHERE employee_id = ? AND content_hash = ?")
2475
+ .get(employeeId, hash);
2476
+ if (existing)
2477
+ return { id: String(existing.id), employeeId,
2478
+ content: String(existing.content), createdAt: String(existing.created_at) };
2479
+ if (this.countEmployeeKnowledge(employeeId) >= EMPLOYEE_KNOWLEDGE_MAX_ENTRIES) {
2480
+ throw new Error("该员工知识库已满,请先删除不需要的内容,不会自动丢弃旧知识。");
2481
+ }
2482
+ const entry = { id: `k_${crypto.randomUUID().replace(/-/g, "")}`, employeeId,
2483
+ content, createdAt: new Date().toISOString() };
2484
+ this.db.prepare(`INSERT INTO employee_knowledge (id, employee_id, content, content_hash, created_at)
2485
+ VALUES (?, ?, ?, ?, ?)`).run(entry.id, employeeId, content, hash, entry.createdAt);
2486
+ return entry;
2487
+ });
2488
+ }
2489
+ forgetEmployeeKnowledge(employeeId, entryId, accessToken) {
2490
+ return this.transaction(() => {
2491
+ this.requireKnowledgeEmployee(employeeId);
2492
+ if (accessToken && this.resolveEmployeeKnowledgeAccess(accessToken) !== employeeId) {
2493
+ throw new Error("知识库访问已失效,请等待新的会话执行。");
2494
+ }
2495
+ return this.db.prepare("DELETE FROM employee_knowledge WHERE employee_id = ? AND id = ?")
2496
+ .run(employeeId, entryId).changes === 1;
2497
+ });
2498
+ }
2499
+ clearEmployeeKnowledge(employeeId) {
2500
+ this.transaction(() => {
2501
+ this.requireKnowledgeEmployee(employeeId);
2502
+ this.db.prepare("DELETE FROM employee_knowledge WHERE employee_id = ?").run(employeeId);
2503
+ this.db.prepare("DELETE FROM employee_knowledge_access WHERE employee_id = ?").run(employeeId);
2504
+ });
2505
+ }
2506
+ /** Opaque per-execution capability: no employee selector, app token or DB secret in model prompts. */
2507
+ issueEmployeeKnowledgeAccess(sessionId, employeeId, now = Date.now()) {
2508
+ this.requireKnowledgeEmployee(employeeId);
2509
+ const row = this.getSessionSlim(sessionId);
2510
+ if (!row || row.employeeId !== employeeId)
2511
+ throw new Error("知识库归属与会话员工不一致。");
2512
+ this.db.prepare("DELETE FROM employee_knowledge_access WHERE expires_at <= ?").run(now);
2513
+ const token = crypto.randomBytes(32).toString("base64url");
2514
+ const hash = crypto.createHash("sha256").update(token).digest("hex");
2515
+ this.db.prepare(`INSERT INTO employee_knowledge_access (token_hash, employee_id, session_id, expires_at)
2516
+ VALUES (?, ?, ?, ?)`).run(hash, employeeId, sessionId, now + 6 * 60 * 60 * 1000);
2517
+ return token;
2518
+ }
2519
+ resolveEmployeeKnowledgeAccess(token, now = Date.now()) {
2520
+ if (!/^[A-Za-z0-9_-]{43}$/.test(token))
2521
+ return null;
2522
+ const hash = crypto.createHash("sha256").update(token).digest("hex");
2523
+ const row = this.db.prepare(`SELECT a.employee_id FROM employee_knowledge_access a
2524
+ JOIN command_sessions s ON s.id = a.session_id JOIN silicon_employees e ON e.id = a.employee_id
2525
+ WHERE a.token_hash = ? AND a.expires_at > ? AND json_extract(s.session_options, '$.employeeId') = a.employee_id`)
2526
+ .get(hash, now);
2527
+ return typeof row?.employee_id === "string" ? row.employee_id : null;
2528
+ }
2529
+ revokeEmployeeKnowledgeAccess(token) {
2530
+ this.db.prepare("DELETE FROM employee_knowledge_access WHERE token_hash = ?")
2531
+ .run(crypto.createHash("sha256").update(token).digest("hex"));
2532
+ }
2533
+ // ============ User short-term memory ============
2534
+ /** No IO on input hot paths; queued observations still CAS against the database before writing. */
2535
+ userMemoryCaptureState() {
2536
+ return this.memoryCapture;
2537
+ }
2538
+ stopUserMemoryCapture() {
2539
+ this.memoryCapture = { ...this.memoryCapture, enabled: false };
2540
+ }
2541
+ getUserMemoryState(now = Date.now()) {
2542
+ const row = this.db.prepare("SELECT * FROM user_memory_state WHERE id = 1").get();
2543
+ const candidate = safeJsonParse(row.profile_json);
2544
+ let profile = candidate && candidate.expiresAt > now && Array.isArray(candidate.preferences)
2545
+ && candidate.preferences.length <= 10 && candidate.preferences.every((entry) => entry && typeof entry.text === "string" && entry.text.length <= 160
2546
+ && Array.isArray(entry.evidenceIds) && entry.evidenceIds.length <= 5
2547
+ && entry.evidenceIds.every((id) => Number.isSafeInteger(id))) ? candidate : null;
2548
+ const ids = profile ? [...new Set(profile.preferences.flatMap((entry) => entry.evidenceIds))] : [];
2549
+ if (ids.length) {
2550
+ const count = this.db.prepare(`SELECT COUNT(*) AS total FROM user_memory_events
2551
+ WHERE id IN (${ids.map(() => "?").join(",")}) AND created_at > ? AND created_at <= ?`)
2552
+ .get(...ids, now - USER_MEMORY_RETENTION_MS, now)?.total;
2553
+ if (count !== ids.length)
2554
+ profile = null;
2555
+ }
2556
+ return {
2557
+ enabled: row.enabled === 1,
2558
+ revision: Number(row.revision),
2559
+ lastAttemptAt: Number(row.last_attempt_at),
2560
+ sourceId: Number(row.source_id),
2561
+ profile,
2562
+ };
2563
+ }
2564
+ listUserMemoryEvents(now = Date.now(), limit = 80) {
2565
+ const rows = this.db.prepare(`SELECT id, feature, text, created_at FROM user_memory_events
2566
+ WHERE created_at > ? AND created_at <= ? ORDER BY id DESC LIMIT ?`)
2567
+ .all(now - USER_MEMORY_RETENTION_MS, now, Math.max(1, Math.min(limit, USER_MEMORY_MAX_EVENTS)));
2568
+ return rows.reverse().map((row) => ({
2569
+ id: row.id, feature: row.feature, text: row.text, createdAt: row.created_at,
2570
+ }));
2571
+ }
2572
+ userMemoryFeatureCounts(now = Date.now()) {
2573
+ return this.db.prepare(`SELECT feature, COUNT(*) AS count FROM user_memory_events
2574
+ WHERE created_at > ? AND created_at <= ? GROUP BY feature ORDER BY count DESC, feature`)
2575
+ .all(now - USER_MEMORY_RETENTION_MS, now);
2576
+ }
2577
+ /** Only already-redacted, bounded observations reach this table. Queued writes use revision CAS. */
2578
+ appendUserMemoryEvent(feature, text, dedupKey, revision, now = Date.now()) {
2579
+ this.transaction(() => {
2580
+ const state = this.getUserMemoryState(now);
2581
+ if (!state.enabled || state.revision !== revision)
2582
+ return;
2583
+ this.pruneUserMemory(now);
2584
+ const duplicate = this.db.prepare(`SELECT id FROM user_memory_events
2585
+ WHERE dedup_key = ? AND created_at > ? LIMIT 1`).get(dedupKey, now - 120_000);
2586
+ if (duplicate)
2587
+ return;
2588
+ this.db.prepare(`INSERT INTO user_memory_events (feature, text, dedup_key, created_at)
2589
+ VALUES (?, ?, ?, ?)`).run(feature, text, dedupKey, now);
2590
+ this.db.prepare(`DELETE FROM user_memory_events WHERE id NOT IN
2591
+ (SELECT id FROM user_memory_events ORDER BY id DESC LIMIT ?)`)
2592
+ .run(USER_MEMORY_MAX_EVENTS);
2593
+ this.pruneUserMemory(now);
2594
+ });
2595
+ }
2596
+ pruneUserMemory(now = Date.now()) {
2597
+ this.db.prepare("DELETE FROM user_memory_events WHERE created_at <= ?")
2598
+ .run(now - USER_MEMORY_RETENTION_MS);
2599
+ const saved = this.db.prepare("SELECT profile_json FROM user_memory_state WHERE id = 1").get();
2600
+ if (saved?.profile_json && !this.getUserMemoryState(now).profile) {
2601
+ this.db.exec("UPDATE user_memory_state SET profile_json = NULL, source_id = 0 WHERE id = 1");
2602
+ this.ensureDefaultSiliconEmployee();
2603
+ }
2604
+ }
2605
+ setUserMemoryEnabled(enabled) {
2606
+ this.transaction(() => {
2607
+ this.db.prepare(`UPDATE user_memory_state SET enabled = ?, revision = revision + 1,
2608
+ last_attempt_at = 0, source_id = 0, profile_json = NULL WHERE id = 1`).run(enabled ? 1 : 0);
2609
+ this.ensureDefaultSiliconEmployee();
2610
+ });
2611
+ this.memoryCapture = { enabled, revision: this.memoryCapture.revision + 1 };
2612
+ }
2613
+ clearUserMemory() {
2614
+ this.transaction(() => {
2615
+ this.db.exec(`DELETE FROM user_memory_events;
2616
+ UPDATE user_memory_state SET revision = revision + 1, last_attempt_at = 0,
2617
+ source_id = 0, profile_json = NULL WHERE id = 1;`);
2618
+ this.ensureDefaultSiliconEmployee();
2619
+ });
2620
+ this.memoryCapture = { ...this.memoryCapture, revision: this.memoryCapture.revision + 1 };
2621
+ }
2622
+ markUserMemoryAttempt(revision, now) {
2623
+ return this.db.prepare(`UPDATE user_memory_state SET last_attempt_at = ?
2624
+ WHERE id = 1 AND enabled = 1 AND revision = ?`).run(now, revision).changes === 1;
2625
+ }
2626
+ /** Read fresh candidates inside the transaction; a late model result cannot undo user edits/clear. */
2627
+ applyUserMemoryProfile(profile, revision, sourceId, now = Date.now()) {
2628
+ return this.transaction(() => {
2629
+ const state = this.getUserMemoryState(now);
2630
+ if (!state.enabled || state.revision !== revision || profile.expiresAt <= now)
2631
+ return false;
2632
+ const evidence = this.db.prepare("SELECT created_at FROM user_memory_events WHERE id = ?");
2633
+ for (const id of new Set(profile.preferences.flatMap((entry) => entry.evidenceIds))) {
2634
+ const row = evidence.get(id);
2635
+ if (!row || Number(row.created_at) <= now - USER_MEMORY_RETENTION_MS)
2636
+ return false;
2637
+ }
2638
+ this.db.prepare(`UPDATE user_memory_state SET profile_json = ?, source_id = ? WHERE id = 1`)
2639
+ .run(JSON.stringify(profile), sourceId);
2640
+ this.ensureDefaultSiliconEmployee();
2641
+ return true;
2642
+ });
2643
+ }
2371
2644
  // ============ AI Teams ============
2372
2645
  listAiTeams() {
2373
2646
  const rows = this.db.prepare("SELECT * FROM ai_teams ORDER BY created_at ASC").all();
@@ -2521,7 +2794,7 @@ export class WandStorage {
2521
2794
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2522
2795
  ON CONFLICT(id) DO UPDATE SET
2523
2796
  session_id = excluded.session_id, status = excluded.status, report = excluded.report,
2524
- instructions = excluded.instructions,
2797
+ instructions = excluded.instructions, report_path = excluded.report_path,
2525
2798
  depends_on_json = excluded.depends_on_json,
2526
2799
  dispatch_info_json = excluded.dispatch_info_json,
2527
2800
  started_at = excluded.started_at, ended_at = excluded.ended_at`).run(step.id, step.runId, step.seq, step.kind, step.memberId, step.title, step.instructions, step.sessionId, step.status, step.report, step.reportPath, JSON.stringify(step.dependsOn), step.startedAt, step.endedAt, JSON.stringify(step.dispatchInfo ?? { usedCandidate: 0, skipped: [] }));
@@ -12,7 +12,7 @@ export declare function buildAppendSystemPromptParts(language: string | undefine
12
12
  * 完整 append 列表:模式 / 语言指令 + 会话级系统提示(团队、自动化会话的角色与规则)。
13
13
  * CLI 与 SDK 两条路径共用,避免只改一边。
14
14
  */
15
- export declare function buildSessionSystemPromptParts(session: Pick<SessionSnapshot, "mode" | "systemPrompt">, language: string | undefined): string[];
15
+ export declare function buildSessionSystemPromptParts(session: Pick<SessionSnapshot, "mode" | "systemPrompt" | "runtimeSystemPrompt">, language: string | undefined): string[];
16
16
  export interface ClaudeCliArgsOptions {
17
17
  permissionPolicy: PermissionPolicy;
18
18
  systemPromptParts?: string[];
@@ -101,6 +101,8 @@ export function buildSessionSystemPromptParts(session, language) {
101
101
  const sessionPrompt = session.systemPrompt?.trim();
102
102
  if (sessionPrompt)
103
103
  parts.push(sessionPrompt);
104
+ if (session.runtimeSystemPrompt?.trim())
105
+ parts.push(session.runtimeSystemPrompt.trim());
104
106
  return parts;
105
107
  }
106
108
  export function buildClaudeCliArgs(session, options) {
@@ -7,12 +7,12 @@ import type { SessionProvider, SessionRunner, SessionSnapshot, StructuredSession
7
7
  */
8
8
  export declare function systemPromptFlag(provider: SessionProvider | null | undefined): string | null;
9
9
  /** 结构化 runner 的 args:provider 支持时把系统提示当成独立参数传。 */
10
- export declare function systemPromptArgs(session: Pick<SessionSnapshot, "provider" | "systemPrompt">): string[];
10
+ export declare function systemPromptArgs(session: Pick<SessionSnapshot, "provider" | "systemPrompt" | "runtimeSystemPrompt">): string[];
11
11
  /**
12
12
  * provider 没有系统提示通道时的兜底:把它放在本会话第一条消息最前面,并写明它不是用户输入。
13
13
  * 只在首轮拼一次:后续轮次的消息已经接在有它的对话历史后面。有通道的 provider 不会走这里。
14
14
  */
15
- export declare function promptWithSystemFallback(session: Pick<SessionSnapshot, "provider" | "systemPrompt" | "messages">, prompt: string): string;
15
+ export declare function promptWithSystemFallback(session: Pick<SessionSnapshot, "provider" | "systemPrompt" | "runtimeSystemPrompt" | "messages">, prompt: string): string;
16
16
  /** provider 没有系统提示通道时的并接格式:明确标出这不是用户输入。 */
17
17
  export declare function composeSystemFallback(systemPrompt: string, prompt: string): string;
18
18
  /** 把系统提示接到 CLI 命令上(provider 支持时);不支持时原样返回,由调用方并进首条输入。 */
@@ -20,7 +20,7 @@ export function systemPromptFlag(provider) {
20
20
  }
21
21
  /** 结构化 runner 的 args:provider 支持时把系统提示当成独立参数传。 */
22
22
  export function systemPromptArgs(session) {
23
- const text = session.systemPrompt?.trim();
23
+ const text = [session.systemPrompt?.trim(), session.runtimeSystemPrompt?.trim()].filter(Boolean).join("\n\n");
24
24
  if (!text)
25
25
  return [];
26
26
  const flag = systemPromptFlag(session.provider);
@@ -31,12 +31,12 @@ export function systemPromptArgs(session) {
31
31
  * 只在首轮拼一次:后续轮次的消息已经接在有它的对话历史后面。有通道的 provider 不会走这里。
32
32
  */
33
33
  export function promptWithSystemFallback(session, prompt) {
34
- const text = session.systemPrompt?.trim();
35
- if (!text || systemPromptFlag(session.provider))
34
+ if (systemPromptFlag(session.provider))
36
35
  return prompt;
37
- if ((session.messages?.length ?? 0) > 1)
38
- return prompt;
39
- return composeSystemFallback(text, prompt);
36
+ const base = (session.messages?.length ?? 0) <= 1 ? session.systemPrompt?.trim() : "";
37
+ // Knowledge is live per turn, including resumed Codex/OpenCode/Gemini conversations.
38
+ const text = [base, session.runtimeSystemPrompt?.trim()].filter(Boolean).join("\n\n");
39
+ return text ? composeSystemFallback(text, prompt) : prompt;
40
40
  }
41
41
  /** provider 没有系统提示通道时的并接格式:明确标出这不是用户输入。 */
42
42
  export function composeSystemFallback(systemPrompt, prompt) {
@@ -6,6 +6,7 @@ import { buildChildEnv } from "./env-utils.js";
6
6
  import { getErrorMessage } from "./error-utils.js";
7
7
  import { getDefaultModelForProvider } from "./config.js";
8
8
  import { recordIterationPrompt } from "./iteration-log.js";
9
+ import { startEmployeeKnowledgeRunner } from "./employee-knowledge.js";
9
10
  import { signalNameFromNumber } from "./signal-utils.js";
10
11
  import { provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
11
12
  import { resolveSessionCwd } from "./session-cwd.js";
@@ -2037,7 +2038,7 @@ export class StructuredSessionManager {
2037
2038
  emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2038
2039
  }
2039
2040
  };
2040
- const execution = this.codexRunner.start({
2041
+ const execution = startEmployeeKnowledgeRunner(this.storage, this.codexRunner, {
2041
2042
  session,
2042
2043
  prompt,
2043
2044
  env: buildChildEnv(this.config.inheritEnv !== false),
@@ -2183,7 +2184,7 @@ export class StructuredSessionManager {
2183
2184
  if (!emitTimer)
2184
2185
  emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2185
2186
  };
2186
- const execution = this.grokRunner.start({
2187
+ const execution = startEmployeeKnowledgeRunner(this.storage, this.grokRunner, {
2187
2188
  session,
2188
2189
  prompt,
2189
2190
  env: buildChildEnv(this.config.inheritEnv !== false),
@@ -2325,7 +2326,7 @@ export class StructuredSessionManager {
2325
2326
  emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
2326
2327
  }
2327
2328
  };
2328
- const execution = this.openCodeRunner.start({
2329
+ const execution = startEmployeeKnowledgeRunner(this.storage, this.openCodeRunner, {
2329
2330
  session,
2330
2331
  prompt,
2331
2332
  env: buildChildEnv(this.config.inheritEnv !== false),
@@ -2501,7 +2502,7 @@ export class StructuredSessionManager {
2501
2502
  const provider = options.provider ?? "claude";
2502
2503
  const commandLabel = options.commandLabel ?? "claude -p";
2503
2504
  const logKind = options.logKind ?? "claude-print";
2504
- const execution = runner.start({
2505
+ const execution = startEmployeeKnowledgeRunner(this.storage, runner, {
2505
2506
  session,
2506
2507
  prompt,
2507
2508
  env: buildChildEnv(this.config.inheritEnv !== false),
@@ -78,11 +78,12 @@ export async function generateWandTaskTitle(description, cwd, language = "", ai
78
78
  ].join("\n"),
79
79
  prompt: `任务描述:\n${input}`,
80
80
  };
81
- const raw = await callConfiguredAiText(request, cwd || process.cwd(), language, ai);
82
- const stripped = stripGeneratedTitle(raw);
83
- // 拿到报错文案 / 整段解释时宁可保留占位标题,也不能把垃圾写进看板。
84
- // 先判可信度再裁剪:截断过的长句看起来就像标题,会绕过长度检查。
85
- if (!isPlausibleTaskTitle(stripped))
86
- throw new Error("模型返回的任务标题不可用。");
87
- return clipAtWordBoundary(stripped, TASK_TITLE_MAX_LENGTH);
81
+ return callConfiguredAiText(request, cwd || process.cwd(), language, ai, (raw) => {
82
+ const stripped = stripGeneratedTitle(raw);
83
+ // 校验在候选链内:格式不可用也要继续备用候选,不能先把非空文本当成功。
84
+ // 先判可信度再裁剪:截断过的长句看起来就像标题,会绕过长度检查。
85
+ if (!isPlausibleTaskTitle(stripped))
86
+ throw new Error("模型返回的任务标题不可用。");
87
+ return clipAtWordBoundary(stripped, TASK_TITLE_MAX_LENGTH);
88
+ });
88
89
  }
@@ -0,0 +1,6 @@
1
+ /** 只识别明确交付的报告链接,不能把普通代码/历史文档或外部 URL 当成新报告。 */
2
+ export declare function linkedTeamReportFile(cwd: string, reply: string, startedAt: string | null): {
3
+ path: string;
4
+ relativePath: string;
5
+ mtimeMs: number;
6
+ } | null;
@@ -0,0 +1,54 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** 只识别明确交付的报告链接,不能把普通代码/历史文档或外部 URL 当成新报告。 */
5
+ export function linkedTeamReportFile(cwd, reply, startedAt) {
6
+ const started = Date.parse(startedAt ?? "");
7
+ if (!Number.isFinite(started))
8
+ return null;
9
+ const candidates = new Map();
10
+ try {
11
+ const root = realpathSync(cwd);
12
+ const links = /\[([^\]\r\n]+)\]\(\s*(?:<([^>\r\n]+)>|([^\s)]+))\s*\)/g;
13
+ const references = [
14
+ ...[...reply.matchAll(links)].map((m) => ({ raw: m[2] ?? m[3], label: m[1], index: m.index, text: m[0] })),
15
+ ...[...reply.matchAll(/`([^`\r\n]+\.(?:md|markdown))`/gi)]
16
+ .map((m) => ({ raw: m[1], label: "", index: m.index, text: m[0] })),
17
+ ];
18
+ for (const reference of references) {
19
+ const { raw } = reference;
20
+ const before = reply.slice(reply.lastIndexOf("\n", reference.index) + 1, reference.index);
21
+ const delivered = /(?:已(?:写入|保存|生成|完成)|交付|报告(?:文件)?[::]|report\s*[::]|\b(?:written|saved|created|delivered)\b)/i.test(before)
22
+ || reply.trim() === reference.text;
23
+ if (!delivered || !/(?:报告|report)/i.test(`${reference.label} ${before} ${path.basename(raw)}`))
24
+ continue;
25
+ if (!/\.(?:md|markdown)$/i.test(raw))
26
+ continue;
27
+ try {
28
+ const local = /^file:/i.test(raw) ? fileURLToPath(raw)
29
+ : /^[a-z][a-z\d+.-]*:/i.test(raw) && !path.isAbsolute(raw) ? null
30
+ : decodeURIComponent(raw);
31
+ if (!local)
32
+ continue;
33
+ const file = realpathSync(path.resolve(root, local));
34
+ const relativePath = path.relative(root, file);
35
+ if (!/\.(?:md|markdown)$/i.test(file))
36
+ continue;
37
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath))
38
+ continue;
39
+ const stat = statSync(file);
40
+ // 文件系统有亚毫秒精度,startedAt 只有整数毫秒;同一毫秒不能误判为旧文件。
41
+ if (!stat.isFile() || Math.ceil(stat.mtimeMs) < started)
42
+ continue;
43
+ candidates.set(file, { path: file, relativePath, mtimeMs: stat.mtimeMs });
44
+ }
45
+ catch {
46
+ // 无法读取/无效路径不构成文件交付,保留原有回复兜底。
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ return candidates.size === 1 ? [...candidates.values()][0] : null;
54
+ }
@@ -0,0 +1,3 @@
1
+ import type { TeamReportFile } from "./types.js";
2
+ /** 完成时冻结有界预览;后续群聊列表不为预览再次读取整份文件。 */
3
+ export declare function teamReportPreview(text: string, fallbackTitle: string): NonNullable<TeamReportFile["preview"]>;
@@ -0,0 +1,70 @@
1
+ const TITLE_LIMIT = 100;
2
+ const EXCERPT_LIMIT = 240;
3
+ const EXCERPT_LINES = 3;
4
+ function bounded(text, limit) {
5
+ const chars = Array.from(text);
6
+ return chars.length > limit ? `${chars.slice(0, limit - 1).join("").trimEnd()}…` : text;
7
+ }
8
+ /** 只取真实文本,不渲染 Markdown/HTML、不读链接、不调用模型。 */
9
+ function plainText(line) {
10
+ return line
11
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
12
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
13
+ .replace(/<[^>]*>/g, "")
14
+ .replace(/(`+)(.*?)\1/g, "$2")
15
+ .replace(/(\*\*|__|~~)(.*?)\1/g, "$2")
16
+ .replace(/^\s*(?:>\s*)+/, "")
17
+ .replace(/^\s*(?:[-+*]|\d+[.)])\s+(?:\[[ xX]\]\s*)?/, "")
18
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
19
+ .replace(/\s+/g, " ")
20
+ .trim();
21
+ }
22
+ const SUMMARY_HEADING = /^(?:\d+[.、\s]*)?(?:结论|摘要|总结|概述|验证结果|summary|conclusions?)(?:[::].*)?$/i;
23
+ /** 完成时冻结有界预览;后续群聊列表不为预览再次读取整份文件。 */
24
+ export function teamReportPreview(text, fallbackTitle) {
25
+ const source = text.slice(0, 64 * 1024)
26
+ .replace(/^\uFEFF/, "")
27
+ .replace(/^---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$)/, "")
28
+ .replace(/<!--[\s\S]*?-->/g, "")
29
+ .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, "");
30
+ const lines = source.split(/\r?\n/);
31
+ const entries = [];
32
+ let fence = null;
33
+ for (let i = 0; i < lines.length; i++) {
34
+ const raw = lines[i].trim();
35
+ const marker = /^(?:`{3,}|~{3,})/.exec(raw)?.[0];
36
+ if (marker) {
37
+ if (!fence)
38
+ fence = marker;
39
+ else if (marker[0] === fence[0] && marker.length >= fence.length && raw === marker)
40
+ fence = null;
41
+ continue;
42
+ }
43
+ if (fence || !raw || /^(?:[-*_]\s*){3,}$/.test(raw))
44
+ continue;
45
+ const heading = /^(#{1,6})\s+(.+?)(?:\s+#+)?$/.exec(raw);
46
+ const setext = /^(=+|-+)\s*$/.exec(lines[i + 1]?.trim() ?? "");
47
+ const value = plainText(heading ? heading[2] : raw);
48
+ if (!value)
49
+ continue;
50
+ entries.push({ text: value, heading: heading ? heading[1].length : setext ? (setext[1][0] === "=" ? 1 : 2) : 0 });
51
+ if (setext)
52
+ i++;
53
+ }
54
+ const titleEntry = entries.find((entry) => entry.heading === 1)
55
+ ?? entries.find((entry) => entry.heading > 0 && !SUMMARY_HEADING.test(entry.text));
56
+ const title = bounded(titleEntry?.text || plainText(fallbackTitle) || "成员报告", TITLE_LIMIT);
57
+ const summaryAt = entries.findIndex((entry) => entry.heading > 0 && SUMMARY_HEADING.test(entry.text));
58
+ let candidates = entries.filter((entry) => entry.heading === 0);
59
+ if (summaryAt >= 0) {
60
+ const section = entries.slice(summaryAt + 1);
61
+ const nextHeading = section.findIndex((entry) => entry.heading > 0);
62
+ const summary = section.slice(0, nextHeading < 0 ? section.length : nextHeading);
63
+ if (summary.length)
64
+ candidates = summary;
65
+ }
66
+ let excerpt = candidates.slice(0, EXCERPT_LINES).map((entry) => entry.text).join("\n");
67
+ if (candidates.length > EXCERPT_LINES)
68
+ excerpt += "…";
69
+ return { title, excerpt: bounded(excerpt, EXCERPT_LIMIT) };
70
+ }
package/dist/types.d.ts CHANGED
@@ -547,10 +547,23 @@ export interface ConversationAuthor {
547
547
  /** 这条发言对应的成员会话,可点开看完整过程。 */
548
548
  sessionId?: string;
549
549
  }
550
+ /** 团队成员完成报告后投递的文件元数据;正文按需预览,不嵌进群聊。 */
551
+ export interface TeamReportFile {
552
+ stepId: string;
553
+ path: string;
554
+ name: string;
555
+ size: number;
556
+ /** 完成时从原文件提取的标题与有限摘录;旧消息可缺省。 */
557
+ preview?: {
558
+ title: string;
559
+ excerpt: string;
560
+ };
561
+ }
550
562
  export interface ConversationTurn {
551
563
  role: "user" | "assistant";
552
564
  content: ContentBlock[];
553
565
  author?: ConversationAuthor;
566
+ reportFile?: TeamReportFile;
554
567
  /** 群聊里的进度提示(派工、开始、出错):渲染成居中的一行,不是气泡。 */
555
568
  notice?: boolean;
556
569
  /** ISO time when this turn was first recorded (user send / assistant start). */
@@ -598,6 +611,8 @@ export interface SessionSnapshot {
598
611
  * 不要拼进首条用户消息。provider 没有该通道时(codex / opencode)由调用方退回并入消息。
599
612
  */
600
613
  systemPrompt?: string | null;
614
+ /** Runner-only live employee knowledge. Never persisted in session_options or sent in DTOs. */
615
+ runtimeSystemPrompt?: string;
601
616
  /** 所属工作空间 ID(多标签 / 分屏项目)。会话在该工作空间窗口内作为一个标签。 */
602
617
  workspaceId?: string;
603
618
  /** 所属工作空间任务 ID;任务独占一个 worktree,其下所有会话共享该 worktree 目录。 */