@kienntpixon/dieu-phoi 0.7.1 → 0.7.2
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 +21 -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 +14 -2
- package/hook.cjs +576 -44
- package/lib/aggregate.cjs +5 -4
- package/lib/args.cjs +1 -1
- package/lib/commands.cjs +168 -35
- package/lib/knowledge.cjs +3 -4
- package/lib/procs.cjs +223 -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 +56 -8
- package/lib/setup.cjs +31 -7
- package/lib/worktree.cjs +186 -0
- package/package.json +3 -1
- package/profile/lessons.md +83 -0
- package/profile/unity-lanes.md +316 -0
package/hook.cjs
CHANGED
|
@@ -26,7 +26,7 @@ const path = require('path');
|
|
|
26
26
|
const os = require('os');
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
|
|
29
|
-
const CORE_VERSION = '0.7.
|
|
29
|
+
const CORE_VERSION = '0.7.2';
|
|
30
30
|
const SCHEMA = 1;
|
|
31
31
|
const TASK_TYPES = [
|
|
32
32
|
'search', 'extract', 'mechanical-edit', 'feature-impl', 'debug', 'refactor',
|
|
@@ -272,6 +272,8 @@ function parseTag(text) {
|
|
|
272
272
|
tag.parent_task_id = mm[1]; // core 0.5.0: gen-2 helper -> the dp-exec step that spawned it
|
|
273
273
|
} else if (p === 'chain_ok') {
|
|
274
274
|
tag.chain_ok = true; // core 0.7.1 (C1): the user agreed to continue a long handoff chain
|
|
275
|
+
} else if (p === 'session_ok') {
|
|
276
|
+
tag.session_ok = true; // core 0.7.2 (K5): the user agreed to open a new root in an oversized session
|
|
275
277
|
} else if (p === 'batch') {
|
|
276
278
|
tag.batch = true; // core 0.7.1 (C5): the one batched verifier of a fast request
|
|
277
279
|
}
|
|
@@ -331,6 +333,9 @@ const TYPE_ALIASES = {
|
|
|
331
333
|
'art-fix': 'art-gen', art: 'art-gen',
|
|
332
334
|
// retro 2026-10-26 (C2): scout/read labels
|
|
333
335
|
scan: 'search', read: 'search', 'code-read': 'search', explore: 'search', scout: 'search', lookup: 'search',
|
|
336
|
+
// core 0.7.2 (K6, retro 2026-11-05): labels that still arrived as bad-type in the 11-05 logs
|
|
337
|
+
'test-run': 'test-write', 'test-verify': 'test-write', 'device-qa': 'test-write',
|
|
338
|
+
'git-ops': 'mechanical-edit', 'release-build': 'mechanical-edit', 'read-scan': 'search',
|
|
334
339
|
};
|
|
335
340
|
|
|
336
341
|
/** Fixed task types + the profile's extra_task_types (art-gen when no profile is readable). */
|
|
@@ -634,7 +639,7 @@ function summarizeTranscript(lines, opts) {
|
|
|
634
639
|
// ------------------------------------------------------------------ orchestrator (main session) usage
|
|
635
640
|
|
|
636
641
|
const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
637
|
-
const ORCH_CACHE_V = 2; // 2: + compactions
|
|
642
|
+
const ORCH_CACHE_V = 2; // 2: + compactions (0.7.2: a cold-probe cache with compactions null is re-probed, see orchestratorUsage)
|
|
638
643
|
const ORCH_CHUNK = 1 << 20; // read granularity; memory = chunk + longest line
|
|
639
644
|
const ORCH_SEEN_KEEP = 64; // requestIds kept for last-line-wins dedupe across scans
|
|
640
645
|
const ORCH_HEAD_BYTES = 4096; // fingerprint of the file start (rotation / replacement check)
|
|
@@ -682,8 +687,9 @@ function ctxOfBuckets(b) {
|
|
|
682
687
|
* passed to onLine (return true to stop). Returns { consumed: byte just past the last complete line
|
|
683
688
|
* handled, read: bytes read }.
|
|
684
689
|
*/
|
|
685
|
-
function scanUsageLines(fd, start, end, onLine) {
|
|
690
|
+
function scanUsageLines(fd, start, end, onLine, needle) {
|
|
686
691
|
const buf = Buffer.allocUnsafe(ORCH_CHUNK);
|
|
692
|
+
const one = needle || null; // null: usage or compaction lines; else only lines holding `needle`
|
|
687
693
|
let carry = null;
|
|
688
694
|
let pos = start;
|
|
689
695
|
let consumed = start;
|
|
@@ -700,8 +706,9 @@ function scanUsageLines(fd, start, end, onLine) {
|
|
|
700
706
|
while ((nl = data.indexOf(0x0a, s)) >= 0) {
|
|
701
707
|
const line = data.subarray(s, nl);
|
|
702
708
|
s = nl + 1;
|
|
703
|
-
|
|
704
|
-
|
|
709
|
+
const hit = one ? line.indexOf(one) >= 0
|
|
710
|
+
: (line.indexOf(USAGE_NEEDLE) >= 0 || line.indexOf(COMPACT_NEEDLE) >= 0);
|
|
711
|
+
if (hit && onLine(line.toString('utf8')) === true) {
|
|
705
712
|
return { consumed: base + s, read };
|
|
706
713
|
}
|
|
707
714
|
}
|
|
@@ -772,13 +779,22 @@ function edgeProbe(fd, size, st) {
|
|
|
772
779
|
}
|
|
773
780
|
st.cum_known = false;
|
|
774
781
|
st.requests = null;
|
|
775
|
-
st.
|
|
782
|
+
st.offset = lastNl >= 0 ? lastNl + 1 : 0;
|
|
783
|
+
// core 0.7.2 (K5): count compactions over [0, offset) too (byte scan, JSON only on hits). The incremental
|
|
784
|
+
// scan takes over from offset, so nothing is counted twice; before 0.7.2 this stayed null for the session.
|
|
785
|
+
st.compactions = countCompactions(fd, st.offset);
|
|
776
786
|
if (first) {
|
|
777
787
|
st.baseline = ctxOfBuckets(usageBuckets(first.usage));
|
|
778
788
|
st.baseline_req = first.key;
|
|
779
789
|
}
|
|
780
790
|
if (last) st.last_ctx = ctxOfBuckets(usageBuckets(last.usage));
|
|
781
|
-
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Main-thread compaction markers in [0, end) (sidechain and look-alike text ignored). Never throws. */
|
|
794
|
+
function countCompactions(fd, end) {
|
|
795
|
+
let n = 0;
|
|
796
|
+
if (end > 0) scanUsageLines(fd, 0, end, (t) => { const u = parseMainLine(t); if (u && u.compact) n++; return false; }, COMPACT_NEEDLE);
|
|
797
|
+
return n;
|
|
782
798
|
}
|
|
783
799
|
|
|
784
800
|
/** Drop stale session caches. Only ever touches <data>/orch/*.json (never another directory). */
|
|
@@ -814,8 +830,9 @@ function orchCacheFile(paths, sessionId, transcriptPath) {
|
|
|
814
830
|
* opts.cacheFile: per-session state ({offset, cost, seen requestIds, baseline, …}); each call then parses
|
|
815
831
|
* only the bytes appended since the last call. A shrunk or replaced file (offset > size or a different
|
|
816
832
|
* head fingerprint) is recomputed from 0.
|
|
817
|
-
* opts.coldMaxBytes: no usable cache and the file is larger -> head/tail probe
|
|
818
|
-
*
|
|
833
|
+
* opts.coldMaxBytes: no usable cache and the file is larger -> head/tail probe (ctx + baseline, cum_usd null)
|
|
834
|
+
* plus a byte scan of the compaction markers before the probe offset; a later call without the limit
|
|
835
|
+
* (`dieu-phoi ctx`) recomputes the full sum.
|
|
819
836
|
* Reads in chunks (memory = chunk + longest line), never the whole file at once. Never throws.
|
|
820
837
|
*/
|
|
821
838
|
function orchestratorUsage(transcriptPath, pricing, opts) {
|
|
@@ -834,7 +851,8 @@ function orchestratorUsage(transcriptPath, pricing, opts) {
|
|
|
834
851
|
const cached = o.cacheFile ? readJsonFile(o.cacheFile, null) : null;
|
|
835
852
|
if (cached && cached.v === ORCH_CACHE_V && Number.isFinite(cached.offset) && cached.offset <= size
|
|
836
853
|
&& cached.head_len <= size && Array.isArray(cached.seen)
|
|
837
|
-
&& (cached.cum_known || o.coldMaxBytes)) {
|
|
854
|
+
&& (cached.cum_known || (o.coldMaxBytes && typeof cached.compactions === 'number'))) {
|
|
855
|
+
// a probe state written before 0.7.2 holds compactions null: not reused, so the probe counts it (cum_known kept)
|
|
838
856
|
const now = cached.head_len === fp.head_len ? fp : (() => {
|
|
839
857
|
const b = Buffer.alloc(cached.head_len);
|
|
840
858
|
if (cached.head_len) fs.readSync(fd, b, 0, cached.head_len, 0);
|
|
@@ -1184,7 +1202,8 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1184
1202
|
const id = ev.task_id || agentTask.get(ev.agent_id);
|
|
1185
1203
|
if (per.has(id)) per.get(id).steers.push(ev);
|
|
1186
1204
|
}
|
|
1187
|
-
|
|
1205
|
+
// core 0.7.2 (K4): an implicit override is a recorded deviation, not the user's choice: it never moves the approved tier
|
|
1206
|
+
if (ev.kind === 'override' && ev.source !== 'implicit' && per.has(ev.task_id)) per.get(ev.task_id).overrides.push(ev);
|
|
1188
1207
|
}
|
|
1189
1208
|
const byTime = (a, b) => evMs(a) - evMs(b) || a._i - b._i;
|
|
1190
1209
|
const tasks = [];
|
|
@@ -1288,7 +1307,8 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1288
1307
|
mode_suggest: sug.mode_suggest, mode_suggest_reason: sug.mode_suggest_reason,
|
|
1289
1308
|
};
|
|
1290
1309
|
});
|
|
1291
|
-
|
|
1310
|
+
const orphans = findOrphanAgents(events, taskIds.filter((id) => !discarded.has(id)), { nowMs: o.nowMs });
|
|
1311
|
+
return Object.assign({
|
|
1292
1312
|
session_id: sessionId,
|
|
1293
1313
|
generated_at: nowIso(),
|
|
1294
1314
|
runs_dir: `${runs}/`,
|
|
@@ -1300,7 +1320,85 @@ function buildSessionState(events, sessionId, opts) {
|
|
|
1300
1320
|
steps: p.steps.length, run_dir: `${runs}/${p.root_task_id}/`,
|
|
1301
1321
|
})),
|
|
1302
1322
|
tasks,
|
|
1303
|
-
};
|
|
1323
|
+
}, orphans.length ? { orphans } : {});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// retro 2026-11-05 K2: orphan agents. An exec/verify/plan agent whose LAST segment ended without a STATUS:/VERDICT: line
|
|
1327
|
+
// (`complete.ended_without_status`; older events: status "none"), or that never reported ending, and that has no agent
|
|
1328
|
+
// verdict, no newer dispatch of the same task+role and no user/auto accept, is listed so the orchestrator re-dispatches
|
|
1329
|
+
// (try+1) or drops it instead of waiting forever. Pure function of the events; never reads run-dir files.
|
|
1330
|
+
const ORPHAN_MIN_MS = 15 * 60 * 1000;
|
|
1331
|
+
// An agent with no `complete` at all may simply still be running in the background (state is rebuilt right after /compact):
|
|
1332
|
+
// only after this long is it called orphaned, and the line says to ignore it when the agent is still alive.
|
|
1333
|
+
const ORPHAN_NO_COMPLETE_MS = 2 * 3600 * 1000;
|
|
1334
|
+
const ORPHAN_MAX_LINES = 6;
|
|
1335
|
+
|
|
1336
|
+
function findOrphanAgents(events, taskIds, opts) {
|
|
1337
|
+
const now = (opts && opts.nowMs) || Date.now();
|
|
1338
|
+
const want = new Set(taskIds || []);
|
|
1339
|
+
const seenAgent = new Set();
|
|
1340
|
+
const latest = new Map(); // "task|role" -> newest dispatch
|
|
1341
|
+
const lastComplete = new Map();
|
|
1342
|
+
const judged = new Set(); // agent ids with any verdict
|
|
1343
|
+
const accepted = new Set();
|
|
1344
|
+
for (const ev of events) {
|
|
1345
|
+
if (!ev || typeof ev !== 'object') continue;
|
|
1346
|
+
if (ev.kind === 'dispatch' && want.has(ev.task_id) && !seenAgent.has(ev.agent_id)) {
|
|
1347
|
+
const role = ev.role || 'exec';
|
|
1348
|
+
if (!RESULT_ROLES.includes(role)) continue;
|
|
1349
|
+
seenAgent.add(ev.agent_id);
|
|
1350
|
+
latest.set(`${ev.task_id}|${role}`, ev);
|
|
1351
|
+
} else if (ev.kind === 'complete') {
|
|
1352
|
+
lastComplete.set(ev.agent_id, ev);
|
|
1353
|
+
} else if (ev.kind === 'verdict') {
|
|
1354
|
+
if (ev.agent_id) judged.add(ev.agent_id);
|
|
1355
|
+
if (ev.task_id && ev.outcome === 'accept') accepted.add(ev.task_id);
|
|
1356
|
+
else if (ev.task_id && (ev.outcome === 'reject' || ev.outcome === 'rework')) accepted.delete(ev.task_id);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const out = [];
|
|
1360
|
+
for (const d of latest.values()) {
|
|
1361
|
+
if (judged.has(d.agent_id) || accepted.has(d.task_id)) continue;
|
|
1362
|
+
const c = lastComplete.get(d.agent_id);
|
|
1363
|
+
let reason;
|
|
1364
|
+
let since;
|
|
1365
|
+
if (c) {
|
|
1366
|
+
const ended = c.ended_without_status === true || (c.ended_without_status === undefined && c.status === 'none');
|
|
1367
|
+
if (!ended) continue;
|
|
1368
|
+
since = evMs(c);
|
|
1369
|
+
if (!since || now - since < ORPHAN_MIN_MS) continue;
|
|
1370
|
+
reason = c.inline_result === true ? 'inline' : 'no-status';
|
|
1371
|
+
} else {
|
|
1372
|
+
since = evMs(d);
|
|
1373
|
+
if (!since || now - since < ORPHAN_NO_COMPLETE_MS) continue;
|
|
1374
|
+
reason = 'no-complete';
|
|
1375
|
+
}
|
|
1376
|
+
out.push({
|
|
1377
|
+
task_id: d.task_id, agent_id: d.agent_id, role: d.role || 'exec', try: Number(d.try) || 1, task_type: d.task_type || null,
|
|
1378
|
+
score: Number.isFinite(d.score) ? d.score : null, urgency: d.urgency || 'normal', reason, age_min: Math.round((now - since) / 60000),
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
return out;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const ORPHAN_REASON_TEXT = {
|
|
1385
|
+
'no-status': 'kết thúc mà báo cáo cuối không có dòng STATUS:/VERDICT:',
|
|
1386
|
+
inline: 'không ghi file dp-out (hook chỉ lưu .inline.md) và không có dòng STATUS:/VERDICT:',
|
|
1387
|
+
'no-complete': 'không thấy kết thúc (nếu agent còn chạy nền thì bỏ qua dòng này)',
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
/** Lines telling the orchestrator what to do with each orphan agent (at most ORPHAN_MAX_LINES). [] when none. */
|
|
1391
|
+
function orphanLines(orphans) {
|
|
1392
|
+
const list = Array.isArray(orphans) ? orphans : [];
|
|
1393
|
+
if (!list.length) return [];
|
|
1394
|
+
const lines = [`agent mồ côi: ${list.length} lượt giao chưa có kết quả dùng được, chưa verdict, chưa giao lại — đọc dp-out/.inline.md của bước (nếu có) rồi giao lại hoặc bỏ:`];
|
|
1395
|
+
for (const x of list.slice(0, ORPHAN_MAX_LINES)) {
|
|
1396
|
+
lines.push(` - ${x.task_id} [${x.task_type || '?'}] ${x.role} try${x.try} (${x.agent_id}) — ${ORPHAN_REASON_TEXT[x.reason] || x.reason}, ${x.age_min} phút trước; `
|
|
1397
|
+
+ `giao lại: [dp:${x.task_type || '<type>'}|s${x.score === null ? '<n>' : x.score}|${x.urgency}|try${x.try + 1}|task=${x.task_id}|role=${x.role}]; `
|
|
1398
|
+
+ `bỏ: dieu-phoi verdict ${x.agent_id} fail --reason ${x.reason === 'no-complete' ? 'no-complete' : 'ended-without-status'}`);
|
|
1399
|
+
}
|
|
1400
|
+
if (list.length > ORPHAN_MAX_LINES) lines.push(` (+${list.length - ORPHAN_MAX_LINES} nữa — xem đủ: dieu-phoi state --json, khoá orphans)`);
|
|
1401
|
+
return lines;
|
|
1304
1402
|
}
|
|
1305
1403
|
|
|
1306
1404
|
function toPosix(p) {
|
|
@@ -1362,7 +1460,8 @@ function stateCounts(state) {
|
|
|
1362
1460
|
/** One short Vietnamese line shown to the user after /compact re-injected the state. */
|
|
1363
1461
|
function compactSystemMessage(state) {
|
|
1364
1462
|
const c = stateCounts(state);
|
|
1365
|
-
|
|
1463
|
+
const orph = Array.isArray(state.orphans) && state.orphans.length ? `, ${state.orphans.length} agent mồ côi` : '';
|
|
1464
|
+
return `dieu-phoi: đã nạp lại trạng thái điều phối sau khi nén (${c.tasks} task, ${c.running} agent đang chạy, ${c.pending} bước chưa giao${orph})`;
|
|
1366
1465
|
}
|
|
1367
1466
|
|
|
1368
1467
|
/** "\nMode: <root>=<mode> …" for the roots of a session (last 6); '' when the state carries none. */
|
|
@@ -1393,7 +1492,8 @@ function formatSessionState(state, cap) {
|
|
|
1393
1492
|
const render = (omitted) => {
|
|
1394
1493
|
const body = stateBody(state, kept);
|
|
1395
1494
|
const tail = omitted ? `\n(bỏ bớt ${omitted} task cũ nhất để vừa ${limit} ký tự; xem đủ: dieu-phoi state --json)` : '';
|
|
1396
|
-
|
|
1495
|
+
const orph = orphanLines(state.orphans);
|
|
1496
|
+
return `${header}\n${body.join('\n')}${tail}${orph.length ? `\n${orph.join('\n')}` : ''}`;
|
|
1397
1497
|
};
|
|
1398
1498
|
let omitted = 0;
|
|
1399
1499
|
let text = render(0);
|
|
@@ -1578,8 +1678,8 @@ function startInfo(ctx, cwd) {
|
|
|
1578
1678
|
};
|
|
1579
1679
|
const k = startKnowledgeOf(cwd);
|
|
1580
1680
|
if (k) info.knowledge = k;
|
|
1581
|
-
const d =
|
|
1582
|
-
if (d
|
|
1681
|
+
const d = scanUnmergedDeltasFor(ctx);
|
|
1682
|
+
if (hasUnmergedDeltas(d)) info.unmerged_deltas = d;
|
|
1583
1683
|
return info;
|
|
1584
1684
|
}
|
|
1585
1685
|
|
|
@@ -1715,6 +1815,28 @@ const HANDOFF_CHAIN_WARN = 2;
|
|
|
1715
1815
|
const HANDOFF_CHAIN_DENY = 3;
|
|
1716
1816
|
|
|
1717
1817
|
/** Number of agents whose `complete` event has handoff:true for this task (substring scan of events.jsonl). */
|
|
1818
|
+
/**
|
|
1819
|
+
* Core 0.7.2 (K3/K5): whether events.jsonl holds a dispatch for any task in `taskIds` (optionally only of `role`).
|
|
1820
|
+
* Same substring scan as countTaskHandoffs: rare path, one read of the file.
|
|
1821
|
+
*/
|
|
1822
|
+
function hasDispatchFor(ctx, taskIds, role) {
|
|
1823
|
+
let txt;
|
|
1824
|
+
try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { return false; }
|
|
1825
|
+
for (const id of taskIds) {
|
|
1826
|
+
if (!id) continue;
|
|
1827
|
+
const needle = `"task_id":${JSON.stringify(id)}`;
|
|
1828
|
+
let i = txt.indexOf(needle);
|
|
1829
|
+
while (i >= 0) {
|
|
1830
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
1831
|
+
const end = txt.indexOf('\n', i);
|
|
1832
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
1833
|
+
if (line.includes('"kind":"dispatch"') && (!role || line.includes(`"role":${JSON.stringify(role)}`))) return true;
|
|
1834
|
+
i = end < 0 ? -1 : txt.indexOf(needle, end);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1718
1840
|
function countTaskHandoffs(ctx, taskId) {
|
|
1719
1841
|
if (!taskId) return 0;
|
|
1720
1842
|
let txt;
|
|
@@ -1749,9 +1871,9 @@ function handoffChainMessage(taskId, n) {
|
|
|
1749
1871
|
const MODES = ['fast', 'normal', 'strict'];
|
|
1750
1872
|
const DEFAULT_MODE = 'normal';
|
|
1751
1873
|
/** Event kinds that stay on this machine: runSync does not send them (an older sink would answer bad_kind). */
|
|
1752
|
-
const LOCAL_ONLY_KINDS = new Set(['mode']);
|
|
1753
|
-
const MODE_ASK_LINE = 'mode:
|
|
1754
|
-
+ '
|
|
1874
|
+
const LOCAL_ONLY_KINDS = new Set(['mode', 'knowledge_merge', 'leftover', 'worktree']);
|
|
1875
|
+
const MODE_ASK_LINE = 'mode: chưa chọn — chỉ hỏi ở cổng 1 khi plan-table báo ≥4 bước s≤6 (fast | normal | strict, mặc định normal); '
|
|
1876
|
+
+ 'đặt bằng `dieu-phoi mode --root <root_task_id> <mode>`';
|
|
1755
1877
|
|
|
1756
1878
|
function isMode(m) {
|
|
1757
1879
|
return typeof m === 'string' && MODES.includes(m);
|
|
@@ -2528,9 +2650,48 @@ function storeByName(name, stores) {
|
|
|
2528
2650
|
return { matches, store: matches.length === 1 ? matches[0] : null };
|
|
2529
2651
|
}
|
|
2530
2652
|
|
|
2653
|
+
/** Task id a delta file belongs to: knowledge-delta-<task_id>[-r<n>|-h<n>].md (8 chars a-z0-9); null = untagged. */
|
|
2654
|
+
function deltaTagOf(file) {
|
|
2655
|
+
const m = /^knowledge-delta-([a-z0-9]{8})(?:-[rh]\d+)?\.md$/i.exec(path.basename(String(file)));
|
|
2656
|
+
return m ? m[1].toLowerCase() : null;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* retro 11-05 (K1): who has judged what, from events + approved plans. `deltaState(run, tag)` is the user's decision on
|
|
2661
|
+
* a delta file in run dir `run` ('accept' | 'reject' | null): a tagged file follows the LAST user verdict of its task;
|
|
2662
|
+
* an untagged one (or one tagged with the run id) follows the tasks living in that run (own id, plan members,
|
|
2663
|
+
* dispatch `run`): accept when any of them was accepted, reject when all were rejected, else null.
|
|
2664
|
+
*/
|
|
2665
|
+
function deltaClassifier(events, plansDir) {
|
|
2666
|
+
const last = new Map();
|
|
2667
|
+
const runOf = new Map();
|
|
2668
|
+
for (const e of events || []) {
|
|
2669
|
+
if (!e || !e.task_id) continue;
|
|
2670
|
+
if (e.kind === 'verdict' && e.by === 'user' && (e.outcome === 'accept' || e.outcome === 'reject')) last.set(e.task_id, e.outcome);
|
|
2671
|
+
if (e.kind === 'dispatch' && typeof e.run === 'string' && e.run) runOf.set(e.task_id, e.run);
|
|
2672
|
+
}
|
|
2673
|
+
const planRoot = new Map();
|
|
2674
|
+
for (const p of loadPlans(plansDir)) for (const x of p.steps) if (x && x.task_id) planRoot.set(x.task_id, p.root_task_id);
|
|
2675
|
+
const byRun = new Map();
|
|
2676
|
+
const add = (run, t) => { if (!byRun.has(run)) byRun.set(run, new Set()); byRun.get(run).add(t); };
|
|
2677
|
+
for (const t of last.keys()) { add(t, t); if (planRoot.has(t)) add(planRoot.get(t), t); if (runOf.has(t)) add(runOf.get(t), t); }
|
|
2678
|
+
return {
|
|
2679
|
+
state: (t) => last.get(t) || null,
|
|
2680
|
+
deltaState(run, tag) {
|
|
2681
|
+
if (tag && tag !== run) return last.get(tag) || null;
|
|
2682
|
+
const ts = Array.from(byRun.get(run) || []).map((t) => last.get(t));
|
|
2683
|
+
if (!ts.length) return null;
|
|
2684
|
+
return ts.includes('accept') ? 'accept' : 'reject';
|
|
2685
|
+
},
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2531
2689
|
/**
|
|
2532
2690
|
* Delta files in run dirs that are 24 h .. 30 d old and that no store has applied (hash in meta.json `applied`).
|
|
2533
2691
|
* { count, runs: [{ run, n, oldest_ms, repos: { name: n } }], truncated } — never throws; count 0 when all is merged.
|
|
2692
|
+
* With opts.events (+ opts.plansDir) the files are split by what the user decided: count/runs = no user verdict yet
|
|
2693
|
+
* (24 h .. 30 d old); `accepted` = { count, runs: [{ run, n, oldest_ms, repos, tasks, untagged }] } = ALREADY ACCEPTED but
|
|
2694
|
+
* not merged (an error: no age floor, only the 30 d window); a task the user rejected is left out of both.
|
|
2534
2695
|
*/
|
|
2535
2696
|
function scanUnmergedDeltas(opts) {
|
|
2536
2697
|
const o = opts || {};
|
|
@@ -2538,6 +2699,8 @@ function scanUnmergedDeltas(opts) {
|
|
|
2538
2699
|
const warnMs = o.warnMs === undefined ? DELTA_WARN_MS : o.warnMs;
|
|
2539
2700
|
const windowMs = o.windowMs === undefined ? DELTA_WINDOW_MS : o.windowMs;
|
|
2540
2701
|
const res = { count: 0, runs: [], truncated: false };
|
|
2702
|
+
const cls = Array.isArray(o.events) ? deltaClassifier(o.events, o.plansDir) : null;
|
|
2703
|
+
const acc = { count: 0, runs: [] };
|
|
2541
2704
|
try {
|
|
2542
2705
|
const runs = runsDir();
|
|
2543
2706
|
const applied = new Set();
|
|
@@ -2551,26 +2714,36 @@ function scanUnmergedDeltas(opts) {
|
|
|
2551
2714
|
let names;
|
|
2552
2715
|
try { names = fs.readdirSync(dir); } catch (_) { continue; }
|
|
2553
2716
|
const row = { run, n: 0, oldest_ms: 0, repos: {} };
|
|
2717
|
+
const arow = { run, n: 0, oldest_ms: 0, repos: {}, tasks: [], untagged: false };
|
|
2554
2718
|
for (const f of names) {
|
|
2555
2719
|
if (!/^knowledge-delta.*\.md$/i.test(f)) continue;
|
|
2556
2720
|
const file = path.join(dir, f);
|
|
2557
2721
|
let fst;
|
|
2558
2722
|
try { fst = fs.statSync(file); } catch (_) { continue; }
|
|
2559
2723
|
const age = now - fst.mtimeMs;
|
|
2560
|
-
if (age
|
|
2724
|
+
if (age > windowMs) continue;
|
|
2725
|
+
const tag = deltaTagOf(f);
|
|
2726
|
+
const verdict = cls ? cls.deltaState(run, tag) : null;
|
|
2727
|
+
if (verdict === 'reject') continue;
|
|
2728
|
+
const into = verdict === 'accept' ? arow : row;
|
|
2729
|
+
if (into === row && age < warnMs) continue;
|
|
2561
2730
|
if (seen++ >= DELTA_SCAN_MAX) { res.truncated = true; break; }
|
|
2562
2731
|
const text = readTextFile(file);
|
|
2563
2732
|
if (text == null || applied.has(deltaHash(text))) continue;
|
|
2564
2733
|
const repo = deltaRepoOf(text);
|
|
2565
2734
|
const label = repo ? (isBareRepoName(repo) ? repo : path.basename(repo.replace(/[\\/]+$/, ''))) : '?';
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2735
|
+
into.n++;
|
|
2736
|
+
into.oldest_ms = Math.max(into.oldest_ms, age);
|
|
2737
|
+
into.repos[label] = (into.repos[label] || 0) + 1;
|
|
2738
|
+
if (into === arow) { if (tag && tag !== run) { if (!arow.tasks.includes(tag)) arow.tasks.push(tag); } else arow.untagged = true; }
|
|
2569
2739
|
}
|
|
2570
2740
|
if (row.n) { res.runs.push(row); res.count += row.n; }
|
|
2741
|
+
if (arow.n) { acc.runs.push(arow); acc.count += arow.n; }
|
|
2571
2742
|
if (res.truncated) break;
|
|
2572
2743
|
}
|
|
2573
2744
|
res.runs.sort((a, b) => b.n - a.n || b.oldest_ms - a.oldest_ms);
|
|
2745
|
+
acc.runs.sort((a, b) => b.n - a.n || b.oldest_ms - a.oldest_ms);
|
|
2746
|
+
if (acc.count) res.accepted = acc;
|
|
2574
2747
|
} catch (e) {
|
|
2575
2748
|
res.error = e.message;
|
|
2576
2749
|
}
|
|
@@ -2583,15 +2756,38 @@ function ageText(ms) {
|
|
|
2583
2756
|
|
|
2584
2757
|
/** One warning line (as an array, empty when nothing is pending) for the start block and `state`. */
|
|
2585
2758
|
function unmergedDeltaLines(scan) {
|
|
2586
|
-
|
|
2759
|
+
const out = [];
|
|
2760
|
+
const a = scan && scan.accepted;
|
|
2761
|
+
if (a && a.count) {
|
|
2762
|
+
const r0 = a.runs[0];
|
|
2763
|
+
const cmd = `dieu-phoi knowledge merge --run ${r0.run}${r0.tasks.length ? ` --task ${r0.tasks.slice(0, 8).join(',')}` : ''}${r0.untagged ? ' --untagged' : ''}`;
|
|
2764
|
+
const runText = a.runs.slice(0, 3).map((r) => `${r.run} ×${r.n}`).join(', ') + (a.runs.length > 3 ? ` (+${a.runs.length - 3} run)` : '');
|
|
2765
|
+
out.push(`knowledge_delta_accepted: LỖI — ${a.count} file delta của task ĐÃ nghiệm thu mà chưa vào kho (gộp lúc accept thất bại hoặc bị bỏ sót) — run ${runText}; cũ nhất ${ageText(Math.max(...a.runs.map((r) => r.oldest_ms)))}. `
|
|
2766
|
+
+ `Gộp: \`${cmd} --dry-run\` rồi bỏ --dry-run.`);
|
|
2767
|
+
}
|
|
2768
|
+
if (!scan || !scan.count) return out;
|
|
2587
2769
|
const oldest = Math.max(...scan.runs.map((r) => r.oldest_ms));
|
|
2588
2770
|
const repos = {};
|
|
2589
2771
|
for (const r of scan.runs) for (const [k, v] of Object.entries(r.repos)) repos[k] = (repos[k] || 0) + v;
|
|
2590
2772
|
const repoText = Object.entries(repos).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([k, v]) => `${k} ×${v}`).join(', ');
|
|
2591
2773
|
const runText = scan.runs.slice(0, 3).map((r) => `${r.run} ×${r.n}`).join(', ') + (scan.runs.length > 3 ? ` (+${scan.runs.length - 3} run)` : '');
|
|
2592
2774
|
const more = scan.truncated ? ' (chỉ quét một phần)' : '';
|
|
2593
|
-
|
|
2594
|
-
|
|
2775
|
+
const pending = scan.accepted ? ' (chưa có verdict người dùng)' : '';
|
|
2776
|
+
out.push(`knowledge_delta: ${scan.count} file delta chưa gộp quá 24h${pending}${more} — run ${runText}; kho ${repoText}; cũ nhất ${ageText(oldest)}. `
|
|
2777
|
+
+ `Gộp: \`dieu-phoi knowledge merge --run ${scan.runs[0].run} --dry-run\` rồi bỏ --dry-run (hoặc verdict accept); bỏ qua nếu cố ý không gộp.`);
|
|
2778
|
+
return out;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
/** Whether a scan has anything to show (undecided and/or accepted-but-unmerged deltas). */
|
|
2782
|
+
function hasUnmergedDeltas(scan) {
|
|
2783
|
+
return !!(scan && (scan.count || (scan.accepted && scan.accepted.count)));
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/** scanUnmergedDeltas with the user's verdicts (events + approved plans of ctx), so accepted-unmerged is told apart. */
|
|
2787
|
+
function scanUnmergedDeltasFor(ctx) {
|
|
2788
|
+
let events = [];
|
|
2789
|
+
try { events = readEvents(ctx); } catch (_) { /* no log: everything stays undecided */ }
|
|
2790
|
+
return scanUnmergedDeltas({ events, plansDir: ctx && ctx.paths && ctx.paths.plans });
|
|
2595
2791
|
}
|
|
2596
2792
|
|
|
2597
2793
|
/**
|
|
@@ -2664,6 +2860,16 @@ function projectFields(p) {
|
|
|
2664
2860
|
return {};
|
|
2665
2861
|
}
|
|
2666
2862
|
|
|
2863
|
+
/** Run dir id the brief points at (retro 11-05 K1: accept finds the delta there whatever the plan/root says), else null. */
|
|
2864
|
+
function dispatchRun(prompt) {
|
|
2865
|
+
try {
|
|
2866
|
+
const probe = 'norundir0'; // resultRunDir falls back to runs/<task_id>: the probe id tells "named in the brief" from that fallback
|
|
2867
|
+
const d = resultRunDir(prompt, probe);
|
|
2868
|
+
const id = path.basename(d);
|
|
2869
|
+
return path.dirname(d) === path.resolve(runsDir()) && id !== probe && /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : null;
|
|
2870
|
+
} catch (_) { return null; }
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2667
2873
|
function buildDispatch(p, ctx) {
|
|
2668
2874
|
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
2669
2875
|
const tr = p.tool_response;
|
|
@@ -2693,7 +2899,9 @@ function buildDispatch(p, ctx) {
|
|
|
2693
2899
|
...(tag.tag_issue ? { tag_issue: tag.tag_issue } : {}),
|
|
2694
2900
|
...(tag.type_alias_from ? { type_alias_from: tag.type_alias_from } : {}),
|
|
2695
2901
|
...(tag.chain_ok ? { chain_ok: true } : {}),
|
|
2902
|
+
...(tag.session_ok ? { session_ok: true } : {}),
|
|
2696
2903
|
...(tag.batch ? { batch: true } : {}),
|
|
2904
|
+
...(dispatchRun(ti.prompt) ? { run: dispatchRun(ti.prompt) } : {}),
|
|
2697
2905
|
task_type: tag.task_type,
|
|
2698
2906
|
score: tag.score,
|
|
2699
2907
|
urgency: tag.urgency,
|
|
@@ -2775,7 +2983,15 @@ function handlePostToolUse(p, ctx) {
|
|
|
2775
2983
|
if (n) ev.task_handoffs = n;
|
|
2776
2984
|
if (n >= HANDOFF_CHAIN_WARN) notes.push(handoffChainMessage(ev.task_id, n));
|
|
2777
2985
|
}
|
|
2986
|
+
// core 0.7.2 (K4): an exec with no choose record gets a reminder; a tier that differs from choose's answer without an
|
|
2987
|
+
// override logged for it leaves an implicit override event right after the dispatch.
|
|
2988
|
+
let implicit = null;
|
|
2989
|
+
if (ev.task_id && ev.role === 'exec' && isValidTaskId(ev.task_id)) {
|
|
2990
|
+
if (!ev.suggested_tier) notes.push(noChooseMessage(ev.task_id));
|
|
2991
|
+
else implicit = implicitOverrideEvent(ctx, ev);
|
|
2992
|
+
}
|
|
2778
2993
|
appendEvent(ctx, ev);
|
|
2994
|
+
if (implicit) appendEvent(ctx, implicit);
|
|
2779
2995
|
if (notes.length) {
|
|
2780
2996
|
Object.defineProperty(ev, 'stdout', {
|
|
2781
2997
|
enumerable: false,
|
|
@@ -2785,6 +3001,51 @@ function handlePostToolUse(p, ctx) {
|
|
|
2785
3001
|
return ev;
|
|
2786
3002
|
}
|
|
2787
3003
|
|
|
3004
|
+
/** core 0.7.2 (K4): reminder for an exec whose task has no choose record (data/suggest) when it was dispatched. */
|
|
3005
|
+
function noChooseMessage(taskId) {
|
|
3006
|
+
return `dieu-phoi: exec của task ${taskId} được giao mà chưa có kết quả \`choose\` (không có tier trong data/suggest). Lần giao sau chạy \`dieu-phoi choose --type <task_type> --task ${taskId} --json\` trước khi giao; tier giao khác tier choose trả mà không có --override sẽ được ghi là override implicit.`;
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/**
|
|
3010
|
+
* core 0.7.2 (K4): the implicit override for an exec dispatch whose tier differs from what choose answered
|
|
3011
|
+
* (choose_tier, else suggested_tier) while no override of this task to that tier is logged yet. Null otherwise.
|
|
3012
|
+
*/
|
|
3013
|
+
function implicitOverrideEvent(ctx, ev) {
|
|
3014
|
+
const dispatched = ev.dispatched_tier;
|
|
3015
|
+
const said = ev.choose_tier || ev.suggested_tier;
|
|
3016
|
+
if (!dispatched || dispatched.includes('?') || dispatched === said) return null;
|
|
3017
|
+
if (overrideLoggedFor(ctx, ev.task_id, dispatched)) return null;
|
|
3018
|
+
return Object.assign({
|
|
3019
|
+
schema: SCHEMA,
|
|
3020
|
+
kind: 'override',
|
|
3021
|
+
ts: nowIso(),
|
|
3022
|
+
agent_id: `task:${ev.task_id}`,
|
|
3023
|
+
task_id: ev.task_id,
|
|
3024
|
+
task_type: ev.task_type || null,
|
|
3025
|
+
suggested_tier: said,
|
|
3026
|
+
chosen_tier: dispatched,
|
|
3027
|
+
source: 'implicit',
|
|
3028
|
+
}, commonFields(ctx));
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/** Whether events.jsonl already holds an override (explicit or implicit) of this task to this tier. */
|
|
3032
|
+
function overrideLoggedFor(ctx, taskId, tier) {
|
|
3033
|
+
let txt;
|
|
3034
|
+
try { txt = fs.readFileSync(ctx.paths.events, 'utf8'); } catch (_) { return false; }
|
|
3035
|
+
const needle = `"task_id":${JSON.stringify(taskId)}`;
|
|
3036
|
+
let i = txt.indexOf('"kind":"override"');
|
|
3037
|
+
while (i >= 0) {
|
|
3038
|
+
const start = txt.lastIndexOf('\n', i) + 1;
|
|
3039
|
+
const end = txt.indexOf('\n', i);
|
|
3040
|
+
const line = txt.slice(start, end < 0 ? undefined : end);
|
|
3041
|
+
if (line.includes(needle)) {
|
|
3042
|
+
try { if (JSON.parse(line).chosen_tier === tier) return true; } catch (_) { /* a junk line never matches */ }
|
|
3043
|
+
}
|
|
3044
|
+
i = end < 0 ? -1 : txt.indexOf('"kind":"override"', end);
|
|
3045
|
+
}
|
|
3046
|
+
return false;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
2788
3049
|
function hasAssistantUsage(lines) {
|
|
2789
3050
|
return lines.some((l) => l && l.type === 'assistant' && l.message && l.message.usage
|
|
2790
3051
|
&& l.message.model !== '<synthetic>');
|
|
@@ -2930,6 +3191,101 @@ function persistInlineResult(lines, tag, opts) {
|
|
|
2930
3191
|
return file;
|
|
2931
3192
|
}
|
|
2932
3193
|
|
|
3194
|
+
// s15 (core 0.7.2): tools that start background work an agent may leave running, and the tools that stop it.
|
|
3195
|
+
// s16 (core 0.7.2): the formats below were checked against real subagent transcripts (Claude Code, 2026-11):
|
|
3196
|
+
// - Bash/PowerShell result "Command running in background with ID: <id>. Output is being written to: ..." appears for
|
|
3197
|
+
// run_in_background:true and also when a long foreground command is moved to the background by itself (no flag);
|
|
3198
|
+
// - Monitor result "Monitor started (task <id>, timeout ...)" (no "ID:"); a refused Monitor is a tool_result is_error:true;
|
|
3199
|
+
// - TaskStop input {task_id}, the same id;
|
|
3200
|
+
// - task finished = a <task-notification> carrying <status> (+ <task-id>, <tool-use-id>). It is delivered mostly as an
|
|
3201
|
+
// `attachment` line (attachment.type "queued_command", attachment.origin "task-notification", XML in attachment.prompt),
|
|
3202
|
+
// sometimes as a user line with string content. A <task-notification> without <status> is a Monitor progress <event>.
|
|
3203
|
+
const BG_START_TOOLS = new Set(['Bash', 'PowerShell', 'Monitor']);
|
|
3204
|
+
const BG_SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
|
|
3205
|
+
const BG_STOP_TOOLS = new Set(['TaskStop', 'KillShell']);
|
|
3206
|
+
const BG_ID_RE = /\bID:\s*([A-Za-z0-9_-]{2,64})/;
|
|
3207
|
+
const BG_MONITOR_ID_RE = /\(task\s+([A-Za-z0-9_-]{2,64})[,)]/;
|
|
3208
|
+
const BG_AUTO_RE = /^\s*Command running in background with ID:/; // anchored: test/grep output may quote the phrase
|
|
3209
|
+
const NOTIF_BLOCK_RE = /<task-notification>[\s\S]*?<\/task-notification>/g;
|
|
3210
|
+
const TASK_ID_TAG_RE = /<task-id>\s*([A-Za-z0-9_-]{2,64})\s*<\/task-id>/;
|
|
3211
|
+
const TOOL_USE_ID_TAG_RE = /<tool-use-id>\s*([A-Za-z0-9_-]{2,80})\s*<\/tool-use-id>/;
|
|
3212
|
+
|
|
3213
|
+
function textOfContent(c) {
|
|
3214
|
+
if (typeof c === 'string') return c;
|
|
3215
|
+
if (!Array.isArray(c)) return '';
|
|
3216
|
+
return c.map((b) => (b && b.type === 'text' && typeof b.text === 'string' ? b.text : '')).join('\n');
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
/**
|
|
3220
|
+
* s15/s16: background work an agent opened and did not stop, read from its transcript (pure; returns ids, never a command).
|
|
3221
|
+
* Opened = a Bash/PowerShell tool_use with run_in_background:true, or whose result says "Command running in background
|
|
3222
|
+
* with ID:" (a long command moved to the background by itself), or a Monitor tool_use whose result is not an error. The id
|
|
3223
|
+
* comes from the tool_result ("... ID: <id>" / "(task <id>, ..."); without one the tool_use id stands in. Closed = a
|
|
3224
|
+
* TaskStop/KillShell whose input names the id, or a finished-task <task-notification> (one with <status>) naming the id
|
|
3225
|
+
* or the tool_use id, wherever it sits (queued_command attachment, user text). Returns the ids still open, in start order.
|
|
3226
|
+
*/
|
|
3227
|
+
function openBackgroundTasks(lines) {
|
|
3228
|
+
const open = new Map(); // tool_use id -> task id, null while no id is known
|
|
3229
|
+
const finished = new Set(); // task ids and tool_use ids named by a stop or a finished notice (may precede the start result)
|
|
3230
|
+
const kinds = new Map(); // tool_use id -> tool name of every candidate start
|
|
3231
|
+
const flagged = new Set(); // tool_use ids that asked for the background or are Monitors
|
|
3232
|
+
const close = (id) => {
|
|
3233
|
+
finished.add(id);
|
|
3234
|
+
for (const [k, v] of open) if (k === id || v === id) open.delete(k);
|
|
3235
|
+
};
|
|
3236
|
+
const closeNotified = (text) => {
|
|
3237
|
+
if (!text || !text.includes('<task-notification>')) return;
|
|
3238
|
+
for (const blk of text.match(NOTIF_BLOCK_RE) || []) {
|
|
3239
|
+
if (!blk.includes('<status>')) continue; // a Monitor <event> notice: the task is still running
|
|
3240
|
+
const t = TASK_ID_TAG_RE.exec(blk);
|
|
3241
|
+
const u = TOOL_USE_ID_TAG_RE.exec(blk);
|
|
3242
|
+
if (t) close(t[1]);
|
|
3243
|
+
if (u) close(u[1]);
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
for (const l of Array.isArray(lines) ? lines : []) {
|
|
3247
|
+
if (!l) continue;
|
|
3248
|
+
if (l.type === 'attachment') {
|
|
3249
|
+
const a = l.attachment;
|
|
3250
|
+
if (a && a.type === 'queued_command' && typeof a.prompt === 'string') closeNotified(a.prompt);
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
const msg = l.message;
|
|
3254
|
+
if (!msg) continue;
|
|
3255
|
+
const isUser = l.type === 'user';
|
|
3256
|
+
if (typeof msg.content === 'string') {
|
|
3257
|
+
if (isUser) closeNotified(msg.content);
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
if (!Array.isArray(msg.content)) continue;
|
|
3261
|
+
for (const b of msg.content) {
|
|
3262
|
+
if (!b || typeof b !== 'object') continue;
|
|
3263
|
+
if (b.type === 'tool_use' && typeof b.id === 'string') {
|
|
3264
|
+
const input = b.input && typeof b.input === 'object' ? b.input : {};
|
|
3265
|
+
if (BG_START_TOOLS.has(b.name)) {
|
|
3266
|
+
kinds.set(b.id, b.name);
|
|
3267
|
+
if (b.name === 'Monitor' || input.run_in_background === true) flagged.add(b.id);
|
|
3268
|
+
} else if (BG_STOP_TOOLS.has(b.name)) {
|
|
3269
|
+
const id = input.task_id || input.shell_id;
|
|
3270
|
+
if (typeof id === 'string' && id) close(id);
|
|
3271
|
+
}
|
|
3272
|
+
} else if (b.type === 'tool_result' && kinds.has(b.tool_use_id)) {
|
|
3273
|
+
const text = textOfContent(b.content);
|
|
3274
|
+
const kind = kinds.get(b.tool_use_id);
|
|
3275
|
+
const auto = BG_SHELL_TOOLS.has(kind) && BG_AUTO_RE.test(text);
|
|
3276
|
+
if ((!flagged.has(b.tool_use_id) && !auto) || b.is_error === true) continue; // foreground, or refused: nothing started
|
|
3277
|
+
const m = (kind === 'Monitor' ? BG_MONITOR_ID_RE.exec(text) : null) || BG_ID_RE.exec(text);
|
|
3278
|
+
const id = m ? m[1] : null;
|
|
3279
|
+
if (finished.has(b.tool_use_id) || (id && finished.has(id))) continue;
|
|
3280
|
+
open.set(b.tool_use_id, id);
|
|
3281
|
+
} else if (b.type === 'text' && typeof b.text === 'string' && isUser) {
|
|
3282
|
+
closeNotified(b.text);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
return Array.from(open, ([k, v]) => v || k);
|
|
3287
|
+
}
|
|
3288
|
+
|
|
2933
3289
|
function handleSubagentStop(p, ctx) {
|
|
2934
3290
|
const agentId = typeof p.agent_id === 'string' && p.agent_id ? p.agent_id : null;
|
|
2935
3291
|
if (!agentId) return null;
|
|
@@ -2952,6 +3308,14 @@ function handleSubagentStop(p, ctx) {
|
|
|
2952
3308
|
sum = summarizeTranscript(lines, { afterUuids: seen, pricing: loadPricing(ctx) });
|
|
2953
3309
|
tag = tagFromTranscript(lines, knownTaskTypes(ctx));
|
|
2954
3310
|
}
|
|
3311
|
+
// s15 (core 0.7.2): a tagged dp-* agent that stops while a background task it opened still runs is sent back once
|
|
3312
|
+
// (decision block: TaskStop it, then report). That second stop has stop_hook_active true: recorded, never blocked again.
|
|
3313
|
+
// The blocked stop writes nothing, so the next stop still counts the whole segment.
|
|
3314
|
+
const leftover = tag && tag.task_id ? openBackgroundTasks(lines) : [];
|
|
3315
|
+
if (leftover.length && p.stop_hook_active !== true && ctx.config.block_leftover_tasks !== false) {
|
|
3316
|
+
const reason = `Còn task nền chưa dừng: ${leftover.slice(0, 5).join(', ')}. TaskStop từng cái rồi trả lại STATUS.`;
|
|
3317
|
+
return { stdout: `${JSON.stringify({ decision: 'block', reason })}\n` };
|
|
3318
|
+
}
|
|
2955
3319
|
const t = tag || UNTAGGED;
|
|
2956
3320
|
const ev = Object.assign({
|
|
2957
3321
|
schema: SCHEMA,
|
|
@@ -2980,12 +3344,18 @@ function handleSubagentStop(p, ctx) {
|
|
|
2980
3344
|
ev.handoff = ev.status === 'handoff';
|
|
2981
3345
|
ev.peak_ctx = sum ? sum.peak_ctx : null;
|
|
2982
3346
|
ev.compactions = sum ? sum.compactions : null;
|
|
3347
|
+
// s15: background tasks still open when the segment ended (after the one allowed block): a count, never the commands
|
|
3348
|
+
if (leftover.length) ev.leftover_tasks = leftover.length;
|
|
2983
3349
|
let inline = null;
|
|
2984
3350
|
if (exists && tag) {
|
|
2985
3351
|
try { inline = persistInlineResult(lines, tag, { finalText: p.last_assistant_message }); } catch (_) { /* best effort */ }
|
|
2986
3352
|
}
|
|
2987
3353
|
// exec/verify/plan only: true = it returned without writing its dp-out file (the hook saved the .inline.md)
|
|
2988
|
-
if (tag && RESULT_ROLES.includes(tag.role))
|
|
3354
|
+
if (tag && RESULT_ROLES.includes(tag.role)) {
|
|
3355
|
+
ev.inline_result = Boolean(inline);
|
|
3356
|
+
// retro 2026-11-05 K2: the segment ended with no STATUS:/VERDICT: line (or no final text) and was neither a quota pause nor a handoff
|
|
3357
|
+
ev.ended_without_status = exists && (ev.status === 'none' || ev.status === null);
|
|
3358
|
+
}
|
|
2989
3359
|
// core 0.7.0 (s2): seconds spent running tests in this segment (transcript; else the dp-out `TEST_S:` line)
|
|
2990
3360
|
const tt = exists && sum ? testTimeOf(lines.slice(sum.segment_start)) : null;
|
|
2991
3361
|
ev.test_s = tt ? tt.test_s : null;
|
|
@@ -3013,6 +3383,100 @@ function handleSubagentStop(p, ctx) {
|
|
|
3013
3383
|
verify: 'none', reason_code: `auto-${why}`, cause: null, round: null,
|
|
3014
3384
|
}, commonFields(ctx)));
|
|
3015
3385
|
}
|
|
3386
|
+
// s16 (core 0.7.2): processes the agent left behind (report; kill only with config kill_leftovers). Never throws, bounded to ~5 s.
|
|
3387
|
+
if (tag && tag.task_id && exists) {
|
|
3388
|
+
try { scanLeftoverProcs(ctx, { agentId, taskId: tag.task_id, lines }); } catch (e) { logError(ctx.home, e, 'leftover-procs'); }
|
|
3389
|
+
}
|
|
3390
|
+
return ev;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// ------------------------------------------------------------------ leftover processes (s16, core 0.7.2)
|
|
3394
|
+
|
|
3395
|
+
// The process-table helpers live in lib/procs.cjs. The installed hook is a single copied file, so `setup` puts the same
|
|
3396
|
+
// module next to it as bin/procs.cjs; the repo layout finds it under ./lib. Missing module = no scan, never an error.
|
|
3397
|
+
function loadProcs() {
|
|
3398
|
+
for (const f of ['./procs.cjs', './lib/procs.cjs']) {
|
|
3399
|
+
try { return require(f); } catch (_) { /* try the next place */ }
|
|
3400
|
+
}
|
|
3401
|
+
return null;
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const LEFTOVER_BUDGET_MS = 5000;
|
|
3405
|
+
const LEFTOVER_LIST_TIMEOUT_MS = 4000;
|
|
3406
|
+
const LEFTOVER_OVERLAP_MAX_AGE_MS = 24 * 3600 * 1000;
|
|
3407
|
+
|
|
3408
|
+
/** Start of an agent transcript (first line timestamp, epoch ms) or null. */
|
|
3409
|
+
function transcriptStartMs(lines) {
|
|
3410
|
+
for (const l of Array.isArray(lines) ? lines : []) {
|
|
3411
|
+
const t = l && typeof l.timestamp === 'string' ? Date.parse(l.timestamp) : NaN;
|
|
3412
|
+
if (Number.isFinite(t)) return t;
|
|
3413
|
+
}
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
/**
|
|
3418
|
+
* Is another dp agent alive during [startMs, endMs]? From dispatch/complete events: dispatched at or before endMs and not
|
|
3419
|
+
* completed before startMs (a foreground agent's dispatch is logged when it returns, so it counts too: the safe side).
|
|
3420
|
+
* Dispatches older than 24 h without a complete are ignored (crashed agents).
|
|
3421
|
+
*/
|
|
3422
|
+
function otherAgentsRunning(events, agentId, startMs, endMs) {
|
|
3423
|
+
const disp = new Map();
|
|
3424
|
+
const done = new Map();
|
|
3425
|
+
for (const ev of Array.isArray(events) ? events : []) {
|
|
3426
|
+
if (!ev || !ev.agent_id || ev.agent_id === agentId) continue;
|
|
3427
|
+
const t = Date.parse(ev.ts);
|
|
3428
|
+
if (!Number.isFinite(t)) continue;
|
|
3429
|
+
if (ev.kind === 'dispatch') disp.set(ev.agent_id, Math.max(disp.get(ev.agent_id) || 0, t));
|
|
3430
|
+
else if (ev.kind === 'complete') done.set(ev.agent_id, Math.max(done.get(ev.agent_id) || 0, t));
|
|
3431
|
+
}
|
|
3432
|
+
for (const [id, d] of disp) {
|
|
3433
|
+
if (d > endMs) continue;
|
|
3434
|
+
const c = done.get(id);
|
|
3435
|
+
if (c === undefined) { if (endMs - d <= LEFTOVER_OVERLAP_MAX_AGE_MS) return true; continue; }
|
|
3436
|
+
if (c >= startMs) return true;
|
|
3437
|
+
}
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
/**
|
|
3442
|
+
* Looks at the process table after a dp agent stopped. Descendants of this session's Claude Code process created since the
|
|
3443
|
+
* agent started and still alive (not the hook's own chain, not claude/code-graph) -> one local `leftover` event
|
|
3444
|
+
* {task_id, n, names}. They are only reported, unless config kill_leftovers is true AND no other dp agent ran in the
|
|
3445
|
+
* meantime (then the leftover trees are killed). deps = { procs, now, selfPid, events } for tests. Returns the event or null.
|
|
3446
|
+
*/
|
|
3447
|
+
function scanLeftoverProcs(ctx, a, deps) {
|
|
3448
|
+
const d = deps || {};
|
|
3449
|
+
if (ctx.config.scan_leftover_procs === false || (!d.procs && process.env.DIEU_PHOI_NO_PROCS === '1')) return null;
|
|
3450
|
+
const procs = d.procs || loadProcs();
|
|
3451
|
+
if (!procs) return null;
|
|
3452
|
+
const startMs = transcriptStartMs(a.lines);
|
|
3453
|
+
const now = typeof d.now === 'number' ? d.now : Date.now();
|
|
3454
|
+
// an agent that started over 24 h ago (a long-resumed or odd transcript) has no meaningful "created since" window
|
|
3455
|
+
if (startMs === null || now - startMs > LEFTOVER_OVERLAP_MAX_AGE_MS) return null;
|
|
3456
|
+
const t0 = Date.now();
|
|
3457
|
+
const list = procs.listProcesses({ timeoutMs: LEFTOVER_LIST_TIMEOUT_MS });
|
|
3458
|
+
if (!Array.isArray(list)) return null;
|
|
3459
|
+
const selfPid = d.selfPid || process.pid;
|
|
3460
|
+
const root = procs.claudeAncestor(list, selfPid);
|
|
3461
|
+
if (!root) return null;
|
|
3462
|
+
const left = procs.leftoverProcesses(list, { rootPid: root.pid, startMs, selfPid });
|
|
3463
|
+
if (!left.all.length) return null;
|
|
3464
|
+
const counts = new Map();
|
|
3465
|
+
for (const p of left.all) { const n = String(p.name || '?').toLowerCase(); counts.set(n, (counts.get(n) || 0) + 1); }
|
|
3466
|
+
const names = Array.from(counts).sort((x, y) => y[1] - x[1] || (x[0] < y[0] ? -1 : 1)).slice(0, 8).map((x) => x[0]);
|
|
3467
|
+
const killOn = ctx.config.kill_leftovers === true;
|
|
3468
|
+
let killed = 0;
|
|
3469
|
+
let skipped = null;
|
|
3470
|
+
if (killOn) {
|
|
3471
|
+
const events = d.events || readEvents(ctx);
|
|
3472
|
+
if (otherAgentsRunning(events, a.agentId, startMs, now)) skipped = 'other-agent-running';
|
|
3473
|
+
else killed = procs.killTree(left.roots.map((p) => p.pid), { deadline: t0 + LEFTOVER_BUDGET_MS }).length;
|
|
3474
|
+
}
|
|
3475
|
+
const ev = Object.assign({
|
|
3476
|
+
schema: SCHEMA, kind: 'leftover', ts: nowIso(), agent_id: a.agentId, task_id: a.taskId,
|
|
3477
|
+
n: left.all.length, names, kill_leftovers: killOn, killed,
|
|
3478
|
+
}, skipped ? { kill_skipped: skipped } : {}, commonFields(ctx));
|
|
3479
|
+
appendEvent(ctx, ev);
|
|
3016
3480
|
return ev;
|
|
3017
3481
|
}
|
|
3018
3482
|
|
|
@@ -3488,7 +3952,22 @@ function handleChainGate(p, ctx) {
|
|
|
3488
3952
|
if (watchAgentId(p)) return null;
|
|
3489
3953
|
const ti = p.tool_input && typeof p.tool_input === 'object' ? p.tool_input : {};
|
|
3490
3954
|
const tag = resolveTag(ti.prompt, knownTaskTypes(ctx));
|
|
3491
|
-
if (!tag ||
|
|
3955
|
+
if (!tag || !tag.task_id || !isValidTaskId(tag.task_id)) return null;
|
|
3956
|
+
if (tag.role === 'verify') return verifyOrphanGate(p, ctx, tag);
|
|
3957
|
+
if (tag.role !== 'exec') return null;
|
|
3958
|
+
const chain = tag.chain_ok ? null : chainGateDenial(p, ctx, tag);
|
|
3959
|
+
if (chain) return chain;
|
|
3960
|
+
return sessionCapGate(p, ctx, tag);
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
function preToolDeny(reason) {
|
|
3964
|
+
return JSON.stringify({
|
|
3965
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason },
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
/** Core 0.7.1 (C1): exec continuation of a task that already handed off HANDOFF_CHAIN_DENY times. */
|
|
3970
|
+
function chainGateDenial(p, ctx, tag) {
|
|
3492
3971
|
const n = countTaskHandoffs(ctx, tag.task_id);
|
|
3493
3972
|
if (n < HANDOFF_CHAIN_DENY) return null;
|
|
3494
3973
|
const ev = logGate(ctx, p, `task:${tag.task_id}`, { task_id: tag.task_id, role: 'exec' },
|
|
@@ -3496,15 +3975,65 @@ function handleChainGate(p, ctx) {
|
|
|
3496
3975
|
return {
|
|
3497
3976
|
denied: true,
|
|
3498
3977
|
event: ev,
|
|
3499
|
-
stdout:
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3978
|
+
stdout: preToolDeny(`dieu-phoi: task ${tag.task_id} đã handoff ${n} lần (trần ${HANDOFF_CHAIN_DENY}) — không giao thêm exec tiếp nối. `
|
|
3979
|
+
+ `Giao planner (dp-scout) đọc handoff-${tag.task_id}.md rồi chia phần còn lại thành bước mới (task_id mới từ \`dieu-phoi new-id\`). `
|
|
3980
|
+
+ 'Chỉ khi người dùng đồng ý nối tiếp: thêm `|chain_ok` vào thẻ.'),
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
/**
|
|
3985
|
+
* Core 0.7.2 (K3, retro 2026-11-05): a `role=verify` dispatch whose task has no exec dispatch in events.jsonl and sits
|
|
3986
|
+
* in no approved plan is a verifier for work that was never dispatched -> denied (`verify-orphan`). `|batch` (the one
|
|
3987
|
+
* verifier of a fast request, covering several steps) is exempt.
|
|
3988
|
+
*/
|
|
3989
|
+
function verifyOrphanGate(p, ctx, tag) {
|
|
3990
|
+
if (tag.batch) return null;
|
|
3991
|
+
if (hasDispatchFor(ctx, [tag.task_id], 'exec')) return null;
|
|
3992
|
+
if (planOfTask(ctx, tag.task_id)) return null;
|
|
3993
|
+
const ev = logGate(ctx, p, `task:${tag.task_id}`, { task_id: tag.task_id, role: 'verify' },
|
|
3994
|
+
{ gate_action: 'deny', reason_code: 'verify-orphan' });
|
|
3995
|
+
return {
|
|
3996
|
+
denied: true,
|
|
3997
|
+
event: ev,
|
|
3998
|
+
stdout: preToolDeny(`dieu-phoi: verifier của task ${tag.task_id} không có lượt exec nào trong phiên này và không nằm trong kế hoạch đã duyệt — `
|
|
3999
|
+
+ 'không chạy verifier cho việc chưa giao. Giao exec của task trước; verifier gộp của mode fast dùng thẻ `|batch`.'),
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
/** Core 0.7.2 (K5): trần phiên điều phối cứng. Vượt một trong hai → exec mở ROOT MỚI bị chặn (session-cap). */
|
|
4004
|
+
const SESSION_CAP_TOKENS = 800000;
|
|
4005
|
+
const SESSION_CAP_COMPACTIONS = 10;
|
|
4006
|
+
|
|
4007
|
+
/**
|
|
4008
|
+
* Core 0.7.2 (K5, retro 2026-11-05): the orchestrating session is past SESSION_CAP_TOKENS of context or
|
|
4009
|
+
* SESSION_CAP_COMPACTIONS compactions. An exec that opens a new root is denied (`session-cap`); an exec of a root that
|
|
4010
|
+
* already has a dispatch (a continuation) passes; `|session_ok` lets the user accept the risk. Main-session only.
|
|
4011
|
+
*/
|
|
4012
|
+
function sessionCapGate(p, ctx, tag) {
|
|
4013
|
+
if (tag.session_ok) return null;
|
|
4014
|
+
const plans = loadPlans(ctx.paths.plans);
|
|
4015
|
+
const step = planOfTask(ctx, tag.task_id);
|
|
4016
|
+
const root = step ? step.root : tag.task_id;
|
|
4017
|
+
if (hasDispatchFor(ctx, rootTaskIds(plans, root), null)) return null;
|
|
4018
|
+
const u = orchestratorUsage(p.transcript_path, loadPricing(ctx), {
|
|
4019
|
+
cacheFile: orchCacheFile(ctx.paths, p.session_id, p.transcript_path),
|
|
4020
|
+
coldMaxBytes: HOOK_COLD_MAX_BYTES,
|
|
4021
|
+
});
|
|
4022
|
+
const tokens = typeof u.ctx_tokens === 'number' ? u.ctx_tokens : null;
|
|
4023
|
+
const comp = typeof u.compactions === 'number' ? u.compactions : null;
|
|
4024
|
+
const over = (tokens !== null && tokens > SESSION_CAP_TOKENS) || (comp !== null && comp >= SESSION_CAP_COMPACTIONS);
|
|
4025
|
+
if (!over) return null;
|
|
4026
|
+
const ev = logGate(ctx, p, `task:${tag.task_id}`, { task_id: tag.task_id, role: 'exec' },
|
|
4027
|
+
{ gate_action: 'deny', reason_code: 'session-cap' });
|
|
4028
|
+
const sid = p.session_id || '<session>';
|
|
4029
|
+
return {
|
|
4030
|
+
denied: true,
|
|
4031
|
+
event: ev,
|
|
4032
|
+
stdout: preToolDeny(`dieu-phoi: phiên điều phối đã chạm trần cứng (context ${tokens === null ? '?' : tokens} token, `
|
|
4033
|
+
+ `${comp === null ? '?' : comp} lần nén; trần ${SESSION_CAP_TOKENS} token hoặc ${SESSION_CAP_COMPACTIONS} lần nén) — `
|
|
4034
|
+
+ `không mở root mới trong phiên này. Đừng /compact thêm: ghi trạng thái (\`dieu-phoi state --session ${sid}\` + file trạng thái trong run dir), `
|
|
4035
|
+
+ 'trình phần đã xong, rồi MỞ PHIÊN MỚI và nạp lại từ state; giao root mới từ phiên đó. '
|
|
4036
|
+
+ 'Chỉ khi người dùng đồng ý giao ngay ở đây: thêm `|session_ok` vào thẻ.'),
|
|
3508
4037
|
};
|
|
3509
4038
|
}
|
|
3510
4039
|
|
|
@@ -3667,8 +4196,11 @@ function runHook(opts) {
|
|
|
3667
4196
|
if (name === 'PreToolUse') return handleChainGate(p, ctx); // { stdout } (deny) or null
|
|
3668
4197
|
if (name === 'SessionStart') return handleSessionStart(p, ctx); // { stdout } or null, never an event
|
|
3669
4198
|
if (name === 'UserPromptSubmit') return handleUserPromptSubmit(p, ctx); // { stdout } or null
|
|
3670
|
-
if (name === 'SubagentStop')
|
|
3671
|
-
|
|
4199
|
+
if (name === 'SubagentStop') {
|
|
4200
|
+
const r = handleSubagentStop(p, ctx);
|
|
4201
|
+
if (r && typeof r.stdout === 'string') return r; // s15: block decision, printed as the hook's JSON answer
|
|
4202
|
+
ev = r;
|
|
4203
|
+
} else if (name === 'PostToolUse' || (!name && p.tool_name)) ev = handlePostToolUse(p, ctx);
|
|
3672
4204
|
if (ev) {
|
|
3673
4205
|
try { maybeSpawnSync(ctx); } catch (e) { logError(home, e, 'spawn-sync'); }
|
|
3674
4206
|
}
|
|
@@ -3818,7 +4350,7 @@ async function runSync(ctx, opts) {
|
|
|
3818
4350
|
// ------------------------------------------------------------------ exports / main
|
|
3819
4351
|
|
|
3820
4352
|
module.exports = {
|
|
3821
|
-
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, startKnowledgeOf, knowledgeLines, orchLessons, orchLessonLines, abortReasonOf, completionStatus, logGate, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, buildSessionState, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
4353
|
+
runsDir, loadPlans, isDieuPhoiPrompt, foldPrompt, sinkStatusCached, sinkUrlKey, SINK_CACHE_TTL_MS, scoutBriefPath, quickHealth, startRun, startInfo, startKnowledgeOf, knowledgeLines, orchLessons, orchLessonLines, abortReasonOf, completionStatus, logGate, QUOTA_PAUSE_MARKER, stepIds, START_STEP_IDS, healthLine, formatStartBlock, handleUserPromptSubmit, START_PREFIX, buildSessionState, findOrphanAgents, orphanLines, ORPHAN_MIN_MS, ORPHAN_NO_COMPLETE_MS, formatSessionState, stateCounts, compactSystemMessage, handleSessionStart, dispatchInSession, STATE_TEXT_CAP, COMPACT_PREFIX,
|
|
3822
4354
|
modeLine, MODES, DEFAULT_MODE, LOCAL_ONLY_KINDS, MODE_ASK_LINE, isMode, rootModeFromEvents, rootModeFromText, dispatchModeFromEvents, taskModeInfo, planOfTask, modeFields,
|
|
3823
4355
|
modeSuggestion, modeSuggestLine, fastFailRate, rootTaskIds, FAST_FAIL_WINDOW, FAST_FAIL_MIN_N, FAST_FAIL_RATE,
|
|
3824
4356
|
testCommandInfo, testTimeOf, testSFromText,
|
|
@@ -3828,12 +4360,12 @@ module.exports = {
|
|
|
3828
4360
|
recordSuggestion, readSuggestion, suggestFile, SUGGEST_MAX_AGE_MS, countTaskHandoffs, handoffChainMessage, HANDOFF_CHAIN_WARN, HANDOFF_CHAIN_DENY, handleChainGate, tierFields, TASK_ID_RE, isValidTaskId, newTaskId, taskIdError, normalizeModel, loadPricing,
|
|
3829
4361
|
priceFor, usageBuckets, requestCost, parseJsonl, readTranscript, lastUuidOf,
|
|
3830
4362
|
firstPromptText, tagFromTranscript, summarizeTranscript,
|
|
3831
|
-
RESULT_ROLES, BLOCKED_RESULT_NAME_RE, resultFileName, findResultFiles, resultRunDir, finalAssistantText, persistInlineResult, orchestratorUsage, orchCacheFile, findSessionTranscript, SESSION_ID_RE, HOOK_COLD_MAX_BYTES, appendEvent, EVENTS_LOCK_WAIT_MS, eventLinesFrom, readEventLines, readEvents, readEventsBuffer, archiveFiles, readArchiveLines,
|
|
4363
|
+
RESULT_ROLES, BLOCKED_RESULT_NAME_RE, resultFileName, findResultFiles, resultRunDir, finalAssistantText, persistInlineResult, orchestratorUsage, countCompactions, orchCacheFile, findSessionTranscript, SESSION_ID_RE, HOOK_COLD_MAX_BYTES, SESSION_CAP_TOKENS, SESSION_CAP_COMPACTIONS, appendEvent, EVENTS_LOCK_WAIT_MS, eventLinesFrom, readEventLines, readEvents, readEventsBuffer, archiveFiles, readArchiveLines,
|
|
3832
4364
|
loadSyncOffset, saveSyncOffset, recordRejected, unsyncedInfo, loadSegments, saveSegments, segmentFile,
|
|
3833
4365
|
HANDOFF_MARKER, HANDOFF_THRESHOLDS, watchThresholds, GATE_MAX_CHILDREN, HELPER_AGENT_TYPE, watchAgentId, subagentTranscriptPath, readHeadTag, lastContextTokens, watchState, handoffMessage, handleWatch, handleGate, handleGateRelease, gateDir, gateSlots, gateReserve, gateConfirm, GATE_PENDING_MS,
|
|
3834
|
-
globMatch, pathspecOf, diffShas, dirtyFiles, computeFreshness, addDirty, indexStamp, freshnessCacheFile, readFreshCache, cachedFor, freshCounts, knowledgeSummary, shortFileList, readStoreIndex, relatedStatus, startKnowledge, deltaHash, deltaRepoOf, isBareRepoName, storeCatalog, storeByName, scanUnmergedDeltas, unmergedDeltaLines, DELTA_WARN_MS, FRESH_CACHE_MAX, DIFF_TIMEOUT_MS, HOOK_DIFF_TIMEOUT_MS, DIRTY_TIMEOUT_MS,
|
|
4366
|
+
globMatch, pathspecOf, diffShas, dirtyFiles, computeFreshness, addDirty, indexStamp, freshnessCacheFile, readFreshCache, cachedFor, freshCounts, knowledgeSummary, shortFileList, readStoreIndex, relatedStatus, startKnowledge, deltaHash, deltaRepoOf, isBareRepoName, storeCatalog, storeByName, scanUnmergedDeltas, scanUnmergedDeltasFor, unmergedDeltaLines, hasUnmergedDeltas, deltaTagOf, deltaClassifier, dispatchRun, DELTA_WARN_MS, DELTA_WINDOW_MS, FRESH_CACHE_MAX, DIFF_TIMEOUT_MS, HOOK_DIFF_TIMEOUT_MS, DIRTY_TIMEOUT_MS,
|
|
3835
4367
|
projectsDir, findGit, originUrlFromConfig, normalizeRemoteUrl, resolveProject, readHead, repoFromBrief, projectFields, nativePath, isStoreDirName, aliasesFile, loadAliases, keyDirName,
|
|
3836
|
-
extractAgentId, hasAssistantUsage, hasDispatch, buildDispatch, handlePostToolUse, handleSubagentStop, maybeSpawnSync, runHook, runSync,
|
|
4368
|
+
extractAgentId, hasAssistantUsage, hasDispatch, buildDispatch, handlePostToolUse, handleSubagentStop, openBackgroundTasks, scanLeftoverProcs, otherAgentsRunning, maybeSpawnSync, runHook, runSync,
|
|
3837
4369
|
};
|
|
3838
4370
|
|
|
3839
4371
|
if (require.main === module) {
|