dsh-daruma 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -33,7 +33,11 @@ window.__ModuleLoader__.load({
33
33
  currentBackup: "当前备用",
34
34
  currentBackupMark: "备用",
35
35
  noCandidates: "未加载到候选模型",
36
- empty: "暂无渠道数据"
36
+ empty: "暂无渠道数据",
37
+ failoverNoticeLine: "{from} 请求失败({reason})→ 本次尝试 {to}",
38
+ failoverNoticeDetail: "第 {count}/{budget} 次失败切换 · turn {turn} step {step}",
39
+ failoverGiveUpLine: "{from} 请求失败({reason})→ 已用尽恢复手段,停止重试",
40
+ failoverGiveUpDetail: "失败切换 {count}/{budget} · turn {turn} step {step}"
37
41
  };
38
42
  const en = {
39
43
  status: "Channel status",
@@ -59,7 +63,11 @@ window.__ModuleLoader__.load({
59
63
  currentBackup: "Current backup",
60
64
  currentBackupMark: "backup",
61
65
  noCandidates: "No candidates loaded",
62
- empty: "No channel data yet"
66
+ empty: "No channel data yet",
67
+ failoverNoticeLine: "{from} failed ({reason}) → trying {to}",
68
+ failoverNoticeDetail: "failover {count}/{budget} · turn {turn} step {step}",
69
+ failoverGiveUpLine: "{from} failed ({reason}) → recovery budget exhausted, giving up",
70
+ failoverGiveUpDetail: "failovers {count}/{budget} · turn {turn} step {step}"
63
71
  };
64
72
  //#endregion
65
73
  //#region lib/client/api.js
@@ -92,11 +100,13 @@ window.__ModuleLoader__.load({
92
100
  /**
93
101
  * Time-aware channel-health classification for the status dock.
94
102
  *
95
- * The host exposes no request-success event, so a channel whose cooldown has
96
- * expired keeps its `COOLDOWN` state field forever — it becomes routable again
97
- * (`canRouteNow`) without ever flipping back to `HEALTHY`. The dock therefore
98
- * must judge by time, not by the raw state field: only a channel that is
99
- * *still* cooling (cooldown not yet expired) counts as an error.
103
+ * The adapter now records successes (inferred from `agent/pre-step`), which
104
+ * resets a channel to HEALTHY on its first successful request. But a channel
105
+ * that never routes again (removed from the chain, one-shot backup) keeps its
106
+ * last persisted state forever — including a stale COOLDOWN whose deadline
107
+ * has long passed. The dock therefore still judges by time, not by the raw
108
+ * state field: only a channel that is *still* cooling (cooldown not yet
109
+ * expired) counts as an error.
100
110
  */
101
111
  /**
102
112
  * Overall dock state:
@@ -134,7 +144,7 @@ window.__ModuleLoader__.load({
134
144
  padding: "12px 0",
135
145
  fontSize: "13px"
136
146
  };
137
- const rowStyle = {
147
+ const rowStyle$1 = {
138
148
  display: "flex",
139
149
  alignItems: "center",
140
150
  gap: "8px"
@@ -167,7 +177,7 @@ window.__ModuleLoader__.load({
167
177
  };
168
178
  /** Skip rendering rows outside the viewport while scrolling. */
169
179
  const candidateRowStyle = {
170
- ...rowStyle,
180
+ ...rowStyle$1,
171
181
  contentVisibility: "auto",
172
182
  containIntrinsicSize: "28px"
173
183
  };
@@ -291,7 +301,7 @@ window.__ModuleLoader__.load({
291
301
  ]
292
302
  }),
293
303
  backupShown !== null && (0, react_jsx_runtime.jsxs)("div", {
294
- style: rowStyle,
304
+ style: rowStyle$1,
295
305
  children: [
296
306
  (0, react_jsx_runtime.jsxs)("span", {
297
307
  style: { flexShrink: 0 },
@@ -321,7 +331,7 @@ window.__ModuleLoader__.load({
321
331
  ]
322
332
  }),
323
333
  (0, react_jsx_runtime.jsxs)("div", {
324
- style: rowStyle,
334
+ style: rowStyle$1,
325
335
  children: [(status?.providers ?? []).length > 0 ? (0, react_jsx_runtime.jsx)("select", {
326
336
  value: provider,
327
337
  disabled: loading,
@@ -358,7 +368,7 @@ window.__ModuleLoader__.load({
358
368
  }),
359
369
  candidates !== null && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
360
370
  (0, react_jsx_runtime.jsx)("div", {
361
- style: rowStyle,
371
+ style: rowStyle$1,
362
372
  children: (0, react_jsx_runtime.jsxs)("span", { children: [
363
373
  t("candidates"),
364
374
  " (",
@@ -464,6 +474,207 @@ window.__ModuleLoader__.load({
464
474
  })] });
465
475
  }
466
476
  //#endregion
477
+ //#region lib/client/failover-notice.js
478
+ /**
479
+ * Pure failover-notice logic for the daruma web client.
480
+ *
481
+ * Bridges the server-appended `daruma/failover` session event into a visible
482
+ * row inside the DSH conversation window. Host chat rendering is
483
+ * definition-driven and out-of-repo event types are never rendered on their
484
+ * own (the host fallback only claims append-surface events), so the client
485
+ * registers its own conversation definition (kind {@link FAILOVER_NOTICE_KIND},
486
+ * target `chat`) plus a keyed renderer for that kind.
487
+ *
488
+ * This module is framework- and DOM-free on purpose: the event contract and
489
+ * the produced chat node are unit-testable without mounting a browser.
490
+ */
491
+ /** Session event type appended by the daruma server on every channel switch. */
492
+ const FAILOVER_EVENT_TYPE = "daruma/failover";
493
+ /** Chat node kind (conversation-definition kind + renderer key) used here. */
494
+ const FAILOVER_NOTICE_KIND = "daruma-failover";
495
+ /** Display length caps mirroring the server-side event assembly. */
496
+ const CHANNEL_LIMIT = 80;
497
+ const REASON_LIMIT = 40;
498
+ function isRecord(value) {
499
+ return typeof value === "object" && value !== null && !Array.isArray(value);
500
+ }
501
+ /** Trim a payload string, clipping over-long values with an ellipsis. */
502
+ function clip(value, limit) {
503
+ if (typeof value !== "string") return void 0;
504
+ const text = value.trim();
505
+ if (text.length === 0) return void 0;
506
+ return text.length <= limit ? text : `${text.slice(0, limit)}…`;
507
+ }
508
+ function finiteNumber(value) {
509
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
510
+ }
511
+ /**
512
+ * Defensively parse one `daruma/failover` (or `daruma/give-up`) payload into
513
+ * display data. Returns `null` when the payload is not a usable record, so a
514
+ * malformed event never renders (and is never claimed by this definition).
515
+ * A `give-up` row needs only `from`; a `failover` row needs `from` and `to`.
516
+ */
517
+ function parseFailoverNotice(raw) {
518
+ if (!isRecord(raw)) return null;
519
+ const kind = raw.kind === "give-up" ? "give-up" : "failover";
520
+ const from = clip(raw.from, CHANNEL_LIMIT);
521
+ const to = clip(raw.to, CHANNEL_LIMIT);
522
+ if (from === void 0) return null;
523
+ if (kind === "failover" && to === void 0) return null;
524
+ const reason = clip(raw.reason, REASON_LIMIT) ?? "FAILURE";
525
+ const at = finiteNumber(raw.at);
526
+ return {
527
+ kind,
528
+ from,
529
+ to: to ?? "",
530
+ reason,
531
+ at: at === 0 ? Date.now() : at,
532
+ turn: finiteNumber(raw.turn),
533
+ step: finiteNumber(raw.step),
534
+ count: finiteNumber(raw.failoverCount),
535
+ budget: finiteNumber(raw.giveUpBudget)
536
+ };
537
+ }
538
+ /** Replace `{key}` placeholders with matching values; unknown keys stay. */
539
+ function fillTemplate(template, values) {
540
+ return template.replace(/\{(\w+)\}/g, (match, name) => name in values ? values[name] ?? "" : match);
541
+ }
542
+ /** Compose the row copy through the daruma locale dictionary. */
543
+ function failoverNoticeCopy(data, t) {
544
+ if (data.kind === "give-up") return {
545
+ line: fillTemplate(t("failoverGiveUpLine"), {
546
+ from: data.from,
547
+ reason: data.reason
548
+ }),
549
+ detail: fillTemplate(t("failoverGiveUpDetail"), {
550
+ count: String(data.count),
551
+ budget: String(data.budget),
552
+ turn: String(data.turn),
553
+ step: String(data.step)
554
+ })
555
+ };
556
+ return {
557
+ line: fillTemplate(t("failoverNoticeLine"), {
558
+ from: data.from,
559
+ to: data.to,
560
+ reason: data.reason
561
+ }),
562
+ detail: fillTemplate(t("failoverNoticeDetail"), {
563
+ count: String(data.count),
564
+ budget: String(data.budget),
565
+ turn: String(data.turn),
566
+ step: String(data.step)
567
+ })
568
+ };
569
+ }
570
+ /**
571
+ * The conversation Definition claiming `daruma/failover` and `daruma/give-up`
572
+ * events for the chat target. One decision = one context (id = its event seq)
573
+ * = one settled row, anchored at the event's own log position so it lands
574
+ * inline in the flow.
575
+ */
576
+ function createFailoverNoticeDefinition() {
577
+ const start = (_context, match) => {
578
+ const parsed = parseFailoverNotice(match.event.data);
579
+ if (parsed === null) throw new Error(`daruma failover notice: unparseable ${FAILOVER_EVENT_TYPE} event at seq ${match.event.seq}`);
580
+ return parsed;
581
+ };
582
+ return {
583
+ kind: FAILOVER_NOTICE_KIND,
584
+ target: "chat",
585
+ match: (event) => {
586
+ if (event.type !== "daruma/failover" && event.type !== "daruma/give-up") return null;
587
+ if (parseFailoverNotice(event.data) === null) return null;
588
+ return {
589
+ id: String(event.seq),
590
+ role: "start"
591
+ };
592
+ },
593
+ start,
594
+ update: (context, match) => start(context, match),
595
+ buildViewNode: (context) => {
596
+ const match = context.start;
597
+ if (match === void 0) return null;
598
+ const data = parseFailoverNotice(match.event.data);
599
+ if (data === null) return null;
600
+ return {
601
+ key: context.key,
602
+ kind: context.kind,
603
+ id: context.id,
604
+ target: "chat",
605
+ anchorSeq: match.event.seq,
606
+ location: match.location ?? { kind: "unresolved" },
607
+ visibility: "visible",
608
+ data
609
+ };
610
+ }
611
+ };
612
+ }
613
+ //#endregion
614
+ //#region lib/client/FailoverNoticeRow.js
615
+ /**
616
+ * FailoverNoticeRow — the daruma chat renderer for {@link FAILOVER_NOTICE_KIND}.
617
+ *
618
+ * A single quiet row inside the conversation flow telling the user that
619
+ * daruma caught a failed model request and switched the channel for the
620
+ * retry. Registered behind the host's keyed `conversation.chat.node` seat,
621
+ * so it renders exactly like an in-repo chat row (no DOM injection).
622
+ */
623
+ const rowStyle = {
624
+ display: "flex",
625
+ alignItems: "center",
626
+ gap: "6px",
627
+ minWidth: 0,
628
+ padding: "2px 4px",
629
+ fontSize: "12px",
630
+ lineHeight: "18px",
631
+ color: "var(--dsw-alias-label-secondary)"
632
+ };
633
+ const dotStyle = {
634
+ width: "6px",
635
+ height: "6px",
636
+ borderRadius: "50%",
637
+ flex: "none",
638
+ background: "var(--dsw-static-deepseek-500)"
639
+ };
640
+ /** Give-up rows use the error tint instead of the brand dot. */
641
+ const giveUpDotStyle = {
642
+ ...dotStyle,
643
+ background: "var(--dsw-alias-status-error, #e5484d)"
644
+ };
645
+ const brandStyle = {
646
+ flex: "none",
647
+ fontWeight: 600,
648
+ color: "var(--dsw-alias-label-primary)"
649
+ };
650
+ const lineStyle = {
651
+ overflow: "hidden",
652
+ textOverflow: "ellipsis",
653
+ whiteSpace: "nowrap",
654
+ minWidth: 0
655
+ };
656
+ /** Render one settled failover / give-up row, or nothing when unusable. */
657
+ function FailoverNoticeRow({ node, t }) {
658
+ const data = parseFailoverNotice(node.data);
659
+ if (data === null) return null;
660
+ const { line, detail } = failoverNoticeCopy(data, t);
661
+ const title = line === detail ? detail : `${line} · ${detail}`;
662
+ const isGiveUp = data.kind === "give-up";
663
+ return (0, react.createElement)("div", {
664
+ "data-daruma-notice": isGiveUp ? "give-up" : "failover",
665
+ role: "note",
666
+ style: rowStyle,
667
+ title
668
+ }, [
669
+ (0, react.createElement)("span", {
670
+ "aria-hidden": true,
671
+ style: isGiveUp ? giveUpDotStyle : dotStyle
672
+ }),
673
+ (0, react.createElement)("span", { style: brandStyle }, "daruma"),
674
+ (0, react.createElement)("span", { style: lineStyle }, line)
675
+ ]);
676
+ }
677
+ //#endregion
467
678
  //#region lib/client/index.js
468
679
  /**
469
680
  * dsh-daruma client: registers a compact channel-status dock above the
@@ -472,6 +683,33 @@ window.__ModuleLoader__.load({
472
683
  * __ModuleLoader__ factory bundle at lib/client.js.
473
684
  */
474
685
  const NS = "daruma";
686
+ /**
687
+ * Register the live failover notice: one conversation Definition claiming
688
+ * `daruma/failover` events for the chat target, plus the keyed renderer for
689
+ * its node kind. Absent or failing registrations degrade to nothing visible
690
+ * (warn only) — the panel keeps working either way.
691
+ */
692
+ function registerFailoverChatNotice(ctx) {
693
+ const registry = ctx.get("conversationEvents");
694
+ if (registry === void 0) console.warn("[dsh-daruma] conversation event registry unavailable — failover chat rows disabled");
695
+ else try {
696
+ ctx.effect(() => registry.register(createFailoverNoticeDefinition()), "dsh-daruma: failover notice definition");
697
+ } catch (error) {
698
+ console.warn("[dsh-daruma] failover notice definition registration failed", error);
699
+ }
700
+ ctx.slots.inject("conversation.chat.node", () => {
701
+ try {
702
+ return ctx.slots.register({
703
+ name: "conversation.chat.node",
704
+ key: FAILOVER_NOTICE_KIND,
705
+ locale: NS
706
+ }, FailoverNoticeRow);
707
+ } catch (error) {
708
+ console.warn("[dsh-daruma] failover notice renderer registration failed", error);
709
+ return () => {};
710
+ }
711
+ });
712
+ }
475
713
  const name = "dsh-daruma";
476
714
  const inject = ["slots", "locale"];
477
715
  function apply(ctx) {
@@ -508,6 +746,7 @@ window.__ModuleLoader__.load({
508
746
  api,
509
747
  t
510
748
  })));
749
+ registerFailoverChatNotice(ctx);
511
750
  }
512
751
  //#endregion
513
752
  exports.apply = apply;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["memo","_jsxs","_jsx","Pill","Button","useState","useMemo","useCallback","Modal","_Fragment","_jsx","IconCheckOutline16","IconWarningOutline16","useState","useCallback","_jsxs","_Fragment","Button","h"],"sources":["client/locales.js","client/api.js","client/health.js","client/BackupPanel.js","client/StatusDock.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};\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};\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 host exposes no request-success event, so a channel whose cooldown has\n * expired keeps its `COOLDOWN` state field forever — it becomes routable again\n * (`canRouteNow`) without ever flipping back to `HEALTHY`. The dock therefore\n * must judge by time, not by the raw state field: only a channel that is\n * *still* cooling (cooldown not yet 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 * 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\";\nconst NS = 'daruma';\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}\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/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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"}
package/lib/config.d.ts CHANGED
@@ -12,9 +12,12 @@ export interface PluginConfig {
12
12
  readonly cooldownMs?: number;
13
13
  readonly giveUpBudget?: number;
14
14
  readonly stateFile?: string;
15
+ readonly logFile?: string;
15
16
  }
16
17
  export interface ResolvedConfig extends RecoveryPolicyConfig {
17
18
  readonly stateFile: string;
19
+ readonly logFile: string;
18
20
  }
19
21
  export declare function defaultStateFile(): string;
22
+ export declare function defaultLogFile(stateFile: string): string;
20
23
  export declare function resolveConfig(raw?: PluginConfig): ResolvedConfig;
package/lib/config.js CHANGED
@@ -2,12 +2,15 @@
2
2
  * Plugin config parsing: cordis.patch.yml `config` → daruma-core config.
3
3
  */
4
4
  import { homedir } from 'node:os';
5
- import { join } from 'node:path';
5
+ import { dirname, join } from 'node:path';
6
6
  import { modelId } from 'daruma-core';
7
7
  import { channelIdOf } from "./mapping.js";
8
8
  export function defaultStateFile() {
9
9
  return join(homedir(), '.dsh', 'daruma', 'channel-health.json');
10
10
  }
11
+ export function defaultLogFile(stateFile) {
12
+ return join(dirname(stateFile), 'failover-log.jsonl');
13
+ }
11
14
  export function resolveConfig(raw = {}) {
12
15
  if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
13
16
  throw new Error('invalid config: expected an object');
@@ -44,12 +47,17 @@ export function resolveConfig(raw = {}) {
44
47
  if (raw.stateFile !== undefined && (typeof raw.stateFile !== 'string' || raw.stateFile.trim() === '')) {
45
48
  throw new Error('invalid stateFile: expected a non-empty string');
46
49
  }
50
+ if (raw.logFile !== undefined && (typeof raw.logFile !== 'string' || raw.logFile.trim() === '')) {
51
+ throw new Error('invalid logFile: expected a non-empty string');
52
+ }
53
+ const stateFile = raw.stateFile || defaultStateFile();
47
54
  return {
48
55
  channels,
49
56
  failureBudget,
50
57
  cooldownMs,
51
58
  giveUpBudget,
52
- stateFile: raw.stateFile || defaultStateFile(),
59
+ stateFile,
60
+ logFile: raw.logFile || defaultLogFile(stateFile),
53
61
  };
54
62
  }
55
63
  //# sourceMappingURL=config.js.map
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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"}
package/lib/engine.d.ts CHANGED
@@ -36,6 +36,18 @@ export declare class RecoveryEngine {
36
36
  onFailure(signal: FailureSignal, preferred?: Channel, scope?: string): RecoveryPlan;
37
37
  /** Release per-agent/session counters when the host disposes the scope. */
38
38
  clearScope(scope: string): void;
39
+ /**
40
+ * Record a successful request on `channel`: closes the circuit and resets
41
+ * the failure counter (HEALTHY / 0). The host exposes no request-success
42
+ * event, so the adapter infers success from the next `agent/pre-step`.
43
+ * Unknown channels (e.g. an out-of-chain backup that only ever failed)
44
+ * start from a fresh health so their stale counters also clear.
45
+ */
46
+ onSuccess(channel: ChannelId): void;
47
+ /** Scope's failover count so far (0 when never failed over). */
48
+ failoverCountFor(scope: string): number;
49
+ /** Per-scope give-up budget from config. */
50
+ get giveUpBudget(): number;
39
51
  /** Snapshot of every tracked channel's health, for the status UI. */
40
52
  listHealth(): ChannelHealthView[];
41
53
  get failoverCount(): number;
package/lib/engine.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * and persistence through a `ChannelHealthStore`. All decision logic stays in
7
7
  * daruma-core; this class only threads state and I/O.
8
8
  */
9
- import { decide, freshHealth, } from 'daruma-core';
9
+ import { decide, freshHealth, recordSuccess, } from 'daruma-core';
10
10
  const systemClock = { nowMs: () => Date.now() };
11
11
  const FAILOVER_HISTORY_LIMIT = 5;
12
12
  export class RecoveryEngine {
@@ -65,6 +65,27 @@ export class RecoveryEngine {
65
65
  clearScope(scope) {
66
66
  this.failoversByScope.delete(scope);
67
67
  }
68
+ /**
69
+ * Record a successful request on `channel`: closes the circuit and resets
70
+ * the failure counter (HEALTHY / 0). The host exposes no request-success
71
+ * event, so the adapter infers success from the next `agent/pre-step`.
72
+ * Unknown channels (e.g. an out-of-chain backup that only ever failed)
73
+ * start from a fresh health so their stale counters also clear.
74
+ */
75
+ onSuccess(channel) {
76
+ const current = this.healths.get(channel) ?? freshHealth(channel, this.clock.nowMs());
77
+ const next = recordSuccess(current, this.clock.nowMs());
78
+ this.healths.set(channel, next);
79
+ this.store.save(next);
80
+ }
81
+ /** Scope's failover count so far (0 when never failed over). */
82
+ failoverCountFor(scope) {
83
+ return this.failoversByScope.get(scope) ?? 0;
84
+ }
85
+ /** Per-scope give-up budget from config. */
86
+ get giveUpBudget() {
87
+ return this.config.giveUpBudget;
88
+ }
68
89
  /** Snapshot of every tracked channel's health, for the status UI. */
69
90
  listHealth() {
70
91
  return [...this.healths.values()].map((health) => ({
package/lib/engine.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}