@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,522 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { continuityHome } from './threads-store.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* `continuity` — model-facing thread scaffold for the Continuity preset.
|
|
6
|
+
*
|
|
7
|
+
* One tool with four actions (`save`, `load`, `active`, `list`) rather than dot
|
|
8
|
+
* names: provider function-name grammars reject dots. The four actions are the
|
|
9
|
+
* contract's `continuity.save` / `.load` / `.active` / `.list`.
|
|
10
|
+
*/
|
|
11
|
+
import { getThreadsBackend } from './threads-backend.mjs';
|
|
12
|
+
import { makeApi, ThreadsCore } from './threads-core.mjs';
|
|
13
|
+
import { markThreadsTool } from './state.mjs';
|
|
14
|
+
import { loadFacetQuery } from './facets/load.mjs';
|
|
15
|
+
import { createPeerTool } from './peer-tool.mjs';
|
|
16
|
+
import { createRecallTool } from './recall-tool.mjs';
|
|
17
|
+
|
|
18
|
+
export const name = 'dsh-continuity/threads-tool';
|
|
19
|
+
export const inject = ['tools', 'sessionQuery', 'tokenMeter'];
|
|
20
|
+
|
|
21
|
+
const DESCRIPTION = [
|
|
22
|
+
'Explicit work threads for this Continuity session.',
|
|
23
|
+
'save folds a completed surface range into a retrievable [thread] gist and records why/next/span/tokens.',
|
|
24
|
+
'With threadId it appends that range as a new segment of the existing thread and makes it active;',
|
|
25
|
+
'without threadId it creates a new thread and clears the active pointer. next is required: it is the',
|
|
26
|
+
'first action on returning. An unknown threadId is rejected before anything is written.',
|
|
27
|
+
'load appends the newest raw segment of one thread (default 8000 tokens, max 20000), sets it active,',
|
|
28
|
+
'and says "now on this thread"; active/list inspect the index and segment count.',
|
|
29
|
+
'scan returns a cheap event index for fromSeq..toSeq or seqs:[...] without injecting;',
|
|
30
|
+
'reasoning is excluded by default and synthetic runtime-context/checkpoint messages are skipped.',
|
|
31
|
+
'find returns the seqs of a tag id (T2, a thread id, or T0) from tags.json with no text;',
|
|
32
|
+
'tag({seqs,id}) writes exact overrides, retag({from,to}) moves a whole tag, tags() lists them.',
|
|
33
|
+
'load also accepts seqs:[...] (exact), or a tag id (earliest 3 + latest 5, budget-trimmed);',
|
|
34
|
+
'thread ids keep the old latest-segment/range/select behavior. It returns segs listing what was',
|
|
35
|
+
'actually injected; its quote excludes reasoning by default.',
|
|
36
|
+
'save rejects any range that cuts a tool call/result pair and never folds the current open turn;',
|
|
37
|
+
'the default range is capped at 200 message-producing nodes, keeping the newest ones.',
|
|
38
|
+
'save and load return pending:true; the durable gist or quoted segment is appended at the next pre-step,',
|
|
39
|
+
'after this tool result, so the current call/result pair is never split.',
|
|
40
|
+
'Use save when you deliberately put a piece of work down; load when you deliberately pick it up.',
|
|
41
|
+
'The user maintains the usage rules separately in ~/.dsh/continuity/rules.md; this tool only provides the mechanics.',
|
|
42
|
+
].join(' ');
|
|
43
|
+
|
|
44
|
+
const PARAMETERS = {
|
|
45
|
+
type: 'object',
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
action: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
enum: ['save', 'switch', 'load', 'scan', 'find', 'tag', 'untag', 'retag', 'delete', 'tags', 'active', 'list'],
|
|
51
|
+
description: 'save = close a range into a thread; load = reopen a thread/tag/seqs/query; scan = cheap event index; find = tag seqs/ranges; tag = metadata, exact overrides, or one run; untag = range back to T0; retag = move a tag; delete = remove a thread with a backup; tags = tag index; active/list = threads.',
|
|
52
|
+
},
|
|
53
|
+
query: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
description: 'load: facet query or alias, e.g. "跑团". Resolves through facet-index.json to seqs; no T switch. Use instead of id/seqs.',
|
|
56
|
+
},
|
|
57
|
+
label: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'save: one-line title, e.g. "T1 爬塔". For an append, the existing thread title is kept unless relabel:true. tag: optional display label for the tag.',
|
|
60
|
+
},
|
|
61
|
+
relabel: {
|
|
62
|
+
type: 'boolean',
|
|
63
|
+
description: 'save: explicit permission to change an existing thread label; default false keeps it.',
|
|
64
|
+
},
|
|
65
|
+
backfill: {
|
|
66
|
+
type: 'boolean',
|
|
67
|
+
description: 'save: historical archival segment from raw session events. Requires fromSeq/toSeq; appends to an existing thread and never touches the live surface.',
|
|
68
|
+
},
|
|
69
|
+
alias: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'save/tag: optional short symbol, e.g. "T3". save sets the thread alias; tag sets the tag/thread alias. Auto-tagging and tags() display prefer it.',
|
|
72
|
+
},
|
|
73
|
+
keywords: {
|
|
74
|
+
type: 'array',
|
|
75
|
+
items: { type: 'string' },
|
|
76
|
+
description: 'tag: optional keywords shown in tags.md.',
|
|
77
|
+
},
|
|
78
|
+
threadId: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'save: optional existing thread id. Appends the range as a new segment and makes that thread active; unknown ids are rejected. Omit to create a new thread.',
|
|
81
|
+
},
|
|
82
|
+
why: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'save: why this is being put down; future-you reads this on return.',
|
|
85
|
+
},
|
|
86
|
+
next: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
description: 'save: REQUIRED first action when loading this thread back. Start with a verb.',
|
|
89
|
+
},
|
|
90
|
+
residue: {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: 'save: optional small deliberate remnant (≤1k tokens) to keep in the gist.',
|
|
93
|
+
},
|
|
94
|
+
fromSeq: {
|
|
95
|
+
type: 'integer',
|
|
96
|
+
description: 'save: optional inclusive start surface seq. load: optional start of the range to quote. scan: required start. tag/untag: inclusive start of a run.',
|
|
97
|
+
},
|
|
98
|
+
split: {
|
|
99
|
+
type: 'boolean',
|
|
100
|
+
description: 'save: when true and no fromSeq/toSeq, return a topic-split PLAN (no save happens). pending has one slot, so execute the plan one segment per step with explicit fromSeq/toSeq/threadId.',
|
|
101
|
+
},
|
|
102
|
+
toSeq: {
|
|
103
|
+
type: 'integer',
|
|
104
|
+
description: 'save: optional inclusive end surface seq. load: optional end of the range to quote. scan: required end. tag/untag: inclusive end of a run.',
|
|
105
|
+
},
|
|
106
|
+
id: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'load/find/tag/delete: thread id, tag id (T2), or T0. For load, a thread keeps old behavior; a tag uses first/last (default 3/5).',
|
|
109
|
+
},
|
|
110
|
+
first: {
|
|
111
|
+
type: 'integer',
|
|
112
|
+
description: 'load tag: oldest N events (default 3).',
|
|
113
|
+
},
|
|
114
|
+
last: {
|
|
115
|
+
type: 'integer',
|
|
116
|
+
description: 'load tag: newest N events (default 5).',
|
|
117
|
+
},
|
|
118
|
+
autoSave: {
|
|
119
|
+
type: 'boolean',
|
|
120
|
+
description: 'load: default true. When loading a different thread, save the current one first (switch = put down, then pick up).',
|
|
121
|
+
},
|
|
122
|
+
force: {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description: 'delete: required to remove a thread that has segments/tokens; the backup is written first.',
|
|
125
|
+
},
|
|
126
|
+
budgetTokens: {
|
|
127
|
+
type: 'integer',
|
|
128
|
+
description: 'load: quoted raw budget for the default/latest or fromSeq/toSeq range (default 8000, max 20000). An explicit select is not tail-truncated.',
|
|
129
|
+
},
|
|
130
|
+
withReasoning: {
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
description: 'load: include paired reasoning under one 16k body+reasoning budget; reasoning is separate and truncated above 800 tokens. Default false (0.4.5 behavior).',
|
|
133
|
+
},
|
|
134
|
+
select: {
|
|
135
|
+
type: 'array',
|
|
136
|
+
items: { type: 'integer' },
|
|
137
|
+
description: 'load (thread only): exact seqs to inject, usually from scan. Only visible non-reasoning text is quoted; the returned segs lists what was actually injected.',
|
|
138
|
+
},
|
|
139
|
+
seqs: {
|
|
140
|
+
type: 'array',
|
|
141
|
+
items: { type: 'integer' },
|
|
142
|
+
description: 'scan/load/tag: exact seqs. scan may use seqs instead of fromSeq/toSeq; load injects exactly these; tag writes exact overrides. tag also accepts fromSeq/toSeq instead, as one run.',
|
|
143
|
+
},
|
|
144
|
+
after: {
|
|
145
|
+
type: 'integer',
|
|
146
|
+
description: 'find: strict lower bound (seq > after).',
|
|
147
|
+
},
|
|
148
|
+
before: {
|
|
149
|
+
type: 'integer',
|
|
150
|
+
description: 'find: strict upper bound (seq < before).',
|
|
151
|
+
},
|
|
152
|
+
from: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
description: 'retag: old tag id.',
|
|
155
|
+
},
|
|
156
|
+
to: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
description: 'retag: new tag id.',
|
|
159
|
+
},
|
|
160
|
+
headChars: {
|
|
161
|
+
type: 'integer',
|
|
162
|
+
description: 'scan: head length (default 160, max 200).',
|
|
163
|
+
},
|
|
164
|
+
includeReasoning: {
|
|
165
|
+
type: 'boolean',
|
|
166
|
+
description: 'scan: include reasoning blocks (default false).',
|
|
167
|
+
},
|
|
168
|
+
types: {
|
|
169
|
+
type: 'array',
|
|
170
|
+
items: { type: 'string' },
|
|
171
|
+
description: 'scan: optional category/event-type/role filter, e.g. ["message"], ["tool-result"], ["user/message"]. Default ["message"].',
|
|
172
|
+
},
|
|
173
|
+
limit: {
|
|
174
|
+
type: 'integer',
|
|
175
|
+
description: 'scan: maximum items (default 20, max 100); the payload also stays under ~900 estimated tokens.',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['action'],
|
|
179
|
+
allOf: [
|
|
180
|
+
{
|
|
181
|
+
if: { properties: { action: { const: 'load' } }, required: ['action'] },
|
|
182
|
+
then: { anyOf: [{ required: ['id'] }, { required: ['seqs'] }, { required: ['query'] }] },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
if: { properties: { action: { const: 'scan' } }, required: ['action'] },
|
|
186
|
+
then: {
|
|
187
|
+
anyOf: [
|
|
188
|
+
{ required: ['fromSeq', 'toSeq'] },
|
|
189
|
+
{ required: ['seqs'] },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
if: { properties: { action: { const: 'find' } }, required: ['action'] },
|
|
195
|
+
then: { required: ['id'] },
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
if: { properties: { action: { const: 'tag' } }, required: ['action'] },
|
|
199
|
+
then: {
|
|
200
|
+
required: ['id'],
|
|
201
|
+
anyOf: [
|
|
202
|
+
{ required: ['seqs'] },
|
|
203
|
+
{ required: ['fromSeq', 'toSeq'] },
|
|
204
|
+
{ required: ['label'] },
|
|
205
|
+
{ required: ['keywords'] },
|
|
206
|
+
{ required: ['alias'] },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
if: { properties: { action: { const: 'untag' } }, required: ['action'] },
|
|
212
|
+
then: { required: ['fromSeq', 'toSeq'] },
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
if: { properties: { action: { const: 'retag' } }, required: ['action'] },
|
|
216
|
+
then: { required: ['from', 'to'] },
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
if: { properties: { action: { const: 'delete' } }, required: ['action'] },
|
|
220
|
+
then: { required: ['id'] },
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
function assertAction(input) {
|
|
226
|
+
if (input === null || typeof input !== 'object') throw new Error('continuity: arguments must be an object');
|
|
227
|
+
const actions = ['save', 'switch', 'load', 'scan', 'find', 'tag', 'untag', 'retag', 'delete', 'tags', 'active', 'list'];
|
|
228
|
+
if (!actions.includes(input.action)) {
|
|
229
|
+
throw new Error(`continuity: action must be ${actions.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
if (input.action === 'switch') {
|
|
232
|
+
const hasId = typeof input.id === 'string' && input.id.trim().length > 0;
|
|
233
|
+
const hasLabel = typeof input.label === 'string' && input.label.trim().length > 0;
|
|
234
|
+
if (!hasId && !hasLabel) throw new Error('continuity.switch: id or label is required');
|
|
235
|
+
}
|
|
236
|
+
if (input.action === 'save') {
|
|
237
|
+
if (input.backfill !== true) {
|
|
238
|
+
for (const key of ['label', 'why', 'next']) {
|
|
239
|
+
if (typeof input[key] !== 'string' || input[key].trim().length === 0) {
|
|
240
|
+
throw new Error(`continuity.save: ${key} is required and must be a non-empty string`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (input.backfill !== undefined && typeof input.backfill !== 'boolean') {
|
|
245
|
+
throw new Error('continuity.save: backfill must be boolean');
|
|
246
|
+
}
|
|
247
|
+
if (input.threadId !== undefined
|
|
248
|
+
&& (typeof input.threadId !== 'string' || input.threadId.trim().length === 0)) {
|
|
249
|
+
throw new Error('continuity.save: threadId must be a non-empty string when provided');
|
|
250
|
+
}
|
|
251
|
+
if (input.alias !== undefined && (typeof input.alias !== 'string' || input.alias.trim().length === 0)) {
|
|
252
|
+
throw new Error('continuity.save: alias must be a non-empty string when provided');
|
|
253
|
+
}
|
|
254
|
+
if (input.relabel !== undefined && typeof input.relabel !== 'boolean') {
|
|
255
|
+
throw new Error('continuity.save: relabel must be boolean');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (input.action === 'load') {
|
|
259
|
+
const hasId = typeof input.id === 'string' && input.id.trim().length > 0;
|
|
260
|
+
const hasSeqs = Array.isArray(input.seqs) && input.seqs.length > 0;
|
|
261
|
+
const hasQuery = typeof input.query === 'string' && input.query.trim().length > 0;
|
|
262
|
+
if (!hasId && !hasSeqs && !hasQuery) throw new Error('continuity.load: id, seqs, or query is required');
|
|
263
|
+
const hasFrom = input.fromSeq !== undefined && input.fromSeq !== null;
|
|
264
|
+
const hasTo = input.toSeq !== undefined && input.toSeq !== null;
|
|
265
|
+
if (hasFrom !== hasTo) throw new Error('continuity.load: pass both fromSeq and toSeq, or neither');
|
|
266
|
+
if (hasQuery && (hasId || hasSeqs || hasFrom || input.select !== undefined)) {
|
|
267
|
+
throw new Error('continuity.load: query is exclusive; do not combine it with id/seqs/fromSeq/select');
|
|
268
|
+
}
|
|
269
|
+
if (input.select !== undefined
|
|
270
|
+
&& (!Array.isArray(input.select) || input.select.length === 0
|
|
271
|
+
|| input.select.some((seq) => !Number.isInteger(Number(seq))))) {
|
|
272
|
+
throw new Error('continuity.load: select must be a non-empty array of integer seqs');
|
|
273
|
+
}
|
|
274
|
+
if (hasSeqs && (hasFrom || input.select !== undefined)) {
|
|
275
|
+
throw new Error('continuity.load: use seqs or fromSeq/toSeq/select, not both');
|
|
276
|
+
}
|
|
277
|
+
if (input.withReasoning !== undefined && typeof input.withReasoning !== 'boolean') {
|
|
278
|
+
throw new Error('continuity.load: withReasoning must be boolean');
|
|
279
|
+
}
|
|
280
|
+
if (input.first !== undefined && (!Number.isInteger(Number(input.first)) || Number(input.first) < 0)) {
|
|
281
|
+
throw new Error('continuity.load: first must be an integer >= 0');
|
|
282
|
+
}
|
|
283
|
+
if (input.last !== undefined && (!Number.isInteger(Number(input.last)) || Number(input.last) < 0)) {
|
|
284
|
+
throw new Error('continuity.load: last must be an integer >= 0');
|
|
285
|
+
}
|
|
286
|
+
if (input.first !== undefined && input.last !== undefined
|
|
287
|
+
&& Number(input.first) === 0 && Number(input.last) === 0) {
|
|
288
|
+
throw new Error('continuity.load: first and last cannot both be 0');
|
|
289
|
+
}
|
|
290
|
+
if ((input.first !== undefined || input.last !== undefined)
|
|
291
|
+
&& (hasSeqs || hasFrom || input.select !== undefined)) {
|
|
292
|
+
throw new Error('continuity.load: first/last are only valid for a tag id');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (input.action === 'scan') {
|
|
296
|
+
const hasSeqs = Array.isArray(input.seqs) && input.seqs.length > 0;
|
|
297
|
+
const hasFrom = Number.isInteger(Number(input.fromSeq));
|
|
298
|
+
const hasTo = Number.isInteger(Number(input.toSeq));
|
|
299
|
+
if (!hasSeqs && (!hasFrom || !hasTo)) {
|
|
300
|
+
throw new Error('continuity.scan: pass seqs, or both fromSeq and toSeq');
|
|
301
|
+
}
|
|
302
|
+
if (hasFrom && hasTo && Number(input.fromSeq) > Number(input.toSeq)) {
|
|
303
|
+
throw new Error('continuity.scan: fromSeq must be <= toSeq');
|
|
304
|
+
}
|
|
305
|
+
if (input.types !== undefined
|
|
306
|
+
&& (!Array.isArray(input.types) || input.types.some((entry) => typeof entry !== 'string'))) {
|
|
307
|
+
throw new Error('continuity.scan: types must be an array of strings');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (input.action === 'find' && (typeof input.id !== 'string' || input.id.trim().length === 0)) {
|
|
311
|
+
throw new Error('continuity.find: id is required');
|
|
312
|
+
}
|
|
313
|
+
if (input.action === 'tag') {
|
|
314
|
+
if (typeof input.id !== 'string' || input.id.trim().length === 0) throw new Error('continuity.tag: id is required');
|
|
315
|
+
const hasSeqs = Array.isArray(input.seqs) && input.seqs.length > 0;
|
|
316
|
+
const hasRange = input.fromSeq !== undefined || input.toSeq !== undefined;
|
|
317
|
+
const hasMetadata = input.label !== undefined || input.keywords !== undefined || input.alias !== undefined;
|
|
318
|
+
if (hasSeqs && hasRange) throw new Error('continuity.tag: use seqs or fromSeq/toSeq, not both');
|
|
319
|
+
if (!hasSeqs && !hasRange && !hasMetadata) {
|
|
320
|
+
throw new Error('continuity.tag: seqs, fromSeq/toSeq, or a metadata field is required');
|
|
321
|
+
}
|
|
322
|
+
if (hasSeqs && input.seqs.some((seq) => !Number.isInteger(Number(seq)))) {
|
|
323
|
+
throw new Error('continuity.tag: seqs must be an array of integer seqs');
|
|
324
|
+
}
|
|
325
|
+
if (hasRange) {
|
|
326
|
+
if (!Number.isInteger(Number(input.fromSeq)) || !Number.isInteger(Number(input.toSeq))) {
|
|
327
|
+
throw new Error('continuity.tag: fromSeq and toSeq must both be integers');
|
|
328
|
+
}
|
|
329
|
+
if (Number(input.fromSeq) > Number(input.toSeq)) {
|
|
330
|
+
throw new Error('continuity.tag: fromSeq must be <= toSeq');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (input.label !== undefined && (typeof input.label !== 'string' || input.label.trim().length === 0)) {
|
|
334
|
+
throw new Error('continuity.tag: label must be a non-empty string when provided');
|
|
335
|
+
}
|
|
336
|
+
if (input.alias !== undefined && (typeof input.alias !== 'string' || input.alias.trim().length === 0)) {
|
|
337
|
+
throw new Error('continuity.tag: alias must be a non-empty string when provided');
|
|
338
|
+
}
|
|
339
|
+
if (input.keywords !== undefined
|
|
340
|
+
&& (!Array.isArray(input.keywords) || input.keywords.some((entry) => typeof entry !== 'string'))) {
|
|
341
|
+
throw new Error('continuity.tag: keywords must be an array of strings');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (input.action === 'untag') {
|
|
345
|
+
if (!Number.isInteger(Number(input.fromSeq)) || !Number.isInteger(Number(input.toSeq))) {
|
|
346
|
+
throw new Error('continuity.untag: fromSeq and toSeq must both be integers');
|
|
347
|
+
}
|
|
348
|
+
if (Number(input.fromSeq) > Number(input.toSeq)) {
|
|
349
|
+
throw new Error('continuity.untag: fromSeq must be <= toSeq');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (input.action === 'retag') {
|
|
353
|
+
for (const key of ['from', 'to']) {
|
|
354
|
+
if (typeof input[key] !== 'string' || input[key].trim().length === 0) {
|
|
355
|
+
throw new Error(`continuity.retag: ${key} is required`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (input.action === 'delete') {
|
|
360
|
+
if (typeof input.id !== 'string' || input.id.trim().length === 0) {
|
|
361
|
+
throw new Error('continuity.delete: id is required');
|
|
362
|
+
}
|
|
363
|
+
if (input.force !== undefined && typeof input.force !== 'boolean') {
|
|
364
|
+
throw new Error('continuity.delete: force must be boolean');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Build the tool. Exported for tests with a fake session/agent.
|
|
371
|
+
* @param {{ sessionQuery: object, tokenMeter?: object, getBackend?: Function }} deps
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* 切换线头 = 先放下、再拿起。
|
|
376
|
+
* load 一个不同的 thread 之前,自动把当前线头收成一段(save),
|
|
377
|
+
* 它的 gist 会在下一个 pre-step 提交,从而折叠掉旧区间。
|
|
378
|
+
* 失败不阻塞 load —— 拿不起新线比放下失败更糟。
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
/** 在 load 之后留一个“接手”标记:下一轮注入三问(刚醒来的人视角)。 */
|
|
382
|
+
function markHandoff(kind, threadId) {
|
|
383
|
+
try {
|
|
384
|
+
const dir = (typeof process.env.USERPROFILE === "string" && process.env.USERPROFILE.length > 0)
|
|
385
|
+
? process.env.USERPROFILE + "/.dsh/continuity"
|
|
386
|
+
: (process.env.HOME ?? "") + "/.dsh/continuity";
|
|
387
|
+
writeFileSync(dir + "/handoff-pending.json", JSON.stringify({
|
|
388
|
+
at: Date.now(),
|
|
389
|
+
pid: process.pid,
|
|
390
|
+
kind,
|
|
391
|
+
threadId: threadId ?? null,
|
|
392
|
+
}), "utf8");
|
|
393
|
+
} catch { /* 记不下不影响 load */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* 切换前收起:**唯一的收起路径**。直接调 save,不另写一套折叠逻辑。
|
|
398
|
+
* - 切到别的线:exclude=目标 → 只折「不属于目标」的块;目标自己的轮原样留着
|
|
399
|
+
* - 同线 / 没给目标:threadId=当前线 → 折当前线自己的块
|
|
400
|
+
*/
|
|
401
|
+
async function forceSaveCurrent(core, input) {
|
|
402
|
+
const active = await core.active();
|
|
403
|
+
if (!active || typeof active.id !== 'string' || active.id.length === 0) return;
|
|
404
|
+
const target = typeof input.id === 'string' && input.id.length > 0 ? input.id : null;
|
|
405
|
+
const sameLine = target !== null && target === active.id;
|
|
406
|
+
await core.save({
|
|
407
|
+
label: active.label ?? active.id,
|
|
408
|
+
why: typeof input.why === 'string' && input.why.length > 0 ? input.why : '切换线头:先收起再切换',
|
|
409
|
+
next: typeof input.next === 'string' && input.next.length > 0
|
|
410
|
+
? input.next
|
|
411
|
+
: (typeof active.next === 'string' && active.next.length > 0 ? active.next : '(未写)'),
|
|
412
|
+
...(sameLine || target === null ? { threadId: active.id } : { exclude: target }),
|
|
413
|
+
...(typeof input.residue === 'string' && input.residue.length > 0 ? { residue: input.residue } : {}),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function createThreadsTool({ sessionQuery, tokenMeter, getBackend = getThreadsBackend }) {
|
|
418
|
+
if (sessionQuery === undefined || sessionQuery === null) throw new Error('continuity: sessionQuery is required');
|
|
419
|
+
return {
|
|
420
|
+
name: 'continuity',
|
|
421
|
+
description: DESCRIPTION,
|
|
422
|
+
parameters: PARAMETERS,
|
|
423
|
+
output: {
|
|
424
|
+
schema: { type: 'string' },
|
|
425
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
426
|
+
},
|
|
427
|
+
timeoutMs: 120_000,
|
|
428
|
+
isConcurrencySafe: () => false,
|
|
429
|
+
async execute(args, exec) {
|
|
430
|
+
const input = args !== null && typeof args === 'object' ? args : {};
|
|
431
|
+
assertAction(input);
|
|
432
|
+
exec?.signal?.throwIfAborted?.();
|
|
433
|
+
// 欠账**不再锁工具**(2026-10-30 用户裁定:挡 save 只会让实例最后存不下、消失;
|
|
434
|
+
// 挡读只会让人恼怒)。真正的闸在回合末:'agent/turn-stopping' 里催补标并把本轮续上。
|
|
435
|
+
// tag-debt.json 仍由引擎写,只作为头部提示的来源,工具层不再据此拒绝。
|
|
436
|
+
const backend = {
|
|
437
|
+
prepareSave: (agent, saveArgs) => getBackend().prepareSave(agent, saveArgs),
|
|
438
|
+
};
|
|
439
|
+
const core = new ThreadsCore({
|
|
440
|
+
agent: exec?.agent,
|
|
441
|
+
sessionQuery,
|
|
442
|
+
tokenMeter,
|
|
443
|
+
backend,
|
|
444
|
+
});
|
|
445
|
+
const api = makeApi(core);
|
|
446
|
+
// 收起失败不再静默:记一行到 save-before-switch.log,并把错误挂到 load/switch 的回执上。
|
|
447
|
+
let saveError = null;
|
|
448
|
+
const logSaveFailure = (action) => (error) => {
|
|
449
|
+
saveError = `${String(error?.name ?? 'Error')}: ${String(error?.message ?? error).slice(0, 300)}`;
|
|
450
|
+
try {
|
|
451
|
+
appendFileSync(join(continuityHome(), 'save-before-switch.log'), JSON.stringify({
|
|
452
|
+
at: new Date().toISOString(),
|
|
453
|
+
action,
|
|
454
|
+
name: String(error?.name ?? 'Error'),
|
|
455
|
+
message: String(error?.message ?? error).slice(0, 400),
|
|
456
|
+
}) + '\n');
|
|
457
|
+
} catch { /* 写不了日志也不能阻断 */ }
|
|
458
|
+
};
|
|
459
|
+
let result;
|
|
460
|
+
if (input.action === 'switch') {
|
|
461
|
+
try { await forceSaveCurrent(core, input); } catch (error) { logSaveFailure('switch')(error); }
|
|
462
|
+
result = await api.load({ ...input, autoSave: false }, exec?.signal);
|
|
463
|
+
}
|
|
464
|
+
else if (input.action === 'save') { result = await api.save(input, exec?.signal); }
|
|
465
|
+
else if (input.action === 'load') {
|
|
466
|
+
// load 现在等同 switch:先收当前段(收起即折叠),再载入目标。
|
|
467
|
+
let loadInput = input;
|
|
468
|
+
let facetLoad = null;
|
|
469
|
+
if (typeof input.query === 'string' && input.query.trim().length > 0) {
|
|
470
|
+
facetLoad = loadFacetQuery(input.query, {
|
|
471
|
+
indexFile: join(continuityHome(), 'facet-index.json'),
|
|
472
|
+
topK: 50,
|
|
473
|
+
});
|
|
474
|
+
if (facetLoad.seqs.length === 0) {
|
|
475
|
+
return JSON.stringify({
|
|
476
|
+
ok: false,
|
|
477
|
+
reason: 'facet-no-match',
|
|
478
|
+
query: input.query,
|
|
479
|
+
hint: 'run facet-scan or generate facet-index.json first',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
loadInput = { ...input, seqs: facetLoad.seqs, query: undefined };
|
|
483
|
+
}
|
|
484
|
+
if (loadInput.autoSave !== false) {
|
|
485
|
+
try { await forceSaveCurrent(core, loadInput); } catch (error) { logSaveFailure('load')(error); }
|
|
486
|
+
}
|
|
487
|
+
result = await api.load(loadInput, exec?.signal);
|
|
488
|
+
if (facetLoad !== null) result = { ...result, facetQuery: { query: input.query, ranges: facetLoad.ranges, seqs: facetLoad.seqs.length } };
|
|
489
|
+
markHandoff('load', loadInput?.id ?? null);
|
|
490
|
+
}
|
|
491
|
+
else if (input.action === 'scan') result = await api.scan(input, exec?.signal);
|
|
492
|
+
else if (input.action === 'find') result = await api.find(input);
|
|
493
|
+
else if (input.action === 'tag') result = await api.tag(input);
|
|
494
|
+
else if (input.action === 'untag') result = await api.untag(input);
|
|
495
|
+
else if (input.action === 'retag') result = await api.retag(input);
|
|
496
|
+
else if (input.action === 'delete') result = await api.delete(input);
|
|
497
|
+
else if (input.action === 'tags') result = await api.tags();
|
|
498
|
+
else if (input.action === 'active') result = await api.active();
|
|
499
|
+
else result = await api.list();
|
|
500
|
+
// 收起失败要看得见(以前只写日志或干脆吞掉,实测时表现为"什么都没折")。
|
|
501
|
+
if (saveError !== null) result = { ...result, autoSaveError: saveError };
|
|
502
|
+
return JSON.stringify(result);
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function apply(ctx, config = {}) {
|
|
508
|
+
if (config?.safeMode !== true) {
|
|
509
|
+
const tool = createThreadsTool({ sessionQuery: ctx.sessionQuery, tokenMeter: ctx.tokenMeter });
|
|
510
|
+
ctx.effect(() => {
|
|
511
|
+
const dispose = ctx.tools.register(tool);
|
|
512
|
+
markThreadsTool({ name: tool.name, scope: 'continuity preset' });
|
|
513
|
+
return dispose;
|
|
514
|
+
});
|
|
515
|
+
} else {
|
|
516
|
+
try { ctx.logger?.info?.('[dsh-continuity] safe mode: continuity/T tool not registered'); } catch { /* ignore */ }
|
|
517
|
+
}
|
|
518
|
+
const recallTool = createRecallTool({ sessionQuery: ctx.sessionQuery });
|
|
519
|
+
ctx.effect(() => ctx.tools.register(recallTool));
|
|
520
|
+
const peer = createPeerTool();
|
|
521
|
+
ctx.effect(() => ctx.tools.register(peer));
|
|
522
|
+
}
|