@goodandready/dsh-clinebot 0.5.13 → 0.5.15

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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to `@goodandready/dsh-clinebot` will be documented in this f
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.5.15] - 2026-10-02
9
+
10
+ ### Fixed
11
+ - **Atomic Catalog Snapshot Reset on Settings Persistence (Issue #192)**: In `settingsApi.update` and `settingsApi.replace`, defer invalidating the verified model catalog snapshot (`resetCatalogSnapshot()`) and updating `lastCatalogIdentity` until after persistence to the DSH settings service completes successfully. If settings persistence fails (e.g. revision conflict or rejection), local in-memory dynamic models and plan synchronization timestamp are strictly preserved, preventing catalog loss when endpoint and credentials remain unchanged.
12
+
13
+ ## [0.5.14] - 2026-10-02
14
+
15
+ ### Fixed
16
+ - **Catalog Identity Lifecycle on Endpoint & Primary Credential Switch (Issue #168)**: Unified catalog invalidation under an identity contract: the verified catalog and plan synchronization timestamp strictly belong to the identity tuple (`baseUrl::activeAccount::apiKeyEnv`). Changing primary credential reference (`apiKeyEnv`) or provider endpoint (`baseUrl`) — via native Cosmokit volatile updates or `PUT /dsh-clinebot/config` — immediately resets `dynamicModels = []`, `planSyncedAt = 0`, and purges both usage and probe caches (`clearUsageCache()`, `clearProbeCache()`), preventing foreign or unverified model snapshots from persisting across endpoint or credential changes.
17
+
8
18
  ## [0.5.13] - 2026-10-02
9
19
 
10
20
  ### Fixed
package/lib/index.js CHANGED
@@ -106,25 +106,35 @@ export function apply(ctx, config) {
106
106
  }
107
107
  }
108
108
 
109
- let lastActiveAccount = publicConfig(live()).activeAccount
109
+ const normalizeEndpoint = (url) => String(url || '').trim().replace(/\/+$/, '')
110
+ const getCatalogIdentity = (cfg) => {
111
+ const pub = publicConfig(cfg)
112
+ return `${normalizeEndpoint(pub.baseUrl)}::${String(pub.activeAccount || '').trim()}::${String(pub.apiKeyEnv || '').trim()}`
113
+ }
114
+
115
+ let lastCatalogIdentity = getCatalogIdentity(live())
116
+ const resetCatalogSnapshot = () => {
117
+ try {
118
+ if (currentConfig && typeof currentConfig === 'object') {
119
+ currentConfig.dynamicModels = []
120
+ currentConfig.planSyncedAt = 0
121
+ }
122
+ } catch {
123
+ /* ignore read-only or frozen */
124
+ }
125
+ overrides.dynamicModels = []
126
+ overrides.planSyncedAt = 0
127
+ clearUsageCache()
128
+ clearProbeCache()
129
+ }
130
+
110
131
  if (typeof ctx?.on === 'function') {
111
132
  ctx.on('loader/volatile-update', () => {
112
133
  const liveCfg = live()
113
- const currentActive = publicConfig(liveCfg).activeAccount
114
- if (currentActive !== lastActiveAccount) {
115
- lastActiveAccount = currentActive
116
- try {
117
- if (currentConfig && typeof currentConfig === 'object') {
118
- currentConfig.dynamicModels = []
119
- currentConfig.planSyncedAt = 0
120
- }
121
- } catch {
122
- /* ignore read-only or frozen */
123
- }
124
- overrides.dynamicModels = []
125
- overrides.planSyncedAt = 0
126
- clearUsageCache()
127
- clearProbeCache()
134
+ const currentIdentity = getCatalogIdentity(liveCfg)
135
+ if (currentIdentity !== lastCatalogIdentity) {
136
+ lastCatalogIdentity = currentIdentity
137
+ resetCatalogSnapshot()
128
138
  }
129
139
  syncStatsStorage(liveCfg)
130
140
  })
@@ -181,7 +191,14 @@ export function apply(ctx, config) {
181
191
  if (!svc || (typeof svc.replace !== 'function' && typeof svc.update !== 'function')) {
182
192
  throw new Error('DSH settings service is unavailable or non-writable')
183
193
  }
194
+ const prevIdentity = getCatalogIdentity(live())
184
195
  const parsed = Config(structuredClone(plainConfig(next)))
196
+ const nextIdentity = getCatalogIdentity(parsed)
197
+ const identityChanged = nextIdentity !== prevIdentity
198
+ if (identityChanged) {
199
+ if (!('dynamicModels' in next)) parsed.dynamicModels = []
200
+ if (!('planSyncedAt' in next)) parsed.planSyncedAt = 0
201
+ }
185
202
  const payload = volatileConfig(parsed)
186
203
  const rev = getRevision(svc)
187
204
  if (typeof svc.replace === 'function') {
@@ -189,6 +206,10 @@ export function apply(ctx, config) {
189
206
  } else {
190
207
  await svc.update(NS, payload, rev)
191
208
  }
209
+ if (identityChanged) {
210
+ lastCatalogIdentity = nextIdentity
211
+ resetCatalogSnapshot()
212
+ }
192
213
  applyConfigMutations(currentConfig, parsed)
193
214
  syncStatsStorage(live())
194
215
  await syncProviderState(live())
@@ -199,7 +220,14 @@ export function apply(ctx, config) {
199
220
  if (!svc || (typeof svc.update !== 'function' && typeof svc.replace !== 'function')) {
200
221
  throw new Error('DSH settings service is unavailable or non-writable')
201
222
  }
223
+ const prevIdentity = getCatalogIdentity(live())
202
224
  const merged = Config({ ...publicConfig(live()), ...plainConfig(patch) })
225
+ const nextIdentity = getCatalogIdentity(merged)
226
+ const identityChanged = nextIdentity !== prevIdentity
227
+ if (identityChanged) {
228
+ if (!('dynamicModels' in patch)) merged.dynamicModels = []
229
+ if (!('planSyncedAt' in patch)) merged.planSyncedAt = 0
230
+ }
203
231
  const payload = volatileConfig(merged)
204
232
  const rev = getRevision(svc)
205
233
  if (typeof svc.update === 'function') {
@@ -207,6 +235,10 @@ export function apply(ctx, config) {
207
235
  } else {
208
236
  await svc.replace(NS, payload, rev)
209
237
  }
238
+ if (identityChanged) {
239
+ lastCatalogIdentity = nextIdentity
240
+ resetCatalogSnapshot()
241
+ }
210
242
  applyConfigMutations(currentConfig, merged)
211
243
  syncStatsStorage(live())
212
244
  await syncProviderState(live())
@@ -93,7 +93,10 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, syncProvider
93
93
  const base = plainConfig(live()) || {}
94
94
  delete base.enabledModels
95
95
  const merged = { ...base, ...rawPayload }
96
- if (rawPayload.activeAccount !== undefined && String(rawPayload.activeAccount).trim() !== String(base.activeAccount || "").trim()) {
96
+ const activeChanged = rawPayload.activeAccount !== undefined && String(rawPayload.activeAccount).trim() !== String(base.activeAccount || "").trim()
97
+ const apiKeyEnvChanged = rawPayload.apiKeyEnv !== undefined && String(rawPayload.apiKeyEnv).trim() !== String(base.apiKeyEnv || "").trim()
98
+ const baseUrlChanged = rawPayload.baseUrl !== undefined && String(rawPayload.baseUrl).trim().replace(/\/+$/, '') !== String(base.baseUrl || "").trim().replace(/\/+$/, '')
99
+ if (activeChanged || apiKeyEnvChanged || baseUrlChanged) {
97
100
  if (!("dynamicModels" in rawPayload)) merged.dynamicModels = []
98
101
  if (!("planSyncedAt" in rawPayload)) merged.planSyncedAt = 0
99
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-clinebot",
3
- "version": "0.5.13",
3
+ "version": "0.5.15",
4
4
  "description": "DeepSeek Harness companion for ClineBot / ClinePass: dynamic subscription models sync, quota exhaustion warnings, session metrics, dedicated settings page, live usage limits, and /cline slash-command.",
5
5
  "license": "MIT",
6
6
  "type": "module",