stratagate-dsh 0.2.82 → 0.2.84

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;
@@ -5727,12 +5755,26 @@ var STRUCTURED_FIELDS = {
5727
5755
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
5728
5756
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
5729
5757
  var SUMMARIZER_PARAMETERS = {
5730
- l0Title: { type: "string", required: true },
5731
- l0Tags: { ...STRING_ARRAY, required: true },
5732
- l1Summary: { type: "string", required: true },
5733
- l2Keypoints: { ...STRING_ARRAY, required: true },
5734
- shouldExtract: { type: "boolean", required: true }
5758
+ l0Title: { type: "string", description: "L0: a short phrase naming the Block's central subject for quick recognition; do not turn it into a sentence-length summary.", required: true },
5759
+ l0Tags: { ...STRING_ARRAY, description: "L0: a few distinctive topical labels (usually 2-5) for rapid recognition of the Block, such as named people, projects, organizations, tools, technical topics, or specific task concepts; omit generic labels such as discussion, conversation, problem, or task.", required: true },
5760
+ l1Summary: { type: "string", description: "L1: a compact, self-contained overview of what happened across the Block, its conclusions or results, significant state changes, and key unresolved work; richer than L0 but still quick to read.", required: true },
5761
+ l2Keypoints: { ...STRING_ARRAY, description: "L2: specific, self-contained points needed to recover context later. Make each point mostly atomic: one decision, constraint, preference, fact, result, failure reason, or open item. Add detail beyond L1 without splitting or repeating its sentences.", required: true },
5762
+ shouldExtract: { type: "boolean", description: "High-recall pre-screen for Event extraction, not the final Event decision. True when this Block clearly contains or may reasonably contain a long-term Event; when uncertain, use true so the Event Extractor can decide. False only when it clearly lacks lasting value, such as greetings, repeated confirmations, or disposable process noise. Mere presence of a fact is not sufficient.", required: true }
5735
5763
  };
5764
+ var SUMMARIZER_SYSTEM_PROMPT = `You are the Block Summarizer in StrataGate's memory pipeline. Compress one sealed conversation Block, typically about six turns, from the supplied provenance-preserving derivation messages into L0-L2 layered memory and decide whether it warrants later Event extraction. You are a background model, not the main Agent; you have no other conversation context. L5 retains the complete original source, while L3/L4 are produced separately. Your L0-L2 may later replace detailed chat in the Agent's context, so preserve what future work needs without inventing missing context.
5765
+
5766
+ L0 -> L1 -> L2 are progressively higher-resolution views of the same history, not three independent or repetitive summaries:
5767
+ - L0: l0Title is a short subject phrase for rapid recognition, ideally under about 60 characters; l0Tags are a few distinctive topical labels for rapid recognition, usually 2-5. Prefer named people, projects, organizations, tools, technical topics, and specific task concepts. Avoid generic tags such as discussion, conversation, problem, or task.
5768
+ - L1: l1Summary is a compact, self-contained overview of the whole Block: what happened, conclusions or results, meaningful state changes, and key unresolved work. Usually 1-3 sentences; do not retell the conversation turn by turn.
5769
+ - L2: l2Keypoints retain concrete details needed to resume work. Usually 3-8 concise points when there is enough substance, fewer for a thin Block. Each point should stand alone and express mainly one decision, constraint, preference, fact, result, significant failure reason, or open item. Be more specific than L1; do not merely split L1 into repeated sentences.
5770
+
5771
+ Prioritize important decisions, constraints, user preferences, final outcomes, significant failure causes, and unresolved work; then task-relevant process details. Omit repetition, greetings, and disposable execution noise. Do not discard a constraint or conclusion that would change how later work is understood just to shorten the output.
5772
+
5773
+ Keep provenance and uncertainty. Distinguish what the user stated or decided, what the assistant only proposed or suspected, and what a tool actually observed. Do not promote an assistant hypothesis or a recollection of older memory into a verified fact or new outcome. Preserve uncertainty about timing, causes, status, results, and relationships. Ordinary user/assistant text is retained, but large tool arguments or results may be compacted; use only retained tool names, evidence summaries, and excerpts, and never guess omitted payload details.
5774
+
5775
+ shouldExtract is a high-recall pre-screen, not the final Event decision. If it is false, Event extraction is skipped entirely; if true, the Event Extractor makes the final evidence-based decision. Set true when this Block clearly contains or may reasonably contain a long-term Event, such as a decision, stable preference, material project change, meaningful task result, important failure and possible cause, future-useful fact, or open item worth tracking. When uncertain whether a plausible candidate has lasting value, choose true and let the Event Extractor decide. Set false only when the Block clearly lacks such value, for example greetings, repeated confirmations, or disposable execution noise. Do not set true merely because some fact appears; an unsupported assistant suggestion or recap of older memory alone does not establish a new Event candidate.
5776
+
5777
+ Call stratagate_summarize_block exactly once with l0Title, l0Tags, l1Summary, l2Keypoints, and shouldExtract. Do not return the summary as ordinary text.`;
5736
5778
  var EVENT_ITEM = {
5737
5779
  type: "object",
5738
5780
  additionalProperties: false,
@@ -5862,7 +5904,7 @@ var PROFILE_MAINTENANCE_PARAMETERS = Object.fromEntries(
5862
5904
  var STRUCTURED_TOOLS = {
5863
5905
  summarizer: {
5864
5906
  name: "stratagate_summarize_block",
5865
- description: "Submit the completed durable summary for the supplied conversation block.",
5907
+ description: "Submit the L0-L2 layered compression of one sealed StrataGate conversation Block and decide whether it should proceed to Event extraction.",
5866
5908
  parameters: SUMMARIZER_PARAMETERS
5867
5909
  },
5868
5910
  extractor: {
@@ -5892,7 +5934,7 @@ var STRUCTURED_TOOLS = {
5892
5934
  },
5893
5935
  profileMaintenance: {
5894
5936
  name: "stratagate_maintain_profile",
5895
- description: "Return the same eight Persistent Profile fields with only safe wording and redundancy cleanup.",
5937
+ 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
5938
  parameters: PROFILE_MAINTENANCE_PARAMETERS
5897
5939
  }
5898
5940
  };
@@ -6001,7 +6043,7 @@ var DshModelBridge = class {
6001
6043
  summarizer = async (messages) => {
6002
6044
  const raw = object(await this.callStructured(
6003
6045
  "summarizer",
6004
- `You compress agent conversations into durable memory blocks. Read the supplied provenance-preserving derivation messages and call ${STRUCTURED_TOOLS.summarizer.name} exactly once with l0Title, l0Tags, l1Summary, l2Keypoints, and shouldExtract. Preserve decisions, constraints, preferences, outcomes, and unresolved work. Tool code and oversized tool payloads may be marked compacted; use the retained tool names, evidence summaries, and excerpts without inventing omitted details. shouldExtract is true only when durable events or facts exist. Do not return the summary as text.`,
6046
+ SUMMARIZER_SYSTEM_PROMPT,
6005
6047
  { messages: buildMemoryDerivationMessages(messages) }
6006
6048
  ));
6007
6049
  return {
@@ -6189,7 +6231,7 @@ var DshModelBridge = class {
6189
6231
  async maintainProfile(profile) {
6190
6232
  const raw = object(await this.callStructured(
6191
6233
  "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.`,
6234
+ `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
6235
  { profile, fieldDefinitions: PROFILE_FIELDS }
6194
6236
  ));
6195
6237
  if (Object.keys(raw).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(raw).some((field) => !(field in PROFILE_FIELDS))) {
@@ -6197,7 +6239,7 @@ var DshModelBridge = class {
6197
6239
  }
6198
6240
  const proposed = raw;
6199
6241
  validateProfile(proposed);
6200
- for (const field of ["userPreferredName", "assistantPreferredName", "preferredLanguage"]) {
6242
+ for (const field of PROFILE_PROTECTED_SHORT_FIELDS) {
6201
6243
  if (proposed[field].trim() !== profile[field].trim()) throw new Error(`Profile maintenance changed protected short field ${field}`);
6202
6244
  }
6203
6245
  return proposed;
@@ -6985,14 +7027,17 @@ var StrataGateRuntime = class {
6985
7027
  getPersistentProfile() {
6986
7028
  return this.profileStore().getPersistentProfile();
6987
7029
  }
7030
+ getProfileSnapshot() {
7031
+ return this.profileStore().getProfileSnapshot();
7032
+ }
6988
7033
  renderProfileContext() {
6989
7034
  return renderPersistentProfile(this.getPersistentProfile());
6990
7035
  }
6991
7036
  getProfileChanges() {
6992
7037
  return this.profileStore().getProfileChanges();
6993
7038
  }
6994
- updatePersistentProfile(field, value, source, sourceMessageId) {
6995
- const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId);
7039
+ updatePersistentProfile(field, value, source, sourceMessageId, expectedValue, expectedRevision) {
7040
+ const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId, expectedValue, expectedRevision);
6996
7041
  if (result.modified) this.wakeBackgroundWorker();
6997
7042
  return result;
6998
7043
  }
@@ -8811,7 +8856,14 @@ function sessionOf(exec) {
8811
8856
  function registerMemoryTools(ctx, runtime) {
8812
8857
  ctx.tools.register(defineTool({
8813
8858
  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.
8859
+ 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.
8860
+
8861
+ 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.
8862
+
8863
+ Examples:
8864
+ 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.
8865
+ 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.
8866
+ 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
8867
 
8816
8868
  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
8869
 
@@ -8821,7 +8873,7 @@ If the assistant only infers that something might be a useful persistent prefere
8821
8873
 
8822
8874
  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
8875
  parameters: {
8824
- field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8876
+ field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8825
8877
  value: { type: "string", required: true }
8826
8878
  },
8827
8879
  output: jsonOutput,
@@ -9579,7 +9631,10 @@ async function updateSettings(runtime, url) {
9579
9631
  return result;
9580
9632
  }
9581
9633
  async function persistentProfile(runtime, req) {
9582
- if (req.method === "GET") return runtime.getPersistentProfile();
9634
+ if (req.method === "GET") {
9635
+ const snapshot = runtime.getProfileSnapshot();
9636
+ return { ...snapshot.profile, _revisions: snapshot.revisions };
9637
+ }
9583
9638
  if (req.method !== "PATCH") throw new AdminHttpError(405, "Persistent Profile requires GET or PATCH");
9584
9639
  let suppliedBody = req.body;
9585
9640
  if (suppliedBody === void 0 && typeof req[Symbol.asyncIterator] === "function") {
@@ -9603,11 +9658,14 @@ async function persistentProfile(runtime, req) {
9603
9658
  }
9604
9659
  if (!body || typeof body !== "object" || Array.isArray(body)) throw new AdminHttpError(400, "Profile update must be an object");
9605
9660
  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");
9661
+ 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) {
9662
+ throw new AdminHttpError(400, "Profile update requires field, value, expectedValue strings and expectedRevision number");
9608
9663
  }
9609
9664
  try {
9610
- return runtime.updatePersistentProfile(input.field, input.value, "settings");
9665
+ const result = runtime.updatePersistentProfile(input.field, input.value, "settings", null, input.expectedValue, input.expectedRevision);
9666
+ if (result.conflict) throw new AdminHttpError(409, "\u8BE5\u9879\u521A\u521A\u5728\u5176\u4ED6\u4F4D\u7F6E\u66F4\u65B0");
9667
+ const snapshot = runtime.getProfileSnapshot();
9668
+ return { ...result, snapshot: { ...snapshot.profile, _revisions: snapshot.revisions } };
9611
9669
  } catch (error) {
9612
9670
  if (error instanceof TypeError || error instanceof RangeError) throw new AdminHttpError(400, error.message);
9613
9671
  throw error;