ei-tui 0.1.10 → 0.1.13
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/cli.ts +58 -0
- package/src/core/handlers/index.ts +76 -21
- package/src/core/llm-client.ts +13 -2
- package/src/core/processor.ts +116 -4
- package/src/core/queue-processor.ts +4 -3
- package/src/core/types.ts +11 -1
- package/src/integrations/claude-code/importer.ts +323 -0
- package/src/integrations/claude-code/index.ts +10 -0
- package/src/integrations/claude-code/reader.ts +238 -0
- package/src/integrations/claude-code/types.ts +163 -0
- package/src/integrations/opencode/importer.ts +10 -3
- package/src/prompts/generation/persona.ts +5 -3
- package/src/prompts/generation/types.ts +1 -1
- package/src/prompts/human/fact-scan.ts +6 -6
- package/src/prompts/human/person-scan.ts +6 -6
- package/src/prompts/human/topic-scan.ts +4 -4
- package/src/prompts/human/trait-scan.ts +6 -6
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/response/sections.ts +15 -0
- package/src/storage/interface.ts +2 -0
- package/src/storage/local.ts +5 -0
- package/tui/README.md +13 -0
- package/tui/src/index.tsx +20 -0
- package/tui/src/storage/file.ts +39 -2
- package/tui/src/util/instance-lock.ts +92 -0
- package/tui/src/util/logger.ts +0 -2
- package/tui/src/util/yaml-serializers.ts +49 -3
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -126,6 +126,63 @@ async function installOpenCodeTool(): Promise<void> {
|
|
|
126
126
|
console.log(` Restart OpenCode to activate.`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
async function installClaudeCodeMcp(): Promise<void> {
|
|
130
|
+
const home = process.env.HOME || "~";
|
|
131
|
+
const claudeJsonPath = join(home, ".claude.json");
|
|
132
|
+
|
|
133
|
+
// Prefer shelling out to `claude mcp add` — lets Claude Code manage its own config
|
|
134
|
+
// and avoids race conditions with a live state file.
|
|
135
|
+
try {
|
|
136
|
+
const which = Bun.spawnSync(["which", "claude"], { stdout: "pipe", stderr: "pipe" });
|
|
137
|
+
if (which.exitCode === 0) {
|
|
138
|
+
const result = Bun.spawnSync(
|
|
139
|
+
["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "ei", "--", "ei"],
|
|
140
|
+
{ stdout: "pipe", stderr: "pipe" }
|
|
141
|
+
);
|
|
142
|
+
if (result.exitCode === 0) {
|
|
143
|
+
console.log(`✓ Registered Ei as Claude Code MCP server (user scope)`);
|
|
144
|
+
console.log(` Restart Claude Code to activate.`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
console.warn(` claude mcp add failed (exit ${result.exitCode}), falling back to direct write`);
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// claude binary not found — fall through to direct write
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fallback: direct atomic write to ~/.claude.json
|
|
154
|
+
let config: Record<string, unknown> = {};
|
|
155
|
+
try {
|
|
156
|
+
const text = await Bun.file(claudeJsonPath).text();
|
|
157
|
+
config = JSON.parse(text) as Record<string, unknown>;
|
|
158
|
+
} catch {
|
|
159
|
+
// File doesn't exist or isn't valid JSON — start fresh
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Resolve the ei binary: if running as compiled binary, argv[1] is our path;
|
|
163
|
+
// if running as 'bun src/cli.ts', fall back to 'ei' (assumed on PATH after npm install -g)
|
|
164
|
+
const isBunScript = process.argv[1]?.endsWith("/cli.ts") || process.argv[1]?.endsWith("/cli.js");
|
|
165
|
+
const command = isBunScript ? "ei" : (process.argv[1] ?? "ei");
|
|
166
|
+
|
|
167
|
+
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
168
|
+
mcpServers["ei"] = {
|
|
169
|
+
type: "stdio",
|
|
170
|
+
command,
|
|
171
|
+
args: [],
|
|
172
|
+
env: {},
|
|
173
|
+
};
|
|
174
|
+
config.mcpServers = mcpServers;
|
|
175
|
+
|
|
176
|
+
// Atomic write: write to temp file then rename to avoid partial writes
|
|
177
|
+
const tmpPath = `${claudeJsonPath}.ei-install.tmp`;
|
|
178
|
+
await Bun.write(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
179
|
+
const { rename } = await import(/* @vite-ignore */ "fs/promises");
|
|
180
|
+
await rename(tmpPath, claudeJsonPath);
|
|
181
|
+
|
|
182
|
+
console.log(`✓ Installed Ei MCP server to ${claudeJsonPath}`);
|
|
183
|
+
console.log(` Restart Claude Code to activate.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
129
186
|
async function main(): Promise<void> {
|
|
130
187
|
const args = process.argv.slice(2);
|
|
131
188
|
|
|
@@ -148,6 +205,7 @@ async function main(): Promise<void> {
|
|
|
148
205
|
|
|
149
206
|
if (args[0] === "--install") {
|
|
150
207
|
await installOpenCodeTool();
|
|
208
|
+
await installClaudeCodeMcp();
|
|
151
209
|
process.exit(0);
|
|
152
210
|
}
|
|
153
211
|
|
|
@@ -276,33 +276,88 @@ function handlePersonaGeneration(response: LLMResponse, state: StateManager): vo
|
|
|
276
276
|
|
|
277
277
|
const now = new Date().toISOString();
|
|
278
278
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
// Merge LLM traits into user-provided traits by name.
|
|
280
|
+
// User-provided fields win; LLM fills in what the user left blank.
|
|
281
|
+
const userTraitsByName = new Map(
|
|
282
|
+
(existingPartial.traits ?? []).filter(t => t.name?.trim()).map(t => [t.name!.toLowerCase().trim(), t])
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const mergedLlmTraits: Trait[] = (result?.traits || []).map(t => {
|
|
286
|
+
const userTrait = userTraitsByName.get(t.name?.toLowerCase().trim() ?? '');
|
|
287
|
+
return {
|
|
288
|
+
id: (userTrait as Trait | undefined)?.id ?? crypto.randomUUID(),
|
|
289
|
+
name: t.name,
|
|
290
|
+
description: userTrait?.description?.trim() || t.description,
|
|
291
|
+
sentiment: userTrait?.sentiment ?? t.sentiment ?? 0,
|
|
292
|
+
strength: userTrait?.strength ?? t.strength,
|
|
293
|
+
last_updated: now,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
// Keep user-provided traits the LLM didn't return
|
|
298
|
+
const llmTraitNames = new Set(mergedLlmTraits.map(t => t.name?.toLowerCase().trim()));
|
|
299
|
+
const preservedUserTraits: Trait[] = (existingPartial.traits ?? [])
|
|
300
|
+
.filter(t => t.name?.trim() && !llmTraitNames.has(t.name.toLowerCase().trim()))
|
|
301
|
+
.map(t => ({
|
|
302
|
+
id: (t as Trait).id ?? crypto.randomUUID(),
|
|
303
|
+
name: t.name!,
|
|
304
|
+
description: t.description || '',
|
|
305
|
+
sentiment: t.sentiment ?? 0,
|
|
306
|
+
strength: t.strength,
|
|
307
|
+
last_updated: now,
|
|
308
|
+
}));
|
|
309
|
+
|
|
310
|
+
const mergedTraits: Trait[] = mergedLlmTraits.length > 0
|
|
311
|
+
? [...mergedLlmTraits, ...preservedUserTraits]
|
|
312
|
+
: (existingPartial.traits as Trait[] | undefined) ?? [];
|
|
313
|
+
|
|
314
|
+
// Merge LLM topics into user-provided topics by name.
|
|
315
|
+
// User-provided fields win; LLM fills in what the user left blank.
|
|
316
|
+
const userTopicsByName = new Map(
|
|
317
|
+
(existingPartial.topics ?? []).filter(t => t.name?.trim()).map(t => [t.name!.toLowerCase().trim(), t])
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const llmTopics: PersonaTopic[] = (result?.topics || []).map(t => {
|
|
321
|
+
const userTopic = userTopicsByName.get(t.name?.toLowerCase().trim() ?? '');
|
|
322
|
+
return {
|
|
323
|
+
id: (userTopic as PersonaTopic | undefined)?.id ?? crypto.randomUUID(),
|
|
324
|
+
name: t.name,
|
|
325
|
+
perspective: userTopic?.perspective?.trim() || t.perspective || '',
|
|
326
|
+
approach: userTopic?.approach?.trim() || t.approach || '',
|
|
327
|
+
personal_stake: userTopic?.personal_stake?.trim() || t.personal_stake || '',
|
|
328
|
+
sentiment: userTopic?.sentiment ?? t.sentiment ?? 0,
|
|
329
|
+
exposure_current: userTopic?.exposure_current ?? t.exposure_current ?? 0.5,
|
|
330
|
+
exposure_desired: userTopic?.exposure_desired ?? t.exposure_desired ?? 0.5,
|
|
331
|
+
last_updated: now,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Keep user-provided topics the LLM didn't return (not in its output list)
|
|
336
|
+
const llmTopicNames = new Set(llmTopics.map(t => t.name?.toLowerCase().trim()));
|
|
337
|
+
const preservedUserTopics: PersonaTopic[] = (existingPartial.topics ?? [])
|
|
338
|
+
.filter(t => t.name?.trim() && !llmTopicNames.has(t.name.toLowerCase().trim()))
|
|
339
|
+
.map(t => ({
|
|
340
|
+
id: (t as PersonaTopic).id ?? crypto.randomUUID(),
|
|
341
|
+
name: t.name!,
|
|
342
|
+
perspective: t.perspective || '',
|
|
343
|
+
approach: t.approach || '',
|
|
344
|
+
personal_stake: t.personal_stake || '',
|
|
345
|
+
sentiment: t.sentiment ?? 0,
|
|
346
|
+
exposure_current: t.exposure_current ?? 0.5,
|
|
347
|
+
exposure_desired: t.exposure_desired ?? 0.5,
|
|
348
|
+
last_updated: now,
|
|
349
|
+
}));
|
|
350
|
+
|
|
351
|
+
const topics: PersonaTopic[] = llmTopics.length > 0
|
|
352
|
+
? [...llmTopics, ...preservedUserTopics]
|
|
353
|
+
: (existingPartial.topics as PersonaTopic[] | undefined) ?? [];
|
|
299
354
|
|
|
300
355
|
const updatedPartial: PartialPersona = {
|
|
301
356
|
...existingPartial,
|
|
302
357
|
short_description: result?.short_description ?? existingPartial.short_description,
|
|
303
358
|
long_description: existingPartial.long_description ?? result?.long_description,
|
|
304
|
-
traits:
|
|
305
|
-
topics
|
|
359
|
+
traits: mergedTraits.length > 0 ? mergedTraits : existingPartial.traits,
|
|
360
|
+
topics,
|
|
306
361
|
};
|
|
307
362
|
|
|
308
363
|
orchestratePersonaGeneration(updatedPartial, state);
|
package/src/core/llm-client.ts
CHANGED
|
@@ -259,7 +259,18 @@ export function parseJSONResponse(content: string): unknown {
|
|
|
259
259
|
|
|
260
260
|
export function cleanResponseContent(content: string): string {
|
|
261
261
|
return content
|
|
262
|
-
|
|
263
|
-
.replace(
|
|
262
|
+
// Complete paired blocks (space-tolerant, case-insensitive)
|
|
263
|
+
.replace(/<\s*think\s*>[\s\S]*?<\s*\/\s*think\s*>/gi, "")
|
|
264
|
+
.replace(/<\s*thinking\s*>[\s\S]*?<\s*\/\s*thinking\s*>/gi, "")
|
|
265
|
+
// Seed-OSS (ByteDance) namespaced thinking tags — always paired
|
|
266
|
+
.replace(/<seed:think>[\s\S]*?<\/seed:think>/gi, "")
|
|
267
|
+
// Seed-OSS budget reflection tokens (may appear outside stripped think block)
|
|
268
|
+
.replace(/<seed:cot_budget_reflect>[\s\S]*?<\/seed:cot_budget_reflect>/gi, "")
|
|
269
|
+
// Orphaned closing tag with content before it (MiniMax / streaming accumulation)
|
|
270
|
+
.replace(/^[\s\S]*?<\s*\/\s*think(?:ing)?\s*>/i, "")
|
|
271
|
+
// Remaining orphaned closing tags
|
|
272
|
+
.replace(/<\s*\/\s*think(?:ing)?\s*>/gi, "")
|
|
273
|
+
// Remaining orphaned opening tags
|
|
274
|
+
.replace(/<\s*think(?:ing)?\s*>/gi, "")
|
|
264
275
|
.trim();
|
|
265
276
|
}
|
package/src/core/processor.ts
CHANGED
|
@@ -87,6 +87,7 @@ function stripHumanEmbeddings(human: HumanEntity): HumanEntity {
|
|
|
87
87
|
const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
88
88
|
const DEFAULT_CONTEXT_WINDOW_HOURS = 8;
|
|
89
89
|
const DEFAULT_OPENCODE_POLLING_MS = 1800000;
|
|
90
|
+
const DEFAULT_CLAUDE_CODE_POLLING_MS = 1800000;
|
|
90
91
|
|
|
91
92
|
let processorInstanceCount = 0;
|
|
92
93
|
|
|
@@ -127,7 +128,11 @@ export class Processor {
|
|
|
127
128
|
private lastOpenCodeSync = 0;
|
|
128
129
|
private lastDLQTrim = 0;
|
|
129
130
|
private openCodeImportInProgress = false;
|
|
131
|
+
private lastClaudeCodeSync = 0;
|
|
132
|
+
private claudeCodeImportInProgress = false;
|
|
130
133
|
private pendingConflict: StateConflictData | null = null;
|
|
134
|
+
private storage: Storage | null = null;
|
|
135
|
+
private importAbortController = new AbortController();
|
|
131
136
|
|
|
132
137
|
constructor(ei: Ei_Interface) {
|
|
133
138
|
this.interface = ei;
|
|
@@ -147,6 +152,7 @@ export class Processor {
|
|
|
147
152
|
|
|
148
153
|
async start(storage: Storage): Promise<void> {
|
|
149
154
|
console.log(`[Processor ${this.instanceId}] start() called`);
|
|
155
|
+
this.storage = storage;
|
|
150
156
|
await this.stateManager.initialize(storage);
|
|
151
157
|
if (this.stopped) {
|
|
152
158
|
console.log(`[Processor ${this.instanceId}] stopped during init, not starting loop`);
|
|
@@ -255,6 +261,7 @@ export class Processor {
|
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
this.running = false;
|
|
264
|
+
this.importAbortController.abort();
|
|
258
265
|
this.queueProcessor.abort();
|
|
259
266
|
await this.stateManager.flush();
|
|
260
267
|
console.log(`[Processor ${this.instanceId}] stopped`);
|
|
@@ -265,10 +272,15 @@ export class Processor {
|
|
|
265
272
|
this.interface.onSaveAndExitStart?.();
|
|
266
273
|
|
|
267
274
|
this.queueProcessor.abort();
|
|
275
|
+
this.importAbortController.abort();
|
|
268
276
|
if (this.openCodeImportInProgress) {
|
|
269
277
|
console.log(`[Processor ${this.instanceId}] Aborting OpenCode import in progress`);
|
|
270
278
|
this.openCodeImportInProgress = false;
|
|
271
279
|
}
|
|
280
|
+
if (this.claudeCodeImportInProgress) {
|
|
281
|
+
console.log(`[Processor ${this.instanceId}] Aborting Claude Code import in progress`);
|
|
282
|
+
this.claudeCodeImportInProgress = false;
|
|
283
|
+
}
|
|
272
284
|
|
|
273
285
|
await this.stateManager.flush();
|
|
274
286
|
|
|
@@ -325,6 +337,7 @@ export class Processor {
|
|
|
325
337
|
}
|
|
326
338
|
|
|
327
339
|
this.pendingConflict = null;
|
|
340
|
+
this.importAbortController = new AbortController();
|
|
328
341
|
this.running = true;
|
|
329
342
|
this.runLoop();
|
|
330
343
|
this.interface.onStateImported?.();
|
|
@@ -383,6 +396,13 @@ export class Processor {
|
|
|
383
396
|
if (this.isTUI && human.settings?.opencode?.integration && this.stateManager.queue_length() === 0) {
|
|
384
397
|
await this.checkAndSyncOpenCode(human, now);
|
|
385
398
|
}
|
|
399
|
+
|
|
400
|
+
if (this.isTUI && human.settings?.backup?.enabled) {
|
|
401
|
+
await this.checkAndRunRollingBackup(human, now);
|
|
402
|
+
}
|
|
403
|
+
if (this.isTUI && human.settings?.claudeCode?.integration && this.stateManager.queue_length() === 0) {
|
|
404
|
+
await this.checkAndSyncClaudeCode(human, now);
|
|
405
|
+
}
|
|
386
406
|
|
|
387
407
|
if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
|
|
388
408
|
// Auto-backup to remote before ceremony (if configured)
|
|
@@ -425,6 +445,33 @@ export class Processor {
|
|
|
425
445
|
}
|
|
426
446
|
}
|
|
427
447
|
|
|
448
|
+
private async checkAndRunRollingBackup(human: HumanEntity, now: number): Promise<void> {
|
|
449
|
+
if (!this.storage) return;
|
|
450
|
+
const cfg = human.settings!.backup!;
|
|
451
|
+
const intervalMs = cfg.interval_ms ?? 3_600_000; // default: 1 hour
|
|
452
|
+
const maxBackups = cfg.max_backups ?? 24;
|
|
453
|
+
const lastBackup = cfg.last_backup ? new Date(cfg.last_backup).getTime() : 0;
|
|
454
|
+
|
|
455
|
+
if (now - lastBackup < intervalMs) return;
|
|
456
|
+
|
|
457
|
+
// Update timestamp BEFORE async work to prevent duplicate triggers
|
|
458
|
+
this.stateManager.setHuman({
|
|
459
|
+
...this.stateManager.getHuman(),
|
|
460
|
+
settings: {
|
|
461
|
+
...this.stateManager.getHuman().settings,
|
|
462
|
+
backup: { ...cfg, last_backup: new Date(now).toISOString() },
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const state = this.stateManager.getStorageState();
|
|
467
|
+
try {
|
|
468
|
+
await this.storage.saveRollingBackup(state, maxBackups);
|
|
469
|
+
console.log(`[Processor] Rolling backup saved (max=${maxBackups})`);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
console.warn(`[Processor] Rolling backup failed:`, err);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
428
475
|
private async checkAndSyncOpenCode(human: HumanEntity, now: number): Promise<void> {
|
|
429
476
|
if (this.openCodeImportInProgress) {
|
|
430
477
|
return;
|
|
@@ -460,6 +507,7 @@ export class Processor {
|
|
|
460
507
|
importOpenCodeSessions({
|
|
461
508
|
stateManager: this.stateManager,
|
|
462
509
|
interface: this.interface,
|
|
510
|
+
signal: this.importAbortController.signal,
|
|
463
511
|
})
|
|
464
512
|
)
|
|
465
513
|
.then((result) => {
|
|
@@ -479,6 +527,61 @@ export class Processor {
|
|
|
479
527
|
});
|
|
480
528
|
}
|
|
481
529
|
|
|
530
|
+
private async checkAndSyncClaudeCode(human: HumanEntity, now: number): Promise<void> {
|
|
531
|
+
if (this.claudeCodeImportInProgress) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const claudeCode = human.settings?.claudeCode;
|
|
536
|
+
const pollingInterval = claudeCode?.polling_interval_ms ?? DEFAULT_CLAUDE_CODE_POLLING_MS;
|
|
537
|
+
const lastSync = claudeCode?.last_sync
|
|
538
|
+
? new Date(claudeCode.last_sync).getTime()
|
|
539
|
+
: 0;
|
|
540
|
+
const timeSinceSync = now - lastSync;
|
|
541
|
+
|
|
542
|
+
if (timeSinceSync < pollingInterval && this.lastClaudeCodeSync > 0) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this.lastClaudeCodeSync = now;
|
|
547
|
+
const syncTimestamp = new Date().toISOString();
|
|
548
|
+
this.stateManager.setHuman({
|
|
549
|
+
...this.stateManager.getHuman(),
|
|
550
|
+
settings: {
|
|
551
|
+
...this.stateManager.getHuman().settings,
|
|
552
|
+
claudeCode: {
|
|
553
|
+
...claudeCode,
|
|
554
|
+
last_sync: syncTimestamp,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
this.claudeCodeImportInProgress = true;
|
|
560
|
+
import("../integrations/claude-code/importer.js")
|
|
561
|
+
.then(({ importClaudeCodeSessions }) =>
|
|
562
|
+
importClaudeCodeSessions({
|
|
563
|
+
stateManager: this.stateManager,
|
|
564
|
+
interface: this.interface,
|
|
565
|
+
signal: this.importAbortController.signal,
|
|
566
|
+
})
|
|
567
|
+
)
|
|
568
|
+
.then((result) => {
|
|
569
|
+
if (result.sessionsProcessed > 0) {
|
|
570
|
+
console.log(
|
|
571
|
+
`[Processor] Claude Code sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
572
|
+
`${result.topicsCreated} topics created, ${result.messagesImported} messages imported, ` +
|
|
573
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
})
|
|
577
|
+
.catch((err) => {
|
|
578
|
+
console.warn(`[Processor] Claude Code sync failed:`, err);
|
|
579
|
+
})
|
|
580
|
+
.finally(() => {
|
|
581
|
+
this.claudeCodeImportInProgress = false;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
482
585
|
private getModelForPersona(personaId?: string): string | undefined {
|
|
483
586
|
const human = this.stateManager.getHuman();
|
|
484
587
|
if (personaId) {
|
|
@@ -488,6 +591,11 @@ export class Processor {
|
|
|
488
591
|
return human.settings?.default_model;
|
|
489
592
|
}
|
|
490
593
|
|
|
594
|
+
private getOneshotModel(): string | undefined {
|
|
595
|
+
const human = this.stateManager.getHuman();
|
|
596
|
+
return human.settings?.oneshot_model || human.settings?.default_model;
|
|
597
|
+
}
|
|
598
|
+
|
|
491
599
|
private fetchMessagesForLLM(personaId: string): import("./types.js").ChatMessage[] {
|
|
492
600
|
const persona = this.stateManager.persona_getById(personaId);
|
|
493
601
|
if (!persona) return [];
|
|
@@ -695,6 +803,10 @@ export class Processor {
|
|
|
695
803
|
message += ` (attempt ${response.request.attempts}, retrying in ${Math.round(result.retryDelay / 1000)}s)`;
|
|
696
804
|
} else if (result.dropped) {
|
|
697
805
|
message += " (permanent failure \u2014 request removed)";
|
|
806
|
+
if (response.request.next_step === LLMNextStep.HandleOneShot) {
|
|
807
|
+
const guid = response.request.data.guid as string;
|
|
808
|
+
this.interface.onOneShotReturned?.(guid, "");
|
|
809
|
+
}
|
|
698
810
|
}
|
|
699
811
|
|
|
700
812
|
this.interface.onError?.({ code, message });
|
|
@@ -922,9 +1034,9 @@ export class Processor {
|
|
|
922
1034
|
const responsesToClear = [
|
|
923
1035
|
LLMNextStep.HandlePersonaResponse,
|
|
924
1036
|
LLMNextStep.HandlePersonaTraitExtraction,
|
|
925
|
-
LLMNextStep.
|
|
926
|
-
LLMNextStep.
|
|
927
|
-
|
|
1037
|
+
LLMNextStep.HandleHeartbeatCheck, // clear stale heartbeat when user is active
|
|
1038
|
+
LLMNextStep.HandleEiHeartbeat, // clear stale Ei heartbeat when user is active
|
|
1039
|
+
// Note: TopicScan/Match/Update are ceremony-only — never clear them here
|
|
928
1040
|
];
|
|
929
1041
|
|
|
930
1042
|
let removedAny = false;
|
|
@@ -1535,7 +1647,7 @@ export class Processor {
|
|
|
1535
1647
|
system: systemPrompt,
|
|
1536
1648
|
user: userPrompt,
|
|
1537
1649
|
next_step: LLMNextStep.HandleOneShot,
|
|
1538
|
-
model: this.
|
|
1650
|
+
model: this.getOneshotModel(),
|
|
1539
1651
|
data: { guid },
|
|
1540
1652
|
});
|
|
1541
1653
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LLMRequest, LLMResponse, LLMRequestType, ProviderAccount, ChatMessage, Message } from "./types.js";
|
|
2
|
-
import { callLLMRaw, parseJSONResponse } from "./llm-client.js";
|
|
2
|
+
import { callLLMRaw, parseJSONResponse, cleanResponseContent } from "./llm-client.js";
|
|
3
3
|
import { hydratePromptPlaceholders } from "../prompts/message-utils.js";
|
|
4
4
|
|
|
5
5
|
type QueueProcessorState = "idle" | "busy";
|
|
@@ -131,16 +131,17 @@ export class QueueProcessor {
|
|
|
131
131
|
content: string,
|
|
132
132
|
finishReason: string | null
|
|
133
133
|
): LLMResponse {
|
|
134
|
+
const cleanedContent = cleanResponseContent(content);
|
|
134
135
|
switch (request.type) {
|
|
135
136
|
case "json" as LLMRequestType:
|
|
136
137
|
case "response" as LLMRequestType:
|
|
137
|
-
return this.handleJSONResponse(request,
|
|
138
|
+
return this.handleJSONResponse(request, cleanedContent, finishReason);
|
|
138
139
|
case "raw" as LLMRequestType:
|
|
139
140
|
default:
|
|
140
141
|
return {
|
|
141
142
|
request,
|
|
142
143
|
success: true,
|
|
143
|
-
content,
|
|
144
|
+
content: cleanedContent,
|
|
144
145
|
finish_reason: finishReason ?? undefined,
|
|
145
146
|
};
|
|
146
147
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -192,8 +192,16 @@ export interface CeremonyConfig {
|
|
|
192
192
|
dedup_threshold?: number; // Cosine similarity threshold for dedup candidates. Default: 0.85
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
export interface BackupConfig {
|
|
196
|
+
enabled?: boolean; // Default: false (opt-in)
|
|
197
|
+
max_backups?: number; // Default: 24
|
|
198
|
+
interval_ms?: number; // Default: 3600000 (1 hour)
|
|
199
|
+
last_backup?: string; // ISO timestamp of last backup run
|
|
200
|
+
}
|
|
201
|
+
|
|
195
202
|
export interface HumanSettings {
|
|
196
203
|
default_model?: string;
|
|
204
|
+
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model
|
|
197
205
|
queue_paused?: boolean;
|
|
198
206
|
skip_quote_delete_confirm?: boolean;
|
|
199
207
|
name_display?: string;
|
|
@@ -202,6 +210,8 @@ export interface HumanSettings {
|
|
|
202
210
|
sync?: SyncCredentials;
|
|
203
211
|
opencode?: OpenCodeSettings;
|
|
204
212
|
ceremony?: CeremonyConfig;
|
|
213
|
+
backup?: BackupConfig;
|
|
214
|
+
claudeCode?: import("../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
export interface HumanEntity {
|
|
@@ -248,7 +258,7 @@ export interface PersonaCreationInput {
|
|
|
248
258
|
long_description?: string;
|
|
249
259
|
short_description?: string;
|
|
250
260
|
traits?: Partial<Trait>[];
|
|
251
|
-
topics?: Partial<
|
|
261
|
+
topics?: Partial<PersonaTopic>[];
|
|
252
262
|
model?: string;
|
|
253
263
|
group_primary?: string;
|
|
254
264
|
groups_visible?: string[];
|