@aiwayds/dsh-topics-memory 0.17.1 → 0.18.1
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/README.md +5 -5
- package/README.zh.md +4 -4
- package/icon.svg +6 -0
- package/lib/config.d.ts +55 -55
- package/lib/config.js +26 -26
- package/lib/distill.d.ts +8 -0
- package/lib/distill.js +1 -1
- package/lib/index.d.ts +67 -36
- package/lib/index.js +169 -155
- package/lib/legacy-import.d.ts +94 -0
- package/lib/legacy-import.js +224 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +30 -27
- package/skills/dsh-topics-memory-config/SKILL.md +6 -6
- package/src/config.ts +26 -26
- package/src/distill.ts +15 -1
- package/src/index.ts +182 -171
- package/src/legacy-import.ts +267 -0
package/lib/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* dsh-topics-memory — OKF topic memory for DeepSeek Harness.
|
|
3
3
|
*
|
|
4
4
|
* Wiring (per ADR 0001–0008):
|
|
5
|
-
* - `
|
|
5
|
+
* - settings page = projection of the plugin `Config` schema, namespace =
|
|
6
|
+
* profile entry id `dsh-topics-memory` (user-tunable via /topics set)
|
|
6
7
|
* - static systemPrompt section teaching the topic tools (never volatile
|
|
7
8
|
* content — the provider cache prefix stays byte-stable)
|
|
8
9
|
* - same-turn injection: retrieval runs synchronously at inbox-claim time
|
|
@@ -34,6 +35,7 @@ import { Consolidator, dropExpiredDeprecated } from "./consolidate.js";
|
|
|
34
35
|
import { SlowLane } from "./quality.js";
|
|
35
36
|
import { CONFIG_KEYS, TopicsConfig } from "./config.js";
|
|
36
37
|
import { isDelegated } from "./delegation.js";
|
|
38
|
+
import { runLegacySettingsImport } from "./legacy-import.js";
|
|
37
39
|
export const name = 'dsh-topics-memory';
|
|
38
40
|
/**
|
|
39
41
|
* Bounded wait for the process-exit meta commit. The exit path is
|
|
@@ -154,68 +156,20 @@ export function resolveClaimedText(input) {
|
|
|
154
156
|
source: 'projection',
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
|
-
// dsh
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
* describe().user is the only way to tell user keys from schema defaults,
|
|
172
|
-
* because scope.get() folds the defaults in;
|
|
173
|
-
* - idempotent: after the copy (or any config written on the new name) the
|
|
174
|
-
* override check short-circuits every later boot;
|
|
175
|
-
* - fail-open: registering the legacy namespace can throw (duplicate or
|
|
176
|
-
* schema-invalid stored section) and the write can reject — both contained,
|
|
177
|
-
* so plugin startup never depends on the migration.
|
|
178
|
-
* Keys whose stored value equals the schema default are not copied (nothing
|
|
179
|
-
* user-tuned to preserve). Returns the in-flight write for tests; apply()
|
|
180
|
-
* fire-and-forgets it.
|
|
181
|
-
*/
|
|
182
|
-
export function migrateLegacySettings(settings, warn) {
|
|
183
|
-
try {
|
|
184
|
-
const describe = settings.describe?.bind(settings);
|
|
185
|
-
if (describe === undefined)
|
|
186
|
-
return undefined;
|
|
187
|
-
const findUser = (ns) => {
|
|
188
|
-
const user = describe().find((entry) => entry.ns === ns)?.user;
|
|
189
|
-
return user !== undefined && user !== null && typeof user === 'object' ? user : undefined;
|
|
190
|
-
};
|
|
191
|
-
// Already migrated / user-configured on the new name — never touch it.
|
|
192
|
-
if (findUser(OWN_NS) !== undefined)
|
|
193
|
-
return undefined;
|
|
194
|
-
// Old-plugin coexistence (ADR 0013): `llmwiki` already registered means
|
|
195
|
-
// the pre-rename dsh-llmwiki-memory 0.5.x is still loaded in this host.
|
|
196
|
-
// Our register below would hit the duplicate guard, migration would
|
|
197
|
-
// silently skip, and both plugins would drift onto separate bundles
|
|
198
|
-
// (split brain). Warn loudly instead of registering blind.
|
|
199
|
-
if (describe().some((entry) => entry.ns === LEGACY_NS)) {
|
|
200
|
-
warn?.('dsh-topics-memory: the legacy dsh-llmwiki-memory (0.5.x) is still running in this host — ' +
|
|
201
|
-
'settings migration was skipped because the "llmwiki" namespace is already registered. ' +
|
|
202
|
-
'Remove @aiwayds/dsh-llmwiki-memory from this profile so only dsh-topics-memory loads.');
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
205
|
-
// Registering the legacy namespace is the only API that surfaces its
|
|
206
|
-
// stored section; a duplicate or schema-invalid section throws — contained.
|
|
207
|
-
settings.register(LEGACY_NS, TopicsConfig);
|
|
208
|
-
const legacyUser = findUser(LEGACY_NS);
|
|
209
|
-
if (legacyUser === undefined)
|
|
210
|
-
return undefined;
|
|
211
|
-
const ops = CONFIG_KEYS.filter((k) => k in legacyUser && legacyUser[k] !== DEFAULTS[k]).map((k) => ({ op: 'set', path: [k], value: legacyUser[k] }));
|
|
212
|
-
if (ops.length === 0)
|
|
213
|
-
return undefined;
|
|
214
|
-
return settings.mutate?.call(settings, OWN_NS, ops)?.catch(() => undefined);
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
159
|
+
// dsh 0.1.7 settings: the runtime namespace registry (register/describe per
|
|
160
|
+
// plugin) is gone. A plugin's settings page is the projection of its `Config`
|
|
161
|
+
// schema below, and the settings namespace is the profile entry id
|
|
162
|
+
// (`dsh-topics-memory`, cordis.patch.yml) — the same spelling /topics set
|
|
163
|
+
// passes to ctx.settings.mutate(). A legacy top-level `topics:` section in
|
|
164
|
+
// settings.yaml is NOT auto-imported under this id — the host imports the
|
|
165
|
+
// old settings.yaml once, then renames it to settings.yaml.imported.
|
|
166
|
+
export const Config = TopicsConfig;
|
|
167
|
+
/** Settings namespace this plugin reads and writes: the profile entry id. */
|
|
168
|
+
const ENTRY_ID = 'dsh-topics-memory';
|
|
169
|
+
/** Snapshot one config slot: live ref → current value, plain value → itself. */
|
|
170
|
+
function readConfigValue(slot) {
|
|
171
|
+
const ref = slot;
|
|
172
|
+
return typeof ref?.get === 'function' ? ref.get() : slot;
|
|
219
173
|
}
|
|
220
174
|
// --- Bundled skill -----------------------------------------------------------
|
|
221
175
|
/** Provider name under `ctx.skills`; doubles as the skill name. */
|
|
@@ -229,7 +183,7 @@ const SKILL_RESOURCE_BASE = {
|
|
|
229
183
|
};
|
|
230
184
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
231
185
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
232
|
-
const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏/整理,或要配置 topics
|
|
186
|
+
const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏/整理,或要配置 topics 时先读本指南:设置页 `dsh-topics-memory` 条目全部键(repo/autoInject/topK/注入预算/蒸馏/整理/观察/图游走等)、/topics 命令族(onboard/status/distill/consolidate/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest。触发词:topics、记忆、topic、蒸馏、distill、整理、consolidate、合并重复、deprecatedTtl、usageBoost、使用加成、autoInject、记忆库、include-subagents。';
|
|
233
187
|
const SKILL_CANDIDATE = {
|
|
234
188
|
name: SKILL_PROVIDER_NAME,
|
|
235
189
|
description: SKILL_DESCRIPTION,
|
|
@@ -282,19 +236,16 @@ export function stripFrontmatter(raw) {
|
|
|
282
236
|
}
|
|
283
237
|
return raw;
|
|
284
238
|
}
|
|
285
|
-
export function apply(ctx) {
|
|
239
|
+
export async function apply(ctx, config = {}) {
|
|
286
240
|
// `inject = ['skills']` guarantees the service exists on every real host;
|
|
287
241
|
// register unconditionally so a missing service fails loud instead of
|
|
288
242
|
// silently dropping the bundled skill.
|
|
289
243
|
ctx.skills.registerProvider(() => skillProvider);
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
if (settingsNs === undefined)
|
|
294
|
-
return;
|
|
295
|
-
const scope = settingsNs.register(OWN_NS, TopicsConfig);
|
|
244
|
+
// 0.1.7 hosts validate the profile patch against `Config` above and hand
|
|
245
|
+
// apply() live volatile references; bare harnesses pass plain values (or
|
|
246
|
+
// nothing) — readConfigValue treats both shapes the same.
|
|
296
247
|
// Host logger access is best-effort: cordis always provides one, but bare
|
|
297
|
-
// test harnesses may not — the
|
|
248
|
+
// test harnesses may not — the logging stays silent rather than throwing.
|
|
298
249
|
const warn = (message) => {
|
|
299
250
|
try {
|
|
300
251
|
const logger = ctx.logger;
|
|
@@ -305,10 +256,6 @@ export function apply(ctx) {
|
|
|
305
256
|
// contained — a missing/broken logger must not break startup
|
|
306
257
|
}
|
|
307
258
|
};
|
|
308
|
-
// Carry user-tuned values over from the pre-rename namespace (one-time,
|
|
309
|
-
// fail-open, idempotent — see migrateLegacySettings). Fire-and-forget: the
|
|
310
|
-
// write settles on its own; a rejection only skips the migration.
|
|
311
|
-
void migrateLegacySettings(settingsNs, warn);
|
|
312
259
|
// Low-noise observability for the fast lane's early exits. The 2026-09-21
|
|
313
260
|
// silent-death incident (0.1.5-rc.2 claim order) died precisely because
|
|
314
261
|
// every exit path was silent: the injected log stayed empty for weeks
|
|
@@ -326,7 +273,14 @@ export function apply(ctx) {
|
|
|
326
273
|
}
|
|
327
274
|
};
|
|
328
275
|
const cfgNow = () => {
|
|
329
|
-
const v =
|
|
276
|
+
const v = {};
|
|
277
|
+
for (const k of CONFIG_KEYS) {
|
|
278
|
+
const slot = config[k];
|
|
279
|
+
// Skip absent keys so the DEFAULTS fold below keeps them (an explicit
|
|
280
|
+
// undefined spread would wipe the default).
|
|
281
|
+
if (slot !== undefined)
|
|
282
|
+
v[k] = readConfigValue(slot);
|
|
283
|
+
}
|
|
330
284
|
// Schema defaults may not be applied by bare test harnesses; fill them in.
|
|
331
285
|
return { ...DEFAULTS, ...v };
|
|
332
286
|
};
|
|
@@ -616,6 +570,11 @@ export function apply(ctx) {
|
|
|
616
570
|
let logEvents;
|
|
617
571
|
if (typeof seq === 'number') {
|
|
618
572
|
try {
|
|
573
|
+
// Soft-deprecated surface, deliberately kept (dsh 0.17 migration):
|
|
574
|
+
// `snapshotEvents` still ships in dsh-session 0.1.7-rc.1 and this is
|
|
575
|
+
// the replay's data source, reachable only behind event.seq with a
|
|
576
|
+
// full projection-read fallback below. Migrating to registered
|
|
577
|
+
// SessionMessageProjections is a dedicated project, not a drive-by.
|
|
619
578
|
const snapshot = session.snapshotEvents;
|
|
620
579
|
if (typeof snapshot === 'function')
|
|
621
580
|
logEvents = snapshot.call(session, 0, seq);
|
|
@@ -668,10 +627,6 @@ export function apply(ctx) {
|
|
|
668
627
|
if (event.type === 'turn/end')
|
|
669
628
|
dispatchSlowLane(sessionId);
|
|
670
629
|
}));
|
|
671
|
-
// Session-start boot chain dedup: rc.2's creation transaction emits BOTH
|
|
672
|
-
// `agent/session-start` and `agent/created`, so the pair collapses to one
|
|
673
|
-
// chain run per session; teardown clears the entry.
|
|
674
|
-
const bootedSessions = new Set();
|
|
675
630
|
// ---- Session teardown: real cordis events, not session/event types ----
|
|
676
631
|
// dsh-session 0.1.2-alpha.4's SessionEventMap has no `agent/disposed` or
|
|
677
632
|
// `session/disposed` event type — the old `event.type === 'agent/disposed'`
|
|
@@ -696,7 +651,6 @@ export function apply(ctx) {
|
|
|
696
651
|
observer.onSessionEvent(sessionId, name, undefined);
|
|
697
652
|
injectedBySession.delete(sessionId);
|
|
698
653
|
slowLane.clear(sessionId);
|
|
699
|
-
bootedSessions.delete(sessionId);
|
|
700
654
|
// A teardown-triggered run (session-end distill) reads the payload
|
|
701
655
|
// capture lazily: drop the entry here only when no run can still read
|
|
702
656
|
// it. The slow lane's in-flight pipeline reads it too (shared caller);
|
|
@@ -704,91 +658,128 @@ export function apply(ctx) {
|
|
|
704
658
|
releaseSessionLlm(sessionId);
|
|
705
659
|
}));
|
|
706
660
|
}
|
|
707
|
-
// ----
|
|
661
|
+
// ---- Boot chain: pull on session start, replay-distill, consolidate, TTL ----
|
|
708
662
|
// Session start is an agent-bus event, never a session/event firehose type:
|
|
709
|
-
// the firehose only carries Session.append types.
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
663
|
+
// the firehose only carries Session.append types. dsh 0.1.7 announces ONE
|
|
664
|
+
// serial `agent/created` event (payload { agent, source, signal? }, source
|
|
665
|
+
// ∈ 'startup' | 'resume' | 'clear' | 'compact') and `agent/session-start`
|
|
666
|
+
// is gone with no shim — 0.16.1's dual-name registration collapses to a
|
|
667
|
+
// single listener, and with it the per-session dedup Set: the registry
|
|
668
|
+
// guarantees exactly one announce per agent entry (a second announce
|
|
669
|
+
// throws), so a 'clear'/'compact' re-creation legitimately re-runs the
|
|
670
|
+
// chain on its fresh entry and teardown has nothing to re-arm. Only
|
|
671
|
+
// { agent } is structural; the rest is defensive.
|
|
672
|
+
//
|
|
673
|
+
// BOOT GUARANTEE (dsh 0.1.7): the serial dispatch awaits every
|
|
674
|
+
// `agent/created` listener and a throw or rejection ROLLS THE ENTIRE AGENT
|
|
675
|
+
// CREATION BACK. This listener is therefore total and synchronous — it
|
|
676
|
+
// never throws, never returns a promise — and the chain is deferred off
|
|
677
|
+
// the creation transaction entirely (setImmediate, unref'd): the pull /
|
|
678
|
+
// replay-distill / consolidation slow work starts only after the serial
|
|
679
|
+
// dispatch has moved on. Plugin initialization must never block, delay,
|
|
680
|
+
// or abort session startup; a chain failure at most warns.
|
|
716
681
|
const runBootChain = (sessionId) => {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
.
|
|
729
|
-
.then((
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
//
|
|
682
|
+
// Contained at the top too: a synchronous prologue throw (config read,
|
|
683
|
+
// sync bookkeeping) must degrade to a warn, not leak toward the host.
|
|
684
|
+
try {
|
|
685
|
+
void sync
|
|
686
|
+
.pull()
|
|
687
|
+
.catch((error) => {
|
|
688
|
+
// The pull failing must stay non-fatal for the chain (replay and
|
|
689
|
+
// TTL still run on the local pool), but no longer silent — a dead
|
|
690
|
+
// sync route should leave a host-log trace.
|
|
691
|
+
warn(`dsh-topics-memory 启动同步 pull 失败(继续本地启动链):${error instanceof Error ? error.message : String(error)}`);
|
|
692
|
+
})
|
|
693
|
+
.finally(() => store.ensure().catch(() => undefined))
|
|
694
|
+
.then(() => {
|
|
695
|
+
// Boot replay: the observations JSONL is the durable distill queue.
|
|
696
|
+
// Whatever the previous exit skipped (local-only exit since the
|
|
697
|
+
// no-network exit commit) or a killed run left unmarked is still
|
|
698
|
+
// undistilled here. Empty pool and no-model are cheap no-ops inside
|
|
699
|
+
// the distiller; the per-session dedup guards double runs.
|
|
700
|
+
void store
|
|
701
|
+
.undistilledObservations(1)
|
|
702
|
+
.then((pending) => {
|
|
703
|
+
if (pending.length === 0)
|
|
704
|
+
return;
|
|
705
|
+
// Same trigger-time capture the observer callback does: the
|
|
706
|
+
// replay run's caller resolves candidates lazily and a fresh
|
|
707
|
+
// session has no prior capture to lean on.
|
|
708
|
+
try {
|
|
709
|
+
captureFromAgent(agents()?.get(sessionId));
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// contained — the run below fails with the readable
|
|
713
|
+
// no-adapter detail instead
|
|
714
|
+
}
|
|
715
|
+
const run = distiller.request(sessionId, 'boot-replay');
|
|
716
|
+
if (run !== undefined)
|
|
717
|
+
void run.catch(() => undefined);
|
|
718
|
+
})
|
|
719
|
+
.catch(() => undefined);
|
|
720
|
+
// Consolidation cadence check — after the pull (freshest pool),
|
|
721
|
+
// fully fire-and-forget: cadence/single-flight gates live inside
|
|
722
|
+
// maybeRun, and a no-model boot just skips (no stamp advanced).
|
|
723
|
+
// Failures are NOT silent at the host log: the 2026-09-09 real-host
|
|
724
|
+
// test showed a dead distill route would otherwise hide here with
|
|
725
|
+
// zero observable trace (the state file only advances on success).
|
|
735
726
|
try {
|
|
736
727
|
captureFromAgent(agents()?.get(sessionId));
|
|
737
728
|
}
|
|
738
729
|
catch {
|
|
739
|
-
// contained — the run below fails with
|
|
740
|
-
// no-adapter detail instead
|
|
741
|
-
}
|
|
742
|
-
const run = distiller.request(sessionId, 'boot-replay');
|
|
743
|
-
if (run !== undefined)
|
|
744
|
-
void run.catch(() => undefined);
|
|
745
|
-
})
|
|
746
|
-
.catch(() => undefined);
|
|
747
|
-
// Consolidation cadence check — after the pull (freshest pool),
|
|
748
|
-
// fully fire-and-forget: cadence/single-flight gates live inside
|
|
749
|
-
// maybeRun, and a no-model boot just skips (no stamp advanced).
|
|
750
|
-
// Failures are NOT silent at the host log: the 2026-09-09 real-host
|
|
751
|
-
// test showed a dead distill route would otherwise hide here with
|
|
752
|
-
// zero observable trace (the state file only advances on success).
|
|
753
|
-
try {
|
|
754
|
-
captureFromAgent(agents()?.get(sessionId));
|
|
755
|
-
}
|
|
756
|
-
catch {
|
|
757
|
-
// contained — the run below fails with a readable no-model detail
|
|
758
|
-
}
|
|
759
|
-
void consolidator
|
|
760
|
-
.maybeRun({ sessionId })
|
|
761
|
-
.then((r) => {
|
|
762
|
-
if (r !== undefined && !r.ok && r.reason !== 'no-clusters') {
|
|
763
|
-
warn(`dsh-topics-memory 整理 lane 未执行:${r.reason ?? 'unknown'}${r.detail !== undefined ? `(${r.detail})` : ''}`);
|
|
730
|
+
// contained — the run below fails with a readable no-model detail
|
|
764
731
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const ttl = cfgNow().deprecatedTtlDays;
|
|
771
|
-
if (ttl > 0) {
|
|
772
|
-
void dropExpiredDeprecated(service, ttl)
|
|
773
|
-
.then((dropped) => {
|
|
774
|
-
if (dropped.length > 0) {
|
|
775
|
-
warn(`dsh-topics-memory:按 ${ttl} 天 TTL 删除了 ${dropped.length} 条 deprecated topic(${dropped.map((s) => `topics/${s}`).join('、')});git 历史可找回`);
|
|
732
|
+
void consolidator
|
|
733
|
+
.maybeRun({ sessionId })
|
|
734
|
+
.then((r) => {
|
|
735
|
+
if (r !== undefined && !r.ok && r.reason !== 'no-clusters') {
|
|
736
|
+
warn(`dsh-topics-memory 整理 lane 未执行:${r.reason ?? 'unknown'}${r.detail !== undefined ? `(${r.detail})` : ''}`);
|
|
776
737
|
}
|
|
777
738
|
})
|
|
778
739
|
.catch(() => undefined);
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
740
|
+
// Deprecated-TTL sweep — pure local rule, no model, runs even when
|
|
741
|
+
// the distill route is unconfigured. Drops are logged: deletion is
|
|
742
|
+
// the one housekeeping action the user should always see happened.
|
|
743
|
+
const ttl = cfgNow().deprecatedTtlDays;
|
|
744
|
+
if (ttl > 0) {
|
|
745
|
+
void dropExpiredDeprecated(service, ttl)
|
|
746
|
+
.then((dropped) => {
|
|
747
|
+
if (dropped.length > 0) {
|
|
748
|
+
warn(`dsh-topics-memory:按 ${ttl} 天 TTL 删除了 ${dropped.length} 条 deprecated topic(${dropped.map((s) => `topics/${s}`).join('、')});git 历史可找回`);
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
.catch(() => undefined);
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
.catch((error) => {
|
|
755
|
+
warn(`dsh-topics-memory 启动链失败(不影响会话启动):${error instanceof Error ? error.message : String(error)}`);
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
warn(`dsh-topics-memory 启动链无法启动(不影响会话启动):${error instanceof Error ? error.message : String(error)}`);
|
|
760
|
+
}
|
|
782
761
|
};
|
|
783
|
-
|
|
784
|
-
|
|
762
|
+
ctx.on('agent/created', ((payload) => {
|
|
763
|
+
// The boot-fallback contract lives HERE, not inside runBootChain: dsh
|
|
764
|
+
// 0.1.7 rolls agent creation back when this listener throws, so even a
|
|
765
|
+
// hostile/malformed payload (a throwing getter, a proxy trap) degrades to
|
|
766
|
+
// a warn. Returning undefined (never a thenable) keeps the serial
|
|
767
|
+
// dispatch from awaiting anything.
|
|
768
|
+
try {
|
|
785
769
|
const sessionId = String(payload?.agent?.id ?? '');
|
|
786
|
-
if (sessionId === ''
|
|
770
|
+
if (sessionId === '')
|
|
787
771
|
return;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
772
|
+
// Off the creation transaction: the unref'd immediate fires after the
|
|
773
|
+
// current serial dispatch slice, so the chain's synchronous prologue
|
|
774
|
+
// (config read, sync state) never delays the remaining listeners nor
|
|
775
|
+
// the loop start, and the timer cannot hold a dying process open.
|
|
776
|
+
const deferred = setImmediate(() => runBootChain(sessionId));
|
|
777
|
+
deferred.unref?.();
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
warn(`dsh-topics-memory boot 链触发失败(不影响会话启动):${error instanceof Error ? error.message : String(error)}`);
|
|
781
|
+
}
|
|
782
|
+
}));
|
|
792
783
|
ctx.effect(() => {
|
|
793
784
|
void store.ensure().catch(() => undefined);
|
|
794
785
|
// Async disposer: cordis awaits it during unload (Disposable may be
|
|
@@ -821,7 +812,9 @@ export function apply(ctx) {
|
|
|
821
812
|
const mutate = async (ops) => {
|
|
822
813
|
if (settingsMutator?.mutate === undefined)
|
|
823
814
|
throw new Error('settings 服务不可用,无法写入配置');
|
|
824
|
-
|
|
815
|
+
// 0.1.7: the settings namespace is the profile entry id, not the old
|
|
816
|
+
// `topics` namespace name.
|
|
817
|
+
await settingsMutator.mutate(ENTRY_ID, ops);
|
|
825
818
|
};
|
|
826
819
|
// Resolved lazily at invocation time: whichever UI registered the ask-user
|
|
827
820
|
// provider (TUI panel / web composer / feishu card, ask-router optional)
|
|
@@ -901,6 +894,27 @@ export function apply(ctx) {
|
|
|
901
894
|
};
|
|
902
895
|
cmdCtx.effect(() => commands.register(buildTopicsCommand(service, mutate, resolveAsk, resolveLlm, manualDistill, manualConsolidate)), 'topics: /topics');
|
|
903
896
|
});
|
|
897
|
+
// ---- One-time legacy settings import (0.1.5 → 0.1.7 upgrade path) ----
|
|
898
|
+
// The 0.1.7 host imports the old settings.yaml ONCE by "section name =
|
|
899
|
+
// entry id" and renames it settings.yaml.imported — this plugin's legacy
|
|
900
|
+
// section was named `topics`, so the host's import silently dropped it.
|
|
901
|
+
// Recover it here (see legacy-import.ts). Awaited as apply()'s LAST step:
|
|
902
|
+
// every registration above stays synchronous (fake-ctx tests dispatch
|
|
903
|
+
// immediately), while a real boot — whose loader awaits the fiber setup —
|
|
904
|
+
// also settles the import before activation completes. Fully contained:
|
|
905
|
+
// any failure only warns and leaves the audit marker unwritten, so the
|
|
906
|
+
// next boot retries; activation never depends on this.
|
|
907
|
+
try {
|
|
908
|
+
await runLegacySettingsImport({
|
|
909
|
+
home: paths.resolveDshHome(),
|
|
910
|
+
settings: ctx.settings,
|
|
911
|
+
logger: ctx.logger,
|
|
912
|
+
getCurrent: (key) => cfgNow()[key],
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
warn(`dsh-topics-memory 旧 settings 迁移失败(不影响启动,下次启动重试):${error instanceof Error ? error.message : String(error)}`);
|
|
917
|
+
}
|
|
904
918
|
}
|
|
905
919
|
/** Config defaults for harnesses that skip schemastery's default application. */
|
|
906
920
|
const DEFAULTS = {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time legacy settings import for the 0.1.5 → 0.1.7 upgrade: the dsh
|
|
3
|
+
* 0.1.7 host renames the old `~/.dsh/settings.yaml` to
|
|
4
|
+
* `settings.yaml.imported` and imports its sections as profile patches keyed
|
|
5
|
+
* by "section name = entry id". This plugin's old section was named `topics`,
|
|
6
|
+
* NOT `dsh-topics-memory` — the host's one-shot import silently drops it.
|
|
7
|
+
* This module recovers it once:
|
|
8
|
+
*
|
|
9
|
+
* 1. read `<home>/settings.yaml.imported` (preferred) →
|
|
10
|
+
* `<home>/settings.yaml` (fallback);
|
|
11
|
+
* 2. flat-parse the `topics:` section (scalars only, conservative);
|
|
12
|
+
* 3. keep keys whose legacy value differs from the current effective value;
|
|
13
|
+
* 4. `settings.update('dsh-topics-memory', patch)` → on success write the
|
|
14
|
+
* audit marker `<home>/storages/dsh-topics-memory/legacy-import.json`;
|
|
15
|
+
* on failure warn and write nothing (the next boot retries);
|
|
16
|
+
* 5. marker present → the whole migration is skipped (idempotent — a
|
|
17
|
+
* later manual edit of the imported file must not resurrect old values).
|
|
18
|
+
*
|
|
19
|
+
* A marker is written for EVERY settled outcome (imported / no-op /
|
|
20
|
+
* no-legacy / no-section) so this runs at most once per home. Never throws
|
|
21
|
+
* into the caller beyond what the apply()-side catch contains: plugin
|
|
22
|
+
* activation must not depend on this.
|
|
23
|
+
*
|
|
24
|
+
* @module legacy-import
|
|
25
|
+
*/
|
|
26
|
+
/** Profile entry id the recovered values are written to. */
|
|
27
|
+
export declare const ENTRY_ID = "dsh-topics-memory";
|
|
28
|
+
/** Legacy top-level section name in the old settings.yaml. */
|
|
29
|
+
export declare const LEGACY_SECTION = "topics";
|
|
30
|
+
/** Storage dir name for the audit marker (repo dir name, host convention). */
|
|
31
|
+
export declare const STORAGE_DIR_NAME = "dsh-topics-memory";
|
|
32
|
+
/** Legacy keys worth recovering, mapped 1:1 onto Config keys of the same name. */
|
|
33
|
+
export declare const LEGACY_KEYS: readonly ["repo", "distillProvider", "distillModel", "topK", "totalBudget", "autoObserve"];
|
|
34
|
+
export type LegacyKey = (typeof LEGACY_KEYS)[number];
|
|
35
|
+
/** The settings seam the import writes through (`ctx.settings.update`). */
|
|
36
|
+
export interface SettingsUpdateSeam {
|
|
37
|
+
update(ns: string, patch: object): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** Minimal logger surface (both sinks optional — bare harnesses have none). */
|
|
40
|
+
export interface LegacyImportLogger {
|
|
41
|
+
info?(message: string): void;
|
|
42
|
+
warn?(message: string): void;
|
|
43
|
+
}
|
|
44
|
+
export interface LegacyImportInput {
|
|
45
|
+
/** Resolved dsh home (`paths.resolveDshHome()` at the call site). */
|
|
46
|
+
home: string;
|
|
47
|
+
/** `ctx.settings` — absent/unusable seams skip the import without a marker. */
|
|
48
|
+
settings?: SettingsUpdateSeam | undefined;
|
|
49
|
+
/** Host logger (optional). */
|
|
50
|
+
logger?: LegacyImportLogger | undefined;
|
|
51
|
+
/** Current effective value for one legacy key (Volatile ref `.get()` at the call site). */
|
|
52
|
+
getCurrent(key: LegacyKey): unknown;
|
|
53
|
+
}
|
|
54
|
+
export type LegacyImportOutcome = 'imported' | 'no-op' | 'no-legacy' | 'no-section' | 'marker-exists' | 'no-settings' | 'update-failed';
|
|
55
|
+
export interface LegacyImportResult {
|
|
56
|
+
outcome: LegacyImportOutcome;
|
|
57
|
+
/** Keys written through settings.update (also in the marker). */
|
|
58
|
+
imported: Record<string, unknown>;
|
|
59
|
+
/** Keys not imported, with the reason (audit trail). */
|
|
60
|
+
skipped: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
/** Candidates in priority order: the host's renamed document first. */
|
|
63
|
+
export declare function legacySettingsCandidates(home: string): string[];
|
|
64
|
+
/** Audit marker path: `<home>/storages/<dir>/legacy-import.json`. */
|
|
65
|
+
export declare function legacyMarkerPath(home: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Scalar inference for one flat `key: value` value. Quoted values are
|
|
68
|
+
* unquoted (mirroring src/yaml.ts); a single-line quoted flow-JSON value
|
|
69
|
+
* (`'["a","b"]'`) is JSON-parsed after unquoting — a malformed one stays a
|
|
70
|
+
* string. Unquoted flow collections are kept as strings (conservative:
|
|
71
|
+
* this is not a full YAML parser). Empty never reaches here (the line
|
|
72
|
+
* parser skips value-less keys — they open a nested block).
|
|
73
|
+
*/
|
|
74
|
+
export declare function inferScalar(raw: string): unknown;
|
|
75
|
+
/**
|
|
76
|
+
* Flat section parser: locate the top-level `section:` line and collect its
|
|
77
|
+
* DIRECT children (`key: value` at one common indent) until the next
|
|
78
|
+
* top-level key. Deliberately conservative — NOT a YAML parser:
|
|
79
|
+
* - `key:` with an empty value (opens a nested map/sequence) is skipped, and
|
|
80
|
+
* the deeper lines under it never match the child indent, so folded values
|
|
81
|
+
* are skipped whole;
|
|
82
|
+
* - lines at any other indent are ignored;
|
|
83
|
+
* - an inline value on the header line itself (`section: x`) is not a flat
|
|
84
|
+
* map → `{}`.
|
|
85
|
+
* Pure function — tests run it against literal strings.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parseFlatSection(raw: string, section: string): Record<string, unknown>;
|
|
88
|
+
/**
|
|
89
|
+
* Run the one-time import. Resolves with the outcome; only throws on truly
|
|
90
|
+
* unexpected I/O errors (the apply()-side catch contains those). The marker
|
|
91
|
+
* is written for every settled outcome EXCEPT the retry-worthy failures
|
|
92
|
+
* (`no-settings`, `update-failed`) — those must re-run at the next boot.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runLegacySettingsImport(input: LegacyImportInput): Promise<LegacyImportResult>;
|