dsh-lcx-codex 0.4.3 → 0.4.4-pre.1

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.
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+ const ABORT_MESSAGE = "The operation was aborted";
3
+ function abortInstance() {
4
+ if (typeof DOMException === "function")
5
+ return new DOMException(ABORT_MESSAGE, "AbortError");
6
+ const error = new Error(ABORT_MESSAGE);
7
+ error.name = "AbortError";
8
+ return error;
9
+ }
10
+ // `DOMException.prototype.code` is a getter-only accessor, so own properties are
11
+ // defined rather than assigned. Both stay non-enumerable, like a native `cause`.
12
+ function define(target, key, value) {
13
+ try {
14
+ Object.defineProperty(target, key, {
15
+ value,
16
+ configurable: true,
17
+ writable: true,
18
+ enumerable: false,
19
+ });
20
+ }
21
+ catch { }
22
+ }
23
+ function synthesize(reason) {
24
+ const error = abortInstance();
25
+ define(error, "code", "LCX_ABORTED");
26
+ if (reason !== undefined && reason !== null)
27
+ define(error, "cause", reason);
28
+ return error;
29
+ }
30
+ /**
31
+ * Normalize an abort reason into something safe to throw. `fallback` is used only when
32
+ * the reason is absent, preserving the older `signal.reason ?? error` behaviour.
33
+ */
34
+ export function normalizeAbortReason(reason, fallback) {
35
+ if (reason instanceof Error)
36
+ return reason;
37
+ if (reason === undefined || reason === null) {
38
+ if (fallback instanceof Error)
39
+ return fallback;
40
+ return synthesize(undefined);
41
+ }
42
+ return synthesize(reason);
43
+ }
44
+ /** The error to throw for `signal`'s cancellation. */
45
+ export function abortError(signal, fallback) {
46
+ return normalizeAbortReason(signal?.reason, fallback);
47
+ }
package/lib/client.js CHANGED
@@ -727,7 +727,6 @@
727
727
  // src/search-accounting.ts
728
728
  var object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
729
729
  var count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
730
- var zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
731
730
  var isSearchTool = (name) => name === "web_search" || name === "websearch_gpt_advanced";
732
731
  function auxiliaryUsageOf(event, toolName) {
733
732
  if (!object(event) || event.type !== "tool/result" || !object(event.data?.meta)) return [];
@@ -744,38 +743,6 @@
744
743
  }
745
744
  return result;
746
745
  }
747
- function addUsage(base, records) {
748
- if (!records.length) return base;
749
- const next = { ...base };
750
- for (const { usage: u } of records) {
751
- next.uncachedInputTokens += u.inputTokens;
752
- next.outputTokens += u.outputTokens;
753
- next.cacheReadTokens += u.cacheReadTokens;
754
- next.cacheWriteTokens += u.cacheWriteTokens;
755
- }
756
- if (!Object.values(next).every(count)) throw new Error("LCX search usage exceeds safe counters");
757
- return next;
758
- }
759
- function mergeBuckets(base, extra) {
760
- if (!object(base) || Object.values(extra).every((n) => n === 0)) return base;
761
- const next = { ...base };
762
- for (const key of Object.keys(extra)) {
763
- if (count(base[key])) next[key] = base[key] + extra[key];
764
- }
765
- return next;
766
- }
767
- function addTurnUsage(base, records) {
768
- if (!object(base) || !records.length || !count(base.totalTokens)) return base;
769
- const next = mergeBuckets(base, addUsage(zeroBuckets(), records));
770
- next.totalTokens = base.totalTokens + records.reduce((n, r) => n + r.usage.totalTokens, 0);
771
- delete next.reasoningTokens;
772
- if (Array.isArray(base.routes)) {
773
- const routes = /* @__PURE__ */ new Map();
774
- for (const r of [...base.routes, ...records]) routes.set(`${r.provider}\0${r.model}`, { provider: r.provider, model: r.model });
775
- next.routes = [...routes.values()];
776
- }
777
- return next;
778
- }
779
746
 
780
747
  // src/client/search-usage-ui.ts
781
748
  var searchUsageDefinition = {
@@ -803,64 +770,70 @@
803
770
  return scope === "turn" && s?.complete ? { kind: "turn", turn: s.turn, key: "lcx-search-usage", value: s.records } : null;
804
771
  }
805
772
  };
773
+ function tokenCount(value) {
774
+ return String(value);
775
+ }
776
+ function recordsTotal(records) {
777
+ let total = 0;
778
+ for (const record2 of records) {
779
+ const value = record2.usage.totalTokens;
780
+ if (!Number.isSafeInteger(value) || value < 0) continue;
781
+ total += value;
782
+ if (!Number.isSafeInteger(total)) return 0;
783
+ }
784
+ return total;
785
+ }
786
+ function bucketsTotal(value) {
787
+ if (!object(value)) return 0;
788
+ let total = 0;
789
+ for (const key of ["uncachedInputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
790
+ const count2 = value[key];
791
+ if (!Number.isSafeInteger(count2) || count2 < 0) return 0;
792
+ total += count2;
793
+ if (!Number.isSafeInteger(total)) return 0;
794
+ }
795
+ return total;
796
+ }
797
+ function usageView(createElement, scope, total, t) {
798
+ if (total <= 0) return null;
799
+ const label = t?.(scope === "turn" ? "usageTurn" : "usageSession") ?? "Search usage";
800
+ return createElement("span", {
801
+ className: `lcx-search-usage lcx-search-usage-${scope}`,
802
+ "data-lcx-search-usage": scope,
803
+ title: `${label}: ${tokenCount(total)}`
804
+ }, `${label}: ${tokenCount(total)}`);
805
+ }
806
+ function TurnTailUsage({ turn, t }, createElement) {
807
+ if (!createElement) return null;
808
+ return usageView(createElement, "turn", recordsTotal(turn.data.get("lcx-search-usage") ?? []), t);
809
+ }
810
+ function SessionUsage({ useProjection, t }, createElement) {
811
+ if (!createElement) return null;
812
+ const extra = useProjection("lcxSearchUsage");
813
+ const auxiliary = object(extra) ? extra.auxiliary : void 0;
814
+ return usageView(createElement, "session", bucketsTotal(auxiliary), t);
815
+ }
806
816
  function installUsageSlots(slots, createElement) {
817
+ const registrations = [
818
+ [
819
+ { name: "conversation.chat.turnTail", id: "lcx-search-usage-turn", order: 0, locale: "lcx-codex" },
820
+ (props) => TurnTailUsage(props, createElement)
821
+ ],
822
+ [
823
+ { name: "conversation.composer.dock", id: "lcx-search-usage-session", order: 10, locale: "lcx-codex" },
824
+ (props) => SessionUsage(props, createElement)
825
+ ]
826
+ ];
807
827
  const cleanups = [];
808
- for (const [name, key] of [
809
- ["conversation.composer.dock", "stats"],
810
- ["conversation.composer.bar", ""],
811
- ["conversation.chat.node", "turn-tail"]
812
- ]) {
813
- let effect;
828
+ for (const [name, component] of registrations) {
814
829
  try {
815
- effect = slots.inject(name, () => {
816
- let entries;
817
- try {
818
- entries = slots.entriesOfSlot(name);
819
- } catch {
820
- return;
821
- }
822
- if (!Array.isArray(entries) || entries.length === 0) return;
823
- const candidates = key === "" ? [entries[0]] : entries.filter((e) => e?.options?.id === key || e?.options?.key === key);
824
- if (candidates.length !== 1) return;
825
- const base = candidates[0], original = base?.component;
826
- if (!base || typeof original !== "function" || !base.options || typeof base.options !== "object") return;
827
- const descriptor = Object.getOwnPropertyDescriptor(base, "component");
828
- if (descriptor && descriptor.set === void 0 && descriptor.writable === false) return;
829
- function WithSearchUsage(props) {
830
- if (key === "turn-tail") {
831
- const location = props?.node?.location;
832
- const records = (location?.kind === "turn" || location?.kind === "step") && location.turn?.data?.get ? location.turn.data.get("lcx-search-usage") : void 0;
833
- if (!records?.length) return createElement(original, props);
834
- return createElement(original, { ...props, node: { ...props.node, data: { ...props.node.data, tokenUsage: addTurnUsage(props.node.data.tokenUsage, records) } } });
835
- }
836
- if (typeof props?.useProjection !== "function") return createElement(original, props);
837
- const extra = props.useProjection("lcxSearchUsage");
838
- const breakdown = props.useProjection("contextBreakdown");
839
- const useProjection = (projection) => {
840
- const value = props.useProjection(projection);
841
- if (projection === "tokenUsage" && extra?.auxiliary) return mergeBuckets(value, extra.auxiliary);
842
- if (projection === "contextPressure" && extra?.aggregateContext && object(breakdown)) {
843
- const tokens = breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
844
- if (Number.isSafeInteger(tokens) && tokens >= 0 && object(value)) return { ...value, pressureTokens: tokens, projectedTokens: tokens };
845
- }
846
- return value;
847
- };
848
- return createElement(original, { ...props, useProjection });
849
- }
850
- try {
851
- base.component = WithSearchUsage;
852
- } catch {
853
- return;
854
- }
855
- if (base.component !== WithSearchUsage) return;
856
- return () => {
857
- if (base.component === WithSearchUsage) base.component = original;
858
- };
830
+ const effect = slots.inject(name.name, () => {
831
+ const dispose = slots.register(name, component);
832
+ return typeof dispose === "function" ? dispose : void 0;
859
833
  });
834
+ if (typeof effect === "function") cleanups.push(effect);
860
835
  } catch {
861
- continue;
862
836
  }
863
- if (typeof effect === "function") cleanups.push(effect);
864
837
  }
865
838
  return () => {
866
839
  for (const dispose of cleanups.reverse()) dispose();
@@ -894,7 +867,7 @@
894
867
  const DEFAULTS = Object.fromEntries(
895
868
  FIELDS.map((field) => [field, false])
896
869
  );
897
- const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}` + inlineMediaCss;
870
+ const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}.lcx-search-usage{display:inline-flex;align-items:center;gap:4px;margin:2px 0;color:var(--dsw-alias-label-tertiary);font:12px/18px system-ui}.lcx-search-usage-session{padding:0 4px}.lcx-search-usage-turn{padding:0 6px}` + inlineMediaCss;
898
871
  function mountCss() {
899
872
  if (typeof document === "undefined") return () => {
900
873
  };
@@ -910,6 +883,7 @@
910
883
  const copy = {
911
884
  zh: {
912
885
  title: "Responses / Codex \u80FD\u529B",
886
+ summary: "GPT Responses route \u4E0E\u641C\u7D22\u80FD\u529B",
913
887
  desc: "LCX \u8DDF\u968F DSH \u5F53\u524D\u4F1A\u8BDD\u9009\u62E9\u7684 GPT Responses route\uFF1B\u6A21\u578B\u3001endpoint \u548C credential \u7EE7\u7EED\u53EA\u7531 DSH \u7BA1\u7406\u3002",
914
888
  enabled: "\u542F\u7528 LCX\uFF08\u63A5\u7BA1\u5F53\u524D GPT Responses \u4F1A\u8BDD\uFF09",
915
889
  enabledHelp: "\u6B64\u5F00\u5173\u4EC5\u63A5\u7BA1 GPT Responses \u4F1A\u8BDD\uFF1BGrok \u539F\u751F\u641C\u7D22\u7531\u4E0B\u65B9\u72EC\u7ACB\u5F00\u5173\u63A7\u5236\uFF0C\u5176\u4ED6\u975E GPT \u6A21\u578B\u7EE7\u7EED\u4F7F\u7528 DSH \u539F\u751F\u8DEF\u5F84\u3002",
@@ -939,6 +913,8 @@
939
913
  mediaVideo: "\u89C6\u9891",
940
914
  mediaFilter: "\u5A92\u4F53\u7C7B\u578B",
941
915
  mediaOpen: "\u6253\u5F00\u539F\u59CB\u5A92\u4F53",
916
+ usageTurn: "\u672C\u8F6E\u641C\u7D22\u7528\u91CF",
917
+ usageSession: "\u641C\u7D22\u7528\u91CF",
942
918
  grokTitle: "Grok \u539F\u751F\u641C\u7D22",
943
919
  grokDesc: "\u4F7F\u7528 DSH \u5F53\u524D\u9009\u62E9\u7684 Grok \u6A21\u578B\u53CA\u5176\u670D\u52A1\u914D\u7F6E\uFF1B\u4E0E GPT \u529F\u80FD\u5F00\u5173\u72EC\u7ACB\u3002\u5F00\u542F\u4EFB\u4E00\u641C\u7D22\u540E\uFF0CGrok \u4EC5\u4F7F\u7528\u539F\u751F\u641C\u7D22\uFF0C\u7F51\u9875\u8BFB\u53D6\u548C\u5176\u4ED6\u5DE5\u5177\u4ECD\u53EF\u7528\u3002",
944
920
  grokWeb: "\u542F\u7528\u539F\u751F Web Search",
@@ -952,6 +928,7 @@
952
928
  },
953
929
  en: {
954
930
  title: "Responses / Codex capabilities",
931
+ summary: "GPT Responses route and search capabilities",
955
932
  desc: "LCX follows the GPT Responses route selected by the current DSH session. Model, endpoint and credentials remain DSH-owned.",
956
933
  enabled: "Enable LCX (own current GPT Responses conversation)",
957
934
  enabledHelp: "Applies only to GPT Responses conversations. Grok native search is controlled independently below; other non-GPT models continue through DSH normally.",
@@ -981,6 +958,8 @@
981
958
  mediaVideo: "Video",
982
959
  mediaFilter: "Media type",
983
960
  mediaOpen: "Open original media",
961
+ usageTurn: "Turn search usage",
962
+ usageSession: "Search usage",
984
963
  grokTitle: "Grok native search",
985
964
  grokDesc: "Uses the Grok model and provider profile currently selected in DSH, independently of the GPT switch. When either search is enabled, Grok uses native search while page reading and other tools remain available.",
986
965
  grokWeb: "Enable native Web Search",
@@ -1102,12 +1081,14 @@
1102
1081
  saveError;
1103
1082
  store;
1104
1083
  stop;
1084
+ saveEpoch;
1105
1085
  constructor(scope) {
1106
1086
  this.scope = scope;
1107
1087
  this.draft = null;
1108
1088
  this.dirty = false;
1109
1089
  this.saving = false;
1110
1090
  this.saveError = false;
1091
+ this.saveEpoch = 0;
1111
1092
  this.store = createSnapshotStore(this.projection());
1112
1093
  mediaStore.set({ enabled: this.value().searchMediaPreview });
1113
1094
  this.stop = scope.subscribe(() => {
@@ -1159,10 +1140,19 @@
1159
1140
  this.saveError = false;
1160
1141
  this.publish();
1161
1142
  }
1143
+ leavePage() {
1144
+ this.saveEpoch += 1;
1145
+ this.draft = null;
1146
+ this.dirty = false;
1147
+ this.saving = false;
1148
+ this.saveError = false;
1149
+ this.publish();
1150
+ }
1162
1151
  async save() {
1163
1152
  if (!this.dirty || this.saving || !this.snapshot().writable || this.snapshot().status !== "ready") return;
1164
1153
  const next = this.draftValue(), prev = this.value();
1165
1154
  const fields = FIELDS.filter((field) => next[field] !== prev[field]);
1155
+ const saveEpoch = ++this.saveEpoch;
1166
1156
  this.saving = true;
1167
1157
  this.saveError = false;
1168
1158
  this.publish();
@@ -1173,16 +1163,20 @@
1173
1163
  path: [field],
1174
1164
  value: next[field]
1175
1165
  })));
1166
+ if (saveEpoch !== this.saveEpoch) return;
1176
1167
  if (this.snapshot().status !== "ready" || fields.some((field) => this.value()[field] !== next[field]))
1177
1168
  throw new Error("Settings mutation was not confirmed");
1178
1169
  }
1170
+ if (saveEpoch !== this.saveEpoch) return;
1179
1171
  this.draft = null;
1180
1172
  this.dirty = false;
1181
1173
  } catch {
1182
- this.saveError = true;
1174
+ if (saveEpoch === this.saveEpoch) this.saveError = true;
1183
1175
  } finally {
1184
- this.saving = false;
1185
- this.publish();
1176
+ if (saveEpoch === this.saveEpoch) {
1177
+ this.saving = false;
1178
+ this.publish();
1179
+ }
1186
1180
  }
1187
1181
  }
1188
1182
  inject() {
@@ -1194,7 +1188,8 @@
1194
1188
  setMediaPreview: (value) => this.edit("searchMediaPreview", value),
1195
1189
  edit: (field, value) => this.edit(field, value),
1196
1190
  save: () => void this.save(),
1197
- discard: () => this.discard()
1191
+ discard: () => this.discard(),
1192
+ leavePage: () => this.leavePage()
1198
1193
  };
1199
1194
  }
1200
1195
  }
@@ -1226,7 +1221,13 @@
1226
1221
  }
1227
1222
  function Card(props) {
1228
1223
  const t = props.t, s = props.useLcxCard((state) => state), [open, setOpen] = React.useState(false);
1224
+ React.useEffect(
1225
+ () => props.view === "page" ? () => props.leavePage() : void 0,
1226
+ [props.view, props.leavePage]
1227
+ );
1229
1228
  if (!s.available) return null;
1229
+ if (props.view === "summary")
1230
+ return React.createElement("span", { className: "lcx-summary" }, t("summary"));
1230
1231
  const disabled = !s.writable || s.saving;
1231
1232
  return React.createElement(
1232
1233
  "li",
@@ -1345,10 +1346,8 @@
1345
1346
  function apply(ctx) {
1346
1347
  const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.settingsScope ?? ctx.get("settingsScope"), locale = ctx.locale ?? ctx.get("locale"), uiConversation = ctx.uiConversation ?? ctx.get("uiConversation");
1347
1348
  if (!slots || !svc || !locale || !uiConversation) return;
1348
- if (typeof slots.entriesOfSlot === "function") {
1349
- ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
1350
- ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
1351
- }
1349
+ ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
1350
+ ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
1352
1351
  ctx.effect(mountCss, "lcx-codex styles");
1353
1352
  for (const language of ["zh", "en"])
1354
1353
  ctx.effect(() => locale.register(NAMESPACE, language, copy[language]), `lcx-codex ${language} dictionary`);
@@ -1364,12 +1363,12 @@
1364
1363
  }, label);
1365
1364
  const controller = new Controller(svc.bind({ namespace: NAMESPACE }));
1366
1365
  installSlot(
1367
- "settings.plugin.item",
1368
- "lcx-codex settings slot",
1366
+ "plugins.bundle.config",
1367
+ "lcx-codex plugin configuration",
1369
1368
  () => slots.register(
1370
1369
  {
1371
- name: "settings.plugin.item",
1372
- key: NAMESPACE,
1370
+ name: "plugins.bundle.config",
1371
+ key: "dsh-lcx-codex",
1373
1372
  locale: NAMESPACE,
1374
1373
  inject: () => controller.inject()
1375
1374
  },
@@ -1,5 +1,6 @@
1
- import { offloadedImageText, offloadRequestImagesWithPolicy, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
2
- import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, } from "./pi-responses-runtime.js";
1
+ import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImageText, projectOffloadedImages, requiredImageOffload, requestImageHandleText, resolveImageAttachmentAccess, } from "@deepseek-ai/dsh-llm";
2
+ import { requestImageDimensions } from "@deepseek-ai/dsh-attachment";
3
+ import { convertResponsesMessages, convertResponsesTools, createGrammarToolInputProperties, getBuiltinModels, normalizeContext, } from "./pi-responses-runtime.js";
3
4
  const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
4
5
  const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
5
6
  const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
@@ -87,42 +88,45 @@ export async function resolveModelImageSupport(ctx, route, signal) {
87
88
  return "unknown";
88
89
  }
89
90
  }
90
- function attachmentResolver(ctx, options) {
91
+ function attachmentResolver(ctx, options, requestImages) {
91
92
  const attachments = attachmentServices(ctx?.attachments);
92
93
  return async (block, signal) => {
93
94
  if (attachments === undefined)
94
95
  throw error("LCX requires the DSH request-image attachment API", "LCX_COMPACT_IMAGE_API_UNAVAILABLE");
95
- const request = await attachments.store.readImageRequest(block.attachment, { maxPixels: options.requestImagePixelBudget, maxBytes: options.requestImageMaxBytes }, signal);
96
+ const request = requestImages === undefined
97
+ ? await attachments.store.readImageRequest(block.attachment, { ...requestImageDimensions(block.attachment.width, block.attachment.height, options.requestImagePixelBudget), maxBytes: options.requestImageMaxBytes }, signal)
98
+ : requestImages.get(String(block.attachment.attachmentId));
99
+ if (request === undefined)
100
+ throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
96
101
  const data = request.data;
97
102
  const mediaType = request.mediaType ?? block.attachment.mediaType;
98
103
  if (!(data instanceof Uint8Array) && !Buffer.isBuffer(data))
99
104
  throw error("DSH attachment returned no request-image bytes", "LCX_COMPACT_IMAGE_UNAVAILABLE");
100
105
  if (typeof mediaType !== "string" || !mediaType.startsWith("image/"))
101
106
  throw error("DSH attachment returned an invalid request-image media type", "LCX_COMPACT_IMAGE_UNAVAILABLE");
102
- const ref = request.attachment ?? block.attachment;
103
107
  return {
104
108
  data: Buffer.from(data),
105
109
  mediaType,
106
- ref,
110
+ ref: block.attachment,
107
111
  request,
108
- access: imageAccess(ctx, attachments, ref),
112
+ access: imageAccess(ctx, attachments, block.attachment),
109
113
  };
110
114
  };
111
115
  }
112
- async function imagePart(block, ctx, options, imageMap) {
116
+ async function imagePart(block, ctx, options, imageMap, requestImages) {
113
117
  if (options.imageSupport === "unsupported")
114
118
  return { type: "input_text", text: "[image omitted because the target model does not support image input]" };
115
- const image = await attachmentResolver(ctx, options)(block, options.signal);
119
+ const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
116
120
  if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
117
121
  throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
118
122
  const imageUrl = `data:${image.mediaType};base64,${image.data.toString("base64")}`;
119
123
  imageMap.set(imageUrl, structuredClone(image.ref));
120
124
  return { type: "input_image", detail: "auto", image_url: imageUrl };
121
125
  }
122
- async function piImageParts(block, ctx, options, imageMap) {
126
+ async function piImageParts(block, ctx, options, imageMap, requestImages) {
123
127
  if (options.imageSupport === "unsupported")
124
128
  return [{ type: "text", text: "[image omitted because the target model does not support image input]" }];
125
- const image = await attachmentResolver(ctx, options)(block, options.signal);
129
+ const image = await attachmentResolver(ctx, options, requestImages)(block, options.signal);
126
130
  if (Math.ceil(image.data.byteLength / 3) * 4 > options.maxRequestImageBytes)
127
131
  throw error("one image exceeds the configured LCX request image bound", "LCX_COMPACT_IMAGE_TOO_LARGE");
128
132
  const data = image.data.toString("base64");
@@ -292,17 +296,17 @@ function toPiAssistant(message, onReplayDegrade) {
292
296
  return foreignAssistant(message);
293
297
  }
294
298
  }
295
- async function piToolContent(blocks, ctx, options, imageMap) {
299
+ async function piToolContent(blocks, ctx, options, imageMap, requestImages) {
296
300
  const content = [];
297
301
  for (const block of blocks) {
298
302
  if (block.type === "text")
299
303
  content.push({ type: "text", text: block.text });
300
304
  else if (block.type === "image")
301
- content.push(...(await piImageParts(block, ctx, options, imageMap)));
305
+ content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
302
306
  else if (block.type === "reasoning")
303
307
  continue;
304
308
  else if (block.type === "tool-result")
305
- content.push(...(await piToolContent(block.content, ctx, options, imageMap)));
309
+ content.push(...(await piToolContent(block.content, ctx, options, imageMap, requestImages)));
306
310
  else
307
311
  throw unsupportedContent(block.type);
308
312
  }
@@ -331,12 +335,39 @@ function projectFiles(messages, ctx) {
331
335
  content: projectFileContent(message.content, fileRequestText),
332
336
  }));
333
337
  }
338
+ function collectRequestImageRefs(blocks, refs) {
339
+ for (const block of blocks) {
340
+ if (block.type === "image") {
341
+ if (block.offloaded !== true)
342
+ refs.set(String(block.attachment.attachmentId), block.attachment);
343
+ }
344
+ else if (block.type === "tool-result") {
345
+ collectRequestImageRefs(block.content, refs);
346
+ }
347
+ }
348
+ }
349
+ async function prepareRequestImageVersions(messages, ctx, options) {
350
+ const attachments = attachmentServices(ctx?.attachments);
351
+ if (attachments === undefined)
352
+ return new Map();
353
+ const refs = new Map();
354
+ for (const message of messages)
355
+ collectRequestImageRefs(message.content, refs);
356
+ const versions = await Promise.all([...refs.values()].map(async (ref) => {
357
+ const target = {
358
+ ...requestImageDimensions(ref.width, ref.height, options.requestImagePixelBudget),
359
+ maxBytes: options.requestImageMaxBytes,
360
+ };
361
+ return [String(ref.attachmentId), await attachments.store.readImageRequest(ref, target, options.signal)];
362
+ }));
363
+ return new Map(versions);
364
+ }
334
365
  function flattenMessageText(message) {
335
366
  return message.content
336
367
  .map((block) => (block.type === "text" ? block.text : ""))
337
368
  .join("");
338
369
  }
339
- async function dshToPiMessages(messages, ctx, options, imageMap) {
370
+ async function dshToPiMessages(messages, ctx, options, imageMap, requestImages) {
340
371
  const result = [];
341
372
  const toolNames = new Map();
342
373
  for (const message of messages) {
@@ -370,7 +401,7 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
370
401
  if (block.type === "text")
371
402
  content.push({ type: "text", text: block.text });
372
403
  else if (block.type === "image")
373
- content.push(...(await piImageParts(block, ctx, options, imageMap)));
404
+ content.push(...(await piImageParts(block, ctx, options, imageMap, requestImages)));
374
405
  }
375
406
  if (content.length > 0)
376
407
  result.push({ role: "user", content, timestamp: 0 });
@@ -379,7 +410,7 @@ async function dshToPiMessages(messages, ctx, options, imageMap) {
379
410
  if (block.type !== "tool-result")
380
411
  continue;
381
412
  const toolCallId = String(block.toolCallId);
382
- result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap), addedToolNames: [], isError: block.isError === true, timestamp: 0 });
413
+ result.push({ role: "toolResult", toolCallId, toolName: toolNames.get(toolCallId) ?? "unknown", content: await piToolContent(block.content, ctx, options, imageMap, requestImages), isError: block.isError === true, timestamp: 0 });
383
414
  }
384
415
  }
385
416
  return result;
@@ -419,34 +450,40 @@ export async function serializeDshMessages(messages, ctx, options = {}) {
419
450
  const imageMap = new Map();
420
451
  const fileProjected = projectFiles(messages, ctx);
421
452
  const attachments = attachmentServices(ctx?.attachments);
422
- const projected = offloadRequestImagesWithPolicy(fileProjected, {
423
- representation: "base64", maxBytes: normalized.maxRequestImageBytes, byteQuantum: 1,
424
- byteLength: (ref) => Math.min(ref.bytes, normalized.requestImageMaxBytes),
425
- placeholder: (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)),
426
- });
453
+ const requestImages = normalized.imageSupport === "unsupported"
454
+ ? new Map()
455
+ : await prepareRequestImageVersions(fileProjected, ctx, normalized);
456
+ const offloadImages = attachments === undefined || normalized.imageSupport === "unsupported"
457
+ ? 0
458
+ : requiredImageOffload(fileProjected, { representation: "base64", maxBytes: normalized.maxRequestImageBytes }, (block) => {
459
+ // `requiredImageOffload` applies the base64 expansion itself for a
460
+ // base64 representation, so this reports raw request-version bytes.
461
+ const request = requestImages.get(String(block.attachment.attachmentId));
462
+ return request?.bytes ?? block.attachment.bytes;
463
+ });
464
+ if (offloadImages > 0) {
465
+ throw new LlmError(`LCX request images exceed the configured base64 bound; ${offloadImages} more oldest occurrence(s) must be offloaded.`, IMAGE_OFFLOAD_REQUIRED_CODE, { offloadImages });
466
+ }
467
+ const projected = projectOffloadedImages(fileProjected, (ref) => offloadedImageText(ref, attachments === undefined ? undefined : imageAccess(ctx, attachments, ref)));
427
468
  const model = resolvePiResponsesModel(normalized);
428
- const context = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap), tools: [...normalized.tools] };
469
+ const dshContext = { systemPrompt: normalized.systemPrompt, messages: await dshToPiMessages(projected, ctx, normalized, imageMap, requestImages), tools: [...normalized.tools] };
429
470
  const compat = isObject(model.compat) ? model.compat : {};
430
471
  const supportsStrictMode = compat.supportsStrictMode === true;
431
472
  const supportsOpenAIGrammarTools = compat.supportsOpenAIGrammarTools === true;
432
473
  const supportsAdditionalTools = compat.supportsAdditionalTools === true;
433
474
  const supportsToolSearch = compat.supportsToolSearch === true;
434
475
  const deferredToolsMode = supportsAdditionalTools ? "additional-tools" : supportsToolSearch ? "tool-search" : undefined;
435
- const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, supportsOpenAIGrammarTools);
476
+ const grammarToolInputProperties = createGrammarToolInputProperties(dshContext.tools, supportsOpenAIGrammarTools);
436
477
  // DSH has no authoritative added-tool provenance; keep its full catalog immediate.
437
478
  const immediateTools = new Map();
438
- for (const tool of context.tools)
479
+ for (const tool of dshContext.tools)
439
480
  if (tool.name)
440
481
  immediateTools.set(tool.name, tool);
441
- const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools };
442
- const piContext = {
443
- systemPrompt: context.systemPrompt,
444
- messages: context.messages,
445
- tools: context.tools,
446
- };
482
+ const toolOptions = { supportsStrictMode, supportsOpenAIGrammarTools, toolSearchResult: false };
483
+ const piContext = normalizeContext(dshContext);
447
484
  const piModel = model;
448
- const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, deferredTools: new Map(), deferredToolsMode, toolOptions });
449
- const input = restorePortableAssistantPhases(convertedInput, context.messages, { provider: model.provider, id: model.id, api: model.api });
485
+ const convertedInput = convertResponsesMessages(piModel, piContext, new Set(["openai", "openai-codex", "opencode"]), { includeSystemPrompt: normalized.includeSystemPrompt, grammarToolInputProperties, supportsAdditionalTools, supportsToolSearch, toolOptions });
486
+ const input = restorePortableAssistantPhases(convertedInput, dshContext.messages, { provider: model.provider, id: model.id, api: model.api });
450
487
  const tools = options.tools === undefined ? undefined : convertResponsesTools([...immediateTools.values()], toolOptions);
451
488
  return { input, imageMap, tools, model: piModel, grammarToolInputProperties, deferredToolsMode };
452
489
  }
package/lib/index.js CHANGED
@@ -1045,25 +1045,41 @@ function installInjected(ctx, configInput = {}) {
1045
1045
  ctx.inject(["compaction"], (compactionCtx) => {
1046
1046
  patchCompactionPressureService(resolveContextService(compactionCtx, "compaction"), state, () => runtimeConfig, ctx, compactionPatchRecords);
1047
1047
  });
1048
- ctx.on("agent/created", ({ agent }) => {
1048
+ /**
1049
+ * LCX instrumentation is optional capability, never a creation precondition.
1050
+ * DSH 0.1.6 awaits `agent/created` serially, so an escaping failure would make
1051
+ * an otherwise valid Agent uncreatable; `agent/status` stays a contained emit.
1052
+ * Both edges therefore fail closed locally: the Agent runs uninstrumented and
1053
+ * the reason is logged, rather than propagating into the host lifecycle.
1054
+ */
1055
+ const instrumentAgent = (agent, edge) => {
1056
+ try {
1057
+ managedAgents.add(agent);
1058
+ syncAgentTools(agent);
1059
+ const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords);
1060
+ if (installed)
1061
+ ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
1062
+ }
1063
+ catch (error) {
1064
+ ctx.logger?.warn?.(`[lcx-codex] agent instrumentation skipped on ${edge}: ${String(error)}`);
1065
+ }
1066
+ };
1067
+ /** DSH 0.1.6 lifecycle source, recorded for diagnostics only. */
1068
+ const startSource = (value) => typeof value === "string" && /^[a-z]+$/u.test(value) ? value : "unknown";
1069
+ ctx.on("agent/created", (payload) => {
1070
+ const agent = payload?.agent;
1049
1071
  if (agent === null || typeof agent !== "object")
1050
1072
  return;
1051
- managedAgents.add(agent);
1052
- syncAgentTools(agent);
1053
- const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords);
1054
- if (installed)
1055
- ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
1073
+ // `source` is startup | resume | clear | compact. Recorded, not branched on:
1074
+ // no runtime evidence yet that a source needs different instrumentation.
1075
+ instrumentAgent(agent, `agent/created(${startSource(payload?.source)})`);
1056
1076
  }, { global: true });
1057
1077
  ctx.on("agent/status", ({ agent, status }) => {
1058
1078
  if (status !== "running" ||
1059
1079
  agent === null ||
1060
1080
  typeof agent !== "object")
1061
1081
  return;
1062
- managedAgents.add(agent);
1063
- syncAgentTools(agent);
1064
- const installed = patchCompactionPressureForAgent(agent, state, () => runtimeConfig, ctx, compactionPatchRecords);
1065
- if (installed)
1066
- ctx.logger?.info?.("[lcx-codex] pressure coordination installed through AgentPresets service resolver");
1082
+ instrumentAgent(agent, "agent/status(running)");
1067
1083
  }, { global: true });
1068
1084
  ctx.on("llm/stream", (options, next) => {
1069
1085
  if (options.purpose === "session-title")