dsh-connect-workbuddy 5.11.0 → 5.12.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 +24 -0
- package/README.en.md +5 -0
- package/README.md +5 -0
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-BYdr-pVq.js → host-heartbeat-xEJRdYaC.js} +1 -1
- package/lib/index.d.ts +134 -0
- package/lib/index.js +186 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.12.0 (2026-10-26)
|
|
4
|
+
|
|
5
|
+
> 特性版本:新增 `dropForeignThinking`(**默认关闭**),修 issue #147——跨 provider 切换后
|
|
6
|
+
> pi-ai 把外来轮次的 thinking 改写成正文,模型随后照抄这个格式,整轮回答变成英文工作笔记。
|
|
7
|
+
> 关闭时请求字节与 5.11.0 完全一致;打开后只删「本来会被压成正文、且不可能作为带签名 reasoning
|
|
8
|
+
> 重放」的外来 thinking 块,正文块、工具调用与每条消息的身份都不动。请求路径、provider 路由、
|
|
9
|
+
> 计费与凭据处理一律未变。
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- **思考变正文:跨 provider 切换后模型把工作笔记写进回答(#147)**
|
|
14
|
+
- **病因在 pi-ai,不在本插件。** `transform-messages.js:68` 用 `isSameModel`(provider **且** api **且** model id)决定一条历史 thinking 能否按 reasoning 重放;不成立时它**不丢弃**该块,而是改写成 `{type:'text', text: thinking}`(`:87–90`)——也就是答案通道。会话一旦跨 provider 切换过,另一个模型的内部工作笔记就以「本轮回答」的身份出现,模型读到「我过去的思考全是正文」这个形状后持续照抄。
|
|
15
|
+
- **实测复现(本机 pi-ai 0.85.1 + 真实 `transformMessages`)**:`api: 'dsh-foreign'` 的外来轮次 → `[text]`(200/300 字符全部进正文);本插件自己的轮次 → `[thinking]`;把该块丢掉 → `[]`(不产生任何正文)。报告者的一轮真实会话里,80 条 assistant 消息中 56 条带正文而 `reasoning` ≤ 1 字符,20 条英文工作笔记合计 58,960 字符,CJK 占比跌到 1.37%;会话从 696,966 涨到 758,005 total token。
|
|
16
|
+
- **为什么是「删除」而不是「保留为 thinking」**:`isSameModel` 只比对消息自身字段,唯一的满足方式是把这个外来轮次宣称为本模型的输出——那正是 #111 刚删掉的来源谎言。而这些块由宿主 `foreignAssistant()` 构造、**不带 `thinkingSignature`**,即便被宣称也无法作为带签名 reasoning 重放,它们对线上唯一的贡献就是被压成的那段正文。
|
|
17
|
+
- **修法**:新增 `src/adapter.ts` 的 `foreignThinkingOf()`(只读诊断)与 `dropForeignThinking()`(实施删除),在 `withLegacyContext` 的 `prepare` 里、**`alignAssistantIdentities` 之后**执行。顺序是承重的:国际区自己的轮次到达时同样带 `api: 'dsh-foreign'`,先诊断会把它误判成外来轮次并删掉它自己的推理(#102 的反向回归)。
|
|
18
|
+
- **开关**:`dropForeignThinking`(volatile,默认 `false`)。用 getter 读活值,拨动后**下一个请求**即生效,不需要重载插件——用户正是在看着那条工作笔记时打开它。
|
|
19
|
+
- **诊断可见**:被删的块数与位置写入 status 端点的 `lastForeignThinking`(进程内、last-wins、重启即忘),`dropForeignThinking` 开关本身也随 status 下发,供卡片渲染提示。
|
|
20
|
+
|
|
21
|
+
### Tests
|
|
22
|
+
|
|
23
|
+
- 新增 `tests/issue147-foreign-thinking.spec.ts`(15 条),全部经过**真实 pi-ai 的 `transformMessages`**,而不是桩:缺陷本身可复现、开启后不产生正文、关闭时原对象返回(零改动)、本插件国内外两区轮次都保留 thinking、正文与工具调用不被触碰、redacted 与空块不误报、错误轮次报 `dropped` 变体、顺序必须在 alignment 之后、幂等。
|
|
24
|
+
- 变异验证:把 `dropForeignThinking` 改成空操作 → 4 条变红;把判断放宽到所有 assistant 轮次 → 7 条变红。
|
|
25
|
+
- 全量:**142 文件 / 2458 通过(2 条按平台跳过)**;两个 tsconfig 均 exit 0。
|
|
26
|
+
|
|
3
27
|
## 5.11.0 (2026-10-26)
|
|
4
28
|
|
|
5
29
|
> 特性版本:新增 `workbuddy_draw_video`(文生视频 / 图生视频,异步任务「提交 → 轮询 → 下载」,
|
package/README.en.md
CHANGED
|
@@ -482,6 +482,11 @@ Marketplace screenshots and GitHub README badges are two separate mechanisms:
|
|
|
482
482
|
|
|
483
483
|
- **Switching region makes the context "grow", and can hit `code 11115` immediately — the gateway is not over-charging you.** The same history is counted under different buckets in the two regions, and the root cause is how the host's bundled `@earendil-works/pi-ai` replays history. It evaluates `isSameModel` for every assistant message (`transform-messages.js:68`, keyed on provider **and** api **and** model id all being equal): for a message that fails, its thinking is **downgraded to ordinary content** (`:88–96` — not dropped), and content is what the gateways count, while thinking goes out in a different field. This plugin registers the domestic and international editions as **two provider ids** (`workbuddy` / `workbuddy-global`, see [How it works](#how-it-works)) that carry the **same model id** — so one region switch moves **all the other side's thinking into the content bucket at once**. That is the cause of issue #59's "the same conversation history is charged 1.374M by the international gateway and 708K by the domestic one (1.94×)"; switching back is the same formula read the other way, 0.696×.
|
|
484
484
|
- Three consequences you can see: a region switch can move the token count by ±30%–94%, and **how far depends only on the other side's thinking character count, not on total history length**; **switching models inside one region** triggers the same downgrade (the model leg of `isSameModel`), so the count can move without a region switch at all; and that jump lands on top of the host's current compaction threshold (`min(0.8 × window, window − 128000 − 65536)`), so a session that was comfortably under the limit can hit `code 11115 prompt is too long` the moment you switch region.
|
|
485
|
+
- **The downgrade is not only a counting problem: the reasoning becomes the answer, and the model then copies that shape (issue #147).** The bullet above covers what the downgrade does to BILLING; this one covers what it does to CONTENT. For a message where `isSameModel` fails, pi-ai does not drop its thinking — it rewrites each block to `{type:'text'}` (`transform-messages.js:87–90`), i.e. the answer channel. So after a provider switch, **another model's internal working notes are presented as this turn's reply**, and a model that reads "all of my past reasoning was plain text" keeps writing its plan into the text channel from then on. The reporter measured one real turn: 56 of 80 assistant messages carried an answer while their own `reasoning` was ≤ 1 character, 20 of those held English working notes totalling 58,960 characters, and CJK fell to 1.37% of the 60,984 visible characters where the previous six turns had been entirely Chinese. It also pushed the session from 696,966 to 758,005 total tokens against an 800,000 compaction threshold.
|
|
486
|
+
- **`dropForeignThinking` (off by default)**: when on, this plugin **removes** those foreign thinking blocks before the request goes out, instead of letting them be flattened into prose. Removal rather than preservation, because these blocks are built by the host's `foreignAssistant()` and carry **no `thinkingSignature`** — even claimed as our own turn they could never be replayed as signed reasoning, so their only possible contribution to the wire was the prose they became. Text blocks and tool calls are never touched, and neither is any message's identity.
|
|
487
|
+
- With the switch off the request is **byte-for-byte** what earlier builds sent: it changes bytes the upstream counts, so it is an explicit opt-in on the same rule as `usageIdentity`.
|
|
488
|
+
- The ORDER is load-bearing: the repair runs **after** `alignAssistantIdentities`. A global-region turn of this plugin's own arrives carrying `api: 'dsh-foreign'` too (the host's replay-degrade path), so diagnosing first would classify it as foreign and delete its reasoning — exactly issue #102 in reverse. `tests/issue147-foreign-thinking.spec.ts` pins the order through pi-ai's real `transformMessages`.
|
|
489
|
+
- Turns already recorded in history are **not** retroactively repaired: they are already stored as prose, and the switch governs later requests only. Starting a new session after the provider switch is the direct way out.
|
|
485
490
|
- **This is a mismatch between the host and pi-ai, not a bug in this plugin**: not replaying thinking across providers/models is right in itself (another model's train of thought is not context this model should be fed), and the mistake is treating `provider` as a necessary condition for "the same model" when the two provider ids here are **two access regions of one model**. The fix belongs upstream (same api + same model id, providers as an equivalence group); on the plugin side the backstop is `src/context-rescue.ts` since 4.8.0 — **once the upstream confirms an overflow, it folds history in chunks and resends**, instead of replaying the entire range the host meant to compact (which is exactly what makes a compaction request overflow itself and deadlock). To see where it happened: set `WORKBUDDY_BODY_REPORT` to capture a body-composition report — it records counts only, never text. **It appends continuously**: one `<region>.jsonl` per region, rolled to `.1` past 32 MB (one old copy kept); unset the variable and delete the files when you are done — do not leave it on for long sessions.
|
|
486
491
|
|
|
487
492
|
## Disclaimer
|
package/README.md
CHANGED
|
@@ -497,6 +497,11 @@ else { await envelope.response.text() /* 或者按 SSE 消费 */ }
|
|
|
497
497
|
- **这是宿主与 pi-ai 之间的一处错配,不是本插件的 bug**:不跨 provider/模型重放 thinking 这件事本身是对的(别家模型的思考过程对当前模型没有上下文价值),错在把 `provider` 当作「是不是同一个模型」的必要条件,而这里两个 provider 字符串指的是**同一个模型的两个接入区域**。
|
|
498
498
|
- **本插件已在 4.9.0 起从插件侧绕开它**:两条路由注册的 **model 身份统一为 `workbuddy`**(`src/adapter.ts` 的 `WORKBUDDY_MODEL_IDENTITY`),也就是模型自己声明的 `provider` 字段两边相同,于是 `isSameModel` 成立、换区不再把 thinking 降级成正文。**路由 id 没有合并**——它仍是 `workbuddy` / `workbuddy-global`,各自指向自己的 shim、目录与凭据,所以一侧故障不会带走另一侧,模型选择器也仍是两组。**4.9.0/4.9.1 的这版修复漏了一半,国际版整区不可用(issue #75)**:上面的说法只对 `getModel(provider, id)` 这一条按**参数**查表的路径成立;真正跑聊天的 `models.streamSimple(model, …)` 第一步是 `requireProvider(model)` = `providers.get(model.provider)`(`models.js:351–355`),查的是**模型上盖的那个字段**。于是国内版(路由 `workbuddy`、盖 `workbuddy`)自洽,国际版(路由 `workbuddy-global`、盖 `workbuddy`)在只注册了 `workbuddy-global` 的注册表里查不到,**每一个请求都在发出前就抛 `Unknown provider: workbuddy`**——一次也会失败,且报错里不带任何 WorkBuddy 字样,看起来像宿主坏了。**4.9.2 起改为:每条路由把自己的注册表同时登记上「路由 id」和「共享身份」两个键**(`src/adapter.ts` 的 `modelIdentityKeysOf`),模型上盖的身份仍然两边相同(#59 不回退),而 `requireProvider` 在国际版自己的注册表里也能查到、并拿到**国际版自己的** provider 对象(含它自己的 shim 地址与凭据)。pi-ai 的注册表本来就是 `Map`(`models.js:37` 的 `providers.set(provider.id, provider)`,键取的是 **provider 对象的 `id`**,不是 profiles 映射的键),所以这是注册表原生支持的事;**路由集合、模型选择器、目录与凭据都没有变化**,多出来的那个键不出现在任何 UI 里。回归测试 `tests/issue75-global-provider-dispatch.spec.ts` 走的是宿主的真实构造(`createModels` + `setProvider` + `streamSimple`),并保留一个反证用例固定住旧的失败文案。详见 `docs/audit/ISSUE59-OPTIONS.md` 的「#75 CORRECTION」一节。
|
|
499
499
|
- 升级后仍可能看到计数变化的部分:**在同一区域内换模型**仍会降级(`model` 那条腿是真的换了模型,thinking 签名未必对新模型有效,保留才是错的);**升级之前就录在历史里的消息**带着旧的按区域 id,重放时仍按旧规则降级——这种历史只会越聊越少,不会反过来影响新录的消息。
|
|
500
|
+
- **降级不只是计数问题:思考会变成正文,模型随后照抄这个格式(issue #147)。** 上一条说的是「thinking 被降级」对计费的影响,这条说的是它对**内容**的影响。pi-ai 对 `isSameModel` 不成立的消息**不是丢弃** thinking,而是把它改写成 `{type:'text'}`(`transform-messages.js:87–90`)——也就是答案通道。于是跨 provider 切换后,**另一个模型的内部工作笔记会作为本轮回答呈现**,模型读到「我过去的思考都是正文」这个形状后,接下来的每一轮都继续把计划写进正文。报告者在一轮真实会话里量到:80 条 assistant 消息中 56 条带正文而自身 `reasoning` ≤ 1 字符,其中 20 条是英文工作笔记、合计 58,960 字符;整轮 60,984 字符正文里 CJK 只剩 1.37%,而此前 6 轮的可见回复全是中文。它同时把会话从 696,966 推到 758,005 total token(压缩阈值 800,000)。
|
|
501
|
+
- **开关 `dropForeignThinking`(默认关闭)**:打开后,本插件在请求发出前**删掉**那些「本来会被压成正文」的外来 thinking 块。之所以是删而不是保留:这些块由宿主以 `foreignAssistant()` 构造,**不带 `thinkingSignature`**,即使被当成自己的轮次也无法作为带签名的 reasoning 重放——它们对线上唯一的贡献就是被压成的那段正文。正文块与工具调用一律不动,每条消息的身份也不动。
|
|
502
|
+
- 关闭时**一个字节都不变**,与之前版本完全一致——它改的是上游会数到的字节,所以按 `usageIdentity` 的同一条规矩做成显式 opt-in。
|
|
503
|
+
- 顺序很关键:修复跑在 `alignAssistantIdentities` **之后**。国际区自己的轮次到达时也带 `api: 'dsh-foreign'`(宿主的重放降级路径),先诊断会把它误判成外来轮次并删掉它自己的推理——那正是 #102 的反向回归。`tests/issue147-foreign-thinking.spec.ts` 用真实 pi-ai 的 `transformMessages` 钉住了这个顺序。
|
|
504
|
+
- 已经录进历史的坏轮次**不会**被追溯修复:它们已经作为正文存在会话里,开关只管之后的请求。想彻底摆脱,切换 provider 后新开会话是最直接的办法。
|
|
500
505
|
- 真正的修法仍在上游(让 `isSameModel` 认「provider 等价组」声明,而不是让插件去伪装自己的身份);插件侧的兜底是 4.8.0 起的 `src/context-rescue.ts`——**一旦上游确认超限,就按块折叠历史后重发**,而不是把宿主要压缩的整段历史原样重放一遍(原样重放正是「压缩请求自己超限 → 压缩死锁」的成因)。想亲眼看到这件事发生在哪条消息上:设 `WORKBUDDY_BODY_REPORT` 抓一次 body 组成报告,它只记计数、不记正文。**注意它会持续追加**:每个区一份 `<region>.jsonl`,超过 32 MB 自动滚成 `.1`(只保留一份旧档);排查完请关闭该环境变量并删除报告文件,不要开着它长期跑。
|
|
501
506
|
|
|
502
507
|
## 免责声明
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, Ut as findWorkbuddyAppExecutableWithSource, V as WorkBuddyUpstreamClient, Zt as hostPeersInfo, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-
|
|
2
|
+
import { C as WorkBuddyCredentialStore, D as defaultDesktopAuthDirs, E as defaultDesktopAuthCandidates, I as workbuddyOwnAuthPath, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, S as WORKBUDDY_AUTH_FILE_ENV, Ut as findWorkbuddyAppExecutableWithSource, V as WorkBuddyUpstreamClient, Zt as hostPeersInfo, a as readHostHeartbeat, c as WORKBUDDY_CONNECT_VERSION, d as rosterSeenPath, f as piAiRuntimeInfo, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, o as workbuddyHostHeartbeatPath, p as FALLBACK_WORKBUDDY_MODELS, r as isHeartbeatProcessAlive, u as describeRosterSeenPayload } from "./host-heartbeat-xEJRdYaC.js";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -7566,7 +7566,7 @@ function describeRosterSeenPayload(raw) {
|
|
|
7566
7566
|
//#endregion
|
|
7567
7567
|
//#region src/version.ts
|
|
7568
7568
|
/** The npm package version this build was produced from. */
|
|
7569
|
-
const WORKBUDDY_CONNECT_VERSION = "5.
|
|
7569
|
+
const WORKBUDDY_CONNECT_VERSION = "5.12.0";
|
|
7570
7570
|
//#endregion
|
|
7571
7571
|
//#region src/host-heartbeat.ts
|
|
7572
7572
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -2333,6 +2333,22 @@ interface WorkBuddyAdapterOptions {
|
|
|
2333
2333
|
* them. Production callers leave it unset.
|
|
2334
2334
|
*/
|
|
2335
2335
|
piAiGeneration?: PiAiGeneration;
|
|
2336
|
+
/**
|
|
2337
|
+
* Drop foreign thinking blocks pi-ai would publish as prose (issue #147).
|
|
2338
|
+
*
|
|
2339
|
+
* OFF by default, and read LIVE (a getter, not a boolean) so the switch takes
|
|
2340
|
+
* effect on the next request without reloading the plugin: turning it on
|
|
2341
|
+
* mid-session is the whole point — the user sees the working-notes turn,
|
|
2342
|
+
* flips the switch, and the next turn is answered normally.
|
|
2343
|
+
*/
|
|
2344
|
+
dropForeignThinking?: () => boolean;
|
|
2345
|
+
/**
|
|
2346
|
+
* Told which foreign thinking blocks a request dropped, so the reply can name
|
|
2347
|
+
* the condition instead of leaving the user to conclude the model broke
|
|
2348
|
+
* (issue #147 asks for at least a visible hint). Called at most once per
|
|
2349
|
+
* request; failures are swallowed.
|
|
2350
|
+
*/
|
|
2351
|
+
onForeignThinking?: (blocks: readonly ForeignThinkingBlock[]) => void;
|
|
2336
2352
|
}
|
|
2337
2353
|
/** What {@link createWorkBuddyAdapter} hands back. */
|
|
2338
2354
|
interface WorkBuddyAdapter {
|
|
@@ -2384,6 +2400,32 @@ declare function workBuddyDisplayName(info: WorkBuddyModelInfo): string;
|
|
|
2384
2400
|
* treats undeclared capability.
|
|
2385
2401
|
*/
|
|
2386
2402
|
declare function workBuddyThinkingLevelMap(info: WorkBuddyModelInfo): WorkBuddyThinkingLevelMap | undefined;
|
|
2403
|
+
/**
|
|
2404
|
+
* One foreign thinking block that pi-ai would publish as PROSE, and the reason it
|
|
2405
|
+
* would (issue #147).
|
|
2406
|
+
*
|
|
2407
|
+
* `variant` is not cosmetic: the two are handled by the same repair but they
|
|
2408
|
+
* mean different things, and the card explains them differently.
|
|
2409
|
+
*
|
|
2410
|
+
* - `flattened`: `stopReason` is a normal completion, so `transform-messages`
|
|
2411
|
+
* turns the block into `{type:'text', text: thinking}` — the whole reasoning
|
|
2412
|
+
* body lands in the visible answer.
|
|
2413
|
+
* - `dropped`: `stopReason` is `error`/`aborted`, so the entire message is
|
|
2414
|
+
* skipped; nothing reaches the wire and nothing reaches the user. It is
|
|
2415
|
+
* reported anyway, because it still costs context: the model's answer to
|
|
2416
|
+
* "why did my last turn say nothing" is this line, and a caller cannot see it
|
|
2417
|
+
* from the transcript.
|
|
2418
|
+
*/
|
|
2419
|
+
interface ForeignThinkingBlock {
|
|
2420
|
+
/** Index of the message in the context's `messages`. */
|
|
2421
|
+
messageIndex: number;
|
|
2422
|
+
/** Index of the block within that message's `content`. */
|
|
2423
|
+
blockIndex: number;
|
|
2424
|
+
/** How many characters of reasoning the block carried. */
|
|
2425
|
+
characters: number;
|
|
2426
|
+
/** Whether pi-ai would flatten it into prose or drop the whole message. */
|
|
2427
|
+
variant: 'flattened' | 'dropped';
|
|
2428
|
+
}
|
|
2387
2429
|
/**
|
|
2388
2430
|
* Assemble the adapter. The provider's `getModels` reads the live catalog,
|
|
2389
2431
|
* and every model's `baseUrl` is re-resolved per read so the shim's
|
|
@@ -3663,6 +3705,30 @@ type WorkBuddyWebUsage =
|
|
|
3663
3705
|
* GLOBAL and Host-carried for the same reasons as `cliBridgeModel`.
|
|
3664
3706
|
*/
|
|
3665
3707
|
cliBridgeRegionOrder?: string;
|
|
3708
|
+
/**
|
|
3709
|
+
* Whether the request path drops foreign thinking blocks (issue #147).
|
|
3710
|
+
*
|
|
3711
|
+
* Optional and absent reads as OFF: the repair changes bytes the upstream
|
|
3712
|
+
* counts, so a Host that cannot answer must not be read as consent.
|
|
3713
|
+
*/
|
|
3714
|
+
dropForeignThinking?: boolean;
|
|
3715
|
+
/**
|
|
3716
|
+
* What the LAST request had to drop, for the card's explanation of a turn
|
|
3717
|
+
* that arrived looking like working notes (issue #147).
|
|
3718
|
+
*
|
|
3719
|
+
* Absent means "no report", not "feature off" — the two are different facts
|
|
3720
|
+
* and the card says different things about them. Process-local and
|
|
3721
|
+
* last-wins: it describes the turn the reader is looking at.
|
|
3722
|
+
*/
|
|
3723
|
+
lastForeignThinking?: {
|
|
3724
|
+
blocks: readonly {
|
|
3725
|
+
messageIndex: number;
|
|
3726
|
+
blockIndex: number;
|
|
3727
|
+
characters: number;
|
|
3728
|
+
variant: 'flattened' | 'dropped';
|
|
3729
|
+
}[];
|
|
3730
|
+
atMs: number;
|
|
3731
|
+
};
|
|
3666
3732
|
/**
|
|
3667
3733
|
* The LIVE load level per model id, for the card's icon.
|
|
3668
3734
|
*
|
|
@@ -3762,6 +3828,30 @@ type WorkBuddyWebUsage =
|
|
|
3762
3828
|
* GLOBAL and Host-carried for the same reasons as `cliBridgeModel`.
|
|
3763
3829
|
*/
|
|
3764
3830
|
cliBridgeRegionOrder?: string;
|
|
3831
|
+
/**
|
|
3832
|
+
* Whether the request path drops foreign thinking blocks (issue #147).
|
|
3833
|
+
*
|
|
3834
|
+
* Optional and absent reads as OFF: the repair changes bytes the upstream
|
|
3835
|
+
* counts, so a Host that cannot answer must not be read as consent.
|
|
3836
|
+
*/
|
|
3837
|
+
dropForeignThinking?: boolean;
|
|
3838
|
+
/**
|
|
3839
|
+
* What the LAST request had to drop, for the card's explanation of a turn
|
|
3840
|
+
* that arrived looking like working notes (issue #147).
|
|
3841
|
+
*
|
|
3842
|
+
* Absent means "no report", not "feature off" — the two are different facts
|
|
3843
|
+
* and the card says different things about them. Process-local and
|
|
3844
|
+
* last-wins: it describes the turn the reader is looking at.
|
|
3845
|
+
*/
|
|
3846
|
+
lastForeignThinking?: {
|
|
3847
|
+
blocks: readonly {
|
|
3848
|
+
messageIndex: number;
|
|
3849
|
+
blockIndex: number;
|
|
3850
|
+
characters: number;
|
|
3851
|
+
variant: 'flattened' | 'dropped';
|
|
3852
|
+
}[];
|
|
3853
|
+
atMs: number;
|
|
3854
|
+
};
|
|
3765
3855
|
/**
|
|
3766
3856
|
* The LIVE load level per model id, for the card's icon.
|
|
3767
3857
|
*
|
|
@@ -4377,6 +4467,30 @@ interface WorkBuddyStatusRouteOptions {
|
|
|
4377
4467
|
* as {@link cliBridgeModel}.
|
|
4378
4468
|
*/
|
|
4379
4469
|
cliBridgeRegionOrder?(): string;
|
|
4470
|
+
/**
|
|
4471
|
+
* Whether the request path drops foreign thinking blocks (issue #147).
|
|
4472
|
+
*
|
|
4473
|
+
* Optional, absent reads as OFF: the repair changes bytes the upstream counts,
|
|
4474
|
+
* so a Host that cannot answer must not be read as consent.
|
|
4475
|
+
*/
|
|
4476
|
+
dropForeignThinking?(): boolean;
|
|
4477
|
+
/**
|
|
4478
|
+
* What the LAST request had to drop, for the card's explanation of a turn that
|
|
4479
|
+
* arrived looking like working notes (issue #147).
|
|
4480
|
+
*
|
|
4481
|
+
* Optional, and absent means "nothing to report" — the card then renders no
|
|
4482
|
+
* warning rather than a permanent one. Process-local and last-wins: it
|
|
4483
|
+
* describes the turn the user is looking at, not a historical tally.
|
|
4484
|
+
*/
|
|
4485
|
+
latestForeignThinking?(): {
|
|
4486
|
+
blocks: readonly {
|
|
4487
|
+
messageIndex: number;
|
|
4488
|
+
blockIndex: number;
|
|
4489
|
+
characters: number;
|
|
4490
|
+
variant: 'flattened' | 'dropped';
|
|
4491
|
+
}[];
|
|
4492
|
+
atMs: number;
|
|
4493
|
+
} | undefined;
|
|
4380
4494
|
/**
|
|
4381
4495
|
* The LIVE load level per model id, for the card's icon.
|
|
4382
4496
|
*
|
|
@@ -5045,6 +5159,26 @@ interface Config {
|
|
|
5045
5159
|
* entry is matched literally at the start of a trailing `user` message.
|
|
5046
5160
|
*/
|
|
5047
5161
|
extraInjectedPrefixes?: string[];
|
|
5162
|
+
/**
|
|
5163
|
+
* Drop foreign thinking blocks pi-ai would publish as prose (issue #147).
|
|
5164
|
+
*
|
|
5165
|
+
* When a session moves between providers, `dsh-llm-pi-ai` rebuilds the other
|
|
5166
|
+
* provider's assistant turns with `api: 'dsh-foreign'`, and pi-ai's
|
|
5167
|
+
* `isSameModel` then fails — so it does not DROP their `thinking` blocks, it
|
|
5168
|
+
* rewrites each one to plain `text` (`transform-messages.js:87–90`). The
|
|
5169
|
+
* other model's working notes therefore arrive as this turn's answer, and the
|
|
5170
|
+
* model copies that shape on every later turn.
|
|
5171
|
+
*
|
|
5172
|
+
* On, those blocks are removed from the request instead: they carry no
|
|
5173
|
+
* `thinkingSignature` (the host builds them without one), so they could never
|
|
5174
|
+
* be replayed as reasoning anyway — their only possible contribution to the
|
|
5175
|
+
* wire was the prose they turned into. Text blocks and tool calls are
|
|
5176
|
+
* untouched, and so is every message's identity.
|
|
5177
|
+
*
|
|
5178
|
+
* OFF by default: it changes bytes the upstream counts, so it is an explicit
|
|
5179
|
+
* opt-in on the same rule as {@link Config.usageIdentity}.
|
|
5180
|
+
*/
|
|
5181
|
+
dropForeignThinking?: boolean;
|
|
5048
5182
|
/**
|
|
5049
5183
|
* Move the host's session-title request onto the hidden lightweight model
|
|
5050
5184
|
* (issues #132/#133). Off by default: it is a real upstream request either
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-
|
|
1
|
+
import { $ as parseCreditMultiplier, $t as withFileLock, A as isEncryptedCredentialError, At as pickMediaModel, B as WorkBuddyCredentialRejectedError, Bt as deriveAtRestKeyId, C as WorkBuddyCredentialStore, Ct as IMAGE_TAG_TEXT_TO_IMAGE, D as defaultDesktopAuthDirs, Dt as imageMimeForPath, E as defaultDesktopAuthCandidates, Et as imageDataUrl, F as workbuddyAccountId, Ft as imageDefaultFor, G as forceNeutralHead, Gt as isWorkbuddyBundle, H as channelGateMemberOf, Ht as findWorkbuddyAppExecutable, I as workbuddyOwnAuthPath, It as nativeModalityOf, J as isCredentialRejectedError, Jt as openEncryptedField, K as isContextTooLongBody, Kt as macosBundleExecutable, L as CONTEXT_OVERFLOW_MARKER, Lt as WORKBUDDY_APP_EXECUTABLE_ENV, M as legacyWorkbuddyOwnAuthPath, Mt as parseQueueAnswer, N as parseWorkBuddyAuth, Nt as queueNoticeText, O as defaultDesktopAuthPath, Ot as isBareBase64Image, P as preferredAccountName, Pt as NATIVE_MODALITY_BY_MODEL_ID, Q as openingEvidenceOfBody, Qt as resolveDshHome, R as CREDENTIAL_REJECTED_CODE, Rt as clearAtRestKeyCache, S as WORKBUDDY_AUTH_FILE_ENV, St as IMAGE_TAG_IMAGE_TO_IMAGE, T as authFileName, Tt as WorkBuddyImageError, U as classifyUpstreamError, V as WorkBuddyUpstreamClient, Vt as fetchAtRestKeyPayload, W as declaredTools, Wt as isEncryptedFieldWrapper, X as knownRegionOf, Xt as workbuddyAppExecutableCandidates, Y as isFirstMessageGateBody, Yt as readAtRestKey, Z as modelIdOfBody, _ as deriveCatalog, _t as WorkBuddyVideoError, a as readHostHeartbeat, at as requestEvidenceOf, b as ENCRYPTED_CREDENTIAL_CODE, bt as resolveVideoParams, c as WORKBUDDY_CONNECT_VERSION, ct as REFUSES_OFF_MODEL_IDS, dt as offDefaultFor, en as writeFileAtomic, et as parseReasoning, f as piAiRuntimeInfo, ft as offRefused, g as applyContextBudgets, gt as VIDEO_TAG_TEXT_TO_VIDEO, h as WorkBuddyCatalog, ht as VIDEO_TAG_IMAGE_TO_VIDEO, i as processStartTimeMs, it as regionOf, j as isVaultAccountId, jt as sniffImageMime, k as hasEncryptedCredentialFields, kt as parseImageDataUrl, l as createFileRosterSeenStore, lt as defaultOffModelIds, m as FALLBACK_WORKBUDDY_MODELS_GLOBAL, mt as withEffectiveOff, n as clearHostHeartbeat, nt as perturbChannelFingerprints, o as workbuddyHostHeartbeatPath, ot as rewriteSessionTitleModel, p as FALLBACK_WORKBUDDY_MODELS, pt as recordRefusal, q as isConversationIdGateBody, qt as macosNestedAppBundles, r as isHeartbeatProcessAlive, rt as prepareChatBody, s as writeHostHeartbeat, st as attachBusyLevels, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as parseUpstreamModel, ut as lastRefusalFor, v as fallbackModelsFor, vt as parseSupportedSecondsRange, w as WorkBuddyEncryptedCredentialError, wt as MAX_INPUT_IMAGE_BYTES, x as WORKBUDDY_AUTH_FILENAME, xt as DEFAULT_IMAGE_SIZE, y as withoutRouters, yt as pickVideoModel, z as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, zt as deriveAtRestKey } from "./host-heartbeat-xEJRdYaC.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -1574,9 +1574,140 @@ function alignAssistantIdentities(context, identity = WORKBUDDY_MODEL_IDENTITY,
|
|
|
1574
1574
|
messages: next
|
|
1575
1575
|
} : context;
|
|
1576
1576
|
}
|
|
1577
|
-
|
|
1577
|
+
/** Read `content` as a block array without assuming the shape. */
|
|
1578
|
+
function blocksOf$1(message) {
|
|
1579
|
+
if (typeof message !== "object" || message === null) return [];
|
|
1580
|
+
const content = message["content"];
|
|
1581
|
+
return Array.isArray(content) ? content : [];
|
|
1582
|
+
}
|
|
1583
|
+
/** Whether this assistant message's identity proves a DIFFERENT model produced it. */
|
|
1584
|
+
function isForeignAssistant(message, identity) {
|
|
1585
|
+
if (typeof message !== "object" || message === null) return false;
|
|
1586
|
+
const record = message;
|
|
1587
|
+
if (record["role"] !== "assistant") return false;
|
|
1588
|
+
if (record["api"] === "dsh-foreign") return true;
|
|
1589
|
+
const provider = record["provider"];
|
|
1590
|
+
return typeof provider === "string" && provider !== identity && provider !== "workbuddy" && provider !== "workbuddy-global";
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Every foreign thinking block pi-ai would turn into visible prose (issue #147).
|
|
1594
|
+
*
|
|
1595
|
+
* THE DEFECT. pi-ai keeps a historical `thinking` block only while
|
|
1596
|
+
* `isSameModel` holds — provider AND api AND model id all equal to the request
|
|
1597
|
+
* (`transform-messages.js:68`). For everything else it does NOT drop the block:
|
|
1598
|
+
* `:87–90` rewrites it to `{type:'text', text: thinking}`, which is the answer
|
|
1599
|
+
* channel. A session that moved between providers therefore arrives with its
|
|
1600
|
+
* other provider's working notes already spelled as prose, and the model reads
|
|
1601
|
+
* that shape and copies it.
|
|
1602
|
+
*
|
|
1603
|
+
* Issue #147 measured the consequence on a real 80-step turn: 56 assistant
|
|
1604
|
+
* messages carried an answer while their own `reasoning` was ≤ 1 character,
|
|
1605
|
+
* 20 of them held English working notes totalling 58,960 characters, and CJK
|
|
1606
|
+
* fell to 1.37% of the visible text where the preceding six turns had been
|
|
1607
|
+
* entirely Chinese. That is not a formatting complaint — it also pushed the
|
|
1608
|
+
* session from 696,966 to 758,005 total tokens against an 800,000 compression
|
|
1609
|
+
* threshold.
|
|
1610
|
+
*
|
|
1611
|
+
* WHY THIS IS READ-ONLY. Nothing here rewrites the transcript:
|
|
1612
|
+
* {@link dropForeignThinking} does, and the split keeps the DIAGNOSIS available
|
|
1613
|
+
* to surfaces that must not change bytes (the card, the reply notice).
|
|
1614
|
+
*
|
|
1615
|
+
* @param context - a pi-ai context, before or after `alignAssistantIdentities`.
|
|
1616
|
+
* @param identity - the identity this adapter stamps on its own models.
|
|
1617
|
+
* @returns one entry per affected block, in transcript order.
|
|
1618
|
+
*/
|
|
1619
|
+
function foreignThinkingOf(context, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
1620
|
+
const messages = context.messages;
|
|
1621
|
+
if (!Array.isArray(messages)) return [];
|
|
1622
|
+
const found = [];
|
|
1623
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
1624
|
+
if (!isForeignAssistant(message, identity)) continue;
|
|
1625
|
+
const stopReason = message["stopReason"];
|
|
1626
|
+
const variant = stopReason === "error" || stopReason === "aborted" ? "dropped" : "flattened";
|
|
1627
|
+
for (const [blockIndex, block] of blocksOf$1(message).entries()) {
|
|
1628
|
+
if (typeof block !== "object" || block === null) continue;
|
|
1629
|
+
const fields = block;
|
|
1630
|
+
if (fields["type"] !== "thinking") continue;
|
|
1631
|
+
if (fields["redacted"] === true) continue;
|
|
1632
|
+
const thinking = fields["thinking"];
|
|
1633
|
+
const characters = typeof thinking === "string" ? thinking.length : 0;
|
|
1634
|
+
if (characters === 0) continue;
|
|
1635
|
+
found.push({
|
|
1636
|
+
messageIndex,
|
|
1637
|
+
blockIndex,
|
|
1638
|
+
characters,
|
|
1639
|
+
variant
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
return found;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Remove the foreign thinking blocks {@link foreignThinkingOf} finds.
|
|
1647
|
+
*
|
|
1648
|
+
* WHY REMOVAL IS THE HONEST REPAIR. The alternatives are both worse:
|
|
1649
|
+
*
|
|
1650
|
+
* - LEAVE IT: pi-ai publishes the other provider's private working notes as
|
|
1651
|
+
* this turn's answer (the #147 defect), so the user reads reasoning instead
|
|
1652
|
+
* of a reply and the model imitates that shape forever after.
|
|
1653
|
+
* - KEEP IT AS THINKING: impossible from here. `isSameModel` is pi-ai's own
|
|
1654
|
+
* predicate over fields this plugin does not control (`model.provider` /
|
|
1655
|
+
* `model.api` / `model.id` are compared against the MESSAGE's), and the
|
|
1656
|
+
* only way to satisfy it is to claim the foreign turn as this model's own
|
|
1657
|
+
* output — which is exactly the provenance lie issue #111 removed. These
|
|
1658
|
+
* blocks carry no `thinkingSignature` (the host builds them that way), so
|
|
1659
|
+
* they could not be replayed as signed reasoning even if claimed.
|
|
1660
|
+
*
|
|
1661
|
+
* Dropping is what the request builder already does with them in every other
|
|
1662
|
+
* branch: an unsigned foreign thinking block is never sent as
|
|
1663
|
+
* `reasoning_content`, so its only possible contribution to the wire was the
|
|
1664
|
+
* flattened prose this function removes. Every other block — text, tool calls —
|
|
1665
|
+
* is left exactly where it is, and so is every message's identity.
|
|
1666
|
+
*
|
|
1667
|
+
* Idempotent: a second pass finds nothing to remove. Returns the SAME context
|
|
1668
|
+
* object when nothing changed, so the common case allocates nothing.
|
|
1669
|
+
*
|
|
1670
|
+
* @param context - a pi-ai context.
|
|
1671
|
+
* @param identity - the identity this adapter stamps on its own models.
|
|
1672
|
+
* @returns the repaired context, or the input when no block qualified.
|
|
1673
|
+
*/
|
|
1674
|
+
function dropForeignThinking(context, identity = WORKBUDDY_MODEL_IDENTITY) {
|
|
1675
|
+
const affected = foreignThinkingOf(context, identity);
|
|
1676
|
+
if (affected.length === 0) return context;
|
|
1677
|
+
const messages = context.messages;
|
|
1678
|
+
const byMessage = /* @__PURE__ */ new Map();
|
|
1679
|
+
for (const block of affected) {
|
|
1680
|
+
const set = byMessage.get(block.messageIndex) ?? /* @__PURE__ */ new Set();
|
|
1681
|
+
set.add(block.blockIndex);
|
|
1682
|
+
byMessage.set(block.messageIndex, set);
|
|
1683
|
+
}
|
|
1684
|
+
const next = messages.map((message, index) => {
|
|
1685
|
+
const remove = byMessage.get(index);
|
|
1686
|
+
if (remove === void 0) return message;
|
|
1687
|
+
const record = message;
|
|
1688
|
+
const content = blocksOf$1(message).filter((_, blockIndex) => !remove.has(blockIndex));
|
|
1689
|
+
return {
|
|
1690
|
+
...record,
|
|
1691
|
+
content
|
|
1692
|
+
};
|
|
1693
|
+
});
|
|
1694
|
+
return {
|
|
1695
|
+
...context,
|
|
1696
|
+
messages: next
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function withLegacyContext(api, generation, identity = WORKBUDDY_MODEL_IDENTITY, ownedRoutes = [WORKBUDDY_PROVIDER, WORKBUDDY_GLOBAL_PROVIDER], repair) {
|
|
1578
1700
|
/** One place so both branches and both entry points agree on the order. */
|
|
1579
|
-
const prepare = (context) =>
|
|
1701
|
+
const prepare = (context) => {
|
|
1702
|
+
const aligned = alignAssistantIdentities(generation === "modern" ? foldModernContext(context) : adaptLegacyPiAiContext(context), identity, ownedRoutes);
|
|
1703
|
+
if (repair?.dropForeignThinking !== true) return aligned;
|
|
1704
|
+
const affected = foreignThinkingOf(aligned, identity);
|
|
1705
|
+
if (affected.length === 0) return aligned;
|
|
1706
|
+
try {
|
|
1707
|
+
repair.onForeignThinking?.(affected);
|
|
1708
|
+
} catch {}
|
|
1709
|
+
return dropForeignThinking(aligned, identity);
|
|
1710
|
+
};
|
|
1580
1711
|
if (generation === "modern") return {
|
|
1581
1712
|
...api,
|
|
1582
1713
|
stream: (model, context, options) => api.stream(model, transcript(prepare(context)), options),
|
|
@@ -1763,7 +1894,12 @@ function createWorkBuddyAdapter(options) {
|
|
|
1763
1894
|
}
|
|
1764
1895
|
} },
|
|
1765
1896
|
models: buildModels(),
|
|
1766
|
-
api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation, modelIdentity
|
|
1897
|
+
api: withContextRescue(withLegacyContext(openAICompletionsApi(), generation, modelIdentity, void 0, {
|
|
1898
|
+
get dropForeignThinking() {
|
|
1899
|
+
return options.dropForeignThinking?.() === true;
|
|
1900
|
+
},
|
|
1901
|
+
...options.onForeignThinking === void 0 ? {} : { onForeignThinking: options.onForeignThinking }
|
|
1902
|
+
}), options.resolveContextRescue)
|
|
1767
1903
|
}),
|
|
1768
1904
|
getModels: () => buildModels()
|
|
1769
1905
|
};
|
|
@@ -9276,7 +9412,8 @@ const UPSTREAM_SWITCH_FIELDS = /* @__PURE__ */ new Set([
|
|
|
9276
9412
|
"usageIdentity",
|
|
9277
9413
|
"keepPromptLast",
|
|
9278
9414
|
"extraInjectedPrefixes",
|
|
9279
|
-
"sessionTitle"
|
|
9415
|
+
"sessionTitle",
|
|
9416
|
+
"dropForeignThinking"
|
|
9280
9417
|
]);
|
|
9281
9418
|
/** A non-null, non-array object: the only shape the slot merge applies to. */
|
|
9282
9419
|
function isPlainRecord(value) {
|
|
@@ -9568,6 +9705,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
9568
9705
|
const cliBridgeModel = deps.cliBridgeModel?.() ?? "";
|
|
9569
9706
|
const cliBridgeEffort = deps.cliBridgeEffort?.() ?? "";
|
|
9570
9707
|
const cliBridgeRegionOrder = deps.cliBridgeRegionOrder?.() ?? "";
|
|
9708
|
+
const dropForeignThinking = deps.dropForeignThinking?.() === true;
|
|
9709
|
+
const lastForeignThinking = deps.latestForeignThinking?.();
|
|
9571
9710
|
const busyLevels = deps.busyLevels?.(region) ?? {};
|
|
9572
9711
|
const authStatus = await store.status();
|
|
9573
9712
|
const selectionExplicit = store.hasExplicitSelection();
|
|
@@ -9607,6 +9746,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
9607
9746
|
cliBridgeModel,
|
|
9608
9747
|
cliBridgeEffort,
|
|
9609
9748
|
cliBridgeRegionOrder,
|
|
9749
|
+
dropForeignThinking,
|
|
9750
|
+
...lastForeignThinking === void 0 ? {} : { lastForeignThinking },
|
|
9610
9751
|
busyLevels,
|
|
9611
9752
|
...await store.selectionLost() ? { selectionLost: true } : {},
|
|
9612
9753
|
...webAccounts.length === 0 ? await searchedPaths(store) : {}
|
|
@@ -9631,6 +9772,8 @@ async function workBuddyWebStatus(deps, region, options = {}) {
|
|
|
9631
9772
|
cliBridgeModel,
|
|
9632
9773
|
cliBridgeEffort,
|
|
9633
9774
|
cliBridgeRegionOrder,
|
|
9775
|
+
dropForeignThinking,
|
|
9776
|
+
...lastForeignThinking === void 0 ? {} : { lastForeignThinking },
|
|
9634
9777
|
busyLevels,
|
|
9635
9778
|
accounts: accounts.map(toWebAccount),
|
|
9636
9779
|
models: deps.displayModels(region).map((model) => toWebModel(model, deps.contextBudgets(region))).map((model) => model.reasoning === void 0 || lastRefusalFor(model.id) === void 0 ? model : {
|
|
@@ -10179,7 +10322,7 @@ function registerWorkBuddyStatusRoute(ctx, deps) {
|
|
|
10179
10322
|
req.on("error", reject);
|
|
10180
10323
|
});
|
|
10181
10324
|
const field = body.field;
|
|
10182
|
-
if (field !== "regions" && field !== "accounts" && field !== "cliBridge" && !(typeof field === "string" && UPSTREAM_SWITCH_FIELDS.has(field))) return json(res, 400, { error: "field must be regions, accounts, cliBridge, usageIdentity, keepPromptLast, extraInjectedPrefixes or
|
|
10325
|
+
if (field !== "regions" && field !== "accounts" && field !== "cliBridge" && !(typeof field === "string" && UPSTREAM_SWITCH_FIELDS.has(field))) return json(res, 400, { error: "field must be regions, accounts, cliBridge, usageIdentity, keepPromptLast, extraInjectedPrefixes, sessionTitle or dropForeignThinking" });
|
|
10183
10326
|
const row = settings.describe().find((r) => String(r.ns).includes("workbuddy"));
|
|
10184
10327
|
if (row === void 0) return json(res, 503, { error: "workbuddy namespace missing from describe()" });
|
|
10185
10328
|
const current = field === "regions" ? regionsMapOf(row.value?.[field]) : unwrapVolatileDeep(row.value?.[field] ?? {});
|
|
@@ -10590,6 +10733,7 @@ const Config = z.object({
|
|
|
10590
10733
|
usageIdentity: asVolatile(z.boolean().default(false).description("Make the upstream's usage page record these calls like the official client's: add the X-IDE-* identity headers (which fill the 使用端 column) and send the conversation ids as 32-hex UUIDv7. Off by default — it is a display alignment measured by a reporter, not by this plugin, and the chat channel identity (User-Agent) and request body never change.")),
|
|
10591
10734
|
keepPromptLast: asVolatile(z.boolean().default(false).description("Move the harness-injected runtime context above the real prompt, so the WorkBuddy usage page records what you actually typed instead of the injected block. Message count and text are preserved; the one edit is the recorded prompt's own leading <acp> marker. Off by default because it changes the message order the upstream counts.")),
|
|
10592
10735
|
extraInjectedPrefixes: asVolatile(z.array(z.string()).default([]).description("Extra literal prefixes (issue #133) treated like the harness-injected context markers when keepPromptLast is on — for blocks a companion plugin injects that the built-in list does not recognize. Each entry matches the start of a trailing user message; read only while keepPromptLast is enabled. Sampled when the plugin mounts: reload the plugin (or restart the host) after changing it.")),
|
|
10736
|
+
dropForeignThinking: asVolatile(z.boolean().default(false).description("Drop foreign thinking blocks that pi-ai would publish as prose (issue #147). After a provider switch the host rebuilds the other provider's assistant turns as dsh-foreign, and pi-ai then rewrites their thinking blocks into visible text, so the model's working notes become the answer and it copies that shape on later turns. On, those unsigned blocks are removed from the request (they could never be replayed as reasoning anyway); text and tool calls are untouched. Off by default because it changes bytes the upstream counts.")),
|
|
10593
10737
|
sessionTitle: asVolatile(z.object({
|
|
10594
10738
|
enabled: z.boolean().default(false),
|
|
10595
10739
|
model: z.string().default("wb-lite")
|
|
@@ -10883,6 +11027,21 @@ function apply(ctx, config) {
|
|
|
10883
11027
|
for (const model of live) if (model.busyLevel !== void 0) levels[model.id] = model.busyLevel;
|
|
10884
11028
|
return levels;
|
|
10885
11029
|
};
|
|
11030
|
+
/**
|
|
11031
|
+
* The most recent request whose foreign thinking blocks were dropped
|
|
11032
|
+
* (issue #147).
|
|
11033
|
+
*
|
|
11034
|
+
* Process-local and last-wins, deliberately: this is DIAGNOSTIC state that
|
|
11035
|
+
* answers "why did my answer look like working notes", and it is only read by
|
|
11036
|
+
* the status endpoint. Persisting it would let a stale turn from yesterday
|
|
11037
|
+
* explain a session that has since been fixed — the whole point is that the
|
|
11038
|
+
* reader is looking at the turn that just happened. It forgets on restart,
|
|
11039
|
+
* which is the natural expiry for "what this process removed recently".
|
|
11040
|
+
*
|
|
11041
|
+
* Declared HERE rather than inside the adapter mount because the status route
|
|
11042
|
+
* is registered outside that callback and must be able to read it.
|
|
11043
|
+
*/
|
|
11044
|
+
let latestForeignThinking;
|
|
10886
11045
|
let current = () => config;
|
|
10887
11046
|
let invalidateCatalog = () => {};
|
|
10888
11047
|
/**
|
|
@@ -11406,6 +11565,19 @@ function apply(ctx, config) {
|
|
|
11406
11565
|
cliBridgeEffort: () => unwrapVolatile(current().cliBridge)?.effort ?? "",
|
|
11407
11566
|
cliBridgeRegionOrder: () => unwrapVolatile(current().cliBridge)?.regionOrder ?? "",
|
|
11408
11567
|
/**
|
|
11568
|
+
* Whether the request path drops foreign thinking blocks (issue #147), read
|
|
11569
|
+
* live so the card's switch reflects the value the NEXT request will use
|
|
11570
|
+
* rather than the one captured at mount.
|
|
11571
|
+
*/
|
|
11572
|
+
dropForeignThinking: () => unwrapVolatile(current().dropForeignThinking) === true,
|
|
11573
|
+
/**
|
|
11574
|
+
* What the last request had to drop, so the card can explain a turn that
|
|
11575
|
+
* arrived looking like working notes instead of leaving the user to conclude
|
|
11576
|
+
* the model broke. `undefined` until a request actually removes something —
|
|
11577
|
+
* an empty report would render as a permanent warning.
|
|
11578
|
+
*/
|
|
11579
|
+
latestForeignThinking: () => latestForeignThinking,
|
|
11580
|
+
/**
|
|
11409
11581
|
* WHERE the readout sits, resolved here rather than read from the browser
|
|
11410
11582
|
* settings mirror, for the same measured reason as the flag above: on the
|
|
11411
11583
|
* affected 0.1.7 deployments that mirror never picks up this plugin's
|
|
@@ -12279,7 +12451,14 @@ function apply(ctx, config) {
|
|
|
12279
12451
|
provider: WORKBUDDY_PROVIDERS[region],
|
|
12280
12452
|
displayName: WORKBUDDY_PROVIDER_DISPLAY_NAMES[region],
|
|
12281
12453
|
resolveAttachments: () => ctx.get("attachments"),
|
|
12282
|
-
resolveContextRescue: () => contextRescue
|
|
12454
|
+
resolveContextRescue: () => contextRescue,
|
|
12455
|
+
dropForeignThinking: () => unwrapVolatile(current().dropForeignThinking) === true,
|
|
12456
|
+
onForeignThinking: (blocks) => {
|
|
12457
|
+
latestForeignThinking = {
|
|
12458
|
+
blocks,
|
|
12459
|
+
atMs: Date.now()
|
|
12460
|
+
};
|
|
12461
|
+
}
|
|
12283
12462
|
});
|
|
12284
12463
|
invalidateCatalog = () => {
|
|
12285
12464
|
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": "5.
|
|
5
|
+
"version": "5.12.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|