editmamei 1.3.0 → 1.5.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.
Files changed (39) hide show
  1. package/README.md +10 -9
  2. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  3. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  4. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  5. package/dist/cli/help.js +1 -1
  6. package/dist/core/server.js +112 -15
  7. package/dist/core/tool-groups.js +4 -3
  8. package/dist/core/tool-registry.js +3 -1
  9. package/dist/core/tool-tiers.js +2 -1
  10. package/dist/install-channel.js +52 -2
  11. package/dist/kernel/kernel.js +1 -0
  12. package/dist/kernel/module-lifecycle.js +44 -17
  13. package/dist/modules/ce/index.js +7 -0
  14. package/dist/perception/facets.js +4 -3
  15. package/dist/perception/region-scorer.js +1 -1
  16. package/dist/perception/scene-model.js +1 -1
  17. package/dist/perception/select-recipes.js +14 -3
  18. package/dist/platform/launch-readiness.js +25 -0
  19. package/dist/platform/macos-runner.js +18 -8
  20. package/dist/platform/script-queue.js +5 -19
  21. package/dist/platform/windows-runner.js +18 -8
  22. package/dist/skills/editmamei-skill.zip +0 -0
  23. package/dist/telemetry/activity.js +149 -0
  24. package/dist/telemetry/client.js +125 -8
  25. package/dist/telemetry/events.js +48 -3
  26. package/dist/tools/detection-tools.js +4 -4
  27. package/dist/tools/document-tools.js +23 -1
  28. package/dist/tools/preview-tools.js +1 -1
  29. package/dist/tools/scene-tools.js +8 -6
  30. package/dist/tools/selection-tools.js +155 -109
  31. package/dist/tools/sequence-tools.js +435 -0
  32. package/dist/update/check.js +24 -10
  33. package/dist/utils/operation-timeouts.js +92 -0
  34. package/dist/utils/run-script.js +25 -1
  35. package/dist/utils/session-log.js +18 -4
  36. package/dist/utils/tool-budget-context.js +14 -0
  37. package/dist/utils/xmp-crs.js +682 -0
  38. package/dist/version.js +1 -1
  39. package/package.json +10 -4
@@ -41,7 +41,7 @@ export function estimateHorizon(rowMeans, docHeight, pick, mainSubjectBox, prima
41
41
  const split = pick.bright_fraction;
42
42
  const splitConfidence = Math.max(0, Math.min(1, 1 - Math.abs(0.5 - split) * 2)) * 0.6 + 0.2;
43
43
  if (!rowMeans || rowMeans.length === 0) {
44
- return { y: Math.round(docHeight / 3), placement: 1 / 3, confidence: 0.2 };
44
+ return { detected: false, reason: 'no-row-profile' };
45
45
  }
46
46
  const R = rowMeans.length;
47
47
  let crossRow = -1;
@@ -52,14 +52,15 @@ export function estimateHorizon(rowMeans, docHeight, pick, mainSubjectBox, prima
52
52
  }
53
53
  }
54
54
  if (crossRow <= 0) {
55
- return { y: Math.round(docHeight / 3), placement: 1 / 3, confidence: 0.2 };
55
+ return { detected: false, reason: 'no-luminance-crossing' };
56
56
  }
57
57
  const y = Math.round((crossRow / R) * docHeight);
58
58
  if ((mainSubjectBox && isDominantSubjectCrossing(y, docHeight, mainSubjectBox)) ||
59
59
  (primaryFaceBox && isPortraitFaceCrossing(y, docHeight, primaryFaceBox))) {
60
- return { y: Math.round(docHeight / 3), placement: 1 / 3, confidence: 0.2 };
60
+ return { detected: false, reason: 'dominant-subject' };
61
61
  }
62
62
  return {
63
+ detected: true,
63
64
  y,
64
65
  placement: y / docHeight,
65
66
  confidence: Math.max(0, Math.min(1, splitConfidence)),
@@ -49,7 +49,7 @@ export function buildRegionSignals(info, scene) {
49
49
  centroidY = (bounds.top + bounds.bottom) / 2 / docH;
50
50
  touchesBottom = bounds.bottom >= docH * 0.97;
51
51
  touchesTop = bounds.top <= docH * 0.03;
52
- if (scene.horizonConfidence > 0) {
52
+ if (scene.horizonConfidence > 0 && scene.horizonY !== null) {
53
53
  lowerEdgeAlign = 1 - Math.min(1, Math.abs(bounds.bottom - scene.horizonY) / docH);
54
54
  upperEdgeAlign = 1 - Math.min(1, Math.abs(bounds.top - scene.horizonY) / docH);
55
55
  }
@@ -142,7 +142,7 @@ export async function buildSceneModel(connection, snippet, client, opts = {}) {
142
142
  recipe: { kind: 'posterize_region', levels: 3, sample: 'below_horizon' },
143
143
  },
144
144
  ];
145
- const composition = computeComposition(mainBox, allBoxes, docW, docH, horizon.placement);
145
+ const composition = computeComposition(mainBox, allBoxes, docW, docH, horizon.detected ? horizon.placement : null);
146
146
  const backends = {
147
147
  faces: det.result.backends.faces,
148
148
  objects: det.result.backends.objects,
@@ -58,8 +58,8 @@ function sceneCtx(model) {
58
58
  return {
59
59
  docW: model.doc.width,
60
60
  docH: model.doc.height,
61
- horizonY: model.horizon.y,
62
- horizonConfidence: model.horizon.confidence,
61
+ horizonY: model.horizon.detected ? model.horizon.y : null,
62
+ horizonConfidence: model.horizon.detected ? model.horizon.confidence : 0,
63
63
  indoorObjectCount,
64
64
  };
65
65
  }
@@ -490,7 +490,18 @@ async function resolveFaceFeature(connection, snippet, target, feature, opts) {
490
490
  };
491
491
  }
492
492
  async function resolveAboveHorizon(connection, snippet, model, composition) {
493
- const y = Math.max(1, Math.min(model.doc.height, model.horizon.y));
493
+ const horizon = model.horizon;
494
+ if (!horizon.detected) {
495
+ return {
496
+ target: 'above_horizon',
497
+ method: 'none',
498
+ passed: false,
499
+ confidence: 0,
500
+ reasons: [`no horizon was measured in this frame (${horizon.reason})`],
501
+ detail: { horizon_detected: false, horizon_reason: horizon.reason },
502
+ };
503
+ }
504
+ const y = Math.max(1, Math.min(model.doc.height, horizon.y));
494
505
  const result = (await runScript(connection, await snippet.build('selectRectangle', {
495
506
  left: 0,
496
507
  top: 0,
@@ -0,0 +1,25 @@
1
+ const DEFAULT_POLL_INTERVAL_MS = 250;
2
+ export const LAUNCH_READY_MAX_WAIT_MS = 5_000;
3
+ function defaultSleep(ms) {
4
+ return new Promise((resolve) => {
5
+ setTimeout(resolve, ms).unref();
6
+ });
7
+ }
8
+ export async function waitForLaunchReady(isReady, options = {}) {
9
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
10
+ const maxWaitMs = options.maxWaitMs ?? LAUNCH_READY_MAX_WAIT_MS;
11
+ const sleep = options.sleep ?? defaultSleep;
12
+ const isAborted = options.isAborted ?? (() => false);
13
+ const now = options.now ?? Date.now;
14
+ const deadline = now() + maxWaitMs;
15
+ while (now() < deadline) {
16
+ if (isAborted())
17
+ return false;
18
+ if (await isReady())
19
+ return true;
20
+ if (now() >= deadline)
21
+ break;
22
+ await sleep(intervalMs);
23
+ }
24
+ return false;
25
+ }
@@ -2,12 +2,14 @@ import { exec, spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { Logger } from '../utils/logger.js';
4
4
  import { TempDir } from '../utils/temp.js';
5
+ import { DEFAULT_SCRIPT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
5
6
  import { ScriptQueue } from './script-queue.js';
6
7
  import { runChildWithTimeout } from './run-child.js';
7
8
  import { decodeScriptResult } from './script-result.js';
9
+ import { waitForLaunchReady } from './launch-readiness.js';
8
10
  const execAsync = promisify(exec);
9
- const DEFAULT_SCRIPT_TIMEOUT_MS = 30_000;
10
- const LAUNCH_GRACE_MS = 5_000;
11
+ const LAUNCH_PROBE_TIMEOUT_MS = 2_000;
12
+ const IS_RUNNING_EXEC_TIMEOUT_MS = 3_000;
11
13
  const UNSAFE_IN_APPLESCRIPT_LITERAL = /["\\\r\n\t]/;
12
14
  export class MacOSScriptRunner {
13
15
  logger = new Logger('MacOSScriptRunner');
@@ -69,7 +71,9 @@ end timeout`;
69
71
  }
70
72
  async isRunning() {
71
73
  try {
72
- const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"');
74
+ const { stdout } = await execAsync('pgrep -f "Adobe Photoshop"', {
75
+ timeout: IS_RUNNING_EXEC_TIMEOUT_MS,
76
+ });
73
77
  return stdout.trim().length > 0;
74
78
  }
75
79
  catch {
@@ -81,14 +85,20 @@ end timeout`;
81
85
  this.logger.info('Launching Photoshop', executablePath);
82
86
  const child = spawn('open', ['-a', executablePath], { detached: true, stdio: 'ignore' });
83
87
  child.unref();
84
- const grace = setTimeout(() => {
85
- grace.unref();
86
- resolve();
87
- }, LAUNCH_GRACE_MS);
88
+ let aborted = false;
88
89
  child.on('error', (error) => {
89
- clearTimeout(grace);
90
+ aborted = true;
90
91
  reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
91
92
  });
93
+ const probe = () => this.run("'pong';", LAUNCH_PROBE_TIMEOUT_MS)
94
+ .then(() => true)
95
+ .catch(() => false);
96
+ waitForLaunchReady(probe, { isAborted: () => aborted })
97
+ .catch(() => false)
98
+ .then(() => {
99
+ if (!aborted)
100
+ resolve();
101
+ });
92
102
  });
93
103
  }
94
104
  }
@@ -8,23 +8,7 @@ export class ScriptQueue {
8
8
  }
9
9
  enqueue(run, timeout) {
10
10
  return new Promise((resolve, reject) => {
11
- const task = {
12
- run,
13
- resolve,
14
- reject,
15
- cancelled: false,
16
- settled: false,
17
- timeoutId: null,
18
- };
19
- task.timeoutId = setTimeout(() => {
20
- if (task.settled)
21
- return;
22
- task.cancelled = true;
23
- task.settled = true;
24
- task.timeoutId = null;
25
- reject(new Error('Script execution timeout'));
26
- }, timeout + QUEUE_SLACK_MS);
27
- this.queue.push(task);
11
+ this.queue.push({ run, timeout, resolve, reject, settled: false, timeoutId: null });
28
12
  void this.processQueue();
29
13
  });
30
14
  }
@@ -45,8 +29,10 @@ export class ScriptQueue {
45
29
  try {
46
30
  while (this.queue.length > 0) {
47
31
  const task = this.queue.shift();
48
- if (task.cancelled)
49
- continue;
32
+ task.timeoutId = setTimeout(() => {
33
+ this.settleTask(task, () => task.reject(new Error('Script execution timeout')));
34
+ }, task.timeout + QUEUE_SLACK_MS);
35
+ task.timeoutId.unref?.();
50
36
  try {
51
37
  const result = await task.run();
52
38
  this.settleTask(task, () => task.resolve(result));
@@ -2,12 +2,14 @@ import { exec, spawn } from 'child_process';
2
2
  import { promisify } from 'util';
3
3
  import { Logger } from '../utils/logger.js';
4
4
  import { TempDir } from '../utils/temp.js';
5
+ import { DEFAULT_SCRIPT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
5
6
  import { ScriptQueue } from './script-queue.js';
6
7
  import { runChildWithTimeout } from './run-child.js';
7
8
  import { decodeScriptResult } from './script-result.js';
9
+ import { waitForLaunchReady } from './launch-readiness.js';
8
10
  const execAsync = promisify(exec);
9
- const DEFAULT_SCRIPT_TIMEOUT_MS = 30_000;
10
- const LAUNCH_GRACE_MS = 5_000;
11
+ const LAUNCH_PROBE_TIMEOUT_MS = 2_000;
12
+ const IS_RUNNING_EXEC_TIMEOUT_MS = 3_000;
11
13
  export class WindowsScriptRunner {
12
14
  logger = new Logger('WindowsScriptRunner');
13
15
  queue = new ScriptQueue(this.logger);
@@ -59,7 +61,9 @@ End If
59
61
  }
60
62
  async isRunning() {
61
63
  try {
62
- const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"');
64
+ const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"', {
65
+ timeout: IS_RUNNING_EXEC_TIMEOUT_MS,
66
+ });
63
67
  return stdout.toLowerCase().includes('photoshop.exe');
64
68
  }
65
69
  catch {
@@ -71,14 +75,20 @@ End If
71
75
  this.logger.info('Launching Photoshop', executablePath);
72
76
  const child = spawn(executablePath, [], { detached: true, stdio: 'ignore' });
73
77
  child.unref();
74
- const grace = setTimeout(() => {
75
- grace.unref();
76
- resolve();
77
- }, LAUNCH_GRACE_MS);
78
+ let aborted = false;
78
79
  child.on('error', (error) => {
79
- clearTimeout(grace);
80
+ aborted = true;
80
81
  reject(new Error(`Could not launch Photoshop at ${executablePath}: ${error.message}`));
81
82
  });
83
+ const probe = () => this.run("'pong';", LAUNCH_PROBE_TIMEOUT_MS)
84
+ .then(() => true)
85
+ .catch(() => false);
86
+ waitForLaunchReady(probe, { isAborted: () => aborted })
87
+ .catch(() => false)
88
+ .then(() => {
89
+ if (!aborted)
90
+ resolve();
91
+ });
82
92
  });
83
93
  }
84
94
  }
Binary file
@@ -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,8 +1,10 @@
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, 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
10
  import { appendOutboxSync, clearOutbox, clearSessionState, readOutbox, readSessionState, writeSessionStateSync, } from './outbox.js';
@@ -10,6 +12,8 @@ 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,13 +29,25 @@ 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
+ behindLatest = null;
47
+ moduleUpdate = 'none';
48
+ installAssets = {};
34
49
  lastSessionPersistMs = 0;
50
+ startDayBucket = null;
35
51
  constructor(opts) {
36
52
  this.settings = opts.settings;
37
53
  this.transport = opts.transport ?? httpTransport();
@@ -60,27 +76,104 @@ export class TelemetryClient {
60
76
  this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
61
77
  this.timer.unref?.();
62
78
  if (this.settings.telemetry.usage) {
63
- this.enqueue(buildSessionStart(this.dims, this.now()));
64
- const moduleStatus = this.getModuleStatus();
79
+ const hostNodeMajor = boundMajor(nodeMajor(), 999);
80
+ const hostOsMajor = boundMajor(osMajor(process.platform, release()), 999);
81
+ this.enqueue(buildSessionStart(this.dims, this.now(), {
82
+ ...(hostNodeMajor !== null ? { node_major: hostNodeMajor } : {}),
83
+ arch: archToken(),
84
+ ...(hostOsMajor !== null ? { os_major: hostOsMajor } : {}),
85
+ }));
86
+ const moduleStatus = this.moduleStatus();
65
87
  if (moduleStatus)
66
88
  this.enqueue(buildModuleStatus(this.dims, moduleStatus, this.now()));
67
89
  void this.flush();
68
90
  }
69
91
  }
92
+ recordClientConnected(info) {
93
+ if (!this.active || !this.settings.telemetry.usage)
94
+ return;
95
+ this.enqueue(buildClientConnected(this.dims, info, this.now()));
96
+ }
70
97
  recordCall(call) {
71
98
  if (!this.active || !this.settings.telemetry.usage)
72
99
  return;
100
+ this.ensureStartDayBucket();
101
+ const nowDate = this.now();
102
+ const nowMs = nowDate.getTime();
73
103
  this.toolCallCount += 1;
74
104
  this.distinctTools.add(call.tool);
75
105
  if (!call.success)
76
106
  this.anyFailures = true;
77
- this.enqueue(buildUsageEvent(this.dims, call, this.now()));
107
+ this.lastCallSuccess = call.success;
108
+ if (call.retry)
109
+ this.retryCount += 1;
110
+ if (call.success) {
111
+ if (!READ_ONLY_TOOLS.has(call.tool))
112
+ this.editsOk += 1;
113
+ if (KEPT_WORK_TOOLS.has(call.tool))
114
+ this.keptWork += 1;
115
+ }
116
+ if (this.firstCallAtMs === null)
117
+ this.firstCallAtMs = nowMs;
118
+ this.lastCallAtMs = nowMs;
119
+ this.enqueue(buildUsageEvent(this.dims, call, nowDate));
78
120
  this.persistSessionStateThrottled();
79
121
  }
122
+ setBehindLatest(value) {
123
+ this.behindLatest = value;
124
+ }
125
+ setModuleUpdate(outcome) {
126
+ this.moduleUpdate = outcome;
127
+ }
128
+ setInstallAssets(assets) {
129
+ const templatesSaved = assets.templates_saved !== undefined ? clampCount(assets.templates_saved) : null;
130
+ const actionSets = assets.action_sets !== undefined ? clampCount(assets.action_sets) : null;
131
+ this.installAssets = {
132
+ ...this.installAssets,
133
+ ...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
134
+ ...(actionSets !== null ? { action_sets: actionSets } : {}),
135
+ };
136
+ }
137
+ moduleStatus() {
138
+ if (this.resolvedModuleStatus === null) {
139
+ this.resolvedModuleStatus = { value: this.getModuleStatus() };
140
+ }
141
+ return this.resolvedModuleStatus.value;
142
+ }
143
+ durationS() {
144
+ if (this.firstCallAtMs === null || this.lastCallAtMs === null)
145
+ return undefined;
146
+ return Math.min(Math.max(0, Math.floor((this.lastCallAtMs - this.firstCallAtMs) / 1000)), MAX_SESSION_DURATION_S);
147
+ }
148
+ summaryFields() {
149
+ const duration = this.durationS();
150
+ const moduleStatus = this.moduleStatus();
151
+ return {
152
+ ...(duration !== undefined ? { duration_s: duration } : {}),
153
+ retry_count: this.retryCount,
154
+ ...(this.lastCallSuccess !== null ? { ended_after_failure: !this.lastCallSuccess } : {}),
155
+ edits_ok: this.editsOk,
156
+ kept_work: this.keptWork,
157
+ ...(this.behindLatest !== null ? { behind_latest: this.behindLatest } : {}),
158
+ dropped_events: this.droppedEvents,
159
+ ...(moduleStatus !== null ? { module_update: this.moduleUpdate } : {}),
160
+ ...(this.installAssets.templates_saved !== undefined
161
+ ? { templates_saved: this.installAssets.templates_saved }
162
+ : {}),
163
+ ...(this.installAssets.action_sets !== undefined
164
+ ? { action_sets: this.installAssets.action_sets }
165
+ : {}),
166
+ };
167
+ }
80
168
  psVersion() {
81
169
  const v = this.dims.getPsVersion();
82
170
  return v && v.length > 0 ? v : PS_VERSION_UNKNOWN;
83
171
  }
172
+ ensureStartDayBucket() {
173
+ if (this.startDayBucket === null)
174
+ this.startDayBucket = dayBucket(this.now());
175
+ return this.startDayBucket;
176
+ }
84
177
  persistSessionStateThrottled() {
85
178
  const nowMs = this.now().getTime();
86
179
  if (this.lastSessionPersistMs !== 0 &&
@@ -90,7 +183,7 @@ export class TelemetryClient {
90
183
  this.lastSessionPersistMs = nowMs;
91
184
  const state = {
92
185
  install_id: this.dims.install_id,
93
- ts_bucket: dayBucket(this.now()),
186
+ ts_bucket: this.ensureStartDayBucket(),
94
187
  editmamei_version: this.dims.editmamei_version,
95
188
  edition: this.dims.edition,
96
189
  platform: this.dims.platform,
@@ -98,6 +191,7 @@ export class TelemetryClient {
98
191
  tool_call_count: this.toolCallCount,
99
192
  distinct_tools: this.distinctTools.size,
100
193
  any_failures: this.anyFailures,
194
+ ...this.summaryFields(),
101
195
  };
102
196
  writeSessionStateSync(state, this.outboxOpts);
103
197
  }
@@ -116,6 +210,9 @@ export class TelemetryClient {
116
210
  error_message: sanitizeMessage(diag.error_message),
117
211
  ...(diag.snippet ? { snippet: sanitizeSnippet(diag.snippet) } : {}),
118
212
  ...(diag.stderr_tail ? { stderr_tail: sanitizeStderrTail(diag.stderr_tail) } : {}),
213
+ ...(diag.doc_depth !== undefined ? { doc_depth: diag.doc_depth } : {}),
214
+ ...(diag.doc_mode !== undefined ? { doc_mode: diag.doc_mode } : {}),
215
+ ...(diag.ps_locale !== undefined ? { ps_locale: diag.ps_locale } : {}),
119
216
  }, this.now()));
120
217
  }
121
218
  restampPsVersion(events) {
@@ -134,7 +231,9 @@ export class TelemetryClient {
134
231
  enqueue(event) {
135
232
  this.queue.push(event);
136
233
  if (this.queue.length > MAX_QUEUE_SIZE) {
137
- this.queue.splice(0, this.queue.length - MAX_QUEUE_SIZE);
234
+ const excess = this.queue.length - MAX_QUEUE_SIZE;
235
+ this.queue.splice(0, excess);
236
+ this.droppedEvents += excess;
138
237
  }
139
238
  if (this.queue.length >= this.maxBatchSize)
140
239
  void this.flush();
@@ -170,7 +269,8 @@ export class TelemetryClient {
170
269
  tool_call_count: this.toolCallCount,
171
270
  distinct_tools: this.distinctTools.size,
172
271
  any_failures: this.anyFailures,
173
- }, this.now()));
272
+ ...this.summaryFields(),
273
+ }, this.ensureStartDayBucket(), this.now()));
174
274
  }
175
275
  if (this.queue.length > 0) {
176
276
  appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
@@ -221,11 +321,13 @@ export class TelemetryClient {
221
321
  }
222
322
  }
223
323
  function summaryFromState(s) {
324
+ const templatesSaved = s.templates_saved !== undefined ? clampCount(s.templates_saved) : null;
325
+ const actionSets = s.action_sets !== undefined ? clampCount(s.action_sets) : null;
224
326
  return {
225
327
  v: 2,
226
328
  type: 'session_summary',
227
329
  install_id: s.install_id,
228
- ts_bucket: s.ts_bucket,
330
+ ts_bucket: normalizeDayBucket(s.ts_bucket, new Date()),
229
331
  editmamei_version: s.editmamei_version,
230
332
  edition: s.edition,
231
333
  platform: s.platform,
@@ -233,8 +335,23 @@ function summaryFromState(s) {
233
335
  tool_call_count: s.tool_call_count,
234
336
  distinct_tools: s.distinct_tools,
235
337
  any_failures: s.any_failures,
338
+ ...(s.duration_s !== undefined ? { duration_s: s.duration_s } : {}),
339
+ ...(s.retry_count !== undefined ? { retry_count: s.retry_count } : {}),
340
+ ...(s.ended_after_failure !== undefined ? { ended_after_failure: s.ended_after_failure } : {}),
341
+ ...(s.edits_ok !== undefined ? { edits_ok: s.edits_ok } : {}),
342
+ ...(s.kept_work !== undefined ? { kept_work: s.kept_work } : {}),
343
+ ...(s.behind_latest !== undefined ? { behind_latest: s.behind_latest } : {}),
344
+ ...(s.dropped_events !== undefined ? { dropped_events: s.dropped_events } : {}),
345
+ ...(s.module_update !== undefined ? { module_update: s.module_update } : {}),
346
+ ...(templatesSaved !== null ? { templates_saved: templatesSaved } : {}),
347
+ ...(actionSets !== null ? { action_sets: actionSets } : {}),
236
348
  };
237
349
  }
238
350
  function errMsg(err) {
239
351
  return err instanceof Error ? err.message : String(err);
240
352
  }
353
+ function clampCount(n) {
354
+ if (!Number.isFinite(n))
355
+ return null;
356
+ return Math.min(Math.max(0, Math.trunc(n)), MAX_INSTALL_ASSET_COUNT);
357
+ }