ei-tui 0.2.0 → 0.3.1
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 +2 -1
- package/src/cli/README.md +83 -2
- package/src/cli/commands/facts.ts +2 -2
- package/src/cli/commands/people.ts +2 -2
- package/src/cli/commands/quotes.ts +2 -2
- package/src/cli/commands/topics.ts +2 -2
- package/src/cli/mcp.ts +94 -0
- package/src/cli/retrieval.ts +64 -6
- package/src/cli.ts +61 -16
- package/src/core/handlers/dedup.ts +2 -8
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +2 -0
- package/src/core/handlers/rewrite.ts +3 -25
- package/src/core/human-data-manager.ts +35 -11
- package/src/core/orchestrators/ceremony.ts +0 -6
- package/src/core/orchestrators/dedup-phase.ts +2 -3
- package/src/core/processor.ts +69 -4
- package/src/core/tools/builtin/read-memory.ts +7 -11
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +1 -0
- package/src/integrations/claude-code/importer.ts +6 -1
- package/src/integrations/claude-code/types.ts +1 -0
- package/src/integrations/cursor/importer.ts +282 -0
- package/src/integrations/cursor/index.ts +10 -0
- package/src/integrations/cursor/reader.ts +209 -0
- package/src/integrations/cursor/types.ts +140 -0
- package/src/integrations/opencode/importer.ts +7 -1
- package/src/prompts/ceremony/dedup.ts +0 -33
- package/src/prompts/ceremony/rewrite.ts +4 -20
- package/src/prompts/ceremony/types.ts +1 -1
- package/tui/src/util/yaml-serializers.ts +28 -0
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
LLMPriority,
|
|
4
4
|
LLMNextStep,
|
|
5
5
|
type LLMResponse,
|
|
6
|
-
type Fact,
|
|
7
6
|
type Topic,
|
|
8
7
|
type Person,
|
|
9
8
|
type DataItemBase,
|
|
@@ -46,7 +45,7 @@ export async function handleRewriteScan(response: LLMResponse, state: StateManag
|
|
|
46
45
|
// Re-read the item from current state (it may have changed since scan was queued)
|
|
47
46
|
const human = state.getHuman();
|
|
48
47
|
const allItems: DataItemBase[] = [
|
|
49
|
-
...human.
|
|
48
|
+
...human.topics, ...human.people,
|
|
50
49
|
];
|
|
51
50
|
const currentItem = allItems.find(i => i.id === itemId);
|
|
52
51
|
if (!currentItem) {
|
|
@@ -59,7 +58,7 @@ export async function handleRewriteScan(response: LLMResponse, state: StateManag
|
|
|
59
58
|
for (const searchTerm of subjects) {
|
|
60
59
|
try {
|
|
61
60
|
const results = await searchHumanData(state, searchTerm, {
|
|
62
|
-
types: ["
|
|
61
|
+
types: ["topic", "person"],
|
|
63
62
|
limit: 4, // fetch 4 so we can exclude original and still have 3
|
|
64
63
|
});
|
|
65
64
|
const allMatches: DataItemBase[] = [
|
|
@@ -120,14 +119,13 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
120
119
|
|
|
121
120
|
// Look up the original item to inherit persona_groups
|
|
122
121
|
const allItems: DataItemBase[] = [
|
|
123
|
-
...human.
|
|
122
|
+
...human.topics, ...human.people,
|
|
124
123
|
];
|
|
125
124
|
const originalItem = allItems.find(i => i.id === itemId);
|
|
126
125
|
const inheritedGroups = originalItem?.persona_groups;
|
|
127
126
|
|
|
128
127
|
// Helper: resolve actual type from existing records (don't trust LLM's type field)
|
|
129
128
|
const resolveExistingType = (id: string): RewriteItemType | null => {
|
|
130
|
-
if (human.facts.find(f => f.id === id)) return "fact";
|
|
131
129
|
if (human.topics.find(t => t.id === id)) return "topic";
|
|
132
130
|
if (human.people.find(p => p.id === id)) return "person";
|
|
133
131
|
return null;
|
|
@@ -160,18 +158,6 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
160
158
|
}
|
|
161
159
|
|
|
162
160
|
switch (resolvedType) {
|
|
163
|
-
case "fact": {
|
|
164
|
-
const existing = human.facts.find(f => f.id === item.id)!;
|
|
165
|
-
state.human_fact_upsert({
|
|
166
|
-
...existing,
|
|
167
|
-
name: item.name,
|
|
168
|
-
description: item.description,
|
|
169
|
-
sentiment: item.sentiment ?? existing.sentiment,
|
|
170
|
-
last_updated: now,
|
|
171
|
-
embedding,
|
|
172
|
-
});
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
161
|
case "topic": {
|
|
176
162
|
const existing = human.topics.find(t => t.id === item.id)!;
|
|
177
163
|
state.human_topic_upsert({
|
|
@@ -228,14 +214,6 @@ export async function handleRewriteRewrite(response: LLMResponse, state: StateMa
|
|
|
228
214
|
};
|
|
229
215
|
|
|
230
216
|
switch (item.type) {
|
|
231
|
-
case "fact": {
|
|
232
|
-
const fact: Fact = {
|
|
233
|
-
...baseFields,
|
|
234
|
-
validated_date: now,
|
|
235
|
-
};
|
|
236
|
-
state.human_fact_upsert(fact);
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
217
|
case "topic": {
|
|
240
218
|
if (!item.category) {
|
|
241
219
|
console.warn(`[handleRewriteRewrite] New topic "${item.name}" missing category — defaulting to "Interest"`);
|
|
@@ -145,14 +145,14 @@ export async function getQuotesForMessage(sm: StateManager, messageId: string):
|
|
|
145
145
|
export async function searchHumanData(
|
|
146
146
|
sm: StateManager,
|
|
147
147
|
query: string,
|
|
148
|
-
options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number } = {}
|
|
148
|
+
options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean } = {}
|
|
149
149
|
): Promise<{
|
|
150
150
|
facts: Fact[];
|
|
151
151
|
topics: Topic[];
|
|
152
152
|
people: Person[];
|
|
153
153
|
quotes: Quote[];
|
|
154
154
|
}> {
|
|
155
|
-
const { types = ["fact", "topic", "person", "quote"], limit = 10 } = options;
|
|
155
|
+
const { types = ["fact", "topic", "person", "quote"], limit = 10, recent } = options;
|
|
156
156
|
const human = sm.getHuman();
|
|
157
157
|
const SIMILARITY_THRESHOLD = 0.3;
|
|
158
158
|
|
|
@@ -163,30 +163,54 @@ export async function searchHumanData(
|
|
|
163
163
|
quotes: [] as Quote[],
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
+
const recentSort = <T extends { last_updated?: string; last_mentioned?: string }>(items: T[]): T[] =>
|
|
167
|
+
[...items].sort((a, b) => {
|
|
168
|
+
const aDate = a.last_mentioned ?? a.last_updated ?? "";
|
|
169
|
+
const bDate = b.last_mentioned ?? b.last_updated ?? "";
|
|
170
|
+
return bDate.localeCompare(aDate);
|
|
171
|
+
});
|
|
172
|
+
|
|
166
173
|
let queryVector: number[] | null = null;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
if (query) {
|
|
175
|
+
try {
|
|
176
|
+
const embeddingService = getEmbeddingService();
|
|
177
|
+
queryVector = await embeddingService.embed(query);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn("[searchHumanData] Failed to generate query embedding:", err);
|
|
180
|
+
}
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
const searchItems = <T extends { id: string; embedding?: number[] }>(
|
|
183
|
+
const searchItems = <T extends { id: string; embedding?: number[]; last_updated?: string; last_mentioned?: string }>(
|
|
175
184
|
items: T[],
|
|
176
185
|
textExtractor: (item: T) => string
|
|
177
186
|
): T[] => {
|
|
187
|
+
if (recent && !query) {
|
|
188
|
+
return recentSort(items).slice(0, limit);
|
|
189
|
+
}
|
|
190
|
+
|
|
178
191
|
const withEmbeddings = items.filter((i) => i.embedding?.length);
|
|
179
192
|
|
|
180
193
|
if (queryVector && withEmbeddings.length > 0) {
|
|
181
|
-
|
|
194
|
+
const topK = recent ? Math.max(limit * 5, 50) : limit;
|
|
195
|
+
const found = findTopK(queryVector, withEmbeddings, topK)
|
|
182
196
|
.filter(({ similarity }) => similarity >= SIMILARITY_THRESHOLD)
|
|
183
197
|
.map(({ item }) => item);
|
|
198
|
+
if (recent) {
|
|
199
|
+
return recentSort(found).slice(0, limit);
|
|
200
|
+
}
|
|
201
|
+
return found;
|
|
184
202
|
}
|
|
185
203
|
|
|
204
|
+
if (!query) return [];
|
|
205
|
+
|
|
186
206
|
const lowerQuery = query.toLowerCase();
|
|
187
|
-
|
|
207
|
+
const found = items
|
|
188
208
|
.filter((i) => textExtractor(i).toLowerCase().includes(lowerQuery))
|
|
189
|
-
.slice(0, limit);
|
|
209
|
+
.slice(0, recent ? Math.max(limit * 5, 50) : limit);
|
|
210
|
+
if (recent) {
|
|
211
|
+
return recentSort(found).slice(0, limit);
|
|
212
|
+
}
|
|
213
|
+
return found;
|
|
190
214
|
};
|
|
191
215
|
|
|
192
216
|
if (types.includes("fact")) {
|
|
@@ -616,12 +616,6 @@ export function queueRewritePhase(state: StateManager): void {
|
|
|
616
616
|
|
|
617
617
|
const itemsToScan: Array<{ item: DataItemBase; type: RewriteItemType }> = [];
|
|
618
618
|
|
|
619
|
-
for (const fact of human.facts) {
|
|
620
|
-
if ((fact.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
621
|
-
itemsToScan.push({ item: fact, type: "fact" });
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
619
|
for (const topic of human.topics) {
|
|
626
620
|
if ((topic.description?.length ?? 0) > REWRITE_DESCRIPTION_THRESHOLD) {
|
|
627
621
|
itemsToScan.push({ item: topic, type: "topic" });
|
|
@@ -20,7 +20,7 @@ interface Cluster {
|
|
|
20
20
|
// DEDUP CANDIDATE FINDING (copied from ceremony.ts)
|
|
21
21
|
// =============================================================================
|
|
22
22
|
|
|
23
|
-
const DEDUP_DEFAULT_THRESHOLD = 0.
|
|
23
|
+
const DEDUP_DEFAULT_THRESHOLD = 0.95; // Raised from 0.90: Ei's topic corpus is a single dense project domain — mega-cluster persists all the way to 0.92. At 0.95, max cluster drops to 7 items.
|
|
24
24
|
|
|
25
25
|
function findDedupCandidates<T extends DedupableItem>(
|
|
26
26
|
items: T[],
|
|
@@ -139,7 +139,6 @@ export function queueDedupPhase(state: StateManager): void {
|
|
|
139
139
|
console.log(`[Dedup] Starting deduplication phase (threshold: ${threshold})`);
|
|
140
140
|
|
|
141
141
|
const entityTypes: Array<{ type: DataItemType; items: DedupableItem[] }> = [
|
|
142
|
-
{ type: "fact", items: human.facts },
|
|
143
142
|
{ type: "topic", items: human.topics },
|
|
144
143
|
{ type: "person", items: human.people },
|
|
145
144
|
];
|
|
@@ -178,7 +177,7 @@ export function queueDedupPhase(state: StateManager): void {
|
|
|
178
177
|
// Build prompt
|
|
179
178
|
const prompt = buildDedupPrompt({
|
|
180
179
|
cluster: clusterEntities,
|
|
181
|
-
itemType: type,
|
|
180
|
+
itemType: type as "topic" | "person",
|
|
182
181
|
similarityRange: { min: cluster.minSim, max: cluster.maxSim }
|
|
183
182
|
});
|
|
184
183
|
|
package/src/core/processor.ts
CHANGED
|
@@ -108,6 +108,7 @@ const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
|
108
108
|
const DEFAULT_CONTEXT_WINDOW_HOURS = 8;
|
|
109
109
|
const DEFAULT_OPENCODE_POLLING_MS = 1800000;
|
|
110
110
|
const DEFAULT_CLAUDE_CODE_POLLING_MS = 1800000;
|
|
111
|
+
const DEFAULT_CURSOR_POLLING_MS = 1800000;
|
|
111
112
|
|
|
112
113
|
let processorInstanceCount = 0;
|
|
113
114
|
|
|
@@ -128,6 +129,8 @@ export class Processor {
|
|
|
128
129
|
private openCodeImportInProgress = false;
|
|
129
130
|
private lastClaudeCodeSync = 0;
|
|
130
131
|
private claudeCodeImportInProgress = false;
|
|
132
|
+
private lastCursorSync = 0;
|
|
133
|
+
private cursorImportInProgress = false;
|
|
131
134
|
private pendingConflict: StateConflictData | null = null;
|
|
132
135
|
private storage: Storage | null = null;
|
|
133
136
|
private importAbortController = new AbortController();
|
|
@@ -278,19 +281,20 @@ export class Processor {
|
|
|
278
281
|
name: "read_memory",
|
|
279
282
|
display_name: "Read Memory",
|
|
280
283
|
description:
|
|
281
|
-
"Search your personal memory for relevant facts,
|
|
284
|
+
"Search your personal memory for relevant facts, topics, people, or quotes. Use this when you need information about the user that may not be in the current conversation. Use `recent: true` to retrieve what's been discussed recently.",
|
|
282
285
|
input_schema: {
|
|
283
286
|
type: "object",
|
|
284
287
|
properties: {
|
|
285
288
|
query: { type: "string", description: "What to search for in memory" },
|
|
286
289
|
types: {
|
|
287
290
|
type: "array",
|
|
288
|
-
items: { type: "string", enum: ["fact", "
|
|
291
|
+
items: { type: "string", enum: ["fact", "topic", "person", "quote"] },
|
|
289
292
|
description: "Limit search to specific memory types (default: all types)",
|
|
290
293
|
},
|
|
291
294
|
limit: { type: "number", description: "Max results to return (default: 10, max: 20)" },
|
|
295
|
+
recent: { type: "boolean", description: "If true, return recently-mentioned results sorted by last_mentioned date instead of relevance. Combine with a query to filter recent results by topic." },
|
|
292
296
|
},
|
|
293
|
-
required: [
|
|
297
|
+
required: [],
|
|
294
298
|
},
|
|
295
299
|
runtime: "any",
|
|
296
300
|
builtin: true,
|
|
@@ -876,6 +880,14 @@ const toolNextSteps = new Set([
|
|
|
876
880
|
await this.checkAndSyncClaudeCode(human, now);
|
|
877
881
|
}
|
|
878
882
|
|
|
883
|
+
if (
|
|
884
|
+
this.isTUI &&
|
|
885
|
+
human.settings?.cursor?.integration &&
|
|
886
|
+
this.stateManager.queue_length() === 0
|
|
887
|
+
) {
|
|
888
|
+
await this.checkAndSyncCursor(human, now);
|
|
889
|
+
}
|
|
890
|
+
|
|
879
891
|
if (human.settings?.ceremony && shouldStartCeremony(human.settings.ceremony, this.stateManager)) {
|
|
880
892
|
if (human.settings?.sync && remoteSync.isConfigured()) {
|
|
881
893
|
const state = this.stateManager.getStorageState();
|
|
@@ -1062,6 +1074,59 @@ const toolNextSteps = new Set([
|
|
|
1062
1074
|
});
|
|
1063
1075
|
}
|
|
1064
1076
|
|
|
1077
|
+
private async checkAndSyncCursor(human: HumanEntity, now: number): Promise<void> {
|
|
1078
|
+
if (this.cursorImportInProgress) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const cursor = human.settings?.cursor;
|
|
1083
|
+
const pollingInterval = cursor?.polling_interval_ms ?? DEFAULT_CURSOR_POLLING_MS;
|
|
1084
|
+
const lastSync = cursor?.last_sync ? new Date(cursor.last_sync).getTime() : 0;
|
|
1085
|
+
const timeSinceSync = now - lastSync;
|
|
1086
|
+
|
|
1087
|
+
if (timeSinceSync < pollingInterval && this.lastCursorSync > 0) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
this.lastCursorSync = now;
|
|
1092
|
+
const syncTimestamp = new Date().toISOString();
|
|
1093
|
+
this.stateManager.setHuman({
|
|
1094
|
+
...this.stateManager.getHuman(),
|
|
1095
|
+
settings: {
|
|
1096
|
+
...this.stateManager.getHuman().settings,
|
|
1097
|
+
cursor: {
|
|
1098
|
+
...cursor,
|
|
1099
|
+
last_sync: syncTimestamp,
|
|
1100
|
+
},
|
|
1101
|
+
},
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
this.cursorImportInProgress = true;
|
|
1105
|
+
import("../integrations/cursor/importer.js")
|
|
1106
|
+
.then(({ importCursorSessions }) =>
|
|
1107
|
+
importCursorSessions({
|
|
1108
|
+
stateManager: this.stateManager,
|
|
1109
|
+
interface: this.interface,
|
|
1110
|
+
signal: this.importAbortController.signal,
|
|
1111
|
+
})
|
|
1112
|
+
)
|
|
1113
|
+
.then((result) => {
|
|
1114
|
+
if (result.sessionsProcessed > 0) {
|
|
1115
|
+
console.log(
|
|
1116
|
+
`[Processor] Cursor sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1117
|
+
`${result.topicsCreated} topics created, ${result.messagesImported} messages imported, ` +
|
|
1118
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
})
|
|
1122
|
+
.catch((err) => {
|
|
1123
|
+
console.warn(`[Processor] Cursor sync failed:`, err);
|
|
1124
|
+
})
|
|
1125
|
+
.finally(() => {
|
|
1126
|
+
this.cursorImportInProgress = false;
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1065
1130
|
private classifyLLMError(error: string): string {
|
|
1066
1131
|
const match = error.match(/\((\d{3})\)/);
|
|
1067
1132
|
if (match) {
|
|
@@ -1396,7 +1461,7 @@ const toolNextSteps = new Set([
|
|
|
1396
1461
|
|
|
1397
1462
|
async searchHumanData(
|
|
1398
1463
|
query: string,
|
|
1399
|
-
options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number } = {}
|
|
1464
|
+
options: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean } = {}
|
|
1400
1465
|
): Promise<{
|
|
1401
1466
|
facts: Fact[];
|
|
1402
1467
|
topics: Topic[];
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* read_memory builtin tool
|
|
3
|
-
*
|
|
4
|
-
* Delegates to Processor.searchHumanData() — no external call, runtime: "any".
|
|
5
|
-
* The searchHumanData function is injected at construction to avoid circular deps.
|
|
6
|
-
*/
|
|
7
1
|
import type { ToolExecutor } from "../types.js";
|
|
8
2
|
import type { Fact, Topic, Person, Quote } from "../../types.js";
|
|
9
3
|
|
|
10
4
|
type SearchHumanData = (
|
|
11
5
|
query: string,
|
|
12
|
-
options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
|
|
6
|
+
options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean }
|
|
13
7
|
) => Promise<{ facts: Fact[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
|
|
14
8
|
|
|
15
9
|
export function createReadMemoryExecutor(searchHumanData: SearchHumanData): ToolExecutor {
|
|
@@ -18,10 +12,12 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData): Tool
|
|
|
18
12
|
|
|
19
13
|
async execute(args: Record<string, unknown>): Promise<string> {
|
|
20
14
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const recent = args.recent === true;
|
|
16
|
+
console.log(`[read_memory] called with query="${query}", types=${JSON.stringify(args.types ?? null)}, limit=${args.limit ?? 10}, recent=${recent}`);
|
|
17
|
+
|
|
18
|
+
if (!query && !recent) {
|
|
23
19
|
console.warn("[read_memory] missing query argument");
|
|
24
|
-
return JSON.stringify({ error: "Missing required argument: query" });
|
|
20
|
+
return JSON.stringify({ error: "Missing required argument: query (or use recent: true)" });
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
const types = Array.isArray(args.types)
|
|
@@ -32,7 +28,7 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData): Tool
|
|
|
32
28
|
|
|
33
29
|
const limit = typeof args.limit === "number" && args.limit > 0 ? Math.min(args.limit, 20) : 10;
|
|
34
30
|
|
|
35
|
-
const results = await searchHumanData(query, { types, limit });
|
|
31
|
+
const results = await searchHumanData(query, { types, limit, recent });
|
|
36
32
|
|
|
37
33
|
const total = results.facts.length + results.topics.length + results.people.length + results.quotes.length;
|
|
38
34
|
console.log(`[read_memory] query="${query}" => ${total} results (facts=${results.facts.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
|
|
@@ -10,6 +10,7 @@ export interface DataItemBase {
|
|
|
10
10
|
description: string;
|
|
11
11
|
sentiment: number;
|
|
12
12
|
last_updated: string;
|
|
13
|
+
last_mentioned?: string; // Set by extraction only, never ceremony. Used for --recent sorting.
|
|
13
14
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
14
15
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
15
16
|
persona_groups?: string[];
|
|
@@ -86,6 +86,7 @@ export interface HumanSettings {
|
|
|
86
86
|
ceremony?: CeremonyConfig;
|
|
87
87
|
backup?: BackupConfig;
|
|
88
88
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
89
|
+
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export interface HumanEntity {
|
|
@@ -158,6 +158,10 @@ function updateProcessedState(
|
|
|
158
158
|
...human.settings,
|
|
159
159
|
claudeCode: {
|
|
160
160
|
...human.settings?.claudeCode,
|
|
161
|
+
// extraction_point is a progress indicator for user visibility only —
|
|
162
|
+
// it does NOT gate imports. processed_sessions is the sole source of
|
|
163
|
+
// truth for which sessions have been seen and when.
|
|
164
|
+
extraction_point: session.lastMessageAt,
|
|
161
165
|
processed_sessions: processedSessions,
|
|
162
166
|
},
|
|
163
167
|
},
|
|
@@ -212,7 +216,8 @@ export async function importClaudeCodeSessions(
|
|
|
212
216
|
|
|
213
217
|
// ─── Step 2: Find next unprocessed session ────────────────────────────
|
|
214
218
|
const human = stateManager.getHuman();
|
|
215
|
-
const
|
|
219
|
+
const settings = human.settings?.claudeCode;
|
|
220
|
+
const processedSessions = settings?.processed_sessions ?? {};
|
|
216
221
|
const now = Date.now();
|
|
217
222
|
|
|
218
223
|
let targetSession: ClaudeCodeSession | null = null;
|
|
@@ -161,5 +161,6 @@ export interface ClaudeCodeSettings {
|
|
|
161
161
|
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
162
162
|
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
163
163
|
last_sync?: string; // ISO timestamp
|
|
164
|
+
extraction_point?: string; // ISO timestamp - floor cursor for processed-session skip
|
|
164
165
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
165
166
|
}
|