@emqo/claudebridge 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,14 @@ Instead of hardcoded commands, ClaudeBridge injects a **skill document** into Cl
38
38
  - **Parallel Execution**: Multiple `claude` instances running simultaneously (`max_parallel` config)
39
39
  - **Observability**: `/status` command shows task queue, chain progress, and execution stats
40
40
 
41
+ ### v0.10.0: Dispatcher Architecture (Master-Worker Sessions)
42
+
43
+ - **Dispatcher (Master Session)**: Every user has a single dispatcher that receives all messages and routes them to the correct sub-session. Users never interact with sub-sessions directly
44
+ - **Intelligent Routing**: Fast path ($0) for 0-1 active sessions; Claude-powered classification with user memories + session summaries for 2+ sessions
45
+ - **Session Summaries**: Each sub-session maintains an auto-generated summary, giving the dispatcher context about what each conversation is doing
46
+ - **Memory-Aware Dispatch**: Dispatcher sees user memories + all active session summaries when classifying, enabling accurate routing even for ambiguous messages
47
+ - **Concurrent Sub-Sessions**: Multiple sub-sessions execute in parallel with per-session locks
48
+
41
49
  ## Quick Start
42
50
 
43
51
  ### Global Install (npm)
@@ -91,6 +99,11 @@ agent:
91
99
  max_memories: 50
92
100
  skill:
93
101
  enabled: true
102
+ session:
103
+ enabled: true
104
+ max_per_user: 3
105
+ idle_timeout_minutes: 30
106
+ dispatcher_budget: 0.05
94
107
 
95
108
  workspace:
96
109
  base_dir: "./workspaces"
@@ -231,11 +244,13 @@ src/
231
244
  ctl.ts claudebridge-ctl: memory/task/reminder/auto ops via SQLite
232
245
  webhook.ts HTTP server + GitHub webhooks + cron scheduler
233
246
  core/
234
- agent.ts Claude CLI subprocess spawner (runStream + runParallel)
247
+ agent.ts Claude CLI subprocess spawner + session summary sync
235
248
  config.ts YAML config with env fallback
236
249
  keys.ts Endpoint round-robin with cooldown
237
- lock.ts Per-user concurrency mutex (Redis or in-memory)
250
+ lock.ts Per-user/per-session concurrency mutex (Redis or in-memory)
238
251
  store.ts SQLite (WAL): sessions, usage, history, memories, tasks
252
+ router.ts Dispatcher: message routing with memories + session summaries
253
+ session.ts Sub-session lifecycle management
239
254
  permissions.ts Whitelist access control
240
255
  markdown.ts Markdown → Telegram MarkdownV2
241
256
  i18n.ts Internationalization (en/zh)
@@ -250,13 +265,16 @@ src/
250
265
  ### Data Flow
251
266
 
252
267
  ```
253
- User message → Adapter → Access check → Claude subprocess → Stream response back
254
-
255
- Reads skill doc → calls ctl via Bash
256
-
257
- SQLite: memories, tasks, reminders
258
-
259
- Bridge timers: auto-execute, remind, approve
268
+ User message → Adapter → Access check
269
+
270
+ Dispatcher (master session)
271
+ ├─ Fast path: 0-1 sessions → direct route ($0)
272
+ └─ Classify: 2+ sessions → Claude call (memories + summaries)
273
+
274
+ Sub-session execution (per-session lock)
275
+ ├─ Inject memories + skill doc → spawn claude CLI
276
+ ├─ Stream response back to adapter
277
+ └─ Post: save history, sync summary, auto-summarize to shared memory
260
278
  ```
261
279
 
262
280
  ## Prerequisites
@@ -305,6 +323,14 @@ MIT
305
323
  - **并行执行**:多个 `claude` 实例同时运行(`max_parallel` 配置)
306
324
  - **可观测性**:`/status` 命令显示任务队列、链路进度和执行统计
307
325
 
326
+ ### v0.10.0:Dispatcher 架构(主从会话)
327
+
328
+ - **Dispatcher(主会话)**:每个用户有一个 Dispatcher 接收所有消息并路由到正确的子会话,用户无需感知子会话的存在
329
+ - **智能路由**:0-1 个活跃会话走快速路径($0);2+ 个会话时 Claude 分类器携带用户记忆 + 会话摘要进行判断
330
+ - **会话摘要**:每个子会话自动生成摘要,让 Dispatcher 了解每个对话在做什么
331
+ - **记忆感知分发**:Dispatcher 分类时可见用户记忆 + 所有活跃会话摘要,即使模糊消息也能准确路由
332
+ - **并发子会话**:多个子会话并行执行,per-session 锁保证安全
333
+
308
334
  ## 快速开始
309
335
 
310
336
  ### 全局安装(npm)
@@ -37,7 +37,7 @@ agent:
37
37
  enabled: true # Enable multi-session (concurrent conversations per user)
38
38
  max_per_user: 3 # Max concurrent sub-sessions per user
39
39
  idle_timeout_minutes: 30 # Auto-close idle sub-sessions after this time
40
- classifier_budget: 0.05 # Max budget for routing classifier (Tier 3)
40
+ dispatcher_budget: 0.05 # Max budget for dispatcher classifier
41
41
  classifier_model: "" # Model for classifier (empty = use default)
42
42
 
43
43
  workspace:
@@ -3,7 +3,7 @@ import { Store } from "./store.js";
3
3
  import { AccessControl } from "./permissions.js";
4
4
  import { EndpointRotator } from "./keys.js";
5
5
  import { SessionManager } from "./session.js";
6
- import { SessionRouter } from "./router.js";
6
+ import { Dispatcher } from "./router.js";
7
7
  export interface AgentResponse {
8
8
  text: string;
9
9
  sessionId: string;
@@ -19,7 +19,7 @@ export declare class AgentEngine {
19
19
  private lock;
20
20
  private rotator;
21
21
  private sessionMgr;
22
- private router;
22
+ private dispatcher;
23
23
  private sessionExpiryTimer?;
24
24
  access: AccessControl;
25
25
  constructor(config: Config, store: Store);
@@ -32,7 +32,7 @@ export declare class AgentEngine {
32
32
  getEndpointCount(): number;
33
33
  getMaxParallel(): number;
34
34
  getSessionManager(): SessionManager;
35
- getRouter(): SessionRouter;
35
+ getDispatcher(): Dispatcher;
36
36
  getWorkDir(userId: string): string;
37
37
  /** @deprecated Use isSessionLocked() for multi-session mode */
38
38
  isLocked(userId: string): boolean;
@@ -68,5 +68,6 @@ export declare class AgentEngine {
68
68
  private _execute;
69
69
  /** Parallel execution without session resume. Thin wrapper around _spawnAgent. */
70
70
  private _executeNoSession;
71
+ private _syncSessionSummary;
71
72
  private _autoSummarize;
72
73
  }
@@ -6,7 +6,7 @@ import { AccessControl } from "./permissions.js";
6
6
  import { EndpointRotator } from "./keys.js";
7
7
  import { generateSkillDoc } from "../skills/bridge.js";
8
8
  import { SessionManager } from "./session.js";
9
- import { SessionRouter } from "./router.js";
9
+ import { Dispatcher } from "./router.js";
10
10
  import { log as rootLog } from "./logger.js";
11
11
  import { getProvider } from "../providers/registry.js";
12
12
  const log = rootLog.child("agent");
@@ -16,7 +16,7 @@ export class AgentEngine {
16
16
  lock;
17
17
  rotator;
18
18
  sessionMgr;
19
- router;
19
+ dispatcher;
20
20
  sessionExpiryTimer;
21
21
  access;
22
22
  constructor(config, store) {
@@ -26,7 +26,7 @@ export class AgentEngine {
26
26
  this.access = new AccessControl(config.access.allowed_users, config.access.allowed_groups);
27
27
  this.rotator = new EndpointRotator(config.endpoints);
28
28
  this.sessionMgr = new SessionManager(store, config.agent.session);
29
- this.router = new SessionRouter(this.sessionMgr, this.rotator, config.agent.session);
29
+ this.dispatcher = new Dispatcher(this.sessionMgr, this.rotator, config.agent.session, store);
30
30
  // Periodic idle session expiry (every 5 min)
31
31
  this.sessionExpiryTimer = setInterval(() => {
32
32
  this.sessionMgr.expireIdle();
@@ -53,8 +53,8 @@ export class AgentEngine {
53
53
  getSessionManager() {
54
54
  return this.sessionMgr;
55
55
  }
56
- getRouter() {
57
- return this.router;
56
+ getDispatcher() {
57
+ return this.dispatcher;
58
58
  }
59
59
  getWorkDir(userId) {
60
60
  if (!this.config.workspace.isolation) {
@@ -80,8 +80,8 @@ export class AgentEngine {
80
80
  * Routes to the correct sub-session and executes concurrently.
81
81
  */
82
82
  async handleUserMessage(userId, prompt, platform, chatId, replyToMsgId, onChunk, overrideTimeoutMs) {
83
- // 1. Route
84
- const decision = await this.router.route(userId, platform, chatId, prompt, replyToMsgId);
83
+ // 1. Dispatch
84
+ const decision = await this.dispatcher.dispatch(userId, platform, chatId, prompt, replyToMsgId);
85
85
  // 2. Create or get sub-session
86
86
  let subSession;
87
87
  if (decision.action === "create") {
@@ -121,6 +121,8 @@ export class AgentEngine {
121
121
  // 6. Auto-summarize
122
122
  if (this.config.agent.memory?.auto_summary)
123
123
  this._autoSummarize(userId, prompt, res.text);
124
+ // 7. Sync sub-session summary for dispatcher context
125
+ this._syncSessionSummary(subSession, prompt, res.text);
124
126
  return { ...res, subSessionId: subSession.id, label: subSession.label };
125
127
  }
126
128
  /**
@@ -336,6 +338,47 @@ export class AgentEngine {
336
338
  logLabel: "parallel", verbose: false,
337
339
  });
338
340
  }
341
+ _syncSessionSummary(subSession, prompt, response) {
342
+ const ep = this.rotator.count
343
+ ? this.rotator.next()
344
+ : { name: "default", provider: "claude", model: "" };
345
+ const summaryPrompt = `Summarize this conversation exchange in 1-2 sentences for a dispatcher that routes messages. Focus on the topic/task being discussed.\n\nUser: ${prompt.slice(0, 300)}\nAssistant: ${response.slice(0, 500)}`;
346
+ const args = ["-p", summaryPrompt, "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.02"];
347
+ if (ep.model)
348
+ args.push("--model", ep.model);
349
+ const env = { ...process.env };
350
+ const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
351
+ child.stdin.end();
352
+ const killTimer = setTimeout(() => { try {
353
+ child.kill("SIGTERM");
354
+ }
355
+ catch { } }, 30000);
356
+ let result = "";
357
+ let buffer = "";
358
+ child.stdout.on("data", (data) => {
359
+ buffer += data.toString();
360
+ const lines = buffer.split("\n");
361
+ buffer = lines.pop() || "";
362
+ for (const line of lines) {
363
+ if (!line.trim())
364
+ continue;
365
+ try {
366
+ const msg = JSON.parse(line);
367
+ if (msg.type === "result" && msg.result)
368
+ result = msg.result;
369
+ }
370
+ catch { }
371
+ }
372
+ });
373
+ child.on("close", () => {
374
+ clearTimeout(killTimer);
375
+ if (result && result.length > 0) {
376
+ this.sessionMgr.updateSummary(subSession.id, result.trim().slice(0, 200));
377
+ log.info("session summary synced", { sessionId: subSession.id.slice(0, 8) });
378
+ }
379
+ });
380
+ child.on("error", (err) => { log.warn("session summary error", { error: err.message }); });
381
+ }
339
382
  _autoSummarize(userId, prompt, response) {
340
383
  const ep = this.rotator.count
341
384
  ? this.rotator.next()
@@ -1,25 +1,29 @@
1
1
  import { SessionManager } from "./session.js";
2
2
  import { EndpointRotator } from "./keys.js";
3
3
  import { SessionConfig } from "./config.js";
4
+ import { Store } from "./store.js";
4
5
  export interface RouterDecision {
5
6
  action: "route" | "create";
6
7
  subSessionId?: string;
7
8
  label?: string;
8
9
  }
9
- export declare class SessionRouter {
10
+ export declare class Dispatcher {
10
11
  private sessionMgr;
11
12
  private rotator;
12
13
  private config;
13
- constructor(sessionMgr: SessionManager, rotator: EndpointRotator, config: SessionConfig);
14
+ private store;
15
+ constructor(sessionMgr: SessionManager, rotator: EndpointRotator, config: SessionConfig, store: Store);
14
16
  /**
15
- * 3-tier routing:
16
- * Tier 1: reply-to → direct route ($0)
17
- * Tier 2: 0-1 active sessions bypass ($0)
18
- * Tier 3: 2+ active sessions Claude classifier (~$0.002)
17
+ * Dispatch user message:
18
+ * Fast path: reply-to → direct route ($0)
19
+ * Fast path: 0 active → create ($0)
20
+ * Fast path: 1 active → route ($0)
21
+ * Classify: 2+ active → Claude classifier with memories + summaries
19
22
  */
20
- route(userId: string, platform: string, chatId: string, messageText: string, replyToMsgId?: string): Promise<RouterDecision>;
21
- /** Single-turn Claude call to classify which session a message belongs to */
23
+ dispatch(userId: string, platform: string, chatId: string, messageText: string, replyToMsgId?: string): Promise<RouterDecision>;
24
+ /** Classify with user memories + sub-session summaries for context */
22
25
  private _classify;
23
- /** Spawn claude CLI for single-turn classification (no tools, no session) */
26
+ /** Spawn claude CLI for single-turn classification */
24
27
  private _callClassifier;
25
28
  }
29
+ export { Dispatcher as SessionRouter };
@@ -1,23 +1,26 @@
1
1
  import { spawn } from "child_process";
2
2
  import { log as rootLog } from "./logger.js";
3
- const log = rootLog.child("router");
4
- export class SessionRouter {
3
+ const log = rootLog.child("dispatcher");
4
+ export class Dispatcher {
5
5
  sessionMgr;
6
6
  rotator;
7
7
  config;
8
- constructor(sessionMgr, rotator, config) {
8
+ store;
9
+ constructor(sessionMgr, rotator, config, store) {
9
10
  this.sessionMgr = sessionMgr;
10
11
  this.rotator = rotator;
11
12
  this.config = config;
13
+ this.store = store;
12
14
  }
13
15
  /**
14
- * 3-tier routing:
15
- * Tier 1: reply-to → direct route ($0)
16
- * Tier 2: 0-1 active sessions bypass ($0)
17
- * Tier 3: 2+ active sessions Claude classifier (~$0.002)
16
+ * Dispatch user message:
17
+ * Fast path: reply-to → direct route ($0)
18
+ * Fast path: 0 active → create ($0)
19
+ * Fast path: 1 active → route ($0)
20
+ * Classify: 2+ active → Claude classifier with memories + summaries
18
21
  */
19
- async route(userId, platform, chatId, messageText, replyToMsgId) {
20
- // Tier 1: reply-to routing
22
+ async dispatch(userId, platform, chatId, messageText, replyToMsgId) {
23
+ // Fast path: reply-to routing
21
24
  if (replyToMsgId) {
22
25
  const sessId = this.sessionMgr.getSessionByMessage(replyToMsgId, chatId);
23
26
  if (sessId) {
@@ -26,9 +29,8 @@ export class SessionRouter {
26
29
  return { action: "route", subSessionId: sessId };
27
30
  }
28
31
  }
29
- // reply-to pointed to closed/expired session — fall through to Tier 2/3
30
32
  }
31
- // Tier 2: 0-1 active sessions → direct
33
+ // Fast path: 0-1 active sessions
32
34
  const active = this.sessionMgr.getActive(userId, platform);
33
35
  if (active.length === 0) {
34
36
  return { action: "create", label: messageText.slice(0, 50) };
@@ -36,49 +38,67 @@ export class SessionRouter {
36
38
  if (active.length === 1) {
37
39
  return { action: "route", subSessionId: active[0].id };
38
40
  }
39
- // Tier 3: 2+ sessions Claude classifier
41
+ // 2+ sessions: classify with memories + summaries
40
42
  return await this._classify(userId, platform, messageText, active);
41
43
  }
42
- /** Single-turn Claude call to classify which session a message belongs to */
44
+ /** Classify with user memories + sub-session summaries for context */
43
45
  async _classify(userId, platform, text, sessions) {
44
46
  try {
47
+ // Gather context
48
+ const memories = this.store.getMemories(userId);
49
+ const summaries = this.sessionMgr.getSummaries(userId, platform);
45
50
  const sessionList = sessions
46
51
  .map(s => {
47
52
  const ago = Math.round((Date.now() - s.lastActiveAt) / 60000);
48
- return `[${s.id.slice(0, 8)}] "${s.label || "(no topic)"}" (${ago}min ago)`;
53
+ const sum = summaries.find(x => x.id === s.id);
54
+ const summaryText = sum?.summary ? ` | Summary: ${sum.summary}` : "";
55
+ return `[${s.id.slice(0, 8)}] "${s.label || "(no topic)"}" (${ago}min ago${summaryText})`;
49
56
  })
50
57
  .join("\n");
51
- const prompt = `You are a message router. Active conversations:\n${sessionList}\n\nUser message: "${text.slice(0, 200)}"\n\nReply with ONLY the 8-char session ID to route to, or "new" for a new conversation. No explanation.`;
58
+ const memoryBlock = memories.length
59
+ ? `\nUser context:\n${memories.slice(0, 10).map(m => `- ${m.content}`).join("\n")}\n`
60
+ : "";
61
+ const prompt = `You are a message dispatcher. Route the user's message to the correct conversation, or decide to create a new one, or handle a management request.
62
+ ${memoryBlock}
63
+ Active conversations:
64
+ ${sessionList}
65
+
66
+ User message: "${text.slice(0, 300)}"
67
+
68
+ Reply with ONLY one of:
69
+ - An 8-char session ID to route to
70
+ - "new" to create a new conversation
71
+
72
+ No explanation.`;
52
73
  const result = await this._callClassifier(prompt);
53
- const cleaned = result.trim().toLowerCase();
54
- if (cleaned === "new") {
74
+ const cleaned = result.trim();
75
+ if (cleaned.toLowerCase() === "new") {
55
76
  return { action: "create", label: text.slice(0, 50) };
56
77
  }
57
78
  // Match against active sessions (first 8 chars of ID)
58
- const match = sessions.find(s => s.id.slice(0, 8) === cleaned);
79
+ const match = sessions.find(s => s.id.slice(0, 8) === cleaned.toLowerCase());
59
80
  if (match) {
60
81
  return { action: "route", subSessionId: match.id };
61
82
  }
62
- // Fallback: if classifier returned something unexpected, route to most recently active
83
+ // Fallback: route to most recently active
63
84
  log.warn("classifier returned unexpected, falling back", { result: cleaned });
64
85
  return { action: "route", subSessionId: sessions[0].id };
65
86
  }
66
87
  catch (err) {
67
- // Classifier failed — fallback: create new session
68
88
  log.warn("classifier error, creating new session", { error: err.message });
69
89
  return { action: "create", label: text.slice(0, 50) };
70
90
  }
71
91
  }
72
- /** Spawn claude CLI for single-turn classification (no tools, no session) */
92
+ /** Spawn claude CLI for single-turn classification */
73
93
  _callClassifier(prompt) {
74
94
  return new Promise((resolve, reject) => {
95
+ const budget = this.config.dispatcher_budget ?? this.config.classifier_budget ?? 0.05;
75
96
  const args = ["-p", prompt, "--output-format", "stream-json", "--max-turns", "1"];
76
- if (this.config.classifier_budget)
77
- args.push("--max-budget-usd", String(this.config.classifier_budget));
97
+ if (budget)
98
+ args.push("--max-budget-usd", String(budget));
78
99
  if (this.config.classifier_model)
79
100
  args.push("--model", this.config.classifier_model);
80
101
  const env = { ...process.env };
81
- // Use the first available endpoint for the classifier model
82
102
  if (this.rotator.count) {
83
103
  const ep = this.rotator.next();
84
104
  if (!this.config.classifier_model && ep.model)
@@ -123,3 +143,5 @@ export class SessionRouter {
123
143
  });
124
144
  }
125
145
  }
146
+ // Backward compatibility alias
147
+ export { Dispatcher as SessionRouter };
@@ -16,7 +16,7 @@ declare const SessionConfigSchema: z.ZodObject<{
16
16
  enabled: z.ZodDefault<z.ZodBoolean>;
17
17
  max_per_user: z.ZodDefault<z.ZodNumber>;
18
18
  idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
19
- classifier_budget: z.ZodDefault<z.ZodNumber>;
19
+ dispatcher_budget: z.ZodDefault<z.ZodNumber>;
20
20
  classifier_model: z.ZodDefault<z.ZodString>;
21
21
  }, z.core.$strip>;
22
22
  declare const AgentConfigSchema: z.ZodObject<{
@@ -40,7 +40,7 @@ declare const AgentConfigSchema: z.ZodObject<{
40
40
  enabled: z.ZodDefault<z.ZodBoolean>;
41
41
  max_per_user: z.ZodDefault<z.ZodNumber>;
42
42
  idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
43
- classifier_budget: z.ZodDefault<z.ZodNumber>;
43
+ dispatcher_budget: z.ZodDefault<z.ZodNumber>;
44
44
  classifier_model: z.ZodDefault<z.ZodString>;
45
45
  }, z.core.$strip>>;
46
46
  }, z.core.$strip>;
@@ -108,7 +108,7 @@ export declare const ConfigSchema: z.ZodObject<{
108
108
  enabled: z.ZodDefault<z.ZodBoolean>;
109
109
  max_per_user: z.ZodDefault<z.ZodNumber>;
110
110
  idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
111
- classifier_budget: z.ZodDefault<z.ZodNumber>;
111
+ dispatcher_budget: z.ZodDefault<z.ZodNumber>;
112
112
  classifier_model: z.ZodDefault<z.ZodString>;
113
113
  }, z.core.$strip>>;
114
114
  }, z.core.$strip>>;
@@ -16,7 +16,7 @@ const SessionConfigSchema = z.object({
16
16
  enabled: z.boolean().default(true),
17
17
  max_per_user: z.number().int().positive().default(3),
18
18
  idle_timeout_minutes: z.number().positive().default(30),
19
- classifier_budget: z.number().nonnegative().default(0.05),
19
+ dispatcher_budget: z.number().nonnegative().default(0.05),
20
20
  classifier_model: z.string().default(""),
21
21
  });
22
22
  const AgentConfigSchema = z.object({
@@ -7,6 +7,7 @@ export interface SubSession {
7
7
  chatId: string;
8
8
  claudeSessionId: string | null;
9
9
  label: string;
10
+ summary: string;
10
11
  status: "active" | "idle" | "expired" | "closed";
11
12
  createdAt: number;
12
13
  lastActiveAt: number;
@@ -47,4 +48,13 @@ export declare class SessionManager {
47
48
  isUsable(session: SubSession): boolean;
48
49
  /** Get all sub-sessions for a user (all statuses) */
49
50
  getAll(userId: string): SubSession[];
51
+ /** Update the summary of a sub-session */
52
+ updateSummary(sessionId: string, summary: string): void;
53
+ /** Get summaries of active sub-sessions for dispatcher context */
54
+ getSummaries(userId: string, platform: string): {
55
+ id: string;
56
+ label: string;
57
+ summary: string;
58
+ lastActiveAt: number;
59
+ }[];
50
60
  }
@@ -10,6 +10,7 @@ function toSubSession(row) {
10
10
  chatId: row.chat_id,
11
11
  claudeSessionId: row.claude_session_id ?? null,
12
12
  label: row.label,
13
+ summary: row.summary ?? "",
13
14
  status: row.status,
14
15
  createdAt: row.created_at,
15
16
  lastActiveAt: row.last_active_at,
@@ -97,4 +98,14 @@ export class SessionManager {
97
98
  getAll(userId) {
98
99
  return this.store.getAllSubSessions(userId).map(toSubSession);
99
100
  }
101
+ /** Update the summary of a sub-session */
102
+ updateSummary(sessionId, summary) {
103
+ this.store.updateSubSessionSummary(sessionId, summary);
104
+ }
105
+ /** Get summaries of active sub-sessions for dispatcher context */
106
+ getSummaries(userId, platform) {
107
+ return this.store.getSubSessionSummaries(userId, platform).map(r => ({
108
+ id: r.id, label: r.label, summary: r.summary, lastActiveAt: r.last_active_at,
109
+ }));
110
+ }
100
111
  }
@@ -145,4 +145,11 @@ export declare class Store {
145
145
  message_count: number;
146
146
  total_cost: number;
147
147
  }[];
148
+ updateSubSessionSummary(id: string, summary: string): void;
149
+ getSubSessionSummaries(userId: string, platform: string): {
150
+ id: string;
151
+ label: string;
152
+ summary: string;
153
+ last_active_at: number;
154
+ }[];
148
155
  }
@@ -81,7 +81,7 @@ export class Store {
81
81
  PRIMARY KEY (platform_msg_id, chat_id)
82
82
  );
83
83
  `);
84
- // Schema migration: add parent_id, result, and scheduled_at columns
84
+ // Schema migration: add parent_id, result, scheduled_at, and sub_session summary columns
85
85
  try {
86
86
  this.db.exec("ALTER TABLE tasks ADD COLUMN parent_id INTEGER");
87
87
  }
@@ -94,6 +94,10 @@ export class Store {
94
94
  this.db.exec("ALTER TABLE tasks ADD COLUMN scheduled_at INTEGER");
95
95
  }
96
96
  catch { }
97
+ try {
98
+ this.db.exec("ALTER TABLE sub_sessions ADD COLUMN summary TEXT DEFAULT ''");
99
+ }
100
+ catch { }
97
101
  this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
98
102
  // Startup recovery: reset orphaned 'running' tasks back to 'auto' so they get re-executed
99
103
  const orphaned = this.db.prepare("SELECT id, description FROM tasks WHERE status = 'running'").all();
@@ -309,4 +313,10 @@ export class Store {
309
313
  getAllSubSessions(userId) {
310
314
  return this.db.prepare("SELECT * FROM sub_sessions WHERE user_id = ? ORDER BY last_active_at DESC").all(userId);
311
315
  }
316
+ updateSubSessionSummary(id, summary) {
317
+ this.db.prepare("UPDATE sub_sessions SET summary = ? WHERE id = ?").run(summary, id);
318
+ }
319
+ getSubSessionSummaries(userId, platform) {
320
+ return this.db.prepare("SELECT id, label, summary, last_active_at FROM sub_sessions WHERE user_id = ? AND platform = ? AND status IN ('active','idle') ORDER BY last_active_at DESC").all(userId, platform);
321
+ }
312
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emqo/claudebridge",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "Bridge claude CLI to chat platforms (Telegram, Discord) with scheduled auto-tasks, autonomous project management, HITL approval, conditional branching, webhook triggers, parallel execution, and observability",
5
5
  "main": "dist/index.js",
6
6
  "bin": {