dsh-root-planner-preset 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tools/extract-tool-inventory.mjs +65 -0
- package/tools/verify-session.mjs +57 -32
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.1",
|
|
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,65 @@
|
|
|
1
|
+
// Extracts the runtime's own global-tool inventory from a session log.
|
|
2
|
+
//
|
|
3
|
+
// `tools.restrict()` prints every known global tool when it refuses a name, so a
|
|
4
|
+
// session that hit `names unknown global tool` carries the authoritative list for
|
|
5
|
+
// its own environment. That list is what the child mask has to be a subset of, and
|
|
6
|
+
// it is the only trustworthy source — a mask built from memory breaks every child
|
|
7
|
+
// creation, which is exactly what happened twice in this repository.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node tools/extract-tool-inventory.mjs <session.jsonl.zstd> [...]
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
13
|
+
|
|
14
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
15
|
+
function firstFrameEnd(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
|
+
/**
|
|
23
|
+
* Decompress a concatenated-zstd log frame by frame. A single decompress call
|
|
24
|
+
* stops at the first frame, which is why a naive read of these logs returns a few
|
|
25
|
+
* hundred bytes out of tens of kilobytes and hides every event.
|
|
26
|
+
* @param {string} path - the .jsonl.zstd log.
|
|
27
|
+
* @returns {Promise<string>} the whole decompressed text.
|
|
28
|
+
*/
|
|
29
|
+
async function decompressAll(path) {
|
|
30
|
+
let rest = readFileSync(path);
|
|
31
|
+
let text = '';
|
|
32
|
+
while (rest.length > 0) {
|
|
33
|
+
const slice = rest.subarray(0, firstFrameEnd(rest));
|
|
34
|
+
try {
|
|
35
|
+
text += await new Promise((resolve, reject) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
const decoder = createZstdDecompress();
|
|
38
|
+
decoder.on('data', (chunk) => chunks.push(chunk));
|
|
39
|
+
decoder.on('error', reject);
|
|
40
|
+
decoder.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
41
|
+
decoder.end(slice);
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
if (slice.length >= rest.length) break;
|
|
47
|
+
rest = rest.subarray(slice.length);
|
|
48
|
+
}
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const path of process.argv.slice(2)) {
|
|
53
|
+
const text = await decompressAll(path);
|
|
54
|
+
const match = /known global tools: ([^"\\\]]+)/.exec(text);
|
|
55
|
+
process.stdout.write(`\n=== ${path.split(/[\\/]/).pop()} (${text.length} chars decompressed) ===\n`);
|
|
56
|
+
if (match === null) {
|
|
57
|
+
process.stdout.write(`no inventory line found. mentions "unknown global tool": ${text.includes('unknown global tool')}\n`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const tools = match[1]
|
|
61
|
+
.split(',')
|
|
62
|
+
.map((name) => name.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
process.stdout.write(`${tools.length} global tools:\n ${tools.join('\n ')}\n`);
|
|
65
|
+
}
|
package/tools/verify-session.mjs
CHANGED
|
@@ -17,42 +17,45 @@ import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { createZstdDecompress } from 'node:zlib';
|
|
19
19
|
|
|
20
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
21
|
+
function frameBytes(buf) {
|
|
22
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
23
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
24
|
+
}
|
|
25
|
+
return buf.length;
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
/**
|
|
21
|
-
* Decompress a concatenated-zstd file
|
|
22
|
-
*
|
|
23
|
-
*
|
|
29
|
+
* Decompress a concatenated-zstd file frame by frame.
|
|
30
|
+
*
|
|
31
|
+
* A single decompress call stops at the first frame, which is why a naive read of
|
|
32
|
+
* these logs returns 323 bytes out of 139 KB and silently hides every event.
|
|
33
|
+
* Measured on a real log: frame-by-frame reads 143 654 characters where one call
|
|
34
|
+
* read 323.
|
|
24
35
|
* @param {Buffer} buf - whole file.
|
|
25
36
|
* @returns {Promise<string>} the complete decompressed text.
|
|
26
37
|
*/
|
|
27
38
|
async function decompressAll(buf) {
|
|
28
|
-
const out = [];
|
|
29
39
|
let rest = buf;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return out.join('');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Locate the end of the first zstd frame by magic-number scanning. */
|
|
51
|
-
function frameBytes(buf, ) {
|
|
52
|
-
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
53
|
-
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
40
|
+
let text = '';
|
|
41
|
+
while (rest.length > 0) {
|
|
42
|
+
const slice = rest.subarray(0, frameBytes(rest));
|
|
43
|
+
try {
|
|
44
|
+
text += await new Promise((resolve, reject) => {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
const dec = createZstdDecompress();
|
|
47
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
48
|
+
dec.on('error', reject);
|
|
49
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
50
|
+
dec.end(slice);
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
if (slice.length >= rest.length) break;
|
|
56
|
+
rest = rest.subarray(slice.length);
|
|
54
57
|
}
|
|
55
|
-
return
|
|
58
|
+
return text;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/** Read one session directory into {header, events}. */
|
|
@@ -86,6 +89,7 @@ const base = join(rootDir, '..');
|
|
|
86
89
|
const root = await readSession(rootDir);
|
|
87
90
|
|
|
88
91
|
const sessions = [{ dir: rootDir, ...root }];
|
|
92
|
+
const candidates = [];
|
|
89
93
|
for (const name of readdirSync(base)) {
|
|
90
94
|
if (name === rootDir.split(/[\\/]/).pop()) continue;
|
|
91
95
|
const candidate = join(base, name);
|
|
@@ -96,10 +100,31 @@ for (const name of readdirSync(base)) {
|
|
|
96
100
|
}
|
|
97
101
|
const session = await readSession(candidate);
|
|
98
102
|
// Only real children count. Sibling ROOT sessions of other conversations live in
|
|
99
|
-
// the same directory and have no parentSession — including them made
|
|
100
|
-
// claim
|
|
103
|
+
// the same directory and have no parentSession — including them made this report
|
|
104
|
+
// claim a run had failed when it had not.
|
|
101
105
|
if (session.header?.origin !== 'subagent') continue;
|
|
102
|
-
|
|
106
|
+
candidates.push({ dir: candidate, ...session });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Keep only the root's real descendants. Other presets delegate too, and their
|
|
111
|
+
* subagent sessions sit in the same directory; without this walk they appear as
|
|
112
|
+
* children of this run and fail the preset check for the wrong reason.
|
|
113
|
+
*/
|
|
114
|
+
const rootId = root.header?.id;
|
|
115
|
+
const childrenOf = new Map();
|
|
116
|
+
for (const session of candidates) {
|
|
117
|
+
const parent = session.header?.parentSession;
|
|
118
|
+
if (parent === undefined) continue;
|
|
119
|
+
const list = childrenOf.get(parent) ?? [];
|
|
120
|
+
list.push(session);
|
|
121
|
+
childrenOf.set(parent, list);
|
|
122
|
+
}
|
|
123
|
+
const queue = [...(childrenOf.get(rootId) ?? [])];
|
|
124
|
+
while (queue.length > 0) {
|
|
125
|
+
const session = queue.shift();
|
|
126
|
+
sessions.push(session);
|
|
127
|
+
queue.push(...(childrenOf.get(session.header?.id) ?? []));
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
const failures = [];
|