dsh-llm-fallbacks 0.4.1 → 0.4.3

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/dist/seeds.d.ts CHANGED
@@ -11,10 +11,13 @@
11
11
  * State model (spec §9.2) — two stores, strictly separated:
12
12
  * 1. operator config rows (persisted; a seeded role is a plain
13
13
  * `roles.list` row `{ id, persona }`), and
14
- * 2. an in-memory per-apply seed registry (`Map<id, seedPersona>`),
15
- * declare = replacement (the batch is the companion's full current set).
16
- * `seeded` / `personaOverridden` are DERIVED at read time, never stored —
17
- * a config round-trip cannot orphan an override (AC-3).
14
+ * 2. an in-memory per-apply seed registry (`Map<id, { persona, set }>` —
15
+ * the declared default persona plus its provenance label), declare =
16
+ * replacement (the batch is the companion's full current set).
17
+ * `seeded` / `personaOverridden` / `source` are DERIVED at read time, never
18
+ * stored — a config round-trip cannot orphan an override (AC-3), and
19
+ * provenance rides the in-memory registry only, so it can never churn a
20
+ * settings write.
18
21
  *
19
22
  * Materialization (spec §9.2): append `{ id, persona }` (two keys only,
20
23
  * R4) / attach row untouched / at-default tracking / override preserved +
@@ -31,6 +34,41 @@ export interface SeedDeclaration {
31
34
  id: string;
32
35
  persona: string;
33
36
  }
37
+ /**
38
+ * Per-row provenance label (spec §2). The fixed labels: `bundled` (the
39
+ * plugin's own bundled preset self-declare), `external` (a companion
40
+ * declare without a set name — or with an invalid one, degraded), `user`
41
+ * (no live declaration). Any other value is a declared set name; the
42
+ * reserved names can never be declared as a set, so the fixed labels are
43
+ * unambiguous.
44
+ */
45
+ export type SeedSource = 'bundled' | 'external' | 'user' | (string & {});
46
+ /**
47
+ * Optional provenance for one `declare()` call (spec §2) — the PUBLIC
48
+ * options face (a companion's registered set name). This is also the type
49
+ * of the `FallbacksService.declareSeeds` second parameter: consumers can
50
+ * never label a declare `bundled` (reserved).
51
+ */
52
+ export interface SeedsDeclareOptions {
53
+ /**
54
+ * Registered set name — trimmed. Empty after trim, non-string, or a
55
+ * reserved value (`bundled` / `user` / `external`) warns once and
56
+ * degrades the batch to the unnamed `external` source; the seeds still
57
+ * apply (a bad label must never drop roles).
58
+ */
59
+ set?: string;
60
+ }
61
+ /**
62
+ * The manager-level declare options — the public face plus the INTERNAL
63
+ * bundled marker for the plugin's own preset self-declare. Module-internal
64
+ * (never exported, never re-exported from the package root): only the
65
+ * preset child call site passes `bundled`, and only a structurally-checked
66
+ * object literal at that — the type itself is not nameable by consumers.
67
+ */
68
+ interface SeedsInternalDeclareOptions extends SeedsDeclareOptions {
69
+ /** INTERNAL — bundled preset self-declare provenance. Not a consumer option. */
70
+ bundled?: true;
71
+ }
34
72
  /**
35
73
  * Why one declared id was skipped (spec §9.1). Per-id skip + warn, never
36
74
  * coercion — valid siblings in the same batch still apply (AC-5).
@@ -68,6 +106,8 @@ export interface EffectiveRole {
68
106
  seeded: boolean;
69
107
  /** `seeded` && row persona !== current seed default. */
70
108
  personaOverridden: boolean;
109
+ /** Where the row's seed state comes from (read-time derived, never persisted) — `user` iff not seeded. */
110
+ source: SeedSource;
71
111
  /** Present iff seeded. */
72
112
  seedPersona?: string;
73
113
  }
@@ -86,6 +126,12 @@ export interface SeedRevertOutcome {
86
126
  export interface SeedsWireStatus {
87
127
  id: string;
88
128
  overridden: boolean;
129
+ /**
130
+ * Provenance label (spec §2) — additive and OPTIONAL on the wire: a
131
+ * gateway predating the field (version skew) sends entries without it;
132
+ * consumers treat a missing label as "no badge".
133
+ */
134
+ source?: SeedSource;
89
135
  }
90
136
  /**
91
137
  * The narrow IO seam this module writes through — keeps `src/seeds.ts`
@@ -105,6 +151,7 @@ export interface SeedsIo {
105
151
  */
106
152
  export declare class FallbacksSeedManager {
107
153
  private readonly logger;
154
+ /** Per-apply declaration registry: declared default persona + provenance label. */
108
155
  private registry;
109
156
  constructor(logger: FallbacksConfigLogger);
110
157
  /**
@@ -112,6 +159,10 @@ export declare class FallbacksSeedManager {
112
159
  * companion's FULL current declaration set; ids omitted from the batch
113
160
  * drop out of the registry while their rows remain (R2).
114
161
  *
162
+ * `options` labels the whole batch's provenance (spec §2), resolved once
163
+ * per call: an invalid set name warns once and degrades to the unnamed
164
+ * `external` case — the seeds still apply.
165
+ *
115
166
  * Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
116
167
  * reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
117
168
  * still apply (AC-5). Materializes per spec §9.2, writes only when the
@@ -119,15 +170,17 @@ export declare class FallbacksSeedManager {
119
170
  * (idempotent, AC-1), and commits the registry only after a successful
120
171
  * write (compute → write → commit; retry-safe).
121
172
  */
122
- declare(seeds: readonly SeedDeclaration[], io: SeedsIo): Promise<SeedDeclareOutcome>;
173
+ declare(seeds: readonly SeedDeclaration[], io: SeedsIo, options?: SeedsInternalDeclareOptions): Promise<SeedDeclareOutcome>;
123
174
  /**
124
175
  * Readback (b) — sync, derived: every config row annotated with
125
176
  * `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
126
- * membership in the live declaration set; persona inequality). Nothing
177
+ * membership in the live declaration set; persona inequality) plus the
178
+ * row's provenance `source` (spec §2): the declaring set's label, or
179
+ * `user` when no live declaration covers the row. Nothing
127
180
  * override-shaped is stored, so a config round-trip cannot orphan state.
128
181
  */
129
182
  effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
130
- /** Card badge state (spec §9.4): seeded rows, with the override flag. */
183
+ /** Card badge state (spec §9.4): seeded rows, with the override flag and the provenance label. */
131
184
  wireStatus(io: SeedsIo): SeedsWireStatus[];
132
185
  /**
133
186
  * Revert one id to the CURRENT declared seed default (AC-3). Writes
@@ -139,3 +192,4 @@ export declare class FallbacksSeedManager {
139
192
  revert(id: string, io: SeedsIo): Promise<SeedRevertOutcome>;
140
193
  private warnSkip;
141
194
  }
195
+ export {};
package/dist/state.d.ts CHANGED
@@ -31,6 +31,19 @@ import type { FallbackSwitchReason } from './events.ts';
31
31
  import { CooldownStore, StepFailureSet } from './cooldown.ts';
32
32
  import { RecoveryStore } from './recovery.ts';
33
33
  import type { RecoveryPolicy } from './config.ts';
34
+ /**
35
+ * What a switch's failure was actually about.
36
+ *
37
+ * `'route'` (the default) is every failure the plugin has ever walked: the
38
+ * provider/model itself is unhealthy (auth, quota, rate limit, ...), so the
39
+ * from-route is cooled down and counted as a route failure.
40
+ * `'request'` is a failure of the REQUEST on a healthy route — a
41
+ * `CONTEXT_WINDOW_EXCEEDED` rejection is the request being too big, not the
42
+ * model being down. `commit()` keeps the step-scoped bookkeeping for those
43
+ * but writes no suppression and no recovery failure (see `src/index.ts`
44
+ * `REQUEST_SCOPED_CODES`).
45
+ */
46
+ export type SwitchScope = 'route' | 'request';
34
47
  /** Decision awaiting application at the next `agent/request` boundary (spec §5.1). */
35
48
  export interface PendingSwitch {
36
49
  from: {
@@ -43,6 +56,13 @@ export interface PendingSwitch {
43
56
  };
44
57
  role: string;
45
58
  reason: FallbackSwitchReason;
59
+ /**
60
+ * What the failure was about — absent means `'route'`, the semantics every
61
+ * switch had before request-scoped codes existed (same optional-with-a-
62
+ * documented-default idiom as `FallbacksRole.fallback`). Only the
63
+ * `agent/request-error` listener ever sets `'request'`.
64
+ */
65
+ scope?: SwitchScope;
46
66
  }
47
67
  /**
48
68
  * One plugin-originated subagent switch the host allowlist blocked (plan
@@ -169,9 +189,17 @@ export declare class FallbackStateStore {
169
189
  */
170
190
  syncRecovery(state: AgentFallbackState, now: number, mode: RecoveryPolicy): void;
171
191
  /**
172
- * Observed completion ⇒ close the circuit (plan P2 rule 6): acts only when
173
- * the entry exists and is half-open; otherwise a no-op (a stale in-flight
174
- * success must not cancel a fresher escalated re-suppression).
192
+ * Observed completion ⇒ close the circuit (plan P2 rule 6). The lazy
193
+ * expiry runs FIRST — the same read the decision path performs — so a
194
+ * cooldown that lapsed BEFORE this completion is treated as what it is:
195
+ * an expired suppression, not an active one. The completion is
196
+ * post-expiry evidence and closes the circuit even when no decision-path
197
+ * read has transitioned the entry to half-open yet (a manual
198
+ * re-selection of the route, or a route no walk ever consulted because the
199
+ * current route kept succeeding). An ACTIVE suppression (`until > now`,
200
+ * including the `Infinity` of `revertPolicy: 'never'`) is untouched:
201
+ * a stale in-flight success must not cancel a fresher escalated
202
+ * re-suppression.
175
203
  */
176
- observeSuccess(state: AgentFallbackState, key: string): void;
204
+ observeSuccess(state: AgentFallbackState, key: string, now?: number): void;
177
205
  }
@@ -125,12 +125,14 @@ export declare const JSON_FIELD_MAX_DRAFT_BYTES: number;
125
125
  export declare function buildFallbacksTuiSection(): TuiSettingsSection;
126
126
  /**
127
127
  * Register the `fallbacks` section on the optional `tuiSettingsSections`
128
- * service. First-fiber-only (`serviceOwned === true` — mirrors
129
- * `installTuiClient` and the gateway/typert multi-fiber dedupe; the host
130
- * registry throws on a duplicate namespace, so a deduped later fiber must
131
- * never register). The service is optional: a composition without
132
- * `dsh-tui-settings-sections` keeps the plugin working and simply omits the
133
- * TUI settings surface.
128
+ * service. Gated by `serviceOwned` — the optimistic apply-time claim (see
129
+ * `apply()` in src/index.ts) — with a sibling dedupe guard inside the child,
130
+ * mirroring `installTuiClient`: the claim is not a fact, so a fiber applying
131
+ * inside the claim window can race another fiber's registration, and the
132
+ * host registry's duplicate throw must degrade (no-op disposer + debug log)
133
+ * instead of failing the child. Any other error stays loud. The service is
134
+ * optional: a composition without `dsh-tui-settings-sections` keeps the
135
+ * plugin working and simply omits the TUI settings surface.
134
136
  *
135
137
  * The inject child returns the registry disposer so cordis withdraws the
136
138
  * registration when this fiber (or the service) goes away.
package/dist/tui.d.ts CHANGED
@@ -42,11 +42,18 @@ export interface TuiCommandTreeProvider {
42
42
  export declare const FALLBACKS_TUI_ROOT = "fallbacks";
43
43
  /**
44
44
  * Register the `/fallbacks` provider on the optional `tuiCommandTrees`
45
- * service. First-fiber-only (`serviceOwned === true` — mirrors the
46
- * gateway/typert multi-fiber dedupe; the host registry throws on duplicate
47
- * roots, so a deduped later fiber must never register). The service is
48
- * optional: a composition without `dsh-tui-command-trees` keeps the plugin
49
- * working and simply omits the TUI surface.
45
+ * service. Gated by the OPTIMISTIC `serviceOwned` claim taken at apply time
46
+ * (the service itself is provided inside the settings inject child, after
47
+ * apply returns — see `apply()` in src/index.ts), with the sibling dedupe
48
+ * guard inside the child: the claim is not a fact, so a fiber applying
49
+ * inside the claim window registers here and, when it loses the race for
50
+ * the host registry's root, degrades via the `already registered` catch
51
+ * below — only a post-settlement duplicate fiber skips at this gate (its
52
+ * claim INITIALIZED false: the service was already visible when the claim
53
+ * was taken; the child-side catch correction settles strictly after apply()
54
+ * and can never influence this synchronous read). The service is optional:
55
+ * a composition without `dsh-tui-command-trees` keeps the plugin working
56
+ * and simply omits the TUI surface.
50
57
  *
51
58
  * The inject child returns the registry disposer so cordis withdraws the
52
59
  * registration when this fiber (or the service) goes away.
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.4.1",
8
+ "version": "0.4.3",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -362,7 +362,12 @@ for (const match of injectedCssMatches) {
362
362
  }
363
363
  }
364
364
  }
365
- const classMapMatches = [...bundleText.matchAll(/\b\w+_module_css_default\s*=\s*(\{[\s\S]*?\n\s*\});/g)]
365
+ // The class-map object literal is FLAT (string → string, emitted by
366
+ // cssModuleContents as one JSON.stringify), so the lazy match to the first
367
+ // `};` is exact for both emit shapes the printer produces — multi-entry maps
368
+ // across lines and a single-entry map inline (`{ "badge": "_<8hex>_badge" };`
369
+ // has no newline before its closing brace).
370
+ const classMapMatches = [...bundleText.matchAll(/\b\w+_module_css_default\s*=\s*(\{[\s\S]*?\});/g)]
366
371
  if (classMapMatches.length === 0) {
367
372
  throw new Error('client bundle contract: no inlined css-module class map found — check the dsh-css-modules-inline plugin')
368
373
  }
@@ -79,7 +79,7 @@ export function parseArgs(argv: string[]): { version?: string; autoBump: boolean
79
79
  * version stays in its prerelease line, bumping only the numeric tail.
80
80
  */
81
81
  export function autoBumpPatch(current: string): string {
82
- const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(current)
82
+ const m = current.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/)
83
83
  if (!m) throw new Error(`Cannot auto-bump "${current}": not a parseable X.Y.Z[-pre] version`)
84
84
  const [, major, minor, patch, pre] = m
85
85
  if (pre === undefined) return `${major}.${minor}.${Number(patch) + 1}`