dsh-workbuddy-connect 0.3.2 → 0.4.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.
@@ -3,6 +3,7 @@ import { homedir, release } from "node:os";
3
3
  import { basename, join } from "node:path";
4
4
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
5
5
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
6
+ import { randomBytes } from "node:crypto";
6
7
  import { execFileSync } from "node:child_process";
7
8
  //#region src/auth.ts
8
9
  /**
@@ -572,7 +573,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
572
573
  supportedEfforts: [
573
574
  "low",
574
575
  "high",
575
- "xhigh"
576
+ "max"
576
577
  ],
577
578
  defaultEffort: "high",
578
579
  canDisableThinking: true
@@ -618,6 +619,137 @@ var WorkBuddyCatalog = class {
618
619
  }
619
620
  };
620
621
  //#endregion
622
+ //#region src/probe.ts
623
+ /**
624
+ * The reasoning-effort probe: decide whether a model's `reasoning_effort`
625
+ * parameter is actually validated, and if so which canonical values it accepts.
626
+ *
627
+ * Implements `docs/reasoning-effort-probe-plan.md` §4. The order matters and is
628
+ * not an optimization:
629
+ *
630
+ * 1. **Baseline** (no `reasoning_effort`) proves the model, credential, and
631
+ * request shape work at all, so a later rejection can be attributed.
632
+ * 2. **Sentinel** (a fresh random, impossible-to-collide value) answers the one
633
+ * question a per-level sweep cannot: does the upstream validate the field?
634
+ * A model that accepts the sentinel answers 200 to *everything*, so its
635
+ * per-level results would be uniformly false positives.
636
+ * 3. **Levels**, only after the sentinel was refused.
637
+ *
638
+ * The result is an observation, never a capability claim. Even a fully
639
+ * successful sweep means "the upstream accepted these spellings", not "these
640
+ * spellings change how the model thinks".
641
+ *
642
+ * @module dsh-workbuddy-connect/probe
643
+ */
644
+ /**
645
+ * The canonical values a probe tests, in a fixed order.
646
+ *
647
+ * `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
648
+ * by policy — disabling thinking is a separate capability the upstream must
649
+ * declare through `canDisableThinking`, never something probing may infer.
650
+ */
651
+ const PROBE_EFFORT_CANDIDATES = [
652
+ "low",
653
+ "medium",
654
+ "high",
655
+ "xhigh",
656
+ "max"
657
+ ];
658
+ /** Prompt body used by every probe request; carries nothing user-specific. */
659
+ const PROBE_PROMPT = "ping";
660
+ /** Default sentinel: unmistakably non-canonical, different on every call. */
661
+ function randomSentinel() {
662
+ return `probe_sentinel_${randomBytes(12).toString("hex")}`;
663
+ }
664
+ /**
665
+ * The upstream's "this effort value is not supported" code, measured
666
+ * 2026-09-11 (plan §4.2). It is *not* treated as a permanent protocol promise:
667
+ * anything unrecognized degrades to `unknown` rather than to a capability
668
+ * conclusion.
669
+ */
670
+ const INVALID_EFFORT_CODE = "invalid_reasoning_effort";
671
+ /** Whether an attempt is an attributable rejection of the effort value. */
672
+ function isEffortRejection(attempt) {
673
+ return attempt.status === 400 && attempt.errorCode === INVALID_EFFORT_CODE;
674
+ }
675
+ /** Whether an attempt shows the upstream accepted the request and streamed. */
676
+ function isAcceptance(attempt) {
677
+ return attempt.status === 200 && attempt.streamed;
678
+ }
679
+ /** Why an attempt ended in `unknown`, phrased for a log line. */
680
+ function unknownReason(stage, attempt) {
681
+ const code = attempt.errorCode === void 0 ? "" : ` (${attempt.errorCode})`;
682
+ const detail = attempt.detail === void 0 ? "" : `: ${attempt.detail}`;
683
+ return `${stage} status ${attempt.status}${code}${detail}`;
684
+ }
685
+ /**
686
+ * Probe one model.
687
+ *
688
+ * `options.candidates` exists so tests can shorten the sweep; production always
689
+ * uses {@link PROBE_EFFORT_CANDIDATES}.
690
+ */
691
+ async function probeModel(options) {
692
+ const sentinel = options.sentinel ?? randomSentinel;
693
+ const candidates = options.candidates ?? PROBE_EFFORT_CANDIDATES;
694
+ const timeoutMs = options.timeoutMs ?? 3e4;
695
+ let requests = 0;
696
+ const attempt = async (effort) => {
697
+ requests += 1;
698
+ const controller = new AbortController();
699
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
700
+ try {
701
+ return await options.send(effort, controller.signal);
702
+ } catch (error) {
703
+ return {
704
+ status: 0,
705
+ streamed: false,
706
+ detail: `transport error: ${String(error)}`
707
+ };
708
+ } finally {
709
+ clearTimeout(timer);
710
+ }
711
+ };
712
+ const baseline = await attempt(void 0);
713
+ if (!isAcceptance(baseline)) return {
714
+ validation: "unknown",
715
+ efforts: [],
716
+ requests,
717
+ reason: unknownReason("baseline", baseline)
718
+ };
719
+ const sentinelAttempt = await attempt(sentinel());
720
+ if (isAcceptance(sentinelAttempt)) return {
721
+ validation: "non-validating",
722
+ efforts: [],
723
+ requests
724
+ };
725
+ if (!isEffortRejection(sentinelAttempt)) return {
726
+ validation: "unknown",
727
+ efforts: [],
728
+ requests,
729
+ reason: unknownReason("sentinel", sentinelAttempt)
730
+ };
731
+ const accepted = [];
732
+ for (const effort of candidates) {
733
+ const levelAttempt = await attempt(effort);
734
+ if (isAcceptance(levelAttempt)) {
735
+ accepted.push(effort);
736
+ continue;
737
+ }
738
+ if (isEffortRejection(levelAttempt)) continue;
739
+ return {
740
+ validation: "unknown",
741
+ efforts: [],
742
+ requests,
743
+ reason: unknownReason(`level ${effort}`, levelAttempt)
744
+ };
745
+ }
746
+ return {
747
+ validation: "validating",
748
+ efforts: accepted,
749
+ requests
750
+ };
751
+ }
752
+ //#endregion
621
753
  //#region src/upstream.ts
622
754
  const CN_CHAT_BASE = "https://copilot.tencent.com";
623
755
  const CN_BILLING_BASE = "https://www.codebuddy.cn";
@@ -1049,10 +1181,107 @@ var WorkBuddyUpstreamClient = class {
1049
1181
  accounts
1050
1182
  };
1051
1183
  }
1184
+ /**
1185
+ * One probe request: a real streaming chat call carrying the effort under
1186
+ * test.
1187
+ *
1188
+ * Shares `chatHeaders` with the normal chat path on purpose — the plan
1189
+ * forbids probing through anything but the plugin's own credential handling,
1190
+ * so a result describes what a real message would experience.
1191
+ *
1192
+ * The caller aborts as soon as a parseable event arrives; the body is never
1193
+ * assembled into an answer. `reasoning_effort` is omitted entirely (rather
1194
+ * than sent empty) when `effort` is undefined, so the baseline case is a
1195
+ * genuinely bare request.
1196
+ */
1197
+ async probeEffort(credential, model, effort, signal) {
1198
+ const payload = {
1199
+ model,
1200
+ stream: true,
1201
+ messages: [{
1202
+ role: "user",
1203
+ content: PROBE_PROMPT
1204
+ }],
1205
+ max_tokens: 1
1206
+ };
1207
+ if (effort !== void 0) payload["reasoning_effort"] = effort;
1208
+ let response;
1209
+ try {
1210
+ response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
1211
+ method: "POST",
1212
+ headers: {
1213
+ ...chatHeaders(credential),
1214
+ "Authorization": `Bearer ${credential.accessToken}`
1215
+ },
1216
+ body: JSON.stringify(payload),
1217
+ signal
1218
+ });
1219
+ } catch (error) {
1220
+ return {
1221
+ status: 0,
1222
+ streamed: false,
1223
+ detail: `transport error: ${String(error)}`
1224
+ };
1225
+ }
1226
+ if (!response.ok) {
1227
+ const text = (await response.text()).slice(0, ERROR_BODY_LIMIT);
1228
+ return {
1229
+ status: response.status,
1230
+ streamed: false,
1231
+ ...errorCodeOf(text)
1232
+ };
1233
+ }
1234
+ const streamed = await readFirstEvent(response);
1235
+ return {
1236
+ status: response.status,
1237
+ streamed
1238
+ };
1239
+ }
1052
1240
  };
1241
+ /** Pull `extError.code` out of an upstream error body, if it is shaped that way. */
1242
+ function errorCodeOf(text) {
1243
+ try {
1244
+ const parsed = JSON.parse(text);
1245
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1246
+ const extError = parsed["extError"];
1247
+ if (typeof extError === "object" && extError !== null && !Array.isArray(extError)) {
1248
+ const code = extError["code"];
1249
+ if (typeof code === "string") return {
1250
+ errorCode: code,
1251
+ detail: code
1252
+ };
1253
+ }
1254
+ }
1255
+ } catch {}
1256
+ return { detail: text.slice(0, 200) };
1257
+ }
1258
+ /**
1259
+ * Consume just enough of a streaming response to know it really streams.
1260
+ *
1261
+ * Returns true on the first chunk containing a data line. Cancels the body
1262
+ * afterwards; a stream that ends or errors before that counts as not streamed,
1263
+ * because an empty 200 is not evidence the effort was accepted.
1264
+ */
1265
+ async function readFirstEvent(response) {
1266
+ const body = response.body;
1267
+ if (body === null) return false;
1268
+ const reader = body.getReader();
1269
+ const decoder = new TextDecoder();
1270
+ try {
1271
+ for (;;) {
1272
+ const { done, value } = await reader.read();
1273
+ if (done) return false;
1274
+ if (decoder.decode(value, { stream: true }).includes("data:")) return true;
1275
+ }
1276
+ } catch {
1277
+ return false;
1278
+ } finally {
1279
+ await reader.cancel().catch(() => {});
1280
+ }
1281
+ }
1053
1282
  //#endregion
1054
1283
  //#region src/version.ts
1055
- const WORKBUDDY_CONNECT_VERSION = "0.3.2";
1284
+ const WORKBUDDY_CONNECT_VERSION = "0.4.0";
1056
1285
  //#endregion
1057
1286
  //#region src/host-heartbeat.ts
1058
1287
  /**
@@ -1195,4 +1424,4 @@ function isHeartbeatProcessAlive(heartbeat) {
1195
1424
  return startAtMs <= heartbeat.registeredAt;
1196
1425
  }
1197
1426
  //#endregion
1198
- export { workbuddyOwnAuthPath as S, WORKBUDDY_AUTH_FILE_ENV as _, readHostHeartbeat as a, defaultDesktopAuthPath as b, WORKBUDDY_CONNECT_VERSION as c, normalizeCredits as d, prepareChatBody as f, WORKBUDDY_AUTH_FILENAME as g, WorkBuddyCatalog as h, processStartTimeMs as i, WorkBuddyUpstreamClient as l, FALLBACK_WORKBUDDY_MODELS as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, regionOf as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, classifyUpstreamError as u, WorkBuddyCredentialStore as v, parseWorkBuddyAuth as x, defaultDesktopAuthCandidates as y };
1427
+ export { defaultDesktopAuthPath as C, defaultDesktopAuthCandidates as S, workbuddyOwnAuthPath as T, FALLBACK_WORKBUDDY_MODELS as _, readHostHeartbeat as a, WORKBUDDY_AUTH_FILE_ENV as b, WORKBUDDY_CONNECT_VERSION as c, normalizeCredits as d, prepareChatBody as f, randomSentinel as g, probeModel as h, processStartTimeMs as i, WorkBuddyUpstreamClient as l, PROBE_EFFORT_CANDIDATES as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, regionOf as p, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, classifyUpstreamError as u, WorkBuddyCatalog as v, parseWorkBuddyAuth as w, WorkBuddyCredentialStore as x, WORKBUDDY_AUTH_FILENAME as y };
package/lib/index.d.ts CHANGED
@@ -1,8 +1,66 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
+ import "@earendil-works/pi-ai";
2
3
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
3
4
  import { Context } from "@deepseek-ai/cordis";
4
5
  import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
5
6
  import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
7
+ //#region src/probe.d.ts
8
+ /**
9
+ * The canonical values a probe tests, in a fixed order.
10
+ *
11
+ * `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
12
+ * by policy — disabling thinking is a separate capability the upstream must
13
+ * declare through `canDisableThinking`, never something probing may infer.
14
+ */
15
+ declare const PROBE_EFFORT_CANDIDATES: readonly WorkBuddyEffort[];
16
+ /** Sentinel generator; injectable so tests get deterministic values. */
17
+ type SentinelFactory = () => string;
18
+ /** Default sentinel: unmistakably non-canonical, different on every call. */
19
+ declare function randomSentinel(): string;
20
+ /**
21
+ * One response as the probe sees it, split into the only distinctions the
22
+ * attribution rule needs.
23
+ */
24
+ interface ProbeAttempt {
25
+ /** HTTP status, or 0 for a transport failure. */
26
+ status: number;
27
+ /** True when a parseable SSE event arrived. */
28
+ streamed: boolean;
29
+ /** `extError.code` from a JSON error body, when present. */
30
+ errorCode?: string;
31
+ /** Free-form detail for logs; never shown as a capability claim. */
32
+ detail?: string;
33
+ }
34
+ /** How one attempt is performed; the caller owns credentials and HTTP. */
35
+ type ProbeSender = (effort: string | undefined, signal: AbortSignal) => Promise<ProbeAttempt>;
36
+ /** The outcome of probing one model. */
37
+ type ProbeOutcome = {
38
+ validation: 'validating';
39
+ efforts: readonly WorkBuddyEffort[];
40
+ requests: number;
41
+ } | {
42
+ validation: 'non-validating';
43
+ efforts: readonly [];
44
+ requests: number;
45
+ } | {
46
+ validation: 'unknown';
47
+ efforts: readonly [];
48
+ requests: number;
49
+ reason: string;
50
+ };
51
+ /**
52
+ * Probe one model.
53
+ *
54
+ * `options.candidates` exists so tests can shorten the sweep; production always
55
+ * uses {@link PROBE_EFFORT_CANDIDATES}.
56
+ */
57
+ declare function probeModel(options: {
58
+ send: ProbeSender;
59
+ sentinel?: SentinelFactory;
60
+ candidates?: readonly WorkBuddyEffort[];
61
+ timeoutMs?: number;
62
+ }): Promise<ProbeOutcome>;
63
+ //#endregion
6
64
  //#region src/upstream.d.ts
7
65
  /** WorkBuddy region selected by the credential's login domain. */
8
66
  type WorkBuddyRegion = 'cn' | 'global';
@@ -137,6 +195,20 @@ declare class WorkBuddyUpstreamClient {
137
195
  fetchModels(credential: WorkBuddyCredential): Promise<readonly WorkBuddyUpstreamModel[]>;
138
196
  /** POST the billing endpoint for the aggregated remaining credit. */
139
197
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
198
+ /**
199
+ * One probe request: a real streaming chat call carrying the effort under
200
+ * test.
201
+ *
202
+ * Shares `chatHeaders` with the normal chat path on purpose — the plan
203
+ * forbids probing through anything but the plugin's own credential handling,
204
+ * so a result describes what a real message would experience.
205
+ *
206
+ * The caller aborts as soon as a parseable event arrives; the body is never
207
+ * assembled into an answer. `reasoning_effort` is omitted entirely (rather
208
+ * than sent empty) when `effort` is undefined, so the baseline case is a
209
+ * genuinely bare request.
210
+ */
211
+ probeEffort(credential: WorkBuddyCredential, model: string, effort: string | undefined, signal: AbortSignal): Promise<ProbeAttempt>;
140
212
  }
141
213
  //#endregion
142
214
  //#region src/auth.d.ts
@@ -277,6 +349,93 @@ declare class WorkBuddyCatalog {
277
349
  set(models: readonly WorkBuddyModelInfo[]): void;
278
350
  }
279
351
  //#endregion
352
+ //#region src/probe-store.d.ts
353
+ /** Basename of the probe record inside the Harness home. */
354
+ declare const WORKBUDDY_PROBE_FILENAME = ".workbuddy-probe.json";
355
+ /**
356
+ * Whether the model's effort parameter is actually validated.
357
+ *
358
+ * - `validating`: the upstream rejected an unknown sentinel value, so a
359
+ * per-level answer is meaningful.
360
+ * - `non-validating`: the upstream accepted the sentinel, so it ignores or
361
+ * loosely coerces the parameter and no per-level answer can be trusted.
362
+ * - `unknown`: baseline or sentinel failed for an unrelated reason (auth,
363
+ * rate limit, transport, ambiguous error body). Not a negative claim.
364
+ */
365
+ type WorkBuddyProbeValidation = 'validating' | 'non-validating' | 'unknown';
366
+ /** One model's recorded observation. */
367
+ interface WorkBuddyProbeRecord {
368
+ /** Fingerprint of the catalog row this observation was made against. */
369
+ fingerprint: string;
370
+ validation: WorkBuddyProbeValidation;
371
+ /** Efforts verified as accepted; only ever non-empty for `validating`. */
372
+ efforts: readonly WorkBuddyEffort[];
373
+ /** When the probe ran, epoch milliseconds. */
374
+ probedAtMs: number;
375
+ /** Plugin version that produced the record. */
376
+ pluginVersion: string;
377
+ }
378
+ /** Plugin-owned probe record path inside the Harness home. */
379
+ declare function workbuddyProbePath(): string;
380
+ /**
381
+ * Fingerprint the catalog fields a probe depends on.
382
+ *
383
+ * Deliberately excludes display-only fields (`name`, `billing`, `contextWindow`)
384
+ * so a rename or a promo badge does not throw away a valid observation, and
385
+ * deliberately includes the whole reasoning object so any change to the
386
+ * declared shape re-probes.
387
+ */
388
+ declare function fingerprintModel(info: WorkBuddyModelInfo): string;
389
+ /** Options for {@link WorkBuddyProbeStore}. */
390
+ interface WorkBuddyProbeStoreOptions {
391
+ /** Explicit state-file path, overriding the `$DSH_HOME` default. */
392
+ path?: string;
393
+ /** Observation lifetime; defaults to 14 days. */
394
+ ttlMs?: number;
395
+ /** Plugin version stamped into new records. */
396
+ pluginVersion: string;
397
+ /** Clock injection for tests. */
398
+ now?: () => number;
399
+ }
400
+ /**
401
+ * The plugin's probe records: read once, written atomically, never trusted
402
+ * across a fingerprint change or past the TTL.
403
+ */
404
+ declare class WorkBuddyProbeStore {
405
+ private readonly path;
406
+ private readonly ttlMs;
407
+ private readonly pluginVersion;
408
+ private readonly now;
409
+ private records;
410
+ constructor(options: WorkBuddyProbeStoreOptions | string);
411
+ /** Resolved state-file path, for the CLI and tests. */
412
+ filePath(): string;
413
+ private load;
414
+ /**
415
+ * The usable record for a model, or `undefined` when there is none, it is
416
+ * expired, or it was taken against a different catalog row.
417
+ */
418
+ get(modelId: string, fingerprint: string): WorkBuddyProbeRecord | undefined;
419
+ /**
420
+ * Store one observation. Only a decisive answer (`validating` /
421
+ * `non-validating`) replaces an existing decisive record: a transient
422
+ * `unknown` must not erase knowledge the user already paid for.
423
+ */
424
+ set(modelId: string, record: WorkBuddyProbeRecord): void;
425
+ /** Drop every record; used by the card's explicit "clear" action. */
426
+ clear(): void;
427
+ /** Every record currently held, for status display. */
428
+ all(): Readonly<Record<string, WorkBuddyProbeRecord>>;
429
+ /** Build a record stamped with this store's clock and version. */
430
+ record(fingerprint: string, validation: WorkBuddyProbeValidation, efforts: readonly WorkBuddyEffort[]): WorkBuddyProbeRecord;
431
+ /**
432
+ * Write through a temporary file and rename, so a crash mid-write cannot
433
+ * leave a half-parsed document that reads as "no records" and silently drops
434
+ * every observation.
435
+ */
436
+ private persist;
437
+ }
438
+ //#endregion
280
439
  //#region src/shim.d.ts
281
440
  /** Minimal logger surface the plugin context already provides. */
282
441
  interface ShimLogger {
@@ -324,6 +483,11 @@ interface WorkBuddyAdapterOptions {
324
483
  catalog: WorkBuddyCatalog;
325
484
  /** Resolve the durable attachment service at request time, when present. */
326
485
  resolveAttachments?: () => AttachmentStore | undefined;
486
+ /**
487
+ * Look up a local probe observation for a model. Consulted only for rows the
488
+ * upstream left undeclared; absent means declared-set-only behavior.
489
+ */
490
+ observe?: (modelId: string) => WorkBuddyProbeRecord | undefined;
327
491
  }
328
492
  /** What {@link createWorkBuddyAdapter} hands back. */
329
493
  interface WorkBuddyAdapter {
@@ -345,6 +509,59 @@ interface WorkBuddyAdapter {
345
509
  */
346
510
  declare function createWorkBuddyAdapter(options: WorkBuddyAdapterOptions): WorkBuddyAdapter;
347
511
  //#endregion
512
+ //#region src/probe-service.d.ts
513
+ /** What the caller learns about a completed probe. */
514
+ type WorkBuddyProbeStatus = {
515
+ state: 'ok';
516
+ validation: WorkBuddyProbeRecord['validation'];
517
+ efforts: readonly string[];
518
+ requests: number;
519
+ } | {
520
+ state: 'unavailable';
521
+ reason: string;
522
+ };
523
+ /** Options for {@link WorkBuddyProbeService}. */
524
+ interface WorkBuddyProbeServiceOptions {
525
+ store: WorkBuddyProbeStore;
526
+ catalog: WorkBuddyCatalog;
527
+ credentials: WorkBuddyCredentialStore;
528
+ client: WorkBuddyUpstreamClient;
529
+ /** Whether probing is permitted at all; consulted before every sweep. */
530
+ consent: () => boolean;
531
+ sentinel?: SentinelFactory;
532
+ /** Injectable for tests; defaults to the live upstream sender. */
533
+ send?: (modelId: string) => ProbeSender;
534
+ }
535
+ /**
536
+ * Serial probe runner. One instance is shared by the manual API and any
537
+ * future automatic trigger, so the two can never overlap.
538
+ */
539
+ declare class WorkBuddyProbeService {
540
+ private readonly options;
541
+ private queue;
542
+ private readonly pending;
543
+ private running;
544
+ constructor(options: WorkBuddyProbeServiceOptions);
545
+ /** Whether a sweep is in flight right now. */
546
+ isRunning(): boolean;
547
+ /**
548
+ * The record the adapter may use for this model, or `undefined`.
549
+ *
550
+ * Applies the plan's precedence (§5): a declared set always wins, so a model
551
+ * that declares `supportedEfforts` is never answered from an observation.
552
+ */
553
+ recordFor(modelId: string): WorkBuddyProbeRecord | undefined;
554
+ /**
555
+ * Probe one model, serially.
556
+ *
557
+ * The authenticated manual route supplies one-request consent after UI
558
+ * confirmation. Other callers must pass the configured consent gate.
559
+ * Manual consent never changes the automatic-probing configuration.
560
+ * Explicit requests bypass historical results, but share an ongoing run.
561
+ */
562
+ probe(modelId: string, manualConsent?: boolean): Promise<WorkBuddyProbeStatus>;
563
+ }
564
+ //#endregion
348
565
  //#region src/host-heartbeat.d.ts
349
566
  /**
350
567
  * Host-side heartbeat: a small JSON file written under `$DSH_HOME` once the
@@ -432,6 +649,12 @@ declare const WORKBUDDY_SETTINGS_NS: SettingsNamespace;
432
649
  interface Config {
433
650
  /** Explicit WorkBuddy desktop auth-file path, overriding env and platform defaults. */
434
651
  authFile?: string;
652
+ /**
653
+ * Whether the user has authorized sending probe requests about reasoning
654
+ * efforts. Off by default: a probe spends real credit, so nothing is sent
655
+ * until the user explicitly agrees.
656
+ */
657
+ probeConsent?: boolean;
435
658
  }
436
659
  declare const Config: z<Config>;
437
660
  /**
@@ -442,4 +665,4 @@ declare const Config: z<Config>;
442
665
  */
443
666
  declare function apply(ctx: Context, config: Config): void;
444
667
  //#endregion
445
- export { Config, FALLBACK_WORKBUDDY_MODELS, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, processStartTimeMs, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath };
668
+ export { Config, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, type ProbeAttempt, type ProbeOutcome, type ProbeSender, type UpstreamErrorKind, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_SETTINGS_NS, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, type WorkBuddyAdapter, type WorkBuddyAuthStatus, WorkBuddyCatalog, type WorkBuddyChatResult, type WorkBuddyCredential, WorkBuddyCredentialStore, type WorkBuddyCredits, type WorkBuddyEffort, type WorkBuddyHostHeartbeat, type WorkBuddyModelBilling, type WorkBuddyModelInfo, type WorkBuddyModelReasoning, type WorkBuddyProbeRecord, WorkBuddyProbeService, type WorkBuddyProbeStatus, WorkBuddyProbeStore, type WorkBuddyProbeValidation, type WorkBuddyRefreshOutcome, type WorkBuddyShim, WorkBuddyUpstreamClient, type WorkBuddyUpstreamModel, apply, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, fingerprintModel, inject, isHeartbeatProcessAlive, name, normalizeCredits, parseWorkBuddyAuth, prepareChatBody, probeModel, processStartTimeMs, randomSentinel, readHostHeartbeat, regionOf, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath };