ei-tui 0.5.0 → 0.5.2
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/package.json +1 -1
- package/src/core/AGENTS.md +2 -2
- package/src/core/context-utils.ts +3 -4
- package/src/core/handlers/human-matching.ts +33 -21
- package/src/core/llm-client.ts +119 -39
- package/src/core/orchestrators/human-extraction.ts +0 -5
- package/src/core/processor.ts +5 -0
- package/src/core/queue-manager.ts +4 -0
- package/src/core/queue-processor.ts +1 -0
- package/src/core/state/queue.ts +7 -0
- package/src/core/state-manager.ts +233 -4
- package/src/core/tools/index.ts +1 -1
- package/src/core/types/data-items.ts +3 -1
- package/src/core/types/entities.ts +21 -4
- package/src/integrations/claude-code/importer.ts +0 -1
- package/src/integrations/claude-code/types.ts +0 -1
- package/src/integrations/opencode/importer.ts +0 -1
- package/src/storage/merge.ts +47 -2
- package/tui/src/commands/dlq.ts +12 -4
- package/tui/src/commands/provider.tsx +110 -90
- package/tui/src/commands/queue.ts +11 -3
- package/tui/src/commands/settings.tsx +9 -17
- package/tui/src/components/ModelListOverlay.tsx +203 -0
- package/tui/src/components/PromptInput.tsx +0 -2
- package/tui/src/context/ei.tsx +7 -0
- package/tui/src/util/persona-editor.tsx +15 -12
- package/tui/src/util/provider-editor.tsx +23 -6
- package/tui/src/util/yaml-serializers.ts +255 -73
- package/src/core/model-context-windows.ts +0 -49
- package/tui/src/commands/model.ts +0 -47
|
@@ -50,15 +50,20 @@ export class StateManager {
|
|
|
50
50
|
this.queueState.load(state.queue);
|
|
51
51
|
this.tools = state.tools ?? [];
|
|
52
52
|
this.providers = state.providers ?? [];
|
|
53
|
-
this.
|
|
54
|
-
this.migrateFactValidation();
|
|
55
|
-
this.migrateMessageFlags();
|
|
56
|
-
this.migrateInterestedPersonas();
|
|
53
|
+
this.runMigrations();
|
|
57
54
|
} else {
|
|
58
55
|
this.humanState.load(createDefaultHumanEntity());
|
|
59
56
|
}
|
|
60
57
|
}
|
|
61
58
|
|
|
59
|
+
private runMigrations(): void {
|
|
60
|
+
this.migrateLearnedByToIds();
|
|
61
|
+
this.migrateFactValidation();
|
|
62
|
+
this.migrateMessageFlags();
|
|
63
|
+
this.migrateInterestedPersonas();
|
|
64
|
+
this.migrateProviderModel();
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
/**
|
|
63
68
|
* Migration: learned_by used to store display names; now stores persona IDs.
|
|
64
69
|
* On load, attempt to resolve display names -> IDs using current persona map.
|
|
@@ -222,6 +227,162 @@ export class StateManager {
|
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
229
|
|
|
230
|
+
private migrateProviderModel(): void {
|
|
231
|
+
const human = this.humanState.get();
|
|
232
|
+
const settings = human.settings;
|
|
233
|
+
if (!settings?.accounts?.length) return;
|
|
234
|
+
|
|
235
|
+
const modelLookup = new Map<string, string>();
|
|
236
|
+
|
|
237
|
+
// Helper: ensure a model exists in an account and register it in modelLookup.
|
|
238
|
+
// ref must be in "ProviderName:model-name" format.
|
|
239
|
+
const ensureModelInAccount = (ref: string): void => {
|
|
240
|
+
const colonIdx = ref.indexOf(":");
|
|
241
|
+
if (colonIdx === -1) return;
|
|
242
|
+
const providerName = ref.substring(0, colonIdx);
|
|
243
|
+
const modelName = ref.substring(colonIdx + 1);
|
|
244
|
+
const account = settings.accounts!.find((a) => a.name === providerName);
|
|
245
|
+
if (!account) return;
|
|
246
|
+
|
|
247
|
+
if (!account.models) account.models = [];
|
|
248
|
+
|
|
249
|
+
const existing = account.models.find((m) => m.name === modelName);
|
|
250
|
+
if (existing) {
|
|
251
|
+
modelLookup.set(ref, existing.id);
|
|
252
|
+
} else {
|
|
253
|
+
const newModel = {
|
|
254
|
+
id: crypto.randomUUID(),
|
|
255
|
+
name: modelName,
|
|
256
|
+
};
|
|
257
|
+
account.models.push(newModel);
|
|
258
|
+
modelLookup.set(ref, newModel.id);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const isProviderRef = (val: string): boolean => {
|
|
263
|
+
const colonIdx = val.indexOf(":");
|
|
264
|
+
if (colonIdx === -1) return false;
|
|
265
|
+
const providerName = val.substring(0, colonIdx);
|
|
266
|
+
return settings.accounts!.some((a) => a.name === providerName);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// Phase 1: Collect ALL model refs from everywhere they can appear.
|
|
270
|
+
const allRefs: string[] = [];
|
|
271
|
+
const pushRef = (ref: string | undefined): void => {
|
|
272
|
+
if (ref && isProviderRef(ref)) allRefs.push(ref);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
pushRef(settings.default_model);
|
|
276
|
+
pushRef(settings.oneshot_model);
|
|
277
|
+
pushRef(settings.rewrite_model);
|
|
278
|
+
pushRef(settings.opencode?.extraction_model);
|
|
279
|
+
pushRef(settings.claudeCode?.extraction_model);
|
|
280
|
+
|
|
281
|
+
const personas = this.personaState.getAll();
|
|
282
|
+
for (const persona of personas) {
|
|
283
|
+
pushRef(persona.model);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Also include account.default_model values (legacy strings, not yet GUIDs)
|
|
287
|
+
for (const account of settings.accounts) {
|
|
288
|
+
if (account.default_model && isProviderRef(account.default_model)) {
|
|
289
|
+
allRefs.push(account.default_model);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Phase 2: For each ref, ensure model exists in the matching account.
|
|
294
|
+
for (const ref of allRefs) {
|
|
295
|
+
ensureModelInAccount(ref);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Helper: check if a value looks like a UUID (already migrated)
|
|
299
|
+
const isUUID = (val: string): boolean =>
|
|
300
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val);
|
|
301
|
+
|
|
302
|
+
// Phase 3: Ensure every account has a models array and default_model is a GUID.
|
|
303
|
+
for (const account of settings.accounts) {
|
|
304
|
+
if (!account.models) {
|
|
305
|
+
account.models = [];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Handle account.default_model - could be:
|
|
309
|
+
// 1. Already a GUID (already migrated) - leave it
|
|
310
|
+
// 2. A "Provider:model" ref - look up in modelLookup
|
|
311
|
+
// 3. A plain model name like "claude-haiku-4-5-20251001" - add to models[] and convert
|
|
312
|
+
if (account.default_model) {
|
|
313
|
+
if (isUUID(account.default_model)) {
|
|
314
|
+
// Already migrated, nothing to do
|
|
315
|
+
} else if (isProviderRef(account.default_model)) {
|
|
316
|
+
// It's a "Provider:model" ref - should be in modelLookup from Phase 2
|
|
317
|
+
const guid = modelLookup.get(account.default_model);
|
|
318
|
+
if (guid) account.default_model = guid;
|
|
319
|
+
} else {
|
|
320
|
+
// Plain model name - check if it exists in models[], add if not, convert to GUID
|
|
321
|
+
const existing = account.models.find((m) => m.name === account.default_model);
|
|
322
|
+
if (existing) {
|
|
323
|
+
account.default_model = existing.id;
|
|
324
|
+
} else {
|
|
325
|
+
const model = {
|
|
326
|
+
id: crypto.randomUUID(),
|
|
327
|
+
name: account.default_model,
|
|
328
|
+
token_limit: (account as any).token_limit as number | undefined,
|
|
329
|
+
max_output_tokens: undefined as number | undefined,
|
|
330
|
+
};
|
|
331
|
+
account.models.push(model);
|
|
332
|
+
modelLookup.set(`${account.name}:${model.name}`, model.id);
|
|
333
|
+
account.default_model = model.id;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If still no models, create a placeholder
|
|
339
|
+
if (account.models.length === 0) {
|
|
340
|
+
const model = { id: crypto.randomUUID(), name: "(default)" };
|
|
341
|
+
account.models.push(model);
|
|
342
|
+
modelLookup.set(`${account.name}:(default)`, model.id);
|
|
343
|
+
account.default_model = model.id;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
delete (account as any).token_limit;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const resolveRef = (ref: string | undefined): string | undefined => {
|
|
350
|
+
if (!ref) return ref;
|
|
351
|
+
const guid = modelLookup.get(ref);
|
|
352
|
+
if (guid) return guid;
|
|
353
|
+
const colonIdx = ref.indexOf(":");
|
|
354
|
+
if (colonIdx === -1) return ref;
|
|
355
|
+
return undefined;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
settings.default_model = resolveRef(settings.default_model);
|
|
359
|
+
settings.oneshot_model = resolveRef(settings.oneshot_model);
|
|
360
|
+
settings.rewrite_model = resolveRef(settings.rewrite_model);
|
|
361
|
+
|
|
362
|
+
if (settings.opencode) {
|
|
363
|
+
settings.opencode.extraction_model = resolveRef(settings.opencode.extraction_model);
|
|
364
|
+
delete (settings.opencode as any).extraction_token_limit;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (settings.claudeCode) {
|
|
368
|
+
settings.claudeCode.extraction_model = resolveRef(settings.claudeCode.extraction_model);
|
|
369
|
+
delete (settings.claudeCode as any).extraction_token_limit;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const persona of personas) {
|
|
373
|
+
if (persona.model) {
|
|
374
|
+
const resolved = resolveRef(persona.model);
|
|
375
|
+
const colonIdx = (resolved ?? persona.model).indexOf(":");
|
|
376
|
+
const finalModel = colonIdx !== -1 ? undefined : (resolved ?? persona.model);
|
|
377
|
+
this.personaState.update(persona.id, { model: finalModel });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.humanState.set(human);
|
|
382
|
+
this.scheduleSave();
|
|
383
|
+
console.log("[StateManager] Migrated provider/model references to GUID-based ModelConfig system");
|
|
384
|
+
}
|
|
385
|
+
|
|
225
386
|
/**
|
|
226
387
|
* Returns true if value looks like a persona ID (UUID or the special "ei" id).
|
|
227
388
|
* Display names are free-form strings that won't match UUID format.
|
|
@@ -621,6 +782,12 @@ export class StateManager {
|
|
|
621
782
|
return result;
|
|
622
783
|
}
|
|
623
784
|
|
|
785
|
+
queue_deleteItems(ids: string[]): number {
|
|
786
|
+
const result = this.queueState.deleteItems(ids);
|
|
787
|
+
if (result > 0) this.scheduleSave();
|
|
788
|
+
return result;
|
|
789
|
+
}
|
|
790
|
+
|
|
624
791
|
queue_dlqLength(): number {
|
|
625
792
|
return this.queueState.dlqLength();
|
|
626
793
|
}
|
|
@@ -764,6 +931,67 @@ export class StateManager {
|
|
|
764
931
|
return result;
|
|
765
932
|
}
|
|
766
933
|
|
|
934
|
+
deleteModel(providerId: string, modelId: string): { success: boolean; error?: string; cleared: string[] } {
|
|
935
|
+
const human = this.humanState.get();
|
|
936
|
+
const settings = human.settings;
|
|
937
|
+
if (!settings?.accounts?.length) {
|
|
938
|
+
return { success: false, error: `Provider not found: ${providerId}`, cleared: [] };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const provider = settings.accounts.find(a => a.id === providerId);
|
|
942
|
+
if (!provider) {
|
|
943
|
+
return { success: false, error: `Provider not found: ${providerId}`, cleared: [] };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (!provider.models?.find(m => m.id === modelId)) {
|
|
947
|
+
return { success: false, error: `Model not found: ${modelId}`, cleared: [] };
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if ((provider.models?.length ?? 0) <= 1) {
|
|
951
|
+
return { success: false, error: `Cannot delete the last model on a provider`, cleared: [] };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const cleared: string[] = [];
|
|
955
|
+
|
|
956
|
+
if (settings.default_model === modelId) {
|
|
957
|
+
settings.default_model = undefined;
|
|
958
|
+
cleared.push("settings.default_model");
|
|
959
|
+
}
|
|
960
|
+
if (settings.oneshot_model === modelId) {
|
|
961
|
+
settings.oneshot_model = undefined;
|
|
962
|
+
cleared.push("settings.oneshot_model");
|
|
963
|
+
}
|
|
964
|
+
if (settings.rewrite_model === modelId) {
|
|
965
|
+
settings.rewrite_model = undefined;
|
|
966
|
+
cleared.push("settings.rewrite_model");
|
|
967
|
+
}
|
|
968
|
+
if (settings.opencode?.extraction_model === modelId) {
|
|
969
|
+
settings.opencode.extraction_model = undefined;
|
|
970
|
+
cleared.push("settings.opencode.extraction_model");
|
|
971
|
+
}
|
|
972
|
+
if (settings.claudeCode?.extraction_model === modelId) {
|
|
973
|
+
settings.claudeCode.extraction_model = undefined;
|
|
974
|
+
cleared.push("settings.claudeCode.extraction_model");
|
|
975
|
+
}
|
|
976
|
+
if (provider.default_model === modelId) {
|
|
977
|
+
provider.default_model = undefined;
|
|
978
|
+
cleared.push("provider.default_model");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
provider.models = provider.models!.filter(m => m.id !== modelId);
|
|
982
|
+
this.humanState.set(human);
|
|
983
|
+
|
|
984
|
+
for (const persona of this.personaState.getAll()) {
|
|
985
|
+
if (persona.model === modelId) {
|
|
986
|
+
this.personaState.update(persona.id, { model: undefined });
|
|
987
|
+
cleared.push(`persona:${persona.display_name}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
this.scheduleSave();
|
|
992
|
+
return { success: true, cleared };
|
|
993
|
+
}
|
|
994
|
+
|
|
767
995
|
async flush(): Promise<void> {
|
|
768
996
|
await this.persistenceState.flush();
|
|
769
997
|
}
|
|
@@ -788,6 +1016,7 @@ export class StateManager {
|
|
|
788
1016
|
this.providers = state.providers ?? [];
|
|
789
1017
|
this.tools = state.tools ?? [];
|
|
790
1018
|
this.persistenceState.markExistingData();
|
|
1019
|
+
this.runMigrations();
|
|
791
1020
|
this.scheduleSave();
|
|
792
1021
|
}
|
|
793
1022
|
|
package/src/core/tools/index.ts
CHANGED
|
@@ -67,7 +67,9 @@ export interface Quote {
|
|
|
67
67
|
data_item_ids: string[]; // FK[] to DataItemBase.id
|
|
68
68
|
persona_groups: string[]; // Visibility groups
|
|
69
69
|
text: string; // The quote content
|
|
70
|
-
speaker: "human" | string; //
|
|
70
|
+
speaker: "human" | string; // Actual speaker: "human" or the persona's display_name
|
|
71
|
+
channel?: string; // Display name of the Channel (persona or room) where captured.
|
|
72
|
+
// Undefined on pre-migration quotes.
|
|
71
73
|
timestamp: string; // ISO timestamp (from original message)
|
|
72
74
|
start: number | null; // Character offset in message (null = can't highlight)
|
|
73
75
|
end: number | null; // Character offset in message (null = can't highlight)
|
|
@@ -15,7 +15,6 @@ export interface OpenCodeSettings {
|
|
|
15
15
|
integration?: boolean;
|
|
16
16
|
polling_interval_ms?: number; // Default: 60000 (1 min)
|
|
17
17
|
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
18
|
-
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
19
18
|
last_sync?: string; // ISO timestamp
|
|
20
19
|
extraction_point?: string; // ISO timestamp - cursor for single-session archive scan
|
|
21
20
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
@@ -37,6 +36,23 @@ export interface BackupConfig {
|
|
|
37
36
|
last_backup?: string; // ISO timestamp of last backup run
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
/**
|
|
40
|
+
* ModelConfig - Configuration and usage tracking for a specific LLM model
|
|
41
|
+
*
|
|
42
|
+
* Models are first-class entities tied to a ProviderAccount. Each model has
|
|
43
|
+
* its own capability configuration and usage statistics.
|
|
44
|
+
*/
|
|
45
|
+
export interface ModelConfig {
|
|
46
|
+
id: string; // GUID (crypto.randomUUID())
|
|
47
|
+
name: string; // Model identifier, e.g. "claude-haiku-4-5", "(default)"
|
|
48
|
+
token_limit?: number; // Input token limit (user sets effective limit)
|
|
49
|
+
max_output_tokens?: number; // Output token limit (API-enforced)
|
|
50
|
+
total_calls?: number; // Usage counter
|
|
51
|
+
total_tokens_in?: number; // Usage counter
|
|
52
|
+
total_tokens_out?: number; // Usage counter
|
|
53
|
+
last_used?: string; // ISO timestamp
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
/**
|
|
41
57
|
* ProviderAccount - Configuration for external service connections
|
|
42
58
|
*
|
|
@@ -60,6 +76,7 @@ export interface ProviderAccount {
|
|
|
60
76
|
// LLM-specific
|
|
61
77
|
default_model?: string; // Default model for this account
|
|
62
78
|
token_limit?: number; // Context window override (tokens). Used for extraction chunking.
|
|
79
|
+
models?: ModelConfig[]; // First-class model registry for this account
|
|
63
80
|
|
|
64
81
|
// Provider-specific extras (e.g., OpenRouter needs HTTP-Referer, X-Title)
|
|
65
82
|
extra_headers?: Record<string, string>;
|
|
@@ -73,9 +90,9 @@ export interface ProviderAccount {
|
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
export interface HumanSettings {
|
|
76
|
-
default_model?: string;
|
|
77
|
-
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model
|
|
78
|
-
rewrite_model?: string; // Model for rewrite ceremony step; must be capable (Sonnet/Opus class). Unset = rewrite disabled.
|
|
93
|
+
default_model?: string; // Will store ModelConfig.id GUID post-migration
|
|
94
|
+
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
|
|
95
|
+
rewrite_model?: string; // Model for rewrite ceremony step; must be capable (Sonnet/Opus class). Unset = rewrite disabled. Will store ModelConfig.id GUID post-migration.
|
|
79
96
|
queue_paused?: boolean;
|
|
80
97
|
skip_quote_delete_confirm?: boolean;
|
|
81
98
|
name_display?: string;
|
|
@@ -327,7 +327,6 @@ export async function importClaudeCodeSessions(
|
|
|
327
327
|
const ccSettings = stateManager.getHuman().settings?.claudeCode;
|
|
328
328
|
queueAllScans(context, stateManager, {
|
|
329
329
|
extraction_model: ccSettings?.extraction_model,
|
|
330
|
-
extraction_token_limit: ccSettings?.extraction_token_limit,
|
|
331
330
|
external_filter: "only",
|
|
332
331
|
});
|
|
333
332
|
result.extractionScansQueued += 4;
|
|
@@ -159,7 +159,6 @@ export interface ClaudeCodeSettings {
|
|
|
159
159
|
integration?: boolean;
|
|
160
160
|
polling_interval_ms?: number; // Default: 60000 (1 min)
|
|
161
161
|
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
162
|
-
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
163
162
|
last_sync?: string; // ISO timestamp
|
|
164
163
|
extraction_point?: string; // ISO timestamp - floor cursor for processed-session skip
|
|
165
164
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
@@ -256,7 +256,6 @@ export async function importOpenCodeSessions(
|
|
|
256
256
|
const openCodeSettings = stateManager.getHuman().settings?.opencode;
|
|
257
257
|
queueAllScans(context, stateManager, {
|
|
258
258
|
extraction_model: openCodeSettings?.extraction_model,
|
|
259
|
-
extraction_token_limit: openCodeSettings?.extraction_token_limit,
|
|
260
259
|
external_filter: "only",
|
|
261
260
|
});
|
|
262
261
|
result.extractionScansQueued += 4;
|
package/src/storage/merge.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageState, DataItem, Quote, ToolProvider, ToolDefinition, ProviderAccount } from "../core/types.js";
|
|
1
|
+
import type { StorageState, DataItem, Quote, ToolProvider, ToolDefinition, ProviderAccount, ModelConfig } from "../core/types.js";
|
|
2
2
|
|
|
3
3
|
function mergeByName<T extends { name: string }>(
|
|
4
4
|
local: T[],
|
|
@@ -19,6 +19,51 @@ function mergeByName<T extends { name: string }>(
|
|
|
19
19
|
return merged;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function mergeModels(local: ModelConfig[], remote: ModelConfig[], preferRemote: boolean): ModelConfig[] {
|
|
23
|
+
const merged = [...local];
|
|
24
|
+
|
|
25
|
+
for (const remoteModel of remote) {
|
|
26
|
+
const localIndex = merged.findIndex(m => m.id === remoteModel.id);
|
|
27
|
+
|
|
28
|
+
if (localIndex === -1) {
|
|
29
|
+
merged.push(remoteModel);
|
|
30
|
+
} else {
|
|
31
|
+
const localModel = merged[localIndex];
|
|
32
|
+
const localCalls = localModel.total_calls ?? 0;
|
|
33
|
+
const remoteCalls = remoteModel.total_calls ?? 0;
|
|
34
|
+
|
|
35
|
+
const total_calls = Math.max(localCalls, remoteCalls);
|
|
36
|
+
const total_tokens_in = Math.max(localModel.total_tokens_in ?? 0, remoteModel.total_tokens_in ?? 0);
|
|
37
|
+
const total_tokens_out = Math.max(localModel.total_tokens_out ?? 0, remoteModel.total_tokens_out ?? 0);
|
|
38
|
+
const last_used = localCalls >= remoteCalls ? localModel.last_used : remoteModel.last_used;
|
|
39
|
+
|
|
40
|
+
const base = preferRemote ? { ...localModel, ...remoteModel } : localModel;
|
|
41
|
+
merged[localIndex] = { ...base, total_calls, total_tokens_in, total_tokens_out, last_used };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return merged;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mergeById(local: ProviderAccount[], remote: ProviderAccount[], preferRemote: boolean): ProviderAccount[] {
|
|
49
|
+
const merged = [...local];
|
|
50
|
+
|
|
51
|
+
for (const remoteProvider of remote) {
|
|
52
|
+
const localIndex = merged.findIndex(p => p.id === remoteProvider.id);
|
|
53
|
+
|
|
54
|
+
if (localIndex === -1) {
|
|
55
|
+
merged.push(remoteProvider);
|
|
56
|
+
} else {
|
|
57
|
+
const localProvider = merged[localIndex];
|
|
58
|
+
const mergedModels = mergeModels(localProvider.models ?? [], remoteProvider.models ?? [], preferRemote);
|
|
59
|
+
const base = preferRemote ? { ...localProvider, ...remoteProvider } : localProvider;
|
|
60
|
+
merged[localIndex] = { ...base, models: mergedModels };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
function mergeDataItems<T extends DataItem>(local: T[], remote: T[]): T[] {
|
|
23
68
|
const merged = [...local];
|
|
24
69
|
|
|
@@ -88,7 +133,7 @@ export function yoloMerge(local: StorageState, remote: StorageState): StorageSta
|
|
|
88
133
|
const preferRemote = remote.timestamp > local.timestamp;
|
|
89
134
|
|
|
90
135
|
if (remote.human.settings?.accounts && merged.human.settings) {
|
|
91
|
-
merged.human.settings.accounts =
|
|
136
|
+
merged.human.settings.accounts = mergeById(
|
|
92
137
|
merged.human.settings?.accounts || [],
|
|
93
138
|
remote.human.settings.accounts,
|
|
94
139
|
preferRemote,
|
package/tui/src/commands/dlq.ts
CHANGED
|
@@ -17,7 +17,9 @@ export const dlqCommand: Command = {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const human = await ctx.ei.getHuman();
|
|
21
|
+
const accounts = human.settings?.accounts ?? [];
|
|
22
|
+
let yamlContent = queueItemsToYAML(items, accounts);
|
|
21
23
|
|
|
22
24
|
while (true) {
|
|
23
25
|
const result = await spawnEditor({
|
|
@@ -37,14 +39,20 @@ export const dlqCommand: Command = {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
try {
|
|
40
|
-
const updates = queueItemsFromYAML(result.content);
|
|
42
|
+
const { updates, deletedIds } = queueItemsFromYAML(result.content, accounts);
|
|
43
|
+
if (deletedIds.length > 0) {
|
|
44
|
+
ctx.ei.deleteQueueItems(deletedIds);
|
|
45
|
+
}
|
|
41
46
|
let recovered = 0;
|
|
42
47
|
for (const update of updates) {
|
|
43
48
|
await ctx.ei.updateQueueItem(update.id, update);
|
|
44
49
|
if (update.state === "pending") recovered++;
|
|
45
50
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
if (recovered > 0) parts.push(`${recovered} item(s) requeued`);
|
|
53
|
+
if (deletedIds.length > 0) parts.push(`${deletedIds.length} deleted`);
|
|
54
|
+
const msg = parts.length > 0
|
|
55
|
+
? `DLQ updated — ${parts.join(", ")}`
|
|
48
56
|
: `DLQ updated (no items requeued)`;
|
|
49
57
|
ctx.showNotification(msg, "info");
|
|
50
58
|
return;
|