stratagate-dsh 0.2.80 → 0.2.82

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,59 @@ 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 language", maxLength: 100 },
1618
+ responsePreferences: { label: "Response preferences", maxLength: 1e3 },
1619
+ standingInstructions: { label: "Standing instructions", maxLength: 1e3 },
1620
+ userBackground: { label: "User background", maxLength: 1500 },
1621
+ longTermGoals: { label: "Long-term goals", maxLength: 1e3 },
1622
+ persistentNotes: { label: "Persistent notes", maxLength: 1200 }
1623
+ };
1624
+ var PROFILE_TOTAL_MAX_LENGTH = 6e3;
1625
+ var PROFILE_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1626
+ var PROFILE_MAINTENANCE_TOTAL_THRESHOLD = 4800;
1627
+ function profileLength(value) {
1628
+ return Array.from(value).length;
1629
+ }
1630
+ function emptyProfile() {
1631
+ return Object.fromEntries(Object.keys(PROFILE_FIELDS).map((field) => [field, ""]));
1632
+ }
1633
+ function isProfileField(value) {
1634
+ return Object.prototype.hasOwnProperty.call(PROFILE_FIELDS, value);
1635
+ }
1636
+ function validateProfile(profile) {
1637
+ let total = 0;
1638
+ for (const field of Object.keys(PROFILE_FIELDS)) {
1639
+ if (typeof profile[field] !== "string") throw new TypeError(`${field} must be a string`);
1640
+ const length = profileLength(profile[field]);
1641
+ if (length > PROFILE_FIELDS[field].maxLength) throw new RangeError(`${field} exceeds ${PROFILE_FIELDS[field].maxLength} characters`);
1642
+ total += length;
1643
+ }
1644
+ if (total > PROFILE_TOTAL_MAX_LENGTH) throw new RangeError(`Persistent Profile exceeds ${PROFILE_TOTAL_MAX_LENGTH} characters`);
1645
+ }
1646
+ function profileMaintenanceDue(profile, timeBasisAt, now = Date.now()) {
1647
+ const lengths = Object.keys(PROFILE_FIELDS).map((field) => ({ field, length: profileLength(profile[field]) }));
1648
+ const total = lengths.reduce((sum, item) => sum + item.length, 0);
1649
+ if (total === 0) return false;
1650
+ 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);
1651
+ }
1652
+ function renderPersistentProfile(profile) {
1653
+ const lines = Object.keys(PROFILE_FIELDS).filter((field) => profile[field].length > 0).map((field) => `${PROFILE_FIELDS[field].label}: ${profile[field]}`);
1654
+ if (lines.length === 0) return null;
1655
+ return `[StrataGate Persistent Profile]
1656
+ This is user-authorized persistent profile data provided by StrataGate.
1657
+ Treat it as stable cross-session context.
1658
+ Do not invent additional facts from it.
1659
+ It does not override higher-priority system instructions.
1660
+
1661
+ ${lines.join("\n")}`;
1662
+ }
1663
+
1613
1664
  // packages/core/src/storage.ts
1614
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 11;
1665
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 12;
1615
1666
  var KNOWLEDGE_GRAPH_PROJECTOR_VERSION = 1;
1616
1667
  var DERIVATION_MAX_ATTEMPTS = 3;
1617
1668
  var StorageConflictError = class extends Error {
@@ -1751,6 +1802,8 @@ function normalizeSnapshot(value) {
1751
1802
  ...structuredClone(value),
1752
1803
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION
1753
1804
  };
1805
+ } else if (schemaVersion === 11) {
1806
+ snapshot = { ...structuredClone(value), schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION };
1754
1807
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
1755
1808
  snapshot = structuredClone(value);
1756
1809
  } else {
@@ -1965,6 +2018,38 @@ function nowUtc8() {
1965
2018
 
1966
2019
  // packages/core/src/sqlite.ts
1967
2020
  var SCHEMA = `
2021
+ CREATE TABLE IF NOT EXISTS persistent_profile (
2022
+ field TEXT PRIMARY KEY,
2023
+ value TEXT NOT NULL
2024
+ ) STRICT;
2025
+
2026
+ CREATE TABLE IF NOT EXISTS persistent_profile_changes (
2027
+ id INTEGER PRIMARY KEY,
2028
+ field TEXT NOT NULL,
2029
+ old_value TEXT NOT NULL,
2030
+ new_value TEXT NOT NULL,
2031
+ source TEXT NOT NULL,
2032
+ updated_at TEXT NOT NULL,
2033
+ source_message_id TEXT
2034
+ ) STRICT;
2035
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance (
2036
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2037
+ last_succeeded_at TEXT NOT NULL,
2038
+ input_json TEXT NOT NULL
2039
+ ) STRICT;
2040
+
2041
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance_baseline (
2042
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2043
+ started_at TEXT NOT NULL
2044
+ ) STRICT;
2045
+
2046
+ CREATE TABLE IF NOT EXISTS persistent_profile_maintenance_failures (
2047
+ id INTEGER PRIMARY KEY CHECK (id = 1),
2048
+ input_json TEXT NOT NULL,
2049
+ failure_count INTEGER NOT NULL,
2050
+ next_retry_at TEXT NOT NULL
2051
+ ) STRICT;
2052
+
1968
2053
  CREATE TABLE IF NOT EXISTS memory_spaces (
1969
2054
  namespace TEXT PRIMARY KEY,
1970
2055
  schema_version INTEGER NOT NULL,
@@ -2554,6 +2639,98 @@ var SqliteStorage = class {
2554
2639
  this.database.close();
2555
2640
  this.closed = true;
2556
2641
  }
2642
+ /** Installation-wide state; intentionally has no namespace column. */
2643
+ getPersistentProfile() {
2644
+ this.assertOpen();
2645
+ const profile = emptyProfile();
2646
+ const rows = this.database.prepare("SELECT field, value FROM persistent_profile").all();
2647
+ for (const row of rows) if (isProfileField(row.field)) profile[row.field] = row.value;
2648
+ return profile;
2649
+ }
2650
+ getProfileChanges() {
2651
+ this.assertOpen();
2652
+ 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();
2653
+ return rows.map((row) => ({
2654
+ field: row.field,
2655
+ oldValue: row.old_value,
2656
+ newValue: row.new_value,
2657
+ source: row.source,
2658
+ updatedAt: row.updated_at,
2659
+ sourceMessageId: row.source_message_id
2660
+ }));
2661
+ }
2662
+ getProfileMaintenanceStartedAt() {
2663
+ this.assertOpen();
2664
+ const row = this.database.prepare("SELECT started_at FROM persistent_profile_maintenance_baseline WHERE id = 1").get();
2665
+ return row?.started_at ?? null;
2666
+ }
2667
+ getProfileMaintenanceState() {
2668
+ this.assertOpen();
2669
+ const row = this.database.prepare("SELECT last_succeeded_at, input_json FROM persistent_profile_maintenance WHERE id = 1").get();
2670
+ return row ? { lastSucceededAt: row.last_succeeded_at, inputJson: row.input_json } : null;
2671
+ }
2672
+ recordProfileMaintenanceFailure(profile, now = Date.now()) {
2673
+ this.assertOpen();
2674
+ const inputJson = JSON.stringify(profile);
2675
+ const previous = this.database.prepare("SELECT input_json, failure_count, next_retry_at FROM persistent_profile_maintenance_failures WHERE id = 1").get();
2676
+ const sameWindow = previous?.input_json === inputJson && !(previous.failure_count >= 3 && now >= Date.parse(previous.next_retry_at));
2677
+ const count = sameWindow ? previous.failure_count + 1 : 1;
2678
+ const nextRetryAt = new Date(now + (count >= 3 ? 24 * 60 : count * 5) * 60 * 1e3).toISOString();
2679
+ 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
+ }
2681
+ updateProfileField(field, value, source, sourceMessageId) {
2682
+ this.assertOpen();
2683
+ if (!isProfileField(field)) throw new TypeError(`Unknown Persistent Profile field: ${field}`);
2684
+ if (typeof value !== "string") throw new TypeError("Persistent Profile value must be a string");
2685
+ if (!["settings", "user_explicit", "agent_tool", "maintenance"].includes(source)) throw new TypeError("Invalid Profile change source");
2686
+ return this.immediateTransaction(() => {
2687
+ const profile = this.getPersistentProfile();
2688
+ const wasNonempty = Object.values(profile).some((entry) => entry.length > 0);
2689
+ const oldValue = profile[field];
2690
+ if (oldValue === value) return { field, value, modified: false };
2691
+ profile[field] = value;
2692
+ validateProfile(profile);
2693
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2694
+ this.writeProfileField(field, oldValue, value, source, sourceMessageId ?? null, now);
2695
+ const isNonempty = Object.values(profile).some((entry) => entry.length > 0);
2696
+ if (!wasNonempty && isNonempty) {
2697
+ 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);
2698
+ this.database.prepare("DELETE FROM persistent_profile_maintenance WHERE id = 1").run();
2699
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_failures WHERE id = 1").run();
2700
+ } else if (!isNonempty) {
2701
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_baseline WHERE id = 1").run();
2702
+ }
2703
+ return { field, value, modified: true };
2704
+ });
2705
+ }
2706
+ /** Compare the complete input snapshot before writing a model result. */
2707
+ applyProfileMaintenance(expected, proposed, now = (/* @__PURE__ */ new Date()).toISOString()) {
2708
+ this.assertOpen();
2709
+ validateProfile(proposed);
2710
+ 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");
2711
+ return this.immediateTransaction(() => {
2712
+ const current = this.getPersistentProfile();
2713
+ if (JSON.stringify(current) !== JSON.stringify(expected)) return false;
2714
+ for (const field of Object.keys(PROFILE_FIELDS)) {
2715
+ if (current[field] !== proposed[field]) this.writeProfileField(field, current[field], proposed[field], "maintenance", null, now);
2716
+ }
2717
+ 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));
2718
+ this.database.prepare("DELETE FROM persistent_profile_maintenance_failures WHERE id = 1").run();
2719
+ return true;
2720
+ });
2721
+ }
2722
+ profileMaintenanceDue(now = Date.now()) {
2723
+ const profile = this.getPersistentProfile();
2724
+ const failed = this.database.prepare("SELECT input_json, failure_count, next_retry_at FROM persistent_profile_maintenance_failures WHERE id = 1").get();
2725
+ if (failed?.input_json === JSON.stringify(profile) && now < Date.parse(failed.next_retry_at)) return false;
2726
+ const state = this.getProfileMaintenanceState();
2727
+ if (!profileMaintenanceDue(profile, state?.lastSucceededAt ?? this.getProfileMaintenanceStartedAt(), now)) return false;
2728
+ return !state || now - Date.parse(state.lastSucceededAt) >= 24 * 60 * 60 * 1e3 || state.inputJson !== JSON.stringify(profile);
2729
+ }
2730
+ writeProfileField(field, oldValue, newValue, source, sourceMessageId, now = (/* @__PURE__ */ new Date()).toISOString()) {
2731
+ this.database.prepare("INSERT INTO persistent_profile (field, value) VALUES (?, ?) ON CONFLICT (field) DO UPDATE SET value = excluded.value").run(field, newValue);
2732
+ 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);
2733
+ }
2557
2734
  searchRawMessageIds(namespace, tokens, limit, threadId, includeUnthreaded = false) {
2558
2735
  this.assertOpen();
2559
2736
  if (!this.rawSearchFtsAvailable || tokens.length === 0) return null;
@@ -3057,6 +3234,19 @@ var SqliteStorage = class {
3057
3234
  this.database.exec(THREAD_INDEXES);
3058
3235
  this.enableRawSearchFts();
3059
3236
  }
3237
+ this.immediateTransaction(() => {
3238
+ this.database.exec("DROP INDEX IF EXISTS persistent_profile_single_consent");
3239
+ this.database.exec(`
3240
+ INSERT INTO persistent_profile_maintenance_baseline (id, started_at)
3241
+ SELECT 1, COALESCE(
3242
+ (SELECT MIN(updated_at) FROM persistent_profile_changes WHERE new_value <> ''),
3243
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
3244
+ )
3245
+ WHERE EXISTS (SELECT 1 FROM persistent_profile WHERE value <> '')
3246
+ ON CONFLICT (id) DO NOTHING
3247
+ `);
3248
+ this.database.exec("DELETE FROM persistent_profile_maintenance_baseline WHERE NOT EXISTS (SELECT 1 FROM persistent_profile WHERE value <> '')");
3249
+ });
3060
3250
  this.assertSchemaVersion();
3061
3251
  }
3062
3252
  enableRawSearchFts() {
@@ -5093,11 +5283,19 @@ var StrataGate = class _StrataGate {
5093
5283
  });
5094
5284
  let result;
5095
5285
  try {
5286
+ const query = [target.l0Title, target.l1Summary, ...target.l2Keypoints ?? []].filter(Boolean).join(" ");
5287
+ const relevant = query ? (await this.searchEvents(query, { limit: 8, trackRetrieval: false })).map(({ event }) => event) : [];
5288
+ const sourceSequence = new Map(this.blocks.map((block) => [block.id, block.sequence]));
5289
+ const recent = this.events.filter((event) => event.status === "active" || event.status === "superseded").sort((left, right) => (sourceSequence.get(right.sourceBlockId) ?? -1) - (sourceSequence.get(left.sourceBlockId) ?? -1) || (right.formedTurn ?? -1) - (left.formedTurn ?? -1) || right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id)).slice(0, 4);
5290
+ const timelineEvents = new Map(relevant.map((event) => [event.id, event]));
5291
+ for (const event of recent) {
5292
+ if (!timelineEvents.has(event.id)) timelineEvents.set(event.id, event);
5293
+ }
5096
5294
  result = await this.extractor({
5097
5295
  previous: threadBlocks.slice(0, targetIndex).reverse().find((block) => block.l2Keypoints !== void 0) ?? null,
5098
5296
  target,
5099
5297
  next,
5100
- timeline: this.events.map((event) => ({ id: event.id, title: event.title, temporal: event.temporal }))
5298
+ timeline: [...timelineEvents.values()].map((event) => ({ id: event.id, title: event.title, temporal: event.temporal }))
5101
5299
  });
5102
5300
  } catch (error) {
5103
5301
  await this.commitMutation(() => {
@@ -5523,7 +5721,8 @@ var STRUCTURED_FIELDS = {
5523
5721
  projector: ["reason", "changes"],
5524
5722
  graphProjector: ["reason", "nodes", "edges"],
5525
5723
  externalMemoryExtractor: ["reason", "candidates"],
5526
- externalMemoryDecider: ["action", "reason", "confidence"]
5724
+ externalMemoryDecider: ["action", "reason", "confidence"],
5725
+ profileMaintenance: Object.keys(PROFILE_FIELDS)
5527
5726
  };
5528
5727
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
5529
5728
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
@@ -5657,6 +5856,9 @@ var EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS = {
5657
5856
  reason: { type: "string", required: true },
5658
5857
  candidates: { type: "array", items: OPEN_OBJECT, required: true }
5659
5858
  };
5859
+ var PROFILE_MAINTENANCE_PARAMETERS = Object.fromEntries(
5860
+ Object.keys(PROFILE_FIELDS).map((field) => [field, { type: "string", required: true }])
5861
+ );
5660
5862
  var STRUCTURED_TOOLS = {
5661
5863
  summarizer: {
5662
5864
  name: "stratagate_summarize_block",
@@ -5687,6 +5889,11 @@ var STRUCTURED_TOOLS = {
5687
5889
  name: "stratagate_recover_external_memory",
5688
5890
  description: "Recover structured external-memory candidates from malformed JSON or plain text.",
5689
5891
  parameters: EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS
5892
+ },
5893
+ profileMaintenance: {
5894
+ name: "stratagate_maintain_profile",
5895
+ description: "Return the same eight Persistent Profile fields with only safe wording and redundancy cleanup.",
5896
+ parameters: PROFILE_MAINTENANCE_PARAMETERS
5690
5897
  }
5691
5898
  };
5692
5899
  function toolSchema(kind) {
@@ -5979,6 +6186,22 @@ var DshModelBridge = class {
5979
6186
  }));
5980
6187
  return { candidates: parsed.candidates, reason: text2(raw.reason, parsed.reason) };
5981
6188
  };
6189
+ async maintainProfile(profile) {
6190
+ const raw = object(await this.callStructured(
6191
+ "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.`,
6193
+ { profile, fieldDefinitions: PROFILE_FIELDS }
6194
+ ));
6195
+ if (Object.keys(raw).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(raw).some((field) => !(field in PROFILE_FIELDS))) {
6196
+ throw new Error("Profile maintenance returned unexpected fields");
6197
+ }
6198
+ const proposed = raw;
6199
+ validateProfile(proposed);
6200
+ for (const field of ["userPreferredName", "assistantPreferredName", "preferredLanguage"]) {
6201
+ if (proposed[field].trim() !== profile[field].trim()) throw new Error(`Profile maintenance changed protected short field ${field}`);
6202
+ }
6203
+ return proposed;
6204
+ }
5982
6205
  async callStructured(kind, system, payload) {
5983
6206
  const execution = this.sessions.getStore();
5984
6207
  if (!execution) throw new Error("StrataGate model callback ran without an execution context");
@@ -6012,7 +6235,7 @@ ${JSON_RETRY_INSTRUCTION}`,
6012
6235
  type: "function",
6013
6236
  function: { name: STRUCTURED_TOOLS[kind].name }
6014
6237
  },
6015
- maxTokens: this.config.maxOutputTokens,
6238
+ maxTokens: kind === "profileMaintenance" ? Math.max(this.config.maxOutputTokens, Math.min(12e3, 512 + 2 * Array.from(JSON.stringify(payload)).length)) : this.config.maxOutputTokens,
6016
6239
  sessionId: execution.sessionId,
6017
6240
  purpose: "compaction"
6018
6241
  };
@@ -6095,13 +6318,15 @@ ${JSON_RETRY_INSTRUCTION}`,
6095
6318
  );
6096
6319
  }
6097
6320
  }
6098
- this.successfulResponses.push({
6099
- id: `model_response_${crypto.randomUUID()}`,
6100
- kind,
6101
- response: responseForError,
6102
- createdAt: nowUtc8()
6103
- });
6104
- if (this.successfulResponses.length > 5) this.successfulResponses.shift();
6321
+ if (kind !== "profileMaintenance") {
6322
+ this.successfulResponses.push({
6323
+ id: `model_response_${crypto.randomUUID()}`,
6324
+ kind,
6325
+ response: responseForError,
6326
+ createdAt: nowUtc8()
6327
+ });
6328
+ if (this.successfulResponses.length > 5) this.successfulResponses.shift();
6329
+ }
6105
6330
  return parsed;
6106
6331
  } catch (error) {
6107
6332
  if (!(error instanceof ModelJsonResponseError)) throw error;
@@ -6667,6 +6892,7 @@ var StrataGateRuntime = class {
6667
6892
  flushNativeSession;
6668
6893
  feedbackOrigin;
6669
6894
  openPath;
6895
+ profileStorage;
6670
6896
  folder = new TurnFolder();
6671
6897
  spaces = /* @__PURE__ */ new Map();
6672
6898
  batches = /* @__PURE__ */ new Map();
@@ -6693,6 +6919,7 @@ var StrataGateRuntime = class {
6693
6919
  backgroundWorkerTimer;
6694
6920
  backgroundWorkerRun;
6695
6921
  backgroundWorkerWakePending = false;
6922
+ profileMaintenanceRun;
6696
6923
  disposeAdaptersUpdated;
6697
6924
  settingsTail = Promise.resolve();
6698
6925
  batchSequence = 0;
@@ -6718,6 +6945,7 @@ var StrataGateRuntime = class {
6718
6945
  }
6719
6946
  async runBackgroundWorker() {
6720
6947
  if (this.closed || !this.models.isReady()) return;
6948
+ void this.runProfileMaintenance().catch((error) => this.onIngestError(error));
6721
6949
  let namespaces;
6722
6950
  try {
6723
6951
  namespaces = await this.adminNamespaces();
@@ -6734,6 +6962,46 @@ var StrataGateRuntime = class {
6734
6962
  }
6735
6963
  }
6736
6964
  }
6965
+ async runProfileMaintenance(now = Date.now()) {
6966
+ if (this.profileMaintenanceRun) return this.profileMaintenanceRun;
6967
+ if (this.closed || !this.models.isReady() || !this.profileStore().profileMaintenanceDue(now)) return false;
6968
+ const run = (async () => {
6969
+ const current = this.profileStore().getPersistentProfile();
6970
+ try {
6971
+ const proposed = await this.models.runDetached("stratagate-profile-maintenance", () => this.models.maintainProfile(current));
6972
+ return this.profileStore().applyProfileMaintenance(current, proposed, new Date(now).toISOString());
6973
+ } catch (error) {
6974
+ this.profileStore().recordProfileMaintenanceFailure(current, now);
6975
+ throw error;
6976
+ }
6977
+ })();
6978
+ this.profileMaintenanceRun = run;
6979
+ try {
6980
+ return await run;
6981
+ } finally {
6982
+ if (this.profileMaintenanceRun === run) this.profileMaintenanceRun = void 0;
6983
+ }
6984
+ }
6985
+ getPersistentProfile() {
6986
+ return this.profileStore().getPersistentProfile();
6987
+ }
6988
+ renderProfileContext() {
6989
+ return renderPersistentProfile(this.getPersistentProfile());
6990
+ }
6991
+ getProfileChanges() {
6992
+ return this.profileStore().getProfileChanges();
6993
+ }
6994
+ updatePersistentProfile(field, value, source, sourceMessageId) {
6995
+ const result = this.profileStore().updateProfileField(field, value, source, sourceMessageId);
6996
+ if (result.modified) this.wakeBackgroundWorker();
6997
+ return result;
6998
+ }
6999
+ profileStore() {
7000
+ return this.profileStorage ??= new SqliteStorage({ filename: this.config.database });
7001
+ }
7002
+ updatePersistentProfileFromTool(field, value) {
7003
+ return this.updatePersistentProfile(field, value, "agent_tool");
7004
+ }
6737
7005
  async runBackgroundNamespace(namespace) {
6738
7006
  if (this.closed || !this.models.isReady()) return;
6739
7007
  const existing = this.backgroundNamespaceRuns.get(namespace);
@@ -7311,10 +7579,12 @@ var StrataGateRuntime = class {
7311
7579
  const settled = await Promise.allSettled(this.spaces.values());
7312
7580
  await Promise.allSettled(this.backgroundNamespaceRuns.values());
7313
7581
  if (this.backgroundWorkerRun) await Promise.allSettled([this.backgroundWorkerRun]);
7582
+ if (this.profileMaintenanceRun) await Promise.allSettled([this.profileMaintenanceRun]);
7314
7583
  await Promise.allSettled(this.migrationRuns.values());
7315
7584
  await Promise.allSettled(this.derivationRuns.values());
7316
7585
  await Promise.allSettled(this.externalImportRuns.values());
7317
7586
  await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
7587
+ if (this.profileStorage) await this.profileStorage.close();
7318
7588
  this.sessionsById.clear();
7319
7589
  if (flushError !== void 0) throw flushError;
7320
7590
  }
@@ -8539,6 +8809,28 @@ function sessionOf(exec) {
8539
8809
  return exec.agent.session;
8540
8810
  }
8541
8811
  function registerMemoryTools(ctx, runtime) {
8812
+ ctx.tools.register(defineTool({
8813
+ 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.
8815
+
8816
+ 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
+
8818
+ When the user explicitly asks to make a persistent change, that request is already authorization and the tool may be called immediately.
8819
+
8820
+ 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.
8821
+
8822
+ 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
+ parameters: {
8824
+ field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
8825
+ value: { type: "string", required: true }
8826
+ },
8827
+ output: jsonOutput,
8828
+ execute: async (args, exec) => {
8829
+ if (Object.keys(args).some((key) => key !== "field" && key !== "value")) throw new TypeError("Unknown Profile update argument");
8830
+ sessionOf(exec);
8831
+ return runtime.updatePersistentProfileFromTool(args.field, args.value);
8832
+ }
8833
+ }));
8542
8834
  ctx.tools.register(defineTool({
8543
8835
  name: "feedback_prepare",
8544
8836
  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.',
@@ -8858,11 +9150,11 @@ function installedPackageVersion(names) {
8858
9150
  }
8859
9151
  return "unknown";
8860
9152
  }
8861
- function sendJson(res, status, body) {
9153
+ function sendJson(res, status, body, preserveProfileText = false) {
8862
9154
  res.statusCode = status;
8863
9155
  res.setHeader("Content-Type", "application/json; charset=utf-8");
8864
9156
  res.setHeader("Cache-Control", "no-store");
8865
- res.end(JSON.stringify(redactValue(body)));
9157
+ res.end(JSON.stringify(preserveProfileText ? body : redactValue(body)));
8866
9158
  }
8867
9159
  function numeric(value, fallback, minimum, maximum) {
8868
9160
  const parsed = Number(value);
@@ -9286,6 +9578,41 @@ async function updateSettings(runtime, url) {
9286
9578
  if (lambda !== void 0) result.blockDecayLambda = await runtime.adminSetBlockDecayLambda(lambda);
9287
9579
  return result;
9288
9580
  }
9581
+ async function persistentProfile(runtime, req) {
9582
+ if (req.method === "GET") return runtime.getPersistentProfile();
9583
+ if (req.method !== "PATCH") throw new AdminHttpError(405, "Persistent Profile requires GET or PATCH");
9584
+ let suppliedBody = req.body;
9585
+ if (suppliedBody === void 0 && typeof req[Symbol.asyncIterator] === "function") {
9586
+ const chunks = [];
9587
+ let size = 0;
9588
+ for await (const chunk of req) {
9589
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
9590
+ size += value.length;
9591
+ if (size > 16 * 1024) throw new AdminHttpError(413, "Profile update exceeds 16 KB");
9592
+ chunks.push(value);
9593
+ }
9594
+ suppliedBody = Buffer.concat(chunks).toString("utf8");
9595
+ }
9596
+ let body = suppliedBody;
9597
+ if (typeof body === "string") {
9598
+ try {
9599
+ body = JSON.parse(body);
9600
+ } catch {
9601
+ throw new AdminHttpError(400, "Profile update must be valid JSON");
9602
+ }
9603
+ }
9604
+ if (!body || typeof body !== "object" || Array.isArray(body)) throw new AdminHttpError(400, "Profile update must be an object");
9605
+ 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");
9608
+ }
9609
+ try {
9610
+ return runtime.updatePersistentProfile(input.field, input.value, "settings");
9611
+ } catch (error) {
9612
+ if (error instanceof TypeError || error instanceof RangeError) throw new AdminHttpError(400, error.message);
9613
+ throw error;
9614
+ }
9615
+ }
9289
9616
  async function feedback(runtime, req, url) {
9290
9617
  if (req.method === "GET") {
9291
9618
  const namespace2 = url.searchParams.get("namespace")?.trim() ?? "";
@@ -9936,6 +10263,8 @@ async function handleAdminRequest(runtime, req, res) {
9936
10263
  const path = url.pathname.replace(/\/$/, "");
9937
10264
  if (path === "/api/stratagate/feedback") {
9938
10265
  sendJson(res, 200, await feedback(runtime, req, url));
10266
+ } else if (path === "/api/stratagate/profile") {
10267
+ sendJson(res, 200, await persistentProfile(runtime, req), true);
9939
10268
  } else if (path === "/api/stratagate/settings") {
9940
10269
  if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
9941
10270
  sendJson(res, 200, await updateSettings(runtime, url));
@@ -10373,9 +10702,16 @@ async function apply(ctx, config) {
10373
10702
  ctx.systemPrompt.section({ name: "tool:stratagate-feedback", order: 114, text: FEEDBACK_PROTOCOL });
10374
10703
  ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
10375
10704
  const assembled = await next();
10376
- const session = context.agent?.session;
10377
- if (!session) return assembled;
10378
10705
  const contexts = [...assembled.contexts];
10706
+ try {
10707
+ const profile = runtime.renderProfileContext();
10708
+ if (profile) contexts.push({ name: "stratagate:persistent-profile", text: profile });
10709
+ } catch (error) {
10710
+ ctx.logger.warn(`stratagate-memory profile context failed: ${renderError(error)}`);
10711
+ throw error;
10712
+ }
10713
+ const session = context.agent?.session;
10714
+ if (!session) return { ...assembled, contexts };
10379
10715
  try {
10380
10716
  const text3 = await runtime.buildAutoContext(session);
10381
10717
  contexts.push({ name: "stratagate:auto-memory", text: text3 });