clawmate 1.3.0 → 1.4.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.
Files changed (43) hide show
  1. package/electron-builder.yml +1 -1
  2. package/index.js +589 -406
  3. package/main/ai-bridge.js +64 -58
  4. package/main/ai-connector.js +67 -62
  5. package/main/autostart.js +7 -7
  6. package/main/desktop-path.js +4 -4
  7. package/main/file-command-parser.js +77 -41
  8. package/main/file-ops.js +27 -27
  9. package/main/index.js +18 -16
  10. package/main/ipc-handlers.js +27 -24
  11. package/main/manifest.js +2 -2
  12. package/main/platform.js +16 -16
  13. package/main/smart-file-ops.js +64 -64
  14. package/main/store.js +1 -1
  15. package/main/telegram.js +154 -121
  16. package/main/tray.js +226 -71
  17. package/main/updater.js +13 -13
  18. package/openclaw.plugin.json +1 -1
  19. package/package.json +3 -4
  20. package/preload/preload.js +18 -18
  21. package/renderer/css/effects.css +6 -6
  22. package/renderer/css/pet.css +8 -8
  23. package/renderer/css/speech.css +5 -5
  24. package/renderer/first-run.html +15 -15
  25. package/renderer/index.html +4 -4
  26. package/renderer/js/ai-controller.js +99 -88
  27. package/renderer/js/app.js +26 -23
  28. package/renderer/js/browser-watcher.js +32 -32
  29. package/renderer/js/character.js +33 -33
  30. package/renderer/js/interactions.js +57 -14
  31. package/renderer/js/memory.js +144 -37
  32. package/renderer/js/metrics.js +141 -141
  33. package/renderer/js/mode-manager.js +59 -15
  34. package/renderer/js/pet-engine.js +236 -236
  35. package/renderer/js/speech.js +19 -19
  36. package/renderer/js/state-machine.js +23 -23
  37. package/renderer/js/time-aware.js +15 -15
  38. package/renderer/launcher.html +9 -9
  39. package/shared/constants.js +11 -11
  40. package/shared/messages.js +130 -130
  41. package/shared/personalities.js +72 -37
  42. package/skills/launch-pet/index.js +13 -13
  43. package/skills/launch-pet/skill.json +12 -23
@@ -1,87 +1,87 @@
1
1
  /**
2
- * 동작 품질 실시간 계측기 (Self-Observation System)
2
+ * Real-time pet behavior quality meter (Self-Observation System)
3
3
  *
4
- * OpenClaw이 자신의 동작 품질을 관찰하고 계량할 있도록
5
- * 렌더러 측에서 다양한 메트릭을 수집하여 main process로 전송한다.
4
+ * Collects various metrics on the renderer side so ClawMate can
5
+ * observe and measure its own behavior quality, then sends them to the main process.
6
6
  *
7
- * 수집 메트릭:
8
- * - frameRate: 실제 FPS (requestAnimationFrame 기반)
9
- * - stateTransitions: 상태 전환 횟수/패턴 (최근 60)
10
- * - movementSmoothness: 이동 부드러움 (연속 위치 변화의 분산)
11
- * - wallContactAccuracy: 벽면 밀착 정확도 (edge offset 효과)
12
- * - interactionResponseTime: 클릭 반응까지 시간
13
- * - animationFrameConsistency: 프레임 전환 일관성
14
- * - idleRatio: 전체 시간 idle 비율
15
- * - explorationCoverage: 화면 탐험 커버리지 (방문한 영역 비율)
16
- * - speechFrequency: 말풍선 빈도
17
- * - userEngagement: 사용자 상호작용 빈도
7
+ * Collected metrics:
8
+ * - frameRate: Actual FPS (requestAnimationFrame based)
9
+ * - stateTransitions: State transition count/patterns (last 60 seconds)
10
+ * - movementSmoothness: Movement smoothness (variance of consecutive position changes)
11
+ * - wallContactAccuracy: Wall contact accuracy (edge offset effect)
12
+ * - interactionResponseTime: Click -> response time
13
+ * - animationFrameConsistency: Frame transition consistency
14
+ * - idleRatio: Idle time ratio of total time
15
+ * - explorationCoverage: Screen exploration coverage (visited area ratio)
16
+ * - speechFrequency: Speech bubble frequency
17
+ * - userEngagement: User interaction frequency
18
18
  *
19
- * 성능 주의:
20
- * - requestAnimationFrame 루프에 직접 개입하지 않음
21
- * - 가벼운 샘플링 방식 ( 프레임 수집 X, 주기적 폴링 O)
22
- * - 30초마다 요약 전송
19
+ * Performance notes:
20
+ * - Does not directly interfere with requestAnimationFrame loop
21
+ * - Lightweight sampling approach (periodic polling, not per-frame collection)
22
+ * - Summary sent every 30 seconds
23
23
  */
24
24
  const Metrics = (() => {
25
- // --- 설정 상수 ---
26
- const REPORT_INTERVAL = 30000; // 메트릭 보고 주기 (30초)
27
- const SAMPLE_INTERVAL = 200; // 위치 샘플링 주기 (200ms)
28
- const FPS_SAMPLE_INTERVAL = 1000; // FPS 측정 주기 (1초)
29
- const GRID_SIZE = 8; // 탐험 커버리지 그리드 (8x8 = 64)
30
- const TRANSITION_WINDOW = 60000; // 상태 전환 기록 유지 시간 (60초)
31
-
32
- // --- 내부 상태 ---
25
+ // --- Configuration constants ---
26
+ const REPORT_INTERVAL = 30000; // Metric report interval (30s)
27
+ const SAMPLE_INTERVAL = 200; // Position sampling interval (200ms)
28
+ const FPS_SAMPLE_INTERVAL = 1000; // FPS measurement interval (1s)
29
+ const GRID_SIZE = 8; // Exploration coverage grid (8x8 = 64 cells)
30
+ const TRANSITION_WINDOW = 60000; // State transition record retention time (60s)
31
+
32
+ // --- Internal state ---
33
33
  let initialized = false;
34
34
  let reportTimer = null;
35
35
  let sampleTimer = null;
36
36
  let fpsRafId = null;
37
37
 
38
- // FPS 측정
38
+ // FPS measurement
39
39
  let fpsFrameCount = 0;
40
40
  let fpsLastTime = 0;
41
41
  let currentFps = 60;
42
- let fpsHistory = []; // 최근 30초간 FPS 기록
42
+ let fpsHistory = []; // FPS records for the last 30 seconds
43
43
 
44
- // 상태 전환 추적
44
+ // State transition tracking
45
45
  let stateTransitions = []; // [{ from, to, timestamp }]
46
46
  let stateTimeAccum = {}; // { 'idle': totalMs, 'walking': totalMs, ... }
47
47
  let lastStateChangeTime = 0;
48
48
  let lastObservedState = null;
49
49
 
50
- // 이동 부드러움 측정
50
+ // Movement smoothness measurement
51
51
  let positionSamples = []; // [{ x, y, timestamp }]
52
52
 
53
- // 벽면 밀착 정확도
54
- let wallContactSamples = 0; // 벽면 접촉 샘플 수
55
- let wallContactAccurateSamples = 0; // 정확한 밀착 샘플 수
53
+ // Wall contact accuracy
54
+ let wallContactSamples = 0; // Samples during wall contact
55
+ let wallContactAccurateSamples = 0; // Accurate contact samples
56
56
 
57
- // 상호작용 응답 시간
58
- let lastClickTime = 0; // 마지막 클릭 시각
59
- let interactionResponseTimes = []; // [ms] 응답 시간 기록
57
+ // Interaction response time
58
+ let lastClickTime = 0; // Last click timestamp
59
+ let interactionResponseTimes = []; // [ms] Response time records
60
60
 
61
- // 프레임 전환 일관성
62
- let animFrameTimestamps = []; // 애니메이션 프레임 전환 시각 기록
61
+ // Frame transition consistency
62
+ let animFrameTimestamps = []; // Animation frame transition timestamps
63
63
 
64
- // 탐험 커버리지 (8x8 그리드)
65
- let visitedGrid = new Set(); // 방문한 그리드 (문자열 )
64
+ // Exploration coverage (8x8 grid)
65
+ let visitedGrid = new Set(); // Visited grid cells (string keys)
66
66
  let screenW = 0;
67
67
  let screenH = 0;
68
68
 
69
- // 말풍선 빈도
69
+ // Speech bubble frequency
70
70
  let speechCount = 0;
71
71
 
72
- // 사용자 상호작용 빈도
72
+ // User interaction frequency
73
73
  let userClickCount = 0;
74
74
 
75
- // 보고 기간 시작 시각
75
+ // Report period start time
76
76
  let periodStartTime = 0;
77
77
 
78
78
  // ===================================
79
- // 초기화
79
+ // Initialization
80
80
  // ===================================
81
81
 
82
82
  /**
83
- * 메트릭 시스템 초기화
84
- * 기존 엔진/FSM에 간섭하지 않고 외부에서 관찰만 수행
83
+ * Initialize metrics system
84
+ * Observes externally without interfering with existing engine/FSM
85
85
  */
86
86
  function init() {
87
87
  if (initialized) return;
@@ -97,34 +97,34 @@ const Metrics = (() => {
97
97
  screenH = window.innerHeight;
98
98
  });
99
99
 
100
- // FPS 측정 루프 (별도 rAF 기존 엔진 루프에 무개입)
100
+ // FPS measurement loop (separate rAF -- no interference with existing engine loop)
101
101
  _startFpsMeasurement();
102
102
 
103
- // 주기적 위치/상태 샘플링 (200ms 간격)
103
+ // Periodic position/state sampling (200ms interval)
104
104
  sampleTimer = setInterval(_sampleState, SAMPLE_INTERVAL);
105
105
 
106
- // 30초마다 요약 보고
106
+ // Summary report every 30 seconds
107
107
  reportTimer = setInterval(_reportSummary, REPORT_INTERVAL);
108
108
 
109
- // StateMachine 상태 변화 감시 (기존 콜백 체인 비파괴적 래핑)
109
+ // Monitor StateMachine state changes (non-destructive wrapping of existing callback chain)
110
110
  _hookStateChanges();
111
111
 
112
- // 사용자 클릭 이벤트 감시
112
+ // Monitor user click events
113
113
  _hookUserInteractions();
114
114
 
115
- // 말풍선 이벤트 감시
115
+ // Monitor speech bubble events
116
116
  _hookSpeechEvents();
117
117
 
118
- console.log('[Metrics] 자기 관찰 시스템 초기화 완료');
118
+ console.log('[Metrics] Self-observation system initialized');
119
119
  }
120
120
 
121
121
  // ===================================
122
- // FPS 측정
122
+ // FPS Measurement
123
123
  // ===================================
124
124
 
125
125
  /**
126
- * 별도의 rAF 루프로 실제 프레임레이트를 측정
127
- * PetEngine rAF 루프와 독립적으로 동작
126
+ * Measure actual framerate with a separate rAF loop
127
+ * Operates independently from PetEngine's rAF loop
128
128
  */
129
129
  function _startFpsMeasurement() {
130
130
  fpsFrameCount = 0;
@@ -133,13 +133,13 @@ const Metrics = (() => {
133
133
  function fpsLoop(timestamp) {
134
134
  fpsFrameCount++;
135
135
 
136
- // 1초마다 FPS 계산
136
+ // Calculate FPS every second
137
137
  const elapsed = timestamp - fpsLastTime;
138
138
  if (elapsed >= FPS_SAMPLE_INTERVAL) {
139
139
  currentFps = Math.round((fpsFrameCount / elapsed) * 1000 * 10) / 10;
140
140
  fpsHistory.push(currentFps);
141
141
 
142
- // 최근 30 (30) 유지
142
+ // Keep last 30 entries (30 seconds)
143
143
  if (fpsHistory.length > 30) fpsHistory.shift();
144
144
 
145
145
  fpsFrameCount = 0;
@@ -153,25 +153,25 @@ const Metrics = (() => {
153
153
  }
154
154
 
155
155
  // ===================================
156
- // 주기적 상태 샘플링
156
+ // Periodic State Sampling
157
157
  // ===================================
158
158
 
159
159
  /**
160
- * 200ms마다 펫의 위치/상태를 샘플링
161
- * PetEngine StateMachine에서 읽기 전용으로 데이터를 가져옴
160
+ * Sample pet position/state every 200ms
161
+ * Reads data from PetEngine and StateMachine in read-only mode
162
162
  */
163
163
  function _sampleState() {
164
164
  const now = Date.now();
165
165
 
166
- // 위치 샘플링 (이동 부드러움 계산용)
166
+ // Position sampling (for movement smoothness calculation)
167
167
  if (typeof PetEngine !== 'undefined') {
168
168
  const pos = PetEngine.getPosition();
169
169
  positionSamples.push({ x: pos.x, y: pos.y, timestamp: now });
170
170
 
171
- // 최근 30초분만 유지 (150)
171
+ // Keep only last 30 seconds worth (150 entries)
172
172
  if (positionSamples.length > 150) positionSamples.shift();
173
173
 
174
- // 탐험 커버리지: 현재 위치를 그리드에 기록
174
+ // Exploration coverage: record current position on grid
175
175
  if (screenW > 0 && screenH > 0) {
176
176
  const gridX = Math.floor((pos.x / screenW) * GRID_SIZE);
177
177
  const gridY = Math.floor((pos.y / screenH) * GRID_SIZE);
@@ -180,16 +180,16 @@ const Metrics = (() => {
180
180
  visitedGrid.add(`${clampedX},${clampedY}`);
181
181
  }
182
182
 
183
- // 벽면 밀착 정확도 샘플링
183
+ // Wall contact accuracy sampling
184
184
  if (pos.onSurface && pos.movementMode === 'crawling') {
185
185
  wallContactSamples++;
186
- // 가장자리에 정확히 밀착해 있는지 확인 (CHAR_SIZE = 64 기준)
186
+ // Check if accurately flush against edge (based on CHAR_SIZE = 64)
187
187
  const charSize = PetEngine.CHAR_SIZE || 64;
188
188
  let isAccurate = false;
189
189
 
190
190
  switch (pos.edge) {
191
191
  case 'bottom':
192
- isAccurate = pos.y >= (screenH - charSize - 6); // EDGE_OFFSET(4) + 2 허용오차
192
+ isAccurate = pos.y >= (screenH - charSize - 6); // EDGE_OFFSET(4) + 2 tolerance
193
193
  break;
194
194
  case 'top':
195
195
  isAccurate = pos.y <= 6;
@@ -201,18 +201,18 @@ const Metrics = (() => {
201
201
  isAccurate = pos.x >= (screenW - charSize - 6);
202
202
  break;
203
203
  case 'surface':
204
- isAccurate = true; // 표면 위는 항상 정확
204
+ isAccurate = true; // On surface is always accurate
205
205
  break;
206
206
  }
207
207
  if (isAccurate) wallContactAccurateSamples++;
208
208
  }
209
209
  }
210
210
 
211
- // 상태별 누적 시간 갱신
211
+ // Update accumulated time per state
212
212
  if (typeof StateMachine !== 'undefined') {
213
213
  const state = StateMachine.getState();
214
214
  if (state !== lastObservedState) {
215
- // 이전 상태의 시간 누적
215
+ // Accumulate time for previous state
216
216
  if (lastObservedState) {
217
217
  const duration = now - lastStateChangeTime;
218
218
  stateTimeAccum[lastObservedState] = (stateTimeAccum[lastObservedState] || 0) + duration;
@@ -224,31 +224,31 @@ const Metrics = (() => {
224
224
  }
225
225
 
226
226
  // ===================================
227
- // 이벤트 (비파괴적)
227
+ // Event Hooks (non-destructive)
228
228
  // ===================================
229
229
 
230
230
  /**
231
- * StateMachine 상태 전환 감시
232
- * 기존 onStateChange 콜백을 래핑하여 메트릭도 수집
231
+ * Monitor StateMachine state transitions
232
+ * Wraps existing onStateChange callback to also collect metrics
233
233
  */
234
234
  function _hookStateChanges() {
235
235
  if (typeof StateMachine === 'undefined') return;
236
236
 
237
- // 기존 콜백 보존
237
+ // Preserve existing callback
238
238
  const originalCallback = StateMachine._metricsOriginalCallback;
239
239
 
240
240
  StateMachine.setOnStateChange((prevState, newState) => {
241
- // 메트릭 수집: 상태 전환 기록
241
+ // Metrics collection: record state transition
242
242
  const now = Date.now();
243
243
  stateTransitions.push({ from: prevState, to: newState, timestamp: now });
244
244
 
245
- // TRANSITION_WINDOW 이전 기록 제거
245
+ // Remove records older than TRANSITION_WINDOW
246
246
  while (stateTransitions.length > 0 &&
247
247
  stateTransitions[0].timestamp < now - TRANSITION_WINDOW) {
248
248
  stateTransitions.shift();
249
249
  }
250
250
 
251
- // 이전 상태 시간 누적
251
+ // Accumulate previous state time
252
252
  if (prevState) {
253
253
  const duration = now - lastStateChangeTime;
254
254
  stateTimeAccum[prevState] = (stateTimeAccum[prevState] || 0) + duration;
@@ -256,20 +256,20 @@ const Metrics = (() => {
256
256
  lastObservedState = newState;
257
257
  lastStateChangeTime = now;
258
258
 
259
- // 기존 app.js 콜백 체인 실행
260
- // (app.js에서 setOnStateChange 먼저 호출했으므로,
261
- // Metrics.init() 뒤에 호출되면 기존 콜백이 덮어씌워짐.
262
- // 따라서 app.js 콜백 로직을 여기서 재현한다.)
259
+ // Execute existing app.js callback chain
260
+ // (Since app.js calls setOnStateChange first,
261
+ // Metrics.init() called afterward overwrites the existing callback.
262
+ // Therefore we reproduce the app.js callback logic here.)
263
263
  _invokeOriginalStateChangeHandler(prevState, newState);
264
264
  });
265
265
  }
266
266
 
267
267
  /**
268
- * app.js에서 설정한 기존 상태 변화 콜백 로직 재현
269
- * Metrics setOnStateChange 덮어쓰므로, 원래 동작을 보존한다.
268
+ * Reproduce the original state change callback logic set in app.js
269
+ * Since Metrics overwrites setOnStateChange, the original behavior must be preserved.
270
270
  */
271
271
  function _invokeOriginalStateChangeHandler(prevState, newState) {
272
- // 수면 'z' 파티클
272
+ // Sleep 'z' particles
273
273
  if (newState === 'sleeping') {
274
274
  const pet = document.getElementById('pet-container');
275
275
  if (pet) {
@@ -290,7 +290,7 @@ const Metrics = (() => {
290
290
  }
291
291
  }
292
292
 
293
- // OpenClaw에 상태 변화 리포트
293
+ // Report state change to AI
294
294
  if (window.clawmate && window.clawmate.reportToAI) {
295
295
  window.clawmate.reportToAI('state_change', {
296
296
  from: prevState, to: newState,
@@ -299,8 +299,8 @@ const Metrics = (() => {
299
299
  }
300
300
 
301
301
  /**
302
- * 사용자 상호작용 감시 (클릭 이벤트)
303
- * 클릭 상태 변화까지의 응답 시간을 측정
302
+ * Monitor user interactions (click events)
303
+ * Measure response time from click to state change
304
304
  */
305
305
  function _hookUserInteractions() {
306
306
  const petContainer = document.getElementById('pet-container');
@@ -311,40 +311,40 @@ const Metrics = (() => {
311
311
  userClickCount++;
312
312
  });
313
313
 
314
- // 클릭 상태 변화가 발생하면 응답 시간 기록
315
- // (stateTransitions에 항목이 추가될 체크)
314
+ // Record response time when state change occurs after click
315
+ // (Check when new entry is added to stateTransitions)
316
316
  const origPush = Array.prototype.push;
317
317
  const responseTimes = interactionResponseTimes;
318
318
  const clickTimeRef = { get: () => lastClickTime };
319
319
 
320
- // MutationObserver 방식 대신, _hookStateChanges 내부에서 처리
321
- // 상태 전환 시점에 클릭으로부터의 시간을 계산
320
+ // Instead of MutationObserver, handled inside _hookStateChanges
321
+ // Calculate time from click at the moment of state transition
322
322
  setInterval(() => {
323
323
  if (lastClickTime > 0 && stateTransitions.length > 0) {
324
324
  const lastTransition = stateTransitions[stateTransitions.length - 1];
325
325
  if (lastTransition.timestamp > lastClickTime) {
326
326
  const responseTime = lastTransition.timestamp - lastClickTime;
327
- // 3 이내의 응답만 유효 ( 이상은 클릭과 무관한 전환)
327
+ // Only responses within 3 seconds are valid (beyond that is an unrelated transition)
328
328
  if (responseTime < 3000) {
329
329
  interactionResponseTimes.push(responseTime);
330
330
  if (interactionResponseTimes.length > 50) {
331
331
  interactionResponseTimes.shift();
332
332
  }
333
333
  }
334
- lastClickTime = 0; // 측정 완료, 초기화
334
+ lastClickTime = 0; // Measurement complete, reset
335
335
  }
336
336
  }
337
337
  }, 500);
338
338
  }
339
339
 
340
340
  /**
341
- * 말풍선 이벤트 감시
342
- * Speech 모듈의 show() 호출을 감지하여 카운트
341
+ * Monitor speech bubble events
342
+ * Detect Speech module's show() calls and count them
343
343
  */
344
344
  function _hookSpeechEvents() {
345
345
  if (typeof Speech === 'undefined') return;
346
346
 
347
- // Speech.show 래핑하여 호출 횟수 카운트
347
+ // Wrap Speech.show to count invocations
348
348
  const originalShow = Speech.show;
349
349
  if (typeof originalShow === 'function') {
350
350
  Speech.show = function(...args) {
@@ -355,19 +355,19 @@ const Metrics = (() => {
355
355
  }
356
356
 
357
357
  // ===================================
358
- // 메트릭 계산
358
+ // Metric Calculations
359
359
  // ===================================
360
360
 
361
361
  /**
362
- * 이동 부드러움 계산
363
- * 연속 위치 변화의 분산이 작을수록 부드러움
362
+ * Calculate movement smoothness
363
+ * Smaller variance of consecutive position changes = smoother
364
364
  *
365
- * @returns {number} 0~1 (1 가장 부드러움)
365
+ * @returns {number} 0~1 (1 is smoothest)
366
366
  */
367
367
  function _calcMovementSmoothness() {
368
368
  if (positionSamples.length < 3) return 1.0;
369
369
 
370
- // 연속 이동 벡터의 크기 변화 분산을 계산
370
+ // Calculate variance of consecutive movement vector magnitude changes
371
371
  const deltas = [];
372
372
  for (let i = 1; i < positionSamples.length; i++) {
373
373
  const dx = positionSamples[i].x - positionSamples[i - 1].x;
@@ -377,7 +377,7 @@ const Metrics = (() => {
377
377
 
378
378
  if (deltas.length < 2) return 1.0;
379
379
 
380
- // 연속 delta 차이의 분산 (가속도의 변화)
380
+ // Variance of differences between consecutive deltas (change in acceleration)
381
381
  const accelChanges = [];
382
382
  for (let i = 1; i < deltas.length; i++) {
383
383
  accelChanges.push(Math.abs(deltas[i] - deltas[i - 1]));
@@ -385,17 +385,17 @@ const Metrics = (() => {
385
385
 
386
386
  const avgAccelChange = accelChanges.reduce((a, b) => a + b, 0) / accelChanges.length;
387
387
 
388
- // 분산을 0~1 점수로 변환 (avgAccelChange 클수록 부드러움)
389
- // 10px 이상의 급격한 변화 = 0점, 0 = 1
388
+ // Convert variance to 0~1 score (larger avgAccelChange = less smooth)
389
+ // Abrupt change of 10px+ = 0, 0 = 1
390
390
  const smoothness = Math.max(0, Math.min(1, 1 - (avgAccelChange / 10)));
391
391
  return Math.round(smoothness * 100) / 100;
392
392
  }
393
393
 
394
394
  /**
395
- * 프레임 전환 일관성 계산
396
- * 애니메이션 프레임 간격의 일관성 (FPS 안정성)
395
+ * Calculate frame transition consistency
396
+ * Consistency of animation frame intervals (FPS stability)
397
397
  *
398
- * @returns {number} 0~1 (1 가장 일관)
398
+ * @returns {number} 0~1 (1 is most consistent)
399
399
  */
400
400
  function _calcFrameConsistency() {
401
401
  if (fpsHistory.length < 2) return 1.0;
@@ -403,19 +403,19 @@ const Metrics = (() => {
403
403
  const avg = fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length;
404
404
  if (avg === 0) return 0;
405
405
 
406
- // FPS 표준편차 / 평균 (변동계수)
406
+ // FPS standard deviation / mean (coefficient of variation)
407
407
  const variance = fpsHistory.reduce((sum, fps) => sum + Math.pow(fps - avg, 2), 0) / fpsHistory.length;
408
408
  const stdDev = Math.sqrt(variance);
409
- const cv = stdDev / avg; // 변동계수
409
+ const cv = stdDev / avg; // Coefficient of variation
410
410
 
411
- // cv 0이면 완벽한 일관성, 0.5 이상이면 매우 불안정
411
+ // cv of 0 = perfect consistency, 0.5+ = very unstable
412
412
  const consistency = Math.max(0, Math.min(1, 1 - cv * 2));
413
413
  return Math.round(consistency * 100) / 100;
414
414
  }
415
415
 
416
416
  /**
417
- * 상태 전환 카운트 집계
418
- * 최근 60초 상태별 전환 횟수
417
+ * Aggregate state transition counts
418
+ * Transition count per state within the last 60 seconds
419
419
  *
420
420
  * @returns {object} { idle: n, walking: n, ... }
421
421
  */
@@ -431,8 +431,8 @@ const Metrics = (() => {
431
431
  }
432
432
 
433
433
  /**
434
- * idle 비율 계산
435
- * 보고 기간 idle 상태로 보낸 시간 비율
434
+ * Calculate idle ratio
435
+ * Ratio of time spent in idle state within the report period
436
436
  *
437
437
  * @returns {number} 0~1
438
438
  */
@@ -446,8 +446,8 @@ const Metrics = (() => {
446
446
  }
447
447
 
448
448
  /**
449
- * 탐험 커버리지 계산
450
- * 8x8 그리드 방문한 셀의 비율
449
+ * Calculate exploration coverage
450
+ * Ratio of visited cells in the 8x8 grid
451
451
  *
452
452
  * @returns {number} 0~1
453
453
  */
@@ -458,7 +458,7 @@ const Metrics = (() => {
458
458
  }
459
459
 
460
460
  /**
461
- * 벽면 밀착 정확도 계산
461
+ * Calculate wall contact accuracy
462
462
  *
463
463
  * @returns {number} 0~1
464
464
  */
@@ -469,9 +469,9 @@ const Metrics = (() => {
469
469
  }
470
470
 
471
471
  /**
472
- * 상호작용 평균 응답 시간 계산
472
+ * Calculate average interaction response time
473
473
  *
474
- * @returns {number} ms (응답 기록이 없으면 0)
474
+ * @returns {number} ms (0 if no response records)
475
475
  */
476
476
  function _calcAvgInteractionResponse() {
477
477
  if (interactionResponseTimes.length === 0) return 0;
@@ -480,11 +480,11 @@ const Metrics = (() => {
480
480
  }
481
481
 
482
482
  // ===================================
483
- // 스냅샷 요약
483
+ // Snapshot and Summary
484
484
  // ===================================
485
485
 
486
486
  /**
487
- * 현재 시점의 메트릭 스냅샷 반환 (실시간)
487
+ * Return current metric snapshot (real-time)
488
488
  * @returns {object}
489
489
  */
490
490
  function getSnapshot() {
@@ -504,21 +504,21 @@ const Metrics = (() => {
504
504
  }
505
505
 
506
506
  /**
507
- * 30 기간 요약 생성 + 카운터 리셋
508
- * @returns {object} 메트릭 요약 데이터
507
+ * Generate 30-second period summary + reset counters
508
+ * @returns {object} Metric summary data
509
509
  */
510
510
  function getSummary() {
511
511
  const now = Date.now();
512
512
  const period = now - periodStartTime;
513
513
 
514
- // 마지막 관찰 상태의 시간도 누적
514
+ // Also accumulate time for the last observed state
515
515
  if (lastObservedState) {
516
516
  const duration = now - lastStateChangeTime;
517
517
  stateTimeAccum[lastObservedState] = (stateTimeAccum[lastObservedState] || 0) + duration;
518
518
  lastStateChangeTime = now;
519
519
  }
520
520
 
521
- // 평균 FPS 계산
521
+ // Calculate average FPS
522
522
  const avgFps = fpsHistory.length > 0
523
523
  ? Math.round((fpsHistory.reduce((a, b) => a + b, 0) / fpsHistory.length) * 10) / 10
524
524
  : 60;
@@ -538,65 +538,65 @@ const Metrics = (() => {
538
538
  period: period,
539
539
  };
540
540
 
541
- // 기간 카운터 리셋 (누적 데이터는 유지, 기간 카운터만 초기화)
541
+ // Reset period counters (keep accumulated data, only reset period counters)
542
542
  periodStartTime = now;
543
543
  speechCount = 0;
544
544
  userClickCount = 0;
545
545
  stateTimeAccum = {};
546
546
  fpsHistory = [];
547
547
  interactionResponseTimes = [];
548
- // visitedGrid 리셋하지 않음 (누적 탐험 기록)
549
- // positionSamples 자동으로 오래된 제거됨
548
+ // visitedGrid is not reset (accumulated exploration record)
549
+ // positionSamples automatically removes old entries
550
550
 
551
551
  return summary;
552
552
  }
553
553
 
554
554
  // ===================================
555
- // 보고
555
+ // Reporting
556
556
  // ===================================
557
557
 
558
558
  /**
559
- * 30초마다 메트릭 요약을 main process 전송
559
+ * Send metric summary to main process every 30 seconds
560
560
  */
561
561
  function _reportSummary() {
562
562
  const summary = getSummary();
563
563
 
564
- // main process 전송 (preload 브릿지 경유)
564
+ // Send to main process (via preload bridge)
565
565
  if (window.clawmate && typeof window.clawmate.reportMetrics === 'function') {
566
566
  window.clawmate.reportMetrics(summary);
567
567
  }
568
568
 
569
- // 콘솔에도 간략히 출력 (디버그용)
569
+ // Brief console output (for debugging)
570
570
  console.log(
571
571
  `[Metrics] FPS:${summary.fps} | ` +
572
- `부드러움:${summary.movementSmoothness} | ` +
572
+ `smooth:${summary.movementSmoothness} | ` +
573
573
  `idle:${(summary.idleRatio * 100).toFixed(0)}% | ` +
574
- `탐험:${(summary.explorationCoverage * 100).toFixed(0)}% | ` +
575
- `클릭:${summary.userClicks} | ` +
576
- `말풍선:${summary.speechCount}`
574
+ `explore:${(summary.explorationCoverage * 100).toFixed(0)}% | ` +
575
+ `clicks:${summary.userClicks} | ` +
576
+ `speech:${summary.speechCount}`
577
577
  );
578
578
  }
579
579
 
580
580
  /**
581
- * 탐험 커버리지 그리드 리셋
582
- * 외부에서 탐험 세션을 시작할 사용
581
+ * Reset exploration coverage grid
582
+ * Used when starting a new exploration session externally
583
583
  */
584
584
  function resetExplorationGrid() {
585
585
  visitedGrid.clear();
586
586
  }
587
587
 
588
588
  /**
589
- * 시스템 정리
589
+ * System cleanup
590
590
  */
591
591
  function destroy() {
592
592
  if (reportTimer) clearInterval(reportTimer);
593
593
  if (sampleTimer) clearInterval(sampleTimer);
594
594
  if (fpsRafId) cancelAnimationFrame(fpsRafId);
595
595
  initialized = false;
596
- console.log('[Metrics] 자기 관찰 시스템 종료');
596
+ console.log('[Metrics] Self-observation system terminated');
597
597
  }
598
598
 
599
- // --- 공개 API ---
599
+ // --- Public API ---
600
600
  return {
601
601
  init,
602
602
  getSnapshot,