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/CHANGELOG.md +11 -0
- package/dist/client.js +7 -2
- package/dist/plugin.js +386 -14
- package/dist/plugin.js.map +1 -1
- package/docs/DSH.md +5 -2
- package/docs/DSH.zh-CN.md +5 -2
- package/docs/assets/stratagate-profile-0.2.83-conflict.png +0 -0
- package/docs/assets/stratagate-profile-0.2.83-lower.png +0 -0
- package/docs/assets/stratagate-profile-0.2.83-reasoning-conflict.png +0 -0
- package/docs/assets/stratagate-profile-0.2.83.png +0 -0
- package/package.json +3 -1
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 =
|
|
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
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
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 });
|