@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 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
- extraction
3515
- ? h(
3516
- 'div',
3517
- { className: 'dss-row-meta' },
3518
- h('span', null, t('memory.extractionEvents', { n: extraction.eventsSeen ?? 0 })),
3519
- h('span', null, t('memory.extractionTurns', { n: extraction.turnEndsSeen ?? 0 })),
3520
- h(
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
- : null,
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
@@ -175,7 +175,13 @@ export function installMemory({ ctx, root, config, log }) {
175
175
  log(`explicit memory stored (${instruction.scope})`)
176
176
  }
177
177
 
178
- const question = lastUserText(messages)
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.4.4",
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",