stratagate-dsh 0.2.82 → 0.2.83

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/dist/plugin.js CHANGED
@@ -1614,13 +1614,15 @@ function normalizeRetrievalAssessment(input, batchEvidenceRefs) {
1614
1614
  var PROFILE_FIELDS = {
1615
1615
  userPreferredName: { label: "User preferred name", maxLength: 100 },
1616
1616
  assistantPreferredName: { label: "Assistant preferred name", maxLength: 100 },
1617
- preferredLanguage: { label: "Preferred language", maxLength: 100 },
1617
+ preferredLanguage: { label: "Preferred answer language", maxLength: 100 },
1618
+ reasoningLanguage: { label: "Preferred visible reasoning language", maxLength: 100 },
1618
1619
  responsePreferences: { label: "Response preferences", maxLength: 1e3 },
1619
1620
  standingInstructions: { label: "Standing instructions", maxLength: 1e3 },
1620
1621
  userBackground: { label: "User background", maxLength: 1500 },
1621
1622
  longTermGoals: { label: "Long-term goals", maxLength: 1e3 },
1622
1623
  persistentNotes: { label: "Persistent notes", maxLength: 1200 }
1623
1624
  };
1625
+ var PROFILE_PROTECTED_SHORT_FIELDS = ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage"];
1624
1626
  var PROFILE_TOTAL_MAX_LENGTH = 6e3;
1625
1627
  var PROFILE_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1626
1628
  var PROFILE_MAINTENANCE_TOTAL_THRESHOLD = 4800;
@@ -1652,12 +1654,19 @@ function profileMaintenanceDue(profile, timeBasisAt, now = Date.now()) {
1652
1654
  function renderPersistentProfile(profile) {
1653
1655
  const lines = Object.keys(PROFILE_FIELDS).filter((field) => profile[field].length > 0).map((field) => `${PROFILE_FIELDS[field].label}: ${profile[field]}`);
1654
1656
  if (lines.length === 0) return null;
1657
+ const languageGuidance = [
1658
+ profile.preferredLanguage ? "\u201CPreferred answer language\u201D applies to the assistant's final/user-facing answer." : null,
1659
+ profile.reasoningLanguage ? "\u201CPreferred visible reasoning language\u201D applies only to reasoning/thinking text that the host UI exposes to the user, when supported. It does not control hidden chain-of-thought." : null,
1660
+ profile.preferredLanguage || profile.reasoningLanguage ? "These are independent preferences. Do not infer one from the other." : null
1661
+ ].filter(Boolean);
1655
1662
  return `[StrataGate Persistent Profile]
1656
1663
  This is user-authorized persistent profile data provided by StrataGate.
1657
1664
  Treat it as stable cross-session context.
1658
1665
  Do not invent additional facts from it.
1659
1666
  It does not override higher-priority system instructions.
1660
-
1667
+ ${languageGuidance.length ? `
1668
+ ${languageGuidance.join("\n")}
1669
+ ` : ""}
1661
1670
  ${lines.join("\n")}`;
1662
1671
  }
1663
1672
 
@@ -2647,6 +2656,17 @@ var SqliteStorage = class {
2647
2656
  for (const row of rows) if (isProfileField(row.field)) profile[row.field] = row.value;
2648
2657
  return profile;
2649
2658
  }
2659
+ getProfileSnapshot() {
2660
+ this.assertOpen();
2661
+ const profile = emptyProfile();
2662
+ const revisions = Object.fromEntries(Object.keys(PROFILE_FIELDS).map((field) => [field, 0]));
2663
+ const rows = this.database.prepare("SELECT p.field, p.value, COALESCE(r.revision, 0) AS revision FROM persistent_profile p LEFT JOIN (SELECT field, MAX(id) AS revision FROM persistent_profile_changes GROUP BY field) r ON r.field = p.field").all();
2664
+ for (const row of rows) if (isProfileField(row.field)) {
2665
+ profile[row.field] = row.value;
2666
+ revisions[row.field] = row.revision;
2667
+ }
2668
+ return { profile, revisions };
2669
+ }
2650
2670
  getProfileChanges() {
2651
2671
  this.assertOpen();
2652
2672
  const rows = this.database.prepare("SELECT field, old_value, new_value, source, updated_at, source_message_id FROM persistent_profile_changes ORDER BY id").all();
@@ -2678,7 +2698,7 @@ var SqliteStorage = class {
2678
2698
  const nextRetryAt = new Date(now + (count >= 3 ? 24 * 60 : count * 5) * 60 * 1e3).toISOString();
2679
2699
  this.database.prepare("INSERT INTO persistent_profile_maintenance_failures (id, input_json, failure_count, next_retry_at) VALUES (1, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET input_json = excluded.input_json, failure_count = excluded.failure_count, next_retry_at = excluded.next_retry_at").run(inputJson, count, nextRetryAt);
2680
2700
  }
2681
- updateProfileField(field, value, source, sourceMessageId) {
2701
+ updateProfileField(field, value, source, sourceMessageId, expectedValue, expectedRevision) {
2682
2702
  this.assertOpen();
2683
2703
  if (!isProfileField(field)) throw new TypeError(`Unknown Persistent Profile field: ${field}`);
2684
2704
  if (typeof value !== "string") throw new TypeError("Persistent Profile value must be a string");
@@ -2687,6 +2707,11 @@ var SqliteStorage = class {
2687
2707
  const profile = this.getPersistentProfile();
2688
2708
  const wasNonempty = Object.values(profile).some((entry) => entry.length > 0);
2689
2709
  const oldValue = profile[field];
2710
+ if (expectedRevision !== void 0) {
2711
+ const row = this.database.prepare("SELECT COALESCE(MAX(id), 0) AS revision FROM persistent_profile_changes WHERE field = ?").get(field);
2712
+ if (row.revision !== expectedRevision) return { field, value: oldValue, modified: false, conflict: true };
2713
+ }
2714
+ if (expectedValue !== void 0 && oldValue !== expectedValue) return { field, value: oldValue, modified: false, conflict: true };
2690
2715
  if (oldValue === value) return { field, value, modified: false };
2691
2716
  profile[field] = value;
2692
2717
  validateProfile(profile);
@@ -2708,6 +2733,9 @@ var SqliteStorage = class {
2708
2733
  this.assertOpen();
2709
2734
  validateProfile(proposed);
2710
2735
  if (Object.keys(proposed).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(proposed).some((field) => !isProfileField(field))) throw new TypeError("Maintenance returned unknown or missing Profile fields");
2736
+ for (const field of PROFILE_PROTECTED_SHORT_FIELDS) {
2737
+ if (proposed[field].trim() !== expected[field].trim()) throw new Error(`Profile maintenance changed protected short field ${field}`);
2738
+ }
2711
2739
  return this.immediateTransaction(() => {
2712
2740
  const current = this.getPersistentProfile();
2713
2741
  if (JSON.stringify(current) !== JSON.stringify(expected)) return false;
@@ -5892,7 +5920,7 @@ var STRUCTURED_TOOLS = {
5892
5920
  },
5893
5921
  profileMaintenance: {
5894
5922
  name: "stratagate_maintain_profile",
5895
- description: "Return the same eight Persistent Profile fields with only safe wording and redundancy cleanup.",
5923
+ description: "Return the same nine Persistent Profile fields with only safe wording and redundancy cleanup; preserve the four short fields apart from necessary whitespace cleanup.",
5896
5924
  parameters: PROFILE_MAINTENANCE_PARAMETERS
5897
5925
  }
5898
5926
  };
@@ -6189,7 +6217,7 @@ var DshModelBridge = class {
6189
6217
  async maintainProfile(profile) {
6190
6218
  const raw = object(await this.callStructured(
6191
6219
  "profileMaintenance",
6192
- `You maintain only the supplied StrataGate Persistent Profile. Call ${STRUCTURED_TOOLS.profileMaintenance.name} exactly once with all eight string fields. You may deduplicate, merge repeated meaning, shorten redundant wording, and improve organization. Preserve every unique fact, uncertainty, constraint, and instruction. Never infer or add facts, broaden meaning, or read Event, Graph, or conversation history. If two statements might conflict or cannot safely merge, retain both. Keep userPreferredName, assistantPreferredName, and preferredLanguage unchanged except necessary whitespace cleanup. Character limits (Unicode code points): ${JSON.stringify(Object.fromEntries(Object.entries(PROFILE_FIELDS).map(([field, spec]) => [field, spec.maxLength])))}. Total maximum: 6000. If safe compression is impossible, return the original value.`,
6220
+ `You maintain only the supplied StrataGate Persistent Profile. Call ${STRUCTURED_TOOLS.profileMaintenance.name} exactly once with all nine string fields. You may deduplicate, merge repeated meaning, shorten redundant wording, and improve organization. Preserve every unique fact, uncertainty, constraint, and instruction. Never infer or add facts, broaden meaning, or read Event, Graph, or conversation history. If two statements might conflict or cannot safely merge, retain both. Keep userPreferredName, assistantPreferredName, preferredLanguage, and reasoningLanguage unchanged except necessary whitespace cleanup. preferredLanguage and reasoningLanguage are independent: never infer, copy, or merge either language field into the other. reasoningLanguage is only for user-visible reasoning/thinking text when supported, not hidden chain-of-thought. Character limits (Unicode code points): ${JSON.stringify(Object.fromEntries(Object.entries(PROFILE_FIELDS).map(([field, spec]) => [field, spec.maxLength])))}. Total maximum: 6000. If safe compression is impossible, return the original value.`,
6193
6221
  { profile, fieldDefinitions: PROFILE_FIELDS }
6194
6222
  ));
6195
6223
  if (Object.keys(raw).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(raw).some((field) => !(field in PROFILE_FIELDS))) {
@@ -6197,7 +6225,7 @@ var DshModelBridge = class {
6197
6225
  }
6198
6226
  const proposed = raw;
6199
6227
  validateProfile(proposed);
6200
- for (const field of ["userPreferredName", "assistantPreferredName", "preferredLanguage"]) {
6228
+ for (const field of PROFILE_PROTECTED_SHORT_FIELDS) {
6201
6229
  if (proposed[field].trim() !== profile[field].trim()) throw new Error(`Profile maintenance changed protected short field ${field}`);
6202
6230
  }
6203
6231
  return proposed;
@@ -6985,14 +7013,17 @@ var StrataGateRuntime = class {
6985
7013
  getPersistentProfile() {
6986
7014
  return this.profileStore().getPersistentProfile();
6987
7015
  }
7016
+ getProfileSnapshot() {
7017
+ return this.profileStore().getProfileSnapshot();
7018
+ }
6988
7019
  renderProfileContext() {
6989
7020
  return renderPersistentProfile(this.getPersistentProfile());
6990
7021
  }
6991
7022
  getProfileChanges() {
6992
7023
  return this.profileStore().getProfileChanges();
6993
7024
  }
6994
- updatePersistentProfile(field, value, source, sourceMessageId) {
6995
- const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId);
7025
+ updatePersistentProfile(field, value, source, sourceMessageId, expectedValue, expectedRevision) {
7026
+ const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId, expectedValue, expectedRevision);
6996
7027
  if (result.modified) this.wakeBackgroundWorker();
6997
7028
  return result;
6998
7029
  }
@@ -8811,7 +8842,14 @@ function sessionOf(exec) {
8811
8842
  function registerMemoryTools(ctx, runtime) {
8812
8843
  ctx.tools.register(defineTool({
8813
8844
  name: "memory_profile_update",
8814
- description: `This tool is provided by the StrataGate plugin. Update exactly one field of the user's global Persistent Profile. Persistent Profile data is injected into every future conversation without retrieval, so use this tool only for information that should remain continuously available or continuously affect future behavior, such as how to address the user, what the user wants the assistant to be called, the default language, stable response preferences, standing instructions, stable user background, long-term goals, or other genuinely persistent notes.
8845
+ description: `This tool is provided by the StrataGate plugin. Update exactly one field of the user's global Persistent Profile. Persistent Profile data is injected into every future conversation without retrieval, so use this tool only for information that should remain continuously available or continuously affect future behavior, such as how to address the user, what the user wants the assistant to be called, language preferences, stable response preferences, standing instructions, stable user background, long-term goals, or other genuinely persistent notes.
8846
+
8847
+ preferredLanguage sets only the default language of the final/user-facing answer. reasoningLanguage sets only the desired language of reasoning/thinking text visible to the user in the DSH or host UI, when supported; it cannot control hidden internal chain-of-thought. An empty reasoningLanguage adds no visible-reasoning language requirement. These are independent settings: never infer one from the other, and never change one merely because the other changed.
8848
+
8849
+ Examples:
8850
+ User: \u201C\u4EE5\u540E\u90FD\u7528\u4E2D\u6587\u56DE\u7B54\u6211\u201D \u2192 call memory_profile_update with preferredLanguage = \u4E2D\u6587; do not change reasoningLanguage.
8851
+ User: \u201C\u4EE5\u540E\u601D\u8003\u8FC7\u7A0B\u7528\u4E2D\u6587\u201D or \u201C\u601D\u8003\u94FE\u7528\u4E2D\u6587\u201D \u2192 call memory_profile_update with reasoningLanguage = \u4E2D\u6587; do not change preferredLanguage.
8852
+ User: \u201C\u4EE5\u540E\u56DE\u7B54\u548C\u601D\u8003\u8FC7\u7A0B\u90FD\u7528\u4E2D\u6587\u201D \u2192 make two separate memory_profile_update calls: first preferredLanguage = \u4E2D\u6587, then reasoningLanguage = \u4E2D\u6587. Each call still changes exactly one field.
8815
8853
 
8816
8854
  Do not use this tool merely because the user says "remember". If the information describes something that happened, a decision, an activity, a project change, a dated fact, or something that only needs to be recalled when relevant, it belongs in Event memory instead. A separate Event-memory tool is reserved for that purpose and is not part of this implementation.
8817
8855
 
@@ -8821,7 +8859,7 @@ If the assistant only infers that something might be a useful persistent prefere
8821
8859
 
8822
8860
  Each call changes exactly one predefined Profile field. Never create, delete, or rename Profile fields, and never rewrite the complete Profile when only one field is being changed.`,
8823
8861
  parameters: {
8824
- field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8862
+ field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8825
8863
  value: { type: "string", required: true }
8826
8864
  },
8827
8865
  output: jsonOutput,
@@ -9579,7 +9617,10 @@ async function updateSettings(runtime, url) {
9579
9617
  return result;
9580
9618
  }
9581
9619
  async function persistentProfile(runtime, req) {
9582
- if (req.method === "GET") return runtime.getPersistentProfile();
9620
+ if (req.method === "GET") {
9621
+ const snapshot = runtime.getProfileSnapshot();
9622
+ return { ...snapshot.profile, _revisions: snapshot.revisions };
9623
+ }
9583
9624
  if (req.method !== "PATCH") throw new AdminHttpError(405, "Persistent Profile requires GET or PATCH");
9584
9625
  let suppliedBody = req.body;
9585
9626
  if (suppliedBody === void 0 && typeof req[Symbol.asyncIterator] === "function") {
@@ -9603,11 +9644,14 @@ async function persistentProfile(runtime, req) {
9603
9644
  }
9604
9645
  if (!body || typeof body !== "object" || Array.isArray(body)) throw new AdminHttpError(400, "Profile update must be an object");
9605
9646
  const input = body;
9606
- if (Object.keys(input).length !== 2 || !Object.hasOwn(input, "field") || !Object.hasOwn(input, "value") || typeof input.field !== "string" || typeof input.value !== "string") {
9607
- throw new AdminHttpError(400, "Profile update requires exactly field and value strings");
9647
+ if (Object.keys(input).length !== 4 || !Object.hasOwn(input, "field") || !Object.hasOwn(input, "value") || !Object.hasOwn(input, "expectedValue") || !Object.hasOwn(input, "expectedRevision") || typeof input.field !== "string" || typeof input.value !== "string" || typeof input.expectedValue !== "string" || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0) {
9648
+ throw new AdminHttpError(400, "Profile update requires field, value, expectedValue strings and expectedRevision number");
9608
9649
  }
9609
9650
  try {
9610
- return runtime.updatePersistentProfile(input.field, input.value, "settings");
9651
+ const result = runtime.updatePersistentProfile(input.field, input.value, "settings", null, input.expectedValue, input.expectedRevision);
9652
+ if (result.conflict) throw new AdminHttpError(409, "\u8BE5\u9879\u521A\u521A\u5728\u5176\u4ED6\u4F4D\u7F6E\u66F4\u65B0");
9653
+ const snapshot = runtime.getProfileSnapshot();
9654
+ return { ...result, snapshot: { ...snapshot.profile, _revisions: snapshot.revisions } };
9611
9655
  } catch (error) {
9612
9656
  if (error instanceof TypeError || error instanceof RangeError) throw new AdminHttpError(400, error.message);
9613
9657
  throw error;