@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
package/src/backfill.mjs
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historical backfill: select content events from the raw session snapshot,
|
|
3
|
+
* trim incomplete tool pairs at the edges, and build an archival segment.
|
|
4
|
+
* It never touches the live surface.
|
|
5
|
+
*/
|
|
6
|
+
import { estimateTextTokens, quoteEventText } from './threads.mjs';
|
|
7
|
+
|
|
8
|
+
export const BACKFILL_TYPES = new Set(['user/message', 'assistant/message', 'tool/call', 'tool/result']);
|
|
9
|
+
|
|
10
|
+
export function selectBackfillEvents(events, fromSeq, toSeq) {
|
|
11
|
+
const bySeq = new Map();
|
|
12
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
13
|
+
if (!BACKFILL_TYPES.has(event?.type)) continue;
|
|
14
|
+
const seq = Number(event?.seq);
|
|
15
|
+
if (!Number.isInteger(seq) || seq < fromSeq || seq > toSeq) continue;
|
|
16
|
+
bySeq.set(seq, event);
|
|
17
|
+
}
|
|
18
|
+
return [...bySeq.values()].sort((left, right) => Number(left.seq) - Number(right.seq));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Drop a leading result or a trailing call; the live hole boundaries are complete. */
|
|
22
|
+
export function trimBackfillEvents(events) {
|
|
23
|
+
const list = Array.isArray(events) ? [...events] : [];
|
|
24
|
+
while (list.length > 0 && list[0]?.type === 'tool/result') list.shift();
|
|
25
|
+
while (list.length > 0 && list[list.length - 1]?.type === 'tool/call') list.pop();
|
|
26
|
+
return list;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildBackfillSegment(events, {
|
|
30
|
+
sessionId,
|
|
31
|
+
why,
|
|
32
|
+
next,
|
|
33
|
+
residue,
|
|
34
|
+
tCoverage = 'T0',
|
|
35
|
+
now = new Date().toISOString(),
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (!Array.isArray(events) || events.length === 0) return null;
|
|
38
|
+
const fromSeq = Number(events[0].seq);
|
|
39
|
+
const toSeq = Number(events[events.length - 1].seq);
|
|
40
|
+
let tokens = 0;
|
|
41
|
+
for (const event of events) tokens += estimateTextTokens(quoteEventText(event));
|
|
42
|
+
const firstTime = Number(events[0]?.time);
|
|
43
|
+
const lastTime = Number(events[events.length - 1]?.time);
|
|
44
|
+
const spanStart = Number.isFinite(firstTime) ? new Date(firstTime).toISOString() : now;
|
|
45
|
+
const spanEnd = Number.isFinite(lastTime) ? new Date(lastTime).toISOString() : spanStart;
|
|
46
|
+
return {
|
|
47
|
+
fromSeq,
|
|
48
|
+
toSeq,
|
|
49
|
+
seqs: events.map((event) => Number(event.seq)),
|
|
50
|
+
count: events.length,
|
|
51
|
+
spanStart,
|
|
52
|
+
spanEnd,
|
|
53
|
+
minutes: Math.max(0, Number(((Date.parse(spanEnd) - Date.parse(spanStart)) / 60000).toFixed(1))),
|
|
54
|
+
tokens,
|
|
55
|
+
why,
|
|
56
|
+
next,
|
|
57
|
+
...(residue === undefined ? {} : { residue }),
|
|
58
|
+
// `at` is content recency (when the conversation happened), not when the
|
|
59
|
+
// backfill was written. Keep the insertion clock separately.
|
|
60
|
+
at: spanEnd,
|
|
61
|
+
archivedAt: now,
|
|
62
|
+
archiveRef: `log:${sessionId}#seq=${fromSeq}-${toSeq}`,
|
|
63
|
+
tCoverage,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function rangeOverlaps(existing, fromSeq, toSeq) {
|
|
68
|
+
return (Array.isArray(existing) ? existing : []).some((segment) => {
|
|
69
|
+
const a = Number(segment?.fromSeq);
|
|
70
|
+
const b = Number(segment?.toSeq);
|
|
71
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
|
|
72
|
+
return fromSeq <= b && toSeq >= a;
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/backup.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session 冷备份(本机复制)。供引擎每轮调用:距上次 ≥ intervalMs 才真正复制。
|
|
3
|
+
*
|
|
4
|
+
* 注意:会话文件正在被追加写,复制可能拿到残缺末帧——所以复制后校验帧,
|
|
5
|
+
* 末帧解不开只记警告,不删备份。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import zlib from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
12
|
+
|
|
13
|
+
export function sessionFilePath({ home, sid }) {
|
|
14
|
+
if (!home || !sid) return null;
|
|
15
|
+
const root = path.join(home, '.dsh', 'sessions');
|
|
16
|
+
if (!fs.existsSync(root)) return null;
|
|
17
|
+
for (const dir of fs.readdirSync(root)) {
|
|
18
|
+
const candidate = path.join(root, dir, sid, 'session.v4.jsonl.zstd');
|
|
19
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function frameOffsets(buf) {
|
|
25
|
+
const out = [];
|
|
26
|
+
let pos = -1;
|
|
27
|
+
while ((pos = buf.indexOf(MAGIC, pos + 1)) >= 0) out.push(pos);
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function verifySessionFile(file) {
|
|
32
|
+
const buf = fs.readFileSync(file);
|
|
33
|
+
const offsets = frameOffsets(buf);
|
|
34
|
+
if (offsets.length === 0) return { ok: false, reason: 'no-frame', bytes: buf.length };
|
|
35
|
+
let decoded = 0;
|
|
36
|
+
let textBytes = 0;
|
|
37
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
38
|
+
const start = offsets[i];
|
|
39
|
+
const end = i + 1 < offsets.length ? offsets[i + 1] : buf.length;
|
|
40
|
+
try {
|
|
41
|
+
textBytes += zlib.zstdDecompressSync(buf.subarray(start, end)).toString('utf8').length;
|
|
42
|
+
decoded += 1;
|
|
43
|
+
} catch { /* 末帧可能残缺 */ }
|
|
44
|
+
}
|
|
45
|
+
return { ok: decoded > 0, bytes: buf.length, frames: offsets.length, decodedFrames: decoded, textBytes };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @returns {{skipped:true, reason:string} | {skipped:false, backup:string, check:object, removed:string[]}}
|
|
50
|
+
*/
|
|
51
|
+
export function maybeBackupSession({ home, sid, intervalMs = 15 * 60 * 1000, keep = 2, force = false, now = Date.now() } = {}) {
|
|
52
|
+
const src = sessionFilePath({ home, sid });
|
|
53
|
+
if (src === null) return { skipped: true, reason: 'no-session-file' };
|
|
54
|
+
const stateFile = path.join(home, '.dsh', 'continuity', 'backup-state.json');
|
|
55
|
+
let state = { lastAt: 0 };
|
|
56
|
+
try { state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); } catch { state = { lastAt: 0 }; }
|
|
57
|
+
if (!force && now - Number(state.lastAt ?? 0) < intervalMs) {
|
|
58
|
+
return { skipped: true, reason: 'too-soon' };
|
|
59
|
+
}
|
|
60
|
+
const stamp = new Date(now).toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, '').replace('T', 'T');
|
|
61
|
+
const destDir = path.join(home, '.dsh', 'archives', stamp);
|
|
62
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
63
|
+
const backup = path.join(destDir, 'session-' + String(sid).replace(/^session-/, '') + '.jsonl.zstd');
|
|
64
|
+
fs.copyFileSync(src, backup);
|
|
65
|
+
const check = verifySessionFile(backup);
|
|
66
|
+
|
|
67
|
+
const root = path.join(home, '.dsh', 'archives');
|
|
68
|
+
const mine = fs.readdirSync(root)
|
|
69
|
+
.map((name) => ({ name, full: path.join(root, name) }))
|
|
70
|
+
.filter((entry) => fs.existsSync(path.join(entry.full, 'session-' + String(sid).replace(/^session-/, '') + '.jsonl.zstd')))
|
|
71
|
+
.sort((left, right) => (left.name < right.name ? 1 : -1));
|
|
72
|
+
const removed = [];
|
|
73
|
+
for (const entry of mine.slice(keep)) {
|
|
74
|
+
fs.rmSync(entry.full, { recursive: true, force: true });
|
|
75
|
+
removed.push(entry.name);
|
|
76
|
+
}
|
|
77
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
|
|
78
|
+
fs.writeFileSync(stateFile, JSON.stringify({ lastAt: now, lastFile: backup, check }, null, 2), 'utf8');
|
|
79
|
+
return { skipped: false, backup, check, removed };
|
|
80
|
+
}
|
package/src/deadend.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C7 死胡同症状检测(纯函数)。
|
|
3
|
+
*
|
|
4
|
+
* 两条可数症状 + 一条强制提问:
|
|
5
|
+
* 1. sameSpot :同一路径在窗口内被改 ≥ limit 次(“同处改三版”)
|
|
6
|
+
* 2. fixLoop :同一个错误签名在“改动之后”再次出现 ≥2 次(“每次都在补上一版的洞”)
|
|
7
|
+
* 3. noCriteria :这一轮在改东西,但回复里没有判据/验收/测试(“说不清改完怎样”)
|
|
8
|
+
*
|
|
9
|
+
* 注意:它只报症状,不判“是不是死胡同”——那个判断仍归实例。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function pathFromArguments(name, argumentsText) {
|
|
13
|
+
if (typeof argumentsText !== 'string' || argumentsText.length === 0) return null;
|
|
14
|
+
if (name !== 'edit' && name !== 'write') return null;
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(argumentsText);
|
|
17
|
+
const path = parsed?.file_path ?? parsed?.path;
|
|
18
|
+
return typeof path === 'string' && path.length > 0 ? path : null;
|
|
19
|
+
} catch {
|
|
20
|
+
const match = argumentsText.match(/"file_path"\s*:\s*"([^"]+)"/);
|
|
21
|
+
return match === null ? null : match[1];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 为将来多路径留接口;当前只有 edit/write 可靠,所以最多一个。 */
|
|
26
|
+
export function pathsFromArguments(name, argumentsText) {
|
|
27
|
+
const single = pathFromArguments(name, argumentsText);
|
|
28
|
+
return single === null ? [] : [single];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectDeadEnds({
|
|
32
|
+
toolCalls = [],
|
|
33
|
+
replies = [],
|
|
34
|
+
errors = [],
|
|
35
|
+
windowTurns = 8,
|
|
36
|
+
limit = 3,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const calls = Array.isArray(toolCalls) ? toolCalls : [];
|
|
39
|
+
const maxTurn = calls.reduce((max, call) => Math.max(max, Number(call?.turn) || 0), 0);
|
|
40
|
+
const recent = calls.filter((call) => (Number(call?.turn) || 0) > maxTurn - windowTurns);
|
|
41
|
+
|
|
42
|
+
// 1) 同处改三版
|
|
43
|
+
const byPath = new Map();
|
|
44
|
+
for (const call of recent) {
|
|
45
|
+
const paths = Array.isArray(call?.paths) ? call.paths : (typeof call?.path === 'string' ? [call.path] : []);
|
|
46
|
+
for (const path of paths) {
|
|
47
|
+
if (typeof path !== 'string' || path.length === 0) continue;
|
|
48
|
+
const entry = byPath.get(path) ?? { path, turns: new Set(), count: 0 };
|
|
49
|
+
entry.turns.add(Number(call?.turn) || 0);
|
|
50
|
+
entry.count += 1;
|
|
51
|
+
byPath.set(path, entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const sameSpot = [...byPath.values()]
|
|
55
|
+
.filter((entry) => entry.turns.size >= limit)
|
|
56
|
+
.map((entry) => ({ path: entry.path, turns: entry.turns.size, edits: entry.count }))
|
|
57
|
+
.sort((left, right) => right.turns - left.turns);
|
|
58
|
+
|
|
59
|
+
// 2) 修完仍同错:同一签名在改动之后又出现
|
|
60
|
+
const editedTurns = new Set(recent.map((call) => Number(call?.turn) || 0));
|
|
61
|
+
const bySig = new Map();
|
|
62
|
+
for (const error of Array.isArray(errors) ? errors : []) {
|
|
63
|
+
const sig = error?.sig;
|
|
64
|
+
if (typeof sig !== 'string' || sig.length === 0) continue;
|
|
65
|
+
const turn = Number(error?.turn) || 0;
|
|
66
|
+
if (!editedTurns.has(turn)) continue;
|
|
67
|
+
const entry = bySig.get(sig) ?? { sig, turns: new Set() };
|
|
68
|
+
entry.turns.add(turn);
|
|
69
|
+
bySig.set(sig, entry);
|
|
70
|
+
}
|
|
71
|
+
const fixLoop = [...bySig.values()]
|
|
72
|
+
.filter((entry) => entry.turns.size >= 2)
|
|
73
|
+
.map((entry) => ({ sig: entry.sig, turns: entry.turns.size }));
|
|
74
|
+
|
|
75
|
+
// 3) 说不清改完怎样
|
|
76
|
+
const lastReply = (Array.isArray(replies) ? replies : [])
|
|
77
|
+
.slice()
|
|
78
|
+
.sort((left, right) => (Number(left?.turn) || 0) - (Number(right?.turn) || 0))
|
|
79
|
+
.at(-1);
|
|
80
|
+
const text = typeof lastReply?.text === 'string' ? lastReply.text : '';
|
|
81
|
+
// 只在“真的动手改”的轮里才问判据:这一轮必须有 edit/write 调用。
|
|
82
|
+
// 否则讲哲学、讨论方案也会被误报(第一次上线就是这么翻车的)。
|
|
83
|
+
const lastTurn = Number(lastReply?.turn) || 0;
|
|
84
|
+
const touchedFiles = editedTurns.has(lastTurn);
|
|
85
|
+
const CHANGE_VERB = /(改成|修改|改一下|改代码|改文件|打补丁|patch\s*(一下|掉)?|调整成|重写)/;
|
|
86
|
+
const changesSomething = touchedFiles && CHANGE_VERB.test(text);
|
|
87
|
+
const hasCriteria = /判据|验收|测试|验证|证据|怎么算成功|标准/.test(text);
|
|
88
|
+
const noCriteria = changesSomething && !hasCriteria;
|
|
89
|
+
|
|
90
|
+
const symptoms = [];
|
|
91
|
+
if (sameSpot.length > 0) symptoms.push('同处改多版:' + sameSpot.slice(0, 2).map((entry) => entry.path + '(' + entry.turns + ' 轮)').join('、'));
|
|
92
|
+
if (fixLoop.length > 0) symptoms.push('改完仍同错:' + fixLoop.slice(0, 2).map((entry) => entry.sig).join('、'));
|
|
93
|
+
if (noCriteria) symptoms.push('这条改动缺验收标准');
|
|
94
|
+
return { sameSpot, fixLoop, noCriteria, symptoms };
|
|
95
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit-delete backups (~/.dsh/continuity/deleted/<id>-<ts>.json).
|
|
3
|
+
* Writes are atomic; nothing here deletes or restores automatically.
|
|
4
|
+
*/
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises';
|
|
8
|
+
|
|
9
|
+
export function continuityHome() {
|
|
10
|
+
const override = process.env.DSH_CONTINUITY_HOME;
|
|
11
|
+
return override !== undefined && override.length > 0
|
|
12
|
+
? override
|
|
13
|
+
: join(homedir(), '.dsh', 'continuity');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function deletedDir() {
|
|
17
|
+
return join(continuityHome(), 'deleted');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function writeDeletedBackup(id, payload) {
|
|
21
|
+
const safe = String(id).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
22
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
23
|
+
const dir = deletedDir();
|
|
24
|
+
await mkdir(dir, { recursive: true });
|
|
25
|
+
const file = join(dir, `${safe}-${stamp}.json`);
|
|
26
|
+
const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
27
|
+
try {
|
|
28
|
+
await writeFile(temp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
29
|
+
await rename(temp, file);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
try { await unlink(temp); } catch { /* best effort */ }
|
|
32
|
+
throw new Error(`continuity: cannot write ${file}: ${error?.message ?? error}`);
|
|
33
|
+
}
|
|
34
|
+
return file;
|
|
35
|
+
}
|