deepseek-coordination 0.10.0 → 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,28 +13448,32 @@ 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 {
13454
13466
  const value = get.call(context, name);
13455
13467
  if (isRecord(value) || typeof value === "function") return isRecord(value) ? value : { call: value };
13456
13468
  } catch {}
13457
- return asRecord$1(holder[name]);
13469
+ try {
13470
+ return asRecord$2(holder[name]);
13471
+ } catch {
13472
+ return;
13473
+ }
13458
13474
  }
13459
13475
  function makeLogger(context) {
13460
- const logger = asRecord$1(context)?.["logger"];
13476
+ const logger = asRecord$2(context)?.["logger"];
13461
13477
  if (!isRecord(logger)) return void 0;
13462
13478
  const record = (level) => {
13463
13479
  const fn = asFunction(logger[level]);
@@ -13476,42 +13492,42 @@ function makeLogger(context) {
13476
13492
  };
13477
13493
  }
13478
13494
  function adaptProvider(value) {
13479
- const record = asRecord$1(value) ?? {};
13480
- 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);
13481
13497
  return {
13482
13498
  id,
13483
- name: asString(record.name) || id
13499
+ name: asString$1(record.name) || id
13484
13500
  };
13485
13501
  }
13486
13502
  function adaptConfigurableProvider(value) {
13487
- const record = asRecord$1(value) ?? {};
13488
- 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);
13489
13505
  const settingsPath = Array.isArray(record.settingsPath) ? record.settingsPath.filter((entry) => typeof entry === "string") : [];
13490
13506
  return {
13491
13507
  provider,
13492
- displayName: asString(record.displayName) || provider,
13493
- settingsNs: asString(record.settingsNs),
13508
+ displayName: asString$1(record.displayName) || provider,
13509
+ settingsNs: asString$1(record.settingsNs),
13494
13510
  settingsPath,
13495
13511
  ...typeof record.declared === "boolean" ? { declared: record.declared } : {},
13496
13512
  ...typeof record.error === "string" ? { error: record.error } : {}
13497
13513
  };
13498
13514
  }
13499
13515
  function adaptModel(provider, value) {
13500
- const record = asRecord$1(value) ?? {};
13501
- 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);
13502
13518
  return {
13503
- provider: asString(record.provider) || provider,
13519
+ provider: asString$1(record.provider) || provider,
13504
13520
  id,
13505
- name: asString(record.name) || id,
13521
+ name: asString$1(record.name) || id,
13506
13522
  ...typeof record.description === "string" ? { description: record.description } : {},
13507
13523
  ...Array.isArray(record.inputModalities) ? { inputModalities: record.inputModalities.filter((entry) => typeof entry === "string") } : {}
13508
13524
  };
13509
13525
  }
13510
13526
  function adaptResolvedModel(provider, model, value) {
13511
13527
  const base = adaptModel(provider, value);
13512
- const record = asRecord$1(value) ?? {};
13513
- const context = asRecord$1(record.context);
13514
- 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);
13515
13531
  const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts : [];
13516
13532
  return {
13517
13533
  ...base,
@@ -13519,9 +13535,9 @@ function adaptResolvedModel(provider, model, value) {
13519
13535
  ...context !== void 0 && typeof context.contextWindow === "number" ? { context: { contextWindow: context.contextWindow } } : {},
13520
13536
  ...typeof record.defaultMaxTokens === "number" ? { defaultMaxTokens: record.defaultMaxTokens } : {},
13521
13537
  ...reasoning === void 0 ? {} : { reasoning: {
13522
- efforts: efforts.map((entry) => asRecord$1(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13523
- id: asString(entry.id),
13524
- 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),
13525
13541
  ...typeof entry.description === "string" ? { description: entry.description } : {}
13526
13542
  })),
13527
13543
  ...typeof reasoning.defaultEffort === "string" ? { defaultEffort: reasoning.defaultEffort } : {}
@@ -13565,9 +13581,9 @@ function makeAgentPort(context) {
13565
13581
  const list = asFunction(agents.list);
13566
13582
  if (get === void 0 && list === void 0) return void 0;
13567
13583
  const toView = (value) => {
13568
- const record = asRecord$1(value);
13584
+ const record = asRecord$2(value);
13569
13585
  if (record === void 0) return void 0;
13570
- const id = asString(record.id);
13586
+ const id = asString$1(record.id);
13571
13587
  return id.length === 0 ? void 0 : {
13572
13588
  id,
13573
13589
  session: record.session
@@ -13677,7 +13693,7 @@ function makeCompactionPort(context, rawAgent) {
13677
13693
  return await compactNow.call(compaction, live, signal);
13678
13694
  },
13679
13695
  ...compactIfNeeded === void 0 ? {} : { compactIfNeeded: async (agent, trigger, signal) => {
13680
- const live = asRecord$1(rawAgent(agent.id)) ?? {};
13696
+ const live = asRecord$2(rawAgent(agent.id)) ?? {};
13681
13697
  const session = live.session;
13682
13698
  if (session === void 0) return null;
13683
13699
  const options = isRecord(live.options) ? live.options : {};
@@ -13715,22 +13731,22 @@ function makePermissionPresetPort(context) {
13715
13731
  const resolve = asFunction(presets.resolve);
13716
13732
  return {
13717
13733
  catalog: () => {
13718
- const value = asRecord$1(catalog.call(presets)) ?? {};
13734
+ const value = asRecord$2(catalog.call(presets)) ?? {};
13719
13735
  return {
13720
- options: (Array.isArray(value.options) ? value.options : []).map((entry) => asRecord$1(entry)).filter((entry) => entry !== void 0).map((entry) => ({
13721
- value: asString(entry.value),
13722
- 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),
13723
13739
  ...typeof entry.description === "string" ? { description: entry.description } : {}
13724
13740
  })).filter((entry) => entry.value.length > 0),
13725
13741
  ...typeof value.defaultPreset === "string" ? { defaultPreset: value.defaultPreset } : {}
13726
13742
  };
13727
13743
  },
13728
- current: (session) => asString(current.call(presets, session)),
13744
+ current: (session) => asString$1(current.call(presets, session)),
13729
13745
  set: (session, name) => {
13730
13746
  set.call(presets, session, name);
13731
13747
  },
13732
13748
  ...resolve === void 0 ? {} : { resolve: (name) => {
13733
- const value = asRecord$1(resolve.call(presets, name)) ?? {};
13749
+ const value = asRecord$2(resolve.call(presets, name)) ?? {};
13734
13750
  return {
13735
13751
  ...typeof value.sandbox === "string" ? { sandbox: value.sandbox } : {},
13736
13752
  ...typeof value.approval === "string" ? { approval: value.approval } : {},
@@ -13745,15 +13761,15 @@ function makeSandboxPolicyPort(context) {
13745
13761
  if (policy === void 0) return void 0;
13746
13762
  const resolve = asFunction(policy.resolve);
13747
13763
  const overrideOf = asFunction(policy.overrideOf);
13748
- const defaultMode = asString(policy.defaultMode, "workspace-write");
13764
+ const defaultMode = asString$1(policy.defaultMode, "workspace-write");
13749
13765
  if (resolve === void 0) return void 0;
13750
13766
  return {
13751
13767
  defaultMode,
13752
13768
  resolve: (request) => {
13753
- const value = asRecord$1(resolve.call(policy, request ?? {})) ?? {};
13769
+ const value = asRecord$2(resolve.call(policy, request ?? {})) ?? {};
13754
13770
  return {
13755
- mode: asString(value.mode, defaultMode),
13756
- workspaceRoot: asString(value.workspaceRoot)
13771
+ mode: asString$1(value.mode, defaultMode),
13772
+ workspaceRoot: asString$1(value.workspaceRoot)
13757
13773
  };
13758
13774
  },
13759
13775
  overrideOf: (session) => {
@@ -13784,7 +13800,7 @@ function createRuntime(context) {
13784
13800
  };
13785
13801
  }
13786
13802
  function warn(context, message, error) {
13787
- const logger = asRecord$1(asRecord$1(context)?.["logger"]);
13803
+ const logger = asRecord$2(asRecord$2(context)?.["logger"]);
13788
13804
  const fn = asFunction(logger?.warn);
13789
13805
  if (fn !== void 0) try {
13790
13806
  fn.call(logger, `deepseek-coordination: ${message}: ${error instanceof Error ? error.message : String(error)}`);
@@ -13801,7 +13817,7 @@ function contentText(value) {
13801
13817
  parts.push(entry);
13802
13818
  continue;
13803
13819
  }
13804
- const text = asString(asRecord$1(entry)?.text);
13820
+ const text = asString$1(asRecord$2(entry)?.text);
13805
13821
  if (text.length > 0) parts.push(text);
13806
13822
  }
13807
13823
  return parts.join("\n");
@@ -13814,7 +13830,7 @@ function contentText(value) {
13814
13830
  function promptTextOfMessages(messages) {
13815
13831
  if (!Array.isArray(messages)) return void 0;
13816
13832
  for (let index = messages.length - 1; index >= 0; index -= 1) {
13817
- const message = asRecord$1(messages[index]);
13833
+ const message = asRecord$2(messages[index]);
13818
13834
  if (message?.role !== "user") continue;
13819
13835
  const text = contentText(message.content).trim();
13820
13836
  if (text.length > 0) return text;
@@ -13832,12 +13848,12 @@ const TurnHookBudgetMs = 5e3;
13832
13848
  * here at all.
13833
13849
  */
13834
13850
  function installTurnLifecycle(context, lifecycle) {
13835
- const on = asFunction(asRecord$1(context)?.on);
13851
+ const on = asFunction(asRecord$2(context)?.on);
13836
13852
  if (on === void 0) return () => {};
13837
13853
  const disposers = [];
13838
13854
  const preStep = on.call(context, "agent/pre-step", async (payload, next) => {
13839
- const record = asRecord$1(payload);
13840
- const agentId = asString(asRecord$1(record?.agent)?.id);
13855
+ const record = asRecord$2(payload);
13856
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13841
13857
  if (agentId.length > 0 && typeof record?.turn === "number") {
13842
13858
  const prompt = promptTextOfMessages(record?.messages);
13843
13859
  const observation = {
@@ -13864,8 +13880,8 @@ function installTurnLifecycle(context, lifecycle) {
13864
13880
  });
13865
13881
  if (typeof preStep === "function") disposers.push(preStep);
13866
13882
  const stopping = on.call(context, "agent/turn-stopping", async (payload) => {
13867
- const record = asRecord$1(payload);
13868
- const agentId = asString(asRecord$1(record?.agent)?.id);
13883
+ const record = asRecord$2(payload);
13884
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13869
13885
  try {
13870
13886
  if (agentId.length > 0 && typeof record?.turn === "number") await lifecycle.end({
13871
13887
  session: agentId,
@@ -13878,8 +13894,8 @@ function installTurnLifecycle(context, lifecycle) {
13878
13894
  if (typeof stopping === "function") disposers.push(stopping);
13879
13895
  if (lifecycle.fail !== void 0) {
13880
13896
  const failed = on.call(context, "agent/error", (payload) => {
13881
- const record = asRecord$1(payload);
13882
- const agentId = asString(asRecord$1(record?.agent)?.id);
13897
+ const record = asRecord$2(payload);
13898
+ const agentId = asString$1(asRecord$2(record?.agent)?.id);
13883
13899
  if (agentId.length === 0 || typeof record?.turn !== "number") return;
13884
13900
  const turn = record.turn;
13885
13901
  const failure = record.error;
@@ -13900,6 +13916,463 @@ function installTurnLifecycle(context, lifecycle) {
13900
13916
  };
13901
13917
  }
13902
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
13903
14376
  //#region src/shared/i18n.ts
13904
14377
  /**
13905
14378
  * The plugin's single source of user-visible and model-facing copy (v24 §22,
@@ -14534,14 +15007,6 @@ const ZH = {
14534
15007
  label: "工具结果字节上限",
14535
15008
  hint: "工具返回给模型的字节上限;0 表示不限制。"
14536
15009
  },
14537
- "scene.thinking": {
14538
- label: "思考模式",
14539
- hint: "是否把推理过程摊开;对话和办公默认关闭。"
14540
- },
14541
- "scene.reasoningEffort": {
14542
- label: "推理强度",
14543
- hint: "原样传给宿主模型的强度名,例如 high;本插件不改写。"
14544
- },
14545
15010
  "scene.requirePlan": {
14546
15011
  label: "必须出计划",
14547
15012
  hint: "这一情景是否必须先有可评分的计划。"
@@ -14603,10 +15068,6 @@ const ZH = {
14603
15068
  compact: "压缩",
14604
15069
  permission: "权限"
14605
15070
  },
14606
- thinkingOptions: {
14607
- enabled: "开启",
14608
- disabled: "关闭"
14609
- },
14610
15071
  defaultValue: "默认",
14611
15072
  scene: "情景",
14612
15073
  allScenes: "所有情景",
@@ -15328,14 +15789,6 @@ const COPY_BY_LOCALE = {
15328
15789
  label: "Tool result bytes",
15329
15790
  hint: "Byte cap on a tool result returned to the model; 0 means no cap."
15330
15791
  },
15331
- "scene.thinking": {
15332
- label: "Thinking mode",
15333
- hint: "Whether reasoning is shown; off by default for dialogue and office."
15334
- },
15335
- "scene.reasoningEffort": {
15336
- label: "Reasoning effort",
15337
- hint: "Effort name passed to the host model unchanged, e.g. high."
15338
- },
15339
15792
  "scene.requirePlan": {
15340
15793
  label: "Plan required",
15341
15794
  hint: "Whether this scene must produce a scoreable plan first."
@@ -15397,10 +15850,6 @@ const COPY_BY_LOCALE = {
15397
15850
  compact: "Compact",
15398
15851
  permission: "Permission"
15399
15852
  },
15400
- thinkingOptions: {
15401
- enabled: "On",
15402
- disabled: "Off"
15403
- },
15404
15853
  defaultValue: "Default",
15405
15854
  scene: "Scene",
15406
15855
  allScenes: "All scenes",
@@ -27002,27 +27451,307 @@ function sessionOf(url, body) {
27002
27451
  return (fromQuery ?? fromBody ?? "").trim();
27003
27452
  }
27004
27453
  /**
27005
- * Best-effort persistence of an already-applied settings patch.
27454
+ * Nest a flat dotted override map, then merge it over the deployment config.
27006
27455
  *
27007
- * DSH owns persistence; the plugin never writes a second config file. When the
27008
- * host exposes `ctx.settings.update`, the patch is forwarded under the plugin
27009
- * name. A missing port or a refusal leaves the live config (already applied) in
27010
- * force and reports `persisted: false`, so the page can say "applies to this
27011
- * 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).
27012
27459
  */
27013
- async function persistConfig(context, patch) {
27014
- const settings = context.settings;
27015
- 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;
27016
27477
  try {
27017
- await settings.update(name, { ...patch });
27018
- return true;
27478
+ const value = getter.call(context, name);
27479
+ return typeof value === "object" && value !== null ? value : void 0;
27019
27480
  } catch {
27020
- 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;
27021
27527
  }
27022
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
+ }
27023
27739
  function apply(context, configInput) {
27024
- 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));
27025
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
+ }
27026
27755
  /** Re-register the tools after `POST /locale` changed the copy language. */
27027
27756
  let refreshTools;
27028
27757
  context.effect(() => () => {
@@ -27080,7 +27809,9 @@ function apply(context, configInput) {
27080
27809
  };
27081
27810
  }, `${name}: tools`);
27082
27811
  context.effect(() => {
27083
- const dispose = context.webServer.register({
27812
+ const webServer = webServerOf(context);
27813
+ if (webServer === void 0) return () => {};
27814
+ const dispose = webServer.register({
27084
27815
  kind: "prefix",
27085
27816
  path: ROUTE_PREFIX,
27086
27817
  handler: async (request, response) => {
@@ -27099,6 +27830,13 @@ function apply(context, configInput) {
27099
27830
  });
27100
27831
  return;
27101
27832
  }
27833
+ if (path === "/proxy/status" && method === "GET") {
27834
+ json(response, {
27835
+ ok: true,
27836
+ value: cacheProxyStatus()
27837
+ });
27838
+ return;
27839
+ }
27102
27840
  if (path === "/echo" && method === "POST") {
27103
27841
  json(response, {
27104
27842
  ok: true,
@@ -27316,7 +28054,7 @@ function apply(context, configInput) {
27316
28054
  if (outcome.compaction !== null) service.applyCompactionPolicy(outcome.compaction);
27317
28055
  const appliedPatch = {};
27318
28056
  for (const key of outcome.applied) appliedPatch[key] = patch[key];
27319
- const persisted = outcome.applied.length > 0 ? await persistConfig(context, appliedPatch) : false;
28057
+ const persisted = outcome.applied.length > 0 ? writeOverrides(settingsFile, appliedPatch) : false;
27320
28058
  json(response, {
27321
28059
  ok: true,
27322
28060
  value: {
@@ -27371,4 +28109,4 @@ function apply(context, configInput) {
27371
28109
  }, `${name}: ${ROUTE_PREFIX} routes`);
27372
28110
  }
27373
28111
  //#endregion
27374
- export { Config, apply, inject, name };
28112
+ export { Config, apply, cacheProxyStatus, inject, name };