@steerable/agent-shell 0.6.20 → 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 (64) 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 -15
  7. package/dist/host/visible-terminal-exec.d.ts +0 -2
  8. package/dist/host/visible-terminal-exec.js +4 -8
  9. package/dist/insights/flush.d.ts +5 -4
  10. package/dist/insights/flush.js +14 -15
  11. package/dist/insights/record.d.ts +6 -5
  12. package/dist/insights/record.js +9 -10
  13. package/dist/llm/index.d.ts +6 -2
  14. package/dist/llm/index.js +19 -9
  15. package/dist/local-backend/ai-suggestions.d.ts +32 -0
  16. package/dist/local-backend/ai-suggestions.js +160 -0
  17. package/dist/local-backend/coreloop-stream.js +1 -1
  18. package/dist/local-backend/mentioned-paths.d.ts +28 -0
  19. package/dist/local-backend/mentioned-paths.js +67 -0
  20. package/dist/local-backend/prompt-builder.d.ts +10 -4
  21. package/dist/local-backend/prompt-builder.js +13 -12
  22. package/dist/local-backend/router.d.ts +26 -4
  23. package/dist/local-backend/router.js +243 -96
  24. package/dist/local-backend/skills/00-identity/SKILL.md +1 -1
  25. package/dist/local-backend/task-service.d.ts +14 -13
  26. package/dist/local-backend/task-service.js +32 -29
  27. package/dist/local-backend/turn-files.d.ts +57 -0
  28. package/dist/local-backend/turn-files.js +285 -0
  29. package/dist/local-backend/worktree-service.d.ts +1 -1
  30. package/dist/local-backend/worktree-service.js +6 -6
  31. package/dist/main.js +45 -26
  32. package/dist/preload.cjs +30 -10
  33. package/dist/preload.js +33 -10
  34. package/dist/server/http-server.d.ts +13 -1
  35. package/dist/server/http-server.js +45 -8
  36. package/dist/server/index.js +4 -90
  37. package/dist/server/sse-bus.d.ts +1 -1
  38. package/dist/server/start.d.ts +26 -0
  39. package/dist/server/start.js +111 -0
  40. package/dist/sidecar/boot.d.ts +3 -1
  41. package/dist/sidecar/boot.js +6 -7
  42. package/dist/sidecar/reverse-approval.d.ts +2 -0
  43. package/dist/sidecar/reverse-approval.js +2 -1
  44. package/dist/sidecar/reverse-ask-user.d.ts +2 -0
  45. package/dist/sidecar/reverse-ask-user.js +2 -1
  46. package/dist/sidecar/reverse-spawn.js +12 -4
  47. package/dist/sidecar/reverse-tools.d.ts +1 -1
  48. package/dist/sidecar/reverse-tools.js +1 -1
  49. package/dist/sidecar/storage-path.js +2 -3
  50. package/dist/storage/driver.d.ts +57 -0
  51. package/dist/storage/driver.js +104 -0
  52. package/dist/storage/index.d.ts +80 -67
  53. package/dist/storage/index.js +528 -296
  54. package/dist/storage/pack-migrations.d.ts +2 -4
  55. package/dist/storage/pack-migrations.js +2 -4
  56. package/dist/storage/scoped-store.d.ts +125 -0
  57. package/dist/storage/scoped-store.js +1 -0
  58. package/dist/storage/sqlite-driver.d.ts +16 -0
  59. package/dist/storage/sqlite-driver.js +135 -0
  60. package/dist/storage/write-lease-error.d.ts +11 -0
  61. package/dist/storage/write-lease-error.js +18 -0
  62. package/package.json +14 -5
  63. package/dist/storage/local-store-singleton.d.ts +0 -28
  64. package/dist/storage/local-store-singleton.js +0 -38
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @steerable/agent-shell
2
+
3
+ Product-neutral Electron and headless HTTP host for Steerable Framework.
4
+
5
+ ## BS composition
6
+
7
+ Products can register an `AuthProvider` from `@steerable/agent-shell/auth`
8
+ before starting the host. A registered provider authenticates every
9
+ `/api/v2/*` and `/host/*` request and supplies the request `Principal`.
10
+ Without a provider, the host retains its loopback Host check and
11
+ per-process Bearer token.
12
+
13
+ Use `startBsHost` from `@steerable/agent-shell/server/start` to embed the BS
14
+ host. Its options support host and port selection, a fixed Bearer token, and
15
+ ordered middleware. Middleware returns `pass` to continue or `handled` to
16
+ short-circuit. The returned handle owns shutdown of both HTTP and runtime
17
+ services.
18
+
19
+ ## Storage drivers
20
+
21
+ Storage drivers register during product composition and initialize after all
22
+ scenario-pack migrations and seeds are registered. `initializeStorage()` must
23
+ complete before `getScopedStore(scope)` or `getPackDbAccess(scope)` is used.
24
+ The default `SqliteStorageDriver` preserves the local database path, migration
25
+ sequence, seed behavior, and write lease.
26
+
27
+ Host repositories use the async `ScopedStore` API. Scenario packs receive
28
+ `PackDbAccess`, which carries `tenantId` and `userId` and does not expose the
29
+ driver connection. Because arbitrary SQL cannot be rewritten safely,
30
+ pack repositories must include both ownership columns in every statement;
31
+ the SQLite and MySQL access objects reject missing or mismatched ownership,
32
+ and pack boundary checks enforce the same requirement before runtime.
33
+
34
+ License: Apache-2.0.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Product-neutral authenticated identity attached to one local-backend request.
3
+ */
4
+ export interface Principal {
5
+ id: string;
6
+ tenantId: string;
7
+ displayName: string;
8
+ email: string | null;
9
+ roles: readonly string[];
10
+ isAdmin: boolean;
11
+ }
12
+ export type AuthDecision = {
13
+ ok: true;
14
+ principal: Principal;
15
+ } | {
16
+ ok: false;
17
+ status: 401 | 403;
18
+ body?: unknown;
19
+ };
20
+ /**
21
+ * Authentication extension point for BS deployments.
22
+ *
23
+ * Implementations own all interpretation and validation of request headers,
24
+ * including cookies and any identity assertions added by a trusted proxy.
25
+ */
26
+ export interface AuthProvider {
27
+ readonly id: string;
28
+ authenticate(headers: Readonly<Record<string, string | string[] | undefined>>): Promise<AuthDecision>;
29
+ describeSelf(principal: Principal): Promise<unknown>;
30
+ }
31
+ /**
32
+ * Registers the process-wide provider before the BS host starts.
33
+ *
34
+ * @returns A disposer that removes this provider if it is still active.
35
+ */
36
+ export declare function registerAuthProvider(provider: AuthProvider): () => void;
37
+ /** Returns the provider registered by the product composition root, if any. */
38
+ export declare function getAuthProvider(): AuthProvider | null;
@@ -0,0 +1,20 @@
1
+ let activeProvider = null;
2
+ /**
3
+ * Registers the process-wide provider before the BS host starts.
4
+ *
5
+ * @returns A disposer that removes this provider if it is still active.
6
+ */
7
+ export function registerAuthProvider(provider) {
8
+ if (activeProvider) {
9
+ throw new Error(`[auth] provider already registered: ${activeProvider.id}`);
10
+ }
11
+ activeProvider = provider;
12
+ return () => {
13
+ if (activeProvider === provider)
14
+ activeProvider = null;
15
+ };
16
+ }
17
+ /** Returns the provider registered by the product composition root, if any. */
18
+ export function getAuthProvider() {
19
+ return activeProvider;
20
+ }
@@ -14,12 +14,12 @@
14
14
  * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
15
  * 不出现任何包名。
16
16
  */
17
- import type Database from 'better-sqlite3';
18
17
  import type { HttpRouteContribution, IpcContribution, ToolContribution } from '../scenario/pack.js';
18
+ import type { PackDbAccess } from '../storage/driver.js';
19
19
  /** 宿主注入包的装配能力面。包不 import 宿主装配模块,全部经回调注入。 */
20
20
  export interface PackAssemblyDeps {
21
- /** 宿主借出的同一个 SQLite 连接(包只读写自己声明的包前缀表)。 */
22
- readonly db: Database.Database;
21
+ /** Scope-bound database access; the underlying driver connection is hidden. */
22
+ readonly packDb: PackDbAccess;
23
23
  /** 模型可见工具面(与普通回合同源)。 */
24
24
  readonly listTools: () => {
25
25
  name: string;
@@ -27,10 +27,10 @@ export interface PackAssemblyDeps {
27
27
  inputSchema: unknown;
28
28
  }[];
29
29
  /** chatId → 绑定项目(无项目对话返回 null)。与 router.resolveChatProject 同源。 */
30
- readonly resolveChatProject: (chatId: string) => {
30
+ readonly resolveChatProject: (chatId: string) => Promise<{
31
31
  name: string;
32
32
  folderPath: string;
33
- } | null;
33
+ } | null>;
34
34
  /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
35
35
  readonly broadcast: (channel: string, payload: unknown) => void;
36
36
  /** 工具贡献注册口(toolRouter.registerToolContributions 的收窄面)。 */
@@ -46,7 +46,7 @@ export interface PackAssemblyDeps {
46
46
  totalTokens: number;
47
47
  cachedPromptTokens?: number;
48
48
  costUsd?: number | null;
49
- }) => void;
49
+ }) => Promise<void>;
50
50
  /** 行为洞察(与主对话同款 turn 事件)。 */
51
51
  readonly recordInsight: (input: {
52
52
  chatId: string;
@@ -57,7 +57,7 @@ export interface PackAssemblyDeps {
57
57
  toolNames?: unknown;
58
58
  userText?: string;
59
59
  assistantText?: string;
60
- }) => void;
60
+ }) => Promise<void>;
61
61
  /** 装配期日志行(进宿主日志)。 */
62
62
  readonly onLog: (line: string) => void;
63
63
  }
@@ -20,10 +20,13 @@ import { ProjectRegistry } from '../project-registry.js';
20
20
  import { LocalBackendRouter } from '../local-backend/router.js';
21
21
  import { WorktreeService } from '../local-backend/worktree-service.js';
22
22
  import { TaskService } from '../local-backend/task-service.js';
23
+ import { type TenantScope } from '../storage/driver.js';
23
24
  import { createApprovalBridge } from '../sidecar/reverse-approval.js';
24
25
  import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
25
26
  import { type PackAssemblyHandle } from './pack-assembly.js';
26
27
  export interface HostRuntimeOptions {
28
+ /** Storage ownership for this host process. Defaults to local personal use. */
29
+ scope?: TenantScope;
27
30
  /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
28
31
  broadcast: (channel: string, payload: unknown) => void;
29
32
  /**
@@ -42,6 +45,7 @@ export interface HostRuntimeOptions {
42
45
  taskSweepReason: string;
43
46
  }
44
47
  export interface HostRuntime {
48
+ store: import('../storage/scoped-store.js').ScopedStore;
45
49
  localExecutor: LocalExecutor;
46
50
  localScriptRegistry: LocalScriptRegistry;
47
51
  terminalManager: TerminalManager;
@@ -62,8 +66,8 @@ export interface HostRuntime {
62
66
  * 不阻塞调用方的就绪路径(sidecar python import 慢,竞速的回合由
63
67
  * router 回退处理)。
64
68
  */
65
- start(): void;
69
+ start(): Promise<void>;
66
70
  /** 关停:杀终端、停 mock、关 sidecar。 */
67
71
  shutdown(): Promise<void>;
68
72
  }
69
- export declare function createHostRuntime(options: HostRuntimeOptions): HostRuntime;
73
+ export declare function createHostRuntime(options: HostRuntimeOptions): Promise<HostRuntime>;
@@ -20,7 +20,7 @@ import { ProjectRegistry } from '../project-registry.js';
20
20
  import { LocalBackendRouter } from '../local-backend/router.js';
21
21
  import { WorktreeService } from '../local-backend/worktree-service.js';
22
22
  import { TaskService } from '../local-backend/task-service.js';
23
- import { localStore } from '../storage/index.js';
23
+ import { LOCAL_SCOPE, closeStorage, getPackDbAccess, getScopedStore, initializeStorage, } from '../storage/driver.js';
24
24
  import { createApprovalBridge } from '../sidecar/reverse-approval.js';
25
25
  import { createAskUserBridge } from '../sidecar/reverse-ask-user.js';
26
26
  import { startHostSidecar, shutdownHostSidecar } from '../sidecar/boot.js';
@@ -29,18 +29,52 @@ import { createJsonStore } from '../json-store.js';
29
29
  import { bindWorkspaceSkillRoots } from '../local-backend/skill-loader.js';
30
30
  import { getChatAttachmentsDir } from '../attachments.js';
31
31
  import { recordInsightTurn } from '../insights/record.js';
32
+ import { llmService } from '../llm/index.js';
32
33
  import { getPackAssemblies, } from './pack-assembly.js';
33
- export function createHostRuntime(options) {
34
+ export async function createHostRuntime(options) {
34
35
  const { broadcast, hasWindow, onLog } = options;
35
36
  const broadcastMain = options.broadcastMain ?? broadcast;
37
+ await initializeStorage();
38
+ const scope = options.scope ?? LOCAL_SCOPE;
39
+ const defaultStore = getScopedStore(scope);
40
+ let localBackendRouter;
41
+ const activeScope = () => localBackendRouter?.activeStore.scope ?? scope;
42
+ const packDb = {
43
+ get scope() {
44
+ return activeScope();
45
+ },
46
+ get(sql, params) {
47
+ return getPackDbAccess(activeScope()).get(sql, params);
48
+ },
49
+ all(sql, params) {
50
+ return getPackDbAccess(activeScope()).all(sql, params);
51
+ },
52
+ run(sql, params) {
53
+ return getPackDbAccess(activeScope()).run(sql, params);
54
+ },
55
+ exec(sql) {
56
+ return getPackDbAccess(activeScope()).exec(sql);
57
+ },
58
+ transaction(operation) {
59
+ return getPackDbAccess(activeScope()).transaction(() => operation(packDb));
60
+ },
61
+ };
36
62
  const localExecutor = new LocalExecutor();
37
63
  // 恢复设置界面里配置的"命令默认超时"(保存时由 local-backend router 即时
38
64
  // 生效,这里负责进程重启后的恢复)。
39
65
  {
40
- const persistedLlmSettings = localStore.getLlmSettings();
66
+ const persistedLlmSettings = await defaultStore.getLlmSettings();
41
67
  setDefaultExecTimeoutMs(persistedLlmSettings?.execTimeoutSeconds
42
68
  ? persistedLlmSettings.execTimeoutSeconds * 1000
43
69
  : null);
70
+ llmService.initialize(persistedLlmSettings ?? {
71
+ provider: 'ollama',
72
+ model: 'llama3.1:8b',
73
+ baseUrl: 'http://127.0.0.1:11434',
74
+ temperature: 0.3,
75
+ }, {
76
+ setLlmSettings: (settings) => defaultStore.setLlmSettings(settings),
77
+ });
44
78
  }
45
79
  const localScriptRegistry = new LocalScriptRegistry();
46
80
  const terminalManager = new TerminalManager();
@@ -68,7 +102,6 @@ export function createHostRuntime(options) {
68
102
  const maybeExecInTerminal = createVisibleTerminalExec({
69
103
  localExecutor,
70
104
  terminalManager,
71
- broadcast,
72
105
  });
73
106
  const toolRouter = new ToolRouter(localExecutor, localScriptRegistry, async (request) => {
74
107
  const viaTerminal = await maybeExecInTerminal(request);
@@ -79,8 +112,8 @@ export function createHostRuntime(options) {
79
112
  // 4.6a/4.6b:跨 turn 后台任务 + git worktree 隔离。任务流由宿主独立驱动
80
113
  // (不绑父 turn 生命周期,见 task-service.ts 模块头);终态经广播推到
81
114
  // 用户面任务面板。
82
- const resolveChatProjectRoot = (chatId) => {
83
- const projectId = localStore.getChat(chatId)?.projectId;
115
+ const resolveChatProjectRoot = async (chatId) => {
116
+ const projectId = (await defaultStore.getChat(chatId))?.projectId;
84
117
  if (!projectId)
85
118
  return null;
86
119
  const project = projectRegistry.get(projectId);
@@ -92,20 +125,19 @@ export function createHostRuntime(options) {
92
125
  const packHandles = new Map();
93
126
  {
94
127
  const packDeps = {
95
- db: localStore.getPackDb(),
128
+ packDb,
96
129
  listTools: () => toolRouter.listModelSchemas(),
97
130
  resolveChatProject: resolveChatProjectRoot,
98
131
  broadcast,
99
132
  registerTools: (tools) => toolRouter.registerToolContributions(tools),
100
- recordUsage: (input) => localStore.recordUsageEvent(input),
101
- recordInsight: (input) => recordInsightTurn(input),
133
+ recordUsage: (input) => (localBackendRouter?.activeStore ?? defaultStore).recordUsageEvent(input),
134
+ recordInsight: (input) => recordInsightTurn(localBackendRouter?.activeStore ?? defaultStore, input),
102
135
  onLog,
103
136
  };
104
137
  // 包迁移容错应用(3.2):包的 import 链可能经 llm/index 等模块提前
105
138
  // 触发 storage 单例构造(ESM 深度优先,active.ts 的注册体后于链上
106
139
  // 单例求值)——构造时注册表尚空,此处(装配前)补上已注册包迁移。
107
140
  // 幂等(按包 id 去重),注册表为空时零开销。
108
- localStore.applyPackMigrations();
109
141
  for (const [packId, assemble] of getPackAssemblies()) {
110
142
  const handle = assemble(packDeps);
111
143
  if (handle)
@@ -114,14 +146,18 @@ export function createHostRuntime(options) {
114
146
  }
115
147
  const worktreeService = new WorktreeService({ resolveProject: resolveChatProjectRoot });
116
148
  const taskService = new TaskService({
117
- store: localStore,
149
+ store: () => localBackendRouter?.activeStore ?? defaultStore,
118
150
  toolRouter,
119
151
  worktreeService,
120
152
  resolveChatProject: resolveChatProjectRoot,
121
153
  broadcast,
122
154
  });
123
155
  toolRouter.setTaskServices({ taskService, worktreeService });
124
- const localBackendRouter = new LocalBackendRouter(toolRouter, {
156
+ localBackendRouter = new LocalBackendRouter(toolRouter, {
157
+ store: defaultStore,
158
+ resolveStore: (principal) => principal
159
+ ? getScopedStore({ tenantId: principal.tenantId, userId: principal.id })
160
+ : defaultStore,
125
161
  taskService,
126
162
  broadcast: broadcastMain,
127
163
  });
@@ -138,6 +174,7 @@ export function createHostRuntime(options) {
138
174
  });
139
175
  let started = false;
140
176
  return {
177
+ store: defaultStore,
141
178
  localExecutor,
142
179
  localScriptRegistry,
143
180
  terminalManager,
@@ -151,13 +188,13 @@ export function createHostRuntime(options) {
151
188
  approvalBridge,
152
189
  askUserBridge,
153
190
  maybeExecInTerminal,
154
- start() {
191
+ async start() {
155
192
  if (started)
156
193
  return;
157
194
  started = true;
158
195
  // 4.6a:上次进程崩溃/强杀留下的 running 任务不是真相——任务流随进程
159
196
  // 一起死了,启动时落成 failed,任务面板据此显示"重启中断"。
160
- const sweptTasks = localStore.failRunningTasks(options.taskSweepReason);
197
+ const sweptTasks = await defaultStore.failRunningTasks(options.taskSweepReason);
161
198
  if (sweptTasks > 0) {
162
199
  onLog(`[task] swept ${sweptTasks} stale running task(s)`);
163
200
  }
@@ -165,8 +202,9 @@ export function createHostRuntime(options) {
165
202
  // Not awaited: sidecar boot (python import) must not delay readiness.
166
203
  // A turn that races the boot falls back for that turn only.
167
204
  void startHostSidecar({
205
+ store: defaultStore,
168
206
  toolRouter,
169
- resolveProjectRoot: (chatId) => localBackendRouter.resolveChatProject(chatId)?.folderPath ?? null,
207
+ resolveProjectRoot: async (chatId) => (await localBackendRouter.resolveChatProject(chatId))?.folderPath ?? null,
170
208
  // 项目模式下文件读写被围栏在项目目录内;会话附件目录是额外放行的
171
209
  // 只读根,保证用户上传的文件即使在项目会话里也能被 agent 读回。
172
210
  resolveAdditionalReadRoots: (chatId) => [getChatAttachmentsDir(chatId)],
@@ -202,6 +240,7 @@ export function createHostRuntime(options) {
202
240
  await handle.dispose?.();
203
241
  }
204
242
  await shutdownHostSidecar();
243
+ await closeStorage();
205
244
  },
206
245
  };
207
246
  }
@@ -1,10 +1,8 @@
1
1
  import { type LocalExecRequest, type LocalExecResult, type LocalExecutor } from '../local-executor.js';
2
2
  import type { TerminalManager } from '../terminal-manager.js';
3
- export type HostBroadcast = (channel: string, payload: unknown) => void;
4
3
  export interface VisibleTerminalExecDeps {
5
4
  localExecutor: LocalExecutor;
6
5
  terminalManager: TerminalManager;
7
- broadcast: HostBroadcast;
8
6
  }
9
7
  /**
10
8
  * Map a raw PTY shell binary name (e.g. `powershell.exe`, `cmd.exe`, `zsh`) to
@@ -1,8 +1,7 @@
1
1
  /**
2
- * 可见 PTY 执行路由(Codex 式"看 agent 打字"):shell 命令优先打进共享的
3
- * 可见终端,多行/heredoc 等 sentinel 抓不住的形态回退 headless。
4
- * Electron main 与 BS server 共用;`terminal:reveal` 等事件经注入的
5
- * broadcast 出口(Electron 窗口广播 / BS SSE 总线)。
2
+ * 可见 PTY 执行路由:shell 命令优先打进共享的可见终端,多行/heredoc 等
3
+ * sentinel 抓不住的形态回退 headless。Electron main 与 BS server 共用。
4
+ * 命令进入 PTY 时不通知 renderer 展开面板——终端只由用户手动打开。
6
5
  */
7
6
  import log from 'electron-log';
8
7
  import { GUI_LAUNCH_WAIT_MS, getConfiguredExecTimeoutMs, isGuiLaunchCommand, rewriteExeCommandIfNeeded, } from '../local-executor.js';
@@ -32,7 +31,7 @@ export function mapSessionShell(shell) {
32
31
  * process.
33
32
  */
34
33
  export function createVisibleTerminalExec(deps) {
35
- const { localExecutor, terminalManager, broadcast } = deps;
34
+ const { localExecutor, terminalManager } = deps;
36
35
  return async function maybeExecInTerminal(req) {
37
36
  if (req.command) {
38
37
  req.command = rewriteExeCommandIfNeeded(req.command);
@@ -73,9 +72,6 @@ export function createVisibleTerminalExec(deps) {
73
72
  timeoutMs = getConfiguredExecTimeoutMs() ?? undefined;
74
73
  }
75
74
  const session = terminalManager.ensurePrimary({ cwd: req.cwd });
76
- // Codex 式面板自动展开:命令进入可见 PTY 时通知 renderer 打开内嵌终端
77
- // 面板。幂等——面板已打开时 renderer 端是 no-op。
78
- broadcast('terminal:reveal', { sessionId: session.id });
79
75
  const resolvedShell = mapSessionShell(session.shell);
80
76
  // 项目模式(ToolRouter 会把项目根塞进 req.cwd):可见 PTY 是共享交互
81
77
  // 会话,已有 session 不会随 ensurePrimary 改目录——显式 cd 过去。
@@ -1,10 +1,11 @@
1
+ import type { ScopedStore } from '../storage/scoped-store.js';
1
2
  export declare function insightsNetworkDisabled(): boolean;
2
- export declare function buildInsightsExportPayload(): Record<string, unknown>;
3
+ export declare function buildInsightsExportPayload(store: ScopedStore): Promise<Record<string, unknown>>;
3
4
  export declare function postInsightsJson(url: string, body: unknown): Promise<boolean>;
4
- export declare function flushInsightsOutbox(): Promise<{
5
+ export declare function flushInsightsOutbox(store: ScopedStore): Promise<{
5
6
  uploaded: number;
6
7
  skipped: number;
7
8
  failed: number;
8
9
  }>;
9
- export declare function uploadInsightsBundle(): Promise<boolean>;
10
- export declare function scheduleInsightsFlush(): void;
10
+ export declare function uploadInsightsBundle(store: ScopedStore): Promise<boolean>;
11
+ export declare function scheduleInsightsFlush(store: ScopedStore): void;
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { getBrand } from '../brand.js';
5
5
  import { resolveInsightsApiBase, rowsEligibleForAutoUpload, } from '../storage/insights-settings.js';
6
- import { localStore } from '../storage/index.js';
7
6
  const FLUSH_TIMEOUT_MS = 8_000;
8
7
  const MAX_BATCH = 20;
9
8
  export function insightsNetworkDisabled() {
@@ -41,10 +40,10 @@ function clientMeta(settings) {
41
40
  appVersion: detectAppVersion(),
42
41
  };
43
42
  }
44
- export function buildInsightsExportPayload() {
45
- const settings = localStore.ensureInsightsSettings();
43
+ export async function buildInsightsExportPayload(store) {
44
+ const settings = await store.ensureInsightsSettings();
46
45
  return {
47
- ...localStore.exportInsightsBundle(),
46
+ ...await store.exportInsightsBundle(),
48
47
  ...clientMeta(settings),
49
48
  };
50
49
  }
@@ -84,12 +83,12 @@ function pathForKind(kind) {
84
83
  return '/api/v2/agent-insights/turns';
85
84
  return '/api/v2/agent-insights/profile';
86
85
  }
87
- export async function flushInsightsOutbox() {
86
+ export async function flushInsightsOutbox(store) {
88
87
  if (insightsNetworkDisabled()) {
89
88
  return { uploaded: 0, skipped: 0, failed: 0 };
90
89
  }
91
- const settings = localStore.ensureInsightsSettings();
92
- const pending = localStore.listInsightOutbox({ uploaded: false, limit: MAX_BATCH });
90
+ const settings = await store.ensureInsightsSettings();
91
+ const pending = await store.listInsightOutbox({ uploaded: false, limit: MAX_BATCH });
93
92
  const { upload, skipped } = rowsEligibleForAutoUpload(settings, pending);
94
93
  let uploaded = 0;
95
94
  let failed = 0;
@@ -105,22 +104,22 @@ export async function flushInsightsOutbox() {
105
104
  clientEventId: row.id,
106
105
  });
107
106
  if (ok) {
108
- localStore.markInsightUploaded(row.id);
107
+ await store.markInsightUploaded(row.id);
109
108
  uploaded += 1;
110
109
  }
111
110
  else {
112
- localStore.markInsightUploadError(row.id, 'upload_failed');
111
+ await store.markInsightUploadError(row.id, 'upload_failed');
113
112
  failed += 1;
114
113
  }
115
114
  }
116
115
  return { uploaded, skipped, failed };
117
116
  }
118
- export async function uploadInsightsBundle() {
117
+ export async function uploadInsightsBundle(store) {
119
118
  if (insightsNetworkDisabled()) {
120
119
  return false;
121
120
  }
122
- const settings = localStore.ensureInsightsSettings();
123
- const bundle = buildInsightsExportPayload();
121
+ const settings = await store.ensureInsightsSettings();
122
+ const bundle = await buildInsightsExportPayload(store);
124
123
  const base = resolveInsightsApiBase(settings, process.env.DEEPPATH_INSIGHTS_API_BASE);
125
124
  if (!base)
126
125
  return false;
@@ -128,12 +127,12 @@ export async function uploadInsightsBundle() {
128
127
  if (ok) {
129
128
  const records = Array.isArray(bundle.records) ? bundle.records : [];
130
129
  for (const row of records)
131
- localStore.markInsightUploaded(row.id);
130
+ await store.markInsightUploaded(row.id);
132
131
  }
133
132
  return ok;
134
133
  }
135
- export function scheduleInsightsFlush() {
136
- void flushInsightsOutbox().catch((err) => {
134
+ export function scheduleInsightsFlush(store) {
135
+ void flushInsightsOutbox(store).catch((err) => {
137
136
  console.warn('[insights] flush crashed', err);
138
137
  });
139
138
  }
@@ -1,5 +1,6 @@
1
- export declare function recordInsightEvent(eventName: string, properties?: Record<string, unknown>): void;
2
- export declare function recordInsightTurn(input: {
1
+ import type { ScopedStore } from '../storage/scoped-store.js';
2
+ export declare function recordInsightEvent(store: ScopedStore, eventName: string, properties?: Record<string, unknown>): Promise<void>;
3
+ export declare function recordInsightTurn(store: ScopedStore, input: {
3
4
  chatId: string;
4
5
  mode?: string;
5
6
  modelId?: string | null;
@@ -8,10 +9,10 @@ export declare function recordInsightTurn(input: {
8
9
  toolNames?: unknown;
9
10
  userText?: string;
10
11
  assistantText?: string;
11
- }): void;
12
- export declare function recordInsightProfile(profile: {
12
+ }): Promise<void>;
13
+ export declare function recordInsightProfile(store: ScopedStore, profile: {
13
14
  displayName?: string;
14
15
  email?: string;
15
16
  company?: string;
16
17
  note?: string;
17
- }): void;
18
+ }): Promise<void>;
@@ -1,18 +1,17 @@
1
1
  import { redactInsightText, toolNamesOnly } from '../storage/insights-redact.js';
2
- import { localStore } from '../storage/index.js';
3
2
  import { scheduleInsightsFlush } from './flush.js';
4
- export function recordInsightEvent(eventName, properties = {}) {
3
+ export async function recordInsightEvent(store, eventName, properties = {}) {
5
4
  try {
6
- localStore.enqueueInsight('event', { eventName, properties });
7
- scheduleInsightsFlush();
5
+ await store.enqueueInsight('event', { eventName, properties });
6
+ scheduleInsightsFlush(store);
8
7
  }
9
8
  catch (err) {
10
9
  console.warn('[insights] event enqueue failed', err);
11
10
  }
12
11
  }
13
- export function recordInsightTurn(input) {
12
+ export async function recordInsightTurn(store, input) {
14
13
  try {
15
- localStore.enqueueInsight('turn', {
14
+ await store.enqueueInsight('turn', {
16
15
  chatId: input.chatId,
17
16
  mode: input.mode === 'plan' ? 'plan' : 'agent',
18
17
  modelId: typeof input.modelId === 'string' ? input.modelId.slice(0, 128) : undefined,
@@ -22,21 +21,21 @@ export function recordInsightTurn(input) {
22
21
  userText: redactInsightText(input.userText, 4000),
23
22
  assistantText: redactInsightText(input.assistantText, 8000),
24
23
  });
25
- scheduleInsightsFlush();
24
+ scheduleInsightsFlush(store);
26
25
  }
27
26
  catch (err) {
28
27
  console.warn('[insights] turn enqueue failed', err);
29
28
  }
30
29
  }
31
- export function recordInsightProfile(profile) {
30
+ export async function recordInsightProfile(store, profile) {
32
31
  try {
33
- localStore.enqueueInsight('profile', {
32
+ await store.enqueueInsight('profile', {
34
33
  displayName: redactInsightText(profile.displayName, 128),
35
34
  email: redactInsightText(profile.email, 191),
36
35
  company: redactInsightText(profile.company, 191),
37
36
  note: redactInsightText(profile.note, 500),
38
37
  });
39
- scheduleInsightsFlush();
38
+ scheduleInsightsFlush(store);
40
39
  }
41
40
  catch (err) {
42
41
  console.warn('[insights] profile enqueue failed', err);
@@ -1,10 +1,14 @@
1
- import { type LlmSettings } from '../storage/index.js';
1
+ import type { LlmSettings, ScopedStore } from '../storage/index.js';
2
2
  import { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor } from '../sidecar/handle.js';
3
3
  export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending, whenSidecarSupervisor };
4
4
  import type { LlmGenerateRequest, LlmGenerateResult, LlmGenerateStreamRequest } from './types.js';
5
5
  export declare class LlmService {
6
6
  private provider;
7
7
  private cachedConfigSignature;
8
+ private settings;
9
+ private store;
10
+ /** Initializes the synchronous runtime cache after storage has loaded. */
11
+ initialize(settings: LlmSettings, store: Pick<ScopedStore, 'setLlmSettings'>): void;
8
12
  listModels(): Promise<string[]>;
9
13
  generate(request: Omit<LlmGenerateRequest, 'model'> & {
10
14
  model?: string;
@@ -16,7 +20,7 @@ export declare class LlmService {
16
20
  model?: string;
17
21
  }): Promise<LlmGenerateResult>;
18
22
  getSettings(): LlmSettings;
19
- setSettings(next: LlmSettings): LlmSettings;
23
+ setSettings(next: LlmSettings, store?: Pick<ScopedStore, "setLlmSettings"> | null): Promise<LlmSettings>;
20
24
  private getProvider;
21
25
  }
22
26
  export declare const llmService: LlmService;
package/dist/llm/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { localStore } from '../storage/index.js';
2
1
  import { sidecarWireProvider } from '../storage/llm-settings.js';
3
2
  import { OllamaProvider } from './ollama.js';
4
3
  import { OpenAICompatProvider } from './openai-compat.js';
@@ -11,6 +10,15 @@ export { getSidecarSupervisor, setSidecarSupervisor, setSidecarSupervisorPending
11
10
  export class LlmService {
12
11
  provider = null;
13
12
  cachedConfigSignature = '';
13
+ settings = null;
14
+ store = null;
15
+ /** Initializes the synchronous runtime cache after storage has loaded. */
16
+ initialize(settings, store) {
17
+ this.settings = settings;
18
+ this.store = store;
19
+ this.provider = null;
20
+ this.cachedConfigSignature = '';
21
+ }
14
22
  async listModels() {
15
23
  const provider = this.getProvider();
16
24
  return provider.listModels();
@@ -51,15 +59,17 @@ export class LlmService {
51
59
  return result;
52
60
  }
53
61
  getSettings() {
54
- return localStore.getLlmSettings() || {
55
- provider: 'ollama',
56
- model: 'llama3.1:8b',
57
- baseUrl: 'http://127.0.0.1:11434',
58
- temperature: 0.3,
59
- };
62
+ if (!this.settings) {
63
+ throw new Error('LlmService must be initialized after storage');
64
+ }
65
+ return this.settings;
60
66
  }
61
- setSettings(next) {
62
- const saved = localStore.setLlmSettings(next);
67
+ async setSettings(next, store = this.store) {
68
+ if (!store) {
69
+ throw new Error('LlmService must be initialized after storage');
70
+ }
71
+ const saved = await store.setLlmSettings(next);
72
+ this.settings = saved;
63
73
  this.provider = null;
64
74
  this.cachedConfigSignature = '';
65
75
  return saved;