Files
CyberGril/docs
zonggongandmultica-agent 78a5cfd8d7 HEL-879 rework: fix the four manager-review edges - (1) dual-port launcher owns the app lifecycle exactly once: uvicorn listeners run with lifespan=off, either listener ending stops the other BEFORE the shared store closes, cancellation cleans up listeners first, and create_app's lifespan gains a fail-closed double-entry guard (the old shape silently re-initialized Store/epoch/life/chat_lock on the second entry); (2) room ownership survives delete failures: end_session keeps the session in a bounded persisted cleanup ledger until the remote delete lands (retry/idempotent, LiveKit 404 = already gone = settled), expiry moves rooms into that ledger instead of silently dropping them (the manager measured zero DELETE attempts on expired ends), graceful shutdown does not hang up live calls, restart compensates only this instance's own recorded rooms bounded by attempts/TTL+grace with abandonment recorded, and the router answers 503 (not 404) while a room is still owned so the browser keeps its owed DELETE; (3) TurnLedger binds each speech handle to its own generation stream explicitly (oldest pending handle gets the stream the SDK starts; a newer speech retires an older generation-less handle) - a handle without a generation can no longer steal the next turn's stream/receipt, the worker discards interrupted/failed bindings fail-closed before consuming; (4) release snapshot v3: verify requires three-way count equality (manifest source = manifest snapshot = restored actual; rows_match is an extra constraint, never an OR bypass), the rollback drill actually restores by source path into a persistent isolated tree and opens the restored DBs there, same-basename DBs get collision-free archive names, duplicate sources are refused; new regression file tests/test_voice_rework_hel879.py 14/14 reproducing every edge first, full pytest 338/338
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 04:10:23 +08:00
..
HEL-879 unified voice candidate assembled and verified on hel-879-assembly (isolated branch; PR#11 and hel-870-assembly untouched) - the merge brings exactly the audited HEL-877 voice backend (aba2aeb: src/cybergril/voice module with zero new dependencies, byte-identical to the author's head) and the HEL-897-verified voice frontend (c61d9c9: web/src byte-identical to the original author's head - the CSS hash change to index-BpZaybfD.css is entirely the author's own components.css +21 lines, no visual rework by this card); the one integration adjustment is test-harness alignment, not a product change: the voice adapter fixtures and verify_hel877_voice_chain.py built their router without a budget config, which the mainline's HEL-882 hard gate (guard.acquire before every real send) now correctly refuses at cap 0 - they now use the same tests/conftest.engineering_budget() every mainline test uses, no gate weakened, no src change on either author's side; full battery rerun on the assembled head: pytest 324/324 (299 mainline + 25 voice), ruff clean, web typecheck 0 errors / lint 0 errors 1 pre-existing warning / vitest 88/88 / build passes, real-server browser 9/9 with the HEL-896 precise-interaction revision (reach card over real HTTP, stage plays the reach-out, unread clears, message-id dedup, restart epoch+1, zero permission API calls, zero non-loopback outbound - evidence docs/rework/hel879/browser/), verify_hel877_voice_chain 8/8 offline full-chain, the .12 livekit-agents 1.8.8 real-SDK stdin check regenerated from THIS head's sources 8/8 ALL PASS (zero writes zero network), check_release_offline R1-R7 passes, pinned-SDK regressions on hel857-venv all green (close-ownership 16/16, shutdown-queued 12/12, inflight 6/6, semantic-recovery 7/7, mem0-real 12/12 - the seal change is exercised through the same PersistentBudget paths), requirement checks 4/4 (origin/main and c121c42, each with and without handoff-HEL-879.json); release-artifacts.json records the exact release code commit (01ff919), the tarball sha256 measured equal to the lock, and the cache-reusable wheels reverified with content digests after the disk-recovery cache incident; honest gaps kept open: real-call acceptance untested and unauthorized (D1 voice, trusted HTTPS device path, budgets), LiveKit room/worker release after API restart still lacks real-environment evidence (_prune only drops the in-memory registry, end_session does delete_room), incremental subtitles remain a contract gap not a completed feature, CG-011 proactive voice stays an explicit no-op; zero real LLM/embed/ASR/TTS, zero downloads, no mic, no service started or stopped on any shared machine, not deployed, nothing merged, authoritative ledgers/freezes/HEL-835 gate untouched, real caps stay 0
2026-10-27 03:36:37 +08:00
HEL-879 rework: fix the four manager-review edges - (1) dual-port launcher owns the app lifecycle exactly once: uvicorn listeners run with lifespan=off, either listener ending stops the other BEFORE the shared store closes, cancellation cleans up listeners first, and create_app's lifespan gains a fail-closed double-entry guard (the old shape silently re-initialized Store/epoch/life/chat_lock on the second entry); (2) room ownership survives delete failures: end_session keeps the session in a bounded persisted cleanup ledger until the remote delete lands (retry/idempotent, LiveKit 404 = already gone = settled), expiry moves rooms into that ledger instead of silently dropping them (the manager measured zero DELETE attempts on expired ends), graceful shutdown does not hang up live calls, restart compensates only this instance's own recorded rooms bounded by attempts/TTL+grace with abandonment recorded, and the router answers 503 (not 404) while a room is still owned so the browser keeps its owed DELETE; (3) TurnLedger binds each speech handle to its own generation stream explicitly (oldest pending handle gets the stream the SDK starts; a newer speech retires an older generation-less handle) - a handle without a generation can no longer steal the next turn's stream/receipt, the worker discards interrupted/failed bindings fail-closed before consuming; (4) release snapshot v3: verify requires three-way count equality (manifest source = manifest snapshot = restored actual; rows_match is an extra constraint, never an OR bypass), the rollback drill actually restores by source path into a persistent isolated tree and opens the restored DBs there, same-basename DBs get collision-free archive names, duplicate sources are refused; new regression file tests/test_voice_rework_hel879.py 14/14 reproducing every edge first, full pytest 338/338
2026-10-27 04:10:23 +08:00
HEL-879 release closure: the four HEL-896 audit findings become enforced behavior - (1) archive consistency: tools/release_snapshot_hel896.py v2 checks the wal_checkpoint(TRUNCATE) return (busy!=0 = hard failure, no deliverable tar, half-built dir removed - 'stopped service first' is a precondition, never a reason to ignore an actual failed result), aborts packaging on any source-vs-snapshot table-count mismatch instead of recording rows_match=False and shipping anyway, and verify_archive now rejects legacy packages that recorded rows_match=False while mapping a real rollback drill (unpack to an isolated dir, open every DB, check rows AND hashes against both recorded source and snapshot counts, emit a source-path rollback_plan - no more tar-internal self-attestation); selftest 9/9 reproduces every old failure shape first (naive WAL copy loses companion, held-reader checkpoint=(1,9,8) leaves the copied image malformed/missing the events row, the manager's rows_match=False legacy package verifying ok) then proves each fix (busy fails with zero artifacts, stale-snapshot mismatch fails with zero artifacts, online mode refuses --file dirs containing SQLite by magic-header scan, stop mode recursively takes embedded SQLite like history_root/mem0_public_history.db through checkpoint+per-table verification, verify maps source paths and passes); (2) early-finish seal: PersistentBudget gains sealed()/seal() writing sealed/sealed_requests/sealed_tokens/sealed_at atomically into budget_total beside the real usage (never faking 55, never zeroing), acquire checks the seal FIRST so every category and every remaining allowance rejects budget_sealed:51/55, the marker survives restart and backup-API archive/restore, reseal is idempotent, and there is deliberately no unseal path (re-test = new approval + new instance per budget-package 6.2.4); tools/seal_budget_hel879.py makes the closure an explicit auditable operator command with a post-seal rejection self-check; verify_budget_split_hel896.py gains S6 (51/55 experience chain completes -> seal keeps usage 51 -> 52nd and cross-category rejected with dialogue daily still 19/60 proving it is the seal not the caps, mistaken restart rejected, archive restore rejected, marker requests=51 intact) now 10/10, HEAD made dynamic instead of hardcoded 04eff61; budget-package-hel895.md 6.2 rewritten to the executable seal order (stop -> seal -> consistency snapshot); (3) single install entry: deploy/hel895-release-candidate.md 2 is now the only copy-paste install (uv --offline, npm --offline, cache-miss = stop and record, network only inside an authorized release window with per-item sha256) and 4 is the only fixed-SDK entry as a five-step chain (verify tarball sha256 9dcef930...dda23 against mem0-stack.lock -> build the wheel from the fixed source with hatchling honestly listed as the missing build backend -> cp311 dependency prep with greenlet/pillow/truststore confirmed cache-reusable on re-run -> local-wheels-only --no-index install where mem0ai must come from the fixed-source wheel, the lock line is version-only -> same-process co-import plus the 92MB fastembed cache placement); deploy/hel874-release.md 2/4 carry superseded banners pointing here, mem0-stack.lock no longer routes installs through the index mem0ai==2.2.1 wheel; (4) exact-commit honesty: audit_sdk_offline_hel896.py derives head from git rev-parse and accepts an output dir, fresh evidence written to docs/rework/hel879/sdk-audit/ (hel896 evidence left untouched) recording closure 55 / missing 41 (28 pure + 13 native) / cache-reusable [greenlet, pillow, truststore] / tarball-matches-lock at c121c42; regression on this head: pytest 299/299, ruff clean (src/tests/tools), release-snapshot selftest 9/9, budget-split 10/10 including the new S6 negatives; zero provider calls, zero downloads, authoritative ledgers/freezes/HEL-835 gate untouched, not deployed, PR#11 untouched
2026-10-27 03:26:30 +08:00
HEL-885 rework: per-row progress matches what was really sent, cross-day and unknown-outcome accounting stay conservative, positive caps require the ledger authority, and the release dependency-fill command actually exists - every Memory.add now carries an anchor for exactly its own row (the HEL-882 whole-batch anchor let the first stored vector mark unsent rows complete; the manager's real-SDK partial reproduction showed the user's original words never leaving the process), a row finishing with a legitimately EMPTY extraction gets a durable sem_row_done record (r4.sql additive table) written inside the worker thread the moment its add completes, resume skips = vector-store anchors ∪ sem_row_done so no row is lost, none falsely complete, and finished work is never re-sent across budget-refusal/crash/restart/lease/concurrency, entry_ids merge across partial attempts, and the swallowed-failure detection is per-row; PersistentBudget.settle/release book on hold.day (the old write to state.day manufactured -80 used on the new day and re-opened headroom the reservation day had already spent; token_usage/request_usage refund the paying day, budget_total stays global) and the empty-budget_total TypeError crash in total_requests_used/total_tokens_used is fixed - the old one-shot-total test passed only because that crash surfaced as chat.error, it now books the total honestly; _atomic_reserve no longer purges aged reserve: rows (a crashed worker's hold is an UNKNOWN outcome - an hour-old timeout is not proof the call never happened; unknown consumption stays billed across restarts, only a provably-unsent release() ever removes a hold, embed_ops and llm_ops share the fix); Mem0Backend refuses construction before importing mem0 when any cap>0 lacks a (non-empty dict) authority block - ledger_authority_required - or when the prefix check fails, closing the fresh-empty-ledger bypass the manager reproduced live, config/semantic.example.yaml ships the authority block as REQUIRED with the explicit cumulative formula (embed_cap=500+approved-increment e.g. 680, embed_reserve=2 frozen untouched, min_used=498; never write 180 as the cumulative cap of a 498-used account) mirrored into docs/budget-plan-hel874.md §2 with the HEL-885 accounting rules; deploy/hel874-release.md §4 replaces the nonexistent uv pip download (rc=2, re-verified) with python3 -m pip download --only-binary :all: --platform manylinux2014_x86_64 --python-version 3.11 --implementation cp --abi cp311 + sha256 manifest + uv pip install --no-index --find-links offline merge + same-process co-import verdict, honest missing-materials list unchanged (no 3.11 wheels cached, zero downloads this card, no version swaps), and tools/check_release_offline_hel882.py gains R7 probing both command surfaces offline; verification: 281 python tests pass on 3.11.16 venv + requirements.lock offline (9 new tests/test_rework_hel885.py cases: partial batch claims only the sent row / empty-extraction row completes and is never resent / process restart sends only the missing row / concurrent ticks never double-send / cross-day settle and release book on the reservation day / aged unknown llm+embed reservations never refunded / missing-empty-malformed-wrong authority all fail closed; test_resume_never_loses_or_duplicates_rows rewritten to drive the REAL product path instead of hand-planting single-row anchors the product never wrote), ruff clean (src/tests/tools), web tsc + 42 vitest + build via npm ci --offline, pinned mem0 2.2.1 install on the deterministic boundary: contract re-run 12/12 (harness now hands a synthetic authority prefix - production defaults not relaxed), new tools/verify_semantic_recovery_hel885.py 7/7 (S0 authority fail-closed, S1 per-row partial with exactly 1 HTTP, S2 restart sends only the missing row proven by HTTP count since mem0 prompts legitimately carry old rows in rolling history, S3 lease recovery + concurrent ticks single-send, S4a/S4b unknown reservations kept), both manager review scripts re-run VERBATIM against this head (partial: completed=[2] only, resume done with 2 entries, HTTP=2, user words actually sent; extra: new-day used=0, over-cap request refused, aged reservation second_allowed=false, uv probe still rc=2), release offline check R1-R7 green, dual-target requirement checks 4/4 with handoff-HEL-885.json; zero real LLM, zero real embeddings, zero non-loopback outbound, authoritative ledgers and freezes untouched, not deployed, PR not merged, no visual changes
2026-10-27 00:01:44 +08:00
HEL-894 rework: a cancelled close owns nothing it did not start — aclose cancellation is now a two-phase ownership protocol - the manager's independent pinned-SDK review of ceb0f35 reproduced two combo holes in the HEL-889 close path: (1) cancelling aclose AFTER backend.aclose began made the shielded finish keep running unowned in the background while the outer except already released the lock and cleared closing — the layer was reported reusable (closing=false/closed=false/client usable), a new tick claimed a batch onto real HTTP, then the orphaned finish closed Qdrant under it (SDK-only branch: batch failed on 'QdrantLocal instance is closed' with row_done=0/llm_used=1; with the caller closing the store afterwards the late worker died on ProgrammingError 'Cannot operate on a closed database' and the reopened batch sat pending), and (2) cancelling a SECOND aclose that was still WAITING for the close lock ran the same except and cleared the FIRST closer's flag (first close still pending but signal=false, a new batch ran to done while the original worker was still on the wire). SemanticMemory.aclose now states the revocation boundary explicitly: the DRAIN phase (nothing closed yet) stays revocable — a cancelled caller leaves everything open, resets the closing signal and the layer remains usable (HEL-889 semantics preserved); the FINISH phase (backend.aclose begun) is irreversible — a cancelled caller keeps the closing markers (a half-closed layer is never reported usable, ticks keep refusing), keeps the lock, and waits — repeated cancels only re-shield — for the layer-owned close task it started (self._close_task, created exactly once with asyncio.create_task and stored on the layer, never dropped to the background) to land before propagating CancelledError, so resources close exactly once; _finish_close also joins this layer's OWN executor via asyncio.to_thread so a cancelled caller can never block the event loop on a synchronous shutdown wait (caller-injected executors stay caller-owned), and signal reset is ownership-gated by owns_close: only the call that actually acquired the lock and set the signal may clear it, and only while no close task exists — a lock waiter cancelled on acquisition touches nothing; module docstring, aclose docstring and the app lifespan comment state the same contract; verification: 299 python tests pass on 3.11.16 venv + requirements.lock offline (5 new tests/test_rework_hel894.py cases, all barrier-driven real product paths: finish-phase cancel keeps ownership and refuses new work with zero sends and zero budget / repeated cancels during finish land the close exactly once / a same-phase second aclose waits then returns idempotently while a third cancelled lock-waiter touches nothing / waiter-cancel + owner draining + refused new tick + original worker completing combo / drain-phase cancel still re-arms for reuse), ruff clean (src/tests/tools), no frontend changes (frontend checks not re-run, stated honestly); pinned mem0 2.2.1 on the deterministic boundary (512-dim stand-in before TextEmbedding construction, loopback-only): new tools/verify_close_ownership_hel894.py 16/16 reruns the manager's two negatives in the same shape with assertions restated for the new protocol (F1 SDK-only: after the cancel closing=true/closed=false/client usable and the cancelled caller does not return before its close lands; the new tick is refused with zero gateway arrivals, batch queued/owner NULL/attempts 0 and llm_used=0; after the gate the close lands exactly once with live_workers=0 and the reopened batch is intact queued/row_done=0/llm_used=0 — before the fix the same run gave closing=false, a claimed batch, QdrantLocal instance is closed, failed/pending with row_done=0/llm_used=1; F2 + store-close branch: same shape and no late error — before: ProgrammingError; W waiter cancel: closing stays set, the new batch stays queued, the original worker completes done/row_done=1 after release and the owner's close then lands once); the manager's review_hel889_cancel.py re-run VERBATIM on the pristine ceb0f35 first reproduced both defects exactly (docs/rework/hel894/before-ceb0f35.json matches the manager's independent JSON), and on the fixed tree stops honestly at its own premise assert server.arrived — 'a finish-phase-cancelled layer still starts new real HTTP' is structurally gone because tick refuses while closing is held (verbatim-after-fix.log; original negative and baseline kept); regressions all green: verify_shutdown_queued_hel889.py 12/12 (queued cancel, timeout drain, between-rows resume, drain-cancel re-arm, concurrent close all preserved), verify_inflight_lifecycle_hel886.py 6/6, verify_semantic_recovery_hel885.py 7/7, verify_mem0_real_hel874.py 12/12, check_release_offline_hel882.py R1-R7 15/15, both manager review_hel882_{partial,extra}.py scripts re-run verbatim pass (extra's uv rc=2 is its own expected negative), HEL-889 same-shape rerun review_hel886_edges_rerun_hel889.py passes; crash semantics unchanged and still honest (unknown wire outcomes billed, proof-gated retries, strict-once not claimed); runbook §4 gains the HEL-894 cancellation-ownership paragraph, docs/requirements/handoff-HEL-894.json written, dual-target requirement checks 4/4 (origin/main and origin/agent/agent/de8874a3803b, with and without handoff); zero real LLM, zero real embeddings, zero non-loopback outbound, authoritative ledgers and freezes untouched, not deployed, PR not merged, no visual changes
2026-10-27 01:43:54 +08:00
HEL-877: wire the audited .12 voice base into the one CyberGril chat chain as backend-only code (no deploy, no real voice call) - new independent src/cybergril/voice module: session+router add POST /api/voice/session / DELETE end / GET honest status on the unified instance (server-side room + dedicated cybergril-voice-agent dispatch + short-TTL join token via zero-new-dependency livekit_wire HS256/Twirp matching the installed SDK's wire format verified claim-for-claim on the .12 agent venv stdin-only; keys never reach the browser; unconfigured/no-profile/TTS-down refuse honestly and capabilities.voice stays false), chat_api_client speaks the exact real.ts RealChatAdapter protocol (per-turn health epoch, bounded 409 resync, robust incremental SSE parsing across multibyte/CRLF/multi-data chunk splits, fixed turn_id idempotency, immediate HTTP-stream close on barge-in without waiting for the next ASR final), llm_chat_api is a livekit llm.LLM adapter whose only backend is POST /api/chat so persona/mood/dyad/memory/life-sharing/budget gates and the one same-turn chat_lock govern text and voice identically (phi spoken verbatim via ChatChunk, never archived, no receipt), receipt.py confirms a sharing receipt only after real SDK playback completion with every negative (interrupted/failed/mid-playout/phi/incomplete/stale-epoch) fail-closed and exactly once per turn, stt_funasr/tts_cosyvoice reuse the proven .12 adapters verbatim with provenance, worker.py assembles the dedicated worker (independent voice_state_cybergril.json so D1 stays undecided, no second persona, cg.subtitle data-channel contract for the visual lead, CG-011 proactive seam left as an explicit no-op hook, built-in health port 8082); app.py keeps a minimal registration with voice_service injection for isolated tests; deployment drafts only (config/voice.example.yaml, worker.env.example, restart_cybergril.sh.example with exact-pattern process matching, serve_dual_8360_8361.py keeping HTTP 8360 and the HTTPS voice entry on ONE process sharing state/locks, runbook-hel877-voice.md contract+startup+rollback incl. the >=6GB GPU precondition and the do-not-copy historical fuser warning) - verification: 281 pytest pass on 3.11 venv + requirements.lock after merging the chief engineer's mid-flight HEL-874 rework e978e87 in full (parallel work preserved), verify_hel877_voice_chain.py 8/8 offline full-chain incl. socket-level barge-in fail-close and text/voice mutual same-gate cancellation, real-SDK check on the .12 livekit-agents 1.8.8 venv 8/8 ALL PASS via SSH stdin with zero writes zero network, dual-port single-process smoke on loopback 18360/18361 with a throwaway self-signed cert then stopped, ruff clean on all new code, dual-target requirement checks pass vs origin/hel-870-assembly (current tip e978e87) and origin/main with handoff-HEL-877.json; zero real LLM/embed/TTS/ASR requests, no mic/camera, no downloads/installs, no service start/stop, no 4080 access, no third-account or profile writes, web/ untouched, not merged, not deployed
2026-10-26 22:42:19 +08:00
HEL-876: voice-base read-only verification + R5/R6 readiness materials - docs/voice-r5-readiness-HEL876.md records the 2026-10-26 bounded read-only audit of 192.168.200.12 with timestamps: LiveKit/FunASR/8090 alive with health evidence, 8081 corrected as the voice-agent livekit worker (not a legacy lip-sync piece), CosyVoice TTS 9880 gracefully stopped since 2026-10-24 22:17 and NOT running, MuseTalk 8010 unit gone, ComfyUI 8188 holding 26.6GB of the 32GB GPU leaving 3.9GB free < ~5.6GB TTS requirement (the only hard resource gap, coordination not unilateral action), voice catalog (11 voices, rain selected) with whitelisted config field names only and no credentials read or printed, .11 gateway model-list probe and open-meteo outbound both 200; the R5 seam is fixed at src/cybergril/api/app.py:488 - the worker must reuse the same chat chain via a livekit LLM adapter speaking the identical RealChatAdapter protocol (epoch sync/serialized turns/barge-in cancel/idempotent replay/receipt round-trip), proven offline with zero model calls by scripts/verify_hel876_voice_seam.py (six checks all pass on 3.11 venv + requirements.lock installed from the local uv offline cache, ruff clean, explicitly NOT claimed as end-to-end voice verification); R6 seams located read-only: city placeholder ManagePanel.tsx:182 with no backend, no weather client (outbound now proven), demo-only material import :178/:183, full-backup boundary = companion.db + vec_public + mem0_public_history.db + embed ledger where copying the single db file is not a complete backup and Persona.fingerprint is the same-her check; three-column honest conclusions (verified / unverified / actually-missing) plus six zero-cost work items ready for dispatch; no product code touched (src/ web/ unchanged), no new product PR, candidate branch not merged, dual-target requirement checks 4/4 pass with handoff-HEL-876.json
2026-10-26 21:42:41 +08:00