dsh-session-pages 0.5.6 → 0.5.7
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/README.md +15 -0
- package/package.json +1 -1
- package/src/client.js +1 -1
- package/src/index.js +11 -2
- package/src/logic.js +56 -1
- package/src/projection.js +7 -1
- package/test/client.test.js +1 -1
- package/test/logic.test.js +72 -0
package/README.md
CHANGED
|
@@ -4,6 +4,21 @@ DeepSeek Harness 的第四个对话视图标签:**窗口**(在「对话 /
|
|
|
4
4
|
|
|
5
5
|
会话按**压缩(compaction)分界**切成若干窗口:第一窗从会话开头起算,之后每次压缩的 checkpoint 开头即新一窗。
|
|
6
6
|
|
|
7
|
+
## 设置前缀不算一窗
|
|
8
|
+
|
|
9
|
+
被 `dsh-log-trim` 裁剪过的日志开头是一段纯设置前缀(系统提示词、预设、权限等),没有对话。若按 checkpoint 切分,它会被当成「第 1 窗」,于是「保留 2 窗」显示成 3 窗。
|
|
10
|
+
|
|
11
|
+
规则:
|
|
12
|
+
|
|
13
|
+
| 情况 | 结果 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| 首窗有 checkpoint | 是真正的对话窗,保留 |
|
|
16
|
+
| 首窗无 checkpoint,但含 user/assistant 消息 | 裁剪没清掉它,保留 |
|
|
17
|
+
| 首窗无 checkpoint,且只有设置事件 | **并入**后面第一个带 checkpoint 的窗,不计入窗数 |
|
|
18
|
+
| 全程没有 checkpoint | 仍然是 1 窗 |
|
|
19
|
+
|
|
20
|
+
并入后,那个窗自己持有系统提示词与工具声明(`inherited: false`),不再显示「沿用上一窗」。
|
|
21
|
+
|
|
7
22
|
## 每一窗画两层
|
|
8
23
|
|
|
9
24
|
一个窗口**就是**两层,所以视图也分两层画:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-pages",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Fourth conversation view for DeepSeek Harness: the session split into windows at every compaction, each window headed by its checkpoint summary.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/client.js
CHANGED
package/src/index.js
CHANGED
|
@@ -129,12 +129,21 @@ function timeAtStart(page, events) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
function stateFromProjection(projection, events, options) {
|
|
132
|
-
|
|
132
|
+
// A trimmed log opens with a settings-only prefix; it is not a window. Same
|
|
133
|
+
// rule as the fold: keep it only when it actually holds dialogue.
|
|
134
|
+
const first = projection.pages.length > 1 ? projection.pages[0] : undefined
|
|
135
|
+
const folded = first !== undefined && first.compactionId === null && (first.dialogue || 0) === 0
|
|
136
|
+
const source = folded
|
|
137
|
+
? projection.pages.slice(1).map((page, index) => ({ ...page, index: index + 1 }))
|
|
138
|
+
: projection.pages
|
|
139
|
+
const pages = source.map((page, position) => {
|
|
133
140
|
const context = {}
|
|
134
141
|
if (page.systemChars > 0) {
|
|
135
142
|
context.system = {
|
|
136
143
|
present: true,
|
|
137
|
-
|
|
144
|
+
// The window that now opens the list owns the frame, even though the
|
|
145
|
+
// events carrying it sat in the dropped prefix.
|
|
146
|
+
inherited: folded === true && position === 0 ? false : page.systemInherited === true,
|
|
138
147
|
chars: page.systemChars,
|
|
139
148
|
preview: clipText(page.systemText, options.clipChars).text,
|
|
140
149
|
full: page.systemText,
|
package/src/logic.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// * it replaces the older range and is FOLLOWED by the retained recent units.
|
|
24
24
|
|
|
25
25
|
export const PLUGIN_ID = 'dsh-session-pages'
|
|
26
|
-
export const VERSION = '0.5.
|
|
26
|
+
export const VERSION = '0.5.7'
|
|
27
27
|
export const ROUTE_PREFIX = '/dsh-session-pages'
|
|
28
28
|
export const VIEW_ID = 'windows'
|
|
29
29
|
export const VIEW_ORDER = 30
|
|
@@ -133,6 +133,42 @@ export function isCheckpoint(event) {
|
|
|
133
133
|
return source !== null && source !== undefined && source.kind === CHECKPOINT_KIND
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
|
|
137
|
+
/** True when the window holds at least one visible dialogue message. */
|
|
138
|
+
function windowHasDialogue(sorted, page) {
|
|
139
|
+
const from = page.startSeq
|
|
140
|
+
const to = page.endSeq === null ? Number.POSITIVE_INFINITY : page.endSeq
|
|
141
|
+
for (const event of sorted) {
|
|
142
|
+
const seq = Number(event.seq) || 0
|
|
143
|
+
if (seq < from) continue
|
|
144
|
+
if (seq > to) break
|
|
145
|
+
if (event.type === 'assistant/message') return true
|
|
146
|
+
if (event.type === 'user/message' && isHumanUserMessage(event)) return true
|
|
147
|
+
}
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Drop a leading window that holds nothing but settings.
|
|
153
|
+
*
|
|
154
|
+
* A trimmed log starts with a prefix of system/settings events and no dialogue.
|
|
155
|
+
* Splitting windows at checkpoints would count that prefix as "window 1", so
|
|
156
|
+
* "2 windows kept" reads as 3. The rule:
|
|
157
|
+
*
|
|
158
|
+
* - fewer than two windows -> keep as is (a fresh session is 1 window);
|
|
159
|
+
* - first window has a checkpoint -> it IS a conversation window, keep it;
|
|
160
|
+
* - prefix holds visible dialogue -> the trim did not clear it, keep it;
|
|
161
|
+
* - otherwise -> drop it, renumber, and let the first
|
|
162
|
+
* checkpoint window carry the frame.
|
|
163
|
+
*/
|
|
164
|
+
export function collapseLeadingSettingsPrefix(sorted, pages) {
|
|
165
|
+
if (pages.length < 2) return pages
|
|
166
|
+
const first = pages[0]
|
|
167
|
+
if (first.compactionId !== null) return pages
|
|
168
|
+
if (windowHasDialogue(sorted, first) === true) return pages
|
|
169
|
+
return pages.slice(1).map((page, index) => ({ ...page, index: index + 1 }))
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
export function isHumanUserMessage(event) {
|
|
137
173
|
if (event === null || typeof event !== 'object' || event.type !== 'user/message' || isCheckpoint(event)) return false
|
|
138
174
|
const source = event.data === null || event.data === undefined ? undefined : event.data.source
|
|
@@ -401,10 +437,29 @@ export function buildPages(events, options = {}) {
|
|
|
401
437
|
}
|
|
402
438
|
}
|
|
403
439
|
|
|
440
|
+
// 2b. A settings-only prefix is not a window of its own.
|
|
441
|
+
const withoutPrefix = collapseLeadingSettingsPrefix(sorted, pages)
|
|
442
|
+
let floorSeq = 0
|
|
443
|
+
if (withoutPrefix !== pages) {
|
|
444
|
+
pages.length = 0
|
|
445
|
+
for (const page of withoutPrefix) pages.push(page)
|
|
446
|
+
// The prefix's system prompt and tool roster now belong to this window, so
|
|
447
|
+
// it no longer inherits them from a window that is not shown.
|
|
448
|
+
if (pages[0].context.system !== null && pages[0].context.system.present === true) {
|
|
449
|
+
pages[0].context.system.inherited = false
|
|
450
|
+
}
|
|
451
|
+
if (pages[0].context.tools !== null && pages[0].context.tools.count > 0) {
|
|
452
|
+
pages[0].context.tools.inherited = false
|
|
453
|
+
}
|
|
454
|
+
floorSeq = pages[0].startSeq
|
|
455
|
+
}
|
|
456
|
+
|
|
404
457
|
// 3. The window's own timeline.
|
|
405
458
|
let dropped = 0
|
|
406
459
|
for (const event of sorted) {
|
|
407
460
|
const seq = Number(event.seq) || 0
|
|
461
|
+
// Events before the first shown window belong to the dropped prefix.
|
|
462
|
+
if (seq < floorSeq) continue
|
|
408
463
|
const page = pageForSeq(seq)
|
|
409
464
|
const time = Number(event.time)
|
|
410
465
|
if (Number.isFinite(time)) {
|
package/src/projection.js
CHANGED
|
@@ -27,6 +27,9 @@ const FIRST_PAGE = () => ({
|
|
|
27
27
|
systemText: '',
|
|
28
28
|
systemInherited: false,
|
|
29
29
|
toolNames: null,
|
|
30
|
+
// Visible dialogue rows (~user/assistant). Settings-only prefixes stay 0 and
|
|
31
|
+
// are folded into the first checkpoint window by the reader.
|
|
32
|
+
dialogue: 0,
|
|
30
33
|
turnFrom: null,
|
|
31
34
|
turnTo: null,
|
|
32
35
|
steps: 0,
|
|
@@ -117,8 +120,10 @@ export const windowProjection = {
|
|
|
117
120
|
// rows are rows, not messages, except the checkpoint above.
|
|
118
121
|
// The fold draws a row only when there is text to draw; a message that
|
|
119
122
|
// carries nothing but an attachment still counts as a message, not a row.
|
|
120
|
-
const
|
|
123
|
+
const text = textOfMessage(messageOf(event))
|
|
124
|
+
const rendered = text.length > 0 ? 1 : 0
|
|
121
125
|
const page = addToPage(last, {
|
|
126
|
+
dialogue: last.dialogue + (rendered > 0 && (kind === 'user' || kind === undefined) ? 1 : 0),
|
|
122
127
|
itemCount: last.itemCount + rendered,
|
|
123
128
|
messages: last.messages + 1,
|
|
124
129
|
})
|
|
@@ -138,6 +143,7 @@ export const windowProjection = {
|
|
|
138
143
|
const text = textOfMessage(message)
|
|
139
144
|
const page = {
|
|
140
145
|
...last,
|
|
146
|
+
dialogue: last.dialogue + 1,
|
|
141
147
|
steps: last.steps + 1,
|
|
142
148
|
messages: last.messages + 1,
|
|
143
149
|
toolCalls: last.toolCalls + toolCalls,
|
package/test/client.test.js
CHANGED
|
@@ -38,7 +38,7 @@ function loadClient() {
|
|
|
38
38
|
test('the bundle registers the windows view beside chat and trajectory', async () => {
|
|
39
39
|
const exports = await loadClient()
|
|
40
40
|
assert.equal(typeof exports.apply, 'function')
|
|
41
|
-
assert.equal(exports.version, '0.5.
|
|
41
|
+
assert.equal(exports.version, '0.5.7')
|
|
42
42
|
|
|
43
43
|
const registered = []
|
|
44
44
|
const slots = {
|
package/test/logic.test.js
CHANGED
|
@@ -209,3 +209,75 @@ test('session ids are validated', () => {
|
|
|
209
209
|
assert.equal(isSessionId('1234abcd'), false)
|
|
210
210
|
assert.equal(isSessionId('session-'), false)
|
|
211
211
|
})
|
|
212
|
+
|
|
213
|
+
// --- a trimmed log must not count its settings prefix as a window -----------
|
|
214
|
+
|
|
215
|
+
test('a settings-only prefix is folded into the first checkpoint window', () => {
|
|
216
|
+
// Trimmed shape: settings + two checkpoints, no dialogue before the first one.
|
|
217
|
+
const events = [
|
|
218
|
+
at(1, 1001, 'permission/preset', { preset: 'standard' }),
|
|
219
|
+
at(2, 1002, 'sandbox/mode', { mode: 'workspace-write' }),
|
|
220
|
+
injection(3, 'runtime-context', 'Current runtime context.'),
|
|
221
|
+
checkpoint(10, 'cmp-1', 'first summary', { op: 'replace', startSeq: 4, endSeq: 9 }),
|
|
222
|
+
at(11, 1011, 'system/message', { role: 'system', content: [{ type: 'text', text: 'You are an agent.' }] }),
|
|
223
|
+
human(12, 1, 'hello'),
|
|
224
|
+
at(20, 1020, 'assistant/message', {
|
|
225
|
+
role: 'assistant', turn: 1, step: 1,
|
|
226
|
+
content: [{ type: 'text', text: 'hi' }],
|
|
227
|
+
usage: { totalTokens: 100 },
|
|
228
|
+
}),
|
|
229
|
+
checkpoint(30, 'cmp-2', 'second summary', { op: 'replace', startSeq: 10, endSeq: 29 }),
|
|
230
|
+
human(31, 2, 'again'),
|
|
231
|
+
at(40, 1040, 'assistant/message', {
|
|
232
|
+
role: 'assistant', turn: 2, step: 1,
|
|
233
|
+
content: [{ type: 'text', text: 'yes' }],
|
|
234
|
+
usage: { totalTokens: 120 },
|
|
235
|
+
}),
|
|
236
|
+
]
|
|
237
|
+
const result = buildPages(events)
|
|
238
|
+
assert.equal(result.pages.length, 2)
|
|
239
|
+
assert.equal(result.totals.pages, 2)
|
|
240
|
+
// The window that opens the list carries the frame itself.
|
|
241
|
+
assert.equal(result.pages[0].context.system.present, true)
|
|
242
|
+
assert.equal(result.pages[0].context.system.inherited, false)
|
|
243
|
+
assert.equal(result.pages[0].index, 1)
|
|
244
|
+
assert.equal(result.pages[1].index, 2)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('a prefix that still holds dialogue stays a window of its own', () => {
|
|
248
|
+
const events = [
|
|
249
|
+
injection(1, 'runtime-context', 'Current runtime context.'),
|
|
250
|
+
human(2, 1, 'still here'),
|
|
251
|
+
at(3, 1003, 'assistant/message', {
|
|
252
|
+
role: 'assistant', turn: 1, step: 1,
|
|
253
|
+
content: [{ type: 'text', text: 'ok' }],
|
|
254
|
+
usage: { totalTokens: 50 },
|
|
255
|
+
}),
|
|
256
|
+
checkpoint(10, 'cmp-1', 'summary', { op: 'replace', startSeq: 4, endSeq: 9 }),
|
|
257
|
+
human(11, 2, 'after'),
|
|
258
|
+
at(12, 1012, 'assistant/message', {
|
|
259
|
+
role: 'assistant', turn: 2, step: 1,
|
|
260
|
+
content: [{ type: 'text', text: 'yep' }],
|
|
261
|
+
usage: { totalTokens: 60 },
|
|
262
|
+
}),
|
|
263
|
+
]
|
|
264
|
+
const result = buildPages(events)
|
|
265
|
+
assert.equal(result.pages.length, 2)
|
|
266
|
+
assert.equal(result.pages[0].compactionId, null)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('a session with no checkpoint is still one window', () => {
|
|
270
|
+
const events = [
|
|
271
|
+
at(1, 1001, 'permission/preset', { preset: 'standard' }),
|
|
272
|
+
injection(2, 'runtime-context', 'Current runtime context.'),
|
|
273
|
+
human(3, 1, 'first'),
|
|
274
|
+
at(4, 1004, 'assistant/message', {
|
|
275
|
+
role: 'assistant', turn: 1, step: 1,
|
|
276
|
+
content: [{ type: 'text', text: 'reply' }],
|
|
277
|
+
usage: { totalTokens: 10 },
|
|
278
|
+
}),
|
|
279
|
+
]
|
|
280
|
+
const result = buildPages(events)
|
|
281
|
+
assert.equal(result.pages.length, 1)
|
|
282
|
+
assert.equal(result.totals.pages, 1)
|
|
283
|
+
})
|