@simonren/quorum 0.8.0 → 0.8.1

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.
@@ -124,27 +124,30 @@ export class ClaudeAdapter {
124
124
  const result = await executor.run();
125
125
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
126
126
  console.error(`[claude] ✓ complete (${elapsed}s)`);
127
- // Check for errors captured from stream events
127
+ // Prefer a completed response over a captured error: a non-fatal error event
128
+ // that precedes a successful agent_message must not discard the result (same
129
+ // class of bug fixed in the codex adapter). Only treat a decoder error as
130
+ // fatal when no final response was produced.
131
+ const finalResponse = decoder.getFinalResponse();
132
+ if (finalResponse) {
133
+ return {
134
+ stdout: finalResponse,
135
+ stderr: result.stderr,
136
+ exitCode: result.exitCode,
137
+ truncated: result.truncated,
138
+ };
139
+ }
128
140
  const decoderError = decoder.getError();
129
141
  if (decoderError) {
130
142
  const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
131
143
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
132
144
  }
133
- const finalResponse = decoder.getFinalResponse();
134
- if (!finalResponse && decoder.hasNoOutput()) {
145
+ if (decoder.hasNoOutput()) {
135
146
  const combined = result.stderr ? `No output from Claude\n\nCLI stderr: ${result.stderr}` : 'No output from Claude';
136
147
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
137
148
  }
138
- if (!finalResponse) {
139
- const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
140
- return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
141
- }
142
- return {
143
- stdout: finalResponse,
144
- stderr: result.stderr,
145
- exitCode: result.exitCode,
146
- truncated: result.truncated,
147
- };
149
+ const combined = result.stderr ? `No result event from Claude\n\nCLI stderr: ${result.stderr}` : 'No result event from Claude';
150
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
148
151
  }
149
152
  handleException(error, startTime) {
150
153
  const err = error;
@@ -125,27 +125,45 @@ export class CodexAdapter {
125
125
  const result = await executor.run();
126
126
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
127
127
  console.error(`[codex] ✓ complete (${elapsed}s)`);
128
- // Check for errors captured from JSONL events
128
+ // A genuine fatal error (a top-level `error` event or `turn.failed`) is
129
+ // TERMINAL and wins even when a — possibly intermediate/stale — agent_message
130
+ // exists. `--full-auto` runs a multi-turn loop, so a turn can emit a message
131
+ // and a later turn can fail; without this check, that failure would be
132
+ // reported as a successful review with stale content.
133
+ const fatalError = decoder.getFatalError();
134
+ if (fatalError) {
135
+ const combined = result.stderr ? `${fatalError}\n\nCLI stderr: ${result.stderr}` : fatalError;
136
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
137
+ }
138
+ // No fatal error: a completed response wins even if a NON-fatal error-item
139
+ // appeared earlier. Codex emits its "Skill descriptions were shortened to fit
140
+ // the 2% skills context budget" notice as an item.completed/type:error event —
141
+ // purely informational ("Codex can still see every skill"); the turn still
142
+ // completes and produces an agent_message. Use `!== null` (not truthiness) so
143
+ // an empty-string response is preserved for the caller's empty-response
144
+ // handling rather than misclassified as "no result event".
145
+ // (Checking getError() first discarded otherwise-successful reviews.)
146
+ const finalResponse = decoder.getFinalResponse();
147
+ if (finalResponse !== null) {
148
+ return {
149
+ stdout: finalResponse,
150
+ stderr: result.stderr,
151
+ exitCode: result.exitCode,
152
+ truncated: result.truncated,
153
+ };
154
+ }
155
+ // No response: surface a non-fatal item-level error if one was captured.
129
156
  const decoderError = decoder.getError();
130
157
  if (decoderError) {
131
158
  const combined = result.stderr ? `${decoderError}\n\nCLI stderr: ${result.stderr}` : decoderError;
132
159
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
133
160
  }
134
- const finalResponse = decoder.getFinalResponse();
135
- if (!finalResponse && decoder.hasNoOutput()) {
161
+ if (decoder.hasNoOutput()) {
136
162
  const combined = result.stderr ? `No output from Codex\n\nCLI stderr: ${result.stderr}` : 'No output from Codex';
137
163
  return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
138
164
  }
139
- if (!finalResponse) {
140
- const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
141
- return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
142
- }
143
- return {
144
- stdout: finalResponse,
145
- stderr: result.stderr,
146
- exitCode: result.exitCode,
147
- truncated: result.truncated,
148
- };
165
+ const combined = result.stderr ? `No result event from Codex\n\nCLI stderr: ${result.stderr}` : 'No result event from Codex';
166
+ return { stdout: '', stderr: combined, exitCode: 1, truncated: false };
149
167
  }
150
168
  handleException(error, startTime) {
151
169
  const err = error;
@@ -43,6 +43,7 @@ export declare class CodexEventDecoder {
43
43
  private _finalResponse;
44
44
  private _usage;
45
45
  private _error;
46
+ private _fatalError;
46
47
  private _eventCount;
47
48
  /**
48
49
  * Parse a single JSONL line. Silently skips malformed or empty input.
@@ -59,9 +60,18 @@ export declare class CodexEventDecoder {
59
60
  */
60
61
  getUsage(): CodexEvent['usage'] | null;
61
62
  /**
62
- * Returns the error message from `error` or `turn.failed` events, or `null`.
63
+ * Returns the error message from any source a top-level `error`, a
64
+ * `turn.failed`, or a non-fatal item-level error notice — or `null`.
65
+ * Superset of `getFatalError()`.
63
66
  */
64
67
  getError(): string | null;
68
+ /**
69
+ * Returns the message from a TERMINAL fatal error only — a top-level `error`
70
+ * event or `turn.failed` — or `null`. The benign item-level error notice
71
+ * (e.g. the "skills shortened" budget message) is NOT fatal and is excluded,
72
+ * so a successful turn can win over it without masking a genuine failure.
73
+ */
74
+ getFatalError(): string | null;
65
75
  /**
66
76
  * Returns true if events were received but no agent_message was produced.
67
77
  * Combined with a fast exit, this indicates rate limiting or instant rejection.
@@ -25,8 +25,14 @@ export class CodexEventDecoder {
25
25
  _finalResponse = null;
26
26
  // Token usage from the most recently seen turn.completed
27
27
  _usage = null;
28
- // Error message from error/turn.failed events
28
+ // Error message from ANY source (top-level error, turn.failed, or a non-fatal
29
+ // item-level error notice). Superset of _fatalError.
29
30
  _error = null;
31
+ // Error message from a TERMINAL fatal source ONLY: a top-level `error` event
32
+ // or `turn.failed`. Kept separate from the benign item-level error notice
33
+ // (e.g. Codex's "skills shortened" budget message) so the adapter can let a
34
+ // completed response override the benign notice WITHOUT masking a real failure.
35
+ _fatalError = null;
30
36
  // Count of events received (0 = possible rate limit / instant rejection)
31
37
  _eventCount = 0;
32
38
  // =============================================================================
@@ -68,11 +74,22 @@ export class CodexEventDecoder {
68
74
  return this._usage;
69
75
  }
70
76
  /**
71
- * Returns the error message from `error` or `turn.failed` events, or `null`.
77
+ * Returns the error message from any source a top-level `error`, a
78
+ * `turn.failed`, or a non-fatal item-level error notice — or `null`.
79
+ * Superset of `getFatalError()`.
72
80
  */
73
81
  getError() {
74
82
  return this._error;
75
83
  }
84
+ /**
85
+ * Returns the message from a TERMINAL fatal error only — a top-level `error`
86
+ * event or `turn.failed` — or `null`. The benign item-level error notice
87
+ * (e.g. the "skills shortened" budget message) is NOT fatal and is excluded,
88
+ * so a successful turn can win over it without masking a genuine failure.
89
+ */
90
+ getFatalError() {
91
+ return this._fatalError;
92
+ }
76
93
  /**
77
94
  * Returns true if events were received but no agent_message was produced.
78
95
  * Combined with a fast exit, this indicates rate limiting or instant rejection.
@@ -95,14 +112,19 @@ export class CodexEventDecoder {
95
112
  if (event.type === 'turn.completed' && event.usage != null) {
96
113
  this._usage = event.usage;
97
114
  }
98
- // Capture errors from error/turn.failed events
115
+ // Capture errors. A top-level `error` event and `turn.failed` are TERMINAL
116
+ // failures — record them as fatal (and in the general _error superset).
99
117
  if (event.type === 'error') {
100
118
  this._error = event.message || 'Unknown error from Codex';
119
+ this._fatalError = this._error;
101
120
  }
102
121
  if (event.type === 'turn.failed') {
103
122
  this._error = event.error?.message || 'Turn failed';
123
+ this._fatalError = this._error;
104
124
  }
105
- // Capture error items (e.g. model errors reported as item.completed with type=error)
125
+ // An item.completed/type:error is a NON-fatal notice (e.g. the "skills
126
+ // shortened" budget message, or a per-item model error). Record it only in
127
+ // the general _error bucket — never as fatal — so it cannot mask a response.
106
128
  if (event.type === 'item.completed' && event.item?.type === 'error') {
107
129
  this._error = event.item.message || event.item.text || 'Model error';
108
130
  }
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonren/quorum",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "MCP server for Claude Code — a quorum of AI models (Codex, Gemini, Claude) for adversarial review and consultation, synthesized by Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",