| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat: add local harness safety guards | 3 个月前 | |
feat(dev-harness): opt-in make lane-backend (fast wheel install) for the mobile harness and pre-push gates (#14349) * fix(app): refuse remote APIs and non-dev pairings in hermetic tests app/test.sh skipped writing .dev.env when generated files already existed, so a leftover API_BASE_URL or mobile_beta/prod pairing could silently survive. Fail closed at the checker, test.sh, and doctor without rewriting the file. Failure-Class: new Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dev-harness): add lane-bootstrap that skips the full backend lock A linked worktree can run cheap pre-push gates and app/test.sh after one idempotent command: pinned Flutter, shared PUB_CACHE, Python 3.11 via uv, and yaml+dotenv only. An incomplete .venv directory is no longer treated as ready. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): keep fixture venvs passing the cheap-gate import probe Manifest-contract stubs only accepted `import yaml`. The resolver now probes `import dotenv, yaml` so an empty .venv is not treated as ready, and those stubs were skipped. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dev-harness): make setup-backend the fast wheel install, not pylock sync uv pip sync of pylock.macos.toml lists hashed sdist+wheel for av/llvmlite/scipy/pyarrow and stalled ~20 minutes; uv pip install -r requirements.txt takes index wheels (69s here) and covers uvicorn/pyright/yaml/dotenv/google.auth for session start and the pre-push typecheck. Lock-faithful sync stays in sync-python-deps.sh. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): keep setup-backend as the lock sync; add opt-in lane-backend make setup-backend is the contributor lock-synced entrypoint; pointing it at unlocked requirements.txt broke scripts/test-make-setup.sh. The fast wheel install is make lane-backend. generate-app-env runs build_runner without the flags that deleted tracked manifest.g.dart. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(backend): keep AGENTS.md under the lean budget while naming lane-backend Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
harden(backend,app): client-compatibility replay contract (C10) (#14372) * harden(dev-harness): define V1 live-session and V8 spine contracts Add strict Python/Dart pending contracts and introducing-commit protection. Reserve the live-session CLI, wire, ownership/attachment seams and failing builder acceptance tests. Add live attribution before freezing evidence v1. Verification: - bash scripts/dev-harness/run-tests.sh: 287 passed, 7 existing skips, 40 strict xfailed (24 pending V1 markers), Python 3.11.15. - make preflight: 16 checks passed, using supported PYTHON selection. - make mobile-verify fast --paths harness/session contract inputs with an absolute evidence directory: 20 executed, 20 passed, no failures/skips. - bash app/test.sh: 2014 passed, 5 skipped, one unchanged UTC+7 search_rank_grouping_test failure; affected baseline inputs match main. - Real module CLI refusals exit 2 without traceback; regression covered. - Dart SDK probe proves ext. prefix required; marker-removal formatter probe proves no incidental Dart diff. Runtime is intentionally unimplemented. App UI must repair current invalid VM extension names before live acceptance; unsupported journeys block. Simulator/emulator latency and detached-process acceptance remain V1 work. make setup installed hooks but full backend wheel sync stalled; isolated Python 3.11 harness dependencies ran the owner checks without any skip flag. Failure-Class: none * harden(dev-harness): scope the spine-contracts check to spine paths The check protects files under the spine roots and their registry, so it only needs to run when those change. A repo-wide trigger would run it, and its full-history requirement, on every unrelated contributor PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(dev-harness): clarify the live journey adapter boundary V1 must refuse live journey execution until a shared executable adapter exists; B1 addressability alone cannot attach Dart integration tests. Pure admission contracts remain valid with injected supported IDs. Verification: harness 287 passed, 7 skipped, 40 strict xfailed; make preflight passed 20 checks. * harden(dev-harness): pin reviewed spine revisions and align B0 capabilities Coordinator turn-4 review authorizes the V1 fixture correction. Exact before/after digests pin the revised oracle; immutable revision records do not permit builder assertion edits. Active tests reject missing/tampered records and prove the fake machine response. Verification: full harness 289 passed, 7 skipped, 40 strict xfailed; UTC app 2020 passed; spine 24 pending/7 protected; make preflight 16 passed. No live device acceptance claimed. * harden(dev-harness): preserve marker retirement across spine revisions Track retained marker occurrences rather than counts so retiring a second test cannot restore the first test's retired marker. The active regression covers this swap and legitimate continued retirement. Verification: full harness 290 passed, 7 skipped, 40 strict xfailed; spine 24 pending/7 protected; make preflight 16 passed. Dart sources unchanged after the earlier UTC app 2020-pass run. * harden(dev-harness): retain corrected spine oracles through squash merges A squash can introduce a corrected contract and its review records in the same commit. Absorb only the revision prefix ending at that introducing file's exact digest, preserving immutable records and later marker-only edits. Active temp-repo regression covers multiple corrections in one squash and rejects restoring old or altered assertions. Verification: harness 291 passed, 7 skipped, 40 strict xfailed; B1 make preflight 20 passed; final B1 journeys 20/20. Dart unchanged after UTC app 41 opt-in and 2051 ordinary passed. * test(dev-harness): revise live session acceptance after builder review * test(dev-harness): bind machine fixture to actual child identity * test(contracts): define released mobile client compatibility replay C10 pins adopted consumer shapes and release-source provenance, reserves frozen-decoder replay, and leaves capture plus real backend acceptance visibly pending. Build tags are not release evidence. Verified app 2064, harness 309 passed/7 skipped/61 xfailed, spine 41 markers/11 protected, preflight 31 checks, journeys 3/3. Backend E2E stopped at missing fake_firestore prerequisite; no setup or live contact. * harden(dev-harness): scope compatibility replay to supported captures Keep the two-build bootstrap oracle historical while ongoing replay checks exact current router responses for supported cases. Archived captures remain immutable; validated server retirement is explicitly out of scope. Permit append-only coverage bundles for an existing build. Verified: UTC app suite 2064 passed (Dart unchanged); full harness 310 passed, 7 skipped, 61 strict xfailed; spine 41 pending, 11 protected; make preflight 31 passed; fast journeys 3/3. Three historical backend commits pass the adoption-scoped check. Backend E2E remains blocked at its dependency prerequisite; no real-router execution claimed. * harden(dev-harness): separate oracle revisions from implementations Use the actual PR base and immutable exact scaffolding snapshots to reject mixed revision/implementation diffs. Exercise a real squash and child merge. Correct local_dev wire spelling from the simulator RPC and add adversarial V1 conformance oracles after reviewing the first broker. Verification: UTC app 2064 passed; full harness 303 passed, 7 skipped, 55 strict xfailed; spine 37 pending/9 protected; preflight 16 checks passed. Isolated reviewed-broker probes failed 9/9 as documented in the conformance report. No real app/device launch. * fix(ci): repair the checks-manifest merge of main into the client-compat contract branch The coordinator's conflict resolution left a truncated duplicate spine-contracts entry; the pre-push manifest validation caught it. Keep one entry, with this branch's extra trigger path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * harden(dev-harness): retain pinned corrections after older parent merges * harden(dev-harness): pin oracle content across shared runners and merge orders Keep exact oracle digests and immutable revision records. Traverse full reachable history and match revision payloads by digest. Shared runner metadata requires direct fail-fast suite invocation instead of pinning shared file bytes. Both new regressions fail on the old checker. UTC app 2064 passed; final harness 306 passed, 7 skipped, 59 strict xfailed; spine 39 pending/10 protected; preflight 16 passed. No protected oracle bytes or legacy scope pins changed. * harden(dev-harness): separate replay engine from evidenced release admission Bind distribution receipts to captured identities, pin handwritten decoder sources and exact candidate requests, require original-Flutter extraction equivalence, and execute a real Dart decoder oracle. Preserve unadopted endpoint freedom and report missing OpenAPI reference closure. Verification: app2064; harness326 passed/7 skipped/78 strict xfailed; spine52/15; preflight31 passed. No real release receipts or backend replay claimed. * harden(dev-harness): inventory both literal pending marker quote styles Single-quoted Python contracts executed under pytest but were invisible to inventory and could not retire their markers. Recognize paired quote styles in both languages; reject nonliteral builder markers while allowing active MECHANISM-owned API self-tests. Preserve all oracle bytes and digest pins. Verification: harness328 passed/7 skipped/79 strict xfailed; spine55 pending/16 protected; preflight31 passed; unchanged Dart suite2064 passed. * test(dev-harness): require replay loader to preserve frozen request vectors Compare every admitted Case to its pinned method, ordered query/headers, body, status, decoder and observations. A HEAD-shaped request with the right endpoint name cannot pass. Real capture admission remains pending after synthetic engine delivery. Verification: harness328 passed/7 skipped/79 strict xfailed; spine55/16; preflight31 passed; Dart2064 passed. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 6 天前 | |
free tier: the on-device summary is produced, selected, rendered, and actually reachable (#14513) * harness(dev): light the free-tier local-processing path the harness exists to exercise The free tier replaces managed summarization with an on-device projection. `FREE_TIER_LOCAL_PROCESSING` ships default-off, so the local harness has always processed every conversation normally and the on-device path was unreachable on the one machine built to run it end to end. Set it in `_harness_service_extra`, alongside `SCREEN_FRAME_EGRESS_ENABLED`, which is here for the same reason and carries the same comment. The flag alone lights nobody: `utils.free_tier_cohort` makes a lit flag mean "lit for the configured cohort" and admits no one when the cohort is unset, empty, or malformed. So the cohort is set too. `pct:100` is safe precisely here — the harness runs against the Firebase emulator (project `demo-omi-local`) with one local account per instance, so "100%" is every account that can exist on it, and none of them belongs to a real user. Both halves are asserted in `test_child_env_for_real_mode`, because setting only the boolean would leave the harness silently processing normally — the exact failure this change exists to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(desktop): select Apple Foundation Models as the default local engine when available Ordinary free-tier Macs do not run a loopback server. Resolve AFM availability once at makeDefault and keep local-server as the developer fallback. A failed selected engine still fail-closes to the deterministic minimum. Co-authored-by: Cursor <cursoragent@cursor.com> * desktop: render the on-device summary the client already uploads The free tier replaces cloud summarization with an on-device projection. The produce side was complete: summarize locally, store, upload to `client_processing`. Nothing rendered it, so a free-tier user saw the deterministic minimum forever and the whole feature was invisible. The backend is deliberate about this. The bind endpoint "updates only `client_processing`. Never touches `structured`", and `ConversationProcessingState`'s docstring makes rendering a client contract: clients check `client_processing` first and render the projection regardless of `processing_state`. `ServerConversation.init` decoded the field and dropped it. Every display consumer - the list, the automation bridge, the reconciliation policy, the GRDB mirror - reads `ServerConversation.structured`, so selection happens once at the decode seam and every surface is correct without touching any view. What the seam had to get right, beyond the mapping: - Staleness. A projection names the transcript it summarized. When the response includes segments, the digest is recomputed and a mismatch is refused. List responses omit segments, so they rely on the server's binding and record `transcriptVerified: false` rather than pretending it was checked. - Unknown schema versions, in two layers. A future envelope is stripped before decoding so it cannot make the conversation unreadable, and `resolve` renders v1 only. - Precedence. `processing_state` cannot establish it - it is absent for both enriched and projected rows. The projection is selected only when the canonical structure is the deterministic minimum, which `build_deterministic_minimum_structured` defines as empty overview, category `other`, no sections, action items, or events. Cloud enrichment outranks a retained projection. Known limit: a title-only cloud result with otherwise default fields is indistinguishable from the minimum here. - Action items merge on exact description with canonical winning, including when `completed` is false, because OR-ing completion would resurrect a task the user had reopened. - Display attribution persists in its own `localSummaryJson` column. `clientProcessingJson` stays the producer-owned immutable retry blob; mixing them would corrupt retries. Drafted with codex gpt-6-astra, then reviewed, verified, and finished here. The precedence guard was checked field by field against the backend builder rather than taken on report. 18 tests, 0 failures: xcrun swift test --package-path Desktop --filter 'ConversationProjection(Rendering|Persistence)Tests' Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(backend): the free-tier gate must pass the uid, or it lights nobody `process_conversation` gated the entire S6 free-tier branch on `free_tier_local_processing_enabled()` with no argument, two lines above `resolve_free_tier_processing_plan(uid=uid, ...)` - so `uid` was in scope the whole time. That function answers False whenever it is given no uid, on purpose: `utils.free_tier_cohort` makes a lit flag mean "lit for the configured cohort", and a caller that cannot name the account is never admitted, so a boolean alone lights nobody. Called bare, it always returned False. So the free-tier branch was unreachable in every environment, whatever cohort was configured. Lighting FREE_TIER_LOCAL_PROCESSING plus a 100% cohort changed nothing: desktop conversations fell through to the legacy deferred path, and the on-device projection the client had already uploaded was never the basis for processing. The unit suites cannot catch this - they monkeypatch `free_tier_local_processing_enabled` itself, which its docstring says is the supported way to test the policy. It took an end-to-end run with the flag genuinely lit on a real bundle. The backend said so plainly once it was: free-tier cohort: FREE_TIER_LOCAL_PROCESSING consulted without a uid; admitting nobody at that call site until it passes one After the fix, the same capture produces: free-tier: stored projected conversation uid=alice conv=21377fc9... mode=store_projection reason=basic_not_entitled with canonical `structured` left at the deterministic minimum by policy rather than by a failed cloud call, and `client_processing.provenance.model_id=afm`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(backend): the rollout-gate double must be asked about an account Every test in this suite replaces `free_tier_local_processing_enabled` with a zero-argument lambda, so none of them could see how the coordinator calls it. That is exactly how the missing uid survived: the real function answers False for a missing uid by design, the branch was unreachable in every environment, and the suite stayed green because a zero-argument double only ever typechecks against the broken call. The doubles now mirror the real signature and assert they were given a uid, so the shape of the call is checked rather than assumed. One new test pins the argument itself, since replacing the function is what blinds the others. Red-proof: reverting `free_tier_local_processing_enabled(uid)` back to a bare call fails `test_rollout_gate_is_asked_about_this_account_not_asked_bare` with a TypeError at the call site. With the fix, 40 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(backend): the memory-suppression gate must pass the uid too `extract_memories(uid, conversation)` called `free_tier_memory_suppression_enabled()` bare, using `uid` on the very next line. Same contract and same consequence as the S6 gate: the cohort admits nobody when it is not told which account it is deciding about, so the S5 branch was unreachable however the cohort was configured. The sweep and the connectors already passed `uid`; this site was the exception. The registry described both sites as not yet passing a uid. That is no longer true, so the registry says so. This does not turn S5 on. Both flags stay unset, admitting nobody, and lighting memory suppression is a separate decision that needs its own replacement story first - suppressing server-formed memories without an on-device equivalent is takeaway before replacement. 55 passed across the free-tier branch and memory-policy suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * desktop: the free-tier client gate defaults on in Beta The flag read env-or-UserDefaults and defaulted off, and nothing in the app ever wrote it. A shipped Beta build therefore had the client half dark with no way to light it short of a rebuild. That is not a missing convenience, it is the takeaway-before-replacement case. Beta serves through the development plane while keeping production identity. Lighting FREE_TIER_LOCAL_PROCESSING there with the client dark means an identified-basic user's conversation arrives carrying no projection; the policy resolves `store_projection if has_projection else deterministic_minimum`, the minimum is terminal, and the cloud summary is gone with nothing in its place. The two halves have to be lit together, so Beta is where the client default belongs. Only the absence of both an environment value and a stored default falls through to the channel default, so this stays a kill switch in both directions: - `object(forKey:)` rather than `bool(forKey:)`, because the latter cannot tell a stored `false` from a key that was never set - without the distinction Beta could not be turned off without a rebuild. - A blank environment value is "unset", not "off", so an exported-but-empty variable cannot silently dark the channel. Stable and every other bundle are unchanged and still default off. 13 tests, 0 failures: xcrun swift test --package-path Desktop --filter LocalProjectionFinalizationTests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * registry: name the class where a cohort gate is consulted without its subject A rollout gate that decides per subject and is asked without one admits nobody by design, so the branch behind it is unreachable in every environment while the flag and cohort still read as configured. The prevention has two halves: pass the subject at every call site, and make the test doubles carry the real signature, because a zero-argument stub stays green across exactly this defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * e2e: cover the projection rendering seam in the recording-finalization flow The flow already covers every source that produces and uploads the on-device projection. The seam that selects it at decode is the read side of the same feature, and uncovered it is exactly the gap this flow's later steps would observe as a placeholder title on a free-tier account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(desktop): read fixture objects without a force cast SwiftLint bans force casts and the fixture mutators used seven. A silent `?? [:]` would be worse than what it replaces: a mistyped key would mutate nothing, the fixture would decode unchanged, and the assertion would pass for the wrong reason. The helpers fail the test at the caller instead. 18 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
fix(dev-harness): keep local desktop auth-scoped calls off production The local dev harness desktop profile set OMI_PYTHON_API_URL and OMI_DESKTOP_API_URL but not OMI_AUTH_API_URL, so the macOS app's DesktopBackendEnvironment.authBaseURL() fell back to the production API for auth-scoped calls (GET /v2/desktop/prompts, GET /v1/csat/config). A local named bundle sent its Firebase Auth emulator token to production, got 401, and AuthSessionCoordinator treated the post-refresh 401 as a dead session (invalidateSession reason=postRefreshHTTP401), signing every local desktop profile out about a second after signing in. Point OMI_AUTH_API_URL at the local python backend, same as OMI_PYTHON_API_URL, so auth-scoped calls stay on the local backend. Verified locally: with the fix the bundle stays signed in and those calls return 200 from the local backend; scripts/dev-harness/tests/test_desktop_profile.py passes (5 tests). Co-Authored-By: ZCode (GLM-5.3) on behalf of Claude Code session | 1 天前 | |
SCA-486: isolated mobile sessions, local-dev auth, capture replay, and seeded journeys (#14213) * mobile sessions: freeze session-evidence-v1 receipt contract Freeze the minimal versioned session/evidence schema shared by the mobile development-foundation consumers (C2 journeys, C3 capture replay, C4 verification, C5 devices) and its executable validator. - contracts/session/session-evidence-v1.schema.json: closed v1 object binding source SHA + dirty digest, built artifact identity, loopback-only endpoints, fixture/runner versions, real timestamps, status/blocked reason and exact execution counts. No credential-shaped field exists anywhere. - dev_harness/session_evidence.py: builder + validator enforcing the cross-field semantics a JSON Schema cannot express: ready/running require an artifact whose git_sha matches the source (a stale build cannot be reported ready), production-family profiles are rejected as session targets, counts must account exactly, zero-execution receipts are only valid pre-run, and credential-shaped keys are refused at any depth. - Egress guards: validate_local_http_url/validate_local_host_port reject non-loopback or non-plain-HTTP endpoints (api.omi.me / api.omiapi.com by name) before any request is attempted. Evidence: scripts/dev-harness/run-tests.sh (test_session_evidence.py, 20 contract tests incl. stale-artifact, egress, credential and accounting rejections). * mobile sessions: deterministic synthetic auth fixture v1 Seed a synthetic Auth-emulator user through the local-dev custom-token endpoint contract from open PR #11784 (feat/app+backend local-development sign-in without OAuth) — reuse, not a competing endpoint. The PR stays with its owner; scripts/dev-harness/MOBILE_SESSIONS.md records the integration plan and provenance. - fixtures/mobile/v1.json: one deterministic user (omi-fixture-v1-user-1@local.test); RFC-reserved domain so fixture identities can never collide with a real account. No real Google/Apple user, provider key, or copied token involved. - dev_harness/mobile_fixtures.py: fail-closed seeding client — the backend URL is validated as loopback plain-HTTP before any request, production hosts are denied by name, and the persisted receipt records identity and outcome only: token_minted/token_retained, never the token itself. Evidence: test_mobile_fixtures.py (16 tests) — determinism, reserved-domain enforcement, pre-request egress refusal, 404/unreachable/wrong-uid fail-closed paths, credential-free receipts. * mobile sessions: structured doctor for the session lanes Every readiness failure classifies exactly one of ready / agent-remediable (with the exact resumption command) / operator-action-needed (privileged install, license, host capacity), per lane (backend, android, ios). - Flutter version is read from the mobile CI pin in .github/workflows/mobile-app-checks.yml — never 'latest'; inconsistent pins refuse rather than guess. - Backend lane: python3.11 (venv must be 3.11, ambient 3.14 must not select the runtime), JDK 21 for the firebase emulators, firebase-tools, redis/typesense via native binary or a responding docker daemon. - Android lane: ANDROID_HOME + adb + emulator engine + system image, each with the exact sdkmanager remedy and a capacity-gated download note. - iOS lane: Xcode + simctl runtime; missing runtime is an operator action. - Capacity: <12GiB free on the shared Data/scratch container is an operator gate for emulator/build lanes (agents never free space themselves); contract/unit lanes skip it via --skip-capacity. - Egress: an ambient production OMI_LOCAL_API_BASE_URL override is reported as a blocking misconfiguration. Evidence: test_mobile_doctor.py (13 tests) over an injected runner — lane filtering, ready/degraded/blocked classification, pin parsing, capacity and operator-gate behavior; live run on m1-mac-studio via 'make mobile-session ARGS="doctor --platform android --platform ios"' reports backend+ios ready, android emulator engine agent-remediable. * mobile sessions: isolated session lifecycle CLI behind one entrypoint 'make mobile-session ARGS="…"' (scripts/dev-harness/mobile-session.sh) owns a uniquely-leased local mobile session: doctor / acquire / start / seed / reset / status / evidence / stop / recover / release. A session is an existing dev-harness instance + port offset + device lease + seed receipt + evidence receipt — the harness lifecycle is reused in-process under OMI_LOCAL_INSTANCE/OMI_HARNESS_PORT_OFFSET, not duplicated. Ownership is fail-closed: - leases are created atomically (O_EXCL) with owner host/user/pid and a harness-standard sentinel; a live foreign owner or another local user's session is never touched; cross-host takeover is an operator decision; recover bumps the generation for same-host/same-user takeovers. - ports come from a claimed offset registry; a foreign process occupying a port is refused (never killed) and the allocator skips that offset; release frees the claim only when it belongs to the session. - start gates device attach on doctor readiness (precise blocked reason, not a crash); ios-simulator devices are created/booted/deleted session-owned via simctl. - seed/reset/stop/release are idempotent; reset only touches the session's own harness instance (sentinel-validated underneath). - evidence emits session-evidence-v1 receipts; ready/running refuse without a bound artifact and refuse when the source moved since acquire. app/setup.sh (separate commit): OMI_IOS_DEVICE_ID pins non-interactive device selection; OMI_DEVICE_SUFFIX overrides hostname identity. Evidence: test_mobile_session.py (19 tests) + wrapper tests — exclusivity, disjoint ports, dead-owner/live-foreign/different-user/cross-host refusals, foreign-port refusal with a real live listener, idempotent release, artifact binding, stale-source refusal, harness env handoff. Live CLI run: acquire/list/evidence/seed-fail-closed/stop/release with exit codes 0/2 on m1-mac-studio. * app/setup.sh: non-interactive device pin and per-session device suffix - OMI_IOS_DEVICE_ID: when set, select_ios_device uses exactly that device id, failing precisely (with the available device list) when absent, instead of enumerating and prompting — the mobile-session harness, CI and nested agents cannot answer an interactive prompt, and the current no-TTY path errors out whenever more than one iOS destination exists. - OMI_DEVICE_SUFFIX: let a session harness (or a second checkout on one host) inject a unique device-identity suffix instead of the hostname, which collides across concurrent sessions on the same machine. Unset behavior is unchanged. Verified by sourcing the function with a stubbed flutter devices --machine: pinned-present emits the id; pinned-absent fails with the list; unpinned multi-device no-TTY keeps the existing enumeration failure. * mobile sessions: apply repo python formatter to the new modules black 26.5.1, --line-length 120 --skip-string-normalization via scripts/backend-python-format; behavior unchanged, dev-harness lane re-run green (214 passed; 1 pre-existing environmental failure — the host's global git worktree guard blocks pytest-tmp linked worktrees). * test: place linked-worktree pytest fixtures under OMI_WORKTREES The managed git wrapper correctly refuses worktrees in /private/tmp. Keep that guard and put the fixture where task worktrees are allowed. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: treat session-evidence-v1 as proposed until consumers review it C1 shipped the schema; freeze it only after C2/C3/C4 agree, not from a single worker declaration. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: reuse PR 11784 local-dev custom-token auth on current main Copy the reviewed emulator-gated sign-in path onto this integration branch so synthetic seed talks to real local services. Leave the original PR open and unmerged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: boot isolated sessions on real CoreSimulator IDs and offline STT Use the installed iPhone 17 Pro / iOS 26.5 identifiers, pin PROVIDER_MODE=offline, and drop soniox from the offline STT chain so the local backend can start without a paid key. Co-authored-by: Cursor <cursoragent@cursor.com> * test: isolate provider-secret fixtures from ambient PROVIDER_MODE A previous offline session left PROVIDER_MODE in the shell and made the secret-injection tests read ambient offline instead of the fixture file. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): injectable capture seams for deterministic recovery replay CaptureController and the phone WAL resolved clock, timers, connectivity, auth, mic, socket and upload policy through global singletons, so the capture -> WAL -> recovery path could not be replayed deterministically. Add narrow constructor seams (capture_seams.dart) with production-identical defaults: CaptureScheduling, CaptureAuthBoundary, CaptureConnectivityBoundary, plus wal/phoneMic/clock/scheduler injection on CaptureController; clock/periodic/job-status injection on LocalWalSyncImpl threaded through WalSyncs/WalService; periodic-timer injection on the NativeMicRecorderService watchdogs. The in-progress-conversation loader seam now covers the socket-connect path too, and streamRecording honors the microphone permission requester like the batch path already did. No behavior change with default construction; every seam is optional. Evidence: bash app/test.sh (1990 passed, 5 pre-existing skips); analyze ratchet green. * test(app): deterministic capture-recovery replay schedules (SCA-489/C3) Replay the REAL production capture pipeline (CaptureController, NativeMicRecorderService, TranscriptSegmentSocketService, WalService, RecordingTransferCoordinator) against controlled external I/O: virtual clock, manual bounded scheduler, scripted transport/upload boundary, fake native host. Restart evidence destroys and reconstructs the object graph from real temp files (torn wals.json -> backup recovery, missing audio -> terminal corruption, process kill -> disk reload and re-upload). Six schedules with invariant oracles: network loss/reconnect mid-capture (exact frame identity in the stored WAL, single upload), stale native events after stop/new session (session-identity gate, no double teardown), interruption/resumption (live + batch, bounded stall escalation), partial/torn persistence plus reconstruction, failed upload with bounded backoff and persisted/enqueued/server-acknowledged distinctions, and ownership transition (signed-out reconnect cancellation, bounded 4001 token refresh). Also publishes the C2/C4 adapter (capture_scenario.dart: catalog + result contract) and the C5 native-event vector schema (phone-mic-native-events/v1) mirroring the Pigeon PhoneMicFlutterApi contract without touching Pigeon. Falsification evidence: removing the NativeMicRecorderService session gate flips the stale-idle schedule to failure (record->stop); removing the finalizeCurrentSession unsynced-retention guard drops the WAL and fails the network-loss schedule. Evidence: flutter test test/unit/capture_recovery_replay_scenarios_test.dart (17 passed); bash app/test.sh full suite green. * chore(app): allowlist SCA-489 replay contract libs in the dead-code ratchet The scenario catalog/result contract and the native-event vector schema are library-only by design until the C2/C4 and C5 lanes import them; the ratchet demands an explicit allowlist entry with a reason for exactly this case. * test: pin conversation-window capture session id across sequential phone-mic lives activeCaptureSessionId is WAL/conversation-scoped so a late ConversationEvent can still stamp WALs. C2 must use activeRecordingId as the live recording identity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: wait for unawaited capture-upload retries before asserting and teardown CI failed the bounded-backoff replay because cooldown wakes are unawaited and settle used wall-clock sleeps that missed the drain under load, then deleted the temp WAL dir mid-write. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): typed debug semantic controls for the local journey lane Extends the existing debug Marionette surface (same debug VM-service transport, no new server/framework) with product-semantic controls: versioned capabilities (semantic-controls/v1), privacy-safe state (route, principal, capture lifecycle with activeRecordingId as the authoritative recording identity), bounded wait_ready, production-path navigation, and named journey faults. Fail-closed eligibility: kDebugMode AND local_dev profile AND OMI_DEV_CONTROLS=1 dart-define. Ineligible builds (including production-flavor debug) install nothing and the HTTP fault chokepoint is a pure pass-through — pinned by semantic_controls_guard_test.dart. Narrow seams added for the hermetic journey lane: - AuthService.installLocalHarnessTokenGateway (debug+local_dev gated Firebase token I/O boundary; isSignedIn routes through the gateway) - PlatformManager.initializeForLocalHarness (header fields only) - CrashlyticsManager report paths tolerate a missing Firebase app the same way main.dart's zone handler already does, so host-lane errors surface instead of being masked by [core/no-app] Verified: flutter test test/unit/semantic_controls_guard_test.dart (10 passed); auth regression suites (34 passed); C3 capture replay (17 passed); dead-code ratchet at baseline. * test(app): five strict seeded acceptance journeys with negative fault variants Canonical executable definitions (one per behavior) under app/integration_test/journeys/, runnable hermetically (flutter-tester + loopback fixture backend) or on a simulator via run_journeys.sh: j1 seeded conversation detail — real provider fetch + real detail page, exact synthetic identity; negative: wrong-owner session refused. j2 chat send -> distinct assistant reply — real input/send-button keys (omi.chat.input / omi.chat.send), request observed server-side, server-minted ai-role reply distinct from the prompt, rendered; negatives: suppress-send, suppress-assistant-reply, wrong-owner-session. j3 memory create/edit surviving reload — production provider path, server-minted id required after reload; negative: drop-memory-save. j4 expired session — transient failure re-mints via the real custom-token endpoint; terminal failure emits expiry and blocks requests; negative: production-family profiles never silently re-mint. j5 capture interruption/reconnect — C3 capture-scenario/v1 adapter: real temp files, process reconstruction, drain exactly once; negative: fail-capture-recovery. Each negative arms exactly one named fault and must fail with the invariant named. Evidence receipts follow session-evidence-v1 accounting; zero-execution runs never pass. Verified: bash integration_test/journeys/run_journeys.sh (5/5 pass); repeated deterministic vertical: bash integration_test/journeys/run_journeys.sh --filter j2 --runs 5. * fix: stamp journey evidence finished_at at write time Receipts were recording construction time as the end timestamp, so duration could not be distinguished from start. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): clear new analyzer-ratchet regressions in the integrated journeys The integrated checkpoint (b217eb1a9d) fails app/scripts/analyze_ratchet.sh with 7 new occurrences: 3 unused imports plus a bogus 'show WalStatus' in j5, an unused-looking nested import that actually provides SingleChildWidget in hermetic_boot, a missing const in j4, and two depend_on_referenced_packages for test-only platform interfaces. Declares path_provider_platform_interface and nested as direct dev dependencies (same pattern as the existing web_socket_channel dev deps) and removes the dead imports. Mechanical lint repairs only — j4/j5 hermetic journeys re-run green after the change. * feat: unified mobile verify lanes, mechanical journey selection, and CI/contributor path (SCA-490/C4) One canonical verification entrypoint over the proven lanes: make mobile-verify select|doctor|fast|smoke|physical (scripts/dev-harness/mobile-verify.sh -> dev_harness.mobile_verify). It never adds a second runner: journeys delegate to the C2 canonical runner and session infrastructure to the C1 session CLI. Selection is mechanical and fail-closed: journeys are glob-discovered (runner --list contract test), changed paths map through a per-seam rule table, unknown app/lib impact falls back to the full suite, and an empty selection is drift (exit 65), never PASS(0) — run_journeys.sh now fails closed the same way. Receipts are validated against session-evidence-v1 accounting (honest counts, zero-execution never passes) and every lane writes a verify-receipt.json binding source SHA + dirty digest, runner versions, outcomes, and the exact rerun command. smoke is fail-closed (exit 2 + remedy, never CI), physical is a separately reported admission lane. CI runs the same command in a new journeys-hermetic job in the existing mobile-app-checks.yml when has_app_journeys fires (journey definitions and support, C3 replay world, dev controls, non-generated app/lib Dart, evidence contract, or this entrypoint) — synthetic fixtures only, fork-safe, receipts uploaded on pass and failure. Selection is resolved by the shared pre_push_ci_prediction.py and deliberately stays out of the bounded pre-push gate. Docs reconciled around the real command: app README, app AGENTS (within the lean budget), and the e2e SKILL now point here instead of diverging on setup/auth. * chore(app): stop tracking Flutter's iOS ephemeral tree app/ios/Flutter/ephemeral/** is regenerated by flutter on every pub get and self-describes as 'Generated file. Do not edit.' It was committed by accident in a formatting sweep (dec329a84a) and has been stale ever since: the tracked SwiftPM Package.swift lists pods (in_app_review, pasteboard) that no pub dependency provides, so any flutter run rewrites it, dirties every worktree, and fails the diff-hygiene push gate on regenerated trailing whitespace. Untrack the four files and ignore the tree, mirroring the existing **/macos/Flutter/ephemeral/ rules. Xcode resolves the local package after flutter regenerates it during setup; nothing consumes a committed copy. * feat: native lifecycle seams, vector replay, and leased device qualification (SCA-491/C5) - PhoneMicController (iOS + Android) now consumes narrow, injectable environment/ports seams: event sink, engine, permission, session config, interruption source, batch pipeline, main loop. Production behavior is unchanged; all live wiring lives in PhoneMicHostApiImpl.swift (iOS) and PhoneMicControllerPorts.production (Android). - Canonical phone-mic-native-events/v1 vector fixtures (8 schedules incl. session adoption) shared by Dart guard, iOS ruby harness, Android JVM harness; Pigeon contract types extracted at iOS test time (drift-guarded). - iOS: ios/test/phone_mic_lifecycle_replay_test.rb replays all vectors through the production controller+emitter with fakes for OS I/O only. - Android: PhoneMicLifecycleReplayTest (JVM, virtual main loop + manual audio queue) replays the same vectors through the production controller. - device_lease.py: exclusive physical-device leases with qualification registry (personal-device refusal), bounded acquisition, live-lease never-stolen, stale-owner recovery with generation bump, safe release. - device_runner.py + 'mobile-session device' CLI: readiness doctor with exact operator steps, and a runner consuming C1 session manifests (install/adb-reverse/untethered launch/permission cycle/device-run evidence v1). All hermetically tested with fake devices (25 tests). - PHYSICAL_DEVICES.md: m1-mac-studio read-only inventory, operator runbook, and the external physical-test handoff template. Physical acceptance stays pending user-run evidence by design. * docs: point mobile-verify physical at the C5 device handoff C4's physical lane stays fail-closed (exit 2). After C5 landed, the admission document should name the real runner and PHYSICAL_DEVICES.md instead of implying the software path is still missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: expect four Flutter pins in mobile-app-checks C4 added journeys-hermetic as a fourth Flutter job on the same repository toolchain pin. The workflow-contract count of 3 was stale. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: raise Desktop Swift PR-lane suite budget to 3000s Run 35134593036 measured 2778s against 2700s on a cache-hit PR lane. The overrun was one 1500s batch ceiling plus isolation, not a slow desktop suite; this mobile PR has no desktop sources. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 7 天前 | |
feat(mobile): add iPhone capture probes and offline replay (#15956) * feat(mobile): add offline iPhone capture replay and diagnostic harnesses * fix(dev-harness): mark Apple certificate fingerprint as non-security * fix(dev-harness): use platform certificate fingerprint tooling * fix(mobile): preserve startup ordering tripwire | 1 天前 | |
feat(dev-harness): pin D7 fixture-audio contract without injecting yet (#14391) A later phone-mic journey must hear the LibriSpeech release-probe WAV and name platform_mic vs in_app_fake; this pins that receipt so -no-audio and a capture-source fake cannot pose as a microphone. Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
feat: add JIT knowledge ledger foundation and guarded adoption (#12084) * feat: add JIT knowledge ledger foundation * chore: refresh integration OpenAPI contract * fix: make trigger evaluation release-safe Failure-Class: none * fix: preserve lifecycle semantics in ledger apply Failure-Class: none * feat: adopt guarded JIT knowledge surfaces Route the agent preference writer through the intent-backed ledger, register a privacy-filtered entity timeline tool, render optional evidence on Windows, and add a base-ref-protected Gate F legacy-surface ratchet. Failure-Class: none * feat: add progressive JIT knowledge reads Register owner-scoped current-ledger search and explicit playbook hydration, with pre-limit semantic filtering and bounded outputs. Add a content-free planner/resume migration fixture without claiming canonical transaction completion.\n\nValidation: 141 focused backend tests passed; backend typecheck reported 0 errors; repository preflight passed 120 checks. * feat: render chat evidence on web Render bounded, fail-soft conversation evidence after authoritative answers in both web chat entry points. Unsupported, future, duplicate, and raw failure details remain inert.\n\nValidation: 337 web tests passed; web typecheck, oxlint, and Prettier passed; repository preflight passed 120 checks. * feat: require intent-backed ledger search results Apply the intent-backed requirement at the final merged canonical/history filter, with a passive historical-row regression case.\n\nValidation: 54 focused backend tests passed. * test: keep agent tool isolation stubs current * feat: gate JIT conversation retrieval * fix: make entity timeline scans deterministic Failure-Class: none * fix: honor rejected ledger projections Failure-Class: none * feat: render inert screen evidence on web * fix: reuse canonical review projection Failure-Class: none * fix(web): await recap context effect Failure-Class: none * test: amortize preference tool isolation load Failure-Class: none * feat(app): add knowledge ledger review surface Failure-Class: none * feat(macos): use canonical ledger prompt projection Failure-Class: none * test(memory): classify legacy surface inventory roles Failure-Class: none * fix(app): preserve ledger history completeness state Failure-Class: none * feat(macos): preserve canonical ledger mirror metadata Failure-Class: none * fix(app): match canonical ledger ordering Failure-Class: none * test(api): prove ledger client schema parity Failure-Class: none * feat(memory): expose bounded ledger history Failure-Class: none * chore(api): generate ledger history clients Failure-Class: none * feat(retrieval): add bounded card participants Failure-Class: none * feat(macos): project ledger trigger watchlist Failure-Class: none * fix(clients): fail closed on ledger authority Failure-Class: none * feat(macos): expose bounded trigger snapshot Failure-Class: none * fix(memory): keep closed history read only Failure-Class: none * feat(app): disclose partial ledger history Failure-Class: none * test(macos): cover ledger trigger bridge Failure-Class: none * fix(app): use neutral ledger accents Failure-Class: none * chore(api): declare ledger history route policy Failure-Class: none * test(macos): remove unsafe JSON fixture unwraps Failure-Class: none * fix(memory): satisfy typed history boundary Failure-Class: none * fix(macos): require prompt snapshot authority Failure-Class: none * test(memory): prove ledger migration on emulator Failure-Class: none * feat(retrieval): emit bounded screen evidence Failure-Class: none * test(memory): classify maintenance retirement readiness Failure-Class: none * feat(macos): adapt Rewind metadata for triggers Failure-Class: none * test(retrieval): align screen timestamp contract Failure-Class: none * feat(memory): correct ledger facts by amendment Failure-Class: none * test(memory): prove ledger correction on emulator Failure-Class: none * feat(macos): harden local trigger observations Failure-Class: none * feat(agent): search bounded historical facts Failure-Class: none * test(macos): cover trigger observation adapter * fix(memory): gate historical fact retrieval * feat(memory): add gated JIT retrieval strategy * test(memory): prove mixed-version JIT runtime parity * chore(memory): keep JIT gate exports type-safe * refactor(memory): isolate JIT prompt contract * fix(conversations): round-trip owner-scoped references Accept the conversation:<id> references emitted by JIT result cards while retaining strict UUID-only bare IDs and share links. Restrict machine IDs to a bounded safe alphabet so evidence suffixes and path-like values fail closed. Failure-Class: none * test(memory): join JIT citations to evidence envelope * fix(retrieval): enforce JIT conversation search budget Cap JIT summary searches per request and bound database hydration to the projection limit before reads. Preserve the legacy path when JIT is disabled. Failure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep JIT retrieval request scoped * test(macos): prove future JIT evidence stays inert * fix(memory): keep JIT card citations request-global Failure-Class: new * fix(retrieval): separate JIT hydration from search Treat gated owner-scoped references as exact hydration without searching transcript text for the reference. Charge every JIT candidate search to the shared four-search request budget, including snippet-bearing requests, while keeping exact hydration free and preserving released JIT-off UUID/share-link behavior.\n\nVerified:\n- cd backend && ./.venv/bin/python -m pytest tests/unit/test_conversation_jit_processing.py tests/unit/test_conversation_exact_reference_search.py -q (58 passed)\n- cd backend && uvx --from pyright==1.1.403 pyright -p pyrightconfig.json --pythonpath .venv/bin/python (0 errors)\n- git diff --check\n\nFailure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep repeated JIT cards index-safe * fix(retrieval): satisfy JIT card type contract * fix(retrieval): hydrate collected JIT cards * test(app): preserve answers during delayed evidence requests * test(app): exercise production evidence composition * feat(memories): restore superseded ledger facts * fix(memories): reconcile reverted ledger facts * feat(memories): append reverted ledger facts * feat(memories): synchronize revert client contract * fix(memory): name ledger revert identity * fix(memories): type and enlarge revert controls * fix(memories): fence revert retries and refreshes * fix(memories): fence ledger revert authority * test(memory): count ledger revert rate limit * feat: expose agent-controlled historical facts * feat: reopen standalone ledger facts * feat: add fail-closed JIT QA bundle routing * feat: add safe local JIT QA backend stack * fix: harden isolated JIT QA stack * feat: add explicit multi-source entity timeline * feat(backend): add JIT rollout authority * feat(backend): fence every proactive paid boundary * fix(backend): release proactive quota on cancellation Release the reserved proactive quota exactly once when cancellation interrupts paid-boundary refresh or a provider retry, then re-raise cancellation without emitting retry telemetry. Add deterministic regression coverage for both cancellation points. Failure-Class: FC-proactive-quota-cancellation | new * fix(backend): make proactive quota cancellation safe Detach in-flight Redis reservations on request cancellation and release only admitted slots once they settle. Move direct-provider fallback telemetry behind the fresh paid-boundary rollout check so late kill or unknown decisions cannot report false recovery.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): preserve quota compensation during shutdown Keep late Redis reservation compensators outside the ordinary cancellable background-task drain. Desktop and main application shutdown paths now wait for these critical compensators before cancelling ordinary work, with deterministic blocked-thread and lifecycle-order regressions.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): use expiring proactive quota leases * fix(backend): make quota finalization clock-safe * fix(backend): isolate jit rollout control plane * fix(backend): close jit control plane safely * fix(backend): emit retry recovery after quota commit * test(backend): keep rollout app contract fast * feat(jit): add guarded proactivity and first-open policies * chore(desktop): mark jit policy as internal * test(desktop): cover jit proactivity policy flow * feat(backend): wire durable JIT first-open processing * feat(desktop): fence JIT proactivity runtime admission * feat: activate authoritative JIT proactivity runtime * fix: harden JIT proactivity authority * fix: close proactive runtime authority gaps * fix(jit): make first-open effects resumable * fix(jit): fence outstanding first-open work * fix(jit): resume app usage receipts * fix(jit): make app usage retries no-op Failure-Class: none * fix(jit): allow completed usage after app deletion Failure-Class: none * fix(jit): register first-open folder query Failure-Class: none * Fix first-open import isolation * feat(memory): govern ledger slots and prompt winners * feat(macos): stage guarded ledger prompt adoption * feat(jit): adopt authoritative ledger prompts on macOS * fix(jit): close ledger adoption authority leaks * fix(jit): reauthorize every ledger migration write * fix(jit): fence ledger cutover publication * fix: keep ledger prompt rollback reversible * feat(jit): add guarded frame request retention contracts * fix(jit): close frame retention authority and evidence lifecycle * fix(jit): make frame retention retries and cleanup durable * fix(jit): make frame evidence recovery and retention complete * fix(jit): close frame retention recovery gaps * Harden temporary frame retention and deployment * fix: harden JIT frame retention and consumption * fix: close JIT frame lifecycle recovery gaps * fix: unify JIT frame authority and retention Failure-Class: FC-split-mutation-authority * docs: keep frame retention guidance lean * fix: retire duplicate frame flag bindings Failure-Class: FC-split-mutation-authority * fix: register frame keyframe queries Failure-Class: FC-split-mutation-authority * fix: serialize frame retention deploys Failure-Class: FC-split-mutation-authority * test: cover frame pixel deletion ordering * style: format cumulative Dart changes * fix(app): retain permanent conversation photo fetches * fix: bound frame vision retention and authority * fix: drain terminal frame request metadata * chore: record internal ledger adoption change * feat(memory): add dark daily sweep authority * feat(memory): harden daily sweep fences and runtime seam * feat(memory): reconcile existing standing triggers in sweep adapter * fix(memory): harden daily sweep recovery and source fences * fix(memory): close daily sweep source producers * fix(memory): close daily sweep review findings * Add dark daily memory sweep authority and recovery * fix(memory): harden daily sweep rejection repairs * test(listen): stub onboarding admission in bootstrap regression The daily sweep PR fences onboarding mode behind the server-owned backend admission (get_backend_onboarding_admission), so the bootstrap regression test now simulates an admitted session instead of failing closed on a real Firestore read. Verification: focused test passes in 1.64s (previously failed after a 4m27s Firestore timeout); full test_listen_runtime_regressions.py + test_onboarding_question_start.py: 26 passed; black --check clean. * fix(memory): close daily sweep rollout and retry cursors * fix(memory): isolate daily sweep lifecycle and retry fairness * Harden daily sweep admission and completed-day staging * fix daily memory sweep reliability boundaries * preserve daily sweep invocation tombstones * close daily sweep invocation lifecycle fences * fix: keep daily sweep lifecycle cleanup active * fix: acquire ledger snapshot client off event loop * fix(memory): preserve migration tier fence without legacy growth * test(memory): prove legacy adjudication race fences * fix(dev): allow bounded ADC readiness refresh * test: keep ledger prepush deterministic * test(memory): register prompt receipt control path * fix(memory): fence ledger writer transitions * feat(backend): preserve closed ledger history in export * feat(memory): define ledger query semantics * fix(backend): fence trigger snapshots on final authority * fix(backend): bypass stale coalesced JIT refreshes * feat(macos): mirror bounded memory evidence Decode generated v3 evidence into a domain mirror, persist canonical bounded JSON through the memory cache, and preserve it across compatibility sync and older-local conflicts. Invalid, future-shaped, oversized, and over-count payloads fail closed without hiding memory text or granting prompt authority. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): fence and classify memory evidence Keep generated memory fields independent from malformed evidence, distinguish absent valid and invalid evidence states, preserve prior evidence on invalid payloads, and gate replacements on a monotonic server timestamp so stale active evidence cannot resurrect redacted rows. Cover populated-table migration upgrades. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): preserve evidence fences and scrub redactions Advance evidence revisions for identical valid payloads, fence stale active responses after a local edit, and remove artifact/device pointers from redacted evidence before canonical persistence. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * chore(macos): record ledger evidence mirror * feat(macos): deep-link local evidence cards to Rewind * fix(macos): fence Rewind frame evidence version * fix(macos): validate Rewind evidence card availability * fix(macos): bind task detail Rewind navigation to local leases * fix(macos): fence Rewind citation owner handoff * chore(macos): register Rewind evidence deep links * test(macos): cover Rewind evidence navigation * feat(desktop): evaluate JIT trigger watchlists locally * feat(desktop): wire authoritative JIT trigger runtime * feat(desktop): bind JIT claims to snapshot authority * fix(desktop): revalidate trigger authority at execution * fix(desktop): keep JIT execution leases live * test(memory): bind standalone reopen to direct-user writer * fix: make JIT QA sign-in self-contained Failure-Class: new Verification: bash desktop/macos/tests/test-jit-qa-target.sh; bash desktop/macos/tests/test-yolo-dev-backend.sh; repaired named-bundle Google sign-in reached authenticated onboarding. * feat(memory): complete JIT policy and native Windows parity * docs(backend): keep service map within context budget * test(macos): cover JIT client and staging flows * chore(backend): declare JIT mirror route policy * fix(backend): use strict Firestore boundary for JIT admission Failure-Class: FC-malformed-doc-read * chore(quality): register malformed-document guard surface * fix(backend): fail closed on malformed JIT authority Failure-Class: FC-malformed-doc-read * refactor(backend): name JIT workflow boundary results * test: repair JIT CI contracts * fix(backend): preserve ledger query exports Retain the explicit same-name re-exports consumed by tests and downstream callers while satisfying the enforced Pyright unused-import boundary after the main rebase. Failure-Class: none * test(backend): isolate gateway setup timing Failure-Class: none * style(memory): format direct-user evidence path Failure-Class: none * test(agent): isolate ACP process-group fallback Failure-Class: none * fix(dev-harness): preserve ownership markers in narrow CI * test(jit): refresh emulator fixtures for current contracts * test(jit): orchestrate local rollout dogfood * test(jit): harden local dogfood authority * fix(dev-harness): install PostHog for CI tests * fix(chat): project server JIT rollout into retrieval Resolve the backend-owned PostHog decision inside the bounded agent setup path and pass only its boolean result to prompt/tool configuration. Unknown or failed authority remains on the released legacy path, while callers cannot self-enroll through configurable input.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: new * fix(memory): preserve preference writer compatibility Select the agent preference write path from the canonical per-user writer control. Default compatibility mode retains the released MemoryService payload and receipt behavior; ledger mode keeps the retry-stable ledger write, and transition states fail closed.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: FC-split-mutation-authority * fix(jit): separate migration rollout authority Keep staged JIT chat and proactive exposure independent from legacy-row migration and writer cutover. Migration now requires its own default-off PostHog flag and still rechecks the shared kill switch at every mutation and publication boundary. Repair the isolated conversation-JIT fixture for main's chat-scope import. Verification: 217 focused JIT, chat-scope, migration, and lifecycle tests passed; 28 conversation-JIT fixture tests passed; independent Sol review accepted the split for QA-only dev rollout. Failure-Class: FC-split-mutation-authority * fix(photos): preserve retained image retrieval Treat an empty legacy inline marker as absent when permanent storage is authoritative, while malformed non-empty inline payloads still fail closed. Route live and retained thumbnails through the storage-aware image loader and preserve the conversation identity through the full-screen viewer.\n\nVerification: backend data-export tests 32 passed; Flutter photo-viewer tests 5 passed; focused Dart analysis clean; independent Sol review found and verified the viewer identity repair.\n\nFailure-Class: none * fix(memory): keep disabled daily sweep dark Resolve the backend-owned authority before inventory and require its literal true decision before any UID discovery, registry, cleanup, scheduler, model, or commit work. Missing, malformed, throwing, disabled, and kill-switched authority now exits without touching user data; enabled behavior is preserved.\n\nVerification: 60 focused daily-sweep job, scheduler, and inventory tests passed; independent Sol review accepted the fail-closed gate.\n\nFailure-Class: FC-split-mutation-authority * fix(jit): satisfy fail-closed type contracts * test(backend): admit full runtime contract checks * style(backend): format conversation bound test * test(backend): keep conversation router isolation current * test(backend): admit export boundary duration * fix(macos): persist failed chat turn notice Failure-Class: none * fix(macos): repair JIT rollout admission contracts Failure-Class: none * fix(windows): treat JIT screen evidence as untrusted Failure-Class: none * fix(backend): preserve explicit app failure contract Failure-Class: none * fix(app): finish photo viewer consolidation * fix(backend): make provider writes lock-free against the deletion gate The account-wide legal-hold deletion gate wrapped every GCS upload and Pinecone/Typesense upsert in an exclusive per-uid Firestore mutex with no lease: concurrent same-account writes hard-failed (dropped audio, lost vectors) and a crash between acquire and finish blocked the account's gated operations forever, with no janitor. Provider writes now use a lock-free fence that refuses only during account deletion or a live destructive operation; destructive kinds keep exclusive ownership, an abandoned gate self-expires after six hours, and releasing a gate on the failure path can no longer mask the original error. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): issue onboarding admission at socket connect The completed-onboarding early exit returned False from an Optional[str] function; the listen runtime derives admission via 'is not None', so users who had already completed onboarding were admitted with a fabricated session id — the exact provenance forgery the admission exists to prevent. Separately, the 20-minute admission TTL was anchored to the app-launch state read, so a user reaching the speech-profile step late (or any client that never calls the state endpoint) silently lost onboarding questions and is_user tagging. The bootstrap now issues or refreshes the admission from the durable account state at connect time; completed accounts still can never re-enter, and issuing stays best-effort with the read failing closed. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep the released proactivity lane open for legacy clients Gating /v1/desktop/proactivity/completions on the JIT cohort returned 403 to every non-admitted user — which is the entire deployed desktop fleet on deploy day, since shipped clients poll this route continuously and treat 403 as a plain error. Context-bucket extraction and the director would have died fleet-wide, dark cohort or not, and any environment without a PostHog key (local, self-host) would have lost the lane entirely. The route returns to merge-base admission semantics (tier quotas only); JIT admission remains enforced on the JIT reservation routes, and retiring this lane stays a later explicit operation after clients migrate. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): withhold JIT tools and history reads outside the rollout Five new tools (search_knowledge, search_historical_facts, read_playbook, get_entity_timeline, look_at_frame) sat unconditionally in CORE_TOOLS, so every legacy chat request carried their schemas and the model burned tool budget on 'no entries found' answers. They are now filtered per request off the same resolved rollout boolean that gates the JIT prompt appendix. The memories-tab ledger-history endpoint likewise answered every user with a bounded 501-row provider scan that can only ever be empty outside the rollout; it now returns empty without the scan for non-admitted (and unknown/error) states. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): bound rollout control-plane cost and confine sync resolution Synchronous callers resolved rollout flags via per-call asyncio.run against the shared provider singleton, crossing event loops: awaiting a Task attached to another loop raises, a timed-out asyncio.run strands a coalescer entry that then serves stale UNKNOWN forever, and the LRU cache was mutated from multiple threads. Sync resolution now runs on one long-lived control-loop thread with its own authority instance. Unknown snapshots gain a 5-second negative cache — UNKNOWN can never authorize work, and without it a fleet whose flags are simply absent pays one uncached PostHog call per conversation finalization. The screen-sync loop drops its force_refresh (one uncached decide per device per minute fleet-wide) and moves to its own rate bucket so two Macs' background sync can no longer starve conversation photo reads out of the shared 120/hour frame-requests bucket. The first-open policy's kill-switch telemetry label also reported str(Enum) instead of the value and could never match. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): skip eager extraction under a non-compatibility writer mode A ledger-cutover user still ran the full L1 extraction model call at finalization, after which writer admission refused the compatibility write — the conflict retried, exhausted, and failed the entire finalization for every conversation, with the model spend already paid. Extraction now checks the canonical writer mode first and skips when the daily sweep owns memory formation; only a positively-read non-compatibility mode skips, so any control-state read failure preserves the legacy eager path. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): export tolerates byte-less legacy photo rows A conversation photo row carrying the legacy empty inline marker and no storage reference failed the whole portability export forever, though it holds no durable image anywhere — there is nothing to omit. Such rows now export as metadata with a content-free gap reason. Frame requests in a retained state keep the fail-closed contract via an explicit require_bytes parameter. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(windows): harden JIT delivery, admission, and bootstrap boundaries Five verified defects: (1) the exclusive notification delivery slot leaked on any throw between reservation and commit — one SQLite hiccup during a JIT turn permanently silenced every proactive lane; the span is now try/finally-guarded and stale slots expire after ten minutes. (2) The ambient lane interpolated the raw window title into a tool-capable agent prompt; the turn now carries only the opaque context handle plus a sanitized executable name, framed as untrusted data like the nano-triage lane. (3) Google Calendar was fetched every ~60s before admission, so non-cohort users with Google connected paid ~1,440 reads a day for a refused feature; observation now gates calendar evidence on the cached authority. (4) Rollout-authority errors reset the cache and retried every frame (~1 req/s offline, forever); failures now back off from 30s to 10 minutes. (5) An unguarded JIT schema exec inside the shared database open could abort local storage for all features; the mirror bootstrap is now isolated, keeps the host-facing tables alive, and JIT stays inert when unavailable. Also re-checks the control-plane owner before committing the toast so an account switch mid-turn cannot show the previous owner's advice. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(macos): restore screen provenance, guard migrations, fence chat turns Four verified defects: (1) every pre-existing screen-derived task lost its 'Screen context / Open Rewind' source row because the new evidence policy dropped any provenance that is not rewind_frame.v1; the merge-base fallback row is restored for capture.v2/legacy refs (a test flipped to match the regression is restored to its merge-base assertions). (2) RewindDatabase published its pool before migrating, latching a failed migration into a permanent false-initialized state, and three unguarded ALTER TABLE memories migrations died with duplicate-column on machines that ran earlier builds of this branch; migration now precedes publication and the ALTERs/CREATEs are existence-guarded. (3) EventKit was queried on every context visit before the flags check; non-admitted owners now build no observation inputs. (4) A failed chat turn's reconstructed notice could be appended into a different conversation's transcript when the user switched sessions or cleared chat mid-flight; both transcript resets now revoke the active turn like selectApp already did. The pre-terminalized discard class (user Stop/watchdog) still drops the durable notice on relaunch — pinned by a characterization test in agent/tests/conversation-journal.test.ts with the least-invasive fix described there. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(testing): resolve firebase-tools from the checked-in dependency npx --prefix resolves the package bin against the current directory on some npm versions, and the admission runner deliberately launches from an isolated temp dir (firebase writes debug logs to cwd) — surfacing as 'sh: firebase: command not found' on hosts without brew node@22. Prefer the vendored node_modules binary when it matches the pin; npx remains the fallback. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): keep one eager-extraction call site for the surface ratchet Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): gate eager extraction at the public boundary The writer-mode skip moves from _extract_memories_inner to extract_memories: the replace-policy contract test pins the inner helper to exactly the canonical replacement path, and the public boundary is the better seam anyway — a sweep-owned user now skips parity capture and usage tracking along with the model call. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): stub onboarding admission issuance in bootstrap regression The connect-time ensure call landed in a harness that only stubbed the read, so the bootstrap test paid an extra real-module exception path and grazed the 0.30s fast-unit CPU budget under fanout load. Stub the issuance like the read. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): allowlist the bootstrap regression's CPU budget The full listen-runtime bootstrap test measures exactly at the 0.30s fast-unit CPU budget under a saturated pre-push fanout (CPU inflates ~2x there per the guard's own notes) while passing comfortably alone. It exercises deliberately heavyweight machinery; record it as an intentional exception rather than trimming the coverage. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep list(CORE_TOOLS) literal through JIT tool gating The JIT-only tool filter replaced the list(CORE_TOOLS) assignment with an inline comprehension, which broke the prompt-cache structural invariant (test_prompt_cache_optimization.py::test_core_tools_used_in_both_functions). Restore the list(CORE_TOOLS) copy and apply the JIT-only filter as a conditional pass, preserving rollout semantics and tool order. * feat(jit): drop automatic goal updates from the JIT featureset Product decision (David, 2026-08-26): goals change only through explicit user action for JIT-admitted conversations. Goal progress is no longer a first-open obligation — the effect is removed from FIRST_OPEN_EFFECTS and the worker, and the policy plan can no longer express deferring it. Legacy obligations carrying a pending goal_progress row are normalized away and complete on the remaining two effects. Non-JIT (legacy eager) conversations keep today's automatic goal updates unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): one summary-spine agent pass per day, with folder backstop Replaces the per-conversation transcript extractor in the completed-day producer with a single two-phase agent run: the whole day's conversation summaries go in as one bounded spine (200 conversations / 120k chars — effectively unreachable, so heavy days no longer stall the cursor), and the agent may request up to 8 raw transcript excerpts (8k chars each) to verify specifics before finalizing. At most two provider calls per user per day, both inside the existing at-most-once invocation fence; the staged page carries the memory candidates AND folder assignments for the day's unopened, unfiled conversations, applied idempotently (first-open or user assignment always wins). Memories must cite their source conversations; uncited output is dropped. The cost gate becomes a worst-case ceiling checked before any call. The onboarding cold-start channel keeps per-conversation transcript extraction unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): harden the daily agent prompts from a real-data lab pass Iterated on one real heavy day (26 conversations) with strong- and weak-model stand-ins, an adversarial judge, and hand-verified transcript ground truths. Rules added, each pinned to an observed failure: actor binding in active voice with a personal-attribute gate (a discussed or recommended topic is never someone's attribute; judgments about named people are stored as assessments); decision-state basis labels binding the verb (decided/proposed/observed, discussed-no-outcome dropped); salience ordering (money, metrics, named-party intent, identity, and durable decisions before any operational fact; one fact per memory); never guessing the direction of an invitation/offer/commitment (verify or drop); and no deferring the whole answer to verification. The agent output schema gains a 'basis' field. The memories QoS call-site inventories now count the daily-sweep agent's call site (3 -> 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): tune the daily agent prompts against the real memories model Ran the assembled prompts against gpt-5.6-luna (the real 'memories' route model) on the same real day. Three refinements from observed behavior: the basis label no longer leaks into memory text (metrics read as metrics, not 'David observed that…'); the never-guess-direction trigger is mechanical (passive/verbless summary phrasing or 'Speaker' as the actor forces a transcript_request — luna confidently inverted 'Tim: Invited to New York' until this; with it, phase B verifies and corrects to the true direction), hedging is itself a request signal, and nothing high-salience may be silently dropped; and a rich-day yield anchor (8-16 memories for 15+ conversations) counters the model's over-pruning without inviting padding. Final real-model run: 11 true memories + 2 legitimate verification requests, zero fabrications, ~22k tokens (~2 calls) for a 26-conversation day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): profile-maintaining slots, ledger lookups, cache-ready prompts The daily agent now sees the user's current profile (the same get_prompt_memories seam chat uses — the ledger render for migrated users), may run up to 4 owner-scoped prior-memory keyword lookups (provider fail-soft; hits re-read through the canonical store before disclosure) to dedup and supersede, and may name a slot for standing attributes — an occupied slot becomes an amend through the existing canonical occupancy check, so the daily run maintains the rendered profile with no second write path. Both phase prompts share a byte-identical prefix (pinned by a test) and pass a per-user prompt_cache_key through get_llm; measured against gpt-5.6-luna the provider cache is exact-match rather than prefix-based today, so this is future-proofing rather than present savings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): type the memory-searcher seam for the pyright contract CI's authoritative typecheck rejected the untyped lookup seam (memories.py: list(Any or [])). The searcher is now Optional[Callable[[str], Sequence[str]]] and results are built through a typed comprehension; behavior unchanged (absent or failing searcher still degrades to an empty result block). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair four main-inherited CI breakages after sync origin/main is currently red on its own tip; syncing it into this PR inherits the breakage, so the fixes ride here: - subscription.py: drop the unused get_byok_keys import (pyright reportUnusedImport fails the Backend unit suite). - AppState+Transcription.swift: explicit self for alertPresenter inside the escaping showAlert completion (strict-concurrency compile error in all three Desktop Swift lanes, shipped red on main by d49f978512). - AppState+Permissions.swift: pinned swift-format drift from the same main commit (desktop-swift-format-lint). - web/app/bun.lock: add the prettier + prettier-plugin-tailwindcss entries 64db30c791 pinned in package.json without updating the lockfile (frozen install fails web-app-checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): close the second review round's findings Three parallel adversarial reviews over the post-takeover additions: - Clamp every model-controlled phase-B input (draft memories, request reasons, lookup queries/results) and add the clamped worst case to the pre-call cost ceiling, which previously under-estimated phase B. - Attest an empty consumed day when the staged page carries an older stage schema version instead of stalling the cursor forever on every deploy-boundary schema bump. - Make the folder backstop's unfiled check and write share one transaction so a concurrent first-open/user assignment always wins. - Let equal-rank sweep candidates amend sweep-authored slot occupants: the profile-maintenance path froze after a slot's first write. User statements still always win; slotless subject matches still dedup. - Neutralize ``` fences in summaries/excerpts/lookup results, and mark raw-transcript fallback rows '(unstructured transcript excerpt)' with a prompt rule refusing slots/personal attributes from them without transcript verification (test pins the marker to the rule). - Remove the dead first-open goal-authority threading left by the goals removal, and update the stale jit-first-open-runtime doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair three more main-inherited breakages All shipped red on main and only surfaced once earlier failures were cleared: - AppState.swift: move the alertPresenter default out of the stored property initializer — Xcode 16.4's SILGen segfaults (signal 11) emitting it, which failed all three Desktop Swift lanes even after the explicit-self fix. - test_byok_security.py: main's BYOK rewrite (d0e3a4eb3a, 1da8880175) changed request_has_llm_byok_key to per-provider enrollment checks and made partial headers fail closed, but left the tests targeting the old get_byok_keys()-based lenient contract (masked on main because pyright failed before pytest ran). The tests now assert the shipped strict contract their own docstrings already describe. - subscription.py: pinned-black formatting for the BYOK fallback expression (the Formatting lane rejects the file as main wrote it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): stub the chat-agent gateway route pin in the chat router harness Main's a6988be309 made routers.chat import CHAT_AGENT_ROUTE_DIRECT / get_chat_agent_route from utils.llm.gateway_client, but the chat-router test harness (and test_chat_file_upload_unsupported's local override) stub utils.llm.gateway_client without those symbols, so every suite that loads the real router failed at import — masked on main because pyright fails its Backend unit suite before pytest runs. Ninth main-inherited repair in this sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): teach test_chat_quota's utils.byok stub the rewritten import surface utils/subscription.py now imports get_byok_uid and get_cached_byok_state (main's BYOK rewrite); the module-scoped utils.byok fake predates them, so reloading subscription under the fake raised ImportError at setup — and the polluted process took test_chat_openapi_operation_ids and test_desktop_screen_crisp down with it in CI's batched run (all three pass standalone). Tenth main-inherited repair, same pyright-masked pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): update three more suites for main's BYOK/gateway import surface Same pyright-masked pattern as the harness and test_chat_quota repairs: - test_desktop_transcribe stubbed utils.llm as a non-package, so routers.chat's new utils.llm.gateway_client import could not resolve (50 failures); the submodule is now in its stub list. - test_paywall_reconnect_gate's BYOK escape-hatch tests never set the request uid context that the enrollment-verifying rewrite requires (middleware sets it in production); they now do, and teardown clears it. - test_chat_session_app_identity's enforce_chat_quota stub rejected the new required_llm_provider keyword. All three suites pass locally (69 + 35 + 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): enroll fingerprints in the desktop BYOK tests PR #11454 moved macOS BYOK activation to enrollment-verified fingerprints (isByokActive and usableBYOKEnvironment gate on persistEnrolledFingerprints), and its own test lanes shipped red: the tests store raw keys but never enroll them, so every key reads as inactive. Their teardowns already clear enrollment — the setups now enroll what they store, matching the production activation path. All 8 previously-failing cases (BYOKPaywallTests + the two AgentRuntimeProcessTests BYOK-environment cases) pass locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deploy): enable the daily memory sweep on development The sweep's five deployment inputs were pinned off in every environment, so cohort enrolment alone could never start it -- turning it on for a dogfood account required a second PR. Development now carries the live values: - ENABLED/MODEL_ENABLED on, so the job stops exiting at its first authority gate and the model authority can budget a route. - MODEL_NAME pinned to gpt-5.6-luna, which is the declaration interlock the runner checks against get_model('memories') before any provider call. - MAX_MODEL_COST_USD 0.80, the worst-case pre-call ceiling for a maximal day including phase B's clamped draft/reason/lookup overhead. - COHORT_ENABLED on with COHORT_FLAG daily-memory-sweep-v1, so enrolment is a per-uid PostHog boolean and an unnamed cohort stays a closed rollout. Production is deliberately untouched and stays fully pinned off. The job still cannot form a memory for anyone until that flag exists and resolves true for a uid, which remains a control-plane action rather than a deployment one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(firestore): terminate the daily-sweep occupant indexes with __name__ The six daily-sweep occupant lookups were the only declarations in the manifest without a trailing __name__ field -- 63 of 69 entries carry one, and main had none missing it. Firestore appends the terminator itself and reports the index back that way, so these six could never match the live inventory. The failure mode is not a missing index; the indexes build fine. It is that reconciliation never converges: every run reports the same six as missing, tries to create them, and fails on ALREADY_EXISTS. That takes down the Firestore schema workflow on both environments permanently, and with it the development backend deploy's readiness gate -- the same class of outage the workflow's own header records from the hourly_usage index in PR #11979. The derived specs previously appended their extra predicates to the base spec's index_fields, which would have placed them after the terminator, so the shared prefixes are now named explicitly and each spec ends with __name__. Verified against real Firestore: reconciliation reports zero missing indexes in both based-hardware and based-hardware-dev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: close final JIT rollout and CI gaps Fence direct JIT tools and frame pixels, keep Windows account wipes safe after optional schema failures, and repair inherited CI regressions. Failure-Class: none --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 27 天前 | |
feat(dev-harness): fake-backed V1 live-session broker (reload, restart, controls, evidence, owned teardown) (#14362) * harden(dev-harness): define V1 live-session and V8 spine contracts Add strict Python/Dart pending contracts and introducing-commit protection. Reserve the live-session CLI, wire, ownership/attachment seams and failing builder acceptance tests. Add live attribution before freezing evidence v1. Verification: - bash scripts/dev-harness/run-tests.sh: 287 passed, 7 existing skips, 40 strict xfailed (24 pending V1 markers), Python 3.11.15. - make preflight: 16 checks passed, using supported PYTHON selection. - make mobile-verify fast --paths harness/session contract inputs with an absolute evidence directory: 20 executed, 20 passed, no failures/skips. - bash app/test.sh: 2014 passed, 5 skipped, one unchanged UTC+7 search_rank_grouping_test failure; affected baseline inputs match main. - Real module CLI refusals exit 2 without traceback; regression covered. - Dart SDK probe proves ext. prefix required; marker-removal formatter probe proves no incidental Dart diff. Runtime is intentionally unimplemented. App UI must repair current invalid VM extension names before live acceptance; unsupported journeys block. Simulator/emulator latency and detached-process acceptance remain V1 work. make setup installed hooks but full backend wheel sync stalled; isolated Python 3.11 harness dependencies ran the owner checks without any skip flag. Failure-Class: none * harden(dev-harness): scope the spine-contracts check to spine paths The check protects files under the spine roots and their registry, so it only needs to run when those change. A repo-wide trigger would run it, and its full-history requirement, on every unrelated contributor PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(dev-harness): clarify the live journey adapter boundary V1 must refuse live journey execution until a shared executable adapter exists; B1 addressability alone cannot attach Dart integration tests. Pure admission contracts remain valid with injected supported IDs. Verification: harness 287 passed, 7 skipped, 40 strict xfailed; make preflight passed 20 checks. * harden(dev-harness): pin reviewed spine revisions and align B0 capabilities Coordinator turn-4 review authorizes the V1 fixture correction. Exact before/after digests pin the revised oracle; immutable revision records do not permit builder assertion edits. Active tests reject missing/tampered records and prove the fake machine response. Verification: full harness 289 passed, 7 skipped, 40 strict xfailed; UTC app 2020 passed; spine 24 pending/7 protected; make preflight 16 passed. No live device acceptance claimed. * harden(dev-harness): preserve marker retirement across spine revisions Track retained marker occurrences rather than counts so retiring a second test cannot restore the first test's retired marker. The active regression covers this swap and legitimate continued retirement. Verification: full harness 290 passed, 7 skipped, 40 strict xfailed; spine 24 pending/7 protected; make preflight 16 passed. Dart sources unchanged after the earlier UTC app 2020-pass run. * harden(dev-harness): retain corrected spine oracles through squash merges A squash can introduce a corrected contract and its review records in the same commit. Absorb only the revision prefix ending at that introducing file's exact digest, preserving immutable records and later marker-only edits. Active temp-repo regression covers multiple corrections in one squash and rejects restoring old or altered assertions. Verification: harness 291 passed, 7 skipped, 40 strict xfailed; B1 make preflight 20 passed; final B1 journeys 20/20. Dart unchanged after UTC app 41 opt-in and 2051 ordinary passed. * test(dev-harness): revise live session acceptance after builder review * test(dev-harness): bind machine fixture to actual child identity * feat(dev-harness): implement the fake-backed V1 live session broker LiveSession speaks the Flutter machine wire against an injected child, records BrokerIdentity in live.json, and refuses real flutter run from CLI dispatch. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dev-harness): tear down live before stop, reset, recover, and release Lifecycle order is live teardown, then services, then device detach; recover still has generation 1 while teardown runs. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 live-session pending markers The fake-backed broker now satisfies those spine tests; only whole pending-marker lines were deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): cover lazy live-session holes with real child processes A dead flutter child mid-reload is blocked, never success; teardown will not signal a live PID whose start time, marker, or boot id does not match. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): hash iOS .app bundles as directory trees file_sha256 raised IsADirectoryError on a simulator .app; identity is now the sorted tree of regular files. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): pin live-session source, generation, and stdio The architect's thirteen probes rejected attributing a compile to source observed after launch, announcing stopped after a failed reap, and grepping one fixture secret. Keep one monotonic RPC deadline and actually validate negotiated capabilities. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fence live publication, boolean readiness, and auth redaction _assert_generation only covered admission, so a lease roll during ext.omi.controls.state still published ok and advanced loaded identity. Recheck lease and child generation after daemon/readiness work, refuse non-boolean readiness, and redact complete Authorization values before logs are retained. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): restore live teardown after main merge and retire passed T7 markers Keep main's port-claim mobile_session.py; put back V1 live teardown on stop/reset/recover. The formatted round-7 oracles now pass, so remove only the pending-marker lines. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(dev-harness): restore app sources that the merge hook reformatted The merge commit ran dart format without package:flutter_lints resolved and rewrote main's files. Restore origin/main bytes so the V1 PR does not carry unrelated UI diffs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail fast when a live owner holds the session lease Name the session and holder pid immediately. The same overlap that used to look like a controls-extension miss is a held lease. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 pending markers that main's live fences now pass Merging origin/main imported strict xfails for lease-roll, negotiation, and auth-redaction fences this branch already implements. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail closed on non-boolean live readiness flags Finding 2 of the adversarial review reproduced: a fake reporting readiness.signedIn as the string "false" was classified not-ready and fell through to the wait_ready recovery path, which then observed real booleans and published ok. _is_ready now raises LiveError (malformed-response) for present-but-non-boolean flags, so start blocks instead of self-healing; start/reload/restart wrap it and surface blocked/malformed-response. The stringy-false regression test's expected error_code moves unready -> malformed-response; blocked-stays-blocked is unchanged. The satisfied V1 pending marker on the boolean probe is retired. Findings 1, 3 and 4 did not reproduce at this head (probes re-run verbatim; evidence in PR comment). Failure-Class: new --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
feat: converge universal memory and task authority | 1 个月前 | |
fix(dev-harness): distinguish failed Android inventory from a missing engine (#14400) sdkmanager --list_installed exiting nonzero was reported as engine present. A missing emulator binary, a successful empty listing, and a failed look are three different findings; cmdline-tools 23 slash paths still prove an image is installed. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
SCA-486: isolated mobile sessions, local-dev auth, capture replay, and seeded journeys (#14213) * mobile sessions: freeze session-evidence-v1 receipt contract Freeze the minimal versioned session/evidence schema shared by the mobile development-foundation consumers (C2 journeys, C3 capture replay, C4 verification, C5 devices) and its executable validator. - contracts/session/session-evidence-v1.schema.json: closed v1 object binding source SHA + dirty digest, built artifact identity, loopback-only endpoints, fixture/runner versions, real timestamps, status/blocked reason and exact execution counts. No credential-shaped field exists anywhere. - dev_harness/session_evidence.py: builder + validator enforcing the cross-field semantics a JSON Schema cannot express: ready/running require an artifact whose git_sha matches the source (a stale build cannot be reported ready), production-family profiles are rejected as session targets, counts must account exactly, zero-execution receipts are only valid pre-run, and credential-shaped keys are refused at any depth. - Egress guards: validate_local_http_url/validate_local_host_port reject non-loopback or non-plain-HTTP endpoints (api.omi.me / api.omiapi.com by name) before any request is attempted. Evidence: scripts/dev-harness/run-tests.sh (test_session_evidence.py, 20 contract tests incl. stale-artifact, egress, credential and accounting rejections). * mobile sessions: deterministic synthetic auth fixture v1 Seed a synthetic Auth-emulator user through the local-dev custom-token endpoint contract from open PR #11784 (feat/app+backend local-development sign-in without OAuth) — reuse, not a competing endpoint. The PR stays with its owner; scripts/dev-harness/MOBILE_SESSIONS.md records the integration plan and provenance. - fixtures/mobile/v1.json: one deterministic user (omi-fixture-v1-user-1@local.test); RFC-reserved domain so fixture identities can never collide with a real account. No real Google/Apple user, provider key, or copied token involved. - dev_harness/mobile_fixtures.py: fail-closed seeding client — the backend URL is validated as loopback plain-HTTP before any request, production hosts are denied by name, and the persisted receipt records identity and outcome only: token_minted/token_retained, never the token itself. Evidence: test_mobile_fixtures.py (16 tests) — determinism, reserved-domain enforcement, pre-request egress refusal, 404/unreachable/wrong-uid fail-closed paths, credential-free receipts. * mobile sessions: structured doctor for the session lanes Every readiness failure classifies exactly one of ready / agent-remediable (with the exact resumption command) / operator-action-needed (privileged install, license, host capacity), per lane (backend, android, ios). - Flutter version is read from the mobile CI pin in .github/workflows/mobile-app-checks.yml — never 'latest'; inconsistent pins refuse rather than guess. - Backend lane: python3.11 (venv must be 3.11, ambient 3.14 must not select the runtime), JDK 21 for the firebase emulators, firebase-tools, redis/typesense via native binary or a responding docker daemon. - Android lane: ANDROID_HOME + adb + emulator engine + system image, each with the exact sdkmanager remedy and a capacity-gated download note. - iOS lane: Xcode + simctl runtime; missing runtime is an operator action. - Capacity: <12GiB free on the shared Data/scratch container is an operator gate for emulator/build lanes (agents never free space themselves); contract/unit lanes skip it via --skip-capacity. - Egress: an ambient production OMI_LOCAL_API_BASE_URL override is reported as a blocking misconfiguration. Evidence: test_mobile_doctor.py (13 tests) over an injected runner — lane filtering, ready/degraded/blocked classification, pin parsing, capacity and operator-gate behavior; live run on m1-mac-studio via 'make mobile-session ARGS="doctor --platform android --platform ios"' reports backend+ios ready, android emulator engine agent-remediable. * mobile sessions: isolated session lifecycle CLI behind one entrypoint 'make mobile-session ARGS="…"' (scripts/dev-harness/mobile-session.sh) owns a uniquely-leased local mobile session: doctor / acquire / start / seed / reset / status / evidence / stop / recover / release. A session is an existing dev-harness instance + port offset + device lease + seed receipt + evidence receipt — the harness lifecycle is reused in-process under OMI_LOCAL_INSTANCE/OMI_HARNESS_PORT_OFFSET, not duplicated. Ownership is fail-closed: - leases are created atomically (O_EXCL) with owner host/user/pid and a harness-standard sentinel; a live foreign owner or another local user's session is never touched; cross-host takeover is an operator decision; recover bumps the generation for same-host/same-user takeovers. - ports come from a claimed offset registry; a foreign process occupying a port is refused (never killed) and the allocator skips that offset; release frees the claim only when it belongs to the session. - start gates device attach on doctor readiness (precise blocked reason, not a crash); ios-simulator devices are created/booted/deleted session-owned via simctl. - seed/reset/stop/release are idempotent; reset only touches the session's own harness instance (sentinel-validated underneath). - evidence emits session-evidence-v1 receipts; ready/running refuse without a bound artifact and refuse when the source moved since acquire. app/setup.sh (separate commit): OMI_IOS_DEVICE_ID pins non-interactive device selection; OMI_DEVICE_SUFFIX overrides hostname identity. Evidence: test_mobile_session.py (19 tests) + wrapper tests — exclusivity, disjoint ports, dead-owner/live-foreign/different-user/cross-host refusals, foreign-port refusal with a real live listener, idempotent release, artifact binding, stale-source refusal, harness env handoff. Live CLI run: acquire/list/evidence/seed-fail-closed/stop/release with exit codes 0/2 on m1-mac-studio. * app/setup.sh: non-interactive device pin and per-session device suffix - OMI_IOS_DEVICE_ID: when set, select_ios_device uses exactly that device id, failing precisely (with the available device list) when absent, instead of enumerating and prompting — the mobile-session harness, CI and nested agents cannot answer an interactive prompt, and the current no-TTY path errors out whenever more than one iOS destination exists. - OMI_DEVICE_SUFFIX: let a session harness (or a second checkout on one host) inject a unique device-identity suffix instead of the hostname, which collides across concurrent sessions on the same machine. Unset behavior is unchanged. Verified by sourcing the function with a stubbed flutter devices --machine: pinned-present emits the id; pinned-absent fails with the list; unpinned multi-device no-TTY keeps the existing enumeration failure. * mobile sessions: apply repo python formatter to the new modules black 26.5.1, --line-length 120 --skip-string-normalization via scripts/backend-python-format; behavior unchanged, dev-harness lane re-run green (214 passed; 1 pre-existing environmental failure — the host's global git worktree guard blocks pytest-tmp linked worktrees). * test: place linked-worktree pytest fixtures under OMI_WORKTREES The managed git wrapper correctly refuses worktrees in /private/tmp. Keep that guard and put the fixture where task worktrees are allowed. Co-authored-by: Cursor <cursoragent@cursor.com> * docs: treat session-evidence-v1 as proposed until consumers review it C1 shipped the schema; freeze it only after C2/C3/C4 agree, not from a single worker declaration. Co-authored-by: Cursor <cursoragent@cursor.com> * feat: reuse PR 11784 local-dev custom-token auth on current main Copy the reviewed emulator-gated sign-in path onto this integration branch so synthetic seed talks to real local services. Leave the original PR open and unmerged. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: boot isolated sessions on real CoreSimulator IDs and offline STT Use the installed iPhone 17 Pro / iOS 26.5 identifiers, pin PROVIDER_MODE=offline, and drop soniox from the offline STT chain so the local backend can start without a paid key. Co-authored-by: Cursor <cursoragent@cursor.com> * test: isolate provider-secret fixtures from ambient PROVIDER_MODE A previous offline session left PROVIDER_MODE in the shell and made the secret-injection tests read ambient offline instead of the fixture file. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): injectable capture seams for deterministic recovery replay CaptureController and the phone WAL resolved clock, timers, connectivity, auth, mic, socket and upload policy through global singletons, so the capture -> WAL -> recovery path could not be replayed deterministically. Add narrow constructor seams (capture_seams.dart) with production-identical defaults: CaptureScheduling, CaptureAuthBoundary, CaptureConnectivityBoundary, plus wal/phoneMic/clock/scheduler injection on CaptureController; clock/periodic/job-status injection on LocalWalSyncImpl threaded through WalSyncs/WalService; periodic-timer injection on the NativeMicRecorderService watchdogs. The in-progress-conversation loader seam now covers the socket-connect path too, and streamRecording honors the microphone permission requester like the batch path already did. No behavior change with default construction; every seam is optional. Evidence: bash app/test.sh (1990 passed, 5 pre-existing skips); analyze ratchet green. * test(app): deterministic capture-recovery replay schedules (SCA-489/C3) Replay the REAL production capture pipeline (CaptureController, NativeMicRecorderService, TranscriptSegmentSocketService, WalService, RecordingTransferCoordinator) against controlled external I/O: virtual clock, manual bounded scheduler, scripted transport/upload boundary, fake native host. Restart evidence destroys and reconstructs the object graph from real temp files (torn wals.json -> backup recovery, missing audio -> terminal corruption, process kill -> disk reload and re-upload). Six schedules with invariant oracles: network loss/reconnect mid-capture (exact frame identity in the stored WAL, single upload), stale native events after stop/new session (session-identity gate, no double teardown), interruption/resumption (live + batch, bounded stall escalation), partial/torn persistence plus reconstruction, failed upload with bounded backoff and persisted/enqueued/server-acknowledged distinctions, and ownership transition (signed-out reconnect cancellation, bounded 4001 token refresh). Also publishes the C2/C4 adapter (capture_scenario.dart: catalog + result contract) and the C5 native-event vector schema (phone-mic-native-events/v1) mirroring the Pigeon PhoneMicFlutterApi contract without touching Pigeon. Falsification evidence: removing the NativeMicRecorderService session gate flips the stale-idle schedule to failure (record->stop); removing the finalizeCurrentSession unsynced-retention guard drops the WAL and fails the network-loss schedule. Evidence: flutter test test/unit/capture_recovery_replay_scenarios_test.dart (17 passed); bash app/test.sh full suite green. * chore(app): allowlist SCA-489 replay contract libs in the dead-code ratchet The scenario catalog/result contract and the native-event vector schema are library-only by design until the C2/C4 and C5 lanes import them; the ratchet demands an explicit allowlist entry with a reason for exactly this case. * test: pin conversation-window capture session id across sequential phone-mic lives activeCaptureSessionId is WAL/conversation-scoped so a late ConversationEvent can still stamp WALs. C2 must use activeRecordingId as the live recording identity. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: wait for unawaited capture-upload retries before asserting and teardown CI failed the bounded-backoff replay because cooldown wakes are unawaited and settle used wall-clock sleeps that missed the drain under load, then deleted the temp WAL dir mid-write. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): typed debug semantic controls for the local journey lane Extends the existing debug Marionette surface (same debug VM-service transport, no new server/framework) with product-semantic controls: versioned capabilities (semantic-controls/v1), privacy-safe state (route, principal, capture lifecycle with activeRecordingId as the authoritative recording identity), bounded wait_ready, production-path navigation, and named journey faults. Fail-closed eligibility: kDebugMode AND local_dev profile AND OMI_DEV_CONTROLS=1 dart-define. Ineligible builds (including production-flavor debug) install nothing and the HTTP fault chokepoint is a pure pass-through — pinned by semantic_controls_guard_test.dart. Narrow seams added for the hermetic journey lane: - AuthService.installLocalHarnessTokenGateway (debug+local_dev gated Firebase token I/O boundary; isSignedIn routes through the gateway) - PlatformManager.initializeForLocalHarness (header fields only) - CrashlyticsManager report paths tolerate a missing Firebase app the same way main.dart's zone handler already does, so host-lane errors surface instead of being masked by [core/no-app] Verified: flutter test test/unit/semantic_controls_guard_test.dart (10 passed); auth regression suites (34 passed); C3 capture replay (17 passed); dead-code ratchet at baseline. * test(app): five strict seeded acceptance journeys with negative fault variants Canonical executable definitions (one per behavior) under app/integration_test/journeys/, runnable hermetically (flutter-tester + loopback fixture backend) or on a simulator via run_journeys.sh: j1 seeded conversation detail — real provider fetch + real detail page, exact synthetic identity; negative: wrong-owner session refused. j2 chat send -> distinct assistant reply — real input/send-button keys (omi.chat.input / omi.chat.send), request observed server-side, server-minted ai-role reply distinct from the prompt, rendered; negatives: suppress-send, suppress-assistant-reply, wrong-owner-session. j3 memory create/edit surviving reload — production provider path, server-minted id required after reload; negative: drop-memory-save. j4 expired session — transient failure re-mints via the real custom-token endpoint; terminal failure emits expiry and blocks requests; negative: production-family profiles never silently re-mint. j5 capture interruption/reconnect — C3 capture-scenario/v1 adapter: real temp files, process reconstruction, drain exactly once; negative: fail-capture-recovery. Each negative arms exactly one named fault and must fail with the invariant named. Evidence receipts follow session-evidence-v1 accounting; zero-execution runs never pass. Verified: bash integration_test/journeys/run_journeys.sh (5/5 pass); repeated deterministic vertical: bash integration_test/journeys/run_journeys.sh --filter j2 --runs 5. * fix: stamp journey evidence finished_at at write time Receipts were recording construction time as the end timestamp, so duration could not be distinguished from start. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(app): clear new analyzer-ratchet regressions in the integrated journeys The integrated checkpoint (b217eb1a9d) fails app/scripts/analyze_ratchet.sh with 7 new occurrences: 3 unused imports plus a bogus 'show WalStatus' in j5, an unused-looking nested import that actually provides SingleChildWidget in hermetic_boot, a missing const in j4, and two depend_on_referenced_packages for test-only platform interfaces. Declares path_provider_platform_interface and nested as direct dev dependencies (same pattern as the existing web_socket_channel dev deps) and removes the dead imports. Mechanical lint repairs only — j4/j5 hermetic journeys re-run green after the change. * feat: unified mobile verify lanes, mechanical journey selection, and CI/contributor path (SCA-490/C4) One canonical verification entrypoint over the proven lanes: make mobile-verify select|doctor|fast|smoke|physical (scripts/dev-harness/mobile-verify.sh -> dev_harness.mobile_verify). It never adds a second runner: journeys delegate to the C2 canonical runner and session infrastructure to the C1 session CLI. Selection is mechanical and fail-closed: journeys are glob-discovered (runner --list contract test), changed paths map through a per-seam rule table, unknown app/lib impact falls back to the full suite, and an empty selection is drift (exit 65), never PASS(0) — run_journeys.sh now fails closed the same way. Receipts are validated against session-evidence-v1 accounting (honest counts, zero-execution never passes) and every lane writes a verify-receipt.json binding source SHA + dirty digest, runner versions, outcomes, and the exact rerun command. smoke is fail-closed (exit 2 + remedy, never CI), physical is a separately reported admission lane. CI runs the same command in a new journeys-hermetic job in the existing mobile-app-checks.yml when has_app_journeys fires (journey definitions and support, C3 replay world, dev controls, non-generated app/lib Dart, evidence contract, or this entrypoint) — synthetic fixtures only, fork-safe, receipts uploaded on pass and failure. Selection is resolved by the shared pre_push_ci_prediction.py and deliberately stays out of the bounded pre-push gate. Docs reconciled around the real command: app README, app AGENTS (within the lean budget), and the e2e SKILL now point here instead of diverging on setup/auth. * chore(app): stop tracking Flutter's iOS ephemeral tree app/ios/Flutter/ephemeral/** is regenerated by flutter on every pub get and self-describes as 'Generated file. Do not edit.' It was committed by accident in a formatting sweep (dec329a84a) and has been stale ever since: the tracked SwiftPM Package.swift lists pods (in_app_review, pasteboard) that no pub dependency provides, so any flutter run rewrites it, dirties every worktree, and fails the diff-hygiene push gate on regenerated trailing whitespace. Untrack the four files and ignore the tree, mirroring the existing **/macos/Flutter/ephemeral/ rules. Xcode resolves the local package after flutter regenerates it during setup; nothing consumes a committed copy. * feat: native lifecycle seams, vector replay, and leased device qualification (SCA-491/C5) - PhoneMicController (iOS + Android) now consumes narrow, injectable environment/ports seams: event sink, engine, permission, session config, interruption source, batch pipeline, main loop. Production behavior is unchanged; all live wiring lives in PhoneMicHostApiImpl.swift (iOS) and PhoneMicControllerPorts.production (Android). - Canonical phone-mic-native-events/v1 vector fixtures (8 schedules incl. session adoption) shared by Dart guard, iOS ruby harness, Android JVM harness; Pigeon contract types extracted at iOS test time (drift-guarded). - iOS: ios/test/phone_mic_lifecycle_replay_test.rb replays all vectors through the production controller+emitter with fakes for OS I/O only. - Android: PhoneMicLifecycleReplayTest (JVM, virtual main loop + manual audio queue) replays the same vectors through the production controller. - device_lease.py: exclusive physical-device leases with qualification registry (personal-device refusal), bounded acquisition, live-lease never-stolen, stale-owner recovery with generation bump, safe release. - device_runner.py + 'mobile-session device' CLI: readiness doctor with exact operator steps, and a runner consuming C1 session manifests (install/adb-reverse/untethered launch/permission cycle/device-run evidence v1). All hermetically tested with fake devices (25 tests). - PHYSICAL_DEVICES.md: m1-mac-studio read-only inventory, operator runbook, and the external physical-test handoff template. Physical acceptance stays pending user-run evidence by design. * docs: point mobile-verify physical at the C5 device handoff C4's physical lane stays fail-closed (exit 2). After C5 landed, the admission document should name the real runner and PHYSICAL_DEVICES.md instead of implying the software path is still missing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: expect four Flutter pins in mobile-app-checks C4 added journeys-hermetic as a fourth Flutter job on the same repository toolchain pin. The workflow-contract count of 3 was stale. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: raise Desktop Swift PR-lane suite budget to 3000s Run 35134593036 measured 2778s against 2700s on a cache-hit PR lane. The overrun was one 1500s batch ceiling plus isolation, not a slow desktop suite; this mobile PR has no desktop sources. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> | 7 天前 | |
feat(dev-harness): fake-backed V1 live-session broker (reload, restart, controls, evidence, owned teardown) (#14362) * harden(dev-harness): define V1 live-session and V8 spine contracts Add strict Python/Dart pending contracts and introducing-commit protection. Reserve the live-session CLI, wire, ownership/attachment seams and failing builder acceptance tests. Add live attribution before freezing evidence v1. Verification: - bash scripts/dev-harness/run-tests.sh: 287 passed, 7 existing skips, 40 strict xfailed (24 pending V1 markers), Python 3.11.15. - make preflight: 16 checks passed, using supported PYTHON selection. - make mobile-verify fast --paths harness/session contract inputs with an absolute evidence directory: 20 executed, 20 passed, no failures/skips. - bash app/test.sh: 2014 passed, 5 skipped, one unchanged UTC+7 search_rank_grouping_test failure; affected baseline inputs match main. - Real module CLI refusals exit 2 without traceback; regression covered. - Dart SDK probe proves ext. prefix required; marker-removal formatter probe proves no incidental Dart diff. Runtime is intentionally unimplemented. App UI must repair current invalid VM extension names before live acceptance; unsupported journeys block. Simulator/emulator latency and detached-process acceptance remain V1 work. make setup installed hooks but full backend wheel sync stalled; isolated Python 3.11 harness dependencies ran the owner checks without any skip flag. Failure-Class: none * harden(dev-harness): scope the spine-contracts check to spine paths The check protects files under the spine roots and their registry, so it only needs to run when those change. A repo-wide trigger would run it, and its full-history requirement, on every unrelated contributor PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(dev-harness): clarify the live journey adapter boundary V1 must refuse live journey execution until a shared executable adapter exists; B1 addressability alone cannot attach Dart integration tests. Pure admission contracts remain valid with injected supported IDs. Verification: harness 287 passed, 7 skipped, 40 strict xfailed; make preflight passed 20 checks. * harden(dev-harness): pin reviewed spine revisions and align B0 capabilities Coordinator turn-4 review authorizes the V1 fixture correction. Exact before/after digests pin the revised oracle; immutable revision records do not permit builder assertion edits. Active tests reject missing/tampered records and prove the fake machine response. Verification: full harness 289 passed, 7 skipped, 40 strict xfailed; UTC app 2020 passed; spine 24 pending/7 protected; make preflight 16 passed. No live device acceptance claimed. * harden(dev-harness): preserve marker retirement across spine revisions Track retained marker occurrences rather than counts so retiring a second test cannot restore the first test's retired marker. The active regression covers this swap and legitimate continued retirement. Verification: full harness 290 passed, 7 skipped, 40 strict xfailed; spine 24 pending/7 protected; make preflight 16 passed. Dart sources unchanged after the earlier UTC app 2020-pass run. * harden(dev-harness): retain corrected spine oracles through squash merges A squash can introduce a corrected contract and its review records in the same commit. Absorb only the revision prefix ending at that introducing file's exact digest, preserving immutable records and later marker-only edits. Active temp-repo regression covers multiple corrections in one squash and rejects restoring old or altered assertions. Verification: harness 291 passed, 7 skipped, 40 strict xfailed; B1 make preflight 20 passed; final B1 journeys 20/20. Dart unchanged after UTC app 41 opt-in and 2051 ordinary passed. * test(dev-harness): revise live session acceptance after builder review * test(dev-harness): bind machine fixture to actual child identity * feat(dev-harness): implement the fake-backed V1 live session broker LiveSession speaks the Flutter machine wire against an injected child, records BrokerIdentity in live.json, and refuses real flutter run from CLI dispatch. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dev-harness): tear down live before stop, reset, recover, and release Lifecycle order is live teardown, then services, then device detach; recover still has generation 1 while teardown runs. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 live-session pending markers The fake-backed broker now satisfies those spine tests; only whole pending-marker lines were deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): cover lazy live-session holes with real child processes A dead flutter child mid-reload is blocked, never success; teardown will not signal a live PID whose start time, marker, or boot id does not match. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): hash iOS .app bundles as directory trees file_sha256 raised IsADirectoryError on a simulator .app; identity is now the sorted tree of regular files. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): pin live-session source, generation, and stdio The architect's thirteen probes rejected attributing a compile to source observed after launch, announcing stopped after a failed reap, and grepping one fixture secret. Keep one monotonic RPC deadline and actually validate negotiated capabilities. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fence live publication, boolean readiness, and auth redaction _assert_generation only covered admission, so a lease roll during ext.omi.controls.state still published ok and advanced loaded identity. Recheck lease and child generation after daemon/readiness work, refuse non-boolean readiness, and redact complete Authorization values before logs are retained. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): restore live teardown after main merge and retire passed T7 markers Keep main's port-claim mobile_session.py; put back V1 live teardown on stop/reset/recover. The formatted round-7 oracles now pass, so remove only the pending-marker lines. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(dev-harness): restore app sources that the merge hook reformatted The merge commit ran dart format without package:flutter_lints resolved and rewrote main's files. Restore origin/main bytes so the V1 PR does not carry unrelated UI diffs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail fast when a live owner holds the session lease Name the session and holder pid immediately. The same overlap that used to look like a controls-extension miss is a held lease. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 pending markers that main's live fences now pass Merging origin/main imported strict xfails for lease-roll, negotiation, and auth-redaction fences this branch already implements. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail closed on non-boolean live readiness flags Finding 2 of the adversarial review reproduced: a fake reporting readiness.signedIn as the string "false" was classified not-ready and fell through to the wait_ready recovery path, which then observed real booleans and published ok. _is_ready now raises LiveError (malformed-response) for present-but-non-boolean flags, so start blocks instead of self-healing; start/reload/restart wrap it and surface blocked/malformed-response. The stringy-false regression test's expected error_code moves unready -> malformed-response; blocked-stays-blocked is unchanged. The satisfied V1 pending marker on the boolean probe is retired. Findings 1, 3 and 4 did not reproduce at this head (probes re-run verbatim; evidence in PR comment). Failure-Class: new --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
harden(dev-harness): live-session V1 contract, evidence-v1 freeze, and strict pending contracts (#14310) * harden(dev-harness): define V1 live-session and V8 spine contracts Add strict Python/Dart pending contracts and introducing-commit protection. Reserve the live-session CLI, wire, ownership/attachment seams and failing builder acceptance tests. Add live attribution before freezing evidence v1. Verification: - bash scripts/dev-harness/run-tests.sh: 287 passed, 7 existing skips, 40 strict xfailed (24 pending V1 markers), Python 3.11.15. - make preflight: 16 checks passed, using supported PYTHON selection. - make mobile-verify fast --paths harness/session contract inputs with an absolute evidence directory: 20 executed, 20 passed, no failures/skips. - bash app/test.sh: 2014 passed, 5 skipped, one unchanged UTC+7 search_rank_grouping_test failure; affected baseline inputs match main. - Real module CLI refusals exit 2 without traceback; regression covered. - Dart SDK probe proves ext. prefix required; marker-removal formatter probe proves no incidental Dart diff. Runtime is intentionally unimplemented. App UI must repair current invalid VM extension names before live acceptance; unsupported journeys block. Simulator/emulator latency and detached-process acceptance remain V1 work. make setup installed hooks but full backend wheel sync stalled; isolated Python 3.11 harness dependencies ran the owner checks without any skip flag. Failure-Class: none * harden(dev-harness): scope the spine-contracts check to spine paths The check protects files under the spine roots and their registry, so it only needs to run when those change. A repo-wide trigger would run it, and its full-history requirement, on every unrelated contributor PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(dev-harness): clarify the live journey adapter boundary V1 must refuse live journey execution until a shared executable adapter exists; B1 addressability alone cannot attach Dart integration tests. Pure admission contracts remain valid with injected supported IDs. Verification: harness 287 passed, 7 skipped, 40 strict xfailed; make preflight passed 20 checks. * harden(dev-harness): pin reviewed spine revisions and align B0 capabilities Coordinator turn-4 review authorizes the V1 fixture correction. Exact before/after digests pin the revised oracle; immutable revision records do not permit builder assertion edits. Active tests reject missing/tampered records and prove the fake machine response. Verification: full harness 289 passed, 7 skipped, 40 strict xfailed; UTC app 2020 passed; spine 24 pending/7 protected; make preflight 16 passed. No live device acceptance claimed. * harden(dev-harness): preserve marker retirement across spine revisions Track retained marker occurrences rather than counts so retiring a second test cannot restore the first test's retired marker. The active regression covers this swap and legitimate continued retirement. Verification: full harness 290 passed, 7 skipped, 40 strict xfailed; spine 24 pending/7 protected; make preflight 16 passed. Dart sources unchanged after the earlier UTC app 2020-pass run. * harden(dev-harness): retain corrected spine oracles through squash merges A squash can introduce a corrected contract and its review records in the same commit. Absorb only the revision prefix ending at that introducing file's exact digest, preserving immutable records and later marker-only edits. Active temp-repo regression covers multiple corrections in one squash and rejects restoring old or altered assertions. Verification: harness 291 passed, 7 skipped, 40 strict xfailed; B1 make preflight 20 passed; final B1 journeys 20/20. Dart unchanged after UTC app 41 opt-in and 2051 ordinary passed. * test(dev-harness): revise live session acceptance after builder review * test(dev-harness): bind machine fixture to actual child identity --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> | 6 天前 | |
feat: add JIT knowledge ledger foundation and guarded adoption (#12084) * feat: add JIT knowledge ledger foundation * chore: refresh integration OpenAPI contract * fix: make trigger evaluation release-safe Failure-Class: none * fix: preserve lifecycle semantics in ledger apply Failure-Class: none * feat: adopt guarded JIT knowledge surfaces Route the agent preference writer through the intent-backed ledger, register a privacy-filtered entity timeline tool, render optional evidence on Windows, and add a base-ref-protected Gate F legacy-surface ratchet. Failure-Class: none * feat: add progressive JIT knowledge reads Register owner-scoped current-ledger search and explicit playbook hydration, with pre-limit semantic filtering and bounded outputs. Add a content-free planner/resume migration fixture without claiming canonical transaction completion.\n\nValidation: 141 focused backend tests passed; backend typecheck reported 0 errors; repository preflight passed 120 checks. * feat: render chat evidence on web Render bounded, fail-soft conversation evidence after authoritative answers in both web chat entry points. Unsupported, future, duplicate, and raw failure details remain inert.\n\nValidation: 337 web tests passed; web typecheck, oxlint, and Prettier passed; repository preflight passed 120 checks. * feat: require intent-backed ledger search results Apply the intent-backed requirement at the final merged canonical/history filter, with a passive historical-row regression case.\n\nValidation: 54 focused backend tests passed. * test: keep agent tool isolation stubs current * feat: gate JIT conversation retrieval * fix: make entity timeline scans deterministic Failure-Class: none * fix: honor rejected ledger projections Failure-Class: none * feat: render inert screen evidence on web * fix: reuse canonical review projection Failure-Class: none * fix(web): await recap context effect Failure-Class: none * test: amortize preference tool isolation load Failure-Class: none * feat(app): add knowledge ledger review surface Failure-Class: none * feat(macos): use canonical ledger prompt projection Failure-Class: none * test(memory): classify legacy surface inventory roles Failure-Class: none * fix(app): preserve ledger history completeness state Failure-Class: none * feat(macos): preserve canonical ledger mirror metadata Failure-Class: none * fix(app): match canonical ledger ordering Failure-Class: none * test(api): prove ledger client schema parity Failure-Class: none * feat(memory): expose bounded ledger history Failure-Class: none * chore(api): generate ledger history clients Failure-Class: none * feat(retrieval): add bounded card participants Failure-Class: none * feat(macos): project ledger trigger watchlist Failure-Class: none * fix(clients): fail closed on ledger authority Failure-Class: none * feat(macos): expose bounded trigger snapshot Failure-Class: none * fix(memory): keep closed history read only Failure-Class: none * feat(app): disclose partial ledger history Failure-Class: none * test(macos): cover ledger trigger bridge Failure-Class: none * fix(app): use neutral ledger accents Failure-Class: none * chore(api): declare ledger history route policy Failure-Class: none * test(macos): remove unsafe JSON fixture unwraps Failure-Class: none * fix(memory): satisfy typed history boundary Failure-Class: none * fix(macos): require prompt snapshot authority Failure-Class: none * test(memory): prove ledger migration on emulator Failure-Class: none * feat(retrieval): emit bounded screen evidence Failure-Class: none * test(memory): classify maintenance retirement readiness Failure-Class: none * feat(macos): adapt Rewind metadata for triggers Failure-Class: none * test(retrieval): align screen timestamp contract Failure-Class: none * feat(memory): correct ledger facts by amendment Failure-Class: none * test(memory): prove ledger correction on emulator Failure-Class: none * feat(macos): harden local trigger observations Failure-Class: none * feat(agent): search bounded historical facts Failure-Class: none * test(macos): cover trigger observation adapter * fix(memory): gate historical fact retrieval * feat(memory): add gated JIT retrieval strategy * test(memory): prove mixed-version JIT runtime parity * chore(memory): keep JIT gate exports type-safe * refactor(memory): isolate JIT prompt contract * fix(conversations): round-trip owner-scoped references Accept the conversation:<id> references emitted by JIT result cards while retaining strict UUID-only bare IDs and share links. Restrict machine IDs to a bounded safe alphabet so evidence suffixes and path-like values fail closed. Failure-Class: none * test(memory): join JIT citations to evidence envelope * fix(retrieval): enforce JIT conversation search budget Cap JIT summary searches per request and bound database hydration to the projection limit before reads. Preserve the legacy path when JIT is disabled. Failure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep JIT retrieval request scoped * test(macos): prove future JIT evidence stays inert * fix(memory): keep JIT card citations request-global Failure-Class: new * fix(retrieval): separate JIT hydration from search Treat gated owner-scoped references as exact hydration without searching transcript text for the reference. Charge every JIT candidate search to the shared four-search request budget, including snippet-bearing requests, while keeping exact hydration free and preserving released JIT-off UUID/share-link behavior.\n\nVerified:\n- cd backend && ./.venv/bin/python -m pytest tests/unit/test_conversation_jit_processing.py tests/unit/test_conversation_exact_reference_search.py -q (58 passed)\n- cd backend && uvx --from pyright==1.1.403 pyright -p pyrightconfig.json --pythonpath .venv/bin/python (0 errors)\n- git diff --check\n\nFailure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep repeated JIT cards index-safe * fix(retrieval): satisfy JIT card type contract * fix(retrieval): hydrate collected JIT cards * test(app): preserve answers during delayed evidence requests * test(app): exercise production evidence composition * feat(memories): restore superseded ledger facts * fix(memories): reconcile reverted ledger facts * feat(memories): append reverted ledger facts * feat(memories): synchronize revert client contract * fix(memory): name ledger revert identity * fix(memories): type and enlarge revert controls * fix(memories): fence revert retries and refreshes * fix(memories): fence ledger revert authority * test(memory): count ledger revert rate limit * feat: expose agent-controlled historical facts * feat: reopen standalone ledger facts * feat: add fail-closed JIT QA bundle routing * feat: add safe local JIT QA backend stack * fix: harden isolated JIT QA stack * feat: add explicit multi-source entity timeline * feat(backend): add JIT rollout authority * feat(backend): fence every proactive paid boundary * fix(backend): release proactive quota on cancellation Release the reserved proactive quota exactly once when cancellation interrupts paid-boundary refresh or a provider retry, then re-raise cancellation without emitting retry telemetry. Add deterministic regression coverage for both cancellation points. Failure-Class: FC-proactive-quota-cancellation | new * fix(backend): make proactive quota cancellation safe Detach in-flight Redis reservations on request cancellation and release only admitted slots once they settle. Move direct-provider fallback telemetry behind the fresh paid-boundary rollout check so late kill or unknown decisions cannot report false recovery.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): preserve quota compensation during shutdown Keep late Redis reservation compensators outside the ordinary cancellable background-task drain. Desktop and main application shutdown paths now wait for these critical compensators before cancelling ordinary work, with deterministic blocked-thread and lifecycle-order regressions.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): use expiring proactive quota leases * fix(backend): make quota finalization clock-safe * fix(backend): isolate jit rollout control plane * fix(backend): close jit control plane safely * fix(backend): emit retry recovery after quota commit * test(backend): keep rollout app contract fast * feat(jit): add guarded proactivity and first-open policies * chore(desktop): mark jit policy as internal * test(desktop): cover jit proactivity policy flow * feat(backend): wire durable JIT first-open processing * feat(desktop): fence JIT proactivity runtime admission * feat: activate authoritative JIT proactivity runtime * fix: harden JIT proactivity authority * fix: close proactive runtime authority gaps * fix(jit): make first-open effects resumable * fix(jit): fence outstanding first-open work * fix(jit): resume app usage receipts * fix(jit): make app usage retries no-op Failure-Class: none * fix(jit): allow completed usage after app deletion Failure-Class: none * fix(jit): register first-open folder query Failure-Class: none * Fix first-open import isolation * feat(memory): govern ledger slots and prompt winners * feat(macos): stage guarded ledger prompt adoption * feat(jit): adopt authoritative ledger prompts on macOS * fix(jit): close ledger adoption authority leaks * fix(jit): reauthorize every ledger migration write * fix(jit): fence ledger cutover publication * fix: keep ledger prompt rollback reversible * feat(jit): add guarded frame request retention contracts * fix(jit): close frame retention authority and evidence lifecycle * fix(jit): make frame retention retries and cleanup durable * fix(jit): make frame evidence recovery and retention complete * fix(jit): close frame retention recovery gaps * Harden temporary frame retention and deployment * fix: harden JIT frame retention and consumption * fix: close JIT frame lifecycle recovery gaps * fix: unify JIT frame authority and retention Failure-Class: FC-split-mutation-authority * docs: keep frame retention guidance lean * fix: retire duplicate frame flag bindings Failure-Class: FC-split-mutation-authority * fix: register frame keyframe queries Failure-Class: FC-split-mutation-authority * fix: serialize frame retention deploys Failure-Class: FC-split-mutation-authority * test: cover frame pixel deletion ordering * style: format cumulative Dart changes * fix(app): retain permanent conversation photo fetches * fix: bound frame vision retention and authority * fix: drain terminal frame request metadata * chore: record internal ledger adoption change * feat(memory): add dark daily sweep authority * feat(memory): harden daily sweep fences and runtime seam * feat(memory): reconcile existing standing triggers in sweep adapter * fix(memory): harden daily sweep recovery and source fences * fix(memory): close daily sweep source producers * fix(memory): close daily sweep review findings * Add dark daily memory sweep authority and recovery * fix(memory): harden daily sweep rejection repairs * test(listen): stub onboarding admission in bootstrap regression The daily sweep PR fences onboarding mode behind the server-owned backend admission (get_backend_onboarding_admission), so the bootstrap regression test now simulates an admitted session instead of failing closed on a real Firestore read. Verification: focused test passes in 1.64s (previously failed after a 4m27s Firestore timeout); full test_listen_runtime_regressions.py + test_onboarding_question_start.py: 26 passed; black --check clean. * fix(memory): close daily sweep rollout and retry cursors * fix(memory): isolate daily sweep lifecycle and retry fairness * Harden daily sweep admission and completed-day staging * fix daily memory sweep reliability boundaries * preserve daily sweep invocation tombstones * close daily sweep invocation lifecycle fences * fix: keep daily sweep lifecycle cleanup active * fix: acquire ledger snapshot client off event loop * fix(memory): preserve migration tier fence without legacy growth * test(memory): prove legacy adjudication race fences * fix(dev): allow bounded ADC readiness refresh * test: keep ledger prepush deterministic * test(memory): register prompt receipt control path * fix(memory): fence ledger writer transitions * feat(backend): preserve closed ledger history in export * feat(memory): define ledger query semantics * fix(backend): fence trigger snapshots on final authority * fix(backend): bypass stale coalesced JIT refreshes * feat(macos): mirror bounded memory evidence Decode generated v3 evidence into a domain mirror, persist canonical bounded JSON through the memory cache, and preserve it across compatibility sync and older-local conflicts. Invalid, future-shaped, oversized, and over-count payloads fail closed without hiding memory text or granting prompt authority. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): fence and classify memory evidence Keep generated memory fields independent from malformed evidence, distinguish absent valid and invalid evidence states, preserve prior evidence on invalid payloads, and gate replacements on a monotonic server timestamp so stale active evidence cannot resurrect redacted rows. Cover populated-table migration upgrades. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): preserve evidence fences and scrub redactions Advance evidence revisions for identical valid payloads, fence stale active responses after a local edit, and remove artifact/device pointers from redacted evidence before canonical persistence. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * chore(macos): record ledger evidence mirror * feat(macos): deep-link local evidence cards to Rewind * fix(macos): fence Rewind frame evidence version * fix(macos): validate Rewind evidence card availability * fix(macos): bind task detail Rewind navigation to local leases * fix(macos): fence Rewind citation owner handoff * chore(macos): register Rewind evidence deep links * test(macos): cover Rewind evidence navigation * feat(desktop): evaluate JIT trigger watchlists locally * feat(desktop): wire authoritative JIT trigger runtime * feat(desktop): bind JIT claims to snapshot authority * fix(desktop): revalidate trigger authority at execution * fix(desktop): keep JIT execution leases live * test(memory): bind standalone reopen to direct-user writer * fix: make JIT QA sign-in self-contained Failure-Class: new Verification: bash desktop/macos/tests/test-jit-qa-target.sh; bash desktop/macos/tests/test-yolo-dev-backend.sh; repaired named-bundle Google sign-in reached authenticated onboarding. * feat(memory): complete JIT policy and native Windows parity * docs(backend): keep service map within context budget * test(macos): cover JIT client and staging flows * chore(backend): declare JIT mirror route policy * fix(backend): use strict Firestore boundary for JIT admission Failure-Class: FC-malformed-doc-read * chore(quality): register malformed-document guard surface * fix(backend): fail closed on malformed JIT authority Failure-Class: FC-malformed-doc-read * refactor(backend): name JIT workflow boundary results * test: repair JIT CI contracts * fix(backend): preserve ledger query exports Retain the explicit same-name re-exports consumed by tests and downstream callers while satisfying the enforced Pyright unused-import boundary after the main rebase. Failure-Class: none * test(backend): isolate gateway setup timing Failure-Class: none * style(memory): format direct-user evidence path Failure-Class: none * test(agent): isolate ACP process-group fallback Failure-Class: none * fix(dev-harness): preserve ownership markers in narrow CI * test(jit): refresh emulator fixtures for current contracts * test(jit): orchestrate local rollout dogfood * test(jit): harden local dogfood authority * fix(dev-harness): install PostHog for CI tests * fix(chat): project server JIT rollout into retrieval Resolve the backend-owned PostHog decision inside the bounded agent setup path and pass only its boolean result to prompt/tool configuration. Unknown or failed authority remains on the released legacy path, while callers cannot self-enroll through configurable input.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: new * fix(memory): preserve preference writer compatibility Select the agent preference write path from the canonical per-user writer control. Default compatibility mode retains the released MemoryService payload and receipt behavior; ledger mode keeps the retry-stable ledger write, and transition states fail closed.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: FC-split-mutation-authority * fix(jit): separate migration rollout authority Keep staged JIT chat and proactive exposure independent from legacy-row migration and writer cutover. Migration now requires its own default-off PostHog flag and still rechecks the shared kill switch at every mutation and publication boundary. Repair the isolated conversation-JIT fixture for main's chat-scope import. Verification: 217 focused JIT, chat-scope, migration, and lifecycle tests passed; 28 conversation-JIT fixture tests passed; independent Sol review accepted the split for QA-only dev rollout. Failure-Class: FC-split-mutation-authority * fix(photos): preserve retained image retrieval Treat an empty legacy inline marker as absent when permanent storage is authoritative, while malformed non-empty inline payloads still fail closed. Route live and retained thumbnails through the storage-aware image loader and preserve the conversation identity through the full-screen viewer.\n\nVerification: backend data-export tests 32 passed; Flutter photo-viewer tests 5 passed; focused Dart analysis clean; independent Sol review found and verified the viewer identity repair.\n\nFailure-Class: none * fix(memory): keep disabled daily sweep dark Resolve the backend-owned authority before inventory and require its literal true decision before any UID discovery, registry, cleanup, scheduler, model, or commit work. Missing, malformed, throwing, disabled, and kill-switched authority now exits without touching user data; enabled behavior is preserved.\n\nVerification: 60 focused daily-sweep job, scheduler, and inventory tests passed; independent Sol review accepted the fail-closed gate.\n\nFailure-Class: FC-split-mutation-authority * fix(jit): satisfy fail-closed type contracts * test(backend): admit full runtime contract checks * style(backend): format conversation bound test * test(backend): keep conversation router isolation current * test(backend): admit export boundary duration * fix(macos): persist failed chat turn notice Failure-Class: none * fix(macos): repair JIT rollout admission contracts Failure-Class: none * fix(windows): treat JIT screen evidence as untrusted Failure-Class: none * fix(backend): preserve explicit app failure contract Failure-Class: none * fix(app): finish photo viewer consolidation * fix(backend): make provider writes lock-free against the deletion gate The account-wide legal-hold deletion gate wrapped every GCS upload and Pinecone/Typesense upsert in an exclusive per-uid Firestore mutex with no lease: concurrent same-account writes hard-failed (dropped audio, lost vectors) and a crash between acquire and finish blocked the account's gated operations forever, with no janitor. Provider writes now use a lock-free fence that refuses only during account deletion or a live destructive operation; destructive kinds keep exclusive ownership, an abandoned gate self-expires after six hours, and releasing a gate on the failure path can no longer mask the original error. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): issue onboarding admission at socket connect The completed-onboarding early exit returned False from an Optional[str] function; the listen runtime derives admission via 'is not None', so users who had already completed onboarding were admitted with a fabricated session id — the exact provenance forgery the admission exists to prevent. Separately, the 20-minute admission TTL was anchored to the app-launch state read, so a user reaching the speech-profile step late (or any client that never calls the state endpoint) silently lost onboarding questions and is_user tagging. The bootstrap now issues or refreshes the admission from the durable account state at connect time; completed accounts still can never re-enter, and issuing stays best-effort with the read failing closed. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep the released proactivity lane open for legacy clients Gating /v1/desktop/proactivity/completions on the JIT cohort returned 403 to every non-admitted user — which is the entire deployed desktop fleet on deploy day, since shipped clients poll this route continuously and treat 403 as a plain error. Context-bucket extraction and the director would have died fleet-wide, dark cohort or not, and any environment without a PostHog key (local, self-host) would have lost the lane entirely. The route returns to merge-base admission semantics (tier quotas only); JIT admission remains enforced on the JIT reservation routes, and retiring this lane stays a later explicit operation after clients migrate. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): withhold JIT tools and history reads outside the rollout Five new tools (search_knowledge, search_historical_facts, read_playbook, get_entity_timeline, look_at_frame) sat unconditionally in CORE_TOOLS, so every legacy chat request carried their schemas and the model burned tool budget on 'no entries found' answers. They are now filtered per request off the same resolved rollout boolean that gates the JIT prompt appendix. The memories-tab ledger-history endpoint likewise answered every user with a bounded 501-row provider scan that can only ever be empty outside the rollout; it now returns empty without the scan for non-admitted (and unknown/error) states. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): bound rollout control-plane cost and confine sync resolution Synchronous callers resolved rollout flags via per-call asyncio.run against the shared provider singleton, crossing event loops: awaiting a Task attached to another loop raises, a timed-out asyncio.run strands a coalescer entry that then serves stale UNKNOWN forever, and the LRU cache was mutated from multiple threads. Sync resolution now runs on one long-lived control-loop thread with its own authority instance. Unknown snapshots gain a 5-second negative cache — UNKNOWN can never authorize work, and without it a fleet whose flags are simply absent pays one uncached PostHog call per conversation finalization. The screen-sync loop drops its force_refresh (one uncached decide per device per minute fleet-wide) and moves to its own rate bucket so two Macs' background sync can no longer starve conversation photo reads out of the shared 120/hour frame-requests bucket. The first-open policy's kill-switch telemetry label also reported str(Enum) instead of the value and could never match. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): skip eager extraction under a non-compatibility writer mode A ledger-cutover user still ran the full L1 extraction model call at finalization, after which writer admission refused the compatibility write — the conflict retried, exhausted, and failed the entire finalization for every conversation, with the model spend already paid. Extraction now checks the canonical writer mode first and skips when the daily sweep owns memory formation; only a positively-read non-compatibility mode skips, so any control-state read failure preserves the legacy eager path. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): export tolerates byte-less legacy photo rows A conversation photo row carrying the legacy empty inline marker and no storage reference failed the whole portability export forever, though it holds no durable image anywhere — there is nothing to omit. Such rows now export as metadata with a content-free gap reason. Frame requests in a retained state keep the fail-closed contract via an explicit require_bytes parameter. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(windows): harden JIT delivery, admission, and bootstrap boundaries Five verified defects: (1) the exclusive notification delivery slot leaked on any throw between reservation and commit — one SQLite hiccup during a JIT turn permanently silenced every proactive lane; the span is now try/finally-guarded and stale slots expire after ten minutes. (2) The ambient lane interpolated the raw window title into a tool-capable agent prompt; the turn now carries only the opaque context handle plus a sanitized executable name, framed as untrusted data like the nano-triage lane. (3) Google Calendar was fetched every ~60s before admission, so non-cohort users with Google connected paid ~1,440 reads a day for a refused feature; observation now gates calendar evidence on the cached authority. (4) Rollout-authority errors reset the cache and retried every frame (~1 req/s offline, forever); failures now back off from 30s to 10 minutes. (5) An unguarded JIT schema exec inside the shared database open could abort local storage for all features; the mirror bootstrap is now isolated, keeps the host-facing tables alive, and JIT stays inert when unavailable. Also re-checks the control-plane owner before committing the toast so an account switch mid-turn cannot show the previous owner's advice. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(macos): restore screen provenance, guard migrations, fence chat turns Four verified defects: (1) every pre-existing screen-derived task lost its 'Screen context / Open Rewind' source row because the new evidence policy dropped any provenance that is not rewind_frame.v1; the merge-base fallback row is restored for capture.v2/legacy refs (a test flipped to match the regression is restored to its merge-base assertions). (2) RewindDatabase published its pool before migrating, latching a failed migration into a permanent false-initialized state, and three unguarded ALTER TABLE memories migrations died with duplicate-column on machines that ran earlier builds of this branch; migration now precedes publication and the ALTERs/CREATEs are existence-guarded. (3) EventKit was queried on every context visit before the flags check; non-admitted owners now build no observation inputs. (4) A failed chat turn's reconstructed notice could be appended into a different conversation's transcript when the user switched sessions or cleared chat mid-flight; both transcript resets now revoke the active turn like selectApp already did. The pre-terminalized discard class (user Stop/watchdog) still drops the durable notice on relaunch — pinned by a characterization test in agent/tests/conversation-journal.test.ts with the least-invasive fix described there. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(testing): resolve firebase-tools from the checked-in dependency npx --prefix resolves the package bin against the current directory on some npm versions, and the admission runner deliberately launches from an isolated temp dir (firebase writes debug logs to cwd) — surfacing as 'sh: firebase: command not found' on hosts without brew node@22. Prefer the vendored node_modules binary when it matches the pin; npx remains the fallback. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): keep one eager-extraction call site for the surface ratchet Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): gate eager extraction at the public boundary The writer-mode skip moves from _extract_memories_inner to extract_memories: the replace-policy contract test pins the inner helper to exactly the canonical replacement path, and the public boundary is the better seam anyway — a sweep-owned user now skips parity capture and usage tracking along with the model call. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): stub onboarding admission issuance in bootstrap regression The connect-time ensure call landed in a harness that only stubbed the read, so the bootstrap test paid an extra real-module exception path and grazed the 0.30s fast-unit CPU budget under fanout load. Stub the issuance like the read. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): allowlist the bootstrap regression's CPU budget The full listen-runtime bootstrap test measures exactly at the 0.30s fast-unit CPU budget under a saturated pre-push fanout (CPU inflates ~2x there per the guard's own notes) while passing comfortably alone. It exercises deliberately heavyweight machinery; record it as an intentional exception rather than trimming the coverage. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep list(CORE_TOOLS) literal through JIT tool gating The JIT-only tool filter replaced the list(CORE_TOOLS) assignment with an inline comprehension, which broke the prompt-cache structural invariant (test_prompt_cache_optimization.py::test_core_tools_used_in_both_functions). Restore the list(CORE_TOOLS) copy and apply the JIT-only filter as a conditional pass, preserving rollout semantics and tool order. * feat(jit): drop automatic goal updates from the JIT featureset Product decision (David, 2026-08-26): goals change only through explicit user action for JIT-admitted conversations. Goal progress is no longer a first-open obligation — the effect is removed from FIRST_OPEN_EFFECTS and the worker, and the policy plan can no longer express deferring it. Legacy obligations carrying a pending goal_progress row are normalized away and complete on the remaining two effects. Non-JIT (legacy eager) conversations keep today's automatic goal updates unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): one summary-spine agent pass per day, with folder backstop Replaces the per-conversation transcript extractor in the completed-day producer with a single two-phase agent run: the whole day's conversation summaries go in as one bounded spine (200 conversations / 120k chars — effectively unreachable, so heavy days no longer stall the cursor), and the agent may request up to 8 raw transcript excerpts (8k chars each) to verify specifics before finalizing. At most two provider calls per user per day, both inside the existing at-most-once invocation fence; the staged page carries the memory candidates AND folder assignments for the day's unopened, unfiled conversations, applied idempotently (first-open or user assignment always wins). Memories must cite their source conversations; uncited output is dropped. The cost gate becomes a worst-case ceiling checked before any call. The onboarding cold-start channel keeps per-conversation transcript extraction unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): harden the daily agent prompts from a real-data lab pass Iterated on one real heavy day (26 conversations) with strong- and weak-model stand-ins, an adversarial judge, and hand-verified transcript ground truths. Rules added, each pinned to an observed failure: actor binding in active voice with a personal-attribute gate (a discussed or recommended topic is never someone's attribute; judgments about named people are stored as assessments); decision-state basis labels binding the verb (decided/proposed/observed, discussed-no-outcome dropped); salience ordering (money, metrics, named-party intent, identity, and durable decisions before any operational fact; one fact per memory); never guessing the direction of an invitation/offer/commitment (verify or drop); and no deferring the whole answer to verification. The agent output schema gains a 'basis' field. The memories QoS call-site inventories now count the daily-sweep agent's call site (3 -> 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): tune the daily agent prompts against the real memories model Ran the assembled prompts against gpt-5.6-luna (the real 'memories' route model) on the same real day. Three refinements from observed behavior: the basis label no longer leaks into memory text (metrics read as metrics, not 'David observed that…'); the never-guess-direction trigger is mechanical (passive/verbless summary phrasing or 'Speaker' as the actor forces a transcript_request — luna confidently inverted 'Tim: Invited to New York' until this; with it, phase B verifies and corrects to the true direction), hedging is itself a request signal, and nothing high-salience may be silently dropped; and a rich-day yield anchor (8-16 memories for 15+ conversations) counters the model's over-pruning without inviting padding. Final real-model run: 11 true memories + 2 legitimate verification requests, zero fabrications, ~22k tokens (~2 calls) for a 26-conversation day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): profile-maintaining slots, ledger lookups, cache-ready prompts The daily agent now sees the user's current profile (the same get_prompt_memories seam chat uses — the ledger render for migrated users), may run up to 4 owner-scoped prior-memory keyword lookups (provider fail-soft; hits re-read through the canonical store before disclosure) to dedup and supersede, and may name a slot for standing attributes — an occupied slot becomes an amend through the existing canonical occupancy check, so the daily run maintains the rendered profile with no second write path. Both phase prompts share a byte-identical prefix (pinned by a test) and pass a per-user prompt_cache_key through get_llm; measured against gpt-5.6-luna the provider cache is exact-match rather than prefix-based today, so this is future-proofing rather than present savings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): type the memory-searcher seam for the pyright contract CI's authoritative typecheck rejected the untyped lookup seam (memories.py: list(Any or [])). The searcher is now Optional[Callable[[str], Sequence[str]]] and results are built through a typed comprehension; behavior unchanged (absent or failing searcher still degrades to an empty result block). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair four main-inherited CI breakages after sync origin/main is currently red on its own tip; syncing it into this PR inherits the breakage, so the fixes ride here: - subscription.py: drop the unused get_byok_keys import (pyright reportUnusedImport fails the Backend unit suite). - AppState+Transcription.swift: explicit self for alertPresenter inside the escaping showAlert completion (strict-concurrency compile error in all three Desktop Swift lanes, shipped red on main by d49f978512). - AppState+Permissions.swift: pinned swift-format drift from the same main commit (desktop-swift-format-lint). - web/app/bun.lock: add the prettier + prettier-plugin-tailwindcss entries 64db30c791 pinned in package.json without updating the lockfile (frozen install fails web-app-checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): close the second review round's findings Three parallel adversarial reviews over the post-takeover additions: - Clamp every model-controlled phase-B input (draft memories, request reasons, lookup queries/results) and add the clamped worst case to the pre-call cost ceiling, which previously under-estimated phase B. - Attest an empty consumed day when the staged page carries an older stage schema version instead of stalling the cursor forever on every deploy-boundary schema bump. - Make the folder backstop's unfiled check and write share one transaction so a concurrent first-open/user assignment always wins. - Let equal-rank sweep candidates amend sweep-authored slot occupants: the profile-maintenance path froze after a slot's first write. User statements still always win; slotless subject matches still dedup. - Neutralize ``` fences in summaries/excerpts/lookup results, and mark raw-transcript fallback rows '(unstructured transcript excerpt)' with a prompt rule refusing slots/personal attributes from them without transcript verification (test pins the marker to the rule). - Remove the dead first-open goal-authority threading left by the goals removal, and update the stale jit-first-open-runtime doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair three more main-inherited breakages All shipped red on main and only surfaced once earlier failures were cleared: - AppState.swift: move the alertPresenter default out of the stored property initializer — Xcode 16.4's SILGen segfaults (signal 11) emitting it, which failed all three Desktop Swift lanes even after the explicit-self fix. - test_byok_security.py: main's BYOK rewrite (d0e3a4eb3a, 1da8880175) changed request_has_llm_byok_key to per-provider enrollment checks and made partial headers fail closed, but left the tests targeting the old get_byok_keys()-based lenient contract (masked on main because pyright failed before pytest ran). The tests now assert the shipped strict contract their own docstrings already describe. - subscription.py: pinned-black formatting for the BYOK fallback expression (the Formatting lane rejects the file as main wrote it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): stub the chat-agent gateway route pin in the chat router harness Main's a6988be309 made routers.chat import CHAT_AGENT_ROUTE_DIRECT / get_chat_agent_route from utils.llm.gateway_client, but the chat-router test harness (and test_chat_file_upload_unsupported's local override) stub utils.llm.gateway_client without those symbols, so every suite that loads the real router failed at import — masked on main because pyright fails its Backend unit suite before pytest runs. Ninth main-inherited repair in this sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): teach test_chat_quota's utils.byok stub the rewritten import surface utils/subscription.py now imports get_byok_uid and get_cached_byok_state (main's BYOK rewrite); the module-scoped utils.byok fake predates them, so reloading subscription under the fake raised ImportError at setup — and the polluted process took test_chat_openapi_operation_ids and test_desktop_screen_crisp down with it in CI's batched run (all three pass standalone). Tenth main-inherited repair, same pyright-masked pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): update three more suites for main's BYOK/gateway import surface Same pyright-masked pattern as the harness and test_chat_quota repairs: - test_desktop_transcribe stubbed utils.llm as a non-package, so routers.chat's new utils.llm.gateway_client import could not resolve (50 failures); the submodule is now in its stub list. - test_paywall_reconnect_gate's BYOK escape-hatch tests never set the request uid context that the enrollment-verifying rewrite requires (middleware sets it in production); they now do, and teardown clears it. - test_chat_session_app_identity's enforce_chat_quota stub rejected the new required_llm_provider keyword. All three suites pass locally (69 + 35 + 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): enroll fingerprints in the desktop BYOK tests PR #11454 moved macOS BYOK activation to enrollment-verified fingerprints (isByokActive and usableBYOKEnvironment gate on persistEnrolledFingerprints), and its own test lanes shipped red: the tests store raw keys but never enroll them, so every key reads as inactive. Their teardowns already clear enrollment — the setups now enroll what they store, matching the production activation path. All 8 previously-failing cases (BYOKPaywallTests + the two AgentRuntimeProcessTests BYOK-environment cases) pass locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deploy): enable the daily memory sweep on development The sweep's five deployment inputs were pinned off in every environment, so cohort enrolment alone could never start it -- turning it on for a dogfood account required a second PR. Development now carries the live values: - ENABLED/MODEL_ENABLED on, so the job stops exiting at its first authority gate and the model authority can budget a route. - MODEL_NAME pinned to gpt-5.6-luna, which is the declaration interlock the runner checks against get_model('memories') before any provider call. - MAX_MODEL_COST_USD 0.80, the worst-case pre-call ceiling for a maximal day including phase B's clamped draft/reason/lookup overhead. - COHORT_ENABLED on with COHORT_FLAG daily-memory-sweep-v1, so enrolment is a per-uid PostHog boolean and an unnamed cohort stays a closed rollout. Production is deliberately untouched and stays fully pinned off. The job still cannot form a memory for anyone until that flag exists and resolves true for a uid, which remains a control-plane action rather than a deployment one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(firestore): terminate the daily-sweep occupant indexes with __name__ The six daily-sweep occupant lookups were the only declarations in the manifest without a trailing __name__ field -- 63 of 69 entries carry one, and main had none missing it. Firestore appends the terminator itself and reports the index back that way, so these six could never match the live inventory. The failure mode is not a missing index; the indexes build fine. It is that reconciliation never converges: every run reports the same six as missing, tries to create them, and fails on ALREADY_EXISTS. That takes down the Firestore schema workflow on both environments permanently, and with it the development backend deploy's readiness gate -- the same class of outage the workflow's own header records from the hourly_usage index in PR #11979. The derived specs previously appended their extra predicates to the base spec's index_fields, which would have placed them after the terminator, so the shared prefixes are now named explicitly and each spec ends with __name__. Verified against real Firestore: reconciliation reports zero missing indexes in both based-hardware and based-hardware-dev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: close final JIT rollout and CI gaps Fence direct JIT tools and frame pixels, keep Windows account wipes safe after optional schema failures, and repair inherited CI regressions. Failure-Class: none --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 27 天前 | |
Speaker identification: measured threshold + margin, live clip pooling, SpeechBrain retirement; carries #12531 without the onboarding-step removal (#12935) * fix: unblock speech-profile redo and STT pre-flight for already-onboarded accounts Rebased onto origin/main as a single commit. Keep both main's open_provider_selection_circuit and this PR's is_stt_available helpers, then regenerate OpenAPI clients from the rebased backend. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(app): fade transcript words in as they arrive on the speech-profile screens Add FadeInWordsText: a centered word Wrap where only the words appended since the previous render animate from transparent to opaque with a short stagger, existing words stay put, and a rewritten transcript re-reveals from the start. Both the onboarding speech-profile step and the Settings redo page adopt it in the next commit so the live transcript reads the same whether the words come from the server or the on-device fallback. Verification: flutter test test/widgets/fade_in_words_text_test.dart (4 passed); observed on an iPhone 16 Pro via hot reload while dictating. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1zmRTRh3DV8NsM3QUrrAB * feat(app): fall back to on-device speech recognition when server STT is unavailable The speech-profile question flow (onboarding step and Settings redo) needs a transcript only to drive the questions and progress; the voice print itself is computed server-side from the WAV uploaded at finalize(). So when the backend's streaming STT is down, transcribe on the phone instead of dead-ending: - SpeechProfileProvider gains a local-STT mode. It is entered up front when the stt-availability pre-flight fails, or mid-session after the existing three 1011 closes with no captured speech (previously STT_UNAVAILABLE). The socket becomes the existing CompositeTranscriptionSocket: an on-device polling primary (Apple speech on iOS, downloaded Whisper on Android) forwarding suggested_transcript frames to the backend listen socket in custom_stt mode, which the OnboardingHandler already consumes like server STT output. No backend change; a receiver regression test pins that seam. - iOS on-device recognition hardening (AppDelegate.swift): resolve the app's bare language code to an installed on-device locale (a recognizer built from "en" failed every request with kAFAssistantErrorDomain 1101); reply exactly once per clip on final result, error, or a 20 s timeout, keeping partial results; and expose onDeviceAvailable, which probes a silent clip so a phone with Siri and Dictation disabled (kLSRErrorDomain 201) is reported as "no local STT" instead of entering the fallback blind. - PurePollingSocket bounds each transcribe() with a 30 s timeout. A provider that never answered left the processing flag set forever and silently stopped transcription for the rest of the session; now the audio is requeued and the next tick retries. This also protects the main app's on-device mode. - When neither server nor on-device STT is available, the pre-flight dialog now says to check the connection or turn on Dictation. - Speech-profile UI: subtler mic-level glow, and the live transcript uses the new fade-in words widget. Verification: - flutter test (full suite): 1731 passed, 5 skipped; scripts/analyze_ratchet.sh passed - new tests: speech_profile_provider_test (5 fallback cases), pure_polling_test (hung-provider timeout), fade_in_words_text_test (4), backend test_onboarding_question_start (suggested_transcript reaches the transcript queue only in custom-STT mode) - live on iPhone 16 Pro against the local dev harness with the STT primary forced unavailable: session connects with custom_stt+onboarding flags, Apple on-device recognition returns the spoken answer (~180 ms per clip) and it is forwarded to the backend; with Dictation disabled the probe reports unavailable and the dialog appears. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1zmRTRh3DV8NsM3QUrrAB * feat(app): use SpeechAnalyzer for on-device speech on iOS 26 SFSpeechRecognizer's on-device mode fails with kLSRErrorDomain 201 whenever Siri and Dictation are turned off in Settings, which is what produced the "turn on Dictation" pre-flight dialog in the speech-profile fallback. iOS 26's SpeechAnalyzer/SpeechTranscriber has no such dependency: the language model is an asset the app installs itself through AssetInventory. - transcribe: on iOS 26 run the clip through SpeechAnalyzer (preset .transcription, analyzeSequence(from:) + finalizeAndFinish), falling back to the SFSpeechRecognizer path only if the analyzer throws. - onDeviceAvailable: report true when a supported locale's model is installed or installs within 8 s; a longer download keeps going in the background and the first transcribe() waits for it. Concurrent callers share one download. - SFSpeechRecognizer remains the path for iOS 15-18. Verified on an iPhone (iOS 26.6.1) with Dictation off: the speech-profile redo enters local-STT mode and transcribes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): speech profile talks through three topics and completes on a word target Speech-profile recording (onboarding step and Settings redo) no longer walks one question at a time with a percentage bar. Instead: - A compact white-outlined card headed "Answer with your voice:" lists three topics (where you live, what you do for work, your long-term goal), and a thin bar under it fills as the user speaks. Reaching SpeechProfileProvider.targetWordCount (60 spoken words) finalizes the recording; the backend's onboarding_complete event no longer does, so "bar full" and "done" are the same moment. Omi's own question segments are excluded from the count. - The live transcript is bottom-anchored in a box exactly three lines tall above the card, so whole lines scroll off the top and nothing overlaps. - The Play button on the Settings page plays the saved profile audio in place (just_audio) and turns into Stop, instead of opening the samples page. Redo stops playback first. - Backend ONBOARDING_QUESTIONS is the same three topics, and OnboardingHandler keeps the transcript across questions so one stretch of speech can satisfy several of them. - Removed the unused percentage progress-bar widget and the "Skip this question" button; onboarding keeps "Skip for now". Tests: speech_profile_provider_test (word target fills, finalizes once, ignores Omi segments and the backend completion event); backend test_onboarding_talk_about_flow (one transcript answers every topic; the transcript is kept when it stops answering). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(dev-harness): advertise local-storage links on OMI_DEV_HOST A phone built against OMI_DEV_HOST could reach the backend but not the files it links to: OMI_LOCAL_STORAGE_BASE_URL was always http://127.0.0.1:<port>/_local/storage, so playing the saved speech profile from a device failed. The harness now derives a dev_advertise_host from OMI_DEV_HOST (loopback stays the default) and uses it only for that base URL; every other service address still binds and talks over loopback. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the last three whole transcript lines instead of a clipped scroll The speech-profile transcript was a bottom-scrolled ListView clipped to a three-line box, so a sliver of the line above always showed at the top edge and read as cut-off text. FadeInWordsText now takes visibleLines: it replays the Wrap line breaking with measured word widths and builds only the words on the last N lines, so earlier lines drop off whole, nothing is clipped or scrolled, and words keep their reveal state while on screen. Both screens use visibleLines: 3 inside a fixed three-line, bottom-anchored area, moved a little further above the topics card. Test: fade_in_words_text_test covers short text showing everything, earlier lines dropping once the text exceeds three lines, and the shown words matching the line-break replay. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): measure transcript lines with the effective text style; 40-word target FadeInWordsText replayed the Wrap line breaking with the caller's raw style, but each word's Text inherits the ambient DefaultTextStyle (font family, weight) under that style, so the replay undercounted lines and the real layout could reach four lines and draw over the topics card. Measure with the same merged style, and clip the fixed three-line area on both screens as a safety net so a stray line can never overlap the card. Also lower SpeechProfileProvider.targetWordCount from 60 to 40 so the recording finishes sooner. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the "<Name>'s Speech Profile" title on one line The title wrapped onto two lines for longer names; it now scales down to fit a single line instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): never clip the speech-profile transcript; raise it above the card The three-line transcript area was a fixed-height clipped box, so whenever the rendered lines ran taller than the fontSize*height estimate (text scaling, font metrics) the top line was cut off. FadeInWordsText already guarantees at most three lines, so the area now only has a three-line minimum height (scaled with the text scaler) and grows to its content instead of clipping. Both screens also keep more space between the transcript and the topics card. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): finish the speech profile after three sentences; no page-load spinner The recording now completes once the user has spoken three sentences (SpeechProfileProvider.targetSentenceCount, counted on ./!/? boundaries followed by a space or the end of the text, so "3.5" is not one) instead of a word count, and the bar under the topics card fills per sentence. The progress-bar widget is renamed SpeechProgressBar to match. The Settings speech-profile page no longer swaps its Play/Redo or Get Started buttons for a spinner while the page initialises or the STT pre-flight runs; the buttons stay put and startRecording() ignores taps until the check finishes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): play the saved speech profile on the loudspeaker The app's audio session is normally configured for recording, so tapping Play on the Settings speech-profile page routed the WAV to the quiet earpiece. Before playing, configure a playback-category session (default mode, media usage on Android) and play at full volume, so the profile comes out of the main speaker like any other media. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): start every speech-profile recording with an empty transcript Tapping Redo showed the previous recording's words (and counted them toward the sentence target) because nothing cleared the provider's transcript before a new session; only close() did, on leaving the page. initialise() now calls a new resetTranscript() first, which forgets the segments, text, progress, completion and upload flags without touching the audio storage it recreates right after. resetSegments() reuses it. Test: a completed session's transcript is gone after resetTranscript and the fresh session counts sentences from zero and can finalize again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): finish the speech profile after a pause, keeping the last sentence on screen Reaching the third sentence finalized immediately, which stopped the mic mid-utterance (the recognizers punctuate each clip, so a pause can read as a sentence end) and swapped the transcript for a spinner at once. Now: - After the target is reached the provider waits completionGrace (2 s) without new speech before finalizing, restarting the wait on every new segment, and finalizes at completionCap (8 s past the target) at the latest. Once fired it does not re-arm; resetTranscript() clears it. - Both screens keep the last three transcript lines visible through the upload and the All done state, so the final sentence lingers instead of vanishing. Tests: grace/cap timing under fakeAsync, no double finalize, and the reset test now elapses the grace. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(backend): give every speech-profile recording its own conversation Tapping Redo within two minutes of the previous attempt showed last time's words as soon as the user spoke again. The new listen socket attached to the still-open in-progress conversation from the previous attempt (same source, inside conversation_creation_timeout), so combine_segments() merged the first new segment into that conversation's last segment and the merged segment, old text included, was what the client received. LiveConversationController.prepare() now always creates a fresh in-progress conversation for onboarding_mode sessions (the onboarding step and the Settings redo both set it) instead of consulting the in-progress pointer. Ordinary listen sessions are unchanged. Test: test_listen_speech_profile_fresh_conversation.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): cross-fade the speech-profile recording UI into a plain All done button Keeping the transcript on screen through the upload made it pop back in on its own above the spinner and the All done button. The Settings page now cross-fades (450 ms) from the recording UI (transcript, topics card, bar) to nothing while uploading and then to the All done button, which is the same black capsule with a plain white border as the other buttons instead of the gradient box. The onboarding step likewise no longer shows the transcript in its upload and All done states. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * chore(app): remove the speech-samples page and dead progress-state code The Play button now plays the saved profile in place, so the samples page and its provider became unreachable (CI dead-code ratchet). Also drop the scroll controllers and SCROLL_DOWN signal the old clipped transcript used, and the word-count progress-message state (SpeechProfileProgressState, percentageCompleted, questionProgress) nothing reads any more. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): hold the finished speech-profile recording on screen before All done After the third sentence the final words disappeared as soon as the upload began. Both screens now keep the finished recording (last words, topics card, full bar) on screen through the upload and for a further 1.5 s (allDoneHold) after the profile is saved, then cross-fade into the All done button. Onboarding's upload spinner row and its now-unused loading-text helper are gone; Skip for now hides once recording ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): fade the finished speech-profile recording out as one block Parts of the finished recording could change on their own before the cross-fade (the transcript and the mic disclaimer are built from live provider state that finalize() and its callbacks touch), so they did not disappear together. Both screens now snapshot the recording view (last words, no-device flag) the moment recording ends and build from that until a new recording starts, and the onboarding step's All done switch is now the same AnimatedSwitcher cross-fade as the Settings page, so the words, the topics card, the bar and the disclaimer fade out at the same time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): keep the speech-profile bar full until the finished recording fades finalize() clears the provider's text once the profile is saved, and the bar derived its value from that text, so it dropped back to zero before the cross-fade. The frozen recording view now pins the bar at full from the moment recording ends. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * feat(app): drop the speech-profile and memory-graph steps from first-run onboarding Onboarding now goes from Permissions straight to the completion screen. The speech profile is recorded from Settings instead, and the memory-graph preview (with its background graph prebuild) is gone. The two step widgets are deleted; their page indices stay as placeholders like the other retired steps so the existing page constants keep working. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): ease the mic glow shut as the finished speech profile fades The white glow behind the device graphic vanished the instant the upload began. It now stays through the upload and hold and eases down to nothing over the same 450 ms in which the recording view fades into All done. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * fix(app): hold the mic glow at its last size until it eases out The glow followed the live mic level, which drops to zero the instant the microphone stops after the recording ends, so it snapped down to its resting size before the ease-out. The frozen recording view now also captures the last mic level, so the glow holds still and then eases shut with the fade. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017iFNr6rZDcD1FkCS3dJ3Cm * Revert "feat(app): drop the speech-profile and memory-graph steps from first-run onboarding" This reverts commit 4ebcb3d2c773747051dfeb2d519fe88aeb1faea0. * feat(backend): tune speaker verification from measured enrollments and retire the SpeechBrain matcher Speaker identification rejected most of the owner's own audio. The verification threshold (0.45 cosine distance) was copied from a clean-studio VoxCeleb figure; an offline bench over real enrollments in the speech-profiles bucket (229 users with a current profile plus an older one, 16 with extra recordings, 60 taught persons, 400 impostors; wespeaker-voxceleb-resnet34-LM, the diarizer's /v2/embedding model) puts same-user cross-session distance at a median of 0.40-0.53 and other users at 0.93. At 0.45 the owner was rejected 37-71% of the time at a 0.0% false-accept rate; the equal-error threshold is ~0.78. Same-session audio matched at either value, which is why the old constant looked fine in demos. - New utils/stt/speaker_match.py owns the policy (numpy only, shared by the live socket and the sync pipeline): threshold 0.65, plus a 0.10 margin over the runner-up so the owner is not guessed as a taught household member. - Live sessions pool up to three recent clips per diarized speaker and decide on the centroid once 5 s of clip audio has accumulated, instead of letting the first 2 s clip that lands under the threshold stick for the session. - Both surfaces log one structured speaker_id_decision line (best, runner-up, evidence, accepted) so the prod distribution can be checked against the bench from a day of logs. - The bench scripts live in backend/scripts/speaker_id_bench for reruns; user audio never leaves the machine running them. - Retire the dead SpeechBrain speaker-identification path: modal/speech_profile_modal, utils/stt/speech_profile (zero production callers), the /v1/speaker-identification route, HOSTED_SPEECH_PROFILE_API_URL in every chart/env, the speechbrain dependency, the shared-package COPY lines in the modal image, the dev-harness and e2e fakes, and the unused ListenLimits.speaker_id_target_audio field. Drop the now-unused is_same_speaker/find_best_match/bytes helpers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(backend): keep speaker_match real in sync test isolation, allowlist its import cost utils/sync/pipeline.py now imports utils.stt.speaker_match, but the hand-maintained heavy_deps mock list in test_sync_cloud_tasks.py and test_sync_v2.py didn't know about it, so `from utils.stt.speaker_match import select_speaker_match` raised ModuleNotFoundError: 'utils.stt' is not a package once utils.stt was replaced with a MagicMock. Real-import speaker_match (pure, dependency-free, like utils.stt.outcomes) instead of stubbing it, since a MagicMock decision object would also break the %.3f log formatting on decision.best_distance/runner_up_distance. Also allowlist test_speaker_match.py::test_short_clips_are_pooled_before_a_live_decision in the fast-unit duration guard: it's the first test in the file to import routers.listen.speakers, so it amortizes that module's FastAPI router-graph import cost, same structural pattern already documented for other files in the allowlist. * fix(speaker-id): preserve distinct evidence and household ambiguity Serialize live matches per speaker, subtract previously embedded audio, invalidate late session results, and keep all enrolled candidates in sync margin comparisons before enforcing unique assignment. Validation: 396 selected backend tests passed; Python typecheck has zero errors. Five live regression cases and two sync cases failed before the fixes. Changed sync expectations follow PR #12935's measured household-confusion margin. Failure-Class: new * fix(speaker-id): include owners in household benchmark cohorts Include available owner profiles even outside legacy, additional, and impostor cohorts. Distinguish offline benchmark evidence from deployed accuracy. Validation: synthetic manifest regression passes for owners outside other cohorts and people without an owner profile. No private audio or threshold retuning. Failure-Class: new * fix(speech-profile): bound native recognition and discard stale work Use one native completion owner so availability deadlines do not wait for shared model downloads and recognition cleanup precedes timeout completion. Serialize legacy recognition callbacks on the main queue. Propagate native failures to retain audio for retry; remove the polling Future timeout that allowed overlapping work. Scope fallback availability and polling results to their recording session. Validation: full Flutter suite 1836 passed, 5 skipped; analyzer ratchet passed. Native deadline behavioral tests pass and are registered in the existing manifest. Native speech code typechecks for iOS 15 deployment with Flutter boundary stubs; no full iPhone build or live enrollment claim. Preflight passed 53 selected checks. Failure-Class: new * fix(l10n): translate speech-profile flow in every supported locale Translate the eight speech-profile keys across all 48 non-English ARBs and fill two inherited missing keys exposed by generation. Use device-neutral speech recognition guidance and regenerate localization output from source catalogs. Validation: flutter gen-l10n reports zero untranslated messages; owner-name placeholders and complete catalog coverage verified. Full Flutter suite passed. Failure-Class: new * fix(speaker-id): require persisted speech profile before the redo admission bypass cubic P1: the client-supplied speech_profile_redo flag alone proved nothing; any authenticated client could send it to skip the completed-account onboarding-provenance admission gate. The runtime now confirms the redo from durable state (an actually stored speech_profile.wav) before taking the bypass, and an unprovable claim falls through to the provenance admission, failing closed when the check errors. Adds a regression test asserting a redo claim without a persisted profile is judged by the gate. * fix(listen): gate the onboarding fresh-conversation path on server admission cubic P2: onboarding=enabled is a client hint, yet prepare() took the fresh-conversation shortcut on the raw flag even when _bootstrap refused to admit the session — a client could dodge the existing-conversation lookup with a query parameter. The path now requires the runtime's onboarding_admitted (also true for the authorized Settings redo); an unadmitted claim keeps an ordinary session's behavior. Adds a regression test for the unadmitted path. * fix(onboarding): queue segments that arrive during AI answer checks cubic P2: is_checking_answer stayed set across up to three awaited LLM calls in _check_answer, and on_segments_received dropped everything spoken in that window, so answers covering later topics could be lost. Segments received while a check is in flight are now queued and replayed when it finishes, re-entering the normal accumulate-and-timer flow. Adds a regression test. * fix(speaker-id-bench): report the production threshold and true impostor rates cubic P2 x2: score.py evaluated the retired 0.45 operating point while the README and shipped policy (SPEAKER_MATCH_THRESHOLD) sit at 0.65, making its false-reject/false-accept and live-decision numbers misleading; and cohort-C impostor distances included the current user's own owner profile when that user was also sampled as an impostor, folding owner-vs-own-person confusion into the random-impostor sweep. score.py now pins T to the production 0.65 and formats every label from it; sweep.py excludes each cohort-C user's own profile from their impostor pool (the confusion keeps its dedicated diagnostic). Owner profiles for cohort C were already added to the cohort inputs by 8126713612. * fix(speech-profile): close startup, playback, and socket adoption races cubic review follow-ups still present after 1d7a2fd917: - page.dart: _isCheckingAvailability is now held until the entire startup path exits (dialogs, codec lookup, stopDeviceRecording, initialise), not just the availability round-trip, so a second tap cannot race socket and microphone init; context/mounted are rechecked after the language dialog and before initialise. - page.dart: profile playback deactivates the activated audio session on every teardown path (stop, natural completion, failure after activation, disposal) instead of leaving media routing active. - speech_profile_provider: a socket created while the session was closed or reset is discarded instead of adopted, which previously leaked a live backend session stop() never saw. - transcription_service: the speech-profile on-device fallback forwards raw audio per config.sendRawAudioToOmi, matching the conversation composite, instead of hardcoding every frame onto the Omi socket; suggested transcripts still flow and keep the backend session clock alive. Pinned by a factory test. * docs(app): keep AGENTS.md within its lean-budget ratchet after the main merge The merge combined this PR's on-device speech pointer with main's profile-build-mode and batch-contract lines, pushing app/AGENTS.md past its agents-md-lean budget (11747 > 11500 bytes). Tightens wording without dropping any fact: the batch-writer guarantee detail lives in the manifest reason and the ruby test itself; the other compressions are same-fact rewording. --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Nathan Cheng <nathanjcx@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Nathan <nathan@Nathans-MacBook-Air.local> | 16 天前 | |
feat: meeting-note screenshots end to end — gated server egress, macOS, and web (#12135) * feat(desktop): stage meeting screenshots behind local gate Keep the local Rewind selection and presentation prototype unavailable in every distributed bundle. A named development bundle must opt in with OMI_FORCE_MEETING_NOTE_SCREENSHOTS=1, and the store independently fails closed before logging or touching Rewind. Render judge failures silently because the only adjudicator in this change is a manually launched loopback sidecar. Defer the user-facing setting until the server attachment and deletion contract exists. Verification: - xcrun swift build -c debug - xcrun swift test --package-path desktop/macos/Desktop --filter MeetingScreenshotsTests * test(desktop): cover meeting screenshot safeguards Exercise the developer-only gate, disabled-store short circuit, inclusive meeting window, privacy denylist, bucket selection, text and image similarity, and every deterministic adjudication correction measured in the prototype. Verification: - xcrun swift test --package-path desktop/macos/Desktop --filter MeetingScreenshotsTests - python3 desktop/macos/scripts/check_desktop_test_quality.py * test(desktop): pin meeting screenshots dark flow Make the existing hermetic conversation-detail journey own the new screenshot surfaces and assert that an ordinary no-opt-in launch emits no MeetingScreenshots pipeline logs. The enabled prototype remains covered by focused unit tests because this flow intentionally has no judge sidecar. Verification: - python3 desktop/macos/scripts/desktop-flow-lint.py - python3 desktop/macos/scripts/check-e2e-flow-coverage.py --base origin/main --strict * fix(desktop): keep the summary's comments, log the diagnostics, contain the sidecar's paths Three corrections to the staging commit, none of them behavioural in the shipped build. `ConversationDetailView` had lost about twenty lines of existing explanatory comments — the header-title-colour doc, the `// MARK: - Summary Content` marker, and the rationale for rendering `structured.sections` only when Omi's own summary is on screen. Splitting `summaryContent` into before/after halves is a fine way to slot the screenshot layout in, but it is not a reason to delete the reasoning that was already there. The line-count ratchet does not apply here either way: it only bites above 1500 lines and this file is 1430. `diagnostics` was accumulated, cached, and rendered nowhere, which is dead published state. It now also goes to the log, and its doc comment says plainly that nothing renders it yet, why, and that it stays `@Published` for the eventual disclosure rather than because a view reads it. The sidecar joined a request-supplied `video_root` with a request-supplied relative path and opened the result. A `../..` component would have let any local process name any readable file and have its pixels sent to the judge. Binding to loopback is not containment; `contained()` is. Verified: swift build clean under -strict-concurrency=complete -warnings-as-errors; 9/9 MeetingScreenshotsTests; run_checks macos lane 6/6 with 0 swiftlint violations; containment exercised directly against `../../etc/passwd`, `/etc/passwd`, and `a/b.mp4`. * fix(desktop): hash the thumbnail where the thumbnail lives `Desktop Swift Release Compile` rejected `MeetingFrameSimilarity.perceptualHash`: non-sendable result type 'NSImage?' cannot be sent from main actor-isolated context in call to instance method 'thumbnail(for:)' `RewindThumbnailLoader` is `@MainActor` and `NSImage` predates `Sendable`, so a nonisolated caller cannot receive the picture. Do the whole hash on the main actor and return only the `UInt64`, which is `Sendable` — nothing non-`Sendable` crosses an isolation boundary at all. The dHash grid is 9x8, so the draw is negligible work to keep on the main actor, and this is a narrower fix than the `@preconcurrency import AppKit` the sibling Rewind UI files use. Local builds cannot catch this class of error on this machine: CI pins Xcode 16.4 and the only Xcode installed here is 26.6, whose Swift accepts what 6.0 rejects. Both toolchains are named in the code comment so the next reader does not conclude the compiler is wrong. Verified: swift build -c debug and -c release both clean under Xcode 26.6 (-strict-concurrency=complete -warnings-as-errors); 9/9 MeetingScreenshotsTests; swift-format lint clean. The 16.4 result can only come from CI. * feat(desktop): give the banner a hue it actually earned, and a layout that survives a narrow pane The banner's strategy was already right — a designed header with the approved frame as a small inset, because 1,690 of 2,061 measured frames came from Claude Code, Cursor, ChatGPT or Chrome and dense text cropped wide is grey noise. The execution had two real faults. **The hue was a mean over a circle.** Averaging a diff's reds and greens, or a browser's blue chrome against an amber warning, lands on a colour that appears nowhere in the picture. Now it takes the mode — the heaviest bin of a 24-bin histogram — and only then refines by a circular mean of that one bin's members. **An achromatic frame still got a confident colour.** A terminal, a black editor, a plain document: the old code averaged their noise and returned some angle, so the banner was tinted by nothing. Below a chroma floor it now says so, and the caller uses a neutral slate instead of inventing one. This is the more common case on this user's screen, not the edge case. Contrast is measured rather than assumed. Yellow and cyan are far brighter than blue at identical HSB brightness, so a constant brightness cannot keep white title text legible; the ground now darkens until it clears 4.5:1 relative luminance against white. Layout follows the design's 25-35% inset rather than a fixed 176pt, and stacks the inset under the title below 420pt instead of cropping the title to a word and a half. Palette extraction moved to its own file so the arithmetic is testable without a window server, and so nothing non-Sendable crosses an isolation boundary to reach it. Verified: 9 new MeetingBannerPaletteTests, including one that pins the circular-mean bug (70% red + 30% green must resolve to red, not the yellow a mean returns) and one that walks 36 hues asserting measured white-text contrast. swift build clean; swift-format clean. * feat: meeting-note screenshots end to end — server gate, macOS, and web Completes the feature. The client half landed dark because its only adjudicator was a loopback sidecar a developer ran by hand; this builds the server that was designed for it, then the two surfaces that consume it. **The gate is the point.** The client uploads candidate bytes and nothing else. The server canonicalises them, judges those exact bytes with gemini-2.5-flash-lite, and mints an internal approval bound to the canonical digest. `upload_screen_frame_blobs` has exactly one production caller — the writer — which re-derives the digest over the bytes it was handed, verifies every claim and the expiry, and atomically consumes the approval's one-use nonce before anything reaches storage. There is no client-supplied verdict anywhere in the design, so a modified client cannot express an opinion about what may be stored. That is David's ruling R1 ("for this and future screenshot uploads") expressed as a code boundary rather than a convention. The four enforcement rules move server-side, where they belong: each exists because the model was measured violating it, and a rule a client enforces is a rule an attacker skips. **Face policy is an assumption, not a decision.** `identifiable_person` rejects. A meeting participant did not consent to appearing in someone else's stored note, and this is the reversible direction — we can loosen later, we cannot un-store. `REJECT_IDENTIFIABLE_PERSONS` is read by the prompt builder, so flipping it changes behaviour rather than documentation. David has not ruled on this. **The banner is composed from a server-side palette.** Both clients were about to derive it themselves — macOS by sampling pixels, web by blurring a copy of the frame because it cannot sample cross-origin pixels. The blur is the treatment the design explicitly rules out, and two extractions would drift anyway. The server holds the canonical bytes, so it extracts once and both surfaces draw the same two stops. Its algorithm is a port of the macOS one, keeping both properties that exist for measured reasons: the mode rather than a mean over the hue circle, and admitting when a frame is achromatic instead of inventing a hue for a terminal. **An all-rejected pass is now recorded.** `revision` only moves when a frame is approved, so a conversation whose every candidate was refused read as "never attempted" and the client re-uploaded on every reopen — and what it re-uploaded was precisely what the judge refused: the credentials, the DM window, the inbox. A privacy gate that re-ships its own rejects on a loop is worse than no gate. `adjudicated_at` is stamped whatever the outcome. Also: the settings toggle now round-trips to the account through a small feature-scoped settings route, so turning it off on desktop stops the web surface too; per-frame and clear-all deletion close the loop across Firestore, both GCS objects and the cached signed URLs; and the loopback sidecar is deleted. Verified: backend 111 tests, route-policy baseline current; macOS build clean under -strict-concurrency=complete -warnings-as-errors, 93 tests, swift-format clean; web typecheck clean, 379 tests. Not provisioned by this change, and required before it can be switched on in production: the BUCKET_SCREEN_FRAMES bucket, a signing secret (or SCREEN_FRAME_KMS_KEY), and the IAM split that grants bucket write to the writer's service account alone. The KMS signer is structurally complete but untested — only HMAC-vs-KMS dispatch is covered, because google-cloud-kms is not installed here. * refactor(backend): give screen frames their own database module `database/conversations.py` had grown from 1926 to 2119 lines, tripping the product-file line-count ratchet. The ratchet offers a declared PR-body exception as the escape hatch, but +193 lines into an already-oversized file is the kind of growth the ratchet exists to catch, and the block was one self-contained section. The screen-frame subcollection helpers move to `database/screen_frames.py`. They are a conversation subcollection exactly like `photos`, which is why they were written next to it — the honest reason for the split is the parent file's size, and the new module's header says so rather than inventing an architectural rationale. `conversations.py` returns to exactly 1926 lines, its size before this feature. One non-obvious call site: `tests/unit/test_conversation_events_bounds.py` stubs the whole `database` package with an explicit submodule allowlist, which could not know about a module that did not exist when it was written. Missing it turns a hermetic test into one that reaches for real Firestore and hangs for minutes instead of failing fast. Verified: 111 screen-frame tests pass in ~1s (the runtime matters — a hermetic suite that suddenly takes minutes is reaching the network). `import main` clean. Conversation and photo suites run file-by-file, with three pre-existing failures reproduced identically on the unmodified checkout, and a collection segfault likewise reproduced there — neither is caused by this change. * test(desktop): cover the new screenshot sources in the conversation-detail flow The three files added since the flow's covers: list was last touched — the banner palette, the egress wire types, and the APIClient extension — are all reached by opening a conversation detail, which is what this flow already drives. The flow asserts the dark path (no MeetingScreenshots: activity in the log for an ordinary open), so a regression that starts the pipeline unbidden still fails here. * chore(web): give the screen-frame types TODO a tracking issue The hand-written types in `screenFrames.ts` break the house rule that types are re-exported from the generated client rather than hand-duplicated. That is deliberate and temporary — the OpenAPI spec has not been regenerated since these routes landed — but an untracked TODO is a promise nobody holds, which is what the deferred-work-markers check exists to catch. Filed as #12155, which names the routes to regenerate and why the drift would be silent: `ground.stops` feeds the banner background directly, so a backend shape change that the hand-written type does not follow would not fail typecheck, it would just render wrong. * fix(backend): type the screen-frame read hook to the contract it is passed to pyright rejected `_prepare_screen_frame_for_read` because it declared Optional on both sides while `prepare_for_read` is annotated to hand it a dict and receive one back. `_prepare_photo_for_read` — the function this was modelled on — has the identical mismatch and is not flagged, because the typecheck runs over changed files. Copying its signature would have been copying a latent bug forward, so this matches the decorator's real contract instead. The falsy guard stays for an empty document, but returns the shape it was given rather than None. Verified: pyright 0 errors; 111 screen-frame tests pass. * chore: regenerate the API clients and drop the hand-written screen-frame types The pre-push OpenAPI contract check flagged the spec as stale, which revealed that both artifacts I had just filed #12155 to defer are produced by one command each: `backend/scripts/export_openapi.py` and `backend/scripts/generate_ts_openapi_types.py`. So the deferral was wrong. Regenerated both, deleted `web/app/src/types/screenFrames.ts`, and re-exported the screen-frame types from the generated client, which is the house rule that file was violating. The migration was not purely mechanical: the generated schema makes `strip` and `banner` optional where the hand-written type had them required, and `ground` required where it was optional. Those are handled at the use sites — an absent strip defaults to `[]`, and the banner defaults each gradient stop independently rather than the whole ground object — not with casts or non-null assertions. Verified: web `bun run check` 379 tests across 63 files. (An earlier run showed two failures in GoalComposer; both are userEvent.type races against a 5s timeout, reproduced only while parallel Swift and Python builds were saturating the machine, and green in isolation and on a quiet run.) * fix(backend): drop the always-None screen_frames field from Conversation The Dart model generator refused it: `ConversationScreenFrameSet` is not in the mobile schema group, and adding `screen_frames` to `Conversation` — which is — made the $ref unresolvable. The fix is to remove the field rather than widen the Dart group, because the field was never earning its place. Nothing populated or read it anywhere in the backend; building the set means minting fresh 60-minute signed URLs for every persisted frame, which no ordinary conversation read should pay for, so it was permanently None. A field that always says nothing is worse than an absent one on the most widely consumed model in the product. It told every client — iOS, Android, web, Windows — that a conversation carries its screenshots inline, when the only way to get them is GET /v1/conversations/{id}/screenshots. Regenerated the OpenAPI spec, the four TypeScript clients, and the Dart wire models. Verified: 111 backend tests; web check 379 tests across 63 files. * chore(desktop): regenerate the Swift API client for the screenshot routes Completes the generated-artifact set the OpenAPI contract check requires: spec, four TypeScript clients, Dart wire models, and now Swift. Worth noting for review: this does NOT make ScreenFrameEgressWireTypes.swift redundant the way it made web/app/src/types/screenFrames.ts redundant. The Swift generator emits the routes but types every response as OmiAnyCodable, so the hand-written DTOs are what actually give the client a shape to decode into. The web generator emits real interfaces, which is why that duplicate could be deleted and this one cannot. Verified: swift build clean, 93 tests. * fix(backend): keep the screenshot routes off the public Developer API surface CI's Public Developer API contract check flagged five new routes as undocumented. They sit under /v1/conversations, which is an audited public prefix, so anything added there must either appear in the public spec or carry an explicit justification for staying off it. Staying off it is the right answer, and not merely for tidiness. These routes hand back signed URLs to private screen capture. Putting them on the Developer API means any third-party API key can read and delete a user's screenshots — a privacy expansion nobody asked for, on material the whole screen-frame egress design exists to keep narrow. It is also the irreversible direction: once integrations depend on those keys, withdrawing the surface breaks them. So they join the existing UNDOCUMENTED_PUBLIC_ROUTES entries alongside the other Firebase-authenticated first-party conversation routes, with the reason stated rather than implied. Regenerated the public spec; screenshots correctly appear in app-client (11 references) and nowhere in the public one. Verified: 125 tests including the OpenAPI contract suite. * fix(backend): teach two conversation test stubs about utils.screen_frames CI's backend unit suite died at collection: routers/conversations.py:15: from utils.screen_frames.store import delete_conversation_screen_frames E ModuleNotFoundError: No module named 'utils.screen_frames' Both files stub the module graph with an explicit allowlist so importing routers.conversations does not drag in Firestore. Wiring screenshot deletion into the conversation-delete path added an import those allowlists could not have known about, so collection failed before a single test ran. This is the same defect already fixed in test_conversation_events_bounds.py during the database/screen_frames.py extraction. It was reported to me as pre-existing in these two files, and I passed that on without checking; it was not. Locally a collection segfault in this environment masked the real error, and only CI showed it plainly. Verified against this branch: 23 and 9 tests respectively, both green. test_lazy_conversation_processing.py is genuinely pre-existing — it fails identically on an unmodified origin/main worktree (there on a FieldFilter import, here on get_customer_firestore_client, both the same fragile sys.modules stubbing in this local environment), and CI does not report it. Verified: 167 tests across the affected suites. * fix(backend): register the screenshot judge and its routes in the sibling registries Four guardrail suites failed, each because a new model-backed feature was declared in one registry without the others that exist to stop exactly that. The desktop REST inventory named the real cause: the egress routes had response_models and were included in main.py, but export_openapi.py's APP_CLIENT_PREFIXES had no /v1/screen-frame-egress entry, so they never reached the app-client surface. Fixed at the prefix rather than by adding them to KNOWN_MISSING_ROUTES — that list is for tracked gaps, and this was a missing line, not a gap. No suppression list was touched. screen_frame_judge is now declared in docs/llm/model_endpoint_inventory.yaml under structured_output, and in the QoS tier expectations. Those hardcoded sets are the registration mechanism, not an allowlist: a new feature cannot land without stating its routing and cost characteristics. Regenerated every dependent artifact: both OpenAPI surfaces, four TypeScript clients, the Dart wire models, and the Swift client. The egress routes now appear in app-client and remain correctly absent from the public Developer API. Worth a reviewer's attention: screen_frame_judge routes to Gemini flash-lite in all three QoS profiles including BYOK, matching translation and trends. That means a bring-your-own-key user's candidate frames are judged by Omi's Gemini rather than their own provider. It is the routing the feature was built with, not something these registrations changed, but it is a privacy-relevant default worth confirming deliberately. Verified: 131 guardrail tests, 125 screen-frame and OpenAPI-contract tests, swift build clean. * feat(backend): include faces — David's ruling, 2026-08-24 "Faces are the fastest way to remind a person what their meeting was about and who it was with." This overrides the conservative default I shipped, which rejected any recognisable face. Flipping the constant was not enough, and that is the interesting part. The policy lived in two halves of the prompt: an identifiable_person reject rule gated on the constant, and an approval criterion that independently demanded "no private or identifying information". Removing only the first would have left the model still refusing faces with the flag already False — a gate that looks flipped and is not. Both moved, and a test pins them together so the next person to touch this cannot half-flip it. The ruling's reasoning also changes what a good banner is. A hero image exists to remind someone what a meeting was and who was in it, so banner_suitability now scores for recall rather than decoration, and the prompt says a frame showing the people usually carries more of that than a wall of code. That matters here specifically: 1,690 of 2,061 measured frames came from Claude Code, Cursor, ChatGPT or Chrome. What did not change, and is tested: a face does not rescue an otherwise rejectable frame. A face on a banking screen, in a medical portal or in a DM thread is still rejected for that reason. A personal photo library or social feed is still not meeting-relevant. "Faces are fine" quietly becoming "frames with faces are fine" is the obvious way this goes wrong. Verified: 129 backend tests including four new ones covering both halves of the policy, the survival of every other reject reason, and the no-rescue rule. * fix(backend): the account toggle must hide existing frames on every surface Found by an omp review pass. The setting was checked at adjudication admission and by the settings GET, but by neither read route. macOS hid frames anyway because it has its own local gate — so turning meeting-note screenshots off on desktop looked like it worked, while `GET /v1/conversations/{id}/screenshots` and the public `GET /v1/conversations/{id}/shared/screenshots` kept serving the full persisted set to the web app and to anyone holding a share link. That is the toggle David asked for in R2 not doing what it says. Enforced server-side rather than in each client, so a surface cannot forget: the web GET has no local gate and never did. The public route checks the OWNER's setting, resolved from the share index, not a viewer's. Also fixes three comment and prompt splices I left behind while editing by string replacement: - The judge's `banner_suitability` criterion had a dangling fragment. My face-policy edit matched only through "hero banner" and appended new text, orphaning the original continuation "image (favor a single clear focal subject...)" as a bullet-less line. The model was being handed that. Rejoined so the recall guidance and the original focal-subject guidance read as one criterion. My own tests missed it because they assert substring presence, not coherence. - `MeetingScreenshotsStore.run()` had three interleaved half-sentences from a line-range replacement, including a duplicated lead-in and a sentence ending on "starting". - A stranded line break in `models/conversation.py`. Kept, against the review's own edit: the shared-route happy path reasserts `build_frame_set_response` is called with the OWNER's uid. The review dropped that assertion while adding its new cases; serving another uid's frames from an unauthenticated endpoint is the worst thing that route could do, and asserting the return value alone would not catch it. Verified: backend 132 tests (was 129); swift build clean, 93 tests, swift-format clean; judge prompt re-rendered and read end to end. * feat(dev-harness): make meeting screenshots runnable on the local stack The egress routes fail closed without a bucket and a signing key, so before this the feature could not be exercised anywhere except production — the one place it must not be exercised first. The local harness already emulates GCS through utils/other/local_storage; screen frames were simply missing from the bucket list. SCREEN_FRAME_SIGNING_SECRET has to be named in _LOCAL_BACKEND_SECRET_KEYS because it matches the provider-credential regex on "SECRET". It is not a provider credential — it is the harness's own HMAC key for internal approval tokens — and refusing it in offline mode would break the one mode that needs no external credentials at all. Verified against a running harness: all 8 routes serve, adjudication reaches the judge, and the run fails closed (no bucket writes, adjudicated_at still stamped) when the judge cannot be called. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(backend): refuse screen-frame egress until it is explicitly turned on The pipeline is canonicalize -> judge -> mint approval -> write. The judge is the first step that sends a user's screen bytes off Omi's infrastructure, and it runs two stages before anything reads BUCKET_SCREEN_FRAMES or the approval signer. So "the bucket is not provisioned" was never an off switch: merged as it stood, every Rewind user's candidate frames would have been uploaded and sent to Gemini on every completed meeting, and only then failed — cost spent, screens seen, nothing stored. screen_frame_egress_enabled() is checked above everything else in the handler. Three conditions, all required: SCREEN_FRAME_EGRESS_ENABLED=true, a bucket, and a signer. The explicit flag is what keeps provisioning the bucket from silently switching the feature on for everyone the moment infrastructure lands. 409 rather than 200-with-empty-set, deliberately: an empty set would stamp screen_frames_adjudicated_at, and a conversation adjudicated while the feature was off would then never be retried once it was switched on. The client already treats any 4xx as "render nothing, try again next open". The harness sets all three so local QA is unaffected; verified against a running stack that adjudication reaches the judge with the flag on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: retrigger CI The push of 6b94a67d42 landed on the remote but GitHub never created its workflow runs — 1 check-run against the 40 the previous commit got, with Actions healthy and building other SHAs throughout. Empty commit rather than a force-push so the reviewed history is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: regenerate API artifacts after merging main The five merge conflicts were all generated files. Resolved by taking main's copy and re-running the generators over the merged source rather than hand-merging output nobody writes by hand. Verified after regeneration: all six screenshot routes are present on the app-client surface, and absent from both the public Developer API and the integration-public surface — the deliberate exclusion this branch already justified, re-checked because a regeneration is exactly where it could have been silently reversed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * revert: drop the Dart reformat the merge's format hook introduced Committing the merge ran the repo's dart format hook over every staged file, and it rewrapped 18 files main has committed differently — ~1370 lines of pure line-wrap churn across account_cutover, capture, chat and conversation code, none of it related to meeting screenshots. It also pulled two files under INV-CUTOVER-1's locked globs into the diff, which is what caught it. --no-verify on this commit specifically because the hook is what caused the problem; the content is exactly main's bytes restored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(gateway): send Gemini the image instead of silently dropping it The gateway's Vertex translator built every message with `parts: [{'text': _text_content(content)}]`, and `_text_content` keeps only parts whose type is "text". Any image attached to a Gemini request was discarded without a word. In production `OMI_LLM_GATEWAY_FEATURE_MODE=gateway` with `OMI_LLM_GATEWAY_ALLOW_PROD_FEATURE_MODE=true`, so `get_llm('screen_frame_judge')` returns the gateway client, the gateway maps 'gemini' to VertexGeminiProvider, and the screenshot privacy judge would have been asked to rule on a frame it was never sent. It would have answered — well-formed, confident, and about nothing. None of the judge's fail-closed paths catch that: they trigger on exceptions, malformed output and self-contradiction, and a blind verdict is none of those. An `approved_clean` from it writes real frames to the bucket. That is a fail-open on the one gate this whole feature exists to enforce. `_vertex_parts` now translates `image_url` data URIs into Vertex `inlineData` parts, and raises CAPABILITY_MISMATCH for anything it cannot represent — a remote https image (Vertex cannot fetch one), an unknown part type, an image in a system instruction. The refusal matters as much as the translation: this function already refused unsupported *parameters* that way, and had it done the same for unsupported *content*, the judge would have failed closed from the start instead of running blind. screen_frame_judge is the only image-sending feature routed to 'gemini'; openglass resolves to openai, whose provider forwards messages whole. Also hardens the judge prompt to reject with "unreadable" if no image reaches it. The provider bug is fixed, but nothing downstream of the model can distinguish "looked and approved" from "saw nothing and approved", so the model is told to fail closed itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(gateway,screen-frames): close three gaps an adversarial review found All three came from reviewing the image-passthrough fix rather than from a failing test, and two of them were regressions that fix introduced. 1. `_vertex_parts` returned an empty parts list for `content=None` or `[]`, where the old text-only path always produced `[{'text': ''}]`. Vertex rejects a Content with zero parts, and an assistant tool-call turn carries content=None — dormant only because no Gemini feature is multi-turn yet. 2. The data-URL pattern rejected `data:image/jpeg;charset=utf-8;base64,...`. RFC 2397 allows parameters before the base64 token and browsers emit them, so this refused an image that can in fact be represented — the mirror of the bug the original fix was written to close. 3. `build_frame_set_response` had no per-frame error isolation. One stored doc that violates ConversationScreenFrame's contract — legacy data, a hand edit, a future write path that skips ScreenFrameJudgement — raised straight out of the loop and 500'd the whole screenshots read, hiding every good frame with it. The caption relaxation in the previous commit leaned on that wire constraint as a backstop; it was a crash point, not a backstop. Now the bad frame is skipped and logged. Also makes caption truncation grapheme-aware: a plain [:160] can cut inside an emoji sequence and leave a dangling zero-width joiner that every client renders as a broken glyph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(hermetic): raise scope job timeout so full-history fetch fits The Detect Hermetic Backend Scope job caps itself at 2 minutes but checks out with fetch-depth: 0 so it can resolve the PR base live for the scope diff. On large merge refs that fetch exceeds 2 minutes on hosted runners, the job is killed mid-transfer ("The operation was canceled"), and the merge gate correctly fails closed on SCOPE_RESULT=cancelled -- three runs in a row on this PR with no code failure. Other PRs' scope jobs finish in 49-65s; this PR's merge-ref fetch sits just above the old cap. Raise the cap to 10 minutes, matching the apt-step precedent in this same file: the cap is a hang backstop, not a budget the legitimate path must fit under. * feat(desktop): show frames in Quick Look, put the banner behind the header Three defects in how a captured frame reached the reader, and one duplication. **The viewer could not show the picture.** Its panel was `glassCard`, whose whole background is `Ink.rowFill` — `labelColor.opacity(0.045)` — over a 0.28 scrim, so the "modal" was about 68% transparent and the note's text read through the screenshot. Its frame was a hardcoded `height: 560` rather than a fraction of what was available, so a shorter shell clipped it. And a 5120pt capture fit into 1030x560 renders at a fifth of native with no zoom, no pan and no 1:1 — the one thing the reader clicked for was the one thing it could not do. `ScreenFrameLightbox` is therefore replaced by `QLPreviewPanel`: an opaque system panel, sized by the system, with pinch and scroll zoom, space-to-close, full screen, share, Open With, and left/right stepping across the whole set — which is why `present` is handed every frame rather than the one that was clicked. Net -263 lines of our own viewer for +334 of adapter, and the adapter owes it only a file, a title, and cleanup. Three things that adapter has to get right, all now verified against the live panel by `screen_frame_quick_look_probe`: - Quick Look needs a file and almost none of these frames are one — a meeting frame is a signed URL, a Rewind moment is usually a frame inside an H.264 chunk. Both materialise into `OmiQuickLook/` under the temp directory. - Those files must not outlive the panel. Rewind's own store is unencrypted on disk so a local copy is not a new class of exposure, but a *server* frame's is: "delete this screenshot" cannot reach a stray copy in /tmp. The directory is purged when the panel closes and again at launch. - The responder chain. The claim lives on `AppDelegate`, not on a view, so there is no fight with SwiftUI over first responder and no window state in which the panel silently refuses to open. **Clicking a moment in the Activity spine used to discard the moment** and navigate to the Rewind page. It now opens that frame, at full resolution, in place. Navigating to Rewind moves onto the strip's own "8 of 184 moments" caption, which is the thing that says the other 176 exist. **The banner drew a second copy of the title.** It painted the conversation's title and date onto a gradient a hundred points below the header, which was already drawing the same title and the same date — two copies of one line, and 132pt of the pane spent on the second. The ground moves behind the header and draws no text at all; the title stays real, selectable, resizable chrome. The store hoists to `ConversationDetailView` because the header is the summary's sibling and both halves must read the same banner. At a 62pt band the frame cannot be legible, so it is thrown out of focus and is atmosphere; the legible copy is the strip tile below, which now opens properly. The banner file's "never blurred into a texture" rule was written about the inset that had to stay readable, and is stated as superseded here rather than half-obeyed — measured on a real note, a sharp frame put the trash button on top of the trash icon inside the screenshot. Deleting a frame moves to a right-click on the tile: Quick Look's chrome is Apple's and cannot take a trash button, and a context menu is where the Finder puts exactly this command anyway. Verified against the running QA bundle, both source paths: - meeting frames: panel_controlled=true, panel_visible=true, 6/6 items materialised from signed URLs to real JPEGs at 1579x1130, purged on close - Rewind moments: panel_controlled=true, video-chunk frame decoded to 126.jpg The Rewind path failed on the first probe — "Storage not initialized", because a viewer reached from the spine is routinely the first thing in a session to want those pixels. `materialize` now initialises storage the way every other reader of Rewind pixels does, and a failure is no longer latched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(app): one media viewer, with the share action mobile never had Audited mobile against the three defects the macOS viewer had, and two of them are simply not there: both viewers were pushed full-screen `Scaffold` routes with an explicit `backgroundColor: Colors.black`, and both already used `photo_view` with `minScale: contained` / `maxScale: covered`, so an opaque barrier, the whole image visible, and pinch zoom were all already correct. Not fixing what is not broken; recording that it was checked. The real gap is what the OS handed macOS for free and mobile has no panel to inherit from: neither viewer had any way to share or save the photo you were looking at. That is now an explicit AppBar action, on `share_plus`, which was already a dependency — matching the `SharePlus.instance.share(ShareParams(...))` call style and the `shareSheetOrigin` anchor every other call site in this repo uses, rather than the deprecated static API some older files still call. `FullScreenImageViewer` and `PhotoViewerPage` were ~90% the same widget — black ground, `PhotoView`, a way out — differing only in single-vs-gallery and network-vs-memory image source. They are one `MediaViewerPage` now, so there is one place for the share action to live instead of two. Each call site keeps exactly what it looked like before, via parameters for the things that genuinely differed: AppBar colour, max scale, close button vs back arrow, safe area, and whether the caption/processing/discarded strip applies. One incidental gain: the app-store thumbnail call sites now pass the whole thumbnail list, so those screenshots page like a gallery instead of opening one image with no way to reach the next. `flutter analyze` clean on all six touched files. The five pre-existing files were already conformant under the repo's own `dart format --line-length 120`, so this carries no reformat churn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(desktop): make the Quick Look panel provable on a bundle with no data `ScreenFrameQuickLook.swift` was the one changed Swift file no e2e flow covered, and the obvious cover would have been a lie: the frames need a provisioned bucket and a live judge, and the panel is a *system* window that no capture path in this app can photograph — `capture_main_window_png` uses `cacheDisplay`, which draws Omi's own content view and nothing else. So the probe grows a hermetic source. It seeds a 1x1 PNG to a temp file and reaches the panel through `file://`, which `URLSession` serves for a data task — the same materialise-then-present path a signed URL takes, with no capture history, no backend and no network. `conversation-detail.yaml` asserts the one thing that is otherwise unprovable: `panel_controlled`, which is only true if `beginPreviewPanelControl` ran, which only happens if something in the responder chain claimed the panel. Measured on the QA bundle: panel_exists/panel_visible/panel_controlled all true, ready_count 1, seed materialised as probe.png, dismissed cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(desktop): declare the Quick Look callbacks without implicitly unwrapped optionals QuickLookUI predates nullability audits, so every `QLPreviewPanel *` imports as an implicitly unwrapped optional and the repo's SwiftLint rule rejects all eight. Declaring them non-optional keeps the same Objective-C selectors and types, so the responder-chain claim and the data-source binding are unchanged — re-measured against the live panel on both the synthetic and the real signed-URL path after the change: panel_controlled true, panel_visible true, purge clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: close the defects an adversarial review of this work found Ran `codex exec -m gpt-5.6-sol` over the previous three commits with full access. Nine findings; eight were real, and chasing one of them by hand found a ninth that neither of us had predicted. Each fix is measured against the running QA bundle, not argued. **Quick Look could not be opened twice in a session.** `QLPreviewPanel` searches the responder chain once and caches the answer. After a close it has already asked and been told no — we clear `items` on the way out, so `acceptsPreviewPanelControl` correctly returns false — and every later `makeKeyAndOrderFront` reuses that stale "nobody wants this". Measured: the first Quick Look of a session came up controlled and the second and third came up with no data source and no picture at all. `updateController()` is the API for exactly this, called after the new set is installed. Six open/close rounds now pass where three used to fail. **A late close could kill a live panel.** AppKit delivers `endPreviewPanelControl` on its own schedule and it carries no identity, so an end for an ordered-out panel is indistinguishable from an end for the one on screen now. A person cannot expose this — their next click is dispatched behind the close AppKit already queued — but two HTTP requests can, and one probe round in four died on it. A scripted close now waits until the close it asked for has actually been observed, and a close for a presentation that is already over does nothing. **Indexing a set a second click had already replaced would trap.** `present` started an untracked task that indexed the singleton's mutable array after an await; a second, shorter set arriving first made that a crash rather than a wrong picture. Every presentation now carries a generation and walks the array it was given. **Downloaded server bytes could be recreated after the panel closed.** Closing purged the directory but cancelled nothing, so a download still in flight landed afterwards and put the file back — defeating the one guarantee this design makes, since "delete this screenshot from the note" cannot reach a copy in /tmp. Closing now cancels the work, `materialize` refuses to write once cancelled, and the purge runs a second time after the cancelled work has actually finished. **The launch-time purge was lazy, not launch-time.** It lived in the singleton's initialiser and `shared` is lazy, so a session in which nobody opened Quick Look never ran it — leaving the frames a force-quit left behind on disk for that whole session, which is precisely the case it exists to cover. It is a static now, called from `applicationDidFinishLaunching`. **Two frames could overwrite each other's file.** Sanitising an id collapses distinct ids onto one string: `frame/a` and `frame?a` both become `frame-a`, and two long ids can agree for their first N characters. Both preview items then pointed at whichever bytes landed last, so Quick Look would show a screenshot the reader did not ask to look at. The id is a label now; a UUID is the name. **An expired signed URL no longer recovered.** The deleted lightbox reported back to the store through `onContentUnavailable`; nothing replaced it, so a note left open past the 60-minute signature would open Quick Look on "no preview" and stay that way however many times it was clicked. `present` takes a `refreshing` closure, called once, only when the clicked frame fails. **The header veil was set by the pretty case, not the worst one.** White at 0.38 over a black screenshot resolves to about #616161, against which this shell's near-black `Ink.primary` measures roughly 3.4:1 — and approved frames are routinely near-black, because most of them are dark-mode editors. The floor is 0.58 now, which clears 5:1 on the same frame. Verified in a window capture: `cacheDisplay` drops CALayer filters so the blur does not appear there, but opacity does, and the action buttons now sit on a light ground. **A row of eight or fewer moments had no route to Rewind at all**, because the caption that carries it only exists when the strip is truncated. Rewind is on the tile's context menu as well now, and the tile's accessibility hint no longer claims it opens Rewind. Mobile, from the same review: the merged viewer decoded every photo in the gallery before the first frame rendered, where `PhotoViewGallery.builder` had only ever decoded pages it built — items carry the encoded source now and providers are built lazily and memoised. Sharing wrote a full-resolution copy into the temp directory and never removed it; it does now, in `finally`. The thumbnail download had no timeout, no size bound and no MIME type, so a server that accepted and then stalled left the button spinning for the session — it now has all three, and takes the type from the response rather than from the URL's extension. Not changed, and worth stating: the reviewer's read that `QuickLookItem`'s lock-based isolation is correct, that the spine hands over exactly the visible strip, and that the disabled-feature gate still holds, all match mine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(desktop): make the frame visible in the header, and Rewind's frames openable Both of these are David's QA, and both were things I had not actually covered. **The header banner was showing a gradient and no screenshot** — correct, and the two requirements it was trying to satisfy are mutually exclusive. The shell is light-pinned (`InkGlass.appearance` is `.aqua`, so `Ink.primary` is near-black in every window) and approved frames are routinely near-black too, because most of them are dark-mode editors. So the veil that keeps a title and five action buttons legible over a black screenshot is, necessarily, a veil that erases the screenshot; adding blur on top finished the job. A picture with chrome written over it is not a picture. So the banner is two things now. `MeetingNoteHeaderBanner` is the ground: a wash of the server's `ground` gradient under a veil whose floor is set by the worst case (0.58, which clears 5:1 for `Ink.primary` over a black frame) — no image, no blur. `MeetingNoteHeaderInset` is the frame itself, sharp and unveiled, given room of its own between the title block and the action buttons, at the same proportion as a strip tile because it is the same picture. It sets the header band's height, so a note with a banner gets a slightly taller header and a note without one is exactly as it was. Clicking it opens Quick Look, like every other frame on the page. **Rewind's own screenshots were still not clickable**, because I wired the Activity spine and stopped there. The Rewind page's stage is a *preview*: fit to whatever the pane happens to be, which on a half-width window is a fraction of a 5120pt capture — enough to recognise the moment, not enough to read the line someone scrubbed to it for. Clicking it now opens the real frame at full resolution, and the whole visible run goes with it so the arrow keys walk the same sequence the track does. A search result card keeps its click (open the group on the timeline, which is what searching usually wants) and gains "Quick Look" on right-click, which is the other thing that reader might want. That is every live surface that draws a captured frame: the note's strip and header inset, the Activity spine's tiles, the Rewind stage, and search results. `RewindTimelineView` and `RewindTimelinePlayerView` also draw frames and are deliberately left alone — neither has a call site outside its own SwiftUI preview. Note for whoever hits it next: `RewindCaptureExclusionGenerationTests .testOwnerSnapshotStaysCurrentWhenAuthLeadsUnresolvedRewindDatabase` fails when the local suite is filtered to include both it and the Rewind suites, and passes alone. Measured against a stash of this change: it fails identically without it, so it is a pre-existing ordering interaction and not this work. CI's ordering does not reach it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> | 29 天前 | |
feat(dev-harness): fake-backed V1 live-session broker (reload, restart, controls, evidence, owned teardown) (#14362) * harden(dev-harness): define V1 live-session and V8 spine contracts Add strict Python/Dart pending contracts and introducing-commit protection. Reserve the live-session CLI, wire, ownership/attachment seams and failing builder acceptance tests. Add live attribution before freezing evidence v1. Verification: - bash scripts/dev-harness/run-tests.sh: 287 passed, 7 existing skips, 40 strict xfailed (24 pending V1 markers), Python 3.11.15. - make preflight: 16 checks passed, using supported PYTHON selection. - make mobile-verify fast --paths harness/session contract inputs with an absolute evidence directory: 20 executed, 20 passed, no failures/skips. - bash app/test.sh: 2014 passed, 5 skipped, one unchanged UTC+7 search_rank_grouping_test failure; affected baseline inputs match main. - Real module CLI refusals exit 2 without traceback; regression covered. - Dart SDK probe proves ext. prefix required; marker-removal formatter probe proves no incidental Dart diff. Runtime is intentionally unimplemented. App UI must repair current invalid VM extension names before live acceptance; unsupported journeys block. Simulator/emulator latency and detached-process acceptance remain V1 work. make setup installed hooks but full backend wheel sync stalled; isolated Python 3.11 harness dependencies ran the owner checks without any skip flag. Failure-Class: none * harden(dev-harness): scope the spine-contracts check to spine paths The check protects files under the spine roots and their registry, so it only needs to run when those change. A repo-wide trigger would run it, and its full-history requirement, on every unrelated contributor PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(dev-harness): clarify the live journey adapter boundary V1 must refuse live journey execution until a shared executable adapter exists; B1 addressability alone cannot attach Dart integration tests. Pure admission contracts remain valid with injected supported IDs. Verification: harness 287 passed, 7 skipped, 40 strict xfailed; make preflight passed 20 checks. * harden(dev-harness): pin reviewed spine revisions and align B0 capabilities Coordinator turn-4 review authorizes the V1 fixture correction. Exact before/after digests pin the revised oracle; immutable revision records do not permit builder assertion edits. Active tests reject missing/tampered records and prove the fake machine response. Verification: full harness 289 passed, 7 skipped, 40 strict xfailed; UTC app 2020 passed; spine 24 pending/7 protected; make preflight 16 passed. No live device acceptance claimed. * harden(dev-harness): preserve marker retirement across spine revisions Track retained marker occurrences rather than counts so retiring a second test cannot restore the first test's retired marker. The active regression covers this swap and legitimate continued retirement. Verification: full harness 290 passed, 7 skipped, 40 strict xfailed; spine 24 pending/7 protected; make preflight 16 passed. Dart sources unchanged after the earlier UTC app 2020-pass run. * harden(dev-harness): retain corrected spine oracles through squash merges A squash can introduce a corrected contract and its review records in the same commit. Absorb only the revision prefix ending at that introducing file's exact digest, preserving immutable records and later marker-only edits. Active temp-repo regression covers multiple corrections in one squash and rejects restoring old or altered assertions. Verification: harness 291 passed, 7 skipped, 40 strict xfailed; B1 make preflight 20 passed; final B1 journeys 20/20. Dart unchanged after UTC app 41 opt-in and 2051 ordinary passed. * test(dev-harness): revise live session acceptance after builder review * test(dev-harness): bind machine fixture to actual child identity * feat(dev-harness): implement the fake-backed V1 live session broker LiveSession speaks the Flutter machine wire against an injected child, records BrokerIdentity in live.json, and refuses real flutter run from CLI dispatch. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dev-harness): tear down live before stop, reset, recover, and release Lifecycle order is live teardown, then services, then device detach; recover still has generation 1 while teardown runs. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 live-session pending markers The fake-backed broker now satisfies those spine tests; only whole pending-marker lines were deleted. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): cover lazy live-session holes with real child processes A dead flutter child mid-reload is blocked, never success; teardown will not signal a live PID whose start time, marker, or boot id does not match. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): hash iOS .app bundles as directory trees file_sha256 raised IsADirectoryError on a simulator .app; identity is now the sorted tree of regular files. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): pin live-session source, generation, and stdio The architect's thirteen probes rejected attributing a compile to source observed after launch, announcing stopped after a failed reap, and grepping one fixture secret. Keep one monotonic RPC deadline and actually validate negotiated capabilities. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fence live publication, boolean readiness, and auth redaction _assert_generation only covered admission, so a lease roll during ext.omi.controls.state still published ok and advanced loaded identity. Recheck lease and child generation after daemon/readiness work, refuse non-boolean readiness, and redact complete Authorization values before logs are retained. Failure-Class: none Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): restore live teardown after main merge and retire passed T7 markers Keep main's port-claim mobile_session.py; put back V1 live teardown on stop/reset/recover. The formatted round-7 oracles now pass, so remove only the pending-marker lines. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(dev-harness): restore app sources that the merge hook reformatted The merge commit ran dart format without package:flutter_lints resolved and rewrote main's files. Restore origin/main bytes so the V1 PR does not carry unrelated UI diffs. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail fast when a live owner holds the session lease Name the session and holder pid immediately. The same overlap that used to look like a controls-extension miss is a held lease. Co-authored-by: Cursor <cursoragent@cursor.com> * test(dev-harness): retire V1 pending markers that main's live fences now pass Merging origin/main imported strict xfails for lease-roll, negotiation, and auth-redaction fences this branch already implements. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dev-harness): fail closed on non-boolean live readiness flags Finding 2 of the adversarial review reproduced: a fake reporting readiness.signedIn as the string "false" was classified not-ready and fell through to the wait_ready recovery path, which then observed real booleans and published ok. _is_ready now raises LiveError (malformed-response) for present-but-non-boolean flags, so start blocks instead of self-healing; start/reload/restart wrap it and surface blocked/malformed-response. The stringy-false regression test's expected error_code moves unready -> malformed-response; blocked-stays-blocked is unchanged. The satisfied V1 pending marker on the boolean probe is retired. Findings 1, 3 and 4 did not reproduce at this head (probes re-run verbatim; evidence in PR comment). Failure-Class: new --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
feat: add JIT knowledge ledger foundation and guarded adoption (#12084) * feat: add JIT knowledge ledger foundation * chore: refresh integration OpenAPI contract * fix: make trigger evaluation release-safe Failure-Class: none * fix: preserve lifecycle semantics in ledger apply Failure-Class: none * feat: adopt guarded JIT knowledge surfaces Route the agent preference writer through the intent-backed ledger, register a privacy-filtered entity timeline tool, render optional evidence on Windows, and add a base-ref-protected Gate F legacy-surface ratchet. Failure-Class: none * feat: add progressive JIT knowledge reads Register owner-scoped current-ledger search and explicit playbook hydration, with pre-limit semantic filtering and bounded outputs. Add a content-free planner/resume migration fixture without claiming canonical transaction completion.\n\nValidation: 141 focused backend tests passed; backend typecheck reported 0 errors; repository preflight passed 120 checks. * feat: render chat evidence on web Render bounded, fail-soft conversation evidence after authoritative answers in both web chat entry points. Unsupported, future, duplicate, and raw failure details remain inert.\n\nValidation: 337 web tests passed; web typecheck, oxlint, and Prettier passed; repository preflight passed 120 checks. * feat: require intent-backed ledger search results Apply the intent-backed requirement at the final merged canonical/history filter, with a passive historical-row regression case.\n\nValidation: 54 focused backend tests passed. * test: keep agent tool isolation stubs current * feat: gate JIT conversation retrieval * fix: make entity timeline scans deterministic Failure-Class: none * fix: honor rejected ledger projections Failure-Class: none * feat: render inert screen evidence on web * fix: reuse canonical review projection Failure-Class: none * fix(web): await recap context effect Failure-Class: none * test: amortize preference tool isolation load Failure-Class: none * feat(app): add knowledge ledger review surface Failure-Class: none * feat(macos): use canonical ledger prompt projection Failure-Class: none * test(memory): classify legacy surface inventory roles Failure-Class: none * fix(app): preserve ledger history completeness state Failure-Class: none * feat(macos): preserve canonical ledger mirror metadata Failure-Class: none * fix(app): match canonical ledger ordering Failure-Class: none * test(api): prove ledger client schema parity Failure-Class: none * feat(memory): expose bounded ledger history Failure-Class: none * chore(api): generate ledger history clients Failure-Class: none * feat(retrieval): add bounded card participants Failure-Class: none * feat(macos): project ledger trigger watchlist Failure-Class: none * fix(clients): fail closed on ledger authority Failure-Class: none * feat(macos): expose bounded trigger snapshot Failure-Class: none * fix(memory): keep closed history read only Failure-Class: none * feat(app): disclose partial ledger history Failure-Class: none * test(macos): cover ledger trigger bridge Failure-Class: none * fix(app): use neutral ledger accents Failure-Class: none * chore(api): declare ledger history route policy Failure-Class: none * test(macos): remove unsafe JSON fixture unwraps Failure-Class: none * fix(memory): satisfy typed history boundary Failure-Class: none * fix(macos): require prompt snapshot authority Failure-Class: none * test(memory): prove ledger migration on emulator Failure-Class: none * feat(retrieval): emit bounded screen evidence Failure-Class: none * test(memory): classify maintenance retirement readiness Failure-Class: none * feat(macos): adapt Rewind metadata for triggers Failure-Class: none * test(retrieval): align screen timestamp contract Failure-Class: none * feat(memory): correct ledger facts by amendment Failure-Class: none * test(memory): prove ledger correction on emulator Failure-Class: none * feat(macos): harden local trigger observations Failure-Class: none * feat(agent): search bounded historical facts Failure-Class: none * test(macos): cover trigger observation adapter * fix(memory): gate historical fact retrieval * feat(memory): add gated JIT retrieval strategy * test(memory): prove mixed-version JIT runtime parity * chore(memory): keep JIT gate exports type-safe * refactor(memory): isolate JIT prompt contract * fix(conversations): round-trip owner-scoped references Accept the conversation:<id> references emitted by JIT result cards while retaining strict UUID-only bare IDs and share links. Restrict machine IDs to a bounded safe alphabet so evidence suffixes and path-like values fail closed. Failure-Class: none * test(memory): join JIT citations to evidence envelope * fix(retrieval): enforce JIT conversation search budget Cap JIT summary searches per request and bound database hydration to the projection limit before reads. Preserve the legacy path when JIT is disabled. Failure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep JIT retrieval request scoped * test(macos): prove future JIT evidence stays inert * fix(memory): keep JIT card citations request-global Failure-Class: new * fix(retrieval): separate JIT hydration from search Treat gated owner-scoped references as exact hydration without searching transcript text for the reference. Charge every JIT candidate search to the shared four-search request budget, including snippet-bearing requests, while keeping exact hydration free and preserving released JIT-off UUID/share-link behavior.\n\nVerified:\n- cd backend && ./.venv/bin/python -m pytest tests/unit/test_conversation_jit_processing.py tests/unit/test_conversation_exact_reference_search.py -q (58 passed)\n- cd backend && uvx --from pyright==1.1.403 pyright -p pyrightconfig.json --pythonpath .venv/bin/python (0 errors)\n- git diff --check\n\nFailure-Class: FC-unbounded-user-collection-in-prompt * fix(memory): keep repeated JIT cards index-safe * fix(retrieval): satisfy JIT card type contract * fix(retrieval): hydrate collected JIT cards * test(app): preserve answers during delayed evidence requests * test(app): exercise production evidence composition * feat(memories): restore superseded ledger facts * fix(memories): reconcile reverted ledger facts * feat(memories): append reverted ledger facts * feat(memories): synchronize revert client contract * fix(memory): name ledger revert identity * fix(memories): type and enlarge revert controls * fix(memories): fence revert retries and refreshes * fix(memories): fence ledger revert authority * test(memory): count ledger revert rate limit * feat: expose agent-controlled historical facts * feat: reopen standalone ledger facts * feat: add fail-closed JIT QA bundle routing * feat: add safe local JIT QA backend stack * fix: harden isolated JIT QA stack * feat: add explicit multi-source entity timeline * feat(backend): add JIT rollout authority * feat(backend): fence every proactive paid boundary * fix(backend): release proactive quota on cancellation Release the reserved proactive quota exactly once when cancellation interrupts paid-boundary refresh or a provider retry, then re-raise cancellation without emitting retry telemetry. Add deterministic regression coverage for both cancellation points. Failure-Class: FC-proactive-quota-cancellation | new * fix(backend): make proactive quota cancellation safe Detach in-flight Redis reservations on request cancellation and release only admitted slots once they settle. Move direct-provider fallback telemetry behind the fresh paid-boundary rollout check so late kill or unknown decisions cannot report false recovery.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): preserve quota compensation during shutdown Keep late Redis reservation compensators outside the ordinary cancellable background-task drain. Desktop and main application shutdown paths now wait for these critical compensators before cancelling ordinary work, with deterministic blocked-thread and lifecycle-order regressions.\n\nFailure-Class: FC-proactive-quota-cancellation | new * fix(backend): use expiring proactive quota leases * fix(backend): make quota finalization clock-safe * fix(backend): isolate jit rollout control plane * fix(backend): close jit control plane safely * fix(backend): emit retry recovery after quota commit * test(backend): keep rollout app contract fast * feat(jit): add guarded proactivity and first-open policies * chore(desktop): mark jit policy as internal * test(desktop): cover jit proactivity policy flow * feat(backend): wire durable JIT first-open processing * feat(desktop): fence JIT proactivity runtime admission * feat: activate authoritative JIT proactivity runtime * fix: harden JIT proactivity authority * fix: close proactive runtime authority gaps * fix(jit): make first-open effects resumable * fix(jit): fence outstanding first-open work * fix(jit): resume app usage receipts * fix(jit): make app usage retries no-op Failure-Class: none * fix(jit): allow completed usage after app deletion Failure-Class: none * fix(jit): register first-open folder query Failure-Class: none * Fix first-open import isolation * feat(memory): govern ledger slots and prompt winners * feat(macos): stage guarded ledger prompt adoption * feat(jit): adopt authoritative ledger prompts on macOS * fix(jit): close ledger adoption authority leaks * fix(jit): reauthorize every ledger migration write * fix(jit): fence ledger cutover publication * fix: keep ledger prompt rollback reversible * feat(jit): add guarded frame request retention contracts * fix(jit): close frame retention authority and evidence lifecycle * fix(jit): make frame retention retries and cleanup durable * fix(jit): make frame evidence recovery and retention complete * fix(jit): close frame retention recovery gaps * Harden temporary frame retention and deployment * fix: harden JIT frame retention and consumption * fix: close JIT frame lifecycle recovery gaps * fix: unify JIT frame authority and retention Failure-Class: FC-split-mutation-authority * docs: keep frame retention guidance lean * fix: retire duplicate frame flag bindings Failure-Class: FC-split-mutation-authority * fix: register frame keyframe queries Failure-Class: FC-split-mutation-authority * fix: serialize frame retention deploys Failure-Class: FC-split-mutation-authority * test: cover frame pixel deletion ordering * style: format cumulative Dart changes * fix(app): retain permanent conversation photo fetches * fix: bound frame vision retention and authority * fix: drain terminal frame request metadata * chore: record internal ledger adoption change * feat(memory): add dark daily sweep authority * feat(memory): harden daily sweep fences and runtime seam * feat(memory): reconcile existing standing triggers in sweep adapter * fix(memory): harden daily sweep recovery and source fences * fix(memory): close daily sweep source producers * fix(memory): close daily sweep review findings * Add dark daily memory sweep authority and recovery * fix(memory): harden daily sweep rejection repairs * test(listen): stub onboarding admission in bootstrap regression The daily sweep PR fences onboarding mode behind the server-owned backend admission (get_backend_onboarding_admission), so the bootstrap regression test now simulates an admitted session instead of failing closed on a real Firestore read. Verification: focused test passes in 1.64s (previously failed after a 4m27s Firestore timeout); full test_listen_runtime_regressions.py + test_onboarding_question_start.py: 26 passed; black --check clean. * fix(memory): close daily sweep rollout and retry cursors * fix(memory): isolate daily sweep lifecycle and retry fairness * Harden daily sweep admission and completed-day staging * fix daily memory sweep reliability boundaries * preserve daily sweep invocation tombstones * close daily sweep invocation lifecycle fences * fix: keep daily sweep lifecycle cleanup active * fix: acquire ledger snapshot client off event loop * fix(memory): preserve migration tier fence without legacy growth * test(memory): prove legacy adjudication race fences * fix(dev): allow bounded ADC readiness refresh * test: keep ledger prepush deterministic * test(memory): register prompt receipt control path * fix(memory): fence ledger writer transitions * feat(backend): preserve closed ledger history in export * feat(memory): define ledger query semantics * fix(backend): fence trigger snapshots on final authority * fix(backend): bypass stale coalesced JIT refreshes * feat(macos): mirror bounded memory evidence Decode generated v3 evidence into a domain mirror, persist canonical bounded JSON through the memory cache, and preserve it across compatibility sync and older-local conflicts. Invalid, future-shaped, oversized, and over-count payloads fail closed without hiding memory text or granting prompt authority. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): fence and classify memory evidence Keep generated memory fields independent from malformed evidence, distinguish absent valid and invalid evidence states, preserve prior evidence on invalid payloads, and gate replacements on a monotonic server timestamp so stale active evidence cannot resurrect redacted rows. Cover populated-table migration upgrades. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * fix(macos): preserve evidence fences and scrub redactions Advance evidence revisions for identical valid payloads, fence stale active responses after a local edit, and remove artifact/device pointers from redacted evidence before canonical persistence. Tests: xcrun swift test --package-path Desktop --filter ServerMemoryV17DecodingTests Tests: xcrun swift test --package-path Desktop --filter MemoryLedgerMirrorTests Tests: python3 scripts/check_desktop_test_quality.py Failure-Class: none * chore(macos): record ledger evidence mirror * feat(macos): deep-link local evidence cards to Rewind * fix(macos): fence Rewind frame evidence version * fix(macos): validate Rewind evidence card availability * fix(macos): bind task detail Rewind navigation to local leases * fix(macos): fence Rewind citation owner handoff * chore(macos): register Rewind evidence deep links * test(macos): cover Rewind evidence navigation * feat(desktop): evaluate JIT trigger watchlists locally * feat(desktop): wire authoritative JIT trigger runtime * feat(desktop): bind JIT claims to snapshot authority * fix(desktop): revalidate trigger authority at execution * fix(desktop): keep JIT execution leases live * test(memory): bind standalone reopen to direct-user writer * fix: make JIT QA sign-in self-contained Failure-Class: new Verification: bash desktop/macos/tests/test-jit-qa-target.sh; bash desktop/macos/tests/test-yolo-dev-backend.sh; repaired named-bundle Google sign-in reached authenticated onboarding. * feat(memory): complete JIT policy and native Windows parity * docs(backend): keep service map within context budget * test(macos): cover JIT client and staging flows * chore(backend): declare JIT mirror route policy * fix(backend): use strict Firestore boundary for JIT admission Failure-Class: FC-malformed-doc-read * chore(quality): register malformed-document guard surface * fix(backend): fail closed on malformed JIT authority Failure-Class: FC-malformed-doc-read * refactor(backend): name JIT workflow boundary results * test: repair JIT CI contracts * fix(backend): preserve ledger query exports Retain the explicit same-name re-exports consumed by tests and downstream callers while satisfying the enforced Pyright unused-import boundary after the main rebase. Failure-Class: none * test(backend): isolate gateway setup timing Failure-Class: none * style(memory): format direct-user evidence path Failure-Class: none * test(agent): isolate ACP process-group fallback Failure-Class: none * fix(dev-harness): preserve ownership markers in narrow CI * test(jit): refresh emulator fixtures for current contracts * test(jit): orchestrate local rollout dogfood * test(jit): harden local dogfood authority * fix(dev-harness): install PostHog for CI tests * fix(chat): project server JIT rollout into retrieval Resolve the backend-owned PostHog decision inside the bounded agent setup path and pass only its boolean result to prompt/tool configuration. Unknown or failed authority remains on the released legacy path, while callers cannot self-enroll through configurable input.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: new * fix(memory): preserve preference writer compatibility Select the agent preference write path from the canonical per-user writer control. Default compatibility mode retains the released MemoryService payload and receipt behavior; ledger mode keeps the retry-stable ledger write, and transition states fail closed.\n\nVerification: backend/.venv/bin/python -m pytest -q backend/tests/unit/test_chat_async_offload.py backend/tests/unit/test_atomicity_lifecycle_regressions.py (41 passed)\n\nFailure-Class: FC-split-mutation-authority * fix(jit): separate migration rollout authority Keep staged JIT chat and proactive exposure independent from legacy-row migration and writer cutover. Migration now requires its own default-off PostHog flag and still rechecks the shared kill switch at every mutation and publication boundary. Repair the isolated conversation-JIT fixture for main's chat-scope import. Verification: 217 focused JIT, chat-scope, migration, and lifecycle tests passed; 28 conversation-JIT fixture tests passed; independent Sol review accepted the split for QA-only dev rollout. Failure-Class: FC-split-mutation-authority * fix(photos): preserve retained image retrieval Treat an empty legacy inline marker as absent when permanent storage is authoritative, while malformed non-empty inline payloads still fail closed. Route live and retained thumbnails through the storage-aware image loader and preserve the conversation identity through the full-screen viewer.\n\nVerification: backend data-export tests 32 passed; Flutter photo-viewer tests 5 passed; focused Dart analysis clean; independent Sol review found and verified the viewer identity repair.\n\nFailure-Class: none * fix(memory): keep disabled daily sweep dark Resolve the backend-owned authority before inventory and require its literal true decision before any UID discovery, registry, cleanup, scheduler, model, or commit work. Missing, malformed, throwing, disabled, and kill-switched authority now exits without touching user data; enabled behavior is preserved.\n\nVerification: 60 focused daily-sweep job, scheduler, and inventory tests passed; independent Sol review accepted the fail-closed gate.\n\nFailure-Class: FC-split-mutation-authority * fix(jit): satisfy fail-closed type contracts * test(backend): admit full runtime contract checks * style(backend): format conversation bound test * test(backend): keep conversation router isolation current * test(backend): admit export boundary duration * fix(macos): persist failed chat turn notice Failure-Class: none * fix(macos): repair JIT rollout admission contracts Failure-Class: none * fix(windows): treat JIT screen evidence as untrusted Failure-Class: none * fix(backend): preserve explicit app failure contract Failure-Class: none * fix(app): finish photo viewer consolidation * fix(backend): make provider writes lock-free against the deletion gate The account-wide legal-hold deletion gate wrapped every GCS upload and Pinecone/Typesense upsert in an exclusive per-uid Firestore mutex with no lease: concurrent same-account writes hard-failed (dropped audio, lost vectors) and a crash between acquire and finish blocked the account's gated operations forever, with no janitor. Provider writes now use a lock-free fence that refuses only during account deletion or a live destructive operation; destructive kinds keep exclusive ownership, an abandoned gate self-expires after six hours, and releasing a gate on the failure path can no longer mask the original error. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): issue onboarding admission at socket connect The completed-onboarding early exit returned False from an Optional[str] function; the listen runtime derives admission via 'is not None', so users who had already completed onboarding were admitted with a fabricated session id — the exact provenance forgery the admission exists to prevent. Separately, the 20-minute admission TTL was anchored to the app-launch state read, so a user reaching the speech-profile step late (or any client that never calls the state endpoint) silently lost onboarding questions and is_user tagging. The bootstrap now issues or refreshes the admission from the durable account state at connect time; completed accounts still can never re-enter, and issuing stays best-effort with the read failing closed. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep the released proactivity lane open for legacy clients Gating /v1/desktop/proactivity/completions on the JIT cohort returned 403 to every non-admitted user — which is the entire deployed desktop fleet on deploy day, since shipped clients poll this route continuously and treat 403 as a plain error. Context-bucket extraction and the director would have died fleet-wide, dark cohort or not, and any environment without a PostHog key (local, self-host) would have lost the lane entirely. The route returns to merge-base admission semantics (tier quotas only); JIT admission remains enforced on the JIT reservation routes, and retiring this lane stays a later explicit operation after clients migrate. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): withhold JIT tools and history reads outside the rollout Five new tools (search_knowledge, search_historical_facts, read_playbook, get_entity_timeline, look_at_frame) sat unconditionally in CORE_TOOLS, so every legacy chat request carried their schemas and the model burned tool budget on 'no entries found' answers. They are now filtered per request off the same resolved rollout boolean that gates the JIT prompt appendix. The memories-tab ledger-history endpoint likewise answered every user with a bounded 501-row provider scan that can only ever be empty outside the rollout; it now returns empty without the scan for non-admitted (and unknown/error) states. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): bound rollout control-plane cost and confine sync resolution Synchronous callers resolved rollout flags via per-call asyncio.run against the shared provider singleton, crossing event loops: awaiting a Task attached to another loop raises, a timed-out asyncio.run strands a coalescer entry that then serves stale UNKNOWN forever, and the LRU cache was mutated from multiple threads. Sync resolution now runs on one long-lived control-loop thread with its own authority instance. Unknown snapshots gain a 5-second negative cache — UNKNOWN can never authorize work, and without it a fleet whose flags are simply absent pays one uncached PostHog call per conversation finalization. The screen-sync loop drops its force_refresh (one uncached decide per device per minute fleet-wide) and moves to its own rate bucket so two Macs' background sync can no longer starve conversation photo reads out of the shared 120/hour frame-requests bucket. The first-open policy's kill-switch telemetry label also reported str(Enum) instead of the value and could never match. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): skip eager extraction under a non-compatibility writer mode A ledger-cutover user still ran the full L1 extraction model call at finalization, after which writer admission refused the compatibility write — the conflict retried, exhausted, and failed the entire finalization for every conversation, with the model spend already paid. Extraction now checks the canonical writer mode first and skips when the daily sweep owns memory formation; only a positively-read non-compatibility mode skips, so any control-state read failure preserves the legacy eager path. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): export tolerates byte-less legacy photo rows A conversation photo row carrying the legacy empty inline marker and no storage reference failed the whole portability export forever, though it holds no durable image anywhere — there is nothing to omit. Such rows now export as metadata with a content-free gap reason. Frame requests in a retained state keep the fail-closed contract via an explicit require_bytes parameter. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(windows): harden JIT delivery, admission, and bootstrap boundaries Five verified defects: (1) the exclusive notification delivery slot leaked on any throw between reservation and commit — one SQLite hiccup during a JIT turn permanently silenced every proactive lane; the span is now try/finally-guarded and stale slots expire after ten minutes. (2) The ambient lane interpolated the raw window title into a tool-capable agent prompt; the turn now carries only the opaque context handle plus a sanitized executable name, framed as untrusted data like the nano-triage lane. (3) Google Calendar was fetched every ~60s before admission, so non-cohort users with Google connected paid ~1,440 reads a day for a refused feature; observation now gates calendar evidence on the cached authority. (4) Rollout-authority errors reset the cache and retried every frame (~1 req/s offline, forever); failures now back off from 30s to 10 minutes. (5) An unguarded JIT schema exec inside the shared database open could abort local storage for all features; the mirror bootstrap is now isolated, keeps the host-facing tables alive, and JIT stays inert when unavailable. Also re-checks the control-plane owner before committing the toast so an account switch mid-turn cannot show the previous owner's advice. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(macos): restore screen provenance, guard migrations, fence chat turns Four verified defects: (1) every pre-existing screen-derived task lost its 'Screen context / Open Rewind' source row because the new evidence policy dropped any provenance that is not rewind_frame.v1; the merge-base fallback row is restored for capture.v2/legacy refs (a test flipped to match the regression is restored to its merge-base assertions). (2) RewindDatabase published its pool before migrating, latching a failed migration into a permanent false-initialized state, and three unguarded ALTER TABLE memories migrations died with duplicate-column on machines that ran earlier builds of this branch; migration now precedes publication and the ALTERs/CREATEs are existence-guarded. (3) EventKit was queried on every context visit before the flags check; non-admitted owners now build no observation inputs. (4) A failed chat turn's reconstructed notice could be appended into a different conversation's transcript when the user switched sessions or cleared chat mid-flight; both transcript resets now revoke the active turn like selectApp already did. The pre-terminalized discard class (user Stop/watchdog) still drops the durable notice on relaunch — pinned by a characterization test in agent/tests/conversation-journal.test.ts with the least-invasive fix described there. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(testing): resolve firebase-tools from the checked-in dependency npx --prefix resolves the package bin against the current directory on some npm versions, and the admission runner deliberately launches from an isolated temp dir (firebase writes debug logs to cwd) — surfacing as 'sh: firebase: command not found' on hosts without brew node@22. Prefer the vendored node_modules binary when it matches the pin; npx remains the fallback. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): keep one eager-extraction call site for the surface ratchet Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(backend): gate eager extraction at the public boundary The writer-mode skip moves from _extract_memories_inner to extract_memories: the replace-policy contract test pins the inner helper to exactly the canonical replacement path, and the public boundary is the better seam anyway — a sweep-owned user now skips parity capture and usage tracking along with the model call. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): stub onboarding admission issuance in bootstrap regression The connect-time ensure call landed in a harness that only stubbed the read, so the bootstrap test paid an extra real-module exception path and grazed the 0.30s fast-unit CPU budget under fanout load. Stub the issuance like the read. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(listen): allowlist the bootstrap regression's CPU budget The full listen-runtime bootstrap test measures exactly at the 0.30s fast-unit CPU budget under a saturated pre-push fanout (CPU inflates ~2x there per the guard's own notes) while passing comfortably alone. It exercises deliberately heavyweight machinery; record it as an intentional exception rather than trimming the coverage. Failure-Class: none Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(backend): keep list(CORE_TOOLS) literal through JIT tool gating The JIT-only tool filter replaced the list(CORE_TOOLS) assignment with an inline comprehension, which broke the prompt-cache structural invariant (test_prompt_cache_optimization.py::test_core_tools_used_in_both_functions). Restore the list(CORE_TOOLS) copy and apply the JIT-only filter as a conditional pass, preserving rollout semantics and tool order. * feat(jit): drop automatic goal updates from the JIT featureset Product decision (David, 2026-08-26): goals change only through explicit user action for JIT-admitted conversations. Goal progress is no longer a first-open obligation — the effect is removed from FIRST_OPEN_EFFECTS and the worker, and the policy plan can no longer express deferring it. Legacy obligations carrying a pending goal_progress row are normalized away and complete on the remaining two effects. Non-JIT (legacy eager) conversations keep today's automatic goal updates unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): one summary-spine agent pass per day, with folder backstop Replaces the per-conversation transcript extractor in the completed-day producer with a single two-phase agent run: the whole day's conversation summaries go in as one bounded spine (200 conversations / 120k chars — effectively unreachable, so heavy days no longer stall the cursor), and the agent may request up to 8 raw transcript excerpts (8k chars each) to verify specifics before finalizing. At most two provider calls per user per day, both inside the existing at-most-once invocation fence; the staged page carries the memory candidates AND folder assignments for the day's unopened, unfiled conversations, applied idempotently (first-open or user assignment always wins). Memories must cite their source conversations; uncited output is dropped. The cost gate becomes a worst-case ceiling checked before any call. The onboarding cold-start channel keeps per-conversation transcript extraction unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): harden the daily agent prompts from a real-data lab pass Iterated on one real heavy day (26 conversations) with strong- and weak-model stand-ins, an adversarial judge, and hand-verified transcript ground truths. Rules added, each pinned to an observed failure: actor binding in active voice with a personal-attribute gate (a discussed or recommended topic is never someone's attribute; judgments about named people are stored as assessments); decision-state basis labels binding the verb (decided/proposed/observed, discussed-no-outcome dropped); salience ordering (money, metrics, named-party intent, identity, and durable decisions before any operational fact; one fact per memory); never guessing the direction of an invitation/offer/commitment (verify or drop); and no deferring the whole answer to verification. The agent output schema gains a 'basis' field. The memories QoS call-site inventories now count the daily-sweep agent's call site (3 -> 4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): tune the daily agent prompts against the real memories model Ran the assembled prompts against gpt-5.6-luna (the real 'memories' route model) on the same real day. Three refinements from observed behavior: the basis label no longer leaks into memory text (metrics read as metrics, not 'David observed that…'); the never-guess-direction trigger is mechanical (passive/verbless summary phrasing or 'Speaker' as the actor forces a transcript_request — luna confidently inverted 'Tim: Invited to New York' until this; with it, phase B verifies and corrects to the true direction), hedging is itself a request signal, and nothing high-salience may be silently dropped; and a rich-day yield anchor (8-16 memories for 15+ conversations) counters the model's over-pruning without inviting padding. Final real-model run: 11 true memories + 2 legitimate verification requests, zero fabrications, ~22k tokens (~2 calls) for a 26-conversation day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sweep): profile-maintaining slots, ledger lookups, cache-ready prompts The daily agent now sees the user's current profile (the same get_prompt_memories seam chat uses — the ledger render for migrated users), may run up to 4 owner-scoped prior-memory keyword lookups (provider fail-soft; hits re-read through the canonical store before disclosure) to dedup and supersede, and may name a slot for standing attributes — an occupied slot becomes an amend through the existing canonical occupancy check, so the daily run maintains the rendered profile with no second write path. Both phase prompts share a byte-identical prefix (pinned by a test) and pass a per-user prompt_cache_key through get_llm; measured against gpt-5.6-luna the provider cache is exact-match rather than prefix-based today, so this is future-proofing rather than present savings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): type the memory-searcher seam for the pyright contract CI's authoritative typecheck rejected the untyped lookup seam (memories.py: list(Any or [])). The searcher is now Optional[Callable[[str], Sequence[str]]] and results are built through a typed comprehension; behavior unchanged (absent or failing searcher still degrades to an empty result block). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair four main-inherited CI breakages after sync origin/main is currently red on its own tip; syncing it into this PR inherits the breakage, so the fixes ride here: - subscription.py: drop the unused get_byok_keys import (pyright reportUnusedImport fails the Backend unit suite). - AppState+Transcription.swift: explicit self for alertPresenter inside the escaping showAlert completion (strict-concurrency compile error in all three Desktop Swift lanes, shipped red on main by d49f978512). - AppState+Permissions.swift: pinned swift-format drift from the same main commit (desktop-swift-format-lint). - web/app/bun.lock: add the prettier + prettier-plugin-tailwindcss entries 64db30c791 pinned in package.json without updating the lockfile (frozen install fails web-app-checks). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sweep): close the second review round's findings Three parallel adversarial reviews over the post-takeover additions: - Clamp every model-controlled phase-B input (draft memories, request reasons, lookup queries/results) and add the clamped worst case to the pre-call cost ceiling, which previously under-estimated phase B. - Attest an empty consumed day when the staged page carries an older stage schema version instead of stalling the cursor forever on every deploy-boundary schema bump. - Make the folder backstop's unfiled check and write share one transaction so a concurrent first-open/user assignment always wins. - Let equal-rank sweep candidates amend sweep-authored slot occupants: the profile-maintenance path froze after a slot's first write. User statements still always win; slotless subject matches still dedup. - Neutralize ``` fences in summaries/excerpts/lookup results, and mark raw-transcript fallback rows '(unstructured transcript excerpt)' with a prompt rule refusing slots/personal attributes from them without transcript verification (test pins the marker to the rule). - Remove the dead first-open goal-authority threading left by the goals removal, and update the stale jit-first-open-runtime doc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: repair three more main-inherited breakages All shipped red on main and only surfaced once earlier failures were cleared: - AppState.swift: move the alertPresenter default out of the stored property initializer — Xcode 16.4's SILGen segfaults (signal 11) emitting it, which failed all three Desktop Swift lanes even after the explicit-self fix. - test_byok_security.py: main's BYOK rewrite (d0e3a4eb3a, 1da8880175) changed request_has_llm_byok_key to per-provider enrollment checks and made partial headers fail closed, but left the tests targeting the old get_byok_keys()-based lenient contract (masked on main because pyright failed before pytest ran). The tests now assert the shipped strict contract their own docstrings already describe. - subscription.py: pinned-black formatting for the BYOK fallback expression (the Formatting lane rejects the file as main wrote it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): stub the chat-agent gateway route pin in the chat router harness Main's a6988be309 made routers.chat import CHAT_AGENT_ROUTE_DIRECT / get_chat_agent_route from utils.llm.gateway_client, but the chat-router test harness (and test_chat_file_upload_unsupported's local override) stub utils.llm.gateway_client without those symbols, so every suite that loads the real router failed at import — masked on main because pyright fails its Backend unit suite before pytest runs. Ninth main-inherited repair in this sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): teach test_chat_quota's utils.byok stub the rewritten import surface utils/subscription.py now imports get_byok_uid and get_cached_byok_state (main's BYOK rewrite); the module-scoped utils.byok fake predates them, so reloading subscription under the fake raised ImportError at setup — and the polluted process took test_chat_openapi_operation_ids and test_desktop_screen_crisp down with it in CI's batched run (all three pass standalone). Tenth main-inherited repair, same pyright-masked pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): update three more suites for main's BYOK/gateway import surface Same pyright-masked pattern as the harness and test_chat_quota repairs: - test_desktop_transcribe stubbed utils.llm as a non-package, so routers.chat's new utils.llm.gateway_client import could not resolve (50 failures); the submodule is now in its stub list. - test_paywall_reconnect_gate's BYOK escape-hatch tests never set the request uid context that the enrollment-verifying rewrite requires (middleware sets it in production); they now do, and teardown clears it. - test_chat_session_app_identity's enforce_chat_quota stub rejected the new required_llm_provider keyword. All three suites pass locally (69 + 35 + 6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): enroll fingerprints in the desktop BYOK tests PR #11454 moved macOS BYOK activation to enrollment-verified fingerprints (isByokActive and usableBYOKEnvironment gate on persistEnrolledFingerprints), and its own test lanes shipped red: the tests store raw keys but never enroll them, so every key reads as inactive. Their teardowns already clear enrollment — the setups now enroll what they store, matching the production activation path. All 8 previously-failing cases (BYOKPaywallTests + the two AgentRuntimeProcessTests BYOK-environment cases) pass locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(deploy): enable the daily memory sweep on development The sweep's five deployment inputs were pinned off in every environment, so cohort enrolment alone could never start it -- turning it on for a dogfood account required a second PR. Development now carries the live values: - ENABLED/MODEL_ENABLED on, so the job stops exiting at its first authority gate and the model authority can budget a route. - MODEL_NAME pinned to gpt-5.6-luna, which is the declaration interlock the runner checks against get_model('memories') before any provider call. - MAX_MODEL_COST_USD 0.80, the worst-case pre-call ceiling for a maximal day including phase B's clamped draft/reason/lookup overhead. - COHORT_ENABLED on with COHORT_FLAG daily-memory-sweep-v1, so enrolment is a per-uid PostHog boolean and an unnamed cohort stays a closed rollout. Production is deliberately untouched and stays fully pinned off. The job still cannot form a memory for anyone until that flag exists and resolves true for a uid, which remains a control-plane action rather than a deployment one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(firestore): terminate the daily-sweep occupant indexes with __name__ The six daily-sweep occupant lookups were the only declarations in the manifest without a trailing __name__ field -- 63 of 69 entries carry one, and main had none missing it. Firestore appends the terminator itself and reports the index back that way, so these six could never match the live inventory. The failure mode is not a missing index; the indexes build fine. It is that reconciliation never converges: every run reports the same six as missing, tries to create them, and fails on ALREADY_EXISTS. That takes down the Firestore schema workflow on both environments permanently, and with it the development backend deploy's readiness gate -- the same class of outage the workflow's own header records from the hourly_usage index in PR #11979. The derived specs previously appended their extra predicates to the base spec's index_fields, which would have placed them after the terminator, so the shared prefixes are now named explicitly and each spec ends with __name__. Verified against real Firestore: reconciliation reports zero missing indexes in both based-hardware and based-hardware-dev. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: close final JIT rollout and CI gaps Fence direct JIT tools and frame pixels, keep Windows account wipes safe after optional schema failures, and repair inherited CI regressions. Failure-Class: none --------- Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 27 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 3 个月前 | ||
| 6 天前 | ||
| 6 天前 | ||
| 6 天前 | ||
| 1 天前 | ||
| 7 天前 | ||
| 1 天前 | ||
| 6 天前 | ||
| 27 天前 | ||
| 6 天前 | ||
| 1 个月前 | ||
| 6 天前 | ||
| 7 天前 | ||
| 6 天前 | ||
| 6 天前 | ||
| 27 天前 | ||
| 16 天前 | ||
| 29 天前 | ||
| 6 天前 | ||
| 27 天前 |