blun-king-cli 9.1.394 → 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/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
|
@@ -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() {
|