@link-assistant/hive-mind 1.56.11 ā 1.56.13
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/github.lib.mjs +10 -61
- package/src/log-upload.lib.mjs +63 -24
- package/src/session-monitor.lib.mjs +32 -5
- package/src/telegram-bot-launcher.lib.mjs +21 -3
- package/src/telegram-bot.mjs +63 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.56.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca1ac93: Start Telegram work-session monitoring before Telegraf long polling can block startup code, and keep completed screen-isolated sessions in memory until their completion message is updated.
|
|
8
|
+
|
|
9
|
+
## 1.56.12
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 71e1ef5: Prevent `--attach-logs` from posting truncated fallback comments when full `gh-upload-log` uploads fail, and parse newer `gh-upload-log` repository output including shared-repository paths.
|
|
14
|
+
|
|
3
15
|
## 1.56.11
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.56.
|
|
3
|
+
"version": "1.56.13",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-issue-1680-session-monitoring.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
19
19
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
20
20
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
21
21
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
package/src/github.lib.mjs
CHANGED
|
@@ -614,7 +614,10 @@ ${logContent}
|
|
|
614
614
|
if (uploadResult.success) {
|
|
615
615
|
// Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks)
|
|
616
616
|
// Requirements: 1 chunk = direct raw link, >1 chunks = repo link
|
|
617
|
-
|
|
617
|
+
// Private repository raw URLs can contain short-lived tokens, so keep
|
|
618
|
+
// private uploads on the stable repository/tree page URL.
|
|
619
|
+
const useRawLogUrl = uploadResult.chunks === 1 && uploadResult.rawUrl && (isPublicRepo || uploadResult.type !== 'repository');
|
|
620
|
+
const logUrl = useRawLogUrl ? uploadResult.rawUrl : uploadResult.url;
|
|
618
621
|
const uploadTypeLabel = uploadResult.type === 'gist' ? 'Gist' : 'Repository';
|
|
619
622
|
const chunkInfo = uploadResult.chunks > 1 ? ` (${uploadResult.chunks} chunks)` : '';
|
|
620
623
|
|
|
@@ -752,10 +755,9 @@ ${sessionNote}
|
|
|
752
755
|
}
|
|
753
756
|
} else {
|
|
754
757
|
await log(' ā gh-upload-log failed');
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
return await attachTruncatedLog(options);
|
|
758
|
+
await log(' ā ļø Full log upload failed; not posting a truncated log because --attach-logs must preserve complete logs');
|
|
759
|
+
await log(` š Full log remains available locally at: ${logFile}`);
|
|
760
|
+
return false;
|
|
759
761
|
}
|
|
760
762
|
} catch (uploadError) {
|
|
761
763
|
reportError(uploadError, {
|
|
@@ -763,8 +765,9 @@ ${sessionNote}
|
|
|
763
765
|
level: 'error',
|
|
764
766
|
});
|
|
765
767
|
await log(` ā Error uploading log: ${uploadError.message}`);
|
|
766
|
-
|
|
767
|
-
|
|
768
|
+
await log(' ā ļø Full log upload failed; not posting a truncated log because --attach-logs must preserve complete logs');
|
|
769
|
+
await log(` š Full log remains available locally at: ${logFile}`);
|
|
770
|
+
return false;
|
|
768
771
|
}
|
|
769
772
|
} else {
|
|
770
773
|
// Comment fits within limit
|
|
@@ -777,60 +780,6 @@ ${sessionNote}
|
|
|
777
780
|
return false;
|
|
778
781
|
}
|
|
779
782
|
}
|
|
780
|
-
/**
|
|
781
|
-
* Helper to attach a truncated log when full log is too large
|
|
782
|
-
*/
|
|
783
|
-
async function attachTruncatedLog(options) {
|
|
784
|
-
const fs = (await use('fs')).promises;
|
|
785
|
-
const { logFile, targetType, targetNumber, owner, repo, $, log, sanitizeLogContent } = options;
|
|
786
|
-
|
|
787
|
-
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
788
|
-
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
789
|
-
|
|
790
|
-
const rawLogContent = await fs.readFile(logFile, 'utf8');
|
|
791
|
-
let logContent = await sanitizeLogContent(rawLogContent);
|
|
792
|
-
// Escape code blocks to prevent markdown breaking
|
|
793
|
-
logContent = escapeCodeBlocksInLog(logContent);
|
|
794
|
-
const logStats = await fs.stat(logFile);
|
|
795
|
-
|
|
796
|
-
const GITHUB_COMMENT_LIMIT = 65536;
|
|
797
|
-
const maxContentLength = GITHUB_COMMENT_LIMIT - 500;
|
|
798
|
-
const truncatedContent = logContent.substring(0, maxContentLength) + '\n\n[... Log truncated due to length ...]';
|
|
799
|
-
|
|
800
|
-
const truncatedComment = `## š¤ ${SOLUTION_DRAFT_LOG_MARKER} (Truncated)
|
|
801
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.
|
|
802
|
-
ā ļø **Log was truncated** due to GitHub comment size limits.
|
|
803
|
-
|
|
804
|
-
<details>
|
|
805
|
-
<summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB, truncated)</summary>
|
|
806
|
-
|
|
807
|
-
\`\`\`
|
|
808
|
-
${truncatedContent}
|
|
809
|
-
\`\`\`
|
|
810
|
-
|
|
811
|
-
</details>
|
|
812
|
-
|
|
813
|
-
---
|
|
814
|
-
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
815
|
-
const tempFile = `/tmp/log-truncated-comment-${targetType}-${Date.now()}.md`;
|
|
816
|
-
await fs.writeFile(tempFile, truncatedComment);
|
|
817
|
-
|
|
818
|
-
// Issue #1625: track the posted comment ID so it's excluded from the
|
|
819
|
-
// AI-authored-comment check in --auto-attach-solution-summary.
|
|
820
|
-
const posted = await postTrackedCommentFromFile({ $, owner, repo, targetNumber, bodyFile: tempFile });
|
|
821
|
-
await fs.unlink(tempFile).catch(() => {});
|
|
822
|
-
// ghCommand and targetName are retained in signature for symmetry with
|
|
823
|
-
// attachLogToGitHub's logging vocabulary.
|
|
824
|
-
void ghCommand;
|
|
825
|
-
if (posted.ok) {
|
|
826
|
-
await log(` ā
Truncated solution draft log uploaded to ${targetName}${posted.commentId ? ` (comment id=${posted.commentId})` : ''}`);
|
|
827
|
-
await log(` š Log size: ${Math.round(logStats.size / 1024)}KB (truncated)`);
|
|
828
|
-
return true;
|
|
829
|
-
} else {
|
|
830
|
-
await log(` ā Failed to upload truncated log: ${posted.stderr || 'unknown error'}`);
|
|
831
|
-
return false;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
783
|
/**
|
|
835
784
|
* Helper to attach a regular comment when it fits within limits
|
|
836
785
|
*/
|
package/src/log-upload.lib.mjs
CHANGED
|
@@ -27,6 +27,54 @@ const summarizeCommandOutput = value => {
|
|
|
27
27
|
return text.length > 500 ? `${text.slice(0, 500)}... [truncated ${text.length - 500} chars]` : text;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
export const parseGhUploadLogOutput = outputValue => {
|
|
31
|
+
const output = outputValue?.toString?.() || '';
|
|
32
|
+
const parsed = {
|
|
33
|
+
url: null,
|
|
34
|
+
rawUrl: null,
|
|
35
|
+
type: null,
|
|
36
|
+
chunks: 1,
|
|
37
|
+
repositoryName: null,
|
|
38
|
+
repositoryPath: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const urlMatch = output.match(/(?:^|\n)š\s+(https:\/\/[^\s\n]+)/u);
|
|
42
|
+
if (urlMatch) {
|
|
43
|
+
parsed.url = urlMatch[1].trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const rawUrlMatch = output.match(/(?:^|\n)š\s+(https:\/\/[^\s\n]+)/u);
|
|
47
|
+
if (rawUrlMatch) {
|
|
48
|
+
parsed.rawUrl = rawUrlMatch[1].trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (output.includes('Type: š Gist') || parsed.url?.includes('gist.github.com')) {
|
|
52
|
+
parsed.type = 'gist';
|
|
53
|
+
} else if (output.includes('Type: š¦ Repository') || (parsed.url?.includes('github.com') && !parsed.url?.includes('gist'))) {
|
|
54
|
+
parsed.type = 'repository';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fileCountMatch = output.match(/File count:\s*(\d+)/i);
|
|
58
|
+
const chunkMatch = output.match(/split into (\d+) chunks/i);
|
|
59
|
+
if (fileCountMatch) {
|
|
60
|
+
parsed.chunks = parseInt(fileCountMatch[1], 10);
|
|
61
|
+
} else if (chunkMatch) {
|
|
62
|
+
parsed.chunks = parseInt(chunkMatch[1], 10);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const repositoryMatch = output.match(/Repository:\s*([^\s\n]+)/i);
|
|
66
|
+
if (repositoryMatch) {
|
|
67
|
+
parsed.repositoryName = repositoryMatch[1].trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pathMatch = output.match(/Path:\s*([^\s\n]+)/i);
|
|
71
|
+
if (pathMatch) {
|
|
72
|
+
parsed.repositoryPath = pathMatch[1].trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return parsed;
|
|
76
|
+
};
|
|
77
|
+
|
|
30
78
|
/**
|
|
31
79
|
* Upload a log file using gh-upload-log command
|
|
32
80
|
* @param {Object} options - Upload options
|
|
@@ -34,7 +82,7 @@ const summarizeCommandOutput = value => {
|
|
|
34
82
|
* @param {boolean} options.isPublic - Whether to make the upload public
|
|
35
83
|
* @param {string} options.description - Description for the upload
|
|
36
84
|
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
37
|
-
* @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number}>}
|
|
85
|
+
* @returns {Promise<{success: boolean, url: string|null, rawUrl: string|null, type: 'gist'|'repository'|null, chunks: number, repositoryName?: string|null, repositoryPath?: string|null}>}
|
|
38
86
|
*/
|
|
39
87
|
export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description, verbose = false }) => {
|
|
40
88
|
const result = { success: false, url: null, rawUrl: null, type: null, chunks: 1 };
|
|
@@ -71,25 +119,7 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
71
119
|
return result;
|
|
72
120
|
}
|
|
73
121
|
|
|
74
|
-
|
|
75
|
-
// Look for the URL line: š https://...
|
|
76
|
-
const urlMatch = output.match(/š\s+(https:\/\/[^\s\n]+)/);
|
|
77
|
-
if (urlMatch) {
|
|
78
|
-
result.url = urlMatch[1].trim();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Determine type from output
|
|
82
|
-
if (output.includes('Type: š Gist') || result.url?.includes('gist.github.com')) {
|
|
83
|
-
result.type = 'gist';
|
|
84
|
-
} else if (output.includes('Type: š¦ Repository') || (result.url?.includes('github.com') && !result.url?.includes('gist'))) {
|
|
85
|
-
result.type = 'repository';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Extract chunk count if mentioned
|
|
89
|
-
const chunkMatch = output.match(/split into (\d+) chunks/i);
|
|
90
|
-
if (chunkMatch) {
|
|
91
|
-
result.chunks = parseInt(chunkMatch[1], 10);
|
|
92
|
-
}
|
|
122
|
+
Object.assign(result, parseGhUploadLogOutput(output));
|
|
93
123
|
|
|
94
124
|
// Construct raw URL based on type and chunks
|
|
95
125
|
if (result.url) {
|
|
@@ -139,17 +169,23 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
139
169
|
result.rawUrl = result.url;
|
|
140
170
|
}
|
|
141
171
|
} else if (result.type === 'repository') {
|
|
142
|
-
if (result.
|
|
172
|
+
if (result.rawUrl) {
|
|
173
|
+
// gh-upload-log v0.8+ prints the exact raw/download URL. Prefer it
|
|
174
|
+
// over reconstructing paths, especially for shared repositories.
|
|
175
|
+
} else if (result.chunks === 1) {
|
|
143
176
|
// For single chunk repository: construct raw URL to the file
|
|
144
177
|
// Repository URL format: https://github.com/owner/repo
|
|
145
178
|
// We need to find the actual file name in the repo
|
|
146
179
|
try {
|
|
147
180
|
const repoUrl = result.url;
|
|
148
|
-
const
|
|
181
|
+
const repoMatch = repoUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)\/(.+))?$/);
|
|
182
|
+
const [, repoOwner, repoName, branchName = 'main', treePath = null] = repoMatch || [];
|
|
183
|
+
const repoPath = repoOwner && repoName ? `${repoOwner}/${repoName}` : repoUrl.replace('https://github.com/', '');
|
|
184
|
+
const apiPath = treePath ? `repos/${repoPath}/contents/${treePath}?ref=${branchName}` : `repos/${repoPath}/contents`;
|
|
149
185
|
if (verbose) {
|
|
150
186
|
await log(` š Fetching repository contents for raw URL resolution (repoPath=${repoPath})`, { verbose: true });
|
|
151
187
|
}
|
|
152
|
-
const contentsResult = await $silent`gh api
|
|
188
|
+
const contentsResult = await $silent`gh api ${apiPath} --paginate --jq '.[].name'`;
|
|
153
189
|
if (verbose) {
|
|
154
190
|
await log(` š„ Repository contents fetch completed (code=${contentsResult.code ?? 'unknown'})`, { verbose: true });
|
|
155
191
|
}
|
|
@@ -161,7 +197,9 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
161
197
|
.filter(f => f && !f.startsWith('.'));
|
|
162
198
|
if (files.length > 0) {
|
|
163
199
|
const fileName = files[0];
|
|
164
|
-
|
|
200
|
+
const rawPath = treePath ? `${treePath}/${fileName}` : fileName;
|
|
201
|
+
const baseRepoUrl = repoOwner && repoName ? `https://github.com/${repoOwner}/${repoName}` : repoUrl;
|
|
202
|
+
result.rawUrl = `${baseRepoUrl}/raw/${branchName}/${rawPath}`;
|
|
165
203
|
if (verbose) {
|
|
166
204
|
await log(` š§© Repository contents resolved fileName=${fileName}`, { verbose: true });
|
|
167
205
|
}
|
|
@@ -212,5 +250,6 @@ export const uploadLogWithGhUploadLog = async ({ logFile, isPublic, description,
|
|
|
212
250
|
|
|
213
251
|
// Export all functions as default object too
|
|
214
252
|
export default {
|
|
253
|
+
parseGhUploadLogOutput,
|
|
215
254
|
uploadLogWithGhUploadLog,
|
|
216
255
|
};
|
|
@@ -34,6 +34,10 @@ async function getIsolationRunner() {
|
|
|
34
34
|
// In-memory session store
|
|
35
35
|
const activeSessions = new Map();
|
|
36
36
|
|
|
37
|
+
export function resetSessionMonitorForTests() {
|
|
38
|
+
activeSessions.clear();
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
/**
|
|
38
42
|
* Issue #1586: Timeout for non-isolation sessions.
|
|
39
43
|
* Non-isolation (plain start-screen) sessions cannot reliably detect completion
|
|
@@ -124,6 +128,11 @@ function completeSession(sessionName, exitCode = 0, verbose = false) {
|
|
|
124
128
|
}
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
function isMessageAlreadyUpdatedError(error) {
|
|
132
|
+
const message = String(error?.message || '').toLowerCase();
|
|
133
|
+
return message.includes('message is not modified');
|
|
134
|
+
}
|
|
135
|
+
|
|
127
136
|
function normalizeSessionUrl(url) {
|
|
128
137
|
return url.replace(/\/+$/, '').replace(/#.*$/, '').toLowerCase();
|
|
129
138
|
}
|
|
@@ -190,7 +199,7 @@ async function getIsolationSessionState(sessionName, sessionInfo, options = {})
|
|
|
190
199
|
* @param {Object} bot - Telegraf bot instance for sending messages
|
|
191
200
|
* @param {boolean} verbose - Whether to log verbose output
|
|
192
201
|
*/
|
|
193
|
-
export async function monitorSessions(bot, verbose = false) {
|
|
202
|
+
export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
194
203
|
const sessions = getActiveSessions(verbose);
|
|
195
204
|
|
|
196
205
|
if (sessions.length === 0) {
|
|
@@ -210,7 +219,10 @@ export async function monitorSessions(bot, verbose = false) {
|
|
|
210
219
|
// Isolation mode: use $ --status, with screen -ls only as a fallback
|
|
211
220
|
// when the status record is unavailable. Terminal $ statuses are
|
|
212
221
|
// authoritative so completed screen sessions do not stay blocked.
|
|
213
|
-
const state = await getIsolationSessionState(sessionName, sessionInfo, {
|
|
222
|
+
const state = await getIsolationSessionState(sessionName, sessionInfo, {
|
|
223
|
+
verbose,
|
|
224
|
+
statusProvider: options.statusProvider,
|
|
225
|
+
});
|
|
214
226
|
stillRunning = state.running;
|
|
215
227
|
exitCode = state.exitCode;
|
|
216
228
|
statusResult = state.statusResult;
|
|
@@ -257,7 +269,16 @@ export async function monitorSessions(bot, verbose = false) {
|
|
|
257
269
|
completeSession(sessionName, finalExitCode || 0, verbose);
|
|
258
270
|
} catch (error) {
|
|
259
271
|
console.error(`Failed to send completion notification for ${sessionName}:`, error);
|
|
260
|
-
|
|
272
|
+
if (isMessageAlreadyUpdatedError(error)) {
|
|
273
|
+
completeSession(sessionName, exitCode || 0, verbose);
|
|
274
|
+
} else {
|
|
275
|
+
sessionInfo.lastNotificationError = error.message;
|
|
276
|
+
sessionInfo.lastKnownStatus = statusResult?.status || sessionInfo.lastKnownStatus || null;
|
|
277
|
+
sessionInfo.lastKnownExitCode = exitCode ?? sessionInfo.lastKnownExitCode ?? null;
|
|
278
|
+
if (verbose) {
|
|
279
|
+
console.log(`[VERBOSE] Session ${sessionName} kept in memory so the completion notification can be retried`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
261
282
|
}
|
|
262
283
|
}
|
|
263
284
|
}
|
|
@@ -270,8 +291,14 @@ export async function monitorSessions(bot, verbose = false) {
|
|
|
270
291
|
* @param {number} intervalMs - Monitoring interval in milliseconds (default: 30000)
|
|
271
292
|
* @returns {NodeJS.Timer} The interval timer (can be cleared with clearInterval)
|
|
272
293
|
*/
|
|
273
|
-
export function startSessionMonitoring(bot, verbose = false, intervalMs = 30000) {
|
|
274
|
-
const
|
|
294
|
+
export function startSessionMonitoring(bot, verbose = false, intervalMs = 30000, options = {}) {
|
|
295
|
+
const runMonitor = () => {
|
|
296
|
+
monitorSessions(bot, verbose, options).catch(error => {
|
|
297
|
+
console.error(`[session-monitor] Session monitoring tick failed: ${error.message}`);
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
const timer = setInterval(runMonitor, intervalMs);
|
|
301
|
+
runMonitor();
|
|
275
302
|
console.log(`š Session monitoring started (checking every ${intervalMs / 1000} seconds, storage: in-memory)`);
|
|
276
303
|
return timer;
|
|
277
304
|
}
|
|
@@ -105,13 +105,28 @@ export function formatDelay(delayMs) {
|
|
|
105
105
|
* @param {number} [retryOptions.jitterFraction] - Jitter fraction (default: 0.1)
|
|
106
106
|
* @param {boolean} [retryOptions.verbose] - Enable verbose logging
|
|
107
107
|
* @param {Function} [retryOptions.onRetry] - Callback on each retry: (attempt, error, delayMs) => void
|
|
108
|
+
* @param {Function} [retryOptions.onLaunch] - Callback when Telegraf reports that launch has started
|
|
108
109
|
* @param {AbortSignal} [retryOptions.signal] - AbortSignal to cancel retry loop
|
|
109
110
|
* @returns {Promise<void>} Resolves when bot is successfully launched
|
|
110
111
|
* @throws {Error} If a non-retryable error occurs or signal is aborted
|
|
111
112
|
*/
|
|
112
113
|
export async function launchBotWithRetry(bot, launchOptions, retryOptions = {}) {
|
|
113
|
-
const { verbose = false, onRetry, signal, ...backoffConfig } = retryOptions;
|
|
114
|
+
const { verbose = false, onRetry, onLaunch, signal, ...backoffConfig } = retryOptions;
|
|
114
115
|
let attempt = 0;
|
|
116
|
+
let launchNotified = false;
|
|
117
|
+
|
|
118
|
+
const notifyLaunched = () => {
|
|
119
|
+
if (launchNotified || typeof onLaunch !== 'function') return;
|
|
120
|
+
launchNotified = true;
|
|
121
|
+
try {
|
|
122
|
+
const result = onLaunch();
|
|
123
|
+
if (result && typeof result.catch === 'function') {
|
|
124
|
+
result.catch(error => console.error(`[telegram-bot-launcher] onLaunch callback failed: ${error.message}`));
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`[telegram-bot-launcher] onLaunch callback failed: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
115
130
|
|
|
116
131
|
while (true) {
|
|
117
132
|
// Check if abort was requested (e.g., during shutdown)
|
|
@@ -130,10 +145,13 @@ export async function launchBotWithRetry(bot, launchOptions, retryOptions = {})
|
|
|
130
145
|
|
|
131
146
|
if (verbose) console.log(`[VERBOSE] Launch attempt ${attempt}: starting polling...`);
|
|
132
147
|
|
|
133
|
-
// Step 2: Launch bot in polling mode
|
|
134
|
-
|
|
148
|
+
// Step 2: Launch bot in polling mode. In Telegraf long-polling mode the
|
|
149
|
+
// launch promise may stay pending while polling is active, so use the
|
|
150
|
+
// launch callback for startup side effects such as session monitoring.
|
|
151
|
+
await bot.launch(launchOptions, notifyLaunched);
|
|
135
152
|
|
|
136
153
|
// Success -- bot is running
|
|
154
|
+
notifyLaunched();
|
|
137
155
|
if (attempt > 1) {
|
|
138
156
|
console.log(`ā
Bot launched successfully after ${attempt} attempts`);
|
|
139
157
|
}
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -1376,6 +1376,64 @@ if (VERBOSE) {
|
|
|
1376
1376
|
// The launcher handles deleteWebhook + bot.launch() with retry on transient errors.
|
|
1377
1377
|
// Non-retryable errors (401 Unauthorized) cause immediate exit.
|
|
1378
1378
|
const launchAbortController = new AbortController();
|
|
1379
|
+
let sessionMonitoringTimer = null;
|
|
1380
|
+
let launchAnnouncementShown = false;
|
|
1381
|
+
|
|
1382
|
+
function startSessionMonitoringOnce() {
|
|
1383
|
+
if (sessionMonitoringTimer) return;
|
|
1384
|
+
sessionMonitoringTimer = startSessionMonitoring(bot, VERBOSE);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async function onBotLaunched() {
|
|
1388
|
+
if (isShuttingDown || launchAnnouncementShown) return;
|
|
1389
|
+
launchAnnouncementShown = true;
|
|
1390
|
+
|
|
1391
|
+
console.log('ā
SwarmMindBot is now running!');
|
|
1392
|
+
console.log('Press Ctrl+C to stop');
|
|
1393
|
+
startSessionMonitoringOnce();
|
|
1394
|
+
|
|
1395
|
+
if (VERBOSE) {
|
|
1396
|
+
console.log('[VERBOSE] Bot launched successfully');
|
|
1397
|
+
console.log('[VERBOSE] Polling is active, waiting for messages...');
|
|
1398
|
+
|
|
1399
|
+
// Get bot info and webhook status for diagnostics
|
|
1400
|
+
try {
|
|
1401
|
+
const botInfo = await bot.telegram.getMe();
|
|
1402
|
+
const webhookInfo = await bot.telegram.getWebhookInfo();
|
|
1403
|
+
|
|
1404
|
+
console.log('[VERBOSE] Bot info:');
|
|
1405
|
+
console.log('[VERBOSE] Username: @' + botInfo.username);
|
|
1406
|
+
console.log('[VERBOSE] Bot ID:', botInfo.id);
|
|
1407
|
+
console.log('[VERBOSE] Webhook info:');
|
|
1408
|
+
console.log('[VERBOSE] URL:', webhookInfo.url || 'none (polling mode)');
|
|
1409
|
+
console.log('[VERBOSE] Pending updates:', webhookInfo.pending_update_count);
|
|
1410
|
+
if (webhookInfo.last_error_date) {
|
|
1411
|
+
console.log('[VERBOSE] Last error:', new Date(webhookInfo.last_error_date * 1000).toISOString());
|
|
1412
|
+
console.log('[VERBOSE] Error message:', webhookInfo.last_error_message);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
console.log('[VERBOSE]');
|
|
1416
|
+
console.log('[VERBOSE] ā ļø IMPORTANT: If bot is not receiving messages in group chats:');
|
|
1417
|
+
console.log('[VERBOSE] 1. Privacy Mode: Check if bot has privacy mode enabled in @BotFather');
|
|
1418
|
+
console.log('[VERBOSE] - Send /setprivacy to @BotFather');
|
|
1419
|
+
console.log('[VERBOSE] - Select @' + botInfo.username);
|
|
1420
|
+
console.log('[VERBOSE] - Choose "Disable" to receive all group messages');
|
|
1421
|
+
console.log('[VERBOSE] - IMPORTANT: Remove bot from group and re-add after changing!');
|
|
1422
|
+
console.log('[VERBOSE] 2. Admin Status: Make bot an admin in the group (admins see all messages)');
|
|
1423
|
+
console.log('[VERBOSE] 3. Run diagnostic: node experiments/test-telegram-bot-privacy-mode.mjs');
|
|
1424
|
+
console.log('[VERBOSE]');
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
console.log('[VERBOSE] Could not fetch bot info:', err.message);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
console.log('[VERBOSE] Send a message to the bot to test message reception');
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Start completion polling before entering Telegraf long polling. The active
|
|
1434
|
+
// session map is empty until commands are received, but bot.launch() may stay
|
|
1435
|
+
// pending while polling is active.
|
|
1436
|
+
startSessionMonitoringOnce();
|
|
1379
1437
|
|
|
1380
1438
|
launchBotWithRetry(
|
|
1381
1439
|
bot,
|
|
@@ -1386,52 +1444,11 @@ launchBotWithRetry(
|
|
|
1386
1444
|
{
|
|
1387
1445
|
verbose: VERBOSE,
|
|
1388
1446
|
signal: launchAbortController.signal,
|
|
1447
|
+
onLaunch: onBotLaunched,
|
|
1389
1448
|
}
|
|
1390
1449
|
)
|
|
1391
|
-
.then(
|
|
1392
|
-
if (isShuttingDown
|
|
1393
|
-
|
|
1394
|
-
console.log('ā
SwarmMindBot is now running!');
|
|
1395
|
-
console.log('Press Ctrl+C to stop');
|
|
1396
|
-
if (VERBOSE) {
|
|
1397
|
-
console.log('[VERBOSE] Bot launched successfully');
|
|
1398
|
-
console.log('[VERBOSE] Polling is active, waiting for messages...');
|
|
1399
|
-
|
|
1400
|
-
// Get bot info and webhook status for diagnostics
|
|
1401
|
-
try {
|
|
1402
|
-
const botInfo = await bot.telegram.getMe();
|
|
1403
|
-
const webhookInfo = await bot.telegram.getWebhookInfo();
|
|
1404
|
-
|
|
1405
|
-
console.log('[VERBOSE] Bot info:');
|
|
1406
|
-
console.log('[VERBOSE] Username: @' + botInfo.username);
|
|
1407
|
-
console.log('[VERBOSE] Bot ID:', botInfo.id);
|
|
1408
|
-
console.log('[VERBOSE] Webhook info:');
|
|
1409
|
-
console.log('[VERBOSE] URL:', webhookInfo.url || 'none (polling mode)');
|
|
1410
|
-
console.log('[VERBOSE] Pending updates:', webhookInfo.pending_update_count);
|
|
1411
|
-
if (webhookInfo.last_error_date) {
|
|
1412
|
-
console.log('[VERBOSE] Last error:', new Date(webhookInfo.last_error_date * 1000).toISOString());
|
|
1413
|
-
console.log('[VERBOSE] Error message:', webhookInfo.last_error_message);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
console.log('[VERBOSE]');
|
|
1417
|
-
console.log('[VERBOSE] ā ļø IMPORTANT: If bot is not receiving messages in group chats:');
|
|
1418
|
-
console.log('[VERBOSE] 1. Privacy Mode: Check if bot has privacy mode enabled in @BotFather');
|
|
1419
|
-
console.log('[VERBOSE] - Send /setprivacy to @BotFather');
|
|
1420
|
-
console.log('[VERBOSE] - Select @' + botInfo.username);
|
|
1421
|
-
console.log('[VERBOSE] - Choose "Disable" to receive all group messages');
|
|
1422
|
-
console.log('[VERBOSE] - IMPORTANT: Remove bot from group and re-add after changing!');
|
|
1423
|
-
console.log('[VERBOSE] 2. Admin Status: Make bot an admin in the group (admins see all messages)');
|
|
1424
|
-
console.log('[VERBOSE] 3. Run diagnostic: node experiments/test-telegram-bot-privacy-mode.mjs');
|
|
1425
|
-
console.log('[VERBOSE]');
|
|
1426
|
-
} catch (err) {
|
|
1427
|
-
console.log('[VERBOSE] Could not fetch bot info:', err.message);
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
console.log('[VERBOSE] Send a message to the bot to test message reception');
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Start session monitoring - check for completed sessions every 30 seconds
|
|
1434
|
-
startSessionMonitoring(bot, VERBOSE);
|
|
1450
|
+
.then(() => {
|
|
1451
|
+
if (!isShuttingDown && VERBOSE) console.log('[VERBOSE] Bot launch promise resolved');
|
|
1435
1452
|
})
|
|
1436
1453
|
.catch(error => {
|
|
1437
1454
|
console.error('ā Failed to start bot:', error);
|
|
@@ -1460,6 +1477,7 @@ process.once('SIGINT', () => {
|
|
|
1460
1477
|
console.log('\nš Received SIGINT (Ctrl+C), stopping bot...');
|
|
1461
1478
|
if (VERBOSE) console.log(`[VERBOSE] Signal: SIGINT, PID: ${process.pid}, PPID: ${process.ppid}`);
|
|
1462
1479
|
launchAbortController.abort(); // Cancel retry loop if still retrying (issue #1240)
|
|
1480
|
+
if (sessionMonitoringTimer) clearInterval(sessionMonitoringTimer);
|
|
1463
1481
|
stopSolveQueue();
|
|
1464
1482
|
bot.stop('SIGINT');
|
|
1465
1483
|
});
|
|
@@ -1469,6 +1487,7 @@ process.once('SIGTERM', () => {
|
|
|
1469
1487
|
console.log('\nš Received SIGTERM, stopping bot... (Check system logs: journalctl -u <service> or dmesg)');
|
|
1470
1488
|
if (VERBOSE) console.log(`[VERBOSE] Signal: SIGTERM, PID: ${process.pid}, PPID: ${process.ppid}`);
|
|
1471
1489
|
launchAbortController.abort(); // Cancel retry loop if still retrying (issue #1240)
|
|
1490
|
+
if (sessionMonitoringTimer) clearInterval(sessionMonitoringTimer);
|
|
1472
1491
|
stopSolveQueue();
|
|
1473
1492
|
bot.stop('SIGTERM');
|
|
1474
1493
|
});
|