stratagate-dsh 0.2.81 → 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
@@ -1610,8 +1610,68 @@ function normalizeRetrievalAssessment(input, batchEvidenceRefs) {
1610
1610
  };
1611
1611
  }
1612
1612
 
1613
+ // packages/core/src/profile.ts
1614
+ var PROFILE_FIELDS = {
1615
+ userPreferredName: { label: "User preferred name", maxLength: 100 },
1616
+ assistantPreferredName: { label: "Assistant preferred name", maxLength: 100 },
1617
+ preferredLanguage: { label: "Preferred answer language", maxLength: 100 },
1618
+ reasoningLanguage: { label: "Preferred visible reasoning language", maxLength: 100 },
1619
+ responsePreferences: { label: "Response preferences", maxLength: 1e3 },
1620
+ standingInstructions: { label: "Standing instructions", maxLength: 1e3 },
1621
+ userBackground: { label: "User background", maxLength: 1500 },
1622
+ longTermGoals: { label: "Long-term goals", maxLength: 1e3 },
1623
+ persistentNotes: { label: "Persistent notes", maxLength: 1200 }
1624
+ };
1625
+ var PROFILE_PROTECTED_SHORT_FIELDS = ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage"];
1626
+ var PROFILE_TOTAL_MAX_LENGTH = 6e3;
1627
+ var PROFILE_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1628
+ var PROFILE_MAINTENANCE_TOTAL_THRESHOLD = 4800;
1629
+ function profileLength(value) {
1630
+ return Array.from(value).length;
1631
+ }
1632
+ function emptyProfile() {
1633
+ return Object.fromEntries(Object.keys(PROFILE_FIELDS).map((field) => [field, ""]));
1634
+ }
1635
+ function isProfileField(value) {
1636
+ return Object.prototype.hasOwnProperty.call(PROFILE_FIELDS, value);
1637
+ }
1638
+ function validateProfile(profile) {
1639
+ let total = 0;
1640
+ for (const field of Object.keys(PROFILE_FIELDS)) {
1641
+ if (typeof profile[field] !== "string") throw new TypeError(`${field} must be a string`);
1642
+ const length = profileLength(profile[field]);
1643
+ if (length > PROFILE_FIELDS[field].maxLength) throw new RangeError(`${field} exceeds ${PROFILE_FIELDS[field].maxLength} characters`);
1644
+ total += length;
1645
+ }
1646
+ if (total > PROFILE_TOTAL_MAX_LENGTH) throw new RangeError(`Persistent Profile exceeds ${PROFILE_TOTAL_MAX_LENGTH} characters`);
1647
+ }
1648
+ function profileMaintenanceDue(profile, timeBasisAt, now = Date.now()) {
1649
+ const lengths = Object.keys(PROFILE_FIELDS).map((field) => ({ field, length: profileLength(profile[field]) }));
1650
+ const total = lengths.reduce((sum, item) => sum + item.length, 0);
1651
+ if (total === 0) return false;
1652
+ return timeBasisAt !== null && now - Date.parse(timeBasisAt) >= PROFILE_MAINTENANCE_INTERVAL_MS || total >= PROFILE_MAINTENANCE_TOTAL_THRESHOLD || lengths.some(({ field, length }) => length >= PROFILE_FIELDS[field].maxLength * 0.8);
1653
+ }
1654
+ function renderPersistentProfile(profile) {
1655
+ const lines = Object.keys(PROFILE_FIELDS).filter((field) => profile[field].length > 0).map((field) => `${PROFILE_FIELDS[field].label}: ${profile[field]}`);
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);
1662
+ return `[StrataGate Persistent Profile]
1663
+ This is user-authorized persistent profile data provided by StrataGate.
1664
+ Treat it as stable cross-session context.
1665
+ Do not invent additional facts from it.
1666
+ It does not override higher-priority system instructions.
1667
+ ${languageGuidance.length ? `
1668
+ ${languageGuidance.join("\n")}
1669
+ ` : ""}
1670
+ ${lines.join("\n")}`;
1671
+ }
1672
+
1613
1673
  // packages/core/src/storage.ts
1614
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 11;
1674
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 12;
1615
1675
  var KNOWLEDGE_GRAPH_PROJECTOR_VERSION = 1;
1616
1676
  var DERIVATION_MAX_ATTEMPTS = 3;
1617
1677
  var StorageConflictError = class extends Error {
@@ -1751,6 +1811,8 @@ function normalizeSnapshot(value) {
1751
1811
  ...structuredClone(value),
1752
1812
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION
1753
1813
  };
1814
+ } else if (schemaVersion === 11) {
1815
+ snapshot = { ...structuredClone(value), schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION };
1754
1816
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
1755
1817
  snapshot = structuredClone(value);
1756
1818
  } else {
@@ -1965,6 +2027,38 @@ function nowUtc8() {
1965
2027
 
1966
2028
  // packages/core/src/sqlite.ts
1967
2029
  var SCHEMA = `
2030
+ CREATE TABLE IF NOT EXISTS persistent_profile (
2031
+ field TEXT PRIMARY KEY,
2032
+ value TEXT NOT NULL
2033
+ ) STRICT;
2034
+
2035
+ CREATE TABLE IF NOT EXISTS persistent_profile_changes (
2036
+ id INTEGER PRIMARY KEY,
2037
+ field TEXT NOT NULL,
2038
+ old_value TEXT NOT NULL,
2039
+ new_value TEXT NOT NULL,
2040
+ source TEXT NOT NULL,
2041
+ updated_at TEXT NOT NULL,
2042
+ source_message_id TEXT
2043
+ ) STRICT;
2044
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance (
2045
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2046
+ last_succeeded_at TEXT NOT NULL,
2047
+ input_json TEXT NOT NULL
2048
+ ) STRICT;
2049
+
2050
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance_baseline (
2051
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2052
+ started_at TEXT NOT NULL
2053
+ ) STRICT;
2054
+
2055
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance_failures (
2056
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2057
+ input_json TEXT NOT NULL,
2058
+ failure_count INTEGER NOT NULL,
2059
+ next_retry_at TEXT NOT NULL
2060
+ ) STRICT;
2061
+
1968
2062
  CREATE TABLE IF NOT EXISTS memory_spaces (
1969
2063
  namespace TEXT PRIMARY KEY,
1970
2064
  schema_version INTEGER NOT NULL,
@@ -2554,6 +2648,117 @@ var SqliteStorage = class {
2554
2648
  this.database.close();
2555
2649
  this.closed = true;
2556
2650
  }
2651
+ /** Installation-wide state; intentionally has no namespace column. */
2652
+ getPersistentProfile() {
2653
+ this.assertOpen();
2654
+ const profile = emptyProfile();
2655
+ const rows = this.database.prepare("SELECT field, value FROM persistent_profile").all();
2656
+ for (const row of rows) if (isProfileField(row.field)) profile[row.field] = row.value;
2657
+ return profile;
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
+ }
2670
+ getProfileChanges() {
2671
+ this.assertOpen();
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();
2673
+ return rows.map((row) => ({
2674
+ field: row.field,
2675
+ oldValue: row.old_value,
2676
+ newValue: row.new_value,
2677
+ source: row.source,
2678
+ updatedAt: row.updated_at,
2679
+ sourceMessageId: row.source_message_id
2680
+ }));
2681
+ }
2682
+ getProfileMaintenanceStartedAt() {
2683
+ this.assertOpen();
2684
+ const row = this.database.prepare("SELECT started_at FROM persistent_profile_maintenance_baseline WHERE id = 1").get();
2685
+ return row?.started_at ?? null;
2686
+ }
2687
+ getProfileMaintenanceState() {
2688
+ this.assertOpen();
2689
+ const row = this.database.prepare("SELECT last_succeeded_at, input_json FROM persistent_profile_maintenance WHERE id = 1").get();
2690
+ return row ? { lastSucceededAt: row.last_succeeded_at, inputJson: row.input_json } : null;
2691
+ }
2692
+ recordProfileMaintenanceFailure(profile, now = Date.now()) {
2693
+ this.assertOpen();
2694
+ const inputJson = JSON.stringify(profile);
2695
+ const previous = this.database.prepare("SELECT input_json, failure_count, next_retry_at FROM persistent_profile_maintenance_failures WHERE id = 1").get();
2696
+ const sameWindow = previous?.input_json === inputJson && !(previous.failure_count >= 3 && now >= Date.parse(previous.next_retry_at));
2697
+ const count = sameWindow ? previous.failure_count + 1 : 1;
2698
+ const nextRetryAt = new Date(now + (count >= 3 ? 24 * 60 : count * 5) * 60 * 1e3).toISOString();
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);
2700
+ }
2701
+ updateProfileField(field, value, source, sourceMessageId, expectedValue, expectedRevision) {
2702
+ this.assertOpen();
2703
+ if (!isProfileField(field)) throw new TypeError(`Unknown Persistent Profile field: ${field}`);
2704
+ if (typeof value !== "string") throw new TypeError("Persistent Profile value must be a string");
2705
+ if (!["settings", "user_explicit", "agent_tool", "maintenance"].includes(source)) throw new TypeError("Invalid Profile change source");
2706
+ return this.immediateTransaction(() => {
2707
+ const profile = this.getPersistentProfile();
2708
+ const wasNonempty = Object.values(profile).some((entry) => entry.length > 0);
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 };
2715
+ if (oldValue === value) return { field, value, modified: false };
2716
+ profile[field] = value;
2717
+ validateProfile(profile);
2718
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2719
+ this.writeProfileField(field, oldValue, value, source, sourceMessageId ?? null, now);
2720
+ const isNonempty = Object.values(profile).some((entry) => entry.length > 0);
2721
+ if (!wasNonempty && isNonempty) {
2722
+ this.database.prepare("INSERT INTO persistent_profile_maintenance_baseline (id, started_at) VALUES (1, ?) ON CONFLICT (id) DO UPDATE SET started_at = excluded.started_at").run(now);
2723
+ this.database.prepare("DELETE FROM persistent_profile_maintenance WHERE id = 1").run();
2724
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_failures WHERE id = 1").run();
2725
+ } else if (!isNonempty) {
2726
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_baseline WHERE id = 1").run();
2727
+ }
2728
+ return { field, value, modified: true };
2729
+ });
2730
+ }
2731
+ /** Compare the complete input snapshot before writing a model result. */
2732
+ applyProfileMaintenance(expected, proposed, now = (/* @__PURE__ */ new Date()).toISOString()) {
2733
+ this.assertOpen();
2734
+ validateProfile(proposed);
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
+ }
2739
+ return this.immediateTransaction(() => {
2740
+ const current = this.getPersistentProfile();
2741
+ if (JSON.stringify(current) !== JSON.stringify(expected)) return false;
2742
+ for (const field of Object.keys(PROFILE_FIELDS)) {
2743
+ if (current[field] !== proposed[field]) this.writeProfileField(field, current[field], proposed[field], "maintenance", null, now);
2744
+ }
2745
+ this.database.prepare("INSERT INTO persistent_profile_maintenance (id, last_succeeded_at, input_json) VALUES (1, ?, ?) ON CONFLICT (id) DO UPDATE SET last_succeeded_at = excluded.last_succeeded_at, input_json = excluded.input_json").run(now, JSON.stringify(proposed));
2746
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_failures WHERE id = 1").run();
2747
+ return true;
2748
+ });
2749
+ }
2750
+ profileMaintenanceDue(now = Date.now()) {
2751
+ const profile = this.getPersistentProfile();
2752
+ const failed = this.database.prepare("SELECT input_json, failure_count, next_retry_at FROM persistent_profile_maintenance_failures WHERE id = 1").get();
2753
+ if (failed?.input_json === JSON.stringify(profile) && now < Date.parse(failed.next_retry_at)) return false;
2754
+ const state = this.getProfileMaintenanceState();
2755
+ if (!profileMaintenanceDue(profile, state?.lastSucceededAt ?? this.getProfileMaintenanceStartedAt(), now)) return false;
2756
+ return !state || now - Date.parse(state.lastSucceededAt) >= 24 * 60 * 60 * 1e3 || state.inputJson !== JSON.stringify(profile);
2757
+ }
2758
+ writeProfileField(field, oldValue, newValue, source, sourceMessageId, now = (/* @__PURE__ */ new Date()).toISOString()) {
2759
+ this.database.prepare("INSERT INTO persistent_profile (field, value) VALUES (?, ?) ON CONFLICT (field) DO UPDATE SET value = excluded.value").run(field, newValue);
2760
+ this.database.prepare("INSERT INTO persistent_profile_changes (field, old_value, new_value, source, updated_at, source_message_id) VALUES (?, ?, ?, ?, ?, ?)").run(field, oldValue, newValue, source, now, sourceMessageId);
2761
+ }
2557
2762
  searchRawMessageIds(namespace, tokens, limit, threadId, includeUnthreaded = false) {
2558
2763
  this.assertOpen();
2559
2764
  if (!this.rawSearchFtsAvailable || tokens.length === 0) return null;
@@ -3057,6 +3262,19 @@ var SqliteStorage = class {
3057
3262
  this.database.exec(THREAD_INDEXES);
3058
3263
  this.enableRawSearchFts();
3059
3264
  }
3265
+ this.immediateTransaction(() => {
3266
+ this.database.exec("DROP INDEX IF EXISTS persistent_profile_single_consent");
3267
+ this.database.exec(`
3268
+ INSERT INTO persistent_profile_maintenance_baseline (id, started_at)
3269
+ SELECT 1, COALESCE(
3270
+ (SELECT MIN(updated_at) FROM persistent_profile_changes WHERE new_value <> ''),
3271
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
3272
+ )
3273
+ WHERE EXISTS (SELECT 1 FROM persistent_profile WHERE value <> '')
3274
+ ON CONFLICT (id) DO NOTHING
3275
+ `);
3276
+ this.database.exec("DELETE FROM persistent_profile_maintenance_baseline WHERE NOT EXISTS (SELECT 1 FROM persistent_profile WHERE value <> '')");
3277
+ });
3060
3278
  this.assertSchemaVersion();
3061
3279
  }
3062
3280
  enableRawSearchFts() {
@@ -5531,7 +5749,8 @@ var STRUCTURED_FIELDS = {
5531
5749
  projector: ["reason", "changes"],
5532
5750
  graphProjector: ["reason", "nodes", "edges"],
5533
5751
  externalMemoryExtractor: ["reason", "candidates"],
5534
- externalMemoryDecider: ["action", "reason", "confidence"]
5752
+ externalMemoryDecider: ["action", "reason", "confidence"],
5753
+ profileMaintenance: Object.keys(PROFILE_FIELDS)
5535
5754
  };
5536
5755
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
5537
5756
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
@@ -5665,6 +5884,9 @@ var EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS = {
5665
5884
  reason: { type: "string", required: true },
5666
5885
  candidates: { type: "array", items: OPEN_OBJECT, required: true }
5667
5886
  };
5887
+ var PROFILE_MAINTENANCE_PARAMETERS = Object.fromEntries(
5888
+ Object.keys(PROFILE_FIELDS).map((field) => [field, { type: "string", required: true }])
5889
+ );
5668
5890
  var STRUCTURED_TOOLS = {
5669
5891
  summarizer: {
5670
5892
  name: "stratagate_summarize_block",
@@ -5695,6 +5917,11 @@ var STRUCTURED_TOOLS = {
5695
5917
  name: "stratagate_recover_external_memory",
5696
5918
  description: "Recover structured external-memory candidates from malformed JSON or plain text.",
5697
5919
  parameters: EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS
5920
+ },
5921
+ profileMaintenance: {
5922
+ name: "stratagate_maintain_profile",
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.",
5924
+ parameters: PROFILE_MAINTENANCE_PARAMETERS
5698
5925
  }
5699
5926
  };
5700
5927
  function toolSchema(kind) {
@@ -5987,6 +6214,22 @@ var DshModelBridge = class {
5987
6214
  }));
5988
6215
  return { candidates: parsed.candidates, reason: text2(raw.reason, parsed.reason) };
5989
6216
  };
6217
+ async maintainProfile(profile) {
6218
+ const raw = object(await this.callStructured(
6219
+ "profileMaintenance",
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.`,
6221
+ { profile, fieldDefinitions: PROFILE_FIELDS }
6222
+ ));
6223
+ if (Object.keys(raw).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(raw).some((field) => !(field in PROFILE_FIELDS))) {
6224
+ throw new Error("Profile maintenance returned unexpected fields");
6225
+ }
6226
+ const proposed = raw;
6227
+ validateProfile(proposed);
6228
+ for (const field of PROFILE_PROTECTED_SHORT_FIELDS) {
6229
+ if (proposed[field].trim() !== profile[field].trim()) throw new Error(`Profile maintenance changed protected short field ${field}`);
6230
+ }
6231
+ return proposed;
6232
+ }
5990
6233
  async callStructured(kind, system, payload) {
5991
6234
  const execution = this.sessions.getStore();
5992
6235
  if (!execution) throw new Error("StrataGate model callback ran without an execution context");
@@ -6020,7 +6263,7 @@ ${JSON_RETRY_INSTRUCTION}`,
6020
6263
  type: "function",
6021
6264
  function: { name: STRUCTURED_TOOLS[kind].name }
6022
6265
  },
6023
- maxTokens: this.config.maxOutputTokens,
6266
+ maxTokens: kind === "profileMaintenance" ? Math.max(this.config.maxOutputTokens, Math.min(12e3, 512 + 2 * Array.from(JSON.stringify(payload)).length)) : this.config.maxOutputTokens,
6024
6267
  sessionId: execution.sessionId,
6025
6268
  purpose: "compaction"
6026
6269
  };
@@ -6103,13 +6346,15 @@ ${JSON_RETRY_INSTRUCTION}`,
6103
6346
  );
6104
6347
  }
6105
6348
  }
6106
- this.successfulResponses.push({
6107
- id: `model_response_${crypto.randomUUID()}`,
6108
- kind,
6109
- response: responseForError,
6110
- createdAt: nowUtc8()
6111
- });
6112
- if (this.successfulResponses.length > 5) this.successfulResponses.shift();
6349
+ if (kind !== "profileMaintenance") {
6350
+ this.successfulResponses.push({
6351
+ id: `model_response_${crypto.randomUUID()}`,
6352
+ kind,
6353
+ response: responseForError,
6354
+ createdAt: nowUtc8()
6355
+ });
6356
+ if (this.successfulResponses.length > 5) this.successfulResponses.shift();
6357
+ }
6113
6358
  return parsed;
6114
6359
  } catch (error) {
6115
6360
  if (!(error instanceof ModelJsonResponseError)) throw error;
@@ -6675,6 +6920,7 @@ var StrataGateRuntime = class {
6675
6920
  flushNativeSession;
6676
6921
  feedbackOrigin;
6677
6922
  openPath;
6923
+ profileStorage;
6678
6924
  folder = new TurnFolder();
6679
6925
  spaces = /* @__PURE__ */ new Map();
6680
6926
  batches = /* @__PURE__ */ new Map();
@@ -6701,6 +6947,7 @@ var StrataGateRuntime = class {
6701
6947
  backgroundWorkerTimer;
6702
6948
  backgroundWorkerRun;
6703
6949
  backgroundWorkerWakePending = false;
6950
+ profileMaintenanceRun;
6704
6951
  disposeAdaptersUpdated;
6705
6952
  settingsTail = Promise.resolve();
6706
6953
  batchSequence = 0;
@@ -6726,6 +6973,7 @@ var StrataGateRuntime = class {
6726
6973
  }
6727
6974
  async runBackgroundWorker() {
6728
6975
  if (this.closed || !this.models.isReady()) return;
6976
+ void this.runProfileMaintenance().catch((error) => this.onIngestError(error));
6729
6977
  let namespaces;
6730
6978
  try {
6731
6979
  namespaces = await this.adminNamespaces();
@@ -6742,6 +6990,49 @@ var StrataGateRuntime = class {
6742
6990
  }
6743
6991
  }
6744
6992
  }
6993
+ async runProfileMaintenance(now = Date.now()) {
6994
+ if (this.profileMaintenanceRun) return this.profileMaintenanceRun;
6995
+ if (this.closed || !this.models.isReady() || !this.profileStore().profileMaintenanceDue(now)) return false;
6996
+ const run = (async () => {
6997
+ const current = this.profileStore().getPersistentProfile();
6998
+ try {
6999
+ const proposed = await this.models.runDetached("stratagate-profile-maintenance", () => this.models.maintainProfile(current));
7000
+ return this.profileStore().applyProfileMaintenance(current, proposed, new Date(now).toISOString());
7001
+ } catch (error) {
7002
+ this.profileStore().recordProfileMaintenanceFailure(current, now);
7003
+ throw error;
7004
+ }
7005
+ })();
7006
+ this.profileMaintenanceRun = run;
7007
+ try {
7008
+ return await run;
7009
+ } finally {
7010
+ if (this.profileMaintenanceRun === run) this.profileMaintenanceRun = void 0;
7011
+ }
7012
+ }
7013
+ getPersistentProfile() {
7014
+ return this.profileStore().getPersistentProfile();
7015
+ }
7016
+ getProfileSnapshot() {
7017
+ return this.profileStore().getProfileSnapshot();
7018
+ }
7019
+ renderProfileContext() {
7020
+ return renderPersistentProfile(this.getPersistentProfile());
7021
+ }
7022
+ getProfileChanges() {
7023
+ return this.profileStore().getProfileChanges();
7024
+ }
7025
+ updatePersistentProfile(field, value, source, sourceMessageId, expectedValue, expectedRevision) {
7026
+ const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId, expectedValue, expectedRevision);
7027
+ if (result.modified) this.wakeBackgroundWorker();
7028
+ return result;
7029
+ }
7030
+ profileStore() {
7031
+ return this.profileStorage ??= new SqliteStorage({ filename: this.config.database });
7032
+ }
7033
+ updatePersistentProfileFromTool(field, value) {
7034
+ return this.updatePersistentProfile(field, value, "agent_tool");
7035
+ }
6745
7036
  async runBackgroundNamespace(namespace) {
6746
7037
  if (this.closed || !this.models.isReady()) return;
6747
7038
  const existing = this.backgroundNamespaceRuns.get(namespace);
@@ -7319,10 +7610,12 @@ var StrataGateRuntime = class {
7319
7610
  const settled = await Promise.allSettled(this.spaces.values());
7320
7611
  await Promise.allSettled(this.backgroundNamespaceRuns.values());
7321
7612
  if (this.backgroundWorkerRun) await Promise.allSettled([this.backgroundWorkerRun]);
7613
+ if (this.profileMaintenanceRun) await Promise.allSettled([this.profileMaintenanceRun]);
7322
7614
  await Promise.allSettled(this.migrationRuns.values());
7323
7615
  await Promise.allSettled(this.derivationRuns.values());
7324
7616
  await Promise.allSettled(this.externalImportRuns.values());
7325
7617
  await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
7618
+ if (this.profileStorage) await this.profileStorage.close();
7326
7619
  this.sessionsById.clear();
7327
7620
  if (flushError !== void 0) throw flushError;
7328
7621
  }
@@ -8547,6 +8840,35 @@ function sessionOf(exec) {
8547
8840
  return exec.agent.session;
8548
8841
  }
8549
8842
  function registerMemoryTools(ctx, runtime) {
8843
+ ctx.tools.register(defineTool({
8844
+ name: "memory_profile_update",
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.
8853
+
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.
8855
+
8856
+ When the user explicitly asks to make a persistent change, that request is already authorization and the tool may be called immediately.
8857
+
8858
+ If the assistant only infers that something might be a useful persistent preference or profile fact, it must not update the profile immediately. It must first tell the user exactly which field it proposes to change and what the new value would be, and ask the user to reply exactly "\u540C\u610F". Only a directly subsequent "\u540C\u610F" authorizes that single proposed change. If the user refuses, does not reply "\u540C\u610F", changes the subject, or proposes a different change, do not perform the update.
8859
+
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.`,
8861
+ parameters: {
8862
+ field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8863
+ value: { type: "string", required: true }
8864
+ },
8865
+ output: jsonOutput,
8866
+ execute: async (args, exec) => {
8867
+ if (Object.keys(args).some((key) => key !== "field" && key !== "value")) throw new TypeError("Unknown Profile update argument");
8868
+ sessionOf(exec);
8869
+ return runtime.updatePersistentProfileFromTool(args.field, args.value);
8870
+ }
8871
+ }));
8550
8872
  ctx.tools.register(defineTool({
8551
8873
  name: "feedback_prepare",
8552
8874
  description: 'This tool is provided by the StrataGate plugin. Create or revise a local StrataGate feedback draft when the user directly requests it, or after a proactive suggestion permitted by the StrataGate feedback policy and the user explicitly agrees. A direct user request is already authorization. Use only facts known from the current conversation; leave unknown fields empty and never invent versions, logs, Block counts, or diagnostics. Never submit anything to GitHub. After success, briefly say the draft is local and not submitted, then render feedbackUrl as a Markdown link labeled "\u6253\u5F00\u53CD\u9988\u8349\u7A3F". Then ask exactly once: "\u8981\u4E0D\u8981\u987A\u4FBF\u8BA9\u6211\u5C1D\u8BD5\u4FEE\u590D\u8FD9\u4E2A\u95EE\u9898\uFF0C\u5E76\u63D0\u4EA4\u4E00\u4E2A PR\uFF1F" If the user does not respond or declines, do not ask again. Do not use a popup or other additional UI. Do not print draft fields or an Issue-content table, and do not direct the user through Settings manually.',
@@ -8866,11 +9188,11 @@ function installedPackageVersion(names) {
8866
9188
  }
8867
9189
  return "unknown";
8868
9190
  }
8869
- function sendJson(res, status, body) {
9191
+ function sendJson(res, status, body, preserveProfileText = false) {
8870
9192
  res.statusCode = status;
8871
9193
  res.setHeader("Content-Type", "application/json; charset=utf-8");
8872
9194
  res.setHeader("Cache-Control", "no-store");
8873
- res.end(JSON.stringify(redactValue(body)));
9195
+ res.end(JSON.stringify(preserveProfileText ? body : redactValue(body)));
8874
9196
  }
8875
9197
  function numeric(value, fallback, minimum, maximum) {
8876
9198
  const parsed = Number(value);
@@ -9294,6 +9616,47 @@ async function updateSettings(runtime, url) {
9294
9616
  if (lambda !== void 0) result.blockDecayLambda = await runtime.adminSetBlockDecayLambda(lambda);
9295
9617
  return result;
9296
9618
  }
9619
+ async function persistentProfile(runtime, req) {
9620
+ if (req.method === "GET") {
9621
+ const snapshot = runtime.getProfileSnapshot();
9622
+ return { ...snapshot.profile, _revisions: snapshot.revisions };
9623
+ }
9624
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "Persistent Profile requires GET or PATCH");
9625
+ let suppliedBody = req.body;
9626
+ if (suppliedBody === void 0 && typeof req[Symbol.asyncIterator] === "function") {
9627
+ const chunks = [];
9628
+ let size = 0;
9629
+ for await (const chunk of req) {
9630
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
9631
+ size += value.length;
9632
+ if (size > 16 * 1024) throw new AdminHttpError(413, "Profile update exceeds 16 KB");
9633
+ chunks.push(value);
9634
+ }
9635
+ suppliedBody = Buffer.concat(chunks).toString("utf8");
9636
+ }
9637
+ let body = suppliedBody;
9638
+ if (typeof body === "string") {
9639
+ try {
9640
+ body = JSON.parse(body);
9641
+ } catch {
9642
+ throw new AdminHttpError(400, "Profile update must be valid JSON");
9643
+ }
9644
+ }
9645
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new AdminHttpError(400, "Profile update must be an object");
9646
+ const input = body;
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");
9649
+ }
9650
+ try {
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 } };
9655
+ } catch (error) {
9656
+ if (error instanceof TypeError || error instanceof RangeError) throw new AdminHttpError(400, error.message);
9657
+ throw error;
9658
+ }
9659
+ }
9297
9660
  async function feedback(runtime, req, url) {
9298
9661
  if (req.method === "GET") {
9299
9662
  const namespace2 = url.searchParams.get("namespace")?.trim() ?? "";
@@ -9944,6 +10307,8 @@ async function handleAdminRequest(runtime, req, res) {
9944
10307
  const path = url.pathname.replace(/\/$/, "");
9945
10308
  if (path === "/api/stratagate/feedback") {
9946
10309
  sendJson(res, 200, await feedback(runtime, req, url));
10310
+ } else if (path === "/api/stratagate/profile") {
10311
+ sendJson(res, 200, await persistentProfile(runtime, req), true);
9947
10312
  } else if (path === "/api/stratagate/settings") {
9948
10313
  if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
9949
10314
  sendJson(res, 200, await updateSettings(runtime, url));
@@ -10381,9 +10746,16 @@ async function apply(ctx, config) {
10381
10746
  ctx.systemPrompt.section({ name: "tool:stratagate-feedback", order: 114, text: FEEDBACK_PROTOCOL });
10382
10747
  ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
10383
10748
  const assembled = await next();
10384
- const session = context.agent?.session;
10385
- if (!session) return assembled;
10386
10749
  const contexts = [...assembled.contexts];
10750
+ try {
10751
+ const profile = runtime.renderProfileContext();
10752
+ if (profile) contexts.push({ name: "stratagate:persistent-profile", text: profile });
10753
+ } catch (error) {
10754
+ ctx.logger.warn(`stratagate-memory profile context failed: ${renderError(error)}`);
10755
+ throw error;
10756
+ }
10757
+ const session = context.agent?.session;
10758
+ if (!session) return { ...assembled, contexts };
10387
10759
  try {
10388
10760
  const text3 = await runtime.buildAutoContext(session);
10389
10761
  contexts.push({ name: "stratagate:auto-memory", text: text3 });