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 +25 -0
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-Dd9fsFMR.js → host-heartbeat--S6JVSci.js} +1 -1
- package/lib/index.d.ts +18 -0
- package/lib/index.js +183 -14
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
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.
|
|
5
|
+
"version": "4.8.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|