@commonlyai/cli 0.1.221 → 0.1.222
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/package.json
CHANGED
|
@@ -97,6 +97,8 @@ import { claudeConfigDirFor, claudeContextTokens, claudeTranscriptPath } from '.
|
|
|
97
97
|
import { skillsAsText } from '../skill-prompt.js';
|
|
98
98
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
99
99
|
import { PUBLIC_WITHHELD_TOOLS } from '../public-withheld-tools.js';
|
|
100
|
+
import { isShippedCommonlyMcpCommand } from '../declared-mcp-guard.js';
|
|
101
|
+
import { isAnswerableToolError, PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE } from '../public-tool-error.js';
|
|
100
102
|
|
|
101
103
|
// See codex.js for the rationale on bumping the default + env override.
|
|
102
104
|
// Keeping both adapters in lockstep so any wrapper agent runtime has the
|
|
@@ -533,6 +535,87 @@ export const claudeResultText = (stdout, { credentials = [] } = {}) => {
|
|
|
533
535
|
.trim();
|
|
534
536
|
};
|
|
535
537
|
|
|
538
|
+
// TASK-256: what a public seat's own stream says about its Commonly MCP server.
|
|
539
|
+
// Claude Code reports each server's state in the init event (`connected` or
|
|
540
|
+
// `failed`), and a failed tool call as a `tool_result` with `is_error: true`
|
|
541
|
+
// whose content is the server's text, the shipped MCP's `{status, body,
|
|
542
|
+
// message}` JSON (both measured on Claude Code 2.1.314; `-p` waits for a slow
|
|
543
|
+
// server, so init reports it connected rather than pending). Ready is a positive
|
|
544
|
+
// witness: `connected` in init, or `pending` followed by a Commonly call that
|
|
545
|
+
// returned, since a server may finish starting during the turn. A stream with no
|
|
546
|
+
// init event, or with a Commonly server in any other state, never showed the
|
|
547
|
+
// server working, so the turn is withheld. A failed
|
|
548
|
+
// Commonly call withholds it unless `isAnswerableToolError` names an answer the
|
|
549
|
+
// person can be given (the predicate the OpenCode adapter uses). Only servers
|
|
550
|
+
// that run the shipped Commonly MCP count. Returns the failure, or null.
|
|
551
|
+
const toolResultText = (content) => {
|
|
552
|
+
if (typeof content === 'string') return content;
|
|
553
|
+
if (!Array.isArray(content)) return '';
|
|
554
|
+
return content.filter((part) => part?.type === 'text').map((part) => String(part.text ?? '')).join('');
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export const claudePublicMcpWitness = (stdout, mcpServers) => {
|
|
558
|
+
const names = (Array.isArray(mcpServers) ? mcpServers : [])
|
|
559
|
+
.filter((server) => server?.name && isShippedCommonlyMcpCommand(server.command))
|
|
560
|
+
.map((server) => String(server.name));
|
|
561
|
+
if (!names.length) return null;
|
|
562
|
+
const events = String(stdout || '').split('\n').map(parseEvent).filter(Boolean);
|
|
563
|
+
const init = events.find((event) => event.type === 'system' && event.subtype === 'init');
|
|
564
|
+
const servers = Array.isArray(init?.mcp_servers) ? init.mcp_servers : [];
|
|
565
|
+
const toolNames = new Map();
|
|
566
|
+
for (const event of events) {
|
|
567
|
+
if (event.type !== 'assistant') continue;
|
|
568
|
+
for (const block of event.message?.content || []) {
|
|
569
|
+
if (block?.type === 'tool_use' && block.id) toolNames.set(block.id, String(block.name || ''));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const results = events
|
|
573
|
+
.filter((event) => event.type === 'user' && Array.isArray(event.message?.content))
|
|
574
|
+
.flatMap((event) => event.message.content)
|
|
575
|
+
.filter((block) => block?.type === 'tool_result')
|
|
576
|
+
.map((block) => ({ block, tool: toolNames.get(block.tool_use_id) || '' }));
|
|
577
|
+
// Init names a server as declared; tool names carry it with every character
|
|
578
|
+
// outside [A-Za-z0-9_-] turned to `_` (`commonly.me` → `mcp__commonly_me__`,
|
|
579
|
+
// measured on 2.1.314).
|
|
580
|
+
const prefixOf = (name) => `mcp__${name.replace(/[^A-Za-z0-9_-]/g, '_')}__`;
|
|
581
|
+
for (const name of names) {
|
|
582
|
+
const status = servers.find((server) => server?.name === name)?.status;
|
|
583
|
+
const returned = status === 'pending'
|
|
584
|
+
&& results.some(({ block, tool }) => block.is_error !== true && tool.startsWith(prefixOf(name)));
|
|
585
|
+
if (status !== 'connected' && !returned) {
|
|
586
|
+
return { code: PUBLIC_MCP_NOT_READY_CODE, tool: name, error: init ? `status ${status || 'absent'}` : 'no init event' };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const prefixes = names.map(prefixOf);
|
|
590
|
+
for (const { block, tool } of results) {
|
|
591
|
+
if (block.is_error !== true || !prefixes.some((prefix) => tool.startsWith(prefix))) continue;
|
|
592
|
+
const error = toolResultText(block.content);
|
|
593
|
+
if (!isAnswerableToolError(error)) return { code: PUBLIC_TOOL_FAILED_CODE, tool, error };
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const publicWitnessError = ({ code, tool, error }) => {
|
|
599
|
+
const reason = String(error).replace(/\s+/g, ' ').slice(0, 160) || '(empty error)';
|
|
600
|
+
const what = code === PUBLIC_MCP_NOT_READY_CODE
|
|
601
|
+
? `Commonly MCP server ${tool} was not shown connected`
|
|
602
|
+
: `Commonly tool ${tool} failed in a way the model must not narrate`;
|
|
603
|
+
const failure = new Error(
|
|
604
|
+
`public seat withheld this turn's reply: ${what} (${reason}), so nothing was posted and the event stays unacked for retry`,
|
|
605
|
+
);
|
|
606
|
+
failure.code = code;
|
|
607
|
+
return failure;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// The reply a turn posts. A public seat's turn is witnessed first (TASK-256).
|
|
611
|
+
const turnReply = (stdout, { credentials, publicMcp }) => {
|
|
612
|
+
if (publicMcp) {
|
|
613
|
+
const failure = claudePublicMcpWitness(stdout, publicMcp);
|
|
614
|
+
if (failure) throw publicWitnessError(failure);
|
|
615
|
+
}
|
|
616
|
+
return claudeResultText(stdout, { credentials });
|
|
617
|
+
};
|
|
618
|
+
|
|
536
619
|
// The failure tail for a non-zero exit. Stream events are mostly system init
|
|
537
620
|
// and status lines, which would push the terminal error past the 2000-char cut,
|
|
538
621
|
// so keep the result text, or the lines that are not system or stream events.
|
|
@@ -1191,6 +1274,10 @@ export default {
|
|
|
1191
1274
|
// undefined and '' as absence itself; it does not need a guard, it needs
|
|
1192
1275
|
// the value.
|
|
1193
1276
|
const publicTrust = isConfinedTrust(ctx.environment?.sandbox?.trust);
|
|
1277
|
+
// Only a stranger-facing seat is witnessed (TASK-256). A workspace seat is
|
|
1278
|
+
// confined too, but it answers its own team, and an outage it narrates
|
|
1279
|
+
// there is a signal, not a leak.
|
|
1280
|
+
const witnessedMcp = ctx.environment?.sandbox?.trust === 'public' ? ctx.environment?.mcp : null;
|
|
1194
1281
|
const workCues = publicTrust ? [] : WORK_CUES;
|
|
1195
1282
|
const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
1196
1283
|
freshSession: !isResume,
|
|
@@ -1330,12 +1417,16 @@ export default {
|
|
|
1330
1417
|
const contextTokens = claudeContextTokens({
|
|
1331
1418
|
cwd: physicalPath(ctx.cwd), sessionId, configDir: claudeConfigDirFor({ ...env, HOME: childHome || env?.HOME }),
|
|
1332
1419
|
});
|
|
1333
|
-
return {
|
|
1420
|
+
return {
|
|
1421
|
+
text: turnReply(stdout, { credentials: spawnCredentials(spawnCtx), publicMcp: witnessedMcp }),
|
|
1422
|
+
newSessionId: sessionId,
|
|
1423
|
+
contextTokens,
|
|
1424
|
+
};
|
|
1334
1425
|
} catch (err) {
|
|
1335
1426
|
// A public seat's refusal is not a session-store failure, whatever its
|
|
1336
1427
|
// message happens to contain: it must reach the run loop, never the
|
|
1337
1428
|
// fresh-session retry below (#2247).
|
|
1338
|
-
if (err?.code
|
|
1429
|
+
if ([PUBLIC_MCP_UNAVAILABLE_CODE, PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE].includes(err?.code)) throw err;
|
|
1339
1430
|
// Self-heal against a corrupted session store. If the persisted id is
|
|
1340
1431
|
// already in use (or claude has forgotten about it), discard it and
|
|
1341
1432
|
// restart the turn with a fresh UUID. Without this, a single bad
|
|
@@ -1393,7 +1484,11 @@ export default {
|
|
|
1393
1484
|
cwd: physicalPath(ctx.cwd), sessionId: freshId,
|
|
1394
1485
|
configDir: claudeConfigDirFor({ ...retry.env, HOME: retry.childHome || retry.env?.HOME }),
|
|
1395
1486
|
});
|
|
1396
|
-
return {
|
|
1487
|
+
return {
|
|
1488
|
+
text: turnReply(stdout, { credentials: spawnCredentials(retryCtx), publicMcp: witnessedMcp }),
|
|
1489
|
+
newSessionId: freshId,
|
|
1490
|
+
contextTokens,
|
|
1491
|
+
};
|
|
1397
1492
|
}
|
|
1398
1493
|
throw err;
|
|
1399
1494
|
} finally {
|
|
@@ -56,7 +56,7 @@ import { wrapArgvWithSeatbelt } from '../sandbox/seatbelt.js';
|
|
|
56
56
|
import { resolvePublicSandboxMode } from '../sandbox/mode.js';
|
|
57
57
|
import { resolveProviderKeyFile, startProviderProxy } from '../provider-proxy.js';
|
|
58
58
|
import { isShippedCommonlyMcpCommand } from '../declared-mcp-guard.js';
|
|
59
|
-
import { isAnswerableToolError } from '../public-tool-error.js';
|
|
59
|
+
import { isAnswerableToolError, PUBLIC_TOOL_FAILED_CODE } from '../public-tool-error.js';
|
|
60
60
|
import { startInstanceProxy } from '../instance-proxy.js';
|
|
61
61
|
import { PUBLIC_WITHHELD_TOOLS } from '../public-withheld-tools.js';
|
|
62
62
|
|
|
@@ -390,7 +390,7 @@ const makeEventParser = () => {
|
|
|
390
390
|
// stays beside the adapter-agnostic readiness witness (TASK-256): it sees the
|
|
391
391
|
// runtime's own view of each call, and the witness sees a server that never
|
|
392
392
|
// started, so each catches what the other cannot.
|
|
393
|
-
export
|
|
393
|
+
export { PUBLIC_TOOL_FAILED_CODE };
|
|
394
394
|
|
|
395
395
|
export const publicToolFailure = (toolErrors, mcpServers) => {
|
|
396
396
|
const prefixes = (Array.isArray(mcpServers) ? mcpServers : [])
|
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
|
|
34
34
|
export const ANSWERABLE_API_STATUSES = new Set([400, 403, 404, 409, 410, 413, 422]);
|
|
35
35
|
|
|
36
|
+
// The two ways a public turn is withheld over its Commonly MCP server: a tool
|
|
37
|
+
// call failed in a way the model must not narrate, or the server was never shown
|
|
38
|
+
// working. Both leave the event unacked for retry.
|
|
39
|
+
export const PUBLIC_TOOL_FAILED_CODE = 'public_tool_failed';
|
|
40
|
+
export const PUBLIC_MCP_NOT_READY_CODE = 'public_mcp_not_ready';
|
|
41
|
+
|
|
36
42
|
const INVALID_ARGUMENTS_RE = /^The \S+ tool was called with invalid arguments: /;
|
|
37
43
|
|
|
38
44
|
const parsePayload = (text) => {
|
package/src/lib/spawn-retry.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* retry policy here so every adapter gets the same bounded behavior.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE } from './public-tool-error.js';
|
|
11
|
+
|
|
10
12
|
export const SPAWN_FAILURE_CLASS = Object.freeze({
|
|
11
13
|
// The provider refused the request's CONTENT. Re-sending the same content
|
|
12
14
|
// can only be refused again, so this class is never retried: the run loop
|
|
@@ -121,6 +123,15 @@ const errorText = (error) => [
|
|
|
121
123
|
|
|
122
124
|
export const classifySpawnFailure = (error) => {
|
|
123
125
|
const text = errorText(error);
|
|
126
|
+
// A public seat's withheld turn (TASK-256) quotes the tool's own error, which
|
|
127
|
+
// is vendor text: the code decides, so the event stays unacked for retry and
|
|
128
|
+
// is never acked as refused or parked behind a quota or configuration
|
|
129
|
+
// cooldown by that text. A throttle keeps its backoff, which also retries.
|
|
130
|
+
if (error?.code === PUBLIC_TOOL_FAILED_CODE || error?.code === PUBLIC_MCP_NOT_READY_CODE) {
|
|
131
|
+
return error?.status === 429 || RATE_LIMIT_RE.test(text)
|
|
132
|
+
? SPAWN_FAILURE_CLASS.RATE_LIMIT
|
|
133
|
+
: SPAWN_FAILURE_CLASS.RUNTIME;
|
|
134
|
+
}
|
|
124
135
|
// First: the refusal text carries none of the words below today, but a
|
|
125
136
|
// refusal must never be read as a transient condition and retried.
|
|
126
137
|
if (CONTENT_REFUSED_RE.test(text)) return SPAWN_FAILURE_CLASS.CONTENT_REFUSED;
|