behavior-wrapped 0.8.14 → 0.9.1
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/dist/assets/{index-DnLfWUh_.js → index-ajQ5w6T5.js} +2 -2
- package/dist/assets/index-ddSuXgMW.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/analysis.mjs +1 -1
- package/server/cli.mjs +31 -10
- package/server/launcher.mjs +44 -21
- package/server/leaderboard.mjs +2 -1
- package/server/phrase-card.mjs +8 -0
- package/server/public-report-schema.mjs +1 -1
- package/server/session-length-distribution.mjs +74 -0
- package/dist/assets/index-Ctd7RCMR.css +0 -1
package/server/launcher.mjs
CHANGED
|
@@ -25,14 +25,32 @@ const portArg = process.argv.find((arg) => arg.startsWith("--port="));
|
|
|
25
25
|
const port = Number(portArg?.split("=")[1] || 4317);
|
|
26
26
|
const configuredIdleMs = Number(process.env.BEHAVIOR_WRAPPED_HELPER_IDLE_MS);
|
|
27
27
|
const helperIdleMs = Number.isFinite(configuredIdleMs) && configuredIdleMs >= 100 ? configuredIdleMs : 5 * 60 * 1_000;
|
|
28
|
-
|
|
28
|
+
const configuredTestCatalogDelayMs = process.env.NODE_ENV === "test" ? Number(process.env.BEHAVIOR_WRAPPED_TEST_CATALOG_DELAY_MS) : 0;
|
|
29
|
+
const testCatalogDelayMs = Number.isFinite(configuredTestCatalogDelayMs) && configuredTestCatalogDelayMs > 0 ? configuredTestCatalogDelayMs : 0;
|
|
30
|
+
let catalog = null;
|
|
31
|
+
let catalogPromise = null;
|
|
29
32
|
|
|
30
33
|
async function loadCatalog() {
|
|
34
|
+
if (testCatalogDelayMs) await new Promise((resolve) => setTimeout(resolve, testCatalogDelayMs));
|
|
31
35
|
const found = await discoverAllSessionsAsync(demo ? { claudeRoot: fixtureRoot, coworkRoot: coworkFixtureRoot, codexRoots: [codexFixtureRoot], cache: false } : undefined);
|
|
32
36
|
if (demo) found.sessions = found.sessions.map((session, index) => ({ ...session, synthetic: true, label: `Demo session ${index + 1}` }));
|
|
33
37
|
return found;
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
async function catalogForRequest({ refresh = false } = {}) {
|
|
41
|
+
if (refresh || !catalogPromise) {
|
|
42
|
+
const loading = loadCatalog();
|
|
43
|
+
catalogPromise = loading;
|
|
44
|
+
try {
|
|
45
|
+
catalog = await loading;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (catalogPromise === loading) catalogPromise = null;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return catalogPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
function securityHeaders(extra = {}) {
|
|
37
55
|
return {
|
|
38
56
|
"X-Content-Type-Options": "nosniff",
|
|
@@ -71,23 +89,24 @@ function readBody(request, maximumBytes = 1_000_000) {
|
|
|
71
89
|
});
|
|
72
90
|
}
|
|
73
91
|
|
|
74
|
-
async function chosenRecords(ids, options = {}) {
|
|
92
|
+
async function chosenRecords(ids, options = {}, activeCatalog = null) {
|
|
93
|
+
const availableCatalog = activeCatalog || await catalogForRequest();
|
|
75
94
|
const selected = [];
|
|
76
95
|
for (const id of ids) {
|
|
77
|
-
const session =
|
|
96
|
+
const session = availableCatalog.index.get(id);
|
|
78
97
|
if (session) selected.push({ sessionId: id, agent: session.agent, records: await readRecordsAsync(session.file, session.agent, options) });
|
|
79
98
|
}
|
|
80
99
|
return selected;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
|
-
function publicCatalog() {
|
|
102
|
+
function publicCatalog(availableCatalog) {
|
|
84
103
|
const agentNames = supportedAgentNames(process.platform, { includeCowork: demo || process.platform === "darwin" });
|
|
85
104
|
return {
|
|
86
|
-
rootAvailable:
|
|
105
|
+
rootAvailable: availableCatalog.rootAvailable,
|
|
87
106
|
demo,
|
|
88
|
-
projects:
|
|
89
|
-
sessions:
|
|
90
|
-
defaultRange: defaultDateRange(
|
|
107
|
+
projects: availableCatalog.projects,
|
|
108
|
+
sessions: availableCatalog.sessions.map((session, index) => ({ ...session, label: session.label || `Session ${index + 1}` })),
|
|
109
|
+
defaultRange: defaultDateRange(availableCatalog.sessions, { days: DEFAULT_WINDOW_DAYS, anchorLatest: demo }),
|
|
91
110
|
agentNames,
|
|
92
111
|
privacy: { canonicalDirectories: canonicalSessionDirectoryLabels(), networkRequests: "only-after-final-donation-consent" },
|
|
93
112
|
};
|
|
@@ -99,10 +118,10 @@ const server = http.createServer(async (request, response) => {
|
|
|
99
118
|
if (!new Set([`127.0.0.1:${port}`, `localhost:${port}`]).has(request.headers.host || "")) return json(response, 403, { error: "Local access only" });
|
|
100
119
|
idleShutdown.touch();
|
|
101
120
|
const url = new URL(request.url || "/", `http://${request.headers.host}`);
|
|
102
|
-
if (request.method === "GET" && url.pathname === "/api/health") return json(response, 200, { app: "behavior-wrapped", version: APP_VERSION, local: true, purpose: "research-donation", donationProtocol: LOCAL_DONATION_PROTOCOL, pid: process.pid, demo });
|
|
121
|
+
if (request.method === "GET" && url.pathname === "/api/health") return json(response, 200, { app: "behavior-wrapped", version: APP_VERSION, local: true, purpose: "research-donation", donationProtocol: LOCAL_DONATION_PROTOCOL, pid: process.pid, demo, catalogState: catalog ? "ready" : catalogPromise ? "loading" : "not-loaded" });
|
|
103
122
|
if (request.method === "GET" && url.pathname === "/api/discover") {
|
|
104
|
-
|
|
105
|
-
return json(response, 200, publicCatalog());
|
|
123
|
+
const availableCatalog = await catalogForRequest({ refresh: true });
|
|
124
|
+
return json(response, 200, publicCatalog(availableCatalog));
|
|
106
125
|
}
|
|
107
126
|
const reportMatch = url.pathname.match(/^\/api\/reports\/([A-Za-z0-9_-]{8,32})$/);
|
|
108
127
|
if (request.method === "GET" && reportMatch) {
|
|
@@ -116,39 +135,43 @@ const server = http.createServer(async (request, response) => {
|
|
|
116
135
|
if (request.method === "GET" && selectionMatch) {
|
|
117
136
|
const report = loadReport(selectionMatch[1]);
|
|
118
137
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
119
|
-
const
|
|
138
|
+
const availableCatalog = await catalogForRequest();
|
|
139
|
+
const available = new Set(availableCatalog.sessions.map((session) => session.id));
|
|
120
140
|
return json(response, 200, { sessionIds: (report.sessionIds || []).filter((id) => available.has(id)), localPrivateSelection: true });
|
|
121
141
|
}
|
|
122
142
|
const workaroundEvidenceMatch = url.pathname.match(/^\/api\/reports\/([A-Za-z0-9_-]{8,32})\/workarounds$/);
|
|
123
143
|
if (request.method === "GET" && workaroundEvidenceMatch) {
|
|
124
144
|
const report = loadReport(workaroundEvidenceMatch[1]);
|
|
125
145
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
146
|
+
const availableCatalog = await catalogForRequest();
|
|
126
147
|
const allowed = new Set(report.sessionIds || []);
|
|
127
|
-
const ids = [...new Set((report.workaroundReview?.occurrences || []).map((occurrence) => occurrence?.location?.sessionId).filter((id) => allowed.has(id) &&
|
|
128
|
-
const records = await chosenRecords(ids, { includePrivateToolDetails: true });
|
|
129
|
-
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
148
|
+
const ids = [...new Set((report.workaroundReview?.occurrences || []).map((occurrence) => occurrence?.location?.sessionId).filter((id) => allowed.has(id) && availableCatalog.index.has(id)))].slice(0, 100);
|
|
149
|
+
const records = await chosenRecords(ids, { includePrivateToolDetails: true }, availableCatalog);
|
|
150
|
+
const labels = new Map(publicCatalog(availableCatalog).sessions.map((session) => [session.id, session]));
|
|
130
151
|
return json(response, 200, makeWorkaroundEvidencePreview(report, records, labels));
|
|
131
152
|
}
|
|
132
153
|
const interactionEvidenceMatch = url.pathname.match(/^\/api\/reports\/([A-Za-z0-9_-]{8,32})\/interactions$/);
|
|
133
154
|
if (request.method === "GET" && interactionEvidenceMatch) {
|
|
134
155
|
const report = loadReport(interactionEvidenceMatch[1]);
|
|
135
156
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
157
|
+
const availableCatalog = await catalogForRequest();
|
|
136
158
|
const allowed = new Set(report.sessionIds || []);
|
|
137
159
|
const references = [...(report.interactionReview?.frustrated || []), ...(report.interactionReview?.grateful || []), ...(report.apologyReview?.user || []), ...(report.apologyReview?.agent || [])];
|
|
138
|
-
const ids = [...new Set(references.map((reference) => reference?.location?.sessionId).filter((id) => allowed.has(id) &&
|
|
139
|
-
const records = await chosenRecords(ids);
|
|
140
|
-
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
160
|
+
const ids = [...new Set(references.map((reference) => reference?.location?.sessionId).filter((id) => allowed.has(id) && availableCatalog.index.has(id)))].slice(0, 200);
|
|
161
|
+
const records = await chosenRecords(ids, {}, availableCatalog);
|
|
162
|
+
const labels = new Map(publicCatalog(availableCatalog).sessions.map((session) => [session.id, session]));
|
|
141
163
|
return json(response, 200, makeInteractionEvidencePreview(report, records, labels));
|
|
142
164
|
}
|
|
143
165
|
if (request.method === "POST" && url.pathname === "/api/donation-preview") {
|
|
144
166
|
const body = await readBody(request);
|
|
145
167
|
const report = loadReport(body.reportId);
|
|
146
168
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
169
|
+
const availableCatalog = await catalogForRequest();
|
|
147
170
|
const allowed = new Set(report.sessionIds || []);
|
|
148
|
-
const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((id) => allowed.has(id) &&
|
|
149
|
-
const records = await chosenRecords(ids);
|
|
171
|
+
const ids = Array.isArray(body.sessionIds) ? body.sessionIds.filter((id) => allowed.has(id) && availableCatalog.index.has(id)).slice(0, 250) : [];
|
|
172
|
+
const records = await chosenRecords(ids, {}, availableCatalog);
|
|
150
173
|
if (!records.length) return json(response, 400, { error: "Choose at least one available session." });
|
|
151
|
-
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
174
|
+
const labels = new Map(publicCatalog(availableCatalog).sessions.map((session) => [session.id, session]));
|
|
152
175
|
const disabledRedactions = Array.isArray(body.disabledRedactions) ? body.disabledRedactions.filter((kind) => typeof kind === "string" && /^[a-z0-9-]{1,64}$/.test(kind)).slice(0, 20) : [];
|
|
153
176
|
const disabledMatches = Array.isArray(body.disabledMatches) ? body.disabledMatches.filter((id) => typeof id === "string" && /^[a-f0-9]{24}$/.test(id)).slice(0, 5_000) : [];
|
|
154
177
|
const unredacted = body.previewMode === "unredacted";
|
package/server/leaderboard.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BEHAVIOR_WRAPPED_ORIGIN } from "./origins.mjs";
|
|
2
|
+
import { buildSessionLengthDistribution } from "./session-length-distribution.mjs";
|
|
2
3
|
export const LEADERBOARD_RELAY_ORIGIN = BEHAVIOR_WRAPPED_ORIGIN;
|
|
3
4
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
4
5
|
const demoTokens = [820_000, 2_400_000, 8_900_000, 14_300_000, 31_000_000, 47_500_000, 83_000_000, 126_000_000, 210_000_000, 380_000_000, 620_000_000, 940_000_000];
|
|
@@ -88,7 +89,7 @@ export function syntheticLeaderboardSnapshot(aggregate, participation = null) {
|
|
|
88
89
|
},
|
|
89
90
|
session_lengths: {
|
|
90
91
|
values: aggregate.session_turn_counts,
|
|
91
|
-
|
|
92
|
+
distribution: buildSessionLengthDistribution(demoSessionTurnCounts.flat()),
|
|
92
93
|
},
|
|
93
94
|
phrases: {
|
|
94
95
|
entries: aggregate.favorite_phrase ? [{ participant_id: 1, phrase: aggregate.favorite_phrase, occurrences: aggregate.phrase_occurrences, sessions: aggregate.phrase_sessions }] : [],
|
package/server/phrase-card.mjs
CHANGED
|
@@ -208,6 +208,14 @@ export function buildLocalPhraseCard(candidates) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
export async function phraseCardWithLocalFallback(candidates, judgedCard, { onFallback } = {}) {
|
|
212
|
+
try { return await judgedCard; }
|
|
213
|
+
catch (error) {
|
|
214
|
+
onFallback?.(error);
|
|
215
|
+
return buildLocalPhraseCard(candidates);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
211
219
|
function timeoutMessage(error, timeoutMs) {
|
|
212
220
|
if (error?.name === "TimeoutError" || error?.name === "AbortError") return new Error(`${PHRASE_JUDGE_NAME} timed out after ${Math.round(timeoutMs / 1000)} seconds.`);
|
|
213
221
|
return error;
|
|
@@ -19,7 +19,7 @@ function safeBreakdown(value, labelKey, countKey, allowedLabels) {
|
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const stockPhraseLabels = ["You're right", "Say the word", "genuinely", "one wrinkle", "load bearing", "the full picture", "delve"];
|
|
22
|
+
const stockPhraseLabels = ["You're right", "Say the word", "genuinely", "one wrinkle", "load bearing", "full picture", "the full picture", "delve"];
|
|
23
23
|
const allowedAgents = new Set(["claude", "cowork", "codex"]);
|
|
24
24
|
|
|
25
25
|
function safeTurnCounts(value) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const MAX_SESSION_TURNS = 1_000_000;
|
|
2
|
+
const CONTOUR_POINT_COUNT = 64;
|
|
3
|
+
|
|
4
|
+
function validSessionTurns(values) {
|
|
5
|
+
return Array.isArray(values)
|
|
6
|
+
? values.flatMap((value) => Number.isInteger(value) && value >= 1 && value <= MAX_SESSION_TURNS ? [value] : [])
|
|
7
|
+
: [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function median(values) {
|
|
11
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
12
|
+
if (!sorted.length) return 0;
|
|
13
|
+
const middle = Math.floor(sorted.length / 2);
|
|
14
|
+
return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildSessionLengthDistribution(values) {
|
|
18
|
+
const sessions = validSessionTurns(values);
|
|
19
|
+
if (!sessions.length) return { session_count: 0, median_turns: 0, min_turns: 0, max_turns: 0, points: [] };
|
|
20
|
+
|
|
21
|
+
const logs = sessions.map((value) => Math.log10(value));
|
|
22
|
+
const observedMinimum = Math.min(...logs);
|
|
23
|
+
const observedMaximum = Math.max(...logs);
|
|
24
|
+
const observedSpan = observedMaximum - observedMinimum;
|
|
25
|
+
const padding = Math.max(.12, observedSpan * .08);
|
|
26
|
+
const domainMinimum = Math.max(0, observedMinimum - padding);
|
|
27
|
+
const domainMaximum = Math.max(domainMinimum + .32, observedMaximum + padding);
|
|
28
|
+
const mean = logs.reduce((sum, value) => sum + value, 0) / logs.length;
|
|
29
|
+
const variance = logs.reduce((sum, value) => sum + (value - mean) ** 2, 0) / logs.length;
|
|
30
|
+
const deviation = Math.sqrt(variance);
|
|
31
|
+
const bandwidth = Math.max(.08, Math.min(.32, 1.06 * (deviation || .18) * logs.length ** -.2));
|
|
32
|
+
const raw = Array.from({ length: CONTOUR_POINT_COUNT }, (_, index) => {
|
|
33
|
+
const position = domainMinimum + index / (CONTOUR_POINT_COUNT - 1) * (domainMaximum - domainMinimum);
|
|
34
|
+
const density = logs.reduce((sum, value) => sum + Math.exp(-.5 * ((position - value) / bandwidth) ** 2), 0) / logs.length;
|
|
35
|
+
return { turns: 10 ** position, density };
|
|
36
|
+
});
|
|
37
|
+
const maximumDensity = Math.max(...raw.map((point) => point.density), Number.EPSILON);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
session_count: sessions.length,
|
|
41
|
+
median_turns: Number(median(sessions).toFixed(1)),
|
|
42
|
+
min_turns: Math.min(...sessions),
|
|
43
|
+
max_turns: Math.max(...sessions),
|
|
44
|
+
points: raw.map((point) => ({
|
|
45
|
+
turns: Number(point.turns.toPrecision(7)),
|
|
46
|
+
density: Number((point.density / maximumDensity).toFixed(4)),
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseSessionLengthDistribution(value) {
|
|
52
|
+
let parsed;
|
|
53
|
+
try { parsed = typeof value === "string" ? JSON.parse(value) : value; }
|
|
54
|
+
catch { return null; }
|
|
55
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
56
|
+
const sessionCount = Number(parsed.session_count);
|
|
57
|
+
const medianTurns = Number(parsed.median_turns);
|
|
58
|
+
const minTurns = Number(parsed.min_turns);
|
|
59
|
+
const maxTurns = Number(parsed.max_turns);
|
|
60
|
+
if (![sessionCount, medianTurns, minTurns, maxTurns].every(Number.isFinite)) return null;
|
|
61
|
+
if (!Number.isInteger(sessionCount) || sessionCount < 0 || sessionCount > 100_000_000) return null;
|
|
62
|
+
if (sessionCount === 0) return parsed.points?.length === 0 ? { session_count: 0, median_turns: 0, min_turns: 0, max_turns: 0, points: [] } : null;
|
|
63
|
+
if (medianTurns < 1 || minTurns < 1 || maxTurns < minTurns || maxTurns > MAX_SESSION_TURNS || medianTurns > maxTurns) return null;
|
|
64
|
+
if (!Array.isArray(parsed.points) || parsed.points.length !== CONTOUR_POINT_COUNT) return null;
|
|
65
|
+
let previousTurns = 0;
|
|
66
|
+
const points = parsed.points.flatMap((point) => {
|
|
67
|
+
const turns = Number(point?.turns);
|
|
68
|
+
const density = Number(point?.density);
|
|
69
|
+
if (!Number.isFinite(turns) || turns <= previousTurns || turns < 1 || turns > MAX_SESSION_TURNS * 2 || !Number.isFinite(density) || density < 0 || density > 1) return [];
|
|
70
|
+
previousTurns = turns;
|
|
71
|
+
return [{ turns, density }];
|
|
72
|
+
});
|
|
73
|
+
return points.length === CONTOUR_POINT_COUNT ? { session_count: sessionCount, median_turns: medianTurns, min_turns: minTurns, max_turns: maxTurns, points } : null;
|
|
74
|
+
}
|