@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.221",
3
+ "version": "0.1.223",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -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 { text: claudeResultText(stdout, { credentials: spawnCredentials(spawnCtx) }), newSessionId: sessionId, contextTokens };
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 === PUBLIC_MCP_UNAVAILABLE_CODE) throw err;
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 { text: claudeResultText(stdout, { credentials: spawnCredentials(retryCtx) }), newSessionId: freshId, contextTokens };
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 const PUBLIC_TOOL_FAILED_CODE = 'public_tool_failed';
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) => {
@@ -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;