docs/design/daemon-session-maintenance-writer-lease.md
The daemon can delete, archive, or unarchive a persisted transcript after its in-process ACP owner has closed. A different daemon process can still own the same transcript, so the in-process archive coordinator alone does not prevent the daemon from racing an external writer.
The transcript path and writer-lock path must also be resolved from the same workspace runtime. Falling back to the primary daemon runtime can mutate one workspace while checking a lock in another.
This change covers daemon-owned maintenance:
It does not add lease expiry, heartbeat, hostname-based recovery, automatic steal, force unlock, or a lock-schema migration. Writers that do not participate in the lease protocol still require platform-level single-writer fencing.
Each WorkspaceRuntime resolves one absolute session runtime base directory at
creation. Resolution keeps the existing priority:
QWEN_RUNTIME_DIRadvanced.runtimeOutputDir, resolved relative to the workspaceThe resolved directory is stored on the runtime and injected as
QWEN_RUNTIME_DIR into every managed ACP child. Environment reload may update
other values but preserves this pinned value because changing
runtimeOutputDir requires a runtime restart.
Daemon parent operations that list, read, export, organize, or maintain sessions run inside the selected runtime's storage context. Runtime resolution failures do not fall back to the primary runtime.
SessionService.acquireSessionWriterLease() derives both the writer-lock root
and the active transcript path from the service's fixed Storage instance.
Callers provide only the session ID, process kind, version, and reclaim policy.
Invalid session IDs are rejected before the lock directory is touched.
Daemon maintenance always uses processKind: 'daemon' and
reclaimPolicy: 'never'. The existing lock schema, key, owner record, and
acquire/release protocol remain unchanged.
Every session is processed independently:
Batch requests may process independent sessions concurrently, but a worker holds at most one cross-process lease and never waits while holding multiple leases.
A failed mutation remains the reported error when release succeeds. A release or ownership failure is the externally safe error even if mutation also failed. Logs record the workspace, session, action, error kind, and whether the transcript mutation reached disk; they never include owner tokens or lock paths. Scheduled-task reconciliation follows the actual transcript mutation, not whether lease release subsequently succeeded.
Orphan cleanup first closes the local owner and respects
requireZeroAttaches. A newly attached owner therefore prevents deletion.
Late-spawn cleanup awaits close before acquiring the lease and deleting the
transcript.
SessionArchiveCoordinator.sealMaintenanceAndWait() synchronously rejects new
exclusive maintenance and waits for exclusive operations already admitted.
Shared transcript reads are not included, so a long export does not consume the
termination budget. REST returns 503 daemon_draining; ACP returns a JSON-RPC
server error with data.errorKind = daemon_draining.
Daemon shutdown seals maintenance before child/process teardown and completes only after admitted maintenance leases have been released.
Batch response shapes and existing archive/delete/unarchive idempotency
remain unchanged. Pre-check local session_archiving conflicts (raised by
assertNotTransitioning before admission) still surface as a request-level
409. Conflicts raised inside the admission gate are reported per session in
the 200 response body (errors[]) for archive, unarchive, and delete
alike. Mixed-version writers are unsafe, so deployment and rollback must
drain the old daemon and managed ACP processes before starting the new
version.
Tests use real temporary runtime roots for writer contention and root isolation, cover state changes between the initial and locked classifications, and verify close, mutation, release, scheduled-task reconciliation, and shutdown ordering. Unit tests also cover invalid IDs, duplicate IDs, active/archive conflicts, lease release failures, orphan reattachment, and log redaction. Relevant package tests, build, and typecheck are required before merge.