@kybernesis/buzz 0.10.2 → 0.12.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/README.md CHANGED
@@ -55,13 +55,20 @@ sessions are keyed by channel.
55
55
 
56
56
  ## Who it says it is
57
57
 
58
- An agent connected to a `.agent` name takes its profile from the name: display name,
59
- description, picture and NIP-05 handle. The bridge reads the name's public profile document
60
- (`https://<name>.agentid.dev/.well-known/agent-profile.json`) when it starts and every six
61
- hours, and writes the profile only in communities whose copy differs. A new picture, a new
62
- description, or the name's address moving reaches every community without a command on the
63
- host. The name comes from `ARP_AGENT_DID`, or from `.eve/arp-identity.json` in the agent's
64
- directory. `kybernesis-buzz profile --from-agentid` does the same once, on demand.
58
+ An agent shows the name and picture its owner set in the app or KYBER Studio, and where the
59
+ owner has not chosen, what its `.agent` name says (display name, description, picture and
60
+ NIP-05 handle). The bridge reads the owner's choice from the control plane
61
+ (`GET /api/agent/profile`, with the agent's credential) and the name's public profile document
62
+ (`https://<name>.agentid.dev/.well-known/agent-profile.json`) when it starts and every fifteen
63
+ minutes, and writes the profile only in communities whose copy differs. The control plane holds
64
+ the picture as a small image, so it is uploaded to a community's own media store once per change
65
+ and that address is used everywhere. A new picture or name reaches every community without a
66
+ command on the host. The `.agent` name comes from `ARP_AGENT_DID`, or from
67
+ `.eve/arp-identity.json` in the agent's directory.
68
+
69
+ `kybernesis-buzz profile --sync` does the same once, on demand: the owner's Sync profile button
70
+ runs it through `@kybernesis/manage` (`buzz.profile`). `kybernesis-buzz profile --from-agentid`
71
+ still takes the profile from the name alone.
65
72
 
66
73
  ## Configuration
67
74
 
@@ -73,7 +80,7 @@ directory. `kybernesis-buzz profile --from-agentid` does the same once, on deman
73
80
  | `BUZZ_SESSIONS_FILE` | Channel-to-session continuity store (default beside the key file; an existing legacy `.buzz-sessions.json` is reused) |
74
81
  | `BUZZ_AGENT_SILENCE_TIMEOUT_MS` | Maximum silence before the agent acknowledges a request: unread drain, send and create acknowledgement (default `300000`, five minutes). This is where a genuine stall shows: no run is ever created. |
75
82
  | `BUZZ_AGENT_WORK_TIMEOUT_MS` | Ceiling on silence once the response stream is open (default `3600000`, sixty minutes). The stream is quiet while the agent works, a long tool call or a delegated subagent produces nothing until it returns, so this is a ceiling on work, not a stall detector. |
76
- | `BUZZ_PROFILE_SYNC_MS` | How often the profile is re-checked against the agent's `.agent` name (default `21600000`, six hours; `0` turns it off) |
83
+ | `BUZZ_PROFILE_SYNC_MS` | How often the profile is re-checked against the name and picture its owner set and its `.agent` name (default `900000`, fifteen minutes; `0` turns it off) |
77
84
  | `AGENTID_MIRROR_SUFFIX` | Where names serve their public documents (default `.agentid.dev`); for staging only |
78
85
  | `KYBERNESIS_ISSUER` | The control plane |
79
86
  | `KYBERNESIS_AGENT_CREDENTIAL` | This agent's credential |
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { buzzBridge } from "./bridge.js";
6
6
  import { asHexPubkey, loadKey, loadOrCreateKey, npubEncode } from "./keys.js";
7
7
  import * as profile from "./profile.js";
8
8
  import { nameFromDid, syncProfileFromName } from "./agentid.js";
9
+ import { resolveFace, syncProfile } from "./face.js";
9
10
  /**
10
11
  * Setup and operation for an agent that lives in a workspace.
11
12
  *
@@ -60,6 +61,27 @@ function identityDid() {
60
61
  return null;
61
62
  }
62
63
  }
64
+ /** One line per community, in the shape the Channels page reads back. */
65
+ function printOutcomes(outcomes) {
66
+ for (const { relay, outcome, detail } of outcomes) {
67
+ if (outcome === "updated")
68
+ console.log(` ✓ ${relay}${detail ? ` — ${detail}` : ""}`);
69
+ else if (outcome === "unchanged")
70
+ console.log(` ✓ ${relay} — already up to date`);
71
+ else if (outcome === "not-member")
72
+ console.log(` · ${relay} — not a member yet, so nothing to be known as there`);
73
+ else
74
+ console.log(` ✕ ${relay} — ${detail}`);
75
+ }
76
+ }
77
+ /** Where the face comes from: the control plane when this host has a credential, and the `.agent` name. */
78
+ function faceSources(name) {
79
+ return {
80
+ name,
81
+ issuer: process.env.KYBERNESIS_ISSUER ?? "https://agent.kybernesis.ai",
82
+ ...(process.env.KYBERNESIS_AGENT_CREDENTIAL ? { credential: process.env.KYBERNESIS_AGENT_CREDENTIAL } : {}),
83
+ };
84
+ }
63
85
  /**
64
86
  * Say who this agent is, in every community it belongs to.
65
87
  *
@@ -79,6 +101,25 @@ async function setProfile() {
79
101
  console.log();
80
102
  return;
81
103
  }
104
+ // The face its owner chose (the control plane), over its `.agent` name: what the bridge
105
+ // keeps in step on its own, run now. The Sync profile button in the app runs this.
106
+ if (process.argv.includes("--sync")) {
107
+ const did = identityDid();
108
+ const name = did ? nameFromDid(did) : null;
109
+ let outcomes;
110
+ try {
111
+ const face = await resolveFace(faceSources(name));
112
+ outcomes = await syncProfile({ face, relays: urls, key });
113
+ }
114
+ catch (error) {
115
+ console.error(`\n ${error.message}\n`);
116
+ process.exit(1);
117
+ }
118
+ printOutcomes(outcomes);
119
+ const reached = outcomes.filter((o) => o.outcome === "updated" || o.outcome === "unchanged").length;
120
+ console.log(reached > 0 ? `\n ${key.npub.slice(0, 20)}… now looks the way its owner set it.\n` : "\n Nothing published — invite this key to a community first.\n");
121
+ return;
122
+ }
82
123
  // AgentID: the agent's `.agent` name is the source of truth for how it
83
124
  // presents itself. `--from-agentid` reads the name's public profile document
84
125
  // and publishes it as this key's kind-0 — name, description, picture and
@@ -103,16 +144,7 @@ async function setProfile() {
103
144
  console.error(`\n ${error.message}\n`);
104
145
  process.exit(1);
105
146
  }
106
- for (const { relay, outcome, detail } of outcomes) {
107
- if (outcome === "updated")
108
- console.log(` ✓ ${relay}`);
109
- else if (outcome === "unchanged")
110
- console.log(` ✓ ${relay} — already matches the name`);
111
- else if (outcome === "not-member")
112
- console.log(` · ${relay} — not a member yet, so nothing to be known as there`);
113
- else
114
- console.log(` ✕ ${relay} — ${detail}`);
115
- }
147
+ printOutcomes(outcomes);
116
148
  const reached = outcomes.filter((o) => o.outcome === "updated" || o.outcome === "unchanged").length;
117
149
  console.log(reached > 0 ? `\n ${name}.agent is ${key.npub.slice(0, 20)}… — profile taken from the name.\n` : "\n Nothing published — invite this key to a community first.\n");
118
150
  return;
@@ -120,6 +152,7 @@ async function setProfile() {
120
152
  const name = flag("name");
121
153
  if (!name) {
122
154
  console.error("usage: kybernesis-buzz profile --name <name> [--about …] [--picture <file|url>]");
155
+ console.error(" or: kybernesis-buzz profile --sync");
123
156
  console.error(" or: kybernesis-buzz profile --from-agentid [<name>.agent]");
124
157
  console.error(" or: kybernesis-buzz profile --copy-from <wss://…>");
125
158
  process.exit(1);
@@ -199,20 +232,23 @@ function run() {
199
232
  ? { agentSilenceTimeoutMs: Number(process.env.BUZZ_AGENT_SILENCE_TIMEOUT_MS) }
200
233
  : {}),
201
234
  });
202
- // How the agent presents itself follows its `.agent` name: checked on start and then every
203
- // BUZZ_PROFILE_SYNC_MS (default six hours, 0 turns it off), and written only where a
204
- // community's copy differs. A new picture or a moved address needs no command on this host.
235
+ // How the agent presents itself follows the face its owner chose (name and picture, from the
236
+ // control plane), with its `.agent` name filling the gaps: checked on start and then every
237
+ // BUZZ_PROFILE_SYNC_MS (default fifteen minutes, 0 turns it off), and written only where a
238
+ // community's copy differs. A new picture or name needs no command on this host.
205
239
  const did = identityDid();
206
240
  const name = did ? nameFromDid(did) : null;
207
- const every = Number(process.env.BUZZ_PROFILE_SYNC_MS ?? 6 * 60 * 60 * 1000);
241
+ const every = Number(process.env.BUZZ_PROFILE_SYNC_MS ?? 15 * 60 * 1000);
208
242
  const timers = [];
209
- if (name && every > 0) {
243
+ const sources = faceSources(name);
244
+ if ((name || sources.credential) && every > 0) {
210
245
  const log = (message) => console.log(new Date().toISOString().slice(11, 19), message);
211
- const sync = () => syncProfileFromName({ name, relays: relayList(), key: loadKey(KEY_FILE) })
246
+ const sync = () => resolveFace(sources)
247
+ .then((face) => syncProfile({ face, relays: relayList(), key: loadKey(KEY_FILE) }))
212
248
  .then((outcomes) => {
213
249
  for (const { relay, outcome, detail } of outcomes) {
214
250
  if (outcome === "updated")
215
- log(`profile: ${relay} updated from ${name}.agent`);
251
+ log(`profile: ${relay} updated${detail ? ` (${detail})` : ""}`);
216
252
  if (outcome === "failed")
217
253
  log(`profile: ${relay} not updated — ${detail}`);
218
254
  }
@@ -384,8 +420,9 @@ else {
384
420
  BUZZ_SESSIONS_FILE channel-to-session continuity store
385
421
  BUZZ_AGENT_SILENCE_TIMEOUT_MS maximum silence before a turn is acknowledged (default 300000ms)
386
422
  BUZZ_AGENT_WORK_TIMEOUT_MS ceiling on a turn once acknowledged (default 3600000ms)
387
- BUZZ_PROFILE_SYNC_MS how often the profile is re-checked against the .agent name
388
- (default 21600000ms, six hours; 0 turns it off)
423
+ BUZZ_PROFILE_SYNC_MS how often the profile is re-checked against the name and
424
+ picture its owner set (default 900000ms, fifteen
425
+ minutes; 0 turns it off)
389
426
  KYBERNESIS_ISSUER the control plane
390
427
  KYBERNESIS_AGENT_CREDENTIAL this agent's credential
391
428
  `);
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "kind": "eve-extension",
3
3
  "formatVersion": 2,
4
- "builtWithEve": "0.68.0",
4
+ "builtWithEve": "0.81.1",
5
5
  "requires": {
6
6
  "extension": 1,
7
- "tool": 59,
7
+ "tool": 93,
8
8
  "instructions": 2
9
9
  }
10
10
  }
package/dist/face.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import type { AgentKey } from "./keys.js";
2
+ import * as profile from "./profile.js";
3
+ import type { Profile } from "./profile.js";
4
+ import { type SyncOutcome } from "./agentid.js";
5
+ /**
6
+ * The agent's face in every community: the name and picture its owner chose.
7
+ *
8
+ * @remarks
9
+ * The owner sets them in the app or KYBER Studio, and the control plane holds them. Before
10
+ * this, a community only ever saw what was published when the agent joined (its bare
11
+ * registered name) or what its `.agent` name said, so a new picture or name chosen by the
12
+ * owner never reached Buzz, and a name sync put the old one back. Now the owner's choice wins
13
+ * field by field and the `.agent` name fills the gaps, the same order every client draws.
14
+ *
15
+ * The control plane keeps the picture as a small data URL; a profile can only carry an
16
+ * address. So the picture is uploaded to a community's own media store, addressed by the hash
17
+ * of its bytes, and a community whose copy already points at that hash is left alone.
18
+ */
19
+ export type Face = {
20
+ /** The handle: lowercase, no spaces. */
21
+ handle: string;
22
+ /** Null when nobody has chosen one: the community keeps what it has. */
23
+ displayName: string | null;
24
+ /** A data URL, an https address, or null to leave the picture as it is. */
25
+ picture: string | null;
26
+ about: string | null;
27
+ nip05: string | null;
28
+ };
29
+ type ControlPlaneFace = {
30
+ agent: string;
31
+ displayName: string | null;
32
+ picture: string | null;
33
+ about: string | null;
34
+ nip05: string | null;
35
+ };
36
+ /** GET /api/agent/profile: the calling agent's own face, as its owner set it. */
37
+ export declare function fetchControlPlaneFace(options: {
38
+ issuer: string;
39
+ credential: string;
40
+ fetch?: typeof fetch;
41
+ }): Promise<ControlPlaneFace>;
42
+ /**
43
+ * What the agent should look like: the owner's choices over its `.agent` name.
44
+ *
45
+ * @remarks
46
+ * Either source may be missing (an older control plane, an agent with no name); only both
47
+ * missing is an error. The `.agent` name is read even when the control plane answers, because
48
+ * the control plane only folds it in once the name lists the agent back as verified, and an
49
+ * agent that has always taken its picture from its name should keep doing so.
50
+ */
51
+ export declare function resolveFace(options: {
52
+ /** The `.agent` name, when this agent has one. */
53
+ name: string | null;
54
+ issuer?: string;
55
+ credential?: string;
56
+ fetch?: typeof fetch;
57
+ }): Promise<Face>;
58
+ /** The profile fields the face decides, given where its picture now lives. */
59
+ export declare function profileFromFace(face: Face, pictureUrl: string | null): Profile;
60
+ /**
61
+ * Bring this agent's profile in every community in line with its face.
62
+ *
63
+ * @remarks
64
+ * Each community is read first; only one whose copy differs is written, so running this on a
65
+ * timer costs one read per community when nothing has changed. A picture is uploaded at most
66
+ * once per change, to the first community that takes it, and that address is used everywhere,
67
+ * the way `kybernesis-buzz profile --picture` always has. Fields the face does not decide are
68
+ * kept. A community that refuses or cannot be reached is reported, not fatal.
69
+ */
70
+ export declare function syncProfile(options: {
71
+ face: Face;
72
+ relays: readonly string[];
73
+ key: AgentKey;
74
+ read?: typeof profile.read;
75
+ write?: typeof profile.write;
76
+ upload?: typeof profile.uploadImage;
77
+ }): Promise<SyncOutcome[]>;
78
+ export {};
package/dist/face.js ADDED
@@ -0,0 +1,148 @@
1
+ import * as profile from "./profile.js";
2
+ import { fetchNameProfile } from "./agentid.js";
3
+ /** GET /api/agent/profile: the calling agent's own face, as its owner set it. */
4
+ export async function fetchControlPlaneFace(options) {
5
+ const url = `${options.issuer.replace(/\/+$/, "")}/api/agent/profile`;
6
+ const res = await (options.fetch ?? fetch)(url, {
7
+ headers: { authorization: `Bearer ${options.credential}`, accept: "application/json" },
8
+ signal: AbortSignal.timeout(10_000),
9
+ });
10
+ if (!res.ok)
11
+ throw new Error(`the control plane did not give this agent's profile (${res.status} from ${url})`);
12
+ return (await res.json());
13
+ }
14
+ function handleOf(name) {
15
+ return name.trim().toLowerCase().replace(/\s+/g, "-");
16
+ }
17
+ /**
18
+ * What the agent should look like: the owner's choices over its `.agent` name.
19
+ *
20
+ * @remarks
21
+ * Either source may be missing (an older control plane, an agent with no name); only both
22
+ * missing is an error. The `.agent` name is read even when the control plane answers, because
23
+ * the control plane only folds it in once the name lists the agent back as verified, and an
24
+ * agent that has always taken its picture from its name should keep doing so.
25
+ */
26
+ export async function resolveFace(options) {
27
+ let owner = null;
28
+ let ownerError = null;
29
+ if (options.issuer && options.credential) {
30
+ try {
31
+ owner = await fetchControlPlaneFace({ issuer: options.issuer, credential: options.credential, fetch: options.fetch });
32
+ }
33
+ catch (error) {
34
+ ownerError = error;
35
+ }
36
+ }
37
+ let named = null;
38
+ let nameError = null;
39
+ if (options.name) {
40
+ try {
41
+ named = await fetchNameProfile(options.name, options.fetch);
42
+ }
43
+ catch (error) {
44
+ nameError = error;
45
+ }
46
+ }
47
+ if (!owner && !named) {
48
+ throw ownerError ?? nameError ?? new Error("Nothing says who this agent is: no control plane credential and no .agent name.");
49
+ }
50
+ return {
51
+ handle: options.name ?? handleOf(owner.agent),
52
+ displayName: owner?.displayName ?? named?.name ?? null,
53
+ picture: owner?.picture ?? named?.picture ?? null,
54
+ about: owner?.about ?? (named?.description || null),
55
+ nip05: owner?.nip05 ?? named?.nip05 ?? null,
56
+ };
57
+ }
58
+ /** A picture held as a data URL, decoded and stripped the way an upload will be. */
59
+ async function pictureBytes(dataUrl) {
60
+ const match = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i.exec(dataUrl);
61
+ if (!match)
62
+ return null;
63
+ const mediaType = match[1].toLowerCase();
64
+ const bytes = new Uint8Array(Buffer.from(match[2], "base64"));
65
+ const { createHash } = await import("node:crypto");
66
+ const sha256 = createHash("sha256").update(profile.stripMetadata(bytes, mediaType)).digest("hex");
67
+ return { bytes, mediaType, sha256 };
68
+ }
69
+ /** The profile fields the face decides, given where its picture now lives. */
70
+ export function profileFromFace(face, pictureUrl) {
71
+ return {
72
+ name: face.handle,
73
+ ...(face.displayName ? { display_name: face.displayName } : {}),
74
+ ...(face.about ? { about: face.about } : {}),
75
+ ...(pictureUrl ? { picture: pictureUrl } : {}),
76
+ ...(face.nip05 ? { nip05: face.nip05 } : {}),
77
+ bot: true,
78
+ };
79
+ }
80
+ /**
81
+ * Bring this agent's profile in every community in line with its face.
82
+ *
83
+ * @remarks
84
+ * Each community is read first; only one whose copy differs is written, so running this on a
85
+ * timer costs one read per community when nothing has changed. A picture is uploaded at most
86
+ * once per change, to the first community that takes it, and that address is used everywhere,
87
+ * the way `kybernesis-buzz profile --picture` always has. Fields the face does not decide are
88
+ * kept. A community that refuses or cannot be reached is reported, not fatal.
89
+ */
90
+ export async function syncProfile(options) {
91
+ const read = options.read ?? profile.read;
92
+ const write = options.write ?? profile.write;
93
+ const upload = options.upload ?? profile.uploadImage;
94
+ const { face, key } = options;
95
+ const current = new Map();
96
+ const outcomes = new Map();
97
+ for (const relay of options.relays) {
98
+ try {
99
+ current.set(relay, await read(relay, key));
100
+ }
101
+ catch (error) {
102
+ const why = error.message;
103
+ outcomes.set(relay, { relay, outcome: why.includes("not a relay member") ? "not-member" : "failed", detail: why });
104
+ }
105
+ }
106
+ // Where the picture lives. Null leaves each community's picture as it is.
107
+ let pictureUrl = null;
108
+ let pictureProblem = null;
109
+ if (face.picture && /^https:\/\//.test(face.picture)) {
110
+ pictureUrl = face.picture;
111
+ }
112
+ else if (face.picture) {
113
+ const picture = await pictureBytes(face.picture);
114
+ if (!picture) {
115
+ pictureProblem = "the picture is not an image this can send";
116
+ }
117
+ else {
118
+ pictureUrl = [...current.values()].map((p) => p?.picture).find((url) => url?.includes(picture.sha256)) ?? null;
119
+ for (const relay of current.keys()) {
120
+ if (pictureUrl)
121
+ break;
122
+ try {
123
+ pictureUrl = await upload(relay, key, picture.bytes, picture.mediaType);
124
+ }
125
+ catch (error) {
126
+ pictureProblem = error.message;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ const wanted = profileFromFace(face, pictureUrl);
132
+ for (const [relay, existing] of current) {
133
+ const matches = existing && Object.keys(wanted).every((field) => existing[field] === wanted[field]);
134
+ if (matches) {
135
+ outcomes.set(relay, { relay, outcome: "unchanged" });
136
+ continue;
137
+ }
138
+ try {
139
+ await write(relay, key, { ...(existing ?? {}), ...wanted });
140
+ outcomes.set(relay, { relay, outcome: "updated", ...(pictureProblem ? { detail: `picture not sent: ${pictureProblem}` } : {}) });
141
+ }
142
+ catch (error) {
143
+ const why = error.message;
144
+ outcomes.set(relay, { relay, outcome: why.includes("not a relay member") ? "not-member" : "failed", detail: why });
145
+ }
146
+ }
147
+ return options.relays.map((relay) => outcomes.get(relay));
148
+ }
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export { BuzzRelay, type NostrEvent, type PresenceStatus, type RelayOptions, KIN
3
3
  export { asHexPubkey, loadKey, loadOrCreateKey, npubEncode, type AgentKey } from "./keys.js";
4
4
  export * as profile from "./profile.js";
5
5
  export type { Profile } from "./profile.js";
6
+ export { fetchControlPlaneFace, profileFromFace, resolveFace, syncProfile, type Face } from "./face.js";
6
7
  export { readSurfaceManifest, surfaceWriter, surfacesDir, type SurfaceManifest } from "./surface.js";
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@ export { buzzBridge } from "./bridge.js";
2
2
  export { BuzzRelay, KIND_MESSAGE, KIND_REACTION, KIND_PRESENCE, KIND_TYPING, KIND_DM_OPEN, } from "./relay.js";
3
3
  export { asHexPubkey, loadKey, loadOrCreateKey, npubEncode } from "./keys.js";
4
4
  export * as profile from "./profile.js";
5
+ export { fetchControlPlaneFace, profileFromFace, resolveFace, syncProfile } from "./face.js";
5
6
  export { readSurfaceManifest, surfaceWriter, surfacesDir } from "./surface.js";
package/dist/turn.js CHANGED
@@ -196,6 +196,15 @@ async function drainUnread(stream, timeouts) {
196
196
  }
197
197
  }
198
198
  const isTurnBoundary = (type) => type === "session.waiting" || type === "session.completed" || type === "session.failed";
199
+ /**
200
+ * eve ≥0.69 no longer ends a turn at a question or sign-in from inside a tool:
201
+ * it emits `turn.waiting` with `on: "input"` and holds the turn open, and the
202
+ * answer resumes that same turn. For the bridge that is where this send stops,
203
+ * as `session.waiting` did before, with the questions to relay. eve's own
204
+ * `result()` stops at the same event. `on: "tasks"` is not a stop: delegated
205
+ * work is running and the reply is still to come.
206
+ */
207
+ const isHeldForInput = (event) => event.type === "turn.waiting" && event.data?.on === "input";
199
208
  async function collectResponse(response, watchdog, phase, streamIndex, continueStream, log = () => { }) {
200
209
  let message = "";
201
210
  let status = "completed";
@@ -242,7 +251,7 @@ async function collectResponse(response, watchdog, phase, streamIndex, continueS
242
251
  else if (event.type === "input.requested") {
243
252
  inputRequests.push(...event.data.requests);
244
253
  }
245
- else if (event.type === "session.waiting") {
254
+ else if (event.type === "session.waiting" || isHeldForInput(event)) {
246
255
  status = "waiting";
247
256
  }
248
257
  else if (event.type === "session.failed") {
@@ -251,11 +260,12 @@ async function collectResponse(response, watchdog, phase, streamIndex, continueS
251
260
  else if (event.type === "session.completed") {
252
261
  status = "completed";
253
262
  }
254
- if (isTurnBoundary(event.type) && !staleBoundaryFirst) {
263
+ const ends = isTurnBoundary(event.type) || isHeldForInput(event);
264
+ if (ends && !staleBoundaryFirst) {
255
265
  endedAtOwnBoundary = true;
256
266
  return true;
257
267
  }
258
- if (isTurnBoundary(event.type) && staleBoundaryFirst && sawTurnStart) {
268
+ if (ends && staleBoundaryFirst && sawTurnStart) {
259
269
  endedAtOwnBoundary = true;
260
270
  return true;
261
271
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/buzz",
3
- "version": "0.10.2",
3
+ "version": "0.12.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",
@@ -44,17 +44,17 @@
44
44
  "test": "node --test \"test/**/*.test.mjs\""
45
45
  },
46
46
  "dependencies": {
47
- "@kybernesis/enterprise": "^0.9.0",
47
+ "@kybernesis/enterprise": "^0.10.0",
48
48
  "nostr-tools": "^2.10.0",
49
49
  "zod": "^4"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "24.x",
53
- "eve": "0.68.0",
53
+ "eve": "0.81.1",
54
54
  "typescript": "7.0.2"
55
55
  },
56
56
  "peerDependencies": {
57
- "eve": ">=0.68.0 <0.69.0"
57
+ "eve": ">=0.81.1 <0.82.0"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=20"
@@ -69,5 +69,8 @@
69
69
  "source": "./extension",
70
70
  "dist": "./dist/extension"
71
71
  }
72
+ },
73
+ "publishConfig": {
74
+ "tag": "eve-0.81"
72
75
  }
73
76
  }