dsh-connect-workbuddy 5.8.1 → 5.8.3

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.
@@ -2115,6 +2115,27 @@ function normalizeVerdictText(text) {
2115
2115
  return text.normalize("NFKC").replace(/[\u200b-\u200d\ufeff]/gu, "").replace(/\s+/gu, " ").trim().toLowerCase();
2116
2116
  }
2117
2117
  /**
2118
+ * A `11140` body whose display fields decode to NOTHING at all.
2119
+ *
2120
+ * THE FAILURE MODE THIS CATCHES. `isAccountRestrictedBody` needs the business
2121
+ * code AND a phrase in `displayMsg` / `displayTips` / `msg` — deliberately, so a
2122
+ * shared sentence cannot bench an account. The mirror risk is a MISS: if the
2123
+ * upstream renames those fields, escapes them differently, or moves the
2124
+ * restriction wording somewhere new, every restriction degrades silently to
2125
+ * `policy_reject` and a benched-worthy account keeps taking traffic with no log
2126
+ * line anywhere. A genuine content refusal always carries the fields (its whole
2127
+ * point is a human-facing sentence), so a `11140` that decodes to no display
2128
+ * text at all is an anomalous envelope shape, not a normal refusal — exactly the
2129
+ * narrow condition worth a warning. Bodies WITH display text that merely lack
2130
+ * the restriction phrase are the everyday `policy_reject` and stay silent.
2131
+ *
2132
+ * Pure and side-effect-free: the caller (the transport layer, which holds the
2133
+ * injected logger) decides what to do with the answer.
2134
+ */
2135
+ function isUndecodablePolicyBody(body) {
2136
+ return isContentPolicyBody(body) && accountVerdictText(body) === "";
2137
+ }
2138
+ /**
2118
2139
  * Business code of the MODEL-scoped throttle (`14003`, "too many requests").
2119
2140
  *
2120
2141
  * The quoted-key form for the same reason as the policy markers: a request id is
@@ -3342,12 +3363,22 @@ function modelIdOfBody(bodyJson) {
3342
3363
  }
3343
3364
  }
3344
3365
  /**
3366
+ * The optional run of `billion-context` markers every injected-prefix match
3367
+ * tolerates in front of the marker text, and the built-in marker alternatives.
3368
+ */
3369
+ const INJECTED_ACP_MARKER_RUN = "(?:<acp\\b[^>]*>m\\d{1,8}<\\/acp>[ \\t]*\\r?\\n?)*";
3370
+ const INJECTED_BUILT_IN_PREFIXES = "Time sampled while preparing turn \\d+, step \\d+:|Current runtime context\\.|<system-reminder>|\\[Compressed conversation section\\]|This is an efficiency nudge";
3371
+ /**
3345
3372
  * Prefixes the DSH harness uses for the context it injects as `user` messages.
3346
3373
  *
3347
3374
  * MEASURED shapes, not guesses: the harness's time-context provider appends
3348
3375
  * `Time sampled while preparing turn N, step M: …`, its runtime-context
3349
3376
  * provider appends `Current runtime context. …`, and its reminders arrive as
3350
- * `<system-reminder>` blocks. `billion-context` may additionally wrap each
3377
+ * `<system-reminder>` blocks. `billion-context` injects its compression record
3378
+ * and its efficiency note as `user` messages too (issue #133): the compressed
3379
+ * section opens with a bracketed `[Compressed conversation section]` heading
3380
+ * line, and the nudge opens with `This is an efficiency …`. `billion-context`
3381
+ * may additionally wrap each
3351
3382
  * message in an `<acp …>mNNNNN</acp>` marker, so the pattern TOLERATES that
3352
3383
  * marker in front of an injected block — the marker itself is never removed
3353
3384
  * from an injected message (see {@link ACP_LEADING_TAG} for where a marker IS
@@ -3357,7 +3388,24 @@ function modelIdOfBody(bodyJson) {
3357
3388
  * as it arrived — the alternative (moving "the last non-empty user message")
3358
3389
  * would reorder real conversations from other clients.
3359
3390
  */
3360
- const INJECTED_USER_PREFIX = /^(?:<acp\b[^>]*>m\d{1,8}<\/acp>[ \t]*\r?\n?)*(?:Time sampled while preparing turn \d+, step \d+:|Current runtime context\.|<system-reminder>)/u;
3391
+ const INJECTED_USER_PREFIX = new RegExp(`^${INJECTED_ACP_MARKER_RUN}(?:${INJECTED_BUILT_IN_PREFIXES})`, "u");
3392
+ /**
3393
+ * Escape a user-supplied prefix so it can be embedded as a literal alternative.
3394
+ */
3395
+ function escapeRegExp(text) {
3396
+ return text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
3397
+ }
3398
+ /**
3399
+ * The prefix pattern for one request: the built-ins plus any configured
3400
+ * extras, each extra matched literally at the same position the built-ins
3401
+ * match (after optional leading whitespace and the `billion-context` marker).
3402
+ * Returns the shared built-in pattern when there are no usable extras.
3403
+ */
3404
+ function injectedPrefixPattern(extras) {
3405
+ const literals = (extras ?? []).filter((extra) => typeof extra === "string" && extra !== "");
3406
+ if (literals.length === 0) return INJECTED_USER_PREFIX;
3407
+ return new RegExp(`^${INJECTED_ACP_MARKER_RUN}(?:${INJECTED_BUILT_IN_PREFIXES}|${literals.map(escapeRegExp).join("|")})`, "u");
3408
+ }
3361
3409
  /**
3362
3410
  * The per-message marker `billion-context` puts in front of every message.
3363
3411
  *
@@ -3431,9 +3479,9 @@ function stripLeadingAcpTag(content) {
3431
3479
  };
3432
3480
  }
3433
3481
  /** Whether this message is one of the harness's injected context blocks. */
3434
- function isInjectedUserMessage(message) {
3482
+ function isInjectedUserMessage(message, pattern) {
3435
3483
  if (message["role"] !== "user") return false;
3436
- return INJECTED_USER_PREFIX.test(textOfContent(message["content"]).replace(/^\s+/u, ""));
3484
+ return pattern.test(textOfContent(message["content"]).replace(/^\s+/u, ""));
3437
3485
  }
3438
3486
  /**
3439
3487
  * Move the trailing injected `user` messages above the real prompt.
@@ -3441,22 +3489,22 @@ function isInjectedUserMessage(message) {
3441
3489
  * Returns `true` when the order changed, so the caller can serialize only once.
3442
3490
  * Never throws, and a body with nothing to move keeps its exact message order.
3443
3491
  */
3444
- function keepPromptLast(messages) {
3492
+ function keepPromptLast(messages, pattern) {
3445
3493
  let promptIndex = -1;
3446
3494
  for (let index = messages.length - 1; index >= 0; index--) {
3447
3495
  const value = messages[index];
3448
3496
  if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
3449
3497
  const message = value;
3450
3498
  if (message["role"] !== "user") continue;
3451
- if (isInjectedUserMessage(message)) continue;
3499
+ if (isInjectedUserMessage(message, pattern)) continue;
3452
3500
  if (textOfContent(message["content"]).trim() === "") continue;
3453
3501
  promptIndex = index;
3454
3502
  break;
3455
3503
  }
3456
3504
  if (promptIndex < 0) return false;
3457
- const trailing = messages.slice(promptIndex + 1).filter((value) => typeof value === "object" && value !== null && !Array.isArray(value) ? isInjectedUserMessage(value) : false);
3505
+ const trailing = messages.slice(promptIndex + 1).filter((value) => typeof value === "object" && value !== null && !Array.isArray(value) ? isInjectedUserMessage(value, pattern) : false);
3458
3506
  if (trailing.length === 0) return false;
3459
- const kept = messages.filter((value, index) => !(index > promptIndex && typeof value === "object" && value !== null && !Array.isArray(value) && isInjectedUserMessage(value)));
3507
+ const kept = messages.filter((value, index) => !(index > promptIndex && typeof value === "object" && value !== null && !Array.isArray(value) && isInjectedUserMessage(value, pattern)));
3460
3508
  kept.splice(promptIndex, 0, ...trailing);
3461
3509
  const prompt = kept[promptIndex + trailing.length];
3462
3510
  if (typeof prompt === "object" && prompt !== null && !Array.isArray(prompt)) {
@@ -3501,13 +3549,97 @@ function prepareChatBody(source, options) {
3501
3549
  if (message["role"] === "developer") message["role"] = "system";
3502
3550
  }
3503
3551
  ensureSystemHead(obj);
3504
- if (options?.keepPromptLast === true && Array.isArray(obj["messages"])) keepPromptLast(obj["messages"]);
3552
+ if (options?.keepPromptLast === true && Array.isArray(obj["messages"])) keepPromptLast(obj["messages"], injectedPrefixPattern(options.extraInjectedPrefixes));
3505
3553
  neutralizeChannelFingerprints(obj);
3506
3554
  normalizeToolChoice(obj);
3507
3555
  dropRefusedReasoning(obj);
3508
3556
  return JSON.stringify(obj);
3509
3557
  }
3510
3558
  /**
3559
+ * The host's session-title prompt template, verbatim up to the JSON payload.
3560
+ *
3561
+ * MEASURED in the installed DSH desktop 0.2.1-rc.2 `app.asar`, twice: the
3562
+ * request builder interpolates exactly this prefix before the JSON array of
3563
+ * `{seq, text}` records, and the host's own format validator asserts the
3564
+ * session-event carries the same text. Every title plugin the host bundles
3565
+ * (`dsh-session-title-llm`, `…-first-prompt-llm`, `…-all-prompts-llm`) shares
3566
+ * the template, so one prefix covers all variants. `purpose: 'session-title'`
3567
+ * exists on the host's `GenerateOptions` type but does not travel on the
3568
+ * OpenAI wire, so body shape is the only discriminator the shim has.
3569
+ */
3570
+ const SESSION_TITLE_PROMPT_PREFIX = "Generate the session title from this JSON array of human messages:\n";
3571
+ const SESSION_TITLE_PROMPT_HEAD = "Generate the session title from this JSON array of human messages:";
3572
+ /**
3573
+ * The ASCII-only head of the template, up to (not including) its newline.
3574
+ *
3575
+ * The full prefix contains a raw newline, which a serialized body escapes to
3576
+ * `\n`, so `prepared.includes(SESSION_TITLE_PROMPT_PREFIX)` would MISS every
3577
+ * real title. This head is all plain ASCII, so it appears verbatim in the
3578
+ * serialized body whenever the host sends the template as a single text part
3579
+ * — which its own format validator asserts (`content.length === 1`). It
3580
+ * therefore makes a prefilter without false negatives for the real shape; a
3581
+ * body split mid-template across array parts would slip past it and simply
3582
+ * keep the pre-rewrite behaviour. False positives (a conversation quoting the
3583
+ * template) are fine: the full detection below rejects them — the prefilter
3584
+ * only skips work, never changes a verdict.
3585
+ */
3586
+ /**
3587
+ * Rewrite a recognized session-title request onto the hidden lightweight model.
3588
+ *
3589
+ * WHY (issues #132/#133). The host asks its title plugins' prompt through the
3590
+ * same provider the conversation uses, so the title of every new session is
3591
+ * billed at the main paid model's rate. The reporter measured the upstream's
3592
+ * hidden `wb-lite` at 0 credits for exactly these requests (real upstream
3593
+ * calls, counted toward request volume, only fired once per new session), so
3594
+ * the rewrite moves the title to that model.
3595
+ *
3596
+ * DETECTION is fail-closed: the body is rewritten only when it has NO
3597
+ * assistant or tool messages and EXACTLY ONE `user` message, and that message
3598
+ * opens with the host's title template above. A real conversation can hardly
3599
+ * collide with the template (the host's own validator asserts it), and the
3600
+ * failure direction if the host ever rewords it is safe: the rewrite simply
3601
+ * stops, and the title is billed as before. `keepPromptLast` is unaffected —
3602
+ * the template prefix is not an injected-context marker, so a title body is
3603
+ * never reordered.
3604
+ *
3605
+ * WHAT is rewritten: the model id and the reasoning fields the lightweight
3606
+ * model does not accept. Everything else — `stream`, `max_tokens`, the
3607
+ * identity headers, every message byte — is untouched, so the other switches
3608
+ * keep behaving exactly as they do for conversation requests.
3609
+ *
3610
+ * Returns the original string unchanged when not enabled or not a title
3611
+ * request, so the caller does not need a separate shape check.
3612
+ */
3613
+ function rewriteSessionTitleModel(prepared, options) {
3614
+ if (options?.enabled !== true) return prepared;
3615
+ if (!prepared.includes(SESSION_TITLE_PROMPT_HEAD)) return prepared;
3616
+ let body;
3617
+ try {
3618
+ body = JSON.parse(prepared);
3619
+ } catch {
3620
+ return prepared;
3621
+ }
3622
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return prepared;
3623
+ const obj = body;
3624
+ const messages = obj["messages"];
3625
+ if (!Array.isArray(messages)) return prepared;
3626
+ let titleText;
3627
+ for (const value of messages) {
3628
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return prepared;
3629
+ const message = value;
3630
+ if (message["role"] === "assistant" || message["role"] === "tool") return prepared;
3631
+ if (message["role"] !== "user") continue;
3632
+ if (titleText !== void 0) return prepared;
3633
+ titleText = textOfContent(message["content"]);
3634
+ }
3635
+ if (titleText === void 0) return prepared;
3636
+ if (!titleText.replace(/^\s+/u, "").startsWith(SESSION_TITLE_PROMPT_PREFIX)) return prepared;
3637
+ obj["model"] = typeof options.model === "string" && options.model.trim() !== "" ? options.model.trim() : "wb-lite";
3638
+ delete obj["reasoning_effort"];
3639
+ delete obj["reasoning"];
3640
+ return JSON.stringify(obj);
3641
+ }
3642
+ /**
3511
3643
  * Remove `reasoning_effort: "off"` when it would only ever fail (issue #83).
3512
3644
  *
3513
3645
  * The refusal table is keyed by exact model id, but the model a request
@@ -3957,7 +4089,7 @@ var WorkBuddyUpstreamClient = class {
3957
4089
  onFallback;
3958
4090
  identity;
3959
4091
  /**
3960
- * Reports a fallback the catalog reader had to take.
4092
+ * Reports an anomaly the reader or classifier had to tolerate.
3961
4093
  *
3962
4094
  * Injected rather than logged here because this module holds no logger on
3963
4095
  * purpose — it is pure transport + parsing, so it stays importable from
@@ -3995,10 +4127,12 @@ var WorkBuddyUpstreamClient = class {
3995
4127
  response
3996
4128
  };
3997
4129
  const text = (await response.text()).slice(0, ERROR_BODY_LIMIT);
4130
+ const kind = classifyUpstreamError(response.status, text);
4131
+ if (isUndecodablePolicyBody(text)) this.onFallback?.(`11140 失败体没有任何可解码的 displayMsg/displayTips/msg 字段,若上游调整了限制措辞或字段名,账号停权判定会全部漏判。失败体片段:${text.slice(0, 200)}`);
3998
4132
  return {
3999
4133
  ok: false,
4000
4134
  status: response.status,
4001
- kind: classifyUpstreamError(response.status, text),
4135
+ kind,
4002
4136
  message: text,
4003
4137
  detail: parseUpstreamErrorDetail(text)
4004
4138
  };
@@ -6806,7 +6940,7 @@ function describeRosterSeenPayload(raw) {
6806
6940
  //#endregion
6807
6941
  //#region src/version.ts
6808
6942
  /** The npm package version this build was produced from. */
6809
- const WORKBUDDY_CONNECT_VERSION = "5.8.1";
6943
+ const WORKBUDDY_CONNECT_VERSION = "5.8.3";
6810
6944
  //#endregion
6811
6945
  //#region src/host-heartbeat.ts
6812
6946
  /**
@@ -6964,4 +7098,4 @@ async function clearHostHeartbeat() {
6964
7098
  await rm(workbuddyHostHeartbeatPath(), { force: true });
6965
7099
  }
6966
7100
  //#endregion
6967
- export { parseCreditMultiplier as $, isEncryptedCredentialError as A, isWorkbuddyBundle as At, WorkBuddyCredentialRejectedError as B, WorkBuddyCredentialStore as C, clearAtRestKeyCache as Ct, defaultDesktopAuthDirs as D, findWorkbuddyAppExecutable as Dt, defaultDesktopAuthCandidates as E, fetchAtRestKeyPayload as Et, workbuddyAccountId as F, workbuddyAppExecutableCandidates as Ft, forceNeutralHead as G, channelGateMemberOf as H, workbuddyOwnAuthPath as I, hostPeersInfo as It, isCredentialRejectedError as J, isContextTooLongBody as K, CONTEXT_OVERFLOW_MARKER as L, resolveDshHome as Lt, legacyWorkbuddyOwnAuthPath as M, macosNestedAppBundles as Mt, parseWorkBuddyAuth as N, openEncryptedField as Nt, defaultDesktopAuthPath as O, findWorkbuddyAppExecutableWithSource as Ot, preferredAccountName as P, readAtRestKey as Pt, openingEvidenceOfBody as Q, CREDENTIAL_REJECTED_CODE as R, withFileLock as Rt, WORKBUDDY_AUTH_FILE_ENV as S, WORKBUDDY_APP_EXECUTABLE_ENV as St, authFileName as T, deriveAtRestKeyId as Tt, classifyUpstreamError as U, WorkBuddyUpstreamClient as V, declaredTools as W, knownRegionOf as X, isFirstMessageGateBody as Y, modelIdOfBody as Z, deriveCatalog as _, parseQueueAnswer as _t, readHostHeartbeat as a, requestEvidenceOf as at, ENCRYPTED_CREDENTIAL_CODE as b, imageDefaultFor as bt, WORKBUDDY_CONNECT_VERSION as c, defaultOffModelIds as ct, rosterSeenPath as d, offRefused as dt, parseReasoning as et, piAiRuntimeInfo as f, recordRefusal as ft, applyContextBudgets as g, pickImageModel as gt, WorkBuddyCatalog as h, WorkBuddyImageError as ht, processStartTimeMs as i, regionOf as it, isVaultAccountId as j, macosBundleExecutable as jt, hasEncryptedCredentialFields as k, isEncryptedFieldWrapper as kt, createFileRosterSeenStore as l, lastRefusalFor as lt, FALLBACK_WORKBUDDY_MODELS_GLOBAL as m, DEFAULT_IMAGE_SIZE as mt, clearHostHeartbeat as n, perturbChannelFingerprints as nt, workbuddyHostHeartbeatPath as o, attachBusyLevels as ot, FALLBACK_WORKBUDDY_MODELS as p, withEffectiveOff as pt, isConversationIdGateBody as q, isHeartbeatProcessAlive as r, prepareChatBody as rt, writeHostHeartbeat as s, REFUSES_OFF_MODEL_IDS as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, parseUpstreamModel as tt, describeRosterSeenPayload as u, offDefaultFor as ut, fallbackModelsFor as v, queueNoticeText as vt, WorkBuddyEncryptedCredentialError as w, deriveAtRestKey as wt, WORKBUDDY_AUTH_FILENAME as x, nativeModalityOf as xt, withoutRouters as y, NATIVE_MODALITY_BY_MODEL_ID as yt, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as z, writeFileAtomic as zt };
7101
+ export { parseCreditMultiplier as $, isEncryptedCredentialError as A, isEncryptedFieldWrapper as At, WorkBuddyCredentialRejectedError as B, writeFileAtomic as Bt, WorkBuddyCredentialStore as C, WORKBUDDY_APP_EXECUTABLE_ENV as Ct, defaultDesktopAuthDirs as D, fetchAtRestKeyPayload as Dt, defaultDesktopAuthCandidates as E, deriveAtRestKeyId as Et, workbuddyAccountId as F, readAtRestKey as Ft, forceNeutralHead as G, channelGateMemberOf as H, workbuddyOwnAuthPath as I, workbuddyAppExecutableCandidates as It, isCredentialRejectedError as J, isContextTooLongBody as K, CONTEXT_OVERFLOW_MARKER as L, hostPeersInfo as Lt, legacyWorkbuddyOwnAuthPath as M, macosBundleExecutable as Mt, parseWorkBuddyAuth as N, macosNestedAppBundles as Nt, defaultDesktopAuthPath as O, findWorkbuddyAppExecutable as Ot, preferredAccountName as P, openEncryptedField as Pt, openingEvidenceOfBody as Q, CREDENTIAL_REJECTED_CODE as R, resolveDshHome as Rt, WORKBUDDY_AUTH_FILE_ENV as S, nativeModalityOf as St, authFileName as T, deriveAtRestKey as Tt, classifyUpstreamError as U, WorkBuddyUpstreamClient as V, declaredTools as W, knownRegionOf as X, isFirstMessageGateBody as Y, modelIdOfBody as Z, deriveCatalog as _, pickImageModel as _t, readHostHeartbeat as a, requestEvidenceOf as at, ENCRYPTED_CREDENTIAL_CODE as b, NATIVE_MODALITY_BY_MODEL_ID as bt, WORKBUDDY_CONNECT_VERSION as c, REFUSES_OFF_MODEL_IDS as ct, rosterSeenPath as d, offDefaultFor as dt, parseReasoning as et, piAiRuntimeInfo as f, offRefused as ft, applyContextBudgets as g, WorkBuddyImageError as gt, WorkBuddyCatalog as h, DEFAULT_IMAGE_SIZE as ht, processStartTimeMs as i, regionOf as it, isVaultAccountId as j, isWorkbuddyBundle as jt, hasEncryptedCredentialFields as k, findWorkbuddyAppExecutableWithSource as kt, createFileRosterSeenStore as l, defaultOffModelIds as lt, FALLBACK_WORKBUDDY_MODELS_GLOBAL as m, withEffectiveOff as mt, clearHostHeartbeat as n, perturbChannelFingerprints as nt, workbuddyHostHeartbeatPath as o, rewriteSessionTitleModel as ot, FALLBACK_WORKBUDDY_MODELS as p, recordRefusal as pt, isConversationIdGateBody as q, isHeartbeatProcessAlive as r, prepareChatBody as rt, writeHostHeartbeat as s, attachBusyLevels as st, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, parseUpstreamModel as tt, describeRosterSeenPayload as u, lastRefusalFor as ut, fallbackModelsFor as v, parseQueueAnswer as vt, WorkBuddyEncryptedCredentialError as w, clearAtRestKeyCache as wt, WORKBUDDY_AUTH_FILENAME as x, imageDefaultFor as xt, withoutRouters as y, queueNoticeText as yt, WORKBUDDY_FALLBACK_SYSTEM_PROMPT as z, withFileLock as zt };
package/lib/index.d.ts CHANGED
@@ -1403,6 +1403,20 @@ interface PrepareChatBodyOptions {
1403
1403
  * conversation whose trailing user messages are real turns.
1404
1404
  */
1405
1405
  keepPromptLast?: boolean;
1406
+ /**
1407
+ * Additional leading markers that mark a trailing `user` message as injected
1408
+ * context, one literal prefix per entry.
1409
+ *
1410
+ * WHY. The built-in pattern at {@link INJECTED_USER_PREFIX} knows only the
1411
+ * prefixes measured at the time it was written; a companion plugin (or a DSH
1412
+ * update) that starts injecting new text would silently fall back to having
1413
+ * its blocks recorded as the prompt. Each entry is matched LITERALLY at the
1414
+ * start of the message text (after leading whitespace and the
1415
+ * `billion-context` marker, which the built-in pattern tolerates) and OR'ed
1416
+ * with the built-ins; entries are escaped, so no configuration value can turn
1417
+ * the pattern invalid.
1418
+ */
1419
+ extraInjectedPrefixes?: readonly string[];
1406
1420
  }
1407
1421
  /**
1408
1422
  * Normalize an OpenAI chat-completions body for the WorkBuddy upstream: force
@@ -1468,7 +1482,7 @@ declare class WorkBuddyUpstreamClient {
1468
1482
  private readonly onFallback?;
1469
1483
  private readonly identity;
1470
1484
  /**
1471
- * Reports a fallback the catalog reader had to take.
1485
+ * Reports an anomaly the reader or classifier had to tolerate.
1472
1486
  *
1473
1487
  * Injected rather than logged here because this module holds no logger on
1474
1488
  * purpose — it is pure transport + parsing, so it stays importable from
@@ -1973,6 +1987,24 @@ interface WorkBuddyShimOptions {
1973
1987
  * Default false: it changes bytes the upstream counts, so it is opt-in.
1974
1988
  */
1975
1989
  keepPromptLast?: boolean;
1990
+ /**
1991
+ * Extra literal prefixes (issue #133) treated like the harness's injected
1992
+ * markers under `keepPromptLast`, for context blocks a companion plugin
1993
+ * injects that the built-in list does not know about. Only read when
1994
+ * `keepPromptLast` is on.
1995
+ */
1996
+ extraInjectedPrefixes?: readonly string[];
1997
+ /**
1998
+ * Move the host's session-title request onto the hidden lightweight model
1999
+ * (issues #132/#133). Disabled by default; when enabled, a body recognized
2000
+ * as the one-message title prompt is re-pointed at `model` (default
2001
+ * `wb-lite`, measured 0 credits) instead of billing the main paid model.
2002
+ * See `rewriteSessionTitleModel` for the fail-closed detection rules.
2003
+ */
2004
+ sessionTitle?: {
2005
+ enabled?: boolean;
2006
+ model?: string;
2007
+ };
1976
2008
  /**
1977
2009
  * Pool failover for chat requests. Absent means no failover at all — the
1978
2010
  * pre-pool behaviour, where the selected account serves and its failure is
@@ -4825,6 +4857,34 @@ interface Config {
4825
4857
  * an explicit opt-in rather than a silent change on upgrade.
4826
4858
  */
4827
4859
  keepPromptLast?: boolean;
4860
+ /**
4861
+ * Extra literal prefixes (issue #133) treated like the harness's injected
4862
+ * context markers when `keepPromptLast` is on, for injected blocks a
4863
+ * companion plugin produces that the built-in list does not recognize. Each
4864
+ * entry is matched literally at the start of a trailing `user` message.
4865
+ */
4866
+ extraInjectedPrefixes?: string[];
4867
+ /**
4868
+ * Move the host's session-title request onto the hidden lightweight model
4869
+ * (issues #132/#133). Off by default: it is a real upstream request either
4870
+ * way, and the rewrite changes which bucket it is billed against.
4871
+ */
4872
+ sessionTitle?: {
4873
+ /**
4874
+ * Whether title requests are re-pointed at {@link sessionTitle.model}.
4875
+ * The host still fires the request exactly once per new session; the
4876
+ * reporter measured the lightweight model at 0 credits for these calls
4877
+ * (request volume still counts, and the evidence is reporter-level, not
4878
+ * reproduced by this plugin).
4879
+ */
4880
+ enabled?: boolean;
4881
+ /**
4882
+ * Target model for the rewritten title request. Defaults to `wb-lite`,
4883
+ * the hidden model the reporter measured; configurable in case the id
4884
+ * changes or a region exposes a different one.
4885
+ */
4886
+ model?: string;
4887
+ };
4828
4888
  }
4829
4889
  /**
4830
4890
  * Mark a schema's field as volatile on the DSH lines that support it.