lazy-gravity 0.6.2 → 0.7.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.
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectTelegramStartupChatId = selectTelegramStartupChatId;
4
+ function getBaseChatId(chatId) {
5
+ const sepIdx = chatId.indexOf('_');
6
+ return sepIdx > 0 ? chatId.slice(0, sepIdx) : chatId;
7
+ }
8
+ function normalizeTitle(title) {
9
+ return title.trim().replace(/^#/, '').toLowerCase();
10
+ }
11
+ function isGeneralChat(title) {
12
+ return normalizeTitle(title) === 'general';
13
+ }
14
+ async function selectTelegramStartupChatId(api, bindings) {
15
+ const candidates = [];
16
+ const seenResolvedIds = new Set();
17
+ for (const binding of bindings) {
18
+ const resolvedChatId = getBaseChatId(binding.chatId);
19
+ if (seenResolvedIds.has(resolvedChatId))
20
+ continue;
21
+ seenResolvedIds.add(resolvedChatId);
22
+ try {
23
+ const chat = await api.getChat(resolvedChatId);
24
+ candidates.push({
25
+ bindingChatId: binding.chatId,
26
+ resolvedChatId,
27
+ type: String(chat?.type ?? ''),
28
+ title: String(chat?.title ?? chat?.first_name ?? ''),
29
+ isDirectBinding: binding.chatId === resolvedChatId,
30
+ });
31
+ }
32
+ catch {
33
+ candidates.push({
34
+ bindingChatId: binding.chatId,
35
+ resolvedChatId,
36
+ type: '',
37
+ title: '',
38
+ isDirectBinding: binding.chatId === resolvedChatId,
39
+ });
40
+ }
41
+ }
42
+ if (candidates.length === 0)
43
+ return null;
44
+ const generalGroup = candidates.find((candidate) => candidate.type !== 'private' && isGeneralChat(candidate.title));
45
+ if (generalGroup)
46
+ return generalGroup.resolvedChatId;
47
+ const directGroup = candidates.find((candidate) => candidate.type !== 'private' && candidate.isDirectBinding);
48
+ if (directGroup)
49
+ return directGroup.resolvedChatId;
50
+ const privateChat = candidates.find((candidate) => candidate.type === 'private');
51
+ if (privateChat)
52
+ return privateChat.bindingChatId;
53
+ return candidates[0].resolvedChatId;
54
+ }
@@ -17,13 +17,15 @@ class ChatCommandHandler {
17
17
  channelManager;
18
18
  pool;
19
19
  workspaceService;
20
- constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, workspaceService, pool) {
20
+ resolveAccountForChannel;
21
+ constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, workspaceService, pool, resolveAccountForChannel) {
21
22
  this.chatSessionService = chatSessionService;
22
23
  this.chatSessionRepo = chatSessionRepo;
23
24
  this.bindingRepo = bindingRepo;
24
25
  this.channelManager = channelManager;
25
26
  this.workspaceService = workspaceService;
26
27
  this.pool = pool ?? null;
28
+ this.resolveAccountForChannel = resolveAccountForChannel ?? null;
27
29
  }
28
30
  /**
29
31
  * /new -- Create a new session channel under the category and start a new chat in Antigravity
@@ -39,19 +41,11 @@ class ChatCommandHandler {
39
41
  await interaction.editReply({ content: (0, i18n_1.t)('⚠️ Please execute in a text channel.') });
40
42
  return;
41
43
  }
42
- // Check if the current channel is under a project category
43
- const parentId = 'parentId' in channel ? channel.parentId : null;
44
- if (!parentId) {
45
- await interaction.editReply({
46
- content: (0, i18n_1.t)('⚠️ Please run in a project category channel.\nUse `/project` to create a project first.'),
47
- });
48
- return;
49
- }
50
- // Determine the project path
51
44
  const currentSession = this.chatSessionRepo.findByChannelId(interaction.channelId);
52
45
  const binding = this.bindingRepo.findByChannelId(interaction.channelId);
46
+ const parentId = currentSession?.categoryId ?? ('parentId' in channel ? channel.parentId : null);
53
47
  const workspaceName = currentSession?.workspacePath ?? binding?.workspacePath;
54
- if (!workspaceName) {
48
+ if (!parentId || !workspaceName) {
55
49
  await interaction.editReply({
56
50
  content: (0, i18n_1.t)('⚠️ Please run in a project category channel.\nUse `/project` to create a project first.'),
57
51
  });
@@ -61,9 +55,10 @@ class ChatCommandHandler {
61
55
  const workspacePath = this.workspaceService.getWorkspacePath(workspaceName);
62
56
  // Switch project (connect to the correct workbench page)
63
57
  let workspaceCdp;
58
+ const selectedAccount = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
64
59
  if (this.pool) {
65
60
  try {
66
- workspaceCdp = await this.pool.getOrConnect(workspacePath);
61
+ workspaceCdp = await this.pool.getOrConnect(workspacePath, { name: selectedAccount });
67
62
  }
68
63
  catch (e) {
69
64
  await interaction.editReply({
@@ -94,6 +89,7 @@ class ChatCommandHandler {
94
89
  categoryId: parentId,
95
90
  workspacePath: workspaceName,
96
91
  sessionNumber,
92
+ activeAccountName: selectedAccount,
97
93
  guildId: guild.id,
98
94
  });
99
95
  const embed = new discord_js_1.EmbedBuilder()
@@ -25,9 +25,10 @@ class JoinCommandHandler {
25
25
  client;
26
26
  extractionMode;
27
27
  responseTimeoutMs;
28
+ resolveAccountForChannel;
28
29
  /** Active ResponseMonitors per workspace (for AI response mirroring) */
29
30
  activeResponseMonitors = new Map();
30
- constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client, extractionMode, responseTimeoutMs) {
31
+ constructor(chatSessionService, chatSessionRepo, bindingRepo, channelManager, pool, workspaceService, client, extractionMode, responseTimeoutMs, resolveAccountForChannel) {
31
32
  this.chatSessionService = chatSessionService;
32
33
  this.chatSessionRepo = chatSessionRepo;
33
34
  this.bindingRepo = bindingRepo;
@@ -37,6 +38,7 @@ class JoinCommandHandler {
37
38
  this.client = client;
38
39
  this.extractionMode = extractionMode;
39
40
  this.responseTimeoutMs = responseTimeoutMs;
41
+ this.resolveAccountForChannel = resolveAccountForChannel ?? null;
40
42
  }
41
43
  /**
42
44
  * Resolve a project name (from DB) to its full absolute path.
@@ -59,9 +61,10 @@ class JoinCommandHandler {
59
61
  return;
60
62
  }
61
63
  const projectPath = this.resolveProjectPath(projectName);
64
+ const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
62
65
  let cdp;
63
66
  try {
64
- cdp = await this.pool.getOrConnect(projectPath);
67
+ cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
65
68
  }
66
69
  catch (e) {
67
70
  await interaction.editReply({
@@ -96,6 +99,7 @@ class JoinCommandHandler {
96
99
  return;
97
100
  }
98
101
  const projectPath = this.resolveProjectPath(projectName);
102
+ const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
99
103
  // Step 1: Check if a channel already exists for this session
100
104
  const existingSession = this.chatSessionRepo.findByDisplayName(projectName, selectedTitle);
101
105
  if (existingSession) {
@@ -119,7 +123,7 @@ class JoinCommandHandler {
119
123
  // Step 2: Connect to CDP
120
124
  let cdp;
121
125
  try {
122
- cdp = await this.pool.getOrConnect(projectPath);
126
+ cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
123
127
  }
124
128
  catch (e) {
125
129
  await interaction.editReply({ content: (0, i18n_1.t)(`⚠️ Failed to connect to project: ${e.message}`) });
@@ -149,11 +153,12 @@ class JoinCommandHandler {
149
153
  categoryId,
150
154
  workspacePath: projectName,
151
155
  sessionNumber,
156
+ activeAccountName: accountName,
152
157
  guildId: guild.id,
153
158
  });
154
159
  this.chatSessionRepo.updateDisplayName(newChannelId, selectedTitle);
155
160
  // Step 6: Start mirroring (routes dynamically to all bound session channels)
156
- this.startMirroring(bridge, cdp, projectName);
161
+ this.startMirroring(bridge, cdp, projectName, accountName);
157
162
  const embed = new discord_js_1.EmbedBuilder()
158
163
  .setTitle((0, i18n_1.t)('🔗 Joined Session'))
159
164
  .setDescription((0, i18n_1.t)(`Connected to: **${selectedTitle}**\n→ <#${newChannelId}>\n\n` +
@@ -177,7 +182,8 @@ class JoinCommandHandler {
177
182
  return;
178
183
  }
179
184
  const projectPath = this.resolveProjectPath(projectName);
180
- const detector = this.pool.getUserMessageDetector(projectName);
185
+ const accountName = this.resolveAccountForChannel?.(interaction.channelId, interaction.user.id) ?? 'default';
186
+ const detector = this.pool.getUserMessageDetector(projectName, accountName);
181
187
  if (detector?.isActive()) {
182
188
  // Turn OFF — stop user message detector and any active response monitor
183
189
  detector.stop();
@@ -197,7 +203,7 @@ class JoinCommandHandler {
197
203
  // Turn ON
198
204
  let cdp;
199
205
  try {
200
- cdp = await this.pool.getOrConnect(projectPath);
206
+ cdp = await this.pool.getOrConnect(projectPath, { name: accountName });
201
207
  }
202
208
  catch (e) {
203
209
  await interaction.editReply({
@@ -205,7 +211,7 @@ class JoinCommandHandler {
205
211
  });
206
212
  return;
207
213
  }
208
- this.startMirroring(bridge, cdp, projectName);
214
+ this.startMirroring(bridge, cdp, projectName, accountName);
209
215
  const embed = new discord_js_1.EmbedBuilder()
210
216
  .setTitle((0, i18n_1.t)('📡 Mirroring ON'))
211
217
  .setDescription((0, i18n_1.t)('PC-to-Discord message mirroring is now active.\n' +
@@ -222,11 +228,11 @@ class JoinCommandHandler {
222
228
  * channel via chatSessionRepo.findByDisplayName. Only explicitly joined
223
229
  * sessions (with a displayName binding) receive mirrored messages.
224
230
  */
225
- startMirroring(bridge, cdp, projectName) {
231
+ startMirroring(bridge, cdp, projectName, accountName) {
226
232
  // Force re-prime: stop existing detector so that ensureUserMessageDetector
227
233
  // creates a fresh one. This prevents the detector from treating the
228
234
  // new session's last message as a "new" user message after /join.
229
- const existing = this.pool.getUserMessageDetector(projectName);
235
+ const existing = this.pool.getUserMessageDetector(projectName, accountName);
230
236
  if (existing?.isActive()) {
231
237
  existing.stop();
232
238
  }
@@ -235,7 +241,7 @@ class JoinCommandHandler {
235
241
  .catch((err) => {
236
242
  logger_1.logger.error('[Mirror] Error routing mirrored message:', err);
237
243
  });
238
- });
244
+ }, accountName);
239
245
  }
240
246
  /**
241
247
  * Route a mirrored PC message to the correct Discord channel and
@@ -79,7 +79,14 @@ const projectCommand = new discord_js_1.SlashCommandBuilder()
79
79
  .addStringOption((option) => option
80
80
  .setName('name')
81
81
  .setDescription((0, i18n_1.t)('Name of the project to create'))
82
- .setRequired(true)));
82
+ .setRequired(true)))
83
+ .addSubcommand((sub) => sub
84
+ .setName('account')
85
+ .setDescription((0, i18n_1.t)('Display or change the Antigravity account bound to this project channel'))
86
+ .addStringOption((option) => option
87
+ .setName('name')
88
+ .setDescription((0, i18n_1.t)('Name of the account to bind to this project channel'))
89
+ .setRequired(false)));
83
90
  /** /new command definition (formerly /chat new, made into a standalone command) */
84
91
  const newCommand = new discord_js_1.SlashCommandBuilder()
85
92
  .setName('new')
@@ -118,6 +125,10 @@ const outputCommand = new discord_js_1.SlashCommandBuilder()
118
125
  .setName('format')
119
126
  .setDescription((0, i18n_1.t)('embed / plain (optional direct switch)'))
120
127
  .setRequired(false));
128
+ /** /account command definition */
129
+ const accountCommand = new discord_js_1.SlashCommandBuilder()
130
+ .setName('account')
131
+ .setDescription((0, i18n_1.t)('Select the Antigravity account for the current session'));
121
132
  /** /logs command definition */
122
133
  const logsCommand = new discord_js_1.SlashCommandBuilder()
123
134
  .setName('logs')
@@ -153,6 +164,7 @@ exports.slashCommands = [
153
164
  cleanupCommand,
154
165
  joinCommand,
155
166
  mirrorCommand,
167
+ accountCommand,
156
168
  outputCommand,
157
169
  pingCommand,
158
170
  logsCommand,
@@ -22,6 +22,7 @@ class WorkspaceCommandHandler {
22
22
  chatSessionRepo;
23
23
  workspaceService;
24
24
  channelManager;
25
+ onSessionChannelCreated;
25
26
  processingWorkspaces = new Set();
26
27
  /**
27
28
  * Filters out stale bindings where the Discord channel no longer exists.
@@ -58,11 +59,12 @@ class WorkspaceCommandHandler {
58
59
  }
59
60
  return validBindings;
60
61
  }
61
- constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager) {
62
+ constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager, onSessionChannelCreated) {
62
63
  this.bindingRepo = bindingRepo;
63
64
  this.chatSessionRepo = chatSessionRepo;
64
65
  this.workspaceService = workspaceService;
65
66
  this.channelManager = channelManager;
67
+ this.onSessionChannelCreated = onSessionChannelCreated;
66
68
  }
67
69
  /**
68
70
  * /project list -- Display project list via select menu
@@ -87,9 +89,16 @@ class WorkspaceCommandHandler {
87
89
  * Creates a category + session-1 channel and binds them.
88
90
  */
89
91
  async handleSelectMenu(interaction, guild) {
92
+ const respond = async (payload) => {
93
+ if (typeof interaction.editReply === 'function') {
94
+ await interaction.editReply(payload);
95
+ return;
96
+ }
97
+ await interaction.update(payload);
98
+ };
90
99
  const workspacePath = interaction.values[0];
91
100
  if (!this.workspaceService.exists(workspacePath)) {
92
- await interaction.update({
101
+ await respond({
93
102
  content: (0, i18n_1.t)(`❌ Project \`${workspacePath}\` not found.`),
94
103
  embeds: [],
95
104
  components: [],
@@ -109,7 +118,7 @@ class WorkspaceCommandHandler {
109
118
  `→ ${channelLinks}`)
110
119
  .addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
111
120
  .setTimestamp();
112
- await interaction.update({
121
+ await respond({
113
122
  embeds: [embed],
114
123
  components: [],
115
124
  });
@@ -117,7 +126,7 @@ class WorkspaceCommandHandler {
117
126
  }
118
127
  // Lock project being processed (prevent rapid repeated clicks)
119
128
  if (this.processingWorkspaces.has(workspacePath)) {
120
- await interaction.update({
129
+ await respond({
121
130
  content: (0, i18n_1.t)(`⏳ **${workspacePath}** is being created. Please wait.`),
122
131
  embeds: [],
123
132
  components: [],
@@ -148,6 +157,7 @@ class WorkspaceCommandHandler {
148
157
  sessionNumber,
149
158
  guildId: guild.id,
150
159
  });
160
+ await this.onSessionChannelCreated?.(workspacePath, channelId, interaction.channelId, interaction.user.id);
151
161
  const fullPath = this.workspaceService.getWorkspacePath(workspacePath);
152
162
  const embed = new discord_js_1.EmbedBuilder()
153
163
  .setTitle('📁 Projects')
@@ -156,7 +166,7 @@ class WorkspaceCommandHandler {
156
166
  `→ <#${channelId}>`)
157
167
  .addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
158
168
  .setTimestamp();
159
- await interaction.update({
169
+ await respond({
160
170
  embeds: [embed],
161
171
  components: [],
162
172
  });
@@ -231,6 +241,7 @@ class WorkspaceCommandHandler {
231
241
  sessionNumber,
232
242
  guildId: guild.id,
233
243
  });
244
+ await this.onSessionChannelCreated?.(name, channelId, interaction.channelId, interaction.user.id);
234
245
  const embed = new discord_js_1.EmbedBuilder()
235
246
  .setTitle('📁 Project Created')
236
247
  .setColor(0x00AA00)
@@ -249,9 +260,13 @@ class WorkspaceCommandHandler {
249
260
  */
250
261
  getWorkspaceForChannel(channelId) {
251
262
  const binding = this.bindingRepo.findByChannelId(channelId);
252
- if (!binding)
263
+ if (binding) {
264
+ return this.workspaceService.getWorkspacePath(binding.workspacePath);
265
+ }
266
+ const session = this.chatSessionRepo.findByChannelId(channelId);
267
+ if (!session)
253
268
  return undefined;
254
- return this.workspaceService.getWorkspacePath(binding.workspacePath);
269
+ return this.workspaceService.getWorkspacePath(session.workspacePath);
255
270
  }
256
271
  }
257
272
  exports.WorkspaceCommandHandler = WorkspaceCommandHandler;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AccountPreferenceRepository = void 0;
4
+ class AccountPreferenceRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ this.db.exec(`
9
+ CREATE TABLE IF NOT EXISTS account_preferences (
10
+ user_id TEXT PRIMARY KEY,
11
+ account_name TEXT NOT NULL,
12
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
13
+ )
14
+ `);
15
+ }
16
+ getAccountName(userId) {
17
+ const row = this.db.prepare('SELECT account_name FROM account_preferences WHERE user_id = ?').get(userId);
18
+ return row?.account_name ?? null;
19
+ }
20
+ setAccountName(userId, accountName) {
21
+ this.db.prepare(`
22
+ INSERT INTO account_preferences (user_id, account_name)
23
+ VALUES (?, ?)
24
+ ON CONFLICT(user_id)
25
+ DO UPDATE SET account_name = excluded.account_name, updated_at = datetime('now')
26
+ `).run(userId, accountName);
27
+ }
28
+ }
29
+ exports.AccountPreferenceRepository = AccountPreferenceRepository;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChannelPreferenceRepository = void 0;
4
+ class ChannelPreferenceRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ this.db.exec(`
9
+ CREATE TABLE IF NOT EXISTS channel_preferences (
10
+ channel_id TEXT PRIMARY KEY,
11
+ account_name TEXT,
12
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
13
+ )
14
+ `);
15
+ }
16
+ getAccountName(channelId) {
17
+ const row = this.db.prepare('SELECT account_name FROM channel_preferences WHERE channel_id = ?').get(channelId);
18
+ return row?.account_name ?? null;
19
+ }
20
+ setAccountName(channelId, accountName) {
21
+ this.db.prepare(`
22
+ INSERT INTO channel_preferences (channel_id, account_name)
23
+ VALUES (?, ?)
24
+ ON CONFLICT(channel_id)
25
+ DO UPDATE SET account_name = excluded.account_name, updated_at = datetime('now')
26
+ `).run(channelId, accountName);
27
+ }
28
+ }
29
+ exports.ChannelPreferenceRepository = ChannelPreferenceRepository;
@@ -19,25 +19,65 @@ class ChatSessionRepository {
19
19
  category_id TEXT NOT NULL,
20
20
  workspace_path TEXT NOT NULL,
21
21
  session_number INTEGER NOT NULL,
22
+ conversation_id TEXT,
23
+ active_account_name TEXT,
24
+ origin_account_name TEXT,
22
25
  display_name TEXT,
23
26
  is_renamed INTEGER NOT NULL DEFAULT 0,
24
27
  guild_id TEXT NOT NULL,
25
28
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
26
29
  )
27
30
  `);
31
+ const columns = this.db.prepare('PRAGMA table_info(chat_sessions)').all();
32
+ const hasActiveAccountName = columns.some((column) => column.name === 'active_account_name');
33
+ const hasConversationId = columns.some((column) => column.name === 'conversation_id');
34
+ if (!hasConversationId) {
35
+ this.db.exec('ALTER TABLE chat_sessions ADD COLUMN conversation_id TEXT');
36
+ }
37
+ if (!hasActiveAccountName) {
38
+ this.db.exec('ALTER TABLE chat_sessions ADD COLUMN active_account_name TEXT');
39
+ }
40
+ const hasOriginAccountName = columns.some((column) => column.name === 'origin_account_name');
41
+ if (!hasOriginAccountName) {
42
+ this.db.exec('ALTER TABLE chat_sessions ADD COLUMN origin_account_name TEXT');
43
+ }
44
+ const hasLegacyAccountName = columns.some((column) => column.name === 'account_name');
45
+ if (hasLegacyAccountName) {
46
+ this.db.exec(`
47
+ UPDATE chat_sessions
48
+ SET origin_account_name = account_name
49
+ WHERE origin_account_name IS NULL AND account_name IS NOT NULL
50
+ `);
51
+ this.db.exec(`
52
+ UPDATE chat_sessions
53
+ SET active_account_name = COALESCE(active_account_name, origin_account_name, account_name)
54
+ WHERE active_account_name IS NULL
55
+ `);
56
+ }
57
+ else {
58
+ this.db.exec(`
59
+ UPDATE chat_sessions
60
+ SET active_account_name = origin_account_name
61
+ WHERE active_account_name IS NULL AND origin_account_name IS NOT NULL
62
+ `);
63
+ }
28
64
  }
29
65
  create(input) {
66
+ const activeAccountName = input.activeAccountName ?? null;
30
67
  const stmt = this.db.prepare(`
31
- INSERT INTO chat_sessions (channel_id, category_id, workspace_path, session_number, guild_id)
32
- VALUES (?, ?, ?, ?, ?)
68
+ INSERT INTO chat_sessions (channel_id, category_id, workspace_path, session_number, active_account_name, origin_account_name, guild_id)
69
+ VALUES (?, ?, ?, ?, ?, NULL, ?)
33
70
  `);
34
- const result = stmt.run(input.channelId, input.categoryId, input.workspacePath, input.sessionNumber, input.guildId);
71
+ const result = stmt.run(input.channelId, input.categoryId, input.workspacePath, input.sessionNumber, activeAccountName, input.guildId);
35
72
  return {
36
73
  id: result.lastInsertRowid,
37
74
  channelId: input.channelId,
38
75
  categoryId: input.categoryId,
39
76
  workspacePath: input.workspacePath,
40
77
  sessionNumber: input.sessionNumber,
78
+ conversationId: null,
79
+ activeAccountName,
80
+ originAccountName: null,
41
81
  displayName: null,
42
82
  isRenamed: false,
43
83
  guildId: input.guildId,
@@ -67,6 +107,26 @@ class ChatSessionRepository {
67
107
  const result = this.db.prepare('UPDATE chat_sessions SET display_name = ?, is_renamed = 1 WHERE channel_id = ?').run(displayName, channelId);
68
108
  return result.changes > 0;
69
109
  }
110
+ setActiveAccountName(channelId, accountName) {
111
+ const result = this.db.prepare('UPDATE chat_sessions SET active_account_name = ? WHERE channel_id = ?').run(accountName, channelId);
112
+ return result.changes > 0;
113
+ }
114
+ setOriginAccountName(channelId, accountName) {
115
+ const result = this.db.prepare('UPDATE chat_sessions SET origin_account_name = ? WHERE channel_id = ?').run(accountName, channelId);
116
+ return result.changes > 0;
117
+ }
118
+ setConversationId(channelId, conversationId) {
119
+ const result = this.db.prepare('UPDATE chat_sessions SET conversation_id = ? WHERE channel_id = ?').run(conversationId, channelId);
120
+ return result.changes > 0;
121
+ }
122
+ initializeConversationId(channelId, conversationId) {
123
+ const result = this.db.prepare('UPDATE chat_sessions SET conversation_id = ? WHERE channel_id = ? AND conversation_id IS NULL').run(conversationId, channelId);
124
+ return result.changes > 0;
125
+ }
126
+ initializeOriginAccountName(channelId, accountName) {
127
+ const result = this.db.prepare('UPDATE chat_sessions SET origin_account_name = ? WHERE channel_id = ? AND origin_account_name IS NULL').run(accountName, channelId);
128
+ return result.changes > 0;
129
+ }
70
130
  /**
71
131
  * Find a session by display name within a workspace.
72
132
  * Returns the first match (most recent).
@@ -88,6 +148,9 @@ class ChatSessionRepository {
88
148
  categoryId: row.category_id,
89
149
  workspacePath: row.workspace_path,
90
150
  sessionNumber: row.session_number,
151
+ conversationId: row.conversation_id ?? null,
152
+ activeAccountName: row.active_account_name ?? row.account_name ?? null,
153
+ originAccountName: row.origin_account_name ?? null,
91
154
  displayName: row.display_name,
92
155
  isRenamed: row.is_renamed === 1,
93
156
  guildId: row.guild_id,
@@ -48,6 +48,19 @@ class TelegramBindingRepository {
48
48
  return undefined;
49
49
  return this.mapRow(row);
50
50
  }
51
+ /**
52
+ * Find binding by exact chat ID, or fall back to the base chat ID for topic-style IDs.
53
+ * Example: "12345_67" falls back to "12345" when the topic itself has no direct binding.
54
+ */
55
+ findByChatIdWithParentFallback(chatId) {
56
+ const exact = this.findByChatId(chatId);
57
+ if (exact)
58
+ return exact;
59
+ const underscoreIndex = chatId.indexOf('_');
60
+ if (underscoreIndex <= 0)
61
+ return undefined;
62
+ return this.findByChatId(chatId.slice(0, underscoreIndex));
63
+ }
51
64
  /**
52
65
  * Find bindings by workspace path
53
66
  */