@lyhue1991/dsh-soup 0.5.7 → 0.5.9
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 +45 -20
- package/package.json +1 -1
package/lib/host/speed.js
CHANGED
|
@@ -83,45 +83,66 @@ 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 && !sessionId) {
|
|
131
|
+
// No-id fallback (badge could not resolve any session id): pick the
|
|
132
|
+
// session with the freshest activity. A real sessionId must never see
|
|
133
|
+
// another session's traffic, so an unknown id simply stays idle.
|
|
134
|
+
let best = null
|
|
135
|
+
for (const states of streams.values()) {
|
|
136
|
+
const latest = states[states.length - 1]
|
|
137
|
+
if (latest && (!best || latest.lastSeen > best.lastSeen)) best = latest
|
|
138
|
+
}
|
|
139
|
+
pool = best ? [best] : []
|
|
124
140
|
}
|
|
141
|
+
// Prefer the most recently active still-running stream (the main reply),
|
|
142
|
+
// falling back to the most recent settled one only when nothing is live.
|
|
143
|
+
const live = pool.filter((s) => s.phase !== 'done')
|
|
144
|
+
const sorted = (live.length ? live : pool).slice().sort((a, b) => b.lastSeen - a.lastSeen)
|
|
145
|
+
const state = sorted[0]
|
|
125
146
|
if (!state) return { phase: 'idle' }
|
|
126
147
|
const now = Date.now()
|
|
127
148
|
const tokens = state.hasReal ? state.realTokens : Math.round(state.charTokens)
|
|
@@ -145,7 +166,11 @@ export function createSpeedTracker() {
|
|
|
145
166
|
}
|
|
146
167
|
|
|
147
168
|
function reap(now = Date.now()) {
|
|
148
|
-
for (const [key,
|
|
169
|
+
for (const [key, states] of streams) {
|
|
170
|
+
const kept = states.filter((state) => state.phase !== 'done' || now - state.lastSeen <= 4000)
|
|
171
|
+
if (kept.length === 0) streams.delete(key)
|
|
172
|
+
else if (kept.length !== states.length) streams.set(key, kept)
|
|
173
|
+
}
|
|
149
174
|
}
|
|
150
175
|
|
|
151
176
|
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.9",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"scripts": {
|