@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B+C panel state: counts of B generic labels in the pointer index.
|
|
3
|
+
* Read-only; no session access.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { loadIndexJsonl } from './bc-index.mjs';
|
|
8
|
+
import { DEFAULT_C_SEED } from './bc-recall.mjs';
|
|
9
|
+
import { defaultCMapPath, loadCMapEntries } from './cmap-file.mjs';
|
|
10
|
+
import { defaultSpecialKeywordsPath, loadSpecialKeywords } from './special-keywords.mjs';
|
|
11
|
+
import { loadConcepts } from './concepts-file.mjs';
|
|
12
|
+
|
|
13
|
+
export function buildBCPanelState({ file = null, keywordsFile = null, cmapFile = null, conceptsFile = null, home = process.env.USERPROFILE ?? process.env.HOME ?? '' } = {}) {
|
|
14
|
+
const indexFile = file ?? path.join(home, '.dsh', 'continuity', 'bc-index.jsonl');
|
|
15
|
+
if (!fs.existsSync(indexFile)) {
|
|
16
|
+
return { available: false, file: indexFile, error: 'bc-index.jsonl not found', generatedAt: new Date().toISOString() };
|
|
17
|
+
}
|
|
18
|
+
const records = loadIndexJsonl(indexFile);
|
|
19
|
+
const turns = records.map((record) => record.turn).filter((turn) => Number.isInteger(turn)).sort((a, b) => a - b);
|
|
20
|
+
const stat = fs.statSync(indexFile);
|
|
21
|
+
const firstTurn = turns.length > 0 ? turns[0] : null;
|
|
22
|
+
const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
23
|
+
const keywordsPath = keywordsFile ?? defaultSpecialKeywordsPath(home);
|
|
24
|
+
let specialKeywords;
|
|
25
|
+
if (fs.existsSync(keywordsPath)) {
|
|
26
|
+
const raw = fs.readFileSync(keywordsPath, 'utf8');
|
|
27
|
+
const hasWords = raw.split(/\r?\n/).some((line) => {
|
|
28
|
+
const text = line.trim();
|
|
29
|
+
return text.length > 0 && !text.startsWith('#');
|
|
30
|
+
});
|
|
31
|
+
specialKeywords = hasWords ? loadSpecialKeywords(keywordsPath) : [];
|
|
32
|
+
} else {
|
|
33
|
+
specialKeywords = keywordsFile ? [] : loadSpecialKeywords();
|
|
34
|
+
}
|
|
35
|
+
const configuredALabels = loadConcepts(conceptsFile ?? path.join(home, '.dsh', 'continuity', 'concepts.md'));
|
|
36
|
+
const knownConcepts = new Set(configuredALabels.map((row) => row.label));
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
const kCounts = new Map();
|
|
39
|
+
const cCounts = new Map();
|
|
40
|
+
const cFileEntries = loadCMapEntries(cmapFile ?? defaultCMapPath(home));
|
|
41
|
+
const cMeta = new Map();
|
|
42
|
+
for (const entry of DEFAULT_C_SEED) {
|
|
43
|
+
cMeta.set(entry.term, { generic: entry.generic, keywords: [], minB: 0, summary: '' });
|
|
44
|
+
}
|
|
45
|
+
for (const entry of cFileEntries) cMeta.set(entry.term, entry);
|
|
46
|
+
const cTerms = [...cMeta.keys()];
|
|
47
|
+
let weak = 0;
|
|
48
|
+
for (const record of records) {
|
|
49
|
+
if (record.status === 'weak') weak += 1;
|
|
50
|
+
for (const concept of record.generic ?? []) {
|
|
51
|
+
if (knownConcepts.has(concept)) counts.set(concept, (counts.get(concept) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
for (const term of cTerms) {
|
|
54
|
+
if ((record.terms ?? []).includes(term) || (record.keywords ?? []).includes(term)) {
|
|
55
|
+
cCounts.set(term, (cCounts.get(term) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const words = new Set((record.keywords ?? []).map((word) => String(word).toLowerCase()));
|
|
59
|
+
for (const keyword of specialKeywords) {
|
|
60
|
+
if (words.has(keyword.toLowerCase())) kCounts.set(keyword, (kCounts.get(keyword) ?? 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Configured C entries remain visible even before their first hit.
|
|
64
|
+
for (const entry of cFileEntries) {
|
|
65
|
+
if (!cCounts.has(entry.term)) cCounts.set(entry.term, 0);
|
|
66
|
+
}
|
|
67
|
+
const labels = [...counts.entries()]
|
|
68
|
+
.map(([label, turns]) => ({ label, turns }))
|
|
69
|
+
.sort((a, b) => b.turns - a.turns || a.label.localeCompare(b.label));
|
|
70
|
+
const aLabels = configuredALabels.map((row) => ({
|
|
71
|
+
...row,
|
|
72
|
+
turns: counts.get(row.label) ?? 0,
|
|
73
|
+
}));
|
|
74
|
+
const kLabels = [...kCounts.entries()]
|
|
75
|
+
.map(([label, turns]) => ({ label, turns }))
|
|
76
|
+
.sort((a, b) => b.turns - a.turns || a.label.localeCompare(b.label));
|
|
77
|
+
const cLabels = [...cCounts.entries()]
|
|
78
|
+
.map(([label, turns]) => {
|
|
79
|
+
const meta = cMeta.get(label) ?? {};
|
|
80
|
+
return {
|
|
81
|
+
label,
|
|
82
|
+
turns,
|
|
83
|
+
concepts: meta.generic ?? [],
|
|
84
|
+
keywords: meta.keywords ?? [],
|
|
85
|
+
minB: meta.minB ?? 0,
|
|
86
|
+
limit: meta.limit ?? 20,
|
|
87
|
+
summary: meta.summary ?? '',
|
|
88
|
+
};
|
|
89
|
+
})
|
|
90
|
+
.sort((a, b) => b.turns - a.turns || a.label.localeCompare(b.label));
|
|
91
|
+
return {
|
|
92
|
+
available: true,
|
|
93
|
+
file: indexFile,
|
|
94
|
+
totalTurns: records.length,
|
|
95
|
+
firstTurn,
|
|
96
|
+
lastTurn,
|
|
97
|
+
updatedAt: stat.mtime.toISOString(),
|
|
98
|
+
weakTurns: weak,
|
|
99
|
+
labels,
|
|
100
|
+
kLabels,
|
|
101
|
+
cLabels,
|
|
102
|
+
aLabels,
|
|
103
|
+
aTopics: aLabels.filter((row) => row.kind !== 'workflow'),
|
|
104
|
+
aWorkflow: aLabels.filter((row) => row.kind === 'workflow'),
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B+C pre-step note injection.
|
|
3
|
+
*
|
|
4
|
+
* Pure: no LLM, no network, no writes. The caller owns C and the record store.
|
|
5
|
+
*/
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { contextMessage } from '../threads.mjs';
|
|
9
|
+
import { lastUserText } from './extraction-pre-step.mjs';
|
|
10
|
+
import { createCMap, DEFAULT_C_SEED, formatIntervals, groupHits, markAsked, renderNote, retrieve } from './bc-recall.mjs';
|
|
11
|
+
import { detectRecallCue } from './recall-cue.mjs';
|
|
12
|
+
import { loadIndexJsonl } from './bc-index.mjs';
|
|
13
|
+
import { defaultBCStorePath, loadBCStore } from './bc-store.mjs';
|
|
14
|
+
import { loadSpecialKeywords, matchSpecialKeywords } from './special-keywords.mjs';
|
|
15
|
+
import { loadCMapEntries } from './cmap-file.mjs';
|
|
16
|
+
import { loadConcepts } from './concepts-file.mjs';
|
|
17
|
+
import { appendCostLog, createIndexUpdater, defaultCostLogPath } from './bc-index-updater.mjs';
|
|
18
|
+
import { createBLabeler } from './bc-label.mjs';
|
|
19
|
+
import { runRelabelMigration } from './relabel-migrate.mjs';
|
|
20
|
+
import { consumePendingHint, maybeRunAudit } from './bc-audit.mjs';
|
|
21
|
+
import { ensureSessionScaffolds, maybeOnboard, bootstrapPaths } from './onboarding.mjs';
|
|
22
|
+
import { isPersonaEnabled, resolvePersona, resolveVocabFile, sessionFile } from './persona.mjs';
|
|
23
|
+
import { embedTexts } from '../facets/embedding.mjs';
|
|
24
|
+
import { eventText } from '../session-log.mjs';
|
|
25
|
+
|
|
26
|
+
const INJECTED = /^\[(?:抽取|工作线|标签|规则|习惯|技能|归位|候选|存\?|闸|回正|学习|追忆|记忆|continuity:[a-z-]+)\]/;
|
|
27
|
+
const MACHINE_PREFIX = /^(?:Current runtime context|Current working directory|Current DSH file policy|Approval prompts|Time sampled)/;
|
|
28
|
+
|
|
29
|
+
function liveNote(events, query, { beforeTurn = Infinity } = {}) {
|
|
30
|
+
const hits = (events ?? []).filter((event) => {
|
|
31
|
+
const source = event?.data?.source ?? event?.source;
|
|
32
|
+
if (source && typeof source.kind === 'string' && source.kind !== 'user') return false;
|
|
33
|
+
if (source && source.peer === true) return false;
|
|
34
|
+
const text = eventText(event).trim();
|
|
35
|
+
if (!text || INJECTED.test(text) || MACHINE_PREFIX.test(text) || !text.includes(query)) return false;
|
|
36
|
+
const turn = Number(event?.data?.turn ?? event?.turn);
|
|
37
|
+
return !Number.isFinite(turn) || turn < beforeTurn;
|
|
38
|
+
}).sort((a, b) => Number(b.seq) - Number(a.seq)).map((event) => {
|
|
39
|
+
const turn = Number(event?.data?.turn ?? event?.turn);
|
|
40
|
+
const seq = Number(event?.seq);
|
|
41
|
+
return { turn: Number.isFinite(turn) ? turn : null, seqStart: seq, seqEnd: seq, status: 'live', generic: [] };
|
|
42
|
+
});
|
|
43
|
+
if (hits.length === 0) return null;
|
|
44
|
+
const groups = groupHits(hits);
|
|
45
|
+
const line = (group) => group.fromTurn === null
|
|
46
|
+
? `seq ${group.fromSeq}-${group.toSeq}`
|
|
47
|
+
: group.fromTurn === group.toTurn
|
|
48
|
+
? `turn ${group.fromTurn} / seq ${group.fromSeq}-${group.toSeq}`
|
|
49
|
+
: `turn ${group.fromTurn}-${group.toTurn} / seq ${group.fromSeq}-${group.toSeq}`;
|
|
50
|
+
const note = groups.length === 1
|
|
51
|
+
? `[追忆] 最近 ${query} -> ${line(groups[0])}`
|
|
52
|
+
: `[追忆] 最近 ${query} -> ${groups.length} 段\n${formatIntervals(groups)}`;
|
|
53
|
+
return {
|
|
54
|
+
result: { kind: 'recall', via: 'live', term: query, hits },
|
|
55
|
+
note: `${note}\n如需上下文,可按需前后读更多 turn。`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function autoSpecialKeywords(words) {
|
|
60
|
+
// One-character Chinese names/emoji are too common as vocatives ("创看看").
|
|
61
|
+
// They remain available through manual recall(), but do not auto-trigger.
|
|
62
|
+
return words.filter((word) => [...String(word)].length >= 2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderAnchorNote(result, anchor) {
|
|
66
|
+
if (!result || result.kind !== 'recall' || !Array.isArray(result.hits) || result.hits.length === 0) return null;
|
|
67
|
+
const groups = groupHits(result.hits);
|
|
68
|
+
if (groups.length === 0) return null;
|
|
69
|
+
const weak = groups.some((group) => group.weak) ? ' · weak' : '';
|
|
70
|
+
const header = groups.length === 1
|
|
71
|
+
? `[追忆] ${anchor.term}${weak} · ${groups[0].fromTurn === null ? `seq ${groups[0].fromSeq}-${groups[0].toSeq}` : `turn ${groups[0].fromTurn}${groups[0].fromTurn === groups[0].toTurn ? '' : '-' + groups[0].toTurn} / seq ${groups[0].fromSeq}-${groups[0].toSeq}`}`
|
|
72
|
+
: `[追忆] ${anchor.term}${weak} · 显示 ${Math.min(3, groups.length)} / 共 ${groups.length} 个区间`;
|
|
73
|
+
const lines = [header];
|
|
74
|
+
if (groups.length > 1) lines.push(formatIntervals(groups, { max: 3, showRemainder: false }));
|
|
75
|
+
lines.push(`更多用 recall("${anchor.term}")`);
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function logIndexError(error) {
|
|
80
|
+
try {
|
|
81
|
+
const home = process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
82
|
+
appendFileSync(join(home, '.dsh', 'continuity', 'gate-errors.jsonl'), JSON.stringify({
|
|
83
|
+
at: new Date().toISOString(), code: 'bc-index-update-failed',
|
|
84
|
+
name: String(error?.name ?? 'Error'),
|
|
85
|
+
message: String(error?.message ?? error),
|
|
86
|
+
}) + '\n', 'utf8');
|
|
87
|
+
} catch { /* error logging must not block the turn */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function logIndexUnavailable(reason, store = null) {
|
|
91
|
+
try {
|
|
92
|
+
const home = process.env.USERPROFILE ?? process.env.HOME ?? '';
|
|
93
|
+
appendFileSync(join(home, '.dsh', 'continuity', 'gate-errors.jsonl'), JSON.stringify({
|
|
94
|
+
at: new Date().toISOString(), code: 'bc-index-unavailable', reason,
|
|
95
|
+
store, pid: process.pid,
|
|
96
|
+
}) + '\n', 'utf8');
|
|
97
|
+
} catch { /* error logging must not block the turn */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function createBCPreStep({ records = [], cmap, prefix = '', sessionQuery = null, cmapFile = undefined, keywordsFile = null, updateIndex = null, bLabeler = null, auditIndexFile = null, auditConceptsFile = null } = {}) {
|
|
101
|
+
let repairTimer = null;
|
|
102
|
+
let activeBLabeler = bLabeler;
|
|
103
|
+
const scheduleRepair = (agent, lastTurn) => {
|
|
104
|
+
if (repairTimer !== null || updateIndex === null) return;
|
|
105
|
+
repairTimer = setTimeout(async () => {
|
|
106
|
+
repairTimer = null;
|
|
107
|
+
const update = await updateIndex({ agent, lastTurn });
|
|
108
|
+
if (update?.records?.length) records.push(...update.records);
|
|
109
|
+
if (update?.remaining > 0) scheduleRepair(agent, lastTurn);
|
|
110
|
+
}, 250);
|
|
111
|
+
repairTimer.unref?.();
|
|
112
|
+
};
|
|
113
|
+
return async function bcPreStep(payload, next) {
|
|
114
|
+
const decision = await next();
|
|
115
|
+
if (!decision || !Array.isArray(decision.messages)) return decision;
|
|
116
|
+
if (Number.isInteger(payload?.step) && payload.step !== 1) return decision;
|
|
117
|
+
if (updateIndex !== null) {
|
|
118
|
+
const update = await updateIndex({ agent: payload?.agent, lastTurn: payload?.agent?.lastTurn });
|
|
119
|
+
if (update?.records?.length) records.push(...update.records);
|
|
120
|
+
if (update?.remaining > 0) scheduleRepair(payload?.agent, payload?.agent?.lastTurn);
|
|
121
|
+
if (auditIndexFile !== null && auditConceptsFile !== null && loadConcepts(auditConceptsFile).length > 0) {
|
|
122
|
+
const lastTurn = update?.records?.at(-1)?.turn ?? payload?.agent?.lastTurn;
|
|
123
|
+
try { maybeRunAudit({ indexFile: auditIndexFile, conceptsFile: auditConceptsFile, lastTurn }); } catch { /* audit must not block */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let auditNote = null;
|
|
127
|
+
if (auditIndexFile !== null) {
|
|
128
|
+
try { auditNote = consumePendingHint(auditIndexFile); } catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
maybeOnboard({ session: payload?.agent?.session, messages: decision.messages });
|
|
132
|
+
} catch { /* onboarding must not block */ }
|
|
133
|
+
let auditInjected = false;
|
|
134
|
+
if (auditNote) {
|
|
135
|
+
const auditText = prefix ? `${prefix}\n${auditNote}` : auditNote;
|
|
136
|
+
const session = payload?.agent?.session;
|
|
137
|
+
if (session && typeof session.append === 'function') {
|
|
138
|
+
try {
|
|
139
|
+
session.append('user/message', contextMessage(auditText), { surfaceOp: 'append' });
|
|
140
|
+
auditInjected = true;
|
|
141
|
+
} catch { /* fall back to decision messages */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const withAudit = (value) => auditInjected
|
|
145
|
+
? value
|
|
146
|
+
: (auditNote ? { ...value, messages: [...value.messages, contextMessage(prefix ? `${prefix}\n${auditNote}` : auditNote)] } : value);
|
|
147
|
+
const before = withAudit(decision);
|
|
148
|
+
const text = lastUserText(before.messages);
|
|
149
|
+
if (!text) return before;
|
|
150
|
+
const cue = detectRecallCue(text);
|
|
151
|
+
if (!cue.hit) return before;
|
|
152
|
+
if (cmap?.terms && cmapFile !== null) {
|
|
153
|
+
for (const entry of loadCMapEntries(cmapFile)) cmap.terms.set(entry.term, entry.generic);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const pronoun = /^(?:那个|那件事|这件事|这个|它)$/;
|
|
157
|
+
const specialWords = keywordsFile && !existsSync(keywordsFile) ? [] : loadSpecialKeywords(keywordsFile ?? undefined);
|
|
158
|
+
const kAnchors = autoSpecialKeywords(matchSpecialKeywords(text, specialWords))
|
|
159
|
+
.filter((word) => !pronoun.test(word))
|
|
160
|
+
.map((word) => ({ term: word, type: 'K', score: 1 }));
|
|
161
|
+
let anchor = kAnchors[0] ?? null;
|
|
162
|
+
if (anchor === null && cmap?.terms) {
|
|
163
|
+
const cTerms = [...cmap.terms.keys()]
|
|
164
|
+
.filter((term) => term.length > 0 && text.includes(term) && !pronoun.test(term))
|
|
165
|
+
.sort((a, b) => b.length - a.length);
|
|
166
|
+
if (cTerms.length > 0) anchor = { term: cTerms[0], type: 'C', score: 0.9 };
|
|
167
|
+
}
|
|
168
|
+
if (anchor === null) {
|
|
169
|
+
if (activeBLabeler === null) activeBLabeler = createBLabeler({ embed: embedTexts });
|
|
170
|
+
const label = await activeBLabeler.label(text, { topK: 3, minScore: 0.5 });
|
|
171
|
+
const top = label.candidates?.[0] ?? null;
|
|
172
|
+
if (top && top.score >= 0.5 && text.includes(top.concept)) anchor = { term: top.concept, type: 'B', score: top.score };
|
|
173
|
+
}
|
|
174
|
+
if (anchor === null) return withAudit({ ...decision, cue });
|
|
175
|
+
|
|
176
|
+
const result = retrieve({
|
|
177
|
+
records,
|
|
178
|
+
cmap,
|
|
179
|
+
query: anchor.term,
|
|
180
|
+
specialKeywords: anchor.type === 'K' ? [anchor.term] : [],
|
|
181
|
+
});
|
|
182
|
+
const note = renderAnchorNote(result, anchor);
|
|
183
|
+
if (!note) return withAudit({ ...decision, bc: result, cue, anchor });
|
|
184
|
+
if (result.kind === 'ask') markAsked(cmap, result.term);
|
|
185
|
+
return {
|
|
186
|
+
...before,
|
|
187
|
+
messages: [...before.messages, contextMessage(prefix ? `${prefix}\n${note}` : note)],
|
|
188
|
+
bc: result,
|
|
189
|
+
cue,
|
|
190
|
+
anchor,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function installBCPreStep(ctx, { enabled = false, storePath = null, prefix = '', sessionQuery = null, sessionScoped = null, relabelRunner = runRelabelMigration } = {}) {
|
|
196
|
+
if (enabled !== true) return { registered: false, reason: 'disabled' };
|
|
197
|
+
if (!ctx || typeof ctx.on !== 'function') return { registered: false, reason: 'no-ctx' };
|
|
198
|
+
const resolvedStore = storePath ?? defaultBCStorePath();
|
|
199
|
+
const isJsonl = /\.jsonl$/i.test(resolvedStore);
|
|
200
|
+
const scoped = sessionScoped === null ? (storePath === null && isJsonl) : sessionScoped === true;
|
|
201
|
+
const sessionsDir = join(dirname(resolvedStore), 'sessions');
|
|
202
|
+
// 迁移互斥用的共享 gate:迁移期间(本 session 在 relabelling 里,或本进程持有文件锁)
|
|
203
|
+
// updater 一律不写盘、不推进 state;被挡的行进 pending 队列(updater 内部)。
|
|
204
|
+
const relabelGate = {
|
|
205
|
+
relabelling: new Set(),
|
|
206
|
+
lockOwned: false,
|
|
207
|
+
isBlocked(sessionId) {
|
|
208
|
+
return this.lockOwned === true || (sessionId !== undefined && this.relabelling.has(sessionId));
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const resolveSessionQuery = () => {
|
|
212
|
+
if (sessionQuery && typeof sessionQuery.readSession === 'function') return sessionQuery;
|
|
213
|
+
if (typeof ctx.get === 'function') return ctx.get('sessionQuery');
|
|
214
|
+
return ctx.sessionQuery;
|
|
215
|
+
};
|
|
216
|
+
const lazySessionQuery = {
|
|
217
|
+
async readSession(sessionId) {
|
|
218
|
+
const query = resolveSessionQuery();
|
|
219
|
+
if (!query || typeof query.readSession !== 'function') {
|
|
220
|
+
throw new Error('sessionQuery service unavailable');
|
|
221
|
+
}
|
|
222
|
+
return query.readSession(sessionId);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const conceptsPath = resolveVocabFile('continuity', 'concepts.md');
|
|
226
|
+
const cmapPath = resolveVocabFile('continuity', 'cmap.md');
|
|
227
|
+
const keywordsPath = resolveVocabFile('continuity', 'keywords.md');
|
|
228
|
+
const conceptRows = loadConcepts(conceptsPath);
|
|
229
|
+
const noBootstrap = !existsSync(bootstrapPaths().stateFile);
|
|
230
|
+
const initialLabels = conceptRows.length > 0
|
|
231
|
+
? Object.fromEntries(conceptRows.map((row) => [row.label, row.description]))
|
|
232
|
+
: (existsSync(conceptsPath) || noBootstrap ? {} : null);
|
|
233
|
+
const initialKinds = Object.fromEntries(conceptRows.map((row) => [row.label, row.kind]));
|
|
234
|
+
const initialLabeler = initialLabels === null
|
|
235
|
+
? null
|
|
236
|
+
: createBLabeler({ embed: embedTexts, labels: initialLabels, concepts: Object.keys(initialLabels), kinds: initialKinds });
|
|
237
|
+
const sessionVocabStamp = (sessionId) => {
|
|
238
|
+
const stampOf = (file) => {
|
|
239
|
+
try {
|
|
240
|
+
const stat = statSync(file);
|
|
241
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
242
|
+
} catch {
|
|
243
|
+
return 'missing';
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
return `${stampOf(sessionFile(sessionId, 'concepts.md'))}|${stampOf(sessionFile(sessionId, 'cmap.md'))}`;
|
|
247
|
+
};
|
|
248
|
+
const makeStep = (indexFile, sessionId) => {
|
|
249
|
+
const records = existsSync(indexFile) ? loadIndexJsonl(indexFile) : [];
|
|
250
|
+
const conceptsPath = sessionFile(sessionId, 'concepts.md');
|
|
251
|
+
const keywordsPath = sessionFile(sessionId, 'keywords.md');
|
|
252
|
+
const cmapPath = sessionFile(sessionId, 'cmap.md');
|
|
253
|
+
const stampOf = (file) => {
|
|
254
|
+
try {
|
|
255
|
+
const stat = statSync(file);
|
|
256
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
257
|
+
} catch {
|
|
258
|
+
return 'missing';
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const vocabStamp = `${stampOf(conceptsPath)}|${stampOf(cmapPath)}`;
|
|
262
|
+
const conceptRows = loadConcepts(conceptsPath);
|
|
263
|
+
const labels = conceptRows.length > 0
|
|
264
|
+
? Object.fromEntries(conceptRows.map((row) => [row.label, row.description]))
|
|
265
|
+
: {};
|
|
266
|
+
const kinds = Object.fromEntries(conceptRows.map((row) => [row.label, row.kind]));
|
|
267
|
+
const labeler = createBLabeler({ embed: embedTexts, labels, concepts: Object.keys(labels), kinds });
|
|
268
|
+
const cmap = createCMap([...DEFAULT_C_SEED, ...loadCMapEntries(cmapPath)]);
|
|
269
|
+
const updateIndex = createIndexUpdater({
|
|
270
|
+
file: indexFile,
|
|
271
|
+
stateFile: join(dirname(indexFile), 'bc-index-state.json'),
|
|
272
|
+
sessionQuery: lazySessionQuery,
|
|
273
|
+
labels,
|
|
274
|
+
kinds,
|
|
275
|
+
keywordsFile: keywordsPath,
|
|
276
|
+
cmapFile: cmapPath,
|
|
277
|
+
conceptsFile: conceptsPath,
|
|
278
|
+
gate: relabelGate,
|
|
279
|
+
onError: logIndexError,
|
|
280
|
+
onCost: (cost) => appendCostLog(defaultCostLogPath(), cost),
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
records,
|
|
284
|
+
cmap,
|
|
285
|
+
vocabStamp,
|
|
286
|
+
step: createBCPreStep({ records, cmap, prefix, sessionQuery: lazySessionQuery, updateIndex, bLabeler: labeler, cmapFile: cmapPath, keywordsFile: keywordsPath, auditIndexFile: indexFile, auditConceptsFile: conceptsPath }),
|
|
287
|
+
updateIndex,
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (scoped) {
|
|
292
|
+
const bySession = new Map();
|
|
293
|
+
// 迁移期间的 session 用这个哨兵返回:不建 entry、不写盘
|
|
294
|
+
const SKIP_DURING_RELABEL = Symbol('skip-during-relabel');
|
|
295
|
+
// 每个进程只自动尝试一次;abort 后不再反复重试(否则每轮都 abort 刷日志),
|
|
296
|
+
// 失败后本进程恢复普通索引,marker 留着等下次启动或人工处理。
|
|
297
|
+
const relabelAttempted = new Set();
|
|
298
|
+
|
|
299
|
+
// session 词表才是唯一来源(与 makeStep 同口径);不能用 persona/global 回退,
|
|
300
|
+
// 否则迁移会拿另一份 concepts.md 去算 hash 与打标(实际踩过:hash 不符直接 abort)。
|
|
301
|
+
const relabelConceptsFile = (sessionId) => sessionFile(sessionId, 'concepts.md');
|
|
302
|
+
|
|
303
|
+
const buildRelabelLabeler = (sessionId) => {
|
|
304
|
+
const conceptRows = loadConcepts(relabelConceptsFile(sessionId));
|
|
305
|
+
const labels = Object.fromEntries(conceptRows.map((row) => [row.label, row.description]));
|
|
306
|
+
const kinds = Object.fromEntries(conceptRows.map((row) => [row.label, row.kind]));
|
|
307
|
+
return createBLabeler({ embed: embedTexts, labels, concepts: Object.keys(labels), kinds });
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 后台启动一次历史重标。不 await、不阻塞 pre-step;
|
|
312
|
+
* 期间该 session 在 gate 里 → updater 不写盘、pre-step 直接 next()。
|
|
313
|
+
* 结束后重建 entry(从磁盘重读 records),updater 靠 stateFile 的 afterSeq 自动补齐。
|
|
314
|
+
*
|
|
315
|
+
* 所有结果/异常都落一行到 `<sessionDir>/relabel-log.jsonl`:
|
|
316
|
+
* 迁移可能发生在没有任何终端可见的地方,"失败必须留痕"是硬要求。
|
|
317
|
+
*/
|
|
318
|
+
const appendRelabelLog = (sessionId, entry) => {
|
|
319
|
+
try {
|
|
320
|
+
const dir = join(sessionsDir, String(sessionId));
|
|
321
|
+
mkdirSync(dir, { recursive: true });
|
|
322
|
+
appendFileSync(join(dir, 'relabel-log.jsonl'), JSON.stringify({
|
|
323
|
+
at: new Date().toISOString(),
|
|
324
|
+
sessionId,
|
|
325
|
+
...entry,
|
|
326
|
+
}) + '\n', 'utf8');
|
|
327
|
+
} catch { /* 日志失败不能影响主流程 */ }
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const startRelabel = (sessionId, indexFile, markerFile) => {
|
|
331
|
+
relabelGate.relabelling.add(sessionId);
|
|
332
|
+
const startedAt = new Date().toISOString();
|
|
333
|
+
appendRelabelLog(sessionId, { event: 'start', markerFile, indexFile });
|
|
334
|
+
console.log(`[dsh-continuity][relabel] 检测到 marker,后台开始(session ${sessionId})`);
|
|
335
|
+
// 失败写回 marker:没有终端可看时,下一次启动能从 marker 直接读到原因
|
|
336
|
+
const noteMarkerFailure = (reason, detail = '') => {
|
|
337
|
+
try {
|
|
338
|
+
const marker = JSON.parse(readFileSync(markerFile, 'utf8').replace(/^\uFEFF/, ''));
|
|
339
|
+
const next = {
|
|
340
|
+
...marker,
|
|
341
|
+
attempts: (Number(marker.attempts) || 0) + 1,
|
|
342
|
+
lastError: { at: new Date().toISOString(), reason, detail: String(detail).slice(0, 500) },
|
|
343
|
+
};
|
|
344
|
+
writeFileSync(markerFile, JSON.stringify(next, null, 2) + '\n', 'utf8');
|
|
345
|
+
} catch { /* marker 写不进去也不能影响主流程 */ }
|
|
346
|
+
};
|
|
347
|
+
void (async () => {
|
|
348
|
+
try {
|
|
349
|
+
const result = await relabelRunner({
|
|
350
|
+
indexFile,
|
|
351
|
+
events: async () => {
|
|
352
|
+
const snapshot = await lazySessionQuery.readSession(sessionId);
|
|
353
|
+
return Array.isArray(snapshot?.events) ? snapshot.events : [];
|
|
354
|
+
},
|
|
355
|
+
conceptsFile: relabelConceptsFile(sessionId),
|
|
356
|
+
markerFile,
|
|
357
|
+
labeler: buildRelabelLabeler(sessionId),
|
|
358
|
+
eventText,
|
|
359
|
+
log: (message) => {
|
|
360
|
+
console.log(`[dsh-continuity][relabel] ${message}`);
|
|
361
|
+
appendRelabelLog(sessionId, { event: 'log', message: String(message) });
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
console.log(`[dsh-continuity][relabel] 结束:${result.status}${result.reason ? ' / ' + result.reason : ''}`);
|
|
365
|
+
if (result.status === 'abort' || result.status === 'skip') {
|
|
366
|
+
noteMarkerFailure(result.status + (result.reason ? ':' + result.reason : ''), JSON.stringify(result.unresolved ?? result.stats ?? {}));
|
|
367
|
+
}
|
|
368
|
+
appendRelabelLog(sessionId, {
|
|
369
|
+
event: 'result',
|
|
370
|
+
status: result.status,
|
|
371
|
+
reason: result.reason ?? null,
|
|
372
|
+
unresolved: result.unresolved ?? null,
|
|
373
|
+
stats: result.stats ?? null,
|
|
374
|
+
startedAt,
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error(`[dsh-continuity][relabel] 失败:${error?.message ?? error}`);
|
|
378
|
+
appendRelabelLog(sessionId, {
|
|
379
|
+
event: 'error',
|
|
380
|
+
message: String(error?.message ?? error),
|
|
381
|
+
stack: String(error?.stack ?? '').split('\n').slice(0, 5).join(' | '),
|
|
382
|
+
startedAt,
|
|
383
|
+
});
|
|
384
|
+
noteMarkerFailure('error', String(error?.message ?? error));
|
|
385
|
+
} finally {
|
|
386
|
+
relabelGate.relabelling.delete(sessionId);
|
|
387
|
+
bySession.delete(sessionId);
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const getSession = (sessionId) => {
|
|
393
|
+
if (!sessionId) return null;
|
|
394
|
+
const personaId = resolvePersona(sessionId);
|
|
395
|
+
if (!isPersonaEnabled(personaId)) return null;
|
|
396
|
+
ensureSessionScaffolds({ sessionId });
|
|
397
|
+
const vocabStamp = sessionVocabStamp(sessionId);
|
|
398
|
+
let entry = bySession.get(sessionId);
|
|
399
|
+
if (entry !== undefined && entry.vocabStamp !== vocabStamp) {
|
|
400
|
+
bySession.delete(sessionId);
|
|
401
|
+
entry = undefined;
|
|
402
|
+
}
|
|
403
|
+
if (entry === undefined) {
|
|
404
|
+
const indexFile = join(sessionsDir, String(sessionId), 'bc-index.jsonl');
|
|
405
|
+
const markerFile = join(dirname(indexFile), 'bc-index.relabel-pending.json');
|
|
406
|
+
// 迁移只在"显式 marker"存在时发生;升级本身绝不触发。
|
|
407
|
+
// 每进程只尝试一次;abort 后本进程恢复普通索引(marker 保留待人工/下次启动)。
|
|
408
|
+
if (existsSync(markerFile) && !relabelAttempted.has(sessionId)) {
|
|
409
|
+
relabelAttempted.add(sessionId);
|
|
410
|
+
startRelabel(sessionId, indexFile, markerFile);
|
|
411
|
+
}
|
|
412
|
+
if (relabelGate.relabelling.has(sessionId)) return SKIP_DURING_RELABEL;
|
|
413
|
+
entry = makeStep(indexFile, sessionId);
|
|
414
|
+
bySession.set(sessionId, entry);
|
|
415
|
+
}
|
|
416
|
+
return entry;
|
|
417
|
+
};
|
|
418
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
419
|
+
const entry = getSession(payload?.agent?.session?.id);
|
|
420
|
+
if (entry === null || entry === SKIP_DURING_RELABEL) return next();
|
|
421
|
+
return entry.step(payload, next);
|
|
422
|
+
});
|
|
423
|
+
return {
|
|
424
|
+
registered: true,
|
|
425
|
+
records: 0,
|
|
426
|
+
cmapTerms: 0,
|
|
427
|
+
indexUpdater: true,
|
|
428
|
+
sessionQuery: 'lazy',
|
|
429
|
+
store: sessionsDir,
|
|
430
|
+
sessionScoped: true,
|
|
431
|
+
relabel: { enabled: true },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const store = isJsonl
|
|
436
|
+
? { records: loadIndexJsonl(resolvedStore), cmap: createCMap([...DEFAULT_C_SEED, ...loadCMapEntries(cmapPath)]) }
|
|
437
|
+
: loadBCStore(resolvedStore);
|
|
438
|
+
const updateIndex = isJsonl ? createIndexUpdater({
|
|
439
|
+
file: resolvedStore,
|
|
440
|
+
stateFile: join(dirname(resolvedStore), 'bc-index-state.json'),
|
|
441
|
+
sessionQuery: lazySessionQuery,
|
|
442
|
+
labels: initialLabels,
|
|
443
|
+
kinds: initialKinds,
|
|
444
|
+
keywordsFile: keywordsPath,
|
|
445
|
+
cmapFile: cmapPath,
|
|
446
|
+
conceptsFile: conceptsPath,
|
|
447
|
+
gate: relabelGate,
|
|
448
|
+
onError: logIndexError,
|
|
449
|
+
onCost: (cost) => appendCostLog(defaultCostLogPath(), cost),
|
|
450
|
+
}) : null;
|
|
451
|
+
const isTempProbe = /[\\/]Temp[\\/]bc-install-[^\\/]+[\\/]/i.test(resolvedStore);
|
|
452
|
+
if (updateIndex === null && !isTempProbe) logIndexUnavailable('store is not bc-index.jsonl', resolvedStore);
|
|
453
|
+
ctx.on('agent/pre-step', createBCPreStep({ records: store.records, cmap: store.cmap, prefix, sessionQuery: lazySessionQuery, updateIndex, bLabeler: initialLabeler, cmapFile: cmapPath, keywordsFile: keywordsPath, auditIndexFile: isJsonl ? resolvedStore : null, auditConceptsFile: conceptsPath }));
|
|
454
|
+
return {
|
|
455
|
+
registered: true,
|
|
456
|
+
records: store.records.length,
|
|
457
|
+
cmapTerms: store.cmap.terms.size,
|
|
458
|
+
indexUpdater: Boolean(updateIndex),
|
|
459
|
+
sessionQuery: 'lazy',
|
|
460
|
+
store: resolvedStore,
|
|
461
|
+
sessionScoped: false,
|
|
462
|
+
};
|
|
463
|
+
}
|