@commonlyai/cli 0.1.221 → 0.1.223
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
package/src/commands/agent.js
CHANGED
|
@@ -96,6 +96,7 @@ import { createSeatStop } from '../lib/seat-stop.js';
|
|
|
96
96
|
import { detectSeatbelt } from '../lib/sandbox/seatbelt.js';
|
|
97
97
|
import { describeUpstreamRefusal } from '../lib/upstream-refusal.js';
|
|
98
98
|
import { createTurnRunner, supervisedRunPolicy } from '../lib/supervised-turn.js';
|
|
99
|
+
import { describeFirstOutputWatch } from '../lib/first-output-watchdog.js';
|
|
99
100
|
import {
|
|
100
101
|
DEFAULT_HOOK_TIMEOUT_MS,
|
|
101
102
|
clampHookTimeoutMs,
|
|
@@ -1586,6 +1587,7 @@ export const performRun = ({
|
|
|
1586
1587
|
seat: agentName, turnTimeoutMs: effectiveTurnTimeoutMs, supervise: supervising, log,
|
|
1587
1588
|
});
|
|
1588
1589
|
if (supervising) log(turnRunner.describe(adapter?.name));
|
|
1590
|
+
if (adapter?.name === 'claude') log(describeFirstOutputWatch());
|
|
1589
1591
|
const runtimeReporter = createRuntimeReporter({
|
|
1590
1592
|
client: runtimeReportClient || { post: async () => {} },
|
|
1591
1593
|
harness: adapter.name, detect: () => adapter.detect(),
|
|
@@ -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.
|
|
@@ -593,6 +676,13 @@ const runClaude = ({
|
|
|
593
676
|
const table = Number.isInteger(proc.pid) ? processTable() : null;
|
|
594
677
|
if (table) stallSnapshot = descendantsOf(table, proc.pid);
|
|
595
678
|
killProcessTree(proc, 'SIGTERM', treeIo(() => table));
|
|
679
|
+
// A stopped process holds a SIGTERM it handles (claude does) until it is
|
|
680
|
+
// continued, so a turn frozen with SIGSTOP sat out the whole grace below:
|
|
681
|
+
// 191 s on a 180 s window (rivet, 2026-11-01). SIGCONT lets it act on the
|
|
682
|
+
// SIGTERM now. A process that is not stopped is unaffected unless it
|
|
683
|
+
// handles SIGCONT (a job-control shell does); the SIGKILL below still
|
|
684
|
+
// lands at the grace either way.
|
|
685
|
+
killProcessTree(proc, 'SIGCONT', treeIo(() => table));
|
|
596
686
|
// A process parked in a system call may not act on SIGTERM. SIGKILL the
|
|
597
687
|
// child, its group when it leads one, and its descendants.
|
|
598
688
|
stallKill = setTimeout(() => {
|
|
@@ -1191,6 +1281,10 @@ export default {
|
|
|
1191
1281
|
// undefined and '' as absence itself; it does not need a guard, it needs
|
|
1192
1282
|
// the value.
|
|
1193
1283
|
const publicTrust = isConfinedTrust(ctx.environment?.sandbox?.trust);
|
|
1284
|
+
// Only a stranger-facing seat is witnessed (TASK-256). A workspace seat is
|
|
1285
|
+
// confined too, but it answers its own team, and an outage it narrates
|
|
1286
|
+
// there is a signal, not a leak.
|
|
1287
|
+
const witnessedMcp = ctx.environment?.sandbox?.trust === 'public' ? ctx.environment?.mcp : null;
|
|
1194
1288
|
const workCues = publicTrust ? [] : WORK_CUES;
|
|
1195
1289
|
const fullPrompt = buildPrompt(prompt, ctx.memoryLongTerm, {
|
|
1196
1290
|
freshSession: !isResume,
|
|
@@ -1330,12 +1424,16 @@ export default {
|
|
|
1330
1424
|
const contextTokens = claudeContextTokens({
|
|
1331
1425
|
cwd: physicalPath(ctx.cwd), sessionId, configDir: claudeConfigDirFor({ ...env, HOME: childHome || env?.HOME }),
|
|
1332
1426
|
});
|
|
1333
|
-
return {
|
|
1427
|
+
return {
|
|
1428
|
+
text: turnReply(stdout, { credentials: spawnCredentials(spawnCtx), publicMcp: witnessedMcp }),
|
|
1429
|
+
newSessionId: sessionId,
|
|
1430
|
+
contextTokens,
|
|
1431
|
+
};
|
|
1334
1432
|
} catch (err) {
|
|
1335
1433
|
// A public seat's refusal is not a session-store failure, whatever its
|
|
1336
1434
|
// message happens to contain: it must reach the run loop, never the
|
|
1337
1435
|
// fresh-session retry below (#2247).
|
|
1338
|
-
if (err?.code
|
|
1436
|
+
if ([PUBLIC_MCP_UNAVAILABLE_CODE, PUBLIC_MCP_NOT_READY_CODE, PUBLIC_TOOL_FAILED_CODE].includes(err?.code)) throw err;
|
|
1339
1437
|
// Self-heal against a corrupted session store. If the persisted id is
|
|
1340
1438
|
// already in use (or claude has forgotten about it), discard it and
|
|
1341
1439
|
// restart the turn with a fresh UUID. Without this, a single bad
|
|
@@ -1393,7 +1491,11 @@ export default {
|
|
|
1393
1491
|
cwd: physicalPath(ctx.cwd), sessionId: freshId,
|
|
1394
1492
|
configDir: claudeConfigDirFor({ ...retry.env, HOME: retry.childHome || retry.env?.HOME }),
|
|
1395
1493
|
});
|
|
1396
|
-
return {
|
|
1494
|
+
return {
|
|
1495
|
+
text: turnReply(stdout, { credentials: spawnCredentials(retryCtx), publicMcp: witnessedMcp }),
|
|
1496
|
+
newSessionId: freshId,
|
|
1497
|
+
contextTokens,
|
|
1498
|
+
};
|
|
1397
1499
|
}
|
|
1398
1500
|
throw err;
|
|
1399
1501
|
} 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 : [])
|
|
@@ -77,6 +77,17 @@ export const physicalPath = (dir) => {
|
|
|
77
77
|
// How long a stalled spawn gets to exit after SIGTERM before SIGKILL.
|
|
78
78
|
export const STALL_KILL_GRACE_MS = 10_000;
|
|
79
79
|
|
|
80
|
+
// The boot line for a claude seat. This watchdog is the stall guard a claude
|
|
81
|
+
// turn runs: the seats supervisor's stall window is off for claude
|
|
82
|
+
// (STALL_DEFAULTS_MS in supervised-turn.js), and its package default of 120 s
|
|
83
|
+
// was once read as this one's (the G6 walk, 2026-11-01).
|
|
84
|
+
export const describeFirstOutputWatch = (env = process.env) => {
|
|
85
|
+
const windowMs = firstOutputWindowMs(env);
|
|
86
|
+
if (!windowMs) return 'first-output watchdog OFF for claude (COMMONLY_FIRST_OUTPUT_WINDOW_MS=0)';
|
|
87
|
+
return `first-output watchdog ON for claude: a turn with no model output in ${windowMs}ms is killed `
|
|
88
|
+
+ `(SIGTERM, then SIGKILL ${STALL_KILL_GRACE_MS}ms later if it is still alive)`;
|
|
89
|
+
};
|
|
90
|
+
|
|
80
91
|
/**
|
|
81
92
|
* SIGKILL each snapshotted descendant of a stalled spawn that is still alive
|
|
82
93
|
* once the spawn itself has exited. Its parent may have exited, so it may now
|
|
@@ -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;
|