dsh-workbuddy-xdpool 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -1413,7 +1413,7 @@ export declare class WorkBuddyScheduler {
1413
1413
  * long before the settings section exists; a ledger written before that point
1414
1414
  * would have nowhere to go.
1415
1415
  */
1416
- setEarningsPersistence(save: (ledger: AutomationLedger) => void): void;
1416
+ setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
1417
1417
  /**
1418
1418
  * Fold a previously persisted ledger back in, when it belongs to today.
1419
1419
  *
@@ -2158,6 +2158,14 @@ export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
2158
2158
  * also why `contextBudgets` is a real dictionary (`z.dict`) - an open object
2159
2159
  * schema reads as "an object with no fields" and the fold then throws while
2160
2160
  * the provider row is rendered.
2161
+ *
2162
+ * Every field is wrapped in {@link asVolatile}: on the 0.1.7 line the settings
2163
+ * write gate refuses an entry whose schema declares no volatile field at all
2164
+ * ("Plugin entry ... has no volatile fields") and `describe()` skips such an
2165
+ * entry — so an unmarked schema means the card can neither render nor save. On
2166
+ * the 0.1.5 line the wrapper degrades to an identity no-op (see its JSDoc), and
2167
+ * the value the running instance reads is a plain value either way once
2168
+ * unwrapped.
2161
2169
  */
2162
2170
  export declare const Config: z<Config>;
2163
2171
  /** Everything the CLI needs from a live plugin instance. */
package/lib/index.js CHANGED
@@ -1747,8 +1747,20 @@ const AAD_DOMAIN = Buffer.from("WB-AAD\0", "ascii");
1747
1747
  const SYMMETRIC_SCHEME = "sym-v1";
1748
1748
  /** Env override pointing at the WorkBuddy desktop executable. */
1749
1749
  const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
1750
- /** How long the app is given to answer with its key payload. */
1751
- const KEY_FETCH_TIMEOUT_MS = 1e4;
1750
+ /**
1751
+ * How long the app is given to answer with its key payload.
1752
+ *
1753
+ * 30s, not 10s: the child is the WorkBuddy Electron binary running as plain
1754
+ * Node, and its FIRST spawn on a cold machine costs several seconds on its own
1755
+ * (measured 4.5s here) before the endpoint security stack has warmed its scan
1756
+ * cache. Under load — a concurrent `pnpm install` from the market, a running
1757
+ * full-disk scan — that first spawn crosses a 10s budget, the fetch rejects,
1758
+ * `readAtRestKey` returns undefined, and every encrypted credential then reads
1759
+ * as `WorkBuddyEncryptedCredentialError` until the 60s negative cache expires.
1760
+ * A successful fetch is cached for the process lifetime, so the longer budget
1761
+ * is only ever paid once per process, and only when the app is present but slow.
1762
+ */
1763
+ const KEY_FETCH_TIMEOUT_MS = 3e4;
1752
1764
  /**
1753
1765
  * Executable file names the desktop app ships under, in probe order.
1754
1766
  *
@@ -2003,6 +2015,7 @@ function windowsFallbackAppPaths(env) {
2003
2015
  }
2004
2016
  roots.add(join(drive, "Program Files"));
2005
2017
  roots.add(join(drive, "Program Files (x86)"));
2018
+ roots.add(drive);
2006
2019
  }
2007
2020
  for (const root of roots) for (const name of APP_EXECUTABLE_NAMES) {
2008
2021
  out.push(join(root, "WorkBuddy", name));
@@ -4160,12 +4173,16 @@ var WorkBuddyScheduler = class {
4160
4173
  */
4161
4174
  persistEarnings() {
4162
4175
  if (this.saveEarningsFn === void 0) return;
4176
+ const accounts = {};
4177
+ for (const [id, entry] of this.earnings) accounts[id] = entry;
4178
+ const ledger = {
4179
+ date: this.earningsDate,
4180
+ accounts
4181
+ };
4163
4182
  try {
4164
- const accounts = {};
4165
- for (const [id, entry] of this.earnings) accounts[id] = entry;
4166
- this.saveEarningsFn({
4167
- date: this.earningsDate,
4168
- accounts
4183
+ const saved = this.saveEarningsFn(ledger);
4184
+ if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
4185
+ this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
4169
4186
  });
4170
4187
  } catch (error) {
4171
4188
  this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
@@ -5962,6 +5979,56 @@ function automationOptions(automation) {
5962
5979
  * region that has never been saved stays absent so `applyConfigFromSource` can
5963
5980
  * fall back to the legacy flat keys.
5964
5981
  */
5982
+ function asVolatile(schema) {
5983
+ const candidate = schema;
5984
+ return typeof candidate.volatile === "function" ? candidate.volatile() : schema;
5985
+ }
5986
+ /**
5987
+ * Peel one live volatile reference.
5988
+ *
5989
+ * On 0.1.7 a volatile field is handed back as `{ get(): T }` rather than a
5990
+ * plain value, so a running instance observes a settings edit without being
5991
+ * remounted. Every read of a marked field therefore has to unwrap: passing the
5992
+ * reference onward compares an object against a string and reports the field as
5993
+ * unset. On 0.1.5 the field is already a plain value, so this is a no-op.
5994
+ *
5995
+ * The test is duck-typed on purpose — importing `isVolatile` would add a
5996
+ * dependency the 0.1.5 line does not carry.
5997
+ */
5998
+ function unwrapVolatile(value) {
5999
+ if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
6000
+ return value;
6001
+ }
6002
+ /**
6003
+ * Deep copy of a config value with every live `{get(): T}` reference replaced
6004
+ * by the value it resolves to.
6005
+ *
6006
+ * {@link unwrapVolatile} peels only the one level an ordinary read needs. A
6007
+ * settings service is different: it validates and `structuredClone`s the WHOLE
6008
+ * object, so a reference surviving anywhere inside it fails schema validation
6009
+ * with a message that names the field but not the cause —
6010
+ * `$.authFile expected string but got [object Object]`. That is what makes the
6011
+ * namespace fail to register and the card silently disappear, which is why the
6012
+ * object handed to `installSection` goes through this first.
6013
+ */
6014
+ function unwrapVolatileDeep(value) {
6015
+ const peeled = unwrapVolatile(value);
6016
+ if (Array.isArray(peeled)) return peeled.map((entry) => unwrapVolatileDeep(entry));
6017
+ if (peeled !== null && typeof peeled === "object") {
6018
+ const out = {};
6019
+ for (const [key, entry] of Object.entries(peeled)) out[key] = unwrapVolatileDeep(entry);
6020
+ return out;
6021
+ }
6022
+ return peeled;
6023
+ }
6024
+ /**
6025
+ * One region's model-selection schema.
6026
+ *
6027
+ * Every field is optional on purpose: an absent field keeps its documented
6028
+ * meaning ("all enabled" / "follow the upstream image flag" / "no cap"), and a
6029
+ * region that has never been saved stays absent so `applyConfigFromSource` can
6030
+ * fall back to the legacy flat keys.
6031
+ */
5965
6032
  const modelSelectionSchema = z.object({
5966
6033
  enabledModelIds: z.array(z.string()).description("Model ids enabled in this region's picker (absent = all)"),
5967
6034
  imageModelIds: z.array(z.string()).description("Model ids accepting image input in this region (absent = follow upstream)"),
@@ -5999,24 +6066,32 @@ const modelSelectionKeyFor = (region) => region === "cn" ? "modelSelectionCn" :
5999
6066
  * also why `contextBudgets` is a real dictionary (`z.dict`) - an open object
6000
6067
  * schema reads as "an object with no fields" and the fold then throws while
6001
6068
  * the provider row is rendered.
6069
+ *
6070
+ * Every field is wrapped in {@link asVolatile}: on the 0.1.7 line the settings
6071
+ * write gate refuses an entry whose schema declares no volatile field at all
6072
+ * ("Plugin entry ... has no volatile fields") and `describe()` skips such an
6073
+ * entry — so an unmarked schema means the card can neither render nor save. On
6074
+ * the 0.1.5 line the wrapper degrades to an identity no-op (see its JSDoc), and
6075
+ * the value the running instance reads is a plain value either way once
6076
+ * unwrapped.
6002
6077
  */
6003
6078
  const Config = z.object({
6004
- authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
6005
- cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
6006
- distribution: z.union([
6079
+ authFile: asVolatile(z.string().description("WorkBuddy desktop auth file (defaults to the app own location)")),
6080
+ cooldownMs: asVolatile(z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds")),
6081
+ distribution: asVolatile(z.union([
6007
6082
  "priority",
6008
6083
  "round-robin",
6009
6084
  "balanced"
6010
- ]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)"),
6011
- disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
6012
- creditReserves: z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value"),
6013
- enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
6014
- imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
6015
- contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
6016
- modelSelectionCn: modelSelectionSchema.description("Model selection for the domestic gateway"),
6017
- modelSelectionGlobal: modelSelectionSchema.description("Model selection for the international gateway"),
6018
- automation: automationSchema.description("Daily points automation (activity report, task claiming, check-in)"),
6019
- automationEarnings: z.any().description("Automation earnings ledger (written by the scheduler)")
6085
+ ]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)")),
6086
+ disabledAccountIds: asVolatile(z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)")),
6087
+ creditReserves: asVolatile(z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value")),
6088
+ enabledModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet")),
6089
+ imageModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet")),
6090
+ contextBudgets: asVolatile(z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet")),
6091
+ modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
6092
+ modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
6093
+ automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
6094
+ automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
6020
6095
  });
6021
6096
  /** Live API, published for the CLI. */
6022
6097
  let api;
@@ -6073,10 +6148,21 @@ function apply(ctx, config = {}) {
6073
6148
  /** The domestic adapter, published for the CLI after registration. */
6074
6149
  let builtAdapter;
6075
6150
  let invalidateCatalog = () => {};
6076
- let current = () => config;
6151
+ /**
6152
+ * The effective config with every live volatile reference peeled.
6153
+ *
6154
+ * On 0.1.7 a field marked volatile is handed to the plugin as `{get(): T}` so
6155
+ * a settings edit is observed without a remount, and the loader keeps the
6156
+ * reference live. Every read below therefore goes through this view: a raw
6157
+ * reference compared against a string reports the field as unset, which is
6158
+ * how a saved reserve would read back as "0" while the file holds the value.
6159
+ * On 0.1.5 there is nothing to peel and this is the identity.
6160
+ */
6161
+ let rawCurrent = () => config;
6162
+ const current = () => unwrapVolatileDeep(rawCurrent());
6077
6163
  const sectionHooks = {
6078
6164
  setSource(source) {
6079
- current = source;
6165
+ rawCurrent = source;
6080
6166
  },
6081
6167
  onChange() {
6082
6168
  applyConfigFromSource();
@@ -6102,8 +6188,12 @@ function apply(ctx, config = {}) {
6102
6188
  core.scheduler.applyConfig(automationOptions(automation));
6103
6189
  };
6104
6190
  const settingsService = ctx.settings;
6105
- if (typeof settingsService.installSection === "function") settingsService.installSection(ctx, WORKBUDDY_POOL_SETTINGS_NS, Config, config, sectionHooks);
6106
- else ctx.logger.warn?.("dsh-workbuddy-xdpool: settings service has no installSection; card will not mount");
6191
+ if (typeof settingsService.installSection === "function") settingsService.installSection(ctx, WORKBUDDY_POOL_SETTINGS_NS, Config, unwrapVolatileDeep(config), sectionHooks);
6192
+ if (typeof settingsService.configure === "function") ctx.effect(() => settingsService.configure?.({ auto: true }, ctx.fiber) ?? (() => {}));
6193
+ if (typeof settingsService.installSection !== "function" && typeof settingsService.configure !== "function") ctx.logger.warn?.("dsh-workbuddy-xdpool: settings service exposes neither installSection nor configure; the card will not mount");
6194
+ ctx.on("loader/volatile-update", () => {
6195
+ applyConfigFromSource();
6196
+ });
6107
6197
  /**
6108
6198
  * Write one key of the plugin's own settings section. Only ever called with
6109
6199
  * the three model-selection keys, so the settings file cannot be steered from
@@ -6139,11 +6229,31 @@ function apply(ctx, config = {}) {
6139
6229
  if (typeof value === "object" && value !== null) return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
6140
6230
  return JSON.stringify(value);
6141
6231
  }
6232
+ /**
6233
+ * The namespace `settings.update` expects on the running line.
6234
+ *
6235
+ * 0.1.5 resolves writers by the NAMESPACE the plugin registered through
6236
+ * `installSection`, so the plugin picks it. 0.1.7 resolves them by the HOST
6237
+ * PLUGIN ENTRY ID instead (`configEditor.entries().find(row => row.options.id
6238
+ * === ns)`), and the profile patch chooses that id — the live Desktop host
6239
+ * mounts this plugin as `llm-workbuddy-xdpool`, but a marketplace install can
6240
+ * wrap it (`mkt-...`) or mount it through an `include` row. Asking the fiber
6241
+ * for its own entry id is therefore the only correct answer there; guessing
6242
+ * our own namespace makes every save throw `No configurable plugin entry`.
6243
+ */
6244
+ const settingsWriteNs = typeof settingsService.installSection === "function" ? WORKBUDDY_POOL_SETTINGS_NS : (() => {
6245
+ try {
6246
+ const entryId = ctx.fiber?.entry?.options?.id;
6247
+ return entryId === void 0 || entryId === "" ? WORKBUDDY_POOL_SETTINGS_NS : entryId;
6248
+ } catch {
6249
+ return WORKBUDDY_POOL_SETTINGS_NS;
6250
+ }
6251
+ })();
6142
6252
  const setSetting = async (key, value, expected) => {
6143
6253
  if (value === void 0) return;
6144
6254
  const update = settingsService?.update;
6145
6255
  if (update === void 0) throw new Error(`settings service has no update(); ${key} was not saved`);
6146
- await update.call(settingsService, WORKBUDDY_POOL_SETTINGS_NS, { [key]: value });
6256
+ await update.call(settingsService, settingsWriteNs, { [key]: value });
6147
6257
  if (expected !== void 0) {
6148
6258
  const stored = current()[key];
6149
6259
  if (!stableJsonEqual(stored, expected)) throw new Error(`settings field "${key}" was not persisted`);
@@ -6339,7 +6449,9 @@ function apply(ctx, config = {}) {
6339
6449
  ctx.logger.warn(`dsh-workbuddy-xdpool: ${region} model catalog unavailable; using static fallback`, error);
6340
6450
  }
6341
6451
  invalidateCatalog();
6342
- })();
6452
+ })().catch((error) => {
6453
+ ctx.logger.warn("dsh-workbuddy-xdpool: account catalog seed failed", error);
6454
+ });
6343
6455
  }, (error) => {
6344
6456
  ctx.logger.error("dsh-workbuddy-xdpool: shim failed to listen", error);
6345
6457
  });
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-xdpool",
3
3
  "displayName": "DSH WorkBuddy XD Pool",
4
4
  "description": "Merge every locally signed-in WorkBuddy account into DeepSeek Harness as one auto-failing-over model pool (multi-account rotation, live credits, daily check-in and model catalog).",
5
- "version": "1.6.1",
5
+ "version": "1.7.0",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
@@ -24,7 +24,8 @@
24
24
  ],
25
25
  "type": "module",
26
26
  "engines": {
27
- "node": "^22.19.0 || >=24.0.0"
27
+ "node": "^22.19.0 || >=24.0.0",
28
+ "dsh": ">=0.1.5-rc.1 <0.2.0"
28
29
  },
29
30
  "main": "lib/index.js",
30
31
  "types": "lib/index.d.ts",
@@ -63,7 +64,6 @@
63
64
  },
64
65
  "client": {
65
66
  "inject": [
66
- "@deepseek-ai/dsh-client-runtime",
67
67
  "@deepseek-ai/dsh-client-ui-settings",
68
68
  "@deepseek-ai/dsh-client-ui-slots",
69
69
  "@deepseek-ai/dsh-client-ui-settings-plugins",