Skip to content

Background-resilient model downloads, imports, and summaries - #12

Merged
DakePeng merged 41 commits into
mainfrom
codex/background-model-downloads
Jul 4, 2026
Merged

DakePeng merged 41 commits into
mainfrom
codex/background-model-downloads

Conversation

@DakePeng

@DakePeng DakePeng commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What this branch does

Makes every long-running on-device job — model downloads, file imports, re-transcribes, and map/reduce summaries — survive backgrounding, memory pressure, and process kills, and hardens the summary map phase against small-model format drift. 35 commits, three arcs:

1. Resumable background work (downloads, imports)

  • Model downloads continue via URLSession background sessions and resume after cancellation.
  • File imports checkpoint per decoded segment (SessionRecord.ImportCheckpoint), copy audio durably first, and resume from the checkpoint after a kill or backgrounding instead of restarting from scratch.

2. Background GPU abort survival + summary resume/redo correctness

  • mlx-swift is pinned to a fork (DakePeng/mlx-swift, branch loqi/0.31.4-poison-events): upstream 0.31.4 plus the ml-explore/mlx#3523 backport (tseylerd/mlx#1). Stock mlx throws C++ exceptions from Metal completion handlers when iOS aborts GPU work at background entry — uncatchable, killed the app every time a summary was backgrounded mid-generation (or froze it under the debugger). With the fork, aborts poison the stream and surface as catchable errors. Drop the fork once mlx-swift vendors mlx ≥ a025496c.
  • Generation gates every prefill window/token boundary on scene exit; model loads run on the CPU stream; background aborts are matched by error content (scenePhase delivery lag makes the flag alone unreliable) and route to the suspend path, so jobs restart from their chunk-note checkpoints.
  • Resume correctness: resumes skip the (nondeterministic) hygiene pass when a checkpoint exists, the background persistence flush holds a UIApplication background task so jetsam can't lose the last checkpoint, and progress reporting counts cached chunks so the UI never appears to reset after backgrounding.
  • Redo semantics: re-summarize with unchanged style+length clears coverage and regenerates notes (the cached-notes short-circuit used to make the button appear dead); a style/length change keeps the cheap reduce-only path.

3. Map-phase extraction hardening (Qwen3.5-2B format drift)

Driven by privacy-safe device diagnostics (rejection tallies, never content): per-line adaptive TSV field splitting, a localized one-shot example (kills spec parroting), a forced response prefix (T\t<chunk>\t) so the first record line is well-formed by construction, tolerant chunk-id matching with content-based echo guards, a strictly-gated salvage pass for compressed/id-led schemas (grounding via source-id validation preserved), and m-id citation scrubbing at parse/reduce/render boundaries.

Reviewer notes

  • The fork pin is the riskiest piece: project.pbxproj adds a root-level SPM override of the transitive ml-explore/mlx-swift. The fork branch is 0.31.4 + one submodule-pointer commit; the mlx patch is the upstream-merged fix backported to the vendored SHA.
  • Parser leniency is layered deliberately: strict parse always wins; salvage only runs when strict accepted nothing; validly-shaped lines rejected for cause (bad citations, echoes) stay rejected. Tests encode each rule.
  • Verified: 485 unit tests green (65 suites), plus on-device: background/foreground during summarize now suspends and resumes from the checkpoint (previously crashed), and the long-standing chunk-1 extraction failure strict-parses.
  • Known follow-ups (deliberately out of scope): hygiene hotword-restore re-rolls cause full remaps on fresh re-summarizes; Bonsai/LFM2 summary-model A/B per the committed plan docs.

🤖 Generated with Claude Code

Emrisis and others added 30 commits June 21, 2026 13:41
Re-summarizing during an active import (or any two heavy MLX/ASR jobs
overlapping) could exhaust device memory and crash the app. A FIFO
gate now makes summarize/import/retranscribe/diarize-retry mutually
exclusive, live recording waits for it to drain before loading its
own models, and summarize refuses to start mid-recording.

Also lands the Bonsai 8B (ternary 2-bit) summary model behind a
Settings toggle, off by default — see docs/superpowers/plans/2026-06-20-meaningful-summaries.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lets the UI language follow a manual pick (system/English/Simplified
Chinese) instead of always tracking the device locale, so summary
language and settings copy stay in sync with the user's choice.
Background transfers now run concurrently and persist URLSession
resume data on cancellation or failure, so the next attempt continues
the transfer instead of restarting it. Also fixes revision strings
with slashes not being split into separate URL path components.
SerialGate.acquire() and the offline audio decode loop now check
Task.checkCancellation() at their yield points, so a cancelled caller
unwinds immediately instead of running to completion or leaking a
held slot.
The DEBUG-only diagnostic logged note headlines and transcript-derived
counts; remove it now that the map-extraction-on-garbled-input
question is settled.
Recording chip taps were animating implicitly, causing a visible
flash; disable the transaction animation on that row.
Tests read ModelCatalog.all directly, so a stray "model.bonsaiEnabled"
default from another test run could change the expected lineup; scope
each test to its own UserDefaults suite instead.
A killed or backgrounded file import currently loses all progress and
the placeholder record itself. ImportCheckpoint stores the import
options and per-segment transcript text needed to resume instead of
restarting from zero — an all-optional nested struct, so existing
persisted records keep decoding unchanged.
Emrisis and others added 8 commits July 1, 2026 21:37
VAD segmentation is deterministic for the same audio, so a resumed
import can match a freshly-discovered segment's time range against
the checkpoint and reuse its text instead of re-decoding — and reports
each newly-decoded segment via onSegmentComplete so a caller can
checkpoint it immediately. Threaded through OfflineTranscriber and
both file transcribers, same as the existing onProgress plumbing.
The durable audio copy used to happen last, after transcription,
diarization, and translation all succeeded — so a kill mid-decode left
nothing to resume from, not even the audio. Copy it first instead, via
a new onAudioReady callback, and add a resumeImport entry point that
reads straight from that durable copy (never the original picker URL,
which may not be valid after a relaunch). The diarization/translation
tail is now shared between a fresh import and a resumed one.
loadIfNeeded() and mergedLoadedSessions() treated every interrupted
import as an unrecoverable tombstone and deleted it on relaunch. Now
only imports without a checkpoint and durable audio (killed before
either existed — genuinely nothing to resume) get swept; the rest
survive so SummaryJobCenter can pick them back up.
startImport now persists the checkpoint as the import progresses
(audio-ready, then each decoded segment), instead of only writing a
finished record at the very end. resumeUnfinishedImports() restarts
any session still marked importing from its checkpoint — called on
cold launch and after a recording that preempted an import ends.

Also fixes yieldToRecording() permanently cancelling an import when a
new recording preempts it: it now marks the activity paused before
cancelling, so the checkpoint survives and resumeAfterRecording()
re-enqueues it instead of the placeholder being deleted.
Stock mlx throws std::runtime_error from a Metal completion handler when
iOS aborts a command buffer at background entry — uncatchable, kills the
process (the re-summarize background freeze). The fork (DakePeng/mlx-swift
branch loqi/0.31.4-poison-events) is upstream 0.31.4 plus tseylerd's
backport of ml-explore/mlx#3523: buffer errors poison the stream and
surface at safe, catchable points, and poisoned events are force-signaled
so the eval lock can never wedge. Drop the fork once mlx-swift vendors
mlx >= a025496c.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scene-exit hardening around the forked mlx: generation gates every
prefill window and token boundary, loads run on the CPU stream, and a
background abort now surfaces as MLXError — matched by content (not just
the scene flag, which misses scenePhase delivery lag on both edges) and
mapped to the abandon/suspend path so jobs restart from their chunk-note
checkpoints instead of crashing or laundering the abort into fake notes.

Resume correctness: resumes skip the hygiene pass when a usable map
checkpoint exists (re-rolling nondeterministic hotword restores used to
wipe the checkpoint and remap everything), the background persistence
flush holds a BackgroundTaskGrace so a jetsam can't lose the last
checkpoint, and pendingSummary markers restart interrupted summaries at
launch.

Redo semantics: a manual re-summarize with unchanged style+length clears
map coverage and regenerates the notes — the cached-notes short-circuit
used to reduce-only re-render the same notes, making the button appear
dead. Style/length changes keep the cheap reduce-only path.

Job-center diagnostics throughout (suspend/resume/checkpoint/rejection
logging) so the next background bug is a one-log diagnosis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Qwen3.5-2B drifts off the TSV record schema in reproducible ways —
device diagnostics drove each countermeasure:

- Per-line adaptive field splitting (tabs, 2+-space runs, single-space
  last resort) instead of one whole-response strategy; mixed-separator
  responses used to reject every line.
- One-shot example in the prompt (six lines, per note language) ends
  spec-block parroting; a three-line example anchored output cardinality,
  so it demonstrates more record kinds. Verbatim example echoes are
  rejected by their distinctive content strings.
- Response prefix "T\t<chunk>\t" appended after the assistant header so
  the first record line is well-formed by construction.
- Any cNNN chunk id is accepted (only one chunk exists per call; the
  model borrows the example's c000 onto real records) — grounding stays
  enforced by the source-id check.
- Salvage pass when strict parsing accepts nothing: compressed-schema
  near-misses (T without time range, uncited P-family, A without owner/
  deadline) and id-led lines whose citations validate against the chunk's
  real source ids. Strict output always wins; validly-shaped lines
  rejected for cause stay rejected.
- m-id citation tokens scrubbed from all content fields at parse time
  and at the reduce/render boundaries (covers previously persisted notes).
- Resume progress counts cached chunks as completed steps so the UI
  never restarts at 0/remaining after backgrounding.
- Privacy-safe parse diagnostics (rejection tallies, no content) log on
  every empty parse and salvage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ab1f5ddf6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift Outdated
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift
Comment thread Loqi/Support/SessionArchive.swift
Emrisis and others added 2 commits July 4, 2026 10:29
- Live chunk notes now pass the same "T\t<chunk>\t" response prefix as
  batch summarize — the live path used the identical prompt and parser
  but had no first-line anchor, leaving it exposed to the 2B schema
  drift the batch path was hardened against.
- isBackgroundGPUAbort matches two independent phrasings of the
  background-permission error (IOGPU constant + Metal description) so a
  wording change in one layer can't silently unmap aborts from the
  suspend path; TODO added to surface a typed code through the fork.
- hasUsableMapCheckpoint now derives from SummaryEngine.uncoveredEntries
  instead of re-implementing its coverage semantics — the two can no
  longer diverge, and an all-stub checkpoint correctly counts as
  unusable (hygiene runs before the inevitable full remap).
- Parser cleanups: tag→kind mapping extracted to recordKind(for:)
  (was copy-pasted in three places), and action records scrub the
  task field once instead of twice per record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resumeUnfinishedImports guards !isBackgrounded itself (stop-recording
  from the lock screen no longer restarts Metal-unsafe ASR in the
  background) and skips sessions with a held activity badge.
- finishJob re-sweeps unfinished imports after teardown, so an import
  whose cancellation unwind outlived the foreground resume sweep is
  picked up when its task actually exits instead of stranding until the
  next cold launch.
- cancel() tears down a held/unwound import's placeholder + checkpoint
  (mirroring the task's own user-cancel path) so "Cancel processing" on
  a paused import can't be resurrected by the next resume sweep.
- A failed import drops its checkpoint: resume sweeps skip it and the
  checkpoint-less placeholder is swept at next launch — without this
  the finishJob re-sweep would loop a deterministic failure forever.
- PendingSummary persists the user's download consent so a summary
  killed mid-download keeps downloading on restart instead of failing
  with "model not downloaded".
- Session deletion kicks an immediate persistence flush so a force-quit
  right after deleting can't resurrect the record at next launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DakePeng

DakePeng commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36d2af4839

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Loqi/Support/CaptionPipeline.swift
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift
Comment thread Loqi/Models/AppLanguage.swift
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift Outdated
Comment thread Loqi/Pipeline/Summary/SummaryJobCenter.swift Outdated
- Background launches (iOS relaunching for background URLSession events)
  seed scene state from UIApplication.applicationState before the
  startup sweeps, and resumeUnfinishedSummaries guards !isBackgrounded
  like the imports sweep — no Metal work from a background launch.
- FileImportEngine rethrows CancellationError from the diarization
  catch instead of swallowing it as a label failure, so a background/
  yield preempt actually stops the import at its checkpoint.
- Auto ASR fallback split from the reader language: fallbackLanguage
  uses the new AppLanguage.speechPreferred (system language list only),
  so a zh UI + English speech in Auto mode no longer builds a zh
  recognizer; summaries keep the reader-language override.
- Memory warnings full-unload the LLM during import-only jobs (new
  usesLLM predicate) instead of shed-only: imports never touch the
  model, so resident weights were pure jetsam bait.
- A preempt before the durable audio + checkpoint landed deletes the
  placeholder instead of holding an unresumable "importing" row until
  the next relaunch sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DakePeng

DakePeng commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 12a9e5783b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@DakePeng
DakePeng merged commit bccf611 into main Jul 4, 2026
@DakePeng
DakePeng deleted the codex/background-model-downloads branch July 4, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant