Replies: 3 comments
|
Follow-up after living with this for a day. The lost tools are the visible problem; the silence is the expensive one. Every call to a removed tool returns That cost three restarts today. The natural reading of Two suggestions, both small:
Counts from the two events, for whatever they are worth: 65 tools to 41, then 66 to 41 to 40. The surviving set was the 35 |
|
A fix for the reporting half of this, on a branch since the repository takes no pull requests:
It does not stop the rebuild. It makes the damage visible, because silence is what made this cost three restarts: from inside the session,
Each live agent's visible tools are snapshotted before the patch generation is applied and again after it settles: const toolsBefore = visibleToolsByAgent(ctx)
await entry.update({ config: { ...includeConfig, patches: prepared } })
// ... settle ...
return [
...failures.map(inactiveDiagnostic),
...strandedToolDiagnostics(toolsBefore, visibleToolsByAgent(ctx), binName),
]A stranded agent produces: An agent that disappears across the reload is not reported, since it ended rather than lost anything, and a composition mounting no agent or tool registry skips the comparison. Three tests: a loss, no loss, and no registry. The loss case fails without the change and passes with it. What it does not do is the harder half. A patch that changes one value in a profile layer still rebuilds the application tree, and the live agents are still left holding scopes from the previous one. Reporting turns an unattributable failure into an attributable one; it does not make the reload safe. That part needs either a narrower rebuild than the root entry or a rebuild of the agents alongside it, and I did not attempt it. |
|
This thread's title is the question, so here is the boundary, measured rather than inferred - and it also explains a result that looked like it contradicted this report. What a profile edit reaches
const manifestPath = join(profile.dir, "package.json")
const patchFiles = [profile.patchPath, join(profile.home, PROFILE_PATCH_FILENAME)]
const refresh = async (manifestOnly) => {
const bundles = JSON.stringify(readProfileManifest("dsh", profile.dir).dsh?.profile?.bundles ?? [])
if (manifestOnly && bundles === lastBundles) return
const inputs = JSON.stringify([bundles, ...patchFiles.map((filename) => readFileSync(filename, "utf8"))])
if (inputs === lastInputs) return
const patches = readProfilePatches("dsh", profile)
const warnings = await reconcileProfilePatches(this.ownerContext.root, patches, "dsh")
...
}
for (const filename of patchFiles) await this.watchConfig(filename, () => refresh(false))
await this.watchConfig(manifestPath, () => refresh(true))A patch-file write therefore always recomposes; a Nothing in that path re-mounts an agent. An agent's scope, the preset it was mounted from and every registration that preset made into it are resolved at mount, which for a resumed session means the next launch. So the answer depends on which layer the edited row belongs to, and the log agrees:
Times are UTC. After the first instant in each row, the others are when each session next took a turn rather than when the change landed. The two comments above are the reporting half and the detection half of this. What the boundary adds Two corrections to this reportThe third install was not manifest-only. It names "editing only The loss is not a property of the patch layer. The same file, written twice in two hours, took 25 tools away and then added one cleanly. What separates the two writes is the shape of the patch entry, and this is where measurement stops: the destructive write introduced a new profile-layer override of an existing host-plane row ( One more correction, because it is the first thing we tried to check against the source: the survivors are not "the tools contributed by profile rows mounted outside the agent preset". What we can hand overThe cheap experiment for the shape question is one profile write of each shape against one live session, reading the next Independent of the above, and worth a look on your side: a bundle installed into a profile but never added to |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Editing a profile patch layer while DSH is running strips the agent's tools from live sessions
Severity: high. The session stays alive but can no longer read, write, or run anything.
What happens
Editing the profile's patch layer,
cordis.patch.yml, whiledshis running removes most of the session's tools. The session keeps running, and every call to a removed tool returnsError: unknown tool "<name>". Nothing in the transcript says why.Restarting restores everything, which is what makes it expensive to diagnose: it looks like the profile is broken, so the natural move is to revert the config change rather than restart.
A later install that edited only
package.jsondid not reproduce it, so the trigger looks specific tothe patch list rather than to the profile directory in general. The evidence for that is under
the trigger below.
Exactly what is lost
From the session log, comparing the
request/headerbefore and after. 65 tools become 41.Lost (25):
Survived (41): 35
mcp__sage__*tools, plusapply_patch,read_mcp_resource,list_mcp_resources,list_mcp_resource_templates,load_workspace_dependencies,subagent.The split is not random. The survivors are the tools contributed by profile rows mounted outside the agent preset. Everything the preset contributes goes, including the tools the agent cannot work without.
The practical effect: the session can still talk to its MCP servers, but it cannot
reada file,writeone, or runbash. It is not a degraded session, it is a useless one.subagentsurviving whilesubagent_fork,list_agents,send_messageandinterrupt_agentall vanish is unexplained. All five come fromtool-subagentrows in the same preset (packages/bundle/base/cordis.patch.yml:364-376), so a plain scope teardown does not account for it.Reproduction
dsh, confirmbashandreadwork.~/.dsh/profiles/<name>/cordis.patch.yml. Changing a value in anexisting row is enough; the first and second events did that and appended a row respectively.
bashin the running session.Expected: the edit has no effect until restart, or the reload completes and tools remain.
Actual: 25 tools gone for the rest of the session.
Editing only
package.jsondid not reproduce it in a later attempt, so treat the patch file as thetrigger and the manifest as unconfirmed.
A control I ran
Both times this happened, I had also just used a sandbox escalation, so escalation was a candidate cause. It is not: I ran four escalated commands later in a healthy session (adding a git remote, committing, pushing, removing scratch files) and the tool count stayed at 66 throughout. The profile edit is the trigger.
Evidence
Reproduced twice on the same profile, with the tool counts above as the record.
The first time, I had just installed three plugins and rewritten
package.jsonplus appended a row tocordis.patch.yml. I assumed the install was at fault, had the user revert it, and restarted. The revert was unnecessary: later, with the plugins still installed and no revert, a restart came up with all 66 tools.The second time, the only change was one value in
cordis.patch.yml. Same result.--dump-configcomposes correctly in both states, and booting a throwaway copy of the same profile activates every entry, so composition is fine and the damage happens only in the live reconcile.The trigger is narrower than "any profile edit"
A third install refined this. Editing only
package.jsonto add a bundle left the session intact:the tool count went 66 to 67, the new tool mounted, and nothing was lost. Both earlier events edited
cordis.patch.yml.package.jsonandcordis.patch.ymlcordis.patch.ymlonlypackage.jsononlySo the earlier framing, "editing any config file in the profile directory", is too broad. The
correlation is with the patch layer. Two observations against one is not proof, and I have not run
the edit that would confirm it because doing so costs a session, but it points at the patch list
being what changes rather than the manifest.
Mechanism
Traced.
packages/boot/hmr/src/index.tswatches the profile's patch files and, on a change, callsreconcileProfilePatches:reconcileProfilePatchesresolves the root Include entry, the one that mounts the wholeapplication (
bootstrapIncludes.set(ctx, entry)whereentry = loader.resolve(includeId)), andre-applies it:
Updating the root entry's config rebuilds the plugin tree beneath it. Plugins mounted at the root
re-run
apply(), which is why tools contributed by top-level rows came back. The live agent's ownscope is not rebuilt, because the agent already exists and nothing recreates it, so every
registration its preset made into that scope is gone.
That accounts for the observed split: root-contributed tools survived, preset-contributed tools did
not. It does not explain the one anomaly above,
subagentsurviving whilesubagent_fork,list_agents,send_messageandinterrupt_agentvanished, which suggeststool-subagentis alsomounted at root scope somewhere that I did not chase down.
It also fits the third event: a manifest-only change that leaves the patch list identical produces
an
entry.updatewhose config is unchanged, so the tree is not rebuilt and nothing is stranded.That is a guess at the mechanism behind the correlation, not a measurement.
It accounts for the silence, which is the expensive part.
reconcileProfilePatchesdoes raise onnewly introduced activation failures:
but nothing failed to activate. Every plugin is healthy. The tree they were mounted into for that
agent is simply not the tree the agent is still holding.
Workaround
Restart after editing the profile. Do not edit profile files from inside a running session.
Environment
dsh-v0.2.0-rc.2, commit639ed0153@deepseek-ai/dsh-baseand@deepseek-ai/dsh-web-appplus three local pluginsrequest/headerrecords insession.v4.jsonl.zstdAll reactions