@twin3-ai/agent-id 0.3.50 → 0.3.52
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/browser-sdk.js +1 -1
- package/package.json +1 -1
- package/site-agent.js +49 -10
- package/telemetry-collector.js +89 -8
package/browser-sdk.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// browser script and shipped in the package byte-for-byte, so it can
|
|
17
17
|
// neither read the package at runtime nor go through a build step:
|
|
18
18
|
// the version is written once here and CI refuses a mismatch.
|
|
19
|
-
var SDK_VERSION = "0.3.
|
|
19
|
+
var SDK_VERSION = "0.3.52";
|
|
20
20
|
var w = window;
|
|
21
21
|
var d = document;
|
|
22
22
|
var s = d.currentScript || {};
|
package/package.json
CHANGED
package/site-agent.js
CHANGED
|
@@ -249,27 +249,45 @@ function createSiteAgentClient(options = {}) {
|
|
|
249
249
|
const maxAttempts = Math.max(1, Math.min(5, Number(options.maxAttempts || 3)));
|
|
250
250
|
const retryBaseMs = Math.max(0, Number(options.retryBaseMs == null ? 250 : options.retryBaseMs));
|
|
251
251
|
const sleep = options.sleep || defaultSleep;
|
|
252
|
+
const timeoutMs = Math.max(1, Number(options.timeoutMs == null ? 60000 : options.timeoutMs));
|
|
253
|
+
const callerSignal = options.signal;
|
|
252
254
|
if (typeof fetcher !== "function") {
|
|
253
255
|
throw siteAgentError("FETCH_UNAVAILABLE", "A fetch implementation is required to call the Site Agent API.");
|
|
254
256
|
}
|
|
255
257
|
|
|
256
|
-
|
|
258
|
+
function isRedirect(res) {
|
|
259
|
+
return Boolean(res) && (res.type === "opaqueredirect" || (res.status >= 300 && res.status < 400));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function fetchWithRetry(url, request, signal) {
|
|
257
263
|
let lastError;
|
|
258
264
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
259
265
|
try {
|
|
260
|
-
const res = await fetcher(url, request);
|
|
266
|
+
const res = await fetcher(url, { ...request, redirect: "manual", signal });
|
|
267
|
+
if (isRedirect(res)) {
|
|
268
|
+
throw siteAgentError(
|
|
269
|
+
"SITE_AGENT_REDIRECT_REFUSED",
|
|
270
|
+
`xAgent ID answered with a redirect (HTTP ${res.status}); credentials are never re-sent to another location.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
261
273
|
const retryableStateConflict = res && res.status === 409 && res.headers
|
|
262
274
|
&& typeof res.headers.get === "function"
|
|
263
275
|
&& res.headers.get("x-agent-id-retryable") === "state-conflict";
|
|
264
276
|
const retryable = res && (res.status === 429 || res.status >= 500 || retryableStateConflict);
|
|
265
277
|
if (!retryable || attempt >= maxAttempts) return res;
|
|
266
278
|
if (typeof res.text === "function") await res.text();
|
|
267
|
-
await sleep(retryDelayMs(res, attempt, retryBaseMs));
|
|
279
|
+
await sleep(retryDelayMs(res, attempt, retryBaseMs), signal);
|
|
268
280
|
} catch (err) {
|
|
281
|
+
if (err && err.code === "SITE_AGENT_REDIRECT_REFUSED") throw err;
|
|
282
|
+
if (signal.aborted) break;
|
|
269
283
|
lastError = err;
|
|
270
284
|
if (attempt >= maxAttempts) break;
|
|
271
|
-
await sleep(Math.min(30000, retryBaseMs * (2 ** Math.max(0, attempt - 1))));
|
|
285
|
+
await sleep(Math.min(30000, retryBaseMs * (2 ** Math.max(0, attempt - 1))), signal);
|
|
272
286
|
}
|
|
287
|
+
if (signal.aborted) break;
|
|
288
|
+
}
|
|
289
|
+
if (signal.aborted) {
|
|
290
|
+
throw siteAgentError("SITE_AGENT_TIMEOUT", `Site Agent request did not finish within ${timeoutMs} ms`);
|
|
273
291
|
}
|
|
274
292
|
throw siteAgentError(
|
|
275
293
|
"SITE_AGENT_NETWORK_FAILED",
|
|
@@ -277,6 +295,30 @@ function createSiteAgentClient(options = {}) {
|
|
|
277
295
|
);
|
|
278
296
|
}
|
|
279
297
|
|
|
298
|
+
async function requestJson(url, request) {
|
|
299
|
+
const controller = new AbortController();
|
|
300
|
+
const abort = () => controller.abort();
|
|
301
|
+
const timer = setTimeout(abort, timeoutMs);
|
|
302
|
+
if (callerSignal) {
|
|
303
|
+
if (callerSignal.aborted) abort();
|
|
304
|
+
else callerSignal.addEventListener("abort", abort, { once: true });
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const res = await fetchWithRetry(url, request, controller.signal);
|
|
308
|
+
try {
|
|
309
|
+
return { res, data: await readJsonResponse(res) };
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (controller.signal.aborted) {
|
|
312
|
+
throw siteAgentError("SITE_AGENT_TIMEOUT", `Site Agent request did not finish within ${timeoutMs} ms`);
|
|
313
|
+
}
|
|
314
|
+
throw err;
|
|
315
|
+
}
|
|
316
|
+
} finally {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
if (callerSignal) callerSignal.removeEventListener("abort", abort);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
280
322
|
async function call(action, payload = {}) {
|
|
281
323
|
const key = payload.agent_key || payload.agentKey || agentKey;
|
|
282
324
|
if (!key) {
|
|
@@ -291,14 +333,13 @@ function createSiteAgentClient(options = {}) {
|
|
|
291
333
|
action
|
|
292
334
|
};
|
|
293
335
|
delete body.agentKey;
|
|
294
|
-
const res = await
|
|
336
|
+
const { res, data } = await requestJson(endpoint + SITE_AGENT_KEY_POLICY.privateEndpoint, {
|
|
295
337
|
method: "POST",
|
|
296
338
|
// Authorization is the canonical credential channel; the body field
|
|
297
339
|
// stays for compatibility with servers that predate header auth.
|
|
298
340
|
headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
|
|
299
341
|
body: JSON.stringify(body)
|
|
300
342
|
});
|
|
301
|
-
const data = await readJsonResponse(res);
|
|
302
343
|
if (!res.ok || data.ok === false) {
|
|
303
344
|
const error = siteAgentError(
|
|
304
345
|
data.code || "SITE_AGENT_CALL_FAILED",
|
|
@@ -313,12 +354,11 @@ function createSiteAgentClient(options = {}) {
|
|
|
313
354
|
}
|
|
314
355
|
|
|
315
356
|
async function publicPost(path, payload = {}) {
|
|
316
|
-
const res = await
|
|
357
|
+
const { res, data } = await requestJson(endpoint + path, {
|
|
317
358
|
method: "POST",
|
|
318
359
|
headers: { "content-type": "application/json" },
|
|
319
360
|
body: JSON.stringify(payload || {})
|
|
320
361
|
});
|
|
321
|
-
const data = await readJsonResponse(res);
|
|
322
362
|
if (!res.ok || data.ok === false) {
|
|
323
363
|
const error = siteAgentError(
|
|
324
364
|
data.code || "AGENT_ID_PUBLIC_CALL_FAILED",
|
|
@@ -353,8 +393,7 @@ function createSiteAgentClient(options = {}) {
|
|
|
353
393
|
},
|
|
354
394
|
...(method === "POST" ? { body: JSON.stringify(body) } : {})
|
|
355
395
|
};
|
|
356
|
-
const res = await
|
|
357
|
-
const data = await readJsonResponse(res);
|
|
396
|
+
const { res, data } = await requestJson(endpoint + "/api/site_agents/permissions" + suffix, request);
|
|
358
397
|
if (!res.ok || data.ok === false) {
|
|
359
398
|
const error = siteAgentError(
|
|
360
399
|
data.error || "SITE_AGENT_PERMISSION_FAILED",
|
package/telemetry-collector.js
CHANGED
|
@@ -28,6 +28,39 @@ function bytes(value) { return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
|
28
28
|
function emptyState() {
|
|
29
29
|
return { schema: "agentx-telemetry-buffer-v1", events: [], dead_letters: [], retry_at: 0, attempts: 0, last_flush_at: 0, last_error: null };
|
|
30
30
|
}
|
|
31
|
+
function journalPathFor(statePath) { return `${path.resolve(statePath)}.journal`; }
|
|
32
|
+
function applyJournal(state, journalPath, maxFileBytes, maxDeadLetters) {
|
|
33
|
+
let text;
|
|
34
|
+
try {
|
|
35
|
+
if (fs.statSync(journalPath).size > maxFileBytes) throw new Error("collector_state_too_large");
|
|
36
|
+
text = fs.readFileSync(journalPath, "utf8");
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code === "ENOENT") return 0;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
const known = new Set(state.events.map((event) => event.event_id));
|
|
42
|
+
let applied = 0;
|
|
43
|
+
for (const line of text.split("\n")) {
|
|
44
|
+
if (!line) continue;
|
|
45
|
+
let entry;
|
|
46
|
+
try { entry = JSON.parse(line); } catch (_error) { break; }
|
|
47
|
+
if (entry.add && !known.has(entry.add.event_id)) {
|
|
48
|
+
state.events.push(entry.add);
|
|
49
|
+
known.add(entry.add.event_id);
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(entry.drop) && entry.drop.length) {
|
|
52
|
+
const dropped = new Set(entry.drop);
|
|
53
|
+
state.events = state.events.filter((event) => !dropped.has(event.event_id));
|
|
54
|
+
for (const id of dropped) known.delete(id);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(entry.dead) && entry.dead.length) {
|
|
57
|
+
state.dead_letters.push(...entry.dead);
|
|
58
|
+
state.dead_letters = state.dead_letters.slice(-maxDeadLetters);
|
|
59
|
+
}
|
|
60
|
+
applied += 1;
|
|
61
|
+
}
|
|
62
|
+
return applied;
|
|
63
|
+
}
|
|
31
64
|
function loadState(statePath, maxFileBytes) {
|
|
32
65
|
try {
|
|
33
66
|
if (fs.statSync(statePath).size > maxFileBytes) throw new Error("collector_state_too_large");
|
|
@@ -47,6 +80,7 @@ function writeState(statePath, state) {
|
|
|
47
80
|
fs.renameSync(temporary, target);
|
|
48
81
|
fs.chmodSync(target, 0o600);
|
|
49
82
|
} finally { try { fs.rmSync(temporary, { force: true }); } catch (_error) {} }
|
|
83
|
+
fs.rmSync(journalPathFor(statePath), { force: true });
|
|
50
84
|
}
|
|
51
85
|
function createTelemetryCollector({
|
|
52
86
|
client,
|
|
@@ -68,25 +102,70 @@ function createTelemetryCollector({
|
|
|
68
102
|
maxBatchBytes = Math.max(512, Number(maxBatchBytes) || 256 * 1024);
|
|
69
103
|
maxBatchEvents = Math.max(1, Math.min(250, Number(maxBatchEvents) || 250));
|
|
70
104
|
maxDeadLetters = Math.max(1, Math.min(100, Number(maxDeadLetters) || 100));
|
|
105
|
+
const journalPath = journalPathFor(statePath);
|
|
71
106
|
const state = loadState(statePath, maxBytes + 1024 * 1024);
|
|
72
|
-
|
|
73
|
-
|
|
107
|
+
const replayed = applyJournal(state, journalPath, 2 * maxBytes + 1024 * 1024, maxDeadLetters);
|
|
108
|
+
const eventSizes = new WeakMap();
|
|
109
|
+
let journalBytes = 0;
|
|
110
|
+
let snapshotBytes = 0;
|
|
111
|
+
function eventBytes(event) {
|
|
112
|
+
let size = eventSizes.get(event);
|
|
113
|
+
if (size === undefined) { size = bytes(event); eventSizes.set(event, size); }
|
|
114
|
+
return size;
|
|
115
|
+
}
|
|
116
|
+
function bufferBytes(events = state.events) {
|
|
117
|
+
let total = 2 + Math.max(0, events.length - 1);
|
|
118
|
+
for (const event of events) total += eventBytes(event);
|
|
119
|
+
return total;
|
|
120
|
+
}
|
|
121
|
+
function knownIds() {
|
|
122
|
+
const ids = new Set(state.events.map((event) => event.event_id));
|
|
123
|
+
for (const item of state.dead_letters) ids.add(item.event_id);
|
|
124
|
+
return ids;
|
|
125
|
+
}
|
|
126
|
+
let ids = knownIds();
|
|
127
|
+
function persist() {
|
|
128
|
+
writeState(statePath, state);
|
|
129
|
+
snapshotBytes = bufferBytes();
|
|
130
|
+
journalBytes = 0;
|
|
131
|
+
ids = knownIds();
|
|
132
|
+
}
|
|
133
|
+
function journal(entry) {
|
|
134
|
+
const line = JSON.stringify(entry) + "\n";
|
|
135
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true, mode: 0o700 });
|
|
136
|
+
fs.appendFileSync(journalPath, line, { mode: 0o600 });
|
|
137
|
+
journalBytes += Buffer.byteLength(line, "utf8");
|
|
138
|
+
if (journalBytes > Math.max(1024 * 1024, snapshotBytes)) persist();
|
|
139
|
+
}
|
|
140
|
+
function evict(timestamp) {
|
|
74
141
|
const cutoff = timestamp - maxAgeMs;
|
|
75
142
|
const removed = [];
|
|
76
143
|
state.events = state.events.filter((event) => {
|
|
77
144
|
if (Number(event.ingestion_time) < cutoff) { removed.push({ ...event, dead_letter_reason: "expired_buffer_event" }); return false; }
|
|
78
145
|
return true;
|
|
79
146
|
});
|
|
80
|
-
|
|
81
|
-
state.
|
|
147
|
+
let total = bufferBytes();
|
|
148
|
+
while (state.events.length > maxEvents || total > maxBytes) {
|
|
149
|
+
const event = state.events.shift();
|
|
150
|
+
total -= eventBytes(event) + (state.events.length ? 1 : 0);
|
|
151
|
+
removed.push({ ...event, dead_letter_reason: "buffer_limit_pruned" });
|
|
152
|
+
}
|
|
153
|
+
const dead = removed.map((event) => ({ event_id: event.event_id, source: event.source, dead_letter_reason: event.dead_letter_reason, failed_at: timestamp }));
|
|
154
|
+
state.dead_letters.push(...dead);
|
|
82
155
|
state.dead_letters = state.dead_letters.slice(-maxDeadLetters);
|
|
83
|
-
|
|
156
|
+
for (const item of dead) ids.add(item.event_id);
|
|
157
|
+
return dead;
|
|
158
|
+
}
|
|
159
|
+
function prune(timestamp = now()) {
|
|
160
|
+
const removed = evict(timestamp);
|
|
161
|
+
if (removed.length) persist();
|
|
84
162
|
return { removed: removed.length, event_count: state.events.length };
|
|
85
163
|
}
|
|
164
|
+
if (replayed) persist();
|
|
86
165
|
async function record(event) {
|
|
87
166
|
const timestamp = now();
|
|
88
167
|
const eventId = String(event && (event.event_id || event.id) || crypto.randomUUID());
|
|
89
|
-
if (
|
|
168
|
+
if (ids.has(eventId)) return { recorded: false, duplicate: true, event_id: eventId };
|
|
90
169
|
const cleaned = redact(event || {});
|
|
91
170
|
const normalized = {
|
|
92
171
|
...cleaned,
|
|
@@ -100,7 +179,9 @@ function createTelemetryCollector({
|
|
|
100
179
|
};
|
|
101
180
|
if (normalized.user_agent && !normalized.verified_bot_identity) normalized.identity_confidence = "user_agent_only_spoofable";
|
|
102
181
|
state.events.push(normalized);
|
|
103
|
-
|
|
182
|
+
ids.add(eventId);
|
|
183
|
+
const dead = evict(timestamp);
|
|
184
|
+
journal({ add: normalized, drop: dead.map((item) => item.event_id), dead });
|
|
104
185
|
return { recorded: true, event_id: eventId };
|
|
105
186
|
}
|
|
106
187
|
function nextBatch(timestamp) {
|
|
@@ -155,7 +236,7 @@ function createTelemetryCollector({
|
|
|
155
236
|
}
|
|
156
237
|
async function status() {
|
|
157
238
|
prune(now());
|
|
158
|
-
return { schema: "agentx-telemetry-status-v1", event_count: state.events.length, dead_letter_count: state.dead_letters.length, bytes:
|
|
239
|
+
return { schema: "agentx-telemetry-status-v1", event_count: state.events.length, dead_letter_count: state.dead_letters.length, bytes: bufferBytes(), retry_at: state.retry_at, last_flush_at: state.last_flush_at, last_error: state.last_error };
|
|
159
240
|
}
|
|
160
241
|
return Object.freeze({ record, flush, status, prune });
|
|
161
242
|
}
|