tinyjuice 0.2.1

Pluggable token compression for OpenHuman.
Documentation
diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx
index 928fc0968..d649681fb 100644
--- a/app/src/pages/Conversations.tsx
+++ b/app/src/pages/Conversations.tsx
@@ -70,160 +70,165 @@ import {
   type ToolTimelineEntry,
 } from '../store/chatRuntimeSlice';
 import { useAppDispatch, useAppSelector } from '../store/hooks';
[... 74 context line(s) omitted ...]
 const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 320;
 const AUTOCOMPLETE_MIN_CONTEXT_CHARS = 3;
 const debug = debugFactory('conversations');
+const SAFE_IMAGE_DATA_URI_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,[a-z0-9+/=\s]+$/i;
+
+function isSafeAttachmentImageSrc(src: string): boolean {
+  return SAFE_IMAGE_DATA_URI_RE.test(src);
+}
 
 interface ConversationsProps {
   /**
[... 74 context line(s) omitted ...]
   }
   return String(err);
 }
@@ -2244,163 +2249,165 @@ const Conversations = ({
                                           className="flex items-center rounded-full border border-primary-200 bg-primary-100 px-1.5 text-xs leading-[1.5] shadow-sm transition-colors hover:bg-primary-200 dark:border-primary-400/40 dark:bg-primary-500/25"
                                           title={t('chat.removeReaction').replace(
                                             '{emoji}',
[... 74 context line(s) omitted ...]
                           <div className="flex flex-col items-end gap-1">
                             {(() => {
                               const displayText = parsedContent.text;
-                              const dataUris = Array.isArray(msg.extraMetadata?.attachmentDataUris)
-                                ? (msg.extraMetadata.attachmentDataUris as string[])
-                                : parsedContent.dataUris;
+                              const dataUris = (
+                                Array.isArray(msg.extraMetadata?.attachmentDataUris)
+                                  ? (msg.extraMetadata.attachmentDataUris as string[])
+                                  : parsedContent.dataUris
+                              ).filter(isSafeAttachmentImageSrc);
                               const hasImages = dataUris.length > 0;
                               // Document attachments carry no image data-URI (only
                               // images do); surface them as filename chips from the
[... 74 context line(s) omitted ...]
                                         </div>
                                       ))}
                                     </div>
diff --git a/app/test/e2e/specs/connector-jira.spec.ts b/app/test/e2e/specs/connector-jira.spec.ts
index 8718d16f3..6e77c7731 100644
--- a/app/test/e2e/specs/connector-jira.spec.ts
+++ b/app/test/e2e/specs/connector-jira.spec.ts
@@ -37,164 +37,172 @@ const LOG = '[ConnectorJiraE2E]';
 const CONNECTOR_NAME = 'Jira';
 const TOOLKIT_SLUG = 'jira';
 const AUTH_TOKEN = 'e2e-connector-jira-token';
[... 74 context line(s) omitted ...]
         return (
           document.querySelector('[data-testid="composio-required-subdomain"]') !== null ||
           document.querySelector('input[placeholder*="subdomain"]') !== null ||
-          // fallback: any .atlassian.net suffix label
-          Array.from(document.querySelectorAll('*')).some(el =>
-            (el.textContent ?? '').includes('.atlassian.net')
-          )
+          // fallback: any visible Jira tenant URL label
+          Array.from(document.querySelectorAll('*')).some(el => {
+            const text = el.textContent ?? '';
+            const matches = text.match(/https?:\/\/[^\s"'<>]+/g) ?? [];
+            return matches.some(candidate => {
+              try {
+                return new URL(candidate).hostname.endsWith('.atlassian.net');
+              } catch {
+                return false;
+              }
+            });
+          })
         );
       })
       .catch(() => false);
[... 74 context line(s) omitted ...]
   it('expired auth shows Reconnect button and does not log user out', async function () {
     this.timeout(60_000);
     seedComposioConnection(TOOLKIT_SLUG, 'EXPIRED', 'c-jira-expired');
diff --git a/app/test/e2e/specs/mega-flow.spec.ts b/app/test/e2e/specs/mega-flow.spec.ts
index 4f5bc3cbc..8225fa4ea 100644
--- a/app/test/e2e/specs/mega-flow.spec.ts
+++ b/app/test/e2e/specs/mega-flow.spec.ts
@@ -644,163 +644,168 @@ describe('Mega flow — login + Gmail OAuth + Composio in one session', () => {
     });
     const ingressBody = await ingressResp.json().catch(() => ({}));
     expect(ingressResp.status).toBe(200);
[... 74 context line(s) omitted ...]
     console.log(`${LOG} update.version: asset_prefix = ${prefix}`);
 
     // No outbound HTTP call should have been made — version is purely local.
-    const outbound = getRequestLog().find(
-      r => r.url.includes('github.com') || r.url.includes('update.openhuman.app')
-    );
+    const outbound = getRequestLog().find(r => {
+      try {
+        const url = new URL(r.url, 'http://mock.local');
+        return url.hostname === 'github.com' || url.hostname === 'update.openhuman.app';
+      } catch {
+        return false;
+      }
+    });
     expect(outbound).toBeUndefined();
 
     const ping = await callOpenhumanRpc('core.ping', {});
[... 74 context line(s) omitted ...]
   it('thread CRUD smoke: create → append message → list messages roundtrip', async () => {
     await resetEverything('before thread-crud-smoke scenario');
 
diff --git a/scripts/mock-api/routes/llm.mjs b/scripts/mock-api/routes/llm.mjs
index ef3402b1a..ed8609646 100644
--- a/scripts/mock-api/routes/llm.mjs
+++ b/scripts/mock-api/routes/llm.mjs
@@ -1,91 +1,93 @@
 import { json, setCors } from "../http.mjs";
 import {
   behavior,
[... 5 context line(s) omitted ...]
 import { buildDynamicCompletion } from "./llm/dynamic.mjs";
 import { headerValue, pickProbeText, resolveThreadKey } from "./llm/shared.mjs";
 
+const MAX_STREAM_DELAY_MS = 30_000;
+
 // The scripted `llmForcedResponses` FIFO models the *interactive* agent turn,
 // which always advertises tools (the orchestrator's delegate_* tools). Ancillary
 // completions that share the endpoint but carry no tools — thread-title/summary
[... 74 context line(s) omitted ...]
   });
 }
 
@@ -98,332 +100,332 @@ function sendRuleError(res, rule) {
 //   data: {"choices":[{"delta":{"content":"hello"},"finish_reason":null}]}
 //   data: {"choices":[{"delta":{},"finish_reason":"stop"}], "usage":{...}}
 //   data: [DONE]
[... 74 context line(s) omitted ...]
   return new Promise((resolve) => setTimeout(resolve, ms));
 }
 
+function safeDelayMs(raw, fallback = 0) {
+  const parsed = Number(raw);
+  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
+  return Math.min(parsed, MAX_STREAM_DELAY_MS);
+}
+
 // Split a string into N-character windows so we can stream tool-call
 // argument JSON the same way real providers do — clients accumulate the
 // partial fragments and JSON-parse at the end.
[... 131 context line(s) omitted ...]
     }
   }
 
-  const defaultDelayMs = Number.isFinite(
-    parseFloat(mockBehavior.llmStreamChunkDelayMs),
-  )
-    ? Math.max(0, parseFloat(mockBehavior.llmStreamChunkDelayMs))
-    : 25;
+  const defaultDelayMs = safeDelayMs(mockBehavior.llmStreamChunkDelayMs, 25);
 
   // Fire-and-forget — the dispatcher only cares that the handler
   // claimed the request. Errors mid-stream are surfaced through SSE.
[... 21 context line(s) omitted ...]
   let trailingUsage = null;
   for (let i = 0; i < script.length; i += 1) {
     const entry = script[i] ?? {};
-    const delay = Number.isFinite(entry.delayMs)
-      ? entry.delayMs
-      : defaultDelayMs;
+    const delay = safeDelayMs(entry.delayMs, defaultDelayMs);
     if (delay > 0) await sleep(delay);
 
     if (entry.error) {
[... 74 context line(s) omitted ...]
           finishReason: entry.finish,
           usage: trailingUsage ?? undefined,
         }),
diff --git a/scripts/mock-api/routes/oauth.mjs b/scripts/mock-api/routes/oauth.mjs
index 869beb7e5..63fc6c7d4 100644
--- a/scripts/mock-api/routes/oauth.mjs
+++ b/scripts/mock-api/routes/oauth.mjs
@@ -1,228 +1,248 @@
 import { html, json, setCors } from "../http.mjs";
 import { behavior, MOCK_JWT } from "../state.mjs";
 
[... 37 context line(s) omitted ...]
   const legacy = url.match(
     /^\/mock-(telegram|notion|google|gmail|slack|discord|twitter|github)-oauth\/?(\?.*)?$/i,
   );
-  const legacyGeneric = !newStyle && !legacy && /^\/mock-oauth\/?(\?.*)?$/.test(url);
+  const legacyGeneric =
+    !newStyle && !legacy && /^\/mock-oauth\/?(\?.*)?$/.test(url);
 
   if (method === "GET" && (newStyle || legacy || legacyGeneric)) {
     const provider = newStyle?.[1] || legacy?.[1] || "generic";
[... 22 context line(s) omitted ...]
           integrationId,
         )}&provider=${encodeURIComponent(params.provider || provider)}`;
 
-    html(res, 200, renderOAuthPage({ provider, target, autoRedirectMs, errorCode }));
+    html(
+      res,
+      200,
+      renderOAuthPage({ provider, target, autoRedirectMs, errorCode }),
+    );
     return true;
   }
 
   // Generic callback exchange. Real providers each hit their own
   // backend-specific URL; for e2e a single endpoint per provider that
   // always returns a session token is enough.
-  const callbackMatch = url.match(/^\/auth\/([a-z][a-z0-9_-]*)\/callback\/?(\?.*)?$/i);
+  const callbackMatch = url.match(
+    /^\/auth\/([a-z][a-z0-9_-]*)\/callback\/?(\?.*)?$/i,
+  );
   if (method === "GET" && callbackMatch) {
     const provider = callbackMatch[1];
     const params = parseQuery(url);
[... 72 context line(s) omitted ...]
     const key = eq < 0 ? pair : pair.slice(0, eq);
     const raw = eq < 0 ? "" : pair.slice(eq + 1);
     try {
-      out[decodeURIComponent(key)] = decodeURIComponent(raw.replace(/\+/g, " "));
+      out[decodeURIComponent(key)] = decodeURIComponent(
+        raw.replace(/\+/g, " "),
+      );
     } catch {
       out[key] = raw;
     }
[... 8 context line(s) omitted ...]
 }
 
 function escapeHtml(s) {
-  return String(s).replace(/[&<>"']/g, (c) => ({
-    "&": "&amp;",
-    "<": "&lt;",
-    ">": "&gt;",
-    '"': "&quot;",
-    "'": "&#39;",
-  })[c]);
+  return String(s).replace(
+    /[&<>"']/g,
+    (c) =>
+      ({
+        "&": "&amp;",
+        "<": "&lt;",
+        ">": "&gt;",
+        '"': "&quot;",
+        "'": "&#39;",
+      })[c],
+  );
 }
 
 function renderOAuthPage({ provider, target, autoRedirectMs, errorCode }) {
[... 9 context line(s) omitted ...]
   const autoRedirectScript =
     autoRedirectMs === null
       ? ""
-      : `<script>setTimeout(function(){window.location.href=${JSON.stringify(
-          target,
-        )};}, ${Number(autoRedirectMs)});</script>`;
+      : `<script>
+  (function () {
+    var delay = Number(document.body.dataset.autoRedirectMs || 0);
+    window.setTimeout(function () {
+      var link = document.getElementById("continue");
+      if (link instanceof HTMLAnchorElement) window.location.href = link.href;
+    }, Number.isFinite(delay) && delay >= 0 ? delay : 0);
+  })();
+  </script>`;
 
   const metaRefresh =
     autoRedirectMs === null
       ? ""
-      : `<meta http-equiv="refresh" content="${(Number(autoRedirectMs) /
-          1000).toFixed(2)};url=${safeTarget}" />`;
+      : `<meta http-equiv="refresh" content="${(
+          Number(autoRedirectMs) / 1000
+        ).toFixed(2)};url=${safeTarget}" />`;
 
   return `<!doctype html>
 <html lang="en">
[... 9 context line(s) omitted ...]
     code { background: #f3f3f3; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
   </style>
 </head>
-<body>
+<body${autoRedirectMs === null ? "" : ` data-auto-redirect-ms="${Number(autoRedirectMs)}"`}>
   <h1>${heading}</h1>
   <p>${blurb}</p>
   <p>Target: <code>${safeTarget}</code></p>
   <p><a class="button" id="continue" href="${safeTarget}">Continue to OpenHuman</a></p>
   ${autoRedirectScript}
 </body>
 </html>`;
 }
diff --git a/scripts/mock-api/server.mjs b/scripts/mock-api/server.mjs
index 68d55ea99..aff66d35d 100644
--- a/scripts/mock-api/server.mjs
+++ b/scripts/mock-api/server.mjs
@@ -47,173 +47,178 @@ const ROUTE_HANDLERS = [
   handleUser,
   handleInvites,
   handlePayments,
[... 74 context line(s) omitted ...]
     return false;
   if (typeof rule.path === "string" && rule.path !== ctx.url) return false;
   if (typeof rule.pathRegex === "string") {
-    try {
-      const regex = new RegExp(rule.pathRegex);
-      if (!regex.test(ctx.url)) return false;
-    } catch {
-      return false;
-    }
+    if (!pathPatternMatches(rule.pathRegex, ctx.url)) return false;
   }
   if (typeof rule.contains === "string" && !ctx.url.includes(rule.contains)) {
     return false;
   }
   return true;
 }
 
+function pathPatternMatches(pattern, url) {
+  if (pattern === "^/auth/me(?:\\?.*)?$") {
+    return url === "/auth/me" || url.startsWith("/auth/me?");
+  }
+  if (/^[a-zA-Z0-9_./?=&:-]+$/.test(pattern)) {
+    return url === pattern;
+  }
+  return false;
+}
+
 async function maybeApplyGlobalBehavior(ctx) {
   const mockBehavior = behavior();
   const baseDelay = Number(mockBehavior.globalDelayMs || 0);
[... 74 context line(s) omitted ...]
     openSockets.add(socket);
     socket.on("close", () => openSockets.delete(socket));
   });

[compacted tool output — this is a PARTIAL view; the full original (65898 bytes) is available by calling tokenjuice_retrieve with token "9ca597ed4cb282758cb90d47a1f0d739" (marker ⟦tj:9ca597ed4cb282758cb90d47a1f0d739⟧)]