ei-tui 0.1.3 → 0.1.5
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 +36 -35
- package/package.json +6 -2
- package/src/README.md +85 -1
- package/src/cli/README.md +30 -20
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +195 -172
- package/src/core/orchestrators/ceremony.ts +4 -4
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/processor.ts +245 -77
- package/src/core/queue-processor.ts +3 -26
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/personas.ts +13 -1
- package/src/core/state/queue.ts +80 -23
- package/src/core/state-manager.ts +36 -10
- package/src/core/types.ts +23 -11
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +118 -691
- package/src/prompts/heartbeat/check.ts +27 -13
- package/src/prompts/heartbeat/ei.ts +65 -136
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/human/item-update.ts +20 -8
- package/src/prompts/index.ts +2 -5
- package/src/prompts/message-utils.ts +42 -3
- package/src/prompts/response/index.ts +13 -6
- package/src/prompts/response/sections.ts +65 -12
- package/src/prompts/response/types.ts +10 -0
- package/tui/README.md +89 -4
- package/tui/src/commands/dlq.ts +75 -0
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/commands/queue.ts +77 -0
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/MessageList.tsx +12 -2
- package/tui/src/components/PromptInput.tsx +118 -30
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/components/StatusBar.tsx +12 -5
- package/tui/src/context/ei.tsx +43 -3
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +81 -11
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
2
|
import type { Ei_Interface, Topic, Message, ContextStatus } from "../../core/types.js";
|
|
3
|
-
import { MESSAGE_MIN_COUNT, MESSAGE_MAX_AGE_DAYS } from "../../core/types.js";
|
|
4
3
|
import type { IOpenCodeReader, OpenCodeSession, OpenCodeMessage } from "./types.js";
|
|
5
4
|
import { UTILITY_AGENTS, AGENT_TO_AGENT_PREFIXES } from "./types.js";
|
|
6
5
|
import { createOpenCodeReader } from "./reader-factory.js";
|
|
7
6
|
import { ensureAgentPersona } from "../../core/personas/opencode-agent.js";
|
|
8
7
|
import {
|
|
9
|
-
queueDirectTopicUpdate,
|
|
10
8
|
queueAllScans,
|
|
11
9
|
type ExtractionContext,
|
|
12
10
|
} from "../../core/orchestrators/human-extraction.js";
|
|
13
|
-
import { resolveTokenLimit } from "../../core/llm-client.js";
|
|
14
11
|
|
|
15
12
|
// =============================================================================
|
|
16
13
|
// Constants
|
|
@@ -18,48 +15,17 @@ import { resolveTokenLimit } from "../../core/llm-client.js";
|
|
|
18
15
|
|
|
19
16
|
const OPENCODE_TOPIC_GROUPS = ["General", "Coding", "OpenCode"];
|
|
20
17
|
|
|
21
|
-
/** Max extraction calls per archive scan cycle (bounds queue flooding). */
|
|
22
|
-
const ARCHIVE_SCAN_MAX_CALLS = 50;
|
|
23
|
-
const CHARS_PER_TOKEN = 4;
|
|
24
|
-
const MIN_EXTRACTION_TOKENS = 10000;
|
|
25
|
-
const EXTRACTION_BUDGET_RATIO = 0.75;
|
|
26
|
-
|
|
27
|
-
// =============================================================================
|
|
28
|
-
// Transient Types (used only during import analysis, never persisted)
|
|
29
|
-
// =============================================================================
|
|
30
|
-
|
|
31
|
-
interface MiniMessage {
|
|
32
|
-
id: string;
|
|
33
|
-
timestamp: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface ExternalMessage extends MiniMessage {
|
|
37
|
-
isExternal: true;
|
|
38
|
-
sessionId: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
18
|
// =============================================================================
|
|
42
19
|
// Export Types
|
|
43
20
|
// =============================================================================
|
|
44
21
|
|
|
45
|
-
interface SessionAgentMessages {
|
|
46
|
-
sessionId: string;
|
|
47
|
-
agentName: string;
|
|
48
|
-
personaId?: string;
|
|
49
|
-
messages: OpenCodeMessage[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
22
|
export interface ImportResult {
|
|
53
23
|
sessionsProcessed: number;
|
|
54
24
|
topicsCreated: number;
|
|
55
25
|
topicsUpdated: number;
|
|
56
26
|
messagesImported: number;
|
|
57
|
-
messagesPruned: number;
|
|
58
27
|
personasCreated: string[];
|
|
59
|
-
topicUpdatesQueued: number;
|
|
60
28
|
extractionScansQueued: number;
|
|
61
|
-
partialSessionsFound: number;
|
|
62
|
-
archiveScansQueued: number;
|
|
63
29
|
}
|
|
64
30
|
|
|
65
31
|
export interface OpenCodeImporterOptions {
|
|
@@ -81,7 +47,7 @@ function convertToEiMessage(ocMsg: OpenCodeMessage): Message {
|
|
|
81
47
|
return {
|
|
82
48
|
id: ocMsg.id,
|
|
83
49
|
role: ocMsg.role === "user" ? "human" : "system",
|
|
84
|
-
|
|
50
|
+
verbal_response: ocMsg.content,
|
|
85
51
|
timestamp: ocMsg.timestamp,
|
|
86
52
|
read: true,
|
|
87
53
|
context_status: "default" as ContextStatus,
|
|
@@ -107,19 +73,25 @@ function filterRelevantMessages(messages: OpenCodeMessage[]): OpenCodeMessage[]
|
|
|
107
73
|
});
|
|
108
74
|
}
|
|
109
75
|
|
|
110
|
-
function estimateTokensForMessages(messages: OpenCodeMessage[]): number {
|
|
111
|
-
return messages.reduce(
|
|
112
|
-
(sum, msg) => sum + Math.ceil(msg.content.length / CHARS_PER_TOKEN) + 4,
|
|
113
|
-
0
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
76
|
// =============================================================================
|
|
118
77
|
// Main Import Function
|
|
119
78
|
// =============================================================================
|
|
120
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Import one OpenCode session per call.
|
|
82
|
+
*
|
|
83
|
+
* Flow:
|
|
84
|
+
* 1. Ensure topics exist for all primary sessions (always, cheaply).
|
|
85
|
+
* 2. Find the next unprocessed session after extraction_point.
|
|
86
|
+
* 3. Write all messages for that session to their persona(s) — archived,
|
|
87
|
+
* messages cleared first. Messages before last_imported are pre-marked
|
|
88
|
+
* [p,r,o,f]=true; newer messages are unmarked and queued for extraction.
|
|
89
|
+
* 4. Advance extraction_point to session.time.updated.
|
|
90
|
+
*
|
|
91
|
+
* The processor gate (queue_length() === 0) ensures we never pile onto a
|
|
92
|
+
* backed-up queue.
|
|
93
|
+
*/
|
|
121
94
|
export async function importOpenCodeSessions(
|
|
122
|
-
since: Date,
|
|
123
95
|
options: OpenCodeImporterOptions
|
|
124
96
|
): Promise<ImportResult> {
|
|
125
97
|
const { stateManager, interface: eiInterface } = options;
|
|
@@ -130,17 +102,13 @@ export async function importOpenCodeSessions(
|
|
|
130
102
|
topicsCreated: 0,
|
|
131
103
|
topicsUpdated: 0,
|
|
132
104
|
messagesImported: 0,
|
|
133
|
-
messagesPruned: 0,
|
|
134
105
|
personasCreated: [],
|
|
135
|
-
topicUpdatesQueued: 0,
|
|
136
106
|
extractionScansQueued: 0,
|
|
137
|
-
partialSessionsFound: 0,
|
|
138
|
-
archiveScansQueued: 0,
|
|
139
107
|
};
|
|
140
108
|
|
|
141
|
-
// ─── Step 1:
|
|
142
|
-
//
|
|
143
|
-
//
|
|
109
|
+
// ─── Step 1: Ensure topics exist for ALL primary sessions ─────────────
|
|
110
|
+
// Always runs (cheap), so session titles stay current regardless of
|
|
111
|
+
// whether we process messages this cycle.
|
|
144
112
|
const allSessions = await reader.getSessionsUpdatedSince(new Date(0));
|
|
145
113
|
const primarySessions = allSessions.filter(s => !s.parentId);
|
|
146
114
|
|
|
@@ -150,656 +118,136 @@ export async function importOpenCodeSessions(
|
|
|
150
118
|
else if (topicResult === "updated") result.topicsUpdated++;
|
|
151
119
|
}
|
|
152
120
|
|
|
153
|
-
|
|
154
|
-
const sinceMs = since.getTime();
|
|
155
|
-
const updatedSessions = primarySessions.filter(s => s.time.updated > sinceMs);
|
|
156
|
-
|
|
157
|
-
console.log(
|
|
158
|
-
`[OpenCode] Found ${primarySessions.length} total sessions, ` +
|
|
159
|
-
`${updatedSessions.length} updated since ${since.toISOString()}`
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const agentsForPersona = new Set<string>();
|
|
163
|
-
const sessionAgentBatches: SessionAgentMessages[] = [];
|
|
164
|
-
|
|
165
|
-
for (const session of updatedSessions) {
|
|
166
|
-
result.sessionsProcessed++;
|
|
167
|
-
const messages = await reader.getMessagesForSession(session.id, since);
|
|
168
|
-
const relevant = filterRelevantMessages(messages);
|
|
169
|
-
const messagesByAgent = new Map<string, OpenCodeMessage[]>();
|
|
170
|
-
|
|
171
|
-
for (const msg of relevant) {
|
|
172
|
-
agentsForPersona.add(msg.agent);
|
|
173
|
-
const existing = messagesByAgent.get(msg.agent) ?? [];
|
|
174
|
-
existing.push(msg);
|
|
175
|
-
messagesByAgent.set(msg.agent, existing);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
for (const [agentName, agentMessages] of messagesByAgent) {
|
|
179
|
-
sessionAgentBatches.push({
|
|
180
|
-
sessionId: session.id,
|
|
181
|
-
agentName,
|
|
182
|
-
messages: agentMessages,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ─── Steps 3-8: Only run if we have new messages to process ──────────
|
|
188
|
-
let isFirstImport = false;
|
|
189
|
-
|
|
190
|
-
if (sessionAgentBatches.length > 0) {
|
|
191
|
-
// ─── Step 3: Ensure personas exist ─────────────────────────────────
|
|
192
|
-
const agentNameToPersonaId = new Map<string, string>();
|
|
193
|
-
|
|
194
|
-
for (const agentName of agentsForPersona) {
|
|
195
|
-
let existing = stateManager.persona_getByName(agentName);
|
|
196
|
-
if (!existing) {
|
|
197
|
-
existing = await ensureAgentPersona(agentName, {
|
|
198
|
-
stateManager,
|
|
199
|
-
interface: eiInterface,
|
|
200
|
-
reader,
|
|
201
|
-
});
|
|
202
|
-
result.personasCreated.push(agentName);
|
|
203
|
-
}
|
|
204
|
-
agentNameToPersonaId.set(agentName, existing.id);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
for (const batch of sessionAgentBatches) {
|
|
208
|
-
batch.personaId = agentNameToPersonaId.get(batch.agentName);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Build reverse mapping: personaId → agent names
|
|
212
|
-
const personaIdToAgentNames = new Map<string, Set<string>>();
|
|
213
|
-
for (const [agentName, personaId] of agentNameToPersonaId) {
|
|
214
|
-
const names = personaIdToAgentNames.get(personaId) ?? new Set();
|
|
215
|
-
names.add(agentName);
|
|
216
|
-
personaIdToAgentNames.set(personaId, names);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ─── Steps 4-5: Merge/Dedup/Prune per persona ─────────────────────
|
|
220
|
-
const batchesByPersona = new Map<string, SessionAgentMessages[]>();
|
|
221
|
-
for (const batch of sessionAgentBatches) {
|
|
222
|
-
if (!batch.personaId) continue;
|
|
223
|
-
const existing = batchesByPersona.get(batch.personaId) ?? [];
|
|
224
|
-
existing.push(batch);
|
|
225
|
-
batchesByPersona.set(batch.personaId, existing);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Track surviving new messages per persona for partial session detection
|
|
229
|
-
const survivingNewByPersona = new Map<string, OpenCodeMessage[]>();
|
|
230
|
-
|
|
231
|
-
for (const [personaId, personaBatches] of batchesByPersona) {
|
|
232
|
-
// Combine all new OC messages for this persona
|
|
233
|
-
const allNew: OpenCodeMessage[] = [];
|
|
234
|
-
for (const batch of personaBatches) {
|
|
235
|
-
allNew.push(...batch.messages);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Get existing persona messages for dedup + pruning analysis
|
|
239
|
-
const existingMessages = stateManager.messages_get(personaId);
|
|
240
|
-
const existingIds = new Set(existingMessages.map(m => m.id));
|
|
241
|
-
|
|
242
|
-
// Dedup: only messages not already in state
|
|
243
|
-
const genuinelyNew = allNew.filter(m => !existingIds.has(m.id));
|
|
244
|
-
if (genuinelyNew.length === 0) continue;
|
|
245
|
-
|
|
246
|
-
// Build merged list for pruning analysis
|
|
247
|
-
const merged: (MiniMessage | ExternalMessage)[] = [
|
|
248
|
-
...existingMessages.map(m => ({ id: m.id, timestamp: m.timestamp })),
|
|
249
|
-
...genuinelyNew.map(m => ({
|
|
250
|
-
id: m.id,
|
|
251
|
-
timestamp: m.timestamp,
|
|
252
|
-
isExternal: true as const,
|
|
253
|
-
sessionId: m.sessionId,
|
|
254
|
-
})),
|
|
255
|
-
];
|
|
256
|
-
|
|
257
|
-
// Prune
|
|
258
|
-
const keptIds = pruneImportMessages(merged, existingMessages);
|
|
259
|
-
const keptSet = new Set(keptIds);
|
|
260
|
-
|
|
261
|
-
// ─── Step 6: Write to persona state ────────────────────────────
|
|
262
|
-
const survivingNew: OpenCodeMessage[] = [];
|
|
263
|
-
for (const ocMsg of genuinelyNew) {
|
|
264
|
-
if (keptSet.has(ocMsg.id)) {
|
|
265
|
-
stateManager.messages_append(personaId, convertToEiMessage(ocMsg));
|
|
266
|
-
survivingNew.push(ocMsg);
|
|
267
|
-
result.messagesImported++;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const prunedExistingIds = existingMessages
|
|
272
|
-
.filter(m => !keptSet.has(m.id))
|
|
273
|
-
.map(m => m.id);
|
|
274
|
-
if (prunedExistingIds.length > 0) {
|
|
275
|
-
stateManager.messages_remove(personaId, prunedExistingIds);
|
|
276
|
-
result.messagesPruned += prunedExistingIds.length;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
stateManager.messages_sort(personaId);
|
|
280
|
-
stateManager.persona_update(personaId, {
|
|
281
|
-
last_activity: new Date().toISOString(),
|
|
282
|
-
});
|
|
283
|
-
eiInterface?.onMessageAdded?.(personaId);
|
|
284
|
-
|
|
285
|
-
if (survivingNew.length > 0) {
|
|
286
|
-
survivingNewByPersona.set(personaId, survivingNew);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
291
|
-
eiInterface?.onHumanUpdated?.();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ─── Step 7: Detect partial sessions → SessionUpdate ─────────────
|
|
295
|
-
for (const [personaId, survivingMsgs] of survivingNewByPersona) {
|
|
296
|
-
const agentNames = personaIdToAgentNames.get(personaId) ?? new Set();
|
|
297
|
-
const persona = stateManager.persona_getById(personaId);
|
|
298
|
-
if (!persona) continue;
|
|
299
|
-
|
|
300
|
-
// Group surviving new messages by session
|
|
301
|
-
const bySession = new Map<string, OpenCodeMessage[]>();
|
|
302
|
-
for (const msg of survivingMsgs) {
|
|
303
|
-
const existing = bySession.get(msg.sessionId) ?? [];
|
|
304
|
-
existing.push(msg);
|
|
305
|
-
bySession.set(msg.sessionId, existing);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
for (const [sessionId] of bySession) {
|
|
309
|
-
const isPartial = await checkPartialSession(
|
|
310
|
-
personaId, sessionId, agentNames, reader, stateManager
|
|
311
|
-
);
|
|
312
|
-
if (isPartial) {
|
|
313
|
-
result.partialSessionsFound++;
|
|
314
|
-
const scans = await processSessionUpdate(
|
|
315
|
-
personaId, persona.display_name, sessionId, agentNames,
|
|
316
|
-
reader, stateManager
|
|
317
|
-
);
|
|
318
|
-
result.extractionScansQueued += scans;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ─── Step 8: Queue topic updates + extraction scans ──────────────
|
|
324
|
-
isFirstImport = initializeExtractionPointIfNeeded(
|
|
325
|
-
sessionAgentBatches, stateManager
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
if (result.messagesImported > 0) {
|
|
329
|
-
// Topic description updates for sessions with new messages
|
|
330
|
-
result.topicUpdatesQueued = queueTopicUpdatesForBatches(
|
|
331
|
-
sessionAgentBatches, stateManager
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
if (isFirstImport) {
|
|
335
|
-
// First import: all 4 extraction types on ALL surviving messages
|
|
336
|
-
result.extractionScansQueued += queueAllExtractionsForAllMessages(
|
|
337
|
-
batchesByPersona, stateManager
|
|
338
|
-
);
|
|
339
|
-
console.log(
|
|
340
|
-
`[OpenCode] First import: queued extraction scans for ` +
|
|
341
|
-
`${batchesByPersona.size} persona(s)`
|
|
342
|
-
);
|
|
343
|
-
} else {
|
|
344
|
-
// Normal sync: all 4 extraction types on newly imported messages
|
|
345
|
-
result.extractionScansQueued += queueExtractionsForNewMessages(
|
|
346
|
-
batchesByPersona, stateManager
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
console.log(
|
|
351
|
-
`[OpenCode] Queued ${result.topicUpdatesQueued} topic updates, ` +
|
|
352
|
-
`${result.extractionScansQueued} extraction scans`
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
} else if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
121
|
+
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
356
122
|
eiInterface?.onHumanUpdated?.();
|
|
357
123
|
}
|
|
358
124
|
|
|
359
|
-
// ─── Step
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
stateManager, reader, eiInterface
|
|
363
|
-
);
|
|
364
|
-
result.archiveScansQueued = archiveResult.scansQueued;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return result;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// =============================================================================
|
|
371
|
-
// Pruning
|
|
372
|
-
// =============================================================================
|
|
125
|
+
// ─── Step 2: Find next unprocessed session ────────────────────────────
|
|
126
|
+
const human = stateManager.getHuman();
|
|
127
|
+
const processedSessions = human.settings?.opencode?.processed_sessions ?? {};
|
|
373
128
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
* Rules:
|
|
378
|
-
* - Always keep at least minMessages (even if they're ancient)
|
|
379
|
-
* - Remove messages older than maxAgeDays IF:
|
|
380
|
-
* - External (never in state → safe to drop), OR
|
|
381
|
-
* - Fully extracted ([p,r,o,f] all true → knowledge already captured)
|
|
382
|
-
* - Messages that are old but NOT external and NOT fully extracted are KEPT
|
|
383
|
-
* (they still have knowledge to extract)
|
|
384
|
-
*
|
|
385
|
-
* @returns Array of message IDs to keep
|
|
386
|
-
*/
|
|
387
|
-
export function pruneImportMessages(
|
|
388
|
-
merged: (MiniMessage | ExternalMessage)[],
|
|
389
|
-
existingMessages: Message[],
|
|
390
|
-
minMessages: number = MESSAGE_MIN_COUNT,
|
|
391
|
-
maxAgeDays: number = MESSAGE_MAX_AGE_DAYS
|
|
392
|
-
): string[] {
|
|
393
|
-
if (merged.length <= minMessages) return merged.map(m => m.id);
|
|
394
|
-
|
|
395
|
-
const cutoffMs = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
|
|
396
|
-
const sorted = [...merged].sort(
|
|
397
|
-
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
129
|
+
// Sessions sorted oldest-first; find first unprocessed or updated-since-last-import
|
|
130
|
+
const sortedSessions = [...primarySessions].sort(
|
|
131
|
+
(a, b) => a.time.updated - b.time.updated
|
|
398
132
|
);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const isExternal = "isExternal" in m && (m as ExternalMessage).isExternal;
|
|
410
|
-
const existing = existingById.get(m.id);
|
|
411
|
-
const fullyExtracted = existing?.f && existing?.r && existing?.o && existing?.p;
|
|
412
|
-
|
|
413
|
-
if (isExternal || fullyExtracted) {
|
|
414
|
-
toRemove.push(m.id);
|
|
133
|
+
let targetSession: OpenCodeSession | null = null;
|
|
134
|
+
for (const session of sortedSessions) {
|
|
135
|
+
const lastImported = processedSessions[session.id];
|
|
136
|
+
if (!lastImported) {
|
|
137
|
+
targetSession = session;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (session.time.updated > new Date(lastImported).getTime()) {
|
|
141
|
+
targetSession = session;
|
|
142
|
+
break;
|
|
415
143
|
}
|
|
416
144
|
}
|
|
417
145
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// =============================================================================
|
|
423
|
-
// Partial Session Detection & SessionUpdate
|
|
424
|
-
// =============================================================================
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Check if a session is "partial" — some messages in state but not all.
|
|
428
|
-
* This happens when an old session gets new messages ("necro session").
|
|
429
|
-
*/
|
|
430
|
-
async function checkPartialSession(
|
|
431
|
-
personaId: string,
|
|
432
|
-
sessionId: string,
|
|
433
|
-
agentNames: Set<string>,
|
|
434
|
-
reader: IOpenCodeReader,
|
|
435
|
-
stateManager: StateManager
|
|
436
|
-
): Promise<boolean> {
|
|
437
|
-
const allSessionMsgs = await reader.getMessagesForSession(sessionId);
|
|
438
|
-
const relevantMsgs = filterRelevantMessages(
|
|
439
|
-
allSessionMsgs.filter(m => agentNames.has(m.agent))
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
const personaMsgs = stateManager.messages_get(personaId);
|
|
443
|
-
const stateIds = new Set(personaMsgs.map(m => m.id));
|
|
444
|
-
|
|
445
|
-
return relevantMsgs.some(m => !stateIds.has(m.id));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Handle a partial session by injecting missing messages and queuing extraction.
|
|
450
|
-
*
|
|
451
|
-
* Old (missing) messages are injected with [p,r,o,f]=true — they serve as
|
|
452
|
-
* context only and are already "fully extracted" so ceremony can prune them.
|
|
453
|
-
* The new messages (already in state, NOT pre-marked) go to messages_analyze
|
|
454
|
-
* for actual extraction.
|
|
455
|
-
*/
|
|
456
|
-
async function processSessionUpdate(
|
|
457
|
-
personaId: string,
|
|
458
|
-
personaDisplayName: string,
|
|
459
|
-
sessionId: string,
|
|
460
|
-
agentNames: Set<string>,
|
|
461
|
-
reader: IOpenCodeReader,
|
|
462
|
-
stateManager: StateManager
|
|
463
|
-
): Promise<number> {
|
|
464
|
-
const allSessionMsgs = await reader.getMessagesForSession(sessionId);
|
|
465
|
-
const relevantMsgs = filterRelevantMessages(
|
|
466
|
-
allSessionMsgs.filter(m => agentNames.has(m.agent))
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
if (relevantMsgs.length === 0) return 0;
|
|
470
|
-
|
|
471
|
-
// Find which messages are missing from state
|
|
472
|
-
const personaMsgs = stateManager.messages_get(personaId);
|
|
473
|
-
const stateIds = new Set(personaMsgs.map(m => m.id));
|
|
474
|
-
const missingMsgs = relevantMsgs.filter(m => !stateIds.has(m.id));
|
|
475
|
-
|
|
476
|
-
// Inject missing messages PRE-MARKED as fully extracted.
|
|
477
|
-
// They're context only — ceremony will prune them (old + [p,r,o,f]=true).
|
|
478
|
-
for (const ocMsg of missingMsgs) {
|
|
479
|
-
stateManager.messages_append(personaId, convertToPreMarkedEiMessage(ocMsg));
|
|
146
|
+
if (!targetSession) {
|
|
147
|
+
// Nothing new to process — bump last_sync and return
|
|
148
|
+
console.log(`[OpenCode] All sessions processed, nothing new since extraction_point`);
|
|
149
|
+
return result;
|
|
480
150
|
}
|
|
481
|
-
stateManager.messages_sort(personaId);
|
|
482
151
|
|
|
483
152
|
console.log(
|
|
484
|
-
`[OpenCode]
|
|
485
|
-
`
|
|
153
|
+
`[OpenCode] Processing session: "${targetSession.title}" ` +
|
|
154
|
+
`(updated: ${new Date(targetSession.time.updated).toISOString()})`
|
|
486
155
|
);
|
|
487
156
|
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const allInState = stateManager.messages_get(personaId);
|
|
492
|
-
const sessionMsgIds = new Set(relevantMsgs.map(m => m.id));
|
|
493
|
-
const missingIds = new Set(missingMsgs.map(m => m.id));
|
|
494
|
-
|
|
495
|
-
const contextMsgs = allInState.filter(
|
|
496
|
-
m => sessionMsgIds.has(m.id) && missingIds.has(m.id)
|
|
497
|
-
);
|
|
498
|
-
const analyzeMsgs = allInState.filter(
|
|
499
|
-
m => sessionMsgIds.has(m.id) && !missingIds.has(m.id)
|
|
500
|
-
);
|
|
501
|
-
|
|
502
|
-
if (analyzeMsgs.length === 0) return 0;
|
|
503
|
-
|
|
504
|
-
const context: ExtractionContext = {
|
|
505
|
-
personaId,
|
|
506
|
-
personaDisplayName,
|
|
507
|
-
messages_context: contextMsgs,
|
|
508
|
-
messages_analyze: analyzeMsgs,
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
queueAllScans(context, stateManager);
|
|
512
|
-
|
|
513
|
-
const human = stateManager.getHuman();
|
|
514
|
-
const topic = human.topics.find(t => t.id === sessionId);
|
|
515
|
-
if (topic) {
|
|
516
|
-
queueDirectTopicUpdate(topic, context, stateManager);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return 4;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// =============================================================================
|
|
523
|
-
// Archive Scan
|
|
524
|
-
// =============================================================================
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Process old sessions from SQLite for extraction.
|
|
528
|
-
*
|
|
529
|
-
* Replaces gradual-extraction.ts entirely. Reads sessions between
|
|
530
|
-
* extraction_point and the 14-day cutoff, injects their messages into
|
|
531
|
-
* persona state, and queues all 4 extraction types.
|
|
532
|
-
*
|
|
533
|
-
* Unlike SessionUpdate, archive messages are NOT pre-marked — they need
|
|
534
|
-
* actual extraction. The queue-empty gate on ceremony (in ceremony.ts)
|
|
535
|
-
* prevents premature pruning while extraction is pending.
|
|
536
|
-
*
|
|
537
|
-
* Bounded by a token budget to prevent queue flooding.
|
|
538
|
-
*/
|
|
539
|
-
async function processArchiveScan(
|
|
540
|
-
stateManager: StateManager,
|
|
541
|
-
reader: IOpenCodeReader,
|
|
542
|
-
eiInterface?: Ei_Interface
|
|
543
|
-
): Promise<{ scansQueued: number; newExtractionPoint: string | null }> {
|
|
544
|
-
const human = stateManager.getHuman();
|
|
545
|
-
const extractionPoint = human.settings?.opencode?.extraction_point;
|
|
157
|
+
// ─── Step 3: Pull and filter messages ────────────────────────────────
|
|
158
|
+
const allMsgs = await reader.getMessagesForSession(targetSession.id);
|
|
159
|
+
const relevant = filterRelevantMessages(allMsgs);
|
|
546
160
|
|
|
547
|
-
if (
|
|
548
|
-
|
|
161
|
+
if (relevant.length === 0) {
|
|
162
|
+
// Empty session — mark processed and advance
|
|
163
|
+
updateExtractionState(stateManager, targetSession);
|
|
164
|
+
return result;
|
|
549
165
|
}
|
|
550
166
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
167
|
+
// ─── Step 4: Resolve agents → personas, group by persona ID ────────
|
|
168
|
+
// Resolve aliases up front so 'sisyphus' and 'Sisyphus (Ultraworker)'
|
|
169
|
+
// land in the same bucket instead of clobbering each other.
|
|
170
|
+
const byPersonaId = new Map<string, { persona: NonNullable<ReturnType<typeof stateManager.persona_getByName>>; msgs: OpenCodeMessage[] }>();
|
|
171
|
+
for (const msg of relevant) {
|
|
172
|
+
let persona = stateManager.persona_getByName(msg.agent);
|
|
173
|
+
if (!persona) {
|
|
174
|
+
persona = await ensureAgentPersona(msg.agent, {
|
|
175
|
+
stateManager,
|
|
176
|
+
interface: eiInterface,
|
|
177
|
+
reader,
|
|
178
|
+
});
|
|
179
|
+
result.personasCreated.push(msg.agent);
|
|
180
|
+
}
|
|
181
|
+
const bucket = byPersonaId.get(persona.id);
|
|
182
|
+
if (bucket) {
|
|
183
|
+
bucket.msgs.push(msg);
|
|
184
|
+
} else {
|
|
185
|
+
byPersonaId.set(persona.id, { persona, msgs: [msg] });
|
|
186
|
+
}
|
|
571
187
|
}
|
|
572
188
|
|
|
573
|
-
|
|
574
|
-
|
|
189
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
190
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
575
191
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
for (const persona of openCodePersonas) {
|
|
581
|
-
for (const m of stateManager.messages_get(persona.id)) {
|
|
582
|
-
allStateMessageIds.add(m.id);
|
|
192
|
+
for (const [, { persona, msgs: agentMsgs }] of byPersonaId) {
|
|
193
|
+
// Archive persona (message store only, not a conversational persona)
|
|
194
|
+
if (!persona.is_archived) {
|
|
195
|
+
stateManager.persona_archive(persona.id);
|
|
583
196
|
}
|
|
584
|
-
}
|
|
585
197
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
let scansQueued = 0;
|
|
591
|
-
let lastProcessed: OpenCodeSession | null = null;
|
|
592
|
-
|
|
593
|
-
for (const session of sessions) {
|
|
594
|
-
const allMsgs = await reader.getMessagesForSession(session.id);
|
|
595
|
-
const relevant = filterRelevantMessages(allMsgs);
|
|
596
|
-
|
|
597
|
-
// Skip sessions whose messages are already in state
|
|
598
|
-
if (relevant.some(m => allStateMessageIds.has(m.id))) {
|
|
599
|
-
lastProcessed = session;
|
|
600
|
-
continue;
|
|
198
|
+
// Clear existing messages — this persona holds exactly one session at a time
|
|
199
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
200
|
+
if (existingMsgs.length > 0) {
|
|
201
|
+
stateManager.messages_remove(persona.id, existingMsgs.map(m => m.id));
|
|
601
202
|
}
|
|
602
203
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
204
|
+
// Write messages — pre-mark old ones, leave new ones unmarked for extraction
|
|
205
|
+
const toAnalyze: Message[] = [];
|
|
206
|
+
for (const ocMsg of agentMsgs) {
|
|
207
|
+
const msgMs = new Date(ocMsg.timestamp).getTime();
|
|
208
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
209
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(ocMsg) : convertToEiMessage(ocMsg);
|
|
210
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
211
|
+
result.messagesImported++;
|
|
212
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
606
213
|
}
|
|
607
214
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
215
|
+
stateManager.messages_sort(persona.id);
|
|
216
|
+
stateManager.persona_update(persona.id, {
|
|
217
|
+
last_activity: new Date().toISOString(),
|
|
218
|
+
});
|
|
219
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
611
220
|
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
for (const [agentName, agentMsgs] of byAgent) {
|
|
621
|
-
// Resolve persona (create if needed — unlikely for archive but safe)
|
|
622
|
-
let persona = stateManager.persona_getByName(agentName);
|
|
623
|
-
if (!persona) {
|
|
624
|
-
persona = await ensureAgentPersona(agentName, {
|
|
625
|
-
stateManager,
|
|
626
|
-
interface: eiInterface,
|
|
627
|
-
reader,
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Inject messages into persona state (NOT pre-marked — need extraction)
|
|
632
|
-
for (const ocMsg of agentMsgs) {
|
|
633
|
-
stateManager.messages_append(persona.id, convertToEiMessage(ocMsg));
|
|
634
|
-
allStateMessageIds.add(ocMsg.id);
|
|
635
|
-
}
|
|
636
|
-
stateManager.messages_sort(persona.id);
|
|
637
|
-
|
|
638
|
-
// Build extraction context from the injected messages
|
|
639
|
-
const injectedMsgs = stateManager.messages_get(persona.id)
|
|
640
|
-
.filter(m => agentMsgs.some(am => am.id === m.id));
|
|
221
|
+
// ─── Step 5: Queue extraction for unmarked messages ────────────────
|
|
222
|
+
if (toAnalyze.length > 0) {
|
|
223
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
224
|
+
const analyzeIds = new Set(toAnalyze.map(m => m.id));
|
|
225
|
+
const analyzeStartIndex = allInState.findIndex(m => analyzeIds.has(m.id));
|
|
226
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
641
227
|
|
|
642
228
|
const context: ExtractionContext = {
|
|
643
229
|
personaId: persona.id,
|
|
644
230
|
personaDisplayName: persona.display_name,
|
|
645
|
-
messages_context:
|
|
646
|
-
messages_analyze:
|
|
231
|
+
messages_context: contextMsgs,
|
|
232
|
+
messages_analyze: toAnalyze,
|
|
647
233
|
};
|
|
648
234
|
|
|
649
235
|
queueAllScans(context, stateManager);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const topic = human.topics.find(t => t.id === session.id);
|
|
653
|
-
if (topic) {
|
|
654
|
-
queueDirectTopicUpdate(topic, context, stateManager);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
lastProcessed = session;
|
|
659
|
-
|
|
660
|
-
if (tokenBudget >= tokenLimit) {
|
|
661
|
-
console.log(
|
|
662
|
-
`[OpenCode] Archive scan: token budget reached after ${scansQueued} scans`
|
|
663
|
-
);
|
|
664
|
-
break;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// Advance extraction_point
|
|
669
|
-
if (lastProcessed) {
|
|
670
|
-
const newPoint = new Date(lastProcessed.time.updated).toISOString();
|
|
671
|
-
updateExtractionPoint(stateManager, newPoint);
|
|
672
|
-
console.log(
|
|
673
|
-
`[OpenCode] Archive scan: ${scansQueued} scans queued, ` +
|
|
674
|
-
`extraction_point → ${newPoint}`
|
|
675
|
-
);
|
|
676
|
-
return { scansQueued, newExtractionPoint: newPoint };
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return { scansQueued: 0, newExtractionPoint: null };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// =============================================================================
|
|
683
|
-
// Extraction Queueing
|
|
684
|
-
// =============================================================================
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Queue topic description updates for sessions with new messages.
|
|
688
|
-
* Finds batch messages in persona state and builds ExtractionContext.
|
|
689
|
-
*/
|
|
690
|
-
function queueTopicUpdatesForBatches(
|
|
691
|
-
batches: SessionAgentMessages[],
|
|
692
|
-
stateManager: StateManager
|
|
693
|
-
): number {
|
|
694
|
-
const human = stateManager.getHuman();
|
|
695
|
-
let totalChunks = 0;
|
|
696
|
-
|
|
697
|
-
for (const batch of batches) {
|
|
698
|
-
if (!batch.personaId) continue;
|
|
699
|
-
|
|
700
|
-
const topic = human.topics.find(t => t.id === batch.sessionId);
|
|
701
|
-
if (!topic) continue;
|
|
702
|
-
|
|
703
|
-
const persona = stateManager.persona_getById(batch.personaId);
|
|
704
|
-
if (!persona) continue;
|
|
705
|
-
|
|
706
|
-
const allMessages = stateManager.messages_get(batch.personaId);
|
|
707
|
-
const batchMessageIds = new Set(batch.messages.map(m => m.id));
|
|
708
|
-
|
|
709
|
-
const analyzeStartIndex = allMessages.findIndex(m => batchMessageIds.has(m.id));
|
|
710
|
-
if (analyzeStartIndex === -1) continue;
|
|
711
|
-
|
|
712
|
-
const context: ExtractionContext = {
|
|
713
|
-
personaId: batch.personaId,
|
|
714
|
-
personaDisplayName: persona.display_name,
|
|
715
|
-
messages_context: allMessages.slice(0, analyzeStartIndex),
|
|
716
|
-
messages_analyze: allMessages.filter(m => batchMessageIds.has(m.id)),
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
if (context.messages_analyze.length === 0) continue;
|
|
720
|
-
|
|
721
|
-
totalChunks += queueDirectTopicUpdate(topic, context, stateManager);
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return totalChunks;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Queue all 4 extraction types for newly imported messages (normal sync).
|
|
729
|
-
* Groups by persona to avoid duplicate scan queueing.
|
|
730
|
-
*/
|
|
731
|
-
function queueExtractionsForNewMessages(
|
|
732
|
-
batchesByPersona: Map<string, SessionAgentMessages[]>,
|
|
733
|
-
stateManager: StateManager
|
|
734
|
-
): number {
|
|
735
|
-
let scansQueued = 0;
|
|
736
|
-
|
|
737
|
-
for (const [personaId, personaBatches] of batchesByPersona) {
|
|
738
|
-
const persona = stateManager.persona_getById(personaId);
|
|
739
|
-
if (!persona) continue;
|
|
740
|
-
|
|
741
|
-
const allMessages = stateManager.messages_get(personaId);
|
|
742
|
-
|
|
743
|
-
// Combine all batch message IDs for this persona
|
|
744
|
-
const batchMessageIds = new Set<string>();
|
|
745
|
-
for (const batch of personaBatches) {
|
|
746
|
-
for (const m of batch.messages) {
|
|
747
|
-
batchMessageIds.add(m.id);
|
|
748
|
-
}
|
|
236
|
+
result.extractionScansQueued += 4;
|
|
749
237
|
}
|
|
750
|
-
|
|
751
|
-
// Find the new messages that are actually in state (survived pruning)
|
|
752
|
-
const analyzeMessages = allMessages.filter(m => batchMessageIds.has(m.id));
|
|
753
|
-
if (analyzeMessages.length === 0) continue;
|
|
754
|
-
|
|
755
|
-
const analyzeStartIndex = allMessages.findIndex(m => batchMessageIds.has(m.id));
|
|
756
|
-
const contextMessages = analyzeStartIndex > 0
|
|
757
|
-
? allMessages.slice(0, analyzeStartIndex)
|
|
758
|
-
: [];
|
|
759
|
-
|
|
760
|
-
const context: ExtractionContext = {
|
|
761
|
-
personaId,
|
|
762
|
-
personaDisplayName: persona.display_name,
|
|
763
|
-
messages_context: contextMessages,
|
|
764
|
-
messages_analyze: analyzeMessages,
|
|
765
|
-
};
|
|
766
|
-
|
|
767
|
-
queueAllScans(context, stateManager);
|
|
768
|
-
scansQueued += 4;
|
|
769
238
|
}
|
|
770
239
|
|
|
771
|
-
|
|
772
|
-
}
|
|
240
|
+
result.sessionsProcessed = 1;
|
|
773
241
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
* Used on first import — the "fun" moment where extraction kicks off.
|
|
777
|
-
*/
|
|
778
|
-
function queueAllExtractionsForAllMessages(
|
|
779
|
-
batchesByPersona: Map<string, SessionAgentMessages[]>,
|
|
780
|
-
stateManager: StateManager
|
|
781
|
-
): number {
|
|
782
|
-
let scansQueued = 0;
|
|
783
|
-
|
|
784
|
-
for (const [personaId] of batchesByPersona) {
|
|
785
|
-
const persona = stateManager.persona_getById(personaId);
|
|
786
|
-
if (!persona) continue;
|
|
787
|
-
|
|
788
|
-
const allMessages = stateManager.messages_get(personaId);
|
|
789
|
-
if (allMessages.length === 0) continue;
|
|
790
|
-
|
|
791
|
-
const context: ExtractionContext = {
|
|
792
|
-
personaId,
|
|
793
|
-
personaDisplayName: persona.display_name,
|
|
794
|
-
messages_context: [],
|
|
795
|
-
messages_analyze: allMessages,
|
|
796
|
-
};
|
|
242
|
+
// ─── Step 6: Advance extraction state ────────────────────────────────
|
|
243
|
+
updateExtractionState(stateManager, targetSession);
|
|
797
244
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
245
|
+
console.log(
|
|
246
|
+
`[OpenCode] Session complete: ${result.messagesImported} messages imported, ` +
|
|
247
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
248
|
+
);
|
|
801
249
|
|
|
802
|
-
return
|
|
250
|
+
return result;
|
|
803
251
|
}
|
|
804
252
|
|
|
805
253
|
// =============================================================================
|
|
@@ -850,39 +298,17 @@ async function ensureSessionTopic(
|
|
|
850
298
|
// State Helpers
|
|
851
299
|
// =============================================================================
|
|
852
300
|
|
|
853
|
-
function
|
|
854
|
-
batches: SessionAgentMessages[],
|
|
855
|
-
stateManager: StateManager
|
|
856
|
-
): boolean {
|
|
857
|
-
const human = stateManager.getHuman();
|
|
858
|
-
const existingPoint = human.settings?.opencode?.extraction_point;
|
|
859
|
-
|
|
860
|
-
if (existingPoint) return false;
|
|
861
|
-
|
|
862
|
-
let earliestTimestamp: number | null = null;
|
|
863
|
-
|
|
864
|
-
for (const batch of batches) {
|
|
865
|
-
for (const msg of batch.messages) {
|
|
866
|
-
const msgMs = new Date(msg.timestamp).getTime();
|
|
867
|
-
if (earliestTimestamp === null || msgMs < earliestTimestamp) {
|
|
868
|
-
earliestTimestamp = msgMs;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
if (earliestTimestamp === null) return false;
|
|
874
|
-
|
|
875
|
-
const extractionPoint = new Date(earliestTimestamp).toISOString();
|
|
876
|
-
updateExtractionPoint(stateManager, extractionPoint);
|
|
877
|
-
console.log(`[OpenCode] Initialized extraction_point to ${extractionPoint}`);
|
|
878
|
-
return true;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function updateExtractionPoint(
|
|
301
|
+
function updateExtractionState(
|
|
882
302
|
stateManager: StateManager,
|
|
883
|
-
|
|
303
|
+
session: OpenCodeSession
|
|
884
304
|
): void {
|
|
885
305
|
const human = stateManager.getHuman();
|
|
306
|
+
const newPoint = new Date(session.time.updated).toISOString();
|
|
307
|
+
const processedSessions = {
|
|
308
|
+
...(human.settings?.opencode?.processed_sessions ?? {}),
|
|
309
|
+
[session.id]: new Date().toISOString(),
|
|
310
|
+
};
|
|
311
|
+
|
|
886
312
|
stateManager.setHuman({
|
|
887
313
|
...human,
|
|
888
314
|
settings: {
|
|
@@ -890,6 +316,7 @@ function updateExtractionPoint(
|
|
|
890
316
|
opencode: {
|
|
891
317
|
...human.settings?.opencode,
|
|
892
318
|
extraction_point: newPoint,
|
|
319
|
+
processed_sessions: processedSessions,
|
|
893
320
|
},
|
|
894
321
|
},
|
|
895
322
|
});
|