@klarkxy/dsh-plugin-kit 0.6.1 → 0.8.0

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.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/docs/README.zh-CN.md +1 -1
  3. package/lib/auxiliary-model-client.d.ts +4 -1
  4. package/lib/auxiliary-model-client.js +72 -33
  5. package/lib/auxiliary-model-client.js.map +1 -1
  6. package/lib/auxiliary-model.js +26 -2
  7. package/lib/auxiliary-model.js.map +1 -1
  8. package/lib/cli-media.js +1 -1
  9. package/lib/cli-process.d.ts +2 -0
  10. package/lib/cli-process.js +36 -2
  11. package/lib/cli-process.js.map +1 -1
  12. package/lib/client-logger.d.ts +19 -0
  13. package/lib/client-logger.js +153 -0
  14. package/lib/client-logger.js.map +1 -0
  15. package/lib/client-utils.d.ts +2 -1
  16. package/lib/client-utils.js +9 -3
  17. package/lib/client-utils.js.map +1 -1
  18. package/lib/domain.d.ts +12 -4
  19. package/lib/domain.js +32 -6
  20. package/lib/domain.js.map +1 -1
  21. package/lib/host-rpc.d.ts +2 -0
  22. package/lib/host-rpc.js +71 -2
  23. package/lib/host-rpc.js.map +1 -1
  24. package/lib/index.d.ts +3 -1
  25. package/lib/index.js +2 -1
  26. package/lib/llm-call.d.ts +5 -1
  27. package/lib/llm-call.js +79 -46
  28. package/lib/llm-call.js.map +1 -1
  29. package/lib/log-shared.d.ts +2 -0
  30. package/lib/log-shared.js +169 -0
  31. package/lib/log-shared.js.map +1 -0
  32. package/lib/log-shared2.d.ts +72 -0
  33. package/lib/logger.d.ts +87 -0
  34. package/lib/logger.js +572 -0
  35. package/lib/logger.js.map +1 -0
  36. package/lib/model-picker.d.ts +34 -0
  37. package/lib/model-picker.js +96 -0
  38. package/lib/model-picker.js.map +1 -0
  39. package/lib/official-ui.d.ts +1 -1
  40. package/lib/official-ui.js +3 -1
  41. package/lib/official-ui.js.map +1 -1
  42. package/lib/zod.js +79 -79
  43. package/lib/zod.js.map +1 -1
  44. package/package.json +34 -14
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [简体中文](docs/README.zh-CN.md)
6
6
 
7
- Shared runtime and UI support library for the DeepSeek Harness plugins in this repository — covering the default auxiliary model, shared session settings, storage and host-client helpers, official UI styling, and subagent session detection. Plugins that depend on it install it automatically; plugin developers should refer to the source code and type declarations.
7
+ Shared runtime and UI support library for the DeepSeek Harness plugins in this repository — covering the default auxiliary model, shared session settings, diagnostic logging, storage and host-client helpers, official UI styling, the optional shared model picker hookup, and subagent session detection. Plugins that depend on it install it automatically; plugin developers should refer to the source code and type declarations.
8
8
 
9
9
  ## License
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [English](../README.md)
6
6
 
7
- 本仓库 DeepSeek Harness 插件共用的运行时与界面支撑库,提供默认辅助模型、共享会话设置页、存储与前后端通信辅助、官方 UI 样式及子代理会话识别等共享能力。依赖它的插件会自动安装;插件开发者请直接参考源码和类型声明。
7
+ 本仓库 DeepSeek Harness 插件共用的运行时与界面支撑库,提供默认辅助模型、共享会话设置页、诊断日志、存储与前后端通信辅助、官方 UI 样式、可选共享模型选择器接入及子代理会话识别等共享能力。依赖它的插件会自动安装;插件开发者请直接参考源码和类型声明。
8
8
 
9
9
  ## 许可证
10
10
 
@@ -1,3 +1,4 @@
1
+ import { RenderFactorySlot } from "./model-picker.js";
1
2
  import { AuxiliaryModelDefaults } from "./contracts.js";
2
3
  import { ReactNode } from "react";
3
4
  import { Context } from "@deepseek-ai/cordis";
@@ -34,10 +35,12 @@ interface AuxiliaryModelClient {
34
35
  * The shared card. All state comes from the shared host service over the
35
36
  * plugin-kit RPC channel; the card only renders the stored row.
36
37
  */
37
- declare function AuxiliaryModelCard({ client, subscribe }: {
38
+ declare function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }: {
38
39
  client: AuxiliaryModelClient;
39
40
  /** Settings-change invalidation; injected for tests without a live stream. */
40
41
  subscribe?: (change: () => void, error?: (message: string) => void) => () => void;
42
+ /** Framework-composed factory renderer; absent under direct test renders. */
43
+ renderFactorySlot?: RenderFactorySlot;
41
44
  }): ReactNode;
42
45
  /**
43
46
  * Show the shared "default auxiliary model" card while the calling plugin is
@@ -1,12 +1,13 @@
1
1
  import { AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_NS } from "./contracts.js";
2
2
  import { createModelSettingsSubscriber, modelSettingsChangesRemote } from "./model-settings-client.js";
3
3
  import { registerSessionSettingsItem } from "./client-utils.js";
4
+ import { renderModelPicker, useModelMenuAnchor } from "./model-picker.js";
4
5
  import { officialUiCss } from "./official-ui.js";
5
6
  import { modelMenuEffortLabel, modelMenuEffortOptions, parseModelMenuChoices, readModelCatalog } from "@klarkxy/dsh-model-route";
6
7
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
7
8
  import { Button, IconChevronDownOutlineRegular } from "@deepseek-ai/dsh-client-ui-primitives";
8
- import { ModelCatalogStatus, ModelMenu, modelMenuCss, useModelCatalog } from "@klarkxy/dsh-model-route/ui";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
+ import { ModelCatalogStatus, ModelMenu, modelMenuCss, useModelCatalog } from "@klarkxy/dsh-model-route/ui";
10
11
  //#region src/auxiliary-model-client.tsx
11
12
  const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
12
13
  const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
@@ -85,7 +86,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
85
86
  * The shared card. All state comes from the shared host service over the
86
87
  * plugin-kit RPC channel; the card only renders the stored row.
87
88
  */
88
- function AuxiliaryModelCard({ client, subscribe }) {
89
+ function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
89
90
  const language = useAuxiliaryLocale(client);
90
91
  const text = auxiliaryCopy[language];
91
92
  const [settings, setSettings] = useState();
@@ -208,6 +209,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
208
209
  refreshCatalog
209
210
  ]);
210
211
  const locked = busy;
212
+ const current = settings?.model ?? {
213
+ provider: "",
214
+ model: ""
215
+ };
216
+ const follow = !current.provider || !current.model;
217
+ const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
218
+ const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
219
+ const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
220
+ const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
221
+ const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
222
+ const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
211
223
  if (!settings) return /* @__PURE__ */ jsxs("section", {
212
224
  className: "ams-root dsh-ui-card",
213
225
  "data-testid": "auxiliary-model-settings",
@@ -246,16 +258,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
246
258
  })
247
259
  ]
248
260
  });
249
- const current = settings.model ?? {
250
- provider: "",
251
- model: ""
252
- };
253
- const follow = !current.provider || !current.model;
254
- const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
255
- const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
256
- const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
257
- const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
258
- const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
259
261
  return /* @__PURE__ */ jsxs("section", {
260
262
  className: "ams-root dsh-ui-card",
261
263
  "data-testid": "auxiliary-model-settings",
@@ -279,28 +281,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
279
281
  state: catalogState,
280
282
  locale: language
281
283
  }),
282
- /* @__PURE__ */ jsx(ModelMenu, {
284
+ renderModelPicker(renderFactorySlot, {
283
285
  locale: language,
284
- open: menuOpen && !locked,
286
+ disabled: locked,
287
+ busy: locked,
285
288
  onOpenChange: (next) => setMenuOpen(next && !locked),
286
289
  listClassName: "ams-root ams-menu-portal",
287
- anchor: /* @__PURE__ */ jsxs("button", {
288
- type: "button",
289
- className: "dsh-model-menu-trigger",
290
- disabled: locked,
291
- "aria-label": `${text.model}: ${currentLabel}`,
292
- children: [
293
- /* @__PURE__ */ jsx("span", {
294
- className: "dsh-model-menu-triggerLabel",
295
- children: currentLabel
296
- }),
297
- !follow && efforts.length ? /* @__PURE__ */ jsx("span", {
298
- className: "dsh-model-menu-triggerEffort",
299
- children: effortName ?? text.effortDefault
300
- }) : null,
301
- /* @__PURE__ */ jsx(IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
302
- ]
303
- }),
304
290
  choices,
305
291
  selected: follow ? void 0 : {
306
292
  provider: current.provider,
@@ -323,9 +309,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
323
309
  model: current.model,
324
310
  ...effort ? { reasoningEffort: effort } : {}
325
311
  }),
312
+ triggerLabel: currentLabel,
313
+ triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
326
314
  modelLabel: text.model,
327
315
  modelValue: currentLabel,
328
316
  defaultEffortLabel: text.effortDefault
317
+ }, {
318
+ anchor,
319
+ fallback: /* @__PURE__ */ jsx(ModelMenu, {
320
+ locale: language,
321
+ open: menuOpen && !locked,
322
+ onOpenChange: (next) => setMenuOpen(next && !locked),
323
+ listClassName: "ams-root ams-menu-portal",
324
+ anchor: /* @__PURE__ */ jsxs("button", {
325
+ type: "button",
326
+ className: "dsh-model-menu-trigger",
327
+ disabled: locked,
328
+ "aria-label": `${text.model}: ${currentLabel}`,
329
+ children: [
330
+ /* @__PURE__ */ jsx("span", {
331
+ className: "dsh-model-menu-triggerLabel",
332
+ children: currentLabel
333
+ }),
334
+ !follow && efforts.length ? /* @__PURE__ */ jsx("span", {
335
+ className: "dsh-model-menu-triggerEffort",
336
+ children: effortName ?? text.effortDefault
337
+ }) : null,
338
+ /* @__PURE__ */ jsx(IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
339
+ ]
340
+ }),
341
+ choices,
342
+ selected: follow ? void 0 : {
343
+ provider: current.provider,
344
+ model: current.model
345
+ },
346
+ onPick: (picked) => save({
347
+ provider: picked.provider,
348
+ model: picked.model
349
+ }),
350
+ leading: [{
351
+ id: "default",
352
+ label: text.modelDefault,
353
+ selected: follow
354
+ }],
355
+ onPickLeading: () => save(null),
356
+ efforts,
357
+ selectedEffort: current.reasoningEffort || "",
358
+ onPickEffort: (effort) => save(follow ? null : {
359
+ provider: current.provider,
360
+ model: current.model,
361
+ ...effort ? { reasoningEffort: effort } : {}
362
+ }),
363
+ modelLabel: text.model,
364
+ modelValue: currentLabel,
365
+ defaultEffortLabel: text.effortDefault
366
+ })
329
367
  }),
330
368
  /* @__PURE__ */ jsx("span", {
331
369
  className: "dsh-ui-hint",
@@ -427,9 +465,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
427
465
  let unprovide;
428
466
  const watchLease = acquireWatchCoordinator(root).mount();
429
467
  const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
430
- const render = () => /* @__PURE__ */ jsx(AuxiliaryModelCard, {
468
+ const render = (props) => /* @__PURE__ */ jsx(AuxiliaryModelCard, {
431
469
  client: root,
432
- subscribe: subscriber.subscribe
470
+ subscribe: subscriber.subscribe,
471
+ renderFactorySlot: props.renderFactorySlot
433
472
  });
434
473
  const service = {
435
474
  retain() {
@@ -1 +1 @@
1
- {"version":3,"file":"auxiliary-model-client.js","names":[],"sources":["../src/auxiliary-model-client.tsx"],"sourcesContent":["/**\n * Shared \"default auxiliary model\" settings card.\n *\n * Any number of feature plugins call `registerAuxiliaryModelSettings(ctx)`\n * from their client half; a single card appears as the `auxiliary-model` row\n * of the shared Session Management settings column for as long as at least\n * one participant lives. The card is root-owned shared infrastructure, not a\n * business plugin: every bundled copy of this helper rendezvouses on the same\n * Cordis root service, so no participant depends on another being enabled, the\n * card is mounted once (duplicate row ids would throw), and the last\n * `release()` removes it. The card also retains the shared page itself, so a\n * lone auxiliary plugin still yields the column — participants never depend\n * on another business plugin providing their settings page.\n *\n * The storage side lives in the shared host service behind the plugin-kit RPC\n * channel: `defaults.get` returns `{revision, model}` and `defaults.set`\n * carries `{model, expectedRevision}` with an empty provider/model clearing\n * the override. Cross-window invalidation rides the existing model-settings\n * change stream — no polling is added anywhere in this file.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { Button, IconChevronDownOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport {\n readModelCatalog, modelMenuEffortLabel, modelMenuEffortOptions, parseModelMenuChoices, type ModelRoute,\n} from '@klarkxy/dsh-model-route'\nimport { ModelCatalogStatus, ModelMenu, modelMenuCss, useModelCatalog } from '@klarkxy/dsh-model-route/ui'\nimport { officialUiCss } from './official-ui.ts'\nimport {\n createModelSettingsSubscriber, modelSettingsChangesRemote, type ModelSettingsSubscriber,\n} from './model-settings-client.ts'\nimport { registerSessionSettingsItem } from './client-utils.ts'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n type AuxiliaryModelDefaults, type RpcResult,\n} from './contracts.ts'\n\nconst AUXILIARY_MODEL_PACKAGE = '@klarkxy/dsh-plugin-kit'\n\n/** The stored row; re-exported so card consumers share the contract shape. */\nexport type AuxiliaryModelSnapshot = AuxiliaryModelDefaults\n\n/* The card is one row of the shared Session Management column: the item slot\n * is a root-scope list declared by the shared page, and row ids must be\n * unique. The row sorts ahead of every business participant. */\nexport const AUXILIARY_MODEL_SEAT_ID = 'auxiliary-model'\nexport const AUXILIARY_MODEL_SEAT_ORDER = 55\n\n/** Private Cordis rendezvous; every bundled copy resolves the same root service. */\nconst AUXILIARY_MODEL_CARD_SERVICE = 'dshAuxiliaryModelSettingsCard'\n\n/** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */\nexport const auxiliaryModelCss = `${officialUiCss('ams-root')}${modelMenuCss}\n.ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }\n`\n\nconst auxiliaryCopy = {\n zh: {\n title: '默认辅助模型',\n help: '各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。',\n model: '辅助模型',\n modelDefault: '跟随会话或宿主默认',\n modelHint: '不选择模型时,各功能按自己的规则决定对话模型。',\n effortDefault: '跟随模型默认',\n loading: '正在读取设置…',\n failed: '无法读取模型设置。',\n stale: '设置已被其他窗口更新,请刷新后重试。',\n saveFailed: '保存失败,原设置未更改。请重试。',\n saved: '已保存。',\n retry: '重试',\n refresh: '刷新设置',\n syncUnavailable: '自动刷新不可用,请手动刷新设置。',\n },\n en: {\n title: 'Default Auxiliary Model',\n help: 'The model used by auxiliary features (such as title generation or summarizing) that do not name their own model; only features that opted in are affected.',\n model: 'Auxiliary model',\n modelDefault: 'Follow session or host default',\n modelHint: 'With no model selected, each feature follows its own rules for the conversation model.',\n effortDefault: 'Follow model default',\n loading: 'Loading settings…',\n failed: 'Could not load model settings.',\n stale: 'Settings were changed elsewhere; refresh and retry.',\n saveFailed: 'Save failed; the previous setting was not changed. Try again.',\n saved: 'Saved.',\n retry: 'Retry',\n refresh: 'Refresh settings',\n syncUnavailable: 'Automatic refresh unavailable; refresh settings manually.',\n },\n} as const\ntype AuxiliaryCopy = (typeof auxiliaryCopy)['zh'] | (typeof auxiliaryCopy)['en']\n\n/** A failed RPC keeps its code so the card can translate known rejections. */\nclass AuxiliaryRpcError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'AuxiliaryRpcError' }\n}\n\n/** The client services this card reads; the shared service passes ctx.root. */\nexport interface AuxiliaryModelClient {\n connection: {\n rpc: { call(channel: string, endpoint: string, payload: unknown): Promise<unknown> }\n generation?: { subscribe(listener: () => void): () => void }\n }\n remote?: { session?: { modelCatalog?: () => Promise<unknown> } }\n locale?: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n}\n\n/** Host UI language, following live switches with the host's English fallback. */\nfunction useAuxiliaryLocale(client: AuxiliaryModelClient): 'zh' | 'en' {\n const subscribe = useCallback((listener: () => void) => client.locale?.subscribe(listener) ?? (() => {}), [client])\n const snapshot = useCallback(() => client.locale?.getSnapshot().active ?? 'en', [client])\n return String(useSyncExternalStore(subscribe, snapshot, snapshot)).startsWith('zh') ? 'zh' : 'en'\n}\n\nfunction isAbort(error: unknown): boolean {\n return error instanceof Error && (error.name === 'AbortError' || /abort|cancel/i.test(error.message))\n}\n\n/* A revision mismatch is a lost race, not a broken save: the stored row moved\n * past the one the user edited. The host reports it as auxiliary-model/conflict;\n * the message fallback covers older hosts that surfaced it as internal. */\nfunction isStaleRejection(error: unknown): boolean {\n if (!(error instanceof AuxiliaryRpcError)) return false\n return error.code === 'stale' || error.code === 'auxiliary-model/conflict'\n || /changed|revision|reload|stale|conflict/i.test(error.message)\n}\n\nasync function callAuxiliaryRpc<T>(\n client: AuxiliaryModelClient, endpoint: string, payload: unknown = {},\n): Promise<T> {\n const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload) as RpcResult<T> | undefined\n if (!result || result.ok !== true) {\n const error = result && 'error' in result ? result.error : undefined\n throw new AuxiliaryRpcError(error?.code ?? 'unknown', error?.message ?? `RPC ${endpoint} failed`)\n }\n return result.value\n}\n\n/**\n * The shared card. All state comes from the shared host service over the\n * plugin-kit RPC channel; the card only renders the stored row.\n */\nexport function AuxiliaryModelCard({ client, subscribe }: {\n client: AuxiliaryModelClient\n /** Settings-change invalidation; injected for tests without a live stream. */\n subscribe?: (change: () => void, error?: (message: string) => void) => () => void\n}): ReactNode {\n const language = useAuxiliaryLocale(client)\n const text: AuxiliaryCopy = auxiliaryCopy[language]\n const [settings, setSettings] = useState<AuxiliaryModelSnapshot>()\n const loadCatalog = useCallback(() => readModelCatalog(client), [client])\n const subscribeCatalog = useCallback(\n (change: () => void) => client.connection.generation?.subscribe(change), [client])\n const catalogState = useModelCatalog(loadCatalog, { subscribe: subscribeCatalog })\n const { refresh: refreshCatalog } = catalogState\n const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? undefined)\n const [menuOpen, setMenuOpen] = useState(false)\n const [error, setError] = useState<'' | 'failed' | 'stale' | 'saveFailed'>('')\n const [note, setNote] = useState(false)\n const [busy, setBusy] = useState(false)\n const [syncError, setSyncError] = useState('')\n const operation = useRef({ epoch: 0, active: false, reading: false, writing: false, pending: false })\n const refreshRef = useRef<() => void>(() => {})\n\n const refresh = () => {\n const state = operation.current\n if (!state.active) return\n if (state.reading || state.writing) { state.pending = true; return }\n state.pending = false; state.reading = true; setBusy(true)\n const epoch = state.epoch\n void callAuxiliaryRpc<AuxiliaryModelSnapshot>(client, 'defaults.get').then(next => {\n if (state.active && state.epoch === epoch && !state.pending) {\n setSettings(next); setError('')\n }\n }).catch(reason => {\n if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError('failed')\n }).finally(() => {\n if (!state.active || state.epoch !== epoch) return\n state.reading = false; setBusy(false)\n if (state.pending) refreshRef.current()\n })\n }\n refreshRef.current = refresh\n useEffect(() => {\n const state = operation.current\n state.epoch++; state.active = true; state.reading = false; state.writing = false; state.pending = false\n setBusy(false); setSettings(undefined); setError(''); setNote(false)\n refreshRef.current()\n return () => { state.active = false; state.epoch++ }\n }, [client])\n // Cross-window invalidation rides the shared model-settings change stream.\n useEffect(() => {\n setSyncError('')\n let live = true\n let stop: (() => void) | undefined\n try {\n stop = subscribe?.(() => {\n if (live) { setSyncError(''); refreshCatalog(); refreshRef.current() }\n }, message => { if (live) setSyncError(message) })\n } catch {\n // A subscriber that cannot even attach degrades to manual refresh.\n if (live) setSyncError('Model settings sync service unavailable')\n }\n return () => { live = false; try { stop?.() } catch { /* detach is best-effort */ } }\n }, [subscribe, refreshCatalog])\n\n const save = useCallback((model: ModelRoute | null) => {\n const state = operation.current\n if (!settings || !state.active || state.writing || state.reading) return\n state.writing = true\n const epoch = state.epoch\n setBusy(true); setNote(false); setError('')\n void callAuxiliaryRpc<AuxiliaryModelSnapshot>(client, 'defaults.set', {\n model: model ?? { provider: '', model: '' },\n expectedRevision: settings.revision,\n }).then(next => {\n if (state.active && state.epoch === epoch) { setSettings(next); setNote(true); refreshCatalog() }\n }).catch(reason => {\n if (state.active && state.epoch === epoch) {\n setError(isStaleRejection(reason) ? 'stale' : 'saveFailed')\n }\n }).finally(() => {\n if (!state.active || state.epoch !== epoch) return\n state.writing = false; setBusy(false)\n if (state.pending) refreshRef.current()\n })\n }, [client, settings, refreshCatalog])\n\n const locked = busy\n if (!settings) {\n return <section className=\"ams-root dsh-ui-card\" data-testid=\"auxiliary-model-settings\">\n <span className=\"dsh-ui-heading\">{text.title}</span>\n <p className=\"dsh-ui-help\">{text.help}</p>\n {error\n ? <p role=\"alert\" className=\"dsh-ui-error\">{text[error]}</p>\n : <p role=\"status\" className=\"dsh-ui-hint\">{text.loading}</p>}\n {syncError && <p role=\"status\" className=\"dsh-ui-hint\">{text.syncUnavailable}</p>}\n <div className=\"dsh-ui-actions\">\n <Button variant=\"ghost\" size=\"sm\" disabled={locked} onClick={() => refreshRef.current()}>{text.retry}</Button>\n </div>\n </section>\n }\n\n const current = settings.model ?? { provider: '', model: '' }\n const follow = !current.provider || !current.model\n const selected = follow ? undefined : choices.find(item => item.provider === current.provider && item.model === current.model)\n const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort)\n const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`\n const selectedEffort = efforts.find(item => item.id === (current.reasoningEffort || ''))\n const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : undefined\n\n return <section className=\"ams-root dsh-ui-card\" data-testid=\"auxiliary-model-settings\">\n <span className=\"dsh-ui-heading\">{text.title}</span>\n <p className=\"dsh-ui-help\">{text.help}</p>\n <div className=\"dsh-ui-field\">\n <span className=\"dsh-ui-label\">{text.model}</span>\n <ModelCatalogStatus state={catalogState} locale={language} />\n <ModelMenu\n locale={language}\n open={menuOpen && !locked}\n onOpenChange={next => setMenuOpen(next && !locked)}\n listClassName=\"ams-root ams-menu-portal\"\n anchor={<button type=\"button\" className=\"dsh-model-menu-trigger\" disabled={locked}\n aria-label={`${text.model}: ${currentLabel}`}>\n <span className=\"dsh-model-menu-triggerLabel\">{currentLabel}</span>\n {!follow && efforts.length ? <span className=\"dsh-model-menu-triggerEffort\">{effortName ?? text.effortDefault}</span> : null}\n <IconChevronDownOutlineRegular className={`dsh-model-menu-chevron${menuOpen && !locked ? ' dsh-model-menu-chevronOpen' : ''}`} />\n </button>}\n choices={choices}\n selected={follow ? undefined : { provider: current.provider, model: current.model }}\n onPick={picked => save({ provider: picked.provider, model: picked.model })}\n leading={[{ id: 'default', label: text.modelDefault, selected: follow }]}\n onPickLeading={() => save(null)}\n efforts={efforts}\n selectedEffort={current.reasoningEffort || ''}\n onPickEffort={effort => save(follow\n ? null\n : { provider: current.provider, model: current.model, ...(effort ? { reasoningEffort: effort } : {}) })}\n modelLabel={text.model}\n modelValue={currentLabel}\n defaultEffortLabel={text.effortDefault}\n />\n <span className=\"dsh-ui-hint\">{text.modelHint}</span>\n </div>\n <div className=\"ams-row\">\n {syncError && <p role=\"status\" className=\"dsh-ui-hint\">{text.syncUnavailable}</p>}\n {error && <p role=\"alert\" className=\"dsh-ui-error\">{text[error]}</p>}\n {note && <p role=\"status\" className=\"dsh-ui-notice\">{text.saved}</p>}\n {error || syncError ? (\n <Button variant=\"ghost\" size=\"sm\" disabled={locked} onClick={() => refreshRef.current()}>{text.refresh}</Button>\n ) : null}\n </div>\n </section>\n}\n\ninterface AuxiliaryRoot {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: string[], callback: (scope: unknown) => void): AuxiliaryFiber\n}\ninterface AuxiliaryFiber {\n dispose(): void | Promise<void>\n}\ninterface AuxiliaryRemoteMountScope {\n remote: { $mount(contribution: unknown): Promise<unknown> }\n effect(cb: () => () => void | Promise<void>): unknown\n}\n\n/* The client-side stream descriptor mount, serialized through one root\n * coordinator. Its tail carries mounts and unmounts in strict order: a new\n * mount cannot start while a previous fiber's unmount is in flight, and a\n * lease that closes before its continuation runs never mounts at all. The\n * coordinator leaves the store only when the queue is drained and no lease\n * is active — a fast teardown→re-acquire chains onto the same tail instead\n * of hitting \"already registered\" or overlapping a second mount. */\ninterface AuxiliaryWatchLease {\n fiber: AuxiliaryFiber | undefined\n closed: boolean\n done(): void\n}\ninterface AuxiliaryWatchCoordinator {\n tail: Promise<void>\n active: number\n mount(): AuxiliaryWatchLease\n}\nconst AUXILIARY_MODEL_WATCH_COORDINATOR = 'dshAuxiliaryModelWatchCoordinator'\n\nfunction acquireWatchCoordinator(root: AuxiliaryRoot): AuxiliaryWatchCoordinator {\n const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) as AuxiliaryWatchCoordinator | undefined\n if (existing) return existing\n let unprovide: (() => void) | undefined\n const coordinator: AuxiliaryWatchCoordinator = {\n tail: Promise.resolve(),\n active: 0,\n mount() {\n coordinator.active += 1\n const previousTail = coordinator.tail\n const lease: AuxiliaryWatchLease = {\n closed: false,\n done() {\n if (lease.closed) return\n lease.closed = true\n coordinator.active -= 1\n const owned = lease.fiber\n lease.fiber = undefined\n // The unmount joins the tail every later mount waits on.\n coordinator.tail = coordinator.tail\n .then(() => Promise.resolve(owned?.dispose()).catch(() => {}))\n // Unprovide only once the queue is really drained: a newer lease\n // already queued on this coordinator must keep it alive.\n void coordinator.tail.then(() => {\n if (coordinator.active > 0) return\n try {\n if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) {\n void Promise.resolve(unprovide?.()).catch(() => {})\n }\n } catch { /* Root already unloading. */ }\n })\n },\n fiber: undefined,\n }\n void previousTail.then(() => {\n // A release that beat the mount ends here: nothing mounted, nothing\n // to unmount, and the tail already moved on.\n if (lease.closed) return\n // mountModelSettingsChanges performs this exact inject/$mount pair;\n // it is repeated here only because that helper returns void and the\n // fiber must be captured for symmetric teardown.\n try {\n lease.fiber = root.inject(['remote'], injected => {\n const scope = injected as AuxiliaryRemoteMountScope\n const mounting = scope.remote.$mount(\n modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY),\n ) as Promise<(() => Promise<void>) | undefined>\n void mounting.catch(() => {})\n scope.effect(() => async () => {\n const unmount = await mounting.catch(() => undefined)\n await unmount?.()\n })\n })\n } catch { /* remote unavailable: the card degrades to manual refresh */ }\n })\n return lease\n },\n }\n unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator)\n return coordinator\n}\n\ninterface AuxiliaryCardService {\n retain(): void\n release(): void\n}\n\n/**\n * Acquire the root-owned card service, creating it on first use. One counter\n * covers participants, the seat wait and the change-stream mount: the last\n * release removes the wait, then the watch contribution, then the provider\n * itself synchronously (Cordis deletes the impl before its async disposer\n * awaits), so a later acquire sees a clean get() miss and can re-establish.\n */\nfunction acquireAuxiliaryModelCard(root: AuxiliaryRoot, ctx: Context): AuxiliaryCardService {\n const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false) as AuxiliaryCardService | undefined\n if (existing) return existing\n let users = 0\n let styleElement: HTMLStyleElement | undefined\n let stopItem: (() => void) | undefined\n let unprovide: (() => void) | undefined\n // The client-side stream descriptor hangs off the shared root context —\n // never a plugin fiber — so the first participant's unload cannot take the\n // card's change stream down. The coordinator still unmounts it, in tail\n // order, on the last release.\n const watchLease = acquireWatchCoordinator(root).mount()\n const subscriber: ModelSettingsSubscriber = createModelSettingsSubscriber(\n ctx.root as unknown as Parameters<typeof createModelSettingsSubscriber>[0],\n AUXILIARY_MODEL_PACKAGE,\n AUXILIARY_MODEL_CHANGES_IDENTITY,\n ns => ns === AUXILIARY_MODEL_NS,\n )\n const render = () => <AuxiliaryModelCard client={root as unknown as AuxiliaryModelClient}\n subscribe={subscriber.subscribe} />\n const service: AuxiliaryCardService = {\n retain() {\n // The reservation is counted before the throwing installs, so a failure\n // lets the caller's release() undo it fully instead of leaking the service.\n users += 1\n if (styleElement === undefined && typeof document !== 'undefined') {\n styleElement = document.createElement('style')\n styleElement.setAttribute('data-plugin', 'dsh-auxiliary-model-shared')\n styleElement.textContent = auxiliaryModelCss\n document.head.appendChild(styleElement)\n }\n /* The card is a participant of the shared column too: the root-owned\n * call contributes its row AND retains the page and nav icon, which is\n * what guarantees a lone auxiliary plugin still creates the Session\n * Management section — no business plugin has to provide it. The waits\n * sit on the root context, so they live exactly as long as the card. */\n if (!stopItem) {\n stopItem = registerSessionSettingsItem(ctx.root, {\n id: AUXILIARY_MODEL_SEAT_ID,\n order: AUXILIARY_MODEL_SEAT_ORDER,\n }, render)\n }\n },\n release() {\n if (users <= 0) return\n users -= 1\n if (users > 0) return\n stopItem?.()\n stopItem = undefined\n styleElement?.remove()\n styleElement = undefined\n watchLease.done()\n // The store entry leaves synchronously, like the shared page service:\n // the next acquire must not see a stale \"already registered\" marker.\n try { void Promise.resolve(unprovide?.()).catch(() => {}) } catch { /* Root already unloading. */ }\n unprovide = undefined\n },\n }\n unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service)\n return service\n}\n\n/**\n * Show the shared \"default auxiliary model\" card while the calling plugin is\n * enabled.\n *\n * `ctx` is the plugin's own client context: the card itself is retained on the\n * shared root context for as long as any participant lives, so an arbitrary\n * subset of enabled plugins yields the same single card. The mount's slot wait\n * and the change-stream contribution are owned by the root service — the first\n * participant's unload cannot take the card down. Returns a disposer; it may\n * be invoked by both ctx.effect and an explicit caller and releases at most\n * once, so a double call cannot unbalance the shared refcount.\n */\nexport function registerAuxiliaryModelSettings(ctx: Context): () => void {\n const root = ctx.root as unknown as AuxiliaryRoot\n const service = acquireAuxiliaryModelCard(root, ctx)\n try {\n service.retain()\n } catch (error) {\n // A synchronous failure (for example a duplicate row id) must not pin the\n // shared card for a participant that never joined.\n service.release()\n throw error\n }\n let disposed = false\n return () => {\n if (disposed) return\n disposed = true\n service.release()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"auxiliary-model-client.js","names":[],"sources":["../src/auxiliary-model-client.tsx"],"sourcesContent":["/**\n * Shared \"default auxiliary model\" settings card.\n *\n * Any number of feature plugins call `registerAuxiliaryModelSettings(ctx)`\n * from their client half; a single card appears as the `auxiliary-model` row\n * of the shared Session Management settings column for as long as at least\n * one participant lives. The card is root-owned shared infrastructure, not a\n * business plugin: every bundled copy of this helper rendezvouses on the same\n * Cordis root service, so no participant depends on another being enabled, the\n * card is mounted once (duplicate row ids would throw), and the last\n * `release()` removes it. The card also retains the shared page itself, so a\n * lone auxiliary plugin still yields the column — participants never depend\n * on another business plugin providing their settings page.\n *\n * The storage side lives in the shared host service behind the plugin-kit RPC\n * channel: `defaults.get` returns `{revision, model}` and `defaults.set`\n * carries `{model, expectedRevision}` with an empty provider/model clearing\n * the override. Cross-window invalidation rides the existing model-settings\n * change stream — no polling is added anywhere in this file.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { Button, IconChevronDownOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives'\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\nimport {\n readModelCatalog, modelMenuEffortLabel, modelMenuEffortOptions, parseModelMenuChoices, type ModelRoute,\n} from '@klarkxy/dsh-model-route'\nimport { ModelCatalogStatus, ModelMenu, modelMenuCss, useModelCatalog } from '@klarkxy/dsh-model-route/ui'\nimport { officialUiCss } from './official-ui.ts'\nimport { renderModelPicker, useModelMenuAnchor, type RenderFactorySlot } from './model-picker.tsx'\nimport {\n createModelSettingsSubscriber, modelSettingsChangesRemote, type ModelSettingsSubscriber,\n} from './model-settings-client.ts'\nimport { registerSessionSettingsItem } from './client-utils.ts'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n type AuxiliaryModelDefaults, type RpcResult,\n} from './contracts.ts'\n\nconst AUXILIARY_MODEL_PACKAGE = '@klarkxy/dsh-plugin-kit'\n\n/** The stored row; re-exported so card consumers share the contract shape. */\nexport type AuxiliaryModelSnapshot = AuxiliaryModelDefaults\n\n/* The card is one row of the shared Session Management column: the item slot\n * is a root-scope list declared by the shared page, and row ids must be\n * unique. The row sorts ahead of every business participant. */\nexport const AUXILIARY_MODEL_SEAT_ID = 'auxiliary-model'\nexport const AUXILIARY_MODEL_SEAT_ORDER = 55\n\n/** Private Cordis rendezvous; every bundled copy resolves the same root service. */\nconst AUXILIARY_MODEL_CARD_SERVICE = 'dshAuxiliaryModelSettingsCard'\n\n/** Theme-token-only stylesheet: officialUiCss scope plus the shared ModelMenu recipe. */\nexport const auxiliaryModelCss = `${officialUiCss('ams-root')}${modelMenuCss}\n.ams-root .ams-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }\n`\n\nconst auxiliaryCopy = {\n zh: {\n title: '默认辅助模型',\n help: '各辅助功能(如标题生成、摘要)没有单独指定模型时使用的模型;仅影响声明使用它的功能。',\n model: '辅助模型',\n modelDefault: '跟随会话或宿主默认',\n modelHint: '不选择模型时,各功能按自己的规则决定对话模型。',\n effortDefault: '跟随模型默认',\n loading: '正在读取设置…',\n failed: '无法读取模型设置。',\n stale: '设置已被其他窗口更新,请刷新后重试。',\n saveFailed: '保存失败,原设置未更改。请重试。',\n saved: '已保存。',\n retry: '重试',\n refresh: '刷新设置',\n syncUnavailable: '自动刷新不可用,请手动刷新设置。',\n },\n en: {\n title: 'Default Auxiliary Model',\n help: 'The model used by auxiliary features (such as title generation or summarizing) that do not name their own model; only features that opted in are affected.',\n model: 'Auxiliary model',\n modelDefault: 'Follow session or host default',\n modelHint: 'With no model selected, each feature follows its own rules for the conversation model.',\n effortDefault: 'Follow model default',\n loading: 'Loading settings…',\n failed: 'Could not load model settings.',\n stale: 'Settings were changed elsewhere; refresh and retry.',\n saveFailed: 'Save failed; the previous setting was not changed. Try again.',\n saved: 'Saved.',\n retry: 'Retry',\n refresh: 'Refresh settings',\n syncUnavailable: 'Automatic refresh unavailable; refresh settings manually.',\n },\n} as const\ntype AuxiliaryCopy = (typeof auxiliaryCopy)['zh'] | (typeof auxiliaryCopy)['en']\n\n/** A failed RPC keeps its code so the card can translate known rejections. */\nclass AuxiliaryRpcError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'AuxiliaryRpcError' }\n}\n\n/** The client services this card reads; the shared service passes ctx.root. */\nexport interface AuxiliaryModelClient {\n connection: {\n rpc: { call(channel: string, endpoint: string, payload: unknown): Promise<unknown> }\n generation?: { subscribe(listener: () => void): () => void }\n }\n remote?: { session?: { modelCatalog?: () => Promise<unknown> } }\n locale?: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n}\n\n/** Host UI language, following live switches with the host's English fallback. */\nfunction useAuxiliaryLocale(client: AuxiliaryModelClient): 'zh' | 'en' {\n const subscribe = useCallback((listener: () => void) => client.locale?.subscribe(listener) ?? (() => {}), [client])\n const snapshot = useCallback(() => client.locale?.getSnapshot().active ?? 'en', [client])\n return String(useSyncExternalStore(subscribe, snapshot, snapshot)).startsWith('zh') ? 'zh' : 'en'\n}\n\nfunction isAbort(error: unknown): boolean {\n return error instanceof Error && (error.name === 'AbortError' || /abort|cancel/i.test(error.message))\n}\n\n/* A revision mismatch is a lost race, not a broken save: the stored row moved\n * past the one the user edited. The host reports it as auxiliary-model/conflict;\n * the message fallback covers older hosts that surfaced it as internal. */\nfunction isStaleRejection(error: unknown): boolean {\n if (!(error instanceof AuxiliaryRpcError)) return false\n return error.code === 'stale' || error.code === 'auxiliary-model/conflict'\n || /changed|revision|reload|stale|conflict/i.test(error.message)\n}\n\nasync function callAuxiliaryRpc<T>(\n client: AuxiliaryModelClient, endpoint: string, payload: unknown = {},\n): Promise<T> {\n const result = await client.connection.rpc.call(AUXILIARY_MODEL_CHANNEL, endpoint, payload) as RpcResult<T> | undefined\n if (!result || result.ok !== true) {\n const error = result && 'error' in result ? result.error : undefined\n throw new AuxiliaryRpcError(error?.code ?? 'unknown', error?.message ?? `RPC ${endpoint} failed`)\n }\n return result.value\n}\n\n/**\n * The shared card. All state comes from the shared host service over the\n * plugin-kit RPC channel; the card only renders the stored row.\n */\nexport function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }: {\n client: AuxiliaryModelClient\n /** Settings-change invalidation; injected for tests without a live stream. */\n subscribe?: (change: () => void, error?: (message: string) => void) => () => void\n /** Framework-composed factory renderer; absent under direct test renders. */\n renderFactorySlot?: RenderFactorySlot\n}): ReactNode {\n const language = useAuxiliaryLocale(client)\n const text: AuxiliaryCopy = auxiliaryCopy[language]\n const [settings, setSettings] = useState<AuxiliaryModelSnapshot>()\n const loadCatalog = useCallback(() => readModelCatalog(client), [client])\n const subscribeCatalog = useCallback(\n (change: () => void) => client.connection.generation?.subscribe(change), [client])\n const catalogState = useModelCatalog(loadCatalog, { subscribe: subscribeCatalog })\n const { refresh: refreshCatalog } = catalogState\n const choices = parseModelMenuChoices(catalogState.catalog, settings?.model ?? undefined)\n const [menuOpen, setMenuOpen] = useState(false)\n const [error, setError] = useState<'' | 'failed' | 'stale' | 'saveFailed'>('')\n const [note, setNote] = useState(false)\n const [busy, setBusy] = useState(false)\n const [syncError, setSyncError] = useState('')\n const operation = useRef({ epoch: 0, active: false, reading: false, writing: false, pending: false })\n const refreshRef = useRef<() => void>(() => {})\n\n const refresh = () => {\n const state = operation.current\n if (!state.active) return\n if (state.reading || state.writing) { state.pending = true; return }\n state.pending = false; state.reading = true; setBusy(true)\n const epoch = state.epoch\n void callAuxiliaryRpc<AuxiliaryModelSnapshot>(client, 'defaults.get').then(next => {\n if (state.active && state.epoch === epoch && !state.pending) {\n setSettings(next); setError('')\n }\n }).catch(reason => {\n if (state.active && state.epoch === epoch && !state.pending && !isAbort(reason)) setError('failed')\n }).finally(() => {\n if (!state.active || state.epoch !== epoch) return\n state.reading = false; setBusy(false)\n if (state.pending) refreshRef.current()\n })\n }\n refreshRef.current = refresh\n useEffect(() => {\n const state = operation.current\n state.epoch++; state.active = true; state.reading = false; state.writing = false; state.pending = false\n setBusy(false); setSettings(undefined); setError(''); setNote(false)\n refreshRef.current()\n return () => { state.active = false; state.epoch++ }\n }, [client])\n // Cross-window invalidation rides the shared model-settings change stream.\n useEffect(() => {\n setSyncError('')\n let live = true\n let stop: (() => void) | undefined\n try {\n stop = subscribe?.(() => {\n if (live) { setSyncError(''); refreshCatalog(); refreshRef.current() }\n }, message => { if (live) setSyncError(message) })\n } catch {\n // A subscriber that cannot even attach degrades to manual refresh.\n if (live) setSyncError('Model settings sync service unavailable')\n }\n return () => { live = false; try { stop?.() } catch { /* detach is best-effort */ } }\n }, [subscribe, refreshCatalog])\n\n const save = useCallback((model: ModelRoute | null) => {\n const state = operation.current\n if (!settings || !state.active || state.writing || state.reading) return\n state.writing = true\n const epoch = state.epoch\n setBusy(true); setNote(false); setError('')\n void callAuxiliaryRpc<AuxiliaryModelSnapshot>(client, 'defaults.set', {\n model: model ?? { provider: '', model: '' },\n expectedRevision: settings.revision,\n }).then(next => {\n if (state.active && state.epoch === epoch) { setSettings(next); setNote(true); refreshCatalog() }\n }).catch(reason => {\n if (state.active && state.epoch === epoch) {\n setError(isStaleRejection(reason) ? 'stale' : 'saveFailed')\n }\n }).finally(() => {\n if (!state.active || state.epoch !== epoch) return\n state.writing = false; setBusy(false)\n if (state.pending) refreshRef.current()\n })\n }, [client, settings, refreshCatalog])\n\n const locked = busy\n const current = settings?.model ?? { provider: '', model: '' }\n const follow = !current.provider || !current.model\n const selected = follow ? undefined : choices.find(item => item.provider === current.provider && item.model === current.model)\n const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort)\n const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`\n const selectedEffort = efforts.find(item => item.id === (current.reasoningEffort || ''))\n const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : undefined\n // The shared picker's anchor mirrors the trigger the fallback menu renders.\n const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ''}`, undefined,\n () => setMenuOpen(false))\n if (!settings) {\n return <section className=\"ams-root dsh-ui-card\" data-testid=\"auxiliary-model-settings\">\n <span className=\"dsh-ui-heading\">{text.title}</span>\n <p className=\"dsh-ui-help\">{text.help}</p>\n {error\n ? <p role=\"alert\" className=\"dsh-ui-error\">{text[error]}</p>\n : <p role=\"status\" className=\"dsh-ui-hint\">{text.loading}</p>}\n {syncError && <p role=\"status\" className=\"dsh-ui-hint\">{text.syncUnavailable}</p>}\n <div className=\"dsh-ui-actions\">\n <Button variant=\"ghost\" size=\"sm\" disabled={locked} onClick={() => refreshRef.current()}>{text.retry}</Button>\n </div>\n </section>\n }\n\n\n return <section className=\"ams-root dsh-ui-card\" data-testid=\"auxiliary-model-settings\">\n <span className=\"dsh-ui-heading\">{text.title}</span>\n <p className=\"dsh-ui-help\">{text.help}</p>\n <div className=\"dsh-ui-field\">\n <span className=\"dsh-ui-label\">{text.model}</span>\n <ModelCatalogStatus state={catalogState} locale={language} />\n {renderModelPicker(renderFactorySlot, {\n locale: language,\n disabled: locked,\n busy: locked,\n onOpenChange: next => setMenuOpen(next && !locked),\n listClassName: 'ams-root ams-menu-portal',\n choices,\n selected: follow ? undefined : { provider: current.provider, model: current.model },\n onPick: picked => save({ provider: picked.provider, model: picked.model }),\n leading: [{ id: 'default', label: text.modelDefault, selected: follow }],\n onPickLeading: () => save(null),\n efforts,\n selectedEffort: current.reasoningEffort || '',\n onPickEffort: effort => save(follow\n ? null\n : { provider: current.provider, model: current.model, ...(effort ? { reasoningEffort: effort } : {}) }),\n triggerLabel: currentLabel,\n triggerEffortLabel: !follow && efforts.length ? (effortName ?? text.effortDefault) : undefined,\n modelLabel: text.model,\n modelValue: currentLabel,\n defaultEffortLabel: text.effortDefault,\n }, {\n anchor,\n fallback: <ModelMenu\n locale={language}\n open={menuOpen && !locked}\n onOpenChange={next => setMenuOpen(next && !locked)}\n listClassName=\"ams-root ams-menu-portal\"\n anchor={<button type=\"button\" className=\"dsh-model-menu-trigger\" disabled={locked}\n aria-label={`${text.model}: ${currentLabel}`}>\n <span className=\"dsh-model-menu-triggerLabel\">{currentLabel}</span>\n {!follow && efforts.length ? <span className=\"dsh-model-menu-triggerEffort\">{effortName ?? text.effortDefault}</span> : null}\n <IconChevronDownOutlineRegular className={`dsh-model-menu-chevron${menuOpen && !locked ? ' dsh-model-menu-chevronOpen' : ''}`} />\n </button>}\n choices={choices}\n selected={follow ? undefined : { provider: current.provider, model: current.model }}\n onPick={picked => save({ provider: picked.provider, model: picked.model })}\n leading={[{ id: 'default', label: text.modelDefault, selected: follow }]}\n onPickLeading={() => save(null)}\n efforts={efforts}\n selectedEffort={current.reasoningEffort || ''}\n onPickEffort={effort => save(follow\n ? null\n : { provider: current.provider, model: current.model, ...(effort ? { reasoningEffort: effort } : {}) })}\n modelLabel={text.model}\n modelValue={currentLabel}\n defaultEffortLabel={text.effortDefault}\n />,\n })}\n <span className=\"dsh-ui-hint\">{text.modelHint}</span>\n </div>\n <div className=\"ams-row\">\n {syncError && <p role=\"status\" className=\"dsh-ui-hint\">{text.syncUnavailable}</p>}\n {error && <p role=\"alert\" className=\"dsh-ui-error\">{text[error]}</p>}\n {note && <p role=\"status\" className=\"dsh-ui-notice\">{text.saved}</p>}\n {error || syncError ? (\n <Button variant=\"ghost\" size=\"sm\" disabled={locked} onClick={() => refreshRef.current()}>{text.refresh}</Button>\n ) : null}\n </div>\n </section>\n}\n\ninterface AuxiliaryRoot {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: string[], callback: (scope: unknown) => void): AuxiliaryFiber\n}\ninterface AuxiliaryFiber {\n dispose(): void | Promise<void>\n}\ninterface AuxiliaryRemoteMountScope {\n remote: { $mount(contribution: unknown): Promise<unknown> }\n effect(cb: () => () => void | Promise<void>): unknown\n}\n\n/* The client-side stream descriptor mount, serialized through one root\n * coordinator. Its tail carries mounts and unmounts in strict order: a new\n * mount cannot start while a previous fiber's unmount is in flight, and a\n * lease that closes before its continuation runs never mounts at all. The\n * coordinator leaves the store only when the queue is drained and no lease\n * is active — a fast teardown→re-acquire chains onto the same tail instead\n * of hitting \"already registered\" or overlapping a second mount. */\ninterface AuxiliaryWatchLease {\n fiber: AuxiliaryFiber | undefined\n closed: boolean\n done(): void\n}\ninterface AuxiliaryWatchCoordinator {\n tail: Promise<void>\n active: number\n mount(): AuxiliaryWatchLease\n}\nconst AUXILIARY_MODEL_WATCH_COORDINATOR = 'dshAuxiliaryModelWatchCoordinator'\n\nfunction acquireWatchCoordinator(root: AuxiliaryRoot): AuxiliaryWatchCoordinator {\n const existing = root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) as AuxiliaryWatchCoordinator | undefined\n if (existing) return existing\n let unprovide: (() => void) | undefined\n const coordinator: AuxiliaryWatchCoordinator = {\n tail: Promise.resolve(),\n active: 0,\n mount() {\n coordinator.active += 1\n const previousTail = coordinator.tail\n const lease: AuxiliaryWatchLease = {\n closed: false,\n done() {\n if (lease.closed) return\n lease.closed = true\n coordinator.active -= 1\n const owned = lease.fiber\n lease.fiber = undefined\n // The unmount joins the tail every later mount waits on.\n coordinator.tail = coordinator.tail\n .then(() => Promise.resolve(owned?.dispose()).catch(() => {}))\n // Unprovide only once the queue is really drained: a newer lease\n // already queued on this coordinator must keep it alive.\n void coordinator.tail.then(() => {\n if (coordinator.active > 0) return\n try {\n if (root.get(AUXILIARY_MODEL_WATCH_COORDINATOR, false) === coordinator) {\n void Promise.resolve(unprovide?.()).catch(() => {})\n }\n } catch { /* Root already unloading. */ }\n })\n },\n fiber: undefined,\n }\n void previousTail.then(() => {\n // A release that beat the mount ends here: nothing mounted, nothing\n // to unmount, and the tail already moved on.\n if (lease.closed) return\n // mountModelSettingsChanges performs this exact inject/$mount pair;\n // it is repeated here only because that helper returns void and the\n // fiber must be captured for symmetric teardown.\n try {\n lease.fiber = root.inject(['remote'], injected => {\n const scope = injected as AuxiliaryRemoteMountScope\n const mounting = scope.remote.$mount(\n modelSettingsChangesRemote(AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY),\n ) as Promise<(() => Promise<void>) | undefined>\n void mounting.catch(() => {})\n scope.effect(() => async () => {\n const unmount = await mounting.catch(() => undefined)\n await unmount?.()\n })\n })\n } catch { /* remote unavailable: the card degrades to manual refresh */ }\n })\n return lease\n },\n }\n unprovide = root.provide(AUXILIARY_MODEL_WATCH_COORDINATOR, coordinator)\n return coordinator\n}\n\ninterface AuxiliaryCardService {\n retain(): void\n release(): void\n}\n\n/**\n * Acquire the root-owned card service, creating it on first use. One counter\n * covers participants, the seat wait and the change-stream mount: the last\n * release removes the wait, then the watch contribution, then the provider\n * itself synchronously (Cordis deletes the impl before its async disposer\n * awaits), so a later acquire sees a clean get() miss and can re-establish.\n */\nfunction acquireAuxiliaryModelCard(root: AuxiliaryRoot, ctx: Context): AuxiliaryCardService {\n const existing = root.get(AUXILIARY_MODEL_CARD_SERVICE, false) as AuxiliaryCardService | undefined\n if (existing) return existing\n let users = 0\n let styleElement: HTMLStyleElement | undefined\n let stopItem: (() => void) | undefined\n let unprovide: (() => void) | undefined\n // The client-side stream descriptor hangs off the shared root context —\n // never a plugin fiber — so the first participant's unload cannot take the\n // card's change stream down. The coordinator still unmounts it, in tail\n // order, on the last release.\n const watchLease = acquireWatchCoordinator(root).mount()\n const subscriber: ModelSettingsSubscriber = createModelSettingsSubscriber(\n ctx.root as unknown as Parameters<typeof createModelSettingsSubscriber>[0],\n AUXILIARY_MODEL_PACKAGE,\n AUXILIARY_MODEL_CHANGES_IDENTITY,\n ns => ns === AUXILIARY_MODEL_NS,\n )\n const render = (props: { renderFactorySlot?: RenderFactorySlot }) => <AuxiliaryModelCard\n client={root as unknown as AuxiliaryModelClient}\n subscribe={subscriber.subscribe}\n renderFactorySlot={props.renderFactorySlot} />\n const service: AuxiliaryCardService = {\n retain() {\n // The reservation is counted before the throwing installs, so a failure\n // lets the caller's release() undo it fully instead of leaking the service.\n users += 1\n if (styleElement === undefined && typeof document !== 'undefined') {\n styleElement = document.createElement('style')\n styleElement.setAttribute('data-plugin', 'dsh-auxiliary-model-shared')\n styleElement.textContent = auxiliaryModelCss\n document.head.appendChild(styleElement)\n }\n /* The card is a participant of the shared column too: the root-owned\n * call contributes its row AND retains the page and nav icon, which is\n * what guarantees a lone auxiliary plugin still creates the Session\n * Management section — no business plugin has to provide it. The waits\n * sit on the root context, so they live exactly as long as the card. */\n if (!stopItem) {\n stopItem = registerSessionSettingsItem(ctx.root, {\n id: AUXILIARY_MODEL_SEAT_ID,\n order: AUXILIARY_MODEL_SEAT_ORDER,\n }, render)\n }\n },\n release() {\n if (users <= 0) return\n users -= 1\n if (users > 0) return\n stopItem?.()\n stopItem = undefined\n styleElement?.remove()\n styleElement = undefined\n watchLease.done()\n // The store entry leaves synchronously, like the shared page service:\n // the next acquire must not see a stale \"already registered\" marker.\n try { void Promise.resolve(unprovide?.()).catch(() => {}) } catch { /* Root already unloading. */ }\n unprovide = undefined\n },\n }\n unprovide = root.provide(AUXILIARY_MODEL_CARD_SERVICE, service)\n return service\n}\n\n/**\n * Show the shared \"default auxiliary model\" card while the calling plugin is\n * enabled.\n *\n * `ctx` is the plugin's own client context: the card itself is retained on the\n * shared root context for as long as any participant lives, so an arbitrary\n * subset of enabled plugins yields the same single card. The mount's slot wait\n * and the change-stream contribution are owned by the root service — the first\n * participant's unload cannot take the card down. Returns a disposer; it may\n * be invoked by both ctx.effect and an explicit caller and releases at most\n * once, so a double call cannot unbalance the shared refcount.\n */\nexport function registerAuxiliaryModelSettings(ctx: Context): () => void {\n const root = ctx.root as unknown as AuxiliaryRoot\n const service = acquireAuxiliaryModelCard(root, ctx)\n try {\n service.retain()\n } catch (error) {\n // A synchronous failure (for example a duplicate row id) must not pin the\n // shared card for a participant that never joined.\n service.release()\n throw error\n }\n let disposed = false\n return () => {\n if (disposed) return\n disposed = true\n service.release()\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_NS, AUXILIARY_MODEL_SERVICE, getAuxiliaryModel } from "./contracts.js";
2
+ import { createPluginLogger } from "./logger.js";
2
3
  import { registerHostRpc } from "./host-rpc.js";
3
4
  import { notifyModelSettingsChanged, registerModelSettings } from "./model-settings.js";
4
5
  import { createModelSettingsChanges } from "./model-settings-changes.js";
@@ -84,7 +85,13 @@ function setSharedModel(state, ctx, input, expectedRevision) {
84
85
  if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) return Promise.reject(/* @__PURE__ */ new Error("Invalid model route"));
85
86
  const commit = state.tail.then(async () => {
86
87
  if (state.closed || !state.domain) throw new Error("Auxiliary model settings are unloading; retry in a moment");
87
- if (expectedRevision !== state.data.revision) throw conflictError("Auxiliary model settings changed; reload and retry");
88
+ if (expectedRevision !== state.data.revision) {
89
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).warn("auxiliary-model.conflict", "Auxiliary model update rejected due to revision conflict.", void 0, {
90
+ expectedRevision: Number(expectedRevision),
91
+ currentRevision: state.data.revision
92
+ });
93
+ throw conflictError("Auxiliary model settings changed; reload and retry");
94
+ }
88
95
  const next = {
89
96
  revision: state.data.revision + 1,
90
97
  model: modelRouteOverride(normalized) ?? null
@@ -94,6 +101,11 @@ function setSharedModel(state, ctx, input, expectedRevision) {
94
101
  model: next.model
95
102
  });
96
103
  state.data = next;
104
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).info("auxiliary-model.updated", "Updated global auxiliary model default.", {
105
+ revision: next.revision,
106
+ provider: next.model?.provider ?? null,
107
+ model: next.model?.model ?? null
108
+ });
97
109
  notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision);
98
110
  return next;
99
111
  });
@@ -101,6 +113,7 @@ function setSharedModel(state, ctx, input, expectedRevision) {
101
113
  return commit;
102
114
  }
103
115
  function initScope(state, ctx, scope) {
116
+ const log = createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false });
104
117
  const generation = ++state.generation;
105
118
  let disposed = false;
106
119
  scope.effect(() => () => {
@@ -132,6 +145,11 @@ function initScope(state, ctx, scope) {
132
145
  };
133
146
  state.error = void 0;
134
147
  state.available = true;
148
+ log.info("auxiliary-model.opened", "Shared auxiliary model store opened.", {
149
+ generation,
150
+ revision: state.data.revision,
151
+ hasModel: Boolean(state.data.model)
152
+ });
135
153
  scope.effect(() => registerModelSettings(scope, {
136
154
  ns: AUXILIARY_MODEL_NS,
137
155
  title: {
@@ -215,7 +233,10 @@ function initScope(state, ctx, scope) {
215
233
  message: `Unknown endpoint: ${endpoint}`
216
234
  }
217
235
  };
218
- }, { isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint) }));
236
+ }, {
237
+ isEndpoint: (endpoint) => /^[a-z.]+$/.test(endpoint),
238
+ logger: log
239
+ }));
219
240
  createModelSettingsChanges(scope, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS);
220
241
  scope.effect(() => () => {
221
242
  if (state.generation === generation) {
@@ -224,6 +245,8 @@ function initScope(state, ctx, scope) {
224
245
  }
225
246
  });
226
247
  })().catch((error) => {
248
+ if (error && typeof error === "object" && error[RECOVERABLE]) log.debug("auxiliary-model.superseded", "Shared auxiliary model init superseded during open.", { generation });
249
+ else log.error("auxiliary-model.init-failed", "Shared auxiliary model store failed to open.", error, { generation });
227
250
  if (error && typeof error === "object") error.auxGeneration = generation;
228
251
  if (state.generation === generation) {
229
252
  state.available = false;
@@ -247,6 +270,7 @@ function teardown(state, ctx) {
247
270
  try {
248
271
  await state.fiber?.dispose();
249
272
  } catch (error) {
273
+ createPluginLogger(ctx, "dsh-plugin-kit", { forwardHostWarnError: false }).fault("auxiliary-model.dispose-failed", "Shared auxiliary model owner fiber failed to dispose.", error);
250
274
  try {
251
275
  ctx.logger?.warn("auxiliary-model: fiber dispose failed", error);
252
276
  } catch {}
@@ -1 +1 @@
1
- {"version":3,"file":"auxiliary-model.js","names":[],"sources":["../src/auxiliary-model.ts"],"sourcesContent":["/**\n * Shared auxiliary-model default: host-side lifecycle.\n *\n * The default model is repository-shared infrastructure, not one plugin's\n * business state. Its owner is a dedicated Cordis fiber started on the root\n * context by the first participant and disposed by the last release, so the\n * storage domain, the Typert change stream, the RPC channel and the\n * model-settings provider all live and die with the shared owner — never\n * with a business plugin's fiber. Every bundler's own copy of this module\n * rendezvous through `ctx.root`, so no participant depends on another being\n * enabled.\n *\n * Concurrency:\n * - Writes serialize on `state.tail`; the revision check and the bump both\n * happen inside the queued commit, so two callers carrying the same\n * expectedRevision cannot both succeed.\n * - `state.closing` is the single teardown promise: it settles init, drains\n * queued writes, disposes the owner fiber (route, stream, provider,\n * domain) and only then unprovides the state, so a re-entrant retain can\n * always await it and reinstall cleanly.\n * - Initialization and stored-row failures are sticky (`state.error`) and\n * rethrow from `snapshot()`: a storage error must never read as \"no\n * default\" and silently fall back to another model.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { modelRouteOverride, normalizeModelRoute, type ModelRoute } from '@klarkxy/dsh-model-route'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n AUXILIARY_MODEL_SERVICE, type AuxiliaryModelDefaults, type AuxiliaryModelShared, type RpcResult,\n} from './contracts.ts'\nimport { registerHostRpc, type HostRpcContext } from './host-rpc.ts'\nimport { createModelSettingsChanges } from './model-settings-changes.ts'\nimport { registerModelSettings, notifyModelSettingsChanged, type ModelSettingsEvents } from './model-settings.ts'\n\nconst DOMAIN_NAME = 'dsh_auxiliary'\nconst DOMAIN_VERSION = 1\nconst TABLE = 'settings'\nconst KEY = 'defaults'\nconst INJECT = ['storageDomain', 'webServer', 'connection'] as const\n\ninterface DomainHandle {\n table(name: string): {\n get(key: string): { revision: number; model: ModelRoute | null } | undefined\n put(key: string, value: unknown): Promise<void>\n }\n close(): Promise<void>\n}\n\ninterface Scope extends HostRpcContext, ModelSettingsEvents {\n storageDomain: { open(spec: unknown): Promise<DomainHandle> }\n effect(callback: () => (() => void) | Promise<(() => void) | void>): unknown\n}\n\ninterface RootLike {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: readonly string[], callback: (scope: Scope) => void): { dispose(): Promise<void> } & PromiseLike<unknown>\n}\n\ninterface SharedState extends AuxiliaryModelShared {\n ready: Promise<void>\n users: number\n closed: boolean\n /** True only while the owner fiber's domain is open; snapshot() gates on it. */\n available: boolean\n /** Generation of the latest scope init; a stale run must not win. */\n generation: number\n /** Sticky init/storage failure; snapshot() rethrows it. */\n error?: unknown\n closing?: Promise<void>\n domain?: DomainHandle\n data: AuxiliaryModelDefaults\n tail: Promise<void>\n fiber?: { dispose(): Promise<void> }\n scopeInit?: Promise<void>\n stopUnprovide?: () => void\n teardown(): Promise<void>\n}\n\n/** Marker on the benign \"released while the domain was still opening\" error. */\nconst RECOVERABLE = Symbol.for('klarkxy.dsh.auxiliary-model.recoverable')\nfunction recoverable<T extends object>(error: T): T & { [RECOVERABLE]?: boolean } {\n (error as Record<symbol, unknown>)[RECOVERABLE] = true\n return error as T & { [RECOVERABLE]?: boolean }\n}\n\nasync function openSharedDomain(scope: Scope): Promise<DomainHandle> {\n const [{ defineDomain, domainTable }, { z }] = await Promise.all([\n import('@deepseek-ai/dsh-storage-domain'), import('zod'),\n ])\n // Strict, write-side semantics at the durable boundary: identifiers must be\n // non-blank, already trimmed and control-free, or the row is rejected —\n // a malformed stored route must never normalize into \"no default\".\n const identifier = z.string().min(1).max(250).refine(\n value => value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value),\n 'model route identifiers must be non-blank, trimmed and control-free',\n )\n const model = z.union([\n z.object({\n provider: identifier,\n model: identifier,\n reasoningEffort: identifier.max(80).optional(),\n }).strict(),\n z.null(),\n ])\n return scope.storageDomain.open(defineDomain({\n name: DOMAIN_NAME, version: DOMAIN_VERSION,\n tables: {\n [TABLE]: domainTable(z.object({\n revision: z.number().int().nonnegative(),\n model,\n }).strict()),\n },\n }))\n}\n\n/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */\nfunction conflictError(message: string): Error & { conflict: true } {\n return Object.assign(new Error(message), { conflict: true as const })\n}\n\n/** RPC-visible error classification; storage failures stay sanitized. */\nfunction rpcFailure(error: unknown): RpcResult {\n if (error && typeof error === 'object' && 'conflict' in error) {\n return { ok: false, error: { code: 'auxiliary-model/conflict', message: error instanceof Error ? error.message : 'Revision conflict' } }\n }\n if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) {\n return { ok: false, error: { code: 'auxiliary-model/rejected', message: error.message } }\n }\n return { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } }\n}\n\n/**\n * Serialized optimistic-lock write; resolves after the durable put commits.\n * Revision verification happens inside the queue: two writers carrying the\n * same expectedRevision race for one slot and only the first commits.\n * Notification fires only after commit, so a failed observer can never turn\n * an already committed write into an apparent save failure.\n */\nfunction setSharedModel(state: SharedState, ctx: Context, input: unknown, expectedRevision: unknown): Promise<AuxiliaryModelDefaults> {\n if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError('A valid expected revision is required; reload and retry'))\n const row = input && typeof input === 'object' && !Array.isArray(input) ? input as Record<string, unknown> : undefined\n const validField = (value: unknown): value is string =>\n typeof value === 'string' && value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value)\n if (!row || ['provider', 'model', 'reasoningEffort'].some(key => row[key] !== undefined && !validField(row[key]))) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const normalized = normalizeModelRoute(row)\n if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const commit = state.tail.then(async (): Promise<AuxiliaryModelDefaults> => {\n if (state.closed || !state.domain) throw new Error('Auxiliary model settings are unloading; retry in a moment')\n if (expectedRevision !== state.data.revision) throw conflictError('Auxiliary model settings changed; reload and retry')\n const next: AuxiliaryModelDefaults = { revision: state.data.revision + 1, model: modelRouteOverride(normalized) ?? null }\n await state.domain.table(TABLE).put(KEY, { revision: next.revision, model: next.model })\n state.data = next\n notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision)\n return next\n })\n // The tail itself never rejects: a failed commit must not poison later writes.\n state.tail = commit.then(() => {}, () => {})\n return commit\n}\n\nfunction initScope(state: SharedState, ctx: Context, scope: Scope): Promise<void> {\n // A dependency loss unloads this fiber (running its disposers) and a\n // remount re-runs the callback (bumping state.generation), so an init that\n // was still opening always detects its own supersession through these two\n // public signals. Service identity is intentionally NOT compared anywhere:\n // `ctx.<service>` reads hand out a fresh traceable proxy per access for\n // every tracker'd impl (all Service subclasses — storageDomain, webServer,\n // connection), so identity comparisons reported a fake dep swap after each\n // await and aborted every open with 'released during open'.\n const generation = ++state.generation\n let disposed = false\n // Registered synchronously while the callback runs; a throw means the\n // fiber is already inactive and this init must not open anything.\n scope.effect(() => () => { disposed = true })\n const stale = () => state.closed || disposed || state.generation !== generation\n const run = (async () => {\n // Reads must stay unavailable until this init completes.\n state.available = false\n const domain = await openSharedDomain(scope)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store released during open')) }\n state.domain = domain\n // scope.effect throws on an inactive fiber (disposed mid-open): close the\n // domain directly so it is never leaked, then rethrow — a store that\n // cannot register its own cleanup must not publish data.\n try { scope.effect(() => () => domain.close()) } catch (error) { await domain.close(); throw error }\n // Strict schema: a malformed stored row fails the open path instead of\n // silently collapsing to \"no default\".\n const stored = domain.table(TABLE).get(KEY)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store superseded during open')) }\n state.data = { revision: stored?.revision ?? 0, model: stored?.model ?? null }\n state.error = undefined\n state.available = true\n scope.effect(() => registerModelSettings(scope, {\n ns: AUXILIARY_MODEL_NS,\n title: { zh: '辅助模型', en: 'Auxiliary model' },\n read(signal) {\n signal.throwIfAborted()\n const { revision, model } = state.data\n return {\n revision,\n fields: [{\n path: ['model'],\n label: { zh: '全局辅助模型', en: 'Global auxiliary model' },\n current: model ?? undefined,\n allowFollow: true,\n followLabel: { zh: '当前会话模型', en: 'Current conversation model' },\n }],\n }\n },\n async write(path, route, expectedRevision, signal) {\n signal.throwIfAborted()\n if (path.length !== 1 || path[0] !== 'model') throw new Error('Unknown auxiliary model setting')\n return setSharedModel(state, ctx, route, expectedRevision)\n },\n }))\n scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {\n signal.throwIfAborted()\n if (state.closed) return { ok: false, error: { code: 'auxiliary-model/unloading', message: 'Auxiliary model settings are unloading; retry in a moment' } } satisfies RpcResult\n if (!state.available) {\n return state.error\n ? { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } } satisfies RpcResult\n : { ok: false, error: { code: 'auxiliary-model/unavailable', message: 'Auxiliary model settings are unavailable; retry in a moment' } } satisfies RpcResult\n }\n if (endpoint === 'defaults.get') {\n const { revision, model } = state.data\n return { ok: true, value: { revision, model } } satisfies RpcResult\n }\n if (endpoint === 'defaults.set') {\n try {\n const body = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record<string, unknown> : {}\n const value = await setSharedModel(state, ctx, body.model, body.expectedRevision)\n signal.throwIfAborted()\n return { ok: true, value } satisfies RpcResult\n } catch (error) {\n if (signal.aborted) throw error\n return rpcFailure(error)\n }\n }\n return { ok: false, error: { code: 'auxiliary-model/unknown-endpoint', message: `Unknown endpoint: ${endpoint}` } } satisfies RpcResult\n }, { isEndpoint: endpoint => /^[a-z.]+$/.test(endpoint) }))\n // The change stream lives and dies with this shared fiber.\n createModelSettingsChanges(scope as unknown as Context, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS)\n // Disposers run LIFO on unload: registered last, this runs FIRST when a\n // dependency remount tears the scope down — reads go unavailable before\n // the domain closes, so no stale default ever leaks. Gated on generation\n // alone: the same generation's unload must clear state even when its dep\n // impl just changed; only a superseded scope's teardown stays out.\n scope.effect(() => () => { if (state.generation === generation) { state.available = false; state.domain = undefined } })\n })().catch(error => {\n // Tag the failure with its generation so ready.catch can distinguish a\n // superseded init's rejection from the live scope's.\n if (error && typeof error === 'object') (error as Record<string, unknown>).auxGeneration = generation\n // Only THIS generation's failure poisons the store: a superseded init's\n // rejection must not clear a newer scope's available state.\n if (state.generation === generation) {\n state.available = false\n state.error = error\n }\n throw error\n })\n // Retainers always await the CURRENT init: re-arm state.ready so nobody is\n // left waiting on a superseded generation's settled promise. Both promises\n // get a handled branch so a rejected init never surfaces as an unhandled\n // rejection while awaiters still see it.\n run.catch(() => {})\n const armed = run.then(() => {})\n armed.catch(() => {})\n state.ready = armed\n return run\n}\n\nfunction teardown(state: SharedState, ctx: Context): Promise<void> {\n if (!state.closing) {\n state.closed = true\n const root = ctx.root as unknown as RootLike\n state.closing = (async () => {\n // A still-opening init settles here so its domain/fiber are disposed.\n await state.scopeInit?.catch(() => {})\n await state.tail.catch(() => {})\n try { await state.fiber?.dispose() } catch (error) {\n // A failed dispose may leave the domain open; log loudly so the next\n // open's 'already open' error is attributable instead of mysterious.\n try { (ctx as unknown as { logger?: { warn(...args: unknown[]): void } }).logger?.warn('auxiliary-model: fiber dispose failed', error) }\n catch { /* no logger */ }\n }\n try {\n if (root.get(AUXILIARY_MODEL_SERVICE, false) === state) state.stopUnprovide?.()\n } catch { /* root already gone */ }\n state.stopUnprovide = undefined\n })()\n }\n return state.closing\n}\n\nfunction createSharedState(ctx: Context): SharedState {\n const root = ctx.root as unknown as RootLike\n const state: SharedState = {\n users: 0, closed: false, available: false, generation: 0, tail: Promise.resolve(),\n data: { revision: 0, model: null },\n snapshot() {\n // A persisted-open failure propagates: falling back to another model\n // after a storage error would silently switch what the user pays for.\n if (this.error) throw this.error\n // While the owner fiber re-initializes after a dependency remount, the\n // stored default is unreadable: reads fail instead of serving stale data.\n if (!this.available) throw new Error('Auxiliary model store is unavailable; retry in a moment')\n return this.data\n },\n ready: Promise.resolve(),\n teardown() { return teardown(this, ctx) },\n }\n // Strict requirement: every service must already be ACTIVE. Callers are\n // plugins declaring these in inject — Cordis only runs their apply once the\n // deps are active, so a missing or still-pending service is a hard error,\n // not something to wait on. Retry is the caller's Cordis lifecycle, not ours.\n const inactive = INJECT.filter(service => {\n try { return root.get(service, true) === undefined } catch { return true }\n })\n if (inactive.length) {\n throw new Error(`retainAuxiliaryModel requires active ${inactive.join(', ')}; declare them in inject`)\n }\n state.fiber = root.inject(INJECT, scope => { state.scopeInit = initScope(state, ctx, scope) })\n state.ready = Promise.resolve(state.fiber).then(() => {\n // The owner fiber ran to settle: its scope must have started init, and\n // that init must complete before the store reads as ready. A dep dropping\n // between the strict check and inject leaves the fiber PENDING; Cordis\n // re-runs it when the dep returns — never a silent half-initialized read.\n if (!state.scopeInit) throw recoverable(new Error('Auxiliary model owner fiber settled without initializing its scope'))\n return state.scopeInit\n })\n // A failed open must not leave a live-looking state behind: mark it\n // closed and tear it down so the next retain reinstalls. A rejection\n // tagged with a superseded generation describes a scope this state has\n // already moved past — it must not tear down a newer, live scope.\n state.ready.catch(error => {\n const tag = (error as { auxGeneration?: number }).auxGeneration\n if (tag !== undefined && tag !== state.generation) return\n state.error = state.error ?? error\n state.closed = true\n void state.teardown()\n })\n // Publish only after construction completes: another retainer can now\n // either reuse this state or watch its closed flag.\n state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state)\n return state\n}\n\nasync function obtainShared(ctx: Context): Promise<SharedState> {\n const root = ctx.root as unknown as RootLike\n for (;;) {\n const existing = root.get(AUXILIARY_MODEL_SERVICE, false) as SharedState | undefined\n if (existing) {\n if (!existing.closed) return existing\n // closing always settles; wait for it, then re-lookup.\n await existing.closing\n continue\n }\n return createSharedState(ctx)\n }\n}\n\n/**\n * Retain the shared auxiliary-model store for the calling plugin.\n *\n * Resolves once storage is open and the read/write surfaces are live, with a\n * release function; register it as `ctx.effect(() => release)` so the store\n * unloads with the plugin. The first participant opens the shared owner\n * fiber; `await release()` drains pending writes and fully tears the owner\n * down when the last participant leaves. Requires `storageDomain`,\n * `webServer` and `connection` to exist on the host.\n *\n * Concurrent retainers share one owner; a retain arriving during teardown\n * waits for the closing owner and then reinstalls — it never inherits a\n * half-torn-down state.\n */\nexport async function retainAuxiliaryModel(ctx: Context): Promise<() => Promise<void>> {\n for (;;) {\n const state = await obtainShared(ctx)\n // One await, one answer: a failed or superseded init rejects the retainer\n // once. The caller's plugin lifecycle (inject refresh) drives any retry —\n // no same-state spin here.\n await state.ready\n if (state.closed) continue\n state.users += 1\n let released = false\n return async () => {\n if (released) return\n released = true\n state.users -= 1\n if (state.users <= 0) await state.teardown()\n else await state.tail.catch(() => {})\n }\n }\n}\n\n/**\n * Re-exported here so consumers reading the shared default use the same\n * subpath as the store lifecycle: '@klarkxy/dsh-plugin-kit/auxiliary-model'.\n */\nexport { getAuxiliaryModel } from './contracts.ts'\n"],"mappings":"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"}
1
+ {"version":3,"file":"auxiliary-model.js","names":[],"sources":["../src/auxiliary-model.ts"],"sourcesContent":["/**\n * Shared auxiliary-model default: host-side lifecycle.\n *\n * The default model is repository-shared infrastructure, not one plugin's\n * business state. Its owner is a dedicated Cordis fiber started on the root\n * context by the first participant and disposed by the last release, so the\n * storage domain, the Typert change stream, the RPC channel and the\n * model-settings provider all live and die with the shared owner — never\n * with a business plugin's fiber. Every bundler's own copy of this module\n * rendezvous through `ctx.root`, so no participant depends on another being\n * enabled.\n *\n * Concurrency:\n * - Writes serialize on `state.tail`; the revision check and the bump both\n * happen inside the queued commit, so two callers carrying the same\n * expectedRevision cannot both succeed.\n * - `state.closing` is the single teardown promise: it settles init, drains\n * queued writes, disposes the owner fiber (route, stream, provider,\n * domain) and only then unprovides the state, so a re-entrant retain can\n * always await it and reinstall cleanly.\n * - Initialization and stored-row failures are sticky (`state.error`) and\n * rethrow from `snapshot()`: a storage error must never read as \"no\n * default\" and silently fall back to another model.\n */\nimport type { Context } from '@deepseek-ai/cordis'\nimport { modelRouteOverride, normalizeModelRoute, type ModelRoute } from '@klarkxy/dsh-model-route'\nimport {\n AUXILIARY_MODEL_CHANNEL, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS,\n AUXILIARY_MODEL_SERVICE, type AuxiliaryModelDefaults, type AuxiliaryModelShared, type RpcResult,\n} from './contracts.ts'\nimport { registerHostRpc, type HostRpcContext } from './host-rpc.ts'\nimport { createPluginLogger } from './logger.ts'\nimport { createModelSettingsChanges } from './model-settings-changes.ts'\nimport { registerModelSettings, notifyModelSettingsChanged, type ModelSettingsEvents } from './model-settings.ts'\n\nconst DOMAIN_NAME = 'dsh_auxiliary'\nconst DOMAIN_VERSION = 1\nconst TABLE = 'settings'\nconst KEY = 'defaults'\nconst INJECT = ['storageDomain', 'webServer', 'connection'] as const\n\ninterface DomainHandle {\n table(name: string): {\n get(key: string): { revision: number; model: ModelRoute | null } | undefined\n put(key: string, value: unknown): Promise<void>\n }\n close(): Promise<void>\n}\n\ninterface Scope extends HostRpcContext, ModelSettingsEvents {\n storageDomain: { open(spec: unknown): Promise<DomainHandle> }\n effect(callback: () => (() => void) | Promise<(() => void) | void>): unknown\n}\n\ninterface RootLike {\n get(name: string, strict?: boolean): unknown\n provide(name: string, value: unknown): () => void\n inject(deps: readonly string[], callback: (scope: Scope) => void): { dispose(): Promise<void> } & PromiseLike<unknown>\n}\n\ninterface SharedState extends AuxiliaryModelShared {\n ready: Promise<void>\n users: number\n closed: boolean\n /** True only while the owner fiber's domain is open; snapshot() gates on it. */\n available: boolean\n /** Generation of the latest scope init; a stale run must not win. */\n generation: number\n /** Sticky init/storage failure; snapshot() rethrows it. */\n error?: unknown\n closing?: Promise<void>\n domain?: DomainHandle\n data: AuxiliaryModelDefaults\n tail: Promise<void>\n fiber?: { dispose(): Promise<void> }\n scopeInit?: Promise<void>\n stopUnprovide?: () => void\n teardown(): Promise<void>\n}\n\n/** Marker on the benign \"released while the domain was still opening\" error. */\nconst RECOVERABLE = Symbol.for('klarkxy.dsh.auxiliary-model.recoverable')\nfunction recoverable<T extends object>(error: T): T & { [RECOVERABLE]?: boolean } {\n (error as Record<symbol, unknown>)[RECOVERABLE] = true\n return error as T & { [RECOVERABLE]?: boolean }\n}\n\nasync function openSharedDomain(scope: Scope): Promise<DomainHandle> {\n const [{ defineDomain, domainTable }, { z }] = await Promise.all([\n import('@deepseek-ai/dsh-storage-domain'), import('zod'),\n ])\n // Strict, write-side semantics at the durable boundary: identifiers must be\n // non-blank, already trimmed and control-free, or the row is rejected —\n // a malformed stored route must never normalize into \"no default\".\n const identifier = z.string().min(1).max(250).refine(\n value => value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value),\n 'model route identifiers must be non-blank, trimmed and control-free',\n )\n const model = z.union([\n z.object({\n provider: identifier,\n model: identifier,\n reasoningEffort: identifier.max(80).optional(),\n }).strict(),\n z.null(),\n ])\n return scope.storageDomain.open(defineDomain({\n name: DOMAIN_NAME, version: DOMAIN_VERSION,\n tables: {\n [TABLE]: domainTable(z.object({\n revision: z.number().int().nonnegative(),\n model,\n }).strict()),\n },\n }))\n}\n\n/** Marks an optimistic-lock conflict so the RPC envelope can carry 'conflict'. */\nfunction conflictError(message: string): Error & { conflict: true } {\n return Object.assign(new Error(message), { conflict: true as const })\n}\n\n/** RPC-visible error classification; storage failures stay sanitized. */\nfunction rpcFailure(error: unknown): RpcResult {\n if (error && typeof error === 'object' && 'conflict' in error) {\n return { ok: false, error: { code: 'auxiliary-model/conflict', message: error instanceof Error ? error.message : 'Revision conflict' } }\n }\n if (error instanceof Error && /Invalid model route|valid expected revision|Unknown auxiliary model setting/.test(error.message)) {\n return { ok: false, error: { code: 'auxiliary-model/rejected', message: error.message } }\n }\n return { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } }\n}\n\n/**\n * Serialized optimistic-lock write; resolves after the durable put commits.\n * Revision verification happens inside the queue: two writers carrying the\n * same expectedRevision race for one slot and only the first commits.\n * Notification fires only after commit, so a failed observer can never turn\n * an already committed write into an apparent save failure.\n */\nfunction setSharedModel(state: SharedState, ctx: Context, input: unknown, expectedRevision: unknown): Promise<AuxiliaryModelDefaults> {\n if (!Number.isSafeInteger(expectedRevision)) return Promise.reject(conflictError('A valid expected revision is required; reload and retry'))\n const row = input && typeof input === 'object' && !Array.isArray(input) ? input as Record<string, unknown> : undefined\n const validField = (value: unknown): value is string =>\n typeof value === 'string' && value === value.trim() && !/[\\x00-\\x1f\\x7f]/.test(value)\n if (!row || ['provider', 'model', 'reasoningEffort'].some(key => row[key] !== undefined && !validField(row[key]))) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const normalized = normalizeModelRoute(row)\n if ((Boolean(row.provider) || Boolean(row.model)) && !modelRouteOverride(normalized)) {\n return Promise.reject(new Error('Invalid model route'))\n }\n const commit = state.tail.then(async (): Promise<AuxiliaryModelDefaults> => {\n if (state.closed || !state.domain) throw new Error('Auxiliary model settings are unloading; retry in a moment')\n if (expectedRevision !== state.data.revision) {\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).warn(\n 'auxiliary-model.conflict',\n 'Auxiliary model update rejected due to revision conflict.',\n undefined,\n { expectedRevision: Number(expectedRevision), currentRevision: state.data.revision },\n )\n throw conflictError('Auxiliary model settings changed; reload and retry')\n }\n const next: AuxiliaryModelDefaults = { revision: state.data.revision + 1, model: modelRouteOverride(normalized) ?? null }\n await state.domain.table(TABLE).put(KEY, { revision: next.revision, model: next.model })\n state.data = next\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).info(\n 'auxiliary-model.updated',\n 'Updated global auxiliary model default.',\n { revision: next.revision, provider: next.model?.provider ?? null, model: next.model?.model ?? null },\n )\n notifyModelSettingsChanged(ctx.root, AUXILIARY_MODEL_NS, next.revision)\n return next\n })\n // The tail itself never rejects: a failed commit must not poison later writes.\n state.tail = commit.then(() => {}, () => {})\n return commit\n}\n\nfunction initScope(state: SharedState, ctx: Context, scope: Scope): Promise<void> {\n const log = createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false })\n // A dependency loss unloads this fiber (running its disposers) and a\n // remount re-runs the callback (bumping state.generation), so an init that\n // was still opening always detects its own supersession through these two\n // public signals. Service identity is intentionally NOT compared anywhere:\n // `ctx.<service>` reads hand out a fresh traceable proxy per access for\n // every tracker'd impl (all Service subclasses — storageDomain, webServer,\n // connection), so identity comparisons reported a fake dep swap after each\n // await and aborted every open with 'released during open'.\n const generation = ++state.generation\n let disposed = false\n // Registered synchronously while the callback runs; a throw means the\n // fiber is already inactive and this init must not open anything.\n scope.effect(() => () => { disposed = true })\n const stale = () => state.closed || disposed || state.generation !== generation\n const run = (async () => {\n // Reads must stay unavailable until this init completes.\n state.available = false\n const domain = await openSharedDomain(scope)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store released during open')) }\n state.domain = domain\n // scope.effect throws on an inactive fiber (disposed mid-open): close the\n // domain directly so it is never leaked, then rethrow — a store that\n // cannot register its own cleanup must not publish data.\n try { scope.effect(() => () => domain.close()) } catch (error) { await domain.close(); throw error }\n // Strict schema: a malformed stored row fails the open path instead of\n // silently collapsing to \"no default\".\n const stored = domain.table(TABLE).get(KEY)\n if (stale()) { await domain.close(); throw recoverable(new Error('Auxiliary model store superseded during open')) }\n state.data = { revision: stored?.revision ?? 0, model: stored?.model ?? null }\n state.error = undefined\n state.available = true\n log.info('auxiliary-model.opened', 'Shared auxiliary model store opened.', {\n generation, revision: state.data.revision, hasModel: Boolean(state.data.model),\n })\n scope.effect(() => registerModelSettings(scope, {\n ns: AUXILIARY_MODEL_NS,\n title: { zh: '辅助模型', en: 'Auxiliary model' },\n read(signal) {\n signal.throwIfAborted()\n const { revision, model } = state.data\n return {\n revision,\n fields: [{\n path: ['model'],\n label: { zh: '全局辅助模型', en: 'Global auxiliary model' },\n current: model ?? undefined,\n allowFollow: true,\n followLabel: { zh: '当前会话模型', en: 'Current conversation model' },\n }],\n }\n },\n async write(path, route, expectedRevision, signal) {\n signal.throwIfAborted()\n if (path.length !== 1 || path[0] !== 'model') throw new Error('Unknown auxiliary model setting')\n return setSharedModel(state, ctx, route, expectedRevision)\n },\n }))\n scope.effect(() => registerHostRpc(scope, AUXILIARY_MODEL_CHANNEL, async (endpoint, payload, signal) => {\n signal.throwIfAborted()\n if (state.closed) return { ok: false, error: { code: 'auxiliary-model/unloading', message: 'Auxiliary model settings are unloading; retry in a moment' } } satisfies RpcResult\n if (!state.available) {\n return state.error\n ? { ok: false, error: { code: 'auxiliary-model/store', message: 'Auxiliary model store request failed' } } satisfies RpcResult\n : { ok: false, error: { code: 'auxiliary-model/unavailable', message: 'Auxiliary model settings are unavailable; retry in a moment' } } satisfies RpcResult\n }\n if (endpoint === 'defaults.get') {\n const { revision, model } = state.data\n return { ok: true, value: { revision, model } } satisfies RpcResult\n }\n if (endpoint === 'defaults.set') {\n try {\n const body = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record<string, unknown> : {}\n const value = await setSharedModel(state, ctx, body.model, body.expectedRevision)\n signal.throwIfAborted()\n return { ok: true, value } satisfies RpcResult\n } catch (error) {\n if (signal.aborted) throw error\n return rpcFailure(error)\n }\n }\n return { ok: false, error: { code: 'auxiliary-model/unknown-endpoint', message: `Unknown endpoint: ${endpoint}` } } satisfies RpcResult\n }, { isEndpoint: endpoint => /^[a-z.]+$/.test(endpoint), logger: log }))\n // The change stream lives and dies with this shared fiber.\n createModelSettingsChanges(scope as unknown as Context, AUXILIARY_MODEL_CHANGES_IDENTITY, AUXILIARY_MODEL_NS)\n // Disposers run LIFO on unload: registered last, this runs FIRST when a\n // dependency remount tears the scope down — reads go unavailable before\n // the domain closes, so no stale default ever leaks. Gated on generation\n // alone: the same generation's unload must clear state even when its dep\n // impl just changed; only a superseded scope's teardown stays out.\n scope.effect(() => () => { if (state.generation === generation) { state.available = false; state.domain = undefined } })\n })().catch(error => {\n if (error && typeof error === 'object' && (error as Record<symbol, unknown>)[RECOVERABLE]) {\n log.debug('auxiliary-model.superseded', 'Shared auxiliary model init superseded during open.', { generation })\n } else {\n log.error('auxiliary-model.init-failed', 'Shared auxiliary model store failed to open.', error, { generation })\n }\n // Tag the failure with its generation so ready.catch can distinguish a\n // superseded init's rejection from the live scope's.\n if (error && typeof error === 'object') (error as Record<string, unknown>).auxGeneration = generation\n // Only THIS generation's failure poisons the store: a superseded init's\n // rejection must not clear a newer scope's available state.\n if (state.generation === generation) {\n state.available = false\n state.error = error\n }\n throw error\n })\n // Retainers always await the CURRENT init: re-arm state.ready so nobody is\n // left waiting on a superseded generation's settled promise. Both promises\n // get a handled branch so a rejected init never surfaces as an unhandled\n // rejection while awaiters still see it.\n run.catch(() => {})\n const armed = run.then(() => {})\n armed.catch(() => {})\n state.ready = armed\n return run\n}\n\nfunction teardown(state: SharedState, ctx: Context): Promise<void> {\n if (!state.closing) {\n state.closed = true\n const root = ctx.root as unknown as RootLike\n state.closing = (async () => {\n // A still-opening init settles here so its domain/fiber are disposed.\n await state.scopeInit?.catch(() => {})\n await state.tail.catch(() => {})\n try { await state.fiber?.dispose() } catch (error) {\n createPluginLogger(ctx, 'dsh-plugin-kit', { forwardHostWarnError: false }).fault(\n 'auxiliary-model.dispose-failed',\n 'Shared auxiliary model owner fiber failed to dispose.',\n error,\n )\n // A failed dispose may leave the domain open; log loudly so the next\n // open's 'already open' error is attributable instead of mysterious.\n try { (ctx as unknown as { logger?: { warn(...args: unknown[]): void } }).logger?.warn('auxiliary-model: fiber dispose failed', error) }\n catch { /* no logger */ }\n }\n try {\n if (root.get(AUXILIARY_MODEL_SERVICE, false) === state) state.stopUnprovide?.()\n } catch { /* root already gone */ }\n state.stopUnprovide = undefined\n })()\n }\n return state.closing\n}\n\nfunction createSharedState(ctx: Context): SharedState {\n const root = ctx.root as unknown as RootLike\n const state: SharedState = {\n users: 0, closed: false, available: false, generation: 0, tail: Promise.resolve(),\n data: { revision: 0, model: null },\n snapshot() {\n // A persisted-open failure propagates: falling back to another model\n // after a storage error would silently switch what the user pays for.\n if (this.error) throw this.error\n // While the owner fiber re-initializes after a dependency remount, the\n // stored default is unreadable: reads fail instead of serving stale data.\n if (!this.available) throw new Error('Auxiliary model store is unavailable; retry in a moment')\n return this.data\n },\n ready: Promise.resolve(),\n teardown() { return teardown(this, ctx) },\n }\n // Strict requirement: every service must already be ACTIVE. Callers are\n // plugins declaring these in inject — Cordis only runs their apply once the\n // deps are active, so a missing or still-pending service is a hard error,\n // not something to wait on. Retry is the caller's Cordis lifecycle, not ours.\n const inactive = INJECT.filter(service => {\n try { return root.get(service, true) === undefined } catch { return true }\n })\n if (inactive.length) {\n throw new Error(`retainAuxiliaryModel requires active ${inactive.join(', ')}; declare them in inject`)\n }\n state.fiber = root.inject(INJECT, scope => { state.scopeInit = initScope(state, ctx, scope) })\n state.ready = Promise.resolve(state.fiber).then(() => {\n // The owner fiber ran to settle: its scope must have started init, and\n // that init must complete before the store reads as ready. A dep dropping\n // between the strict check and inject leaves the fiber PENDING; Cordis\n // re-runs it when the dep returns — never a silent half-initialized read.\n if (!state.scopeInit) throw recoverable(new Error('Auxiliary model owner fiber settled without initializing its scope'))\n return state.scopeInit\n })\n // A failed open must not leave a live-looking state behind: mark it\n // closed and tear it down so the next retain reinstalls. A rejection\n // tagged with a superseded generation describes a scope this state has\n // already moved past — it must not tear down a newer, live scope.\n state.ready.catch(error => {\n const tag = (error as { auxGeneration?: number }).auxGeneration\n if (tag !== undefined && tag !== state.generation) return\n state.error = state.error ?? error\n state.closed = true\n void state.teardown()\n })\n // Publish only after construction completes: another retainer can now\n // either reuse this state or watch its closed flag.\n state.stopUnprovide = root.provide(AUXILIARY_MODEL_SERVICE, state)\n return state\n}\n\nasync function obtainShared(ctx: Context): Promise<SharedState> {\n const root = ctx.root as unknown as RootLike\n for (;;) {\n const existing = root.get(AUXILIARY_MODEL_SERVICE, false) as SharedState | undefined\n if (existing) {\n if (!existing.closed) return existing\n // closing always settles; wait for it, then re-lookup.\n await existing.closing\n continue\n }\n return createSharedState(ctx)\n }\n}\n\n/**\n * Retain the shared auxiliary-model store for the calling plugin.\n *\n * Resolves once storage is open and the read/write surfaces are live, with a\n * release function; register it as `ctx.effect(() => release)` so the store\n * unloads with the plugin. The first participant opens the shared owner\n * fiber; `await release()` drains pending writes and fully tears the owner\n * down when the last participant leaves. Requires `storageDomain`,\n * `webServer` and `connection` to exist on the host.\n *\n * Concurrent retainers share one owner; a retain arriving during teardown\n * waits for the closing owner and then reinstalls — it never inherits a\n * half-torn-down state.\n */\nexport async function retainAuxiliaryModel(ctx: Context): Promise<() => Promise<void>> {\n for (;;) {\n const state = await obtainShared(ctx)\n // One await, one answer: a failed or superseded init rejects the retainer\n // once. The caller's plugin lifecycle (inject refresh) drives any retry —\n // no same-state spin here.\n await state.ready\n if (state.closed) continue\n state.users += 1\n let released = false\n return async () => {\n if (released) return\n released = true\n state.users -= 1\n if (state.users <= 0) await state.teardown()\n else await state.tail.catch(() => {})\n }\n }\n}\n\n/**\n * Re-exported here so consumers reading the shared default use the same\n * subpath as the store lifecycle: '@klarkxy/dsh-plugin-kit/auxiliary-model'.\n */\nexport { getAuxiliaryModel } from './contracts.ts'\n"],"mappings":"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"}
package/lib/cli-media.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createReadStream, createWriteStream } from "node:fs";
2
+ import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
2
3
  import { lstat, mkdir, open, readFile, realpath, rename, writeFile } from "node:fs/promises";
3
4
  import { pipeline } from "node:stream/promises";
4
- import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  //#region src/cli-media.ts
7
7
  var CliMediaError = class extends Error {
@@ -1,3 +1,4 @@
1
+ import { d as PluginLogger } from "./log-shared2.js";
1
2
  import { spawn } from "node:child_process";
2
3
  //#region src/cli-process.d.ts
3
4
  /** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */
@@ -32,6 +33,7 @@ type CliOptions = {
32
33
  maxLineBytes?: number;
33
34
  onLine?: (line: string) => void | Promise<void>;
34
35
  spawnChild?: typeof spawn;
36
+ logger?: PluginLogger;
35
37
  };
36
38
  /** Own one process tree until it exits, including caller cancellation and protocol failure. */
37
39
  declare function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput>;