velixar-mcp-server 0.2.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -64
- package/dist/api.d.ts +64 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +405 -0
- package/dist/api.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +52 -0
- package/dist/errors.js.map +1 -0
- package/dist/justify.d.ts +9 -0
- package/dist/justify.d.ts.map +1 -0
- package/dist/justify.js +114 -0
- package/dist/justify.js.map +1 -0
- package/dist/prompts.d.ts +88 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +693 -0
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +25 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +278 -0
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +392 -0
- package/dist/server.js.map +1 -0
- package/dist/simulation/personas.d.ts +41 -0
- package/dist/simulation/personas.d.ts.map +1 -0
- package/dist/simulation/personas.js +173 -0
- package/dist/simulation/personas.js.map +1 -0
- package/dist/simulation/report.d.ts +28 -0
- package/dist/simulation/report.d.ts.map +1 -0
- package/dist/simulation/report.js +105 -0
- package/dist/simulation/report.js.map +1 -0
- package/dist/simulation/runner.d.ts +63 -0
- package/dist/simulation/runner.d.ts.map +1 -0
- package/dist/simulation/runner.js +167 -0
- package/dist/simulation/runner.js.map +1 -0
- package/dist/temporal_merge.d.ts +70 -0
- package/dist/temporal_merge.d.ts.map +1 -0
- package/dist/temporal_merge.js +258 -0
- package/dist/temporal_merge.js.map +1 -0
- package/dist/tools/clairvoyance.d.ts +9 -0
- package/dist/tools/clairvoyance.d.ts.map +1 -0
- package/dist/tools/clairvoyance.js +321 -0
- package/dist/tools/clairvoyance.js.map +1 -0
- package/dist/tools/cognitive.d.ts +10 -0
- package/dist/tools/cognitive.d.ts.map +1 -0
- package/dist/tools/cognitive.js +462 -0
- package/dist/tools/cognitive.js.map +1 -0
- package/dist/tools/construction.d.ts +9 -0
- package/dist/tools/construction.d.ts.map +1 -0
- package/dist/tools/construction.js +350 -0
- package/dist/tools/construction.js.map +1 -0
- package/dist/tools/graph.d.ts +14 -0
- package/dist/tools/graph.d.ts.map +1 -0
- package/dist/tools/graph.js +263 -0
- package/dist/tools/graph.js.map +1 -0
- package/dist/tools/graph_sanitizer.d.ts +24 -0
- package/dist/tools/graph_sanitizer.d.ts.map +1 -0
- package/dist/tools/graph_sanitizer.js +79 -0
- package/dist/tools/graph_sanitizer.js.map +1 -0
- package/dist/tools/lifecycle.d.ts +9 -0
- package/dist/tools/lifecycle.d.ts.map +1 -0
- package/dist/tools/lifecycle.js +1091 -0
- package/dist/tools/lifecycle.js.map +1 -0
- package/dist/tools/livedata.d.ts +9 -0
- package/dist/tools/livedata.d.ts.map +1 -0
- package/dist/tools/livedata.js +222 -0
- package/dist/tools/livedata.js.map +1 -0
- package/dist/tools/memory.d.ts +10 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +275 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/recall.d.ts +9 -0
- package/dist/tools/recall.d.ts.map +1 -0
- package/dist/tools/recall.js +206 -0
- package/dist/tools/recall.js.map +1 -0
- package/dist/tools/retrieval.d.ts +9 -0
- package/dist/tools/retrieval.d.ts.map +1 -0
- package/dist/tools/retrieval.js +338 -0
- package/dist/tools/retrieval.js.map +1 -0
- package/dist/tools/system.d.ts +12 -0
- package/dist/tools/system.d.ts.map +1 -0
- package/dist/tools/system.js +228 -0
- package/dist/tools/system.js.map +1 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +79 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +174 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate_retrieval.d.ts +33 -0
- package/dist/validate_retrieval.d.ts.map +1 -0
- package/dist/validate_retrieval.js +97 -0
- package/dist/validate_retrieval.js.map +1 -0
- package/package.json +22 -6
- package/server.json +2 -2
- package/.github/workflows/ci.yml +0 -18
- package/.mcpregistry_github_token +0 -1
- package/.mcpregistry_registry_token +0 -1
- package/src/index.js +0 -262
- package/tests/server.test.js +0 -193
|
@@ -0,0 +1,1091 @@
|
|
|
1
|
+
// ── Lifecycle Tools ──
|
|
2
|
+
// velixar_distill — extract durable memories from session content
|
|
3
|
+
// Phase 3: batch, consolidate, retag, session
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { wrapResponse } from '../api.js';
|
|
6
|
+
import { validateSearchResponse, validateListResponse } from '../validate.js';
|
|
7
|
+
// ── Idempotency Cache ──
|
|
8
|
+
// Tracks content hashes of recently stored memories to prevent duplicates on retry.
|
|
9
|
+
// TTL: 5 minutes. Keyed by workspace_id + content hash to prevent cross-workspace dedup in HTTP mode.
|
|
10
|
+
const idempotencyCache = new Map();
|
|
11
|
+
const IDEMPOTENCY_TTL_MS = 5 * 60 * 1000;
|
|
12
|
+
function contentHash(content) {
|
|
13
|
+
return createHash('sha256').update(content.trim().toLowerCase()).digest('hex').slice(0, 16);
|
|
14
|
+
}
|
|
15
|
+
function idempotencyKey(workspaceId, content) {
|
|
16
|
+
return `${workspaceId}:${contentHash(content)}`;
|
|
17
|
+
}
|
|
18
|
+
function checkIdempotency(workspaceId, content) {
|
|
19
|
+
const key = idempotencyKey(workspaceId, content);
|
|
20
|
+
const entry = idempotencyCache.get(key);
|
|
21
|
+
if (entry && Date.now() - entry.timestamp < IDEMPOTENCY_TTL_MS)
|
|
22
|
+
return entry.id;
|
|
23
|
+
if (entry)
|
|
24
|
+
idempotencyCache.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function recordIdempotency(workspaceId, content, id) {
|
|
28
|
+
idempotencyCache.set(idempotencyKey(workspaceId, content), { id, timestamp: Date.now() });
|
|
29
|
+
}
|
|
30
|
+
// Simple keyword-based auto-tagging when user provides no tags
|
|
31
|
+
const TAG_PATTERNS = [
|
|
32
|
+
[/\b(bug|fix|error|crash|issue)\b/i, 'bugfix'],
|
|
33
|
+
[/\b(decision|decided|chose|chosen)\b/i, 'decision'],
|
|
34
|
+
[/\b(preference|prefer|style|like|dislike)\b/i, 'preference'],
|
|
35
|
+
[/\b(architecture|design|pattern|structure)\b/i, 'architecture'],
|
|
36
|
+
[/\b(deploy|release|ship|launch)\b/i, 'deployment'],
|
|
37
|
+
[/\b(config|setting|env|environment)\b/i, 'config'],
|
|
38
|
+
[/\b(api|endpoint|route|request)\b/i, 'api'],
|
|
39
|
+
[/\b(database|db|sql|query|table)\b/i, 'database'],
|
|
40
|
+
];
|
|
41
|
+
function autoTags(content) {
|
|
42
|
+
const tags = [];
|
|
43
|
+
for (const [re, tag] of TAG_PATTERNS) {
|
|
44
|
+
if (re.test(content))
|
|
45
|
+
tags.push(tag);
|
|
46
|
+
if (tags.length >= 4)
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return tags;
|
|
50
|
+
}
|
|
51
|
+
export const lifecycleTools = [
|
|
52
|
+
{
|
|
53
|
+
name: 'velixar_distill',
|
|
54
|
+
description: 'Extract durable memories from session content. Use at natural memory-worthy breakpoints: task complete, decision made, bug solved, preference clarified. ' +
|
|
55
|
+
'Detects duplicates (skips near-identical content) and flags active contradictions. ' +
|
|
56
|
+
'Use preview: true to see extractions without storing.',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
content: { type: 'string', description: 'Session text to distill into durable memories' },
|
|
61
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags to apply (auto-generated if absent)' },
|
|
62
|
+
source_ids: { type: 'array', items: { type: 'string' }, description: 'Source memory IDs for provenance tracking' },
|
|
63
|
+
preview: { type: 'boolean', description: 'Preview mode: return extracted memories without storing (default: false)' },
|
|
64
|
+
max_memories: { type: 'number', description: 'Maximum number of memories to extract' },
|
|
65
|
+
},
|
|
66
|
+
required: ['content'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'velixar_session_save',
|
|
71
|
+
description: 'Save a session summary for later recall. Auto-generates session_id if not provided. ' +
|
|
72
|
+
'Returns the session_id for later recall.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
summary: { type: 'string', description: 'Session summary to save' },
|
|
77
|
+
session_id: { type: 'string', description: 'Session/conversation ID (auto-generated if omitted)' },
|
|
78
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Additional tags' },
|
|
79
|
+
},
|
|
80
|
+
required: ['summary'],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'velixar_session_recall',
|
|
85
|
+
description: 'Recall memories from a previous session by session ID, date, or topic. ' +
|
|
86
|
+
'Use when resuming work to restore prior context. ' +
|
|
87
|
+
'Supports chunk_id for drill-down into specific time segments (from session_resume manifest). ' +
|
|
88
|
+
'Use order="chronological" for full narrative reconstruction (oldest-first).',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
session_id: { type: 'string', description: 'Session ID to recall' },
|
|
93
|
+
topic: { type: 'string', description: 'Topic to search for in session memories' },
|
|
94
|
+
chunk_id: { type: 'string', description: 'Specific chunk ID for drill-down into a time segment' },
|
|
95
|
+
start_time: { type: 'string', description: 'ISO timestamp — only return memories after this time' },
|
|
96
|
+
end_time: { type: 'string', description: 'ISO timestamp — only return memories before this time' },
|
|
97
|
+
order: { type: 'string', enum: ['recent_first', 'chronological'], description: 'Sort order (default: recent_first)' },
|
|
98
|
+
limit: { type: 'number', description: 'Max results (default 10)' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'velixar_session_resume',
|
|
104
|
+
description: 'Reconstruct full session context in a single call — the recommended way to resume work. ' +
|
|
105
|
+
'Returns narrative summary, key decisions, open threads, and last state.',
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
session_id: { type: 'string', description: 'Session ID to resume (optional — uses most recent if omitted)' },
|
|
110
|
+
topic: { type: 'string', description: 'Topic to focus reconstruction on' },
|
|
111
|
+
intent: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
enum: ['continue_coding', 'write_postmortem', 'catch_up'],
|
|
114
|
+
description: 'What you need the context for — affects which details are preserved (default: continue_coding)',
|
|
115
|
+
},
|
|
116
|
+
focus: { type: 'string', description: 'Specific entity or topic to prioritize in reconstruction' },
|
|
117
|
+
max_tokens: { type: 'number', description: 'Token budget for the response (default 4000)' },
|
|
118
|
+
from_memory_id: { type: 'string', description: 'Resume from a specific point in the session (memory ID)' },
|
|
119
|
+
exclude_topics: { type: 'array', items: { type: 'string' }, description: 'Filter out irrelevant threads during reconstruction' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'velixar_batch_store',
|
|
125
|
+
description: 'Store multiple memories in one call. Returns per-item status. ' +
|
|
126
|
+
'Use for bulk imports or multi-fact storage. Max 20 items per call.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
items: {
|
|
131
|
+
type: 'array',
|
|
132
|
+
items: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
content: { type: 'string' },
|
|
136
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
137
|
+
tier: { type: 'number' },
|
|
138
|
+
},
|
|
139
|
+
required: ['content'],
|
|
140
|
+
},
|
|
141
|
+
description: 'Array of memories to store (max 20)',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
required: ['items'],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'velixar_consolidate',
|
|
149
|
+
description: 'Merge related episodic memories into a single semantic memory. ' +
|
|
150
|
+
'Preserves originals as provenance. Use when multiple episodic memories cover the same topic and should be unified. ' +
|
|
151
|
+
'Provide memory IDs to consolidate, or a topic to auto-find candidates. ' +
|
|
152
|
+
'Set preview=true to see what would be merged without executing.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
memory_ids: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to consolidate' },
|
|
157
|
+
topic: { type: 'string', description: 'Topic to auto-find consolidation candidates' },
|
|
158
|
+
summary: { type: 'string', description: 'Optional: provide the consolidated summary (otherwise auto-generated)' },
|
|
159
|
+
preview: { type: 'boolean', description: 'Preview mode — show what would be merged without executing (default: false)' },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'velixar_retag',
|
|
165
|
+
description: 'Update tags on one or more memories. Use for organizing, correcting, or enriching memory metadata. ' +
|
|
166
|
+
'Supports add, remove, or replace operations.',
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
memory_ids: { type: 'array', items: { type: 'string' }, description: 'Memory IDs to retag' },
|
|
171
|
+
add_tags: { type: 'array', items: { type: 'string' }, description: 'Tags to add' },
|
|
172
|
+
remove_tags: { type: 'array', items: { type: 'string' }, description: 'Tags to remove' },
|
|
173
|
+
replace_tags: { type: 'array', items: { type: 'string' }, description: 'Replace all tags with these' },
|
|
174
|
+
},
|
|
175
|
+
required: ['memory_ids'],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'velixar_export',
|
|
180
|
+
description: 'Export memories as structured data (JSON or Markdown). Supports filtering by tags, tier, date range, and search query.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
format: { type: 'string', enum: ['json', 'markdown'], description: 'Export format (default: json)' },
|
|
185
|
+
query: { type: 'string', description: 'Optional: filter by search query' },
|
|
186
|
+
limit: { type: 'number', description: 'Max memories to export (default 50)' },
|
|
187
|
+
include_graph: { type: 'boolean', description: 'Include graph entities and relationships (default: false)' },
|
|
188
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags (AND logic)' },
|
|
189
|
+
tier: { type: 'number', description: 'Filter by memory tier' },
|
|
190
|
+
before: { type: 'string', description: 'ISO timestamp — only export memories created before this time' },
|
|
191
|
+
after: { type: 'string', description: 'ISO timestamp — only export memories created after this time' },
|
|
192
|
+
all: { type: 'boolean', description: 'Export all memories (overrides limit)' },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'velixar_import',
|
|
198
|
+
description: 'Bulk import memories from structured data (JSON or Markdown). Max 50 items per call. ' +
|
|
199
|
+
'Supports conflict detection: skip duplicates, overwrite, or merge.',
|
|
200
|
+
inputSchema: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {
|
|
203
|
+
format: { type: 'string', enum: ['json', 'markdown'], description: 'Input format (default: json)' },
|
|
204
|
+
data: {
|
|
205
|
+
description: 'For JSON: array of {content, tags?, tier?}. For Markdown: string with --- separators between entries.',
|
|
206
|
+
oneOf: [
|
|
207
|
+
{ type: 'array', items: { type: 'object', properties: { content: { type: 'string' }, tags: { type: 'array', items: { type: 'string' } }, tier: { type: 'number' } }, required: ['content'] } },
|
|
208
|
+
{ type: 'string' },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
default_tags: { type: 'array', items: { type: 'string' }, description: 'Tags to apply to all imported items' },
|
|
212
|
+
source: { type: 'string', description: 'Provenance label (e.g. "notion-export", "obsidian-vault")' },
|
|
213
|
+
quarantine_zone: { type: 'string', description: 'Optional quarantine zone ID for all imported memories' },
|
|
214
|
+
conflict_strategy: { type: 'string', enum: ['skip', 'overwrite', 'merge'], description: 'How to handle duplicates (default: skip)' },
|
|
215
|
+
},
|
|
216
|
+
required: ['data'],
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: 'velixar_upload',
|
|
221
|
+
description: 'Upload a file into Velixar memory with full source provenance. ' +
|
|
222
|
+
'Supports PDF, Markdown, text, CSV, JSON, DOCX, and code files. Parsed, chunked, and tagged with source metadata.',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
file_path: { type: 'string', description: 'Absolute path to the file to upload (e.g. /Users/me/docs/report.pdf)' },
|
|
227
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags to apply to all resulting memories' },
|
|
228
|
+
quarantine_zone: { type: 'string', description: 'Optional quarantine zone ID for all uploaded memories' },
|
|
229
|
+
},
|
|
230
|
+
required: ['file_path'],
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
export async function handleLifecycleTool(name, args, api, config) {
|
|
235
|
+
if (name === 'velixar_distill') {
|
|
236
|
+
const content = args.content;
|
|
237
|
+
const userTags = args.tags || [];
|
|
238
|
+
const sourceIds = args.source_ids || [];
|
|
239
|
+
const preview = args.preview;
|
|
240
|
+
const maxMemories = args.max_memories;
|
|
241
|
+
// Auto-generate tags when absent
|
|
242
|
+
const tags = userTags.length
|
|
243
|
+
? [...userTags, 'distilled']
|
|
244
|
+
: [...autoTags(content), 'distilled'];
|
|
245
|
+
// Build 1.4: max_memories — truncate content if capped
|
|
246
|
+
let distillContent = content;
|
|
247
|
+
if (maxMemories === 1) {
|
|
248
|
+
// Single memory mode — use content as-is
|
|
249
|
+
distillContent = content;
|
|
250
|
+
}
|
|
251
|
+
// Duplicate detection: search for similar content before storing
|
|
252
|
+
// H20: Adaptive threshold — shorter content needs higher similarity to be a true duplicate
|
|
253
|
+
let duplicateDetected = false;
|
|
254
|
+
let contradictionDetected = false;
|
|
255
|
+
const contradictionsFound = [];
|
|
256
|
+
// M28: Exact dedup fast path — check content hash before expensive cosine similarity
|
|
257
|
+
const existingExact = checkIdempotency(config.workspaceId, distillContent);
|
|
258
|
+
if (existingExact) {
|
|
259
|
+
duplicateDetected = true;
|
|
260
|
+
}
|
|
261
|
+
if (!duplicateDetected) {
|
|
262
|
+
try {
|
|
263
|
+
const searchParams = new URLSearchParams({
|
|
264
|
+
q: content.slice(0, 200),
|
|
265
|
+
user_id: config.userId,
|
|
266
|
+
limit: '3',
|
|
267
|
+
});
|
|
268
|
+
const existing = await api.get(`/memory/search?${searchParams}`, true);
|
|
269
|
+
const validated = validateSearchResponse(existing, '/memory/search');
|
|
270
|
+
const topMatch = validated.memories[0];
|
|
271
|
+
if (topMatch && typeof topMatch.score === 'number') {
|
|
272
|
+
const score = topMatch.score;
|
|
273
|
+
// Shorter content → higher threshold (short strings match too easily)
|
|
274
|
+
const threshold = content.length < 100 ? 0.96 : content.length < 300 ? 0.94 : 0.92;
|
|
275
|
+
if (score > threshold)
|
|
276
|
+
duplicateDetected = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* non-blocking */ }
|
|
280
|
+
}
|
|
281
|
+
// Contradiction detection: check if content conflicts with existing beliefs
|
|
282
|
+
try {
|
|
283
|
+
const contradictions = await api.get('/exocortex/contradictions?status=open', true);
|
|
284
|
+
const cObj = (contradictions && typeof contradictions === 'object') ? contradictions : {};
|
|
285
|
+
const cArr = Array.isArray(cObj.contradictions) ? cObj.contradictions : [];
|
|
286
|
+
if (cArr.length) {
|
|
287
|
+
contradictionDetected = true;
|
|
288
|
+
for (const c of cArr.slice(0, 3)) {
|
|
289
|
+
const entry = c;
|
|
290
|
+
contradictionsFound.push(String(entry.explanation || entry.description || 'Conflict detected'));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch { /* non-blocking */ }
|
|
295
|
+
// Build 1.4: Preview mode — return extraction without storing
|
|
296
|
+
if (preview) {
|
|
297
|
+
const words = distillContent.split(/\s+/).length;
|
|
298
|
+
const hasSpecifics = /\b(decided|chose|because|prefer|always|never|bug|fix|error|version|v\d)\b/i.test(distillContent);
|
|
299
|
+
const qualityIssues = [];
|
|
300
|
+
if (words < 5)
|
|
301
|
+
qualityIssues.push('too_short');
|
|
302
|
+
if (words > 500)
|
|
303
|
+
qualityIssues.push('too_long_for_single_memory');
|
|
304
|
+
if (!hasSpecifics)
|
|
305
|
+
qualityIssues.push('low_specificity');
|
|
306
|
+
return {
|
|
307
|
+
text: JSON.stringify(wrapResponse({
|
|
308
|
+
preview: true,
|
|
309
|
+
candidates: [{
|
|
310
|
+
content: distillContent, rationale: 'Would be distilled from session content', tags,
|
|
311
|
+
confidence: 0.8, memory_type: 'semantic', source_type: 'distill',
|
|
312
|
+
duplicate_detected: duplicateDetected, contradiction_detected: contradictionDetected,
|
|
313
|
+
derived_from: sourceIds,
|
|
314
|
+
}],
|
|
315
|
+
would_store: duplicateDetected ? 0 : 1,
|
|
316
|
+
would_skip: duplicateDetected ? 1 : 0,
|
|
317
|
+
contradictions_found: contradictionsFound,
|
|
318
|
+
quality: { score: Math.max(0, 1 - qualityIssues.length * 0.25), issues: qualityIssues, word_count: words },
|
|
319
|
+
}, config)),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// Skip storing if duplicate
|
|
323
|
+
if (duplicateDetected) {
|
|
324
|
+
return {
|
|
325
|
+
text: JSON.stringify(wrapResponse({
|
|
326
|
+
candidates: [{
|
|
327
|
+
content, rationale: 'Skipped — near-duplicate already exists', tags,
|
|
328
|
+
confidence: 0, memory_type: 'semantic', source_type: 'distill',
|
|
329
|
+
duplicate_detected: true, contradiction_detected: contradictionDetected,
|
|
330
|
+
derived_from: sourceIds,
|
|
331
|
+
}],
|
|
332
|
+
stored_count: 0, skipped_count: 1, contradictions_found: contradictionsFound,
|
|
333
|
+
}, config)),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Store as semantic memory
|
|
337
|
+
const result = await api.post('/memory', {
|
|
338
|
+
content,
|
|
339
|
+
user_id: config.userId,
|
|
340
|
+
tier: 2,
|
|
341
|
+
tags,
|
|
342
|
+
author: { type: 'distill', agent_id: config.userId },
|
|
343
|
+
source_type: 'mcp_distill',
|
|
344
|
+
});
|
|
345
|
+
if (result.error)
|
|
346
|
+
throw new Error(result.error);
|
|
347
|
+
// M28: Record hash for future exact dedup
|
|
348
|
+
if (result.id)
|
|
349
|
+
recordIdempotency(config.workspaceId, content, result.id);
|
|
350
|
+
// H15/H27: Distillation quality scoring
|
|
351
|
+
const words = content.split(/\s+/).length;
|
|
352
|
+
const hasSpecifics = /\b(decided|chose|because|prefer|always|never|bug|fix|error|version|v\d)\b/i.test(content);
|
|
353
|
+
const qualityIssues = [];
|
|
354
|
+
if (words < 5)
|
|
355
|
+
qualityIssues.push('too_short');
|
|
356
|
+
if (words > 500)
|
|
357
|
+
qualityIssues.push('too_long_for_single_memory');
|
|
358
|
+
if (!hasSpecifics)
|
|
359
|
+
qualityIssues.push('low_specificity');
|
|
360
|
+
// H27: Completeness — if source_ids provided, flag that we can't verify extraction coverage
|
|
361
|
+
if (sourceIds.length > 0)
|
|
362
|
+
qualityIssues.push('completeness_unverified');
|
|
363
|
+
const qualityScore = Math.max(0, 1 - qualityIssues.length * 0.25);
|
|
364
|
+
return {
|
|
365
|
+
text: JSON.stringify(wrapResponse({
|
|
366
|
+
candidates: [{
|
|
367
|
+
content, rationale: 'Distilled from session content', tags,
|
|
368
|
+
confidence: 0.8, memory_type: 'semantic', source_type: 'distill',
|
|
369
|
+
duplicate_detected: false, contradiction_detected: contradictionDetected,
|
|
370
|
+
stored_id: result.id, derived_from: sourceIds,
|
|
371
|
+
}],
|
|
372
|
+
stored_count: 1, skipped_count: 0, contradictions_found: contradictionsFound,
|
|
373
|
+
quality: { score: qualityScore, issues: qualityIssues, word_count: words },
|
|
374
|
+
// M4: Aging grace period — signal to backend that source episodic memories
|
|
375
|
+
// should not be archived until distillation completeness is verified
|
|
376
|
+
archival_policy: { min_age_days: 7, require_completeness_check: sourceIds.length > 0 },
|
|
377
|
+
}, config)),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (name === 'velixar_session_save') {
|
|
381
|
+
const summary = args.summary;
|
|
382
|
+
const sessionId = args.session_id || `session-${Date.now()}`;
|
|
383
|
+
const tags = [...(args.tags || []), 'session', `session:${sessionId}`];
|
|
384
|
+
const result = await api.post('/memory', {
|
|
385
|
+
content: `[Session ${sessionId}] ${summary}`,
|
|
386
|
+
user_id: config.userId,
|
|
387
|
+
tier: 2,
|
|
388
|
+
tags,
|
|
389
|
+
author: { type: 'agent', session_id: sessionId },
|
|
390
|
+
source_type: 'mcp_session',
|
|
391
|
+
});
|
|
392
|
+
if (result.error)
|
|
393
|
+
throw new Error(result.error);
|
|
394
|
+
// M27: Dual session storage — also store a compact summary version for session_resume
|
|
395
|
+
const compactSummary = summary.length > 500 ? summary.slice(0, 500) + '…' : summary;
|
|
396
|
+
try {
|
|
397
|
+
await api.post('/memory', {
|
|
398
|
+
content: `[Session ${sessionId} summary] ${compactSummary}`,
|
|
399
|
+
user_id: config.userId,
|
|
400
|
+
tier: 2,
|
|
401
|
+
tags: [...tags, 'session_summary'],
|
|
402
|
+
author: { type: 'agent', session_id: sessionId },
|
|
403
|
+
source_type: 'mcp_session',
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch { /* non-blocking — raw is the primary */ }
|
|
407
|
+
return { text: JSON.stringify(wrapResponse({ session_id: sessionId, stored_id: result.id, tags, dual_stored: true }, config)) };
|
|
408
|
+
}
|
|
409
|
+
if (name === 'velixar_session_recall') {
|
|
410
|
+
const sessionId = args.session_id;
|
|
411
|
+
const topic = args.topic;
|
|
412
|
+
const chunkId = args.chunk_id;
|
|
413
|
+
const startTime = args.start_time;
|
|
414
|
+
const endTime = args.end_time;
|
|
415
|
+
const order = args.order || 'recent_first';
|
|
416
|
+
const limit = Math.min(args.limit || 10, 50);
|
|
417
|
+
if (!sessionId && !topic)
|
|
418
|
+
throw new Error('Either session_id or topic required');
|
|
419
|
+
let rawMemories = [];
|
|
420
|
+
if (sessionId) {
|
|
421
|
+
const result = await api.get(`/memory/session/${sessionId}?limit=${limit}`, true);
|
|
422
|
+
const rObj = (result && typeof result === 'object') ? result : {};
|
|
423
|
+
rawMemories = Array.isArray(rObj.memories) ? rObj.memories : [];
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const params = new URLSearchParams({ q: `session ${topic}`, user_id: config.userId, limit: String(limit) });
|
|
427
|
+
const raw = await api.get(`/memory/search?${params}`, true);
|
|
428
|
+
const validated = validateSearchResponse(raw, '/memory/search');
|
|
429
|
+
rawMemories = validated.memories;
|
|
430
|
+
}
|
|
431
|
+
let memories = rawMemories.map(m => {
|
|
432
|
+
const _ts = new Date(String(m.created_at || m.timestamp || '')).getTime() || 0;
|
|
433
|
+
return Object.assign({}, m, { _ts });
|
|
434
|
+
});
|
|
435
|
+
if (startTime) {
|
|
436
|
+
const startMs = new Date(startTime).getTime();
|
|
437
|
+
if (!isNaN(startMs))
|
|
438
|
+
memories = memories.filter(m => m._ts >= startMs);
|
|
439
|
+
}
|
|
440
|
+
if (endTime) {
|
|
441
|
+
const endMs = new Date(endTime).getTime();
|
|
442
|
+
if (!isNaN(endMs))
|
|
443
|
+
memories = memories.filter(m => m._ts <= endMs);
|
|
444
|
+
}
|
|
445
|
+
// H23: Chronological ordering
|
|
446
|
+
if (order === 'chronological') {
|
|
447
|
+
memories.sort((a, b) => a._ts - b._ts);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
memories.sort((a, b) => b._ts - a._ts);
|
|
451
|
+
}
|
|
452
|
+
// H24: Build chunk manifest (15-min windows) for navigation
|
|
453
|
+
const chronological = [...memories].sort((a, b) => a._ts - b._ts);
|
|
454
|
+
const CHUNK_MS = 15 * 60 * 1000;
|
|
455
|
+
const manifest = [];
|
|
456
|
+
let cStart = chronological[0]?._ts || 0;
|
|
457
|
+
let cBuf = [];
|
|
458
|
+
for (const m of chronological) {
|
|
459
|
+
if (m._ts - cStart > CHUNK_MS && cBuf.length) {
|
|
460
|
+
manifest.push({
|
|
461
|
+
id: `chunk-${manifest.length}`,
|
|
462
|
+
time_range: `${new Date(cStart).toISOString()} → ${new Date(cBuf[cBuf.length - 1]._ts).toISOString()}`,
|
|
463
|
+
summary: String(cBuf[0].content || '').slice(0, 100),
|
|
464
|
+
count: cBuf.length,
|
|
465
|
+
});
|
|
466
|
+
cBuf = [];
|
|
467
|
+
cStart = m._ts;
|
|
468
|
+
}
|
|
469
|
+
cBuf.push(m);
|
|
470
|
+
}
|
|
471
|
+
if (cBuf.length) {
|
|
472
|
+
manifest.push({
|
|
473
|
+
id: `chunk-${manifest.length}`,
|
|
474
|
+
time_range: `${new Date(cStart).toISOString()} → ${new Date(cBuf[cBuf.length - 1]._ts).toISOString()}`,
|
|
475
|
+
summary: String(cBuf[0].content || '').slice(0, 100),
|
|
476
|
+
count: cBuf.length,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// Chunk drill-down: filter to specific chunk if requested
|
|
480
|
+
if (chunkId && manifest.length) {
|
|
481
|
+
const idx = parseInt(chunkId.replace('chunk-', ''), 10);
|
|
482
|
+
if (!isNaN(idx) && idx >= 0 && idx < manifest.length) {
|
|
483
|
+
const chunk = manifest[idx];
|
|
484
|
+
const [rangeStart] = chunk.time_range.split(' → ');
|
|
485
|
+
const rangeStartMs = new Date(rangeStart).getTime();
|
|
486
|
+
const rangeEndMs = idx + 1 < manifest.length
|
|
487
|
+
? new Date(manifest[idx + 1].time_range.split(' → ')[0]).getTime()
|
|
488
|
+
: Infinity;
|
|
489
|
+
memories = memories.filter(m => m._ts >= rangeStartMs && m._ts < rangeEndMs);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Strip internal _ts before returning
|
|
493
|
+
const cleaned = memories.map(({ _ts, ...rest }) => rest);
|
|
494
|
+
return {
|
|
495
|
+
text: JSON.stringify(wrapResponse({
|
|
496
|
+
session_id: sessionId || null,
|
|
497
|
+
topic: topic || null,
|
|
498
|
+
order,
|
|
499
|
+
memories: cleaned,
|
|
500
|
+
count: cleaned.length,
|
|
501
|
+
chunk_manifest: manifest,
|
|
502
|
+
total_chunks: manifest.length,
|
|
503
|
+
}, config, { data_absent: cleaned.length === 0 })),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (name === 'velixar_session_resume') {
|
|
507
|
+
const sessionId = args.session_id;
|
|
508
|
+
const topic = args.topic;
|
|
509
|
+
const intent = args.intent || 'continue_coding';
|
|
510
|
+
const focus = args.focus;
|
|
511
|
+
const maxTokens = Math.min(args.max_tokens || 4000, 8000);
|
|
512
|
+
const fromMemoryId = args.from_memory_id;
|
|
513
|
+
const excludeTopics = args.exclude_topics || [];
|
|
514
|
+
// Step 1: Find session memories — by ID, topic, or most recent
|
|
515
|
+
let memories = [];
|
|
516
|
+
if (sessionId) {
|
|
517
|
+
const result = await api.get(`/memory/session/${sessionId}?limit=50`, true);
|
|
518
|
+
const rObj = (result && typeof result === 'object') ? result : {};
|
|
519
|
+
memories = Array.isArray(rObj.memories) ? rObj.memories : [];
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
// Search for session-tagged memories
|
|
523
|
+
const q = topic ? `session ${topic}` : 'session';
|
|
524
|
+
const params = new URLSearchParams({ q, user_id: config.userId, limit: '50' });
|
|
525
|
+
const raw = await api.get(`/memory/search?${params}`, true);
|
|
526
|
+
const validated = validateSearchResponse(raw, '/memory/search');
|
|
527
|
+
memories = validated.memories;
|
|
528
|
+
}
|
|
529
|
+
if (!memories.length) {
|
|
530
|
+
return {
|
|
531
|
+
text: JSON.stringify(wrapResponse({
|
|
532
|
+
summary: 'No session memories found.',
|
|
533
|
+
chunks: [], decisions: [], open_threads: [], last_state: null,
|
|
534
|
+
}, config, { data_absent: true })),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
// Step 2: Sort chronologically and chunk by time windows (~15 min)
|
|
538
|
+
let sorted = memories
|
|
539
|
+
.map(m => {
|
|
540
|
+
const ts = new Date(String(m.created_at || m.timestamp || '')).getTime() || 0;
|
|
541
|
+
return { content: String(m.content || ''), tags: Array.isArray(m.tags) ? m.tags : [], ts, id: String(m.id || '') };
|
|
542
|
+
})
|
|
543
|
+
.filter(m => m.ts > 0)
|
|
544
|
+
.sort((a, b) => a.ts - b.ts);
|
|
545
|
+
// Build 6.3: from_memory_id — resume from a specific point
|
|
546
|
+
if (fromMemoryId) {
|
|
547
|
+
const idx = sorted.findIndex(m => m.id === fromMemoryId);
|
|
548
|
+
if (idx >= 0)
|
|
549
|
+
sorted = sorted.slice(idx);
|
|
550
|
+
}
|
|
551
|
+
// Build 6.3: exclude_topics — filter out irrelevant threads
|
|
552
|
+
if (excludeTopics.length > 0) {
|
|
553
|
+
const excludeLower = excludeTopics.map(t => t.toLowerCase());
|
|
554
|
+
sorted = sorted.filter(m => !excludeLower.some(t => m.content.toLowerCase().includes(t)));
|
|
555
|
+
}
|
|
556
|
+
const CHUNK_WINDOW_MS = 15 * 60 * 1000;
|
|
557
|
+
const chunks = [];
|
|
558
|
+
let chunkStart = sorted[0]?.ts || 0;
|
|
559
|
+
let currentChunk = [];
|
|
560
|
+
for (const m of sorted) {
|
|
561
|
+
if (m.ts - chunkStart > CHUNK_WINDOW_MS && currentChunk.length > 0) {
|
|
562
|
+
const startStr = new Date(chunkStart).toISOString();
|
|
563
|
+
const endStr = new Date(currentChunk[currentChunk.length - 1].ts).toISOString();
|
|
564
|
+
const content = currentChunk.map(c => c.content).join(' ');
|
|
565
|
+
chunks.push({
|
|
566
|
+
id: `chunk-${chunks.length}`,
|
|
567
|
+
time_range: `${startStr} → ${endStr}`,
|
|
568
|
+
summary: content.slice(0, 150),
|
|
569
|
+
memories: currentChunk,
|
|
570
|
+
char_count: content.length,
|
|
571
|
+
});
|
|
572
|
+
currentChunk = [];
|
|
573
|
+
chunkStart = m.ts;
|
|
574
|
+
}
|
|
575
|
+
currentChunk.push(m);
|
|
576
|
+
}
|
|
577
|
+
if (currentChunk.length) {
|
|
578
|
+
const startStr = new Date(chunkStart).toISOString();
|
|
579
|
+
const endStr = new Date(currentChunk[currentChunk.length - 1].ts).toISOString();
|
|
580
|
+
const content = currentChunk.map(c => c.content).join(' ');
|
|
581
|
+
chunks.push({
|
|
582
|
+
id: `chunk-${chunks.length}`,
|
|
583
|
+
time_range: `${startStr} → ${endStr}`,
|
|
584
|
+
summary: content.slice(0, 150),
|
|
585
|
+
memories: currentChunk,
|
|
586
|
+
char_count: content.length,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
// Step 3: Select chunks based on intent and budget
|
|
590
|
+
// Estimate ~4 chars per token
|
|
591
|
+
const charBudget = maxTokens * 4;
|
|
592
|
+
let selectedContent = [];
|
|
593
|
+
let usedChars = 0;
|
|
594
|
+
// Intent-based selection: recent chunks get full detail, older get summaries
|
|
595
|
+
const recentChunks = chunks.slice(-3); // last 3 chunks = full detail
|
|
596
|
+
const olderChunks = chunks.slice(0, -3); // older = summary only
|
|
597
|
+
for (const chunk of recentChunks) {
|
|
598
|
+
const detail = chunk.memories.map(m => m.content).join('\n');
|
|
599
|
+
if (usedChars + detail.length <= charBudget) {
|
|
600
|
+
selectedContent.push(`[${chunk.time_range}]\n${detail}`);
|
|
601
|
+
usedChars += detail.length;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const chunk of olderChunks.reverse()) {
|
|
605
|
+
if (usedChars + chunk.summary.length + 50 <= charBudget) {
|
|
606
|
+
selectedContent.unshift(`[${chunk.time_range}] (summary) ${chunk.summary}`);
|
|
607
|
+
usedChars += chunk.summary.length + 50;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Step 4: Extract decisions and open threads based on intent
|
|
611
|
+
// M26: Intent-aware extraction — different intents preserve different details
|
|
612
|
+
const codingPatterns = /\b(file|function|class|module|import|error|bug|fix|test|deploy|commit|branch|PR|TODO)\b/i;
|
|
613
|
+
const postmortemPatterns = /\b(decided|decision|failed|broke|root cause|incident|outage|rollback|lesson|mistake)\b/i;
|
|
614
|
+
const intentFilter = intent === 'continue_coding' ? codingPatterns
|
|
615
|
+
: intent === 'write_postmortem' ? postmortemPatterns
|
|
616
|
+
: null; // catch_up = no filter, show everything
|
|
617
|
+
const decisions = sorted
|
|
618
|
+
.filter(m => {
|
|
619
|
+
const c = m.content.toLowerCase();
|
|
620
|
+
return c.includes('decided') || c.includes('decision') || c.includes('chose') || c.includes('going with');
|
|
621
|
+
})
|
|
622
|
+
.map(m => m.content.slice(0, 200));
|
|
623
|
+
const openThreads = sorted
|
|
624
|
+
.filter(m => {
|
|
625
|
+
const c = m.content.toLowerCase();
|
|
626
|
+
return c.includes('todo') || c.includes('need to') || c.includes('should') || c.includes('next step') || c.includes('blocked');
|
|
627
|
+
})
|
|
628
|
+
.map(m => m.content.slice(0, 200));
|
|
629
|
+
// Step 5: Build context package
|
|
630
|
+
const lastMemory = sorted[sorted.length - 1];
|
|
631
|
+
const lastState = lastMemory ? lastMemory.content.slice(0, 300) : null;
|
|
632
|
+
// Focus filtering
|
|
633
|
+
if (focus) {
|
|
634
|
+
selectedContent = selectedContent.filter(c => c.toLowerCase().includes(focus.toLowerCase()));
|
|
635
|
+
}
|
|
636
|
+
// M26: Intent filtering — boost intent-relevant content
|
|
637
|
+
if (intentFilter && selectedContent.length > 3) {
|
|
638
|
+
selectedContent.sort((a, b) => {
|
|
639
|
+
const aMatch = intentFilter.test(a) ? 1 : 0;
|
|
640
|
+
const bMatch = intentFilter.test(b) ? 1 : 0;
|
|
641
|
+
return bMatch - aMatch;
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
const manifest = chunks.map(c => ({
|
|
645
|
+
id: c.id,
|
|
646
|
+
time_range: c.time_range,
|
|
647
|
+
summary: c.summary,
|
|
648
|
+
memory_count: c.memories.length,
|
|
649
|
+
}));
|
|
650
|
+
return {
|
|
651
|
+
text: JSON.stringify(wrapResponse({
|
|
652
|
+
intent,
|
|
653
|
+
session_id: sessionId || 'auto-detected',
|
|
654
|
+
chunk_manifest: manifest,
|
|
655
|
+
total_chunks: chunks.length,
|
|
656
|
+
total_memories: sorted.length,
|
|
657
|
+
narrative: selectedContent.join('\n\n'),
|
|
658
|
+
decisions: decisions.slice(0, 10),
|
|
659
|
+
open_threads: openThreads.slice(0, 10),
|
|
660
|
+
last_state: lastState,
|
|
661
|
+
focus: focus || null,
|
|
662
|
+
token_estimate: Math.ceil(usedChars / 4),
|
|
663
|
+
drill_down_hint: 'Use velixar_session_recall with chunk_id to expand any chunk in the manifest.',
|
|
664
|
+
}, config, {
|
|
665
|
+
data_absent: selectedContent.length === 0,
|
|
666
|
+
})),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (name === 'velixar_batch_store') {
|
|
670
|
+
const items = args.items.slice(0, 20);
|
|
671
|
+
// Build 1.3: Intra-batch dedup — detect near-duplicate items within the batch by content hash
|
|
672
|
+
const seen = new Map(); // hash → first index
|
|
673
|
+
const intraBatchDupes = new Set();
|
|
674
|
+
for (let i = 0; i < items.length; i++) {
|
|
675
|
+
const hash = items[i].content.trim().toLowerCase().slice(0, 200);
|
|
676
|
+
if (seen.has(hash)) {
|
|
677
|
+
intraBatchDupes.add(i);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
seen.set(hash, i);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const results = await Promise.allSettled(items.map(async (item, idx) => {
|
|
684
|
+
// Intra-batch duplicate
|
|
685
|
+
if (intraBatchDupes.has(idx)) {
|
|
686
|
+
return { id: undefined, status: 'skipped_intra_batch_duplicate' };
|
|
687
|
+
}
|
|
688
|
+
// Idempotency: skip if same content was stored recently
|
|
689
|
+
const existingId = checkIdempotency(config.workspaceId, item.content);
|
|
690
|
+
if (existingId)
|
|
691
|
+
return { id: existingId, status: 'skipped_duplicate' };
|
|
692
|
+
// Build 1.3: Distill always checks duplicates (per Round 2 Chain 10)
|
|
693
|
+
// For batch_store, do a lightweight similarity check
|
|
694
|
+
let similarWarning;
|
|
695
|
+
try {
|
|
696
|
+
const searchParams = new URLSearchParams({ q: item.content.slice(0, 200), user_id: config.userId, limit: '1' });
|
|
697
|
+
const searchRaw = await api.get(`/memory/search?${searchParams}`, false);
|
|
698
|
+
const searchResult = validateSearchResponse(searchRaw, '/memory/search');
|
|
699
|
+
if (searchResult.memories.length > 0 && (searchResult.memories[0].score ?? 0) >= 0.95) {
|
|
700
|
+
similarWarning = `similar to existing memory ${searchResult.memories[0].id}`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch { /* non-blocking */ }
|
|
704
|
+
const res = await api.post('/memory', {
|
|
705
|
+
content: item.content,
|
|
706
|
+
user_id: config.userId,
|
|
707
|
+
tier: item.tier ?? 2,
|
|
708
|
+
tags: item.tags || autoTags(item.content),
|
|
709
|
+
author: { type: 'user' },
|
|
710
|
+
source_type: 'mcp_batch',
|
|
711
|
+
});
|
|
712
|
+
if (res.id)
|
|
713
|
+
recordIdempotency(config.workspaceId, item.content, res.id);
|
|
714
|
+
return { id: res.id, status: 'stored', similar_warning: similarWarning };
|
|
715
|
+
}));
|
|
716
|
+
const statuses = results.map((r, i) => {
|
|
717
|
+
const val = r.status === 'fulfilled' ? r.value : null;
|
|
718
|
+
return {
|
|
719
|
+
index: i,
|
|
720
|
+
status: r.status === 'fulfilled' && !val?.error ? (val?.status || 'stored') : 'error',
|
|
721
|
+
id: val?.id,
|
|
722
|
+
similar_warning: val?.similar_warning,
|
|
723
|
+
error: r.status === 'rejected' ? String(r.reason) : val?.error ? String(val.error) : undefined,
|
|
724
|
+
...(r.status === 'rejected' || val?.error ? { content_preview: items[i].content.slice(0, 80) } : {}),
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
const stored = statuses.filter(s => s.status === 'stored').length;
|
|
728
|
+
const skipped = statuses.filter(s => s.status.startsWith('skipped')).length;
|
|
729
|
+
const failed = statuses.filter(s => s.status === 'error');
|
|
730
|
+
return {
|
|
731
|
+
text: JSON.stringify(wrapResponse({
|
|
732
|
+
items: statuses,
|
|
733
|
+
stored_count: stored,
|
|
734
|
+
skipped_count: skipped,
|
|
735
|
+
error_count: failed.length,
|
|
736
|
+
...(failed.length ? { retry_hint: `${failed.length} item(s) failed. Retry only the failed indices: [${failed.map(f => f.index).join(', ')}]` } : {}),
|
|
737
|
+
}, config)),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
// velixar_batch_search removed — now handled as alias in retrieval.ts (Build 2.2)
|
|
741
|
+
if (name === 'velixar_consolidate') {
|
|
742
|
+
const memoryIds = args.memory_ids;
|
|
743
|
+
const topic = args.topic;
|
|
744
|
+
const providedSummary = args.summary;
|
|
745
|
+
const preview = args.preview === true;
|
|
746
|
+
// Find candidates: either by IDs or by topic search
|
|
747
|
+
let candidates = [];
|
|
748
|
+
if (memoryIds?.length) {
|
|
749
|
+
const fetches = await Promise.allSettled(memoryIds.slice(0, 10).map(id => api.get(`/memory/${id}`, true)));
|
|
750
|
+
for (const f of fetches) {
|
|
751
|
+
if (f.status !== 'fulfilled')
|
|
752
|
+
continue;
|
|
753
|
+
const val = f.value;
|
|
754
|
+
const rObj = (val && typeof val === 'object') ? val : {};
|
|
755
|
+
const mem = (rObj.memory && typeof rObj.memory === 'object') ? rObj.memory : null;
|
|
756
|
+
if (mem) {
|
|
757
|
+
candidates.push({ id: String(mem.id || ''), content: String(mem.content || ''), tags: Array.isArray(mem.tags) ? mem.tags.filter((t) => typeof t === 'string') : [] });
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else if (topic) {
|
|
762
|
+
const params = new URLSearchParams({ q: topic, user_id: config.userId, limit: '10' });
|
|
763
|
+
const raw = await api.get(`/memory/search?${params}`, true);
|
|
764
|
+
const validated = validateSearchResponse(raw, '/memory/search');
|
|
765
|
+
candidates = validated.memories.map(m => ({
|
|
766
|
+
id: m.id, content: m.content || '', tags: m.tags || [],
|
|
767
|
+
}));
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
throw new Error('Either memory_ids or topic required');
|
|
771
|
+
}
|
|
772
|
+
if (candidates.length < 2) {
|
|
773
|
+
return { text: JSON.stringify(wrapResponse({ message: 'Need at least 2 memories to consolidate', candidates: candidates.length }, config)) };
|
|
774
|
+
}
|
|
775
|
+
// Build consolidated summary
|
|
776
|
+
const summary = providedSummary || candidates.map(c => c.content).join(' | ');
|
|
777
|
+
const allTags = [...new Set(candidates.flatMap(c => c.tags).concat(['consolidated']))];
|
|
778
|
+
// H26: Preview mode — show what would be merged without executing
|
|
779
|
+
if (preview) {
|
|
780
|
+
// Identify unique details per memory that might be lost in consolidation
|
|
781
|
+
const uniqueDetails = candidates.map(c => {
|
|
782
|
+
const otherContent = candidates.filter(o => o.id !== c.id).map(o => o.content).join(' ');
|
|
783
|
+
const words = c.content.split(/\s+/);
|
|
784
|
+
const unique = words.filter(w => w.length > 4 && !otherContent.toLowerCase().includes(w.toLowerCase()));
|
|
785
|
+
return { id: c.id, preview: c.content.slice(0, 120), unique_terms: unique.slice(0, 10), tag_count: c.tags.length };
|
|
786
|
+
});
|
|
787
|
+
return {
|
|
788
|
+
text: JSON.stringify(wrapResponse({
|
|
789
|
+
preview: true,
|
|
790
|
+
source_count: candidates.length,
|
|
791
|
+
sources: uniqueDetails,
|
|
792
|
+
proposed_summary: summary.slice(0, 500),
|
|
793
|
+
proposed_tags: allTags,
|
|
794
|
+
nuance_risk: uniqueDetails.some(d => d.unique_terms.length > 3) ? 'Some memories contain unique details that may be lost in consolidation' : 'Low risk — memories are highly overlapping',
|
|
795
|
+
}, config)),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// Store consolidated semantic memory
|
|
799
|
+
const stored = await api.post('/memory', {
|
|
800
|
+
content: summary.slice(0, 4000),
|
|
801
|
+
user_id: config.userId,
|
|
802
|
+
tier: 2,
|
|
803
|
+
tags: allTags,
|
|
804
|
+
author: { type: 'pipeline' },
|
|
805
|
+
});
|
|
806
|
+
if (stored.error)
|
|
807
|
+
throw new Error(stored.error);
|
|
808
|
+
return {
|
|
809
|
+
text: JSON.stringify(wrapResponse({
|
|
810
|
+
consolidated_id: stored.id,
|
|
811
|
+
source_count: candidates.length,
|
|
812
|
+
source_ids: candidates.map(c => c.id),
|
|
813
|
+
tags: allTags,
|
|
814
|
+
before: candidates.map(c => ({ id: c.id, preview: c.content.slice(0, 80) })),
|
|
815
|
+
after: { id: stored.id, preview: summary.slice(0, 200) },
|
|
816
|
+
}, config)),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
if (name === 'velixar_retag') {
|
|
820
|
+
const memoryIds = args.memory_ids.slice(0, 20);
|
|
821
|
+
const addTags = args.add_tags;
|
|
822
|
+
const removeTags = args.remove_tags;
|
|
823
|
+
const replaceTags = args.replace_tags;
|
|
824
|
+
const results = await Promise.allSettled(memoryIds.map(async (id) => {
|
|
825
|
+
let newTags;
|
|
826
|
+
if (replaceTags) {
|
|
827
|
+
newTags = replaceTags;
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
// Fetch current tags
|
|
831
|
+
const raw = await api.get(`/memory/${id}`, true);
|
|
832
|
+
const rObj = (raw && typeof raw === 'object') ? raw : {};
|
|
833
|
+
const memObj = (rObj.memory && typeof rObj.memory === 'object') ? rObj.memory : {};
|
|
834
|
+
const rawTags = memObj.tags;
|
|
835
|
+
const current = Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === 'string') : [];
|
|
836
|
+
newTags = [...current];
|
|
837
|
+
if (addTags)
|
|
838
|
+
newTags.push(...addTags.filter(t => !newTags.includes(t)));
|
|
839
|
+
if (removeTags)
|
|
840
|
+
newTags = newTags.filter(t => !removeTags.includes(t));
|
|
841
|
+
}
|
|
842
|
+
return api.patch(`/memory/${id}`, { tags: newTags });
|
|
843
|
+
}));
|
|
844
|
+
const statuses = results.map((r, i) => ({
|
|
845
|
+
id: memoryIds[i],
|
|
846
|
+
status: r.status === 'fulfilled' ? 'ok' : 'error',
|
|
847
|
+
error: r.status === 'rejected' ? String(r.reason) : undefined,
|
|
848
|
+
}));
|
|
849
|
+
return {
|
|
850
|
+
text: JSON.stringify(wrapResponse({
|
|
851
|
+
items: statuses,
|
|
852
|
+
updated_count: statuses.filter(s => s.status === 'ok').length,
|
|
853
|
+
}, config)),
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
if (name === 'velixar_export') {
|
|
857
|
+
const format = args.format || 'json';
|
|
858
|
+
const exportAll = args.all;
|
|
859
|
+
const limit = exportAll ? 1000 : Math.min(args.limit || 50, 200);
|
|
860
|
+
const query = args.query;
|
|
861
|
+
const includeGraph = args.include_graph;
|
|
862
|
+
let memories = [];
|
|
863
|
+
if (query) {
|
|
864
|
+
const params = new URLSearchParams({ q: query, user_id: config.userId, limit: String(limit) });
|
|
865
|
+
if (args.tags)
|
|
866
|
+
params.set('tags', args.tags.join(','));
|
|
867
|
+
if (args.before)
|
|
868
|
+
params.set('before', args.before);
|
|
869
|
+
if (args.after)
|
|
870
|
+
params.set('after', args.after);
|
|
871
|
+
if (args.tier !== undefined)
|
|
872
|
+
params.set('tier', String(args.tier));
|
|
873
|
+
const raw = await api.get(`/memory/search?${params}`, true);
|
|
874
|
+
const validated = validateSearchResponse(raw, '/memory/search');
|
|
875
|
+
memories = validated.memories;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
const params = new URLSearchParams({ user_id: config.userId, limit: String(limit) });
|
|
879
|
+
if (args.tags)
|
|
880
|
+
params.set('tags', args.tags.join(','));
|
|
881
|
+
if (args.before)
|
|
882
|
+
params.set('before', args.before);
|
|
883
|
+
if (args.after)
|
|
884
|
+
params.set('after', args.after);
|
|
885
|
+
if (args.tier !== undefined)
|
|
886
|
+
params.set('tier', String(args.tier));
|
|
887
|
+
const raw = await api.get(`/memory/list?${params}`, true);
|
|
888
|
+
const validated = validateListResponse(raw, '/memory/list');
|
|
889
|
+
memories = validated.memories;
|
|
890
|
+
}
|
|
891
|
+
let graph;
|
|
892
|
+
if (includeGraph) {
|
|
893
|
+
try {
|
|
894
|
+
graph = await api.get('/exocortex/overview', true);
|
|
895
|
+
}
|
|
896
|
+
catch { /* graph optional */ }
|
|
897
|
+
}
|
|
898
|
+
if (format === 'markdown') {
|
|
899
|
+
const md = memories.map((m) => {
|
|
900
|
+
const tags = m.tags?.length ? `Tags: ${m.tags.join(', ')}` : '';
|
|
901
|
+
const date = m.created_at || '';
|
|
902
|
+
return `## ${m.id}\n${date ? `*${date}*\n` : ''}\n${m.content}\n\n${tags}`;
|
|
903
|
+
}).join('\n\n---\n\n');
|
|
904
|
+
return { text: JSON.stringify(wrapResponse({ format: 'markdown', count: memories.length, content: md, ...(graph ? { graph } : {}) }, config)) };
|
|
905
|
+
}
|
|
906
|
+
return {
|
|
907
|
+
text: JSON.stringify(wrapResponse({
|
|
908
|
+
format: 'json',
|
|
909
|
+
count: memories.length,
|
|
910
|
+
memories: memories.map((m) => ({
|
|
911
|
+
id: m.id, content: m.content, tags: m.tags || [],
|
|
912
|
+
created_at: m.created_at, tier: m.tier,
|
|
913
|
+
})),
|
|
914
|
+
...(graph ? { graph } : {}),
|
|
915
|
+
}, config)),
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
if (name === 'velixar_import') {
|
|
919
|
+
const format = args.format || 'json';
|
|
920
|
+
const defaultTags = args.default_tags || [];
|
|
921
|
+
const source = args.source;
|
|
922
|
+
const conflictStrategy = args.conflict_strategy || 'skip';
|
|
923
|
+
let items = [];
|
|
924
|
+
if (format === 'markdown' && typeof args.data === 'string') {
|
|
925
|
+
items = args.data.split(/\n---\n/).map(block => {
|
|
926
|
+
const lines = block.trim().split('\n');
|
|
927
|
+
const tagLine = lines.find(l => /^Tags:\s/i.test(l));
|
|
928
|
+
const tags = tagLine ? tagLine.replace(/^Tags:\s*/i, '').split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
929
|
+
const content = lines.filter(l => l !== tagLine && !/^##\s/.test(l) && !/^\*.*\*$/.test(l)).join('\n').trim();
|
|
930
|
+
return { content, tags };
|
|
931
|
+
}).filter(i => i.content.length > 0);
|
|
932
|
+
}
|
|
933
|
+
else if (Array.isArray(args.data)) {
|
|
934
|
+
items = args.data;
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
return { text: JSON.stringify(wrapResponse({ error: 'data must be an array (JSON) or string (Markdown)' }, config)), isError: true };
|
|
938
|
+
}
|
|
939
|
+
if (items.length > 50)
|
|
940
|
+
items = items.slice(0, 50);
|
|
941
|
+
const results = await Promise.allSettled(items.map(async (item) => {
|
|
942
|
+
// Check for existing duplicate
|
|
943
|
+
const existingId = checkIdempotency(config.workspaceId, item.content);
|
|
944
|
+
if (existingId) {
|
|
945
|
+
if (conflictStrategy === 'skip')
|
|
946
|
+
return { id: existingId, status: 'skipped_duplicate' };
|
|
947
|
+
if (conflictStrategy === 'overwrite') {
|
|
948
|
+
// Update existing memory with new content/tags
|
|
949
|
+
await api.patch(`/memory/${existingId}`, {
|
|
950
|
+
content: item.content,
|
|
951
|
+
tags: [...(item.tags || []), ...defaultTags],
|
|
952
|
+
user_id: config.userId,
|
|
953
|
+
});
|
|
954
|
+
return { id: existingId, status: 'overwritten' };
|
|
955
|
+
}
|
|
956
|
+
// merge: append tags to existing
|
|
957
|
+
if (conflictStrategy === 'merge') {
|
|
958
|
+
await api.patch(`/memory/${existingId}`, {
|
|
959
|
+
tags: [...(item.tags || []), ...defaultTags],
|
|
960
|
+
user_id: config.userId,
|
|
961
|
+
});
|
|
962
|
+
return { id: existingId, status: 'merged' };
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const res = await api.post('/memory/store', {
|
|
966
|
+
content: item.content,
|
|
967
|
+
tags: [...(item.tags || []), ...defaultTags, ...(source ? [`source:${source}`] : [])],
|
|
968
|
+
tier: item.tier ?? 2,
|
|
969
|
+
user_id: config.userId,
|
|
970
|
+
source_type: 'mcp_import',
|
|
971
|
+
source_file: source || undefined,
|
|
972
|
+
});
|
|
973
|
+
if (res.id)
|
|
974
|
+
recordIdempotency(config.workspaceId, item.content, res.id);
|
|
975
|
+
return { id: res.id, status: 'imported' };
|
|
976
|
+
}));
|
|
977
|
+
const statuses = results.map((r, i) => ({
|
|
978
|
+
index: i,
|
|
979
|
+
status: r.status === 'fulfilled' ? r.value.status : 'error',
|
|
980
|
+
id: r.status === 'fulfilled' ? r.value.id : undefined,
|
|
981
|
+
error: r.status === 'rejected' ? String(r.reason) : undefined,
|
|
982
|
+
}));
|
|
983
|
+
const importedCount = statuses.filter(s => s.status === 'imported').length;
|
|
984
|
+
const skippedCount = statuses.filter(s => s.status === 'skipped_duplicate').length;
|
|
985
|
+
const overwrittenCount = statuses.filter(s => s.status === 'overwritten').length;
|
|
986
|
+
const mergedCount = statuses.filter(s => s.status === 'merged').length;
|
|
987
|
+
return {
|
|
988
|
+
text: JSON.stringify(wrapResponse({
|
|
989
|
+
imported: importedCount,
|
|
990
|
+
skipped_duplicate: skippedCount,
|
|
991
|
+
overwritten: overwrittenCount,
|
|
992
|
+
merged: mergedCount,
|
|
993
|
+
failed: statuses.filter(s => s.status === 'error').length,
|
|
994
|
+
total: items.length,
|
|
995
|
+
conflict_strategy: conflictStrategy,
|
|
996
|
+
items: statuses,
|
|
997
|
+
...(source ? { source } : {}),
|
|
998
|
+
}, config)),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
if (name === 'velixar_upload') {
|
|
1002
|
+
const filePath = args.file_path;
|
|
1003
|
+
const userTags = args.tags || [];
|
|
1004
|
+
const { readFileSync, existsSync, statSync } = await import('node:fs');
|
|
1005
|
+
const { basename, extname } = await import('node:path');
|
|
1006
|
+
const { createHash } = await import('node:crypto');
|
|
1007
|
+
if (!existsSync(filePath)) {
|
|
1008
|
+
return { text: JSON.stringify({ status: 'error', error: `File not found: ${filePath}` }), isError: true };
|
|
1009
|
+
}
|
|
1010
|
+
const stat = statSync(filePath);
|
|
1011
|
+
if (stat.size > 50 * 1024 * 1024) {
|
|
1012
|
+
return { text: JSON.stringify({ status: 'error', error: 'File exceeds 50MB limit' }), isError: true };
|
|
1013
|
+
}
|
|
1014
|
+
const filename = basename(filePath);
|
|
1015
|
+
const ext = extname(filePath).toLowerCase().replace('.', '');
|
|
1016
|
+
const mimeMap = {
|
|
1017
|
+
pdf: 'application/pdf', md: 'text/markdown', txt: 'text/plain',
|
|
1018
|
+
csv: 'text/csv', json: 'application/json', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1019
|
+
py: 'text/x-python', js: 'text/javascript', ts: 'text/typescript',
|
|
1020
|
+
rs: 'text/x-rust', go: 'text/x-go', java: 'text/x-java',
|
|
1021
|
+
rb: 'text/x-ruby', c: 'text/x-c', cpp: 'text/x-c++',
|
|
1022
|
+
html: 'text/html', xml: 'text/xml', yaml: 'text/yaml', yml: 'text/yaml',
|
|
1023
|
+
};
|
|
1024
|
+
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
|
1025
|
+
const fileBytes = readFileSync(filePath);
|
|
1026
|
+
const contentHash = createHash('sha256').update(fileBytes).digest('hex');
|
|
1027
|
+
// Step 1: Get presigned URL
|
|
1028
|
+
let presignRes;
|
|
1029
|
+
try {
|
|
1030
|
+
presignRes = await api.post('/upload/presign', {
|
|
1031
|
+
filename, mime_type: mimeType, size_bytes: stat.size, content_hash: contentHash,
|
|
1032
|
+
...(userTags.length ? { tags: userTags } : {}),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
catch (e) {
|
|
1036
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1037
|
+
const isWaf = msg.includes('403') || msg.includes('WAF');
|
|
1038
|
+
return {
|
|
1039
|
+
text: JSON.stringify({ status: 'error', error: isWaf
|
|
1040
|
+
? 'Upload blocked by firewall — contact support. Request may have triggered WAF rules.'
|
|
1041
|
+
: `Presign failed: ${msg}` }),
|
|
1042
|
+
isError: true,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const uploadId = presignRes.upload_id;
|
|
1046
|
+
const uploadUrl = presignRes.upload_url;
|
|
1047
|
+
if (!uploadId || !uploadUrl) {
|
|
1048
|
+
return { text: JSON.stringify({ status: 'error', error: 'Presign response missing upload_id or upload_url' }), isError: true };
|
|
1049
|
+
}
|
|
1050
|
+
// Step 2: Upload to S3
|
|
1051
|
+
try {
|
|
1052
|
+
const s3Res = await fetch(uploadUrl, {
|
|
1053
|
+
method: 'PUT',
|
|
1054
|
+
body: fileBytes,
|
|
1055
|
+
headers: { 'Content-Type': mimeType },
|
|
1056
|
+
});
|
|
1057
|
+
if (!s3Res.ok) {
|
|
1058
|
+
return { text: JSON.stringify({ status: 'error', error: `S3 upload failed: ${s3Res.status}` }), isError: true };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
catch (e) {
|
|
1062
|
+
return { text: JSON.stringify({ status: 'error', error: `S3 upload failed: ${e instanceof Error ? e.message : e}` }), isError: true };
|
|
1063
|
+
}
|
|
1064
|
+
// Step 3: Trigger ingestion
|
|
1065
|
+
let ingestRes;
|
|
1066
|
+
try {
|
|
1067
|
+
ingestRes = await api.post('/upload/ingest', {
|
|
1068
|
+
upload_id: uploadId, filename, content_hash: contentHash,
|
|
1069
|
+
...(userTags.length ? { tags: userTags } : {}),
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
catch (e) {
|
|
1073
|
+
return { text: JSON.stringify({ status: 'error', error: `Ingest failed: ${e instanceof Error ? e.message : e}`, upload_id: uploadId }), isError: true };
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
text: JSON.stringify(wrapResponse({
|
|
1077
|
+
upload_id: uploadId,
|
|
1078
|
+
filename,
|
|
1079
|
+
content_hash: contentHash,
|
|
1080
|
+
status: ingestRes.status || 'complete',
|
|
1081
|
+
chunks_created: ingestRes.stored || 0,
|
|
1082
|
+
skipped: ingestRes.skipped || 0,
|
|
1083
|
+
total_chunks: ingestRes.total_chunks || 0,
|
|
1084
|
+
skip_reasons: ingestRes.skip_reasons || [],
|
|
1085
|
+
source_type: 'upload',
|
|
1086
|
+
}, config)),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
throw new Error(`Unknown lifecycle tool: ${name}`);
|
|
1090
|
+
}
|
|
1091
|
+
//# sourceMappingURL=lifecycle.js.map
|