@1presence/bridge 0.32.0 → 0.34.0
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/dist/claude.js +115 -27
- package/dist/index.js +4 -4
- package/package.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SECTION_COLORS = void 0;
|
|
3
4
|
exports.setVerbose = setVerbose;
|
|
4
5
|
exports.setDebug = setDebug;
|
|
6
|
+
exports.paint = paint;
|
|
5
7
|
exports.spawnClaude = spawnClaude;
|
|
6
8
|
exports.killAll = killAll;
|
|
7
9
|
const child_process_1 = require("child_process");
|
|
@@ -65,8 +67,12 @@ const USE_COLOR = process.stderr.isTTY === true && !process.env['NO_COLOR'];
|
|
|
65
67
|
function paint(code, s) {
|
|
66
68
|
return USE_COLOR ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
67
69
|
}
|
|
68
|
-
// ANSI colour codes per section, mirroring the admin debug palette.
|
|
69
|
-
|
|
70
|
+
// ANSI colour codes per section, mirroring the admin debug palette. Shared
|
|
71
|
+
// across all three console modes (debug / verbose / normal) so the same kind
|
|
72
|
+
// of content is always the same colour — system prompts magenta, user prompts
|
|
73
|
+
// blue, assistant text green, tool inputs cyan, tool results yellow.
|
|
74
|
+
exports.SECTION_COLORS = {
|
|
75
|
+
system: '35', // magenta
|
|
70
76
|
user: '34', // blue
|
|
71
77
|
assistant: '32', // green
|
|
72
78
|
input: '36', // cyan
|
|
@@ -97,7 +103,7 @@ function summariseHistoryBlock(block) {
|
|
|
97
103
|
// can tell user turns from assistant turns at a glance — the missing
|
|
98
104
|
// distinction that made replayed context unreadable in --debug.
|
|
99
105
|
function renderHistoryMessage(msg) {
|
|
100
|
-
const color = msg.role === 'user' ?
|
|
106
|
+
const color = msg.role === 'user' ? exports.SECTION_COLORS.user : exports.SECTION_COLORS.assistant;
|
|
101
107
|
const body = typeof msg.content === 'string'
|
|
102
108
|
? msg.content
|
|
103
109
|
: msg.content.map(summariseHistoryBlock).join('\n');
|
|
@@ -105,25 +111,64 @@ function renderHistoryMessage(msg) {
|
|
|
105
111
|
}
|
|
106
112
|
// ─── Active processes ─────────────────────────────────────────────────────────
|
|
107
113
|
const active = new Map();
|
|
114
|
+
// Maximum automatic retries when the `claude` CLI exits non-zero BEFORE
|
|
115
|
+
// producing any real output. This covers the known Claude Code print-mode 400
|
|
116
|
+
// regression that surfaces as "API Error: 400 due to tool use concurrency
|
|
117
|
+
// issues" (GitHub anthropics/claude-code#18131, still open) — it is
|
|
118
|
+
// non-deterministic enough that a fresh spawn frequently succeeds. We retry
|
|
119
|
+
// ONLY when the failed attempt produced no real assistant text and no tool
|
|
120
|
+
// calls, so a failure that lands after real work (where retrying could
|
|
121
|
+
// double-execute a side-effectful tool) is surfaced, never silently re-run.
|
|
122
|
+
const MAX_TURN_RETRIES = 1;
|
|
123
|
+
// Map a non-zero CLI exit + any captured "API Error:" line to a concise,
|
|
124
|
+
// user-facing Local Mode message. The raw upstream text stays in operator logs
|
|
125
|
+
// only — we never echo a wall of provider error JSON into the chat. Referring
|
|
126
|
+
// to "Claude Code" here is intentional and consistent with Local Mode's other
|
|
127
|
+
// operational errors (e.g. the "claude CLI not found" message): in Local Mode
|
|
128
|
+
// the user is knowingly running their own Claude Code install.
|
|
129
|
+
//
|
|
130
|
+
// NOTE on the 400 tool-use case: this is an open Claude Code print-mode
|
|
131
|
+
// regression (introduced in 2.1.19, still present in 2.1.146 — the current
|
|
132
|
+
// latest), so upgrading does NOT fix it. We deliberately do not suggest an
|
|
133
|
+
// upgrade; the automatic retry is the real mitigation and resending sometimes
|
|
134
|
+
// gets through.
|
|
135
|
+
function describeCliFailure(code, apiErrorText) {
|
|
136
|
+
const t = apiErrorText.trim();
|
|
137
|
+
if (/API Error:\s*400/i.test(t) && /(tool use|concurren|parallel)/i.test(t)) {
|
|
138
|
+
return 'Local Mode hit a known Claude Code error (a print-mode bug that affects every current version). I retried once automatically — sending the message again sometimes gets through.';
|
|
139
|
+
}
|
|
140
|
+
if (/^API Error:/i.test(t)) {
|
|
141
|
+
return `Local Mode error from Claude Code: ${t.replace(/^API Error:\s*/i, '').trim()}`;
|
|
142
|
+
}
|
|
143
|
+
return `Local Mode stopped unexpectedly (claude exited with code ${code ?? 'unknown'}). Please try again.`;
|
|
144
|
+
}
|
|
108
145
|
// ─── Spawn ────────────────────────────────────────────────────────────────────
|
|
109
146
|
function spawnClaude(params) {
|
|
110
147
|
const { conversationId, presenceSessionId, text, uid, history, vaultFileOpen, clientCapabilities, syncedFolders, onEvent, onDone, onError } = params;
|
|
148
|
+
const attemptIdx = params._attemptIdx ?? 0;
|
|
111
149
|
const systemPromptPath = (0, path_1.join)((0, os_1.tmpdir)(), `agent-${uid}.md`);
|
|
112
150
|
const mcpConfigPath = (0, path_1.join)((0, os_1.tmpdir)(), `mcp-${uid}.json`);
|
|
113
151
|
if (verbose) {
|
|
114
|
-
process.stderr.write(`[bridge:verbose] cwd: ${BRIDGE_CWD}\n
|
|
115
|
-
process.stderr.write(`[bridge:verbose] override md: ${(0, path_1.join)(BRIDGE_CWD, 'CLAUDE.md')}\n
|
|
116
|
-
process.stderr.write(`[bridge:verbose] system prompt: ${systemPromptPath}\n
|
|
117
|
-
process.stderr.write(`[bridge:verbose] mcp config: ${mcpConfigPath}\n
|
|
118
|
-
process.stderr.write(`[bridge:verbose] session id: ${presenceSessionId}\n
|
|
119
|
-
process.stderr.write(`[bridge:verbose] conversation: ${conversationId}\n
|
|
120
|
-
process.stderr.write(`[bridge:verbose] history turns: ${history.length}\n
|
|
152
|
+
process.stderr.write(paint('90', `[bridge:verbose] cwd: ${BRIDGE_CWD}`) + '\n');
|
|
153
|
+
process.stderr.write(paint('90', `[bridge:verbose] override md: ${(0, path_1.join)(BRIDGE_CWD, 'CLAUDE.md')}`) + '\n');
|
|
154
|
+
process.stderr.write(paint('90', `[bridge:verbose] system prompt: ${systemPromptPath}`) + '\n');
|
|
155
|
+
process.stderr.write(paint('90', `[bridge:verbose] mcp config: ${mcpConfigPath}`) + '\n');
|
|
156
|
+
process.stderr.write(paint('90', `[bridge:verbose] session id: ${presenceSessionId}`) + '\n');
|
|
157
|
+
process.stderr.write(paint('90', `[bridge:verbose] conversation: ${conversationId}`) + '\n');
|
|
158
|
+
process.stderr.write(paint('90', `[bridge:verbose] history turns: ${history.length}`) + '\n');
|
|
121
159
|
}
|
|
160
|
+
// Surface the user's UID before the session line in every mode — it's the
|
|
161
|
+
// Firestore doc prefix (`sessions/<uid>_<conversationId>`), so logging it
|
|
162
|
+
// makes a reported bridge failure correlatable to the stored session without
|
|
163
|
+
// having to ask which account hit it. The CLI's own `--session-id` is
|
|
164
|
+
// ephemeral and is NOT the Firestore conversationId, so the uid is the key
|
|
165
|
+
// join column when debugging.
|
|
166
|
+
process.stderr.write(`[bridge] user ${uid}\n`);
|
|
122
167
|
// Debug transcript: lead with the user prompt for this turn (the clean
|
|
123
168
|
// message, before the gateway's ephemeral-context prefix), plus the session
|
|
124
169
|
// id (correlates with the chat URL / Firestore session doc) and a hint at
|
|
125
170
|
// how much prior context is being replayed.
|
|
126
|
-
if (debug) {
|
|
171
|
+
if (debug || verbose) {
|
|
127
172
|
process.stderr.write(`\n${paint('1', `══ session ${presenceSessionId} ══`)}\n`);
|
|
128
173
|
// `history` already ends with the new user prompt (gateway-appended). Render
|
|
129
174
|
// every PRIOR message with its role colour so what the model saw as context
|
|
@@ -137,9 +182,9 @@ function spawnClaude(params) {
|
|
|
137
182
|
for (const msg of prior)
|
|
138
183
|
renderHistoryMessage(msg);
|
|
139
184
|
}
|
|
140
|
-
debugBlock('user · this turn',
|
|
185
|
+
debugBlock('user · this turn', exports.SECTION_COLORS.user, text);
|
|
141
186
|
}
|
|
142
|
-
else
|
|
187
|
+
else {
|
|
143
188
|
// Default mode is quiet, but always surface the session id once per turn so
|
|
144
189
|
// it can be matched to the chat URL / Firestore session doc when debugging.
|
|
145
190
|
process.stderr.write(`[bridge] session ${presenceSessionId}\n`);
|
|
@@ -192,6 +237,10 @@ function spawnClaude(params) {
|
|
|
192
237
|
// across turns of a chat — even with --no-session-persistence. The
|
|
193
238
|
// bridge passes the per-spawn `conversationId` here; the presence
|
|
194
239
|
// sessionId is correlated separately via bridge logs and spool records.
|
|
240
|
+
// The CLI treats --session-id as "claim this new session ID" and rejects a
|
|
241
|
+
// reused id with "Session ID X is already in use". A retry is a fresh spawn,
|
|
242
|
+
// so it MUST use a new uuid; the first attempt keeps the correlation id.
|
|
243
|
+
const spawnSessionId = attemptIdx === 0 ? presenceSessionId : crypto.randomUUID();
|
|
195
244
|
const args = [
|
|
196
245
|
'--print',
|
|
197
246
|
'--input-format', 'stream-json',
|
|
@@ -204,7 +253,7 @@ function spawnClaude(params) {
|
|
|
204
253
|
'--mcp-config', mcpConfigPath,
|
|
205
254
|
'--strict-mcp-config',
|
|
206
255
|
'--no-session-persistence',
|
|
207
|
-
'--session-id',
|
|
256
|
+
'--session-id', spawnSessionId,
|
|
208
257
|
];
|
|
209
258
|
const pinnedModel = (0, config_1.getBridgeModel)();
|
|
210
259
|
if (pinnedModel) {
|
|
@@ -260,6 +309,15 @@ function spawnClaude(params) {
|
|
|
260
309
|
let extractedModel = null;
|
|
261
310
|
let buffer = '';
|
|
262
311
|
let killedForViolation = false;
|
|
312
|
+
// Retry/error-surfacing tracking for this attempt:
|
|
313
|
+
// - sawApiError: the CLI emitted an "API Error:" assistant text event (the
|
|
314
|
+
// way Claude Code reports an underlying API failure mid-turn).
|
|
315
|
+
// - apiErrorText: that text, captured for describeCliFailure().
|
|
316
|
+
// - producedRealOutput: any real assistant text or tool_use was emitted, so
|
|
317
|
+
// a later failure must NOT be retried (could double-run a side-effect).
|
|
318
|
+
let sawApiError = false;
|
|
319
|
+
let apiErrorText = '';
|
|
320
|
+
let producedRealOutput = false;
|
|
263
321
|
proc.stdout.on('data', (chunk) => {
|
|
264
322
|
buffer += chunk.toString('utf-8');
|
|
265
323
|
const lines = buffer.split('\n');
|
|
@@ -276,6 +334,10 @@ function spawnClaude(params) {
|
|
|
276
334
|
continue;
|
|
277
335
|
}
|
|
278
336
|
const type = event['type'];
|
|
337
|
+
// Set when this event is the CLI's "API Error:" turn — we neither forward
|
|
338
|
+
// it to the PWA nor let it reach the accumulator (it carries no real
|
|
339
|
+
// content and would poison history / show a raw error mid-stream).
|
|
340
|
+
let suppressEvent = false;
|
|
279
341
|
// Extract model + key source info from the first system/init event.
|
|
280
342
|
// No session-id persistence — Firestore is the only source of truth
|
|
281
343
|
// now, and we pin --session-id to presenceSessionId on every spawn.
|
|
@@ -316,13 +378,14 @@ function spawnClaude(params) {
|
|
|
316
378
|
let wroteText = false;
|
|
317
379
|
for (const block of content) {
|
|
318
380
|
if (block['type'] === 'tool_use') {
|
|
381
|
+
producedRealOutput = true;
|
|
319
382
|
const toolName = block['name'];
|
|
320
383
|
const toolId = block['id'];
|
|
321
384
|
if (toolId)
|
|
322
385
|
toolNames.set(toolId, toolName);
|
|
323
386
|
if (debug) {
|
|
324
387
|
// Clean transcript: a single coloured block with the full input.
|
|
325
|
-
debugBlock(`tool → ${toolName}`,
|
|
388
|
+
debugBlock(`tool → ${toolName}`, exports.SECTION_COLORS.input, formatPayload(block['input']));
|
|
326
389
|
}
|
|
327
390
|
else {
|
|
328
391
|
if (wroteText) {
|
|
@@ -330,10 +393,10 @@ function spawnClaude(params) {
|
|
|
330
393
|
wroteText = false;
|
|
331
394
|
}
|
|
332
395
|
const prefix = toolName.startsWith('mcp__') ? '[mcp]' : '[tool]';
|
|
333
|
-
process.stderr.write(`[bridge] ${prefix} ${toolName}\n
|
|
396
|
+
process.stderr.write(paint(exports.SECTION_COLORS.input, `[bridge] ${prefix} ${toolName}`) + '\n');
|
|
334
397
|
if (verbose) {
|
|
335
398
|
const input = block['input'];
|
|
336
|
-
process.stderr.write(`[bridge:verbose] ─── input ${toolName} ───\n${formatPayload(input)}\n[bridge:verbose] ─── end input
|
|
399
|
+
process.stderr.write(paint(exports.SECTION_COLORS.input, `[bridge:verbose] ─── input ${toolName} ───\n${formatPayload(input)}\n[bridge:verbose] ─── end input ───`) + '\n');
|
|
337
400
|
}
|
|
338
401
|
}
|
|
339
402
|
// Defense-in-depth: CLI flags (--tools "", --allowedTools, --strict-mcp-config,
|
|
@@ -364,13 +427,27 @@ function spawnClaude(params) {
|
|
|
364
427
|
else if (block['type'] === 'text') {
|
|
365
428
|
const text = block['text'];
|
|
366
429
|
if (text) {
|
|
367
|
-
if (
|
|
368
|
-
//
|
|
369
|
-
|
|
430
|
+
if (/^API Error:/i.test(text.trimStart())) {
|
|
431
|
+
// The CLI is reporting an underlying API failure as assistant
|
|
432
|
+
// text. Capture it for the user-facing message, and suppress
|
|
433
|
+
// the whole event so the raw error never reaches the PWA or
|
|
434
|
+
// the accumulator (the gateway also blanks it via
|
|
435
|
+
// cleanTurnText — this is the upstream defense).
|
|
436
|
+
sawApiError = true;
|
|
437
|
+
apiErrorText = text.trim();
|
|
438
|
+
suppressEvent = true;
|
|
439
|
+
process.stderr.write(paint(exports.SECTION_COLORS.result, `[bridge] ${text.replace(/\n+/g, ' ')}`) + '\n');
|
|
370
440
|
}
|
|
371
441
|
else {
|
|
372
|
-
|
|
373
|
-
|
|
442
|
+
producedRealOutput = true;
|
|
443
|
+
if (debug) {
|
|
444
|
+
// Full text, newlines intact — the readable transcript.
|
|
445
|
+
debugBlock('assistant', exports.SECTION_COLORS.assistant, text);
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
process.stderr.write(paint(exports.SECTION_COLORS.assistant, text.replace(/\n+/g, ' ')));
|
|
449
|
+
wroteText = true;
|
|
450
|
+
}
|
|
374
451
|
}
|
|
375
452
|
}
|
|
376
453
|
}
|
|
@@ -391,10 +468,10 @@ function spawnClaude(params) {
|
|
|
391
468
|
if (debug) {
|
|
392
469
|
const name = toolNames.get(id) ?? id ?? 'result';
|
|
393
470
|
const errFlag = block['is_error'] ? ' [error]' : '';
|
|
394
|
-
debugBlock(`result ← ${name}${errFlag}`,
|
|
471
|
+
debugBlock(`result ← ${name}${errFlag}`, exports.SECTION_COLORS.result, formatPayload(out));
|
|
395
472
|
}
|
|
396
473
|
else {
|
|
397
|
-
process.stderr.write(`[bridge:verbose] ─── output ${id} ───\n${formatPayload(out)}\n[bridge:verbose] ─── end output
|
|
474
|
+
process.stderr.write(paint(exports.SECTION_COLORS.result, `[bridge:verbose] ─── output ${id} ───\n${formatPayload(out)}\n[bridge:verbose] ─── end output ───`) + '\n');
|
|
398
475
|
}
|
|
399
476
|
}
|
|
400
477
|
}
|
|
@@ -406,7 +483,8 @@ function spawnClaude(params) {
|
|
|
406
483
|
if (typeof c === 'number')
|
|
407
484
|
costUsd = c;
|
|
408
485
|
}
|
|
409
|
-
|
|
486
|
+
if (!suppressEvent)
|
|
487
|
+
onEvent(event);
|
|
410
488
|
}
|
|
411
489
|
});
|
|
412
490
|
proc.stderr.on('data', (chunk) => {
|
|
@@ -428,9 +506,19 @@ function spawnClaude(params) {
|
|
|
428
506
|
catch { /* ignore */ }
|
|
429
507
|
}
|
|
430
508
|
if (code !== 0 && code !== null) {
|
|
509
|
+
// Auto-retry once when the CLI failed BEFORE producing any real output —
|
|
510
|
+
// the signature of the known print-mode 400 regression. A fresh spawn
|
|
511
|
+
// (new --session-id) usually succeeds. We never retry once real text or a
|
|
512
|
+
// tool call landed, to avoid double-running a side-effectful tool.
|
|
513
|
+
if (attemptIdx < MAX_TURN_RETRIES && sawApiError && !producedRealOutput) {
|
|
514
|
+
process.stderr.write(`[bridge] turn failed before output (${apiErrorText.replace(/\n+/g, ' ').slice(0, 120)}) — retrying once\n`);
|
|
515
|
+
spawnClaude({ ...params, _attemptIdx: attemptIdx + 1 });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
431
518
|
// Pass any partial token usage we observed before the failure so the
|
|
432
|
-
// PWA and the gateway's bridge usage store can still record it.
|
|
433
|
-
|
|
519
|
+
// PWA and the gateway's bridge usage store can still record it. Surface a
|
|
520
|
+
// classified, user-readable message instead of the opaque exit code.
|
|
521
|
+
onError(describeCliFailure(code, apiErrorText), usage, extractedModel);
|
|
434
522
|
}
|
|
435
523
|
else {
|
|
436
524
|
onDone(messageCount, costUsd, usage, extractedModel);
|
package/dist/index.js
CHANGED
|
@@ -98,9 +98,9 @@ async function writeSystemPrompt(auth, agentSlug) {
|
|
|
98
98
|
const systemPrompt = await fetchSystemPrompt(token, agentSlug);
|
|
99
99
|
writeRestricted(tmpFile(`agent-${uid}.md`), systemPrompt);
|
|
100
100
|
if (VERBOSE) {
|
|
101
|
-
console.log('\n[bridge:verbose] ─── system prompt ───────────────────────');
|
|
102
|
-
console.log(systemPrompt);
|
|
103
|
-
console.log('[bridge:verbose] ─── end system prompt ───────────────────\n');
|
|
101
|
+
console.log((0, claude_1.paint)(claude_1.SECTION_COLORS.system, '\n[bridge:verbose] ─── system prompt ───────────────────────'));
|
|
102
|
+
console.log((0, claude_1.paint)(claude_1.SECTION_COLORS.system, systemPrompt));
|
|
103
|
+
console.log((0, claude_1.paint)(claude_1.SECTION_COLORS.system, '[bridge:verbose] ─── end system prompt ───────────────────\n'));
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
function writeMcpConfig(auth) {
|
|
@@ -430,7 +430,7 @@ async function main() {
|
|
|
430
430
|
console.log(`1Presence Bridge v${package_json_1.version}\n`);
|
|
431
431
|
if (VERBOSE) {
|
|
432
432
|
(0, claude_1.setVerbose)(true);
|
|
433
|
-
console.log('[bridge:verbose] verbose logging enabled — system prompts, tool inputs, and tool outputs will be printed.\n');
|
|
433
|
+
console.log('[bridge:verbose] verbose logging enabled — system prompts (magenta), user prompts (blue), assistant text (green), tool inputs (cyan), and tool outputs (yellow) will be printed, colour-coded by kind.\n');
|
|
434
434
|
}
|
|
435
435
|
if (DEBUG) {
|
|
436
436
|
(0, claude_1.setDebug)(true);
|