dsh-root-planner-preset 1.1.5 → 1.1.6
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/README.md +20 -2
- package/README.zh.md +13 -2
- package/package.json +1 -1
- package/tools/check-pwsh-background.mjs +91 -0
- package/tools/child-status.mjs +102 -0
- package/tools/count-tool-calls.mjs +78 -0
- package/tools/extract-offered-tools.mjs +69 -0
package/README.md
CHANGED
|
@@ -122,9 +122,27 @@ Then **start a new session** and pick **Root Planner** in the preset selector. F
|
|
|
122
122
|
|
|
123
123
|
**Verified**: the patch parses as loader YAML; the parsed persona is **byte-identical** to `persona.txt`; both delegation masks exclude every mutation entry point and name only tools the runtime reports as global; a clean checkout passes verification (`.gitattributes` pins LF, without which a Windows clone breaks the byte-identity check); the depth override reached the composed profile.
|
|
124
124
|
|
|
125
|
-
**
|
|
125
|
+
**Verified end to end on a real task** — a 24-descendant run over a large reverse-engineering directory tree. Every line below was read from the session logs' own persisted tool schemas, which are the registry's output rather than a model's recollection:
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
| Property | Evidence |
|
|
128
|
+
|---|---|
|
|
129
|
+
| The mask reaches every child | The root is offered **31** tools; depth-1 and depth-2 agents are offered **25**. The difference is exactly the six the mask omits: `workflow`, `present`, `job_output`, `job_list`, `job_kill`, `exit_plan_mode`. |
|
|
130
|
+
| Subagents really are workers | `write`, `edit`, `ops_replace` and `pwsh` are present at every depth and were used: 10 `write` and 60 `edit` from the root, 14 `write` from one depth-1 agent, 20 `write` from a depth-2 agent. |
|
|
131
|
+
| No subagent can leave a process running | `run_in_background` is absent from the `pwsh` schema at all three depths. (A naive log search says otherwise — the phrase also occurs in `ops_bash`'s own description, which is why `tools/check-pwsh-background.mjs` inspects the schema window instead.) |
|
|
132
|
+
| Recursion works | 12 direct children, 4 of which delegated onward to 12 grandchildren; the root itself made 14 `subagent` calls. |
|
|
133
|
+
| The depth cap holds | The tree stops at depth 2; no depth-3 session exists anywhere in the log directory. |
|
|
134
|
+
| The live-child cap is soft, not fatal | One `subagent limit reached (active child limit: 8)` during an 8-wide fan-out. Settlements returned slots as children finished, and every later delegation succeeded. |
|
|
135
|
+
|
|
136
|
+
**Corrected after the fact**: an earlier version of this file claimed the mask made
|
|
137
|
+
children leaf agents. It does not. `subagent` is registered per composed agent rather
|
|
138
|
+
than globally, so the mask can never name it — and because a restriction filters
|
|
139
|
+
globals only, a child keeps it. Recursion is bounded by `maxDepth`, not by the mask.
|
|
140
|
+
|
|
141
|
+
**How to check it yourself**: `tools/verify-session.mjs` proves which preset each
|
|
142
|
+
session ran and at what depth, but it does not read tool schemas. Use
|
|
143
|
+
`tools/extract-offered-tools.mjs` for the per-level tool list,
|
|
144
|
+
`tools/check-pwsh-background.mjs` for the process bound, and
|
|
145
|
+
`tools/count-tool-calls.mjs` for what a level actually called.
|
|
128
146
|
|
|
129
147
|
## Contributing
|
|
130
148
|
|
package/README.zh.md
CHANGED
|
@@ -102,9 +102,20 @@ dsh plugin --profile web add github:XiaoOu2024/dsh-root-planner-preset
|
|
|
102
102
|
|
|
103
103
|
**已验证**:补丁可被 loader 解析;解析出的人设与 `persona.txt` **逐字节相同**;两个委派工具的白名单排除了所有写入入口、且只包含**运行时报告的全局工具**;干净检出后校验通过(`.gitattributes` 已钉死 LF,否则 Windows 克隆会破坏逐字节校验);深度覆盖已落到合成配置。
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
**已在真实任务上端到端验证**——那次运行在一个大型逆向工程目录树上产生了 **24 个后代会话**。下面每一条都读自**会话日志自己持久化的工具 schema**,那是注册表的输出,不是模型的回忆:
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
| 性质 | 证据 |
|
|
108
|
+
|---|---|
|
|
109
|
+
| 白名单覆盖每一层 | 根被提供 **31** 个工具;深度 1 与深度 2 的 agent 都是 **25** 个。差额正好是白名单排除的那 6 个:`workflow`、`present`、`job_output`、`job_list`、`job_kill`、`exit_plan_mode`。 |
|
|
110
|
+
| 子代理确实是工人 | `write`、`edit`、`ops_replace`、`pwsh` 在每一层都在,而且真的被用了:根 10 次 `write` + 60 次 `edit`,某个深度 1 的子代理 14 次 `write`,某个深度 2 的孙代理 20 次 `write`。 |
|
|
111
|
+
| 子代理留不下运行中的进程 | `run_in_background` 在三个层级的 `pwsh` schema 里**都不存在**。(朴素的全日志搜索会给出相反答案——`ops_bash` 自己的描述里也有这个词,所以要用 `tools/check-pwsh-background.mjs` 看 schema 窗口。) |
|
|
112
|
+
| 递归成立 | 12 个直接子代理,其中 4 个继续派出了 12 个孙代理;根自己做了 14 次 `subagent` 调用。 |
|
|
113
|
+
| 深度上限有效 | 树停在深度 2;整个日志目录里不存在任何深度 3 的会话。 |
|
|
114
|
+
| 在世上限是软拒绝而非致命 | 8 路扇出时出现一次 `subagent limit reached (active child limit: 8)`;子代理完结时结算通知归还了槽位,此后每次委派都成功。 |
|
|
115
|
+
|
|
116
|
+
**一处事后更正**:本文件早期版本称"白名单让子代理成为叶子"。**并非如此。** `subagent` 是按每个被组合 Agent 注册、而非全局注册,所以白名单**永远无法写下它的名字**;而限制只过滤全局工具,因此子代理**保有此工具**。递归的边界是 `maxDepth`,不是白名单。
|
|
117
|
+
|
|
118
|
+
**想自己核**:`tools/verify-session.mjs` 能证明每个会话跑的是哪个预设、深度多少,但它不读工具 schema。逐层工具清单用 `tools/extract-offered-tools.mjs`,进程边界用 `tools/check-pwsh-background.mjs`,实际调用用 `tools/count-tool-calls.mjs`。
|
|
108
119
|
|
|
109
120
|
## 参与维护
|
|
110
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-root-planner-preset",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH agent preset that outsources file reading to strictly read-only subagents, while the root agent keeps the plan and owns every file mutation.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Checks whether `run_in_background` still exists inside a session's `pwsh` tool
|
|
2
|
+
// schema — the one property this preset enforces structurally rather than by asking
|
|
3
|
+
// the model nicely.
|
|
4
|
+
//
|
|
5
|
+
// A whole-log substring search cannot answer it: the phrase also appears in
|
|
6
|
+
// `ops_bash`'s own description on this machine, so the naive check reports "present"
|
|
7
|
+
// for a session where the parameter has genuinely been removed.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node tools/check-pwsh-background.mjs <session-dir> [...]
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
14
|
+
|
|
15
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
16
|
+
function frameEnd(buf) {
|
|
17
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
18
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
19
|
+
}
|
|
20
|
+
return buf.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
24
|
+
async function decompressAll(buf) {
|
|
25
|
+
let rest = buf;
|
|
26
|
+
let text = '';
|
|
27
|
+
while (rest.length > 0) {
|
|
28
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
29
|
+
try {
|
|
30
|
+
text += await new Promise((resolve, reject) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
const dec = createZstdDecompress();
|
|
33
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
34
|
+
dec.on('error', reject);
|
|
35
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
36
|
+
dec.end(slice);
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
if (slice.length >= rest.length) break;
|
|
42
|
+
rest = rest.subarray(slice.length);
|
|
43
|
+
}
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find every `"name":"<tool>"` occurrence and return the window of text that follows
|
|
49
|
+
* it — close enough to hold that tool's own parameters object.
|
|
50
|
+
* @param text - whole decompressed log.
|
|
51
|
+
* @param tool - tool name to locate.
|
|
52
|
+
* @returns the following windows, longest first.
|
|
53
|
+
*/
|
|
54
|
+
function windowsAfter(text, tool) {
|
|
55
|
+
const needle = `\\"name\\":\\"${tool}\\"`;
|
|
56
|
+
const plain = `"name":"${tool}"`;
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const pattern of [needle, plain]) {
|
|
59
|
+
let from = 0;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const at = text.indexOf(pattern, from);
|
|
62
|
+
if (at === -1) break;
|
|
63
|
+
out.push(text.slice(at, at + 4000));
|
|
64
|
+
from = at + pattern.length;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out.sort((a, b) => b.length - a.length);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const dir of process.argv.slice(2)) {
|
|
71
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
72
|
+
const header = JSON.parse(text.split('\n')[0]);
|
|
73
|
+
|
|
74
|
+
const pwshWindows = windowsAfter(text, 'pwsh');
|
|
75
|
+
// The parameters object is the one that mentions the tool's own argument names.
|
|
76
|
+
const schemaWindow = pwshWindows.find((w) => w.includes('workdir') || w.includes('command')) ?? pwshWindows[0] ?? '';
|
|
77
|
+
const backgroundInSchema = /run_in_background/.test(schemaWindow);
|
|
78
|
+
const backgroundAnywhere = text.includes('run_in_background');
|
|
79
|
+
|
|
80
|
+
process.stdout.write(
|
|
81
|
+
`\n${dir.split(/[\\/]/).pop().slice(0, 20)} depth=${header?.delegationDepth}\n` +
|
|
82
|
+
` "run_in_background" anywhere in the log : ${backgroundAnywhere}\n` +
|
|
83
|
+
` inside the pwsh schema window : ${backgroundInSchema}` +
|
|
84
|
+
`${backgroundInSchema ? ' <-- the parameter is still offered' : ' <-- removed, as intended'}\n` +
|
|
85
|
+
` pwsh schema window size : ${schemaWindow.length} chars\n`,
|
|
86
|
+
);
|
|
87
|
+
if (backgroundAnywhere && !backgroundInSchema) {
|
|
88
|
+
const at = text.indexOf('run_in_background');
|
|
89
|
+
process.stdout.write(` first mention context: …${text.slice(Math.max(0, at - 120), at + 60)}…\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Reports whether each child session in a run is finished or still active, from its
|
|
2
|
+
// own log — the question behind "subagent limit reached": are slots being held by
|
|
3
|
+
// children that already stopped, or by children that are genuinely still working?
|
|
4
|
+
//
|
|
5
|
+
// Usage: node tools/child-status.mjs <root-session-dir>
|
|
6
|
+
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
12
|
+
function frameEnd(buf) {
|
|
13
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
14
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
15
|
+
}
|
|
16
|
+
return buf.length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
20
|
+
async function decompressAll(buf) {
|
|
21
|
+
let rest = buf;
|
|
22
|
+
let text = '';
|
|
23
|
+
while (rest.length > 0) {
|
|
24
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
25
|
+
try {
|
|
26
|
+
text += await new Promise((resolve, reject) => {
|
|
27
|
+
const chunks = [];
|
|
28
|
+
const dec = createZstdDecompress();
|
|
29
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
30
|
+
dec.on('error', reject);
|
|
31
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
32
|
+
dec.end(slice);
|
|
33
|
+
});
|
|
34
|
+
} catch {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
if (slice.length >= rest.length) break;
|
|
38
|
+
rest = rest.subarray(slice.length);
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rootDir = process.argv[2];
|
|
44
|
+
if (rootDir === undefined) {
|
|
45
|
+
process.stderr.write('usage: node tools/child-status.mjs <root-session-dir>\n');
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const base = join(rootDir, '..');
|
|
50
|
+
const rootId = rootDir.split(/[\\/]/).pop();
|
|
51
|
+
const rows = [];
|
|
52
|
+
|
|
53
|
+
for (const name of readdirSync(base)) {
|
|
54
|
+
const file = join(base, name, 'session.v4.jsonl.zstd');
|
|
55
|
+
try {
|
|
56
|
+
if (!statSync(file).isFile()) continue;
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const text = await decompressAll(readFileSync(file));
|
|
61
|
+
const events = text
|
|
62
|
+
.split('\n')
|
|
63
|
+
.filter((line) => line.trim().length > 0)
|
|
64
|
+
.flatMap((line) => {
|
|
65
|
+
try {
|
|
66
|
+
return [JSON.parse(line)];
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const header = events[0];
|
|
72
|
+
if (name !== rootId && header?.origin !== 'subagent') continue;
|
|
73
|
+
|
|
74
|
+
const types = new Map();
|
|
75
|
+
for (const event of events) types.set(event.type, (types.get(event.type) ?? 0) + 1);
|
|
76
|
+
|
|
77
|
+
const last = events[events.length - 1];
|
|
78
|
+
rows.push({
|
|
79
|
+
id: name,
|
|
80
|
+
depth: header?.delegationDepth,
|
|
81
|
+
mtime: statSync(file).mtime,
|
|
82
|
+
events: events.length,
|
|
83
|
+
lastType: last?.type,
|
|
84
|
+
lastTime: last?.time,
|
|
85
|
+
// A turn that ended and left nothing pending is the shape of a settled child.
|
|
86
|
+
turnEnds: types.get('turn/end') ?? 0,
|
|
87
|
+
hasInboxPending: types.has('agent/inbox/spliced') && !types.has('agent/inbox/claimed'),
|
|
88
|
+
cancels: types.get('agent/cancel') ?? 0,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
rows.sort((a, b) => (a.lastTime ?? 0) - (b.lastTime ?? 0));
|
|
93
|
+
const fmt = (ms) => (ms === undefined ? '?' : new Date(ms).toISOString().slice(11, 19));
|
|
94
|
+
|
|
95
|
+
process.stdout.write(`${rows.length} agent session(s) under ${base}\n\n`);
|
|
96
|
+
process.stdout.write('depth turnEnds events last event type last event time session\n');
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
process.stdout.write(
|
|
99
|
+
`${String(row.depth ?? '-').padEnd(6)} ${String(row.turnEnds).padEnd(9)} ${String(row.events).padEnd(7)} ` +
|
|
100
|
+
`${String(row.lastType ?? '-').padEnd(25)} ${fmt(row.lastTime).padEnd(16)} ${row.id.slice(0, 20)}\n`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Counts the tool calls actually made in one session, by name.
|
|
2
|
+
//
|
|
3
|
+
// Written to settle a question that string counting cannot: which delegation tool a
|
|
4
|
+
// given level really called, and therefore which tools the child mask actually left
|
|
5
|
+
// visible. A raw occurrence count of "subagent" matches "subagent_fork" too, and the
|
|
6
|
+
// preset's own prose mentions both.
|
|
7
|
+
//
|
|
8
|
+
// Usage: node tools/count-tool-calls.mjs <session-dir> [...]
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
13
|
+
|
|
14
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
15
|
+
function frameEnd(buf) {
|
|
16
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
17
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
18
|
+
}
|
|
19
|
+
return buf.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
23
|
+
async function decompressAll(buf) {
|
|
24
|
+
let rest = buf;
|
|
25
|
+
let text = '';
|
|
26
|
+
while (rest.length > 0) {
|
|
27
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
28
|
+
try {
|
|
29
|
+
text += await new Promise((resolve, reject) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
const dec = createZstdDecompress();
|
|
32
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
33
|
+
dec.on('error', reject);
|
|
34
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
35
|
+
dec.end(slice);
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (slice.length >= rest.length) break;
|
|
41
|
+
rest = rest.subarray(slice.length);
|
|
42
|
+
}
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const dir of process.argv.slice(2)) {
|
|
47
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
48
|
+
const events = text
|
|
49
|
+
.split('\n')
|
|
50
|
+
.filter((line) => line.trim().length > 0)
|
|
51
|
+
.flatMap((line) => {
|
|
52
|
+
try {
|
|
53
|
+
return [JSON.parse(line)];
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const header = events[0];
|
|
60
|
+
const calls = new Map();
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
if (event.type !== 'tool/call') continue;
|
|
63
|
+
const name = event.data?.name ?? event.data?.message?.name;
|
|
64
|
+
if (typeof name !== 'string') continue;
|
|
65
|
+
calls.set(name, (calls.get(name) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const name = dir.split(/[\\/]/).pop().slice(0, 20);
|
|
69
|
+
process.stdout.write(
|
|
70
|
+
`\n${name} depth=${header?.delegationDepth} origin=${header?.origin} ${events.length} events\n`,
|
|
71
|
+
);
|
|
72
|
+
const rows = [...calls.entries()].sort((a, b) => b[1] - a[1]);
|
|
73
|
+
process.stdout.write(` ${rows.length} distinct tools called: ${rows.map(([t, n]) => `${t}×${n}`).join(', ') || '(none)'}\n`);
|
|
74
|
+
const delegation = rows.filter(([t]) => t.startsWith('subagent'));
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
` delegation calls: ${delegation.map(([t, n]) => `${t}×${n}`).join(', ') || '(none)'}\n`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Extracts the tool names a session was actually offered, from the tool schemas the
|
|
2
|
+
// session log persists.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: a subagent's own account of its tools is a language-model
|
|
5
|
+
// recollection, and it can be wrong in both directions (the first such report
|
|
6
|
+
// omitted `write` from its list while claiming to discuss it). The persisted
|
|
7
|
+
// schemas are the registry's own output, so they settle questions like "does the
|
|
8
|
+
// child mask really apply?" and "is run_in_background gone?" without guessing.
|
|
9
|
+
//
|
|
10
|
+
// Usage: node tools/extract-offered-tools.mjs <session-dir> [...]
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
15
|
+
|
|
16
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
17
|
+
function frameEnd(buf) {
|
|
18
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
19
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
20
|
+
}
|
|
21
|
+
return buf.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
25
|
+
async function decompressAll(buf) {
|
|
26
|
+
let rest = buf;
|
|
27
|
+
let text = '';
|
|
28
|
+
while (rest.length > 0) {
|
|
29
|
+
const slice = rest.subarray(0, frameEnd(rest));
|
|
30
|
+
try {
|
|
31
|
+
text += await new Promise((resolve, reject) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const dec = createZstdDecompress();
|
|
34
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
35
|
+
dec.on('error', reject);
|
|
36
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
37
|
+
dec.end(slice);
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (slice.length >= rest.length) break;
|
|
43
|
+
rest = rest.subarray(slice.length);
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const dir of process.argv.slice(2)) {
|
|
49
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
50
|
+
|
|
51
|
+
// A tool schema is an object carrying `name` plus `parameters` (and `description`).
|
|
52
|
+
// Collect every name that appears next to a parameters object anywhere in the log.
|
|
53
|
+
const names = new Set();
|
|
54
|
+
for (const match of text.matchAll(/\{\\?"name\\?":\\?"([a-zA-Z_][a-zA-Z0-9_]*)\\?":\{[^{}]*\\?"type\\?":\\?"object\\?"/g)) {
|
|
55
|
+
names.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
for (const match of text.matchAll(/\\?"name\\?":\\?"([a-zA-Z_][a-zA-Z0-9_]*)\\?",\\?"description\\?":/g)) {
|
|
58
|
+
names.add(match[1]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const header = JSON.parse(text.split('\n')[0]);
|
|
62
|
+
const listed = [...names].sort();
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
`\n${dir.split(/[\\/]/).pop().slice(0, 20)} depth=${header?.delegationDepth}\n ${listed.length} names: ${listed.join(', ')}\n`,
|
|
65
|
+
);
|
|
66
|
+
for (const probe of ['workflow', 'present', 'job_output', 'exit_plan_mode', 'subagent', 'subagent_fork', 'pwsh', 'write']) {
|
|
67
|
+
process.stdout.write(` ${probe.padEnd(16)} ${names.has(probe) ? 'present' : 'ABSENT'}\n`);
|
|
68
|
+
}
|
|
69
|
+
}
|