@heixui/dsh-session-suite 0.4.4 → 0.5.0
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/client.js +50 -10
- package/cordis.patch.yml +8 -0
- package/lib/memory-host.js +81 -1
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -270,6 +270,8 @@ window.__ModuleLoader__.load({
|
|
|
270
270
|
'memory.extractionTurns': '{n} turn(s) ended',
|
|
271
271
|
'memory.extractionRegistered': 'hook registered',
|
|
272
272
|
'memory.extractionNotRegistered': 'HOOK NOT REGISTERED',
|
|
273
|
+
'memory.extractionNoTrace': 'no extraction trace recorded by this Host',
|
|
274
|
+
'memory.diagTitle': 'Extraction diagnostics:',
|
|
273
275
|
'memory.accept': 'Keep',
|
|
274
276
|
'memory.reject': 'Discard',
|
|
275
277
|
'memory.accepted': 'Kept',
|
|
@@ -516,6 +518,8 @@ window.__ModuleLoader__.load({
|
|
|
516
518
|
'memory.extractionTurns': '结束 {n} 轮',
|
|
517
519
|
'memory.extractionRegistered': '钩子已注册',
|
|
518
520
|
'memory.extractionNotRegistered': '钩子未注册',
|
|
521
|
+
'memory.extractionNoTrace': '本次运行没有记录到提取痕迹',
|
|
522
|
+
'memory.diagTitle': '提取诊断:',
|
|
519
523
|
'memory.accept': '保留',
|
|
520
524
|
'memory.reject': '丢弃',
|
|
521
525
|
'memory.accepted': '已保留',
|
|
@@ -1026,6 +1030,23 @@ window.__ModuleLoader__.load({
|
|
|
1026
1030
|
overflow-y: auto;
|
|
1027
1031
|
overscroll-behavior: contain;
|
|
1028
1032
|
}
|
|
1033
|
+
/* The extraction diagnostic. Deliberately larger than the meta text around it and set
|
|
1034
|
+
on its own tinted strip: it exists to answer "why is the queue empty", and the first
|
|
1035
|
+
version was 11px grey text inside a bordered box that the user reported not seeing
|
|
1036
|
+
at all. A diagnostic that cannot be found is not a diagnostic. */
|
|
1037
|
+
.dss-extraction-diag {
|
|
1038
|
+
display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: baseline;
|
|
1039
|
+
margin-top: 8px; padding: 8px 12px;
|
|
1040
|
+
border-radius: 8px;
|
|
1041
|
+
background: var(--dsw-alias-bg-layer-2, #f2f3f5);
|
|
1042
|
+
font-size: 12.5px; line-height: 1.5;
|
|
1043
|
+
color: var(--dsw-alias-label-secondary, #4a4a4a);
|
|
1044
|
+
font-variant-numeric: tabular-nums;
|
|
1045
|
+
}
|
|
1046
|
+
.dss-diag-title {
|
|
1047
|
+
font-weight: 600;
|
|
1048
|
+
color: var(--dsw-alias-label-primary, #1f1f1f);
|
|
1049
|
+
}
|
|
1029
1050
|
/* The summary charts. Two narrow panels for composition and health, one wide panel
|
|
1030
1051
|
for tags: composition and health are short lists, tags can run long. Bars are divs,
|
|
1031
1052
|
not SVG, so they inherit the theme and stay legible in both palettes. */
|
|
@@ -3511,21 +3532,40 @@ window.__ModuleLoader__.load({
|
|
|
3511
3532
|
// `registered` is the third state: the hook was never installed at all
|
|
3512
3533
|
// (the optional registration threw), which is a different bug from
|
|
3513
3534
|
// "installed but never called".
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3535
|
+
//
|
|
3536
|
+
// This block renders even when `extraction` is null, and says so. The
|
|
3537
|
+
// previous version was gated on `extraction` being present — so a Host
|
|
3538
|
+
// that never recorded a trace (registration failed, or the status
|
|
3539
|
+
// endpoint reads a different engine) showed nothing at all, which is
|
|
3540
|
+
// exactly the case the diagnostic existed to expose.
|
|
3541
|
+
//
|
|
3542
|
+
// Rendered at a readable size rather than in the row-meta micro type:
|
|
3543
|
+
// it is a diagnostic, and a diagnostic nobody can find is the same as
|
|
3544
|
+
// no diagnostic. The 11px meta line was reported as "I don't see it".
|
|
3545
|
+
h(
|
|
3546
|
+
'div',
|
|
3547
|
+
{ className: 'dss-extraction-diag' },
|
|
3548
|
+
h('span', { className: 'dss-diag-title' }, t('memory.diagTitle')),
|
|
3549
|
+
h(
|
|
3550
|
+
'span',
|
|
3551
|
+
null,
|
|
3552
|
+
extraction
|
|
3553
|
+
? t('memory.extractionEvents', { n: extraction.eventsSeen ?? 0 })
|
|
3554
|
+
: t('memory.extractionNoTrace'),
|
|
3555
|
+
),
|
|
3556
|
+
extraction
|
|
3557
|
+
? h('span', null, t('memory.extractionTurns', { n: extraction.turnEndsSeen ?? 0 }))
|
|
3558
|
+
: null,
|
|
3559
|
+
extraction
|
|
3560
|
+
? h(
|
|
3521
3561
|
'span',
|
|
3522
3562
|
null,
|
|
3523
3563
|
extraction.registered
|
|
3524
3564
|
? t('memory.extractionRegistered')
|
|
3525
3565
|
: t('memory.extractionNotRegistered'),
|
|
3526
|
-
)
|
|
3527
|
-
|
|
3528
|
-
|
|
3566
|
+
)
|
|
3567
|
+
: null,
|
|
3568
|
+
),
|
|
3529
3569
|
)
|
|
3530
3570
|
|
|
3531
3571
|
// The list gets its own scroll box.
|
package/cordis.patch.yml
CHANGED
|
@@ -53,6 +53,14 @@
|
|
|
53
53
|
# the budget would claim six memories are relevant when the evidence
|
|
54
54
|
# says otherwise.
|
|
55
55
|
recallLimit: 6
|
|
56
|
+
# How much of the conversation before the current message is folded into the
|
|
57
|
+
# retrieval query. A follow-up like "那它为什么慢?" has no referent for "它", so
|
|
58
|
+
# searching those characters alone cannot find the memory that answers it.
|
|
59
|
+
# The previous user turns are appended BEFORE the current question, so the
|
|
60
|
+
# question's own terms still dominate the score. 0 disables the behaviour and
|
|
61
|
+
# searches the current message exactly as before.
|
|
62
|
+
contextQueryChars: 160
|
|
63
|
+
contextQueryTurns: 3
|
|
56
64
|
# How many of the most important memories ride along regardless of the
|
|
57
65
|
# question. Without a resident layer, an ordinary follow-up like "继续"
|
|
58
66
|
# shares no vocabulary with any memory and a strict relevance filter
|
package/lib/memory-host.js
CHANGED
|
@@ -175,7 +175,13 @@ export function installMemory({ ctx, root, config, log }) {
|
|
|
175
175
|
log(`explicit memory stored (${instruction.scope})`)
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
// The question, plus enough of the conversation before it to resolve a
|
|
179
|
+
// short follow-up. See contextualQuery: a bare 「那它为什么慢?」 has no
|
|
180
|
+
// referent for 「它」 and cannot match the memory that answers it.
|
|
181
|
+
const question = contextualQuery(messages, {
|
|
182
|
+
chars: typeof config?.contextQueryChars === 'number' ? config.contextQueryChars : undefined,
|
|
183
|
+
turns: typeof config?.contextQueryTurns === 'number' ? config.contextQueryTurns : undefined,
|
|
184
|
+
})
|
|
179
185
|
const text = await buildRecallText({ engine, cwd, question, config })
|
|
180
186
|
if (agent) recallByScope.set(agent, { text, at: Date.now() })
|
|
181
187
|
lastRecall = text
|
|
@@ -495,6 +501,80 @@ function lastUserText(messages) {
|
|
|
495
501
|
return ''
|
|
496
502
|
}
|
|
497
503
|
|
|
504
|
+
/**
|
|
505
|
+
* The current question plus the tail of the conversation before it.
|
|
506
|
+
*
|
|
507
|
+
* Retrieval used to search for the last user message alone, so a follow-up like
|
|
508
|
+
* 「那它为什么慢?」 searched for those seven characters and nothing else. "它" is a
|
|
509
|
+
* pronoun with no referent in the query, so the memory that explains the slowness could
|
|
510
|
+
* not be found no matter how well it was written. That is a real gap, not a tuning
|
|
511
|
+
* problem.
|
|
512
|
+
*
|
|
513
|
+
* The shape here is deliberate:
|
|
514
|
+
*
|
|
515
|
+
* • the current question comes LAST, so it dominates the lexical score (BM25 and the
|
|
516
|
+
* coverage route both reward a match in the query; putting context first would let
|
|
517
|
+
* an older turn outvote the thing being asked now);
|
|
518
|
+
* • only the previous USER turns are included, not assistant replies. Assistant text
|
|
519
|
+
* is verbose, and a long reply would drown the query in words the user never typed;
|
|
520
|
+
* • the tail is capped in characters rather than turns, because one long message and
|
|
521
|
+
* ten short ones should not cost the same;
|
|
522
|
+
* • nothing is included when the current question already stands alone (long enough
|
|
523
|
+
* and not pronoun-led), so an ordinary self-contained question is searched exactly
|
|
524
|
+
* as before.
|
|
525
|
+
*
|
|
526
|
+
* This is a heuristic, not a language model. It will not resolve 「那个东西」 back to a
|
|
527
|
+
* noun three turns ago. It does fix the common case — a short follow-up — and it costs
|
|
528
|
+
* nothing at query time.
|
|
529
|
+
*
|
|
530
|
+
* @param {readonly any[]} messages step messages
|
|
531
|
+
* @param {{ chars?: number, turns?: number }} [limits] budget
|
|
532
|
+
* @returns {string} query text
|
|
533
|
+
*/
|
|
534
|
+
function contextualQuery(messages, limits = {}) {
|
|
535
|
+
const current = lastUserText(messages)
|
|
536
|
+
if (!current) return ''
|
|
537
|
+
|
|
538
|
+
const maxChars = limits.chars ?? 160
|
|
539
|
+
const maxTurns = limits.turns ?? 3
|
|
540
|
+
|
|
541
|
+
// A self-contained question needs no help. The test is NOT length alone: a short
|
|
542
|
+
// question that names its own subject ("导出的图片为什么断链") is self-contained,
|
|
543
|
+
// while a long one made of pronouns ("那这个呢,还有刚才说的那个,它到底是怎么回事")
|
|
544
|
+
// is not. Pronouns are the actual signal — they are what a query cannot resolve.
|
|
545
|
+
//
|
|
546
|
+
// An earlier version used `length >= 24`, which was a guess and misclassified a
|
|
547
|
+
// perfectly clear 20-character question as needing context. Pronoun presence is a
|
|
548
|
+
// narrower, more defensible rule: it fires exactly when the query contains a word
|
|
549
|
+
// that refers to something outside itself.
|
|
550
|
+
const PRONOUNS = /(它|他|她|这个|那个|这些|那些|该|此|上述|前面|刚才|上面|之前那个)/
|
|
551
|
+
const looksSelfContained = !PRONOUNS.test(current)
|
|
552
|
+
|
|
553
|
+
const earlier = []
|
|
554
|
+
let budget = maxChars
|
|
555
|
+
let seen = 0
|
|
556
|
+
for (let index = (messages?.length ?? 0) - 1; index >= 0 && seen < maxTurns; index -= 1) {
|
|
557
|
+
const message = messages[index]
|
|
558
|
+
if (message?.role !== 'user') continue
|
|
559
|
+
const text = contentText(message.content)
|
|
560
|
+
if (!text) continue
|
|
561
|
+
seen += 1
|
|
562
|
+
// The newest user message IS `current`; skip it so it is not duplicated.
|
|
563
|
+
if (text === current) continue
|
|
564
|
+
if (budget <= 0) break
|
|
565
|
+
const slice = text.length > budget ? text.slice(-budget) : text
|
|
566
|
+
earlier.unshift(slice)
|
|
567
|
+
budget -= slice.length
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Nothing to add, or the question stands on its own: search it as-is.
|
|
571
|
+
if (earlier.length === 0 || looksSelfContained) return current
|
|
572
|
+
|
|
573
|
+
// Context first, question last. The engine tokenises the whole string, and the
|
|
574
|
+
// question's own terms are what must win.
|
|
575
|
+
return `${earlier.join(' ')} ${current}`.trim()
|
|
576
|
+
}
|
|
577
|
+
|
|
498
578
|
/**
|
|
499
579
|
* Ask a small model call what, if anything, is worth remembering from this turn.
|
|
500
580
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heixui/dsh-session-suite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Session checkpoints for DSH: rewind a conversation with its files and memory, permanent session deletion, and shareable exports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|