@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,1743 @@
|
|
|
1
|
+
# API notes — what the adapter still needs
|
|
2
|
+
|
|
3
|
+
> **Historical.** Research notes from an older `0.2.x` host. Current architecture is in
|
|
4
|
+
> `docs/ARCHITECTURE.md`; do not use these paths/APIs without re-checking the live host.
|
|
5
|
+
|
|
6
|
+
Researched from the public npm type declarations, not from the running host. The runtime may be a
|
|
7
|
+
newer `0.2.x`; pin peers before publishing.
|
|
8
|
+
|
|
9
|
+
Sources:
|
|
10
|
+
|
|
11
|
+
- [`@deepseek-ai/dsh-compaction` 0.2.1-rc.3 `index.d.ts`](https://unpkg.com/@deepseek-ai/dsh-compaction@0.2.1-rc.3/lib/types/index.d.ts)
|
|
12
|
+
- [`@deepseek-ai/dsh-compaction` `types.d.ts`](https://unpkg.com/@deepseek-ai/dsh-compaction@0.2.1-rc.3/lib/types/types.d.ts)
|
|
13
|
+
- [`@deepseek-ai/dsh-compaction-basic` `index.d.ts`](https://unpkg.com/@deepseek-ai/dsh-compaction-basic@0.2.1-rc.3/lib/types/index.d.ts)
|
|
14
|
+
- [`@deepseek-ai/dsh-compaction-basic` README (zh)](https://unpkg.com/@deepseek-ai/dsh-compaction-basic@0.2.1-rc.3/README.zh.md)
|
|
15
|
+
|
|
16
|
+
## The seam
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
abstract class CompactionEngine extends Service {
|
|
20
|
+
abstract compactIfNeeded(
|
|
21
|
+
agent: CompactionAgentContext,
|
|
22
|
+
trigger: 'pressure' | 'context-overflow',
|
|
23
|
+
signal: AbortSignal,
|
|
24
|
+
): Promise<CompactionResult | null>;
|
|
25
|
+
|
|
26
|
+
abstract compactNow(
|
|
27
|
+
agent: ManualCompactAgentContext,
|
|
28
|
+
signal: AbortSignal,
|
|
29
|
+
sourceCommandId?: CommandId,
|
|
30
|
+
): Promise<CompactionResult | null>;
|
|
31
|
+
|
|
32
|
+
abstract compactRegion(
|
|
33
|
+
start: SessionSeq,
|
|
34
|
+
end: SessionSeq,
|
|
35
|
+
agent: CompactionAgentContext,
|
|
36
|
+
signal?: AbortSignal,
|
|
37
|
+
): Promise<CompactionResult>;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A successful run replaces the selected surface span with **one summary user message**. The
|
|
42
|
+
transaction writes `compaction/start` → `compaction/summary` → replacement `user/message` →
|
|
43
|
+
`compaction/end`. Edges must be balanced (`toolPairingBalancedBefore` / `After`). The system
|
|
44
|
+
message at surface position 0 is never shadowed.
|
|
45
|
+
|
|
46
|
+
## Why extend the basic backend
|
|
47
|
+
|
|
48
|
+
`@deepseek-ai/dsh-compaction-basic` already implements the expensive parts:
|
|
49
|
+
|
|
50
|
+
- `BasicCompactionEngine` (subclass of `CompactionEngine`);
|
|
51
|
+
- token-meter-driven pressure and overflow recovery;
|
|
52
|
+
- range selection, retention, durable marker transaction;
|
|
53
|
+
- `summarize()` as the sole subclass hook;
|
|
54
|
+
- public `compactRegion(start, end, agent, signal)`.
|
|
55
|
+
|
|
56
|
+
So the skeleton subclasses it and only overrides `compactIfNeeded` to choose **smaller ranges**
|
|
57
|
+
than the base policy would. `compactNow` and `compactRegion` stay inherited.
|
|
58
|
+
|
|
59
|
+
Its config (all optional):
|
|
60
|
+
|
|
61
|
+
| Field | Default | Meaning |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `thresholdRatio` | `0.8` | trigger at `floor(min(W × ratio, W − O − headroom))` |
|
|
64
|
+
| `headroomTokens` | `65536` | extra margin beyond output reserve |
|
|
65
|
+
| `retainRatio` | `0.16` | recent verbatim budget as a fraction of `W − O` |
|
|
66
|
+
| `retainTokens` | — | absolute form, mutually exclusive with `retainRatio` |
|
|
67
|
+
| `maxTokens` | headroom | summary output cap |
|
|
68
|
+
| `auto` | `true` | automatic pressure + overflow recovery |
|
|
69
|
+
| `modelPolicies` | `[]` | per-`{provider,model}` overrides |
|
|
70
|
+
|
|
71
|
+
## The two missing pieces
|
|
72
|
+
|
|
73
|
+
### 1. `measure(agent)`
|
|
74
|
+
|
|
75
|
+
Need the used and window tokens for the agent's latest routed request from `ctx.tokenMeter`. The
|
|
76
|
+
README describes the semantics (singleton meter, latest durable routed envelope, fixed estimator)
|
|
77
|
+
but not the call signatures. Inspect with Creator mode:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
cordis_inspect_list
|
|
81
|
+
cordis_inspect_query → the token-meter provider's methods/types
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. `oldestRange(agent, budget)`
|
|
85
|
+
|
|
86
|
+
Need to walk `agent.session`'s surface, skipping the system node at position 0, accumulate nodes
|
|
87
|
+
until `budget` is reached, then move the end back to a balanced boundary. The exported helpers:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
toolPairingBalancedBefore(session, startNode): boolean
|
|
91
|
+
toolPairingBalancedAfter(session, endNode): boolean
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Return `{ start, end }` as surface seqs (inclusive), or `null`. If `null`, fall back to
|
|
95
|
+
`super.compactIfNeeded(...)` so the session still compacts at the base threshold.
|
|
96
|
+
|
|
97
|
+
## Provenance: quiet, not silent
|
|
98
|
+
|
|
99
|
+
The fixed checkpoint preamble is an **event marker**; the provenance header is a **property of the
|
|
100
|
+
memory**. Both can coexist:
|
|
101
|
+
|
|
102
|
+
- `gistHeader({ sessionId, startSeq, endSeq, count })` produces
|
|
103
|
+
`[gist · log:<session>#seq=<start>-<end> · N messages · original retrievable]`.
|
|
104
|
+
- `annotateSummary(blocks, header)` prepends it to the first text block of the summary.
|
|
105
|
+
- `SYSTEM_CONVENTION` goes into the system prompt **once**; it explains the tag and the fallback
|
|
106
|
+
("say you only remember the gist").
|
|
107
|
+
|
|
108
|
+
The intended `summarize()` override:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
protected async summarize(input, agent, signal) {
|
|
112
|
+
const base = await super.summarize(input, agent, signal);
|
|
113
|
+
const header = this.pendingHeader;
|
|
114
|
+
this.pendingHeader = null;
|
|
115
|
+
if (!header) return base;
|
|
116
|
+
return { ...base, summary: annotateSummary(base.summary, header) };
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Unknowns to confirm at runtime:
|
|
121
|
+
|
|
122
|
+
- the exact field of `SummaryResult` holding the blocks (`summary` is the guess);
|
|
123
|
+
- whether a `ctx.systemPrompt.section({ name, order, interpolate: false, text })` seam exists
|
|
124
|
+
(the `dsh-soul` plugin uses that shape) and where to mount it so it is not itself compacted;
|
|
125
|
+
- whether the preamble can be shortened without reimplementing the replacement transaction.
|
|
126
|
+
|
|
127
|
+
Test for the failure this prevents: ask about a detail that exists only in a compacted range. The
|
|
128
|
+
expected answer is "I only have the gist; the original is at `log:…#seq=…`", not a confident
|
|
129
|
+
fabrication.
|
|
130
|
+
|
|
131
|
+
## Hard constraints
|
|
132
|
+
|
|
133
|
+
- **One engine per context.** `ctx.compaction` holds exactly one implementation. A plugin must
|
|
134
|
+
**replace** the existing `compaction-basic` row, not insert a second one.
|
|
135
|
+
- **`compactRegion` needs an open turn**; `compactNow` reserves an idle agent instead.
|
|
136
|
+
- **Every pass is an LLM call.** `summarize()` runs one `ctx.llm.stream()`. Ten small passes cost
|
|
137
|
+
ten calls. This is the main reason "breathing" is expensive at the compaction layer; a
|
|
138
|
+
model-free prune path (rewriting old tool receipts) would be cheaper but is a different engine
|
|
139
|
+
and cannot summarize conversation.
|
|
140
|
+
- **Cache:** each replacement invalidates the prefix from the first changed token. Batch small
|
|
141
|
+
passes at turn boundaries; keep the system prefix untouched.
|
|
142
|
+
- **Safety:** replacing a compaction backend can corrupt a session's surface. Test in a scratch
|
|
143
|
+
profile with a disposable conversation; keep the default engine one patch line away.
|
|
144
|
+
|
|
145
|
+
## Config-only quick win
|
|
146
|
+
|
|
147
|
+
Before writing any engine code, the shipped backend can already approximate the Coopanion band:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
- id: <existing compaction-basic loader id>
|
|
151
|
+
config:
|
|
152
|
+
thresholdRatio: 0.92
|
|
153
|
+
retainRatio: 0.5
|
|
154
|
+
headroomTokens: 65536
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This keeps a 50% recent verbatim tail and compacts at 92%. It is still a threshold + one summary,
|
|
158
|
+
not breathing, but it is a real, reversible test of the band on DSH — and it changes no code.
|
|
159
|
+
|
|
160
|
+
Find the id in Creator mode: `cordis_inspect_list` for the `Config` provider, locate the
|
|
161
|
+
`@deepseek-ai/dsh-compaction-basic` entry, and read its loader id / tree id. An unknown id in a
|
|
162
|
+
patch is not guaranteed to be a harmless no-op.
|
|
163
|
+
|
|
164
|
+
> **Measured caveat (2026-10-26):** on this desktop profile the base row is disabled and the live
|
|
165
|
+
> engine is preset-scoped (`preset-standard` → group `compaction` → `compaction-basic`), which no
|
|
166
|
+
> patch id can address. The experiment is therefore shipped as a **copy of the preset declaration**
|
|
167
|
+
> with the changed child config (`docs/EXPERIMENT.md`), not as a row override. The base row id is
|
|
168
|
+
> still recorded above for profiles where compaction-basic is host-level.
|
|
169
|
+
|
|
170
|
+
## Version pinning
|
|
171
|
+
|
|
172
|
+
The DSH app ships `@deepseek-ai/dsh-compaction` / `-basic` `0.2.x` (runtime `0.2.1-rc.3` in the
|
|
173
|
+
packages inspected). Before publishing, pin `peerDependencies` to the range the app actually
|
|
174
|
+
provides and run `dsh plugin ... add` against a scratch profile.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
# Measured at runtime — 2026-10-26
|
|
179
|
+
|
|
180
|
+
Everything below was read from the **running** desktop Host and its installed packages, not from
|
|
181
|
+
npm types. Environment: `@deepseek-ai/dsh-desktop` `0.2.1-rc.3`, bundled runtime
|
|
182
|
+
`@deepseek-ai/dsh-desktop-runtime` `0.2.1-rc.3` under `resources/app.asar/dsh`, profile `desktop`,
|
|
183
|
+
Electron `process.resourcesPath` present.
|
|
184
|
+
|
|
185
|
+
## 1. Compaction loader rows
|
|
186
|
+
|
|
187
|
+
`cordis_inspect_query` → Host `Config`, `listConfigs`:
|
|
188
|
+
|
|
189
|
+
| What | Exact value |
|
|
190
|
+
|---|---|
|
|
191
|
+
| Base row id | `include:compaction-basic` |
|
|
192
|
+
| Base row `patchId` | `compaction-basic` |
|
|
193
|
+
| Base row name | `@deepseek-ai/dsh-compaction-basic` |
|
|
194
|
+
| Base row enabled in this profile | **false** (`fiberPhase: null`) |
|
|
195
|
+
| Base row `packageDir` | `…/app.asar/dsh/node_modules/@deepseek-ai/dsh-compaction-basic` |
|
|
196
|
+
| Preset row id | `include:preset-standard` |
|
|
197
|
+
| Preset row `patchId` | `preset-standard` |
|
|
198
|
+
| Preset row name | `@deepseek-ai/dsh-agent-preset` |
|
|
199
|
+
| Preset row `packageDir` | `…/app.asar/dsh/node_modules/@deepseek-ai/dsh-agent-preset` |
|
|
200
|
+
|
|
201
|
+
The **effective** engine is not the base row. It is a child of the `standard` preset:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
preset-standard
|
|
205
|
+
└─ group: compaction isolate: { compaction: true, toolResultPruner: true }
|
|
206
|
+
├─ compaction-basic
|
|
207
|
+
├─ command-compact
|
|
208
|
+
└─ tool-result-pruner
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Consequences, confirmed live:
|
|
212
|
+
|
|
213
|
+
- The preset child is **not addressable** by a loader patch id. `Config.listConfigs` rejects
|
|
214
|
+
`include:preset-standard:compaction:compaction-basic` (nested entry ids use `:`), and a patch
|
|
215
|
+
with an id plus `config` targets only top-level rows.
|
|
216
|
+
- Therefore the config-only experiment cannot override "the existing row"; it must **copy the
|
|
217
|
+
preset declaration** with the changed child config. This plugin ships that as the `band` preset
|
|
218
|
+
and leaves `preset-standard` untouched. See `docs/EXPERIMENT.md`.
|
|
219
|
+
- The preset declaration `Config` schema is `{ id, name?, description?, order?, plugins }`.
|
|
220
|
+
- After installing this plugin, the Host roster contains `include:preset-band` and
|
|
221
|
+
`include:preset-continuity` in addition to the four shipped presets.
|
|
222
|
+
|
|
223
|
+
## 2. `ctx.tokenMeter` (exact)
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement
|
|
227
|
+
estimateMessage(message: Message): number
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`TokenMeasurement` is detached and deeply immutable:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
interface TokenMeasurement {
|
|
234
|
+
readonly logRevision: SessionLogOffset
|
|
235
|
+
readonly baseline:
|
|
236
|
+
| { readonly kind: 'none'; readonly tokens: 0 }
|
|
237
|
+
| { readonly kind: 'estimated'; readonly tokens: number }
|
|
238
|
+
| { readonly kind: 'usage'; readonly tokens: number; readonly usage: Readonly<TokenUsage> }
|
|
239
|
+
readonly surfaceDeltaTokens: number
|
|
240
|
+
readonly totalTokens: number // the pressure number the policy uses
|
|
241
|
+
readonly surfaceTokens: number
|
|
242
|
+
readonly nodes: readonly TokenSurfaceNode[]
|
|
243
|
+
}
|
|
244
|
+
interface TokenSurfaceNode {
|
|
245
|
+
readonly seq: SessionSeq // same positional seqs as session.surface.nodes
|
|
246
|
+
readonly tokens: number // route-priced (images included)
|
|
247
|
+
readonly heuristicTokens: number // fixed estimator, used for shadowedTokenCount
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The `nodes` array is positionally aligned with `session.surface.nodes`; `BasicCompactionEngine`
|
|
252
|
+
rejects a measurement whose surface does not match exactly. `measure()` clones the positional
|
|
253
|
+
nodes, so holding one across a surface change is safe but stale.
|
|
254
|
+
|
|
255
|
+
## 3. Session surface (exact)
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
session.surface: {
|
|
259
|
+
readonly nodes: readonly SessionSeq[] // surface positions, oldest first
|
|
260
|
+
readonly replaceGeneration: number // bumps on a replace transaction
|
|
261
|
+
readonly contentGeneration: number
|
|
262
|
+
}
|
|
263
|
+
session.id: SessionId
|
|
264
|
+
session.seq: SessionLogOffset
|
|
265
|
+
session.eventAt(seq: SessionSeq): SessionEvent | undefined
|
|
266
|
+
session.deriveEventMessage(event: SessionEvent): Message | null
|
|
267
|
+
session.requestHeader(): EpochHeader | undefined
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`EpochHeader` is `{ config: { provider, model, maxTokens?, … }, tools?: ToolSchema[], adapterDefaults? }`.
|
|
271
|
+
Real header observed in a compacted session:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{ "provider": "deepseek-official", "model": "deepseek-flash", "reasoningEffort": "max", "maxTokens": 256000 }
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
That `maxTokens` is the **conversation request's** reserved output, and it is what
|
|
278
|
+
`BasicCompactionEngine` subtracts from the context window. It is not the summary cap.
|
|
279
|
+
|
|
280
|
+
## 4. Window capacity and the config-only arithmetic
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
ctx.llm.resolveModelInfo(provider, model, signal?): Promise<{
|
|
284
|
+
context?: { contextWindow: number }
|
|
285
|
+
defaultMaxTokens?: number
|
|
286
|
+
…
|
|
287
|
+
}>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Measured for `deepseek-official/deepseek-flash`: `contextWindow = 1_048_576`,
|
|
291
|
+
`defaultMaxTokens = 256_000`. The base backend computes (`resolveCompactSpec`):
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
messageBudget = contextWindow − reservedCompletionTokens
|
|
295
|
+
threshold = floor(min(contextWindow × thresholdRatio, messageBudget − headroomTokens))
|
|
296
|
+
retain = floor(messageBudget × retainRatio)
|
|
297
|
+
require retain < threshold
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Defaults: `thresholdRatio 0.8`, `retainRatio 0.16`, `headroomTokens 65_536`,
|
|
301
|
+
`maxTokens = headroomTokens` (the summary output cap), `compactionRetries 1`,
|
|
302
|
+
`maxOverflowRetries 1`.
|
|
303
|
+
|
|
304
|
+
For this model (`messageBudget = 792_576`):
|
|
305
|
+
|
|
306
|
+
| Config | Threshold | Verbatim retain | One pass at threshold |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| shipped `0.8 / 0.16` | 727,040 | 126,812 | 600,228 |
|
|
309
|
+
| experiment `0.92 / 0.5` | **727,040** | **396,288** | **330,752** |
|
|
310
|
+
| band engine, cache-first `86% / 62% / protect 30%` | triggers at **681,615** | protects **237,773** | one ~190,000 drain to low |
|
|
311
|
+
|
|
312
|
+
Note the cap: `0.92 × W` is 964,690, but `W − O − headroom` is 727,040, so the 92% experiment is
|
|
313
|
+
limited by headroom, not by its ratio. The config is valid (`396,288 < 727,040`).
|
|
314
|
+
|
|
315
|
+
Version note: the older `0.1.5` runtime's `resolveCompactSpec` used
|
|
316
|
+
`threshold = floor(W × ratio)` and `retain = floor(W × retainRatio)` with **no** output reserve or
|
|
317
|
+
headroom, and defaulted `maxTokens` to **8192**. The current `0.2.1` Host subtracts the reserve and
|
|
318
|
+
headroom as shown above and defaults `maxTokens` to the headroom (65,536). Older compaction records
|
|
319
|
+
with `maxTokens: 8192` (for example `session-4185b80f…`) come from that generation; the current-era
|
|
320
|
+
records carry 65,536. The projection in the panel is the 0.2.1 formula.
|
|
321
|
+
|
|
322
|
+
## 5. Summary result and the provenance field
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
summarize(input: SummarizationInput, agent: Agent, signal?): Promise<SummaryResult>
|
|
326
|
+
type SummaryResult = {
|
|
327
|
+
summary: ContentBlock[] // ← confirmed; blocks, not a string
|
|
328
|
+
provider: string
|
|
329
|
+
model: string
|
|
330
|
+
maxTokens?: number
|
|
331
|
+
usage?: TokenUsage
|
|
332
|
+
} & ({ rawOutput: ContentBlock[]; llmStreamCall: true }
|
|
333
|
+
| { rawOutput?: ContentBlock[]; llmStreamCall?: never })
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`frameSummary()` wraps the returned blocks in the fixed checkpoint preamble plus
|
|
337
|
+
`<compacted-summary>…</compacted-summary>`. Our `annotateSummary(base.summary, header)` therefore
|
|
338
|
+
prepends the `[gist …]` line to the first text block, inside the summary tags and after the
|
|
339
|
+
preamble. Verified format from a real transaction:
|
|
340
|
+
|
|
341
|
+
```
|
|
342
|
+
block 0: "This is an automatically generated checkpoint …" (preamble)
|
|
343
|
+
block 1: "## Primary Request and Intent …" (summary; our header lands here)
|
|
344
|
+
block 2: "</compacted-summary>"
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## 6. Real compaction transaction record
|
|
348
|
+
|
|
349
|
+
Decoded from the local `session.v4.jsonl.zstd` logs (read-only scan; the logs are concatenated
|
|
350
|
+
zstd frames, so one-shot `zstdDecompressSync` only yields the first frame — split at the
|
|
351
|
+
`28 B5 2F FD` magic bytes and decode each frame, or use the persistence package's frame scanner):
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
session session-2f831fa2-a345-4e99-bce7-5796444fea57
|
|
355
|
+
compactionId b57ba840-1892-4175-b652-06c7e30b8609
|
|
356
|
+
start seq 8255
|
|
357
|
+
summary seq 8257 shadowedRange 4424–7043
|
|
358
|
+
1259 surface nodes, 363,912 tokens
|
|
359
|
+
summary model deepseek-flash, maxTokens 65536
|
|
360
|
+
usage 505,602 input / 4,731 output
|
|
361
|
+
replace seq 8258 surfaceOp { replace, startSeq 4424, endSeq 7043 }
|
|
362
|
+
source { kind: 'compact-checkpoint', compactionId: b57ba840… }
|
|
363
|
+
end seq 8259
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Other observed one-pass cliffs in the same log set: **354,500**, **342,756**, **425,772**,
|
|
367
|
+
**442,443** shadowed tokens. Those are the behavior the band controller exists to replace.
|
|
368
|
+
The summary text in all seven observed transactions starts directly with `## Primary Request`
|
|
369
|
+
— there is no gist header in the shipped backend, which is why `summarize()` is overridden.
|
|
370
|
+
|
|
371
|
+
## 7. Tool pairing and the transaction seam
|
|
372
|
+
|
|
373
|
+
Confirmed exports of `@deepseek-ai/dsh-compaction` (runtime `lib/index.js`, and exercised by the
|
|
374
|
+
engine's import):
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
toolPairingBalancedBefore(session, seq): boolean // cut immediately BEFORE seq
|
|
378
|
+
toolPairingBalancedAfter(session, seq): boolean // cut immediately AFTER seq
|
|
379
|
+
compactCheckpointSource(compactionId, sourceCommandId?): { kind: 'compact-checkpoint', … }
|
|
380
|
+
isCompactCheckpointSource(source): boolean
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The predicates throw when the seq is no longer on the current surface. `compactRegion(start, end,
|
|
384
|
+
agent, signal?)` validates both edges, the span, the active-turn lock, and then runs the transaction.
|
|
385
|
+
`CompactionResult` carries `compactionId, startSeq, summarySeq, endSeq, summary, shadowedRange,
|
|
386
|
+
shadowedSeqs, shadowedTokenCount`.
|
|
387
|
+
|
|
388
|
+
## 8. System prompt section
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
ctx.systemPrompt.section(section: PromptSection): () => void
|
|
392
|
+
interface PromptSection {
|
|
393
|
+
readonly name: string
|
|
394
|
+
readonly order: number
|
|
395
|
+
readonly text: string | ((context) => string)
|
|
396
|
+
readonly interpolate?: boolean
|
|
397
|
+
readonly complete?: boolean
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Section order is a plain number; the repository's named orders run from `HARNESS_IDENTITY (−1000)`
|
|
402
|
+
to `DEPLOYMENT_PERSONA_SUFFIX (10200)`. The standing provenance convention is mounted once at
|
|
403
|
+
**order 9500**, after the tool sections and before structured output, via
|
|
404
|
+
`ctx.inject(['systemPrompt'], …)` so the engine still loads in a profile without that service.
|
|
405
|
+
Live state reports `standingConvention: 'registered'`.
|
|
406
|
+
|
|
407
|
+
## 9. How this plugin loads the base class
|
|
408
|
+
|
|
409
|
+
The plugin is installed as a **`link:`** dependency (`pnpm` warns that peer dependencies do not
|
|
410
|
+
resolve from the linked target). A plain `import '@deepseek-ai/dsh-compaction-basic'` from the
|
|
411
|
+
workspace would therefore fail, or find a different runtime generation. The engine instead tries,
|
|
412
|
+
in order:
|
|
413
|
+
|
|
414
|
+
1. `file://<resources>/app.asar/dsh/node_modules/@deepseek-ai/<pkg>/lib/index.js`
|
|
415
|
+
2. `file://<resources>/app.asar/node_modules/@deepseek-ai/<pkg>/lib/index.js`
|
|
416
|
+
3. `@deepseek-ai/<pkg>` (portable fallback)
|
|
417
|
+
|
|
418
|
+
The live panel snapshot confirms candidate 1 was used for both `dsh-compaction-basic` and
|
|
419
|
+
`dsh-compaction`; the engine extends the same `BasicCompactionEngine` / Cordis `Service` generation
|
|
420
|
+
as the Host. If Electron cannot import from `app.asar`, the fallback keeps the engine loadable but
|
|
421
|
+
would mix generations — the panel surfaces the chosen package URL under `engine.base`.
|
|
422
|
+
|
|
423
|
+
## 10. Remaining unknowns / limits
|
|
424
|
+
|
|
425
|
+
- **Manual `/compact` has no gist header yet.** `compactNow` selects its range inside the inherited
|
|
426
|
+
transaction, so this version cannot label it before `summarize()` runs. Automatic band passes are
|
|
427
|
+
labeled; manual compaction keeps the plain checkpoint preamble.
|
|
428
|
+
- **The cache-first policy was observed live on 2026-10-26 at 18:33:16.** One `band-high` pass:
|
|
429
|
+
`estTokens 745,243`, `tokens 179,725`, `count 143`; the next requests returned to ~99.96% cache
|
|
430
|
+
hits. The same session's old policy had produced 53 passes (698,108 tokens, 34 one-node passes).
|
|
431
|
+
See `docs/CACHE.md`.
|
|
432
|
+
- **Panel pixels are unverified.** There is no browser control in this environment. The client
|
|
433
|
+
module, locale registration, both slot registrations, and the full element tree were exercised
|
|
434
|
+
headlessly (`tools/smoke-client.mjs`), but light/dark appearance and layout are not claimed.
|
|
435
|
+
- The **summary cap** `maxTokens` is inherited from `headroomTokens` (65,536) when not configured;
|
|
436
|
+
`compaction/summary` records show 65,536 or 8,192 depending on the session's config generation.
|
|
437
|
+
|
|
438
|
+
## 11. Cold-start route bug (fixed 2026-10-26)
|
|
439
|
+
|
|
440
|
+
**Symptom after a DSH restart:** `include:dsh-continuity` is `enabled: true, fiberPhase: active`,
|
|
441
|
+
`include:preset-continuity` is active, but `GET /dsh-continuity/state.json` returns **404**. A
|
|
442
|
+
running install worked; only a fresh boot lost the panel.
|
|
443
|
+
|
|
444
|
+
**Cause:** `apply()` read `ctx.get('webServer')` synchronously and returned early when the service
|
|
445
|
+
was not yet published. On a cold start the row can activate before the Web-server row, so the row
|
|
446
|
+
stayed active with no route. The previous session had installed the plugin into an already-running
|
|
447
|
+
Host, which hid the ordering bug.
|
|
448
|
+
|
|
449
|
+
**Fix:** the route is registered through `ctx.inject(['webServer'], (scoped) => { … })`, so it
|
|
450
|
+
appears when the service is ready and is disposed with the plugin. Do not read an optional service
|
|
451
|
+
synchronously in `apply()` unless the row declares it in `inject`; use `ctx.inject` for optional
|
|
452
|
+
services.
|
|
453
|
+
|
|
454
|
+
**Verification after the fix:** the state route must return 200 and `engine` must be non-null on a
|
|
455
|
+
fresh boot, not only after a hot install.
|
|
456
|
+
|
|
457
|
+
## 12. Cache-first policy (2026-10-26)
|
|
458
|
+
|
|
459
|
+
The policy source of truth is `CACHE_FIRST_POLICY` in `src/policy.mjs`. `preset-continuity` pins
|
|
460
|
+
only `policy.setpoint: 0.86` (the cache-first value) so a hot preset reconciliation cannot fall
|
|
461
|
+
back to the older 0.72 module default; every other field inherits. It previously restated the whole
|
|
462
|
+
policy with the old `0.65/0.80/0.55/8k/4k` values and silently overrode the new code defaults —
|
|
463
|
+
that mismatch was the reason the 18:07 edit appeared not to work. The full analysis and
|
|
464
|
+
measurements are in `docs/CACHE.md`. Key facts:
|
|
465
|
+
|
|
466
|
+
- 53 compactions in `session-0b094f6e…` removed 698,108 tokens; 34 were one-node passes.
|
|
467
|
+
- One request read `cacheRead 733,184`; the request after a 4k compaction read
|
|
468
|
+
`input 733,414 / cacheRead 8,704` — one pass invalidated a 733k-token prefix.
|
|
469
|
+
- `CACHE_FIRST_POLICY` sets `setpoint = high = 0.86`, `low = 0.62`, `maxPerBatch = 190_000`,
|
|
470
|
+
`minAction = 24_000`, `protect = 0.30`, `emergency = 0.96 → 0.80` (0.2.1; was 0.88). At
|
|
471
|
+
`W = 792,576` that is a 681,615 trigger, a 491,397 drain, and a 237,773 protected tail;
|
|
472
|
+
698,108 / 190,000 ≈ 4 passes.
|
|
473
|
+
- `setpoint = high` removes the proportional-zone passes. `CACHE_GRADUAL_POLICY` (setpoint 0.72)
|
|
474
|
+
is the smoother, lower-cache variant.
|
|
475
|
+
- The preset pins `policy.setpoint: 0.86` and (0.2.1) `policy.emergencyTo: 0.80`. A plugin-manager
|
|
476
|
+
remount at 18:27:37 applied the setpoint pin live; the emergencyTo pin is what lets a remount
|
|
477
|
+
pick up the 0.2.1 fix without reloading the cached `policy.mjs`.
|
|
478
|
+
- Node's ESM module cache persists across a preset reconciliation: a hot remount re-reads the
|
|
479
|
+
preset config but **not** the edited `policy.mjs`. The explicit pin is what made cache-first
|
|
480
|
+
live without a process restart. A full restart is still required to load the `ctx.inject`
|
|
481
|
+
cold-start fix and any code-only default.
|
|
482
|
+
- Existing sessions keep their mounted revision; a restart, a bundle remount, or a fork mounts the
|
|
483
|
+
current policy.
|
|
484
|
+
|
|
485
|
+
## 13. `session_log` retrieval tool (0.2.0)
|
|
486
|
+
|
|
487
|
+
**Host read API.** The tool uses the exact-read face of `ctx.sessionQuery` (the base
|
|
488
|
+
`session-query-sqlite` row keeps exact reads even with `openAt: never`):
|
|
489
|
+
|
|
490
|
+
```ts
|
|
491
|
+
ctx.sessionQuery.readSession(sessionId): Promise<{
|
|
492
|
+
session: SessionHeader
|
|
493
|
+
inheritedEventCount: SessionLogOffset
|
|
494
|
+
events: SessionEvent[] // complete raw log, replay-validated
|
|
495
|
+
}>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
`readSession` is live-preferred: a live session is read from memory, a persisted one from JSONL.
|
|
499
|
+
It makes the session live? No — the docs say “without making it live”; it returns a cloned
|
|
500
|
+
snapshot. There is also `readEvent({sessionId, seq, before, after})` for a window, but the tool
|
|
501
|
+
reads one snapshot and formats locally so index/get/grep share one view.
|
|
502
|
+
|
|
503
|
+
**Registration.** `src/session-log-tool.mjs` exports `inject = ['tools', 'sessionQuery']` and
|
|
504
|
+
`apply`, which builds a plain `ToolDefinition` and calls `ctx.tools.register(tool)` in the
|
|
505
|
+
**calling (preset) scope**. The `preset-continuity` plugin list carries:
|
|
506
|
+
|
|
507
|
+
```yaml
|
|
508
|
+
- id: session-log-tool
|
|
509
|
+
name: dsh-continuity/session-log-tool
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
`preset-band` and `preset-standard` do **not** carry the row, so their agents see no `session_log`
|
|
513
|
+
and the tool registry reports it absent for those scopes. The tool needs no
|
|
514
|
+
`@deepseek-ai/dsh-tools` import (`defineTool` is a validation wrapper; a plain JSON-Schema
|
|
515
|
+
`ToolDefinition` is accepted), which keeps the `link:` install from needing a workspace peer.
|
|
516
|
+
|
|
517
|
+
**Seq contract.** `seq` is the session event seq, the same number in
|
|
518
|
+
`[gist · log:<session>#seq=A-B]`. `A-B` is a *surface* range and can be non-monotonic (for example
|
|
519
|
+
`3005-1872` after a replacement). `get` first looks for a `compaction/summary` whose
|
|
520
|
+
`shadowedRange` equals the requested range and then reads exactly its `shadowedSeqs` in surface
|
|
521
|
+
order; only an unmatched, ordered range falls back to a numeric filter. This is what lets the
|
|
522
|
+
0.1.0-era gists remain retrievable without rewriting their headers.
|
|
523
|
+
|
|
524
|
+
**Read-only / budget.** The tool calls only `readSession`; it never appends, replaces, projects, or
|
|
525
|
+
compacts. Output is bounded: `index` 12k chars, `get` 16k with `MAX_EVENT_CHARS 2400`, `grep` 12k;
|
|
526
|
+
`limit` default 20 (max 100), `context` max 10. A truncated result carries a manifest, the role
|
|
527
|
+
histogram, and a next seq or a “narrow with grep” instruction. `grep` excludes `system/message` so
|
|
528
|
+
the convention/tool descriptions cannot match; `get <seq>` can still read a system event.
|
|
529
|
+
|
|
530
|
+
**Scope verification.** `state.json` now carries `tools.sessionLog` (registration) and
|
|
531
|
+
`tools.scopes` (per live agent: current preset, `sessionLog`, `convention`, tool count). The probe
|
|
532
|
+
reads the **`agentPreset` projection**, not `session.header.agentPreset`: the header is the
|
|
533
|
+
creation fact, and `session-0b094f6e…` was created as `standard` then switched to `continuity`.
|
|
534
|
+
Using the header alone mislabels that session and skips the convention check. The projection is
|
|
535
|
+
authoritative.
|
|
536
|
+
|
|
537
|
+
**Live 0.2.0 state (before the required restart):** the host row re-imported as `0.2.0` and the
|
|
538
|
+
scope probe shows the long session as `preset: continuity`, `convention: true`, `sessionLog: false`
|
|
539
|
+
with 32 tools; the `session-log-tool` row is part of the preset declaration read at boot, so the
|
|
540
|
+
running process still has the old preset. A restart is required to mount it; after the restart the
|
|
541
|
+
same probe should read `sessionLog: true` and the tool count should increase by one. The retrieval
|
|
542
|
+
logic itself is exercised against the real log by `tools/acceptance-0.2.0.mjs`.
|
|
543
|
+
|
|
544
|
+
The exact-read Host API is already verified live: `state.json.sessionQuery` reports
|
|
545
|
+
`{ source: "ctx.sessionQuery.readSession", sessionId: "session-0b094f6e…", events: 3292,
|
|
546
|
+
firstSeq: 0, lastSeq: 3291 }`. That is the same `readSession` call the tool's `execute` makes;
|
|
547
|
+
only the preset row registration is waiting for the restart.
|
|
548
|
+
|
|
549
|
+
**Policy override answer.** The handoff's premise that `DEFAULT_POLICY.setpoint` is 0.72 is stale:
|
|
550
|
+
the source now exports `CACHE_FIRST_POLICY` as `DEFAULT_POLICY` with 0.86, and
|
|
551
|
+
`CACHE_GRADUAL_POLICY` is the 0.72 variant. The runtime 0.86 comes from both the source default and
|
|
552
|
+
the explicit `policy.setpoint: 0.86` pin in `cordis.patch.yml`. The pin is intentional (it made the
|
|
553
|
+
cache-first policy live across a hot remount and protects against a stale module default), not a
|
|
554
|
+
leftover. The two options for 0.2.0 are: keep 0.86 (one 190k pass per high crossing, fewer cache
|
|
555
|
+
invalidations, higher average context) or switch to 0.72 (24k proportional passes, smoother
|
|
556
|
+
context, more invalidations). The package ships 0.86; the switch is one config/policy line.
|
|
557
|
+
|
|
558
|
+
## 14. 0.2.0 acceptance record (real session log)
|
|
559
|
+
|
|
560
|
+
`tools/acceptance-0.2.0.mjs` decodes `session-0b094f6e…` (3,293 events, 54 gists) and runs the
|
|
561
|
+
same pure functions and ToolDefinition the model uses:
|
|
562
|
+
|
|
563
|
+
- `index` lists the newest gist `seq=3020 · surface 3005-1872 · 143 msgs · ~179,725 tok`.
|
|
564
|
+
- `get` with the pointer `[gist · log:… #seq=3005-1872 …]` resolves via
|
|
565
|
+
`compactionId b07faeb0-ca36-45c8-88f9-9bfcf1dca9b2` to exactly 143 events, returns a manifest
|
|
566
|
+
(`user:14 assistant:53 tool:76`) and truncates at 5 (`next seq=1222`).
|
|
567
|
+
- `get seq=1604 context=1` returns the raw user message containing
|
|
568
|
+
“鲸鳍不是尖刀/叶形——它是宽的扇面…”.
|
|
569
|
+
- `grep "鲸鳍不是尖刀"` returns seq 1604 (raw) plus the newer gist copies at seq 3020/3021; `get`
|
|
570
|
+
the match ±1 returns the original.
|
|
571
|
+
- `get seq=999999` fails with
|
|
572
|
+
`seq 999999 is not in this session (valid event seqs 0-3291). If the original is unavailable, say
|
|
573
|
+
you only remember the gist.`
|
|
574
|
+
- Frozen-event read-only test and `parsePointer` for plain ids, pointers, and full gist headers
|
|
575
|
+
pass in `test/session-log.test.mjs` (15 tests).
|
|
576
|
+
|
|
577
|
+
## 15. `emergencyTo` double-pass fix (0.2.1)
|
|
578
|
+
|
|
579
|
+
`emergencyTo` was 0.88, **above** `high = 0.86`. At 97% the emergency pass landed at 88% (697,467),
|
|
580
|
+
still above high, and the next batch immediately ran a second band-high pass to 62% — two prefix
|
|
581
|
+
invalidations and two summarization calls for one overshoot. 96% exactly was *better* because it
|
|
582
|
+
went straight to band-high.
|
|
583
|
+
|
|
584
|
+
0.2.1 sets `emergencyTo = 0.80` (and pins it in `preset-continuity`, like `setpoint`):
|
|
585
|
+
|
|
586
|
+
| At | One pass | After | Headroom to high 681,615 |
|
|
587
|
+
|---|---:|---:|---:|
|
|
588
|
+
| 97% (768,799) | emergency, budget 134,738 | 634,061 (80%) | 47,554 (6% W) |
|
|
589
|
+
| 96.1% (761,666) | emergency, budget 127,605 | 634,061 (80%) | 47,554 |
|
|
590
|
+
| 96% exactly (760,873) | band-high, budget 190,000 | 570,873 | 110,742 |
|
|
591
|
+
| 0.88 (old) at 97% | emergency 72,000, then band-high 190,000 | 570,873 after two passes | two invalidations |
|
|
592
|
+
|
|
593
|
+
Why 0.80: one emergency lands below high with a 6% window before band-high can fire again; it is
|
|
594
|
+
shallower than the old 0.88-plus-band-high chain and shallower than a band-high at 97% would be
|
|
595
|
+
(190k to 570,873). 0.85 leaves only 1% W (7,926 tokens) and re-triggers almost immediately; 0.62
|
|
596
|
+
would be one pass but a 277k cut. `high`, `low`, `protect`, `maxPerBatch`, `minAction`, `setpoint`,
|
|
597
|
+
`standard`, and Coo were not changed.
|
|
598
|
+
|
|
599
|
+
Tests: `test/policy.test.mjs` now has 14 cases, including exact-96% band-high and a just-over-96%
|
|
600
|
+
emergency that must leave `after ≤ high − 0.05W`; the suite is 46 tests total. The policy pin also
|
|
601
|
+
makes the value visible in `state.json` after a remount even while the cached module still carries
|
|
602
|
+
0.88. Live check on the 5276 Host after the 0.2.1 pin remount: `engine.policy.emergencyTo = 0.80`,
|
|
603
|
+
`tools.sessionLog.name = session_log`, continuity `sessionLog: true` / `convention: true` /
|
|
604
|
+
`toolCount: 33`, `sessionQuery.events = 4776`; `plugin.version` is still `0.2.0` because the host
|
|
605
|
+
row's package.json require is cached, so a full restart is needed to display `0.2.1`. The 0.2.1 tgz
|
|
606
|
+
carries the new version, code, pin, and docs.
|
|
607
|
+
|
|
608
|
+
## 16. 0.2.3 — surface stats, A4-safe, summary view
|
|
609
|
+
|
|
610
|
+
### Stats口径
|
|
611
|
+
|
|
612
|
+
`session_log stats` now defaults to `scope=surface`, reads `session.surface.nodes` on the live
|
|
613
|
+
session, and uses `tokenMeter.measure(session)` for route-priced tokens:
|
|
614
|
+
|
|
615
|
+
- `surfaceTokens` is the model-visible message surface; `baseline` (system/tool schema/request
|
|
616
|
+
overhead) is reported separately; `totalTokens = surfaceTokens + baseline`.
|
|
617
|
+
- Categories: `reasoning`, `tool_result`, `assistant_text`, `tool_call`, `user`, `system`,
|
|
618
|
+
`developer`, `gist` (raw only).
|
|
619
|
+
- `tool_result` is `tool/result` content; `tool_call` is assistant tool-call arguments; the
|
|
620
|
+
`tool/call` marker is not double-counted. Within an assistant event, the event's route-priced
|
|
621
|
+
token count is split across reasoning/text/tool-call by chars, so those three token rows are a
|
|
622
|
+
split of an exact event total.
|
|
623
|
+
- `tools_schema` is a separate baseline row (33 tools / 31,115 chars / ~7,779 tokens in the
|
|
624
|
+
2026-10-26 session).
|
|
625
|
+
- `tail` walks `tokenMeter` nodes from the end to `protect × W`; `protect 0.30` is a fraction
|
|
626
|
+
(237,773 tokens at W=792,576), not 30 tokens. `scope=raw` remains available and is labelled
|
|
627
|
+
`chars4 estimate`; it includes historical/shadowed content and must not be read as the context.
|
|
628
|
+
|
|
629
|
+
Measured on `session-0b094f6e…` before the A4 change:
|
|
630
|
+
|
|
631
|
+
- current surface (584 nodes, 1,251,664 text chars): assistant 86.8%, tool result 8.8%, user 3.9%,
|
|
632
|
+
system 0.5% by chars; block proxy reasoning ~78%.
|
|
633
|
+
- the last cache-first pass (183,383 tokens, 728,081 chars): reasoning 80.0%, tool result 11.0%,
|
|
634
|
+
tool-call 4.4%, user 2.7%, assistant text 1.9%; 46% of the reasoning was in assistant messages
|
|
635
|
+
that also carried a tool call.
|
|
636
|
+
- 10 v4 sessions: tool result 8.8–81.6% of surface chars, aggregate 31.0%; assistant 66.0%, user
|
|
637
|
+
1.8%. The Continuity session is reasoning-heavy; coding sessions are tool-heavy.
|
|
638
|
+
|
|
639
|
+
### A4-safe
|
|
640
|
+
|
|
641
|
+
- Active call ids: every `tool/call` in the current open turn plus every call with no matching
|
|
642
|
+
`tool/result`; pins are the assistant tool-call surface messages and their tool results.
|
|
643
|
+
- `applyToolPreviews` walks non-active `tool/result` nodes oldest-first, builds a 1,280-char
|
|
644
|
+
head/marker/tail preview, and estimates `estimateMessage(original) - estimateMessage(preview)`.
|
|
645
|
+
The target is **`low` (62%)** for `band-high` and **`emergencyTo` (80%)** for `emergency`, not
|
|
646
|
+
`high`: it applies the chosen previews even when they cannot cover the whole gap, and the
|
|
647
|
+
remaining pass then continues to that target even if the estimate is already below `high`.
|
|
648
|
+
Stopping at `high` was the 0.2.2 bug: it left the context hovering at 86% and invalidated the
|
|
649
|
+
prefix on every subsequent batch. The full compression now produces one 24%-W band per high
|
|
650
|
+
crossing.
|
|
651
|
+
- If the preview already reaches the target, no pass runs. If it cannot, the pass folds the oldest
|
|
652
|
+
user/reasoning only then; active pins stay full. `band-high` keeps the 190k pass cap, `emergency`
|
|
653
|
+
keeps its normal uncapped drain; the preview itself is not a pass and is not capped by
|
|
654
|
+
`maxPerBatch`.
|
|
655
|
+
- Applied previews append a `compaction/prune` shadow-price event and a replacement `tool/result`
|
|
656
|
+
with `surfaceOp: replace`; the raw event remains in the log and the marker cites
|
|
657
|
+
`session_log get seq=<n>`. User/assistant/reasoning text is untouched. Only text-only tool
|
|
658
|
+
results are previewed; mixed image/file results are skipped.
|
|
659
|
+
- Active pins count toward the `oldestRange` tail floor, so the pass stops before the oldest active
|
|
660
|
+
pin; if no range fits with an active pin, pressure compaction is skipped rather than folding it.
|
|
661
|
+
- `protect 0.30`, `high 0.86`, `low 0.62`, `maxPerBatch 190000`, `minAction 24000`, `setpoint 0.86`
|
|
662
|
+
are unchanged; A4 is a pre-pass preview, not a policy change.
|
|
663
|
+
|
|
664
|
+
### B — summary view
|
|
665
|
+
|
|
666
|
+
`summarize()` copies `SummarizationInput.messages` and previews only `role: 'tool'` long text with
|
|
667
|
+
the same 1,280-char cap; human/assistant/reasoning messages stay verbatim. The preview marker cites
|
|
668
|
+
the gist header so the summary model can `session_log get` the range. This changes the summary
|
|
669
|
+
call's input, not the surface, tail, cache, or pass count. A4 and B overlap on the same tool
|
|
670
|
+
results: after A4 has previewed the pass range, B sees the already-short preview and adds little;
|
|
671
|
+
B remains the fallback when A4 is skipped and a quality layer for the gist.
|
|
672
|
+
|
|
673
|
+
### Tests / version
|
|
674
|
+
|
|
675
|
+
`test/tool-preview.test.mjs` covers active call/pin detection, preview head/tail/marker,
|
|
676
|
+
non-text and short skips, frozen replacement, summary-view copy, and the low/emergency target
|
|
677
|
+
budget. `test/stats.test.mjs` covers block classification, token splitting, tail walking,
|
|
678
|
+
live-surface vs raw, and the tail output. 0.2.3 is 61 tests total (policy 14, provenance 8, range 9,
|
|
679
|
+
session-log 15, tool-preview 9, stats 6). The `session_log` tool injects `tokenMeter`; `state.json`
|
|
680
|
+
now also carries `context` and `previews` for verification.
|
|
681
|
+
|
|
682
|
+
## 17. 0.2.4 — 3-turn active window, non-text handles, overflow pause
|
|
683
|
+
|
|
684
|
+
### B: active window
|
|
685
|
+
|
|
686
|
+
`activeCallIds(events, { recentTurns: 3 })`: every call in the current open turn, plus every
|
|
687
|
+
unpaired call whose `turn` is in the last 3 turn numbers. Older unmatched calls are treated as
|
|
688
|
+
abandoned project debris and do not pin the tail. This replaces the 0.2.3 global unmatched set,
|
|
689
|
+
which could pin a very old interrupted call forever. Tests cover the 3-turn boundary.
|
|
690
|
+
|
|
691
|
+
### D: non-text handles
|
|
692
|
+
|
|
693
|
+
At every `compactIfNeeded` entry (pressure and overflow), `handleNonTextResults()` walks the
|
|
694
|
+
surface, finds `tool/result` nodes with image/file/audio/video blocks, and replaces the whole
|
|
695
|
+
message with a text handle:
|
|
696
|
+
|
|
697
|
+
```text
|
|
698
|
+
[tool result handle · seq=N · tool=... · call=... · types=image/png · name=... · bytes=...
|
|
699
|
+
path=...]
|
|
700
|
+
topic / function / importance / caveats / summary
|
|
701
|
+
original archived at seq=N; ...
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The payload remains in the raw log; it is not in the model context. `nonTextInfo()` is
|
|
705
|
+
deterministic. The semantic fields are produced by one auxiliary `summarize()` call with the
|
|
706
|
+
non-text content in a user message and a structured prompt; unsupported content or an auxiliary
|
|
707
|
+
failure falls back to metadata, with `importance: unknown` (or `high (error)`) rather than a
|
|
708
|
+
fabricated judgment. The handle keeps the same call id, so an active non-text result remains
|
|
709
|
+
active/pinned; a non-active handle may be folded later, with its summary preserved in the gist.
|
|
710
|
+
`session_log get` cannot re-inject binary; the handle carries the attachment path when present.
|
|
711
|
+
|
|
712
|
+
### A: active-overflow pause
|
|
713
|
+
|
|
714
|
+
`context-overflow` is the only provider-confirmed path that may touch an active result. After D,
|
|
715
|
+
`handleOverflow()` finds the largest active tool result. If its estimated tokens are at least the
|
|
716
|
+
measured overage, it replaces it with a `[continuity pause]` text handle (same call id, still
|
|
717
|
+
active). The handle tells the instance to stop retrying/compressing, tell the user the context
|
|
718
|
+
window is too small, and ask for a larger one; the raw original is at `seq=N`. The base
|
|
719
|
+
`agent/request-error` listener sees the surface progress and retries once; if the result was not
|
|
720
|
+
the cause, the base overflow fallback runs. Normal pressure/band paths never detach an active
|
|
721
|
+
result.
|
|
722
|
+
|
|
723
|
+
### C: raw overlap
|
|
724
|
+
|
|
725
|
+
`stats scope=raw` includes both a `compaction/summary` event and its replacement user message; the
|
|
726
|
+
raw gist/user shares can be inflated by roughly 10 points. This is archive-only and does not enter
|
|
727
|
+
model context; surface stats remain authoritative. The raw output now says
|
|
728
|
+
`historical; may overlap summaries; not context`.
|
|
729
|
+
|
|
730
|
+
Tests: 0.2.4 is 65 tests total (policy 14, provenance 8, range 9, session-log 15, tool-preview 13,
|
|
731
|
+
stats 6). `test/tool-preview.test.mjs` now covers the 3-turn boundary, non-text handle/pause text,
|
|
732
|
+
original immutability, and call metadata. The `state.json` FACTS retrieval row includes `stats`.
|
|
733
|
+
|
|
734
|
+
## 18. 0.2.5 — handle re-preview fix, post-pass measurement, manual labels
|
|
735
|
+
|
|
736
|
+
### A4 must not preview a handle/pause/preview
|
|
737
|
+
|
|
738
|
+
`isNonPreviewableBlocks(blocks)` rejects text containing `[tool result handle`,
|
|
739
|
+
`[continuity pause`, or `[... tool result preview`. `previewToolResultEvent` and
|
|
740
|
+
`previewSummaryInput` both return null/unchanged for those messages. Before 0.2.5, a D semantic
|
|
741
|
+
handle (~1,700 chars) could be truncated by A4; the summary/importance/path were cut and the A4
|
|
742
|
+
marker pointed at the handle's replacement seq instead of the original non-text event. Handles and
|
|
743
|
+
pauses are now terminal representations.
|
|
744
|
+
|
|
745
|
+
### Post-pass measurement
|
|
746
|
+
|
|
747
|
+
After `compactRegion()` succeeds, the engine calls `measure()` again and writes `afterTokens`,
|
|
748
|
+
`afterSurfaceTokens`, `afterBaselineTokens`, and `rebloatTokens` into the pass record. The panel's
|
|
749
|
+
"after" is now a real measurement; `state.context` updates immediately. A real post-pass re-bloat is
|
|
750
|
+
visible without waiting for the next pressure trigger. `measure()` failure is recorded as a warning;
|
|
751
|
+
the pass record still exists.
|
|
752
|
+
|
|
753
|
+
### Manual `/compact` labeling
|
|
754
|
+
|
|
755
|
+
`compactNow()` still delegates to the base manual cut (it can be much deeper than the 62% band), but
|
|
756
|
+
after it settles 0.2.5:
|
|
757
|
+
|
|
758
|
+
1. appends a `[gist · log:…#seq=A-B · N messages · original retrievable via session_log]` header as
|
|
759
|
+
a plugin `user/message` after the summary;
|
|
760
|
+
2. measures the post-pass surface;
|
|
761
|
+
3. records a `recordPass` with `reason: 'manual'`, `budget: 0`, the header, and the after/re-bloat
|
|
762
|
+
fields.
|
|
763
|
+
|
|
764
|
+
The base summary itself is unchanged and still unlabeled; the follow-up header is the pointer. This
|
|
765
|
+
makes a manual cut visible in `passes` and retrievable by the model.
|
|
766
|
+
|
|
767
|
+
Tests: 0.2.5 is 66 tests total (policy 14, provenance 8, range 9, session-log 15, tool-preview 14,
|
|
768
|
+
stats 6). `test/tool-preview.test.mjs` adds the handle/pause/preview non-repreview case. The engine
|
|
769
|
+
integration (post-pass/manual header) is checked with `node --check` and the live/state path rather
|
|
770
|
+
than a base-engine unit test.
|
|
771
|
+
|
|
772
|
+
## 19. 0.2.6 — 515k hard-cap policy
|
|
773
|
+
|
|
774
|
+
`CACHE_FIRST_POLICY` changes from the cache-first 86/62/96→80 band to the 515k hard-cap band:
|
|
775
|
+
|
|
776
|
+
```text
|
|
777
|
+
setpoint 0.62 → 491,397 (46.9% full)
|
|
778
|
+
high 0.62 → 491,397
|
|
779
|
+
low 0.42 → 332,882 (31.7% full)
|
|
780
|
+
emergency 0.65 → 515,174 (49.1% full)
|
|
781
|
+
emergencyTo 0.45 → 356,659 (34.0% full)
|
|
782
|
+
protect 0.30 → 237,773
|
|
783
|
+
maxPerBatch 190000
|
|
784
|
+
minAction 24000
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
The full context is 1,048,576; 515,174 is **49.1% full / 65% of W**. The emergency trigger is the
|
|
788
|
+
515k cap, not the old 96% near-overflow line. `planFold` uses strict `>` for emergency: exactly
|
|
789
|
+
0.65 still takes one band-high pass to low; only above 0.65 is the emergency path. `emergencyTo`
|
|
790
|
+
0.45 is below high 0.62, so one emergency cannot chain into band-high.
|
|
791
|
+
|
|
792
|
+
`CACHE_GRADUAL_POLICY` keeps the same water marks with `setpoint: 0.50` for a 50–62% proportional
|
|
793
|
+
zone (the old 0.72 setpoint is invalid above high 0.62).
|
|
794
|
+
|
|
795
|
+
Tests remain 66; `test/policy.test.mjs` now asserts the 61/62 no-op boundary, the 0.64 band-high
|
|
796
|
+
drain, the 0.65 still-band-high boundary, the 0.45 emergency target, and the 0.50 gradual setpoint.
|
|
797
|
+
The preset pins are `setpoint: 0.62` and `emergencyTo: 0.45` in `cordis.patch.yml`; the other
|
|
798
|
+
fields inherit from `src/policy.mjs`.
|
|
799
|
+
|
|
800
|
+
## 20. 0.2.7 — 92% high/emergency ratio
|
|
801
|
+
|
|
802
|
+
0.2.7 keeps the 515k emergency cap and moves high/low down:
|
|
803
|
+
|
|
804
|
+
```text
|
|
805
|
+
setpoint 0.60 → 475,546 (45.4% full)
|
|
806
|
+
high 0.60 → 475,546
|
|
807
|
+
low 0.40 → 317,030 (30.2% full)
|
|
808
|
+
emergency 0.65 → 515,174 (49.1% full)
|
|
809
|
+
emergencyTo 0.42 → 332,882 (31.8% full)
|
|
810
|
+
protect 0.30 → 237,773
|
|
811
|
+
maxPerBatch 190000
|
|
812
|
+
minAction 24000
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
`high / emergency = 0.60 / 0.65 = 92.3%`, leaving **39,628 tokens (5% W)** between the normal
|
|
816
|
+
trigger and the hard cap. The previous 0.62/0.65 ratio left only 23,777 (3% W), below
|
|
817
|
+
`minAction 24k`; 0.2.7 crosses that structural boundary. The band is still 158,515 (20% W), and the
|
|
818
|
+
emergency band is 182,292, within `maxPerBatch`.
|
|
819
|
+
|
|
820
|
+
The exact boundary is unchanged: at exactly 0.65 the pass is still band-high and is capped at
|
|
821
|
+
190,000, so it may take two passes to reach 0.40; above 0.65 the emergency pass ignores the cap and
|
|
822
|
+
lands at 0.42.
|
|
823
|
+
|
|
824
|
+
`CACHE_GRADUAL_POLICY` remains `setpoint: 0.50`; its proportional zone is now 50–60% (valid below
|
|
825
|
+
high 0.60). The preset pins are `setpoint: 0.60` and `emergencyTo: 0.42`.
|
|
826
|
+
|
|
827
|
+
Tests: 66 total. Policy tests assert the 59/60 no-op boundary, the 0.62 band-high drain, the
|
|
828
|
+
0.65 band-high cap, the 0.42 emergency target, and the 0.50 gradual setpoint.
|
|
829
|
+
|
|
830
|
+
## 21. 0.3.0 — explicit work threads
|
|
831
|
+
|
|
832
|
+
### State
|
|
833
|
+
|
|
834
|
+
`~/.dsh/continuity/threads.json`, partitioned by session:
|
|
835
|
+
|
|
836
|
+
```json
|
|
837
|
+
{
|
|
838
|
+
"version": 1,
|
|
839
|
+
"sessions": {
|
|
840
|
+
"<session>": {
|
|
841
|
+
"activeId": "t-…",
|
|
842
|
+
"threads": [{
|
|
843
|
+
"id": "t-…", "label": "…", "why": "…", "next": "…",
|
|
844
|
+
"seqStart": 1, "seqEnd": 2, "seqs": [1, 2], "count": 2,
|
|
845
|
+
"spanStart": "…", "spanEnd": "…", "minutes": 1, "tokens": 2,
|
|
846
|
+
"residue": "…", "archiveRef": "log:<id>#seq=1-2",
|
|
847
|
+
"gistSeq": 3, "compactionId": "…", "active": false,
|
|
848
|
+
"loadSeq": 4, "injected": 2, "loadTokens": 2
|
|
849
|
+
}],
|
|
850
|
+
"pending": { "kind": "save" }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
Writes are atomic (`threads.json.<pid>.<time>.tmp` → rename) and serialized. The rules file
|
|
857
|
+
`~/.dsh/continuity/rules.md` is read-only; it is never created, written, or defaulted. A missing
|
|
858
|
+
file means no rules.
|
|
859
|
+
|
|
860
|
+
### Tool
|
|
861
|
+
|
|
862
|
+
A single `continuity` tool with actions `save`, `load`, `active`, `list` (provider function names
|
|
863
|
+
cannot contain dots; these are the contract's `continuity.save/load/active/list`). `save` requires
|
|
864
|
+
`label`, `why`, and `next`; `load` requires `id`; `budgetTokens` defaults to 8,000 and is capped at
|
|
865
|
+
20,000.
|
|
866
|
+
|
|
867
|
+
### Save: validate, defer, commit
|
|
868
|
+
|
|
869
|
+
`prepareSave()` runs in the tool call and writes a `pending` record; it does not append. It:
|
|
870
|
+
|
|
871
|
+
1. accepts only surface seqs, never the system head, never a gist/thread node, never a node at or
|
|
872
|
+
after the current `turn/start`;
|
|
873
|
+
2. requires both endpoints to be `toolPairingBalancedBefore/After`; a cut is rejected with the seq;
|
|
874
|
+
3. defaults to after the latest gist and before the current open turn, walking inward to balanced
|
|
875
|
+
edges (`adjusted: true` when it had to move);
|
|
876
|
+
4. prices the range, computes the real-clock span/minutes, and creates the thread id/archiveRef.
|
|
877
|
+
|
|
878
|
+
At the next pre-step, after the tool result, `saveThread()` calls the inherited `compactRegion()`
|
|
879
|
+
with a one-shot thread summary. The base transaction appends `compaction/start`, the summary, the
|
|
880
|
+
replacement `user/message` with `surfaceOp` and `sourceEventSeqs`, and `compaction/end`. The engine
|
|
881
|
+
then appends a `compaction/prune` price marker, records the thread, clears `activeId`, and clears
|
|
882
|
+
`pending`. This is the same host validation as an automatic gist: the raw range is never rewritten,
|
|
883
|
+
and the fold is append-only.
|
|
884
|
+
|
|
885
|
+
The gist body starts with `[thread] … 下一步:…` and carries a second `[gist · log:…]` line so
|
|
886
|
+
`session_log index` still recognizes it.
|
|
887
|
+
|
|
888
|
+
### Load: quote the newest raw segment
|
|
889
|
+
|
|
890
|
+
The tool reads `sessionQuery.readSession`, resolves the thread's exact `seqs` (surface order, not a
|
|
891
|
+
numeric range), and selects newest-to-oldest until `budgetTokens`; the last event is always included,
|
|
892
|
+
so the count is a lower bound. It writes a pending load and sets `activeId`, without appending. At
|
|
893
|
+
the next pre-step the engine appends one normal `user/message`:
|
|
894
|
+
|
|
895
|
+
```
|
|
896
|
+
[放] <label> · 现在算在「<id>」这条线上 · 下一步:<next> · 上次收:<why> · 存档:<ref>
|
|
897
|
+
—— 以下是从原文取回的最后一段 ——
|
|
898
|
+
[<role>] <raw text>
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
No original node ids are copied. Missing raw, or a missing seq, produces the index-only line and
|
|
902
|
+
`{ ok:false, reason:"raw-missing", active:true, pending:true }`; it does not invent text.
|
|
903
|
+
|
|
904
|
+
### Per-turn injection and pins
|
|
905
|
+
|
|
906
|
+
At the first pre-step of each turn (`agent.lastTurn`, else the latest `turn/start` seq), the engine
|
|
907
|
+
appends one `user/message` with:
|
|
908
|
+
|
|
909
|
+
```
|
|
910
|
+
[线] 当前:<label>(已载入)· 下一步:<next>
|
|
911
|
+
已收起 N 条:<labels>
|
|
912
|
+
<rules.md verbatim, ≤400 estimated tokens>
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
This is after the surface tail, never in the system/tool prefix; it is a user message because the DSH
|
|
916
|
+
surface has no developer role. Rules are re-read every turn; >400 tokens is truncated with a warning.
|
|
917
|
+
The line is capped at 200 estimated tokens. The same turn is injected once; the next turn re-reads.
|
|
918
|
+
|
|
919
|
+
After a load, `loadSeq` joins the active tool pins. `oldestRange()` counts from the oldest pin to the
|
|
920
|
+
end as the tail floor, so automatic compaction stops before the loaded quote. A save clears
|
|
921
|
+
`activeId`, making the old thread foldable.
|
|
922
|
+
|
|
923
|
+
Stage switches live in `continuity-engine.config.threads`: `inject` (stage 2) and `pin` (stage 3),
|
|
924
|
+
both default true. Stage 1 (tool + state) is always present. Disabling one stage does not rewrite
|
|
925
|
+
the session log; disabling the tool row returns to 0.2.7 behavior and leaves `threads.json` inert.
|
|
926
|
+
|
|
927
|
+
### Why the commit is deferred
|
|
928
|
+
|
|
929
|
+
Appending a gist/quote inside the save/load tool call would put a `user/message` between the
|
|
930
|
+
assistant tool-call and its tool result, exactly the pairing failure described in
|
|
931
|
+
`CONTINUITY-THREADS.md` trap 1. The tool therefore returns `pending:true`; the next pre-step commits.
|
|
932
|
+
|
|
933
|
+
Tests: 84 total (threads adds 18). `test/threads.test.mjs` covers explicit cut rejection, current-turn
|
|
934
|
+
exclusion, gist start, default balanced adjustment, load budget/missing raw, active/list/pins,
|
|
935
|
+
A→B→A switching, per-session persistence, read-only rules, the `makeApi()` facade, tool registration,
|
|
936
|
+
and the no-append pending path.
|
|
937
|
+
|
|
938
|
+
## 22. 0.3.1 — producer-owned source kind hotfix
|
|
939
|
+
|
|
940
|
+
Live 0.3.0 failed its first turn with:
|
|
941
|
+
|
|
942
|
+
```
|
|
943
|
+
format v4 message requires a producer-owned source kind
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
The thread injection and load quote used `source: { kind: "plugin", plugin: "dsh-continuity" }`.
|
|
947
|
+
The v4 session format only accepts producer-owned source kinds; an unregistered plugin name is not
|
|
948
|
+
one (the base compaction checkpoint uses the registered `plugin: "compact"` marker). The rejected
|
|
949
|
+
append aborted the turn before an assistant message.
|
|
950
|
+
|
|
951
|
+
0.3.1 changes every injected message to `source: { kind: "user" }`:
|
|
952
|
+
|
|
953
|
+
- active/rules line (`injectThreadContext`);
|
|
954
|
+
- load quote (`processPendingThread`);
|
|
955
|
+
- manual `/compact` follow-up gist header (`compactNow`);
|
|
956
|
+
- the semantic-summary user input (`semanticSummaryFor`).
|
|
957
|
+
|
|
958
|
+
The `[thread]` gist replacement itself is still created by the base `compactRegion` transaction with
|
|
959
|
+
`compactCheckpointSource` (`plugin: "compact"`), so the gist provenance is unchanged.
|
|
960
|
+
|
|
961
|
+
Tests: 85 total (threads 19). Added `contextMessage` source-kind assertion. Reinstall 0.3.1 and
|
|
962
|
+
restart; the failed turn itself left no invalid event in the raw log (the append was rejected before
|
|
963
|
+
persistence), so no session repair is needed.
|
|
964
|
+
|
|
965
|
+
## 23. 0.3.2 — post-replacement prune removal and runtime-context source
|
|
966
|
+
|
|
967
|
+
### The corrupt prune
|
|
968
|
+
|
|
969
|
+
0.3.0/0.3.1 `saveThread()` appended this after `compactRegion()`:
|
|
970
|
+
|
|
971
|
+
```js
|
|
972
|
+
session.append('compaction/prune', {
|
|
973
|
+
shadowedRange: { start: draft.seqStart, end: draft.seqEnd },
|
|
974
|
+
shadowedSeqs: [...draft.seqs],
|
|
975
|
+
shadowedTokenCount: result.shadowedTokenCount,
|
|
976
|
+
});
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
By then the summary replacement had already shadowed that range. v4 replay rejected it:
|
|
980
|
+
|
|
981
|
+
```
|
|
982
|
+
compaction/prune shadowedSeqs do not name an exact current surface span
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
0.3.2 removes the append. The base `compaction/summary` already records
|
|
986
|
+
`shadowedRange`/`shadowedSeqs`/`shadowedTokenCount`; A4's prune remains immediately before its own
|
|
987
|
+
tool-result replacement, where its single seq is still current surface.
|
|
988
|
+
|
|
989
|
+
### Live repair
|
|
990
|
+
|
|
991
|
+
The invalid event was live session seq 10762 (the save at seq 10755). The original raw log was
|
|
992
|
+
backed up byte-for-byte before repair:
|
|
993
|
+
|
|
994
|
+
```
|
|
995
|
+
backups/session-0b094f6e-…-20261027-221254.v4.jsonl.zstd
|
|
996
|
+
SHA256 14BD80428DDABCB3A6F1EEE67AD21D94B21B7E51AF61F1C5543759A106E38BEB
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
The repair keeps the event and its seq (no renumbering) and retargets the prune to the replacement
|
|
1000
|
+
checkpoint seq 10760 with `shadowedTokenCount: 0`; the replacement is a current surface node, so the
|
|
1001
|
+
format check passes and the price is a no-op.
|
|
1002
|
+
|
|
1003
|
+
The first repair decompressed all 5,939 zstd frames and recompressed them as **one** frame. That
|
|
1004
|
+
kept the JSONL but merged frame 0 — the **session header** — with the events. The session store
|
|
1005
|
+
reads frame 0 as the header, so the session disappeared from the list. The final repair preserves
|
|
1006
|
+
the frame structure: every original frame is copied byte-for-byte except the one frame containing
|
|
1007
|
+
seq 10762; frame 0 is untouched.
|
|
1008
|
+
|
|
1009
|
+
```
|
|
1010
|
+
source frames 5,939
|
|
1011
|
+
repaired frames 5,939
|
|
1012
|
+
frame 0 {"type":"session",…}
|
|
1013
|
+
events 10,910
|
|
1014
|
+
invalid prunes 0
|
|
1015
|
+
surface tail 10884…10906
|
|
1016
|
+
size 10,868,579
|
|
1017
|
+
SHA256 92159A24E888CB412D1D45A8CFC5ED5FBFA03CB84F8FA5A4F1F0A1BD8057995A
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
The live file now has that frame-preserving hash; the original and the intermediate single-frame
|
|
1021
|
+
repaired copy remain in `backups/` for forensics. The `.repaired.v4.jsonl` is the full logical log.
|
|
1022
|
+
|
|
1023
|
+
### Source kind
|
|
1024
|
+
|
|
1025
|
+
0.3.1's `{kind:"user"}` passed v4 but rendered the per-turn rules as a human chat bubble. 0.3.2 uses
|
|
1026
|
+
the registered runtime-context source:
|
|
1027
|
+
|
|
1028
|
+
```json
|
|
1029
|
+
{ "kind": "runtime-context", "form": "snapshot",
|
|
1030
|
+
"sections": [{ "name": "continuity:threads", "text": "…" }] }
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
The content and section text are identical. The UI renders it as a context block, not as the user
|
|
1034
|
+
typing. The semantic-summary input remains an in-memory `{kind:"user"}` message and is never
|
|
1035
|
+
persisted.
|
|
1036
|
+
|
|
1037
|
+
Tests: 85 total (threads 19). The engine smoke additionally asserts that `saveThread` writes no
|
|
1038
|
+
`compaction/prune` at all.
|
|
1039
|
+
|
|
1040
|
+
## 24. 0.3.3 — self-authored rules attribution
|
|
1041
|
+
|
|
1042
|
+
0.3.2 already made the injection **once per turn**, keyed by `agent.lastTurn` / the latest
|
|
1043
|
+
`turn/start` seq (`threadInjectTurn`), not once per model step. It is a `runtime-context` snapshot,
|
|
1044
|
+
not a `user/message`.
|
|
1045
|
+
|
|
1046
|
+
0.3.3 adds an explicit attribution between the active line and the rules:
|
|
1047
|
+
|
|
1048
|
+
```text
|
|
1049
|
+
[习惯] 下面是你自己写在 ~/.dsh/continuity/rules.md 的习惯,不是用户的新指令,也不是系统要求;
|
|
1050
|
+
它不是任务,你可以按自己的判断采用、忽略或改写它。
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
`threadContextText(line, rulesText)` builds the block; `RULES_ATTRIBUTION` is the constant. This
|
|
1054
|
+
matters because the rules file is authored by the instance, not by the human: the model should not
|
|
1055
|
+
read it as a fresh external demand or a system invariant. The plugin still never writes the file.
|
|
1056
|
+
|
|
1057
|
+
The block is capped at 200 estimated tokens for the line and 400 for the rules; the whole block is
|
|
1058
|
+
appended after the surface tail, so it does not touch the static prefix. If the instance still feels
|
|
1059
|
+
the block is heavy, `continuity-engine.config.threads.inject: false` disables stage 2 while leaving
|
|
1060
|
+
save/load/active/list and the rules file intact.
|
|
1061
|
+
|
|
1062
|
+
Tests: 86 total (threads 20). Added the attribution/source assertions.
|
|
1063
|
+
|
|
1064
|
+
## 25. 0.3.4 — attach a save to an existing thread
|
|
1065
|
+
|
|
1066
|
+
`save` gains optional `threadId`. The engine never decides whether two ranges belong together; the
|
|
1067
|
+
caller/rules decide and pass the id.
|
|
1068
|
+
|
|
1069
|
+
- Omitted: new thread, generated id, `activeId: null` (unchanged).
|
|
1070
|
+
- Known: the range becomes a new segment of that thread and `activeId` points at it.
|
|
1071
|
+
- Unknown: `continuity.save: unknown threadId <id>` before the range is priced/compacted. There is no
|
|
1072
|
+
pending, gist, or state write.
|
|
1073
|
+
|
|
1074
|
+
### Shape
|
|
1075
|
+
|
|
1076
|
+
`thread.segments[]` holds `{fromSeq, toSeq, seqs, count, tokens, why, next, residue?, at, spanStart,
|
|
1077
|
+
spanEnd, minutes, archiveRef, gistSeq?, compactionId?}`. Thread-level `why/next/seqs/count/tokens/
|
|
1078
|
+
archiveRef/gistSeq` follow the **latest** segment; `seqStart`/`spanStart` stay the first segment and
|
|
1079
|
+
`seqEnd`/`spanEnd` the latest. `tokens` is the thread total. Old entries without `segments` normalize
|
|
1080
|
+
to exactly one segment in `threads-store.getSession()` and are rewritten with segments on the next
|
|
1081
|
+
`updateSession()`; no old thread is dropped.
|
|
1082
|
+
|
|
1083
|
+
### Receipts
|
|
1084
|
+
|
|
1085
|
+
```
|
|
1086
|
+
saved as T5 · seq 1200..1310 · 12.3k tokens
|
|
1087
|
+
saved into T3 · segment 2 · seq 1310..1420 · 9.8k tokens
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
The tool result also carries `mode`, `segment`, `segments`, and `active`. The durable gist is still
|
|
1091
|
+
committed at the next pre-step; no post-replacement prune.
|
|
1092
|
+
|
|
1093
|
+
### list / active / load
|
|
1094
|
+
|
|
1095
|
+
`listView`/`activeView` add `segments`, `lastAt`, and `display`
|
|
1096
|
+
(`T3 · 2 段 · 最近 21:40`). `load` keeps whole-thread semantics but quotes the latest segment,
|
|
1097
|
+
carries `segment`/`segments`, and its header says `第2/2段`; the index-only fallback does the same.
|
|
1098
|
+
|
|
1099
|
+
Tests: 89 total (threads 23): new, append, unknown-id-no-write, old one-range compatibility, and the
|
|
1100
|
+
existing load test now uses two segments and asserts the latest. The real-engine smoke performs two
|
|
1101
|
+
saves through the Core, commits through the engine, and asserts one thread with two segments,
|
|
1102
|
+
`activeId` on it, the append receipt, and unknown-id no-write.
|
|
1103
|
+
|
|
1104
|
+
## 26. 0.3.5 — scan and range/select load
|
|
1105
|
+
|
|
1106
|
+
### scan
|
|
1107
|
+
|
|
1108
|
+
`continuity.scan({fromSeq, toSeq, headChars?, includeReasoning?, types?, limit?})` is read-only and
|
|
1109
|
+
does not inject. It returns:
|
|
1110
|
+
|
|
1111
|
+
```json
|
|
1112
|
+
[{ "seq": 1030, "role": "assistant", "type": "message",
|
|
1113
|
+
"head": "**能出图。** …", "chars": 1190, "tokens": 605 }]
|
|
1114
|
+
```
|
|
1115
|
+
|
|
1116
|
+
Defaults: `types:["message"]` (categories are `message`, `tool-result`, `tool-call`, `gist`,
|
|
1117
|
+
`compaction`, `turn`, `step`, `request`; raw event types and roles are also accepted), reasoning
|
|
1118
|
+
excluded, synthetic `runtime-context`/`compact-checkpoint` messages skipped, 160-char heads (max
|
|
1119
|
+
200), at most 20 items, and a payload budget of ~900 estimated tokens. `includeReasoning` restores
|
|
1120
|
+
reasoning blocks. It never summarizes and never decides importance.
|
|
1121
|
+
|
|
1122
|
+
Live check on the actual avatar range:
|
|
1123
|
+
|
|
1124
|
+
```text
|
|
1125
|
+
scan(958, 1050): 10 items, 787 estimated tokens, every head <= 160 chars, no [reasoning]
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
The 10 items include user/assistant text and assistant tool-call heads; `types:["user/message"]`
|
|
1129
|
+
narrows further. This is deliberately a list, not a judgment.
|
|
1130
|
+
|
|
1131
|
+
### load range/select
|
|
1132
|
+
|
|
1133
|
+
`continuity.load` accepts, alongside `id`/`budgetTokens`:
|
|
1134
|
+
|
|
1135
|
+
- `fromSeq`/`toSeq`: both or neither; restrict the thread's seqs to the interval;
|
|
1136
|
+
- `select:[seqs]`: exactly these seqs, usually from scan; not tail-truncated;
|
|
1137
|
+
- both: the select must lie inside the range.
|
|
1138
|
+
|
|
1139
|
+
The quote uses `eventText(..., {includeReasoning:false})`; reasoning-only events are skipped. The
|
|
1140
|
+
result adds `segs` (the seqs actually injected), `range`, and `select`; `injected`/`tokens` describe
|
|
1141
|
+
the same set. An explicit select that is outside the thread or outside the range returns
|
|
1142
|
+
`seq-not-in-thread` / `select-outside-range` without writing pending state. The default no-range
|
|
1143
|
+
call still quotes the latest segment; the old object shape and the 8k budget are unchanged.
|
|
1144
|
+
|
|
1145
|
+
The pending text says either `第N/M段`, `范围 A..B`, or `范围 A..B · 选择 N 条` (or `选择 N 条`),
|
|
1146
|
+
so the receipt says which part was used. Tests: 93 total (threads 27), including reasoning-free scan,
|
|
1147
|
+
scan cost <1k, exact select with `segs`, default old-shape load, range load, and rejection paths. The
|
|
1148
|
+
real-engine smoke also does a range/select load and a scan after the two saves.
|
|
1149
|
+
|
|
1150
|
+
## 27. 0.3.6 — tags sidecar (find / scan / load)
|
|
1151
|
+
|
|
1152
|
+
### tags.json
|
|
1153
|
+
|
|
1154
|
+
```
|
|
1155
|
+
~/.dsh/continuity/tags.json
|
|
1156
|
+
{ version, tags, runs: [[from,to,id], …], overrides: { "<seq>": id }, updatedAt }
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
`resolveTag(seq)` = override → last containing run → `T0`. Runs may overlap; later runs win.
|
|
1160
|
+
Missing and corrupt both return the empty T0 state, set `missing`/`corrupt` on the read result, and
|
|
1161
|
+
never throw. `readTags`/`updateTags` are serialized/atomic; `tags.json` is the only new file and the
|
|
1162
|
+
session log is never written by the tag layer.
|
|
1163
|
+
|
|
1164
|
+
### find / tag / retag / tags
|
|
1165
|
+
|
|
1166
|
+
`find({id, after?, before?, limit?})` returns `{seqs, count, first, last, span}` and defaults to
|
|
1167
|
+
limit 20. `seqs` come from the message universe (user/assistant, synthetic runtime/checkpoint
|
|
1168
|
+
skipped) resolved through the tags plus exact overrides. `after`/`before` are inclusive seq bounds;
|
|
1169
|
+
`span` uses the first/last event `time`, or null. `tag({seqs,id})` writes overrides;
|
|
1170
|
+
`retag({from,to})` moves all runs and overrides and drops the old tag; `tags()` reports
|
|
1171
|
+
label/status/runs/overrides/lastSeq. The default find payload is <200 tokens; the scan list keeps its
|
|
1172
|
+
~900-token budget, so 50 turns stay under 2k.
|
|
1173
|
+
|
|
1174
|
+
### scan / load
|
|
1175
|
+
|
|
1176
|
+
`scan` accepts `seqs` as an alternative to `fromSeq`/`toSeq`; all other defaults are unchanged.
|
|
1177
|
+
`load` accepts:
|
|
1178
|
+
|
|
1179
|
+
- a **thread id**: old latest-segment/range/select behavior; sets active;
|
|
1180
|
+
- a **tag id**: `find(id)` and take earliest 3 + latest 5, visible/non-reasoning, then trim the
|
|
1181
|
+
largest events until the budget is met; `active` is unchanged;
|
|
1182
|
+
- `seqs:[...]`: exact visible seqs (no tail truncation); `active` is unchanged.
|
|
1183
|
+
|
|
1184
|
+
Tag/seqs loads return `segs`; their pending text starts `[取]` and says `默认最早3+最新5` or
|
|
1185
|
+
`按指定 seqs`. `load` with a tag and `fromSeq/toSeq/select` is rejected (use `seqs`). The old
|
|
1186
|
+
`load(threadId, budgetTokens)` test remains green.
|
|
1187
|
+
|
|
1188
|
+
### Auto tag
|
|
1189
|
+
|
|
1190
|
+
At each pre-step, `autoTagCompletedTurn` finds the latest completed `turn/start..turn/end` range and
|
|
1191
|
+
writes one run if that exact range is absent. T is the active thread id, or `T0`; label is the thread
|
|
1192
|
+
label or `未分类`. This is sidecar-only and never appends to the raw log. A missing/corrupt tags file
|
|
1193
|
+
is recovered with one warning.
|
|
1194
|
+
|
|
1195
|
+
Tests: 104 total (threads 27, tags 11), including resolve precedence, missing/corrupt fallback,
|
|
1196
|
+
find/filters/span/empty, tag+retag immediacy, tool actions, tag/seqs loads, scan seqs, 50-turn cost,
|
|
1197
|
+
budget trim, and the real-engine smoke's thread-tagged turn, idempotent second auto-tag, and T0
|
|
1198
|
+
fallback.
|
|
1199
|
+
|
|
1200
|
+
## 28. 0.3.7 — tag fixes from live use
|
|
1201
|
+
|
|
1202
|
+
### P1 tag loads must not move active
|
|
1203
|
+
|
|
1204
|
+
`processPendingThread` used to treat every `pending.kind === 'load'` as a thread load and set
|
|
1205
|
+
`activeId = pending.id`, so `load({id:"T2"})` made the next `[线]` say “当前:无”. 0.3.7 gates the active
|
|
1206
|
+
update on `pending.mode`: `thread` (or legacy no-mode) moves the pointer; `tag`/`seqs` only append the
|
|
1207
|
+
quote and clear `pending`. `tags()` rows include all threads, with `active` from the real `activeId`.
|
|
1208
|
+
|
|
1209
|
+
### P2 find(T0) is the complement
|
|
1210
|
+
|
|
1211
|
+
`find({id:"T0"})` now returns `{seqs:[], ranges, count, first, last, span, complement:true}`, where
|
|
1212
|
+
`ranges` are the contiguous seq spans whose resolved tag is T0 (explicit T0, untagged, and T0
|
|
1213
|
+
overrides inside other tags). The span is 0..max event seq (or max run/override); `limit` caps the
|
|
1214
|
+
number of ranges, not the total count. T2/T-n find remains the message seq list. So T2=[958,1330]
|
|
1215
|
+
yields T0 ranges [0,957] and [1331,max], never 958–1330.
|
|
1216
|
+
|
|
1217
|
+
### P3/P4 names and labels
|
|
1218
|
+
|
|
1219
|
+
`tags()` rows are `{id, label?, display, count, runs, overrides, lastSeq, active, ...}`; `display` is
|
|
1220
|
+
the label, else the first 8 chars of the id. `tag({seqs,id,label?})` creates/updates the label. The
|
|
1221
|
+
auto tag keeps an existing real label and replaces an id-placeholder label with the thread label.
|
|
1222
|
+
Tag load receipts use `selectionLoadText` with one display name, so `[取] Avatar` rather than
|
|
1223
|
+
`[取] T2 · T2`.
|
|
1224
|
+
|
|
1225
|
+
### P5/P6 find bounds and small items
|
|
1226
|
+
|
|
1227
|
+
`after` is strict `seq > after`; `before` is strict `seq < before`; `limit` is stable ascending.
|
|
1228
|
+
Empty tags return count0. Missing/corrupt tags fall back to T0 and warn. `tag` writes update
|
|
1229
|
+
`updatedAt` (and the test asserts it).
|
|
1230
|
+
|
|
1231
|
+
Tests: 108 total (threads 27, tags 15). The engine smoke additionally loads a tag while a thread is
|
|
1232
|
+
active, proves `activeId` and the active line stay, checks the tag row’s `active:false` and the
|
|
1233
|
+
thread row’s `active:true`, and verifies the no-duplicate receipt.
|
|
1234
|
+
|
|
1235
|
+
## 29. 0.3.8 — tag ranges
|
|
1236
|
+
|
|
1237
|
+
`tag` accepts `{fromSeq,toSeq,id,label?}` as well as `{seqs,id,label?}`. The range form appends one
|
|
1238
|
+
run `[from,to,id]` (the same record the auto-tagger writes) and returns
|
|
1239
|
+
`{ok:true,id,range:[from,to],count:to-from+1}`. It does not write per-seq overrides; the existing
|
|
1240
|
+
`seqs` form still writes overrides and its behavior is unchanged. Per-seq overrides still beat any
|
|
1241
|
+
run, and later runs still beat earlier runs. Adjacent runs are not merged.
|
|
1242
|
+
|
|
1243
|
+
`find` now also returns coverage for a tag: `ranges` (contiguous seq spans where resolve(seq) === id),
|
|
1244
|
+
`covered` (their total length), and `rangeFirst`/`rangeLast`/`rangeSpan`. This makes a range tag cover
|
|
1245
|
+
tool events as well as messages, while `seqs` remains the message list. `find(T0)` remains the
|
|
1246
|
+
complement with `seqs:[]` and `ranges`.
|
|
1247
|
+
|
|
1248
|
+
`untag({fromSeq,toSeq})` appends a T0 run and deletes overrides in the range, returning
|
|
1249
|
+
`{range,count,removedOverrides}`. The tool exposes `untag`; `retag` moves the whole run unchanged.
|
|
1250
|
+
|
|
1251
|
+
Tests: 110 total (threads 27, tags 17). Coverage includes a range run with override priority,
|
|
1252
|
+
tag→find over the whole segment, retag of a run, untag clearing the range, no adjacent-run merge,
|
|
1253
|
+
tool range/untag, and the earlier P1–P6 cases. No raw/gist/save changes.
|
|
1254
|
+
|
|
1255
|
+
## 30. 0.3.9 — tags() count semantics
|
|
1256
|
+
|
|
1257
|
+
The 0.3.7/0.3.8 `tags()` row used `count = runs + overrides`; a thread tag with one run over ten
|
|
1258
|
+
thousand events displayed `count: 108`-style entry counts. 0.3.9 computes actual resolved coverage
|
|
1259
|
+
once over `0..maxSeq` (max of session events, run ends, and override keys) and exposes:
|
|
1260
|
+
|
|
1261
|
+
- `entries`: runs + overrides (the old number);
|
|
1262
|
+
- `events`: seqs whose resolved tag is this id;
|
|
1263
|
+
- `count`: alias of `events` (kept for the existing field name, now actual events).
|
|
1264
|
+
|
|
1265
|
+
T0’s `events` is its complement; thread rows without a run keep events 0. The `tags` action reads the
|
|
1266
|
+
session max seq through its existing `sessionQuery` so the T0/tail counts are over the whole span.
|
|
1267
|
+
No other tag/load/save behavior changes.
|
|
1268
|
+
|
|
1269
|
+
Tests remain 110 total (tags 17); the assertions now check `entries`/`events`/`count` on a range run
|
|
1270
|
+
and an override tag.
|
|
1271
|
+
|
|
1272
|
+
## 31. 0.4.0 — fold provenance, aliases, tags.md, first/last, slim injection
|
|
1273
|
+
|
|
1274
|
+
- **Gist T.** `gistHeader` accepts `t` and renders `T=...` inline. The engine computes it with
|
|
1275
|
+
`tagCoverageForRange(tags, from, to)` (resolve per seq, counts sorted desc), for pressure, manual, and
|
|
1276
|
+
thread-save gists. `threadGistText` renders the segment's `tCoverage` and `thread=<segment index>`.
|
|
1277
|
+
- **Folds.** The engine records each compacted shadowed range and summary seq in
|
|
1278
|
+
`~/.dsh/continuity/folds.json` per session. `find` splits its message `seqs` into `visible`/`folded`;
|
|
1279
|
+
T0 also reports empty visible/folded and `covered`. A load selecting a folded seq prepends
|
|
1280
|
+
`[已折叠于 gist seq=N]`; the seqs are also returned as `folded`/`visible`.
|
|
1281
|
+
- **Alias.** Threads accept `alias` on save (new or append). Auto-tag writes the alias as T when
|
|
1282
|
+
present; `tags()` display is alias → label → short id; the alias row is active. tags.md shows
|
|
1283
|
+
symbol → thread id.
|
|
1284
|
+
- **tags.md.** `tags.json` entries accept `keywords`. Every tag/untag/retag plus alias save and auto
|
|
1285
|
+
tag regenerates `tags.md` via `renderTagsMd`; the file is never read. Hand edits are overwritten.
|
|
1286
|
+
- **first/last.** Tag loads accept `first`/`last` (default 3/5); `first:0` or `last:0` means omit that
|
|
1287
|
+
end; both zero is rejected. Thread/seqs loads reject first/last. The receipt text says the actual
|
|
1288
|
+
selection.
|
|
1289
|
+
- **Injection.** The runtime-context block is `[线]` + `[标签] 当前 <alias>(label)· 另有 N 条`, then
|
|
1290
|
+
`[习惯] <H2 headings> · 全文 ~/.dsh/continuity/rules.md`. The full rules text is no longer injected.
|
|
1291
|
+
|
|
1292
|
+
Tests: 114 total (threads 27, tags 21). The key cases are gist T pure/cross, find folded/load
|
|
1293
|
+
annotation, first/last, tags.md overwrite, alias display/active, and the compact habit line. No save/
|
|
1294
|
+
load segment semantics, 8k fallback, or tool-pairing behavior changed.
|
|
1295
|
+
|
|
1296
|
+
## 32. 0.4.1 — metadata-only tag, tag alias, explicit-only T0 counts
|
|
1297
|
+
|
|
1298
|
+
- `tag` accepts no seqs/ranges when at least one metadata field is present:
|
|
1299
|
+
`tag({id,label})`, `tag({id,keywords})`, `tag({id,alias})`. It updates tags.json, advances
|
|
1300
|
+
`updatedAt`, writes tags.md, and returns the updated tags() row. No run/override is written, so no
|
|
1301
|
+
seq changes hands.
|
|
1302
|
+
- Tag records now accept `alias` (and `tag({id,alias})` also sets the matching thread record’s alias).
|
|
1303
|
+
`tags()` rows include alias; display is tag/thread alias → label → short id; the alias row is active;
|
|
1304
|
+
auto-tag writes the thread alias; tags.md’s symbol column is `alias ?? id`.
|
|
1305
|
+
- T0 counting was initially explicit-only in 0.4.1; 0.4.2 replaces that with the complement so it
|
|
1306
|
+
matches `find(T0).count` (see §33).
|
|
1307
|
+
- Existing `tag({seqs})` and `tag({fromSeq,toSeq})` keep their behavior; they may additionally carry
|
|
1308
|
+
alias/label/keywords.
|
|
1309
|
+
|
|
1310
|
+
Tests: 116 total (threads 27, tags 23). New coverage: metadata-only label/keywords/alias, tag alias
|
|
1311
|
+
setting the thread alias and active row, tags.md symbol, and the smoke path tag alias → auto T9 →
|
|
1312
|
+
`[标签] 当前 T9`.
|
|
1313
|
+
|
|
1314
|
+
## 33. 0.4.2 — unified T0 counts and display-only gist T
|
|
1315
|
+
|
|
1316
|
+
- `tags()` T0 `events` is the complement seq count over 0..max, equal to `find(T0).count` (including
|
|
1317
|
+
folded seqs). Other tags’ `events` equal `find(id).covered`. `tags.md` renders the same row value.
|
|
1318
|
+
`entries` remains runs+overrides (sidecar records, not messages); `count` is an alias of `events`.
|
|
1319
|
+
A fold does not alter tags, maxSeq, or these numbers.
|
|
1320
|
+
- `tagCoverageForRange(tags,from,to,{threads})` returns display labels: alias → label → short id,
|
|
1321
|
+
excludes T0 when any non-T0 tag is present, uses T0 only for an all-T0 range, sorts by event count,
|
|
1322
|
+
keeps at most three, and appends `+N`. `threadGistText` and `gistHeader` consume that label; the
|
|
1323
|
+
field order remains `thread=… · T=…`.
|
|
1324
|
+
- No changes to find’s complement/ranges, tags() display/keywords, tags.md generation, or save’s
|
|
1325
|
+
segment merge/pending/8k behavior.
|
|
1326
|
+
|
|
1327
|
+
Tests still 116 (threads 27, tags 23): the T0 test now asserts `tags().T0.events === find(T0).count`
|
|
1328
|
+
before/after a fold, and the gist test asserts display, T0 omission, three-tag cap, and thread/T field
|
|
1329
|
+
order.
|
|
1330
|
+
|
|
1331
|
+
## 34. 0.4.3 — canonical alias identity and find count/page
|
|
1332
|
+
|
|
1333
|
+
- Alias is display only. `tag`, auto-tag, run writes, and find/load resolve alias → canonical thread id.
|
|
1334
|
+
Auto-tag writes `active.id`; the tag entry may carry `alias` for display. `load({id:"T3"})` canonical
|
|
1335
|
+
resolves to the thread, not a second tag.
|
|
1336
|
+
- Migration: on tags/find/load/tag/auto, `migrateAliasTags` moves every alias-named run/override into
|
|
1337
|
+
the canonical thread tag, ensures the canonical alias/label, deletes the alias tag record, and writes
|
|
1338
|
+
tags.md. So `tags()` has one T3 row, `find(T3)` equals `find(threadId)`.
|
|
1339
|
+
- `find` count semantics: `count` = total covered events for the tag (including folded), independent
|
|
1340
|
+
of `limit`; `seqs` = this page (`seqs.length <= limit`); `covered` = `count`. T0’s count is the
|
|
1341
|
+
complement. The tags()/tags.md `events` use the same total.
|
|
1342
|
+
- Untouched: `ranges`/`complement`/`visible`/`folded`, tags() display/keywords, tags.md generation,
|
|
1343
|
+
save merge/pending/8k.
|
|
1344
|
+
|
|
1345
|
+
Tests: 117 (threads 27, tags 24). New coverage: alias migration to canonical, find(T3) ≡ find(threadId),
|
|
1346
|
+
load(T3) → thread, tag(T3) writes canonical, count stable across limit, and the auto-tag canonical smoke.
|
|
1347
|
+
|
|
1348
|
+
## 35. 0.4.4 — default save clamps to current T; primary-only gist T
|
|
1349
|
+
|
|
1350
|
+
- Default save (`fromSeq`/`toSeq` omitted) still uses the latest-200-message ceiling, but additionally
|
|
1351
|
+
clamps its start to the first surface node at/after the start of the tag run containing the selected
|
|
1352
|
+
end. If the run is shorter, the save is shorter. Explicit from/to is passed through unchanged. This
|
|
1353
|
+
is selection only; pairing, the 8k fallback, and folding math are unchanged.
|
|
1354
|
+
- `tagCoverageForRange` now returns one primary display (largest event count) plus the number of other
|
|
1355
|
+
non-T0 events, e.g. `T3 +2`; no comma list. Display is alias → short label → short id, one form only.
|
|
1356
|
+
All-T0 stays `T0`. `thread=` remains before `T=`.
|
|
1357
|
+
- Tests: 118 (threads 28, tags 24): selectSaveRange minStartSeq clamp and explicit bypass, tagRunStartAt,
|
|
1358
|
+
and primary+secondary labels.
|
|
1359
|
+
|
|
1360
|
+
## 36. 0.4.5 — dominance block, default append, auto-bound new threads
|
|
1361
|
+
|
|
1362
|
+
- `dominanceStartSeq(tags,end,{islandMax=5})` in threads.mjs starts at the run containing end, walks
|
|
1363
|
+
earlier runs: own runs join; other-T runs of <=5 events are islands crossed; a larger other run is a
|
|
1364
|
+
boundary. 0.4.4’s run-start clamp is replaced by this block start; `selectSaveRange` minStartSeq is
|
|
1365
|
+
unchanged. Explicit from/to still bypasses it.
|
|
1366
|
+
- `prepareSave` without threadId appends when the current T resolves to an existing thread id or
|
|
1367
|
+
alias (active:false is not a filter); T0 or unbound T creates. A new non-T0 thread inherits the
|
|
1368
|
+
current T alias and is active, so `[标签]` has a T immediately. New T0 threads stay inactive.
|
|
1369
|
+
- Header label remains primary + secondary count (`T3`, `T3 +2`), display one form only; 0.4.4 already
|
|
1370
|
+
changed this; 0.4.5 keeps it.
|
|
1371
|
+
- Tests: 119 (threads 28, tags 25): dominance islands vs real boundaries, selectSaveRange clamp, and
|
|
1372
|
+
primary+secondary labels.
|
|
1373
|
+
|
|
1374
|
+
## 37. 0.4.6 — load withReasoning
|
|
1375
|
+
|
|
1376
|
+
- `load(..., withReasoning:true)` applies to thread, tag, and seqs loads. Default `false` leaves the
|
|
1377
|
+
0.4.5 8k/no-reasoning path untouched.
|
|
1378
|
+
- The selected visible entries are sorted chronologically and share one 16,000-token body+reasoning
|
|
1379
|
+
budget. Each assistant body pairs its own reasoning blocks, or the nearest reasoning-only event
|
|
1380
|
+
before it in the same turn. Pairing is silent when absent; user messages never pair.
|
|
1381
|
+
- Reasoning renders separately (`[reasoning]`; truncated label `[reasoning · 截断]`). A reasoning over
|
|
1382
|
+
800 estimated tokens keeps a 150-token head and 650-token tail. Bodies plus reasoning stop at the
|
|
1383
|
+
budget; remaining entries are counted as `skipped`.
|
|
1384
|
+
- Receipts add `reasoningInjected`, `reasoningTokens`, `skipped`; `tokens` is the actual body+reasoning
|
|
1385
|
+
total and `budget` is 16,000 for this mode. Text headers say `含 reasoningN 条 · skipped K`.
|
|
1386
|
+
- Gist generation and scan do not include reasoning.
|
|
1387
|
+
|
|
1388
|
+
Tests: 121 (threads 28, tags 27): internal and preceding reasoning pairing, truncation, 16k stop/skipped,
|
|
1389
|
+
thread and tag paths, default no-reasoning output, and the tool parameter.
|
|
1390
|
+
|
|
1391
|
+
## 38. 0.4.7 — delete, header counts, label protection, alias id resolution
|
|
1392
|
+
|
|
1393
|
+
- `delete({id,force?})` allows zero-segment/zero-token threads; otherwise `force:true` is required.
|
|
1394
|
+
Before mutation it writes `<id>-<ts>.json` under `~/.dsh/continuity/deleted/` with the raw/normalized
|
|
1395
|
+
thread and tag entries/runs/overrides. It then removes the thread, its tag rows/runs/overrides, regenerates
|
|
1396
|
+
tags.md, and returns `{ok,id,backup}`. No automatic deletion.
|
|
1397
|
+
- `[线]` is `当前:label(N 段 · M tokens)` (M=sum of segment tokens), with `(活动:symbol)` when
|
|
1398
|
+
the active-flag thread differs from `activeId`. `另有 K 条` is unchanged.
|
|
1399
|
+
- Existing thread saves keep their label; `relabel:true` is the only override.
|
|
1400
|
+
- tag/retag resolve tag id first, then alias (tag or thread); unknown metadata retag returns
|
|
1401
|
+
`unknown-tag-id` instead of creating an empty record. Retagging an alias to its own canonical is an
|
|
1402
|
+
idempotent success.
|
|
1403
|
+
- tags.md omits zero-event rows; list reflects threads after explicit deletion. JSON shapes remain
|
|
1404
|
+
read-compatible with 0.4.6.
|
|
1405
|
+
|
|
1406
|
+
Tests: 122 (threads 28, tags 28): empty/forced delete plus backup, tag-row removal and zero-event md,
|
|
1407
|
+
header segment/token/activity mismatch, label protected and relabel, alias retag no-op/unknown.
|
|
1408
|
+
|
|
1409
|
+
## 39. 0.7.6 — target-addressed peer route
|
|
1410
|
+
|
|
1411
|
+
`POST /dsh-continuity/peer` on the Host's existing Web server. Body:
|
|
1412
|
+
`{ "to": "session-...", "from": "创", "text": "..." }`.
|
|
1413
|
+
|
|
1414
|
+
- `to` is mandatory and must be an exact live main-session id. Lookup is a
|
|
1415
|
+
`Map<sessionId, agent>` fed by `agent/created` / `session/disposed`. A missing target returns
|
|
1416
|
+
`404 target-not-live`; there is no active, newest, or owner fallback.
|
|
1417
|
+
- Delivery is `agent.followup(userMessage("[from] text"))`: fresh id, role user,
|
|
1418
|
+
`source.kind: "user"`; no raw `session.append`, no direct event construction.
|
|
1419
|
+
- The route is loopback-only, POST-only, 16 KiB body and 8,000-character text limits.
|
|
1420
|
+
- It is independent of the Coo pet bridge; it does not open or replace `/coopanion/pet`.
|
|
1421
|
+
- `followup` queues for the next turn; a 200 means accepted, not replied. A reply is a separate
|
|
1422
|
+
call from the other side.
|
|
1423
|
+
|
|
1424
|
+
Tests: 131 (peer 5, route 1): exact target/no fallback, registry ignores subagents/no-followup, payload
|
|
1425
|
+
validation, source/id shape, loopback/method/body refusals, and panel-disabled route mounting.
|
|
1426
|
+
|
|
1427
|
+
## 40. 0.7.7 — peer_message preset tool
|
|
1428
|
+
|
|
1429
|
+
The continuity preset mounts `peer_message({ to, text, from? })` next to `continuity`. It shares the
|
|
1430
|
+
same singleton live-agent registry as the route.
|
|
1431
|
+
|
|
1432
|
+
- `to` is required and exact; `from` defaults to `Continuity`; text is 1–8000 chars.
|
|
1433
|
+
- Delivery is `target.followup(peerMessage(...))` with a fresh id and `source.kind: "user"`; no raw
|
|
1434
|
+
session append and no owner/active fallback.
|
|
1435
|
+
- Result is `{ok,to,from,queued:"next-turn",id}`; unknown target throws `peer_message: target-not-live`.
|
|
1436
|
+
- The tool is preset-scoped, so standard sessions do not see it; the Host route remains available for
|
|
1437
|
+
external callers such as 创.
|
|
1438
|
+
|
|
1439
|
+
Tests: 134 (peer 5, route 1, peer-tool 3): exact target, default/custom from, source/id/prefix,
|
|
1440
|
+
unknown-target refusal, and input validation before delivery.
|
|
1441
|
+
|
|
1442
|
+
## 41. 0.7.8 — one-shot tag rejection display
|
|
1443
|
+
|
|
1444
|
+
The Tag gate's decision rules are unchanged. `tag-reject.json` is now a two-step display record:
|
|
1445
|
+
|
|
1446
|
+
- rejection writes `{turn,token,kind,reason,shown:false}`;
|
|
1447
|
+
- the first consuming pre-step renders the existing `[打标·退回]` text, then writes
|
|
1448
|
+
`shown:true` and `shownAt` (does not delete);
|
|
1449
|
+
- the next consuming pre-step deletes the file without rendering;
|
|
1450
|
+
- a second rejection for the same turn preserves `shown:true`, so it cannot render twice.
|
|
1451
|
+
|
|
1452
|
+
`renderTagRejectHeader` keeps the existing wording and `turnmap ... set --turn N --tag <Tx>` line.
|
|
1453
|
+
All Tag-gate writes remain on `userMessage()`; T99 rejections still never append `turn-tags.jsonl`.
|
|
1454
|
+
|
|
1455
|
+
Tests: 138 (peer 5, route 1, peer-tool 3, tag-reject 4): first render/mark, second-step delete,
|
|
1456
|
+
same-turn no-reset, new-turn render, and exact header text.
|
|
1457
|
+
|
|
1458
|
+
## 42. 0.7.9 — stale rejection and one-steer cleanup
|
|
1459
|
+
|
|
1460
|
+
- `consumeTagReject` now accepts `isResolved(turn)`. The engine header passes the same `tagged`
|
|
1461
|
+
set computed from `turn-tags.jsonl` (last write wins per turn is equivalent for this existence
|
|
1462
|
+
check). A resolved turn deletes `tag-reject.json` without rendering.
|
|
1463
|
+
- The two rejection paths no longer call `appendToSession`; they emit one `steer` whose first line
|
|
1464
|
+
is the old short message and whose remainder is the complete error count/examples. The header
|
|
1465
|
+
remains the fallback when steer cannot be delivered.
|
|
1466
|
+
- Decision rules and `userMessage()` source/id discipline are unchanged.
|
|
1467
|
+
|
|
1468
|
+
Acceptance target: T99 then a legal T14 must not render a stale header; T99 without repair must render
|
|
1469
|
+
once and disappear; one steer per rejection; no new `turn-tags` rows or `gate-errors`.
|
|
1470
|
+
|
|
1471
|
+
Tests: 139 (peer 5, route 1, peer-tool 3, tag-reject 5): resolved delete, one-shot display, same-turn
|
|
1472
|
+
no-reset, new-turn render, and exact header text.
|
|
1473
|
+
|
|
1474
|
+
## 43. 0.8.0 — dispatch probe
|
|
1475
|
+
|
|
1476
|
+
The Host listener's `agent/turn-stopping` handler writes `dispatch-probe.jsonl` before reading
|
|
1477
|
+
`continuityState.engine`:
|
|
1478
|
+
|
|
1479
|
+
```json
|
|
1480
|
+
{"at":"...","event":"agent/turn-stopping","turn":1196,"enginePresent":true}
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1483
|
+
The engine entry probe `hook-probe.jsonl` now includes `taggedSize` alongside `hasAgent` and
|
|
1484
|
+
`canSteer`. Comparison:
|
|
1485
|
+
|
|
1486
|
+
- both files have the turn → dispatch and engine entry both happened;
|
|
1487
|
+
- dispatch only, `enginePresent:false` → engine was not ready;
|
|
1488
|
+
- dispatch only, `enginePresent:true` → engine returned/threw before its entry probe;
|
|
1489
|
+
- neither → the Host did not dispatch that turn-stopping.
|
|
1490
|
+
|
|
1491
|
+
No decision rules or user text changed. Tests: 140 (dispatch 1) verify probe-before-engine and the
|
|
1492
|
+
null/true engine distinction.
|
|
1493
|
+
|
|
1494
|
+
## 44. 0.8.1 — dispatch identity fields
|
|
1495
|
+
|
|
1496
|
+
`dispatch-probe.jsonl` records:
|
|
1497
|
+
|
|
1498
|
+
- `engineCallable`: `typeof continuityState.engine?.onTurnStopping === 'function'`;
|
|
1499
|
+
- `agentSession`: the session id carried by the event's agent;
|
|
1500
|
+
- `continuitySession`: the session the mounted engine has actually served
|
|
1501
|
+
(`continuityState.engineSession`, set at engine `onTurnStopping` entry);
|
|
1502
|
+
- `foreignSession`: both ids are present and differ.
|
|
1503
|
+
|
|
1504
|
+
`hook-probe.jsonl` additionally records `agentSession`. The fields only diagnose: a foreign session
|
|
1505
|
+
is noise for this gate; a local event with dispatch but no hook points at the index/engine fallback;
|
|
1506
|
+
neither file is a Host non-dispatch. Tests: 140 (dispatch 1 covers the fields and foreign flag).
|
|
1507
|
+
|
|
1508
|
+
## 45. 0.8.2 — gate messages are runtime context
|
|
1509
|
+
|
|
1510
|
+
The Tag gate's `steer` still holds the current turn open, but its message now uses
|
|
1511
|
+
`source:{kind:"runtime-context", form:"notice", sections:[{name:"continuity:tag-gate",text}]}`
|
|
1512
|
+
instead of `source:{kind:"user"}`. The `[闸] TAG:` / `[闸·打标]` marker stays in the text.
|
|
1513
|
+
|
|
1514
|
+
- The UI no longer attributes the gate to the person as a user bubble; it is a context block.
|
|
1515
|
+
- The model still receives the text (the gate must explain the hold); this is not a full hide.
|
|
1516
|
+
- Same-turn `agent.steer`, decision rules, headers and `tag-reject` behavior are unchanged.
|
|
1517
|
+
- Peer messages still use the real `userMessage()` path; only the gate source changed.
|
|
1518
|
+
|
|
1519
|
+
Tests: 141 (threads 31): source kind/form/section and marker, plus the existing gate suite.
|
|
1520
|
+
|
|
1521
|
+
## 46. 0.8.3 — first-line TAG declaration
|
|
1522
|
+
|
|
1523
|
+
`findTagDeclaration` (new `src/tag-detect.mjs`) accepts the first non-empty, non-fenced line and the
|
|
1524
|
+
last five non-empty, non-fenced lines. If both contain a TAG, the last-five candidate wins, preserving
|
|
1525
|
+
the old tail priority. Fenced examples remain ignored.
|
|
1526
|
+
|
|
1527
|
+
`onTurnStopping` now uses this helper instead of only reverse-walking five lines, so a long report
|
|
1528
|
+
with `TAG:` on its first line is accepted. The pre-step auto-copy already considered first/last; this
|
|
1529
|
+
aligns the turn gate with the documented rule.
|
|
1530
|
+
|
|
1531
|
+
Tests: 147 (tag-detect 6): first-line, last-line, last-wins, fenced-only, fenced-first followed by
|
|
1532
|
+
real tag, and candidate de-duplication.
|
|
1533
|
+
|
|
1534
|
+
## 47. 0.8.4 — real-user filtering
|
|
1535
|
+
|
|
1536
|
+
`latestUserTurn` and the pre-step turn map now require `source.kind === "user"` for both
|
|
1537
|
+
`user/message` and `agent/inbox/spliced` entries. `runtime-context`, `time-context`, delivery notices,
|
|
1538
|
+
and the gate's own `continuity:tag-gate` notices are skipped.
|
|
1539
|
+
|
|
1540
|
+
This removes the `[新标签?]` / `[暂停]` quotations of runtime/time context. The helper lives in
|
|
1541
|
+
`src/user-turn.mjs`; the gate's latest-user selection and the `userFirstBySeq` map share it.
|
|
1542
|
+
|
|
1543
|
+
Tests: 151 (user-turn 4): time/runtime skipped, spliced user selected, only-context → null, and source
|
|
1544
|
+
lookup precedence.
|
|
1545
|
+
|
|
1546
|
+
## 48. 0.8.5 — current turn is not debt
|
|
1547
|
+
|
|
1548
|
+
`closedTurns` now filters `turn < currentTurn`, using `agent.lastTurn` (fallback: latest
|
|
1549
|
+
`turn/start`). The current open turn is excluded from `[打标?]` and `tag-debt.json`.
|
|
1550
|
+
|
|
1551
|
+
`[暂停]` is emitted only when the previous completed turn has a real user quote; an empty quote now
|
|
1552
|
+
omits the whole line instead of printing `「」`. The `[打标?]` list remains for genuinely older
|
|
1553
|
+
missing turns.
|
|
1554
|
+
|
|
1555
|
+
Tests: 151; header behavior is exercised live (turn 1210: no `缺 1 轮`, no empty `[暂停]`).
|
|
1556
|
+
|
|
1557
|
+
## 49. 0.8.6 — surface-floor start and skippedRange
|
|
1558
|
+
|
|
1559
|
+
`resolveSaveStart` keeps the normal save start at `surface[0]` even when an explicit `fromSeq` is
|
|
1560
|
+
below it; it never throws `not a surface node`. `detectSkippedRange` reports
|
|
1561
|
+
`{from,to,events}` for the uncovered interval before `surface[0]` (message events only), records
|
|
1562
|
+
`save-hole`, and the result/receipt carry `skippedRange`.
|
|
1563
|
+
|
|
1564
|
+
`preflight` now takes the actual segment start/end, so its plan and the landed segment share one
|
|
1565
|
+
source. Nothing auto-fills the hole; `backfill:true` is reserved for 0.8.9.
|
|
1566
|
+
|
|
1567
|
+
Tests: 155 (save-start 4): below-surface fallback, at/above/no explicit, hole count, no-hole/no-range
|
|
1568
|
+
cases.
|
|
1569
|
+
|
|
1570
|
+
## 50. 0.8.7 — save-hole noise and T0 machine sources
|
|
1571
|
+
|
|
1572
|
+
- `detectSkippedRange` returns null when the interval has zero message events; no receipt/header
|
|
1573
|
+
noise for a purely structural gap.
|
|
1574
|
+
- Peer messages carry `source.peer:true` (still `source.kind:"user"` for the UI) and
|
|
1575
|
+
`contentUndecided` skips `source.peer`, `runtime-context`, `time-context` and other non-user
|
|
1576
|
+
sources. Machine steers/peer notes no longer create T0 undecided debt.
|
|
1577
|
+
- Normal save emits `planNote` when `preflight.plan[0].from !== segment.fromSeq`, naming both values
|
|
1578
|
+
and the reason (plan only counts turn-attributed content events).
|
|
1579
|
+
|
|
1580
|
+
Tests: 168 (save-start 4, t0 13): zero-event suppression, machine-source filtering, and existing
|
|
1581
|
+
save-start cases.
|
|
1582
|
+
|
|
1583
|
+
## 51. 0.8.8 — peer prefix fallback for T0
|
|
1584
|
+
|
|
1585
|
+
The Host may persist only `source.kind:"user"` on a peer-delivered user message, dropping the extra
|
|
1586
|
+
`source.peer` flag. `contentUndecided` therefore also skips content starting with `[创]`, `[闸]`, or
|
|
1587
|
+
`[闸·打标]`; these are the stable prefixes written by the peer route and the Tag gate.
|
|
1588
|
+
|
|
1589
|
+
This removes the remaining T0 debt from cross-session peer notes and gate steers. The `source.peer`
|
|
1590
|
+
flag remains as an additional, non-required marker.
|
|
1591
|
+
|
|
1592
|
+
Tests: 168 (t0 13): `[创]`/`[闸]` with only `kind:"user"` are skipped; real user content still
|
|
1593
|
+
counts.
|
|
1594
|
+
|
|
1595
|
+
## 52. 0.8.9 — historical backfill
|
|
1596
|
+
|
|
1597
|
+
`save({fromSeq,toSeq,backfill:true,threadId?})` does not call `selectSaveRange`. It reads
|
|
1598
|
+
`sessionQuery.readSession` events, keeps `user/message`, `assistant/message`, `tool/call`,
|
|
1599
|
+
`tool/result` in `[fromSeq,toSeq]`, drops a leading tool/result or trailing tool/call, and appends
|
|
1600
|
+
one segment to an existing thread (active by default).
|
|
1601
|
+
|
|
1602
|
+
- Strict overlap with any existing segment → `range-already-covered`.
|
|
1603
|
+
- Same gates: `t0-undecided`, `turn-untagged`, `tags-gap`; empty content → `backfill-empty`.
|
|
1604
|
+
- Returns `mode:"backfill"`, `surfaceTouched:false`, `adjusted:false`, actual `seqStart/seqEnd/count`,
|
|
1605
|
+
`seqs`, and `requestedRange` (requested vs content boundary).
|
|
1606
|
+
- No live surface mutation, no pending gist, no make-surface path.
|
|
1607
|
+
|
|
1608
|
+
Tests: 172 (backfill 4): whitelist/range, edge-pair trim, segment boundaries/tokens, strict overlap.
|
|
1609
|
+
|
|
1610
|
+
## 53. 0.8.10 — ordered segments after backfill
|
|
1611
|
+
|
|
1612
|
+
`normalizeThread` sorts segments by `fromSeq`. This restores the invariant every reader relies on:
|
|
1613
|
+
the last segment is the chronologically latest, so `seqEnd`, `why`, `next`, gist index, `coveredTo`
|
|
1614
|
+
and `at(-1)` projections stay correct after a historical backfill.
|
|
1615
|
+
|
|
1616
|
+
Backfill inserts the new segment at its sorted position, reports that position, and does not let the
|
|
1617
|
+
archival segment's `why/next` become the thread's latest metadata. `label/why/next` are optional for
|
|
1618
|
+
backfill and receive deterministic defaults.
|
|
1619
|
+
|
|
1620
|
+
The backfill segment still inherits the thread's current `gistSeq`/`compactionId` through
|
|
1621
|
+
`normalizeSegment`; it is not recomputed for the historical range. If this is undesirable, that is a
|
|
1622
|
+
separate backfill-gist design change.
|
|
1623
|
+
|
|
1624
|
+
Tests: 173 (backfill 5): out-of-order normalization keeps `seqEnd`/`why`/`next` from the latest
|
|
1625
|
+
segment.
|
|
1626
|
+
|
|
1627
|
+
## 54. 0.8.11 — fieldless backfill validation
|
|
1628
|
+
|
|
1629
|
+
The tool validator's `save` branch previously required non-empty `label/why/next` for every save.
|
|
1630
|
+
It now skips that requirement when `backfill:true`; normal saves still enforce it in the Core.
|
|
1631
|
+
|
|
1632
|
+
Core's backfill parser generates `历史回补 <from>..<to>` and `检查这段原话` when those fields are
|
|
1633
|
+
absent, so the archival segment never carries empty `why/next`. Tests: 173; the tool-level path is
|
|
1634
|
+
exercised live (fieldless backfill).
|
|
1635
|
+
|
|
1636
|
+
## 55. 0.8.12 — content time is not write time
|
|
1637
|
+
|
|
1638
|
+
A backfill segment now sets `at = spanEnd` (when its content happened) and `archivedAt = now` (when
|
|
1639
|
+
it was appended). `normalizeSegment` preserves `archivedAt`.
|
|
1640
|
+
|
|
1641
|
+
`normalizeThread` takes thread `updatedAt` from the latest segment's `spanEnd` instead of the
|
|
1642
|
+
source `updatedAt`/`at`; `saveBackfill` writes the latest content's `spanEnd` as the thread
|
|
1643
|
+
timestamp. This prevents a historical backfill from making the thread look recently active.
|
|
1644
|
+
|
|
1645
|
+
Q2 of the user-facing table was corrected: system ordering is fixed, but a reader can still confuse
|
|
1646
|
+
"when the note was written" with "when the event happened" unless it uses `span` or the event
|
|
1647
|
+
timestamp.
|
|
1648
|
+
|
|
1649
|
+
Tests: 173; backfill assertions cover `at === spanEnd`, `archivedAt`, and `normalizeThread`
|
|
1650
|
+
`updatedAt` from the latest `spanEnd`.
|
|
1651
|
+
|
|
1652
|
+
## 56. 0.8.13 — normalize the persisted file too
|
|
1653
|
+
|
|
1654
|
+
0.8.12 normalized old backfill timestamps only in the in-memory `normalizeThread` view; direct
|
|
1655
|
+
`threads.json` consumers still saw the old write-time `at`.
|
|
1656
|
+
|
|
1657
|
+
`migrateThreadsFile` now runs once at plugin startup:
|
|
1658
|
+
|
|
1659
|
+
- reads every session, normalizes its threads, compares with the raw JSON;
|
|
1660
|
+
- on change, writes a `threads.json.bak-normalize-*` backup and calls `updateSession` so the file is
|
|
1661
|
+
rewritten in normalized form;
|
|
1662
|
+
- old backfill rows get `at = spanEnd` and `archivedAt = old at` (the old write moment is preserved,
|
|
1663
|
+
not discarded); thread `updatedAt` becomes the latest content `spanEnd`.
|
|
1664
|
+
|
|
1665
|
+
Tests: 174 (migrate 1): old `历史回补` row is normalized on disk, `archivedAt` is preserved, and the
|
|
1666
|
+
backup exists.
|
|
1667
|
+
|
|
1668
|
+
## 57. 0.8.14 — boundary-silent header
|
|
1669
|
+
|
|
1670
|
+
The header now follows “正常不吵,越界才说”:
|
|
1671
|
+
|
|
1672
|
+
- auto-assignment success is silent; only failure/ambiguity/active-line change speaks;
|
|
1673
|
+
- `[新标签?]` requires zero candidates, ≥40 chars, non-machine text, 30-day text cap, and 20-turn
|
|
1674
|
+
cooldown; it is mutually exclusive with `[候选]`;
|
|
1675
|
+
- `[话题?]` uses keywords/anchors only (label weight 0), generic weight 0; needs one exclusive word
|
|
1676
|
+
(precision ≥0.5, hits ≥5) or ≥2 non-generic words; score gap ≥2; one target; 10-minute same-target
|
|
1677
|
+
cooldown;
|
|
1678
|
+
- `[归拢]`/`[心境]` no longer auto-broadcast; `[接手]` only after load and one line;
|
|
1679
|
+
- `[存?]` requires 8+ unseen content messages, 20+ turns from segment end, and once per
|
|
1680
|
+
`(thread,toSeq)`;
|
|
1681
|
+
- `[规则膨胀?]` once per size signature;
|
|
1682
|
+
- skill body injects only when the skill set changes; the `[技能] 正在用` line remains.
|
|
1683
|
+
|
|
1684
|
+
Self-check: `node bin/noise-selfcheck.mjs`. Tests: 188 (topic 7, inject-policy 7).
|
|
1685
|
+
|
|
1686
|
+
## 58. 0.8.15 — anchors do not promote generic words
|
|
1687
|
+
|
|
1688
|
+
`topic.mjs` now treats anchors only as a presence requirement; they never move a generic word into
|
|
1689
|
+
`strong`. `strong` is strictly non-generic; `exclusive` requires the word stats (precision ≥0.5,
|
|
1690
|
+
hits ≥5). The `[话题?]` display marks a generic anchor as `(泛·锚)` instead of calling it hard
|
|
1691
|
+
evidence.
|
|
1692
|
+
|
|
1693
|
+
`[接手]` now requires `handoff-pending.json` to have the current `process.pid` and be younger than
|
|
1694
|
+
10 minutes. A pre-restart pending is deleted without firing. Save still does not create a pending.
|
|
1695
|
+
|
|
1696
|
+
Tests: 189 (topic 8, inject-policy 7).
|
|
1697
|
+
|
|
1698
|
+
## 59. 0.8.16 — first-miss TAG prompt
|
|
1699
|
+
|
|
1700
|
+
The first missing-TAG steer is no longer the bare `[闸] TAG:`. It now says:
|
|
1701
|
+
|
|
1702
|
+
```text
|
|
1703
|
+
[闸] 收尾前,先用最后一行总结本轮主题:
|
|
1704
|
+
TAG: <最符合的 T 别名;没有就写一个 2–30 字的新词> — 一句话理由
|
|
1705
|
+
例:TAG: T14 — 折叠边界
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
The parser still requires the literal `TAG:`; “总结本轮主题” is explanatory, not a replacement.
|
|
1709
|
+
Later errors keep the detailed `EXPECT` block. It lowers accidental gibberish, but does not stop a
|
|
1710
|
+
deliberate 2–30 char unknown word from auto-creating a T.
|
|
1711
|
+
|
|
1712
|
+
Tests: 189; selfcheck also asserts the new prompt text.
|
|
1713
|
+
|
|
1714
|
+
## 60. 0.8.17 — confirm new names with evidence
|
|
1715
|
+
|
|
1716
|
+
New-word confirmation now:
|
|
1717
|
+
|
|
1718
|
+
- asks only y/n and says “这一轮最后一行只写 y 或 n;不要写 TAG”;
|
|
1719
|
+
- accepts `y` immediately before a TAG line (last-line parser looks back one line);
|
|
1720
|
+
- requires the word to appear in the current user message or assistant body, excluding TAG
|
|
1721
|
+
lines and `[闸…]` gate text;
|
|
1722
|
+
- rejects a word within edit distance 1 of an existing label and names that alias;
|
|
1723
|
+
- splits declarations only on `—`/`–`, so `F-35`, `COVID-19`, and `D-Day` stay intact;
|
|
1724
|
+
- uses a placeholder new-word example rather than the existing T16 label;
|
|
1725
|
+
- records the confirmed token in `turn-tags.jsonl` and clears pending on the safety valve.
|
|
1726
|
+
|
|
1727
|
+
Tests: 194 (tag-confirm 5).
|
|
1728
|
+
|
|
1729
|
+
## 61. 0.8.18 — backfill uses the same gate rules
|
|
1730
|
+
|
|
1731
|
+
The declaration-as-record path in the header builder (`tagLineOf`) used a separate ASCII-hyphen
|
|
1732
|
+
split and `resolveTagToken`’s default `allowCreate:true`. That meant a backfill could silently create
|
|
1733
|
+
a tag without y/n.
|
|
1734
|
+
|
|
1735
|
+
It now uses `tagTokenFromDeclaration(line)` and `resolveTagToken(token, { allowCreate: false })`:
|
|
1736
|
+
existing aliases/ids can be copied, new words are `new-pending` and left for the gate. `F-35` is no
|
|
1737
|
+
longer truncated to `F`.
|
|
1738
|
+
|
|
1739
|
+
Tests: 194; selfcheck asserts the backfill line uses `tagTokenFromDeclaration(line)`.
|
|
1740
|
+
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
|