@tloncorp/openclaw 0.1.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.
Files changed (63) hide show
  1. package/README.md +174 -0
  2. package/dist/index.js +190 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/src/account-fields.js +17 -0
  5. package/dist/src/account-fields.js.map +1 -0
  6. package/dist/src/actions.js +164 -0
  7. package/dist/src/actions.js.map +1 -0
  8. package/dist/src/channel.js +400 -0
  9. package/dist/src/channel.js.map +1 -0
  10. package/dist/src/config-schema.js +55 -0
  11. package/dist/src/config-schema.js.map +1 -0
  12. package/dist/src/monitor/approval.js +194 -0
  13. package/dist/src/monitor/approval.js.map +1 -0
  14. package/dist/src/monitor/discovery.js +64 -0
  15. package/dist/src/monitor/discovery.js.map +1 -0
  16. package/dist/src/monitor/history.js +158 -0
  17. package/dist/src/monitor/history.js.map +1 -0
  18. package/dist/src/monitor/index.js +1940 -0
  19. package/dist/src/monitor/index.js.map +1 -0
  20. package/dist/src/monitor/media.js +128 -0
  21. package/dist/src/monitor/media.js.map +1 -0
  22. package/dist/src/monitor/processed-messages.js +38 -0
  23. package/dist/src/monitor/processed-messages.js.map +1 -0
  24. package/dist/src/monitor/utils.js +283 -0
  25. package/dist/src/monitor/utils.js.map +1 -0
  26. package/dist/src/onboarding.js +178 -0
  27. package/dist/src/onboarding.js.map +1 -0
  28. package/dist/src/runtime.js +11 -0
  29. package/dist/src/runtime.js.map +1 -0
  30. package/dist/src/settings.js +305 -0
  31. package/dist/src/settings.js.map +1 -0
  32. package/dist/src/targets.js +85 -0
  33. package/dist/src/targets.js.map +1 -0
  34. package/dist/src/types.js +79 -0
  35. package/dist/src/types.js.map +1 -0
  36. package/dist/src/urbit/api-client.js +104 -0
  37. package/dist/src/urbit/api-client.js.map +1 -0
  38. package/dist/src/urbit/auth.js +35 -0
  39. package/dist/src/urbit/auth.js.map +1 -0
  40. package/dist/src/urbit/base-url.js +45 -0
  41. package/dist/src/urbit/base-url.js.map +1 -0
  42. package/dist/src/urbit/channel-ops.js +136 -0
  43. package/dist/src/urbit/channel-ops.js.map +1 -0
  44. package/dist/src/urbit/context.js +42 -0
  45. package/dist/src/urbit/context.js.map +1 -0
  46. package/dist/src/urbit/errors.js +36 -0
  47. package/dist/src/urbit/errors.js.map +1 -0
  48. package/dist/src/urbit/fetch.js +23 -0
  49. package/dist/src/urbit/fetch.js.map +1 -0
  50. package/dist/src/urbit/foreigns.js +6 -0
  51. package/dist/src/urbit/foreigns.js.map +1 -0
  52. package/dist/src/urbit/http-poke.js +56 -0
  53. package/dist/src/urbit/http-poke.js.map +1 -0
  54. package/dist/src/urbit/send.js +208 -0
  55. package/dist/src/urbit/send.js.map +1 -0
  56. package/dist/src/urbit/sse-client.js +453 -0
  57. package/dist/src/urbit/sse-client.js.map +1 -0
  58. package/dist/src/urbit/story.js +286 -0
  59. package/dist/src/urbit/story.js.map +1 -0
  60. package/dist/src/urbit/upload.js +51 -0
  61. package/dist/src/urbit/upload.js.map +1 -0
  62. package/openclaw.plugin.json +10 -0
  63. package/package.json +84 -0
@@ -0,0 +1,208 @@
1
+ import { sendPost as apiSendPost, sendReply as apiSendReply, addReaction as apiAddReaction, removeReaction as apiRemoveReaction, deletePost as apiDeletePost, } from "@tloncorp/api";
2
+ import { scot, da } from "@urbit/aura";
3
+ import { markdownToStory, createImageBlock, isImageUrl } from "./story.js";
4
+ // --- Helpers ---
5
+ /**
6
+ * Format a post ID as @ud (with dots) if it's a bare digit string.
7
+ * Tlon requires @ud-formatted IDs for post references.
8
+ */
9
+ function formatPostId(postId) {
10
+ if (/^\d+$/.test(postId)) {
11
+ try {
12
+ return scot("ud", BigInt(postId));
13
+ }
14
+ catch {
15
+ // fall through
16
+ }
17
+ }
18
+ return postId;
19
+ }
20
+ /**
21
+ * Parse a writ-id string into author and bare ID components.
22
+ * Writ-ids look like "~sampel-palnet/170.141.184..." (author/udId).
23
+ * Returns the components for use with @tloncorp/api which expects them separately.
24
+ */
25
+ function parseWritId(id) {
26
+ if (id.includes("/") && id.startsWith("~")) {
27
+ const idx = id.indexOf("/");
28
+ return { author: id.slice(0, idx), bareId: id.slice(idx + 1) };
29
+ }
30
+ return { author: "", bareId: id };
31
+ }
32
+ /**
33
+ * Compute a @ud-formatted timestamp for building message IDs.
34
+ */
35
+ function formatSentAt(sentAt) {
36
+ return scot("ud", da.fromUnix(sentAt));
37
+ }
38
+ export async function sendDm(params) {
39
+ const story = markdownToStory(params.text);
40
+ return sendDmWithStory({ ...params, story });
41
+ }
42
+ export async function sendDmWithStory({ fromShip, toShip, story, replyToId, parentAuthor, }) {
43
+ const sentAt = Date.now();
44
+ const messageId = `${fromShip}/${formatSentAt(sentAt)}`;
45
+ if (replyToId) {
46
+ const parsed = parseWritId(replyToId);
47
+ const effectiveAuthor = parentAuthor || parsed.author || toShip;
48
+ const bareParentId = formatPostId(parsed.bareId);
49
+ await apiSendReply({
50
+ channelId: toShip,
51
+ parentId: bareParentId,
52
+ parentAuthor: effectiveAuthor,
53
+ content: story,
54
+ sentAt,
55
+ authorId: fromShip,
56
+ });
57
+ return { channel: "tlon", messageId };
58
+ }
59
+ await apiSendPost({
60
+ channelId: toShip,
61
+ authorId: fromShip,
62
+ sentAt,
63
+ content: story,
64
+ });
65
+ return { channel: "tlon", messageId };
66
+ }
67
+ /**
68
+ * Unified function for posting to any channel type (chat, heap, diary).
69
+ * Takes a full nest string directly.
70
+ */
71
+ export async function sendChannelPost({ fromShip, nest, story, replyToId, title, }) {
72
+ const sentAt = Date.now();
73
+ if (replyToId) {
74
+ const formattedReplyId = formatPostId(replyToId);
75
+ await apiSendReply({
76
+ channelId: nest,
77
+ parentId: formattedReplyId,
78
+ parentAuthor: "", // Not used for channel replies
79
+ content: story,
80
+ sentAt,
81
+ authorId: fromShip,
82
+ });
83
+ return { channel: "tlon", messageId: `${fromShip}/${sentAt}` };
84
+ }
85
+ await apiSendPost({
86
+ channelId: nest,
87
+ authorId: fromShip,
88
+ sentAt,
89
+ content: story,
90
+ metadata: title ? { title } : undefined,
91
+ });
92
+ return { channel: "tlon", messageId: `${fromShip}/${sentAt}` };
93
+ }
94
+ // --- Utilities ---
95
+ export function buildMediaText(text, mediaUrl) {
96
+ const cleanText = text?.trim() ?? "";
97
+ const cleanUrl = mediaUrl?.trim() ?? "";
98
+ if (cleanText && cleanUrl) {
99
+ return `${cleanText}\n${cleanUrl}`;
100
+ }
101
+ if (cleanUrl) {
102
+ return cleanUrl;
103
+ }
104
+ return cleanText;
105
+ }
106
+ /**
107
+ * Build a story with text and optional media (image)
108
+ */
109
+ export function buildMediaStory(text, mediaUrl) {
110
+ const story = [];
111
+ const cleanText = text?.trim() ?? "";
112
+ const cleanUrl = mediaUrl?.trim() ?? "";
113
+ // Add text content if present
114
+ if (cleanText) {
115
+ story.push(...markdownToStory(cleanText));
116
+ }
117
+ // Add image block if URL looks like an image
118
+ if (cleanUrl && isImageUrl(cleanUrl)) {
119
+ story.push(createImageBlock(cleanUrl, ""));
120
+ }
121
+ else if (cleanUrl) {
122
+ // For non-image URLs, add as a link
123
+ story.push({ inline: [{ link: { href: cleanUrl, content: cleanUrl } }] });
124
+ }
125
+ return story.length > 0 ? story : [{ inline: [""] }];
126
+ }
127
+ export async function addChannelReaction({ fromShip, hostShip, channelName, postId, react, nestPrefix = "chat", parentId, }) {
128
+ const nest = `${nestPrefix}/${hostShip}/${channelName}`;
129
+ const formattedPostId = formatPostId(postId);
130
+ await apiAddReaction({
131
+ channelId: nest,
132
+ postId: formattedPostId,
133
+ emoji: react,
134
+ our: fromShip,
135
+ postAuthor: fromShip, // Not used for channel reactions
136
+ ...(parentId && { parentId: formatPostId(parentId) }),
137
+ });
138
+ }
139
+ export async function removeChannelReaction({ fromShip, hostShip, channelName, postId, nestPrefix = "chat", parentId, }) {
140
+ const nest = `${nestPrefix}/${hostShip}/${channelName}`;
141
+ const formattedPostId = formatPostId(postId);
142
+ await apiRemoveReaction({
143
+ channelId: nest,
144
+ postId: formattedPostId,
145
+ our: fromShip,
146
+ postAuthor: fromShip, // Not used for channel reactions
147
+ ...(parentId && { parentId: formatPostId(parentId) }),
148
+ });
149
+ }
150
+ export async function addDmReaction({ fromShip, toShip, messageId, react, parentId, postAuthor, parentAuthor, }) {
151
+ const parsedMessage = parseWritId(messageId);
152
+ const effectivePostAuthor = postAuthor || parsedMessage.author || toShip;
153
+ const formattedPostId = formatPostId(parsedMessage.bareId);
154
+ if (parentId) {
155
+ const parsedParent = parseWritId(parentId);
156
+ const effectiveParentAuthor = parentAuthor || parsedParent.author || toShip;
157
+ const formattedParentId = formatPostId(parsedParent.bareId);
158
+ await apiAddReaction({
159
+ channelId: toShip,
160
+ postId: formattedPostId,
161
+ emoji: react,
162
+ our: fromShip,
163
+ postAuthor: effectivePostAuthor,
164
+ parentId: formattedParentId,
165
+ parentAuthorId: effectiveParentAuthor,
166
+ });
167
+ return;
168
+ }
169
+ await apiAddReaction({
170
+ channelId: toShip,
171
+ postId: formattedPostId,
172
+ emoji: react,
173
+ our: fromShip,
174
+ postAuthor: effectivePostAuthor,
175
+ });
176
+ }
177
+ export async function removeDmReaction({ fromShip, toShip, messageId, parentId, postAuthor, parentAuthor, }) {
178
+ const parsedMessage = parseWritId(messageId);
179
+ const effectivePostAuthor = postAuthor || parsedMessage.author || toShip;
180
+ const formattedPostId = formatPostId(parsedMessage.bareId);
181
+ if (parentId) {
182
+ const parsedParent = parseWritId(parentId);
183
+ const effectiveParentAuthor = parentAuthor || parsedParent.author || toShip;
184
+ const formattedParentId = formatPostId(parsedParent.bareId);
185
+ await apiRemoveReaction({
186
+ channelId: toShip,
187
+ postId: formattedPostId,
188
+ our: fromShip,
189
+ postAuthor: effectivePostAuthor,
190
+ parentId: formattedParentId,
191
+ parentAuthorId: effectiveParentAuthor,
192
+ });
193
+ return;
194
+ }
195
+ await apiRemoveReaction({
196
+ channelId: toShip,
197
+ postId: formattedPostId,
198
+ our: fromShip,
199
+ postAuthor: effectivePostAuthor,
200
+ });
201
+ }
202
+ export async function deleteHeapPost({ hostShip, channelName, curioId, }) {
203
+ const nest = `heap/${hostShip}/${channelName}`;
204
+ const formattedCurioId = formatPostId(curioId);
205
+ await apiDeletePost(nest, formattedCurioId, "");
206
+ return { ok: true };
207
+ }
208
+ //# sourceMappingURL=send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../../../src/urbit/send.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,453 @@
1
+ import { Readable } from "node:stream";
2
+ import { ensureUrbitChannelOpen, pokeUrbitChannel, scryUrbitPath } from "./channel-ops.js";
3
+ import { getUrbitContext, normalizeUrbitCookie } from "./context.js";
4
+ import { urbitFetch } from "./fetch.js";
5
+ export class UrbitSSEClient {
6
+ url;
7
+ cookie;
8
+ ship;
9
+ channelId;
10
+ channelUrl;
11
+ subscriptions = [];
12
+ eventHandlers = new Map();
13
+ aborted = false;
14
+ streamController = null;
15
+ onReconnect;
16
+ autoReconnect;
17
+ reconnectAttempts = 0;
18
+ maxReconnectAttempts;
19
+ reconnectDelay;
20
+ maxReconnectDelay;
21
+ isConnected = false;
22
+ logger;
23
+ ssrfPolicy;
24
+ lookupFn;
25
+ fetchImpl;
26
+ streamRelease = null;
27
+ // Event ack tracking - must ack every ~50 events to keep channel healthy
28
+ lastHeardEventId = -1;
29
+ lastAcknowledgedEventId = -1;
30
+ ackThreshold = 20;
31
+ constructor(url, cookie, options = {}) {
32
+ const ctx = getUrbitContext(url, options.ship);
33
+ this.url = ctx.baseUrl;
34
+ this.cookie = normalizeUrbitCookie(cookie);
35
+ this.ship = ctx.ship;
36
+ this.channelId = `${Math.floor(Date.now() / 1000)}-${Math.random().toString(36).substring(2, 8)}`;
37
+ this.channelUrl = new URL(`/~/channel/${this.channelId}`, this.url).toString();
38
+ this.onReconnect = options.onReconnect ?? null;
39
+ this.autoReconnect = options.autoReconnect !== false;
40
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 10;
41
+ this.reconnectDelay = options.reconnectDelay ?? 1000;
42
+ this.maxReconnectDelay = options.maxReconnectDelay ?? 30000;
43
+ this.logger = options.logger ?? {};
44
+ this.ssrfPolicy = options.ssrfPolicy;
45
+ this.lookupFn = options.lookupFn;
46
+ this.fetchImpl = options.fetchImpl;
47
+ }
48
+ async subscribe(params) {
49
+ const subId = this.subscriptions.length + 1;
50
+ const subscription = {
51
+ id: subId,
52
+ action: "subscribe",
53
+ ship: this.ship,
54
+ app: params.app,
55
+ path: params.path,
56
+ };
57
+ this.subscriptions.push(subscription);
58
+ this.eventHandlers.set(subId, { event: params.event, err: params.err, quit: params.quit });
59
+ if (this.isConnected) {
60
+ try {
61
+ await this.sendSubscription(subscription);
62
+ }
63
+ catch (error) {
64
+ const handler = this.eventHandlers.get(subId);
65
+ handler?.err?.(error);
66
+ }
67
+ }
68
+ return subId;
69
+ }
70
+ async sendSubscription(subscription) {
71
+ const { response, release } = await urbitFetch({
72
+ baseUrl: this.url,
73
+ path: `/~/channel/${this.channelId}`,
74
+ init: {
75
+ method: "PUT",
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ Cookie: this.cookie,
79
+ },
80
+ body: JSON.stringify([subscription]),
81
+ },
82
+ ssrfPolicy: this.ssrfPolicy,
83
+ lookupFn: this.lookupFn,
84
+ fetchImpl: this.fetchImpl,
85
+ timeoutMs: 30_000,
86
+ auditContext: "tlon-urbit-subscribe",
87
+ });
88
+ try {
89
+ if (!response.ok && response.status !== 204) {
90
+ const errorText = await response.text().catch(() => "");
91
+ throw new Error(`Subscribe failed: ${response.status}${errorText ? ` - ${errorText}` : ""}`);
92
+ }
93
+ }
94
+ finally {
95
+ await release();
96
+ }
97
+ }
98
+ async connect() {
99
+ await ensureUrbitChannelOpen({
100
+ baseUrl: this.url,
101
+ cookie: this.cookie,
102
+ ship: this.ship,
103
+ channelId: this.channelId,
104
+ ssrfPolicy: this.ssrfPolicy,
105
+ lookupFn: this.lookupFn,
106
+ fetchImpl: this.fetchImpl,
107
+ }, {
108
+ createBody: this.subscriptions,
109
+ createAuditContext: "tlon-urbit-channel-create",
110
+ });
111
+ await this.openStream();
112
+ this.isConnected = true;
113
+ this.reconnectAttempts = 0;
114
+ }
115
+ async openStream() {
116
+ // Use AbortController with manual timeout so we only abort during initial connection,
117
+ // not after the SSE stream is established and actively streaming.
118
+ const controller = new AbortController();
119
+ const timeoutId = setTimeout(() => controller.abort(), 60_000);
120
+ this.streamController = controller;
121
+ const { response, release } = await urbitFetch({
122
+ baseUrl: this.url,
123
+ path: `/~/channel/${this.channelId}`,
124
+ init: {
125
+ method: "GET",
126
+ headers: {
127
+ Accept: "text/event-stream",
128
+ Cookie: this.cookie,
129
+ },
130
+ },
131
+ ssrfPolicy: this.ssrfPolicy,
132
+ lookupFn: this.lookupFn,
133
+ fetchImpl: this.fetchImpl,
134
+ signal: controller.signal,
135
+ auditContext: "tlon-urbit-sse-stream",
136
+ });
137
+ this.streamRelease = release;
138
+ // Clear timeout once connection established (headers received).
139
+ clearTimeout(timeoutId);
140
+ if (!response.ok) {
141
+ await release();
142
+ this.streamRelease = null;
143
+ throw new Error(`Stream connection failed: ${response.status}`);
144
+ }
145
+ this.processStream(response.body).catch((error) => {
146
+ if (!this.aborted) {
147
+ this.logger.error?.(`Stream error: ${String(error)}`);
148
+ for (const { err } of this.eventHandlers.values()) {
149
+ if (err) {
150
+ err(error);
151
+ }
152
+ }
153
+ }
154
+ });
155
+ }
156
+ async processStream(body) {
157
+ if (!body) {
158
+ return;
159
+ }
160
+ // oxlint-disable-next-line typescript/no-explicit-any
161
+ const stream = body instanceof ReadableStream ? Readable.fromWeb(body) : body;
162
+ let buffer = "";
163
+ try {
164
+ for await (const chunk of stream) {
165
+ if (this.aborted) {
166
+ break;
167
+ }
168
+ buffer += chunk.toString();
169
+ let eventEnd;
170
+ while ((eventEnd = buffer.indexOf("\n\n")) !== -1) {
171
+ const eventData = buffer.substring(0, eventEnd);
172
+ buffer = buffer.substring(eventEnd + 2);
173
+ this.processEvent(eventData);
174
+ }
175
+ }
176
+ }
177
+ finally {
178
+ if (this.streamRelease) {
179
+ const release = this.streamRelease;
180
+ this.streamRelease = null;
181
+ await release();
182
+ }
183
+ this.streamController = null;
184
+ if (!this.aborted && this.autoReconnect) {
185
+ this.isConnected = false;
186
+ this.logger.log?.("[SSE] Stream ended, attempting reconnection...");
187
+ await this.attemptReconnect();
188
+ }
189
+ }
190
+ }
191
+ processEvent(eventData) {
192
+ const lines = eventData.split("\n");
193
+ let data = null;
194
+ let eventId = null;
195
+ for (const line of lines) {
196
+ if (line.startsWith("id: ")) {
197
+ eventId = parseInt(line.substring(4), 10);
198
+ }
199
+ if (line.startsWith("data: ")) {
200
+ data = line.substring(6);
201
+ }
202
+ }
203
+ if (!data) {
204
+ return;
205
+ }
206
+ // Track event ID and send ack if needed
207
+ if (eventId !== null && !isNaN(eventId)) {
208
+ if (eventId > this.lastHeardEventId) {
209
+ this.lastHeardEventId = eventId;
210
+ if (eventId - this.lastAcknowledgedEventId > this.ackThreshold) {
211
+ this.logger.log?.(`[SSE] Acking event ${eventId} (last acked: ${this.lastAcknowledgedEventId})`);
212
+ this.ack(eventId).catch((err) => {
213
+ this.logger.error?.(`Failed to ack event ${eventId}: ${String(err)}`);
214
+ });
215
+ }
216
+ }
217
+ }
218
+ try {
219
+ const parsed = JSON.parse(data);
220
+ if (parsed.response === "quit") {
221
+ if (parsed.id) {
222
+ const handlers = this.eventHandlers.get(parsed.id);
223
+ if (handlers?.quit) {
224
+ handlers.quit();
225
+ }
226
+ // Auto-resubscribe after the agent kicks us
227
+ void this.resubscribeAfterQuit(parsed.id);
228
+ }
229
+ return;
230
+ }
231
+ if (parsed.id && this.eventHandlers.has(parsed.id)) {
232
+ const { event } = this.eventHandlers.get(parsed.id) ?? {};
233
+ if (event && parsed.json) {
234
+ event(parsed.json);
235
+ }
236
+ }
237
+ else if (parsed.json) {
238
+ for (const { event } of this.eventHandlers.values()) {
239
+ if (event) {
240
+ event(parsed.json);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ catch (error) {
246
+ this.logger.error?.(`Error parsing SSE event: ${String(error)}`);
247
+ }
248
+ }
249
+ async poke(params) {
250
+ return await pokeUrbitChannel({
251
+ baseUrl: this.url,
252
+ cookie: this.cookie,
253
+ ship: this.ship,
254
+ channelId: this.channelId,
255
+ ssrfPolicy: this.ssrfPolicy,
256
+ lookupFn: this.lookupFn,
257
+ fetchImpl: this.fetchImpl,
258
+ }, { ...params, auditContext: "tlon-urbit-poke" });
259
+ }
260
+ async scry(path) {
261
+ return await scryUrbitPath({
262
+ baseUrl: this.url,
263
+ cookie: this.cookie,
264
+ ssrfPolicy: this.ssrfPolicy,
265
+ lookupFn: this.lookupFn,
266
+ fetchImpl: this.fetchImpl,
267
+ }, { path, auditContext: "tlon-urbit-scry" });
268
+ }
269
+ /**
270
+ * Update the cookie used for authentication.
271
+ * Call this when re-authenticating after session expiry.
272
+ */
273
+ updateCookie(newCookie) {
274
+ this.cookie = normalizeUrbitCookie(newCookie);
275
+ }
276
+ async ack(eventId) {
277
+ this.lastAcknowledgedEventId = eventId;
278
+ const ackData = {
279
+ id: Date.now(),
280
+ action: "ack",
281
+ "event-id": eventId,
282
+ };
283
+ const { response, release } = await urbitFetch({
284
+ baseUrl: this.url,
285
+ path: `/~/channel/${this.channelId}`,
286
+ init: {
287
+ method: "PUT",
288
+ headers: {
289
+ "Content-Type": "application/json",
290
+ Cookie: this.cookie,
291
+ },
292
+ body: JSON.stringify([ackData]),
293
+ },
294
+ ssrfPolicy: this.ssrfPolicy,
295
+ lookupFn: this.lookupFn,
296
+ fetchImpl: this.fetchImpl,
297
+ timeoutMs: 10_000,
298
+ auditContext: "tlon-urbit-ack",
299
+ });
300
+ try {
301
+ if (!response.ok) {
302
+ throw new Error(`Ack failed with status ${response.status}`);
303
+ }
304
+ }
305
+ finally {
306
+ await release();
307
+ }
308
+ }
309
+ async attemptReconnect() {
310
+ if (this.aborted || !this.autoReconnect) {
311
+ this.logger.log?.("[SSE] Reconnection aborted or disabled");
312
+ return;
313
+ }
314
+ // If we've hit max attempts, wait longer then reset and keep trying
315
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
316
+ this.logger.log?.(`[SSE] Max reconnection attempts (${this.maxReconnectAttempts}) reached. Waiting 10s before resetting...`);
317
+ // Wait 10 seconds before resetting and trying again
318
+ const extendedBackoff = 10000; // 10 seconds
319
+ await new Promise((resolve) => setTimeout(resolve, extendedBackoff));
320
+ this.reconnectAttempts = 0; // Reset counter to continue trying
321
+ this.logger.log?.("[SSE] Reconnection attempts reset, resuming reconnection...");
322
+ }
323
+ this.reconnectAttempts += 1;
324
+ const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
325
+ this.logger.log?.(`[SSE] Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms...`);
326
+ await new Promise((resolve) => setTimeout(resolve, delay));
327
+ try {
328
+ this.channelId = `${Math.floor(Date.now() / 1000)}-${Math.random().toString(36).substring(2, 8)}`;
329
+ this.channelUrl = new URL(`/~/channel/${this.channelId}`, this.url).toString();
330
+ if (this.onReconnect) {
331
+ await this.onReconnect(this);
332
+ }
333
+ await this.connect();
334
+ this.logger.log?.("[SSE] Reconnection successful!");
335
+ }
336
+ catch (error) {
337
+ this.logger.error?.(`[SSE] Reconnection failed: ${String(error)}`);
338
+ await this.attemptReconnect();
339
+ }
340
+ }
341
+ /**
342
+ * Re-subscribe to an app/path after the Gall agent sends a quit.
343
+ * Creates a new subscription with a fresh ID, transfers event handlers,
344
+ * and retries with exponential backoff.
345
+ */
346
+ async resubscribeAfterQuit(oldSubId) {
347
+ const oldSub = this.subscriptions.find((s) => s.id === oldSubId);
348
+ if (!oldSub || this.aborted)
349
+ return;
350
+ const handlers = this.eventHandlers.get(oldSubId);
351
+ if (!handlers)
352
+ return;
353
+ const maxAttempts = 5;
354
+ const baseDelay = 2000;
355
+ const maxDelay = 30000;
356
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
357
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
358
+ this.logger.log?.(`[SSE] Resubscribing to ${oldSub.app}${oldSub.path} after quit (attempt ${attempt}/${maxAttempts}) in ${delay}ms...`);
359
+ await new Promise((resolve) => setTimeout(resolve, delay));
360
+ if (this.aborted || !this.isConnected)
361
+ return;
362
+ try {
363
+ const newSubId = this.subscriptions.length + 1;
364
+ const newSub = {
365
+ id: newSubId,
366
+ action: "subscribe",
367
+ ship: this.ship,
368
+ app: oldSub.app,
369
+ path: oldSub.path,
370
+ };
371
+ this.subscriptions.push(newSub);
372
+ this.eventHandlers.set(newSubId, handlers);
373
+ this.eventHandlers.delete(oldSubId);
374
+ await this.sendSubscription(newSub);
375
+ this.logger.log?.(`[SSE] Resubscribed to ${oldSub.app}${oldSub.path} successfully (new id=${newSubId})`);
376
+ return;
377
+ }
378
+ catch (error) {
379
+ this.logger.error?.(`[SSE] Resubscribe failed for ${oldSub.app}${oldSub.path}: ${String(error)}`);
380
+ }
381
+ }
382
+ this.logger.error?.(`[SSE] Failed to resubscribe to ${oldSub.app}${oldSub.path} after ${maxAttempts} attempts`);
383
+ }
384
+ async close() {
385
+ this.aborted = true;
386
+ this.isConnected = false;
387
+ this.streamController?.abort();
388
+ try {
389
+ const unsubscribes = this.subscriptions.map((sub) => ({
390
+ id: sub.id,
391
+ action: "unsubscribe",
392
+ subscription: sub.id,
393
+ }));
394
+ {
395
+ const { response, release } = await urbitFetch({
396
+ baseUrl: this.url,
397
+ path: `/~/channel/${this.channelId}`,
398
+ init: {
399
+ method: "PUT",
400
+ headers: {
401
+ "Content-Type": "application/json",
402
+ Cookie: this.cookie,
403
+ },
404
+ body: JSON.stringify(unsubscribes),
405
+ },
406
+ ssrfPolicy: this.ssrfPolicy,
407
+ lookupFn: this.lookupFn,
408
+ fetchImpl: this.fetchImpl,
409
+ timeoutMs: 30_000,
410
+ auditContext: "tlon-urbit-unsubscribe",
411
+ });
412
+ try {
413
+ void response.body?.cancel();
414
+ }
415
+ finally {
416
+ await release();
417
+ }
418
+ }
419
+ {
420
+ const { response, release } = await urbitFetch({
421
+ baseUrl: this.url,
422
+ path: `/~/channel/${this.channelId}`,
423
+ init: {
424
+ method: "DELETE",
425
+ headers: {
426
+ Cookie: this.cookie,
427
+ },
428
+ },
429
+ ssrfPolicy: this.ssrfPolicy,
430
+ lookupFn: this.lookupFn,
431
+ fetchImpl: this.fetchImpl,
432
+ timeoutMs: 30_000,
433
+ auditContext: "tlon-urbit-channel-close",
434
+ });
435
+ try {
436
+ void response.body?.cancel();
437
+ }
438
+ finally {
439
+ await release();
440
+ }
441
+ }
442
+ }
443
+ catch (error) {
444
+ this.logger.error?.(`Error closing channel: ${String(error)}`);
445
+ }
446
+ if (this.streamRelease) {
447
+ const release = this.streamRelease;
448
+ this.streamRelease = null;
449
+ await release();
450
+ }
451
+ }
452
+ }
453
+ //# sourceMappingURL=sse-client.js.map