behavior-wrapped 0.8.2 → 0.8.4
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 +1 -1
- package/dist/assets/index-CAStSiD9.js +11 -0
- package/dist/assets/index-CudcHj4C.css +1 -0
- package/dist/index.html +2 -2
- package/fixtures/cowork-sessions/synthetic-account/synthetic-workspace/local_44444444-4444-4444-8444-444444444444/audit.jsonl +1 -1
- package/package.json +1 -1
- package/server/analysis.mjs +136 -6
- package/server/cli.mjs +1 -1
- package/server/discovery.mjs +19 -2
- package/server/interaction-evidence.mjs +14 -12
- package/server/launcher.mjs +2 -2
- package/server/local-helper-runtime.mjs +4 -3
- package/server/public-report-schema.mjs +57 -0
- package/dist/assets/index-CdzEeY5M.js +0 -11
- package/dist/assets/index-hFREdA4j.css +0 -1
package/server/cli.mjs
CHANGED
|
@@ -259,7 +259,7 @@ async function createWrapped() {
|
|
|
259
259
|
const id = createReportId();
|
|
260
260
|
const safeFindings = analyzed.findings.map(({ evidence, method, ...finding }) => finding);
|
|
261
261
|
const hasPrivateWorkaroundEvidence = Boolean(analyzed.workaroundReview?.occurrences?.length || analyzed.workaroundReview?.borderline?.length);
|
|
262
|
-
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: formatAgentSource(chosenSessions), stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, interactionReview: analyzed.interactionReview, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !localOnly, analysisMode: localOnly ? "local-only" : "remote", leaderboardParticipation: localOnly ? "excluded" : "included-by-default", ...(localOnly ? { transmittedData: `None; ${testMode ? "test" : "local-only"} mode stays on this device.`, externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
262
|
+
const report = { id, createdAt: new Date().toISOString(), rangeLabel: formatRange(chosenSessions), source: formatAgentSource(chosenSessions), stats: analyzed.stats, findings: safeFindings, phraseCard: analyzed.phraseCard, interactionCard: analyzed.interactionCard, interactionReview: analyzed.interactionReview, apologyReview: analyzed.apologyReview, workaroundCard: analyzed.workaroundCard, workaroundReview: analyzed.workaroundReview, sessionSummaries: analyzed.sessionSummaries || [], sessionIds: chosenSessions.map((session) => session.id), donationHelperUrl: `${baseUrl}/donate/${id}`, privacy: { shareSafe: !hasPrivateWorkaroundEvidence, containsTranscriptText: hasPrivateWorkaroundEvidence, externalTransmission: !localOnly, analysisMode: localOnly ? "local-only" : "remote", leaderboardParticipation: localOnly ? "excluded" : "included-by-default", ...(localOnly ? { transmittedData: `None; ${testMode ? "test" : "local-only"} mode stays on this device.`, externalRecipient: "None" } : { transmittedData: "redacted phrase, interaction-tone, and session-topic candidates; locally redacted context windows around explicit blockers for workaround discovery; aggregate report statistics; and a random client ID only", externalRecipient: "Behavior Wrapped relay, OpenRouter, a zero-data-retention GPT-5.6 Luna provider, and public report hosting" }) } };
|
|
263
263
|
let publicUrl = null;
|
|
264
264
|
if (!localOnly) {
|
|
265
265
|
progress.start("Publishing share-safe Wrapped", "strict aggregate-only schema");
|
package/server/discovery.mjs
CHANGED
|
@@ -126,7 +126,7 @@ function shouldKeepCoworkRecord(record, seenUuids) {
|
|
|
126
126
|
if (seenUuids.has(record.uuid)) return false;
|
|
127
127
|
seenUuids.add(record.uuid);
|
|
128
128
|
}
|
|
129
|
-
return ["user", "assistant", "system"].includes(record.type);
|
|
129
|
+
return ["user", "assistant", "system", "result"].includes(record.type);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function coworkMetadataFromRecords(file, stat, records, metadata = readCoworkMetadata(file)) {
|
|
@@ -427,6 +427,14 @@ function normalizeCodexRecords(records, { includePrivateToolDetails = false } =
|
|
|
427
427
|
if (record.type === "turn_context" && typeof payload.model === "string") {
|
|
428
428
|
currentModel = payload.model;
|
|
429
429
|
hasSeenModelContext = true;
|
|
430
|
+
const sandboxPolicy = payload.sandbox_policy?.type || payload.permission_profile?.type || (typeof payload.sandbox_policy === "string" ? payload.sandbox_policy : null);
|
|
431
|
+
if (payload.approval_policy || sandboxPolicy) normalized.push({
|
|
432
|
+
type: "system",
|
|
433
|
+
subtype: "permission_mode",
|
|
434
|
+
timestamp: record.timestamp,
|
|
435
|
+
approvalPolicy: typeof payload.approval_policy === "string" ? payload.approval_policy : null,
|
|
436
|
+
sandboxPolicy,
|
|
437
|
+
});
|
|
430
438
|
} else if (record.type === "response_item" && payload.type === "message" && (payload.role === "user" || payload.role === "assistant")) {
|
|
431
439
|
const content = textBlocks(payload.content);
|
|
432
440
|
if (content.length) normalized.push({ type: payload.role, timestamp: record.timestamp, message: { content, ...(payload.role === "assistant" ? { model: currentModel } : {}) } });
|
|
@@ -447,8 +455,10 @@ function normalizeCodexRecords(records, { includePrivateToolDetails = false } =
|
|
|
447
455
|
const canSummarizeRestriction = status.failed || (!status.wrapped && restrictionEligibleActions.has(semantics?.action));
|
|
448
456
|
const errorSummary = canSummarizeRestriction ? restrictionErrorSummary(output) : null;
|
|
449
457
|
normalized.push({ type: "user", isMeta: true, timestamp: record.timestamp, message: { content: [{ type: "tool_result", is_error: Boolean(errorSummary) || status.failed || unwrappedFailure, error_summary: errorSummary, ...(includePrivateToolDetails && output ? { content: output } : {}) }] } });
|
|
458
|
+
} else if (record.type === "event_msg" && payload.type === "task_complete" && Number(payload.duration_ms) > 0) {
|
|
459
|
+
normalized.push({ type: "system", subtype: "turn_duration", timestamp: record.timestamp, durationMs: Number(payload.duration_ms), model: currentModel });
|
|
450
460
|
} else if (record.type === "event_msg" && payload.type === "turn_aborted") {
|
|
451
|
-
normalized.push({ type: "system", subtype: "interrupt", timestamp: record.timestamp, content: "interrupt" });
|
|
461
|
+
normalized.push({ type: "system", subtype: "interrupt", timestamp: record.timestamp, content: "interrupt", model: currentModel });
|
|
452
462
|
} else if (record.type === "event_msg" && payload.type === "token_count" && payload.info?.total_token_usage) {
|
|
453
463
|
const total = payload.info.total_token_usage;
|
|
454
464
|
const usage = Object.fromEntries(Object.keys(previousUsage).map((key) => [key, Math.max(0, (Number(total[key]) || 0) - previousUsage[key])]));
|
|
@@ -477,6 +487,11 @@ function normalizeCoworkRecords(records) {
|
|
|
477
487
|
for (const record of records) {
|
|
478
488
|
if (!shouldKeepCoworkRecord(record, seenUuids)) continue;
|
|
479
489
|
const timestamp = coworkRecordTimestamp(record);
|
|
490
|
+
if (record.type === "result") {
|
|
491
|
+
const durationMs = Number(record.duration_ms);
|
|
492
|
+
if (!record.is_error && record.subtype === "success" && durationMs > 0) normalized.push({ type: "system", subtype: "turn_duration", ...(timestamp ? { timestamp } : {}), durationMs, model: typeof record.model === "string" ? record.model : "Cowork model" });
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
480
495
|
const message = record.message && typeof record.message === "object" ? {
|
|
481
496
|
...(record.message.content !== undefined ? { content: record.message.content } : {}),
|
|
482
497
|
...(typeof record.message.model === "string" ? { model: record.message.model } : {}),
|
|
@@ -488,6 +503,8 @@ function normalizeCoworkRecords(records) {
|
|
|
488
503
|
...(record.isMeta ? { isMeta: true } : {}),
|
|
489
504
|
...(record.subtype ? { subtype: record.subtype } : {}),
|
|
490
505
|
...(record.content !== undefined ? { content: record.content } : {}),
|
|
506
|
+
...(typeof record.model === "string" ? { model: record.model } : {}),
|
|
507
|
+
...(typeof record.permissionMode === "string" ? { permissionMode: record.permissionMode } : {}),
|
|
491
508
|
...(message ? { message } : {}),
|
|
492
509
|
};
|
|
493
510
|
const messageId = record.type === "assistant" && typeof record?.message?.id === "string" ? record.message.id : null;
|
|
@@ -15,12 +15,12 @@ function transcriptRole(record) {
|
|
|
15
15
|
return record?.type === "assistant" ? "assistant" : record?.type === "user" && !record?.isMeta ? "user" : null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function matchingRecordIndex(reference, records) {
|
|
18
|
+
function matchingRecordIndex(reference, records, expectedRole) {
|
|
19
19
|
const index = reference?.location?.recordIndex;
|
|
20
20
|
if (Number.isInteger(index) && index >= 0 && index < records.length) return index;
|
|
21
21
|
const timestamp = reference?.location?.timestamp;
|
|
22
22
|
if (!timestamp) return null;
|
|
23
|
-
const fallback = records.findIndex((record) => record?.timestamp === timestamp && record
|
|
23
|
+
const fallback = records.findIndex((record) => record?.timestamp === timestamp && transcriptRole(record) === expectedRole);
|
|
24
24
|
return fallback >= 0 ? fallback : null;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -33,12 +33,12 @@ function adjacentMessage(records, fromIndex, direction) {
|
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function exactOccurrence(reference, index, records, metadata) {
|
|
37
|
-
const recordIndex = matchingRecordIndex(reference, records);
|
|
36
|
+
function exactOccurrence(reference, index, records, metadata, expectedRole) {
|
|
37
|
+
const recordIndex = matchingRecordIndex(reference, records, expectedRole);
|
|
38
38
|
if (recordIndex === null) return null;
|
|
39
39
|
const record = records[recordIndex];
|
|
40
40
|
const text = visibleText(record);
|
|
41
|
-
if (record
|
|
41
|
+
if (transcriptRole(record) !== expectedRole || !text) return null;
|
|
42
42
|
const before = adjacentMessage(records, recordIndex, -1);
|
|
43
43
|
const after = adjacentMessage(records, recordIndex, 1);
|
|
44
44
|
return {
|
|
@@ -50,16 +50,16 @@ function exactOccurrence(reference, index, records, metadata) {
|
|
|
50
50
|
startedAt: metadata?.startedAt || records.find((item) => item?.timestamp)?.timestamp || null,
|
|
51
51
|
},
|
|
52
52
|
timestamp: record.timestamp || null,
|
|
53
|
-
messages: [before, { role:
|
|
53
|
+
messages: [before, { role: expectedRole, text, timestamp: record.timestamp || null, highlighted: true }, after].filter(Boolean),
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function buildKind(
|
|
58
|
-
return (
|
|
57
|
+
function buildKind(review, kind, expectedRole, recordsById, metadataById) {
|
|
58
|
+
return (review?.[kind] || []).slice(0, MAX_OCCURRENCES_PER_KIND).flatMap((reference, index) => {
|
|
59
59
|
const sessionId = reference?.location?.sessionId;
|
|
60
60
|
const records = recordsById.get(sessionId);
|
|
61
61
|
if (!records) return [];
|
|
62
|
-
const occurrence = exactOccurrence(reference, index, records, metadataById.get(sessionId));
|
|
62
|
+
const occurrence = exactOccurrence(reference, index, records, metadataById.get(sessionId), expectedRole);
|
|
63
63
|
return occurrence ? [occurrence] : [];
|
|
64
64
|
});
|
|
65
65
|
}
|
|
@@ -67,11 +67,13 @@ function buildKind(report, kind, recordsById, metadataById) {
|
|
|
67
67
|
export function makeInteractionEvidencePreview(report, sessionRecords, metadataById) {
|
|
68
68
|
const recordsById = new Map(sessionRecords.map((session) => [session.sessionId, session.records]));
|
|
69
69
|
return {
|
|
70
|
-
format: "behavior-wrapped-interaction-evidence-
|
|
70
|
+
format: "behavior-wrapped-interaction-evidence-v2",
|
|
71
71
|
localPrivate: true,
|
|
72
72
|
standardRedactionsApplied: false,
|
|
73
73
|
reportId: report?.id,
|
|
74
|
-
frustrated: buildKind(report, "frustrated", recordsById, metadataById),
|
|
75
|
-
grateful: buildKind(report, "grateful", recordsById, metadataById),
|
|
74
|
+
frustrated: buildKind(report?.interactionReview, "frustrated", "user", recordsById, metadataById),
|
|
75
|
+
grateful: buildKind(report?.interactionReview, "grateful", "user", recordsById, metadataById),
|
|
76
|
+
userApologies: buildKind(report?.apologyReview, "user", "user", recordsById, metadataById),
|
|
77
|
+
agentApologies: buildKind(report?.apologyReview, "agent", "assistant", recordsById, metadataById),
|
|
76
78
|
};
|
|
77
79
|
}
|
package/server/launcher.mjs
CHANGED
|
@@ -107,7 +107,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
107
107
|
if (request.method === "GET" && reportMatch) {
|
|
108
108
|
const report = loadReport(reportMatch[1]);
|
|
109
109
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
110
|
-
const { sessionIds, workaroundReview, interactionReview, ...shareSafeReport } = report;
|
|
110
|
+
const { sessionIds, workaroundReview, interactionReview, apologyReview, ...shareSafeReport } = report;
|
|
111
111
|
shareSafeReport.privacy = { ...shareSafeReport.privacy, shareSafe: true, containsTranscriptText: false };
|
|
112
112
|
return json(response, 200, shareSafeReport);
|
|
113
113
|
}
|
|
@@ -133,7 +133,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
133
133
|
const report = loadReport(interactionEvidenceMatch[1]);
|
|
134
134
|
if (!report) return json(response, 404, { error: "Saved report not found" });
|
|
135
135
|
const allowed = new Set(report.sessionIds || []);
|
|
136
|
-
const references = [...(report.interactionReview?.frustrated || []), ...(report.interactionReview?.grateful || [])];
|
|
136
|
+
const references = [...(report.interactionReview?.frustrated || []), ...(report.interactionReview?.grateful || []), ...(report.apologyReview?.user || []), ...(report.apologyReview?.agent || [])];
|
|
137
137
|
const ids = [...new Set(references.map((reference) => reference?.location?.sessionId).filter((id) => allowed.has(id) && catalog.index.has(id)))].slice(0, 200);
|
|
138
138
|
const records = await chosenRecords(ids);
|
|
139
139
|
const labels = new Map(publicCatalog().sessions.map((session) => [session.id, session]));
|
|
@@ -42,9 +42,10 @@ export function createIdleShutdownController({
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function isVerifiedLauncherCommand(command, port) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const value = String(command || "");
|
|
46
|
+
if (!new RegExp(`(?:/(?:agent-)?behavior-wrapped/|(?:^|\\s))server/launcher\\.mjs(?:\\s|$)`, "i").test(value)) return false;
|
|
47
|
+
const explicitPort = value.match(/(?:^|\s)--port=(\d+)(?:\s|$)/)?.[1];
|
|
48
|
+
return explicitPort ? Number(explicitPort) === Number(port) : Number(port) === 4317;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function processTools(platform) {
|
|
@@ -36,6 +36,44 @@ function safeStockPhrases(value) {
|
|
|
36
36
|
return stockPhraseLabels.map((phrase) => ({ phrase, count: counts.get(phrase) || 0 }));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function safeRepeatedInstructions(value) {
|
|
40
|
+
if (!Array.isArray(value)) return [];
|
|
41
|
+
return value.slice(0, 4).flatMap((item) => {
|
|
42
|
+
const instruction = safeText(item?.instruction, 160).trim();
|
|
43
|
+
const occurrences = Math.round(safeNumber(item?.occurrences, 1_000_000));
|
|
44
|
+
const distinctSessions = Math.round(safeNumber(item?.distinctSessions, 1_000_000));
|
|
45
|
+
if (!instruction || occurrences < 2 || distinctSessions < 1 || distinctSessions > occurrences) return [];
|
|
46
|
+
if (/\[(?:REDACTED|REMOVED)[^\]]*\]|https?:\/\/|(?:\/Users\/|\/home\/)|```|<[^>]+>|[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i.test(instruction)) return [];
|
|
47
|
+
return [{ instruction, occurrences, distinctSessions }];
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function safeTrustCurve(value) {
|
|
52
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.points)) return null;
|
|
53
|
+
let previousOffset = -1;
|
|
54
|
+
const points = value.points.slice(0, 4_000).flatMap((item) => {
|
|
55
|
+
const dayOffset = Math.round(safeNumber(item?.dayOffset, 3_650));
|
|
56
|
+
const score = safeNumber(item?.score, 100);
|
|
57
|
+
const observations = Math.round(safeNumber(item?.observations, 1_000_000));
|
|
58
|
+
if (dayOffset <= previousOffset || observations < 1) return [];
|
|
59
|
+
previousOffset = dayOffset;
|
|
60
|
+
return [{ dayOffset, score: Number(score.toFixed(1)), observations }];
|
|
61
|
+
});
|
|
62
|
+
if (points.length < 2) return null;
|
|
63
|
+
const observations = Math.round(safeNumber(value.observations, 10_000_000));
|
|
64
|
+
const autonomousObservations = Math.round(safeNumber(value.autonomousObservations, observations));
|
|
65
|
+
if (!observations || autonomousObservations > observations) return null;
|
|
66
|
+
return {
|
|
67
|
+
points,
|
|
68
|
+
startScore: points[0].score,
|
|
69
|
+
endScore: points.at(-1).score,
|
|
70
|
+
change: Number((points.at(-1).score - points[0].score).toFixed(1)),
|
|
71
|
+
observations,
|
|
72
|
+
autonomousObservations,
|
|
73
|
+
autonomousPercentage: Number((autonomousObservations / observations * 100).toFixed(1)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
39
77
|
export function sanitizePublicReport(value) {
|
|
40
78
|
if (!value || typeof value !== "object" || Array.isArray(value) || !/^[A-Za-z0-9_-]{8,32}$/.test(value.id || "")) return null;
|
|
41
79
|
const stats = value.stats;
|
|
@@ -52,6 +90,12 @@ export function sanitizePublicReport(value) {
|
|
|
52
90
|
const safeTokenBreakdown = tokenBreakdown && Object.values(tokenBreakdown).reduce((sum, count) => sum + count, 0) === safeTokens ? tokenBreakdown : null;
|
|
53
91
|
const safeStockPhraseCounts = safeStockPhrases(stats.stockPhrases);
|
|
54
92
|
const safeSessionTurnCounts = safeTurnCounts(stats.sessionTurnCounts);
|
|
93
|
+
const runAgent = allowedAgents.has(stats.longestUninterruptedRun?.agent) ? stats.longestUninterruptedRun.agent : null;
|
|
94
|
+
const safeLongestUninterruptedRun = runAgent && safeNumber(stats.longestUninterruptedRun?.durationMs, 7 * 24 * 60 * 60 * 1000) > 0 ? {
|
|
95
|
+
durationMs: Math.round(safeNumber(stats.longestUninterruptedRun.durationMs, 7 * 24 * 60 * 60 * 1000)),
|
|
96
|
+
agent: runAgent,
|
|
97
|
+
agentName: safeText(stats.longestUninterruptedRun?.agentName, 30),
|
|
98
|
+
} : null;
|
|
55
99
|
const phrase = value.phraseCard?.phrase;
|
|
56
100
|
const safePhrase = typeof phrase === "string" && /^[a-z]+(?:'[a-z]+)?(?: [a-z]+(?:'[a-z]+)?){3,9}$/.test(phrase) ? {
|
|
57
101
|
phrase,
|
|
@@ -90,16 +134,29 @@ export function sanitizePublicReport(value) {
|
|
|
90
134
|
sessions: Math.round(safeNumber(stats.sessions, 1_000_000)), activeDays: Math.round(safeNumber(stats.activeDays, 1_000_000)),
|
|
91
135
|
durationMinutes: Math.round(safeNumber(stats.durationMinutes)), prompts: Math.round(safeNumber(stats.prompts)), toolCalls: Math.round(safeNumber(stats.toolCalls)),
|
|
92
136
|
interruptions: Math.round(safeNumber(stats.interruptions)), tokens: safeTokens, ...(safeTokenBreakdown ? { tokenBreakdown: safeTokenBreakdown } : {}), agentWords: Math.round(safeNumber(stats.agentWords)),
|
|
137
|
+
interruptionsByModel: Array.isArray(stats.interruptionsByModel) ? stats.interruptionsByModel.slice(0, 10).flatMap((item) => {
|
|
138
|
+
const model = safeText(item?.model, 80);
|
|
139
|
+
const name = safeText(item?.name, 80);
|
|
140
|
+
const count = Math.round(safeNumber(item?.count, 1_000_000));
|
|
141
|
+
return model && name && count > 0 ? [{ model, name, count }] : [];
|
|
142
|
+
}) : [],
|
|
93
143
|
userWords: Math.round(safeNumber(stats.userWords)), agentUserWordRatio: safeNumber(stats.agentUserWordRatio, 10_000),
|
|
94
144
|
averageAgentResponseWords: Math.round(safeNumber(stats.averageAgentResponseWords)), averageUserInputWords: Math.round(safeNumber(stats.averageUserInputWords)),
|
|
95
145
|
longestSessionTurns: Math.max(0, ...safeSessionTurnCounts),
|
|
96
146
|
sessionTurnCounts: safeSessionTurnCounts,
|
|
147
|
+
longestUninterruptedRun: safeLongestUninterruptedRun,
|
|
148
|
+
trustCurve: safeTrustCurve(stats.trustCurve),
|
|
97
149
|
interactionTone: {
|
|
98
150
|
frustratedMessages: Math.round(safeNumber(stats.interactionTone?.frustratedMessages, 1_000_000)),
|
|
99
151
|
gratefulMessages: Math.round(safeNumber(stats.interactionTone?.gratefulMessages, 1_000_000)),
|
|
100
152
|
analyzedMessages: Math.round(safeNumber(stats.interactionTone?.analyzedMessages, 1_000_000)),
|
|
101
153
|
},
|
|
154
|
+
apologyCounts: {
|
|
155
|
+
user: Math.round(safeNumber(stats.apologyCounts?.user, 1_000_000)),
|
|
156
|
+
agent: Math.round(safeNumber(stats.apologyCounts?.agent, 1_000_000)),
|
|
157
|
+
},
|
|
102
158
|
...(safeStockPhraseCounts ? { stockPhrases: safeStockPhraseCounts } : {}),
|
|
159
|
+
repeatedInstructions: safeRepeatedInstructions(stats.repeatedInstructions),
|
|
103
160
|
outputLanguages: safeBreakdown(stats.outputLanguages, "language", "words", allowedLanguages),
|
|
104
161
|
languageAnomaly: safeLanguageAnomaly,
|
|
105
162
|
topics: safeBreakdown(stats.topics, "topic", "tokens", new Set(["Coding", "Writing", "Personal advice", "Research & search", "Planning", "Data & analysis", "Other"])),
|