jev-devtools 0.1.3 → 0.2.0
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/README.md +5 -3
- package/THIRD_PARTY_NOTICES.txt +27 -0
- package/lib/server/index.js +78 -8
- package/lib/server/recorder.d.ts +8 -1
- package/lib/server/recorder.js +35 -8
- package/lib/src/ChoiceProbabilities.d.ts +4 -0
- package/lib/src/ChoiceProbabilities.js +19 -0
- package/lib/src/ConnectedInspector.js +37 -29
- package/lib/src/JevDevtools.js +37 -47
- package/lib/src/JevDrawer.d.ts +3 -2
- package/lib/src/JevDrawer.js +12 -3
- package/lib/src/Questions.d.ts +5 -0
- package/lib/src/Questions.js +174 -0
- package/lib/src/TraceContent.d.ts +11 -0
- package/lib/src/TraceContent.js +32 -0
- package/lib/src/TraceDiagram.js +86 -18
- package/lib/src/flow-model.js +1 -1
- package/lib/src/index.d.ts +1 -1
- package/lib/src/inspector-styles.js +1 -1
- package/lib/src/inspector.css +224 -0
- package/lib/src/protocol.d.ts +13 -0
- package/lib/src/trace-store.d.ts +6 -0
- package/lib/src/trace-store.js +66 -0
- package/lib/src/trace-stream.d.ts +2 -0
- package/lib/src/trace-stream.js +65 -0
- package/lib/src/useDiagramViewport.d.ts +1 -0
- package/lib/src/useDiagramViewport.js +15 -4
- package/package.json +2 -1
- package/skills/setup-jev-devtools/references/verify.md +2 -0
package/lib/src/inspector.css
CHANGED
|
@@ -1040,3 +1040,227 @@
|
|
|
1040
1040
|
width: auto;
|
|
1041
1041
|
max-width: none;
|
|
1042
1042
|
}
|
|
1043
|
+
|
|
1044
|
+
.jev-devtools .detail-body-questions {
|
|
1045
|
+
display: flex;
|
|
1046
|
+
flex-direction: column;
|
|
1047
|
+
overflow: auto;
|
|
1048
|
+
}
|
|
1049
|
+
.jev-devtools .questions-list {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
flex-direction: column;
|
|
1052
|
+
flex: 1;
|
|
1053
|
+
min-height: 180px;
|
|
1054
|
+
}
|
|
1055
|
+
.jev-devtools .questions-navigation {
|
|
1056
|
+
display: flex;
|
|
1057
|
+
align-items: end;
|
|
1058
|
+
gap: 8px;
|
|
1059
|
+
flex-wrap: wrap;
|
|
1060
|
+
flex-shrink: 0;
|
|
1061
|
+
}
|
|
1062
|
+
.jev-devtools .questions-navigation label {
|
|
1063
|
+
display: flex;
|
|
1064
|
+
align-items: center;
|
|
1065
|
+
gap: 8px;
|
|
1066
|
+
white-space: nowrap;
|
|
1067
|
+
flex: 1;
|
|
1068
|
+
min-width: 240px;
|
|
1069
|
+
max-width: 480px;
|
|
1070
|
+
font-size: 11px;
|
|
1071
|
+
color: #bc9db5;
|
|
1072
|
+
}
|
|
1073
|
+
.jev-devtools .questions-navigation select {
|
|
1074
|
+
display: block;
|
|
1075
|
+
min-width: 0;
|
|
1076
|
+
}
|
|
1077
|
+
.jev-devtools .questions-navigation > span {
|
|
1078
|
+
align-self: center;
|
|
1079
|
+
color: #a89bac;
|
|
1080
|
+
}
|
|
1081
|
+
.jev-devtools .questions-help {
|
|
1082
|
+
margin: 8px 0;
|
|
1083
|
+
flex-shrink: 0;
|
|
1084
|
+
}
|
|
1085
|
+
.jev-devtools .questions-viewport {
|
|
1086
|
+
flex: 1;
|
|
1087
|
+
min-height: 0;
|
|
1088
|
+
overflow: auto;
|
|
1089
|
+
overflow-anchor: none;
|
|
1090
|
+
scrollbar-gutter: stable;
|
|
1091
|
+
padding: 3px;
|
|
1092
|
+
}
|
|
1093
|
+
.jev-devtools .questions-spacer {
|
|
1094
|
+
position: relative;
|
|
1095
|
+
width: 100%;
|
|
1096
|
+
}
|
|
1097
|
+
.jev-devtools .questions-row {
|
|
1098
|
+
position: absolute;
|
|
1099
|
+
top: 0;
|
|
1100
|
+
left: 0;
|
|
1101
|
+
width: 100%;
|
|
1102
|
+
padding-bottom: 14px;
|
|
1103
|
+
}
|
|
1104
|
+
.jev-devtools .questions-row .standalone {
|
|
1105
|
+
margin-bottom: 0;
|
|
1106
|
+
}
|
|
1107
|
+
.jev-devtools .questions-viewport:focus-visible,
|
|
1108
|
+
.jev-devtools .questions-row article:focus-visible {
|
|
1109
|
+
outline: 2px solid #e551ba;
|
|
1110
|
+
outline-offset: -2px;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/* Reserve measured card geometry while rapid scrolling uses a light preview. */
|
|
1114
|
+
.jev-devtools .questions-row[data-preview="true"] article {
|
|
1115
|
+
height: 100%;
|
|
1116
|
+
}
|
|
1117
|
+
.jev-devtools .question-scroll-preview p {
|
|
1118
|
+
margin-top: 16px;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/* The full distribution stays at reading size, outside the zoomable canvas. */
|
|
1122
|
+
.jev-devtools .lab-flow-viewport[hidden] {
|
|
1123
|
+
display: none;
|
|
1124
|
+
}
|
|
1125
|
+
.jev-devtools .lab-choice-browser {
|
|
1126
|
+
display: flex;
|
|
1127
|
+
flex-direction: column;
|
|
1128
|
+
flex: 1;
|
|
1129
|
+
min-height: 0;
|
|
1130
|
+
padding: 8px 16px;
|
|
1131
|
+
gap: 10px;
|
|
1132
|
+
}
|
|
1133
|
+
.jev-devtools .lab-choice-controls {
|
|
1134
|
+
display: flex;
|
|
1135
|
+
gap: 12px;
|
|
1136
|
+
align-items: end;
|
|
1137
|
+
flex-wrap: wrap;
|
|
1138
|
+
font-size: 12px;
|
|
1139
|
+
}
|
|
1140
|
+
.jev-devtools .lab-choice-controls label {
|
|
1141
|
+
display: grid;
|
|
1142
|
+
gap: 5px;
|
|
1143
|
+
}
|
|
1144
|
+
.jev-devtools .lab-choice-table-scroll {
|
|
1145
|
+
overflow: auto;
|
|
1146
|
+
min-height: 0;
|
|
1147
|
+
flex: 1;
|
|
1148
|
+
}
|
|
1149
|
+
.jev-devtools .lab-choice-browser table {
|
|
1150
|
+
width: 100%;
|
|
1151
|
+
border-collapse: collapse;
|
|
1152
|
+
font-size: 13px;
|
|
1153
|
+
}
|
|
1154
|
+
.jev-devtools .lab-choice-browser caption {
|
|
1155
|
+
text-align: left;
|
|
1156
|
+
padding: 8px 0;
|
|
1157
|
+
color: #aaa0af;
|
|
1158
|
+
}
|
|
1159
|
+
.jev-devtools .lab-choice-browser th,
|
|
1160
|
+
.jev-devtools .lab-choice-browser td {
|
|
1161
|
+
padding: 9px 12px;
|
|
1162
|
+
border-bottom: 1px solid #302634;
|
|
1163
|
+
text-align: left;
|
|
1164
|
+
}
|
|
1165
|
+
.jev-devtools .lab-choice-browser th {
|
|
1166
|
+
overflow-wrap: anywhere;
|
|
1167
|
+
max-width: 320px;
|
|
1168
|
+
}
|
|
1169
|
+
.jev-devtools .lab-choice-browser td {
|
|
1170
|
+
font-variant-numeric: tabular-nums;
|
|
1171
|
+
}
|
|
1172
|
+
.jev-devtools .lab-choice-browser code {
|
|
1173
|
+
white-space: normal;
|
|
1174
|
+
overflow-wrap: anywhere;
|
|
1175
|
+
}
|
|
1176
|
+
.jev-devtools .lab-choice-returned {
|
|
1177
|
+
display: block;
|
|
1178
|
+
font-size: 10px;
|
|
1179
|
+
color: #e551ba;
|
|
1180
|
+
}
|
|
1181
|
+
.jev-devtools .lab-choice-pagination {
|
|
1182
|
+
display: flex;
|
|
1183
|
+
align-items: center;
|
|
1184
|
+
gap: 12px;
|
|
1185
|
+
justify-content: space-between;
|
|
1186
|
+
font-size: 12px;
|
|
1187
|
+
}
|
|
1188
|
+
.jev-devtools .lab-choice-remainder button {
|
|
1189
|
+
margin-top: 7px;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.jev-devtools .lab-flow-compact .lab-output {
|
|
1193
|
+
padding: 12px;
|
|
1194
|
+
height: 104px;
|
|
1195
|
+
}
|
|
1196
|
+
.jev-devtools .lab-flow-compact .lab-output .lab-eyebrow {
|
|
1197
|
+
margin-bottom: 6px;
|
|
1198
|
+
}
|
|
1199
|
+
.jev-devtools .lab-flow-compact .lab-output h3 {
|
|
1200
|
+
font-size: 16px;
|
|
1201
|
+
margin-bottom: 8px;
|
|
1202
|
+
}
|
|
1203
|
+
.jev-devtools .lab-flow-compact .lab-output p {
|
|
1204
|
+
font-size: 12px;
|
|
1205
|
+
margin: 4px 0;
|
|
1206
|
+
}
|
|
1207
|
+
.jev-devtools .lab-flow-compact .lab-result {
|
|
1208
|
+
display: -webkit-box;
|
|
1209
|
+
-webkit-line-clamp: 2;
|
|
1210
|
+
-webkit-box-orient: vertical;
|
|
1211
|
+
overflow: hidden;
|
|
1212
|
+
overflow-wrap: anywhere;
|
|
1213
|
+
}
|
|
1214
|
+
.jev-devtools .lab-choice-remainder {
|
|
1215
|
+
padding: 12px;
|
|
1216
|
+
}
|
|
1217
|
+
.jev-devtools .lab-choice-remainder .lab-small {
|
|
1218
|
+
margin: 8px 0 0;
|
|
1219
|
+
font-size: 11px;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.jev-devtools .detail-body.detail-body-flow {
|
|
1223
|
+
flex-direction: column;
|
|
1224
|
+
}
|
|
1225
|
+
.jev-devtools .detail-body-flow > .warning,
|
|
1226
|
+
.jev-devtools .detail-body-flow > .error-banner {
|
|
1227
|
+
flex-shrink: 0;
|
|
1228
|
+
margin: 6px 16px;
|
|
1229
|
+
}
|
|
1230
|
+
.jev-devtools .lab-choice-browser {
|
|
1231
|
+
overflow: auto;
|
|
1232
|
+
}
|
|
1233
|
+
.jev-devtools .lab-choice-table-scroll {
|
|
1234
|
+
flex: 1 0 110px;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/* Only freshly revealed branches enter; pan/zoom remains on the canvas. */
|
|
1238
|
+
.jev-devtools .lab-card-enter {
|
|
1239
|
+
animation: jev-card-enter 280ms cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
|
|
1240
|
+
}
|
|
1241
|
+
.jev-devtools .lab-branch-enter {
|
|
1242
|
+
stroke-dasharray: 1;
|
|
1243
|
+
animation: jev-branch-enter 280ms ease-out backwards;
|
|
1244
|
+
}
|
|
1245
|
+
@keyframes jev-card-enter {
|
|
1246
|
+
from {
|
|
1247
|
+
opacity: 0;
|
|
1248
|
+
transform: translateX(-16px) scale(0.96);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
@keyframes jev-branch-enter {
|
|
1252
|
+
from {
|
|
1253
|
+
opacity: 0;
|
|
1254
|
+
stroke-dashoffset: 1;
|
|
1255
|
+
}
|
|
1256
|
+
to {
|
|
1257
|
+
opacity: 1;
|
|
1258
|
+
stroke-dashoffset: 0;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1262
|
+
.jev-devtools .lab-card-enter,
|
|
1263
|
+
.jev-devtools .lab-branch-enter {
|
|
1264
|
+
animation: none;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
package/lib/src/protocol.d.ts
CHANGED
|
@@ -62,3 +62,16 @@ export type Snapshot = {
|
|
|
62
62
|
version: 1;
|
|
63
63
|
traces: Trace[];
|
|
64
64
|
};
|
|
65
|
+
export type StreamSnapshot = {
|
|
66
|
+
version: 2;
|
|
67
|
+
revision: number;
|
|
68
|
+
traces: Trace[];
|
|
69
|
+
};
|
|
70
|
+
export type TraceDelta = {
|
|
71
|
+
version: 2;
|
|
72
|
+
base: number;
|
|
73
|
+
revision: number;
|
|
74
|
+
upserts: Trace[];
|
|
75
|
+
/** Authoritative newest-first retained IDs. Omitted IDs are removed. */
|
|
76
|
+
order: string[];
|
|
77
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const revision = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
2
|
+
function traces(value) {
|
|
3
|
+
if (!Array.isArray(value) || value.length > 200)
|
|
4
|
+
throw new Error("Invalid traces");
|
|
5
|
+
const ids = new Set();
|
|
6
|
+
for (const trace of value) {
|
|
7
|
+
if (!trace ||
|
|
8
|
+
trace.version !== 1 ||
|
|
9
|
+
typeof trace.id !== "string" ||
|
|
10
|
+
!trace.id ||
|
|
11
|
+
ids.has(trace.id))
|
|
12
|
+
throw new Error("Invalid trace ID");
|
|
13
|
+
ids.add(trace.id);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
// Owned by one EventSource connection. Unchanged trace objects keep their
|
|
18
|
+
// identity; validate the complete update before replacing any current state.
|
|
19
|
+
export class TraceStore {
|
|
20
|
+
current = [];
|
|
21
|
+
revision;
|
|
22
|
+
apply(event, input) {
|
|
23
|
+
if (!input || typeof input !== "object")
|
|
24
|
+
throw new Error("Invalid update");
|
|
25
|
+
const data = input;
|
|
26
|
+
if (event === "snapshot") {
|
|
27
|
+
if (data.version !== 1 && data.version !== 2)
|
|
28
|
+
throw new Error("Unsupported stream");
|
|
29
|
+
if (data.version === 2 && !revision(data.revision))
|
|
30
|
+
throw new Error("Invalid revision");
|
|
31
|
+
const next = traces(data.traces);
|
|
32
|
+
this.current = next;
|
|
33
|
+
this.revision =
|
|
34
|
+
data.version === 2 ? data.revision : undefined;
|
|
35
|
+
return next;
|
|
36
|
+
}
|
|
37
|
+
if (data.version !== 2 ||
|
|
38
|
+
this.revision === undefined ||
|
|
39
|
+
data.base !== this.revision ||
|
|
40
|
+
!revision(data.revision) ||
|
|
41
|
+
data.revision <= this.revision)
|
|
42
|
+
throw new Error("Inspector stream needs a fresh snapshot");
|
|
43
|
+
if (!Array.isArray(data.order) ||
|
|
44
|
+
data.order.length > 200 ||
|
|
45
|
+
data.order.some((id) => typeof id !== "string") ||
|
|
46
|
+
new Set(data.order).size !== data.order.length)
|
|
47
|
+
throw new Error("Invalid retained order");
|
|
48
|
+
const order = data.order;
|
|
49
|
+
const retained = new Set(order);
|
|
50
|
+
const upserts = traces(data.upserts);
|
|
51
|
+
if (upserts.some((t) => !retained.has(t.id)))
|
|
52
|
+
throw new Error("Unexpected upsert");
|
|
53
|
+
const byId = new Map(this.current.map((t) => [t.id, t]));
|
|
54
|
+
for (const trace of upserts)
|
|
55
|
+
byId.set(trace.id, trace);
|
|
56
|
+
const next = order.map((id) => {
|
|
57
|
+
const trace = byId.get(id);
|
|
58
|
+
if (!trace)
|
|
59
|
+
throw new Error("Missing retained trace");
|
|
60
|
+
return trace;
|
|
61
|
+
});
|
|
62
|
+
this.current = next;
|
|
63
|
+
this.revision = data.revision;
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { TraceStore } from "./trace-store.js";
|
|
2
|
+
export function connectTraceStream(url, onTraces, onConnection) {
|
|
3
|
+
let alive = true, events, retry, frame, fallback, pending;
|
|
4
|
+
const cancelPublish = () => {
|
|
5
|
+
if (frame !== undefined)
|
|
6
|
+
cancelAnimationFrame(frame);
|
|
7
|
+
clearTimeout(fallback);
|
|
8
|
+
frame = undefined;
|
|
9
|
+
fallback = undefined;
|
|
10
|
+
pending = undefined;
|
|
11
|
+
};
|
|
12
|
+
const publish = () => {
|
|
13
|
+
const next = pending;
|
|
14
|
+
cancelPublish();
|
|
15
|
+
if (alive && next)
|
|
16
|
+
onTraces(next);
|
|
17
|
+
};
|
|
18
|
+
const schedule = (next) => {
|
|
19
|
+
pending = next;
|
|
20
|
+
if (frame !== undefined)
|
|
21
|
+
return;
|
|
22
|
+
// Merge every protocol event, but render only the newest state per frame.
|
|
23
|
+
// Background tabs may suspend rAF; keep delivery bounded there as well.
|
|
24
|
+
frame = requestAnimationFrame(publish);
|
|
25
|
+
fallback = setTimeout(publish, 100);
|
|
26
|
+
};
|
|
27
|
+
const connect = () => {
|
|
28
|
+
if (!alive)
|
|
29
|
+
return;
|
|
30
|
+
// Native reconnects receive a full snapshot too. After an invalid delta,
|
|
31
|
+
// discard the cursor and reconnect rather than applying partial history.
|
|
32
|
+
const store = new TraceStore();
|
|
33
|
+
const source = new EventSource(url + "/events?protocol=2");
|
|
34
|
+
events = source;
|
|
35
|
+
const receive = (kind, event) => {
|
|
36
|
+
if (!alive || events !== source)
|
|
37
|
+
return;
|
|
38
|
+
try {
|
|
39
|
+
const next = store.apply(kind, JSON.parse(event.data));
|
|
40
|
+
schedule(next);
|
|
41
|
+
onConnection("connected");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
cancelPublish();
|
|
45
|
+
source.close();
|
|
46
|
+
events = undefined;
|
|
47
|
+
onConnection("resynchronizing");
|
|
48
|
+
retry = setTimeout(connect, 1000);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
source.addEventListener("snapshot", (event) => receive("snapshot", event));
|
|
52
|
+
source.addEventListener("delta", (event) => receive("delta", event));
|
|
53
|
+
source.onerror = () => {
|
|
54
|
+
if (alive && events === source)
|
|
55
|
+
onConnection("reconnecting");
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
connect();
|
|
59
|
+
return () => {
|
|
60
|
+
alive = false;
|
|
61
|
+
clearTimeout(retry);
|
|
62
|
+
cancelPublish();
|
|
63
|
+
events?.close();
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react";
|
|
2
2
|
import { fitCamera, zoomCamera, } from "./diagram-camera.js";
|
|
3
3
|
/** Lab-style camera. Pointer frames update the transform without rerendering cards. */
|
|
4
4
|
export function useDiagramViewport(width, height, scene) {
|
|
@@ -7,6 +7,10 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
7
7
|
const camera = useRef({ x: 0, y: 0, scale: 1 });
|
|
8
8
|
const size = useRef({ width: 0, height: 0 });
|
|
9
9
|
const fitted = useRef(true);
|
|
10
|
+
const bounds = useRef({ width, height });
|
|
11
|
+
useLayoutEffect(() => {
|
|
12
|
+
bounds.current = { width, height };
|
|
13
|
+
}, [width, height]);
|
|
10
14
|
const frame = useRef(null);
|
|
11
15
|
const [zoom, setZoom] = useState(100);
|
|
12
16
|
const paint = useCallback(() => {
|
|
@@ -28,9 +32,16 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
28
32
|
fitted.current = true;
|
|
29
33
|
if (size.current.width <= 0 || size.current.height <= 0)
|
|
30
34
|
return;
|
|
31
|
-
camera.current = fitCamera(size.current,
|
|
35
|
+
camera.current = fitCamera(size.current, bounds.current);
|
|
32
36
|
paint();
|
|
33
|
-
}, [
|
|
37
|
+
}, [paint]);
|
|
38
|
+
const preserveCamera = useCallback(() => {
|
|
39
|
+
fitted.current = false;
|
|
40
|
+
}, []);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (fitted.current)
|
|
43
|
+
fit();
|
|
44
|
+
}, [width, height, fit]);
|
|
34
45
|
const zoomBy = useCallback((factor, point) => {
|
|
35
46
|
fitted.current = false;
|
|
36
47
|
camera.current = zoomCamera(camera.current, factor, point ?? { x: size.current.width / 2, y: size.current.height / 2 });
|
|
@@ -168,5 +179,5 @@ export function useDiagramViewport(width, height, scene) {
|
|
|
168
179
|
frame.current = null;
|
|
169
180
|
};
|
|
170
181
|
}, [fit, zoomBy, paint, scene]);
|
|
171
|
-
return { viewport, canvas, zoom, zoomBy, fit };
|
|
182
|
+
return { viewport, canvas, zoom, zoomBy, fit, preserveCamera };
|
|
172
183
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.12"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
+
"@tanstack/react-virtual": "3.14.22",
|
|
9
10
|
"@typesafe-ai/sdk": "0.7.0"
|
|
10
11
|
},
|
|
11
12
|
"description": "A React drawer and Node server adapter for inspecting TypeSafe Jev requests.",
|
|
@@ -79,3 +79,5 @@ Report HTTP/capture checks separately from visual browser checks, and sample ver
|
|
|
79
79
|
| Production 404 | Expected when disabled; app inference remains independent. |
|
|
80
80
|
|
|
81
81
|
Capture is bounded to 50 calls in memory by default. Built-in redaction has limits; arbitrary secrets in prose are not detected. Diagrams show captured inputs/outputs, not hidden model reasoning or application routing decisions.
|
|
82
|
+
|
|
83
|
+
The current browser requests `/events?protocol=2`: the first event is a full version 2 `snapshot`, followed by `delta` events containing changed traces and the retained ID order. The unqualified `/events` check above intentionally remains compatible with legacy clients. Reconnecting always supplies a fresh snapshot.
|