@link-assistant/hive-mind 2.1.2 → 2.1.4
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/README.hi.md +3 -1
- package/README.md +3 -1
- package/README.ru.md +3 -1
- package/README.zh.md +3 -1
- package/package.json +1 -1
- package/src/agent.lib.mjs +132 -32
- package/src/bidirectional-interactive.lib.mjs +169 -71
- package/src/limits-i18n.lib.mjs +8 -0
- package/src/limits-subscription.lib.mjs +40 -1
- package/src/limits.lib.mjs +109 -150
- package/src/live-input-capabilities.lib.mjs +221 -0
- package/src/locales/en.lino +16 -0
- package/src/locales/hi.lino +16 -0
- package/src/locales/ru.lino +16 -0
- package/src/locales/zh.lino +16 -0
- package/src/solve.auto-merge-helpers.lib.mjs +66 -0
- package/src/solve.auto-merge.lib.mjs +34 -5
- package/src/solve.config.lib.mjs +10 -10
- package/src/solve.validation.lib.mjs +3 -1
package/src/locales/ru.lino
CHANGED
|
@@ -192,6 +192,11 @@ ru
|
|
|
192
192
|
hour
|
|
193
193
|
session "5-часовой сеанс Claude"
|
|
194
194
|
limits "Лимиты Claude"
|
|
195
|
+
subscription
|
|
196
|
+
title "Подписка Claude{{plan}}"
|
|
197
|
+
chatgpt
|
|
198
|
+
subscription
|
|
199
|
+
title "Подписка ChatGPT{{plan}}"
|
|
195
200
|
codex
|
|
196
201
|
5
|
|
197
202
|
hour
|
|
@@ -205,6 +210,7 @@ ru
|
|
|
205
210
|
current
|
|
206
211
|
time "Текущее время"
|
|
207
212
|
week
|
|
213
|
+
label "Текущая неделя"
|
|
208
214
|
all
|
|
209
215
|
models "Текущая неделя (все модели)"
|
|
210
216
|
sonnet
|
|
@@ -226,6 +232,8 @@ ru
|
|
|
226
232
|
end "Конец"
|
|
227
233
|
five
|
|
228
234
|
hour
|
|
235
|
+
limit
|
|
236
|
+
session "5-часовой сеанс"
|
|
229
237
|
session "5-часовой сеанс"
|
|
230
238
|
min
|
|
231
239
|
load
|
|
@@ -314,6 +322,14 @@ ru
|
|
|
314
322
|
session "сеанс"
|
|
315
323
|
start "Начало"
|
|
316
324
|
subscription
|
|
325
|
+
detail
|
|
326
|
+
ends
|
|
327
|
+
label "заканчивается {{time}}"
|
|
328
|
+
in "заканчивается через {{duration}}; {{time}}"
|
|
329
|
+
trial
|
|
330
|
+
ends
|
|
331
|
+
label "пробный период заканчивается {{time}}"
|
|
332
|
+
in "пробный период заканчивается через {{duration}}; {{time}}"
|
|
317
333
|
ends
|
|
318
334
|
label "Подписка заканчивается {{time}}"
|
|
319
335
|
in "Подписка заканчивается через {{duration}} ({{time}})"
|
package/src/locales/zh.lino
CHANGED
|
@@ -192,6 +192,11 @@ zh
|
|
|
192
192
|
hour
|
|
193
193
|
session "Claude 5 小时会话"
|
|
194
194
|
limits "Claude 限额"
|
|
195
|
+
subscription
|
|
196
|
+
title "Claude{{plan}} 订阅"
|
|
197
|
+
chatgpt
|
|
198
|
+
subscription
|
|
199
|
+
title "ChatGPT{{plan}} 订阅"
|
|
195
200
|
codex
|
|
196
201
|
5
|
|
197
202
|
hour
|
|
@@ -205,6 +210,7 @@ zh
|
|
|
205
210
|
current
|
|
206
211
|
time "当前时间"
|
|
207
212
|
week
|
|
213
|
+
label "本周"
|
|
208
214
|
all
|
|
209
215
|
models "本周(所有模型)"
|
|
210
216
|
sonnet
|
|
@@ -226,6 +232,8 @@ zh
|
|
|
226
232
|
end "结束"
|
|
227
233
|
five
|
|
228
234
|
hour
|
|
235
|
+
limit
|
|
236
|
+
session "5 小时会话"
|
|
229
237
|
session "5 小时会话"
|
|
230
238
|
min
|
|
231
239
|
load
|
|
@@ -314,6 +322,14 @@ zh
|
|
|
314
322
|
session "会话"
|
|
315
323
|
start "开始"
|
|
316
324
|
subscription
|
|
325
|
+
detail
|
|
326
|
+
ends
|
|
327
|
+
label "结束于 {{time}}"
|
|
328
|
+
in "将在 {{duration}} 后结束;{{time}}"
|
|
329
|
+
trial
|
|
330
|
+
ends
|
|
331
|
+
label "试用结束于 {{time}}"
|
|
332
|
+
in "试用将在 {{duration}} 后结束;{{time}}"
|
|
317
333
|
ends
|
|
318
334
|
label "订阅结束于 {{time}}"
|
|
319
335
|
in "订阅将在 {{duration}} 后结束 ({{time}})"
|
|
@@ -956,9 +956,75 @@ export const getMergeBlockers = async (owner, repo, prNumber, verbose = false, c
|
|
|
956
956
|
return { blockers, ciStatus, noCiConfigured: false, noCiTriggered: false, noWorkflowRunsForCommit };
|
|
957
957
|
};
|
|
958
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Issue #2007: Detect issue title/description changes across auto-restart
|
|
961
|
+
* iterations so the restart/resume fallback can deliver them as feedback for
|
|
962
|
+
* tools without a live input channel.
|
|
963
|
+
*
|
|
964
|
+
* The issue title and body are user-owned feedback surfaces (unlike the PR
|
|
965
|
+
* description, which #2007 treats as AI-owned). When they change while the AI
|
|
966
|
+
* is not streaming input, the next session must be told, otherwise the update
|
|
967
|
+
* would be silently ignored until the agent happens to re-read the issue.
|
|
968
|
+
*
|
|
969
|
+
* The first call (previousSnapshot = null) establishes the baseline and reports
|
|
970
|
+
* no change. Subsequent calls diff against the prior snapshot.
|
|
971
|
+
*
|
|
972
|
+
* @param {string} owner - Repository owner
|
|
973
|
+
* @param {string} repo - Repository name
|
|
974
|
+
* @param {number} issueNumber - Linked issue number
|
|
975
|
+
* @param {Object|null} previousSnapshot - Prior { title, body } snapshot, or null on first call
|
|
976
|
+
* @param {boolean} [verbose=false]
|
|
977
|
+
* @param {Function} [commandRunner=$] - Tagged-template command runner (injectable for tests)
|
|
978
|
+
* @returns {Promise<{changed: boolean, snapshot: Object|null, changes: Array<{field: string, from: string, to: string}>}>}
|
|
979
|
+
*/
|
|
980
|
+
export const checkForIssueMetadataChanges = async (owner, repo, issueNumber, previousSnapshot, verbose = false, commandRunner = $) => {
|
|
981
|
+
const empty = { changed: false, snapshot: previousSnapshot || null, changes: [] };
|
|
982
|
+
if (!issueNumber) return empty;
|
|
983
|
+
|
|
984
|
+
let snapshot;
|
|
985
|
+
try {
|
|
986
|
+
const result = await commandRunner`gh api repos/${owner}/${repo}/issues/${issueNumber} --jq '{title: .title, body: .body}'`;
|
|
987
|
+
if (result.code !== 0 || !result.stdout) return empty;
|
|
988
|
+
const parsed = JSON.parse(result.stdout.toString() || '{}');
|
|
989
|
+
snapshot = {
|
|
990
|
+
title: typeof parsed.title === 'string' ? parsed.title : '',
|
|
991
|
+
body: typeof parsed.body === 'string' ? parsed.body : '',
|
|
992
|
+
};
|
|
993
|
+
} catch (error) {
|
|
994
|
+
reportError(error, {
|
|
995
|
+
context: 'check_issue_metadata_changes',
|
|
996
|
+
owner,
|
|
997
|
+
repo,
|
|
998
|
+
issueNumber,
|
|
999
|
+
operation: 'fetch_issue_metadata',
|
|
1000
|
+
});
|
|
1001
|
+
return empty;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// First observation: establish the baseline without reporting a change.
|
|
1005
|
+
if (!previousSnapshot) {
|
|
1006
|
+
return { changed: false, snapshot, changes: [] };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const changes = [];
|
|
1010
|
+
if (snapshot.title !== previousSnapshot.title) {
|
|
1011
|
+
changes.push({ field: 'title', from: previousSnapshot.title, to: snapshot.title });
|
|
1012
|
+
}
|
|
1013
|
+
if (snapshot.body !== previousSnapshot.body) {
|
|
1014
|
+
changes.push({ field: 'body', from: previousSnapshot.body, to: snapshot.body });
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (verbose && changes.length > 0) {
|
|
1018
|
+
console.log(`[VERBOSE] Issue #${issueNumber} metadata changed: ${changes.map(c => c.field).join(', ')}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return { changed: changes.length > 0, snapshot, changes };
|
|
1022
|
+
};
|
|
1023
|
+
|
|
959
1024
|
export default {
|
|
960
1025
|
checkForExistingComment,
|
|
961
1026
|
checkForNonBotComments,
|
|
1027
|
+
checkForIssueMetadataChanges,
|
|
962
1028
|
getMergeBlockers,
|
|
963
1029
|
shouldResetNoRunsCounter,
|
|
964
1030
|
};
|
|
@@ -59,7 +59,7 @@ import { limitReset } from './config.lib.mjs';
|
|
|
59
59
|
|
|
60
60
|
// Import helper functions extracted for file size management (Issue #1593)
|
|
61
61
|
const autoMergeHelpers = await import('./solve.auto-merge-helpers.lib.mjs');
|
|
62
|
-
const { checkForExistingComment, checkForNonBotComments, getMergeBlockers, shouldResetNoRunsCounter, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
|
|
62
|
+
const { checkForExistingComment, checkForNonBotComments, checkForIssueMetadataChanges, getMergeBlockers, shouldResetNoRunsCounter, trackAuthenticatedUserCommentsSince, nextMonotonicCheckTime } = autoMergeHelpers;
|
|
63
63
|
|
|
64
64
|
// Issue #1769: cancelled/stale CI re-run failures need a human action stop, not polling forever.
|
|
65
65
|
const cancelledCiRerunLib = await import('./cancelled-ci-rerun.lib.mjs');
|
|
@@ -137,7 +137,7 @@ export const watchUntilMergeable = async params => {
|
|
|
137
137
|
await log(formatAligned('', 'Max limit resumes:', formatAutoIterationLimit(maxAutoResumeIterations), 2));
|
|
138
138
|
await log(formatAligned('', 'Wait for all repo actions:', waitForAllRepoActionsFlag ? 'Yes (strict repo-wide safety)' : 'No (PR-scoped CI only)', 2));
|
|
139
139
|
await log(formatAligned('', 'Stop conditions:', 'PR merged, PR closed, or becomes mergeable', 2));
|
|
140
|
-
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, CI failures, merge conflicts', 2));
|
|
140
|
+
await log(formatAligned('', 'Restart triggers:', 'New non-bot comments, issue title/description edits, CI failures, merge conflicts', 2));
|
|
141
141
|
// Issue #1708: Surface that --auto-input-until-mergeable streamed feedback
|
|
142
142
|
// into the prior session, so any restart triggered here is a fallback.
|
|
143
143
|
if (argv.autoInputUntilMergeable) {
|
|
@@ -157,6 +157,11 @@ export const watchUntilMergeable = async params => {
|
|
|
157
157
|
let iteration = 0;
|
|
158
158
|
let lastCheckTime = new Date();
|
|
159
159
|
|
|
160
|
+
// Issue #2007: Track the issue title/body across iterations so the
|
|
161
|
+
// restart/resume fallback can detect user edits to those surfaces and deliver
|
|
162
|
+
// them as feedback to the next session. The first check seeds the baseline.
|
|
163
|
+
let issueMetadataSnapshot = null;
|
|
164
|
+
|
|
160
165
|
while (true) {
|
|
161
166
|
iteration++;
|
|
162
167
|
const currentTime = new Date();
|
|
@@ -250,6 +255,14 @@ export const watchUntilMergeable = async params => {
|
|
|
250
255
|
trustAuthenticatedUserComments: true,
|
|
251
256
|
});
|
|
252
257
|
|
|
258
|
+
// Issue #2007: Detect issue title/description edits (user-owned feedback
|
|
259
|
+
// surfaces) so the fallback resumes the AI with them. The first iteration
|
|
260
|
+
// seeds the baseline and never reports a change.
|
|
261
|
+
const metadataCheck = await checkForIssueMetadataChanges(owner, repo, issueNumber, issueMetadataSnapshot, argv.verbose, $);
|
|
262
|
+
issueMetadataSnapshot = metadataCheck.snapshot || issueMetadataSnapshot;
|
|
263
|
+
const hasIssueMetadataChanges = metadataCheck.changed === true;
|
|
264
|
+
const issueMetadataChanges = metadataCheck.changes || [];
|
|
265
|
+
|
|
253
266
|
// Check for uncommitted changes using shared utility
|
|
254
267
|
const hasUncommittedChanges = await checkForUncommittedChanges(tempDir, argv);
|
|
255
268
|
|
|
@@ -264,8 +277,9 @@ export const watchUntilMergeable = async params => {
|
|
|
264
277
|
}
|
|
265
278
|
}
|
|
266
279
|
|
|
267
|
-
// If PR is mergeable, no blockers, no new comments,
|
|
268
|
-
|
|
280
|
+
// If PR is mergeable, no blockers, no new comments, no issue metadata
|
|
281
|
+
// edits, and no uncommitted changes
|
|
282
|
+
if (blockers.length === 0 && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges) {
|
|
269
283
|
// Issue #1503 (enhanced): Multi-mechanism consensus + repo-wide action check.
|
|
270
284
|
// Before declaring PR mergeable, run multiple independent CI detection mechanisms
|
|
271
285
|
// and require all to agree. This catches race conditions where CI starts between
|
|
@@ -420,6 +434,21 @@ export const watchUntilMergeable = async params => {
|
|
|
420
434
|
feedbackLines.push('Please review and address the feedback from these comments.');
|
|
421
435
|
}
|
|
422
436
|
|
|
437
|
+
// Issue #2007: Reason 1b: Issue title/description edited by the user.
|
|
438
|
+
if (hasIssueMetadataChanges) {
|
|
439
|
+
shouldRestart = true;
|
|
440
|
+
const changedFields = issueMetadataChanges.map(c => (c.field === 'title' ? 'title' : 'description')).join(' and ');
|
|
441
|
+
restartReason = restartReason ? `${restartReason}; Issue ${changedFields} edited` : `Issue ${changedFields} edited`;
|
|
442
|
+
feedbackLines.push(`✏️ The issue ${changedFields} was edited after the last session:`);
|
|
443
|
+
for (const change of issueMetadataChanges) {
|
|
444
|
+
const label = change.field === 'title' ? 'Issue title' : 'Issue description';
|
|
445
|
+
const updated = String(change.to ?? '');
|
|
446
|
+
feedbackLines.push(` - ${label} is now: "${updated.substring(0, 200)}${updated.length > 200 ? '...' : ''}"`);
|
|
447
|
+
}
|
|
448
|
+
feedbackLines.push('');
|
|
449
|
+
feedbackLines.push('Please re-read the updated issue and make sure your solution still matches the requirements.');
|
|
450
|
+
}
|
|
451
|
+
|
|
423
452
|
// Issue #1314: Check for billing limit errors BEFORE regular CI failures
|
|
424
453
|
// Billing limits require human intervention and should NOT trigger AI restarts
|
|
425
454
|
const billingBlocker = blockers.find(b => b.type === 'billing_limit');
|
|
@@ -581,7 +610,7 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
581
610
|
// take the restart path rather than the cancelled-review path.
|
|
582
611
|
const ciBlocker = ciFailureBlocker;
|
|
583
612
|
const hasMergeConflictBlocker = blockers.some(b => b.type === 'not_mergeable' && b.message?.includes('conflicts'));
|
|
584
|
-
if (externalReviewLimitBlocker && !ciBlocker && !billingBlocker && !cancelledBlocker && !hasNewComments && !hasUncommittedChanges && !hasMergeConflictBlocker) {
|
|
613
|
+
if (externalReviewLimitBlocker && !ciBlocker && !billingBlocker && !cancelledBlocker && !hasNewComments && !hasIssueMetadataChanges && !hasUncommittedChanges && !hasMergeConflictBlocker) {
|
|
585
614
|
await log('');
|
|
586
615
|
await log(formatAligned('🟡', 'READY FOR REVIEW', 'External review quota/credit limit requires human decision'));
|
|
587
616
|
for (const detail of externalReviewLimitBlocker.details || []) {
|
package/src/solve.config.lib.mjs
CHANGED
|
@@ -242,13 +242,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
242
242
|
description: 'Auto-restart until PR becomes mergeable (no iteration limit). Restarts on new comments from non-bot users, CI failures, merge conflicts, or other issues. Does NOT auto-merge.',
|
|
243
243
|
default: true,
|
|
244
244
|
},
|
|
245
|
-
// Issue #1708:
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
245
|
+
// Issue #1708/#2007: streaming-first feedback into the running tool session.
|
|
246
|
+
// Claude and Agent are wired through bidirectional stream-json stdin pipes;
|
|
247
|
+
// other tools retain restart/resume fallback behavior until a verified
|
|
248
|
+
// mid-session input protocol is wired into their solve runners.
|
|
249
249
|
'auto-input-until-mergeable': {
|
|
250
250
|
type: 'boolean',
|
|
251
|
-
description: '[EXPERIMENTAL]
|
|
251
|
+
description: '[EXPERIMENTAL] Keep feeding new issue/PR events (uncommitted changes, CI/CD failures, PR/issue comments, issue title/description edits) into the running AI session, in all ways possible. For --tool claude and --tool agent this streams the events directly into the live process via stream-json stdin (implies --accept-incomming-comments-as-input and --queue-comments-to-input by default, deferring comments until the AI finishes the current step). For codex, opencode, gemini, qwen, and unknown tools, it uses the universal restart/resume fallback: wait for the current turn to finish in the JSON output, stop the process, then resume/restart the AI session with the new events via --auto-restart-until-mergeable. Codex live streaming should be wired in a future runner through Codex app-server turn/steer. Disabled by default.',
|
|
252
252
|
default: false,
|
|
253
253
|
},
|
|
254
254
|
'wait-for-all-actions-in-repository-before-mergeable': {
|
|
@@ -436,7 +436,7 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
436
436
|
// Issue #817: Bidirectional interactive options
|
|
437
437
|
'accept-incomming-comments-as-input': {
|
|
438
438
|
type: 'boolean',
|
|
439
|
-
description: '[EXPERIMENTAL] Accept new PR/issue comments as input for
|
|
439
|
+
description: '[EXPERIMENTAL] Accept new PR/issue comments as input for the running stream-json tool during execution (excludes outgoing comments generated by solve itself). Does not require --interactive-mode; disabled by default. Only supported for --tool claude and --tool agent.',
|
|
440
440
|
default: false,
|
|
441
441
|
},
|
|
442
442
|
'exclude-all-own-incomming-comments-from-input': {
|
|
@@ -446,13 +446,13 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
446
446
|
},
|
|
447
447
|
'bidirectional-interactive-mode': {
|
|
448
448
|
type: 'boolean',
|
|
449
|
-
description: '[EXPERIMENTAL] Convenience flag that enables --interactive-mode, --accept-incomming-comments-as-input and --exclude-all-own-incomming-comments-from-input together. Only supported for --tool claude.',
|
|
449
|
+
description: '[EXPERIMENTAL] Convenience flag that enables --interactive-mode, --accept-incomming-comments-as-input and --exclude-all-own-incomming-comments-from-input together. Only supported for --tool claude and --tool agent.',
|
|
450
450
|
default: false,
|
|
451
451
|
},
|
|
452
452
|
// Issue #1708: Comment delivery mode for --accept-incomming-comments-as-input.
|
|
453
453
|
// --stream-comments-to-input: forward comments immediately as they arrive
|
|
454
454
|
// (the default for --accept-incomming-comments-as-input on its own; matches
|
|
455
|
-
// the existing #817 behavior of pushing comments to
|
|
455
|
+
// the existing #817 behavior of pushing comments to the stream-json tool as soon as
|
|
456
456
|
// pollIncomingComments sees them).
|
|
457
457
|
// --queue-comments-to-input: hold comments until the AI signals it is idle
|
|
458
458
|
// (waiting for input), then flush the queue. Used by
|
|
@@ -461,12 +461,12 @@ export const SOLVE_OPTION_DEFINITIONS = {
|
|
|
461
461
|
// The two flags are mutually exclusive; if both are set, queue mode wins.
|
|
462
462
|
'stream-comments-to-input': {
|
|
463
463
|
type: 'boolean',
|
|
464
|
-
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, forward each new PR/issue comment to the AI immediately as it arrives (real-time streaming). This is the default behavior for --accept-incomming-comments-as-input on its own. Mutually exclusive with --queue-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
|
|
464
|
+
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, forward each new PR/issue comment to the AI immediately as it arrives (real-time streaming). This is the default behavior for --accept-incomming-comments-as-input on its own. Mutually exclusive with --queue-comments-to-input; queue mode wins if both are set. Only supported for --tool claude and --tool agent.',
|
|
465
465
|
default: false,
|
|
466
466
|
},
|
|
467
467
|
'queue-comments-to-input': {
|
|
468
468
|
type: 'boolean',
|
|
469
|
-
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, queue new PR/issue comments and only flush them once the AI signals it is idle (waiting for input). This is the default mode implied by --auto-input-until-mergeable so the AI completes the current step before being interrupted with new instructions. Mutually exclusive with --stream-comments-to-input; queue mode wins if both are set. Only supported for --tool claude.',
|
|
469
|
+
description: '[EXPERIMENTAL] When --accept-incomming-comments-as-input is enabled, queue new PR/issue comments and only flush them once the AI signals it is idle (waiting for input). This is the default mode implied by --auto-input-until-mergeable so the AI completes the current step before being interrupted with new instructions. Mutually exclusive with --stream-comments-to-input; queue mode wins if both are set. Only supported for --tool claude and --tool agent.',
|
|
470
470
|
default: false,
|
|
471
471
|
},
|
|
472
472
|
'prompt-explore-sub-agent': {
|
|
@@ -341,7 +341,9 @@ export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnecti
|
|
|
341
341
|
} else if (argv.tool === 'agent') {
|
|
342
342
|
// Validate Agent connection
|
|
343
343
|
const agentLib = await import('./agent.lib.mjs');
|
|
344
|
-
isToolConnected = await agentLib.validateAgentConnection(model
|
|
344
|
+
isToolConnected = await agentLib.validateAgentConnection(model, {
|
|
345
|
+
requireLiveInput: !!(argv.autoInputUntilMergeable || argv.acceptIncommingCommentsAsInput),
|
|
346
|
+
});
|
|
345
347
|
if (!isToolConnected) {
|
|
346
348
|
await log('❌ Cannot proceed without Agent connection', { level: 'error' });
|
|
347
349
|
return false;
|