* feat(agent-launch): let a caller reserve the pane its terminal launch creates
* fix(agent-launch): refuse a launch whose reserved pane is already live
* fix(agent-launch): refuse a live reserved pane before it is revealed
The live-pane refusal used to fire in the executor, after createTerminal
had already issued a handle, published the mobile snapshot and revealed
the tab. The reveal re-registered a fresh launch config over the running
agent's. agent.launch now passes requireFreshPane with a reserved pane,
and createTerminal throws AgentLaunchPaneAlreadyLiveError as soon as
spawn reports it attached to a live pane. That is before any handle,
snapshot or reveal. The spawn reattach itself is the one terminal.create
already uses, so the live PTY is never killed, and the stable-pane
create claim is still released in finally. The isReattach plumbing
added to the launch factory for the old check is gone.
A replay-safe launch refused this way on an existing workspace now
records a failed ledger row, the same way a name collision does.
Before, the row stayed claimed, so every retry got
agent_session_operation_unknown. agent.launchReplay passes the code
through. On create-worktree the workspace already exists when the
terminal is refused, so the row stays unknown. The code is added to
the runtime passthrough list so callers can branch on it.
The pane key is now in the replay fingerprint, deliberately. It is not
placement: group, anchor and focus still stay out of the request and
out of the ledger. It is identity. It is written into the pane's PTY
environment and names the tab the caller has placed. A retry that
reserved a different pane is therefore a different request. Replaying
the first answer would return a key the new reservation can never
find. This matches terminal.createAgentSession, which also fingerprints
its tab and leaf ids. The key is only folded in when present, so every
existing digest is unchanged, and a test pins that.
The wire schema now refuses a tab id the runtime would not adopt as
sent: one with surrounding whitespace, which the runtime trims, and one
longer than 512 characters, which the spawn reservation does not key
on. It reuses the tab-id schema that Placement uses.
* test(agent-launch): pin that a refused live pane issues no handle
The refusal test named handle issuance but only asserted the reveal, so a
throw moved to just before the reveal would still pass. Assert no terminal
is registered, with the attach test as the positive control.
* feat(agent-launch): carry the launch inputs the host cannot derive
Desktop's launch call sites cannot move onto `agent.launch` while the wire
drops inputs they depend on. This adds the three the host genuinely cannot
work out for itself, and deliberately adds nothing the host can.
- `agentArgs` — the host read only `settings.agentDefaultArgs`, so a saved
launch recipe's arguments had no way across. Tri-state is preserved: `null`
is "no arguments", absent is "use the settings default".
- `cwd` — `TerminalCreateOptions.cwd` already reached the spawn, but nothing
on the wire filled it. It also decides the route: only a terminal can start
somewhere other than its workspace, so the host now feeds it to
`requiresTuiLaunchCommand` and downgrades with `tui_launch_command` rather
than running a structured session in the wrong directory.
- `launchSource` — telemetry, and the only member of the `agent_started`
triple the host cannot derive; `agent_kind` and `request_kind` are computed
host-side. Typed `z.string()`, not the closed enum: params are validated by
the HOST, so a closed arm set would let an older host refuse a newer
client's launch over a label. Attribution must not gate a user action.
Not added, because the host already derives them: `launchPlatform`
(`getAgentLaunchPlatformForWorkspace`, from the same connectionId/path/
projectRuntime the renderer uses) and `startupCommandDelivery` (a pure
function of the agent inside `buildAgentStartupPlan`).
Fingerprint: `agentArgs` and `cwd` are in — they change what the call does, so
a retry carrying different ones must conflict rather than replay.
`launchSource` is out — two buttons producing the same launch are one
operation, and folding it in would refuse an honest re-attributed retry. A
caller sending none of the new fields digests exactly as before, because the
canonicalizer drops undefined keys, so launches admitted by an older build
still replay across the upgrade.
Arguments reaching a structured route are ignored by an existing deliberate
decision (the Agent SDK and app-server version their option sets separately
from the interactive CLI), so the host reports it in `warning` instead of
overriding the user's preference on the strength of a field that is not
evidence about the surface.
* fix(agent-launch): forward create-target launch inputs