amicus 1.5.0 → 1.5.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "author": { "name": "Christian Wagner" },
6
6
  "homepage": "https://bourbondog.github.io/amicus/",
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to Amicus are documented here. Format follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.5.1] - 2026-06-29
9
+
10
+ A headless-reliability fix for reasoning-heavy models.
11
+
12
+ ### Fixed
13
+ - **Gemini (and other reasoning-only models) no longer hang headless with "No Output."** On the
14
+ direct-Google provider path, Gemini 3.x returns its answer as a `reasoning` part with no separate
15
+ `text` part. The conversation mirror only accumulated `text` parts, so it captured zero output, the
16
+ headless completion gates (which key on `output.length > 0`) never fired, and the run burned the full
17
+ timeout — while still billing input/thinking tokens. The mirror now accumulates reasoning into a
18
+ dedicated buffer and promotes it to the output **only when a finished assistant message produced no
19
+ visible text**, so models that emit both a reasoning part and a text part are unaffected (their
20
+ thinking never pollutes the answer). Fixes `--model gemini` / `gemini-pro` and any direct `google/*`
21
+ alias in headless `start`, `fanout`, and council runs.
22
+
8
23
  ## [1.5.0] - 2026-06-29
9
24
 
10
25
  A visual refresh plus a council-reliability fix and a config-dir consolidation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amicus",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
5
5
  "keywords": [
6
6
  "claude",
@@ -17,6 +17,8 @@ function createMirrorState() {
17
17
  seenToolResultIds: new Set(),
18
18
  receivingReported: false,
19
19
  output: '', // accumulated assistant text
20
+ seenReasoningParts: new Map(), // partId -> last captured reasoning length
21
+ reasoningOutput: '', // accumulated reasoning text (promoted to output only if no text part arrives)
20
22
  usageByMsg: new Map(), // msgId -> {tokens, cost}
21
23
  };
22
24
  }
@@ -101,6 +103,22 @@ function mirrorMessages(messages, state, opts = {}) {
101
103
  timestamp: now(),
102
104
  });
103
105
  }
106
+ } else if (part.type === 'reasoning' && part.text) {
107
+ // Some providers (e.g. Gemini 3.x on the direct Google path) return the
108
+ // answer as a reasoning part with no separate text part. Accumulate it in a
109
+ // dedicated buffer; it is promoted to `output` at finalization ONLY when no
110
+ // visible text part ever arrives (see below), so models that emit BOTH a
111
+ // reasoning part and a text part are unaffected (their thinking never
112
+ // pollutes the answer).
113
+ const prevLen = state.seenReasoningParts.get(partId) || 0;
114
+ if (part.text.length > prevLen) {
115
+ state.reasoningOutput += part.text.slice(prevLen);
116
+ state.seenReasoningParts.set(partId, part.text.length);
117
+ if (!state.receivingReported) {
118
+ state.receivingReported = true;
119
+ progressUpdates.push({ stage: 'receiving', extra: { messagesReceived: 1 } });
120
+ }
121
+ }
104
122
  }
105
123
  }
106
124
  }
@@ -110,6 +128,15 @@ function mirrorMessages(messages, state, opts = {}) {
110
128
  const lastAssistant = list.filter(m => m.info && m.info.role === 'assistant').pop();
111
129
  assistantFinished = !!(lastAssistant && lastAssistant.info.time && lastAssistant.info.time.completed);
112
130
 
131
+ // Reasoning-only fallback: if the assistant finished but emitted only reasoning
132
+ // parts (no visible text), promote the reasoning text to `output` so the headless
133
+ // completion gates fire and the answer isn't lost as "No Output". Runs once — once
134
+ // `output` is non-empty this is skipped on subsequent polls.
135
+ if (assistantFinished && !state.output && state.reasoningOutput) {
136
+ state.output = state.reasoningOutput;
137
+ appendLines.push({ role: 'assistant', content: state.reasoningOutput, timestamp: now() });
138
+ }
139
+
113
140
  return { appendLines, progressUpdates, state, currentAssistantMsgId, assistantFinished, sessionError, messageCount };
114
141
  }
115
142