ei-tui 0.6.6 → 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/README.md +16 -7
- package/src/cli/commands/people.ts +1 -0
- package/src/cli/mcp.ts +36 -11
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli/retrieval.ts +3 -1
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +20 -4
- 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 +24 -7
- package/src/core/persona-manager.ts +3 -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 +74 -0
- package/src/core/tools/builtin/read-memory.ts +1 -1
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +13 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +4 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +24 -0
- package/src/core/utils/theme-codec.ts +78 -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/README.md
CHANGED
|
@@ -72,9 +72,11 @@ ei "What are the user's current preferences, active projects, and workflow?"
|
|
|
72
72
|
|
|
73
73
|
Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
|
|
74
74
|
people, topics, personas. Use it when the user references past work, mentions how they like things done,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
asks "how did we do X," or needs to look up a person by any name, handle, or account (GitHub username,
|
|
76
|
+
Discord handle, email, nickname, etc.) — people results include an `identifiers` array covering all
|
|
77
|
+
known accounts and aliases for that person. Use `ei --persona "Beta" "walruses"` to scope results to
|
|
78
|
+
what a specific persona has learned. Use `ei personas "name"` to find personas by name. Query again
|
|
79
|
+
mid-session when they correct you or reference something from a previous session.
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
### Claude Code
|
|
@@ -87,9 +89,11 @@ natural-language query about the user's preferences, active projects, and workfl
|
|
|
87
89
|
A `persona` filter is available to scope results to what a specific persona has learned.
|
|
88
90
|
Use `type: "personas"` to search for personas by name.
|
|
89
91
|
|
|
90
|
-
Use Ei when the user references past decisions, mentions people or preferences,
|
|
91
|
-
"how did we do X
|
|
92
|
-
|
|
92
|
+
Use Ei when the user references past decisions, mentions people or preferences, asks
|
|
93
|
+
"how did we do X," or needs to look up a person by any name, handle, or account — people
|
|
94
|
+
results include an `identifiers` array (GitHub username, Discord handle, email, nickname, etc.)
|
|
95
|
+
covering all known accounts and aliases. Query again when they correct you or reference
|
|
96
|
+
something from a previous session.
|
|
93
97
|
```
|
|
94
98
|
|
|
95
99
|
### Cursor
|
|
@@ -111,6 +115,9 @@ conversations (facts, people, topics, quotes, personas).
|
|
|
111
115
|
doesn't have that context.
|
|
112
116
|
- You need the user's preferences, contacts, or project conventions (e.g. who to ask for
|
|
113
117
|
access, how something was fixed).
|
|
118
|
+
- You need to look up a person by any name, handle, or account — people results include an
|
|
119
|
+
`identifiers` array (GitHub username, Discord handle, email, nickname, etc.) covering all
|
|
120
|
+
known accounts and aliases for that person.
|
|
114
121
|
- The question is about the user personally (people, workflow, prior discussions) rather
|
|
115
122
|
than only code.
|
|
116
123
|
|
|
@@ -138,7 +145,9 @@ The installed tool gives OpenCode agents access to all five data types with prop
|
|
|
138
145
|
|
|
139
146
|
All search commands return arrays. Each result includes a `type` field.
|
|
140
147
|
|
|
141
|
-
**Fact /
|
|
148
|
+
**Fact / Topic**: `{ type, id, name, description, sentiment, ...type-specific fields }`
|
|
149
|
+
|
|
150
|
+
**Person**: `{ type, id, name, description, relationship, sentiment, identifiers[] }` — `identifiers` contains all known accounts and aliases (e.g. `{ type: "GitHub", value: "flare576" }`)
|
|
142
151
|
|
|
143
152
|
**Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
|
|
144
153
|
|
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 {
|
|
@@ -16,7 +16,7 @@ export function createMcpServer(): McpServer {
|
|
|
16
16
|
"ei_search",
|
|
17
17
|
{
|
|
18
18
|
description:
|
|
19
|
-
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
|
|
19
|
+
"Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. People results include an identifiers array (e.g. GitHub username, Discord handle, email, nickname) — query by any name or handle to find what Ei knows about that person. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
|
|
20
20
|
inputSchema: {
|
|
21
21
|
query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
|
|
22
22
|
type: z
|
|
@@ -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/retrieval.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { StorageState, Quote, Fact, Person, Topic } from "../core/types";
|
|
2
2
|
import type { PersonaEntity } from "../core/types/entities.js";
|
|
3
|
-
import type { PersonaTrait, PersonaTopic } from "../core/types/data-items.js";
|
|
3
|
+
import type { PersonaTrait, PersonaTopic, PersonIdentifier } from "../core/types/data-items.js";
|
|
4
4
|
import { decodeAllEmbeddings } from "../storage/embeddings";
|
|
5
5
|
import { crossFind } from "../core/utils/index.ts";
|
|
6
6
|
import { join } from "path";
|
|
@@ -102,6 +102,7 @@ export interface PersonResult {
|
|
|
102
102
|
description: string;
|
|
103
103
|
relationship: string;
|
|
104
104
|
sentiment: number;
|
|
105
|
+
identifiers: PersonIdentifier[];
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
export interface TopicResult {
|
|
@@ -182,6 +183,7 @@ function mapPerson(person: Person): PersonResult {
|
|
|
182
183
|
description: person.description,
|
|
183
184
|
relationship: person.relationship,
|
|
184
185
|
sentiment: person.sentiment,
|
|
186
|
+
identifiers: person.identifiers ?? [],
|
|
185
187
|
};
|
|
186
188
|
}
|
|
187
189
|
|
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
|
|
|
@@ -14,7 +14,7 @@ import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
import { resolveMessageWindow, getMessageText, normalizeRoomMessages } from "./utils.js";
|
|
17
|
-
import { sanitizeEiPersonaIdentifiers } from "../utils/identifier-utils.js";
|
|
17
|
+
import { sanitizeEiPersonaIdentifiers, normalizeIdentifierType } from "../utils/identifier-utils.js";
|
|
18
18
|
|
|
19
19
|
export function handleTopicMatch(response: LLMResponse, state: StateManager): void {
|
|
20
20
|
const result = response.parsed as ItemMatchResult | undefined;
|
|
@@ -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,12 +284,16 @@ 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) {
|
|
285
294
|
const llmIdentifiers: PersonIdentifier[] = sanitizeEiPersonaIdentifiers(
|
|
286
295
|
(result.identifiers ?? []).map(i => ({
|
|
287
|
-
type: i.type,
|
|
296
|
+
type: normalizeIdentifierType(i.type, state),
|
|
288
297
|
value: i.value,
|
|
289
298
|
...(i.is_primary ? { is_primary: i.is_primary } : {}),
|
|
290
299
|
})),
|
|
@@ -293,7 +302,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
293
302
|
const allCandidateIds = [...llmIdentifiers, ...candidateIdentifiers];
|
|
294
303
|
if (allCandidateIds.length === 0) {
|
|
295
304
|
const hasSpace = candidateName.includes(' ');
|
|
296
|
-
allCandidateIds.push({ type: hasSpace ? "
|
|
305
|
+
allCandidateIds.push({ type: hasSpace ? "Full Name" : "Nickname", value: candidateName, is_primary: true });
|
|
297
306
|
}
|
|
298
307
|
const deduped: PersonIdentifier[] = [];
|
|
299
308
|
for (const id of allCandidateIds) {
|
|
@@ -304,7 +313,13 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
304
313
|
resolvedIdentifiers = deduped;
|
|
305
314
|
} else {
|
|
306
315
|
const base = [...(existingPerson?.identifiers ?? [])];
|
|
307
|
-
const sanitizedToAdd = sanitizeEiPersonaIdentifiers(
|
|
316
|
+
const sanitizedToAdd = sanitizeEiPersonaIdentifiers(
|
|
317
|
+
(result.identifiers_to_add ?? []).map(i => ({
|
|
318
|
+
...i,
|
|
319
|
+
type: normalizeIdentifierType(i.type, state),
|
|
320
|
+
})),
|
|
321
|
+
state
|
|
322
|
+
);
|
|
308
323
|
for (const id of sanitizedToAdd) {
|
|
309
324
|
if (!base.some(e => e.value === id.value)) {
|
|
310
325
|
base.push({ type: id.type, value: id.value, ...(id.is_primary ? { is_primary: id.is_primary } : {}) });
|
|
@@ -329,6 +344,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
329
344
|
learned_by: isNewItem ? primaryId : existingPerson?.learned_by,
|
|
330
345
|
last_changed_by: primaryId,
|
|
331
346
|
interested_personas: interestedPersonas,
|
|
347
|
+
sources: personSources.length > 0 ? personSources : undefined,
|
|
332
348
|
persona_groups: personaGroupsMerged,
|
|
333
349
|
embedding,
|
|
334
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
|
}
|
|
@@ -281,6 +285,7 @@ export function queueDirectTopicUpdate(
|
|
|
281
285
|
isNewItem: false,
|
|
282
286
|
existingItemId: topic.id,
|
|
283
287
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
288
|
+
extraction_model: extractionModel,
|
|
284
289
|
},
|
|
285
290
|
});
|
|
286
291
|
}
|
|
@@ -425,14 +430,11 @@ export function queueTopicUpdate(
|
|
|
425
430
|
user: prompt.user,
|
|
426
431
|
next_step: LLMNextStep.HandleTopicUpdate,
|
|
427
432
|
data: {
|
|
428
|
-
|
|
429
|
-
personaDisplayName: context.personaDisplayName,
|
|
430
|
-
roomId: context.roomId,
|
|
433
|
+
...context,
|
|
431
434
|
isNewItem,
|
|
432
435
|
existingItemId: existingItem?.id,
|
|
433
436
|
candidateName: isNewItem ? context.candidateName : undefined,
|
|
434
437
|
candidateDescription: isNewItem ? context.candidateDescription : undefined,
|
|
435
|
-
candidateCategory: context.candidateCategory,
|
|
436
438
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
437
439
|
},
|
|
438
440
|
});
|
|
@@ -469,13 +471,25 @@ export function queueEventSummary(
|
|
|
469
471
|
|
|
470
472
|
const allMessages = state.messages_get(personaId);
|
|
471
473
|
const extractionModel = options?.extraction_model;
|
|
474
|
+
const gapMs = gapHours * 60 * 60 * 1000;
|
|
475
|
+
const now = Date.now();
|
|
472
476
|
let totalChunks = 0;
|
|
473
477
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
for (const windowMessages of windows) {
|
|
478
|
+
for (let i = 0; i < windows.length; i++) {
|
|
479
|
+
const windowMessages = windows[i];
|
|
477
480
|
if (windowMessages.length === 0) continue;
|
|
478
481
|
|
|
482
|
+
const isLastWindow = i === windows.length - 1;
|
|
483
|
+
if (isLastWindow) {
|
|
484
|
+
const lastMsgTime = new Date(windowMessages[windowMessages.length - 1].timestamp).getTime();
|
|
485
|
+
if (now - lastMsgTime < gapMs) {
|
|
486
|
+
console.log(`[queueEventSummary] Skipping open window for ${persona.display_name} — last message < ${gapHours}h ago`);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
state.messages_markExtracted(personaId, windowMessages.map(m => m.id), "e");
|
|
492
|
+
|
|
479
493
|
const windowStartTime = new Date(windowMessages[0].timestamp).getTime();
|
|
480
494
|
const messages_context = allMessages.filter(
|
|
481
495
|
m => m.e === true && new Date(m.timestamp).getTime() < windowStartTime
|
|
@@ -599,6 +613,8 @@ export function queuePersonUpdate(
|
|
|
599
613
|
candidateRelationship: context.candidateRelationship,
|
|
600
614
|
candidateIdentifiers: isNewItem ? candidateIdentifiers : undefined,
|
|
601
615
|
analyze_from_timestamp: getAnalyzeFromTimestamp(chunk),
|
|
616
|
+
extraction_model: context.extraction_model,
|
|
617
|
+
sources: context.sources,
|
|
602
618
|
},
|
|
603
619
|
});
|
|
604
620
|
}
|
|
@@ -653,6 +669,7 @@ export async function queueTopicValidate(
|
|
|
653
669
|
data: {
|
|
654
670
|
entity_type: "topic",
|
|
655
671
|
entity_ids: [existingTopic.id, newTopic.id],
|
|
672
|
+
extraction_model: extractionModel,
|
|
656
673
|
},
|
|
657
674
|
});
|
|
658
675
|
}
|
|
@@ -20,6 +20,9 @@ export async function getPersonaList(sm: StateManager): Promise<PersonaSummary[]
|
|
|
20
20
|
unread_count: sm.messages_countUnread(entity.id),
|
|
21
21
|
last_activity: entity.last_activity,
|
|
22
22
|
context_boundary: entity.context_boundary,
|
|
23
|
+
avatar_emoji: entity.avatar_emoji,
|
|
24
|
+
avatar_image: entity.avatar_image,
|
|
25
|
+
preferred_theme: entity.preferred_theme,
|
|
23
26
|
}));
|
|
24
27
|
}
|
|
25
28
|
|
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,
|