@dropout-ai/runtime 0.2.5 → 0.2.7

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 +182 -134
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.2.05",
3
+ "version": "0.2.07",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -3,12 +3,12 @@
3
3
  * Role: Passive observer inside the user’s app.
4
4
  *
5
5
  * Runtime never reasons, never interprets, never stores intelligence.
6
- * It only: Observes, Normalizes, Emits signals.
6
+ * it only: Observes, Normalizes, Emits signals.
7
7
  */
8
8
 
9
9
  const crypto = require('crypto');
10
10
 
11
- // --- A. Session Boundary (CRITICAL) ---
11
+ // --- Identity & State ---
12
12
  const GLOBAL_OBJ = typeof window !== 'undefined' ? window : global;
13
13
 
14
14
  function generateSessionId() {
@@ -19,15 +19,16 @@ function generateSessionId() {
19
19
  }
20
20
  }
21
21
 
22
- // One session = one continuous user attempt
23
- // Resets on page reload (Browser) or process restart (Node)
24
22
  if (!GLOBAL_OBJ.__dropout_session_id__) {
25
23
  GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
26
24
  }
27
25
 
28
26
  let turnIndex = 0;
27
+ let lastTurnConfirmed = true;
28
+ let lastPromptHash = null;
29
+ let lastResponseHash = null;
29
30
 
30
- // --- D. Runtime guarantees ---
31
+ // --- Runtime Guarantees ---
31
32
  let config = {
32
33
  maxOutputBytes: 32768,
33
34
  captureEndpoint: 'http://localhost:4000/capture',
@@ -35,7 +36,7 @@ let config = {
35
36
  privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'safe'
36
37
  };
37
38
 
38
- // Zero user config required: Non-blocking remote config fetch
39
+ // Remote config fetch (Non-blocking)
39
40
  setTimeout(async () => {
40
41
  const fetchFn = GLOBAL_OBJ.__dropout_original_fetch__ || GLOBAL_OBJ.fetch;
41
42
  if (typeof fetchFn !== 'function') return;
@@ -65,9 +66,42 @@ function emit(payload) {
65
66
  }, 0);
66
67
  }
67
68
 
68
- // --- C. Privacy Mode Helpers ---
69
+ // --- Lifecycle Signals ---
69
70
 
70
- function getSemanticHash(text) {
71
+ function emitSessionEnd(reason) {
72
+ if (GLOBAL_OBJ.__dropout_session_ended__) return;
73
+ GLOBAL_OBJ.__dropout_session_ended__ = true;
74
+
75
+ emit({
76
+ identity: {
77
+ session_id: GLOBAL_OBJ.__dropout_session_id__,
78
+ turn_index: turnIndex,
79
+ direction: 'meta',
80
+ turn_role: 'system'
81
+ },
82
+ timing: { created_at: Date.now() },
83
+ metadata_flags: {
84
+ session_end: true,
85
+ end_reason: reason
86
+ }
87
+ });
88
+ }
89
+
90
+ // Browser: Navigation/Reload
91
+ if (typeof window !== 'undefined' && window.addEventListener) {
92
+ window.addEventListener('beforeunload', () => emitSessionEnd('navigation'));
93
+ }
94
+
95
+ // Node.js: Process Exit
96
+ if (typeof process !== 'undefined' && process.on) {
97
+ process.on('exit', () => emitSessionEnd('process_exit'));
98
+ process.on('SIGINT', () => { emitSessionEnd('sigint'); process.exit(); });
99
+ process.on('SIGTERM', () => { emitSessionEnd('sigterm'); process.exit(); });
100
+ }
101
+
102
+ // --- Content Utilities ---
103
+
104
+ function hash(text) {
71
105
  if (!text) return null;
72
106
  try {
73
107
  return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
@@ -76,34 +110,7 @@ function getSemanticHash(text) {
76
110
  }
77
111
  }
78
112
 
79
- function getMarkers(text) {
80
- if (!text) return null;
81
- return {
82
- chars: text.length,
83
- words: text.split(/\s+/).length,
84
- lines: text.split('\n').length,
85
- has_code: /```/.test(text),
86
- has_list: /^\s*[-*•\d+.]/m.test(text)
87
- };
88
- }
89
-
90
- const PATTERNS = {
91
- negative: ["wrong", "bad", "not helpful", "incorrect", "stupid", "error", "worst"],
92
- positive: ["thanks", "good", "perfect", "correct", "great", "helpful"],
93
- struggle: ["but", "why", "stop", "don't", "no", "again", "explain"]
94
- };
95
-
96
- function getFlags(text) {
97
- if (!text) return [];
98
- const t = text.toLowerCase();
99
- const flags = [];
100
- if (PATTERNS.negative.some(p => t.includes(p))) flags.push('neg');
101
- if (PATTERNS.positive.some(p => t.includes(p))) flags.push('pos');
102
- if (PATTERNS.struggle.some(p => t.includes(p))) flags.push('clash');
103
- return flags;
104
- }
105
-
106
- // --- B. Event Capture (Provider-agnostic) ---
113
+ // --- Provider Normalization ---
107
114
 
108
115
  function normalize(url, body) {
109
116
  let provider = 'unknown';
@@ -131,16 +138,13 @@ function normalize(url, body) {
131
138
  return { provider, model };
132
139
  }
133
140
 
134
- /**
135
- * The Monkey Patch
136
- */
141
+ // --- The Monkey Patch ---
142
+
137
143
  if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patched__) {
138
144
  GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
139
145
 
140
146
  GLOBAL_OBJ.fetch = async function (input, init) {
141
- const start = Date.now();
142
147
  const url = typeof input === 'string' ? input : (input && input.url);
143
-
144
148
  const isAI = url && (
145
149
  url.includes('openai.com') ||
146
150
  url.includes('anthropic.com') ||
@@ -149,50 +153,107 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
149
153
  (init && init.body && (init.body.includes('"model"') || init.body.includes('"messages"')))
150
154
  );
151
155
 
152
- const response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
156
+ if (!isAI) return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
153
157
 
154
- if (isAI) {
155
- const latency = Date.now() - start;
156
- const turn = turnIndex++;
157
- const { provider, model } = normalize(url, init && init.body);
158
+ const start = Date.now();
158
159
 
159
- let pText = "";
160
- if (init && init.body) {
161
- try { pText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
162
- }
160
+ // --- Explicit Turn Increment Rule ---
161
+ let activeTurn;
162
+ if (lastTurnConfirmed) {
163
+ activeTurn = turnIndex++;
164
+ lastTurnConfirmed = false;
165
+ } else {
166
+ activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
167
+ }
163
168
 
164
- let oText = "";
165
- try {
166
- const cloned = response.clone();
167
- oText = await cloned.text();
168
- if (oText && oText.length > config.maxOutputBytes) {
169
- oText = oText.slice(0, config.maxOutputBytes);
170
- }
171
- } catch (e) { }
169
+ const { provider, model } = normalize(url, init && init.body);
172
170
 
173
- const payload = {
171
+ // --- 1. Emit Request Event ---
172
+ let pText = "";
173
+ if (init && init.body) {
174
+ try { pText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
175
+ }
176
+ const pHash = hash(pText);
177
+ const isRetry = pHash && pHash === lastPromptHash;
178
+
179
+ const requestEvent = {
180
+ identity: {
174
181
  session_id: GLOBAL_OBJ.__dropout_session_id__,
175
- timestamp: Date.now(),
176
- latency,
177
- turn_position: turn,
182
+ turn_index: activeTurn,
183
+ direction: 'request',
184
+ turn_role: 'user'
185
+ },
186
+ timing: {
187
+ created_at: Date.now()
188
+ },
189
+ provider_context: {
178
190
  provider,
179
- model,
180
- mode: config.privacyMode
181
- };
182
-
183
- if (config.privacyMode === 'full') {
184
- payload.prompt = pText;
185
- payload.output = oText;
186
- } else {
187
- payload.prompt_hash = getSemanticHash(pText);
188
- payload.output_hash = getSemanticHash(oText);
189
- payload.markers = { p: getMarkers(pText), o: getMarkers(oText) };
190
- payload.flags = getFlags(pText + " " + oText);
191
+ model
192
+ },
193
+ content: {
194
+ content_raw: config.privacyMode === 'full' ? pText : null,
195
+ content_hash: pHash
196
+ },
197
+ metadata_flags: {
198
+ retry_like: isRetry ? 1 : 0
191
199
  }
200
+ };
201
+
202
+ emit(requestEvent);
203
+ lastPromptHash = pHash;
192
204
 
193
- emit(payload);
205
+ // Execute actual fetch
206
+ let response;
207
+ let oText = "";
208
+ try {
209
+ response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
210
+ } catch (err) {
211
+ throw err;
194
212
  }
195
213
 
214
+ const latency = Date.now() - start;
215
+
216
+ // --- 2. Emit Response Event ---
217
+ try {
218
+ const cloned = response.clone();
219
+ oText = await cloned.text();
220
+ if (oText && oText.length > config.maxOutputBytes) {
221
+ oText = oText.slice(0, config.maxOutputBytes);
222
+ }
223
+ } catch (e) { }
224
+
225
+ const oHash = hash(oText);
226
+ const isNonAdaptive = oHash && oHash === lastResponseHash;
227
+
228
+ const responseEvent = {
229
+ identity: {
230
+ session_id: GLOBAL_OBJ.__dropout_session_id__,
231
+ turn_index: activeTurn,
232
+ direction: 'response',
233
+ turn_role: 'assistant'
234
+ },
235
+ timing: {
236
+ created_at: Date.now(),
237
+ latency_ms: latency
238
+ },
239
+ provider_context: {
240
+ provider,
241
+ model
242
+ },
243
+ content: {
244
+ content_raw: config.privacyMode === 'full' ? oText : null,
245
+ content_hash: oHash
246
+ },
247
+ metadata_flags: {
248
+ non_adaptive_response: isNonAdaptive ? 1 : 0,
249
+ turn_boundary_confirmed: 1
250
+ }
251
+ };
252
+
253
+ emit(responseEvent);
254
+ lastResponseHash = oHash;
255
+ lastTurnConfirmed = true; // Confirmation Rule locked
256
+
196
257
  return response;
197
258
  };
198
259
 
@@ -205,78 +266,65 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
205
266
  async function capture(target, options = {}) {
206
267
  const start = Date.now();
207
268
 
208
- // Resolve target (function, promise, or static object)
209
- let result;
210
- if (typeof target === 'function') {
211
- result = await target();
212
- } else if (target && typeof target.then === 'function') {
213
- result = await target;
269
+ let activeTurn;
270
+ if (lastTurnConfirmed) {
271
+ activeTurn = turnIndex++;
272
+ lastTurnConfirmed = false;
214
273
  } else {
215
- // case: capture({ prompt, output })
216
- const { prompt, output } = target;
217
- const latency = options.latency || 0;
218
- const turn = turnIndex++;
219
- const mode = options.privacy || config.privacyMode;
220
-
221
- const payload = {
222
- session_id: GLOBAL_OBJ.__dropout_session_id__,
223
- timestamp: Date.now(),
224
- latency,
225
- turn_position: turn,
226
- provider: options.provider || 'manual',
227
- model: options.model || 'unknown',
228
- mode
229
- };
230
-
231
- if (mode === 'full') {
232
- payload.prompt = prompt;
233
- payload.output = output;
234
- } else {
235
- payload.prompt_hash = getSemanticHash(prompt);
236
- payload.output_hash = getSemanticHash(output);
237
- payload.markers = { p: getMarkers(prompt), o: getMarkers(output) };
238
- payload.flags = getFlags((prompt || "") + " " + (output || ""));
239
- }
240
-
241
- emit(payload);
242
- return output;
274
+ activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
243
275
  }
244
276
 
245
- // Wrapped execution capture
246
- const latency = Date.now() - start;
247
- const turn = turnIndex++;
248
277
  const mode = options.privacy || config.privacyMode;
249
- const prompt = options.prompt;
250
- const output = typeof result === 'string' ? result : JSON.stringify(result);
251
-
252
- const payload = {
253
- session_id: GLOBAL_OBJ.__dropout_session_id__,
254
- timestamp: Date.now(),
255
- latency,
256
- turn_position: turn,
257
- provider: options.provider || 'manual',
258
- model: options.model || 'unknown',
259
- mode
260
- };
261
278
 
262
- if (mode === 'full') {
263
- payload.prompt = prompt;
264
- payload.output = output;
279
+ let prompt, output, latency_ms = options.latency || 0;
280
+
281
+ if (typeof target === 'function' || (target && typeof target.then === 'function')) {
282
+ prompt = options.prompt;
283
+ const result = typeof target === 'function' ? await target() : await target;
284
+ output = typeof result === 'string' ? result : JSON.stringify(result);
285
+ latency_ms = Date.now() - start;
265
286
  } else {
266
- payload.prompt_hash = getSemanticHash(prompt);
267
- payload.output_hash = getSemanticHash(output);
268
- payload.markers = { p: getMarkers(prompt), o: getMarkers(output) };
269
- payload.flags = getFlags((prompt || "") + " " + (output || ""));
287
+ prompt = target.prompt;
288
+ output = target.output;
270
289
  }
271
290
 
272
- emit(payload);
273
- return result;
291
+ const pHash = hash(prompt);
292
+ const oHash = hash(output);
293
+
294
+ // Emit Request
295
+ emit({
296
+ identity: { session_id: GLOBAL_OBJ.__dropout_session_id__, turn_index: activeTurn, direction: 'request', turn_role: 'user' },
297
+ timing: { created_at: Date.now() },
298
+ provider_context: { provider: options.provider || 'manual', model: options.model || 'unknown' },
299
+ content: { content_raw: mode === 'full' ? prompt : null, content_hash: pHash },
300
+ metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
301
+ });
302
+
303
+ // Emit Response
304
+ emit({
305
+ identity: { session_id: GLOBAL_OBJ.__dropout_session_id__, turn_index: activeTurn, direction: 'response', turn_role: 'assistant' },
306
+ timing: { created_at: Date.now(), latency_ms },
307
+ provider_context: { provider: options.provider || 'manual', model: options.model || 'unknown' },
308
+ content: { content_raw: mode === 'full' ? output : null, content_hash: oHash },
309
+ metadata_flags: { non_adaptive_response: oHash === lastResponseHash ? 1 : 0, turn_boundary_confirmed: 1 }
310
+ });
311
+
312
+ lastPromptHash = pHash;
313
+ lastResponseHash = oHash;
314
+ lastTurnConfirmed = true;
315
+
316
+ return output;
274
317
  }
275
318
 
276
319
  module.exports = {
277
320
  capture,
278
- reset: () => {
321
+ reset: (reason = 'manual_reset') => {
322
+ emitSessionEnd(reason);
279
323
  GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
324
+ GLOBAL_OBJ.__dropout_session_ended__ = false;
280
325
  turnIndex = 0;
326
+ lastTurnConfirmed = true;
327
+ lastPromptHash = null;
328
+ lastResponseHash = null;
281
329
  }
282
330
  };