@aiwayds/dsh-topics-memory 0.18.1 → 0.18.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/lib/index.js +16 -13
- package/lib/legacy-import.d.ts +15 -0
- package/lib/legacy-import.js +23 -4
- package/package.json +1 -1
- package/src/index.ts +16 -12
- package/src/legacy-import.ts +34 -4
package/lib/index.js
CHANGED
|
@@ -898,23 +898,26 @@ export async function apply(ctx, config = {}) {
|
|
|
898
898
|
// The 0.1.7 host imports the old settings.yaml ONCE by "section name =
|
|
899
899
|
// entry id" and renames it settings.yaml.imported — this plugin's legacy
|
|
900
900
|
// section was named `topics`, so the host's import silently dropped it.
|
|
901
|
-
// Recover it here (see legacy-import.ts).
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
901
|
+
// Recover it here (see legacy-import.ts). Wired through
|
|
902
|
+
// ctx.inject(['settings'], …) instead of a direct `ctx.settings` read: a
|
|
903
|
+
// real boot can run apply() before the settings service is mounted, the
|
|
904
|
+
// direct read came up undefined, and the import then took the silent
|
|
905
|
+
// no-settings exit EVERY boot (no marker, nothing ever recovered). The
|
|
906
|
+
// inject callback fires the moment the service exists (immediately when
|
|
907
|
+
// it already does — fake-ctx tests stay synchronous), fire-and-forget so
|
|
908
|
+
// activation never waits on it. Fully contained: any failure only warns
|
|
909
|
+
// and leaves the audit marker unwritten, so the next boot retries;
|
|
910
|
+
// activation never depends on this.
|
|
911
|
+
ctx.inject(['settings'], (sctx) => {
|
|
912
|
+
void runLegacySettingsImport({
|
|
909
913
|
home: paths.resolveDshHome(),
|
|
910
|
-
settings:
|
|
914
|
+
settings: sctx.settings,
|
|
911
915
|
logger: ctx.logger,
|
|
912
916
|
getCurrent: (key) => cfgNow()[key],
|
|
917
|
+
}).catch((error) => {
|
|
918
|
+
warn(`dsh-topics-memory 旧 settings 迁移失败(不影响启动,下次启动重试):${error instanceof Error ? error.message : String(error)}`);
|
|
913
919
|
});
|
|
914
|
-
}
|
|
915
|
-
catch (error) {
|
|
916
|
-
warn(`dsh-topics-memory 旧 settings 迁移失败(不影响启动,下次启动重试):${error instanceof Error ? error.message : String(error)}`);
|
|
917
|
-
}
|
|
920
|
+
});
|
|
918
921
|
}
|
|
919
922
|
/** Config defaults for harnesses that skip schemastery's default application. */
|
|
920
923
|
const DEFAULTS = {
|
package/lib/legacy-import.d.ts
CHANGED
|
@@ -50,6 +50,21 @@ export interface LegacyImportInput {
|
|
|
50
50
|
logger?: LegacyImportLogger | undefined;
|
|
51
51
|
/** Current effective value for one legacy key (Volatile ref `.get()` at the call site). */
|
|
52
52
|
getCurrent(key: LegacyKey): unknown;
|
|
53
|
+
/**
|
|
54
|
+
* Retry budget for `settings.update`. The host serializes profile writes
|
|
55
|
+
* (settings.update → dsh-config-editor) behind the `<profile>/package.json`
|
|
56
|
+
* writer lock with a 2s per-call deadline, and during BOOT the host itself
|
|
57
|
+
* holds that lock while preparing the profile — a single bare call almost
|
|
58
|
+
* always times out (2026-09-25 incident: the import failed EVERY boot and
|
|
59
|
+
* the migration never converged). Defaults to 5 attempts 2s apart; a
|
|
60
|
+
* permanently orphaned lock (a SIGKILLed host never releases it — orphan
|
|
61
|
+
* recovery is an operator action) still exhausts the budget and stays
|
|
62
|
+
* retry-worthy. Tests shrink this to keep the suite fast.
|
|
63
|
+
*/
|
|
64
|
+
updateRetry?: {
|
|
65
|
+
attempts: number;
|
|
66
|
+
delayMs: number;
|
|
67
|
+
} | undefined;
|
|
53
68
|
}
|
|
54
69
|
export type LegacyImportOutcome = 'imported' | 'no-op' | 'no-legacy' | 'no-section' | 'marker-exists' | 'no-settings' | 'update-failed';
|
|
55
70
|
export interface LegacyImportResult {
|
package/lib/legacy-import.js
CHANGED
|
@@ -160,8 +160,11 @@ export async function runLegacySettingsImport(input) {
|
|
|
160
160
|
// A settings seam that cannot accept the patch is a retry-worthy state:
|
|
161
161
|
// return WITHOUT reading/marking, so a host that mounts the service later
|
|
162
162
|
// (and a test harness pointing at a real home) neither imports nor writes.
|
|
163
|
+
// Not silent: the 2026-09-24 incident hid exactly behind this quiet exit —
|
|
164
|
+
// the warn is what makes a never-mounting seam observable in a boot log.
|
|
163
165
|
const settings = input.settings;
|
|
164
166
|
if (settings === undefined || typeof settings.update !== 'function') {
|
|
167
|
+
warn(`${ENTRY_ID}: legacy settings import deferred: settings seam unusable (no-settings; will retry next boot)`);
|
|
165
168
|
return { outcome: 'no-settings', imported, skipped };
|
|
166
169
|
}
|
|
167
170
|
// Locate the legacy document.
|
|
@@ -198,12 +201,28 @@ export async function runLegacySettingsImport(input) {
|
|
|
198
201
|
info(`${ENTRY_ID}: legacy settings import (${LEGACY_SECTION} → ${ENTRY_ID}): nothing to import (all values equal or unknown)`);
|
|
199
202
|
return { outcome: 'no-op', imported, skipped };
|
|
200
203
|
}
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
// The update contends on the host's profile writer lock (see
|
|
205
|
+
// {@link LegacyImportInput.updateRetry}): retry with a fixed gap before
|
|
206
|
+
// conceding. Every failed attempt warns — a silent loss here cost a full
|
|
207
|
+
// day of "the migration never runs" debugging (2026-09-25).
|
|
208
|
+
const retry = input.updateRetry ?? { attempts: 5, delayMs: 2_000 };
|
|
209
|
+
let updateFailed;
|
|
210
|
+
for (let attempt = 1; attempt <= retry.attempts; attempt++) {
|
|
211
|
+
try {
|
|
212
|
+
await settings.update(ENTRY_ID, imported);
|
|
213
|
+
updateFailed = undefined;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
updateFailed = error;
|
|
218
|
+
warn(`${ENTRY_ID}: legacy settings import: settings.update attempt ${attempt}/${retry.attempts} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
if (attempt < retry.attempts)
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, retry.delayMs));
|
|
221
|
+
}
|
|
203
222
|
}
|
|
204
|
-
|
|
223
|
+
if (updateFailed !== undefined) {
|
|
224
|
+
warn(`${ENTRY_ID}: legacy settings import failed (will retry next boot): ${updateFailed instanceof Error ? updateFailed.message : String(updateFailed)}`);
|
|
205
225
|
// No marker: the next boot retries the import.
|
|
206
|
-
warn(`${ENTRY_ID}: legacy settings import failed (will retry next boot): ${error instanceof Error ? error.message : String(error)}`);
|
|
207
226
|
return { outcome: 'update-failed', imported, skipped };
|
|
208
227
|
}
|
|
209
228
|
writeMarker(markerPath, { at: new Date().toISOString(), outcome: 'imported', source: sourceName, imported, skipped });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-topics-memory",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"description": "dsh-plugin: quality-first topic memory for dsh — a topic keeps the starting question, conclusion, impact and dependencies, not the process; local-first git-traced OKF bundle, budgeted LLM-free injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/index.ts
CHANGED
|
@@ -1016,22 +1016,26 @@ export async function apply(ctx: Context, config: TopicsRuntimeConfig = {}): Pro
|
|
|
1016
1016
|
// The 0.1.7 host imports the old settings.yaml ONCE by "section name =
|
|
1017
1017
|
// entry id" and renames it settings.yaml.imported — this plugin's legacy
|
|
1018
1018
|
// section was named `topics`, so the host's import silently dropped it.
|
|
1019
|
-
// Recover it here (see legacy-import.ts).
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1019
|
+
// Recover it here (see legacy-import.ts). Wired through
|
|
1020
|
+
// ctx.inject(['settings'], …) instead of a direct `ctx.settings` read: a
|
|
1021
|
+
// real boot can run apply() before the settings service is mounted, the
|
|
1022
|
+
// direct read came up undefined, and the import then took the silent
|
|
1023
|
+
// no-settings exit EVERY boot (no marker, nothing ever recovered). The
|
|
1024
|
+
// inject callback fires the moment the service exists (immediately when
|
|
1025
|
+
// it already does — fake-ctx tests stay synchronous), fire-and-forget so
|
|
1026
|
+
// activation never waits on it. Fully contained: any failure only warns
|
|
1027
|
+
// and leaves the audit marker unwritten, so the next boot retries;
|
|
1028
|
+
// activation never depends on this.
|
|
1029
|
+
ctx.inject(['settings'], (sctx) => {
|
|
1030
|
+
void runLegacySettingsImport({
|
|
1027
1031
|
home: paths.resolveDshHome(),
|
|
1028
|
-
settings:
|
|
1032
|
+
settings: (sctx as unknown as { settings?: SettingsUpdateSeam }).settings,
|
|
1029
1033
|
logger: (ctx as unknown as { logger?: LegacyImportLogger }).logger,
|
|
1030
1034
|
getCurrent: (key) => cfgNow()[key],
|
|
1035
|
+
}).catch((error) => {
|
|
1036
|
+
warn(`dsh-topics-memory 旧 settings 迁移失败(不影响启动,下次启动重试):${error instanceof Error ? error.message : String(error)}`)
|
|
1031
1037
|
})
|
|
1032
|
-
}
|
|
1033
|
-
warn(`dsh-topics-memory 旧 settings 迁移失败(不影响启动,下次启动重试):${error instanceof Error ? error.message : String(error)}`)
|
|
1034
|
-
}
|
|
1038
|
+
})
|
|
1035
1039
|
}
|
|
1036
1040
|
|
|
1037
1041
|
/** Config defaults for harnesses that skip schemastery's default application. */
|
package/src/legacy-import.ts
CHANGED
|
@@ -61,6 +61,18 @@ export interface LegacyImportInput {
|
|
|
61
61
|
logger?: LegacyImportLogger | undefined
|
|
62
62
|
/** Current effective value for one legacy key (Volatile ref `.get()` at the call site). */
|
|
63
63
|
getCurrent(key: LegacyKey): unknown
|
|
64
|
+
/**
|
|
65
|
+
* Retry budget for `settings.update`. The host serializes profile writes
|
|
66
|
+
* (settings.update → dsh-config-editor) behind the `<profile>/package.json`
|
|
67
|
+
* writer lock with a 2s per-call deadline, and during BOOT the host itself
|
|
68
|
+
* holds that lock while preparing the profile — a single bare call almost
|
|
69
|
+
* always times out (2026-09-25 incident: the import failed EVERY boot and
|
|
70
|
+
* the migration never converged). Defaults to 5 attempts 2s apart; a
|
|
71
|
+
* permanently orphaned lock (a SIGKILLed host never releases it — orphan
|
|
72
|
+
* recovery is an operator action) still exhausts the budget and stays
|
|
73
|
+
* retry-worthy. Tests shrink this to keep the suite fast.
|
|
74
|
+
*/
|
|
75
|
+
updateRetry?: { attempts: number; delayMs: number } | undefined
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export type LegacyImportOutcome =
|
|
@@ -200,8 +212,11 @@ export async function runLegacySettingsImport(input: LegacyImportInput): Promise
|
|
|
200
212
|
// A settings seam that cannot accept the patch is a retry-worthy state:
|
|
201
213
|
// return WITHOUT reading/marking, so a host that mounts the service later
|
|
202
214
|
// (and a test harness pointing at a real home) neither imports nor writes.
|
|
215
|
+
// Not silent: the 2026-09-24 incident hid exactly behind this quiet exit —
|
|
216
|
+
// the warn is what makes a never-mounting seam observable in a boot log.
|
|
203
217
|
const settings = input.settings
|
|
204
218
|
if (settings === undefined || typeof settings.update !== 'function') {
|
|
219
|
+
warn(`${ENTRY_ID}: legacy settings import deferred: settings seam unusable (no-settings; will retry next boot)`)
|
|
205
220
|
return { outcome: 'no-settings', imported, skipped }
|
|
206
221
|
}
|
|
207
222
|
|
|
@@ -241,11 +256,26 @@ export async function runLegacySettingsImport(input: LegacyImportInput): Promise
|
|
|
241
256
|
return { outcome: 'no-op', imported, skipped }
|
|
242
257
|
}
|
|
243
258
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
// The update contends on the host's profile writer lock (see
|
|
260
|
+
// {@link LegacyImportInput.updateRetry}): retry with a fixed gap before
|
|
261
|
+
// conceding. Every failed attempt warns — a silent loss here cost a full
|
|
262
|
+
// day of "the migration never runs" debugging (2026-09-25).
|
|
263
|
+
const retry = input.updateRetry ?? { attempts: 5, delayMs: 2_000 }
|
|
264
|
+
let updateFailed: unknown
|
|
265
|
+
for (let attempt = 1; attempt <= retry.attempts; attempt++) {
|
|
266
|
+
try {
|
|
267
|
+
await settings.update(ENTRY_ID, imported)
|
|
268
|
+
updateFailed = undefined
|
|
269
|
+
break
|
|
270
|
+
} catch (error) {
|
|
271
|
+
updateFailed = error
|
|
272
|
+
warn(`${ENTRY_ID}: legacy settings import: settings.update attempt ${attempt}/${retry.attempts} failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
273
|
+
if (attempt < retry.attempts) await new Promise((resolve) => setTimeout(resolve, retry.delayMs))
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (updateFailed !== undefined) {
|
|
277
|
+
warn(`${ENTRY_ID}: legacy settings import failed (will retry next boot): ${updateFailed instanceof Error ? updateFailed.message : String(updateFailed)}`)
|
|
247
278
|
// No marker: the next boot retries the import.
|
|
248
|
-
warn(`${ENTRY_ID}: legacy settings import failed (will retry next boot): ${error instanceof Error ? error.message : String(error)}`)
|
|
249
279
|
return { outcome: 'update-failed', imported, skipped }
|
|
250
280
|
}
|
|
251
281
|
|