dsh-connect-workbuddy 4.7.0 → 4.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.8.0 (2026-10-18)
4
+
5
+ > 版本号是 **minor**:这不是只改内部的 patch —— 它**会改动会话历史**。跨区/跨窗口切换后原本必然卡死的那一类会话,本版能把上下文折回去继续用。没有溢出时行为完全不变。
6
+
7
+ ### Fixes
8
+
9
+ - **切到窗口更小的路由后,会话永久卡在 `400 code 11115 prompt is too long`(#59 后半)**
10
+ - **症状**:会话在一个 1M 窗口的模型上跑得很长,切成国际区(`workbuddy-global`,窗口 1,048,576)后,**每一条**请求都返回 `prompt is too long: 1374460 tokens > 1048576 maximum`。换回原区域能用,再切过去还是卡死;手动压缩同样 400。会话实质报废。
11
+ - **根因(宿主侧,两处叠加)**:
12
+ 1. **压力阈值按「当前模型」窗口现算**,`compaction-basic/src/config.ts:188-192` 的 `thresholdTokens = min(contextWindow × thresholdRatio, messageBudgetTokens)`。1M 窗口下阈值是 800,000,而 707,801 未到 —— 会话在**切区那一刻就已经超了新模型的窗口**,可自动压缩的触发点还在旧模型的尺度上,永远等不到。
13
+ 2. **溢出兜底把整段历史塞进一次摘要请求**,`compaction-basic/src/index.ts:299` 的 `selectCompactableRange(session, measurement, 0)` 保留量为 0 → `region.ts:150-154` 选出「除最后一条外的全部历史」;`summarizer.ts:141-151` 又把这份摘要发给**当前那条小窗口路由**。于是救援请求和它要救援的请求一样溢出 —— 死锁。
14
+ 宿主侧已按 deepseek-ai/deepseek-harness#9814 提交,上面两处即该 issue 的 A、B 两点。
15
+ - **本版做的事**:适配器在溢出错误**仍然持有 session id** 的那一刻接手,用宿主**自己的**压缩事务(`ctx.compaction.compactRegion`)把历史折回去,而不是另发一次摘要请求。
16
+ - 分块上限 = 模型窗口的 **35%**,因此摘要请求本身**按构造**一定装得下(宿主默认那一次是 131%)。
17
+ - 折叠目标是窗口的 **60%**,逐块折叠直到低于目标;块边界来自宿主自己的 `tokenMeter`。
18
+ - **绝不折掉当前请求携带的那条消息**(`nodes.at(-1)` 永远保留)。
19
+ - 宿主拒绝某个区间(切断了 tool call 与 result 的配对、或正有压缩在跑)时,把尾边向内收一格重试;仍是拒绝就放弃,绝不空转。
20
+ - 折完仍降不下去(单条消息本身就超过窗口)时立刻停手 —— 这是结构性问题,重试只会烧掉重试预算。
21
+ - **实测(真实 GL 400 载荷)**:`isContextOverflow` 对 `prompt is too long: 1374460 tokens > 1048576 maximum` 与 `{"code":11115,...,"type":"CONTEXT_WINDOW_EXCEEDED"}` 均判定为真,对 `rate limit exceeded` 判定为假 —— 只在真溢出时动手。
22
+ - **降级路径**:宿主没装压缩包(`compaction` 服务不存在)、或请求没带 `sessionId`(非主循环调用)时,适配器原样返回,行为与本版之前**逐字节相同**。
23
+
24
+ ### 已知未修
25
+
26
+ - **跨区 prompt token 计数漂移**(#59 前半):同一份 3.6 MB 请求体,CN 计 642,800、GL 计 447,306。这不是网关数错 —— 历史里的 thinking 块按上游当初实际返回的字段名(`reasoning` / `reasoning_content`)原样回放(宿主 `pi-ai` 0.85.1 `openai-completions.js:428` → `:1006`),同一批字节因此落进不同计费桶。归属宿主,本插件没有改写历史的权限,详见 #59 与 `src/body-report.ts` 头部说明。
27
+
3
28
  ## 4.7.0 (2026-10-17)
4
29
 
5
30
  > 版本号是 **minor**:这不是只改内部的 patch —— 它**改变了发往上游的请求体**。DSH Desktop 0.2.1-rc.1 随包 pi-ai 1.1.0,本版修的是该组合下「请求静默丢掉 system prompt 与全部工具声明」。若你停留在更早的宿主,行为不变。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat-Dd9fsFMR.js";
2
+ import { C as defaultDesktopAuthDirs, F as WorkBuddyUpstreamClient, K as WORKBUDDY_APP_EXECUTABLE_ENV, O as legacyWorkbuddyOwnAuthPath, Q as findWorkbuddyAppExecutableWithSource, S as defaultDesktopAuthCandidates, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, j as workbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, ot as hostPeersInfo, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, y as WorkBuddyCredentialStore } from "./host-heartbeat--S6JVSci.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -3957,7 +3957,7 @@ function generationOf(version) {
3957
3957
  //#endregion
3958
3958
  //#region src/version.ts
3959
3959
  /** The npm package version this build was produced from. */
3960
- const WORKBUDDY_CONNECT_VERSION = "4.7.0";
3960
+ const WORKBUDDY_CONNECT_VERSION = "4.8.0";
3961
3961
  //#endregion
3962
3962
  //#region src/host-heartbeat.ts
3963
3963
  /**
package/lib/index.d.ts CHANGED
@@ -1246,6 +1246,18 @@ declare function createWorkBuddyShim(options: WorkBuddyShimOptions): WorkBuddySh
1246
1246
  /** The resolved pi-ai generation, by context-shape capability. */
1247
1247
  type PiAiGeneration = 'modern' | 'legacy';
1248
1248
  //#endregion
1249
+ //#region src/context-rescue.d.ts
1250
+ interface ContextRescue {
1251
+ /**
1252
+ * Fold history until the next request fits the given window.
1253
+ *
1254
+ * @param sessionId - session whose history should be folded.
1255
+ * @param window - context window of the route that just overflowed.
1256
+ * @returns whether any history was folded.
1257
+ */
1258
+ rescue(sessionId: string, window: number): Promise<boolean>;
1259
+ }
1260
+ //#endregion
1249
1261
  //#region src/adapter.d.ts
1250
1262
  /** Provider route this bundle owns for the domestic (CN) gateway. */
1251
1263
  declare const WORKBUDDY_PROVIDER = "workbuddy";
@@ -1270,6 +1282,12 @@ interface WorkBuddyAdapterOptions {
1270
1282
  displayName?: string;
1271
1283
  /** Resolve the durable attachment service at request time, when present. */
1272
1284
  resolveAttachments?: () => AttachmentStore | undefined;
1285
+ /**
1286
+ * Resolve the host-side context-overflow rescue, when this host exposes one.
1287
+ * Omitted (or resolving to `undefined`) leaves the adapter exactly as it was:
1288
+ * a provider overflow is forwarded and the host's own recovery decides.
1289
+ */
1290
+ resolveContextRescue?: () => ContextRescue | undefined;
1273
1291
  /**
1274
1292
  * Override the pi-ai generation gate (see {@link piAiRuntimeInfo}); tests
1275
1293
  * inject both branches because this checkout only ever RESOLVES one of
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-Dd9fsFMR.js";
1
+ import { $ as isEncryptedFieldWrapper, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as deriveAtRestKey, K as WORKBUDDY_APP_EXECUTABLE_ENV, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as fetchAtRestKeyPayload, Y as deriveAtRestKeyId, Z as findWorkbuddyAppExecutable, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, at as workbuddyAppExecutableCandidates, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as withFileLock, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, et as isWorkbuddyBundle, f as WorkBuddyCatalog, g as ENCRYPTED_CREDENTIAL_CODE, h as fallbackModelsFor, i as processStartTimeMs, it as readAtRestKey, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as writeFileAtomic, m as deriveCatalog, n as clearHostHeartbeat, nt as macosNestedAppBundles, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, q as clearAtRestKeyCache, r as isHeartbeatProcessAlive, rt as openEncryptedField, s as writeHostHeartbeat, st as resolveDshHome, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as macosBundleExecutable, u as FALLBACK_WORKBUDDY_MODELS, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat--S6JVSci.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
5
5
  import { join } from "node:path";
6
- import { createProvider } from "@earendil-works/pi-ai";
6
+ import { createAssistantMessageEventStream, createProvider, isContextOverflow } from "@earendil-works/pi-ai";
7
7
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
8
8
  import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
9
9
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
@@ -473,6 +473,51 @@ function withLegacyContext(api, generation) {
473
473
  };
474
474
  }
475
475
  /**
476
+ * Layer the host-side context-overflow rescue onto a provider's streams.
477
+ *
478
+ * pi-ai reports a provider failure as an `error` event rather than a throw, so
479
+ * the rescue runs while the failing assistant message is still in hand: history
480
+ * is folded before the event continues on its way, and the host's retry then
481
+ * rebuilds its request against a smaller surface. Nothing is swallowed — the
482
+ * error event is forwarded unchanged, because deciding whether to retry is the
483
+ * host's policy, not this transport's.
484
+ *
485
+ * Both gates are deliberate. Without `sessionId` there is no session to fold,
486
+ * and without a declared `contextWindow` there is no budget to fold against;
487
+ * either absence leaves the stream untouched.
488
+ */
489
+ function withContextRescue(api, resolve) {
490
+ if (resolve === void 0) return api;
491
+ const wrap = (inner) => (model, context, options) => {
492
+ const source = inner(model, context, options);
493
+ const rescue = resolve();
494
+ const sessionId = options?.sessionId;
495
+ const window = model.contextWindow;
496
+ if (rescue === void 0 || sessionId === void 0 || window === void 0) return source;
497
+ const out = createAssistantMessageEventStream();
498
+ let final;
499
+ (async () => {
500
+ try {
501
+ for await (const event of source) {
502
+ if (event.type === "done") final = event.message;
503
+ if (event.type === "error") final = event.error;
504
+ if (event.type === "error" && isContextOverflow(event.error)) await rescue.rescue(sessionId, window);
505
+ out.push(event);
506
+ }
507
+ out.end(final);
508
+ } catch {
509
+ out.end(final);
510
+ }
511
+ })();
512
+ return out;
513
+ };
514
+ return {
515
+ ...api,
516
+ stream: wrap(api.stream),
517
+ streamSimple: wrap(api.streamSimple)
518
+ };
519
+ }
520
+ /**
476
521
  * Assemble the adapter. The provider's `getModels` reads the live catalog,
477
522
  * and every model's `baseUrl` is re-resolved per read so the shim's
478
523
  * ephemeral port applies from the first snapshot after startup.
@@ -501,7 +546,7 @@ function createWorkBuddyAdapter(options) {
501
546
  }
502
547
  } },
503
548
  models: buildModels(),
504
- api: withLegacyContext(openAICompletionsApi(), generation)
549
+ api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation), options.resolveContextRescue)
505
550
  }),
506
551
  getModels: () => buildModels()
507
552
  };
@@ -529,6 +574,104 @@ function createWorkBuddyAdapter(options) {
529
574
  };
530
575
  }
531
576
  //#endregion
577
+ //#region src/context-rescue.ts
578
+ /** Share of the window one fold chunk may consume, leaving room for the prompt. */
579
+ const CHUNK_FRACTION = .35;
580
+ /** Post-fold measurement target: comfortably below the window the route allows. */
581
+ const TARGET_FRACTION = .6;
582
+ /** A session is never folded more than this many times by one rescue. */
583
+ const MAX_ROUNDS = 8;
584
+ /** How many times one chunk may shrink its tail edge before giving up. */
585
+ const MAX_SHRINK = 24;
586
+ /** A single fold is abandoned after this long. */
587
+ const FOLD_TIMEOUT_MS = 18e4;
588
+ /**
589
+ * Create the rescue bound to a live host.
590
+ *
591
+ * @param resolve - resolves host services at call time.
592
+ * @returns the rescue handle the adapter calls on a provider overflow.
593
+ */
594
+ function createContextRescue(resolve) {
595
+ const inflight = /* @__PURE__ */ new Set();
596
+ return { async rescue(sessionId, window) {
597
+ if (inflight.has(sessionId)) return false;
598
+ inflight.add(sessionId);
599
+ try {
600
+ return await foldUntilFits(resolve(), sessionId, window);
601
+ } catch (error) {
602
+ resolve().log?.("dsh-connect-workbuddy: context rescue failed", error);
603
+ return false;
604
+ } finally {
605
+ inflight.delete(sessionId);
606
+ }
607
+ } };
608
+ }
609
+ async function foldUntilFits(deps, sessionId, window) {
610
+ const { meter, compaction, agents } = deps;
611
+ if (meter === void 0 || compaction === void 0 || agents === void 0) return false;
612
+ const agent = agents.get(sessionId);
613
+ if (agent === void 0) return false;
614
+ const target = Math.max(1, Math.floor(window * TARGET_FRACTION));
615
+ let foldedAny = false;
616
+ let previous = Number.POSITIVE_INFINITY;
617
+ for (let round = 0; round < MAX_ROUNDS; round += 1) {
618
+ const measurement = meter.measure(agent.session);
619
+ if (measurement.totalTokens <= target) return foldedAny;
620
+ if (measurement.totalTokens >= previous) return foldedAny;
621
+ previous = measurement.totalTokens;
622
+ if (!await foldOnce(deps, agent, measurement, window, target)) return foldedAny;
623
+ foldedAny = true;
624
+ }
625
+ return foldedAny;
626
+ }
627
+ /**
628
+ * Fold the oldest chunk that fits, shrinking its tail edge when the host
629
+ * refuses the range.
630
+ *
631
+ * The host rejects a range whose edges split a tool call from its result, and
632
+ * it rejects an active or missing range before appending anything — so a
633
+ * refused attempt costs no durable state and the edge can simply move inward.
634
+ *
635
+ * @returns whether one chunk was folded.
636
+ */
637
+ async function foldOnce(deps, agent, measurement, window, target) {
638
+ const nodes = measurement.nodes;
639
+ if (nodes.length < 2) return false;
640
+ const budget = Math.floor(window * CHUNK_FRACTION);
641
+ const excess = measurement.totalTokens - target;
642
+ const chunk = Math.max(1, Math.min(budget, excess));
643
+ const head = nodes[0];
644
+ const compaction = deps.compaction;
645
+ if (head === void 0 || compaction === void 0) return false;
646
+ let end = -1;
647
+ let accumulated = 0;
648
+ for (let index = 0; index < nodes.length - 1; index += 1) {
649
+ const node = nodes[index];
650
+ if (node === void 0) break;
651
+ accumulated += node.tokens;
652
+ end = index;
653
+ if (accumulated >= chunk) break;
654
+ }
655
+ if (end < 0) return false;
656
+ const signal = AbortSignal.timeout(FOLD_TIMEOUT_MS);
657
+ const floor = Math.max(0, end - MAX_SHRINK);
658
+ for (; end >= floor; end -= 1) {
659
+ const tail = nodes[end];
660
+ if (tail === void 0) break;
661
+ try {
662
+ await compaction.compactRegion(head.seq, tail.seq, agent, signal);
663
+ return true;
664
+ } catch (error) {
665
+ if (signal.aborted) {
666
+ deps.log?.("dsh-connect-workbuddy: context rescue timed out", error);
667
+ return false;
668
+ }
669
+ }
670
+ }
671
+ deps.log?.("dsh-connect-workbuddy: context rescue found no foldable range");
672
+ return false;
673
+ }
674
+ //#endregion
532
675
  //#region src/body-report.ts
533
676
  /**
534
677
  * Body-composition report: WHAT was sent upstream, in counts (issue #59).
@@ -541,16 +684,32 @@ function createWorkBuddyAdapter(options) {
541
684
  * - the gateways count identical bytes differently (upstream's business); or
542
685
  * - the two requests did not carry the same messages (ours to fix).
543
686
  *
544
- * The second world was left standing by two independent measurements. The
545
- * reporter ran this project's `--kind`/`--language` probe on BOTH regions and
546
- * got the same tokens-per-1000-chars on byte-identical prose, code and JSON
547
- * (ratios 0.98–1.06 across six cells), so identical bytes really do bill
548
- * identically. And a code trace showed historical `reasoning` never reaches the
549
- * wire at all: pi-ai only forwards `thinking` blocks as text when
550
- * `compat.requiresThinkingAsText` is set — this plugin never sets it — and the
551
- * `reasoning_details` route needs a signature the stream parser never writes.
552
- * With the counting difference excluded and reasoning excluded, the remaining
553
- * candidate is that the two bodies differ in the first place.
687
+ * Neither world, it turned out — the answer was a third one, and this report is
688
+ * what produced it. A paired capture in one session (4.6.7) measured
689
+ * `bodyBytes` 3,628,068 vs 3,626,374, a ratio of 0.9995: both requests carried
690
+ * the same bytes. What differed was the BUCKET each byte was counted in — CN
691
+ * text 741,952 vs GL 184,821, CN reasoning 72,270 vs GL 627,994, with a
692
+ * complementary residual of 0.3%. The gateways did not miscount; the same
693
+ * history was labelled differently on the two sides.
694
+ *
695
+ * The labelling is pi-ai's replay, not this plugin's. `openai-completions.js`
696
+ * records the reasoning field name the upstream stream actually used into
697
+ * `thinkingSignature` (`:428`), and on the way back out writes
698
+ * `assistantMsg[<that name>] = <the thinking text>` (`:1006`), so a
699
+ * transcript produced against a gateway that says `reasoning` is replayed as
700
+ * `reasoning` and one that says `reasoning_content` as `reasoning_content`.
701
+ * This plugin sets only `compat.supportsReasoningEffort` (`src/adapter.ts:200`),
702
+ * so the `else` branch is taken and the field goes out raw rather than folded
703
+ * into text.
704
+ *
705
+ * An earlier revision of this comment claimed the opposite — that historical
706
+ * `reasoning` never reaches the wire, because `requiresThinkingAsText` is unset
707
+ * and the `reasoning_details` route needs a signature the parser never writes.
708
+ * Both halves are wrong under pi-ai 0.85.1: the unset flag is precisely what
709
+ * routes thinking out as a raw field, and the parser does write the
710
+ * `reasoning_details` signature (`:188–193`). The correction matters because it
711
+ * turns `reasoning` into a bucket that can be non-zero — and diffing it across
712
+ * regions is the direct measurement of the drift.
554
713
  *
555
714
  * This module exists to settle exactly that, and it can do it WITHOUT either
556
715
  * region having credit: the body is assembled locally, before the upstream ever
@@ -6609,6 +6768,15 @@ function apply(ctx, config) {
6609
6768
  Promise.all(REGION_KEYS.map((region) => stacks[region].shim.ready)).then(async () => {
6610
6769
  if (stopped) return;
6611
6770
  const adapters = {};
6771
+ const hostService = (name) => ctx.get(name);
6772
+ const contextRescue = createContextRescue(() => ({
6773
+ meter: hostService("tokenMeter"),
6774
+ compaction: hostService("compaction"),
6775
+ agents: hostService("agents"),
6776
+ log: (message, error) => {
6777
+ ctx.logger.warn(message, error);
6778
+ }
6779
+ }));
6612
6780
  try {
6613
6781
  for (const region of REGION_KEYS) adapters[region] = createWorkBuddyAdapter({
6614
6782
  shim: stacks[region].shim,
@@ -6616,7 +6784,8 @@ function apply(ctx, config) {
6616
6784
  catalog: stacks[region].catalog,
6617
6785
  provider: WORKBUDDY_PROVIDERS[region],
6618
6786
  displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
6619
- resolveAttachments: () => ctx.get("attachments")
6787
+ resolveAttachments: () => ctx.get("attachments"),
6788
+ resolveContextRescue: () => contextRescue
6620
6789
  });
6621
6790
  invalidateCatalog = () => {
6622
6791
  for (const region of REGION_KEYS) adapters[region].invalidate();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-workbuddy",
3
3
  "displayName": "DSH Connect WorkBuddy",
4
4
  "description": "把本机登录的 WorkBuddy 模型接入 DeepSeek Harness,并提供只读的积分概览与模型管理。",
5
- "version": "4.7.0",
5
+ "version": "4.8.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",