@gorlitzer-labs/apiary 1.14.0 → 1.15.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
@@ -96,11 +96,13 @@ Both put an agent in a room. They differ in who wakes it up:
96
96
  | | MCP client (`apiary mcp`) | tmux wrapper (`apiary claude` / `codex`) |
97
97
  |---|---|---|
98
98
  | Setup | Any session you already have — Claude Code, Cursor, Windsurf | Apiary launches the CLI in its own `apiary_<name>` tmux session |
99
- | Room messages | **Pulled** via `catch_up()`; inside tmux, an @mention, whisper or ping also nudges the session | **Pushed** into the session, even while it's idle |
100
- | Idle agent | Inside tmux: woken by a mention, whisper or ping. Outside tmux: sleeps until you type | Woken by a mention or a message |
99
+ | Room messages | **Pulled** via `catch_up()`; inside tmux, new messages also nudge the session, as its engagement mode says | **Pushed** into the session, even while it's idle |
100
+ | Idle agent | Inside tmux: woken by room messages (`everyone`/`people`/`agents`) or only a mention, whisper or ping (`standby-*`). Outside tmux: sleeps until you type | Woken by a mention or a message |
101
101
  | `apiary stop`, factory, supervisor | Not managed — just a participant | Tracked by pid; factory can staff it |
102
102
 
103
- **How the MCP client wakes a session.** Claude Code passes `$TMUX_PANE` to its MCP servers, so `apiary mcp` knows which pane it lives in. When the room addresses the agent it types one fixed line there — `apiary: <sender> mentioned you in room "<room>". Read it with apiary__catch_up("<room>") …` — never the message itself, so nobody in the room can put words in your prompt. It waits for an empty composer (your draft is never touched) and never types into a permission prompt. `APIARY_MCP_WAKE=off` disables it.
103
+ **How the MCP client wakes a session.** Claude Code passes `$TMUX_PANE` to its MCP servers, so `apiary mcp` knows which pane it lives in, and types one fixed line there when the room has something for the agent. What counts follows the agent's engagement mode in that room: in `everyone` (the default), `people` or `agents`, a message from someone else that the mode lets through wakes it with `apiary: new messages in room "<room>" from <sender>. Read them with apiary__catch_up("<room>") …`; in a `standby-*` mode only an @mention, a whisper or a ping does, with `apiary: <sender> mentioned you in room "<room>". Read it with apiary__catch_up("<room>") …`. A room gets at most one such nudge until the agent calls `catch_up` for it, so a busy room costs one line, not one per message; an @mention, whisper or ping still wakes it while that nudge is outstanding, never twice for one message. Its own messages, joins, leaves and activity never wake it. The line is never the message itself, so nobody in the room can put words in your prompt; room and sender names are reduced to plain labels. It waits for an empty composer (your draft is never touched) and never types into a permission prompt. `APIARY_MCP_WAKE=addressed` wakes only on @mentions, whispers and pings whatever the mode (the 1.14 behaviour); `APIARY_MCP_WAKE=off` disables it. The startup line on stderr says which rule is live.
104
+
105
+ **How the MCP client survives a room restart.** A room server keeps session tokens in memory, so after it restarts every call from a joined agent answers 401. `apiary mcp` keeps what joining took (the share token, the agent's name and type), and on a 401 from a call or the event stream joins again under the same name — the server replaces the old seat, so no ghost is left — swaps the new session in and retries the call once. A restart also mints new share tokens: if the agent joined with one of the room's own tokens and the room runs on this machine, the same-tier token from `~/.apiary/sessions/room_<name>.json` is used, never a higher one. If the URL the agent joined with dies (an expired tunnel) while the room still answers on `127.0.0.1` or `localhost` per that file, it moves there; no other host is ever tried. A link the room no longer accepts (403 `Invalid share token`), a kick by an admin, or a seat another session keeps taking over is not retried: the agent gets an error saying why and what to do, and inside tmux a fixed one-line nudge. Retries back off. Every reconnect is logged on stderr.
104
106
 
105
107
  Use the MCP client for agents **you're driving** — "join the room, post what you found, ask X" — and for adding agents on the fly. Use the wrapper for agents that must **run unattended**: a coordinator, workers, anything that should react while nobody is at the keyboard.
106
108
 
@@ -534,6 +534,11 @@ interface RuntimeMcpServerOptions {
534
534
  authority?: AuthorityLevel;
535
535
  /** Called when the agent requests joining a new room mid-session. */
536
536
  onJoinRoom?: (url: string, alias?: string, name?: string) => Promise<JoinRoomResult>;
537
+ /**
538
+ * Called after the agent read a room with catch_up(room). The `apiary mcp`
539
+ * waker re-arms that room's nudge here.
540
+ */
541
+ onCatchUp?: (room: string) => void;
537
542
  /** Called when the agent requests leaving a room. */
538
543
  onLeaveRoom?: (room: string) => Promise<{
539
544
  success: boolean;
@@ -630,6 +635,22 @@ declare class RefMap {
630
635
  clear(): void;
631
636
  }
632
637
 
638
+ /** Restores a lost session. Implemented by the CLI runtime's RoomRejoiner. */
639
+ interface SessionRecovery {
640
+ /**
641
+ * The server answered 401 (the session token is no longer valid). Put a new
642
+ * session in place with `setSession()` and resolve, or throw a
643
+ * RoomConnectionError saying why that was not possible. `usedToken` is the
644
+ * token the rejected request carried: if it is no longer the current one,
645
+ * someone else already re-joined and there is nothing to do.
646
+ */
647
+ sessionInvalid(usedToken?: string): Promise<void>;
648
+ /**
649
+ * A request never reached the server (network error). Resolve true when the
650
+ * source now points at a URL that answers, false otherwise. Never throws.
651
+ */
652
+ unreachable(): Promise<boolean>;
653
+ }
633
654
  declare class RemoteRoomDataSource implements RoomDataSource {
634
655
  private _serverUrl;
635
656
  private _sessionToken;
@@ -637,9 +658,14 @@ declare class RemoteRoomDataSource implements RoomDataSource {
637
658
  private _participants;
638
659
  private _selfId;
639
660
  private _selfName;
661
+ private _recovery;
640
662
  constructor(_serverUrl: string, _sessionToken: string, _roomId: string);
641
663
  /** Set own identity for populating outgoing message stubs. */
642
664
  setSelf(id: string, name: string): void;
665
+ /** Swap in a new server URL and/or session token (after a re-join or URL fallback). */
666
+ setSession(serverUrl: string, sessionToken: string): void;
667
+ /** Install the handler that restores a lost session. */
668
+ setRecovery(recovery: SessionRecovery | null): void;
643
669
  get roomId(): string;
644
670
  get selfId(): string;
645
671
  get serverUrl(): string;
@@ -651,6 +677,17 @@ declare class RemoteRoomDataSource implements RoomDataSource {
651
677
  /** Remove a participant (on ParticipantLeft event). */
652
678
  removeParticipant(participantId: string): void;
653
679
  listParticipants(): Participant[];
680
+ /**
681
+ * An authenticated request to the room server, with the CURRENT URL and
682
+ * token. On a network error or a 401 it asks the recovery handler once, then
683
+ * retries once with whatever URL and token are in place afterwards.
684
+ *
685
+ * Throws RoomConnectionError when the session was invalid and could not be
686
+ * restored; rethrows a network error that no fallback could route around.
687
+ */
688
+ request(path: string, init?: RequestInit & {
689
+ headers?: Record<string, string>;
690
+ }): Promise<Response>;
654
691
  getMessage(id: string): Promise<Message | null>;
655
692
  searchMessages(query: string, limit?: number, cursor?: string | null): Promise<PaginatedResult<Message>>;
656
693
  getMessages(limit?: number, cursor?: string | null): Promise<PaginatedResult<Message>>;
@@ -672,18 +709,54 @@ declare class RemoteRoomDataSource implements RoomDataSource {
672
709
  *
673
710
  * Connections can be added/removed while the multiplexer is running.
674
711
  * Each connection has its own AbortController for independent lifecycle.
712
+ *
713
+ * A dropped stream is retried with backoff. Retrying alone never recovers a
714
+ * room server that restarted — its session tokens are gone, so every retry is
715
+ * a 401 — or a URL that died while the room is still up on localhost. Those go
716
+ * to the recovery handler (see cli/rejoin.ts), which re-joins or switches URL
717
+ * and calls `updateConnection()`; each attempt reads the connection's current
718
+ * URL and token, so the next one uses the new session.
675
719
  */
676
720
 
721
+ /** What the SSE loop asks for when retrying alone cannot help. */
722
+ interface SseRecoveryHandler {
723
+ /**
724
+ * /events answered 401 or 403: the session is gone. Resolve true when a new
725
+ * session is in place (retry now), false to back off and retry later. Throw
726
+ * to stop this connection for good — the error says why. `usedToken` is the
727
+ * token the rejected attempt carried.
728
+ */
729
+ authRejected(roomId: string, status: number, usedToken: string): Promise<boolean>;
730
+ /** /events could not be reached. Resolve true when a different URL now answers. */
731
+ unreachable(roomId: string): Promise<boolean>;
732
+ }
733
+ interface SseMultiplexerOptions {
734
+ initialBackoffMs?: number;
735
+ maxBackoffMs?: number;
736
+ }
677
737
  declare class SseMultiplexer {
678
738
  private _queue;
679
739
  private _waiters;
680
740
  private _connections;
681
741
  private _closed;
742
+ private _recovery;
743
+ private readonly _initialBackoff;
744
+ private readonly _maxBackoff;
745
+ constructor(options?: SseMultiplexerOptions);
746
+ /** Install the handler for lost sessions and dead URLs. */
747
+ setRecoveryHandler(handler: SseRecoveryHandler | null): void;
682
748
  /**
683
749
  * Add an SSE connection to a stoop server.
684
750
  * Starts streaming events immediately.
685
751
  */
686
752
  addConnection(serverUrl: string, sessionToken: string, roomName: string, roomId: string): void;
753
+ /** Is there a live (not stopped) connection for this room? */
754
+ hasConnection(roomId: string): boolean;
755
+ /**
756
+ * Point a connection at a new URL and/or session token. The current attempt
757
+ * is dropped so the stream reconnects with the new session straight away.
758
+ */
759
+ updateConnection(roomId: string, serverUrl: string, sessionToken: string): void;
687
760
  /** Remove a connection by room ID. */
688
761
  removeConnection(roomId: string): void;
689
762
  /** Close all connections and signal the iterator to finish. */
@@ -8,7 +8,7 @@ import {
8
8
  StoopsEngagement,
9
9
  classifyEvent,
10
10
  createRuntimeMcpServer
11
- } from "../chunk-WJD5EKO6.js";
11
+ } from "../chunk-Z4J6TRJY.js";
12
12
  import {
13
13
  contentPartsToString,
14
14
  formatEvent,
@@ -560,8 +560,6 @@ async function serve(options) {
560
560
  this._max = _max;
561
561
  this._windowMs = _windowMs;
562
562
  }
563
- _max;
564
- _windowMs;
565
563
  _buckets = /* @__PURE__ */ new Map();
566
564
  _opsSincePrune = 0;
567
565
  check(key) {
@@ -1547,4 +1545,4 @@ export {
1547
1545
  removeRoomSession,
1548
1546
  serve
1549
1547
  };
1550
- //# sourceMappingURL=chunk-LOONM4G3.js.map
1548
+ //# sourceMappingURL=chunk-3UQBSELW.js.map