@link-assistant/hive-mind 2.0.4 → 2.0.6
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 +42 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +8 -8
- package/src/gemini.lib.mjs +3 -3
- package/src/interactive-mcp-status.lib.mjs +29 -8
- package/src/lib.mjs +54 -0
- package/src/opencode.lib.mjs +3 -2
- package/src/playwright-mcp.lib.mjs +83 -18
- package/src/qwen.lib.mjs +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0c63706: Stop surfacing meaningless stream fragments as tool errors (#1941). When a tool
|
|
8
|
+
run is interrupted mid-stream (CTRL+C / SIGINT, exit code 130), the last captured
|
|
9
|
+
stdout line could be a stray structural character such as a lone `}`, which leaked
|
|
10
|
+
into the GitHub failure comment as "CLAUDE execution failed with }". A new shared
|
|
11
|
+
`isMeaningfulErrorText` helper (any error with at least one Unicode letter or digit
|
|
12
|
+
is real; pure punctuation is not) now guards the `extractToolErrorCore` chokepoint,
|
|
13
|
+
and a new `buildToolErrorMessage` helper labels interruptions explicitly
|
|
14
|
+
("Claude command interrupted (CTRL+C)") across the Claude and OpenCode runners.
|
|
15
|
+
- d4efc82: fix(playwright-mcp): do not abort the solve when the Playwright MCP preflight probe is inconclusive (#1943)
|
|
16
|
+
|
|
17
|
+
A `solve` run aborted before creating a pull request with
|
|
18
|
+
`❌ Playwright MCP preflight failed for Claude Code`. The local preflight ran
|
|
19
|
+
`timeout 5 claude mcp list`, but that command performs a live health check that
|
|
20
|
+
launches a browser and can take longer than five seconds; when the `timeout`
|
|
21
|
+
killed the probe, `ensureConnectedPlaywrightMcpServer` treated the non-zero exit
|
|
22
|
+
as a failure and stopped the whole run.
|
|
23
|
+
|
|
24
|
+
An inconclusive `mcp list` probe (timeout / crash / missing CLI) now falls back
|
|
25
|
+
to the local `@playwright/mcp` package check instead of aborting: if the package
|
|
26
|
+
is installed, the server connects on demand via Tool Search (issue #1901), so the
|
|
27
|
+
working session proceeds. The probe timeout now defaults to 30s and is overridable
|
|
28
|
+
via `PLAYWRIGHT_MCP_PREFLIGHT_TIMEOUT_SECONDS`, and the preflight emits verbose
|
|
29
|
+
diagnostics (probe exit code, matched rows, decision branch) so failures are
|
|
30
|
+
diagnosable from the log. The preflight still fails only when `@playwright/mcp`
|
|
31
|
+
is genuinely unavailable.
|
|
32
|
+
|
|
33
|
+
## 2.0.5
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- d815c7d: Treat a Claude Code `pending` Playwright MCP `system.init` status as a normal
|
|
38
|
+
still-connecting state instead of a failure (#1901). Claude Code enables Tool
|
|
39
|
+
Search by default, so the deferred `mcp__playwright__*` browser tools load on
|
|
40
|
+
demand and Claude waits for the connecting server before using them. Hive Mind
|
|
41
|
+
no longer aborts the working session on a `pending` status; only a terminal
|
|
42
|
+
`failed`/`error` status surfaces a non-blocking diagnostic in the session-start
|
|
43
|
+
comment. See `docs/case-studies/issue-1901`.
|
|
44
|
+
|
|
3
45
|
## 2.0.4
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -6,7 +6,7 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
6
6
|
const { $ } = await use('command-stream');
|
|
7
7
|
const fs = (await use('fs')).promises;
|
|
8
8
|
const path = (await use('path')).default;
|
|
9
|
-
import { log, isENOSPC } from './lib.mjs';
|
|
9
|
+
import { log, isENOSPC, buildToolErrorMessage } from './lib.mjs';
|
|
10
10
|
import { reportError } from './sentry.lib.mjs';
|
|
11
11
|
import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToTokens, getTokensToThinkingLevel, supportsThinkingBudget, DEFAULT_MAX_THINKING_BUDGET, getMaxOutputTokensForModel } from './config.lib.mjs';
|
|
12
12
|
import { detectUsageLimit, formatUsageLimitMessage, isUsageLimitError } from './usage-limit.lib.mjs';
|
|
@@ -1211,8 +1211,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1211
1211
|
is503Error,
|
|
1212
1212
|
anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnStuckRetry, // Issue #1104/#1886
|
|
1213
1213
|
resultSummary,
|
|
1214
|
-
// Issue #1845: surface the actual error
|
|
1215
|
-
errorInfo: { message: lastMessage
|
|
1214
|
+
// Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
|
|
1215
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: 'API explicitly marked error as not retryable', toolLabel: 'Claude' }), exitCode },
|
|
1216
1216
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1217
1217
|
};
|
|
1218
1218
|
}
|
|
@@ -1260,8 +1260,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1260
1260
|
is503Error, // preserve for callers that check this
|
|
1261
1261
|
anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnRetriesExhausted, // Issue #1104/#1886: Include cumulative cost even on failure
|
|
1262
1262
|
resultSummary, // Issue #1263: Include result summary
|
|
1263
|
-
// Issue #1845: surface the actual error
|
|
1264
|
-
errorInfo: { message: lastMessage
|
|
1263
|
+
// Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
|
|
1264
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: `Transient API error persisted after ${maxRetries} retries`, toolLabel: 'Claude' }), exitCode },
|
|
1265
1265
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1266
1266
|
};
|
|
1267
1267
|
}
|
|
@@ -1327,9 +1327,9 @@ export const executeClaudeCommand = async params => {
|
|
|
1327
1327
|
errorDuringExecution,
|
|
1328
1328
|
anthropicTotalCostUSD: cumulativeAnthropicCostUSDOnFailure, // Issue #1104/#1886: cumulative cost even on failure
|
|
1329
1329
|
resultSummary, // Issue #1263: Include result summary
|
|
1330
|
-
// Issue #1845: surface the core error (e.g. "API Error: Output blocked by content
|
|
1331
|
-
//
|
|
1332
|
-
errorInfo: { message: lastMessage
|
|
1330
|
+
// Issue #1845: surface the core error (e.g. "API Error: Output blocked by content filtering policy").
|
|
1331
|
+
// Issue #1941: a lone "}" fragment at interrupt time must not become "CLAUDE execution failed with }".
|
|
1332
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage, exitCode, fallback: `Claude command failed with exit code ${exitCode}`, toolLabel: 'Claude' }), exitCode },
|
|
1333
1333
|
queuedFeedback, // Issue #817: Bidirectional mode feedback
|
|
1334
1334
|
};
|
|
1335
1335
|
}
|
package/src/gemini.lib.mjs
CHANGED
|
@@ -10,7 +10,7 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
10
10
|
|
|
11
11
|
const { $ } = await use('command-stream');
|
|
12
12
|
|
|
13
|
-
import { log } from './lib.mjs';
|
|
13
|
+
import { log, buildToolErrorMessage } from './lib.mjs';
|
|
14
14
|
import { reportError } from './sentry.lib.mjs';
|
|
15
15
|
import { timeouts, retryLimits } from './config.lib.mjs';
|
|
16
16
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -576,8 +576,8 @@ export const executeGeminiCommand = async params => {
|
|
|
576
576
|
pricingInfo: { modelId: mappedModel, modelName: mappedModel, provider: 'Google', totalCostUSD: null },
|
|
577
577
|
publicPricingEstimate: null,
|
|
578
578
|
resultSummary: geminiJsonState.resultSummary || null,
|
|
579
|
-
// Issue #1845: surface the actual error
|
|
580
|
-
errorInfo: { message: errorText
|
|
579
|
+
// Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
|
|
580
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage: errorText, exitCode, fallback: `Gemini command failed with exit code ${exitCode}`, toolLabel: 'Gemini' }), exitCode },
|
|
581
581
|
};
|
|
582
582
|
}
|
|
583
583
|
|
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
const PLAYWRIGHT_TOOL_PREFIX = 'mcp__playwright__';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// A `pending` (or `connecting`) MCP server is still being connected/reconnected
|
|
4
|
+
// in the background. It is NOT a failure: Claude Code enables Tool Search by
|
|
5
|
+
// default, so MCP tools are deferred and load on demand, and Claude waits for a
|
|
6
|
+
// still-connecting server before it uses one of that server's tools. See
|
|
7
|
+
// https://code.claude.com/docs/en/mcp and issue #1901.
|
|
8
|
+
export const isConnectingMcpStatus = status => /\b(pending|connecting)\b/i.test(String(status || ''));
|
|
9
|
+
|
|
10
|
+
// Terminal/unhealthy states where the MCP client has given up (or the server is
|
|
11
|
+
// turned off). Claude Code reconnects an HTTP/SSE server with exponential
|
|
12
|
+
// backoff and only marks it `failed` after the attempts are exhausted; at that
|
|
13
|
+
// point the server's tools never load.
|
|
14
|
+
export const isFailedMcpStatus = status => {
|
|
4
15
|
const normalized = String(status || '').toLowerCase();
|
|
5
|
-
return /\b(
|
|
16
|
+
return /\b(disabled|failed|error|disconnected|not[-_\s]+connected|unavailable|timed[-_\s]+out)\b|(?:^|[^a-z0-9_-])timeout(?:$|[^a-z0-9_-])/.test(normalized);
|
|
6
17
|
};
|
|
7
18
|
|
|
19
|
+
// Backwards-compatible umbrella: any non-connected status (still connecting OR
|
|
20
|
+
// failed). Prefer the narrower helpers above when the connecting/failed
|
|
21
|
+
// distinction matters (e.g. whether to warn a human reviewer).
|
|
22
|
+
export const isUnavailableMcpStatus = status => isConnectingMcpStatus(status) || isFailedMcpStatus(status);
|
|
23
|
+
|
|
8
24
|
export const hasPlaywrightMcpTools = tools => (Array.isArray(tools) ? tools : []).some(tool => String(tool || '').startsWith(PLAYWRIGHT_TOOL_PREFIX));
|
|
9
25
|
|
|
10
26
|
export const formatInteractiveMcpServerStatus = server => {
|
|
11
27
|
const name = server?.name || 'unknown';
|
|
12
28
|
const status = String(server?.status || 'unknown').trim() || 'unknown';
|
|
13
|
-
const normalizedStatus = status.toLowerCase();
|
|
14
29
|
let displayStatus = status;
|
|
15
30
|
|
|
16
|
-
if (
|
|
17
|
-
displayStatus =
|
|
18
|
-
} else if (
|
|
31
|
+
if (isConnectingMcpStatus(status)) {
|
|
32
|
+
displayStatus = `${status} - connecting; tools load on demand via Tool Search`;
|
|
33
|
+
} else if (isFailedMcpStatus(status)) {
|
|
19
34
|
displayStatus = `${status} - MCP tools unavailable`;
|
|
20
35
|
}
|
|
21
36
|
|
|
@@ -29,10 +44,16 @@ export const getInteractiveMcpDiagnostics = (mcpServers = [], tools = []) => {
|
|
|
29
44
|
for (const server of servers) {
|
|
30
45
|
const name = String(server?.name || '').toLowerCase();
|
|
31
46
|
if (!name.includes('playwright')) continue;
|
|
32
|
-
|
|
47
|
+
// With Tool Search the deferred `mcp__playwright__*` tools are intentionally
|
|
48
|
+
// absent from system.init `tools`, so their absence is not a problem by
|
|
49
|
+
// itself. If they are already present the server is fully connected.
|
|
33
50
|
if (hasPlaywrightMcpTools(tools)) continue;
|
|
51
|
+
// `pending`/`connecting` is the normal startup state — Claude waits for the
|
|
52
|
+
// server before using a browser tool — so only warn when the MCP client has
|
|
53
|
+
// actually failed to connect.
|
|
54
|
+
if (!isFailedMcpStatus(server?.status)) continue;
|
|
34
55
|
|
|
35
|
-
diagnostics.push(`⚠️ Playwright MCP server is ${server?.status || 'unknown'},
|
|
56
|
+
diagnostics.push(`⚠️ Playwright MCP server is ${server?.status || 'unknown'} (failed to connect), so no \`${PLAYWRIGHT_TOOL_PREFIX}*\` browser tools are available. Browser automation stays disabled until the MCP server connects.`);
|
|
36
57
|
}
|
|
37
58
|
|
|
38
59
|
return diagnostics;
|
package/src/lib.mjs
CHANGED
|
@@ -665,6 +665,53 @@ export const cleanErrorMessage = error => {
|
|
|
665
665
|
return message;
|
|
666
666
|
};
|
|
667
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Decide whether a string looks like a meaningful, human-readable error message
|
|
670
|
+
* rather than a stray structural fragment (Issue #1941).
|
|
671
|
+
*
|
|
672
|
+
* When a tool process is interrupted mid-stream (CTRL+C / SIGINT) or killed, the
|
|
673
|
+
* last captured stdout line can be a lone JSON-structural character left over
|
|
674
|
+
* from a truncated stream — for example a bare `}` or `{`. Surfacing that as the
|
|
675
|
+
* "core error" produced nonsense failure messages such as
|
|
676
|
+
* "CLAUDE execution failed with }" / "failed by {". A real error message always
|
|
677
|
+
* contains at least one letter or digit (in any script), so we treat fragments
|
|
678
|
+
* that contain none as not meaningful.
|
|
679
|
+
*
|
|
680
|
+
* @param {*} value - Candidate error string
|
|
681
|
+
* @returns {boolean} True when the value contains usable error text
|
|
682
|
+
*/
|
|
683
|
+
export const isMeaningfulErrorText = value => {
|
|
684
|
+
if (!value || typeof value !== 'string') return false;
|
|
685
|
+
const collapsed = value.replace(/\s+/g, ' ').trim();
|
|
686
|
+
if (!collapsed) return false;
|
|
687
|
+
// Require at least one Unicode letter or number; pure punctuation/brackets
|
|
688
|
+
// (e.g. "}", "{", "[]", ",") are stream fragments, not real errors.
|
|
689
|
+
return /[\p{L}\p{N}]/u.test(collapsed);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Build a clean tool error message for `errorInfo.message`, rejecting
|
|
694
|
+
* meaningless stream fragments (Issue #1941).
|
|
695
|
+
*
|
|
696
|
+
* Picks the tool-reported `lastMessage` only when it is meaningful; otherwise
|
|
697
|
+
* falls back to an interrupt label (exit code 130 = SIGINT/CTRL+C) or the
|
|
698
|
+
* provided generic fallback. This keeps junk like a lone `}` out of the stored
|
|
699
|
+
* error so every downstream surface (GitHub comment, terminal, retry logic)
|
|
700
|
+
* shows something honest.
|
|
701
|
+
*
|
|
702
|
+
* @param {Object} options
|
|
703
|
+
* @param {string} [options.lastMessage] - The last message captured from the tool stream
|
|
704
|
+
* @param {number} [options.exitCode] - Process exit code
|
|
705
|
+
* @param {string} [options.fallback] - Generic fallback message
|
|
706
|
+
* @param {string} [options.toolLabel='Tool'] - Human tool label for the interrupt message
|
|
707
|
+
* @returns {string} A clean, meaningful error message
|
|
708
|
+
*/
|
|
709
|
+
export const buildToolErrorMessage = ({ lastMessage, exitCode, fallback, toolLabel = 'Tool' } = {}) => {
|
|
710
|
+
if (isMeaningfulErrorText(lastMessage)) return lastMessage.replace(/\s+/g, ' ').trim();
|
|
711
|
+
if (exitCode === 130) return `${toolLabel} command interrupted (CTRL+C)`;
|
|
712
|
+
return fallback;
|
|
713
|
+
};
|
|
714
|
+
|
|
668
715
|
/**
|
|
669
716
|
* Extract the core/root error string from a tool runner result (Issue #1845).
|
|
670
717
|
*
|
|
@@ -675,6 +722,10 @@ export const cleanErrorMessage = error => {
|
|
|
675
722
|
* (GitHub comments / exit message) and the terminal "Error details:" lines in
|
|
676
723
|
* watch / auto-merge so they never diverge.
|
|
677
724
|
*
|
|
725
|
+
* Issue #1941: a meaningless structural fragment (e.g. a lone `}` captured when
|
|
726
|
+
* a tool is interrupted mid-stream) is treated as "no usable error" so callers
|
|
727
|
+
* fall back to the generic phrase instead of "execution failed with }".
|
|
728
|
+
*
|
|
678
729
|
* @param {Object} options
|
|
679
730
|
* @param {Object} [options.toolResult] - Result object returned by the tool runner
|
|
680
731
|
* @returns {string|null} The core error string, or null when none is available
|
|
@@ -688,6 +739,9 @@ export const extractToolErrorCore = ({ toolResult } = {}) => {
|
|
|
688
739
|
|
|
689
740
|
if (!rawCore || typeof rawCore !== 'string') return null;
|
|
690
741
|
|
|
742
|
+
// Issue #1941: reject stray fragments with no letters/digits (e.g. "}").
|
|
743
|
+
if (!isMeaningfulErrorText(rawCore)) return null;
|
|
744
|
+
|
|
691
745
|
// Collapse to a single clean line and strip noise.
|
|
692
746
|
const core = rawCore.replace(/\s+/g, ' ').trim();
|
|
693
747
|
return core || null;
|
package/src/opencode.lib.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const path = (await use('path')).default;
|
|
|
14
14
|
const os = (await use('os')).default;
|
|
15
15
|
|
|
16
16
|
// Import log from general lib
|
|
17
|
-
import { log } from './lib.mjs';
|
|
17
|
+
import { log, isMeaningfulErrorText, buildToolErrorMessage } from './lib.mjs';
|
|
18
18
|
import { reportError } from './sentry.lib.mjs';
|
|
19
19
|
import { timeouts, retryLimits } from './config.lib.mjs';
|
|
20
20
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -532,7 +532,8 @@ export const executeOpenCodeCommand = async params => {
|
|
|
532
532
|
...pricingResult,
|
|
533
533
|
resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
|
|
534
534
|
// Issue #1845: surface the actual error so callers can show it to users
|
|
535
|
-
|
|
535
|
+
// Issue #1941: reject meaningless stream fragments (e.g. a lone "}").
|
|
536
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage: isMeaningfulErrorText(lastMessage) ? lastMessage : allOutput, exitCode, fallback: `OpenCode command failed with exit code ${exitCode}`, toolLabel: 'OpenCode' }), exitCode },
|
|
536
537
|
};
|
|
537
538
|
}
|
|
538
539
|
|
|
@@ -30,44 +30,109 @@ export const hasConnectedPlaywrightMcpServer = output => {
|
|
|
30
30
|
return rows.some(row => PLAYWRIGHT_MCP_CONNECTED_PATTERN.test(row) && !PLAYWRIGHT_MCP_UNAVAILABLE_PATTERN.test(row));
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// `claude mcp list` / `codex mcp list` perform live health checks against every
|
|
34
|
+
// registered MCP server (Playwright MCP launches a browser to report status),
|
|
35
|
+
// which can take noticeably longer than a couple of seconds on a cold cache or
|
|
36
|
+
// a busy CI host. A too-aggressive `timeout` kills the probe before it answers,
|
|
37
|
+
// which previously aborted the entire solve (issue #1943). The probe timeout is
|
|
38
|
+
// therefore generous by default and overridable for slow/fast environments.
|
|
39
|
+
export const PLAYWRIGHT_MCP_LIST_TIMEOUT_SECONDS_DEFAULT = 30;
|
|
40
|
+
|
|
41
|
+
export const getPlaywrightMcpListTimeoutSeconds = (env = process.env) => {
|
|
42
|
+
const parsed = Number.parseInt(env?.PLAYWRIGHT_MCP_PREFLIGHT_TIMEOUT_SECONDS ?? '', 10);
|
|
43
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : PLAYWRIGHT_MCP_LIST_TIMEOUT_SECONDS_DEFAULT;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const diagLog =
|
|
47
|
+
(log, opts = { verbose: true }) =>
|
|
48
|
+
async message => {
|
|
49
|
+
if (log) await log(message, opts);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const checkPlaywrightMcpPackageAvailability = async ({ log } = {}) => {
|
|
53
|
+
const diag = diagLog(log);
|
|
34
54
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
const timeoutSeconds = getPlaywrightMcpListTimeoutSeconds();
|
|
56
|
+
const result = await $`timeout ${timeoutSeconds} npx --no-install @playwright/mcp --help 2>&1`.catch(() => null);
|
|
57
|
+
if (isCommandResultSuccess(result)) {
|
|
58
|
+
await diag('🎭 @playwright/mcp package is available via npx');
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const npmResult = await $`timeout ${timeoutSeconds} npm ls -g @playwright/mcp 2>&1`.catch(() => null);
|
|
62
|
+
const available = getCommandResultOutput(npmResult).includes('@playwright/mcp');
|
|
63
|
+
await diag(`🎭 @playwright/mcp global package ${available ? 'is installed' : 'was not found'}`);
|
|
64
|
+
return available;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await diag(`⚠️ @playwright/mcp package availability check threw: ${error.message}`);
|
|
40
67
|
return false;
|
|
41
68
|
}
|
|
42
69
|
};
|
|
43
70
|
|
|
44
|
-
export const ensureConnectedPlaywrightMcpServer = async ({ list, add, hasPackage = checkPlaywrightMcpPackageAvailability }) => {
|
|
71
|
+
export const ensureConnectedPlaywrightMcpServer = async ({ list, add, hasPackage = checkPlaywrightMcpPackageAvailability, log } = {}) => {
|
|
72
|
+
const diag = diagLog(log);
|
|
45
73
|
try {
|
|
46
74
|
const result = await list().catch(() => null);
|
|
47
|
-
|
|
75
|
+
const code = getCommandResultCode(result);
|
|
48
76
|
const output = getCommandResultOutput(result);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
77
|
+
const rows = getPlaywrightMcpListRows(output);
|
|
78
|
+
await diag(`🎭 Playwright MCP probe: 'mcp list' exit=${code === null ? 'timeout/none' : code}, playwright rows=${rows.length}${rows.length ? ` [${rows.join(' | ')}]` : ''}`);
|
|
79
|
+
|
|
80
|
+
// Inconclusive probe: the `mcp list` command itself failed (timed out,
|
|
81
|
+
// crashed, or its binary is missing). That tells us NOTHING about whether
|
|
82
|
+
// Playwright MCP actually works, so it must not abort the whole solve as it
|
|
83
|
+
// did in issue #1943. A still-connecting server is normal — Tool Search
|
|
84
|
+
// loads MCP tools on demand (issue #1901) — so fall back to the local
|
|
85
|
+
// @playwright/mcp package check: if the package is installed, the server can
|
|
86
|
+
// connect on demand and the working session should proceed.
|
|
87
|
+
if (!isCommandResultSuccess(result)) {
|
|
88
|
+
const packageAvailable = await hasPackage({ log });
|
|
89
|
+
await diag(`⚠️ Playwright MCP 'mcp list' probe was inconclusive (exit=${code === null ? 'timeout' : code}); @playwright/mcp package ${packageAvailable ? 'is installed, so Tool Search can connect it on demand — preflight passes' : 'is NOT installed — preflight fails'}`);
|
|
90
|
+
return packageAvailable;
|
|
91
|
+
}
|
|
52
92
|
|
|
93
|
+
if (hasConnectedPlaywrightMcpServer(output)) {
|
|
94
|
+
await diag('🎭 Playwright MCP reported as connected by mcp list');
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A registration row exists but is not reported connected (pending /
|
|
99
|
+
// disabled / failed). Leave it untouched (do not overwrite an intentional
|
|
100
|
+
// or in-progress registration) and let the caller decide.
|
|
101
|
+
if (rows.length > 0) {
|
|
102
|
+
await diag('🎭 Playwright MCP is registered but not reported connected by mcp list; leaving registration unchanged');
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// No registration at all → register the default server when the package is
|
|
107
|
+
// available, then re-probe.
|
|
108
|
+
if (!(await hasPackage({ log }))) {
|
|
109
|
+
await diag('⚠️ No Playwright MCP registration found and @playwright/mcp package is unavailable');
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
await diag('🎭 No Playwright MCP registration found; registering the default server...');
|
|
53
113
|
await add().catch(() => null);
|
|
54
114
|
const retryResult = await list().catch(() => null);
|
|
55
|
-
|
|
56
|
-
|
|
115
|
+
const connected = isCommandResultSuccess(retryResult) && hasConnectedPlaywrightMcpServer(getCommandResultOutput(retryResult));
|
|
116
|
+
await diag(`🎭 Playwright MCP registration ${connected ? 'succeeded and is now connected' : 'did not report connected after add'}`);
|
|
117
|
+
return connected;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
await diag(`⚠️ Playwright MCP preflight probe threw: ${error.message}`);
|
|
57
120
|
return false;
|
|
58
121
|
}
|
|
59
122
|
};
|
|
60
123
|
|
|
61
|
-
export const ensureClaudePlaywrightMcpServer = async () =>
|
|
124
|
+
export const ensureClaudePlaywrightMcpServer = async ({ log } = {}) =>
|
|
62
125
|
ensureConnectedPlaywrightMcpServer({
|
|
63
|
-
list: () => $`timeout
|
|
126
|
+
list: () => $`timeout ${getPlaywrightMcpListTimeoutSeconds()} claude mcp list 2>&1`,
|
|
64
127
|
add: () => $`claude mcp add playwright -s user -- npx -y @playwright/mcp@latest --isolated --headless --no-sandbox --timeout-action=600000 --viewport-size 1920x1080`,
|
|
128
|
+
log,
|
|
65
129
|
});
|
|
66
130
|
|
|
67
|
-
export const ensureCodexPlaywrightMcpServer = async () =>
|
|
131
|
+
export const ensureCodexPlaywrightMcpServer = async ({ log } = {}) =>
|
|
68
132
|
ensureConnectedPlaywrightMcpServer({
|
|
69
|
-
list: () => $`timeout
|
|
133
|
+
list: () => $`timeout ${getPlaywrightMcpListTimeoutSeconds()} codex mcp list 2>&1`,
|
|
70
134
|
add: () => $`codex mcp add playwright -- npx -y @playwright/mcp@latest --isolated --headless --no-sandbox --timeout-action=600000 --viewport-size 1920x1080`,
|
|
135
|
+
log,
|
|
71
136
|
});
|
|
72
137
|
|
|
73
138
|
const SOLVE_PLAYWRIGHT_MCP_CHECKS = {
|
|
@@ -102,7 +167,7 @@ export const ensureSolvePlaywrightMcpReady = async ({ argv = {}, log = async ()
|
|
|
102
167
|
await log(`🎭 Checking Playwright MCP preflight for ${label}...`, { verbose: true });
|
|
103
168
|
|
|
104
169
|
try {
|
|
105
|
-
if (await checkFn()) {
|
|
170
|
+
if (await checkFn({ log })) {
|
|
106
171
|
await log(`🎭 Playwright MCP ready for ${label}`, { verbose: true });
|
|
107
172
|
return { ok: true, checkedTools: [tool], skipped: false };
|
|
108
173
|
}
|
package/src/qwen.lib.mjs
CHANGED
|
@@ -13,7 +13,7 @@ const fs = (await use('fs')).promises;
|
|
|
13
13
|
const path = (await use('path')).default;
|
|
14
14
|
const os = (await use('os')).default;
|
|
15
15
|
|
|
16
|
-
import { log } from './lib.mjs';
|
|
16
|
+
import { log, buildToolErrorMessage } from './lib.mjs';
|
|
17
17
|
import { reportError } from './sentry.lib.mjs';
|
|
18
18
|
import { timeouts, retryLimits } from './config.lib.mjs';
|
|
19
19
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -633,8 +633,8 @@ export const executeQwenCommand = async params => {
|
|
|
633
633
|
limitResetTime: null,
|
|
634
634
|
...usageResult,
|
|
635
635
|
resultSummary,
|
|
636
|
-
// Issue #1845: surface the actual error
|
|
637
|
-
errorInfo: { message: combinedErrorText || errorMessage
|
|
636
|
+
// Issue #1845/#1941: surface the actual error, rejecting meaningless fragments (e.g. a lone "}")
|
|
637
|
+
errorInfo: { message: buildToolErrorMessage({ lastMessage: combinedErrorText || errorMessage, exitCode, fallback: `Qwen Code command failed${exitCode !== 0 ? ` with exit code ${exitCode}` : ''}`, toolLabel: 'Qwen Code' }), exitCode },
|
|
638
638
|
};
|
|
639
639
|
}
|
|
640
640
|
|