blun-king-cli 9.1.393 → 9.1.395
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/LIESMICH.txt +19 -0
- package/README.md +18 -0
- package/bin/cognitive-memory-provider.cjs +92 -0
- package/bin/cognitive-turn-lifecycle.cjs +18 -3
- package/bin/media-auto-retrieval-policy.cjs +90 -0
- package/bin/media-result-policy.cjs +59 -0
- package/bin/pending-media-policy.cjs +149 -10
- package/blun.mjs +58 -4
- package/package.json +1 -1
package/LIESMICH.txt
CHANGED
|
@@ -326,6 +326,25 @@ Nachweisbare Arbeitsabläufe
|
|
|
326
326
|
|
|
327
327
|
Alle Befehle funktionieren identisch mit `king`.
|
|
328
328
|
|
|
329
|
+
Gemeinsamer kognitiver Speicherkern (optional)
|
|
330
|
+
------------------------------------------------
|
|
331
|
+
|
|
332
|
+
Ohne zusätzliche Konfiguration bleibt der bisherige lokale SQLite-Speicher
|
|
333
|
+
aktiv. Betreiber können einen gemeinsamen, portalneutralen Speicherkern
|
|
334
|
+
ausdrücklich über BLUN_COGNITIVE_MEMORY_ADAPTER_MODULE wählen. Der Wert muss
|
|
335
|
+
auf eine absolute, kanonische CommonJS-Datei zeigen.
|
|
336
|
+
|
|
337
|
+
Das Modul exportiert synchron createCognitiveMemoryAdapter(context) und liefert
|
|
338
|
+
einen Adapter mit Vertragsversion 6. Relative Pfade, symbolische Verknüpfungen,
|
|
339
|
+
asynchrone Fabriken und abweichende Vertragsversionen werden geschlossen
|
|
340
|
+
abgewiesen. Der Adapter erhält nur die Vertragsversion, das Profilverzeichnis
|
|
341
|
+
sowie Mandanten-, Agenten- und Anzeigenamen. Zugangsdaten werden nicht
|
|
342
|
+
übergeben.
|
|
343
|
+
|
|
344
|
+
Damit kann derselbe geprüfte Ereignisstrom über CLI und Portale hinweg gelesen,
|
|
345
|
+
korrigiert und zurückgezogen werden. Ohne den optionalen Anbieter entstehen
|
|
346
|
+
keine neuen Netzwerkzugriffe und keine Verhaltensänderung.
|
|
347
|
+
|
|
329
348
|
Medienerzeugung
|
|
330
349
|
---------------
|
|
331
350
|
Für Medienaufträge bleiben GenerateImage, GenerateVideo, GenerateSpeech und
|
package/README.md
CHANGED
|
@@ -338,6 +338,24 @@ Version 9.1.0 enthält sieben zusätzliche, getrennt nutzbare Befehlsgruppen:
|
|
|
338
338
|
|
|
339
339
|
Sie stehen unter `blun` und `king` identisch zur Verfügung.
|
|
340
340
|
|
|
341
|
+
## Gemeinsamer kognitiver Speicherkern (optional)
|
|
342
|
+
|
|
343
|
+
Ohne zusätzliche Konfiguration bleibt der bisherige lokale SQLite-Speicher
|
|
344
|
+
aktiv. Betreiber können einen gemeinsamen, portalneutralen Speicherkern
|
|
345
|
+
ausdrücklich über `BLUN_COGNITIVE_MEMORY_ADAPTER_MODULE` wählen. Der Wert muss
|
|
346
|
+
auf eine absolute, kanonische CommonJS-Datei zeigen.
|
|
347
|
+
|
|
348
|
+
Das Modul exportiert synchron `createCognitiveMemoryAdapter(context)` und
|
|
349
|
+
liefert einen Adapter mit Vertragsversion 6. Relative Pfade, symbolische
|
|
350
|
+
Verknüpfungen, asynchrone Fabriken und abweichende Vertragsversionen werden
|
|
351
|
+
geschlossen abgewiesen. Der Adapter erhält nur die Vertragsversion, das
|
|
352
|
+
Profilverzeichnis sowie Mandanten-, Agenten- und Anzeigenamen. Zugangsdaten
|
|
353
|
+
werden nicht übergeben.
|
|
354
|
+
|
|
355
|
+
Damit kann derselbe geprüfte Ereignisstrom über CLI und Portale hinweg gelesen,
|
|
356
|
+
korrigiert und zurückgezogen werden. Ohne den optionalen Anbieter entstehen
|
|
357
|
+
keine neuen Netzwerkzugriffe und keine Verhaltensänderung.
|
|
358
|
+
|
|
341
359
|
## Medienerzeugung
|
|
342
360
|
|
|
343
361
|
Für Medienaufträge bleiben `GenerateImage`, `GenerateVideo`, `GenerateSpeech` und
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const {
|
|
6
|
+
COGNITIVE_MEMORY_ADAPTER_VERSION,
|
|
7
|
+
assertCognitiveMemoryAdapter,
|
|
8
|
+
} = require('./cognitive-memory-adapter.cjs');
|
|
9
|
+
|
|
10
|
+
const PROVIDER_ENV = 'BLUN_COGNITIVE_MEMORY_ADAPTER_MODULE';
|
|
11
|
+
const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
12
|
+
|
|
13
|
+
function fail(code) {
|
|
14
|
+
const error = new Error(code);
|
|
15
|
+
error.code = code;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function canonicalFile(input) {
|
|
20
|
+
const configured = String(input ?? '').trim();
|
|
21
|
+
if (!configured || !path.isAbsolute(configured)) fail('COGNITIVE_MEMORY_PROVIDER_PATH_INVALID');
|
|
22
|
+
const resolved = path.resolve(configured);
|
|
23
|
+
let stat;
|
|
24
|
+
let canonical;
|
|
25
|
+
try {
|
|
26
|
+
stat = fs.lstatSync(resolved);
|
|
27
|
+
canonical = fs.realpathSync(resolved);
|
|
28
|
+
} catch {
|
|
29
|
+
fail('COGNITIVE_MEMORY_PROVIDER_PATH_INVALID');
|
|
30
|
+
}
|
|
31
|
+
if (!stat.isFile() || stat.isSymbolicLink()
|
|
32
|
+
|| path.normalize(canonical).toLowerCase() !== path.normalize(resolved).toLowerCase()) {
|
|
33
|
+
fail('COGNITIVE_MEMORY_PROVIDER_PATH_INVALID');
|
|
34
|
+
}
|
|
35
|
+
return canonical;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function boundedContext({ home, tenantId, agentId, agentName }) {
|
|
39
|
+
const tenant = String(tenantId ?? '').trim();
|
|
40
|
+
const agent = String(agentId ?? '').trim();
|
|
41
|
+
const name = String(agentName ?? '').trim();
|
|
42
|
+
let canonicalHome;
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.lstatSync(path.resolve(String(home ?? '')));
|
|
45
|
+
canonicalHome = fs.realpathSync(path.resolve(String(home ?? '')));
|
|
46
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('COGNITIVE_MEMORY_PROVIDER_CONTEXT_INVALID');
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error?.code === 'COGNITIVE_MEMORY_PROVIDER_CONTEXT_INVALID') throw error;
|
|
49
|
+
fail('COGNITIVE_MEMORY_PROVIDER_CONTEXT_INVALID');
|
|
50
|
+
}
|
|
51
|
+
if (!SAFE_ID_RE.test(tenant) || !SAFE_ID_RE.test(agent) || !name || name.length > 128
|
|
52
|
+
|| /[\u0000-\u001f\u007f]/u.test(name)) {
|
|
53
|
+
fail('COGNITIVE_MEMORY_PROVIDER_CONTEXT_INVALID');
|
|
54
|
+
}
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
contractVersion: COGNITIVE_MEMORY_ADAPTER_VERSION,
|
|
57
|
+
home: canonicalHome,
|
|
58
|
+
tenantId: tenant,
|
|
59
|
+
agentId: agent,
|
|
60
|
+
agentName: name,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadConfiguredCognitiveMemoryAdapter({
|
|
65
|
+
modulePath,
|
|
66
|
+
env = process.env,
|
|
67
|
+
home,
|
|
68
|
+
tenantId,
|
|
69
|
+
agentId,
|
|
70
|
+
agentName,
|
|
71
|
+
} = {}) {
|
|
72
|
+
const configured = modulePath === undefined ? env?.[PROVIDER_ENV] : modulePath;
|
|
73
|
+
if (!String(configured ?? '').trim()) return undefined;
|
|
74
|
+
const providerPath = canonicalFile(configured);
|
|
75
|
+
const provider = require(providerPath);
|
|
76
|
+
if (!provider || typeof provider.createCognitiveMemoryAdapter !== 'function') {
|
|
77
|
+
fail('COGNITIVE_MEMORY_PROVIDER_FACTORY_REQUIRED');
|
|
78
|
+
}
|
|
79
|
+
const adapter = provider.createCognitiveMemoryAdapter(boundedContext({
|
|
80
|
+
home,
|
|
81
|
+
tenantId,
|
|
82
|
+
agentId,
|
|
83
|
+
agentName,
|
|
84
|
+
}));
|
|
85
|
+
if (adapter && typeof adapter.then === 'function') fail('COGNITIVE_MEMORY_PROVIDER_ASYNC_UNSUPPORTED');
|
|
86
|
+
return assertCognitiveMemoryAdapter(adapter);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
PROVIDER_ENV,
|
|
91
|
+
loadConfiguredCognitiveMemoryAdapter,
|
|
92
|
+
};
|
|
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { openCognitiveMemoryAdapter } = require('./cognitive-memory-adapter.cjs');
|
|
7
|
+
const { loadConfiguredCognitiveMemoryAdapter } = require('./cognitive-memory-provider.cjs');
|
|
7
8
|
const { buildCognitiveContextProjection } = require('./cognitive-context-projection.cjs');
|
|
8
9
|
const { buildCognitiveFocusProjection } = require('./cognitive-focus-projection.cjs');
|
|
9
10
|
const { authorizeAttentionCandidate } = require('./cognitive-attention-runtime.cjs');
|
|
@@ -521,6 +522,7 @@ function createRuntimeCognitiveTurnLifecycle({
|
|
|
521
522
|
agentId,
|
|
522
523
|
runtimeId,
|
|
523
524
|
memoryAdapter,
|
|
525
|
+
memoryAdapterModule,
|
|
524
526
|
now,
|
|
525
527
|
} = {}) {
|
|
526
528
|
const resolvedHome = String(home ?? '').trim();
|
|
@@ -537,12 +539,25 @@ function createRuntimeCognitiveTurnLifecycle({
|
|
|
537
539
|
agentId: explicitAgent || manifestIdentity?.agentId || '',
|
|
538
540
|
};
|
|
539
541
|
const hasSharedIdentity = Boolean(identity.tenantId && identity.agentId);
|
|
542
|
+
const resolvedTenantId = hasSharedIdentity
|
|
543
|
+
? identity.tenantId : digestId('home', [resolvedHome.toLowerCase()]);
|
|
544
|
+
const resolvedAgentId = hasSharedIdentity
|
|
545
|
+
? identity.agentId : digestId('agent', [resolvedAgent.toLowerCase()]);
|
|
546
|
+
const resolvedMemoryAdapter = memoryAdapter === undefined
|
|
547
|
+
? loadConfiguredCognitiveMemoryAdapter({
|
|
548
|
+
modulePath: memoryAdapterModule,
|
|
549
|
+
home: resolvedHome,
|
|
550
|
+
tenantId: resolvedTenantId,
|
|
551
|
+
agentId: resolvedAgentId,
|
|
552
|
+
agentName: resolvedAgent,
|
|
553
|
+
})
|
|
554
|
+
: memoryAdapter;
|
|
540
555
|
return createCognitiveTurnLifecycle({
|
|
541
556
|
home: resolvedHome,
|
|
542
|
-
tenantId:
|
|
543
|
-
agentId:
|
|
557
|
+
tenantId: resolvedTenantId,
|
|
558
|
+
agentId: resolvedAgentId,
|
|
544
559
|
runtimeId,
|
|
545
|
-
memoryAdapter,
|
|
560
|
+
memoryAdapter: resolvedMemoryAdapter,
|
|
546
561
|
now,
|
|
547
562
|
});
|
|
548
563
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TERMINAL_FAILURES = new Set(['blocked', 'cancelled', 'canceled', 'expired', 'failed']);
|
|
4
|
+
|
|
5
|
+
function parseAcceptedMediaJob(output) {
|
|
6
|
+
if (typeof output !== 'string') return undefined;
|
|
7
|
+
return /\bMedia job ([A-Za-z0-9_-]{1,200}) accepted with status\b/u.exec(output)?.[1];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createMediaAutoRetrievalController(options) {
|
|
11
|
+
const active = new Map();
|
|
12
|
+
const completed = new Set();
|
|
13
|
+
const inflight = new Set();
|
|
14
|
+
const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 5000);
|
|
15
|
+
const schedule = options.schedule ?? ((fn, delay) => setTimeout(fn, delay));
|
|
16
|
+
const cancel = options.cancel ?? clearTimeout;
|
|
17
|
+
|
|
18
|
+
function later(id, chatId) {
|
|
19
|
+
const timer = schedule(() => {
|
|
20
|
+
active.delete(id);
|
|
21
|
+
watch(id, chatId);
|
|
22
|
+
}, pollIntervalMs);
|
|
23
|
+
active.set(id, timer);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function poll(id, chatId) {
|
|
27
|
+
inflight.add(id);
|
|
28
|
+
try {
|
|
29
|
+
const result = await options.getMedia(id);
|
|
30
|
+
options.tracker.observeLookup(result);
|
|
31
|
+
if (result?.kind === 'status') {
|
|
32
|
+
const status = String(result.status ?? '').trim().toLowerCase();
|
|
33
|
+
if (TERMINAL_FAILURES.has(status)) {
|
|
34
|
+
options.tracker.noteDeliveryFailure(id, `media_${status}`);
|
|
35
|
+
completed.add(id);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
later(id, chatId);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (result?.kind !== 'file') {
|
|
42
|
+
options.tracker.noteDeliveryFailure(id, 'media_result_not_file');
|
|
43
|
+
completed.add(id);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const localPath = await options.saveMedia(result);
|
|
47
|
+
options.tracker.noteSaved(id, localPath);
|
|
48
|
+
const sent = await options.deliver(chatId, localPath);
|
|
49
|
+
if (!sent) {
|
|
50
|
+
options.tracker.noteDeliveryFailure(id, 'channel_delivery_failed');
|
|
51
|
+
later(id, chatId);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
options.tracker.noteDelivered(id);
|
|
55
|
+
completed.add(id);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error?.code === 'MEDIA_QUALITY_REJECTED') {
|
|
58
|
+
options.tracker.noteRejected?.(id, error.message);
|
|
59
|
+
completed.add(id);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
options.tracker.noteDeliveryFailure(id, error?.message ?? error);
|
|
63
|
+
later(id, chatId);
|
|
64
|
+
} finally {
|
|
65
|
+
inflight.delete(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function watch(id, chatId) {
|
|
70
|
+
if (completed.has(id) || active.has(id) || inflight.has(id)) return;
|
|
71
|
+
options.tracker.associateDelivery(id, chatId);
|
|
72
|
+
void poll(id, chatId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
watch,
|
|
77
|
+
resume() {
|
|
78
|
+
for (const job of options.tracker.pendingDeliveries()) watch(job.id, job.chatId);
|
|
79
|
+
},
|
|
80
|
+
stop() {
|
|
81
|
+
for (const timer of active.values()) cancel(timer);
|
|
82
|
+
active.clear();
|
|
83
|
+
},
|
|
84
|
+
async flushForTest() {
|
|
85
|
+
while (inflight.size > 0) await new Promise((resolve) => setImmediate(resolve));
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { createMediaAutoRetrievalController, parseAcceptedMediaJob };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MIN_IMAGE_BYTES = 100000;
|
|
4
|
+
const MIN_IMAGE_EDGE = 1024;
|
|
5
|
+
|
|
6
|
+
function pngDimensions(data) {
|
|
7
|
+
const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
8
|
+
if (bytes.length < 24 || !bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
if (bytes.toString('ascii', 12, 16) !== 'IHDR') return undefined;
|
|
12
|
+
return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function jpegDimensions(data) {
|
|
16
|
+
const bytes = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
17
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return undefined;
|
|
18
|
+
let offset = 2;
|
|
19
|
+
while (offset + 9 < bytes.length) {
|
|
20
|
+
if (bytes[offset] !== 0xff) {
|
|
21
|
+
offset += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const marker = bytes[offset + 1];
|
|
25
|
+
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
|
|
26
|
+
return { height: bytes.readUInt16BE(offset + 5), width: bytes.readUInt16BE(offset + 7) };
|
|
27
|
+
}
|
|
28
|
+
if (marker === 0xd8 || marker === 0xd9) {
|
|
29
|
+
offset += 2;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const length = bytes.readUInt16BE(offset + 2);
|
|
33
|
+
if (length < 2) return undefined;
|
|
34
|
+
offset += 2 + length;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateCompletedMedia(result) {
|
|
40
|
+
if (result?.kind !== 'file' || !(result.data instanceof Uint8Array) || result.data.byteLength === 0) {
|
|
41
|
+
return { ok: false, reason: 'empty_media_payload' };
|
|
42
|
+
}
|
|
43
|
+
if (!String(result.mimeType).startsWith('image/')) return { ok: true };
|
|
44
|
+
const dimensions = result.mimeType === 'image/png'
|
|
45
|
+
? pngDimensions(result.data)
|
|
46
|
+
: result.mimeType === 'image/jpeg'
|
|
47
|
+
? jpegDimensions(result.data)
|
|
48
|
+
: undefined;
|
|
49
|
+
if (!dimensions) return { ok: false, reason: 'image_dimensions_unreadable' };
|
|
50
|
+
if (dimensions.width < MIN_IMAGE_EDGE || dimensions.height < MIN_IMAGE_EDGE) {
|
|
51
|
+
return { ok: false, reason: `image_dimensions_below_${MIN_IMAGE_EDGE}`, ...dimensions };
|
|
52
|
+
}
|
|
53
|
+
if (result.data.byteLength < MIN_IMAGE_BYTES) {
|
|
54
|
+
return { ok: false, reason: `image_payload_below_${MIN_IMAGE_BYTES}`, ...dimensions };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, ...dimensions };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { validateCompletedMedia };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
3
7
|
const TERMINAL_STATUSES = new Set([
|
|
4
8
|
'blocked',
|
|
5
9
|
'cancelled',
|
|
@@ -10,34 +14,169 @@ const TERMINAL_STATUSES = new Set([
|
|
|
10
14
|
'failed',
|
|
11
15
|
'succeeded',
|
|
12
16
|
]);
|
|
17
|
+
const STATE_VERSION = 1;
|
|
18
|
+
const MAX_RECORDS = 100;
|
|
13
19
|
|
|
14
20
|
function validMediaId(value) {
|
|
15
21
|
return typeof value === 'string' && value.length > 0 && value.length <= 200
|
|
16
22
|
&& /^[A-Za-z0-9_-]+$/.test(value);
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
function normalizedStatus(value, fallback = 'processing') {
|
|
26
|
+
const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
27
|
+
return status || fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
function terminalStatus(value) {
|
|
20
|
-
return
|
|
31
|
+
return TERMINAL_STATUSES.has(normalizedStatus(value, ''));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mediaRequestKey(requestPath, body) {
|
|
35
|
+
const stableBody = Object.fromEntries(Object.entries(body ?? {}).sort(([a], [b]) => a.localeCompare(b)));
|
|
36
|
+
return crypto.createHash('sha256')
|
|
37
|
+
.update(String(requestPath))
|
|
38
|
+
.update('\0')
|
|
39
|
+
.update(JSON.stringify(stableBody))
|
|
40
|
+
.digest('hex');
|
|
21
41
|
}
|
|
22
42
|
|
|
23
|
-
function
|
|
24
|
-
|
|
43
|
+
function safeRecord(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || !validMediaId(value.id)) return undefined;
|
|
45
|
+
return {
|
|
46
|
+
id: value.id,
|
|
47
|
+
status: normalizedStatus(value.status),
|
|
48
|
+
...(typeof value.requestKey === 'string' && /^[a-f0-9]{64}$/.test(value.requestKey)
|
|
49
|
+
? { requestKey: value.requestKey }
|
|
50
|
+
: {}),
|
|
51
|
+
...(typeof value.chatId === 'string' && value.chatId.length > 0 && value.chatId.length <= 100
|
|
52
|
+
? { chatId: value.chatId }
|
|
53
|
+
: {}),
|
|
54
|
+
...(typeof value.localPath === 'string' && value.localPath.length > 0 && value.localPath.length <= 4096
|
|
55
|
+
? { localPath: value.localPath }
|
|
56
|
+
: {}),
|
|
57
|
+
createdAt: Number.isFinite(value.createdAt) ? value.createdAt : Date.now(),
|
|
58
|
+
updatedAt: Number.isFinite(value.updatedAt) ? value.updatedAt : Date.now(),
|
|
59
|
+
...(Number.isFinite(value.deliveredAt) ? { deliveredAt: value.deliveredAt } : {}),
|
|
60
|
+
...(typeof value.lastError === 'string' && value.lastError.length > 0
|
|
61
|
+
? { lastError: value.lastError.slice(0, 500) }
|
|
62
|
+
: {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createPendingMediaTracker(options = {}) {
|
|
67
|
+
const statePath = typeof options.statePath === 'string' && options.statePath.length > 0
|
|
68
|
+
? options.statePath
|
|
69
|
+
: undefined;
|
|
70
|
+
const now = typeof options.now === 'function' ? options.now : Date.now;
|
|
71
|
+
const records = new Map();
|
|
72
|
+
|
|
73
|
+
function load() {
|
|
74
|
+
if (!statePath) return;
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
77
|
+
if (parsed?.version !== STATE_VERSION || !Array.isArray(parsed.jobs)) return;
|
|
78
|
+
for (const value of parsed.jobs) {
|
|
79
|
+
const record = safeRecord(value);
|
|
80
|
+
if (record) records.set(record.id, record);
|
|
81
|
+
}
|
|
82
|
+
} catch {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function persist() {
|
|
86
|
+
if (!statePath) return;
|
|
87
|
+
const jobs = [...records.values()]
|
|
88
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
89
|
+
.slice(0, MAX_RECORDS);
|
|
90
|
+
records.clear();
|
|
91
|
+
for (const job of jobs) records.set(job.id, job);
|
|
92
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
93
|
+
const temporary = `${statePath}.${process.pid}.tmp`;
|
|
94
|
+
fs.writeFileSync(temporary, `${JSON.stringify({ version: STATE_VERSION, jobs })}\n`, { mode: 0o600 });
|
|
95
|
+
fs.renameSync(temporary, statePath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function change(id, updates) {
|
|
99
|
+
if (!validMediaId(id)) return undefined;
|
|
100
|
+
const existing = records.get(id) ?? {
|
|
101
|
+
id,
|
|
102
|
+
status: 'processing',
|
|
103
|
+
createdAt: now(),
|
|
104
|
+
updatedAt: now(),
|
|
105
|
+
};
|
|
106
|
+
const record = safeRecord({ ...existing, ...updates, id, updatedAt: now() });
|
|
107
|
+
if (!record) return undefined;
|
|
108
|
+
records.set(id, record);
|
|
109
|
+
persist();
|
|
110
|
+
return record;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
load();
|
|
25
114
|
|
|
26
115
|
return {
|
|
27
|
-
observeSubmission(job) {
|
|
116
|
+
observeSubmission(job, requestKey) {
|
|
28
117
|
if (!validMediaId(job?.id)) return;
|
|
29
|
-
|
|
30
|
-
|
|
118
|
+
change(job.id, {
|
|
119
|
+
status: normalizedStatus(job.status),
|
|
120
|
+
...(typeof requestKey === 'string' ? { requestKey } : {}),
|
|
121
|
+
});
|
|
31
122
|
},
|
|
32
123
|
observeLookup(result) {
|
|
33
124
|
if (!validMediaId(result?.id)) return;
|
|
34
|
-
if (result.kind
|
|
35
|
-
|
|
125
|
+
if (result.kind === 'file' || result.kind === 'text') {
|
|
126
|
+
change(result.id, { status: 'complete' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
change(result.id, { status: normalizedStatus(result.status) });
|
|
130
|
+
},
|
|
131
|
+
findReusableRequest(requestKey) {
|
|
132
|
+
if (typeof requestKey !== 'string') return undefined;
|
|
133
|
+
const record = [...records.values()]
|
|
134
|
+
.filter((job) => job.requestKey === requestKey && job.deliveredAt === undefined)
|
|
135
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)[0];
|
|
136
|
+
if (!record || (terminalStatus(record.status) && record.status !== 'complete' && record.status !== 'completed')) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
return { id: record.id, status: record.status };
|
|
140
|
+
},
|
|
141
|
+
associateDelivery(id, chatId) {
|
|
142
|
+
if (typeof chatId !== 'string' || chatId.length === 0) return;
|
|
143
|
+
change(id, { chatId });
|
|
144
|
+
},
|
|
145
|
+
noteSaved(id, localPath) {
|
|
146
|
+
if (typeof localPath !== 'string' || localPath.length === 0) return;
|
|
147
|
+
change(id, { localPath, status: 'complete', lastError: undefined });
|
|
148
|
+
},
|
|
149
|
+
noteDelivered(id) {
|
|
150
|
+
change(id, { deliveredAt: now(), status: 'complete', lastError: undefined });
|
|
151
|
+
},
|
|
152
|
+
noteDeliveryFailure(id, error) {
|
|
153
|
+
change(id, { lastError: String(error ?? 'delivery_failed').slice(0, 500) });
|
|
154
|
+
},
|
|
155
|
+
noteRejected(id, error) {
|
|
156
|
+
change(id, {
|
|
157
|
+
status: 'failed',
|
|
158
|
+
lastError: String(error ?? 'media_quality_rejected').slice(0, 500),
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
pendingDeliveries() {
|
|
162
|
+
return [...records.values()]
|
|
163
|
+
.filter((job) => job.chatId !== undefined && job.deliveredAt === undefined
|
|
164
|
+
&& (!terminalStatus(job.status) || job.status === 'complete' || job.status === 'completed'))
|
|
165
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
166
|
+
.map((job) => ({
|
|
167
|
+
id: job.id,
|
|
168
|
+
chatId: job.chatId,
|
|
169
|
+
status: job.status,
|
|
170
|
+
...(job.localPath === undefined ? {} : { localPath: job.localPath }),
|
|
171
|
+
}));
|
|
36
172
|
},
|
|
37
173
|
ids() {
|
|
38
|
-
return [...
|
|
174
|
+
return [...records.values()]
|
|
175
|
+
.filter((job) => !terminalStatus(job.status))
|
|
176
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
177
|
+
.map((job) => job.id);
|
|
39
178
|
},
|
|
40
179
|
};
|
|
41
180
|
}
|
|
42
181
|
|
|
43
|
-
module.exports = { createPendingMediaTracker };
|
|
182
|
+
module.exports = { createPendingMediaTracker, mediaRequestKey };
|
package/blun.mjs
CHANGED
|
@@ -312949,9 +312949,9 @@ async function assertSuccess(response, operation) {
|
|
|
312949
312949
|
} catch {}
|
|
312950
312950
|
throw new Error(`${operation} failed: HTTP ${String(response.status)}${detail ? `: ${detail}` : ""}`);
|
|
312951
312951
|
}
|
|
312952
|
-
var createPendingMediaTracker, BlunMediaService;
|
|
312952
|
+
var createPendingMediaTracker, mediaRequestKey, activeBlunMediaService, BlunMediaService;
|
|
312953
312953
|
var init_blun_media = __esmMin((() => {
|
|
312954
|
-
({ createPendingMediaTracker } = createRequire(import.meta.url)("./bin/pending-media-policy.cjs"));
|
|
312954
|
+
({ createPendingMediaTracker, mediaRequestKey } = createRequire(import.meta.url)("./bin/pending-media-policy.cjs"));
|
|
312955
312955
|
BlunMediaService = class {
|
|
312956
312956
|
tokenProvider;
|
|
312957
312957
|
apiKey;
|
|
@@ -312959,7 +312959,7 @@ var init_blun_media = __esmMin((() => {
|
|
|
312959
312959
|
defaultHeaders;
|
|
312960
312960
|
customHeaders;
|
|
312961
312961
|
fetchImpl;
|
|
312962
|
-
pendingMedia
|
|
312962
|
+
pendingMedia;
|
|
312963
312963
|
constructor(options) {
|
|
312964
312964
|
this.tokenProvider = options.tokenProvider;
|
|
312965
312965
|
this.apiKey = options.apiKey;
|
|
@@ -312967,6 +312967,8 @@ var init_blun_media = __esmMin((() => {
|
|
|
312967
312967
|
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
312968
312968
|
this.customHeaders = options.customHeaders ?? {};
|
|
312969
312969
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
312970
|
+
this.pendingMedia = createPendingMediaTracker({ statePath: join(resolveBlunHome$1(), "media", "pending-jobs.json") });
|
|
312971
|
+
activeBlunMediaService = this;
|
|
312970
312972
|
}
|
|
312971
312973
|
generateImage(prompt, options) {
|
|
312972
312974
|
return this.submit("/images/generations", { prompt }, options);
|
|
@@ -313036,6 +313038,9 @@ var init_blun_media = __esmMin((() => {
|
|
|
313036
313038
|
});
|
|
313037
313039
|
}
|
|
313038
313040
|
async submit(path, body, options) {
|
|
313041
|
+
const requestKey = mediaRequestKey(path, body);
|
|
313042
|
+
const reusable = this.pendingMedia.findReusableRequest(requestKey);
|
|
313043
|
+
if (reusable !== void 0) return reusable;
|
|
313039
313044
|
const response = await this.request(path, {
|
|
313040
313045
|
method: "POST",
|
|
313041
313046
|
body: JSON.stringify(body)
|
|
@@ -313043,7 +313048,7 @@ var init_blun_media = __esmMin((() => {
|
|
|
313043
313048
|
if (response.status === 409) throw await blockedSubmissionError(response);
|
|
313044
313049
|
await assertSuccess(response, "Media request");
|
|
313045
313050
|
const job = parseJob(await response.json());
|
|
313046
|
-
this.pendingMedia.observeSubmission(job);
|
|
313051
|
+
this.pendingMedia.observeSubmission(job, requestKey);
|
|
313047
313052
|
return job;
|
|
313048
313053
|
}
|
|
313049
313054
|
trackMediaLookup(result) {
|
|
@@ -313053,6 +313058,9 @@ var init_blun_media = __esmMin((() => {
|
|
|
313053
313058
|
pendingMediaJobIds() {
|
|
313054
313059
|
return this.pendingMedia.ids();
|
|
313055
313060
|
}
|
|
313061
|
+
pendingMediaTracker() {
|
|
313062
|
+
return this.pendingMedia;
|
|
313063
|
+
}
|
|
313056
313064
|
async request(path, init, options) {
|
|
313057
313065
|
const firstToken = await this.resolveToken(false);
|
|
313058
313066
|
const first = await this.fetchWithToken(path, init, options, firstToken);
|
|
@@ -508124,6 +508132,7 @@ var SessionEventHandler = class {
|
|
|
508124
508132
|
mediaKind: mediaKindForToolName(matchedCall.name),
|
|
508125
508133
|
phase: "failed"
|
|
508126
508134
|
});
|
|
508135
|
+
if (matchedCall !== void 0 && matchedCall.name !== "GetMedia" && isMediaToolName(matchedCall.name) && event.isError !== true) this.host.trackChannelMediaJob(event.output);
|
|
508127
508136
|
if (matchedCall?.name === "GetMedia" && event.isError !== true) this.host.runChannelMediaFallback(event.output);
|
|
508128
508137
|
this.subAgentEventHandler.handleAgentSwarmToolResult(event.toolCallId, resultData, event.isError === true);
|
|
508129
508138
|
if (matchedCall !== void 0 && matchedCall.name === "TodoList" && !event.isError) {
|
|
@@ -514706,6 +514715,9 @@ function outboxDeliveredFile(marker, chatId, filePath) {
|
|
|
514706
514715
|
}
|
|
514707
514716
|
var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
|
|
514708
514717
|
({ retryTelegramMediaDelivery, retryableTelegramMediaFailure } = createRequire(import.meta.url)("./bin/telegram-media-delivery-policy.cjs"));
|
|
514718
|
+
var createMediaAutoRetrievalController, parseAcceptedMediaJob, validateCompletedMedia;
|
|
514719
|
+
({ createMediaAutoRetrievalController, parseAcceptedMediaJob } = createRequire(import.meta.url)("./bin/media-auto-retrieval-policy.cjs"));
|
|
514720
|
+
({ validateCompletedMedia } = createRequire(import.meta.url)("./bin/media-result-policy.cjs"));
|
|
514709
514721
|
var { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
|
|
514710
514722
|
const TELEGRAM_TEXT_LIMIT = 4096;
|
|
514711
514723
|
const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
|
|
@@ -514751,6 +514763,26 @@ function completedMediaLocalPath(output) {
|
|
|
514751
514763
|
const candidate = /Media job [^\r\n]+ is complete\. Local file: (.+?)\. The BLUN host/u.exec(text)?.[1]?.trim();
|
|
514752
514764
|
return candidate === void 0 || candidate.length === 0 ? void 0 : safeCompletedMediaPath(candidate);
|
|
514753
514765
|
}
|
|
514766
|
+
function completedMediaJobId(output) {
|
|
514767
|
+
const text = Array.isArray(output) ? output.filter((part) => typeof part === "object" && part !== null && part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n") : typeof output === "string" ? output : "";
|
|
514768
|
+
return /Media job ([A-Za-z0-9_-]{1,200}) is complete\./u.exec(text)?.[1];
|
|
514769
|
+
}
|
|
514770
|
+
async function saveAutoRetrievedMedia(result) {
|
|
514771
|
+
const quality = validateCompletedMedia(result);
|
|
514772
|
+
if (!quality.ok) {
|
|
514773
|
+
const error = new Error(`Media quality gate rejected ${result.id}: ${quality.reason}`);
|
|
514774
|
+
error.code = "MEDIA_QUALITY_REJECTED";
|
|
514775
|
+
throw error;
|
|
514776
|
+
}
|
|
514777
|
+
const outputDir = mediaDir();
|
|
514778
|
+
await mkdir(outputDir, {
|
|
514779
|
+
recursive: true,
|
|
514780
|
+
mode: 448
|
|
514781
|
+
});
|
|
514782
|
+
const localPath = join(outputDir, `${safeMediaId(result.id)}.${extensionForMediaType(result.mimeType)}`);
|
|
514783
|
+
await writeFile(localPath, result.data, { mode: 384 });
|
|
514784
|
+
return localPath;
|
|
514785
|
+
}
|
|
514754
514786
|
/** Telegram group/supergroup ids are negative; DMs are the positive user id. */
|
|
514755
514787
|
function isGroupChat(chatId) {
|
|
514756
514788
|
return chatId.startsWith("-");
|
|
@@ -515956,6 +515988,7 @@ var BlunTUI = class {
|
|
|
515956
515988
|
mediaActivityStore = new MediaActivityStore();
|
|
515957
515989
|
mediaActivityTickTimer;
|
|
515958
515990
|
mediaActivityExpanded = false;
|
|
515991
|
+
mediaAutoRetrieval;
|
|
515959
515992
|
lastHistoryContent;
|
|
515960
515993
|
inputDraftTimer;
|
|
515961
515994
|
pendingInputDraft;
|
|
@@ -517431,16 +517464,36 @@ var BlunTUI = class {
|
|
|
517431
517464
|
/** See SessionEventHost.runChannelReplyFallback — called at turn end. */
|
|
517432
517465
|
channelMediaDeliveries = /* @__PURE__ */ new Set();
|
|
517433
517466
|
channelMediaDeliveryFailures = /* @__PURE__ */ new Set();
|
|
517467
|
+
ensureMediaAutoRetrieval() {
|
|
517468
|
+
if (this.mediaAutoRetrieval !== void 0) return this.mediaAutoRetrieval;
|
|
517469
|
+
const service = activeBlunMediaService;
|
|
517470
|
+
if (service === void 0) return void 0;
|
|
517471
|
+
this.mediaAutoRetrieval = createMediaAutoRetrievalController({
|
|
517472
|
+
tracker: service.pendingMediaTracker(),
|
|
517473
|
+
getMedia: (id) => service.getMedia(id),
|
|
517474
|
+
saveMedia: saveAutoRetrievedMedia,
|
|
517475
|
+
deliver: sendMediaReplyFallback
|
|
517476
|
+
});
|
|
517477
|
+
return this.mediaAutoRetrieval;
|
|
517478
|
+
}
|
|
517479
|
+
trackChannelMediaJob(output) {
|
|
517480
|
+
const guard = this.pendingChannelReplyGuard;
|
|
517481
|
+
const id = parseAcceptedMediaJob(typeof output === "string" ? output : "");
|
|
517482
|
+
if (guard === void 0 || id === void 0) return;
|
|
517483
|
+
this.ensureMediaAutoRetrieval()?.watch(id, guard.chatId);
|
|
517484
|
+
}
|
|
517434
517485
|
/** Deliver completed media at tool-result time so later queued work cannot hide it. */
|
|
517435
517486
|
runChannelMediaFallback(output) {
|
|
517436
517487
|
const guard = this.pendingChannelReplyGuard;
|
|
517437
517488
|
const filePath = completedMediaLocalPath(output);
|
|
517489
|
+
const jobId = completedMediaJobId(output);
|
|
517438
517490
|
if (guard === void 0 || filePath === void 0) return;
|
|
517439
517491
|
const deliveryKey = `${guard.chatId}\0${filePath}`;
|
|
517440
517492
|
if (this.channelMediaDeliveries.has(deliveryKey)) return;
|
|
517441
517493
|
this.channelMediaDeliveries.add(deliveryKey);
|
|
517442
517494
|
sendMediaReplyFallback(guard.chatId, filePath).then((sent) => {
|
|
517443
517495
|
if (sent) {
|
|
517496
|
+
if (jobId !== void 0) activeBlunMediaService?.pendingMediaTracker().noteDelivered(jobId);
|
|
517444
517497
|
this.channelMediaDeliveryFailures.delete(deliveryKey);
|
|
517445
517498
|
return;
|
|
517446
517499
|
}
|
|
@@ -517496,6 +517549,7 @@ var BlunTUI = class {
|
|
|
517496
517549
|
connectTelegramChannel() {
|
|
517497
517550
|
process.env["BLUN_TELEGRAM_ATTACH"] = "on";
|
|
517498
517551
|
this.startTelegramChannel();
|
|
517552
|
+
this.ensureMediaAutoRetrieval()?.resume();
|
|
517499
517553
|
return this.telegramChannel !== void 0;
|
|
517500
517554
|
}
|
|
517501
517555
|
startTelegramChannel() {
|