@gamaze/hicortex 0.7.1 → 0.10.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/README.md +72 -39
- package/dist/claude-md.d.ts +9 -21
- package/dist/claude-md.js +9 -241
- package/dist/cli.d.ts +3 -2
- package/dist/cli.js +29 -11
- package/dist/consolidate.js +0 -7
- package/dist/db.js +24 -0
- package/dist/embedder.d.ts +11 -0
- package/dist/embedder.js +27 -0
- package/dist/extensions.d.ts +41 -88
- package/dist/extensions.js +36 -61
- package/dist/features.d.ts +21 -25
- package/dist/features.js +47 -83
- package/dist/hermes-transcript-reader.d.ts +27 -0
- package/dist/hermes-transcript-reader.js +134 -0
- package/dist/index.d.ts +16 -4
- package/dist/index.js +252 -344
- package/dist/init.d.ts +41 -1
- package/dist/init.js +545 -190
- package/dist/lesson-selection.d.ts +62 -0
- package/dist/lesson-selection.js +159 -0
- package/dist/lessons-context.d.ts +17 -0
- package/dist/lessons-context.js +96 -0
- package/dist/llm.d.ts +42 -29
- package/dist/llm.js +89 -270
- package/dist/mcp-server.d.ts +0 -1
- package/dist/mcp-server.js +404 -86
- package/dist/nightly.d.ts +9 -6
- package/dist/nightly.js +197 -357
- package/dist/oc-transcript-reader.d.ts +20 -0
- package/dist/oc-transcript-reader.js +61 -0
- package/dist/pi-transcript-reader.d.ts +1 -0
- package/dist/status.js +22 -2
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +28 -7
- package/dist/transcript-reader.d.ts +19 -0
- package/dist/transcript-reader.js +17 -3
- package/dist/types.d.ts +10 -0
- package/dist/uninstall.js +31 -1
- package/hermes-plugin/hicortex/README.md +77 -0
- package/hermes-plugin/hicortex/__init__.py +17 -0
- package/hermes-plugin/hicortex/client.py +162 -0
- package/hermes-plugin/hicortex/config.py +105 -0
- package/hermes-plugin/hicortex/plugin.yaml +12 -0
- package/hermes-plugin/hicortex/provider.py +432 -0
- package/openclaw.plugin.json +17 -44
- package/package.json +7 -5
- package/dist/pro-loader.d.ts +0 -33
- package/dist/pro-loader.js +0 -187
package/dist/nightly.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Nightly pipeline — manual trigger or called by the persistent server.
|
|
4
4
|
*
|
|
5
|
-
* Steps:
|
|
6
|
-
* 1. Read new
|
|
7
|
-
* 2.
|
|
8
|
-
* 3. Run consolidation (scoring, reflection, linking, decay)
|
|
9
|
-
* 4.
|
|
10
|
-
*
|
|
5
|
+
* Steps (0.9.0+):
|
|
6
|
+
* 1. Read new harness transcripts since last run
|
|
7
|
+
* 2. Denoise + POST each session to /distill (server captures for itself via localhost)
|
|
8
|
+
* 3. Run consolidation (scoring, reflection, linking, decay) — server mode only
|
|
9
|
+
* 4. Update last-run timestamp
|
|
10
|
+
*
|
|
11
|
+
* Every machine (server + clients) uses the same capture path: denoise locally,
|
|
12
|
+
* POST to /distill. No local LLM required for capture; distillation is server-side.
|
|
11
13
|
*/
|
|
12
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
15
|
if (k2 === undefined) k2 = k;
|
|
@@ -59,10 +61,10 @@ const storage = __importStar(require("./storage.js"));
|
|
|
59
61
|
const distiller_js_1 = require("./distiller.js");
|
|
60
62
|
const consolidate_js_1 = require("./consolidate.js");
|
|
61
63
|
const transcript_reader_js_1 = require("./transcript-reader.js");
|
|
64
|
+
const hermes_transcript_reader_js_1 = require("./hermes-transcript-reader.js");
|
|
62
65
|
const pi_transcript_reader_js_1 = require("./pi-transcript-reader.js");
|
|
63
|
-
const
|
|
66
|
+
const oc_transcript_reader_js_1 = require("./oc-transcript-reader.js");
|
|
64
67
|
const features_js_1 = require("./features.js");
|
|
65
|
-
const extensions_js_1 = require("./extensions.js");
|
|
66
68
|
const state_js_1 = require("./state.js");
|
|
67
69
|
const telemetry_js_1 = require("./telemetry.js");
|
|
68
70
|
const HICORTEX_HOME = (0, node_path_1.join)((0, node_os_1.homedir)(), ".hicortex");
|
|
@@ -101,40 +103,44 @@ function writeLastRun(stateDir = HICORTEX_HOME) {
|
|
|
101
103
|
}
|
|
102
104
|
async function runNightly(options = {}) {
|
|
103
105
|
const dryRun = options.dryRun ?? false;
|
|
106
|
+
const captureOnly = options.captureOnly ?? false;
|
|
104
107
|
const stateDir = options.stateDir ?? HICORTEX_HOME;
|
|
105
108
|
// One-time migration of legacy state files (no-op if state.json exists)
|
|
106
109
|
(0, state_js_1.migrateLegacyState)(stateDir);
|
|
107
110
|
// Check mode: client or server
|
|
108
111
|
const savedConfig = readNightlyConfig(stateDir);
|
|
109
112
|
if (savedConfig?.mode === "client") {
|
|
113
|
+
// --capture-only is accepted in client mode but irrelevant: client nightly
|
|
114
|
+
// is already capture-only (no consolidation step).
|
|
110
115
|
await runClientNightly(savedConfig, dryRun);
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
const dbPath = (0, db_js_1.resolveDbPath)(options.dbPath);
|
|
114
|
-
|
|
119
|
+
const port = savedConfig?.port ?? 8787;
|
|
120
|
+
const modeLabel = captureOnly ? " (capture-only)" : dryRun ? " (dry run)" : "";
|
|
121
|
+
console.log(`[hicortex] Nightly pipeline starting${modeLabel}`);
|
|
122
|
+
if (captureOnly) {
|
|
123
|
+
console.log(`[hicortex] capture-only run — consolidation skipped`);
|
|
124
|
+
}
|
|
115
125
|
console.log(`[hicortex] DB: ${dbPath}`);
|
|
116
|
-
// Init DB
|
|
126
|
+
// Init DB — consolidation reads the DB directly; capture goes via HTTP.
|
|
117
127
|
const db = (0, db_js_1.initDb)(dbPath);
|
|
118
128
|
try {
|
|
119
129
|
// License: read from config file or env var, init feature cache
|
|
120
130
|
const licenseKey = readConfigLicenseKey(stateDir) ?? process.env.HICORTEX_LICENSE_KEY;
|
|
121
131
|
await (0, features_js_1.initFeatures)(licenseKey, stateDir);
|
|
122
|
-
// Init LLM
|
|
123
|
-
|
|
124
|
-
|
|
132
|
+
// Init LLM for consolidation (scoring + reflection). Capture (distillation)
|
|
133
|
+
// is handled by the running daemon over /distill — no local distill LLM needed.
|
|
134
|
+
// No LLM → capture loop still runs (sessions POST to /distill, which will 503
|
|
135
|
+
// transient-fail and hold the watermark), but consolidation is skipped.
|
|
136
|
+
let llmConfig = null;
|
|
125
137
|
if (savedConfig?.llmBackend === "claude-cli") {
|
|
126
138
|
const claudePath = (0, llm_js_1.findClaudeBinary)();
|
|
127
139
|
if (claudePath) {
|
|
128
140
|
llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
|
|
129
141
|
}
|
|
130
142
|
else {
|
|
131
|
-
console.warn("[hicortex] claude-cli configured but binary not found
|
|
132
|
-
llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
|
|
133
|
-
llmBaseUrl: savedConfig?.llmBaseUrl,
|
|
134
|
-
llmApiKey: savedConfig?.llmApiKey,
|
|
135
|
-
llmModel: savedConfig?.llmModel,
|
|
136
|
-
reflectModel: savedConfig?.reflectModel,
|
|
137
|
-
});
|
|
143
|
+
console.warn("[hicortex] claude-cli configured but binary not found — consolidation skipped");
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
else if (savedConfig?.llmBackend === "ollama") {
|
|
@@ -147,167 +153,138 @@ async function runNightly(options = {}) {
|
|
|
147
153
|
};
|
|
148
154
|
}
|
|
149
155
|
else {
|
|
150
|
-
llmConfig = (0, llm_js_1.
|
|
156
|
+
llmConfig = (0, llm_js_1.resolveExplicitLlmConfig)({
|
|
151
157
|
llmBaseUrl: savedConfig?.llmBaseUrl,
|
|
152
158
|
llmApiKey: savedConfig?.llmApiKey,
|
|
153
159
|
llmModel: savedConfig?.llmModel,
|
|
154
160
|
reflectModel: savedConfig?.reflectModel,
|
|
155
161
|
});
|
|
156
162
|
}
|
|
157
|
-
|
|
158
|
-
if (savedConfig?.distillModel) {
|
|
159
|
-
llmConfig.distillModel = savedConfig.distillModel;
|
|
160
|
-
}
|
|
161
|
-
if (savedConfig?.distillBaseUrl) {
|
|
162
|
-
llmConfig.distillBaseUrl = savedConfig.distillBaseUrl;
|
|
163
|
-
llmConfig.distillApiKey = savedConfig.distillApiKey ?? llmConfig.apiKey;
|
|
164
|
-
llmConfig.distillProvider = savedConfig.distillProvider ?? llmConfig.provider;
|
|
165
|
-
}
|
|
166
|
-
if (savedConfig?.reflectBaseUrl) {
|
|
163
|
+
if (llmConfig && savedConfig?.reflectBaseUrl) {
|
|
167
164
|
llmConfig.reflectBaseUrl = savedConfig.reflectBaseUrl;
|
|
168
165
|
llmConfig.reflectApiKey = savedConfig.reflectApiKey ?? llmConfig.apiKey;
|
|
169
166
|
llmConfig.reflectProvider = savedConfig.reflectProvider ?? llmConfig.provider;
|
|
170
167
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (llmConfig.provider === "ollama") {
|
|
174
|
-
console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
|
|
175
|
-
}
|
|
176
|
-
const llm = new llm_js_1.LlmClient(llmConfig);
|
|
177
|
-
const distillInfo = llmConfig.distillBaseUrl
|
|
178
|
-
? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
|
|
179
|
-
: llmConfig.distillModel ?? "";
|
|
180
|
-
console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
|
|
181
|
-
// Step 1: Read new transcripts (CC + Pi)
|
|
168
|
+
const llm = llmConfig ? new llm_js_1.LlmClient(llmConfig) : null;
|
|
169
|
+
// Step 1: Read new transcripts (CC + Hermes + Pi + OpenClaw)
|
|
182
170
|
const since = readLastRun();
|
|
183
171
|
console.log(`[hicortex] Reading transcripts since ${since.toISOString()}`);
|
|
184
172
|
const ccBatches = (0, transcript_reader_js_1.readCcTranscripts)(since);
|
|
173
|
+
const hermesBatches = (0, hermes_transcript_reader_js_1.readHermesSessions)(since);
|
|
174
|
+
// Pi is a supported harness in the product (readPiTranscripts no-ops when
|
|
175
|
+
// ~/.pi/agent/sessions is absent). Retired only on specific deployments by
|
|
176
|
+
// simply having no Pi session files — not removed from the pipeline.
|
|
185
177
|
const piBatches = (0, pi_transcript_reader_js_1.readPiTranscripts)(since);
|
|
186
|
-
|
|
178
|
+
// OpenClaw persists sessions in the Pi v3 format at ~/.openclaw/agents/;
|
|
179
|
+
// no-ops when OC isn't installed.
|
|
180
|
+
const ocBatches = (0, oc_transcript_reader_js_1.readOcTranscripts)(since);
|
|
181
|
+
const batches = [...ccBatches, ...hermesBatches, ...piBatches, ...ocBatches];
|
|
187
182
|
if (ccBatches.length > 0)
|
|
188
183
|
console.log(`[hicortex] Found ${ccBatches.length} CC session(s)`);
|
|
184
|
+
if (hermesBatches.length > 0)
|
|
185
|
+
console.log(`[hicortex] Found ${hermesBatches.length} Hermes session(s)`);
|
|
189
186
|
if (piBatches.length > 0)
|
|
190
187
|
console.log(`[hicortex] Found ${piBatches.length} Pi session(s)`);
|
|
188
|
+
if (ocBatches.length > 0)
|
|
189
|
+
console.log(`[hicortex] Found ${ocBatches.length} OpenClaw session(s)`);
|
|
191
190
|
console.log(`[hicortex] Total: ${batches.length} new session(s)`);
|
|
192
191
|
if (batches.length === 0 && !dryRun) {
|
|
193
|
-
// Still run consolidation — there may be unscored memories from OC
|
|
194
|
-
console.log(
|
|
192
|
+
// Still run consolidation (unless capture-only) — there may be unscored memories from OC.
|
|
193
|
+
console.log(captureOnly
|
|
194
|
+
? `[hicortex] No new transcripts. Nothing to capture.`
|
|
195
|
+
: `[hicortex] No new transcripts. Running consolidation only.`);
|
|
195
196
|
}
|
|
196
|
-
// Step 2:
|
|
197
|
+
// Step 2: Denoise and POST each session to the local daemon via /distill.
|
|
198
|
+
// The dedup check and distillation quality (35B) are the server's concern.
|
|
197
199
|
let memoriesIngested = 0;
|
|
198
200
|
let hadTransientFailure = false;
|
|
199
|
-
// Pre-flight health check for a remote distill endpoint.
|
|
200
|
-
// If the distill provider is Ollama on a remote host and that host (or the
|
|
201
|
-
// required model) is unreachable, abort BEFORE touching any sessions —
|
|
202
|
-
// prevents the data-loss bug where lastRun advances past sessions that
|
|
203
|
-
// were never actually processed.
|
|
204
|
-
if (batches.length > 0 && llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
|
|
205
|
-
const distillModel = llmConfig.distillModel ?? llmConfig.model;
|
|
206
|
-
const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
|
|
207
|
-
if (!health.ok) {
|
|
208
|
-
const reason = health.reason === "unreachable"
|
|
209
|
-
? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
|
|
210
|
-
: `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
|
|
211
|
-
console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
|
|
212
|
-
hadTransientFailure = true;
|
|
213
|
-
batches.length = 0; // Skip the distillation loop entirely
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Detect safe chunk size based on model context window
|
|
217
|
-
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
218
201
|
for (const batch of batches) {
|
|
219
202
|
const transcript = (0, distiller_js_1.extractConversationText)(batch.entries);
|
|
220
203
|
if (transcript.length < 200) {
|
|
221
204
|
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
|
|
222
205
|
continue;
|
|
223
206
|
}
|
|
224
|
-
|
|
225
|
-
// Client mode gets this for free via the server's /ingest endpoint;
|
|
226
|
-
// server mode writes directly via storage.insertMemory and needs
|
|
227
|
-
// an explicit check. This makes retries of previously-failed runs
|
|
228
|
-
// idempotent.
|
|
229
|
-
if (!dryRun) {
|
|
230
|
-
const existing = db
|
|
231
|
-
.prepare("SELECT COUNT(*) as c FROM memories WHERE source_session = ?")
|
|
232
|
-
.get(batch.sessionId);
|
|
233
|
-
if (existing.c > 0) {
|
|
234
|
-
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
console.log(`[hicortex] Distilling ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
|
|
207
|
+
console.log(`[hicortex] Capturing ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
|
|
239
208
|
if (dryRun) {
|
|
240
|
-
console.log(`[hicortex] [dry-run] Would
|
|
209
|
+
console.log(`[hicortex] [dry-run] Would POST ${transcript.length} chars to /distill`);
|
|
241
210
|
continue;
|
|
242
211
|
}
|
|
243
|
-
// Check cap before distilling
|
|
244
|
-
if ((0, features_js_1.memoryCapReached)(storage.countMemories(db))) {
|
|
245
|
-
console.log(`[hicortex] Free tier limit (${(0, features_js_1.maxMemoriesAllowed)()} memories). Skipping new ingestion. ` +
|
|
246
|
-
`Upgrade: https://hicortex.gamaze.com/`);
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
212
|
try {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
213
|
+
const resp = await fetch(`http://127.0.0.1:${port}/distill`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({
|
|
217
|
+
text: transcript,
|
|
218
|
+
source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
|
|
219
|
+
project: batch.projectName,
|
|
220
|
+
session_id: batch.sessionId,
|
|
221
|
+
session_date: batch.date,
|
|
222
|
+
privacy: "WORK",
|
|
223
|
+
}),
|
|
224
|
+
// Synchronous 35B distillation of a large session can take minutes.
|
|
225
|
+
signal: AbortSignal.timeout(20 * 60 * 1000),
|
|
226
|
+
});
|
|
227
|
+
if (resp.status === 200) {
|
|
228
|
+
const data = await resp.json();
|
|
229
|
+
if (data.skipped) {
|
|
230
|
+
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): already ingested`);
|
|
266
231
|
}
|
|
267
232
|
}
|
|
268
|
-
|
|
233
|
+
else if (resp.status === 201) {
|
|
234
|
+
const data = await resp.json();
|
|
235
|
+
memoriesIngested += data.distilled ?? 0;
|
|
236
|
+
console.log(`[hicortex] → ${data.distilled ?? 0} memories extracted`);
|
|
237
|
+
}
|
|
238
|
+
else if (resp.status === 429) {
|
|
239
|
+
const data = await resp.json();
|
|
240
|
+
console.log(`[hicortex] Memory limit reached: ${data.error}. Stopping capture.`);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const data = await resp.json().catch(() => ({}));
|
|
245
|
+
console.error(`[hicortex] /distill returned ${resp.status}: ${data.error ?? "unknown error"} — will retry next run`);
|
|
246
|
+
hadTransientFailure = true;
|
|
247
|
+
}
|
|
269
248
|
}
|
|
270
249
|
catch (err) {
|
|
271
250
|
const msg = err instanceof Error ? err.message : String(err);
|
|
272
|
-
console.error(`[hicortex]
|
|
251
|
+
console.error(`[hicortex] Capture failed: ${msg} — will retry next run`);
|
|
273
252
|
hadTransientFailure = true;
|
|
274
253
|
}
|
|
275
254
|
}
|
|
276
|
-
console.log(`[hicortex]
|
|
277
|
-
// Step 3: Consolidation
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
255
|
+
console.log(`[hicortex] Capture complete: ${memoriesIngested} new memories`);
|
|
256
|
+
// Step 3: Consolidation — skipped in capture-only mode, dry-run, or no LLM.
|
|
257
|
+
// Runs even if capture had transient failures (opens DB directly, independent
|
|
258
|
+
// of the HTTP capture path). Full nightly only — capture-only runs are
|
|
259
|
+
// intended to run more frequently than once daily.
|
|
260
|
+
if (!dryRun && !captureOnly) {
|
|
261
|
+
if (!llm || !llmConfig) {
|
|
262
|
+
console.error("[hicortex] consolidation skipped: no LLM configured — run npx @gamaze/hicortex init");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// Pre-flight health check for the reflect endpoint.
|
|
266
|
+
// If reflectBaseUrl points to a remote Ollama and it's down (MBP offline),
|
|
267
|
+
// skip reflection entirely instead of waiting through 3 retries (~3.5 min).
|
|
268
|
+
// Scoring + linking + decay still run.
|
|
269
|
+
let skipReflection = false;
|
|
270
|
+
if (llmConfig.reflectBaseUrl && (llmConfig.reflectProvider ?? llmConfig.provider) === "ollama") {
|
|
271
|
+
const reflectModel = llmConfig.reflectModel ?? llmConfig.model;
|
|
272
|
+
const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.reflectBaseUrl, reflectModel);
|
|
273
|
+
if (!health.ok) {
|
|
274
|
+
const reason = health.reason === "unreachable"
|
|
275
|
+
? `reflect endpoint unreachable (${llmConfig.reflectBaseUrl})`
|
|
276
|
+
: `reflect model not loaded (${reflectModel} missing on ${llmConfig.reflectBaseUrl})`;
|
|
277
|
+
console.warn(`[hicortex] ${reason} — skipping reflection, scoring + linking will still run`);
|
|
278
|
+
skipReflection = true;
|
|
279
|
+
}
|
|
293
280
|
}
|
|
281
|
+
console.log(`[hicortex] Running consolidation...`);
|
|
282
|
+
const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, skipReflection);
|
|
283
|
+
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
284
|
+
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
294
285
|
}
|
|
295
|
-
console.log(`[hicortex] Running consolidation...`);
|
|
296
|
-
const report = await (0, consolidate_js_1.runConsolidation)(db, llm, embedder_js_1.embed, dryRun, skipReflection);
|
|
297
|
-
console.log(`[hicortex] Consolidation ${report.status} in ${report.elapsed_seconds}s` +
|
|
298
|
-
(report.stages.reflection ? ` (${report.stages.reflection.lessons_generated} lessons)` : ""));
|
|
299
|
-
}
|
|
300
|
-
// Step 4: Inject lessons into the target file (CLAUDE.md or EXPERIENCE.md
|
|
301
|
-
// or custom path — configurable via lessonTarget in config.json)
|
|
302
|
-
if (!dryRun) {
|
|
303
|
-
const lessonTarget = savedConfig?.lessonTarget;
|
|
304
|
-
const injection = await (0, claude_md_js_1.injectLessons)(db, {
|
|
305
|
-
claudeMdPath: lessonTarget,
|
|
306
|
-
stateDir,
|
|
307
|
-
});
|
|
308
|
-
console.log(`[hicortex] Lessons updated: ${injection.lessonsCount} lessons at ${injection.path}`);
|
|
309
286
|
}
|
|
310
|
-
// Step
|
|
287
|
+
// Step 4: Update last-run timestamp.
|
|
311
288
|
// CRITICAL: only advance lastRun if every session was processed without
|
|
312
289
|
// a transient failure. Otherwise failed sessions would be permanently
|
|
313
290
|
// lost — they'd be older than the new lastRun and never retried.
|
|
@@ -321,11 +298,16 @@ async function runNightly(options = {}) {
|
|
|
321
298
|
}
|
|
322
299
|
}
|
|
323
300
|
console.log(`[hicortex] Nightly pipeline complete.`);
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
301
|
+
// Anonymous telemetry (fire-and-forget, full nightly only).
|
|
302
|
+
// Capture-only runs are excluded to avoid inflating install pings.
|
|
303
|
+
if (!dryRun && !captureOnly && (0, telemetry_js_1.isTelemetryEnabled)(savedConfig)) {
|
|
304
|
+
const kinds = [
|
|
305
|
+
ccBatches.length > 0 && "cc",
|
|
306
|
+
hermesBatches.length > 0 && "hermes",
|
|
307
|
+
piBatches.length > 0 && "pi",
|
|
308
|
+
ocBatches.length > 0 && "oc",
|
|
309
|
+
].filter(Boolean);
|
|
310
|
+
const agentType = kinds.length > 1 ? "mixed" : (kinds[0] ?? "cc");
|
|
329
311
|
await (0, telemetry_js_1.sendTelemetry)({
|
|
330
312
|
id: (0, telemetry_js_1.getTelemetryId)(stateDir),
|
|
331
313
|
v: VERSION,
|
|
@@ -343,7 +325,7 @@ async function runNightly(options = {}) {
|
|
|
343
325
|
}
|
|
344
326
|
}
|
|
345
327
|
// ---------------------------------------------------------------------------
|
|
346
|
-
// Client Mode Nightly —
|
|
328
|
+
// Client Mode Nightly — denoise locally, POST to remote server's /distill
|
|
347
329
|
// ---------------------------------------------------------------------------
|
|
348
330
|
async function runClientNightly(config, dryRun) {
|
|
349
331
|
const serverUrl = config.serverUrl.replace(/\/+$/, "");
|
|
@@ -363,81 +345,33 @@ async function runClientNightly(config, dryRun) {
|
|
|
363
345
|
console.error(`[hicortex] Aborting. Will retry next run.`);
|
|
364
346
|
return; // Don't update last-run so we retry
|
|
365
347
|
}
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (claudePath) {
|
|
371
|
-
llmConfig = (0, llm_js_1.claudeCliConfig)(claudePath);
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
llmConfig = (0, llm_js_1.resolveLlmConfigForCC)();
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
else if (config.llmBackend === "ollama") {
|
|
378
|
-
llmConfig = {
|
|
379
|
-
baseUrl: config.llmBaseUrl ?? "http://localhost:11434",
|
|
380
|
-
apiKey: "",
|
|
381
|
-
model: config.llmModel ?? "qwen3.5:4b",
|
|
382
|
-
reflectModel: config.reflectModel ?? config.llmModel ?? "qwen3.5:4b",
|
|
383
|
-
provider: "ollama",
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
llmConfig = (0, llm_js_1.resolveLlmConfigForCC)({
|
|
388
|
-
llmBaseUrl: config.llmBaseUrl,
|
|
389
|
-
llmApiKey: config.llmApiKey,
|
|
390
|
-
llmModel: config.llmModel,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (config.distillModel) {
|
|
394
|
-
llmConfig.distillModel = config.distillModel;
|
|
395
|
-
}
|
|
396
|
-
if (config.distillBaseUrl) {
|
|
397
|
-
llmConfig.distillBaseUrl = config.distillBaseUrl;
|
|
398
|
-
llmConfig.distillApiKey = config.distillApiKey ?? llmConfig.apiKey;
|
|
399
|
-
llmConfig.distillProvider = config.distillProvider ?? llmConfig.provider;
|
|
400
|
-
}
|
|
401
|
-
// Auto-detect Ollama for batch distillation when claude-cli was resolved (fallback)
|
|
402
|
-
llmConfig = await (0, llm_js_1.preferOllamaForBatch)(llmConfig);
|
|
403
|
-
if (llmConfig.provider === "ollama" && !config.distillBaseUrl) {
|
|
404
|
-
console.log(`[hicortex] Auto-detected local Ollama (${llmConfig.model}) — using for batch distillation`);
|
|
405
|
-
}
|
|
406
|
-
const llm = new llm_js_1.LlmClient(llmConfig);
|
|
407
|
-
const distillInfo = llmConfig.distillBaseUrl
|
|
408
|
-
? `${llmConfig.distillProvider}/${llmConfig.distillModel}@${llmConfig.distillBaseUrl}`
|
|
409
|
-
: llmConfig.distillModel ?? "";
|
|
410
|
-
console.log(`[hicortex] LLM: ${llmConfig.provider}/${llmConfig.model}${distillInfo ? `, distill: ${distillInfo}` : ""}`);
|
|
411
|
-
// Detect safe chunk size based on model context window
|
|
412
|
-
const chunkSize = await (0, distiller_js_1.detectChunkSize)(llmConfig.provider, llmConfig.distillModel ?? llmConfig.model, llmConfig.baseUrl);
|
|
413
|
-
// Read new CC transcripts
|
|
348
|
+
// No local LLM needed — distillation happens on the server.
|
|
349
|
+
// Read new transcripts (CC + Hermes + Pi + OpenClaw). Client reads local
|
|
350
|
+
// logs, denoises, and POSTs the denoised text to the server's /distill
|
|
351
|
+
// endpoint. All readers no-op when their harness isn't installed.
|
|
414
352
|
const since = readLastRun();
|
|
415
|
-
console.log(`[hicortex] Reading
|
|
416
|
-
const
|
|
417
|
-
|
|
353
|
+
console.log(`[hicortex] Reading transcripts since ${since.toISOString()}`);
|
|
354
|
+
const ccBatches = (0, transcript_reader_js_1.readCcTranscripts)(since);
|
|
355
|
+
const hermesBatches = (0, hermes_transcript_reader_js_1.readHermesSessions)(since);
|
|
356
|
+
const piBatches = (0, pi_transcript_reader_js_1.readPiTranscripts)(since);
|
|
357
|
+
const ocBatches = (0, oc_transcript_reader_js_1.readOcTranscripts)(since);
|
|
358
|
+
const batches = [...ccBatches, ...hermesBatches, ...piBatches, ...ocBatches];
|
|
359
|
+
if (ccBatches.length > 0)
|
|
360
|
+
console.log(`[hicortex] Found ${ccBatches.length} CC session(s)`);
|
|
361
|
+
if (hermesBatches.length > 0)
|
|
362
|
+
console.log(`[hicortex] Found ${hermesBatches.length} Hermes session(s)`);
|
|
363
|
+
if (piBatches.length > 0)
|
|
364
|
+
console.log(`[hicortex] Found ${piBatches.length} Pi session(s)`);
|
|
365
|
+
if (ocBatches.length > 0)
|
|
366
|
+
console.log(`[hicortex] Found ${ocBatches.length} OpenClaw session(s)`);
|
|
367
|
+
console.log(`[hicortex] Total: ${batches.length} new session(s)`);
|
|
418
368
|
if (batches.length === 0) {
|
|
419
|
-
console.log(`[hicortex] Nothing to
|
|
369
|
+
console.log(`[hicortex] Nothing to capture.`);
|
|
420
370
|
if (!dryRun)
|
|
421
371
|
writeLastRun();
|
|
422
372
|
return;
|
|
423
373
|
}
|
|
424
|
-
// Pre-flight health check for a remote distill endpoint (client mode).
|
|
425
|
-
// If the distill provider is Ollama on a remote host and the required model
|
|
426
|
-
// isn't loaded, abort BEFORE touching any sessions — same data-loss fix
|
|
427
|
-
// as server mode.
|
|
428
374
|
let hadTransientFailure = false;
|
|
429
|
-
if (llmConfig.distillBaseUrl && (llmConfig.distillProvider ?? llmConfig.provider) === "ollama") {
|
|
430
|
-
const distillModel = llmConfig.distillModel ?? llmConfig.model;
|
|
431
|
-
const health = await (0, llm_js_1.probeOllamaModel)(llmConfig.distillBaseUrl, distillModel);
|
|
432
|
-
if (!health.ok) {
|
|
433
|
-
const reason = health.reason === "unreachable"
|
|
434
|
-
? `distill endpoint unreachable (${llmConfig.distillBaseUrl})`
|
|
435
|
-
: `distill model not loaded (${distillModel} missing on ${llmConfig.distillBaseUrl})`;
|
|
436
|
-
console.error(`[hicortex] ABORT: ${reason} — will retry next run, lastRun unchanged`);
|
|
437
|
-
return; // Don't touch lastRun; next trigger retries the same sessions
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// Distill each session and POST to server
|
|
441
375
|
let memoriesIngested = 0;
|
|
442
376
|
let sessionsSent = 0;
|
|
443
377
|
for (const batch of batches) {
|
|
@@ -446,78 +380,62 @@ async function runClientNightly(config, dryRun) {
|
|
|
446
380
|
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)} (${batch.projectName}): too short`);
|
|
447
381
|
continue;
|
|
448
382
|
}
|
|
449
|
-
console.log(`[hicortex]
|
|
383
|
+
console.log(`[hicortex] Capturing ${batch.sessionId.slice(0, 8)} (${batch.projectName}, ${batch.date})`);
|
|
450
384
|
if (dryRun) {
|
|
451
|
-
console.log(`[hicortex] [dry-run] Would
|
|
385
|
+
console.log(`[hicortex] [dry-run] Would POST ${transcript.length} chars to ${serverUrl}/distill`);
|
|
452
386
|
continue;
|
|
453
387
|
}
|
|
454
388
|
try {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
session_date: batch.date,
|
|
477
|
-
}),
|
|
478
|
-
signal: AbortSignal.timeout(30_000),
|
|
479
|
-
});
|
|
480
|
-
const result = await resp.json();
|
|
481
|
-
if (resp.status === 201) {
|
|
482
|
-
sessionCount++;
|
|
483
|
-
memoriesIngested++;
|
|
484
|
-
}
|
|
485
|
-
else if (result.skipped) {
|
|
486
|
-
console.log(`[hicortex] → Already ingested (${result.existing_count} existing)`);
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
else if (resp.status === 401) {
|
|
490
|
-
console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
else if (resp.status === 429) {
|
|
494
|
-
console.log(`[hicortex] Server memory limit reached.`);
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
console.error(`[hicortex] Ingest failed (${resp.status}): ${result.error}`);
|
|
499
|
-
hadTransientFailure = true;
|
|
389
|
+
const resp = await fetch(`${serverUrl}/distill`, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: {
|
|
392
|
+
"Content-Type": "application/json",
|
|
393
|
+
...(authToken ? { "Authorization": `Bearer ${authToken}` } : {}),
|
|
394
|
+
},
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
text: transcript,
|
|
397
|
+
source_agent: batch.sourceAgent ?? `claude-code/${batch.projectName}`,
|
|
398
|
+
project: batch.projectName,
|
|
399
|
+
session_id: batch.sessionId,
|
|
400
|
+
session_date: batch.date,
|
|
401
|
+
privacy: "WORK",
|
|
402
|
+
}),
|
|
403
|
+
// Synchronous 35B distillation of a large session can take minutes.
|
|
404
|
+
signal: AbortSignal.timeout(20 * 60 * 1000),
|
|
405
|
+
});
|
|
406
|
+
if (resp.status === 200) {
|
|
407
|
+
const data = await resp.json();
|
|
408
|
+
if (data.skipped) {
|
|
409
|
+
console.log(`[hicortex] Skip ${batch.sessionId.slice(0, 8)}: already ingested on server`);
|
|
500
410
|
}
|
|
501
411
|
}
|
|
502
|
-
if (
|
|
412
|
+
else if (resp.status === 201) {
|
|
413
|
+
const data = await resp.json();
|
|
414
|
+
const count = data.distilled ?? 0;
|
|
415
|
+
memoriesIngested += count;
|
|
503
416
|
sessionsSent++;
|
|
504
|
-
console.log(`[hicortex] → ${
|
|
417
|
+
console.log(`[hicortex] → ${count} memories sent to server`);
|
|
418
|
+
}
|
|
419
|
+
else if (resp.status === 401) {
|
|
420
|
+
console.error(`[hicortex] Auth failed. Check authToken in ~/.hicortex/config.json`);
|
|
421
|
+
return; // No point retrying with wrong credentials
|
|
422
|
+
}
|
|
423
|
+
else if (resp.status === 429) {
|
|
424
|
+
const data = await resp.json().catch(() => ({}));
|
|
425
|
+
console.log(`[hicortex] Server memory limit reached: ${data.error}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const data = await resp.json().catch(() => ({}));
|
|
430
|
+
console.error(`[hicortex] /distill returned ${resp.status}: ${data.error ?? "unknown error"} — will retry next run`);
|
|
431
|
+
hadTransientFailure = true;
|
|
505
432
|
}
|
|
506
433
|
}
|
|
507
434
|
catch (err) {
|
|
508
|
-
console.error(`[hicortex]
|
|
435
|
+
console.error(`[hicortex] Capture failed: ${err instanceof Error ? err.message : String(err)} — will retry next run`);
|
|
509
436
|
hadTransientFailure = true;
|
|
510
437
|
}
|
|
511
438
|
}
|
|
512
|
-
// Inject lessons from server into CLAUDE.md
|
|
513
|
-
if (!dryRun) {
|
|
514
|
-
try {
|
|
515
|
-
await injectLessonsFromServer(serverUrl, authToken);
|
|
516
|
-
}
|
|
517
|
-
catch (err) {
|
|
518
|
-
console.error(`[hicortex] CLAUDE.md injection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
439
|
// Only advance lastRun if every session was processed without a transient
|
|
522
440
|
// failure. Otherwise failed sessions would be permanently lost.
|
|
523
441
|
if (!dryRun) {
|
|
@@ -532,100 +450,22 @@ async function runClientNightly(config, dryRun) {
|
|
|
532
450
|
console.log(`[hicortex] Client nightly complete: ${memoriesIngested} memories from ${sessionsSent} sessions → ${serverUrl}`);
|
|
533
451
|
// Anonymous telemetry (fire-and-forget, opt-out via config)
|
|
534
452
|
if (!dryRun && (0, telemetry_js_1.isTelemetryEnabled)(config)) {
|
|
453
|
+
const kinds = [
|
|
454
|
+
ccBatches.length > 0 && "cc",
|
|
455
|
+
hermesBatches.length > 0 && "hermes",
|
|
456
|
+
piBatches.length > 0 && "pi",
|
|
457
|
+
ocBatches.length > 0 && "oc",
|
|
458
|
+
].filter(Boolean);
|
|
459
|
+
const agentType = kinds.length > 1 ? "mixed" : (kinds[0] ?? "cc");
|
|
535
460
|
await (0, telemetry_js_1.sendTelemetry)({
|
|
536
461
|
id: (0, telemetry_js_1.getTelemetryId)(HICORTEX_HOME),
|
|
537
462
|
v: VERSION,
|
|
538
463
|
mode: "client",
|
|
539
|
-
agent:
|
|
464
|
+
agent: agentType,
|
|
540
465
|
mem: memoriesIngested,
|
|
541
|
-
lessons: 0, // client doesn't
|
|
466
|
+
lessons: 0, // client doesn't have direct DB access
|
|
542
467
|
sessions: batches.length,
|
|
543
468
|
ok: !hadTransientFailure,
|
|
544
469
|
});
|
|
545
470
|
}
|
|
546
471
|
}
|
|
547
|
-
/**
|
|
548
|
-
* Fetch lessons + memory index from server and inject into CLAUDE.md.
|
|
549
|
-
* Client mode equivalent of the server's injectLessons(db, ...).
|
|
550
|
-
*/
|
|
551
|
-
async function injectLessonsFromServer(serverUrl, authToken) {
|
|
552
|
-
const resp = await fetch(`${serverUrl}/lessons`, {
|
|
553
|
-
headers: authToken ? { "Authorization": `Bearer ${authToken}` } : {},
|
|
554
|
-
signal: AbortSignal.timeout(10_000),
|
|
555
|
-
});
|
|
556
|
-
if (!resp.ok) {
|
|
557
|
-
console.log(`[hicortex] Could not fetch lessons from server (${resp.status})`);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
const data = await resp.json();
|
|
561
|
-
const maxLessons = (0, features_js_1.lessonsLimit)();
|
|
562
|
-
// Use moduleIndex from server response, fall back to local state
|
|
563
|
-
const moduleIndex = data.moduleIndex ?? (0, state_js_1.loadState)().moduleIndex;
|
|
564
|
-
const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons, moduleIndex });
|
|
565
|
-
// Format lessons
|
|
566
|
-
const lessonLines = selected.map((l) => {
|
|
567
|
-
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
568
|
-
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
569
|
-
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
570
|
-
const title = titleMatch ? titleMatch[1] : l.content.slice(0, 150);
|
|
571
|
-
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
572
|
-
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
573
|
-
});
|
|
574
|
-
// Format module/project index
|
|
575
|
-
let indexLines;
|
|
576
|
-
if (moduleIndex && moduleIndex.domains.length > 0) {
|
|
577
|
-
indexLines = [];
|
|
578
|
-
for (const domain of moduleIndex.domains) {
|
|
579
|
-
const kwStr = domain.keywords.length > 0 ? `: ${domain.keywords.join(", ")}` : "";
|
|
580
|
-
indexLines.push(`${domain.name} (${domain.memoryCount} memories, ${domain.lessonCount} lessons)${kwStr}`);
|
|
581
|
-
indexLines.push(` ${domain.projects.join(" | ")}`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
indexLines = [data.index.projects.map(p => `${p.name}: ${p.count}`).join(" | ")];
|
|
586
|
-
}
|
|
587
|
-
// Build block
|
|
588
|
-
const START_MARKER = "<!-- HICORTEX-LEARNINGS:START -->";
|
|
589
|
-
const END_MARKER = "<!-- HICORTEX-LEARNINGS:END -->";
|
|
590
|
-
const blockParts = [START_MARKER, "## Hicortex Memory"];
|
|
591
|
-
blockParts.push("", "You have access to shared long-term memory across all agents and sessions.", "BEFORE making decisions, search memory: `hicortex_search` for prior decisions on the same topic.", "Use `hicortex_context` at session start for recent project state.");
|
|
592
|
-
if (lessonLines.length > 0) {
|
|
593
|
-
blockParts.push("", "### Lessons (updated nightly)");
|
|
594
|
-
blockParts.push(...lessonLines);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
blockParts.push("", "### Getting Started");
|
|
598
|
-
blockParts.push("- Search past decisions with `hicortex_search` before starting work");
|
|
599
|
-
blockParts.push("- Save important decisions with `hicortex_ingest`");
|
|
600
|
-
blockParts.push("- Lessons will appear here after the first nightly run");
|
|
601
|
-
}
|
|
602
|
-
if (indexLines.length > 0) {
|
|
603
|
-
blockParts.push("", "### Memory Index");
|
|
604
|
-
blockParts.push(...indexLines);
|
|
605
|
-
blockParts.push(`${data.index.total} memories, ${data.index.lessonCount} lessons, ${data.index.sourceCount} agents. Search with \`hicortex_search\`.`);
|
|
606
|
-
}
|
|
607
|
-
blockParts.push(END_MARKER);
|
|
608
|
-
const block = blockParts.join("\n");
|
|
609
|
-
// Write to CLAUDE.md (uses fs/path/os already imported at top of file)
|
|
610
|
-
const claudeMdPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", "CLAUDE.md");
|
|
611
|
-
let content = "";
|
|
612
|
-
try {
|
|
613
|
-
content = (0, node_fs_1.readFileSync)(claudeMdPath, "utf-8");
|
|
614
|
-
}
|
|
615
|
-
catch { }
|
|
616
|
-
const startIdx = content.indexOf(START_MARKER);
|
|
617
|
-
const endIdx = content.indexOf(END_MARKER);
|
|
618
|
-
if (startIdx !== -1 && endIdx !== -1) {
|
|
619
|
-
content = content.slice(0, startIdx) + block + content.slice(endIdx + END_MARKER.length);
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
if (content.length > 0 && !content.endsWith("\n"))
|
|
623
|
-
content += "\n";
|
|
624
|
-
if (content.length > 0)
|
|
625
|
-
content += "\n";
|
|
626
|
-
content += block + "\n";
|
|
627
|
-
}
|
|
628
|
-
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(claudeMdPath), { recursive: true });
|
|
629
|
-
(0, node_fs_1.writeFileSync)(claudeMdPath, content);
|
|
630
|
-
console.log(`[hicortex] CLAUDE.md updated: ${lessonLines.length} lessons, ${data.index.total} memories indexed`);
|
|
631
|
-
}
|