Write handles now hold a durable lease (session.lock.json beside the
log): a random owner token, diagnostic pid, and an expiry. Acquisition
wins by exclusive create; a second process's create or write open rejects
while the record is unrenewed for less than leaseTtlMs (default 5 min),
and takes over after that — a crashed holder is waited out, never
reclaimed by pid. The holder renews every leaseRenewIntervalMs (default
4 min); a renewal that finds a foreign, vanished, or expired record — or
fails outright — marks the lease lost permanently, so every later
append/flush rejects with SessionOwnershipLostError while reads continue.
Close releases the record; read handles never touch it. Takeover of an
expired record is eventually exclusive: a replaced holder stops within
one renewal interval.
Refs #3245
The probe called any package whose main export is a function a `plugin`,
so `lodash` installed as an addable plugin module. A `plugin` now needs a
`dsh` section in its manifest or a dependency on `@deepseek-ai/cordis` as
well as a plugin-shaped export; a bare function export is a `library`.
The install dialog lists the packages the Host removed again after
`pnpm add` with the Host's reason, and every action renders the two
refusals any mutation can meet — another change still running, a session
still running — in the Host's words instead of a generic failure line.
A successful `pnpm add` is no longer the end of `plugins/install`: a new
package that declares neither a bundle nor a plugin module, or a bundle
whose row id a composed layer already owns, is removed again and reported
under `removed` with its reason, while a package the probe refused stays
for the view to explain. The profile manifest is snapshotted before the run
and restored when pnpm fails. The manager refuses a second mutation while
one runs (`plugins/busy`) instead of racing on the manifest and user
layers, and `install`/`uninstall` refuse while a session is running
(`plugins/agents-running`), because pnpm rewrites the directory those
sessions import from.
`resolveProfileLayer` factors one bundle's layer out of `loadProfile` so the
manager can judge a candidate against the composed layers.
Adopt owned row ids in the browser: the plugin manager tab shows `rowId`,
the web scaffold's profile runtime composes a stack, and the inventory spec
covers provenance and conflict attribution. Widen the install-result alias
the store spec had narrowed to `never[]`.
Adopt owned row ids in the plugin manager and inventory: a row view carries
`rowId` (the declared id a patch or row action targets) instead of the
withdrawn `originalId`, probe rows are listed under their declared ids, a
conflict record is attributed to the bundle it names, and the manager's
test boot records conflicts the way the launcher does.
An external bundle keeps the ids its patch declares. composeProfileStack
decides ownership before anything mounts: built-in and boot-staged layers
claim first and a duplicate among them fails the boot, a contained bundle
that collides is left out whole, a user-layer insert of a taken id is
dropped, and every row left out is a `conflict` record in pluginFailures,
printed at boot and replaced on each recomposition. Boot, live reloads, and
--dump-config compose through the same function, so the dump shows the
contained groups the tree mounts.
The prefix made a user patch, a third-party manager's disabled rows, and a
bundle's own `!!js` id comparison miss silently; the note records both cases.