@dropout-ai/runtime 0.5.2 → 0.5.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +66 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Invisible Node.js runtime for understanding behavioral impact of AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Stability: High (Browser-Safe, Next.js Safe)
5
5
  */
6
6
 
7
- let https, http, crypto;
7
+ let https, http, crypto, AsyncLocalStorage;
8
8
 
9
9
  // --- DEFAULT CONFIGURATION ---
10
10
  const SUPABASE_FUNCTION_URL =
@@ -34,6 +34,7 @@ class Dropout {
34
34
 
35
35
  // 🛡️ Browser Guard (prevents client execution)
36
36
  if (typeof window !== "undefined" && typeof window.document !== "undefined") {
37
+ if (config.debug) console.warn("[Dropout] ⚠️ Skipped: Browser detected. SDK is Server-Only.");
37
38
  return;
38
39
  }
39
40
 
@@ -45,11 +46,16 @@ class Dropout {
45
46
  return;
46
47
  }
47
48
 
49
+ // 🛡️ Safe Dependency Import
48
50
  try {
49
51
  https = require("https");
50
52
  http = require("http");
51
53
  crypto = require("crypto");
54
+ // Import async_hooks dynamically to avoid browser build crashes
55
+ const asyncHooks = require("async_hooks");
56
+ AsyncLocalStorage = asyncHooks.AsyncLocalStorage;
52
57
  } catch {
58
+ if (config.debug) console.warn("[Dropout] ⚠️ Node core modules missing.");
53
59
  return;
54
60
  }
55
61
 
@@ -64,13 +70,8 @@ class Dropout {
64
70
  // ✅ ASYNC CONTEXT (LAZY, NODE-ONLY)
65
71
  // --------------------------------------------------
66
72
  this.storage = null;
67
-
68
- try {
69
- const asyncHooks = require("async_hooks");
70
- this.storage = new asyncHooks.AsyncLocalStorage();
71
- } catch {
72
- // Edge / Browser / Unsupported — safely disabled
73
- this.storage = null;
73
+ if (AsyncLocalStorage) {
74
+ this.storage = new AsyncLocalStorage();
74
75
  }
75
76
 
76
77
  // --------------------------------------------------
@@ -112,6 +113,7 @@ class Dropout {
112
113
  // CONTEXT API (OPTIONAL BUT SAFE)
113
114
  // --------------------------------------------------
114
115
  run(sessionId, callback) {
116
+ // If storage isn't available, just run the callback
115
117
  if (!this.storage) return callback();
116
118
 
117
119
  const context = {
@@ -127,11 +129,6 @@ class Dropout {
127
129
  return this.storage.getStore() || null;
128
130
  }
129
131
 
130
- getSessionId() {
131
- const ctx = this.getContext();
132
- return ctx?.sessionId || "global_session";
133
- }
134
-
135
132
  // --------------------------------------------------
136
133
  // NORMALIZATION
137
134
  // --------------------------------------------------
@@ -191,10 +188,11 @@ class Dropout {
191
188
  received_at: new Date().toISOString(),
192
189
  };
193
190
 
194
- const req = (this.captureEndpoint.startsWith("http:")
191
+ const requestModule = this.captureEndpoint.startsWith("http:")
195
192
  ? http
196
- : https
197
- ).request(this.captureEndpoint, {
193
+ : https;
194
+
195
+ const req = requestModule.request(this.captureEndpoint, {
198
196
  method: "POST",
199
197
  headers: {
200
198
  "Content-Type": "application/json",
@@ -209,7 +207,7 @@ class Dropout {
209
207
  }
210
208
 
211
209
  // --------------------------------------------------
212
- // NETWORK PATCHING
210
+ // NETWORK PATCHING (INTELLIGENT)
213
211
  // --------------------------------------------------
214
212
  patchNetwork() {
215
213
  const _this = this;
@@ -227,12 +225,14 @@ class Dropout {
227
225
  }
228
226
 
229
227
  let bodyStr = "";
228
+ let parsedBody = null;
230
229
  if (init?.body) {
231
230
  try {
232
231
  bodyStr =
233
232
  typeof init.body === "string"
234
233
  ? init.body
235
234
  : JSON.stringify(init.body);
235
+ parsedBody = JSON.parse(bodyStr);
236
236
  } catch { }
237
237
  }
238
238
 
@@ -240,13 +240,58 @@ class Dropout {
240
240
  return originalFetch.apply(this, arguments);
241
241
  }
242
242
 
243
- const sessionId = _this.getSessionId();
243
+ // 🕵️ 1. SESSION LOGIC (PRIORITY ORDER)
244
+ // A. Explicit Context (if wrapped in .run())
245
+ const ctx = _this.getContext();
246
+ let activeSessionId = ctx?.sessionId;
247
+
248
+ if (!activeSessionId && parsedBody) {
249
+ // B. Explicit Override in Payload
250
+ const explicitId =
251
+ parsedBody.session_id ||
252
+ parsedBody.metadata?.session_id ||
253
+ parsedBody.metadata?.sessionId;
254
+
255
+ if (explicitId) {
256
+ activeSessionId = explicitId;
257
+ } else {
258
+ // C. SMART FINGERPRINTING (The Fix)
259
+ const userId =
260
+ parsedBody.user ||
261
+ parsedBody.userId ||
262
+ parsedBody.metadata?.user_id ||
263
+ "anon_user";
264
+
265
+ let rootContext = "";
266
+ // Grab first 64 chars of the FIRST message as "Conversation Anchor"
267
+ if (parsedBody.messages?.length > 0) {
268
+ rootContext = parsedBody.messages[0].content?.slice(0, 64) || "";
269
+ } else if (parsedBody.prompt) {
270
+ rootContext =
271
+ typeof parsedBody.prompt === "string"
272
+ ? parsedBody.prompt.slice(0, 64)
273
+ : JSON.stringify(parsedBody.prompt).slice(0, 64);
274
+ }
275
+
276
+ if (rootContext) {
277
+ // Salt with Date (YYYY-MM-DD) to differentiate sessions across days
278
+ const dateSalt = new Date().toISOString().slice(0, 10);
279
+ activeSessionId =
280
+ "sess_" +
281
+ _this.hash(`${userId}::${rootContext}::${dateSalt}`).substring(0, 12);
282
+ }
283
+ }
284
+ }
285
+
286
+ // D. Fallback
287
+ activeSessionId = activeSessionId || _this.generateSessionId();
288
+
244
289
  const { provider, model } = _this.normalize(url, bodyStr);
245
290
  const pHash = _this.hash(bodyStr);
246
291
  const start = Date.now();
247
292
 
248
293
  _this.emit({
249
- session_id: sessionId,
294
+ session_id: activeSessionId,
250
295
  turn_index: 0,
251
296
  turn_role: "user",
252
297
  provider,
@@ -266,7 +311,7 @@ class Dropout {
266
311
  }
267
312
 
268
313
  _this.emit({
269
- session_id: sessionId,
314
+ session_id: activeSessionId,
270
315
  turn_index: 1,
271
316
  turn_role: "assistant",
272
317
  latency_ms: Date.now() - start,
@@ -296,4 +341,4 @@ if (
296
341
  }
297
342
 
298
343
  Dropout.default = Dropout;
299
- module.exports = Dropout;
344
+ module.exports = Dropout;