@glyphteck/veyl 0.72.0 → 0.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/account.js +3231 -213
- package/dist/accountprofiles.js +2 -0
- package/dist/cli.js +4305 -730
- package/dist/index.js +4304 -729
- package/examples/bot-fleet/policy.js +18 -0
- package/examples/bot-fleet/readme.md +1 -0
- package/examples/bot-fleet/runtime.js +8 -1
- package/examples/bot-fleet/voice.js +157 -0
- package/package.json +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createVoiceRole } from './voice.js';
|
|
2
3
|
|
|
3
4
|
export const BOT_FLEET_ROLES = new Set([
|
|
4
5
|
'read',
|
|
@@ -7,6 +8,7 @@ export const BOT_FLEET_ROLES = new Set([
|
|
|
7
8
|
'traffic',
|
|
8
9
|
'live',
|
|
9
10
|
'typing',
|
|
11
|
+
'voice',
|
|
10
12
|
]);
|
|
11
13
|
const BOT_UNDERFUNDED_TEXT = 'insufficient funds';
|
|
12
14
|
const ATTACHMENT_TYPES = new Set(['img', 'gif', 'm4a', 'mp4', 'file']);
|
|
@@ -520,6 +522,7 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
520
522
|
const echoChatKeys = new Set();
|
|
521
523
|
const claimStates = new Map();
|
|
522
524
|
const liveStates = new Map();
|
|
525
|
+
const voiceStates = new Map();
|
|
523
526
|
|
|
524
527
|
function shouldMirror(account, event) {
|
|
525
528
|
if (isFleetGeneratedMessage(event)) return false;
|
|
@@ -591,9 +594,22 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
591
594
|
if (roles.has('live')) {
|
|
592
595
|
await startLiveRole(account, client, liveStates);
|
|
593
596
|
}
|
|
597
|
+
if (roles.has('voice')) {
|
|
598
|
+
const voice = createVoiceRole(client, current.uid);
|
|
599
|
+
voiceStates.set(account.profile, voice);
|
|
600
|
+
try { await voice.start(); }
|
|
601
|
+
catch (error) {
|
|
602
|
+
voiceStates.delete(account.profile);
|
|
603
|
+
await voice.close();
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
594
607
|
}
|
|
595
608
|
},
|
|
596
609
|
async stop({ account }) {
|
|
610
|
+
const voice = voiceStates.get(account.profile);
|
|
611
|
+
voiceStates.delete(account.profile);
|
|
612
|
+
await voice?.close();
|
|
597
613
|
await stopLiveRole(account, liveStates);
|
|
598
614
|
await stopClaimLoop(account.profile, claimStates);
|
|
599
615
|
const username = usernameByProfile.get(account.profile);
|
|
@@ -605,6 +621,8 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
605
621
|
if (username) managedUsernames.delete(username);
|
|
606
622
|
},
|
|
607
623
|
async stopFleet() {
|
|
624
|
+
await Promise.all([...voiceStates.values()].map(voice => voice.close()));
|
|
625
|
+
voiceStates.clear();
|
|
608
626
|
await Promise.all([...liveStates.keys()].map((profile) =>
|
|
609
627
|
stopLiveRole({ profile, roles: [] }, liveStates)
|
|
610
628
|
));
|
|
@@ -12,6 +12,7 @@ The secret-free version-2 manifest stores account indices, usernames, networks,
|
|
|
12
12
|
- `traffic` is an eligibility marker for an external local operator policy; it performs no work by itself.
|
|
13
13
|
- `live` holds the account's normal encrypted live-room connection in each chat without advancing read state or sending messages. The concrete host supplies the same realm-matched portable live transport used by graphical clients; startup rejects this role when that client port is absent.
|
|
14
14
|
- `typing` requires `live` and continually renews one stable composition through the same encrypted live state until the policy stops.
|
|
15
|
+
- `voice` starts muted and joins the newest active, foreign-occupied call among the account's watched chats. It uses `client.chat.watch(onChats, { count: 500 })`, `client.calls.observe(chatId, onAvailable)`, and strict `joinExisting(chatId, callId)`; it never creates a call or inherits read/echo behavior. The host must supply real `calls.media` and ephemeral `calls.mls` ports. One shared owner admits the new destination before leaving an old call, supports cancellation, and leaves when no other account remains. A failed unchanged room is not retried on every heartbeat.
|
|
15
16
|
|
|
16
17
|
Only response-capable fleet peers are loop-suppressed. Incoming transaction events and staggered fallback loops call the public wallet claim method. Startup replays a bounded visible window; an event checkpoint suppresses completed source messages, while the action journal suppresses completed effects and stops ambiguous payments for explicit reconciliation.
|
|
17
18
|
|
|
@@ -24,6 +24,13 @@ export async function resolveBotFleetClientOptions(source, profile) {
|
|
|
24
24
|
) {
|
|
25
25
|
throw new Error(`live bot transport required: ${profile.profile}`);
|
|
26
26
|
}
|
|
27
|
+
if (profile?.roles?.includes('voice') && (
|
|
28
|
+
typeof options?.calls?.media?.open !== 'function'
|
|
29
|
+
|| typeof options?.calls?.media?.prepare !== 'function'
|
|
30
|
+
|| typeof options?.calls?.mls?.createMember !== 'function'
|
|
31
|
+
)) {
|
|
32
|
+
throw new Error(`voice bot media and encryption ports required: ${profile.profile}`);
|
|
33
|
+
}
|
|
27
34
|
return options;
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -41,7 +48,7 @@ export async function createExampleBotFleetRuntime(options = {}) {
|
|
|
41
48
|
policies: [],
|
|
42
49
|
eventOptions: async (profile) => ({
|
|
43
50
|
replay: true,
|
|
44
|
-
chats: profile.roles.
|
|
51
|
+
chats: profile.roles.some(role => ['read', 'live', 'voice'].includes(role)),
|
|
45
52
|
persistentChats: true,
|
|
46
53
|
persistentChatIdleMs: options.persistentChatIdleMs || 5 * 60_000,
|
|
47
54
|
transactions: true,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
function sameRoom(left, right) {
|
|
2
|
+
return !!left && !!right
|
|
3
|
+
&& left.chatId === right.chatId && left.callId === right.callId;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function availableRoom(value, chatId, uid) {
|
|
7
|
+
if (value?.chatId !== chatId || !value.callId || !Number.isFinite(value.startedAt)) return null;
|
|
8
|
+
const others = (value.roster || []).filter(peer => peer.uid && peer.uid !== uid);
|
|
9
|
+
if (!others.length) return null;
|
|
10
|
+
// Own admission and audio preferences do not constitute a new opportunity
|
|
11
|
+
// to retry a failed room. Only a new call or changed foreign membership does.
|
|
12
|
+
const members = others.map(peer => [peer.uid, peer.chatPK, peer.holder]).sort();
|
|
13
|
+
return {
|
|
14
|
+
chatId, callId: value.callId, startedAt: value.startedAt,
|
|
15
|
+
fingerprint: JSON.stringify([value.callId, value.startedAt, members]),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createVoiceRole(client, uid) {
|
|
20
|
+
if (!uid) throw new Error('voice bot account identity required');
|
|
21
|
+
const calls = client.calls;
|
|
22
|
+
const chats = new Map();
|
|
23
|
+
let closed = false;
|
|
24
|
+
let dirty = false;
|
|
25
|
+
let running = null;
|
|
26
|
+
let startup = null;
|
|
27
|
+
let stopping = null;
|
|
28
|
+
let pending = null;
|
|
29
|
+
let releaseChats = null;
|
|
30
|
+
let releaseCalls = null;
|
|
31
|
+
let lastError = null;
|
|
32
|
+
|
|
33
|
+
function report(error) {
|
|
34
|
+
const code = /^[a-z0-9/-]{1,80}$/u.test(error?.code || '') ? error.code : 'calls/failed';
|
|
35
|
+
client.diag?.('bot.voice.error', { code });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function newest() {
|
|
39
|
+
let selected = null;
|
|
40
|
+
for (const entry of chats.values()) {
|
|
41
|
+
const room = entry.room;
|
|
42
|
+
if (!room || entry.failed === room.fingerprint) continue;
|
|
43
|
+
if (!selected || room.startedAt > selected.startedAt
|
|
44
|
+
|| (room.startedAt === selected.startedAt && room.chatId > selected.chatId)) selected = room;
|
|
45
|
+
}
|
|
46
|
+
return selected;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fail(room, error) {
|
|
50
|
+
const entry = chats.get(room?.chatId);
|
|
51
|
+
if (sameRoom(entry?.room, room)) entry.failed = entry.room.fingerprint;
|
|
52
|
+
if (error?.code !== 'calls/cancelled') report(error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function reconcile() {
|
|
56
|
+
const room = newest();
|
|
57
|
+
const current = calls.getSnapshot();
|
|
58
|
+
if (!room) {
|
|
59
|
+
if (current.joiningChatId || !['idle', 'error'].includes(current.phase)) await calls.leave();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// callId identifies the admitted instance even while ICE is joining or
|
|
63
|
+
// waiting. Only joiningChatId represents an unadmitted candidate.
|
|
64
|
+
if (sameRoom(current, room) && !['idle', 'error'].includes(current.phase)) return;
|
|
65
|
+
const attempt = { ...room, cancelled: false };
|
|
66
|
+
pending = attempt;
|
|
67
|
+
try {
|
|
68
|
+
// The shared owner admits the destination before retiring an
|
|
69
|
+
// existing call. A stale discovery can never create a fresh call.
|
|
70
|
+
await calls.joinExisting(room.chatId, room.callId);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!closed && !attempt.cancelled) fail(room, error);
|
|
73
|
+
} finally {
|
|
74
|
+
if (pending === attempt) pending = null;
|
|
75
|
+
dirty = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function changed() {
|
|
80
|
+
if (closed) return;
|
|
81
|
+
dirty = true;
|
|
82
|
+
if (pending && !pending.cancelled && !sameRoom(pending, newest())) {
|
|
83
|
+
pending.cancelled = true;
|
|
84
|
+
void calls.cancelJoin().catch(report);
|
|
85
|
+
}
|
|
86
|
+
if (running) return;
|
|
87
|
+
running = Promise.resolve().then(async () => {
|
|
88
|
+
while (dirty && !closed) {
|
|
89
|
+
dirty = false;
|
|
90
|
+
await reconcile();
|
|
91
|
+
}
|
|
92
|
+
}).catch(report).finally(() => {
|
|
93
|
+
running = null;
|
|
94
|
+
if (dirty && !closed) changed();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function syncChats(values) {
|
|
99
|
+
if (closed) return;
|
|
100
|
+
const ids = new Set(values.filter(chat => !chat.messageRequest && !chat.membershipRemoved).map(chat => chat.id));
|
|
101
|
+
for (const [chatId, entry] of chats) {
|
|
102
|
+
if (ids.has(chatId)) continue;
|
|
103
|
+
chats.delete(chatId);
|
|
104
|
+
entry.release();
|
|
105
|
+
}
|
|
106
|
+
for (const chatId of ids) {
|
|
107
|
+
if (chats.has(chatId)) continue;
|
|
108
|
+
const entry = { room: null, failed: null, release: () => {} };
|
|
109
|
+
chats.set(chatId, entry);
|
|
110
|
+
entry.release = calls.observe(chatId, value => {
|
|
111
|
+
if (closed || chats.get(chatId) !== entry) return;
|
|
112
|
+
const room = availableRoom(value, chatId, uid);
|
|
113
|
+
if (entry.room?.fingerprint === room?.fingerprint) return;
|
|
114
|
+
entry.room = room;
|
|
115
|
+
entry.failed = null;
|
|
116
|
+
changed();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
changed();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
start() {
|
|
124
|
+
startup ||= (async () => {
|
|
125
|
+
await calls.setMuted(true);
|
|
126
|
+
if (closed) return;
|
|
127
|
+
releaseCalls = calls.subscribe(() => {
|
|
128
|
+
const { error } = calls.getSnapshot();
|
|
129
|
+
if (error && error !== lastError && error.code !== 'calls/cancelled') {
|
|
130
|
+
lastError = error;
|
|
131
|
+
const entry = chats.get(error.chatId);
|
|
132
|
+
if (entry?.room) entry.failed = entry.room.fingerprint;
|
|
133
|
+
report(error);
|
|
134
|
+
}
|
|
135
|
+
changed();
|
|
136
|
+
});
|
|
137
|
+
const release = await client.chat.watch(syncChats, { count: 500 });
|
|
138
|
+
if (closed) release();
|
|
139
|
+
else releaseChats = release;
|
|
140
|
+
})();
|
|
141
|
+
return startup;
|
|
142
|
+
},
|
|
143
|
+
close() {
|
|
144
|
+
if (stopping) return stopping;
|
|
145
|
+
closed = true;
|
|
146
|
+
releaseCalls?.();
|
|
147
|
+
releaseChats?.();
|
|
148
|
+
for (const entry of chats.values()) entry.release();
|
|
149
|
+
chats.clear();
|
|
150
|
+
// leave synchronously revokes local playback/join intent. A late
|
|
151
|
+
// watch setup or old join completion cannot retain this role.
|
|
152
|
+
const leaving = calls.leave();
|
|
153
|
+
stopping = Promise.allSettled([startup, running, leaving]).then(() => undefined);
|
|
154
|
+
return stopping;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"examples/bot-fleet/index.js",
|
|
26
26
|
"examples/bot-fleet/policy.js",
|
|
27
27
|
"examples/bot-fleet/runtime.js",
|
|
28
|
+
"examples/bot-fleet/voice.js",
|
|
28
29
|
"examples/bot-fleet/readme.md",
|
|
29
30
|
"examples/codex-agent/agent-instructions.js",
|
|
30
31
|
"examples/codex-agent/agent-state.js",
|
|
@@ -53,5 +54,5 @@
|
|
|
53
54
|
"start": "node src/cli.js",
|
|
54
55
|
"lint": "eslint src --quiet"
|
|
55
56
|
},
|
|
56
|
-
"version": "0.
|
|
57
|
+
"version": "0.73.0"
|
|
57
58
|
}
|