@steerable/agent-shell 0.6.21 → 0.6.22

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 (48) hide show
  1. package/README.md +34 -0
  2. package/dist/auth/index.d.ts +38 -0
  3. package/dist/auth/index.js +20 -0
  4. package/dist/host/pack-assembly.d.ts +7 -7
  5. package/dist/host/runtime.d.ts +6 -2
  6. package/dist/host/runtime.js +54 -14
  7. package/dist/insights/flush.d.ts +5 -4
  8. package/dist/insights/flush.js +14 -15
  9. package/dist/insights/record.d.ts +6 -5
  10. package/dist/insights/record.js +9 -10
  11. package/dist/llm/index.d.ts +6 -2
  12. package/dist/llm/index.js +19 -9
  13. package/dist/local-backend/prompt-builder.d.ts +10 -4
  14. package/dist/local-backend/prompt-builder.js +13 -12
  15. package/dist/local-backend/router.d.ts +18 -4
  16. package/dist/local-backend/router.js +133 -98
  17. package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
  18. package/dist/local-backend/task-service.d.ts +14 -13
  19. package/dist/local-backend/task-service.js +32 -29
  20. package/dist/local-backend/worktree-service.d.ts +1 -1
  21. package/dist/local-backend/worktree-service.js +6 -6
  22. package/dist/main.js +43 -26
  23. package/dist/server/http-server.d.ts +13 -1
  24. package/dist/server/http-server.js +37 -8
  25. package/dist/server/index.js +4 -90
  26. package/dist/server/start.d.ts +26 -0
  27. package/dist/server/start.js +111 -0
  28. package/dist/sidecar/boot.d.ts +3 -1
  29. package/dist/sidecar/boot.js +6 -7
  30. package/dist/sidecar/reverse-spawn.js +12 -4
  31. package/dist/sidecar/reverse-tools.d.ts +1 -1
  32. package/dist/sidecar/reverse-tools.js +1 -1
  33. package/dist/sidecar/storage-path.js +2 -3
  34. package/dist/storage/driver.d.ts +57 -0
  35. package/dist/storage/driver.js +104 -0
  36. package/dist/storage/index.d.ts +75 -68
  37. package/dist/storage/index.js +506 -301
  38. package/dist/storage/pack-migrations.d.ts +2 -4
  39. package/dist/storage/pack-migrations.js +2 -4
  40. package/dist/storage/scoped-store.d.ts +125 -0
  41. package/dist/storage/scoped-store.js +1 -0
  42. package/dist/storage/sqlite-driver.d.ts +16 -0
  43. package/dist/storage/sqlite-driver.js +135 -0
  44. package/dist/storage/write-lease-error.d.ts +11 -0
  45. package/dist/storage/write-lease-error.js +18 -0
  46. package/package.json +14 -5
  47. package/dist/storage/local-store-singleton.d.ts +0 -28
  48. package/dist/storage/local-store-singleton.js +0 -38
@@ -1,17 +1,10 @@
1
1
  import { randomUUID } from 'crypto';
2
- import path from 'path';
3
- import Database from 'better-sqlite3';
4
- import { getUserDataDir } from '../runtime.js';
5
- import { getProductConfig } from '../product-config.js';
6
2
  import { ALL_ROUND_ASSISTANT_AGENT_ID, LOCAL_ASSISTANT_AGENT_ID, getBrand, } from '../brand.js';
7
3
  import { DEFAULT_LLM_SETTINGS, llmSettingsCarryExpiredBakedKey, llmSettingsMatchOllamaDefaults, mergeLlmSettings, } from './llm-settings.js';
8
- import { MESSAGE_CUT_FROM_ID, MESSAGE_ORDER_DESC, } from './message-order.js';
9
- import { LIST_EMPTY_CHAT_IDS_SQL } from './empty-chats.js';
4
+ import { MESSAGE_ORDER_DESC } from './message-order.js';
10
5
  import { mergeTelemetrySettings, } from './telemetry-settings.js';
11
6
  import { mergeWebSearchSettings, } from './web-search-settings.js';
12
7
  import { mergeInsightsProfile, mergeInsightsSettings, } from './insights-settings.js';
13
- import { acquireWriteLease, lockPathForDb } from './write-lease.js';
14
- import { createLocalStore } from './local-store-singleton.js';
15
8
  import { getPackMigrations } from './pack-migrations.js';
16
9
  import { getPackAgentSeeds, isUntouchedSeed, matchesPreviousIdentity, } from './pack-seeds.js';
17
10
  import { buildUsageSummary, } from './usage-summary.js';
@@ -38,31 +31,89 @@ const DEFAULT_LOCAL_USER_ID = 'local';
38
31
  // 线协议常量:insights 导出包 schema 名由遥测接收端(产品注入的
39
32
  // insightsApiBase 对端)定义,改名会破坏对端解析。
40
33
  const INSIGHTS_EXPORT_SCHEMA = 'deeppath-agent-insights/v1'; // shell-neutral:allow(线协议常量,见上行)
41
- export class LocalStore {
34
+ export class SqliteScopedStore {
42
35
  db;
43
- writeLease;
44
- constructor() {
45
- // 主库文件名是产品注入配置(3.1,product.json dbFileName)——产品
46
- // 借此保住存量数据文件;中性 shell 缺省 agent-shell.db。
47
- const dbPath = path.join(getUserDataDir(), getProductConfig().dbFileName ?? 'agent-shell.db');
48
- this.writeLease = acquireWriteLease(lockPathForDb(dbPath));
49
- this.db = new Database(dbPath);
50
- this.db.pragma('journal_mode = WAL');
51
- // The schema declares `ON DELETE CASCADE` foreign keys (chat_messages,
52
- // harness_traces -> chat_sessions), but SQLite ignores FK constraints
53
- // unless explicitly enabled per-connection. Without this, deleteChat()
54
- // only removed the session row and left orphaned messages/traces behind.
55
- this.db.pragma('foreign_keys = ON');
56
- this.migrate();
36
+ scope;
37
+ constructor(db, scope) {
38
+ this.db = db;
39
+ this.scope = scope;
40
+ }
41
+ /** Initializes schema, cleanup, migrations, and seeds for this scope. */
42
+ async initialize() {
43
+ this.migrateScopedSchema();
57
44
  this.cleanupOrphanedRows();
58
- this.seedDefaults();
45
+ await this.seedDefaults();
46
+ }
47
+ migrateScopedSchema() {
48
+ const tables = [
49
+ 'chat_sessions',
50
+ 'chat_messages',
51
+ 'chat_agents',
52
+ 'settings_kv',
53
+ 'harness_traces',
54
+ 'usage_events',
55
+ 'insights_outbox',
56
+ 'tasks',
57
+ ];
58
+ const existing = tables.filter((table) => this.tableColumns(table).length > 0);
59
+ const requiresRebuild = existing.some((table) => {
60
+ const columns = this.tableColumns(table);
61
+ const primaryKey = columns
62
+ .filter((column) => column.pk > 0)
63
+ .sort((a, b) => a.pk - b.pk)
64
+ .map((column) => column.name);
65
+ return !columns.some((column) => column.name === 'tenant_id')
66
+ || !columns.some((column) => column.name === 'user_id')
67
+ || primaryKey[0] !== 'tenant_id'
68
+ || primaryKey[1] !== 'user_id';
69
+ });
70
+ if (!requiresRebuild) {
71
+ this.migrate();
72
+ return;
73
+ }
74
+ this.db.pragma('foreign_keys = OFF');
75
+ this.db.exec('BEGIN IMMEDIATE');
76
+ try {
77
+ for (const table of existing) {
78
+ this.db.exec(`ALTER TABLE ${table} RENAME TO ${table}__scope_legacy`);
79
+ }
80
+ this.migrate();
81
+ for (const table of existing)
82
+ this.copyLegacyScopeRows(table);
83
+ for (const table of [...existing].reverse()) {
84
+ this.db.exec(`DROP TABLE ${table}__scope_legacy`);
85
+ }
86
+ // Legacy index names disappear with their tables; recreate them.
87
+ this.migrate();
88
+ this.db.exec('COMMIT');
89
+ }
90
+ catch (error) {
91
+ this.db.exec('ROLLBACK');
92
+ throw error;
93
+ }
94
+ finally {
95
+ this.db.pragma('foreign_keys = ON');
96
+ }
59
97
  }
60
- /**
61
- * 场景包存储扩展点:把同一个 SQLite 连接借给包内存储类(各包的
62
- * <Pack>Store)。包只读写自己 migrations 声明的包前缀表;shell 表结构对包不透明。
63
- */
64
- getPackDb() {
65
- return this.db;
98
+ copyLegacyScopeRows(table) {
99
+ const legacy = `${table}__scope_legacy`;
100
+ const oldColumns = this.tableColumns(legacy);
101
+ const newNames = new Set(this.tableColumns(table).map((column) => column.name));
102
+ const oldNames = new Set(oldColumns.map((column) => column.name));
103
+ const copied = oldColumns
104
+ .map((column) => column.name)
105
+ .filter((name) => name !== 'tenant_id' && name !== 'user_id' && newNames.has(name));
106
+ const target = ['tenant_id', 'user_id', ...copied];
107
+ const select = [
108
+ oldNames.has('tenant_id') ? `COALESCE(tenant_id, 'local')` : `'local'`,
109
+ oldNames.has('user_id') ? `COALESCE(user_id, 'local')` : `'local'`,
110
+ ...copied,
111
+ ];
112
+ this.db.exec(`INSERT OR IGNORE INTO ${table} (${target.join(', ')})
113
+ SELECT ${select.join(', ')} FROM ${legacy}`);
114
+ }
115
+ tableColumns(table) {
116
+ return this.db.prepare(`PRAGMA table_info(${table})`).all();
66
117
  }
67
118
  /**
68
119
  * One-time cleanup for rows that became orphaned while `foreign_keys` was
@@ -70,41 +121,95 @@ export class LocalStore {
70
121
  * chat_messages/harness_traces behind for deleted chat_sessions).
71
122
  */
72
123
  cleanupOrphanedRows() {
124
+ for (const table of [
125
+ 'chat_sessions',
126
+ 'chat_messages',
127
+ 'chat_agents',
128
+ 'settings_kv',
129
+ 'harness_traces',
130
+ 'usage_events',
131
+ 'insights_outbox',
132
+ 'tasks',
133
+ ]) {
134
+ this.ensureColumn(table, 'tenant_id', `TEXT NOT NULL DEFAULT 'local'`);
135
+ this.ensureColumn(table, 'user_id', `TEXT NOT NULL DEFAULT 'local'`);
136
+ }
73
137
  this.db.exec(`
74
138
  DELETE FROM chat_messages
75
- WHERE chat_id NOT IN (SELECT id FROM chat_sessions);
139
+ WHERE NOT EXISTS (
140
+ SELECT 1 FROM chat_sessions
141
+ WHERE chat_sessions.tenant_id = chat_messages.tenant_id
142
+ AND chat_sessions.user_id = chat_messages.user_id
143
+ AND chat_sessions.id = chat_messages.chat_id
144
+ );
76
145
  DELETE FROM harness_traces
77
- WHERE chat_id NOT IN (SELECT id FROM chat_sessions);
146
+ WHERE NOT EXISTS (
147
+ SELECT 1 FROM chat_sessions
148
+ WHERE chat_sessions.tenant_id = harness_traces.tenant_id
149
+ AND chat_sessions.user_id = harness_traces.user_id
150
+ AND chat_sessions.id = harness_traces.chat_id
151
+ );
78
152
  DELETE FROM tasks
79
- WHERE chat_id NOT IN (SELECT id FROM chat_sessions);
153
+ WHERE NOT EXISTS (
154
+ SELECT 1 FROM chat_sessions
155
+ WHERE chat_sessions.tenant_id = tasks.tenant_id
156
+ AND chat_sessions.user_id = tasks.user_id
157
+ AND chat_sessions.id = tasks.chat_id
158
+ );
159
+ `);
160
+ this.db.exec(`
161
+ CREATE INDEX IF NOT EXISTS idx_chat_sessions_scope
162
+ ON chat_sessions(tenant_id, user_id, updated_at DESC);
163
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_scope
164
+ ON chat_messages(tenant_id, user_id, chat_id, created_at);
165
+ CREATE INDEX IF NOT EXISTS idx_chat_agents_scope
166
+ ON chat_agents(tenant_id, user_id, sort_order);
167
+ CREATE INDEX IF NOT EXISTS idx_settings_scope
168
+ ON settings_kv(tenant_id, user_id, key);
169
+ CREATE INDEX IF NOT EXISTS idx_traces_scope
170
+ ON harness_traces(tenant_id, user_id, id);
171
+ CREATE INDEX IF NOT EXISTS idx_usage_scope
172
+ ON usage_events(tenant_id, user_id, created_at DESC);
173
+ CREATE INDEX IF NOT EXISTS idx_insights_scope
174
+ ON insights_outbox(tenant_id, user_id, created_at DESC);
175
+ CREATE INDEX IF NOT EXISTS idx_tasks_scope
176
+ ON tasks(tenant_id, user_id, updated_at DESC);
80
177
  `);
81
178
  }
82
179
  migrate() {
83
180
  this.db.exec(`
84
181
  CREATE TABLE IF NOT EXISTS chat_sessions (
85
- id TEXT PRIMARY KEY,
86
- title TEXT NOT NULL,
182
+ tenant_id TEXT NOT NULL,
87
183
  user_id TEXT NOT NULL,
184
+ id TEXT NOT NULL,
185
+ title TEXT NOT NULL,
88
186
  agent_id TEXT,
89
187
  is_pinned INTEGER NOT NULL DEFAULT 0,
90
188
  system_prompt TEXT,
91
189
  pinned_refs TEXT,
92
190
  created_at TEXT NOT NULL,
93
- updated_at TEXT NOT NULL
191
+ updated_at TEXT NOT NULL,
192
+ PRIMARY KEY (tenant_id, user_id, id)
94
193
  );
95
194
 
96
195
  CREATE TABLE IF NOT EXISTS chat_messages (
97
- id TEXT PRIMARY KEY,
196
+ tenant_id TEXT NOT NULL,
197
+ user_id TEXT NOT NULL,
198
+ id TEXT NOT NULL,
98
199
  chat_id TEXT NOT NULL,
99
200
  role TEXT NOT NULL,
100
201
  content TEXT NOT NULL,
101
202
  message_metadata TEXT,
102
203
  created_at TEXT NOT NULL,
103
- FOREIGN KEY(chat_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
204
+ PRIMARY KEY (tenant_id, user_id, id),
205
+ FOREIGN KEY(tenant_id, user_id, chat_id)
206
+ REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
104
207
  );
105
208
 
106
209
  CREATE TABLE IF NOT EXISTS chat_agents (
107
- id TEXT PRIMARY KEY,
210
+ tenant_id TEXT NOT NULL,
211
+ user_id TEXT NOT NULL,
212
+ id TEXT NOT NULL,
108
213
  slug TEXT,
109
214
  name TEXT NOT NULL,
110
215
  icon TEXT,
@@ -119,16 +224,22 @@ export class LocalStore {
119
224
  is_archived INTEGER NOT NULL DEFAULT 0,
120
225
  sort_order INTEGER NOT NULL DEFAULT 0,
121
226
  created_at TEXT NOT NULL,
122
- updated_at TEXT NOT NULL
227
+ updated_at TEXT NOT NULL,
228
+ PRIMARY KEY (tenant_id, user_id, id)
123
229
  );
124
230
 
125
231
  CREATE TABLE IF NOT EXISTS settings_kv (
126
- key TEXT PRIMARY KEY,
127
- value TEXT NOT NULL
232
+ tenant_id TEXT NOT NULL,
233
+ user_id TEXT NOT NULL,
234
+ key TEXT NOT NULL,
235
+ value TEXT NOT NULL,
236
+ PRIMARY KEY (tenant_id, user_id, key)
128
237
  );
129
238
 
130
239
  CREATE TABLE IF NOT EXISTS harness_traces (
131
- id TEXT PRIMARY KEY,
240
+ tenant_id TEXT NOT NULL,
241
+ user_id TEXT NOT NULL,
242
+ id TEXT NOT NULL,
132
243
  chat_id TEXT NOT NULL,
133
244
  message_id TEXT,
134
245
  started_at_ms INTEGER NOT NULL,
@@ -136,13 +247,17 @@ export class LocalStore {
136
247
  status TEXT NOT NULL,
137
248
  payload TEXT NOT NULL,
138
249
  created_at TEXT NOT NULL,
139
- FOREIGN KEY(chat_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
250
+ PRIMARY KEY (tenant_id, user_id, id),
251
+ FOREIGN KEY(tenant_id, user_id, chat_id)
252
+ REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
140
253
  );
141
254
 
142
255
  CREATE INDEX IF NOT EXISTS idx_harness_traces_chat ON harness_traces(chat_id, started_at_ms DESC);
143
256
 
144
257
  CREATE TABLE IF NOT EXISTS usage_events (
145
- id TEXT PRIMARY KEY,
258
+ tenant_id TEXT NOT NULL,
259
+ user_id TEXT NOT NULL,
260
+ id TEXT NOT NULL,
146
261
  chat_id TEXT,
147
262
  kind TEXT NOT NULL,
148
263
  provider TEXT,
@@ -152,24 +267,30 @@ export class LocalStore {
152
267
  total_tokens INTEGER NOT NULL DEFAULT 0,
153
268
  cached_prompt_tokens INTEGER NOT NULL DEFAULT 0,
154
269
  cost_usd REAL,
155
- created_at TEXT NOT NULL
270
+ created_at TEXT NOT NULL,
271
+ PRIMARY KEY (tenant_id, user_id, id)
156
272
  );
157
273
 
158
274
  CREATE INDEX IF NOT EXISTS idx_usage_events_created ON usage_events(created_at DESC);
159
275
 
160
276
  CREATE TABLE IF NOT EXISTS insights_outbox (
161
- id TEXT PRIMARY KEY,
277
+ tenant_id TEXT NOT NULL,
278
+ user_id TEXT NOT NULL,
279
+ id TEXT NOT NULL,
162
280
  kind TEXT NOT NULL,
163
281
  payload TEXT NOT NULL,
164
282
  created_at TEXT NOT NULL,
165
283
  uploaded_at TEXT,
166
- upload_error TEXT
284
+ upload_error TEXT,
285
+ PRIMARY KEY (tenant_id, user_id, id)
167
286
  );
168
287
 
169
288
  CREATE INDEX IF NOT EXISTS idx_insights_outbox_pending ON insights_outbox(kind, uploaded_at, created_at);
170
289
 
171
290
  CREATE TABLE IF NOT EXISTS tasks (
172
- id TEXT PRIMARY KEY,
291
+ tenant_id TEXT NOT NULL,
292
+ user_id TEXT NOT NULL,
293
+ id TEXT NOT NULL,
173
294
  chat_id TEXT NOT NULL,
174
295
  task TEXT NOT NULL,
175
296
  status TEXT NOT NULL,
@@ -182,7 +303,9 @@ export class LocalStore {
182
303
  trace_id TEXT,
183
304
  created_at TEXT NOT NULL,
184
305
  updated_at TEXT NOT NULL,
185
- FOREIGN KEY(chat_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
306
+ PRIMARY KEY (tenant_id, user_id, id),
307
+ FOREIGN KEY(tenant_id, user_id, chat_id)
308
+ REFERENCES chat_sessions(tenant_id, user_id, id) ON DELETE CASCADE
186
309
  );
187
310
 
188
311
  CREATE INDEX IF NOT EXISTS idx_tasks_chat ON tasks(chat_id, datetime(updated_at) DESC);
@@ -207,29 +330,44 @@ export class LocalStore {
207
330
  // 由用户在智能体管理页自由开关(不会每次启动被重置)。
208
331
  if (this.ensureColumn('chat_agents', 'load_all_skills', 'INTEGER NOT NULL DEFAULT 0')) {
209
332
  this.db
210
- .prepare(`UPDATE chat_agents SET load_all_skills = 1 WHERE id = ?`)
211
- .run(ALL_ROUND_ASSISTANT_AGENT_ID);
333
+ .prepare(`UPDATE chat_agents SET load_all_skills = 1
334
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
335
+ .run(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
212
336
  }
213
- // 场景包迁移(0.3b):构造时应用一次已注册的包迁移(兼容注册先于
214
- // 构造的路径);注册晚于构造的容错路径见 applyRegisteredPackMigrations
215
- // 模块级函数(host/runtime 在包装配前调用)。
216
- this.applyPackMigrations();
217
337
  }
218
- appliedPackMigrations = new Set();
219
338
  /**
220
- * 应用尚未应用的已注册包迁移(幂等,按包 id 去重)。
339
+ * Applies registered migrations once per driver-persisted pack generation.
221
340
  */
222
341
  applyPackMigrations() {
342
+ this.db.exec(`
343
+ CREATE TABLE IF NOT EXISTS storage_pack_migrations (
344
+ pack_id TEXT PRIMARY KEY,
345
+ version INTEGER NOT NULL
346
+ )
347
+ `);
223
348
  for (const { packId, migration } of getPackMigrations()) {
224
- if (this.appliedPackMigrations.has(packId))
225
- continue;
226
- if (migration.ddl && migration.ddl.length > 0) {
227
- this.db.exec(migration.ddl.join(';\n'));
228
- }
229
- for (const col of migration.ensureColumns ?? []) {
230
- this.ensureColumn(col.table, col.column, col.type);
349
+ const version = migration.version ?? 1;
350
+ const applied = this.db.prepare(`SELECT version FROM storage_pack_migrations WHERE pack_id = ?`).get(packId);
351
+ if (applied && applied.version > version) {
352
+ throw new Error(`[storage] pack ${packId} schema ${applied.version} is newer than supported ${version}`);
231
353
  }
232
- this.appliedPackMigrations.add(packId);
354
+ if (applied?.version === version)
355
+ continue;
356
+ this.db.transaction(() => {
357
+ if (migration.beforeDdl && migration.beforeDdl.length > 0) {
358
+ this.db.exec(migration.beforeDdl.join(';\n'));
359
+ }
360
+ for (const col of migration.ensureColumns ?? []) {
361
+ this.ensureColumn(col.table, col.column, col.type);
362
+ }
363
+ if (migration.ddl && migration.ddl.length > 0) {
364
+ this.db.exec(migration.ddl.join(';\n'));
365
+ }
366
+ this.db.prepare(`
367
+ INSERT INTO storage_pack_migrations (pack_id, version) VALUES (?, ?)
368
+ ON CONFLICT(pack_id) DO UPDATE SET version = excluded.version
369
+ `).run(packId, version);
370
+ })();
233
371
  }
234
372
  }
235
373
  /**
@@ -239,18 +377,22 @@ export class LocalStore {
239
377
  */
240
378
  applyPackAgentSeed(seed, active, insertAgent, now) {
241
379
  const row = this.db
242
- .prepare(`SELECT id, name, description, role_prompt, is_archived FROM chat_agents WHERE id = ?`)
243
- .get(seed.id);
380
+ .prepare(`SELECT id, name, description, role_prompt, is_archived FROM chat_agents
381
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
382
+ .get(this.scope.tenantId, this.scope.userId, seed.id);
244
383
  if (!active) {
245
384
  if (row && row.is_archived === 0 && isUntouchedSeed(seed, row)) {
246
385
  this.db
247
- .prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ? WHERE id = ?`)
248
- .run(now, seed.id);
386
+ .prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ?
387
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
388
+ .run(now, this.scope.tenantId, this.scope.userId, seed.id);
249
389
  }
250
390
  return;
251
391
  }
252
392
  if (!row) {
253
393
  insertAgent.run({
394
+ tenantId: this.scope.tenantId,
395
+ userId: this.scope.userId,
254
396
  id: seed.id,
255
397
  slug: seed.slug,
256
398
  name: seed.name,
@@ -279,20 +421,23 @@ export class LocalStore {
279
421
  description = @description,
280
422
  role_prompt = @rolePrompt,
281
423
  updated_at = @updatedAt
282
- WHERE id = @id`)
424
+ WHERE tenant_id = @tenantId AND user_id = @userId AND id = @id`)
283
425
  .run({
284
426
  name: seed.name,
285
427
  description: seed.description ?? null,
286
428
  rolePrompt: seed.rolePrompt,
287
429
  updatedAt: now,
430
+ tenantId: this.scope.tenantId,
431
+ userId: this.scope.userId,
288
432
  id: seed.id,
289
433
  });
290
434
  }
291
435
  // 从未定制过、却被归档的行:包装回该产品后重新露出。
292
436
  if (row.is_archived === 1 && isUntouchedSeed(seed, row)) {
293
437
  this.db
294
- .prepare(`UPDATE chat_agents SET is_archived = 0, updated_at = ? WHERE id = ?`)
295
- .run(now, seed.id);
438
+ .prepare(`UPDATE chat_agents SET is_archived = 0, updated_at = ?
439
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
440
+ .run(now, this.scope.tenantId, this.scope.userId, seed.id);
296
441
  }
297
442
  }
298
443
  /**
@@ -307,20 +452,22 @@ export class LocalStore {
307
452
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
308
453
  return true;
309
454
  }
310
- seedDefaults() {
455
+ async seedDefaults() {
311
456
  const now = new Date().toISOString();
312
457
  const hasBuiltin = this.db
313
- .prepare(`SELECT id FROM chat_agents WHERE id = ?`)
314
- .get(LOCAL_ASSISTANT_AGENT_ID);
458
+ .prepare(`SELECT id FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`)
459
+ .get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
315
460
  // 激活包中 sortOrder 0 的主打种子占据列表首位时,shell 默认智能体
316
461
  // 让位到 1(0.3d 起由包种子驱动,不再按 flavor 硬编码)。
317
462
  const hasHeroSeed = getPackAgentSeeds().some((r) => r.active && r.seeds.some((s) => s.sortOrder === 0));
318
463
  const insertAgent = this.db.prepare(`
319
464
  INSERT INTO chat_agents (
465
+ tenant_id, user_id,
320
466
  id, slug, name, icon, color, description, role_prompt, forbidden_prompt,
321
467
  skill_ids, tool_policy, allow_external_skills, load_all_skills, is_builtin, is_archived,
322
468
  sort_order, created_at, updated_at
323
469
  ) VALUES (
470
+ @tenantId, @userId,
324
471
  @id, @slug, @name, @icon, @color, @description, @rolePrompt, @forbiddenPrompt,
325
472
  @skillIds, @toolPolicy, @allowExternalSkills, @loadAllSkills, @isBuiltin, @isArchived,
326
473
  @sortOrder, @createdAt, @updatedAt
@@ -328,13 +475,15 @@ export class LocalStore {
328
475
  `);
329
476
  if (!hasBuiltin) {
330
477
  insertAgent.run({
478
+ tenantId: this.scope.tenantId,
479
+ userId: this.scope.userId,
331
480
  id: LOCAL_ASSISTANT_AGENT_ID,
332
481
  slug: LOCAL_ASSISTANT_AGENT_ID,
333
482
  name: '电脑操作员',
334
483
  icon: 'Cpu',
335
484
  color: '#4f46e5',
336
485
  description: '默认本地助手,可调用 shell、文件与 MCP 工具。',
337
- rolePrompt: '你是本地离线助手,回答时清晰、可执行。',
486
+ rolePrompt: '你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。',
338
487
  forbiddenPrompt: null,
339
488
  skillIds: JSON.stringify([]),
340
489
  toolPolicy: JSON.stringify({ mode: 'all', tools: [] }),
@@ -349,13 +498,27 @@ export class LocalStore {
349
498
  });
350
499
  }
351
500
  else {
352
- this.db.prepare(`UPDATE chat_agents SET name = ? WHERE id = ?`).run('电脑操作员', LOCAL_ASSISTANT_AGENT_ID);
501
+ this.db.prepare(`UPDATE chat_agents SET name = ?
502
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
503
+ .run('电脑操作员', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
504
+ const local = this.db
505
+ .prepare(`SELECT role_prompt FROM chat_agents
506
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
507
+ .get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
508
+ if (local?.role_prompt === '你是本地离线助手,回答时清晰、可执行。') {
509
+ this.db
510
+ .prepare(`UPDATE chat_agents SET role_prompt = ?
511
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
512
+ .run('你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
513
+ }
353
514
  }
354
515
  const hasAllRound = this.db
355
- .prepare(`SELECT id FROM chat_agents WHERE id = ?`)
356
- .get(ALL_ROUND_ASSISTANT_AGENT_ID);
516
+ .prepare(`SELECT id FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`)
517
+ .get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
357
518
  if (!hasAllRound) {
358
519
  insertAgent.run({
520
+ tenantId: this.scope.tenantId,
521
+ userId: this.scope.userId,
359
522
  id: ALL_ROUND_ASSISTANT_AGENT_ID,
360
523
  slug: ALL_ROUND_ASSISTANT_AGENT_ID,
361
524
  name: '智能助手',
@@ -382,13 +545,18 @@ export class LocalStore {
382
545
  });
383
546
  }
384
547
  else {
385
- const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents WHERE id = ?`).get(ALL_ROUND_ASSISTANT_AGENT_ID);
548
+ const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents
549
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
386
550
  if (agent) {
387
551
  const updatedPrompt = agent.role_prompt.replace(/全能专家助手/g, '智能助手');
388
- this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ? WHERE id = ?`).run('智能助手', updatedPrompt, ALL_ROUND_ASSISTANT_AGENT_ID);
552
+ this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ?
553
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
554
+ .run('智能助手', updatedPrompt, this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
389
555
  }
390
556
  else {
391
- this.db.prepare(`UPDATE chat_agents SET name = ? WHERE id = ?`).run('智能助手', ALL_ROUND_ASSISTANT_AGENT_ID);
557
+ this.db.prepare(`UPDATE chat_agents SET name = ?
558
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
559
+ .run('智能助手', this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
392
560
  }
393
561
  }
394
562
  // ─── 场景包智能体种子(0.3d) ───
@@ -409,38 +577,42 @@ export class LocalStore {
409
577
  if (seed.sortOrder !== 0)
410
578
  continue;
411
579
  const localSort = this.db
412
- .prepare(`SELECT sort_order FROM chat_agents WHERE id = ?`)
413
- .get(LOCAL_ASSISTANT_AGENT_ID);
580
+ .prepare(`SELECT sort_order FROM chat_agents
581
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
582
+ .get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
414
583
  const seedSort = this.db
415
- .prepare(`SELECT sort_order FROM chat_agents WHERE id = ?`)
416
- .get(seed.id);
584
+ .prepare(`SELECT sort_order FROM chat_agents
585
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
586
+ .get(this.scope.tenantId, this.scope.userId, seed.id);
417
587
  if (localSort &&
418
588
  seedSort &&
419
589
  localSort.sort_order === 0 &&
420
590
  (seedSort.sort_order === 0 || seedSort.sort_order === 1)) {
421
591
  this.db
422
- .prepare(`UPDATE chat_agents SET sort_order = 1, updated_at = ? WHERE id = ?`)
423
- .run(now, LOCAL_ASSISTANT_AGENT_ID);
592
+ .prepare(`UPDATE chat_agents SET sort_order = 1, updated_at = ?
593
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
594
+ .run(now, this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
424
595
  this.db
425
- .prepare(`UPDATE chat_agents SET sort_order = 0, updated_at = ? WHERE id = ?`)
426
- .run(now, seed.id);
596
+ .prepare(`UPDATE chat_agents SET sort_order = 0, updated_at = ?
597
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
598
+ .run(now, this.scope.tenantId, this.scope.userId, seed.id);
427
599
  }
428
600
  }
429
601
  }
430
- const hasSettings = this.getLlmSettings();
602
+ const hasSettings = await this.getLlmSettings();
431
603
  if (!hasSettings) {
432
604
  // 全新安装:直接写出厂默认 (DEFAULT_LLM_SETTINGS = OpenAI 兼容 / DeepSeek)。
433
- this.setLlmSettings(DEFAULT_LLM_SETTINGS);
605
+ await this.setLlmSettings(DEFAULT_LLM_SETTINGS);
434
606
  }
435
607
  else if (llmSettingsMatchOllamaDefaults(hasSettings)) {
436
608
  // 老安装但用户从未改过 LLM 设置(依然停留在出厂 Ollama 预设):静默迁移
437
609
  // 到 DeepSeek 默认,避免 Windows 用户卡在"本机没装 ollama → 上来就不能聊"。
438
- this.setLlmSettings(DEFAULT_LLM_SETTINGS);
610
+ await this.setLlmSettings(DEFAULT_LLM_SETTINGS);
439
611
  }
440
612
  else if (llmSettingsCarryExpiredBakedKey(hasSettings)) {
441
613
  // 老安装保存过已作废的出厂内置 key(≤0.0.35):静默清掉(保留其它自定义
442
614
  // 字段),设置页会引导用户填自己的 key。不清则用户永远拿着死 key 撞 401。
443
- this.setLlmSettings({ ...hasSettings, apiKey: undefined });
615
+ await this.setLlmSettings({ ...hasSettings, apiKey: undefined });
444
616
  }
445
617
  // 其它情况(用户改过 provider/model/baseUrl/apiKey 任一项):尊重自定义,不动。
446
618
  }
@@ -456,24 +628,25 @@ export class LocalStore {
456
628
  return fallback;
457
629
  return Math.max(min, Math.min(Math.trunc(value), max));
458
630
  }
459
- listChats(page = 1, limit = 50) {
460
- const safePage = LocalStore.clampInt(page, 1, 1, Number.MAX_SAFE_INTEGER);
461
- const safeLimit = LocalStore.clampInt(limit, 50, 1, 200);
631
+ async listChats(page = 1, limit = 50) {
632
+ const safePage = SqliteScopedStore.clampInt(page, 1, 1, Number.MAX_SAFE_INTEGER);
633
+ const safeLimit = SqliteScopedStore.clampInt(limit, 50, 1, 200);
462
634
  const offset = (safePage - 1) * safeLimit;
463
- const totalRow = this.db.prepare(`SELECT COUNT(*) as count FROM chat_sessions`).get();
635
+ const totalRow = this.db.prepare(`SELECT COUNT(*) as count FROM chat_sessions WHERE tenant_id = ? AND user_id = ?`).get(this.scope.tenantId, this.scope.userId);
464
636
  const rows = this.db.prepare(`
465
637
  SELECT *
466
638
  FROM chat_sessions
639
+ WHERE tenant_id = ? AND user_id = ?
467
640
  ORDER BY is_pinned DESC, datetime(updated_at) DESC
468
641
  LIMIT ? OFFSET ?
469
- `).all(safeLimit, offset);
642
+ `).all(this.scope.tenantId, this.scope.userId, safeLimit, offset);
470
643
  return {
471
644
  chats: rows.map(row => this.mapChatSession(row)),
472
645
  total: totalRow.count,
473
646
  };
474
647
  }
475
- createChat(title = '新对话', agentId = getBrand().defaultAgentId, projectId = null) {
476
- return this.createChatWithId(randomUUID(), title, agentId, projectId);
648
+ async createChat(title = '新对话', agentId = getBrand().defaultAgentId, projectId = null) {
649
+ return await this.createChatWithId(randomUUID(), title, agentId, projectId);
477
650
  }
478
651
  /**
479
652
  * 用调用方给定的 id 建会话(**幂等**:已存在则原样返回,不覆盖)。
@@ -484,35 +657,37 @@ export class LocalStore {
484
657
  * 按 URL 里的 id 现场补建,才不会把一次本来能跑的任务打断在
485
658
  * `chat not found`。
486
659
  */
487
- createChatWithId(chatId, title = '新对话', agentId = 'local-assistant', projectId = null) {
488
- const existing = this.getChat(chatId);
660
+ async createChatWithId(chatId, title = '新对话', agentId = 'local-assistant', projectId = null) {
661
+ const existing = await this.getChat(chatId);
489
662
  if (existing)
490
663
  return existing;
491
664
  const now = new Date().toISOString();
492
665
  // INSERT OR IGNORE:两个请求同时补建同一个 id(并发首发送)时,后到的
493
666
  // 那条不会因主键冲突被打成 500——两边拿到的都是同一条会话。
494
667
  this.db.prepare(`
495
- INSERT OR IGNORE INTO chat_sessions (id, title, user_id, agent_id, project_id, created_at, updated_at)
496
- VALUES (?, ?, ?, ?, ?, ?, ?)
497
- `).run(chatId, title, DEFAULT_LOCAL_USER_ID, agentId, projectId, now, now);
498
- const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE id = ?`).get(chatId);
668
+ INSERT OR IGNORE INTO chat_sessions
669
+ (tenant_id, user_id, id, title, agent_id, project_id, created_at, updated_at)
670
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
671
+ `).run(this.scope.tenantId, this.scope.userId, chatId, title, agentId, projectId, now, now);
672
+ const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, chatId);
499
673
  return this.mapChatSession(row);
500
674
  }
501
675
  /** 删除项目时调用:把该项目下所有会话降级为无项目对话(不删会话)。 */
502
- clearProjectAssignment(projectId) {
676
+ async clearProjectAssignment(projectId) {
503
677
  const info = this.db
504
- .prepare(`UPDATE chat_sessions SET project_id = NULL WHERE project_id = ?`)
505
- .run(projectId);
678
+ .prepare(`UPDATE chat_sessions SET project_id = NULL
679
+ WHERE tenant_id = ? AND user_id = ? AND project_id = ?`)
680
+ .run(this.scope.tenantId, this.scope.userId, projectId);
506
681
  return info.changes;
507
682
  }
508
- getChat(chatId) {
509
- const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE id = ?`).get(chatId);
683
+ async getChat(chatId) {
684
+ const row = this.db.prepare(`SELECT * FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, chatId);
510
685
  if (!row)
511
686
  return null;
512
687
  return this.mapChatSession(row);
513
688
  }
514
- updateChat(chatId, updates) {
515
- const existing = this.getChat(chatId);
689
+ async updateChat(chatId, updates) {
690
+ const existing = await this.getChat(chatId);
516
691
  if (!existing)
517
692
  return null;
518
693
  // 过滤 undefined:调用方对"未提供的字段"传 undefined,直接展开会把
@@ -527,20 +702,21 @@ export class LocalStore {
527
702
  this.db.prepare(`
528
703
  UPDATE chat_sessions
529
704
  SET title = ?, system_prompt = ?, pinned_refs = ?, is_pinned = ?, project_id = ?, updated_at = ?
530
- WHERE id = ?
531
- `).run(next.title, next.systemPrompt, JSON.stringify(next.pinnedRefs ?? null), next.isPinned ? 1 : 0, next.projectId ?? null, next.updatedAt, chatId);
532
- return this.getChat(chatId);
533
- }
534
- deleteChat(chatId) {
535
- const info = this.db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(chatId);
536
- this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`chat_record:${chatId}`);
537
- this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`turn_active:${chatId}`);
705
+ WHERE tenant_id = ? AND user_id = ? AND id = ?
706
+ `).run(next.title, next.systemPrompt, JSON.stringify(next.pinnedRefs ?? null), next.isPinned ? 1 : 0, next.projectId ?? null, next.updatedAt, this.scope.tenantId, this.scope.userId, chatId);
707
+ return await this.getChat(chatId);
708
+ }
709
+ async deleteChat(chatId) {
710
+ const info = this.db.prepare(`DELETE FROM chat_sessions WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(this.scope.tenantId, this.scope.userId, chatId);
711
+ this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `chat_record:${chatId}`);
712
+ this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `turn_active:${chatId}`);
538
713
  return info.changes > 0;
539
714
  }
540
- chatHasMessages(chatId) {
715
+ async chatHasMessages(chatId) {
541
716
  const row = this.db
542
- .prepare(`SELECT 1 FROM chat_messages WHERE chat_id = ? LIMIT 1`)
543
- .get(chatId);
717
+ .prepare(`SELECT 1 FROM chat_messages
718
+ WHERE tenant_id = ? AND user_id = ? AND chat_id = ? LIMIT 1`)
719
+ .get(this.scope.tenantId, this.scope.userId, chatId);
544
720
  return Boolean(row);
545
721
  }
546
722
  /**
@@ -548,21 +724,30 @@ export class LocalStore {
548
724
  * "新对话" composer without sending — empty sessions must not stay in the
549
725
  * sidebar. Missing or already-populated chats are a no-op (`false`).
550
726
  */
551
- deleteChatIfEmpty(chatId) {
552
- if (!this.getChat(chatId) || this.chatHasMessages(chatId))
727
+ async deleteChatIfEmpty(chatId) {
728
+ if (!(await this.getChat(chatId)) || (await this.chatHasMessages(chatId)))
553
729
  return false;
554
- return this.deleteChat(chatId);
730
+ return await this.deleteChat(chatId);
555
731
  }
556
732
  /**
557
733
  * Delete every session with zero messages, optionally keeping `exceptChatId`
558
734
  * (the open composer). Returns the removed ids.
559
735
  */
560
- deleteEmptyChats(exceptChatId) {
736
+ async deleteEmptyChats(exceptChatId) {
561
737
  const except = exceptChatId ?? null;
562
- const rows = this.db.prepare(LIST_EMPTY_CHAT_IDS_SQL).all(except, except);
738
+ const rows = this.db.prepare(`
739
+ SELECT id FROM chat_sessions
740
+ WHERE tenant_id = ? AND user_id = ?
741
+ AND (? IS NULL OR id != ?)
742
+ AND NOT EXISTS (
743
+ SELECT 1 FROM chat_messages
744
+ WHERE tenant_id = ? AND user_id = ?
745
+ AND chat_id = chat_sessions.id
746
+ )
747
+ `).all(this.scope.tenantId, this.scope.userId, except, except, this.scope.tenantId, this.scope.userId);
563
748
  const ids = rows.map((row) => row.id);
564
749
  for (const id of ids)
565
- this.deleteChat(id);
750
+ await this.deleteChat(id);
566
751
  return ids;
567
752
  }
568
753
  /**
@@ -571,17 +756,11 @@ export class LocalStore {
571
756
  * a branch), so a chat's live record moves from `chatId` to its branch;
572
757
  * null means the chatId itself.
573
758
  */
574
- getChatRecordId(chatId) {
575
- const row = this.db
576
- .prepare(`SELECT value FROM settings_kv WHERE key = ?`)
577
- .get(`chat_record:${chatId}`);
578
- return row?.value ?? null;
759
+ async getChatRecordId(chatId) {
760
+ return this.getSetting(`chat_record:${chatId}`);
579
761
  }
580
- setChatRecordId(chatId, recordId) {
581
- this.db.prepare(`
582
- INSERT INTO settings_kv (key, value) VALUES (?, ?)
583
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
584
- `).run(`chat_record:${chatId}`, recordId);
762
+ async setChatRecordId(chatId, recordId) {
763
+ this.setSetting(`chat_record:${chatId}`, recordId);
585
764
  }
586
765
  /**
587
766
  * W7-1: durable "a turn is in flight" marker, written BEFORE the stream
@@ -595,23 +774,18 @@ export class LocalStore {
595
774
  * stale marker the detector dismisses once it sees the trailing
596
775
  * assistant message.
597
776
  */
598
- setTurnActive(chatId) {
599
- this.db.prepare(`
600
- INSERT INTO settings_kv (key, value) VALUES (?, ?)
601
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
602
- `).run(`turn_active:${chatId}`, JSON.stringify({ startedAt: new Date().toISOString() }));
777
+ async setTurnActive(chatId) {
778
+ this.setSetting(`turn_active:${chatId}`, JSON.stringify({ startedAt: new Date().toISOString() }));
603
779
  }
604
- clearTurnActive(chatId) {
605
- this.db.prepare(`DELETE FROM settings_kv WHERE key = ?`).run(`turn_active:${chatId}`);
780
+ async clearTurnActive(chatId) {
781
+ this.db.prepare(`DELETE FROM settings_kv WHERE tenant_id = ? AND user_id = ? AND key = ?`).run(this.scope.tenantId, this.scope.userId, `turn_active:${chatId}`);
606
782
  }
607
- getTurnActive(chatId) {
608
- const row = this.db
609
- .prepare(`SELECT value FROM settings_kv WHERE key = ?`)
610
- .get(`turn_active:${chatId}`);
611
- if (!row)
783
+ async getTurnActive(chatId) {
784
+ const value = this.getSetting(`turn_active:${chatId}`);
785
+ if (!value)
612
786
  return null;
613
787
  try {
614
- const parsed = JSON.parse(row.value);
788
+ const parsed = JSON.parse(value);
615
789
  return typeof parsed.startedAt === 'string' ? { startedAt: parsed.startedAt } : null;
616
790
  }
617
791
  catch {
@@ -622,20 +796,21 @@ export class LocalStore {
622
796
  * Newest first under {@link MESSAGE_ORDER_DESC}, capped at `limit`; callers
623
797
  * that want transcript order reverse it.
624
798
  */
625
- listMessages(chatId, limit = 200) {
799
+ async listMessages(chatId, limit = 200) {
626
800
  const rows = this.db.prepare(`
627
801
  SELECT *
628
802
  FROM chat_messages
629
- WHERE chat_id = ?
803
+ WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
630
804
  ORDER BY ${MESSAGE_ORDER_DESC}
631
805
  LIMIT ?
632
- `).all(chatId, LocalStore.clampInt(limit, 200, 1, 1000));
806
+ `).all(this.scope.tenantId, this.scope.userId, chatId, SqliteScopedStore.clampInt(limit, 200, 1, 1000));
633
807
  return rows.map(row => this.mapChatMessage(row));
634
808
  }
635
- getMessage(chatId, messageId) {
809
+ async getMessage(chatId, messageId) {
636
810
  const row = this.db
637
- .prepare(`SELECT * FROM chat_messages WHERE id = ? AND chat_id = ?`)
638
- .get(messageId, chatId);
811
+ .prepare(`SELECT * FROM chat_messages
812
+ WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?`)
813
+ .get(this.scope.tenantId, this.scope.userId, messageId, chatId);
639
814
  return row ? this.mapChatMessage(row) : null;
640
815
  }
641
816
  /**
@@ -648,25 +823,30 @@ export class LocalStore {
648
823
  * "After" is {@link MESSAGE_CUT_FROM_ID}, the same key `listMessages` orders
649
824
  * by, so the cut matches what the user sees below the regenerated reply.
650
825
  */
651
- deleteMessagesFrom(chatId, messageId) {
826
+ async deleteMessagesFrom(chatId, messageId) {
652
827
  const info = this.db
653
828
  .prepare(`
654
829
  DELETE FROM chat_messages
655
- WHERE chat_id = ?
656
- AND ${MESSAGE_CUT_FROM_ID}
830
+ WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
831
+ AND (created_at, rowid) >= (
832
+ SELECT created_at, rowid FROM chat_messages
833
+ WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?
834
+ )
657
835
  `)
658
- .run(chatId, messageId, chatId);
836
+ .run(this.scope.tenantId, this.scope.userId, chatId, this.scope.tenantId, this.scope.userId, messageId, chatId);
659
837
  return info.changes;
660
838
  }
661
- addMessage(chatId, role, content, messageMetadata = null) {
839
+ async addMessage(chatId, role, content, messageMetadata = null) {
662
840
  const id = randomUUID();
663
841
  const now = new Date().toISOString();
664
842
  this.db.prepare(`
665
- INSERT INTO chat_messages (id, chat_id, role, content, message_metadata, created_at)
666
- VALUES (?, ?, ?, ?, ?, ?)
667
- `).run(id, chatId, role, content, messageMetadata, now);
668
- this.db.prepare(`UPDATE chat_sessions SET updated_at = ? WHERE id = ?`).run(now, chatId);
669
- const row = this.db.prepare(`SELECT * FROM chat_messages WHERE id = ?`).get(id);
843
+ INSERT INTO chat_messages
844
+ (tenant_id, user_id, id, chat_id, role, content, message_metadata, created_at)
845
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
846
+ `).run(this.scope.tenantId, this.scope.userId, id, chatId, role, content, messageMetadata, now);
847
+ this.db.prepare(`UPDATE chat_sessions SET updated_at = ?
848
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(now, this.scope.tenantId, this.scope.userId, chatId);
849
+ const row = this.db.prepare(`SELECT * FROM chat_messages WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, id);
670
850
  return this.mapChatMessage(row);
671
851
  }
672
852
  /**
@@ -674,8 +854,8 @@ export class LocalStore {
674
854
  * 不碰 updated_at:这类补丁不是用户可见的会话活动。
675
855
  * metadata 损坏或非对象时从空对象开始合。
676
856
  */
677
- patchMessageMetadata(chatId, messageId, patch) {
678
- const msg = this.getMessage(chatId, messageId);
857
+ async patchMessageMetadata(chatId, messageId, patch) {
858
+ const msg = await this.getMessage(chatId, messageId);
679
859
  if (!msg)
680
860
  return null;
681
861
  let current = {};
@@ -692,9 +872,10 @@ export class LocalStore {
692
872
  }
693
873
  const next = JSON.stringify({ ...current, ...patch });
694
874
  this.db
695
- .prepare(`UPDATE chat_messages SET message_metadata = ? WHERE id = ? AND chat_id = ?`)
696
- .run(next, messageId, chatId);
697
- return this.getMessage(chatId, messageId);
875
+ .prepare(`UPDATE chat_messages SET message_metadata = ?
876
+ WHERE tenant_id = ? AND user_id = ? AND id = ? AND chat_id = ?`)
877
+ .run(next, this.scope.tenantId, this.scope.userId, messageId, chatId);
878
+ return await this.getMessage(chatId, messageId);
698
879
  }
699
880
  /**
700
881
  * W1.2.1: replace the chat's whole message list with a branch projection.
@@ -704,54 +885,58 @@ export class LocalStore {
704
885
  * Timestamps are synthesized (base + index ms) to preserve projection
705
886
  * order under the created_at ordering.
706
887
  */
707
- replaceChatMessages(chatId, messages) {
888
+ async replaceChatMessages(chatId, messages) {
708
889
  const base = Date.now();
709
890
  const insert = this.db.prepare(`
710
- INSERT INTO chat_messages (id, chat_id, role, content, message_metadata, created_at)
711
- VALUES (?, ?, ?, ?, NULL, ?)
891
+ INSERT INTO chat_messages
892
+ (tenant_id, user_id, id, chat_id, role, content, message_metadata, created_at)
893
+ VALUES (?, ?, ?, ?, ?, ?, NULL, ?)
712
894
  `);
713
895
  this.db.transaction(() => {
714
- this.db.prepare(`DELETE FROM chat_messages WHERE chat_id = ?`).run(chatId);
896
+ this.db.prepare(`DELETE FROM chat_messages WHERE tenant_id = ? AND user_id = ? AND chat_id = ?`).run(this.scope.tenantId, this.scope.userId, chatId);
715
897
  messages.forEach((m, i) => {
716
- insert.run(randomUUID(), chatId, m.role, m.content, new Date(base + i).toISOString());
898
+ insert.run(this.scope.tenantId, this.scope.userId, randomUUID(), chatId, m.role, m.content, new Date(base + i).toISOString());
717
899
  });
718
900
  this.db
719
- .prepare(`UPDATE chat_sessions SET updated_at = ? WHERE id = ?`)
720
- .run(new Date(base + messages.length).toISOString(), chatId);
901
+ .prepare(`UPDATE chat_sessions SET updated_at = ?
902
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
903
+ .run(new Date(base + messages.length).toISOString(), this.scope.tenantId, this.scope.userId, chatId);
721
904
  })();
722
905
  }
723
- listChatAgents(includeArchived = false) {
906
+ async listChatAgents(includeArchived = false) {
724
907
  const rows = this.db.prepare(`
725
908
  SELECT *
726
909
  FROM chat_agents
727
- ${includeArchived ? '' : 'WHERE is_archived = 0'}
910
+ WHERE tenant_id = ? AND user_id = ?
911
+ ${includeArchived ? '' : 'AND is_archived = 0'}
728
912
  ORDER BY sort_order ASC, created_at ASC, rowid ASC
729
- `).all();
913
+ `).all(this.scope.tenantId, this.scope.userId);
730
914
  return rows.map(row => this.mapChatAgent(row));
731
915
  }
732
- getChatAgent(agentId) {
733
- const row = this.db.prepare(`SELECT * FROM chat_agents WHERE id = ?`).get(agentId);
916
+ async getChatAgent(agentId) {
917
+ const row = this.db.prepare(`SELECT * FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, agentId);
734
918
  if (!row)
735
919
  return null;
736
920
  return this.mapChatAgent(row);
737
921
  }
738
- createChatAgent(input) {
922
+ async createChatAgent(input) {
739
923
  const id = input.id || randomUUID();
740
924
  const now = new Date().toISOString();
741
925
  this.db.prepare(`
742
926
  INSERT INTO chat_agents (
927
+ tenant_id, user_id,
743
928
  id, slug, name, icon, color, description, role_prompt, forbidden_prompt,
744
929
  skill_ids, tool_policy, allow_external_skills, load_all_skills,
745
930
  is_builtin, is_archived, sort_order, created_at, updated_at
746
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
747
- `).run(id, input.slug ?? null, input.name, input.icon ?? null, input.color ?? null, input.description ?? null, input.rolePrompt ?? null, input.forbiddenPrompt ?? null, JSON.stringify(input.skillIds ?? []), JSON.stringify(input.toolPolicy ?? { mode: 'all', tools: [] }), input.allowExternalSkills ? 1 : 0, input.loadAllSkills ? 1 : 0, input.isBuiltin ? 1 : 0, input.isArchived ? 1 : 0, input.sortOrder ?? 0, now, now);
748
- const created = this.getChatAgent(id);
931
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
932
+ `).run(this.scope.tenantId, this.scope.userId, id, input.slug ?? null, input.name, input.icon ?? null, input.color ?? null, input.description ?? null, input.rolePrompt ?? null, input.forbiddenPrompt ?? null, JSON.stringify(input.skillIds ?? []), JSON.stringify(input.toolPolicy ?? { mode: 'all', tools: [] }), input.allowExternalSkills ? 1 : 0, input.loadAllSkills ? 1 : 0, input.isBuiltin ? 1 : 0, input.isArchived ? 1 : 0, input.sortOrder ?? 0, now, now);
933
+ const created = await this.getChatAgent(id);
749
934
  if (!created)
750
935
  throw new Error('Failed to create chat agent');
751
936
  return created;
752
937
  }
753
- updateChatAgent(agentId, updates) {
754
- const existing = this.getChatAgent(agentId);
938
+ async updateChatAgent(agentId, updates) {
939
+ const existing = await this.getChatAgent(agentId);
755
940
  if (!existing)
756
941
  return null;
757
942
  // 过滤 undefined(同 updateChat):PATCH 路由对"未提供的字段"传 undefined,
@@ -768,74 +953,82 @@ export class LocalStore {
768
953
  SET slug = ?, name = ?, icon = ?, color = ?, description = ?, role_prompt = ?, forbidden_prompt = ?,
769
954
  skill_ids = ?, tool_policy = ?, allow_external_skills = ?, load_all_skills = ?,
770
955
  is_builtin = ?, is_archived = ?, sort_order = ?, updated_at = ?
771
- WHERE id = ?
772
- `).run(merged.slug, merged.name, merged.icon, merged.color, merged.description, merged.rolePrompt, merged.forbiddenPrompt, JSON.stringify(merged.skillIds), JSON.stringify(merged.toolPolicy), merged.allowExternalSkills ? 1 : 0, merged.loadAllSkills ? 1 : 0, merged.isBuiltin ? 1 : 0, merged.isArchived ? 1 : 0, merged.sortOrder, merged.updatedAt, agentId);
773
- return this.getChatAgent(agentId);
956
+ WHERE tenant_id = ? AND user_id = ? AND id = ?
957
+ `).run(merged.slug, merged.name, merged.icon, merged.color, merged.description, merged.rolePrompt, merged.forbiddenPrompt, JSON.stringify(merged.skillIds), JSON.stringify(merged.toolPolicy), merged.allowExternalSkills ? 1 : 0, merged.loadAllSkills ? 1 : 0, merged.isBuiltin ? 1 : 0, merged.isArchived ? 1 : 0, merged.sortOrder, merged.updatedAt, this.scope.tenantId, this.scope.userId, agentId);
958
+ return await this.getChatAgent(agentId);
774
959
  }
775
- archiveChatAgent(agentId) {
776
- const updated = this.updateChatAgent(agentId, { isArchived: true });
960
+ async archiveChatAgent(agentId) {
961
+ const updated = await this.updateChatAgent(agentId, { isArchived: true });
777
962
  return Boolean(updated);
778
963
  }
779
- saveTrace(input) {
964
+ async saveTrace(input) {
780
965
  const now = new Date().toISOString();
781
966
  this.db.prepare(`
782
- INSERT INTO harness_traces (id, chat_id, message_id, started_at_ms, duration_ms, status, payload, created_at)
783
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
784
- `).run(input.id, input.chatId, input.messageId ?? null, input.startedAtMs, input.durationMs ?? null, input.status, JSON.stringify(input.payload), now);
785
- const row = this.db.prepare(`SELECT * FROM harness_traces WHERE id = ?`).get(input.id);
967
+ INSERT INTO harness_traces
968
+ (tenant_id, user_id, id, chat_id, message_id, started_at_ms, duration_ms, status, payload, created_at)
969
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
970
+ `).run(this.scope.tenantId, this.scope.userId, input.id, input.chatId, input.messageId ?? null, input.startedAtMs, input.durationMs ?? null, input.status, JSON.stringify(input.payload), now);
971
+ const row = this.db.prepare(`SELECT * FROM harness_traces WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, input.id);
786
972
  return this.mapHarnessTrace(row);
787
973
  }
788
- listTracesByChat(chatId, limit = 50) {
974
+ async listTracesByChat(chatId, limit = 50) {
789
975
  const rows = this.db.prepare(`
790
976
  SELECT *
791
977
  FROM harness_traces
792
- WHERE chat_id = ?
978
+ WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
793
979
  ORDER BY started_at_ms DESC
794
980
  LIMIT ?
795
- `).all(chatId, LocalStore.clampInt(limit, 50, 1, 500));
981
+ `).all(this.scope.tenantId, this.scope.userId, chatId, SqliteScopedStore.clampInt(limit, 50, 1, 500));
796
982
  return rows.map(row => this.mapHarnessTrace(row));
797
983
  }
798
- getTrace(traceId) {
799
- const row = this.db.prepare(`SELECT * FROM harness_traces WHERE id = ?`).get(traceId);
984
+ async getTrace(traceId) {
985
+ const row = this.db.prepare(`SELECT * FROM harness_traces WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, traceId);
800
986
  if (!row)
801
987
  return null;
802
988
  return this.mapHarnessTrace(row);
803
989
  }
804
990
  // ─── 4.6a 跨 turn 后台任务 ───────────────────────────────────────────
805
- createTask(input) {
991
+ async createTask(input) {
806
992
  const id = randomUUID();
807
993
  const now = new Date().toISOString();
808
994
  this.db.prepare(`
809
995
  INSERT INTO tasks (
996
+ tenant_id, user_id,
810
997
  id, chat_id, task, status, answer, error,
811
998
  worktree_path, worktree_branch, worktree_state, record_id, trace_id,
812
999
  depends_on, created_at, updated_at
813
- ) VALUES (?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)
814
- `).run(id, input.chatId, input.task, input.initialStatus ?? 'running', input.worktreePath ?? null, input.worktreeBranch ?? null,
1000
+ ) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, NULL, ?, ?, ?)
1001
+ `).run(this.scope.tenantId, this.scope.userId, id, input.chatId, input.task, input.initialStatus ?? 'running', input.worktreePath ?? null, input.worktreeBranch ?? null,
815
1002
  // worktree 任务一创建就进入 pending(等待终态后合并/丢弃);
816
1003
  // 非 worktree 任务该列无意义,保持 NULL。
817
1004
  input.worktreePath ? 'pending' : null, input.recordId ?? null, input.dependsOn?.length ? JSON.stringify(input.dependsOn) : null, now, now);
818
- const row = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(id);
1005
+ const row = this.db.prepare(`SELECT * FROM tasks WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, id);
819
1006
  return this.mapTask(row);
820
1007
  }
821
- getTask(taskId) {
822
- const row = this.db.prepare(`SELECT * FROM tasks WHERE id = ?`).get(taskId);
1008
+ async getTask(taskId) {
1009
+ const row = this.db.prepare(`SELECT * FROM tasks WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, taskId);
823
1010
  return row ? this.mapTask(row) : null;
824
1011
  }
825
1012
  /** 按 chat 列任务(updated_at 新→旧);chatId 缺省时列全部(任务面板的全局视图)。 */
826
- listTasks(chatId, limit = 100) {
827
- const safeLimit = LocalStore.clampInt(limit, 100, 1, 500);
1013
+ async listTasks(chatId, limit = 100) {
1014
+ const safeLimit = SqliteScopedStore.clampInt(limit, 100, 1, 500);
828
1015
  const rows = (chatId
829
- ? this.db.prepare(`SELECT * FROM tasks WHERE chat_id = ? ORDER BY datetime(updated_at) DESC LIMIT ?`).all(chatId, safeLimit)
830
- : this.db.prepare(`SELECT * FROM tasks ORDER BY datetime(updated_at) DESC LIMIT ?`).all(safeLimit));
1016
+ ? this.db.prepare(`SELECT * FROM tasks
1017
+ WHERE tenant_id = ? AND user_id = ? AND chat_id = ?
1018
+ ORDER BY datetime(updated_at) DESC LIMIT ?`)
1019
+ .all(this.scope.tenantId, this.scope.userId, chatId, safeLimit)
1020
+ : this.db.prepare(`SELECT * FROM tasks
1021
+ WHERE tenant_id = ? AND user_id = ?
1022
+ ORDER BY datetime(updated_at) DESC LIMIT ?`)
1023
+ .all(this.scope.tenantId, this.scope.userId, safeLimit));
831
1024
  return rows.map(row => this.mapTask(row));
832
1025
  }
833
1026
  /**
834
1027
  * 终态/进度回写。只写传入的字段(undefined 过滤),updated_at 总是刷新。
835
1028
  * 与 updateChat 同约定:显式传 null 才是置空。
836
1029
  */
837
- updateTask(taskId, updates) {
838
- const existing = this.getTask(taskId);
1030
+ async updateTask(taskId, updates) {
1031
+ const existing = await this.getTask(taskId);
839
1032
  if (!existing)
840
1033
  return null;
841
1034
  const clean = Object.fromEntries(Object.entries(updates).filter(([, value]) => value !== undefined));
@@ -847,36 +1040,41 @@ export class LocalStore {
847
1040
  this.db.prepare(`
848
1041
  UPDATE tasks
849
1042
  SET status = ?, answer = ?, error = ?, worktree_state = ?, trace_id = ?, record_id = ?, updated_at = ?
850
- WHERE id = ?
851
- `).run(next.status, next.answer, next.error, next.worktreeState, next.traceId, next.recordId, next.updatedAt, taskId);
852
- return this.getTask(taskId);
1043
+ WHERE tenant_id = ? AND user_id = ? AND id = ?
1044
+ `).run(next.status, next.answer, next.error, next.worktreeState, next.traceId, next.recordId, next.updatedAt, this.scope.tenantId, this.scope.userId, taskId);
1045
+ return await this.getTask(taskId);
853
1046
  }
854
1047
  /**
855
1048
  * 回写推理时间线。故意不碰 updated_at——流过程中的增量不应把任务
856
1049
  * 面板按「最近活动」反复顶到最上。
857
1050
  */
858
- saveTaskProcess(taskId, processJson) {
859
- this.db.prepare(`UPDATE tasks SET process_json = ? WHERE id = ?`).run(processJson, taskId);
1051
+ async saveTaskProcess(taskId, processJson) {
1052
+ this.db.prepare(`UPDATE tasks SET process_json = ?
1053
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
1054
+ .run(processJson, this.scope.tenantId, this.scope.userId, taskId);
860
1055
  }
861
1056
  /**
862
1057
  * 启动清扫:running 任务只可能属于上一个已死的进程(任务流不跨进程
863
1058
  * 存活),全部落成 failed/interrupted——与 W7-1 turn_active 检测器同
864
1059
  * 理,残留 running 是崩溃签名,不是真相。
865
1060
  */
866
- failRunningTasks(reason) {
1061
+ async failRunningTasks(reason) {
867
1062
  const info = this.db
868
- .prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ? WHERE status = 'running'`)
869
- .run(reason, new Date().toISOString());
1063
+ .prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
1064
+ WHERE tenant_id = ? AND user_id = ? AND status = 'running'`)
1065
+ .run(reason, new Date().toISOString(), this.scope.tenantId, this.scope.userId);
870
1066
  // 级联:blocked 任务的点火能力在 TaskService(进程内),不跨进程存活;
871
1067
  // 重启后统一标 failed,模型经 task_status 看到原因后可重跑。
872
1068
  const blocked = this.db
873
- .prepare(`SELECT id FROM tasks WHERE status = 'blocked'`)
874
- .all();
1069
+ .prepare(`SELECT id FROM tasks
1070
+ WHERE tenant_id = ? AND user_id = ? AND status = 'blocked'`)
1071
+ .all(this.scope.tenantId, this.scope.userId);
875
1072
  const now = new Date().toISOString();
876
1073
  for (const row of blocked) {
877
1074
  this.db
878
- .prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ? WHERE id = ?`)
879
- .run(`进程重启中断了编排等待(依赖任务已随进程终止);请重新 task_run。`, now, row.id);
1075
+ .prepare(`UPDATE tasks SET status = 'failed', error = ?, updated_at = ?
1076
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
1077
+ .run(`进程重启中断了编排等待(依赖任务已随进程终止);请重新 task_run。`, now, this.scope.tenantId, this.scope.userId, row.id);
880
1078
  }
881
1079
  return info.changes + blocked.length;
882
1080
  }
@@ -885,19 +1083,19 @@ export class LocalStore {
885
1083
  * 记录一条用量事件(一轮 chat / 一次标题生成 / 一次摘要压缩等)。
886
1084
  * `costUsd` 为 null 表示该模型无单价(本地/未知)——存 NULL,面板渲染为 "—"。
887
1085
  */
888
- recordUsageEvent(input) {
1086
+ async recordUsageEvent(input) {
889
1087
  this.db.prepare(`
890
1088
  INSERT INTO usage_events
891
- (id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at)
892
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
893
- `).run(randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString());
1089
+ (tenant_id, user_id, id, chat_id, kind, provider, model, prompt_tokens, completion_tokens, total_tokens, cached_prompt_tokens, cost_usd, created_at)
1090
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1091
+ `).run(this.scope.tenantId, this.scope.userId, randomUUID(), input.chatId ?? null, input.kind, input.provider ?? null, input.model ?? null, Math.max(0, Math.floor(input.promptTokens)), Math.max(0, Math.floor(input.completionTokens)), Math.max(0, Math.floor(input.totalTokens)), Math.max(0, Math.floor(input.cachedPromptTokens ?? 0)), input.costUsd ?? null, new Date().toISOString());
894
1092
  }
895
1093
  /**
896
1094
  * 聚合最近 `sinceDays` 天的用量:按 model 分桶( token 合计 + 成本合计),
897
1095
  * 外加总计。成本只统计有单价的模型(cost_usd 非 NULL);无单价模型的
898
1096
  * token 照常计入,但成本列不计。返回按 totalTokens 降序。
899
1097
  */
900
- getUsageSummary(sinceDays = 30) {
1098
+ async getUsageSummary(sinceDays = 30) {
901
1099
  const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000).toISOString();
902
1100
  const rows = this.db.prepare(`
903
1101
  SELECT
@@ -910,104 +1108,92 @@ export class LocalStore {
910
1108
  SUM(cached_prompt_tokens) AS cached_prompt_tokens,
911
1109
  SUM(cost_usd) AS cost_usd
912
1110
  FROM usage_events
913
- WHERE created_at >= ?
1111
+ WHERE tenant_id = ? AND user_id = ? AND created_at >= ?
914
1112
  GROUP BY model, provider
915
1113
  ORDER BY total_tokens DESC
916
- `).all(since);
1114
+ `).all(this.scope.tenantId, this.scope.userId, since);
917
1115
  return buildUsageSummary(rows, sinceDays);
918
1116
  }
919
- getLlmSettings() {
920
- const row = this.db.prepare(`SELECT value FROM settings_kv WHERE key = 'llm_settings'`).get();
921
- if (!row)
1117
+ async getLlmSettings() {
1118
+ const value = this.getSetting('llm_settings');
1119
+ if (!value)
922
1120
  return null;
923
1121
  try {
924
- return JSON.parse(row.value);
1122
+ return JSON.parse(value);
925
1123
  }
926
1124
  catch {
927
1125
  return null;
928
1126
  }
929
1127
  }
930
- setLlmSettings(settings) {
1128
+ async setLlmSettings(settings) {
931
1129
  const merged = mergeLlmSettings(settings);
932
- this.db.prepare(`
933
- INSERT INTO settings_kv (key, value) VALUES ('llm_settings', ?)
934
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
935
- `).run(JSON.stringify(merged));
1130
+ this.setSetting('llm_settings', JSON.stringify(merged));
936
1131
  return merged;
937
1132
  }
938
1133
  // ─── W6-6 遥测(OTLP collector + 隐私模式)────────────────────────────
939
1134
  /** 读取遥测设置;从未配置过时返回 null(调用方按"关"处理)。 */
940
- getTelemetrySettings() {
941
- const row = this.db.prepare(`SELECT value FROM settings_kv WHERE key = 'telemetry_settings'`).get();
942
- if (!row)
1135
+ async getTelemetrySettings() {
1136
+ const value = this.getSetting('telemetry_settings');
1137
+ if (!value)
943
1138
  return null;
944
1139
  try {
945
- return mergeTelemetrySettings(JSON.parse(row.value));
1140
+ return mergeTelemetrySettings(JSON.parse(value));
946
1141
  }
947
1142
  catch {
948
1143
  return null;
949
1144
  }
950
1145
  }
951
- setTelemetrySettings(settings) {
1146
+ async setTelemetrySettings(settings) {
952
1147
  const merged = mergeTelemetrySettings(settings);
953
- this.db.prepare(`
954
- INSERT INTO settings_kv (key, value) VALUES ('telemetry_settings', ?)
955
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
956
- `).run(JSON.stringify(merged));
1148
+ this.setSetting('telemetry_settings', JSON.stringify(merged));
957
1149
  return merged;
958
1150
  }
959
- getWebSearchSettings() {
960
- const row = this.db.prepare(`SELECT value FROM settings_kv WHERE key = 'web_search_settings'`).get();
961
- if (!row)
1151
+ async getWebSearchSettings() {
1152
+ const value = this.getSetting('web_search_settings');
1153
+ if (!value)
962
1154
  return null;
963
1155
  try {
964
- return mergeWebSearchSettings(JSON.parse(row.value));
1156
+ return mergeWebSearchSettings(JSON.parse(value));
965
1157
  }
966
1158
  catch {
967
1159
  return null;
968
1160
  }
969
1161
  }
970
- setWebSearchSettings(settings) {
1162
+ async setWebSearchSettings(settings) {
971
1163
  const merged = mergeWebSearchSettings(settings);
972
- this.db.prepare(`
973
- INSERT INTO settings_kv (key, value) VALUES ('web_search_settings', ?)
974
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
975
- `).run(JSON.stringify(merged));
1164
+ this.setSetting('web_search_settings', JSON.stringify(merged));
976
1165
  return merged;
977
1166
  }
978
1167
  // ─── 帮助改进产品(行为 / 对话 / 资料 分项同意 + 本地队列)──────────
979
- getInsightsSettings() {
980
- const row = this.db.prepare(`SELECT value FROM settings_kv WHERE key = 'insights_settings'`).get();
981
- if (!row)
1168
+ async getInsightsSettings() {
1169
+ const value = this.getSetting('insights_settings');
1170
+ if (!value)
982
1171
  return null;
983
1172
  try {
984
- return mergeInsightsSettings(JSON.parse(row.value));
1173
+ return mergeInsightsSettings(JSON.parse(value));
985
1174
  }
986
1175
  catch {
987
1176
  return null;
988
1177
  }
989
1178
  }
990
- ensureInsightsSettings() {
991
- const existing = this.getInsightsSettings();
1179
+ async ensureInsightsSettings() {
1180
+ const existing = await this.getInsightsSettings();
992
1181
  if (existing)
993
1182
  return existing;
994
- return this.setInsightsSettings({});
1183
+ return await this.setInsightsSettings({});
995
1184
  }
996
- setInsightsSettings(settings) {
997
- const current = this.getInsightsSettings();
1185
+ async setInsightsSettings(settings) {
1186
+ const current = await this.getInsightsSettings();
998
1187
  const merged = mergeInsightsSettings({
999
1188
  ...current,
1000
1189
  ...settings,
1001
1190
  installId: current?.installId ?? settings.installId,
1002
1191
  profile: mergeInsightsProfile({ ...current?.profile, ...settings.profile }),
1003
1192
  });
1004
- this.db.prepare(`
1005
- INSERT INTO settings_kv (key, value) VALUES ('insights_settings', ?)
1006
- ON CONFLICT(key) DO UPDATE SET value = excluded.value
1007
- `).run(JSON.stringify(merged));
1193
+ this.setSetting('insights_settings', JSON.stringify(merged));
1008
1194
  return merged;
1009
1195
  }
1010
- enqueueInsight(kind, payload) {
1196
+ async enqueueInsight(kind, payload) {
1011
1197
  const row = {
1012
1198
  id: randomUUID(),
1013
1199
  kind,
@@ -1017,16 +1203,17 @@ export class LocalStore {
1017
1203
  uploadError: null,
1018
1204
  };
1019
1205
  this.db.prepare(`
1020
- INSERT INTO insights_outbox (id, kind, payload, created_at, uploaded_at, upload_error)
1021
- VALUES (?, ?, ?, ?, NULL, NULL)
1022
- `).run(row.id, row.kind, JSON.stringify(row.payload), row.createdAt);
1206
+ INSERT INTO insights_outbox
1207
+ (tenant_id, user_id, id, kind, payload, created_at, uploaded_at, upload_error)
1208
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL)
1209
+ `).run(this.scope.tenantId, this.scope.userId, row.id, row.kind, JSON.stringify(row.payload), row.createdAt);
1023
1210
  return row;
1024
1211
  }
1025
- listInsightOutbox(opts = {}) {
1212
+ async listInsightOutbox(opts = {}) {
1026
1213
  const limit = Math.max(1, Math.min(opts.limit ?? 200, 500));
1027
1214
  let sql = `SELECT * FROM insights_outbox`;
1028
- const where = [];
1029
- const params = [];
1215
+ const where = ['tenant_id = ?', 'user_id = ?'];
1216
+ const params = [this.scope.tenantId, this.scope.userId];
1030
1217
  if (opts.kind) {
1031
1218
  where.push('kind = ?');
1032
1219
  params.push(opts.kind);
@@ -1037,20 +1224,22 @@ export class LocalStore {
1037
1224
  else if (opts.uploaded === false) {
1038
1225
  where.push('uploaded_at IS NULL');
1039
1226
  }
1040
- if (where.length)
1041
- sql += ` WHERE ${where.join(' AND ')}`;
1227
+ sql += ` WHERE ${where.join(' AND ')}`;
1042
1228
  sql += ` ORDER BY created_at DESC LIMIT ?`;
1043
1229
  params.push(limit);
1044
1230
  const rows = this.db.prepare(sql).all(...params);
1045
1231
  return rows.map((row) => this.mapInsightOutbox(row));
1046
1232
  }
1047
- markInsightUploaded(id) {
1048
- this.db.prepare(`UPDATE insights_outbox SET uploaded_at = ?, upload_error = NULL WHERE id = ?`).run(new Date().toISOString(), id);
1233
+ async markInsightUploaded(id) {
1234
+ this.db.prepare(`UPDATE insights_outbox SET uploaded_at = ?, upload_error = NULL
1235
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`).run(new Date().toISOString(), this.scope.tenantId, this.scope.userId, id);
1049
1236
  }
1050
- markInsightUploadError(id, error) {
1051
- this.db.prepare(`UPDATE insights_outbox SET upload_error = ? WHERE id = ?`).run(error.slice(0, 200), id);
1237
+ async markInsightUploadError(id, error) {
1238
+ this.db.prepare(`UPDATE insights_outbox SET upload_error = ?
1239
+ WHERE tenant_id = ? AND user_id = ? AND id = ?`)
1240
+ .run(error.slice(0, 200), this.scope.tenantId, this.scope.userId, id);
1052
1241
  }
1053
- insightStats() {
1242
+ async insightStats() {
1054
1243
  const row = this.db.prepare(`
1055
1244
  SELECT
1056
1245
  SUM(CASE WHEN kind = 'event' THEN 1 ELSE 0 END) AS events,
@@ -1058,7 +1247,8 @@ export class LocalStore {
1058
1247
  SUM(CASE WHEN kind = 'profile' THEN 1 ELSE 0 END) AS profile,
1059
1248
  SUM(CASE WHEN uploaded_at IS NULL THEN 1 ELSE 0 END) AS pending
1060
1249
  FROM insights_outbox
1061
- `).get();
1250
+ WHERE tenant_id = ? AND user_id = ?
1251
+ `).get(this.scope.tenantId, this.scope.userId);
1062
1252
  return {
1063
1253
  events: Number(row?.events ?? 0),
1064
1254
  turns: Number(row?.turns ?? 0),
@@ -1066,8 +1256,8 @@ export class LocalStore {
1066
1256
  pending: Number(row?.pending ?? 0),
1067
1257
  };
1068
1258
  }
1069
- exportInsightsBundle() {
1070
- const settings = this.ensureInsightsSettings();
1259
+ async exportInsightsBundle() {
1260
+ const settings = await this.ensureInsightsSettings();
1071
1261
  return {
1072
1262
  schema: INSIGHTS_EXPORT_SCHEMA,
1073
1263
  exportedAt: new Date().toISOString(),
@@ -1078,8 +1268,8 @@ export class LocalStore {
1078
1268
  shareProfile: settings.shareProfile,
1079
1269
  },
1080
1270
  profile: settings.profile,
1081
- stats: this.insightStats(),
1082
- records: this.listInsightOutbox({ limit: 500 }),
1271
+ stats: await this.insightStats(),
1272
+ records: await this.listInsightOutbox({ limit: 500 }),
1083
1273
  };
1084
1274
  }
1085
1275
  mapInsightOutbox(row) {
@@ -1094,7 +1284,7 @@ export class LocalStore {
1094
1284
  }
1095
1285
  // ─── 场景包存储 ─────────────────────────────────────────────────
1096
1286
  // 包数据访问收敛进包内存储(0.4,如 packages/pack-*/src/store.ts)。
1097
- // 宿主只通过 getPackDb() 把同一个 SQLite 句柄借给包存储。
1287
+ // Scenario packs receive driver-neutral SQL access; raw handles stay here.
1098
1288
  mapChatSession(row) {
1099
1289
  return {
1100
1290
  id: String(row.id),
@@ -1171,6 +1361,19 @@ export class LocalStore {
1171
1361
  updatedAt: String(row.updated_at),
1172
1362
  };
1173
1363
  }
1364
+ getSetting(key) {
1365
+ const row = this.db.prepare(`SELECT value FROM settings_kv
1366
+ WHERE tenant_id = ? AND user_id = ? AND key = ?`).get(this.scope.tenantId, this.scope.userId, key);
1367
+ return row?.value ?? null;
1368
+ }
1369
+ setSetting(key, value) {
1370
+ this.db.prepare(`
1371
+ INSERT INTO settings_kv (tenant_id, user_id, key, value)
1372
+ VALUES (?, ?, ?, ?)
1373
+ ON CONFLICT(tenant_id, user_id, key)
1374
+ DO UPDATE SET value = excluded.value
1375
+ `).run(this.scope.tenantId, this.scope.userId, key, value);
1376
+ }
1174
1377
  safeJson(value, fallback = null) {
1175
1378
  if (typeof value !== 'string')
1176
1379
  return fallback;
@@ -1182,4 +1385,6 @@ export class LocalStore {
1182
1385
  }
1183
1386
  }
1184
1387
  }
1185
- export const localStore = createLocalStore(() => new LocalStore());
1388
+ export { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, selectStorageDriver, } from './driver.js';
1389
+ export { registerStorageDriver } from './driver.js';
1390
+ export { SqliteStorageDriver } from './sqlite-driver.js';