@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,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance labels for compacted memory. Pure functions; no DSH imports.
|
|
3
|
+
*
|
|
4
|
+
* The point is not to announce a compaction event, but to make the result
|
|
5
|
+
* carry its own epistemic status: a block tagged `[gist]` is a reconstruction,
|
|
6
|
+
* with a pointer back to the original. A reader can then say "I only remember
|
|
7
|
+
* the gist" instead of mistaking the summary for verbatim memory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** One standing convention for the system prompt; not a per-event banner. */
|
|
11
|
+
export const SYSTEM_CONVENTION = [
|
|
12
|
+
'Blocks tagged [gist] are reconstructions, not verbatim memory.',
|
|
13
|
+
'Use the gist when it is enough; before quoting an exact phrase, date, number, promise, or name, retrieve the original with session_log.',
|
|
14
|
+
'If the gist header has seq=A-B, call session_log get for that session and range.',
|
|
15
|
+
'If a gist contains an older gist pointer, follow the older pointer to the original.',
|
|
16
|
+
'Without a pointer, call session_log index, then grep, then get the matching seq ± context.',
|
|
17
|
+
'For a large range, get a preview or grep first and narrow; do not dump it.',
|
|
18
|
+
'Tool-result previews are truncations, not verbatim output; if the exact tool output matters, call session_log get seq=<n> for the cited original.',
|
|
19
|
+
'A [tool result handle] replaces a non-text payload; the payload is archived, so inspect the handle and retrieve the original by seq or attachment path before relying on it.',
|
|
20
|
+
'If a [continuity pause] handle appears, do not retry that tool or keep compressing: tell the user the context window is too small and ask them to raise it, while preserving the session.',
|
|
21
|
+
'If the original is unavailable, say you only remember the gist; stop after 2–3 fetches.',
|
|
22
|
+
].join(' ');
|
|
23
|
+
|
|
24
|
+
export const LABEL = Object.freeze({
|
|
25
|
+
verbatim: '[verbatim]',
|
|
26
|
+
gist: '[gist]',
|
|
27
|
+
uncertain: '[uncertain]',
|
|
28
|
+
open: '[open]',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Header for one compacted block.
|
|
33
|
+
*
|
|
34
|
+
* @param {{ pointer?: string, sessionId?: string, startSeq?: number|string, endSeq?: number|string, count?: number, t?: string }} input
|
|
35
|
+
* @returns {string} e.g. `[gist · log:main#seq=120-154 · T=T3 · 34 messages · original retrievable via session_log]`
|
|
36
|
+
*/
|
|
37
|
+
export function gistHeader({ pointer, sessionId, startSeq, endSeq, count, t } = {}) {
|
|
38
|
+
let where = pointer?.trim() || '';
|
|
39
|
+
if (!where) {
|
|
40
|
+
if (sessionId == null || startSeq == null || endSeq == null) {
|
|
41
|
+
throw new Error('gistHeader needs a pointer or a session id with a seq range');
|
|
42
|
+
}
|
|
43
|
+
where = `log:${sessionId}#seq=${startSeq}-${endSeq}`;
|
|
44
|
+
}
|
|
45
|
+
const suffix = Number.isFinite(count) && count > 0 ? ` · ${count} messages` : '';
|
|
46
|
+
const tags = typeof t === 'string' && t.trim().length > 0 ? ` · T=${t.trim()}` : '';
|
|
47
|
+
return `[gist · ${where}${tags}${suffix} · original retrievable via session_log]`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Render one labeled block. Empty sections are omitted; order is fixed:
|
|
52
|
+
* verbatim, gist, uncertain, open.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ header: string, verbatim?: string[], gist?: string[], uncertain?: string[], open?: string[] }} input
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function renderGistBlock({ header, verbatim = [], gist = [], uncertain = [], open = [] } = {}) {
|
|
58
|
+
if (typeof header !== 'string' || !header.startsWith('[gist')) {
|
|
59
|
+
throw new Error('renderGistBlock needs the header from gistHeader()');
|
|
60
|
+
}
|
|
61
|
+
const lines = [header];
|
|
62
|
+
const push = (tag, items) => {
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const text = String(item ?? '').trim();
|
|
65
|
+
if (text) lines.push(`- ${tag} ${text}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
push(LABEL.verbatim, verbatim);
|
|
69
|
+
push(LABEL.gist, gist);
|
|
70
|
+
push(LABEL.uncertain, uncertain);
|
|
71
|
+
push(LABEL.open, open);
|
|
72
|
+
return lines.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prepend a gist header to the first text block of a summary, without touching
|
|
77
|
+
* the other blocks. Used by an engine's `summarize()` hook.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array<Record<string, unknown>>} blocks
|
|
80
|
+
* @param {string} header
|
|
81
|
+
* @returns {Array<Record<string, unknown>>}
|
|
82
|
+
*/
|
|
83
|
+
export function annotateSummary(blocks, header) {
|
|
84
|
+
if (typeof header !== 'string' || !header) throw new Error('annotateSummary needs a header');
|
|
85
|
+
const list = Array.isArray(blocks) ? blocks.map((block) => ({ ...block })) : [];
|
|
86
|
+
const index = list.findIndex((block) => typeof block?.text === 'string');
|
|
87
|
+
if (index >= 0) {
|
|
88
|
+
list[index] = { ...list[index], text: `${header}\n${list[index].text}` };
|
|
89
|
+
return list;
|
|
90
|
+
}
|
|
91
|
+
return [{ type: 'text', text: header }, ...list];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default { SYSTEM_CONVENTION, LABEL, gistHeader, renderGistBlock, annotateSummary };
|
package/src/range.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure oldest-range selection for the band controller. No DSH imports.
|
|
3
|
+
*
|
|
4
|
+
* The engine supplies the session surface, each surface node's route-priced
|
|
5
|
+
* token count, and the two tool-pairing predicates. This module owns only the
|
|
6
|
+
* arithmetic and the edge rules:
|
|
7
|
+
*
|
|
8
|
+
* - never include a system head at surface position 0;
|
|
9
|
+
* - take the oldest contiguous prefix so the summary replaces "the beginning";
|
|
10
|
+
* - stop at `budget` and leave at least `tailFloor` tokens after the cut;
|
|
11
|
+
* - move the end backwards until the trailing cut is balanced;
|
|
12
|
+
* - return null when no balanced span fits.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} input
|
|
17
|
+
* @param {readonly (number|string)[]} input.surfaceSeqs current surface positions, oldest first
|
|
18
|
+
* @param {readonly number[]} input.pricedTokens route-priced tokens per position
|
|
19
|
+
* @param {boolean} [input.systemHead] true when position 0 is the system message
|
|
20
|
+
* @param {number} input.budget maximum route-priced tokens to shadow in this pass
|
|
21
|
+
* @param {number} [input.tailFloor] minimum route-priced tokens that must remain after the cut
|
|
22
|
+
* @param {(seq: number|string) => boolean} input.balancedBefore leading-cut predicate
|
|
23
|
+
* @param {(seq: number|string) => boolean} input.balancedAfter trailing-cut predicate
|
|
24
|
+
* @returns {{ startIdx: number, endIdx: number, tokens: number } | null}
|
|
25
|
+
*/
|
|
26
|
+
export function selectOldestRange({
|
|
27
|
+
surfaceSeqs,
|
|
28
|
+
pricedTokens,
|
|
29
|
+
systemHead = false,
|
|
30
|
+
budget,
|
|
31
|
+
tailFloor = 0,
|
|
32
|
+
balancedBefore,
|
|
33
|
+
balancedAfter,
|
|
34
|
+
}) {
|
|
35
|
+
if (!Array.isArray(surfaceSeqs) || surfaceSeqs.length === 0) return null;
|
|
36
|
+
if (!Array.isArray(pricedTokens) || pricedTokens.length !== surfaceSeqs.length) return null;
|
|
37
|
+
if (!Number.isFinite(budget) || budget <= 0) return null;
|
|
38
|
+
if (typeof balancedBefore !== 'function' || typeof balancedAfter !== 'function') {
|
|
39
|
+
throw new TypeError('selectOldestRange needs both balance predicates');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const firstIdx = systemHead ? 1 : 0;
|
|
43
|
+
if (firstIdx >= surfaceSeqs.length) return null;
|
|
44
|
+
|
|
45
|
+
let surfaceTotal = 0;
|
|
46
|
+
for (const tokens of pricedTokens) surfaceTotal += Number.isFinite(tokens) ? tokens : 0;
|
|
47
|
+
|
|
48
|
+
let accumulated = 0;
|
|
49
|
+
let endIdx = -1;
|
|
50
|
+
for (let index = firstIdx; index < surfaceSeqs.length; index += 1) {
|
|
51
|
+
const tokens = Number.isFinite(pricedTokens[index]) ? pricedTokens[index] : 0;
|
|
52
|
+
const remainingAfter = surfaceTotal - accumulated - tokens;
|
|
53
|
+
// Leaving less than the protected tail after the cut is never allowed:
|
|
54
|
+
// that is the "recent verbatim window" the policy refuses to fold.
|
|
55
|
+
if (remainingAfter < tailFloor) break;
|
|
56
|
+
if (accumulated + tokens > budget) break;
|
|
57
|
+
accumulated += tokens;
|
|
58
|
+
endIdx = index;
|
|
59
|
+
}
|
|
60
|
+
if (endIdx < firstIdx) return null;
|
|
61
|
+
|
|
62
|
+
// A balanced trailing cut can only move earlier, which keeps the pass small
|
|
63
|
+
// and leaves more of the recent tail untouched.
|
|
64
|
+
while (endIdx >= firstIdx && !balancedAfter(surfaceSeqs[endIdx])) endIdx -= 1;
|
|
65
|
+
if (endIdx < firstIdx) return null;
|
|
66
|
+
if (!balancedBefore(surfaceSeqs[firstIdx])) return null;
|
|
67
|
+
|
|
68
|
+
let tokens = 0;
|
|
69
|
+
for (let index = firstIdx; index <= endIdx; index += 1) {
|
|
70
|
+
tokens += Number.isFinite(pricedTokens[index]) ? pricedTokens[index] : 0;
|
|
71
|
+
}
|
|
72
|
+
return { startIdx: firstIdx, endIdx, tokens };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default selectOldestRange;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* recall_exact — read-only literal search inside the current Continuity session.
|
|
3
|
+
*
|
|
4
|
+
* Merged from the former `@local/recall-exact-shadow`. It does not replace
|
|
5
|
+
* `recall`, does not rank, and does not write. It binds to the current session
|
|
6
|
+
* from the tool exec context; there is no path parameter. Source is
|
|
7
|
+
* `sessionQuery.readSession(sessionId)` / `observeSession`.
|
|
8
|
+
*/
|
|
9
|
+
const ASCII_WORD = /[0-9a-z]/;
|
|
10
|
+
const CJK = /[\u3400-\u9fff]/;
|
|
11
|
+
const SINGLE_CHAR_WHITELIST = new Set(['创']);
|
|
12
|
+
const TOO_BROAD_INTERVALS = 20;
|
|
13
|
+
const CACHE_LIMIT = 2;
|
|
14
|
+
const SESSION_CACHE = new Map();
|
|
15
|
+
const INJECTED = /^\[(?:continuity:|追忆|抽取|工作线|标签|规则|规矩|习惯|技能|归位|候选|存\?|闸|回正|学习|记忆|创|peer)|^<system-reminder>|^Current runtime context|^Time sampled while preparing/i;
|
|
16
|
+
|
|
17
|
+
function normalizeText(text) {
|
|
18
|
+
return String(text ?? '').normalize('NFKC').toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractBlocks(events) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const excluded = { reasoning: 0, toolCall: 0, other: 0, injected: 0 };
|
|
24
|
+
let currentTurn = null;
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
const type = String(event?.type ?? '');
|
|
27
|
+
if (type === 'turn/start') {
|
|
28
|
+
const turn = Number(event?.data?.turn);
|
|
29
|
+
if (Number.isInteger(turn)) currentTurn = turn;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (type !== 'user/message' && type !== 'assistant/message') continue;
|
|
33
|
+
const content = type === 'assistant/message'
|
|
34
|
+
? event?.data?.message?.content ?? event?.data?.content
|
|
35
|
+
: event?.data?.content ?? event?.data?.message?.content;
|
|
36
|
+
let text = '';
|
|
37
|
+
for (const block of content ?? []) {
|
|
38
|
+
if (!block) continue;
|
|
39
|
+
if (block.type === 'text') text += block.text ?? '';
|
|
40
|
+
else if (block.type === 'reasoning') excluded.reasoning += 1;
|
|
41
|
+
else if (block.type === 'tool-call' || block.type === 'tool_call') excluded.toolCall += 1;
|
|
42
|
+
else excluded.other += 1;
|
|
43
|
+
}
|
|
44
|
+
text = text.trim();
|
|
45
|
+
if (!text) continue;
|
|
46
|
+
if (INJECTED.test(text)) {
|
|
47
|
+
excluded.injected += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const rawSeq = Number(event.seq);
|
|
51
|
+
blocks.push({
|
|
52
|
+
seq: Number.isInteger(rawSeq) ? rawSeq : null,
|
|
53
|
+
turn: Number.isInteger(event?.data?.turn) ? Number(event.data.turn) : currentTurn,
|
|
54
|
+
role: type === 'user/message' ? 'user' : 'assistant',
|
|
55
|
+
ts: Number.isFinite(Number(event.time)) ? new Date(Number(event.time)).toISOString() : String(event.ts ?? ''),
|
|
56
|
+
text,
|
|
57
|
+
norm: normalizeText(text),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
blocks.excluded = excluded;
|
|
61
|
+
blocks.currentTurn = currentTurn;
|
|
62
|
+
return blocks;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function loadBlocks(sessionQuery, sessionId) {
|
|
66
|
+
let events = [];
|
|
67
|
+
let revision = null;
|
|
68
|
+
if (typeof sessionQuery.observeSession === 'function') {
|
|
69
|
+
const observation = await sessionQuery.observeSession(sessionId, { projectionMode: 'none' });
|
|
70
|
+
try {
|
|
71
|
+
events = Array.isArray(observation?.events) ? [...observation.events] : [];
|
|
72
|
+
revision = observation?.revision ?? null;
|
|
73
|
+
} finally {
|
|
74
|
+
if (typeof observation?.[Symbol.dispose] === 'function') observation[Symbol.dispose]();
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
const snapshot = await sessionQuery.readSession(sessionId);
|
|
78
|
+
events = Array.isArray(snapshot?.events) ? snapshot.events : [];
|
|
79
|
+
revision = events.length > 0 ? events[events.length - 1]?.seq ?? null : null;
|
|
80
|
+
}
|
|
81
|
+
const lastSeq = events.length > 0 ? events[events.length - 1]?.seq ?? null : null;
|
|
82
|
+
const key = `${events.length}|${String(revision)}|${String(lastSeq)}`;
|
|
83
|
+
const cached = SESSION_CACHE.get(sessionId);
|
|
84
|
+
if (cached && cached.key === key) return { blocks: cached.blocks, cache: 'hit' };
|
|
85
|
+
const blocks = extractBlocks(events);
|
|
86
|
+
if (SESSION_CACHE.size >= CACHE_LIMIT) SESSION_CACHE.delete(SESSION_CACHE.keys().next().value);
|
|
87
|
+
SESSION_CACHE.set(sessionId, { key, blocks });
|
|
88
|
+
return { blocks, cache: 'miss' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function boundaryHit(norm, at, length) {
|
|
92
|
+
const before = at === 0 ? '' : norm[at - 1];
|
|
93
|
+
const after = at + length >= norm.length ? '' : norm[at + length];
|
|
94
|
+
return !ASCII_WORD.test(before) && !ASCII_WORD.test(after);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function countOccurrences(block, query) {
|
|
98
|
+
const norm = block.norm ?? normalizeText(block.text);
|
|
99
|
+
if (!norm.includes(query)) return 0;
|
|
100
|
+
let count = 0;
|
|
101
|
+
let from = 0;
|
|
102
|
+
while (from <= norm.length - query.length) {
|
|
103
|
+
const at = norm.indexOf(query, from);
|
|
104
|
+
if (at < 0) break;
|
|
105
|
+
if (boundaryHit(norm, at, query.length)) count += 1;
|
|
106
|
+
from = at + Math.max(1, query.length);
|
|
107
|
+
}
|
|
108
|
+
return count;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function groupIntervals(items) {
|
|
112
|
+
const sorted = [...items].sort((a, b) => a.turn - b.turn || (a.seq ?? 0) - (b.seq ?? 0));
|
|
113
|
+
const groups = [];
|
|
114
|
+
for (const item of sorted) {
|
|
115
|
+
const last = groups.at(-1);
|
|
116
|
+
const seq = Number.isInteger(item.seq) ? item.seq : null;
|
|
117
|
+
if (last && item.turn - last.toTurn <= 1) {
|
|
118
|
+
last.toTurn = item.turn;
|
|
119
|
+
last.toSeq = seq;
|
|
120
|
+
if (seq !== null) last.seqEnd = Math.max(last.seqEnd ?? seq, seq);
|
|
121
|
+
last.occurrences += item.occurrences;
|
|
122
|
+
last.hits += 1;
|
|
123
|
+
} else {
|
|
124
|
+
groups.push({
|
|
125
|
+
fromTurn: item.turn,
|
|
126
|
+
toTurn: item.turn,
|
|
127
|
+
fromSeq: seq,
|
|
128
|
+
toSeq: seq,
|
|
129
|
+
seqEnd: seq,
|
|
130
|
+
occurrences: item.occurrences,
|
|
131
|
+
hits: 1,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return groups;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fail(error) {
|
|
139
|
+
return JSON.stringify({ ok: false, error });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createRecallExactTool({ sessionQuery }) {
|
|
143
|
+
return {
|
|
144
|
+
name: 'recall_exact',
|
|
145
|
+
description: 'Read-only exact token/substring search over the current session. Use when recall returned tooBroad or a K hit that is too wide; it is a parallel channel and never replaces recall. It searches visible user + assistant text only (no reasoning/tool-call), returns grouped turn/seq intervals plus totalTurns/totalIntervals/turnShare, and does not rank or write.',
|
|
146
|
+
parameters: {
|
|
147
|
+
type: 'object',
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
properties: {
|
|
150
|
+
query: { type: 'string', description: 'Exact token or substring. ASCII tokens use [0-9A-Za-z] boundaries; non-ASCII uses substring matching; single CJK chars must be whitelisted.' },
|
|
151
|
+
since: { type: 'integer', description: 'Optional inclusive lower turn bound.' },
|
|
152
|
+
until: { type: 'integer', description: 'Optional inclusive upper turn bound.' },
|
|
153
|
+
near: { type: 'integer', description: 'Optional center turn; cannot be combined with since/until.' },
|
|
154
|
+
window: { type: 'integer', description: 'Turn radius for near; defaults to 20.' },
|
|
155
|
+
limit: { type: 'integer', description: 'Maximum returned intervals; default 20, max 200. Totals are never truncated.' },
|
|
156
|
+
},
|
|
157
|
+
required: ['query'],
|
|
158
|
+
},
|
|
159
|
+
output: {
|
|
160
|
+
schema: { type: 'string' },
|
|
161
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
162
|
+
},
|
|
163
|
+
timeoutMs: 30_000,
|
|
164
|
+
async execute(args = {}, exec = {}) {
|
|
165
|
+
const queryRaw = String(args.query ?? '').trim();
|
|
166
|
+
if (!queryRaw) return fail('query-required');
|
|
167
|
+
const query = normalizeText(queryRaw);
|
|
168
|
+
if (CJK.test(query) && [...query].length === 1 && !SINGLE_CHAR_WHITELIST.has(query)) {
|
|
169
|
+
return fail('single-char-cjk-not-whitelisted');
|
|
170
|
+
}
|
|
171
|
+
const numberArg = (key) => {
|
|
172
|
+
if (args[key] === undefined || args[key] === null) return null;
|
|
173
|
+
const value = Number(args[key]);
|
|
174
|
+
return Number.isFinite(value) && Number.isInteger(value) ? value : NaN;
|
|
175
|
+
};
|
|
176
|
+
const since = numberArg('since');
|
|
177
|
+
const until = numberArg('until');
|
|
178
|
+
const near = numberArg('near');
|
|
179
|
+
const windowRaw = numberArg('window');
|
|
180
|
+
const limitRaw = numberArg('limit');
|
|
181
|
+
if ([since, until, near, windowRaw, limitRaw].some(Number.isNaN)) return fail('bounds-must-be-integers');
|
|
182
|
+
if (near !== null && (since !== null || until !== null)) return fail('near-cannot-be-combined-with-since-until');
|
|
183
|
+
const window = near === null ? null : Math.max(1, windowRaw ?? 20);
|
|
184
|
+
const limit = Math.min(Math.max(limitRaw ?? 20, 1), 200);
|
|
185
|
+
const sessionId = exec?.agent?.session?.id;
|
|
186
|
+
if (sessionId === undefined || sessionId === null) return fail('current-session-required');
|
|
187
|
+
const started = Date.now();
|
|
188
|
+
let loaded;
|
|
189
|
+
try {
|
|
190
|
+
loaded = await loadBlocks(sessionQuery, sessionId);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return fail(`session-read-failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
193
|
+
}
|
|
194
|
+
const { blocks, cache } = loaded;
|
|
195
|
+
const loadMs = Date.now() - started;
|
|
196
|
+
let skippedNoTurn = 0;
|
|
197
|
+
const selected = [];
|
|
198
|
+
for (const block of blocks) {
|
|
199
|
+
if (!Number.isInteger(block.turn)) { skippedNoTurn += 1; continue; }
|
|
200
|
+
if (near !== null && Math.abs(block.turn - near) > window) continue;
|
|
201
|
+
if (near === null && since !== null && block.turn < since) continue;
|
|
202
|
+
if (near === null && until !== null && block.turn > until) continue;
|
|
203
|
+
selected.push(block);
|
|
204
|
+
}
|
|
205
|
+
const matches = [];
|
|
206
|
+
for (const block of selected) {
|
|
207
|
+
const occurrences = countOccurrences(block, query);
|
|
208
|
+
if (occurrences > 0) matches.push({ ...block, occurrences });
|
|
209
|
+
}
|
|
210
|
+
const intervals = groupIntervals(matches);
|
|
211
|
+
const turns = new Set(matches.map((match) => match.turn));
|
|
212
|
+
const roleTurns = { user: new Set(), assistant: new Set() };
|
|
213
|
+
for (const match of matches) roleTurns[match.role].add(match.turn);
|
|
214
|
+
const sessionTurns = new Set(blocks.map((block) => block.turn).filter(Number.isInteger)).size;
|
|
215
|
+
const turnShare = sessionTurns > 0 ? Number((turns.size / sessionTurns).toFixed(3)) : null;
|
|
216
|
+
const nullSeq = matches.filter((match) => !Number.isInteger(match.seq)).length;
|
|
217
|
+
const days = new Map();
|
|
218
|
+
for (const match of matches) {
|
|
219
|
+
const day = String(match.ts).slice(0, 10) || 'unknown';
|
|
220
|
+
const entry = days.get(day) ?? { turns: new Set(), blocks: 0 };
|
|
221
|
+
entry.blocks += 1;
|
|
222
|
+
entry.turns.add(match.turn);
|
|
223
|
+
days.set(day, entry);
|
|
224
|
+
}
|
|
225
|
+
const histogram = Object.fromEntries([...days.entries()].map(([day, value]) => [day, {
|
|
226
|
+
turns: value.turns.size,
|
|
227
|
+
blocks: value.blocks,
|
|
228
|
+
}]));
|
|
229
|
+
const wideByIntervals = intervals.length > TOO_BROAD_INTERVALS;
|
|
230
|
+
const tooBroad = wideByIntervals && (turnShare === null || turnShare >= 0.2);
|
|
231
|
+
const report = {
|
|
232
|
+
ok: true,
|
|
233
|
+
query: queryRaw,
|
|
234
|
+
source: 'sessionQuery',
|
|
235
|
+
sessionScope: 'current session id from exec; no path parameter',
|
|
236
|
+
surface: 'user+assistant.text',
|
|
237
|
+
mode: 'token/substring',
|
|
238
|
+
scope: near !== null
|
|
239
|
+
? { unit: 'turn', near, window }
|
|
240
|
+
: { unit: 'turn', since, until },
|
|
241
|
+
cache,
|
|
242
|
+
loadMs,
|
|
243
|
+
skippedNoTurn,
|
|
244
|
+
nullSeq,
|
|
245
|
+
surfaceStats: {
|
|
246
|
+
userBlocks: blocks.filter((block) => block.role === 'user').length,
|
|
247
|
+
assistantBlocks: blocks.filter((block) => block.role === 'assistant').length,
|
|
248
|
+
excluded: blocks.excluded ?? { reasoning: 0, toolCall: 0, other: 0, injected: 0 },
|
|
249
|
+
},
|
|
250
|
+
totalTurns: turns.size,
|
|
251
|
+
totalIntervals: intervals.length,
|
|
252
|
+
shown: Math.min(limit, intervals.length),
|
|
253
|
+
wideByIntervals,
|
|
254
|
+
tooBroad,
|
|
255
|
+
tooBroadBy: tooBroad ? 'intervals+turnShare' : 'none',
|
|
256
|
+
turnShare,
|
|
257
|
+
roleTurns: { user: roleTurns.user.size, assistant: roleTurns.assistant.size },
|
|
258
|
+
firstTurn: intervals[0]?.fromTurn ?? null,
|
|
259
|
+
lastTurn: intervals.at(-1)?.toTurn ?? null,
|
|
260
|
+
histogram,
|
|
261
|
+
note: 'read-only; exact token/substring only; no reasoning/tool-call; tooBroad = intervals>20 AND turnShare>=0.2; use recall_exact when recall is too broad',
|
|
262
|
+
hits: intervals.slice(0, limit),
|
|
263
|
+
};
|
|
264
|
+
return JSON.stringify(report);
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|