dsh-workbuddy-bridge 0.1.0 → 0.3.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as normalizeCredits, B as FALLBACK_CN_APP_VERSION, C as desktopAuthCandidatesFor, D as cnAppDiscovery, E as WorkBuddyAtRestKeyProvider, F as PROBE_EFFORT_CANDIDATES, G as validCliVersion, H as fallbackChatIdentity, I as probeModel, J as installedAppVersion, K as WORKBUDDY_APP_VERSION_FILENAME, L as randomSentinel, M as prepareChatBody, N as prepareInternationalChatBody, O as WorkBuddyUpstreamClient, P as regionOf, R as classifyUpstreamError, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as readCliVersion, V as chatUserAgent, W as resolveChatIdentity, X as resolveAppVersion, Y as readBundleVersion, Z as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_BRIDGE_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as parseModelCatalog, k as modelWithCurrentPromotion, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as appUserAgent, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as CN_APP_VERSION_FILENAME } from "./variants-CFO_LIBm.js";
1
+ import { A as modelWithCurrentPromotion, B as CN_APP_VERSION_FILENAME, C as defaultDesktopAuthPath, D as WorkBuddyAtRestKeyProvider, E as workbuddyOwnAuthPath, F as regionOf, G as resolveChatIdentity, H as chatUserAgent, I as PROBE_EFFORT_CANDIDATES, J as appUserAgent, K as validCliVersion, L as probeModel, M as parseModelCatalog, N as prepareChatBody, O as cnAppDiscovery, P as prepareInternationalChatBody, Q as validAppVersion, R as randomSentinel, S as defaultDesktopAuthCandidates, T as parseWorkBuddyAuth, U as fallbackChatIdentity, V as FALLBACK_CN_APP_VERSION, W as readCliVersion, X as readBundleVersion, Y as installedAppVersion, Z as resolveAppVersion, _ as FALLBACK_WORKBUDDY_MODELS, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WORKBUDDY_AUTH_FILE_ENV, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_BRIDGE_VERSION, g as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as normalizeCredits, k as WorkBuddyUpstreamClient, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, p as WORKBUDDY_CREDIT_PATH, q as WORKBUDDY_APP_VERSION_FILENAME, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WorkBuddyCatalog, w as desktopAuthCandidatesFor, x as WorkBuddyCredentialStore, y as WORKBUDDY_AUTH_FILENAME, z as classifyUpstreamError } from "./variants-3SmUZ0e_.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -10,6 +10,7 @@ import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
10
10
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
11
11
  import { createServer } from "node:http";
12
12
  import { Readable } from "node:stream";
13
+ import { StringDecoder } from "node:string_decoder";
13
14
  //#region src/catalog/store.ts
14
15
  /**
15
16
  * The last catalog that actually loaded, kept per variant and per account.
@@ -265,6 +266,44 @@ var WorkBuddyVisibilityStore = class {
265
266
  }
266
267
  };
267
268
  //#endregion
269
+ //#region src/probe/efforts.ts
270
+ /** The resolution for a model that does not reason: no switch, nothing to detect. */
271
+ const NO_SWITCH = {
272
+ efforts: [],
273
+ source: "none",
274
+ detectable: false,
275
+ canDisable: false
276
+ };
277
+ /**
278
+ * Resolve one model's thinking-level switch.
279
+ *
280
+ * @param info - the catalog row, whose `reasoning` block carries any declaration.
281
+ * @param observed - this account's recorded detection for the model, if any.
282
+ */
283
+ function resolveEfforts(info, observed) {
284
+ const reasoning = info.reasoning;
285
+ if (reasoning === void 0 || reasoning.supports !== true) return NO_SWITCH;
286
+ const declared = reasoning.supportedEfforts;
287
+ if (declared !== void 0 && declared.length > 0) return {
288
+ efforts: declared,
289
+ source: "declared",
290
+ detectable: false,
291
+ canDisable: reasoning.canDisableThinking === true
292
+ };
293
+ if (observed?.validation === "validating" && observed.efforts.length > 0) return {
294
+ efforts: observed.efforts,
295
+ source: "observed",
296
+ detectable: true,
297
+ canDisable: false
298
+ };
299
+ return {
300
+ efforts: [],
301
+ source: "none",
302
+ detectable: true,
303
+ canDisable: false
304
+ };
305
+ }
306
+ //#endregion
268
307
  //#region src/llm/adapter.ts
269
308
  /**
270
309
  * The `workbuddy` pi-ai provider: one loopback-backed adapter registered
@@ -361,44 +400,28 @@ function withCatalogDisplay(name, info) {
361
400
  return suffix === void 0 ? name : `${name}${RATE_SEPARATOR}${suffix}`;
362
401
  }
363
402
  /**
364
- * Resolve a WorkBuddy model's reasoning capability into pi-ai's
403
+ * Translate a model's resolved thinking-level switch into pi-ai's
365
404
  * `thinkingLevelMap` (every level pinned to its wire spelling or `null` for
366
405
  * unsupported), mirroring `dsh-llm-pi-ai`'s own `resolveModelReasoning`.
367
406
  *
368
- * Two sources, strictly ordered (`docs/reasoning-effort-probe-plan.md` §5):
369
- *
370
- * 1. **The declared set.** When the upstream declares a non-empty
371
- * `supportedEfforts`, exactly those values are offered and nothing else.
372
- * This always wins: an observation never widens or narrows a declared set.
373
- * 2. **A local observation.** Rows without a declared set (the older
374
- * `{effort, summary}` shape) normally get no control at all — their
375
- * selectable set is client-side knowledge the catalog does not carry, and
376
- * the desktop app differs per model there. If the user authorized a probe
377
- * and it established that the upstream *validates* the parameter, the
378
- * verified spellings are offered.
379
- *
380
- * A `non-validating` observation deliberately yields no control: the upstream
381
- * accepts values that cannot exist (measured on `glm-5.2`), so every per-level
382
- * acceptance it produced would be a false positive.
383
- *
384
- * `off` is offered only when the upstream declares `canDisableThinking: true`.
385
- * It is never probed — disabling thinking is a separate capability, and the
386
- * per-model acceptance of `off` cannot be inferred from the row's shape.
407
+ * The *decision* of which levels a model offers is not made here — it lives in
408
+ * {@link resolveEfforts}, which the status document reads too. That sharing is
409
+ * the point: the composer's bulb is lit from the same answer the picker is
410
+ * built from, so a model cannot show a control in one place and none in the
411
+ * other. This function only reshapes that answer for pi-ai.
387
412
  *
388
413
  * The offered set is described internally as "verified accepted", never as
389
414
  * "verified effective": acceptance proves the upstream did not reject the
390
415
  * spelling, not that it changes what the model does.
391
416
  */
392
417
  function reasoningFields(info, observed) {
393
- const reasoning = info.reasoning;
394
- if (reasoning === void 0 || reasoning.supports !== true) return { reasoning: false };
395
- const declared = reasoning.supportedEfforts;
396
- const efforts = declared !== void 0 && declared.length > 0 ? declared : observed?.validation === "validating" && observed.efforts.length > 0 ? observed.efforts : void 0;
397
- if (efforts === void 0) return { reasoning: false };
418
+ const resolved = resolveEfforts(info, observed);
419
+ if (resolved.efforts.length === 0) return { reasoning: false };
420
+ const efforts = resolved.efforts;
398
421
  return {
399
422
  reasoning: true,
400
423
  thinkingLevelMap: {
401
- off: reasoning.canDisableThinking === true && declared !== void 0 && declared.length > 0 ? "off" : null,
424
+ off: resolved.canDisable ? "off" : null,
402
425
  minimal: null,
403
426
  low: efforts.includes("low") ? "low" : null,
404
427
  medium: efforts.includes("medium") ? "medium" : null,
@@ -604,6 +627,67 @@ function originIsLoopback(origin) {
604
627
  * @module dsh-workbuddy-bridge/shim
605
628
  */
606
629
  const REQUEST_BODY_LIMIT = 67108864;
630
+ /**
631
+ * Pull the response id and `credit` out of one SSE `data:` payload.
632
+ *
633
+ * WorkBuddy's OpenAI-compatible stream reports what a message cost as an extra
634
+ * `credit` member beside the standard token buckets on the final usage frame
635
+ * (measured: `glm-5.3` answering 900 tokens reported `credit: 0.51`). The
636
+ * member is not part of the OpenAI schema, so it is read defensively off the
637
+ * parsed object rather than through any typed shape, and every frame without a
638
+ * usable number is skipped.
639
+ *
640
+ * @param payload - the text after `data: ` on one frame.
641
+ * @returns the response id and credit, or undefined when this frame carries none.
642
+ */
643
+ function readCreditFrame(payload) {
644
+ if (payload === "" || payload.startsWith("[DONE]")) return void 0;
645
+ let parsed;
646
+ try {
647
+ parsed = JSON.parse(payload);
648
+ } catch {
649
+ return;
650
+ }
651
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
652
+ const frame = parsed;
653
+ const id = typeof frame["id"] === "string" ? frame["id"] : "";
654
+ if (id === "") return void 0;
655
+ const usage = frame["usage"];
656
+ if (typeof usage !== "object" || usage === null || Array.isArray(usage)) return void 0;
657
+ const credit = usage["credit"];
658
+ if (typeof credit !== "number" || !Number.isFinite(credit) || credit < 0) return void 0;
659
+ return {
660
+ id,
661
+ credit
662
+ };
663
+ }
664
+ /**
665
+ * Split a raw chunk into complete SSE `data:` payloads, carrying the trailing
666
+ * partial line into the next chunk.
667
+ *
668
+ * A TCP read boundary lands wherever it likes — mid-JSON, mid-line — so
669
+ * scanning each chunk on its own would parse a truncated object and drop the
670
+ * very frame that carries the cost. The remainder is returned for the caller to
671
+ * prepend.
672
+ *
673
+ * @param carry - bytes left over from the previous chunk.
674
+ * @param chunk - the bytes just read.
675
+ * @returns the complete payloads, and the new carry.
676
+ */
677
+ function readCreditChunk(carry, chunk) {
678
+ const lines = (carry + chunk).split("\n");
679
+ const carryOver = lines.pop() ?? "";
680
+ const payloads = [];
681
+ for (const line of lines) {
682
+ const trimmed = line.trimEnd();
683
+ if (!trimmed.startsWith("data:")) continue;
684
+ payloads.push(trimmed.slice(5).trim());
685
+ }
686
+ return {
687
+ payloads,
688
+ carry: carryOver
689
+ };
690
+ }
607
691
  /** Chat-completion POSTs must carry a JSON body type (simple-request CSRF drops here). */
608
692
  function isJsonContentType(req) {
609
693
  const type = req.headers["content-type"];
@@ -656,7 +740,7 @@ function readBody$1(req) {
656
740
  * is the boundary, and the upstream credential comes from the store alone.
657
741
  */
658
742
  function createWorkBuddyShim(options) {
659
- const { store, client, catalog } = options;
743
+ const { store, client, catalog, observeCredit } = options;
660
744
  const logger = options.logger;
661
745
  const SHARED_SECRET = randomBytes(32).toString("base64url");
662
746
  /** Constant-time bearer check; absent or mismatched bearers are rejected. */
@@ -750,18 +834,50 @@ function createWorkBuddyShim(options) {
750
834
  res.writeHead(200, {
751
835
  "Content-Type": "text/event-stream",
752
836
  "Cache-Control": "no-cache",
753
- "Connection": "keep-alive",
837
+ Connection: "keep-alive",
754
838
  "X-Accel-Buffering": "no"
755
839
  });
756
840
  let sawDone = false;
841
+ /**
842
+ * Bytes of the frame currently being read.
843
+ *
844
+ * The cost arrives on the last usage frame, and that frame is exactly the
845
+ * one a TCP read boundary is most likely to split: parsing each chunk in
846
+ * isolation would drop it. Complete lines are accumulated into `carry` and
847
+ * only whole `data:` payloads are inspected.
848
+ */
849
+ let carry = "";
850
+ /** The last cost observed for this response; reported once, at the end. */
851
+ let observed;
852
+ /**
853
+ * Decodes the byte stream without splitting a multi-byte character across
854
+ * chunks.
855
+ *
856
+ * `chunk.toString('utf8')` would turn a character straddling a read
857
+ * boundary into U+FFFD, and since answers routinely contain Chinese that is
858
+ * not hypothetical. A mangled frame fails `JSON.parse` and is dropped —
859
+ * which for the frame carrying the cost would mean a message silently
860
+ * losing its label.
861
+ */
862
+ const decoder = new StringDecoder("utf8");
757
863
  const body = Readable.fromWeb(result.response.body);
758
864
  body.on("data", (chunk) => {
759
865
  if (chunk.includes("[DONE]")) sawDone = true;
866
+ if (observeCredit === void 0) return;
867
+ const read = readCreditChunk(carry, decoder.write(chunk));
868
+ carry = read.carry;
869
+ for (const payload of read.payloads) {
870
+ const frame = readCreditFrame(payload);
871
+ if (frame !== void 0) observed = frame;
872
+ }
760
873
  });
761
874
  body.on("error", (error) => {
762
875
  logger?.warn("dsh-workbuddy-bridge: upstream stream failed mid-flight", error);
763
876
  if (!sawDone && res.writable) res.end("data: [DONE]\n\n");
764
877
  });
878
+ body.on("end", () => {
879
+ if (observeCredit !== void 0 && observed !== void 0) observeCredit(observed.id, observed.credit);
880
+ });
765
881
  body.pipe(res);
766
882
  }
767
883
  return {
@@ -776,16 +892,226 @@ function createWorkBuddyShim(options) {
776
892
  };
777
893
  }
778
894
  //#endregion
895
+ //#region src/llm/credit-log.ts
896
+ /**
897
+ * Per-message credit accounting, as two plain registries the Host half fills.
898
+ *
899
+ * WorkBuddy reports what a message actually cost as a `credit` field on the
900
+ * last usage frame of its OpenAI-compatible SSE stream. That number is
901
+ * **server-supplied**: the desktop app accumulates `cost.amount` stage
902
+ * increments and never multiplies tokens by the catalog's `x0.79` multiplier
903
+ * (the multiplier is display copy). So the plugin must not invent a formula —
904
+ * it observes the number where it exists.
905
+ *
906
+ * The field travels on the stream the shim already proxies, and the same
907
+ * stream carries the upstream response id (`cmb-…`) that DSH records on the
908
+ * assistant message as `source.replayState.response.responseId`. That pair is
909
+ * what makes attribution exact rather than guessed:
910
+ *
911
+ * ```text
912
+ * SSE id + SSE usage.credit ──► creditLog.record(id, credit)
913
+ * │
914
+ * session/event assistant/message ─────┴──► index.set(sessionId, messageId, credit)
915
+ * (messageId, responseId) │
916
+ * browser row fetches by messageId
917
+ * ```
918
+ *
919
+ * Both structures are pure data with no Host dependencies, so the matching
920
+ * rules are unit-testable without a Session, a socket, or an upstream.
921
+ *
922
+ * @module dsh-workbuddy-bridge/credit-log
923
+ */
924
+ /** How many response ids one log remembers before dropping the oldest. */
925
+ const DEFAULT_LOG_LIMIT = 512;
926
+ /**
927
+ * Bounded `responseId → credit` registry, with one-shot waiters.
928
+ *
929
+ * The waiter exists because the two writes race in principle: the shim sees the
930
+ * credit when the last SSE frame arrives, while the Session event that names
931
+ * the response id is appended when the stream settles. Either order must
932
+ * produce the same attribution, so a lookup that misses registers interest and
933
+ * is answered by the next `record` instead of silently reporting "unknown".
934
+ */
935
+ var WorkBuddyCreditLog = class {
936
+ limit;
937
+ /**
938
+ * Insertion-ordered, so eviction is FIFO. A `Map` is used rather than an
939
+ * object because the response id is attacker-influenced text: a plain object
940
+ * would let `__proto__` reach the prototype chain.
941
+ */
942
+ credits = /* @__PURE__ */ new Map();
943
+ /** Pending attributions, keyed by response id; each resolves at most once. */
944
+ waiters = /* @__PURE__ */ new Map();
945
+ constructor(limit = DEFAULT_LOG_LIMIT) {
946
+ this.limit = limit;
947
+ }
948
+ /**
949
+ * Remember what one response cost.
950
+ *
951
+ * A non-finite or negative value is refused rather than stored: the field is
952
+ * remote input, and a `NaN` reaching the browser would render as `NaN` in the
953
+ * credit label instead of being reported as unknown.
954
+ */
955
+ record(responseId, credit) {
956
+ if (responseId === "") return;
957
+ if (!Number.isFinite(credit) || credit < 0) return;
958
+ this.credits.delete(responseId);
959
+ this.credits.set(responseId, credit);
960
+ this.evict();
961
+ const waiter = this.waiters.get(responseId);
962
+ if (waiter !== void 0) {
963
+ this.waiters.delete(responseId);
964
+ waiter(credit);
965
+ }
966
+ }
967
+ /** What one response cost, or undefined when it was never observed. */
968
+ lookup(responseId) {
969
+ return this.credits.get(responseId);
970
+ }
971
+ /**
972
+ * Call `onCredit` once, when this response's credit becomes known.
973
+ *
974
+ * @returns a canceller; calling it after the credit landed is a no-op.
975
+ */
976
+ await(responseId, onCredit) {
977
+ const known = this.credits.get(responseId);
978
+ if (known !== void 0) {
979
+ onCredit(known);
980
+ return () => {};
981
+ }
982
+ this.waiters.set(responseId, onCredit);
983
+ return () => {
984
+ if (this.waiters.get(responseId) === onCredit) this.waiters.delete(responseId);
985
+ };
986
+ }
987
+ /** Drop the oldest entries until the log is inside its bound. */
988
+ evict() {
989
+ while (this.credits.size > this.limit) {
990
+ const oldest = this.credits.keys().next();
991
+ if (oldest.done === true) return;
992
+ this.credits.delete(oldest.value);
993
+ }
994
+ }
995
+ };
996
+ /**
997
+ * `sessionId → messageId → credit`, the shape the browser row reads.
998
+ *
999
+ * Nested rather than a flat `messageId → credit` map so the route can answer
1000
+ * one Session without shipping every Session's accounting, and so a Session's
1001
+ * rows can be dropped as a unit.
1002
+ */
1003
+ var WorkBuddySessionCreditIndex = class {
1004
+ perSessionLimit;
1005
+ /** Per-Session maps, each bounded independently. */
1006
+ sessions = /* @__PURE__ */ new Map();
1007
+ constructor(perSessionLimit = DEFAULT_LOG_LIMIT) {
1008
+ this.perSessionLimit = perSessionLimit;
1009
+ }
1010
+ /** Attribute one message's cost, refreshing its position in its Session. */
1011
+ set(sessionId, messageId, credit) {
1012
+ if (sessionId === "" || messageId === "") return;
1013
+ if (!Number.isFinite(credit) || credit < 0) return;
1014
+ let messages = this.sessions.get(sessionId);
1015
+ if (messages === void 0) {
1016
+ messages = /* @__PURE__ */ new Map();
1017
+ this.sessions.set(sessionId, messages);
1018
+ }
1019
+ messages.delete(messageId);
1020
+ messages.set(messageId, credit);
1021
+ while (messages.size > this.perSessionLimit) {
1022
+ const oldest = messages.keys().next();
1023
+ if (oldest.done === true) break;
1024
+ messages.delete(oldest.value);
1025
+ }
1026
+ }
1027
+ /**
1028
+ * One Session's accounting as a plain object, or an empty object when the
1029
+ * Session has none. Always an object, never undefined: the browser half then
1030
+ * needs no "no data yet" branch distinct from "no credits recorded".
1031
+ */
1032
+ forSession(sessionId) {
1033
+ const messages = this.sessions.get(sessionId);
1034
+ if (messages === void 0) return {};
1035
+ return Object.fromEntries(messages);
1036
+ }
1037
+ /** Forget one Session's accounting, e.g. when it leaves the store. */
1038
+ delete(sessionId) {
1039
+ this.sessions.delete(sessionId);
1040
+ }
1041
+ };
1042
+ /**
1043
+ * Read the upstream response id out of an assistant message's replay state.
1044
+ *
1045
+ * `replayState` is declared `unknown` on the LLM seam on purpose: it is
1046
+ * adapter-private state that only the owning adapter may interpret. This walks
1047
+ * it structurally and gives up quietly on anything unexpected, reading only the
1048
+ * pi-ai envelope's `response.responseId` and only when it is a non-empty string.
1049
+ *
1050
+ * A missing id is the normal case for a non-WorkBuddy provider and for a
1051
+ * message assembled from a stream that never carried one. It means "cannot
1052
+ * attribute", never "costs nothing" -- which is why every caller treats
1053
+ * `undefined` as a reason to do nothing rather than to record a zero.
1054
+ *
1055
+ * @param replayState - the message source's adapter-private state.
1056
+ * @returns the upstream response id, or undefined when there is none to read.
1057
+ */
1058
+ function responseIdOf(replayState) {
1059
+ if (typeof replayState !== "object" || replayState === null) return void 0;
1060
+ const response = replayState["response"];
1061
+ if (typeof response !== "object" || response === null) return void 0;
1062
+ const responseId = response["responseId"];
1063
+ return typeof responseId === "string" && responseId !== "" ? responseId : void 0;
1064
+ }
1065
+ /**
1066
+ * Attribute one observed response cost to the message that caused it.
1067
+ *
1068
+ * The two halves of the key are produced by different layers and in either
1069
+ * order, so this is a join rather than a straight read:
1070
+ *
1071
+ * - the **shim** sees `usage.credit` on the stream and knows the upstream
1072
+ * response id, but nothing about DSH's message ids;
1073
+ * - the **session log** records the assistant message with its DSH `messageId`
1074
+ * and, inside `source.replayState.response.responseId`, that same id.
1075
+ *
1076
+ * The event normally lands *after* the stream's final frame (the message is
1077
+ * appended once the stream settles), so the log usually answers immediately.
1078
+ * The waiter covers the opposite interleaving instead of leaving that message
1079
+ * permanently unlabelled.
1080
+ *
1081
+ * @param log - the variant's response-id to credit registry.
1082
+ * @param index - the shared session to message to credit index.
1083
+ * @param sessionId - the Session the event came from.
1084
+ * @param data - the event's data payload, read structurally.
1085
+ * @returns whether an attribution was made or armed.
1086
+ */
1087
+ function attributeCredit(log, index, sessionId, data) {
1088
+ const message = data.message;
1089
+ const messageId = message?.id;
1090
+ if (typeof messageId !== "string" || messageId === "") return false;
1091
+ const responseId = responseIdOf(message?.source?.replayState);
1092
+ if (responseId === void 0) return false;
1093
+ const known = log.lookup(responseId);
1094
+ if (known !== void 0) {
1095
+ index.set(sessionId, messageId, known);
1096
+ return true;
1097
+ }
1098
+ log.await(responseId, (credit) => {
1099
+ index.set(sessionId, messageId, credit);
1100
+ });
1101
+ return true;
1102
+ }
1103
+ //#endregion
779
1104
  //#region src/probe/store.ts
780
1105
  /**
781
1106
  * Local record of reasoning-effort probes.
782
1107
  *
783
1108
  * What this stores is an *observation*, never a claim about the upstream: a
784
1109
  * model's row is only consulted when the catalog carries no explicit
785
- * `supportedEfforts` set, and it always loses to a declared set. The plan this
786
- * implements (`docs/reasoning-effort-probe-plan.md` §5) requires that a result
787
- * is invalidated whenever the model's catalog row changes, so every record
788
- * carries a fingerprint of the fields the probe depended on.
1110
+ * `supportedEfforts` set, and it always loses to a declared set. Every record
1111
+ * carries a fingerprint of the fields the probe depended on, so a result is
1112
+ * invalidated whenever the model's catalog row changes — the precedence rules
1113
+ * are asserted by `tests/probe-store.spec.ts`, and the fingerprint's own input
1114
+ * key order is a compatibility contract (`docs/ARCHITECTURE.md`).
789
1115
  *
790
1116
  * The file lives beside the plugin's own credential copy under `$DSH_HOME`,
791
1117
  * never in the desktop app's files, and carries no token, prompt, or response
@@ -980,17 +1306,6 @@ var WorkBuddyProbeStore = class {
980
1306
  } catch {}
981
1307
  }
982
1308
  };
983
- /**
984
- * Order observations newest-first for display.
985
- *
986
- * The store keeps insertion order so the file reads chronologically, but the
987
- * card wants the most recent detection at the top: a sweep the user just ran
988
- * should not appear below every earlier one, which is what appending to an
989
- * insertion-ordered list does.
990
- */
991
- function newestFirst(records) {
992
- return [...records].sort((a, b) => b.probedAt - a.probedAt);
993
- }
994
1309
  //#endregion
995
1310
  //#region src/probe/service.ts
996
1311
  /**
@@ -1120,7 +1435,7 @@ var WorkBuddyProbeService = class {
1120
1435
  function safeMessage(error) {
1121
1436
  return (error instanceof Error ? error.message : String(error)).replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[redacted token]").replace(/(\b(?:code|token|refresh_token|access_token)=)[^&\s]+/giu, "$1[redacted]").slice(0, 500);
1122
1437
  }
1123
- function json$1(res, status, body) {
1438
+ function json$2(res, status, body) {
1124
1439
  const payload = JSON.stringify(body);
1125
1440
  res.writeHead(status, {
1126
1441
  "Content-Type": "application/json",
@@ -1134,7 +1449,7 @@ function json$1(res, status, body) {
1134
1449
  * DNS-rebinding pages (their Host is the attacker's domain, not loopback);
1135
1450
  * the card's same-origin fetches carry no Origin and pass on Host alone.
1136
1451
  */
1137
- function loopbackRequest(req) {
1452
+ function loopbackRequest$1(req) {
1138
1453
  return hostIsLoopback(req.headers.host) && originIsLoopback(req.headers.origin);
1139
1454
  }
1140
1455
  /**
@@ -1219,17 +1534,17 @@ async function workBuddyWebStatus(deps) {
1219
1534
  function workBuddyStatusHandler(deps) {
1220
1535
  return async (req, res) => {
1221
1536
  if (req.method !== "GET") {
1222
- json$1(res, 405, { error: "method not allowed" });
1537
+ json$2(res, 405, { error: "method not allowed" });
1223
1538
  return;
1224
1539
  }
1225
- if (!loopbackRequest(req)) {
1226
- json$1(res, 403, { error: "request-not-trusted" });
1540
+ if (!loopbackRequest$1(req)) {
1541
+ json$2(res, 403, { error: "request-not-trusted" });
1227
1542
  return;
1228
1543
  }
1229
1544
  try {
1230
- json$1(res, 200, await workBuddyWebStatus(deps));
1545
+ json$2(res, 200, await workBuddyWebStatus(deps));
1231
1546
  } catch (error) {
1232
- json$1(res, 500, { error: safeMessage(error) });
1547
+ json$2(res, 500, { error: safeMessage(error) });
1233
1548
  }
1234
1549
  };
1235
1550
  }
@@ -1248,12 +1563,79 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
1248
1563
  }, "dsh-workbuddy-bridge: Web status route");
1249
1564
  }
1250
1565
  //#endregion
1566
+ //#region src/web/credit.ts
1567
+ /**
1568
+ * The request must be addressed to the loopback interface, and a
1569
+ * browser-attached Origin must be loopback too. Identical to the status route's
1570
+ * guard: the Host check drops DNS-rebinding pages, and the card's same-origin
1571
+ * fetches carry no Origin and pass on Host alone.
1572
+ */
1573
+ function loopbackRequest(req) {
1574
+ return hostIsLoopback(req.headers.host) && originIsLoopback(req.headers.origin);
1575
+ }
1576
+ function json$1(res, status, body) {
1577
+ const payload = JSON.stringify(body);
1578
+ res.writeHead(status, {
1579
+ "Content-Type": "application/json",
1580
+ "Cache-Control": "no-store",
1581
+ "Content-Length": Buffer.byteLength(payload)
1582
+ });
1583
+ res.end(payload);
1584
+ }
1585
+ /**
1586
+ * One Session's accounting.
1587
+ *
1588
+ * A missing `sessionId` is answered with an empty document rather than an
1589
+ * error: the browser half asks on behalf of whatever Session it is rendering,
1590
+ * and "this Session has no recorded costs" is a normal, silent state — the row
1591
+ * simply shows no label.
1592
+ *
1593
+ * @param deps - the accounting index.
1594
+ * @param sessionId - the Session to answer for.
1595
+ * @returns the document the browser half renders from.
1596
+ */
1597
+ function workBuddySessionCredits(deps, sessionId) {
1598
+ return {
1599
+ sessionId,
1600
+ credits: deps.credits.forSession(sessionId)
1601
+ };
1602
+ }
1603
+ /** The credit route's request handler, extracted so tests can mount it bare. */
1604
+ function workBuddyCreditHandler(deps) {
1605
+ return async (req, res) => {
1606
+ if (req.method !== "GET") {
1607
+ json$1(res, 405, { error: "method not allowed" });
1608
+ return;
1609
+ }
1610
+ if (!loopbackRequest(req)) {
1611
+ json$1(res, 403, { error: "request-not-trusted" });
1612
+ return;
1613
+ }
1614
+ json$1(res, 200, workBuddySessionCredits(deps, new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("sessionId") ?? ""));
1615
+ };
1616
+ }
1617
+ /** Mount the GET credit route on an optional webServer context. */
1618
+ function registerWorkBuddyCreditRoute(ctx, deps) {
1619
+ const path = deps.path ?? "/plugins/dsh-workbuddy-bridge/credit";
1620
+ ctx.effect(() => {
1621
+ const dispose = ctx.webServer.register({
1622
+ kind: "exact",
1623
+ path,
1624
+ handler: workBuddyCreditHandler(deps)
1625
+ });
1626
+ return () => {
1627
+ dispose();
1628
+ };
1629
+ }, "dsh-workbuddy-bridge: Web credit route");
1630
+ }
1631
+ //#endregion
1251
1632
  //#region src/web/probe-route.ts
1252
1633
  /**
1253
1634
  * Probe control route: the only state-changing endpoint the plugin exposes.
1254
1635
  *
1255
- * Two guards, because they stop different things (see `docs/reasoning-effort-probe-plan.md`
1256
- * §6.4 and the v0.3.1 note in AGENTS.md about their exact scope):
1636
+ * Two guards, because they stop different things (each is pinned by its own
1637
+ * spec — `tests/loopback.spec.ts` for the first, `tests/probe-route.spec.ts`
1638
+ * for both together):
1257
1639
  *
1258
1640
  * 1. **Loopback Host + Origin**, shared with the status route. This drops
1259
1641
  * DNS-rebinding pages, whose requests arrive addressed to the attacker's
@@ -1531,7 +1913,7 @@ function fallbackFor(variant) {
1531
1913
  return variant.id === CN_VARIANT.id ? FALLBACK_WORKBUDDY_MODELS : FALLBACK_WORKBUDDY_AI_MODELS;
1532
1914
  }
1533
1915
  /** Build one variant's stores and probe state. */
1534
- function createVariantRuntime(config, variant, identityOf, accountOf, keyProvider) {
1916
+ function createVariantRuntime(config, variant, identityOf, accountOf, keyProvider, creditIndex) {
1535
1917
  const client = new WorkBuddyUpstreamClient();
1536
1918
  const configured = configuredAuthFile(config, variant);
1537
1919
  const store = new WorkBuddyCredentialStore({
@@ -1564,6 +1946,8 @@ function createVariantRuntime(config, variant, identityOf, accountOf, keyProvide
1564
1946
  consent: () => config.probeConsent,
1565
1947
  account: () => identityOf(variant.id)
1566
1948
  }),
1949
+ creditLog: new WorkBuddyCreditLog(),
1950
+ creditIndex,
1567
1951
  savedCatalogs,
1568
1952
  visibilityStore,
1569
1953
  account: () => accountOf(variant.id),
@@ -1589,41 +1973,57 @@ function catalogSection(runtime) {
1589
1973
  };
1590
1974
  }
1591
1975
  /**
1592
- * Whether a model can be probed by hand: it reasons and the upstream declares
1593
- * no effort set for it.
1594
- *
1595
- * Deliberately *not* filtered by whether a result already exists. Dropping a
1596
- * model once it has been detected made the list shrink with use, so
1597
- * re-detecting one model — after an upstream change, say — meant clearing every
1598
- * other result first. The list stays stable and the card marks which entries
1599
- * already have an answer.
1976
+ * One row per reasoning model, describing its thinking-level switch.
1977
+ *
1978
+ * The judgement comes from {@link resolveEfforts} — the *same* function the
1979
+ * adapter builds the model picker from — so the composer's bulb and the picker
1980
+ * cannot describe different models. That sharing is the fix for what used to be
1981
+ * two independent answers: a candidate list ("who could be probed") beside a
1982
+ * result list ("who has been probed"), which between them told the user nothing
1983
+ * about the only question they had, namely which models let them pick a level.
1984
+ *
1985
+ * Read through `recordFor` rather than straight from the store, so a record the
1986
+ * adapter already discounts — its catalog row changed, it aged past the TTL, or
1987
+ * the upstream has since declared a set — is discounted here too. A model the
1988
+ * upstream dropped leaves the catalog entirely and drops out of this list.
1600
1989
  */
1601
- function isProbeCandidate(info) {
1602
- if (info.reasoning?.supports !== true) return false;
1603
- return (info.reasoning.supportedEfforts?.length ?? 0) === 0;
1604
- }
1605
- /** Compact probe state for one card: consent, candidates, observations. */
1606
1990
  function probeSection(runtime, consent) {
1607
- const models = runtime.catalog.current();
1608
- const results = models.flatMap((info) => {
1991
+ const models = runtime.catalog.current().flatMap((info) => {
1992
+ if (info.reasoning?.supports !== true) return [];
1609
1993
  const record = runtime.probeService.recordFor(info.id);
1610
- if (record === void 0) return [];
1994
+ const resolved = resolveEfforts(info, record);
1611
1995
  return [{
1612
1996
  id: info.id,
1613
1997
  name: info.name,
1614
- validation: record.validation,
1615
- efforts: record.efforts,
1616
- probedAt: record.probedAtMs
1998
+ efforts: resolved.efforts,
1999
+ source: resolved.source,
2000
+ detectable: resolved.detectable,
2001
+ ...record === void 0 ? {} : {
2002
+ validation: record.validation,
2003
+ probedAt: record.probedAtMs
2004
+ }
1617
2005
  }];
1618
2006
  });
1619
2007
  return {
1620
2008
  consent,
1621
2009
  running: runtime.probeService.isRunning(),
1622
- candidates: models.filter(isProbeCandidate).map((info) => info.id),
1623
- results: newestFirst(results)
2010
+ models: detectedFirst(models)
1624
2011
  };
1625
2012
  }
1626
2013
  /**
2014
+ * Order the effort rows for display: most recently detected first, then the
2015
+ * catalog's own order.
2016
+ *
2017
+ * The list is no longer "the models you could probe" but "every model and what
2018
+ * its switch does", so it must stay stable as detections land — a row that
2019
+ * moves when its own result arrives is a row the user has to find again. Only
2020
+ * the detected rows are promoted, and ties keep catalog order (a stable sort),
2021
+ * which is what makes the movement predictable.
2022
+ */
2023
+ function detectedFirst(rows) {
2024
+ return [...rows].sort((a, b) => (b.probedAt ?? -1) - (a.probedAt ?? -1));
2025
+ }
2026
+ /**
1627
2027
  * Start one variant: its loopback endpoint, provider registration, and
1628
2028
  * configuration-card wiring.
1629
2029
  *
@@ -1634,12 +2034,15 @@ function probeSection(runtime, consent) {
1634
2034
  * @returns whether the provider registered.
1635
2035
  */
1636
2036
  async function startVariant(ctx, runtime) {
1637
- const { variant, store, client, catalog, probeService } = runtime;
2037
+ const { variant, store, client, catalog, probeService, creditLog } = runtime;
1638
2038
  const shim = createWorkBuddyShim({
1639
2039
  store,
1640
2040
  client,
1641
2041
  catalog,
1642
- logger: ctx.logger
2042
+ logger: ctx.logger,
2043
+ observeCredit: (responseId, credit) => {
2044
+ creditLog.record(responseId, credit);
2045
+ }
1643
2046
  });
1644
2047
  try {
1645
2048
  await shim.ready;
@@ -1719,7 +2122,8 @@ function apply(ctx, refs) {
1719
2122
  */
1720
2123
  const lastAccounts = /* @__PURE__ */ new Map();
1721
2124
  const atRestKeysFor = (variant) => new WorkBuddyAtRestKeyProvider({ discovery: variant.id === CN_VARIANT.id ? cnAppDiscovery() : "none" });
1722
- const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config(), variant, (id) => lastIdentities.get(id), (id) => lastAccounts.get(id), atRestKeysFor(variant)));
2125
+ const creditIndex = new WorkBuddySessionCreditIndex();
2126
+ const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config(), variant, (id) => lastIdentities.get(id), (id) => lastAccounts.get(id), atRestKeysFor(variant), creditIndex));
1723
2127
  const probeKey = createProbeKey();
1724
2128
  /**
1725
2129
  * Point a variant at an account identity, invalidating whatever the previous
@@ -1783,6 +2187,7 @@ function apply(ctx, refs) {
1783
2187
  runtime.invalidate();
1784
2188
  };
1785
2189
  ctx.inject(["webServer"], (webCtx) => {
2190
+ registerWorkBuddyCreditRoute(webCtx, { credits: creditIndex });
1786
2191
  for (const runtime of runtimes) {
1787
2192
  registerWorkBuddyStatusRoute(webCtx, {
1788
2193
  path: runtime.variant.statusPath,
@@ -1865,6 +2270,39 @@ function apply(ctx, refs) {
1865
2270
  }, probeKey);
1866
2271
  }
1867
2272
  });
2273
+ /**
2274
+ * Attribute an observed response cost to the message that caused it.
2275
+ *
2276
+ * The two halves of the key are produced by different layers and in either
2277
+ * order, so this is deliberately a join rather than a straight read:
2278
+ *
2279
+ * - the **shim** sees `usage.credit` on the stream and knows the upstream
2280
+ * response id (`cmb-…`), but nothing about DSH's message ids;
2281
+ * - the **session log** records the assistant message with its DSH
2282
+ * `messageId` and, inside `source.replayState.response.responseId`, that
2283
+ * same upstream id.
2284
+ *
2285
+ * The session event normally lands *after* the stream's final frame (the
2286
+ * message is appended once the stream settles), so `creditLog.lookup` usually
2287
+ * hits. `creditLog.await` covers the opposite interleaving — a response whose
2288
+ * final frame arrives after its message was appended — instead of leaving
2289
+ * that message permanently unlabelled.
2290
+ *
2291
+ * Only variants' own provider routes are considered: the response id is
2292
+ * matched against each variant's own log, so a DeepSeek-account message can
2293
+ * never be charged a WorkBuddy cost.
2294
+ */
2295
+ ctx.inject(["sessions"], (sessionCtx) => {
2296
+ sessionCtx.on("session/event", (session, event) => {
2297
+ if (event.type !== "assistant/message") return;
2298
+ const data = event.data;
2299
+ const provider = data.message?.source?.provider;
2300
+ const runtime = typeof provider === "string" ? runtimes.find((candidate) => candidate.variant.id === provider) : void 0;
2301
+ if (runtime === void 0) return;
2302
+ const sessionId = session.id;
2303
+ attributeCredit(runtime.creditLog, runtime.creditIndex, sessionId, data);
2304
+ });
2305
+ });
1868
2306
  ctx.effect(() => () => {
1869
2307
  stopped = true;
1870
2308
  for (const timer of timers) clearInterval(timer);
@@ -1896,8 +2334,7 @@ function apply(ctx, refs) {
1896
2334
  if (inflight !== void 0 && inflight.identity === identity && inflight.generation === generation) return inflight.promise;
1897
2335
  inflight?.controller.abort();
1898
2336
  const controller = new AbortController();
1899
- let run;
1900
- run = (async () => {
2337
+ const run = (async () => {
1901
2338
  let models;
1902
2339
  try {
1903
2340
  const credential = await runtime.store.resolve();
@@ -1963,9 +2400,7 @@ function apply(ctx, refs) {
1963
2400
  */
1964
2401
  const syncVariant = async (runtime) => {
1965
2402
  if (stopped || !runtime.registered) return;
1966
- const credential = await runtime.store.current().catch((error) => {
1967
- ctx.logger.warn(`dsh-workbuddy-bridge: ${runtime.variant.displayName} credential read failed`, error);
1968
- });
2403
+ const credential = await readCredential(runtime);
1969
2404
  if (stopped) return;
1970
2405
  if (credential === void 0) {
1971
2406
  adoptIdentity(runtime, void 0, void 0);
@@ -1981,11 +2416,70 @@ function apply(ctx, refs) {
1981
2416
  adoptIdentity(runtime, identity, visibilityAccountOf(credential));
1982
2417
  await fetchCatalog(runtime, identity);
1983
2418
  };
2419
+ /**
2420
+ * Read a variant's credential, reporting a refusal instead of swallowing it.
2421
+ *
2422
+ * A region mismatch or an unreadable file is *reported*, not treated as
2423
+ * "signed out": the user needs to know which file to fix, and the difference
2424
+ * between "no credential" and "a credential I refused" is exactly what the
2425
+ * card's reason codes are for.
2426
+ *
2427
+ * @returns the credential, or `undefined` when there is none to use.
2428
+ */
2429
+ const readCredential = async (runtime) => {
2430
+ try {
2431
+ return await runtime.store.current();
2432
+ } catch (error) {
2433
+ ctx.logger.warn(`dsh-workbuddy-bridge: ${runtime.variant.displayName} credential read failed`, error);
2434
+ return;
2435
+ }
2436
+ };
2437
+ /**
2438
+ * Adopt a variant's account **before its provider registers**.
2439
+ *
2440
+ * Registration and revelation used to happen in the other order: the adapter
2441
+ * registered first (reading a catalog that was still hidden, so it built
2442
+ * *zero* models), and the first credential sweep revealed the group afterwards
2443
+ * in a fire-and-forget `syncAll()`. Between those two moments
2444
+ * `ctx.llm.listProviders()` named the provider while `resolveModelInfo`
2445
+ * answered `UNKNOWN_MODEL` for every one of its models — invisible in the
2446
+ * picker, but a session that already had one of those models selected (a
2447
+ * restored conversation, or `agent-default-model`) failed on its first
2448
+ * message. The window is tens of milliseconds here and much longer on a real
2449
+ * machine: macOS unlocks the credential through Spotlight and the app's
2450
+ * Electron, Windows goes through `reg query` and at-rest decryption, and
2451
+ * either can sit on a slow or networked drive.
2452
+ *
2453
+ * Adopting first closes it: when the provider appears, its catalog is already
2454
+ * visible, so the models exist from the first read. Nothing is lost when the
2455
+ * read fails or nobody is signed in — the variant registers hidden exactly as
2456
+ * it does today, and the poll reveals it later.
2457
+ *
2458
+ * @see .agents/notes/2026-10-02-startup-window-fix-direction.md
2459
+ */
2460
+ const adoptBeforeRegistering = async (runtime) => {
2461
+ const credential = await readCredential(runtime);
2462
+ if (stopped) return;
2463
+ adoptIdentity(runtime, credential === void 0 ? void 0 : credentialIdentity(credential), credential === void 0 ? void 0 : visibilityAccountOf(credential));
2464
+ };
1984
2465
  /** Run one reconcile sweep across both variants. */
1985
2466
  const syncAll = async () => {
1986
2467
  for (const runtime of runtimes) await syncVariant(runtime);
1987
2468
  };
1988
- Promise.all(runtimes.map(async (runtime) => startVariant(ctx, runtime))).then(() => {
2469
+ /**
2470
+ * Bring both variants up, then start the credential sweep.
2471
+ *
2472
+ * The phases are ordered and deliberate: adopting an identity must finish
2473
+ * before the matching provider registers (see
2474
+ * {@link adoptBeforeRegistering}), and the catalogs are fetched after
2475
+ * registration so a fetch failure lands on an already-usable group rather than
2476
+ * on nothing. The two variants never wait on each other within a phase — one
2477
+ * slow credential read must not hold the other product's models back.
2478
+ */
2479
+ const startAll = async () => {
2480
+ await Promise.all(runtimes.map((runtime) => adoptBeforeRegistering(runtime)));
2481
+ if (stopped) return;
2482
+ await Promise.all(runtimes.map(async (runtime) => startVariant(ctx, runtime)));
1989
2483
  if (stopped) return;
1990
2484
  if (runtimes.some((runtime) => runtime.registered)) writeHostHeartbeat();
1991
2485
  syncAll();
@@ -1994,7 +2488,8 @@ function apply(ctx, refs) {
1994
2488
  }, credentialPollMs());
1995
2489
  timer.unref?.();
1996
2490
  timers.push(timer);
1997
- });
2491
+ };
2492
+ startAll();
1998
2493
  }
1999
2494
  //#endregion
2000
- export { AI_VARIANT, CN_APP_VERSION_FILENAME, CN_VARIANT, Config, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, WorkBuddyCatalog, WorkBuddyCatalogStore, WorkBuddyCredentialStore, WorkBuddyProbeService, WorkBuddyProbeStore, WorkBuddyUpstreamClient, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };
2495
+ export { AI_VARIANT, CN_APP_VERSION_FILENAME, CN_VARIANT, Config, FALLBACK_CN_APP_VERSION, FALLBACK_WORKBUDDY_AI_MODELS, FALLBACK_WORKBUDDY_MODELS, PROBE_EFFORT_CANDIDATES, WORKBUDDY_APP_VERSION_FILENAME, WORKBUDDY_AUTH_FILENAME, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_CATALOG_FILENAME, WORKBUDDY_CREDIT_PATH, WORKBUDDY_HOST_HEARTBEAT_FILENAME, WORKBUDDY_PROBE_FILENAME, WORKBUDDY_PROVIDER, WORKBUDDY_STREAM_IDLE_TIMEOUT_MS, WORKBUDDY_VARIANTS, WORKBUDDY_VISIBILITY_FILENAME, WorkBuddyCatalog, WorkBuddyCatalogStore, WorkBuddyCredentialStore, WorkBuddyCreditLog, WorkBuddyProbeService, WorkBuddyProbeStore, WorkBuddySessionCreditIndex, WorkBuddyUpstreamClient, WorkBuddyVisibilityStore, appUserAgent, apply, chatUserAgent, classifyUpstreamError, clearHostHeartbeat, createWorkBuddyAdapter, createWorkBuddyShim, defaultDesktopAuthCandidates, defaultDesktopAuthPath, desktopAuthCandidatesFor, fallbackChatIdentity, fingerprintModel, inject, installedAppVersion, isHeartbeatProcessAlive, modelWithCurrentPromotion, name, normalizeCredits, parseModelCatalog, parseWorkBuddyAuth, prepareChatBody, prepareInternationalChatBody, probeModel, processStartTimeMs, randomSentinel, readBundleVersion, readCliVersion, readHostHeartbeat, regionOf, registerWorkBuddyCreditRoute, resolveAppVersion, resolveChatIdentity, validAppVersion, validCliVersion, variantFor, visibilityAccountOf, workBuddyCreditHandler, workBuddySessionCredits, workbuddyCatalogPath, workbuddyHostHeartbeatPath, workbuddyOwnAuthPath, workbuddyProbePath, workbuddyVisibilityPath };