@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.2
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/INSTALL.md +25 -0
- package/PANEL.md +92 -0
- package/README.md +286 -2
- package/RELEASE-NOTES.md +732 -0
- package/bin/bc-acceptance-full.mjs +91 -0
- package/bin/bc-acceptance-tight.mjs +66 -0
- package/bin/bc-acceptance.mjs +22 -0
- package/bin/bc-add-keywords.mjs +21 -0
- package/bin/bc-demo.mjs +47 -0
- package/bin/bc-expand-hint.mjs +179 -0
- package/bin/bc-ingest-index.mjs +35 -0
- package/bin/bc-ingest.mjs +31 -0
- package/bin/bc-label-demo.mjs +18 -0
- package/bin/bc-relabel.mjs +150 -0
- package/bin/bc-sessionize.mjs +59 -0
- package/bin/coref_pipeline.py +47 -0
- package/bin/coref_service.py +57 -0
- package/bin/debug_ckip.py +16 -0
- package/bin/debug_ner_zh.py +18 -0
- package/bin/doctor.mjs +275 -0
- package/bin/en_ner_error_audit.py +33 -0
- package/bin/eval-label-rules.mjs +342 -0
- package/bin/export-turns.py +68 -0
- package/bin/extract-user-paragraph.mjs +14 -0
- package/bin/extraction-composite-smoke.mjs +28 -0
- package/bin/extraction-latency.mjs +58 -0
- package/bin/extraction-user-smoke.mjs +28 -0
- package/bin/extraction_service.py +164 -0
- package/bin/facet-active-experiment.mjs +110 -0
- package/bin/facet-covid.mjs +8 -0
- package/bin/facet-curve.mjs +27 -0
- package/bin/facet-embedding-train.mjs +20 -0
- package/bin/facet-embedding-validate.mjs +83 -0
- package/bin/facet-eval-roleplay.mjs +42 -0
- package/bin/facet-eval.mjs +38 -0
- package/bin/facet-fold.mjs +31 -0
- package/bin/facet-load.mjs +16 -0
- package/bin/facet-mlp-scan.mjs +45 -0
- package/bin/facet-mvp.mjs +34 -0
- package/bin/facet-online-experiments.mjs +78 -0
- package/bin/facet-online-race.mjs +99 -0
- package/bin/facet-online-validate.mjs +51 -0
- package/bin/facet-panel.mjs +22 -0
- package/bin/facet-passage.mjs +41 -0
- package/bin/facet-random-validate.mjs +48 -0
- package/bin/facet-roleplay.mjs +33 -0
- package/bin/facet-scan.mjs +56 -0
- package/bin/facet-segment.mjs +34 -0
- package/bin/facet-train.mjs +19 -0
- package/bin/facet-validate.mjs +103 -0
- package/bin/facet-zeroshot.mjs +14 -0
- package/bin/install-templates.mjs +72 -0
- package/bin/memory-bench.mjs +53 -0
- package/bin/memory-build-summary.mjs +43 -0
- package/bin/memory-build-vector.mjs +31 -0
- package/bin/memory-build.mjs +41 -0
- package/bin/memory-calibration.mjs +70 -0
- package/bin/memory-closed-loop.mjs +119 -0
- package/bin/memory-dry-run.mjs +70 -0
- package/bin/memory-eval.mjs +104 -0
- package/bin/memory-event-alias-test.mjs +89 -0
- package/bin/memory-extraction-bench.mjs +58 -0
- package/bin/memory-load-gate-replay.mjs +137 -0
- package/bin/memory-loop-test.mjs +90 -0
- package/bin/memory-pre-turn-harness.mjs +63 -0
- package/bin/memory-query.mjs +39 -0
- package/bin/memory-replay-hints.mjs +84 -0
- package/bin/memory-scale.mjs +58 -0
- package/bin/memory-scenario-test.mjs +146 -0
- package/bin/memory-user-loop.mjs +95 -0
- package/bin/ner_bench_tiny_zh.py +59 -0
- package/bin/ner_bench_zh.py +60 -0
- package/bin/ner_conll_en.py +139 -0
- package/bin/ner_direct_en.py +123 -0
- package/bin/ner_direct_zh.py +100 -0
- package/bin/ner_pipeline.py +208 -0
- package/bin/noise-selfcheck.mjs +50 -0
- package/bin/pack-integrity.mjs +47 -0
- package/bin/panel-check.mjs +57 -0
- package/bin/plugin-integrity.mjs +74 -0
- package/bin/preview-live.mjs +13 -0
- package/bin/public-install-check.mjs +222 -0
- package/bin/relabel-apply.mjs +272 -0
- package/bin/relabel-dry-run.mjs +381 -0
- package/bin/relik_pipeline.py +39 -0
- package/bin/relik_service.py +52 -0
- package/bin/srl_pipeline.py +55 -0
- package/bin/srl_service.py +68 -0
- package/bin/summary-index.mjs +151 -0
- package/bin/summary-query.mjs +79 -0
- package/bin/vec-check.mjs +27 -0
- package/bin/zh_ner_error_audit.py +36 -0
- package/bin/zh_ner_slot_eval.py +38 -0
- package/client.js +760 -0
- package/cordis.patch.yml +378 -0
- package/docs/API-NOTES.md +1743 -0
- package/docs/ARCHITECTURE.md +355 -0
- package/docs/CACHE.md +253 -0
- package/docs/CONDUCT.md +30 -0
- package/docs/CREATOR-HANDOFF.md +102 -0
- package/docs/EXPERIMENT.md +136 -0
- package/docs/FACET-MVP-REPORT.md +473 -0
- package/docs/GUIDE-0.4.66.md +195 -0
- package/docs/GUIDE.md +127 -0
- package/docs/MECHANISM.md +243 -0
- package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
- package/docs/README.md +37 -0
- package/docs/SESSION-LAYOUT.md +151 -0
- package/docs/history-map.md +249 -0
- package/docs/relabel-migration-wiring.md +366 -0
- package/guides/00-READ-FIRST.md +206 -0
- package/guides/01-INSTALL.md +207 -0
- package/guides/02-TABLES.md +225 -0
- package/guides/03-AUDIT.md +201 -0
- package/guides/04-REBUILD.md +306 -0
- package/guides/05-FIRST-RUN.md +191 -0
- package/guides/06-EMBEDDING.md +146 -0
- package/guides/07-PTC-AND-ASSETS.md +221 -0
- package/icon.svg +7 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/models/README.md +13 -0
- package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +139 -4
- package/src/asset-tool.mjs +237 -0
- package/src/assets.mjs +244 -0
- package/src/backfill.mjs +74 -0
- package/src/backup.mjs +80 -0
- package/src/deadend.mjs +95 -0
- package/src/deleted-store.mjs +35 -0
- package/src/engine.mjs +3831 -0
- package/src/facets/classifier.mjs +146 -0
- package/src/facets/embedding-service.mjs +53 -0
- package/src/facets/embedding-worker.mjs +27 -0
- package/src/facets/embedding.mjs +163 -0
- package/src/facets/extract.mjs +80 -0
- package/src/facets/fold-plan.mjs +84 -0
- package/src/facets/hybrid.mjs +29 -0
- package/src/facets/index.mjs +113 -0
- package/src/facets/linear-probe.mjs +43 -0
- package/src/facets/load.mjs +37 -0
- package/src/facets/mlp-probe.mjs +54 -0
- package/src/facets/mlp-service.mjs +53 -0
- package/src/facets/mlp-worker.mjs +26 -0
- package/src/facets/online-knn.mjs +58 -0
- package/src/facets/online-models.mjs +227 -0
- package/src/facets/panel-state.mjs +23 -0
- package/src/facets/panel.mjs +73 -0
- package/src/facets/random.mjs +26 -0
- package/src/facets/scanner.mjs +35 -0
- package/src/facets/schema.mjs +75 -0
- package/src/facets/seed-examples.mjs +58 -0
- package/src/facets/sensitivity.mjs +98 -0
- package/src/facets/service.mjs +53 -0
- package/src/facets/store.mjs +39 -0
- package/src/facets/worker.mjs +23 -0
- package/src/folds-store.mjs +109 -0
- package/src/groups.mjs +167 -0
- package/src/handoff-tool.mjs +51 -0
- package/src/handoff.mjs +127 -0
- package/src/index.mjs +649 -0
- package/src/inject-policy.mjs +17 -0
- package/src/learning.mjs +88 -0
- package/src/memory/alias-proposal.mjs +40 -0
- package/src/memory/bc-audit.mjs +245 -0
- package/src/memory/bc-index-updater.mjs +404 -0
- package/src/memory/bc-index.mjs +48 -0
- package/src/memory/bc-label.mjs +220 -0
- package/src/memory/bc-panel-state.mjs +107 -0
- package/src/memory/bc-pre-step.mjs +463 -0
- package/src/memory/bc-recall.mjs +470 -0
- package/src/memory/bc-shift.mjs +123 -0
- package/src/memory/bc-store.mjs +41 -0
- package/src/memory/bm25.mjs +112 -0
- package/src/memory/calibration.mjs +54 -0
- package/src/memory/chatter.mjs +29 -0
- package/src/memory/cmap-file.mjs +107 -0
- package/src/memory/concepts-file.mjs +41 -0
- package/src/memory/coref-slots.mjs +40 -0
- package/src/memory/event-alias-ledger.mjs +87 -0
- package/src/memory/event-log.mjs +95 -0
- package/src/memory/event-model.mjs +132 -0
- package/src/memory/event-segmenter.mjs +36 -0
- package/src/memory/event-summary-tree.mjs +122 -0
- package/src/memory/extraction-adapter.mjs +76 -0
- package/src/memory/extraction-composite.mjs +75 -0
- package/src/memory/extraction-core.mjs +76 -0
- package/src/memory/extraction-pre-step.mjs +161 -0
- package/src/memory/extraction-provider.mjs +300 -0
- package/src/memory/extraction-score.mjs +36 -0
- package/src/memory/facet-stabilizer.mjs +59 -0
- package/src/memory/history-map.mjs +139 -0
- package/src/memory/language-router.mjs +30 -0
- package/src/memory/load-gate.mjs +161 -0
- package/src/memory/mechanical-note.mjs +104 -0
- package/src/memory/onboarding.mjs +163 -0
- package/src/memory/persona.mjs +78 -0
- package/src/memory/planner.mjs +249 -0
- package/src/memory/pre-turn.mjs +80 -0
- package/src/memory/query-aliases.mjs +39 -0
- package/src/memory/recall-cue.mjs +43 -0
- package/src/memory/recall.mjs +65 -0
- package/src/memory/referent-candidates.mjs +55 -0
- package/src/memory/referent-ledger.mjs +86 -0
- package/src/memory/relabel-migrate.mjs +376 -0
- package/src/memory/relik-adapter.mjs +25 -0
- package/src/memory/special-keywords.mjs +60 -0
- package/src/memory/structured-adapters.mjs +75 -0
- package/src/memory/summary-index.mjs +335 -0
- package/src/memory/summary-tree.mjs +110 -0
- package/src/memory/term-extractor.mjs +44 -0
- package/src/memory/tokenize.mjs +9 -0
- package/src/memory/vector-index.mjs +54 -0
- package/src/memory/window-summary.mjs +133 -0
- package/src/mood.mjs +133 -0
- package/src/peer-tool.mjs +63 -0
- package/src/peer.mjs +144 -0
- package/src/policy.mjs +170 -0
- package/src/preflight.mjs +113 -0
- package/src/provenance.mjs +94 -0
- package/src/range.mjs +75 -0
- package/src/recall-exact-tool.mjs +267 -0
- package/src/recall-tool.mjs +521 -0
- package/src/recheck.mjs +99 -0
- package/src/save-start.mjs +29 -0
- package/src/session-guard.mjs +140 -0
- package/src/session-log-tool.mjs +322 -0
- package/src/session-log.mjs +606 -0
- package/src/skills.mjs +111 -0
- package/src/split-range.mjs +115 -0
- package/src/state.mjs +198 -0
- package/src/t0.mjs +149 -0
- package/src/tag-confirm.mjs +78 -0
- package/src/tag-detect.mjs +42 -0
- package/src/tag-maintenance.mjs +285 -0
- package/src/tag-reject.mjs +73 -0
- package/src/tags-store.mjs +94 -0
- package/src/tags.mjs +692 -0
- package/src/threads-backend.mjs +26 -0
- package/src/threads-core.mjs +1887 -0
- package/src/threads-store.mjs +207 -0
- package/src/threads-tool.mjs +522 -0
- package/src/threads.mjs +929 -0
- package/src/tool-preview.mjs +427 -0
- package/src/topic.mjs +63 -0
- package/src/training.mjs +75 -0
- package/src/turn-tag.mjs +62 -0
- package/src/user-turn.mjs +39 -0
- package/templates/cmap.md +11 -0
- package/templates/concepts.md +12 -0
- package/templates/habits.md +40 -0
- package/templates/keywords.md +6 -0
- package/templates/rebuild.md +134 -0
- package/templates/rules-safe.md +184 -0
- package/templates/rules.md +177 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summary index for Continuity.
|
|
3
|
+
*
|
|
4
|
+
* This is the middle layer between gist pointers and raw session text:
|
|
5
|
+
* gist short pointer (existing)
|
|
6
|
+
* summary structured text over a seq range (this index)
|
|
7
|
+
* raw session log, source of truth
|
|
8
|
+
*
|
|
9
|
+
* Phase 0/1 is read-only/index-only: it imports existing compaction/summary
|
|
10
|
+
* events (native checkpoints and Continuity gists) and plans missing windows.
|
|
11
|
+
* It never rewrites or deletes raw events.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
export const SUMMARY_INDEX_VERSION = 1;
|
|
18
|
+
export const DEFAULT_WINDOW_TOKENS = 40_000;
|
|
19
|
+
|
|
20
|
+
const GIST_PREFIX = /^\[(?:thread|gist)\b/i;
|
|
21
|
+
|
|
22
|
+
function numberOrNull(value) {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isInteger(n) ? n : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function summaryText(event) {
|
|
28
|
+
const summary = event?.data?.summary;
|
|
29
|
+
if (Array.isArray(summary)) {
|
|
30
|
+
return summary
|
|
31
|
+
.map((block) => (block && typeof block === 'object' && typeof block.text === 'string' ? block.text : ''))
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
34
|
+
return typeof summary === 'string' ? summary : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function turnMarkers(events) {
|
|
38
|
+
return (Array.isArray(events) ? events : [])
|
|
39
|
+
.filter((event) => event?.type === 'turn/start' && Number.isInteger(Number(event?.data?.turn)))
|
|
40
|
+
.map((event) => ({ seq: Number(event.seq), turn: Number(event.data.turn) }))
|
|
41
|
+
.sort((a, b) => a.seq - b.seq);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function turnAt(markers, seq) {
|
|
45
|
+
let found = null;
|
|
46
|
+
for (const marker of markers) {
|
|
47
|
+
if (marker.seq <= seq) found = marker.turn;
|
|
48
|
+
else break;
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textTokens(text) {
|
|
54
|
+
return Math.max(1, Math.ceil(String(text ?? '').length / 4));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseThreads(text) {
|
|
58
|
+
const out = new Set();
|
|
59
|
+
const pattern = /\b(?:T|thread)=([A-Za-z0-9_-]+)/g;
|
|
60
|
+
let match;
|
|
61
|
+
while ((match = pattern.exec(String(text ?? ''))) !== null) out.add(match[1]);
|
|
62
|
+
return [...out];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function recordFromEvent(event, markers) {
|
|
66
|
+
const text = summaryText(event).trim();
|
|
67
|
+
if (!text) return null;
|
|
68
|
+
const start = numberOrNull(event?.data?.shadowedRange?.start);
|
|
69
|
+
const end = numberOrNull(event?.data?.shadowedRange?.end);
|
|
70
|
+
if (start === null || end === null) return null;
|
|
71
|
+
const fromSeq = Math.min(start, end);
|
|
72
|
+
const toSeq = Math.max(start, end);
|
|
73
|
+
const kind = GIST_PREFIX.test(text) ? 'gist' : 'checkpoint';
|
|
74
|
+
return {
|
|
75
|
+
id: `s-${fromSeq}-${toSeq}`,
|
|
76
|
+
kind,
|
|
77
|
+
fromSeq,
|
|
78
|
+
toSeq,
|
|
79
|
+
fromTurn: turnAt(markers, fromSeq),
|
|
80
|
+
toTurn: turnAt(markers, toSeq),
|
|
81
|
+
threads: parseThreads(text),
|
|
82
|
+
chars: text.length,
|
|
83
|
+
approxTokens: textTokens(text),
|
|
84
|
+
sourceTokens: numberOrNull(event?.data?.shadowedTokenCount),
|
|
85
|
+
sourceHash: createHash('sha256').update(`${fromSeq}:${toSeq}:${text}`, 'utf8').digest('hex'),
|
|
86
|
+
text,
|
|
87
|
+
status: 'ready',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergeRanges(records) {
|
|
92
|
+
const ranges = records
|
|
93
|
+
.map((record) => ({ start: record.fromSeq, end: record.toSeq }))
|
|
94
|
+
.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
95
|
+
const merged = [];
|
|
96
|
+
for (const range of ranges) {
|
|
97
|
+
const last = merged.at(-1);
|
|
98
|
+
if (last && range.start <= last.end + 1) last.end = Math.max(last.end, range.end);
|
|
99
|
+
else merged.push({ ...range });
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function visibleText(event) {
|
|
105
|
+
if (!event || (event.type !== 'user/message' && event.type !== 'assistant/message')) return '';
|
|
106
|
+
const content = event.type === 'assistant/message'
|
|
107
|
+
? event?.data?.message?.content
|
|
108
|
+
: event?.data?.content;
|
|
109
|
+
let out = '';
|
|
110
|
+
for (const block of content ?? []) {
|
|
111
|
+
if (block && block.type === 'text') out += block.text ?? '';
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function estimateGapTokens(events, start, end) {
|
|
117
|
+
let tokens = 0;
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
const seq = Number(event?.seq);
|
|
120
|
+
if (!Number.isInteger(seq) || seq < start || seq > end) continue;
|
|
121
|
+
tokens += textTokens(visibleText(event));
|
|
122
|
+
}
|
|
123
|
+
return tokens;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Build a summary index from one session's raw events.
|
|
128
|
+
* @param {{sessionId: string, events: object[], windowTokens?: number}} input
|
|
129
|
+
*/
|
|
130
|
+
export function buildSummaryIndex({
|
|
131
|
+
sessionId,
|
|
132
|
+
events,
|
|
133
|
+
windowTokens = DEFAULT_WINDOW_TOKENS,
|
|
134
|
+
bootstrapRanges = [],
|
|
135
|
+
windowRanges = [],
|
|
136
|
+
windowRecords = [],
|
|
137
|
+
}) {
|
|
138
|
+
const list = Array.isArray(events) ? events : [];
|
|
139
|
+
const markers = turnMarkers(list);
|
|
140
|
+
const records = list
|
|
141
|
+
.filter((event) => event?.type === 'compaction/summary')
|
|
142
|
+
.map((event) => recordFromEvent(event, markers))
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.sort((a, b) => a.fromSeq - b.fromSeq || a.toSeq - b.toSeq);
|
|
145
|
+
const unique = [];
|
|
146
|
+
const seen = new Set();
|
|
147
|
+
for (const record of records) {
|
|
148
|
+
const key = `${record.fromSeq}:${record.toSeq}:${record.sourceHash}`;
|
|
149
|
+
if (seen.has(key)) continue;
|
|
150
|
+
seen.add(key);
|
|
151
|
+
unique.push(record);
|
|
152
|
+
}
|
|
153
|
+
const boot = (Array.isArray(bootstrapRanges) ? bootstrapRanges : [])
|
|
154
|
+
.map((range) => ({
|
|
155
|
+
fromSeq: Number(range?.fromSeq ?? range?.start),
|
|
156
|
+
toSeq: Number(range?.toSeq ?? range?.end),
|
|
157
|
+
tokens: Number(range?.tokens ?? 0),
|
|
158
|
+
carrierSeq: Number(range?.carrierSeq ?? 0),
|
|
159
|
+
at: typeof range?.at === 'string' ? range.at : null,
|
|
160
|
+
kind: 'bootstrap-replace',
|
|
161
|
+
}))
|
|
162
|
+
.filter((range) => Number.isInteger(range.fromSeq) && Number.isInteger(range.toSeq))
|
|
163
|
+
.map((range) => (range.fromSeq <= range.toSeq ? range : { ...range, fromSeq: range.toSeq, toSeq: range.fromSeq }))
|
|
164
|
+
.sort((a, b) => a.fromSeq - b.fromSeq || a.toSeq - b.toSeq);
|
|
165
|
+
const windows = (Array.isArray(windowRanges) ? windowRanges : [])
|
|
166
|
+
.map((range) => ({
|
|
167
|
+
fromSeq: Number(range?.fromSeq ?? range?.start),
|
|
168
|
+
toSeq: Number(range?.toSeq ?? range?.end),
|
|
169
|
+
tokens: Number(range?.tokens ?? 0),
|
|
170
|
+
hash: typeof range?.hash === 'string' ? range.hash : null,
|
|
171
|
+
at: typeof range?.at === 'string' ? range.at : null,
|
|
172
|
+
kind: 'window-extractive',
|
|
173
|
+
}))
|
|
174
|
+
.filter((range) => Number.isInteger(range.fromSeq) && Number.isInteger(range.toSeq))
|
|
175
|
+
.map((range) => (range.fromSeq <= range.toSeq ? range : { ...range, fromSeq: range.toSeq, toSeq: range.fromSeq }))
|
|
176
|
+
.sort((a, b) => a.fromSeq - b.fromSeq || a.toSeq - b.toSeq);
|
|
177
|
+
const windowRecordList = (Array.isArray(windowRecords) ? windowRecords : [])
|
|
178
|
+
.map((summary) => ({
|
|
179
|
+
id: `window-${Number(summary?.fromSeq)}-${Number(summary?.toSeq)}`,
|
|
180
|
+
kind: 'window-extractive',
|
|
181
|
+
quality: 'extractive',
|
|
182
|
+
fromSeq: Number(summary?.fromSeq),
|
|
183
|
+
toSeq: Number(summary?.toSeq),
|
|
184
|
+
text: String(summary?.text ?? ''),
|
|
185
|
+
sourceHash: String(summary?.hash ?? ''),
|
|
186
|
+
sourceTokens: Number(summary?.approxTokens ?? 0),
|
|
187
|
+
fromTurn: null,
|
|
188
|
+
toTurn: null,
|
|
189
|
+
}))
|
|
190
|
+
.filter((record) => Number.isInteger(record.fromSeq) && Number.isInteger(record.toSeq) && record.text.length > 0);
|
|
191
|
+
|
|
192
|
+
const seqs = list.map((event) => Number(event?.seq)).filter(Number.isInteger);
|
|
193
|
+
const minSeq = seqs.length ? Math.min(...seqs) : null;
|
|
194
|
+
const maxSeq = seqs.length ? Math.max(...seqs) : null;
|
|
195
|
+
const ranges = mergeRanges([...unique, ...boot, ...windows]);
|
|
196
|
+
const gaps = [];
|
|
197
|
+
if (minSeq !== null && maxSeq !== null) {
|
|
198
|
+
let cursor = minSeq;
|
|
199
|
+
for (const range of ranges) {
|
|
200
|
+
if (range.start > cursor) gaps.push({ start: cursor, end: range.start - 1 });
|
|
201
|
+
cursor = Math.max(cursor, range.end + 1);
|
|
202
|
+
}
|
|
203
|
+
if (cursor <= maxSeq) gaps.push({ start: cursor, end: maxSeq });
|
|
204
|
+
}
|
|
205
|
+
const pendingWindows = gaps
|
|
206
|
+
.map((gap) => ({
|
|
207
|
+
...gap,
|
|
208
|
+
approxTokens: estimateGapTokens(list, gap.start, gap.end),
|
|
209
|
+
}))
|
|
210
|
+
.filter((gap) => gap.approxTokens >= windowTokens)
|
|
211
|
+
.map((gap) => ({
|
|
212
|
+
id: `pending-${gap.start}-${gap.end}`,
|
|
213
|
+
fromSeq: gap.start,
|
|
214
|
+
toSeq: gap.end,
|
|
215
|
+
approxTokens: gap.approxTokens,
|
|
216
|
+
status: 'missing',
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
const sourceTokens = ranges.reduce((sum, range) => sum + estimateGapTokens(list, range.start, range.end), 0);
|
|
220
|
+
const summaryEvents = list.filter((event) => event?.type === 'compaction/summary');
|
|
221
|
+
const maxSummarySeq = summaryEvents.reduce((max, event) => {
|
|
222
|
+
const seq = Number(event?.seq);
|
|
223
|
+
return Number.isInteger(seq) && seq > max ? seq : max;
|
|
224
|
+
}, -1);
|
|
225
|
+
const coveredThroughSeq = [...unique, ...boot, ...windows].reduce((max, record) => (record.toSeq > max ? record.toSeq : max), -1);
|
|
226
|
+
const latestSummaryAt = summaryEvents.reduce((latest, event) => {
|
|
227
|
+
const time = Number(event?.time);
|
|
228
|
+
return Number.isFinite(time) && time > latest ? time : latest;
|
|
229
|
+
}, 0);
|
|
230
|
+
return {
|
|
231
|
+
version: SUMMARY_INDEX_VERSION,
|
|
232
|
+
sessionId: String(sessionId ?? ''),
|
|
233
|
+
generatedAt: new Date().toISOString(),
|
|
234
|
+
windowTokens,
|
|
235
|
+
maxSummarySeq,
|
|
236
|
+
coveredThroughSeq,
|
|
237
|
+
latestSummaryAt: latestSummaryAt > 0 ? new Date(latestSummaryAt).toISOString() : null,
|
|
238
|
+
records: [...unique, ...windowRecordList],
|
|
239
|
+
bootstrapRanges: boot,
|
|
240
|
+
windowRanges: windows,
|
|
241
|
+
pendingWindows,
|
|
242
|
+
coverage: {
|
|
243
|
+
minSeq,
|
|
244
|
+
maxSeq,
|
|
245
|
+
records: unique.length,
|
|
246
|
+
modelRecords: unique.length,
|
|
247
|
+
ready: unique.length,
|
|
248
|
+
windows: windows.length,
|
|
249
|
+
pending: pendingWindows.length,
|
|
250
|
+
bootstrap: boot.length,
|
|
251
|
+
ranges: ranges.length,
|
|
252
|
+
sourceTokensBasis: 'dedup-merged-ranges',
|
|
253
|
+
sourceTokens,
|
|
254
|
+
sourceTokensSum: unique.reduce((sum, record) => sum + (record.sourceTokens ?? 0), 0),
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function summaryIndexPath(sessionDir) {
|
|
260
|
+
return path.join(sessionDir, 'summaries', 'index.json');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Compare the summary index against the newest live summary event. */
|
|
264
|
+
export function summaryIndexStaleness(index, records, { latestSummarySeq = null } = {}) {
|
|
265
|
+
const fallbackSeq = (Array.isArray(records) ? records : [])
|
|
266
|
+
.map((record) => Number(record?.seqEnd ?? record?.seq))
|
|
267
|
+
.filter(Number.isInteger)
|
|
268
|
+
.reduce((max, seq) => (seq > max ? seq : max), -1);
|
|
269
|
+
const latestSeq = Number.isInteger(latestSummarySeq) ? latestSummarySeq : fallbackSeq;
|
|
270
|
+
const rawMax = Number(index?.maxSummarySeq);
|
|
271
|
+
const hasMax = Number.isInteger(rawMax) && rawMax >= 0;
|
|
272
|
+
const maxSummarySeq = hasMax ? rawMax : null;
|
|
273
|
+
const stale = !hasMax || !Number.isInteger(latestSeq) || latestSeq !== maxSummarySeq;
|
|
274
|
+
const staleBy = hasMax && Number.isInteger(latestSeq) ? latestSeq - maxSummarySeq : null;
|
|
275
|
+
return {
|
|
276
|
+
stale,
|
|
277
|
+
staleBy,
|
|
278
|
+
latestSeq,
|
|
279
|
+
maxSummarySeq,
|
|
280
|
+
latestSummaryAt: index?.latestSummaryAt ?? null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Local, read-only summary lookup. This is the seed for recall.summaryHits. */
|
|
285
|
+
export function querySummaryIndex(index, query, { limit = 5 } = {}) {
|
|
286
|
+
const q = String(query ?? '').normalize('NFKC').toLowerCase();
|
|
287
|
+
if (!q) return { total: 0, hits: [] };
|
|
288
|
+
const qChars = [...q];
|
|
289
|
+
const cjkQuery = /[\u3400-\u9fff]/.test(q);
|
|
290
|
+
const records = Array.isArray(index?.records) ? index.records : [];
|
|
291
|
+
const scored = [];
|
|
292
|
+
for (const record of records) {
|
|
293
|
+
const text = String(record.text ?? '').normalize('NFKC').toLowerCase();
|
|
294
|
+
if (!text) continue;
|
|
295
|
+
let score = 0;
|
|
296
|
+
const exact = text.includes(q);
|
|
297
|
+
if (exact) score += 100;
|
|
298
|
+
if (cjkQuery && qChars.length >= 2) {
|
|
299
|
+
const covered = qChars.filter((ch) => text.includes(ch)).length;
|
|
300
|
+
if (covered === qChars.length) score += 5;
|
|
301
|
+
}
|
|
302
|
+
if (score > 0) scored.push({ record, score, exact });
|
|
303
|
+
}
|
|
304
|
+
scored.sort((a, b) => b.score - a.score || b.record.toSeq - a.record.toSeq);
|
|
305
|
+
return {
|
|
306
|
+
total: scored.length,
|
|
307
|
+
hits: scored.slice(0, limit).map(({ record, score, exact }) => ({
|
|
308
|
+
id: record.id,
|
|
309
|
+
kind: record.kind,
|
|
310
|
+
quality: record.quality ?? null,
|
|
311
|
+
fromSeq: record.fromSeq,
|
|
312
|
+
toSeq: record.toSeq,
|
|
313
|
+
fromTurn: record.fromTurn,
|
|
314
|
+
toTurn: record.toTurn,
|
|
315
|
+
threads: record.threads,
|
|
316
|
+
approxTokens: record.approxTokens,
|
|
317
|
+
score: Number(score.toFixed(2)),
|
|
318
|
+
exact,
|
|
319
|
+
excerpt: String(record.text ?? '').slice(0, 500),
|
|
320
|
+
})),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function writeSummaryIndex(sessionDir, index) {
|
|
325
|
+
const file = summaryIndexPath(sessionDir);
|
|
326
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
327
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
328
|
+
fs.writeFileSync(tmp, JSON.stringify(index, null, 2), 'utf8');
|
|
329
|
+
fs.renameSync(tmp, file);
|
|
330
|
+
return file;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function readSummaryIndex(file) {
|
|
334
|
+
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
335
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, versioned extractive summary tree.
|
|
3
|
+
* Leaves are turns; segments and chapters only point at children and seq ranges.
|
|
4
|
+
* No LLM is used here, so rebuilding is byte-stable.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
function sha256(text) {
|
|
9
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function clip(text, max) {
|
|
13
|
+
const clean = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
14
|
+
return clean.length <= max ? clean : `${clean.slice(0, max)}…`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function nodeId(level, index) {
|
|
18
|
+
return `${level}-${String(index + 1).padStart(5, '0')}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildSummaryTree(records, {
|
|
22
|
+
segmentSize = 8,
|
|
23
|
+
chapterSize = 8,
|
|
24
|
+
} = {}) {
|
|
25
|
+
const leaves = records.map((record, index) => ({
|
|
26
|
+
id: nodeId('turn', index),
|
|
27
|
+
level: 'turn',
|
|
28
|
+
parentId: null,
|
|
29
|
+
childIds: [],
|
|
30
|
+
turn: record.turn,
|
|
31
|
+
seqStart: record.seqStart,
|
|
32
|
+
seqEnd: record.seqEnd,
|
|
33
|
+
summary: clip(record.text, 280),
|
|
34
|
+
sourceHash: record.hash,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const segments = [];
|
|
38
|
+
for (let start = 0; start < leaves.length; start += segmentSize) {
|
|
39
|
+
const children = leaves.slice(start, start + segmentSize);
|
|
40
|
+
const id = nodeId('segment', segments.length);
|
|
41
|
+
const summary = clip(children.map((child) => child.summary).join(' / '), 700);
|
|
42
|
+
const node = {
|
|
43
|
+
id,
|
|
44
|
+
level: 'segment',
|
|
45
|
+
parentId: null,
|
|
46
|
+
childIds: children.map((child) => child.id),
|
|
47
|
+
seqStart: children[0].seqStart,
|
|
48
|
+
seqEnd: children[children.length - 1].seqEnd,
|
|
49
|
+
firstTurn: children[0].turn,
|
|
50
|
+
lastTurn: children[children.length - 1].turn,
|
|
51
|
+
summary,
|
|
52
|
+
sourceHash: sha256(children.map((child) => child.sourceHash).join('')),
|
|
53
|
+
};
|
|
54
|
+
for (const child of children) child.parentId = id;
|
|
55
|
+
segments.push(node);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const chapters = [];
|
|
59
|
+
for (let start = 0; start < segments.length; start += chapterSize) {
|
|
60
|
+
const children = segments.slice(start, start + chapterSize);
|
|
61
|
+
const id = nodeId('chapter', chapters.length);
|
|
62
|
+
const summary = clip(children.map((child) => child.summary).join(' / '), 1200);
|
|
63
|
+
const node = {
|
|
64
|
+
id,
|
|
65
|
+
level: 'chapter',
|
|
66
|
+
parentId: null,
|
|
67
|
+
childIds: children.map((child) => child.id),
|
|
68
|
+
seqStart: children[0].seqStart,
|
|
69
|
+
seqEnd: children[children.length - 1].seqEnd,
|
|
70
|
+
firstTurn: children[0].firstTurn,
|
|
71
|
+
lastTurn: children[children.length - 1].lastTurn,
|
|
72
|
+
summary,
|
|
73
|
+
sourceHash: sha256(children.map((child) => child.sourceHash).join('')),
|
|
74
|
+
};
|
|
75
|
+
for (const child of children) child.parentId = id;
|
|
76
|
+
chapters.push(node);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const nodes = [...leaves, ...segments, ...chapters];
|
|
80
|
+
const treeRootHash = sha256(nodes.map((node) => node.sourceHash).join(''));
|
|
81
|
+
return {
|
|
82
|
+
schema: 1,
|
|
83
|
+
version: 1,
|
|
84
|
+
segmentSize,
|
|
85
|
+
chapterSize,
|
|
86
|
+
nodeCount: nodes.length,
|
|
87
|
+
levels: { turn: leaves.length, segment: segments.length, chapter: chapters.length },
|
|
88
|
+
treeRootHash,
|
|
89
|
+
nodes,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function verifySummaryTree(tree) {
|
|
94
|
+
const byId = new Map(tree.nodes.map((node) => [node.id, node]));
|
|
95
|
+
const errors = [];
|
|
96
|
+
for (const node of tree.nodes) {
|
|
97
|
+
for (const childId of node.childIds) {
|
|
98
|
+
const child = byId.get(childId);
|
|
99
|
+
if (child === undefined) {
|
|
100
|
+
errors.push({ id: node.id, reason: 'missing-child', childId });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (child.parentId !== node.id) errors.push({ id: child.id, reason: 'parent-mismatch' });
|
|
104
|
+
if (child.seqStart < node.seqStart || child.seqEnd > node.seqEnd) {
|
|
105
|
+
errors.push({ id: child.id, reason: 'child-outside-parent' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { ok: errors.length === 0, errors };
|
|
110
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term candidate extraction.
|
|
3
|
+
* Extracts definitional names, quoted names, and "X的规则/设定" patterns.
|
|
4
|
+
* Source weighting is applied later by the referent ledger.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFINITION = /(?:叫做|叫|名为|称为|名字是|称作)\s*[《「“"]?([\u4e00-\u9fffA-Za-z0-9_·-]{2,12})[》」”"]?/gu;
|
|
8
|
+
const QUOTED = /[《「“"]([^》」”"\n]{2,16})[》」”"]/gu;
|
|
9
|
+
const BEFORE_RULE = /([\u4e00-\u9fffA-Za-z0-9_·-]{2,10})(?:的)?(?:规则|设定|世界观|背景|系统)/gu;
|
|
10
|
+
const STOP = /^(什么|这个|那个|我们|你们|他们|最近|现在|已经|可以|需要|因为|所以|问题|东西|地方|名字|标签|备份|一个|一种|一下|这些|那些)$/u;
|
|
11
|
+
|
|
12
|
+
export function normalizeTerm(value) {
|
|
13
|
+
return String(value ?? '')
|
|
14
|
+
.replace(/[《》「」“”"'`]/g, '')
|
|
15
|
+
.replace(/[,。!?、;:\s]+$/g, '')
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validTerm(term) {
|
|
20
|
+
if (term.length < 2 || term.length > 12) return false;
|
|
21
|
+
if (STOP.test(term)) return false;
|
|
22
|
+
if (/^[0-9]+$/u.test(term)) return false;
|
|
23
|
+
if (/[,。!?、;:]/u.test(term)) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function extractTerms(text, { max = 8 } = {}) {
|
|
28
|
+
const source = String(text ?? '');
|
|
29
|
+
const found = [];
|
|
30
|
+
const add = (value, kind, index) => {
|
|
31
|
+
const term = normalizeTerm(value);
|
|
32
|
+
if (!validTerm(term)) return;
|
|
33
|
+
if (found.some((item) => item.term === term)) return;
|
|
34
|
+
found.push({ term, kind, index });
|
|
35
|
+
};
|
|
36
|
+
for (const regex of [DEFINITION, QUOTED, BEFORE_RULE]) {
|
|
37
|
+
regex.lastIndex = 0;
|
|
38
|
+
let match;
|
|
39
|
+
while ((match = regex.exec(source)) !== null) {
|
|
40
|
+
add(match[1], regex === DEFINITION ? 'definition' : regex === QUOTED ? 'quoted' : 'mention', match.index);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return found.sort((a, b) => a.index - b.index).slice(0, max);
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Shared tokenizer for BM25. */
|
|
2
|
+
export function tokenize(text) {
|
|
3
|
+
const raw = String(text ?? '').toLowerCase();
|
|
4
|
+
const words = raw.match(/[a-z0-9_]+/g) ?? [];
|
|
5
|
+
const chinese = [...raw.replace(/[^\u4e00-\u9fff]/g, '')];
|
|
6
|
+
const bigrams = [];
|
|
7
|
+
for (let i = 0; i + 1 < chinese.length; i += 1) bigrams.push(chinese[i] + chinese[i + 1]);
|
|
8
|
+
return [...words, ...bigrams];
|
|
9
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brute-force vector index for the durable-memory prototype.
|
|
3
|
+
* At 1,462 turns this is exact and cheap; HNSW can replace it later.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
function cosine(a, b) {
|
|
8
|
+
let dot = 0;
|
|
9
|
+
for (let i = 0; i < a.length; i += 1) dot += a[i] * b[i];
|
|
10
|
+
return dot;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class VectorIndex {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.docs = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
build(records, vectorsByTurn) {
|
|
19
|
+
this.docs = [];
|
|
20
|
+
for (const record of records) {
|
|
21
|
+
const vector = vectorsByTurn.get(record.turn);
|
|
22
|
+
if (vector === undefined) continue;
|
|
23
|
+
this.docs.push({
|
|
24
|
+
turn: record.turn,
|
|
25
|
+
seqStart: record.seqStart,
|
|
26
|
+
seqEnd: record.seqEnd,
|
|
27
|
+
vector,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
search(vector, { topK = 20 } = {}) {
|
|
34
|
+
return this.docs
|
|
35
|
+
.map((doc) => ({ ...doc, score: cosine(vector, doc.vector) }))
|
|
36
|
+
.sort((a, b) => b.score - a.score)
|
|
37
|
+
.slice(0, topK);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
toJSON() {
|
|
41
|
+
return { docs: this.docs };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static fromJSON(raw) {
|
|
45
|
+
const index = new VectorIndex();
|
|
46
|
+
index.docs = raw.docs;
|
|
47
|
+
return index;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadEmbeddings(file) {
|
|
52
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
53
|
+
return new Map(data.map((row) => [row.turn, row.vector]));
|
|
54
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic window summary.
|
|
3
|
+
*
|
|
4
|
+
* This is the fallback generator for a pending archive window when no
|
|
5
|
+
* compaction/checkpoint summary exists. It is deliberately structural, not a
|
|
6
|
+
* replacement for an LLM summary: it preserves who said/did what and where the
|
|
7
|
+
* raw seqs are, so a later model pass can replace the body without changing the
|
|
8
|
+
* carrier contract.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_TOKENS = 800;
|
|
13
|
+
const CHARS_PER_TOKEN = 4;
|
|
14
|
+
const INJECTED = /^(?:\[(?:continuity:|追忆|抽取|工作线|标签|规则|规矩|习惯|技能|归位|候选|存\?|闸|回正|学习|记忆)|<system-reminder>|Current runtime context|Time sampled while preparing)/i;
|
|
15
|
+
const PEER = /^\[(?:创|peer)\]/i;
|
|
16
|
+
|
|
17
|
+
function textOf(event) {
|
|
18
|
+
const data = event?.data ?? {};
|
|
19
|
+
const blocks = data?.message?.content ?? data?.content ?? [];
|
|
20
|
+
if (!Array.isArray(blocks)) return '';
|
|
21
|
+
return blocks
|
|
22
|
+
.filter((block) => block && typeof block === 'object' && block.type === 'text')
|
|
23
|
+
.map((block) => String(block.text ?? ''))
|
|
24
|
+
.join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function oneLine(text, maxChars) {
|
|
28
|
+
const clean = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
29
|
+
if (clean.length <= maxChars) return clean;
|
|
30
|
+
return `${clean.slice(0, maxChars).replace(/\s+\S*$/, '')}…`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toolNames(event) {
|
|
34
|
+
const data = event?.data ?? {};
|
|
35
|
+
const name = data?.name ?? data?.toolName ?? data?.message?.name;
|
|
36
|
+
return typeof name === 'string' && name.length > 0 ? name : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildWindowSummary({
|
|
40
|
+
events,
|
|
41
|
+
fromSeq,
|
|
42
|
+
toSeq,
|
|
43
|
+
maxTokens = DEFAULT_MAX_TOKENS,
|
|
44
|
+
}) {
|
|
45
|
+
const from = Math.min(Number(fromSeq), Number(toSeq));
|
|
46
|
+
const to = Math.max(Number(fromSeq), Number(toSeq));
|
|
47
|
+
const list = (Array.isArray(events) ? events : [])
|
|
48
|
+
.filter((event) => {
|
|
49
|
+
const seq = Number(event?.seq);
|
|
50
|
+
return Number.isInteger(seq) && seq >= from && seq <= to;
|
|
51
|
+
})
|
|
52
|
+
.sort((a, b) => Number(a.seq) - Number(b.seq));
|
|
53
|
+
|
|
54
|
+
const users = [];
|
|
55
|
+
const peers = [];
|
|
56
|
+
const assistants = [];
|
|
57
|
+
const tools = [];
|
|
58
|
+
for (const event of list) {
|
|
59
|
+
if (event?.type === 'user/message') {
|
|
60
|
+
const text = textOf(event).trim();
|
|
61
|
+
if (text.length === 0 || INJECTED.test(text)) continue;
|
|
62
|
+
if (PEER.test(text)) peers.push(event);
|
|
63
|
+
else users.push(event);
|
|
64
|
+
} else if (event?.type === 'assistant/message') {
|
|
65
|
+
if (textOf(event).trim().length > 0) assistants.push(event);
|
|
66
|
+
} else if (event?.type === 'tool/call') {
|
|
67
|
+
const name = toolNames(event);
|
|
68
|
+
if (name !== null) tools.push(name);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(`## 窗口 seq ${from}-${to}`);
|
|
74
|
+
lines.push(`事件 ${list.length} · 用户 ${users.length} · 同伴 ${peers.length} · 助手 ${assistants.length} · 工具 ${tools.length}`);
|
|
75
|
+
|
|
76
|
+
const pickedUsers = users.length <= 4
|
|
77
|
+
? users
|
|
78
|
+
: [...users.slice(0, 2), ...users.slice(-2)];
|
|
79
|
+
const pickedPeers = peers.length <= 4
|
|
80
|
+
? peers
|
|
81
|
+
: [...peers.slice(0, 2), ...peers.slice(-2)];
|
|
82
|
+
const pickedAssistants = assistants.length <= 4
|
|
83
|
+
? assistants
|
|
84
|
+
: [...assistants.slice(0, 2), ...assistants.slice(-2)];
|
|
85
|
+
|
|
86
|
+
if (pickedUsers.length > 0) {
|
|
87
|
+
lines.push('## 用户');
|
|
88
|
+
for (const event of pickedUsers) {
|
|
89
|
+
const text = oneLine(textOf(event), 220);
|
|
90
|
+
if (text.length > 0) lines.push(`- seq ${event.seq}: ${text}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (pickedPeers.length > 0) {
|
|
94
|
+
lines.push('## 同伴');
|
|
95
|
+
for (const event of pickedPeers) {
|
|
96
|
+
const text = oneLine(textOf(event), 220);
|
|
97
|
+
if (text.length > 0) lines.push(`- (peer) seq ${event.seq}: ${text}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (pickedAssistants.length > 0) {
|
|
101
|
+
lines.push('## 助手');
|
|
102
|
+
for (const event of pickedAssistants) {
|
|
103
|
+
const text = oneLine(textOf(event), 220);
|
|
104
|
+
if (text.length > 0) lines.push(`- seq ${event.seq}: ${text}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const uniqueTools = [...new Set(tools)];
|
|
108
|
+
if (uniqueTools.length > 0) {
|
|
109
|
+
lines.push('## 工具');
|
|
110
|
+
lines.push(uniqueTools.slice(0, 12).join(', '));
|
|
111
|
+
}
|
|
112
|
+
lines.push('## 指针');
|
|
113
|
+
lines.push(`本窗口 raw 首尾:${from}..${to};细节按 seq 回 session_log 取。`);
|
|
114
|
+
|
|
115
|
+
let text = lines.join('\n');
|
|
116
|
+
const capChars = Math.max(400, maxTokens) * CHARS_PER_TOKEN;
|
|
117
|
+
if (text.length > capChars) {
|
|
118
|
+
text = `${text.slice(0, capChars).replace(/\s+\S*$/, '')}…`;
|
|
119
|
+
}
|
|
120
|
+
const approxTokens = Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
121
|
+
return {
|
|
122
|
+
fromSeq: from,
|
|
123
|
+
toSeq: to,
|
|
124
|
+
text,
|
|
125
|
+
chars: text.length,
|
|
126
|
+
approxTokens,
|
|
127
|
+
hash: createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 16),
|
|
128
|
+
kind: 'window-extractive',
|
|
129
|
+
quality: list.length > 1000 && approxTokens < 400 ? 'extractive-thin' : 'extractive',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default buildWindowSummary;
|