tandem-editor 0.8.0 → 0.9.1
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/CHANGELOG.md +84 -8
- package/README.md +46 -52
- package/dist/channel/index.js +201 -95
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +210 -96
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/{CoworkSettings-CXODT6KV.js → CoworkSettings-BlGNryD3.js} +1 -1
- package/dist/client/assets/index-Bnc4LNBi.css +1 -0
- package/dist/client/assets/index-D0gSEOxm.js +228 -0
- package/dist/client/index.html +2 -2
- package/dist/monitor/index.js +4 -4
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +644 -306
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
- package/sample/table-test.md +8 -0
- package/skills/tandem/SKILL.md +11 -11
- package/dist/client/assets/index-C6rbXHNq.css +0 -1
- package/dist/client/assets/index-q_8NVSM3.js +0 -228
package/dist/channel/index.js
CHANGED
|
@@ -17914,6 +17914,14 @@ var IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
|
17914
17914
|
var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
17915
17915
|
var CHANNEL_MAX_RETRIES = 5;
|
|
17916
17916
|
var CHANNEL_RETRY_DELAY_MS = 2e3;
|
|
17917
|
+
var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
|
|
17918
|
+
var CHANNEL_SSE_INACTIVITY_TIMEOUT_MS = 6e4;
|
|
17919
|
+
var CHANNEL_MODE_FETCH_TIMEOUT_MS = 2e3;
|
|
17920
|
+
var CHANNEL_AWARENESS_FETCH_TIMEOUT_MS = 5e3;
|
|
17921
|
+
var CHANNEL_ERROR_REPORT_TIMEOUT_MS = 3e3;
|
|
17922
|
+
var CHANNEL_REPLY_FETCH_TIMEOUT_MS = 5e3;
|
|
17923
|
+
var CHANNEL_PERMISSION_FETCH_TIMEOUT_MS = 5e3;
|
|
17924
|
+
var CHANNEL_MAX_SSE_BUFFER_BYTES = 1e6;
|
|
17917
17925
|
|
|
17918
17926
|
// src/shared/cli-runtime.ts
|
|
17919
17927
|
function redirectConsoleToStderr() {
|
|
@@ -17945,6 +17953,21 @@ async function authFetch(url, init) {
|
|
|
17945
17953
|
return fetch(url, init);
|
|
17946
17954
|
}
|
|
17947
17955
|
|
|
17956
|
+
// src/shared/fetch-with-timeout.ts
|
|
17957
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
17958
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
17959
|
+
return authFetch(url, { ...init, signal });
|
|
17960
|
+
}
|
|
17961
|
+
function describeFetchError(err, endpoint, timeoutMs) {
|
|
17962
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
17963
|
+
return `${endpoint} timed out after ${timeoutMs}ms`;
|
|
17964
|
+
}
|
|
17965
|
+
return err instanceof Error ? err.message : String(err);
|
|
17966
|
+
}
|
|
17967
|
+
function isAbortOrTimeoutError(err) {
|
|
17968
|
+
return err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError");
|
|
17969
|
+
}
|
|
17970
|
+
|
|
17948
17971
|
// src/shared/events/types.ts
|
|
17949
17972
|
var VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
17950
17973
|
"annotation:created",
|
|
@@ -17966,9 +17989,9 @@ function formatEventContent(event) {
|
|
|
17966
17989
|
const doc = event.documentId ? ` [doc: ${event.documentId}]` : "";
|
|
17967
17990
|
switch (event.type) {
|
|
17968
17991
|
case "annotation:created": {
|
|
17969
|
-
const { annotationType, content, textSnippet, hasSuggestedText
|
|
17992
|
+
const { annotationType, content, textSnippet, hasSuggestedText } = event.payload;
|
|
17970
17993
|
const snippet = textSnippet ? ` on "${textSnippet}"` : "";
|
|
17971
|
-
const label = hasSuggestedText ? "replacement" :
|
|
17994
|
+
const label = hasSuggestedText ? "replacement" : annotationType;
|
|
17972
17995
|
return `User created ${label}${snippet}: ${content || "(no content)"}${doc}`;
|
|
17973
17996
|
}
|
|
17974
17997
|
case "annotation:accepted": {
|
|
@@ -18063,18 +18086,22 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18063
18086
|
if (retries >= CHANNEL_MAX_RETRIES) {
|
|
18064
18087
|
console.error("[Channel] SSE connection exhausted, reporting error and exiting");
|
|
18065
18088
|
try {
|
|
18066
|
-
await
|
|
18067
|
-
|
|
18068
|
-
|
|
18069
|
-
|
|
18070
|
-
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18089
|
+
await fetchWithTimeout(
|
|
18090
|
+
`${tandemUrl}/api/channel-error`,
|
|
18091
|
+
{
|
|
18092
|
+
method: "POST",
|
|
18093
|
+
headers: { "Content-Type": "application/json" },
|
|
18094
|
+
body: JSON.stringify({
|
|
18095
|
+
error: "CHANNEL_CONNECT_FAILED",
|
|
18096
|
+
message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
|
|
18097
|
+
})
|
|
18098
|
+
},
|
|
18099
|
+
CHANNEL_ERROR_REPORT_TIMEOUT_MS
|
|
18100
|
+
);
|
|
18074
18101
|
} catch (reportErr) {
|
|
18075
18102
|
console.error(
|
|
18076
18103
|
"[Channel] Could not report failure to server:",
|
|
18077
|
-
reportErr
|
|
18104
|
+
describeFetchError(reportErr, "/api/channel-error", CHANNEL_ERROR_REPORT_TIMEOUT_MS)
|
|
18078
18105
|
);
|
|
18079
18106
|
}
|
|
18080
18107
|
process.exit(1);
|
|
@@ -18086,42 +18113,80 @@ async function startEventBridge(mcp, tandemUrl) {
|
|
|
18086
18113
|
async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
18087
18114
|
const headers = { Accept: "text/event-stream" };
|
|
18088
18115
|
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
18089
|
-
const
|
|
18116
|
+
const connectCtrl = new AbortController();
|
|
18117
|
+
const connectTimer = setTimeout(
|
|
18118
|
+
() => connectCtrl.abort(new Error("handshake timeout")),
|
|
18119
|
+
CHANNEL_CONNECT_FETCH_TIMEOUT_MS
|
|
18120
|
+
);
|
|
18121
|
+
let res;
|
|
18122
|
+
try {
|
|
18123
|
+
res = await authFetch(`${tandemUrl}/api/events`, { headers, signal: connectCtrl.signal });
|
|
18124
|
+
} finally {
|
|
18125
|
+
clearTimeout(connectTimer);
|
|
18126
|
+
}
|
|
18090
18127
|
if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);
|
|
18091
18128
|
if (!res.body) throw new Error("SSE endpoint returned no body");
|
|
18092
18129
|
const reader = res.body.getReader();
|
|
18093
18130
|
const decoder = new TextDecoder();
|
|
18094
18131
|
let buffer = "";
|
|
18132
|
+
let lastActivityAt = Date.now();
|
|
18133
|
+
let inactivityTimedOut = false;
|
|
18134
|
+
const watchdog = setInterval(() => {
|
|
18135
|
+
if (Date.now() - lastActivityAt > CHANNEL_SSE_INACTIVITY_TIMEOUT_MS) {
|
|
18136
|
+
inactivityTimedOut = true;
|
|
18137
|
+
reader.cancel(new Error("SSE inactivity timeout")).catch(() => {
|
|
18138
|
+
});
|
|
18139
|
+
}
|
|
18140
|
+
}, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
|
|
18095
18141
|
let awarenessTimer = null;
|
|
18096
18142
|
let clearAwarenessTimer = null;
|
|
18097
18143
|
let pendingAwareness = null;
|
|
18098
18144
|
const AWARENESS_CLEAR_MS = 3e3;
|
|
18099
18145
|
function clearAwareness(documentId) {
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
|
|
18103
|
-
|
|
18104
|
-
|
|
18105
|
-
|
|
18106
|
-
|
|
18107
|
-
|
|
18108
|
-
|
|
18146
|
+
fetchWithTimeout(
|
|
18147
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18148
|
+
{
|
|
18149
|
+
method: "POST",
|
|
18150
|
+
headers: { "Content-Type": "application/json" },
|
|
18151
|
+
body: JSON.stringify({
|
|
18152
|
+
documentId: documentId ?? null,
|
|
18153
|
+
status: "idle",
|
|
18154
|
+
active: false
|
|
18155
|
+
})
|
|
18156
|
+
},
|
|
18157
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18158
|
+
).catch((err) => {
|
|
18159
|
+
console.error(
|
|
18160
|
+
"[Channel] clearAwareness failed (non-fatal):",
|
|
18161
|
+
describeFetchError(err, "/api/channel-awareness clear", CHANNEL_AWARENESS_FETCH_TIMEOUT_MS)
|
|
18162
|
+
);
|
|
18109
18163
|
});
|
|
18110
18164
|
}
|
|
18111
18165
|
function flushAwareness() {
|
|
18112
18166
|
if (!pendingAwareness) return;
|
|
18113
18167
|
const event = pendingAwareness;
|
|
18114
18168
|
pendingAwareness = null;
|
|
18115
|
-
|
|
18116
|
-
|
|
18117
|
-
|
|
18118
|
-
|
|
18119
|
-
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18169
|
+
fetchWithTimeout(
|
|
18170
|
+
`${tandemUrl}/api/channel-awareness`,
|
|
18171
|
+
{
|
|
18172
|
+
method: "POST",
|
|
18173
|
+
headers: { "Content-Type": "application/json" },
|
|
18174
|
+
body: JSON.stringify({
|
|
18175
|
+
documentId: event.documentId,
|
|
18176
|
+
status: `processing: ${event.type}`,
|
|
18177
|
+
active: true
|
|
18178
|
+
})
|
|
18179
|
+
},
|
|
18180
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18181
|
+
).catch((err) => {
|
|
18182
|
+
console.error(
|
|
18183
|
+
"[Channel] Awareness update failed:",
|
|
18184
|
+
describeFetchError(
|
|
18185
|
+
err,
|
|
18186
|
+
"/api/channel-awareness update",
|
|
18187
|
+
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18188
|
+
)
|
|
18189
|
+
);
|
|
18125
18190
|
});
|
|
18126
18191
|
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18127
18192
|
clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);
|
|
@@ -18131,56 +18196,81 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
|
|
|
18131
18196
|
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18132
18197
|
awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
|
|
18133
18198
|
}
|
|
18134
|
-
|
|
18135
|
-
|
|
18136
|
-
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18148
|
-
}
|
|
18149
|
-
if (!data) continue;
|
|
18150
|
-
let event;
|
|
18151
|
-
try {
|
|
18152
|
-
event = parseTandemEvent(JSON.parse(data));
|
|
18153
|
-
} catch {
|
|
18154
|
-
console.error("[Channel] Malformed SSE event data (skipping):", data.slice(0, 200));
|
|
18155
|
-
continue;
|
|
18156
|
-
}
|
|
18157
|
-
if (!event) {
|
|
18158
|
-
console.error("[Channel] Received invalid SSE event, skipping");
|
|
18159
|
-
continue;
|
|
18199
|
+
try {
|
|
18200
|
+
while (true) {
|
|
18201
|
+
const { done, value } = await reader.read();
|
|
18202
|
+
if (done) {
|
|
18203
|
+
if (inactivityTimedOut) throw new Error("SSE inactivity timeout");
|
|
18204
|
+
throw new Error("SSE stream ended");
|
|
18205
|
+
}
|
|
18206
|
+
lastActivityAt = Date.now();
|
|
18207
|
+
buffer += decoder.decode(value, { stream: true });
|
|
18208
|
+
if (buffer.length > CHANNEL_MAX_SSE_BUFFER_BYTES) {
|
|
18209
|
+
throw new Error(
|
|
18210
|
+
`SSE buffer exceeded ${CHANNEL_MAX_SSE_BUFFER_BYTES} bytes without a frame boundary`
|
|
18211
|
+
);
|
|
18160
18212
|
}
|
|
18161
|
-
|
|
18162
|
-
|
|
18163
|
-
|
|
18164
|
-
|
|
18213
|
+
let boundary;
|
|
18214
|
+
while ((boundary = buffer.indexOf("\n\n")) !== -1) {
|
|
18215
|
+
const frame = buffer.slice(0, boundary);
|
|
18216
|
+
buffer = buffer.slice(boundary + 2);
|
|
18217
|
+
if (frame.startsWith(":")) continue;
|
|
18218
|
+
let eventId;
|
|
18219
|
+
let data;
|
|
18220
|
+
for (const line of frame.split("\n")) {
|
|
18221
|
+
if (line.startsWith("id: ")) eventId = line.slice(4);
|
|
18222
|
+
else if (line.startsWith("data: ")) data = line.slice(6);
|
|
18223
|
+
}
|
|
18224
|
+
if (!data) continue;
|
|
18225
|
+
let event;
|
|
18226
|
+
try {
|
|
18227
|
+
event = parseTandemEvent(JSON.parse(data));
|
|
18228
|
+
} catch {
|
|
18229
|
+
console.error(
|
|
18230
|
+
"[Channel] Malformed SSE event data (skipping), eventId=%s:",
|
|
18231
|
+
eventId,
|
|
18232
|
+
data.slice(0, 200)
|
|
18233
|
+
);
|
|
18165
18234
|
if (eventId) onEventId(eventId);
|
|
18166
18235
|
continue;
|
|
18167
18236
|
}
|
|
18168
|
-
|
|
18169
|
-
|
|
18170
|
-
|
|
18171
|
-
|
|
18172
|
-
|
|
18173
|
-
|
|
18174
|
-
|
|
18175
|
-
|
|
18237
|
+
if (!event) {
|
|
18238
|
+
console.error(
|
|
18239
|
+
"[Channel] Invalid SSE event structure (skipping), eventId=%s:",
|
|
18240
|
+
eventId,
|
|
18241
|
+
data.slice(0, 200)
|
|
18242
|
+
);
|
|
18243
|
+
if (eventId) onEventId(eventId);
|
|
18244
|
+
continue;
|
|
18245
|
+
}
|
|
18246
|
+
if (event.type !== "chat:message") {
|
|
18247
|
+
const mode = await getCachedMode(tandemUrl);
|
|
18248
|
+
if (mode === "solo") {
|
|
18249
|
+
console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
|
|
18250
|
+
if (eventId) onEventId(eventId);
|
|
18251
|
+
continue;
|
|
18176
18252
|
}
|
|
18177
|
-
}
|
|
18178
|
-
|
|
18179
|
-
|
|
18180
|
-
|
|
18253
|
+
}
|
|
18254
|
+
try {
|
|
18255
|
+
await mcp.notification({
|
|
18256
|
+
method: "notifications/claude/channel",
|
|
18257
|
+
params: {
|
|
18258
|
+
content: formatEventContent(event),
|
|
18259
|
+
meta: formatEventMeta(event)
|
|
18260
|
+
}
|
|
18261
|
+
});
|
|
18262
|
+
} catch (err) {
|
|
18263
|
+
console.error("[Channel] MCP notification failed (transport broken?):", err);
|
|
18264
|
+
throw err;
|
|
18265
|
+
}
|
|
18266
|
+
if (eventId) onEventId(eventId);
|
|
18267
|
+
scheduleAwareness(event);
|
|
18181
18268
|
}
|
|
18182
|
-
scheduleAwareness(event);
|
|
18183
18269
|
}
|
|
18270
|
+
} finally {
|
|
18271
|
+
clearInterval(watchdog);
|
|
18272
|
+
if (awarenessTimer) clearTimeout(awarenessTimer);
|
|
18273
|
+
if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
|
|
18184
18274
|
}
|
|
18185
18275
|
}
|
|
18186
18276
|
var cachedMode = "tandem";
|
|
@@ -18189,7 +18279,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18189
18279
|
const now = Date.now();
|
|
18190
18280
|
if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
|
|
18191
18281
|
try {
|
|
18192
|
-
const res = await
|
|
18282
|
+
const res = await fetchWithTimeout(`${tandemUrl}/api/mode`, {}, CHANNEL_MODE_FETCH_TIMEOUT_MS);
|
|
18193
18283
|
if (res.ok) {
|
|
18194
18284
|
const { mode } = await res.json();
|
|
18195
18285
|
cachedMode = mode;
|
|
@@ -18200,7 +18290,7 @@ async function getCachedMode(tandemUrl) {
|
|
|
18200
18290
|
} catch (err) {
|
|
18201
18291
|
console.error(
|
|
18202
18292
|
"[Channel] Mode check failed, delivering event (fail-open):",
|
|
18203
|
-
err
|
|
18293
|
+
describeFetchError(err, "/api/mode", CHANNEL_MODE_FETCH_TIMEOUT_MS)
|
|
18204
18294
|
);
|
|
18205
18295
|
cachedModeAt = now;
|
|
18206
18296
|
}
|
|
@@ -18227,7 +18317,7 @@ async function runChannel(opts = {}) {
|
|
|
18227
18317
|
"Event types: annotation:created, annotation:accepted, annotation:dismissed, annotation:reply,",
|
|
18228
18318
|
"chat:message, document:opened, document:closed, document:switched.",
|
|
18229
18319
|
"Chat messages may include a 'selection' field with buffered selection context.",
|
|
18230
|
-
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment,
|
|
18320
|
+
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_edit, etc.) to act on them.",
|
|
18231
18321
|
"Reply to chat messages using tandem_reply. Pass document_id from the tag attributes.",
|
|
18232
18322
|
"Do not reply to non-chat events \u2014 just act on them using tools.",
|
|
18233
18323
|
"If you haven't received channel notifications recently, call tandem_checkInbox as a fallback."
|
|
@@ -18261,15 +18351,20 @@ async function runChannel(opts = {}) {
|
|
|
18261
18351
|
if (req.params.name === "tandem_reply") {
|
|
18262
18352
|
const args = req.params.arguments;
|
|
18263
18353
|
try {
|
|
18264
|
-
const res = await
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18354
|
+
const res = await fetchWithTimeout(
|
|
18355
|
+
`${tandemUrl}/api/channel-reply`,
|
|
18356
|
+
{
|
|
18357
|
+
method: "POST",
|
|
18358
|
+
headers: { "Content-Type": "application/json" },
|
|
18359
|
+
body: JSON.stringify(args)
|
|
18360
|
+
},
|
|
18361
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18362
|
+
);
|
|
18269
18363
|
let data;
|
|
18270
18364
|
try {
|
|
18271
18365
|
data = await res.json();
|
|
18272
|
-
} catch {
|
|
18366
|
+
} catch (parseErr) {
|
|
18367
|
+
if (isAbortOrTimeoutError(parseErr)) throw parseErr;
|
|
18273
18368
|
data = { message: "Non-JSON response" };
|
|
18274
18369
|
}
|
|
18275
18370
|
if (!res.ok) {
|
|
@@ -18289,7 +18384,11 @@ async function runChannel(opts = {}) {
|
|
|
18289
18384
|
content: [
|
|
18290
18385
|
{
|
|
18291
18386
|
type: "text",
|
|
18292
|
-
text: `Failed to send reply: ${
|
|
18387
|
+
text: `Failed to send reply: ${describeFetchError(
|
|
18388
|
+
err,
|
|
18389
|
+
"/api/channel-reply",
|
|
18390
|
+
CHANNEL_REPLY_FETCH_TIMEOUT_MS
|
|
18391
|
+
)}`
|
|
18293
18392
|
}
|
|
18294
18393
|
],
|
|
18295
18394
|
isError: true
|
|
@@ -18309,23 +18408,30 @@ async function runChannel(opts = {}) {
|
|
|
18309
18408
|
});
|
|
18310
18409
|
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
|
|
18311
18410
|
try {
|
|
18312
|
-
const res = await
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
|
|
18411
|
+
const res = await fetchWithTimeout(
|
|
18412
|
+
`${tandemUrl}/api/channel-permission`,
|
|
18413
|
+
{
|
|
18414
|
+
method: "POST",
|
|
18415
|
+
headers: { "Content-Type": "application/json" },
|
|
18416
|
+
body: JSON.stringify({
|
|
18417
|
+
requestId: params.request_id,
|
|
18418
|
+
toolName: params.tool_name,
|
|
18419
|
+
description: params.description,
|
|
18420
|
+
inputPreview: params.input_preview
|
|
18421
|
+
})
|
|
18422
|
+
},
|
|
18423
|
+
CHANNEL_PERMISSION_FETCH_TIMEOUT_MS
|
|
18424
|
+
);
|
|
18322
18425
|
if (!res.ok) {
|
|
18323
18426
|
console.error(
|
|
18324
18427
|
`[Channel] Permission relay got HTTP ${res.status} \u2014 browser may not see prompt`
|
|
18325
18428
|
);
|
|
18326
18429
|
}
|
|
18327
18430
|
} catch (err) {
|
|
18328
|
-
console.error(
|
|
18431
|
+
console.error(
|
|
18432
|
+
"[Channel] Failed to forward permission request:",
|
|
18433
|
+
describeFetchError(err, "/api/channel-permission", CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
|
|
18434
|
+
);
|
|
18329
18435
|
}
|
|
18330
18436
|
});
|
|
18331
18437
|
console.error(`[Channel] Tandem channel shim starting (server: ${tandemUrl})`);
|