session.create resolved the requested preset before queueing on the named
identity, so a later request whose preset resolved first could run first, and
an undeclared preset masked the directory conflict the README promises.
Queue first, check the named Session's directory and ownership, then resolve
the preset inside the queue, and report the preset each request left the
Session running instead of reading it after the queue moved on. The no-roster
conflict message now names its actual cause.
openWorkspace and connectWorkspace accept NewSessionOptions, so the Client
Cordis inspect catalog spec pinned outdated signatures and failed both
coverage lanes. Declare openWorkspace on one line so the generated catalog
signature carries no collapsed line breaks, regenerate the catalog, and pin
the new signatures.
session.create now reuses a named Session only while it is a reusable blank,
runs the requested or default preset, and creates a new Session otherwise.
Record the migration for JSON-RPC clients and Client plugins that name a
sessionId, including the removed session/writer-held result and the
existingPreset detail.
Master now creates a fresh Session for a plain New Session, and the Creator
entry names its preset in the session.create request instead of a separate
agentPresets.select call. Wait for that request in the Creator scenario and
count the startup blank in the outdated-cache scenario.
Typing at the end of the highlighted command token lands in the styled
node. The decoration split the overflow back out, but splitText copied
the token style onto it, so Lexical's normalization merged the halves
again and the transform looped until Lexical aborted the update. The
DOM kept the native insertion while the editor state rolled back, which
left the composer desynchronized for every later keystroke.
Clear the overflow style in the same transform pass.
Refs #4490
session.create treats a named sessionId as a blank Session to reuse while
the Host can verify it is still reusable: no turn has started, it belongs to
the requested directory, it is an unarchived member of the requested
Workspace, and no other owner writes it. The reused Session runs the
requested agentPreset or the configured default; its recorded preset is not
resolved, so a blank recorded under a preset that is no longer declared no
longer breaks New Session or startup restoration. A change is logged as
agent-preset/selected before the Agent is published. A candidate that is no
longer reusable stays unchanged and the Host creates a new Session, which
replaces the client's writer-held fallback.
The Web client restores a saved draft with its recorded preset and falls back
to the default when that preset is missing or fails to activate, passes the
creator entry's preset through creation instead of issuing a concurrent
select, and reloads the projections of a declined candidate so a stale blank
hint no longer hides a started conversation.
Related to #2566
Shell discovery keyed entries by resolved path. On merged-/usr hosts the
login shell /bin/bash from $SHELL and the PATH hit /usr/bin/bash for the
`bash` candidate are one file with two spellings, so the menu showed
"bash" twice. Key discovered shells by executable name, compared without
letter case or `.exe`, and keep the earliest entry so the default shell
stays first.
Moves the interface to the position SidebarRightBinding held, after the controller factory, so the change reads as one interface replacing the other. Its content is unchanged.
The fullscreen command needs to know whether the frame is narrow enough
that an expanded panel is fullscreen automatically. Reading it from a new
ctx.layout.viewportWidth widened the layout plugin's public face for this
one consumer. The seats already receive the frame width as owner props and
compute the rule they render with, so each seat now reports that verdict
through its inject face, as it reports its docking kit's room. The command
reads the latest report only when the user runs it, after the commit that
rendered the width has settled.
- Revert ctx.layout.viewportWidth and the test fakes that provided it.
- SidebarRightInjected gains reportAutoFullscreen.
ctx.sidebarRight acted on a binding the right Sidebar seat published from
passive effects. React runs a commit's passive effects in tree order and
the frame renders the Conversation column before the right column, so an
opener mounting in the commit that switched Sessions or returned from a
global panel ran while no seat was bound, and its open threw.
The plugin now names the on-screen Session itself: the selected Session
while the Conversation fills the main column, derived from the Sidebar's
Session views and ctx.layout.panelInfo. Both notify before React renders,
so `mounted` already names the arriving Session during that commit, and
its seat mints the Session's store in the same render. Public commands act
through the Session's adopted store; the seat reports only the room its
docking kit measured, and focus continuity and the auto-fullscreen rule
come from a host the plugin supplies.
- ctx.layout gains viewportWidth from the root layout store.
- SidebarRightBinding, controller.bind(), and the seat's bindService are
removed; the seat's inject face gains measureRoom.
- ui-plan's review opener returns to a plain open gated on `mounted`.
- The plan-review e2e expands the Workspace group by its aria state.
Optional request extension fields had no size bound. A very large field
could exceed the runtime's string limit while the request was serialized,
which failed every attempt, including retries.
- Bound the extension payload carried by one request (8 MiB by default,
configurable) and send the remainder with later requests.
- If the combined request still cannot be serialized, send the base
request without extension fields and log which fields were omitted.
- Add regression tests and update the related references and decision note.
A plan review that mounted in the same commit as a Sidebar seat change
still crashed its opener, which retired the View full plan link and every
automatic open until the page reloaded. Two paths reached the crash:
- Returning from another Session: PlanReviewOpen read the departing seat
as mounted during render, and that seat released before the review's
effect ran while the arriving seat bound after it.
- A Session without a saved Sidebar layout: the seat released and rebound
around every store commit, and the commit that created the layout was
the one in which the review opened.
The seat now republishes its binding after each commit without releasing
it in between, and releases the latest binding only when it leaves the
screen. The review opener reads the mounted snapshot when it acts,
reports whether an open reached a seat, retries whenever the mounted
Session changes, and records the review as opened only after an open
does. Embedded subagent reviews still open in the visible parent Sidebar.
Node's module-hooks thread serializes resolver errors and returns the stack
accessor as a read-only configurable data property. Rewriting the routed
importer then assigned error.stack, threw a TypeError, and replaced the
original ERR_PACKAGE_PATH_NOT_EXPORTED or ERR_MODULE_NOT_FOUND error, so
plugin metadata reported a failure instead of falling back to package fields.
The importer rewrite now redefines a rejected stack value in place and
rethrows the original error with its code.
* feat(workspace): stop a Session's running work before archiving it
Archiving was a pure visibility write, so a Session archived mid-turn kept
running out of sight. The Workspace registry now owns archive admission
through two Host events: `workspace/session-activity` reports what still
runs and refuses the plain archive as `workspace/session-active`;
`workspace/session-stop`, requested with `stopActivity`, stops that work the
way the user's own stop actions do before the archive write. The API Session
Controller reports and stops turns, subagent descendants, and owned jobs and
rejects any model step for an archived Session; the Schedule plugin reports
and deletes active reminders. The sidebar turns the refusal into a
stop-and-archive confirmation listing the Host's answer.
* test(workspace): pin the widened archive signature and prove child liveness by heartbeat
The Client inspect catalog spec pins archiveSession's new options
parameter, and the archive e2e reads the Bash child's liveness from a
heartbeat file instead of a pid: the Linux sandbox runs the tool in its own
PID namespace, so the pid it reports is not comparable from the test.
* test(workspace): cover the archive-admission providers and dialog to the per-file gate
Drop the provider's unused disposer and the dialog's unreachable confirm
guard, read reminders without a catch that no composition can reach, and
add the cases the coverage lane found missing: a stop without a jobs
registry, a looping lineage, a descendant whose projections reject, the
dialog's Escape guard and non-Error reason, and a family merged by another
provider.
* fix(workspace): write the archive before stopping, gate subagent lineage, and delete reminders under the schedule barriers
Review round on #4765. The registry now writes the archive set before it
dispatches `workspace/session-stop`, so the durable set already gates
every wake the stops induce; the controller's `agent/pre-step` gate also
blocks subagent descendants of an archived Session and contains each
cancel or kill on its own. The Schedule plugin's stop listener runs as a
management delete in the agent's transaction queue between the same two
persistence barriers the `schedule_delete` tool uses. The confirm dialog
selects singular or plural English lines and keys list rows by index.
Docs: archive-admission section with type-equiv blocks on the workspace
subsystem page, write-first order and residual windows in the READMEs
and the Agent Note, and the missing zh reminders sentence.
* test(web): scope the question composer's New Session wait to the Sessions tree
The wide sidebar's New Session button carries the same visible label as
a fresh session row, so the unscoped `getByText` either settled on the
button before the row existed or hit a strict-mode violation once the
row rendered first, which is how the snapshots lane failed on
ece68c2ab5. Waiting inside the Sessions tree names the row alone.
* fix(schedule): answer archive admission from the owned runtime's fold, not the projection registry
The `sessionProjections.snapshot` API is the Client projection view and
is not a Host read path (review on #4765). `ScheduleRuntime.activeRecords()`
folds the live log the way every drive does, and the archive-admission
listeners ask the owned runtime for that fold: a session without a live
agent or without an owned runtime has nothing armed to fire, so it reports
nothing and has nothing to stop. Covers the runtime-less agent, a fold that
turns unreadable at the persistence barrier, and the stopping/faulted
runtime answering nothing.
* refactor(subagent): report and stop subagent descendants from the Subagent runtime
The `subagent` family of archive admission moves out of the API Session
Controller into `dsh-subagent`, which records the lineage at delegation
and owns the descriptor (review on #4765): `SubagentRuntime` answers
`workspace/session-activity` with the running descendants, labelled from
each child's descriptor through a live Session observation, and
`workspace/session-stop` by cancelling each of them with the parent
cause. The controller keeps turns, owned jobs, and the lineage step gate,
and composes its provider through `ctx.plugin(SessionActivityProvider)`
with declared injections instead of a bare installer call. `dsh-workspace`
becomes an optional type-only peer of `dsh-subagent`.
* refactor(jobs): answer archive admission for owned jobs from the job registry seam
The `job` family moves out of the API Session Controller into `dsh-jobs`
(review on #4765): the seam's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every implementation through the abstract `list` and `kill` alone, so the
controller no longer knows the jobs vocabulary and keeps only the turn
family and the archived-lineage step gate. `dsh-workspace` becomes an
optional type-only peer of `dsh-jobs`.
* refactor(workspace): leave SessionActivityKindMap empty and let each provider merge its family
The workspace package no longer names any activity family (review on
#4765): `SessionActivityKindMap` is empty, and each provider merges its own
key from a module both its faces import — `turn` in the API Session
Controller's types, `job` in the job registry seam's view, `subagent` in
the Subagent runtime's control types, `schedule` in the Schedule plugin's
types. The confirm dialog imports those modules for its cases and keeps
the generic line for any other key; the registry and controller suites
merge test-owned families instead of borrowing the providers'.
* refactor(agent): report and stop the running turn from the Agent registry
The `turn` family moves out of the API Session Controller into `dsh-agent`
(review on #4765): the registry's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every Agent it publishes, answering with its own lookup and cancelling the
turn with the user cause, and merges the `turn` key from its client-safe
types. The controller keeps only the archived-lineage step gate, renamed
`ArchivedSessionGate`. `dsh-workspace` becomes an optional type-only peer
of `dsh-agent`; the workspace package depends on none of the providers,
so the edge stays acyclic.
Since #3127 a foreground Bash command is a registered job from its start,
so the archive dialog lists it beside the turn; the e2e and its ARIA golden
expect that row, and the capture keeps the running job by design.
The bundle page now titles the fixture by its package name, prints the
row id as code under the row's title, and drops the missing-description
line, as master's row and official goldens already record.
The Plugins page spec's configuration bodies take the view positionally
as master's fixture does, the companion and section specs read a slot
entry's injected face through a Pick of the members they assert instead
of a double assertion, and the no-unknown-casts baseline drops the
entries retired with the moved settings tests.
A foreground shell command is a job while it runs, and its removal reaches
the browser one coalesced roster frame after the tool result, so a
conversation golden captured right after the turn could catch the header's
running-job control; the replay round-trip golden did on CI. captureStableAria
now waits for that control to leave unless a scenario keeps it, which the
two job scenarios that capture a running row do.
The registry's owner fence is the only access rule for a human kill: a job
is killable by the session whose list carries it, and a child session's own
jobs are no exception. The fence had pulled two implementation helpers out
of the Session Controller's package entry (plus a safe-host-export exception
for them) so the job controller could import them; both go, and the job
controller no longer depends on the agent registry or the Session Controller.
The import came with the session.killJob Remote that later moved to the
job controller; nothing in this package reads ctx.jobs any more, and the
package declares no dependency on the seam.
With a job registry composed, tool-bash and tool-pwsh now register every
command with ctx.jobs as it starts and wait on the job with the registry's
own bounded wait: a command that finishes in time returns the ordinary
foreground result and its record is removed, one that outlives the timeout
returns the same job id it already had, and a kill from outside the call is
reported as `[stopped: <reason>]`. Nothing changes hands at the deadline, so
the shell seam loses `onExpiry: 'offer'`, `ShellExecution.promotion`, and
`ShellPromotionOffer`, and both local executors drop their offer arm.
The registry stays optional: the tools register a foreground-only definition
while no registry is composed and swap to the job-backed one inside a
`ctx.inject(['jobs'])` fork, so a composition without jobs keeps a plain bash
with no job tools in the model's schema. A refused registration at the start
(admission, no controller) runs under the executor's deadline kill, and a wait
that expires before the process spawned ends as the preparation timeout.
JobRegistry gains `remove(id, caller?)` and the `settled` event reports
`awaited`, so dsh-tool-jobs no longer keeps its own wait ledger; it withholds
notices for awaited settlements and for kills the model requested. The tool
catalog harvest composes the registry so the shipped schema stays documented,
and the two compaction fixtures move their context window from 10000 to 10300
tokens because the branch's larger tool schemas pushed their condensed history
over the old trigger, which requested a fourth scripted model call.