dsh-strata-sync 0.0.0-stage → 0.8.1
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/LICENSE +21 -0
- package/README.md +169 -2
- package/cordis.patch.yml +3 -0
- package/dist/client.js +537 -0
- package/dist/core.mjs +285 -0
- package/dist/index.js +538 -0
- package/dist/summary.mjs +96 -0
- package/docs/CAPABILITIES.md +47 -0
- package/docs/README.zh-CN.md +144 -0
- package/docs/README.zh-TW.md +144 -0
- package/docs/VALIDATION.md +122 -0
- package/package.json +73 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import z from '@deepseek-ai/schemastery';
|
|
5
|
+
import { discoverStrata, planBudget, StrataSyncError } from './core.mjs';
|
|
6
|
+
import { installSummaryGuidance } from './summary.mjs';
|
|
7
|
+
|
|
8
|
+
export const inject = ['configEditor', 'connection', 'agents', 'credentials', 'agentPresets'];
|
|
9
|
+
export const Config = z.object({
|
|
10
|
+
providerId: z.string().default(''),
|
|
11
|
+
autoSync: z.boolean().default(false),
|
|
12
|
+
followLoadedModel: z.boolean().default(true),
|
|
13
|
+
outputTokens: z.number().step(1).min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const PACKAGE = 'dsh-strata-sync';
|
|
17
|
+
const COMPACTION = '@deepseek-ai/dsh-compaction-basic';
|
|
18
|
+
const PRESET = '@deepseek-ai/dsh-agent-preset';
|
|
19
|
+
const CHANNEL = '/api/strata-sync';
|
|
20
|
+
// Survives plugin hot reloads, but not a new DSH host process. One completed attempt per profile.
|
|
21
|
+
const startupKey = Symbol.for('dsh-strata-sync/startup');
|
|
22
|
+
const startupProfiles = globalThis[startupKey] ??= new Map();
|
|
23
|
+
const clone = (value) => structuredClone(value);
|
|
24
|
+
const hash = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
25
|
+
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
26
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
export function capabilityChanges(capabilities, route, configured = {}) {
|
|
28
|
+
const edits = {}, warnings = [];
|
|
29
|
+
if (capabilities.input) edits.input = capabilities.input;
|
|
30
|
+
if (capabilities.reasoning) {
|
|
31
|
+
const supported = capabilities.reasoning.efforts;
|
|
32
|
+
if (route.reasoning !== undefined && !own(supported, route.reasoning)) warnings.push('REASONING_DEFAULT_CONFLICT');
|
|
33
|
+
else {
|
|
34
|
+
edits.reasoningEfforts = supported;
|
|
35
|
+
edits.compat = clone(capabilities.reasoning.compat);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { edits, warnings };
|
|
39
|
+
}
|
|
40
|
+
export function mergeCapabilityEdits(model, route, edits) {
|
|
41
|
+
const result = clone(edits);
|
|
42
|
+
if (edits.compat) result.compat = { ...model.compat, ...edits.compat,
|
|
43
|
+
chatTemplateKwargs: { ...route.compat?.chatTemplateKwargs, ...model.compat?.chatTemplateKwargs, ...edits.compat.chatTemplateKwargs } };
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function refuse(code, message) { const error = new Error(message); error.code = code; throw error; }
|
|
47
|
+
function payload(value, keys = []) {
|
|
48
|
+
if (!object(value) || Object.keys(value).some((key) => !keys.includes(key))) refuse('invalid-request', 'Invalid Strata Sync request.');
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Native DSH plugin; the server is only queried, never configured or restarted. */
|
|
53
|
+
export function apply(ctx, initialConfig) {
|
|
54
|
+
let settings = { providerId: '', autoSync: false, followLoadedModel: true, ...initialConfig };
|
|
55
|
+
let closed = false;
|
|
56
|
+
let inFlight = false;
|
|
57
|
+
let writeBarrier;
|
|
58
|
+
let releaseWrite;
|
|
59
|
+
let lastPreview = null;
|
|
60
|
+
let lastApplied = null;
|
|
61
|
+
let lastError = null;
|
|
62
|
+
let lastFingerprint = null;
|
|
63
|
+
const previewSnapshots = new WeakMap();
|
|
64
|
+
function ensureOpen() { if (closed) refuse('disposed', 'Strata Sync was disabled or reloaded; this operation was cancelled.'); }
|
|
65
|
+
|
|
66
|
+
function entry(name, id) {
|
|
67
|
+
const found = ctx.configEditor.entries().filter((row) => row.options.name === name && (id === undefined || row.options.id === id) && row.fiber?.state === 2);
|
|
68
|
+
if (found.length !== 1) refuse('unsupported-profile', `Exactly one active ${name} entry is required.`);
|
|
69
|
+
return found[0];
|
|
70
|
+
}
|
|
71
|
+
function compactionOwners() {
|
|
72
|
+
const owners = [];
|
|
73
|
+
for (const row of ctx.configEditor.entries()) {
|
|
74
|
+
if (row.fiber?.state !== 2) continue;
|
|
75
|
+
const { id, name } = row.options;
|
|
76
|
+
const config = clone(row.options.config ?? {});
|
|
77
|
+
if (name === COMPACTION) owners.push({ id, name, config, targets: [{ id, path: [], config }] });
|
|
78
|
+
if (name !== PRESET) continue;
|
|
79
|
+
const targets = [];
|
|
80
|
+
const walk = (rows, path, labels = []) => {
|
|
81
|
+
if (!Array.isArray(rows)) return;
|
|
82
|
+
for (const [index, child] of rows.entries()) {
|
|
83
|
+
// Do not enable disabled branches or guess unevaluated !!js expressions.
|
|
84
|
+
if (!object(child) || child.disabled) continue;
|
|
85
|
+
const childPath = [...path, index, 'config'];
|
|
86
|
+
const childLabels = [...labels, child.id ?? String(index)];
|
|
87
|
+
if (child.name === COMPACTION) targets.push({ id: `${config.id}/${childLabels.join('/')}`, path: childPath, config: clone(child.config ?? {}) });
|
|
88
|
+
if (child.group === true) walk(child.config, childPath, childLabels);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(config.plugins, ['plugins']);
|
|
92
|
+
if (targets.length) owners.push({ id, name, presetId: config.id, config, targets });
|
|
93
|
+
}
|
|
94
|
+
if (!owners.length) refuse('unsupported-profile', 'No active profile or enabled agent-preset compaction configuration is available. Enable a supported DSH agent preset first.');
|
|
95
|
+
return owners;
|
|
96
|
+
}
|
|
97
|
+
function snapshot() {
|
|
98
|
+
refreshSettings();
|
|
99
|
+
const llmEntry = entry('@deepseek-ai/dsh-llm-pi-ai');
|
|
100
|
+
const owners = compactionOwners();
|
|
101
|
+
const defaultEntry = settings.followLoadedModel ? entry('@deepseek-ai/dsh-agent-default-model') : null;
|
|
102
|
+
return { llmEntry, owners, defaultEntry,
|
|
103
|
+
llm: clone(llmEntry.options.config ?? {}),
|
|
104
|
+
defaults: defaultEntry ? clone(defaultEntry.options.config ?? {}) : null,
|
|
105
|
+
settings: publicSettings() };
|
|
106
|
+
}
|
|
107
|
+
function snapshotRevision(value) {
|
|
108
|
+
return hash({ llmId: value.llmEntry.options.id, llm: value.llm, owners: value.owners,
|
|
109
|
+
defaultId: value.defaultEntry?.options.id ?? null, defaults: value.defaults, settings: value.settings });
|
|
110
|
+
}
|
|
111
|
+
function assertSnapshot(before) {
|
|
112
|
+
if (snapshotRevision(before) !== snapshotRevision(snapshot())) {
|
|
113
|
+
refuse('stale-preview', 'The DSH settings changed during discovery. Review a fresh preview.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function checkPresets(owners) {
|
|
117
|
+
for (const owner of owners) {
|
|
118
|
+
if (!owner.presetId) continue;
|
|
119
|
+
const state = await ctx.agentPresets.resolve(owner.presetId);
|
|
120
|
+
ensureOpen();
|
|
121
|
+
if (state.broken) refuse('preset-unavailable', `The DSH agent preset ${owner.presetId} could not activate. Fix its configuration in DSH Settings before syncing.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function providerChoice(providers, requested = settings.providerId) {
|
|
125
|
+
if (requested) {
|
|
126
|
+
if (typeof requested !== 'string' || !own(providers, requested)) refuse('provider-missing', 'Choose an existing Strata provider in DSH Models settings.');
|
|
127
|
+
return requested;
|
|
128
|
+
}
|
|
129
|
+
const candidates = Object.keys(providers).filter((id) => /strata/i.test(id));
|
|
130
|
+
if (candidates.length !== 1) refuse('provider-required', 'Choose which existing provider connects to Strata Server.');
|
|
131
|
+
return candidates[0];
|
|
132
|
+
}
|
|
133
|
+
function busy() {
|
|
134
|
+
// DSH 0.2.1-rc.3 reports maintenance as idle while manual compaction can still run.
|
|
135
|
+
return ctx.agents.list().some((agent) => agent.status !== 'idle' || agent.phase?.kind === 'maintenance');
|
|
136
|
+
}
|
|
137
|
+
function refreshSettings() {
|
|
138
|
+
settings = { providerId: '', autoSync: false, followLoadedModel: true, ...entry(PACKAGE).options.config };
|
|
139
|
+
}
|
|
140
|
+
function publicSettings(value = settings) {
|
|
141
|
+
return { providerId: value.providerId ?? '', autoSync: value.autoSync ?? false,
|
|
142
|
+
followLoadedModel: value.followLoadedModel ?? true, outputTokens: value.outputTokens ?? null };
|
|
143
|
+
}
|
|
144
|
+
function status() {
|
|
145
|
+
refreshSettings();
|
|
146
|
+
let providers = [];
|
|
147
|
+
try {
|
|
148
|
+
const rows = entry('@deepseek-ai/dsh-llm-pi-ai').options.config?.providers ?? {};
|
|
149
|
+
providers = Object.entries(rows).map(([id, route]) => {
|
|
150
|
+
let baseURL = '';
|
|
151
|
+
try { const url = new URL(route.baseURL); if (!url.username && !url.password && !url.search) baseURL = url.href; } catch {}
|
|
152
|
+
return { id, baseURL };
|
|
153
|
+
});
|
|
154
|
+
} catch { /* The page remains usable when the required provider is absent. */ }
|
|
155
|
+
return { ...publicSettings(), settingsRevision: hash(publicSettings()),
|
|
156
|
+
busy: busy() || !!writeBarrier, providers, lastPreview, lastApplied, lastError };
|
|
157
|
+
}
|
|
158
|
+
async function credential(providerId, route) {
|
|
159
|
+
if (route.apiKeyEnv) {
|
|
160
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(route.apiKeyEnv)) refuse('credential-unavailable', 'The provider credential reference is invalid.');
|
|
161
|
+
const resolved = await ctx.credentials.resolve(route.apiKeyEnv);
|
|
162
|
+
if (!resolved?.value) refuse('credential-unavailable', 'The existing provider credential is unavailable. Reconnect it in DSH Models settings.');
|
|
163
|
+
return resolved.value;
|
|
164
|
+
}
|
|
165
|
+
if (/^[a-z][a-z0-9-]*$/.test(providerId)) {
|
|
166
|
+
const record = await ctx.credentials.readRecord(`llm-pi-ai/${providerId}`);
|
|
167
|
+
if (record?.kind === 'api-key' && typeof record.key === 'string') return record.key;
|
|
168
|
+
if (record?.kind === 'grant') refuse('credential-unavailable', 'This plugin supports API-key Strata connections, not OAuth grants.');
|
|
169
|
+
}
|
|
170
|
+
return undefined; // Unauthenticated local Strata servers are also supported.
|
|
171
|
+
}
|
|
172
|
+
async function summaryContext(options) {
|
|
173
|
+
ensureOpen();
|
|
174
|
+
refreshSettings();
|
|
175
|
+
const routes = entry('@deepseek-ai/dsh-llm-pi-ai').options.config?.providers ?? {};
|
|
176
|
+
const providerId = providerChoice(routes);
|
|
177
|
+
if (options.provider !== providerId) return null;
|
|
178
|
+
const route = clone(routes[providerId]);
|
|
179
|
+
if (route.api && route.api !== 'openai-completions') return null;
|
|
180
|
+
if (!Array.isArray(route.models) || Object.keys(route.modelOverrides ?? {}).length) return null;
|
|
181
|
+
const model = route.models.find(row => row.id === options.model);
|
|
182
|
+
if (!Number.isSafeInteger(model?.contextWindow) || model.contextWindow < 1) return null;
|
|
183
|
+
const revision = hash({ route, settings: publicSettings() });
|
|
184
|
+
const apiKey = await credential(providerId, route);
|
|
185
|
+
ensureOpen();
|
|
186
|
+
const discovered = await discoverStrata(route.baseURL, apiKey, {
|
|
187
|
+
modelId: options.model, timeout: 2000, signal: options.signal,
|
|
188
|
+
});
|
|
189
|
+
ensureOpen();
|
|
190
|
+
refreshSettings();
|
|
191
|
+
const current = entry('@deepseek-ai/dsh-llm-pi-ai').options.config?.providers ?? {};
|
|
192
|
+
if (hash({ route: current[providerId], settings: publicSettings() }) !== revision || !discovered.verifiedServer) return null;
|
|
193
|
+
// A server/client mismatch must not enlarge the target past either capacity.
|
|
194
|
+
return Math.min(discovered.model.contextWindow, model.contextWindow);
|
|
195
|
+
}
|
|
196
|
+
installSummaryGuidance(ctx, summaryContext);
|
|
197
|
+
async function preview(request = {}) {
|
|
198
|
+
ensureOpen();
|
|
199
|
+
payload(request, ['providerId', 'outputTokens']);
|
|
200
|
+
const before = snapshot();
|
|
201
|
+
await checkPresets(before.owners);
|
|
202
|
+
ensureOpen();
|
|
203
|
+
const providerId = providerChoice(before.llm.providers ?? {}, request.providerId);
|
|
204
|
+
const route = before.llm.providers[providerId];
|
|
205
|
+
if (route.api && route.api !== 'openai-completions') refuse('unsupported-provider', 'Strata Sync currently supports the openai-completions adapter.');
|
|
206
|
+
if (!Array.isArray(route.models) || route.models.length === 0 || Object.keys(route.modelOverrides ?? {}).length) {
|
|
207
|
+
refuse('unsupported-route', 'Use a dedicated Strata provider with an explicit models list and no modelOverrides. Catalog-backed routes are not supported for synchronization.');
|
|
208
|
+
}
|
|
209
|
+
const outputTokens = request.outputTokens === null ? undefined : request.outputTokens ?? settings.outputTokens;
|
|
210
|
+
if (outputTokens !== undefined && (!Number.isSafeInteger(outputTokens) || outputTokens < 1)) refuse('invalid-budget', 'Output tokens must be a positive integer.');
|
|
211
|
+
const apiKey = await credential(providerId, route);
|
|
212
|
+
ensureOpen();
|
|
213
|
+
const discovered = await discoverStrata(route.baseURL, apiKey);
|
|
214
|
+
ensureOpen();
|
|
215
|
+
assertSnapshot(before);
|
|
216
|
+
const budget = planBudget(discovered.model.contextWindow, { outputTokens, serverOutputTokens: discovered.model.serverOutputTokens });
|
|
217
|
+
const configured = (route.models ?? []).find((model) => model.id === discovered.model.id);
|
|
218
|
+
const capabilities = discovered.model.capabilities;
|
|
219
|
+
const { edits: capabilityEdits, warnings: capabilityWarnings } = capabilityChanges(capabilities, route, configured);
|
|
220
|
+
const capabilityStatus = { input: capabilityEdits.input ? 'confirmed' : 'preserved',
|
|
221
|
+
reasoning: capabilityEdits.reasoningEfforts ? 'confirmed' : 'preserved' };
|
|
222
|
+
discovered.warnings.push(...capabilityWarnings);
|
|
223
|
+
const compactions = before.owners.flatMap((owner) => owner.targets.map((target) => {
|
|
224
|
+
const base = target.config;
|
|
225
|
+
const policy = (base.modelPolicies ?? []).find((row) => row.provider === providerId && row.model === discovered.model.id);
|
|
226
|
+
return { target: target.id, presetId: owner.presetId ?? null, current: {
|
|
227
|
+
headroomTokens: policy?.headroomTokens ?? base.headroomTokens ?? 65536,
|
|
228
|
+
summaryMaxTokens: policy?.maxTokens ?? base.maxTokens ?? policy?.headroomTokens ?? base.headroomTokens ?? 65536,
|
|
229
|
+
thresholdRatio: policy?.thresholdRatio ?? base.thresholdRatio ?? 0.8,
|
|
230
|
+
retainRatio: policy?.retainTokens !== undefined ? null : policy?.retainRatio ?? (base.retainTokens !== undefined ? null : base.retainRatio ?? 0.16),
|
|
231
|
+
retainTokens: policy?.retainRatio !== undefined ? null : policy?.retainTokens ?? base.retainTokens ?? null,
|
|
232
|
+
} };
|
|
233
|
+
}));
|
|
234
|
+
const current = { model: before.defaults?.provider === providerId ? before.defaults.model : null,
|
|
235
|
+
contextWindow: configured?.contextWindow ?? null, outputTokens: configured?.maxTokens ?? null };
|
|
236
|
+
const desired = {
|
|
237
|
+
...(settings.followLoadedModel ? { model: discovered.model.id } : {}),
|
|
238
|
+
contextWindow: budget.contextWindow, outputTokens: budget.outputTokens,
|
|
239
|
+
};
|
|
240
|
+
const changes = Object.entries(desired).filter(([field, after]) => current[field] !== after).map(([field, after]) => ({ field, before: current[field] ?? null, after }));
|
|
241
|
+
for (const [field, after] of Object.entries(capabilityEdits)) {
|
|
242
|
+
let previous = configured?.[field];
|
|
243
|
+
if (field === 'compat') previous = { thinkingFormat: configured?.compat?.thinkingFormat,
|
|
244
|
+
supportsReasoningEffort: configured?.compat?.supportsReasoningEffort,
|
|
245
|
+
chatTemplateKwargs: Object.fromEntries(Object.keys(after.chatTemplateKwargs).map(key=>[key,configured?.compat?.chatTemplateKwargs?.[key]])) };
|
|
246
|
+
if (JSON.stringify(previous) !== JSON.stringify(after)) changes.push({ field,
|
|
247
|
+
before: field === 'compat' ? 'Existing request mapping' : previous ?? null, after });
|
|
248
|
+
}
|
|
249
|
+
const compactionDesired = { headroomTokens: budget.headroomTokens, summaryMaxTokens: budget.summaryMaxTokens,
|
|
250
|
+
thresholdRatio: budget.thresholdRatio, retainRatio: budget.retainRatio, retainTokens: null };
|
|
251
|
+
for (const state of compactions) for (const [field, after] of Object.entries(compactionDesired)) {
|
|
252
|
+
if (state.current[field] !== after) changes.push({ target: state.target, field, before: state.current[field], after });
|
|
253
|
+
}
|
|
254
|
+
// A mixed value is intentionally null; per-target values and differences remain explicit.
|
|
255
|
+
for (const field of Object.keys(compactionDesired)) {
|
|
256
|
+
const values = compactions.map((state) => state.current[field]);
|
|
257
|
+
current[field] = values.every((value) => value === values[0]) ? values[0] : null;
|
|
258
|
+
}
|
|
259
|
+
// Fingerprint includes private config only as a one-way digest; no secret crosses the UI boundary.
|
|
260
|
+
const fingerprint = hash({ providerId, model: discovered.model, budget, profileRevision: snapshotRevision(before) });
|
|
261
|
+
const warnings = [...discovered.warnings ?? [], ...(before.owners.some((owner) => owner.targets.some((target) => target.config.auto === false)) ? ['DSH_AUTO_COMPACTION_DISABLED'] : [])];
|
|
262
|
+
const presetIds = before.owners.flatMap((owner) => owner.presetId ? [owner.presetId] : []);
|
|
263
|
+
const view = { providerId, server: { baseURL: discovered.baseURL, model: discovered.model, warnings }, budget, current, compactions, capabilityEdits, capabilityStatus,
|
|
264
|
+
presetIds, needsNewConversation: presetIds.length > 0, changes, fingerprint, busy: busy() };
|
|
265
|
+
previewSnapshots.set(view, before);
|
|
266
|
+
lastPreview = view;
|
|
267
|
+
lastError = null;
|
|
268
|
+
return view;
|
|
269
|
+
}
|
|
270
|
+
function appliedSummary(view, backupPath, applied) {
|
|
271
|
+
return { at: new Date().toISOString(), modelId: view.server.model.id, providerId: view.providerId, backupPath, applied,
|
|
272
|
+
budget: clone(view.budget), capabilityStatus: clone(view.capabilityStatus), warnings: clone(view.server.warnings),
|
|
273
|
+
serverOutputTokens: view.server.model.serverOutputTokens ?? null, outputLimitStatus: view.server.model.outputLimitStatus,
|
|
274
|
+
needsNewConversation: view.needsNewConversation };
|
|
275
|
+
}
|
|
276
|
+
async function backup() {
|
|
277
|
+
ensureOpen();
|
|
278
|
+
const documentPath = ctx.configEditor.documentPath;
|
|
279
|
+
const backupDir = join(dirname(documentPath), 'backups', 'strata-sync');
|
|
280
|
+
await mkdir(backupDir, { recursive: true, mode: 0o700 });
|
|
281
|
+
ensureOpen();
|
|
282
|
+
const filename = `${new Date().toISOString().replace(/[:.]/g, '-')}-${randomUUID()}.yml`;
|
|
283
|
+
const backupPath = join(backupDir, filename);
|
|
284
|
+
const contents = await readFile(documentPath);
|
|
285
|
+
ensureOpen();
|
|
286
|
+
await writeFile(backupPath, contents, { mode: 0o600, flag: 'wx' });
|
|
287
|
+
ensureOpen();
|
|
288
|
+
return backupPath;
|
|
289
|
+
}
|
|
290
|
+
async function applyPreview(request) {
|
|
291
|
+
ensureOpen();
|
|
292
|
+
payload(request, ['fingerprint', 'providerId', 'outputTokens']);
|
|
293
|
+
if (typeof request.fingerprint !== 'string' || !request.fingerprint) refuse('preview-required', 'Preview the proposed changes before applying them.');
|
|
294
|
+
if (busy()) refuse('busy', 'A DSH task is running. Wait until all tasks are idle before syncing.');
|
|
295
|
+
const view = await preview({ ...(request.providerId ? { providerId: request.providerId } : {}), ...(own(request, 'outputTokens') ? { outputTokens: request.outputTokens } : {}) });
|
|
296
|
+
ensureOpen();
|
|
297
|
+
if (view.fingerprint !== request.fingerprint) refuse('stale-preview', 'The server or DSH settings changed. Review a fresh preview.');
|
|
298
|
+
const before = previewSnapshots.get(view);
|
|
299
|
+
assertSnapshot(before);
|
|
300
|
+
if (view.changes.length === 0) {
|
|
301
|
+
lastApplied = appliedSummary(view, null, false);
|
|
302
|
+
return { applied: false, backupPath: null, preview: view };
|
|
303
|
+
}
|
|
304
|
+
if (busy()) refuse('busy', 'A DSH task started. Sync has been deferred.');
|
|
305
|
+
writeBarrier = new Promise((resolve) => { releaseWrite = resolve; });
|
|
306
|
+
let backupPath;
|
|
307
|
+
const completed = [];
|
|
308
|
+
try {
|
|
309
|
+
backupPath = await backup();
|
|
310
|
+
ensureOpen();
|
|
311
|
+
assertSnapshot(before);
|
|
312
|
+
if (busy()) refuse('busy', 'A DSH task started. Sync has been deferred.');
|
|
313
|
+
const modelId = view.server.model.id;
|
|
314
|
+
const llmNext = clone(before.llm);
|
|
315
|
+
const route = llmNext.providers[view.providerId];
|
|
316
|
+
route.models ??= [];
|
|
317
|
+
let model = route.models.find((row) => row.id === modelId);
|
|
318
|
+
if (!model) { model = { id: modelId, name: view.server.model.name ?? modelId }; route.models.push(model); }
|
|
319
|
+
Object.assign(model, { contextWindow: view.budget.contextWindow, maxTokens: view.budget.outputTokens });
|
|
320
|
+
Object.assign(model, mergeCapabilityEdits(model, route, view.capabilityEdits));
|
|
321
|
+
const edits = [
|
|
322
|
+
[before.llmEntry.options.name, before.llmEntry.options.id, before.llm, llmNext],
|
|
323
|
+
...before.owners.map((owner) => {
|
|
324
|
+
const next = clone(owner.config);
|
|
325
|
+
for (const target of owner.targets) {
|
|
326
|
+
let compaction = next;
|
|
327
|
+
for (const key of target.path) compaction = compaction[key] ??= {};
|
|
328
|
+
compaction.modelPolicies ??= [];
|
|
329
|
+
let policy = compaction.modelPolicies.find((row) => row.provider === view.providerId && row.model === modelId);
|
|
330
|
+
if (!policy) { policy = { provider: view.providerId, model: modelId }; compaction.modelPolicies.push(policy); }
|
|
331
|
+
Object.assign(policy, { headroomTokens: view.budget.headroomTokens, maxTokens: view.budget.summaryMaxTokens,
|
|
332
|
+
thresholdRatio: view.budget.thresholdRatio, retainRatio: view.budget.retainRatio });
|
|
333
|
+
delete policy.retainTokens;
|
|
334
|
+
}
|
|
335
|
+
return [owner.name, owner.id, owner.config, next];
|
|
336
|
+
}),
|
|
337
|
+
...(settings.followLoadedModel ? [[before.defaultEntry.options.name, before.defaultEntry.options.id, before.defaults, { ...before.defaults, provider: view.providerId, model: modelId }]] : []),
|
|
338
|
+
];
|
|
339
|
+
// Validate every owner before the first write; inactive placeholders are never edited.
|
|
340
|
+
for (const [name, id] of edits) entry(name, id);
|
|
341
|
+
for (const [name, id, original, next] of edits) {
|
|
342
|
+
ensureOpen();
|
|
343
|
+
if (hash(original) === hash(next)) continue;
|
|
344
|
+
await ctx.configEditor.edit(entry(name, id), (current) => {
|
|
345
|
+
ensureOpen();
|
|
346
|
+
if (busy()) refuse('busy', 'A DSH task started. Sync has been deferred.');
|
|
347
|
+
// ConfigEditor may queue this callback behind another settings edit.
|
|
348
|
+
// Recheck every owner when our first write actually begins.
|
|
349
|
+
if (completed.length === 0) assertSnapshot(before);
|
|
350
|
+
if (hash(current) !== hash(original)) refuse('concurrent-change', 'DSH settings changed during sync. Preview again.');
|
|
351
|
+
return next;
|
|
352
|
+
});
|
|
353
|
+
completed.push([name, id, original, next]);
|
|
354
|
+
if (name === PRESET) await checkPresets([{ presetId: next.id }]);
|
|
355
|
+
}
|
|
356
|
+
lastApplied = appliedSummary(view, backupPath, true);
|
|
357
|
+
lastFingerprint = view.fingerprint;
|
|
358
|
+
lastError = null;
|
|
359
|
+
return { applied: true, backupPath, preview: view };
|
|
360
|
+
} catch (error) {
|
|
361
|
+
let rollbackFailed = false;
|
|
362
|
+
for (const [name, id, original, written] of completed.reverse()) {
|
|
363
|
+
try {
|
|
364
|
+
await ctx.configEditor.edit(entry(name, id), (current) => {
|
|
365
|
+
if (hash(current) !== hash(written)) refuse('concurrent-change', 'A later settings edit prevents automatic rollback.');
|
|
366
|
+
return original;
|
|
367
|
+
});
|
|
368
|
+
} catch { rollbackFailed = true; }
|
|
369
|
+
}
|
|
370
|
+
if (rollbackFailed) refuse('rollback-required', `Sync was interrupted. Restore the saved profile backup: ${backupPath}`);
|
|
371
|
+
throw error;
|
|
372
|
+
} finally {
|
|
373
|
+
releaseWrite?.();
|
|
374
|
+
writeBarrier = undefined;
|
|
375
|
+
releaseWrite = undefined;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function configure(request) {
|
|
379
|
+
ensureOpen();
|
|
380
|
+
payload(request, ['providerId', 'autoSync', 'followLoadedModel', 'outputTokens', 'expectedRevision']);
|
|
381
|
+
refreshSettings();
|
|
382
|
+
if (typeof request.expectedRevision !== 'string' || request.expectedRevision !== hash(publicSettings())) {
|
|
383
|
+
refuse('stale-settings', 'Sync preferences changed. Reload the latest preferences before saving.');
|
|
384
|
+
}
|
|
385
|
+
if (busy()) refuse('busy', 'Wait until all DSH tasks are idle before changing sync settings.');
|
|
386
|
+
const next = { ...settings };
|
|
387
|
+
if (own(request, 'providerId')) {
|
|
388
|
+
if (typeof request.providerId !== 'string') refuse('invalid-request', 'Provider must be a string.');
|
|
389
|
+
next.providerId = request.providerId;
|
|
390
|
+
}
|
|
391
|
+
for (const key of ['autoSync', 'followLoadedModel']) {
|
|
392
|
+
if (!own(request, key)) continue;
|
|
393
|
+
if (typeof request[key] !== 'boolean') refuse('invalid-request', `${key} must be a boolean.`);
|
|
394
|
+
next[key] = request[key];
|
|
395
|
+
}
|
|
396
|
+
if (own(request, 'outputTokens')) {
|
|
397
|
+
if (request.outputTokens === null) delete next.outputTokens;
|
|
398
|
+
else if (Number.isSafeInteger(request.outputTokens) && request.outputTokens > 0) next.outputTokens = request.outputTokens;
|
|
399
|
+
else refuse('invalid-budget', 'Output tokens must be a positive integer, or null for automatic budgeting.');
|
|
400
|
+
}
|
|
401
|
+
const self = entry(PACKAGE);
|
|
402
|
+
const original = clone(self.options.config ?? {});
|
|
403
|
+
// Editing our own entry can dispose this plugin before edit() resolves.
|
|
404
|
+
// Build the response while this Cordis scope is still alive; never read its
|
|
405
|
+
// injected services after Loader replaces it.
|
|
406
|
+
const response = { ...status(), providerId: next.providerId, autoSync: next.autoSync,
|
|
407
|
+
followLoadedModel: next.followLoadedModel, outputTokens: next.outputTokens ?? null,
|
|
408
|
+
settingsRevision: hash(publicSettings(next)), lastPreview: null, lastError: null };
|
|
409
|
+
await backup();
|
|
410
|
+
ensureOpen();
|
|
411
|
+
await ctx.configEditor.edit(self, (current) => {
|
|
412
|
+
ensureOpen();
|
|
413
|
+
if (hash(current) !== hash(original) || hash(publicSettings(current)) !== request.expectedRevision) {
|
|
414
|
+
refuse('stale-settings', 'Sync preferences changed while saving. Reload the latest preferences.');
|
|
415
|
+
}
|
|
416
|
+
if (busy()) refuse('busy', 'Wait until all DSH tasks are idle before changing sync settings.');
|
|
417
|
+
return next;
|
|
418
|
+
});
|
|
419
|
+
settings = next;
|
|
420
|
+
lastPreview = null;
|
|
421
|
+
lastError = null;
|
|
422
|
+
return response;
|
|
423
|
+
}
|
|
424
|
+
function safeError(error) {
|
|
425
|
+
// Discovery errors contain fixed messages or HTTP status numbers, never response bodies.
|
|
426
|
+
if (error instanceof StrataSyncError) return { code: error.code, message: error.message, details: {} };
|
|
427
|
+
// Adapter/network exceptions may contain credential-bearing URLs or response bodies.
|
|
428
|
+
// Only messages deliberately raised here are exposed verbatim.
|
|
429
|
+
const known = ['disposed','invalid-request','invalid-budget','provider-missing','provider-required','unsupported-profile','unsupported-provider','unsupported-route','credential-unavailable','preview-required','busy','stale-preview','stale-settings','concurrent-change','rollback-required','preset-unavailable'];
|
|
430
|
+
return { code: known.includes(error?.code) ? error.code : 'sync-failed',
|
|
431
|
+
message: known.includes(error?.code) ? error.message : 'Unable to synchronize. Check the Strata endpoint, connection, API-key credential and supported model metadata.', details: {} };
|
|
432
|
+
}
|
|
433
|
+
async function run(action) {
|
|
434
|
+
ensureOpen();
|
|
435
|
+
if (inFlight) refuse('busy', 'Another sync operation is running.');
|
|
436
|
+
inFlight = true;
|
|
437
|
+
try { return await action(); } finally { inFlight = false; }
|
|
438
|
+
}
|
|
439
|
+
async function dispatch(endpoint, data) {
|
|
440
|
+
try {
|
|
441
|
+
if (endpoint === 'status') { payload(data); return { ok: true, value: status() }; }
|
|
442
|
+
const actions = { preview: () => preview(data), apply: () => applyPreview(data), configure: () => configure(data) };
|
|
443
|
+
if (!own(actions, endpoint)) refuse('invalid-request', 'Unknown Strata Sync action.');
|
|
444
|
+
return { ok: true, value: await run(actions[endpoint]) };
|
|
445
|
+
} catch (error) { lastError = safeError(error); return { ok: false, error: lastError }; }
|
|
446
|
+
}
|
|
447
|
+
// Native Connection exact routes inherit its Host/Origin/authentication fence.
|
|
448
|
+
// createSharedFetchHandler only invokes these routes for owner Peers. Keep the
|
|
449
|
+
// existing /api Typert interceptor untouched and retain the standard RPC envelope.
|
|
450
|
+
for (const endpoint of ['status', 'preview', 'apply', 'configure']) {
|
|
451
|
+
ctx.connection.fetch.register({
|
|
452
|
+
path: `${CHANNEL}/${endpoint}`, methods: ['POST'], requestBody: 'streaming',
|
|
453
|
+
fetch: async (request) => {
|
|
454
|
+
let rpcId = 'invalid-request';
|
|
455
|
+
try {
|
|
456
|
+
if (request.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase() !== 'application/json') {
|
|
457
|
+
return new Response('Expected application/json', { status: 415 });
|
|
458
|
+
}
|
|
459
|
+
const maximumBytes = 8192;
|
|
460
|
+
if (Number(request.headers.get('content-length') ?? 0) > maximumBytes) return new Response('Request too large', { status: 413 });
|
|
461
|
+
const reader = request.body?.getReader();
|
|
462
|
+
if (!reader) refuse('invalid-request', 'Missing request body.');
|
|
463
|
+
const chunks = [];
|
|
464
|
+
let size = 0;
|
|
465
|
+
try {
|
|
466
|
+
for (;;) {
|
|
467
|
+
const chunk = await reader.read();
|
|
468
|
+
if (chunk.done) break;
|
|
469
|
+
size += chunk.value.byteLength;
|
|
470
|
+
if (size > maximumBytes) { await reader.cancel(); return new Response('Request too large', { status: 413 }); }
|
|
471
|
+
chunks.push(Buffer.from(chunk.value));
|
|
472
|
+
}
|
|
473
|
+
} finally { reader.releaseLock(); }
|
|
474
|
+
let envelope;
|
|
475
|
+
try { envelope = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
|
|
476
|
+
catch { refuse('invalid-request', 'Invalid JSON request.'); }
|
|
477
|
+
payload(envelope, ['type', 'rpcId', 'method', 'payload']);
|
|
478
|
+
if (envelope.type !== 'client-request' || typeof envelope.rpcId !== 'string' || !envelope.rpcId || envelope.rpcId.length > 128
|
|
479
|
+
|| envelope.method !== `strata-sync/${endpoint}` || !own(envelope, 'payload')) refuse('invalid-request', 'Invalid RPC envelope.');
|
|
480
|
+
rpcId = envelope.rpcId;
|
|
481
|
+
return Response.json({ type: 'server-response', rpcId, result: await dispatch(endpoint, envelope.payload) });
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return Response.json({ type: 'server-response', rpcId, result: { ok: false, error: safeError(error) } }, { status: 400 });
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
ctx.on('agent/pre-step', async (_event, next) => {
|
|
489
|
+
if (writeBarrier) await writeBarrier;
|
|
490
|
+
return next();
|
|
491
|
+
}, { prepend: true });
|
|
492
|
+
const profile = ctx.configEditor.documentPath;
|
|
493
|
+
if (!startupProfiles.has(profile)) startupProfiles.set(profile, { pending: !!settings.autoSync });
|
|
494
|
+
const startup = startupProfiles.get(profile);
|
|
495
|
+
let timer;
|
|
496
|
+
const poll = async () => {
|
|
497
|
+
if (closed || !startup.pending) return;
|
|
498
|
+
try {
|
|
499
|
+
refreshSettings();
|
|
500
|
+
if (!settings.autoSync) { startup.pending = false; clearInterval(timer); return; }
|
|
501
|
+
if (busy() || inFlight || startup.running) return;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
// The host can withdraw a required entry during reload. Do not leave an
|
|
504
|
+
// unhandled timer rejection or retry an unsupported profile indefinitely.
|
|
505
|
+
lastError = safeError(error);
|
|
506
|
+
startup.pending = false;
|
|
507
|
+
clearInterval(timer);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Discovery can outlive an idle period. Busy/disposed attempts keep the one
|
|
511
|
+
// startup operation pending; successful/no-op and terminal errors consume it.
|
|
512
|
+
startup.running = true;
|
|
513
|
+
let deferred = false;
|
|
514
|
+
try {
|
|
515
|
+
await run(async () => {
|
|
516
|
+
const view = await preview();
|
|
517
|
+
if (view.changes.length && view.fingerprint !== lastFingerprint) await applyPreview({ fingerprint: view.fingerprint, providerId: view.providerId });
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
lastError = safeError(error);
|
|
521
|
+
deferred = error?.code === 'busy' || (closed && error?.code === 'disposed');
|
|
522
|
+
} finally {
|
|
523
|
+
startup.running = false;
|
|
524
|
+
if (!deferred) { startup.pending = false; clearInterval(timer); }
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
// DSH 0.2.1-rc.3 can activate us inside HMR's AsyncLocalStorage transaction.
|
|
528
|
+
// A timer otherwise inherits that transaction forever, so later ConfigEditor
|
|
529
|
+
// edits are rejected as nested HMR transactions. Mirror Hmr.watchConfig():
|
|
530
|
+
// detach only timer creation, while every write still uses ConfigEditor and
|
|
531
|
+
// its normal runExclusive queue. `executing` is a version-specific host seam.
|
|
532
|
+
// This timer only waits for startup readiness/idle; no recurring server polling.
|
|
533
|
+
const createTimer = () => setInterval(() => { void poll(); }, 1_000);
|
|
534
|
+
const executing = ctx.get('hmr')?.executing;
|
|
535
|
+
if (startup.pending) timer = typeof executing?.exit === 'function' ? executing.exit(createTimer) : createTimer();
|
|
536
|
+
timer?.unref?.();
|
|
537
|
+
ctx.effect(() => () => { closed = true; clearInterval(timer); releaseWrite?.(); }, 'strata-sync: startup lifetime');
|
|
538
|
+
}
|
package/dist/summary.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** A one-sentence, per-request supplement to DSH's native summary instruction. */
|
|
2
|
+
const NATIVE_OPENING = 'You are now acting as a compaction engine for this AI coding assistant.';
|
|
3
|
+
const NATIVE_HEADINGS = ['Primary Request and Intent', 'Key Technical Concepts', 'Files and Code',
|
|
4
|
+
'Errors and Fixes', 'Pending Jobs', 'Current Work', 'Next Step', 'Critical Context'];
|
|
5
|
+
const GUIDANCE_PREFIX = 'Keep the entire checkpoint, including all required headings, within a target of ';
|
|
6
|
+
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
7
|
+
// Eight mandatory headings need enough space for a meaningful checkpoint. Do
|
|
8
|
+
// not round a tiny target UP; leave native behavior intact instead.
|
|
9
|
+
export const MIN_SUMMARY_TARGET = 256;
|
|
10
|
+
|
|
11
|
+
export function planSummaryTarget(contextWindow, summaryMaxTokens, sourceTokens) {
|
|
12
|
+
if (![contextWindow, summaryMaxTokens, sourceTokens].every(positive)) return null;
|
|
13
|
+
const target = Math.floor(Math.min(contextWindow / 16, summaryMaxTokens / 2, sourceTokens / 2));
|
|
14
|
+
return target >= MIN_SUMMARY_TARGET ? target : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function summaryInstruction(target) {
|
|
18
|
+
if (!positive(target)) throw new TypeError('A positive integer summary target is required.');
|
|
19
|
+
return `${GUIDANCE_PREFIX}${target} tokens by retaining only information essential to continue the remaining work, consolidating prior summaries, and removing redundant or obsolete details.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Recognize the shipped native prompt, without storing or replacing its text. */
|
|
23
|
+
export function nativeSummaryMessage(options) {
|
|
24
|
+
if (options?.purpose !== 'compaction' || !Array.isArray(options.messages)) return null;
|
|
25
|
+
const message = options.messages.at(-1);
|
|
26
|
+
if (message?.role !== 'user' || message.content?.length !== 1 || message.content[0]?.type !== 'text') return null;
|
|
27
|
+
const text = message.content[0].text;
|
|
28
|
+
if (typeof text !== 'string' || !text.startsWith(NATIVE_OPENING) || text.includes(GUIDANCE_PREFIX)) return null;
|
|
29
|
+
// Normalize only the recognition view, never the user's native prompt bytes.
|
|
30
|
+
const recognition = text.replace(/\r\n/g, '\n');
|
|
31
|
+
let position = 0;
|
|
32
|
+
for (const heading of NATIVE_HEADINGS) {
|
|
33
|
+
const next = recognition.indexOf(`\n## ${heading}\n`, position);
|
|
34
|
+
if (next < position) return null;
|
|
35
|
+
position = next + heading.length + 4;
|
|
36
|
+
}
|
|
37
|
+
if (!recognition.includes('\nRules:\n') || !recognition.includes('Do not copy it forward verbatim:')) return null;
|
|
38
|
+
return message;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Match DSH's text heuristic: four characters/token plus block/role framing.
|
|
42
|
+
* Price only the selected conversation span, never the system prompt, tool
|
|
43
|
+
* schemas, or final summary instruction. Unknown attachment pricing falls
|
|
44
|
+
* back to native behavior rather than inventing a visual/file token count.
|
|
45
|
+
*/
|
|
46
|
+
export function estimateSummarySource(messages) {
|
|
47
|
+
if (!Array.isArray(messages) || messages.length < 2) return null;
|
|
48
|
+
let tokens = 0;
|
|
49
|
+
for (const message of messages.slice(0, -1)) {
|
|
50
|
+
if (message?.role === 'system') continue;
|
|
51
|
+
if (!['user', 'assistant', 'tool'].includes(message?.role) || !Array.isArray(message.content)) return null;
|
|
52
|
+
tokens += 4;
|
|
53
|
+
for (const block of message.content) {
|
|
54
|
+
if (['text', 'reasoning'].includes(block?.type) && typeof block.text === 'string') {
|
|
55
|
+
tokens += 4 + Math.ceil(block.text.length / 4);
|
|
56
|
+
} else if (block?.type === 'tool-call' && typeof block.name === 'string' && typeof block.arguments === 'string') {
|
|
57
|
+
tokens += 4 + Math.ceil(block.name.length / 4) + Math.ceil(block.arguments.length / 4);
|
|
58
|
+
} else return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return positive(tokens) ? tokens : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The native waterfall's next() closes over options, so replace only its
|
|
65
|
+
* messages property for the lifetime of this stream; never mutate frozen
|
|
66
|
+
* messages or recursively dispatch a second LLM request.
|
|
67
|
+
*/
|
|
68
|
+
export function installSummaryGuidance(ctx, resolveContext) {
|
|
69
|
+
let closed = false;
|
|
70
|
+
const remove = ctx.on('llm/stream', (options, next) => {
|
|
71
|
+
const native = nativeSummaryMessage(options);
|
|
72
|
+
const descriptor = options && Object.getOwnPropertyDescriptor(options, 'messages');
|
|
73
|
+
if (closed || !native || !descriptor?.writable || options.signal?.aborted) return next();
|
|
74
|
+
const sourceTokens = estimateSummarySource(options.messages);
|
|
75
|
+
if (!positive(sourceTokens) || !positive(options.maxTokens)) return next();
|
|
76
|
+
return (async function* () {
|
|
77
|
+
const original = options.messages;
|
|
78
|
+
let augmented;
|
|
79
|
+
try {
|
|
80
|
+
let contextWindow;
|
|
81
|
+
try { contextWindow = await resolveContext(options); }
|
|
82
|
+
catch { /* Metadata/compatibility failure must not break native inference. */ }
|
|
83
|
+
const target = planSummaryTarget(contextWindow, options.maxTokens, sourceTokens);
|
|
84
|
+
if (!closed && !options.signal?.aborted && target !== null && options.messages === original) {
|
|
85
|
+
const last = { ...native, content: [{ ...native.content[0], text: `${native.content[0].text}\n- ${summaryInstruction(target)}` }] };
|
|
86
|
+
augmented = [...original.slice(0, -1), last];
|
|
87
|
+
options.messages = augmented;
|
|
88
|
+
}
|
|
89
|
+
yield* next();
|
|
90
|
+
} finally {
|
|
91
|
+
if (augmented && options.messages === augmented) options.messages = original;
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
});
|
|
95
|
+
ctx.effect(() => () => { closed = true; remove?.(); }, 'strata-sync: summary guidance lifetime');
|
|
96
|
+
}
|