dsh-daruma 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,23 @@ daruma. daruma then:
16
16
  arms the next routable channel (the user-chosen backup first, then the
17
17
  configured chain) and returns `{ kind: 'retry' }`;
18
18
  3. on the retry turn, the `agent/request` waterfall swaps the request config
19
- onto the armed channel;
19
+ onto the armed channel — dropping the caller's `reasoningEffort` when that
20
+ target does not advertise the level (see below);
20
21
  4. appends one JSON line per decision to the audit log (see below).
21
22
 
23
+ **A failover target has to accept the request.** DSH validates an explicit
24
+ `reasoningEffort` against the **target's** own capability and refuses the
25
+ request before any provider I/O; that refusal is raised outside
26
+ `agent/request-error`, so a swapped request carrying an effort the target
27
+ cannot take kills the turn without ever reaching the provider — the failover
28
+ channel never dispatches, and the channel that is actually broken keeps looking
29
+ healthy. daruma therefore reads the target's model metadata on every swap: a
30
+ supported effort is kept, an unsupported (or unresolvable) one is dropped so
31
+ the target can use its own default, and the drop is logged —
32
+ `dsh-daruma: mt::glm-5.3 does not accept reasoning effort "high" …`. Every
33
+ other field (`maxTokens`, temperature, stop sequences) is passed through
34
+ untouched.
35
+
22
36
  **Self-healing:** the host exposes no request-success event, so success is
23
37
  inferred — an agent whose previous model request never tripped
24
38
  `agent/request-error` earns its channel a success record at the next
@@ -45,6 +59,36 @@ in-repo retries render. The notices are **live-only** — out-of-repo session
45
59
  events are not yet persisted by the harness, so rows do not survive a page
46
60
  reload or a resumed session.
47
61
 
62
+ ## Usage screenshots / 使用截图
63
+
64
+ These are captures from a live DSH Web session running the mock-a → mock-b
65
+ failover flow.
66
+
67
+ The images are linked by absolute URL on purpose: npm's web UI does not resolve
68
+ relative README image paths, so a `./assets/…` link renders broken on the
69
+ package page (it still works on GitHub). The copies shipped in the tarball
70
+ (`files`) are for readers of the package itself.
71
+
72
+ ### English
73
+
74
+ ![English failover notice in the conversation](https://raw.githubusercontent.com/SISCHOI/daruma/main/packages/dsh-daruma/assets/en-failover-row.jpg)
75
+
76
+ *Failover notice shown inline after mock-a returns `RATE_LIMIT`.*
77
+
78
+ ![English backup channel panel](https://raw.githubusercontent.com/SISCHOI/daruma/main/packages/dsh-daruma/assets/en-backup-panel.jpg)
79
+
80
+ *Backup channel picker with the current and candidate models.*
81
+
82
+ ### 中文
83
+
84
+ ![中文会话中的故障转移提示](https://raw.githubusercontent.com/SISCHOI/daruma/main/packages/dsh-daruma/assets/zh-failover-row.jpg)
85
+
86
+ *mock-a 返回 `RATE_LIMIT` 后,中文界面显示故障转移提示。*
87
+
88
+ ![中文备用渠道面板](https://raw.githubusercontent.com/SISCHOI/daruma/main/packages/dsh-daruma/assets/zh-backup-panel.jpg)
89
+
90
+ *备用渠道面板展示当前备用模型和候选模型。*
91
+
48
92
  ## Audit log
49
93
 
50
94
  Every failover / give-up / boot decision is appended to
@@ -115,3 +159,21 @@ custom event surface is logged and degraded without interrupting failover.
115
159
  `src/host-capabilities.ts` records optional host surfaces (RPC and conversation
116
160
  event registry) so client integrations can remain capability-driven as DSH
117
161
  releases evolve.
162
+
163
+ The web transport is bound **late, from an injection callback**
164
+ (`src/host-mount.ts`): the host's `dsh-client-connection` provides its
165
+ `connection` service synchronously through `0.1.0-rc.x`, but awaits browser-auth
166
+ setup first on `0.1.5-rc.1`/`0.1.5-rc.2`, so the service may not exist yet when
167
+ this plugin's `apply` runs. Reading it with a synchronous `ctx.get('connection')`
168
+ skipped the `/dsh-daruma` RPC channel — and with it the status dock and backup
169
+ picker — on those hosts, so the mount waits for the service and records the
170
+ capability snapshot at that point. Hosts without a web transport (headless)
171
+ simply never fire the callback.
172
+
173
+ > Known host limitation: on `0.1.5-rc.1`/`0.1.5-rc.2` the channel still cannot
174
+ > register — those hosts dropped `webServer` from the connection plugin's own
175
+ > inject list while `rpc.handle()` registers its route through the service
176
+ > context, so every caller gets `cannot get property "webServer" without
177
+ > inject`. daruma logs that failure loudly and keeps failover working; the
178
+ > panel needs the host fix. See
179
+ > `docs/aegis/evidence/2026-09-11-latest-harness-compat.md`.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Client transport for the daruma panel.
3
+ *
4
+ * Primary path: the legacy generic channel (`/dsh-daruma`) — served by
5
+ * `rpc.handle` on hosts up to 0.1.2 and on 0.1.5 hosts patched by the
6
+ * community fix.
7
+ *
8
+ * Fallback path: the `/api` shared carrier (`/api/dsh-daruma` exact route,
9
+ * registered by the host-side fallback in `src/http-transport.ts`). The
10
+ * browser caller switches on the first HTTP 404/405 — the route-miss shape of
11
+ * the 0.1.5 `rpc.handle` regression — and sticks with the fallback for the
12
+ * rest of the page lifetime, so the panel needs at most one failed request
13
+ * per load. Auth failures (401/403) and handler errors surface as-is: they
14
+ * say nothing about which transport should be used.
15
+ *
16
+ * Wire shape: exact Fetch routes match by URL path segment, and the shared
17
+ * carrier validates that the envelope `method` equals that segment — so the
18
+ * real endpoint and payload ride inside the payload tunnel, exactly like the
19
+ * real-plane-verified dsh-im management RPC.
20
+ */
21
+ import type { Rpc } from './api.ts';
22
+ /** Structural connection surface (host provides the real one). */
23
+ interface ConnectionLike {
24
+ rpc: {
25
+ call(channel: string, endpoint: string, payload: unknown): Promise<unknown>;
26
+ };
27
+ }
28
+ /** Generic channel served by `connection.rpc.handle` where it works. */
29
+ export declare const LEGACY_CHANNEL = "/dsh-daruma";
30
+ /** Shared browser carrier the host itself mounts on every web-capable host. */
31
+ export declare const FALLBACK_CHANNEL = "/api";
32
+ /** Envelope method demanded by the exact `/api/dsh-daruma` route. */
33
+ export declare const FALLBACK_ENDPOINT = "dsh-daruma";
34
+ /**
35
+ * Whether a rejected legacy call means "the channel route does not exist on
36
+ * this host" (switch to the fallback) rather than anything else. The browser
37
+ * caller throws `transport failure for <channel>/<endpoint>: HTTP <status>`
38
+ * when the response is not ok.
39
+ */
40
+ export declare function isChannelRouteMiss(error: unknown): boolean;
41
+ /** Build the panel `Rpc`: legacy channel first, sticky `/api` fallback. */
42
+ export declare function createAdaptiveRpc(connection: ConnectionLike): Rpc;
43
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Client transport for the daruma panel.
3
+ *
4
+ * Primary path: the legacy generic channel (`/dsh-daruma`) — served by
5
+ * `rpc.handle` on hosts up to 0.1.2 and on 0.1.5 hosts patched by the
6
+ * community fix.
7
+ *
8
+ * Fallback path: the `/api` shared carrier (`/api/dsh-daruma` exact route,
9
+ * registered by the host-side fallback in `src/http-transport.ts`). The
10
+ * browser caller switches on the first HTTP 404/405 — the route-miss shape of
11
+ * the 0.1.5 `rpc.handle` regression — and sticks with the fallback for the
12
+ * rest of the page lifetime, so the panel needs at most one failed request
13
+ * per load. Auth failures (401/403) and handler errors surface as-is: they
14
+ * say nothing about which transport should be used.
15
+ *
16
+ * Wire shape: exact Fetch routes match by URL path segment, and the shared
17
+ * carrier validates that the envelope `method` equals that segment — so the
18
+ * real endpoint and payload ride inside the payload tunnel, exactly like the
19
+ * real-plane-verified dsh-im management RPC.
20
+ */
21
+ /** Generic channel served by `connection.rpc.handle` where it works. */
22
+ export const LEGACY_CHANNEL = '/dsh-daruma';
23
+ /** Shared browser carrier the host itself mounts on every web-capable host. */
24
+ export const FALLBACK_CHANNEL = '/api';
25
+ /** Envelope method demanded by the exact `/api/dsh-daruma` route. */
26
+ export const FALLBACK_ENDPOINT = 'dsh-daruma';
27
+ /**
28
+ * Whether a rejected legacy call means "the channel route does not exist on
29
+ * this host" (switch to the fallback) rather than anything else. The browser
30
+ * caller throws `transport failure for <channel>/<endpoint>: HTTP <status>`
31
+ * when the response is not ok.
32
+ */
33
+ export function isChannelRouteMiss(error) {
34
+ return error instanceof Error && /HTTP (404|405)\b/u.test(error.message);
35
+ }
36
+ function tunnelCall(connection, endpoint, payload) {
37
+ return connection.rpc.call(FALLBACK_CHANNEL, FALLBACK_ENDPOINT, {
38
+ method: endpoint,
39
+ payload: payload ?? {},
40
+ });
41
+ }
42
+ /** Build the panel `Rpc`: legacy channel first, sticky `/api` fallback. */
43
+ export function createAdaptiveRpc(connection) {
44
+ let useFallback = false;
45
+ return (endpoint, payload) => {
46
+ if (useFallback) {
47
+ return tunnelCall(connection, endpoint, payload);
48
+ }
49
+ return connection.rpc.call(LEGACY_CHANNEL, endpoint, payload ?? {}).catch((error) => {
50
+ if (!isChannelRouteMiss(error))
51
+ throw error;
52
+ useFallback = true;
53
+ return tunnelCall(connection, endpoint, payload);
54
+ });
55
+ };
56
+ }
57
+ //# sourceMappingURL=adaptive-rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adaptive-rpc.js","sourceRoot":"","sources":["../../src/client/adaptive-rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAWH,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAA;AAC3C,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAA;AACtC,qEAAqE;AACrE,MAAM,CAAC,MAAM,iBAAiB,GAAG,YAAY,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,KAAK,YAAY,KAAK,IAAI,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;AAC1E,CAAC;AAED,SAAS,UAAU,CACjB,UAA0B,EAC1B,QAAgB,EAChB,OAAgB;IAEhB,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,iBAAiB,EAAE;QAC9D,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,OAAO,IAAI,EAAE;KACvB,CAAC,CAAA;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,UAA0B;IAC1D,IAAI,WAAW,GAAG,KAAK,CAAA;IACvB,OAAO,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE;QAC3B,IAAI,WAAW,EAAE,CAAC;YAChB,OAAO,UAAU,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAuB,CAAA;QACxE,CAAC;QACD,OAAO,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,QAAQ,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,KAAK,CACvE,CAAC,KAAc,EAAE,EAAE;YACjB,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAA;YAC3C,WAAW,GAAG,IAAI,CAAA;YAClB,OAAO,UAAU,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAuB,CAAA;QACxE,CAAC,CACoB,CAAA;IACzB,CAAC,CAAA;AACH,CAAC"}
@@ -7,6 +7,7 @@
7
7
  import { createElement as h } from 'react';
8
8
  import { en, zh } from "./locales.js";
9
9
  import { createApi } from "./api.js";
10
+ import { createAdaptiveRpc } from "./adaptive-rpc.js";
10
11
  import { StatusDock } from "./StatusDock.js";
11
12
  import { createFailoverNoticeDefinition, FAILOVER_NOTICE_KIND, } from "./failover-notice.js";
12
13
  import { FailoverNoticeRow } from "./FailoverNoticeRow.js";
@@ -68,7 +69,7 @@ export function apply(ctx) {
68
69
  console.warn('[dsh-daruma] web transport unavailable — client UI disabled');
69
70
  return;
70
71
  }
71
- const rpc = (endpoint, payload) => connection.rpc.call('/dsh-daruma', endpoint, payload ?? {});
72
+ const rpc = createAdaptiveRpc(connection);
72
73
  const api = createApi(rpc);
73
74
  ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
74
75
  name: 'conversation.input.right',
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
package/lib/client.js CHANGED
@@ -96,6 +96,61 @@ window.__ModuleLoader__.load({
96
96
  };
97
97
  }
98
98
  //#endregion
99
+ //#region lib/client/adaptive-rpc.js
100
+ /**
101
+ * Client transport for the daruma panel.
102
+ *
103
+ * Primary path: the legacy generic channel (`/dsh-daruma`) — served by
104
+ * `rpc.handle` on hosts up to 0.1.2 and on 0.1.5 hosts patched by the
105
+ * community fix.
106
+ *
107
+ * Fallback path: the `/api` shared carrier (`/api/dsh-daruma` exact route,
108
+ * registered by the host-side fallback in `src/http-transport.ts`). The
109
+ * browser caller switches on the first HTTP 404/405 — the route-miss shape of
110
+ * the 0.1.5 `rpc.handle` regression — and sticks with the fallback for the
111
+ * rest of the page lifetime, so the panel needs at most one failed request
112
+ * per load. Auth failures (401/403) and handler errors surface as-is: they
113
+ * say nothing about which transport should be used.
114
+ *
115
+ * Wire shape: exact Fetch routes match by URL path segment, and the shared
116
+ * carrier validates that the envelope `method` equals that segment — so the
117
+ * real endpoint and payload ride inside the payload tunnel, exactly like the
118
+ * real-plane-verified dsh-im management RPC.
119
+ */
120
+ /** Generic channel served by `connection.rpc.handle` where it works. */
121
+ const LEGACY_CHANNEL = "/dsh-daruma";
122
+ /** Shared browser carrier the host itself mounts on every web-capable host. */
123
+ const FALLBACK_CHANNEL = "/api";
124
+ /** Envelope method demanded by the exact `/api/dsh-daruma` route. */
125
+ const FALLBACK_ENDPOINT = "dsh-daruma";
126
+ /**
127
+ * Whether a rejected legacy call means "the channel route does not exist on
128
+ * this host" (switch to the fallback) rather than anything else. The browser
129
+ * caller throws `transport failure for <channel>/<endpoint>: HTTP <status>`
130
+ * when the response is not ok.
131
+ */
132
+ function isChannelRouteMiss(error) {
133
+ return error instanceof Error && /HTTP (404|405)\b/u.test(error.message);
134
+ }
135
+ function tunnelCall(connection, endpoint, payload) {
136
+ return connection.rpc.call(FALLBACK_CHANNEL, FALLBACK_ENDPOINT, {
137
+ method: endpoint,
138
+ payload: payload ?? {}
139
+ });
140
+ }
141
+ /** Build the panel `Rpc`: legacy channel first, sticky `/api` fallback. */
142
+ function createAdaptiveRpc(connection) {
143
+ let useFallback = false;
144
+ return (endpoint, payload) => {
145
+ if (useFallback) return tunnelCall(connection, endpoint, payload);
146
+ return connection.rpc.call(LEGACY_CHANNEL, endpoint, payload ?? {}).catch((error) => {
147
+ if (!isChannelRouteMiss(error)) throw error;
148
+ useFallback = true;
149
+ return tunnelCall(connection, endpoint, payload);
150
+ });
151
+ };
152
+ }
153
+ //#endregion
99
154
  //#region lib/client/health.js
100
155
  /**
101
156
  * Time-aware channel-health classification for the status dock.
@@ -732,8 +787,7 @@ window.__ModuleLoader__.load({
732
787
  console.warn("[dsh-daruma] web transport unavailable — client UI disabled");
733
788
  return;
734
789
  }
735
- const rpc = (endpoint, payload) => connection.rpc.call("/dsh-daruma", endpoint, payload ?? {});
736
- const api = createApi(rpc);
790
+ const api = createApi(createAdaptiveRpc(connection));
737
791
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
738
792
  name: "conversation.input.right",
739
793
  id: "daruma-status",
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["rowStyle","memo","_jsxs","_jsx","Pill","Button","useState","useMemo","useCallback","Modal","_Fragment","_jsx","IconCheckOutline16","IconWarningOutline16","useState","useCallback","_jsxs","_Fragment","Button","h","h"],"sources":["client/locales.js","client/api.js","client/health.js","client/BackupPanel.js","client/StatusDock.js","client/failover-notice.js","client/FailoverNoticeRow.js","client/index.js"],"sourcesContent":["/** zh/en dictionaries for the daruma client UI. */\nexport const zh = {\n status: '渠道状态',\n current: '当前',\n backup: '备用',\n backupNone: '未设置',\n backupManage: '备用渠道',\n backupManageHint: '从候选列表选择备用渠道',\n failoverCount: '已切换',\n lastFailover: '上次切换',\n healthy: '健康',\n cooldown: '冷却中',\n probe: '探测中',\n refresh: '刷新',\n close: '关闭',\n clearBackup: '清除备用',\n provider: '提供方',\n providerPlaceholder: 'provider,如 mt',\n loadCandidates: '加载候选',\n loading: '加载中…',\n candidates: '候选模型',\n setBackup: '设为备用',\n currentBackup: '当前备用',\n currentBackupMark: '备用',\n noCandidates: '未加载到候选模型',\n empty: '暂无渠道数据',\n failoverNoticeLine: '{from} 请求失败({reason})→ 本次尝试 {to}',\n failoverNoticeDetail: '第 {count}/{budget} 次失败切换 · turn {turn} step {step}',\n failoverGiveUpLine: '{from} 请求失败({reason})→ 已用尽恢复手段,停止重试',\n failoverGiveUpDetail: '失败切换 {count}/{budget} · turn {turn} step {step}',\n};\nexport const en = {\n status: 'Channel status',\n current: 'Current',\n backup: 'Backup',\n backupNone: 'none',\n backupManage: 'Backup channel',\n backupManageHint: 'Pick a backup channel from the candidate list',\n failoverCount: 'failovers',\n lastFailover: 'last failover',\n healthy: 'healthy',\n cooldown: 'cooling',\n probe: 'probing',\n refresh: 'Refresh',\n close: 'Close',\n clearBackup: 'Clear backup',\n provider: 'Provider',\n providerPlaceholder: 'provider, e.g. mt',\n loadCandidates: 'Load candidates',\n loading: 'Loading…',\n candidates: 'Candidate models',\n setBackup: 'Set as backup',\n currentBackup: 'Current backup',\n currentBackupMark: 'backup',\n noCandidates: 'No candidates loaded',\n empty: 'No channel data yet',\n failoverNoticeLine: '{from} failed ({reason}) → trying {to}',\n failoverNoticeDetail: 'failover {count}/{budget} · turn {turn} step {step}',\n failoverGiveUpLine: '{from} failed ({reason}) → recovery budget exhausted, giving up',\n failoverGiveUpDetail: 'failovers {count}/{budget} · turn {turn} step {step}',\n};\n//# sourceMappingURL=locales.js.map","/**\n * Client-side wire types and RPC wrapper for the `/dsh-daruma` channel.\n * Structural types only — no monorepo-internal imports.\n */\nexport function createApi(rpc) {\n const call = async (endpoint, payload) => {\n const result = (await rpc(endpoint, payload));\n if (!result.ok) {\n throw new Error(result.error?.message ?? `daruma rpc ${endpoint} failed`);\n }\n return result.value;\n };\n return {\n status: () => call('status'),\n listCandidates: (provider) => call('listCandidates', { provider }),\n setBackup: async (provider, model) => {\n await call('setBackup', { provider, model });\n },\n clearBackup: async () => {\n await call('clearBackup');\n },\n };\n}\n//# sourceMappingURL=api.js.map","/**\n * Time-aware channel-health classification for the status dock.\n *\n * The adapter now records successes (inferred from `agent/pre-step`), which\n * resets a channel to HEALTHY on its first successful request. But a channel\n * that never routes again (removed from the chain, one-shot backup) keeps its\n * last persisted state forever — including a stale COOLDOWN whose deadline\n * has long passed. The dock therefore still judges by time, not by the raw\n * state field: only a channel that is *still* cooling (cooldown not yet\n * expired) counts as an error.\n */\n/**\n * Overall dock state:\n * - `error` — at least one channel is still cooling (`cooldownUntilMs > now`).\n * - `warning` — a probe is in flight (kept for parity; the adapter no longer\n * enters PROBE, but persisted legacy states may surface it).\n * - `done` — everything routable: healthy, or cooled down long enough.\n * - `ongoing` — no snapshot yet.\n */\nexport function overallState(channels, nowMs) {\n if (channels === null)\n return 'ongoing';\n if (channels.some((c) => c.state === 'COOLDOWN' && c.cooldownUntilMs > nowMs))\n return 'error';\n if (channels.some((c) => c.state === 'PROBE'))\n return 'warning';\n return 'done';\n}\n//# sourceMappingURL=health.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * BackupChannelPanel: list candidate models for a provider and pick one as\n * the failover backup. Rendered inside a primitives Modal.\n *\n * No probing happens here anymore — the panel is a plain picker. Channel\n * health still updates from real traffic via the recovery engine.\n */\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Modal, Pill, } from '@deepseek-ai/dsh-client-ui-primitives';\nfunction deriveProvider(status) {\n if (status === null)\n return '';\n if (status.current !== null)\n return status.current.split('::')[0] ?? '';\n if (status.backup !== null)\n return status.backup.provider;\n return status.providers[0] ?? '';\n}\nconst sectionStyle = {\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n padding: '12px 0',\n fontSize: '13px',\n};\nconst rowStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n};\nconst noteStyle = { color: 'var(--dsw-text-tertiary, #999)', fontSize: '12px' };\n/** Clamp a long provider/model name so the row never wraps. */\nconst backupPillStyle = {\n flex: '1 1 auto',\n minWidth: 0,\n overflow: 'hidden',\n};\nconst ellipsisStyle = {\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\nconst listStyle = {\n maxHeight: 300,\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n contain: 'content',\n willChange: 'scroll-position',\n overscrollBehavior: 'contain',\n};\n/** Skip rendering rows outside the viewport while scrolling. */\nconst candidateRowStyle = {\n ...rowStyle,\n contentVisibility: 'auto',\n containIntrinsicSize: '28px',\n};\n/** One candidate row, memoized so selection changes only re-render it. */\nconst CandidateRow = memo(function CandidateRow(props) {\n const { candidate, isBackup, busy, t, onSetBackup } = props;\n return (_jsxs(\"div\", { style: candidateRowStyle, children: [_jsx(\"span\", { style: { width: 14, flexShrink: 0 } }), _jsx(\"span\", { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: candidate.model }), isBackup && _jsx(Pill, { active: true, children: t('currentBackupMark') }), _jsx(Button, { size: \"sm\", variant: \"ghost\", disabled: isBackup || busy, onClick: () => onSetBackup(candidate.model), children: t('setBackup') })] }));\n});\nexport function BackupPanel(props) {\n const { api, t, status, onClose } = props;\n const [provider, setProvider] = useState(deriveProvider(status));\n const [candidates, setCandidates] = useState(null);\n const [loading, setLoading] = useState(false);\n const [busy, setBusy] = useState(null);\n const [error, setError] = useState(null);\n // Local overlay on the status-prop backup: applied optimistically after\n // set/clear so the row reflects the change without waiting for the parent\n // to re-fetch status. Cleared whenever a fresh status arrives.\n const [backupOverride, setBackupOverride] = useState(undefined);\n // The effective backup shown in the panel: local override → status prop.\n const backupShown = backupOverride !== undefined ? backupOverride : status?.backup ?? null;\n // O(1) lookup for the current backup model; re-built only when the effective backup changes.\n const backupModel = useMemo(() => (backupShown !== null && backupShown.provider === provider ? backupShown.model : null), [backupShown, provider]);\n const loadCandidates = async () => {\n if (provider === '')\n return;\n setLoading(true);\n setError(null);\n try {\n setCandidates(await api.listCandidates(provider));\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setLoading(false);\n }\n };\n useEffect(() => {\n if (provider !== '')\n void loadCandidates();\n }, [provider]);\n // A fresh status from the parent supersedes the local override.\n useEffect(() => {\n setBackupOverride(undefined);\n }, [status]);\n const setBackup = async (model) => {\n setBusy(model);\n setError(null);\n try {\n await api.setBackup(provider, model);\n setBackupOverride({ provider, model });\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setBusy(null);\n }\n };\n const clearBackup = async () => {\n setBusy('__clear__');\n setError(null);\n try {\n await api.clearBackup();\n setBackupOverride(null);\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setBusy(null);\n }\n };\n // Stable callback so memoized CandidateRow props don't change every render.\n const handleSetBackup = useCallback((model) => void setBackup(model), [provider]);\n return (_jsx(Modal, { open: true, onClose: onClose, title: t('backupManage'), className: \"daruma-wide-dialog\", closeLabel: t('close'), description: t('backupManageHint'), footer: _jsx(Button, { variant: \"primary\", onClick: onClose, children: t('close') }), children: _jsxs(\"div\", { style: sectionStyle, children: [status !== null && status.failoverCount > 0 && (_jsxs(\"div\", { style: noteStyle, children: [t('failoverCount'), \": \", status.failoverCount, status.failoverHistory.length > 0 && (_jsxs(_Fragment, { children: [' · ', t('lastFailover'), \": \", status.failoverHistory.map((h) => `${h.from} → ${h.to}`).join(' · ')] }))] })), backupShown !== null && (_jsxs(\"div\", { style: rowStyle, children: [_jsxs(\"span\", { style: { flexShrink: 0 }, children: [t('currentBackup'), \":\"] }), _jsx(Pill, { active: true, style: backupPillStyle, title: `${backupShown.provider}/${backupShown.model}`, children: _jsxs(\"span\", { style: ellipsisStyle, children: [backupShown.provider, \"/\", backupShown.model] }) }), _jsx(Button, { size: \"sm\", variant: \"ghost\", style: { flexShrink: 0 }, disabled: busy !== null, onClick: () => void clearBackup(), children: t('clearBackup') })] })), _jsxs(\"div\", { style: rowStyle, children: [(status?.providers ?? []).length > 0 ? (_jsx(\"select\", { value: provider, disabled: loading, onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 }, children: status?.providers.map((name) => (_jsx(\"option\", { value: name, children: name }, name))) })) : (_jsx(\"input\", { value: provider, placeholder: t('providerPlaceholder'), onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 } })), _jsx(Button, { size: \"sm\", variant: \"outline\", onClick: () => void loadCandidates(), disabled: provider === '' || loading, children: loading ? t('loading') : t('loadCandidates') })] }), error !== null && _jsx(\"div\", { style: { color: 'var(--dsw-text-danger, #e5484d)' }, children: error }), candidates !== null && (_jsxs(_Fragment, { children: [_jsx(\"div\", { style: rowStyle, children: _jsxs(\"span\", { children: [t('candidates'), \" (\", candidates.length, \")\"] }) }), candidates.length === 0 && _jsx(\"div\", { style: noteStyle, children: t('noCandidates') }), _jsx(\"div\", { style: listStyle, children: candidates.map((candidate) => (_jsx(CandidateRow, { candidate: candidate, isBackup: candidate.model === backupModel, busy: busy !== null, t: t, onSetBackup: handleSetBackup }, candidate.model))) })] }))] }) }));\n}\n//# sourceMappingURL=BackupPanel.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * DarumaStatus: a compact control at the right end of the composer tool row,\n * next to the model selector. Shows overall channel health and the backup\n * selection; clicking it opens the backup-channel panel.\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { Button, IconCheckOutline16, IconWarningOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { overallState } from \"./health.js\";\nimport { BackupPanel } from \"./BackupPanel.js\";\nconst controlStyle = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n maxWidth: 220,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\nconst iconColor = {\n done: 'var(--dsw-text-success, #30a46c)',\n warning: 'var(--dsw-text-warning, #f5a524)',\n error: 'var(--dsw-text-danger, #e5484d)',\n ongoing: 'var(--dsw-text-secondary, #888)',\n};\n/** Status glyph: a check when healthy, a warning otherwise. */\nfunction StatusIcon(props) {\n return (_jsx(\"span\", { style: { display: 'inline-flex', color: iconColor[props.state], flexShrink: 0 }, children: props.state === 'done'\n ? _jsx(IconCheckOutline16, { size: 14 })\n : _jsx(IconWarningOutline16, { size: 14 }) }));\n}\nexport function StatusDock(props) {\n const [status, setStatus] = useState(null);\n const [panelOpen, setPanelOpen] = useState(false);\n const load = useCallback(async () => {\n try {\n const value = await props.api.status();\n setStatus(value);\n }\n catch {\n // keep last good snapshot; the control stays quiet on RPC failure\n }\n }, [props.api]);\n useEffect(() => {\n void load();\n const timer = setInterval(() => void load(), 30_000);\n return () => clearInterval(timer);\n }, [load]);\n const t = props.t;\n const backupLabel = status?.backup !== null && status?.backup !== undefined\n ? `${status.backup.provider}/${status.backup.model}`\n : t('backupNone');\n return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: \"sm\", variant: \"ghost\", title: t('backupManageHint'), onClick: () => setPanelOpen(true), style: controlStyle, children: [_jsx(StatusIcon, { state: overallState(status?.channels ?? null, Date.now()) }), _jsxs(\"span\", { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: [t('backup'), \": \", backupLabel] })] }), panelOpen && (_jsx(BackupPanel, { api: props.api, t: t, status: status, onClose: () => {\n setPanelOpen(false);\n void load();\n } }))] }));\n}\n//# sourceMappingURL=StatusDock.js.map","/**\n * Pure failover-notice logic for the daruma web client.\n *\n * Bridges the server-appended `daruma/failover` session event into a visible\n * row inside the DSH conversation window. Host chat rendering is\n * definition-driven and out-of-repo event types are never rendered on their\n * own (the host fallback only claims append-surface events), so the client\n * registers its own conversation definition (kind {@link FAILOVER_NOTICE_KIND},\n * target `chat`) plus a keyed renderer for that kind.\n *\n * This module is framework- and DOM-free on purpose: the event contract and\n * the produced chat node are unit-testable without mounting a browser.\n */\n/** Session event type appended by the daruma server on every channel switch. */\nexport const FAILOVER_EVENT_TYPE = 'daruma/failover';\n/** Session event type appended when recovery gives up entirely. */\nexport const GIVE_UP_EVENT_TYPE = 'daruma/give-up';\n/** Chat node kind (conversation-definition kind + renderer key) used here. */\nexport const FAILOVER_NOTICE_KIND = 'daruma-failover';\n/** Display length caps mirroring the server-side event assembly. */\nconst CHANNEL_LIMIT = 80;\nconst REASON_LIMIT = 40;\nfunction isRecord(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n/** Trim a payload string, clipping over-long values with an ellipsis. */\nfunction clip(value, limit) {\n if (typeof value !== 'string')\n return undefined;\n const text = value.trim();\n if (text.length === 0)\n return undefined;\n return text.length <= limit ? text : `${text.slice(0, limit)}…`;\n}\nfunction finiteNumber(value) {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n/**\n * Defensively parse one `daruma/failover` (or `daruma/give-up`) payload into\n * display data. Returns `null` when the payload is not a usable record, so a\n * malformed event never renders (and is never claimed by this definition).\n * A `give-up` row needs only `from`; a `failover` row needs `from` and `to`.\n */\nexport function parseFailoverNotice(raw) {\n if (!isRecord(raw))\n return null;\n const kind = raw.kind === 'give-up' ? 'give-up' : 'failover';\n const from = clip(raw.from, CHANNEL_LIMIT);\n const to = clip(raw.to, CHANNEL_LIMIT);\n if (from === undefined)\n return null;\n if (kind === 'failover' && to === undefined)\n return null;\n const reason = clip(raw.reason, REASON_LIMIT) ?? 'FAILURE';\n const at = finiteNumber(raw.at);\n return {\n kind,\n from,\n to: to ?? '',\n reason,\n at: at === 0 ? Date.now() : at,\n turn: finiteNumber(raw.turn),\n step: finiteNumber(raw.step),\n count: finiteNumber(raw.failoverCount),\n budget: finiteNumber(raw.giveUpBudget),\n };\n}\n/** Replace `{key}` placeholders with matching values; unknown keys stay. */\nexport function fillTemplate(template, values) {\n return template.replace(/\\{(\\w+)\\}/g, (match, name) => name in values ? values[name] ?? '' : match);\n}\n/** Compose the row copy through the daruma locale dictionary. */\nexport function failoverNoticeCopy(data, t) {\n if (data.kind === 'give-up') {\n const line = fillTemplate(t('failoverGiveUpLine'), {\n from: data.from,\n reason: data.reason,\n });\n const detail = fillTemplate(t('failoverGiveUpDetail'), {\n count: String(data.count),\n budget: String(data.budget),\n turn: String(data.turn),\n step: String(data.step),\n });\n return { line, detail };\n }\n const line = fillTemplate(t('failoverNoticeLine'), {\n from: data.from,\n to: data.to,\n reason: data.reason,\n });\n const detail = fillTemplate(t('failoverNoticeDetail'), {\n count: String(data.count),\n budget: String(data.budget),\n turn: String(data.turn),\n step: String(data.step),\n });\n return { line, detail };\n}\n/**\n * The conversation Definition claiming `daruma/failover` and `daruma/give-up`\n * events for the chat target. One decision = one context (id = its event seq)\n * = one settled row, anchored at the event's own log position so it lands\n * inline in the flow.\n */\nexport function createFailoverNoticeDefinition() {\n const start = (_context, match) => {\n const parsed = parseFailoverNotice(match.event.data);\n if (parsed === null) {\n throw new Error(`daruma failover notice: unparseable ${FAILOVER_EVENT_TYPE} event at seq ${match.event.seq}`);\n }\n return parsed;\n };\n return {\n kind: FAILOVER_NOTICE_KIND,\n target: 'chat',\n match: (event) => {\n if (event.type !== FAILOVER_EVENT_TYPE && event.type !== GIVE_UP_EVENT_TYPE)\n return null;\n if (parseFailoverNotice(event.data) === null)\n return null;\n return { id: String(event.seq), role: 'start' };\n },\n start,\n // Every claimed event is a standalone start; keep update total for safety.\n update: (context, match) => start(context, match),\n buildViewNode: (context) => {\n const match = context.start;\n if (match === undefined)\n return null;\n const data = parseFailoverNotice(match.event.data);\n if (data === null)\n return null;\n return {\n key: context.key,\n kind: context.kind,\n id: context.id,\n target: 'chat',\n anchorSeq: match.event.seq,\n location: match.location ?? { kind: 'unresolved' },\n visibility: 'visible',\n data,\n };\n },\n };\n}\n//# sourceMappingURL=failover-notice.js.map","/**\n * FailoverNoticeRow — the daruma chat renderer for {@link FAILOVER_NOTICE_KIND}.\n *\n * A single quiet row inside the conversation flow telling the user that\n * daruma caught a failed model request and switched the channel for the\n * retry. Registered behind the host's keyed `conversation.chat.node` seat,\n * so it renders exactly like an in-repo chat row (no DOM injection).\n */\nimport { createElement as h } from 'react';\nimport { failoverNoticeCopy, parseFailoverNotice, } from \"./failover-notice.js\";\nconst rowStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: '6px',\n minWidth: 0,\n padding: '2px 4px',\n fontSize: '12px',\n lineHeight: '18px',\n color: 'var(--dsw-alias-label-secondary)',\n};\nconst dotStyle = {\n width: '6px',\n height: '6px',\n borderRadius: '50%',\n flex: 'none',\n background: 'var(--dsw-static-deepseek-500)',\n};\n/** Give-up rows use the error tint instead of the brand dot. */\nconst giveUpDotStyle = {\n ...dotStyle,\n background: 'var(--dsw-alias-status-error, #e5484d)',\n};\nconst brandStyle = {\n flex: 'none',\n fontWeight: 600,\n color: 'var(--dsw-alias-label-primary)',\n};\nconst lineStyle = {\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n minWidth: 0,\n};\n/** Render one settled failover / give-up row, or nothing when unusable. */\nexport function FailoverNoticeRow({ node, t }) {\n const data = parseFailoverNotice(node.data);\n if (data === null)\n return null;\n const { line, detail } = failoverNoticeCopy(data, t);\n const title = line === detail ? detail : `${line} · ${detail}`;\n const isGiveUp = data.kind === 'give-up';\n return h('div', {\n 'data-daruma-notice': isGiveUp ? 'give-up' : 'failover',\n role: 'note',\n style: rowStyle,\n title,\n }, [\n h('span', { 'aria-hidden': true, style: isGiveUp ? giveUpDotStyle : dotStyle }),\n h('span', { style: brandStyle }, 'daruma'),\n h('span', { style: lineStyle }, line),\n ]);\n}\n//# sourceMappingURL=FailoverNoticeRow.js.map","/**\n * dsh-daruma client: registers a compact channel-status dock above the\n * composer (conversation.input.dock) with a \"backup channel\" panel that\n * lists candidate models and picks a backup. Built by tsdown into the\n * __ModuleLoader__ factory bundle at lib/client.js.\n */\nimport { createElement as h } from 'react';\nimport { en, zh } from \"./locales.js\";\nimport { createApi } from \"./api.js\";\nimport { StatusDock } from \"./StatusDock.js\";\nimport { createFailoverNoticeDefinition, FAILOVER_NOTICE_KIND, } from \"./failover-notice.js\";\nimport { FailoverNoticeRow } from \"./FailoverNoticeRow.js\";\nconst NS = 'daruma';\n/**\n * Register the live failover notice: one conversation Definition claiming\n * `daruma/failover` events for the chat target, plus the keyed renderer for\n * its node kind. Absent or failing registrations degrade to nothing visible\n * (warn only) — the panel keeps working either way.\n */\nfunction registerFailoverChatNotice(ctx) {\n const registry = ctx.get('conversationEvents');\n if (registry === undefined) {\n console.warn('[dsh-daruma] conversation event registry unavailable — failover chat rows disabled');\n }\n else {\n try {\n ctx.effect(() => registry.register(createFailoverNoticeDefinition()), 'dsh-daruma: failover notice definition');\n }\n catch (error) {\n console.warn('[dsh-daruma] failover notice definition registration failed', error);\n }\n }\n ctx.slots.inject('conversation.chat.node', () => {\n try {\n // The keyed seat instantiates entries with props (node, t, …); the\n // structural slot type is narrower than the host's real registry.\n return ctx.slots.register({\n name: 'conversation.chat.node',\n key: FAILOVER_NOTICE_KIND,\n locale: NS,\n }, FailoverNoticeRow);\n }\n catch (error) {\n console.warn('[dsh-daruma] failover notice renderer registration failed', error);\n return () => { };\n }\n });\n}\nexport const name = 'dsh-daruma';\nexport const inject = ['slots', 'locale'];\nexport function apply(ctx) {\n // Widen the backup-panel dialog: the primitives Modal card is fixed at\n // min(380px, 100%), which truncates long provider/model names. A global\n // class (not a CSS module) outranks the module-scoped `.dialog` rule.\n if (typeof document !== 'undefined') {\n const tagId = 'daruma-panel-style';\n if (document.querySelector(`style[data-daruma=\"${tagId}\"]`) === null) {\n const style = document.createElement('style');\n style.dataset.daruma = tagId;\n style.textContent = '.daruma-wide-dialog.daruma-wide-dialog { width: min(560px, 100%); }';\n document.head.appendChild(style);\n }\n }\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-daruma: dictionaries');\n const t = ctx.locale.bind(NS);\n const connection = ctx.get('connection');\n if (connection === undefined) {\n console.warn('[dsh-daruma] web transport unavailable — client UI disabled');\n return;\n }\n const rpc = (endpoint, payload) => connection.rpc.call('/dsh-daruma', endpoint, payload ?? {});\n const api = createApi(rpc);\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'daruma-status',\n order: 0,\n inject: () => ({ api, t }),\n }, () => h(StatusDock, { api, t })));\n registerFailoverChatNotice(ctx);\n}\n//# sourceMappingURL=index.js.map"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["rowStyle","memo","_jsxs","_jsx","Pill","Button","useState","useMemo","useCallback","Modal","_Fragment","_jsx","IconCheckOutline16","IconWarningOutline16","useState","useCallback","_jsxs","_Fragment","Button","h","h"],"sources":["client/locales.js","client/api.js","client/adaptive-rpc.js","client/health.js","client/BackupPanel.js","client/StatusDock.js","client/failover-notice.js","client/FailoverNoticeRow.js","client/index.js"],"sourcesContent":["/** zh/en dictionaries for the daruma client UI. */\nexport const zh = {\n status: '渠道状态',\n current: '当前',\n backup: '备用',\n backupNone: '未设置',\n backupManage: '备用渠道',\n backupManageHint: '从候选列表选择备用渠道',\n failoverCount: '已切换',\n lastFailover: '上次切换',\n healthy: '健康',\n cooldown: '冷却中',\n probe: '探测中',\n refresh: '刷新',\n close: '关闭',\n clearBackup: '清除备用',\n provider: '提供方',\n providerPlaceholder: 'provider,如 mt',\n loadCandidates: '加载候选',\n loading: '加载中…',\n candidates: '候选模型',\n setBackup: '设为备用',\n currentBackup: '当前备用',\n currentBackupMark: '备用',\n noCandidates: '未加载到候选模型',\n empty: '暂无渠道数据',\n failoverNoticeLine: '{from} 请求失败({reason})→ 本次尝试 {to}',\n failoverNoticeDetail: '第 {count}/{budget} 次失败切换 · turn {turn} step {step}',\n failoverGiveUpLine: '{from} 请求失败({reason})→ 已用尽恢复手段,停止重试',\n failoverGiveUpDetail: '失败切换 {count}/{budget} · turn {turn} step {step}',\n};\nexport const en = {\n status: 'Channel status',\n current: 'Current',\n backup: 'Backup',\n backupNone: 'none',\n backupManage: 'Backup channel',\n backupManageHint: 'Pick a backup channel from the candidate list',\n failoverCount: 'failovers',\n lastFailover: 'last failover',\n healthy: 'healthy',\n cooldown: 'cooling',\n probe: 'probing',\n refresh: 'Refresh',\n close: 'Close',\n clearBackup: 'Clear backup',\n provider: 'Provider',\n providerPlaceholder: 'provider, e.g. mt',\n loadCandidates: 'Load candidates',\n loading: 'Loading…',\n candidates: 'Candidate models',\n setBackup: 'Set as backup',\n currentBackup: 'Current backup',\n currentBackupMark: 'backup',\n noCandidates: 'No candidates loaded',\n empty: 'No channel data yet',\n failoverNoticeLine: '{from} failed ({reason}) → trying {to}',\n failoverNoticeDetail: 'failover {count}/{budget} · turn {turn} step {step}',\n failoverGiveUpLine: '{from} failed ({reason}) → recovery budget exhausted, giving up',\n failoverGiveUpDetail: 'failovers {count}/{budget} · turn {turn} step {step}',\n};\n//# sourceMappingURL=locales.js.map","/**\n * Client-side wire types and RPC wrapper for the `/dsh-daruma` channel.\n * Structural types only — no monorepo-internal imports.\n */\nexport function createApi(rpc) {\n const call = async (endpoint, payload) => {\n const result = (await rpc(endpoint, payload));\n if (!result.ok) {\n throw new Error(result.error?.message ?? `daruma rpc ${endpoint} failed`);\n }\n return result.value;\n };\n return {\n status: () => call('status'),\n listCandidates: (provider) => call('listCandidates', { provider }),\n setBackup: async (provider, model) => {\n await call('setBackup', { provider, model });\n },\n clearBackup: async () => {\n await call('clearBackup');\n },\n };\n}\n//# sourceMappingURL=api.js.map","/**\n * Client transport for the daruma panel.\n *\n * Primary path: the legacy generic channel (`/dsh-daruma`) — served by\n * `rpc.handle` on hosts up to 0.1.2 and on 0.1.5 hosts patched by the\n * community fix.\n *\n * Fallback path: the `/api` shared carrier (`/api/dsh-daruma` exact route,\n * registered by the host-side fallback in `src/http-transport.ts`). The\n * browser caller switches on the first HTTP 404/405 — the route-miss shape of\n * the 0.1.5 `rpc.handle` regression — and sticks with the fallback for the\n * rest of the page lifetime, so the panel needs at most one failed request\n * per load. Auth failures (401/403) and handler errors surface as-is: they\n * say nothing about which transport should be used.\n *\n * Wire shape: exact Fetch routes match by URL path segment, and the shared\n * carrier validates that the envelope `method` equals that segment — so the\n * real endpoint and payload ride inside the payload tunnel, exactly like the\n * real-plane-verified dsh-im management RPC.\n */\n/** Generic channel served by `connection.rpc.handle` where it works. */\nexport const LEGACY_CHANNEL = '/dsh-daruma';\n/** Shared browser carrier the host itself mounts on every web-capable host. */\nexport const FALLBACK_CHANNEL = '/api';\n/** Envelope method demanded by the exact `/api/dsh-daruma` route. */\nexport const FALLBACK_ENDPOINT = 'dsh-daruma';\n/**\n * Whether a rejected legacy call means \"the channel route does not exist on\n * this host\" (switch to the fallback) rather than anything else. The browser\n * caller throws `transport failure for <channel>/<endpoint>: HTTP <status>`\n * when the response is not ok.\n */\nexport function isChannelRouteMiss(error) {\n return error instanceof Error && /HTTP (404|405)\\b/u.test(error.message);\n}\nfunction tunnelCall(connection, endpoint, payload) {\n return connection.rpc.call(FALLBACK_CHANNEL, FALLBACK_ENDPOINT, {\n method: endpoint,\n payload: payload ?? {},\n });\n}\n/** Build the panel `Rpc`: legacy channel first, sticky `/api` fallback. */\nexport function createAdaptiveRpc(connection) {\n let useFallback = false;\n return (endpoint, payload) => {\n if (useFallback) {\n return tunnelCall(connection, endpoint, payload);\n }\n return connection.rpc.call(LEGACY_CHANNEL, endpoint, payload ?? {}).catch((error) => {\n if (!isChannelRouteMiss(error))\n throw error;\n useFallback = true;\n return tunnelCall(connection, endpoint, payload);\n });\n };\n}\n//# sourceMappingURL=adaptive-rpc.js.map","/**\n * Time-aware channel-health classification for the status dock.\n *\n * The adapter now records successes (inferred from `agent/pre-step`), which\n * resets a channel to HEALTHY on its first successful request. But a channel\n * that never routes again (removed from the chain, one-shot backup) keeps its\n * last persisted state forever — including a stale COOLDOWN whose deadline\n * has long passed. The dock therefore still judges by time, not by the raw\n * state field: only a channel that is *still* cooling (cooldown not yet\n * expired) counts as an error.\n */\n/**\n * Overall dock state:\n * - `error` — at least one channel is still cooling (`cooldownUntilMs > now`).\n * - `warning` — a probe is in flight (kept for parity; the adapter no longer\n * enters PROBE, but persisted legacy states may surface it).\n * - `done` — everything routable: healthy, or cooled down long enough.\n * - `ongoing` — no snapshot yet.\n */\nexport function overallState(channels, nowMs) {\n if (channels === null)\n return 'ongoing';\n if (channels.some((c) => c.state === 'COOLDOWN' && c.cooldownUntilMs > nowMs))\n return 'error';\n if (channels.some((c) => c.state === 'PROBE'))\n return 'warning';\n return 'done';\n}\n//# sourceMappingURL=health.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * BackupChannelPanel: list candidate models for a provider and pick one as\n * the failover backup. Rendered inside a primitives Modal.\n *\n * No probing happens here anymore — the panel is a plain picker. Channel\n * health still updates from real traffic via the recovery engine.\n */\nimport { memo, useCallback, useEffect, useMemo, useState } from 'react';\nimport { Button, Modal, Pill, } from '@deepseek-ai/dsh-client-ui-primitives';\nfunction deriveProvider(status) {\n if (status === null)\n return '';\n if (status.current !== null)\n return status.current.split('::')[0] ?? '';\n if (status.backup !== null)\n return status.backup.provider;\n return status.providers[0] ?? '';\n}\nconst sectionStyle = {\n display: 'flex',\n flexDirection: 'column',\n gap: '8px',\n padding: '12px 0',\n fontSize: '13px',\n};\nconst rowStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n};\nconst noteStyle = { color: 'var(--dsw-text-tertiary, #999)', fontSize: '12px' };\n/** Clamp a long provider/model name so the row never wraps. */\nconst backupPillStyle = {\n flex: '1 1 auto',\n minWidth: 0,\n overflow: 'hidden',\n};\nconst ellipsisStyle = {\n minWidth: 0,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\nconst listStyle = {\n maxHeight: 300,\n overflowY: 'auto',\n display: 'flex',\n flexDirection: 'column',\n gap: 4,\n contain: 'content',\n willChange: 'scroll-position',\n overscrollBehavior: 'contain',\n};\n/** Skip rendering rows outside the viewport while scrolling. */\nconst candidateRowStyle = {\n ...rowStyle,\n contentVisibility: 'auto',\n containIntrinsicSize: '28px',\n};\n/** One candidate row, memoized so selection changes only re-render it. */\nconst CandidateRow = memo(function CandidateRow(props) {\n const { candidate, isBackup, busy, t, onSetBackup } = props;\n return (_jsxs(\"div\", { style: candidateRowStyle, children: [_jsx(\"span\", { style: { width: 14, flexShrink: 0 } }), _jsx(\"span\", { style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: candidate.model }), isBackup && _jsx(Pill, { active: true, children: t('currentBackupMark') }), _jsx(Button, { size: \"sm\", variant: \"ghost\", disabled: isBackup || busy, onClick: () => onSetBackup(candidate.model), children: t('setBackup') })] }));\n});\nexport function BackupPanel(props) {\n const { api, t, status, onClose } = props;\n const [provider, setProvider] = useState(deriveProvider(status));\n const [candidates, setCandidates] = useState(null);\n const [loading, setLoading] = useState(false);\n const [busy, setBusy] = useState(null);\n const [error, setError] = useState(null);\n // Local overlay on the status-prop backup: applied optimistically after\n // set/clear so the row reflects the change without waiting for the parent\n // to re-fetch status. Cleared whenever a fresh status arrives.\n const [backupOverride, setBackupOverride] = useState(undefined);\n // The effective backup shown in the panel: local override → status prop.\n const backupShown = backupOverride !== undefined ? backupOverride : status?.backup ?? null;\n // O(1) lookup for the current backup model; re-built only when the effective backup changes.\n const backupModel = useMemo(() => (backupShown !== null && backupShown.provider === provider ? backupShown.model : null), [backupShown, provider]);\n const loadCandidates = async () => {\n if (provider === '')\n return;\n setLoading(true);\n setError(null);\n try {\n setCandidates(await api.listCandidates(provider));\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setLoading(false);\n }\n };\n useEffect(() => {\n if (provider !== '')\n void loadCandidates();\n }, [provider]);\n // A fresh status from the parent supersedes the local override.\n useEffect(() => {\n setBackupOverride(undefined);\n }, [status]);\n const setBackup = async (model) => {\n setBusy(model);\n setError(null);\n try {\n await api.setBackup(provider, model);\n setBackupOverride({ provider, model });\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setBusy(null);\n }\n };\n const clearBackup = async () => {\n setBusy('__clear__');\n setError(null);\n try {\n await api.clearBackup();\n setBackupOverride(null);\n }\n catch (cause) {\n setError(cause instanceof Error ? cause.message : String(cause));\n }\n finally {\n setBusy(null);\n }\n };\n // Stable callback so memoized CandidateRow props don't change every render.\n const handleSetBackup = useCallback((model) => void setBackup(model), [provider]);\n return (_jsx(Modal, { open: true, onClose: onClose, title: t('backupManage'), className: \"daruma-wide-dialog\", closeLabel: t('close'), description: t('backupManageHint'), footer: _jsx(Button, { variant: \"primary\", onClick: onClose, children: t('close') }), children: _jsxs(\"div\", { style: sectionStyle, children: [status !== null && status.failoverCount > 0 && (_jsxs(\"div\", { style: noteStyle, children: [t('failoverCount'), \": \", status.failoverCount, status.failoverHistory.length > 0 && (_jsxs(_Fragment, { children: [' · ', t('lastFailover'), \": \", status.failoverHistory.map((h) => `${h.from} → ${h.to}`).join(' · ')] }))] })), backupShown !== null && (_jsxs(\"div\", { style: rowStyle, children: [_jsxs(\"span\", { style: { flexShrink: 0 }, children: [t('currentBackup'), \":\"] }), _jsx(Pill, { active: true, style: backupPillStyle, title: `${backupShown.provider}/${backupShown.model}`, children: _jsxs(\"span\", { style: ellipsisStyle, children: [backupShown.provider, \"/\", backupShown.model] }) }), _jsx(Button, { size: \"sm\", variant: \"ghost\", style: { flexShrink: 0 }, disabled: busy !== null, onClick: () => void clearBackup(), children: t('clearBackup') })] })), _jsxs(\"div\", { style: rowStyle, children: [(status?.providers ?? []).length > 0 ? (_jsx(\"select\", { value: provider, disabled: loading, onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 }, children: status?.providers.map((name) => (_jsx(\"option\", { value: name, children: name }, name))) })) : (_jsx(\"input\", { value: provider, placeholder: t('providerPlaceholder'), onChange: (event) => setProvider(event.currentTarget.value), style: { flex: 1, padding: '4px 8px', fontSize: 13 } })), _jsx(Button, { size: \"sm\", variant: \"outline\", onClick: () => void loadCandidates(), disabled: provider === '' || loading, children: loading ? t('loading') : t('loadCandidates') })] }), error !== null && _jsx(\"div\", { style: { color: 'var(--dsw-text-danger, #e5484d)' }, children: error }), candidates !== null && (_jsxs(_Fragment, { children: [_jsx(\"div\", { style: rowStyle, children: _jsxs(\"span\", { children: [t('candidates'), \" (\", candidates.length, \")\"] }) }), candidates.length === 0 && _jsx(\"div\", { style: noteStyle, children: t('noCandidates') }), _jsx(\"div\", { style: listStyle, children: candidates.map((candidate) => (_jsx(CandidateRow, { candidate: candidate, isBackup: candidate.model === backupModel, busy: busy !== null, t: t, onSetBackup: handleSetBackup }, candidate.model))) })] }))] }) }));\n}\n//# sourceMappingURL=BackupPanel.js.map","import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from \"react/jsx-runtime\";\n/**\n * DarumaStatus: a compact control at the right end of the composer tool row,\n * next to the model selector. Shows overall channel health and the backup\n * selection; clicking it opens the backup-channel panel.\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { Button, IconCheckOutline16, IconWarningOutline16 } from '@deepseek-ai/dsh-client-ui-primitives';\nimport { overallState } from \"./health.js\";\nimport { BackupPanel } from \"./BackupPanel.js\";\nconst controlStyle = {\n display: 'inline-flex',\n alignItems: 'center',\n gap: 6,\n maxWidth: 220,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n};\nconst iconColor = {\n done: 'var(--dsw-text-success, #30a46c)',\n warning: 'var(--dsw-text-warning, #f5a524)',\n error: 'var(--dsw-text-danger, #e5484d)',\n ongoing: 'var(--dsw-text-secondary, #888)',\n};\n/** Status glyph: a check when healthy, a warning otherwise. */\nfunction StatusIcon(props) {\n return (_jsx(\"span\", { style: { display: 'inline-flex', color: iconColor[props.state], flexShrink: 0 }, children: props.state === 'done'\n ? _jsx(IconCheckOutline16, { size: 14 })\n : _jsx(IconWarningOutline16, { size: 14 }) }));\n}\nexport function StatusDock(props) {\n const [status, setStatus] = useState(null);\n const [panelOpen, setPanelOpen] = useState(false);\n const load = useCallback(async () => {\n try {\n const value = await props.api.status();\n setStatus(value);\n }\n catch {\n // keep last good snapshot; the control stays quiet on RPC failure\n }\n }, [props.api]);\n useEffect(() => {\n void load();\n const timer = setInterval(() => void load(), 30_000);\n return () => clearInterval(timer);\n }, [load]);\n const t = props.t;\n const backupLabel = status?.backup !== null && status?.backup !== undefined\n ? `${status.backup.provider}/${status.backup.model}`\n : t('backupNone');\n return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: \"sm\", variant: \"ghost\", title: t('backupManageHint'), onClick: () => setPanelOpen(true), style: controlStyle, children: [_jsx(StatusIcon, { state: overallState(status?.channels ?? null, Date.now()) }), _jsxs(\"span\", { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: [t('backup'), \": \", backupLabel] })] }), panelOpen && (_jsx(BackupPanel, { api: props.api, t: t, status: status, onClose: () => {\n setPanelOpen(false);\n void load();\n } }))] }));\n}\n//# sourceMappingURL=StatusDock.js.map","/**\n * Pure failover-notice logic for the daruma web client.\n *\n * Bridges the server-appended `daruma/failover` session event into a visible\n * row inside the DSH conversation window. Host chat rendering is\n * definition-driven and out-of-repo event types are never rendered on their\n * own (the host fallback only claims append-surface events), so the client\n * registers its own conversation definition (kind {@link FAILOVER_NOTICE_KIND},\n * target `chat`) plus a keyed renderer for that kind.\n *\n * This module is framework- and DOM-free on purpose: the event contract and\n * the produced chat node are unit-testable without mounting a browser.\n */\n/** Session event type appended by the daruma server on every channel switch. */\nexport const FAILOVER_EVENT_TYPE = 'daruma/failover';\n/** Session event type appended when recovery gives up entirely. */\nexport const GIVE_UP_EVENT_TYPE = 'daruma/give-up';\n/** Chat node kind (conversation-definition kind + renderer key) used here. */\nexport const FAILOVER_NOTICE_KIND = 'daruma-failover';\n/** Display length caps mirroring the server-side event assembly. */\nconst CHANNEL_LIMIT = 80;\nconst REASON_LIMIT = 40;\nfunction isRecord(value) {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n/** Trim a payload string, clipping over-long values with an ellipsis. */\nfunction clip(value, limit) {\n if (typeof value !== 'string')\n return undefined;\n const text = value.trim();\n if (text.length === 0)\n return undefined;\n return text.length <= limit ? text : `${text.slice(0, limit)}…`;\n}\nfunction finiteNumber(value) {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n/**\n * Defensively parse one `daruma/failover` (or `daruma/give-up`) payload into\n * display data. Returns `null` when the payload is not a usable record, so a\n * malformed event never renders (and is never claimed by this definition).\n * A `give-up` row needs only `from`; a `failover` row needs `from` and `to`.\n */\nexport function parseFailoverNotice(raw) {\n if (!isRecord(raw))\n return null;\n const kind = raw.kind === 'give-up' ? 'give-up' : 'failover';\n const from = clip(raw.from, CHANNEL_LIMIT);\n const to = clip(raw.to, CHANNEL_LIMIT);\n if (from === undefined)\n return null;\n if (kind === 'failover' && to === undefined)\n return null;\n const reason = clip(raw.reason, REASON_LIMIT) ?? 'FAILURE';\n const at = finiteNumber(raw.at);\n return {\n kind,\n from,\n to: to ?? '',\n reason,\n at: at === 0 ? Date.now() : at,\n turn: finiteNumber(raw.turn),\n step: finiteNumber(raw.step),\n count: finiteNumber(raw.failoverCount),\n budget: finiteNumber(raw.giveUpBudget),\n };\n}\n/** Replace `{key}` placeholders with matching values; unknown keys stay. */\nexport function fillTemplate(template, values) {\n return template.replace(/\\{(\\w+)\\}/g, (match, name) => name in values ? values[name] ?? '' : match);\n}\n/** Compose the row copy through the daruma locale dictionary. */\nexport function failoverNoticeCopy(data, t) {\n if (data.kind === 'give-up') {\n const line = fillTemplate(t('failoverGiveUpLine'), {\n from: data.from,\n reason: data.reason,\n });\n const detail = fillTemplate(t('failoverGiveUpDetail'), {\n count: String(data.count),\n budget: String(data.budget),\n turn: String(data.turn),\n step: String(data.step),\n });\n return { line, detail };\n }\n const line = fillTemplate(t('failoverNoticeLine'), {\n from: data.from,\n to: data.to,\n reason: data.reason,\n });\n const detail = fillTemplate(t('failoverNoticeDetail'), {\n count: String(data.count),\n budget: String(data.budget),\n turn: String(data.turn),\n step: String(data.step),\n });\n return { line, detail };\n}\n/**\n * The conversation Definition claiming `daruma/failover` and `daruma/give-up`\n * events for the chat target. One decision = one context (id = its event seq)\n * = one settled row, anchored at the event's own log position so it lands\n * inline in the flow.\n */\nexport function createFailoverNoticeDefinition() {\n const start = (_context, match) => {\n const parsed = parseFailoverNotice(match.event.data);\n if (parsed === null) {\n throw new Error(`daruma failover notice: unparseable ${FAILOVER_EVENT_TYPE} event at seq ${match.event.seq}`);\n }\n return parsed;\n };\n return {\n kind: FAILOVER_NOTICE_KIND,\n target: 'chat',\n match: (event) => {\n if (event.type !== FAILOVER_EVENT_TYPE && event.type !== GIVE_UP_EVENT_TYPE)\n return null;\n if (parseFailoverNotice(event.data) === null)\n return null;\n return { id: String(event.seq), role: 'start' };\n },\n start,\n // Every claimed event is a standalone start; keep update total for safety.\n update: (context, match) => start(context, match),\n buildViewNode: (context) => {\n const match = context.start;\n if (match === undefined)\n return null;\n const data = parseFailoverNotice(match.event.data);\n if (data === null)\n return null;\n return {\n key: context.key,\n kind: context.kind,\n id: context.id,\n target: 'chat',\n anchorSeq: match.event.seq,\n location: match.location ?? { kind: 'unresolved' },\n visibility: 'visible',\n data,\n };\n },\n };\n}\n//# sourceMappingURL=failover-notice.js.map","/**\n * FailoverNoticeRow — the daruma chat renderer for {@link FAILOVER_NOTICE_KIND}.\n *\n * A single quiet row inside the conversation flow telling the user that\n * daruma caught a failed model request and switched the channel for the\n * retry. Registered behind the host's keyed `conversation.chat.node` seat,\n * so it renders exactly like an in-repo chat row (no DOM injection).\n */\nimport { createElement as h } from 'react';\nimport { failoverNoticeCopy, parseFailoverNotice, } from \"./failover-notice.js\";\nconst rowStyle = {\n display: 'flex',\n alignItems: 'center',\n gap: '6px',\n minWidth: 0,\n padding: '2px 4px',\n fontSize: '12px',\n lineHeight: '18px',\n color: 'var(--dsw-alias-label-secondary)',\n};\nconst dotStyle = {\n width: '6px',\n height: '6px',\n borderRadius: '50%',\n flex: 'none',\n background: 'var(--dsw-static-deepseek-500)',\n};\n/** Give-up rows use the error tint instead of the brand dot. */\nconst giveUpDotStyle = {\n ...dotStyle,\n background: 'var(--dsw-alias-status-error, #e5484d)',\n};\nconst brandStyle = {\n flex: 'none',\n fontWeight: 600,\n color: 'var(--dsw-alias-label-primary)',\n};\nconst lineStyle = {\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n minWidth: 0,\n};\n/** Render one settled failover / give-up row, or nothing when unusable. */\nexport function FailoverNoticeRow({ node, t }) {\n const data = parseFailoverNotice(node.data);\n if (data === null)\n return null;\n const { line, detail } = failoverNoticeCopy(data, t);\n const title = line === detail ? detail : `${line} · ${detail}`;\n const isGiveUp = data.kind === 'give-up';\n return h('div', {\n 'data-daruma-notice': isGiveUp ? 'give-up' : 'failover',\n role: 'note',\n style: rowStyle,\n title,\n }, [\n h('span', { 'aria-hidden': true, style: isGiveUp ? giveUpDotStyle : dotStyle }),\n h('span', { style: brandStyle }, 'daruma'),\n h('span', { style: lineStyle }, line),\n ]);\n}\n//# sourceMappingURL=FailoverNoticeRow.js.map","/**\n * dsh-daruma client: registers a compact channel-status dock above the\n * composer (conversation.input.dock) with a \"backup channel\" panel that\n * lists candidate models and picks a backup. Built by tsdown into the\n * __ModuleLoader__ factory bundle at lib/client.js.\n */\nimport { createElement as h } from 'react';\nimport { en, zh } from \"./locales.js\";\nimport { createApi } from \"./api.js\";\nimport { createAdaptiveRpc } from \"./adaptive-rpc.js\";\nimport { StatusDock } from \"./StatusDock.js\";\nimport { createFailoverNoticeDefinition, FAILOVER_NOTICE_KIND, } from \"./failover-notice.js\";\nimport { FailoverNoticeRow } from \"./FailoverNoticeRow.js\";\nconst NS = 'daruma';\n/**\n * Register the live failover notice: one conversation Definition claiming\n * `daruma/failover` events for the chat target, plus the keyed renderer for\n * its node kind. Absent or failing registrations degrade to nothing visible\n * (warn only) — the panel keeps working either way.\n */\nfunction registerFailoverChatNotice(ctx) {\n const registry = ctx.get('conversationEvents');\n if (registry === undefined) {\n console.warn('[dsh-daruma] conversation event registry unavailable — failover chat rows disabled');\n }\n else {\n try {\n ctx.effect(() => registry.register(createFailoverNoticeDefinition()), 'dsh-daruma: failover notice definition');\n }\n catch (error) {\n console.warn('[dsh-daruma] failover notice definition registration failed', error);\n }\n }\n ctx.slots.inject('conversation.chat.node', () => {\n try {\n // The keyed seat instantiates entries with props (node, t, …); the\n // structural slot type is narrower than the host's real registry.\n return ctx.slots.register({\n name: 'conversation.chat.node',\n key: FAILOVER_NOTICE_KIND,\n locale: NS,\n }, FailoverNoticeRow);\n }\n catch (error) {\n console.warn('[dsh-daruma] failover notice renderer registration failed', error);\n return () => { };\n }\n });\n}\nexport const name = 'dsh-daruma';\nexport const inject = ['slots', 'locale'];\nexport function apply(ctx) {\n // Widen the backup-panel dialog: the primitives Modal card is fixed at\n // min(380px, 100%), which truncates long provider/model names. A global\n // class (not a CSS module) outranks the module-scoped `.dialog` rule.\n if (typeof document !== 'undefined') {\n const tagId = 'daruma-panel-style';\n if (document.querySelector(`style[data-daruma=\"${tagId}\"]`) === null) {\n const style = document.createElement('style');\n style.dataset.daruma = tagId;\n style.textContent = '.daruma-wide-dialog.daruma-wide-dialog { width: min(560px, 100%); }';\n document.head.appendChild(style);\n }\n }\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-daruma: dictionaries');\n const t = ctx.locale.bind(NS);\n const connection = ctx.get('connection');\n if (connection === undefined) {\n console.warn('[dsh-daruma] web transport unavailable — client UI disabled');\n return;\n }\n const rpc = createAdaptiveRpc(connection);\n const api = createApi(rpc);\n ctx.slots.inject('conversation.input.right', () => ctx.slots.register({\n name: 'conversation.input.right',\n id: 'daruma-status',\n order: 0,\n inject: () => ({ api, t }),\n }, () => h(StatusDock, { api, t })));\n registerFailoverChatNotice(ctx);\n}\n//# sourceMappingURL=index.js.map"],"mappings":"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"}
package/lib/config.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface PluginConfig {
11
11
  readonly failureBudget?: number;
12
12
  readonly cooldownMs?: number;
13
13
  readonly giveUpBudget?: number;
14
+ readonly tripOnRetryExhausted?: boolean;
14
15
  readonly stateFile?: string;
15
16
  readonly logFile?: string;
16
17
  }
@@ -18,6 +19,15 @@ export interface ResolvedConfig extends RecoveryPolicyConfig {
18
19
  readonly stateFile: string;
19
20
  readonly logFile: string;
20
21
  }
21
- export declare function defaultStateFile(): string;
22
+ /**
23
+ * Resolve the harness home the way the host does (`dsh-home-paths`): an
24
+ * explicit `stateFile` wins, then `$DSH_HOME`, then `~/.dsh`. Honoring
25
+ * `DSH_HOME` keeps test and sandbox profiles isolated on every platform
26
+ * instead of writing into the user's real `~/.dsh`.
27
+ *
28
+ * @param env - environment mapping to read `$DSH_HOME` from.
29
+ * @returns the absolute default state-file path.
30
+ */
31
+ export declare function defaultStateFile(env?: NodeJS.ProcessEnv): string;
22
32
  export declare function defaultLogFile(stateFile: string): string;
23
33
  export declare function resolveConfig(raw?: PluginConfig): ResolvedConfig;
package/lib/config.js CHANGED
@@ -5,8 +5,19 @@ import { homedir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { modelId } from 'daruma-core';
7
7
  import { channelIdOf } from "./mapping.js";
8
- export function defaultStateFile() {
9
- return join(homedir(), '.dsh', 'daruma', 'channel-health.json');
8
+ /**
9
+ * Resolve the harness home the way the host does (`dsh-home-paths`): an
10
+ * explicit `stateFile` wins, then `$DSH_HOME`, then `~/.dsh`. Honoring
11
+ * `DSH_HOME` keeps test and sandbox profiles isolated on every platform
12
+ * instead of writing into the user's real `~/.dsh`.
13
+ *
14
+ * @param env - environment mapping to read `$DSH_HOME` from.
15
+ * @returns the absolute default state-file path.
16
+ */
17
+ export function defaultStateFile(env = process.env) {
18
+ const configured = env.DSH_HOME?.trim();
19
+ const home = configured !== undefined && configured !== '' ? configured : join(homedir(), '.dsh');
20
+ return join(home, 'daruma', 'channel-health.json');
10
21
  }
11
22
  export function defaultLogFile(stateFile) {
12
23
  return join(dirname(stateFile), 'failover-log.jsonl');
@@ -44,6 +55,9 @@ export function resolveConfig(raw = {}) {
44
55
  const failureBudget = positiveInteger(raw.failureBudget, 3, 'failureBudget');
45
56
  const cooldownMs = positiveInteger(raw.cooldownMs, 30_000, 'cooldownMs');
46
57
  const giveUpBudget = positiveInteger(raw.giveUpBudget, 8, 'giveUpBudget');
58
+ if (raw.tripOnRetryExhausted !== undefined && typeof raw.tripOnRetryExhausted !== 'boolean') {
59
+ throw new Error('invalid tripOnRetryExhausted: expected a boolean');
60
+ }
47
61
  if (raw.stateFile !== undefined && (typeof raw.stateFile !== 'string' || raw.stateFile.trim() === '')) {
48
62
  throw new Error('invalid stateFile: expected a non-empty string');
49
63
  }
@@ -56,6 +70,9 @@ export function resolveConfig(raw = {}) {
56
70
  failureBudget,
57
71
  cooldownMs,
58
72
  giveUpBudget,
73
+ // Default on: see `RecoveryPolicyConfig.tripOnRetryExhausted`. Only an
74
+ // explicit `false` restores counting a spent retry budget as one failure.
75
+ tripOnRetryExhausted: raw.tripOnRetryExhausted ?? true,
59
76
  stateFile,
60
77
  logFile: raw.logFile || defaultLogFile(stateFile),
61
78
  };
package/lib/config.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -25,6 +25,12 @@ export type FailoverLogRecord = {
25
25
  readonly to: string;
26
26
  /** Stable failure code, e.g. `RATE_LIMIT`. */
27
27
  readonly reason: string;
28
+ /**
29
+ * Present when the switch was escalated by an already-exhausted same-channel
30
+ * retry budget rather than by the failure counter. Absent means the ordinary
31
+ * path, so older lines stay readable.
32
+ */
33
+ readonly retryExhausted?: boolean;
28
34
  /** HTTP status of the failed request, when available. */
29
35
  readonly status?: number;
30
36
  /** Turn / step of the failed request. */