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/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");
@@ -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?.type === "user" && !record?.isMeta);
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?.type !== "user" || record?.isMeta || !text) return null;
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: "user", text, timestamp: record.timestamp || null, highlighted: true }, after].filter(Boolean),
53
+ messages: [before, { role: expectedRole, text, timestamp: record.timestamp || null, highlighted: true }, after].filter(Boolean),
54
54
  };
55
55
  }
56
56
 
57
- function buildKind(report, kind, recordsById, metadataById) {
58
- return (report?.interactionReview?.[kind] || []).slice(0, MAX_OCCURRENCES_PER_KIND).flatMap((reference, index) => {
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-v1",
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
  }
@@ -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 escapedPort = String(port).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46
- return new RegExp(`(?:/(?:agent-)?behavior-wrapped/|(?:^|\\s))server/launcher\\.mjs(?:\\s|$)`, "i").test(command || "")
47
- && new RegExp(`(?:^|\\s)--port=${escapedPort}(?:\\s|$)`).test(command || "");
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"])),