@lyhue1991/dsh-soup 0.5.7 → 0.5.8
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/lib/host/speed.js +43 -20
- package/package.json +1 -1
package/lib/host/speed.js
CHANGED
|
@@ -83,45 +83,64 @@ function extractChunk(chunk) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function createSpeedTracker() {
|
|
86
|
+
// sessionId -> concurrent stream states. Background tasks (subagents, title
|
|
87
|
+
// generation) open LLM streams under the same sessionId as the main reply,
|
|
88
|
+
// so a session may genuinely have several live streams at once; a single
|
|
89
|
+
// slot per session let a short background stream clobber the main reply's
|
|
90
|
+
// state (badge vanished while the main answer was still streaming).
|
|
86
91
|
const streams = new Map()
|
|
92
|
+
const MAX_STATES_PER_KEY = 4
|
|
87
93
|
|
|
88
94
|
function wrap(options, next) {
|
|
89
95
|
const key = sessionKey(options) || '_'
|
|
90
96
|
const now = Date.now()
|
|
91
97
|
const state = { phase: 'waiting', startedAt: now, firstChunkAt: null, charTokens: 0, realTokens: 0, hasReal: false, lastChunkAt: null, lastContentAt: null, samples: [], lastSeen: now }
|
|
92
|
-
streams.
|
|
98
|
+
const states = streams.get(key) || []
|
|
99
|
+
states.push(state)
|
|
100
|
+
while (states.length > MAX_STATES_PER_KEY) states.shift()
|
|
101
|
+
streams.set(key, states)
|
|
93
102
|
const innerPromise = Promise.resolve(next())
|
|
94
103
|
return (async function* () {
|
|
95
104
|
try {
|
|
96
105
|
const inner = await innerPromise
|
|
97
106
|
for await (const chunk of inner) {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
state.lastSeen = time
|
|
111
|
-
}
|
|
107
|
+
// Each stream state is independent: a concurrent (or later) stream
|
|
108
|
+
// for the same session never blocks or corrupts this one's bookkeeping.
|
|
109
|
+
const parsed = extractChunk(chunk)
|
|
110
|
+
const time = Date.now()
|
|
111
|
+
if (state.firstChunkAt === null) { state.firstChunkAt = time; state.phase = 'streaming' }
|
|
112
|
+
if (parsed.text) state.charTokens += estimateChars(parsed.text)
|
|
113
|
+
if (parsed.realTokens != null) { state.realTokens = parsed.realTokens; state.hasReal = true }
|
|
114
|
+
state.lastChunkAt = time
|
|
115
|
+
if (parsed.text || parsed.realTokens != null) state.lastContentAt = time
|
|
116
|
+
state.samples.push([time, state.hasReal ? state.realTokens : state.charTokens])
|
|
117
|
+
if (state.samples.length > 128) state.samples.splice(0, state.samples.length - 128)
|
|
118
|
+
state.lastSeen = time
|
|
112
119
|
yield chunk
|
|
113
120
|
}
|
|
114
121
|
} finally {
|
|
115
|
-
|
|
122
|
+
state.phase = 'done'
|
|
123
|
+
state.lastSeen = Date.now()
|
|
116
124
|
}
|
|
117
125
|
})()
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
function status(sessionId) {
|
|
121
|
-
let
|
|
122
|
-
if (
|
|
123
|
-
|
|
129
|
+
let pool = streams.get(sessionId) || []
|
|
130
|
+
if (pool.length === 0) {
|
|
131
|
+
// Cold-session fallback: pick the session with the freshest activity.
|
|
132
|
+
let best = null
|
|
133
|
+
for (const states of streams.values()) {
|
|
134
|
+
const latest = states[states.length - 1]
|
|
135
|
+
if (latest && (!best || latest.lastSeen > best.lastSeen)) best = latest
|
|
136
|
+
}
|
|
137
|
+
pool = best ? [best] : []
|
|
124
138
|
}
|
|
139
|
+
// Prefer the most recently active still-running stream (the main reply),
|
|
140
|
+
// falling back to the most recent settled one only when nothing is live.
|
|
141
|
+
const live = pool.filter((s) => s.phase !== 'done')
|
|
142
|
+
const sorted = (live.length ? live : pool).slice().sort((a, b) => b.lastSeen - a.lastSeen)
|
|
143
|
+
const state = sorted[0]
|
|
125
144
|
if (!state) return { phase: 'idle' }
|
|
126
145
|
const now = Date.now()
|
|
127
146
|
const tokens = state.hasReal ? state.realTokens : Math.round(state.charTokens)
|
|
@@ -145,7 +164,11 @@ export function createSpeedTracker() {
|
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
function reap(now = Date.now()) {
|
|
148
|
-
for (const [key,
|
|
167
|
+
for (const [key, states] of streams) {
|
|
168
|
+
const kept = states.filter((state) => state.phase !== 'done' || now - state.lastSeen <= 4000)
|
|
169
|
+
if (kept.length === 0) streams.delete(key)
|
|
170
|
+
else if (kept.length !== states.length) streams.set(key, kept)
|
|
171
|
+
}
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
return { wrap, status, reap }
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyhue1991/dsh-soup",
|
|
3
3
|
"description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度对齐宿主 details 合同 300-520px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"scripts": {
|