mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-20 01:21:41 -06:00
fa9c3209d3
* fix(outbound): keep queued TTS/local media alive past its producer process Outbound TTS synthesizes into a producer-owned temp and the raw path is persisted into the durable delivery queue with no copy. The temp is removed when its producer exits (and by a 5-minute timer well inside the queue's ~22-minute retry budget), so a deferred delivery replayed by a fresh recovery process reads a path whose owner already deleted it: the send burns all five retries on a missing file and the voice is never delivered. Give the queue its own copy. Before a deliverable row is published, each authorized local source is read through the same media access the live send uses and copied into a process-generation-owned artifact root outside the media TTL sweep; only the queue payload is rewritten, so the live send stays copy-free on the original path. Custody is proven by owner identity rather than by a marker file or lease table: artifacts are grouped under <pid>-<processStartTime>-<nonce>, and reclaim touches a generation only when its owner is provably gone. A live owner, an unverifiable owner, or a recycled PID whose start time no longer matches are all handled without deleting media that a pending row still needs. Wall-clock age is never an ownership signal. Ordering is durable-state-first: rows commit before artifacts are unlinked, so a crash between the two leaves an orphan for reclaim rather than a delivery that lost its media. Sensitive media reaches neither the spool nor the row, so required sends fail closed and best-effort sends go live-only instead of persisting a row that cannot replay. No schema change, no public SDK surface, no lease/owner table. * fix(outbound): resolve queue media owner identity on every platform Generation ownership read process start time through the Linux/Darwin lock reader only, so on Windows every generation stayed unverifiable and its artifacts were never reclaimed. gateway-lock already paired that reader with the Windows CIM/WMIC creation-time reader behind its own private helper, so the pairing was duplicated policy waiting to drift. Move the pair behind one owner-identity helper and route both callers through it. No new process inspection: Windows still resolves through readWindowsProcessStartTimeSync, Linux/Darwin still through getFileLockProcessStartTime, and neither platform's behavior changes. Also cover the production crash boundary end to end: a real child process stages media, commits the row, and is killed before dispatch; a fresh process then reclaims the dead generation while retaining the artifact its pending row still needs, reads the same bytes after the producer's source is deleted, and drops the artifact once the row is acked. * fix(outbound): close reclaim race and partial-stage leak in queue media Two defects found by external review of the previous commits. Reclaim built its retain set before proving generation ownership, so a short-lived producer (`openclaw message send`) that staged, committed its row, and exited while a sweep was running would have its artifact judged against a snapshot taken before that commit: the sweep saw a dead owner, missed the new reference, and unlinked media a pending row still needed. The retain set is now read after each death proof. That is sufficient rather than merely narrower: a process only ever stages into its own generation, so once an owner is proven dead no further references to it can appear, and everything it committed before exiting is already durable. Staging also leaked on partial failure. When a later source in an entry failed, earlier copies were already published, no row would ever reference them, and reclaim deliberately never touches a live owner's generation, so they accumulated for the process lifetime. Failed staging now releases what it published. Per-payload media staging is sequential for the same reason: a concurrent copy could land after the cleanup ran and orphan itself anyway, and one source at a time also bounds peak memory. * fix(outbound): resolve queue media access exactly as the live send does Queue staging read media through params.mediaAccess directly, while the live send resolves an agent-scoped capability first. The two gates therefore disagreed: agent-generated media under the agent workspace is reachable only through the agent-scoped roots, so staging rejected sources the send itself would have delivered. A required send failed outright, and a best-effort send silently lost its write-ahead row for exactly the media class this change is meant to protect. Resolve the capability once and share it, so staging can neither reject media the send would deliver nor read more than the send may. The live send now routes through the same helper rather than keeping a second copy of the argument list. The spool test previously fabricated an explicit localRoots and placeholder bytes, which hid the gap; it now uses the production shape (no explicit media access, source in the OpenClaw temp root that TTS writes to) and real buffer-verifiable audio, because host-local sends are content-verified. * test(outbound): cover agent-workspace media that only the scoped capability reads The existing staging test used a source in the OpenClaw temp root, which both the raw and the agent-scoped capability permit, so it could not tell the two apart and would have passed against the defect it was meant to guard. Add a case whose source lives in an agent workspace at <state>/workspace-<agentId>. The unscoped default roots reject that path by construction, so a durable row exists only when staging resolves the same agent-scoped capability the live send resolves. Reverting staging to the raw media access, or bypassing the shared resolver, fails this test. * test(outbound): pin agent-workspace staging to the agent-scoped capability The regression case admitted its source two ways that had nothing to do with agent scoping: the state dir sat inside the OpenClaw temp root, which is itself a default media root, and parent-root expansion grants any declared source directory. Either would keep the test green after the agent identity was dropped from the resolver. Move the state dir outside the temp root and set tools.fs.workspaceOnly, which suppresses source-derived root expansion. The agent workspace is then reachable only through the agent-scoped capability, so removing the agent identity, the shared resolver, or reverting to the raw media access each fail the test. * test(gateway): pin queue media retention across retry backoff * fix(outbound): retain queued media for retries Co-authored-by: Masato Hoshino <g515hoshino@gmail.com> * fix(outbound): retain queued media for retries Co-authored-by: Masato Hoshino <g515hoshino@gmail.com> * fix(outbound): retain queued media for retries Co-authored-by: Masato Hoshino <g515hoshino@gmail.com> * docs(changelog): avoid queued TTS merge conflict Co-authored-by: Masato Hoshino <g515hoshino@gmail.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com> Co-authored-by: Peter Steinberger <peter@steipete.me>