Compare commits

...
Author SHA1 Message Date
总管andmultica-agent 8cd4265182 HEL-797: allocate first-use memory budget across team verification and user trial
Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 22:41:31 +08:00
总管andmultica-agent 049d3dbdff HEL-913: correct runtime wiring and closeout instructions after coordinator checks
Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 22:39:04 +08:00
总工andmultica-agent 8ea5fdc4c9 HEL-913 close-out: executable package version/dir entry, consistent pending-approval enablement, complete auditable mem0 install (isolated venv), identity-checked ctl scripts + voice-window wiring (42/42 offline stub checks), fact-table accuracy fixes
Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 22:30:56 +08:00
总工andmultica-agent 0146e370b1 HEL-913: unified-experience run package — fixed-version manifest, sanitized templates, offline zero-real-call verification (34/34), mem0 cp311 gap verified, capability list + one-shot enablement proposal
Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 22:04:52 +08:00
施工员2号andmultica-agent 4a38106eef HEL-911: fix AgentHandoff/shutdown seams, attribute & fix the 3.66s pre-generation wait
- worker _on_item: filter conversation_item_added by the real 1.8.8 item
  discriminator (ChatMessage | AgentHandoff | _TypeDiscriminator); only
  assistant str-part body feeds the echo filter (HEL-910 AttributeError)
- bind_interrupt_channel unload: async (JobContext shutdown callbacks are
  awaited), idempotent, disconnect-safe; interrupt/receipt/budget paths
  untouched (HEL-910 TypeError await None)
- TURN_HANDLING: explicit turn_detection=None — 1.8.8 silently installs an
  English EOU TurnDetector whose unlikely verdicts on Chinese finals push
  endpointing to max_delay=6.0 (measured 3.667/3.654s HEL-910 call2; offline
  A/B on the real SDK: baseline 3.704s vs fixed 0.004s). Endpointing
  0.8/6.0 kept verbatim; no VAD/buffer_turn/semantic change
- scripts/gen_hel911_wait_repro.py: offline reproduction harness (real
  AgentSession + real silero VAD + HEL-910 wav and measured timings)
- tests: +3 real-SDK checks (11/11 on .12 agent venv; prior 8 regressions
  kept, unload call sites await the new protocol)
- hel910_call1.mjs reliability: segmented probe/recorder dumps to disk,
  timing-A trigger requires the server-committed voice turn row (not a
  we-sent-it proxy); node --check only, no real run
- window-record.md §9: appended factual corrections (companion 9 not 18,
  turn_start +6.099 not +4.10, assistant_final is the playout/receipt gate,
  post-window ListRooms token failure is not empty-room evidence); original
  record untouched
- docs/rework/hel911/summary.md + handoff-HEL-911.json; requirement checks
  pass vs origin/main and 407fc81

Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 21:25:25 +08:00
xiaomoandmultica-agent 407fc8158c HEL-910: real-call retest window record — web text interrupt, wait chain, budgets, cleanup evidence
Co-authored-by: multica-agent <github@multica.ai>
2026-10-29 20:24:52 +08:00
xiaomoandmultica-agent 958c6962fb HEL-908 R1/R2 rework (HEL-909 review): whole-turn reasoning gate + cg.interrupt data channel
R1 — reasoning separation truly closed. The streaming hold leaked wholesale
on real token arrival (review A1: 'private reasoning '*20 beyond the 128-char
bound, no sentence ender; review A2: a Chinese sentence ender released the
hold) AND the adapter consumed the stray closer, so the app-level turn split
could never recover — the previous 'persistence and receipt always clean'
claim was false and is retracted. Per the engineering ruling the adapter now
has two explicit endpoint modes (GOV-006, no heuristics, no new engine):
buffer_turn (TurnReasoningGate) emits NOTHING during generation, splits the
whole turn at stream end and only then delivers the clean visible text; it
is set explicitly for all tiers in config/models.yaml (first visible text
waits for the full generation; cancelled/timed-out turns emit nothing and
end exactly as before — recorded and test-pinned, low latency NOT claimed).
stream (default, backward compatible) keeps explicit-tag/field semantics and
passes stray closers through unmodified so the turn-level split keeps its
signal. Both review strings and the recorded id=12 stream replay clean at
every chunk size through the real adapter and the real app into chat.delta,
the archive and the receipt; normal zh/en preserved byte-for-byte; unclosed
explicit thinking still fails honestly.

R2 — an authoritative interrupt when the LLM already finalized and TTS is
still playing (review evidence C: zero interrupts, old audio played out).
The page's existing hush() entry (ChatPanel untouched) now also publishes
{"v":1} on the room's own reliable data channel topic cg.interrupt
(fire-and-forget: a dropped publish never blocks or breaks the text send);
the worker subscribes data_received, validates topic/sender/payload and
calls session.interrupt(force=True) — the SDK's own programmatic interrupt,
force confirmed against the installed 1.8.8 source (non-force raises
RuntimeError on allow_interruptions=False speech). Failures log loudly; the
listener is unloaded via add_shutdown_callback; the server-cancel path in
llm_chat_api also interrupts with force and logs failures.

Verification (offline, zero real requests): real-SDK harness 8/8 including a
real SpeechHandle with a genuinely pending playout wait released by the
cg.interrupt message after a clean chat.final (evidence-C shape) plus the
full validation matrix; .venv311 full suite 480 passed / 8 skipped (2
test_r3 failures pre-exist at 5625de6); web npm ci from the existing lock,
typecheck 0 errors, lint 0 errors + 1 pre-existing warning, vitest 148/148,
build ok; ruff clean; check_requirements vs origin/main and b8ec832 with the
corrected handoff.

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 22:21:16 +08:00
xiaomoandmultica-agent b8ec832f35 HEL-908: terminal chat.cancelled + no-revive gate for text interrupt; reasoning/content separation at the adapter seam
Bug 1 (recorded call1): server-side cancel ended the voice SSE without a
terminal frame BEFORE the first delta (worker.log:108 SDK retry 13ms after
the text POST); the adapter raised a connection error, livekit-agents 1.8.8
silently retried the same turn_id, the retry re-acquired the gate, cancelled
the user's text turn and re-spoke a full ~32s reply. The HEL-907 claim that
the _retry_on_chunk_sent guard never worked is corrected: the guard only
applies after output (output_sent), verified against the installed SDK
source. Fix: explicit chat.cancelled terminal frame; a cancelled turn_id is
refused re-activation (never touches the newer active turn); the worker ends
cancelled turns on the SDK's own 499 path (no retry) and stops the cached
playout via SpeechHandle.interrupt(); a server_cancelled latch short-circuits
any re-entry with zero network. Failed-turn retries, completed-turn replay
and receipt validation are unchanged (dedicated tests).

Bug 2 (recorded row id=12): the endpoint streams implicit-open reasoning
plus a stray </think> inline in content into subtitles and TTS. Fix:
ReasoningSeparator at the OpenAI-compat adapter seam (cross-chunk safe,
reasoning_content field side-channel on LLMDelta.reasoning, never persisted)
plus a turn-level split before persist/final/receipt. Recorded stream
replays clean at every chunk size; normal zh/en content preserved
byte-for-byte; documented residual: implicit-open reasoning whose closer
lies beyond the 128-char hold streams partially (persistence stays clean).

Offline verification: .venv311 full suite 445 passed / 5 skipped (2
pre-existing test_r3 failures fail identically at 5625de6); real-SDK harness
(livekit-agents 1.8.8, agent venv) 5/5; reasoning separation 37/37; ruff
clean. Zero real requests, zero deployment, no dependency changes.

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 21:27:08 +08:00
xiaomoandmultica-agent 5625de6606 HEL-907: custom-voice web flow + 3 real calls window record, budget accounts, recycle evidence; hel900 harness minimal fix
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 20:29:23 +08:00
总工andmultica-agent 038f1eeae7 HEL-906 rework materials: r5-followup-package §4 dual-accounting + fail-closed wording, handoff update
- §4: conservative pre-record vs confirmed-delivered accounting, ledger
  failure fail-closed, guarantees scoped to armed process (cross-process
  bounded overshoot / crash undercount stay monitored boundaries, no
  'never exceeds' claims), window execution convention (3 sequential
  calls, tightened params with margin), verification command now 24 cases
- milestone table registers rework commit cf395d4
- handoff-HEL-906.json: rework deliverable/acceptance/verification as above

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 09:04:29 +08:00
总工andmultica-agent cf395d4e06 HEL-906 rework: persistence fail-closed stop + conservative pre-record accounting
- asr_usage: ledger write/read failure, corruption or mid-window vanish now
  stop all window outbound (fail-closed), preserve known consumption
  (monotonic, no zero-budget default), log + record guard_stop (durable
  across same-window restarts)
- asr_usage: conservative reserve before send (failed/cancelled sends are
  NOT refunded) + separately tracked confirmed-delivered seconds; replay
  restores both
- stt_funasr: reserve_audio before ws.send, note_audio_delivered after
  successful await only
- tests: 24 deterministic offline cases incl. manager's budget-regression
  repro, read/corrupt/missing rejection, record-then-raise/cancel fake ws,
  guard_stop across restart

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 09:02:51 +08:00
总工andmultica-agent ccfc04edcd HEL-906: close r5-followup-package §4 blocker, register milestone, add handoff
- §4: ASR audio/retry rows now program-gated (pre-send/pre-connect refusal
  when armed, fail-closed default); mechanism, arming env, reproducible
  verification commands, program-guarantee vs monitor-only boundary
  (bounded cross-process staleness, bounded crash undercount, .12 real-stack
  pre-window check still required); wall-clock proxy demoted to lower-bound
  cross-check; first-line status updated
- milestone table: register hel-906-asr-usage-counting @ ecec9df (candidate,
  PR not merged, pending 总管 review)
- handoff-HEL-906.json (v1.12, read at 0b804a4); requirement checks 4/4 pass
  (origin/main and 0b804a4, with/without handoff)

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 08:46:07 +08:00
总工andmultica-agent ecec9df8e8 HEL-906: continuous-call ASR usage metering + one-shot window guard in dedicated worker seam
- asr_usage.py (stdlib-only): append-only JSONL ledger, per-call/window audio
  seconds counted from actually-sent frames (sample-rate aware, resend counted),
  connect attempt/failure/retry semantics (normal per-utterance reconnects are
  not retries), fail-closed default (real cap=0 unless a window is armed),
  pre-send blocking so no byte crosses the cap, ledger replay for restart
  recovery and cross-window isolation, resumable summarize CLI
- stt_funasr.py: guard hooks on the real send path (check_connect before
  open_utt, precheck before send, count after send, recv-side death recorded,
  per-utterance flush aggregation); usage=None keeps prior behavior
- worker.py: entrypoint builds guard from env per job, room name as call id,
  on_exhausted -> ctx.shutdown ends the call, window summary in start log
- tests: 17 deterministic offline tests incl. fake livekit/websockets SDK
  driving the real _run seam, sample-rate variants, silence/cancel/failure/
  reconnect/repeated-end, cap rejection before crossing, ledger persistence
  across restart, room deletion keeps history, other windows unaffected

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 08:44:20 +08:00
总工andmultica-agent 0b804a49ef HEL-905: clarify verified baseline vs registration commits in milestone table
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:41:25 +08:00
总工andmultica-agent fdfa3424b7 HEL-905: register final verified head 03cec26 in follow-up package milestone table
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:41:12 +08:00
总工andmultica-agent 03cec2679e HEL-905: unified review handoff + follow-up package honesty fixes (first-line blocker, register_asr scope, ledger jq boundary, mic-interrupt T0 time base)
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:40:57 +08:00
总工 7cb73429f7 HEL-905: integrate HEL-904 backend rework (recoverable register-failure mapping, call-window voice pin) 2026-10-28 01:14:35 +08:00
总工 7c387db0a0 HEL-905: integrate HEL-903 preserved frontend rework (author source 51669aa, 11 files byte-preserved) 2026-10-28 01:14:32 +08:00
总管andmultica-agent 088ac621dc HEL-903: retain author log bytes without newline conversion
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:11:54 +08:00
总管andmultica-agent 3efecced3f HEL-903: record exact source preservation and four requirement checks
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:11:41 +08:00
总管andmultica-agent 51669aae6d HEL-903: preserve visual author voice status, call voice and recovery fixes
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:10:48 +08:00
施工员2号andmultica-agent 07dda2bfa2 HEL-904: backend rework — recoverable register-failure mapping, worker-state corruption docstring, call-window voice pin closes capture/worker race
- register(): local metadata write failure -> LocalRecordFailed -> 503 Chinese
  message with the cg- id (shared registration NOT rolled back, refresh list to
  recover, never re-register); register_asr journaled BEFORE local persist so
  failed registrations still count in the resource ledger
- write_worker_voice(): docstring now states the whole-file rewrite on
  unreadable/corrupt state (other keys preserved only when parseable)
- capture->worker race closed with a session-immutable-snapshot equivalent:
  _capture_voice holds the captured voice (no await gap); apply_selection
  defers its worker-state write while any call is live; open_session releases
  the hold on failure/cancel; VoiceSessionService.on_session_settled releases
  on explicit end, end-into-debt and TTL expiry; response voice == the voice
  the worker actually reads for that call, mid-call changes affect the next
  call only; overlapping-call residual documented honestly
- tests: hel902 fault-injection case 1 rewritten for the new contract
  (recovery + restart + 409 gate kept), 4 new deterministic Event-gated race
  tests (worker read delayed past dispatch, concurrent apply, failure/cancel
  cleanup, next-call voice), zero sleep-based assertions
- docs: contract §2.2/§2.5/§3 updated; r5 followup package tightened (head
  milestones d167ba9/b09a907/pending-review, register ASR vs call ASR split,
  call-ASR counting means + honest pending blocker, single-timebase frame
  silence measurement, D1 female_soft reference audio pinned by sha256,
  recycling checklist, shared-TTS snapshot/restore, git-revert is not
  recycling, caps count failures/retries/outbound-no-response)
- evidence: pytest 380/380 (376 baseline + 4), verify chain 12/12, ruff clean,
  requirement checks 4/4 (origin/main and b09a907, with/without handoff)

Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 01:09:19 +08:00
总工andmultica-agent b09a907747 HEL-902 unified offline review: integration branch verified (pytest 376/376 + chain 12/12 + req checks 4/4), fault-injection suite for register/session/journal half-states, frontend preservation re-verified 8/8 + author logs consistent, one must-fix (services wording) + five suggestions consolidated, real-test approval package prepared (caps stay 0 until approved), requirements README baseline-line erratum v1.11->v1.12
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 00:37:37 +08:00
总工 d167ba927c Merge remote-tracking branch 'origin/hel-879-assembly' into hel-902-integration 2026-10-28 00:20:10 +08:00
总管andmultica-agent 37b4e690cf HEL-900: record byte preservation and requirements verification
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 00:18:06 +08:00
总管andmultica-agent 701442d4d4 HEL-900: preserve visual director custom voice frontend unchanged
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 00:16:59 +08:00
总管andmultica-agent be149cd6f7 HEL-879 verify endpoint grants and correct final window evidence
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 00:11:44 +08:00
opencodeandmultica-agent 376653135b HEL-879 R5 real-machine window executed (2026-10-27 15:26:54-15:58:10 UTC, 31min16s, within 2h/3-call/40req/500k-token/200-TTS-segment/0-embed authorization): real chain verified on .12 (isolated 8360/8361 dual-entry + 8363 WSS bridge + dedicated worker 8082 + shared TTS 9880 recovered for the window only) -- 3 conversational calls incl. 3 continuous rounds with receipts acked (4 sharing_attempt ack_at/judged), 3 real barge-in interrupts (turn_cancel interrupted, old turn fail-closed), hangup deletes room with ~20ms residual, abrupt-drop rooms TTL-compensated clean after API restart epoch 4->5, voice/text mutual cancel via same chat lock observed (voice-cancelled-text direction); usage ledger 14/40 requests, 42717/500000 tokens, 12/200 TTS segments, 0 embeddings, 0 new spend, budget sealed (real usage preserved) + consistency snapshot archived; two real-machine fixes with evidence: livekit_wire server token now carries SDK-parity video grants (room_admin/room_create; grantless bearer rejected 401 by real server) and Twirp calls switched to JSON encoding (protobuf body unparseable), offline fakes updated, 352/352 pytest + ruff(src/tests/scripts) clean; self-signed cert reissued critical CA:FALSE SAN=IP after rustls CaUsedAsEndEntity rejection; D1 audition README corrected to 'non-custom-function entry' per user feedback (comment 01a17de1); precise ~1s interrupt stop timing and text-cancels-voice direction honestly listed as not-obtained (follow-up window); all window-only processes torn down, protected services untouched, ComfyUI stays paused
Co-authored-by: multica-agent <github@multica.ai>
2026-10-28 00:00:41 +08:00
施工员2号andmultica-agent ce7279e68f HEL-899: custom voice via reference audio on the unified API — reuse .12 panel zero-shot registry (server-side proxy, cg- namespaced IDs never collide with the 11 builtin voices, panel stays the only voices.json writer) + CosyVoice fresh-synthesis preview (unknown voice pre-checked: no silent default fallback) + persisted selection/manifest/usage journal in instance data space with atomic writes and full rollback; session-open captures the effective voice (selection if valid else worker-file value else default, D1 domain untouched when user never chose) pinned per call, mid-call changes land at next call at the earliest; explicit allow_asr gates FunASR transcription, preview/ASR journaled, preview writes no chat/memory/receipt rows; zero new deps (raw-bytes upload endpoint), zero real calls in tests: 18 new cases (formats/size/duration refusals, collision+concurrency, panel down/reject/lying, tts down, rollback, restart persistence, call pinning, identity/shared-state untouched) + offline chain script 12/12, suite 370; req governance v1.12: CG-010 boundary updated to custom-voice-first per user comment 01a17de1 (verbatim evidence + changes + manifest), checks pass vs origin/main and base 8dc84f8 with handoff-HEL-899
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 23:34:00 +08:00
zonggongandmultica-agent 8dc84f88e9 HEL-879 final closure: integrate preserved HEL-898 rework (b0dcb17->18aef2a ff, zero backend diff), one-pass review of the three fixes passes offline (agent-left teardown via existing fail path incl. DELETE debt; closed set per-call unbounded, seq gate unchanged; speaking-state sentence-granularity display with in-place grow), web 118/118 + typecheck/build/lint(0err/1known) green, req checks 3/3; deliver D1 zero-synthesis audition package (11 reference recordings fetched read-only, sha256 matched per file, offline index.html + manifest; repo keeps manifest/provenance only) and update r5-test-package section 4/8 into the delivered-audition + consolidated request form; zero real calls/downloads/installs/deploy/merge, caps stay 0
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 07:24:27 +08:00
总管andmultica-agent 18aef2a459 HEL-898: record independent 118 tests and 53 browser replay checks
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 07:14:21 +08:00
总管andmultica-agent 9254c17316 HEL-898: preserve director three-fix rework verbatim on b0dcb17
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 07:09:23 +08:00
总管andmultica-agent b0dcb17e1a HEL-879 keep pending frontend acceptance explicit in R5 package
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 06:49:33 +08:00
总管andmultica-agent 9756db3204 HEL-879 clarify dependency inventory and evidence scope; clean bridge lint
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 06:48:37 +08:00
hel-879-workerandmultica-agent 11fd0e2dc2 HEL-879 R5 test package v2 (materials rework per manager's 4-item list; zero execution): (1) dependencies split into two tracks - R5 voice-only chain verified present (LiveKit 7880 plain ws / FunASR / agent venv livekit-agents 1.8.8 / CosyVoice weights, GPU 30520MiB free) vs FULL unified experience honestly listed as 41-item closure truly-missing (mem0ai fixed-source wheel offline-built sha256 698284b6 352586B, greenlet/pillow/truststore cache-reusable, 37 pending download authorization, hatchling resolved; existing .12 product venvs probed have NO mem0 and are system 3.12.3 - 'zero missing' does not hold for the full experience; fastembed models actually 190.4MB+0.05MB, hel895 doc's ~92MB flagged as erratum); (2) trusted HTTPS+WSS rewritten with evidence - retracted 'one self-signed click covers LiveKit WSS 7880' (7880 is plain ws shared by generic worker and 8090 demo, 7881 is not an HTTP endpoint, the 8090 https page itself hands browsers ws:// = mixed content with no proof it ever worked); proposed dedicated 8363 TLS bridge deploy/voice/wss_bridge.example.py (asyncio+ssl stdlib, zero new deps; local smoke TLS1.3 + byte transparency pass, wss-bridge-smoke.log), livekit_url=wss://...:8363, per-device per-port trust steps, ignore-cert switches explicitly banned, iOS/Android behavior marked pending real device; (3) budget per-category - acquire() is an atomic pre-send hold so failures/retries/interrupts/background all count (LLM call count != successful chat lines); LLM+embedding = code-enforced hard gates, ASR/TTS/session-creation honestly listed as ops-only caps (window<=2h, calls<=3, TTS<=200 segments; session-cap listed as engineering follow-up); proposed caps do not self-activate, caps stay 0; (4) engineering params decided by engineering - 8360/8361/8363/8082 and /opt/voice-agent-cybergril + /opt/cybergril-voice-certs all verified conflict-free read-only; D1 rewritten to the real 11 voice IDs with zero-synthesis reference-audio audition plan (8090 panel has NO audition and mutates shared state - v1 claim retracted; generic demo 'rain' is not this user's choice). .12 facts from read-only SSH probes, evidence sheet r5-facts-20261027.md (secrets redacted); web/src and src zero changes; 4x requirements checks pass (origin/main and f92a86f, with/without handoff); handoff appended; zero real LLM/embed/ASR/TTS/mic, zero download/install/deploy/merge, real caps stay 0
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 06:44:42 +08:00
zonggongandmultica-agent f92a86f5d8 HEL-879 unified integration review: actual fc905fe worker frames (generated on the .12 real SDK, read-only SSH stdin) fed through the preserved HEL-898 frontend - fixtures + 9 vitest cases pass (incremental->final->record takeover, phi/no-receipt/interrupt/proactive-during-playout lifecycles, user-interim/hush/drop-reopen boundaries, replay/late/out-of-order frames all dropped); seq namespace analysis (job=room=dispatch=controller call, one entrypoint per job, reconnect keeps the bus, single dispatch site per fresh room - no same-room second-job reset path; residual gap: agent-left zombie UI, for the author); DOCUMENTED GAP: closed-64 squeeze lets a duplicate high-seq turn_start re-enter a retired speech (not producible by the current worker; unbounded-Set fix suggested for the author); browser layer NOT independently re-run (no GUI system libs locally, zero-install policy) - hel879_unified_shots.mjs harness committed for Edge replay, author's 61/61 + theme screenshots remain the interface evidence; regression pytest 352/352, vitest 108/108, typecheck/build/lint(0 err/1 known warning)/ruff clean, requirements checks 4/4 (origin/main and fc905fe, with/without updated handoff-HEL-879); R5 real-test package prepared for the main card (r5-test-package.md) with zero execution; mock &turn/&id params are additive test-infra extensions, default semantics unchanged
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 06:27:27 +08:00
总管andmultica-agent fb7ea846ff HEL-898: record exact source preservation and independent offline checks
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 05:54:01 +08:00
总管andmultica-agent 46ac0a7a20 HEL-898: preserve director v2 subtitle wiring verbatim on fc905fe
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 05:53:00 +08:00
zonggongandmultica-agent fc905fe6bd HEL-879 timing closure: the three contract-approval findings fixed as implementation-timing only (v2 envelope fields/event kinds untouched, web/src untouched, no re-audit of passed binding/backup/budget work) - (1) open_session re-kicks the cleanup owner AFTER registration+persist: a real dispatch await yields the loop and the pre-dispatch kick's cleaner already exited with nothing to watch (manager repro: 20ms real dispatch await, TTL 25ms, 120ms of silence -> DELETE=0, expired registration still present, cleaner=None; fixed: exactly one DELETE, registration settled, no dangling coroutine - the bounded auto-expiry/shared-cap mechanism itself is unchanged), new regressions reproduce first on the old code with a genuinely-yielding dispatch stand-in (asyncio.sleep, not the instantly-returning MockTransport) plus an earlier-deadline wake negative; (2) speak_proactive_row takes the handle the real sync AgentSession.say returns (iscoroutine only unwraps await-style stand-ins - the real SpeechHandle itself defines __await__, so awaiting it would wait out the whole playout), publishes proactive_start IMMEDIATELY, registers the handle's own end/cancel, then waits out that one row (sequential rows kept); .12 real-SDK probe: start visible while handle_done=false, end only after, cancel-only on interrupt, v2 fields identical; (3) _after_playout gates assistant_final on the completed+legal-same-turn-receipt conditions BEFORE anything is sent - incomplete streams, missing/partial receipts and stale epochs close with turn_cancel only (reason incomplete/no-receipt), never final-then-unack; the complete+receipt path keeps BOTH final and ack; phi/interrupt/failure/unattributed fail-closed unchanged; real-SDK check extended to 14/14 ALL PASS on .12 (proactive timing, proactive interrupt, no-receipt no-final negatives all driving the actual worker module's registered callbacks), before/after evidence in docs/rework/hel879/timing-fix/ and sdk-check-hel879-timing.log, reusable probe generator scripts/gen_hel879_timing_probe.py; regression at this head: pytest 352/352 x3 consecutive runs, ruff(src/tests/tools) clean, verify_hel877_voice_chain 8/8, requirements checks 4/4 (origin/main and c121c42, each with/without handoff-HEL-879.json, handoff appended with this round's scope); zero real LLM/embed/ASR/TTS, zero downloads, .12 touched read-only via SSH stdin only, not deployed, nothing merged, PR#11 and hel-870-assembly untouched, real caps stay 0
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 05:31:05 +08:00
zonggongandmultica-agent 3228b1e0d7 HEL-879 remaining closure: auto-expiry cleanup ownership, exact SDK speech-turn attribution, and the versioned v2 subtitle contract for HEL-898
A. Room lifecycle (总管 review items A-1/A-2): the single cleanup owner now watches
   ACTIVE session deadlines as well as debts - a session nobody touches expires into
   the ledger and has its room really deleted with zero further requests (old code:
   _cleanup_loop only scanned debts, expired rooms were never deleted until some
   request ran _prune). Explicit end and the background sweep share ONE persisted
   attempt/age cap and abandoned state - _sessions no longer bypasses it (the
   manager measured 2 background attempts + 3 more interactive deletes = 5); an
   abandoned debt raises CleanupAbandoned -> 503 with room/attempts/reason instead
   of pretending done-404, success clears both _sessions and _debts, restart
   continues the same persisted cap, close/cancel leaves no dangling coroutine, and
   only this instance's own rooms are ever touched. New wake event makes a fresh
   debt retry immediately instead of waiting out a deadline sleep.

B. Queued speech turns bind EXACTLY (总管 interleaving note_handle(H1)->H2->
   note_stream(S1)->S2 gave H1=None, H2='turn-one'): CyberGrillLLM.chat now reads
   the SDK's own agents_speech_handle ContextVar (livekit-agents 1.8.8 sets it in
   _create_speech_task around every speech pipeline; source-verified on .12 and
   asserted at runtime) so each ChatApiLLMStream carries speech_handle/speech_id of
   the very handle whose generation created it. TurnLedger binds only on that exact
   attribution - a newer speech NEVER retires an older pending handle, say/proactive
   generations bind nobody, missing attribution fails closed (no ack, no subtitle
   turn), interrupt/failure discards only its own binding.

C. Versioned subtitle contract v2 for the visual author's wiring card (HEL-898):
   every cg.subtitle payload gains {v:2, seq} (session-monotonic), turn identity
   (speech_id, turn_id, same-source message_id for proactive rows) and explicit
   lifecycle events - turn_start/assistant_interim/assistant_final/turn_end/
   turn_cancel(reason) plus proactive_start/proactive_end/proactive_cancel that
   ride the say SpeechHandle instead of the receipt branch. Accumulators are
   isolated per speech_id (a late delta after cancel is dropped producer-side);
   seq + speech_id give the frontend two independent stale-rejection rules. Full
   executable contract: docs/voice-subtitle-contract-v2.md with recorded samples in
   docs/rework/hel879/subtitle-samples/ (scripts/gen_subtitle_samples_hel879.py).
   web/src untouched.

Verification: the .12 real-SDK check now LOADS THE ACTUAL WORKER MODULE and drives
the callbacks IT registered on a real AgentSession (no hand-copied functions) -
both-queued delayed generations bind exactly, a say sandwiched between user turns
steals nothing, unattributed generations fail closed, interrupted turns cancel
without final/receipt (11/11 ALL PASS, source-level proof of the ContextVar wiring
included; evidence docs/rework/hel879/sdk-check-hel879-remaining.log). The old r2
lifespan test flake is root-caused (clock freeze and block were two separate portal
calls; a 10ms life tick could slip between them and claim the frozen day unblocked)
and fixed atomically without raising timeouts or dropping assertions - 12/12 solo,
full suite 350/350 three times in a row on .venv311; ruff back to the single
pre-existing error; voice chain 8/8; release offline R1-R7 with zero failures;
requirement checks 4/4 (origin/main and c121c42, each with/without handoff).
Zero real LLM/embed/ASR/TTS calls, zero downloads/installs, nothing deployed or
merged, real caps stay 0.

Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 05:10:52 +08:00
zonggongandmultica-agent b4dadd82b3 HEL-879: refresh release-snapshot selftest (12/12), budget-split (10/10) and sdk-audit evidence at the exact rework head so the recorded code_commit matches the code it verifies
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 04:27:27 +08:00
zonggongandmultica-agent 7c73d392b9 HEL-879 rework materials: hatchling cache correction with the offline mem0ai wheel actually built from the fixed source (sha256 698284b6..., zero downloads/installs, no 3.10 mixing, recorded in release-artifacts.json), hel895 release doc §0 now separates the historical HEL-895/896 candidate from the current hel-879-assembly candidate and §4/§5 carry the v3 snapshot semantics, budget §6.2.4 amended - a sealed library is never reopened by raising total_requests (contradiction removed), runbook §3.3/3.5/3.6/4.4/4.5 corrected (bounded cleanup ledger replaces the unproven rooms-die-naturally claim; assistant_interim kind; real-but-default-off proactive seam; single-owner dual launcher), one comment-only correction in web/src/voice/session.ts removing the unproven server-auto-cleanup claim (no logic/style change; CSS and JS bundle hashes byte-stable), CG-011 + her incremental subtitles now have a real minimal backend seam (voice/seam.py + worker/llm_chat_api wiring, proactive default off) with the precise frontend interface and acceptance list in docs/rework/hel879/voice-seam-interfaces.md, real-SDK check extended to 10 items (worker callback path on real SpeechHandle/SpeechCreatedEvent; interim seam through real LLMStream) and re-run on .12 ALL PASS, handoff-HEL-879.json appended with the rework scope; regression at this head: pytest 341/341, ruff clean except 1 pre-existing error identical at 7f0a739, web typecheck 0 errors / lint 0 errors 1 pre-existing warning / vitest 88/88 / build passes (CSS index-BpZaybfD.css unchanged), voice chain 8/8, budget split 10/10, release offline R1-R7, http pagination 7/7, real-service browser 9/9 with zero non-loopback outbound, pinned-SDK 16/16+12/12+6/6+7/7+12/12, requirements checks 4/4 (origin/main and c121c42, each with/without handoff)
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 04:27:04 +08:00
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
zonggongandmultica-agent 7f0a739213 HEL-879: refresh release-snapshot selftest (9/9) and budget-split (10/10) evidence at the exact final head 7f66015 so the recorded code_commit matches the code it verifies
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 03:37:07 +08:00
zonggongandmultica-agent 7f66015ac8 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
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 03:36:37 +08:00
zonggong 8afdabc7d1 HEL-879 assembly: merge the HEL-897-verified voice frontend (c61d9c9, hel-878-voice-wiring) - original author's six-file visual base byte-preserved with the late-cleanup/interleaved-start fix (761a2331) already independently re-verified 88/88 by the manager; no visual rework, no backend changes on this side 2026-10-27 03:27:07 +08:00
zonggong 48252b1dcd HEL-879 assembly: merge the audited HEL-877 voice backend (aba2aeb) onto the HEL-879 release-closure mainline - backend-only voice module (session/token router, chat_api_client, llm_chat_api, receipt, stt/tts adapters, worker) with zero new dependencies, provenance-preserved, e978e87 fully contained in both parents; web/ untouched by this side 2026-10-27 03:27:03 +08:00
zonggongandmultica-agent 01ff919859 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
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 03:26:30 +08:00
leeferandmultica-agent c61d9c95c9 HEL-897: record bytewise preservation and independent regression checks
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 02:52:42 +08:00
leeferandmultica-agent 761a2331f5 HEL-897: preserve visual author voice session race fixes
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 02:52:08 +08:00
leeferandmultica-agent f259bd34e1 HEL-878: retain requirement sources and record independent review
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 02:39:05 +08:00
leeferandmultica-agent b95ce798b4 HEL-878: preserve original author continuous voice wiring
Co-authored-by: multica-agent <github@multica.ai>
2026-10-27 02:35:37 +08:00
施工员2号 aba2aebeb0 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
Co-authored-by: multica-agent <github@multica.ai>

## HEL-877 交付摘要(R5 后台施工,未部署未合并)

- 新独立模块 src/cybergril/voice/:专属 livekit worker(agent_name=cybergril-voice-agent,复用 .12 FunASR/CosyVoice/silero 底座,不 fork 不另搭平台,无第二人格/记忆,不直连 LLM);llm_chat_api 适配器与文字前端 real.ts 同协议走 POST /api/chat(epoch 409 有界重同步、SSE 分片、turn_id 幂等、打断即断流);/api/voice/session 建房+专属派发+短时限 token 全服务端(密钥不进浏览器;缺配置/无档案/TTS 停机诚实拒绝;capabilities.voice 不伪造);receipt 仅在 SDK 真实播放完成后确认(打断/失败/φ 轮/旧 epoch 全 fail-closed)。
- 部署草案:/opt/voice-agent-cybergril 目录、worker 健康端口 8082、HTTP 8360 + HTTPS 8361 同进程双入口(deploy/voice/serve_dual_8360_8361.py,共用同一状态/锁)、启动顺序含 TTS GPU≥6GB 前置与回滚;本卡未执行任何启停。契约(开关/字幕/失败/打断/挂断 + CG-011 主动开口接缝)见 docs/runbook-hel877-voice.md §3。
- 自测(零真实请求):281 pytest 全过(含语音 25 例:socket 级打断断流、SSE 边界、重放零调用、文字/语音同门互打断、回执全负例、token 房间隔离);scripts/verify_hel877_voice_chain.py 8/8;.12 agent venv(livekit-agents 1.8.8)真 SDK 检查 8/8(stdin-only 零写入零网络);双端口同进程本地冒烟通过;ruff 干净;需求检查双目标通过(含 handoff-HEL-877.json)。
- 施工中总工 HEL-874(e978e87)推上同分支,本卡已完整并入,不覆盖并行成果。未验证如实列明:端到端语音(TTS 停机/GPU 3.9GB<5.6GB)、真 LiveKit server 的 dispatch/delete、移动端实机——待接线/审核卡真机验收,未经真实验收不称 R5 完成。
2026-10-26 22:42:19 +08:00
施工员2号 a324fd57b7 Merge remote-tracking branch 'origin/hel-870-assembly' into agent/2/6a08a4c18f23 2026-10-26 22:39:06 +08:00
施工员2号andmultica-agent 23415fbcd0 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
Co-authored-by: multica-agent <github@multica.ai>
2026-10-26 21:42:41 +08:00
463 changed files with 41897 additions and 281 deletions
+2
View File
@@ -1,2 +1,4 @@
__pycache__/
*.pyc
web/node_modules
.venv311/
+5
View File
@@ -23,6 +23,9 @@ tiers:
api_key_env: null
timeout: 60.0
temperature: 0.85
# HEL-908/909:该端点会把隐式开头思考(无 <think>、模板预填)混入 content,
# 只能整轮缓冲后分离;首段可见文本因此等整轮生成完成(见 reasoning.py)。
reasoning: buffer_turn
# ---- 中档:可以指向同一个模型,但配更短的上下文预算 ----
# 省钱的杠杆通常不是换更小的模型,而是少喂上下文。
@@ -33,6 +36,7 @@ tiers:
api_key_env: null
timeout: 40.0
temperature: 0.7
reasoning: buffer_turn
# ---- 本地小模型:后台任务的主力 ----
# 没有本地服务时,把它指向 openai_compat 的远端小模型即可。
@@ -43,6 +47,7 @@ tiers:
api_key_env: null
timeout: 20.0
temperature: 0.4
reasoning: buffer_turn
# ---- 场景 → 档位 ----
# 改这里就能调成本结构,不用改代码。
+42
View File
@@ -0,0 +1,42 @@
# CyberGril 语音会话端点脱敏配置示例(HEL-877,R5 后台施工)
#
# 复制为 config/voice.yaml(或用 CYBERGRIL_VOICE_CONFIG 指定路径)后由总管
# 按部署值填写;api_key/api_secret 只写服务器本地文件,不进仓库、不进浏览器、
# 不进任何日志。环境变量 CYBERGRIL_VOICE_* 可覆盖同名字段。
#
# 没有这份文件 = 本实例未配置语音:POST /api/voice/session 如实 503 拒绝,
# /api/health 的 capabilities.voice 保持 false,不伪造已接通。
# LiveKit SFU(.12 已有 docker voice-livekit;浏览器接入地址)
# 注意:7880 是纯 ws 且被通用 worker/8090 共享,不得改 TLS。页面走 HTTPS 时
# 浏览器连 ws:// 属混合内容会被拦——R5 方案是独立的 TLS 桥(拟议,未部署):
# deploy/voice/wss_bridge.example.py,livekit_url 用 wss://192.168.200.12:8363
# (详见 docs/rework/hel879/unified-review/r5-test-package.md §3)。
livekit_url: ws://192.168.200.12:7880
# 服务端 Twirp 调用地址(本机回环即可)
livekit_internal: http://127.0.0.1:7880
# LiveKit API 凭据(与 .12 /opt/voice-agent/.env 中 LIVEKIT_KEYS 同源;此处只放占位符)
api_key: <LIVEKIT_API_KEY_PLACEHOLDER>
api_secret: <LIVEKIT_API_SECRET_PLACEHOLDER>
# 专属 worker 的 agent_name:与通用 cascade-voice-agent 区分派发,互不争抢
agent_name: cybergril-voice-agent
# CosyVoice TTS 健康探测(TTS 不在就诚实拒绝开通话,不假装能说话)
tts_health_url: http://127.0.0.1:9880/health
# ---- 自定义音色(HEL-899)----
# .12 语音面板(8090,https 自签):音色列表/zero-shot 登记的唯一写入者,
# CyberGril 统一 API 服务端代理调用;浏览器永不直连,凭据不外发。
panel_url: https://127.0.0.1:8090
# CosyVoice 合成入口(试听现合成;/health 的热加载名单用于通话开启时校验)
tts_url: http://127.0.0.1:9880
# 共享 voices.json 只读回看(补全 ASR 自动转写的参考文本,供备份清单);不配则留空
registry_file: /opt/voice-agent/cosyvoice/voices.json
# CyberGril 专属 worker 的音色状态文件(worker 会话入口读一次;通话中改选最迟下一通话生效)
worker_state_file: /opt/voice-agent-cybergril/voice_state_cybergril.json
# 浏览器接入 token 有效期(秒):只够加入房间,短时限
token_ttl_seconds: 300
# 会话登记有效期(秒):房间早空/挂断后登记自然过期
session_ttl_seconds: 7200
+9
View File
@@ -20,6 +20,10 @@ mem0 同进程导入的准确缺项与不下载解决路径、空账初始化、
## 2. 安装命令(发布卡执行)
> **已被取代(HEL-879,2026-10-27)**:本节安装命令缺 `--offline`/`--no-index`
> 强制项。唯一照抄入口改为 `deploy/hel895-release-candidate.md` §2。本文件其余
> 内容作为历史交付证据保留原样。
```bash
# 全新目录,逐条照抄;任何一步失败即停
# 2.1 服务账号与目录(独立可写数据路径,权限边界明确)
@@ -63,6 +67,11 @@ Mem0 提取都会在发送前被硬闸拒绝(`budget_*_exhausted`)——这
## 4. 真实语义激活前置(B 部分批准后;准确缺项与不下载路径)
> **已被取代(HEL-879,2026-10-27)**:本节 4.1/4.2 按索引拉 `mem0ai==2.2.1`
> 同名 wheel,不能保证固定源码身份 2c81af3;固定 SDK 激活的唯一照抄入口改为
> `deploy/hel895-release-candidate.md` §4(固定 tarball 哈希 → 固定源码构建轮 →
> cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import)。以下保留为历史证据。
**当前事实(HEL-882 离线核实,tools/check_release_offline_hel882.py R6):**
固定安装 hel857-venv 是 Python 3.10.12,产品发布 venv 是 3.11.16,两者
site-packages 不能混用;本机 uv/pip 缓存里没有 mem0ai/fastembed/qdrant-client
+136 -41
View File
@@ -1,5 +1,17 @@
# HEL-895 · 统一发布候选(离线核验过,未执行部署)
> **HEL-879 发布工具收口(2026-10-27,隔离候选分支 `hel-879-assembly`)**:按总管
> 对 HEL-896 的独立复核完成四项有限收口——①归档一致性:checkpoint 返回值必查、
> 源/快照行数不一致或 checkpoint busy 一律 CLI 非 0 且不形成可交付包,verify 拒绝
> 记录了 `rows_match=False` 的旧包(§5);②目录内真实 SQLite(history_root 的
> `mem0_public_history.db` 等)停服口径走 checkpoint+逐表核对,在线模式只允许显式
> 单库 backup API、含 SQLite 的目录直接拒绝;③提前封账:`tools/seal_budget_hel879.py`
> 显式封账(真实用量保留、跨重启/归档拒绝,负例 S6);④安装入口唯一化:本文件 §2/§4
> 为唯一照抄入口(全 offline/no-index;mem0ai 只认固定源码轮),`deploy/hel874-release.md`
> §2/§4 已标注被取代。产物清单与精确提交见 `docs/rework/hel879/sdk-audit/` 与
> `docs/rework/hel879/release-artifacts.json`。**不要用旧父提交(04eff61/152479b)
> 的说明冒充本候选 head:以交付评论与 handoff JSON 记录的精确提交为准。**
> **HEL-896 修订(2026-10-27,正常追加 PR#11)**:按总管独立复审补齐——①浏览器
> 「真实服务 7/7」中 B3 原为已证实误报,测试已修为精确交互+严格断言(9 项检查,
> 含权限 API 调用计数与非回环出站守卫,证据 `docs/rework/hel896/browser/`);
@@ -16,6 +28,16 @@ HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选
## 0. 精确 head 与整合方式
> **HEL-879 返修勘误(2026-10-27)**:本节以下原文记录的是 **HEL-895/896 历史
> 候选**(分支 `hel-870-assembly`,PR #11 未合并),其 299 pytest / 55 vitest 等
> 验证数字属于那个历史 head,**不是当前发布候选**。当前统一发布候选是分支
> `hel-879-assembly`(在 7f0a739 之后正常追加返修提交,不强推、不并入 PR #11),
> 其精确 head、pytest/vitest/build 全量数字与证据以**交付评论与
> `docs/requirements/handoff-HEL-879.json` 记录为准**;本文件的安装/检查入口
> (§2/§4/§5/§6)继续适用于当前候选,产物清单见
> `docs/rework/hel879/sdk-audit/` 与 `docs/rework/hel879/release-artifacts.json`
> (`rework_head` 字段)。历史叙述保留原样,不冒充、不改写。
- **候选 head**:见交付评论(分支 `hel-870-assembly`,PR #11 的正常追加提交,
未强推、未合并)。第一父为 HEL-894 通过复审的 `152479b3503c3d1ab27681170e6bfd592c54380a`,
合并入 HEL-881 已审核的 `bf234570c0b6595a72f8dbc79e42c63685e08e01`
@@ -51,13 +73,36 @@ HEL-881 已审核的主动留言前端(`bf234570`)装成一个统一候选
| 预算执行包 | `docs/budget-package-hel895.md`(含 §6 HEL-896 分账修订) | A/B/C 一次批准核定稿,默认 0 |
| HEL-896 工具 | `tools/verify_budget_split_hel896.py`、`tools/audit_sdk_offline_hel896.py`、`tools/release_snapshot_hel896.py`、修订版 `web/tests/hel895_real.mjs` | 分账切换验证、SDK 余缺只读审计、一致性快照/回退验证、严格浏览器检查 |
## 2. 安装命令(发布卡执行)
## 2. 安装命令(发布卡执行;本节是唯一照抄入口——HEL-879 收口)
与 `deploy/hel874-release.md` §2 完全相同(服务账号 → 全新空目录 clone 本 head →
`uv venv --python 3.11` + `uv pip install -r requirements.lock` → `npm ci &&
npm run build` → 安装 service → `curl /api/health`),此处不重复抄写;唯一差别是
`git checkout <本文件 §0 的精确 head>`。发布前必过检查同样按 `deploy/hel874-release.md`
§6 执行,另加本卡两个新工具(真实 HTTP 分页合同、真实服务浏览器)在目标机重跑。
```bash
# 全新目录,逐条照抄;任何一步失败即停。全部安装强制 offline/no-index:
# 缓存命中即装,缓存缺失即停并留证;联网补齐只发生在获批的发布窗口内,
# 逐项 sha256 留证(seq48–51 勘误教训:不写 offline 的安装发生过网络获取)。
# 2.1 服务账号与目录(独立可写数据路径,权限边界明确)
useradd --system --home-dir /opt/cybergril-hel874 --shell /usr/sbin/nologin cybergril || true
mkdir -p /opt/cybergril-hel874/data # data 必须为空:新账从零初始化
chown -R cybergril:cybergril /opt/cybergril-hel874
# 2.2 代码 checkout:全新空目录 clone(空目录里没有仓库,不能直接 git fetch)
git clone ssh://git@192.168.200.36:222/leefer/CyberGril.git /opt/cybergril-hel874/current
git -C /opt/cybergril-hel874/current checkout <交付评论/handoff JSON 记录的精确head>
# 2.3 依赖与前端(uv/npm 一律 offline;缓存缺失即停)
cd /opt/cybergril-hel874/current
uv venv --python 3.11 .venv311
uv pip install --python .venv311/bin/python --offline -r requirements.lock
(cd web && npm ci --offline && npm run build)
# 2.4 服务单元(单元内已带 PYTHONPATH=<current>/src:requirements.lock 只装依赖,
# 产品代码经 PYTHONPATH 进程内可 import)
cp deploy/cybergril-hel874-8360.service.example /etc/systemd/system/cybergril-hel874.service
systemctl daemon-reload && systemctl enable --now cybergril-hel874
curl -s http://127.0.0.1:8360/api/health | python3 -m json.tool
```
发布前必过检查按 §6 与 `deploy/hel874-release.md` §6 的清单执行(该节仍有效),
另加本卡工具(真实 HTTP 分页合同、真实服务浏览器)在目标机重跑。
## 3. 健康能力矩阵(/api/health 预期)
@@ -68,38 +113,70 @@ weather/materials/backup=false。**主动留言前端已并入本候选:生活
真实 reach-out 后,打开的页面每 20 秒读一次新行、回来立即读,未读有标记;该行为
已在本 head 用真实服务验证(§0)。**
## 4. 固定 SDK(3.11)准确缺项与不下载路径
## 4. 固定 SDK(3.11)激活——唯一照抄入口(HEL-879 收口)
**HEL-896 只读审计修订(2026-10-27,`tools/audit_sdk_offline_hel896.py`,证据
`docs/rework/hel896/sdk-audit/`)**:此前「只缺 mem0ai/qdrant-client/fastembed 三个
cp311 wheel 及纯 Python 传递依赖」的概括**不准确**,勘误如下:
**本节取代 `deploy/hel874-release.md` §4(历史文件,已标注被取代,不得再照抄)。**
旧 4.1/4.2 按 `deploy/mem0-stack.lock` 的 `mem0ai==2.2.1` 向索引拉同名 wheel——
**索引同名 wheel 不能证明固定源码身份 2c81af3**,这条路已废除。入口只有一条:
- 三个顶层包固定版本(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1)的
WHEEL 均为 `py3-none-any` 纯 Python,本身无 cp311 专用轮一说;**mem0ai 的安装来源
是本地固定源码**(hel857-src,`deploy/mem0-stack.lock` 源码提交 2c81af3、tarball
SHA256 `9dcef930…dda23` 已在本机 hel857-data 实测一致)——从索引下载同名 2.2.1
wheel 不证明与固定源码相同,发布必须用固定 tarball/源码离线构建安装。
- 按 3.11 Linux markers + extras 从 3.10 固定栈安装元数据求闭包共 **55 项,产品
3.11 venv 缺 41 项**(28 纯 Python + 13 原生扩展:grpcio/numpy/onnxruntime/
tokenizers/sqlalchemy/protobuf/greenlet/pillow 等)。这是**依赖闭包清单**,不是
已完成的 cp311 解析或下载结果。
- 当前本机缓存对 41 项缺项**立即可复用**的只有 3 项(greenlet 3.5.6 cp311、
pillow 12.3.0 cp311、truststore 0.10.5 py3-none-any,uv wheels-v6/v7 缓存实测);
其余 37 项 + mem0ai 固定源码离线构建所需 build backend(hatchling 未在缓存)由
发布卡获授权后按 `deploy/hel874-release.md` §4 拉取,逐项 sha256 留证,下载清单
保持 `deploy/mem0-stack.lock` 与固定源码身份;缺项不阻塞其余发布工作。
- **fastembed 模型缓存实测**:hel857-data/fastembed-cache 含真实模型
`Qdrant/bge-small-zh-v1.5` 与 `Qdrant/bm25`(约 92MB);发布机落位=实例数据目录
内该缓存的拷贝,真实 mem0 模式的 cache_dir 指向它;本卡零下载、不推理。
- **seq48–51 pip 事件勘误(缓存取证)**:HEL-895 运行中去掉 `--offline` 安装的
pip 26.2.2 在 uv 缓存的 wheel 条目创建于 2026-10-27 02:10:04(该运行窗口内,
来源 files.pythonhosted.org,且为窗口内唯一新增缓存条目)——该步发生了网络
获取,前卡「零下载」对此步不成立;requirements.lock 装机同期未产生新缓存条目
(全部命中既有缓存)。字节级完整下载与条件请求无法由缓存取证区分,按保守口径
记为下载。**后续一切安装强制 `--offline`/`--no-index`。**
**固定 tarball 哈希 → 固定源码构建轮 → 目标 cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import。**
- **第 0 步 · 产物清单核对**:`docs/rework/hel879/sdk-audit/sdk-audit.json`(本候选
精确提交上重跑)与 `docs/rework/hel879/release-artifacts.json` 记录闭包 55 项、
缺 41 项、缓存可复用 3 项与逐项哈希;发布卡先核对清单与目标提交一致。
事实口径(HEL-896 审计核定,HEL-879 重跑仍成立):三个顶层固定版本
(mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1)的 wheel 均为
`py3-none-any` 纯 Python;闭包按 3.11 Linux markers+extras 共 **55 项,产品
3.11 venv 缺 41 项**(28 纯 Python + 13 原生:grpcio/numpy/onnxruntime/
tokenizers/sqlalchemy/protobuf 等),这是闭包清单不是已完成的 cp311 下载结果。
seq48–51 pip 事件勘误保留:不写 offline 的安装发生过网络获取(uv 缓存取证),
故本入口全程 `--offline`/`--no-index`。
- **第 1 步 · 固定 tarball 哈希核对**(零下载):
```bash
sha256sum <hel857-data>/mem0-2c81af3.tar.gz
# 必须等于 deploy/mem0-stack.lock 记录的
# 9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23
```
不一致即停;不得改用索引 `mem0ai==2.2.1` wheel 冒充固定源(audit 已证明两者
身份不可互证)。
- **第 2 步 · 固定源码构建轮(HEL-879 返修:已完成可离线构建)**:此前此处写
「hatchling 当前不在任何本机缓存、待补」——经总管只读核查与本卡复核**勘误**:
uv wheels-v6/wheels-v7 均有 hatchling **1.32.4** 缓存(`release-artifacts.json`
`build_backend` 节逐路径留证)。固定源 tarball 已在 3.11.16 + `uv build --wheel
--offline` 下**离线构建出轮**(零下载、零安装、不混 3.10 site-packages):
`mem0ai-2.2.1-py3-none-any.whl`,
sha256 `698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db`。
发布卡在目标机重跑同一命令并核对该哈希:
```bash
tar xzf <hel857-data>/mem0-2c81af3.tar.gz && cd mem0-2c81af3*/
uv build --wheel --offline --python 3.11 -o wheels/
sha256sum wheels/mem0ai-2.2.1-py3-none-any.whl
```
- **第 3 步 · 目标 cp311 依赖准备**:41 项缺项中**缓存今天即可复用 3 项**
(greenlet 3.5.6 cp311、pillow 12.3.0 cp311、truststore 0.10.5 py3-none-any,
uv wheels-v6/v7 缓存实测,HEL-879 重跑核实仍命中);其余 37 项
**由总管获批后发布窗口拉取**(`python3 -m pip download --only-binary :all:
--implementation cp --python-version 3.11 --platform manylinux2014_x86_64
--abi cp311 -r <缺项清单> -d wheels/`),逐项 sha256 留证;本卡零下载、零联网。
某固定版本若无 cp311 wheel,按 lock 注释如实上报缺项,不换版本号、不混 sdist。
- **第 4 步 · 仅本地轮安装**(--no-index 全程,目录里只有上两步产物):
```bash
uv pip install --python .venv311/bin/python --no-index \
--find-links <本地wheels目录> mem0ai==2.2.1 qdrant-client==1.20.0 fastembed==0.9.1 <41缺项>
```
`deploy/mem0-stack.lock` 的 mem0ai 行只作版本记录;安装来源必须是第 2 步的
固定源码轮(同版本号不同来源不互认)。
- **第 5 步 · 同进程 co-import 与模型落位**:
```bash
.venv311/bin/python -c "import cybergril.api, mem0, qdrant_client, fastembed; print('co-import ok')"
```
fastembed 模型缓存(hel857-data/fastembed-cache,Qdrant/bge-small-zh-v1.5 +
bm25 约 92MB,实测存在)拷入实例数据目录,真实 mem0 模式 cache_dir 指向它。
产品 venv 目前 `import mem0/qdrant_client/fastembed` 均缺、产品代码导入不受影响;
在 wheel 落位且 co-import 通过前,不得以 stub/FTS 冒充真实 Mem0 体验就绪。
真缺的下载(37 项运行闭包;hatchling 已勘误为缓存命中并完成离线构建,不在缺单)
仍待主卡授权,本卡不为补命令联网。
## 5. 停用与保留数据回退
@@ -108,26 +185,44 @@ cp311 wheel 及纯 Python 传递依赖」的概括**不准确**,勘误如下
`systemctl stop` → 数据目录原样保留 → 切回上一 release/上一提交(R4 迁移全部
增量新表,旧代码可安全打开同一数据库);彻底撤销留数据档不删。
**HEL-896 归档/回退修订(2026-10-27)**:此前「SQLite WAL 单文件可整体复制/归档」
的口径**不安全**——在线库只复制主 `.db` 会丢 WAL 里已提交的资料(总管复现件连
`companion` 表都不在;本卡 `tools/release_snapshot_hel896.py` 自测同样复现
`no such table: companion`,证据 `docs/rework/hel896/release-snapshot/selftest.json`)。
发布归档一律二选一:**停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档**
(推荐),或**在线用 SQLite backup API 逐库一致性快照**。照抄命令:
**HEL-896 归档/回退修订 + HEL-879 收口(2026-10-27,v3 返修)**:在线库只复制主
`.db` 会丢 WAL 里已提交的资料(总管复现件连 `companion` 表都不在)。发布归档一律
二选一:**停服务 + `PRAGMA wal_checkpoint(TRUNCATE)` 后整目录归档**(推荐),或
**在线用 SQLite backup API 对显式单库做一致性快照**。工具已按收口语义强制执行
(v3;v2 的两处已证缺陷由总管独立复核提出并修复):
- checkpoint 返回 busy≠0 或源/快照行数不一致 → CLI 非 0、不产生可交付归档
(「先停服」是前提,不是忽略失败结果的理由);
- 目录内真实 SQLite(history_root 的 `mem0_public_history.db`、向量目录内库等,
按文件头魔数递归识别)在停服口径一并 checkpoint+逐表核对;
- 在线模式只接受显式 `--db`(backup API);传入含 SQLite 的 `--file` 目录直接拒绝;
- `verify` 要求**三个计数直接相等**(manifest 源行数 = manifest 快照行数 = 恢复
实测;`rows_match` 只是额外约束——v2 的 `rows_match is True or …` 允许记录值
绕过源计数比较,源 2/快照 1 仍 ok,已废除);回退演练按源路径映射**实际恢复**
到归档旁的持久独立目标树、打开恢复库核对行数与哈希,输出 rollback_plan;
- 归档侧目标名无冲突映射:不同目录的同名 `ledger.db` 不再互相覆盖(v2 后写覆盖
前写仍先交成功包);同一源重复传入直接拒绝。
照抄命令:
```bash
# 覆盖范围:产品库(CYBERGRIL_DB)、语义向量目录 vector_root、推导史 history_root、
# 权威嵌入账 ledger_path(按部署实例 config/semantic.yaml 实际路径逐项代入)
# 停服口径(推荐;目录内 SQLite 一并一致性处理):
.venv311/bin/python tools/release_snapshot_hel896.py snapshot \
--name <release-或阶段名> --db <产品库> [--db <权威账>] \
--file <vector_root> --file <history_root> --out /opt/<实例>/archives/
# 在线兜底口径:仅显式单库(backup API),不得传含 SQLite 的目录:
.venv311/bin/python tools/release_snapshot_hel896.py snapshot --name <名> \
--db <产品库> --out /opt/<实例>/archives/ --online
.venv311/bin/python tools/release_snapshot_hel896.py verify --archive <产物>.tar.gz
```
产物含逐文件 SHA256 清单、**代码精确提交**(快照时刻 `git rev-parse HEAD`)、
逐表行数核对与回退演练结果;不删档,旧归档永久保留。回退=停服务 → 解包快照到
新数据目录 → 指回上一 release/上一提交 → 起 service → `verify` 通过后交付。
本节是**发布归档/回退要求**,不冒充 R6(CG-009 每日自动备份/手动完整导出)完成。
新数据目录(按 rollback_plan 的源路径映射)→ 指回上一 release/上一提交 → 起
service → `verify` 通过后交付。本节是**发布归档/回退要求**,不冒充 R6(CG-009
每日自动备份/手动完整导出)完成。
## 6. 本候选不做的事
+393
View File
@@ -0,0 +1,393 @@
# HEL-913 · 统一体验运行包(固定版本,未部署、cap=0)
> 本目录是把已通过 HEL-912 审核的统一候选装成的**可交接运行包**:精确源码
> 版本、产物与哈希、脱敏配置模板、唯一启动方式、独立空资料空间、停止/
> 备份/保留资料回退说明、一次零真实调用的运行准备验证记录、真实记忆的
> 完整可审安装清单,以及语音窗口的唯一布防接线。**本卡未部署任何现网
> 服务、未监听 836x/8082/9880/8090、未改任何现有实例;实际启用与新用量
> 由总管统一向白栖知说明并获批准后再执行。**
## 0. 精确版本:产品基线 vs 运行包提交(不要混同)
- **已审产品基线**:`agent/2/8e99cc2cc592` @ `4a38106eef21c43ba24ab7d07992b1bfe539137d`
(HEL-911 接缝修复;HEL-912 审核「通过,无必须修复项」)。父线:
`407fc81`(HEL-910 窗口记录)← `958c696`(HEL-908 R1/R2 返工)。
**产品代码(src/web/config/tools/tests)以这个提交为准。**
- **运行包提交**:同一分支上、`4a38106` 之后的追加提交——只新增/修改
`deploy/hel913-unified/` 与 `docs/` 下文件,**不改任何产品代码**。交付
评论给出运行包精确 head;本文件自身就在该提交里,所以不能在文件内
写死自己的哈希。部署**照抄入口是运行包提交**(本目录随它一起走),
只 checkout `4a38106` 会没有本目录——这是收口修正点,别再踩。
- **部署前自校验**(两条都必须通过):
```bash
git merge-base --is-ancestor 4a38106eef21c43ba24ab7d07992b1bfe539137d HEAD && echo ok
git diff --stat 4a38106eef21c43ba24ab7d07992b1bfe539137d HEAD -- src web config tools tests # 必须为空
```
- 需求基准 v1.12(`docs/requirements/`),需求检查见 `handoff-HEL-913.json`。
## 1. 交付物清单(本目录)
| 文件 | 作用 |
| --- | --- |
| `README.md` | 本运行说明(唯一照抄入口;不跳旧 HEL-895 通用步骤) |
| `manifest.json` | 机器可读清单:产品基线、dist 哈希、环境引用、边界 |
| `dist-manifest.json` | web dist 8 文件 SHA256(复用 HEL-910 实测构建) |
| `models.template.yaml` | 脱敏模型/预算配置(**一切预算=0**;占位符标注待批写入位置) |
| `voice.template.yaml` | 脱敏语音配置(livekit_url 必须是 wss 桥地址,见 §5) |
| `worker.env.template` | 语音 worker 环境模板(含 ASR 窗口闸三项,默认未布防=拒绝一切 ASR) |
| `voice-window-arm.template.env` | 语音窗口一次性布防参数模板(默认全 0/未布防) |
| `api_ctl.sh` / `worker_ctl.sh` | 统一实例 API(单进程 HTTP+HTTPS)与语音 worker 启停(身份核验版) |
| `bridge_ctl.template.sh` | wss TLS 桥启停(获批语音窗口用;浏览器侧语音的前提) |
| `tts_ctl.template.sh` | 共享 TTS 9880 窗口内临时恢复/停回(只停自己起的 PID) |
| `force_end.template.sh` | 窗口到期强制收尾(`now` 立即 / `watch <epoch>` 到点;封窗停服) |
| `verify_offline.py` | 零真实调用运行准备验证(34 项;TestClient 进程内,不监听端口) |
| `test_ctl_scripts.sh` | 启停脚本离线替身检查(42 项;stub,零真实服务) |
| `mem0-cp311-wheels.txt` | 真实 Mem0 记忆栈 cp311 缺项轮清单(40 项,待批一次性下载) |
运行记录:`docs/rework/hel913/verify-offline-run.txt`(.12 产品 3.11 venv
实跑 34/34)、`docs/rework/hel913/ctl-checks-run.txt`(收口轮启停检查 42/42)。
## 2. 版本固定与产物复用
- **源码**:产品基线 `4a38106`(含 `requirements.lock` 19 项固定版本,
Python 3.11 实测)+ 运行包提交(仅本目录与 docs)。
- **web dist 复用**:`git diff 958c696 4a38106 -- web/` 为空(网页源码零变动),
直接复用 HEL-910 已实测部署的 dist(.12 `/opt/cybergril-hel910/current/web/dist`,
REVISION 标记 `958c6962…`),只读拷贝,不安装 node、不重建、不跳任何会
`npm install` 或创建旧端口实例的旧发布步骤(如 `deploy/hel895-release-candidate.md`
的通用总装流程——那不是本包的入口)。8 文件哈希见 `dist-manifest.json`
(2026-10-29 于 .12 逐文件实测),部署时核对一致即可。
- **解释器(既有环境引用,不新建、不增改)**:
- 产品 API:`.12 /opt/cybergril-hel879/current/.venv311`(Python 3.11.16,
requirements.lock 全量,fastapi 0.159.1 / uvicorn 0.54.0 / httpx 0.28.1 /
pydantic 2.14.1)。**只借用运行,绝不向它安装任何东西**(记忆栈装进
本实例自己的隔离 venv,见 §7)。
- 语音 worker:`.12 /opt/voice-agent/agent/venv`(livekit-agents 1.8.8,
含 silero VAD)。
- 证书:`.12 /opt/cybergril-voice-certs/cybergril-voice.{pem,key}`(既有
自签,覆盖 192.168.200.12;8361 与 wss 桥 8363 共用)。
- 本机 LiveKit `ws://127.0.0.1:7880`(常驻);TTS 9880 / 面板 8090 /
wss 桥 8363 只在获批窗口内启动(§5),未获批保持停机。
## 3. 目标实例布局(唯一布局;用户实例 data 必须为空)
```
/opt/cybergril-hel913/ # 实例根(名字可换;以下结构固定)
current/ # git checkout <运行包提交>(见 §0,不是 4a38106)
web/dist/ # 按 dist-manifest.json 核哈希后放入(只读复用件)
etc/
models.yaml # 由 models.template.yaml 复制,填入获批网关与预算
voice.yaml # 由 voice.template.yaml 复制,填入服务器本地值
semantic.yaml # (仅批准真实记忆时)由 config/semantic.example.yaml 复制,见 §7
data/ # 独立空资料空间:新库从零初始化
worker/.env # 由 worker.env.template 复制并填好(占位全清)
logs/
```
**不打包进她的正式档案**:任何历史测试库(8360 团队库、HEL-907/910 窗口库)、
聊天记录、LiveKit/TTS 凭据、自签证书私钥、agent venv 内实物。用户实例在用户
亲自创建前 `companion` 表必须为空(离线验证第 3 项复现该守门)。
## 4. 首次准备与启停(唯一照抄入口)
### 4.1 首次准备(获批部署窗口内执行一次;不真启动服务)
```bash
# 0) 取运行包提交(精确 head 见交付评论;下述校验两条都要过,见 §0)
git clone -b agent/2/8e99cc2cc592 ssh://git@192.168.200.36:222/leefer/CyberGril.git \
/opt/cybergril-hel913/current
cd /opt/cybergril-hel913/current
git checkout --detach <本轮交付评论的运行包完整提交号>
git merge-base --is-ancestor 4a38106eef21c43ba24ab7d07992b1bfe539137d HEAD
git diff --stat 4a38106eef21c43ba24ab7d07992b1bfe539137d HEAD -- src web config tools tests # 空
# 1) 目录骨架(固定布局)
mkdir -p /opt/cybergril-hel913/{etc,data,logs,worker}
# 2) 配置落位(占位符必须全部替换;预算值未经批准一律保持 0)
cp deploy/hel913-unified/models.template.yaml /opt/cybergril-hel913/etc/models.yaml
cp deploy/hel913-unified/voice.template.yaml /opt/cybergril-hel913/etc/voice.yaml
cp deploy/hel913-unified/worker.env.template /opt/cybergril-hel913/worker/.env
vi /opt/cybergril-hel913/etc/models.yaml /opt/cybergril-hel913/etc/voice.yaml /opt/cybergril-hel913/worker/.env
# 3) web dist 只读复用(核哈希后拷入;不装 node、不重建)
rsync -a /opt/cybergril-hel910/current/web/dist/ /opt/cybergril-hel913/current/web/dist/
python3 - <<'EOF'
import hashlib, json, pathlib
m = json.loads(pathlib.Path("deploy/hel913-unified/dist-manifest.json").read_text())
bad = [k for k, want in m.items()
if hashlib.sha256((pathlib.Path("web/dist")/k).read_bytes()).hexdigest() != want]
raise SystemExit(f"dist hash mismatch: {bad}" if bad else 0)
EOF
# 4) 未创建档案守门复验(进程内 TestClient,零监听零出站;34 项全过才继续)
PYTHONPATH=src /opt/cybergril-hel879/current/.venv311/bin/python \
deploy/hel913-unified/verify_offline.py --web web/dist
```
### 4.2 启动与停止(唯一方式;子命令只认 start|stop|status)
```bash
# API:单进程双入口(HTTP 8360 + HTTPS 8361 共用同一 app 状态;lifespan 只进一次)
cd /opt/cybergril-hel913/current
HEL913_BASE=/opt/cybergril-hel913 bash deploy/hel913-unified/api_ctl.sh start
# 健康检查:curl -s http://127.0.0.1:8360/api/health(version=R4-assembly,
# semantic_backend=stub;llm.ok=false 是诚实状态——cap=0 且未配网关)
# 语音 worker(语音窗口获批后;接线与前置见 §5)
HEL913_BASE=/opt/cybergril-hel913 bash deploy/hel913-unified/worker_ctl.sh start
# 停止顺序:先 worker 后 API(语音会话先释放,再关统一进程)
HEL913_BASE=/opt/cybergril-hel913 bash deploy/hel913-unified/worker_ctl.sh stop
HEL913_BASE=/opt/cybergril-hel913 bash deploy/hel913-unified/api_ctl.sh stop
```
启停脚本安全口径(收口版,`test_ctl_scripts.sh` 42 项离线替身验证):
非法子命令拒绝退出 2,绝不静默当 start;重复 start 拒绝(不丢既有 pid
句柄);stop/失败收尾只杀**身份核验通过**(cmdline 含本启动器/模块名且
cwd 是本实例目录)的进程,PID 被复用时拒杀并报告;启动健康/注册检查
不过时清掉自己刚起的进程、保留日志退出 1;`worker_ctl.sh` 启动前拒绝
含未填 `<...>` 占位或解析失败的 `.env`。
同一数据目录**绝不允许**两个写入实例并存(双入口由 `deploy/voice/serve_dual_8360_8361.py`
在同一进程内保证;另起独立 uvicorn 会触发 lifespan 重入 fail-closed)。
## 5. 语音窗口唯一接线(获批窗口才动;默认全 0/未布防)
浏览器从 HTTPS(8361) 页面连 LiveKit **必须走 wss**(混合内容策略),
`voice.yaml` 的 `livekit_url` 因此必须是 wss 桥地址(默认拟 `wss://192.168.200.12:8363`,
`bridge_ctl.template.sh` 启停,与 8361 同证书、回环转发 7880)——**不是
可选件**;服务器内部(开房/删房/worker 注册)仍走 `livekit_internal` 的
本机明文 7880。窗口内各上限的唯一接线:
| 上限 | 接线(唯一) | 性质 |
| --- | --- | --- |
| ASR 音频总量 / 每通重连 | `worker/.env` 三项 `CYBERGRIL_ASR_WINDOW_ID`、`CYBERGRIL_ASR_WINDOW_MAX_AUDIO_SECONDS`、`CYBERGRIL_ASR_MAX_CONNECT_RETRIES_PER_CALL`(空/0=拒绝一切 ASR;布防值见 `voice-window-arm.template.env`) | 程序硬闸,发送前拒绝(`src/cybergril/voice/asr_usage.py`,逐通落账) |
| 每通墙钟/房间寿命 | `voice.yaml` `session_ttl_seconds: 600` + token TTL 300;挂断即删房(失败留账重试) | 程序硬闸 + 到期失效 |
| TTS 段数 | `tts_ctl.template.sh` start 时记录 tts.log 起始字节,`grep -c "first chunk"` 对照窗口段数上限 | 监控口径(到顶即人工 force_end;程序不硬闸) |
| 窗口总时限 | `force_end.template.sh watch <deadline>` 到点强制收尾(DELETE 全部会话→worker→api→bridge→tts,各按专属句柄);更可靠的等价物是获批后装 systemd timer 调 `now` | 可执行停服,非口头承诺 |
窗口启动顺序:tts_ctl snapshot→start → bridge_ctl start → 改 `worker/.env`
布防三项并 worker_ctl start → api_ctl start(见 §4.2)→ force_end watch
挂上。窗口结束(到期/触线/人工):force_end now → 按 `seal_budget_hel879.py`
封账归档(**封账不解除**,旧三窗口余额也不结转进新窗)。
## 6. 备份与保留资料回退
- **发布/阶段归档**(本包口径,沿用 HEL-896 收口工具,一致性快照):
```bash
# 停服口径(推荐;目录内 SQLite 一并 checkpoint+逐表核对)
bash deploy/hel913-unified/api_ctl.sh stop
/opt/cybergril-hel913/venv311/bin/python tools/release_snapshot_hel896.py snapshot \
--name <阶段名> --db /opt/cybergril-hel913/data/companion.db \
--file /opt/cybergril-hel913/data --out /opt/cybergril-hel913/archives/
```
产物含逐文件 SHA256、代码精确提交、逐表行数与回退演练;旧归档不删。
- **回退**:停服 → 解包快照到新 data 目录(按 rollback_plan 源路径映射)→
`current` 切回上一运行包提交(`git checkout <旧head>`)→ 起服务 → `verify` 通过。
- **整实例撤销但保留资料**:`api_ctl.sh stop`,data/ 原样保留,重新启用 =
同一 data 目录再 start(epoch 递增、聊天记忆保留,离线验证第 11 组复现)。
- **CG-009 每日自动备份/用户手动完整导出(R6)尚未实现**,本节只是发布归档,
不得冒充 R6 完成。
## 7. 零真实调用运行准备验证(已执行,34/34;口径如实)
在 .12 产品 3.11 venv 对产品基线树实跑 `verify_offline.py`(进程内
TestClient,零监听、零出站——网关地址重写为 127.0.0.1:1 + provider.stream
出站哨兵双保险):
- 包完整性:Python 3.11.16、候选树 import、**4 个关键包版本打印**
(fastapi/uvicorn/httpx/pydantic——不是 19 项锁依赖的逐项核对)、记忆栈
四项缺置如实。
- 入口/创建守门:health 诚实(llm.ok=false)、空库 404 无伪造档案、预览
本地计算、创建 201、二次创建 409、底色落库与预览一致、persona 在 cap=0
下诚实 failed(无编造诠释)。
- 传输:SSE 对话在预算 0 下诚实 `chat.error`,零 provider 出站;失败轮如实
落库;同轮重试幂等拒绝。
- 日子与回忆:**空库上的**未分享生活服务端裁剪(told=1;未做正反种子对照,
不夸成实质权限全覆盖)、分页形状、未知回执/详情 404。
- 语音:status 诚实 voice=false;TTS 不可达时开房 503 拒绝(绝不建房);
未知会话 DELETE 404。
- 重开:epoch=2、身份/关系/聊天保留、无伪造回复。
- dist:GET / 出 index.html;8 文件哈希与 manifest 全符。
- worker 接线:agent venv(livekit-agents 1.8.8)下 `import cybergril.voice.worker`
通过(只读 import,未启动注册)。
**预算 0 下只能诚实拒绝的**:一切真实创建诠释/习惯、对话生成、生活事件
生成、判定/成长/gist、主动措辞、嵌入、语音开房。`/api/health` 的 200 不代表
模型已运行——`llm.ok=false` 才是诚实状态。
## 8. 真实记忆(Mem0)最少安装与接线 —— 完整可审清单(零安装零下载,待批)
### 8.1 现状(2026-10-29 只读实测,收口轮复核沿用)
- 服务器 .12 产品 `.venv311`(3.11.16)内 `mem0 / qdrant_client /
fastembed / onnxruntime` 四项均未安装;服务器全盘无 mem0 安装、无固定
tarball、无已建轮、无 fastembed 缓存、uv 缓存仅 28K。**没有另一个已
准备好的 3.11 兼容环境**;旧 3.10 `hel857-venv`(工作站)不能混入 3.11
进程。
- 工作站本地实物(未上服务器):固定源 tarball `mem0-2c81af3.tar.gz`
(sha256 `9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23`,
与 `deploy/mem0-stack.lock` 一致)、固定源已建轮
`mem0ai-2.2.1-py3-none-any.whl`(sha256
`698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db`)、
fastembed 模型缓存 92MB(Qdrant/bge-small-zh-v1.5 + bm25)、uv 缓存可
复用 3 轮(greenlet/pillow/truststore cp311)。
### 8.2 安装目标环境:本实例自己的隔离 venv(不是产品 .venv311)
**绝不向借用的 `/opt/cybergril-hel879/current/.venv311` 增改任何依赖。**
目标环境是 `/opt/cybergril-hel913/venv311`(服务器既有 python3.11 基座
新建,3.11.16),装两套东西:产品锁 19 项 + 记忆栈 41 项(40 缺轮 + 固定
源 mem0ai 轮)。装好后本实例 API 用它跑(`api_ctl.sh` 里
`HEL913_VENV=/opt/cybergril-hel913/venv311/bin/python`),与 hel879 实例
彻底隔离;`.venv311` 保持只读借用前的原样(部署后可直接改回借用口径,
两者兼容——同一 requirements.lock)。
### 8.3 一次性批准窗口内的完整命令(逐项哈希留证;任一步失败即停)
```bash
# W0 · 固定源身份核对(零下载;哈希不符立即停,禁用索引同名轮替代)
sha256sum <hel857-data>/mem0-2c81af3.tar.gz # 必须等于 9dcef930…2dda23
sha256sum <hel857-data>/mem0ai-2.2.1-py3-none-any.whl # 必须等于 698284b6…20e9db
# W1 · 获批下载窗口(工作站联网执行;cp311/manylinux2014_x86_64 定版)
mkdir -p wheels-product wheels-mem0
python3 -m pip download --only-binary :all: --implementation cp \
--python-version 3.11 --platform manylinux2014_x86_64 --abi cp311 \
-r requirements.lock -d wheels-product/ # 产品锁 19 项
python3 -m pip download --only-binary :all: --implementation cp \
--python-version 3.11 --platform manylinux2014_x86_64 --abi cp311 \
-c requirements.lock -r deploy/hel913-unified/mem0-cp311-wheels.txt -d wheels-mem0/ # 记忆栈 40 项
# 其中 greenlet/pillow/truststore 3 项可从工作站 uv 缓存离线复用
# (release-artifacts.json 记录),命中即从 wheels-mem0/ 删除同版本再放缓存件
cp <hel857-data>/mem0ai-2.2.1-py3-none-any.whl wheels-mem0/
( cd wheels-product && sha256sum * > ../wheels-product.sha256 )
( cd wheels-mem0 && sha256sum * > ../wheels-mem0.sha256 ) # 逐项留证入库
# W2 · 传输与落位(scp 到 .12;约 wheels 250–350MB + 模型缓存 92MB)
scp -r wheels-product wheels-mem0 wheels-product.sha256 wheels-mem0.sha256 <hel857-data>/fastembed-cache root@192.168.200.12:/opt/cybergril-hel913/
ssh root@192.168.200.12 'cd /opt/cybergril-hel913/wheels-product && sha256sum -c ../wheels-product.sha256 && \
cd ../wheels-mem0 && sha256sum -c ../wheels-mem0.sha256' # 传输后复核
# W3 · 隔离 venv 创建 + 仅本地轮安装(--no-index 全程,失败即停)
ssh root@192.168.200.12
/opt/cybergril-hel879/runtime/cpython-3.11.16/bin/python3.11 -m venv /opt/cybergril-hel913/venv311
/opt/cybergril-hel913/venv311/bin/python -m pip install --no-index \
--find-links /opt/cybergril-hel913/wheels-product -r /opt/cybergril-hel913/current/requirements.lock
/opt/cybergril-hel913/venv311/bin/python -m pip install --no-index \
--find-links /opt/cybergril-hel913/wheels-mem0 \
--find-links /opt/cybergril-hel913/wheels-product \
-c /opt/cybergril-hel913/current/requirements.lock \
mem0ai==2.2.1 qdrant-client==1.20.0 fastembed==0.9.1
# mem0ai 的安装来源必须是 W0 的固定源码轮(同版本号不同来源不互认)
# W4 · co-import 冒烟(同进程;失败即停,不降级)
cd /opt/cybergril-hel913/current
MEM0_TELEMETRY=False HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 PYTHONPATH=src /opt/cybergril-hel913/venv311/bin/python -c \
"import cybergril.api, mem0, qdrant_client, fastembed; print('co-import ok')"
# W5 · 模型缓存落位(真实本地嵌入零下载)
mkdir -p /opt/cybergril-hel913/data/sem
cp -a /opt/cybergril-hel913/fastembed-cache /opt/cybergril-hel913/data/sem/fastembed-cache
# api_ctl.sh 通过 FASTEMBED_CACHE_PATH 指向它,模型资源禁止联网获取。
# W6 · 清理暂存(轮文件传输件用毕即删;哈希清单留档)
rm -rf /opt/cybergril-hel913/wheels-product /opt/cybergril-hel913/wheels-mem0 /opt/cybergril-hel913/fastembed-cache
```
**空间/影响(.12 实测口径估)**:`venv311` 成品约 450–550MB(产品 19 项
约 120–150MB + 记忆栈约 330–400MB,参照 3.10 同栈实测 366MB)+ 传输暂存
约 350MB(W6 删除)+ 模型缓存 92MB(常驻 data/sem)。首次本地嵌入零下载
(缓存就位)。**当前不触发任何嵌入**:装完不批 B 预算时
`CYBERGRIL_SEMANTIC=stub`,长期检索走词面 FTS,不以 stub 冒充真实 Mem0。
### 8.4 记忆账本与配置接线(新空用户库续账,不搬测试聊天)
- `CYBERGRIL_SEMANTIC`(api_ctl.sh 的 `HEL913_SEMANTIC`):`stub`(默认,
全链可跑、非语义)→ 批准后 `mem0`(真实)。
- 配置:`cp config/semantic.example.yaml /opt/cybergril-hel913/etc/semantic.yaml`,
启动器显式传 `CYBERGRIL_SEMANTIC_CONFIG=$BASE/etc/semantic.yaml`(不再误读仓库默认路径)。
`MEM0_TELEMETRY=False`、`FASTEMBED_CACHE_PATH=$BASE/data/sem/fastembed-cache`、
`HF_HUB_OFFLINE=1`、`TRANSFORMERS_OFFLINE=1` 由启动器在 import 前设置。
填 `ledger_path=/opt/cybergril-hel913/data/embed_ledger.sqlite3`、
`vector_root/history_root=/opt/cybergril-hel913/data/sem/{vec,hist}`、
网关三键(与 models.yaml 同源)、`authority` 块。
- **权威旧账续账(498 已用 / 原批 500 / 冻结 2 不动)**:把 HEL-857 系列
唯一权威嵌入账文件复制为实例 `data/embed_ledger.sqlite3`(只复制账本,
**不搬任何测试聊天/向量**——聊天在旧测试库,向量在新库从零生成);
`authority` 按 `config/semantic.example.yaml` 算式填
`prefix_rows: 181`、`prefix_sha256: <权威账前 181 行逐行摘要,总管按账现值填>`、
`min_used: 498`。批准 B 嵌入 180 条后写累计口径
`embed_cap: 680`、`embed_reserve: 2`(可用 = 680−498−2 = 180;**禁止**
把 180 直接填成 cap、禁止动冻结 2 条;缺/错 authority 在构造前拒绝,
指向新空文件重开余额同样被拒——`src/cybergril/memory/semantic.py`
`verify_ledger_authority`)。
- **不把旧实验余额当新用量**:39/60(HEL-857 真实请求旧账)与旧三窗口
余额一律不结转;用户实例的 LLM/请求账从 0 起记(自己的 SQLite 日账)。
- **副作用如实分开**:真实 embed(写入向量库+权威账)、SDK 内部提取 LLM
(`llm_ops` 独立 cap + 统一请求闸 "semantic" 类)、co-import 冒烟(零
调用,只 import)——三者互不冒充;当前阶段三者全部 0。
## 9. 本包不做的事
不部署、不监听现网端口、不恢复 9880/8090/836x/8082、不触 4080、不下载任何
依赖/模型/浏览器、不改模型/认证/运行时绑定、不合并 PR、不替换正式界面、
8300/8310/8320/8350 与既有资料不动、8340/ComfyUI 保持停机、不向
`/opt/cybergril-hel879/current/.venv311` 增改依赖。旧三次测试窗口余额不
结转;一切提额、安装、部署都是待批准建议。
## 10. 总管独立取件勘误与最终执行约束(2026-10-29)
本节优先于上文笼统模板描述,当前仍全部待批,零真实调用。
- 运行包基线8ea5fdc之后仅机械收口:补语义配置路径/缓存/禁遥测离线变量;
force_end按真实持久化sessions列表取session_id;补桥/TTS非法命令拒绝和失败收尾;
传输hash清单与目标工作目录、既有3.11解释器路径纠正;每通TTL改600秒与10分钟一致。
- 40项记忆缺轮与产品19项锁是现有版本清单。下载使用两份requirements合并约束,
不得让第二次下载/安装解析器静默换掉产品锁版本:W1增加`-c requirements.lock`,
W3记忆安装同时传`--find-links .../wheels-product -c .../requirements.lock`。
解析冲突/缺cp311轮即停并留首错,不自动换版本、不引入新模型。
- B记忆日顶明确为embed_daily=60、llm_daily=40(SDK提取另受累计llm_cap=60),
authority/累计embed_cap=680、reserve=2原样。语义日账按UTC(北京时间08:00)
切日,产品对话/生活日账显式TZ=Asia/Shanghai按北京时间00:00切日,不能混说。
- 不运行测试脚本里的失管sleep替身作为长期任务。未来实际截止保护用获批后的
systemd一次性定时服务(服务/日志/停机句柄可查);不能把本轮本地watch留到结束后。
部署卡执行`systemd-run --unit=cybergril-hel913-expiry --on-calendar="<明确UTC截止时间>" \
/usr/bin/env HEL913_BASE=/opt/cybergril-hel913 /usr/bin/bash \
/opt/cybergril-hel913/current/deploy/hel913-unified/force_end.template.sh now`,
放行前必须核`systemctl list-timers cybergril-hel913-expiry.timer`及临时实例停止句柄。
这是未来待批动作,本轮没有创建服务/定时器。
- 语音为团队陪同的3次预约短用,每次600秒,总ASR1800秒,建房尝试最多6次,
每通ASR重连最多2次,TTS60段及声音登记+试听合计10次由当班执行者监督计数。
这些软上限不冒充程序硬闸;不在无人值守时开启语音窗口。
TTS段顶/语音配额到顶只结束语音,B文字生活仍可用;到期/用户叫停才停全实例。
自定义声音需要既有8090面板的实例专属启动句柄与前后快照,
复用HEL907已有面板启动方法,不动8081;句柄或快照未备齐不放行上传/试听,
默认已有工程声音仍能通话。共享voices列表需恢复前快照,自定义参考材料在独立实例保留。
- 到期脚本负责停本实例入口与自己启动的语音服务。封账归档、LiveKit只读核空、
voices快照恢复由当班执行者另行执行并核实,不能把日志里的done当全部清理完成。
清理失败保留原始错误/账/日志,不解除封账、不重启服务来“证明已停”。
自定义声音细分额度(同一次待批方案):最多登记2段参考音频,每段由陪同人员先核≤20秒;
自动转写最多2次/合计40秒,单列监督账,不冒充通话ASR的1800秒硬闸。
最多试听8次;登记2次+试听8次共10次。试听合成并入60段TTS总上限。
两类ASR合计最大1840秒(通话1800+参考40);失败/重试同样记账,不返余额。
本轮未执行登记、转写或试听。
### 最终统一预算勘误:团队先验真实记忆,再交用户(总管)
此条取代上文A“0嵌入”的旧建议,仅修改待批方案,当前真实额度仍0。
团队先用独立测试资料核真实Mem0接入,不把首次故障留给用户:
A的55请求/50万tokens内,最多10次用于SDK提取;先用最多20条新嵌入。
这10次/20条从同一批新申请的60次提取/180条嵌入中先扣,
不是另加额度;余量(至少50次提取/160条嵌入)留给用户期,使用实账计算。
两阶段共用唯一活动记账文件、串行写入,测试向量/档案与用户向量/档案分开,
只共用计数账,不拷贝测试聊天或测试记忆给用户。旧权威embed_ops前缀不改,
旧39/60实验的授权和账原封保存,不挪用其余额;新提取批建立独立可审计操作记录。
如旧llm_ops表已有用量,新60次必须按现值加增量并保存旧行,不可清表/重置;
操作前核旧基数并留证,实际新增始终≤60。团队检查最多2小时,从首个真实调用计时;
检查不通过则停止并报告首个错误,用户期不自动开始。
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# HEL-913 统一体验运行包 · 统一实例 API 启停(单进程 HTTP+HTTPS 双入口)
#
# 启动器 deploy/voice/serve_dual_8360_8361.py 保证 HTTP 与 HTTPS 共用同一
# API 进程/同一份状态(lifespan 只进一次)。本脚本只做本实例常规启停安全:
# - 子命令只认 start|stop|status,其他一律拒绝退出 2(不会静默当 start);
# - start 前核对 pid 句柄:本实例已在跑 → 拒绝重复 start(不覆盖句柄);
# 句柄 pid 活着但不属于本实例(PID 复用)→ 拒绝动手,提示人工处理;
# - stop/start 失败收尾只杀经身份核验的进程(/proc/<pid> 的 cmdline 含
# 本启动器且 cwd 是本实例代码目录),身份不符一律不杀;
# - start 健康检查不过 → 清掉自己刚起的进程、保留日志、退出 1。
#
# 用法: [ENV=..] bash deploy/hel913-unified/api_ctl.sh start|stop|status
# 环境变量(默认值按部署说明的实例布局):
# HEL913_BASE 实例根目录(含 current/ etc/ data/ logs/)
# HEL913_HTTP HTTP 端口(默认 8360)
# HEL913_HTTPS HTTPS 端口(默认 8361;语音页需要 secure context)
# HEL913_VENV 产品 3.11 venv python(默认 /opt/cybergril-hel879/current/.venv311/bin/python)
# HEL913_CERT / HEL913_KEY 证书路径(默认 /opt/cybergril-voice-certs/ 下既有证书)
set -u
BASE=${HEL913_BASE:-/opt/cybergril-hel913}
CODE=${HEL913_CODE:-$BASE/current}
HTTP=${HEL913_HTTP:-8360}
HTTPS=${HEL913_HTTPS:-8361}
VENV=${HEL913_VENV:-/opt/cybergril-hel879/current/.venv311/bin/python}
CERT=${HEL913_CERT:-/opt/cybergril-voice-certs/cybergril-voice.pem}
KEY=${HEL913_KEY:-/opt/cybergril-voice-certs/cybergril-voice.key}
LOG=$BASE/logs/api.log
PIDF=$BASE/logs/api.pid
MARKER="serve_dual_8360_8361.py"
usage() { echo "usage: [ENV=..] bash $0 start|stop|status" >&2; exit 2; }
# 进程身份核验:pid 存活、cmdline 含本启动器、cwd 是本实例代码目录。
# 任一不满足即视为“不是我们的进程”(含 PID 被系统复用),一律不动它。
ours() {
local p=$1 cmd cw
case "$p" in ''|*[!0-9]*) return 1 ;; esac
[ "$p" != "$$" ] || return 1
kill -0 "$p" 2>/dev/null || return 1
cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null) || return 1
cw=$(readlink "/proc/$p/cwd" 2>/dev/null) || return 1
case "$cmd" in *"$MARKER"*) ;; *) return 1 ;; esac
[ "$cw" = "$CODE" ] || return 1
return 0
}
pidfile_pid() { [ -f "$PIDF" ] && cat "$PIDF" 2>/dev/null; }
stop() {
local p
p=$(pidfile_pid)
if [ -z "$p" ]; then
echo "api not running (no pid file)"
return 0
fi
if kill -0 "$p" 2>/dev/null && ! ours "$p"; then
echo "REFUSED: pid $p is alive but is NOT this instance's api (pid reuse?) — not killing; resolve $PIDF manually" >&2
return 1
fi
if ours "$p"; then
kill "$p"
sleep 3
if ours "$p"; then kill -9 "$p"; sleep 1; fi
fi
rm -f "$PIDF"
echo "api stopped"
return 0
}
start() {
local p
p=$(pidfile_pid)
if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then
if ours "$p"; then
echo "REFUSED: api already running (pid $p) — stop first" >&2
else
echo "REFUSED: pid file has live pid $p that is not this instance's api (pid reuse?) — resolve $PIDF manually" >&2
fi
exit 1
fi
rm -f "$PIDF" # 死句柄:清掉再起
[ -d "$CODE" ] || { echo "REFUSED: code dir missing: $CODE" >&2; exit 1; }
[ -x "$VENV" ] || { echo "REFUSED: venv python not executable: $VENV" >&2; exit 1; }
[ -r "$CERT" ] && [ -r "$KEY" ] || { echo "REFUSED: cert/key not readable ($CERT / $KEY)" >&2; exit 1; }
mkdir -p "$BASE/logs" "$BASE/data"
cd "$CODE" || exit 1
set -a
export CYBERGRIL_DB=$BASE/data/companion.db
export CYBERGRIL_SEMANTIC=${HEL913_SEMANTIC:-stub}
export CYBERGRIL_R2_ENABLED=${HEL913_R2:-1}
export TZ=Asia/Shanghai
export CYBERGRIL_SEMANTIC_CONFIG=$BASE/etc/semantic.yaml
export MEM0_TELEMETRY=False
export FASTEMBED_CACHE_PATH=$BASE/data/sem/fastembed-cache
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export CYBERGRIL_MODELS=$BASE/etc/models.yaml
export CYBERGRIL_VOICE_CONFIG=$BASE/etc/voice.yaml
export CYBERGRIL_WEB=$CODE/web/dist
export PYTHONPATH=$CODE/src
set +a
nohup "$VENV" deploy/voice/serve_dual_8360_8361.py \
--http-port "$HTTP" --https-port "$HTTPS" --cert "$CERT" --key "$KEY" \
> "$LOG" 2>&1 &
p=$!
echo "$p" > "$PIDF"
echo "api starting (pid $p) http=$HTTP https=$HTTPS"
sleep 6
if curl -s --max-time 3 "http://127.0.0.1:$HTTP/api/health" >/dev/null \
&& curl -sk --max-time 3 "https://127.0.0.1:$HTTPS/api/health" >/dev/null; then
echo "OK: $HTTP + $HTTPS healthy"
return 0
fi
echo "FAIL: health check did not pass — full log kept at $LOG; tail:" >&2
tail -20 "$LOG" >&2
# 失败收尾:只清自己刚起的这个进程(身份核验),日志保留
if ours "$p"; then
kill "$p"; sleep 2
ours "$p" && kill -9 "$p"
fi
rm -f "$PIDF"
echo "cleaned up failed start (log kept: $LOG)" >&2
exit 1
}
status() {
local p
p=$(pidfile_pid)
if [ -n "$p" ]; then
if ours "$p"; then echo "running pid $p (identity verified)"
elif kill -0 "$p" 2>/dev/null; then echo "pid $p alive but NOT ours (stale pid file?)"
else echo "pid $p not alive (stale pid file)"; fi
else
echo "no pid file"
fi
curl -s --max-time 3 "http://127.0.0.1:$HTTP/api/health" && echo " <- health" \
|| echo "health not answering"
}
case "${1:-}" in
start) start ;;
stop) stop ;;
status) status ;;
*) usage ;;
esac
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# HEL-913 运行包 · wss TLS 桥启停模板(8363 → 127.0.0.1:7880;脱敏待批件)
#
# 浏览器从 HTTPS(8361) 页面连 LiveKit 必须走 wss(混合内容策略),本桥在新
# 端口终结 TLS 并只向回环 7880 转发(字节级,协议无关;WebRTC 媒体面不经
# 本桥)。与 8361 同一张自签证书。改编自 HEL-910 已实测 bridge_ctl.sh;
# 本文件默认不运行,获批语音窗口才使用。子命令只认 start|stop|status。
#
# 用法: [ENV=..] bash bridge_ctl.template.sh start|stop|status
# 环境变量: HEL913_BASE / HEL913_BRIDGE_PORT(8363) / HEL913_CERT / HEL913_KEY
set -u
case "${1:-}" in start|stop|status) ;; *) echo "invalid subcommand" >&2; exit 2 ;; esac
BASE=${HEL913_BASE:-/opt/cybergril-hel913}
PORT=${HEL913_BRIDGE_PORT:-8363}
CERT=${HEL913_CERT:-/opt/cybergril-voice-certs/cybergril-voice.pem}
KEY=${HEL913_KEY:-/opt/cybergril-voice-certs/cybergril-voice.key}
LOG=$BASE/logs/bridge.log
PIDF=$BASE/logs/bridge.pid
MARKER="wss_bridge.example.py"
usage() { echo "usage: [ENV=..] bash $0 start|stop|status" >&2; exit 2; }
ours() {
local p=$1 cmd cw
case "$p" in ''|*[!0-9]*) return 1 ;; esac
[ "$p" != "$$" ] || return 1
kill -0 "$p" 2>/dev/null || return 1
cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null) || return 1
cw=$(readlink "/proc/$p/cwd" 2>/dev/null) || return 1
case "$cmd" in *"$MARKER"*) ;; *) return 1 ;; esac
[ "$cw" = "$BASE/current/deploy/voice" ] || return 1
return 0
}
pidfile_pid() { [ -f "$PIDF" ] && cat "$PIDF" 2>/dev/null; }
stop() {
local p
p=$(pidfile_pid)
if [ -z "$p" ]; then echo "bridge not running (no pid file)"; return 0; fi
if kill -0 "$p" 2>/dev/null && ! ours "$p"; then
echo "REFUSED: pid $p alive but not this bridge (pid reuse?) — not killing; resolve $PIDF manually" >&2
return 1
fi
if ours "$p"; then kill "$p"; sleep 2; ours "$p" && kill -9 "$p"; fi
rm -f "$PIDF"
echo "bridge stopped"
}
case "${1:-}" in
stop) stop; exit $? ;;
status)
p=$(pidfile_pid)
if [ -n "$p" ]; then
ours "$p" && echo "running pid $p (identity verified)" \
|| { kill -0 "$p" 2>/dev/null && echo "pid $p alive but NOT ours" || echo "pid $p not alive (stale)"; }
else
echo "no pid file"
fi
exit 0 ;;
esac
p=$(pidfile_pid)
if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then
echo "REFUSED: bridge already running (pid $p)" >&2; exit 1
fi
rm -f "$PIDF"
[ -r "$CERT" ] && [ -r "$KEY" ] || { echo "REFUSED: cert/key not readable" >&2; exit 1; }
cd "$BASE/current/deploy/voice" || exit 1
nohup /usr/bin/python3 wss_bridge.example.py \
--listen "0.0.0.0:$PORT" --upstream 127.0.0.1:7880 \
--cert "$CERT" --key "$KEY" > "$LOG" 2>&1 &
echo $! > "$PIDF"
echo "bridge starting (pid $(cat "$PIDF")) on :$PORT"
sleep 3
timeout 8 openssl s_client -connect "127.0.0.1:$PORT" -servername 192.168.200.12 </dev/null 2>/dev/null \
| grep -q "TLSv1.3" && echo "OK: bridge TLS up" \
|| { echo "FAIL: TLS probe failed — check $LOG"; tail -5 "$LOG"; stop; exit 1; }
+10
View File
@@ -0,0 +1,10 @@
{
"assets/her-room.jpg": "51ca450a454c108d304feee2745070fbbbd3232863096126badf279ee53a925a",
"assets/her-talk.jpg": "0b9e91b58d75afb836efdad3d8e8f0aab0c44df64b2c4ddd59d011fd499aeb33",
"assets/icons.svg": "8630691c7349dd33c1accf2331cb4c844ffd9b70465021b1c18bf8f5aea0588e",
"assets/index-B8TdHoQm.css": "a0af3163f564f2ff9cfe7d995378b7ac436bfe2f9fabda9f370ca0aa30c2412f",
"assets/index-Vm-EFPLb.js": "81f55e20c5c2afe4673ccc9020264a2bf99b3c84998c275f03fa48fe4f940bb0",
"demo-material.json": "78fd67e8e4a680e8b3277db4a922360a83fa0be1ceee0cb257ba1c74bc6ede37",
"index.html": "8d84f131b6922fe92ac0c562f5fec73aacd9c6e4db2cfa28f6190b4246f9c8d0",
"vendor/livekit-client-2.22.4.umd.min.js": "040db4c0db72e7f7599cd0e06b0f58f95e0082a395cdde410ca26224a79a0626"
}
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# HEL-913 运行包 · 窗口到期强制收尾模板(脱敏待批件;默认不布防)
#
# 用途:获批体验/语音窗口到截止时点的**可执行**停服与封窗:先经统一实例
# API 逐个 DELETE 本实例房间(欠清理的房间会留在登记账重试),再按专属
# pid 句柄停 worker → api → wss 桥 → 共享 TTS(都不是我们的就如实报告,
# 绝不按进程名/端口扫杀,绝不触碰 8300/8310/8320/8350、7880/7881、10095、
# 8081 等共享服务)。改编自 HEL-910 已实测 force_end.sh/watchdog.sh。
#
# 用法:
# bash force_end.template.sh now # 立即收尾(幂等,可重复)
# bash force_end.template.sh watch <epoch秒> # 到点自动收尾(受监督后台任务;
# # 更可靠的等价物是获批后装
# # systemd timer 调本脚本 now)
# 环境变量: HEL913_BASE / HEL913_HTTP(8360)
set -u
set -o pipefail
BASE=${HEL913_BASE:-/opt/cybergril-hel913}
HTTP=${HEL913_HTTP:-8360}
PKG=$BASE/current/deploy/hel913-unified
LOG=$BASE/logs/force_end.log
usage() { echo "usage: [ENV=..] bash $0 now|watch <deadline_epoch_seconds>" >&2; exit 2; }
force_end() {
mkdir -p "$BASE/logs"
ts() { date -u +%FT%TZ; }
{
echo "[$(ts)] force_end begin"
# 1) 结清本实例房间(DELETE 走统一实例所有者路径;失败房间留账重试)
for sid in $(python3 - "$BASE/data" <<'EOF' 2>/dev/null
import json, sys, pathlib
p = pathlib.Path(sys.argv[1]) / "companion-voice-sessions.json"
try:
d = json.loads(p.read_text())
except Exception:
raise SystemExit
# VoiceSessionService._persist_state stores sessions as a LIST, not a dict.
# Debts already follow bounded SDK cleanup; do not reset their retry budgets here.
for entry in d.get("sessions", []):
sid = entry.get("session_id", "")
if sid and all(c.isalnum() or c in "-_" for c in sid):
print(sid)
EOF
); do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 -X DELETE "http://127.0.0.1:$HTTP/api/voice/session/$sid" || echo err)
echo "[$(ts)] DELETE session $sid -> $code"
done
# 2) 按专属句柄逐个停(每步幂等;不是我们的如实报告)
bash "$PKG/worker_ctl.sh" stop 2>&1 | sed 's/^/ worker: /'
bash "$PKG/api_ctl.sh" stop 2>&1 | sed 's/^/ api: /'
bash "$PKG/bridge_ctl.template.sh" stop 2>&1 | sed 's/^/ bridge: /'
bash "$PKG/tts_ctl.template.sh" stop 2>&1 | sed 's/^/ tts: /'
echo "[$(ts)] force_end done(封账归档另由 seal_budget_hel879.py 按账执行,封账不解除)"
} >> "$LOG" 2>&1
}
case "${1:-}" in
now) force_end ;;
watch)
DL=${2:?deadline epoch seconds required}
case "$DL" in *[!0-9]*|'') usage ;; esac
mkdir -p "$BASE/logs"
echo "[$(date -u +%FT%TZ)] watch start, deadline $(date -u -d "@$DL" +%FT%TZ) (pid $$)" >> "$LOG"
while [ "$(date +%s)" -lt "$DL" ]; do sleep 30; done
echo "[$(date -u +%FT%TZ)] DEADLINE REACHED — force end" >> "$LOG"
force_end
;;
*) usage ;;
esac
+86
View File
@@ -0,0 +1,86 @@
{
"task": "HEL-913",
"product_baseline_head": "4a38106eef21c43ba24ab7d07992b1bfe539137d",
"package_head": "运行包提交 = 本文件所在分支 tip(交付评论给出精确 hash;本文件无法写死自身提交)。部署照抄入口是运行包提交,只 checkout 4a38106 会没有 deploy/hel913-unified/",
"package_only_change_check": "git diff --stat 4a38106 <package_head> -- src web config tools tests 必须为空(运行包只动 deploy/hel913-unified 与 docs)",
"branch": "agent/2/8e99cc2cc592",
"review": "HEL-912 01a187e8-1058-7a2d-84e7-bd1896f212cb passed, no must-fix items (product baseline)",
"requirements_lock": "requirements.lock (19 pinned, Python 3.11.16 verified; offline verify printed 4 key package versions only — not a per-item lock audit)",
"web_dist_reuse": {
"source": "/opt/cybergril-hel910/current/web/dist (REVISION 958c6962fb1f2a4462a6df794d893aee1fcb0667)",
"justification": "git diff 958c696 4a38106 -- web/ is empty",
"manifest": "deploy/hel913-unified/dist-manifest.json (8 files, sha256 measured on .12 2026-10-29)"
},
"environments": {
"api_python_default": "/opt/cybergril-hel879/current/.venv311 (Python 3.11.16) — borrow-run only, NEVER install into it",
"api_python_after_mem0": "/opt/cybergril-hel913/venv311 — isolated instance venv created only inside the approved one-shot window (README §8)",
"worker_python": "/opt/voice-agent/agent/venv (livekit-agents 1.8.8)",
"certs": "/opt/cybergril-voice-certs/cybergril-voice.{pem,key} (shared by 8361 and wss bridge 8363)",
"livekit": "ws://127.0.0.1:7880 internal (standing); browser MUST use wss bridge (8363) — not optional from HTTPS pages",
"tts_panel_bridge": "9880/8090/8363 only inside approved voice windows (README §5); stopped otherwise"
},
"package_files": [
"deploy/hel913-unified/README.md",
"deploy/hel913-unified/manifest.json",
"deploy/hel913-unified/dist-manifest.json",
"deploy/hel913-unified/models.template.yaml",
"deploy/hel913-unified/voice.template.yaml",
"deploy/hel913-unified/worker.env.template",
"deploy/hel913-unified/voice-window-arm.template.env",
"deploy/hel913-unified/api_ctl.sh",
"deploy/hel913-unified/worker_ctl.sh",
"deploy/hel913-unified/bridge_ctl.template.sh",
"deploy/hel913-unified/tts_ctl.template.sh",
"deploy/hel913-unified/force_end.template.sh",
"deploy/hel913-unified/verify_offline.py",
"deploy/hel913-unified/test_ctl_scripts.sh",
"deploy/hel913-unified/mem0-cp311-wheels.txt",
"docs/rework/hel913/verify-offline-run.txt",
"docs/rework/hel913/ctl-checks-run.txt",
"docs/hel913-capability-and-enablement.md"
],
"offline_verification": {
"where": ".12 product .venv311, candidate tree at /tmp/hel913-verify (cleaned after run); re-run by manager: 34/34 passed, dist 8-file hashes match",
"result": "34/34 checks passed; worker import ok under livekit-agents 1.8.8",
"zero_real_calls": "models base_url rewritten to 127.0.0.1:1 + provider.stream outbound sentinel; no listening sockets; no rooms; no gateway contact",
"scope_limits": "deps check printed 4 key versions only; unshared-life trimming verified on an empty DB (told=1) without positive/negative seed contrast — not claimed as full dependency or permission coverage",
"ctl_scripts": "test_ctl_scripts.sh 42/42 offline stub checks (invalid subcommand, duplicate start, pid-reuse refusal, failed-start cleanup, .env gating, templates default 0)"
},
"voice_window_wiring": {
"asr_hard_gate": "worker/.env CYBERGRIL_ASR_WINDOW_ID/_MAX_AUDIO_SECONDS/_MAX_CONNECT_RETRIES_PER_CALL (empty/0 = deny all ASR)",
"room_lifetime": "voice.yaml session_ttl_seconds=900 + token_ttl 300 + delete-on-hangup (bounded cleanup ledger)",
"tts_segments": "monitored via tts.log first-chunk count since window start (tts_ctl.template.sh records start byte); hard stop = window deadline",
"deadline": "force_end.template.sh now|watch <epoch> (DELETE sessions -> worker -> api -> bridge -> tts, pid-handle stops only)"
},
"memory_gap": {
"server_venv311": "mem0/qdrant_client/fastembed/onnxruntime all absent (verified 2026-10-29; never install into this borrowed venv)",
"no_other_311_env_on_server": true,
"local_artifacts": {
"mem0-2c81af3.tar.gz": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"mem0ai-2.2.1-py3-none-any.whl": "698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db",
"fastembed_cache": "92MB (Qdrant/bge-small-zh-v1.5 + bm25)",
"hel857_venv": "Python 3.10.12 — must not mix into 3.11 process"
},
"install_target": "isolated /opt/cybergril-hel913/venv311 (python3.11 base + product lock 19 wheels + mem0 stack 40 wheels + fixed-source mem0ai wheel; all --no-index; complete auditable steps in README §8.3 with per-file sha256 evidence and fail-stop)",
"ledger_continuation": "authoritative embed ledger copied to instance data/embed_ledger.sqlite3 (ledger only — no test chats/vectors moved); authority prefix_rows=181, prefix_sha256 by manager, min_used=498, embed_cap=680 cumulative formula, frozen 2 untouched; 39/60 and old window balances never carried over"
},
"enablement_doc": "docs/hel913-capability-and-enablement.md §2 — single consistent pending-approval proposal: A/B/C separate accounts, all awaiting approval (manager does not self-approve A), B voice = 3 calls x <=10min = <=1800s ASR hard gate with per-cap consequences and executable window end, B/C boundary fixed",
"boundaries": "not deployed; nothing listening; caps all 0; no PR; existing instances and data untouched; no downloads this card; protected venvs untouched",
"coordinator_close_verification": "docs/rework/hel913/coordinator-close-checks.txt: 17/17 targeted offline checks; product baseline unchanged",
"file_sha256": {
"deploy/hel913-unified/README.md": "580e3100b207c8c0e3da9cddc644b00d7b3d66ae0d195ac058dba279f63e242e",
"deploy/hel913-unified/api_ctl.sh": "3eeff54fcdc1becb79d9fe63767eceee9209ed6239663ff7d0a3e90d27379c55",
"deploy/hel913-unified/bridge_ctl.template.sh": "43e951f01ceed07b9c1606cfc046aa1d34eefc8fad3a40c66db2de57f16c1d14",
"deploy/hel913-unified/dist-manifest.json": "c2ebc6e515096b535f5e08cce18dc9c4f104c1b2cabe3f3afb5fdd88f12d63db",
"deploy/hel913-unified/force_end.template.sh": "3f250942ff68e26e4a71750e17fd804fa57be921097d83ee76f1b5b0024fe893",
"deploy/hel913-unified/mem0-cp311-wheels.txt": "2a6fd3d287fe0713a2ac683584ee7f3723c83c236696f39551015ca2db358b94",
"deploy/hel913-unified/models.template.yaml": "0b8f051cedfcf0218929a652fed91b00762c33dd17fbb8f7eadcc50c65d4e837",
"deploy/hel913-unified/test_ctl_scripts.sh": "702f7b911598fc0433f63f250f2f1e58dec9d727dd7eb6ae8a773d521fcad5cd",
"deploy/hel913-unified/tts_ctl.template.sh": "f638f24a41f07cfef64e66eee996b4280fdd7aac80a5fc20be6d47a41b72cabf",
"deploy/hel913-unified/verify_offline.py": "86ea3da6f2c682ad8ee9a932f91d713e0778c61be5d4c15f916e38d15e3e66e4",
"deploy/hel913-unified/voice-window-arm.template.env": "4002b99d63fa556a5f1e8cb86bf74846e7b944571dd76ed40bfb549cf64f3307",
"deploy/hel913-unified/voice.template.yaml": "55e556d5df149ab4fa257640b4d87dc40bb43107a5e90279826516a7ec423f4c",
"deploy/hel913-unified/worker.env.template": "be80fc87cc04a379a776732ad94f392e08af4ecb9eaad4190a090f4071f40383",
"deploy/hel913-unified/worker_ctl.sh": "57fd028e3b0d932cdbde483925cc7e39a5b994fa53742f9350cc5fc22381077f"
}
}
@@ -0,0 +1,45 @@
# HEL-913 记忆栈 cp311 缺项轮清单(40 项;来源 docs/rework/hel879/sdk-audit/sdk-audit.json 闭包 55-产品已有)
# mem0ai 不在本表:只认固定源码轮 sha256 698284b6…(deploy/hel895-release-candidate.md §4 唯一入口)。
# 获批下载命令:pip download --only-binary :all: --implementation cp --python-version 3.11 \
# --platform manylinux2014_x86_64 --abi cp311 -r 本文件 -d wheels/ 逐项 sha256 留证。
# 其中 greenlet/pillow/truststore 三项已在工作站 uv 缓存命中(hel-879 release-artifacts 记录),可离线复用。
backoff==2.2.1
charset-normalizer==3.5.2
coloredlogs==15.0.1
distro==1.9.0
fastembed==0.9.1
filelock==4.1.0
flatbuffers==25.12.19
fsspec==2026.9.0
greenlet==3.5.6
grpcio==1.84.1
h2==4.4.1
hf-xet==1.7.0
hpack==4.2.0
httpcore2==2.14.0
httpx2==2.14.0
huggingface-hub==1.33.0
humanfriendly==10.0
hyperframe==6.1.0
jiter==0.18.0
loguru==0.7.3
mmh3==5.3.1
mpmath==1.3.0
numpy==2.2.6
onnxruntime==1.23.2
openai==3.37.0
pillow==12.3.0
portalocker==4.4.1
posthog==7.75.11
protobuf==6.33.6
py-rust-stemmers==0.1.8
pytz==2026.5
qdrant-client==1.20.0
requests==2.34.2
sniffio==1.3.1
sqlalchemy==2.0.55
sympy==1.14.0
tokenizers==0.23.3
tqdm==4.70.1
truststore==0.10.5
urllib3==2.8.0
@@ -0,0 +1,89 @@
# HEL-913 统一体验运行包 · models 配置模板(脱敏;本文件一切预算=0)
#
# 用法:复制到 <实例>/etc/models.yaml,按部署说明只改 base_url 指向获批网关、
# 按批准包写入预算值。未经批准一律保持 0(发送前拒绝,fail-closed)。
# 候选分支 config/models.yaml 的 tiers/routing 与本模板一致;本模板把
# base_url 换成占位符并保留 HEL-908 reasoning: buffer_turn 与 HEL-911/912
# 审核通过的调度修正(turn_detection=None 在代码侧,见 src/cybergril/voice/worker.py)。
tiers:
high:
provider: openai_compat
model: <获批模型名>
base_url: <http://内网网关:端口/v1>
api_key_env: null
timeout: 60.0
temperature: 0.85
reasoning: buffer_turn
standard:
provider: openai_compat
model: <获批模型名>
base_url: <http://内网网关:端口/v1>
api_key_env: null
timeout: 40.0
temperature: 0.7
reasoning: buffer_turn
low:
provider: openai_compat
model: <获批模型名>
base_url: <http://内网网关:端口/v1>
api_key_env: null
timeout: 20.0
temperature: 0.4
reasoning: buffer_turn
routing:
dialogue_main: high
dialogue_followup: standard
proactive_message: high
crisis: high
life_event: low
memory_gist: low
fact_extract: low
output_guard: low
pattern_detect: low
filler: none
tick: none
reach_out: none
avatar_drive: none
budget:
daily_tokens: 0
warn_ratio: 0.7
hard_stop_ratio: 0.95
daily_requests:
dialogue: 0
life: 0
background: 0
semantic: 0
# 用户实例永不配置一次性总量(total_requests/total_tokens 保持缺省):
# 一次性总量只属于独立团队测试实例,见 docs/budget-package-hel895.md §6。
asr:
provider: scripted
model: qwen3-asr-1.7b
language: zh
paralinguistic:
provider: scripted
model: sensevoice-small
window_ms: 1500
enabled: true
tts:
provider: silence
model: fun-cosyvoice3-0.5b
voice_ref: null
sample_rate: 16000
vad:
provider: energy
model: silero-v6
sample_rate: 16000
turn:
provider: heuristic
silence_min_ms: 150
silence_hard_end_ms: 800
embedding:
provider: openai_compat
model: qwen3-embedding-0.6b
base_url: null
api_key_env: null
dim: 1024
avatar:
route: vrm
renderer: three.js
lipsync: envelope
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env bash
# HEL-913 运行包 · 启停脚本离线替身检查(零真实服务、零真实端口、零出站)
#
# 用离线替身(stub 解释器 = 只休眠的脚本)与静态检查验证收口后的常规启停
# 安全:非法子命令拒绝、重复 start 拒绝、stop 的 PID 复用防护、启动失败
# 收尾清理、.env 占位/解析守门。不启动真实 API/worker/共享服务,不监听
# 任何端口(curl 只连本机高位死端口,必然失败,正是失败路径所需的输入)。
#
# 用法: bash deploy/hel913-unified/test_ctl_scripts.sh (在检出根目录)
set -u
PKG=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$PKG/../.." && pwd)
TMP=$(mktemp -d /tmp/hel913-ctl-test.XXXXXX)
PASS=0; FAIL=0
cleanup() { rm -rf "$TMP"; }
trap cleanup EXIT
ok() { PASS=$((PASS+1)); echo "PASS $1"; }
bad() { FAIL=$((FAIL+1)); echo "FAIL $1"; }
expect_rc() { # expect_rc <want> <got> <label>
[ "$2" = "$1" ] && ok "$3" || bad "$3 (want rc=$1 got rc=$2)"
}
# ---- S1. 静态:语法 + 包文件齐全 ----
for f in api_ctl.sh worker_ctl.sh bridge_ctl.template.sh tts_ctl.template.sh force_end.template.sh; do
bash -n "$PKG/$f" 2>/dev/null && ok "syntax $f" || bad "syntax $f"
done
for f in README.md manifest.json dist-manifest.json models.template.yaml voice.template.yaml \
worker.env.template verify_offline.py mem0-cp311-wheels.txt \
voice-window-arm.template.env test_ctl_scripts.sh; do
[ -f "$PKG/$f" ] && ok "package file $f" || bad "package file $f missing"
done
# ---- 沙盒实例布局(镜像 README §4 的目标布局) ----
BASE=$TMP/inst
mkdir -p "$BASE/current" "$BASE/worker" "$BASE/etc" "$BASE/data" "$BASE/logs"
CODE=$BASE/current; WDIR=$BASE/worker
touch "$BASE/etc/models.yaml" "$BASE/etc/voice.yaml"
printf 'dummy cert\n' > "$TMP/c.pem"; printf 'dummy key\n' > "$TMP/c.key"
# stub 解释器:保持 argv 不变(cmdline 保留 marker),留一行输出后休眠
STUB=$TMP/stub-python
printf '#!/usr/bin/env bash\necho "stub alive pid=$$ argv=$*" >&2\nsleep 600\n' > "$STUB"; chmod +x "$STUB"
HTTP=53881; HTTPS=53882
API_ENV="HEL913_BASE=$BASE HEL913_HTTP=$HTTP HEL913_HTTPS=$HTTPS HEL913_VENV=$STUB HEL913_CERT=$TMP/c.pem HEL913_KEY=$TMP/c.key"
alive_marker() { pgrep -f "$1" | while read -r p; do [ -d "/proc/$p" ] && echo "$p"; done; }
# ---- S2. 非法子命令拒绝(不静默当 start) ----
env $API_ENV bash "$PKG/api_ctl.sh" bogus >/dev/null 2>&1; expect_rc 2 $? "api_ctl invalid subcommand refused (rc=2)"
[ ! -f "$BASE/logs/api.pid" ] && ok "api_ctl invalid subcommand started nothing" || bad "api_ctl invalid subcommand left a pid file"
env HEL913_BASE=$BASE HEL913_AGENT_VENV=$STUB bash "$PKG/worker_ctl.sh" frobnicate >/dev/null 2>&1
expect_rc 2 $? "worker_ctl invalid subcommand refused (rc=2)"
[ ! -f "$BASE/logs/worker.pid" ] && ok "worker_ctl invalid subcommand started nothing" || bad "worker_ctl invalid subcommand left a pid file"
# ---- S3. api 启动失败收尾:健康不过 → 清自己、留日志、rc=1 ----
env $API_ENV bash "$PKG/api_ctl.sh" start >/dev/null 2>&1; expect_rc 1 $? "api_ctl failed start exits 1"
[ ! -f "$BASE/logs/api.pid" ] && ok "api_ctl failed start removed pid file" || bad "api_ctl failed start left pid file"
[ -s "$BASE/logs/api.log" ] && ok "api_ctl failed start kept log" || bad "api_ctl failed start lost log"
sleep 1
N=$(alive_marker "$STUB.*serve_dual_8360_8361" | wc -l)
[ "$N" = "0" ] && ok "api_ctl failed start left no stub process" || bad "api_ctl failed start left $N stub process(es)"
# ---- S4. api 重复 start 拒绝(活进程 + 有效句柄) ----
( cd "$CODE" && nohup "$STUB" deploy/voice/serve_dual_8360_8361.py --http-port $HTTP --https-port $HTTPS > "$BASE/logs/api.log" 2>&1 & echo $! > "$BASE/logs/api.pid" )
LIVEPID=$(cat "$BASE/logs/api.pid")
kill -0 "$LIVEPID" 2>/dev/null && ok "stub api process running (pid $LIVEPID)" || bad "stub api process failed to start"
env $API_ENV bash "$PKG/api_ctl.sh" start >/dev/null 2>&1; expect_rc 1 $? "api_ctl duplicate start refused (rc=1)"
[ "$(cat "$BASE/logs/api.pid" 2>/dev/null)" = "$LIVEPID" ] && ok "api_ctl duplicate start kept original handle" || bad "api_ctl duplicate start overwrote handle"
kill "$LIVEPID" 2>/dev/null; wait 2>/dev/null
# ---- S5. api stop 的 PID 复用防护:活 pid 但身份不符 → 拒杀 ----
sleep 600 & UNRELATED=$!
echo "$UNRELATED" > "$BASE/logs/api.pid"
env $API_ENV bash "$PKG/api_ctl.sh" stop >/dev/null 2>&1; expect_rc 1 $? "api_ctl stop refused reused pid (rc=1)"
kill -0 "$UNRELATED" 2>/dev/null && ok "unrelated process untouched" || bad "api_ctl stop killed an unrelated process!"
kill "$UNRELATED" 2>/dev/null; wait 2>/dev/null
rm -f "$BASE/logs/api.pid"
# ---- S6. api stop 死句柄:直接清 ----
echo 999999999 > "$BASE/logs/api.pid"
env $API_ENV bash "$PKG/api_ctl.sh" stop >/dev/null 2>&1; expect_rc 0 $? "api_ctl stop with dead pid exits 0"
[ ! -f "$BASE/logs/api.pid" ] && ok "api_ctl stop removed stale pid file" || bad "api_ctl stop kept stale pid file"
# ---- S7. worker .env 守门:占位未填 → 拒绝 ----
cp "$PKG/worker.env.template" "$WDIR/.env"
env HEL913_BASE=$BASE HEL913_AGENT_VENV=$STUB bash "$PKG/worker_ctl.sh" start >/dev/null 2>&1
expect_rc 1 $? "worker_ctl refuses .env with unfilled placeholders (rc=1)"
[ ! -f "$BASE/logs/worker.pid" ] && ok "placeholder refusal started nothing" || bad "placeholder refusal left pid file"
# ---- S8. worker .env 守门:解析失败 → 拒绝 ----
printf 'CYBERGRIL_API_BASE=http://127.0.0.1:8360\nFOO=bar baz\n' > "$WDIR/.env"
env HEL913_BASE=$BASE HEL913_AGENT_VENV=$STUB bash "$PKG/worker_ctl.sh" start >/dev/null 2>&1
expect_rc 1 $? "worker_ctl refuses unparseable .env (rc=1)"
[ ! -f "$BASE/logs/worker.pid" ] && ok "parse refusal started nothing" || bad "parse refusal left pid file"
# ---- S9. worker 启动失败收尾(注册不过 → 清自己、留日志) ----
cat > "$WDIR/.env" <<'EOF'
CYBERGRIL_API_BASE=http://127.0.0.1:8360
CYBERGRIL_AGENT_NAME=cybergril-voice-agent
LIVEKIT_URL=ws://127.0.0.1:7880
LIVEKIT_API_KEY=filled
LIVEKIT_API_SECRET=filled
CYBERGRIL_WORKER_HEALTH_PORT=8082
CYBERGRIL_VOICE_STATE=/tmp/x/voice_state.json
CYBERGRIL_ASR_USAGE_LEDGER=/tmp/x/asr-usage.jsonl
CYBERGRIL_ASR_WINDOW_ID=
CYBERGRIL_ASR_WINDOW_MAX_AUDIO_SECONDS=0
CYBERGRIL_ASR_MAX_CONNECT_RETRIES_PER_CALL=0
EOF
env HEL913_BASE=$BASE HEL913_AGENT_VENV=$STUB bash "$PKG/worker_ctl.sh" start >/dev/null 2>&1
expect_rc 1 $? "worker_ctl failed start exits 1"
[ ! -f "$BASE/logs/worker.pid" ] && ok "worker_ctl failed start removed pid file" || bad "worker_ctl failed start left pid file"
[ -s "$BASE/logs/worker.log" ] && ok "worker_ctl failed start kept log" || bad "worker_ctl failed start lost log"
sleep 1
N=$(alive_marker "$STUB.*cybergril.voice.worker" | wc -l)
[ "$N" = "0" ] && ok "worker_ctl failed start left no stub process" || bad "worker_ctl failed start left $N stub process(es)"
# ---- S10. worker 重复 start 拒绝 ----
( cd "$WDIR" && nohup "$STUB" -m cybergril.voice.worker start > "$BASE/logs/worker.log" 2>&1 & echo $! > "$BASE/logs/worker.pid" )
LIVEPID=$(cat "$BASE/logs/worker.pid")
env HEL913_BASE=$BASE HEL913_AGENT_VENV=$STUB bash "$PKG/worker_ctl.sh" start >/dev/null 2>&1
expect_rc 1 $? "worker_ctl duplicate start refused (rc=1)"
kill "$LIVEPID" 2>/dev/null; wait 2>/dev/null
rm -f "$BASE/logs/worker.pid"
# ---- S11. worker stop 的 PID 复用防护 ----
sleep 600 & UNRELATED=$!
echo "$UNRELATED" > "$BASE/logs/worker.pid"
env HEL913_BASE=$BASE bash "$PKG/worker_ctl.sh" stop >/dev/null 2>&1; expect_rc 1 $? "worker_ctl stop refused reused pid (rc=1)"
kill -0 "$UNRELATED" 2>/dev/null && ok "worker stop left unrelated process alive" || bad "worker stop killed unrelated process!"
kill "$UNRELATED" 2>/dev/null; wait 2>/dev/null
rm -f "$BASE/logs/worker.pid"
# ---- S12. 模板可解析性:arm 模板是合法 KEY=value(可被 source,默认全 0) ----
if ( set -a; . "$PKG/voice-window-arm.template.env"; set +a
[ "${HEL913_ASR_MAX_AUDIO_SECONDS:-x}" = "0" ] \
&& [ -n "${HEL913_WINDOW_ID+x}" ] && [ -z "$HEL913_WINDOW_ID" ] \
&& [ "${HEL913_TTS_SEGMENT_CAP:-x}" = "0" ] ) 2>/dev/null; then
ok "voice-window-arm.template.env parses, defaults 0/unarmed"
else
bad "voice-window-arm.template.env unparseable or non-zero default"
fi
echo
echo "$PASS passed, $FAIL failed"
[ "$FAIL" = "0" ]
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# HEL-913 运行包 · 共享 CosyVoice TTS 9880 窗口内临时恢复/停回模板(脱敏待批件)
#
# 只在获批语音窗口内使用;窗口结束必须 stop 停回(force_end.template.sh 会调
# 本脚本 stop)。只按自己写的 pid 句柄停止,快照先行;9880 已被别人启动时
# 不碰。改编自 HEL-910 已实测 tts_ctl.sh。子命令只认 start|stop|status|snapshot。
#
# 用法: [ENV=..] bash tts_ctl.template.sh start|stop|status|snapshot
# 环境变量: HEL913_BASE / HEL913_TTS_VENV(/opt/voice-agent/cosyvoice/venv/bin/python)
set -u
case "${1:-}" in start|stop|status|snapshot) ;; *) echo "invalid subcommand" >&2; exit 2 ;; esac
BASE=${HEL913_BASE:-/opt/cybergril-hel913}
CC=${HEL913_TTS_HOME:-/opt/voice-agent/cosyvoice}
TTSVENV=${HEL913_TTS_VENV:-$CC/venv/bin/python}
PIDF=$BASE/logs/tts9880.pid
SNAP=$BASE/logs/tts9880-before.txt
usage() { echo "usage: [ENV=..] bash $0 start|stop|status|snapshot" >&2; exit 2; }
ours() {
local p=$1 cmd cw
case "$p" in ''|*[!0-9]*) return 1 ;; esac
[ "$p" != "$$" ] || return 1
kill -0 "$p" 2>/dev/null || return 1
cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null) || return 1
cw=$(readlink "/proc/$p/cwd" 2>/dev/null) || return 1
case "$cmd" in *"server.py"*) ;; *) return 1 ;; esac
[ "$cw" = "$CC" ] || return 1
return 0
}
pidfile_pid() { [ -f "$PIDF" ] && cat "$PIDF" 2>/dev/null; }
case "${1:-}" in
status)
p=$(pidfile_pid)
if [ -n "$p" ]; then ours "$p" && echo "running pid $p (ours)" || echo "pid $p not verified as ours"; else echo "not started by us"; fi
curl -s --max-time 3 http://127.0.0.1:9880/health && echo " <- health" || echo "health not answering"
exit 0 ;;
snapshot)
mkdir -p "$BASE/logs"
{ echo "ts=$(date -u +%FT%TZ)"
if curl -s --max-time 3 http://127.0.0.1:9880/health >/dev/null 2>&1; then echo "was=up"; else echo "was=down"; fi
ss -ltn 2>/dev/null | grep ':9880 ' || echo "port9880=free"; } > "$SNAP"
cat "$SNAP"; exit 0 ;;
stop)
p=$(pidfile_pid)
if [ -z "$p" ]; then echo "not ours to stop (no pid file)"; exit 0; fi
if kill -0 "$p" 2>/dev/null && ! ours "$p"; then
echo "REFUSED: pid $p alive but not this tts (pid reuse?) — not killing; resolve $PIDF manually" >&2
exit 1
fi
if ours "$p"; then kill "$p"; sleep 5; ours "$p" && kill -9 "$p"; fi
rm -f "$PIDF"
echo "tts stopped (pid $p)"
exit 0 ;;
esac
# start(仅 start 子命令落到这里)
mkdir -p "$BASE/logs"
p=$(pidfile_pid)
if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then
echo "REFUSED: live pid handle already exists; inspect/stop first" >&2; exit 1
fi
if curl -s --max-time 3 http://127.0.0.1:9880/health >/dev/null 2>&1; then
echo "9880 already up — NOT ours, not touching"; exit 0
fi
if ss -ltn 2>/dev/null | grep -q ':9880 '; then
echo "port 9880 occupied by non-healthy process — abort"; exit 1
fi
[ -x "$TTSVENV" ] || { echo "REFUSED: tts venv python not executable: $TTSVENV" >&2; exit 1; }
cd "$CC" || exit 1
nohup "$TTSVENV" server.py >> "$CC/tts.log" 2>&1 &
echo $! > "$PIDF"
echo "tts starting (pid $(cat "$PIDF")) — model load takes up to ~120s"
for i in $(seq 1 60); do
sleep 2
if curl -s --max-time 3 http://127.0.0.1:9880/health 2>/dev/null | grep -q '"ok"[[:space:]]*:[[:space:]]*true'; then
echo "OK: tts healthy after ~$((i*2))s"
wc -c < "$CC/tts.log" > "$BASE/logs/tts-log-start-byte" # TTS 段数计数的窗口起点
exit 0
fi
kill -0 "$(cat "$PIDF")" 2>/dev/null || { echo "FAIL: tts process died"; tail -10 "$CC/tts.log"; exit 1; }
done
echo "FAIL: tts not healthy in 120s"; tail -10 "$CC/tts.log"
p=$(pidfile_pid)
if ours "$p"; then kill "$p"; sleep 2; ours "$p" && kill -9 "$p"; fi
rm -f "$PIDF"
exit 1
+273
View File
@@ -0,0 +1,273 @@
"""HEL-913 运行准备离线验证:零真实调用(cap=0 + 死地址双重保险)。
在进程内用 TestClient 走统一实例的关键接线:入口健康、创建守门、传输
(SSE 对话在预算 0 下诚实拒绝且绝不触发出站)、关系调整、日子与回忆
读取的服务端裁剪、语音诚实拒绝、重开(epoch/资料保留)、静态页与 dist
哈希。不监听任何端口(836x/8082/9880/8090 全程不出现监听 socket),
不访问真实网关(models.base_url 重写为 127.0.0.1:1,livekit 指向死地址,
即使预算闸失效也只能连本机死端口)。
用法(在候选 head 检出根目录、产品 3.11 venv 下):
PYTHONPATH=src <venv311>/python deploy/hel913-unified/verify_offline.py \
[--web <dist目录>] [--keep]
退出码 0 = 全部通过;任一 FAIL 即非 0。临时库写在 <root>/.hel913-verify-tmp,
默认结束后删除(--keep 保留)。
"""
from __future__ import annotations
import argparse
import atexit
import json
import os
import shutil
import sys
import tempfile
import uuid
from pathlib import Path
RESULTS: list[tuple[bool, str]] = []
def check(ok: bool, label: str) -> bool:
RESULTS.append((bool(ok), label))
print(("PASS " if ok else "FAIL ") + label)
return bool(ok)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--web", default=None, help="web dist 目录(只读;默认不挂静态)")
parser.add_argument("--keep", action="store_true")
args = parser.parse_args()
root = Path(__file__).resolve().parents[2]
os.chdir(root)
tmp = Path(tempfile.mkdtemp(prefix="hel913-verify-", dir=root))
if not args.keep:
atexit.register(shutil.rmtree, tmp, ignore_errors=True)
# ---- 0. 包完整性:Python / 锁定依赖 / 模块树 -----------------------------
check(sys.version_info[:2] == (3, 11), f"python 3.11 (actual {sys.version_info[:3]})")
import cybergril # noqa: E402
check(str(Path(cybergril.__file__).resolve()).startswith(str(root / "src")),
f"cybergril imported from candidate tree ({cybergril.__file__})")
import fastapi # noqa: E402
import httpx # noqa: E402
import pydantic # noqa: E402
import uvicorn # noqa: E402
import yaml # noqa: E402
check(True, f"deps: fastapi {fastapi.__version__} uvicorn {uvicorn.__version__} "
f"httpx {httpx.__version__} pydantic {pydantic.__version__}")
from cybergril.api.app import create_app # noqa: E402
from cybergril.voice.router import build_voice_router # noqa: E402
from cybergril.voice.session import load_voice_config # noqa: E402
check(True, "product modules import (api.app / voice.router / voice.session)")
# 记忆栈缺项如实呈现(不允许 stub 冒充真实 mem0)
for mod, expect in (("mem0", False), ("qdrant_client", False), ("fastembed", False)):
try:
__import__(mod)
got = True
except Exception:
got = False
check(got == expect, f"{mod} {'absent' if not expect else 'present'} in product venv (as documented)")
# ---- 1. 脱敏配置:cap=0 + 死地址 ----------------------------------------
models_src = (root / "config" / "models.yaml").read_text()
assert "daily_tokens" in models_src, "models.yaml missing budget block"
dead = models_src.replace("192.168.200.11:8731", "127.0.0.1:1")
assert "192.168" not in dead, "real gateway address survived rewrite"
models_path = tmp / "models.yaml"
models_path.write_text(dead)
voice_path = tmp / "voice.yaml"
voice_path.write_text(
"# HEL-913 offline template: everything points at dead local ports;\n"
"# no real LiveKit/TTS/panel is contacted by this verification.\n"
"livekit_url: ws://127.0.0.1:1\n"
"livekit_internal: http://127.0.0.1:1\n"
"api_key: dead\n"
"api_secret: dead\n"
"agent_name: cybergril-voice-agent\n"
"tts_health_url: http://127.0.0.1:1/health\n"
"panel_url: https://127.0.0.1:1\n"
"tts_url: http://127.0.0.1:1\n"
"registry_file: " + str(tmp / "voices.json") + "\n"
"worker_state_file: " + str(tmp / "voice_state.json") + "\n"
"token_ttl_seconds: 300\n"
"session_ttl_seconds: 900\n"
)
db_path = tmp / "companion.db"
env = {
"CYBERGRIL_DB": str(db_path),
"CYBERGRIL_MODELS": str(models_path),
"CYBERGRIL_VOICE_CONFIG": str(voice_path),
"CYBERGRIL_SEMANTIC": "stub",
"CYBERGRIL_R2_ENABLED": "1",
**({"CYBERGRIL_WEB": args.web} if args.web else {}),
}
old = {k: os.environ.get(k) for k in env}
os.environ.update(env)
from fastapi.testclient import TestClient # noqa: E402
app = create_app()
# 出站哨兵:把所有已装配 provider 的 stream 打上标记——预算 0 下任何
# 真实发送意图都会把本验证置为 FAIL(而不是悄悄连死地址)。
outbound = []
client = TestClient(app)
with client:
st = app.state
router = st.life.router # LifeRuntime 持有本实例同一 router
for tier in router.available_tiers():
prov = router.provider_for(tier)
def _guard_stream(self, *a, __orig=prov.stream, __name=getattr(prov, "name", tier), **kw):
outbound.append(__name)
raise AssertionError(f"unexpected outbound stream via {__name}")
prov.stream = _guard_stream.__get__(prov, type(prov))
birth = {"birth": "2001-08-16", "hour": "辰时", "name": "临川", "relation": "朋友",
"nickname": "", "boundary": "轻松相处"}
# 2. 入口健康
r = client.get("/api/health")
h = r.json()
check(r.status_code == 200 and h["ok"] and h["capabilities"]["text"] is True,
"GET /api/health 200, text capability true")
check(h["companion_created"] is False, "health: companion_created=false before creation")
check(h["llm"]["ok"] is False, "health: llm honestly not ok (dead addr, cap=0)")
# 3. 创建守门:空库 404,不能读出任何伪造档案
check(client.get("/api/companion").status_code == 404,
"GET /api/companion 404 before creation (no fabricated profile)")
# 4. 生辰预览:纯本地计算,无模型参与(BirthInput 只收 name/birth/hour)
r = client.post("/api/companion/preview",
json={k: birth[k] for k in ("name", "birth", "hour")})
preview_pillars = r.json().get("pillars") if r.status_code == 200 else None
check(r.status_code == 200 and "pillars" in r.json() and "sections" in r.json(),
"POST /api/companion/preview 200 (local bazi compile)")
# 5. 创建 + 底色只读落库
r = client.post("/api/companion", json=birth)
check(r.status_code == 201 and r.json()["name"] == "临川",
"POST /api/companion 201")
row = st.store.query_one("SELECT pillars FROM bazi_core WHERE id=1")
saved_day = json.loads(row["pillars"])["day"][0] if row else None
prev_day = (preview_pillars or {}).get("day", [None])[0]
check(saved_day is not None and saved_day == prev_day,
f"bazi_core persisted and matches preview (day master {saved_day})")
check(client.post("/api/companion", json=birth).status_code == 409,
"second create refused 409 (single companion guard)")
pv = client.get("/api/persona").json()
check(pv["interpretation"] is None and pv["state"] in {"failed", "loading", "disabled"},
f"persona honest at cap=0 (state={pv['state']}, no fabricated interpretation)")
# 6. 传输:SSE 对话在预算 0 下诚实拒绝,零出站
turn1 = str(uuid.uuid4())
with client.stream("POST", "/api/chat",
json={"text": "在吗", "turn_id": turn1, "epoch": st.epoch}) as resp:
check(resp.status_code == 200 and resp.headers["content-type"].startswith("text/event-stream"),
"POST /api/chat opens SSE stream")
kinds = [json.loads(line[6:])["t"] for line in resp.iter_lines()
if line.startswith("data: ")]
check("chat.error" in kinds and "chat.final" not in kinds,
f"chat honestly errors at cap=0 (frames={kinds})")
check(not outbound, f"zero provider stream calls (outbound={outbound})")
msgs = client.get("/api/messages").json()["messages"]
check(msgs and msgs[-1]["role"] == "user" and msgs[-1]["status"] in {"failed", "cancelled"},
"failed turn persisted as user row, no fake companion reply")
with client.stream("POST", "/api/chat",
json={"text": "在吗", "turn_id": turn1, "epoch": st.epoch}) as resp:
kinds2 = [json.loads(line[6:])["t"] for line in resp.iter_lines()
if line.startswith("data: ")]
check("chat.error" in kinds2 and not outbound,
"retry same failed turn: honest error again, still zero outbound")
# 7. 关系可调整、名字保留
r = client.patch("/api/relation", json={"relation": "恋人"})
check(r.status_code == 200 and r.json()["relation"] == "恋人" and r.json()["name"] == "临川",
"PATCH /api/relation 200 (relation adjustable, name kept)")
# 8. 日子与回忆:服务端裁剪 + 分页形状(told=1 之外不可见)
r = client.get("/api/life")
check(r.status_code == 200 and r.json()["events"] == [],
"GET /api/life 200 empty (only told events, none yet)")
r = client.get("/api/memories")
check(r.status_code == 200 and r.json()["memories"] == [],
"GET /api/memories 200 empty")
check(client.get("/api/life/1").status_code == 404,
"GET /api/life/1 404 (unshared detail refused)")
bogus = {"message_id": 99999, "turn_id": str(uuid.uuid4()), "epoch": st.epoch,
"token": "x" * 40, "text_hash": "0" * 64}
check(client.post("/api/chat/received", json=bogus).status_code == 404,
"POST /api/chat/received 404 on unknown receipt")
# 9. 语音诚实拒绝:配置指向死地址 → 开房必须 503,绝不建房
r = client.get("/api/voice/status")
check(r.status_code == 200 and r.json()["configured"] is True and r.json()["voice"] is False,
"GET /api/voice/status 200, voice honestly false")
r = client.post("/api/voice/session")
check(r.status_code == 503,
f"POST /api/voice/session honest 503 when TTS unreachable ({r.status_code})")
check(client.delete("/api/voice/session/no-such").status_code == 404,
"DELETE /api/voice/session/{unknown} honest 404")
# 10. 静态页与 dist 哈希(只读)
if args.web:
page = client.get("/")
check(page.status_code == 200 and b"<div id=" in page.content,
"GET / serves web dist index.html")
# 11. 重开:新进程入口、epoch 递增、资料保留
with TestClient(app) as client2:
h2 = client2.get("/api/health").json()
check(h2["epoch"] == 2, f"reopen: epoch=2 (got {h2['epoch']})")
c2 = client2.get("/api/companion").json()
check(c2["name"] == "临川" and c2["relation"] == "恋人" and c2["birth"] == "2001-08-16",
"reopen: companion identity + adjusted relation retained")
m2 = client2.get("/api/messages").json()["messages"]
check(len(m2) == 1 and m2[0]["role"] == "user",
"reopen: chat history retained, still no fabricated replies")
if args.web:
manifest = json.loads((Path(__file__).parent / "dist-manifest.json").read_text())
bad = []
for rel, want in manifest.items():
p = Path(args.web) / rel
if not p.is_file():
bad.append(f"{rel} missing")
continue
import hashlib
got = hashlib.sha256(p.read_bytes()).hexdigest()
if got != want:
bad.append(f"{rel} {got[:12]}!={want[:12]}")
check(not bad, f"dist matches manifest ({len(manifest)} files; bad={bad})")
for k, v in old.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
failed = [label for ok, label in RESULTS if not ok]
print(f"\n{len(RESULTS) - len(failed)}/{len(RESULTS)} checks passed")
if failed:
print("FAILED:")
for f in failed:
print(" - " + f)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,12 @@
# HEL-913 · 语音窗口一次性布防模板(脱敏;默认全 0/未布防,随包不生效)
#
# 获批语音窗口的负责人在窗口启动时显式填写并在启动 worker 前写入
# <实例>/worker/.env(ASR 三项)与本文件(窗口参数)。唯一接线见
# deploy/hel913-unified/README.md §5;建议值口径见
# docs/hel913-capability-and-enablement.md §2 B。
# 已实测同形工具(HEL-910 窗口):docs/rework/hel910/tools/asr-arm.env。
HEL913_WINDOW_ID= # 例 hel913-20261106-a;空=未布防=拒绝一切 ASR
HEL913_ASR_MAX_AUDIO_SECONDS=0 # 全窗口实际送入 ASR 音频硬闸(秒)
HEL913_ASR_MAX_RETRIES_PER_CALL=0 # 每通连接重试硬闸(失败后的再建连)
HEL913_TTS_SEGMENT_CAP=0 # TTS 段数上限(监控口径;到顶即手动 force_end)
HEL913_WINDOW_DEADLINE_EPOCH= # date -d '<第3日> 24:00' +%s;到点 force_end
+23
View File
@@ -0,0 +1,23 @@
# HEL-913 统一体验运行包 · voice 配置模板(脱敏)
#
# 用法:复制到 <实例>/etc/voice.yaml,把 <...> 占位替换成服务器本地真实值。
# livekit_url 是 POST /api/voice/session 发给**浏览器**的接入地址:统一实例
# 页面走 HTTPS(8361),HTTPS 页面连 ws:// 明文会被混合内容策略阻断,因此
# 语音可用的前提是 wss TLS 桥(deploy/voice/wss_bridge.example.py 在新端口
# 终结 TLS、回环转发 7880,与 8361 同一张自签证书)——livekit_url 必须填
# 桥的 wss:// 地址,不是可选件(桥的启停见 README §5)。服务器内部
# (开房/删房/worker 注册)仍走 livekit_internal 的本机明文 7880。
# TTS/面板地址在语音窗口获批启动后指向本机 9880/8090;registry/worker_state
# 落实例数据目录,与产品库同空间,随实例备份。
livekit_url: wss://192.168.200.12:8363
livekit_internal: http://127.0.0.1:7880
api_key: <LiveKit api key,服务器本地>
api_secret: <LiveKit api secret,服务器本地>
agent_name: cybergril-voice-agent
tts_health_url: http://127.0.0.1:9880/health
panel_url: https://127.0.0.1:8090
tts_url: http://127.0.0.1:9880
registry_file: /opt/cybergril-hel913/data/voices.json
worker_state_file: /opt/cybergril-hel913/data/voice_state_cybergril.json
token_ttl_seconds: 300
session_ttl_seconds: 600
+26
View File
@@ -0,0 +1,26 @@
# HEL-913 统一体验运行包 · 语音 worker 环境模板(脱敏)
#
# 用法:复制到 <实例>/worker/.env,把所有 <...> 占位替换成服务器本地真实值。
# worker_ctl.sh 启动前会拒绝:仍含 <...> 占位的文件、解析失败的文件、缺文件。
# 运行解释器是 agent venv(livekit-agents 1.8.8),不是产品 .venv311。
CYBERGRIL_API_BASE=http://127.0.0.1:8360
CYBERGRIL_AGENT_NAME=cybergril-voice-agent
LIVEKIT_URL=ws://127.0.0.1:7880
LIVEKIT_API_KEY=<LiveKit api key,服务器本地>
LIVEKIT_API_SECRET=<LiveKit api secret,服务器本地>
CYBERGRIL_WORKER_HEALTH_PORT=8082
CYBERGRIL_VOICE_STATE=/opt/cybergril-hel913/data/voice_state_cybergril.json
# ASR 用量账本独立于产品库,落在实例数据目录(逐通历史不随删房消失):
CYBERGRIL_ASR_USAGE_LEDGER=/opt/cybergril-hel913/data/asr-usage.jsonl
# —— ASR 一次性窗口闸(HEL-906;本段是布防接线,默认未布防 = 真实 cap=0)——
# 三项保持空/0 时,worker 拒绝一切真实 ASR 新连接与新音频(fail-closed,
# 拒绝本身落账,src/cybergril/voice/asr_usage.py)。只有获批语音窗口的
# 负责人在窗口启动时显式改这里并重启 worker(接线与建议值见 README §5
# 与 docs/hel913-capability-and-enablement.md §2 B):
# CYBERGRIL_ASR_WINDOW_ID=hel913-<日期>-a # 新窗口 id;空=未布防
# CYBERGRIL_ASR_WINDOW_MAX_AUDIO_SECONDS=1800 # 3 通×10 分钟=30 分钟硬闸
# CYBERGRIL_ASR_MAX_CONNECT_RETRIES_PER_CALL=2 # 每通连接重试上限
CYBERGRIL_ASR_WINDOW_ID=
CYBERGRIL_ASR_WINDOW_MAX_AUDIO_SECONDS=0
CYBERGRIL_ASR_MAX_CONNECT_RETRIES_PER_CALL=0
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# HEL-913 统一体验运行包 · 专属语音 worker 启停
#
# 运行解释器是 agent venv(livekit-agents 1.8.8),产品代码经 PYTHONPATH 注入。
# worker 只连接本实例 API 与本机 LiveKit。本脚本只做本实例常规启停安全:
# - 子命令只认 start|stop|status,其他一律拒绝退出 2(不会静默当 start);
# - start 前核对 pid 句柄与 worker/.env(缺文件/含未填 <...> 占位/解析失败
# 一律拒绝),重复 start 拒绝,句柄 pid 复用拒绝;
# - stop/失败收尾只杀经身份核验的进程(cmdline 含 cybergril.voice.worker
# 且 cwd 是本实例 worker 目录),绝不按进程名扫杀;
# - start 注册检查不过 → 清掉自己刚起的进程、保留日志、退出 1。
#
# 用法: [ENV=..] bash deploy/hel913-unified/worker_ctl.sh start|stop|status
# 环境变量:
# HEL913_BASE 实例根目录(含 current/ worker/ 等)
# HEL913_AGENT_VENV agent venv python(默认 /opt/voice-agent/agent/venv/bin/python)
# HEL913_WORKER_HEALTH 健康端口(默认 8082;与 worker/.env 中一致)
set -u
BASE=${HEL913_BASE:-/opt/cybergril-hel913}
VENV=${HEL913_AGENT_VENV:-/opt/voice-agent/agent/venv/bin/python}
HEALTH=${HEL913_WORKER_HEALTH:-8082}
WDIR=$BASE/worker
ENVF=$WDIR/.env
LOG=$BASE/logs/worker.log
PIDF=$BASE/logs/worker.pid
MARKER="cybergril.voice.worker"
usage() { echo "usage: [ENV=..] bash $0 start|stop|status" >&2; exit 2; }
ours() {
local p=$1 cmd cw
case "$p" in ''|*[!0-9]*) return 1 ;; esac
[ "$p" != "$$" ] || return 1
kill -0 "$p" 2>/dev/null || return 1
cmd=$(tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null) || return 1
cw=$(readlink "/proc/$p/cwd" 2>/dev/null) || return 1
case "$cmd" in *"$MARKER"*) ;; *) return 1 ;; esac
[ "$cw" = "$WDIR" ] || return 1
return 0
}
pidfile_pid() { [ -f "$PIDF" ] && cat "$PIDF" 2>/dev/null; }
stop() {
local p
p=$(pidfile_pid)
if [ -z "$p" ]; then
echo "no pid file; nothing stopped"
return 0
fi
if kill -0 "$p" 2>/dev/null && ! ours "$p"; then
echo "REFUSED: pid $p is alive but is NOT this worker (pid reuse?) — not killing; resolve $PIDF manually" >&2
return 1
fi
if ours "$p"; then
kill "$p"
sleep 3
if ours "$p"; then kill -9 "$p"; sleep 1; fi
fi
rm -f "$PIDF"
echo "worker stopped (pid $p)"
return 0
}
start() {
local p bad
p=$(pidfile_pid)
if [ -n "$p" ] && kill -0 "$p" 2>/dev/null; then
if ours "$p"; then
echo "REFUSED: worker already running (pid $p) — stop first" >&2
else
echo "REFUSED: pid file has live pid $p that is not this worker (pid reuse?) — resolve $PIDF manually" >&2
fi
exit 1
fi
rm -f "$PIDF"
[ -d "$WDIR" ] || { echo "REFUSED: worker dir missing: $WDIR" >&2; exit 1; }
[ -f "$ENVF" ] || { echo "REFUSED: $ENVF missing (copy worker.env.template and fill it)" >&2; exit 1; }
[ -x "$VENV" ] || { echo "REFUSED: agent venv python not executable: $VENV" >&2; exit 1; }
# 占位符守门:模板 <...> 未替换一律拒绝(不让占位串进进程环境)
bad=$(grep -nE '^[A-Za-z_][A-Za-z0-9_]*=.*<' "$ENVF" || true)
if [ -n "$bad" ]; then
echo "REFUSED: $ENVF still has unfilled <...> placeholders:" >&2
echo "$bad" >&2
exit 1
fi
# 解析守门:.env 必须能作为 KEY=value 被 source(坏引号/裸空格拒绝)
if ! (set -a; . "$ENVF"; set +a) 2>/dev/null; then
echo "REFUSED: $ENVF does not parse as KEY=value lines" >&2
exit 1
fi
mkdir -p "$BASE/logs"
cd "$WDIR" || exit 1
set -a
. "$ENVF"
set +a
PYTHONPATH="$BASE/current/src" nohup "$VENV" -m cybergril.voice.worker start > "$LOG" 2>&1 &
p=$!
echo "$p" > "$PIDF"
echo "worker starting (pid $p)"
sleep 18
if grep -m1 "registered worker" "$LOG" >/dev/null; then
echo "OK: worker registered"
return 0
fi
echo "FAIL: worker did not register — full log kept at $LOG; tail:" >&2
tail -8 "$LOG" >&2
if ours "$p"; then
kill "$p"; sleep 2
ours "$p" && kill -9 "$p"
fi
rm -f "$PIDF"
echo "cleaned up failed start (log kept: $LOG)" >&2
exit 1
}
status() {
local p
p=$(pidfile_pid)
if [ -n "$p" ]; then
if ours "$p"; then echo "running pid $p (identity verified)"
elif kill -0 "$p" 2>/dev/null; then echo "pid $p alive but NOT ours (stale pid file?)"
else echo "pid $p not alive (stale pid file)"; fi
else
echo "no pid file"
fi
curl -s --max-time 3 "http://127.0.0.1:$HEALTH/" >/dev/null && echo "health ok" \
|| echo "health port not answering"
}
case "${1:-}" in
start) start ;;
stop) stop ;;
status) status ;;
*) usage ;;
esac
+6 -4
View File
@@ -1,11 +1,13 @@
# CyberGril 语义记忆层固定栈清单(HEL-882;B 部分批准后由发布卡拉取)
# 固定源:mem0ai 2.2.1(源码提交 2c81af3bbe27d9bccb09c772d583df9e9be72ee5,
# tarball SHA256 9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23)。
# 用于 deploy/hel874-release.md §4:在具备内网源权限的环境按 3.11 平台拉全
# wheel 后离线并入发布 venv;本卡不执行下载。版本来自 hel857-venv 实测安装
# HEL-879 唯一安装入口:deploy/hel895-release-candidate.md §4(固定 tarball 哈希 →
# 固定源码构建轮 → cp311 依赖准备 → 仅本地轮安装 → 同进程 co-import)。
# 本文件的 mem0ai==2.2.1 行只作版本记录——不得按此行向索引拉同名 wheel 冒充固定源
# (旧 deploy/hel874-release.md §4 已被取代)。若某包无 3.11 wheel,发布卡须如实
# 上报缺项,不得换版本号冒充固定安装。版本来自 hel857-venv 实测安装
#(Python 3.10.12 侧 mem0ai 2.2.1 / qdrant-client 1.20.0 / fastembed 0.9.1),
# 3.11 侧需同版本 wheel;若某包无 3.11 wheel,发布卡须如实上报缺项,不得
# 换版本号冒充固定安装。
# 3.11 侧需同版本 wheel。
mem0ai==2.2.1
qdrant-client==1.20.0
fastembed==0.9.1
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# CyberGril 专属语音 worker 运维脚本草案(HEL-877,本卡未执行)。
# 用法: bash /opt/voice-agent-cybergril/restart_cybergril.sh [stop|status]
# 边界: 只管本 worker(python -m cybergril.voice.worker),绝不匹配通用
# worker 的 `agent.py start`,不动 /opt/voice-agent 与 8090/8081。
set -u
BASE=/opt/voice-agent-cybergril
cd "$BASE"
VENV=/opt/voice-agent/agent/venv # 复用已装 livekit-agents 1.8.8 的 venv
PAT='cybergril.voice.worker start' # 精确匹配本 worker 命令行
for p in $(pgrep -f "$PAT"); do kill "$p" 2>/dev/null; done
sleep 2
for p in $(pgrep -f "$PAT"); do kill -9 "$p" 2>/dev/null; done
if [ "${1:-}" = "stop" ]; then echo "cybergril voice worker stopped"; exit 0; fi
if [ "${1:-}" = "status" ]; then
pgrep -af "$PAT" || echo "not running"
curl -s --max-time 3 "http://127.0.0.1:${CYBERGRIL_WORKER_HEALTH_PORT:-8082}/" \
&& echo " <- health" || echo "health port not answering"
exit 0
fi
set -a; source "$BASE/.env"; set +a
PYTHONPATH="$BASE" nohup "$VENV/bin/python" -m cybergril.voice.worker start > worker.log 2>&1 &
echo "cybergril voice worker starting (pid $!)"
sleep 18
grep -m1 "registered worker" worker.log >/dev/null && echo "OK: worker registered" \
|| { echo "FAIL:"; tail -5 worker.log; exit 1; }
+105
View File
@@ -0,0 +1,105 @@
"""统一 CyberGril 实例单进程双入口启动器(HEL-877 部署草案,本卡未执行).
HTTP 8360 与 HTTPS 8361(语音页需要 secure context 才能拿麦克风)必须共用
同一个 API 进程、同一份 app.state(epoch / chat_lock / store / 语音会话登记)。
HEL-879 收口(总管复核第 1 项):uvicorn 两个 Server 默认各自进入一次 FastAPI
lifespan,导致 epoch 1→2、Store/life/chat_lock 全部被第二次初始化替换——同一个
app 对象并不保证只初始化一次。本启动器改为:
- 应用生命周期由本启动器**独占管理且只进入一次**(`app.router.lifespan_context`);
- 两个 uvicorn Server 以 `lifespan="off"` 只负责监听,共享同一份状态;
- 任一监听退出(失败/取消/信号)先把另一个也停掉,**再**退出生命周期:
绝不出现一边还在接请求、一边已被另一个监听提前关库;循环与资源只关闭一次;
- `create_app` 的 lifespan 自带重入门卫:任何二次进入直接报错(fail-closed),
防止未来改回 auto 双 Server 时再次静默替换状态。
自签证书签发命令与回滚步骤见 docs/runbook-hel877-voice.md。
用法(部署卡执行,先 `cd` 到仓库根并激活 3.11 venv):
PYTHONPATH=src python deploy/voice/serve_dual_8360_8361.py \
--http-port 8360 --https-port 8361 \
--cert /opt/cybergril-voice-certs/cert.pem --key /opt/cybergril-voice-certs/key.pem
本地离线冒烟(不涉及任何现网/证书变更):
PYTHONPATH=src python deploy/voice/serve_dual_8360_8361.py \
--http-port 18360 --https-port 18361 --cert <本地测试证书> --key <本地测试密钥>
"""
from __future__ import annotations
import argparse
import asyncio
import contextlib
import os
import sys
import uvicorn
from cybergril.api.app import create_app
async def serve(http_port: int, https_port: int, cert: str, key: str, app=None) -> None:
# ONE app object, ONE lifespan owner: both listeners share state, locks,
# epoch and the store. The uvicorn servers never run the app lifespan
# themselves (lifespan="off") — the context manager below is its only
# owner, so initialization and shutdown each happen exactly once.
if app is None:
app = create_app()
http_server = uvicorn.Server(
uvicorn.Config(app, host="0.0.0.0", port=http_port, log_level="info",
lifespan="off")
)
https_server = uvicorn.Server(
uvicorn.Config(app, host="0.0.0.0", port=https_port, ssl_certfile=cert,
ssl_keyfile=key, log_level="info", lifespan="off")
)
async with app.router.lifespan_context(app):
servers = [asyncio.create_task(http_server.serve()),
asyncio.create_task(https_server.serve())]
try:
done, pending = await asyncio.wait(servers,
return_when=asyncio.FIRST_COMPLETED)
except asyncio.CancelledError:
# The launcher itself was cancelled: stop both listeners first
# (graceful should_exit, hard cancel only as a last resort), await
# their full exit, only then let the lifespan close (and re-raise).
# No listener is ever left running without its state.
for server in (http_server, https_server):
server.should_exit = True
for task in servers:
with contextlib.suppress(asyncio.CancelledError, Exception):
await asyncio.wait_for(asyncio.shield(task), timeout=5.0)
for task in servers:
task.cancel()
with contextlib.suppress(asyncio.CancelledError, Exception):
await task
raise
# Any listener ending stops the other FIRST; only after both listeners
# have fully exited does the lifespan context above close the loop's
# resources and the store — never the other way around.
for server in (http_server, https_server):
server.should_exit = True
for task in pending:
with contextlib.suppress(asyncio.CancelledError, Exception):
await task
for task in done:
task.result() # surface the first listener's real error, after cleanup
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--http-port", type=int, default=8360)
parser.add_argument("--https-port", type=int, default=8361)
parser.add_argument("--cert", required=True)
parser.add_argument("--key", required=True)
parser.add_argument("--db", default=os.environ.get("CYBERGRIL_DB"))
args = parser.parse_args()
if args.db:
os.environ["CYBERGRIL_DB"] = args.db
asyncio.run(serve(args.http_port, args.https_port, args.cert, args.key))
return 0
if __name__ == "__main__":
sys.exit(main())
+18
View File
@@ -0,0 +1,18 @@
# CyberGril 语音 worker 脱敏环境变量示例(HEL-877)
#
# 部署位置 /opt/voice-agent-cybergril/.env(权限 600,不进仓库)。
# 复用 /opt/voice-agent/agent/venv(livekit-agents 1.8.8)与 .12 现有
# LiveKit(7880)/FunASR(10095)/CosyVoice(9880);不新装任何依赖。
# 统一 CyberGril 实例(HTTP 8360 / HTTPS 8361 同进程双入口)
CYBERGRIL_API_BASE=http://127.0.0.1:8360
# 专属派发名(与 config/voice.yaml 的 agent_name 一致)
CYBERGRIL_AGENT_NAME=cybergril-voice-agent
# worker 注册 LiveKit 所需凭据(与通用 worker restart 脚本同源;只放占位符)
LIVEKIT_URL=ws://127.0.0.1:7880
LIVEKIT_API_KEY=<LIVEKIT_API_KEY_PLACEHOLDER>
LIVEKIT_API_SECRET=<LIVEKIT_API_SECRET_PLACEHOLDER>
# 专属健康端口(livekit-agents worker 内建 GET / 健康检查;8081 是通用 worker)
CYBERGRIL_WORKER_HEALTH_PORT=8082
# CyberGril 自己的音色状态文件(不读通用助手 voice_state.json)
CYBERGRIL_VOICE_STATE=/opt/voice-agent-cybergril/voice_state_cybergril.json
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""CyberGril R5 · WSS TLS bridge (PROPOSAL — 拟议材料,未在任何服务器执行).
背景(docs/rework/hel879/unified-review/r5-facts-20261027.md E2/E4):.12 的
LiveKit 7880 是纯 HTTP/WS 且通用 worker/8090 演示都依赖它保持明文,不能改
TLS;HTTPS 页面直连 ws:// 又是混合内容。本脚本在**新端口**终结 TLS 并把字节
流透明转发给本机 7880,使浏览器可用 ``wss://192.168.200.12:<port>`` 接入
LiveKit 信令,零新依赖(asyncio + ssl 标准库),不触碰既有服务。
- 监听(拟议):0.0.0.0:8363,证书 = 与 8361 同一张自签证书
(SAN=IP:192.168.200.12,存 /opt/cybergril-voice-certs/,600)。
- 上游:127.0.0.1:7880(仅回环,不暴露第二明文入口)。
- voice.yaml:livekit_url 改 wss://192.168.200.12:8363;
livekit_internal 保持 http://127.0.0.1:7880。
- 实现为透明 TCP TLS 终结(不解析 WebSocket/HTTP),LiveKit 信令是 7880 上的
HTTP/WebSocket,字节级转发即协议无关;WebRTC 媒体面(UDP/7881)不经本桥,
由 WebRTC 自带 DTLS/SRTP 加密。
- 每设备分别打开 https://192.168.200.12:8361 与 :8363,验证证书例外和实际
连接;不推断例外缓存范围。禁止用 --ignore-certificate-errors 冒充验收。
部署(获批后)示例:
python3 wss_bridge.example.py --listen 0.0.0.0:8363 \
--upstream 127.0.0.1:7880 \
--cert /opt/cybergril-voice-certs/cybergril-voice.pem \
--key /opt/cybergril-voice-certs/cybergril-voice.key
回滚:停本进程即可(独立进程,不影响 LiveKit/通用 worker/8090)。
本地冒烟(零网络出站):见
docs/rework/hel879/unified-review/r5-facts-20261027.md 与
docs/rework/hel879/unified-review/wss-bridge-smoke.log(一次性本地证书 +
dummy 上游回环验证字节穿透与 TLS 握手)。
"""
from __future__ import annotations
import argparse
import asyncio
import contextlib
import ssl
import sys
async def _pipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
try:
while True:
data = await reader.read(65536)
if not data:
break
writer.write(data)
await writer.drain()
except (ConnectionError, asyncio.CancelledError, ssl.SSLError):
pass
finally:
with contextlib.suppress(Exception):
writer.close()
async def _handle(
downstream: tuple[asyncio.StreamReader, asyncio.StreamWriter],
upstream_host: str,
upstream_port: int,
) -> None:
down_reader, down_writer = downstream
try:
up_reader, up_writer = await asyncio.open_connection(upstream_host, upstream_port)
except (ConnectionError, OSError):
down_writer.close()
return
await asyncio.gather(
_pipe(down_reader, up_writer),
_pipe(up_reader, down_writer),
return_exceptions=True,
)
async def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="TLS->plain TCP bridge (CyberGril R5 WSS proposal)")
parser.add_argument("--listen", default="0.0.0.0:8363", help="host:port to terminate TLS on")
parser.add_argument("--upstream", default="127.0.0.1:7880", help="plain upstream host:port")
parser.add_argument("--cert", required=True, help="TLS certificate PEM path")
parser.add_argument("--key", required=True, help="TLS private key path")
args = parser.parse_args(argv)
listen_host, _, listen_port = args.listen.rpartition(":")
up_host, _, up_port = args.upstream.rpartition(":")
if not listen_host or not up_host:
parser.error("listen/upstream must be host:port")
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(args.cert, args.key)
async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
await _handle((reader, writer), up_host, int(up_port))
server = await asyncio.start_server(
handler, listen_host or "0.0.0.0", int(listen_port), ssl=ctx
)
addrs = ", ".join(str(s.getsockname()) for s in server.sockets or [])
print(f"wss-bridge(PROPOSAL) tls {addrs} -> {up_host}:{up_port}", file=sys.stderr)
async with server:
await server.serve_forever()
return 0
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))
+73
View File
@@ -0,0 +1,73 @@
{
"base": "bf234570c0b6595a72f8dbc79e42c63685e08e01",
"patch_sha256": "02a7b3d2c03aee79c9fc7c8a470c7053cd7b8aa410699ec51b255c333acdeda4",
"source_zip_sha256": "4596f2cc03210074be1fc9fe23dcdd607f5d240b16f142b67986c1886ce6e9be",
"files": [
{
"path": "docs/requirements/handoff-HEL-878.json",
"sha256": "258cf2935f5aafc20cd7a80a9550ae062024849ff39c078068f371692bbaa516",
"bytes": 9439
},
{
"path": "web/README.md",
"sha256": "f596492a443e7ae9972496a40db2f40c79f8667d4552628afa4718efbd29c726",
"bytes": 3113
},
{
"path": "web/VENDOR.md",
"sha256": "e11c139b5c7a6f86264439b03997bd89c79e40c994ac1db57f31717e83b660b6",
"bytes": 893
},
{
"path": "web/public/vendor/livekit-client-2.22.4.umd.min.js",
"sha256": "040db4c0db72e7f7599cd0e06b0f58f95e0082a395cdde410ca26224a79a0626",
"bytes": 598796
},
{
"path": "web/src/app/RealApp.tsx",
"sha256": "8afb63b8a6b196e29ced1a8bcef541f1f80a10811f1dc0f965631a9f0c201f5e",
"bytes": 14382
},
{
"path": "web/src/chat/ChatPanel.tsx",
"sha256": "419c2f4243d7a147ea8ebcf377d74c97dc558049a3bf76dee39bba0746d2c9e6",
"bytes": 34936
},
{
"path": "web/src/styles/components.css",
"sha256": "f93b875f6918d402aaaefeffdc52fdd908328fb8ac70766704e528c6a8bc154d",
"bytes": 89243
},
{
"path": "web/src/voice/livekit.ts",
"sha256": "c42475d37f7eef7abe0903b66656aedc5379bc9de3dcba11c1b88450a470ab04",
"bytes": 6859
},
{
"path": "web/src/voice/session.ts",
"sha256": "128123ab75e788699150321ca1341081450c34b09918b0b825c542dbc60b6e07",
"bytes": 18058
},
{
"path": "web/tests/fake-livekit.js",
"sha256": "d16e794308073e967566f3ebda16e72d0f5eb0132b657338baa6a29115b02508",
"bytes": 4177
},
{
"path": "web/tests/hel878_mock.mjs",
"sha256": "d7c9d79308979f1f3cd7172f56f52b4cfb35de1fcbae2876fa7d66ea4e2c3071",
"bytes": 8855
},
{
"path": "web/tests/hel878_shots.mjs",
"sha256": "ad664b26b66990e59ca118b0afe1e6dfea8a64351bb9bbb7a666f332b2f15760",
"bytes": 22874
},
{
"path": "web/tests/voice.test.ts",
"sha256": "f6bc899b3202465ec68b276ed21ccbbc5ba6e7edda014e2d7ea3ff63e5683a73",
"bytes": 20829
}
],
"unchanged_archive_files_match_base": true
}
+20
View File
@@ -0,0 +1,20 @@
HEL-878 源码保全与补查(总管,2026-10-27 北京时间)
原作者提交评论:01a1797c-188d-79df-9be1-df6bd47effc6。读取需求 v1.11/60项;前端基点 bf234570c0b6595a72f8dbc79e42c63685e08e01,后台契约 aba2aeb,实际总装基点 04eff611e7d85e539d43717711172441420ac853,main 3a986726d8593e0ed2274f79db6cc5d807d2ebf0。
13 个改动文件逐字节匹配作者 MANIFEST;补丁 SHA-256 匹配,git apply --reverse --check 通过;源码包其余文件与 bf234570 内容一致(仅允许未改文件 CRLF/LF 差异),包内 MANIFEST 与单独附件一致。原样提交 b95ce798b4a9455d941d247a46865b3ac202bf2c,不改作者业务代码或视觉。
补齐前端分支缺少的历史用户原话文件 comment-evidence-full-experience-20261026.json 及 source-manifest,逐字节来自总装 04eff611;无需求变更。Python 原版检查对 origin/main、origin/hel-870-assembly、origin/agent/2/6a08a4c18f23 各带/不带 handoff-HEL-878.json 共6次通过。
使用既有 node_modules(未安装下载)复跑 typecheck、lint、81项单测、build 全通过;lint 0错/1条既有警告。作者45项浏览器检查本轮未重跑,仍为作者证据。真实语音、真实麦克风、WebRTC、ASR/TTS/LLM请求均未执行;未部署、未合并、未改现有服务或绑定。
独立补查发现必须返修的问题:
1. 第一次会话正常开启;模拟掉线,服务端 DELETE s1 延迟返回。
2. 用户立即开启第二次会话,worker已加入,界面 listening,controller.active=true。
3. 放行第一次的 DELETE(含 settle 补偿请求)。旧 fail() 在 await teardown(live) 之后继续写 OFF,没有重新核对代次,覆盖新会话的界面。
4. 实测:controller.active=true,但最后 onView.phase=off。旧失败提示也会覆盖新会话,界面与麦克风会话状态不一致。
定位 web/src/voice/session.ts 的 start() 内 fail()。附件 review878_late_cleanup.test.ts 为独立负例,放到 web/tests/ 后用 ./node_modules/.bin/vitest run tests/review878_late_cleanup.test.ts 可复现;本轮实际执行失败:Expected listening / Received off,原始日志见 review878-race.log。这里只做有限复现,未代替原作者修改;交原作者修复异步清理后的状态提交边界并补有意义回归。
关联 CG-010/018、VIS-013、UX-005/006、GOV-001–006、DEL-001。原81项通过不涵盖这个负例,不能称离线验收通过。修正后交 HEL-879 统一审核;CG-011语音主动开口仍为空操作、增量字幕仍缺、远端断网清理/租约与API重启实情、真实SDK回调与播放回执、真机三轮/打断/停止/HTTPS均继续承接,不因协议现状或mock通过降低完整要求。R6/R7继续主卡HEL-797承接。
+14 -6
View File
@@ -113,16 +113,24 @@ HEL-857 的 60 次实验授权);B/C 需白栖知明确批准。
`docs/budget-plan-hel874.md` §1 体验链 1–7 逐步执行:创建/诠释/习惯 4、对话 16、
生活 8、判定/成长/gist 20、主动措辞 3,其余额度备用至 55;每步在四道闸下原子
预占(类别日顶→日 token 池→一次性总量;S1 验证叠加取最紧)。
2. **A 结束即封账**,两种到达方式同义:额度用满(`budget_total_requests_exhausted:55/55`
如实拒绝、不补发)或体验链走完。封账动作:`systemctl stop` 8360 实例 →
`tools/release_snapshot_hel896.py` 做一致性快照(停服+checkpoint/backup API)→
归档(数据目录 + 哈希清单)交交付评论。封账跨重启、跨归档不失效(S2 验证),
2. **A 结束即封账**,两种到达方式都有可执行动作(HEL-879 收口):额度用满时
`budget_total_requests_exhausted:55/55` 如实拒绝、不补发;**体验链提前走完
(如 51/55)时运行 `tools/seal_budget_hel879.py --db <团队实例库>` 显式封账**
——真实用量 51 原样写入并保留(不伪造 55、不清零),此后该库任何类别任何
余量的 `acquire` 都拒绝 `budget_sealed:51/55`,跨重启、跨 backup API 归档/
快照恢复同样拒绝(S6 负例:51/55 提前收尾、误重启、归档恢复均拒绝;重复
seal 幂等不覆盖原标记;无解除封账代码路径)。封账动作顺序:`systemctl stop`
8360 实例 → `seal_budget_hel879.py` → `tools/release_snapshot_hel896.py`
一致性快照(封账标记随库一同归档)→ 归档(数据目录 + 哈希清单)交交付评论。
`budget_total` 永不手工清零。
3. **旧测试数据不会转成用户伴侣**:测试档案、聊天、生活数据只存在于团队实例库与
归档;用户实例是全新空库,由用户自己创建档案;产品无任何「测试库导入用户库」
路径,发布卡明令禁止把 8360 数据目录复制/合并进用户实例数据目录。
4. 后续任何团队复检 = 新批准 + 新实例/新库(或在 8360 实例上以新批准抬
`total_requests`),绝不续在用户账上。
4. 后续任何团队复检 = **新批准 + 全新隔离实例/新库**,绝不续在用户账上,也
**绝不在已封账的 8360 库上抬 `total_requests` 重开**(HEL-879 返修勘误:本节
旧版括注「或在 8360 实例上以新批准抬 total_requests」与「无解除封账路径」
自相矛盾——封账标记随库持久,抬 cap 不能也不得绕过它;该括注废除)。旧
8360 库及其封账标记原样归档留存,作为真实用量的证据链。
### 6.3 B/C 的开始与日常
+135
View File
@@ -0,0 +1,135 @@
# HEL-913 · 一页能力清单与一次真实启用建议
依据:产品基线 `4a38106`(HEL-912 审核通过)+ 运行包提交(仅 deploy/docs,
见运行包 README §0)、HEL-907/910 真实窗口记录、HEL-913 离线验证 34/34
(`docs/rework/hel913/verify-offline-run.txt`;其中依赖核对只打印 4 个关键
包版本、未分享裁剪只在空库验证——口径限制见运行包 README §7)。分级只分
三种:**✅接好且已验证**(真实窗口或真实服务验证过)/ **🟡有实现、缺真实验证**
(代码与离线/替身验证在,真实链路未跑或未批预算)/ **❌尚未接入**。不照抄
旧表:语音已接(HEL-907/910 实测),下述端口/能力以当前源码为准。
## 1. 能力清单(按用户使用顺序)
| # | 用户动作 | 现状 | 具体行为与证据 |
| --- | --- | --- | --- |
| 1 | 打开网页 | ✅ | HTTPS 入口出界面(自签证书需浏览器「高级→继续」);`GET /` 出 index.html、dist 8 文件哈希核过(HEL-913 验证)。导航:此刻/交谈/日子/设置(`web/src/app/RealApp.tsx`) |
| 2 | 自己给她起名、选生日、选关系 | ✅(起名/生日/关系)/ 🟡(自主诠释与初始习惯) | 用户亲选关系(不预选,AM-002)、亲起名(AM-003);创建守门 404/409、八字底色落库只读、公历农历四柱展示(`src/cybergril/api/app.py:427`、`src/cybergril/core/birth_view.py`)。**自主诠释/习惯生成(persona)从未真实跑过**:所有真实窗口 background=0 诚实拒绝(HEL-910 预检证实),首次用户体验需预算 |
| 3 | 文字聊天 | ✅ | 真实网页 Enter 发送、SSE 流式回复、完整落库;语音在播/待播时发文字打断=旧声停+完整文字回复+语音恢复(HEL-910 两通实测);**生成中的文字取消未取得有效真实样本**(HEL-908 的修复只有离线验证,HEL-910 想测没测成,如实记未验证);失败轮诚实 error 不伪造 |
| 4 | 她的生活(自己运行) | 🟡 | 生活循环/活动线程/分享判定/回执代码接好(`src/cybergril/life/`);未分享生活服务端裁剪(told=1,AM-001)空库离线验证(未做正反种子对照);**真实生活事件生成未发生**(life 预算 0),日子页真实模式下尚无真实事件可看 |
| 5 | 记忆 | 🟡(分两层) | 聊天记忆:逐轮落库、遗忘下限、词面检索(FTS)已接,真实窗口对话随轮落库(团队测试库);**语义记忆(真实 Mem0)缺依赖**:.12 venv311 四项未装(2026-10-29 实测),完整安装与接线见运行包 README §8;激活还需 B 部分嵌入/提取预算 |
| 6 | 关掉网页再回来 | ✅ | 同一位她保留:聊天/关系/八字/记忆持久;epoch 递增同步、分页缺口不跳留言、未读标记(HEL-895/896 真实服务浏览器 9 项检查;HEL-913 离线重开验证) |
| 7 | 她主动留言 | 🟡 | 时机判定+网页内留言卡+回来即看的前端链路已接并经真实服务浏览器验证(种子数据);**真实 LLM 主动措辞生成未发生**(proactive 预算 0);不接手机推送(CG-011 首版范围) |
| 8 | 语音通话 | ✅(工程验证) | 真实网页开通话、连续对话、字幕同步、挂断删房;文字打断在播旧声实测停(作者粗略观察约秒级,原始探针未保留,**不作精确口径、不承诺可独立精测**);首声原实测 9.53s,HEL-911 移除 3.66s 等待后**未实测**(不向用户承诺任何推算值);真人麦克风打断、生成中文字取消的正面样本未取得(承接) |
| 9 | 自定义声音 | ✅(工程验证) | 网页上传参考音频→登记→真 TTS 试听→应用→下一通生效;通话中改选不换声(HEL-907 全链 13/14 步实测,call1/2 以 cg- 音色真实合成;第 14 步真机复测未做);用户本人尚未亲自用过 |
| 10 | 换城市/天气跟随 | ❌ | 设置页城市项为演示(`web/src/manage/ManagePanel.tsx` cap=demo/off);后端无天气源(CG-023 承接 R6,T-07) |
| 11 | 换外观素材 | ❌ | 设置页外观项为演示;素材规范/导入未定稿(CG-005/030,T-03,R6;视觉参考见 PR#8 prototypes/hel-793/) |
| 12 | 备份导出/每日自动备份 | ❌(演示导出除外) | 设置页「本地演示导出」为演示件;CG-009 每日自动备份+手动完整导出未实现(R6);现阶段只有发布归档工具 `tools/release_snapshot_hel896.py`(工程用,不冒充 R6) |
| 13 | 摄像头观察 | ❌ | 无视觉服务接入;CG-021/022(R7),4080 未触,部署由用户负责(T-05) |
| 14 | 真实人物口型/交谈半身 | ❌ | 交谈时人物为静帧参考图(`assets/her-talk.jpg`);成品视频选片与实时驱动器未选型(CG-029,T-09;8340/ComfyUI 停机) |
机器可读口径:`GET /api/health` 的 `capabilities`(voice/vision/weather/materials/
backup=false 为诚实声明;text=true)。
## 2. 一次真实启用建议(**全部待批准**;当前配置仍全 0,旧窗口余额不结转)
> 更正声明:本节早前写「A 总管权限内新批、不用问用户」——与本轮真实
> cap=0 及主卡授权矛盾,**作废**。所有新增真实用量、安装、部署(含 A)
> 一律待批,总管不自行放行;批准前一切 cap=0。三本账彼此独立(独立实例/
> 独立库,HEL-896 §6 分账语义),任何一本用完不自动转另一本;所有硬闸
> 发送前拒绝、碰顶即停、**封账不解除**;提额永远是新批准。费用均为内网
> 自持服务(LLM 网关 .11:8731、TTS/ASR/LiveKit 在 .12),**无新增现金支出**,
> 消耗的是授权额度与本机资源。用户不需要理解 token/EOU,也不需要选算法
> 或决定"要不要真记忆"——工程侧按批准直接配置。
### A · 团队验收检查(建议顶:55 次 / 500,000 tokens / 0 嵌入——待批)
- 沿用 `docs/budget-package-hel895.md` §6 的形状:**独立团队实例**自己的
SQLite 账,体验链 1–7 走完(创建→诠释→对话→生活→情绪关系→主动联系→
重开同步,即本清单 🟡 项的团队级补证)即 `seal_budget_hel879.py` 封账
归档。
- A 是团队实例的账:**不并入用户实例、不给用户实例配置一次性总量**
(一次性总量只属于团队测试实例,见 budget-package-hel895 §6)。
### B · 用户首体验(一次批准;最小够用)
- **前置(窗口开始前,团队完成,不占用户额度)**:如批准真实记忆,先按
运行包 README §8 一次性窗口完成安装+co-import+缓存落位(约 1–2 小时
含验证;+450–550MB 磁盘);不批安装则记忆停在词面检索(stub 已验证
全链可跑),其余体验不受影响。四步检查(安装→独立部署→团队检查→用户
首用)见运行包 README §4/§8 与本节。
- **窗口与时点**:建议 3 个自然日,服务器本地时区(Asia/Shanghai)。T0 =
团队完成检查、把地址交给用户的时刻(如周五 19:00 交付,则周日 24:00
到期);日账按本地自然日零点重置;团队准备耗时全部在 T0 之前。
- **文字与生活(models.yaml 日账,程序硬闸)**:对话 60 轮/日、生活 8 次/
日、判定/成长/摘要 40 次/日、日 token 池 2,000,000、创建+诠释+习惯一次
性 4 次。**语音通话里的对话轮同样计 dialogue 日账**(不另开语音 LLM 账)。
- **记忆(若批安装)**:提取 LLM 累计 60 次(`llm_ops`)、新增嵌入累计
180 条(累计口径 `embed_cap=680`、冻结 2 不动,见运行包 README §8.4)、
语义类 40 次/日;嵌入日顶60条,提取日顶40次(两项按UTC切日,即北京时间08:00)。
- **语音(3 通 × ≤10 分钟 = 合计 ≤30 分钟)**:ASR 音频合计 ≤1800 秒
(程序硬闸,发送前拒绝,重启进程不重置);每通连接重试 ≤2(硬闸);
每通墙钟 ≤10 分钟(session_ttl 600 秒硬顶之内,超时房间失效);TTS
合成 ≤60 段(监督计数,到顶人工仅关闭语音服务,不停文字生活;程序不硬闸);建房即用即删(开房失败 503 不重试建房;挂断清理
失败留账自动重试);声音上传登记+试听合计 ≤10 次(监督口径:由陪同
团队按次计数,程序不硬闸)。需要用户当场在浏览器允许麦克风(HTTPS+
自签证书「高级→继续」)。自定义声音当场可用(上传→试听→应用)。
- **碰顶后果(逐项如实)**:LLM 日顶→当次诚实拒绝、界面报错、次日恢复;
ASR 触顶→发送前拒绝、本通结束、全窗不再接受新音频;TTS 段顶→陪同执行者关闭语音入口和worker/桥(文字生活仍可用);窗口到期→统一停服封账(见下)。
- **截止停服(可执行,不口头承诺)**:`force_end.template.sh watch <到期
epoch>`(受监督后台任务)或获批后装 systemd timer 调 `now`;到期动作 =
逐个 DELETE 本实例房间 → worker → api → wss 桥 → 共享 TTS 按各自专属
pid 句柄停 → `seal_budget_hel879.py` 封账归档。**封账不解除**。
- **断开与恢复**:关网页/断开后她的档案、聊天、关系、记忆原样保留(重开
验证过);共享 TTS 9880/面板 8090/wss 桥 8363 只在批准窗口内启动、到期
停回;LiveKit 7880 常驻不动。
- **与 C 的边界(本轮更正)**:B 窗口**内**的每日生活/主动留言/背景循环
运行,计入 B 的 life/proactive 日账与日 token 池(上表);B 窗口**结束**
后一切停,不自动转任何常驻。C 指窗口之外的长期后台——暂不批。不存在
同一份日用量既写进"B 已包含"又写进"C 未批准"的矛盾口径。
- **如实告知用户的已知边界**:首声延迟修复后未实测(原实测 9.53s,不承诺
任何推算值);真人打断、生成中文字取消的正面样本未取得;日子/主动留言
的真实生成首次发生在这个窗口里,质量如不足属正常迭代范围。
### C · 长期后台
- 暂不批。B 窗口结束后凭实际用量与质量报告(`llm_ops`/`embed_ops`/日账
导出)再定常驻节奏与**新的独立账本**;在此之前不自动续跑,
`CYBERGRIL_R2_ENABLED` 不在窗口外保持开启。
## 3. 不变边界
本建议不改变任何需求与验收状态:8300/8310/8320/8350 与资料不动、正式界面
不替换、PR 不合并、4080 不触、旧三窗口余额不结转;阶段体验不等于完整产品
完成,R5–R7 未完项继续按主卡承接。
## 4. 最终启用边界(总管取件勘误)
运行包README §10为操作勘误,优先于旧模板。全部仍待白栖知批准。
语音仅团队陪同预约,每通600秒、总ASR1800秒、总建房尝试6次;
TTS60段和登记+试听合计10次由当班人员监督,不冒称自动硬停。
文字生活在语音配额耗尽后仍可运行到总截止;到期按受监督定时服务停服,
资料保留、账封存,房间/共享音色与临时服务由团队核实清理。
完整自定义声音的8090面板句柄需在放行前复用HEL907方法准备,不得占用8081。
装包缺轮或解析冲突即停,不改锁版本。准备预计1–2小时,团队真链检查另限2小时,
检查不过不把失败实例交用户。用户首次可用地址交付后计3个自然日(当日算第一日),
用户也可先约定开始日期,避免地址发出时本人不在而浪费体验期。
自定义声音细分额度(同一次待批方案):最多登记2段参考音频,每段由陪同人员先核≤20秒;
自动转写最多2次/合计40秒,单列监督账,不冒充通话ASR的1800秒硬闸。
最多试听8次;登记2次+试听8次共10次。试听合成并入60段TTS总上限。
两类ASR合计最大1840秒(通话1800+参考40);失败/重试同样记账,不返余额。
本轮未执行登记、转写或试听。
### 最终统一预算勘误:团队先验真实记忆,再交用户(总管)
此条取代上文A“0嵌入”的旧建议,仅修改待批方案,当前真实额度仍0。
团队先用独立测试资料核真实Mem0接入,不把首次故障留给用户:
A的55请求/50万tokens内,最多10次用于SDK提取;先用最多20条新嵌入。
这10次/20条从同一批新申请的60次提取/180条嵌入中先扣,
不是另加额度;余量(至少50次提取/160条嵌入)留给用户期,使用实账计算。
两阶段共用唯一活动记账文件、串行写入,测试向量/档案与用户向量/档案分开,
只共用计数账,不拷贝测试聊天或测试记忆给用户。旧权威embed_ops前缀不改,
旧39/60实验的授权和账原封保存,不挪用其余额;新提取批建立独立可审计操作记录。
如旧llm_ops表已有用量,新60次必须按现值加增量并保存旧行,不可清表/重置;
操作前核旧基数并留证,实际新增始终≤60。团队检查最多2小时,从首个真实调用计时;
检查不通过则停止并报告首个错误,用户期不自动开始。
+1 -1
View File
@@ -1,6 +1,6 @@
# CyberGril 需求入口
当前完整基准:**v1.11**。批准需求继承办法的主卡评论:`01a1635b-b47c-79ed-ac66-9408c7905af9`。
当前完整基准:**v1.12**(HEL-899 起 v1.12;本行此前长期写 v1.11,属入口勘误,基准与变更记录本身自 v1.12 起即为准确,未回退)。批准需求继承办法的主卡评论:`01a1635b-b47c-79ed-ac66-9408c7905af9`。
所有新任务、续办、设计、施工、审核先读完整当前需求,再读本次相关来源。只有白栖知明确修改的要求才可变更,阶段范围不能覆盖产品要求。
+4 -3
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"version": "1.11",
"version": "1.12",
"approval_comment_id": "01a1734c-1159-7a44-a81f-3105f1b8d98d",
"owner_id": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
"scope": "完整要求;v1.11在v1.10的59项上新增成熟方案优先复用执行约束,原产品与验收边界保留;不代表已选定底座、授权迁移或功能通过。",
@@ -118,10 +118,11 @@
"category": "产品",
"topic": "连续语音与文字",
"conclusion": "用户主动开启语音后自然连续交谈,支持开口打断她;结束语音后停止收音,同时保留文字聊天。",
"boundary": "响应速度、回声消除和打断效果需用现有语音服务实测,未承诺固定毫秒数。",
"boundary": "响应速度、回声消除和打断效果需用现有语音服务实测,未承诺固定毫秒数。音色入口以可自定义音色为准:在网页上传或录制参考音频→试听→应用到同一位她的后续通话,直接复用已装TTS的零样本克隆;不以仅选预设音色替代,先前参考包试听方案不采用。本轮通话测试授权总管自定音色、最长2小时/3次通话;自定义音色交付不等于用户已验收音色,也不改变换声音只换发声、不重建伴侣、不改出生底色/名字/关系/聊天与记忆的边界。",
"source_comment_ids": [
"01a15750-2cf9-7f23-9269-0f150d2c5e61",
"01a15750-e061-733b-9d80-38459d306b97"
"01a15750-e061-733b-9d80-38459d306b97",
"01a17de1-2892-78be-a422-eb6a11ffebfd"
]
},
{
+10
View File
@@ -108,6 +108,16 @@
"GOV-006"
],
"impact": "原59项保持;不预设保留自研底座,也不指定任何最终开源项目;未授权产品迁移、依赖安装、现网部署或改变资料/隐私边界。"
},
{
"version": "1.12",
"approval_comment_id": "01a17de1-2892-78be-a422-eb6a11ffebfd",
"approved_by": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
"summary": "CG-010音色边界更新:以可自定义音色为准(网页上传/录制参考音频→试听→应用),直接复用已装TTS零样本克隆;不以仅选预设替代,参考包试听方案不采用;本轮通话测试授权总管自定音色、最长2小时/3次通话。",
"affected_ids": [
"CG-010"
],
"impact": "只改音色入口与测试授权;换声音只换发声、不重建伴侣、不改底色/名字/关系/聊天/记忆的既有边界不变;交付与阶段通过不等于用户已验收音色;不新增付费服务、不改变部署与资料边界。"
}
]
}
+10
View File
@@ -39,3 +39,13 @@ v1.7:把原始31项、已确认视觉和后续用户裁定整理到同一入
> 记住我的原则,能借鉴成熟方案的就直接借鉴,从零创造的难度太大需要的是一个专业的团队,不适用于我这个不专业的人加上没有决策性的AI的组合;另外我之前让你看的我跟另外智能体的交流内容希望你提取值得借鉴的地方
旧记录未明确规定通用内核必须优先复用成熟实现;现新增GOV-006,减少从零研发和长期自行维护,不因沉没工作量而默认保留自研内核。原59项不删减。交流材料提炼为候选机制,具体技术栈、底座、情绪映射及迁移范围未获批;不将本次文档记录当功能通过或现网施工授权。
## v1.12:音色入口定为可自定义音色
来源评论:`01a17de1-2892-78be-a422-eb6a11ffebfd`(2026-10-27 15:01:10 UTC,回应总管阻塞通报 `01a17d45-f14e-7b62-9850-646e040c7318`)。白栖知原话:
> .12已恢复,另外以上一条让我解压附件包试听,我不明白是什么意思,是让我听预设音色吗?如果是我觉得你的设计不够合理,我想要的是可以自定义音色,我的.12上安装的TTS是支持自定义音色的。至于最长2小时,3次通话的测试,我同意,音色你自己决定就行
旧边界(CG-010/D1 路线):音色按"先在现有中文女声里挑、都不满意再做零样本克隆"的条件路线推进,本轮以参考包编号试听为交付形式。新要求:**音色入口以可自定义音色为准**——在网页上传或录制参考音频→试听→应用到同一位她的后续通话,直接复用已装TTS的零样本克隆;不以仅选预设音色替代,参考包试听方案不采用。另明确:本轮通话测试授权总管自定音色、最长2小时/3次通话(操作授权,记 HEL-879 测试执行,不进需求边界)。
影响范围:仅 CG-010 音色入口与测试授权;换声音只换发声、不重建伴侣、不改出生底色/名字/关系/聊天/记忆的既有边界不变;CG-010 其余条文与原59项不动。工程交付或测试通过不等于用户已验收音色,也不得把"用户已授权自定义音色"改写成"用户已选定某支音色"。原话记录见 `sources/comment-evidence-custom-voice-20261027.json`(施工员2号按平台原话逐字核对录入)。
+1 -1
View File
@@ -1,5 +1,5 @@
{
"baseline_version": "1.11",
"baseline_version": "1.12",
"items": [
{
"requirement_id": "CG-001",
+5 -3
View File
@@ -1,9 +1,11 @@
# CyberGril 完整当前需求 · v1.11
# CyberGril 完整当前需求 · v1.12
完整要求;版本1.10在v1.9(交流静图认可)之上,把白栖知已体验的对坐动态小样 v2 整体效果(演示层面)记入 VIS-011 与 CG-004 边界;不证明其余页面、真实功能或完整产品已验收。
v1.11新增GOV-006成熟方案优先复用;原59项与v1.10阶段验收边界保持。
v1.12按白栖知2026-10-27原话(`01a17de1-2892-78be-a422-eb6a11ffebfd`)把CG-010音色边界更新为可自定义音色:网页上传/录制参考音频→试听→应用到同一位她的后续通话,直接复用已装TTS零样本克隆,不以仅选预设替代;先前参考包试听方案不采用。本轮通话测试授权总管自定音色、最长2小时/3次通话。该项交付不等于用户已验收音色。
## 阅读与效力
用户明确的新决定覆盖旧决定并记录来源;完整当前记录优先于任务摘录、技术方案和旧仓库 SPEC。无明确用户裁定的冲突先查来源,不擅自取舍。
@@ -84,9 +86,9 @@ v1.11新增GOV-006成熟方案优先复用;原59项与v1.10阶段验收边界
用户主动开启语音后自然连续交谈,支持开口打断她;结束语音后停止收音,同时保留文字聊天。
边界:响应速度、回声消除和打断效果需用现有语音服务实测,未承诺固定毫秒数。
边界:响应速度、回声消除和打断效果需用现有语音服务实测,未承诺固定毫秒数。音色入口以可自定义音色为准:在网页上传或录制参考音频→试听→应用到同一位她的后续通话,直接复用已装TTS的零样本克隆;不以仅选预设音色替代,先前参考包试听方案不采用。本轮通话测试授权总管自定音色、最长2小时/3次通话;自定义音色交付不等于用户已验收音色,也不改变换声音只换发声、不重建伴侣、不改出生底色/名字/关系/聊天与记忆的边界。
来源评论:`01a15750-2cf9-7f23-9269-0f150d2c5e61`、`01a15750-e061-733b-9d80-38459d306b97`。
来源评论:`01a15750-2cf9-7f23-9269-0f150d2c5e61`、`01a15750-e061-733b-9d80-38459d306b97`、`01a17de1-2892-78be-a422-eb6a11ffebfd`。
## CG-011 · 主动联系范围
+51
View File
@@ -0,0 +1,51 @@
{
"baseline_version": "1.11",
"read_commit": "31970c4dd3851aae67a922b03264743d504b8e97",
"task": "HEL-876:核清 .12 已有语音底座真实状态并把 R5 语音接入工程材料备齐;同步只读核对 R6 接缝;零真实模型/嵌入/合成/识别请求,不改现网",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"CG-022",
"CG-023",
"CG-028",
"CG-029",
"CG-030",
"CG-031",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "docs/voice-r5-readiness-HEL876.md(带时间戳的 .12 只读核查:7880/10095/8090 存活证据、8081=voice worker 的身份修正、9880 TTS 于 2026-10-24 22:17 优雅停机未运行、8010 MuseTalk 单元已不存在、8188 ComfyUI 占 26.6GB 显存致空闲仅 3.9GB<所需 5.6GB、音色表 11 支与当前 rain、两个 venv 版本、.11 网关与 open-meteo 探活;R5 worker 复用 app.py:488 同一 chat 链的接缝与协议;R6 城市天气/素材/备份四处接缝定位;三栏结论与零成本工程清单)+ scripts/verify_hel876_voice_seam.py 离线零模型协议检查(六项全过:epoch 同步、完整轮含 receipt、replay 零重复调用、received 确认、barge-in 新轮取消旧行=cancelled、409 重同步)。src/ 与 web/ 零改动,无新产品 PR。",
"acceptance": [
"端口/进程/健康证据全部为本轮 2026-10-26 13:22-13:38 UTC 实测,未照抄 8090/8081 旧解释:8081 实为 livekit-agents worker(pid 451883),MuseTalk 8010 单元已不存在,TTS 9880 停机(tts.log 优雅停机于 10-24 22:17)",
"凭据与 token 未读取未输出;.env 仅列白名单字段名;证书/防火墙/系统环境未改;未启停任何服务;未访问 4080;零真实 LLM 对话/嵌入/合成/识别请求",
"语音 worker 复用同一条业务链的结论落到文件/行为级:chat 链六项协议性质(epoch/串行/取消/replay/receipt/断开清理)以 fake provider 离线证明,全部通过;明确该证明不等于端到端真实通话通过",
"R5 材料备齐:目录/端口/worker 方案、复用依赖与版本、启动顺序与 TTS 重启命令、音色候选清单(无用户决定则不替定,D1 附试听/克隆真实入口及 TTS 停机期间不可试听的如实说明)、D3 麦克风用户动作、GPU 时段协调项",
"R6 只读核对只定位不实现:城市(ManagePanel.tsx:182 占位、后端无代码)、天气(无代码、open-meteo 出网已证)、素材 manifest(仅演示态 :178/:183、后端无代码)、备份边界(companion.db+vec_public+mem0_public_history.db+嵌入账本,复制单 .db 不算完整;Persona.fingerprint 可作同一位她校验)",
"三栏结论如实:已证实/未验证/确实缺资源(GPU 3.9GB<5.6GB 为唯一硬缺口);零成本工程清单六项;文档独立分支提交、候选 PR 不合并"
],
"deferred": [
"R5 真实验收:连续 3 轮、开口打断 ~1s 停声、结束即停麦、无残音、移动端麦克风——TTS 恢复并接好 worker 后在 R5 施工卡执行",
"TTS 重启:需 GPU 空闲≥6GB(ComfyUI 释放协调),由总管与白栖知安排时段,本卡未执行任何启停",
"D1 音色最终决定与 D3 用户设备信任操作:交总管集中沟通;D2 天气源采纳、D4 视觉观察仍按原依赖待用户决策",
"R6 城市/天气/素材/备份的实现(本卡仅定位接缝):建议总管按 §7 零成本清单连续派工",
"统一候选独立审核部署(8360)与语音产品代码增量(/api/voice/session、HTTPS 口、前端语音按钮):后续卡"
],
"requirement_changes": [],
"verification": [
"SSH 只读核查 2026-10-26 13:22-13:38 UTC(ss/ps/systemctl/docker/nvidia-smi/curl/日志尾部/源码阅读),未做任何写操作",
"scripts/verify_hel876_voice_seam.py 于 3.11 venv(uv 离线缓存安装,零下载)+ requirements.lock 全过六项;ruff 0 错",
"check_requirements.py 双目标(origin/main 与 origin/hel-870-assembly)含本 handoff 退出码 0",
"未验证并如实声明:本轮端到端语音可用性(TTS 停机)、真机移动端麦克风、TTS 恢复后延迟"
]
}
+54
View File
@@ -0,0 +1,54 @@
{
"baseline_version": "1.11",
"read_commit": "31970c4dd3851aae67a922b03264743d504b8e97",
"task": "HEL-877:把已核清的语音底座接到统一 CyberGril 业务链(获批 R5 后台施工);新增专属 livekit worker(agent_name=cybergril-voice-agent,复用 .12 FunASR/CosyVoice/silero VAD,不 fork 不另搭平台)+ 统一实例 POST /api/voice/session 最小协议(建房/专属派发/短时限 token 服务端处理,密钥不进浏览器)+ 与文字前端同协议的 livekit LLM 适配器(POST /api/chat,SSE/epoch 409 有界重同步/turn_id 幂等/打断即断流)+ SDK 真实播放完成才确认的分享回执;零真实请求自测交可审核代码",
"requirement_ids": [
"CG-006",
"CG-010",
"CG-011",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "src/cybergril/voice/ 独立模块:session.py+router.py(POST /api/voice/session、DELETE 结束删房释放麦克风/job、GET /api/voice/status 诚实开关;缺配置/无档案/TTS 不可用如实拒绝;app.py 仅最小注册且 capabilities.voice 不伪造)、livekit_wire.py(零新依赖 HS256 token+Twirp 派发/删房,与已装 SDK 同线上格式)、chat_api_client.py(与 real.ts 同协议:health epoch、SSE 分片鲁棒解析、409 一次重试、固定 turn_id、打断 finally 即断流)、llm_chat_api.py(livekit llm.LLM 适配器,只发当前用户话轮,服务端组装全部人格/记忆/关系;φ 原句经 ChatChunk 念出无回执)、receipt.py(should_ack 只有 played_to_end 且未打断未失败且带完整 receipt 且 epoch 一致才确认;TurnLedger 绑定 speech handle↔流;确认恰好一次)、stt_funasr.py/tts_cosyvoice.py(.12 已验证适配器原样复用带出处)、worker.py(agent_name=cybergril-voice-agent、独立 voice_state_cybergril.json 音色、无第二人格、cg.subtitle 字幕数据通道、CG-011 主动开口空操作接缝、专属健康端口 8082);config/voice.example.yaml 与 deploy/voice/{worker.env.example,restart_cybergril.sh.example,serve_dual_8360_8361.py} 部署草案(HTTP 8360+HTTPS 8361 同进程双入口共用同一状态/锁,runbook 含启动顺序/回滚/GPU 前置);docs/runbook-hel877-voice.md 契约+部署文档;tests/test_voice_session.py(9)+test_voice_chat_adapter.py(16)、scripts/verify_hel877_voice_chain.py、scripts/gen_hel877_sdk_check.py",
"acceptance": [
"语音轮次走统一链被证明:worker 适配器只调 POST /api/chat,文字/语音互相打断由服务端同一 chat_lock 轮次门处理(本地 socket 实测:语音打断文字轮旧行=cancelled 无幽灵行、打断取消即刻断 HTTP 流旧行 fail-closed、新轮干净)",
"完成轮重放不新增模型调用;409 旧 epoch 一次有界重同步后成功;SSE 多字节/CRLF/多行 data 分片边界解析正确;turn_id 固定使 SDK 内部重试幂等",
"分享 receipt 仅在 SDK 真实播放完成后确认:should_ack 对被打断/播放失败/未播完/φ 轮/残缺回执/旧 epoch 全负例拒绝(测试覆盖),确认恰好一次,服务端仍独立 fail-closed 校验",
"POST /api/voice/session 服务端建房+专属派发(agent_name=cybergril-voice-agent,protobuf CreateDispatch 与已装 SDK 同格式)+短时限 token(TTL≤300s,与 livekit.api.AccessToken 声明集全等,.12 真 SDK 比对通过);房间隔离(两 session 不同房、token 各自房间);DELETE 删房结束;缺配置/无档案/TTS 停机诚实拒绝且 capabilities.voice 保持 false",
"worker 不持有第二人格/记忆、不直连 LLM、不读通用助手 persona_state/voice_state(独立 voice_state_cybergril.json,D1 未拍板回落 default);CG-011 语音主动开口留明确空操作接缝并写清边界(仅已开启会话、同业务链、可打断、不新回执通道)",
"真 SDK 兼容在 .12 agent venv(livekit-agents 1.8.8)stdin-only 零写入零网络验证 8 项全过(真 LLM/LLMStream 子类、真 ChatContext、真流机制 ChatChunk+final+receipt、φ ChatChunk、token 声明集与 SDK AccessToken 全等、server token 同形、SpeechHandle/事件 API 存在、打断取消立即关流)",
"双入口同进程:本地 18360/18361 冒烟单 pid 双端口同 epoch 两端诚实拒答;HTTP 与 HTTPS 共用同一 app 对象/状态/锁,无第二实例写同一 DB",
"部署草案齐全(/opt/voice-agent-cybergril、健康端口 8082、启动顺序含 TTS GPU≥6GB 前置、回滚、禁抄历史 fuser 命令的明确警告);本卡未执行任何部署/启停/下载安装,未动 web/、三账、档案、4080、现网服务"
],
"deferred": [
"R5 真机验收:连续 3 轮、开口打断 ~1s 停声、结束停麦无残音、移动端 Safari/Chrome 麦克风——TTS 恢复(GPU 需 ≥5.6GB,现 3.9GB,ComfyUI 不停)并接线后在后续卡执行;未经真实验收不得称 R5 完成",
"视觉总监接线:语音按钮/字幕(cg.subtitle)/失败/打断/挂断按 runbook §3 契约;本卡未改 web/",
"总工独立审核与部署(runbook §4):.12 上对真 LiveKit server 的 dispatch/delete、HTTPS 证书签发与实机信任(每台手机高级→继续)待实测,不承诺每台手机点继续必然可用",
"语音内主动开口(CG-011 接缝)与 D1 音色最终决定、D3 用户设备动作、GPU 时段:总管集中与白栖知沟通",
"R6 天气/素材/完整备份承接不变;视觉观察默认关闭不变"
],
"requirement_changes": [],
"verification": [
"PYTHONPATH=src .venv311/bin/python -m pytest tests -q:281 passed(总工 HEL-874 返工后全量 256 + 本卡 25,含语音会话 9 与适配器/回执 16,全 fake HTTP/SSE/本地 127.0.0.1 socket,零真实模型/嵌入/合成/识别)",
"scripts/verify_hel877_voice_chain.py 一键全链 8 项全过(真实 app + fake LiveKit/TTS + 本地 socket:拒答序/专属派发/短时限 token/房间隔离/删房结束/多轮回执/重放零调用/409 重同步/打断 fail-closed/播放完成回执一次/capabilities.voice=false)",
"scripts/gen_hel877_sdk_check.py 生成脚本经 SSH stdin 在 .12 /opt/voice-agent/agent/venv/bin/python(livekit-agents 1.8.8、livekit 1.1.20)执行:8 项 ALL PASS(零写入、零网络、未触任何服务)",
"双入口本地冒烟:一次性自签证书(本机 openssl 生成)+ deploy/voice/serve_dual_8360_8361.py 于 18360/18361,单 pid、同 epoch、/api/voice/status 诚实、未配置 POST 503;测后进程与端口已释放",
"ruff check src tests scripts deploy:本卡代码 0 错(唯一遗留 SIM115 属既有 deploy/hel835_guard.py,非本卡);check_requirements.py 双目标(origin/hel-870-assembly 现tip e978e87 与 origin/main)含本 handoff 通过",
"读取位置记录:开工时 origin/hel-870-assembly=31970c4 与卡文一致;施工中总工推入 HEL-874(e978e87),本卡已完整 merge 保其成果不覆盖;origin/hel-876-voice-readiness=23415fb(总管读取点)已并入",
"未验证如实声明:端到端语音(TTS 停机未连麦)、真 LiveKit server 的 dispatch/delete 实测、worker 运行时行为(字幕/VAD/回声抑制仅源码级对齐 .12 已验证实现与 SDK 静态契约)、移动端实机"
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"baseline_version": "1.11",
"read_commit": "bf234570c0b6595a72f8dbc79e42c63685e08e01",
"task": "HEL-878(R5 前端接线,视觉总监本人设计并编码):在已认可的交谈界面上由用户主动开启/结束连续语音;按 HEL-877 后台契约(aba2aeb docs/runbook-hel877-voice.md §3)接 GET /api/voice/status、POST/DELETE /api/voice/session 与 cg.subtitle 字幕,复用 .12 :8090 既有 livekit-client 2.22.4 浏览器客户端;文字与语音同一位她、同一聊天记录,轮次从 /api/messages 回读不串台;所有退出路径立即停麦并清理;零真实模型/嵌入/合成/识别/采集/部署",
"requirement_ids": [
"CG-006", "CG-010", "CG-011", "CG-018", "CG-020",
"VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013",
"UX-001", "UX-002", "UX-005", "UX-006",
"AM-001",
"GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006",
"DEL-001"
],
"deliverable": "自前端原稿 hel-875-proactive-wiring @ bf23457 起的补丁(仅 web/ 与本交接):1) web/src/voice/session.ts——VoiceController 生命周期(代次计数:任何迟到的 token/房间/麦克风/房间事件只清理不显示);顺序为诚实状态(只 GET status,不建房不采麦)→ 加载客户端 → 采麦(拒绝则服务端什么都不开)→ POST session(结束时已在途的请求等其返回并 DELETE,不留孤儿房)→ 加入并发布麦克风 → 等 worker 加入(20 s 未加入如实失败);状态取 worker 的 lk.agent.state,缺属性时按字幕保守推断;interim 为草稿、user_final 取消在途文字回复并触发回读、assistant_final 触发回读、error(φ) 只显示「未记入聊天」不入记录;文字盖过她的语音时静音残余音频直到她回到聆听;结束顺序为停麦→关房间与音频→DELETE;断网时本地立即停采,远端 DELETE 记入本浏览器 cg.voice.owed,恢复网络/再次进入/再次开启时补发(404 视为已清);页面卸载用 keepalive DELETE;mergeTurns/recentRows 只合并非主动留言的服务器行(主动留言仍走原 inbox 轮询),按 turn_id 与已有文字轮去重,pending 行回读至落定。2) web/src/voice/livekit.ts——同源懒加载 /vendor/livekit-client-2.22.4.umd.min.js(来源 .12:8090/static,SHA-256 见 web/VENDOR.md,未改 package.json/锁文件),回声消除采集、她的音频集中在隐藏容器(结束即暂停/清空/移除)、自动播放受阻时提供「点一下听她的声音」、加入可被结束即时中止。3) ChatPanel.tsx——真实模式麦克风按钮按状态诚实标注(不可用时淡化并说明原因),确认框说明麦克风、去向、同一份记忆、如何打断与停止、未经真机验收;沿用原演示 voice-row 位置与视觉语言做成玻璃单行(连接/等待为呼吸、聆听用你的颜色、她说话用她的光色,文字始终写明状态);文字流在无 final 时先查服务器行:被新轮取消即按已取消处理,不报连接中断、不回填草稿;离开交谈/Esc/结束交谈/导航卸载面板即结束语音;在途语音轮期间她的主动留言照原规则排队。4) RealApp.tsx——语音进行中顶栏写「语音(未经真机验收)」。5) components.css 新增 .voice-row.real 等样式(含手机)。6) 测试:web/tests/voice.test.ts 26 项;web/tests/hel878_mock.mjs(HEL-877 路由契约 mock)+ fake-livekit.js(脚本化替身,浏览器假音频设备)+ hel878_shots.mjs 浏览器检查 45 项与截图;web/VENDOR.md、README 语音段落。",
"acceptance": [
"到达交谈页只发一次 GET /api/voice/status:不建房、不申请麦克风、不加载 LiveKit 客户端;configured=false / tts_ok=false / 无路由各有诚实说明,按钮淡化且不弹确认;voice=false 不被当作已接通(浏览器 1–2、单测 honest switch)",
"用户确认后:一个会话、一个麦克风、发布到该房间;等待 worker 时显示等待而非已接通;聆听/思考/说话随 lk.agent.state 切换;字幕草稿、φ 原句(标未记入聊天且不入记录)均按契约(浏览器 4–6)",
"语音轮只从 /api/messages 进入舞台与完整记录,各一次,与文字同一段记录(浏览器 5);前端从不 POST /api/chat 或 /api/chat/received(单测)",
"双通道:文字盖过她说话时残余音频静音、回到聆听再恢复;语音打断在途文字回复时该轮按已取消处理,无“连接中断/重试”、不回填草稿,记录显示已取消(浏览器 7–8)",
"结束语音/导航离开/Esc/结束交谈/房间掉线/麦克风被撤销/服务端拒绝/重复开关后:本地麦克风 track 全部 ended、她的音频与容器移除、服务端房间删除;迟到事件不复活旧会话(浏览器 10–14,单测 lifecycle 15 项)",
"断网:本地立即停麦停音频,远端房间如实记为欠删(不冒充已删);恢复网络后补发 DELETE 并清除记录;页面卸载 keepalive DELETE 到达(浏览器 14);欠删跨页面保留、404 视为已清(单测)",
"电脑 1440×900 与手机 390×844(2x)、月光/雾光两主题截图;手机语音行不出屏(浏览器 15)",
"真实 livekit-client 2.22.4 构建中,适配器用到的全部名称存在(RoomEvent 七项、Track.Source.Microphone、ParticipantKind.AGENT、createLocalAudioTrack、Room 方法/属性);仅 API 面核对,未连接",
"演示入口零语音请求;工程:tsc 0 错,eslint 0 错(1 条既有警告),vitest 7 文件 81/81,vite build 通过"
],
"deferred": [
"真实通话全部未测(R5 真机验收交 HEL-879 统一审核):连续 3 轮、开口打断约 1 s 停声、结束停麦无残音、回声、首包延迟、移动端 Safari/Chrome 麦克风与自动播放、自签 HTTPS 信任;浏览器检查用的是脚本化 LiveKit 替身与 Chromium 假音频设备,不是 WebRTC 通话",
"lk.agent.state 属性名按 livekit-agents 1.x 惯例接入,并在缺失时用字幕推断;worker 实际是否发布该属性须真机确认",
"断网时服务端房间的即时删除在物理上做不到:依赖 worker 在浏览器参与者离开后结束任务与会话登记过期(runbook §3.4/3.5,后台行为未在本卡实测),本页恢复网络或下次进入时补发 DELETE",
"CG-011 语音内主动开口:worker._maybe_open_proactive 仍是空操作,前端未虚构;后台最小需求见 runbook §3.6(会话内取已落库 proactive 行 session.say 原文、可打断、无新回执通道),呈现节奏待总管/视觉总监另定;目前语音开启时她的主动留言仍以文字进入并在她思考/说话时排队",
"她说话时没有逐字字幕(契约只在播放完成后发 assistant_final);舞台在落库后呈现全文。若要边说边显示需后台发增量字幕,属契约变更",
"D1 音色、D3 设备信任、测试额度由总管与白栖知沟通;本卡未接触任何服务、凭据或部署",
"原版 Python 需求检查未在本运行时执行(无 Python、无 git),须总管/总工对实际目标分支与 origin/main 含/不含本交接复跑;需与 HEL-882 最新母线同步后再审",
"R6 天气/素材/备份、R7 视觉观察、CG-028/029 口型与真实视频仍由主卡 HEL-797 承接;整体沉浸体验由白栖知验收"
],
"requirement_changes": [],
"verification": [
"读取:前端 bf234570(总管输入包 frontend/,HEL-875/880 原作者原稿);后台契约 aba2aeb(输入包 voice-backend/:runbook-hel877-voice.md、voice/router.py、session.py、worker.py、handoff-HEL-877.json、requirements v1.11 含 comment-evidence-full-experience-20261026.json);已读 AGENTS.md、README/current(CG-006/010/011/018/020、UX-005/006、VIS-013)/coverage/changes、integration-plan-v1 R5 与 D1/D3、voice-r5-readiness-HEL876.md",
"web:tsc --noEmit 0;eslint src 0 错;vitest run 7 文件 81/81(原 55 + 语音 26,含变异检查:去掉迟到授权回收或代次判定各使对应用例失败);vite build 通过(node_modules 沿用 HEL-875 同一锁文件离线树,无新增依赖、无下载)",
"浏览器:node tests/hel878_mock.mjs 8878 + node tests/hel878_shots.mjs <msedge.exe> <out> 8878,45/45(无头 Edge,--use-fake-device-for-media-stream;脚本化 LiveKit 替身;本机回环);中断前一轮为 39/43,失败的 4 项(语音打断文字误报、断网远端清理、卸载检查受其连带)经本轮修复后通过",
"唯一外部读取:一次只读 GET .12:8090 页面及其 /static/livekit-client.umd.min.js 以复用既有客户端;无模型、嵌入、合成、识别请求,无真实麦克风/摄像头,无部署或服务改动",
"需求结构检查:本运行时无 Python/git,未执行原版脚本,不声称通过;仅用 HEL-875 留下的 Node 移植版辅助:以 bf23457 为基含本交接通过(60 项);以后台 aba2aeb 的需求文件为基失败「历史来源必须保留」,原因是该线已追加 comment-evidence-full-experience-20261026.json 而前端原稿尚未包含,与本补丁无关,合入母线前须先同步该需求目录"
]
}
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
{
"baseline_version": "1.11",
"read_commit": "b95ce798b4a9455d941d247a46865b3ac202bf2c",
"task": "HEL-897(HEL-878 有限返修,视觉总监本人编码):修正重开语音被旧掉线结果覆盖——旧会话 fail() 在 await teardown 之后提交 OFF,覆盖已 listening 的新会话;并检查同一失败/结束/重开路径中 await 前后的代次与所有权。不重新设计,不改视觉",
"requirement_ids": [
"CG-010", "CG-018",
"VIS-013",
"UX-005", "UX-006",
"GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006",
"DEL-001"
],
"deliverable": "自 HEL-878 原稿(总管保全提交 b95ce798,13 文件哈希逐一核对一致)起的补丁,只动 web/src/voice/session.ts 与测试:1) start() 内 fail():代次与 live 释放后先发起 teardown(麦克风同步先停、房间关闭与 DELETE 已发出),立即在本会话仍拥有页面时提交 OFF 与失败提示,再等待清理完成;清理迟到返回只释放旧会话自己的麦克风/房间/服务端房间,之后不再写界面。2) start() 先同步取得新代次与 live,再等待被替换会话的清理(原先 await end() 之后才取代次:两次开启交错时,较早一次醒来会抢走代次、把已接通的新会话孤立在外,其麦克风与房间不再被释放);被替换后醒来即退出;settle 补发在取得代次之后。3) web/tests/voice.test.ts 新增「a late end of an older call」6 项(掉线/麦克风被撤销各一,worker 未加入超时,正常结束慢 DELETE,交错重开,迟到麦克风授权);Server 假件增加按会话挂起 DELETE。4) web/tests/review878_late_cleanup.test.ts:总管独立负例原样纳入。5) hel878_mock.mjs 增加 /__hold-delete 控制;hel878_shots.mjs 增加第 12 段后 HEL-897 浏览器复现 4 项。",
"acceptance": [
"总管负例 review878_late_cleanup.test.ts:修前失败(Expected listening / Received off,与总管日志一致),修后通过;测试未改",
"掉线与麦克风被撤销后,旧 DELETE 与 settle 补发都挂起期间重开:新会话 listening、active=true、无旧提示;放行后界面不再变化;旧麦克风已停、旧房间已关,新麦克风/房间未被停;服务端只剩新房间;再结束后全部清空(单测各 1 项)",
"worker 未加入超时失败且 DELETE 慢、正常结束 DELETE 慢后重开、两次开启交错(前一次仍在等旧房间归还)、迟到麦克风授权:只有最新会话存活,界面与 active 一致(单测 4 项;交错一项修前失败,暴露同路径的第二处代次漏洞)",
"修前对照:同一测试集在原 session.ts 上 5 项失败(总管负例 + 新增 4 项),修后 33/33",
"浏览器(无头 Edge,脚本化 LiveKit 替身与假音频设备):掉线后旧 DELETE 挂起,提示与停麦立即生效;新会话 listening;放行旧 DELETE 后新会话仍 listening、无旧提示、1 路活麦克风、她的音频在、服务端只剩新房间;结束后全部清理。修前构建 45/49(实测 phase=off 而新麦克风仍活),修后 49/49,原 45 项(结束/导航/Esc/结束交谈/断网/卸载/快速开关等退出路径)全部保持通过",
"工程:tsc 0 错;eslint 0 错(1 条既有警告);vitest 8 文件 88/88;vite build 通过"
],
"deferred": [
"CG-011 语音内主动开口、她说话时的增量字幕、断网时远端即时清理/租约与 API 重启实情、真实 SDK 回调与播放回执、真机连续三轮/打断/停麦/HTTPS/Safari 全部未测,仍交 HEL-879 统一审核;本卡单测与浏览器均为替身与假设备,不是 WebRTC 通话",
"原版 Python 需求检查未在本运行时执行(无 Python、无 git),须总管对实际目标分支复跑(含/不含 handoff-HEL-878.json 与本交接);未用 Node 移植版冒充通过",
"总管在前端分支从 04eff611 补齐的历史原话文件与 source-manifest 以总管保全分支为准;本补丁不触碰需求目录中除本交接以外的任何文件",
"D1 音色、D3 设备信任由总管与白栖知沟通;R6/R7、CG-028/029 仍由主卡 HEL-797 承接;整体沉浸体验由白栖知验收"
],
"requirement_changes": [],
"verification": [
"读取:总管 HEL-897 卡与附件(HEL-878-总管复核.md、preservation-878.json、review878-race.log、review878_late_cleanup.test.ts);本人 HEL-878 原稿树(逐文件 SHA-256 与 preservation-878.json 13 项一致后才施工,原稿另行备份);需求与 integration-plan R5/D1/D3 沿用 HEL-878 已读内容,本卡无需求变更",
"web:tsc --noEmit 0;eslint src 0 错 1 既有警告;vitest run 8 文件 88/88(原 81 + 本卡 6 + 总管负例 1);vite build 通过;node_modules 沿用 HEL-878 离线树,未下载、未安装",
"修前/修后:同一测试集与浏览器脚本分别在原 session.ts 与修正版上运行,日志随交付包",
"无模型、嵌入、合成、识别请求,无真实麦克风采集,无部署、合并、服务或绑定改动;未改后台与 HEL-896 发布包"
]
}
+47
View File
@@ -0,0 +1,47 @@
{
"baseline_version": "1.11",
"read_commit": "f92a86f5d8f839338311cdee08179049de34eb20",
"task": "HEL-898(HEL-879 接线卡,视觉总监本人编码):在已认可的交谈界面接上她的增量字幕(cg.subtitle 契约 v2)与语音内主动开口呈现,处理打断/文字接管/错误/挂断收尾;不另做聊天页、不改主题与整体动效;f92a86f 统一复核后的有限返修三项(worker 离场诚实收场、已退休 speech 不复活、短语音回复播放中可见)及回放基础设施",
"requirement_ids": [
"CG-004", "CG-006", "CG-010", "CG-011", "CG-018", "CG-020",
"VIS-001", "VIS-002", "VIS-003", "VIS-004", "VIS-005", "VIS-006", "VIS-007", "VIS-008", "VIS-009", "VIS-010", "VIS-011", "VIS-012", "VIS-013",
"UX-001", "UX-002", "UX-005", "UX-006",
"AM-001",
"GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006",
"DEL-001"
],
"deliverable": "自总管输入包 3228b1e(INPUT-MANIFEST 逐文件 SHA-256)起的前端补丁,只动 web/src/voice/session.ts、web/src/chat/ChatPanel.tsx 与测试:1) session.ts 严格解析 v2 信封(v===2、正整数 seq、kind 白名单、角色与种类对应、她的事件必带 speech_id、assistant_final 必带 turn_id、proactive_* 必带正整数 message_id;不合格整条丢弃且不推进序号);每次通话(控制器代次)独立 seq 高水位,seq<=已应用即丢;按 speech 生命周期只接受当前打开的一轮,已关闭/被取代的 speech 不再显示,旧 speech 的高 seq 事件不能关闭新轮。VoiceView 新增 her(speech/text/state open|final|done/turnId/messageId/proactive),与用户 ASR 草稿 heard 分槽。2) turn_start 开轮,assistant_interim 整句替换,assistant_final 原子替换为落库文本并请页面回读 /api/messages,turn_end 关轮(无 final 的关轮按未成为正式回复即清),turn_cancel/error(φ)/user_final/文字接管 hush/结束/掉线/卸载/重开即时清;φ 原句仍在语音行以“未记入聊天”显示。3) proactive_start 以落库原文、同源 message_id 显示在她的位置,proactive_end 保留至记录或下一轮接替,proactive_cancel 即清;不发任何请求、不标已读、不开麦、不重 TTS、不进入正式记录。4) ChatPanel:她的语音行进入与文字回复相同的舞台槽位(当前一两句、自动续读、暂停/回看、全文),生长中沿用文字流的“最新一句先压住”规则,逐分片不重启动画;落库行到来时同一 turn 同一位置接替,行键不变不重放;被撤回的语音行以 instant 方式立即离场(不淡出);减少动态设置沿用既有逻辑。你用语音开始新一轮时与发送文字一致恢复自动呈现(原先暂停状态下她的回答不会进来)。5) 测试:voice.test.ts 改为 v2 帧并新增 11 项(录制样例三场景回放、信封负例、乱序/重放/迟到/双 handle、φ、主动开口及打断、文字接管/挂断/重开、舞台行键接替);fake-livekit.js 未带信封的帧自动套 v2;hel878_mock.mjs 增 /__proactive;hel878_shots.mjs 的她的 final 与 φ 改为 v2 生命周期;新增 hel898_shots.mjs(两主题×电脑/手机)。【f92a86f 有限返修,首轮读取 3228b1e;本轮输入包 166 文件 SHA-256 全符】1) livekit.ts 监听 RoomEvent.ParticipantDisconnected(vendor 2.22.4 原名 participantDisconnected,emit(participant, reason)),只在本房间她的 worker(按身份记住的 agent 或 isAgent)离开、且房内没有其他 agent 时报 agentLeft;房间关闭中不报。session.ts 的 RoomEvents 新增 agentLeft:仅当前代次且 agent 已接入时走既有 fail(清字幕/草稿、停麦、清播放、关房、DELETE 本次服务房,失败入欠账重试),中文提示 WORDS.agentLeft;普通参与者离场、旧房间迟到离场、用户主动挂断都不触发。2) 已关闭 speech 由 64 项窗口数组改为每次通话一个 Set,随通话代次释放;seq 严格门不变。3) 语音回复开放态改为 live=speaking:reading.pagesOf 对其最后一块只显示已成句部分(。!?!?… 及其后的收尾引号括号),未成句部分仍压住;文字流 live=generating 的规则不变。已显示的语音行之后只会在同键下增长(短句与下一句合并时):LineStage.grow 原位追加新字的既有墨迹,不重建行、不重放;减少动态时直接设文本。ChatPanel 签名对语音行计入文本,全文页“还在说…”也适用于 speaking;ReadingClock 对同键增长只延长停留时间。没有新布局或样式。4) 测试:hel879_frames 的 DOCUMENTED GAP 改为拒绝断言,并新增每通话独立用例;voice.test 新增短回复/长句/文字流不变用例;新增 hel898_agent_left.test.ts(真实 livekitTransport 适配器 + 脚本化 SDK 事件名,另有控制器负例);fake-livekit.js 增 ParticipantDisconnected/leave/lateLeave/guest;hel878_mock.mjs 增 /__pid、/__reset,监听失败即退出;hel879_unified_shots.mjs 修回放基础设施(端口空闲与 pid 确认、多段录制按当前通话单调重编号并写 seq-renumbering.json、新通话重置、每主题 /__reset 隔离、负例原样发送并断言拒绝),加三项正反例和 CDP screencast→WebM 录屏",
"acceptance": [
"假 LiveKit 有序回放 normal-turn.jsonl:用户草稿→用户终稿→turn_start→两次增量(整句替换)→final(带 turn_id)→turn_end;她的字在舞台出现时 /__rows 尚无该回复;落库后记录中只出现一次(页面只回读,不写)",
"interrupt.jsonl 与负例:turn_cancel 后她的未完成行立即离场;被取消轮之后的高 seq 增量/final/重复 turn_start 均不显示;整段录制重放不改变界面;双 handle 时旧 speech 的 turn_end/turn_cancel 不关闭新轮;无 final 的 turn_end 视为未成为正式回复并清除",
"φ:error 原句在语音行显示并标“未记入聊天”,她的增量行立即清除,随后 turn_cancel(error_phi) 不抹掉 φ 原句;不落正式记录",
"proactive.jsonl:原文一字不差显示在她的位置并保留落库行身份(id/turn),可被你的语音打断(proactive_cancel 即清);全程页面无任何非 GET 请求(无回执、无已读、无消息写入),不调用 onSpoke/onRecord",
"文字接管、结束语音、掉线、新通话:她的行即时清;旧房间迟到帧被代次挡住;新通话序号从 1 重新开始并正常显示",
"两主题(月光/雾光)电脑 1440×900 与手机 390×844 截图与连续帧:沿用已认可位置与组件,无新增组件或样式;减少动态设置下她的字照常出现、无入场动画",
"工程:tsc 0 错;eslint 0 错 1 条既有警告(ChatPanel fullButton ref,HEL-897 起即有);vitest 8 文件 99/99;vite build 通过;浏览器 hel898 61/61,HEL-878 回归 49/49;修前对照:新 voice.test.ts 在原 session.ts/ChatPanel 上 12 项失败",
"f92a86f 返修-1:worker 离场(房间仍开)时通话即时结束:她的行清除,提示“她那边的语音中断了……”,麦克风轨 ended,播放容器移除,本次 session DELETE 恰好一次;普通参与者离场不挂断;旧房间迟到离场不影响新通话;用户主动挂断只有一次 DELETE、没有离场提示;DELETE 失败入欠账并重试(单测)",
"f92a86f 返修-2:adversarial-gates 66 次挤出后,speech_A 以新 seq 重开/增量/终稿都被拒(单测与浏览器原样探针),之后新 speech 正常进入;新通话从 seq 1 开始,不受旧通话已退休 id 影响",
"f92a86f 返修-3:worker-normal 回放中“在听呢,”“…那件事”未成句时舞台不显示她的半句;第三个增量成句后、final 之前即显示整句,并在播放期间保持;final 与落库行接替为同一 DOM 行(不重建、不重放);短句与下一句合并时同节点原位增长、首字墨迹不动;长句只显示已完成的行、不整段铺满;文字流仍压住最新块",
"f92a86f 返修工程:tsc 0;eslint 0 错 1 条既有警告;vitest 10 文件 118/118;build 通过;修前对照 vitest 7 项失败;统一实际 worker 帧浏览器回放(moon/fog × 电脑/手机 + 减少动态)修后 53/53、修前 30/53(失败都落在三项及其连带);hel898_shots 61/61、hel878_shots 49/49;8 段 WebM 录屏取自真实 screencast 帧"
],
"deferred": [
"真实通话未测:真实 LiveKit/WebRTC、真实 worker 与 SDK 回调时序、真机/Safari/iOS、可信 HTTPS+WSS、R5 连续三轮/1 秒打断/停麦无残音/有限预算,均仍由 HEL-797 主卡承接;本卡全部为假 LiveKit、假音频设备与 Chromium 无头 Edge",
"CG-011 语音内主动开口仅是“会话开时念一条已落库旧留言”的最小接缝(后台默认关,本卡不启用);会话中持续主动联系、激活节奏、未读联动策略未完成,不得记为 CG-011 完成",
"后台并行小修(建房延迟后的清理唤醒、主动 say 的 start 发得过晚、无完整 receipt 错误发 final)由原总工在 HEL-879 处理;本卡按 v2 正确语义实现,最终后台 commit 出来后需按其统一复核(特别是 proactive_start 晚于音频开始时的呈现)",
"已知边界:seq 按“一次 worker job 一个序列”实现;若同一房间内 worker 重连且序号归零,前端会按契约丢弃其帧直到超过旧高水位——需后台确认 job 与房间一一对应",
"原版 Python 需求检查(scripts/check_requirements.py --base-ref origin/main,含本交接)未在本运行时执行(无 Python、无 git),须总管对实际目标分支复跑",
"D1 音色、cap=0、R6/R7、整体沉浸体验由主卡与白栖知决定;工程自测不是用户体验验收",
"f92a86f 返修:真实 SDK 下 worker 离场事件的时序(含网络重连期间 remoteParticipants 被清空但不发离场事件)只按 vendor 源码核对,未在真实房间验证",
"回放基础设施把多段录制重编号(原始帧保留在 fixtures,映射见 seq-renumbering.json),证明的是页面在“同一通话单调 seq”下的呈现与门,不证明 worker 跨 job 的 seq 行为"
],
"requirement_changes": [],
"verification": [
"读取:HEL-898 卡(启动交接与原说明)、输入包 3228b1e:docs/voice-subtitle-contract-v2.md、docs/rework/hel879/voice-seam-interfaces.md、subtitle-samples/*.jsonl、src/cybergril/voice/seam.py 与 worker.py(事件发出点)、AGENTS.md、docs/requirements/README.md/current.md(v1.11)/coverage.md/changes.md、sources/integration-plan-v1.md R5",
"工具:沿用本机既有离线工具链(HEL-834 留存 node v22.11.0;HEL-897 留存 web/node_modules,package-lock 哈希与本包一致,目录联接使用);未下载、未安装",
"web:tsc --noEmit 0;eslint src 0 错 1 既有警告;vitest run 8 文件 99/99(原 88 + 新增);vite build 通过;日志随交付包",
"浏览器:node tests/hel878_mock.mjs + node tests/hel898_shots.mjs(无头 Edge、脚本化 LiveKit 替身、Chromium 假音频设备)61/61;node tests/hel878_shots.mjs 回归 49/49;截图与连续帧随交付包",
"无模型、嵌入、ASR、TTS 请求,无真实麦克风采集,无部署、合并、服务或绑定改动;未改后台、总装分支或 PR#11",
"f92a86f 返修读取:卡内三项说明、HEL-898-统一复核返修输入-f92a86f.zip(166 文件 SHA-256 全符)、web/public/vendor/livekit-client-2.22.4.umd.min.js 中 RoomEvent.ParticipantDisconnected 的定义与 emit(participant, reason)、断开清理路径不发该事件;本包缺 docs/rework/hel879/subtitle-samples,测试运行时沿用 3228b1e 输入包同名文件(不入补丁)",
"f92a86f 返修浏览器:node tests/hel879_unified_shots.mjs <msedge> <out> 8879 9499(自带 mock,先确认端口空闲与 pid);hel898_shots/hel878_shots 由本运行自起 mock(核对 pid 后按 PID 停止)"
]
}
+57
View File
@@ -0,0 +1,57 @@
{
"baseline_version": "1.12",
"read_commit": "8dc84f88e9d5e961e17b246f24c23eaf526b012e",
"task": "HEL-899:把现有 CosyVoice 参考音频零样本克隆能力接入 CyberGril 统一 API(网页上传/录制参考音频→登记 cg- 专属 ID→试听现合成→应用到同一位她的后续通话);直接复用 .12 语音面板 8090 的 zero-shot 注册与 CosyVoice 热加载、9880 /tts 合成,不训练新模型、不重造克隆引擎、不自建第二写者;本卡只做后台/协议与零真实调用自测,前端按合同接视觉总监,真实试听放统一审核",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "src/cybergril/voice/custom.py(CustomVoiceService:8090 面板代理=共享 voices.json 唯一写者、cg-+8hex 服务端生成 ID 不撞既有 11 支、WAV 头服务端复验 RIFF/WAVE≤15MB≥2s、显式 allow_asr 才走 FunASR 转写、试听先验存在拦住 CosyVoice 未知音色静默回落 default、原子落盘+失败整体回滚、选择/元数据/资源流水持久在实例资料空间重启读回、备份清单 manifest);router.py 五个新端点(GET/POST /api/voice/voices、POST /api/voice/preview、GET/POST /api/voice/selection)+ /api/voice/session 响应新增 voice=本次通话捕获音色(选择失效诚实回落 default;未选择不代写 worker 文件不替 D1 定音色);session.py 会话记录 voice 字段+/health 名单读回;worker 语义不变(入口读一次→通话中改选最迟下一通话生效、绝不半句换人);契约文档 docs/voice-custom-contract-HEL899.md(前端可直接实现的请求/响应/错误/录音格式/部署与回滚);config/voice.example.yaml 增补(panel_url/tts_url/registry_file/worker_state_file,env 可覆盖);零新依赖(登记端点原始字节体+查询串,特意不引 multipart 库);tests/test_voice_custom.py 18 项 + scripts/verify_hel899_custom_voice.py 12/12;治理:v1.12(CG-010 边界更新,来源评论 01a17de1-2892-78be-a422-eb6a11ffebfd 逐字录入 sources/comment-evidence-custom-voice-20261027.json + manifest/baseline/coverage/changes 同步)",
"acceptance": [
"完整最小合同成立:列出可用声音(builtin 11 支+custom 标记、选择/试听状态、备份清单字段)、登记参考音频(服务端生成 cg- 专属 ID,不可能撞名覆盖既有 11 支,撞名重试不覆盖)、试听生成(每次现合成 WAV 24k/mono、无缓存不拿旧音频冒充)、读取/应用专属音色(换声音只换发声)全部经统一 API,浏览器不直连 8090/9880,凭据不下发",
"当前 session/chat 业务不分叉:worker/统一聊天链零改动;通话开启捕获有效音色并写入会话记录与 worker 状态文件,通话中改选不影响本通话、最迟下一通话生效(测试断言会话 voice 定住)、不打断或误认原轮次/receipt(receipt 链路未动)",
"原子与失败诚实:登记失败(面板 400/5xx/断连/谎报成功)不留下半条可选项、无元数据残留;应用失败整体回滚(选择状态与 worker 文件都回旧值);服务不可用一律 503 中文文案;ASR 仅在 allow_asr=true 显式准许且参考文字为空时发生(面板 FunASR,非 LLM),每次记 register_asr/preview_tts 流水可核算",
"试听不写聊天/记忆/生活/回执(四表计数零变化测试断言);自定义音色未试听不得应用(409),内置 D1 候选不强制;未知音色 404 且 TTS 零调用(前置拦住 CosyVoice 静默回落 default);未知选择通话开启诚实回落 default 不冒充",
"持久化与身份不变:选择/元数据/试听凭据重启读回(重建服务同批文件测试);换声音不改 companion/bazi_core/chat_message(逐字节对比测试);通用助手 voice_state/persona_state/既有音色零写入零删除(桩端点计数零);无人经 API 选择时不代写 worker 文件(D1 域不替用户定)",
"零真实调用:全链 fake 面板/TTS/LiveKit 传输,零真实 LLM/嵌入/ASR/TTS、零模型下载/安装、零共享音色实写、零部署、零现网触碰、未访问 4080、未动 HEL-879 现场"
],
"deferred": [
"真实 CosyVoice 试听/真实 FunASR 转写/克隆相似度:统一审核卡在获批环境实测,复用合法剩余额度,不另开账绕 HEL-879 上限(最长2小时/3次通话授权属总管测试执行,不在本卡)",
"前端入口(上传/录音/试听/应用 UI 与入口呈现):视觉总监按 docs/voice-custom-contract-HEL899.md §2 契约接线;本卡未动 web/",
"自定义音色删除与完整备份/恢复(CG-009/R6):本卡最小合同不含删除(失败/多余登记不影响使用),备份清单字段已就位,恢复=按清单重新登记(面板无导出接口)",
"D1 最终音色决定仍归白栖知/总管;本轮测试音色已授权总管自定",
"R5 整体真机验收与 R1–R7 完整要求继续承接;自定义音色交付不等于 R5 完成或整产品完成"
],
"requirement_changes": [
{
"summary": "v1.12:CG-010 音色边界更新为可自定义音色(网页上传/录制参考音频→试听→应用,直接复用已装TTS零样本克隆,不以仅选预设替代,参考包试听方案不采用);影响仅 CG-010 音色入口与测试授权,换声音只换发声等既有边界不变",
"approval_comment_id": "01a17de1-2892-78be-a422-eb6a11ffebfd",
"record": "sources/comment-evidence-custom-voice-20261027.json + changes.json/changes.md v1.12 + baseline.json/coverage.json/current.md 同步"
}
],
"verification": [
"PYTHONPATH=src .venv311/bin/python -m pytest tests -q:370 通过(基线 8dc84f8 全量 352 + 本卡 18:tests/test_voice_custom.py,全 fake 面板/TTS/LiveKit httpx.MockTransport + 临时文件 + 隔离 registry,零网络零真实模型调用)",
"scripts/verify_hel899_custom_voice.py:12/12 全过(真实 app+router+service,fake 传输:列表→无准许拒ASR→显式登记→未试听409→试听现合成→应用→会话捕获→通话中改选定住本通话→下一通话生效→capabilities.voice=false→重启读回选择/元数据/试听凭据)",
"ruff check src tests scripts deploy:本卡代码 0 错(既有 deploy/hel835_guard.py 的 SIM115 遗留不变,非本卡)",
"python3 scripts/check_requirements.py --base-ref origin/main 与 --base-ref 8dc84f8(实际基准):含本 handoff 通过(v1.12 升版+新来源+变更记录合法,历史来源与变更未改写)",
".12 只读取证(SSH 零写入零服务触碰):/opt/voice-agent/web/server.py 实读核对 upload 校验(名字规则/15MB/WAV magic/32000字节/空参考文本→FunASR 转写/失败删 wav/voices.json 原子写)、/opt/voice-agent/cosyvoice/server.py 实读核对 mtime 热加载与 /tts 未知音色静默回落 default、voices.json 11 支音色清单——契约按实际源码写,不按记忆",
"未验证如实声明:真实 CosyVoice 合成与 FunASR 转写未调用(TTS 9880 停机中且本卡零真实调用);浏览器端录音上传未实现(前端合同已交);.12 未部署、未改任何现网配置/端口/数据"
]
}
+56
View File
@@ -0,0 +1,56 @@
{
"baseline_version": "1.12",
"read_commit": "ce7279e68f64319c080f7ef150cb86c892b81ecb",
"task": "HEL-900:在已认可的设置界面接入自定义声音——网页上传或录制参考声音→登记→试听实际合成→应用给同一位她的后续通话;按 docs/voice-custom-contract-HEL899.md §2 接 HEL-899 后台(agent/2/652da8a82e50 @ ce7279e),只改 web/,零真实调用",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"VIS-003",
"VIS-004",
"VIS-008",
"VIS-010",
"UX-003",
"UX-006",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "设置面板新增「她的声音」分区(沿既有设置结构、组件与两套主题 token,不另起视觉方向):web/src/voice/custom.ts(同源 API 客户端:GET/POST /api/voice/voices、POST /api/voice/preview、POST /api/voice/selection;浏览器解码任意可解音频→16kHz 单声道 16 位 PCM WAV 原始字节体上传,参数走查询串;≥2 秒/≤15MB/静音/解不开在浏览器先拦;allow_asr 仅在参考文字为空且用户明确打开开关时为 true;试听响应须为合法 WAV 才可播放;Recorder 仅由点击开启、只录音频、停止/取消/离开/音轨中断即释放、20 秒自动停、迟到的麦克风授权立即关闭);web/src/manage/VoiceSection.tsx(现在的声音与生效时机、上传/录音二选一、参考原声与「她用这个声音说的·刚刚现场合成」分开标注的主题化播放条、名字/参考文字/转写开关、登记、可用声音列表、试听→浏览器 canplay 确认可播后才能应用、每类请求一代 AbortController + 进行中引用防重复、切换/重试/离开/卸载释放请求与 objectURL、失败保留输入);ManagePanel 接入分区、未登记的参考声音计入未保存离开确认、能力标签随服务器实际状态;components.css 追加分区样式(只用既有 token,两主题与手机断点);离线 mock tests/hel900_mock.mjs、浏览器驱动 tests/hel900_shots.mjs、单元测试 tests/hel900_custom_voice.test.ts",
"acceptance": [
"用户可在网页选择本地音频文件或主动点「开始录音」得到参考声音;产物为真实 PCM WAV(mock 端按 RIFF/WAVE 头核验 16kHz/单声道/16 位、时长≥3 秒、Content-Type audio/wav、原始字节体),改扩展名的非 WAV 在浏览器被拒且零上传",
"参考文字可填写;留空时须用户明确打开转写开关才传 allow_asr=true,默认不代开;已填写参考文字时一律 allow_asr=false;转写结果回显",
"参考原声与实际合成试听分开标注(「你给的声音,不是她的声音」/「刚刚现场合成·不是参考原声」);试听每次重新请求,非 WAV 响应拒绝播放;浏览器 canplay 确认本次新试听可播后「给她用这个声音」才可点;切换声音、重试、离开时中止旧请求、停播并回收 objectURL,迟到的旧试听不出现",
"读取并显示当前实际选择;应用成功后显示新声音并说明从下一次通话生效、已接通的通话不中途换声;换声音文案与请求均不触及名字/底色/关系/聊天/记忆;不读写通用助手状态",
"失败如实:未配置/无通路/登记服务 503/TTS 503/422/409/断网/拒麦/麦克风占用/非安全来源/录音取消/停止/离开/重复提交/旧异步结果晚到各有中文说明;失败保留参考声音与已填内容;重复点击只发一次登记且不中止已发出的上传",
"录音仅由用户点击开启、只请求音频不请求摄像头;停止、取消、切到其他分区、关闭设置、音轨中断后真实音轨全部 ended;打开分区本身只读列表、不申请麦克风;演示入口只显示说明卡、不挂载该组件(代码条件 real && open,未做演示入口浏览器测试)"
],
"deferred": [
"真实 CosyVoice 试听、真实 FunASR 转写与克隆相似度、真实麦克风与真机(iPhone/Android/Safari)录音解码:留总工统一审核在获批环境与额度内实测,本卡零真实调用",
"后端待补核项(总管已列给统一审核):真实 worker 通话开始读音色的时序、登记服务端成功后本地落盘失败/断连的半状态、ASR/试听实际用量与既有限额的衔接;前端不替后台修实现",
"自定义音色删除、完整备份/恢复(CG-009/R6)后续卡;当前界面已写明暂不能在此删除",
"交谈页未显示「本次通话所用声音」(POST /api/voice/session 的 voice 字段);如需在通话中提示,可后续小卡补",
"设置「外接服务」分区仍沿旧文案显示语音识别/合成「尚未接通」,与本卡新分区及 HEL-878 语音接通不一致,未在本卡改动,交总管决定是否另行修正",
"D1 最终音色决定仍归白栖知/总管;R5 整体真机验收与 R1–R7 完整要求继续承接,本卡交付不等于用户已验收音色"
],
"requirement_changes": [],
"verification": [
"web: tsc --noEmit 0 错;eslint src 0 错(仅既有 ChatPanel.tsx:240 警告,非本卡);vitest 137/137 通过(基线 118 + 本卡 19:WAV 编码/头校验、48k 立体声→16k 单声道、过短/静音/解不开/过大拒绝、API 合同与错误透传、离线零请求、allow_asr 规则、非 WAV 试听拒绝、中止的试听不返回结果、409/503、Recorder 仅点击开启/停止保留/取消丢弃/迟到授权立即关闭/20 秒自动停/音轨中断/拒绝与非安全来源/重复点击单开);vite build 通过",
"浏览器:node tests/hel900_mock.mjs 8900 + node tests/hel900_shots.mjs <msedge.exe> <outDir> 8900,headless Edge + Chromium 假音频设备,83/83 通过;月光/雾光 × 电脑 1440×900/手机 390×844 主流程截图各 6 张及边界截图(文件上传、断网、登记 503、旧试听丢弃、409、拒麦、离开确认、未配置/无通路/服务不在)",
"python -I scripts/check_requirements.py --handoff docs/requirements/handoff-HEL-900.json 通过;本运行环境无 git,--base-ref origin/main 比较未能执行,改以与 ce7279e 源码包逐文件哈希比对确认 docs/requirements 除新增本交接文件外零改动,--base-ref 检查留统一审核在仓库内执行",
"未验证如实声明:假麦克风是浏览器内置提示音不是人声;试听是 mock 生成的音调不是 TTS;未接真实 8090/9880/FunASR,未部署,未触碰 HEL-879 现场与共享音色注册表,未访问 4080;Safari 与真机未测"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"baseline_version": "1.12",
"read_commit": "d167ba927c1b07e4771219707c744e90051837d9",
"task": "HEL-902:统一离线审核 HEL-900 前端(hel-900-preserved @ 701442d/37b4e69)与 HEL-899 后台(ce7279e),整合 HEL-879 真实窗口修复(hel-879-assembly @ be149cd)到独立分支 hel-902-integration,零真实调用、零部署、零依赖下载;并备齐真机补测申请包",
"requirement_ids": ["CG-006", "CG-009", "CG-010", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-019", "CG-020", "CG-021", "VIS-003", "VIS-004", "VIS-008", "VIS-010", "UX-003", "UX-006", "AM-001", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006", "DEL-001"],
"deliverable": "独立整合分支 hel-902-integration(701442d+be149cd 无冲突合并,不改写母线/旧现场/PR#11);总工统一审核报告 docs/rework/hel902/REVIEW.md(含后台故障注入新增 tests/test_voice_review_hel902.py 6 用例:本地元数据落盘失败可恢复不逼重复注册、响应断连重试留可回收孤儿条目、开房 worker 写失败诚实 503、worker 状态损坏行为如实记录、流水写不进时应用 fail-closed 409、选择写失败整体回滚);真机补测申请包 docs/rework/hel902/r5-followup-package.md(时窗/逐类上限/停机方法/参考音频来源与 cg- 共享新增回收/旧服务保护/HEL-879 遗留量测并入);需求入口 README 基准行 v1.11→v1.12 勘误(授权非视觉小修,基准不回退)",
"acceptance": [
"统一 head 上实际复跑:pytest 376/376(370 基线+新增6)、verify_hel899_custom_voice.py 12/12、ruff src/tests/scripts 通过、需求检查 4/4(origin/main 与 ce7279e 各带/不带 handoff-HEL-900)",
"前端保全独立复核:8 文件 blob SHA-256 与保全清单 8/8 一致;作者 ZIP 重下校验 SHA-256 一致;36 截图全部有效 PNG 且视口与 harness 定义一致;作者日志(vitest 137/137、lint 0错1既有警告、build、浏览器 83/83)与 handoff 声明逐项相符",
"前端逐行静态复核与合同 §2/用户原话 01a17de1 一致:原始 WAV 字节+查询串、浏览器真转 16k mono PCM16、大小时长静音拒绝、allow_asr 显式门、试听必合法 WAV+canplay 后才可应用、参考原声与现场合成分开、请求代际清理、录音轨迹释放、演示入口不挂载零请求、CSS 仅用既有 token",
"后台重点补核:capture→写 worker 文件→dispatch→worker 入口读一次的顺序锁定;共享注册表仅经 8090 且 cg- 命名空间不碰 11 支内置与通用助手状态;ASR/试听流水仅核算、硬闸仅 LLM/嵌入(如实,未称硬闸)",
"问题清单集中一轮:🔴外接服务文案按实际能力改写(交视觉作者);🟡登记本地落盘失败映射文案、孤儿条目说明、worker 状态 docstring、会话页显示本次音色(结论:需要)、capture-dispatch 串行化;承接 CG-009/R6 删除与备份、R5 整体验收、R1–R7 完整功能"
],
"deferred": [
"真实登记/可选 ASR 转写/真实试听/应用与下一通话验证、克隆相似度、真机(iPhone/Android/Safari)录音路径:待主卡批准补测包后执行,未批前真实 cap=0",
"HEL-879 遗留:打断后约1秒停声毫秒级量测、文字打断语音方向、真实浏览器/设备麦克风路径——并入同一补测窗口,量测方法已写入补测包",
"前端独立复跑(tsc/eslint/vitest/build/浏览器两主题×电脑/手机、演示入口零外发断言):本机依赖被清理且禁下载,需一次性 npm 同锁文件安装授权或随真机窗口在 .12 执行",
"自定义音色删除、完整备份/恢复(CG-009/R6)后续卡;R5 整体验收与 R1–R7 完整功能持续承接;PR#4/PR#8 等仍无合并或替换正式界面授权"
],
"requirement_changes": [],
"verification": [
"pytest tests -q 376/376(py3.11.16 复用既有审核 venv,零安装)",
"python -I scripts/verify_hel899_custom_voice.py 12/12;ruff check src tests scripts 通过",
"python -I scripts/check_requirements.py --base-ref origin/main(及 --handoff docs/requirements/handoff-HEL-900.json、ce7279e 对照)4/4 通过",
"前端与浏览器未在本机复跑(环境缺依赖且本卡禁下载安装),以作者日志+逐行静态复核+保全哈希复核作证据,如实标注未验证范围;真机项全部列入补测包"
]
}
+57
View File
@@ -0,0 +1,57 @@
{
"baseline_version": "1.12",
"read_commit": "b09a907747f9836c4aea04016757321d28b7cb6b",
"task": "HEL-903:HEL-902 统一审核后的原作者有限返修(只改 web/ 与本卡交接/证据)——外接服务语音两行按 /api/voice/status 如实区分状态;通话栏显示 session 响应 voice 固定的「本次通话声音」;登记后无明确结果(500/503/响应断连)时提示先刷新列表看编号条目,列表标出缺本地元数据的 cg- 条目。基于 origin/hel-902-integration @ b09a907,保留 HEL-879 真实窗口修复与 HEL-900 作者源码,不推写统一母线、不回退 ce7279e,零真实调用",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"VIS-003",
"VIS-004",
"VIS-008",
"VIS-010",
"VIS-013",
"UX-001",
"UX-003",
"UX-005",
"UX-006",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "web/src/voice/session.ts:readVoiceStatus 把 GET /api/voice/status 分成 none(无接口)/off(未配置)/ok(已配置且合成健康检查通过)/down(已配置但健康检查未过)/unknown(读不到)五态,voiceStatus 改为由它派生、行为不变;VoiceView 新增 voice(本次通话固定声音,开房前 null,服务器未给或不合规为 {id:null}),只在拿到本代 grant 时写入,结束/失败/迟到旧请求一律随 view 复位;callVoiceOf 只接受列表可容纳的 id。web/src/chat/ChatPanel.tsx:在既有通话条加「本次通话 · 声音名」(default→默认声音、缺字段→声音未说明,名字只按该 id 查列表,查不到显示编号,不用设置里的当前选择),title 写明到本次结束不变、设置里的新选择下一次通话生效。web/src/manage/ManagePanel.tsx:外接服务的语音识别/语音合成两行改为进入分区及点「重新读取状态」时读 /api/voice/status(仅真实入口),按五态如实说明,识别一行写明没有单独健康检查;配置框、按钮、说明卡与文字检查提示随之更正,视觉观察仍写尚未接通。web/src/voice/custom.ts + VoiceSection.tsx:VoiceError 增加 uncertain(已发出后断连、5xx、成功响应体读不全),登记遇到时提示「可能已经登记上了,先刷新列表看 cg- 编号条目,不用再登记」,提供「刷新列表」并把再登记改为「确认没有,再登记」;刷新后对比失败时的自定义列表,指出新出现条目或说明暂无新条目需稍等;列表把无本地元数据的 cg- 条目标「只有编号,缺本地资料」并加说明。4xx 与离线未发出仍为原明确提示。不新增删除入口、不自动删声音。components.css 追加通话条声音标签、状态行配色、编号条目说明与手机按钮换行(只用既有 token,两主题)。测试:tests/hel903_voice_rework.test.ts(11 例)、tests/hel903_mock.mjs、tests/hel903_shots.mjs;tests/hel898_agent_left.test.ts 期望视图补 voice:null。",
"acceptance": [
"外接服务:语音合成在 ok/down/off/none/unknown(500)/unknown(断连) 下分别显示「已配置 · 合成服务健康检查可连接」「已配置 · 合成服务这会儿暂不可用」「未配置 · 这台服务没有语音配置」「这台服务没有语音状态接口」「状态未知 · 这次没读到语音状态」;语音识别不声称有自己的健康检查;任一状态下语音两行都不出现「尚未接通」或「已接通」;读取状态不申请麦克风;说明写明「可连接」不代表整套语音通话已验收",
"通话:开房后通话条显示 session 响应 voice 对应的「本次通话 · 名称」;通话中另一端改选声音,本次通话显示不变;挂断后标签消失、麦克风释放;下一次通话显示新选择;响应缺 voice 字段显示「声音未说明」而非设置当前选择;default 显示「默认声音」;更早一次通话迟到的 grant 不覆盖新通话(单测)",
"登记:服务器已收下却回 500、或响应中途断开、或 503 时,提示可能已登记并先刷新列表;不自动重读、不自动再登记(上传计数验证);点刷新列表后能指出新出现的 cg- 编号条目并标「只有编号,缺本地资料」;若无新条目则说明稍等再刷、确认没有再登记,确认后再登记一次只发一次;参考声音与已填内容在失败后保留",
"回归:既有上传→登记→试听 canplay→应用流程、换声音只换发声的说明、录音点击开启/停止保留/取消释放麦克风、通话字幕与结束清理均保持;演示入口打开外接服务、她的声音与交谈零 /api 请求"
],
"deferred": [
"后台新错误映射(本地元数据落盘失败 OSError→503/422 中文文案等,HEL-902 §5-2)由另卡处理;本卡先兼容既有 500/503,后续统一整合时按合同补核前端文案",
"语音识别没有独立健康状态接口,外接服务只能如实写「没有单独的健康检查」;如需显示识别服务真实状态需后台另行提供",
"编号条目的删除/回收(经 8090 面板自有删除)、自定义音色删除与完整备份恢复(CG-009/R6)继续由后续卡承接,本卡不新增删除入口",
"_capture_voice→worker 读文件的秒级竞态(HEL-902 §5-6)属后台;前端显示的是服务器声明的本次捕获值",
"真实 CosyVoice/FunASR/LiveKit、真机(iPhone/Android/Safari)与 R5 整体验收、R1–R7 完整功能继续承接;HEL-901 仍停放;本卡不部署、不合并"
],
"requirement_changes": [],
"verification": [
"web(复用原作者 HEL-900 工作区已存在的同版 node_modules,package-lock SHA256 F950DE58…7A27 一致,经目录联接使用,零下载安装):tsc --noEmit 0 错;eslint src 0 错 1 既有警告(ChatPanel fullButton,非本卡);vitest 148/148(基线 137 + 本卡 11);vite build 通过",
"浏览器:node tests/hel903_mock.mjs 8903 + node tests/hel903_shots.mjs <msedge.exe> <outDir> 8903,headless Edge + 假 LiveKit(tests/fake-livekit.js)+ Chromium 假音频设备,90/90 通过:月光/雾光 × 电脑 1440×900/手机 390×844 各 18 项(外接服务、应用声音、本次通话声音固定、下一通更新、字幕、挂断释放、登记 500 后恢复),状态五分支、缺 voice 字段/default、响应断连与 503 恢复、录音释放、演示零请求",
"对照截图:同一 mock 跑 b09a907 原样构建(产物 index-C8UHROe1.js 与 HEL-900 作者构建同名)before 模式三张:外接服务、通话条、登记 500",
"需求检查:python -I scripts/check_requirements.py 与 --handoff docs/requirements/handoff-HEL-903.json 本机执行;本运行环境无 git,--base-ref origin/main 与 --base-ref b09a907 均无法执行,改以与 b09a907 源码包逐文件 SHA256 比对确认 docs/requirements 除新增本交接文件外零改动,--base-ref 四组检查交总管在仓库补跑",
"未验证如实声明:假麦克风是浏览器内置提示音;假 LiveKit 不是 WebRTC;试听是 mock 音调不是 TTS;未接真实 8090/9880/FunASR/LiveKit,未部署,未写共享音色,未访问 4080;Safari 与真机未测"
]
}
+28
View File
@@ -0,0 +1,28 @@
{
"baseline_version": "1.12",
"read_commit": "b09a907747f9836c4aea04016757321d28b7cb6b",
"task": "HEL-904:HEL-902 统一审核后的后台有限返修(HEL-899 原作者)。三项:①登记在8090成功而本地元数据落盘失败→503中文映射(编号可恢复、共享登记不回滚、不逼重复登记),ASR流水先记再落盘;②worker状态文件损坏整体重写的实况补入docstring/合同,正常可读文件仍保留其他键;③修通话捕获音色与worker实读竞态:以会话不可变快照(通话窗口持有)保证响应voice=worker本通话实读声音、通话中改选只影响下一通,覆盖失败/取消/挂断失败转欠账/TTL到期结算,确定性Event门控无sleep;④补测申请材料收紧(仅材料不执行)。零真实调用、零部署、零下载、不动web/与共享worker、不新增依赖",
"requirement_ids": ["CG-006", "CG-009", "CG-010", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-019", "CG-020", "CG-021", "AM-001", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006", "DEL-001"],
"deliverable": "独立分支(自b09a907)后台返修:custom.py登记失败映射LocalRecordFailed+ASR流水先记、write_worker_voice损坏实况docstring、通话持有hold/release+apply暂缓写、router捕获登记/失败取消释放/503映射/settle挂钩、session.py on_session_settled结算(挂断成功/转欠账/TTL到期);tests/test_voice_review_hel902.py第1例按新契约改写(含重启恢复与409守门断言)、新增tests/test_voice_rework_hel904.py确定性时序4例;合同docs/voice-custom-contract-HEL899.md §2.2/§2.5/§3更新;docs/rework/hel902/r5-followup-package.md收紧(head里程碑表:d167ba9=整合提交、b09a907=审核head、本卡head待统一复审登记,登记ASR与连续通话ASR区分、连续通话ASR计数手段与待补阻塞如实声明、停声量测单一时间基准帧级录音、参考音频明确D1包female_soft段含sha256/长度、回收清单与不未经确认保留测试垃圾、共享TTS启停快照恢复、git回退不代替共享回收、上限含失败口径与硬闸/监看界线);docs/rework/hel904/REWORK.md返修记录",
"acceptance": [
"登记成功+本地元数据写失败:503中文文案含编号、明确共享登记不回滚、刷新列表可找回并试听/应用、不逼重复登记(tests/test_voice_review_hel902.py改写例:含重启按编号label=编号继续用、新音色未试听409守门不放松、11支内置与通用助手零改写)",
"worker状态文件损坏实况:docstring/合同如实注明整体重写仅剩voice,不再宣称任何情况下保留其他键;正常可读文件其他键保留(既有用例继续通过)",
"竞态收口:确定性Event门控测试把worker读时点延迟到dispatch返回之后、并发应用新声音后——本通话worker读到=响应voice=旧音色;挂断结算后下一通capture用新声音;挂断失败转欠账、TTL到期、dispatch失败、请求取消四条路径都释放持有、应用随即写透;无sleep凑测试(tests/test_voice_rework_hel904.py 4例)",
"补测材料:仅材料不执行,候选上限只收紧不放宽,一切失败/取消/重试/出站无响应计入口径;LLM/嵌入=程序硬闸与其余工程监看界线写明;连续通话ASR无程序计数手段处如实标待补阻塞;真实cap=0保持"
],
"deferred": [
"真实登记/可选转写/真实试听/应用与下一通验证、克隆相似度、打断停声帧级量测、真机麦克风路径:待主卡批准补测包后执行,未批前真实cap=0",
"本卡后台修复与前端另卡(外接服务文案、会话页显示本次音色)需原总工一次统一复审;通过后登记最终已验证head,此前任何head不得当作执行依据",
"连续通话ASR分钟级音频时长与连接重试的程序计数手段:待补阻塞,窗口前由总管在.12验证LiveKit/worker侧指标可执行性,不通过则窗口不得启动含连续通话步骤",
"前端同锁文件npm安装复跑:单独待批项,本轮不执行",
"自定义音色删除、完整备份/恢复(CG-009/R6)后续卡;R5整体验收与R1-R7完整功能持续承接;本卡通过≠自定义音色用户验收,更≠完整产品完成"
],
"requirement_changes": [],
"verification": [
"pytest tests -q:380通过(基线376+新增4,py3.11.16复用既有venv零安装),全量含HEL-899 18项与总工故障注入全数通过",
"python -I scripts/verify_hel899_custom_voice.py:12/12(含通话中改选定住本通话、下一通生效)",
"ruff check src tests scripts:通过",
"需求检查4/4:--base-ref origin/main 与 --base-ref b09a907 各带/不带本handoff全部通过",
"未验证(如实):真实worker读时点、真实dispatch/房间、打断停声与真机路径——全部留在获批补测窗口;重叠通话不同音色的残余竞态如实记录于合同§2.5与REWORK.md,不宣称消除"
]
}
+30
View File
@@ -0,0 +1,30 @@
{
"baseline_version": "1.12",
"read_commit": "7cb73429f7a0585bed17873179a627c890336c70",
"task": "HEL-905:HEL-903 前端返修与 HEL-904 后台返修的统一离线复审(总工)。在自己独立分支 hel-905-unified-review@7cb73429 集成前端088ac62(原作者源码51669aa,11文件逐字节一致)与后台07dda2b,不重出架构、不另起视觉、不重复派工;一次审完三处前端修正(语音合成五态与ASR无单独健康检查的诚实说明、演示零API外发、session voice固定本次声音)与500/503/断连恢复流(先刷新找cg-编号条目、缺资料标注、不自动重登记/不自动删除)、后台通话持有方案的失败/取消/挂断转欠账/TTL结算路径、以及补测包材料可批性。真实cap=0,零部署、零下载安装、不发真实请求、不合并PR",
"requirement_ids": ["CG-006", "CG-009", "CG-010", "CG-013", "CG-014", "CG-015", "CG-016", "CG-018", "CG-019", "CG-020", "CG-021", "VIS-003", "VIS-004", "VIS-008", "VIS-010", "VIS-013", "UX-001", "UX-003", "UX-005", "UX-006", "AM-001", "GOV-001", "GOV-002", "GOV-003", "GOV-004", "GOV-005", "GOV-006", "DEL-001"],
"deliverable": "统一复审结论(本卡评论)+集成分支 origin/hel-905-unified-review @ 7cb73429f7a0585bed17873179a627c890336c70(merge 前端088ac62与后台07dda2b于 b09a907 之上,历史正常保留)。独立核验:11文件SHA256与原作者改后清单逐一一致;前端三处修正与恢复流按源码逐行复核(callVoiceOf只认严格id、default=默认声音、缺答=声音未说明不猜、失败/结束复位OFF、late grant被stale/gen挡住;演示模式全部效果守if(!real)不发任何请求;uncertain分类422/409/离线=不确定false、5xx/发送后断=不确定true,主按钮换刷新列表、从不自动重登记);后台持有hold在capture写文件后无await间隙登记、挂断成功/挂断失败转欠账/TTL到期/dispatch失败/请求取消五路释放、Event门控无sleep。总工独立复跑(复用既有venv311,零安装):pytest tests 380/380、verify_hel899_custom_voice 12/12、ruff src tests scripts 通过;需求检查4/4(origin/main与b09a907各带/不带本handoff)。前端构建/单测/浏览器因web/node_modules缺失未独立复跑,如实列为待批项,不以作者148/148、90/90冒充",
"acceptance": [
"集成核验:7cb73429含088ac62与07dda2b两个先祖、基点b09a907;author-manifest.json 11文件改后SHA256与统一head工作树逐一一致(含handoff JSON共11项match 11/11);前端仅作者源码,总工未替作者改视觉方向",
"前端三处修正复核:readVoiceStatus五态(none/off/ok/down/unknown)+ASR行如实写「没有单独的健康检查,以实际通话为准」;演示模式services/voiceStatus效果全守if(!real)零API外发;session voice只取本通grant严格id、命名查找按id匹配、跨id答案不显示、结束/失败复位、下一通用新选择",
"恢复流复核:uncertain(5xx/发送后断/体不完整)→提示先刷新找cg-新条目、刷新前后集合比对指认新增项、idOnly条目如实标注「只有编号,缺本地资料」并说明可试听可用、无删除入口、绝不自动重登记或自动删除;Chromium自动重发登记POST的服务端非幂等风险如实保留(重试另立新cg-编号+共享注册表孤儿条目,合同§2.2如实声明,不宣称前端提示已解决服务端幂等)",
"后台通话持有复核:capture写文件后同步登记持有(无await间隙);挂断成功、挂断失败转清理欠账、TTL到期_prune、dispatch失败、开房请求在途取消五条路径均释放持有;期间apply只记选择不写worker文件,会话响应voice=worker本通话实读;重叠通话较新capture重写文件的残余如实记录于合同§2.5,顺序用例通过不称竞态根除",
"总工独立复跑:pytest tests -q 380/380、python -I scripts/verify_hel899_custom_voice.py 12/12、ruff check src tests scripts 通过(venv311=Python3.11.16复用,零安装零下载);需求检查4/4通过",
"补测包核对(材料,不执行):连续ASR精确计数仍为待补阻塞且包内如实声明;包首行不声称材料齐备;停声量测单一时基、female_soft参考段sha256、8090新增cg-清单与回收、共享TTS快照恢复、失败计入用量等逐项在案;候选上限未超904已收紧值"
],
"deferred": [
"前端独立复跑(同锁npm安装、tsc/vitest/build、浏览器两主题×电脑/手机、录音释放/字幕/取消迟到/演示零外发):web/node_modules缺失,本轮禁止安装,列为待主卡批准项;作者148/148与90/90仅为作者证据",
"真实登记/试听/应用、真实worker读时点、打断停声帧级量测、真机麦克风:待主卡集中请批新窗口后执行,本轮真实cap=0、未部署、未触旧服务与8090/4080",
"挂断失败转欠账与TTL到期释放持有时,远端房间删除落地前的极端未入场worker仍可能读到后续写透值:与重叠通话同类残余、每worker仍整通话单一音色,建议下轮在合同§2.5残余段一并补写明(非阻塞)",
"连续ASR分钟级时长/连接重试的程序计数手段:仍为待补阻塞,窗口启动前须由总管在.12验证LiveKit/worker侧指标可执行性",
"CG-009/R6删除与完整备份恢复、R5真机与整体验收、R1-R7完整功能继续承接;本卡通过≠自定义音色用户验收,更≠完整产品完成;旧HEL-879窗口封账永不重开"
],
"requirement_changes": [],
"verification": [
"git:7cb73429为origin/hel-905-unified-review head,merge-base核088ac62/07dda2b均为先祖、基点b09a907未回退、母线与PR#11未改写",
"SHA256:docs/rework/hel903/preservation/author-manifest.json全部11项与统一head工作树一致(独立脚本核对match 11/11)",
"venv311(Python3.11.16)独立复跑:pytest tests 380/380、verify_hel899_custom_voice.py 12/12、ruff check src tests scripts 通过",
"需求检查4/4:--base-ref origin/main 与 --base-ref b09a907 各带/不带 docs/requirements/handoff-HEL-905.json 全部通过(60项)",
"未验证(如实):前端构建/单测/浏览器未独立复跑(缺node_modules,不安装);真实语音链路全部未动;补测包材料审读基于文档与源码,实际量测留获批窗口"
]
}
+45
View File
@@ -0,0 +1,45 @@
{
"baseline_version": "1.12",
"read_commit": "0b804a49ef53eb3d251347c147d96a3f6aa5fe19",
"task": "HEL-906:补齐连续语音实际用量计数与测试准备(施工;2026-10-28 总管定点复核返工)。只补 HEL-905 复审明确列出的连续通话 ASR 实际用量/连接重试计数缺口:在 CyberGril 专属 worker/适配层复用现有 LiveKit/FunASR 接口,落地每通话与全窗口的实际送入 ASR 音频量与 连接尝试/重连/失败计数;达到有限窗口上限时在发送前/建连前阻止并结束本通;上限为待用户批准的新一次性窗口,默认未布防=真实 cap=0(fail-closed,与 think/budget.py 同一先例)。定点返工只修总管复核出的两个缺口:①账本写入失败不得使预算倒退(持久化读/写失败或损坏时停止本窗出站并保全已知消耗,禁止默认零预算/继续放行);②发送失败/取消后的不确定出站不得漏计(发送前保守预记整帧、失败/取消不退还,确证送达量另列,不得宣称预记皆确证)。旧 HEL-879 封账保持原样。零模型/嵌入/真实 ASR/TTS、零新增安装下载、不启动服务、不改共享 /opt/voice-agent、不改视觉、不合并 PR。",
"requirement_ids": [
"CG-010",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "独立分支 hel-906-asr-usage-counting(基于 origin/hel-905-unified-review@0b804a4;首轮源码 ecec9df+材料 ccfc04e,定点返工源码 cf395d4+本材料提交):①src/cybergril/voice/asr_usage.py(仅标准库)——追加写 JSONL 账本、每通/全窗计数、fail-closed 默认(无 CYBERGRIL_ASR_WINDOW_ID 即拒绝一切连接/音频)、触线在发送前/建连前拒绝(AsrWindowClosed)、账本重放恢复(同窗重启不重置预算、跨窗口互不影响)、只读汇总 CLI;返工新增——保守预记 reserve_audio(发送前计入,失败/取消不退还)+确证送达 note_audio_delivered 双口径(confirmed_seconds 单独累计与落账、重放恢复)、账本写失败/读失败/行损坏/中途消失 fail-closed:停止本窗一切出站、内存已知消耗只增不减(禁止零预算回退)、error 日志与停机原因、guard_stop 落账且同窗重启重放仍停;②stt_funasr.py 真实送数接缝——建连前 check_connect、建连结果落账、发送前 reserve_audio(越限帧/账本故障在此拒绝,零字节出站)、await ws.send 成功返回后 note_audio_delivered、utterance 边界/流结束聚合落账、接收侧死亡标记;usage=None 时行为与本卡之前完全一致;③worker.py entrypoint 按 job 构造 guard(房间名为每通标识)、on_exhausted→ctx.shutdown 结束本通(触线与账本故障同一接线)、启动日志输出窗口摘要(含停机状态);④tests/test_voice_asr_usage.py 24 项确定性离线测试;⑤docs/rework/hel902/r5-followup-package.md §4 更新为返工后如实口径+里程碑表登记返工提交;本 handoff JSON",
"acceptance": [
"计数口径:音频=发送前按整帧保守预记的样本数/采样率(48k 重采样后按 16k 输出帧计,重发按重复计);发送失败/取消不退还(帧可能已出站);确证送达量(await 成功返回)单独累计,只作核对下界,不得宣称预记皆确证送达;墙钟与事后成功流水不参与",
"账本 fail-closed(总管复现回归):注入 Path.open 追加失败后已送音频仍在内存(1.3s 不回退 0.8s)、重放不得拉回旧账本值、后续 0.6s 在发送前被拒、停机回调恰好一次带原因;读失败(read_text 注入 OSError)、行损坏(坏 JSON 行)、账本中途消失(unlink)同样 fail-closed 停出站且保全已知消耗;guard_stop 落账后同窗重启仍停",
"连接口径:每次建连尝试(含失败)逐条落账;正常说完一轮后的下一次建连不是 retry,只有失败/接收侧死亡/流异常之后的建连计 retry;每通 retry 上限建连前拒绝(测试驱动真实接缝验证拒绝始于建连之前、假 provider 未被触碰)",
"到线停机:全窗口音频上限触线时越限帧在发送前拒绝(出站字节恰至上限,一个字节不越线,触线帧不碰 provider)、on_exhausted 恰好一次、worker 接线断言 ctx.shutdown 真被调用结束本通;未布防默认拒绝一切 ASR(真实 cap=0 的代码级落实)",
"历史与恢复:删房成功后 asr-usage.jsonl 逐通历史原样保留(测试用真实 VoiceSessionService 成功删房对照);同窗口重启 guard 重放恢复预算/逐通计数/确证量,不得重置;不同 window id 互不误停",
"确定性离线测试(24 项):静音/取消、建连失败/重连、反复结束、不同采样率、触线前拒绝、落盘恢复、崩溃少计有界(≤flush 如实声明)、假 ws 先记 payload 再 raise/取消后预算仍含该帧而确证量不含、worker entrypoint 注入断言",
"补测包 §4 更新为返工后口径:保守/确证双口径、账本故障 fail-closed、程序保证限定为布防进程内(跨进程 ≤5s 有界过冲、崩溃 ≤5s 少计仍为监看边界,不承诺全窗口跨进程绝不越限)、窗口执行约定 ≤3 通顺序不重叠+参数收紧预留余量;候选上限不放宽(≤2h/3通话/6开房/LLM20/60000tokens/嵌入0/登记3/预听10/通话TTS40段/ASR45分钟/每通重连6次/新增付费0)"
],
"deferred": [
"窗口前 .12 真实栈联验:用 scripts/gen_hel877_sdk_check.py 同法在真实 agent venv 加载本模块,确认真 SDK 帧率/重采样与 ctx.shutdown 真实行为(离线假件无法替代,未验证前不启动含连续通话步骤)",
"新一次性窗口仍待白栖知批准:未批前真实 cap=0,不启动、不预留出站;获批后由窗口负责人显式布防 CYBERGRIL_ASR_WINDOW_* 环境变量",
"跨进程并行通话的有界过冲(≤5s resync/flush 间隔)与崩溃少计(≤5s)属如实声明的监看边界,不冒充零误差硬闸;窗口执行约定 ≤3 通顺序执行不重叠、实操上限与 flush/resync 收紧预留余量(已写入 §4)",
"真机麦克风/文字打断帧级量测、LiveKit/worker 侧房间指标交叉核对:并入获批窗口执行,本卡不自行采麦/摄像头",
"前端浏览器/构建独立复跑(同锁 npm ci)仍为单独待批项;CG-009/R6 删除与完整备份恢复、R5 整体验收、R1–R7 完整功能继续承接;本卡通过≠自定义音色或连续语音用户验收"
],
"requirement_changes": [],
"verification": [
"pytest tests/test_voice_asr_usage.py -q:24/24 通过(venv311 复用,零安装;含总管复现的预算回退回归路径)",
"受影响语音区回归:test_voice_asr_usage+test_voice_session+test_voice_custom+test_voice_rework_hel904+test_voice_review_hel902+test_voice_chat_adapter+test_voice_rework_hel879 共 105/105 通过;按卡约定未重跑全量 380/12(作者证据保留)",
"ruff check src/ tests/ 通过",
"需求检查:python3 scripts/check_requirements.py 对 origin/main 与 0b804a4 各带/不带本 handoff 四组通过",
"未验证(如实):真实 FunASR/LiveKit/CosyVoice 栈、真实麦克风、浏览器/手机画面——零真实调用,全部待获批窗口"
]
}
+52
View File
@@ -0,0 +1,52 @@
{
"baseline_version": "1.12",
"read_commit": "038f1eeae71a9f67362d4694548d7d8116e0e35f",
"task": "HEL-907:验证网页自定义声音和三通真实交谈。白栖知批准(主卡评论 01a18246-672a-7a19-9e8a-69c6f547a232 同意这轮测试)后,在统一源码 038f1ee 上执行既有锁定依赖构建与浏览器脚本独立复跑;隔离部署(8360/8361 双入口同进程、8363 WSS 桥、8082 专属 worker、共享 TTS 9880 与面板 8090 窗口内临时恢复);真实网页链路(参考录音上传→cg- 登记→真实 TTS 试听→应用);三通真实通话验证 worker 实用音色与会话 voice 字段一致、通话中改选不换声下一通生效;并入 HEL-879 遗留的文字打断在播语音与打断停声时延量测;全程按批准上限执行并在结束后回收 cg- 条目、恢复共享服务、封账交付。",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "docs/rework/hel907/(window-record.md 窗口记录、build/ 构建与浏览器独立复跑日志、web/ 真网页链路截图与试听音频、window/ 逐通字幕/帧时间轴/回流音频/账本/快照);web/tests/hel900_shots.mjs 一处最小脚本适配(HEL-903 后 503 登记进入先刷新状态,检查意图不变,产品代码零改动);服务器证据包 /opt/cybergril-hel907/hel907-evidence-20261028.tar.gz(sha256 37ee1881…2e07e)。实测结果:网页自定义声音全链真实完成(cg-f2ec321d 登记 1 次出站、真实试听 2 次含音频存证、应用后 call1/call2 会话 voice=cg-f2ec321d 且 tts.log 合成音色逐一对应);call2 通话中改选 rain 本通不换声(worker 状态文件钉住)、call3 voice=rain 下一通生效;文字打断在播语音发现真实缺陷(文字轮被 worker 静默重试链取消、语音播完约 32 秒、文字无回复,DB/字幕/api.log 三层证据,源码机制定位至 llm_chat_api.py 重试守卫失效,本卡按边界不修);回放打断 turn_cancel 真实发生但帧级停声时延未取得(打断源过轻+旧轮无可闻声,如实不冒充);全部上限内完成(LLM 8/20 请求、23958/60000 tokens、嵌入 0、建房 4/6、通话 3/3、试听 2/10、通话 TTS 11/40、连续 ASR 195.16/2400 秒、重连 0、新增付费 0,时窗 18 分 32 秒);cg- 条目经 8090 删除入口回收、共享注册表恢复 11 支内置原样、9880/8090 按窗口前状态停回、8360/8361/8363/8082 全停、LiveKit 房间清空。",
"acceptance": [
"构建独立复跑:npm ci(既有锁文件);typecheck 0 错、lint 0 错 1 既有警告、vitest 148/148、build 成功;hel900 浏览器 83/83(1 处最小脚本适配已声明)、hel903 浏览器 90/90(零改动);本机既有 Chromium 156 运行时库解压到用户目录加载,未下载浏览器、未装系统包",
"真网页链路(真实浏览器经 HTTPS 8361):female_soft.wav 上传前 sha256 只读核对一致;登记成功 cg-f2ec321d(8090 唯一写者,参考文字手填、转写 0);真实 TTS 试听 24kHz/3.72s 音频存证且页面真实播放;应用后 /api/voice/selection 一致;逐步截图与网络事件留证",
"通话验证:call1/call2 会话响应 voice=cg-f2ec321d 与 worker 实际合成音色(tts.log)一致;字幕 v2 seq 单调、turn_end/turn_cancel 生命周期正常;挂断 DELETE 200、房间真实删除、删后仅约 20ms 在途帧",
"通话窗口持有:call2 通话中改选 rain 返回 200 而本通 worker 状态文件保持 cg-f2ec321d;call3 会话 voice=rain 下一通生效;会话响应 voice 与 worker 实读一致",
"文字打断在播语音:缺陷如实记录并可复现核对(chat_message id=6 cancelled / id=5、id=8 sent;call1.subtitles.jsonl t0=1793189878442 前后;api.log 第 122-129 行五次 POST 时序),不冒充通过",
"打断停声:回放打断 turn_cancel 真实发生(FunASR 真实识别);帧级旧声停止时延明确未取得(打断源 RMS 过轻致 VAD 15 秒后才判、旧轮 LLM 卡死无可闻声),同钟帧时间轴方法已备(call1/call3.frames.json),不捏造数字",
"上限与封账:全部类别含失败口径计数并留实时账本快照;触线为零;封账后服务全停、cg- 回收前后快照比对、11 支内置与全局默认零变化、共享服务按前快照恢复(9880/8090 窗口前停机→停回)、保护端口零接触、新增付费 0"
],
"deferred": [
"文字打断在播语音缺陷修复与复测:worker 适配器 llm_chat_api.py 重试守卫(_retry_on_chunk_sent 从未置真)与文字/语音互打断语义修复,交总管安排专项卡;修好前不得声称文字可打断语音",
"帧级打断停声时延:方法已验证,待响亮短促打断源+她正在可闻说话时机复测;真实用户麦克风起声时延(D3 用户动作:局域网设备浏览器开 https://192.168.200.12:8361,证书两处高级→继续,允许麦克风后对她说话,建议每次 ≤2 分钟)与 iOS Safari/安卓 Chrome 真机路径未测,交总管向用户交付入口",
"CG-009/R6 自定义音色删除入口与完整备份恢复、R5 整体验收、R1–R7 完整功能继续按获批依赖承接;本卡通过不等于自定义音色用户验收或 R5 完成",
"本轮窗口已封账:LLM 8/20、tokens 23958/60000 等余额不自动授权新窗口;后续窗口须另行获批并显式布防"
],
"requirement_changes": [],
"verification": [
"构建/浏览器:docs/rework/hel907/build/*.log(typecheck/lint/vitest/build/hel900-browser/hel903-browser)与 hel900-shots/hel903-shots 截图",
"网页链路:docs/rework/hel907/web/*.png 逐步截图、preview-cg-f2ec321d.wav 真实合成试听音频、webflow-result.json 网络事件与最终 selection",
"三通通话:docs/rework/hel907/window/call{1,2,3}.json + subtitles.jsonl + frames.json + agent.wav(回流音频逐通落盘)",
"用量与闸门:window/asr-usage.jsonl 与 asr-usage-summary.json(hel907-20261028-a 逐通 95.18/41.34/58.64 秒,confirmed=conservative);budget 快照在 call*.json;tts 窗口段数以 tts-window-tail.log 核对",
"缺陷证据链:call1.subtitles.jsonl + api.log + chat_message 行(id5/6/8)+ 源码定位(src/cybergril/voice/llm_chat_api.py:87-90 与 api/app.py:577-605 取消路径)",
"回收与恢复:voices-before.json / voices-before-recycle.json / panel-list-after-recycle.json 前后比对;pre/post-window-state.txt 端口与 GPU 快照;post 状态 8360/8361/8363/8082/9880/8090 无监听、8300/8310/8320/8350 原样、8340 停机",
"需求检查:check_requirements.py 对 origin/main 与 038f1ee 各带 --handoff 本文件通过(日志随交付);未验证项(真实用户麦克风、真机 Safari/Chrome、帧级停声时延)已如实标注"
]
}
+48
View File
@@ -0,0 +1,48 @@
{
"baseline_version": "1.12",
"read_commit": "5625de6606a607a9d4fd583fe8a52d93d6d2b4ea",
"task": "HEL-908 两轮定点返修(第二轮为 HEL-909 审核不通过后的集中返修一次)。第一轮修复文字打断丢字/旧轮复活/旧语音32秒(chat.cancelled 终态帧+cancelled 轮拒绝重新激活+worker 499 不重试+SpeechHandle.interrupt 停播,HEL-909 复核通过)。第二轮按总管裁定修两个核心缺口:R1 思考分流改显式整轮缓冲模式(原流式 hold 在中文句号/超128字符形状下整段泄漏且 closer 被适配层提前删除、轮级 split 无法兜底,第一轮『落库与 receipt 始终干净』表述不实已删除);R2 补上 LLM 已 final、TTS 仍在播时的权威打断通道(网页既有 hush() 入口经房间数据通道发 cg.interrupt,worker 订阅校验后 session.interrupt(force=True))。全部离线、零真实请求、零部署、零新依赖。",
"requirement_ids": [
"CG-006",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "同分支追加提交(agent/2/15e79a2458bb,第一轮 b8ec832 之上)。R1:providers/llm/reasoning.py 重做为双显式模式——TurnReasoningGate(buffer_turn,整轮缓冲零中途外发,流结束 split_reasoning 后一次性下发干净正文;取消/超时行为不变)与无启发式 ReasoningSeparator(stream 默认:仅滤显式 <think> 块跨chunk安全,stray closer 原样透传保住轮级兜底信号);openai_compat 显式 reasoning 模式参数;LLMConfig.reasoning 字面量校验+models.yaml 三档显式 buffer_turn(未配置=既有语义,向后兼容)。R2:web/src/voice/session.ts+livekit.ts 纯传输接线(VoiceRoom.interrupt(),既有 hush() 附带 publishData reliable topic=cg.interrupt,fire-and-forget 不阻塞文字发送,零视觉改动);worker.py bind_interrupt_channel(本房间 data_received 订阅,topic/发送者/载荷三重校验,session.interrupt(force=True),失败大声留痕,add_shutdown_callback 卸载);llm_chat_api._interrupt_owner 改 force=True+留痕。文档:docs/rework/hel908/summary.md 重写(删除不实表述、保留原始证据与纠偏、记录等待代价与边界),tests-remote-r2.log、web-r2.log、check-requirements.log。",
"acceptance": [
"R1 评审复现串逐字/小块经真实 OpenAICompatLLM→真实 app→持久化/receipt:'private reasoning '*20+'</think>回答。' 与 '我先想一下。内部思考</think>回答。' 的 chat.delta、落库行、chat.final、receipt text_hash 全部只含干净正文(buffer_turn 生成中零外发,修复前同输入已被本卡离线复现整段泄漏);记录流 id=12 按 1~1000 任意分块同样全干净;正常中英文正文逐字节保留;未闭合显式思考→空正文→诚实 chat.error;stream 模式显式块/推理字段/跨 chunk 语义保持,stray closer 透传可被 app 轮级 split 清理",
"R1 等待代价如实记录并测试固定:buffer_turn 首个可见正文等整轮生成完成(生成中零 chat.delta);取消轮照常 chat.cancelled 零泄漏、超时照常 chat.error;无 closer 无字段的推理不可识别边界已声明,不虚称全好",
"R2 真实 SDK 证明 8/8(agent venv 1.8.8):真实 SpeechHandle(allow_interruptions=False 覆盖 force 语义)+真实 pending 播放等待(wait_if_not_interrupted)——完整轮 chat.final 干净结束后(评审 evidence C 形状)注入 cg.interrupt 数据消息,播放等待被真实 interrupt 释放、句柄 interrupted、force=True 断言成立;非 force 在该句柄上 RuntimeError 复核;校验矩阵(错 topic/服务端来源/自身/坏载荷/错版本)零触发;卸载后零触发;session 未运行大声失败不崩溃",
"R2 app 集成记录:完整语音轮后的文字轮为普通轮(无任何行被取消、无 phantom 行);web 侧 hush() 在 speaking/thinking 才发消息且本地静音反馈保持,下一回复恢复正常收听路径未动;ChatPanel/视觉零改动",
"回归:.venv311 全量 480 passed/8 skipped(2 个 test_r3 失败在基准 5625de6 同样失败,预先存在);第一轮 5 项 SDK 检查全保持(取消终态/不复活/不重试/真实连接错误仍有限重试/完成轮流式+receipt 不变);web npm ci(既有锁文件)typecheck 0 错、lint 0 错 1 既有警告、vitest 148/148、build 成功;ruff 全绿;models.yaml 新键经 ModelsConfig 实际解析"
],
"deferred": [
"真实端到端复测(真实房间/真人打断时延/真机/真实端点思考出现频率与长回复起播延迟)须按授权窗口安排;R2 离线证明是真实 SDK 句柄/等待语义+实装源码核对,不冒充端到端毫秒指标",
"无 closer、无 reasoning 字段、无开头标签的推理任何客户端模式都不可识别,彻底解决需上游自行分流(字段或服务端 parser),零真实请求边界内无法探测——最小缺口如实声明",
"chat.cancelled 的前端可见呈现与 ChatPanel 取消判定 limit 扩大(HEL-909 建议 #3/#4,非阻塞)未动:前者属界面核心交视觉总监,后者纯展示层既有行为",
"gen_hel877_sdk_check harness 在基准上即因 HEL-906 导入失效(前轮已留证),交总管决定维护安排;本卡 SDK 验证由 test_sdk_livekit_hel908.py 覆盖(8 项,agent venv 可独立复跑)",
"R1–R7 完整功能、真实用户麦克风、视频/口型、记忆长跑继续按获批依赖承接;本卡不构成整产品完成"
],
"requirement_changes": [],
"verification": [
"docs/rework/hel908/summary.md:两轮根因/纠偏/方案/等待代价/边界全记录;第一轮不实表述已删除,原始证据与纠偏保留",
"docs/rework/hel908/tests-remote-r2.log:.venv311 全量 480 passed/8 skipped/2 pre-existing + agent venv SDK harness 8/8 实况",
"docs/rework/hel908/web-r2.log:typecheck/lint/vitest 148/148/build",
"tests/test_sdk_livekit_hel908.py 可在任何装有 livekit-agents 1.8.8 的环境复跑(pytest 或 --standalone),命令写在文件头",
"需求检查:check_requirements.py 对 origin/main 与 b8ec832 各带 --handoff 本文件通过(日志 docs/rework/hel908/check-requirements.log)"
]
}
+51
View File
@@ -0,0 +1,51 @@
{
"baseline_version": "1.12",
"read_commit": "958c6962fb1f2a4462a6df794d893aee1fcb0667",
"task": "HEL-910:按白栖知批准的《HEL-797-交谈修复复测安排》(主卡评论 01a18753-8c70-7181-9758-5bab4685ba9f「继续测」)对候选 958c696 做两次真实通话复测:真实网页原发送入口的文字打断(在播时机+final待播间隙)、python 同钟时间轴的等待链与回放打断、思考隔离真实端点检查;隔离部署 8360/8361/8363/8082 + 共享 TTS 9880 按快照临时恢复,全程批准上限内执行并清理恢复。",
"requirement_ids": [
"CG-006",
"CG-009",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "docs/rework/hel910/(window-record.md 窗口记录、window/ 逐通证据含 call2 同钟字幕/帧时间轴/回流音频/DB终态/ASR账本/前后快照/日志、web/ 真网页逐步截图、tools/ 本窗部署与测量脚本及配置);服务器证据包 /opt/cybergril-hel910/hel910-evidence-20261029.tar.gz(sha256 83363576…d7a3f0)。实测结论:真实网页发文字(真 Enter 键)在她语音在播时发送——旧声停止(浏览器能量 ≤~1.4s 持续静默、本地 hush 静音即时)、文字获完整回复并落库、旧轮不复活无幽灵行、被打断轮回执正确缺席、后续语音轮正常出声并完整回执(两次确认恢复);输入结束→实际第一声实测 9.53s(user_final 后 7.10s、生成开始后 3.44s,>5s 关注线,如实交回);全部真实回复零思考泄漏但真实端点未出现带 </think> 形状(覆盖范围如实限定,离线证据在 HEL-909);时机A(生成中被文字取消)因网页侧检测失误与 API 直发竞速反向未获正面样本(文字轮被更新语音轮正常取消:行 cancelled+SSE 幂等 chat.cancelled 终态帧,恰为 HEL-908 R1 修复路径的真实表现),随后语音轮撞 10/10 预算硬闸诚实 φ 失败;可闻旧声期间的回放打断样本因时机错过未取得(如实,与 HEL-907 同项均未取得)。用量:LLM 10/10 触线(30,398/40,000 tokens)、建房 4/4 触线、通话 2/2、TTS 6/20、ASR 350.63/720s(保守=确证、重试 0)、窗口 23.6/45 分、登记/上传/转写/试听/嵌入/付费全 0。清理:窗口端口与专属进程全停(精确 PID)、LiveKit 房间空、voices.json 哈希前后一致 cg- 零残留、共享 TTS 按快照停回、面板 8090 全程未启用、8300/8310/8320/8350/7880/10095 未动、GPU 回 747MiB。",
"acceptance": [
"部署核验:服务器源码树与 git archive 958c696 逐文件 sha256 一致(仅多 REVISION 标记);web/dist 含 cg.interrupt 接线;models.yaml 三档 reasoning: buffer_turn 与候选一致仅预算块不同;cap=0 fail-closed 预检(persona 拒绝、chat 诚实 error 零出站、voice 503 不建房)通过后才布防",
"真实网页链路:自签证书按用户姿态逐 origin 接受;语音通话从真实 UI 开启;文字从真实输入框+真实 Enter 键发送(hush→cg.interrupt 真实链路,未绕开网页修复);逐步截图 9 张留证",
"时机B(在播时发文字):旧声停止(浏览器能量探测 ~1.4s 内持续静默+本地静音即时)、T1 完整回复落库 sent、R1 不复活、后续语音轮正常出声并完整回执",
"final待播间隙(网页入口):U3 回复文本完整落库(音频截断、回执正确缺席)、T2 完整回复、再次语音轮正常出声回执",
"等待链(同钟毫秒):input_end→user_final 2.43s→生成开始 4.10s→实际第一声 9.53s→字幕全文/落库 12.10s→回执 ack;数字与 wav 能量对齐方法留证",
"思考隔离:18 条 companion 行/字幕/TTS 文本零思考;如实声明真实端点未出现带 closer 形状",
"上限与封账:全部类别在上限内或触线即停并如实记录;失败/取消均计入(call1 挂断后 U4 轮、call2 竞速取消轮与预算 φ 轮)",
"清理恢复:端口/房间/PID/声音列表/哈希/GPU 前后核对留证;隔离目录与证据包保留;未触碰任何既有实例与真实档案"
],
"deferred": [
"真实用户麦克风打断时延与可闻旧声期间的语音打断停声样本(两轮工程窗口均未取得,需真人同轴或下轮工程窗口以她实际有声为门控重试)",
"时机A(生成中文字取消)正面样本(需文字在语音轮服务端激活后、生成完成前到达;本轮竞速反向+预算封顶)",
"真实端点带 </think> 形状的分离复现(本轮真实回复未出现该形状;离线证据 HEL-909)",
"首声等待是否经常>5s 需多样本(本轮 1 精确样本+1 量级相近);停声时延的帧级精测(本轮为浏览器能量粗粒度 ≤~1.4s)",
"真机/手机麦克风、Safari(D3 用户动作)",
"R1–R7 完整功能未完成项继续按主卡承接;本轮不改变任何需求与验收状态"
],
"requirement_changes": [],
"verification": [
"python3 scripts/check_requirements.py --base-ref origin/main(及 --base-ref 958c696)+ --handoff docs/requirements/handoff-HEL-910.json 全部通过(结构检查,不代替原话核对与体验验收)",
"证据:docs/rework/hel910/window/(call2.subtitles.jsonl 逐事件毫秒时间戳、call2.frames.json 帧到达时间轴、call2.agent.wav 回流音频、db-final.json 全部 20 行含状态与回执账、asr-usage.jsonl、tts-window.log 6 段、api/worker 日志、前后快照);web/ 截图;tools/ 全套部署与测量脚本",
"未验证:真人麦克风、手机真机、真实端点思考形状、多样本首声统计——见 deferred,不冒充"
]
}
+46
View File
@@ -0,0 +1,46 @@
{
"baseline_version": "1.12",
"read_commit": "407fc8158ce392e20ee51c05420c8b74aa545a92",
"task": "HEL-911:修好 HEL-910 实证的两个 livekit-agents 1.8.8 SDK 接缝错误(worker _on_item 对 AgentHandoff 取 role 的 AttributeError;add_shutdown_callback 对同步卸载回调 await None 的 TypeError),并用已有原始证据+实际 SDK 源码+离线复现查清首声 ~9.5s 等待的分段:识别终稿→生成开始的 3.66s 系 1.8.8 默认补入的英文 EOU TurnDetector 对中文终稿的 unlikely 判定把 endpointing 顶到 max_delay=6.0,显式 turn_detection=None 修复(离线 A/B 3.704s→0.004s);同时纠正 HEL-910 交付说明四处事实错误并追加纠偏记录、为下次测量修工具可靠性(分段落盘+服务端活动门控,仅离线验证)。全程真实 cap=0:零 LLM/ASR/TTS/嵌入/登记/试听调用、不建房不开窗不恢复服务,仅只读复用 .12 agent venv 实物 SDK 与既有 wav 做离线证明。",
"requirement_ids": [
"CG-006",
"CG-010",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"AM-001",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-005",
"GOV-006",
"DEL-001"
],
"deliverable": "src/cybergril/voice/worker.py:_on_item 按 1.8.8 ConversationItemAddedEvent.item 判别字段过滤(仅 type==message 且 role==assistant 的 str 部件正文进回声过滤);bind_interrupt_channel 卸载改协程(SDK 关停回调一律被 await)、幂等、断连不抛,interrupt(force=True)/回执/预算路径不动;TURN_HANDLING 显式 turn_detection=None(endpointing 0.8/6.0 逐字保留,不降 VAD 阈值、不动 buffer_turn)。scripts/gen_hel911_wait_repro.py:真 SDK+真 silero VAD+实测时间轴的离线复现(BASELINE 3.704s 复现实测 3.667/3.654s,修复态 0.004s)。tests/test_sdk_livekit_hel908.py +3 项真 SDK 检查(共 11,.12 agent venv 11/11;既有 8 项回归全保持,unload 调用点随协议改 await)。docs/rework/hel910/window-record.md §9 追加纠偏记录(原文未回改);docs/rework/hel910/tools/hel910_call1.mjs 可靠性修正(探针每 5s 分段落盘 call1-probe-live.jsonl+录音分片即存,防超时杀进程丢证;时机A 触发改为服务端已把语音轮落成 chat 行+生成中相位双条件,不再以页面相位/己方发送为 API 已激活代理;node --check 通过)。docs/rework/hel911/summary.md 完整分段归因与验证记录。产品测试套 .12 规范环境 480 passed/11 skipped/2 failed(两个 test_r3 预先存在与 HEL-908 记录一致);本地 py3.10 与基准 407fc81 干树失败集合完全相同(环境差异非本卡引入)。",
"acceptance": [
"真 SDK(.12 /opt/voice-agent/agent/venv livekit-agents 1.8.8)驱动:AgentHandoff/_TypeDiscriminator/user 消息不触发崩溃且不污染回声过滤,assistant 混合 content 只取 str 正文;真 JobContext.add_shutdown_callback 包装协议下协程卸载通过、旧同步形状经同一包装复现生产 TypeError(反向证据)、重复 await 幂等、卸载后监听真摘除;TURN_HANDLING 下 _turn_detection 为 None 且默认探测器存在性前置断言",
"等待链离线复现(复用 HEL-910 实际回放 wav、实测时延脚本化):BASELINE 终稿+2.443→llm.chat+6.142(3.699s,对上实测 3.667/3.654),修复态 +2.439→+2.444(0.005s);VAD 端点 ~0.7s、FunASR 链+0.6s 去抖 2.43s、服务端整轮 ~1.4s、TTS 首包 1.614s 各段归因落在源码行与实测日志",
"既有回归保持:SDK harness 11/11(文字取消不重试/force 停声/通道校验/未启动存活等);产品套 .12 规范环境 480/2 预存失败与基准一致;web 未动未重建",
"纠偏记录入 window-record.md §9:companion 9 条非 18、生成起点 +6.099s 非 +4.10、assistant_final 是播放收尾/回执门非首次全文生成/落库、post-window.txt ListRooms 为 invalid authorization token 不能证明空房(总管独立核实空房成立);原快照/录音/日志一字未改",
"用量:真实 cap=0 全程保持——零模型/识别/合成/嵌入/登记/试听调用、零建房、零窗口、未恢复 9880/8090 或任何本窗服务、未访问 4080、未改绑定;.12 上仅只读使用 agent venv 与既有 wav,临时副本 /tmp/hel911 已清"
],
"deferred": [
"修复后首声真实总时延(离线推算 9.5→约 5.9s,不承诺):需下轮真实工程窗口复测(工具门控与分段落盘已备好)",
"FunASR 适配器 0.6s 去抖与 2pass 离线纠错往返是否可再压:识别链参数,本卡不动语义,与真实复测同窗评估",
"真实用户麦克风打断时延/可闻旧声回放打断正面样本/生成中文字取消正面样本:两轮窗口均未取得,需按新门控重试",
"真实端点带 </think> 形状分离(离线证据 HEL-909);首声是否经常>5s 需多样本",
"真机/手机麦克风、Safari(D3 用户动作);R1–R7 完整功能继续主卡承接,不宣布整产品完成"
],
"requirement_changes": [],
"verification": [
"python3 scripts/check_requirements.py --base-ref origin/main 与 --base-ref 407fc81,均带 --handoff docs/requirements/handoff-HEL-911.json 通过(结构检查,不代替原话核对与体验验收)",
"定向验证:tests/test_sdk_livekit_hel908.py --standalone 于 .12 agent venv 11/11;scripts/gen_hel911_wait_repro.py BASELINE/FIXED 两模式;产品套 .12 .venv311 480 passed/2 预存失败;hel910_call1.mjs node --check",
"未验证(如实):修复后真实通话首声数字、真实麦克风、手机/真机、思考带 closer 真实端点形状——见 deferred,不冒充"
]
}
+58
View File
@@ -0,0 +1,58 @@
{
"baseline_version": "1.12",
"read_commit": "4a38106eef21c43ba24ab7d07992b1bfe539137d",
"task": "HEL-913(含 2026-10-29 总管取件后同卡集中收口):把通过 HEL-912 审核的统一候选(产品基线 agent/2/8e99cc2cc592 @ 4a38106)装成可交接的统一体验运行包并在原候选分支一次收口五项核心缺项:①整包版本/目录可执行(产品基线与运行包提交区分、部署照抄入口=运行包提交、目录/配置/dist 复用/空档案守门完整准备步骤、不跳旧 HEL895 通用步骤);②启用建议收敛为内部一致的一份待批方案(更正「总管权限内新批 A」为全部待批、语音改 3 通×≤10 分钟=≤1800 秒 ASR 硬闸并逐项交代碰顶后果、B/C 边界消除矛盾、T0 计时点/当地日顶/团队准备与用户参与/可执行到期停服);③真实记忆最少安装/接线补全为完整可审命令(隔离 venv、逐项哈希留证、失败即停、co-import、权威账 498/冻结 2 续账不搬测试聊天);④启停实际接线/失败收尾(ASR 布防三变量入模板默认拒绝、wss 桥必需、TTS 限段/房间/总时限唯一接线、启停脚本身份核验版 42 项离线替身验证);⑤事实表只修明确错误(生成中取消未获样本、打断停声非精确口径、首声不承诺推算值、34/34 口径限制如实)。本卡不部署、不监听、不下载、不安装、cap=0、不合 PR、不动现有实例与资料、不向受保护 venv 增改依赖。",
"requirement_ids": [
"CG-006",
"CG-007",
"CG-008",
"CG-009",
"CG-010",
"CG-012",
"CG-013",
"CG-014",
"CG-015",
"CG-016",
"CG-018",
"CG-019",
"CG-020",
"CG-021",
"CG-023",
"AM-001",
"AM-002",
"AM-003",
"GOV-001",
"GOV-002",
"GOV-003",
"GOV-004",
"GOV-006",
"DEL-001"
],
"deliverable": "deploy/hel913-unified/ 收口版:README(唯一照抄入口:§0 产品基线 vs 运行包提交与自校验、§4 首次准备+启停、§5 语音窗口唯一接线表、§7 离线验证口径如实、§8 Mem0 完整可审安装 W0–W6 与权威账续账)、manifest.json、dist-manifest.json、models/voice/worker.env/voice-window-arm 五份脱敏模板(预算全 0/未布防)、api_ctl.sh/worker_ctl.sh 身份核验版、bridge/tts/force_end 三个窗口模板(默认不生效)、verify_offline.py、test_ctl_scripts.sh、mem0-cp311-wheels.txt;docs/rework/hel913/{verify-offline-run.txt,ctl-checks-run.txt};docs/hel913-capability-and-enablement.md(能力表纠错 + §2 单份一致的待批启用方案)。产品代码(src/web/config/tools/tests)自 4a38106 起零改动(git diff --stat 为空可验证)。 总管取件勘误见运行包README §10:语义配置与离线缓存环境、真实会话列表解析、临时语音服务失败收尾、传输校验与固定解释器、TTL600秒、定时服务及人工监督边界。",
"acceptance": [
"整包可执行:部署入口=运行包提交(含本目录);checkout 4a38106 会没有运行包——README §0 明确两者区别并给 merge-base 与 diff 空两条自校验;首次准备含 mkdir 骨架/模板落位/dist 哈希核验后只读复用/verify_offline 空档案守门复验,单一入口、不跳 HEL895 通用总装步骤",
"启停安全(42/42 离线替身验证,docs/rework/hel913/ctl-checks-run.txt):非法子命令拒绝、重复 start 拒绝、PID 复用拒杀(身份核验=cmdline+cwd)、启动失败清自己留日志、worker .env 占位/解析守门、arm 模板默认全 0 可解析",
"语音窗口唯一接线(README §5):ASR 三变量硬闸默认拒绝一切 ASR;session_ttl 600s+挂断删房;TTS 段数监控口径+tts.log 起始字节;force_end now|watch 到期可执行停服(各按专属句柄);wss 桥必需并给出默认 8363 接线",
"Mem0 安装完整可审(README §8):固定源双哈希核对→获批窗口下载 19+40 轮逐项 sha256→传输复核→隔离 venv /opt/cybergril-hel913/venv311 --no-index 安装→co-import→92MB 缓存落位→暂存清理;绝不向 /opt/cybergril-hel879/.venv311 增改;权威账只复制账本不搬测试聊天,authority 冻结前缀+embed_cap=680 累计算式;本卡零安装零下载零嵌入",
"启用方案单份一致且全部待批(capability §2):A 55 次/50 万 tokens 为建议顶、总管不自行放行、不并入用户实例;B 语音 3 通×≤10 分钟=≤1800 秒 ASR 硬闸(含重连≤2、TTS 60 段监控、上传/试听 ≤10 次监督口径)、语音 LLM 计入 B 对话日账、T0=交付时刻/本地自然日/团队准备不占额度、碰顶后果逐项、到期=force_end+seal 可执行且封账不解除;B 窗口内生活/主动计入 B 日账、窗口后全停,与 C(暂不批)无重叠口径",
"事实表纠错:生成中文字取消=未取得有效样本(未验证);打断停声=作者粗略观察非精确口径;首声修复后未实测、不承诺推算值;34/34 的 deps 只打印 4 包版本、空库裁剪未做种子对照——均在 README §7 与能力表如实标注;天气/素材/备份/摄像头/真实口型仍 ❌ 并追到承接",
"边界保持:未部署未监听、8300/8310/8320/8350 与资料未动、8340/ComfyUI 停机、4080 未触、受保护 venv 未增改、需求与用户原话零改动"
],
"deferred": [
"实际部署与启用:待总管向白栖知说明获批后按 deploy/hel913-unified/README.md §4 执行(四步:安装→独立部署→团队检查→用户首用,见 capability §2 B 前置)",
"Mem0 cp311 安装(README §8.3 W0–W6:19+40 轮获批下载窗口 + 隔离 venv --no-index 安装 + co-import + 缓存落位 + 权威账续账):待一次性批准,本卡零安装零下载",
"语音窗口执行:布防值(voice-window-arm 模板)与到期 watchdog/systemd timer 待批后由窗口负责人接线",
"persona 自主诠释、生活事件、主动留言的真实生成:从未在真实窗口发生(预算 0),A 团队检查与 B 用户首体验窗口补证",
"语音遗留实测:修复后首声、真人麦克风打断、生成中文字取消正面样本、真机/手机/Safari(不承诺任何推算值)",
"R5–R7 完整功能(每日自动备份/天气/素材/摄像头/真实口型)继续主卡承接,不因运行包完成而关闭"
],
"requirement_changes": [],
"verification": [
"python3 scripts/check_requirements.py --base-ref origin/main 与 --base-ref 4a38106eef21c43ba24ab7d07992b1bfe539137d,均带 --handoff docs/requirements/handoff-HEL-913.json 通过(结构检查,不代替原话核对与体验验收)",
"docs/rework/hel913/verify-offline-run.txt:34/34(.12 venv311、产品基线树、零监听零出站;总管同环境独立复跑亦 34/34、dist 哈希吻合——已过项保留不重跑)",
"docs/rework/hel913/ctl-checks-run.txt:42/42 启停离线替身检查(工作站、stub、零真实服务/零监听/零出站)",
"产品代码零改动:git diff --stat 4a38106 <运行包提交> -- src web config tools tests 为空",
"未验证(如实):运行包尚未在任何现网端口实际起服(cap=0 未部署);修复后语音首声、真机、Safari——见 deferred,不冒充",
"总管机械收口:docs/rework/hel913/coordinator-close-checks.txt 17/17定向离线检查;真实sessions列表生成DELETE、环境变量传入、非法命令拒绝、失败句柄清理与shell语法;未安装/监听/调用真实服务。"
]
}
+2 -1
View File
@@ -15,5 +15,6 @@
"comment-evidence-mature-reuse-20261025.json": "38ca076e26477e89cd161eaadb0fb00b0ee7bb5a26de2bd6ab41de52faaae79e",
"mature-reuse-principle-20261025.md": "a2a75e071d3af5db722727bfaf8083bb60d07dcdd74e90d1e1374c1aafd2064d",
"other-agent-discussion-20261025.txt": "42cd97029d73eace110e49e0566343dfeb25fe4f0633a10b78b126289938aaeb",
"comment-evidence-full-experience-20261026.json": "75dd55b8755bdad217b44c3f516c9372b046aae06dff511527014c7b209eccff"
"comment-evidence-full-experience-20261026.json": "75dd55b8755bdad217b44c3f516c9372b046aae06dff511527014c7b209eccff",
"comment-evidence-custom-voice-20261027.json": "ed72bd78218e1c0b4ca644bf933d6bdb2e3aa8cd2c0333f3410f6b60845bd3cb"
}
@@ -0,0 +1,10 @@
[
{
"id": "01a17de1-2892-78be-a422-eb6a11ffebfd",
"author_type": "member",
"author_id": "e5c6c938-db48-4ff9-954f-4fb8b063a132",
"content": ".12已恢复,另外以上一条让我解压附件包试听,我不明白是什么意思,是让我听预设音色吗?如果是我觉得你的设计不够合理,我想要的是可以自定义音色,我的.12上安装的TTS是支持自定义音色的。至于最长2小时,3次通话的测试,我同意,音色你自己决定就行",
"created_at": "2026-10-27T15:01:10Z",
"parent_id": "01a17d45-f14e-7b62-9850-646e040c7318"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

@@ -0,0 +1,12 @@
{
"ok": true,
"checks": 9,
"fail_count": 0,
"netguard": {
"guard_loaded": true,
"nonloopback_connect_attempts": 0,
"nonloopback_dns_lookups": 0,
"rejected": []
},
"service_epoch_isolation": "CYBERGRIL_MODELS loopback, caps 0, socket guard rejecting non-loopback"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

+47
View File
@@ -0,0 +1,47 @@
# HEL-879 · D1 参考试听包(零合成,已交付)
> 交付时间 2026-10-27。性质:把服务器既有 11 支音色的 zero-shot 参考录音整理成
> 可离线播放的 HTML 试听页 + 音频包,作为主卡 D1 拍板材料。**零 TTS 合成、零
> 克隆、零共享音色切换、零下载安装**;对 .12 只做 SSH 只读复制,未改服务器任何
> 文件与状态。
>
> **2026-10-27 勘误(主卡授权轮)**:白栖知反馈「让我解压附件包试听」的设计
> 不合理(主卡评论 `01a17de1-2892-78be-a422-eb6a11ffebfd`)——他要的是可
> 自定义音色,而不是从预设音色里挑一个。本包正名为「**非定制功能入口**」:
> 它只是既有 11 支登记音色的参考原声对照材料,**不是**用户音色定制/上传/录制
> 的功能入口;正式的自定义音色(上传、录制、试听、应用)由独立施工分支接续,
> 未完成前不宣称可用。本轮 R5 有限测试由总管选定临时测试音色 `female_soft`
> (已核服务器登记;临时测试值,不是用户最终音色),不要求用户解压本包选 ID。
## 包内容(随本卡评论附件交付,不入仓库大媒体)
`HEL-879-D1-参考试听包.zip`(4.4MB,sha256
`e20764f9f98cbfac5ef966227540a75c5061cf16d151e5d0c9d86b620e3972f9`):
- `index.html` —— 离线试听页(自包含,无外部依赖;解压后直接用浏览器打开),
逐支列出音色 ID、时长/采样率、参考录音原文、播放器、文件哈希前缀;
页首明确注明「参考录音 ≠ 实际合成效果」。
- `manifest.json` —— 机器可读清单:11 支音色的 id / 文件 / prompt_text /
服务器源路径 / sha256 / 字节数 / 秒数 / 采样率 / 声道(本目录留有同内容副本)。
- `voice_samples/*.wav` —— 11 段参考录音原文件。
## 来源与取回方式(可复核)
- 音色登记:`192.168.200.12:/opt/voice-agent/cosyvoice/voices.json`(实读,
11 个 ID:bailu、bajie、default、female_soft、haimianbaobao、jiaojiao、
male_warm、nezha、rain、sunwukong、xiaoyun;每项 `prompt_wav` + `prompt_text`)。
- 音频文件:`prompt_wav` 所指 wav(`voice_samples/<id>.wav` 10 支 +
default 用 `CosyVoice/asset/zero_shot_prompt.wav`),`scp` 只读取回。
- 完整性:每文件本地 sha256 与服务器 `sha256sum` 输出逐一相符(见
manifest.json;default 为 IEEE float32/24kHz/3.5s,bailu 为 44.1kHz 立体声
10s,其余 16k/32k 单声道 2.4–16.8s)。
## 边界(如实)
- 参考录音是各音色**来源说话人的真实录音**(zero-shot 克隆依据),不是
CosyVoice 实际合成产物;实际合成效果与参考录音不同,需在获批测试窗口内
真实合成复核(计入 TTS 预算)。
- 参考录音原文为各来源录音本身的内容,按原样保留,仅作试听对照;其中数支
为影视/角色向来源,是否适合「她」由白栖知试听后判断,工程不代选、不预筛。
- 通用演示当前 `voice_state.json` = rain 是共享演示状态,不是本用户的选择依据。
- 克隆新音色需写共享 `voices.json` 并触发共享 TTS 热加载,**需单独授权**,本包未做。
@@ -0,0 +1,135 @@
{
"package": "HEL-879 D1 参考试听包(零合成)",
"generated": "2026-10-27",
"source_server": "192.168.200.12 (SSH 只读取回,未改服务器任何文件)",
"source_registry": "/opt/voice-agent/cosyvoice/voices.json",
"count": 11,
"notes": [
"参考录音 = 该音色 zero-shot 克隆的来源录音(音色来源说话人的真实录音),不是 CosyVoice 实际合成产物;实际合成效果与参考录音不同,需在获批测试窗口内真实合成复核(计入 TTS 预算)。",
"通用演示当前使用 rain,是共享演示状态,不是本用户的选择依据。",
"本包只读取得,未切换/修改任何共享音色状态;克隆需另行单独授权。"
],
"voices": [
{
"id": "default",
"file": "voice_samples/zero_shot_prompt.wav",
"prompt_text": "希望你以后能够做的比我还好呦。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/CosyVoice/asset/zero_shot_prompt.wav",
"sha256": "c7b31d6dbe7cc6a716dded00550db5b50940bf209e424e4ad207b12e657c8ff6",
"bytes": 334138,
"seconds": 3.5,
"sample_rate": 24000,
"channels": 1
},
{
"id": "sunwukong",
"file": "voice_samples/sunwukong.wav",
"prompt_text": "俺老孙来也,俺乃500年前大闹天宫的齐天大圣孙悟空,这里山势险峻,只怕藏有妖精妖怪,哪里逃吃?俺老孙吧。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/sunwukong.wav",
"sha256": "a0cf44a8ab659ebddb8bc4faffe88f717d14d2c66c09bc5ea657212826806e68",
"bytes": 429746,
"seconds": 13.4,
"sample_rate": 16000,
"channels": 1
},
{
"id": "nezha",
"file": "voice_samples/nezha.wav",
"prompt_text": "天雷滚滚,我好怕怕劈的我浑身掉渣渣,突破天劫,我笑,哈哈逆天改命。我吹喇叭滴答滴答,滴滴答。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/nezha.wav",
"sha256": "9911a73e3f54c235dedb05c18f29356f41ba3b1e1b58811c4f766897f0fe1d10",
"bytes": 305196,
"seconds": 9.5,
"sample_rate": 16000,
"channels": 1
},
{
"id": "bajie",
"file": "voice_samples/bajie.wav",
"prompt_text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去画个圆。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/bajie.wav",
"sha256": "1f9c84ce8c10145ace5f615a428a08e2482f322363f037245878068aa95ed98a",
"bytes": 424320,
"seconds": 13.3,
"sample_rate": 16000,
"channels": 1
},
{
"id": "haimianbaobao",
"file": "voice_samples/haimianbaobao.wav",
"prompt_text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/haimianbaobao.wav",
"sha256": "c60912dfccd76390baa921d4c99b0120f528837608964290e96cad1221da9c9d",
"bytes": 228726,
"seconds": 7.1,
"sample_rate": 16000,
"channels": 1
},
{
"id": "female_soft",
"file": "voice_samples/female_soft.wav",
"prompt_text": "这点我说一下,就是因为那个就是摩根豪泽尔这本儿金钱心理学。其实在我们这行业里也算一本很有名名的书了。就我之前也读过。然后你说到这点啊,让我想起来他这本书里还挺。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/female_soft.wav",
"sha256": "4a65d17099239711601cb17e28b7aa7b6149acd153b92fd0366c23902bd4f687",
"bytes": 341192,
"seconds": 10.7,
"sample_rate": 16000,
"channels": 1
},
{
"id": "male_warm",
"file": "voice_samples/male_warm.wav",
"prompt_text": "um a while we built eight hype looe test track adjacent to space acts just for a student competition。 um to encourage intiative ideas in transport and actually ends up being the the biggest um vacuum chamber in the world after the launch。 haveringcollider。",
"server_prompt_wav": "/opt/voice-agent/cosyvoice/voice_samples/male_warm.wav",
"sha256": "2c4109b2d685e1923ed66433eb08c92047a1f67510629a27edf49af4e5c606dd",
"bytes": 539032,
"seconds": 16.8,
"sample_rate": 16000,
"channels": 1
},
{
"id": "xiaoyun",
"file": "voice_samples/xiaoyun.wav",
"prompt_text": "盯着你看怎么啦,难得看你这么有意思的样子嘛",
"server_prompt_wav": "/opt/voice-agent/web/../cosyvoice/voice_samples/xiaoyun.wav",
"sha256": "6ad3ca7a6f1d9ddd10c5647338200fc9913f7adc71d8495a61b6b8e18eadc87e",
"bytes": 297004,
"seconds": 4.6,
"sample_rate": 32000,
"channels": 1
},
{
"id": "rain",
"file": "voice_samples/rain.wav",
"prompt_text": "你是不是早就想睡我了呀",
"server_prompt_wav": "/opt/voice-agent/web/../cosyvoice/voice_samples/rain.wav",
"sha256": "37db24611b999f2f2328fa19c5c6e26117b5ec5d3d2037068af2fc12c6ad25ba",
"bytes": 152364,
"seconds": 2.4,
"sample_rate": 32000,
"channels": 1
},
{
"id": "jiaojiao",
"file": "voice_samples/jiaojiao.wav",
"prompt_text": "怎么还有没射的啊,是我不够努力吗",
"server_prompt_wav": "/opt/voice-agent/web/../cosyvoice/voice_samples/jiaojiao.wav",
"sha256": "6db2b2de2d59e22bb0badbb1a6325d68885eda409ad63f5941e5ccad6486fc9f",
"bytes": 301484,
"seconds": 4.7,
"sample_rate": 32000,
"channels": 1
},
{
"id": "bailu",
"file": "voice_samples/bailu.wav",
"prompt_text": "烈火军校啊,招摇啊这种类型,我都是那种比较……呃……豪爽,比较开朗的那种类型,阳光大女孩儿,那种感觉",
"server_prompt_wav": "/opt/voice-agent/web/../cosyvoice/voice_samples/bailu.wav",
"sha256": "3fbc0aff09118a9bc10bae87a837e6a3de6a30268c6ee01c727b56c49aecd26d",
"bytes": 1769550,
"seconds": 10.0,
"sample_rate": 44100,
"channels": 2
}
]
}
+124
View File
@@ -0,0 +1,124 @@
# HEL-879 · R5 有限真实语音测试窗口记录(2026-10-27)
> 授权:白栖知主卡评论 `01a17de1-2892-78be-a422-eb6a11ffebfd`(同意最长 2 小时 /
> 3 次通话;音色由工程自定);额度沿《r5-test-package.md》§6 申请上限执行。
> 窗口(UTC):**15:26:54 – 15:58:10**(31 分 16 秒,未用满 2 小时)。
> 候选 `hel-879-assembly` @ `8dc84f8`;模型/运行时/认证与既有实例一致(同一
> `192.168.200.11:8731` halogen 端点),零新增付费支出。
## 1. 部署布局(全部独立,已全部撤除)
| 组件 | 端口/路径 | 说明 |
| --- | --- | --- |
| 统一实例(同进程双入口) | HTTP 8360 + HTTPS 8361 | `deploy/voice/serve_dual_8360_8361.py`,隔离数据 `/opt/cybergril-hel879/data/`;`.venv311`+uv CPython 3.11.16 自本机离线复制(.12 无 uv/node,零下载) |
| WSS TLS 桥 | 8363 → 127.0.0.1:7880 | `deploy/voice/wss_bridge.example.py`,与 8361 同一张自签证书 |
| 专属 worker | 健康口 8082 | `/opt/voice-agent-cybergril/`,复用 agent venv(livekit-agents 1.8.8),`voice_state_cybergril.json` = `female_soft`(临时测试值;未写通用助手 `voice_state.json`/`voices.json`) |
| 共享 TTS(CosyVoice) | 9880 | 窗口内从停机恢复;结束时核实无他人占用后按窗口前状态停回 |
| 测试档案 | — | 隔离实例自建测试伴侣(名字「小测」),非用户真实档案 |
配置差异(均记录在案、可回滚):`models.yaml` 预算写入本轮批准值
(dialogue 40、total 40/500k,其余类别 0);`CYBERGRIL_SEMANTIC=stub`(嵌入 0);
`CYBERGRIL_R2_ENABLED=1`(receipt 签发依赖生活运行时开启;life/background 预算
仍为 0,实测无后台模型调用);`voice.yaml` `session_ttl_seconds: 900`
(真实验证 TTL 到期删房用;仓库默认 7200)。
## 2. 真机发现并修复的代码问题(有限返工,含证据)
1. **Twirp 服务 token 缺少端点授权(401)**:`livekit_wire.mint_server_token`
原按「无 grants 形态」铸造,但已装 SDK 的 `create_dispatch` 以
`VideoGrants(room_admin=True, room=…)` 签名、`delete_room` 以
`VideoGrants(room_create=True)` 签名;真机 LiveKit 拒绝无授权 bearer(401)。
离线期 8 项 SDK 检查只比对了 token 形态,未对真服务器发送,未暴露此问题。
修复:`mint_server_token` 增加 `video` grants 参数,两处调用按 SDK 同款授权。
2. **protobuf 响应不可解析**:CreateDispatch 以 `application/protobuf` 请求时
服务器回二进制,原实现的 `json.loads` 崩溃。修复:改走 Twirp JSON
(`application/json`),零新依赖,dispatch id 正常解析;离线替身同步改为
JSON 断言(`tests/test_voice_session.py`、`tests/test_voice_rework_hel879.py`)。
3. **自签证书 `CA:TRUE` 被 rustls 拒绝**:openssl 默认给自签证书加了
`basicConstraints=CA:TRUE`,livekit 客户端(rustls)报 `CaUsedAsEndEntity`。
重签为 `critical,CA:FALSE` + `SAN=IP:192.168.200.12` 后通过;测试客户端以
`SSL_CERT_FILE` 进程级信任该证书(非系统信任库、非浏览器例外冒充)。
修复后全量 `pytest tests -q` **352/352** 通过、ruff(src/tests/scripts)0 错;
本文件与修复随本轮提交入库。
## 3. 验证结果
真链:预录参考录音(**音频回放,非用户设备麦克风/手机**)→ 真实 WebRTC →
LiveKit 7880(客户端经 8363 WSS 桥)→ FunASR 流式识别 → 统一 `POST /api/chat`
(真 LLM)→ CosyVoice 合成(female_soft)→ WebRTC 回流收音(48k WAV 落盘)。
| 项 | 结果 | 证据 |
| --- | --- | --- |
| 连续 ≥3 轮(同档案、同对话源、音色一致) | ✅ 通话2:haimianbaobao/nezha/rain 三轮全部 `turn_end`;跨通话记忆延续可见(通话2 回复接住通话1 话题) | `call1b.subtitles.jsonl`、`chat_message` 全表 |
| 完整播放 receipt | ✅ 总管收尾独立复核:最终库及一致性快照均为 **8 条** `sharing_attempt`,全部 `ack_at` 落值、state=judged;取消用户轮 msg7/msg23 对应回执均为0。原交付4条是较早阶段数字,不能代表最终库 | 只读SQL与快照恢复核对;`/api/chat/received 200` |
| 开口打断 | ✅(发生真实性)通话1 三次真实 `turn_cancel(interrupted)`,用户开口截断在播回复、旧轮 user 行标 cancelled;**精确 ~1s 停声计时未取得**(量测设计缺陷+后两轮回复过短无以复测,见 §5) | `call1.subtitles.jsonl` seq68/87/92 |
| 软件客户端挂断清理 | ✅ DELETE 200 → 房间真实删除(ListRooms 空)、删除后仅 2 帧在途(约20ms)、软件客户端 track 停发;**不代表用户设备麦克风指示灯或扬声器残音已验** | `call1b.json hangup` |
| 断线(不挂断)+ TTL 到期后台删房 | ✅ 掉线不删 → 会话过期自动转债删除;15:52/15:56/15:57 三笔全部结清,账本空、房间空 | 轮询记录(窗口观察) |
| API 重启清理 | ✅ 重启 epoch 4→5、档案/历史完整、voice 配置在;重启遗留会话按 TTL 由补偿清理全部结清 | 同上 |
| 文字/语音互斥 | ✅(机制)文字轮被后到语音轮取消(msg23=cancelled)——同一把 chat 锁双向互斥成立;**「文字打断语音」方向未单独取证**(触发时机竞态,见 §5) | `call3.json`、消息表 |
| HTTPS/WSS | ✅ 8361/8363 同证书 TLS1.3、SAN=IP 可达;WSS 桥真实承载信令与媒体建立(客户端全程经 8363 接入) | 窗口日志、preflight |
| 嵌入 0 / 语义 stub | ✅ `semantic_backend=stub`,全程零嵌入调用 | `/api/health` |
| 字幕 v2 全生命周期 | ✅ seq 单调(三次通话均验证)、interim→final→turn_end / 打断→turn_cancel 无 final | 各 `subtitles.jsonl` |
## 4. 用量账(逐笔可查,硬闸全程在位)
- **LLM**:14/40 次请求、42,717/500,000 tokens(`budget_total`/`request_usage`/
`token_usage` 逐行;含失败/取消轮,预占语义如代码注释)。结束即封账:
`seal_budget_hel879.py` 已执行,封账标记随库归档,真实用量保留不清零。
- **TTS**:12/200 合成段(`tts.log` POST /tts 计数;本地 GPU、零外部计费)。
- **ASR**:仅窗口内 3 次通话使用(本地 CPU FunASR,零外部计费)。
- **嵌入/新增付费**:0。
- **语音会话**:建房 6 次 = **3 次真实通话**(含全部验证轮次)+ 3 次工具端
崩溃遗留的空会话(1 次手工 DELETE、2 次由 TTL 补偿自动删除;空会话无对话、
零模型消耗,如实计数不隐瞒)。
## 5. 未测 / 留待下轮(如实)
- **精确 1 秒级打断停声计时**:本轮三次真实打断均已发生并留痕,但帧级停声
时间戳未随证据落盘(下一轮客户端应在每帧记录到达时间并按 speech 归属切分)。
`call1.json` 已有发布音频到取消3.26秒、到最后agent帧5.3秒、cancel后204帧的
原始粗计时,不能隐去这些慢值。该计时未区分触发录音的语音起点、旧speech与
后续speech,不能据此量出旧声准确停止时间,**更不能判定达到约1秒**;下轮须
定向测量后再作结论。
- **「文字打断语音」方向**:本轮竞态取证到反向(语音取消文字);机制同一把锁,
下轮把文字触发改为「assistant_interim 出现后」即可定向取证。
- **真实设备浏览器路径**(D3 用户动作):证书「高级→继续」两处(8361/8363)、
真机麦克风(iOS Safari/安卓 Chrome)未测——须用户设备动作,本轮交付地址与
最小步骤,不以 rustls/SSL_CERT_FILE 冒充浏览器例外。
- 回复质量观察:长上下文后出现单字回复与一条英文串扰(与测试档案 boundary
文案提到「测试/临时」相关)——记录为内容质量观察项,非链路故障。
## 6. 收尾与回滚
- 窗口结束:worker/桥/双入口进程全部停止,8360/8361/8363/8082/9880 复核无监听;
TTS 按窗口前状态停回(精确 PID 终止,未用 fuser -k,核过无他人占用)。
- 受保护服务零接触:8300/8310/8320/8350/7880/7881/10095 原样运行,8340 保持
停机,ComfyUI 保持暂停(GPU 回到 97MiB 占用)。
- 数据封存:预算封账 + `release_snapshot_hel896.py` 停服一致性快照
(`/opt/cybergril-hel879/archives/hel879-r5-window-close-*`,rows_match 全过);
证据包 `/opt/cybergril-hel879/hel879-r5-evidence-20261027.tar.gz`
(sha256 `b23af43b…c394`,含三次通话字幕/JSON/音频/worker/TTS 日志/清理账)。
- 回滚:全部为本轮新建独立进程/目录,删除 `/opt/cybergril-hel879`、
`/opt/voice-agent-cybergril`、`/opt/cybergril-voice-certs` 即完全退场;Git 侧
为本分支正常追加提交,PR#11 与既有实例未动。
## 7. 总管独立收尾复核补充
只读核对14次请求/42717 tokens及sealed=1,封账值与真实用量相等;清理账sessions=0、
debts=0,8360/8361/8363/8082/9880无监听。没有重新打开原账或恢复本轮服务。
证据包完整SHA256为`b23af43b39ed77263c7b3f285796e77e8b1278acbf547b4619adad600ed5c394`。
一致性快照已由总管在本地隔离目录按原工具实际恢复,SHA256与三方行数相等,
恢复库同为sealed=1、14/42717用量和8条已确认回执。归档永不因上述“回滚”说明
自动删除;保留测试资料与证据,销毁须另有明确授权。
原MANIFEST的code_commit记录8dc84f8(运行时修复尚未提交),不是最终修复提交。
独立复核部署中的livekit_wire.py SHA256为
`ffc477cd96d48c510905ce9fb887ba5b00edd6941ee59cf1b960f51f18500941`,
与3766531收录源码逐字节一致;本说明补足溯源,不篡改原归档。
本轮只完成R5部分验证,不能称全部R5验收通过。实际6次建房必须连同3个空会话如实
保留,不把“3次实际通话”写成“只建房3次”。窗口已收尾且3次实际通话用完,未用
模型/token/TTS额度不自动授权新窗口;后续将精确打断计时、文字打断语音和用户设备
浏览器路径作为必补项,与自定义音色交回后的统一验证安排集中处理,不擅自重开账。
+96
View File
@@ -0,0 +1,96 @@
{
"rounds": [
{
"play_label": "xiaoyun",
"reply": {
"kind": "turn_cancel",
"speech_id": "speech_b687b8055307",
"reason": "interrupted"
}
},
{
"play_label": "bailu",
"reply": {
"kind": "turn_cancel",
"speech_id": "speech_8f12c4a50957",
"reason": "no-receipt"
}
},
{
"play_label": "jiaojiao",
"reply": {
"kind": "turn_cancel",
"speech_id": "speech_4c73b7d1829c",
"reason": "interrupted"
}
}
],
"interrupt": {
"outcome": "interrupted",
"trigger": "audio",
"publish_ms": 1793115246711,
"cancel_ms": 1793115249971,
"cancel_latency_s": 3.26,
"last_agent_frame_ms": 1793115252008,
"audio_stop_latency_s": 5.3,
"frames_after_cancel": 204
},
"hangup": {
"delete_status": 200,
"delete_body": {
"ended": true,
"room": "cg-voice-qeghdnxehn"
},
"delete_ms": 1793115252011,
"agent_frames_after_delete": 2,
"room_removed": true
},
"call": {
"tag": "call1",
"session": {
"url": "wss://192.168.200.12:8363",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6ImNnLXZvaWNlLXFlZ2hkbnhlaG4iLCJjYW5QdWJsaXNoIjp0cnVlLCJjYW5TdWJzY3JpYmUiOnRydWUsImNhblB1Ymxpc2hEYXRhIjp0cnVlfSwic3ViIjoidXNlci1nd2h2ZXpjeiIsImlzcyI6ImRldmtleSIsIm5iZiI6MTc5MzExNTIwMywiZXhwIjoxNzkzMTE1NTAzLCJuYW1lIjoi55So5oi3In0.D91S6SXWBtoVoSC6TEsULuK8WNHQu36O_vZQS1urwbw",
"room": "cg-voice-qeghdnxehn",
"session_id": "Ey8pBEFHbrO8i6Lbc0-47mh7",
"agent_name": "cybergril-voice-agent",
"epoch": 3,
"expires_at": 1793116103636
},
"subtitle_count": 96,
"seq_monotonic": true,
"agent_frames": 4766,
"agent_audio_seconds": 47.7,
"agent_audio_rate": 48000,
"agent_joined_ms": 1793115204287,
"agent_left_ms": null,
"subtitles_file": "/opt/cybergril-hel879/evidence/call1.subtitles.jsonl",
"agent_audio_file": "/opt/cybergril-hel879/evidence/call1.agent.wav"
},
"budget_after": {
"at_ms": 1793115254555,
"token_usage": [
{
"day": "2026-10-27",
"scene": "dialogue_main",
"used": 11416
}
],
"request_usage": [
{
"day": "2026-10-27",
"category": "dialogue",
"requests": 4
}
],
"budget_total": [
{
"key": "total_requests",
"value": 4
},
{
"key": "total_tokens",
"value": 11416
}
]
}
}
@@ -0,0 +1,96 @@
{"v": 2, "seq": 1, "kind": "interim", "role": "user", "text": "盯", "_at_ms": 1793115205011}
{"v": 2, "seq": 2, "kind": "interim", "role": "user", "text": "着你看怎", "_at_ms": 1793115205596}
{"v": 2, "seq": 3, "kind": "interim", "role": "user", "text": "么啦", "_at_ms": 1793115206196}
{"v": 2, "seq": 4, "kind": "user_final", "role": "user", "text": "盯着你看怎么啦", "_at_ms": 1793115207385}
{"v": 2, "seq": 5, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_b687b8055307", "_at_ms": 1793115207410}
{"v": 2, "seq": 6, "kind": "interim", "role": "user", "text": "难得", "_at_ms": 1793115207554}
{"v": 2, "seq": 7, "kind": "interim", "role": "user", "text": "看你这么", "_at_ms": 1793115208153}
{"v": 2, "seq": 8, "kind": "interim", "role": "user", "text": "有意思的", "_at_ms": 1793115208743}
{"v": 2, "seq": 9, "kind": "interim", "role": "user", "text": "样子", "_at_ms": 1793115209349}
{"v": 2, "seq": 10, "kind": "assistant_interim", "role": "assistant", "text": "没", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209395}
{"v": 2, "seq": 11, "kind": "assistant_interim", "role": "assistant", "text": "没怎么", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209436}
{"v": 2, "seq": 12, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209439}
{"v": 2, "seq": 13, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209478}
{"v": 2, "seq": 14, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209479}
{"v": 2, "seq": 15, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209522}
{"v": 2, "seq": 16, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209563}
{"v": 2, "seq": 17, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209565}
{"v": 2, "seq": 18, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209605}
{"v": 2, "seq": 19, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209611}
{"v": 2, "seq": 20, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209650}
{"v": 2, "seq": 21, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209688}
{"v": 2, "seq": 22, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209731}
{"v": 2, "seq": 23, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209773}
{"v": 2, "seq": 24, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查我", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209816}
{"v": 2, "seq": 25, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查我有没有", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209816}
{"v": 2, "seq": 26, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查我有没有乱", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209857}
{"v": 2, "seq": 27, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查我有没有乱动", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209900}
{"v": 2, "seq": 28, "kind": "assistant_interim", "role": "assistant", "text": "没怎么啦,工程师。你盯着看,我就当你在检查我有没有乱动。", "speech_id": "speech_b687b8055307", "_at_ms": 1793115209947}
{"v": 2, "seq": 29, "kind": "user_final", "role": "user", "text": "难得看你这么有意思的样子吗", "_at_ms": 1793115210503}
{"v": 2, "seq": 30, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_b687b8055307", "reason": "interrupted", "_at_ms": 1793115210511}
{"v": 2, "seq": 31, "kind": "interim", "role": "user", "text": "烈会军校", "_at_ms": 1793115212136}
{"v": 2, "seq": 32, "kind": "interim", "role": "user", "text": "校啊招", "_at_ms": 1793115212733}
{"v": 2, "seq": 33, "kind": "interim", "role": "user", "text": "啊这种类", "_at_ms": 1793115213338}
{"v": 2, "seq": 34, "kind": "interim", "role": "user", "text": "型我都是", "_at_ms": 1793115213936}
{"v": 2, "seq": 35, "kind": "interim", "role": "user", "text": "那种比较", "_at_ms": 1793115214541}
{"v": 2, "seq": 36, "kind": "interim", "role": "user", "text": "呃豪", "_at_ms": 1793115215744}
{"v": 2, "seq": 37, "kind": "interim", "role": "user", "text": "爽比", "_at_ms": 1793115216340}
{"v": 2, "seq": 38, "kind": "interim", "role": "user", "text": "较开", "_at_ms": 1793115216943}
{"v": 2, "seq": 39, "kind": "interim", "role": "user", "text": "朗的那种", "_at_ms": 1793115217541}
{"v": 2, "seq": 40, "kind": "interim", "role": "user", "text": "类型", "_at_ms": 1793115218142}
{"v": 2, "seq": 41, "kind": "interim", "role": "user", "text": "阳光大", "_at_ms": 1793115218753}
{"v": 2, "seq": 42, "kind": "interim", "role": "user", "text": "女孩", "_at_ms": 1793115219344}
{"v": 2, "seq": 43, "kind": "interim", "role": "user", "text": "那种感", "_at_ms": 1793115220543}
{"v": 2, "seq": 44, "kind": "interim", "role": "user", "text": "觉", "_at_ms": 1793115221139}
{"v": 2, "seq": 45, "kind": "interim", "role": "user", "text": "都给", "_at_ms": 1793115221743}
{"v": 2, "seq": 46, "kind": "user_final", "role": "user", "text": "烈火军校啊,朝阳啊这种类型我都是那种比较呃豪爽,比较开朗的那种类型,阳光大女孩那种感觉就", "_at_ms": 1793115223193}
{"v": 2, "seq": 47, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115226967}
{"v": 2, "seq": 48, "kind": "assistant_interim", "role": "assistant", "text": "原来", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228249}
{"v": 2, "seq": 49, "kind": "assistant_interim", "role": "assistant", "text": "原来你是", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228291}
{"v": 2, "seq": 50, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228333}
{"v": 2, "seq": 51, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228335}
{"v": 2, "seq": 52, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228376}
{"v": 2, "seq": 53, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228377}
{"v": 2, "seq": 54, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228420}
{"v": 2, "seq": 55, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228464}
{"v": 2, "seq": 56, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228506}
{"v": 2, "seq": 57, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228508}
{"v": 2, "seq": 58, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228548}
{"v": 2, "seq": 59, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228590}
{"v": 2, "seq": 60, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228634}
{"v": 2, "seq": 61, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228636}
{"v": 2, "seq": 62, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228674}
{"v": 2, "seq": 63, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228716}
{"v": 2, "seq": 64, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股豪", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228717}
{"v": 2, "seq": 65, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股豪爽", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228758}
{"v": 2, "seq": 66, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股豪爽劲儿", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228760}
{"v": 2, "seq": 67, "kind": "assistant_interim", "role": "assistant", "text": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股豪爽劲儿。", "speech_id": "speech_8f12c4a50957", "_at_ms": 1793115228801}
{"v": 2, "seq": 68, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_8f12c4a50957", "reason": "no-receipt", "_at_ms": 1793115239147}
{"v": 2, "seq": 69, "kind": "interim", "role": "user", "text": "怎么还", "_at_ms": 1793115240503}
{"v": 2, "seq": 70, "kind": "interim", "role": "user", "text": "有没事", "_at_ms": 1793115241090}
{"v": 2, "seq": 71, "kind": "interim", "role": "user", "text": "的", "_at_ms": 1793115241700}
{"v": 2, "seq": 72, "kind": "user_final", "role": "user", "text": "怎么还有没事的", "_at_ms": 1793115242576}
{"v": 2, "seq": 73, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115242585}
{"v": 2, "seq": 74, "kind": "interim", "role": "user", "text": "是我不够", "_at_ms": 1793115243790}
{"v": 2, "seq": 75, "kind": "assistant_interim", "role": "assistant", "text": "哈哈", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243833}
{"v": 2, "seq": 76, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243874}
{"v": 2, "seq": 77, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243875}
{"v": 2, "seq": 78, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243916}
{"v": 2, "seq": 79, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243917}
{"v": 2, "seq": 80, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真听", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115243958}
{"v": 2, "seq": 81, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真听你", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115244002}
{"v": 2, "seq": 82, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真听你说话", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115244044}
{"v": 2, "seq": 83, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真听你说话呀", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115244045}
{"v": 2, "seq": 84, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,因为我在认真听你说话呀。", "speech_id": "speech_4c73b7d1829c", "_at_ms": 1793115244086}
{"v": 2, "seq": 85, "kind": "interim", "role": "user", "text": "努力", "_at_ms": 1793115244386}
{"v": 2, "seq": 86, "kind": "user_final", "role": "user", "text": "是我不够努力吗", "_at_ms": 1793115245511}
{"v": 2, "seq": 87, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_4c73b7d1829c", "reason": "interrupted", "_at_ms": 1793115245516}
{"v": 2, "seq": 88, "kind": "interim", "role": "user", "text": "盯着你看", "_at_ms": 1793115248139}
{"v": 2, "seq": 89, "kind": "interim", "role": "user", "text": "怎么", "_at_ms": 1793115248742}
{"v": 2, "seq": 90, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_7a9c0f804ca0", "_at_ms": 1793115249065}
{"v": 2, "seq": 91, "kind": "user_final", "role": "user", "text": "盯着你看怎么啦", "_at_ms": 1793115249963}
{"v": 2, "seq": 92, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_7a9c0f804ca0", "reason": "interrupted", "_at_ms": 1793115249971}
{"v": 2, "seq": 93, "kind": "interim", "role": "user", "text": "难得", "_at_ms": 1793115250198}
{"v": 2, "seq": 94, "kind": "interim", "role": "user", "text": "看你这么", "_at_ms": 1793115250808}
{"v": 2, "seq": 95, "kind": "interim", "role": "user", "text": "有意思的", "_at_ms": 1793115251389}
{"v": 2, "seq": 96, "kind": "interim", "role": "user", "text": "样子", "_at_ms": 1793115251989}
+97
View File
@@ -0,0 +1,97 @@
{
"rounds": [
{
"play_label": "haimianbaobao",
"reply": {
"kind": "turn_end",
"speech_id": "speech_e69e351f1c44",
"reason": null
}
},
{
"play_label": "nezha",
"reply": {
"kind": "turn_end",
"speech_id": "speech_ff61928cdce0",
"reason": null
}
},
{
"play_label": "rain",
"reply": {
"kind": "turn_end",
"speech_id": "speech_2f9dc8b9848d",
"reason": null
}
}
],
"interrupt": {
"outcome": "no-cancel",
"trigger": "audio",
"publish_ms": 1793115490292,
"cancel_ms": null,
"cancel_latency_s": null,
"old_audio_stop_ms": 1793115512315,
"old_audio_stop_latency_s": 22.02,
"old_frames_during_interrupt": 2203,
"frames_after_cancel_plus1s5": 0
},
"hangup": {
"delete_status": 200,
"delete_body": {
"ended": true,
"room": "cg-voice-5c4y64wxkl"
},
"delete_ms": 1793115512327,
"agent_frames_after_delete": 2,
"room_removed": true
},
"call": {
"tag": "call1",
"session": {
"url": "wss://192.168.200.12:8363",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6ImNnLXZvaWNlLTVjNHk2NHd4a2wiLCJjYW5QdWJsaXNoIjp0cnVlLCJjYW5TdWJzY3JpYmUiOnRydWUsImNhblB1Ymxpc2hEYXRhIjp0cnVlfSwic3ViIjoidXNlci12ZHl3c2h2ZSIsImlzcyI6ImRldmtleSIsIm5iZiI6MTc5MzExNTQzMSwiZXhwIjoxNzkzMTE1NzMxLCJuYW1lIjoi55So5oi3In0.PjOElCid-0VLPw8w0iQml4ca9QkLqoCwu9EJf2pz4DE",
"room": "cg-voice-5c4y64wxkl",
"session_id": "jCiBIdbaj2_2_AjxrwChbQ4A",
"agent_name": "cybergril-voice-agent",
"epoch": 4,
"expires_at": 1793116331245
},
"subtitle_count": 98,
"seq_monotonic": true,
"agent_frames": 8042,
"agent_audio_seconds": 80.4,
"agent_audio_rate": 48000,
"agent_joined_ms": 1793115431859,
"agent_left_ms": null,
"subtitles_file": "/opt/cybergril-hel879/evidence/call1b.subtitles.jsonl",
"agent_audio_file": "/opt/cybergril-hel879/evidence/call1b.agent.wav"
},
"budget_after": {
"at_ms": 1793115514868,
"token_usage": [
{
"day": "2026-10-27",
"scene": "dialogue_main",
"used": 23504
}
],
"request_usage": [
{
"day": "2026-10-27",
"category": "dialogue",
"requests": 8
}
],
"budget_total": [
{
"key": "total_requests",
"value": 8
},
{
"key": "total_tokens",
"value": 23504
}
]
}
}
@@ -0,0 +1,98 @@
{"v": 2, "seq": 1, "kind": "interim", "role": "user", "text": "没", "_at_ms": 1793115432559}
{"v": 2, "seq": 2, "kind": "interim", "role": "user", "text": "错", "_at_ms": 1793115433170}
{"v": 2, "seq": 3, "kind": "interim", "role": "user", "text": "我就是个", "_at_ms": 1793115433760}
{"v": 2, "seq": 4, "kind": "interim", "role": "user", "text": "笨蛋", "_at_ms": 1793115434360}
{"v": 2, "seq": 5, "kind": "interim", "role": "user", "text": "派大星", "_at_ms": 1793115434960}
{"v": 2, "seq": 6, "kind": "interim", "role": "user", "text": "我们", "_at_ms": 1793115435559}
{"v": 2, "seq": 7, "kind": "interim", "role": "user", "text": "一起去", "_at_ms": 1793115436165}
{"v": 2, "seq": 8, "kind": "interim", "role": "user", "text": "抓水母", "_at_ms": 1793115436768}
{"v": 2, "seq": 9, "kind": "interim", "role": "user", "text": "吧我", "_at_ms": 1793115437365}
{"v": 2, "seq": 10, "kind": "interim", "role": "user", "text": "们一起度", "_at_ms": 1793115437963}
{"v": 2, "seq": 11, "kind": "interim", "role": "user", "text": "过快乐的", "_at_ms": 1793115438565}
{"v": 2, "seq": 12, "kind": "interim", "role": "user", "text": "一天", "_at_ms": 1793115439163}
{"v": 2, "seq": 13, "kind": "user_final", "role": "user", "text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天", "_at_ms": 1793115440793}
{"v": 2, "seq": 14, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115440831}
{"v": 2, "seq": 15, "kind": "assistant_interim", "role": "assistant", "text": "好", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115442938}
{"v": 2, "seq": 16, "kind": "assistant_interim", "role": "assistant", "text": "好呀", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115442940}
{"v": 2, "seq": 17, "kind": "assistant_interim", "role": "assistant", "text": "好呀,", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115442981}
{"v": 2, "seq": 18, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115442984}
{"v": 2, "seq": 19, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443024}
{"v": 2, "seq": 20, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443067}
{"v": 2, "seq": 21, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443068}
{"v": 2, "seq": 22, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443109}
{"v": 2, "seq": 23, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443110}
{"v": 2, "seq": 24, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443152}
{"v": 2, "seq": 25, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443154}
{"v": 2, "seq": 26, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443195}
{"v": 2, "seq": 27, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443237}
{"v": 2, "seq": 28, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443238}
{"v": 2, "seq": 29, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443279}
{"v": 2, "seq": 30, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443321}
{"v": 2, "seq": 31, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443322}
{"v": 2, "seq": 32, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443363}
{"v": 2, "seq": 33, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443406}
{"v": 2, "seq": 34, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443449}
{"v": 2, "seq": 35, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443450}
{"v": 2, "seq": 36, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443491}
{"v": 2, "seq": 37, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443534}
{"v": 2, "seq": 38, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海边", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443577}
{"v": 2, "seq": 39, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海边晒太阳", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443578}
{"v": 2, "seq": 40, "kind": "assistant_interim", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海边晒太阳。", "speech_id": "speech_e69e351f1c44", "_at_ms": 1793115443620}
{"v": 2, "seq": 41, "kind": "assistant_final", "role": "assistant", "text": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海边晒太阳。", "speech_id": "speech_e69e351f1c44", "turn_id": "b72de17e-bf42-470e-b396-c219bd7593f9", "_at_ms": 1793115457080}
{"v": 2, "seq": 42, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_e69e351f1c44", "turn_id": "b72de17e-bf42-470e-b396-c219bd7593f9", "_at_ms": 1793115457081}
{"v": 2, "seq": 43, "kind": "interim", "role": "user", "text": "天雷滚", "_at_ms": 1793115458260}
{"v": 2, "seq": 44, "kind": "interim", "role": "user", "text": "滚我好", "_at_ms": 1793115458865}
{"v": 2, "seq": 45, "kind": "interim", "role": "user", "text": "怕怕", "_at_ms": 1793115459463}
{"v": 2, "seq": 46, "kind": "interim", "role": "user", "text": "劈", "_at_ms": 1793115460064}
{"v": 2, "seq": 47, "kind": "interim", "role": "user", "text": "的我浑", "_at_ms": 1793115460657}
{"v": 2, "seq": 48, "kind": "interim", "role": "user", "text": "身掉渣", "_at_ms": 1793115461256}
{"v": 2, "seq": 49, "kind": "interim", "role": "user", "text": "渣突", "_at_ms": 1793115461862}
{"v": 2, "seq": 50, "kind": "interim", "role": "user", "text": "破天", "_at_ms": 1793115462458}
{"v": 2, "seq": 51, "kind": "interim", "role": "user", "text": "劫我笑", "_at_ms": 1793115463067}
{"v": 2, "seq": 52, "kind": "interim", "role": "user", "text": "哈哈", "_at_ms": 1793115463659}
{"v": 2, "seq": 53, "kind": "interim", "role": "user", "text": "逆", "_at_ms": 1793115464263}
{"v": 2, "seq": 54, "kind": "interim", "role": "user", "text": "天改命", "_at_ms": 1793115464867}
{"v": 2, "seq": 55, "kind": "interim", "role": "user", "text": "我吹喇", "_at_ms": 1793115465459}
{"v": 2, "seq": 56, "kind": "interim", "role": "user", "text": "叭滴", "_at_ms": 1793115466062}
{"v": 2, "seq": 57, "kind": "interim", "role": "user", "text": "答滴答", "_at_ms": 1793115466663}
{"v": 2, "seq": 58, "kind": "interim", "role": "user", "text": "滴滴", "_at_ms": 1793115467270}
{"v": 2, "seq": 59, "kind": "user_final", "role": "user", "text": "天雷滚滚,我好怕怕劈的我浑身掉渣渣,突破天劫,我笑哈哈逆天改命,我吹喇叭滴答滴答,滴滴答", "_at_ms": 1793115468935}
{"v": 2, "seq": 60, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115468946}
{"v": 2, "seq": 61, "kind": "assistant_interim", "role": "assistant", "text": "哈哈", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470187}
{"v": 2, "seq": 62, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470230}
{"v": 2, "seq": 63, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470231}
{"v": 2, "seq": 64, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470273}
{"v": 2, "seq": 65, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470315}
{"v": 2, "seq": 66, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得还挺", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470316}
{"v": 2, "seq": 67, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得还挺有", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470357}
{"v": 2, "seq": 68, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得还挺有节奏", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470359}
{"v": 2, "seq": 69, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得还挺有节奏感的", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470400}
{"v": 2, "seq": 70, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你唱得还挺有节奏感的!", "speech_id": "speech_ff61928cdce0", "_at_ms": 1793115470443}
{"v": 2, "seq": 71, "kind": "assistant_final", "role": "assistant", "text": "哈哈,你唱得还挺有节奏感的!", "speech_id": "speech_ff61928cdce0", "turn_id": "baa6c5c7-b483-49d4-905c-d6b7e5dd7786", "_at_ms": 1793115476107}
{"v": 2, "seq": 72, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_ff61928cdce0", "turn_id": "baa6c5c7-b483-49d4-905c-d6b7e5dd7786", "_at_ms": 1793115476109}
{"v": 2, "seq": 73, "kind": "interim", "role": "user", "text": "你是不是", "_at_ms": 1793115477463}
{"v": 2, "seq": 74, "kind": "interim", "role": "user", "text": "早就想", "_at_ms": 1793115478068}
{"v": 2, "seq": 75, "kind": "interim", "role": "user", "text": "睡我", "_at_ms": 1793115478661}
{"v": 2, "seq": 76, "kind": "interim", "role": "user", "text": "了", "_at_ms": 1793115479267}
{"v": 2, "seq": 77, "kind": "user_final", "role": "user", "text": "你是不是早就想睡我了呀", "_at_ms": 1793115480206}
{"v": 2, "seq": 78, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115480215}
{"v": 2, "seq": 79, "kind": "assistant_interim", "role": "assistant", "text": "哈哈", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481774}
{"v": 2, "seq": 80, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481776}
{"v": 2, "seq": 81, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481816}
{"v": 2, "seq": 82, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481816}
{"v": 2, "seq": 83, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481857}
{"v": 2, "seq": 84, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481859}
{"v": 2, "seq": 85, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太快了", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481903}
{"v": 2, "seq": 86, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太快了,", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481906}
{"v": 2, "seq": 87, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太快了,我都", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481945}
{"v": 2, "seq": 88, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太快了,我都跟不上", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481987}
{"v": 2, "seq": 89, "kind": "assistant_interim", "role": "assistant", "text": "哈哈,你思维跳跃得太快了,我都跟不上。", "speech_id": "speech_2f9dc8b9848d", "_at_ms": 1793115481988}
{"v": 2, "seq": 90, "kind": "assistant_final", "role": "assistant", "text": "哈哈,你思维跳跃得太快了,我都跟不上。", "speech_id": "speech_2f9dc8b9848d", "turn_id": "a91bbbb6-45b0-4671-84a2-027cc194f420", "_at_ms": 1793115489067}
{"v": 2, "seq": 91, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_2f9dc8b9848d", "turn_id": "a91bbbb6-45b0-4671-84a2-027cc194f420", "_at_ms": 1793115489068}
{"v": 2, "seq": 92, "kind": "interim", "role": "user", "text": "盯着你看", "_at_ms": 1793115491717}
{"v": 2, "seq": 93, "kind": "interim", "role": "user", "text": "怎么啦", "_at_ms": 1793115492309}
{"v": 2, "seq": 94, "kind": "user_final", "role": "user", "text": "盯着你看怎么啦", "_at_ms": 1793115493388}
{"v": 2, "seq": 95, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_0d075d37bc31", "_at_ms": 1793115493398}
{"v": 2, "seq": 96, "kind": "assistant_interim", "role": "assistant", "text": "没", "speech_id": "speech_0d075d37bc31", "_at_ms": 1793115494608}
{"v": 2, "seq": 97, "kind": "assistant_final", "role": "assistant", "text": "没", "speech_id": "speech_0d075d37bc31", "turn_id": "5bd95c66-b42b-4571-bfc2-fbf376c90747", "_at_ms": 1793115501161}
{"v": 2, "seq": 98, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_0d075d37bc31", "turn_id": "5bd95c66-b42b-4571-bfc2-fbf376c90747", "_at_ms": 1793115501163}
+303
View File
@@ -0,0 +1,303 @@
{
"barges": [
{
"label": "bajie",
"outcome": "reply-too-short-to-barge"
},
{
"label": "sunwukong",
"outcome": "reply-too-short-to-barge"
},
{
"label": "haimianbaobao",
"outcome": "reply-too-short-to-barge"
}
],
"text_turn": {
"turn_id": "cb1bbe27-795f-45e3-b7bf-f2d6371c0830",
"status": 200,
"deltas": 0,
"final": null,
"ms": 114
},
"voice_reply_after_text": {
"kind": "turn_end",
"speech_id": "speech_9aa32dc387c3",
"reason": null
},
"voice_cancelled_by_text": false,
"drop": {
"dropped_ms": 1793116007019,
"room": "cg-voice-oleganioz7",
"session_id": "Si6UZJbAENsNExR-B1cfpIEX",
"agent_frames_after_drop": 1
},
"call": {
"tag": "call3",
"session": {
"url": "wss://192.168.200.12:8363",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2aWRlbyI6eyJyb29tSm9pbiI6dHJ1ZSwicm9vbSI6ImNnLXZvaWNlLW9sZWdhbmlvejciLCJjYW5QdWJsaXNoIjp0cnVlLCJjYW5TdWJzY3JpYmUiOnRydWUsImNhblB1Ymxpc2hEYXRhIjp0cnVlfSwic3ViIjoidXNlci11cWZ5dWhudCIsImlzcyI6ImRldmtleSIsIm5iZiI6MTc5MzExNTY5MCwiZXhwIjoxNzkzMTE1OTkwLCJuYW1lIjoi55So5oi3In0.wXERZSpOZVIidVRq-8dkIbb7e2gPa0HUX8_ZV-kHTN4",
"room": "cg-voice-oleganioz7",
"session_id": "Si6UZJbAENsNExR-B1cfpIEX",
"agent_name": "cybergril-voice-agent",
"epoch": 4,
"expires_at": 1793116590248
},
"subtitle_count": 104,
"seq_monotonic": true,
"agent_frames": 31510,
"agent_audio_seconds": 315.1,
"agent_audio_rate": 48000,
"agent_joined_ms": 1793115690867,
"agent_left_ms": null,
"subtitles_file": "/opt/cybergril-hel879/evidence/call3.subtitles.jsonl",
"agent_audio_file": "/opt/cybergril-hel879/evidence/call3.agent.wav"
},
"messages": [
{
"id": 1,
"role": "user",
"t": "盯着你看怎么啦",
"status": "sent",
"turn_id": "61605d50-17f0-4d9e-b407-60574304a827"
},
{
"id": 2,
"role": "companion",
"t": "没怎么啦,工程师。你盯着看,我就当你在检查我有没有乱动。",
"status": "sent",
"turn_id": "61605d50-17f0-4d9e-b407-60574304a827"
},
{
"id": 3,
"role": "user",
"t": "难得看你这么有意思的样子吗 烈火军校啊,朝阳啊这种类型我都是那种比较呃豪爽,比较开朗的那种类型,阳光大女孩那种感觉就",
"status": "sent",
"turn_id": "eb182bcd-3441-42d8-ba60-9eced3a6a5a5"
},
{
"id": 4,
"role": "companion",
"t": "原来你是那种阳光大女孩的类型啊,难怪感觉你说话都透着一股豪爽劲儿。",
"status": "sent",
"turn_id": "eb182bcd-3441-42d8-ba60-9eced3a6a5a5"
},
{
"id": 5,
"role": "user",
"t": "怎么还有没事的",
"status": "sent",
"turn_id": "0e1a5226-4ca6-44bb-a343-39647f0112e3"
},
{
"id": 6,
"role": "companion",
"t": "哈哈,因为我在认真听你说话呀。",
"status": "sent",
"turn_id": "0e1a5226-4ca6-44bb-a343-39647f0112e3"
},
{
"id": 7,
"role": "user",
"t": "是我不够努力吗",
"status": "cancelled",
"turn_id": "47d8d0ce-7ac7-4202-9757-64f5f4fb1300"
},
{
"id": 8,
"role": "user",
"t": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天",
"status": "sent",
"turn_id": "b72de17e-bf42-470e-b396-c219bd7593f9"
},
{
"id": 9,
"role": "companion",
"t": "好呀,那我们这就出发去抓水母!不过我可不想住进那个菠萝屋,我只喜欢在海边晒太阳。",
"status": "sent",
"turn_id": "b72de17e-bf42-470e-b396-c219bd7593f9"
},
{
"id": 10,
"role": "user",
"t": "天雷滚滚,我好怕怕劈的我浑身掉渣渣,突破天劫,我笑哈哈逆天改命,我吹喇叭滴答滴答,滴滴答",
"status": "sent",
"turn_id": "baa6c5c7-b483-49d4-905c-d6b7e5dd7786"
},
{
"id": 11,
"role": "companion",
"t": "哈哈,你唱得还挺有节奏感的!",
"status": "sent",
"turn_id": "baa6c5c7-b483-49d4-905c-d6b7e5dd7786"
},
{
"id": 12,
"role": "user",
"t": "你是不是早就想睡我了呀",
"status": "sent",
"turn_id": "a91bbbb6-45b0-4671-84a2-027cc194f420"
},
{
"id": 13,
"role": "companion",
"t": "哈哈,你思维跳跃得太快了,我都跟不上。",
"status": "sent",
"turn_id": "a91bbbb6-45b0-4671-84a2-027cc194f420"
},
{
"id": 14,
"role": "user",
"t": "盯着你看怎么啦",
"status": "sent",
"turn_id": "5bd95c66-b42b-4571-bfc2-fbf376c90747"
},
{
"id": 15,
"role": "companion",
"t": "没",
"status": "sent",
"turn_id": "5bd95c66-b42b-4571-bfc2-fbf376c90747"
},
{
"id": 16,
"role": "user",
"t": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘",
"status": "sent",
"turn_id": "9bba6412-6679-48c2-9f1c-4bfc07f58aea"
},
{
"id": 17,
"role": "companion",
"t": "师傅,",
"status": "sent",
"turn_id": "9bba6412-6679-48c2-9f1c-4bfc07f58aea"
},
{
"id": 18,
"role": "user",
"t": "俺老孙来也,俺乃500年前大闹天宫的齐天大圣孙悟空,这里山势险峻,只怕藏有妖精妖怪,哪里逃吃?俺老孙吧",
"status": "sent",
"turn_id": "16006868-5ee7-49cc-b9c9-1a8dcfe74d08"
},
{
"id": 19,
"role": "companion",
"t": "好",
"status": "sent",
"turn_id": "16006868-5ee7-49cc-b9c9-1a8dcfe74d08"
},
{
"id": 20,
"role": "user",
"t": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天",
"status": "sent",
"turn_id": "fdf28a03-19a8-4fcb-ba8c-24587736c3f0"
},
{
"id": 21,
"role": "companion",
"t": "好",
"status": "sent",
"turn_id": "fdf28a03-19a8-4fcb-ba8c-24587736c3f0"
},
{
"id": 22,
"role": "user",
"t": "你是不是早就想睡我了呀",
"status": "sent",
"turn_id": "623bab59-0edc-4751-9feb-a9a7e4a11233"
},
{
"id": 23,
"role": "user",
"t": "(文字通道)我改用打字了,这条应该立刻打断你正在说的话。",
"status": "cancelled",
"turn_id": "cb1bbe27-795f-45e3-b7bf-f2d6371c0830"
},
{
"id": 25,
"role": "companion",
"t": "Considering the limited time by the user, I have to give the",
"status": "sent",
"turn_id": "623bab59-0edc-4751-9feb-a9a7e4a11233"
}
],
"receipts": [
{
"message_id": 9,
"turn_id": "b72de17e-bf42-470e-b396-c219bd7593f9",
"ack_at": 1793115457081,
"state": "judged"
},
{
"message_id": 11,
"turn_id": "baa6c5c7-b483-49d4-905c-d6b7e5dd7786",
"ack_at": 1793115476109,
"state": "judged"
},
{
"message_id": 13,
"turn_id": "a91bbbb6-45b0-4671-84a2-027cc194f420",
"ack_at": 1793115489069,
"state": "judged"
},
{
"message_id": 15,
"turn_id": "5bd95c66-b42b-4571-bfc2-fbf376c90747",
"ack_at": 1793115501163,
"state": "judged"
},
{
"message_id": 17,
"turn_id": "9bba6412-6679-48c2-9f1c-4bfc07f58aea",
"ack_at": 1793115715591,
"state": "judged"
},
{
"message_id": 19,
"turn_id": "16006868-5ee7-49cc-b9c9-1a8dcfe74d08",
"ack_at": 1793115816209,
"state": "judged"
},
{
"message_id": 21,
"turn_id": "fdf28a03-19a8-4fcb-ba8c-24587736c3f0",
"ack_at": 1793115908179,
"state": "judged"
},
{
"message_id": 25,
"turn_id": "623bab59-0edc-4751-9feb-a9a7e4a11233",
"ack_at": 1793116005932,
"state": "pending"
}
],
"budget": {
"at_ms": 1793116007051,
"token_usage": [
{
"day": "2026-10-27",
"scene": "dialogue_main",
"used": 42717
}
],
"request_usage": [
{
"day": "2026-10-27",
"category": "dialogue",
"requests": 14
}
],
"budget_total": [
{
"key": "total_requests",
"value": 14
},
{
"key": "total_tokens",
"value": 42717
}
]
}
}
@@ -0,0 +1,104 @@
{"v": 2, "seq": 1, "kind": "interim", "role": "user", "text": "师", "_at_ms": 1793115691554}
{"v": 2, "seq": 2, "kind": "interim", "role": "user", "text": "傅", "_at_ms": 1793115692163}
{"v": 2, "seq": 3, "kind": "interim", "role": "user", "text": "哎这", "_at_ms": 1793115692756}
{"v": 2, "seq": 4, "kind": "interim", "role": "user", "text": "取经", "_at_ms": 1793115693359}
{"v": 2, "seq": 5, "kind": "interim", "role": "user", "text": "路比", "_at_ms": 1793115693964}
{"v": 2, "seq": 6, "kind": "interim", "role": "user", "text": "高老", "_at_ms": 1793115694564}
{"v": 2, "seq": 7, "kind": "interim", "role": "user", "text": "庄种", "_at_ms": 1793115695157}
{"v": 2, "seq": 8, "kind": "interim", "role": "user", "text": "地还", "_at_ms": 1793115695762}
{"v": 2, "seq": 9, "kind": "interim", "role": "user", "text": "苦俺", "_at_ms": 1793115696359}
{"v": 2, "seq": 10, "kind": "interim", "role": "user", "text": "老中", "_at_ms": 1793115696958}
{"v": 2, "seq": 11, "kind": "interim", "role": "user", "text": "饿的能", "_at_ms": 1793115697557}
{"v": 2, "seq": 12, "kind": "interim", "role": "user", "text": "吞下", "_at_ms": 1793115698168}
{"v": 2, "seq": 13, "kind": "interim", "role": "user", "text": "石龙", "_at_ms": 1793115698760}
{"v": 2, "seq": 14, "kind": "interim", "role": "user", "text": "素包", "_at_ms": 1793115699353}
{"v": 2, "seq": 15, "kind": "interim", "role": "user", "text": "子", "_at_ms": 1793115699967}
{"v": 2, "seq": 16, "kind": "interim", "role": "user", "text": "猴哥", "_at_ms": 1793115700562}
{"v": 2, "seq": 17, "kind": "interim", "role": "user", "text": "你筋", "_at_ms": 1793115701159}
{"v": 2, "seq": 18, "kind": "interim", "role": "user", "text": "斗云", "_at_ms": 1793115701768}
{"v": 2, "seq": 19, "kind": "interim", "role": "user", "text": "快", "_at_ms": 1793115702352}
{"v": 2, "seq": 20, "kind": "interim", "role": "user", "text": "要不", "_at_ms": 1793115702961}
{"v": 2, "seq": 21, "kind": "interim", "role": "user", "text": "先", "_at_ms": 1793115703557}
{"v": 2, "seq": 22, "kind": "interim", "role": "user", "text": "去画个", "_at_ms": 1793115704166}
{"v": 2, "seq": 23, "kind": "user_final", "role": "user", "text": "师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘", "_at_ms": 1793115706500}
{"v": 2, "seq": 24, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_ba7b3f730ffd", "_at_ms": 1793115710205}
{"v": 2, "seq": 25, "kind": "assistant_interim", "role": "assistant", "text": "师傅", "speech_id": "speech_ba7b3f730ffd", "_at_ms": 1793115711789}
{"v": 2, "seq": 26, "kind": "assistant_interim", "role": "assistant", "text": "师傅,", "speech_id": "speech_ba7b3f730ffd", "_at_ms": 1793115711850}
{"v": 2, "seq": 27, "kind": "assistant_final", "role": "assistant", "text": "师傅,", "speech_id": "speech_ba7b3f730ffd", "turn_id": "9bba6412-6679-48c2-9f1c-4bfc07f58aea", "_at_ms": 1793115715589}
{"v": 2, "seq": 28, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_ba7b3f730ffd", "turn_id": "9bba6412-6679-48c2-9f1c-4bfc07f58aea", "_at_ms": 1793115715590}
{"v": 2, "seq": 29, "kind": "interim", "role": "user", "text": "俺老", "_at_ms": 1793115792459}
{"v": 2, "seq": 30, "kind": "interim", "role": "user", "text": "孙来", "_at_ms": 1793115793061}
{"v": 2, "seq": 31, "kind": "interim", "role": "user", "text": "也俺", "_at_ms": 1793115793659}
{"v": 2, "seq": 32, "kind": "interim", "role": "user", "text": "乃五百", "_at_ms": 1793115794261}
{"v": 2, "seq": 33, "kind": "interim", "role": "user", "text": "年前", "_at_ms": 1793115794859}
{"v": 2, "seq": 34, "kind": "interim", "role": "user", "text": "大闹", "_at_ms": 1793115795470}
{"v": 2, "seq": 35, "kind": "interim", "role": "user", "text": "天宫的", "_at_ms": 1793115796055}
{"v": 2, "seq": 36, "kind": "interim", "role": "user", "text": "齐天", "_at_ms": 1793115796661}
{"v": 2, "seq": 37, "kind": "interim", "role": "user", "text": "霸圣", "_at_ms": 1793115797263}
{"v": 2, "seq": 38, "kind": "interim", "role": "user", "text": "孙悟", "_at_ms": 1793115797864}
{"v": 2, "seq": 39, "kind": "interim", "role": "user", "text": "空这", "_at_ms": 1793115798464}
{"v": 2, "seq": 40, "kind": "interim", "role": "user", "text": "里山", "_at_ms": 1793115799067}
{"v": 2, "seq": 41, "kind": "interim", "role": "user", "text": "势险", "_at_ms": 1793115799657}
{"v": 2, "seq": 42, "kind": "interim", "role": "user", "text": "峻", "_at_ms": 1793115800262}
{"v": 2, "seq": 43, "kind": "interim", "role": "user", "text": "只怕藏", "_at_ms": 1793115800858}
{"v": 2, "seq": 44, "kind": "interim", "role": "user", "text": "有妖", "_at_ms": 1793115801462}
{"v": 2, "seq": 45, "kind": "interim", "role": "user", "text": "精妖", "_at_ms": 1793115802053}
{"v": 2, "seq": 46, "kind": "interim", "role": "user", "text": "怪", "_at_ms": 1793115802654}
{"v": 2, "seq": 47, "kind": "interim", "role": "user", "text": "哪里", "_at_ms": 1793115803271}
{"v": 2, "seq": 48, "kind": "interim", "role": "user", "text": "逃", "_at_ms": 1793115803858}
{"v": 2, "seq": 49, "kind": "interim", "role": "user", "text": "吃完老", "_at_ms": 1793115804464}
{"v": 2, "seq": 50, "kind": "interim", "role": "user", "text": "我所", "_at_ms": 1793115805066}
{"v": 2, "seq": 51, "kind": "interim", "role": "user", "text": "一", "_at_ms": 1793115805654}
{"v": 2, "seq": 52, "kind": "user_final", "role": "user", "text": "俺老孙来也,俺乃500年前大闹天宫的齐天大圣孙悟空,这里山势险峻,只怕藏有妖精妖怪,哪里逃吃?俺老孙吧", "_at_ms": 1793115807317}
{"v": 2, "seq": 53, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_d1c8f4816b10", "_at_ms": 1793115810983}
{"v": 2, "seq": 54, "kind": "assistant_interim", "role": "assistant", "text": "好", "speech_id": "speech_d1c8f4816b10", "_at_ms": 1793115812231}
{"v": 2, "seq": 55, "kind": "assistant_final", "role": "assistant", "text": "好", "speech_id": "speech_d1c8f4816b10", "turn_id": "16006868-5ee7-49cc-b9c9-1a8dcfe74d08", "_at_ms": 1793115816208}
{"v": 2, "seq": 56, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_d1c8f4816b10", "turn_id": "16006868-5ee7-49cc-b9c9-1a8dcfe74d08", "_at_ms": 1793115816213}
{"v": 2, "seq": 57, "kind": "interim", "role": "user", "text": "没", "_at_ms": 1793115893204}
{"v": 2, "seq": 58, "kind": "interim", "role": "user", "text": "错我", "_at_ms": 1793115893809}
{"v": 2, "seq": 59, "kind": "interim", "role": "user", "text": "就是个笨", "_at_ms": 1793115894415}
{"v": 2, "seq": 60, "kind": "interim", "role": "user", "text": "蛋", "_at_ms": 1793115895004}
{"v": 2, "seq": 61, "kind": "interim", "role": "user", "text": "派大", "_at_ms": 1793115895614}
{"v": 2, "seq": 62, "kind": "interim", "role": "user", "text": "星我们一", "_at_ms": 1793115896206}
{"v": 2, "seq": 63, "kind": "interim", "role": "user", "text": "起去抓", "_at_ms": 1793115896811}
{"v": 2, "seq": 64, "kind": "interim", "role": "user", "text": "水母", "_at_ms": 1793115897406}
{"v": 2, "seq": 65, "kind": "interim", "role": "user", "text": "吧我们", "_at_ms": 1793115898018}
{"v": 2, "seq": 66, "kind": "interim", "role": "user", "text": "一起度", "_at_ms": 1793115898608}
{"v": 2, "seq": 67, "kind": "interim", "role": "user", "text": "过快乐的一", "_at_ms": 1793115899211}
{"v": 2, "seq": 68, "kind": "user_final", "role": "user", "text": "没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天", "_at_ms": 1793115901261}
{"v": 2, "seq": 69, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_8ac7cd94c5db", "_at_ms": 1793115901269}
{"v": 2, "seq": 70, "kind": "assistant_interim", "role": "assistant", "text": "好", "speech_id": "speech_8ac7cd94c5db", "_at_ms": 1793115902769}
{"v": 2, "seq": 71, "kind": "assistant_final", "role": "assistant", "text": "好", "speech_id": "speech_8ac7cd94c5db", "turn_id": "fdf28a03-19a8-4fcb-ba8c-24587736c3f0", "_at_ms": 1793115908179}
{"v": 2, "seq": 72, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_8ac7cd94c5db", "turn_id": "fdf28a03-19a8-4fcb-ba8c-24587736c3f0", "_at_ms": 1793115908181}
{"v": 2, "seq": 73, "kind": "interim", "role": "user", "text": "你是不是", "_at_ms": 1793115991657}
{"v": 2, "seq": 74, "kind": "interim", "role": "user", "text": "早就想", "_at_ms": 1793115992268}
{"v": 2, "seq": 75, "kind": "interim", "role": "user", "text": "睡我", "_at_ms": 1793115992857}
{"v": 2, "seq": 76, "kind": "interim", "role": "user", "text": "了", "_at_ms": 1793115993462}
{"v": 2, "seq": 77, "kind": "user_final", "role": "user", "text": "你是不是早就想睡我了呀", "_at_ms": 1793115994404}
{"v": 2, "seq": 78, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994412}
{"v": 2, "seq": 79, "kind": "assistant_interim", "role": "assistant", "text": "Considering", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994844}
{"v": 2, "seq": 80, "kind": "assistant_interim", "role": "assistant", "text": "Considering the", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994878}
{"v": 2, "seq": 81, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994911}
{"v": 2, "seq": 82, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994944}
{"v": 2, "seq": 83, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115994977}
{"v": 2, "seq": 84, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995010}
{"v": 2, "seq": 85, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995044}
{"v": 2, "seq": 86, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user,", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995077}
{"v": 2, "seq": 87, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995111}
{"v": 2, "seq": 88, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995144}
{"v": 2, "seq": 89, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995178}
{"v": 2, "seq": 90, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995212}
{"v": 2, "seq": 91, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995245}
{"v": 2, "seq": 92, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995278}
{"v": 2, "seq": 93, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995312}
{"v": 2, "seq": 94, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995345}
{"v": 2, "seq": 95, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995378}
{"v": 2, "seq": 96, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995412}
{"v": 2, "seq": 97, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995445}
{"v": 2, "seq": 98, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995478}
{"v": 2, "seq": 99, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now.", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995511}
{"v": 2, "seq": 100, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now.\n", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995549}
{"v": 2, "seq": 101, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now.\n</think>", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995583}
{"v": 2, "seq": 102, "kind": "assistant_interim", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now.\n</think>\n\n", "speech_id": "speech_9aa32dc387c3", "_at_ms": 1793115995614}
{"v": 2, "seq": 103, "kind": "assistant_final", "role": "assistant", "text": "Considering the limited time by the user, I have to give the solution based on the thinking directly now.\n</think>\n\n", "speech_id": "speech_9aa32dc387c3", "turn_id": "623bab59-0edc-4751-9feb-a9a7e4a11233", "_at_ms": 1793116005934}
{"v": 2, "seq": 104, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_9aa32dc387c3", "turn_id": "623bab59-0edc-4751-9feb-a9a7e4a11233", "_at_ms": 1793116005935}
@@ -0,0 +1,2 @@
WINDOW_START=2026-10-27T15:26:54Z
WINDOW_END=2026-10-27T15:58:10Z
+409
View File
@@ -0,0 +1,409 @@
{"message": "starting worker", "level": "INFO", "name": "livekit.agents", "version": "1.8.8", "rtc-version": "1.1.20", "pid": 8800, "timestamp": "2026-10-27T15:21:02.628498+00:00"}
{"message": "plugin registered", "level": "INFO", "name": "livekit.agents", "plugin": "livekit.plugins.silero", "version": "1.8.8", "pid": 8800, "timestamp": "2026-10-27T15:21:02.628622+00:00"}
{"message": "preloading plugins", "level": "INFO", "name": "livekit.agents", "packages": ["livekit.plugins.silero", "livekit.agents.ipc._preload", "livekit.agents.ipc._preload_freeze"], "pid": 8800, "timestamp": "2026-10-27T15:21:02.628690+00:00"}
{"message": "HTTP server listening on :8082", "level": "INFO", "name": "livekit.agents", "pid": 8800, "timestamp": "2026-10-27T15:21:02.632394+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 8879, "timestamp": "2026-10-27T15:21:03.868641+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 8881, "timestamp": "2026-10-27T15:21:03.871820+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 8883, "timestamp": "2026-10-27T15:21:03.874482+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 8885, "timestamp": "2026-10-27T15:21:03.877329+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 8885, "elapsed_time": 0.14, "timestamp": "2026-10-27T15:21:04.013188+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 8883, "elapsed_time": 0.14, "timestamp": "2026-10-27T15:21:04.013380+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 8879, "elapsed_time": 0.14, "timestamp": "2026-10-27T15:21:04.013613+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 8881, "elapsed_time": 0.14, "timestamp": "2026-10-27T15:21:04.016022+00:00"}
/opt/voice-agent/agent/venv/lib/python3.12/site-packages/jwt/api_jwt.py:149: InsecureKeyLengthWarning: The HMAC key is 28 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.
return self._jws.encode(
{"message": "registered worker", "level": "INFO", "name": "livekit.agents", "agent_name": "cybergril-voice-agent", "deployment": "", "id": "AW_LGViCVagSNNu", "url": "ws://127.0.0.1:7880", "region": "", "protocol": 17, "pid": 8800, "timestamp": "2026-10-27T15:21:04.039801+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_dcfbVLNh9cxc", "dispatch_id": "AD_pFPtM5kSSpZH", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:28:57.444695+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 10801, "timestamp": "2026-10-27T15:28:57.518855+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 10801, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:28:57.639650+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:28:57.893684+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:28:57.895149+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_FBBgf5Si2JYd", "dispatch_id": "AD_iJTKQsucFwSK", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:32:32.446385+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 11093, "timestamp": "2026-10-27T15:32:32.549290+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 11093, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:32:32.665948+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:32:32.853330+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:32:32.855790+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_QhLhdpZKohT3", "dispatch_id": "AD_6Esm9sgx9sbT", "room": "cg-voice-74blnuc9c6", "room_id": "RM_q3vSy5gNpvMA", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:32:39.512686+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 11326, "timestamp": "2026-10-27T15:32:39.616162+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 11326, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:32:39.737803+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 8879, "job_id": "AJ_QhLhdpZKohT3", "room": "cg-voice-74blnuc9c6", "room_id": "RM_q3vSy5gNpvMA", "timestamp": "2026-10-27T15:32:39.969275+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 8879, "job_id": "AJ_QhLhdpZKohT3", "room": "cg-voice-74blnuc9c6", "room_id": "RM_q3vSy5gNpvMA", "timestamp": "2026-10-27T15:32:39.971493+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8879, "job_id": "AJ_QhLhdpZKohT3", "room": "cg-voice-74blnuc9c6", "room_id": "RM_q3vSy5gNpvMA", "timestamp": "2026-10-27T15:32:59.437282+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 8879, "job_id": "AJ_QhLhdpZKohT3", "room": "cg-voice-74blnuc9c6", "room_id": "RM_q3vSy5gNpvMA", "timestamp": "2026-10-27T15:33:01.444726+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_4vPjEVFaLPMq", "dispatch_id": "AD_MVu9k7fFLQyy", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:33:23.836716+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 11830, "timestamp": "2026-10-27T15:33:23.895593+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 11830, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:33:24.010796+00:00"}
Nvidia Decoder is supported.
{"message": "RoomIO linked to participant", "level": "INFO", "name": "livekit.agents", "lk.pii.participant_identity": "user-gwhvezcz", "lk.pii.room_name": "cg-voice-qeghdnxehn", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:33:24.256405+00:00"}
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:33:24.260750+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:33:24.261961+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
[stt] utterance open, frame sr=24000
[stt] interim: 盯
[stt->session] interim_transcript: 盯
[stt] interim: 着你看怎
[stt->session] interim_transcript: 着你看怎
[stt] interim: 么啦
[stt->session] interim_transcript: 么啦
[stt] FINAL(merged): 盯着你看怎么啦
[stt->session] final_transcript: 盯着你看怎么啦
[stt] interim: 难得
[stt->session] interim_transcript: 难得
[stt] interim: 看你这么
[stt->session] interim_transcript: 看你这么
[stt] interim: 有意思的
[stt->session] interim_transcript: 有意思的
[stt] interim: 样子
[stt->session] interim_transcript: 样子
[stt] FINAL(merged): 难得看你这么有意思的样子吗
[stt->session] final_transcript: 难得看你这么有意思的样子吗
{"message": "transcript arrives after turn has been committed. consider raising `min_delay` in the endpointing options to accommodate a slow stt. subsequent occurrences will log at debug level.", "level": "WARNING", "name": "livekit.agents", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:33:30.501478+00:00"}
[stt] interim: 烈会军校
[stt->session] interim_transcript: 烈会军校
[stt] interim: 校啊招
[stt->session] interim_transcript: 校啊招
[stt] interim: 啊这种类
[stt->session] interim_transcript: 啊这种类
[stt] interim: 型我都是
[stt->session] interim_transcript: 型我都是
[stt] interim: 那种比较
[stt->session] interim_transcript: 那种比较
[stt] interim: 呃豪
[stt->session] interim_transcript: 呃豪
[stt] interim: 爽比
[stt->session] interim_transcript: 爽比
[stt] interim: 较开
[stt->session] interim_transcript: 较开
[stt] interim: 朗的那种
[stt->session] interim_transcript: 朗的那种
[stt] interim: 类型
[stt->session] interim_transcript: 类型
[stt] interim: 阳光大
[stt->session] interim_transcript: 阳光大
[stt] interim: 女孩
[stt->session] interim_transcript: 女孩
[stt] interim: 那种感
[stt->session] interim_transcript: 那种感
[stt] interim: 觉
[stt->session] interim_transcript: 觉
[stt] interim: 都给
[stt->session] interim_transcript: 都给
[stt] FINAL(merged): 烈火军校啊,朝阳啊这种类型我都是那种比较呃豪爽,比较开朗的那种类型,阳光大女孩那种感觉就
[stt->session] final_transcript: 烈火军校啊,朝阳啊这种类型我都是那种比较呃豪爽,比较开朗的那种类型,阳光大女孩那
[tts] first audio after 3.335s (原来你是那种阳光大女孩的类型啊,难怪感觉...)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:33:57.436181+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:33:57.437201+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:33:57.437449+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 8885, "job_id": "AJ_dcfbVLNh9cxc", "room": "probe-room-2", "room_id": "RM_mNZagsczYXao", "timestamp": "2026-10-27T15:33:59.447143+00:00"}
[stt] interim: 怎么还
[stt->session] interim_transcript: 怎么还
[stt] interim: 有没事
[stt->session] interim_transcript: 有没事
[stt] interim: 的
[stt->session] interim_transcript: 的
[stt] FINAL(merged): 怎么还有没事的
[stt->session] final_transcript: 怎么还有没事的
[stt] interim: 是我不够
[stt->session] interim_transcript: 是我不够
[stt] interim: 努力
[stt->session] interim_transcript: 努力
[stt] FINAL(merged): 是我不够努力吗
[stt->session] final_transcript: 是我不够努力吗
[stt] interim: 盯着你看
[stt->session] interim_transcript: 盯着你看
[stt] interim: 怎么
[stt->session] interim_transcript: 怎么
[stt] FINAL(merged): 盯着你看怎么啦
[stt->session] final_transcript: 盯着你看怎么啦
[stt] interim: 难得
[stt->session] interim_transcript: 难得
[stt] interim: 看你这么
[stt->session] interim_transcript: 看你这么
[stt] interim: 有意思的
[stt->session] interim_transcript: 有意思的
[stt] interim: 样子
[stt->session] interim_transcript: 样子
{"message": "closing agent session due to participant disconnect (disable via `RoomInputOptions.close_on_disconnect=False`)", "level": "INFO", "name": "livekit.agents", "room": "cg-voice-qeghdnxehn", "participant": "user-gwhvezcz", "reason": "ROOM_DELETED", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.027601+00:00"}
{"message": "skipping user input, speech scheduling is paused", "level": "WARNING", "name": "livekit.agents", "lk.pii.user_input": "盯着你看怎么啦", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.040356+00:00"}
{"message": "failed to send session event\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "level": "WARNING", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.042202+00:00"}
[stt] FINAL(merged): 难得看你这么有意思的样子吗
[stt->session] final_transcript: 难得看你这么有意思的样子吗
{"message": "failed to send session event\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "level": "WARNING", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.890538+00:00"}
{"message": "skipping user input, speech scheduling is paused", "level": "WARNING", "name": "livekit.agents", "lk.pii.user_input": "难得看你这么有意思的样子吗", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.891932+00:00"}
{"message": "failed to send session event\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "level": "WARNING", "name": "livekit.agents", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 477, in _write_loop\n await self._transport.send_message(msg)\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/agents/voice/remote_session.py\", line 153, in send_message\n raise RuntimeError(\"room session transport is closed\")\nRuntimeError: room session transport is closed", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.892740+00:00"}
{"message": "Task exception was never retrieved\nfuture: <Task finished name='Task-453' coro=<LocalParticipant.publish_data() done, defined at /opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/participant.py:315> exception=PublishDataError('engine: connection error: engine is closed')>\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/participant.py\", line 359, in publish_data\n raise PublishDataError(cb.publish_data.error)\nlivekit.rtc.participant.PublishDataError: engine: connection error: engine is closed", "level": "ERROR", "name": "asyncio", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/participant.py\", line 359, in publish_data\n raise PublishDataError(cb.publish_data.error)\nlivekit.rtc.participant.PublishDataError: engine: connection error: engine is closed", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.894879+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 8881, "job_id": "AJ_4vPjEVFaLPMq", "room": "cg-voice-qeghdnxehn", "room_id": "RM_EMAorR7C2evE", "timestamp": "2026-10-27T15:34:12.911612+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_HpxBoFkqfBdm", "dispatch_id": "AD_QPVzQMzStdbx", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:37:07.605078+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 12418, "timestamp": "2026-10-27T15:37:07.631804+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 12418, "elapsed_time": 0.11, "timestamp": "2026-10-27T15:37:07.743701+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:37:08.025546+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:37:08.026426+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_MLg4JBtoku27", "dispatch_id": "AD_uMQVHwFMfKqf", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:37:11.445956+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 12620, "timestamp": "2026-10-27T15:37:11.462186+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 12620, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:37:11.583631+00:00"}
Nvidia Decoder is supported.
{"message": "RoomIO linked to participant", "level": "INFO", "name": "livekit.agents", "lk.pii.participant_identity": "user-vdywshve", "lk.pii.room_name": "cg-voice-5c4y64wxkl", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:37:11.840352+00:00"}
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:37:11.842501+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:37:11.844023+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
[stt] utterance open, frame sr=24000
[stt] interim: 没
[stt->session] interim_transcript: 没
[stt] interim: 错
[stt->session] interim_transcript: 错
[stt] interim: 我就是个
[stt->session] interim_transcript: 我就是个
[stt] interim: 笨蛋
[stt->session] interim_transcript: 笨蛋
[stt] interim: 派大星
[stt->session] interim_transcript: 派大星
[stt] interim: 我们
[stt->session] interim_transcript: 我们
[stt] interim: 一起去
[stt->session] interim_transcript: 一起去
[stt] interim: 抓水母
[stt->session] interim_transcript: 抓水母
[stt] interim: 吧我
[stt->session] interim_transcript: 吧我
[stt] interim: 们一起度
[stt->session] interim_transcript: 们一起度
[stt] interim: 过快乐的
[stt->session] interim_transcript: 过快乐的
[stt] interim: 一天
[stt->session] interim_transcript: 一天
[stt] FINAL(merged): 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt->session] final_transcript: 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[tts] first audio after 3.909s (好呀,那我们这就出发去抓水母!不过我可不...)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:37:32.436074+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:37:32.436405+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:37:32.436558+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "room disconnected", "pid": 8883, "job_id": "AJ_FBBgf5Si2JYd", "room": "probe-room-3", "room_id": "RM_msdxb3QWFMYf", "timestamp": "2026-10-27T15:37:34.449378+00:00"}
[stt] interim: 天雷滚
[stt->session] interim_transcript: 天雷滚
[stt] interim: 滚我好
[stt->session] interim_transcript: 滚我好
[stt] interim: 怕怕
[stt->session] interim_transcript: 怕怕
[stt] interim: 劈
[stt->session] interim_transcript: 劈
[stt] interim: 的我浑
[stt->session] interim_transcript: 的我浑
[stt] interim: 身掉渣
[stt->session] interim_transcript: 身掉渣
[stt] interim: 渣突
[stt->session] interim_transcript: 渣突
[stt] interim: 破天
[stt->session] interim_transcript: 破天
[stt] interim: 劫我笑
[stt->session] interim_transcript: 劫我笑
[stt] interim: 哈哈
[stt->session] interim_transcript: 哈哈
[stt] interim: 逆
[stt->session] interim_transcript: 逆
[stt] interim: 天改命
[stt->session] interim_transcript: 天改命
[stt] interim: 我吹喇
[stt->session] interim_transcript: 我吹喇
[stt] interim: 叭滴
[stt->session] interim_transcript: 叭滴
[stt] interim: 答滴答
[stt->session] interim_transcript: 答滴答
[stt] interim: 滴滴
[stt->session] interim_transcript: 滴滴
[stt] FINAL(merged): 天雷滚滚,我好怕怕劈的我浑身掉渣渣,突破天劫,我笑哈哈逆天改命,我吹喇叭滴答滴答,滴滴答
[stt->session] final_transcript: 天雷滚滚,我好怕怕劈的我浑身掉渣渣,突破天劫,我笑哈哈逆天改命,我吹喇叭滴答滴答
[tts] first audio after 3.008s (哈哈,你唱得还挺有节奏感的!...)
[stt] interim: 你是不是
[stt->session] interim_transcript: 你是不是
[stt] interim: 早就想
[stt->session] interim_transcript: 早就想
[stt] interim: 睡我
[stt->session] interim_transcript: 睡我
[stt] interim: 了
[stt->session] interim_transcript: 了
[stt] FINAL(merged): 你是不是早就想睡我了呀
[stt->session] final_transcript: 你是不是早就想睡我了呀
[tts] first audio after 3.304s (哈哈,你思维跳跃得太快了,我都跟不上。...)
[stt] interim: 盯着你看
[stt->session] interim_transcript: 盯着你看
[stt] interim: 怎么啦
[stt->session] interim_transcript: 怎么啦
[stt] FINAL(merged): 盯着你看怎么啦
[stt->session] final_transcript: 盯着你看怎么啦
[tts] first audio after 2.405s (没...)
{"message": "closing agent session due to participant disconnect (disable via `RoomInputOptions.close_on_disconnect=False`)", "level": "INFO", "name": "livekit.agents", "room": "cg-voice-5c4y64wxkl", "participant": "user-vdywshve", "reason": "ROOM_DELETED", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:38:32.340079+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:38:32.351146+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:38:32.352194+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:38:32.353282+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 11093, "job_id": "AJ_MLg4JBtoku27", "room": "cg-voice-5c4y64wxkl", "room_id": "RM_YDaqgLrwdfsB", "timestamp": "2026-10-27T15:38:34.358814+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_a8uXggcKPamC", "dispatch_id": "AD_fxrSDuKoYdTr", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:41:10.456804+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 13261, "timestamp": "2026-10-27T15:41:10.492898+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 13261, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:41:10.613693+00:00"}
Nvidia Decoder is supported.
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:10.904892+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:10.905825+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:30.437268+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:30.437449+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:30.437534+00:00"}
{"message": "received job request", "level": "INFO", "name": "livekit.agents", "job_id": "AJ_sfYen6Augi9u", "dispatch_id": "AD_UmNB9yQbmDEK", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "agent_name": "cybergril-voice-agent", "resuming": false, "enable_recording": false, "pid": 8800, "timestamp": "2026-10-27T15:41:30.448381+00:00"}
{"message": "initializing process", "level": "INFO", "name": "livekit.agents", "pid": 13614, "timestamp": "2026-10-27T15:41:30.549122+00:00"}
{"message": "process initialized", "level": "INFO", "name": "livekit.agents", "pid": 13614, "elapsed_time": 0.12, "timestamp": "2026-10-27T15:41:30.671152+00:00"}
Nvidia Decoder is supported.
{"message": "RoomIO linked to participant", "level": "INFO", "name": "livekit.agents", "lk.pii.participant_identity": "user-uqfyuhnt", "lk.pii.room_name": "cg-voice-oleganioz7", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:41:30.841632+00:00"}
{"message": "adaptive interruption is disabled by default in production mode", "level": "INFO", "name": "livekit.agents", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:41:30.843597+00:00"}
{"message": "failed to emit event conversation_item_added\nTraceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "level": "ERROR", "name": "livekit", "exc_info": "Traceback (most recent call last):\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/livekit/rtc/event_emitter.py\", line 70, in emit\n callback(*callback_args)\n File \"/opt/voice-agent-cybergril/cybergril/voice/worker.py\", line 193, in _on_item\n if ev.item.role == \"assistant\":\n ^^^^^^^^^^^^\n File \"/opt/voice-agent/agent/venv/lib/python3.12/site-packages/pydantic/main.py\", line 1105, in __getattr__\n raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}')\nAttributeError: 'AgentHandoff' object has no attribute 'role'", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:41:30.844442+00:00"}
[cybergril-worker] session started (api=http://127.0.0.1:8360, voice=female_soft, agent_name=cybergril-voice-agent)
[stt] utterance open, frame sr=24000
[stt] interim: 师
[stt->session] interim_transcript: 师
[stt] interim: 傅
[stt->session] interim_transcript: 傅
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 11326, "job_id": "AJ_a8uXggcKPamC", "room": "cg-voice-6d0grxxwno", "room_id": "RM_bHsWETGZc5cQ", "timestamp": "2026-10-27T15:41:32.444706+00:00"}
[stt] interim: 哎这
[stt->session] interim_transcript: 哎这
[stt] interim: 取经
[stt->session] interim_transcript: 取经
[stt] interim: 路比
[stt->session] interim_transcript: 路比
[stt] interim: 高老
[stt->session] interim_transcript: 高老
[stt] interim: 庄种
[stt->session] interim_transcript: 庄种
[stt] interim: 地还
[stt->session] interim_transcript: 地还
[stt] interim: 苦俺
[stt->session] interim_transcript: 苦俺
[stt] interim: 老中
[stt->session] interim_transcript: 老中
[stt] interim: 饿的能
[stt->session] interim_transcript: 饿的能
[stt] interim: 吞下
[stt->session] interim_transcript: 吞下
[stt] interim: 石龙
[stt->session] interim_transcript: 石龙
[stt] interim: 素包
[stt->session] interim_transcript: 素包
[stt] interim: 子
[stt->session] interim_transcript: 子
[stt] interim: 猴哥
[stt->session] interim_transcript: 猴哥
[stt] interim: 你筋
[stt->session] interim_transcript: 你筋
[stt] interim: 斗云
[stt->session] interim_transcript: 斗云
[stt] interim: 快
[stt->session] interim_transcript: 快
[stt] interim: 要不
[stt->session] interim_transcript: 要不
[stt] interim: 先
[stt->session] interim_transcript: 先
[stt] interim: 去画个
[stt->session] interim_transcript: 去画个
[stt] FINAL(merged): 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云块,要不先去化个缘
[stt->session] final_transcript: 师傅,哎,这取经路比高,老庄种地还苦,俺老猪饿的能吞下石龙素包子,猴哥,你筋斗云
[tts] first audio after 2.480s (师傅,...)
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_reliable' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:42:07.442254+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_lossy' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:42:07.442585+00:00"}
{"message": "livekit::rtc_engine::rtc_session:704:livekit::rtc_engine::rtc_session - publisher data channel '_data_track' closed unexpectedly", "level": "ERROR", "name": "livekit", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:42:07.442726+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 10801, "job_id": "AJ_HpxBoFkqfBdm", "room": "cg-voice-ymtapbx42a", "room_id": "RM_5BfQDxZuTsu4", "timestamp": "2026-10-27T15:42:09.450806+00:00"}
[stt] interim: 俺老
[stt->session] interim_transcript: 俺老
[stt] interim: 孙来
[stt->session] interim_transcript: 孙来
[stt] interim: 也俺
[stt->session] interim_transcript: 也俺
[stt] interim: 乃五百
[stt->session] interim_transcript: 乃五百
[stt] interim: 年前
[stt->session] interim_transcript: 年前
[stt] interim: 大闹
[stt->session] interim_transcript: 大闹
[stt] interim: 天宫的
[stt->session] interim_transcript: 天宫的
[stt] interim: 齐天
[stt->session] interim_transcript: 齐天
[stt] interim: 霸圣
[stt->session] interim_transcript: 霸圣
[stt] interim: 孙悟
[stt->session] interim_transcript: 孙悟
[stt] interim: 空这
[stt->session] interim_transcript: 空这
[stt] interim: 里山
[stt->session] interim_transcript: 里山
[stt] interim: 势险
[stt->session] interim_transcript: 势险
[stt] interim: 峻
[stt->session] interim_transcript: 峻
[stt] interim: 只怕藏
[stt->session] interim_transcript: 只怕藏
[stt] interim: 有妖
[stt->session] interim_transcript: 有妖
[stt] interim: 精妖
[stt->session] interim_transcript: 精妖
[stt] interim: 怪
[stt->session] interim_transcript: 怪
[stt] interim: 哪里
[stt->session] interim_transcript: 哪里
[stt] interim: 逃
[stt->session] interim_transcript: 逃
[stt] interim: 吃完老
[stt->session] interim_transcript: 吃完老
[stt] interim: 我所
[stt->session] interim_transcript: 我所
[stt] interim: 一
[stt->session] interim_transcript: 一
[stt] FINAL(merged): 俺老孙来也,俺乃500年前大闹天宫的齐天大圣孙悟空,这里山势险峻,只怕藏有妖精妖怪,哪里逃吃?俺老孙吧
[stt->session] final_transcript: 俺老孙来也,俺乃500年前大闹天宫的齐天大圣孙悟空,这里山势险峻,只怕藏有妖精妖
[tts] first audio after 2.608s (好...)
[stt] interim: 没
[stt->session] interim_transcript: 没
[stt] interim: 错我
[stt->session] interim_transcript: 错我
[stt] interim: 就是个笨
[stt->session] interim_transcript: 就是个笨
[stt] interim: 蛋
[stt->session] interim_transcript: 蛋
[stt] interim: 派大
[stt->session] interim_transcript: 派大
[stt] interim: 星我们一
[stt->session] interim_transcript: 星我们一
[stt] interim: 起去抓
[stt->session] interim_transcript: 起去抓
[stt] interim: 水母
[stt->session] interim_transcript: 水母
[stt] interim: 吧我们
[stt->session] interim_transcript: 吧我们
[stt] interim: 一起度
[stt->session] interim_transcript: 一起度
[stt] interim: 过快乐的一
[stt->session] interim_transcript: 过快乐的一
[stt] FINAL(merged): 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[stt->session] final_transcript: 没错,我就是个笨蛋派大星,我们一起去抓水母吧,我们一起度过快乐的一天
[tts] first audio after 2.300s (好...)
[stt] interim: 你是不是
[stt->session] interim_transcript: 你是不是
[stt] interim: 早就想
[stt->session] interim_transcript: 早就想
[stt] interim: 睡我
[stt->session] interim_transcript: 睡我
[stt] interim: 了
[stt->session] interim_transcript: 了
[stt] FINAL(merged): 你是不是早就想睡我了呀
[stt->session] final_transcript: 你是不是早就想睡我了呀
{"message": "failed to generate LLM completion: chat stream ended without final or error (caused by ChatApiConnectionError: chat stream ended without final or error), retrying in 0.1s", "level": "WARNING", "name": "livekit.agents", "llm": "cybergril.voice.llm_chat_api.CyberGrillLLM", "attempt": 1, "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:46:34.451048+00:00"}
[tts] first audio after 3.847s (Considering the limi...)
[tts] first audio after 2.284s (</think>...)
{"message": "closing agent session due to participant disconnect (disable via `RoomInputOptions.close_on_disconnect=False`)", "level": "INFO", "name": "livekit.agents", "room": "cg-voice-oleganioz7", "participant": "user-uqfyuhnt", "reason": "CLIENT_INITIATED", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:46:46.019622+00:00"}
{"message": "process exiting", "level": "INFO", "name": "livekit.agents", "reason": "parent process shutdown", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:47:06.441875+00:00"}
{"message": "livekit_ffi::server:279:livekit_ffi::server - Attempted to drop unknown FFI handle: 16", "level": "WARNING", "name": "livekit", "pid": 11830, "job_id": "AJ_sfYen6Augi9u", "room": "cg-voice-oleganioz7", "room_id": "RM_anqqGcUZrCbe", "timestamp": "2026-10-27T15:47:06.446226+00:00"}
{"message": "draining worker", "level": "INFO", "name": "livekit.agents", "id": "AW_LGViCVagSNNu", "timeout": 3600, "pid": 8800, "timestamp": "2026-10-27T15:58:10.576647+00:00"}
{"message": "shutting down worker", "level": "INFO", "name": "livekit.agents", "id": "AW_LGViCVagSNNu", "pid": 8800, "timestamp": "2026-10-27T15:58:10.577287+00:00"}
@@ -0,0 +1,215 @@
{
"head": "HEL-894 close ownership under cancellation",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"protocol": "drain-phase cancel re-arms; finish-phase cancel keeps markers, refuses ticks, lands the layer-owned close exactly once; lock waiters never touch the owner's state",
"checks": [
{
"name": "F1 cancelled caller does not return before its close lands",
"ok": true,
"detail": ""
},
{
"name": "F1 markers stay: closing set, not yet closed, client untouched",
"ok": true,
"detail": "{\"closing\": true, \"closed\": false, \"client_usable\": true}"
},
{
"name": "F1 tick during closing claims nothing and sends nothing",
"ok": true,
"detail": "{\"gateway_arrived\": false, \"gateway_posts\": 0, \"llm_used\": 0, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F1 layer-owned close lands exactly once, no live workers",
"ok": true,
"detail": "{\"closed\": true, \"client_usable\": false, \"live_workers\": 0, \"close_calls\": 1, \"closing_still_set\": true, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F1 no late error",
"ok": true,
"detail": "None"
},
{
"name": "F1 reopened batch intact and unbilled",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 0, \"llm_used\": 0}"
},
{
"name": "F2 cancelled caller does not return before its close lands",
"ok": true,
"detail": ""
},
{
"name": "F2 markers stay: closing set, not yet closed, client untouched",
"ok": true,
"detail": "{\"closing\": true, \"closed\": false, \"client_usable\": true}"
},
{
"name": "F2 tick during closing claims nothing and sends nothing",
"ok": true,
"detail": "{\"gateway_arrived\": false, \"gateway_posts\": 0, \"llm_used\": 0, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F2 layer-owned close lands exactly once, no live workers",
"ok": true,
"detail": "{\"closed\": true, \"client_usable\": false, \"live_workers\": 0, \"close_calls\": 1, \"closing_still_set\": true, \"batch\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}}"
},
{
"name": "F2 no late error",
"ok": true,
"detail": "None"
},
{
"name": "F2 reopened batch intact and unbilled",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 0, \"llm_used\": 0}"
},
{
"name": "W owner close still pending, waiter cancel keeps the signal",
"ok": true,
"detail": "{\"active_close_pending\": true, \"signal_after_waiter_cancel\": true}"
},
{
"name": "W new batch refused during the owner's close",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}"
},
{
"name": "W original batch still actively owned while blocked",
"ok": true,
"detail": "{\"state\": \"pending\", \"reason\": null, \"owner\": \"a6173b4a79a84dbd6378281c17b23127\", \"attempts\": 1, \"entry_ids\": null}"
},
{
"name": "W original worker completes, one close, new batch untouched",
"ok": true,
"detail": "{\"original_batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"494b27cf-6e99-4e9a-ad17-bc4c9d2f42ec\\\"]\"}, \"new_batch_still\": {\"state\": \"queued\", \"reason\": null, \"owner\": null, \"attempts\": 0, \"entry_ids\": null}, \"row_done\": 1, \"gateway_posts_after_release\": 1, \"client_closed\": true}"
}
],
"finish_cancel_sdk_only": {
"caller_done_after_cancel": false,
"after_cancellation": {
"closing": true,
"closed": false,
"client_usable": true
},
"tick_during_closing": {
"gateway_arrived": false,
"gateway_posts": 0,
"llm_used": 0,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"finish_landed": {
"closed": true,
"client_usable": false,
"live_workers": 0,
"close_calls": 1,
"closing_still_set": true,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"late_worker_error": null,
"persisted": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 0,
"llm_used": 0
}
},
"finish_cancel_then_store_close": {
"caller_done_after_cancel": false,
"after_cancellation": {
"closing": true,
"closed": false,
"client_usable": true
},
"tick_during_closing": {
"gateway_arrived": false,
"gateway_posts": 0,
"llm_used": 0,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"finish_landed": {
"closed": true,
"client_usable": false,
"live_workers": 0,
"close_calls": 1,
"closing_still_set": true,
"batch": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
}
},
"late_worker_error": null,
"persisted": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 0,
"llm_used": 0
}
},
"waiter_cancel": {
"active_close_pending": true,
"signal_after_waiter_cancel": true,
"new_batch_during_closing": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
},
"original_batch_while_blocked": {
"state": "pending",
"reason": null,
"owner": "a6173b4a79a84dbd6378281c17b23127",
"attempts": 1,
"entry_ids": null
},
"after_release": {
"original_batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"494b27cf-6e99-4e9a-ad17-bc4c9d2f42ec\"]"
},
"new_batch_still": {
"state": "queued",
"reason": null,
"owner": null,
"attempts": 0,
"entry_ids": null
},
"row_done": 1,
"gateway_posts_after_release": 1,
"client_closed": true
}
},
"failures": []
}
@@ -0,0 +1,40 @@
{
"head": "HEL-886 inflight lifecycle",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "L1_cancel_single_send_and_full_record",
"ok": true,
"detail": "arrived=1 concurrent=1 http_total=1 points=1 batch_live={'state': 'pending', 'owner': '9a6afb62871305fe6c7a056acf0b8797'} batch_concurrent=pending final={'state': 'done', 'reason': None, 'entry_ids': '[\"9838ab55-15ab-4872-b7cd-1ba91faea2d7\"]'} row_done=1"
},
{
"name": "L2_lease_single_send_and_full_record",
"ok": true,
"detail": "arrived=1 concurrent=1 http_total=1 points=1 batch_live={'state': 'pending', 'owner': '8f42725681bcfda7043797c128eb0750'} batch_concurrent=pending final={'state': 'done', 'reason': None, 'entry_ids': '[\"e71ba931-650f-40a3-b571-5945829f6f7c\"]'} row_done=1"
},
{
"name": "L3_shutdown_drains_live_worker_before_close",
"ok": true,
"detail": "arrived=1 waited=0.25s row_done_at=2000000 batch={'state': 'done', 'finished_at': 2000000}"
},
{
"name": "L4a_all_done_finalizes_without_new_budget",
"ok": true,
"detail": "before={'state': 'done', 'entry_ids': '[\"80c8cb6b-e437-478c-a3fb-46d386ac9a07\"]'} after={'state': 'done', 'reason': None, 'entry_ids': '[\"80c8cb6b-e437-478c-a3fb-46d386ac9a07\"]'} http=1 llm_used=1"
},
{
"name": "L4b_partial_keeps_the_hard_gate",
"ok": true,
"detail": "after={'state': 'queued', 'reason': 'semantic_llm_budget_exhausted'} http=0"
},
{
"name": "L5_dead_owner_lease_still_recovers",
"ok": true,
"detail": "recovered={'state': 'failed', 'reason': 'lease_expired'} after={'state': 'done'} http=2"
}
],
"gateway_requests_total": 7,
"failures": []
}
@@ -0,0 +1,172 @@
{
"started": "2026-10-26T19:35:46Z",
"embedding_boundary": "deterministic 512-dimensional vectors installed before TextEmbedding construction; no model loaded; zero real embeddings",
"external_network": "socket.connect rejects all non-loopback hosts",
"install": {
"python": "3.10.12",
"mem0ai": "2.2.1",
"qdrant-client": "1.20.0",
"fastembed": "0.9.1",
"mem0_path": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-venv/lib/python3.10/site-packages/mem0",
"module_sha256": {
"memory/main.py": "f329ca4a17af1fbefb32ac10d4cc7ae7ddbdb7435a87eb74c30915cff98b8b7c",
"memory/storage.py": "5f5ecdadd2b260fd14f36a0f338336eb760275465dd1056a99a47e1ef07702bd",
"embeddings/fastembed.py": "3ec2ae87df5d0bebedb8b5471366c3412975847458499f66097f7b8c9ac8c9b8",
"llms/openai.py": "37e6d3c22b55df0731740f4e0868b95bd480ce44363227fe53461db328c5f67e",
"vector_stores/qdrant.py": "da570579d5cad69e6f123fdfe32b725235a25604e70047666bec763ba6e2bf43"
},
"source_tarball_sha256": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23"
},
"adapter_has_llm_guard": true,
"checks": [
{
"name": "C1_construct",
"ok": true,
"detail": "client_path=sdk-vector-store-client"
},
{
"name": "C2_add_anchor_payload",
"ok": true,
"detail": "created=True payload_keys=['created_at', 'data', 'hash', 'input_sha256', 'src_group', 'text_lemmatized', 'turn_ids', 'turn_roles']"
},
{
"name": "C2_add_user_scope",
"ok": true,
"detail": "user_id present in stored payload"
},
{
"name": "C2_add_ledger_shape",
"ok": true,
"detail": "embed_items=3 llm_calls=1"
},
{
"name": "C3_extraction_input_set",
"ok": true,
"detail": "old_memory=True new_row=True history_carry=True"
},
{
"name": "C3b_unshared_canary_never_outbound",
"ok": true,
"detail": "queued_public_batches=2 leaked=False anchor_taint=False"
},
{
"name": "C4_search_filters",
"ok": true,
"detail": "hits=3"
},
{
"name": "C4_search_single_debit",
"ok": true,
"detail": "embed_delta=1 (pre-fix=2)"
},
{
"name": "C6_concurrency_cap",
"ok": true,
"detail": "outcomes=['ok', 'refused'] used=3 cap=3"
},
{
"name": "C7_reopen_after_close",
"ok": true,
"detail": "hits_after_reopen=3"
},
{
"name": "C5_embed_zero_refuses",
"ok": true,
"detail": "err=semantic_embed_budget_not_approved llm_sent=0"
},
{
"name": "C5_llm_zero_refuses",
"ok": true,
"detail": "err=semantic_llm_budget_not_approved llm_sent=0"
}
],
"embed_rows_after_first_add": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
}
],
"final_embed_rows": [
{
"kind": "authority_prefix",
"items": 0,
"note": "authority:synthetic-verify-prefix"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:batch:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:add"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
},
{
"kind": "dense",
"items": 1,
"note": "product:search"
}
],
"final_llm_used": 3,
"finished": "2026-10-26T19:35:47Z",
"gateway_requests_total": 3,
"failures": [],
"temp_dir": "/tmp/multica-task-29494715/hel874-real-sdk-8dkofioe"
}
@@ -0,0 +1,45 @@
{
"head": "HEL-885 rework",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "S0_positive_caps_without_authority_fail_closed",
"ok": true,
"detail": "missing=ledger_authority_required empty=ledger_authority_required"
},
{
"name": "S1_p1_first_tick_partial",
"ok": true,
"detail": "state=queued reason=LLM extraction failed: semantic_llm_budget_exhausted http=1 completed=[2] points=1 per_row_anchor=True"
},
{
"name": "S2_p1_restart_sends_only_missing_row",
"ok": true,
"detail": "state=done http=1 user_sent=True completed=[1, 2] entry_ids=[\"07401cdd-4964-4c74-aab5-516ab09cf5e4\", \"4d76b52d-186c-4b21-8bfa-e81341213589\"]"
},
{
"name": "S3a_lease_expired_batch_recovers",
"ok": true,
"detail": "recovered={'state': 'failed', 'reason': 'lease_expired'} state_after=done http=2"
},
{
"name": "S3b_concurrent_ticks_single_send",
"ok": true,
"detail": "http=2 states=['done']"
},
{
"name": "S4a_aged_unknown_llm_reservation_kept",
"ok": true,
"detail": "second_allowed=False used_after_restart=1"
},
{
"name": "S4b_aged_unknown_embed_reservation_kept",
"ok": true,
"detail": "second_allowed=False used=1 after_release=0"
}
],
"gateway_requests_total": 6,
"failures": []
}
@@ -0,0 +1,182 @@
{
"head": "HEL-889 shutdown ownership & queued-cancel",
"real_llm_requests": 0,
"real_embeddings": 0,
"boundary": "deterministic 512 vectors, non-loopback forbidden",
"checks": [
{
"name": "Q1 queued snapshot shows phantom-liveness",
"ok": true,
"detail": ""
},
{
"name": "Q1 cancel releases registry and requeues",
"ok": true,
"detail": "{\"registered_workers\": 0, \"batch\": {\"state\": \"queued\", \"reason\": \"queued_worker_cancelled\", \"owner\": null, \"attempts\": 0, \"entry_ids\": null}, \"row_done\": 0, \"stored_entries\": 0}"
},
{
"name": "Q1 in-process recovery completes without restart",
"ok": true,
"detail": "{\"batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"stub-1\\\"]\"}, \"registered_workers\": 0, \"stored_entries\": 1, \"row_done\": 1}"
},
{
"name": "S1 aclose does NOT return while the worker is alive",
"ok": true,
"detail": ""
},
{
"name": "S1 worker completes and flushes before store close",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"19039615-6f32-48e6-979b-715d2cd56fcb\\\"]\"}"
},
{
"name": "S1 no late-worker error after honest drain",
"ok": true,
"detail": ""
},
{
"name": "S1 durable reopen: done + row_done=1 + llm_used=1",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"19039615-6f32-48e6-979b-715d2cd56fcb\\\"]\", \"row_done\": 1, \"llm_used\": 1, \"gateway_posts\": 1}"
},
{
"name": "S2 shutdown between rows requeues with first row proven",
"ok": true,
"detail": "{\"state\": \"queued\", \"reason\": \"shutdown_between_rows\", \"owner\": null, \"attempts\": 0, \"entry_ids\": null, \"row_done\": 1, \"gateway_posts_during_shutdown\": 1}"
},
{
"name": "S2 resume sends ONLY the missing row and completes",
"ok": true,
"detail": "{\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"f26d374a-858e-4dde-b2de-ae8af7057021\\\", \\\"258ab9a5-d523-45b0-ada1-d58fbda50ea7\\\"]\", \"row_done\": 2, \"gateway_posts_resume\": 2}"
},
{
"name": "S3 cancelled aclose closes nothing and re-arms",
"ok": true,
"detail": ""
},
{
"name": "S3 worker finishes done and a later aclose closes for real",
"ok": true,
"detail": ""
},
{
"name": "S4 concurrent aclose drains once and closes once",
"ok": true,
"detail": "{\"batch\": {\"state\": \"done\", \"reason\": null, \"owner\": null, \"attempts\": 1, \"entry_ids\": \"[\\\"e553422c-1840-4ff4-b51c-a3586cc34179\\\"]\", \"row_done\": 1}, \"client_usable_after_close\": false}"
}
],
"queued_cancel": {
"queued": {
"registered_workers": 1,
"product_executor_queue": 1
},
"after_cancel": {
"registered_workers": 0,
"batch": {
"state": "queued",
"reason": "queued_worker_cancelled",
"owner": null,
"attempts": 0,
"entry_ids": null
},
"row_done": 0,
"stored_entries": 0
},
"after_recovery": {
"batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"stub-1\"]"
},
"registered_workers": 0,
"stored_entries": 1,
"row_done": 1
}
},
"shutdown_timeout": {
"deadline": {
"aclose_returned_while_worker_alive": false,
"batch_before_any_close": {
"state": "pending",
"reason": null,
"owner": "4a048d364194a961365c30ee1e570233",
"attempts": 1,
"entry_ids": null
},
"client_usable": true
},
"before_store_close": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"19039615-6f32-48e6-979b-715d2cd56fcb\"]"
},
"late_worker_error": null,
"persisted": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"19039615-6f32-48e6-979b-715d2cd56fcb\"]",
"row_done": 1,
"llm_used": 1,
"gateway_posts": 1
}
},
"between_rows": {
"after_shutdown": {
"state": "queued",
"reason": "shutdown_between_rows",
"owner": null,
"attempts": 0,
"entry_ids": null,
"row_done": 1,
"gateway_posts_during_shutdown": 1
},
"after_resume": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"f26d374a-858e-4dde-b2de-ae8af7057021\", \"258ab9a5-d523-45b0-ada1-d58fbda50ea7\"]",
"row_done": 2,
"gateway_posts_resume": 2
}
},
"cancelled_aclose": {
"after_cancel": {
"client_usable": true,
"batch": {
"state": "pending",
"reason": null,
"owner": "a712e0367aa123b265b47e2d3223846a",
"attempts": 1,
"entry_ids": null
},
"closing_cleared": true
},
"after_worker": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"73f87468-f546-46d7-8116-37d4fe2818b1\"]"
},
"client_usable_after_real_close": false
},
"concurrent_aclose": {
"batch": {
"state": "done",
"reason": null,
"owner": null,
"attempts": 1,
"entry_ids": "[\"e553422c-1840-4ff4-b51c-a3586cc34179\"]",
"row_done": 1
},
"client_usable_after_close": false
},
"failures": []
}
+161
View File
@@ -0,0 +1,161 @@
{
"card": "HEL-879",
"generated_by": "release-artifacts inventory (offline, zero downloads)",
"release_code_commit": "01ff91985920162cddabc5f190d146d86793627d",
"rework_head": "see delivery comment (branch hel-879-assembly, appended after 7f0a739; this inventory refreshed on that head)",
"audit_evidence": "docs/rework/hel879/sdk-audit/sdk-audit.json (head c121c42e39174e0f627f129e7f7e5700f33acf26)",
"single_install_entry": "deploy/hel895-release-candidate.md #4 (supersedes deploy/hel874-release.md #4)",
"fixed_source": {
"artifact": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/mem0-2c81af3.tar.gz",
"sha256": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"lock_record": "deploy/mem0-stack.lock: 9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"matches_lock": true,
"source_commit": "2c81af3bbe27d9bccb09c772d583df9e9be72ee5"
},
"closure": {
"total": 55,
"missing": 41,
"missing_pure_python": [
"backoff",
"coloredlogs",
"distro",
"fastembed",
"filelock",
"flatbuffers",
"fsspec",
"h2",
"hpack",
"httpcore2",
"httpx2",
"huggingface-hub",
"humanfriendly",
"hyperframe",
"loguru",
"mem0ai",
"mpmath",
"openai",
"portalocker",
"posthog",
"pytz",
"qdrant-client",
"requests",
"sniffio",
"sympy",
"tqdm",
"truststore",
"urllib3"
],
"missing_native": [
"charset-normalizer",
"greenlet",
"grpcio",
"hf-xet",
"jiter",
"mmh3",
"numpy",
"onnxruntime",
"pillow",
"protobuf",
"py-rust-stemmers",
"sqlalchemy",
"tokenizers"
]
},
"cache_reusable_reverified": [
{
"name": "greenlet",
"version": "3.5.6",
"cache_hits": [
{
"kind": "uv:wheels-v7",
"entry": "3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"content_digest": "d7ae14625784af8a268e8c156533b2e9d7694680e80a8466c8e5808cc3f1a005"
},
{
"kind": "uv:wheels-v6",
"entry": "3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"content_digest": "2fc3afa56933ea2cbaf6005c1e61565e769db1f202b3d8c53a67e59a2ed307f7"
}
]
},
{
"name": "pillow",
"version": "12.3.0",
"cache_hits": [
{
"kind": "uv:wheels-v7",
"entry": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"content_digest": "02620d3560d2e91e0920c1045ed814b02a4d15d8c1c960f43a060739acf0dc1f"
},
{
"kind": "uv:wheels-v6",
"entry": "12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"content_digest": "657f02aee576df196a22c130a65a3dd0f7438a8b859c2221448bea2dac6d7112"
},
{
"kind": "uv:wheels-v6",
"entry": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"content_digest": "02620d3560d2e91e0920c1045ed814b02a4d15d8c1c960f43a060739acf0dc1f"
}
]
},
{
"name": "truststore",
"version": "0.10.5",
"cache_hits": [
{
"kind": "uv:wheels-v7",
"entry": "0.10.5-py3-none-any",
"content_digest": "54c5d4c83a1183954a6ca33169f8881c576160c722e93064ece271776ee2eac4"
},
{
"kind": "uv:wheels-v6",
"entry": "0.10.5-py3-none-any",
"content_digest": "54c5d4c83a1183954a6ca33169f8881c576160c722e93064ece271776ee2eac4"
}
]
}
],
"build_backend": {
"hatchling_cached_or_local": true,
"hatchling_version": "1.32.4",
"hatchling_cache_evidence": [
"uv wheels-v6: ~/.cache/uv/wheels-v6/pypi/hatchling/1.32.4-py3-none-any",
"uv wheels-v7: ~/.cache/uv/wheels-v7/pypi/hatchling/1.32.4-py3-none-any",
"uv archive-v0 unpacked: hatchling-1.32.4.dist-info (wheels-v6 + wheels-v7 linked copies)"
],
"offline_wheel_built": {
"built_at": "2026-10-27T04:10:00Z",
"command": "cd <extracted mem0-2c81af3 tarball> && uv build --wheel --offline --python 3.11 -o <out>",
"tool": "uv 0.13.5, uv-managed CPython 3.11.16, build backend hatchling from the offline cache only",
"source_tarball_sha256": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"source_commit": "2c81af3bbe27d9bccb09c772d583df9e9be72ee5",
"wheel": "mem0ai-2.2.1-py3-none-any.whl",
"wheel_sha256": "698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db",
"wheel_bytes": 352586,
"wheel_files": 153,
"installed_anywhere": false,
"network_used": false,
"note": "HEL-879 勘误:v1 清单一面写 hatchling_cached_or_local=true 一面又称本机缺 hatchling,并把 hatchling 列入待下载——总管只读核查指出 uv v6/v7 均有 1.32.4 缓存。已核实并直接完成可离线构建:固定源码 tarball(sha 与 lock 一致)经缓存 hatchling 离线构建出上面这只 wheel,未安装、未联网、未混 3.10 site-packages。待下载清单据此只余 41 项运行闭包(不含 hatchling)。"
}
},
"fastembed_models": {
"cache_dir": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/fastembed-cache",
"exists": true,
"models": {
"models--Qdrant--bge-small-zh-v1.5": {
"files": 13,
"bytes": 190444780
},
"models--Qdrant--bm25": {
"files": 39,
"bytes": 56247
}
},
"default_cache_dir": "/home/xiaomo/.cache/fastembed",
"default_cache_exists": false,
"release_note": "真实 mem0 模式把 fastembed cache_dir 指向发布实例数据目录内的该缓存拷贝;本次零下载、不推理"
},
"downloads_this_card": 0,
"note": "产物清单以 release_code_commit 为准;不要用旧父提交(04eff61/152479b/c121c42)说明冒充该 head。缓存命中为解包目录时记录内容摘要(目录无单一 sha256),安装时以 --offline 命中为准。"
}
@@ -0,0 +1,86 @@
{
"started": "2026-10-26T19:35:15Z",
"venv_python": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-879-3c721d56b938/workdir/CyberGril/.venv311/bin/python",
"venv_python_version": "3.11.16",
"checks": [
{
"name": "R1_checkout_fresh_dir",
"ok": true,
"detail": "cloned tracked source into /tmp/multica-task-29494715/hel882-release-jk7k2a25/cybergril-hel874/current"
},
{
"name": "R2_service_imports_product",
"ok": true,
"detail": ""
},
{
"name": "R6_mem0_coimport_status_recorded",
"ok": true,
"detail": "import mem0 → ModuleNotFoundError: No module named 'mem0' (resolution path in deploy/hel874-release.md §4; no download in this check)"
},
{
"name": "R7_pip_download_command_exists",
"ok": true,
"detail": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-879-3c721d56b938/workdir/CyberGril/.venv311/bin/python -m pip download --help → rc=0"
},
{
"name": "R7_removed_uv_pip_download_still_absent",
"ok": true,
"detail": "uv pip download --help → rc=2 (kept absent on purpose; do not re-document it)"
},
{
"name": "R2_service_serves_health",
"ok": true,
"detail": "version=R4-assembly semantic=stub"
},
{
"name": "R3_create_status_recorded",
"ok": true,
"detail": "create status=201(409=前次运行的档案仍在,也是保留数据的表现)"
},
{
"name": "R3_dialogue_refused_honestly",
"ok": true,
"detail": "http=200 events=b'data: {\"t\": \"chat.error\", \"epoch\": 1, \"turn_id\": \"00000000-0000-4000-8000-000000000001\", \"phi\": \"\\xe5\\xa5\\xb9\\xe8\\xbf\\x99\\xe4\\xbc\\x9a\\xe5\\x84\\xbf\\xe6\\xb2\\xa1\\xe6\\x8e\\xa5\\xe4\\xb8\\x8a\\xe8\\xaf'"
},
{
"name": "R4_service_restarts_on_same_data",
"ok": true,
"detail": "epoch=2"
},
{
"name": "R5_epoch_bumped_on_reopen",
"ok": true,
"detail": "1 → 2"
},
{
"name": "R4_companion_created_offline",
"ok": true,
"detail": "status=409"
},
{
"name": "R4_dialogue_stream_http",
"ok": true,
"detail": "status=200 events=b'data: {\"t\": \"chat.delta\", \"epoch\": 2, \"turn_id\": \"00000000-0000-4000-8000-000000000002\", \"delta\": \"\\xe6\\x89\\xb9\"}\\n\\ndata: {\"t\": \"c'"
},
{
"name": "R4_messages_roundtrip",
"ok": true,
"detail": "roles=['user', 'user', 'companion']"
},
{
"name": "R5_data_dir_preserved",
"ok": true,
"detail": "['companion.db']"
},
{
"name": "R5_companion_row_survives_restart",
"ok": true,
"detail": "companion rows=1"
}
],
"mem0_import_in_release_venv": "ModuleNotFoundError: No module named 'mem0'",
"removed_uv_pip_download_rc": 2,
"failures": [],
"finished": "2026-10-26T19:35:18Z"
}
@@ -0,0 +1,67 @@
{
"ok": true,
"code_commit": "7c73d392b95f3d55091a066db0ffe9bc6c475e0d",
"tool_version": 3,
"checks": [
{
"name": "naive_online_copy_loses_wal_reproduced",
"ok": true,
"detail": "error=no such table: companion"
},
{
"name": "busy_checkpoint_state_reproduced",
"ok": true,
"detail": "checkpoint=(1, 9, 8) naive_snapshot_events=error=DatabaseError:database disk image is malformed"
},
{
"name": "fix1_busy_checkpoint_fails_no_archive",
"ok": true,
"detail": "error=yes leftovers=0"
},
{
"name": "fix1b_row_mismatch_fails_no_archive",
"ok": true,
"detail": "error=源/快照行数不一致,不打包:/tmp/multica-task-3939683741/hel879-snapshot-s artifacts=[]"
},
{
"name": "fix_verify_rejects_rows_match_false_legacy_package",
"ok": true,
"detail": "ok=False checks=[{'db': 'databases/companion.db', 'restored': '/tmp/multica-task-3939683741/hel879-snapshot-selftest-a4xt_3p_/verify-restore-legacy-pkg.tar-20261027-042723/by-source/tmp__multica-task-3939683741__hel879-snapshot-selftest-a4xt_3p___instance__companion.db', 'sha256_ok': True, 'rows_ok': False, 'counts': {'companion': 1, 'chat_message': 1, 'events': 1}, 'source_counts': {'companion': 1, 'chat_message': 1, 'events': 1}, 'snapshot_counts': {'companion': 1, 'chat_message': 1, 'events': 0}, 'rows_match_recorded': False, 'rejected_legacy_package': True}]"
},
{
"name": "fix2_online_mode_refuses_dir_with_sqlite",
"ok": true,
"detail": "在线模式只允许显式单库 backup API(--db);--file /tmp/multica-task-3939683741/hel879-snapshot-selftest-a4xt_3p_/instance/history_root"
},
{
"name": "fix2_stop_mode_embeds_sqlite_via_consistency_path",
"ok": true,
"detail": "embedded=['/tmp/multica-task-3939683741/hel879-snapshot-selftest-a4xt_3p_/instance/history_root/mem0_public_history.db']"
},
{
"name": "fix3_verify_maps_source_paths_and_passes",
"ok": true,
"detail": "ok=True commit=7c73d392b95f3d55091a066db0ffe9bc6c475e0d plan_entries=4"
},
{
"name": "backup_api_snapshot_complete",
"ok": true,
"detail": "{\"companion\": 1, \"chat_message\": 1, \"events\": 1}"
},
{
"name": "fix_rows_match_true_cannot_bypass_source_counts",
"ok": true,
"detail": "ok=False counts={'companion': 1, 'chat_message': 1, 'events': 1} source={'companion': 1, 'chat_message': 1, 'events': 2}"
},
{
"name": "fix_same_basename_no_overwrite_and_duplicate_refused",
"ok": true,
"detail": "snap_names=['ledger.db', 'ledger__0e75f128.db'] verify_ok=True restored_rows=['first', 'second'] dup_refused=yes"
},
{
"name": "restore_drill_paths_persist",
"ok": true,
"detail": "restore_root=/tmp/multica-task-3939683741/hel879-snapshot-selftest-a4xt_3p_/same-restore"
}
]
}
+886
View File
@@ -0,0 +1,886 @@
{
"head": "7c73d392b95f3d55091a066db0ffe9bc6c475e0d",
"scope": "只读审计:3.10 固定栈安装元数据按 3.11 Linux markers+extras 求闭包,对照产品 3.11 venv 与本机 uv/pip 缓存;这是依赖闭包清单,不是已完成的 cp311 解析或下载结果",
"read_only": true,
"zero_downloads_this_card": true,
"closure_total": 55,
"missing_count": 41,
"missing_pure_python": [
"backoff",
"coloredlogs",
"distro",
"fastembed",
"filelock",
"flatbuffers",
"fsspec",
"h2",
"hpack",
"httpcore2",
"httpx2",
"huggingface-hub",
"humanfriendly",
"hyperframe",
"loguru",
"mem0ai",
"mpmath",
"openai",
"portalocker",
"posthog",
"pytz",
"qdrant-client",
"requests",
"sniffio",
"sympy",
"tqdm",
"truststore",
"urllib3"
],
"missing_native": [
"charset-normalizer",
"greenlet",
"grpcio",
"hf-xet",
"jiter",
"mmh3",
"numpy",
"onnxruntime",
"pillow",
"protobuf",
"py-rust-stemmers",
"sqlalchemy",
"tokenizers"
],
"cache_reusable_now": [
"greenlet",
"pillow",
"truststore"
],
"top_level": {
"mem0ai": {
"name": "mem0ai",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "fixed-source"
},
"qdrant-client": {
"name": "qdrant-client",
"version": "1.20.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
"fastembed": {
"name": "fastembed",
"version": "0.9.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
}
},
"missing_detail": [
{
"name": "backoff",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "charset-normalizer",
"version": "3.5.2",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "coloredlogs",
"version": "15.0.1",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "distro",
"version": "1.9.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "fastembed",
"version": "0.9.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "filelock",
"version": "4.1.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "flatbuffers",
"version": "25.12.19",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "fsspec",
"version": "2026.9.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "greenlet",
"version": "3.5.6",
"wheel_tags": [
"cp310-cp310-manylinux_2_24_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/greenlet/3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"name": "3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/greenlet/3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"name": "3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"3.5.6-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64"
],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "cache-reusable"
},
{
"name": "grpcio",
"version": "1.84.1",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "h2",
"version": "4.4.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "hf-xet",
"version": "1.7.0",
"wheel_tags": [
"cp38-abi3-manylinux_2_17_x86_64",
"cp38-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "hpack",
"version": "4.2.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "httpcore2",
"version": "2.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "httpx2",
"version": "2.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "huggingface-hub",
"version": "1.33.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "humanfriendly",
"version": "10.0",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "hyperframe",
"version": "6.1.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "jiter",
"version": "0.18.0",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "loguru",
"version": "0.7.3",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "mem0ai",
"version": "2.2.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "fixed-source"
},
{
"name": "mmh3",
"version": "5.3.1",
"wheel_tags": [
"cp310-cp310-manylinux_2_5_x86_64",
"cp310-cp310-manylinux1_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "mpmath",
"version": "1.3.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "numpy",
"version": "2.2.6",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "onnxruntime",
"version": "1.23.2",
"wheel_tags": [
"cp310-cp310-manylinux_2_27_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "openai",
"version": "3.37.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "pillow",
"version": "12.3.0",
"wheel_tags": [
"cp310-cp310-manylinux_2_27_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/pillow/12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/pillow/12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/pillow/12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"name": "12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64"
],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "cache-reusable"
},
{
"name": "portalocker",
"version": "4.4.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "posthog",
"version": "7.75.11",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "protobuf",
"version": "6.33.6",
"wheel_tags": [
"cp39-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "py-rust-stemmers",
"version": "0.1.8",
"wheel_tags": [
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "pytz",
"version": "2026.5",
"wheel_tags": [
"py2-none-any",
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "qdrant-client",
"version": "1.20.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "requests",
"version": "2.34.2",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "sniffio",
"version": "1.3.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "sqlalchemy",
"version": "2.0.55",
"wheel_tags": [
"cp310-cp310-manylinux_2_17_x86_64",
"cp310-cp310-manylinux2014_x86_64",
"cp310-cp310-manylinux_2_28_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "sympy",
"version": "1.14.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "tokenizers",
"version": "0.23.3",
"wheel_tags": [
"cp310-abi3-manylinux_2_17_x86_64",
"cp310-abi3-manylinux2014_x86_64"
],
"native_extension": true,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "3.11 目标需 cp311-cp311(或 cp39+ abi3)manylinux x86_64 原生 wheel;3.10 安装标签不可复制,禁混 site-packages",
"classification": "missing-download-at-release"
},
{
"name": "tqdm",
"version": "4.70.1",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
},
{
"name": "truststore",
"version": "0.10.5",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [
{
"kind": "uv:wheels-v7",
"path": "/home/xiaomo/.cache/uv/wheels-v7/pypi/truststore/0.10.5-py3-none-any",
"name": "0.10.5-py3-none-any",
"sha256": null
},
{
"kind": "uv:wheels-v6",
"path": "/home/xiaomo/.cache/uv/wheels-v6/pypi/truststore/0.10.5-py3-none-any",
"name": "0.10.5-py3-none-any",
"sha256": null
}
],
"cache_hits_usable_for_311": [
"0.10.5-py3-none-any",
"0.10.5-py3-none-any"
],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "cache-reusable"
},
{
"name": "urllib3",
"version": "2.8.0",
"wheel_tags": [
"py3-none-any"
],
"native_extension": false,
"installed_sdk": true,
"present_product_311": false,
"product_version": null,
"direct_url": null,
"cache_hits": [],
"cache_hits_usable_for_311": [],
"resolution": "纯 Python py3-none-any:同版本 wheel 可跨 3.10/3.11 使用",
"classification": "missing-download-at-release"
}
],
"mem0_identity": {
"direct_url_of_installed": "{\"dir_info\": {}, \"url\": \"file:///home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src\"}",
"fixed_source_dir": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-src",
"fixed_source_dir_exists": true,
"pinned_tarball": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/mem0-2c81af3.tar.gz",
"pinned_tarball_sha256_measured": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"pinned_tarball_sha256_lock": "9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23",
"tarball_matches_lock": true,
"index_wheel_2_2_1_proves_same_code": false,
"note": "索引下载的 mem0ai==2.2.1 wheel 不自动等于固定源码;发布必须用本 tarball/源码目录离线构建安装",
"built_wheel_from_hel857_install": {
"filename": "mem0ai-2.2.1-py3-none-any.whl",
"size": 352586,
"sha256": "698284b6083ab8b7761454d65876a6d1bb983d278ad21a7bee38d760cb20e9db",
"ephemeral_wheel_cache": "/tmp/multica-task-1816511421 (不再存在)",
"persisted_anywhere": false
},
"offline_build_backend": {
"hatchling_cached_or_local": true,
"note": "离线构建 mem0 wheel 需要 hatchling 及其依赖可离线获得;未命中则发布卡需一并拉取"
}
},
"fastembed_models": {
"cache_dir": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-857-2cf6dc7dfc87/workdir/hel857-data/fastembed-cache",
"exists": true,
"models": {
"models--Qdrant--bge-small-zh-v1.5": {
"files": 13,
"bytes": 190444780
},
"models--Qdrant--bm25": {
"files": 39,
"bytes": 56247
}
},
"default_cache_dir": "/home/xiaomo/.cache/fastembed",
"default_cache_exists": false,
"release_note": "真实 mem0 模式把 fastembed cache_dir 指向发布实例数据目录内的该缓存拷贝;本次零下载、不推理"
},
"pip_2622_event": {
"claim_under_review": "HEL-895 seq48–51:uv pip install --offline pip 失败后去掉 --offline 安装 pip 26.2.2 成功;前卡日志仅存尾两行",
"installed_into": "/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-895-0261e950b9c2/workdir/CyberGril/.venv311/lib/python3.11/site-packages/pip-26.2.2.dist-info",
"installer": "uv",
"uv_cache_entry_created": "2026-10-27T02:10:04.960958",
"uv_http_cache_created": "2026-10-27T02:10:04.972959",
"recorded_source_url": "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl",
"recorded_sha256": "fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70",
"other_new_cache_entries_in_window": [],
"verdict": "勘误成立:uv 缓存中 pip-26.2.2 wheel 条目创建于 2026-10-27 02:10:04 +0800(HEL-895 运行窗口内),来源 files.pythonhosted.org,且是该窗口唯一新增缓存条目——该步发生了网络获取,前卡『零下载』对此步不成立;requirements.lock 装机同期未产生任何新缓存条目(全部命中既有缓存)。字节级『完整下载 vs 条件请求』无法由缓存取证区分,按保守口径记为下载。"
}
}
@@ -0,0 +1,16 @@
# HEL-879 rework: real-SDK check regenerated from THIS head's voice sources
# head: 78a5cfd8d75e3cfd00c60dc5c3592eb08cc757a1
# run: 2026-10-26T20:16:05Z, .12 /opt/voice-agent/agent/venv/bin/python (livekit-agents 1.8.8), stdin-only, zero writes zero network, no service touched
# generator: scripts/gen_hel877_sdk_check.py (10 checks; #9/#10 new: worker callback path on real SpeechHandle/SpeechCreatedEvent; assistant_interim seam through real LLMStream)
HEL-877/879 real-SDK check on livekit-agents 1.8.8: ALL PASS
OK CyberGrillLLM/ChatApiLLMStream subclass real livekit-agents 1.8.8 LLM/LLMStream
OK latest_user_text reads a real ChatContext (items/role/text_content)
OK fake-SSE turn through real LLMStream emits ChatChunks, final text and receipt
OK chat.error phi is spoken verbatim via ChatChunk and carries no receipt
OK mint_token claims byte-parity with livekit.api.AccessToken (video/iss/sub/keys)
OK mint_server_token matches SDK no-grant auth header claims
OK worker.py SDK surface (SpeechHandle, UserInputTranscribedEvent.is_final) present
OK barge-in task cancel closes the in-flight HTTP stream immediately
OK worker callback path on real SpeechHandle/SpeechCreatedEvent: no-generation handle discards, next turn keeps its own stream (explicit binding)
OK assistant_interim seam: SSE deltas through real LLMStream feed her incremental subtitle payloads (replace semantics, reset works)
+9
View File
@@ -0,0 +1,9 @@
# HEL-896 · 固定 SDK 离线余缺审计摘要(只读)
- 闭包 55 项,产品 3.11 venv 缺 41 项:纯 Python 28 项、原生扩展 13 项(charset-normalizer, greenlet, grpcio, hf-xet, jiter, mmh3, numpy, onnxruntime, pillow, protobuf, py-rust-stemmers, sqlalchemy, tokenizers)。
- 三个顶层包 WHEEL 均 py3-none-any(纯 Python,可跨 3.10/3.11)。
- mem0ai 固定源码身份:tarball SHA256 一致(9dcef9303ebcec116eec6739f01b8b35bd24459de6a569686b4d19b9412dda23);索引同名 wheel 不作数;hel857 构建过的 wheel(698284b6083a…)在临时缓存、未持久。
- 当前缓存可直接复用的缺项 wheel:['greenlet', 'pillow', 'truststore']。
- 其余缺项(纯 Python wheel 与 cp311 原生 wheel)由发布卡按 deploy/hel874-release.md §4 获授权后下载并 sha256 留证;缺项不阻塞其余发布工作。
- fastembed 模型缓存实测:{"models--Qdrant--bge-small-zh-v1.5": {"files": 13, "bytes": 190444780}, "models--Qdrant--bm25": {"files": 39, "bytes": 56247}}(hel857-data/fastembed-cache,随发布实例落位,零下载)。
- pip 26.2.2 事件:勘误成立:uv 缓存中 pip-26.2.2 wheel 条目创建于 2026-10-27 02:10:04 +0800(HEL-895 运行窗口内),来源 files.pythonhosted.org,且是该窗口唯一新增缓存条目——该步发生了网络获取,前卡『零下载』对此步不成立;requirements.lock 装机同期未产生任何新缓存条目(全部命中既有缓存)。字节级『完整下载 vs 条件请求』无法由缓存取证区分,按保守口径记为下载。
@@ -0,0 +1,16 @@
/opt/voice-agent/agent/venv/lib/python3.12/site-packages/jwt/api_jwt.py:149: InsecureKeyLengthWarning: The HMAC key is 6 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.
return self._jws.encode(
/opt/voice-agent/agent/venv/lib/python3.12/site-packages/jwt/api_jwt.py:370: InsecureKeyLengthWarning: The HMAC key is 6 bytes long, which is below the minimum recommended length of 32 bytes for SHA256. See RFC 7518 Section 3.2.
decoded = self.decode_complete(
HEL-877/879 real-SDK check on livekit-agents 1.8.8: ALL PASS
OK CyberGrillLLM/ChatApiLLMStream subclass real livekit-agents 1.8.8 LLM/LLMStream
OK latest_user_text reads a real ChatContext (items/role/text_content)
OK fake-SSE turn through real LLMStream emits ChatChunks, final text and receipt
OK chat.error phi is spoken verbatim via ChatChunk and carries no receipt
OK mint_token claims byte-parity with livekit.api.AccessToken (video/iss/sub/keys)
OK mint_server_token matches SDK no-grant auth header claims
OK worker.py SDK surface (SpeechHandle, UserInputTranscribedEvent.is_final) present
OK barge-in task cancel closes the in-flight HTTP stream immediately
OK installed-SDK source verified: speech tasks set agents_speech_handle around the pipeline; speech_created precedes the task; llm_node calls llm.chat inside it
OK ACTUAL worker module loaded; its registered callbacks driven on a real AgentSession: both-queued delayed generations bind exactly (manager interleaving), say never steals a stream, unattributed fails closed, interrupt cancels without final/receipt
OK assistant_interim seam: real LLMStream deltas feed per-speech_id v2 payloads (replace semantics); unattributed streams feed nothing
@@ -0,0 +1,15 @@
HEL-877/879 real-SDK check on livekit-agents 1.8.8: ALL PASS
OK CyberGrillLLM/ChatApiLLMStream subclass real livekit-agents 1.8.8 LLM/LLMStream
OK latest_user_text reads a real ChatContext (items/role/text_content)
OK fake-SSE turn through real LLMStream emits ChatChunks, final text and receipt
OK chat.error phi is spoken verbatim via ChatChunk and carries no receipt
OK mint_token claims byte-parity with livekit.api.AccessToken (video/iss/sub/keys)
OK mint_server_token matches SDK no-grant auth header claims
OK worker.py SDK surface (SpeechHandle, UserInputTranscribedEvent.is_final) present
OK barge-in task cancel closes the in-flight HTTP stream immediately
OK installed-SDK source verified: speech tasks set agents_speech_handle around the pipeline; speech_created precedes the task; llm_node calls llm.chat inside it
OK ACTUAL worker module loaded; its registered callbacks driven on a real AgentSession: both-queued delayed generations bind exactly (manager interleaving), say never steals a stream, unattributed fails closed, interrupt cancels without final/receipt
OK assistant_interim seam: real LLMStream deltas feed per-speech_id v2 payloads (replace semantics); unattributed streams feed nothing
OK speak_proactive_row publishes proactive_start while the real SpeechHandle is still playing (sync-say shape), end/cancel on the handle's done callback
OK interrupted proactive row emits proactive_cancel(reason=interrupted), never proactive_end
OK incomplete/no-receipt attributed stream: actual worker callbacks emit turn_start -> turn_cancel only, no assistant_final, no ack
@@ -0,0 +1,9 @@
{"v": 2, "seq": 1, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_cut"}
{"v": 2, "seq": 2, "kind": "assistant_interim", "role": "assistant", "text": "我正想跟你说,今天下午", "speech_id": "speech_cut"}
{"v": 2, "seq": 3, "kind": "turn_cancel", "role": "assistant", "text": "", "speech_id": "speech_cut", "reason": "interrupted"}
{"v": 2, "seq": 4, "kind": "interim", "role": "user", "text": "等一下"}
{"v": 2, "seq": 5, "kind": "user_final", "role": "user", "text": "等一下,先听我说。"}
{"v": 2, "seq": 6, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_next"}
{"v": 2, "seq": 7, "kind": "assistant_interim", "role": "assistant", "text": "好,你先说。", "speech_id": "speech_next"}
{"v": 2, "seq": 8, "kind": "assistant_final", "role": "assistant", "text": "好,你先说。", "speech_id": "speech_next", "turn_id": "0e2f-demo-next"}
{"v": 2, "seq": 9, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_next", "turn_id": "0e2f-demo-next"}
@@ -0,0 +1,7 @@
{"v": 2, "seq": 1, "kind": "interim", "role": "user", "text": "你在听的"}
{"v": 2, "seq": 2, "kind": "user_final", "role": "user", "text": "你在听吗?"}
{"v": 2, "seq": 3, "kind": "turn_start", "role": "assistant", "text": "", "speech_id": "speech_normal"}
{"v": 2, "seq": 4, "kind": "assistant_interim", "role": "assistant", "text": "在听呢,", "speech_id": "speech_normal"}
{"v": 2, "seq": 5, "kind": "assistant_interim", "role": "assistant", "text": "在听呢,在听呢,你刚才说到一半的那件事我还在想。", "speech_id": "speech_normal"}
{"v": 2, "seq": 6, "kind": "assistant_final", "role": "assistant", "text": "在听呢,你刚才说到一半的那件事我还在想。", "speech_id": "speech_normal", "turn_id": "0e2f-demo-turn"}
{"v": 2, "seq": 7, "kind": "turn_end", "role": "assistant", "text": "", "speech_id": "speech_normal", "turn_id": "0e2f-demo-turn"}
@@ -0,0 +1,4 @@
{"v": 2, "seq": 1, "kind": "proactive_start", "role": "assistant", "text": "我看到一句很适合现在的话,先念给你听。", "speech_id": "speech_pro1", "message_id": 17}
{"v": 2, "seq": 2, "kind": "proactive_end", "role": "assistant", "text": "", "speech_id": "speech_pro1", "message_id": 17}
{"v": 2, "seq": 3, "kind": "proactive_start", "role": "assistant", "text": "还有一件事想告诉你——", "speech_id": "speech_pro2", "message_id": 18}
{"v": 2, "seq": 4, "kind": "proactive_cancel", "role": "assistant", "text": "", "speech_id": "speech_pro2", "message_id": 18, "reason": "interrupted"}
@@ -0,0 +1 @@
{"remote_delete_count_after_ttl": 1, "expired_but_registered": false, "cleanup_owner_present": false}
@@ -0,0 +1 @@
{"remote_delete_count_after_ttl": 0, "expired_but_registered": true, "cleanup_owner_present": false}
@@ -0,0 +1 @@
{"proactive_before_playout_finishes": {"handle_done": false, "events": ["proactive_start"]}, "proactive_after_playout_finishes": ["proactive_start", "proactive_end"], "proactive_payload_fields": {"v": 2, "kind": "proactive_start", "role": "assistant", "text": "offline sample", "speech_id": "speech_f28fdeb15e01", "message_id": 7}, "no_receipt_incomplete_actual_worker_events": ["turn_start", "turn_cancel"], "no_receipt_incomplete_confirmed": [], "complete_with_receipt_actual_worker_events": ["turn_start", "assistant_final", "turn_end"], "complete_with_receipt_confirmed": ["ok-turn"]}
@@ -0,0 +1 @@
{"proactive_before_playout_finishes": {"handle_done": false, "events": []}, "proactive_after_playout_finishes": ["proactive_start", "proactive_end"], "proactive_payload_fields": {"v": 2, "kind": "proactive_start", "role": "assistant", "text": "offline sample", "speech_id": "speech_2880d36ea013", "message_id": 7}, "no_receipt_incomplete_actual_worker_events": ["turn_start", "assistant_final", "turn_end"], "no_receipt_incomplete_confirmed": [], "complete_with_receipt_actual_worker_events": ["turn_start", "assistant_final", "turn_end"], "complete_with_receipt_confirmed": ["ok-turn"]}
+37
View File
@@ -0,0 +1,37 @@
# HEL-879 契约核准后 3 处实现时机收口(2026-10-27)
核对对象:`hel-879-assembly @ 3228b1e0d7f822b7030f945e285eac74126771aa`(总管《HEL-879-契约核准与接续.md》)。
本轮只修 3 处实现与已定契约 v2 不一致的问题:不改 `v` 信封字段、不改事件种类、不动 `web/src`、不重做已过绑定/备份/预算。
## 修前复现(全部先复现失败,与总管证据一致)
| # | 发现 | 修前证据 |
|---|------|----------|
| 1 | `open_session` 只在 `await create_dispatch` 前 kick;建房真实让出事件循环后 cleaner 空转退出,注册后无人再观察截止时间 | `delayed-dispatch-before.json`:`{"remote_delete_count_after_ttl": 0, "expired_but_registered": true, "cleanup_owner_present": false}`(与总管 delayed-dispatch.json 逐字段一致) |
| 2 | `speak_proactive_row` 旧写法 `handle = await session.say(...)`;真实 SDK `say` 为同步函数、`await handle`=等整段播完 → 播完才发 proactive_start | `sdk-probe-before.json`:`proactive_before_playout_finishes.events=[]`,播完后才 `["proactive_start","proactive_end"]` |
| 3 | `_after_playout` 先按 `stream.text` 发 final/end 再查回执;注入 text 非空、completed=False、receipt=None 的确切归属流 → `turn_start→assistant_final→turn_end`,与 v2「无完整同轮回执只 cancel」相反 | 同上:`no_receipt_incomplete_actual_worker_events=["turn_start","assistant_final","turn_end"]`(实际 worker `bind_voice_session` 回调路径,真实 AgentSession/SpeechHandle) |
新增专项测试在**未修复代码上先跑失败**(repro-first):
`tests/test_voice_rework_hel879.py::test_open_session_with_real_dispatch_yield_re_kicks_cleanup`、
`::test_second_registration_wakes_a_sleeping_cleaner_for_earlier_deadline` → 修前 2 failed / 修后 2 passed。
## 修复(精确小修)
1. `src/cybergril/voice/session.py` `open_session`:注册成功并 `_persist_state()` 之后**重新 `_kick_cleanup()`**——建房 await 期间旧 cleaner 可能已因「无可观察对象」退出,新截止时间需要新的所有者;自动截止、共享上限、重启补偿、有界放弃全部保留。测试用**真正让出事件循环**的建房替身(`asyncio.sleep(0.02)`),不再只用立即返回的 MockTransport 替身。
2. `src/cybergril/voice/worker.py` `speak_proactive_row`:先取 handle(`iscoroutine` 只解包 await 型替身——真实 SpeechHandle 自身可 await,不能误等)、**立即**发布 `proactive_start`、注册该 handle 的 `proactive_end/proactive_cancel`,再按需等待这一条播完(逐条顺序念保留)。
3. `src/cybergril/voice/worker.py` `_after_playout`:`assistant_final` 前置必要门——流 `completed` 且 `should_ack`(合法同轮 receipt、非旧 epoch)才 `finish`+`confirm`;未完整/无回执/旧 epoch 一律只 `turn_cancel`(reason=incomplete/no-receipt),**绝不先发 final 再决定不确认**;φ/打断/失败/无归属 fail-closed 原样保留,完整+合法回执路径的 final 与 ack 均保留。
## 修后验证
- 本地:`delayed-dispatch-after.json`:`{"remote_delete_count_after_ttl": 1, "expired_but_registered": false, "cleanup_owner_present": false}`(cleaner 在全部结清后按设计自行退出,无悬空协程)。
- `.12` 真 SDK(stdin 嵌入当前源码,零写入零网络,`/opt/voice-agent/agent/venv` livekit-agents 1.8.8)`sdk-probe-after.json`:
- `proactive_before_playout_finishes={"handle_done": false, "events": ["proactive_start"]}`(播放中即可见 start);
- 播完 `["proactive_start","proactive_end"]`,字段 `v/kind/role/text/speech_id/message_id` 与 v2 完全一致;
- `no_receipt_incomplete=["turn_start","turn_cancel"]`、`confirmed=[]`;
- 完整+合法回执路径 `["turn_start","assistant_final","turn_end"]` + ack 保留。
- 真SDK回归并入常驻检查 `scripts/gen_hel877_sdk_check.py`(**14/14 ALL PASS**,`../sdk-check-hel879-timing.log`):新增「主动播报时机」「主动播报打断只有 cancel」「无回执不 final」三负例,均驱动实际 worker 模块注册的真实回调。
- 产品套件:pytest **352/352**(3 次连跑);ruff(src/tests/tools) **0 错**;`verify_hel877_voice_chain.py` **8/8**;需求检查 **4/4**(origin/main 与 c121c42 各带/不带 handoff-HEL-879.json)。字幕样例 `subtitle-samples/` 与 v2 契约文档无需再生成(字段与生命周期未变)。
## 边界
零真实 LLM/嵌入/ASR/TTS/麦克风;.12 仅 SSH stdin 执行只读检查(零写入零网络,stderr 仅既有 jwt 弱密钥告警);零下载零安装;未部署、未合并;PR#11 与 `hel-870-assembly` 未动;权威账/冻结/共享服务/队员配置未碰;真实 cap 仍 0。R5 真机、完整 CG-011、R6/R7 继续承接,无需求变更(v1.11/60 项)。
@@ -0,0 +1,60 @@
# HEL-879 · 统一候选一次复审收口(总工,2026-10-27)
> 范围:按总管 2026-10-27 交接,把 HEL-898 三项返修的隔离保全候选
> (`origin/hel-898-preserved` @ `18aef2a`:9254c17 = 作者 12 文件逐字节保全,
> 父 b0dcb17;18aef2a = 仅记录/证据)**正常快进纳入** `hel-879-assembly`,
> 对这三项做最后一次复审收口。不开新审核串、不重审已过后台/预算/备份/receipt
> 时机内核、不替原作者改视觉。零真实调用、零下载/安装/部署/合并,真实 cap 仍 0。
## 一、整合事实
- 快进前远端:`hel-879-assembly` @ `b0dcb17`(11fd0e2/9756db3/b0dcb17 材料成果
全保留,未退回 f92a86f)。快进后 head **`18aef2a`**,无冲突、无强推;
PR#11 与 `hel-870-assembly`(c121c42)未动。
- 9254c17 恰为 12 文件(`web/src` 6 + `web/tests` 5 + `handoff-HEL-898.json`),
与总管 preservation-review.md 记录的作者交付逐字节保全一致;18aef2a 仅新增
`docs/rework/hel898/f92a86f-rework/` 记录与证据(31 文件)。
- **b0dcb17→18aef2a 对 `src/`、`tests/`、`tools/`、`deploy/` 零 diff**
(实跑 `git diff --stat` 为空)——后端/协议/预算/备份/receipt 时机零改动成立。
## 二、三项返修复审(源码级 + 测试级,均通过)
1. **agent 离场僵尸界面/麦克风不停**(`web/src/voice/livekit.ts` +
`session.ts`):transport 按 identity 记录已见 agent(连接事件与初始
remoteParticipants 扫描都入 Set);`ParticipantDisconnected` 仅当离场者是
该 agent 且房内再无其它 agent 时触发 `agentLeft`;控制器走**既有** `fail()`
路径(mic 先停 → room 关闭 → 服务器 DELETE,离线欠账照常补发)。普通参与者
离场、旧房间迟到事件、自己挂断、重复第二 worker 均不误触发——
`web/tests/hel898_agent_left.test.ts` 7 例(含 DELETE 欠账重发与 stale 负例)。
2. **closed 64 挤出复活**(`session.ts`):退休 speech 集合由 64 上限环形数组
改为**每通话 `Set<string>` 全程保留、随代次整体释放**;seq 高水位门不变,
未以放宽乱序保护换修复。f92a86f 审核所记 DOCUMENTED GAP 就此关闭。
3. **播放中短回复被压到 final**(`reading.ts` + `lineStage.ts` +
`ChatPanel.tsx` + `voiceLine`):语音打开的她的行改 `live: 'speaking'`,
末块只显示**已成句**部分(`said()` 截到句末标点+引号括号),文字 generating
原持有规则不变;同键行 `grow()` 原位增长(保留 DOM/已演字,非续写才整段
置换),签名纳入语音行文本驱动重呈现;阅读时钟对增长行只增不减时长。
落库接替沿用同键(turn:mi:index),不重播、不重复演字。
## 三、独立检查(针对本次真实变更,均实跑)
- `web` vitest **118/118**(10 文件,含 hel898_agent_left 7 例、hel879_frames
10 例、voice 45 例);typecheck 0 错;lint 0 错/1 既有 fullButton ref 警告;
build 通过(CSS 哈希 `index-BpZaybfD.css` 与既认可视觉一致,JS 因本次修复
变更为 `index-C23OdPgW.js`,属预期)。
- 需求结构检查 3/3 通过:`--base-ref origin/main`、`--base-ref b0dcb17`、
`--base-ref b0dcb17 --handoff docs/requirements/handoff-HEL-898.json`;
本轮结束后再以 origin/main ± 本卡 handoff 复跑(见提交说明)。
- 浏览器统一回放(两主题电脑/手机、减少动态、worker 实际录制帧、8 段 WebM)
**不重复复跑**:总管已在 18aef2a 记录 53/53 与媒体证据
(manager-checks/browser.log),本轮零安装策略下不重开。
## 四、结论与边界
- 三项返修**离线通过**,与后台 v2 契约无冲突;统一候选当前准确 head
`18aef2a`(本轮复审与 D1 试听材料另追加提交,见分支历史)。
- 仍不冒充的部分:以上全部是假 LiveKit/假媒体的离线回放与单测;真实
WebRTC/麦克风/ASR/TTS、三轮/1 秒打断停声停麦、可信 HTTPS+WSS 设备路径、
iOS/Android、API 重启真实释放**均未测**,待主卡批准后 R5 真机窗口执行。
完整 CG-011 会话中主动表达、完整 mem0 统一体验/R6/R7 继续承接。
- v1.11/60 项无变更;冻结账/权威账未写;既有五实例与共享服务零接触。
@@ -0,0 +1,85 @@
{
"sdk": {
"livekit_agents": "1.8.8",
"livekit_api": "1.2.1"
},
"source": "actual fc905fe+fb7ea84 worker driven on .12 real SDK (livekit-agents 1.8.8, livekit-api 1.2.1) via SSH stdin, zero network/writes",
"scenarios": {
"normal": {
"kinds": [
"interim",
"user_final",
"turn_start",
"assistant_interim",
"assistant_interim",
"assistant_interim",
"assistant_final",
"turn_end"
],
"frames": 8,
"acks": [
"382c41d9-0d72-433e-b370-f838fa5d0bd0"
],
"turn_id": "382c41d9-0d72-433e-b370-f838fa5d0bd0"
},
"interrupt": {
"kinds": [
"user_final",
"turn_start",
"assistant_interim",
"assistant_interim",
"turn_cancel",
"user_final",
"turn_start",
"assistant_interim",
"assistant_interim",
"assistant_final",
"turn_end"
],
"frames": 11,
"acks": [
"edbcb837-f19f-47e7-8d5c-057010da43eb"
],
"turn_ids": {
"cut": "09d05c0f-3058-4ff8-a56e-4e94bd17d7b7",
"next": "edbcb837-f19f-47e7-8d5c-057010da43eb"
}
},
"phi": {
"kinds": [
"user_final",
"turn_start",
"assistant_interim",
"assistant_interim",
"assistant_interim",
"error",
"turn_cancel"
],
"frames": 7,
"acks": [],
"turn_id": "31e70e52-1471-4e50-ac49-cce89979a9bf"
},
"no_receipt": {
"kinds": [
"user_final",
"turn_start",
"assistant_interim",
"assistant_interim",
"turn_cancel"
],
"frames": 5,
"acks": [],
"turn_id": "36d160e3-e365-46ff-ad57-c2878c00720f"
},
"proactive": {
"kinds": [
"proactive_start",
"proactive_end",
"proactive_start",
"proactive_cancel"
],
"frames": 4,
"start_while_playing": true
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
需求结构检查通过:60项;来源和覆盖完整。仍须人工核对授权语义与实际体验。
@@ -0,0 +1 @@
需求结构检查通过:60项;来源和覆盖完整。仍须人工核对授权语义与实际体验。
@@ -0,0 +1 @@
需求结构检查通过:60项;来源和覆盖完整。仍须人工核对授权语义与实际体验。
@@ -0,0 +1 @@
需求结构检查通过:60项;来源和覆盖完整。仍须人工核对授权语义与实际体验。

Some files were not shown because too many files have changed in this diff Show More