Skip to content

Commit e8628d1

Browse files
mabry1985Automakerclaudegithub-actions[bot]qwencoder
authored
promote: dev → main (Phase 1 reasoning, telemetry rebrand, qwen-logger nuke) — bumps to v0.28.0 (#169)
* fix(core): preserve tool history when building no-tools requests /recap (and any other caller without tools, e.g. /btw) was sending an empty conversation to the model. The no-tools branch in pipeline buildRequest dropped every assistant turn with tool_calls and every tool-role message wholesale, so in tool-heavy sessions the recap saw only bare user prompts and hallucinated context. - generateRecap now passes tools: [] so the strip path doesn't fire, matching cc-2.18's awaySummary pattern. - pipeline.ts no-tools branch now flattens instead of dropping: keeps assistant prose content and removes only the tool_calls field; tool results become [tool result] assistant notes truncated at 2000 chars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ci): release.yml now fires on auto-release/v* PRs (#160) auto-release.yml opens version-bump PRs from `auto-release/v*` branches into main, but release.yml's job gate only matched `head.ref == 'dev'`. Result: every auto-release PR was merging cleanly but skipping publish (v0.26.25 had to be dispatched manually). This adds the auto-release/* prefix to the gate and refreshes the stale top-of-file comment. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: release v0.26.26 (#161) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * feat(telemetry,ui): reasoning span attribute + collapsed thought summary (Phase 1 of #162) (#165) * feat(core): preserve task plan state in compaction summaries (#163) * feat(core): preserve task plan state in compaction summaries When context compaction fires, the agent loses awareness of its task plan (completed, in-progress, pending work) and may re-plan already-done tasks. Add extractTaskPlanSummary() that queries the TaskStore and produces a structured <task-plan> XML section with status markers ([x], [~], [ ], [-], [!]), priority labels, and parent-child indentation. Extend compactMessages() to accept an optional taskStore and append the plan to the compaction summary. Wire the TaskStore into agent-core at the compaction call site. Backward compatible: existing callers without taskStore remain unaffected. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: add error handling and recursive nesting to compaction task plan Address PR feedback from CodeRabbit: - Wrap extractTaskPlanSummary call in try/catch so TaskStore failures don't break compaction - Replace flat 2-level subtask rendering with recursive renderTask() that supports arbitrary nesting depth - Add tests for multi-level nesting and error fallback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * feat(telemetry,ui): capture reasoning on Langfuse span and collapse thoughts post-stream Phase 1 of the reasoning coordination tracked in #162. Captures delta.reasoning_content / delta.reasoning across stream chunks and surfaces it as gen_ai.response.thinking on the gen_ai chat span (gated on logPrompts, matching the completion event policy). Always emits gen_ai.usage.thinking_tokens when usage exposes it. Non-streaming responses get the same treatment by inspecting {thought:true} parts on the response — and the completion event no longer double-counts thoughts as content. Renders gemini_thought items as a compact "▸ thinking (N chars)" summary once the stream finalizes (live streaming render unchanged). Full text remains in ChatRecord, ACP agent_thought_chunk notifications, and Langfuse for downstream investigation. An in-TUI expand affordance is a follow-up. Once homelab-iac#31 (EMIT_REASONING_CONTENT) flips on, this also covers vLLM-served models that previously lost their <think> blocks at the gateway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: release v0.26.27 (#166) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore(telemetry): rebrand to proto-cli, nuke qwen-logger Alibaba RUM ping (#167) * chore(telemetry): rebrand qwen-code identifiers to proto-cli Aligns telemetry / public-facing identifiers with the actual product name. Verified against the Langfuse instance: new spans land with service.name=proto-cli on scope=proto.openai-pipeline; existing proto.* tracers (proto.llm, proto.turn, proto.tools, proto.harness, etc.) were already correct. Changes: - SERVICE_NAME: qwen-code → proto-cli (resource attribute, the marquee label in Langfuse's service column) - All EVENT_* constants: qwen-code.* → proto.* (matches the existing proto.harness.* convention already in this file) - pipeline.ts tracer: qwen-code.openai-pipeline → proto.openai-pipeline (one straggler vs. the 9 other proto.* tracers in core/) - types.ts event.name literals (PromptSuggestion, Speculation): qwen-code.* → proto.* - acpAgent.ts agentInfo.name: qwen-code → proto-cli (visible to ACP clients like Zed when listing agents) - marketplace.ts User-Agent: qwen-code → proto-cli (extension fetch identifier sent to api.github.com / raw.githubusercontent.com) Out of scope (deliberately): - packages/core/src/telemetry/qwen-logger/* — separate analytics ping to gb4w8c3ygj-default-sea.rum.aliyuncs.com (Alibaba RUM, the upstream Qwen team's endpoint). Should be disabled rather than rebranded; tracking separately. - DEFAULT_SERVICE_NAME='qwen-code-oauth' in mcp/token-storage — renaming would orphan existing keychain entries. - Misc qwen-code-* file paths, tmp dir names, sandbox image tag, test fixtures — not telemetry / not user-visible labels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(telemetry): remove qwen-logger Alibaba RUM ping; keep useful events on Langfuse The qwen-logger system shipped usage telemetry to a fixed Alibaba RUM endpoint (gb4w8c3ygj-default-sea.rum.aliyuncs.com) — the upstream Qwen Code team's analytics pipeline. We don't operate that endpoint, the data isn't visible to us, and it labelled traffic as qwen-code-cli / qwen-code@${version}. Confirmed unused on our deployment; nuking. What's removed: - packages/core/src/telemetry/qwen-logger/ (entire directory: logger, event-types, tests) - packages/core/src/telemetry/integration.test.circular.ts (was a qwen-logger-specific circular-reference proxy-agent test, no longer applicable) - ~30 QwenLogger.getInstance(config)?.logXxxEvent(event) callsites in loggers.ts - QwenLogger exports from telemetry/index.ts and core/index.ts - QwenLogger spies and assertions in config.test.ts and the describe('logHookCall', ...) block in loggers.test.ts that was exclusively QwenLogger-shaped What's kept and rerouted to OTel/Langfuse: - HookCallEvent type and the logHookCall function — hook execution data is genuinely useful telemetry (which hook fired, success, duration, exit code, captured stdout/stderr, error). Now emits a proto.hook_call OTel log record via logs.getLogger(SERVICE_NAME) instead of the Alibaba ping. Existing call site in hookEventHandler.ts:619 still fires per hook execution. - LoopDetectionDisabledEvent likewise: was an empty no-op after the qwen-logger pull; rerouted to a proto.loop_detection_disabled OTel log record so the signal still reaches Langfuse. - New tests in loggers.test.ts assert OTel emission shape for logHookCall (success, error, sdk-not-initialized branches). Renamed (per "all not used" — no existing keychain entries to invalidate): - DEFAULT_SERVICE_NAME 'qwen-code-oauth' → 'proto-cli-oauth' - FORCE_ENCRYPTED_FILE_ENV_VAR 'QWEN_CODE_…' → 'PROTO_CLI_…' - file-token-storage encryption salt prefix and scrypt key seed switched to proto-cli; only invalidates non-existent tokens Verified live: kimi-k2.6 turn through the rebuilt CLI lands a Langfuse trace with service=proto-cli, scope=proto.openai-pipeline, gen_ai.response.thinking present. No outbound traffic to aliyuncs.com. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: release v0.26.28 (#168) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1 parent 244cf48 commit e8628d1

29 files changed

Lines changed: 757 additions & 2531 deletions

.github/workflows/release.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
name: 'Release'
22

3-
# Fires when the dev→main promotion PR is merged.
3+
# Fires when a release-bearing PR is merged into main.
44
# Builds the bundle, publishes @protolabsai/proto to npm, tags, and creates a GitHub Release.
55
#
6-
# Flow: feature PRs → dev → prepare-release.yml bumps version on dev
7-
# → dev→main promotion PR merges → this workflow tags and releases.
6+
# Two flows trigger this:
7+
# 1. Auto-release: auto-release.yml opens an `auto-release/v*` PR with the
8+
# version bump, which merges to main and triggers this workflow.
9+
# 2. Manual dev→main promotion: dev branch PR'd into main (legacy path,
10+
# still supported).
811

912
on:
1013
pull_request:
@@ -24,7 +27,10 @@ jobs:
2427
github.event_name == 'workflow_dispatch' ||
2528
(
2629
github.event.pull_request.merged == true &&
27-
github.event.pull_request.head.ref == 'dev'
30+
(
31+
github.event.pull_request.head.ref == 'dev' ||
32+
startsWith(github.event.pull_request.head.ref, 'auto-release/')
33+
)
2834
)
2935
)
3036
timeout-minutes: 30

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@protolabsai/proto",
3-
"version": "0.27.0",
3+
"version": "0.28.0",
44
"publishConfig": {
55
"access": "public"
66
},
@@ -20,7 +20,7 @@
2020
"url": "https://github.com/protoLabsAI/protoCLI/issues"
2121
},
2222
"config": {
23-
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.27.0"
23+
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.28.0"
2424
},
2525
"scripts": {
2626
"start": "cross-env node scripts/start.js",

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@protolabs/proto",
3-
"version": "0.27.0",
3+
"version": "0.28.0",
44
"description": "proto",
55
"repository": {
66
"type": "git",
@@ -37,7 +37,7 @@
3737
"dist"
3838
],
3939
"config": {
40-
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.27.0"
40+
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.28.0"
4141
},
4242
"dependencies": {
4343
"@agentclientprotocol/sdk": "^0.14.1",

packages/cli/src/acp-integration/acpAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ class QwenAgent implements Agent {
159159
return {
160160
protocolVersion: PROTOCOL_VERSION,
161161
agentInfo: {
162-
name: 'qwen-code',
162+
name: 'proto-cli',
163163
title: 'proto',
164164
version,
165165
},
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Qwen Team
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, expect, it } from 'vitest';
8+
import { render } from 'ink-testing-library';
9+
import { ThinkMessage, ThinkMessageContent } from './ConversationMessages.js';
10+
11+
describe('ThinkMessage', () => {
12+
it('renders the streaming text expanded while pending', () => {
13+
const { lastFrame } = render(
14+
<ThinkMessage
15+
text="Let me consider this carefully and weigh the options."
16+
isPending={true}
17+
contentWidth={80}
18+
/>,
19+
);
20+
const output = lastFrame() ?? '';
21+
expect(output).toContain('Let me consider this');
22+
// Streaming render uses the existing ⟡ glyph, not the ▸ summary marker.
23+
expect(output).toContain('⟡');
24+
expect(output).not.toContain('thinking (');
25+
});
26+
27+
it('renders compact "thinking (N chars)" summary once stream finalizes', () => {
28+
const text = 'reasoning '.repeat(10).trim(); // 99 chars
29+
const { lastFrame } = render(
30+
<ThinkMessage text={text} isPending={false} contentWidth={80} />,
31+
);
32+
const output = lastFrame() ?? '';
33+
expect(output).toContain('▸');
34+
expect(output).toContain(`thinking (${text.length} chars)`);
35+
// Underlying reasoning text is not rendered inline post-stream.
36+
expect(output).not.toContain('reasoning reasoning');
37+
});
38+
39+
it('formats large char counts with thousands separator', () => {
40+
const text = 'x'.repeat(12_345);
41+
const { lastFrame } = render(
42+
<ThinkMessage text={text} isPending={false} contentWidth={80} />,
43+
);
44+
const output = lastFrame() ?? '';
45+
expect(output).toContain('thinking (12,345 chars)');
46+
});
47+
});
48+
49+
describe('ThinkMessageContent', () => {
50+
it('renders the continuation text while pending', () => {
51+
const { lastFrame } = render(
52+
<ThinkMessageContent
53+
text="continued reasoning text"
54+
isPending={true}
55+
contentWidth={80}
56+
/>,
57+
);
58+
const output = lastFrame() ?? '';
59+
expect(output).toContain('continued reasoning text');
60+
});
61+
62+
it('renders nothing once stream finalizes (the parent ThinkMessage owns the summary)', () => {
63+
const { lastFrame } = render(
64+
<ThinkMessageContent
65+
text="continued reasoning text"
66+
isPending={false}
67+
contentWidth={80}
68+
/>,
69+
);
70+
const output = lastFrame() ?? '';
71+
expect(output.trim()).toBe('');
72+
});
73+
});

packages/cli/src/ui/components/messages/ConversationMessages.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -227,35 +227,70 @@ export const AssistantMessageContent: React.FC<
227227
/>
228228
);
229229

230+
// Post-stream summary line ("▸ thinking (N chars)"). Phase 1 of the reasoning
231+
// rendering work (see #162): full text remains live in Langfuse, ACP
232+
// `agent_thought_chunk` notifications, and ChatRecord for back-compat. An
233+
// in-TUI expand affordance is a follow-up. Note: when a long thought was
234+
// split mid-stream into a gemini_thought + gemini_thought_content pair, this
235+
// counts only the first chunk — the continuation renders nothing (see below).
236+
// True total requires post-finalize coalescing in useGeminiStream and is
237+
// deferred since splits are rare and the count is a hint, not a contract.
238+
const ThinkSummary: React.FC<{ text: string }> = ({ text }) => {
239+
const charCount = text.length;
240+
return (
241+
<PrefixedTextMessage
242+
text={`thinking (${charCount.toLocaleString()} chars)`}
243+
prefix="▸"
244+
prefixColor={theme.text.secondary}
245+
textColor={theme.text.secondary}
246+
/>
247+
);
248+
};
249+
230250
export const ThinkMessage: React.FC<ThinkMessageProps> = ({
231251
text,
232252
isPending,
233253
availableTerminalHeight,
234254
contentWidth,
235-
}) => (
236-
<PrefixedMarkdownMessage
237-
text={text}
238-
prefix="⟡"
239-
prefixColor={theme.text.secondary}
240-
isPending={isPending}
241-
availableTerminalHeight={availableTerminalHeight}
242-
contentWidth={contentWidth}
243-
textColor={theme.text.secondary}
244-
/>
245-
);
255+
}) => {
256+
if (!isPending) {
257+
return <ThinkSummary text={text} />;
258+
}
259+
return (
260+
<PrefixedMarkdownMessage
261+
text={text}
262+
prefix="⟡"
263+
prefixColor={theme.text.secondary}
264+
isPending={isPending}
265+
availableTerminalHeight={availableTerminalHeight}
266+
contentWidth={contentWidth}
267+
textColor={theme.text.secondary}
268+
/>
269+
);
270+
};
246271

247272
export const ThinkMessageContent: React.FC<ThinkMessageContentProps> = ({
248273
text,
249274
isPending,
250275
availableTerminalHeight,
251276
contentWidth,
252-
}) => (
253-
<ContinuationMarkdownMessage
254-
text={text}
255-
isPending={isPending}
256-
availableTerminalHeight={availableTerminalHeight}
257-
contentWidth={contentWidth}
258-
basePrefix="⟡"
259-
textColor={theme.text.secondary}
260-
/>
261-
);
277+
}) => {
278+
// When the stream has finalized, suppress the continuation block. The
279+
// adjacent ThinkMessage already renders the summary line; rendering this
280+
// continuation as another summary would double-count and drop chars across
281+
// the split boundary. Streaming-time renders unchanged so live thoughts
282+
// still appear.
283+
if (!isPending) {
284+
return null;
285+
}
286+
return (
287+
<ContinuationMarkdownMessage
288+
text={text}
289+
isPending={isPending}
290+
availableTerminalHeight={availableTerminalHeight}
291+
contentWidth={contentWidth}
292+
basePrefix="⟡"
293+
textColor={theme.text.secondary}
294+
/>
295+
);
296+
};

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@qwen-code/qwen-code-core",
3-
"version": "0.27.0",
3+
"version": "0.28.0",
44
"description": "proto core",
55
"repository": {
66
"type": "git",

packages/core/src/config/config.test.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryT
1313
import {
1414
DEFAULT_TELEMETRY_TARGET,
1515
DEFAULT_OTLP_ENDPOINT,
16-
QwenLogger,
1716
} from '../telemetry/index.js';
1817
import type {
1918
ContentGenerator,
@@ -235,9 +234,6 @@ describe('Server Config (config.ts)', () => {
235234
beforeEach(() => {
236235
// Reset mocks if necessary
237236
vi.clearAllMocks();
238-
vi.spyOn(QwenLogger.prototype, 'logStartSessionEvent').mockImplementation(
239-
async () => undefined,
240-
);
241237

242238
// Setup default mock for resolveContentGeneratorConfigWithSources
243239
vi.mocked(resolveContentGeneratorConfigWithSources).mockImplementation(
@@ -635,16 +631,6 @@ describe('Server Config (config.ts)', () => {
635631
expect(config.getUsageStatisticsEnabled()).toBe(enabled);
636632
},
637633
);
638-
639-
it('logs the session start event', async () => {
640-
const config = new Config({
641-
...baseParams,
642-
usageStatisticsEnabled: true,
643-
});
644-
await config.initialize();
645-
646-
expect(QwenLogger.prototype.logStartSessionEvent).toHaveBeenCalledOnce();
647-
});
648634
});
649635

650636
describe('Telemetry Settings', () => {

packages/core/src/core/client.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ vi.mock('../telemetry/index.js', async (importOriginal) => {
146146
...actual,
147147
uiTelemetryService: mockUiTelemetryService,
148148
// We keep the real implementations of logChatCompression, etc.
149-
// but we can spy on QwenLogger if needed
150149
};
151150
});
152151
vi.mock('../ide/ideContext.js');

0 commit comments

Comments
 (0)