@joekytc/dsh-swarm 0.3.12 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/client/ConfigSection.tsx +127 -18
  2. package/client/ConfigSelect.tsx +1 -0
  3. package/client/config-store.ts +6 -3
  4. package/client/config.css +89 -1
  5. package/client/index.ts +8 -4
  6. package/client/session-bridge.ts +26 -2
  7. package/cordis.patch.yml +10 -52
  8. package/lib/client.js +215 -37
  9. package/lib/config.d.ts +7 -0
  10. package/lib/config.js +7 -1
  11. package/lib/dispatcher/agent-runner.d.ts +4 -0
  12. package/lib/dispatcher/agent-runner.js +28 -17
  13. package/lib/dispatcher/chain-auditor.d.ts +3 -2
  14. package/lib/dispatcher/chain-auditor.js +2 -2
  15. package/lib/dispatcher/model-candidates.d.ts +9 -3
  16. package/lib/dispatcher/model-candidates.js +27 -6
  17. package/lib/dispatcher/model-chain.d.ts +19 -0
  18. package/lib/dispatcher/model-chain.js +110 -0
  19. package/lib/dispatcher/session-events.d.ts +9 -0
  20. package/lib/dispatcher/session-events.js +28 -0
  21. package/lib/dispatcher/v-orchestrator.d.ts +2 -0
  22. package/lib/dispatcher/v-orchestrator.js +14 -9
  23. package/lib/domain/config-override.d.ts +20 -0
  24. package/lib/domain/config-override.js +46 -2
  25. package/lib/index.js +13 -3
  26. package/lib/roles/preset-installer.d.ts +50 -5
  27. package/lib/roles/preset-installer.js +154 -20
  28. package/lib/routes/kanban-http.js +24 -3
  29. package/lib/services/config-provider.js +5 -0
  30. package/package.json +23 -17
  31. package/personas/kanban-d/agent.cordis.yml +3 -1
  32. package/personas/kanban-dt/agent.cordis.yml +2 -1
  33. package/personas/kanban-p/agent.cordis.yml +1 -1
  34. package/personas/kanban-pt/agent.cordis.yml +1 -1
  35. package/personas/kanban-v/agent.cordis.yml +1 -1
  36. package/personas/kanban-w/agent.cordis.yml +1 -1
  37. package/personas/swarm/agent.cordis.yml +1 -1
package/lib/client.js CHANGED
@@ -560,9 +560,27 @@ window.__ModuleLoader__.load({
560
560
  () => EMPTY
561
561
  );
562
562
  }
563
+ var navigator = null;
564
+ function setSessionNavigator(next) {
565
+ navigator = next;
566
+ }
563
567
  function openSession(id) {
568
+ if (navigator) {
569
+ let viaWorkspace = null;
570
+ try {
571
+ viaWorkspace = navigator();
572
+ } catch {
573
+ viaWorkspace = null;
574
+ }
575
+ if (viaWorkspace) {
576
+ viaWorkspace.openSession(id);
577
+ return;
578
+ }
579
+ }
564
580
  if (!service) throw new Error("sessions service unavailable");
565
- service.open(id);
581
+ const open = service.open;
582
+ if (!open) throw new Error("host exposes neither uiWorkspace nor sessions.open; session jump unavailable");
583
+ open(id);
566
584
  }
567
585
 
568
586
  // client/BoardCard.tsx
@@ -1625,7 +1643,7 @@ window.__ModuleLoader__.load({
1625
1643
  // client/config-store.ts
1626
1644
  var SWARM_CONFIG_NS = "swarm-config";
1627
1645
  function createConfigStore(fetchImpl) {
1628
- let state = { effective: { wikiVault: { baseUrl: "", pagePrefix: "" }, roles: { models: {} }, reviewEngine: { mode: "delegate", managed: { provider: "", model: "" } }, imDelivery: { fallbackBotId: "" } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: "idle", log: "" }, saving: false, error: null };
1646
+ let state = { effective: { wikiVault: { baseUrl: "", pagePrefix: "" }, roles: { models: {} }, chainFallback: { provider: "", model: "", reasoningEffort: "high" }, reviewEngine: { mode: "delegate", managed: { provider: "", model: "" } }, imDelivery: { fallbackBotId: "" } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: "idle", log: "" }, saving: false, error: null };
1629
1647
  const listeners = /* @__PURE__ */ new Set();
1630
1648
  const setState = (patch) => {
1631
1649
  state = { ...state, ...patch };
@@ -1745,6 +1763,7 @@ window.__ModuleLoader__.load({
1745
1763
  "aria-haspopup": "listbox",
1746
1764
  "aria-expanded": open,
1747
1765
  disabled,
1766
+ title: current?.label ?? placeholder,
1748
1767
  onClick: () => setOpen((v) => !v),
1749
1768
  children: [
1750
1769
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: current ? void 0 : "dsh-kb-config__select-placeholder", children: current?.label ?? placeholder }),
@@ -1797,6 +1816,15 @@ window.__ModuleLoader__.load({
1797
1816
  // client/ConfigSection.tsx
1798
1817
  var import_jsx_runtime9 = require("react/jsx-runtime");
1799
1818
  var ROLES = ["v", "p", "w", "d", "pt", "dt"];
1819
+ var MAX_FALLBACKS = 2;
1820
+ var ROLE_NAMES = {
1821
+ v: "\u770B\u677F\u7F16\u6392\u5B98\uFF08V\uFF09",
1822
+ p: "\u89C4\u5212\u5B98\uFF08P\uFF09",
1823
+ w: "\u77E5\u8BC6\u5B98\uFF08W\uFF09",
1824
+ d: "\u5168\u6808\u5F00\u53D1\u5B98\uFF08D\uFF09",
1825
+ pt: "\u8BA1\u5212\u5BA1\u67E5\u5B98\uFF08PT\uFF09",
1826
+ dt: "\u4EA4\u4ED8\u8BC4\u5BA1\u5B98\uFF08DT\uFF09"
1827
+ };
1800
1828
  function ocrBanner(ocr, installing) {
1801
1829
  if (installing) return null;
1802
1830
  if (ocr === null) return "ocr \u72B6\u6001\u672A\u77E5\u2014\u2014\u59D4\u6258/\u6258\u7BA1\u8BC4\u5BA1\u53EF\u80FD\u4E0D\u53EF\u7528";
@@ -1810,6 +1838,7 @@ window.__ModuleLoader__.load({
1810
1838
  const state = (0, import_react9.useSyncExternalStore)(store.subscribe, store.get);
1811
1839
  const [draft, setDraft] = (0, import_react9.useState)(null);
1812
1840
  const [wireResult, setWireResult] = (0, import_react9.useState)(null);
1841
+ const [openRole, setOpenRole] = (0, import_react9.useState)(void 0);
1813
1842
  (0, import_react9.useEffect)(() => {
1814
1843
  void store.load().then(() => setDraft(store.get().effective));
1815
1844
  void store.loadOcrStatus();
@@ -1829,9 +1858,33 @@ window.__ModuleLoader__.load({
1829
1858
  setDraft(next);
1830
1859
  void store.save(next);
1831
1860
  };
1861
+ const roleEntry = (r) => draft.roles.models[r] ?? { provider: "", model: "", reasoningEffort: "", fallbacks: [] };
1832
1862
  const setModel = (r, patch) => {
1833
- const prev = draft.roles.models[r] ?? { provider: "", model: "", reasoningEffort: "" };
1834
- commit({ ...draft, roles: { models: { ...draft.roles.models, [r]: { ...prev, ...patch } } } });
1863
+ commit({ ...draft, roles: { models: { ...draft.roles.models, [r]: { ...roleEntry(r), ...patch } } } });
1864
+ };
1865
+ const setFallback = (r, idx, patch) => {
1866
+ const fbs = [...roleEntry(r).fallbacks];
1867
+ const cur = fbs[idx] ?? { provider: "", model: "", reasoningEffort: "" };
1868
+ fbs[idx] = { ...cur, ...patch };
1869
+ setModel(r, { fallbacks: fbs });
1870
+ };
1871
+ const addFallback = (r) => {
1872
+ const fbs = roleEntry(r).fallbacks;
1873
+ if (fbs.length >= MAX_FALLBACKS) return;
1874
+ setModel(r, { fallbacks: [...fbs, { provider: "", model: "", reasoningEffort: "" }] });
1875
+ };
1876
+ const removeFallback = (r, idx) => {
1877
+ setModel(r, { fallbacks: roleEntry(r).fallbacks.filter((_, i) => i !== idx) });
1878
+ };
1879
+ const copyChainToAll = (r) => {
1880
+ const src = roleEntry(r);
1881
+ if (!(src.provider && src.model)) return;
1882
+ const models = { ...draft.roles.models };
1883
+ for (const o of ROLES) {
1884
+ if (o === r) continue;
1885
+ models[o] = { provider: src.provider, model: src.model, reasoningEffort: src.reasoningEffort, fallbacks: src.fallbacks.map((f) => ({ ...f })) };
1886
+ }
1887
+ commit({ ...draft, roles: { models } });
1835
1888
  };
1836
1889
  const commitReviewEngine = (mode, managed) => {
1837
1890
  commit({ ...draft, reviewEngine: { mode, managed } });
@@ -1839,11 +1892,26 @@ window.__ModuleLoader__.load({
1839
1892
  const onBlur = () => {
1840
1893
  void store.save(draft);
1841
1894
  };
1895
+ const chainField = (label, value, placeholder, options, onPick) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "dsh-kb-config__field", children: [
1896
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__field-label", children: label }),
1897
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ConfigSelect, { value, placeholder, options, onChange: onPick })
1898
+ ] });
1899
+ const chainSummary = (m) => {
1900
+ if (!(m.provider && m.model)) return "\u672A\u914D\u7F6E";
1901
+ const n = (m.fallbacks ?? []).filter((f) => f.provider && f.model).length;
1902
+ return `${m.provider} / ${m.model}` + (n ? ` \xB7 ${n} \u964D\u7EA7` : "");
1903
+ };
1904
+ const providerOptions = state.catalog.providers.map((p) => ({ value: p.id, label: p.name }));
1905
+ const expandedRole = openRole === void 0 ? ROLES.find((x) => {
1906
+ const e = draft.roles.models[x];
1907
+ return Boolean(e?.provider && e?.model);
1908
+ }) ?? null : openRole;
1842
1909
  const ocr = state.ocrStatus;
1843
1910
  const banner = ocrBanner(ocr, installing);
1844
1911
  const ocrBlocked = installing || ocr === null || !ocr.installed;
1845
1912
  const re = draft.reviewEngine;
1846
1913
  const managedFieldsEnabled = !ocrBlocked && re.mode === "managed";
1914
+ const cf = draft.chainFallback ?? { provider: "", model: "", reasoningEffort: "high" };
1847
1915
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config", children: [
1848
1916
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dsh-kb-config__card", children: [
1849
1917
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { children: "\u77E5\u8BC6\u5E93" }),
@@ -1871,47 +1939,155 @@ window.__ModuleLoader__.load({
1871
1939
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dsh-kb-config__card", children: [
1872
1940
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { children: "\u6A21\u578B\u94FE" }),
1873
1941
  ROLES.map((r) => {
1874
- const m = draft.roles.models[r] ?? { provider: "", model: "", reasoningEffort: "" };
1942
+ const m = roleEntry(r);
1943
+ const fbs = m.fallbacks ?? [];
1944
+ const configured = Boolean(m.provider && m.model);
1945
+ const open = expandedRole === r;
1875
1946
  const providerModels = state.catalog.models[m.provider] ?? [];
1876
1947
  const efforts = providerModels.find((x) => x.id === m.model)?.efforts ?? [];
1877
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__role", children: [
1878
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: r }),
1879
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1880
- ConfigSelect,
1948
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__role-block", children: [
1949
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1950
+ "button",
1881
1951
  {
1882
- value: m.provider,
1883
- placeholder: "\u9009\u62E9 provider",
1884
- options: state.catalog.providers.map((p) => ({ value: p.id, label: p.name })),
1885
- onChange: (v) => {
1886
- if (v !== m.provider) setModel(r, { provider: v, model: "", reasoningEffort: "" });
1887
- }
1952
+ type: "button",
1953
+ className: "dsh-kb-config__role-head",
1954
+ "aria-expanded": open,
1955
+ onClick: () => setOpenRole(open ? null : r),
1956
+ children: [
1957
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__chevron" + (open ? " dsh-kb-config__chevron--open" : ""), "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1958
+ "path",
1959
+ {
1960
+ d: "M6 3.5 10.5 8 6 12.5",
1961
+ fill: "none",
1962
+ stroke: "currentColor",
1963
+ strokeWidth: "1.5",
1964
+ strokeLinecap: "round",
1965
+ strokeLinejoin: "round"
1966
+ }
1967
+ ) }) }),
1968
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("strong", { children: ROLE_NAMES[r] }),
1969
+ !open && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__role-summary", children: chainSummary(m) })
1970
+ ]
1888
1971
  }
1889
1972
  ),
1890
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1891
- ConfigSelect,
1892
- {
1893
- value: m.model,
1894
- placeholder: "\u9009\u62E9\u6A21\u578B",
1895
- options: providerModels.map((x) => ({ value: x.id, label: x.name })),
1896
- onChange: (v) => {
1973
+ open && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__role-body", children: [
1974
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__group-label", children: "\u4E3B\u6A21\u578B" }),
1975
+ chainField(
1976
+ "\u4F9B\u5E94\u5546",
1977
+ m.provider,
1978
+ "\u9009\u62E9 provider",
1979
+ providerOptions,
1980
+ (v) => {
1981
+ if (v !== m.provider) setModel(r, { provider: v, model: "", reasoningEffort: "" });
1982
+ }
1983
+ ),
1984
+ chainField(
1985
+ "\u6A21\u578B",
1986
+ m.model,
1987
+ "\u9009\u62E9\u6A21\u578B",
1988
+ providerModels.map((x) => ({ value: x.id, label: x.name })),
1989
+ (v) => {
1897
1990
  if (v !== m.model) setModel(r, { model: v, reasoningEffort: "" });
1898
1991
  }
1899
- }
1900
- ),
1901
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1902
- ConfigSelect,
1903
- {
1904
- value: m.reasoningEffort,
1905
- placeholder: "\u8DDF\u968F\u9ED8\u8BA4",
1906
- options: efforts.map((x) => ({ value: x.id, label: x.name })),
1907
- onChange: (v) => {
1992
+ ),
1993
+ chainField(
1994
+ "\u601D\u8003\u7B49\u7EA7",
1995
+ m.reasoningEffort,
1996
+ "\u8DDF\u968F\u9ED8\u8BA4",
1997
+ efforts.map((x) => ({ value: x.id, label: x.name })),
1998
+ (v) => {
1908
1999
  if (v !== m.reasoningEffort) setModel(r, { reasoningEffort: v });
1909
2000
  }
1910
- }
1911
- ),
1912
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__src", children: state.sources[`roles.models.${r}.model`] === "override" ? "\u5DF2\u8986\u76D6" : "\u7EE7\u627F" })
2001
+ ),
2002
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__group-label", children: "\u964D\u7EA7\uFF08\u6700\u591A 2 \u4E2A\uFF0C\u987A\u5E8F = \u5207\u6362\u4F18\u5148\u5E8F\uFF09" }),
2003
+ fbs.map((f, i) => {
2004
+ const fProviderModels = state.catalog.models[f.provider] ?? [];
2005
+ const fEfforts = fProviderModels.find((x) => x.id === f.model)?.efforts ?? [];
2006
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__fb-group", children: [
2007
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__fb-head", children: [
2008
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "dsh-kb-config__fb-title", children: [
2009
+ "\u964D\u7EA7 ",
2010
+ i + 1
2011
+ ] }),
2012
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2013
+ "button",
2014
+ {
2015
+ type: "button",
2016
+ className: "dsh-kb-config__row-btn",
2017
+ title: "\u5220\u9664\u8BE5\u964D\u7EA7\u5019\u9009",
2018
+ onClick: () => removeFallback(r, i),
2019
+ children: "\u5220\u9664"
2020
+ }
2021
+ )
2022
+ ] }),
2023
+ chainField(
2024
+ "\u4F9B\u5E94\u5546",
2025
+ f.provider,
2026
+ "\u9009\u62E9 provider",
2027
+ providerOptions,
2028
+ (v) => {
2029
+ if (v !== f.provider) setFallback(r, i, { provider: v, model: "", reasoningEffort: "" });
2030
+ }
2031
+ ),
2032
+ chainField(
2033
+ "\u6A21\u578B",
2034
+ f.model,
2035
+ "\u9009\u62E9\u6A21\u578B",
2036
+ fProviderModels.map((x) => ({ value: x.id, label: x.name })),
2037
+ (v) => {
2038
+ if (v !== f.model) setFallback(r, i, { model: v, reasoningEffort: "" });
2039
+ }
2040
+ ),
2041
+ chainField(
2042
+ "\u601D\u8003\u7B49\u7EA7",
2043
+ f.reasoningEffort,
2044
+ "\u8DDF\u968F\u9ED8\u8BA4",
2045
+ fEfforts.map((x) => ({ value: x.id, label: x.name })),
2046
+ (v) => {
2047
+ if (v !== f.reasoningEffort) setFallback(r, i, { reasoningEffort: v });
2048
+ }
2049
+ )
2050
+ ] }, i);
2051
+ }),
2052
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__role-actions", children: [
2053
+ fbs.length < MAX_FALLBACKS && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2054
+ "button",
2055
+ {
2056
+ type: "button",
2057
+ className: "dsh-kb-config__row-btn",
2058
+ disabled: !configured,
2059
+ title: configured ? "\u8FFD\u52A0\u4E00\u4E2A\u964D\u7EA7\u5019\u9009" : "\u5148\u9009\u597D\u4E3B\u6A21\u578B\uFF08provider+\u6A21\u578B\uFF09\u518D\u8FFD\u52A0\u964D\u7EA7",
2060
+ onClick: () => addFallback(r),
2061
+ children: "+ \u964D\u7EA7"
2062
+ }
2063
+ ),
2064
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2065
+ "button",
2066
+ {
2067
+ type: "button",
2068
+ className: "dsh-kb-config__row-btn",
2069
+ disabled: !configured,
2070
+ title: "\u628A\u8BE5\u89D2\u8272\u7684\u4E3B\u6A21\u578B+\u964D\u7EA7\u94FE\u6574\u4F53\u590D\u5236\u5230\u5176\u4F59\u89D2\u8272",
2071
+ onClick: () => copyChainToAll(r),
2072
+ children: "\u590D\u5236\u672C\u94FE\u5230\u5168\u90E8\u89D2\u8272"
2073
+ }
2074
+ )
2075
+ ] })
2076
+ ] })
1913
2077
  ] }, r);
1914
- })
2078
+ }),
2079
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-kb-config__role dsh-kb-config__role--official", children: [
2080
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__badge", children: "\u5B98\u65B9\u515C\u5E95" }),
2081
+ cf.provider && cf.model ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { children: [
2082
+ cf.provider,
2083
+ " / ",
2084
+ cf.model,
2085
+ "\uFF08reasoningEffort=",
2086
+ cf.reasoningEffort,
2087
+ "\uFF09"
2088
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__muted", children: "\u672A\u914D\u7F6E\u2014\u2014\u89D2\u8272\u94FE\u5168\u7A7A\u65F6\u8D70\u5BBF\u4E3B\u9ED8\u8BA4\u6A21\u578B" })
2089
+ ] }),
2090
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-kb-config__help", children: "\u6D3E\u53D1\u987A\u5E8F\uFF1A\u4E3B\u6A21\u578B \u2192 \u964D\u7EA7\uFF08\u987A\u5E8F=\u4F18\u5148\u5E8F\uFF0C\u6700\u591A 2 \u4E2A\uFF09\u2192 \u5B98\u65B9\u515C\u5E95\uFF08\u6052\u57AB\u94FE\u5C3E\uFF09\u2192 \u5BBF\u4E3B\u9ED8\u8BA4\u6A21\u578B\uFF1B\u5019\u9009\u4E0D\u53EF\u7528\uFF08\u542B\u5B98\u65B9\u8D26\u53F7\u672A\u767B\u5F55\uFF09\u9759\u9ED8\u5207\u6362\u5E76\u7559 [model-fallback] \u8BC4\u8BBA" })
1915
2091
  ] }),
1916
2092
  /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dsh-kb-config__card", children: [
1917
2093
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { children: "\u8BC4\u5BA1\u5F15\u64CE\uFF08ocr\uFF09" }),
@@ -2893,13 +3069,14 @@ window.__ModuleLoader__.load({
2893
3069
  `;
2894
3070
 
2895
3071
  // client/config.css
2896
- var config_default = ".dsh-kb-config {\n /* \u4E0E kanban.css \u5927\u5706\u89D2\u5951\u7EA6\u5BF9\u9F50\uFF08DSH \u5361\u7247 16px\uFF09 */\n --dsh-kb-config-border: var(--dsw-alias-border-l2, #333);\n --dsh-kb-config-radius-lg: 16px;\n --dsh-kb-config-radius-md: 10px;\n --dsh-kb-config-input: var(--dsw-specific-input-major, rgba(127, 127, 127, 0.12));\n padding: 16px;\n font-size: 13px;\n}\n.dsh-kb-config__card { border: 1px solid var(--dsh-kb-config-border); border-radius: var(--dsh-kb-config-radius-lg); padding: 20px; margin-bottom: 16px; }\n.dsh-kb-config__card h3 { margin: 0 0 14px; }\n\n.dsh-kb-config input,\n.dsh-kb-config select {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: var(--dsh-kb-config-input);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n transition: border-color 150ms ease, background-color 150ms ease;\n}\n.dsh-kb-config input:focus,\n.dsh-kb-config select:focus {\n outline: none;\n /* \u7126\u70B9/\u9009\u4E2D\u5F3A\u8C03\u8272\u8DDF\u968F\u4E3B\u9898\u524D\u666F\uFF08\u6D45\u8272=\u6697\u3001\u6697\u8272=\u767D\uFF09\uFF0C\u4E0E dsh settings \u8272\u7CFB\u4E00\u81F4 */\n border-color: var(--dsw-alias-label-primary, currentColor);\n}\n\n/* DSH \u98CE\u683C\u4E0B\u62C9\uFF1Atrigger \u884C\uFF08\u503C + \u203A chevron\uFF09+ \u5F39\u51FA\u83DC\u5355\uFF08\u9009\u4E2D\u6253\u52FE\u3001hover \u9AD8\u4EAE\uFF09 */\n.dsh-kb-config__select { position: relative; min-width: 0; }\n.dsh-kb-config__select-trigger {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: var(--dsh-kb-config-input);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color 150ms ease, background-color 150ms ease;\n}\n.dsh-kb-config__select-trigger:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__select-trigger[aria-expanded='true'] { border-color: var(--dsw-alias-label-primary, currentColor); }\n.dsh-kb-config__select-placeholder { color: var(--dsw-alias-label-tertiary, #999); }\n.dsh-kb-config__select-trigger > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsh-kb-config__select-chevron { flex: 0 0 auto; display: inline-flex; color: var(--dsw-alias-label-tertiary, #999); }\n.dsh-kb-config__menu {\n position: absolute;\n z-index: 30;\n top: calc(100% + 6px);\n left: 0;\n right: 0;\n margin: 0;\n padding: 6px;\n list-style: none;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-lg);\n background: var(--dsw-alias-bg-layer-3, #222);\n color: var(--dsw-alias-label-primary, inherit);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);\n max-height: 260px;\n overflow: auto;\n}\n.dsh-kb-config__option {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 8px 10px;\n border: none;\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background-color 150ms ease;\n}\n.dsh-kb-config__option:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__option[data-selected='true'] { color: var(--dsw-alias-state-business-primary, #4b7bff); }\n.dsh-kb-config__option > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsh-kb-config__option-check { flex: 0 0 auto; display: inline-flex; }\n\n.dsh-kb-config__label { display: block; margin: 12px 0 6px; font-weight: 600; }\n.dsh-kb-config__help { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; display: block; margin: 6px 0 0; }\n.dsh-kb-config__role { display: grid; grid-template-columns: 32px 1fr 1fr 1fr auto; gap: 10px; margin-bottom: 12px; align-items: center; }\n.dsh-kb-config__src { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; white-space: nowrap; padding-left: 4px; }\n.dsh-kb-config__footer { display: flex; justify-content: flex-end; margin-top: 4px; }\n.dsh-kb-config__footer button {\n padding: 6px 16px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n transition: background-color 150ms ease;\n}\n.dsh-kb-config__footer button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__error { color: var(--dsw-alias-state-error-primary, #e5484d); }\n";
3072
+ var config_default = ".dsh-kb-config {\n /* \u4E0E kanban.css \u5927\u5706\u89D2\u5951\u7EA6\u5BF9\u9F50\uFF08DSH \u5361\u7247 16px\uFF09 */\n --dsh-kb-config-border: var(--dsw-alias-border-l2, #333);\n --dsh-kb-config-radius-lg: 16px;\n --dsh-kb-config-radius-md: 10px;\n --dsh-kb-config-input: var(--dsw-specific-input-major, rgba(127, 127, 127, 0.12));\n padding: 16px;\n font-size: 13px;\n}\n.dsh-kb-config__card { border: 1px solid var(--dsh-kb-config-border); border-radius: var(--dsh-kb-config-radius-lg); padding: 20px; margin-bottom: 16px; }\n.dsh-kb-config__card h3 { margin: 0 0 14px; }\n\n.dsh-kb-config input,\n.dsh-kb-config select {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: var(--dsh-kb-config-input);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n transition: border-color 150ms ease, background-color 150ms ease;\n}\n.dsh-kb-config input:focus,\n.dsh-kb-config select:focus {\n outline: none;\n /* \u7126\u70B9/\u9009\u4E2D\u5F3A\u8C03\u8272\u8DDF\u968F\u4E3B\u9898\u524D\u666F\uFF08\u6D45\u8272=\u6697\u3001\u6697\u8272=\u767D\uFF09\uFF0C\u4E0E dsh settings \u8272\u7CFB\u4E00\u81F4 */\n border-color: var(--dsw-alias-label-primary, currentColor);\n}\n\n/* DSH \u98CE\u683C\u4E0B\u62C9\uFF1Atrigger \u884C\uFF08\u503C + \u203A chevron\uFF09+ \u5F39\u51FA\u83DC\u5355\uFF08\u9009\u4E2D\u6253\u52FE\u3001hover \u9AD8\u4EAE\uFF09 */\n.dsh-kb-config__select { position: relative; min-width: 0; }\n.dsh-kb-config__select-trigger {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 8px 12px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: var(--dsh-kb-config-input);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color 150ms ease, background-color 150ms ease;\n}\n.dsh-kb-config__select-trigger:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__select-trigger[aria-expanded='true'] { border-color: var(--dsw-alias-label-primary, currentColor); }\n.dsh-kb-config__select-placeholder { color: var(--dsw-alias-label-tertiary, #999); }\n.dsh-kb-config__select-trigger > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsh-kb-config__select-chevron { flex: 0 0 auto; display: inline-flex; color: var(--dsw-alias-label-tertiary, #999); }\n.dsh-kb-config__menu {\n position: absolute;\n z-index: 30;\n top: calc(100% + 6px);\n left: 0;\n right: 0;\n margin: 0;\n padding: 6px;\n list-style: none;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-lg);\n background: var(--dsw-alias-bg-layer-3, #222);\n color: var(--dsw-alias-label-primary, inherit);\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.24);\n max-height: 260px;\n overflow: auto;\n}\n.dsh-kb-config__option {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n padding: 8px 10px;\n border: none;\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: background-color 150ms ease;\n}\n.dsh-kb-config__option:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__option[data-selected='true'] { color: var(--dsw-alias-state-business-primary, #4b7bff); }\n.dsh-kb-config__option > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: normal; word-break: break-word; }\n.dsh-kb-config__option-check { flex: 0 0 auto; display: inline-flex; }\n\n.dsh-kb-config__label { display: block; margin: 12px 0 6px; font-weight: 600; }\n.dsh-kb-config__help { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; display: block; margin: 6px 0 0; }\n.dsh-kb-config__role { display: grid; grid-template-columns: 32px 1fr 1fr 1fr auto; gap: 10px; margin-bottom: 12px; align-items: center; }\n.dsh-kb-config__src { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; white-space: nowrap; padding-left: 4px; }\n.dsh-kb-config__footer { display: flex; justify-content: flex-end; margin-top: 4px; }\n.dsh-kb-config__footer button {\n padding: 6px 16px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n cursor: pointer;\n transition: background-color 150ms ease;\n}\n.dsh-kb-config__footer button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__error { color: var(--dsw-alias-state-error-primary, #e5484d); }\n\n/* \u6A21\u578B\u94FE\u5361\uFF1A\u5BBF\u4E3B provider \u5361\u5C55\u5F00\u98CE\u2014\u2014\u89D2\u8272\u5934\u53EF\u6298\u53E0\u3001\u5B57\u6BB5 label \u5728\u4E0A + \u63A7\u4EF6\u5168\u5BBD\u3001\u964D\u7EA7\u4E3A\u6761\u76EE\u7EC4 */\n.dsh-kb-config__role-block { margin-bottom: 4px; }\n.dsh-kb-config__role-head {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n padding: 10px 10px;\n border: none;\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-kb-config__role-head:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__chevron { flex: 0 0 auto; display: inline-flex; color: var(--dsw-alias-label-tertiary, #999); transition: transform 150ms ease; }\n.dsh-kb-config__chevron--open { transform: rotate(90deg); }\n.dsh-kb-config__role-summary {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary, #888);\n font-size: 12px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n/* \u5C55\u5F00\u4F53\u80CC\u666F\u8272\uFF1A\u4E0E\u6298\u53E0\u89D2\u8272\u5934\u5F62\u6210\u5757\u7EA7\u533A\u5206\uFF08\u540C\u5BBF\u4E3B\u8BBE\u7F6E\u5185\u5D4C\u9762\u677F\u5C42\u6B21\uFF09 */\n.dsh-kb-config__role-body {\n padding: 12px 14px 16px;\n margin: 8px 0 4px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.08));\n border-radius: var(--dsh-kb-config-radius-md);\n}\n.dsh-kb-config__group-label { display: block; margin: 10px 0 8px; font-weight: 600; }\n.dsh-kb-config__group-label:first-child { margin-top: 0; }\n.dsh-kb-config__field { display: block; margin: 0 0 10px; }\n.dsh-kb-config__field-label { display: block; margin: 0 0 6px; color: var(--dsw-alias-label-secondary, #888); font-size: 12px; }\n.dsh-kb-config__fb-group {\n margin: 0 0 10px;\n padding: 10px 12px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n}\n.dsh-kb-config__fb-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin: 0 0 8px;\n}\n.dsh-kb-config__fb-title { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; }\n.dsh-kb-config__row-btn {\n padding: 6px 10px;\n border: 1px solid var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n background: transparent;\n color: inherit;\n font: inherit;\n font-size: 12px;\n white-space: nowrap;\n cursor: pointer;\n transition: background-color 150ms ease;\n}\n.dsh-kb-config__row-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }\n.dsh-kb-config__row-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n.dsh-kb-config__role-actions { display: flex; gap: 8px; margin-top: 12px; }\n.dsh-kb-config__role--official {\n display: flex;\n gap: 10px;\n align-items: center;\n margin: 12px 0 0;\n padding: 8px 10px;\n border: 1px dashed var(--dsh-kb-config-border);\n border-radius: var(--dsh-kb-config-radius-md);\n color: var(--dsw-alias-label-secondary, #888);\n font-size: 12px;\n}\n.dsh-kb-config__badge {\n flex: 0 0 auto;\n padding: 2px 8px;\n border-radius: 999px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12));\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 12px;\n white-space: nowrap;\n}\n.dsh-kb-config__muted { color: var(--dsw-alias-label-tertiary, #999); }\n";
2897
3073
 
2898
3074
  // client/index.ts
2899
3075
  var name = "kanban-board";
2900
- var inject = ["slots", "sessions"];
3076
+ var inject = ["slots", "sessions", "uiWorkspace"];
2901
3077
  function apply(ctx) {
2902
3078
  setSessionsService(ctx.sessions);
3079
+ setSessionNavigator(() => ctx.uiWorkspace ?? null);
2903
3080
  let style = null;
2904
3081
  let configStyle = null;
2905
3082
  if (typeof document !== "undefined") {
@@ -2936,6 +3113,7 @@ window.__ModuleLoader__.load({
2936
3113
  if (style) style.remove();
2937
3114
  if (configStyle) configStyle.remove();
2938
3115
  setSessionsService(null);
3116
+ setSessionNavigator(null);
2939
3117
  };
2940
3118
  }
2941
3119
 
package/lib/config.d.ts CHANGED
@@ -19,12 +19,19 @@ export interface KanbanConfig {
19
19
  provider: string;
20
20
  model: string;
21
21
  reasoningEffort?: string;
22
+ /** 降级候选(顺序=优先序,最多 2 个);仅 primary 配齐时生效。 */
22
23
  fallbacks?: Array<{
23
24
  provider: string;
24
25
  model: string;
25
26
  reasoningEffort?: string;
26
27
  }>;
27
28
  }>>;
29
+ /** 全局官方兜底(所有角色共用):GUI 未配角色链时单候选直用;有配置时垫主+降级链尾。 */
30
+ chainFallback: {
31
+ provider: string;
32
+ model: string;
33
+ reasoningEffort?: string;
34
+ };
28
35
  };
29
36
  dispatcher: {
30
37
  staleTimeoutSeconds: number;
package/lib/config.js CHANGED
@@ -13,7 +13,7 @@ const modelItemSchema = () => Schema.object({
13
13
  provider: Schema.string().required(),
14
14
  model: Schema.string().required(),
15
15
  reasoningEffort: Schema.string().default('high'),
16
- })).default([]),
16
+ })).max(2).default([]),
17
17
  });
18
18
  export const Config = Schema.object({
19
19
  storageDir: Schema.string().default('$DSH_HOME/storages/kanban'),
@@ -25,6 +25,12 @@ export const Config = Schema.object({
25
25
  // 角色系统提示词经 personas/kanban-{v,p,w,d}/agent.cordis.yml 组合装配(agentPresets.mount),
26
26
  // 随包安装到 $DSH_HOME/.agent-presets/(preset-installer),不再经 config 引用 md 文本。
27
27
  models: Schema.dict(modelItemSchema()).default({}),
28
+ // 官方兜底默认空(不启用);部署经 bundle patch 配值(如官方账号路由 deepseek-account/deepseek-flash)。
29
+ chainFallback: Schema.object({
30
+ provider: Schema.string().default(''),
31
+ model: Schema.string().default(''),
32
+ reasoningEffort: Schema.string().default('high'),
33
+ }).default({ provider: '', model: '', reasoningEffort: 'high' }),
28
34
  }),
29
35
  dispatcher: Schema.object({
30
36
  staleTimeoutSeconds: Schema.number().default(14400),
@@ -5,6 +5,10 @@ import type { Role } from '../domain/types.js';
5
5
  import type { WikiVaultClient } from '../wiki/wiki-vault-client.js';
6
6
  import { probeOcr } from '../services/ocr-cli.js';
7
7
  import type { AgentModelOptions } from './dispatcher.js';
8
+ /** 关键事件追加落盘 storageDir/dispatcher.log(与 dispatcher.ts 同一文件、同一行格式)——
9
+ * agent-runner 未注入 logFile,按 dispatcher 同款方式经 configProvider 派生 storageDir;
10
+ * 写失败静默忽略(日志只是观测面,事件日志才是事实源)。 */
11
+ export declare function logToDispatcherLog(configProvider: ConfigProvider, msg: string): void;
8
12
  /** 角色组合标记(会话10事故根因A):setup 成功组合角色工具面后写入,live 复用前校验。 */
9
13
  interface RoleCompositionMarker {
10
14
  role: Role;
@@ -1,5 +1,6 @@
1
1
  import { SessionId } from '@deepseek-ai/dsh-session';
2
2
  import { writeFileSync } from 'node:fs';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import { join } from 'node:path';
4
5
  import { probeOcr } from '../services/ocr-cli.js';
5
6
  import { installRoleTools, buildReadOnlyWriteGuard, buildDTWriteGuard, buildPlanWriteGuard, buildKbWriteGuard, registerDtTaskChain, unregisterDtTaskChain } from '../roles/toolsets.js';
@@ -7,6 +8,7 @@ import { ensureLocalKbRoot } from '../wiki/local-kb.js';
7
8
  import { installCleanFsTools } from '../roles/clean-fs-tools.js';
8
9
  import { mountOrRecompose } from '../roles/preset-installer.js';
9
10
  import { buildModelCandidates, isModelUnavailableError } from './model-candidates.js';
11
+ import { registerModelChain } from './model-chain.js';
10
12
  import { toolName, isGuardSynthesizedReply, lastTurnEnd } from './session-events.js';
11
13
  import { filterCandidatesByCatalog } from '../services/llm-catalog.js';
12
14
  import { attachSessionToWorkspace, resolveOrCreateWorkspace } from './workspace-attach.js';
@@ -30,14 +32,14 @@ function isInfraError(err) {
30
32
  function classifyTurnError(code, message) {
31
33
  if (code === 'UNKNOWN_MODEL' || code === 'UNSUPPORTED_REASONING_EFFORT' || code === 'INVALID_REQUEST')
32
34
  return 'config';
33
- if (code === 'QUOTA' || /quota|rate.?limit|\b429\b/i.test(message) || isInfraError(message))
35
+ if ((code ?? '').includes('QUOTA') || /quota|rate.?limit|\b429\b|insufficient balance/i.test(message) || isInfraError(message))
34
36
  return 'transient';
35
37
  return 'unknown';
36
38
  }
37
39
  /** 关键事件追加落盘 storageDir/dispatcher.log(与 dispatcher.ts 同一文件、同一行格式)——
38
40
  * agent-runner 未注入 logFile,按 dispatcher 同款方式经 configProvider 派生 storageDir;
39
41
  * 写失败静默忽略(日志只是观测面,事件日志才是事实源)。 */
40
- function logToDispatcherLog(configProvider, msg) {
42
+ export function logToDispatcherLog(configProvider, msg) {
41
43
  try {
42
44
  const storageDir = configProvider.getEffective().storageDir.replace('$DSH_HOME', process.env.DSH_HOME ?? process.cwd());
43
45
  writeFileSync(join(storageDir, 'dispatcher.log'), new Date().toISOString() + ' ' + msg + '\n', { flag: 'a' });
@@ -266,7 +268,7 @@ ${task.body}`);
266
268
  (task.attempts > 0 || state.events.some((e) => e.taskId === taskId && e.kind === 'task/claimed'));
267
269
  let agent;
268
270
  let context = '';
269
- const setup = async (agentCtx) => {
271
+ const setup = async (agentCtx, agent) => {
270
272
  // 思考等级强制(waterfall):宿主 selection 无 create-options 覆盖层(dsh-host-apiproxy 的
271
273
  // selectionFor 不消费 agentOptions),AgentOptions 也仅有 provider/model/maxTokens——agentOptions
272
274
  // 里的 reasoningEffort 不被宿主消费,新建角色会话思考等级会落回宿主默认。改走 DSH agent/request
@@ -283,7 +285,8 @@ ${task.body}`);
283
285
  // P/W/D = full access(跨目录读:P 读仓库/外部实证、W 读计划、D 执行);
284
286
  // 但 P 挂 plan 写护栏、W 挂只读护栏(写边界由工具级强制,防"改动源码",不靠 prompt 软约束);
285
287
  // PT/DT/V → workspace-write(评审/编排最小权限,PT/DT 只读护栏由 ToolGuard 独立保证)。
286
- const session = agentCtx.agent?.session;
288
+ // approval/sandbox 种子事件经 AgentSetup 第二参的 agent 显式直写(0.2.0 起 ctx.agent 挂载已移除)。
289
+ const session = agent?.session;
287
290
  session?.append?.('approval/policy', { policy: 'never', source: 'delegation' });
288
291
  const fullAccess = isDExecute || task.assignee === 'p' || task.assignee === 'w';
289
292
  session?.append?.('sandbox/mode', fullAccess ? { mode: 'danger-full-access', source: 'delegation' } : { mode: 'workspace-write', source: 'delegation' });
@@ -318,16 +321,13 @@ ${task.body}`);
318
321
  // PT/DT 是 workspace-write(sandbox_permissions 扩权到天花板是合法功能,剥了破坏官方能力);
319
322
  // W 虽 fullAccess 但只读护栏全拒 write,shadow 无意义(最小挂载面,V 无执行工具不涉及)。
320
323
  if (task.assignee === 'p' || isDExecute) {
321
- installCleanFsTools(agentCtx, agentCtx.agent);
324
+ installCleanFsTools(agentCtx, agent);
322
325
  }
323
326
  // 组合标记——角色工具面成功组合后,把 { role, taskId } 记到进程内 WeakMap(键=Agent 实例)。
324
- // 宿主探明:setup 收到的 agentCtx.agent 与发布后 agents.get(id) 返回的是同一 Agent 实例
325
- // (dsh-agent types/index.d.ts:38 `agent?: Agent` 安装为 Agent.ctx own property),
326
- // 故此处键入的实例即 resumeOrReuse 里 agents.get 命中的实例。agentCtx.agent 缺失(宿主变体)时
327
- // 不写标记 → live 复用走工具面校验路径(kanban_complete 可见性),能力无损。
328
- const liveAgent = agentCtx.agent;
329
- if (liveAgent)
330
- markRoleComposition(liveAgent, { role: task.assignee, taskId: task.id });
327
+ // AgentSetup 第二参收到的 Agent 与发布后 agents.get(id) 返回的是同一实例,故此处键入的实例
328
+ // 即 resumeOrReuse 里 agents.get 命中的实例(live 复用走标记匹配,能力无损)。
329
+ if (agent)
330
+ markRoleComposition(agent, { role: task.assignee, taskId: task.id });
331
331
  // 只读评审角色(PT/DT)注册 ToolGuard:拦截 tracked source 写入 / git mutation / 含写标记 bash。
332
332
  // 以 dsh-tools 类型为准:tools.guard(execution => reason|undefined),execution.name/arguments 为实际字段。
333
333
  if (task.assignee === 'pt' || task.assignee === 'dt') {
@@ -396,7 +396,7 @@ ${task.body}`);
396
396
  const builtCandidates = buildModelCandidates(this.configProvider.getEffective(), task.assignee, this.defaultModel);
397
397
  // 派发前预校验(LLM 目录):不存在的 provider/model 组合、模型未声明的 reasoningEffort
398
398
  // 直接剔除,不再打进网络白烧会话(UNKNOWN_MODEL / UNSUPPORTED_REASONING_EFFORT 秒退实证形态——
399
- // 候选 fallback 只护 create/resume 抛错,turn 期错误不触发切换,必须前置拦截)。
399
+ // 必须前置拦截)。
400
400
  // fail-open:llm 服务未就绪/目录探测失败 → 全放行,不阻断派发。
401
401
  // 全部候选被拒 → block(model-unavailable) 终态(与 spawn 全候选失败同语义,等人工修配置)。
402
402
  let candidates = builtCandidates;
@@ -422,6 +422,7 @@ ${task.body}`);
422
422
  }
423
423
  candidates = precheck.ok;
424
424
  }
425
+ let chainStartIndex = 0;
425
426
  if (candidates.length === 0) {
426
427
  // 无任何候选配置:不传 agentOptions(用部署默认),单次尝试
427
428
  agent = hasRunHistory
@@ -435,7 +436,7 @@ ${task.body}`);
435
436
  else {
436
437
  const agents = this.ctx.get('agents');
437
438
  let spawnError = null;
438
- for (const candidate of candidates) {
439
+ for (const [candidateIndex, candidate] of candidates.entries()) {
439
440
  try {
440
441
  // hasRunHistory → resumeOrReuse 直接返回 AgentLike(内部已解包 .agent);create 返回 { agent } 需解包。
441
442
  // 统一归一化为 AgentLike,避免二次解包(h.agent=undefined → if(!agent) 误标 failed)。
@@ -443,6 +444,7 @@ ${task.body}`);
443
444
  ? await this.resumeOrReuse(agents, task.resumeSessionId ?? `kbn-${taskId}`, { agentOptions: candidate, setup, role: task.assignee, taskId: task.id })
444
445
  : (await agents.create({ sessionId: SessionId(`kbn-${taskId}`), meta: { cwd: sessionCwd }, agentOptions: candidate, setup })).agent;
445
446
  agent = h;
447
+ chainStartIndex = candidateIndex;
446
448
  // 切换成功且非首选 → 发可审计 model/fallback 评论(记录证据,不弹用户)
447
449
  if (candidate !== candidates[0]) {
448
450
  try {
@@ -468,6 +470,14 @@ ${task.body}`);
468
470
  throw spawnError;
469
471
  }
470
472
  }
473
+ if (agent && candidates.length > 0) {
474
+ registerModelChain(agent, {
475
+ candidates,
476
+ index: chainStartIndex,
477
+ taskId,
478
+ comment: async (body) => { await this.kanban.comment(taskId, body, 'system'); },
479
+ });
480
+ }
471
481
  }
472
482
  catch (err) {
473
483
  // claim/buildContext/spawn 失败也走 failed(attempts 递增)→ 调度器重派/看门狗熔断;不再让任务永久 claimed
@@ -492,7 +502,7 @@ ${task.body}`);
492
502
  const attachId = task.resumeSessionId ?? `kbn-${task.id}`;
493
503
  await attachSessionToWorkspace(this.ctx, attachId, sessionCwd, 'task ' + task.id + ' ' + task.assignee + '/' + task.mode);
494
504
  try {
495
- agent.followup({ content: [{ type: 'text', text: context }], source: { kind: 'user' } });
505
+ agent.followup({ id: randomUUID(), content: [{ type: 'text', text: context }], source: { kind: 'user' } });
496
506
  console.error('[dsh-swarm][debug] runner followup sent ' + taskId);
497
507
  await agent.whenIdle();
498
508
  console.error('[dsh-swarm][debug] runner whenIdle resolved ' + taskId);
@@ -510,7 +520,7 @@ ${task.body}`);
510
520
  : (sessionLike.events ?? []);
511
521
  if (turnEvents.filter((e) => (Number(e?.seq) || 0) > eventsBase).some(isGuardSynthesizedReply)) {
512
522
  console.error('[dsh-swarm][debug] runner guard-synthesized reply detected (from-cache/from-security-guard) ' + taskId + ' — auto followup retry once');
513
- agent.followup({ content: [{ type: 'text', text: '继续未完成的任务:上一条回复是网关安全护栏/语义缓存误拦的合成拒答,并非你的真实结论。请忽略它,从中断处继续执行任务,直至按协议收敛终态(kanban_complete 或 kanban_block)。' }], source: { kind: 'user' } });
523
+ agent.followup({ id: randomUUID(), content: [{ type: 'text', text: '继续未完成的任务:上一条回复是网关安全护栏/语义缓存误拦的合成拒答,并非你的真实结论。请忽略它,从中断处继续执行任务,直至按协议收敛终态(kanban_complete 或 kanban_block)。' }], source: { kind: 'user' } });
514
524
  console.error('[dsh-swarm][debug] runner guard-retry followup sent ' + taskId);
515
525
  await agent.whenIdle();
516
526
  console.error('[dsh-swarm][debug] runner guard-retry whenIdle resolved ' + taskId);
@@ -650,7 +660,8 @@ ${task.body}`);
650
660
  // GUI 默认组合 incarnation:缺 kanban_complete → 重跑 setup 幂等补挂后复用。
651
661
  // setup 对 live ctx 的各步均可加:effort waterfall/approval+sandbox append(known 事件类型、
652
662
  // latest-wins)/preset mount(bindings 覆盖)/角色工具(此前为默认组合,无同名冲突)/护栏。
653
- await opts.setup(liveCtx);
663
+ // 双参签名(0.2.0):repair 重跑 setup 时显式传 live agent 实例(标记随修复自愈)。
664
+ await opts.setup(liveCtx, live);
654
665
  if (toolsSvc.get('kanban_complete', live))
655
666
  return live; // 修复后必须验证到位,防静默半修复
656
667
  throw new Error('live session composition repair failed: kanban_complete still missing after re-setup (session ' + sessionId + ')');
@@ -11,8 +11,9 @@ import type { AuditEvidence, Task } from '../domain/types.js';
11
11
  * a. 作用域收窄:仅扫描会话工作区(session.header.cwd)位于本链发起工作区
12
12
  * (Chain.workspaceDir = /plan: 主 agent 所在工作空间)内的会话,
13
13
  * 排除其他项目的主会话(如 评估 项目里调试 dsh-swarm 的会话);
14
- * b. 行为判定:run_code 按实际派发子调用(tool/code-dispatch-start / tool/code-dispatch,
15
- * 经 rootCallId 关联外层调用)判定是否真的发生写,而非把 run_code 一律视为写;
14
+ * b. 行为判定:run_code 按实际派发子调用(新旧宿主双名 tool/code-dispatch[-start] /
15
+ * tool/ptc-dispatch[-start],经 rootCallId 关联外层调用)判定是否真的发生写,而非把
16
+ * run_code 一律视为写;
16
17
  * c. 只读排除:bash 命令 / 兜底 code 字符串仅当含写操作标记(BASH_WRITE_RE)且
17
18
  * 含 workspacesRoot 路径时才计为写证据;纯只读排查(ls/cat/glob/read/grep)不产生证据。
18
19
  * 2. 产物归属核对(fallback,机械可测):枚举 workspaces/<chainId>/ 下条目;
@@ -1,7 +1,7 @@
1
1
  // src/dispatcher/chain-auditor.ts
2
2
  import { readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { eventType, toolArgs, toolName } from './session-events.js';
4
+ import { eventType, isRunCodeDispatchEvent, toolArgs, toolName } from './session-events.js';
5
5
  import { isPathInside } from './target-repo.js';
6
6
  import { headerPresetOf } from './session-preset.js';
7
7
  /** 直接写工具(无条件视为写能力;只读工具如 read/glob/grep 不算)。 */
@@ -113,7 +113,7 @@ function sessionWriteEvidence(id, workspacesRoot, events) {
113
113
  }
114
114
  }
115
115
  }
116
- else if (t === 'tool/code-dispatch-start' || t === 'tool/code-dispatch') {
116
+ else if (isRunCodeDispatchEvent(ev)) {
117
117
  const rootCallId = String(dataField(ev, 'rootCallId') ?? '');
118
118
  const name = String(dataField(ev, 'name') ?? '');
119
119
  if (!rootCallId || !name)
@@ -2,12 +2,18 @@ import type { KanbanConfig } from '../config.js';
2
2
  import type { Role } from '../domain/types.js';
3
3
  import type { AgentModelOptions } from './dispatcher.js';
4
4
  /**
5
- * 模型候选链:角色创建时主模型 → fallbacks 依次静默切换。
6
- * primary = config.roles.models[role];未配置回退 defaultModel。
5
+ * 模型候选链(四层):
6
+ * ① 角色 primary(config.roles.models[role])→ ② 降级候选(fallbacks,顺序=优先序,≤2)
7
+ * → ③ 全局官方兜底(config.roles.chainFallback,恒垫链尾)→ ④ 宿主默认模型(defaultModel)。
8
+ * 角色链全空 → 官方兜底单候选直用;官方兜底也未配 → 宿主默认模型;都无 → 空链(调用方不传 agentOptions)。
7
9
  * reasoningEffort 未指定一律 'high'。
8
10
  *
9
11
  * @returns 有序候选链(不含不可用者;可能为空 = 无任何配置)
10
12
  */
11
13
  export declare function buildModelCandidates(config: KanbanConfig, role: Role, defaultModel?: AgentModelOptions): AgentModelOptions[];
12
- /** 判定 create/resume 错误是否属于 model/provider 不可用(可静默切换下一候选)。 */
14
+ /**
15
+ * 判定 create/resume 错误是否属于候选不可用(可静默切换下一候选)。
16
+ * 覆盖三类:provider/model 不可用(无适配器/不存在)、凭据缺失(官方账号未登录
17
+ * MISSING_CREDENTIAL / 登录提示——不切换会把登录态问题误判成任务失败)。
18
+ */
13
19
  export declare function isModelUnavailableError(err: unknown): boolean;