ei-tui 0.6.7 → 0.7.0
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/mcp.ts +35 -10
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +10 -0
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +5 -0
- package/src/core/handlers/utils.ts +4 -1
- package/src/core/orchestrators/ceremony.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +5 -0
- package/src/core/processor.ts +22 -2
- package/src/core/prompt-context-builder.ts +40 -10
- package/src/core/queue-manager.ts +18 -0
- package/src/core/room-manager.ts +21 -4
- package/src/core/state-manager.ts +26 -0
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +1 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/integrations/claude-code/importer.ts +3 -57
- package/src/integrations/cursor/importer.ts +2 -52
- package/src/integrations/opencode/importer.ts +1 -0
- package/src/prompts/response/sections.ts +1 -1
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +2 -2
- package/src/prompts/room/sections.ts +4 -4
- package/src/prompts/room/types.ts +4 -0
- package/tui/src/commands/activate.tsx +7 -6
- package/tui/src/commands/context.tsx +188 -2
- package/tui/src/components/CYPTreeOverlay.tsx +357 -0
- package/tui/src/components/MAPScoreOverlay.tsx +300 -0
- package/tui/src/components/MessageList.tsx +14 -3
- package/tui/src/components/RoomMessageList.tsx +15 -3
- package/tui/src/context/ei.tsx +20 -0
- package/tui/src/util/cyp-tree.ts +62 -0
- package/tui/src/util/yaml-context.ts +87 -1
package/package.json
CHANGED
package/src/cli/mcp.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { retrieveBalanced, lookupById, loadLatestState, type BalancedResult } from "./retrieval.js";
|
|
5
5
|
import type { StorageState } from "../core/types/index.js";
|
|
6
|
-
import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona } from "./persona-filter.js";
|
|
6
|
+
import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona, filterBySource, filterTypeSpecificBySource } from "./persona-filter.js";
|
|
7
7
|
|
|
8
8
|
// Exported so tests can inject their own transport
|
|
9
9
|
export function createMcpServer(): McpServer {
|
|
@@ -31,6 +31,12 @@ export function createMcpServer(): McpServer {
|
|
|
31
31
|
.describe(
|
|
32
32
|
"Filter to entities a specific persona has learned about. Use the persona display name."
|
|
33
33
|
),
|
|
34
|
+
source: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe(
|
|
38
|
+
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'opencode', 'opencode:ses_abc123')."
|
|
39
|
+
),
|
|
34
40
|
limit: z
|
|
35
41
|
.number()
|
|
36
42
|
.optional()
|
|
@@ -42,16 +48,16 @@ export function createMcpServer(): McpServer {
|
|
|
42
48
|
.describe("If true, sort by most recently mentioned."),
|
|
43
49
|
},
|
|
44
50
|
},
|
|
45
|
-
async ({ query: rawQuery, type, persona, limit, recent }) => {
|
|
51
|
+
async ({ query: rawQuery, type, persona, source, limit, recent }) => {
|
|
46
52
|
const query = rawQuery ?? "";
|
|
47
53
|
const options = { recent: recent ?? false };
|
|
48
54
|
const effectiveLimit = limit ?? 10;
|
|
49
55
|
|
|
50
56
|
let state: StorageState | null = null;
|
|
51
57
|
let personaId: string | undefined;
|
|
52
|
-
if (persona) {
|
|
58
|
+
if (persona || source) {
|
|
53
59
|
state = await loadLatestState();
|
|
54
|
-
if (state) {
|
|
60
|
+
if (state && persona) {
|
|
55
61
|
personaId = resolvePersonaId(state, persona) ?? undefined;
|
|
56
62
|
if (!personaId) {
|
|
57
63
|
return {
|
|
@@ -68,11 +74,17 @@ export function createMcpServer(): McpServer {
|
|
|
68
74
|
if (personaId && state) {
|
|
69
75
|
result = filterTypeSpecificByPersona(result as { id: string }[], state, personaId, type);
|
|
70
76
|
}
|
|
77
|
+
if (source && state) {
|
|
78
|
+
result = filterTypeSpecificBySource(result as { id: string }[], state, source, type);
|
|
79
|
+
}
|
|
71
80
|
} else {
|
|
72
81
|
result = await retrieveBalanced(query, effectiveLimit, options);
|
|
73
82
|
if (personaId && state) {
|
|
74
83
|
result = filterByPersona(result as BalancedResult[], state, personaId);
|
|
75
84
|
}
|
|
85
|
+
if (source && state) {
|
|
86
|
+
result = filterBySource(result as BalancedResult[], state, source);
|
|
87
|
+
}
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
return {
|
|
@@ -88,17 +100,30 @@ export function createMcpServer(): McpServer {
|
|
|
88
100
|
"Look up a specific entity in the Ei knowledge base by ID. Returns the full entity record. Use IDs from ei_search results.",
|
|
89
101
|
inputSchema: {
|
|
90
102
|
id: z.string().describe("The entity ID to look up."),
|
|
103
|
+
source: z
|
|
104
|
+
.string()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe(
|
|
107
|
+
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'opencode', 'opencode:ses_abc123'). If the entity does not match, returns not found."
|
|
108
|
+
),
|
|
91
109
|
},
|
|
92
110
|
},
|
|
93
|
-
async ({ id }) => {
|
|
111
|
+
async ({ id, source }) => {
|
|
94
112
|
const result = await lookupById(id);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
|
|
114
|
+
if (result === null) {
|
|
115
|
+
return { content: [{ type: "text" as const, text: `No entity found with ID: ${id}` }] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (source) {
|
|
119
|
+
const sources = (result as { sources?: string[] }).sources;
|
|
120
|
+
if (!sources?.some((s) => s.startsWith(source))) {
|
|
121
|
+
return { content: [{ type: "text" as const, text: `No entity found with ID: ${id}` }] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
99
124
|
|
|
100
125
|
return {
|
|
101
|
-
content: [{ type: "text" as const, text }],
|
|
126
|
+
content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
|
|
102
127
|
};
|
|
103
128
|
}
|
|
104
129
|
);
|
|
@@ -52,3 +52,45 @@ export function filterTypeSpecificByPersona<T extends { id: string }>(
|
|
|
52
52
|
return original?.interested_personas?.includes(personaId) ?? false;
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export function filterBySource(results: BalancedResult[], state: StorageState, sourcePrefix: string): BalancedResult[] {
|
|
57
|
+
return results.filter((result) => {
|
|
58
|
+
if (result.type === "quote") {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const { id } = result;
|
|
62
|
+
let original: { sources?: string[] } | undefined;
|
|
63
|
+
if (result.type === "fact") {
|
|
64
|
+
original = state.human.facts.find((f) => f.id === id);
|
|
65
|
+
} else if (result.type === "topic") {
|
|
66
|
+
original = state.human.topics.find((t) => t.id === id);
|
|
67
|
+
} else if (result.type === "person") {
|
|
68
|
+
original = state.human.people.find((p) => p.id === id);
|
|
69
|
+
}
|
|
70
|
+
return original?.sources?.some((s) => s.startsWith(sourcePrefix)) ?? false;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function filterTypeSpecificBySource<T extends { id: string }>(
|
|
75
|
+
results: T[],
|
|
76
|
+
state: StorageState,
|
|
77
|
+
sourcePrefix: string,
|
|
78
|
+
targetType: string
|
|
79
|
+
): T[] {
|
|
80
|
+
if (targetType === "quotes") {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const collection =
|
|
84
|
+
targetType === "facts"
|
|
85
|
+
? state.human.facts
|
|
86
|
+
: targetType === "topics"
|
|
87
|
+
? state.human.topics
|
|
88
|
+
: targetType === "people"
|
|
89
|
+
? state.human.people
|
|
90
|
+
: null;
|
|
91
|
+
if (!collection) return results;
|
|
92
|
+
return results.filter((r) => {
|
|
93
|
+
const original = collection.find((item) => item.id === r.id) as { sources?: string[] } | undefined;
|
|
94
|
+
return original?.sources?.some((s) => s.startsWith(sourcePrefix)) ?? false;
|
|
95
|
+
});
|
|
96
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { parseArgs } from "util";
|
|
|
15
15
|
import { join } from "path";
|
|
16
16
|
import { retrieveBalanced, lookupById, loadLatestState } from "./cli/retrieval";
|
|
17
17
|
import type { StorageState } from "./core/types";
|
|
18
|
-
import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona } from "./cli/persona-filter.js";
|
|
18
|
+
import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona, filterBySource, filterTypeSpecificBySource } from "./cli/persona-filter.js";
|
|
19
19
|
|
|
20
20
|
const TYPE_ALIASES: Record<string, string> = {
|
|
21
21
|
quote: "quotes",
|
|
@@ -59,6 +59,7 @@ Options:
|
|
|
59
59
|
--number, -n Maximum number of results (default: 10)
|
|
60
60
|
--recent, -r Sort by last_mentioned date (most recent first)
|
|
61
61
|
--persona, -p Filter to entities a specific persona has learned about
|
|
62
|
+
--source, -s Filter to entities from a specific source (prefix match, e.g. "cursor", "opencode:ses_abc123")
|
|
62
63
|
--id Look up entity by ID (accepts value or stdin)
|
|
63
64
|
--install Register Ei with OpenCode, Claude Code, and Cursor
|
|
64
65
|
--help, -h Show this help message
|
|
@@ -70,6 +71,7 @@ Examples:
|
|
|
70
71
|
ei --recent # Most recently mentioned items
|
|
71
72
|
ei topics --recent "work" # Recent work-related topics
|
|
72
73
|
ei --persona "Architect" "work stuff" # What Architect knows about work
|
|
74
|
+
ei topics --source cursor "X" # Topics learned from Cursor sessions
|
|
73
75
|
ei --id abc-123 # Look up entity by ID
|
|
74
76
|
ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
|
|
75
77
|
`);
|
|
@@ -305,6 +307,7 @@ async function main(): Promise<void> {
|
|
|
305
307
|
number: { type: "string", short: "n" },
|
|
306
308
|
recent: { type: "boolean", short: "r" },
|
|
307
309
|
persona: { type: "string", short: "p" },
|
|
310
|
+
source: { type: "string", short: "s" },
|
|
308
311
|
help: { type: "boolean", short: "h" },
|
|
309
312
|
},
|
|
310
313
|
allowPositionals: true,
|
|
@@ -325,6 +328,7 @@ async function main(): Promise<void> {
|
|
|
325
328
|
// Default to recent mode when no query — allows `ei --persona Foo` and `ei` with no args
|
|
326
329
|
const recent = parsed.values.recent === true || !query;
|
|
327
330
|
const personaName = parsed.values.persona?.trim();
|
|
331
|
+
const sourcePrefix = parsed.values.source?.trim();
|
|
328
332
|
|
|
329
333
|
if (isNaN(limit) || limit < 1) {
|
|
330
334
|
console.error("--number must be a positive integer");
|
|
@@ -333,16 +337,18 @@ async function main(): Promise<void> {
|
|
|
333
337
|
|
|
334
338
|
let state: StorageState | null = null;
|
|
335
339
|
let personaId: string | undefined;
|
|
336
|
-
if (personaName) {
|
|
340
|
+
if (personaName || sourcePrefix) {
|
|
337
341
|
state = await loadLatestState();
|
|
338
342
|
if (!state) {
|
|
339
343
|
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
340
344
|
process.exit(1);
|
|
341
345
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
+
if (personaName) {
|
|
347
|
+
personaId = resolvePersonaId(state, personaName) ?? undefined;
|
|
348
|
+
if (!personaId) {
|
|
349
|
+
console.error(`Persona "${personaName}" not found.`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
346
352
|
}
|
|
347
353
|
}
|
|
348
354
|
|
|
@@ -355,11 +361,17 @@ async function main(): Promise<void> {
|
|
|
355
361
|
if (personaId && state) {
|
|
356
362
|
result = filterTypeSpecificByPersona(result, state, personaId, targetType);
|
|
357
363
|
}
|
|
364
|
+
if (sourcePrefix && state) {
|
|
365
|
+
result = filterTypeSpecificBySource(result, state, sourcePrefix, targetType);
|
|
366
|
+
}
|
|
358
367
|
} else {
|
|
359
368
|
result = await retrieveBalanced(query, limit, options);
|
|
360
369
|
if (personaId && state) {
|
|
361
370
|
result = filterByPersona(result, state, personaId);
|
|
362
371
|
}
|
|
372
|
+
if (sourcePrefix && state) {
|
|
373
|
+
result = filterBySource(result, state, sourcePrefix);
|
|
374
|
+
}
|
|
363
375
|
}
|
|
364
376
|
|
|
365
377
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -147,6 +147,7 @@ export async function handleFactFind(response: LLMResponse, state: StateManager)
|
|
|
147
147
|
learned_by: existingFact.learned_by ?? context.personaId,
|
|
148
148
|
last_changed_by: context.personaId,
|
|
149
149
|
interested_personas: [...new Set([...(existingFact.interested_personas ?? []), context.personaId])],
|
|
150
|
+
sources: [...new Set([...(existingFact.sources ?? []), ...(context.sources ?? [])])],
|
|
150
151
|
embedding,
|
|
151
152
|
};
|
|
152
153
|
|
|
@@ -176,6 +176,10 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
176
176
|
const personaGroupsMerged = isNewItem
|
|
177
177
|
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingTopic?.persona_groups)
|
|
178
178
|
: [...new Set([...(existingTopic?.persona_groups ?? []), ...allPersonaGroups])];
|
|
179
|
+
const incomingSources = (response.request.data.sources ?? []) as string[];
|
|
180
|
+
const sources = isNewItem
|
|
181
|
+
? incomingSources
|
|
182
|
+
: [...new Set([...(existingTopic?.sources ?? []), ...incomingSources])];
|
|
179
183
|
|
|
180
184
|
const topic: Topic = {
|
|
181
185
|
id: itemId,
|
|
@@ -191,6 +195,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
191
195
|
learned_by: isNewItem ? primaryId : existingTopic?.learned_by,
|
|
192
196
|
last_changed_by: primaryId,
|
|
193
197
|
interested_personas: interestedPersonas,
|
|
198
|
+
sources: sources.length > 0 ? sources : undefined,
|
|
194
199
|
persona_groups: personaGroupsMerged,
|
|
195
200
|
embedding,
|
|
196
201
|
};
|
|
@@ -279,6 +284,10 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
279
284
|
const personaGroupsMerged = isNewItem
|
|
280
285
|
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingPerson?.persona_groups)
|
|
281
286
|
: [...new Set([...(existingPerson?.persona_groups ?? []), ...allPersonaGroups])];
|
|
287
|
+
const incomingPersonSources = (response.request.data.sources ?? []) as string[];
|
|
288
|
+
const personSources = isNewItem
|
|
289
|
+
? incomingPersonSources
|
|
290
|
+
: [...new Set([...(existingPerson?.sources ?? []), ...incomingPersonSources])];
|
|
282
291
|
|
|
283
292
|
let resolvedIdentifiers: PersonIdentifier[];
|
|
284
293
|
if (isNewItem) {
|
|
@@ -335,6 +344,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
335
344
|
learned_by: isNewItem ? primaryId : existingPerson?.learned_by,
|
|
336
345
|
last_changed_by: primaryId,
|
|
337
346
|
interested_personas: interestedPersonas,
|
|
347
|
+
sources: personSources.length > 0 ? personSources : undefined,
|
|
338
348
|
persona_groups: personaGroupsMerged,
|
|
339
349
|
embedding,
|
|
340
350
|
};
|
|
@@ -6,7 +6,7 @@ import type { PersonIdentifier } from "../types/data-items.js";
|
|
|
6
6
|
|
|
7
7
|
export type { ResponseHandler } from "./persona-response.js";
|
|
8
8
|
|
|
9
|
-
import { handlePersonaResponse, handleToolContinuation, handleOneShot } from "./persona-response.js";
|
|
9
|
+
import { handlePersonaResponse, handleToolContinuation, handleOneShot, handleOneShotJSON } from "./persona-response.js";
|
|
10
10
|
import { handleHeartbeatCheck, handleEiHeartbeat } from "./heartbeat.js";
|
|
11
11
|
import { handlePersonaGeneration, handlePersonaDescriptions, handlePersonaTraitExtraction } from "./persona-generation.js";
|
|
12
12
|
import {
|
|
@@ -79,6 +79,7 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
79
79
|
handleHeartbeatCheck,
|
|
80
80
|
handleEiHeartbeat,
|
|
81
81
|
handleOneShot,
|
|
82
|
+
handleOneShotJSON,
|
|
82
83
|
handleToolContinuation,
|
|
83
84
|
handleRewriteScan,
|
|
84
85
|
handleRewriteRewrite,
|
|
@@ -139,3 +139,8 @@ export function handleOneShot(_response: LLMResponse, _state: StateManager): voi
|
|
|
139
139
|
// One-shot is handled specially in Processor to fire onOneShotReturned
|
|
140
140
|
// This handler is a no-op placeholder
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
export function handleOneShotJSON(_response: LLMResponse, _state: StateManager): void {
|
|
144
|
+
// One-shot JSON is handled specially in Processor to fire onOneShotJSONReturned
|
|
145
|
+
// This handler is a no-op placeholder
|
|
146
|
+
}
|
|
@@ -11,7 +11,10 @@ export function getMessageContent(msg: { content?: string; verbal_response?: str
|
|
|
11
11
|
|
|
12
12
|
export function normalizeRoomMessages(messages: RoomMessage[], state: StateManager): Message[] {
|
|
13
13
|
const human = state.getHuman();
|
|
14
|
-
const humanName =
|
|
14
|
+
const humanName =
|
|
15
|
+
human.settings?.name_display ||
|
|
16
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
17
|
+
"Human";
|
|
15
18
|
return messages.map(m => {
|
|
16
19
|
const speakerName = m.role === "human"
|
|
17
20
|
? humanName
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, type CeremonyConfig, type PersonaTopic, type Topic, type DataItemBase } from "../types.js";
|
|
1
|
+
import { LLMRequestType, LLMPriority, LLMNextStep, RoomMode, ContextStatus, type CeremonyConfig, type PersonaTopic, type Topic, type DataItemBase } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import { normalizeRoomMessages } from "../handlers/utils.js";
|
|
4
4
|
import { applyDecayToValue } from "../utils/index.js";
|
|
@@ -347,7 +347,7 @@ export function prunePersonaMessages(personaId: string, state: StateManager): vo
|
|
|
347
347
|
if (msgMs >= cutoffMs) break; // Sorted by time, no more old ones
|
|
348
348
|
|
|
349
349
|
const fullyExtracted = m.t && m.p && m.f; // r intentionally excluded — trait extraction deprecated
|
|
350
|
-
if (fullyExtracted) {
|
|
350
|
+
if (fullyExtracted && m.context_status !== ContextStatus.Always) {
|
|
351
351
|
toRemove.push(m.id);
|
|
352
352
|
}
|
|
353
353
|
}
|
|
@@ -59,6 +59,7 @@ export interface ExtractionContext {
|
|
|
59
59
|
messages_analyze: Message[];
|
|
60
60
|
extraction_flag?: "f" | "t" | "p" | "e";
|
|
61
61
|
roomId?: string;
|
|
62
|
+
sources?: string[];
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export interface ExtractionOptions {
|
|
@@ -132,6 +133,7 @@ export function queueFactFind(context: ExtractionContext, state: StateManager, o
|
|
|
132
133
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
133
134
|
extraction_flag: context.extraction_flag,
|
|
134
135
|
message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
|
|
136
|
+
sources: context.sources,
|
|
135
137
|
},
|
|
136
138
|
});
|
|
137
139
|
}
|
|
@@ -173,6 +175,7 @@ export function queueTopicScan(context: ExtractionContext, state: StateManager,
|
|
|
173
175
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
174
176
|
extraction_flag: context.extraction_flag,
|
|
175
177
|
message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
|
|
178
|
+
sources: context.sources,
|
|
176
179
|
},
|
|
177
180
|
});
|
|
178
181
|
}
|
|
@@ -222,6 +225,7 @@ export function queuePersonScan(context: ExtractionContext, state: StateManager,
|
|
|
222
225
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
223
226
|
extraction_flag: context.extraction_flag,
|
|
224
227
|
message_ids_to_mark: chunk.messages_analyze.map(m => m.id),
|
|
228
|
+
sources: context.sources,
|
|
225
229
|
},
|
|
226
230
|
});
|
|
227
231
|
}
|
|
@@ -610,6 +614,7 @@ export function queuePersonUpdate(
|
|
|
610
614
|
candidateIdentifiers: isNewItem ? candidateIdentifiers : undefined,
|
|
611
615
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
612
616
|
extraction_model: context.extraction_model,
|
|
617
|
+
sources: context.sources,
|
|
613
618
|
},
|
|
614
619
|
});
|
|
615
620
|
}
|
package/src/core/processor.ts
CHANGED
|
@@ -112,6 +112,7 @@ import {
|
|
|
112
112
|
deleteQueueItems,
|
|
113
113
|
clearQueue,
|
|
114
114
|
submitOneShot,
|
|
115
|
+
submitOneShotJSON,
|
|
115
116
|
} from "./queue-manager.js";
|
|
116
117
|
import {
|
|
117
118
|
getRoomList,
|
|
@@ -1354,7 +1355,7 @@ const toolNextSteps = new Set([
|
|
|
1354
1355
|
if (result.sessionsProcessed > 0) {
|
|
1355
1356
|
console.log(
|
|
1356
1357
|
`[Processor] Claude Code sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1357
|
-
`${result.
|
|
1358
|
+
`${result.messagesImported} messages imported, ` +
|
|
1358
1359
|
`${result.extractionScansQueued} extraction scans queued`
|
|
1359
1360
|
);
|
|
1360
1361
|
}
|
|
@@ -1407,7 +1408,7 @@ const toolNextSteps = new Set([
|
|
|
1407
1408
|
if (result.sessionsProcessed > 0) {
|
|
1408
1409
|
console.log(
|
|
1409
1410
|
`[Processor] Cursor sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1410
|
-
`${result.
|
|
1411
|
+
`${result.messagesImported} messages imported, ` +
|
|
1411
1412
|
`${result.extractionScansQueued} extraction scans queued`
|
|
1412
1413
|
);
|
|
1413
1414
|
}
|
|
@@ -1484,6 +1485,10 @@ const toolNextSteps = new Set([
|
|
|
1484
1485
|
const guid = response.request.data.guid as string;
|
|
1485
1486
|
this.interface.onOneShotReturned?.(guid, "");
|
|
1486
1487
|
}
|
|
1488
|
+
if (response.request.next_step === LLMNextStep.HandleOneShotJSON) {
|
|
1489
|
+
const guid = response.request.data.guid as string;
|
|
1490
|
+
this.interface.onOneShotJSONReturned?.(guid, null);
|
|
1491
|
+
}
|
|
1487
1492
|
}
|
|
1488
1493
|
|
|
1489
1494
|
this.interface.onError?.({ code, message });
|
|
@@ -1529,6 +1534,11 @@ const toolNextSteps = new Set([
|
|
|
1529
1534
|
this.interface.onOneShotReturned?.(guid, content);
|
|
1530
1535
|
}
|
|
1531
1536
|
|
|
1537
|
+
if (response.request.next_step === LLMNextStep.HandleOneShotJSON) {
|
|
1538
|
+
const guid = response.request.data.guid as string;
|
|
1539
|
+
this.interface.onOneShotJSONReturned?.(guid, response.parsed ?? null);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1532
1542
|
if (response.request.next_step === LLMNextStep.HandlePersonaPreview) {
|
|
1533
1543
|
const guid = response.request.data.guid as string;
|
|
1534
1544
|
const loopCounter = (response.request.data.loop_counter as number) ?? 0;
|
|
@@ -1994,6 +2004,16 @@ const toolNextSteps = new Set([
|
|
|
1994
2004
|
);
|
|
1995
2005
|
}
|
|
1996
2006
|
|
|
2007
|
+
async submitOneShotJSON(guid: string, systemPrompt: string, userPrompt: string): Promise<void> {
|
|
2008
|
+
return submitOneShotJSON(
|
|
2009
|
+
this.stateManager,
|
|
2010
|
+
() => getOneshotModel(this.stateManager),
|
|
2011
|
+
guid,
|
|
2012
|
+
systemPrompt,
|
|
2013
|
+
userPrompt
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1997
2017
|
async generatePersonaPreview(
|
|
1998
2018
|
name: string,
|
|
1999
2019
|
description: string,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PersonaEntity, HumanEntity, DataItemBase, Quote, RoomEntity } from "./types.js";
|
|
1
|
+
import type { PersonaEntity, HumanEntity, DataItemBase, Quote, RoomEntity, RoomMessage } from "./types.js";
|
|
2
|
+
import { RoomMode, ContextStatus } from "./types.js";
|
|
2
3
|
import { StateManager } from "./state-manager.js";
|
|
3
4
|
import { getEmbeddingService, findTopK } from "./embedding-service.js";
|
|
4
5
|
import type { ResponsePromptData, PromptOutput } from "../prompts/index.js";
|
|
@@ -121,7 +122,12 @@ export async function filterHumanDataByVisibility(
|
|
|
121
122
|
selectRelevantQuotes(human.quotes ?? [], currentMessage),
|
|
122
123
|
]);
|
|
123
124
|
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
125
|
+
const humanName =
|
|
126
|
+
human.settings?.name_display ||
|
|
127
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
128
|
+
"Human";
|
|
124
129
|
return {
|
|
130
|
+
name: humanName,
|
|
125
131
|
facts,
|
|
126
132
|
topics,
|
|
127
133
|
people,
|
|
@@ -158,7 +164,12 @@ export async function filterHumanDataByVisibility(
|
|
|
158
164
|
]);
|
|
159
165
|
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
160
166
|
|
|
167
|
+
const humanName =
|
|
168
|
+
human.settings?.name_display ||
|
|
169
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
170
|
+
"Human";
|
|
161
171
|
return {
|
|
172
|
+
name: humanName,
|
|
162
173
|
facts,
|
|
163
174
|
topics,
|
|
164
175
|
people,
|
|
@@ -264,14 +275,30 @@ export async function buildRoomResponsePromptData(
|
|
|
264
275
|
? [...sm.getRoomMessages(room.id)].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
265
276
|
: activePath;
|
|
266
277
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
let sourceMessages: RoomMessage[];
|
|
279
|
+
if (room.mode === RoomMode.FreeForAll) {
|
|
280
|
+
const contextWindowHours = room.context_window_hours
|
|
281
|
+
?? human.settings?.default_context_window_hours
|
|
282
|
+
?? 8;
|
|
283
|
+
const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
|
|
284
|
+
const boundaryMs = room.context_boundary ? new Date(room.context_boundary).getTime() : 0;
|
|
285
|
+
sourceMessages = allSourceMessages.filter(m => {
|
|
286
|
+
const msgMs = new Date(m.timestamp).getTime();
|
|
287
|
+
if (m.context_status === ContextStatus.Always) return true;
|
|
288
|
+
if (m.context_status === ContextStatus.Never) return false;
|
|
289
|
+
if (msgMs < new Date(windowCutoff).getTime()) return false;
|
|
290
|
+
if (boundaryMs && msgMs < boundaryMs) return false;
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
|
|
294
|
+
if (byCount.length > sourceMessages.length) sourceMessages = byCount;
|
|
295
|
+
} else {
|
|
296
|
+
const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
|
|
297
|
+
const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
|
|
298
|
+
const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
|
|
299
|
+
const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
|
|
300
|
+
sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
|
|
301
|
+
}
|
|
275
302
|
|
|
276
303
|
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
277
304
|
const currentMessage = lastMessage ? getMessageContent(lastMessage) : undefined;
|
|
@@ -297,7 +324,10 @@ export async function buildRoomResponsePromptData(
|
|
|
297
324
|
}
|
|
298
325
|
otherParticipants.push({
|
|
299
326
|
id: "human",
|
|
300
|
-
name:
|
|
327
|
+
name:
|
|
328
|
+
human.settings?.name_display ||
|
|
329
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
330
|
+
"Human",
|
|
301
331
|
traits: [],
|
|
302
332
|
is_human: true,
|
|
303
333
|
});
|
|
@@ -77,3 +77,21 @@ export async function submitOneShot(
|
|
|
77
77
|
data: { guid },
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
export async function submitOneShotJSON(
|
|
82
|
+
sm: StateManager,
|
|
83
|
+
getOneshotModel: () => string | undefined,
|
|
84
|
+
guid: string,
|
|
85
|
+
systemPrompt: string,
|
|
86
|
+
userPrompt: string
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
sm.queue_enqueue({
|
|
89
|
+
type: LLMRequestType.JSON,
|
|
90
|
+
priority: LLMPriority.High,
|
|
91
|
+
system: systemPrompt,
|
|
92
|
+
user: userPrompt,
|
|
93
|
+
next_step: LLMNextStep.HandleOneShotJSON,
|
|
94
|
+
model: getOneshotModel(),
|
|
95
|
+
data: { guid },
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/core/room-manager.ts
CHANGED
|
@@ -149,8 +149,19 @@ export async function sendFfaMessage(
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const now = new Date().toISOString();
|
|
152
|
+
|
|
153
|
+
// FFA human messages always hang off the room root (the initial message with parent_id === null)
|
|
154
|
+
// so the tree is a flat star: root → every human turn, each human turn → persona responses.
|
|
155
|
+
// This gives the context window a bounded, predictable shape instead of a chain.
|
|
156
|
+
const ffaRootMsg = sm.getRoomMessages(roomId).find(m => m.parent_id === null);
|
|
157
|
+
if (!ffaRootMsg) {
|
|
158
|
+
onError({ code: "ROOM_NO_ROOT", message: "FFA room has no root message. Try archiving and recreating the room." });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const ffaParentId = ffaRootMsg.id;
|
|
162
|
+
|
|
152
163
|
const existing = sm.getRoomMessages(roomId).find(
|
|
153
|
-
m => m.role === "human" && m.
|
|
164
|
+
m => m.role === "human" && m.id === room.active_node_id && m.parent_id === ffaParentId
|
|
154
165
|
);
|
|
155
166
|
|
|
156
167
|
let humanMsgId: string;
|
|
@@ -164,7 +175,7 @@ export async function sendFfaMessage(
|
|
|
164
175
|
} else {
|
|
165
176
|
const msg: RoomMessage = {
|
|
166
177
|
id: crypto.randomUUID(),
|
|
167
|
-
parent_id:
|
|
178
|
+
parent_id: ffaParentId,
|
|
168
179
|
role: "human",
|
|
169
180
|
verbal_response: content ?? undefined,
|
|
170
181
|
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
@@ -278,9 +289,14 @@ export async function activateRoom(
|
|
|
278
289
|
|
|
279
290
|
const currentRound = allMessages.filter(m => m.parent_id === room.active_node_id);
|
|
280
291
|
|
|
292
|
+
const humanDisplayName =
|
|
293
|
+
human.settings?.name_display ||
|
|
294
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
295
|
+
"Human";
|
|
296
|
+
|
|
281
297
|
const context: RoomHistoryMessage[] = sm.getRoomActivePath(roomId).map(m => ({
|
|
282
298
|
speaker_name: m.role === "human"
|
|
283
|
-
?
|
|
299
|
+
? humanDisplayName
|
|
284
300
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
285
301
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
286
302
|
verbal_response: getMessageContent(m) || undefined,
|
|
@@ -290,7 +306,7 @@ export async function activateRoom(
|
|
|
290
306
|
const candidates: RoomJudgeCandidate[] = currentRound.map(m => ({
|
|
291
307
|
message_id: m.id,
|
|
292
308
|
speaker_name: m.role === "human"
|
|
293
|
-
?
|
|
309
|
+
? humanDisplayName
|
|
294
310
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
295
311
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
296
312
|
verbal_response: getMessageContent(m) || undefined,
|
|
@@ -305,6 +321,7 @@ export async function activateRoom(
|
|
|
305
321
|
long_description: judgePersona.long_description,
|
|
306
322
|
traits: judgePersona.traits,
|
|
307
323
|
},
|
|
324
|
+
human: { name: humanDisplayName },
|
|
308
325
|
context,
|
|
309
326
|
candidates,
|
|
310
327
|
});
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
RoomSummary,
|
|
18
18
|
RoomCreationInput,
|
|
19
19
|
} from "./types.js";
|
|
20
|
+
import { RoomMode } from "./types.js";
|
|
20
21
|
import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
|
|
21
22
|
import type { ThemeDefinition } from './types/entities.js';
|
|
22
23
|
import type { Storage } from "../storage/interface.js";
|
|
@@ -70,6 +71,7 @@ export class StateManager {
|
|
|
70
71
|
this.migrateProviderModel();
|
|
71
72
|
this.migrateRoomMessageContent();
|
|
72
73
|
this.migrateThemes();
|
|
74
|
+
this.migrateFfaParentIds();
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
private migrateRoomMessageContent(): void {
|
|
@@ -576,6 +578,30 @@ export class StateManager {
|
|
|
576
578
|
this.humanState.set(human);
|
|
577
579
|
}
|
|
578
580
|
|
|
581
|
+
private migrateFfaParentIds(): void {
|
|
582
|
+
const rooms = this.roomState.getAll(true);
|
|
583
|
+
let migratedCount = 0;
|
|
584
|
+
|
|
585
|
+
for (const room of rooms) {
|
|
586
|
+
if (room.mode !== RoomMode.FreeForAll) continue;
|
|
587
|
+
const rootMsg = room.messages.find(m => m.parent_id === null);
|
|
588
|
+
if (!rootMsg) continue;
|
|
589
|
+
|
|
590
|
+
for (const msg of room.messages) {
|
|
591
|
+
if (msg.role !== "human") continue;
|
|
592
|
+
if (msg.id === rootMsg.id) continue;
|
|
593
|
+
if (msg.parent_id === rootMsg.id) continue;
|
|
594
|
+
msg.parent_id = rootMsg.id;
|
|
595
|
+
migratedCount++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (migratedCount > 0) {
|
|
600
|
+
this.scheduleSave();
|
|
601
|
+
console.log(`[StateManager] Migrated ${migratedCount} FFA human messages to root parent_id`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
579
605
|
getHuman(): HumanEntity {
|
|
580
606
|
return this.humanState.get();
|
|
581
607
|
}
|
|
@@ -15,6 +15,7 @@ export interface DataItemBase {
|
|
|
15
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
16
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
17
17
|
interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
|
|
18
|
+
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
18
19
|
persona_groups?: string[];
|
|
19
20
|
embedding?: number[];
|
|
20
21
|
rewrite_checked?: boolean; // True after rewrite scan finds no changes. Cleared automatically when extraction upserts a fresh item.
|