@glyphteck/veyl 0.71.3 → 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 +17822 -3205
- package/dist/accountprofiles.js +19 -1
- package/dist/auth.js +14 -0
- package/dist/cli.js +20743 -5236
- package/dist/index.js +20742 -5235
- package/docs/api.md +9 -3
- package/docs/cli.md +1 -1
- package/docs/validation.md +1 -9
- package/examples/bot-fleet/policy.js +22 -1
- 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
package/docs/api.md
CHANGED
|
@@ -25,6 +25,8 @@ The auth and account compositions consume explicit runtime ports and have no dir
|
|
|
25
25
|
|
|
26
26
|
Most product methods ensure login and vault unlock when saved account and vault keys are available. Long-running callers should explicitly login/unlock once and call `close()` when finished.
|
|
27
27
|
|
|
28
|
+
`open({ onOperation(event) })` can observe local firebase operations, default chat/presence WebSocket frame counts, and SDK wallet-claim calls. events contain operation names, lifecycle status, counts, latency, and cache/pending-write flags; they omit payloads, document paths, account/chat IDs, and capabilities. this hook observes the default adapters, not custom cloud/live ports or server internals, and its counts are not billing units. wallet-claim observations count the SDK invocation, not Spark's internal requests. see the repository's `costs/benchmarks.md` for controlled dev scenarios and provider reconciliation.
|
|
29
|
+
|
|
28
30
|
### Local access and connectivity
|
|
29
31
|
|
|
30
32
|
The shared account owner separates `localReady`, `online`, and `connection` in its snapshot. `localReady` means the exact locally signed-in account has enough encrypted bootstrap data for vault unlock; it is not server authorization. Unlock opens encrypted cached chats and wallet history without waiting for cloud services. Cached balance is display-only and must be labeled last known. `online` becomes true after cloud reads and online proof succeed; reconnect attaches services to the same unlocked session. Text sends wait up to 30 seconds for connectivity, then remain encrypted on-device as failed attempts for manual retry; reactions expire without a retry row. Avatar changes wait only for the current session, as described below. These are domain-owned pending operations, not a general cloud mutation queue. Live wallet readiness remains mandatory for payments.
|
|
@@ -174,7 +176,7 @@ await account.close();
|
|
|
174
176
|
|
|
175
177
|
`account.peers` is the shared peer-directory owner used by Node, web, and iOS. `openAccount()` supplies its chat, wallet, blocked-user, and encrypted-cache sources directly, including missing-profile chat cleanup. Graphical adapters only subscribe to the same snapshot and profile selectors already defined by core. `openSearch('profiles')` creates profile-only search; web may also use `openSearch('mainmenu')` for its combined local-action and remote-profile menu. Active searches track peer and blocked-user changes and release those subscriptions when cleared or closed.
|
|
176
178
|
|
|
177
|
-
`account.chat` is the one account-scoped protocol-
|
|
179
|
+
`account.chat` is the one account-scoped protocol-4 chat owner used by Node, web, and iOS. It owns notes/direct/group creation, stable logical chat IDs, membership epochs, encrypted list/inbox convergence, epoch-spanning history, delivery, messages, receipts, signed encrypted chat settings, member changes, leaving, and deletion. It clears every route, cache, timer, and key-bearing state on lock/account switch. Graphical providers only expose this owner through React; platform media preparation and native behavior enter through explicit ports.
|
|
178
180
|
|
|
179
181
|
`account.wallet` is the stable account-scoped wallet composition used by Node, web, and iOS. It owns the one live `core/wallet/session.js` engine, its transfer store, cached pre-Spark display state, transaction aggregation, wallet-derived peer recency, late Spark attachment, and lock teardown. `getSnapshot()` preserves the shared `{ value, txValue }` contract; `transactions` exposes aggregate/search/chart data and `transfers` exposes focused list and keyed subscriptions. Graphical wallet providers only adapt those SDK subscriptions and add browser/native behavior through the `wallet` platform ports.
|
|
180
182
|
|
|
@@ -391,11 +393,15 @@ await veyl.chat.sendAttachment('@alice', {
|
|
|
391
393
|
});
|
|
392
394
|
|
|
393
395
|
await veyl.chat.sendAttachmentMany(['@alice', '@bob'], attachment);
|
|
394
|
-
await veyl.chat.
|
|
396
|
+
await veyl.chat.forward('@alice', messageId, ['@bob']);
|
|
395
397
|
const downloaded = await veyl.chat.readAttachment('@alice', messageId);
|
|
396
398
|
```
|
|
397
399
|
|
|
398
|
-
|
|
400
|
+
the caller supplies/consumes bytes; shared owners derive paths, encrypt/decrypt, upload/download, create message actions, and project media cache state. `chat.forward` forwards an existing attachment. forwarded attachments cannot be saved forever, and forwarding reuses the original encrypted object without extending its expiry or transmitting management authority. participants in the original conversation keep the existing save/unsave authority.
|
|
401
|
+
|
|
402
|
+
audio attachment objects may include `duration` in fractional seconds and optional `waveform`: canonical padded base64 encoding of 32 unsigned bytes, representing peak-normalized rms amplitude in equal-duration bins. compact audio events expose the same validated `message.waveform`, and sharing/echoing should preserve it. generic sdk/cli uploads do not decode audio to generate it; the command schema does not accept it, and graphical clients display a stable generic waveform when it is absent. that placeholder is presentation-only and is never attached to a message as measured audio. real waveform data stays inside the encrypted message.
|
|
403
|
+
|
|
404
|
+
media metadata is removed automatically before encryption, including when recognizable media is sent as a generic file. supply prepared jpeg, png, webp, gif, or supported mp4/mov/m4a bytes; video must use h.264 or hevc. normalize other image/video formats first. raw heic/avif/webm, unsupported movie tracks/codecs, and malformed media reject with `media-sanitization-failed` instead of uploading the original. orientation, color transforms, animation timing, audio/video synchronization, and display matrices are retained. this does not scrub ordinary documents, visible/audible content, or authored filenames/captions. public `account.profile.setAvatar` also sanitizes prepared webp bytes before publishing.
|
|
399
405
|
|
|
400
406
|
## Invites
|
|
401
407
|
|
package/docs/cli.md
CHANGED
|
@@ -117,7 +117,7 @@ Attachments use filesystem paths in the CLI while the shared owners still perfor
|
|
|
117
117
|
```bash
|
|
118
118
|
veyl chat send-file @alice ./photo.webp --mime image/webp --caption "caption"
|
|
119
119
|
veyl chat send-file-many @alice,@bob ./document.pdf --name document.pdf
|
|
120
|
-
veyl chat
|
|
120
|
+
veyl chat forward @alice MESSAGE_ID @bob,@carol
|
|
121
121
|
veyl chat download @alice MESSAGE_ID ./downloaded-file
|
|
122
122
|
```
|
|
123
123
|
|
package/docs/validation.md
CHANGED
|
@@ -169,15 +169,7 @@ Cold one-off commands are inherently inefficient for agent loops because Spark b
|
|
|
169
169
|
|
|
170
170
|
## Server-operation cost shape
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
Protocol v3 separates constant content cost from recipient availability cost. One visible action commits one ciphertext, one logical-chat budget update, and at most one coalesced sender owner row: 6 planning reads and 3 writes independent of group size. Each accepted recipient wake adds two route/slot reads, one minimum push-route query when APNs is due, one overwritten slot write, and one Function invocation. The default one-edge/direct upper bound is therefore 9 reads, 4 writes, and 1 invocation. Read receipts add one shared 6-read/2-write action with no wake.
|
|
175
|
-
|
|
176
|
-
The current one-edge planning bundle is about 542 Firestore read-equivalent operations, 163 writes, 28 Function invocations, and one Storage Class A operation per active user/day. Headline projections are about `$2,519/month` at 100k DAU and `$25,217/month` at 1M DAU. At a sustained one visible send per second, the model is `$33.70` gross per month, or `$52.36` with one receipt per send. Set `MESSAGE_RECIPIENT_EDGES` to model larger groups; sender, wake-slot, and APNs coalescing reduce the isolated-send upper bound.
|
|
177
|
-
|
|
178
|
-
Earlier live Cloud Run measurements covered the retired direct-chat delivery contract and must not be treated as v3 evidence. Current source uses anonymous opaque capability delivery, a recipient-owned overwritten slot, prompt-plus-trailing sender burst coalescing, a 250 ms slot debounce, and a 2 s APNs debounce. Dev traces must establish the real accepted-wake ratio before production projections are tightened.
|
|
179
|
-
|
|
180
|
-
These totals intentionally exclude Function CPU/memory duration, outbound network, APNS delivery, Spark/vendor/network fees, media download bytes, and moderation labor until real rates are measured.
|
|
172
|
+
the repository's `costs/README.md` owns cost documentation, `costs/model.mjs` owns the partial workload calculation, and `costs/dau-scale.md` contains generated projections. packaged SDK validation deliberately does not duplicate their prices or operation coefficients. source-level behavior and dated runtime samples do not establish current billing.
|
|
181
173
|
|
|
182
174
|
The live L1 path confirmed that withdrawal, confirmed-UTXO discovery, claim, balance, and transfer status do not call a Veyl wallet Function or write wallet state to Firestore. They are client-local Spark SDK operations against Spark/network services. Active unlocked wallets currently run one identity-scoped deposit check every 20 seconds on REGTEST or every minute on MAINNET; a pending cooperative exit adds one exact-transfer Spark check at most every 15 seconds for ten minutes. Neither cadence is a Firebase cost. The sample consumed 2,190 sats for cooperative exit and 99 sats for the static claim, while all 3,711 recoverable sats were returned to the faucet.
|
|
183
175
|
|
|
@@ -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,12 +8,13 @@ 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']);
|
|
13
15
|
const NORMAL_CLAIM_INTERVAL_MS = 5 * 60_000;
|
|
14
16
|
const FAUCET_CLAIM_INTERVAL_MS = 30_000;
|
|
15
|
-
const MAX_CLAIM_BACKOFF_MS =
|
|
17
|
+
const MAX_CLAIM_BACKOFF_MS = 30 * 60_000;
|
|
16
18
|
const TYPING_HEARTBEAT_MS = 2_000;
|
|
17
19
|
|
|
18
20
|
function rolesFor(account) {
|
|
@@ -236,6 +238,9 @@ async function mirrorAttachment(account, client, event) {
|
|
|
236
238
|
height: event.message.height || payload.h,
|
|
237
239
|
duration: event.message.duration || payload.d,
|
|
238
240
|
audio: typeof event.message.audio === 'boolean' ? event.message.audio : payload.audio,
|
|
241
|
+
...(event.message.type === 'm4a' && event.message.waveform
|
|
242
|
+
? { waveform: event.message.waveform }
|
|
243
|
+
: {}),
|
|
239
244
|
}, {
|
|
240
245
|
cid: botActionCid(event, 'echo', account),
|
|
241
246
|
});
|
|
@@ -517,6 +522,7 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
517
522
|
const echoChatKeys = new Set();
|
|
518
523
|
const claimStates = new Map();
|
|
519
524
|
const liveStates = new Map();
|
|
525
|
+
const voiceStates = new Map();
|
|
520
526
|
|
|
521
527
|
function shouldMirror(account, event) {
|
|
522
528
|
if (isFleetGeneratedMessage(event)) return false;
|
|
@@ -588,9 +594,22 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
588
594
|
if (roles.has('live')) {
|
|
589
595
|
await startLiveRole(account, client, liveStates);
|
|
590
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
|
+
}
|
|
591
607
|
}
|
|
592
608
|
},
|
|
593
609
|
async stop({ account }) {
|
|
610
|
+
const voice = voiceStates.get(account.profile);
|
|
611
|
+
voiceStates.delete(account.profile);
|
|
612
|
+
await voice?.close();
|
|
594
613
|
await stopLiveRole(account, liveStates);
|
|
595
614
|
await stopClaimLoop(account.profile, claimStates);
|
|
596
615
|
const username = usernameByProfile.get(account.profile);
|
|
@@ -602,6 +621,8 @@ export function createBotFleetPolicy(options = {}) {
|
|
|
602
621
|
if (username) managedUsernames.delete(username);
|
|
603
622
|
},
|
|
604
623
|
async stopFleet() {
|
|
624
|
+
await Promise.all([...voiceStates.values()].map(voice => voice.close()));
|
|
625
|
+
voiceStates.clear();
|
|
605
626
|
await Promise.all([...liveStates.keys()].map((profile) =>
|
|
606
627
|
stopLiveRole({ profile, roles: [] }, liveStates)
|
|
607
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
|
}
|