ei-tui 1.1.0 → 1.3.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/README.md +16 -0
- package/package.json +2 -23
- package/src/cli/README.md +12 -2
- package/src/cli/mcp.ts +12 -4
- package/src/cli/retrieval.ts +162 -0
- package/src/cli.ts +7 -1
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +5 -7
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-matching.ts +8 -0
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +48 -0
- package/src/core/handlers/persona-generation.ts +4 -8
- package/src/core/handlers/persona-response.ts +3 -4
- package/src/core/handlers/persona-topics.ts +2 -4
- package/src/core/handlers/rewrite.ts +26 -9
- package/src/core/handlers/rooms.ts +6 -12
- package/src/core/heartbeat-manager.ts +10 -0
- package/src/core/llm-client.ts +13 -3
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +45 -22
- package/src/core/orchestrators/human-extraction.ts +10 -1
- package/src/core/processor.ts +275 -7
- package/src/core/queue-manager.ts +10 -0
- package/src/core/state-manager.ts +35 -0
- package/src/core/tools/builtin/fetch-memory.ts +6 -6
- package/src/core/tools/builtin/fetch-message.ts +27 -1
- package/src/core/tools/builtin/find-memory.ts +11 -3
- package/src/core/tools/index.ts +3 -3
- package/src/core/tools/types.ts +1 -1
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +7 -1
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +3 -1
- package/src/core/types/llm.ts +0 -9
- package/src/core/utils/message-id.ts +114 -0
- package/src/integrations/claude-code/importer.ts +12 -5
- package/src/integrations/cursor/importer.ts +12 -5
- package/src/integrations/document/importer.ts +1 -1
- package/src/integrations/document/unsource.ts +11 -14
- package/src/integrations/opencode/importer.ts +19 -6
- package/src/integrations/opencode/json-reader.ts +65 -0
- package/src/integrations/opencode/sqlite-reader.ts +33 -0
- package/src/integrations/opencode/types.ts +8 -0
- package/src/integrations/persona-history/importer.ts +9 -0
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- package/src/prompts/heartbeat/check.ts +5 -2
- package/src/prompts/heartbeat/ei.ts +7 -0
- package/src/prompts/heartbeat/types.ts +5 -0
- package/src/prompts/index.ts +3 -0
- package/src/prompts/response/sections.ts +30 -16
- package/src/prompts/room/sections.ts +28 -6
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/src/prompts/trait-utils.ts +33 -0
- package/tui/README.md +2 -0
- package/tui/src/commands/generate.tsx +98 -0
- package/tui/src/commands/unsource.tsx +17 -10
- package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/context/ei.tsx +49 -2
- package/tui/src/util/help-content.ts +11 -0
- package/tui/src/util/logger.ts +22 -2
- package/tui/src/util/provider-detection.ts +5 -2
- package/tui/src/util/yaml-provider.ts +2 -8
package/README.md
CHANGED
|
@@ -185,6 +185,22 @@ Ei splits the document into segments, runs them through the extraction pipeline,
|
|
|
185
185
|
|
|
186
186
|
Both surfaces show you which documents have been imported and let you remove their extracted knowledge (web: Delete button in the Documents tab; TUI: `/unsource <source_tag>`).
|
|
187
187
|
|
|
188
|
+
## Knowledge Share
|
|
189
|
+
|
|
190
|
+
Sometimes you want to take what Ei knows and turn it into something you can hand to another human. A new teammate joining a project. A briefing doc before a meeting. A brain dump before a vacation.
|
|
191
|
+
|
|
192
|
+
**TUI**:
|
|
193
|
+
```bash
|
|
194
|
+
/generate everything about the Uniform project
|
|
195
|
+
/generate comprehensive runbook for the IDP deployment issues
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Give it a subject description and Ei searches its memory, chases down related entities, and synthesizes a clean markdown document using your `rewrite_model` (an Opus-class model is recommended — it'll warn you if you're about to use something less capable). The document lands in `$EI_DATA_PATH/docs/` automatically.
|
|
199
|
+
|
|
200
|
+
`/generate` with no args opens a list of your existing generated documents where you can re-run, re-export, or delete them.
|
|
201
|
+
|
|
202
|
+
**Web**: Open **☰ menu** → **My Data** → **Documents** tab. Click **Generate**, enter a subject, and Ei does the rest. Generated docs appear in the same list as imported ones.
|
|
203
|
+
|
|
188
204
|
## Built-in Tool Integrations
|
|
189
205
|
|
|
190
206
|
Personas can use tools. Not just read-from-memory tools — *actual* tools. Web search. Your music. Your filesystem. Here's what ships with Ei out of the box:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ei-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"author": "Flare576",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,28 +51,7 @@
|
|
|
51
51
|
"test:e2e:tui": "cd tui && npm run test:e2e",
|
|
52
52
|
"test:e2e:ui": "playwright test --ui",
|
|
53
53
|
"test:e2e:debug": "playwright test --debug",
|
|
54
|
-
"test:evals": "vite-node tests/evals/
|
|
55
|
-
"test:evals:observe": "vite-node tests/evals/reflection-critic.observe.ts",
|
|
56
|
-
"test:evals:fact-find": "vite-node tests/evals/fact-find.eval.ts",
|
|
57
|
-
"test:evals:topic-scan": "vite-node tests/evals/topic-scan.eval.ts",
|
|
58
|
-
"test:evals:topic-match": "vite-node tests/evals/topic-match.eval.ts",
|
|
59
|
-
"test:evals:topic-update": "vite-node tests/evals/topic-update.eval.ts",
|
|
60
|
-
"test:evals:topic-technical": "vite-node tests/evals/topic-technical.eval.ts",
|
|
61
|
-
"test:evals:rewrite-scan": "vite-node tests/evals/rewrite-scan.eval.ts",
|
|
62
|
-
"test:evals:rewrite-rewrite": "vite-node tests/evals/rewrite-rewrite.eval.ts",
|
|
63
|
-
"test:evals:rewrite-real-data": "vite-node tests/evals/rewrite-real-data.eval.ts",
|
|
64
|
-
"test:evals:topic-validate": "vite-node tests/evals/topic-validate.eval.ts",
|
|
65
|
-
"test:evals:person-scan": "vite-node tests/evals/person-scan.eval.ts",
|
|
66
|
-
"test:evals:person-scan-confidence": "vite-node tests/evals/person-scan-confidence.eval.ts",
|
|
67
|
-
|
|
68
|
-
"test:evals:person-update": "vite-node tests/evals/person-update.eval.ts",
|
|
69
|
-
"test:evals:persona-trait": "vite-node tests/evals/persona-trait-extraction.eval.ts",
|
|
70
|
-
"test:evals:dedup": "vite-node tests/evals/dedup-tool-calls.eval.ts",
|
|
71
|
-
"test:evals:response-read-memory": "vite-node tests/evals/response-read-memory.eval.ts",
|
|
72
|
-
"test:evals:response-pending-update": "vite-node tests/evals/response-pending-update.eval.ts",
|
|
73
|
-
"test:evals:heartbeat-pending-update": "vite-node tests/evals/heartbeat-pending-update.eval.ts",
|
|
74
|
-
"test:evals:real-data": "vite-node tests/evals/real-data-example.eval.ts",
|
|
75
|
-
"test:evals:persona-data-check": "vite-node tests/evals/persona-data-check.eval.ts",
|
|
54
|
+
"test:evals": "vite-node tests/evals/run.ts",
|
|
76
55
|
"test:all": "npm run test && npm run test:e2e && npm run test:e2e:tui",
|
|
77
56
|
"typecheck": "tsc --noEmit",
|
|
78
57
|
"web": "cd web && npm run dev",
|
package/src/cli/README.md
CHANGED
|
@@ -13,7 +13,7 @@ ei personas -n 5 "query string" # Return up to 5 personas (name match)
|
|
|
13
13
|
ei --persona "Beta" "query string" # Filter results to what Beta has learned
|
|
14
14
|
ei --recent # Most recently mentioned items (no query needed)
|
|
15
15
|
ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
|
|
16
|
-
ei --id <id> # Look up a
|
|
16
|
+
ei --id <id> # Look up entity by ID — or fetch a message by FQ ID
|
|
17
17
|
echo <id> | ei --id # Look up entity by ID from stdin
|
|
18
18
|
ei --install # Register Ei with OpenCode, Claude Code, and Cursor
|
|
19
19
|
ei mcp # Start the Ei MCP stdio server (for Cursor/Claude Desktop)
|
|
@@ -29,6 +29,16 @@ The `--id` flag is designed for piping. For example, search for a topic and then
|
|
|
29
29
|
ei "memory leak" | jq '.[0].id' | ei --id
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
It also resolves fully-qualified message IDs from any supported integration, returning the original message content and session context:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
ei --id "opencode:jeremys-macbook-pro:ses_38a7...:msg_c75b..."
|
|
36
|
+
ei --id "claudecode:my-machine:session-uuid:message-uuid"
|
|
37
|
+
ei --id "cursor:my-machine:composer-uuid:bubble-uuid"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Quotes surfaced by `ei_search` or `ei_find_memory` include a `message_id` field in this format — pipe it to `ei --id` to read the original conversation.
|
|
41
|
+
|
|
32
42
|
# OpenCode Integration
|
|
33
43
|
|
|
34
44
|
## Quick Install
|
|
@@ -156,7 +166,7 @@ The MCP server exposes these tools to Claude Code, Cursor, and OpenCode:
|
|
|
156
166
|
| `ei_lookup` | Full-record lookup for any entity by ID (facts, topics, people, quotes, personas). |
|
|
157
167
|
| `ei_find_memory` | Grouped human-data search — facts, topics, people, quotes. Returns results grouped by type. Mirrors the persona `find_memory` tool interface. |
|
|
158
168
|
| `ei_fetch_memory` | Full-record lookup for a human entity (Fact, Topic, Person, or Quote) by ID. Returns the complete record including all fields. |
|
|
159
|
-
| `ei_fetch_message` | Retrieve a specific message by ID with optional `before`/`after` context window.
|
|
169
|
+
| `ei_fetch_message` | Retrieve a specific message by fully-qualified ID with optional `before`/`after` context window. Routes to the correct source: `ei:uuid` searches Ei state, `opencode:machine:session:id` queries the OpenCode SQLite DB, `claudecode:...` scans Claude Code JSONL files, `cursor:...` reads the Cursor global DB. Returns message content, surrounding context, and session metadata. |
|
|
160
170
|
|
|
161
171
|
### `ei_search` / `ei_find_memory` arguments
|
|
162
172
|
|
package/src/cli/mcp.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { retrieveBalanced, lookupById, loadLatestState, type BalancedResult } from "./retrieval.js";
|
|
4
|
+
import { retrieveBalanced, lookupById, resolveExternalMessage, loadLatestState, type BalancedResult } from "./retrieval.js";
|
|
5
5
|
import type { StorageState } from "../core/types.js";
|
|
6
6
|
import type { Message } from "../core/types.js";
|
|
7
7
|
import type { RoomMessage } from "../core/types/rooms.js";
|
|
@@ -270,6 +270,17 @@ export function createMcpServer(): McpServer {
|
|
|
270
270
|
},
|
|
271
271
|
},
|
|
272
272
|
async ({ id, before: beforeCount, after: afterCount }) => {
|
|
273
|
+
const beforeN = Math.max(0, Math.floor(beforeCount ?? 0));
|
|
274
|
+
const afterN = Math.max(0, Math.floor(afterCount ?? 0));
|
|
275
|
+
|
|
276
|
+
const externalResult = await resolveExternalMessage(id, beforeN, afterN);
|
|
277
|
+
if (externalResult) {
|
|
278
|
+
if ("error" in externalResult) {
|
|
279
|
+
return { content: [{ type: "text" as const, text: String(externalResult.error) }] };
|
|
280
|
+
}
|
|
281
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(externalResult, null, 2) }] };
|
|
282
|
+
}
|
|
283
|
+
|
|
273
284
|
const state = await loadLatestState();
|
|
274
285
|
if (!state) {
|
|
275
286
|
return {
|
|
@@ -277,9 +288,6 @@ export function createMcpServer(): McpServer {
|
|
|
277
288
|
};
|
|
278
289
|
}
|
|
279
290
|
|
|
280
|
-
const beforeN = Math.max(0, Math.floor(beforeCount ?? 0));
|
|
281
|
-
const afterN = Math.max(0, Math.floor(afterCount ?? 0));
|
|
282
|
-
|
|
283
291
|
const stripPersonaMessage = (m: Message) => ({
|
|
284
292
|
id: m.id,
|
|
285
293
|
role: m.role,
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { crossFind } from "../core/utils/index.ts";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { readFile } from "fs/promises";
|
|
8
8
|
import { getEmbeddingService, findTopK } from "../core/embedding-service";
|
|
9
|
+
import { parseMessageId } from "../core/utils/message-id.js";
|
|
10
|
+
import { getMachineId } from "../integrations/machine-id.js";
|
|
9
11
|
|
|
10
12
|
const STATE_FILE = "state.json";
|
|
11
13
|
const BACKUP_FILE = "state.backup.json";
|
|
@@ -86,6 +88,7 @@ export interface QuoteResult {
|
|
|
86
88
|
text: string;
|
|
87
89
|
speaker: string;
|
|
88
90
|
timestamp: string;
|
|
91
|
+
message_id: string | null;
|
|
89
92
|
linked_items: LinkedItem[];
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -164,6 +167,7 @@ export function mapQuote(quote: Quote, state: StorageState): QuoteResult {
|
|
|
164
167
|
text: quote.text,
|
|
165
168
|
speaker: quote.speaker,
|
|
166
169
|
timestamp: quote.timestamp,
|
|
170
|
+
message_id: quote.message_id,
|
|
167
171
|
linked_items: resolveLinkedItems(quote.data_item_ids, state),
|
|
168
172
|
};
|
|
169
173
|
}
|
|
@@ -394,6 +398,164 @@ export async function retrieveBalanced(
|
|
|
394
398
|
return embeddingFinal;
|
|
395
399
|
}
|
|
396
400
|
|
|
401
|
+
const OPENCODE_MESSAGE_ID = /^msg_[a-zA-Z0-9]+$/;
|
|
402
|
+
|
|
403
|
+
/** @deprecated Use resolveExternalMessage */
|
|
404
|
+
export async function resolveOpenCodeMessage(
|
|
405
|
+
id: string,
|
|
406
|
+
before = 0,
|
|
407
|
+
after = 0
|
|
408
|
+
): Promise<Record<string, unknown> | null> {
|
|
409
|
+
return resolveExternalMessage(id, before, after);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function resolveExternalMessage(
|
|
413
|
+
id: string,
|
|
414
|
+
before = 0,
|
|
415
|
+
after = 0
|
|
416
|
+
): Promise<Record<string, unknown> | null> {
|
|
417
|
+
const parsed = parseMessageId(id);
|
|
418
|
+
|
|
419
|
+
switch (parsed.integration) {
|
|
420
|
+
case "ei": {
|
|
421
|
+
const state = await loadLatestState();
|
|
422
|
+
if (!state) return null;
|
|
423
|
+
|
|
424
|
+
for (const { entity: persona, messages } of Object.values(state.personas)) {
|
|
425
|
+
const idx = messages.findIndex(m => m.id === id);
|
|
426
|
+
if (idx === -1) continue;
|
|
427
|
+
const msg = messages[idx];
|
|
428
|
+
return {
|
|
429
|
+
type: "opencode_message",
|
|
430
|
+
message: { id: msg.id, role: msg.role, content: msg.content ?? "", timestamp: msg.timestamp },
|
|
431
|
+
before: messages.slice(Math.max(0, idx - before), idx).map(m => ({ id: m.id, role: m.role, content: m.content ?? "", timestamp: m.timestamp })),
|
|
432
|
+
after: messages.slice(idx + 1, idx + 1 + after).map(m => ({ id: m.id, role: m.role, content: m.content ?? "", timestamp: m.timestamp })),
|
|
433
|
+
session: { id: persona.id, title: persona.display_name, directory: "" },
|
|
434
|
+
source: "ei",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const room of Object.values(state.rooms ?? {})) {
|
|
439
|
+
const idx = room.messages.findIndex(m => m.id === id);
|
|
440
|
+
if (idx === -1) continue;
|
|
441
|
+
const msg = room.messages[idx];
|
|
442
|
+
return {
|
|
443
|
+
type: "opencode_message",
|
|
444
|
+
message: { id: msg.id, role: msg.role, content: msg.content ?? "", timestamp: msg.timestamp },
|
|
445
|
+
before: room.messages.slice(Math.max(0, idx - before), idx).map(m => ({ id: m.id, role: m.role, content: m.content ?? "", timestamp: m.timestamp })),
|
|
446
|
+
after: room.messages.slice(idx + 1, idx + 1 + after).map(m => ({ id: m.id, role: m.role, content: m.content ?? "", timestamp: m.timestamp })),
|
|
447
|
+
session: { id: room.id, title: room.display_name, directory: "" },
|
|
448
|
+
source: "ei",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case "opencode": {
|
|
456
|
+
if (parsed.machine !== getMachineId()) {
|
|
457
|
+
return { error: `Message is from machine '${parsed.machine}', not available on this machine (${getMachineId()})` };
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const { createOpenCodeReader } = await import("../integrations/opencode/reader-factory.js");
|
|
461
|
+
const reader = await createOpenCodeReader();
|
|
462
|
+
const win = await reader.getMessageById(parsed.nativeId, before, after);
|
|
463
|
+
if (!win) return null;
|
|
464
|
+
return {
|
|
465
|
+
type: "opencode_message",
|
|
466
|
+
message: { id: win.message.id, role: win.message.role, content: win.message.content, timestamp: win.message.timestamp, agent: win.message.agent },
|
|
467
|
+
before: win.before.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
468
|
+
after: win.after.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
469
|
+
session: { id: win.session.id, title: win.session.title, directory: win.session.directory },
|
|
470
|
+
source: "opencode",
|
|
471
|
+
};
|
|
472
|
+
} catch {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
case "claudecode": {
|
|
478
|
+
if (parsed.machine !== getMachineId()) {
|
|
479
|
+
return { error: `Message is from machine '${parsed.machine}', not available on this machine (${getMachineId()})` };
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const { ClaudeCodeReader } = await import("../integrations/claude-code/reader.js");
|
|
483
|
+
const reader = new ClaudeCodeReader();
|
|
484
|
+
const messages = await reader.getMessagesForSession(parsed.session!);
|
|
485
|
+
const idx = messages.findIndex(m => m.id === parsed.nativeId);
|
|
486
|
+
if (idx === -1) return null;
|
|
487
|
+
const msg = messages[idx];
|
|
488
|
+
return {
|
|
489
|
+
type: "opencode_message",
|
|
490
|
+
message: { id: msg.id, role: msg.role, content: msg.content, timestamp: msg.timestamp },
|
|
491
|
+
before: messages.slice(Math.max(0, idx - before), idx).map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
|
|
492
|
+
after: messages.slice(idx + 1, idx + 1 + after).map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
|
|
493
|
+
session: { id: parsed.session!, title: parsed.session!, directory: "" },
|
|
494
|
+
source: "claudecode",
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
case "cursor": {
|
|
502
|
+
if (parsed.machine !== getMachineId()) {
|
|
503
|
+
return { error: `Message is from machine '${parsed.machine}', not available on this machine (${getMachineId()})` };
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const { CursorReader } = await import("../integrations/cursor/reader.js");
|
|
507
|
+
const reader = new CursorReader();
|
|
508
|
+
const sessions = await reader.getSessions();
|
|
509
|
+
const session = sessions.find(s => s.id === parsed.session);
|
|
510
|
+
if (!session) return null;
|
|
511
|
+
const idx = session.messages.findIndex(m => m.id === parsed.nativeId);
|
|
512
|
+
if (idx === -1) return null;
|
|
513
|
+
const msg = session.messages[idx];
|
|
514
|
+
const mapCursor = (m: { id: string; type: 1 | 2; text: string; timestamp: string }) => ({
|
|
515
|
+
id: m.id,
|
|
516
|
+
role: (m.type === 1 ? "user" : "assistant") as "user" | "assistant",
|
|
517
|
+
content: m.text,
|
|
518
|
+
timestamp: m.timestamp,
|
|
519
|
+
});
|
|
520
|
+
return {
|
|
521
|
+
type: "opencode_message",
|
|
522
|
+
message: mapCursor(msg),
|
|
523
|
+
before: session.messages.slice(Math.max(0, idx - before), idx).map(mapCursor),
|
|
524
|
+
after: session.messages.slice(idx + 1, idx + 1 + after).map(mapCursor),
|
|
525
|
+
session: { id: session.id, title: session.name, directory: session.workspacePath },
|
|
526
|
+
source: "cursor",
|
|
527
|
+
};
|
|
528
|
+
} catch {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
case "unknown":
|
|
534
|
+
default: {
|
|
535
|
+
// Backward compat: bare msg_xxx → treat as opencode (no machine qualifier)
|
|
536
|
+
if (OPENCODE_MESSAGE_ID.test(id)) {
|
|
537
|
+
try {
|
|
538
|
+
const { createOpenCodeReader } = await import("../integrations/opencode/reader-factory.js");
|
|
539
|
+
const reader = await createOpenCodeReader();
|
|
540
|
+
const win = await reader.getMessageById(id, before, after);
|
|
541
|
+
if (!win) return null;
|
|
542
|
+
return {
|
|
543
|
+
type: "opencode_message",
|
|
544
|
+
message: { id: win.message.id, role: win.message.role, content: win.message.content, timestamp: win.message.timestamp, agent: win.message.agent },
|
|
545
|
+
before: win.before.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
546
|
+
after: win.after.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, agent: m.agent })),
|
|
547
|
+
session: { id: win.session.id, title: win.session.title, directory: win.session.directory },
|
|
548
|
+
source: "opencode",
|
|
549
|
+
};
|
|
550
|
+
} catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
397
559
|
export async function lookupById(id: string): Promise<({ type: string } & Record<string, unknown>) | null> {
|
|
398
560
|
const state = await loadLatestState();
|
|
399
561
|
if (!state) {
|
package/src/cli.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { parseArgs } from "util";
|
|
15
15
|
import { join } from "path";
|
|
16
|
-
import { retrieveBalanced, lookupById, loadLatestState } from "./cli/retrieval";
|
|
16
|
+
import { retrieveBalanced, lookupById, resolveExternalMessage, loadLatestState } from "./cli/retrieval";
|
|
17
17
|
import type { StorageState } from "./core/types";
|
|
18
18
|
import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona, filterBySource, filterTypeSpecificBySource } from "./cli/persona-filter.js";
|
|
19
19
|
import pkg from "../package.json" assert { type: "json" };
|
|
@@ -247,6 +247,12 @@ mentions a person, or corrects something you assumed.
|
|
|
247
247
|
// Strip surrounding quotes (from jq output or shell quoting)
|
|
248
248
|
id = id.replace(/^["']|["']$/g, "");
|
|
249
249
|
|
|
250
|
+
const ocMessage = await resolveExternalMessage(id);
|
|
251
|
+
if (ocMessage) {
|
|
252
|
+
console.log(JSON.stringify(ocMessage, null, 2));
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
250
256
|
const entity = await lookupById(id);
|
|
251
257
|
if (!entity) {
|
|
252
258
|
console.error(`No entity found with ID: ${id}`);
|
|
@@ -25,8 +25,7 @@ export async function handleDedupCurate(
|
|
|
25
25
|
|
|
26
26
|
// Validate entity_type
|
|
27
27
|
if (!entity_type || !['topic', 'person'].includes(entity_type)) {
|
|
28
|
-
|
|
29
|
-
return;
|
|
28
|
+
throw new Error(`[Dedup] Invalid entity_type: "${entity_type}" (from request data)`);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
// Parse Opus response
|
|
@@ -37,14 +36,12 @@ export async function handleDedupCurate(
|
|
|
37
36
|
throw new Error("Invalid response format");
|
|
38
37
|
}
|
|
39
38
|
} catch (err) {
|
|
40
|
-
|
|
41
|
-
return;
|
|
39
|
+
throw new Error(`[Dedup] Failed to parse Opus response: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
40
|
}
|
|
43
41
|
|
|
44
42
|
// Validate response structure
|
|
45
43
|
if (!Array.isArray(decisions.update) || !Array.isArray(decisions.remove) || !Array.isArray(decisions.add)) {
|
|
46
|
-
|
|
47
|
-
return;
|
|
44
|
+
throw new Error(`[Dedup] Invalid response structure - missing update/remove/add arrays`);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
console.log(`[Dedup] Processing cluster: ${decisions.update.length} updates, ${decisions.remove.length} removals, ${decisions.add.length} additions`);
|
|
@@ -63,15 +60,7 @@ export async function handleDedupCurate(
|
|
|
63
60
|
|
|
64
61
|
// Validate entityList exists
|
|
65
62
|
if (!entityList || !Array.isArray(entityList)) {
|
|
66
|
-
|
|
67
|
-
entity_type,
|
|
68
|
-
entity_ids,
|
|
69
|
-
stateKeys: Object.keys(state),
|
|
70
|
-
factsExists: !!state.facts,
|
|
71
|
-
topicsExists: !!state.topics,
|
|
72
|
-
peopleExists: !!state.people
|
|
73
|
-
});
|
|
74
|
-
return;
|
|
63
|
+
throw new Error(`[Dedup] entityList is ${entityList === undefined ? 'undefined' : 'not an array'} for entity_type="${entity_type}" (looking for state.${entity_type}s)`);
|
|
75
64
|
}
|
|
76
65
|
const entities = entity_ids
|
|
77
66
|
.map((id: string) => entityList.find((e: Fact | Topic | Person) => e.id === id))
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
queueAllScans,
|
|
6
6
|
type ExtractionContext,
|
|
7
7
|
} from "../orchestrators/human-extraction.js";
|
|
8
|
+
import { qualifyDocumentMessage } from "../utils/message-id.js";
|
|
8
9
|
|
|
9
10
|
function parseSegmentArray(content: string): string[] | null {
|
|
10
11
|
const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) ?? content.match(/```\s*([\s\S]*?)```/);
|
|
@@ -30,8 +31,7 @@ export function handleDocumentSegmentation(response: LLMResponse, state: StateMa
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
if (!batchId || !filename) {
|
|
33
|
-
|
|
34
|
-
return;
|
|
34
|
+
throw new Error("[handleDocumentSegmentation] Missing batchId or filename in request data");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
let segments: string[];
|
|
@@ -49,18 +49,16 @@ export function handleDocumentSegmentation(response: LLMResponse, state: StateMa
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const now = new Date().toISOString();
|
|
52
|
-
const sourceTag = `import:document:${filename}`;
|
|
53
52
|
|
|
54
53
|
for (const segment of segments) {
|
|
55
54
|
const message: Message = {
|
|
56
|
-
id: crypto.randomUUID(),
|
|
55
|
+
id: qualifyDocumentMessage(filename, crypto.randomUUID()),
|
|
57
56
|
role: "system",
|
|
58
57
|
content: segment,
|
|
59
58
|
timestamp: now,
|
|
60
59
|
read: true,
|
|
61
60
|
context_status: ContextStatus.Always,
|
|
62
61
|
external: true,
|
|
63
|
-
source_tag: sourceTag,
|
|
64
62
|
};
|
|
65
63
|
state.messages_append("emmet", message);
|
|
66
64
|
}
|
|
@@ -72,7 +70,7 @@ export function finishDocumentBatch(batchId: string, filename: string, state: St
|
|
|
72
70
|
const sourceTag = `import:document:${filename}`;
|
|
73
71
|
|
|
74
72
|
const emmettMessages = state.messages_get("emmet");
|
|
75
|
-
const docMessages = emmettMessages.filter(m => m.external === true && m.
|
|
73
|
+
const docMessages = emmettMessages.filter(m => m.external === true && m.id.startsWith(`${sourceTag}:`));
|
|
76
74
|
|
|
77
75
|
if (docMessages.length === 0) {
|
|
78
76
|
console.warn(`[finishDocumentBatch] No messages found for ${sourceTag} — skipping extraction`);
|
|
@@ -103,7 +101,7 @@ export function finishDocumentBatch(batchId: string, filename: string, state: St
|
|
|
103
101
|
...updatedHuman.settings?.document,
|
|
104
102
|
processed_documents: {
|
|
105
103
|
...(updatedHuman.settings?.document?.processed_documents ?? {}),
|
|
106
|
-
[filename]: new Date().toISOString(),
|
|
104
|
+
[filename]: { created_at: new Date().toISOString(), type: "imported" },
|
|
107
105
|
},
|
|
108
106
|
},
|
|
109
107
|
},
|
|
@@ -13,14 +13,12 @@ export function handleHeartbeatCheck(response: LLMResponse, state: StateManager)
|
|
|
13
13
|
const personaId = response.request.data.personaId as string;
|
|
14
14
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
15
15
|
if (!personaId) {
|
|
16
|
-
|
|
17
|
-
return;
|
|
16
|
+
throw new Error("[handleHeartbeatCheck] No personaId in request data");
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
const result = response.parsed as HeartbeatCheckResult | undefined;
|
|
21
20
|
if (!result) {
|
|
22
|
-
|
|
23
|
-
return;
|
|
21
|
+
throw new Error(`[HeartbeatCheck ${personaDisplayName}] No parsed result`);
|
|
24
22
|
}
|
|
25
23
|
console.log(`[HeartbeatCheck ${personaDisplayName}] Parsed result - should_respond: ${result.should_respond}, topic: ${result.topic ?? '(none)'}, message: ${result.message ? '(present)' : '(none)'}`);
|
|
26
24
|
|
|
@@ -52,8 +50,7 @@ export function handleHeartbeatCheck(response: LLMResponse, state: StateManager)
|
|
|
52
50
|
export function handleEiHeartbeat(response: LLMResponse, state: StateManager): void {
|
|
53
51
|
const result = response.parsed as EiHeartbeatResult | undefined;
|
|
54
52
|
if (!result) {
|
|
55
|
-
|
|
56
|
-
return;
|
|
53
|
+
throw new Error("[EiHeartbeat] No parsed result");
|
|
57
54
|
}
|
|
58
55
|
console.log(`[EiHeartbeat] Parsed result - should_respond: ${result.should_respond}, id: ${result.id ?? '(none)'}, my_response: ${result.my_response ? '(present)' : '(none)'}`);
|
|
59
56
|
const now = new Date().toISOString();
|
|
@@ -127,8 +124,7 @@ export function handleReflectionCritic(response: LLMResponse, state: StateManage
|
|
|
127
124
|
|
|
128
125
|
const result = response.parsed as ReflectionCriticResult | undefined;
|
|
129
126
|
if (!result?.critique) {
|
|
130
|
-
|
|
131
|
-
return;
|
|
127
|
+
throw new Error(`[ReflectionCritic ${personaDisplayName}] Invalid or missing parsed result`);
|
|
132
128
|
}
|
|
133
129
|
|
|
134
130
|
const personRecord = state.human_person_getByIdentifier("Ei Persona", personaId);
|
|
@@ -150,8 +146,7 @@ export function handleReflectionCritic(response: LLMResponse, state: StateManage
|
|
|
150
146
|
|
|
151
147
|
const persona = state.persona_getById(personaId);
|
|
152
148
|
if (!persona) {
|
|
153
|
-
|
|
154
|
-
return;
|
|
149
|
+
throw new Error(`[ReflectionCritic ${personaDisplayName}] Persona not found after critic`);
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
const mergedTopics = result.updated_identity.topics.map(updatedTopic => {
|
|
@@ -139,6 +139,8 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
139
139
|
? incomingSources
|
|
140
140
|
: [...new Set([...(existingTopic?.sources ?? []), ...incomingSources])];
|
|
141
141
|
|
|
142
|
+
const newDescLen = resolvedDescription?.length ?? 0;
|
|
143
|
+
const existingFloor = existingTopic?.rewrite_length_floor;
|
|
142
144
|
const topic: Topic = {
|
|
143
145
|
id: itemId,
|
|
144
146
|
name: resolvedName,
|
|
@@ -156,6 +158,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
156
158
|
sources: sources.length > 0 ? sources : undefined,
|
|
157
159
|
persona_groups: personaGroupsMerged,
|
|
158
160
|
embedding,
|
|
161
|
+
rewrite_length_floor: existingFloor !== undefined && newDescLen < existingFloor ? existingFloor : undefined,
|
|
159
162
|
};
|
|
160
163
|
state.human_topic_upsert(topic);
|
|
161
164
|
|
|
@@ -333,6 +336,11 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
333
336
|
sources: personSources.length > 0 ? personSources : undefined,
|
|
334
337
|
persona_groups: personaGroupsMerged,
|
|
335
338
|
embedding,
|
|
339
|
+
rewrite_length_floor: (() => {
|
|
340
|
+
const floor = existingPerson?.rewrite_length_floor;
|
|
341
|
+
const newLen = resolvedDescription?.length ?? 0;
|
|
342
|
+
return floor !== undefined && newLen < floor ? floor : undefined;
|
|
343
|
+
})(),
|
|
336
344
|
};
|
|
337
345
|
state.human_person_upsert(person);
|
|
338
346
|
|
|
@@ -16,6 +16,7 @@ import { handleDedupCurate } from "./dedup.js";
|
|
|
16
16
|
import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
|
|
17
17
|
import { handlePersonaPreview } from "./persona-preview.js";
|
|
18
18
|
import { handleDocumentSegmentation } from "./document-segmentation.js";
|
|
19
|
+
import { handleKnowledgeSynthesis } from "./knowledge-synthesis.js";
|
|
19
20
|
|
|
20
21
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
21
22
|
handlePersonaResponse,
|
|
@@ -43,4 +44,5 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
43
44
|
[LLMNextStep.HandleTopicValidate]: handleDedupCurate,
|
|
44
45
|
[LLMNextStep.HandleReflectionCritic]: handleReflectionCritic,
|
|
45
46
|
[LLMNextStep.HandleDocumentSegmentation]: handleDocumentSegmentation,
|
|
47
|
+
[LLMNextStep.HandleKnowledgeSynthesis]: handleKnowledgeSynthesis,
|
|
46
48
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ContextStatus } from "../types.js";
|
|
2
|
+
import type { LLMResponse, Message } from "../types.js";
|
|
3
|
+
import type { StateManager } from "../state-manager.js";
|
|
4
|
+
|
|
5
|
+
export function handleKnowledgeSynthesis(response: LLMResponse, state: StateManager): void {
|
|
6
|
+
const { slug, subject } = response.request.data as {
|
|
7
|
+
slug: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (!slug || !subject) {
|
|
12
|
+
throw new Error("[handleKnowledgeSynthesis] Missing slug or subject in request data");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const content = response.content?.trim() ?? "";
|
|
16
|
+
if (!content) {
|
|
17
|
+
throw new Error(`[handleKnowledgeSynthesis] Empty or null response content for slug "${slug}"`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
|
|
22
|
+
const message: Message = {
|
|
23
|
+
id: `generate:document:${slug}:${crypto.randomUUID()}`,
|
|
24
|
+
role: "system",
|
|
25
|
+
content,
|
|
26
|
+
timestamp: now,
|
|
27
|
+
read: true,
|
|
28
|
+
context_status: ContextStatus.Always,
|
|
29
|
+
external: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
state.messages_append("emmet", message);
|
|
33
|
+
|
|
34
|
+
const updatedHuman = state.getHuman();
|
|
35
|
+
state.setHuman({
|
|
36
|
+
...updatedHuman,
|
|
37
|
+
settings: {
|
|
38
|
+
...updatedHuman.settings,
|
|
39
|
+
document: {
|
|
40
|
+
...updatedHuman.settings?.document,
|
|
41
|
+
processed_documents: {
|
|
42
|
+
...(updatedHuman.settings?.document?.processed_documents ?? {}),
|
|
43
|
+
[slug]: { created_at: now, type: "generated", subject },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -12,8 +12,7 @@ export function handlePersonaGeneration(response: LLMResponse, state: StateManag
|
|
|
12
12
|
const personaId = response.request.data.personaId as string;
|
|
13
13
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
14
14
|
if (!personaId) {
|
|
15
|
-
|
|
16
|
-
return;
|
|
15
|
+
throw new Error("[handlePersonaGeneration] No personaId in request data");
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
const result = response.parsed as PersonaGenerationResult | undefined;
|
|
@@ -115,14 +114,12 @@ export function handlePersonaTraitExtraction(response: LLMResponse, state: State
|
|
|
115
114
|
const personaId = response.request.data.personaId as string;
|
|
116
115
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
117
116
|
if (!personaId) {
|
|
118
|
-
|
|
119
|
-
return;
|
|
117
|
+
throw new Error("[handlePersonaTraitExtraction] No personaId in request data");
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
const result = response.parsed as TraitResult[] | undefined;
|
|
123
121
|
if (!result || !Array.isArray(result)) {
|
|
124
|
-
|
|
125
|
-
return;
|
|
122
|
+
throw new Error("[handlePersonaTraitExtraction] Invalid parsed result");
|
|
126
123
|
}
|
|
127
124
|
|
|
128
125
|
if (result.length === 0) {
|
|
@@ -131,8 +128,7 @@ export function handlePersonaTraitExtraction(response: LLMResponse, state: State
|
|
|
131
128
|
|
|
132
129
|
const persona = state.persona_getById(personaId);
|
|
133
130
|
if (!persona) {
|
|
134
|
-
|
|
135
|
-
return;
|
|
131
|
+
throw new Error(`[handlePersonaTraitExtraction] Persona ${personaId} not found`);
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
const now = new Date().toISOString();
|
|
@@ -15,8 +15,7 @@ export function handlePersonaResponse(response: LLMResponse, state: StateManager
|
|
|
15
15
|
const personaId = response.request.data.personaId as string;
|
|
16
16
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
17
17
|
if (!personaId) {
|
|
18
|
-
|
|
19
|
-
return;
|
|
18
|
+
throw new Error("[handlePersonaResponse] No personaId in request data");
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
state.messages_markPendingAsRead(personaId);
|
|
@@ -108,7 +107,7 @@ export function handleToolContinuation(response: LLMResponse, state: StateManage
|
|
|
108
107
|
const originalStep = response.request.data.originalNextStep as LLMNextStep | undefined;
|
|
109
108
|
|
|
110
109
|
if (!originalStep) {
|
|
111
|
-
console.
|
|
110
|
+
console.warn(`[handleToolContinuation] No originalNextStep in data, falling back to handlePersonaResponse`);
|
|
112
111
|
handlePersonaResponse(response, state);
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
@@ -118,7 +117,7 @@ export function handleToolContinuation(response: LLMResponse, state: StateManage
|
|
|
118
117
|
const handler = handlers[originalStep];
|
|
119
118
|
|
|
120
119
|
if (!handler) {
|
|
121
|
-
console.
|
|
120
|
+
console.warn(`[handleToolContinuation] No handler found for ${originalStep}, falling back to handlePersonaResponse`);
|
|
122
121
|
handlePersonaResponse(response, state);
|
|
123
122
|
return;
|
|
124
123
|
}
|