@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,6 +1,7 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import path from 'node:path';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
5
  import os from 'node:os';
5
6
  import { getAppRootDir, shellOpenPath } from '../runtime.js';
6
7
  import { llmService, getSidecarSupervisor, whenSidecarSupervisor } from '../llm/index.js';
@@ -10,7 +11,7 @@ import { driveWithAutoContinue, resolveAutoContinueMax, } from './auto-continue-
10
11
  import { builtinSubagentParam } from './subagent-profiles.js';
11
12
  import { appendTimelineDelta, syncTimelineTools, } from './turn-timeline.js';
12
13
  import { turnDurationMs } from './turn-duration.js';
13
- import { localStore, DEFAULT_SYSTEM_PROMPT, telemetryEnabled } from '../storage/index.js';
14
+ import { DEFAULT_SYSTEM_PROMPT, telemetryEnabled } from '../storage/index.js';
14
15
  import { isSkillPinned, isToolAllowed, mergeAgentCapabilities, normalizeToolPolicy, resolveSkillExcludes, } from './agent-capability.js';
15
16
  import { getBrand } from '../brand.js';
16
17
  import { recordInsightEvent, recordInsightTurn, recordInsightProfile } from '../insights/record.js';
@@ -21,7 +22,7 @@ import { allowEgressForBaseUrl, getActiveEgressBroker, getEgressPosture } from '
21
22
  import { buildExecSandbox, parseExecPolicy } from '../sidecar/exec-sandbox.js';
22
23
  import { SidecarSupervisor } from '../sidecar/index.js';
23
24
  import { diagnoseLlmConnection } from './llm-diagnose.js';
24
- import { buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
25
+ import { brandSkillVars, buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
25
26
  import { parseUserMessageTriggers } from './message-triggers.js';
26
27
  import { findSkill, loadSkills, classifySkillOrigin, listSkillRoots } from './skill-loader.js';
27
28
  import { installSkillFromDirectory } from './skill-install.js';
@@ -40,6 +41,7 @@ import { beginPackTurnObservers, collectPackExecWritableRoots, collectPackForced
40
41
  import { matchPackBackendRoute } from './pack-backend-routes.js';
41
42
  import { collectTurnFiles } from './turn-files.js';
42
43
  import { resolveMentionedPaths } from './mentioned-paths.js';
44
+ import { getAuthProvider } from '../auth/index.js';
43
45
  /**
44
46
  * 把 CoreLoop/LLM 的底层错误翻译成适合直接显示在助手消息里的中文提示。
45
47
  * 原始错误仍保存在 messageMetadata.completionReason 里,便于排查。
@@ -62,7 +64,7 @@ export function userFacingCoreLoopFailure(reason) {
62
64
  /**
63
65
  * Parses a pagination-style query param (`?page=`, `?limit=`) into a finite
64
66
  * positive integer, falling back to `fallback` for missing/non-numeric input
65
- * (e.g. `?page=abc`) instead of letting `NaN` leak into `localStore` queries
67
+ * (e.g. `?page=abc`) instead of letting `NaN` leak into storage queries
66
68
  * and the echoed-back `pagination` block of the response.
67
69
  */
68
70
  function parsePositiveIntParam(raw, fallback) {
@@ -148,19 +150,44 @@ function buildLocalAgent() {
148
150
  }
149
151
  export class LocalBackendRouter {
150
152
  toolRouter;
153
+ defaultStore;
154
+ storeContext = new AsyncLocalStorage();
155
+ resolveStore;
151
156
  broadcast;
152
157
  /** 4.6a/4.6c:任务面板的路由入口(列表 + 合并/丢弃)。未注入时任务路由 503。 */
153
158
  taskService;
154
- constructor(toolRouter, options = {}) {
159
+ constructor(toolRouter, options) {
155
160
  this.toolRouter = toolRouter;
161
+ this.defaultStore = options.store;
162
+ this.resolveStore = options.resolveStore ?? (() => options.store);
156
163
  this.broadcast = options.broadcast ?? null;
157
164
  this.taskService = options.taskService ?? null;
158
165
  }
159
- async handle(request) {
166
+ get store() {
167
+ return this.storeContext.getStore() ?? this.defaultStore;
168
+ }
169
+ /** Store selected for the active request, or the host default outside one. */
170
+ get activeStore() {
171
+ return this.store;
172
+ }
173
+ handle(request) {
174
+ return this.storeContext.run(this.resolveStore(request.principal), () => this.handleScoped(request));
175
+ }
176
+ async handleScoped(request) {
160
177
  const { method } = request;
161
178
  const url = new URL(request.path, 'http://local.backend');
162
179
  const pathname = url.pathname;
163
180
  if (method === 'GET' && pathname === '/api/v2/auth/me') {
181
+ const authProvider = getAuthProvider();
182
+ if (authProvider) {
183
+ if (!request.principal) {
184
+ return { status: 401, data: { detail: 'unauthorized' } };
185
+ }
186
+ return {
187
+ status: 200,
188
+ data: await authProvider.describeSelf(request.principal),
189
+ };
190
+ }
164
191
  return { status: 200, data: buildLocalApiUser() };
165
192
  }
166
193
  // Desktop Agent —— 在本地模式下虚拟一个永远在线的 agent,所有 exec 直接走 ToolRouter
@@ -218,7 +245,7 @@ export class LocalBackendRouter {
218
245
  if (method === 'GET' && pathname === '/api/v2/chats') {
219
246
  const page = parsePositiveIntParam(url.searchParams.get('page'), 1);
220
247
  const limit = parsePositiveIntParam(url.searchParams.get('limit'), 50);
221
- const { chats, total } = localStore.listChats(page, limit);
248
+ const { chats, total } = await this.store.listChats(page, limit);
222
249
  return {
223
250
  status: 200,
224
251
  data: {
@@ -250,10 +277,10 @@ export class LocalBackendRouter {
250
277
  const branchesMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches$/);
251
278
  if (method === 'GET' && branchesMatch) {
252
279
  const chatId = branchesMatch[1];
253
- const chat = localStore.getChat(chatId);
280
+ const chat = await this.store.getChat(chatId);
254
281
  if (!chat)
255
282
  return { status: 404, data: { detail: 'chat not found' } };
256
- const activeRecordId = localStore.getChatRecordId(chatId) ?? chatId;
283
+ const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
257
284
  const supervisor = getSidecarSupervisor();
258
285
  const branches = supervisor ? await supervisor.sessionBranches(activeRecordId) : null;
259
286
  return {
@@ -271,10 +298,10 @@ export class LocalBackendRouter {
271
298
  const branchTreeMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches\/tree$/);
272
299
  if (method === 'GET' && branchTreeMatch) {
273
300
  const chatId = branchTreeMatch[1];
274
- const chat = localStore.getChat(chatId);
301
+ const chat = await this.store.getChat(chatId);
275
302
  if (!chat)
276
303
  return { status: 404, data: { detail: 'chat not found' } };
277
- const activeRecordId = localStore.getChatRecordId(chatId) ?? chatId;
304
+ const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
278
305
  const supervisor = getSidecarSupervisor();
279
306
  const family = supervisor ? await supervisor.sessionTree(activeRecordId) : null;
280
307
  return {
@@ -295,7 +322,7 @@ export class LocalBackendRouter {
295
322
  const activateMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/branches\/activate$/);
296
323
  if (method === 'POST' && activateMatch) {
297
324
  const chatId = activateMatch[1];
298
- const chat = localStore.getChat(chatId);
325
+ const chat = await this.store.getChat(chatId);
299
326
  if (!chat)
300
327
  return { status: 404, data: { detail: 'chat not found' } };
301
328
  const payload = this.toRecord(request.body);
@@ -307,7 +334,7 @@ export class LocalBackendRouter {
307
334
  if (!supervisor) {
308
335
  return { status: 503, data: { detail: 'sidecar unavailable' } };
309
336
  }
310
- const activeRecordId = localStore.getChatRecordId(chatId) ?? chatId;
337
+ const activeRecordId = await this.store.getChatRecordId(chatId) ?? chatId;
311
338
  const family = await supervisor.sessionTree(activeRecordId);
312
339
  const activation = resolveBranchActivation(activeRecordId, family?.tree ?? null, targetRecordId);
313
340
  if (!activation.ok) {
@@ -317,8 +344,8 @@ export class LocalBackendRouter {
317
344
  if (!projection) {
318
345
  return { status: 404, data: { detail: `record not found: ${targetRecordId}` } };
319
346
  }
320
- localStore.setChatRecordId(chatId, targetRecordId);
321
- localStore.replaceChatMessages(chatId, projection.messages
347
+ await this.store.setChatRecordId(chatId, targetRecordId);
348
+ await this.store.replaceChatMessages(chatId, projection.messages
322
349
  .filter((m) => m.role === 'user' || m.role === 'assistant')
323
350
  .map((m) => ({ role: m.role, content: m.content })));
324
351
  return {
@@ -342,7 +369,7 @@ export class LocalBackendRouter {
342
369
  };
343
370
  }
344
371
  }
345
- const created = localStore.createChat('新对话', typeof payload.agentId === 'string' ? payload.agentId : getBrand().defaultAgentId, projectId);
372
+ const created = await this.store.createChat('新对话', typeof payload.agentId === 'string' ? payload.agentId : getBrand().defaultAgentId, projectId);
346
373
  return {
347
374
  status: 200,
348
375
  data: {
@@ -362,7 +389,7 @@ export class LocalBackendRouter {
362
389
  const exceptChatId = typeof payload.exceptChatId === 'string' && payload.exceptChatId.trim()
363
390
  ? payload.exceptChatId.trim()
364
391
  : null;
365
- const deletedChatIds = localStore.deleteEmptyChats(exceptChatId);
392
+ const deletedChatIds = await this.store.deleteEmptyChats(exceptChatId);
366
393
  return { status: 200, data: { deletedChatIds } };
367
394
  }
368
395
  // ───── 项目模式:projects CRUD ─────
@@ -422,7 +449,7 @@ export class LocalBackendRouter {
422
449
  if (!ok)
423
450
  return this.notFound('项目不存在');
424
451
  // 会话不删,只解绑——它们变成"无项目对话"。
425
- const detached = localStore.clearProjectAssignment(projectId);
452
+ const detached = await this.store.clearProjectAssignment(projectId);
426
453
  return { status: 200, data: { success: true, detachedChats: detached } };
427
454
  }
428
455
  }
@@ -458,17 +485,17 @@ export class LocalBackendRouter {
458
485
  return { status: 503, data: { detail: 'task service unavailable' } };
459
486
  }
460
487
  const chatId = chatTasksMatch[1];
461
- const chat = localStore.getChat(chatId);
488
+ const chat = await this.store.getChat(chatId);
462
489
  if (!chat)
463
490
  return { status: 404, data: { detail: 'chat not found' } };
464
- return { status: 200, data: { tasks: localStore.listTasks(chatId) } };
491
+ return { status: 200, data: { tasks: await this.store.listTasks(chatId) } };
465
492
  }
466
493
  const taskProcessMatch = pathname.match(/^\/api\/v2\/tasks\/([^/]+)\/process$/);
467
494
  if (method === 'GET' && taskProcessMatch) {
468
495
  if (!this.taskService) {
469
496
  return { status: 503, data: { detail: 'task service unavailable' } };
470
497
  }
471
- const snapshot = this.taskService.getProcess(taskProcessMatch[1]);
498
+ const snapshot = await this.taskService.getProcess(taskProcessMatch[1]);
472
499
  if (!snapshot)
473
500
  return { status: 404, data: { detail: '任务不存在' } };
474
501
  return {
@@ -506,7 +533,7 @@ export class LocalBackendRouter {
506
533
  const projectContextMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/project-context$/);
507
534
  if (projectContextMatch && method === 'GET') {
508
535
  const chatId = projectContextMatch[1];
509
- const chat = localStore.getChat(chatId);
536
+ const chat = await this.store.getChat(chatId);
510
537
  if (!chat)
511
538
  return this.notFound('Chat not found');
512
539
  const registry = this.toolRouter.projectRegistry;
@@ -531,7 +558,7 @@ export class LocalBackendRouter {
531
558
  if (chatMatch) {
532
559
  const chatId = chatMatch[1];
533
560
  if (method === 'GET') {
534
- const chat = localStore.getChat(chatId);
561
+ const chat = await this.store.getChat(chatId);
535
562
  if (!chat)
536
563
  return this.notFound('Chat not found');
537
564
  return {
@@ -552,13 +579,13 @@ export class LocalBackendRouter {
552
579
  }
553
580
  if (method === 'DELETE') {
554
581
  if (url.searchParams.get('onlyIfEmpty') === '1') {
555
- const deleted = localStore.deleteChatIfEmpty(chatId);
582
+ const deleted = await this.store.deleteChatIfEmpty(chatId);
556
583
  return {
557
584
  status: 200,
558
585
  data: { success: true, deleted, chatId },
559
586
  };
560
587
  }
561
- const ok = localStore.deleteChat(chatId);
588
+ const ok = await this.store.deleteChat(chatId);
562
589
  if (!ok)
563
590
  return this.notFound('Chat not found');
564
591
  return {
@@ -575,7 +602,7 @@ export class LocalBackendRouter {
575
602
  if (chatPinMatch && method === 'PUT') {
576
603
  const chatId = chatPinMatch[1];
577
604
  const payload = this.toRecord(request.body);
578
- const updated = localStore.updateChat(chatId, { isPinned: Boolean(payload.isPinned) });
605
+ const updated = await this.store.updateChat(chatId, { isPinned: Boolean(payload.isPinned) });
579
606
  if (!updated)
580
607
  return this.notFound('Chat not found');
581
608
  return {
@@ -610,7 +637,7 @@ export class LocalBackendRouter {
610
637
  return this.badRequest('projectId 必须是项目 id 字符串或 null');
611
638
  }
612
639
  }
613
- const updated = localStore.updateChat(chatId, {
640
+ const updated = await this.store.updateChat(chatId, {
614
641
  title: typeof payload.title === 'string' ? payload.title : undefined,
615
642
  systemPrompt: typeof payload.systemPrompt === 'string' ? payload.systemPrompt : undefined,
616
643
  pinnedRefs: Array.isArray(payload.pinnedRefs) ? payload.pinnedRefs : undefined,
@@ -638,7 +665,7 @@ export class LocalBackendRouter {
638
665
  if (messageListMatch && method === 'GET') {
639
666
  const chatId = messageListMatch[1];
640
667
  const limit = parsePositiveIntParam(url.searchParams.get('limit'), 200);
641
- const records = localStore.listMessages(chatId, limit);
668
+ const records = await this.store.listMessages(chatId, limit);
642
669
  const messages = records.map(item => ({
643
670
  id: item.id,
644
671
  chatId: item.chatId,
@@ -655,7 +682,7 @@ export class LocalBackendRouter {
655
682
  // 同一 record 上开出第二个写者)。listMessages 是 DESC,records[0]
656
683
  // 即最后一条。
657
684
  const interrupted = detectInterruptedTurn({
658
- turnActive: localStore.getTurnActive(chatId) !== null,
685
+ turnActive: await this.store.getTurnActive(chatId) !== null,
659
686
  streamActive: getActiveCoreLoopStreamId(chatId) !== undefined,
660
687
  lastMessageRole: records[0]?.role ?? null,
661
688
  });
@@ -711,7 +738,7 @@ export class LocalBackendRouter {
711
738
  }
712
739
  if (method === 'GET' && pathname === '/api/v2/chat-agents') {
713
740
  const includeArchived = url.searchParams.get('include_archived') === 'true';
714
- const agents = localStore.listChatAgents(includeArchived);
741
+ const agents = await this.store.listChatAgents(includeArchived);
715
742
  return {
716
743
  status: 200,
717
744
  data: {
@@ -722,7 +749,7 @@ export class LocalBackendRouter {
722
749
  }
723
750
  if (method === 'POST' && pathname === '/api/v2/chat-agents') {
724
751
  const payload = this.toRecord(request.body);
725
- const agent = localStore.createChatAgent({
752
+ const agent = await this.store.createChatAgent({
726
753
  name: String(payload.name || '新助手'),
727
754
  icon: typeof payload.icon === 'string' ? payload.icon : null,
728
755
  color: typeof payload.color === 'string' ? payload.color : null,
@@ -1147,14 +1174,14 @@ export class LocalBackendRouter {
1147
1174
  if (chatAgentMatch) {
1148
1175
  const agentId = chatAgentMatch[1];
1149
1176
  if (method === 'GET') {
1150
- const agent = localStore.getChatAgent(agentId);
1177
+ const agent = await this.store.getChatAgent(agentId);
1151
1178
  if (!agent)
1152
1179
  return this.notFound('Agent not found');
1153
1180
  return { status: 200, data: agent };
1154
1181
  }
1155
1182
  if (method === 'PATCH') {
1156
1183
  const payload = this.toRecord(request.body);
1157
- const agent = localStore.updateChatAgent(agentId, {
1184
+ const agent = await this.store.updateChatAgent(agentId, {
1158
1185
  name: typeof payload.name === 'string' ? payload.name : undefined,
1159
1186
  icon: typeof payload.icon === 'string' ? payload.icon : undefined,
1160
1187
  color: typeof payload.color === 'string' ? payload.color : undefined,
@@ -1173,7 +1200,7 @@ export class LocalBackendRouter {
1173
1200
  return { status: 200, data: { agent } };
1174
1201
  }
1175
1202
  if (method === 'DELETE') {
1176
- const ok = localStore.archiveChatAgent(agentId);
1203
+ const ok = await this.store.archiveChatAgent(agentId);
1177
1204
  if (!ok)
1178
1205
  return this.notFound('Agent not found');
1179
1206
  return {
@@ -1376,12 +1403,12 @@ export class LocalBackendRouter {
1376
1403
  if (method === 'GET') {
1377
1404
  return {
1378
1405
  status: 200,
1379
- data: localStore.getLlmSettings(),
1406
+ data: await this.store.getLlmSettings(),
1380
1407
  };
1381
1408
  }
1382
1409
  if (method === 'POST') {
1383
1410
  const payload = this.toRecord(request.body);
1384
- const saved = llmService.setSettings({
1411
+ const saved = await llmService.setSettings({
1385
1412
  provider: sanitizeLlmProvider(payload.provider),
1386
1413
  vendorId: sanitizeVendorId(payload.vendorId),
1387
1414
  model: String(payload.model || 'llama3.1:8b'),
@@ -1401,7 +1428,7 @@ export class LocalBackendRouter {
1401
1428
  : undefined,
1402
1429
  compat: sanitizeCompatOverrides(payload.compat),
1403
1430
  presets: sanitizePresetsChoice(payload.presets),
1404
- });
1431
+ }, this.store);
1405
1432
  // 立即让新默认超时对后续 local_exec_shell 生效(重启后由 main.ts 启动时恢复)。
1406
1433
  setDefaultExecTimeoutMs(saved.execTimeoutSeconds ? saved.execTimeoutSeconds * 1000 : null);
1407
1434
  // 代理白名单是 boot 时按当时的 baseUrl 派生的,换了网关后本会话(含
@@ -1421,12 +1448,12 @@ export class LocalBackendRouter {
1421
1448
  if (method === 'GET') {
1422
1449
  return {
1423
1450
  status: 200,
1424
- data: localStore.getTelemetrySettings(),
1451
+ data: await this.store.getTelemetrySettings(),
1425
1452
  };
1426
1453
  }
1427
1454
  if (method === 'POST') {
1428
1455
  const payload = this.toRecord(request.body);
1429
- const saved = localStore.setTelemetrySettings({
1456
+ const saved = await this.store.setTelemetrySettings({
1430
1457
  endpoint: typeof payload.endpoint === 'string' ? payload.endpoint : undefined,
1431
1458
  privacyMode: payload.privacyMode === 'full' ? 'full' : 'metadata',
1432
1459
  serviceName: typeof payload.serviceName === 'string' ? payload.serviceName : undefined,
@@ -1441,12 +1468,12 @@ export class LocalBackendRouter {
1441
1468
  if (method === 'GET') {
1442
1469
  return {
1443
1470
  status: 200,
1444
- data: localStore.getWebSearchSettings(),
1471
+ data: await this.store.getWebSearchSettings(),
1445
1472
  };
1446
1473
  }
1447
1474
  if (method === 'POST') {
1448
1475
  const payload = this.toRecord(request.body);
1449
- const saved = localStore.setWebSearchSettings({
1476
+ const saved = await this.store.setWebSearchSettings({
1450
1477
  provider: payload.provider === 'ddg' ? 'ddg' : 'tavily',
1451
1478
  apiKey: typeof payload.apiKey === 'string' ? payload.apiKey : undefined,
1452
1479
  });
@@ -1458,10 +1485,10 @@ export class LocalBackendRouter {
1458
1485
  }
1459
1486
  if (pathname === '/api/v2/local-settings/insights') {
1460
1487
  if (method === 'GET') {
1461
- const settings = localStore.ensureInsightsSettings();
1488
+ const settings = await this.store.ensureInsightsSettings();
1462
1489
  return {
1463
1490
  status: 200,
1464
- data: { ...settings, stats: localStore.insightStats() },
1491
+ data: { ...settings, stats: await this.store.insightStats() },
1465
1492
  };
1466
1493
  }
1467
1494
  if (method === 'POST') {
@@ -1490,20 +1517,20 @@ export class LocalBackendRouter {
1490
1517
  patch.apiBase = payload.apiBase;
1491
1518
  if (Object.keys(profilePatch).length)
1492
1519
  patch.profile = profilePatch;
1493
- const saved = localStore.setInsightsSettings(patch);
1520
+ const saved = await this.store.setInsightsSettings(patch);
1494
1521
  const hasProfileFields = Boolean(saved.profile.displayName ||
1495
1522
  saved.profile.email ||
1496
1523
  saved.profile.company ||
1497
1524
  saved.profile.note);
1498
1525
  if (hasProfileFields) {
1499
- recordInsightProfile(saved.profile);
1526
+ await recordInsightProfile(this.store, saved.profile);
1500
1527
  }
1501
- void flushInsightsOutbox().catch((err) => {
1528
+ void flushInsightsOutbox(this.store).catch((err) => {
1502
1529
  console.warn('[insights] flush after settings save failed', err);
1503
1530
  });
1504
1531
  return {
1505
1532
  status: 200,
1506
- data: { ...saved, stats: localStore.insightStats() },
1533
+ data: { ...saved, stats: await this.store.insightStats() },
1507
1534
  };
1508
1535
  }
1509
1536
  }
@@ -1516,19 +1543,19 @@ export class LocalBackendRouter {
1516
1543
  const properties = payload.properties && typeof payload.properties === 'object'
1517
1544
  ? payload.properties
1518
1545
  : {};
1519
- recordInsightEvent(eventName, properties);
1546
+ await recordInsightEvent(this.store, eventName, properties);
1520
1547
  return { status: 200, data: { status: 'ok' } };
1521
1548
  }
1522
1549
  if (method === 'GET' && pathname === '/api/v2/insights/export') {
1523
- return { status: 200, data: buildInsightsExportPayload() };
1550
+ return { status: 200, data: await buildInsightsExportPayload(this.store) };
1524
1551
  }
1525
1552
  if (method === 'POST' && pathname === '/api/v2/insights/upload-local') {
1526
- const ok = await uploadInsightsBundle();
1553
+ const ok = await uploadInsightsBundle(this.store);
1527
1554
  return {
1528
1555
  status: ok ? 200 : 502,
1529
1556
  data: {
1530
1557
  ok,
1531
- stats: localStore.insightStats(),
1558
+ stats: await this.store.insightStats(),
1532
1559
  detail: ok ? 'uploaded' : 'upload_failed_kept_local',
1533
1560
  },
1534
1561
  };
@@ -1537,7 +1564,7 @@ export class LocalBackendRouter {
1537
1564
  const days = parsePositiveIntParam(url.searchParams.get('days'), 30);
1538
1565
  return {
1539
1566
  status: 200,
1540
- data: localStore.getUsageSummary(days),
1567
+ data: await this.store.getUsageSummary(days),
1541
1568
  };
1542
1569
  }
1543
1570
  if (method === 'POST' && pathname.match(/^\/api\/v2\/chats\/[^/]+\/suggest-replies$/)) {
@@ -1623,7 +1650,7 @@ export class LocalBackendRouter {
1623
1650
  if (!chatId) {
1624
1651
  return { status: 400, data: { detail: 'chatId is required' } };
1625
1652
  }
1626
- const traces = localStore.listTracesByChat(chatId, limit).map(item => ({
1653
+ const traces = (await this.store.listTracesByChat(chatId, limit)).map(item => ({
1627
1654
  ...item,
1628
1655
  payload: this.safeJson(item.payload, {}),
1629
1656
  }));
@@ -1634,7 +1661,7 @@ export class LocalBackendRouter {
1634
1661
  }
1635
1662
  const traceMatch = pathname.match(/^\/api\/v2\/local\/traces\/([^/]+)$/);
1636
1663
  if (traceMatch && method === 'GET') {
1637
- const trace = localStore.getTrace(traceMatch[1]);
1664
+ const trace = await this.store.getTrace(traceMatch[1]);
1638
1665
  if (!trace)
1639
1666
  return this.notFound('Trace not found');
1640
1667
  return {
@@ -1672,7 +1699,7 @@ export class LocalBackendRouter {
1672
1699
  return { status: 200, data: { resolved: [] } };
1673
1700
  }
1674
1701
  const chatId = typeof payload.chatId === 'string' ? payload.chatId : '';
1675
- const baseDir = (chatId ? this.resolveChatProject(chatId)?.folderPath : null) ?? os.homedir();
1702
+ const baseDir = (chatId ? (await this.resolveChatProject(chatId))?.folderPath : null) ?? os.homedir();
1676
1703
  const resolved = await resolveMentionedPaths({ candidates, baseDir });
1677
1704
  return { status: 200, data: { resolved } };
1678
1705
  }
@@ -1734,7 +1761,10 @@ export class LocalBackendRouter {
1734
1761
  * - 错误:保留 `event:error` + `data: {"message":"..."}`(前端会忽略,但日志/未来 UI 用)
1735
1762
  * - 结束:标准 SSE `[DONE]` 串。
1736
1763
  */
1737
- async handleStream(request, emit, options = {}) {
1764
+ handleStream(request, emit, options = {}) {
1765
+ return this.storeContext.run(this.resolveStore(request.principal), () => this.handleStreamScoped(request, emit, options));
1766
+ }
1767
+ async handleStreamScoped(request, emit, options = {}) {
1738
1768
  const signal = options.signal;
1739
1769
  const url = new URL(request.path, 'http://local.backend');
1740
1770
  const pathname = url.pathname;
@@ -1750,7 +1780,7 @@ export class LocalBackendRouter {
1750
1780
  }
1751
1781
  const chatId = sendMatch ? sendMatch[1] : regenerateMatch[1];
1752
1782
  const payload = this.toRecord(request.body);
1753
- let chat = localStore.getChat(chatId);
1783
+ let chat = await this.store.getChat(chatId);
1754
1784
  if (!chat) {
1755
1785
  // 会话 URL 即会话身份:外部平台(自动化/eval)把一个 chatId 当成一次
1756
1786
  // 会话任务反复打开运行,本地会话却可能已经被用户清理、被空会话 prune、
@@ -1766,7 +1796,7 @@ export class LocalBackendRouter {
1766
1796
  const agentId = typeof payload.agentId === 'string' && payload.agentId.trim()
1767
1797
  ? payload.agentId.trim()
1768
1798
  : 'local-assistant';
1769
- chat = localStore.createChatWithId(chatId, '新对话', agentId);
1799
+ chat = await this.store.createChatWithId(chatId, '新对话', agentId);
1770
1800
  console.warn('[local-backend] stream: chat missing, auto-provisioned', {
1771
1801
  chatId,
1772
1802
  agentId,
@@ -1804,7 +1834,7 @@ export class LocalBackendRouter {
1804
1834
  // the *real* request instead of a generic placeholder (matters for
1805
1835
  // deferred-exec detection, mode preamble, etc., which all key off the
1806
1836
  // actual ask).
1807
- const priorMessages = localStore.listMessages(chatId, 200).reverse();
1837
+ const priorMessages = (await this.store.listMessages(chatId, 200)).reverse();
1808
1838
  const resolution = resolveRegenerateContext(priorMessages, targetMessageId);
1809
1839
  if (!resolution.ok) {
1810
1840
  emit(this.sse('error', { message: 'regenerate target message not found or not an assistant message' }));
@@ -1823,13 +1853,13 @@ export class LocalBackendRouter {
1823
1853
  const supervisor = getSidecarSupervisor();
1824
1854
  const outcome = supervisor
1825
1855
  ? await supervisor.forkSession({
1826
- recordId: localStore.getChatRecordId(chatId) ?? chatId,
1856
+ recordId: await this.store.getChatRecordId(chatId) ?? chatId,
1827
1857
  beforeUserIndex: resolveRegenerateForkOrdinal(priorMessages, targetMessageId),
1828
1858
  newRecordId: `${chatId}:r${Date.now().toString(36)}`,
1829
1859
  })
1830
1860
  : null;
1831
1861
  if (outcome?.ok) {
1832
- localStore.setChatRecordId(chatId, outcome.fork.recordId);
1862
+ await this.store.setChatRecordId(chatId, outcome.fork.recordId);
1833
1863
  console.log('[local-backend] regenerate: record forked (old tail preserved)', {
1834
1864
  chatId,
1835
1865
  branchRecordId: outcome.fork.recordId,
@@ -1848,7 +1878,7 @@ export class LocalBackendRouter {
1848
1878
  emit(this.sse('error', { message: plan.message }));
1849
1879
  return { status: 409 };
1850
1880
  }
1851
- const deleted = localStore.deleteMessagesFrom(chatId, targetMessageId);
1881
+ const deleted = await this.store.deleteMessagesFrom(chatId, targetMessageId);
1852
1882
  console.log('[local-backend] regenerate: truncated history', {
1853
1883
  chatId,
1854
1884
  targetMessageId,
@@ -1860,9 +1890,9 @@ export class LocalBackendRouter {
1860
1890
  // sidecar 回放 durable record 的投影作为循环种子。只在检测确为
1861
1891
  // interrupted 时接受:对一个已正常完结的 turn 续跑只会给完整对话
1862
1892
  // 凭空多接一段回复。
1863
- const prior = localStore.listMessages(chatId, 200);
1893
+ const prior = await this.store.listMessages(chatId, 200);
1864
1894
  const interrupted = detectInterruptedTurn({
1865
- turnActive: localStore.getTurnActive(chatId) !== null,
1895
+ turnActive: await this.store.getTurnActive(chatId) !== null,
1866
1896
  streamActive: getActiveCoreLoopStreamId(chatId) !== undefined,
1867
1897
  lastMessageRole: prior[0]?.role ?? null,
1868
1898
  });
@@ -1893,8 +1923,8 @@ export class LocalBackendRouter {
1893
1923
  // 3. 历史里目前没有任何 assistant 消息——也就是说,本轮即将产生的是第一条助手回复
1894
1924
  // 这避免在已经聊到一半的对话里突然把标题换掉。listMessages 是 DESC,limit 拉
1895
1925
  // 大一点确保看到完整历史(200 已经覆盖绝大多数本地会话)。
1896
- const existingAssistant = localStore
1897
- .listMessages(chatId, 200)
1926
+ const existingAssistant = (await this.store
1927
+ .listMessages(chatId, 200))
1898
1928
  .some((m) => m.role === 'assistant');
1899
1929
  const shouldGenerateTitle = !regenerateMatch &&
1900
1930
  !isResume &&
@@ -1915,7 +1945,7 @@ export class LocalBackendRouter {
1915
1945
  // persisted (and already in the durable record).
1916
1946
  let currentUserMessageId;
1917
1947
  if (!regenerateMatch && !isResume) {
1918
- const userMessage = localStore.addMessage(chatId, 'user', userMessageText);
1948
+ const userMessage = await this.store.addMessage(chatId, 'user', userMessageText);
1919
1949
  currentUserMessageId = userMessage.id;
1920
1950
  emit(this.sseData({ type: 'user_message', message: userMessage }));
1921
1951
  }
@@ -1924,7 +1954,7 @@ export class LocalBackendRouter {
1924
1954
  const chatMode = payload.mode === 'plan' ? 'plan' : 'agent';
1925
1955
  // 本轮生效的智能体:人设、技能勾选、工具策略同源。必须先解析——工具
1926
1956
  // 策略决定工具列表,工具列表又决定技能的触发条件。
1927
- const turnAgents = this.resolveTurnAgents(chatId, payload);
1957
+ const turnAgents = await this.resolveTurnAgents(chatId, payload);
1928
1958
  // Wave 2 工具分层:模型可见列表只出 direct 层(内置工具 + tool_search
1929
1959
  // 发现缝);MCP 动态工具全在 deferred 层,经 tool_search 命中即调。
1930
1960
  // 附带收益:MCP 目录增删不再改动系统提示词里的工具名录,前缀更稳。
@@ -2036,7 +2066,7 @@ export class LocalBackendRouter {
2036
2066
  });
2037
2067
  return;
2038
2068
  }
2039
- const fresh = localStore.getChat(chatId);
2069
+ const fresh = await this.store.getChat(chatId);
2040
2070
  if (!fresh) {
2041
2071
  console.log('[local-backend] title-gen skip', {
2042
2072
  chatId,
@@ -2052,7 +2082,7 @@ export class LocalBackendRouter {
2052
2082
  });
2053
2083
  return;
2054
2084
  }
2055
- const updated = localStore.updateChat(chatId, { title: result.title });
2085
+ const updated = await this.store.updateChat(chatId, { title: result.title });
2056
2086
  if (!updated) {
2057
2087
  console.warn('[local-backend] title-gen write failed', { chatId });
2058
2088
  return;
@@ -2081,8 +2111,8 @@ export class LocalBackendRouter {
2081
2111
  * Public: main.ts 的反向通道(reverse-tools.ts)经它给 CoreLoop 路径的
2082
2112
  * 工具调用补上 projectRoot 围栏。
2083
2113
  */
2084
- resolveChatProject(chatId) {
2085
- const projectId = localStore.getChat(chatId)?.projectId;
2114
+ async resolveChatProject(chatId) {
2115
+ const projectId = (await this.store.getChat(chatId))?.projectId;
2086
2116
  if (!projectId)
2087
2117
  return null;
2088
2118
  const project = this.toolRouter.projectRegistry?.get(projectId);
@@ -2103,9 +2133,9 @@ export class LocalBackendRouter {
2103
2133
  *
2104
2134
  * @param chatId 当前对话 id。
2105
2135
  * @param payload 前端提交的流请求体。
2106
- * @returns 本轮的智能体顺序、人设智能体,以及合并后的能力面。
2136
+ * @returns 本轮的智能体顺序、人设智能体、自称,以及合并后的能力面。
2107
2137
  */
2108
- resolveTurnAgents(chatId, payload) {
2138
+ async resolveTurnAgents(chatId, payload) {
2109
2139
  const mentionedAgentIds = Array.isArray(payload.mentionedAgentIds)
2110
2140
  ? payload.mentionedAgentIds.filter((s) => typeof s === 'string')
2111
2141
  : [];
@@ -2117,19 +2147,20 @@ export class LocalBackendRouter {
2117
2147
  ].filter((id, idx, arr) => arr.indexOf(id) === idx);
2118
2148
  if (orderedAgentIds.length === 0) {
2119
2149
  const fallbackId = (typeof payload.agentId === 'string' && payload.agentId) ||
2120
- localStore.getChat(chatId)?.agentId ||
2150
+ (await this.store.getChat(chatId))?.agentId ||
2121
2151
  null;
2122
2152
  if (fallbackId)
2123
2153
  orderedAgentIds.push(fallbackId);
2124
2154
  }
2125
- const agents = orderedAgentIds
2126
- .map((id) => localStore.getChatAgent(id))
2155
+ const agents = (await Promise.all(orderedAgentIds.map((id) => this.store.getChatAgent(id))))
2127
2156
  .filter((agent) => Boolean(agent));
2128
2157
  return {
2129
2158
  orderedAgentIds,
2130
2159
  // 能力面来自全部解析到的智能体,人设前言只用配了 rolePrompt 的那些——
2131
- // 自建智能体可以只勾技能、不写人设。
2160
+ // 自建智能体可以只勾技能、不写人设。自称始终跟第一个解析到的智能体,
2161
+ // 不要求写了人设——否则没 rolePrompt 的角色会回落成产品品牌。
2132
2162
  personaAgents: agents.filter((agent) => Boolean(agent.rolePrompt)),
2163
+ identityName: agents[0]?.name.trim() || null,
2133
2164
  capability: mergeAgentCapabilities(agents),
2134
2165
  };
2135
2166
  }
@@ -2140,7 +2171,7 @@ export class LocalBackendRouter {
2140
2171
  // 摘要,也不再按 40 条窗口截断——让框架 record 累积完整对话并按需压缩。
2141
2172
  // 上限即存储层 listMessages 的 1000 条硬上限;超出时最旧历史随
2142
2173
  // host_revision 优雅退化。
2143
- let rawHistory = localStore.listMessages(chatId, HISTORY_SEED_LIMIT).reverse();
2174
+ let rawHistory = (await this.store.listMessages(chatId, HISTORY_SEED_LIMIT)).reverse();
2144
2175
  // 剔除刚刚写入数据库的当前轮用户消息,避免在上下文历史中与我们显式添加的
2145
2176
  // latestUserMessage 发生重复。send 路径按 id 精确剔除——上一条 assistant
2146
2177
  // 回复在流结束后异步落库(createdAt 取落库时刻),快速连续发送时它会排到
@@ -2185,7 +2216,7 @@ export class LocalBackendRouter {
2185
2216
  })))
2186
2217
  .filter(Boolean);
2187
2218
  const settings = llmService.getSettings();
2188
- const { personaAgents, capability } = turnAgents;
2219
+ const { personaAgents, capability, identityName } = turnAgents;
2189
2220
  let personaPreamble = '';
2190
2221
  if (personaAgents.length === 1) {
2191
2222
  personaPreamble = `【当前角色】${personaAgents[0].name}\n${personaAgents[0].rolePrompt}`;
@@ -2253,6 +2284,7 @@ export class LocalBackendRouter {
2253
2284
  excludeSkillNames,
2254
2285
  pinnedSkillNames: capability.pinnedSkills,
2255
2286
  personaPreamble: effectivePersonaPreamble,
2287
+ identityName,
2256
2288
  realityCheckSuffix: realityCheck,
2257
2289
  forcedMcpTool,
2258
2290
  ignoreConditions: capability.loadAllSkills,
@@ -2281,7 +2313,7 @@ export class LocalBackendRouter {
2281
2313
  // 项目模式:绑定项目的对话在系统提示末尾追加沙箱约定(两条拼装路径
2282
2314
  // 都追加)。硬围栏在 ToolRouter/LocalExecutor,这里让模型事先知道边界,
2283
2315
  // 减少越界尝试。
2284
- const chatProject = this.resolveChatProject(chatId);
2316
+ const chatProject = await this.resolveChatProject(chatId);
2285
2317
  if (chatProject) {
2286
2318
  systemPrompt +=
2287
2319
  `\n\n【项目模式】当前对话绑定项目「${chatProject.name}」,根目录:${chatProject.folderPath}\n` +
@@ -2293,7 +2325,7 @@ export class LocalBackendRouter {
2293
2325
  // 仅在用户显式信任该项目后才注入模型上下文——未信任一律不读取、不注入
2294
2326
  // (打开恶意仓库时,一段构造的规则文件不能劫持 agent)。信任状态持久化、
2295
2327
  // 可在设置里撤销;每回合重新读盘,规则保存即生效。
2296
- const projectId = localStore.getChat(chatId)?.projectId;
2328
+ const projectId = (await this.store.getChat(chatId))?.projectId;
2297
2329
  const registry = this.toolRouter.projectRegistry;
2298
2330
  if (projectId && registry?.isTrusted(projectId)) {
2299
2331
  const rules = loadProjectRuleFiles(chatProject.folderPath);
@@ -2319,7 +2351,10 @@ export class LocalBackendRouter {
2319
2351
  const forcedSkill = await findSkill(forcedSkillName, { exclude: forcedSkillExcludes });
2320
2352
  if (forcedSkill) {
2321
2353
  // 场景包的正文占位符变量(如包技能正文里的工作区路径占位符)。
2322
- finalUserContent = `${buildForcedSkillMessage(forcedSkill, collectPackForcedSkillVars(chatId))}\n\n---\n\n${latestUserMessage}`;
2354
+ finalUserContent = `${buildForcedSkillMessage(forcedSkill, {
2355
+ ...brandSkillVars({ identityName }),
2356
+ ...collectPackForcedSkillVars(chatId),
2357
+ })}\n\n---\n\n${latestUserMessage}`;
2323
2358
  }
2324
2359
  }
2325
2360
  // W6-3 多模态:渲染进程把图片附件以 metadata.images({path,name})传上来,
@@ -2371,14 +2406,14 @@ export class LocalBackendRouter {
2371
2406
  .slice(0, MAX_REFERENCED_CHATS);
2372
2407
  const out = [];
2373
2408
  for (const refChatId of ids) {
2374
- const refChat = localStore.getChat(refChatId);
2409
+ const refChat = await this.store.getChat(refChatId);
2375
2410
  if (!refChat) {
2376
2411
  console.warn('[local-backend] referenced chat not found, skipping', { refChatId });
2377
2412
  continue;
2378
2413
  }
2379
2414
  const sections = [];
2380
- const recent = (await Promise.all(localStore
2381
- .listMessages(refChatId, RECENT_MESSAGES_PER_CHAT)
2415
+ const recent = (await Promise.all((await this.store
2416
+ .listMessages(refChatId, RECENT_MESSAGES_PER_CHAT))
2382
2417
  .reverse()
2383
2418
  .map(async (item) => {
2384
2419
  if (item.role !== 'user' && item.role !== 'assistant')
@@ -2753,7 +2788,7 @@ export class LocalBackendRouter {
2753
2788
  // empty list, so writes confine to system scratch dirs only.
2754
2789
  // `execPolicy: 'full'`(输入框「完整权限」)关闭这一层,让 mkdir
2755
2790
  // Downloads 这类项目外写入不再被 Seatbelt 拦成 Operation not permitted。
2756
- const chatProject = this.resolveChatProject(chatId);
2791
+ const chatProject = await this.resolveChatProject(chatId);
2757
2792
  const execSandbox = buildExecSandbox([
2758
2793
  // 场景包声明的每会话可写根(如文档包在项目根之外落盘产物的
2759
2794
  // 工作区)。
@@ -2789,7 +2824,7 @@ export class LocalBackendRouter {
2789
2824
  // 下方的清除点,残留的标记就是「上次回复被中断」的签名(与活进程写下的
2790
2825
  // cancelled/failed 终态区分)。标记只覆盖「已交给 sidecar」的阶段:503
2791
2826
  // (sidecar 未运行)在上方已 return,record 里没有这一轮,无可续跑。
2792
- localStore.setTurnActive(chatId);
2827
+ await this.store.setTurnActive(chatId);
2793
2828
  // 实时快照:executedActions / timeline / children 传引用(原地 mutate),
2794
2829
  // content 由 onText 逐段更新。切走再切回的 renderer 靠 GET /live-stream 读它。
2795
2830
  const live = registerLiveStream(chatId, { executedActions, timeline, children });
@@ -2809,7 +2844,7 @@ export class LocalBackendRouter {
2809
2844
  chatId,
2810
2845
  // W5-2: after a regenerate-fork the chat's live record is the
2811
2846
  // branch; undefined falls back to chatId on the sidecar.
2812
- recordId: localStore.getChatRecordId(chatId) ?? undefined,
2847
+ recordId: await this.store.getChatRecordId(chatId) ?? undefined,
2813
2848
  systemPrompt,
2814
2849
  messages,
2815
2850
  resume,
@@ -2993,14 +3028,14 @@ export class LocalBackendRouter {
2993
3028
  : // resume 要求 messages 为空:记录是唯一权威,再喂一份历史会
2994
3029
  // 静默分叉(sidecar 对此 fail loud)。
2995
3030
  { ...coreLoopOptions, resume: true, messages: [] }),
2996
- afterPass: (outcome) => {
3031
+ afterPass: async (outcome) => {
2997
3032
  if (outcome.traceId)
2998
3033
  passTraceIds.push(outcome.traceId);
2999
3034
  // W6-9 用量归因:落一条 chat 用量事件(含成本估算,无单价模型 costUsd=null)。
3000
3035
  // 每趟各落一条——续跑的花费是真实发生的,不该被合并掉。
3001
3036
  if (outcome.usage) {
3002
3037
  try {
3003
- localStore.recordUsageEvent({
3038
+ await this.store.recordUsageEvent({
3004
3039
  chatId,
3005
3040
  kind: 'chat',
3006
3041
  provider: settings.provider,
@@ -3054,7 +3089,7 @@ export class LocalBackendRouter {
3054
3089
  passTraceIds.push(failureTraceId);
3055
3090
  }
3056
3091
  }
3057
- const durationMs = turnDurationMs(localStore.getTurnActive(chatId)?.startedAt, Date.now());
3092
+ const durationMs = turnDurationMs((await this.store.getTurnActive(chatId))?.startedAt, Date.now());
3058
3093
  // 回合产物文件列表(成功/失败/取消都收集——半截回合写出的文件同样是
3059
3094
  // 产物)。收集失败只记日志,永远不该拖垮回合收尾。
3060
3095
  let turnFiles = [];
@@ -3069,7 +3104,7 @@ export class LocalBackendRouter {
3069
3104
  catch (turnFilesErr) {
3070
3105
  console.warn('[local-backend] collect turn files failed', turnFilesErr);
3071
3106
  }
3072
- const assistant = localStore.addMessage(chatId, 'assistant', assistantText, JSON.stringify({
3107
+ const assistant = await this.store.addMessage(chatId, 'assistant', assistantText, JSON.stringify({
3073
3108
  completionStatus,
3074
3109
  completionReason,
3075
3110
  executedActions,
@@ -3082,7 +3117,7 @@ export class LocalBackendRouter {
3082
3117
  ...(turnFiles.length > 0 ? { turnFiles } : {}),
3083
3118
  }));
3084
3119
  try {
3085
- recordInsightTurn({
3120
+ await recordInsightTurn(this.store, {
3086
3121
  chatId,
3087
3122
  mode: chatMode,
3088
3123
  modelId: overrideModel || settings.model,
@@ -3099,7 +3134,7 @@ export class LocalBackendRouter {
3099
3134
  // W7-1: 回复落库后才清标记(顺序不能反——先清后写的话,两者之间崩溃
3100
3135
  // 会丢回复且无任何续跑入口;这个顺序的最坏情况是残留标记被检测器的
3101
3136
  // 「末尾已是 assistant」规则判定为已完结)。
3102
- localStore.clearTurnActive(chatId);
3137
+ await this.store.clearTurnActive(chatId);
3103
3138
  // 回合落库即结束——移除实时快照,后续 GET /live-stream 返回 active=false。
3104
3139
  removeLiveStream(chatId);
3105
3140
  // Persist the sidecar-recorded traces into harness_traces so the CoreLoop
@@ -3108,7 +3143,7 @@ export class LocalBackendRouter {
3108
3143
  // 一趟一条:自动续跑的每一趟都是一次独立的 sidecar 运行,全部挂到本轮
3109
3144
  // 那条 assistant 消息上(message_id 无唯一约束),否则中间趟的 trace 就
3110
3145
  // 从消息侧不可达了。
3111
- const telemetry = localStore.getTelemetrySettings();
3146
+ const telemetry = await this.store.getTelemetrySettings();
3112
3147
  for (const [index, traceId] of passTraceIds.entries()) {
3113
3148
  // 只有最后一趟携带本轮终态;之前每一趟都是撞墙截停才有下一趟。
3114
3149
  const passStatus = index === passTraceIds.length - 1 ? completionStatus : 'budget_exhausted';
@@ -3118,7 +3153,7 @@ export class LocalBackendRouter {
3118
3153
  const durationMs = typeof sidecarTrace.durationMs === 'number'
3119
3154
  ? sidecarTrace.durationMs
3120
3155
  : null;
3121
- localStore.saveTrace({
3156
+ await this.store.saveTrace({
3122
3157
  id: traceId,
3123
3158
  chatId,
3124
3159
  messageId: assistant.id,
@@ -3200,11 +3235,11 @@ export class LocalBackendRouter {
3200
3235
  }
3201
3236
  })();
3202
3237
  }
3203
- publishSuggestedReplies(chatId, messageId, suggestions) {
3238
+ async publishSuggestedReplies(chatId, messageId, suggestions) {
3204
3239
  if (suggestions.length === 0)
3205
3240
  return;
3206
3241
  try {
3207
- localStore.patchMessageMetadata(chatId, messageId, { suggestedReplies: suggestions });
3242
+ await this.store.patchMessageMetadata(chatId, messageId, { suggestedReplies: suggestions });
3208
3243
  }
3209
3244
  catch (err) {
3210
3245
  console.warn('[local-backend] suggested-replies persist failed', { chatId, messageId, err });