| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
Merge origin/main into fix/app-auto-disable-recovery The branch was 1007 commits behind main; 101 paths conflicted. Resolutions: - app/lib/l10n/*.arb (49 files): merged per key, three-way against the merge base rather than by hand. Both sides had appended keys at the same offset, so the textual conflict was positional, not semantic. main's content and key order win; the branch's 9 new keys (appDisabledTitle, appDisabledWebhookFailures, appDisabledGeneric, appDisabledOwnerHint, appReEnable, appReEnableFailedTitle, appReEnableFailedBody, appDisabledOn, appDisabledLastError) are carried over, plus their @-metadata in the app_en template. No key was changed on both sides, so no translation was silently dropped. Files stay byte-identical to `jq --indent 4` output. - app/lib/l10n/app_localizations*.dart (50 files): regenerated with `flutter gen-l10n` from the merged ARBs instead of resolving generated output. - web/app/src/components/apps/AppDetail.tsx: the branch's disabled-app guard on the install button is kept (disabled + title when app.disabled && !app.enabled), as is its `disabled:cursor-not-allowed`. main wins everywhere else: the button, step badge, and developer-response border keep main's de-purpled palette rather than reintroducing bg-purple-primary/text-purple-primary/border-purple-primary (INV-UI-1), and the owner edit route stays /connectors/{id}/edit — main renamed it, and /my-apps/{id}/edit no longer exists. - web/app/tsconfig.tsbuildinfo: took main's deletion; it is a build artifact and main added it to .gitignore in cac340c1ef. Verification: `flutter analyze` reports 0 error-severity issues (158 pre-existing info lints); `flutter gen-l10n` completes with no untranslated-message warnings across all 49 locales; no conflict markers remain in any tracked source file. | 21 天前 | |
fix(native): harden limitless codec length parsing and align audio classification with Dart The Kotlin port truncated varint lengths to Int before bounds checks; a 5-byte length like 2^33-6 becomes -6, walking the parse position backward and spinning the drain executor forever. All length math now happens in Long space and overflowed-negative lengths end the walk, matching Dart's bigint overshoot behavior. hasAudioSubfields on both platforms now mirrors the Dart reference: skip fixed64/fixed32 fields, and classify unknown wire types and parse anomalies as audio so a page whose extraction failed is never surfaced with zero frames and ACKed away (an ACK deletes the pendant's copy). Three new golden fixtures pin these paths across all three implementations; the Kotlin replays run under JUnit timeouts so a reintroduced non-terminating walk fails instead of hanging. | 2 个月前 | |
Establish a canonical subscription plan catalog (#11979) * docs: decide subscription plan authority and migration * feat: establish canonical subscription plan catalog * feat(plans): resolve free quota and chat exhaustion policy * fix(app): preserve unknown subscription plan ids * fix(macos): decode subscription plans losslessly * fix(macos): deny unknown plans paid capability * feat(web): decode subscription plans losslessly * fix(windows): decode subscription plans losslessly * feat: route desktop and phone policy through plan catalog * feat(plans): derive chat policy from catalog * fix(plans): make transcription limits explicit * feat(usage): attribute realized cost to catalog plans * docs(plans): mark the wire zero-sentinel as a deliberate W1-gated bridge The backend retired 0 == unlimited (catalog uses typed {kind: unlimited}, projected as None), but the wire has not been migrated: shipped clients still read 0 as unlimited. The None -> 0 coercion in the subscription response is therefore load-bearing, not a leftover. Annotate it so it is not 'fixed' out of sequence, and branch explicitly on None in the admin reset script where the same coercion would have folded a genuine finite 0 into unlimited. * test(plans): supply _effective_chat_limit to the trial-metadata synthetic namespace test_trial_metadata execs source text extracted from utils/subscription.py in a hand-built namespace. D3a routed get_trial_metadata and TRIAL_FEATURES through the new catalog helper _effective_chat_limit, which the namespace did not provide, so all 19 behavioral trial tests raised NameError. This is the failure mode of source-parsing tests that the design doc calls out when retiring TestStripeEntitlementMismatchScannerDrift: the test does not depend on behavior, it depends on the text of the function it copies. * fix(plans): make the catalog's new failure modes observable and pinned Findings from an independent glm-5.3 review pass, verified against the merge-base before acting: - payment.py: a retained-vs-configured price disagreement now raises where the pre-catalog code let the env mapping win. The bare 'except ValueError: return None' turned that into a silent stop to a paying subscriber's webhook writes. Emit record_fallback + a sanitized error so the Apr 17-20 failure shape is observable rather than log-only. - test_overage_catalog.py: pin chat-limit zero semantics. Base guarded on 'limit_value > 0', so 0 meant unlimited; per David's ruling 0 now means zero and denies. The tests call get_chat_quota_snapshot directly and were mutation-checked: restoring the old guard fails them. - users.py: name the inverse hazard for W1 -- 'or 0' also launders a finite zero into the unlimited sentinel. Latent today, silent capability grant if a plan ever declares a finite-zero allowance. - generate_plan_catalog.py: note that a None base catalog skips the append-only guard, which is correct only for the PR introducing the catalog. * chore(desktop): changelog fragment for the plan-catalog decoder work * test(plans): mirror the new fallback-telemetry names into the payment.py namespace Second instance in this PR of the same failure mode: test_stripe_webhook_behavioral execs source text extracted from routers/payment.py in a hand-built namespace, so the record_fallback/logger/sanitize calls added to the unresolvable-price branch raised NameError there. The test broke on the text of the function it copies, not on its behavior -- which is the argument for retiring source-parsing guards in favour of the catalog-wide check. * fix(web): make the MemoryCard timestamp assertion timezone-independent MemoryCard renders created_at through toLocaleDateString, which formats in the runner's local timezone. The fixture is UTC midnight, so the hardcoded 'Aug 1, 2026' label is 'Jul 31, 2026' on any runner behind UTC and the layout test failed for a reason unrelated to the layout it guards. Already broken on main; it blocks the pre-push web lane for every change touching web/app. Derive the expected label the same way the component does. * feat(firestore): register the hourly-usage plan-attribution query M1's get_usage_by_plan adds a year==/month== compound query on the per-user hourly_usage subcollection, which the coverage ratchet correctly flagged as a new unregistered serving shape. Register it as a query spec rather than baselining it -- baselining a brand-new shape is what the ratchet exists to prevent -- and regenerate firestore.indexes.json from the registry. * fix(plans): fail clearly on an unlimited basic transcription allowance pyright flagged two real errors in the new code. allocation_limit returns None for an unlimited allocation, so the module-level '_BASIC_TIER_SECONDS_DEFAULT // 60' would raise TypeError at import time if the catalog ever declared basic.transcription unlimited. Free is metered by design, so that is an authoring mistake; raise with a sentence that says so instead of crashing on an operator. Also drop the now-unused firestore import left behind in desktop_realtime.py by the cost-attribution work. * chore(api): regenerate the app-client OpenAPI contract cost_usd on RecordLlmUsageBucketRequest is now nullable without a 0.0 default, so an unreported cost stays unreported instead of being recorded as a measured zero. It is a request field, so existing senders are unaffected. No plan enum or plan-bearing response shape changed. * chore(api): regenerate TypeScript clients for the nullable cost_usd Single-line change in each generated client: cost_usd?: number becomes cost_usd?: number | null, matching the OpenAPI regeneration. * fix(plans): restore chat wording, and make the BYOK bypass tests real Three failures surfaced by a per-file isolated sweep of the backend suite. - Plan storefront copy regressed from '{N} chat questions per month' to '{N} questions per month' when the text moved into the catalog. That is user-visible product copy; a consolidation must not reword the product. Restored, with a note saying so. - test_stripe_webhook_none_guard asserted the literal source text 'except ValueError:', which the added telemetry turned into 'except ValueError as e:'. Third source-parsing test in this PR to break on wording rather than behavior; matched on the handler instead. - The BYOK transcription-bypass tests patched utils.byok.get_byok_key, but subscription.py binds that name at import, so the patch never applied and the bypass branch never executed. They passed at base by falling through to the plan-limits path and returning True for an unrelated reason -- the bypass they are named for had no coverage. Patch subscription's own binding and assert the subscription is never consulted, so the short-circuit is actually proven. * fix(plans): legacy zero overlays keep meaning unlimited Independently found by both review passes (codex sol P2, cursor grok P1), and the most consequential defect in this branch. Production sets BASIC_TIER_WORDS_TRANSCRIBED_LIMIT_PER_MONTH and BASIC_TIER_INSIGHTS_GAINED_LIMIT_PER_MONTH to literal '0' on the pre-catalog convention where 0 meant unlimited. Reading them as a finite zero would have given every Free user a zero words/insights allowance and advertised '0 words transcribed per month'. Retiring the sentinel is right; silently reinterpreting already-deployed configuration is not, and it contradicted this PR's own claim that production behavior is unchanged. Legacy overlay values are now read through _legacy_overlay_value, which maps a legacy 0 to unlimited. Applied to the minutes overlay too: charts set 300 so it was latent, but a deployed 0 there would have made has_transcription_credits return False for every Free user. Also: attribute the residual on MIXED usage documents. The first post-deploy write adds plan_usage to a document that already carries pre-deploy root counters; keying only on plan_usage's absence dropped that earlier usage from per-plan reporting entirely. And correct the design doc, which still described B1/B3 as open after the rulings landed. * refactor(plans): assign the Free transcription constants once, with clean types pyright rejected the branch-per-constant form (reportConstantRedefinition) and the string sentinel widened the overlay type to int | str. Return (present, value) from _legacy_overlay and compute both constants in one helper, so each is assigned exactly once and the unlimited case stays typed Optional[int]. * fix(plans): give the Free transcription default a narrowed int type pyright does not carry the module-level None check into the helper's body, so '_BASIC_TIER_SECONDS_DEFAULT // 60' still read as an Optional operand. Narrow once after the guard and annotate the constant as int. * refactor(plans): Stripe owns price amounts; the catalog owns price identity David's ruling, 2026-08-20, revising the original Q1 answer. The repository now stores no dollar amount at all. payment.py already calls stripe.Price.retrieve and renders unit_amount live, so the storefront has always shown Stripe's number; every in-repo dollar figure was display copy, a comment, or a test fixture that nothing compared against Stripe. Removing them dissolves the price-drift problem instead of guarding it -- with one copy there is nothing to drift, and changing a price touches no repository file. Deleted: the amount field on every catalog price, publication_state, the prepare/promote state machine (validate_stripe_publication, price_spec_digest, the --bindings/--stripe-snapshot flags) and its tests, and the publishable gate's 'price has not been imported' errors. Work items P1 and P2 are withdrawn. Kept: the append-only price-id -> plan ledger. Stripe cannot supply that mapping -- it does not know our plan enum -- and an unrecognised price id is exactly what dropped paying subscribers to free in the Apr 17-20 incident. In Git it gets code review, history, and a local lookup that survives a Stripe outage. Storing it in Stripe price metadata was considered and rejected: a typo would become a subscriber-affecting bug with no review and no audit trail. Verified: all ten production and dev price ids still resolve to the correct plan. * fix(desktop): let flow lint see the bridge extension files DesktopAutomationBridge was split into +Notifications and +ChatFirst extension files, but ACTION_SOURCE_RELATIVE_PATHS still listed only the base file. The lint therefore could not see actions those extensions register and reported notifications-settings.yaml as referencing two unknown bridge actions -- a valid flow failing against a stale file list. This was red on main, failing desktop-core-e2e-t0 for every PR touching desktop and blocking the pre-push gate. The contract file's own comment asks for exactly this list to stay complete so an added action cannot skip flow validation. desktop-flow-lint now reports OK: 72 flows, 162 registered actions. * fix(app): drop the now-unused subscription import in settings_drawer The analyzer ratchet allows zero unused_import and reported one. It is ours by reclassification: converting PlanType from an enum to a class changed what models/subscription.dart exposes, so settings_drawer's import went from unnecessary_import (baseline 11, now 10) to outright unused. Nothing in the file referenced a subscription symbol. Verified: analyzer ratchet passes, flutter test 1400 passed / 5 skipped. * test(desktop-chat): stop the BYOK exclusion test building a real Firestore client CI's hermetic network guard failed test_record_usage_skips_byok_requests with BlockedNetworkError against the GCP metadata server. The BYOK skip path passes firestore_client=get_customer_firestore_client(), which is evaluated before the stubbed recorder runs, so a real client was constructed and resolved credentials. It passed locally only because gcloud ADC credentials were present. Stubbing the factory in the test is the correct fix rather than dropping the argument: record_llm_cost_exclusion falls back to the default 'db' when given None, not to the customer client, so removing it would silently change which database exclusions are written to. Verified: 78 passed in test_desktop_chat.py. * fix(usage): stop double-counting attributed questions as unattributed Findings from a glm-5.3 review sweep, each verified against the code first. HIGH -- my own residual-attribution fix was wrong. It read the attributed total from plan_data['questions'], a field no writer writes: questions live at plan_usage.{plan}.{bucket}.quota_questions. The attributed sum was therefore always 0, the residual was the document's FULL root count, and every post-deploy document was counted twice -- once against its real plan and once against _unattributed. questions_by_plan summed to ~2x the truth and fed the per-plan report that MEASUREMENT_CONTRACTS['chat'] points at. Quota enforcement reads the root counters, so no entitlement impact. Now sums recursively the way _accumulate_plan_data traverses, and only creates an _unattributed row when the residual is actually positive. Two regression tests, both failing before. LOW -- the fallback telemetry I added used reason='unrecognized_price', which is not in ALLOWED_REASONS, so bucket_reason relabelled it 'other' and the reason dimension was lost. Now 'config_incomplete'. LOW -- lookup_key validation sat inside the currency-invalid branch, so it only ran when the currency was already wrong. It was publication-machinery schema with no consumer left; removed rather than repaired. Docs: the bridge covers only the Basic overlay family (the chat and Plus overlays are deliberately unbridged because nothing deploys them) -- the doc claimed all three and named a function that does not exist. Also corrected stale claims that survived the publication deletion: catalog-owned 'price amount in minor units', the deleted snapshot validator, --require-publishable reporting P1, and B1/B3/D1 still listed as open. Recorded an unverified dev-Architect ledger gap rather than appending IDs on the evidence of a test comment. | 22 天前 | |
fix(app): re-read device storage after clearing recordings (#12864) * fix(app): re-read device storage after clearing recordings Clearing recordings from the Sync page deleted the files but left the storage card reporting the device as full until the user navigated away and came back. `DeviceProvider.refreshRingStorageStatus()` had exactly one caller: the page's `initState` (auto_sync_page.dart:70). The three Manage Storage actions each awaited their `deleteAll*Wals()`, which refreshes the recordings list via `SyncProvider.refreshWals()`, and then showed a success snackbar — but nothing re-read the ring status the card renders. So the list updated and the card did not, and only leaving the page re-ran the page-open read that corrected it. Observed on a real device: 9 pending recordings cleared to 1, "Pending recordings deleted" shown, and Device Storage still reading 100% full, 472 MB of 472 MB used, 12 KB free. Backing out of the page and re-entering it showed the true 0 B of 472 MB used. The files were deleted correctly throughout; only the on-screen snapshot was stale. All three actions now route through `clearRecordingsThenRefreshStorage`, which runs the clear and then the re-read. Ordering is load-bearing — a read taken first returns the pre-clear numbers being corrected. The re-read also runs when the clear throws, because a clear that failed part-way still deleted files and leaves the card just as wrong; the failure still propagates. Verification: - Both cases fail before the change (the helper does not exist) and pass after: ordering, and re-read on a clear that throws. - flutter test test/pages/ test/providers/: 340 passed, 0 failed. - flutter analyze on both touched files: No issues found. - dart format --line-length 120: both files unchanged. - The real user-facing path is what surfaced this: screenshots before and after the clear, and after re-entering the page. I have not exercised the fixed build on the device — no local iOS build was produced for this change. Failure-Class: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017pEhHaD3Mjxwn1hYUTHbQM * fix(app): pair every Manage Storage clear with the storage re-read Review on #12864 (cubic) was right: the first version tested the pairing helper in isolation, so the Synced, Pending and Clear All handlers could each drop the re-read without failing a test — the exact defect this PR fixes, left uncovered. The three actions are now built at one construction site, `buildStorageClearActions`, which pairs each clear with the re-read. A handler can no longer be added or edited without the pairing, because the handlers no longer own it. Verification: - Regression proven by mutation: replacing `pending: () => thenRefresh(...)` with the bare `clearPending` fails 2 tests (Expected ['clear pending', 'refresh'], Actual ['clear pending']). Reverted. - Five cases now cover all three actions individually, the category isolation, and the re-read surviving a clear that throws: 5 passed. - flutter test test/pages/ test/providers/: 343 passed, 0 failed. - flutter analyze on both touched files: No issues found. - dart format --line-length 120: applied. Failure-Class: none Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017pEhHaD3Mjxwn1hYUTHbQM --------- Co-authored-by: Copilot CLI <copilot@forge-terminal.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 6 天前 | |
fix(conversations): compute duration from the transcript span on the backend discard gate and macOS (#13047) * fix(conversations): measure the discard gate's duration from the transcript `started_at` on a live-socket conversation is the streaming-session origin — the STT stream offset plus the socket's first-audio-byte wall time — not the moment this conversation's speech began, so it drifts tens of minutes behind wall clock across a long socket. `_get_structured` computed `finished_at - started_at` and handed that to `should_discard_conversation`, whose prompt only applies its stricter "under 2 minutes" bar below 120s. An 8-second dictation scrap recorded 42 minutes into a socket therefore reached the model labelled 2565 seconds, the short-content rule never fired, and 8s / 17s / 18s scraps were kept (measured on a real account 2026-09-07). Adds `utils/conversations/duration.py` as the single authority for a conversation's duration: the transcript span (largest validated segment `end`) when the record has usable segments, the wall window only for transcript-free records such as photo-only captures. Segments with blank text, non-finite bounds, or `end < start` are ignored; a record whose segments all fail validation falls back to the wall window and records a `record_fallback` because that input is degraded. Mobile already ships this rule (`ServerConversation.getDurationInSeconds`, #4056); this is the backend half. No change to `started_at`/`finished_at` derivation or persistence, the discard prompt, its thresholds, or the calendar-overlap override. Verification (backend/.venv, Python 3.11.13): - `python -m pytest tests/unit/test_conversation_duration.py` — 13 passed - `python -m pytest tests/unit/test_discard_gate_duration_input.py` — 3 passed; reverting the `_get_structured` change fails it with `2565.0 != 8.0` - `python -m pytest tests/unit/test_discard_calendar_override.py tests/unit/test_process_conversation_free_tier_branch.py tests/unit/test_conversation_discard_revival.py` — 40 passed alongside the new module in one process (its fixture stubs `utils.metrics` / `utils.observability.*` so a second fresh load cannot duplicate-register Prometheus collectors) - `python scripts/check_module_stub_pollution.py` — 0 violations - `python scripts/scan_import_time_side_effects.py` — 0 violations Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(desktop): show conversation duration from the transcript span, not the socket The macOS row and detail header read `finished_at - started_at`, which is the lifetime of the live capture socket rather than of this conversation, so six consecutive conversations measured on a real account 2026-09-07 showed 14m31s / 19m13s / 24m13s / 35m19s / 36m0s / 42m45s where mobile showed 17s / 1m24s / 46s / 2m12s / 18s / 8s. The worst case, an 8-second dictation scrap, rendered as "42m 45s". `ServerConversation.durationInSeconds` now prefers the transcript span (largest validated segment `end`, ignoring blank-text, non-finite, and reversed segments) and falls back to the wall window only when no segment can answer — matching `utils/conversations/duration.py` and the Flutter `getDurationInSeconds` (#4056). The old code also took `transcriptSegments.last` rather than the maximum `end`, so an out-of-order tail under-reported. `formattedDuration` and every caller are unchanged. A list response that omits `transcript_segments` still reports the wall window; that is the same answer mobile gives, and there is nothing else to measure. No `recordFallback` on the wall-window branch: `durationInSeconds` is a computed property evaluated on every row render, so emitting there would produce unbounded telemetry for a static condition. The backend helper records that fallback once, at the seam that owns the record. Toolchain: this Mac has Xcode 26.x while CI pins 16.4, so these results are the local toolchain's, not proof the pinned CI toolchain agrees. Verification (`swift-driver version: 1.135.3`, Apple Swift version 6.2.4, target arm64-apple-macosx26.0): - `xcrun swift build --build-tests --package-path Desktop` — Build complete - `./scripts/dev-feedback.py --once swift 'ConversationDurationTests'` — 13 tests, 0 failures - reverting `durationInSeconds` to its previous body fails all 13, including `("2565") is not equal to ("8")` and `("42m 45s") is not equal to ("8s")` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(parity): pin the conversation-duration rule across backend, app, and macOS Conversation duration is exactly the class of rule the parity contracts exist for: one product rule, three implementations, and a silent divergence that showed 42m45s on macOS and 8s on mobile for the same 8-second capture. `contracts/parity/conversation_duration.json` holds the vectors the three platforms agree on — transcript span wins over the wall window, the span is the maximum segment `end` rather than the last element, a zero-length transcript is zero and not the window, and a transcript-free record falls back to the window. Each platform runs them through its own production code: `conversation_duration_seconds`, `ServerConversation.getDurationInSeconds`, and `ServerConversation.durationInSeconds`. The backend leg also checks the fixture is internally well-formed so a rotted vector cannot pass vacuously everywhere at once. Malformed inputs are deliberately outside the fixture and recorded as Divergence register entry 6: backend and macOS validate each segment and clamp a reversed wall window, while the Flutter getter takes the raw maximum `end` and returns a negative duration unclamped. Flutter is the losing platform; converging it means changing `app/lib/backend/schema/conversation.dart` and adding those vectors in the same PR. Verification: - `python -m pytest tests/unit/test_parity_contracts.py` — 16 passed - `flutter test test/parity/parity_contracts_test.dart` — 35 passed, 5 skipped (day-key cases outside the runner's zone offset); all 8 duration cases ran - `./scripts/dev-feedback.py --once swift 'ConversationDurationTests'` — 13 tests, 0 failures, including `testSharedDurationContractVectors` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(failure-classes): add FC-capture-session-window-read-as-content-duration Registers the class this PR's two fixes instance: a duration displayed to a user or fed to a policy decision must be measured from the content it describes, not from a capture-session timestamp that merely bounds it. The subtraction never fails, so the wrong number reaches a UI label and an LLM policy prompt with no error, no metric, and green per-platform tests. Its guard artifacts are the two surfaces added in this PR: the shared vectors in `contracts/parity/conversation_duration.json`, which every platform runs through its own production code, and `backend/tests/unit/test_discard_gate_duration_input.py`, which drives the real `_get_structured` seam and captures the value the discard gate receives. None of the 29 scope-matched existing definitions states this contract; the nearest are about omitted mirror members, re-derived day-bucket keys, and PTT capture start latency. Verification: `scripts/failure-class validate --pr-body-file .pr-body.md` Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(review): harden duration helpers from Cubic review - macOS: clamp the transcript span into Int range before Int(Double); a finite segment end beyond Int.max traps and crashes the client (P1) - backend: normalize naive timestamps to UTC before the wall-window subtraction; naive-minus-aware raised TypeError and silently dropped duration metadata from the discard prompt (P2) - docs: state that the wall-window fallback also covers records whose segments all fail validation, matching the malformed-doc branch (P3) * style: dart-format the conversation-duration parity test The pre-push gate formats changed files; this file was added unformatted in the parity-test commit. * style: reindent the trailing parity map literal per dart format line-length 120 * fix(desktop): clamp out-of-range span without trapping on the Int.max boundary Double(Int.max) rounds up to 2^63, so Int(Double(Int.max)) traps — the clamp added in 9c0996815d crashed the exact case it was meant to guard. Compare in Double space before converting; the clamp now returns Int.max. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> | 5 天前 | |
ci(desktop): pin ship Xcode and fail closed on compiler-gated APIs (#13564) * ci(desktop): pin ship/CI Xcode to 26.6 (17F113) from one source of truth PR #12867 looked right on a local Xcode 26 named bundle and shipped the icon-less system Picker fallback: Liquid Glass sat behind #if compiler(>=6.2) while desktop CI (GHA desktop-swift-ci.yml and Codemagic omi-desktop-swift-release/-preview) compiled with Xcode 16.4, so the ship toolchain never typechecked the glass APIs (#13548 reverted the tab bar). Bump the desktop ship/CI toolchain off 16.4 and make drift impossible: - desktop/macos/ci/xcode-pin.json is now the only place the version (26.6), build (17F113), and expected app path (/Applications/Xcode_26.6.app) live. 26.6/17F113 exists on both vendors: GHA macos-26 image 20260907.0351.1 (default Xcode, /Applications/Xcode_26.6.app) and Codemagic mac_mini_m4 image 'Xcode 26.6.x (default)' (/Applications/Xcode-26.6.app). - run-swift-ci.sh reads the pin file (fails closed when it is missing) and keeps asserting the exact version+build after selection. - desktop-swift-ci.yml macOS jobs move macos-15 -> macos-26, cache key prefixes move xcode164 -> xcode266 so 16.4 caches cannot poison 26.x builds, and the select steps name the pinned version. - codemagic.yaml omi-desktop-swift-release/-preview move xcode: 16.4 -> 26.6 (Android 16.4 and iOS 26.0.1 untouched); the reviewed codemagic_workflow_contract digests are refreshed for that edit. - test_desktop_swift_ci_contract.py loads the pin file instead of literals and adds codemagic pin-agreement + macos-26 runner assertions. Verified locally: bash desktop/macos/tests/test-run-swift-ci.sh (pin 26.6/17F113 sandbox incl. wrong-version/wrong-build/missing-pin sabotage); python3 .github/scripts/test_desktop_swift_ci_contract.py (38 tests OK); python3 .github/scripts/check-release-process-guards.py (exit 0); xcrun swift build -c debug --package-path Desktop on Xcode 26.6 (17F113) completes (1855s). * ci(desktop): fail closed on compiler-gated Apple SDK APIs New desktop-compiler-gates manifest check: desktop/macos/scripts/ check-desktop-compiler-gates.py fails when Desktop/Sources or Desktop/Tests contain #if compiler(...) / #elseif compiler(...) — the shape that let #12867 compile Liquid Glass out of the Xcode 16.4 ship toolchain while CI stayed green (#13548 reverted the tab bar). Runtime availability gating (if #available(macOS 26, *) with a working fallback) is the sanctioned pattern and passes. The allowlist ships empty and is asserted by exact contents in test_check_desktop_compiler_gates.py, so it cannot silently grow; entries require a documented reason and a matching path+line. checks-manifest.yaml also extends desktop-swift-ci-contract triggers with the pin file, runner script, launcher test, and codemagic.yaml so every pin consumer re-runs the contract test on drift. Verified locally: python3 .github/scripts/test_check_desktop_compiler_gates.py (9 tests OK — planted #if compiler(>=6.2) glassEffect sabotage fails the checker and removing it passes); python3 desktop/macos/scripts/ check-desktop-compiler-gates.py on the real tree reports none found; make preflight manifest lane: all selected checks pass except the pre-existing loaded-host flake in test_pr_preflight.py SingleFlightTests.test_identical_processes_join_and_execute_once, which fails 3/3 on pristine origin/main at the same host load (passes in isolation) and is unrelated to this diff. * changelog(desktop): mark ci xcode pin as internal-only (kind: none) The desktop changelog check requires an in-repo fragment for production desktop paths even with the no-changelog-needed label: the label is invisible after merge and would redden main's Release Eligibility run. This PR is CI/infra only (toolchain pin + fail-closed gate), so the marker is kind: none. * ci(desktop): raise verify ceiling to 90m for the first cold macos-26 full lane Measured on run 34687313733 (PR #13564): with the pin moved to Xcode 26.6, every cache cold under the new xcode266 keys, the full Swift suite ran past 55 minutes and the 60-minute job ceiling cancelled the job mid serial-cluster. Every completed suite was green (one wedged batch cost 1650s before its 1500s watchdog bisected it cleanly; zero test failures) — purely a timing ceiling, so: - desktop-swift-verify timeout-minutes 60 -> 90 - full-lane OMI_SWIFT_TEST_STEP_BUDGET_SECONDS 2700 -> 4200 Both changes cite the run; contract test constants updated to match. * test(ci): close compiler-gate checker holes from review Match compiler() anywhere in #if/#elseif, ignore block-commented directives, and fail closed when the scan root has no Swift files. * test(app): accept ActionItemsFetcher due-window params #13570 added dueStartDate/dueEndDate to ActionItemsFetcher on main. GitHub analyzes the merge of this PR, so the existing test stubs must accept those named args or Dart Analyze fails. * style(app): dart format test stubs at line-length 120 CI Check Dart formatting uses --line-length 120 after pub get. * chore: restore dart test stubs from origin/main after merge format noise * test(app): match ActionItemsFetcher due-window params | 3 小时前 | |
feat(listen): demote freemium threshold event to Plus-only (S18) (#12906) Basic no longer enters on-device through FreemiumThresholdReachedEvent; that event is now the Plus meter warning. The client paywall sheet follows the same plan gate. Co-authored-by: Cursor <cursoragent@cursor.com> | 6 天前 | |
fix(app): warn when setup.sh ios targets a physical device with OMI_DEV_HOST unset (#12091) Local dev backend binds to 127.0.0.1 by default. On a physical iOS device, 127.0.0.1 is the device itself, not the Mac — so a dev build silently hangs waiting for a backend it can never reach, with no indication why. The bind-host option (OMI_DEV_HOST/OMI_DEV_BIND_HOST) and two other setup blockers reported in the same issue were already fixed; this lands the last suggested remedy: detect a physical-device target and warn before the build starts. fixes #11534 | 19 天前 | |
fix(app): fetch Home today tasks by due window (#13570) * fix(app): fetch Home today tasks by due window Home preview filtered ActionItemsProvider's first global page, so a due-today task past offset 100 never appeared. Fetch incomplete items in the same 7-day due window the widget already used, without replacing the Tasks-page list. * fix(app): start Home today-task fetch after first frame Do not kick off the due-window request from build(). Load it once from initState after the widget is mounted. * fix(app): keep first-page today tasks if the due-window fetch is empty Union the due-window rows with the global first page so a successful empty due query cannot hide a matching task already in memory. * test(app): assert Home due-window load leaves the Tasks page list unchanged The Home preview can pass while still overwriting the global first page. Guard that invariant so a regression cannot hide it. * fix(app): retry Home due-window fetch after a failed load A completed failed Future was sticky, so Home never asked the due window again. Coalesce in-flight calls only; keep a successful load until clearUserData. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Da7em-T <328307673+Da7em-T@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> | 21 小时前 | |
fix(app): anchor the remaining share sheet calls share_plus rejects a share with no sharePositionOrigin whenever iOS presents it as a popover, and the throw is the last await in the tap handler, so the button silently does nothing. Crashlytics on TestFlight 1178/1179 shows 19 fatal 'sharePositionOrigin: argument must be set' events from the chat message share button. The shareSheetOrigin helper (d3749f4b50) landed with three call sites still unanchored: chat message share, action item share, daily summary share. Adds a static tripwire test that scans lib/ for share_plus calls without sharePositionOrigin so a fourth cannot ship. Verification: - flutter test test/utils/share_calls_anchored_test.dart test/utils/share_sheet_test.dart -> 5 passed - stashed ai_message.dart fix -> tripwire fails naming lib/pages/chat/widgets/ai_message.dart:1307 Failure-Class: none | 10 天前 | |
ci(desktop): pin ship Xcode and fail closed on compiler-gated APIs (#13564) * ci(desktop): pin ship/CI Xcode to 26.6 (17F113) from one source of truth PR #12867 looked right on a local Xcode 26 named bundle and shipped the icon-less system Picker fallback: Liquid Glass sat behind #if compiler(>=6.2) while desktop CI (GHA desktop-swift-ci.yml and Codemagic omi-desktop-swift-release/-preview) compiled with Xcode 16.4, so the ship toolchain never typechecked the glass APIs (#13548 reverted the tab bar). Bump the desktop ship/CI toolchain off 16.4 and make drift impossible: - desktop/macos/ci/xcode-pin.json is now the only place the version (26.6), build (17F113), and expected app path (/Applications/Xcode_26.6.app) live. 26.6/17F113 exists on both vendors: GHA macos-26 image 20260907.0351.1 (default Xcode, /Applications/Xcode_26.6.app) and Codemagic mac_mini_m4 image 'Xcode 26.6.x (default)' (/Applications/Xcode-26.6.app). - run-swift-ci.sh reads the pin file (fails closed when it is missing) and keeps asserting the exact version+build after selection. - desktop-swift-ci.yml macOS jobs move macos-15 -> macos-26, cache key prefixes move xcode164 -> xcode266 so 16.4 caches cannot poison 26.x builds, and the select steps name the pinned version. - codemagic.yaml omi-desktop-swift-release/-preview move xcode: 16.4 -> 26.6 (Android 16.4 and iOS 26.0.1 untouched); the reviewed codemagic_workflow_contract digests are refreshed for that edit. - test_desktop_swift_ci_contract.py loads the pin file instead of literals and adds codemagic pin-agreement + macos-26 runner assertions. Verified locally: bash desktop/macos/tests/test-run-swift-ci.sh (pin 26.6/17F113 sandbox incl. wrong-version/wrong-build/missing-pin sabotage); python3 .github/scripts/test_desktop_swift_ci_contract.py (38 tests OK); python3 .github/scripts/check-release-process-guards.py (exit 0); xcrun swift build -c debug --package-path Desktop on Xcode 26.6 (17F113) completes (1855s). * ci(desktop): fail closed on compiler-gated Apple SDK APIs New desktop-compiler-gates manifest check: desktop/macos/scripts/ check-desktop-compiler-gates.py fails when Desktop/Sources or Desktop/Tests contain #if compiler(...) / #elseif compiler(...) — the shape that let #12867 compile Liquid Glass out of the Xcode 16.4 ship toolchain while CI stayed green (#13548 reverted the tab bar). Runtime availability gating (if #available(macOS 26, *) with a working fallback) is the sanctioned pattern and passes. The allowlist ships empty and is asserted by exact contents in test_check_desktop_compiler_gates.py, so it cannot silently grow; entries require a documented reason and a matching path+line. checks-manifest.yaml also extends desktop-swift-ci-contract triggers with the pin file, runner script, launcher test, and codemagic.yaml so every pin consumer re-runs the contract test on drift. Verified locally: python3 .github/scripts/test_check_desktop_compiler_gates.py (9 tests OK — planted #if compiler(>=6.2) glassEffect sabotage fails the checker and removing it passes); python3 desktop/macos/scripts/ check-desktop-compiler-gates.py on the real tree reports none found; make preflight manifest lane: all selected checks pass except the pre-existing loaded-host flake in test_pr_preflight.py SingleFlightTests.test_identical_processes_join_and_execute_once, which fails 3/3 on pristine origin/main at the same host load (passes in isolation) and is unrelated to this diff. * changelog(desktop): mark ci xcode pin as internal-only (kind: none) The desktop changelog check requires an in-repo fragment for production desktop paths even with the no-changelog-needed label: the label is invisible after merge and would redden main's Release Eligibility run. This PR is CI/infra only (toolchain pin + fail-closed gate), so the marker is kind: none. * ci(desktop): raise verify ceiling to 90m for the first cold macos-26 full lane Measured on run 34687313733 (PR #13564): with the pin moved to Xcode 26.6, every cache cold under the new xcode266 keys, the full Swift suite ran past 55 minutes and the 60-minute job ceiling cancelled the job mid serial-cluster. Every completed suite was green (one wedged batch cost 1650s before its 1500s watchdog bisected it cleanly; zero test failures) — purely a timing ceiling, so: - desktop-swift-verify timeout-minutes 60 -> 90 - full-lane OMI_SWIFT_TEST_STEP_BUDGET_SECONDS 2700 -> 4200 Both changes cite the run; contract test constants updated to match. * test(ci): close compiler-gate checker holes from review Match compiler() anywhere in #if/#elseif, ignore block-commented directives, and fail closed when the scan root has no Swift files. * test(app): accept ActionItemsFetcher due-window params #13570 added dueStartDate/dueEndDate to ActionItemsFetcher on main. GitHub analyzes the merge of this PR, so the existing test stubs must accept those named args or Dart Analyze fails. * style(app): dart format test stubs at line-length 120 CI Check Dart formatting uses --line-length 120 after pub get. * chore: restore dart test stubs from origin/main after merge format noise * test(app): match ActionItemsFetcher due-window params | 3 小时前 | |
Replace broken default widget_test with codegen-free smoke test The original template test constructed MyApp() directly, which requires all codegen files to exist. Replace with a minimal MaterialApp test. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 5 个月前 |