@kienntpixon/dieu-phoi 0.1.0 → 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog — @kienntpixon/dieu-phoi
2
+
3
+ ## 0.1.1
4
+
5
+ - Sửa: Claude Code bắn `SubagentStop` cả cho subagent nội bộ của nó (không có transcript `agent-<id>.jsonl`, không qua Agent tool). Bản 0.1.0 ghi các lần này thành `complete` rỗng (`subagent_type ""`, `model null`, `tokens null`). Hook giờ bỏ qua khi transcript thiếu / rỗng / không có dòng assistant nào có usage **và** không có `dispatch` cho `agent_id` đó; số lần bỏ qua đếm ở `data/skipped-internal.json` (`doctor` hiển thị).
6
+ - `stats` / `choose` / `retro` (và sink) bỏ qua các `complete` rỗng đã lỡ ghi bởi 0.1.0 — không cần sửa file log.
7
+ - `fixtures/parity-events.jsonl` thêm hai bản ghi dạng này; kết quả parity không đổi.
8
+
9
+ ## 0.1.0
10
+
11
+ - Bản đầu: hook PostToolUse(Agent) + SubagentStop, `setup/doctor/verdict/steer/stats/choose/plan-table/sync/retro/uninstall`.
package/README.md CHANGED
@@ -82,6 +82,7 @@ Kết quả `--json`: `{"action":"dispatch","model":"sonnet","model_id":"claude-
82
82
 
83
83
  - Sự kiện: `~/.claude/dieu-phoi/data/events.jsonl` (`dispatch`, `complete`, `verdict`, `steer`, `override`; `schema: 1`), xem `CONTRACT.md`. File chỉ ghi nối (append-only), không bao giờ viết lại. Tiến độ đẩy lên sink nằm ở `data/sync-offset.json` (byte offset sau dòng cuối đã đẩy); trạng thái segment từng agent ở `data/segments/<agent_id>.json`.
84
84
  - Lỗi của hook: `~/.claude/dieu-phoi/hook-errors.log`.
85
+ - Subagent nội bộ của Claude Code (không có transcript, không qua Agent tool) không được ghi; số lần bỏ qua ở `data/skipped-internal.json`. Log cũ của 0.1.0 có các `complete` rỗng loại này — `stats`/`retro` tự bỏ qua, không cần sửa file (từ 0.1.1).
85
86
  - Người dùng và phiên chỉ lưu dạng sha256 rút gọn 8 ký tự (người dùng = `git config user.email`, không có thì tên tài khoản máy).
86
87
  - Chi phí tính theo giá API trong `pricing.json` (USD / 1M token), đếm usage một lần cho mỗi `requestId`. Hệ số ghi cache (×1.25 cho 5 phút, ×2 cho 1 giờ) là **giả định**. Model không có giá → `cost_usd: null`. Ghi đè giá trong `config.json`:
87
88
 
package/hook.cjs CHANGED
@@ -19,7 +19,7 @@ const path = require('path');
19
19
  const os = require('os');
20
20
  const crypto = require('crypto');
21
21
 
22
- const CORE_VERSION = '0.1.0';
22
+ const CORE_VERSION = '0.1.1';
23
23
  const SCHEMA = 1;
24
24
  const TASK_TYPES = [
25
25
  'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
@@ -119,6 +119,7 @@ function pathsFor(home) {
119
119
  data,
120
120
  events: path.join(data, 'events.jsonl'),
121
121
  segments: path.join(data, 'segments'),
122
+ skippedInternal: path.join(data, 'skipped-internal.json'),
122
123
  syncOffset: path.join(data, 'sync-offset.json'),
123
124
  syncSpawn: path.join(data, 'sync-spawn.json'),
124
125
  lastSync: path.join(data, 'last-sync.json'),
@@ -562,6 +563,33 @@ function handlePostToolUse(p, ctx) {
562
563
  return ev;
563
564
  }
564
565
 
566
+ function hasAssistantUsage(lines) {
567
+ return lines.some((l) => l && l.type === 'assistant' && l.message && l.message.usage
568
+ && l.message.model !== '<synthetic>');
569
+ }
570
+
571
+ /** Whether events.jsonl holds a dispatch for this agent (cheap substring scan, rare path). */
572
+ function hasDispatch(ctx, agentId) {
573
+ let txt;
574
+ try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { return false; }
575
+ const needle = `"agent_id":${JSON.stringify(agentId)}`;
576
+ let i = txt.indexOf(needle);
577
+ while (i >= 0) {
578
+ const start = txt.lastIndexOf('\n', i) + 1;
579
+ const end = txt.indexOf('\n', i);
580
+ if (txt.slice(start, end < 0 ? undefined : end).includes('"kind":"dispatch"')) return true;
581
+ i = txt.indexOf(needle, i + needle.length);
582
+ }
583
+ return false;
584
+ }
585
+
586
+ function countSkippedInternal(ctx) {
587
+ try {
588
+ const cur = readJsonFile(ctx.paths.skippedInternal, {}) || {};
589
+ writeFileAtomic(ctx.paths.skippedInternal, `${JSON.stringify({ count: (Number(cur.count) || 0) + 1, last_ts: nowIso() })}\n`);
590
+ } catch (_) { /* best effort */ }
591
+ }
592
+
565
593
  function handleSubagentStop(p, ctx) {
566
594
  const agentId = typeof p.agent_id === 'string' && p.agent_id ? p.agent_id : null;
567
595
  if (!agentId) return null;
@@ -570,8 +598,15 @@ function handleSubagentStop(p, ctx) {
570
598
  let sum = null;
571
599
  let tag = null;
572
600
  const tpath = p.agent_transcript_path;
573
- if (typeof tpath === 'string' && tpath && fs.existsSync(tpath)) {
574
- const lines = readTranscript(tpath);
601
+ const exists = typeof tpath === 'string' && tpath && fs.existsSync(tpath);
602
+ const lines = exists ? readTranscript(tpath) : [];
603
+ // Claude Code also fires SubagentStop for its own internal subagents: no transcript on disk (or no
604
+ // API usage in it) and no Agent-tool dispatch. Those are not user work; record nothing.
605
+ if (!hasAssistantUsage(lines) && !hasDispatch(ctx, agentId)) {
606
+ countSkippedInternal(ctx);
607
+ return null;
608
+ }
609
+ if (exists) {
575
610
  const last = lastUuidOf(lines);
576
611
  if (last && seen.has(last)) return null; // same segment reported twice
577
612
  sum = summarizeTranscript(lines, { afterUuids: seen, pricing: loadPricing(ctx) });
@@ -815,7 +850,7 @@ module.exports = {
815
850
  priceFor, usageBuckets, requestCost, parseJsonl, readTranscript, lastUuidOf,
816
851
  tagFromTranscript, summarizeTranscript, appendEvent, eventLinesFrom, readEventLines, readEvents,
817
852
  loadSyncOffset, saveSyncOffset, recordRejected, unsyncedInfo, loadSegments, saveSegments, segmentFile,
818
- extractAgentId, buildDispatch, handlePostToolUse, handleSubagentStop, maybeSpawnSync, runHook, runSync,
853
+ extractAgentId, hasAssistantUsage, hasDispatch, buildDispatch, handlePostToolUse, handleSubagentStop, maybeSpawnSync, runHook, runSync,
819
854
  };
820
855
 
821
856
  if (require.main === module) {
package/lib/aggregate.cjs CHANGED
@@ -59,6 +59,19 @@ function normalizeVerdict(ev) {
59
59
  return { by, outcome: ev.outcome, cause, round: Number(ev.round) || null };
60
60
  }
61
61
 
62
+ function blank(v) {
63
+ return v === '' || v === null || v === undefined;
64
+ }
65
+
66
+ /**
67
+ * `complete` written by core 0.1.0 for Claude Code's internal subagents (no transcript, no Agent-tool
68
+ * dispatch): subagent_type "" + tokens null + model null + no dispatch for that agent_id. Ignored.
69
+ */
70
+ function isInternalComplete(ev, dispatched) {
71
+ return ev.kind === 'complete' && blank(ev.subagent_type) && blank(ev.model)
72
+ && (ev.tokens === null || ev.tokens === undefined) && !dispatched.has(ev.agent_id);
73
+ }
74
+
62
75
  function steerIdOf(ev) {
63
76
  return typeof ev.steer_id === 'string' && ev.steer_id ? ev.steer_id : `${ev.agent_id}|${ev.ts}`;
64
77
  }
@@ -75,8 +88,10 @@ function buildAgents(events) {
75
88
  if (!map.has(id)) map.set(id, { agent_id: id, dispatch: null, completes: [], verdicts: [], steers: [] });
76
89
  return map.get(id);
77
90
  };
91
+ const dispatched = new Set(events.filter((ev) => ev && ev.kind === 'dispatch').map((ev) => ev.agent_id));
78
92
  events.forEach((ev, idx) => {
79
93
  if (!ev || typeof ev !== 'object') return;
94
+ if (isInternalComplete(ev, dispatched)) return;
80
95
  const e = Object.assign({ _idx: idx }, ev);
81
96
  if (ev.kind === 'override') { overrides.push(e); return; }
82
97
  if (ev.kind === 'verdict') {
@@ -332,6 +347,6 @@ function analyze(events, opts) {
332
347
  }
333
348
 
334
349
  module.exports = {
335
- MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, buildAgents, buildTasks,
350
+ MIN_N, FAIL_OUTCOMES, HEAVY_STEERS, CANONICAL_CELL_FIELDS, normalizeVerdict, isInternalComplete, buildAgents, buildTasks,
336
351
  buildCells, buildSteers, canonicalCells, analyze, cellKey, mean, percentile, tsOf, steerIdOf,
337
352
  };
package/lib/commands.cjs CHANGED
@@ -120,6 +120,10 @@ async function cmdDoctor(opts) {
120
120
  let errSize = 0;
121
121
  try { errSize = fs.statSync(ctx.paths.errlog).size; } catch (_) { /* none */ }
122
122
  add(errSize === 0, 'hook-errors', errSize ? `${ctx.paths.errlog} có ${errSize} byte lỗi` : 'không có lỗi', 'CẢNH BÁO');
123
+ const skipped = core.readJsonFile(ctx.paths.skippedInternal, null);
124
+ add(true, 'internal-subagents', skipped && skipped.count
125
+ ? `bỏ qua ${skipped.count} SubagentStop của subagent nội bộ Claude Code (lần cuối ${skipped.last_ts})`
126
+ : 'chưa bỏ qua SubagentStop nội bộ nào');
123
127
  if (ctx.config.sink_url && ctx.config.sink_token) {
124
128
  try {
125
129
  const u = new URL(ctx.config.sink_url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kienntpixon/dieu-phoi",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hook + CLI ghi log subagent của Claude Code và chọn model/effort theo chi phí + thời gian tới lúc nghiệm thu.",
5
5
  "license": "MIT",
6
6
  "author": "kiennt <kiennt@pixon.games>",
@@ -15,7 +15,8 @@
15
15
  "hook.cjs",
16
16
  "pricing.json",
17
17
  "README.md",
18
- "LICENSE"
18
+ "LICENSE",
19
+ "CHANGELOG.md"
19
20
  ],
20
21
  "engines": {
21
22
  "node": ">=18"