docs/design/standalone-daemon-sessions.md
This document is the versioned architecture companion to Issue #8908, which is the source of truth for the standalone-session design and delivery plan. PR #8890 is implementation PR0, not a documentation-only gate: it keeps this document synchronized while delivering the Conversations runtime foundation. The remaining ownership, standalone core, capability, SDK, WebUI, and WebShell work is delivered in PR1 through PR6 below.
The design builds on the projectless conversation infrastructure introduced for Live Voice. It does not authorize a second projectless runtime, a second session catalog, or a child process per standalone session.
This contract extends, and does not replace, the projectless runtime decisions in WebShell Live Voice Codex-Parity Refactor Contract.
The daemon currently treats its primary workspace as the implicit target when a
client creates a session without cwd. This makes the top-level New Chat
action project-bound even when the user has not selected a project. It also
exposes the lifetime of that project directory as the lifetime of the chat. If
the directory is moved or removed, the client can only report that the current
working directory no longer exists.
Live Voice already owns a secure projectless storage root at
~/Documents/Qwen Code/Conversations, publishes one daemon-owned runtime for
that root, and relocates each Live session into a deterministic private child
directory. Standalone sessions generalize that substrate into a normal text-chat
product surface while preserving Live-specific behavior.
WebShell models the user-visible context as a discriminated value:
type SessionContext =
| { kind: 'standalone' }
| { kind: 'workspace'; cwd: string }
| { kind: 'live' };
Clients derive this value from the operation they perform and the persisted
session source returned by the daemon. They must not infer product semantics
from workspaceCwd. The legacy field may be accepted only at a workspace
compatibility boundary and must be normalized immediately into an explicit
workspace context. For protocol compatibility, a standalone session still has
an internal workspaceCwd, but that value is a routing detail identifying the
daemon-owned Conversations runtime and must not be displayed as a project or
used to select standalone context.
The entry-point behavior is fixed:
| Entry point | New-session context |
|---|---|
| Top-level home and global New Chat | standalone |
| New Chat within a selected or locked project | workspace |
| Goals and Git entry points | workspace |
| Current-session New Chat | Inherit explicit context |
| Live Voice | live |
Standalone sessions appear in a top-level Recents group separate from Live and project groups. Their chat surface hides workspace selection, Git status, branch and worktree controls, project files, project settings, pin/group controls, and attachments/uploads. Normal model, approval, tool, permission, transcript, and supported session metadata controls remain available.
New top-level standalone transcripts persist sourceType: "standalone" with no
sourceId and no parentSessionId. Live sessions retain their current
sourceType: "default" and sourceId: "realtime_voice:<call-id>" provenance.
standalone is a daemon-reserved source. Generic POST /session creation must
reject it, just as it rejects the reserved Live source. Classification requires
both compatible source metadata and ownership by the validated Conversations
runtime; source metadata alone can never turn a project session into a
standalone session.
Existing top-level Conversations transcripts with no parent, no source ID, and
either no source type or sourceType: "default" are normalized as legacy
standalone sessions at read time. Their transcripts are not rewritten. A source
that is explicitly Live or belongs to another feature is never silently
reclassified.
create_sub_session invoked by a standalone session explicitly persists
sourceType: "standalone" together with parentSessionId. Children remain
loadable by identity but are excluded from top-level Recents. Parent and child
archive or deletion operations do not cascade; each transcript and private
directory has an independent lifecycle.
PR2 extends the relocated source-classification helper so Live task list, read,
wait, and follow-up operations treat explicit and legacy standalone sessions as
loadable projectless task targets. It accepts top-level explicit standalone
sources with no sourceId and standalone children resolved through their parent
chain. This does not relabel them as Live in WebShell and does not expose
Live-only tools in their ordinary text turns. Projectless Live task creation
must use the same standalone creation service instead of creating new legacy
sourceType: "default" sessions.
flowchart TD
C["Daemon client"] --> D["Qwen daemon"]
D --> P["Primary and project runtimes"]
D --> R["Daemon-owned Conversations runtime"]
R --> A["One shared ACP bridge and child"]
A --> S1["Standalone session A"]
A --> S2["Standalone session B"]
A --> L["Live session"]
S1 --> W1["conversation-hash-A"]
S2 --> W2["conversation-hash-B"]
L --> WL["conversation-hash-Live"]
Introduce one one-flight ConversationRuntimeManager per daemon. It lazily
validates the Conversations root and ensures the registered runtime and ACP
bridge even when Live Voice is disabled. ensure() does not preheat the bridge
or start the Qwen ACP child; the first operation that actually needs an ACP
session starts the one shared child. Live enablement only binds and advertises
Live-specific Host, Appshot, Realtime, speech, and task channels; it does not own
the manager or the underlying runtime lifetime. Concurrent ensure failures reset
the one-flight so a later request can retry initialization.
The existing internal runtime provenance value live-conversation is retained
for compatibility in the first implementation. Within daemon routing it means
"daemon-owned Conversations runtime" and must not be used to classify a session
as Live. Persisted session source performs that classification. Renaming the
runtime provenance is unnecessary for this feature and would expand the change
without changing behavior.
Each workspace runtime owns one ACP bridge and a lazily started child process. Standalone and Live sessions therefore share the Conversations runtime's ACP child after first use. Session admission remains subject to the daemon's total and per-runtime limits. One healthy ACP child is a steady-state ownership invariant; a bounded overlap during crash replacement or teardown is not treated as a second runtime.
The Conversations root is user-global, while multiple qwen serve processes
can run concurrently. In-process one-flight and per-session locks are therefore
insufficient.
proper-lockfile.503 conversation_runtime_in_use. Malformed
or unsafe ownership state returns
503 conversation_runtime_ownership_compromised.Acquisition also respects an already-running legacy Live discovery owner. A pre-feature daemon started after a new standalone owner cannot be made to honor the new record, so concurrent mixed-version access is explicitly unsupported.
The existing conversation workspace creates a deterministic direct child for each session:
~/Documents/Qwen Code/Conversations/conversation-<sha256(session-id)>
The root and child must be real directories owned by the daemon user. On POSIX, they must not grant group or other permissions. The daemon validates the root's canonical path, device, and inode before and after sensitive operations, and it requires each session directory to be an exact direct child. Symbolic links, junction/reparse escapes, path traversal, non-direct descendants, and identity changes are rejected.
Device and inode identity are pinned for both the root and every materialized
session child for one daemon ownership lifetime. The owner keeps each child's
validated identity by session ID and compares it before every later use; an
owned 0700 directory substituted at the same path is still compromised.
Identity may be established only at first materialization, after a daemon
restart with no pending deletion journal, or when load, resume, or explicit
repair recreates a path proven absent while holding the lifecycle coordinator.
Archive does not reset it, and the normal-to-staged deletion rename preserves
it. After a restart, a securely recreated root and child at the expected
canonical paths may be accepted only after recovery journals have been
reconciled; the feature does not promise persistent inode attestation across
clean restarts. Windows validates canonical path and link/reparse behavior
exposed by the platform without claiming POSIX owner/mode or ACL guarantees.
Daemon-managed transcripts and sidecars remain in the daemon runtime base's per-runtime storage keyed by the canonical Conversations runtime cwd (under the default user-global base unless the daemon explicitly selects another runtime base). User-authored Conversations-root configuration remains under that root. Neither is moved into the session's private child, which is only the effective tool and shell working directory. Managed relocation updates the effective target directory and workspace context without changing transcript ownership.
User/global settings and user-authored Conversations-root configuration
continue to apply. A child may inherit ancestor QWEN.md/AGENTS.md and shared
Conversations-root MCP/config state. Primary-project settings, memory, Git
state, trust, and cwd must not leak. The design must not describe shared
user-level or Conversations-root configuration as per-session private.
The private directory is a stable default working directory, not an OS sandbox. Relative file and shell operations begin there and normal workspace-aware tools receive that directory as session context. An explicit operation targeting an absolute path outside it remains governed by the existing permission and approval pipeline. This feature does not claim containment that the current tooling cannot enforce.
The Conversations root is not a user workspace. Use a default-deny user-workspace resolver and a separate explicit internal resolver. Generic registration, settings, trust, Git, files, shell, extensions, skills, MCP control, memory control, workspace voice, and workspace-qualified ACP WebSocket routes must reject a request that resolves to the internal runtime. Generic channel and scheduled-task administration is also denied. Compatibility exceptions preserve the existing Live behavior on the workspace-qualified surfaces: channel management remains read-only, and Live-owned scheduled tasks retain list, update, delete, and manual-run access. These exceptions authorize only Live state and do not expose standalone sessions or standalone durable scheduling.
Audit every direct registry consumer, including HTTP routes, ACP and voice
WebSocket upgrades, capabilities, session creation and restore, workspace
management, health, and Live task services. Only owner-routed session
operations, transcript/catalog operations, health/capabilities, and dedicated
Live or standalone services may opt in. The compatibility kind: "live"
runtime entry may remain temporarily, but new clients exclude it from project
selectors and generic route denial remains mandatory.
An unknown, bootstrapping, untrusted, compromised, draining, or removed Conversations runtime returns an error. It must never resolve to or retry against the primary runtime.
The daemon advertises standalone_sessions_v1 in GET /capabilities only when
the complete manager, service, route, and managed-directory lifecycle dependency
set is installed, including embedded createServeApp configurations. A build
constant alone is insufficient. PR0 through PR2 remain behaviorally hidden; PR3
is the atomic advertisement boundary.
The capability is not coupled to Live Voice availability or enablement and describes support rather than current cross-daemon ownership availability. Root materialization remains lazy, so a missing but creatable root does not suppress advertisement. Once advertised, initialization or ownership errors are returned as structured failures and never trigger primary fallback.
The dedicated API is:
POST /standalone/sessions
GET /standalone/sessions
GET /standalone/sessions/:id
POST /standalone/sessions/:id/load
POST /standalone/sessions/:id/resume
POST /standalone/sessions/:id/repair-directory
PATCH /standalone/sessions/:id/metadata
GET /standalone/sessions/:id/export
POST /standalone/sessions/archive
POST /standalone/sessions/unarchive
POST /standalone/sessions/delete
Dedicated routes prevent omission of cwd from silently selecting the primary
runtime. They also let SDK clients distinguish an unsupported old daemon from a
failed standalone operation.
Creation accepts only:
interface CreateStandaloneSessionRequest {
sessionId: string;
modelServiceId?: string;
approvalMode?: DaemonApprovalMode;
}
The wire-level UUID is required and validates as UUID v1 through v5. An SDK
convenience method may omit it only if the SDK generates the UUID before sending
the request. The daemon fixes sessionScope to thread and source to
standalone. Unknown keys are rejected, including cwd, workspaceCwd,
workspaceId, sourceType, sourceId, sessionScope, branch, and
worktree.
GET /standalone/sessions/:id is the non-mutating exact-identity lookup used for
response-loss recovery and deep links:
202 with state: "creating" while the UUID reservation is in flight.200 with an active or archived summary when a compatible transcript
exists.404 standalone_session_not_found when the UUID is absent or belongs
to another context. A retained deletion journal does not make the deleted
session discoverable; cleanup resumes through owner acquisition or an exact
delete retry. Lookup never reveals or guesses another runtime.Load and resume use Omit<RestoreSessionRequest, 'workspaceCwd'>: they retain
the existing approval, history-page, and client timeout options while the route
selects the owner runtime and private directory. Repair has no request body.
Rename and export use dedicated routes so cold and archived transcripts work
without exposing the internal runtime through workspace-qualified APIs. Active
rename additionally notifies the live bridge.
Listing reuses the existing cursor, size, and archive-state semantics. It
includes explicit and compatible legacy top-level sessions, excludes Live and
project sessions and every child, and does not probe working-directory state.
Archive, unarchive, and delete accept the existing bounded, de-duplicated
sessionIds array. Batch errors use { sessionId, code, message }. Successful
delete returns removed, notFound, errors, and fileCleanupPending;
fileCleanupPending is a subset of removed because the transcript is already
gone.
Prompt, cancel, subscribe, permission, transcript, status, and other live session-ID routes retain owner routing after load. Persisted or cold operations that cannot be satisfied from the live owner index use the standalone service, not the primary runtime.
The SDK exposes narrow create, restore, and summary results using common fields:
interface DaemonStandaloneFields {
sourceType: 'standalone';
context: { kind: 'standalone' };
workingDirectory: {
state: 'ready' | 'recreated';
warnings?: string[];
};
}
interface DaemonStandaloneSession
extends DaemonSession,
DaemonStandaloneFields {}
interface DaemonRestoredStandaloneSession
extends DaemonRestoredSession,
DaemonStandaloneFields {}
interface DaemonStandaloneSessionSummary extends DaemonSessionSummary {
sourceType: 'standalone';
context: { kind: 'standalone' };
}
Create returns DaemonStandaloneSession; load and resume return
DaemonRestoredStandaloneSession. A recreated directory warning means the
transcript survived but files previously stored in the directory are not
recoverable. Standalone list summaries expose the explicit context and source
but do not probe or return working-directory state.
The existing internal workspaceCwd field remains required on base daemon
session types for routing and backward compatibility. Standalone SDK methods do
not accept it as input, and WebShell does not expose it as a project.
The SDK provides capability-gated create, list, exact get, load, resume, repair,
rename, export, archive, unarchive, and delete methods. It generates the UUID
before create, exposes that UUID on either a structured
standalone_creation_outcome_unknown response or an outcome-unknown transport
error, performs exact lookup, and never retries creation automatically.
DaemonSessionClient stores an explicit restore strategy: workspace sessions
restore by cwd, while standalone sessions use the dedicated route. Daemon
responses are runtime-validated in both browser and Node builds.
The SDK generates a UUID before sending the request. Creation proceeds as one logical transaction:
409 standalone_session_conflict; a
compromised record returns 409 deletion_recovery_compromised. Neither case
materializes a child. While still holding the coordinator, reserve the UUID
daemon-wide across every active runtime bridge, every active and archived
transcript catalog, the Live owner index, and in-flight creation. Admission
is global, but the new session is created only through the validated
Conversations runtime. Any existing owner is a conflict.sourcePersisted: true.Before source persistence, failure closes the ACP session, releases the UUID,
and removes only an empty child after closure succeeds. If ACP-session closure
fails, the UUID remains reserved as creating, the Conversations runtime is
quarantined, and its shared ACP child is torn down to eliminate the unpersisted
orphan before the UUID can be released. Exact lookup returns
202 state: "creating" until teardown confirms that no orphan remains, then
returns 404; a connected create request receives
500 standalone_creation_outcome_unknown with the UUID and must poll exact
lookup rather than retry create. If pre-persistence cleanup completes, the
connected request returns 500 standalone_creation_rolled_back with the UUID
and is safe to retry with that UUID. After source persistence, transcript
existence is the durable outcome marker. Under the lifecycle lock, the daemon
first closes the ACP session, removes only an empty child, and then attempts
orphan transcript cleanup. Cleanup is complete only after ACP session teardown
succeeds, the empty child is removed, the orphan transcript is removed, and the
UUID reservation is released. Complete cleanup returns
500 standalone_creation_rolled_back with the UUID and is safe to retry with
that UUID. If ACP-session closure or transcript cleanup fails, or the process
crashes, the daemon preserves the transcript and UUID and reports
500 standalone_creation_outcome_unknown with the UUID so the client can query
exact identity. A relocated child that is non-empty or cannot be removed is not
deleted, and transcript cleanup is not attempted. The daemon preserves the
transcript, child, and UUID and returns the same outcome-unknown result; exact
lookup exposes the partial but loadable session.
Once source persistence has succeeded, transcript deletion is not attempted
unless ACP session teardown and empty-child removal have both succeeded; a
partial unwind therefore remains discoverable by exact lookup. The design does
not claim rollback atomicity beyond the transcript store's actual behavior.
Client disconnect does not abort the logical transaction. If relocation commits but the response cannot be written, detach the phantom response client without deleting the session or transcript. The client uses exact lookup by UUID and may then load; it never retries create automatically.
Load and resume first validate source ownership, root, and deterministic child.
Before shared load admission or any missing-child recreation, they check for a
pending deletion journal. If one exists, the daemon runs bounded reconciliation
under the exclusive lifecycle coordinator; it never recreates the normal child
while the journal remains. A non-terminal or compromised recovery returns its
structured deletion error instead of loading the session.
If the child is absent, the daemon recreates it at the same path, relocates the
session, and returns workingDirectory.state: "recreated" with a warning that
deleted files were not recovered. This recreation holds the lifecycle
coordinator and establishes the new validated child identity before returning.
A suspicious existing path fails closed and is never chmodded, replaced, or
deleted.
Before every standalone prompt is admitted, revalidate the root, exact child,
and current session cwd while holding the shared lifecycle admission boundary.
If the child disappeared, return 409 working_directory_missing without
dispatching the prompt. The UI offers explicit repair and never replays a prompt
whose commit status is uncertain.
Repair acquires the exclusive lifecycle coordinator, closes new prompt admission, waits for the active prompt to settle or cancel, restores a valid staged child when required, recreates only an absent child, reapplies relocation, and returns the resulting working-directory state.
ACP currently starts the cron scheduler before managed relocation. Project-level durable cron state would initially bind to the shared Conversations root, so standalone MVP must not load, create, or fire durable scheduled tasks there.
cron_create({ durable: true }) with a clear unsupported error.Per-standalone durable scheduling requires a separate design for relocation, archive, deletion, restart ownership, and UI management.
Use one per-session lifecycle coordinator rather than separate repair, archive, or deletion locks. Shared prompt/read admission and exclusive repair, archive, unarchive, delete, and rename mutations all use this coordinator. Closing active ownership means closing new prompt admission, waiting for the active prompt to settle or cancel, closing the session in the shared Conversations ACP child, and removing it from the live owner index. Transcript mutation also acquires the existing writer lease. Cross-daemon Conversations ownership is the outer boundary; ambiguous ownership never permits fallback.
Archive closes active ownership, moves the transcript into the archived catalog, and retains the private child. Unarchive reactivates the transcript; the next load validates or recreates the child. Parent and child state does not cascade.
Rename appends title metadata to the correct active or archived transcript and never renames the deterministic child. Export reads the correct active or archived transcript under a shared lifecycle lock and does not materialize the directory.
WebShell retains its second confirmation and explains that deletion removes the transcript and private files. The daemon then acquires the exclusive lifecycle coordinator and writer lease, closes prompt admission, and tears down active ownership before changing either the directory or transcript.
Deletion uses a small durable recovery journal beside the stable Conversations
owner record in an owner-only user-global namespace independent of
QWEN_RUNTIME_DIR and project runtime bases. Each atomically written record has
a bounded schema containing the session ID, expected directory hash,
transaction phase, validated Conversations-root canonical/device/inode
identity, the exact normal and staged canonical paths, and the validated
child's device/inode identity captured before rename when a child exists. The
atomic rename preserves that identity, so either path can be matched after a
crash between rename and the staged-phase journal write. Recovery must match
the recorded root and applicable child identity before destructive file
cleanup; an identity mismatch or an unprovable identity fails closed and leaves
files untouched.
If both normal and staged children are absent, record that state, delete the transcript, and clear the journal. Missing files do not block transcript deletion. If either path exists but fails validation, stop before transcript mutation.
.deleting
sibling and atomically advance the journal to the staged phase. Transcript
deletion cannot start until that phase is durable. If the phase update
fails, restore the child before clearing the journal; interruption leaves a
prepared record whose pre-rename child identity safely drives recovery.500 transcript_deletion_failed with the session intact. A fully absent set
commits transcript deletion and continues to step 7. Partial or unknown
state retains the journal and staged child and returns
transcript_deletion_outcome_unknown; recovery must reconcile it before any
rollback or recursive cleanup. If restoring a fully intact set fails, leave
both journal and staged child for repair and return
working_directory_recovery_failed. If both children were already absent,
retain the journal on intact, partial, or unknown deletion failure so an
exact retry or bounded reconciliation can finish the authorized deletion.Final removal failure does not resurrect the transcript. Return the session ID
in fileCleanupPending and retain the journal so an exact retry or bounded
reconciliation can resume cleanup.
Reconciliation has explicit reachable entry points. The first successful
Conversations ownership acquisition in a daemon lifetime runs a bounded pass
over deletion-journal records after secure-root validation and before standalone
route admission; this does not initialize Conversations while Live and
standalone are unused. Each record is reconciled under its exclusive lifecycle
coordinator and the transcript writer lease. A delete retry containing that exact
session ID checks for a matching journal before mapping an absent transcript to
notFound; if no session in another context owns the UUID, a valid record resumes
the authorized deletion and returns the session ID in removed after terminal
cleanup. Creation checks and reconciles the same UUID before reservation, and
load, resume, or repair of an existing transcript checks before normal child
validation or recreation. A startup pass that reaches its fixed safety bound
leaves remaining records untouched and reachable through a singleton delete
retry; it never guesses from staged-looking directories. A non-terminal or
compromised record is isolated to its UUID: the pass records the structured
error, leaves that record untouched, and continues without blocking unrelated
standalone sessions.
Recovery considers active and archived transcripts and every Conversations source before destructive cleanup:
transcript_deletion_failed for a
later exact retry or bounded reconciliation.transcript_deletion_outcome_unknown, and leave every
directory untouched until bounded reconciliation proves a terminal state.deletion_recovery_compromised and leave every file untouched.deletion_recovery_compromised and leave every file untouched.A staged-looking directory without a valid recovery record is never proof that deletion was authorized. Creation cannot establish a new incarnation of a UUID while any journal for that UUID remains, so recovery never treats a fresh normal child as belonging beside an older staged child.
| Condition | Result |
|---|---|
| Invalid/forbidden field or malformed UUID | 400 invalid_request |
| Session is absent or belongs to another context | 404 standalone_session_not_found |
| DELETE sees absent transcript plus journal, no other owner | Resume exact deletion recovery before notFound |
| UUID/source/orphan-directory/session-state conflict | 409 standalone_session_conflict |
| Creation finds a valid journal still pending cleanup | 409 standalone_session_conflict, retryable |
| UUID creation is currently in flight | Exact lookup returns 202 state: "creating" |
| Private child disappeared before prompt | 409 working_directory_missing |
| Existing managed path fails validation | 409 working_directory_compromised |
| Deletion journal or staged state is inconsistent | 409 deletion_recovery_compromised |
| Create crossed persistence and cleanup completed | 500 standalone_creation_rolled_back with UUID |
| Create failed before persistence and cleanup completed | 500 standalone_creation_rolled_back with UUID |
| Transcript deletion failed and directory state recovered | 500 transcript_deletion_failed |
| Transcript or sidecar deletion outcome is partial/unknown | 500 transcript_deletion_outcome_unknown |
| Transcript rollback cannot restore staged child | 500 working_directory_recovery_failed |
| Create cleanup outcome is unknown | 500 standalone_creation_outcome_unknown with UUID |
| Conversations root identity or trust fails | 503 conversation_root_compromised |
| Runtime owner record is unsafe | 503 conversation_runtime_ownership_compromised |
| Another daemon owns the runtime | 503 conversation_runtime_in_use |
| Conversations runtime cannot be initialized | 503 conversation_runtime_unavailable |
| Transcript was deleted but final file cleanup failed | 200 with fileCleanupPending |
Structured errors include the session ID when known, identify retryability, and never expose untrusted filesystem paths. Logs and telemetry record route, runtime provenance, phase, code, ownership outcome, and cleanup state.
An older daemon omits standalone_sessions_v1. A newer WebShell connected to
such a daemon preserves the legacy behavior in which global New Chat targets
the primary workspace. It may explain that standalone chat requires a daemon
upgrade, but must not call the new routes.
If the capability is present and standalone creation fails, the client displays the failure and preserves the user's standalone intent for retry. It must not silently create a primary-workspace session. This distinction prevents a broken or compromised Conversations runtime from changing the target of user actions.
An old client against a new daemon retains generic POST /session behavior and
therefore still targets primary unless it explicitly uses the new routes.
There is no transcript migration. New sessions persist explicit standalone source metadata; compatible legacy projectless transcripts are normalized when read. Removing the feature code leaves existing transcripts in the configured daemon runtime base's per-runtime storage and managed directories under the Conversations root, and does not affect project sessions, but a pre-feature daemon is not required to expose explicit standalone transcripts as projectless sessions.
The capability is published only in PR3 after the hidden runtime foundation, ownership/isolation boundary, and standalone core have landed. SDK and UI changes may then gate on it. Concurrent mixed-version use of the Conversations root remains unsupported.
The design is reviewed and tracked in Issue #8908. Delivery uses seven substantive implementation PRs; this companion document is updated with PR0 but does not occupy a documentation-only stage.
Implementation PR: #8890
Suggested title: refactor(cli): Generalize the Conversations runtime foundation
ConversationRuntimeManager and split optional Live
bindings from runtime lifetime.Verification covers manager concurrency and failure reset, secure root/child validation, absence of ACP/Host/provider preheat, Live enabled/disabled lifecycle, concurrent Live work sharing the runtime, and complete Live regression behavior.
Estimated size: 180-320 production lines and approximately 750-850 test lines. Keep the production refactor below the repository's 500-line core-refactor gate.
Exit criterion: Live uses the generalized manager, and the runtime/bridge can be lazily ensured without enabling Live or starting the ACP child.
Suggested title: fix(cli): Harden the Conversations runtime boundary
standalone_sessions_v1.Verification covers two-process contention, stale reclaim, PID reuse, malformed/symlink/wrong-mode owner records, shutdown races, every generic HTTP and WebSocket route family, no-primary-fallback, and Live regressions.
Estimated size: 300-550 production lines and 600-1,000 test lines.
Exit criterion: at most one supporting daemon owns Conversations, and no ordinary workspace surface can address the internal runtime.
Suggested title: feat(cli): Add standalone session creation and restore
StandaloneSessionService for required-UUID creation, exact
lookup, listing, load, resume, directory repair, prompt preflight, and
working-directory warnings.Verification covers the source/owner matrix, UUID conflicts, every creation
failure boundary, response disconnect before/after persistence, exact lookup
202/200/404, missing/compromised children, concurrent prompt/repair admission,
children, Live task compatibility, and durable-cron denial.
Estimated size: 450-750 production lines and 850-1,400 test lines.
Exit criterion: the core service creates and restores standalone sessions without primary fallback, but clients are not yet told that the full v1 contract is available.
Suggested title: feat(cli): Add standalone daemon session APIs
fileCleanupPending.standalone_sessions_v1 only when every dependency is present..qwen/e2e-tests/.Verification covers the complete REST lifecycle, cold and archived operations, batch schemas, fault injection at every deletion boundary, concurrent prompts and maintenance, restart reconciliation, load while a deletion journal is pending, crashes between child rename and phase persistence, crashes between rollback restore and journal clear, embedded-app capability absence, multi-daemon ownership, and macOS/Linux/Windows path behavior.
Estimated size: 500-850 production lines and 950-1,600 test lines.
Exit criterion: the complete feature works through REST without SDK/WebShell, survives daemon restart, and safely advertises v1.
Suggested title: feat(sdk): Add standalone session APIs
{ kind: 'standalone' } context.workspaceCwd.Verification covers request shapes, capability handling, UUID conflict and
202/200/404 recovery, transport timeout, malformed responses,
standalone/workspace reattach, and Node/browser builds.
Estimated size: 300-500 production lines and 450-800 test lines.
Exit criterion: consumers use the complete lifecycle without constructing routes or supplying internal cwd.
Suggested title: feat(webui): Add explicit daemon session contexts
Dependency: PR4. PR #8882 is merged; re-audit its final API and extend its transaction rather than duplicating it.
standalone | workspace { cwd } | live to connection and transition
state.workspaceCwd only at the workspace compatibility boundary,
normalize it immediately, and reject conflicts. It never selects standalone.Verification covers all #8882 failure and supersession cases plus cross-context switching, capability absence, legacy source, outcome recovery, warning rollback, and no-primary-fallback.
Estimated size: 350-650 production lines and 650-1,100 test lines.
Exit criterion: WebUI represents and switches all contexts explicitly while existing visible WebShell behavior remains unchanged.
Suggested title: feat(web-shell): Add standalone chats
Verification covers every entry point, old/capable daemons, capable failure, deferred creation, deep links and restart, context switching, directory states, lifecycle actions, response loss, cleanup pending, child exclusion, Live coexistence, and platform differences.
Estimated size: 450-800 production lines and 800-1,400 test lines.
Exit criterion: the end-to-end product matches this contract and keeps project-only controls and uploads out of standalone chats.
flowchart LR
PR0["PR0 runtime foundation / PR #8890"] --> PR1["PR1 ownership and isolation"]
PR1 --> PR2["PR2 standalone core"]
PR2 --> PR3["PR3 complete daemon API"]
PR3 --> PR4["PR4 SDK"]
PR4 --> PR5["PR5 WebUI context"]
T["PR #8882 transactional switching"] --> PR5
PR5 --> PR6["PR6 WebShell"]
PR0 through PR6 are the required feature sequence. PR5 builds on the final API merged by PR #8882. PR #8874 (workspace uploads) and PR #8817 (fork/move foundations) are follow-up dependencies rather than MVP blockers. No capability is advertised before PR3.
Expected total implementation size is approximately 2,500-4,400 production lines plus 5,050-8,150 test lines. The companion document is excluded from those totals. Capability advertisement is the atomic rollout boundary: partial internal stages remain unavailable to SDK/WebShell clients until PR3 completes the daemon contract.
fileCleanupPending; a singleton delete retry and
the owner-acquisition startup pass resume only the journaled exact path.Unit tests cover source classification, route ownership, containment, state
transitions, rollback, crash recovery, SDK parsing, and UI context reducers.
Daemon integration tests use the real bridge boundary to assert process sharing,
relocation, restart restoration, and owner routing. WebShell tests cover entry
points and capability fallback. Behavioral stages record baseline and final
manual flows under .qwen/e2e-tests/ as required by repository workflow.
File upload and attachments should reuse the workspace upload work from PR #8874 while applying standalone containment. Moving or forking a conversation into a project should build on PR #8817. Neither dependency blocks the MVP.
Storage quotas and orphan retention need a separate policy because automatic deletion changes user data lifetime. A per-session ACP process or OS sandbox would change resource usage and the security model and therefore requires a new design rather than an extension of this contract.
Durable standalone scheduling requires a separate lifecycle design. Parent and child cascade operations require independent retention semantics. Multi-master or daemon-to-daemon proxying and guaranteed mixed-version concurrent ownership would replace the single-owner process boundary and are not incremental changes to this contract.