blun-king-cli 9.1.394 → 9.1.396
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 +17 -0
- package/README.md +16 -0
- 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/bin/turn-tool-performance-policy.cjs +99 -1
- package/blun.mjs +70 -10
- package/package.json +1 -1
package/LIESMICH.txt
CHANGED
|
@@ -364,6 +364,23 @@ Folgeturns hinweg gespeichert. Solange er offen ist, bleibt `GetMedia`
|
|
|
364
364
|
verfügbar; King prüft den tatsächlichen Status, statt fälschlich zu behaupten,
|
|
365
365
|
die Medienerzeugung sei nicht verfügbar.
|
|
366
366
|
|
|
367
|
+
Passende Werkzeuge ohne Such-Zwischenschritt
|
|
368
|
+
---------------------------------------------
|
|
369
|
+
|
|
370
|
+
Ab BLUN King 9.1.396 vergleicht King die aktuelle Anfrage mit den bereits
|
|
371
|
+
registrierten Werkzeugbeschreibungen. Bei eindeutiger Übereinstimmung lädt er
|
|
372
|
+
für diesen Zug höchstens zwei passende, sonst zurückgestellte Schemata direkt.
|
|
373
|
+
Name, Beschreibung, Parameterhinweise und Beispiele dürfen zur Auswahl
|
|
374
|
+
beitragen; frühere Nachrichten außerhalb des aktuellen Telegram-Kanalblocks
|
|
375
|
+
zählen nicht als neue Absicht.
|
|
376
|
+
|
|
377
|
+
ToolSearch bleibt für unklare, unbekannte und mehrdeutige Anfragen vollständig
|
|
378
|
+
verfügbar. Eine automatische Auswahl wird nicht dauerhaft gespeichert und
|
|
379
|
+
vergrößert den residenten Werkzeugsatz nicht. Dadurch kann eine natürliche
|
|
380
|
+
Anweisung wie "Zeig mir den letzten Telegram-Verlauf" das passende Werkzeug im
|
|
381
|
+
selben Modellschritt erhalten, während Begrüßungen und fachfremde Anfragen keine
|
|
382
|
+
zusätzlichen Schemata laden.
|
|
383
|
+
|
|
367
384
|
Angehängte Bilder werden weiterhin mit ReadMediaFile gelesen. Bild-, Video- und
|
|
368
385
|
Spracherzeugung laufen asynchron, sodass die Konsole während der Verarbeitung
|
|
369
386
|
nutzbar bleibt. GenerateVideo kann außerdem einen abgeschlossenen Bildauftrag
|
package/README.md
CHANGED
|
@@ -375,6 +375,22 @@ Folgeturns hinweg gespeichert. Solange er offen ist, bleibt `GetMedia`
|
|
|
375
375
|
verfügbar; King prüft den tatsächlichen Status, statt fälschlich zu behaupten,
|
|
376
376
|
die Medienerzeugung sei nicht verfügbar.
|
|
377
377
|
|
|
378
|
+
## Passende Werkzeuge ohne Such-Zwischenschritt
|
|
379
|
+
|
|
380
|
+
Ab BLUN King 9.1.396 vergleicht King die aktuelle Anfrage mit den bereits
|
|
381
|
+
registrierten Werkzeugbeschreibungen. Bei eindeutiger Übereinstimmung lädt er
|
|
382
|
+
für diesen Zug höchstens zwei passende, sonst zurückgestellte Schemata direkt.
|
|
383
|
+
Name, Beschreibung, Parameterhinweise und Beispiele dürfen zur Auswahl
|
|
384
|
+
beitragen; frühere Nachrichten außerhalb des aktuellen Telegram-Kanalblocks
|
|
385
|
+
zählen nicht als neue Absicht.
|
|
386
|
+
|
|
387
|
+
`ToolSearch` bleibt für unklare, unbekannte und mehrdeutige Anfragen vollständig
|
|
388
|
+
verfügbar. Eine automatische Auswahl wird nicht dauerhaft gespeichert und
|
|
389
|
+
vergrößert den residenten Werkzeugsatz nicht. Dadurch kann eine natürliche
|
|
390
|
+
Anweisung wie „Zeig mir den letzten Telegram-Verlauf“ das passende Werkzeug im
|
|
391
|
+
selben Modellschritt erhalten, während Begrüßungen und fachfremde Anfragen keine
|
|
392
|
+
zusätzlichen Schemata laden.
|
|
393
|
+
|
|
378
394
|
Angehängte Bilder werden weiterhin mit `ReadMediaFile` gelesen. Bild-, Video-
|
|
379
395
|
und Spracherzeugung laufen asynchron, sodass die Konsole während der Verarbeitung
|
|
380
396
|
nutzbar bleibt. `GenerateVideo` kann außerdem einen abgeschlossenen Bildauftrag
|
|
@@ -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 };
|
|
@@ -5,13 +5,20 @@ const TOOL_SCHEMA_MAX_TOKENS = 32_000;
|
|
|
5
5
|
const DEFERRED_TOOL_LOADER_NAME = 'ToolSearch';
|
|
6
6
|
const MAX_PERSISTENT_DEFERRED_TOOLS = 4;
|
|
7
7
|
const MAX_TOOL_SEARCH_QUERY_CHARS = 1_000;
|
|
8
|
+
const MAX_AUTO_RANKED_TOOLS = 2;
|
|
9
|
+
const TOOL_RANK_STOP_WORDS = new Set([
|
|
10
|
+
'aber', 'also', 'and', 'aus', 'bitte', 'can', 'das', 'den', 'der', 'des', 'die', 'dies', 'diese',
|
|
11
|
+
'du', 'ein', 'eine', 'einer', 'eines', 'for', 'fuer', 'für', 'haben', 'ich', 'ist', 'kann', 'kannst',
|
|
12
|
+
'mal', 'me', 'mein', 'meine', 'mir', 'mit', 'of', 'please', 'soll', 'the', 'und', 'uns', 'von',
|
|
13
|
+
'was', 'wir', 'with', 'you', 'zeige', 'zeig', 'pruefe', 'prüfe', 'lies', 'read', 'show', 'get',
|
|
14
|
+
]);
|
|
8
15
|
const TOOL_SEARCH_INTENT_WORDS = new Set([
|
|
9
16
|
'find', 'fetch', 'get', 'holen', 'list', 'read', 'search', 'show', 'anzeigen', 'lesen', 'suchen',
|
|
10
17
|
]);
|
|
11
18
|
const TOOL_SEARCH_RELATED_TERM_GROUPS = Object.freeze([
|
|
12
19
|
Object.freeze([
|
|
13
20
|
'current', 'inbox', 'latest', 'message', 'messages', 'nachricht', 'nachrichten', 'queue', 'queued',
|
|
14
|
-
'recent', 'update', 'updates',
|
|
21
|
+
'recent', 'letzte', 'letzten', 'letzter', 'vergangen', 'update', 'updates',
|
|
15
22
|
]),
|
|
16
23
|
Object.freeze([
|
|
17
24
|
'chat', 'conversation', 'history', 'log', 'logs', 'processed', 'protokoll', 'transcript', 'verlauf',
|
|
@@ -165,6 +172,96 @@ function relatedSearchTerms(token) {
|
|
|
165
172
|
return group || [token];
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
function rankingIntentTexts(value) {
|
|
176
|
+
const text = String(value || '');
|
|
177
|
+
const channelBodies = [...text.matchAll(
|
|
178
|
+
/(?:^|\r?\n)<channel\b[^>]*>\r?\n([\s\S]*?)\r?\n<\/channel>(?=\r?\n|$)/giu,
|
|
179
|
+
)].map((match) => match[1]);
|
|
180
|
+
return channelBodies.length > 0 ? channelBodies : [text];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function rankingTokens(value) {
|
|
184
|
+
return [...new Set(String(value || '')
|
|
185
|
+
.normalize('NFKC')
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.match(/[\p{L}\p{N}]{3,}/gu) || [])]
|
|
188
|
+
.filter((token) => !TOOL_RANK_STOP_WORDS.has(token));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toolRankingDocument(tool) {
|
|
192
|
+
const name = String(tool?.name || '');
|
|
193
|
+
const leaf = name.split(/__|:/).at(-1) || name;
|
|
194
|
+
const nameTokens = rankingTokens(`${name.replaceAll('_', ' ')} ${leaf.replaceAll('_', ' ')}`);
|
|
195
|
+
const descriptionTokens = rankingTokens(tool?.description);
|
|
196
|
+
const parameterText = [];
|
|
197
|
+
const properties = tool?.parameters?.properties;
|
|
198
|
+
if (properties && typeof properties === 'object') {
|
|
199
|
+
for (const [parameterName, definition] of Object.entries(properties)) {
|
|
200
|
+
parameterText.push(parameterName, definition?.description || '');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const exampleText = Array.isArray(tool?.examples)
|
|
204
|
+
? tool.examples.map((example) => example?.prompt || '').join(' ')
|
|
205
|
+
: '';
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
nameTokens: new Set(nameTokens),
|
|
209
|
+
descriptionTokens: new Set(descriptionTokens),
|
|
210
|
+
detailTokens: new Set(rankingTokens(`${parameterText.join(' ')} ${exampleText}`)),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function tokenMatchScore(document, token) {
|
|
215
|
+
if (document.nameTokens.has(token)) return 10;
|
|
216
|
+
if (document.descriptionTokens.has(token)) return 5;
|
|
217
|
+
if (document.detailTokens.has(token)) return 4;
|
|
218
|
+
const related = relatedSearchTerms(token).filter((term) => term !== token);
|
|
219
|
+
if (related.some((term) => document.nameTokens.has(term))) return 5;
|
|
220
|
+
if (related.some((term) => document.descriptionTokens.has(term))) return 3;
|
|
221
|
+
if (related.some((term) => document.detailTokens.has(term))) return 2;
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Select a tiny, high-confidence subset of deferred schemas for the current
|
|
227
|
+
* request. This mirrors AnythingLLM's tool-reranker boundary without adding an
|
|
228
|
+
* embedding runtime: King reuses the descriptions it already owns and keeps
|
|
229
|
+
* ToolSearch available whenever the lexical evidence is weak or ambiguous.
|
|
230
|
+
*/
|
|
231
|
+
function rankedToolNamesForTurnText(tools, value, options = {}) {
|
|
232
|
+
const maxTools = Math.max(0, Math.min(
|
|
233
|
+
MAX_AUTO_RANKED_TOOLS,
|
|
234
|
+
Number.isInteger(options.maxTools) ? options.maxTools : MAX_AUTO_RANKED_TOOLS,
|
|
235
|
+
));
|
|
236
|
+
if (maxTools === 0) return new Set();
|
|
237
|
+
const queryTokens = [...new Set(rankingIntentTexts(value).flatMap(rankingTokens))];
|
|
238
|
+
if (queryTokens.length === 0) return new Set();
|
|
239
|
+
|
|
240
|
+
const ranked = (Array.isArray(tools) ? tools : []).map((tool) => {
|
|
241
|
+
const document = toolRankingDocument(tool);
|
|
242
|
+
if (!document.name) return null;
|
|
243
|
+
let score = 0;
|
|
244
|
+
let matchedTokens = 0;
|
|
245
|
+
let exactNameMatches = 0;
|
|
246
|
+
for (const token of queryTokens) {
|
|
247
|
+
const tokenScore = tokenMatchScore(document, token);
|
|
248
|
+
if (tokenScore === 0) continue;
|
|
249
|
+
score += tokenScore;
|
|
250
|
+
matchedTokens += 1;
|
|
251
|
+
if (document.nameTokens.has(token)) exactNameMatches += 1;
|
|
252
|
+
}
|
|
253
|
+
const highConfidence = matchedTokens >= 2 || (exactNameMatches >= 1 && score >= 10);
|
|
254
|
+
return highConfidence ? { name: document.name, score, matchedTokens, exactNameMatches } : null;
|
|
255
|
+
}).filter(Boolean).sort((left, right) => (
|
|
256
|
+
right.matchedTokens - left.matchedTokens
|
|
257
|
+
|| right.score - left.score
|
|
258
|
+
|| right.exactNameMatches - left.exactNameMatches
|
|
259
|
+
|| left.name.localeCompare(right.name)
|
|
260
|
+
));
|
|
261
|
+
|
|
262
|
+
return new Set(ranked.slice(0, maxTools).map((entry) => entry.name));
|
|
263
|
+
}
|
|
264
|
+
|
|
168
265
|
function searchRelatedDeferredTools(tools, query, options = {}) {
|
|
169
266
|
const normalized = normalizeDeferredToolQuery(query).toLowerCase();
|
|
170
267
|
if (!normalized || normalized.startsWith('select:')) return [];
|
|
@@ -351,6 +448,7 @@ module.exports = {
|
|
|
351
448
|
mediaGenerationToolNamesForText,
|
|
352
449
|
mediaToolNamesForTurnText,
|
|
353
450
|
normalizeDeferredToolQuery,
|
|
451
|
+
rankedToolNamesForTurnText,
|
|
354
452
|
rememberDeferredToolAfterNotFound,
|
|
355
453
|
toolSchemaBudgetTokens,
|
|
356
454
|
};
|
package/blun.mjs
CHANGED
|
@@ -261514,7 +261514,7 @@ function toolResultText(result) {
|
|
|
261514
261514
|
function abandonedToolResultOutput(ended) {
|
|
261515
261515
|
return `Tool call did not complete: ${ended.reason === "cancelled" ? "the turn was cancelled" : ended.reason === "failed" ? `the turn failed${ended.error !== void 0 ? ` (${ended.error.message})` : ""}` : "the turn ended"} before its result was recorded. Do not assume the tool completed successfully.`;
|
|
261516
261516
|
}
|
|
261517
|
-
var BLUN_CORE_TOOL_NAMES, BLUN_LEAN_TOOL_NAMES, BLUN_ATTACHMENT_MARKER_RE, BLUN_TELEGRAM_OUTBOUND_TOOL_RE, BLUN_TELEGRAM_CHANNEL_RE, BLUN_TOOL_BUDGET_RATIO, createDeferredToolLoader, mediaToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens, projectRecurringCronHistory, TelegramDeliveryLedger, createRuntimeCognitiveTurnLifecycle, cognitiveFocusScopesForTurn, buildCognitiveWorkFocus, buildAttentionQueueItem, LLM_NOT_SET_MESSAGE, GOAL_CONTINUATION_ORIGIN, GOAL_COMPLETION_REMINDER_NAME, GOAL_BLOCKED_REMINDER_NAME, GOAL_RATE_LIMIT_PAUSE_REASON, GOAL_PROVIDER_CONNECTION_PAUSE_PREFIX, GOAL_PROVIDER_AUTH_PAUSE_PREFIX, GOAL_PROVIDER_API_PAUSE_PREFIX, GOAL_MODEL_CONFIG_PAUSE_PREFIX, GOAL_RUNTIME_PAUSE_PREFIX, GOAL_PROVIDER_FILTERED_PAUSE_REASON, GOAL_CONTINUATION_PROMPT, TurnFlow;
|
|
261517
|
+
var BLUN_CORE_TOOL_NAMES, BLUN_LEAN_TOOL_NAMES, BLUN_ATTACHMENT_MARKER_RE, BLUN_TELEGRAM_OUTBOUND_TOOL_RE, BLUN_TELEGRAM_CHANNEL_RE, BLUN_TOOL_BUDGET_RATIO, createDeferredToolLoader, mediaToolNamesForTurnText, rankedToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens, projectRecurringCronHistory, TelegramDeliveryLedger, createRuntimeCognitiveTurnLifecycle, cognitiveFocusScopesForTurn, buildCognitiveWorkFocus, buildAttentionQueueItem, LLM_NOT_SET_MESSAGE, GOAL_CONTINUATION_ORIGIN, GOAL_COMPLETION_REMINDER_NAME, GOAL_BLOCKED_REMINDER_NAME, GOAL_RATE_LIMIT_PAUSE_REASON, GOAL_PROVIDER_CONNECTION_PAUSE_PREFIX, GOAL_PROVIDER_AUTH_PAUSE_PREFIX, GOAL_PROVIDER_API_PAUSE_PREFIX, GOAL_MODEL_CONFIG_PAUSE_PREFIX, GOAL_RUNTIME_PAUSE_PREFIX, GOAL_PROVIDER_FILTERED_PAUSE_REASON, GOAL_CONTINUATION_PROMPT, TurnFlow;
|
|
261518
261518
|
var init_turn = __esmMin((() => {
|
|
261519
261519
|
init_dist$4();
|
|
261520
261520
|
init_src$4();
|
|
@@ -261531,7 +261531,7 @@ var init_turn = __esmMin((() => {
|
|
|
261531
261531
|
init_tool_result_budget();
|
|
261532
261532
|
init_user_message_offload();
|
|
261533
261533
|
init_assistant_message_offload();
|
|
261534
|
-
({ CORE_TOOL_NAMES: BLUN_CORE_TOOL_NAMES, createDeferredToolLoader, mediaToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens } = createRequire(import.meta.url)("./bin/turn-tool-performance-policy.cjs"));
|
|
261534
|
+
({ CORE_TOOL_NAMES: BLUN_CORE_TOOL_NAMES, createDeferredToolLoader, mediaToolNamesForTurnText, rankedToolNamesForTurnText, rememberDeferredToolAfterNotFound, toolSchemaBudgetTokens } = createRequire(import.meta.url)("./bin/turn-tool-performance-policy.cjs"));
|
|
261535
261535
|
({ projectRecurringCronHistory } = createRequire(import.meta.url)("./bin/recurring-cron-history-policy.cjs"));
|
|
261536
261536
|
({ TelegramDeliveryLedger } = createRequire(import.meta.url)("./bin/repeated-user-message-projection.cjs"));
|
|
261537
261537
|
({ createRuntimeCognitiveTurnLifecycle } = createRequire(import.meta.url)("./bin/cognitive-turn-lifecycle.cjs"));
|
|
@@ -262265,12 +262265,13 @@ var init_turn = __esmMin((() => {
|
|
|
262265
262265
|
variant: "cognitive_continuity"
|
|
262266
262266
|
});
|
|
262267
262267
|
this.setActiveSteerAcceptance(turnId, true);
|
|
262268
|
+
const turnText = blunExtractText(input);
|
|
262268
262269
|
const turnNeedsTools = blunTurnNeedsTools(input, origin);
|
|
262269
262270
|
const turnHasAttachment = blunTurnHasAttachment(input);
|
|
262270
262271
|
const turnThinkingEffort = turnHasAttachment ? void 0 : selectThinkingEffortForTurn(blunExtractText(blunThinkingIntentInput(input)), origin.kind);
|
|
262271
262272
|
const fastConversation = turnThinkingEffort === "off" || turnThinkingEffort === "low";
|
|
262272
262273
|
const pendingMediaJobIds = this.agent.toolServices?.media?.pendingMediaJobIds?.() ?? [];
|
|
262273
|
-
const requiredToolNames = mediaToolNamesForTurnText(
|
|
262274
|
+
const requiredToolNames = mediaToolNamesForTurnText(turnText);
|
|
262274
262275
|
const turnSystemPrompt = pendingMediaJobIds.length > 0 ? blunPendingMediaSystemPrompt(fastConversation ? this.agent.fastConversationSystemPrompt : this.agent.effectiveSystemPrompt, pendingMediaJobIds) : fastConversation ? this.agent.fastConversationSystemPrompt : void 0;
|
|
262275
262276
|
const turnLLM = this.agent.llmForTurn(turnThinkingEffort, turnSystemPrompt);
|
|
262276
262277
|
let previousStepToolOutcome = "initial";
|
|
@@ -262285,10 +262286,13 @@ var init_turn = __esmMin((() => {
|
|
|
262285
262286
|
const originTools = blunToolsForOrigin(this.agent.injection.filterPersonalMemoryToolsForTurn(turnId, input, origin, this.agent.tools.loopTools), origin);
|
|
262286
262287
|
const compactConversationTool = originTools.find((tool) => tool.name === "CompactConversation");
|
|
262287
262288
|
const eligibleTools = originTools.filter((tool) => tool.name !== "CompactConversation" || this.agent.fullCompaction.isProactiveCompactionEligible());
|
|
262289
|
+
const turnRequiredToolNames = new Set(requiredToolNames);
|
|
262290
|
+
const rankedToolNames = rankedToolNamesForTurnText(eligibleTools, turnText);
|
|
262291
|
+
for (const name of rankedToolNames) turnRequiredToolNames.add(name);
|
|
262288
262292
|
const toolSelection = fastConversation ? {
|
|
262289
|
-
tools: [...blunFastConversationTools(eligibleTools, input, pendingMediaJobIds,
|
|
262293
|
+
tools: [...blunFastConversationTools(eligibleTools, input, pendingMediaJobIds, turnRequiredToolNames)],
|
|
262290
262294
|
deferredToolCount: 0
|
|
262291
|
-
} : turnNeedsTools ? blunSelectTurnTools(eligibleTools, this.agent.config.modelCapabilities?.max_context_tokens, this.loadedToolNames,
|
|
262295
|
+
} : turnNeedsTools ? blunSelectTurnTools(eligibleTools, this.agent.config.modelCapabilities?.max_context_tokens, this.loadedToolNames, turnRequiredToolNames) : {
|
|
262292
262296
|
tools: [],
|
|
262293
262297
|
deferredToolCount: 0
|
|
262294
262298
|
};
|
|
@@ -262356,7 +262360,9 @@ var init_turn = __esmMin((() => {
|
|
|
262356
262360
|
const steerThinkingEffort = "low";
|
|
262357
262361
|
const steerPendingMediaJobIds = this.agent.toolServices?.media?.pendingMediaJobIds?.() ?? [];
|
|
262358
262362
|
const steerLLM = this.agent.llmForTurn(steerThinkingEffort, blunPendingMediaSystemPrompt(this.agent.fastConversationSystemPrompt, steerPendingMediaJobIds));
|
|
262359
|
-
const
|
|
262363
|
+
const steerText = blunExtractText(steerInput);
|
|
262364
|
+
const steerRequiredToolNames = mediaToolNamesForTurnText(steerText);
|
|
262365
|
+
for (const name of rankedToolNamesForTurnText(eligibleTools, steerText)) steerRequiredToolNames.add(name);
|
|
262360
262366
|
const steerTools = [...blunFastConversationTools(eligibleTools, steerInput, steerPendingMediaJobIds, steerRequiredToolNames)];
|
|
262361
262367
|
const steerHistory = () => blunFastConversationHistory(this.agent.context.history);
|
|
262362
262368
|
const buildSteerMessages = () => this.agent.context.project(steerHistory(), { dropOrphanResults: true });
|
|
@@ -312949,9 +312955,9 @@ async function assertSuccess(response, operation) {
|
|
|
312949
312955
|
} catch {}
|
|
312950
312956
|
throw new Error(`${operation} failed: HTTP ${String(response.status)}${detail ? `: ${detail}` : ""}`);
|
|
312951
312957
|
}
|
|
312952
|
-
var createPendingMediaTracker, BlunMediaService;
|
|
312958
|
+
var createPendingMediaTracker, mediaRequestKey, activeBlunMediaService, BlunMediaService;
|
|
312953
312959
|
var init_blun_media = __esmMin((() => {
|
|
312954
|
-
({ createPendingMediaTracker } = createRequire(import.meta.url)("./bin/pending-media-policy.cjs"));
|
|
312960
|
+
({ createPendingMediaTracker, mediaRequestKey } = createRequire(import.meta.url)("./bin/pending-media-policy.cjs"));
|
|
312955
312961
|
BlunMediaService = class {
|
|
312956
312962
|
tokenProvider;
|
|
312957
312963
|
apiKey;
|
|
@@ -312959,7 +312965,7 @@ var init_blun_media = __esmMin((() => {
|
|
|
312959
312965
|
defaultHeaders;
|
|
312960
312966
|
customHeaders;
|
|
312961
312967
|
fetchImpl;
|
|
312962
|
-
pendingMedia
|
|
312968
|
+
pendingMedia;
|
|
312963
312969
|
constructor(options) {
|
|
312964
312970
|
this.tokenProvider = options.tokenProvider;
|
|
312965
312971
|
this.apiKey = options.apiKey;
|
|
@@ -312967,6 +312973,8 @@ var init_blun_media = __esmMin((() => {
|
|
|
312967
312973
|
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
312968
312974
|
this.customHeaders = options.customHeaders ?? {};
|
|
312969
312975
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
312976
|
+
this.pendingMedia = createPendingMediaTracker({ statePath: join(resolveBlunHome$1(), "media", "pending-jobs.json") });
|
|
312977
|
+
activeBlunMediaService = this;
|
|
312970
312978
|
}
|
|
312971
312979
|
generateImage(prompt, options) {
|
|
312972
312980
|
return this.submit("/images/generations", { prompt }, options);
|
|
@@ -313036,6 +313044,9 @@ var init_blun_media = __esmMin((() => {
|
|
|
313036
313044
|
});
|
|
313037
313045
|
}
|
|
313038
313046
|
async submit(path, body, options) {
|
|
313047
|
+
const requestKey = mediaRequestKey(path, body);
|
|
313048
|
+
const reusable = this.pendingMedia.findReusableRequest(requestKey);
|
|
313049
|
+
if (reusable !== void 0) return reusable;
|
|
313039
313050
|
const response = await this.request(path, {
|
|
313040
313051
|
method: "POST",
|
|
313041
313052
|
body: JSON.stringify(body)
|
|
@@ -313043,7 +313054,7 @@ var init_blun_media = __esmMin((() => {
|
|
|
313043
313054
|
if (response.status === 409) throw await blockedSubmissionError(response);
|
|
313044
313055
|
await assertSuccess(response, "Media request");
|
|
313045
313056
|
const job = parseJob(await response.json());
|
|
313046
|
-
this.pendingMedia.observeSubmission(job);
|
|
313057
|
+
this.pendingMedia.observeSubmission(job, requestKey);
|
|
313047
313058
|
return job;
|
|
313048
313059
|
}
|
|
313049
313060
|
trackMediaLookup(result) {
|
|
@@ -313053,6 +313064,9 @@ var init_blun_media = __esmMin((() => {
|
|
|
313053
313064
|
pendingMediaJobIds() {
|
|
313054
313065
|
return this.pendingMedia.ids();
|
|
313055
313066
|
}
|
|
313067
|
+
pendingMediaTracker() {
|
|
313068
|
+
return this.pendingMedia;
|
|
313069
|
+
}
|
|
313056
313070
|
async request(path, init, options) {
|
|
313057
313071
|
const firstToken = await this.resolveToken(false);
|
|
313058
313072
|
const first = await this.fetchWithToken(path, init, options, firstToken);
|
|
@@ -508124,6 +508138,7 @@ var SessionEventHandler = class {
|
|
|
508124
508138
|
mediaKind: mediaKindForToolName(matchedCall.name),
|
|
508125
508139
|
phase: "failed"
|
|
508126
508140
|
});
|
|
508141
|
+
if (matchedCall !== void 0 && matchedCall.name !== "GetMedia" && isMediaToolName(matchedCall.name) && event.isError !== true) this.host.trackChannelMediaJob(event.output);
|
|
508127
508142
|
if (matchedCall?.name === "GetMedia" && event.isError !== true) this.host.runChannelMediaFallback(event.output);
|
|
508128
508143
|
this.subAgentEventHandler.handleAgentSwarmToolResult(event.toolCallId, resultData, event.isError === true);
|
|
508129
508144
|
if (matchedCall !== void 0 && matchedCall.name === "TodoList" && !event.isError) {
|
|
@@ -514706,6 +514721,9 @@ function outboxDeliveredFile(marker, chatId, filePath) {
|
|
|
514706
514721
|
}
|
|
514707
514722
|
var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
|
|
514708
514723
|
({ retryTelegramMediaDelivery, retryableTelegramMediaFailure } = createRequire(import.meta.url)("./bin/telegram-media-delivery-policy.cjs"));
|
|
514724
|
+
var createMediaAutoRetrievalController, parseAcceptedMediaJob, validateCompletedMedia;
|
|
514725
|
+
({ createMediaAutoRetrievalController, parseAcceptedMediaJob } = createRequire(import.meta.url)("./bin/media-auto-retrieval-policy.cjs"));
|
|
514726
|
+
({ validateCompletedMedia } = createRequire(import.meta.url)("./bin/media-result-policy.cjs"));
|
|
514709
514727
|
var { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
|
|
514710
514728
|
const TELEGRAM_TEXT_LIMIT = 4096;
|
|
514711
514729
|
const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
|
|
@@ -514751,6 +514769,26 @@ function completedMediaLocalPath(output) {
|
|
|
514751
514769
|
const candidate = /Media job [^\r\n]+ is complete\. Local file: (.+?)\. The BLUN host/u.exec(text)?.[1]?.trim();
|
|
514752
514770
|
return candidate === void 0 || candidate.length === 0 ? void 0 : safeCompletedMediaPath(candidate);
|
|
514753
514771
|
}
|
|
514772
|
+
function completedMediaJobId(output) {
|
|
514773
|
+
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 : "";
|
|
514774
|
+
return /Media job ([A-Za-z0-9_-]{1,200}) is complete\./u.exec(text)?.[1];
|
|
514775
|
+
}
|
|
514776
|
+
async function saveAutoRetrievedMedia(result) {
|
|
514777
|
+
const quality = validateCompletedMedia(result);
|
|
514778
|
+
if (!quality.ok) {
|
|
514779
|
+
const error = new Error(`Media quality gate rejected ${result.id}: ${quality.reason}`);
|
|
514780
|
+
error.code = "MEDIA_QUALITY_REJECTED";
|
|
514781
|
+
throw error;
|
|
514782
|
+
}
|
|
514783
|
+
const outputDir = mediaDir();
|
|
514784
|
+
await mkdir(outputDir, {
|
|
514785
|
+
recursive: true,
|
|
514786
|
+
mode: 448
|
|
514787
|
+
});
|
|
514788
|
+
const localPath = join(outputDir, `${safeMediaId(result.id)}.${extensionForMediaType(result.mimeType)}`);
|
|
514789
|
+
await writeFile(localPath, result.data, { mode: 384 });
|
|
514790
|
+
return localPath;
|
|
514791
|
+
}
|
|
514754
514792
|
/** Telegram group/supergroup ids are negative; DMs are the positive user id. */
|
|
514755
514793
|
function isGroupChat(chatId) {
|
|
514756
514794
|
return chatId.startsWith("-");
|
|
@@ -515956,6 +515994,7 @@ var BlunTUI = class {
|
|
|
515956
515994
|
mediaActivityStore = new MediaActivityStore();
|
|
515957
515995
|
mediaActivityTickTimer;
|
|
515958
515996
|
mediaActivityExpanded = false;
|
|
515997
|
+
mediaAutoRetrieval;
|
|
515959
515998
|
lastHistoryContent;
|
|
515960
515999
|
inputDraftTimer;
|
|
515961
516000
|
pendingInputDraft;
|
|
@@ -517431,16 +517470,36 @@ var BlunTUI = class {
|
|
|
517431
517470
|
/** See SessionEventHost.runChannelReplyFallback — called at turn end. */
|
|
517432
517471
|
channelMediaDeliveries = /* @__PURE__ */ new Set();
|
|
517433
517472
|
channelMediaDeliveryFailures = /* @__PURE__ */ new Set();
|
|
517473
|
+
ensureMediaAutoRetrieval() {
|
|
517474
|
+
if (this.mediaAutoRetrieval !== void 0) return this.mediaAutoRetrieval;
|
|
517475
|
+
const service = activeBlunMediaService;
|
|
517476
|
+
if (service === void 0) return void 0;
|
|
517477
|
+
this.mediaAutoRetrieval = createMediaAutoRetrievalController({
|
|
517478
|
+
tracker: service.pendingMediaTracker(),
|
|
517479
|
+
getMedia: (id) => service.getMedia(id),
|
|
517480
|
+
saveMedia: saveAutoRetrievedMedia,
|
|
517481
|
+
deliver: sendMediaReplyFallback
|
|
517482
|
+
});
|
|
517483
|
+
return this.mediaAutoRetrieval;
|
|
517484
|
+
}
|
|
517485
|
+
trackChannelMediaJob(output) {
|
|
517486
|
+
const guard = this.pendingChannelReplyGuard;
|
|
517487
|
+
const id = parseAcceptedMediaJob(typeof output === "string" ? output : "");
|
|
517488
|
+
if (guard === void 0 || id === void 0) return;
|
|
517489
|
+
this.ensureMediaAutoRetrieval()?.watch(id, guard.chatId);
|
|
517490
|
+
}
|
|
517434
517491
|
/** Deliver completed media at tool-result time so later queued work cannot hide it. */
|
|
517435
517492
|
runChannelMediaFallback(output) {
|
|
517436
517493
|
const guard = this.pendingChannelReplyGuard;
|
|
517437
517494
|
const filePath = completedMediaLocalPath(output);
|
|
517495
|
+
const jobId = completedMediaJobId(output);
|
|
517438
517496
|
if (guard === void 0 || filePath === void 0) return;
|
|
517439
517497
|
const deliveryKey = `${guard.chatId}\0${filePath}`;
|
|
517440
517498
|
if (this.channelMediaDeliveries.has(deliveryKey)) return;
|
|
517441
517499
|
this.channelMediaDeliveries.add(deliveryKey);
|
|
517442
517500
|
sendMediaReplyFallback(guard.chatId, filePath).then((sent) => {
|
|
517443
517501
|
if (sent) {
|
|
517502
|
+
if (jobId !== void 0) activeBlunMediaService?.pendingMediaTracker().noteDelivered(jobId);
|
|
517444
517503
|
this.channelMediaDeliveryFailures.delete(deliveryKey);
|
|
517445
517504
|
return;
|
|
517446
517505
|
}
|
|
@@ -517496,6 +517555,7 @@ var BlunTUI = class {
|
|
|
517496
517555
|
connectTelegramChannel() {
|
|
517497
517556
|
process.env["BLUN_TELEGRAM_ATTACH"] = "on";
|
|
517498
517557
|
this.startTelegramChannel();
|
|
517558
|
+
this.ensureMediaAutoRetrieval()?.resume();
|
|
517499
517559
|
return this.telegramChannel !== void 0;
|
|
517500
517560
|
}
|
|
517501
517561
|
startTelegramChannel() {
|