@kienntpixon/dieu-phoi 0.7.1 → 0.7.3
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 +39 -0
- package/README.md +3 -0
- package/agents/dp-exec.md +2 -0
- package/agents/dp-helper.md +2 -0
- package/agents/dp-scout.md +2 -0
- package/agents/dp-verify.md +2 -0
- package/bin/dieu-phoi.cjs +30 -2
- package/hook.cjs +1218 -53
- package/lib/aggregate.cjs +5 -4
- package/lib/args.cjs +1 -1
- package/lib/cbm.cjs +8 -2
- package/lib/commands.cjs +417 -36
- package/lib/deps.cjs +162 -0
- package/lib/knowledge.cjs +3 -4
- package/lib/load.cjs +332 -0
- package/lib/procs.cjs +422 -0
- package/lib/profile-scan.cjs +85 -0
- package/lib/profile-seed.cjs +147 -0
- package/lib/profile.cjs +141 -1
- package/lib/prune.cjs +66 -9
- package/lib/setup.cjs +59 -12
- package/lib/worktree.cjs +417 -0
- package/package.json +3 -1
- package/profile/lessons.md +85 -0
- package/profile/unity-lanes.md +316 -0
package/lib/commands.cjs
CHANGED
|
@@ -9,6 +9,8 @@ const agg = require('./aggregate.cjs');
|
|
|
9
9
|
const chooser = require('./choose.cjs');
|
|
10
10
|
const setupLib = require('./setup.cjs');
|
|
11
11
|
const cbm = require('./cbm.cjs');
|
|
12
|
+
const procs = require('./procs.cjs');
|
|
13
|
+
const wt = require('./worktree.cjs');
|
|
12
14
|
const { claudeDirOf, ctxOf, yesNo, repoOf } = require('./args.cjs');
|
|
13
15
|
|
|
14
16
|
const AGENT_OUTCOMES = ['pass', 'fail', 'timeout', 'aborted'];
|
|
@@ -49,14 +51,24 @@ function table(rows) {
|
|
|
49
51
|
// ------------------------------------------------------------------ setup / uninstall
|
|
50
52
|
|
|
51
53
|
function cmdSetup(opts) {
|
|
52
|
-
if (opts['with-codebase-memory']) return cmdSetupWithCbm(opts);
|
|
54
|
+
if (opts['with-codebase-memory'] && !opts['dry-run']) return cmdSetupWithCbm(opts);
|
|
53
55
|
const res = setupLib.setup({
|
|
54
56
|
claudeDir: claudeDirOf(opts),
|
|
55
57
|
dept: typeof opts.dept === 'string' ? opts.dept : undefined,
|
|
56
58
|
sinkUrl: typeof opts['sink-url'] === 'string' ? opts['sink-url'] : undefined,
|
|
57
59
|
sinkToken: typeof opts['sink-token'] === 'string' ? opts['sink-token'] : undefined,
|
|
58
60
|
skillDir: typeof opts['skill-dir'] === 'string' ? opts['skill-dir'] : undefined,
|
|
61
|
+
dryRun: opts['dry-run'] === true,
|
|
59
62
|
});
|
|
63
|
+
const seed = require('./profile-seed.cjs').seedSummary(res.profile);
|
|
64
|
+
if (res.dry_run) {
|
|
65
|
+
if (opts.json) return { code: 0, out: JSON.stringify(res, null, 2) };
|
|
66
|
+
return { code: 0, out: [
|
|
67
|
+
'setup --dry-run: không ghi gì (hook, settings.json, agent dp-*, config đều giữ nguyên)',
|
|
68
|
+
seed,
|
|
69
|
+
`Skill: ${res.skill_dir || '(không thấy; dùng --skill-dir)'}`,
|
|
70
|
+
].join('\n') };
|
|
71
|
+
}
|
|
60
72
|
if (opts.json) {
|
|
61
73
|
const safe = Object.assign({}, res, { config: Object.assign({}, res.config) });
|
|
62
74
|
if (safe.config.sink_token) safe.config.sink_token = '***';
|
|
@@ -65,13 +77,14 @@ function cmdSetup(opts) {
|
|
|
65
77
|
const lines = [
|
|
66
78
|
`Đã cài hook: ${res.hook}`,
|
|
67
79
|
res.settings_changed
|
|
68
|
-
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SessionStart(compact) + UserPromptSubmit + PreToolUse(Agent) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
80
|
+
? `Đã đăng ký PostToolUse(Agent) + SubagentStop + SubagentStart + SessionStart(compact) + UserPromptSubmit + PreToolUse(Agent) trong ${res.settings}${res.backup ? ` (sao lưu: ${path.basename(res.backup)})` : ''}`
|
|
69
81
|
: `settings.json đã có hook, không đổi gì (${res.settings})`,
|
|
70
82
|
`Agent dp-*: ${res.agents.map((a) => `${a.name}=${a.action}`).join(', ')}${res.agents.some((a) => a.action === 'skipped-foreign') ? ' (file trùng tên không phải của dieu-phoi: giữ nguyên, đổi tên rồi chạy lại setup)' : ''}`,
|
|
71
83
|
`Thư mục báo cáo subagent: ${res.runs_dir} (quyền: ${res.permissions.allow.join(', ')} + additionalDirectories)`,
|
|
72
84
|
`Phòng ban: ${res.config.dept || '(chưa đặt, dùng --dept)'}`,
|
|
73
85
|
`Sink: ${res.config.sink_url ? res.config.sink_url : '(chưa có — log chỉ lưu cục bộ)'}`,
|
|
74
86
|
`Skill: ${res.config.skill_dir || '(không thấy; dùng --skill-dir)'}${res.config.skill_version ? ` v${res.config.skill_version}` : ''}`,
|
|
87
|
+
seed,
|
|
75
88
|
'Khởi động lại Claude Code (hoặc mở /hooks) để hook có hiệu lực.',
|
|
76
89
|
];
|
|
77
90
|
return { code: 0, out: lines.join('\n') };
|
|
@@ -131,6 +144,7 @@ async function cmdDoctor(opts) {
|
|
|
131
144
|
add(reg.PreToolUse, 'chain-gate', reg.PreToolUse
|
|
132
145
|
? 'PreToolUse(Agent) có — chặn exec tiếp nối từ handoff thứ 3 (trừ thẻ chain_ok)'
|
|
133
146
|
: 'thiếu hook PreToolUse(Agent) (core 0.7.1) — chuỗi handoff dài không bị chặn; chạy lại `dieu-phoi setup`', 'CẢNH BÁO');
|
|
147
|
+
add(...liveCheck(ctx, reg));
|
|
134
148
|
const permOk = reg.permissions && reg.runs_dir_exists && !reg.legacy_permissions;
|
|
135
149
|
add(permOk, 'run-permissions', permOk
|
|
136
150
|
? `${reg.permission_entries.allow.join(', ')} + additionalDirectories ${reg.permission_entries.dir}`
|
|
@@ -201,6 +215,7 @@ async function cmdDoctor(opts) {
|
|
|
201
215
|
if (orch.ctx_tokens !== null) {
|
|
202
216
|
add(!orch.over, 'orch-ctx', `session ${orch.session_id}: ${orch.ctx_tokens} token, tăng ${orch.ctx_growth}/${orch.soft_cap}, ≈ ${fmtUsd(orch.cum_usd)} — ${orch.advice}`, 'CẢNH BÁO');
|
|
203
217
|
}
|
|
218
|
+
add(...orphanCheck(ctx));
|
|
204
219
|
if (ctx.config.sink_url && ctx.config.sink_token) {
|
|
205
220
|
const cached = opts.net ? null : core.sinkStatusCached(ctx);
|
|
206
221
|
if (cached && (cached.state === 'ok' || cached.state === 'fail')) {
|
|
@@ -222,6 +237,77 @@ async function cmdDoctor(opts) {
|
|
|
222
237
|
return { code: failed ? 1 : 0, out };
|
|
223
238
|
}
|
|
224
239
|
|
|
240
|
+
// s16 (core 0.7.2): Claude Code / code-graph processes whose parent is gone and that are older than this
|
|
241
|
+
const ORPHAN_MIN_AGE_MS = 6 * 3600 * 1000;
|
|
242
|
+
const ORPHAN_CACHE_MS = 10 * 60 * 1000;
|
|
243
|
+
|
|
244
|
+
/** Orphaned claude.exe / codebase-memory-mcp older than 6 h from a process list: [{pid,name,age_h,ws_mb}], or null when the list is unreadable. */
|
|
245
|
+
function orphanScan(list, now) {
|
|
246
|
+
return Array.isArray(list) ? procs.staleOrphans(list, { minAgeMs: ORPHAN_MIN_AGE_MS, now: now === undefined ? Date.now() : now }) : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* doctor item `live-agents` (core 0.7.3 s1): live register data/live/ + caps. Warns when the SubagentStart hook is missing
|
|
251
|
+
* (the register then only fills from --watch) or the machine count reached warn_parallel_machine. "Toàn máy" = this
|
|
252
|
+
* DIEU_PHOI_HOME: sessions with another home have their own register.
|
|
253
|
+
*/
|
|
254
|
+
function liveCheck(ctx, reg) {
|
|
255
|
+
const s = core.liveSummary(ctx, null);
|
|
256
|
+
const lim = s.limits;
|
|
257
|
+
const caps = `trần phiên ${lim.session || 'tắt'}, toàn máy ${lim.machine || 'tắt'}, cảnh báo ${lim.warn || 'tắt'}`;
|
|
258
|
+
const scope = `"toàn máy" = mọi phiên dùng ${ctx.home}`;
|
|
259
|
+
if (reg && !reg.SubagentStart) {
|
|
260
|
+
return [false, 'live-agents', `thiếu hook SubagentStart (core 0.7.3) — sổ agent sống chỉ cập nhật từ --watch; chạy lại dieu-phoi setup (${s.machine} agent sống; ${caps})`, 'CẢNH BÁO'];
|
|
261
|
+
}
|
|
262
|
+
const warn = lim.warn > 0 && s.machine >= lim.warn;
|
|
263
|
+
const list = s.labels.length ? `: ${s.labels.slice(0, 6).join('; ')}${s.labels.length > 6 ? ` (+${s.labels.length - 6})` : ''}` : '';
|
|
264
|
+
return [!warn, 'live-agents', `${s.machine} agent dieu-phoi đang chạy${list} — ${caps}; ${scope}`, 'CẢNH BÁO'];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** doctor item `orphan-processes`: warns with pid + age + working set and a Stop-Process hint; never kills. Returns the args of add(). */
|
|
268
|
+
function orphanCheck(ctx, deps) {
|
|
269
|
+
const d = deps || {};
|
|
270
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
271
|
+
const list = (d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 });
|
|
272
|
+
const found = orphanScan(list, now);
|
|
273
|
+
if (found === null) return [true, 'orphan-processes', 'không đọc được danh sách process (bỏ qua)'];
|
|
274
|
+
try {
|
|
275
|
+
core.writeFileAtomic(path.join(ctx.paths.data, 'orphan-procs.json'), `${JSON.stringify({ ts: new Date(now).toISOString(), n: found.length })}\n`);
|
|
276
|
+
} catch (_) { /* the cache is optional */ }
|
|
277
|
+
// s7: tail / sleep / Claude Code shells left orphaned (Monitor, run_in_background): reported only; `reap --orphans` cleans
|
|
278
|
+
const minK = orphanKillMin(ctx);
|
|
279
|
+
const watchers = Array.isArray(list) ? orphanTargets(list, now, minK, d).found : [];
|
|
280
|
+
const wMb = watchers.reduce((n, p) => n + (p.ws ? Math.round(p.ws / 1048576) : 0), 0);
|
|
281
|
+
const wTxt = watchers.length
|
|
282
|
+
? `${watchers.length} tail/sleep/shell mồ côi quá ${minK} phút (${wMb} MB) — xem rồi dọn: \`dieu-phoi reap --orphans\` (dry-run) rồi thêm --yes (doctor không tự kill)`
|
|
283
|
+
: '';
|
|
284
|
+
if (!found.length) {
|
|
285
|
+
return wTxt ? [false, 'orphan-processes', `không có claude.exe / codebase-memory-mcp mồ côi quá 6 giờ; ${wTxt}`, 'CẢNH BÁO']
|
|
286
|
+
: [true, 'orphan-processes', 'không có claude.exe / codebase-memory-mcp mồ côi quá 6 giờ'];
|
|
287
|
+
}
|
|
288
|
+
const mb = found.reduce((n, p) => n + (p.ws_mb || 0), 0);
|
|
289
|
+
const shown = found.slice(0, 8);
|
|
290
|
+
const items = shown.map((p) => `${p.name} pid ${p.pid} (${p.age_h} giờ${p.ws_mb ? `, ${p.ws_mb} MB` : ''})`).join('; ');
|
|
291
|
+
return [false, 'orphan-processes',
|
|
292
|
+
`${found.length} tiến trình mồ côi (cha đã thoát) quá 6 giờ, ${mb} MB: ${items}${found.length > shown.length ? '; …' : ''}`
|
|
293
|
+
+ ` — kiểm tra rồi dọn bằng \`Stop-Process -Id ${shown.map((p) => p.pid).join(',')}\` (doctor không tự kill)${wTxt ? `; ${wTxt}` : ''}`, 'CẢNH BÁO'];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** `state` line: orphan count from a 10-minute cache (data/orphan-procs.json), else one live scan. null = unknown (skip the line). */
|
|
297
|
+
function orphanCount(ctx, deps) {
|
|
298
|
+
const d = deps || {};
|
|
299
|
+
const now = d.now === undefined ? Date.now() : d.now;
|
|
300
|
+
const file = path.join(ctx.paths.data, 'orphan-procs.json');
|
|
301
|
+
const c = core.readJsonFile(file, null);
|
|
302
|
+
const at = c && typeof c.ts === 'string' ? Date.parse(c.ts) : NaN;
|
|
303
|
+
if (c && Number.isFinite(at) && now - at >= 0 && now - at < ORPHAN_CACHE_MS) return typeof c.n === 'number' ? c.n : null;
|
|
304
|
+
const found = orphanScan((d.listProcesses || procs.listProcesses)({ timeoutMs: 4000 }), now);
|
|
305
|
+
try {
|
|
306
|
+
core.writeFileAtomic(file, `${JSON.stringify({ ts: new Date(now).toISOString(), n: found ? found.length : null })}\n`);
|
|
307
|
+
} catch (_) { /* the cache is optional */ }
|
|
308
|
+
return found ? found.length : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
225
311
|
const TMP_RE = /\.tmp-\d+-[0-9a-f]+$/;
|
|
226
312
|
const TMP_STALE_MS = 3600 * 1000;
|
|
227
313
|
|
|
@@ -582,28 +668,79 @@ function rootOfTask(ctx, taskId) {
|
|
|
582
668
|
}
|
|
583
669
|
|
|
584
670
|
/**
|
|
585
|
-
*
|
|
586
|
-
* (
|
|
587
|
-
*
|
|
671
|
+
* Run dirs that hold knowledge deltas of `tasks` (retro 11-05 K1). The delta of a task lives where its agent was told to
|
|
672
|
+
* write (the dispatch `run`, 0.7.2), which is not always runs/<root>: a loose task or one outside the approved plan has
|
|
673
|
+
* root = itself yet its brief points at the session's run. Order: runs/<root> (also takes untagged deltas when
|
|
674
|
+
* `untagged`), the dispatch `run` of each task, then any other run dir with a file tagged with one of the tasks.
|
|
675
|
+
* [{ id, dir, files, untagged }]; `scanned` = how many run dirs the fallback looked at; `rootDirExists`.
|
|
588
676
|
*/
|
|
589
|
-
function
|
|
590
|
-
if (opts['no-knowledge']) return null;
|
|
677
|
+
function deltaRunDirs(root, tasks, events, untagged) {
|
|
591
678
|
const kn = require('./knowledge.cjs');
|
|
592
|
-
const
|
|
593
|
-
const
|
|
679
|
+
const runsRoot = core.runsDir();
|
|
680
|
+
const want = new Set(tasks);
|
|
681
|
+
const seen = new Set();
|
|
682
|
+
const found = [];
|
|
683
|
+
let scanned = 0;
|
|
684
|
+
const consider = (id, withUntagged) => {
|
|
685
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id) || seen.has(id)) return false;
|
|
686
|
+
seen.add(id);
|
|
687
|
+
const dir = path.join(runsRoot, id);
|
|
688
|
+
try {
|
|
689
|
+
if (!fs.statSync(dir).isDirectory()) return false;
|
|
690
|
+
const files = kn.deltaFiles(dir, { tasks: want, untagged: withUntagged });
|
|
691
|
+
if (files.length) found.push({ id, dir, files, untagged: withUntagged });
|
|
692
|
+
return true;
|
|
693
|
+
} catch (_) { return false; }
|
|
694
|
+
};
|
|
695
|
+
const rootDirExists = consider(root, untagged);
|
|
696
|
+
for (const e of events || []) if (e && e.kind === 'dispatch' && want.has(e.task_id)) consider(e.run, false);
|
|
594
697
|
try {
|
|
595
|
-
|
|
596
|
-
} catch (_) {
|
|
698
|
+
for (const id of fs.readdirSync(runsRoot)) { scanned++; consider(id, false); }
|
|
699
|
+
} catch (_) { /* no runs dir */ }
|
|
700
|
+
return { found, scanned, rootDirExists };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Gate 3 (core 0.6.0): a user accept merges the knowledge deltas of the accepted tasks (knowledge-delta-<task_id>
|
|
705
|
+
* [-r<n>|-h<n>].md; on --root also the root's own and untagged ones) wherever they are (see deltaRunDirs). Never fails
|
|
706
|
+
* the verdict: conflicts/rejections are reported with the command to retry. --no-knowledge skips it.
|
|
707
|
+
* 0.7.2: never silent. It appends a local `knowledge_merge {code, files, runs}` event on every call (also with 0 files),
|
|
708
|
+
* and says so when no delta was found although the root's run dir is missing. Returns { code, out, files }.
|
|
709
|
+
*/
|
|
710
|
+
function knowledgeOnAccept(opts, ctx, root, tasks, untagged, events) {
|
|
711
|
+
if (opts['no-knowledge']) return null;
|
|
712
|
+
const kn = require('./knowledge.cjs');
|
|
713
|
+
const { found, scanned, rootDirExists } = deltaRunDirs(root, tasks, events, untagged);
|
|
714
|
+
const nFiles = found.reduce((n, f) => n + f.files.length, 0);
|
|
597
715
|
const taskArg = tasks.join(',');
|
|
598
|
-
const
|
|
599
|
-
let
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
716
|
+
const outs = [];
|
|
717
|
+
let code = 0;
|
|
718
|
+
for (const f of found) {
|
|
719
|
+
const retry = `dieu-phoi knowledge merge --run ${f.id} --task ${taskArg}${f.untagged ? ' --untagged' : ''}`;
|
|
720
|
+
let r;
|
|
721
|
+
try {
|
|
722
|
+
r = kn.cmdMerge({ _: [], run: f.dir, task: taskArg, untagged: f.untagged, repo: opts.repo });
|
|
723
|
+
} catch (e) {
|
|
724
|
+
code = Math.max(code, 1);
|
|
725
|
+
outs.push(`knowledge: gộp lỗi (${e.message}) — chạy lại: ${retry}`);
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
code = Math.max(code, r.code);
|
|
729
|
+
const tail = r.code ? `\n→ có mục chưa gộp: get lại mục đó, chép dòng base: mới vào delta (hoặc bỏ mục), rồi ${retry}` : '';
|
|
730
|
+
outs.push(`knowledge (tự gộp khi accept, run ${f.id}):\n${r.out}${tail}`);
|
|
604
731
|
}
|
|
605
|
-
|
|
606
|
-
|
|
732
|
+
let note = '';
|
|
733
|
+
if (!nFiles) {
|
|
734
|
+
// nothing to merge is normal for most tasks; say it only when the run dir the root names is missing (the s1 silent case)
|
|
735
|
+
if (!rootDirExists) note = `knowledge: không có delta của ${taskArg} (run ${root} không tồn tại; đã quét ${scanned} run dir theo tên knowledge-delta-<task>*).`;
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
core.appendEvent(ctx, Object.assign({
|
|
739
|
+
schema: core.SCHEMA, kind: 'knowledge_merge', ts: core.nowIso(), agent_id: `km:${tasks[0]}`, task_id: tasks[0],
|
|
740
|
+
code, files: nFiles, runs: found.map((f) => f.id), tasks: tasks.slice(0, 20),
|
|
741
|
+
}, core.commonFields(ctx)));
|
|
742
|
+
} catch (_) { /* the log is best effort here: the merge result is still printed */ }
|
|
743
|
+
return { code, files: nFiles, out: outs.concat(note ? [note] : []).join('\n') };
|
|
607
744
|
}
|
|
608
745
|
|
|
609
746
|
function cmdVerdict(opts) {
|
|
@@ -613,27 +750,33 @@ function cmdVerdict(opts) {
|
|
|
613
750
|
const list = verdictRoot(opts, ctx, events);
|
|
614
751
|
for (const x of list) core.appendEvent(ctx, x.ev);
|
|
615
752
|
const root = String(opts.root);
|
|
616
|
-
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true);
|
|
753
|
+
const kn = knowledgeOnAccept(opts, ctx, root, [root].concat(list.map((x) => x.ev.task_id)), true, events);
|
|
754
|
+
// s17: accept của người dùng gỡ worktree có dấu của từng bước (sạch); còn thay đổi thì giữ và báo.
|
|
755
|
+
const wtLines = list.reduce((acc, x) => acc.concat(wt.cleanupTask(ctx, events, x.ev.task_id, opts.repo)), []);
|
|
617
756
|
if (!list.length) {
|
|
618
757
|
// Every step was already judged (e.g. accepted one by one with --task): still merge the root + untagged deltas.
|
|
619
758
|
// Exit code follows the merge (0 merged or nothing new, 2 conflict or reject, 1 error); no delta at all keeps the old "nothing to do" error.
|
|
620
|
-
if (!kn) throw new Error(list.emptyMsg);
|
|
759
|
+
if (!kn || !kn.files) throw new Error(`${list.emptyMsg}${kn && kn.out ? `\n${kn.out}` : ''}`);
|
|
621
760
|
const msg = `Không còn bước nào của ${root} chờ nghiệm thu; chỉ gộp delta của root và delta không thẻ.`;
|
|
622
761
|
if (opts.json) { process.stderr.write(`${msg}\n${kn.out}\n`); return { code: kn.code, out: '[]' }; }
|
|
623
762
|
return { code: kn.code, out: `${msg}\n${kn.out}` };
|
|
624
763
|
}
|
|
625
764
|
if (opts.json) {
|
|
626
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
765
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
766
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
627
767
|
return { code: 0, out: JSON.stringify(list.map((x) => x.ev)) };
|
|
628
768
|
}
|
|
629
769
|
const msg = `Đã ghi người dùng accept cho ${list.length} bước của ${opts.root}: ${list.map((x) => `${x.step}=${x.ev.task_id}`).join(', ')}.`;
|
|
630
|
-
return { code: 0, out: kn ?
|
|
770
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
631
771
|
}
|
|
632
772
|
const ev = typeof opts.task === 'string' ? verdictUser(opts, ctx, events) : verdictAgent(opts, ctx, events);
|
|
633
773
|
core.appendEvent(ctx, ev);
|
|
634
|
-
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false) : null;
|
|
774
|
+
const kn = ev.by === 'user' && ev.outcome === 'accept' ? knowledgeOnAccept(opts, ctx, rootOfTask(ctx, ev.task_id), [ev.task_id], false, events) : null;
|
|
775
|
+
// s17: người dùng accept → gỡ worktree có dấu của task (sạch); reject giữ nguyên để làm lại.
|
|
776
|
+
const wtLines = ev.by === 'user' && ev.outcome === 'accept' ? wt.cleanupTask(ctx, events, ev.task_id, opts.repo) : [];
|
|
635
777
|
if (opts.json) {
|
|
636
|
-
if (kn) process.stderr.write(`${kn.out}\n`);
|
|
778
|
+
if (kn && kn.out) process.stderr.write(`${kn.out}\n`);
|
|
779
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
637
780
|
return { code: 0, out: JSON.stringify(ev) };
|
|
638
781
|
}
|
|
639
782
|
if (ev.by === 'auto') {
|
|
@@ -641,7 +784,7 @@ function cmdVerdict(opts) {
|
|
|
641
784
|
}
|
|
642
785
|
if (ev.by === 'user') {
|
|
643
786
|
const msg = `Đã ghi người dùng ${ev.outcome}${ev.cause ? ` (${ev.cause})` : ''} cho task ${ev.task_id}, vòng ${ev.round}.`;
|
|
644
|
-
return { code: 0, out: kn ?
|
|
787
|
+
return { code: 0, out: [msg].concat(wtLines, kn && kn.out ? [kn.out] : []).join('\n') };
|
|
645
788
|
}
|
|
646
789
|
return { code: 0, out: `Đã ghi ${ev.outcome} cho ${ev.agent_id}${ev.task_id ? ` (task ${ev.task_id})` : ''}.` };
|
|
647
790
|
}
|
|
@@ -665,8 +808,14 @@ function cmdDiscard(opts) {
|
|
|
665
808
|
}, core.commonFields(ctx));
|
|
666
809
|
core.appendEvent(ctx, ev);
|
|
667
810
|
const n = events.filter((e) => e.task_id === taskId).length;
|
|
668
|
-
|
|
669
|
-
|
|
811
|
+
// s17: task bị loại → gỡ worktree có dấu của task (sạch); còn thay đổi thì giữ và báo.
|
|
812
|
+
const wtLines = wt.cleanupTask(ctx, events, taskId, opts.repo);
|
|
813
|
+
if (opts.json) {
|
|
814
|
+
if (wtLines.length) process.stderr.write(`${wtLines.join('\n')}\n`);
|
|
815
|
+
return { code: 0, out: JSON.stringify(ev) };
|
|
816
|
+
}
|
|
817
|
+
const msg = `Đã loại task ${taskId} (${n} bản ghi) khỏi thống kê, choose và state. Log không bị sửa; sink nhận bản ghi discard ở lần sync kế tiếp.`;
|
|
818
|
+
return { code: 0, out: [msg].concat(wtLines).join('\n') };
|
|
670
819
|
}
|
|
671
820
|
|
|
672
821
|
// ------------------------------------------------------------------ steer
|
|
@@ -748,7 +897,10 @@ function measureOrch(opts, ctx, prof) {
|
|
|
748
897
|
}
|
|
749
898
|
|
|
750
899
|
const CTX_WARN_KEEP_MS = 14 * 24 * 3600 * 1000;
|
|
751
|
-
|
|
900
|
+
// core 0.7.2 (s11 9y3nh4cb, log 62 phiên / 4.749 lượt điều phối, số liệu trong dp-out): 400k -> 250k. Chi phí điều
|
|
901
|
+
// phối mỗi lượt đứng ~0,27–0,29 $ tới 250k rồi nhảy 0,45 $ ở 250–400k; median ctx trước bước nén 211k (150–250k). Bắt buộc
|
|
902
|
+
// < SESSION_CAP_TOKENS (800k) và < SESSION_CAP_COMPACTIONS (10) — test ctx-compact-count bảo vệ bất biến này.
|
|
903
|
+
const ORCH_NEW_SESSION_TOKENS = 250000;
|
|
752
904
|
const ORCH_NEW_SESSION_COMPACTIONS = 5;
|
|
753
905
|
|
|
754
906
|
/** 0 = within the cap; 1 = over the cap, 2 = over 2×, 3 = over 4×, … */
|
|
@@ -815,22 +967,244 @@ function orchLine(o) {
|
|
|
815
967
|
* Orchestration state of one session rebuilt purely from events.jsonl (same builder the SessionStart(compact)
|
|
816
968
|
* hook uses). Session = --session, else env CLAUDE_CODE_SESSION_ID. Run dirs are pointed to, never read.
|
|
817
969
|
*/
|
|
970
|
+
/** Free slots under a cap (null = cap off). */
|
|
971
|
+
function freeSlots(cap, n) {
|
|
972
|
+
return cap > 0 ? Math.max(0, cap - (Number(n) || 0)) : null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** `state` line: live agents of this session / machine and free slots under the parallel caps. */
|
|
976
|
+
function liveLine(live) {
|
|
977
|
+
const lim = live.limits;
|
|
978
|
+
const part = (n, cap) => `${n}/${cap > 0 ? cap : 'tắt'}${cap > 0 ? ` (trống ${freeSlots(cap, n)})` : ''}`;
|
|
979
|
+
const tasks = live.agents.filter((a) => a.task_id).slice(0, 6).map((a) => `${a.task_id}:${a.role}`).join(', ');
|
|
980
|
+
return `live: phiên ${part(live.session || 0, lim.session)} · máy ${part(live.machine, lim.machine)}${tasks ? ` — ${tasks}` : ''}`;
|
|
981
|
+
}
|
|
982
|
+
|
|
818
983
|
function cmdState(opts) {
|
|
819
984
|
const ctx = ctxOf(opts);
|
|
820
985
|
const sid = typeof opts.session === 'string' && opts.session ? opts.session
|
|
821
986
|
: (process.env.CLAUDE_CODE_SESSION_ID || null);
|
|
822
987
|
// retro C6: knowledge deltas older than 24 h that no store has applied (file reads only, never throws)
|
|
823
|
-
const deltas = core.
|
|
988
|
+
const deltas = core.scanUnmergedDeltasFor(ctx);
|
|
824
989
|
const deltaLines = core.unmergedDeltaLines(deltas);
|
|
990
|
+
const hasDeltas = core.hasUnmergedDeltas(deltas);
|
|
825
991
|
if (!sid) {
|
|
826
992
|
if (opts.json) {
|
|
827
|
-
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] },
|
|
993
|
+
return { code: 0, out: JSON.stringify(Object.assign({ session_id: null, generated_at: core.nowIso(), runs_dir: null, plans: [], tasks: [] }, hasDeltas ? { unmerged_deltas: deltas } : {})) };
|
|
828
994
|
}
|
|
829
995
|
return { code: 0, out: ['Không có session (thiếu CLAUDE_CODE_SESSION_ID, dùng --session <id>).'].concat(deltaLines).join('\n') };
|
|
830
996
|
}
|
|
831
997
|
const state = core.buildSessionState(core.readEvents(ctx), sid, { plansDir: ctx.paths.plans });
|
|
832
|
-
|
|
833
|
-
|
|
998
|
+
const orphans = orphanCount(ctx); // s16: null (unknown) leaves the line out
|
|
999
|
+
const live = core.liveSummary(ctx, sid); // core 0.7.3 (s1)
|
|
1000
|
+
const stuck = core.stuckAgents(ctx, { sessionId: sid }); // s6
|
|
1001
|
+
if (opts.json) {
|
|
1002
|
+
const liveJ = {
|
|
1003
|
+
session: live.session, machine: live.machine, limits: live.limits, free_session: freeSlots(live.limits.session, live.session), free_machine: freeSlots(live.limits.machine, live.machine), agents: live.agents,
|
|
1004
|
+
stuck: stuck.map((a) => ({ agent_id: a.agent_id, task_id: a.task_id, role: a.role, gen: a.gen, state: a.state, reasons: a.reasons, idle_min: a.idle_min, age_min: a.age_min })),
|
|
1005
|
+
};
|
|
1006
|
+
return { code: 0, out: JSON.stringify(Object.assign({}, state, hasDeltas ? { unmerged_deltas: deltas } : {}, orphans === null ? {} : { orphan_procs: orphans }, { live: liveJ }), null, 2) };
|
|
1007
|
+
}
|
|
1008
|
+
const stuckLines = stuck.length
|
|
1009
|
+
? [`stuck: ${stuck.slice(0, 6).map((a) => `${a.task_id || a.agent_id}:${a.role} ${a.state === 'orphan' ? 'mồ côi' : 'kẹt'} ${a.idle_min}'`).join(', ')} → TaskStop rồi \`dieu-phoi reap --yes\``]
|
|
1010
|
+
: [];
|
|
1011
|
+
return { code: 0, out: [core.formatSessionState(state, core.STATE_TEXT_CAP)].concat(deltaLines, orphans === null ? [] : [`orphan_procs: ${orphans}`], [liveLine(live)], stuckLines).join('\n') };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ------------------------------------------------------------------ reap (core 0.7.3 s6)
|
|
1015
|
+
|
|
1016
|
+
const posixPath = (p) => String(p || '').replace(/\\/g, '/');
|
|
1017
|
+
const normPath = (p) => String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Child processes attributable to a stuck agent: created after it started, command line inside its cwd (worktree), not a
|
|
1021
|
+
* Claude Code / code-graph / conhost process, not this process' own chain. Only when no other live agent shares that cwd —
|
|
1022
|
+
* otherwise attribution is a guess. s7: `targets` / `skips` = what the guarded kill may take / keeps and why. Pure.
|
|
1023
|
+
*/
|
|
1024
|
+
function reapProcsOf(a, list, sharedCwds, selfPid) {
|
|
1025
|
+
if (!Array.isArray(list) || !a.cwd) return { procs: [], attributable: false };
|
|
1026
|
+
const cwd = normPath(a.cwd);
|
|
1027
|
+
if (!cwd || sharedCwds.has(cwd)) return { procs: [], attributable: false };
|
|
1028
|
+
const own = new Set([selfPid, ...procs.ancestors(list, selfPid).map((p) => p.pid)]);
|
|
1029
|
+
const skip = new Set(procs.LEFTOVER_SKIP_NAMES.map((n) => n.toLowerCase()));
|
|
1030
|
+
const hits = list.filter((p) => !own.has(p.pid) && p.created && p.created >= a.start_ms && !skip.has(String(p.name || '').toLowerCase())
|
|
1031
|
+
&& normPath(p.cmd).includes(cwd));
|
|
1032
|
+
const ids = new Set(hits.map((p) => p.pid));
|
|
1033
|
+
// s7 guarded: of those, what may be killed — never-kill names, nothing hosting a claude.exe, and under a live claude.exe
|
|
1034
|
+
// only through an agent shell (bash / powershell), never through an MCP server. Below no claude.exe = the agent's shell
|
|
1035
|
+
// died (TaskStop) and left it; the worktree cmd already ties it to this agent.
|
|
1036
|
+
const targets = [];
|
|
1037
|
+
const skips = [];
|
|
1038
|
+
for (const p of hits) {
|
|
1039
|
+
const c = procs.claudeAncestor(list, p.pid);
|
|
1040
|
+
if (c) {
|
|
1041
|
+
const sel = procs.guardedSelect(list, [p], { rootPid: c.pid, exclusive: true });
|
|
1042
|
+
if (sel.kill.length) targets.push(p); else skips.push(sel.skip[0]);
|
|
1043
|
+
} else if (procs.isNeverKill(p)) skips.push({ pid: p.pid, name: p.name, reason: 'never-kill' });
|
|
1044
|
+
else if (procs.hostsClaude(list, p.pid)) skips.push({ pid: p.pid, name: p.name, reason: 'hosts-claude' });
|
|
1045
|
+
else targets.push(p);
|
|
1046
|
+
}
|
|
1047
|
+
return { procs: hits, roots: hits.filter((p) => !ids.has(p.ppid)), attributable: true, targets, skips };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Orphans that `reap --orphans` may kill (procs.killableOrphans), and on Windows only those the MSYS table also calls
|
|
1052
|
+
* orphaned (Git-bash exec makes live `bash -c "tail -f"` look parentless). deps.msys = table|null for tests; otherwise
|
|
1053
|
+
* read on win32 only. Returns { found, skipped: [{pid, name, reason}] }.
|
|
1054
|
+
*/
|
|
1055
|
+
function orphanTargets(list, now, minMin, d) {
|
|
1056
|
+
const cands = procs.killableOrphans(list, { now, minAgeMs: minMin * 60000 });
|
|
1057
|
+
const useMsys = Object.prototype.hasOwnProperty.call(d || {}, 'msys') || process.platform === 'win32';
|
|
1058
|
+
if (!useMsys || !cands.length) return { found: cands, skipped: [] };
|
|
1059
|
+
const msys = Object.prototype.hasOwnProperty.call(d || {}, 'msys') ? d.msys : procs.msysTable({ hints: cands.map((p) => p.cmd) });
|
|
1060
|
+
const r = procs.msysConfirm(cands, msys);
|
|
1061
|
+
return { found: r.keep, skipped: r.skip };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/** `orphan_kill_min` (s7): env DIEU_PHOI_ORPHAN_KILL_MIN → config → 60. Minutes ≥ 1 (0 / junk = the default). */
|
|
1065
|
+
const ORPHAN_KILL_MIN_DEFAULT = 60;
|
|
1066
|
+
function orphanKillMin(ctx) {
|
|
1067
|
+
for (const v of [process.env.DIEU_PHOI_ORPHAN_KILL_MIN, ctx.config && ctx.config.orphan_kill_min]) {
|
|
1068
|
+
if (v === undefined || v === null || v === '') continue;
|
|
1069
|
+
const n = Number(v);
|
|
1070
|
+
if (Number.isFinite(n) && n >= 1) return n;
|
|
1071
|
+
}
|
|
1072
|
+
return ORPHAN_KILL_MIN_DEFAULT;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* `dieu-phoi reap [--yes] [--session <id>] [--json]`: stuck / orphaned live agents (data/live) of every session on this
|
|
1077
|
+
* DIEU_PHOI_HOME, or one session. Dry-run by default. --yes deletes their live files (frees the parallel-cap slot; a live
|
|
1078
|
+
* agent's next --watch re-registers it), writes one `reap` event each (local only), and kills their child processes only
|
|
1079
|
+
* when attributable to that agent alone: kill_leftovers 'guarded' (default, s7) = reapProcsOf targets, each re-checked
|
|
1080
|
+
* (pid + creation time) right before; true = the 0.7.2 tree kill; false = never. Worktrees are never removed.
|
|
1081
|
+
* --orphans adds tail / sleep / Claude Code shells orphaned for over orphan_kill_min (60) minutes (killed with --yes).
|
|
1082
|
+
* deps (tests): { procs: list|null, relist() -> list (default: procs again), kill(pids) -> pids, now }.
|
|
1083
|
+
*/
|
|
1084
|
+
function cmdReap(opts, deps) {
|
|
1085
|
+
const d = deps || {};
|
|
1086
|
+
const ctx = ctxOf(opts);
|
|
1087
|
+
const yes = opts.yes === true || opts.yes === 'y' || opts.yes === 'yes';
|
|
1088
|
+
const sid = typeof opts.session === 'string' && opts.session ? opts.session : null;
|
|
1089
|
+
const now = typeof d.now === 'number' ? d.now : Date.now();
|
|
1090
|
+
const lim = core.stuckLimits(ctx);
|
|
1091
|
+
const all = core.readLive(ctx, now).agents;
|
|
1092
|
+
const cands = core.stuckAgents(ctx, { now, limits: lim, sessionId: sid });
|
|
1093
|
+
const candIds = new Set(cands.map((a) => a.agent_id));
|
|
1094
|
+
// cwd shared with another agent (live and not reaped, or a second candidate) = processes cannot be told apart
|
|
1095
|
+
const cwdCount = new Map();
|
|
1096
|
+
for (const r of all) { const c = normPath(r.cwd); if (c) cwdCount.set(c, (cwdCount.get(c) || 0) + 1); }
|
|
1097
|
+
const shared = new Set(Array.from(cwdCount).filter(([, n]) => n > 1).map(([c]) => c));
|
|
1098
|
+
const wantOrphans = opts.orphans === true;
|
|
1099
|
+
const readList = () => (Object.prototype.hasOwnProperty.call(d, 'procs') ? d.procs : procs.listProcesses({ timeoutMs: 5000 }));
|
|
1100
|
+
const list = cands.length || wantOrphans ? readList() : null;
|
|
1101
|
+
// re-read right before killing (pid reuse); tests pass d.relist (a later process table) or reuse d.procs
|
|
1102
|
+
const relist = typeof d.relist === 'function' ? d.relist : readList;
|
|
1103
|
+
const mode = procs.killMode(ctx.config);
|
|
1104
|
+
const killOn = mode !== 'off';
|
|
1105
|
+
const killVal = procs.killModeValue(mode);
|
|
1106
|
+
const kill = typeof d.kill === 'function' ? d.kill : null;
|
|
1107
|
+
const rows = [];
|
|
1108
|
+
for (const a of cands) {
|
|
1109
|
+
const pr = reapProcsOf(a, list, shared, process.pid);
|
|
1110
|
+
const row = Object.assign({}, a, {
|
|
1111
|
+
procs: pr.procs.map((p) => ({ pid: p.pid, name: p.name })), procs_attributable: pr.attributable,
|
|
1112
|
+
kill_targets: mode === 'guarded' && pr.targets ? pr.targets.map((p) => p.pid) : [],
|
|
1113
|
+
kill_skips: mode === 'guarded' && pr.skips ? pr.skips : [],
|
|
1114
|
+
killed: [], removed: false,
|
|
1115
|
+
});
|
|
1116
|
+
delete row.start_ms; delete row.notified;
|
|
1117
|
+
if (yes) {
|
|
1118
|
+
if (mode === 'tree' && pr.roots && pr.roots.length) {
|
|
1119
|
+
row.killed = (kill ? kill(pr.roots.map((p) => p.pid)) : procs.killTree(pr.roots.map((p) => p.pid), { deadline: Date.now() + 10000 })) || [];
|
|
1120
|
+
} else if (mode === 'guarded' && pr.targets && pr.targets.length) {
|
|
1121
|
+
const res = procs.killVerified(pr.targets, { relist, kill, deadline: Date.now() + 10000 });
|
|
1122
|
+
row.killed = res.killed;
|
|
1123
|
+
row.kill_skips = row.kill_skips.concat(res.skipped);
|
|
1124
|
+
}
|
|
1125
|
+
row.removed = core.removeLive(ctx, a.agent_id);
|
|
1126
|
+
try {
|
|
1127
|
+
core.appendEvent(ctx, Object.assign({
|
|
1128
|
+
schema: core.SCHEMA, kind: 'reap', ts: core.nowIso(), agent_id: a.agent_id, task_id: a.task_id, role: a.role, gen: a.gen,
|
|
1129
|
+
parent_task_id: a.parent_task_id, session: a.session_id ? core.sha8(a.session_id) : null, reap_state: a.state,
|
|
1130
|
+
reasons: a.reasons, idle_min: a.idle_min, age_min: a.age_min, worktree: a.cwd ? posixPath(a.cwd) : null,
|
|
1131
|
+
procs: row.procs.length, killed: row.killed.length, killed_pids: row.killed.slice(0, 20), kill_leftovers: killVal, removed: row.removed,
|
|
1132
|
+
}, row.kill_skips.length ? { kill_skips: row.kill_skips.slice(0, 20).map((s) => ({ pid: s.pid, name: String(s.name || '').toLowerCase(), reason: s.reason })) } : {},
|
|
1133
|
+
core.commonFields(ctx)));
|
|
1134
|
+
} catch (e) { core.logError(ctx.home, e, 'reap-event'); }
|
|
1135
|
+
}
|
|
1136
|
+
rows.push(row);
|
|
1137
|
+
}
|
|
1138
|
+
const ok = all.filter((r) => !candIds.has(r.agent_id) && (!sid || r.session_id === sid)).length;
|
|
1139
|
+
// s7: `--orphans` — tail / sleep / Claude Code shells whose parent is gone, older than orphan_kill_min. Dry-run lists them;
|
|
1140
|
+
// --yes kills each after re-checking pid + creation time (never with kill_leftovers:false). One local event reap_orphans.
|
|
1141
|
+
let orph = null;
|
|
1142
|
+
if (wantOrphans) {
|
|
1143
|
+
const minMin = orphanKillMin(ctx);
|
|
1144
|
+
const ot = Array.isArray(list) ? orphanTargets(list, now, minMin, d) : { found: [], skipped: [] };
|
|
1145
|
+
const found = ot.found;
|
|
1146
|
+
orph = {
|
|
1147
|
+
min_age_min: minMin, listed: Array.isArray(list), n: found.length,
|
|
1148
|
+
procs: found.map((p) => ({ pid: p.pid, name: p.name, age_min: Math.round((now - p.created) / 60000), cmd: p.cmd ? String(p.cmd).slice(0, 100) : null })),
|
|
1149
|
+
killed: [], skipped: [], kept: ot.skipped,
|
|
1150
|
+
};
|
|
1151
|
+
if (yes && killOn && found.length) {
|
|
1152
|
+
const res = procs.killVerified(found, { relist, kill, deadline: Date.now() + Math.max(20000, found.length * 3000) });
|
|
1153
|
+
orph.killed = res.killed;
|
|
1154
|
+
orph.skipped = res.skipped;
|
|
1155
|
+
}
|
|
1156
|
+
if (yes) {
|
|
1157
|
+
try {
|
|
1158
|
+
core.appendEvent(ctx, Object.assign({
|
|
1159
|
+
schema: core.SCHEMA, kind: 'reap_orphans', ts: core.nowIso(), n: found.length, killed: orph.killed.length,
|
|
1160
|
+
killed_pids: orph.killed.slice(0, 50), skipped: orph.skipped.slice(0, 20).map((s) => ({ pid: s.pid, reason: s.reason })),
|
|
1161
|
+
names: Array.from(new Set(found.map((p) => String(p.name || '').toLowerCase()))).slice(0, 8), min_age_min: minMin, kill_leftovers: killVal,
|
|
1162
|
+
}, core.commonFields(ctx)));
|
|
1163
|
+
} catch (e) { core.logError(ctx.home, e, 'reap-orphans-event'); }
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (opts.json) {
|
|
1167
|
+
return { code: 0, out: JSON.stringify(Object.assign({ dry_run: !yes, limits: lim, kill_leftovers: killVal, procs_listed: Array.isArray(list), live_ok: ok, agents: rows }, orph ? { orphans: orph } : {}), null, 2) };
|
|
1168
|
+
}
|
|
1169
|
+
const head = `reap${yes ? '' : ' (dry-run)'}: ${rows.length} agent kẹt/mồ côi${sid ? ` trong phiên ${core.sha8(sid)}` : ' trên máy'}; ${ok} agent live còn bình thường `
|
|
1170
|
+
+ `(ngưỡng: im ${lim.idle || 'tắt'} phút · tổng ${lim.hard || 'tắt'} phút · sau STATUS ${lim.after_status || 'tắt'} phút).`;
|
|
1171
|
+
const lines = [head];
|
|
1172
|
+
for (const r of rows) {
|
|
1173
|
+
const who = `agent ${r.agent_id} task ${r.task_id || '?'} (${r.role || '?'}, gen ${r.gen}${r.session_id ? `, phiên ${core.sha8(r.session_id)}` : ''})`;
|
|
1174
|
+
const procTxt = !Array.isArray(list) ? 'tiến trình: không đọc được bảng process'
|
|
1175
|
+
: (!r.procs_attributable ? 'tiến trình: không quy được về riêng agent này (không có cwd / cwd dùng chung)'
|
|
1176
|
+
: `tiến trình con: ${r.procs.length ? r.procs.slice(0, 6).map((p) => `${p.name} ${p.pid}`).join(', ') + (r.procs.length > 6 ? ` (+${r.procs.length - 6})` : '') : 'không có'}`);
|
|
1177
|
+
const guardTxt = mode === 'guarded' && r.procs.length
|
|
1178
|
+
? `; sẽ kill ${r.kill_targets.length}${r.kill_targets.length ? ` (${r.kill_targets.slice(0, 6).join(', ')})` : ''}${r.kill_skips.length ? `, giữ ${r.kill_skips.length} [${Array.from(new Set(r.kill_skips.map((s) => s.reason))).join(', ')}]` : ''}`
|
|
1179
|
+
: '';
|
|
1180
|
+
const act = !yes ? '' : ` → ${r.removed ? 'đã xoá file live' : 'file live đã mất'}${r.killed.length ? `, đã kill ${r.killed.length}` : (r.procs.length ? (killOn ? ', không kill được / không có gì quy chắc về agent' : ', không kill (kill_leftovers=false)') : '')}`;
|
|
1181
|
+
lines.push(`- ${who}: ${r.state === 'orphan' ? 'mồ côi' : 'kẹt'} [${core.stuckReasonText(r)}]; chạy ${r.age_min} phút, im ${r.idle_min} phút; ${procTxt}${guardTxt}${r.cwd ? `; worktree ${posixPath(r.cwd)} giữ nguyên` : ''}${act}`);
|
|
1182
|
+
}
|
|
1183
|
+
if (rows.length && !yes) {
|
|
1184
|
+
lines.push('Agent còn sống thì TaskStop <agent_id> trong phiên của nó trước; rồi `dieu-phoi reap --yes` xoá file live (trả chỗ trần) + ghi event reap.'
|
|
1185
|
+
+ (mode === 'guarded' ? ' Chỉ tiến trình ghi "sẽ kill" bị kill (kill_leftovers=guarded: kiểm lại pid + giờ tạo ngay trước khi kill).'
|
|
1186
|
+
: mode === 'tree' ? ' Cây tiến trình con quy được về agent sẽ bị kill (kill_leftovers=true).' : ' Tiến trình con chỉ được liệt kê (kill_leftovers=false).'));
|
|
1187
|
+
}
|
|
1188
|
+
if (orph) {
|
|
1189
|
+
const groups = new Map();
|
|
1190
|
+
for (const p of orph.procs) groups.set(String(p.name).toLowerCase(), (groups.get(String(p.name).toLowerCase()) || 0) + 1);
|
|
1191
|
+
const oldest = orph.procs.length ? Math.max(...orph.procs.map((p) => p.age_min)) : 0;
|
|
1192
|
+
lines.push(!orph.listed ? 'orphans: không đọc được bảng process'
|
|
1193
|
+
: `orphans${yes ? '' : ' (dry-run)'}: ${orph.n} tiến trình mồ côi (cha đã thoát) quá ${orph.min_age_min} phút`
|
|
1194
|
+
+ (orph.n ? ` — ${Array.from(groups).map(([n, c]) => `${n} ×${c}`).join(', ')}; cũ nhất ${Math.round(oldest / 60)} giờ` : ''));
|
|
1195
|
+
if (orph.kept.length) {
|
|
1196
|
+
const why = new Map();
|
|
1197
|
+
for (const k of orph.kept) why.set(k.reason, (why.get(k.reason) || 0) + 1);
|
|
1198
|
+
lines.push(` giữ ${orph.kept.length} cái trông mồ côi nhưng chưa chắc: ${Array.from(why).map(([r, c]) => `${r} ×${c}`).join(', ')} (msys-parent-alive = bash cha còn sống theo MSYS, việc nền đang chạy)`);
|
|
1199
|
+
}
|
|
1200
|
+
for (const p of orph.procs.slice(0, 10)) lines.push(` ${p.name} ${p.pid} (${p.age_min} phút)${p.cmd ? `: ${p.cmd}` : ''}`);
|
|
1201
|
+
if (orph.procs.length > 10) lines.push(` … +${orph.procs.length - 10}`);
|
|
1202
|
+
if (yes) {
|
|
1203
|
+
lines.push(!killOn ? ' → không kill (kill_leftovers=false)'
|
|
1204
|
+
: ` → đã kill ${orph.killed.length}/${orph.n}${orph.skipped.length ? `; bỏ qua ${orph.skipped.length} [${Array.from(new Set(orph.skipped.map((s) => s.reason))).join(', ')}]` : ''}; event reap_orphans`);
|
|
1205
|
+
} else if (orph.n) lines.push(' `dieu-phoi reap --orphans --yes` để kill (kiểm lại pid + giờ tạo ngay trước khi kill).');
|
|
1206
|
+
}
|
|
1207
|
+
return { code: 0, out: lines.join('\n') };
|
|
834
1208
|
}
|
|
835
1209
|
|
|
836
1210
|
// ------------------------------------------------------------------ stats / retro
|
|
@@ -1273,7 +1647,8 @@ function planTypeNotes(plan) {
|
|
|
1273
1647
|
* `choose --override` already logged adds nothing.
|
|
1274
1648
|
*/
|
|
1275
1649
|
function planOverrideEvents(ctx, plan, events) {
|
|
1276
|
-
|
|
1650
|
+
// core 0.7.2 (K4): an implicit override must not hide the user's override of the same tier from the learning
|
|
1651
|
+
const seen = new Set(events.filter((e) => e.kind === 'override' && e.source !== 'implicit').map((e) => `${e.task_id}|${e.chosen_tier}`));
|
|
1277
1652
|
const out = [];
|
|
1278
1653
|
for (const r of plan.steps) {
|
|
1279
1654
|
if (!r.overridden || !r.task_id || !r.tier || !r.suggested_tier) continue;
|
|
@@ -1366,6 +1741,7 @@ function cmdPlanTable(opts) {
|
|
|
1366
1741
|
const base = chooseInput(Object.assign({}, opts, { type: 'x', score: 0, override: undefined }), ctx, core.readEvents(ctx));
|
|
1367
1742
|
delete base.override;
|
|
1368
1743
|
const rootEv = typeof opts.root === 'string' && core.isValidTaskId(opts.root) ? core.rootModeFromEvents(base.events, opts.root) : null;
|
|
1744
|
+
const modeChosen = Boolean(base.mode || rootEv || fileMode);
|
|
1369
1745
|
base.mode = base.mode || (rootEv ? rootEv.mode : null) || fileMode || core.DEFAULT_MODE;
|
|
1370
1746
|
const plan = buildPlanTable(raw, base);
|
|
1371
1747
|
const problems = planTaskIdProblems(plan, opts.approve ? String(opts.root) : null);
|
|
@@ -1402,7 +1778,12 @@ function cmdPlanTable(opts) {
|
|
|
1402
1778
|
}
|
|
1403
1779
|
lines.push('');
|
|
1404
1780
|
lines.push(`**Tổng chi phí ước tính:** ~${fmtUsd(plan.total_cost_usd)} · **Thời gian (đường găng, chạy song song khi không phụ thuộc):** ~${fmtNum(plan.critical_path_min)} phút (cộng dồn ${fmtNum(plan.sum_time_min)} phút)`);
|
|
1405
|
-
|
|
1781
|
+
// Mode được hỏi ở cổng 1 chỉ khi root chưa có mode và kế hoạch có ≥4 bước s≤6 (0.7.2); còn lại không hỏi
|
|
1782
|
+
const shortSteps = plan.steps.filter((r) => r.action !== 'refused' && Number.isFinite(r.score) && r.score <= 6).length;
|
|
1783
|
+
const modeText = modeChosen ? plan.mode : (shortSteps >= 4
|
|
1784
|
+
? `HỎI người dùng chọn mode (${shortSteps} bước s≤6): fast | normal | strict (mặc định normal); đặt bằng \`dieu-phoi mode --root ${typeof opts.root === 'string' ? opts.root : '<root_task_id>'} <mode>\``
|
|
1785
|
+
: 'normal (không hỏi)');
|
|
1786
|
+
lines.push(`**Mode:** ${modeText}${plan.self_batch_steps ? ` · Kiểm (fast): bỏ ${plan.self_batch_steps} verifier riêng, 1 verify gộp (cả suite một lần) trước khi trình người dùng` : ''}`);
|
|
1406
1787
|
if (plan.rough) lines.push('', '_(thô) = ô chưa đủ 5 task, ước lượng theo giả định token của từng tier._');
|
|
1407
1788
|
if (plan.refused.length) {
|
|
1408
1789
|
lines.push('', `**Cảnh báo:** ${plan.refused.length} bước bị từ chối (${plan.refused.map((x) => x.step).join(', ')}), không tính vào tổng — sửa rồi chạy lại.`);
|
|
@@ -1415,6 +1796,6 @@ function cmdPlanTable(opts) {
|
|
|
1415
1796
|
}
|
|
1416
1797
|
|
|
1417
1798
|
module.exports = {
|
|
1418
|
-
skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdMode, modeEvent, cmdCtx, cmdState, measureOrch,
|
|
1799
|
+
ORCH_NEW_SESSION_TOKENS, ORCH_NEW_SESSION_COMPACTIONS, skillDirOf, cmdSetup, cmdUninstall, cmdDoctor, cmdStart, checkSinkLive, cmdHook, cmdSync, cmdVerdict, cmdSteer, cmdDiscard, ctxWarnOnce, ctxWarnLevel, sweepStaleTmp, planOverrideEvents, recordPlanSuggestions, cmdStats, cmdRetro, cmdPrune: (o) => require('./prune.cjs').cmdPrune(o), cmdChoose, cmdMode, modeEvent, cmdCtx, cmdState, cmdReap, reapProcsOf, orphanKillMin, orphanCheck, orphanCount, liveCheck, liveLine, freeSlots, measureOrch,
|
|
1419
1800
|
cmdPlanTable, cmdNewId, requireTaskId, buildPlanTable, recordApprovedPlan, planTaskIdProblems, criticalPath, resolveAgentId, AGENT_OUTCOMES, USER_OUTCOMES, VERIFY,
|
|
1420
1801
|
};
|