@kybernesis/buzz 0.4.3 → 0.5.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/bridge.js +71 -6
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -39,6 +39,13 @@ export function buzzBridge(options) {
|
|
|
39
39
|
* same conversation through a client built for the person sending it, which
|
|
40
40
|
* is what the runtime reads to decide whose memory and whose connections the
|
|
41
41
|
* turn may touch.
|
|
42
|
+
*
|
|
43
|
+
* The stream position is kept beside the id for a reason that cost an
|
|
44
|
+
* afternoon: attaching defaults to index 0, so a turn reads the conversation
|
|
45
|
+
* from its beginning and reports the FIRST completed message it finds — the
|
|
46
|
+
* previous person's answer. The turn itself runs correctly, as the right
|
|
47
|
+
* person, against the right connections; only the text posted back is
|
|
48
|
+
* somebody else's, which is the most convincing way to look broken.
|
|
42
49
|
*/
|
|
43
50
|
const sessions = new Map();
|
|
44
51
|
/** Who has already been sent a link, so a room full of strangers is not a room full of spam. */
|
|
@@ -51,15 +58,60 @@ export function buzzBridge(options) {
|
|
|
51
58
|
auth: { bearer: token },
|
|
52
59
|
headers: bundle ? { "x-kybernesis-bundle": bundle } : {},
|
|
53
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* One turn at a time per channel.
|
|
63
|
+
*
|
|
64
|
+
* Two people asking at once used to interleave: both turns read and wrote the
|
|
65
|
+
* same stored stream position, so one of them read the other's boundary and
|
|
66
|
+
* came back with the wrong answer or none. A channel is a conversation; its
|
|
67
|
+
* turns are ordered whether or not the people in it take turns.
|
|
68
|
+
*/
|
|
69
|
+
const inFlight = new Map();
|
|
70
|
+
function serialize(channel, work) {
|
|
71
|
+
const queued = (inFlight.get(channel) ?? Promise.resolve()).then(work, work);
|
|
72
|
+
// Kept only while it matters: the chain holds the LAST promise, not a history.
|
|
73
|
+
inFlight.set(channel, queued.catch(() => { }));
|
|
74
|
+
return queued;
|
|
75
|
+
}
|
|
54
76
|
async function ask(channel, text, token, bundle) {
|
|
55
77
|
// Built per turn, for the person whose turn it is.
|
|
56
78
|
const client = clientFor(token, bundle);
|
|
57
79
|
const existing = sessions.get(channel);
|
|
58
80
|
if (existing) {
|
|
59
81
|
try {
|
|
60
|
-
const session = client.sessions.attach(existing);
|
|
82
|
+
const session = client.sessions.attach(existing.id, { streamIndex: existing.streamIndex });
|
|
83
|
+
/**
|
|
84
|
+
* Move to the true end of the conversation before speaking.
|
|
85
|
+
*
|
|
86
|
+
* `send()` opens its response stream at the position the handle already
|
|
87
|
+
* holds and stops at the FIRST turn boundary it meets. If anything is
|
|
88
|
+
* still unread there — the tail of a turn that outlived its reader, a
|
|
89
|
+
* turn that was running when the bridge restarted — that boundary
|
|
90
|
+
* belongs to the older turn. The read ends on it, this turn's answer is
|
|
91
|
+
* never collected, and the stored position stays one turn behind.
|
|
92
|
+
*
|
|
93
|
+
* Which makes it permanent: every later turn reads the previous turn's
|
|
94
|
+
* tail. The channel answers nothing, then answers a question from ten
|
|
95
|
+
* minutes ago, and a fresh conversation elsewhere works perfectly —
|
|
96
|
+
* because a fresh conversation has nothing left over to trip on.
|
|
97
|
+
*
|
|
98
|
+
* Draining first costs one bounded read of only what is unread, and it
|
|
99
|
+
* repairs a position that has already drifted rather than requiring
|
|
100
|
+
* anyone to notice. Non-following, so it ends at the tail instead of
|
|
101
|
+
* waiting for the future.
|
|
102
|
+
*/
|
|
103
|
+
let unread = 0;
|
|
104
|
+
for await (const _ of session.stream({ follow: false, startIndex: existing.streamIndex })) {
|
|
105
|
+
unread += 1;
|
|
106
|
+
}
|
|
107
|
+
if (unread > 0) {
|
|
108
|
+
log(`caught up on ${unread} unread event(s) in ${channel.slice(0, 8)} before answering`);
|
|
109
|
+
}
|
|
61
110
|
const response = await session.send(text);
|
|
62
|
-
|
|
111
|
+
const result = await response.result();
|
|
112
|
+
// Where this turn left the conversation, so the next one starts after it.
|
|
113
|
+
sessions.set(channel, { id: existing.id, streamIndex: session.state.streamIndex });
|
|
114
|
+
return result.message ?? "";
|
|
63
115
|
}
|
|
64
116
|
catch (error) {
|
|
65
117
|
// A session the agent no longer holds cannot be resumed. Starting a new one loses the
|
|
@@ -69,8 +121,12 @@ export function buzzBridge(options) {
|
|
|
69
121
|
}
|
|
70
122
|
}
|
|
71
123
|
const created = await client.sessions.create({ message: text });
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
const message = (await created.response.result()).message ?? "";
|
|
125
|
+
sessions.set(channel, {
|
|
126
|
+
id: created.session.state.sessionId,
|
|
127
|
+
streamIndex: created.session.state.streamIndex,
|
|
128
|
+
});
|
|
129
|
+
return message;
|
|
74
130
|
}
|
|
75
131
|
/**
|
|
76
132
|
* Send someone the link that makes them known, privately.
|
|
@@ -156,9 +212,18 @@ export function buzzBridge(options) {
|
|
|
156
212
|
void relay.react(event.id, SEEN);
|
|
157
213
|
const stopTyping = relay.typingIn(channel);
|
|
158
214
|
try {
|
|
159
|
-
const reply = await ask(channel, text, speaker.token, speaker.bundle);
|
|
215
|
+
const reply = await serialize(channel, () => ask(channel, text, speaker.token, speaker.bundle));
|
|
160
216
|
if (!reply) {
|
|
161
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Silence is the worst answer available.
|
|
219
|
+
*
|
|
220
|
+
* An empty result used to end here, with a line in a log nobody was
|
|
221
|
+
* reading. In a room that is indistinguishable from the agent ignoring
|
|
222
|
+
* you — so people ask again, which is how one unanswered question
|
|
223
|
+
* became five, none of which looked like a fault to anyone watching.
|
|
224
|
+
*/
|
|
225
|
+
log(`no text for ${channel.slice(0, 8)} — telling them rather than going quiet`);
|
|
226
|
+
relay.reply(channel, "I didn't get an answer back for that one — ask me again and I'll retry.", event);
|
|
162
227
|
return;
|
|
163
228
|
}
|
|
164
229
|
relay.reply(channel, reply, event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/buzz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Put an eve agent in a Buzz workspace as a member: per-speaker verified identity resolved through the control plane, self-service identity linking, presence, typing and seen signals.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|