dsh-connect-workbuddy 5.3.1 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.4.0 (2026-10-22)
4
+
5
+ > 少数版本:修四处真实缺陷,**请求路径、provider 路由、计费与凭据一律未变**。其中三处是「代码写好了但没被走到」的缺陷——功能早就存在,只是有一整类真实报文从未进入它们。
6
+
7
+ ### Fixes
8
+
9
+ - **模型目录不再被陈旧快照锁死(#114 + #115,同一处缺陷的两面)**
10
+ - **病因**:`configuredModels()` / `displayModels()` 把「已保存目录 `lastCatalog`」当作第一优先级,而它**唯一的写入者**是卡片保存、写回的又是**它自己读到的目录**——闭环成立,实时目录被永久压制。于是上游新增的模型(CN 的 `deepseek-v4.1-flash`)永远进不来;更糟的是**冷启动播种**会在上游尚未应答时把静态兜底表写进 `lastCatalog`,而那张表**不含 `reasoning`**,每个模型因此以 `reasoning: false` 发布,宿主只给 `Default` / `Off`。
11
+ - **修法**:目录解析改为「**实时 > 已保存 > 静态兜底**」。已保存目录只承担**名字与勾选**,能力字段(`reasoning` / `creditMultiplier` / `supportsImages`)一律取自实时目录(新增 `src/catalog-sync.ts`)。上游已撤回但用户仍在用的模型继续保留(#32 的规则不变)。
12
+ - **自愈**:实时目录到达后按**内容指纹**比对回写 `lastCatalog`,只在真有差异时写、幂等(新增 `src/catalog-write.ts`),所以升级后无需手动清 profile。与兜底表逐项相同且不含能力字段的旧快照被识别为「从未配置」,不再当真。
13
+ - **播种不再写兜底表**:`src/web-status.ts` 播种前先判断「这份目录是不是兜底表本身」,是则跳过。
14
+ - **`busyLevel` 绝不进入卡片目录**:它是「这一分钟」的事实(`src/busy.ts`),且显式 `undefined` 会被设置写入的严格 JSON 编解码器拒绝,因此按 key 删除而非赋 `undefined`。
15
+
16
+ - **`11128` 一族按 code 整体识别,成员由插件自己的证据点名(#113)**
17
+ - **病因**:旧 `isChannelRejectBody()` 要求「带渠道措辞」**且**「不带另两个门的措辞」。而 #113 实测到 `msg: "conversation id missing"` 出现在**携带全部三个会话头**的请求上——措辞不可信。后果不是文案问题:该 body 被判成 `client`,而 `src/shim.ts` 的两级救援阶梯(`forceNeutralHead` → `perturbChannelFingerprints`)是**按 kind 门控**的,`client` 根本不进那个分支。**门 A、门 B 的救援都写好了,但这一整类真实报文从未走到它们面前**——这解释了「升级了却没变化」的反馈。
18
+ - **修法**:`isChannelRejectBody()` 现在回答「是否属于 11128 一族」,只按 `"code":11128` 判定;新增 `channelGateMemberOf()`,用**请求侧证据**(是否带了会话头 / 首条是否 system)点名成员 `prompt` / `headers` / `first_message` / `unknown`,只影响**建议文案**、不影响分类。回复会回显上游原始 `msg`(新增 `UpstreamErrorDetail.rawMsg`)并说明为何可信或不可信;不认识的措辞明确写「没点名是哪一道门」,而不是猜一个诊断。
19
+
20
+ - **扰动补上词内断点(#113 暴露的真实缺口)**
21
+ - 原来只在 `.` `!` `?` `,` `;` 之后插入零宽字符,所以**结尾没有标点的裸产品名完全不会被改动**——例如 DSH agent 提示的首行 `You are an AI agent powered by DeepSeek Harness` 正好在文末,`perturbChannelFingerprints()` 直接返回 `undefined`(「无可改动」),救援空转。现在按「大写词 + 大写词」加词内断点(`DeepSeek|Harness`),仍只在**上游已拒绝之后**作为最后一级重试发生,正常请求一个字节都不动。
22
+
23
+ - **`alignAssistantIdentities` 只改本插件产生的消息(#111)**
24
+ - **病因**:原实现对**每一条** `role: 'assistant'` 消息重写 `provider` / `api`,其 docblock 断言「transcript 里不会有别的 provider 的 assistant 轮次」。**该前提不成立**:`dsh-llm` 的 `forAdapter()` 只剥掉外来消息的 `replayState`,消息本身留在列表里。于是切换过 provider 的会话会把别人的轮次改写成 `workbuddy` / `openai-completions`——等于把宿主刚关上的「可重放」那道门重新打开,并让 transcript 宣称一个不成立的来源。
25
+ - **修法**:重写范围收敛到本 bundle 的两个路由(`workbuddy` / `workbuddy-global`)或已带 identity 的消息,其余**原对象返回**。#102(全球区 thinking 被压平)与 #59(跨区 identity)的修复均不受影响。`HOST_FOREIGN_API` 改为导出以便测试钉住。
26
+
27
+ ### Docs
28
+
29
+ - 订正三处此前写错的论证:`reasoning` **不会**被设置写入路径剥掉(实测 `projectForm` / standard validate 都完整保留;`simplify()` 只是表单投影、不在写入路径上);「回显的 `You are a helpful assistant.` 就是被拒的开头」实为插件自己垫的中性头挡住了真实开头(4.15.x 已改为从 `raw` 读证据);`systemOpeningsOfBody()` 只读字符串 content 的数组形状盲区已由 `openingEvidenceOfBody()` 取代。
30
+
31
+ ### Tests
32
+
33
+ - 新增 `tests/catalog-sync.spec.ts`(22 条)、`tests/issue113-channel-gate-evidence.spec.ts`(17 条,含一条端到端「用 #113 的真实报文作第一次应答,断言插件真的发出了第二次请求」)、`tests/issue111-align-scope.spec.ts`(9 条)。
34
+ - 订正 `tests/issue90-channel-reject.spec.ts`、`tests/issue98-channel-gate.spec.ts`、`tests/issue98-shim.spec.ts` 里四处**钉住旧行为**的断言(它们把「11128 的另两个门归类为 `client`」当成契约,而那正是 #113 要修的东西)。
35
+ - 全量:**121 文件 / 1996 条通过(另 2 条 skip)**;两个 tsconfig 均 exit 0;build 成功。
36
+
3
37
  ## 5.3.1 (2026-10-22)
4
38
 
5
39
  > 补丁版本:5.3.0 已经发布了负载徽标,这一版修它的**消失**问题,并把它的**呈现方式**换成更好认的样子。 **没有新增任何功能**:模型目录、请求路径、provider 路由、计费与凭据一律未变,5.3.0 的全部能力原样保留。
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { At as hostPeersInfo, C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, St as findWorkbuddyAppExecutableWithSource, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, gt as WORKBUDDY_APP_EXECUTABLE_ENV, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, w as defaultDesktopAuthDirs, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-BHESQ6Ah.js";
2
+ import { C as defaultDesktopAuthCandidates, L as WorkBuddyUpstreamClient, M as workbuddyOwnAuthPath, Mt as hostPeersInfo, a as readHostHeartbeat, b as WorkBuddyCredentialStore, c as WORKBUDDY_CONNECT_VERSION, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, k as legacyWorkbuddyOwnAuthPath, l as piAiRuntimeInfo, o as workbuddyHostHeartbeatPath, r as isHeartbeatProcessAlive, u as FALLBACK_WORKBUDDY_MODELS, vt as WORKBUDDY_APP_EXECUTABLE_ENV, w as defaultDesktopAuthDirs, wt as findWorkbuddyAppExecutableWithSource, y as WORKBUDDY_AUTH_FILE_ENV } from "./host-heartbeat-D5FWM_8k.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -2018,21 +2018,21 @@ const CONTEXT_OVERFLOW_MARKER = "prompt is too long (upstream code 11115)";
2018
2018
  */
2019
2019
  const CHANNEL_REJECT_MARKERS = ["unapproved channel", "illegal api invocation"];
2020
2020
  /**
2021
- * The other gates that answer the SAME business code.
2021
+ * The wordings that identify the OTHER two members of the `11128` family.
2022
2022
  *
2023
- * Excluded explicitly so a body that also carries the channel wording can never
2024
- * be read as the prompt rule: a confidently wrong diagnosis is worse here than
2025
- * the generic `client` reply, because it sends the user to rewrite a system
2026
- * prompt that is not at fault.
2023
+ * Used ONLY to NAME a member ({@link channelGateMemberOf}), never to exclude a
2024
+ * body from the family. The old code used them as an exclusion list, which is
2025
+ * how issue #113's misreported refusal was kept out of the plugin's retry
2026
+ * ladder: the upstream wrote `conversation id missing` over a content refusal,
2027
+ * and the exclusion believed it.
2027
2028
  *
2028
- * Each is also exported as its own predicate ({@link isConversationIdGateBody},
2029
+ * Each is exported as its own predicate ({@link isConversationIdGateBody},
2029
2030
  * {@link isFirstMessageGateBody}) so the reply can NAME the gate instead of
2030
2031
  * relaying the upstream's `displayTips` — which is the SAME misleading
2031
2032
  * sentence for all three members of the `11128` family.
2032
2033
  */
2033
2034
  const CONVERSATION_ID_GATE_MARKER = "conversation id missing";
2034
2035
  const FIRST_MESSAGE_GATE_MARKER = "first message is not system prompt";
2035
- const OTHER_CHANNEL_CODE_GATES = [CONVERSATION_ID_GATE_MARKER, FIRST_MESSAGE_GATE_MARKER];
2036
2036
  /**
2037
2037
  * Whether a failure body is the MODEL-scoped throttle rather than an account
2038
2038
  * limit.
@@ -2054,20 +2054,50 @@ function isModelBusyBody(body) {
2054
2054
  return body.includes(MODEL_BUSY_CODE);
2055
2055
  }
2056
2056
  /**
2057
- * Whether a failure body is the gateway's CHANNEL gate rather than the other
2058
- * `11128` ({@link conversationHeaders}' `conversation id missing`).
2057
+ * Whether a failure body belongs to the `11128` gate family AT ALL.
2059
2058
  *
2060
- * Both halves are required. The code alone would claim the conversation-id gate
2061
- * too, and its reply would then blame the prompt's opening for a request-header
2062
- * problem the plugin already fixed — a confidently wrong diagnosis is worse
2063
- * here than the generic `client` reply, because it sends the user to rewrite a
2064
- * system prompt that is not at fault.
2059
+ * The code is matched in its quoted-key form (`"code":11128`): a request id is
2060
+ * hexadecimal and can contain the digits `11128` by coincidence, and a bare
2061
+ * substring search over an arbitrary failure body is how a red herring gets
2062
+ * promoted into a diagnosis.
2063
+ */
2064
+ function isChannelGateFamilyBody(body) {
2065
+ return body.includes(CHANNEL_REJECT_CODE);
2066
+ }
2067
+ /**
2068
+ * Whether a failure body is the gateway's CHANNEL gate — i.e. whether it
2069
+ * belongs to the `11128` FAMILY.
2070
+ *
2071
+ * ISSUE #113, AND WHY THIS NOW ANSWERS "IS IT THE FAMILY" RATHER THAN "IS IT
2072
+ * THE PROMPT MEMBER". The family has three members (prompt fingerprint,
2073
+ * missing conversation id, missing system head) sharing one business code and
2074
+ * one `displayTips` sentence, so the `msg` wording used to be the only thing
2075
+ * that could tell them apart — and the wording is NOT reliable. Issue #113
2076
+ * measured a CN refusal whose `msg` read `conversation id missing` while the
2077
+ * three conversation-id headers were all present and correct; the refusal was
2078
+ * about the request's CONTENT.
2079
+ *
2080
+ * The old rule required the channel wording AND the absence of the sibling
2081
+ * wordings, so such a body was excluded from the channel gate entirely and
2082
+ * filed as a plain `client`. That was not cosmetic: `client` never enters the
2083
+ * plugin's two-step retry ladder (`forceNeutralHead`, then the invisible
2084
+ * perturbation), so the rescue path that exists for precisely this refusal
2085
+ * never ran. That is very likely why the reporters in issue #98's thread found
2086
+ * that upgrading changed nothing.
2087
+ *
2088
+ * Every member of this family is a CHANNEL-verification refusal — the
2089
+ * upstream's own shared sentence says so — so acting on the family is correct
2090
+ * whoever the member is. Which member it is decides only what the reply
2091
+ * ADVISES, and that is {@link channelGateMemberOf}'s job, decided by the
2092
+ * plugin's own evidence rather than by a wording the upstream may misreport.
2093
+ *
2094
+ * The code is matched in its quoted-key form (`"code":11128`): a request id is
2095
+ * hexadecimal and can contain the digits `11128` by coincidence, and a bare
2096
+ * substring search over an arbitrary failure body is how a red herring gets
2097
+ * promoted into a diagnosis.
2065
2098
  */
2066
2099
  function isChannelRejectBody(body) {
2067
- if (!body.includes(CHANNEL_REJECT_CODE)) return false;
2068
- const lower = body.toLowerCase();
2069
- if (OTHER_CHANNEL_CODE_GATES.some((marker) => lower.includes(marker))) return false;
2070
- return CHANNEL_REJECT_MARKERS.some((marker) => lower.includes(marker));
2100
+ return isChannelGateFamilyBody(body);
2071
2101
  }
2072
2102
  /**
2073
2103
  * Whether a failure body is the CONVERSATION-ID gate (`11128`).
@@ -2110,6 +2140,46 @@ function isFirstMessageGateBody(body) {
2110
2140
  return lower.includes(CHANNEL_REJECT_CODE) && lower.includes(FIRST_MESSAGE_GATE_MARKER);
2111
2141
  }
2112
2142
  /**
2143
+ * Which member of the `11128` family a body is, decided by the plugin's OWN
2144
+ * evidence rather than by the upstream's wording.
2145
+ *
2146
+ * THE EVIDENCE, IN ORDER OF STRENGTH.
2147
+ *
2148
+ * 1. The body carries the channel wording (`unapproved channel` /
2149
+ * `illegal api invocation`). That is the upstream naming the PROMPT gate
2150
+ * explicitly, so it is trusted.
2151
+ * 2. The body says `conversation id missing`. The wording is only believable
2152
+ * when the request did NOT carry the conversation-id triple; the plugin's
2153
+ * `chatHeaders` adds it to every chat request, so in practice it is always
2154
+ * there and the wording is always a misreport — which is issue #113's
2155
+ * exact measurement. The refusal is then about the CONTENT, i.e. the
2156
+ * PROMPT gate, which is also the only member the plugin can act on.
2157
+ * 3. The body names the first-message gate. That gate is about the request's
2158
+ * SHAPE, and `ensureSystemHead` guarantees a `system` head whenever the
2159
+ * request carries messages, so it is only reported as that gate when the
2160
+ * request genuinely had none.
2161
+ * 4. Otherwise: `unknown`. The family is still the family — the retry ladder
2162
+ * applies — but the reply says it cannot name the member rather than
2163
+ * guessing, because a confidently wrong diagnosis sends the user to edit a
2164
+ * prompt that is not at fault.
2165
+ *
2166
+ * @param body The failure body.
2167
+ * @param request What the plugin knows about the request it just sent.
2168
+ */
2169
+ function channelGateMemberOf(body, request) {
2170
+ const lower = body.toLowerCase();
2171
+ if (CHANNEL_REJECT_MARKERS.some((marker) => lower.includes(marker))) return "prompt";
2172
+ if (lower.includes(CONVERSATION_ID_GATE_MARKER)) {
2173
+ if (request?.hasConversationHeaders === false) return "headers";
2174
+ return "prompt";
2175
+ }
2176
+ if (lower.includes(FIRST_MESSAGE_GATE_MARKER)) {
2177
+ if (request?.hasSystemHead === false) return "first_message";
2178
+ return "unknown";
2179
+ }
2180
+ return "unknown";
2181
+ }
2182
+ /**
2113
2183
  * The OPENINGS of the `system` messages in a prepared request body.
2114
2184
  *
2115
2185
  * REMOVED in favour of {@link systemOpeningEvidenceOfBody}, which answers the
@@ -2124,7 +2194,15 @@ const SYSTEM_OPENING_LIMIT = 120;
2124
2194
  function clip(text, limit) {
2125
2195
  return text.length <= limit ? text : `${text.slice(0, limit - 1)}…`;
2126
2196
  }
2127
- /** Classify an upstream failure from its HTTP status and body excerpt. */
2197
+ /**
2198
+ * Classify an upstream failure from its HTTP status and body excerpt.
2199
+ *
2200
+ * The `11128` family is classified BY ITS CODE, not by the `msg` wording —
2201
+ * see {@link isChannelRejectBody} and {@link channelGateMemberOf}. Which
2202
+ * member of the family it is decides the ADVICE in the reply, never the
2203
+ * classification: every member is a channel-verification refusal that no
2204
+ * account, region or top-up can change.
2205
+ */
2128
2206
  function classifyUpstreamError(status, body) {
2129
2207
  if (status === 402) return "hard_credit";
2130
2208
  const lower = body.toLowerCase();
@@ -2176,6 +2254,7 @@ function parseUpstreamErrorDetail(body) {
2176
2254
  const detail = {};
2177
2255
  if (typeof raw.code === "number") detail.upstreamCode = raw.code;
2178
2256
  if (typeof raw.requestId === "string") detail.requestId = raw.requestId;
2257
+ if (typeof raw.msg === "string" && raw.msg !== "") detail.rawMsg = raw.msg;
2179
2258
  if (displayMsg !== void 0) detail.displayMsg = displayMsg;
2180
2259
  if (tips !== void 0) detail.tips = tips;
2181
2260
  const ext = raw.extError;
@@ -2654,6 +2733,28 @@ function forceNeutralHead(bodyJson) {
2654
2733
  */
2655
2734
  const ZERO_WIDTH_SPACE = "​";
2656
2735
  /**
2736
+ * Where an invisible break is inserted INSIDE a capitalised word pair, so a
2737
+ * bare product name stops matching as an exact substring.
2738
+ *
2739
+ * WHY THE PUNCTUATION RULE IS NOT ENOUGH. The boundary rules above only land
2740
+ * where there is a `.`, `!`, `?`, `,`, `;` — so a scanned message that ends
2741
+ * with a bare product name and no trailing punctuation (``… powered by
2742
+ * DeepSeek Harness ``, the host's own agent line, ending at end-of-text) is
2743
+ * left COMPLETELY UNCHANGED. `perturbChannelFingerprints` then answers
2744
+ * `undefined`, the shim spends no retry, and the fingerprint survives the
2745
+ * rescue — measured, and it is the shape issue #113's reporter is most likely
2746
+ * to hit, because that line is the first line of every DSH agent prompt.
2747
+ *
2748
+ * This rule breaks between a capitalised word and the following capitalised
2749
+ * word (“DeepSeek|Harness”, “Visual|Studio”), which covers product names
2750
+ * without touching ordinary prose: a sentence rarely has two capitalised
2751
+ * words adjacent UNLESS they are a proper name, and breaking a proper name is
2752
+ * exactly the intent. The model reads straight through U+200B, so what it
2753
+ * understands is unchanged — that is the whole point of using it for this
2754
+ * rather than rewriting the text.
2755
+ */
2756
+ const WORD_INTERNAL_BREAK = /([A-Z][A-Za-z0-9]*)(\s+)(?=[A-Z][A-Za-z0-9])/gu;
2757
+ /**
2657
2758
  * A body whose scanned message TEXT is perturbed just enough to break an exact
2658
2759
  * content fingerprint.
2659
2760
  *
@@ -2709,7 +2810,7 @@ function perturbChannelFingerprints(bodyJson) {
2709
2810
  let changed = false;
2710
2811
  const perturb = (text) => {
2711
2812
  if (text.includes(ZERO_WIDTH_SPACE)) return void 0;
2712
- const next = text.replace(/([.!?])(\s+)/gu, `$1${ZERO_WIDTH_SPACE}$2`).replace(/([。!?])/gu, `$1${ZERO_WIDTH_SPACE}`).replace(/([,;])(\s+)/gu, `$1${ZERO_WIDTH_SPACE}$2`);
2813
+ const next = text.replace(/([.!?])(\s+)/gu, `$1${ZERO_WIDTH_SPACE}$2`).replace(/([。!?])/gu, `$1${ZERO_WIDTH_SPACE}`).replace(/([,;])(\s+)/gu, `$1${ZERO_WIDTH_SPACE}$2`).replace(WORD_INTERNAL_BREAK, (whole, head, tail) => `${head}${ZERO_WIDTH_SPACE}${tail}`);
2713
2814
  return next === text ? void 0 : next;
2714
2815
  };
2715
2816
  for (const value of messages) {
@@ -3278,6 +3379,57 @@ function withPromos(models, data) {
3278
3379
  return promos.length === 0 ? models : attachPromos(models, promos);
3279
3380
  }
3280
3381
  /**
3382
+ * What the plugin can tell about the request it just sent, for telling the
3383
+ * `11128` family apart.
3384
+ *
3385
+ * TWO FACTS, both read from the request body rather than from the upstream's
3386
+ * `msg` — which issue #113 measured to be unreliable (a content refusal
3387
+ * reported as `conversation id missing`).
3388
+ *
3389
+ * - `hasSystemHead`: does the request open with a `system` message? The
3390
+ * plugin guarantees it does whenever the request carries messages
3391
+ * (`ensureSystemHead`), so a body claiming the first-message gate is only
3392
+ * believable when this is false.
3393
+ * - `hasConversationHeaders`: did the request carry the conversation-id
3394
+ * triple? `chatHeaders` always adds it, so this is true in practice — and
3395
+ * that is exactly what makes a `conversation id missing` wording
3396
+ * unbelievable.
3397
+ *
3398
+ * Never throws: a body that cannot be read answers "unknown", which is the
3399
+ * safe direction (the wording is then believed).
3400
+ */
3401
+ function requestEvidenceOf(bodyJson) {
3402
+ const hasConversationHeaders = true;
3403
+ let parsed;
3404
+ try {
3405
+ parsed = JSON.parse(bodyJson);
3406
+ } catch {
3407
+ return {
3408
+ hasSystemHead: false,
3409
+ hasConversationHeaders
3410
+ };
3411
+ }
3412
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
3413
+ hasSystemHead: false,
3414
+ hasConversationHeaders
3415
+ };
3416
+ const messages = parsed["messages"];
3417
+ if (!Array.isArray(messages)) return {
3418
+ hasSystemHead: false,
3419
+ hasConversationHeaders
3420
+ };
3421
+ const first = messages[0];
3422
+ if (typeof first !== "object" || first === null || Array.isArray(first)) return {
3423
+ hasSystemHead: false,
3424
+ hasConversationHeaders
3425
+ };
3426
+ const role = first["role"];
3427
+ return {
3428
+ hasSystemHead: role === "system" || role === "developer",
3429
+ hasConversationHeaders
3430
+ };
3431
+ }
3432
+ /**
3281
3433
  * Upstream HTTP client. One instance serves the whole plugin; requests take
3282
3434
  * the credential explicitly so token refreshes apply on the next call.
3283
3435
  */
@@ -5804,7 +5956,7 @@ function generationOf(version) {
5804
5956
  //#endregion
5805
5957
  //#region src/version.ts
5806
5958
  /** The npm package version this build was produced from. */
5807
- const WORKBUDDY_CONNECT_VERSION = "5.3.1";
5959
+ const WORKBUDDY_CONNECT_VERSION = "5.4.0";
5808
5960
  //#endregion
5809
5961
  //#region src/host-heartbeat.ts
5810
5962
  /**
@@ -5962,4 +6114,4 @@ async function clearHostHeartbeat() {
5962
6114
  await rm(workbuddyHostHeartbeatPath(), { force: true });
5963
6115
  }
5964
6116
  //#endregion
5965
- export { regionOf as $, parseWorkBuddyAuth as A, hostPeersInfo as At, forceNeutralHead as B, defaultDesktopAuthCandidates as C, isEncryptedFieldWrapper as Ct, isEncryptedCredentialError as D, openEncryptedField as Dt, hasEncryptedCredentialFields as E, macosNestedAppBundles as Et, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as F, knownRegionOf as G, isConversationIdGateBody as H, WorkBuddyCredentialRejectedError as I, parseCreditMultiplier as J, modelIdOfBody as K, WorkBuddyUpstreamClient as L, workbuddyOwnAuthPath as M, withFileLock as Mt, CONTEXT_OVERFLOW_MARKER as N, writeFileAtomic as Nt, isVaultAccountId as O, readAtRestKey as Ot, CREDENTIAL_REJECTED_CODE as P, prepareChatBody as Q, classifyUpstreamError as R, authFileName as S, findWorkbuddyAppExecutableWithSource as St, defaultDesktopAuthPath as T, macosBundleExecutable as Tt, isCredentialRejectedError as U, isContextTooLongBody as V, isFirstMessageGateBody as W, parseUpstreamModel as X, parseReasoning as Y, perturbChannelFingerprints as Z, ENCRYPTED_CREDENTIAL_CODE as _, clearAtRestKeyCache as _t, readHostHeartbeat as a, offRefused as at, WorkBuddyCredentialStore as b, fetchAtRestKeyPayload as bt, WORKBUDDY_CONNECT_VERSION as c, DEFAULT_IMAGE_SIZE as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, parseQueueAnswer as dt, attachBusyLevels as et, WorkBuddyCatalog as f, queueNoticeText as ft, withoutRouters as g, WORKBUDDY_APP_EXECUTABLE_ENV as gt, fallbackModelsFor as h, nativeModalityOf as ht, processStartTimeMs as i, offDefaultFor as it, workbuddyAccountId as j, resolveDshHome as jt, legacyWorkbuddyOwnAuthPath as k, workbuddyAppExecutableCandidates as kt, piAiRuntimeInfo as l, WorkBuddyImageError as lt, deriveCatalog as m, imageDefaultFor as mt, clearHostHeartbeat as n, defaultOffModelIds as nt, workbuddyHostHeartbeatPath as o, recordRefusal as ot, applyContextBudgets as p, NATIVE_MODALITY_BY_MODEL_ID as pt, openingEvidenceOfBody as q, isHeartbeatProcessAlive as r, lastRefusalFor as rt, writeHostHeartbeat as s, withEffectiveOff as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, REFUSES_OFF_MODEL_IDS as tt, FALLBACK_WORKBUDDY_MODELS as u, pickImageModel as ut, WORKBUDDY_AUTH_FILENAME as v, deriveAtRestKey as vt, defaultDesktopAuthDirs as w, isWorkbuddyBundle as wt, WorkBuddyEncryptedCredentialError as x, findWorkbuddyAppExecutable as xt, WORKBUDDY_AUTH_FILE_ENV as y, deriveAtRestKeyId as yt, declaredTools as z };
6117
+ export { prepareChatBody as $, parseWorkBuddyAuth as A, readAtRestKey as At, declaredTools as B, defaultDesktopAuthCandidates as C, findWorkbuddyAppExecutable as Ct, isEncryptedCredentialError as D, macosBundleExecutable as Dt, hasEncryptedCredentialFields as E, isWorkbuddyBundle as Et, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as F, writeFileAtomic as Ft, isFirstMessageGateBody as G, isContextTooLongBody as H, WorkBuddyCredentialRejectedError as I, openingEvidenceOfBody as J, knownRegionOf as K, WorkBuddyUpstreamClient as L, workbuddyOwnAuthPath as M, hostPeersInfo as Mt, CONTEXT_OVERFLOW_MARKER as N, resolveDshHome as Nt, isVaultAccountId as O, macosNestedAppBundles as Ot, CREDENTIAL_REJECTED_CODE as P, withFileLock as Pt, perturbChannelFingerprints as Q, channelGateMemberOf as R, authFileName as S, fetchAtRestKeyPayload as St, defaultDesktopAuthPath as T, isEncryptedFieldWrapper as Tt, isConversationIdGateBody as U, forceNeutralHead as V, isCredentialRejectedError as W, parseReasoning as X, parseCreditMultiplier as Y, parseUpstreamModel as Z, ENCRYPTED_CREDENTIAL_CODE as _, nativeModalityOf as _t, readHostHeartbeat as a, lastRefusalFor as at, WorkBuddyCredentialStore as b, deriveAtRestKey as bt, WORKBUDDY_CONNECT_VERSION as c, recordRefusal as ct, FALLBACK_WORKBUDDY_MODELS_GLOBAL as d, WorkBuddyImageError as dt, regionOf as et, WorkBuddyCatalog as f, pickImageModel as ft, withoutRouters as g, imageDefaultFor as gt, fallbackModelsFor as h, NATIVE_MODALITY_BY_MODEL_ID as ht, processStartTimeMs as i, defaultOffModelIds as it, workbuddyAccountId as j, workbuddyAppExecutableCandidates as jt, legacyWorkbuddyOwnAuthPath as k, openEncryptedField as kt, piAiRuntimeInfo as l, withEffectiveOff as lt, deriveCatalog as m, queueNoticeText as mt, clearHostHeartbeat as n, attachBusyLevels as nt, workbuddyHostHeartbeatPath as o, offDefaultFor as ot, applyContextBudgets as p, parseQueueAnswer as pt, modelIdOfBody as q, isHeartbeatProcessAlive as r, REFUSES_OFF_MODEL_IDS as rt, writeHostHeartbeat as s, offRefused as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, requestEvidenceOf as tt, FALLBACK_WORKBUDDY_MODELS as u, DEFAULT_IMAGE_SIZE as ut, WORKBUDDY_AUTH_FILENAME as v, WORKBUDDY_APP_EXECUTABLE_ENV as vt, defaultDesktopAuthDirs as w, findWorkbuddyAppExecutableWithSource as wt, WorkBuddyEncryptedCredentialError as x, deriveAtRestKeyId as xt, WORKBUDDY_AUTH_FILE_ENV as y, clearAtRestKeyCache as yt, classifyUpstreamError as z };
package/lib/index.d.ts CHANGED
@@ -1118,7 +1118,15 @@ interface WorkBuddyProbeAnswer {
1118
1118
  /** The live stream, only when `ok` is true. The caller drains and discards it. */
1119
1119
  response?: Response;
1120
1120
  }
1121
- /** Classify an upstream failure from its HTTP status and body excerpt. */
1121
+ /**
1122
+ * Classify an upstream failure from its HTTP status and body excerpt.
1123
+ *
1124
+ * The `11128` family is classified BY ITS CODE, not by the `msg` wording —
1125
+ * see {@link isChannelRejectBody} and {@link channelGateMemberOf}. Which
1126
+ * member of the family it is decides the ADVICE in the reply, never the
1127
+ * classification: every member is a channel-verification refusal that no
1128
+ * account, region or top-up can change.
1129
+ */
1122
1130
  declare function classifyUpstreamError(status: number, body: string): UpstreamErrorKind;
1123
1131
  /** Structured fields lifted out of a JSON upstream failure body. */
1124
1132
  interface UpstreamErrorDetail {
@@ -1126,6 +1134,17 @@ interface UpstreamErrorDetail {
1126
1134
  upstreamCode?: number;
1127
1135
  /** Upstream request id — the handle support asks for. */
1128
1136
  requestId?: string;
1137
+ /**
1138
+ * The upstream's RAW `msg`, kept beside {@link displayMsg}.
1139
+ *
1140
+ * On the `11128` family this is the DIAGNOSTIC half — it is what names which
1141
+ * gate fired ("conversation id missing", "first message is not system
1142
+ * prompt"), while `displayMsg` is the user-facing sentence the plugin must
1143
+ * NOT use as a diagnosis (issue #113 measured it to mislabel a content
1144
+ * refusal). Keeping both means a reply can echo what arrived AND say why it
1145
+ * is or is not believable, without one overwriting the other.
1146
+ */
1147
+ rawMsg?: string;
1129
1148
  /** Best human-facing message: `displayMsg.zh`, falling back to `.en`, then `msg`. */
1130
1149
  displayMsg?: string;
1131
1150
  /**