dsh-connect-workbuddy 3.4.2 → 3.4.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.
@@ -1,7 +1,7 @@
1
1
  import { readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
2
2
  import { createDecipheriv, createHash } from "node:crypto";
3
3
  import { homedir } from "node:os";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, join, win32 } from "node:path";
5
5
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
6
6
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
7
7
  import { execFile, execFileSync } from "node:child_process";
@@ -202,6 +202,167 @@ function macosBundleExecutable(bundle) {
202
202
  return join(bundle, "Contents", "MacOS", name);
203
203
  }
204
204
  /**
205
+ * The executable path WorkBuddy's own uninstall registration points at, or
206
+ * undefined when the app was never registered.
207
+ *
208
+ * Why this exists: the four directory candidates in
209
+ * {@link workbuddyAppExecutableCandidates} encode the DEFAULT install layout.
210
+ * An app installed anywhere else — a second drive (the observed case was
211
+ * `E:\workbuddy\WorkBuddy.exe`) — is installed and signed in, yet every
212
+ * candidate misses, so the encrypted credential file cannot be opened and a
213
+ * user who IS signed in is reported as signed out. The README's answer was to
214
+ * set `WORKBUDDY_APP_EXECUTABLE` by hand; the installer already recorded the
215
+ * answer, so this reads it instead.
216
+ *
217
+ * The registration is the app's own claim about itself, which is what makes it
218
+ * safe to hand the path to the credential probe: a display name alone would be
219
+ * a guess, whereas the path here was written by the installer that placed the
220
+ * binary. The value is still checked against {@link APP_EXECUTABLE_NAME} and for
221
+ * existence before use, and the callers fall through to the documented hint when
222
+ * it is absent.
223
+ *
224
+ * The read is done through ALL THREE uninstall views, because the hive a
225
+ * registration lands in depends on how the app was installed: per-user
226
+ * (`/currentuser`, the layout WorkBuddy uses) registers under `HKCU`, a
227
+ * machine-wide install under `HKLM`, and a 32-bit machine-wide one under
228
+ * `HKLM\...\WOW6432Node`. A hive that does not exist or is unreadable is the
229
+ * normal case on a machine without the app, not an error: every failure path
230
+ * returns undefined so the caller can try the next candidate.
231
+ */
232
+ function windowsRegistryAppExecutable(query = queryRegistry) {
233
+ for (const root of [
234
+ "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
235
+ "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
236
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
237
+ ]) {
238
+ const listing = query([
239
+ "query",
240
+ root,
241
+ "/s",
242
+ "/f",
243
+ "WorkBuddy",
244
+ "/t",
245
+ "REG_SZ"
246
+ ]);
247
+ if (listing === void 0) continue;
248
+ const executable = registryExecutableFromQuery(listing);
249
+ if (executable !== void 0) return executable;
250
+ const fromDirectory = registryInstallLocationFromQuery(listing);
251
+ if (fromDirectory !== void 0) return fromDirectory;
252
+ }
253
+ }
254
+ /**
255
+ * Parse `reg query` output for the value that names the installed executable.
256
+ *
257
+ * `reg query` prints keys as `HKEY_CURRENT_USER\...\Uninstall\<id>` followed by
258
+ * indented ` ValueName REG_SZ Data` lines. Only `DisplayIcon` is
259
+ * trusted here: it is the one value the installer writes pointing AT the
260
+ * binary. The value is normalised by {@link normalizeRegistryExecutable}, which
261
+ * strips the quotes and the icon index and rejects anything that is not the
262
+ * desktop executable.
263
+ */
264
+ function registryExecutableFromQuery(output) {
265
+ for (const rawLine of output.split(/\r?\n/u)) {
266
+ const match = /^\s*DisplayIcon\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
267
+ if (match === null) continue;
268
+ const executable = normalizeRegistryExecutable(match[1]);
269
+ if (executable !== void 0) return executable;
270
+ }
271
+ }
272
+ /**
273
+ * The `WorkBuddy.exe` under an `InstallLocation`-style registry directory.
274
+ *
275
+ * Kept separate from {@link registryExecutableFromQuery} because the two values
276
+ * mean different things: `DisplayIcon` NAMES a file, whereas the directory has
277
+ * to have {@link APP_EXECUTABLE_NAME} joined onto it, and the result must still
278
+ * be checked for existence by the caller's probe.
279
+ *
280
+ * `win32.join` rather than the host's `join`: the input is a Windows registry
281
+ * value on EVERY machine this function can be called on. `path.join` is the
282
+ * running platform's, so a suite on Linux/macOS would mangle `E:\workbuddy`
283
+ * into `E:\workbuddy/WorkBuddy.exe` and the caller's `existsSync` could never
284
+ * match it. The separator here is fixed by the DATA, not by the host.
285
+ */
286
+ function registryInstallLocationFromQuery(output) {
287
+ for (const rawLine of output.split(/\r?\n/u)) {
288
+ const match = /^\s*InstallLocation\s+REG_SZ\s+(.*?)\s*$/u.exec(rawLine);
289
+ if (match === null) continue;
290
+ const directory = stripRegistryQuotes(match[1]);
291
+ if (directory === void 0 || directory === "") continue;
292
+ return win32.join(directory, APP_EXECUTABLE_NAME);
293
+ }
294
+ }
295
+ /**
296
+ * A registry executable value reduced to a usable path, or undefined when it
297
+ * cannot be one.
298
+ *
299
+ * `DisplayIcon` is stored as `"E:\workbuddy\WorkBuddy.exe",0`. The quotes and
300
+ * the icon index are stripped, and a value that names something other than the
301
+ * desktop executable — an `.ico`, an uninstaller, another product's binary — is
302
+ * rejected rather than launched. A bare path without quotes is accepted too,
303
+ * because not every installer writes the indexed form.
304
+ *
305
+ * The basename test uses `win32.basename` for the reason given on
306
+ * {@link registryInstallLocationFromQuery}: the separator belongs to the
307
+ * registry value (always Windows), so the host's `basename` — which does not
308
+ * treat `\` as a separator — would return the whole string and reject every
309
+ * legitimate path.
310
+ */
311
+ function normalizeRegistryExecutable(value) {
312
+ const unquoted = stripRegistryQuotes(value);
313
+ if (unquoted === void 0 || unquoted === "") return void 0;
314
+ const withoutIndex = unquoted.replace(/,\s*-?\d+\s*$/u, "");
315
+ if (withoutIndex === "") return void 0;
316
+ if (win32.basename(withoutIndex).toLowerCase() !== APP_EXECUTABLE_NAME.toLowerCase()) return void 0;
317
+ return withoutIndex;
318
+ }
319
+ /**
320
+ * The path inside a `DisplayIcon`/`InstallLocation` value, unquoted.
321
+ *
322
+ * Registry data may be written either quoted (`"E:\workbuddy\WorkBuddy.exe",0`)
323
+ * or bare. Only a leading quote is treated as quoting: a bare `C:\dir\a"b.exe`
324
+ * is a real (if unusual) filename and must not be truncated at the quote.
325
+ */
326
+ function stripRegistryQuotes(value) {
327
+ const trimmed = value.trim();
328
+ if (trimmed === "") return void 0;
329
+ if (!trimmed.startsWith("\"")) return trimmed;
330
+ const closing = trimmed.indexOf("\"", 1);
331
+ if (closing === -1) return void 0;
332
+ return trimmed.slice(1, closing).trim();
333
+ }
334
+ /**
335
+ * Run `reg query` and return its stdout, or undefined when it cannot be run.
336
+ *
337
+ * `windowsHide` is REQUIRED, not cosmetic: the DSH Desktop host is an Electron
338
+ * GUI process with no console, so spawning a console program without it flashes
339
+ * a visible black window on every probe. The same reason is recorded for the
340
+ * heartbeat probe in `host-heartbeat.ts` and in `docs/WINDOWS.md` §1-3.
341
+ *
342
+ * `execFileSync` (not `execFile`) keeps this synchronous so the candidates list
343
+ * stays a pure function of its inputs: it is called during `findWorkbuddyApp`
344
+ * `Executable`, which `readAtRestKey()` and the `doctor` command both treat as
345
+ * a plain lookup. The read is bounded by the registry query's own output and
346
+ * happens at most once per discovery.
347
+ */
348
+ function queryRegistry(args) {
349
+ try {
350
+ return execFileSync("reg", [...args], REGISTRY_PROBE_OPTIONS);
351
+ } catch {
352
+ return;
353
+ }
354
+ }
355
+ /**
356
+ * Options for the registry probe; `windowsHide` is mandatory (see above), and
357
+ * the timeout matches `fetchAtRestKeyPayload()`'s: a hung `reg` must not wedge
358
+ * the synchronous discovery path forever.
359
+ */
360
+ const REGISTRY_PROBE_OPTIONS = {
361
+ encoding: "utf8",
362
+ windowsHide: true,
363
+ timeout: 1e4
364
+ };
365
+ /**
205
366
  * Candidate paths of the WorkBuddy desktop executable, in probe order.
206
367
  *
207
368
  * The Windows build is the one that encrypts credentials, so Windows leads;
@@ -224,7 +385,7 @@ function workbuddyAppExecutableCandidates(platform = process.platform, home = ho
224
385
  const local = env["LOCALAPPDATA"]?.trim();
225
386
  const programFiles = env["ProgramFiles"]?.trim();
226
387
  const programFilesX86 = env["ProgramFiles(x86)"]?.trim();
227
- candidates.push(local === void 0 || local === "" ? void 0 : join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
388
+ candidates.push(local === void 0 || local === "" ? void 0 : win32.join(local, "Programs", "WorkBuddy", APP_EXECUTABLE_NAME), local === void 0 || local === "" ? void 0 : win32.join(local, "WorkBuddy", APP_EXECUTABLE_NAME), programFiles === void 0 || programFiles === "" ? void 0 : win32.join(programFiles, "WorkBuddy", APP_EXECUTABLE_NAME), programFilesX86 === void 0 || programFilesX86 === "" ? void 0 : win32.join(programFilesX86, "WorkBuddy", APP_EXECUTABLE_NAME));
228
389
  } else if (platform === "darwin") for (const name of MACOS_APP_BUNDLE_NAMES) candidates.push(readBundleExecutable(join("/Applications", name)), readBundleExecutable(join(home, "Applications", name)));
229
390
  return candidates.filter((candidate) => candidate !== void 0 && candidate !== "");
230
391
  }
@@ -298,13 +459,55 @@ function macosNestedAppBundles(parent) {
298
459
  return bundles;
299
460
  }
300
461
  /**
462
+ * Locate the desktop executable AND say how it was found.
463
+ *
464
+ * Kept beside {@link findWorkbuddyAppExecutable} rather than folded into it
465
+ * because the two answer different questions: callers on the credential path
466
+ * only want the path, whereas `doctor` must explain the discovery. Sharing the
467
+ * candidate list keeps them from drifting.
468
+ */
469
+ function findWorkbuddyAppExecutableWithSource(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
470
+ const fromEnv = env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
471
+ for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) {
472
+ try {
473
+ if (!existsSync(candidate)) continue;
474
+ } catch {
475
+ continue;
476
+ }
477
+ return {
478
+ executable: candidate,
479
+ source: fromEnv !== void 0 && fromEnv !== "" && candidate === fromEnv ? "env" : "default-layout"
480
+ };
481
+ }
482
+ if (platform === "win32") {
483
+ const fromRegistry = readRegistryAppPath();
484
+ if (fromRegistry !== void 0) try {
485
+ if (existsSync(fromRegistry)) return {
486
+ executable: fromRegistry,
487
+ source: "registry"
488
+ };
489
+ } catch {}
490
+ }
491
+ const executable = findWorkbuddyAppExecutable(platform, home, env, readRegistryAppPath);
492
+ return executable === void 0 ? void 0 : {
493
+ executable,
494
+ source: "nested-bundle"
495
+ };
496
+ }
497
+ /**
301
498
  * The first candidate that exists as a file, or undefined when the desktop app
302
499
  * is not installed where this platform expects it.
303
500
  */
304
- function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env) {
501
+ function findWorkbuddyAppExecutable(platform = process.platform, home = homedir(), env = process.env, readRegistryAppPath = windowsRegistryAppExecutable) {
305
502
  for (const candidate of workbuddyAppExecutableCandidates(platform, home, env)) try {
306
503
  if (existsSync(candidate)) return candidate;
307
504
  } catch {}
505
+ if (platform === "win32") {
506
+ const fromRegistry = readRegistryAppPath();
507
+ if (fromRegistry !== void 0) try {
508
+ if (existsSync(fromRegistry)) return fromRegistry;
509
+ } catch {}
510
+ }
308
511
  if (platform === "darwin") for (const parent of ["/Applications", join(home, "Applications")]) for (const bundle of macosNestedAppBundles(parent)) {
309
512
  const executable = macosBundleExecutable(bundle);
310
513
  if (executable === void 0) continue;
@@ -449,12 +652,19 @@ const HARD_CREDIT_MARKERS = [
449
652
  ];
450
653
  /** Session-invalidation markers that mean "sign in again in the WorkBuddy app". */
451
654
  const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
655
+ /**
656
+ * Content-policy refusal markers from the chat gateway. The quoted-key form
657
+ * avoids substring hits inside unrelated values (request ids are hex and can
658
+ * contain `11140` by coincidence).
659
+ */
660
+ const POLICY_REJECT_MARKERS = ["request illegal", "\"code\":11140"];
452
661
  /** Classify an upstream failure from its HTTP status and body excerpt. */
453
662
  function classifyUpstreamError(status, body) {
454
663
  if (status === 402) return "hard_credit";
455
664
  const lower = body.toLowerCase();
456
665
  for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
457
666
  for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
667
+ for (const marker of POLICY_REJECT_MARKERS) if (body.includes(marker)) return "policy_reject";
458
668
  if (status === 429) return "soft_rate";
459
669
  if (status === 404) return "not_found";
460
670
  if (status >= 500) return "server";
@@ -462,6 +672,34 @@ function classifyUpstreamError(status, body) {
462
672
  return "client";
463
673
  }
464
674
  /**
675
+ * Parse the known-good fields out of an upstream failure body. Non-JSON
676
+ * bodies (HTML error pages, empty strings) yield undefined rather than a guess.
677
+ */
678
+ function parseUpstreamErrorDetail(body) {
679
+ let parsed;
680
+ try {
681
+ parsed = JSON.parse(body);
682
+ } catch {
683
+ return;
684
+ }
685
+ if (typeof parsed !== "object" || parsed === null) return void 0;
686
+ const raw = parsed;
687
+ let displayMsg;
688
+ const display = raw.displayMsg;
689
+ if (typeof display === "string") displayMsg = display;
690
+ else if (typeof display === "object" && display !== null) {
691
+ const localized = display;
692
+ const pick = localized.zh ?? localized.en;
693
+ if (typeof pick === "string") displayMsg = pick;
694
+ }
695
+ if (displayMsg === void 0 && typeof raw.msg === "string") displayMsg = raw.msg;
696
+ const detail = {};
697
+ if (typeof raw.code === "number") detail.upstreamCode = raw.code;
698
+ if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
699
+ if (displayMsg !== void 0) detail.displayMsg = displayMsg;
700
+ return detail;
701
+ }
702
+ /**
465
703
  * Region for a login domain; an empty domain means CN (matching upstream tooling).
466
704
  *
467
705
  * The international product is reachable under TWO brand domains: the WorkBuddy
@@ -944,7 +1182,8 @@ var WorkBuddyUpstreamClient = class {
944
1182
  ok: false,
945
1183
  status: response.status,
946
1184
  kind: classifyUpstreamError(response.status, text),
947
- message: text
1185
+ message: text,
1186
+ detail: parseUpstreamErrorDetail(text)
948
1187
  };
949
1188
  }
950
1189
  /**
@@ -2552,7 +2791,7 @@ function generationOf(version) {
2552
2791
  //#endregion
2553
2792
  //#region src/version.ts
2554
2793
  /** The npm package version this build was produced from. */
2555
- const WORKBUDDY_CONNECT_VERSION = "3.4.2";
2794
+ const WORKBUDDY_CONNECT_VERSION = "3.4.4";
2556
2795
  //#endregion
2557
2796
  //#region src/host-heartbeat.ts
2558
2797
  /**
@@ -2710,4 +2949,4 @@ async function clearHostHeartbeat() {
2710
2949
  await rm(workbuddyHostHeartbeatPath(), { force: true });
2711
2950
  }
2712
2951
  //#endregion
2713
- export { readAtRestKey as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, isEncryptedFieldWrapper as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, openEncryptedField as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, macosBundleExecutable as X, isWorkbuddyBundle as Y, macosNestedAppBundles as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, workbuddyAppExecutableCandidates as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
2952
+ export { openEncryptedField as $, CREDENTIAL_REJECTED_CODE as A, prepareChatBody as B, defaultDesktopAuthPath as C, parseWorkBuddyAuth as D, legacyWorkbuddyOwnAuthPath as E, declaredTools as F, deriveAtRestKeyId as G, WORKBUDDY_APP_EXECUTABLE_ENV as H, isCredentialRejectedError as I, findWorkbuddyAppExecutableWithSource as J, fetchAtRestKeyPayload as K, parseCreditMultiplier as L, WorkBuddyCredentialRejectedError as M, WorkBuddyUpstreamClient as N, workbuddyAccountId as O, classifyUpstreamError as P, macosNestedAppBundles as Q, parseReasoning as R, defaultDesktopAuthDirs as S, isEncryptedCredentialError as T, clearAtRestKeyCache as U, regionOf as V, deriveAtRestKey as W, isWorkbuddyBundle as X, isEncryptedFieldWrapper as Y, macosBundleExecutable as Z, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, authFileName as b, WORKBUDDY_CONNECT_VERSION as c, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, readAtRestKey as et, WorkBuddyCatalog as f, WORKBUDDY_AUTH_FILENAME as g, ENCRYPTED_CREDENTIAL_CODE as h, processStartTimeMs as i, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as j, workbuddyOwnAuthPath as k, piAiRuntimeInfo as l, fallbackModelsFor as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, deriveCatalog as p, findWorkbuddyAppExecutable as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, workbuddyAppExecutableCandidates as tt, FALLBACK_WORKBUDDY_MODELS as u, WorkBuddyCredentialStore as v, hasEncryptedCredentialFields as w, defaultDesktopAuthCandidates as x, WorkBuddyEncryptedCredentialError as y, parseUpstreamModel as z };
package/lib/index.d.ts CHANGED
@@ -435,7 +435,7 @@ declare class WorkBuddyCredentialStore {
435
435
  /** WorkBuddy region selected by the credential's login domain. */
436
436
  type WorkBuddyRegion = 'cn' | 'global';
437
437
  /** Upstream failure classes the shim maps onto distinct HTTP answers. */
438
- type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'not_found' | 'server' | 'client';
438
+ type UpstreamErrorKind = 'hard_credit' | 'soft_rate' | 'session_dead' | 'policy_reject' | 'not_found' | 'server' | 'client';
439
439
  /** Reasoning capability as the upstream catalog declares it. */
440
440
  interface WorkBuddyReasoning {
441
441
  supportedEfforts?: readonly string[];
@@ -532,6 +532,8 @@ type WorkBuddyChatResult = {
532
532
  status: number;
533
533
  kind: UpstreamErrorKind;
534
534
  message: string;
535
+ /** Structured fields lifted from a JSON failure body, when there were any. */
536
+ detail?: UpstreamErrorDetail | undefined;
535
537
  };
536
538
  /**
537
539
  * Chat answer that also carries the response headers.
@@ -554,6 +556,15 @@ interface WorkBuddyProbeAnswer {
554
556
  }
555
557
  /** Classify an upstream failure from its HTTP status and body excerpt. */
556
558
  declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
559
+ /** Structured fields lifted out of a JSON upstream failure body. */
560
+ interface UpstreamErrorDetail {
561
+ /** Numeric upstream error code, e.g. 11140 for a content-policy refusal. */
562
+ upstreamCode?: number;
563
+ /** Upstream request id — the handle support asks for. */
564
+ requestId?: string;
565
+ /** Best human-facing message: `displayMsg.zh`, falling back to `.en`, then `msg`. */
566
+ displayMsg?: string;
567
+ }
557
568
  /**
558
569
  * Region for a login domain; an empty domain means CN (matching upstream tooling).
559
570
  *
@@ -1170,7 +1181,7 @@ declare function macosNestedAppBundles(parent: string): string[];
1170
1181
  * The first candidate that exists as a file, or undefined when the desktop app
1171
1182
  * is not installed where this platform expects it.
1172
1183
  */
1173
- declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv): string | undefined;
1184
+ declare function findWorkbuddyAppExecutable(platform?: NodeJS.Platform, home?: string, env?: NodeJS.ProcessEnv, readRegistryAppPath?: () => string | undefined): string | undefined;
1174
1185
  /**
1175
1186
  * Ask the installed desktop app for its key payload by running its own binary
1176
1187
  * as plain Node (`ELECTRON_RUN_AS_NODE`) and calling the native binding.
@@ -1340,7 +1351,7 @@ declare const PROBE_SYSTEM_PROMPT = "You are a connectivity check. Reply with a
1340
1351
  * Output cap for a probe. One token is enough to prove the model answers.
1341
1352
  *
1342
1353
  * The probe's cost comes from its INPUT, not its output (see
1343
- * {@link PROBE_INPUT_TOKENS}), so paying for a long completion would add money
1354
+ * {@link DEFAULT_PROBE_INPUT_TOKENS}), so paying for a long completion would add money
1344
1355
  * without adding signal.
1345
1356
  */
1346
1357
  declare const PROBE_MAX_TOKENS = 1;
@@ -1367,8 +1378,27 @@ declare const PROBE_MAX_TOKENS = 1;
1367
1378
  * A probe that sent a handful of tokens answered "usable" while every real
1368
1379
  * request in a long conversation was refused — the probe was asking too small a
1369
1380
  * question. 25k is sized to be a realistic long-conversation payload.
1381
+ *
1382
+ * Renamed from `PROBE_INPUT_TOKENS` to say what it now IS: the DEFAULT, not the
1383
+ * only possible size. The size is a property of the user's own traffic (see
1384
+ * {@link probeRequestBody}), so it became a setting; this constant is what an
1385
+ * unset setting, an older profile, and every existing test mean by "a probe".
1386
+ */
1387
+ declare const DEFAULT_PROBE_INPUT_TOKENS = 25000;
1388
+ /**
1389
+ * The probe sizes the card offers, in tokens.
1390
+ *
1391
+ * A closed set, each step a meaningful position relative to the measured 20k~30k
1392
+ * threshold: below it (10k/20k) proves an account can serve SHORT requests and
1393
+ * is the cheap choice; at it (30k) is the size that finally covers the whole
1394
+ * measured window; above it (50k/100k) is for conversations that routinely run
1395
+ * long, where a smaller probe would report "usable" for an account that refuses
1396
+ * the very next message.
1397
+ *
1398
+ * Exported so the schema's description, the card's dropdown, and the coercion
1399
+ * below cannot drift apart into three different menus.
1370
1400
  */
1371
- declare const PROBE_INPUT_TOKENS = 25000;
1401
+ declare const PROBE_INPUT_TOKEN_CHOICES: readonly number[];
1372
1402
  /**
1373
1403
  * Build the request body for one probe.
1374
1404
  *
@@ -1376,10 +1406,13 @@ declare const PROBE_INPUT_TOKENS = 25000;
1376
1406
  * `prepareChatBody` enforces that too, but sending it explicitly keeps this
1377
1407
  * function's output valid on its own.
1378
1408
  *
1379
- * Measured on the built artifact: 112,713 bytes of body, and the upstream
1380
- * counted `prompt_tokens: 25023` at a cost of `credit: 0.72`.
1409
+ * Measured on the built artifact at the DEFAULT size: 112,713 bytes of body, and
1410
+ * the upstream counted `prompt_tokens: 25023` at a cost of `credit: 0.72`. The
1411
+ * body bytes and the credit both scale with the size asked for, which is why the
1412
+ * size is a user setting: a larger probe answers more truthfully for someone
1413
+ * whose conversations are long, and costs proportionally more to ask.
1381
1414
  */
1382
- declare function probeRequestBody(modelId: string): string;
1415
+ declare function probeRequestBody(modelId: string, inputTokens?: number): string;
1383
1416
  /**
1384
1417
  * How one probe ended.
1385
1418
  *
@@ -1387,7 +1420,7 @@ declare function probeRequestBody(modelId: string): string;
1387
1420
  * outcome the feature exists for: it means "this model works, but not right
1388
1421
  * now". Everything else is a real, actionable failure or a success.
1389
1422
  */
1390
- type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
1423
+ type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
1391
1424
  /**
1392
1425
  * One probe result, as the card renders it.
1393
1426
  *
@@ -1455,9 +1488,10 @@ declare function cooldownOf(input: {
1455
1488
  *
1456
1489
  * Built on `classifyUpstreamError` rather than re-testing status codes, so a
1457
1490
  * probe and a real chat request can never disagree about what the same upstream
1458
- * answer means. The extra split here is `credential-rejected`, which the
1491
+ * answer means. The extra splits here are `credential-rejected`, which the
1459
1492
  * upstream signals with a non-JSON 401/403 edge page and which needs completely
1460
- * different advice (re-auth, not "wait").
1493
+ * different advice (re-auth, not "wait"), and `policy-rejected`, a JSON 403
1494
+ * content-policy refusal where re-auth advice would be actively misleading.
1461
1495
  */
1462
1496
  declare function outcomeOfFailure(status: number, body: string): WorkBuddyProbeOutcome;
1463
1497
  /**
@@ -1497,12 +1531,35 @@ declare function probeModel(input: {
1497
1531
  credential: WorkBuddyCredential;
1498
1532
  modelId: string;
1499
1533
  nowMs: number;
1534
+ /**
1535
+ * How much input this probe sends, in tokens.
1536
+ *
1537
+ * Omitted means {@link DEFAULT_PROBE_INPUT_TOKENS}, so every existing caller
1538
+ * keeps the measured 25k and only the pool's user-facing "test" passes a
1539
+ * chosen size through.
1540
+ */
1541
+ inputTokens?: number;
1500
1542
  /** The region's next monthly quota refresh, when known. */
1501
1543
  quotaRefreshAtMs?: number;
1502
1544
  signal?: AbortSignal;
1503
1545
  }): Promise<WorkBuddyProbeResult>;
1504
1546
  //#endregion
1505
1547
  //#region src/account-pool.d.ts
1548
+ /**
1549
+ * Which of the pool's writers produced a measurement.
1550
+ *
1551
+ * `test-batch` — the user pressed "test" (or the check-in batch), so the
1552
+ * outcome is a deliberate measurement of the target model.
1553
+ * `live-request` — a real chat request failed and its failure was recorded so
1554
+ * the NEXT request starts from a measurement instead of rediscovering the
1555
+ * same wall.
1556
+ *
1557
+ * The distinction is diagnostic, not behavioural: both feed the same routing
1558
+ * and cooling logic. It exists so a user who watched an account measure `ok`
1559
+ * and later saw it limited can tell whether that was a later live failure
1560
+ * (expected) or an older value winning (a bug).
1561
+ */
1562
+ type WorkBuddyPoolProbeSource = 'test-batch' | 'live-request';
1506
1563
  /** One account's pool entry: what the picker needs, plus its last measurement. */
1507
1564
  interface WorkBuddyPoolAccount {
1508
1565
  /** Stable account id (the same one `WorkBuddyAccountChoice` carries). */
@@ -1522,6 +1579,26 @@ interface WorkBuddyPoolProbe {
1522
1579
  outcome: WorkBuddyProbeOutcome;
1523
1580
  /** When this measurement was taken, in epoch ms. */
1524
1581
  atMs: number;
1582
+ /**
1583
+ * WHAT WROTE THIS MEASUREMENT.
1584
+ *
1585
+ * Two sites write probes for the same account, and before this field they
1586
+ * were indistinguishable in the file — which made a real confusion
1587
+ * unanswerable: the user presses "test", sees an account measured `ok`, and
1588
+ * later finds it `rate-limited`. Nothing in the record said whether that was
1589
+ * a NEWER measurement (a live request really did hit the limit) or an OLDER
1590
+ * one somehow winning, and the file keeps no history to tell them apart.
1591
+ *
1592
+ * With the source recorded, that question has an answer on sight: a
1593
+ * `live-request` entry that replaced a `test-batch` result is the pool
1594
+ * working as designed (a live failure steers the next request away from the
1595
+ * account); two entries of the SAME source with the OLDER one winning would
1596
+ * be a genuine bug.
1597
+ *
1598
+ * ABSENT for any measurement written before this field existed, so old probe
1599
+ * files stay readable — an absent source means "unknown", never a guess.
1600
+ */
1601
+ source?: WorkBuddyPoolProbeSource;
1525
1602
  /**
1526
1603
  * When this account may be usable again, when the upstream stated a time.
1527
1604
  *
@@ -1902,7 +1979,7 @@ type WorkBuddyWebUsage =
1902
1979
  * here because this module is the node-free bridge and must not import the
1903
1980
  * Host-side probe module (which pulls in the credential type).
1904
1981
  */
1905
- type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'unavailable' | 'not-found' | 'failed';
1982
+ type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
1906
1983
  /** One model's probe result, as the card renders it. */
1907
1984
  interface WorkBuddyWebProbeResult {
1908
1985
  modelId: string;
@@ -1949,6 +2026,14 @@ interface WorkBuddyWebPoolAccount {
1949
2026
  probe?: {
1950
2027
  outcome: WorkBuddyWebProbeOutcome;
1951
2028
  atMs: number;
2029
+ /**
2030
+ * What wrote this measurement, so the card can explain a change the user
2031
+ * watched happen: `test-batch` is a deliberate test, `live-request` is a
2032
+ * real request that failed and overwrote it. ABSENT for records written
2033
+ * before the field existed — the card must then say "unknown" rather than
2034
+ * guess which of the two it was.
2035
+ */
2036
+ source?: 'test-batch' | 'live-request';
1952
2037
  /** Only when the upstream named a time; never invented locally. */
1953
2038
  retryAtMs?: number;
1954
2039
  /**
@@ -2018,6 +2103,14 @@ interface WorkBuddyWebPool {
2018
2103
  targetModelSource: 'preferred' | 'free' | 'none' | 'stale';
2019
2104
  /** The account ids the user has checked into this region's pool. */
2020
2105
  memberAccountIds?: readonly string[];
2106
+ /**
2107
+ * Tokens each test probe sends, as the user saved it; `0`/absent = default.
2108
+ *
2109
+ * The SAVED value, not the resolved one: the card needs to tell "the user
2110
+ * never chose a size" apart from "the user chose 25000", because the first
2111
+ * must keep showing the default option rather than a number they never picked.
2112
+ */
2113
+ probeInputTokens?: number;
2021
2114
  /**
2022
2115
  * The subset of {@link memberAccountIds} that resolves to a real local
2023
2116
  * sign-in — i.e. what a batch actually runs on.
@@ -2176,6 +2269,14 @@ interface WorkBuddyPoolDeps {
2176
2269
  preferences(region: WorkBuddyRegion): {
2177
2270
  enabled: boolean;
2178
2271
  targetModelId: string;
2272
+ /**
2273
+ * Tokens each test probe sends as saved; `0` when the user never chose.
2274
+ *
2275
+ * Optional so a caller that has not learnt about the setting still
2276
+ * satisfies this interface, and so its absence reads as "default" — the
2277
+ * same meaning the host gives an older profile.
2278
+ */
2279
+ probeInputTokens?: number;
2179
2280
  memberAccountIds: readonly string[];
2180
2281
  };
2181
2282
  /** One region's CHECKED accounts, plus their credits and last measurements. */
@@ -2332,6 +2433,23 @@ interface WorkBuddyPoolPreferences {
2332
2433
  enabled?: boolean;
2333
2434
  /** Model id to test; `''` means auto-pick a free model from this region. */
2334
2435
  targetModelId?: string;
2436
+ /**
2437
+ * How much input a probe sends, in tokens — the pool's biggest single knob.
2438
+ *
2439
+ * The upstream's rate limit (6004) fires on request SIZE, and the threshold
2440
+ * was measured to sit between 20k and 30k tokens. The probe has to be sized
2441
+ * near where the user's REAL conversations land: too small and it reports
2442
+ * "usable" while every long conversation is refused (the exact false positive
2443
+ * that made this knob necessary), too large and it costs real credit on every
2444
+ * batch AND excludes accounts that would have served the shorter requests the
2445
+ * user actually sends.
2446
+ *
2447
+ * A fixed value therefore cannot be right for everyone — the right size is a
2448
+ * property of the user's own traffic, which only they know. `undefined` keeps
2449
+ * the measured default ({@link DEFAULT_PROBE_INPUT_TOKENS}), so an existing
2450
+ * profile is unaffected.
2451
+ */
2452
+ probeInputTokens?: number;
2335
2453
  /**
2336
2454
  * The account ids checked into this region's pool.
2337
2455
  *
@@ -2486,4 +2604,4 @@ declare function legacyAttributionRegion(value: Config, accountsFor: (region: Wo
2486
2604
  */
2487
2605
  declare function apply(ctx: Context, config: Config): void;
2488
2606
  //#endregion
2489
- export { CREDENTIAL_REJECTED_CODE, Config, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKENS, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };
2607
+ export { CREDENTIAL_REJECTED_CODE, Config, DEFAULT_PROBE_INPUT_TOKENS, ENCRYPTED_CREDENTIAL_CODE, FALLBACK_WORKBUDDY_MODELS, FALLBACK_WORKBUDDY_MODELS_GLOBAL, NATIVE_MODALITY_BY_MODEL_ID, PROBE_INPUT_TOKEN_CHOICES, PROBE_MAX_TOKENS, PROBE_SYSTEM_PROMPT, type UpstreamErrorKind, WORKBUDDY_ACCOUNTS_REFRESH_PATH, WORKBUDDY_APP_EXECUTABLE_ENV, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CHECKIN_PATH, WORKBUDDY_CONNECT_VERSION, WORKBUDDY_GLOBAL_PROVIDER, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_MODELS_REFRESH_PATH, WORKBUDDY_PROBE_PATH, WORKBUDDY_PROVIDER, WORKBUDDY_PROVIDERS, WORKBUDDY_PROVIDER_DISPLAY_NAMES, WORKBUDDY_REGIONS, WORKBUDDY_REGION_PARAM, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_USAGE_PATH, type WorkBuddyAccountChoice, type WorkBuddyAdapter, type WorkBuddyAuthStatus, type WorkBuddyCandidateFailure, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, type WorkBuddyCredentialRecovery, WorkBuddyCredentialRejectedError, WorkBuddyCredentialStore, type WorkBuddyCreditPackage, type WorkBuddyCredits, WorkBuddyEncryptedCredentialError, type WorkBuddyEncryptedField, type WorkBuddyHostHeartbeat, type WorkBuddyModelInfo, type WorkBuddyNativeModality, WorkBuddyPoolPreferences, type WorkBuddyProbeClient, type WorkBuddyProbeOutcome, type WorkBuddyProbeResult, type WorkBuddyProbeStore, type WorkBuddyReasoning, type WorkBuddyRecoveryCandidate, type WorkBuddyRefreshOutcome, WorkBuddyRegionState, type WorkBuddyShim, type WorkBuddyStatusRouteOptions, type WorkBuddyStoreOptions, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, type WorkBuddyUsabilityProbeOptions, apply, asVolatile, authFileName, classifyUpstreamError, clearAtRestKeyCache, clearHostHeartbeat, cooldownOf, createAccountUsabilityProbe, createWorkBuddyAdapter, createWorkBuddyShim, creditOfStream, defaultDesktopAuthCandidates, defaultDesktopAuthDirs, defaultDesktopAuthPath, deriveAtRestKey, deriveAtRestKeyId, deriveCatalog, fallbackModelsFor, fetchAtRestKeyPayload, findWorkbuddyAppExecutable, hasEncryptedCredentialFields, imageDefaultFor, inject, isCredentialRejectedError, isEncryptedCredentialError, isEncryptedFieldWrapper, isHeartbeatProcessAlive, isWorkbuddyBundle, legacyAttributionRegion, legacyWorkbuddyOwnAuthPath, macosBundleExecutable, macosNestedAppBundles, name, nativeModalityOf, nextRegionEnabled, nextRegionSlots, openEncryptedField, outcomeOfFailure, parseCreditMultiplier, parseReasoning, parseRetryAfter, parseUpstreamModel, parseWorkBuddyAuth, prepareChatBody, probeModel, probeRequestBody, probeSucceeded, processStartTimeMs, readAtRestKey, readHostHeartbeat, regionCleared, regionEnabled, regionEnabledOf, regionOf, regionOfProvider, regionOfStatusUrl, regionStateOf, registerWorkBuddyStatusRoute, resolveCredentialRecovery, selectAccountFor, settingsNamespaceOf, toPersistedWorkBuddyModel, unwrapVolatileDeep, withWorkBuddyRegion, workBuddyDisplayName, workBuddyModelInput, workBuddyThinkingLevelMap, workBuddyWebStatus, workbuddyAccountId, workbuddyAppExecutableCandidates, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, writeHostHeartbeat };