dsh-root-planner-preset 1.1.3 → 1.1.5
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 +16 -0
- package/README.zh.md +8 -0
- package/child-tools.mjs +19 -7
- package/cordis.patch.yml +12 -11
- package/package.json +1 -1
- package/persona.txt +12 -11
- package/release.mjs +22 -1
- package/tools/inspect-preset-events.mjs +85 -0
- package/tools/verify-session.mjs +54 -7
package/README.md
CHANGED
|
@@ -21,10 +21,26 @@ This preset takes the middle line: **workers with tools, bounded by their task.*
|
|
|
21
21
|
| Nobody runs an unbounded service | Persona rules for the root and for subagents | Prompt-level, covering what a schema cannot (`ops_bash` has no job registry) |
|
|
22
22
|
| A subagent stays inside the task it was given | Persona rules: named files only, no drive-by fixes, no racing siblings | Prompt-level |
|
|
23
23
|
| Delegation depth is the user's setting | The runtime depth cap (`subagent.maxDepth`) | **Structural**: past the cap it throws `SubagentDepthError` |
|
|
24
|
+
| Live children are capped as well | `subagent.maxActiveSubagents` | **Structural**: past the cap a delegation fails with `subagent limit reached`, and it does not queue |
|
|
24
25
|
| The root decides; workers execute | Persona rules | Prompt-level |
|
|
25
26
|
|
|
26
27
|
The mask is an **allow** list: a tool added by a future plugin is invisible to subagents until it is added here.
|
|
27
28
|
|
|
29
|
+
### Two limits you will meet on a broad task
|
|
30
|
+
|
|
31
|
+
Both are Host settings, both are structural, and both are documented because they read
|
|
32
|
+
like defects the first time:
|
|
33
|
+
|
|
34
|
+
- **`subagent limit reached (active child limit: N)`** — a delegated child is
|
|
35
|
+
*resident*, not merely running: a slot returns only after its turn ends, its inbox
|
|
36
|
+
is empty, and its own children are disposed. A wide fan-out therefore holds every
|
|
37
|
+
slot until that work actually finishes, and the batch does not queue behind the
|
|
38
|
+
cap. Raise `maxActiveSubagents` (**Settings → Subagents**) if the machine can
|
|
39
|
+
carry more live agents, or dispatch a batch you can finish and collect it before
|
|
40
|
+
opening the next. The author's own profile carries 16 for this reason.
|
|
41
|
+
- **`subagent depth N exceeds maxDepth M`** — a delegation from a leaf. That is the
|
|
42
|
+
cap working, not a failure; the child reads the material itself.
|
|
43
|
+
|
|
28
44
|
## How it works
|
|
29
45
|
|
|
30
46
|
```
|
package/README.zh.md
CHANGED
|
@@ -21,10 +21,18 @@
|
|
|
21
21
|
| 谁都不跑无界服务 | 根与子代理的人设规则 | 提示词级,覆盖 schema 管不到的部分(`ops_bash` 没有任务注册表) |
|
|
22
22
|
| 子代理不越出被交代的任务 | 人设规则:只动点名文件、不顺手修、不与兄弟抢文件 | 提示词级 |
|
|
23
23
|
| 委派层数由用户配置 | 运行时深度上限(`subagent.maxDepth`) | **结构性**:超过上限直接抛 `SubagentDepthError` |
|
|
24
|
+
| 在世子代理数也有上限 | `subagent.maxActiveSubagents` | **结构性**:超过上限时委派直接失败并报 `subagent limit reached`,**不会排队** |
|
|
24
25
|
| 根决策、工人执行 | 人设规则 | 提示词级 |
|
|
25
26
|
|
|
26
27
|
白名单是 **allow 而非 deny**:将来某个插件新增的工具,对子代理默认不可见。
|
|
27
28
|
|
|
29
|
+
### 大任务上你会撞到的两个上限
|
|
30
|
+
|
|
31
|
+
两个都是 Host 设置、都是结构性的,也都列在这里——因为它们第一次出现时看着像 bug:
|
|
32
|
+
|
|
33
|
+
- **`subagent limit reached (active child limit: N)`** —— 子代理是**驻留**的,不只是"在跑":只有它的轮次结束、收件箱清空、它自己的子代理也销毁之后,槽位才归还。所以宽扇出会把槽位一直占着,直到那批活真正做完;而且**超额的那批不会排队**。机器扛得住就把 `maxActiveSubagents`(**设置 → 子智能体**)调大,否则只派一批你能做完的、收完结果再开下一批。作者自己的 profile 因此设为 16。
|
|
34
|
+
- **`subagent depth N exceeds maxDepth M`** —— 叶子层发起委派。这是上限在正常工作,不是故障;子代理自己读就是了。
|
|
35
|
+
|
|
28
36
|
## 工作方式
|
|
29
37
|
|
|
30
38
|
```
|
package/child-tools.mjs
CHANGED
|
@@ -63,13 +63,25 @@ export const CHILD_TOOLS = [
|
|
|
63
63
|
// Running commands. `pwsh` is the one shell this profile reports as a global
|
|
64
64
|
// tool, and background jobs are switched off for it in the preset (see above).
|
|
65
65
|
//
|
|
66
|
-
// `ops_bash` is deliberately NOT here
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
66
|
+
// `ops_bash` is deliberately NOT here, and it is not an oversight — it CANNOT be
|
|
67
|
+
// here, for the same reason `subagent` cannot:
|
|
68
|
+
//
|
|
69
|
+
// * dsh-ops publishes it per AGENT, not globally. `lib/session-shells.js`
|
|
70
|
+
// mounts a child plugin on each `agent.ctx` and registers the tool there
|
|
71
|
+
// (`entry.ctx.tools.register(definition)`), then walks the agents again to
|
|
72
|
+
// `tools.restrict({ deny: [BASH_TOOL] })` on the ones it may not serve. So it
|
|
73
|
+
// never appears in the runtime's global inventory, and naming it in a mask
|
|
74
|
+
// fails child creation the way `subagent` did.
|
|
75
|
+
// * It cannot be granted because it does not need granting. `allowed(agent)`
|
|
76
|
+
// tests the agent's OWN session policy — full access, or a confinement
|
|
77
|
+
// provider available — and a child qualifies on the same terms as the root,
|
|
78
|
+
// so a child receives `ops_bash` from dsh-ops by itself. That project's own
|
|
79
|
+
// manual-validation notes say it plainly: a restricted session does not
|
|
80
|
+
// publish `ops_bash`; it appears only with full access, `publishBashTool`,
|
|
81
|
+
// a resolvable bash payload and an available subprocess service.
|
|
82
|
+
//
|
|
83
|
+
// The consequence worth knowing: a child's shell can differ from the root's
|
|
84
|
+
// because of its own permission mode, not because of anything this mask says.
|
|
73
85
|
'pwsh',
|
|
74
86
|
'todo_write',
|
|
75
87
|
'ask_user_question',
|
package/cordis.patch.yml
CHANGED
|
@@ -42,17 +42,18 @@
|
|
|
42
42
|
## Root agent rules
|
|
43
43
|
|
|
44
44
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
45
|
-
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
46
|
-
3.
|
|
47
|
-
4.
|
|
48
|
-
5.
|
|
49
|
-
6.
|
|
50
|
-
7.
|
|
51
|
-
8.
|
|
52
|
-
9.
|
|
53
|
-
10.
|
|
54
|
-
11.
|
|
55
|
-
12.
|
|
45
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
46
|
+
3. **Mind the live capacity, not just the work.** Delegated children are resident: a slot returns only after a child's turn ends, its inbox is empty, and its own children are gone. A wide fan-out therefore holds every slot until the work actually finishes, and starting more than the environment allows fails outright with a limit error — the batch does not queue. So dispatch a batch you can finish, collect its results before opening the next, and when a delegation is refused for capacity, that is information about your scheduling rather than a reason to retry: use the agents you already have, or finish what is in flight first. Neither the depth limit nor this capacity is yours to change; plan inside them.
|
|
47
|
+
4. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
48
|
+
5. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
49
|
+
6. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 9 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
50
|
+
7. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
51
|
+
8. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
52
|
+
9. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
53
|
+
10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
54
|
+
11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
55
|
+
12. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
56
|
+
13. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
56
57
|
|
|
57
58
|
## Subagent rules
|
|
58
59
|
|
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.5",
|
|
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",
|
package/persona.txt
CHANGED
|
@@ -21,17 +21,18 @@ Reading is not planning. Raw file contents and raw query results belong in the a
|
|
|
21
21
|
## Root agent rules
|
|
22
22
|
|
|
23
23
|
1. Plan first. Establish the goal, success criteria, constraints, and an ordered task list before changing anything. Revise the plan when evidence contradicts it.
|
|
24
|
-
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
28
|
-
6.
|
|
29
|
-
7.
|
|
30
|
-
8.
|
|
31
|
-
9.
|
|
32
|
-
10.
|
|
33
|
-
11.
|
|
34
|
-
12.
|
|
24
|
+
2. Decompose heavy work; do not delegate light work. Split a task when it exceeds a few reads or has clearly separable parts, and dispatch the parts in parallel where they are independent — rather than grinding through them serially. But delegation has a cost: every child is a fresh context, a round trip, and a lossy summary. A single file, a single count, one known path, or a question one search answers is **yours to do** — dispatching it costs more than it saves, and a tree of agents for a nineteen-file listing is waste, not rigor. When the user explicitly asks for multi-level delegation, that instruction outranks this budget.
|
|
25
|
+
3. **Mind the live capacity, not just the work.** Delegated children are resident: a slot returns only after a child's turn ends, its inbox is empty, and its own children are gone. A wide fan-out therefore holds every slot until the work actually finishes, and starting more than the environment allows fails outright with a limit error — the batch does not queue. So dispatch a batch you can finish, collect its results before opening the next, and when a delegation is refused for capacity, that is information about your scheduling rather than a reason to retry: use the agents you already have, or finish what is in flight first. Neither the depth limit nor this capacity is yours to change; plan inside them.
|
|
26
|
+
4. Delegate research by default — within that budget. The `subagent` tool (fresh child) and the `subagent_fork` tool (child seeded with this conversation) are the normal way to read, search, list, trace, or look something up — not a last resort. Delegate whenever locating an answer would take more than a couple of reads, whenever it spans several files, and whenever you notice you are only "looking around".
|
|
27
|
+
5. Keep delegations independent and concurrent. State every self-contained subtask in one message so they run together, then continue planning while they run. Wait only when your next action needs a specific result.
|
|
28
|
+
6. Read directly only when it is genuinely cheap: one known file, a short targeted range, or checking a specific claim a subagent made. Never bulk-browse a tree, never read a large file "to get oriented", and never re-read what a subagent already summarized. **Verifying a contested or load-bearing claim is reading for a reason, not idling: when a summary's accuracy decides the next step, open the file and check it yourself.** Rule 9 tells you to distrust a summary; this is the clause that lets you act on that.
|
|
29
|
+
7. Ask for the summary shape you need. Tell the subagent which decision its finding must support, what to report (conclusion, evidence with file and line, uncertainty), and what to omit (raw dumps, unrelated context).
|
|
30
|
+
8. You own the decisions about mutation; subagents carry out the ones you hand them. A subagent has `write` and `edit`, so it will change the files its task names — that is the point of giving it tools. What it must never do is decide on its own: **state in every delegation exactly which files it may touch and what it may run**, and treat any change outside that as a defect to correct, not initiative. Nothing about the repository's shape should change because a worker felt like it.
|
|
31
|
+
9. Treat a summary as evidence, not a verdict. Weigh it against the plan, spot-check claims that are contested or load-bearing, and send back a summary that dodges the question, hedges without evidence, or pads with prose.
|
|
32
|
+
10. **Leave nothing running.** No servers, watchers, daemons, or detached processes — for you as much as for a subagent. Background execution is your shell's decision to offer, not a convenience to reach for: if a task seems to need a long-lived process, do it in the foreground, bound it with a timeout, or ask the user before starting anything that outlives the work.
|
|
33
|
+
11. Answer a subagent that reports a problem or an out-of-scope finding. Decide it, then reply with `send_message` so the child can continue: either the change is approved and who makes it, or it is out of scope and why, or what to do instead. An unanswered report leaves the child blocked on a decision only you can make. **A child that reports a clean result needs no reply** — silence is the correct response, so do not invent an acknowledgement.
|
|
34
|
+
12. Give concurrent subagents disjoint work. Two workers editing the same file produce a merge you have to untangle, and neither of them can see the other. Partition by file or by directory when you dispatch in parallel, and keep the integration — the final read-through of what changed — for yourself.
|
|
35
|
+
13. Decide who runs it, and do not hand a command down out of habit. Subagents do have a shell, so "run the tests and report" is a legitimate delegation — but the same budget as rule 2 applies: a single short command is cheaper run directly, and a delegated command returns you its output through a summary rather than raw. Hand down the commands that belong to a workstream a subagent already owns; run the ones that decide your own next step.
|
|
35
36
|
|
|
36
37
|
## Subagent rules
|
|
37
38
|
|
package/release.mjs
CHANGED
|
@@ -39,7 +39,8 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
39
39
|
const OFFICIAL_REGISTRY = 'https://registry.npmjs.org/';
|
|
40
40
|
const PROXY = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY ?? 'http://127.0.0.1:7897';
|
|
41
41
|
const GIT = process.env.DSH_RELEASE_GIT ?? 'git';
|
|
42
|
-
|
|
42
|
+
// `gh` is not installed on PATH on this machine; it lives in a standalone extract.
|
|
43
|
+
const GH = process.env.DSH_RELEASE_GH ?? 'D:/Tools/gh/2.102.0/bin/gh.exe';
|
|
43
44
|
const PUBLISHER = process.env.DSH_RELEASE_PUBLISHER ?? (process.platform === 'win32' ? 'pnpm' : 'npm');
|
|
44
45
|
|
|
45
46
|
const argv = process.argv.slice(2);
|
|
@@ -219,6 +220,26 @@ step(`Upgrade the local "${profile}" profile so the machine runs what was publis
|
|
|
219
220
|
if (dryRun || skipInstall) {
|
|
220
221
|
process.stdout.write(' skipped\n');
|
|
221
222
|
} else {
|
|
223
|
+
// The profile installs through the CONFIGURED registry, which is a mirror that
|
|
224
|
+
// lags the official one — so a version that just went live can still answer
|
|
225
|
+
// "No matching version found" here. That is what made this step fail after the
|
|
226
|
+
// 1.1.4 release, with nothing wrong on the npm side. Wait for the mirror, then
|
|
227
|
+
// name the official registry anyway so the install works either way.
|
|
228
|
+
const mirror = process.env.DSH_RELEASE_MIRROR ?? 'https://registry.npmmirror.com/';
|
|
229
|
+
const probeMirror = () =>
|
|
230
|
+
spawnSync('curl', ['-s', '-o', '/dev/null', '-w', '%{http_code}', '-m', '20', '-x', PROXY, `${mirror}${manifest.name}/${version}`], {
|
|
231
|
+
encoding: 'utf8',
|
|
232
|
+
}).stdout?.trim();
|
|
233
|
+
let mirrored = false;
|
|
234
|
+
for (let attempt = 1; attempt <= 30 && !mirrored; attempt += 1) {
|
|
235
|
+
mirrored = probeMirror() === '200';
|
|
236
|
+
if (!mirrored) {
|
|
237
|
+
if (attempt === 1) process.stdout.write(` waiting for ${mirror} to mirror ${version}…\n`);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 20_000));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
process.stdout.write(mirrored ? ` ${mirror} has it\n` : ` ${mirror} still lacks it; using the official registry\n`);
|
|
242
|
+
|
|
222
243
|
// `dsh` is a shell shim on Windows too, so it is invoked through Node rather
|
|
223
244
|
// than spawned directly. DSH_RELEASE_DSH overrides the entry point.
|
|
224
245
|
const dshEntry = process.env.DSH_RELEASE_DSH ?? 'D:/nodejs/npm_global/node_modules/@deepseek-ai/dsh/lib/bin.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Reports the preset-related events in one session log — the durable evidence of
|
|
2
|
+
// which preset a session was composed with, and whether a selection was attempted.
|
|
3
|
+
//
|
|
4
|
+
// Usage: node tools/inspect-preset-events.mjs <session-dir>
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
9
|
+
|
|
10
|
+
/** Locate the end of the first zstd frame, where the next one begins. */
|
|
11
|
+
function firstFrameEnd(buf) {
|
|
12
|
+
for (let i = 4; i + 4 <= buf.length; i += 1) {
|
|
13
|
+
if (buf[i] === 0x28 && buf[i + 1] === 0xb5 && buf[i + 2] === 0x2f && buf[i + 3] === 0xfd) return i;
|
|
14
|
+
}
|
|
15
|
+
return buf.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Decompress a concatenated-zstd log frame by frame. */
|
|
19
|
+
async function decompressAll(buf) {
|
|
20
|
+
let rest = buf;
|
|
21
|
+
let text = '';
|
|
22
|
+
while (rest.length > 0) {
|
|
23
|
+
const slice = rest.subarray(0, firstFrameEnd(rest));
|
|
24
|
+
try {
|
|
25
|
+
text += await new Promise((resolve, reject) => {
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const dec = createZstdDecompress();
|
|
28
|
+
dec.on('data', (chunk) => chunks.push(chunk));
|
|
29
|
+
dec.on('error', reject);
|
|
30
|
+
dec.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
31
|
+
dec.end(slice);
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (slice.length >= rest.length) break;
|
|
37
|
+
rest = rest.subarray(slice.length);
|
|
38
|
+
}
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dir = process.argv[2];
|
|
43
|
+
if (dir === undefined) {
|
|
44
|
+
process.stderr.write('usage: node tools/inspect-preset-events.mjs <session-dir>\n');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
49
|
+
const events = text
|
|
50
|
+
.split('\n')
|
|
51
|
+
.filter((line) => line.trim().length > 0)
|
|
52
|
+
.flatMap((line) => {
|
|
53
|
+
try {
|
|
54
|
+
return [JSON.parse(line)];
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
process.stdout.write(`${events.length} events, ${text.length} chars\n\n`);
|
|
61
|
+
|
|
62
|
+
const header = events[0];
|
|
63
|
+
process.stdout.write(`header: id=${header?.id}\n agentPreset=${header?.agentPreset}\n origin=${header?.origin} depth=${header?.delegationDepth}\n`);
|
|
64
|
+
|
|
65
|
+
process.stdout.write('\npreset-related events:\n');
|
|
66
|
+
let found = 0;
|
|
67
|
+
for (const event of events) {
|
|
68
|
+
const type = String(event.type ?? '');
|
|
69
|
+
if (!/preset/i.test(type)) continue;
|
|
70
|
+
found += 1;
|
|
71
|
+
process.stdout.write(` #${events.indexOf(event)} ${type} ${JSON.stringify(event.data ?? event).slice(0, 220)}\n`);
|
|
72
|
+
}
|
|
73
|
+
if (found === 0) process.stdout.write(' (none — no agent-preset/selected event was ever appended)\n');
|
|
74
|
+
|
|
75
|
+
process.stdout.write('\nuser messages (first 3):\n');
|
|
76
|
+
let users = 0;
|
|
77
|
+
for (const event of events) {
|
|
78
|
+
const content = event.data?.message?.content ?? event.message?.content;
|
|
79
|
+
const role = event.data?.message?.role ?? event.message?.role;
|
|
80
|
+
if (role !== 'user' || typeof content !== 'string') continue;
|
|
81
|
+
users += 1;
|
|
82
|
+
if (users > 3) break;
|
|
83
|
+
process.stdout.write(` #${events.indexOf(event)} ${JSON.stringify(content.slice(0, 160))}\n`);
|
|
84
|
+
}
|
|
85
|
+
if (users === 0) process.stdout.write(' (no user messages recorded yet)\n');
|
package/tools/verify-session.mjs
CHANGED
|
@@ -58,7 +58,7 @@ async function decompressAll(buf) {
|
|
|
58
58
|
return text;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
/** Read one session directory into {header, events}. */
|
|
61
|
+
/** Read one session directory into {header, events, text}. */
|
|
62
62
|
async function readSession(dir) {
|
|
63
63
|
const text = await decompressAll(readFileSync(join(dir, 'session.v4.jsonl.zstd')));
|
|
64
64
|
const events = text
|
|
@@ -71,9 +71,17 @@ async function readSession(dir) {
|
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
-
return { header: events[0], events };
|
|
74
|
+
return { header: events[0], events, text };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* A phrase only this preset's persona contains, so finding it in a session proves
|
|
79
|
+
* that session was composed with root-planner. Chosen against the alternative of
|
|
80
|
+
* matching "You are a delegated subagent", which DSH injects into EVERY subagent's
|
|
81
|
+
* runtime context and therefore proves nothing.
|
|
82
|
+
*/
|
|
83
|
+
const PERSONA_MARKER = 'the same rules govern every node';
|
|
84
|
+
|
|
77
85
|
const argv = process.argv.slice(2);
|
|
78
86
|
const quiet = argv.includes('--quiet');
|
|
79
87
|
const expectDepthIndex = argv.indexOf('--expect-depth');
|
|
@@ -139,11 +147,48 @@ const check = (ok, label) => {
|
|
|
139
147
|
|
|
140
148
|
if (!quiet) process.stdout.write(`root session: ${root.header?.id}\n`);
|
|
141
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Which preset a session is ACTUALLY running.
|
|
152
|
+
*
|
|
153
|
+
* The session header's `agentPreset` is written when the session is created, with
|
|
154
|
+
* the default preset, and the client then switches preset through
|
|
155
|
+
* `agentPresets.select()` — which appends an `agent-preset/selected` event and
|
|
156
|
+
* recomposes the agent, but never rewrites the header. Judging a run by the header
|
|
157
|
+
* reports "standard" for a session that is genuinely running something else, and
|
|
158
|
+
* this tool made exactly that mistake twice before the prompt text settled it.
|
|
159
|
+
*
|
|
160
|
+
* Identity is therefore taken from the strongest available evidence, in order:
|
|
161
|
+
* 1. this preset's persona prose appearing in the session's own content — only a
|
|
162
|
+
* session composed with root-planner can contain it;
|
|
163
|
+
* 2. the recorded `agent-preset/selected` event;
|
|
164
|
+
* 3. the header, which is the creation default and the weakest of the three.
|
|
165
|
+
* @param session - one read session.
|
|
166
|
+
* @returns the resolved preset id and what it was resolved from.
|
|
167
|
+
*/
|
|
168
|
+
function resolvePreset(session) {
|
|
169
|
+
if ((session.text ?? '').includes(PERSONA_MARKER)) {
|
|
170
|
+
return { preset: 'root-planner', evidence: 'persona text in the session' };
|
|
171
|
+
}
|
|
172
|
+
for (const event of session.events) {
|
|
173
|
+
if (event.type === 'agent-preset/selected' && typeof event.data?.agentPreset === 'string') {
|
|
174
|
+
return { preset: event.data.agentPreset, evidence: 'agent-preset/selected event' };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { preset: session.header?.agentPreset, evidence: 'session header (creation default)' };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rootPreset = resolvePreset(root);
|
|
142
181
|
check(
|
|
143
|
-
|
|
144
|
-
`root session
|
|
182
|
+
rootPreset.preset === 'root-planner',
|
|
183
|
+
`root session runs root-planner (found "${rootPreset.preset}" via ${rootPreset.evidence})`,
|
|
145
184
|
);
|
|
146
185
|
check(root.header?.delegationDepth === 0, `root is at depth 0 (found ${root.header?.delegationDepth})`);
|
|
186
|
+
if (!quiet && root.header?.agentPreset !== rootPreset.preset) {
|
|
187
|
+
process.stdout.write(
|
|
188
|
+
`note: the header says "${root.header?.agentPreset}" while the session runs "${rootPreset.preset}" ` +
|
|
189
|
+
`(${rootPreset.evidence}) — the header is the creation default, not the running preset\n`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
147
192
|
|
|
148
193
|
const children = sessions.filter((session) => session.header?.parentSession === root.header?.id);
|
|
149
194
|
const byId = new Map(sessions.map((session) => [session.header?.id, session]));
|
|
@@ -160,8 +205,9 @@ const depthOf = (session) => {
|
|
|
160
205
|
if (!quiet) {
|
|
161
206
|
process.stdout.write(`\n${sessions.length} session(s) total:\n`);
|
|
162
207
|
for (const session of sessions) {
|
|
208
|
+
const preset = resolvePreset(session);
|
|
163
209
|
process.stdout.write(
|
|
164
|
-
` ${String(session.header?.id).slice(0, 20)} preset=${String(
|
|
210
|
+
` ${String(session.header?.id).slice(0, 20)} preset=${String(preset.preset).padEnd(14)} ` +
|
|
165
211
|
`depth=${depthOf(session)} parent=${String(session.header?.parentSession ?? '(root)').slice(0, 20)}\n`,
|
|
166
212
|
);
|
|
167
213
|
}
|
|
@@ -173,9 +219,10 @@ check(descendants.length > 0, 'the root actually delegated (at least one child s
|
|
|
173
219
|
|
|
174
220
|
for (const session of descendants) {
|
|
175
221
|
const depth = depthOf(session);
|
|
222
|
+
const preset = resolvePreset(session);
|
|
176
223
|
check(
|
|
177
|
-
|
|
178
|
-
`child ${String(session.header?.id).slice(0, 8)} at depth ${depth}
|
|
224
|
+
preset.preset === 'root-planner',
|
|
225
|
+
`child ${String(session.header?.id).slice(0, 8)} at depth ${depth} runs root-planner (found "${preset.preset}" via ${preset.evidence})`,
|
|
179
226
|
);
|
|
180
227
|
}
|
|
181
228
|
|