editmamei 1.4.0 → 1.5.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.
@@ -0,0 +1,149 @@
1
+ export const READ_ONLY_TOOLS = new Set([
2
+ 'ps_sequence',
3
+ 'ps_batch',
4
+ 'ps_ping',
5
+ 'ps_overview',
6
+ 'ps_list_capabilities',
7
+ 'ps_get_preview',
8
+ 'ps_get_selection_preview',
9
+ 'ps_get_histogram',
10
+ 'ps_inspect',
11
+ 'ps_read_scene',
12
+ 'ps_compare_regions',
13
+ 'ps_get_layer_bounds_diff',
14
+ 'ps_template_list',
15
+ 'ps_template_recall',
16
+ 'ps_list_actions',
17
+ 'ps_report_problem',
18
+ 'ps_detect',
19
+ 'ps_detect_landmarks',
20
+ 'ps_document',
21
+ 'ps_template_verify',
22
+ 'ps_resolve_placement',
23
+ 'ps_template_save',
24
+ 'ps_template_delete',
25
+ 'ps_template_create_evidence',
26
+ ]);
27
+ export const KEPT_WORK_TOOLS = new Set(['ps_export', 'ps_save_psd']);
28
+ export const MUTATING_TOOLS = new Set([
29
+ 'ps_play_action',
30
+ 'ps_execute_script',
31
+ 'ps_add_adjustment_layer',
32
+ 'ps_apply_adjustment',
33
+ 'ps_create_document',
34
+ 'ps_close_document',
35
+ 'ps_open_document',
36
+ 'ps_develop_raw',
37
+ 'ps_filter',
38
+ 'ps_group',
39
+ 'ps_clipping_mask',
40
+ 'ps_undo',
41
+ 'ps_redo',
42
+ 'ps_place_image',
43
+ 'ps_resize_image',
44
+ 'ps_crop_document',
45
+ 'ps_convert_image_mode',
46
+ 'ps_move_layer_to_position',
47
+ 'ps_convert_to_smart_object',
48
+ 'ps_rasterize_layer',
49
+ 'ps_set_layer',
50
+ 'ps_duplicate_layer',
51
+ 'ps_copy_to_new_layer',
52
+ 'ps_merge',
53
+ 'ps_bake_layer',
54
+ 'ps_add_layer_style',
55
+ 'ps_create_layer',
56
+ 'ps_delete_layer',
57
+ 'ps_fill_layer',
58
+ 'ps_add_fill_layer',
59
+ 'ps_select_layer',
60
+ 'ps_transform_layer',
61
+ 'ps_warp_layer',
62
+ 'ps_warp_layer_mesh',
63
+ 'ps_warp_layer_along',
64
+ 'ps_warp_layer_region',
65
+ 'ps_warp_layer_to',
66
+ 'ps_apply_camera_raw',
67
+ 'ps_transform_canvas',
68
+ 'ps_guides',
69
+ 'ps_retouch',
70
+ 'ps_apply_brush_stroke',
71
+ 'ps_edit_object',
72
+ 'ps_portrait_touchup',
73
+ 'ps_add_text_to_object',
74
+ 'ps_select_face_feature',
75
+ 'ps_stroke_face_contour',
76
+ 'ps_select_by_reference',
77
+ 'ps_select',
78
+ 'ps_select_subject',
79
+ 'ps_select_sky',
80
+ 'ps_select_subject_instance',
81
+ 'ps_select_object',
82
+ 'ps_replace_sky',
83
+ 'ps_modify_selection',
84
+ 'ps_selection_channel',
85
+ 'ps_layer_mask',
86
+ 'ps_path',
87
+ 'ps_vector_mask',
88
+ 'ps_apply_image',
89
+ 'ps_calculations',
90
+ 'ps_shape',
91
+ 'ps_template_apply',
92
+ 'ps_text',
93
+ ]);
94
+ export function mapClientName(name) {
95
+ if (name === undefined)
96
+ return 'other';
97
+ const n = name.toLowerCase();
98
+ if (n.includes('claude-code') || n.includes('claude_code') || n.includes('claudecode')) {
99
+ return 'claude_code';
100
+ }
101
+ if (n.includes('claude'))
102
+ return 'claude_desktop';
103
+ if (n.includes('cursor'))
104
+ return 'cursor';
105
+ if (n.includes('windsurf'))
106
+ return 'windsurf';
107
+ if (n.includes('vscode') || n.includes('visual studio') || n.includes('code-oss')) {
108
+ return 'vscode';
109
+ }
110
+ return 'other';
111
+ }
112
+ export function parseMajor(version) {
113
+ if (version === undefined)
114
+ return null;
115
+ const m = /^(\d+)/.exec(version.trim());
116
+ return m ? Number(m[1]) : null;
117
+ }
118
+ export function boundMajor(n, max) {
119
+ return n !== null && Number.isFinite(n) && n >= 0 && n <= max ? n : null;
120
+ }
121
+ export function nodeMajor() {
122
+ return parseMajor(process.versions.node);
123
+ }
124
+ export function archToken() {
125
+ if (process.arch === 'x64')
126
+ return 'x64';
127
+ if (process.arch === 'arm64')
128
+ return 'arm64';
129
+ return 'other';
130
+ }
131
+ export function osMajor(platform, release) {
132
+ if (platform === 'win32') {
133
+ const parts = release.split('.');
134
+ const major = Number(parts[0]);
135
+ if (!Number.isFinite(major))
136
+ return null;
137
+ const build = Number(parts[2]);
138
+ return Number.isFinite(build) && build >= 22000 ? 11 : major;
139
+ }
140
+ if (platform === 'darwin') {
141
+ const m = /^(\d+)/.exec(release);
142
+ if (!m)
143
+ return null;
144
+ const darwinMajor = Number(m[1]);
145
+ return darwinMajor <= 24 ? darwinMajor - 9 : darwinMajor + 1;
146
+ }
147
+ const m = /^(\d+)/.exec(release);
148
+ return m ? Number(m[1]) : null;
149
+ }
@@ -1,15 +1,19 @@
1
+ import { release } from 'node:os';
1
2
  import { Logger } from '../utils/logger.js';
2
3
  import { EDITION } from '../edition.js';
3
4
  import { VERSION } from '../version.js';
4
5
  import { resolveInstallChannel } from '../install-channel.js';
5
- import { buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, normalizeDayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
6
+ import { READ_ONLY_TOOLS, KEPT_WORK_TOOLS, nodeMajor, archToken, osMajor, boundMajor, } from './activity.js';
7
+ import { buildClientConnected, buildDiagnosticEvent, buildModuleStatus, buildSessionStart, buildSessionSummary, buildUsageEvent, dayBucket, normalizeDayBucket, isContentSafe, PS_VERSION_UNKNOWN, } from './events.js';
6
8
  import { sanitizeMessage, sanitizeSnippet, sanitizeStderrTail } from './sanitize.js';
7
9
  import { httpTransport, resolveEndpoint } from './transport.js';
8
- import { appendOutboxSync, clearOutbox, clearSessionState, readOutbox, readSessionState, writeSessionStateSync, } from './outbox.js';
10
+ import { appendOutboxSync, clearOutbox, clearSessionState, readOutboxWithDiscards, readSessionState, rewriteOutbox, writeSessionStateSync, } from './outbox.js';
9
11
  const MAX_BATCH_SIZE = 100;
10
12
  const MAX_QUEUE_SIZE = 500;
11
13
  const DEFAULT_FLUSH_INTERVAL_MS = 5 * 60_000;
12
14
  const SESSION_PERSIST_THROTTLE_MS = 10_000;
15
+ const MAX_SESSION_DURATION_S = 604_800;
16
+ const MAX_INSTALL_ASSET_COUNT = 100_000;
13
17
  function isTestEnv() {
14
18
  return process.env.VITEST !== undefined || process.env.NODE_ENV === 'test';
15
19
  }
@@ -25,12 +29,26 @@ export class TelemetryClient {
25
29
  active;
26
30
  outboxOpts;
27
31
  getModuleStatus;
32
+ resolvedModuleStatus = null;
28
33
  queue = [];
29
34
  timer = null;
30
35
  shutdownPromise = null;
31
36
  toolCallCount = 0;
32
37
  distinctTools = new Set();
33
38
  anyFailures = false;
39
+ firstCallAtMs = null;
40
+ lastCallAtMs = null;
41
+ retryCount = 0;
42
+ lastCallSuccess = null;
43
+ editsOk = 0;
44
+ keptWork = 0;
45
+ droppedEvents = 0;
46
+ droppedOutbox = 0;
47
+ droppedUnsafe = 0;
48
+ usageCallsSent = 0;
49
+ behindLatest = null;
50
+ moduleUpdate = 'none';
51
+ installAssets = {};
34
52
  lastSessionPersistMs = 0;
35
53
  startDayBucket = null;
36
54
  constructor(opts) {
@@ -61,24 +79,98 @@ export class TelemetryClient {
61
79
  this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
62
80
  this.timer.unref?.();
63
81
  if (this.settings.telemetry.usage) {
64
- this.enqueue(buildSessionStart(this.dims, this.now()));
65
- const moduleStatus = this.getModuleStatus();
82
+ const hostNodeMajor = boundMajor(nodeMajor(), 999);
83
+ const hostOsMajor = boundMajor(osMajor(process.platform, release()), 999);
84
+ this.enqueue(buildSessionStart(this.dims, this.now(), {
85
+ ...(hostNodeMajor !== null ? { node_major: hostNodeMajor } : {}),
86
+ arch: archToken(),
87
+ ...(hostOsMajor !== null ? { os_major: hostOsMajor } : {}),
88
+ }));
89
+ const moduleStatus = this.moduleStatus();
66
90
  if (moduleStatus)
67
91
  this.enqueue(buildModuleStatus(this.dims, moduleStatus, this.now()));
68
92
  void this.flush();
69
93
  }
70
94
  }
95
+ recordClientConnected(info) {
96
+ if (!this.active || !this.settings.telemetry.usage)
97
+ return;
98
+ this.enqueue(buildClientConnected(this.dims, info, this.now()));
99
+ }
71
100
  recordCall(call) {
72
101
  if (!this.active || !this.settings.telemetry.usage)
73
102
  return;
74
103
  this.ensureStartDayBucket();
104
+ const nowDate = this.now();
105
+ const nowMs = nowDate.getTime();
75
106
  this.toolCallCount += 1;
76
107
  this.distinctTools.add(call.tool);
77
108
  if (!call.success)
78
109
  this.anyFailures = true;
79
- this.enqueue(buildUsageEvent(this.dims, call, this.now()));
110
+ this.lastCallSuccess = call.success;
111
+ if (call.retry)
112
+ this.retryCount += 1;
113
+ if (call.success) {
114
+ if (!READ_ONLY_TOOLS.has(call.tool))
115
+ this.editsOk += 1;
116
+ if (KEPT_WORK_TOOLS.has(call.tool))
117
+ this.keptWork += 1;
118
+ }
119
+ if (this.firstCallAtMs === null)
120
+ this.firstCallAtMs = nowMs;
121
+ this.lastCallAtMs = nowMs;
122
+ this.enqueue(buildUsageEvent(this.dims, call, nowDate));
80
123
  this.persistSessionStateThrottled();
81
124
  }
125
+ setBehindLatest(value) {
126
+ this.behindLatest = value;
127
+ }
128
+ setModuleUpdate(outcome) {
129
+ this.moduleUpdate = outcome;
130
+ }
131
+ setInstallAssets(assets) {
132
+ const templatesSaved = assets.templates_saved !== undefined ? clampCount(assets.templates_saved) : null;
133
+ const actionSets = assets.action_sets !== undefined ? clampCount(assets.action_sets) : null;
134
+ this.installAssets = {
135
+ ...this.installAssets,
136
+ ...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
137
+ ...(actionSets !== null ? { action_sets: actionSets } : {}),
138
+ };
139
+ }
140
+ moduleStatus() {
141
+ if (this.resolvedModuleStatus === null) {
142
+ this.resolvedModuleStatus = { value: this.getModuleStatus() };
143
+ }
144
+ return this.resolvedModuleStatus.value;
145
+ }
146
+ durationS() {
147
+ if (this.firstCallAtMs === null || this.lastCallAtMs === null)
148
+ return undefined;
149
+ return Math.min(Math.max(0, Math.floor((this.lastCallAtMs - this.firstCallAtMs) / 1000)), MAX_SESSION_DURATION_S);
150
+ }
151
+ summaryFields() {
152
+ const duration = this.durationS();
153
+ const moduleStatus = this.moduleStatus();
154
+ return {
155
+ ...(duration !== undefined ? { duration_s: duration } : {}),
156
+ retry_count: this.retryCount,
157
+ ...(this.lastCallSuccess !== null ? { ended_after_failure: !this.lastCallSuccess } : {}),
158
+ edits_ok: this.editsOk,
159
+ kept_work: this.keptWork,
160
+ ...(this.behindLatest !== null ? { behind_latest: this.behindLatest } : {}),
161
+ dropped_events: this.droppedEvents,
162
+ dropped_outbox: this.droppedOutbox,
163
+ dropped_unsafe: this.droppedUnsafe,
164
+ usage_calls_sent: this.usageCallsSent,
165
+ ...(moduleStatus !== null ? { module_update: this.moduleUpdate } : {}),
166
+ ...(this.installAssets.templates_saved !== undefined
167
+ ? { templates_saved: this.installAssets.templates_saved }
168
+ : {}),
169
+ ...(this.installAssets.action_sets !== undefined
170
+ ? { action_sets: this.installAssets.action_sets }
171
+ : {}),
172
+ };
173
+ }
82
174
  psVersion() {
83
175
  const v = this.dims.getPsVersion();
84
176
  return v && v.length > 0 ? v : PS_VERSION_UNKNOWN;
@@ -105,6 +197,7 @@ export class TelemetryClient {
105
197
  tool_call_count: this.toolCallCount,
106
198
  distinct_tools: this.distinctTools.size,
107
199
  any_failures: this.anyFailures,
200
+ ...this.summaryFields(),
108
201
  };
109
202
  writeSessionStateSync(state, this.outboxOpts);
110
203
  }
@@ -123,6 +216,9 @@ export class TelemetryClient {
123
216
  error_message: sanitizeMessage(diag.error_message),
124
217
  ...(diag.snippet ? { snippet: sanitizeSnippet(diag.snippet) } : {}),
125
218
  ...(diag.stderr_tail ? { stderr_tail: sanitizeStderrTail(diag.stderr_tail) } : {}),
219
+ ...(diag.doc_depth !== undefined ? { doc_depth: diag.doc_depth } : {}),
220
+ ...(diag.doc_mode !== undefined ? { doc_mode: diag.doc_mode } : {}),
221
+ ...(diag.ps_locale !== undefined ? { ps_locale: diag.ps_locale } : {}),
126
222
  }, this.now()));
127
223
  }
128
224
  restampPsVersion(events) {
@@ -141,7 +237,9 @@ export class TelemetryClient {
141
237
  enqueue(event) {
142
238
  this.queue.push(event);
143
239
  if (this.queue.length > MAX_QUEUE_SIZE) {
144
- this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
240
+ const excess = this.queue.length - MAX_QUEUE_SIZE;
241
+ this.queue.splice(0, excess);
242
+ this.droppedEvents += excess;
145
243
  }
146
244
  if (this.queue.length >= this.maxBatchSize)
147
245
  void this.flush();
@@ -149,15 +247,18 @@ export class TelemetryClient {
149
247
  async flush() {
150
248
  if (!this.active || this.queue.length === 0)
151
249
  return;
152
- const batch = this.restampPsVersion(this.queue.splice(0, this.maxBatchSize)).filter(isContentSafe);
250
+ const picked = this.restampPsVersion(this.queue.splice(0, this.maxBatchSize));
251
+ const batch = picked.filter(isContentSafe);
252
+ this.droppedUnsafe += picked.length - batch.length;
153
253
  if (batch.length === 0)
154
254
  return;
155
255
  try {
156
256
  await this.transport(this.endpoint, JSON.stringify({ events: batch }));
257
+ this.usageCallsSent += countUsageCalls(batch);
157
258
  }
158
259
  catch (err) {
159
260
  this.logger.debug(`telemetry flush failed, persisting ${batch.length} event(s) to outbox: ${errMsg(err)}`);
160
- appendOutboxSync(batch, this.outboxOpts);
261
+ this.droppedOutbox += appendOutboxSync(batch, this.outboxOpts);
161
262
  }
162
263
  }
163
264
  shutdown() {
@@ -172,15 +273,26 @@ export class TelemetryClient {
172
273
  }
173
274
  if (!this.active)
174
275
  return;
175
- if (this.settings.telemetry.usage && this.toolCallCount > 0) {
176
- this.enqueue(buildSessionSummary(this.dims, {
276
+ if (this.queue.length > 0) {
277
+ const picked = this.restampPsVersion(this.queue.splice(0));
278
+ const batch = picked.filter(isContentSafe);
279
+ this.droppedUnsafe += picked.length - batch.length;
280
+ this.droppedOutbox += appendOutboxSync(batch, this.outboxOpts);
281
+ }
282
+ const hasLossToReport = this.droppedEvents > 0 || this.droppedOutbox > 0 || this.droppedUnsafe > 0;
283
+ if (this.settings.telemetry.usage && (this.toolCallCount > 0 || hasLossToReport)) {
284
+ const summary = buildSessionSummary(this.dims, {
177
285
  tool_call_count: this.toolCallCount,
178
286
  distinct_tools: this.distinctTools.size,
179
287
  any_failures: this.anyFailures,
180
- }, this.ensureStartDayBucket(), this.now()));
181
- }
182
- if (this.queue.length > 0) {
183
- appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
288
+ ...this.summaryFields(),
289
+ }, this.ensureStartDayBucket(), this.now());
290
+ if (isContentSafe(summary)) {
291
+ const lost = appendOutboxSync([summary], this.outboxOpts);
292
+ if (lost > 0) {
293
+ this.logger.debug(`outbox discarded ${lost} event(s) while writing the session summary`);
294
+ }
295
+ }
184
296
  }
185
297
  clearSessionState(this.outboxOpts);
186
298
  }
@@ -193,7 +305,8 @@ export class TelemetryClient {
193
305
  appendOutboxSync([summaryFromState(stale)], this.outboxOpts);
194
306
  }
195
307
  clearSessionState(this.outboxOpts);
196
- const pending = readOutbox(this.outboxOpts);
308
+ const { events: pending, discarded } = readOutboxWithDiscards(this.outboxOpts);
309
+ this.droppedOutbox += discarded;
197
310
  if (pending.length === 0) {
198
311
  clearOutbox(this.outboxOpts);
199
312
  return;
@@ -202,22 +315,27 @@ export class TelemetryClient {
202
315
  clearOutbox(this.outboxOpts);
203
316
  return;
204
317
  }
205
- let allAccepted = true;
206
- for (let i = 0; i < pending.length; i += this.maxBatchSize) {
207
- const batch = pending.slice(i, i + this.maxBatchSize).filter(isContentSafe);
208
- if (batch.length === 0)
209
- continue;
318
+ const sendable = pending.filter(isContentSafe);
319
+ this.droppedUnsafe += pending.length - sendable.length;
320
+ let settled = 0;
321
+ for (let i = 0; i < sendable.length; i += this.maxBatchSize) {
322
+ const batch = sendable.slice(i, i + this.maxBatchSize);
210
323
  try {
211
324
  await this.transport(this.endpoint, JSON.stringify({ events: batch }));
325
+ this.usageCallsSent += countUsageCalls(batch);
212
326
  }
213
327
  catch (err) {
214
328
  this.logger.debug(`startup outbox flush failed: ${errMsg(err)}`);
215
- allAccepted = false;
216
329
  break;
217
330
  }
331
+ settled = i + batch.length;
218
332
  }
219
- if (allAccepted)
333
+ if (settled >= sendable.length) {
220
334
  clearOutbox(this.outboxOpts);
335
+ }
336
+ else {
337
+ rewriteOutbox(sendable.slice(settled), this.outboxOpts);
338
+ }
221
339
  }
222
340
  catch (err) {
223
341
  this.logger.debug(`startup outbox flush error: ${errMsg(err)}`);
@@ -228,6 +346,8 @@ export class TelemetryClient {
228
346
  }
229
347
  }
230
348
  function summaryFromState(s) {
349
+ const templatesSaved = s.templates_saved !== undefined ? clampCount(s.templates_saved) : null;
350
+ const actionSets = s.action_sets !== undefined ? clampCount(s.action_sets) : null;
231
351
  return {
232
352
  v: 2,
233
353
  type: 'session_summary',
@@ -240,8 +360,36 @@ function summaryFromState(s) {
240
360
  tool_call_count: s.tool_call_count,
241
361
  distinct_tools: s.distinct_tools,
242
362
  any_failures: s.any_failures,
363
+ ...(s.duration_s !== undefined ? { duration_s: s.duration_s } : {}),
364
+ ...(s.retry_count !== undefined ? { retry_count: s.retry_count } : {}),
365
+ ...(s.ended_after_failure !== undefined ? { ended_after_failure: s.ended_after_failure } : {}),
366
+ ...(s.edits_ok !== undefined ? { edits_ok: s.edits_ok } : {}),
367
+ ...(s.kept_work !== undefined ? { kept_work: s.kept_work } : {}),
368
+ ...(s.behind_latest !== undefined ? { behind_latest: s.behind_latest } : {}),
369
+ ...(s.dropped_events !== undefined ? { dropped_events: s.dropped_events } : {}),
370
+ ...(s.dropped_outbox !== undefined ? { dropped_outbox: s.dropped_outbox } : {}),
371
+ ...(s.dropped_unsafe !== undefined ? { dropped_unsafe: s.dropped_unsafe } : {}),
372
+ ...(s.usage_calls_sent !== undefined ? { usage_calls_sent: s.usage_calls_sent } : {}),
373
+ ...(s.module_update !== undefined ? { module_update: s.module_update } : {}),
374
+ ...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
375
+ ...(actionSets !== null ? { action_sets: actionSets } : {}),
243
376
  };
244
377
  }
245
378
  function errMsg(err) {
246
379
  return err instanceof Error ? err.message : String(err);
247
380
  }
381
+ function countUsageCalls(events) {
382
+ let n = 0;
383
+ for (const event of events) {
384
+ if (event.type !== 'usage')
385
+ continue;
386
+ const count = event.count;
387
+ n += typeof count === 'number' && Number.isInteger(count) && count > 0 ? count : 1;
388
+ }
389
+ return n;
390
+ }
391
+ function clampCount(n) {
392
+ if (!Number.isFinite(n))
393
+ return null;
394
+ return Math.min(Math.max(0, Math.trunc(n)), MAX_INSTALL_ASSET_COUNT);
395
+ }
@@ -1,5 +1,8 @@
1
+ import { mapClientName, parseMajor, boundMajor } from './activity.js';
1
2
  export const TELEMETRY_SCHEMA_VERSION = 2;
2
3
  export const PS_VERSION_UNKNOWN = 'unknown';
4
+ export const MAX_RESULT_BYTES = 16_777_216;
5
+ const PS_LOCALE_RE = /^[a-z]{2,3}_[A-Z]{2}$/;
3
6
  export function dayBucket(now) {
4
7
  return now.toISOString().slice(0, 10);
5
8
  }
@@ -31,6 +34,9 @@ export function buildUsageEvent(dims, call, now) {
31
34
  success: call.success,
32
35
  error_class: call.error_class === null ? null : normalizeErrorClass(call.error_class),
33
36
  duration_ms: call.duration_ms,
37
+ ...(call.result_bytes !== undefined
38
+ ? { result_bytes: Math.min(call.result_bytes, MAX_RESULT_BYTES) }
39
+ : {}),
34
40
  };
35
41
  }
36
42
  export function buildSessionSummary(dims, summary, tsBucket, now) {
@@ -46,9 +52,26 @@ export function buildSessionSummary(dims, summary, tsBucket, now) {
46
52
  tool_call_count: summary.tool_call_count,
47
53
  distinct_tools: summary.distinct_tools,
48
54
  any_failures: summary.any_failures,
55
+ ...(summary.duration_s !== undefined ? { duration_s: summary.duration_s } : {}),
56
+ ...(summary.retry_count !== undefined ? { retry_count: summary.retry_count } : {}),
57
+ ...(summary.ended_after_failure !== undefined
58
+ ? { ended_after_failure: summary.ended_after_failure }
59
+ : {}),
60
+ ...(summary.edits_ok !== undefined ? { edits_ok: summary.edits_ok } : {}),
61
+ ...(summary.kept_work !== undefined ? { kept_work: summary.kept_work } : {}),
62
+ ...(summary.behind_latest !== undefined ? { behind_latest: summary.behind_latest } : {}),
63
+ ...(summary.dropped_events !== undefined ? { dropped_events: summary.dropped_events } : {}),
64
+ ...(summary.dropped_outbox !== undefined ? { dropped_outbox: summary.dropped_outbox } : {}),
65
+ ...(summary.dropped_unsafe !== undefined ? { dropped_unsafe: summary.dropped_unsafe } : {}),
66
+ ...(summary.usage_calls_sent !== undefined
67
+ ? { usage_calls_sent: summary.usage_calls_sent }
68
+ : {}),
69
+ ...(summary.module_update !== undefined ? { module_update: summary.module_update } : {}),
70
+ ...(summary.templates_saved !== undefined ? { templates_saved: summary.templates_saved } : {}),
71
+ ...(summary.action_sets !== undefined ? { action_sets: summary.action_sets } : {}),
49
72
  };
50
73
  }
51
- export function buildSessionStart(dims, now) {
74
+ export function buildSessionStart(dims, now, facts = {}) {
52
75
  return {
53
76
  v: TELEMETRY_SCHEMA_VERSION,
54
77
  type: 'session_start',
@@ -59,6 +82,25 @@ export function buildSessionStart(dims, now) {
59
82
  platform: dims.platform,
60
83
  ps_version: psVersionOf(dims),
61
84
  channel: dims.channel,
85
+ ...(facts.node_major !== undefined ? { node_major: facts.node_major } : {}),
86
+ ...(facts.arch !== undefined ? { arch: facts.arch } : {}),
87
+ ...(facts.os_major !== undefined ? { os_major: facts.os_major } : {}),
88
+ };
89
+ }
90
+ export function buildClientConnected(dims, info, now) {
91
+ return {
92
+ v: TELEMETRY_SCHEMA_VERSION,
93
+ type: 'client_connected',
94
+ install_id: dims.install_id,
95
+ ts_bucket: dayBucket(now),
96
+ editmamei_version: dims.editmamei_version,
97
+ edition: dims.edition,
98
+ platform: dims.platform,
99
+ client: mapClientName(info.clientName),
100
+ client_major: boundMajor(parseMajor(info.clientVersion), 9999),
101
+ cap_sampling: info.capSampling,
102
+ cap_elicitation: info.capElicitation,
103
+ cap_roots: info.capRoots,
62
104
  };
63
105
  }
64
106
  export function buildModuleStatus(dims, status, now) {
@@ -90,6 +132,11 @@ export function buildDiagnosticEvent(dims, diag, now) {
90
132
  error_message: diag.error_message,
91
133
  ...(diag.snippet ? { snippet: diag.snippet } : {}),
92
134
  ...(diag.stderr_tail ? { stderr_tail: diag.stderr_tail } : {}),
135
+ ...(diag.doc_depth !== undefined ? { doc_depth: diag.doc_depth } : {}),
136
+ ...(diag.doc_mode !== undefined ? { doc_mode: diag.doc_mode } : {}),
137
+ ...(diag.ps_locale !== undefined && PS_LOCALE_RE.test(diag.ps_locale)
138
+ ? { ps_locale: diag.ps_locale }
139
+ : {}),
93
140
  };
94
141
  }
95
142
  export function looksLikeAbsolutePath(value) {
@@ -6,8 +6,8 @@ const logger = new Logger('TelemetryOutbox');
6
6
  const DIRNAME = '.editmamei';
7
7
  const OUTBOX_FILENAME = 'telemetry-outbox.ndjson';
8
8
  const SESSION_STATE_FILENAME = 'telemetry-session.json';
9
- export const MAX_OUTBOX_EVENTS = 1000;
10
- const MAX_OUTBOX_BYTES = 2_000_000;
9
+ export const MAX_OUTBOX_EVENTS = 5_000;
10
+ export const MAX_OUTBOX_BYTES = 2_000_000;
11
11
  function baseDir(opts) {
12
12
  return opts.dir ?? join(homedir(), DIRNAME);
13
13
  }
@@ -22,27 +22,31 @@ function ensureDir(path) {
22
22
  }
23
23
  export function appendOutboxSync(events, opts = {}) {
24
24
  if (events.length === 0)
25
- return;
25
+ return 0;
26
26
  const path = outboxPath(opts);
27
+ let discarded = 0;
27
28
  try {
28
29
  ensureDir(path);
29
30
  if (existsSync(path) && statSync(path).size > MAX_OUTBOX_BYTES) {
30
- compactOutbox(opts);
31
+ discarded += compactOutbox(opts);
31
32
  }
32
33
  const lines = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
33
34
  appendFileSync(path, lines, { encoding: 'utf8', mode: 0o600 });
34
35
  }
35
36
  catch (err) {
36
37
  logger.debug(`outbox append dropped ${events.length} event(s): ${errMsg(err)}`);
38
+ discarded += events.length;
37
39
  }
40
+ return discarded;
38
41
  }
39
- export function readOutbox(opts = {}) {
42
+ export function readOutboxWithDiscards(opts = {}) {
40
43
  const path = outboxPath(opts);
41
44
  if (!existsSync(path))
42
- return [];
45
+ return { events: [], discarded: 0 };
43
46
  try {
44
47
  const raw = readFileSync(path, 'utf8');
45
48
  const events = [];
49
+ let discarded = 0;
46
50
  for (const line of raw.split('\n')) {
47
51
  const trimmed = line.trim();
48
52
  if (trimmed.length === 0)
@@ -51,17 +55,23 @@ export function readOutbox(opts = {}) {
51
55
  events.push(JSON.parse(trimmed));
52
56
  }
53
57
  catch {
58
+ discarded += 1;
54
59
  }
55
60
  }
56
- return events.length > MAX_OUTBOX_EVENTS
57
- ? events.slice(events.length - MAX_OUTBOX_EVENTS)
58
- : events;
61
+ if (events.length > MAX_OUTBOX_EVENTS) {
62
+ const over = events.length - MAX_OUTBOX_EVENTS;
63
+ return { events: events.slice(over), discarded: discarded + over };
64
+ }
65
+ return { events, discarded };
59
66
  }
60
67
  catch (err) {
61
68
  logger.debug(`outbox read failed: ${errMsg(err)}`);
62
- return [];
69
+ return { events: [], discarded: 0 };
63
70
  }
64
71
  }
72
+ export function readOutbox(opts = {}) {
73
+ return readOutboxWithDiscards(opts).events;
74
+ }
65
75
  export function clearOutbox(opts = {}) {
66
76
  try {
67
77
  rmSync(outboxPath(opts), { force: true });
@@ -70,24 +80,39 @@ export function clearOutbox(opts = {}) {
70
80
  logger.debug(`outbox clear failed: ${errMsg(err)}`);
71
81
  }
72
82
  }
73
- function compactOutbox(opts = {}) {
74
- const kept = readOutbox(opts);
83
+ export function rewriteOutbox(events, opts = {}) {
84
+ if (events.length === 0) {
85
+ clearOutbox(opts);
86
+ return;
87
+ }
75
88
  const path = outboxPath(opts);
76
89
  try {
77
- if (kept.length === 0) {
78
- clearOutbox(opts);
79
- return;
80
- }
90
+ ensureDir(path);
81
91
  const tmp = join(dirname(path), `.outbox.${process.pid}.tmp`);
82
- writeFileSync(tmp, kept.map((e) => JSON.stringify(e)).join('\n') + '\n', {
92
+ writeFileSync(tmp, events.map((e) => JSON.stringify(e)).join('\n') + '\n', {
83
93
  encoding: 'utf8',
84
94
  mode: 0o600,
85
95
  });
86
96
  renameSync(tmp, path);
87
97
  }
88
98
  catch (err) {
89
- logger.debug(`outbox compaction failed: ${errMsg(err)}`);
99
+ logger.debug(`outbox rewrite failed: ${errMsg(err)}`);
100
+ }
101
+ }
102
+ function compactOutbox(opts = {}) {
103
+ const { events, discarded } = readOutboxWithDiscards(opts);
104
+ const target = MAX_OUTBOX_BYTES / 2;
105
+ let bytes = 0;
106
+ let firstKept = Math.max(events.length - 1, 0);
107
+ for (let i = events.length - 1; i >= 0; i--) {
108
+ bytes += Buffer.byteLength(JSON.stringify(events[i]), 'utf8') + 1;
109
+ if (bytes > target)
110
+ break;
111
+ firstKept = i;
90
112
  }
113
+ const kept = events.slice(firstKept);
114
+ rewriteOutbox(kept, opts);
115
+ return discarded + (events.length - kept.length);
91
116
  }
92
117
  export function writeSessionStateSync(state, opts = {}) {
93
118
  const path = sessionStatePath(opts);