thumbgate 1.12.1 → 1.13.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/config/merge-quality-checks.json +1 -2
- package/package.json +12 -3
- package/public/index.html +2 -2
- package/scripts/context-engine.js +710 -0
- package/scripts/durability/step.js +171 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/session-report.js +120 -0
- package/scripts/swarm-coordinator.js +81 -0
- package/scripts/token-savings.js +179 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* step.js — lightweight durable-step helper.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by the "use step" pattern in Vercel Workflows, without adopting
|
|
7
|
+
* the full durable-execution runtime. Gives each external call (HTTP,
|
|
8
|
+
* LanceDB, LLM) a uniform retry + idempotency wrapper:
|
|
9
|
+
*
|
|
10
|
+
* const result = await runStep('zernio.publishPost', {
|
|
11
|
+
* retries: 3,
|
|
12
|
+
* idempotencyKey: idempotencyKey(content, platforms),
|
|
13
|
+
* }, async ({ attempt }) => {
|
|
14
|
+
* return zernioFetch('POST', '/posts', body, { idempotencyKey: ... });
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* Why a custom helper instead of Vercel Workflows / Temporal / Inngest?
|
|
18
|
+
* - We run on Railway, not Vercel.
|
|
19
|
+
* - SQLite + existing workflow tables already cover the durable state
|
|
20
|
+
* we need; the gap is per-call retry/idempotency, not orchestration.
|
|
21
|
+
* - A 60-line helper captures ~70% of the reliability benefit without
|
|
22
|
+
* the platform migration or new ops surface.
|
|
23
|
+
*
|
|
24
|
+
* Error classification:
|
|
25
|
+
* - Errors with `retryable: true` or a `code` in TRANSIENT_CODES retry.
|
|
26
|
+
* - Errors with `nonRetryable: true` bail immediately.
|
|
27
|
+
* - HTTP status (from `err.status` or parsed from message):
|
|
28
|
+
* * 429 or 5xx → retry
|
|
29
|
+
* * 4xx → fail (no point retrying validation errors)
|
|
30
|
+
* - Unknown errors → retry (capped by `retries` count — fail-open on
|
|
31
|
+
* uncertainty, but bounded).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const crypto = require('node:crypto');
|
|
35
|
+
|
|
36
|
+
const TRANSIENT_CODES = new Set([
|
|
37
|
+
'ECONNRESET',
|
|
38
|
+
'ETIMEDOUT',
|
|
39
|
+
'ENOTFOUND',
|
|
40
|
+
'EAI_AGAIN',
|
|
41
|
+
'ECONNREFUSED',
|
|
42
|
+
'EPIPE',
|
|
43
|
+
'UND_ERR_SOCKET',
|
|
44
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const DEFAULT_BACKOFF_MS = Object.freeze([250, 1000, 4000]);
|
|
48
|
+
|
|
49
|
+
function defaultClassify(err) {
|
|
50
|
+
if (!err) return 'fail';
|
|
51
|
+
if (err.nonRetryable === true) return 'fail';
|
|
52
|
+
if (err.retryable === true) return 'retry';
|
|
53
|
+
if (err.code && TRANSIENT_CODES.has(err.code)) return 'retry';
|
|
54
|
+
|
|
55
|
+
// HTTP status from either an explicit prop or a parsed message.
|
|
56
|
+
const statusFromProp = Number.isFinite(err.status) ? err.status : null;
|
|
57
|
+
const msg = typeof err.message === 'string' ? err.message : '';
|
|
58
|
+
const match = /\b(5\d{2}|4\d{2})\b/.exec(msg);
|
|
59
|
+
const status = statusFromProp || (match ? Number(match[1]) : null);
|
|
60
|
+
|
|
61
|
+
if (status === 429) return 'retry';
|
|
62
|
+
if (status && status >= 500 && status < 600) return 'retry';
|
|
63
|
+
if (status && status >= 400 && status < 500) return 'fail';
|
|
64
|
+
|
|
65
|
+
// Unknown — retry cautiously. Bounded by the `retries` option.
|
|
66
|
+
return 'retry';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sleep(ms) {
|
|
70
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a stable 32-hex-char idempotency key from arbitrary inputs.
|
|
75
|
+
* Same inputs → same key. Safe to use as an Idempotency-Key HTTP header,
|
|
76
|
+
* a LanceDB row id, or a cache key for mid-flight deduplication.
|
|
77
|
+
*
|
|
78
|
+
* Usage:
|
|
79
|
+
* idempotencyKey(content, platformList, scheduledFor)
|
|
80
|
+
*/
|
|
81
|
+
function idempotencyKey(...parts) {
|
|
82
|
+
const h = crypto.createHash('sha256');
|
|
83
|
+
for (const p of parts) {
|
|
84
|
+
if (p == null) {
|
|
85
|
+
h.update('');
|
|
86
|
+
} else if (typeof p === 'string') {
|
|
87
|
+
h.update(p);
|
|
88
|
+
} else {
|
|
89
|
+
h.update(JSON.stringify(p));
|
|
90
|
+
}
|
|
91
|
+
h.update('\0'); // field separator — prevents ["a","b"] colliding with ["ab"]
|
|
92
|
+
}
|
|
93
|
+
return h.digest('hex').slice(0, 32);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Execute `fn` with retry + backoff + classification. Returns the value
|
|
98
|
+
* `fn` resolves to, or throws the last error after exhausting retries /
|
|
99
|
+
* hitting a non-retryable verdict.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} name Step name, used in logs. e.g. 'zernio.publishPost'.
|
|
102
|
+
* @param {object|function} options { retries, backoffMs, classify, onRetry, onFail, logger }
|
|
103
|
+
* (may be passed directly as the callback shorthand)
|
|
104
|
+
* @param {function({attempt:number}):Promise} fn The actual work.
|
|
105
|
+
*/
|
|
106
|
+
function errMessage(err) {
|
|
107
|
+
return err?.message ?? err;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handleStepError({ err, attempt, retries, classify, backoffMs, name, onRetry, onFail, logger }) {
|
|
111
|
+
const verdict = classify(err);
|
|
112
|
+
const terminal = verdict === 'fail' || attempt >= retries;
|
|
113
|
+
if (terminal) {
|
|
114
|
+
if (typeof onFail === 'function') onFail({ name, attempt, err, verdict });
|
|
115
|
+
if (typeof logger === 'function') {
|
|
116
|
+
logger(`[step:${name}] FAIL attempt=${attempt} verdict=${verdict} err=${errMessage(err)}`);
|
|
117
|
+
}
|
|
118
|
+
return { terminal: true };
|
|
119
|
+
}
|
|
120
|
+
const waitMs = backoffMs[Math.min(attempt, backoffMs.length - 1)];
|
|
121
|
+
if (typeof onRetry === 'function') onRetry({ name, attempt, err, waitMs, verdict });
|
|
122
|
+
if (typeof logger === 'function') {
|
|
123
|
+
logger(`[step:${name}] RETRY attempt=${attempt} waitMs=${waitMs} err=${errMessage(err)}`);
|
|
124
|
+
}
|
|
125
|
+
return { terminal: false, waitMs };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runStep(name, options, fn) {
|
|
129
|
+
if (typeof options === 'function') {
|
|
130
|
+
fn = options;
|
|
131
|
+
options = {};
|
|
132
|
+
}
|
|
133
|
+
const {
|
|
134
|
+
retries = 3,
|
|
135
|
+
backoffMs = DEFAULT_BACKOFF_MS,
|
|
136
|
+
classify = defaultClassify,
|
|
137
|
+
onAttempt,
|
|
138
|
+
onRetry,
|
|
139
|
+
onFail,
|
|
140
|
+
logger,
|
|
141
|
+
sleepFn = sleep,
|
|
142
|
+
} = options || {};
|
|
143
|
+
|
|
144
|
+
if (typeof fn !== 'function') {
|
|
145
|
+
throw new TypeError(`runStep(${name}): fn must be a function`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let lastErr;
|
|
149
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
150
|
+
if (typeof onAttempt === 'function') onAttempt({ name, attempt });
|
|
151
|
+
try {
|
|
152
|
+
return await fn({ attempt });
|
|
153
|
+
} catch (err) {
|
|
154
|
+
lastErr = err;
|
|
155
|
+
const outcome = handleStepError({
|
|
156
|
+
err, attempt, retries, classify, backoffMs, name, onRetry, onFail, logger,
|
|
157
|
+
});
|
|
158
|
+
if (outcome.terminal) throw err;
|
|
159
|
+
await sleepFn(outcome.waitMs);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
throw lastErr;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
runStep,
|
|
167
|
+
idempotencyKey,
|
|
168
|
+
defaultClassify,
|
|
169
|
+
TRANSIENT_CODES,
|
|
170
|
+
DEFAULT_BACKOFF_MS,
|
|
171
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { URL, URLSearchParams } = require('node:url');
|
|
4
|
+
const {
|
|
5
|
+
NAMESPACES,
|
|
6
|
+
upsertContextObject,
|
|
7
|
+
recordProvenance,
|
|
8
|
+
constructTemplatedPack,
|
|
9
|
+
} = require('./contextfs');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_HF_PAPERS_API_BASE = process.env.HF_PAPERS_API_BASE || 'https://huggingface.co/api';
|
|
12
|
+
const DEFAULT_LIMIT = 5;
|
|
13
|
+
|
|
14
|
+
function normalizeAuthors(authors) {
|
|
15
|
+
if (!Array.isArray(authors)) return [];
|
|
16
|
+
return authors
|
|
17
|
+
.map((author) => {
|
|
18
|
+
if (typeof author === 'string') return author.trim();
|
|
19
|
+
if (author && typeof author.name === 'string') return author.name.trim();
|
|
20
|
+
return '';
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeTags(tags) {
|
|
26
|
+
if (!Array.isArray(tags)) return [];
|
|
27
|
+
return [...new Set(tags
|
|
28
|
+
.map((tag) => {
|
|
29
|
+
if (typeof tag === 'string') return tag.trim();
|
|
30
|
+
if (tag && typeof tag.label === 'string') return tag.label.trim();
|
|
31
|
+
if (tag && typeof tag.name === 'string') return tag.name.trim();
|
|
32
|
+
return '';
|
|
33
|
+
})
|
|
34
|
+
.filter(Boolean))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePaper(record = {}) {
|
|
38
|
+
const paper = record && typeof record.paper === 'object' ? record.paper : record;
|
|
39
|
+
const paperId = String(
|
|
40
|
+
paper.id
|
|
41
|
+
|| paper.paper_id
|
|
42
|
+
|| paper.paperId
|
|
43
|
+
|| paper.arxiv_id
|
|
44
|
+
|| paper.arxivId
|
|
45
|
+
|| record.id
|
|
46
|
+
|| record.paper_id
|
|
47
|
+
|| record.paperId
|
|
48
|
+
|| record.arxiv_id
|
|
49
|
+
|| record.arxivId
|
|
50
|
+
|| ''
|
|
51
|
+
).trim();
|
|
52
|
+
const title = String(
|
|
53
|
+
paper.title
|
|
54
|
+
|| record.title
|
|
55
|
+
|| (paperId ? `Paper ${paperId}` : 'Untitled paper')
|
|
56
|
+
).trim();
|
|
57
|
+
const summary = String(
|
|
58
|
+
paper.summary
|
|
59
|
+
|| paper.abstract
|
|
60
|
+
|| record.summary
|
|
61
|
+
|| record.abstract
|
|
62
|
+
|| ''
|
|
63
|
+
).trim();
|
|
64
|
+
const url = String(
|
|
65
|
+
paper.url
|
|
66
|
+
|| paper.paper_url
|
|
67
|
+
|| record.url
|
|
68
|
+
|| record.paper_url
|
|
69
|
+
|| (paperId ? `https://arxiv.org/abs/${paperId}` : '')
|
|
70
|
+
).trim();
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
paperId,
|
|
74
|
+
title,
|
|
75
|
+
summary,
|
|
76
|
+
url: url || null,
|
|
77
|
+
authors: normalizeAuthors(paper.authors || record.authors),
|
|
78
|
+
tags: normalizeTags(paper.tags || paper.categories || record.tags || record.categories),
|
|
79
|
+
publishedAt: paper.publishedAt || paper.published_at || record.publishedAt || record.published_at || null,
|
|
80
|
+
source: 'huggingface-papers',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractPaperItems(payload) {
|
|
85
|
+
if (Array.isArray(payload)) return payload;
|
|
86
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
87
|
+
if (Array.isArray(payload.papers)) return payload.papers;
|
|
88
|
+
if (Array.isArray(payload.items)) return payload.items;
|
|
89
|
+
if (Array.isArray(payload.results)) return payload.results;
|
|
90
|
+
if (Array.isArray(payload.dailyPapers)) return payload.dailyPapers;
|
|
91
|
+
if (payload.paper && typeof payload.paper === 'object') return [payload.paper];
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildSearchUrls({ query, limit = DEFAULT_LIMIT, baseUrl = DEFAULT_HF_PAPERS_API_BASE }) {
|
|
96
|
+
const normalizedBase = String(baseUrl || DEFAULT_HF_PAPERS_API_BASE).replace(/\/+$/, '');
|
|
97
|
+
const routes = [
|
|
98
|
+
['/daily_papers', { query, limit: String(limit) }],
|
|
99
|
+
['/papers/search', { q: query, limit: String(limit) }],
|
|
100
|
+
['/papers', { query, limit: String(limit) }],
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
return routes.map(([pathname, params]) => {
|
|
104
|
+
const url = new URL(`${normalizedBase}${pathname}`);
|
|
105
|
+
url.search = new URLSearchParams(params).toString();
|
|
106
|
+
return url.toString();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readJson(url, fetchImpl = global.fetch) {
|
|
111
|
+
if (typeof fetchImpl !== 'function') {
|
|
112
|
+
throw new Error('A fetch implementation is required');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const response = await fetchImpl(url, {
|
|
116
|
+
headers: {
|
|
117
|
+
accept: 'application/json',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const error = new Error(`HF papers request failed: ${response.status} ${response.statusText}`);
|
|
123
|
+
error.status = response.status;
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return response.json();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function searchPapers({
|
|
131
|
+
query,
|
|
132
|
+
limit = DEFAULT_LIMIT,
|
|
133
|
+
baseUrl = DEFAULT_HF_PAPERS_API_BASE,
|
|
134
|
+
fetchImpl = global.fetch,
|
|
135
|
+
} = {}) {
|
|
136
|
+
const normalizedQuery = String(query || '').trim();
|
|
137
|
+
if (!normalizedQuery) {
|
|
138
|
+
throw new Error('searchPapers requires query');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const urls = buildSearchUrls({
|
|
142
|
+
query: normalizedQuery,
|
|
143
|
+
limit: Math.max(1, Number(limit) || DEFAULT_LIMIT),
|
|
144
|
+
baseUrl,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let lastError = null;
|
|
148
|
+
for (const url of urls) {
|
|
149
|
+
try {
|
|
150
|
+
const payload = await readJson(url, fetchImpl);
|
|
151
|
+
const papers = extractPaperItems(payload)
|
|
152
|
+
.map(normalizePaper)
|
|
153
|
+
.filter((paper) => paper.paperId || paper.title);
|
|
154
|
+
|
|
155
|
+
if (papers.length > 0) {
|
|
156
|
+
return papers.slice(0, limit);
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
lastError = error;
|
|
160
|
+
if (error && error.status === 404) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (lastError) throw lastError;
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function paperToMarkdown(paper) {
|
|
171
|
+
const normalized = normalizePaper(paper);
|
|
172
|
+
const lines = [
|
|
173
|
+
`# ${normalized.title}`,
|
|
174
|
+
'',
|
|
175
|
+
`Paper ID: ${normalized.paperId || 'unknown'}`,
|
|
176
|
+
`Source: ${normalized.source}`,
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
if (normalized.url) {
|
|
180
|
+
lines.push(`URL: ${normalized.url}`);
|
|
181
|
+
}
|
|
182
|
+
if (normalized.publishedAt) {
|
|
183
|
+
lines.push(`Published: ${normalized.publishedAt}`);
|
|
184
|
+
}
|
|
185
|
+
if (normalized.authors.length > 0) {
|
|
186
|
+
lines.push(`Authors: ${normalized.authors.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
if (normalized.tags.length > 0) {
|
|
189
|
+
lines.push(`Tags: ${normalized.tags.join(', ')}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push('', '## Abstract', '', normalized.summary || 'No abstract available.', '');
|
|
193
|
+
return lines.join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildCitation(paper) {
|
|
197
|
+
return {
|
|
198
|
+
paperId: paper.paperId || null,
|
|
199
|
+
title: paper.title,
|
|
200
|
+
url: paper.url,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ingestNormalizedPapers(papers, query) {
|
|
205
|
+
const ingested = papers.map((paper) => {
|
|
206
|
+
const normalizedTags = [...new Set([
|
|
207
|
+
'research',
|
|
208
|
+
'paper',
|
|
209
|
+
'hf-papers',
|
|
210
|
+
...paper.tags.map((tag) => String(tag)),
|
|
211
|
+
])].sort();
|
|
212
|
+
|
|
213
|
+
return upsertContextObject({
|
|
214
|
+
namespace: NAMESPACES.research,
|
|
215
|
+
title: `Paper: ${paper.title}`,
|
|
216
|
+
content: paperToMarkdown(paper),
|
|
217
|
+
tags: normalizedTags,
|
|
218
|
+
source: 'hf-papers',
|
|
219
|
+
metadata: {
|
|
220
|
+
provider: 'huggingface',
|
|
221
|
+
paperId: paper.paperId || null,
|
|
222
|
+
url: paper.url,
|
|
223
|
+
authors: paper.authors,
|
|
224
|
+
publishedAt: paper.publishedAt,
|
|
225
|
+
query,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
recordProvenance({
|
|
231
|
+
type: 'hf_papers_ingested',
|
|
232
|
+
query,
|
|
233
|
+
count: ingested.length,
|
|
234
|
+
dedupedCount: ingested.filter((entry) => entry.deduped).length,
|
|
235
|
+
paperIds: papers.map((paper) => paper.paperId).filter(Boolean),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return ingested;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function ingestPaperSearch({
|
|
242
|
+
query,
|
|
243
|
+
limit = DEFAULT_LIMIT,
|
|
244
|
+
baseUrl = DEFAULT_HF_PAPERS_API_BASE,
|
|
245
|
+
fetchImpl = global.fetch,
|
|
246
|
+
searchPapersImpl = searchPapers,
|
|
247
|
+
} = {}) {
|
|
248
|
+
const papers = await searchPapersImpl({
|
|
249
|
+
query,
|
|
250
|
+
limit,
|
|
251
|
+
baseUrl,
|
|
252
|
+
fetchImpl,
|
|
253
|
+
});
|
|
254
|
+
const ingested = ingestNormalizedPapers(papers, query);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
query,
|
|
258
|
+
limit,
|
|
259
|
+
papers,
|
|
260
|
+
ingested,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function buildResearchBrief({
|
|
265
|
+
query,
|
|
266
|
+
limit = DEFAULT_LIMIT,
|
|
267
|
+
template = 'research-brief',
|
|
268
|
+
baseUrl = DEFAULT_HF_PAPERS_API_BASE,
|
|
269
|
+
fetchImpl = global.fetch,
|
|
270
|
+
searchPapersImpl = searchPapers,
|
|
271
|
+
} = {}) {
|
|
272
|
+
const result = await ingestPaperSearch({
|
|
273
|
+
query,
|
|
274
|
+
limit,
|
|
275
|
+
baseUrl,
|
|
276
|
+
fetchImpl,
|
|
277
|
+
searchPapersImpl,
|
|
278
|
+
});
|
|
279
|
+
const pack = constructTemplatedPack({ template, query });
|
|
280
|
+
const citations = result.papers.map(buildCitation);
|
|
281
|
+
const brief = pack.items
|
|
282
|
+
.map((item, index) => {
|
|
283
|
+
const digest = String(item.structuredContext && item.structuredContext.rawContent || '')
|
|
284
|
+
.split('\n')
|
|
285
|
+
.slice(0, 6)
|
|
286
|
+
.join(' ')
|
|
287
|
+
.trim();
|
|
288
|
+
return `${index + 1}. ${item.title} ${digest}`.trim();
|
|
289
|
+
})
|
|
290
|
+
.join('\n');
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
query,
|
|
294
|
+
limit,
|
|
295
|
+
source: 'huggingface-papers',
|
|
296
|
+
template,
|
|
297
|
+
ingestedCount: result.ingested.length,
|
|
298
|
+
packId: pack.packId,
|
|
299
|
+
citations,
|
|
300
|
+
brief,
|
|
301
|
+
pack,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
DEFAULT_HF_PAPERS_API_BASE,
|
|
307
|
+
buildResearchBrief,
|
|
308
|
+
buildSearchUrls,
|
|
309
|
+
extractPaperItems,
|
|
310
|
+
ingestNormalizedPapers,
|
|
311
|
+
ingestPaperSearch,
|
|
312
|
+
normalizeAuthors,
|
|
313
|
+
normalizePaper,
|
|
314
|
+
normalizeTags,
|
|
315
|
+
paperToMarkdown,
|
|
316
|
+
searchPapers,
|
|
317
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MIN_WINDOW_HOURS = 1;
|
|
4
|
+
const MAX_WINDOW_HOURS = 24 * 30;
|
|
5
|
+
const DEFAULT_WINDOW_HOURS = 24;
|
|
6
|
+
|
|
7
|
+
function normalizeWindowHours(input) {
|
|
8
|
+
if (input === null || input === undefined || input === '') return DEFAULT_WINDOW_HOURS;
|
|
9
|
+
const n = Number(input);
|
|
10
|
+
if (!Number.isFinite(n)) return DEFAULT_WINDOW_HOURS;
|
|
11
|
+
if (n < MIN_WINDOW_HOURS) return MIN_WINDOW_HOURS;
|
|
12
|
+
if (n > MAX_WINDOW_HOURS) return MAX_WINDOW_HOURS;
|
|
13
|
+
return Math.floor(n);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function topNegativeTags(tags, limit = 5) {
|
|
17
|
+
if (!tags || typeof tags !== 'object') return [];
|
|
18
|
+
return Object.entries(tags)
|
|
19
|
+
.map(([tag, counts]) => ({
|
|
20
|
+
tag,
|
|
21
|
+
negative: (counts && counts.negative) || 0,
|
|
22
|
+
positive: (counts && counts.positive) || 0,
|
|
23
|
+
total: (counts && counts.total) || 0,
|
|
24
|
+
}))
|
|
25
|
+
.filter((row) => row.negative > 0)
|
|
26
|
+
.sort((a, b) => b.negative - a.negative)
|
|
27
|
+
.slice(0, limit);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function topGates(byGate, limit = 5) {
|
|
31
|
+
if (!byGate || typeof byGate !== 'object') return [];
|
|
32
|
+
return Object.entries(byGate)
|
|
33
|
+
.map(([gate, counts]) => ({
|
|
34
|
+
gate,
|
|
35
|
+
blocked: (counts && counts.blocked) || 0,
|
|
36
|
+
warned: (counts && counts.warned) || 0,
|
|
37
|
+
pendingApproval: (counts && counts.pendingApproval) || 0,
|
|
38
|
+
}))
|
|
39
|
+
.sort((a, b) => b.blocked - a.blocked || b.warned - a.warned)
|
|
40
|
+
.slice(0, limit);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function summarizeProvenance(events, sinceMs) {
|
|
44
|
+
if (!Array.isArray(events)) return { total: 0, byType: {} };
|
|
45
|
+
const byType = {};
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (const evt of events) {
|
|
48
|
+
const ts = Date.parse(evt && evt.timestamp ? evt.timestamp : '');
|
|
49
|
+
if (!Number.isFinite(ts) || ts < sinceMs) continue;
|
|
50
|
+
total += 1;
|
|
51
|
+
const type = (evt && evt.type) || 'unknown';
|
|
52
|
+
byType[type] = (byType[type] || 0) + 1;
|
|
53
|
+
}
|
|
54
|
+
return { total, byType };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildSessionReport({ windowHours } = {}) {
|
|
58
|
+
const hours = normalizeWindowHours(windowHours);
|
|
59
|
+
const sinceMs = Date.now() - hours * 60 * 60 * 1000;
|
|
60
|
+
const report = {
|
|
61
|
+
generatedAt: new Date().toISOString(),
|
|
62
|
+
windowHours: hours,
|
|
63
|
+
since: new Date(sinceMs).toISOString(),
|
|
64
|
+
feedback: { totalPositive: 0, totalNegative: 0, topNegativeTags: [] },
|
|
65
|
+
gates: { blocked: 0, warned: 0, passed: 0, topGates: [] },
|
|
66
|
+
provenance: { total: 0, byType: {} },
|
|
67
|
+
errors: {},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const { analyzeFeedback } = require('./feedback-loop');
|
|
72
|
+
const feedback = analyzeFeedback() || {};
|
|
73
|
+
report.feedback = {
|
|
74
|
+
totalPositive: feedback.totalPositive || 0,
|
|
75
|
+
totalNegative: feedback.totalNegative || 0,
|
|
76
|
+
topNegativeTags: topNegativeTags(feedback.tags || {}),
|
|
77
|
+
};
|
|
78
|
+
} catch (err) {
|
|
79
|
+
report.errors.feedback = String(err && err.message ? err.message : err);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const { loadStats } = require('./gates-engine');
|
|
84
|
+
const stats = loadStats() || {};
|
|
85
|
+
report.gates = {
|
|
86
|
+
blocked: stats.blocked || 0,
|
|
87
|
+
warned: stats.warned || 0,
|
|
88
|
+
passed: stats.passed || 0,
|
|
89
|
+
pendingApproval: stats.pendingApproval || 0,
|
|
90
|
+
topGates: topGates(stats.byGate || {}),
|
|
91
|
+
};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
report.errors.gates = String(err && err.message ? err.message : err);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const { getProvenance } = require('./contextfs');
|
|
98
|
+
const events = getProvenance(500) || [];
|
|
99
|
+
report.provenance = summarizeProvenance(events, sinceMs);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
report.errors.provenance = String(err && err.message ? err.message : err);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (Object.keys(report.errors).length === 0) {
|
|
105
|
+
delete report.errors;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return report;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
buildSessionReport,
|
|
113
|
+
normalizeWindowHours,
|
|
114
|
+
topNegativeTags,
|
|
115
|
+
topGates,
|
|
116
|
+
summarizeProvenance,
|
|
117
|
+
MIN_WINDOW_HOURS,
|
|
118
|
+
MAX_WINDOW_HOURS,
|
|
119
|
+
DEFAULT_WINDOW_HOURS,
|
|
120
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { constructContextPack, recordProvenance } = require('./contextfs');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000;
|
|
6
|
+
const MAX_AGENTS = 32;
|
|
7
|
+
|
|
8
|
+
function normalizeAgents(agents) {
|
|
9
|
+
if (!Array.isArray(agents)) {
|
|
10
|
+
throw new Error('agents must be a non-empty array of agent names');
|
|
11
|
+
}
|
|
12
|
+
const normalized = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (const raw of agents) {
|
|
15
|
+
const name = typeof raw === 'string' ? raw.trim() : String((raw && raw.name) || '').trim();
|
|
16
|
+
if (!name) continue;
|
|
17
|
+
if (seen.has(name)) continue;
|
|
18
|
+
seen.add(name);
|
|
19
|
+
normalized.push(name);
|
|
20
|
+
}
|
|
21
|
+
if (normalized.length === 0) {
|
|
22
|
+
throw new Error('agents must include at least one named agent');
|
|
23
|
+
}
|
|
24
|
+
if (normalized.length > MAX_AGENTS) {
|
|
25
|
+
throw new Error(`agents list exceeds MAX_AGENTS (${MAX_AGENTS})`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function distributeContextToAgents({
|
|
31
|
+
query = '',
|
|
32
|
+
agents,
|
|
33
|
+
maxItems,
|
|
34
|
+
maxChars,
|
|
35
|
+
namespaces,
|
|
36
|
+
ttlMs,
|
|
37
|
+
} = {}) {
|
|
38
|
+
const agentNames = normalizeAgents(agents);
|
|
39
|
+
const ttl = Number.isFinite(Number(ttlMs)) && Number(ttlMs) > 0 ? Number(ttlMs) : DEFAULT_TTL_MS;
|
|
40
|
+
const expiresAt = new Date(Date.now() + ttl).toISOString();
|
|
41
|
+
|
|
42
|
+
const pack = constructContextPack({
|
|
43
|
+
query,
|
|
44
|
+
maxItems: Number.isFinite(Number(maxItems)) && Number(maxItems) > 0 ? Number(maxItems) : undefined,
|
|
45
|
+
maxChars: Number.isFinite(Number(maxChars)) && Number(maxChars) > 0 ? Number(maxChars) : undefined,
|
|
46
|
+
namespaces: Array.isArray(namespaces) ? namespaces : [],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const itemCount = Array.isArray(pack.items) ? pack.items.length : 0;
|
|
50
|
+
const distributions = agentNames.map((agent) => {
|
|
51
|
+
const provenance = recordProvenance({
|
|
52
|
+
type: 'context_pack_distributed',
|
|
53
|
+
packId: pack.packId,
|
|
54
|
+
agent,
|
|
55
|
+
query: pack.query,
|
|
56
|
+
itemCount,
|
|
57
|
+
expiresAt,
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
agent,
|
|
61
|
+
packId: pack.packId,
|
|
62
|
+
provenanceId: provenance.id,
|
|
63
|
+
expiresAt,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
packId: pack.packId,
|
|
69
|
+
query: pack.query,
|
|
70
|
+
totalAgents: distributions.length,
|
|
71
|
+
itemCount,
|
|
72
|
+
expiresAt,
|
|
73
|
+
distributions,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
distributeContextToAgents,
|
|
79
|
+
DEFAULT_TTL_MS,
|
|
80
|
+
MAX_AGENTS,
|
|
81
|
+
};
|