crewx 0.9.0-rc.2 → 0.9.0-rc.21

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 (104) hide show
  1. package/dist/assets/MarketPage-DDPL2d7x.js +36 -0
  2. package/dist/assets/{PromptTab-hdXDbbT6.js → PromptTab-UC3K4qx2.js} +3 -3
  3. package/dist/assets/{arc--CBVVcyH.js → arc-DdP3-fUR.js} +1 -1
  4. package/dist/assets/{architectureDiagram-UYN6MBPD-DUiYwqJn.js → architectureDiagram-UYN6MBPD-D16VON1i.js} +1 -1
  5. package/dist/assets/{blockDiagram-ZHA2E4KO-DV86vAgG.js → blockDiagram-ZHA2E4KO-qs1dIRkl.js} +4 -4
  6. package/dist/assets/{c4Diagram-6F5ED5ID-CvWE7Cx_.js → c4Diagram-6F5ED5ID-BeDy8akQ.js} +1 -1
  7. package/dist/assets/channel-DtrxBSkc.js +1 -0
  8. package/dist/assets/{chunk-5HRBRIJM-DAxM5Ul_.js → chunk-5HRBRIJM-BC1zIb11.js} +1 -1
  9. package/dist/assets/{chunk-7U56Z5CX-BuYaEj8L.js → chunk-7U56Z5CX-BuGil3QD.js} +1 -1
  10. package/dist/assets/{chunk-ASOPGD6M-2Kx0e45C.js → chunk-ASOPGD6M-DmKdDtBQ.js} +1 -1
  11. package/dist/assets/{chunk-KFBOBJHC-CR4QS6Rt.js → chunk-KFBOBJHC-D3BvHLPa.js} +1 -1
  12. package/dist/assets/{chunk-T2TOU4HS-CffcwNHx.js → chunk-T2TOU4HS-B5MoUp-N.js} +1 -1
  13. package/dist/assets/{chunk-TMUBEWPD-BGpYtekN.js → chunk-TMUBEWPD-AvWe_lsV.js} +1 -1
  14. package/dist/assets/classDiagram-LNE6IOMH-BMT5B_JB.js +1 -0
  15. package/dist/assets/classDiagram-v2-MQ7JQ4JX-BMT5B_JB.js +1 -0
  16. package/dist/assets/{dagre-4EVJKHTY-yBWukkrE.js → dagre-4EVJKHTY-B6DewFyM.js} +1 -1
  17. package/dist/assets/{diagram-QW4FP2JN-CIr1PLq3.js → diagram-QW4FP2JN-BTsQ8JSd.js} +1 -1
  18. package/dist/assets/{erDiagram-6RL3IURR-xG7SygyU.js → erDiagram-6RL3IURR-BM5nvgVR.js} +1 -1
  19. package/dist/assets/{flowDiagram-7ASYPVHJ-CKalfOqp.js → flowDiagram-7ASYPVHJ-D1UCCuOG.js} +1 -1
  20. package/dist/assets/{ganttDiagram-NTVNEXSI-Bnx7zrX4.js → ganttDiagram-NTVNEXSI-CNIGs7Cn.js} +1 -1
  21. package/dist/assets/{gitGraph-YCYPL57B-TQlF0fiA.js → gitGraph-YCYPL57B-DSLjx1yQ.js} +1 -1
  22. package/dist/assets/{gitGraphDiagram-NRZ2UAAF-DxlaFkwO.js → gitGraphDiagram-NRZ2UAAF-B11zX1Fa.js} +1 -1
  23. package/dist/assets/{infoDiagram-A4XQUW5V-B1-arkqa.js → infoDiagram-A4XQUW5V-DkjvODYl.js} +1 -1
  24. package/dist/assets/{journeyDiagram-G5WM74LC-Cj439GUX.js → journeyDiagram-G5WM74LC-BkCQPMn6.js} +1 -1
  25. package/dist/assets/{kanban-definition-QRCXZQQD-BvrIwEIc.js → kanban-definition-QRCXZQQD-_OX7pNfJ.js} +1 -1
  26. package/dist/assets/{linear-BKYNK0ri.js → linear-BYjwVAR6.js} +1 -1
  27. package/dist/assets/main-CZdKLdUl.css +10 -0
  28. package/dist/assets/main-CkkZNCy-.js +1561 -0
  29. package/dist/assets/{mindmap-definition-GWI6TPTV-BExfJY0G.js → mindmap-definition-GWI6TPTV-BzYpSnv8.js} +1 -1
  30. package/dist/assets/ort-wasm-simd-threaded.asyncify-DMmc6YqF.wasm +0 -0
  31. package/dist/assets/{pieDiagram-YF2LJOPJ-RPWpvrC8.js → pieDiagram-YF2LJOPJ-CEFg6ao0.js} +1 -1
  32. package/dist/assets/{quadrantDiagram-OS5C2QUG-DmbBzk5Q.js → quadrantDiagram-OS5C2QUG-_5w2x3VV.js} +1 -1
  33. package/dist/assets/{requirementDiagram-MIRIMTAZ-DFQuFXq3.js → requirementDiagram-MIRIMTAZ-B_VQYReZ.js} +1 -1
  34. package/dist/assets/{sankeyDiagram-Y46BX6SQ-p_bw56lJ.js → sankeyDiagram-Y46BX6SQ-CgmOlLhh.js} +1 -1
  35. package/dist/assets/{sequenceDiagram-G6AWOVSC-DTbBkLeD.js → sequenceDiagram-G6AWOVSC-MGVt0Ai2.js} +1 -1
  36. package/dist/assets/{stateDiagram-MAYHULR4-DBvC6UcF.js → stateDiagram-MAYHULR4-DaZjgTu-.js} +1 -1
  37. package/dist/assets/stateDiagram-v2-4JROLMXI-DN1wGYkV.js +1 -0
  38. package/dist/assets/{timeline-definition-U7ZMHBDA-B6utvIg1.js → timeline-definition-U7ZMHBDA-CusetmK6.js} +1 -1
  39. package/dist/assets/transformers.web-BN8Ug43_.js +45 -0
  40. package/dist/assets/{xychartDiagram-6QU3TZC5-BHG0s1Ye.js → xychartDiagram-6QU3TZC5-CTndf-if.js} +1 -1
  41. package/dist/index.html +2 -2
  42. package/dist-server/app.module.js +4 -0
  43. package/dist-server/bootstrap/codex-writable-roots.js +88 -0
  44. package/dist-server/bootstrap/crewx-server.js +10 -2
  45. package/dist-server/common/decorator/skip-request-log.decorator.js +8 -0
  46. package/dist-server/common/interceptor/http-logging.interceptor.js +17 -4
  47. package/dist-server/common/task-log/stdout-json-fallback.js +200 -0
  48. package/dist-server/domain/agent/agent.controller.js +14 -0
  49. package/dist-server/domain/agent/agent.service.js +246 -18
  50. package/dist-server/domain/agent/dto/create-agent.dto.js +12 -0
  51. package/dist-server/domain/agent/dto/remote-provider.dto.js +82 -0
  52. package/dist-server/domain/agent/dto/reorder-agents.dto.js +51 -0
  53. package/dist-server/domain/agent/dto/update-agent.dto.js +3 -1
  54. package/dist-server/domain/agent/provider.controller.js +12 -1
  55. package/dist-server/domain/doc/doc.controller.js +43 -9
  56. package/dist-server/domain/doc/doc.service.js +80 -6
  57. package/dist-server/domain/doc/image-constants.js +11 -1
  58. package/dist-server/domain/events/workspace-events.controller.js +2 -0
  59. package/dist-server/domain/message/dto/send-message.dto.js +49 -1
  60. package/dist-server/domain/message/message.controller.js +1 -1
  61. package/dist-server/domain/message/message.module.js +2 -1
  62. package/dist-server/domain/message/message.service.js +83 -13
  63. package/dist-server/domain/settings/settings.service.js +188 -1
  64. package/dist-server/domain/system/system.controller.js +41 -0
  65. package/dist-server/domain/system/system.module.js +19 -0
  66. package/dist-server/domain/task/task.constants.js +8 -1
  67. package/dist-server/domain/task/task.service.js +59 -67
  68. package/dist-server/domain/thread/dto/list-threads.dto.js +18 -0
  69. package/dist-server/domain/thread/overdrive.types.js +38 -0
  70. package/dist-server/domain/thread/thread.service.js +83 -70
  71. package/dist-server/domain/usage/usage.controller.js +18 -0
  72. package/dist-server/domain/usage/usage.module.js +1 -0
  73. package/dist-server/domain/usage/usage.service.js +246 -4
  74. package/dist-server/domain/usage-report/dto/usage-report.dto.js +2 -0
  75. package/dist-server/domain/usage-report/usage-report.controller.js +72 -0
  76. package/dist-server/domain/usage-report/usage-report.module.js +23 -0
  77. package/dist-server/domain/usage-report/usage-report.service.js +135 -0
  78. package/dist-server/domain/wi/wi.service.js +45 -3
  79. package/dist-server/main.js +1 -0
  80. package/dist-server/repository/repository.module.js +2 -0
  81. package/dist-server/repository/task.repository.js +56 -0
  82. package/dist-server/repository/thread.repository.js +13 -0
  83. package/dist-server/repository/usage-report.repository.js +27 -0
  84. package/package.json +9 -9
  85. package/packages/cli/dist/bootstrap/codex-writable-roots.d.ts +45 -0
  86. package/packages/cli/dist/bootstrap/codex-writable-roots.js +88 -0
  87. package/packages/cli/dist/bootstrap/crewx-cli.js +2 -0
  88. package/packages/cli/dist/commands/execute.js +10 -2
  89. package/packages/cli/dist/commands/parse-common-flags.d.ts +5 -1
  90. package/packages/cli/dist/commands/parse-common-flags.js +9 -2
  91. package/packages/cli/dist/commands/ps.js +51 -2
  92. package/packages/cli/dist/commands/query.js +10 -2
  93. package/packages/cli/dist/commands/restart.js +20 -6
  94. package/packages/cli/dist/commands/write-output.d.ts +3 -0
  95. package/packages/cli/dist/commands/write-output.js +24 -0
  96. package/packages/cli/dist/main.js +7 -0
  97. package/packages/cli/package.json +1 -1
  98. package/dist/assets/MarketPage-T1o_oIrw.js +0 -41
  99. package/dist/assets/channel-7q16D7P9.js +0 -1
  100. package/dist/assets/classDiagram-LNE6IOMH-C6qjk4uc.js +0 -1
  101. package/dist/assets/classDiagram-v2-MQ7JQ4JX-C6qjk4uc.js +0 -1
  102. package/dist/assets/main-C5mwv79r.css +0 -10
  103. package/dist/assets/main-DmvFaz-d.js +0 -1486
  104. package/dist/assets/stateDiagram-v2-4JROLMXI-CCMApx_v.js +0 -1
@@ -17,6 +17,8 @@ const sdk_1 = require("@crewx/sdk");
17
17
  const thread_repository_js_1 = require("../../repository/thread.repository.js");
18
18
  const crewx_pool_service_js_1 = require("../../modules/crewx-pool.service.js");
19
19
  const workspace_context_store_js_1 = require("../../common/workspace-context.store.js");
20
+ const stdout_json_fallback_js_1 = require("../../common/task-log/stdout-json-fallback.js");
21
+ const overdrive_types_js_1 = require("./overdrive.types.js");
20
22
  let ThreadService = ThreadService_1 = class ThreadService {
21
23
  threadRepo;
22
24
  crewxPool;
@@ -45,7 +47,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
45
47
  // Sort
46
48
  const sort = params.sort || 'updatedAt';
47
49
  const order = params.order || 'desc';
48
- filtered.sort((a, b) => {
50
+ const sortCompare = (a, b) => {
49
51
  let cmp = 0;
50
52
  if (sort === 'messageCount') {
51
53
  cmp = (a.message_count ?? 0) - (b.message_count ?? 0);
@@ -57,12 +59,27 @@ let ThreadService = ThreadService_1 = class ThreadService {
57
59
  cmp = new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime();
58
60
  }
59
61
  return order === 'asc' ? cmp : -cmp;
60
- });
61
- // Pagination
62
+ };
63
+ filtered.sort(sortCompare);
62
64
  const total = filtered.length;
63
65
  const limit = this.normalizeLimit(params.limit, 50, 200);
64
66
  const offset = this.normalizeOffset(params.offset);
65
- const paged = filtered.slice(offset, offset + limit);
67
+ let paged;
68
+ if (params.pinnedFirst === false) {
69
+ // Pure sort order (e.g. sidebar recent list) — pinned threads are not
70
+ // separated out, so the page reflects a plain updatedAt/createdAt slice.
71
+ paged = filtered.slice(offset, offset + limit);
72
+ }
73
+ else {
74
+ // Pinned threads must always be visible regardless of the pagination cutoff —
75
+ // otherwise threads pinned long ago silently fall out of the top-N window as
76
+ // newer threads accumulate. Pinned rows are shown in full, sorted by the user's
77
+ // manual pinOrder; only the unpinned remainder is paginated by limit/offset.
78
+ const pinnedRows = filtered.filter((r) => !!(r.pinned ?? 0));
79
+ const unpinnedRows = filtered.filter((r) => !(r.pinned ?? 0));
80
+ pinnedRows.sort((a, b) => this.getPinOrder(a) - this.getPinOrder(b));
81
+ paged = [...pinnedRows, ...unpinnedRows.slice(offset, offset + limit)];
82
+ }
66
83
  // Batch-fetch tasks for paged threads
67
84
  const threadIds = paged.map((r) => r.id);
68
85
  const tasksByThread = this.threadRepo.batchFetchTasks(threadIds, workspaceId);
@@ -101,6 +118,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
101
118
  updatedAt: threadRow.updated_at,
102
119
  pinned: !!(threadRow.pinned ?? 0),
103
120
  starred: !!(threadRow.starred ?? 0),
121
+ overdrive: (0, overdrive_types_js_1.parseOverdriveSummary)(threadRow.metadata),
104
122
  totals: {
105
123
  taskCount: stats.taskCount,
106
124
  inputTokens: stats.inputTokens,
@@ -195,11 +213,11 @@ let ThreadService = ThreadService_1 = class ThreadService {
195
213
  callerAgentId: row.caller_agent_id || undefined,
196
214
  };
197
215
  if (wantLogs) {
198
- result.logs = this.parseLogs(row.logs, limit, this.extractAdapterKey(row));
216
+ result.logs = this.parseLogs(row.logs, limit, this.extractAdapterKey(row)).logs;
199
217
  }
200
218
  if (wantChildren && children) {
201
219
  result.childTasks = children.map((child) => {
202
- const logs = this.parseLogs(child.logs, 1, this.extractAdapterKey(child));
220
+ const { logs } = this.parseLogs(child.logs, 1, this.extractAdapterKey(child));
203
221
  return {
204
222
  taskId: child.id,
205
223
  agentId: child.agent_id || '',
@@ -241,12 +259,28 @@ let ThreadService = ThreadService_1 = class ThreadService {
241
259
  const newTaskId = (0, sdk_1.generateId)('tsk');
242
260
  const agentRef = `@${task.agent_id}`;
243
261
  const mode = task.mode ?? 'execute';
262
+ const originalMetadata = this.parseTaskMetadata(task.metadata);
263
+ const originalProvider = this.readString(originalMetadata.provider) ?? undefined;
264
+ const originalWasOverdrive = originalMetadata.overdrive === true;
265
+ const originalOverdriveMode = originalMetadata.overdriveState === 'latch' ? 'latch' : 'count';
244
266
  const crewx = await this.crewxPool.getForCurrentRequest();
245
267
  const sdkOptions = {
246
268
  threadId,
247
269
  taskId: newTaskId,
248
270
  platform: 'cli',
249
- metadata: { thread: threadId, parentTaskId: null, retriedFromTaskId: taskId },
271
+ model: task.model ?? undefined,
272
+ provider: originalProvider,
273
+ ...(originalWasOverdrive ? { overdrive: { mode: originalOverdriveMode } } : {}),
274
+ metadata: {
275
+ thread: threadId,
276
+ parentTaskId: null,
277
+ retriedFromTaskId: taskId,
278
+ ...(originalWasOverdrive ? {
279
+ overdrive: true,
280
+ overdriveState: originalOverdriveMode,
281
+ overdriveMode: mode,
282
+ } : {}),
283
+ },
250
284
  cwd: (0, workspace_context_store_js_1.getWorkspacePath)(),
251
285
  };
252
286
  const sdkCall = mode === 'execute'
@@ -309,6 +343,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
309
343
  ?? sessionId
310
344
  ?? this.extractProviderSessionIdFromLogs(task.logs);
311
345
  const providerSessionReady = this.hasProviderResumableContext(task.logs, provider);
346
+ const { logs: taskLogs, progressSteps } = this.parseLogs(task.logs, 50, this.extractAdapterKey(task));
312
347
  messages.push({
313
348
  id: `${task.id}-assistant`,
314
349
  role: 'assistant',
@@ -330,7 +365,8 @@ let ThreadService = ThreadService_1 = class ThreadService {
330
365
  totalCostUsd,
331
366
  sessionId,
332
367
  error: task.error || null,
333
- logs: this.parseLogs(task.logs, 50, this.extractAdapterKey(task)),
368
+ logs: taskLogs,
369
+ progressSteps,
334
370
  callerAgentId: task.caller_agent_id ?? null,
335
371
  metadata: {
336
372
  ...taskMetadata,
@@ -369,7 +405,6 @@ let ThreadService = ThreadService_1 = class ThreadService {
369
405
  return cleanLines.join('\n').trim();
370
406
  }
371
407
  toSummary = (row) => {
372
- const meta = row.metadata ? JSON.parse(row.metadata) : {};
373
408
  return {
374
409
  threadId: row.id,
375
410
  platform: row.platform || 'cli',
@@ -379,65 +414,24 @@ let ThreadService = ThreadService_1 = class ThreadService {
379
414
  updatedAt: row.updated_at,
380
415
  pinned: !!(row.pinned ?? 0),
381
416
  starred: !!(row.starred ?? 0),
382
- pinOrder: meta.pinOrder ?? 0,
417
+ pinOrder: this.getPinOrder(row),
418
+ overdrive: (0, overdrive_types_js_1.parseOverdriveSummary)(row.metadata),
383
419
  };
384
420
  };
385
- validateId(id) {
386
- if (!/^[\w\p{L}\p{N}._-]+$/u.test(id)) {
387
- throw new common_1.BadRequestException('Invalid ID format');
388
- }
389
- }
390
- /**
391
- * Returns true when the raw stdout line is a known metadata-only JSON event that
392
- * should be silently dropped rather than shown as raw text.
393
- *
394
- * Covers Claude, Codex, and OpenCode stream-json protocols.
395
- * Only called after parseStdoutEvent() returns [] (no displayable entries).
396
- * Partial / truncated JSON → JSON.parse throws → returns false (keep raw text fallback).
397
- *
398
- * @see TaskService.shouldSkipRawStdoutJsonFallback — keep in sync
399
- */
400
- shouldSkipRawStdoutJsonFallback(message) {
401
- const trimmed = message.trimStart();
402
- if (!trimmed.startsWith('{'))
403
- return false;
404
- let obj;
421
+ getPinOrder(row) {
422
+ if (!row.metadata)
423
+ return 0;
405
424
  try {
406
- obj = JSON.parse(trimmed);
425
+ return JSON.parse(row.metadata).pinOrder ?? 0;
407
426
  }
408
427
  catch {
409
- return false;
410
- }
411
- const type = obj.type;
412
- if (typeof type !== 'string')
413
- return false;
414
- // Claude stream-json metadata events
415
- if (type === 'assistant' ||
416
- type === 'user' ||
417
- type === 'system' ||
418
- type === 'result' ||
419
- type === 'rate_limit_event') {
420
- return true;
421
- }
422
- // Codex metadata-only events
423
- if (type === 'thread.started' ||
424
- type === 'turn.started' ||
425
- type === 'turn.completed' ||
426
- type === 'turn.failed' ||
427
- type === 'item.updated') {
428
- return true;
429
- }
430
- // Codex item.started/item.completed reach this function only after
431
- // parseStdoutEvent() returned no displayable entries. Skip raw fallback while
432
- // preserving the original line in tasks.logs for future reprocessing.
433
- if (type === 'item.started' || type === 'item.completed') {
434
- return true;
428
+ return 0;
435
429
  }
436
- // OpenCode metadata-only events
437
- if (type === 'step_start' || type === 'step_finish') {
438
- return true;
430
+ }
431
+ validateId(id) {
432
+ if (!/^[\w\p{L}\p{N}._-]+$/u.test(id)) {
433
+ throw new common_1.BadRequestException('Invalid ID format');
439
434
  }
440
- return false;
441
435
  }
442
436
  normalizeLimit(limit, fallback, max) {
443
437
  const num = Number(limit);
@@ -459,16 +453,16 @@ let ThreadService = ThreadService_1 = class ThreadService {
459
453
  /** @see TaskService.parseLogs — 동일 역할, ACP 파싱 로직 동기화 필요 */
460
454
  parseLogs(logsJson, logLimit, providerId) {
461
455
  if (!logsJson)
462
- return [];
456
+ return { logs: [], progressSteps: [] };
463
457
  let rawLogs;
464
458
  try {
465
459
  rawLogs = JSON.parse(logsJson);
466
460
  }
467
461
  catch {
468
- return [];
462
+ return { logs: [], progressSteps: [] };
469
463
  }
470
464
  if (!Array.isArray(rawLogs))
471
- return [];
465
+ return { logs: [], progressSteps: [] };
472
466
  const entries = [];
473
467
  for (const log of rawLogs) {
474
468
  if (log.level === 'stdout') {
@@ -480,7 +474,7 @@ let ThreadService = ThreadService_1 = class ThreadService {
480
474
  }
481
475
  // Parsed successfully but no displayable entries.
482
476
  // Skip fallback if the raw message is a known metadata-only stream-json event type.
483
- if (this.shouldSkipRawStdoutJsonFallback(log.message))
477
+ if ((0, stdout_json_fallback_js_1.shouldSkipRawStdoutJsonFallback)(log.message, providerId))
484
478
  continue;
485
479
  }
486
480
  catch { /* adapter parse failure — fallback to text */ }
@@ -490,13 +484,12 @@ let ThreadService = ThreadService_1 = class ThreadService {
490
484
  entries.push(...(0, sdk_1.defaultParseEvent)(log.message, log.timestamp));
491
485
  }
492
486
  }
493
- // Pipeline: merge consecutive text → deduplicate tool_use → logLimit slice
487
+ // Pipeline: merge consecutive text → deduplicate tool_use → [branch: text-only slice(-200) = progressSteps] → logLimit slice = logs
494
488
  const merged = this.mergeConsecutiveText(entries);
495
489
  const deduped = this.deduplicateToolUse(merged);
496
- if (deduped.length > logLimit) {
497
- return deduped.slice(deduped.length - logLimit);
498
- }
499
- return deduped;
490
+ const progressSteps = deduped.filter((e) => e.type === 'text').slice(-200);
491
+ const logs = deduped.length > logLimit ? deduped.slice(deduped.length - logLimit) : deduped;
492
+ return { logs, progressSteps };
500
493
  }
501
494
  /** Merge consecutive type=text entries into one while preserving message boundaries. */
502
495
  mergeConsecutiveText(entries) {
@@ -596,6 +589,11 @@ let ThreadService = ThreadService_1 = class ThreadService {
596
589
  if (claudeSessionId) {
597
590
  return claudeSessionId;
598
591
  }
592
+ // OpenCode: every event carries a top-level `sessionID` (e.g. step_start/tool_use/text).
593
+ const opencodeSessionId = this.readString(message.sessionID);
594
+ if (opencodeSessionId) {
595
+ return opencodeSessionId;
596
+ }
599
597
  }
600
598
  catch {
601
599
  // Ignore non-JSON log lines.
@@ -631,6 +629,9 @@ let ThreadService = ThreadService_1 = class ThreadService {
631
629
  if (providerKey === 'codex' && this.isCodexResumableEvent(message)) {
632
630
  return true;
633
631
  }
632
+ if (providerKey === 'opencode' && this.isOpencodeResumableEvent(message)) {
633
+ return true;
634
+ }
634
635
  }
635
636
  catch {
636
637
  // Ignore non-JSON log lines.
@@ -638,6 +639,18 @@ let ThreadService = ThreadService_1 = class ThreadService {
638
639
  }
639
640
  return false;
640
641
  }
642
+ isOpencodeResumableEvent(message) {
643
+ const type = typeof message.type === 'string' ? message.type : '';
644
+ if (type === 'tool_use') {
645
+ return true;
646
+ }
647
+ if (type === 'text') {
648
+ const part = message.part;
649
+ const text = part?.text;
650
+ return typeof text === 'string' && text.trim().length > 0;
651
+ }
652
+ return false;
653
+ }
641
654
  isClaudeResumableEvent(message) {
642
655
  if (message.type !== 'assistant') {
643
656
  return false;
@@ -46,6 +46,14 @@ let UsageController = class UsageController {
46
46
  const data = this.usageService.getProviderUsage(from, to, workspace);
47
47
  return { success: true, data };
48
48
  }
49
+ // USG.MODEL: 모델별 사용량 집계
50
+ getModelUsage(from, to, workspace) {
51
+ if (!from || !to) {
52
+ throw new common_1.BadRequestException('Both "from" and "to" query parameters are required (ISO 8601)');
53
+ }
54
+ const data = this.usageService.getModelUsage(from, to, workspace);
55
+ return { success: true, data };
56
+ }
49
57
  // USG.LIMITS: 구독 한도 조회
50
58
  async getSubscriptionLimits() {
51
59
  const data = await this.usageService.getSubscriptionLimits();
@@ -125,6 +133,16 @@ __decorate([
125
133
  __metadata("design:paramtypes", [String, String, String]),
126
134
  __metadata("design:returntype", void 0)
127
135
  ], UsageController.prototype, "getProviderUsage", null);
136
+ __decorate([
137
+ (0, swagger_1.ApiOperation)({ operationId: 'USG.MODEL', summary: 'Get per-model usage aggregation' }),
138
+ (0, common_1.Get)('models'),
139
+ __param(0, (0, common_1.Query)('from')),
140
+ __param(1, (0, common_1.Query)('to')),
141
+ __param(2, (0, common_1.Query)('workspace')),
142
+ __metadata("design:type", Function),
143
+ __metadata("design:paramtypes", [String, String, String]),
144
+ __metadata("design:returntype", void 0)
145
+ ], UsageController.prototype, "getModelUsage", null);
128
146
  __decorate([
129
147
  (0, swagger_1.ApiOperation)({ operationId: 'USG.LIMITS', summary: 'Get subscription limit windows (Codex + Claude)' }),
130
148
  (0, common_1.Get)('limits'),
@@ -17,5 +17,6 @@ exports.UsageModule = UsageModule = __decorate([
17
17
  (0, common_1.Module)({
18
18
  controllers: [usage_controller_js_1.UsageController],
19
19
  providers: [usage_service_js_1.UsageService],
20
+ exports: [usage_service_js_1.UsageService],
20
21
  })
21
22
  ], UsageModule);
@@ -62,7 +62,14 @@ const CACHE_MAX_SIZE = 10;
62
62
  const CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5분
63
63
  const CLAUDE_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
64
64
  const GLM_OAUTH_CACHE_TTL = 5 * 60 * 1000; // 5분
65
+ const CODEX_CACHE_TTL = 5 * 60 * 1000; // 5분
65
66
  const OAUTH_FAIL_CACHE_TTL = 30 * 1000; // 30s — short TTL for null (failed) responses
67
+ // --- USR: Usage-report-only aggregation constants ---
68
+ // Rework 판정 임계 — thread 연속호출 간격(분). 실DB 검증: docs/research/ai-skill-report.md §9/§10 (core_dev, WI-20260704-020)
69
+ const REWORK_THRESHOLD_MINUTES = 5;
70
+ // failed 3-way 분류 패턴 (WI-20260704-020 §Side-effect risk areas)
71
+ const USER_STOP_ERROR_RE = /Stopped by user|사용자에 의해 중지됨|SIGTERM 143/i;
72
+ const AGENT_BUG_ERROR_RE = /ACP|orphaned|AGENT_NOT_FOUND|프로세스\s*오류/i;
66
73
  let UsageService = UsageService_1 = class UsageService {
67
74
  taskRepository;
68
75
  projectRepository;
@@ -72,6 +79,7 @@ let UsageService = UsageService_1 = class UsageService {
72
79
  cleanupInterval;
73
80
  claudeOAuthCache = null;
74
81
  glmCache = null;
82
+ codexCache = null;
75
83
  constructor(taskRepository, projectRepository, snapshotRepo) {
76
84
  this.taskRepository = taskRepository;
77
85
  this.projectRepository = projectRepository;
@@ -231,6 +239,179 @@ let UsageService = UsageService_1 = class UsageService {
231
239
  };
232
240
  return { from, to, providers, totals };
233
241
  }
242
+ // --- USG.MODEL: Model usage aggregation ---
243
+ getModelUsage(from, to, workspace) {
244
+ const rows = this.taskRepository.getModelUsage(from, to, workspace);
245
+ const grandTotalTokens = rows.reduce((sum, r) => sum + r.totalTokens, 0);
246
+ const models = rows
247
+ .map((r) => ({
248
+ model: r.model,
249
+ provider: r.provider,
250
+ totalTokens: r.totalTokens,
251
+ inputTokens: r.inputTokens,
252
+ outputTokens: r.outputTokens,
253
+ cachedInputTokens: r.cachedInputTokens,
254
+ totalCost: r.costUsd,
255
+ taskCount: r.totalTasks,
256
+ activeDurationMs: r.activeDurationMs,
257
+ sharePercent: grandTotalTokens > 0
258
+ ? Math.round((r.totalTokens / grandTotalTokens) * 10000) / 100
259
+ : 0,
260
+ lastActiveAt: r.lastActiveAt,
261
+ }))
262
+ .sort((a, b) => b.totalCost - a.totalCost);
263
+ const totals = {
264
+ totalTokens: grandTotalTokens,
265
+ totalCost: rows.reduce((sum, r) => sum + r.costUsd, 0),
266
+ totalTasks: rows.reduce((sum, r) => sum + r.totalTasks, 0),
267
+ totalActiveDurationMs: rows.reduce((sum, r) => sum + r.activeDurationMs, 0),
268
+ };
269
+ return { from, to, models, totals };
270
+ }
271
+ // --- USR: Rework rate (thread consecutive-call heuristic) ---
272
+ //
273
+ // Not a direct DB column — best-effort heuristic per docs/research/ai-skill-report.md §2.1:
274
+ // "재작업률"은 절대값이 아니라 상대·추세로만 해석해야 한다.
275
+ getReworkRate(from, to, workspace) {
276
+ const rows = this.taskRepository.getTasksForReport(from, to, workspace);
277
+ const qualifying = rows.filter((r) => r.agentId !== 'workflow' && (r.status === 'success' || r.status === 'completed') && r.threadId !== null);
278
+ // Rows already ordered by started_at ASC (SQL ORDER BY) — group while preserving order.
279
+ const lastSeenByThread = new Map();
280
+ let reworkCount = 0;
281
+ for (const row of qualifying) {
282
+ const threadId = row.threadId;
283
+ const prevAt = lastSeenByThread.get(threadId);
284
+ if (prevAt) {
285
+ const gapMinutes = (Date.parse(row.startedAt) - Date.parse(prevAt)) / 60000;
286
+ if (gapMinutes <= REWORK_THRESHOLD_MINUTES)
287
+ reworkCount++;
288
+ }
289
+ lastSeenByThread.set(threadId, row.startedAt);
290
+ }
291
+ const threadedTasks = qualifying.length;
292
+ const reworkPct = threadedTasks > 0 ? Math.round((reworkCount / threadedTasks) * 1000) / 10 : 0;
293
+ return { threadedTasks, reworkCount, reworkPct };
294
+ }
295
+ // --- USR: Hourly usage (KST 4-slot) ---
296
+ getHourlyUsage(from, to, workspace) {
297
+ const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
298
+ const buckets = {
299
+ dawn: { total: 0, failed: 0 },
300
+ morning: { total: 0, failed: 0 },
301
+ afternoon: { total: 0, failed: 0 },
302
+ night: { total: 0, failed: 0 },
303
+ };
304
+ for (const row of rows) {
305
+ const kstHour = new Date(Date.parse(row.startedAt) + 9 * 60 * 60 * 1000).getUTCHours();
306
+ const slot = this.kstHourToSlot(kstHour);
307
+ buckets[slot].total++;
308
+ if (row.status === 'failed')
309
+ buckets[slot].failed++;
310
+ }
311
+ const slotOrder = ['dawn', 'morning', 'afternoon', 'night'];
312
+ const slots = slotOrder.map((slot) => {
313
+ const b = buckets[slot];
314
+ return {
315
+ slot,
316
+ total: b.total,
317
+ failPct: b.total > 0 ? Math.round((b.failed / b.total) * 1000) / 10 : 0,
318
+ };
319
+ });
320
+ return { slots };
321
+ }
322
+ kstHourToSlot(kstHour) {
323
+ if (kstHour >= 6 && kstHour <= 11)
324
+ return 'morning';
325
+ if (kstHour >= 12 && kstHour <= 17)
326
+ return 'afternoon';
327
+ if (kstHour >= 18 && kstHour <= 23)
328
+ return 'night';
329
+ return 'dawn';
330
+ }
331
+ // --- USR: Failure 3-way breakdown ---
332
+ //
333
+ // user_stop / agent_bug are excluded from the failure rate — only true_fail counts
334
+ // (docs/wi/WI-20260704-020.md §Side-effect risk areas).
335
+ getFailureBreakdown(from, to, workspace) {
336
+ const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
337
+ let userStop = 0;
338
+ let agentBug = 0;
339
+ let trueFail = 0;
340
+ for (const row of rows) {
341
+ if (row.status !== 'failed')
342
+ continue;
343
+ const error = row.error ?? '';
344
+ if (USER_STOP_ERROR_RE.test(error)) {
345
+ userStop++;
346
+ }
347
+ else if (AGENT_BUG_ERROR_RE.test(error)) {
348
+ agentBug++;
349
+ }
350
+ else {
351
+ trueFail++;
352
+ }
353
+ }
354
+ const total = rows.length;
355
+ const failPct = total > 0 ? Math.round((trueFail / total) * 1000) / 10 : 0;
356
+ return { total, userStop, agentBug, trueFail, failPct };
357
+ }
358
+ // --- USR: Agent success rate + rework rate (WI-20260705-001) ---
359
+ //
360
+ // Per-agent breakdown for the "partner-fit" section — reuses the exact same 3-way
361
+ // classification as getFailureBreakdown (agent_bug/user_stop excluded from the failure
362
+ // count, only true_fail counts) so that per-agent rates stay consistent with the global
363
+ // failureBreakdown. Rework detection reuses the same REWORK_THRESHOLD_MINUTES heuristic
364
+ // as getReworkRate (thread-partitioned, globally ordered), attributing each detected
365
+ // rework event to the agent of the later (follow-up) call.
366
+ getAgentSuccess(from, to, workspace) {
367
+ const rows = this.taskRepository.getTasksForReport(from, to, workspace).filter((r) => r.agentId !== 'workflow');
368
+ const byAgent = new Map();
369
+ const getAcc = (agentId) => {
370
+ let acc = byAgent.get(agentId);
371
+ if (!acc) {
372
+ acc = { taskCount: 0, trueFail: 0, reworkCount: 0 };
373
+ byAgent.set(agentId, acc);
374
+ }
375
+ return acc;
376
+ };
377
+ for (const row of rows) {
378
+ const acc = getAcc(row.agentId);
379
+ acc.taskCount++;
380
+ if (row.status === 'failed') {
381
+ const error = row.error ?? '';
382
+ if (!USER_STOP_ERROR_RE.test(error) && !AGENT_BUG_ERROR_RE.test(error)) {
383
+ acc.trueFail++;
384
+ }
385
+ }
386
+ }
387
+ // Rows already ordered by started_at ASC (SQL ORDER BY) — same qualifying filter as
388
+ // getReworkRate, thread-partitioned globally (not per-agent).
389
+ const qualifying = rows.filter((r) => (r.status === 'success' || r.status === 'completed') && r.threadId !== null);
390
+ const lastSeenByThread = new Map();
391
+ for (const row of qualifying) {
392
+ const threadId = row.threadId;
393
+ const prevAt = lastSeenByThread.get(threadId);
394
+ if (prevAt) {
395
+ const gapMinutes = (Date.parse(row.startedAt) - Date.parse(prevAt)) / 60000;
396
+ if (gapMinutes <= REWORK_THRESHOLD_MINUTES) {
397
+ getAcc(row.agentId).reworkCount++;
398
+ }
399
+ }
400
+ lastSeenByThread.set(threadId, row.startedAt);
401
+ }
402
+ const agents = Array.from(byAgent.entries()).map(([agentId, acc]) => {
403
+ const successCount = acc.taskCount - acc.trueFail;
404
+ return {
405
+ agentId,
406
+ taskCount: acc.taskCount,
407
+ successCount,
408
+ successPct: acc.taskCount > 0 ? Math.round((successCount / acc.taskCount) * 1000) / 10 : 0,
409
+ reworkCount: acc.reworkCount,
410
+ reworkPct: acc.taskCount > 0 ? Math.round((acc.reworkCount / acc.taskCount) * 1000) / 10 : 0,
411
+ };
412
+ });
413
+ return { agents };
414
+ }
234
415
  /** Convert UTC ISO string to local date string (YYYY-MM-DD) using timezone. */
235
416
  utcToLocalDate(utcIso, tz) {
236
417
  try {
@@ -518,6 +699,15 @@ let UsageService = UsageService_1 = class UsageService {
518
699
  return { fromIso: from.toISOString(), toIso };
519
700
  }
520
701
  async parseCodexLimits() {
702
+ const now = Date.now();
703
+ if (this.oauthCacheValid(this.codexCache, now, CODEX_CACHE_TTL)) {
704
+ return this.codexCache.data;
705
+ }
706
+ const result = this.computeCodexLimits();
707
+ this.codexCache = { data: result, ts: now };
708
+ return result;
709
+ }
710
+ computeCodexLimits() {
521
711
  try {
522
712
  const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
523
713
  const jsonlFile = this.findLatestCodexJsonl(sessionsDir);
@@ -536,10 +726,7 @@ let UsageService = UsageService_1 = class UsageService {
536
726
  const rateLimits = payload?.['rate_limits'];
537
727
  if (!rateLimits)
538
728
  continue;
539
- return {
540
- fiveHour: this.extractCodexWindow(rateLimits['primary']),
541
- weekly: this.extractCodexWindow(rateLimits['secondary']),
542
- };
729
+ return this.classifyCodexLimits(rateLimits);
543
730
  }
544
731
  catch {
545
732
  // partial write — try next line
@@ -587,6 +774,61 @@ let UsageService = UsageService_1 = class UsageService {
587
774
  return null;
588
775
  }
589
776
  }
777
+ /**
778
+ * Classify Codex rate_limits.primary/secondary into fiveHour/weekly by window_minutes
779
+ * (WI-20260713-001). Codex CLI payload slot order (primary/secondary) is not stable —
780
+ * some accounts send primary=weekly with secondary=null. window_minutes value is the
781
+ * only reliable discriminator:
782
+ * - 299~300 -> fiveHour
783
+ * - 10079~10080 -> weekly
784
+ * Legacy fallback (positional primary->fiveHour, secondary->weekly) is applied only
785
+ * when window_minutes is entirely absent AND both primary+secondary are present
786
+ * (old dual-slot format). A single present slot with no window_minutes, or a
787
+ * present-but-unrecognized window_minutes value, is left unclassified rather than
788
+ * guessed.
789
+ */
790
+ classifyCodexLimits(rateLimits) {
791
+ const primary = rateLimits['primary'];
792
+ const secondary = rateLimits['secondary'];
793
+ const primaryIsObj = typeof primary === 'object' && primary !== null;
794
+ const secondaryIsObj = typeof secondary === 'object' && secondary !== null;
795
+ let fiveHour = null;
796
+ let weekly = null;
797
+ const classifySlot = (slot) => {
798
+ const wm = slot['window_minutes'];
799
+ if (typeof wm !== 'number')
800
+ return 'absent';
801
+ if (wm >= 299 && wm <= 300)
802
+ return 'fiveHour';
803
+ if (wm >= 10079 && wm <= 10080)
804
+ return 'weekly';
805
+ return 'unrecognized';
806
+ };
807
+ if (primaryIsObj) {
808
+ const cls = classifySlot(primary);
809
+ if (cls === 'fiveHour')
810
+ fiveHour = this.extractCodexWindow(primary);
811
+ else if (cls === 'weekly')
812
+ weekly = this.extractCodexWindow(primary);
813
+ }
814
+ if (secondaryIsObj) {
815
+ const cls = classifySlot(secondary);
816
+ if (cls === 'fiveHour')
817
+ fiveHour = this.extractCodexWindow(secondary);
818
+ else if (cls === 'weekly')
819
+ weekly = this.extractCodexWindow(secondary);
820
+ }
821
+ // Absent-only legacy fallback — requires both slots present (old dual-slot shape).
822
+ if (primaryIsObj && secondaryIsObj) {
823
+ if (fiveHour === null && classifySlot(primary) === 'absent') {
824
+ fiveHour = this.extractCodexWindow(primary);
825
+ }
826
+ if (weekly === null && classifySlot(secondary) === 'absent') {
827
+ weekly = this.extractCodexWindow(secondary);
828
+ }
829
+ }
830
+ return { fiveHour, weekly };
831
+ }
590
832
  extractCodexWindow(raw) {
591
833
  if (!raw || typeof raw !== 'object')
592
834
  return null;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });