stratagate-dsh 0.2.81 → 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/CHANGELOG.md +5 -0
- package/dist/client.js +2 -2
- package/dist/plugin.js +342 -14
- package/dist/plugin.js.map +1 -1
- package/docs/DSH.md +5 -2
- package/docs/DSH.zh-CN.md +5 -2
- package/package.json +1 -1
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 =
|
|
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() {
|
|
@@ -5531,7 +5721,8 @@ var STRUCTURED_FIELDS = {
|
|
|
5531
5721
|
projector: ["reason", "changes"],
|
|
5532
5722
|
graphProjector: ["reason", "nodes", "edges"],
|
|
5533
5723
|
externalMemoryExtractor: ["reason", "candidates"],
|
|
5534
|
-
externalMemoryDecider: ["action", "reason", "confidence"]
|
|
5724
|
+
externalMemoryDecider: ["action", "reason", "confidence"],
|
|
5725
|
+
profileMaintenance: Object.keys(PROFILE_FIELDS)
|
|
5535
5726
|
};
|
|
5536
5727
|
var STRING_ARRAY = { type: "array", items: { type: "string" } };
|
|
5537
5728
|
var OPEN_OBJECT = { type: "object", additionalProperties: true };
|
|
@@ -5665,6 +5856,9 @@ var EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS = {
|
|
|
5665
5856
|
reason: { type: "string", required: true },
|
|
5666
5857
|
candidates: { type: "array", items: OPEN_OBJECT, required: true }
|
|
5667
5858
|
};
|
|
5859
|
+
var PROFILE_MAINTENANCE_PARAMETERS = Object.fromEntries(
|
|
5860
|
+
Object.keys(PROFILE_FIELDS).map((field) => [field, { type: "string", required: true }])
|
|
5861
|
+
);
|
|
5668
5862
|
var STRUCTURED_TOOLS = {
|
|
5669
5863
|
summarizer: {
|
|
5670
5864
|
name: "stratagate_summarize_block",
|
|
@@ -5695,6 +5889,11 @@ var STRUCTURED_TOOLS = {
|
|
|
5695
5889
|
name: "stratagate_recover_external_memory",
|
|
5696
5890
|
description: "Recover structured external-memory candidates from malformed JSON or plain text.",
|
|
5697
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
|
|
5698
5897
|
}
|
|
5699
5898
|
};
|
|
5700
5899
|
function toolSchema(kind) {
|
|
@@ -5987,6 +6186,22 @@ var DshModelBridge = class {
|
|
|
5987
6186
|
}));
|
|
5988
6187
|
return { candidates: parsed.candidates, reason: text2(raw.reason, parsed.reason) };
|
|
5989
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
|
+
}
|
|
5990
6205
|
async callStructured(kind, system, payload) {
|
|
5991
6206
|
const execution = this.sessions.getStore();
|
|
5992
6207
|
if (!execution) throw new Error("StrataGate model callback ran without an execution context");
|
|
@@ -6020,7 +6235,7 @@ ${JSON_RETRY_INSTRUCTION}`,
|
|
|
6020
6235
|
type: "function",
|
|
6021
6236
|
function: { name: STRUCTURED_TOOLS[kind].name }
|
|
6022
6237
|
},
|
|
6023
|
-
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,
|
|
6024
6239
|
sessionId: execution.sessionId,
|
|
6025
6240
|
purpose: "compaction"
|
|
6026
6241
|
};
|
|
@@ -6103,13 +6318,15 @@ ${JSON_RETRY_INSTRUCTION}`,
|
|
|
6103
6318
|
);
|
|
6104
6319
|
}
|
|
6105
6320
|
}
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
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
|
+
}
|
|
6113
6330
|
return parsed;
|
|
6114
6331
|
} catch (error) {
|
|
6115
6332
|
if (!(error instanceof ModelJsonResponseError)) throw error;
|
|
@@ -6675,6 +6892,7 @@ var StrataGateRuntime = class {
|
|
|
6675
6892
|
flushNativeSession;
|
|
6676
6893
|
feedbackOrigin;
|
|
6677
6894
|
openPath;
|
|
6895
|
+
profileStorage;
|
|
6678
6896
|
folder = new TurnFolder();
|
|
6679
6897
|
spaces = /* @__PURE__ */ new Map();
|
|
6680
6898
|
batches = /* @__PURE__ */ new Map();
|
|
@@ -6701,6 +6919,7 @@ var StrataGateRuntime = class {
|
|
|
6701
6919
|
backgroundWorkerTimer;
|
|
6702
6920
|
backgroundWorkerRun;
|
|
6703
6921
|
backgroundWorkerWakePending = false;
|
|
6922
|
+
profileMaintenanceRun;
|
|
6704
6923
|
disposeAdaptersUpdated;
|
|
6705
6924
|
settingsTail = Promise.resolve();
|
|
6706
6925
|
batchSequence = 0;
|
|
@@ -6726,6 +6945,7 @@ var StrataGateRuntime = class {
|
|
|
6726
6945
|
}
|
|
6727
6946
|
async runBackgroundWorker() {
|
|
6728
6947
|
if (this.closed || !this.models.isReady()) return;
|
|
6948
|
+
void this.runProfileMaintenance().catch((error) => this.onIngestError(error));
|
|
6729
6949
|
let namespaces;
|
|
6730
6950
|
try {
|
|
6731
6951
|
namespaces = await this.adminNamespaces();
|
|
@@ -6742,6 +6962,46 @@ var StrataGateRuntime = class {
|
|
|
6742
6962
|
}
|
|
6743
6963
|
}
|
|
6744
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
|
+
}
|
|
6745
7005
|
async runBackgroundNamespace(namespace) {
|
|
6746
7006
|
if (this.closed || !this.models.isReady()) return;
|
|
6747
7007
|
const existing = this.backgroundNamespaceRuns.get(namespace);
|
|
@@ -7319,10 +7579,12 @@ var StrataGateRuntime = class {
|
|
|
7319
7579
|
const settled = await Promise.allSettled(this.spaces.values());
|
|
7320
7580
|
await Promise.allSettled(this.backgroundNamespaceRuns.values());
|
|
7321
7581
|
if (this.backgroundWorkerRun) await Promise.allSettled([this.backgroundWorkerRun]);
|
|
7582
|
+
if (this.profileMaintenanceRun) await Promise.allSettled([this.profileMaintenanceRun]);
|
|
7322
7583
|
await Promise.allSettled(this.migrationRuns.values());
|
|
7323
7584
|
await Promise.allSettled(this.derivationRuns.values());
|
|
7324
7585
|
await Promise.allSettled(this.externalImportRuns.values());
|
|
7325
7586
|
await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
|
|
7587
|
+
if (this.profileStorage) await this.profileStorage.close();
|
|
7326
7588
|
this.sessionsById.clear();
|
|
7327
7589
|
if (flushError !== void 0) throw flushError;
|
|
7328
7590
|
}
|
|
@@ -8547,6 +8809,28 @@ function sessionOf(exec) {
|
|
|
8547
8809
|
return exec.agent.session;
|
|
8548
8810
|
}
|
|
8549
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
|
+
}));
|
|
8550
8834
|
ctx.tools.register(defineTool({
|
|
8551
8835
|
name: "feedback_prepare",
|
|
8552
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.',
|
|
@@ -8866,11 +9150,11 @@ function installedPackageVersion(names) {
|
|
|
8866
9150
|
}
|
|
8867
9151
|
return "unknown";
|
|
8868
9152
|
}
|
|
8869
|
-
function sendJson(res, status, body) {
|
|
9153
|
+
function sendJson(res, status, body, preserveProfileText = false) {
|
|
8870
9154
|
res.statusCode = status;
|
|
8871
9155
|
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
8872
9156
|
res.setHeader("Cache-Control", "no-store");
|
|
8873
|
-
res.end(JSON.stringify(redactValue(body)));
|
|
9157
|
+
res.end(JSON.stringify(preserveProfileText ? body : redactValue(body)));
|
|
8874
9158
|
}
|
|
8875
9159
|
function numeric(value, fallback, minimum, maximum) {
|
|
8876
9160
|
const parsed = Number(value);
|
|
@@ -9294,6 +9578,41 @@ async function updateSettings(runtime, url) {
|
|
|
9294
9578
|
if (lambda !== void 0) result.blockDecayLambda = await runtime.adminSetBlockDecayLambda(lambda);
|
|
9295
9579
|
return result;
|
|
9296
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
|
+
}
|
|
9297
9616
|
async function feedback(runtime, req, url) {
|
|
9298
9617
|
if (req.method === "GET") {
|
|
9299
9618
|
const namespace2 = url.searchParams.get("namespace")?.trim() ?? "";
|
|
@@ -9944,6 +10263,8 @@ async function handleAdminRequest(runtime, req, res) {
|
|
|
9944
10263
|
const path = url.pathname.replace(/\/$/, "");
|
|
9945
10264
|
if (path === "/api/stratagate/feedback") {
|
|
9946
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);
|
|
9947
10268
|
} else if (path === "/api/stratagate/settings") {
|
|
9948
10269
|
if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate settings require PATCH");
|
|
9949
10270
|
sendJson(res, 200, await updateSettings(runtime, url));
|
|
@@ -10381,9 +10702,16 @@ async function apply(ctx, config) {
|
|
|
10381
10702
|
ctx.systemPrompt.section({ name: "tool:stratagate-feedback", order: 114, text: FEEDBACK_PROTOCOL });
|
|
10382
10703
|
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
10383
10704
|
const assembled = await next();
|
|
10384
|
-
const session = context.agent?.session;
|
|
10385
|
-
if (!session) return assembled;
|
|
10386
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 };
|
|
10387
10715
|
try {
|
|
10388
10716
|
const text3 = await runtime.buildAutoContext(session);
|
|
10389
10717
|
contexts.push({ name: "stratagate:auto-memory", text: text3 });
|