deepseek-coordination 0.10.1 → 0.11.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/lib/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
1
3
  import Schema$1 from "@deepseek-ai/schemastery";
2
4
  import { Service } from "@deepseek-ai/cordis";
3
- import { closeSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
6
6
  import { DatabaseSync } from "node:sqlite";
7
7
  import { createHash, randomBytes } from "node:crypto";
8
8
  //#region src/shared/enum.ts
@@ -4428,18 +4428,6 @@ const PROFESSION_CONTROLS = [
4428
4428
  ];
4429
4429
  /** Fields whose six defaults differ, therefore one row per scene. */
4430
4430
  const SCENE_FIELDS = [
4431
- {
4432
- field: "thinking",
4433
- copy: "scene.thinking",
4434
- kind: "enum",
4435
- options: ["enabled", "disabled"]
4436
- },
4437
- {
4438
- field: "reasoningEffort",
4439
- copy: "scene.reasoningEffort",
4440
- kind: "text",
4441
- wired: true
4442
- },
4443
4431
  {
4444
4432
  field: "requirePlan",
4445
4433
  copy: "scene.requirePlan",
@@ -5813,9 +5801,11 @@ var SelectionModule = class {
5813
5801
  } catch {
5814
5802
  spec = {};
5815
5803
  }
5804
+ const rawName = typeof option.name === "string" ? option.name.trim() : "";
5805
+ const name = rawName.length > 0 && rawName !== option.value ? rawName : option.value in PermissionLabel ? PermissionLabel[option.value] : option.value;
5816
5806
  return {
5817
5807
  value: option.value,
5818
- name: option.name || PermissionLabel[option.value] || option.value,
5808
+ name,
5819
5809
  description: option.description ?? spec.description,
5820
5810
  ...spec.sandbox === void 0 ? {} : { sandbox: spec.sandbox },
5821
5811
  ...spec.approval === void 0 ? {} : { approval: spec.approval }
@@ -6754,7 +6744,7 @@ var CompactModule = class {
6754
6744
  * (JSON for objects, String for the rest), so no DSH payload is assumed.
6755
6745
  */
6756
6746
  function readSnapshot(result) {
6757
- const record = asRecord$3(result);
6747
+ const record = asRecord$4(result);
6758
6748
  if (record !== void 0) {
6759
6749
  const hasBefore = Object.hasOwn(record, "before");
6760
6750
  const hasAfter = Object.hasOwn(record, "after");
@@ -6774,7 +6764,7 @@ function readSnapshot(result) {
6774
6764
  after: portText(result)
6775
6765
  };
6776
6766
  }
6777
- function asRecord$3(value) {
6767
+ function asRecord$4(value) {
6778
6768
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6779
6769
  }
6780
6770
  /** Turn a port value into recordable text without trusting its shape. */
@@ -8448,12 +8438,12 @@ var TakeoverGate = class {
8448
8438
  * agent. Returns a disposer; unload delegates every open request.
8449
8439
  */
8450
8440
  install(context) {
8451
- const register = asFunction$1(asRecord$2(context)?.on);
8441
+ const register = asFunction$1(asRecord$3(context)?.on);
8452
8442
  if (register === void 0) return () => {};
8453
8443
  const disposers = [];
8454
8444
  const subscribe = (name, listener) => {
8455
8445
  try {
8456
- const dispose = register.call(context, name, listener);
8446
+ const dispose = register.call(context, name, listener, { prepend: true });
8457
8447
  if (typeof dispose === "function") disposers.push(dispose);
8458
8448
  } catch {}
8459
8449
  };
@@ -8531,7 +8521,7 @@ var TakeoverGate = class {
8531
8521
  */
8532
8522
  async handleApproval(request, next, signal) {
8533
8523
  if (this.config.ui?.takeover !== true) return next();
8534
- const event = asRecord$2(request);
8524
+ const event = asRecord$3(request);
8535
8525
  if (event === void 0) return next();
8536
8526
  const abortSignal = signal ?? abortSignalOf(event.signal);
8537
8527
  if (abortSignal?.aborted === true) return next();
@@ -8547,7 +8537,7 @@ var TakeoverGate = class {
8547
8537
  */
8548
8538
  async handleQuestion(request, next, signal) {
8549
8539
  if (this.config.ui?.takeover !== true) return next();
8550
- const event = asRecord$2(request);
8540
+ const event = asRecord$3(request);
8551
8541
  if (event === void 0) return next();
8552
8542
  const abortSignal = signal ?? abortSignalOf(event.signal);
8553
8543
  if (abortSignal?.aborted === true) return next();
@@ -8665,7 +8655,7 @@ var TakeoverGate = class {
8665
8655
  * dropping an otherwise answerable request.
8666
8656
  */
8667
8657
  sessionOf(event) {
8668
- const agent = asRecord$2(event.agent);
8658
+ const agent = asRecord$3(event.agent);
8669
8659
  const handle = agent === void 0 ? "" : typeof agent.id === "string" ? agent.id : "";
8670
8660
  if (handle.length === 0) return void 0;
8671
8661
  try {
@@ -8675,7 +8665,7 @@ var TakeoverGate = class {
8675
8665
  }
8676
8666
  }
8677
8667
  };
8678
- function asRecord$2(value) {
8668
+ function asRecord$3(value) {
8679
8669
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
8680
8670
  }
8681
8671
  function asFunction$1(value) {
@@ -8683,13 +8673,13 @@ function asFunction$1(value) {
8683
8673
  }
8684
8674
  /** Forward the event signal only when it behaves like one. */
8685
8675
  function abortSignalOf(value) {
8686
- const record = asRecord$2(value);
8676
+ const record = asRecord$3(value);
8687
8677
  if (record === void 0) return void 0;
8688
8678
  if (typeof record.aborted !== "boolean" || typeof record.addEventListener !== "function") return void 0;
8689
8679
  return value;
8690
8680
  }
8691
8681
  function handleOf$1(value) {
8692
- const record = asRecord$2(value);
8682
+ const record = asRecord$3(value);
8693
8683
  return typeof record?.handle === "string" && record.handle.length > 0 ? record.handle : void 0;
8694
8684
  }
8695
8685
  /** Build the JSON-safe approval payload the Client renders. */
@@ -8703,7 +8693,7 @@ function approvalPayloadOf(event) {
8703
8693
  };
8704
8694
  }
8705
8695
  function displayReasonOf(value) {
8706
- const record = asRecord$2(value);
8696
+ const record = asRecord$3(value);
8707
8697
  if (record === void 0) return void 0;
8708
8698
  const localized = {};
8709
8699
  for (const [locale, copy] of Object.entries(record)) if (typeof copy === "string") localized[locale] = copy;
@@ -8715,7 +8705,7 @@ function questionSpecsOf(value) {
8715
8705
  const questions = [];
8716
8706
  const seen = /* @__PURE__ */ new Set();
8717
8707
  for (const candidate of value) {
8718
- const record = asRecord$2(candidate);
8708
+ const record = asRecord$3(candidate);
8719
8709
  if (record === void 0) continue;
8720
8710
  const id = typeof record.id === "string" ? record.id : "";
8721
8711
  const question = typeof record.question === "string" ? record.question : "";
@@ -8739,7 +8729,7 @@ function optionsOf(value) {
8739
8729
  if (!Array.isArray(value)) return [];
8740
8730
  const options = [];
8741
8731
  for (const candidate of value) {
8742
- const record = asRecord$2(candidate);
8732
+ const record = asRecord$3(candidate);
8743
8733
  if (record === void 0) continue;
8744
8734
  const label = typeof record.label === "string" ? record.label : "";
8745
8735
  if (label.length === 0) continue;
@@ -8751,7 +8741,7 @@ function optionsOf(value) {
8751
8741
  return options;
8752
8742
  }
8753
8743
  function intentOf$1(value) {
8754
- const record = asRecord$2(value);
8744
+ const record = asRecord$3(value);
8755
8745
  if (record === void 0) return void 0;
8756
8746
  if (typeof record.kind !== "string" || typeof record.approve !== "string") return void 0;
8757
8747
  return {
@@ -8760,7 +8750,7 @@ function intentOf$1(value) {
8760
8750
  };
8761
8751
  }
8762
8752
  function waitOf(value) {
8763
- const record = asRecord$2(value);
8753
+ const record = asRecord$3(value);
8764
8754
  if (record === void 0) return void 0;
8765
8755
  if (typeof record.callId !== "string" || record.callId.length === 0) return void 0;
8766
8756
  return {
@@ -8769,7 +8759,7 @@ function waitOf(value) {
8769
8759
  };
8770
8760
  }
8771
8761
  function validateAnswer(entry, answer) {
8772
- const record = asRecord$2(answer);
8762
+ const record = asRecord$3(answer);
8773
8763
  if (record === void 0) throw new InvalidArgumentError("takeover answer must be an object");
8774
8764
  if (record.kind !== entry.type) throw new InvalidArgumentError(`takeover answer kind must be "${entry.type}"`, { kind: record.kind });
8775
8765
  if (entry.type === "approval") return {
@@ -8794,7 +8784,7 @@ function questionAnswersOf(questions, value) {
8794
8784
  const seen = /* @__PURE__ */ new Set();
8795
8785
  const answers = [];
8796
8786
  for (const candidate of value) {
8797
- const record = asRecord$2(candidate);
8787
+ const record = asRecord$3(candidate);
8798
8788
  if (record === void 0) throw new InvalidArgumentError("question answer entry must be an object");
8799
8789
  const id = record.id;
8800
8790
  if (typeof id !== "string" || id.length === 0) throw new InvalidArgumentError("question answer entry must carry a non-empty id");
@@ -12802,9 +12792,6 @@ var TurnPipeline = class {
12802
12792
  });
12803
12793
  return report;
12804
12794
  }
12805
- if (turn.effort === profile.reasoningEffort) policy.applied.push("effort");
12806
- else this.defer(policy, report, "reasoningEffort", `turn effort "${turn.effort}" differs from the scene effort "${profile.reasoningEffort}"`);
12807
- this.defer(policy, report, "thinking", "no DSH model-config port: the pipeline records the scene but cannot toggle reasoning");
12808
12795
  this.defer(policy, report, "allowBash", "no execution-policy port: bash permission is left to the DSH permission preset");
12809
12796
  this.defer(policy, report, "maxIteration", "no iterative driver consumes scene.maxIteration yet");
12810
12797
  policy.applied.push("allowWeb");
@@ -12973,6 +12960,12 @@ function evidenceCount(value) {
12973
12960
  return 1;
12974
12961
  }
12975
12962
  }
12963
+ /** The neutral effort recorded when DSH has not committed a route yet. */
12964
+ const DefaultEffort = "medium";
12965
+ /** Narrow an unknown to a plain record without throwing. */
12966
+ function recordOf$1(value) {
12967
+ return typeof value === "object" && value !== null ? value : void 0;
12968
+ }
12976
12969
  function textOr(value, fallback) {
12977
12970
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
12978
12971
  }
@@ -13137,6 +13130,26 @@ var CoordinationService = class extends Service {
13137
13130
  listProfessionRecord(session) {
13138
13131
  return this.profession.listProfessionRecord(session);
13139
13132
  }
13133
+ /**
13134
+ * The reasoning effort DSH will send for this session.
13135
+ *
13136
+ * Read from the session's own request header instead of plugin config: the
13137
+ * model page owns this choice and the plugin never overrides it. A session
13138
+ * with no committed route yet has no header, so the row records the neutral
13139
+ * default rather than guessing from a scene literal.
13140
+ */
13141
+ observedEffort(sessionId) {
13142
+ try {
13143
+ const raw = recordOf$1(this.runtime.rawSession(sessionId));
13144
+ const read = raw?.requestHeader;
13145
+ if (typeof read !== "function") return DefaultEffort;
13146
+ const config = recordOf$1(recordOf$1(read.call(raw))?.config);
13147
+ const effort = typeof config?.reasoningEffort === "string" ? config.reasoningEffort.trim() : "";
13148
+ return effort.length > 0 ? effort : DefaultEffort;
13149
+ } catch {
13150
+ return DefaultEffort;
13151
+ }
13152
+ }
13140
13153
  /** Open (or reuse) the coordination turn for one DSH turn. */
13141
13154
  async beginTurn(sessionId, dshTurn, promptText = "") {
13142
13155
  const session = this.session.ensure(sessionId);
@@ -13144,7 +13157,6 @@ var CoordinationService = class extends Service {
13144
13157
  if (existing !== void 0 && existing.dshTurn === dshTurn) return this.turn.get(existing.identity);
13145
13158
  if (existing !== void 0) this.finishQuietly(existing.identity, "done");
13146
13159
  const selection = this.selection.get(sessionId);
13147
- const scene = resolveSceneProfile(this.config, selection.scene);
13148
13160
  const turn = this.turn.create(session.identity, {
13149
13161
  content: promptText,
13150
13162
  profession: selection.profession,
@@ -13152,7 +13164,7 @@ var CoordinationService = class extends Service {
13152
13164
  scene: selection.scene,
13153
13165
  validation: selection.validation,
13154
13166
  command: "none",
13155
- effort: scene.reasoningEffort
13167
+ effort: this.observedEffort(sessionId)
13156
13168
  });
13157
13169
  this.dshTurns.set(sessionId, {
13158
13170
  dshTurn,
@@ -13436,18 +13448,18 @@ function bootHost(db, config, handles, clock) {
13436
13448
  function isRecord(value) {
13437
13449
  return typeof value === "object" && value !== null;
13438
13450
  }
13439
- function asRecord$1(value) {
13451
+ function asRecord$2(value) {
13440
13452
  return isRecord(value) ? value : void 0;
13441
13453
  }
13442
13454
  function asFunction(value) {
13443
13455
  return typeof value === "function" ? value : void 0;
13444
13456
  }
13445
- function asString(value, fallback = "") {
13457
+ function asString$1(value, fallback = "") {
13446
13458
  return typeof value === "string" ? value : fallback;
13447
13459
  }
13448
13460
  /** Resolve one Cordis service without throwing when it is absent. */
13449
13461
  function serviceOf(context, name) {
13450
- const holder = asRecord$1(context);
13462
+ const holder = asRecord$2(context);
13451
13463
  if (holder === void 0) return void 0;
13452
13464
  const get = asFunction(holder.get);
13453
13465
  if (get !== void 0) try {
@@ -13455,13 +13467,13 @@ function serviceOf(context, name) {
13455
13467
  if (isRecord(value) || typeof value === "function") return isRecord(value) ? value : { call: value };
13456
13468
  } catch {}
13457
13469
  try {
13458
- return asRecord$1(holder[name]);
13470
+ return asRecord$2(holder[name]);
13459
13471
  } catch {
13460
13472
  return;
13461
13473
  }
13462
13474
  }
13463
13475
  function makeLogger(context) {
13464
- const logger = asRecord$1(context)?.["logger"];
13476
+ const logger = asRecord$2(context)?.["logger"];
13465
13477
  if (!isRecord(logger)) return void 0;
13466
13478
  const record = (level) => {
13467
13479
  const fn = asFunction(logger[level]);
@@ -13480,42 +13492,42 @@ function makeLogger(context) {
13480
13492
  };
13481
13493
  }
13482
13494
  function adaptProvider(value) {
13483
- const record = asRecord$1(value) ?? {};
13484
- const id = asString(record.id) || asString(record.provider) || asString(record.name);
13495
+ const record = asRecord$2(value) ?? {};
13496
+ const id = asString$1(record.id) || asString$1(record.provider) || asString$1(record.name);
13485
13497
  return {
13486
13498
  id,
13487
- name: asString(record.name) || id
13499
+ name: asString$1(record.name) || id
13488
13500
  };
13489
13501
  }
13490
13502
  function adaptConfigurableProvider(value) {
13491
- const record = asRecord$1(value) ?? {};
13492
- const provider = asString(record.provider) || asString(record.id);
13503
+ const record = asRecord$2(value) ?? {};
13504
+ const provider = asString$1(record.provider) || asString$1(record.id);
13493
13505
  const settingsPath = Array.isArray(record.settingsPath) ? record.settingsPath.filter((entry) => typeof entry === "string") : [];
13494
13506
  return {
13495
13507
  provider,
13496
- displayName: asString(record.displayName) || provider,
13497
- settingsNs: asString(record.settingsNs),
13508
+ displayName: asString$1(record.displayName) || provider,
13509
+ settingsNs: asString$1(record.settingsNs),
13498
13510
  settingsPath,
13499
13511
  ...typeof record.declared === "boolean" ? { declared: record.declared } : {},
13500
13512
  ...typeof record.error === "string" ? { error: record.error } : {}
13501
13513
  };
13502
13514
  }
13503
13515
  function adaptModel(provider, value) {
13504
- const record = asRecord$1(value) ?? {};
13505
- const id = asString(record.id) || asString(record.model);
13516
+ const record = asRecord$2(value) ?? {};
13517
+ const id = asString$1(record.id) || asString$1(record.model);
13506
13518
  return {
13507
- provider: asString(record.provider) || provider,
13519
+ provider: asString$1(record.provider) || provider,
13508
13520
  id,
13509
- name: asString(record.name) || id,
13521
+ name: asString$1(record.name) || id,
13510
13522
  ...typeof record.description === "string" ? { description: record.description } : {},
13511
13523
  ...Array.isArray(record.inputModalities) ? { inputModalities: record.inputModalities.filter((entry) => typeof entry === "string") } : {}
13512
13524
  };
13513
13525
  }
13514
13526
  function adaptResolvedModel(provider, model, value) {
13515
13527
  const base = adaptModel(provider, value);
13516
- const record = asRecord$1(value) ?? {};
13517
- const context = asRecord$1(record.context);
13518
- const reasoning = asRecord$1(record.reasoning);
13528
+ const record = asRecord$2(value) ?? {};
13529
+ const context = asRecord$2(record.context);
13530
+ const reasoning = asRecord$2(record.reasoning);
13519
13531
  const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts : [];
13520
13532
  return {
13521
13533
  ...base,
@@ -13523,9 +13535,9 @@ function adaptResolvedModel(provider, model, value) {
13523
13535
  ...context !== void 0 && typeof context.contextWindow === "number" ? { context: { contextWindow: context.contextWindow } } : {},
13524
13536
  ...typeof record.defaultMaxTokens === "number" ? { defaultMaxTokens: record.defaultMaxTokens } : {},
13525
13537
  ...reasoning === void 0 ? {} : { reasoning: {
13526
- efforts: efforts.map((entry) => asRecord$1(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13527
- id: asString(entry.id),
13528
- name: asString(entry.name) || asString(entry.id),
13538
+ efforts: efforts.map((entry) => asRecord$2(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13539
+ id: asString$1(entry.id),
13540
+ name: asString$1(entry.name) || asString$1(entry.id),
13529
13541
  ...typeof entry.description === "string" ? { description: entry.description } : {}
13530
13542
  })),
13531
13543
  ...typeof reasoning.defaultEffort === "string" ? { defaultEffort: reasoning.defaultEffort } : {}
@@ -13569,9 +13581,9 @@ function makeAgentPort(context) {
13569
13581
  const list = asFunction(agents.list);
13570
13582
  if (get === void 0 && list === void 0) return void 0;
13571
13583
  const toView = (value) => {
13572
- const record = asRecord$1(value);
13584
+ const record = asRecord$2(value);
13573
13585
  if (record === void 0) return void 0;
13574
- const id = asString(record.id);
13586
+ const id = asString$1(record.id);
13575
13587
  return id.length === 0 ? void 0 : {
13576
13588
  id,
13577
13589
  session: record.session
@@ -13681,7 +13693,7 @@ function makeCompactionPort(context, rawAgent) {
13681
13693
  return await compactNow.call(compaction, live, signal);
13682
13694
  },
13683
13695
  ...compactIfNeeded === void 0 ? {} : { compactIfNeeded: async (agent, trigger, signal) => {
13684
- const live = asRecord$1(rawAgent(agent.id)) ?? {};
13696
+ const live = asRecord$2(rawAgent(agent.id)) ?? {};
13685
13697
  const session = live.session;
13686
13698
  if (session === void 0) return null;
13687
13699
  const options = isRecord(live.options) ? live.options : {};
@@ -13719,22 +13731,22 @@ function makePermissionPresetPort(context) {
13719
13731
  const resolve = asFunction(presets.resolve);
13720
13732
  return {
13721
13733
  catalog: () => {
13722
- const value = asRecord$1(catalog.call(presets)) ?? {};
13734
+ const value = asRecord$2(catalog.call(presets)) ?? {};
13723
13735
  return {
13724
- options: (Array.isArray(value.options) ? value.options : []).map((entry) => asRecord$1(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13725
- value: asString(entry.value),
13726
- name: asString(entry.name) || asString(entry.value),
13736
+ options: (Array.isArray(value.options) ? value.options : []).map((entry) => asRecord$2(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13737
+ value: asString$1(entry.value),
13738
+ name: asString$1(entry.name) || asString$1(entry.value),
13727
13739
  ...typeof entry.description === "string" ? { description: entry.description } : {}
13728
13740
  })).filter((entry) => entry.value.length > 0),
13729
13741
  ...typeof value.defaultPreset === "string" ? { defaultPreset: value.defaultPreset } : {}
13730
13742
  };
13731
13743
  },
13732
- current: (session) => asString(current.call(presets, session)),
13744
+ current: (session) => asString$1(current.call(presets, session)),
13733
13745
  set: (session, name) => {
13734
13746
  set.call(presets, session, name);
13735
13747
  },
13736
13748
  ...resolve === void 0 ? {} : { resolve: (name) => {
13737
- const value = asRecord$1(resolve.call(presets, name)) ?? {};
13749
+ const value = asRecord$2(resolve.call(presets, name)) ?? {};
13738
13750
  return {
13739
13751
  ...typeof value.sandbox === "string" ? { sandbox: value.sandbox } : {},
13740
13752
  ...typeof value.approval === "string" ? { approval: value.approval } : {},
@@ -13749,15 +13761,15 @@ function makeSandboxPolicyPort(context) {
13749
13761
  if (policy === void 0) return void 0;
13750
13762
  const resolve = asFunction(policy.resolve);
13751
13763
  const overrideOf = asFunction(policy.overrideOf);
13752
- const defaultMode = asString(policy.defaultMode, "workspace-write");
13764
+ const defaultMode = asString$1(policy.defaultMode, "workspace-write");
13753
13765
  if (resolve === void 0) return void 0;
13754
13766
  return {
13755
13767
  defaultMode,
13756
13768
  resolve: (request) => {
13757
- const value = asRecord$1(resolve.call(policy, request ?? {})) ?? {};
13769
+ const value = asRecord$2(resolve.call(policy, request ?? {})) ?? {};
13758
13770
  return {
13759
- mode: asString(value.mode, defaultMode),
13760
- workspaceRoot: asString(value.workspaceRoot)
13771
+ mode: asString$1(value.mode, defaultMode),
13772
+ workspaceRoot: asString$1(value.workspaceRoot)
13761
13773
  };
13762
13774
  },
13763
13775
  overrideOf: (session) => {
@@ -13788,7 +13800,7 @@ function createRuntime(context) {
13788
13800
  };
13789
13801
  }
13790
13802
  function warn(context, message, error) {
13791
- const logger = asRecord$1(asRecord$1(context)?.["logger"]);
13803
+ const logger = asRecord$2(asRecord$2(context)?.["logger"]);
13792
13804
  const fn = asFunction(logger?.warn);
13793
13805
  if (fn !== void 0) try {
13794
13806
  fn.call(logger, `deepseek-coordination: ${message}: ${error instanceof Error ? error.message : String(error)}`);
@@ -13805,7 +13817,7 @@ function contentText(value) {
13805
13817
  parts.push(entry);
13806
13818
  continue;
13807
13819
  }
13808
- const text = asString(asRecord$1(entry)?.text);
13820
+ const text = asString$1(asRecord$2(entry)?.text);
13809
13821
  if (text.length > 0) parts.push(text);
13810
13822
  }
13811
13823
  return parts.join("\n");
@@ -13818,7 +13830,7 @@ function contentText(value) {
13818
13830
  function promptTextOfMessages(messages) {
13819
13831
  if (!Array.isArray(messages)) return void 0;
13820
13832
  for (let index = messages.length - 1; index >= 0; index -= 1) {
13821
- const message = asRecord$1(messages[index]);
13833
+ const message = asRecord$2(messages[index]);
13822
13834
  if (message?.role !== "user") continue;
13823
13835
  const text = contentText(message.content).trim();
13824
13836
  if (text.length > 0) return text;
@@ -13836,12 +13848,12 @@ const TurnHookBudgetMs = 5e3;
13836
13848
  * here at all.
13837
13849
  */
13838
13850
  function installTurnLifecycle(context, lifecycle) {
13839
- const on = asFunction(asRecord$1(context)?.on);
13851
+ const on = asFunction(asRecord$2(context)?.on);
13840
13852
  if (on === void 0) return () => {};
13841
13853
  const disposers = [];
13842
13854
  const preStep = on.call(context, "agent/pre-step", async (payload, next) => {
13843
- const record = asRecord$1(payload);
13844
- const agentId = asString(asRecord$1(record?.agent)?.id);
13855
+ const record = asRecord$2(payload);
13856
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13845
13857
  if (agentId.length > 0 && typeof record?.turn === "number") {
13846
13858
  const prompt = promptTextOfMessages(record?.messages);
13847
13859
  const observation = {
@@ -13868,8 +13880,8 @@ function installTurnLifecycle(context, lifecycle) {
13868
13880
  });
13869
13881
  if (typeof preStep === "function") disposers.push(preStep);
13870
13882
  const stopping = on.call(context, "agent/turn-stopping", async (payload) => {
13871
- const record = asRecord$1(payload);
13872
- const agentId = asString(asRecord$1(record?.agent)?.id);
13883
+ const record = asRecord$2(payload);
13884
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13873
13885
  try {
13874
13886
  if (agentId.length > 0 && typeof record?.turn === "number") await lifecycle.end({
13875
13887
  session: agentId,
@@ -13882,8 +13894,8 @@ function installTurnLifecycle(context, lifecycle) {
13882
13894
  if (typeof stopping === "function") disposers.push(stopping);
13883
13895
  if (lifecycle.fail !== void 0) {
13884
13896
  const failed = on.call(context, "agent/error", (payload) => {
13885
- const record = asRecord$1(payload);
13886
- const agentId = asString(asRecord$1(record?.agent)?.id);
13897
+ const record = asRecord$2(payload);
13898
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13887
13899
  if (agentId.length === 0 || typeof record?.turn !== "number") return;
13888
13900
  const turn = record.turn;
13889
13901
  const failure = record.error;
@@ -13904,6 +13916,463 @@ function installTurnLifecycle(context, lifecycle) {
13904
13916
  };
13905
13917
  }
13906
13918
  //#endregion
13919
+ //#region src/host/settings-store.ts
13920
+ /**
13921
+ * Durable settings edits for the host's own knobs (v24 §19).
13922
+ *
13923
+ * DSH's settings service only persists fields whose schema node is volatile,
13924
+ * and a volatile node changes every parsed value into a reference wrapper —
13925
+ * which this host cannot adopt without rewriting every config reader. The
13926
+ * settings page therefore persists through this module instead: one small JSON
13927
+ * document beside the coordination database, holding only the paths the page
13928
+ * actually wrote.
13929
+ *
13930
+ * Contract:
13931
+ * - the document is a flat `{ "<dotted.path>": scalar }` map, the same shape
13932
+ * the `POST /config` route speaks, so nothing is nested twice;
13933
+ * - a missing, unreadable or malformed file means "no overrides", never an
13934
+ * error: a deployment that never opened the page has no state to load;
13935
+ * - writes are atomic enough for one process (temp file + rename), so a crash
13936
+ * mid-write cannot truncate the previous document;
13937
+ * - the two deployment secrets are never stored: `applyConfigPatch` refuses
13938
+ * them before this module is reached, and the filter is repeated here so a
13939
+ * hand-edited file cannot smuggle them in.
13940
+ */
13941
+ /** File name placed beside the coordination database. */
13942
+ const SETTINGS_FILE = "config.overrides.json";
13943
+ /** Paths this module must never persist, even when a patch names them. */
13944
+ const REFUSED_KEYS = /* @__PURE__ */ new Set(["database", "databasePath"]);
13945
+ /** The override document beside one database file. */
13946
+ function settingsPath(databasePath) {
13947
+ return join(dirname(databasePath), SETTINGS_FILE);
13948
+ }
13949
+ /**
13950
+ * Read the stored overrides of one profile.
13951
+ *
13952
+ * Non-scalar entries and refused keys are dropped rather than surfaced: a
13953
+ * corrupt document must degrade to "no overrides", so the resolved config stays
13954
+ * exactly what the deployment patch describes.
13955
+ */
13956
+ function readOverrides(file) {
13957
+ let text;
13958
+ try {
13959
+ text = readFileSync(file, "utf8");
13960
+ } catch {
13961
+ return {};
13962
+ }
13963
+ let parsed;
13964
+ try {
13965
+ parsed = JSON.parse(text);
13966
+ } catch {
13967
+ return {};
13968
+ }
13969
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
13970
+ const overrides = {};
13971
+ for (const [key, value] of Object.entries(parsed)) {
13972
+ if (REFUSED_KEYS.has(key)) continue;
13973
+ if (typeof value === "string" || typeof value === "boolean") overrides[key] = value;
13974
+ else if (typeof value === "number" && Number.isFinite(value)) overrides[key] = value;
13975
+ }
13976
+ return overrides;
13977
+ }
13978
+ /**
13979
+ * Merge one applied patch into the stored overrides.
13980
+ *
13981
+ * @returns true when the document now holds the patch; false when the directory
13982
+ * or file could not be written (the caller then reports `persisted: false`).
13983
+ */
13984
+ function writeOverrides(file, patch) {
13985
+ const merged = readOverrides(file);
13986
+ for (const [key, value] of Object.entries(patch)) {
13987
+ if (REFUSED_KEYS.has(key)) continue;
13988
+ merged[key] = value;
13989
+ }
13990
+ const temporary = `${file}.${process.pid}.tmp`;
13991
+ try {
13992
+ mkdirSync(dirname(file), { recursive: true });
13993
+ writeFileSync(temporary, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
13994
+ renameSync(temporary, file);
13995
+ return true;
13996
+ } catch {
13997
+ return false;
13998
+ }
13999
+ }
14000
+ //#endregion
14001
+ //#region src/host/proxy-adapter.ts
14002
+ /**
14003
+ * Provider allowlist shared by the adapter and the routing listener.
14004
+ *
14005
+ * Only the deployment's own model routes may be proxied. Anything else (a
14006
+ * provider another plugin registered, a future third-party route) is left
14007
+ * exactly as the harness built it.
14008
+ */
14009
+ const PROXY_ALLOWED_PROVIDERS = [
14010
+ "deepseek",
14011
+ "deepseek-account",
14012
+ "deepseek-official"
14013
+ ];
14014
+ /** True when the proxy may intercept this provider route. */
14015
+ function isProxyProviderAllowed(provider, allow = PROXY_ALLOWED_PROVIDERS) {
14016
+ return typeof provider === "string" && allow.includes(provider);
14017
+ }
14018
+ function asRecord$1(value) {
14019
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
14020
+ }
14021
+ function asString(value) {
14022
+ return typeof value === "string" ? value : "";
14023
+ }
14024
+ /** Structural shape of one request: counts only, never content. */
14025
+ function summarise(request) {
14026
+ const messages = Array.isArray(request.messages) ? request.messages : [];
14027
+ let chars = 0;
14028
+ for (const message of messages) {
14029
+ const content = asRecord$1(message)?.content;
14030
+ if (typeof content === "string") chars += content.length;
14031
+ else if (Array.isArray(content)) for (const block of content) {
14032
+ const record = asRecord$1(block);
14033
+ if (typeof record?.text === "string") chars += record.text.length;
14034
+ }
14035
+ }
14036
+ const tools = Array.isArray(request.tools) ? request.tools.length : void 0;
14037
+ return {
14038
+ messages: messages.length,
14039
+ chars,
14040
+ ...tools === void 0 ? {} : { tools }
14041
+ };
14042
+ }
14043
+ /** Build the adapter. Registration and disposal belong to the caller. */
14044
+ function createProxyAdapter(options) {
14045
+ const { provider: proxyProvider, llm } = options;
14046
+ const allow = options.allowProviders ?? PROXY_ALLOWED_PROVIDERS;
14047
+ /** The real route, or `undefined` before the first request names one. */
14048
+ const upstream = () => {
14049
+ const route = options.routeOf();
14050
+ if (route === void 0) return void 0;
14051
+ const provider = asString(route.provider).trim();
14052
+ if (provider.length === 0) return void 0;
14053
+ const model = asString(route.model).trim();
14054
+ return {
14055
+ provider,
14056
+ ...model.length === 0 ? {} : { model }
14057
+ };
14058
+ };
14059
+ const stream = (request) => {
14060
+ const frozen = asRecord$1(request);
14061
+ if (frozen === void 0) {
14062
+ options.trace?.({
14063
+ event: "refuse",
14064
+ provider: proxyProvider,
14065
+ reason: "request-not-an-object"
14066
+ });
14067
+ throw new Error("coordination proxy: the request is not an object");
14068
+ }
14069
+ const route = upstream();
14070
+ if (route === void 0) {
14071
+ options.trace?.({
14072
+ event: "refuse",
14073
+ provider: proxyProvider,
14074
+ reason: "no-upstream-route"
14075
+ });
14076
+ throw new Error("coordination proxy: no upstream route recorded yet");
14077
+ }
14078
+ if (route.provider === proxyProvider) {
14079
+ options.trace?.({
14080
+ event: "refuse",
14081
+ provider: proxyProvider,
14082
+ reason: "self-forward"
14083
+ });
14084
+ throw new Error(`coordination proxy: refusing to forward "${proxyProvider}" to itself`);
14085
+ }
14086
+ if (!isProxyProviderAllowed(route.provider, allow)) {
14087
+ options.trace?.({
14088
+ event: "refuse",
14089
+ provider: route.provider,
14090
+ reason: "provider-not-allowlisted"
14091
+ });
14092
+ throw new Error(`coordination proxy: provider "${route.provider}" is not on the allowlist`);
14093
+ }
14094
+ const forwarding = llm.stream;
14095
+ if (typeof forwarding !== "function") {
14096
+ options.trace?.({
14097
+ event: "refuse",
14098
+ provider: route.provider,
14099
+ reason: "stream-unavailable"
14100
+ });
14101
+ throw new Error("coordination proxy: ctx.llm.stream is unavailable");
14102
+ }
14103
+ const outgoing = {
14104
+ ...options.decorate === void 0 ? { ...frozen } : options.decorate(frozen),
14105
+ provider: route.provider
14106
+ };
14107
+ options.trace?.({
14108
+ event: "forward",
14109
+ provider: route.provider,
14110
+ ...typeof outgoing.model === "string" ? { model: outgoing.model } : {},
14111
+ ...summarise(outgoing)
14112
+ });
14113
+ return forwarding.call(llm, outgoing);
14114
+ };
14115
+ return {
14116
+ providerInfo: (provider) => ({
14117
+ id: provider,
14118
+ name: "Coordination Proxy"
14119
+ }),
14120
+ providerRetryPolicy: () => void 0,
14121
+ imageRequestPricing: () => void 0,
14122
+ imageRequestTarget: (_provider, _model, image) => ({
14123
+ width: image.width,
14124
+ height: image.height,
14125
+ maxBytes: image.bytes
14126
+ }),
14127
+ async listModels(provider) {
14128
+ const route = upstream();
14129
+ if (route === void 0 || typeof llm.listModels !== "function") return [];
14130
+ try {
14131
+ const models = await llm.listModels(route.provider);
14132
+ return Array.isArray(models) ? models.map((entry) => {
14133
+ const record = asRecord$1(entry);
14134
+ return record === void 0 ? entry : {
14135
+ ...record,
14136
+ provider
14137
+ };
14138
+ }) : [];
14139
+ } catch {
14140
+ return [];
14141
+ }
14142
+ },
14143
+ async resolveModel(provider, model, signal) {
14144
+ const route = upstream();
14145
+ if (route !== void 0 && typeof llm.resolveModelInfo === "function") try {
14146
+ const info = asRecord$1(await llm.resolveModelInfo(route.provider, model, signal));
14147
+ if (info !== void 0) return {
14148
+ ...info,
14149
+ provider,
14150
+ id: model,
14151
+ name: asString(info.name) || model
14152
+ };
14153
+ } catch {}
14154
+ return {
14155
+ provider,
14156
+ id: model,
14157
+ name: model
14158
+ };
14159
+ },
14160
+ async prepareCall(provider, model, signal) {
14161
+ return {
14162
+ model: await this.resolveModel(provider, model, signal),
14163
+ stream: (request) => stream(request)
14164
+ };
14165
+ },
14166
+ stream
14167
+ };
14168
+ }
14169
+ //#endregion
14170
+ //#region src/host/model-brand.ts
14171
+ /**
14172
+ * Built-in profiles.
14173
+ *
14174
+ * Where a provider documents both an automatic and an explicit mode (Google),
14175
+ * the conservative automatic reading is used: the plugin can keep a prefix
14176
+ * stable, and cannot emit markers.
14177
+ */
14178
+ const BRAND_PROFILES = {
14179
+ deepseek: {
14180
+ brand: "deepseek",
14181
+ label: "DeepSeek",
14182
+ automaticPrefixCache: true,
14183
+ explicitCacheBreakpoints: false,
14184
+ minPrefixTokens: 64,
14185
+ effortBreaksCache: true,
14186
+ stripVolatileHelps: true,
14187
+ note: "Automatic prefix cache; keep the prefix stable and avoid effort changes."
14188
+ },
14189
+ openai: {
14190
+ brand: "openai",
14191
+ label: "OpenAI",
14192
+ automaticPrefixCache: true,
14193
+ explicitCacheBreakpoints: false,
14194
+ minPrefixTokens: 1024,
14195
+ effortBreaksCache: false,
14196
+ stripVolatileHelps: true,
14197
+ note: "Automatic prompt cache above ~1k tokens; effort does not change the prefix."
14198
+ },
14199
+ anthropic: {
14200
+ brand: "anthropic",
14201
+ label: "Anthropic",
14202
+ automaticPrefixCache: false,
14203
+ explicitCacheBreakpoints: true,
14204
+ minPrefixTokens: 1024,
14205
+ effortBreaksCache: true,
14206
+ stripVolatileHelps: false,
14207
+ note: "Explicit cache_control breakpoints only; this plugin cannot emit them."
14208
+ },
14209
+ google: {
14210
+ brand: "google",
14211
+ label: "Google",
14212
+ automaticPrefixCache: true,
14213
+ explicitCacheBreakpoints: true,
14214
+ minPrefixTokens: 1024,
14215
+ effortBreaksCache: false,
14216
+ stripVolatileHelps: true,
14217
+ note: "Implicit prefix cache plus optional explicit contexts; the implicit mode is the safe lever."
14218
+ },
14219
+ xai: {
14220
+ brand: "xai",
14221
+ label: "xAI",
14222
+ automaticPrefixCache: true,
14223
+ explicitCacheBreakpoints: false,
14224
+ minPrefixTokens: 1024,
14225
+ effortBreaksCache: true,
14226
+ stripVolatileHelps: true,
14227
+ note: "Automatic prefix cache; effort and prefix edits both reset it."
14228
+ },
14229
+ moonshot: {
14230
+ brand: "moonshot",
14231
+ label: "Moonshot",
14232
+ automaticPrefixCache: true,
14233
+ explicitCacheBreakpoints: false,
14234
+ minPrefixTokens: 1024,
14235
+ effortBreaksCache: true,
14236
+ stripVolatileHelps: true,
14237
+ note: "Automatic prefix cache; reasoning content is preserved separately."
14238
+ },
14239
+ qwen: {
14240
+ brand: "qwen",
14241
+ label: "Qwen",
14242
+ automaticPrefixCache: true,
14243
+ explicitCacheBreakpoints: false,
14244
+ minPrefixTokens: 1024,
14245
+ effortBreaksCache: false,
14246
+ stripVolatileHelps: true,
14247
+ note: "Automatic prefix cache; keep the system prompt and tool list stable."
14248
+ },
14249
+ zhipu: {
14250
+ brand: "zhipu",
14251
+ label: "Zhipu",
14252
+ automaticPrefixCache: true,
14253
+ explicitCacheBreakpoints: false,
14254
+ minPrefixTokens: 1024,
14255
+ effortBreaksCache: true,
14256
+ stripVolatileHelps: true,
14257
+ note: "Automatic prefix cache; treat a parameter change as a cache reset."
14258
+ },
14259
+ mistral: {
14260
+ brand: "mistral",
14261
+ label: "Mistral",
14262
+ automaticPrefixCache: true,
14263
+ explicitCacheBreakpoints: false,
14264
+ minPrefixTokens: 1024,
14265
+ effortBreaksCache: false,
14266
+ stripVolatileHelps: true,
14267
+ note: "Prefix caching with a conservative minimum; stable prefixes are the lever."
14268
+ },
14269
+ meta: {
14270
+ brand: "meta",
14271
+ label: "Meta",
14272
+ automaticPrefixCache: true,
14273
+ explicitCacheBreakpoints: false,
14274
+ minPrefixTokens: 1024,
14275
+ effortBreaksCache: false,
14276
+ stripVolatileHelps: true,
14277
+ note: "Self-hosted families vary by deployment; the conservative prefix rule applies."
14278
+ },
14279
+ unknown: {
14280
+ brand: "unknown",
14281
+ label: "Unknown",
14282
+ automaticPrefixCache: true,
14283
+ explicitCacheBreakpoints: false,
14284
+ minPrefixTokens: 1024,
14285
+ effortBreaksCache: true,
14286
+ stripVolatileHelps: true,
14287
+ note: "Unknown family: assume an automatic prefix cache and keep everything stable."
14288
+ }
14289
+ };
14290
+ /**
14291
+ * Brand patterns, tested against provider + model + display name.
14292
+ *
14293
+ * Order matters: the first match wins, so a reseller route named after its
14294
+ * model (`acme-claude`) still lands on Anthropic before any generic rule.
14295
+ */
14296
+ const BRAND_PATTERNS = [
14297
+ {
14298
+ brand: "anthropic",
14299
+ pattern: /anthropic|claude/i
14300
+ },
14301
+ {
14302
+ brand: "openai",
14303
+ pattern: /openai|chatgpt|\bgpt\b|gpt-[0-9o]|\bo[134]\b/i
14304
+ },
14305
+ {
14306
+ brand: "google",
14307
+ pattern: /google|gemini|vertex/i
14308
+ },
14309
+ {
14310
+ brand: "deepseek",
14311
+ pattern: /deepseek/i
14312
+ },
14313
+ {
14314
+ brand: "xai",
14315
+ pattern: /\bxai\b|grok/i
14316
+ },
14317
+ {
14318
+ brand: "moonshot",
14319
+ pattern: /moonshot|kimi/i
14320
+ },
14321
+ {
14322
+ brand: "qwen",
14323
+ pattern: /qwen|tongyi|dashscope|alibaba/i
14324
+ },
14325
+ {
14326
+ brand: "zhipu",
14327
+ pattern: /zhipu|chatglm|\bglm\b/i
14328
+ },
14329
+ {
14330
+ brand: "mistral",
14331
+ pattern: /mistral|mixtral|codestral/i
14332
+ },
14333
+ {
14334
+ brand: "meta",
14335
+ pattern: /\bllama\b|meta-llama/i
14336
+ }
14337
+ ];
14338
+ /** Classify one host-reported model identity onto a known brand. */
14339
+ function classifyModelBrand(identity) {
14340
+ const haystack = [
14341
+ identity.provider,
14342
+ identity.model,
14343
+ identity.name,
14344
+ identity.description
14345
+ ].filter((part) => typeof part === "string" && part.length > 0).join(" ");
14346
+ if (haystack.length === 0) return "unknown";
14347
+ for (const row of BRAND_PATTERNS) if (row.pattern.test(haystack)) return row.brand;
14348
+ return "unknown";
14349
+ }
14350
+ /** The profile for one host-reported identity. */
14351
+ function brandProfileOf(identity) {
14352
+ return BRAND_PROFILES[classifyModelBrand(identity)];
14353
+ }
14354
+ /**
14355
+ * Whether the proxy has a useful local lever for this model.
14356
+ *
14357
+ * With an explicit-breakpoint family the honest answer is "no": the plugin can
14358
+ * keep bytes stable, but reuse is decided by markers it cannot write.
14359
+ */
14360
+ function hasLocalCacheLever(profile) {
14361
+ return profile.automaticPrefixCache && profile.stripVolatileHelps;
14362
+ }
14363
+ /**
14364
+ * Whether outgoing content should be rewritten for this model right now.
14365
+ *
14366
+ * Two gates, both deliberate:
14367
+ * - the operator switch (`cache.enabled`) is read live, so the settings page
14368
+ * turns the rewrite off immediately;
14369
+ * - a family without an automatic prefix cache is never rewritten — rewriting
14370
+ * its bytes cannot improve reuse and only risks changing what the model sees.
14371
+ */
14372
+ function shouldDecorate(profile, cacheEnabled) {
14373
+ return cacheEnabled && hasLocalCacheLever(profile);
14374
+ }
14375
+ //#endregion
13907
14376
  //#region src/shared/i18n.ts
13908
14377
  /**
13909
14378
  * The plugin's single source of user-visible and model-facing copy (v24 §22,
@@ -14538,14 +15007,6 @@ const ZH = {
14538
15007
  label: "工具结果字节上限",
14539
15008
  hint: "工具返回给模型的字节上限;0 表示不限制。"
14540
15009
  },
14541
- "scene.thinking": {
14542
- label: "思考模式",
14543
- hint: "是否把推理过程摊开;对话和办公默认关闭。"
14544
- },
14545
- "scene.reasoningEffort": {
14546
- label: "推理强度",
14547
- hint: "原样传给宿主模型的强度名,例如 high;本插件不改写。"
14548
- },
14549
15010
  "scene.requirePlan": {
14550
15011
  label: "必须出计划",
14551
15012
  hint: "这一情景是否必须先有可评分的计划。"
@@ -14607,10 +15068,6 @@ const ZH = {
14607
15068
  compact: "压缩",
14608
15069
  permission: "权限"
14609
15070
  },
14610
- thinkingOptions: {
14611
- enabled: "开启",
14612
- disabled: "关闭"
14613
- },
14614
15071
  defaultValue: "默认",
14615
15072
  scene: "情景",
14616
15073
  allScenes: "所有情景",
@@ -15332,14 +15789,6 @@ const COPY_BY_LOCALE = {
15332
15789
  label: "Tool result bytes",
15333
15790
  hint: "Byte cap on a tool result returned to the model; 0 means no cap."
15334
15791
  },
15335
- "scene.thinking": {
15336
- label: "Thinking mode",
15337
- hint: "Whether reasoning is shown; off by default for dialogue and office."
15338
- },
15339
- "scene.reasoningEffort": {
15340
- label: "Reasoning effort",
15341
- hint: "Effort name passed to the host model unchanged, e.g. high."
15342
- },
15343
15792
  "scene.requirePlan": {
15344
15793
  label: "Plan required",
15345
15794
  hint: "Whether this scene must produce a scoreable plan first."
@@ -15401,10 +15850,6 @@ const COPY_BY_LOCALE = {
15401
15850
  compact: "Compact",
15402
15851
  permission: "Permission"
15403
15852
  },
15404
- thinkingOptions: {
15405
- enabled: "On",
15406
- disabled: "Off"
15407
- },
15408
15853
  defaultValue: "Default",
15409
15854
  scene: "Scene",
15410
15855
  allScenes: "All scenes",
@@ -27006,27 +27451,307 @@ function sessionOf(url, body) {
27006
27451
  return (fromQuery ?? fromBody ?? "").trim();
27007
27452
  }
27008
27453
  /**
27009
- * Best-effort persistence of an already-applied settings patch.
27454
+ * Nest a flat dotted override map, then merge it over the deployment config.
27010
27455
  *
27011
- * DSH owns persistence; the plugin never writes a second config file. When the
27012
- * host exposes `ctx.settings.update`, the patch is forwarded under the plugin
27013
- * name. A missing port or a refusal leaves the live config (already applied) in
27014
- * force and reports `persisted: false`, so the page can say "applies to this
27015
- * process only" instead of claiming a durable save.
27456
+ * Stored overrides win over the profile patch: the operator changed those paths
27457
+ * from the page, which is the most recent intent. `resolveConfig` still applies
27458
+ * every invariant afterwards (forced flags, weight normalisation, clamping).
27016
27459
  */
27017
- async function persistConfig(context, patch) {
27018
- const settings = context.settings;
27019
- if (settings === void 0 || typeof settings.update !== "function") return false;
27460
+ function mergeOverrides(input, overrides) {
27461
+ const document = {};
27462
+ for (const [key, value] of Object.entries(overrides)) writeConfigPath(document, key, value);
27463
+ return deepMergeRecords(input ?? {}, document);
27464
+ }
27465
+ /** Recursive merge for plain records; arrays and scalars replace. */
27466
+ function deepMergeRecords(base, patch) {
27467
+ if (typeof base !== "object" || base === null || Array.isArray(base)) return patch;
27468
+ if (typeof patch !== "object" || patch === null || Array.isArray(patch)) return patch;
27469
+ const result = { ...base };
27470
+ for (const [key, value] of Object.entries(patch)) result[key] = key in result ? deepMergeRecords(result[key], value) : value;
27471
+ return result;
27472
+ }
27473
+ /** Resolve a Cordis service without tripping the context proxy. */
27474
+ function serviceOrUndefined(context, name) {
27475
+ const getter = context.get;
27476
+ if (typeof getter !== "function") return void 0;
27020
27477
  try {
27021
- await settings.update(name, { ...patch });
27022
- return true;
27478
+ const value = getter.call(context, name);
27479
+ return typeof value === "object" && value !== null ? value : void 0;
27023
27480
  } catch {
27024
- return false;
27481
+ return;
27482
+ }
27483
+ }
27484
+ /** The host webserver, when the profile runs one (a headless profile does not). */
27485
+ function webServerOf(context) {
27486
+ const service = serviceOrUndefined(context, "webServer");
27487
+ return service !== void 0 && typeof service.register === "function" ? service : void 0;
27488
+ }
27489
+ /** The live `ctx.llm` runtime; the proxy adapter needs `stream`, not the port. */
27490
+ function llmRuntimeOf(context) {
27491
+ const service = serviceOrUndefined(context, "llm");
27492
+ return service !== void 0 && typeof service.stream === "function" ? service : void 0;
27493
+ }
27494
+ /** Provider route the cache proxy registers under. */
27495
+ const PROXY_PROVIDER = "coordination-proxy";
27496
+ const proxyStatus = {
27497
+ available: false,
27498
+ enabled: false,
27499
+ provider: PROXY_PROVIDER,
27500
+ forwards: 0,
27501
+ refusals: 0,
27502
+ decorate: false
27503
+ };
27504
+ /** The status snapshot handed to `GET /proxy/status`. */
27505
+ function cacheProxyStatus() {
27506
+ return {
27507
+ ...proxyStatus,
27508
+ ...proxyStatus.upstream === void 0 ? {} : { upstream: { ...proxyStatus.upstream } }
27509
+ };
27510
+ }
27511
+ /** The deployment's default model route, used when no request has named one yet. */
27512
+ function defaultRouteOf(context) {
27513
+ const service = serviceOrUndefined(context, "agentDefaultModel");
27514
+ const current = service?.currentSelection;
27515
+ if (typeof current !== "function") return void 0;
27516
+ try {
27517
+ const selection = current.call(service);
27518
+ const record = typeof selection === "object" && selection !== null ? selection : void 0;
27519
+ const provider = typeof record?.provider === "string" ? record.provider : "";
27520
+ const model = typeof record?.model === "string" ? record.model : "";
27521
+ return provider.length > 0 && model.length > 0 ? {
27522
+ provider,
27523
+ model
27524
+ } : void 0;
27525
+ } catch {
27526
+ return;
27025
27527
  }
27026
27528
  }
27529
+ /**
27530
+ * Register the experimental cache proxy.
27531
+ *
27532
+ * `agent/request` rewrites the route to the proxy provider and remembers the
27533
+ * real one; the adapter forwards each frozen request to it after rebuilding it.
27534
+ * Every step fails open: no llm runtime, no adapter registration or no event bus
27535
+ * simply leaves the normal path untouched.
27536
+ *
27537
+ * Security posture (see `host/proxy-adapter.ts` for the full model):
27538
+ * - only providers on `PROXY_ALLOWED_PROVIDERS` are ever intercepted, so the
27539
+ * proxy cannot redirect traffic to an endpoint this plugin invented;
27540
+ * - the trace file receives structural events only — provider, model, counts,
27541
+ * sizes — never prompt or answer text;
27542
+ * - the adapter holds no transport and no credential: forwarding delegates to
27543
+ * `ctx.llm.stream`, so the real adapter keeps authentication and egress.
27544
+ *
27545
+ * The switch is a file (`proxy.enable`) as well as an environment variable: the
27546
+ * desktop app is launched from the shell's environment, so a file is the only
27547
+ * way to turn the experiment on without restarting with new variables.
27548
+ */
27549
+ function installCacheProxy(context, stateDir, decorate) {
27550
+ const flagFile = join(stateDir, "proxy.enable");
27551
+ const logFile = join(stateDir, "proxy.log");
27552
+ if (process.env.DSH_COORDINATION_PROXY !== "1" && !existsSync(flagFile)) return () => {};
27553
+ const llm = llmRuntimeOf(context);
27554
+ const service = serviceOrUndefined(context, "llm");
27555
+ const register = service?.registerAdapter;
27556
+ const on = context.on;
27557
+ if (llm === void 0 || typeof register !== "function" || typeof on !== "function") return () => {};
27558
+ /**
27559
+ * Append one structural event as a JSON line.
27560
+ *
27561
+ * JSON (not a formatted sentence) keeps the record unambiguous, and the event
27562
+ * type has no field that can carry content — a reviewer can verify the whole
27563
+ * leak surface by reading `ProxyTraceEvent`.
27564
+ */
27565
+ const trace = (event) => {
27566
+ if (event.event === "forward") {
27567
+ proxyStatus.forwards += 1;
27568
+ proxyStatus.lastForwardAt = (/* @__PURE__ */ new Date()).toISOString();
27569
+ }
27570
+ if (event.event === "refuse") proxyStatus.refusals += 1;
27571
+ const line = JSON.stringify({
27572
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
27573
+ ...event
27574
+ });
27575
+ try {
27576
+ appendFileSync(logFile, `${line}\n`, "utf8");
27577
+ } catch {}
27578
+ if (process.env.DSH_COORDINATION_PROXY_DEBUG === "1") console.error(`[coordination-proxy] ${line}`);
27579
+ };
27580
+ let upstream = defaultRouteOf(context);
27581
+ if (upstream !== void 0) {
27582
+ proxyStatus.upstream = { ...upstream };
27583
+ trace({
27584
+ event: "install",
27585
+ provider: PROXY_PROVIDER,
27586
+ model: upstream.model
27587
+ });
27588
+ } else trace({
27589
+ event: "install",
27590
+ provider: PROXY_PROVIDER
27591
+ });
27592
+ /**
27593
+ * Classify the host-reported model identity onto a built-in cache brand.
27594
+ *
27595
+ * `ctx.llm.resolveModelInfo(provider, model)` is the host's own answer for
27596
+ * "what model is this": it carries the display name and description that a
27597
+ * provider route id alone cannot. The call may hit the adapter, so it is
27598
+ * fired and forgotten — routing never waits on catalog I/O, and a failure
27599
+ * simply leaves the `unknown` conservative profile in place.
27600
+ */
27601
+ let describedKey = "";
27602
+ const describeModel = (provider, model) => {
27603
+ const key = `${provider}\u0000${model}`;
27604
+ if (key === describedKey) return;
27605
+ describedKey = key;
27606
+ const llmService = serviceOrUndefined(context, "llm");
27607
+ const resolve = llmService?.resolveModelInfo;
27608
+ const apply = (identity) => {
27609
+ const profile = brandProfileOf(identity);
27610
+ proxyStatus.model = {
27611
+ brand: profile.brand,
27612
+ label: profile.label,
27613
+ automaticPrefixCache: profile.automaticPrefixCache,
27614
+ explicitCacheBreakpoints: profile.explicitCacheBreakpoints,
27615
+ minPrefixTokens: profile.minPrefixTokens,
27616
+ effortBreaksCache: profile.effortBreaksCache,
27617
+ stripVolatileHelps: profile.stripVolatileHelps,
27618
+ note: profile.note
27619
+ };
27620
+ trace({
27621
+ event: "model",
27622
+ provider,
27623
+ model,
27624
+ brand: profile.brand
27625
+ });
27626
+ };
27627
+ if (typeof resolve !== "function") {
27628
+ apply({
27629
+ provider,
27630
+ model
27631
+ });
27632
+ return;
27633
+ }
27634
+ try {
27635
+ const pending = resolve.call(llmService, provider, model);
27636
+ if (pending instanceof Promise) {
27637
+ pending.then((info) => {
27638
+ const record = typeof info === "object" && info !== null ? info : void 0;
27639
+ apply({
27640
+ provider,
27641
+ model,
27642
+ ...typeof record?.name === "string" ? { name: record.name } : {},
27643
+ ...typeof record?.description === "string" ? { description: record.description } : {}
27644
+ });
27645
+ }).catch(() => {
27646
+ apply({
27647
+ provider,
27648
+ model
27649
+ });
27650
+ });
27651
+ return;
27652
+ }
27653
+ const record = typeof pending === "object" && pending !== null ? pending : void 0;
27654
+ apply({
27655
+ provider,
27656
+ model,
27657
+ ...typeof record?.name === "string" ? { name: record.name } : {},
27658
+ ...typeof record?.description === "string" ? { description: record.description } : {}
27659
+ });
27660
+ } catch {
27661
+ apply({
27662
+ provider,
27663
+ model
27664
+ });
27665
+ }
27666
+ };
27667
+ if (upstream !== void 0) describeModel(upstream.provider, upstream.model ?? "");
27668
+ const adapter = createProxyAdapter({
27669
+ provider: PROXY_PROVIDER,
27670
+ llm,
27671
+ routeOf: () => upstream,
27672
+ allowProviders: PROXY_ALLOWED_PROVIDERS,
27673
+ trace,
27674
+ ...decorate === void 0 ? {} : { decorate: (request) => decorate(request, {
27675
+ provider: upstream?.provider ?? "",
27676
+ model: upstream?.model ?? ""
27677
+ }) }
27678
+ });
27679
+ let registration;
27680
+ let detach;
27681
+ try {
27682
+ registration = register.call(service, [PROXY_PROVIDER], adapter);
27683
+ detach = on.call(context, "agent/request", async (_payload, next) => {
27684
+ const seed = await next();
27685
+ const record = typeof seed === "object" && seed !== null ? seed : void 0;
27686
+ const provider = typeof record?.provider === "string" ? record.provider : "";
27687
+ const model = typeof record?.model === "string" ? record.model : "";
27688
+ if (record === void 0 || provider.length === 0 || model.length === 0 || provider === PROXY_PROVIDER) {
27689
+ if (upstream === void 0) upstream = defaultRouteOf(context);
27690
+ return seed;
27691
+ }
27692
+ if (!isProxyProviderAllowed(provider, PROXY_ALLOWED_PROVIDERS)) {
27693
+ trace({
27694
+ event: "refuse",
27695
+ provider,
27696
+ reason: "provider-not-allowlisted"
27697
+ });
27698
+ return seed;
27699
+ }
27700
+ upstream = {
27701
+ provider,
27702
+ model
27703
+ };
27704
+ proxyStatus.upstream = { ...upstream };
27705
+ trace({
27706
+ event: "route",
27707
+ provider,
27708
+ model
27709
+ });
27710
+ describeModel(provider, model);
27711
+ return {
27712
+ ...record,
27713
+ provider: PROXY_PROVIDER,
27714
+ model
27715
+ };
27716
+ }, { prepend: true });
27717
+ } catch {
27718
+ trace({
27719
+ event: "refuse",
27720
+ provider: PROXY_PROVIDER,
27721
+ reason: "registration-failed"
27722
+ });
27723
+ return () => {};
27724
+ }
27725
+ proxyStatus.available = true;
27726
+ proxyStatus.enabled = true;
27727
+ proxyStatus.decorate = decorate !== void 0;
27728
+ return () => {
27729
+ proxyStatus.enabled = false;
27730
+ proxyStatus.decorate = false;
27731
+ if (typeof detach === "function") try {
27732
+ detach();
27733
+ } catch {}
27734
+ if (typeof registration === "function") try {
27735
+ registration();
27736
+ } catch {}
27737
+ };
27738
+ }
27027
27739
  function apply(context, configInput) {
27028
- const service = new CoordinationService(context, resolveConfig(configInput), createRuntime(context));
27740
+ const configured = resolveConfig(configInput);
27741
+ const settingsFile = settingsPath(resolveDatabasePath(configured.databasePath, configured.database));
27742
+ const stored = readOverrides(settingsFile);
27743
+ const config = Object.keys(stored).length === 0 ? configured : resolveConfig(mergeOverrides(configInput, stored));
27744
+ const service = new CoordinationService(context, config, createRuntime(context));
27029
27745
  service.turn.recoverInterrupted();
27746
+ if (process.env.DSH_COORDINATION_PROXY === "1" || existsSync(join(dirname(settingsFile), "proxy.enable"))) {
27747
+ const decorate = (request, identity) => {
27748
+ if (!shouldDecorate(brandProfileOf(identity), config.cache.enabled)) return request;
27749
+ const strategy = service.provider.cacheStrategy(identity.provider);
27750
+ const { cacheWarning: _advice, ...outgoing } = service.cache.decorate(request, strategy);
27751
+ return outgoing;
27752
+ };
27753
+ context.effect(() => installCacheProxy(context, dirname(settingsFile), decorate), `${name}: cache proxy`);
27754
+ }
27030
27755
  /** Re-register the tools after `POST /locale` changed the copy language. */
27031
27756
  let refreshTools;
27032
27757
  context.effect(() => () => {
@@ -27084,7 +27809,9 @@ function apply(context, configInput) {
27084
27809
  };
27085
27810
  }, `${name}: tools`);
27086
27811
  context.effect(() => {
27087
- const dispose = context.webServer.register({
27812
+ const webServer = webServerOf(context);
27813
+ if (webServer === void 0) return () => {};
27814
+ const dispose = webServer.register({
27088
27815
  kind: "prefix",
27089
27816
  path: ROUTE_PREFIX,
27090
27817
  handler: async (request, response) => {
@@ -27103,6 +27830,13 @@ function apply(context, configInput) {
27103
27830
  });
27104
27831
  return;
27105
27832
  }
27833
+ if (path === "/proxy/status" && method === "GET") {
27834
+ json(response, {
27835
+ ok: true,
27836
+ value: cacheProxyStatus()
27837
+ });
27838
+ return;
27839
+ }
27106
27840
  if (path === "/echo" && method === "POST") {
27107
27841
  json(response, {
27108
27842
  ok: true,
@@ -27320,7 +28054,7 @@ function apply(context, configInput) {
27320
28054
  if (outcome.compaction !== null) service.applyCompactionPolicy(outcome.compaction);
27321
28055
  const appliedPatch = {};
27322
28056
  for (const key of outcome.applied) appliedPatch[key] = patch[key];
27323
- const persisted = outcome.applied.length > 0 ? await persistConfig(context, appliedPatch) : false;
28057
+ const persisted = outcome.applied.length > 0 ? writeOverrides(settingsFile, appliedPatch) : false;
27324
28058
  json(response, {
27325
28059
  ok: true,
27326
28060
  value: {
@@ -27375,4 +28109,4 @@ function apply(context, configInput) {
27375
28109
  }, `${name}: ${ROUTE_PREFIX} routes`);
27376
28110
  }
27377
28111
  //#endregion
27378
- export { Config, apply, inject, name };
28112
+ export { Config, apply, cacheProxyStatus, inject, name };