Skip to content

Commit 5fe501a

Browse files
committed
release: v3.3.4 continuity and state endpoints
1 parent 07babfd commit 5fe501a

13 files changed

Lines changed: 664 additions & 301 deletions

RELEASE_NOTES_v3.3.4.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Augment Proxy Manager v3.3.4 - Continuity 修复与协议状态承接
2+
3+
## 核心结论
4+
5+
v3.3.4 的改动集中在代理层的 continuation handling、history projection、state endpoint compatibility 和 per-conversation serialization:
6+
7+
- continuation `"..."` 仅作为 continuation signal 处理,不再改写用户消息内容
8+
- 原始 `chat_history` 保留,压缩结果单独写入 `compressed_chat_history`
9+
- `save-chat``record-*``context-canvas/list` 新增最小状态写入与读取逻辑
10+
- 同一 `conversation_id` 的请求通过 `state.conversationQueues` 串行化处理
11+
12+
## 本次修复
13+
14+
### 1. Continuity 主链修复
15+
- continuation `"..."` 按 continuation signal 处理,不再通过 prompt 改写参与消息编译
16+
- 原始 `chat_history` 保留为会话原始记录
17+
- 压缩结果单独写入 `compressed_chat_history`
18+
- provider 统一从 canonical / normalized timeline 编译消息
19+
20+
### 2. 状态端点最小实现
21+
以下端点已从固定成功响应调整为最小状态实现:
22+
23+
- `/save-chat`
24+
- `/record-session-events`
25+
- `/record-user-events`
26+
- `/record-request-events`
27+
- `/context-canvas/list`
28+
- `/generate-conversation-title`
29+
- `/notifications/mark-read`(兼容处理)
30+
31+
代理层新增以下内存状态容器,用于保存最小会话状态:
32+
33+
- `conversationStates`
34+
- `canvasStates`
35+
- `sessionEventStore`
36+
- `userEventStore`
37+
- `requestEventStore`
38+
39+
### 3. 会话级请求串行化
40+
- 请求队列统一挂到全局 `state.conversationQueues`
41+
- 同一 `conversation_id` 的请求按顺序执行,避免并发交错影响工具调用链
42+
43+
## 验证结果
44+
45+
已完成本地 runtime smoke:
46+
47+
- `save-chat`
48+
- `record-session-events`
49+
- `record-user-events`
50+
- `record-request-events`
51+
- `context-canvas/list`
52+
- `generate-conversation-title`
53+
54+
以上端点均返回与会话状态相关的结构化结果,而不是固定 `success: true` 响应。

package-lock.json

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

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "augment-proxy-manager",
33
"displayName": "Augment Proxy Manager",
44
"description": "管理 Augment API 代理服务器,支持自定义 API 端点和多种 AI 供应商",
5-
"version": "3.1.4",
5+
"version": "3.3.4",
66
"publisher": "legna",
77
"repository": {
88
"type": "git",
@@ -24,7 +24,7 @@
2424
{
2525
"id": "augmentProxy",
2626
"title": "Augment Proxy",
27-
"icon": "$(radio-tower)"
27+
"icon": "icon.svg"
2828
}
2929
]
3030
},
@@ -33,7 +33,8 @@
3333
{
3434
"type": "webview",
3535
"id": "augmentProxy.sidebar",
36-
"name": "控制面板"
36+
"name": "控制面板",
37+
"icon": "icon.svg"
3738
}
3839
]
3940
},

readme.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44

55
# Augment Proxy Manager
66

7-
**用任意 AI 供应商驱动 Augment 的强大编码 Agent**
7+
**Augment 前端 + 多模型后端的本地协议代理**
88

99
零注入 · 零登录 · 零配置
1010

11-
[![Version](https://img.shields.io/badge/version-3.1.4-blue.svg)](https://github.com/LegnaOS/VSC-Augment-Proxy-Manager)
11+
[![Version](https://img.shields.io/badge/version-3.3.4-blue.svg)](https://github.com/LegnaOS/VSC-Augment-Proxy-Manager)
1212
[![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Windows%20%7C%20Linux-lightgrey.svg)]()
1313

1414
</div>
@@ -19,6 +19,8 @@
1919

2020
Augment Proxy Manager 运行一个本地 HTTP 代理服务器,拦截 Augment 扩展的 API 请求并转发到你选择的 AI 供应商。
2121

22+
代理层负责承接 Augment 的请求格式、上下文投影、工具调用和状态端点,再转换到不同模型后端。
23+
2224
```
2325
Augment 扩展 → 本地代理 (:8765) → 你的 AI 供应商 API
2426
↑ 自动配置 ↑ Viking 上下文增强
@@ -66,6 +68,9 @@ Augment 扩展 → 本地代理 (:8765) → 你的 AI 供应商 API
6668
- **零注入绕过** — 自动配置 Augment 使用代理,无需修改任何代码
6769
- **流式响应** — 聊天、补全、指令全程实时 SSE 流式传输
6870
- **完整 Agent 模式** — 工具调用、文件编辑、代码库检索全部正常工作
71+
- **协议转换层** — 代理统一承接 Augment 请求格式,并转换到 Anthropic / OpenAI / Google 等不同后端协议
72+
- **continuity 修复** — 保留原始 `chat_history`,压缩结果写入 `compressed_chat_history`,避免多步任务上下文被压坏
73+
- **状态端点最小实现**`/save-chat``/record-session-events``/record-user-events``/record-request-events``/context-canvas/list` 已从固定成功响应调整为最小状态实现
6974
- **配置热更新** — 切换供应商或模型无需重启代理,实时生效
7075

7176
### 🔍 RAG 语义搜索
@@ -157,6 +162,14 @@ src/
157162

158163
## 更新日志
159164

165+
### v3.3.4 — Continuity 修复 + 协议状态承接
166+
167+
- **continuation handling**`"..."` 按 continuation signal 处理,不再改写用户消息内容
168+
- **history projection** — 原始 `chat_history` 保留,压缩结果写入 `compressed_chat_history`
169+
- **state endpoints**`save-chat` / `record-*` / `context-canvas/list` 新增最小状态写入与读取逻辑
170+
- **request serialization** — 同一 `conversation_id` 的请求统一进入 `state.conversationQueues`
171+
- **runtime smoke** — 已验证 `save-chat``record-session-events``record-user-events``record-request-events``context-canvas/list``generate-conversation-title`
172+
160173
### v3.1.4 — Agent 循环修复 + 任务系统生效
161174

162175
**🔴 致命修复**

src/context-compression.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export async function applyContextCompression(augmentReq: any, providerName: str
88
const compressionThresholdPercent = config.get('compressionThreshold', 60) as number; // 降低到 60%
99
const compressionThreshold = compressionThresholdPercent / 100;
1010

11+
delete augmentReq.compressed_chat_history;
12+
1113
if (!enableCompression || !augmentReq.chat_history || augmentReq.chat_history.length === 0) return;
1214

1315
const modelName = state.currentConfig.model || 'unknown';
@@ -38,7 +40,7 @@ export async function applyContextCompression(augmentReq: any, providerName: str
3840
if (contextStats.needs_compression || contextStats.usage_percentage > (preemptiveThreshold * 100)) {
3941
const compressionResult = await compressChatHistoryByTokens(augmentReq.chat_history, tokenLimit, 0.3, compressionThreshold); // 目标 30%
4042
if (compressionResult.compressed_count < compressionResult.original_count) {
41-
augmentReq.chat_history = compressionResult.compressed_exchanges;
43+
augmentReq.compressed_chat_history = compressionResult.compressed_exchanges;
4244
log(`[CONTEXT] ✂️ 压缩: ${compressionResult.original_count}${compressionResult.compressed_count} 次交互`);
4345
log(`[CONTEXT] 📉 Token: ${compressionResult.estimated_tokens_before}${compressionResult.estimated_tokens_after} (${(compressionResult.compression_ratio * 100).toFixed(1)}%)`);
4446
if (compressionResult.summary) log(`[CONTEXT] 📝 摘要: ${compressionResult.summary.slice(0, 80)}...`);

src/context-manager.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ function estimateExchangesTokens(exchanges: Exchange[]): number {
158158
return totalTokens;
159159
}
160160

161+
function hasToolUse(exchange?: Exchange): boolean {
162+
return !!exchange?.response_nodes?.some((node: any) => node.type === 5 && node.tool_use);
163+
}
164+
161165
/**
162166
* 获取上下文统计信息
163167
*/
@@ -238,8 +242,14 @@ export async function compressChatHistoryByTokens(
238242
keepCount = Math.max(keepCount, Math.min(3, chatHistory.length));
239243

240244
// 分离最近的和需要压缩的历史
241-
const recentExchanges = chatHistory.slice(-keepCount);
242-
const oldExchanges = chatHistory.slice(0, -keepCount);
245+
// 不要在 tool_use / tool_result 的边界中间切断,否则会破坏多步任务连续性
246+
let splitIndex = Math.max(0, chatHistory.length - keepCount);
247+
while (splitIndex > 0 && hasToolUse(chatHistory[splitIndex - 1])) {
248+
splitIndex--;
249+
}
250+
251+
const recentExchanges = chatHistory.slice(splitIndex);
252+
const oldExchanges = chatHistory.slice(0, splitIndex);
243253

244254
if (oldExchanges.length === 0) {
245255
return {
@@ -255,6 +265,7 @@ export async function compressChatHistoryByTokens(
255265
// 生成旧历史的摘要
256266
const summary = generateHistorySummary(oldExchanges);
257267
const summaryTokens = estimateTokens(summary);
268+
const recentTokens = estimateExchangesTokens(recentExchanges);
258269

259270
// 创建一个摘要交互
260271
const summaryExchange: Exchange = {
@@ -269,7 +280,7 @@ export async function compressChatHistoryByTokens(
269280
};
270281

271282
const compressedExchanges = [summaryExchange, ...recentExchanges];
272-
const tokensAfter = summaryTokens + accumulatedTokens;
283+
const tokensAfter = summaryTokens + recentTokens;
273284

274285
return {
275286
compressed_exchanges: compressedExchanges,
@@ -278,7 +289,7 @@ export async function compressChatHistoryByTokens(
278289
compressed_count: compressedExchanges.length,
279290
estimated_tokens_before: tokensBefore,
280291
estimated_tokens_after: tokensAfter,
281-
compression_ratio: tokensAfter / tokensBefore
292+
compression_ratio: tokensBefore > 0 ? tokensAfter / tokensBefore : 1.0
282293
};
283294
}
284295

src/endpoints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export const AUGMENT_ENDPOINTS = [
2323
// 订阅和用户
2424
'/subscription-banner',
2525
'/save-chat',
26+
'/context-canvas/list',
27+
'/generate-conversation-title',
2628
// 用户密钥
2729
'/user-secrets/list',
2830
'/user-secrets/upsert',
@@ -33,6 +35,7 @@ export const AUGMENT_ENDPOINTS = [
3335
// 遥测和事件
3436
'/client-completion-timelines',
3537
'/record-session-events',
38+
'/record-user-events',
3639
'/record-request-events',
3740
// 其他
3841
'/next-edit-stream',

src/globals.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,42 @@ import { CurrentConfig } from './types';
44
import type { VikingContextStore } from './rag/viking-context';
55
import type { SessionMemory } from './rag/session-memory';
66

7+
export interface RecordedEvent {
8+
id: string;
9+
source: 'session' | 'user' | 'request';
10+
type: string;
11+
recordedAt: string;
12+
conversationId?: string;
13+
sessionId?: string;
14+
requestId?: string;
15+
canvasId?: string;
16+
userId?: string;
17+
payload: any;
18+
}
19+
20+
export interface ConversationState {
21+
conversationId: string;
22+
canvasId?: string;
23+
title?: string;
24+
createdAt: string;
25+
updatedAt: string;
26+
lastMessage?: string;
27+
lastRequestId?: string;
28+
chatHistory: any[];
29+
compressedChatHistory?: any[];
30+
nodes: any[];
31+
metadata?: Record<string, any>;
32+
}
33+
34+
export interface CanvasState {
35+
canvasId: string;
36+
conversationId?: string;
37+
title: string;
38+
createdAt: string;
39+
updatedAt: string;
40+
metadata?: Record<string, any>;
41+
}
42+
743
// ===== 全局共享状态 =====
844
// 所有模块通过 state 对象访问共享状态
945

@@ -22,7 +58,13 @@ export const state = {
2258

2359
// 会话级请求队列
2460
conversationQueues: new Map<string, Promise<void>>(),
25-
conversationUserMessages: new Map<string, string>(),
61+
62+
// 最小状态承接
63+
conversationStates: new Map<string, ConversationState>(),
64+
canvasStates: new Map<string, CanvasState>(),
65+
sessionEventStore: new Map<string, RecordedEvent[]>(),
66+
userEventStore: new Map<string, RecordedEvent[]>(),
67+
requestEventStore: new Map<string, RecordedEvent[]>(),
2668

2769
// 当前配置
2870
currentConfig: {

src/injection.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ function generateInjectionCode(proxyUrl: string): string {
9292
if (urlStr.includes('/chat-stream')) ep = '/chat-stream'; else if (urlStr.includes('/chat-input-completion')) ep = '/chat-input-completion'; else if (urlStr.includes('/chat')) ep = '/chat'; else if (urlStr.includes('/instruction-stream')) ep = '/instruction-stream'; else if (urlStr.includes('/smart-paste-stream')) ep = '/smart-paste-stream'; else if (urlStr.includes('/completion')) ep = '/completion';
9393
else if (urlStr.includes('/getPluginState')) ep = '/getPluginState'; else if (urlStr.includes('/get-model-config')) ep = '/get-model-config'; else if (urlStr.includes('/get-models')) ep = '/get-models';
9494
else if (urlStr.includes('/agents/codebase-retrieval')) ep = '/agents/codebase-retrieval'; else if (urlStr.includes('/agents/edit-file')) ep = '/agents/edit-file'; else if (urlStr.includes('/agents/list-remote-tools')) ep = '/agents/list-remote-tools'; else if (urlStr.includes('/agents/run-remote-tool')) ep = '/agents/run-remote-tool';
95-
else if (urlStr.includes('/remote-agents/list-stream')) ep = '/remote-agents/list-stream'; else if (urlStr.includes('/subscription-banner')) ep = '/subscription-banner'; else if (urlStr.includes('/save-chat')) ep = '/save-chat';
95+
else if (urlStr.includes('/remote-agents/list-stream')) ep = '/remote-agents/list-stream'; else if (urlStr.includes('/subscription-banner')) ep = '/subscription-banner'; else if (urlStr.includes('/save-chat')) ep = '/save-chat'; else if (urlStr.includes('/context-canvas/list')) ep = '/context-canvas/list'; else if (urlStr.includes('/generate-conversation-title')) ep = '/generate-conversation-title';
9696
else if (urlStr.includes('/user-secrets/list')) ep = '/user-secrets/list'; else if (urlStr.includes('/user-secrets/upsert')) ep = '/user-secrets/upsert'; else if (urlStr.includes('/user-secrets/delete')) ep = '/user-secrets/delete';
9797
else if (urlStr.includes('/notifications/mark-read')) ep = '/notifications/mark-read'; else if (urlStr.includes('/notifications')) ep = '/notifications';
98-
else if (urlStr.includes('/client-completion-timelines')) ep = '/client-completion-timelines'; else if (urlStr.includes('/record-session-events')) ep = '/record-session-events'; else if (urlStr.includes('/record-request-events')) ep = '/record-request-events';
98+
else if (urlStr.includes('/client-completion-timelines')) ep = '/client-completion-timelines'; else if (urlStr.includes('/record-session-events')) ep = '/record-session-events'; else if (urlStr.includes('/record-user-events')) ep = '/record-user-events'; else if (urlStr.includes('/record-request-events')) ep = '/record-request-events';
9999
else if (urlStr.includes('/next-edit-stream')) ep = '/next-edit-stream'; else if (urlStr.includes('/find-missing')) ep = '/find-missing'; else if (urlStr.includes('/client-metrics')) ep = '/client-metrics'; else if (urlStr.includes('/batch-upload')) ep = '/batch-upload'; else if (urlStr.includes('/report-feature-vector')) ep = '/report-feature-vector'; else if (urlStr.includes('/report-error')) ep = '/report-error';
100100
if (!ep) { log('Passing through (no matching endpoint):', urlStr); return originalFetch.call(this, url, options); }
101101
const target = CONFIG.proxyUrl + ep; log('Intercepted:', urlStr, '->', target);
@@ -105,7 +105,7 @@ function generateInjectionCode(proxyUrl: string): string {
105105
// Node.js http/https 拦截
106106
try {
107107
const https = require('https'); const http = require('http'); const oHttps = https.request; const oHttp = http.request;
108-
const getEp = (u) => { if(u.includes('/chat-stream'))return'/chat-stream';if(u.includes('/chat-input-completion'))return'/chat-input-completion';if(u.includes('/chat'))return'/chat';if(u.includes('/instruction-stream'))return'/instruction-stream';if(u.includes('/smart-paste-stream'))return'/smart-paste-stream';if(u.includes('/completion'))return'/completion';if(u.includes('/getPluginState'))return'/getPluginState';if(u.includes('/get-model-config'))return'/get-model-config';if(u.includes('/get-models'))return'/get-models';if(u.includes('/agents/codebase-retrieval'))return'/agents/codebase-retrieval';if(u.includes('/agents/edit-file'))return'/agents/edit-file';if(u.includes('/agents/list-remote-tools'))return'/agents/list-remote-tools';if(u.includes('/agents/run-remote-tool'))return'/agents/run-remote-tool';if(u.includes('/remote-agents/list-stream'))return'/remote-agents/list-stream';if(u.includes('/next-edit-stream'))return'/next-edit-stream';if(u.includes('/find-missing'))return'/find-missing';if(u.includes('/client-metrics'))return'/client-metrics';if(u.includes('/batch-upload'))return'/batch-upload';if(u.includes('/report-feature-vector'))return'/report-feature-vector';if(u.includes('/report-error'))return'/report-error';return null; };
108+
const getEp = (u) => { if(u.includes('/chat-stream'))return'/chat-stream';if(u.includes('/chat-input-completion'))return'/chat-input-completion';if(u.includes('/chat'))return'/chat';if(u.includes('/instruction-stream'))return'/instruction-stream';if(u.includes('/smart-paste-stream'))return'/smart-paste-stream';if(u.includes('/completion'))return'/completion';if(u.includes('/getPluginState'))return'/getPluginState';if(u.includes('/get-model-config'))return'/get-model-config';if(u.includes('/get-models'))return'/get-models';if(u.includes('/agents/codebase-retrieval'))return'/agents/codebase-retrieval';if(u.includes('/agents/edit-file'))return'/agents/edit-file';if(u.includes('/agents/list-remote-tools'))return'/agents/list-remote-tools';if(u.includes('/agents/run-remote-tool'))return'/agents/run-remote-tool';if(u.includes('/remote-agents/list-stream'))return'/remote-agents/list-stream';if(u.includes('/subscription-banner'))return'/subscription-banner';if(u.includes('/save-chat'))return'/save-chat';if(u.includes('/context-canvas/list'))return'/context-canvas/list';if(u.includes('/generate-conversation-title'))return'/generate-conversation-title';if(u.includes('/record-session-events'))return'/record-session-events';if(u.includes('/record-user-events'))return'/record-user-events';if(u.includes('/record-request-events'))return'/record-request-events';if(u.includes('/next-edit-stream'))return'/next-edit-stream';if(u.includes('/find-missing'))return'/find-missing';if(u.includes('/client-metrics'))return'/client-metrics';if(u.includes('/batch-upload'))return'/batch-upload';if(u.includes('/report-feature-vector'))return'/report-feature-vector';if(u.includes('/report-error'))return'/report-error';return null; };
109109
const wrap = (orig, proto) => function(urlOrOpts, opts, cb) { let t=''; if(typeof urlOrOpts==='string')t=urlOrOpts; else if(urlOrOpts&&urlOrOpts.hostname)t=proto+'://'+urlOrOpts.hostname+(urlOrOpts.path||''); if(!CONFIG.enabled||!t.includes('augmentcode.com')||!CONFIG.proxyAvailable)return orig.apply(this,arguments); const ep=getEp(t); if(!ep){log('HTTP: pass through:',t.substring(0,80));return orig.apply(this,arguments);} log('HTTP: Intercepting '+ep); const po={hostname:'localhost',port:8765,path:ep,method:(typeof urlOrOpts==='object'?urlOrOpts.method:'GET')||'GET',headers:typeof urlOrOpts==='object'?urlOrOpts.headers:{}}; po.headers['Content-Type']='application/json'; return oHttp.call(http,po,typeof opts==='function'?opts:cb); };
110110
https.request = wrap(oHttps, 'https'); http.request = wrap(oHttp, 'http'); log('Node.js http intercepted');
111111
} catch (e) { log('Failed to intercept http:', e.message); }

0 commit comments

Comments
 (0)