Skip to content
LiveShortly
Menu
‹

Understand Pocket AI

EndedOpen linkOn feedclaude-opus-5
@Rohit Sehgal· ended 4d ago· 267 events
Understand the pocketai code we will build things around it
… 28 earlier steps
› running Bash
› Bash sed -n '1,110p' strategy/11-liveshortly-integration.md
› running Bash
› Bash cd ~/rsehgal/github/getpocketai/PocketAI.Core && echo "=== ffi exported fns ===" && grep -n 'pub fn \|pub async fn ' crates/pocket-ffi/src/core.rs | sed 's/ *{$//' | head -80
› running Bash
› Bash cd ~/rsehgal/github/getpocketai && echo "=== Mac AppModel surface ===" && grep -n 'var \|func \|@Observable\|final class\|init(' PocketAI.Mac/Sources/AppModel.swift | head -60

Read plan + code. Map:

What PocketAI is

On-device AI apps. Models run locally on iPhone/Mac/Android. No account, no server. Mac can serve its bigger models to paired iPhone + any OpenAI-compatible tool on LAN. Pro = Keygen.sh Ed25519 license key, verified offline, one-time $39→$59. No IAP, no subscription.

Layout — 5 sibling dirs, not one git repo

DirWhat
PocketAI.CoreRust workspace (8 crates) + Swift packages. Shared brain
PocketAI.MacSwiftUI + MenuBarExtra, 8.4k lines
PocketAI.IphoneSwiftUI, 10.7k lines
PocketAI.AndroidExpo/React Native port of iPhone, 7.8k lines
PocketAI.Websitestatic HTML
strategy/engineering source of truth. 01-architecture.md normative, BOARD.md 119 task cards

Core (Rust, 19k lines)

One crate per concern so parallel agents never touch same files:

  • pocket-types — ids, PocketError, DeviceProfile
  • pocket-store — rusqlite, migration 0001 (conversations/messages/projects/chunks+fts5/devices/peers/audit/egress)
  • pocket-models — catalog, HF search, fit engine (est_runtime_bytes = weights×1.05 + kv@4096 + 350MB), download plan/verify/commit
  • pocket-engine — Engine trait, router (MLX safetensors→Mlx, GGUF→LlamaCpp, mac:<id>/→Remote), memory guard with LRU admit/evict, llama.cpp engine, RAG, tools, benchmark
  • pocket-trust — Ed25519 identity, TLS certs, pairing, Keygen license verify
  • pocket-server — axum: /v1/* OpenAI-compatible + /pocket/v1/* control + SSE events
  • pocket-audit — hash-chained audit log, egress ledger
  • pocket-ffi — only crate exported to Swift. UniFFI. 2591 lines, ~70 methods

Key boundary trick

Engine trait is #[uniffi::export(with_foreign)] — MLX engine lives in Swift (PocketMLX) but implements the Rust trait. Rust owns state/routing; Swift owns Apple frameworks (Keychain, URLSession background downloads, Speech, AVSpeech, FoundationModels, MLX).

Swift side: CoreClient protocol with two impls — LiveCore (real PocketCore) and PreviewCore (fixtures, fake 40 tok/s stream). -PocketPreviewCore YES swaps whole app. That's how UI got built without waiting on Rust. MLX does not run in Simulator, so this is load-bearing.

Pairing

Mac QR pocketai://pair?...&fp=<sha256 cert>&s=<128-bit secret> TTL 300s single-use → iPhone resolves via Bonjour _pocketai._tcp, pins leaf cert to fp → POST /pocket/v1/pair → Mac shows "Allow this iPhone?" → bearer token to Keychain.

Trust tooling (the wedge)

Offline mode blocks app-initiated internet, pauses downloads, keeps paired LAN chat. Network Proof = egress table. CI lint check-no-raw-http.sh forbids URLSession.shared outside Net/ and Peer/.

LiveShortly module

Swift-only, no FFI change. PocketKit/LiveShortly/ + PocketLiveShortly package (1387-line session view) + Mac Sources/LiveShortly/ (LSChatView 626, LSSessionModel, LSShareSheet, LSPermissionBar). Device-flow auth, sessions list, live SSE viewer, comment composer, Allow/Deny on input_requested. 8 event types, unknown types render raw.

State

Nearly all 119 cards done. Remaining blocks are DEVICE (on-device perf/VoiceOver/multi-GB download survival) or OWNER (paid GitHub plan, CORE_REPO_TOKEN, Apple Intelligence toggle, TCC grants). STATUS.md last entry 2026-10-14; Mac sources touched 2026-10-29.

Two real stubs spotted in pocket-ffi/src/core.rs:

  • core.rs:979 — pub fn stop(&self, _conversation_id: Id) {} empty; cancel_conversation at :413 is the working one
  • core.rs:1740 — subscribe(&self, _sink:) ignores the sink, so the §6 event bus isn't delivering to Swift

Ready. What we building on it?

Is mac app signed and build ?
… 8 earlier steps
› running Bash
› Bash echo "=== notary profile check ===" && (xcrun notarytool history --keychain-profile pocketai-notary 2>&1 | head -12); echo "=== certs ==="; security find-identity -v -p codesigning 2>/dev/null | head
› running Bash
› Bash date -u && echo "=== current submission ===" && xcrun notarytool info 59d80e95-90be-4501-8473-ed6c07985d69 --keychain-profile pocketai-notary 2>&1 | head -12; echo "=== prior invalid reasons ==="; xcrun notarytool log f0fb898c-57d2-4aef-bfb6-1472b4a496d3 --keychain-profile pocketai-notary 2>&1 | head -40
› running Bash
› Bash ps aux | grep -iE 'notarytool|notarize-mac|xcodebuild' | grep -v grep | head

Built + signed, not notarized yet — notarization in flight right now.

Signed: build/dist/export/PocketAI.app

  • Developer ID Application: Rohit Sehgal (4RK6YA795V) → Apple Root CA
  • Hardened runtime on (flags=0x10000(runtime)), secure timestamp 29 Oct 15:47
  • arm64 thin, com.getpocketai.app, version 1.0 (1)

Notarization: scripts/notarize-mac.sh still running (PID 36434), notarytool submit --wait blocking since 15:47.

  • Submission 59d80e95 — In Progress, 12 min elapsed
  • Earlier attempt f0fb898c (15:40) came back Invalid: "The executable does not have the hardened runtime enabled." Already fixed — current export has it.

Right now Gatekeeper rejects it: spctl says rejected / source=Unnotarized Developer ID, no stapled ticket. Script will staple + build PocketAI.dmg once Apple returns Accepted.

Other artifacts: build/Build/Products/Debug/PocketAI.app (dev build), build-uitest/.../PocketAI.app + UITests runner. No DMG yet.

Want me to watch the submission and report when it flips Accepted?

Yea once the submission is complete let me know
› running Bash
› Bash cat > /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/scratchpad/watch-notary.sh <<'EOF' #!/usr/bin/env bash # Poll the in-flight notarization until it leaves "In Progress", then report the outcome. set -u ID=59d80e95-90be-4501-8473-ed6c07985d69 APP=~/rsehgal/github/getpocketai/PocketAI.Mac/build/dist/export/PocketAI.app for i in $(seq 1 60); do STATUS=$(xcrun notarytool info "$ID" --keychain-profile pocketai-notary 2>/dev/null | awk -F': ' '…
› running Bash
› Bash /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/scratchpad/watch-notary.sh

Watching submission 59d80e95 in background. Polls every 45s, up to ~45 min.

When it flips I report: status, Apple's reasons if Invalid, and on Accepted — stapled ticket + Gatekeeper verdict + build/dist/PocketAI.dmg.

Meanwhile — what are we building around PocketAI?

We are buiksing a world class app which can run local modela and run models in mac or aystem and also runs claude over liveahirtly
› running Bash
› Bash cd ~/rsehgal/github/getpocketai && echo "=== LiveShortly refs per app ==="; for d in PocketAI.Iphone/Sources PocketAI.Mac/Sources PocketAI.Android/src PocketAI.Android/app PocketAI.Core/swift/Sources; do echo "-- $d: $(grep -rli 'liveshortly\|LSSession\|PocketLiveShortly' $d 2>/dev/null | wc -l | tr -d ' ') files"; done; echo "=== Core LiveShortly files ==="; find PocketAI.Core/swift/Sources -ipath '*LiveShortly*' -name '*.swift' | xargs wc -l | sort -rn; echo "=== project.yml deps ==…
› running Bash
› Bash grep -rli 'liveshortly\|LSSession\|PocketLiveShortly' PocketAI.Iphone/Sources PocketAI.Mac/Sources | sed 's|/Sources/| :: |'

Vision maps onto three engines — two proven, one uneven.

1. Local on-device — done MLX (PocketMLXEngine, 788 lines), llama.cpp (pocket-engine/llama, 874), Apple Foundation Models as zero-download. Router picks by format. Memory guard budgets from os_proc_available_memory() not total RAM, LRU-evicts idle models, never mid-generation. Fit engine refuses before download, not after.

2. Mac/system serving — done, proven end to end RemoteEngine → pinned TLS → Mac's axum server. Model ids prefixed mac:<device-id>/ route to Remote. D115 recorded 24 tokens streamed from host MLX over pinned TLS; D114 pairing round trip paired=1, RTT 0.038s. Plus /v1/* OpenAI-compatible so Cursor/scripts hit the same Mac.

3. Claude over LiveShortly — Mac rich, iPhone thin, Android zero

fileswhat
Core PocketLiveShortly9 (2914 lines)client, device-flow auth, SSE stream, session view, connect, new-session
Mac17own LiveShortly/ layer: LSChatView 626, LSSessionModel, LSShareSheet, LSPermissionBar, LSNewSessionView + sidebar/palette/HUD/onboarding/project hooks
iPhone5AppModel, Settings, Home, ProjectDetail, ChatsList — consumes shared views only, no own layer
Android0nothing

Real gaps for the "world class" bar:

  • Android is a different animal. No Rust core, no FFI — llama.rn + its own src/core/engine/*, downloads.ts, catalog.android.json. Local + remote only. LiveShortly would be a from-scratch TS port of 2914 Swift lines.
  • LiveShortly is watch-mode, not drive-mode. Per 11-liveshortly-integration.md §1, daemon /v1 agent driving, fork/handoff from app, and push notifications are explicitly out of V1. Today: sessions list, live SSE viewer, comments, Allow/Deny on input_requested. Starting Claude from the app isn't there.
  • No push. iOS suspends in seconds and APNs needs a provider server (00-README.md §3). A permission prompt that lands while the phone is asleep waits for app open. That's the sharpest edge on "Claude on my phone."
  • subscribe() is a stub (pocket-ffi/src/core.rs:1740 drops the sink). The §6 event bus doesn't reach Swift, so model/server/device events can't drive UI reactively — each screen polls.

Highest-leverage next move, my call: wire subscribe() for real, then bring iPhone LiveShortly to Mac parity — permission bar + session model are the two pieces the phone most needs and the phone is where an approval request actually finds you.

Notarization watch still armed. Which lane first?

Also there is a wip that we have to add lo alization
… 2 earlier steps
› running Bash
› Bash echo "=== project.yml i18n settings ==="; grep -n -i 'region\|localiz\|LOCALIZ\|xcstrings' PocketAI.Mac/project.yml PocketAI.Iphone/project.yml; echo; echo "=== Android i18n ==="; grep -rli 'i18n\|intl\|localization\|getLocales\|expo-localization' PocketAI.Android/src PocketAI.Android/app 2>/dev/null | head; grep -n 'expo-localization\|i18n' PocketAI.Android/package.json; echo "(none above = no Android i18n)"
› running Bash
› Bash echo "=== Core xcstrings ==="; find PocketAI.Core/swift -name '*.xcstrings' -o -name '*.lproj' | head; grep -n 'defaultLocalization\|resources' PocketAI.Core/Package.swift | head -20; echo; echo "=== state flags across catalogs ==="; for f in PocketAI.Mac/Resources/Localizable.xcstrings PocketAI.Iphone/Resources/Localizable.xcstrings; do python3 -I -c " import json,collections d=json.load(open('$f')); st=collections.Counter(); ex=collections.Counter() for k,v in d['strings'].items(): if not …
› running Bash
› Bash python3 -I -c " import json,collections f='PocketAI.Core/swift/Sources/PocketUI/Resources/Localizable.xcstrings' d=json.load(open(f)); s=d['strings']; langs=collections.Counter() for k,v in s.items(): for l in v.get('localizations',{}): langs[l]+=1 print('PocketUI keys:',len(s),'langs:',dict(langs)) print('keys:',list(s)[:12]) "; echo "=== junk-ish keys in app catalogs ==="; python3 -I -c " import json for f in ['PocketAI.Mac/Resources/Localizable.xcstrings','PocketAI.Iphone/Resources/Locali…

Found the WIP. Localization is half-done, and the half that's missing is the half the stores read.

Targetkeystranslated
PocketUI (shared)54all 8 langs — en, hi, es, fr, de, ja, zh-Hans, pt-BR
Mac app183en only
iPhone app287en only
Android—no i18n stack at all

So the shared component layer is fully localized and proved the format (plurals, %lld tok/s, %@ of %@ · %@ · %@ left). The two app catalogs are extracted but untranslated — every key sits at state: translated with only an en entry.

The shipping problem: both project.yml files declare all 8 in knownRegions and CFBundleLocalizations:

PocketAI.Iphone/project.yml:8 and :50, PocketAI.Mac/project.yml:8 and :58

iOS and the App Store therefore advertise PocketAI as available in Hindi, Spanish, French, German, Japanese, Simplified Chinese and Brazilian Portuguese. Set a device to Hindi today and you get PocketUI's chrome in Hindi wrapped around 470 English strings. Worse than not declaring them.

Also wants cleaning before anyone translates — keys that shouldn't be in a catalog:

  • Mac: "", "—"
  • iPhone: "1024", "512", "768", "·", "•"

Mark those .doNotTranslate (or drop the literal into a non-localized Text(verbatim:)), else they ship to translators and come back "translated."

Scope: 470 strings × 7 languages = 3290 units.

My call on approach — the catalogs are plain JSON, so this is a data task, not a code task. One throwaway script walks strings{} and writes the 7 localizations blocks; translation happens in batches. No new dependency, no tooling, Xcode reads it straight back. PocketUI is the reference for how format specifiers and plurals must survive.

Two ways to go:

  1. Gate fix only (minutes) — strip the 7 junk keys, cut knownRegions/CFBundleLocalizations back to [en]. Ship honest, re-add locales as translations land.
  2. Full fill — translate all 470 × 7 into both catalogs.

Recommend 1 now regardless, since it's true today whatever we do next, then 2 as its own pass.

Which? Notarization watch still running.

Write all the tasks which are pending
› running Bash
› Bash echo "=== BOARD non-done ===" && grep -E '^\| [A-Z]' strategy/BOARD.md | grep -vE '\| done \|$' ; echo; echo "=== BOARD state tally ==="; grep -oE '\| (todo|doing|review|done|blocked) \|$' strategy/BOARD.md | sort | uniq -c
› running Bash
› Bash sed -n '40,91p' strategy/GATES.md
› running Bash
› Bash cat strategy/OWNER-SIGNOFF.md; echo "=== next-release ==="; ls strategy/next-release; head -30 strategy/next-release/*.md 2>/dev/null | head -40

Pulled from BOARD.md (99 done / 20 blocked), GATES.md, OWNER-SIGNOFF.md, plus what this session turned up. Nothing below is a missing implementation except where marked CODE — the 20 board cards are all written and CI-green, held only by a gate.

0. In flight right now

  • Mac notarization — submission 59d80e95 In Progress. Watch armed. On Accepted the script staples + builds build/dist/PocketAI.dmg. Until then spctl rejects the signed app.

1. CODE — real gaps found this session

  • subscribe() is a stub — PocketAI.Core/crates/pocket-ffi/src/core.rs:1740 drops the EventSink. The §6 event bus never reaches Swift; every screen polls instead of reacting.
  • stop() is an empty body — core.rs:979. cancel_conversation (:413) is the working one; either delete stop or forward it.
  • MAC-06 cert export — flagged "no FFI yet" in GATES. Small addition.
  • Localization: 470 untranslated strings. PocketUI has all 8 languages (54 keys). Mac (183) and iPhone (287) are en-only, yet both declare 8 locales in knownRegions + CFBundleLocalizations (project.yml:8 / :50 iPhone, :8 / :58 Mac) — stores advertise Hindi/Spanish/French/German/Japanese/Chinese/Portuguese and serve English.
  • Strip 7 non-translatable keys from the catalogs: Mac "", "—"; iPhone "1024", "512", "768", "·", "•".
  • iPhone LiveShortly parity — Mac has 17 touching files incl. its own LiveShortly/ layer (LSChatView, LSSessionModel, LSPermissionBar, LSShareSheet, LSNewSessionView). iPhone has 5 and consumes shared views only. The permission bar matters most on the phone.
  • Android has no LiveShortly — 0 references. Would be a TS port of 2914 Swift lines, on a stack with no Rust core.

2. Four toggles on this Mac — flip 7 cards green

  • sudo DevToolsSecurity -enable → unblocks the written PocketAIUITests suite → MAC-02, MAC-03, MAC-05, MAC-08, MAC-11, UI-09, UI-11
  • Approve Local Network prompt → APL-08 (Bonjour; status was .notDetermined auto-launched)
  • Approve Speech Recognition prompt → APL-12 (STT; TTS→STT already wired)
  • Enable Apple Intelligence → APL-16 (Foundation Models self-test runs instead of skipping)

3. Needs the physical iPhone

  • UI-05 live tok/s while generating
  • UI-07 fresh-install-to-first-answer < 5 min + VoiceOver pass
  • UI-08 1,000-conversation 120fps Instruments trace
  • UI-10 / MAC-04 on-device download state transitions
  • MAC-12 on-device feature pass (projects, voice, vision, images)
  • APL-03 keychain / thermal / low-power fidelity
  • APL-05 multi-GB download survives backgrounding, relaunch, airplane mode
  • APL-18 Pro unlock — on-device Keygen key activation
  • QA-05 perf harness ×5 per model, CSV against budgets
  • QA-10 accessibility — VoiceOver full pass, Inspector 0 warnings

4. Owner account / decision — launch critical

  • COM-01 App Store Connect Issuer ID + Small Business Program enrollment. Critical path.
  • COM-02 Stripe + Zapier → Keygen license checkout
  • WEB-02 pick waitlist mechanism (mailto / pre-order / form)
  • Paid GitHub plan (branch protection) + CORE_REPO_TOKEN secret for cross-repo CI

5. Polish

  • UI-02 export app icon via Icon Composer, run brand.html pixel-diff
  • CodeBlock has no syntax highlighting (needs a per-language tokenizer)
  • MessageView can't render markdown tables (AttributedString(markdown:) doesn't support them — needs the swift-markdown fallback)

6. Deferred by decision — not pending

Agents with approvals + Pocket Bridge (ACP over stdio to claude-code-acp / codex-acp / opencode / goose). Spec parked in strategy/next-release/agents.md. V1 keeps only the seams: event bus, device auth, audit log.

Launch gate, per the ledger: section 4 alone ships it. Section 2 is four clicks for seven cards. Localization is the one item not in any existing ledger and it blocks an honest store listing.

Want this written into strategy/ as a consolidated ledger, or keep it in OWNER-SIGNOFF.md's shape and just append the new findings?

I am thinking of building a private key Public key licencse approach. Plan this out and pro and cons of distributable license keys
… 2 earlier steps
› running Bash
› Bash sed -n '120,300p' strategy/12-licensing.md
› running Bash
› Bash sed -n '300,451p' strategy/12-licensing.md
› running Bash
› Bash cd ~/rsehgal/github/getpocketai/PocketAI.Core && grep -n 'pub fn \|pub struct \|pub enum \|^fn \|#\[test\]' crates/pocket-trust/src/license.rs | head -40; echo "--- tests count: $(grep -c '#\[test\]' crates/pocket-trust/src/license.rs)"; echo "=== pubkey file ==="; ls crates/pocket-trust/src/ && sed -n '1,25p' crates/pocket-trust/src/keygen_pubkey.rs 2>/dev/null

Plan already exists — strategy/12-licensing.md, 451 lines, status proposal awaiting your review. It covers exactly this. Don't re-plan it; decide it.

You already built the private/public key approach

Keygen isn't doing your crypto. It's only minting.

PieceWhereYours already?
Ed25519 offline verifypocket-trust/src/license.rs (verified: 11 tests)yes
Pinned 32-byte public key + account/product/policy idspocket-trust/src/keygen_pubkey.rsyes
activate_license(key) -> Entitlements, fully offlinepocket-ffi/src/core.rsyes
Pro gates on pairing_offer() / server_start()pocket-ffiyes
Mac license entry + keychain persist + re-activate on launchMac/Settings/SettingsView.swift, AppModel.swiftyes

Envelope is key/<base64url payload>.<base64url ed25519 sig> — a signed JSON blob. Nothing Keygen-specific in it. Minting it yourself = generate a keypair, swap 4 constants in keygen_pubkey.rs, hold the private key. Verifier, tests, FFI, Mac UI unchanged.

So the question isn't "can we build it." It's what you give up by owning the signing key, and what a distributable key can never do.


Distributable (bearer) license keys — the honest ledger

What they buy you

  • No infrastructure. No server to run, nothing to pay for, nothing to go down on launch day. A licensing outage can never lock out a paying customer.
  • Actually offline. Matches the product's whole claim. Network Proof shows zero licensing requests, forever. For an audience that bought this app specifically because nothing leaves, that number is the pitch.
  • No account, consistent with the wedge. Your stated advantage over LM Studio's LM Link is "no account." An activation call is a soft account.
  • No privacy surface. No device fingerprints collected, no activation table, no personal data, no DPA questions.
  • Instant fulfilment, any channel. Email, paper, QR, a conference sticker. No redemption flow to break.
  • Cheap support. Reinstall = paste the key again. No seats to juggle, no "my Mac died" tickets.
  • Survives you. If you stop maintaining PocketAI, every key still works. That's a real selling point for one-time-purchase buyers who got burned by subscription shutdowns.
  • No vendor can deplatform you.

What they can never do

  • A key is a bearer token. Infinitely copyable. One paste on Reddit and everyone has Pro. This is not a bug you can fix — it's what makes offline verification possible.
  • Seat caps are impossible, by construction. Counting machines requires something that sees all the machines. "5 devices" in your terms would be unenforced honour system.
  • Revocation is best-effort only. A refunded or leaked key on an offline Mac works forever. Ceiling is an opportunistic /revocations.json + a denylist baked into the next build. State that limit in the license terms; don't paper over it.
  • Chargebacks have no teeth. Refund processed, key still working.
  • No visibility. You can't tell a key leaked, or size the damage, because nothing phones home.
  • Expiry rests on the user's clock. Rollback bypasses it. Mitigate with a monotonic high-water mark (§9.2). Moot if licenses are perpetual, which yours are.
  • Private key becomes recall-class risk. It leaks → anyone mints lifetime licenses → only fix is shipping a new pinned public key, which invalidates every existing key. Keygen carries that risk for you today; self-minting moves it to you.

My read

Leakage ceiling is the whole argument, and it's a 100× difference:

  • Pure offline: one leaked key unlocks unlimited machines.
  • One activation call per machine, ever (12-licensing.md §1 option B): one leaked key unlocks 5.

That's bought for a single request per machine for the life of that machine — and it's honestly describable in UI copy: "this is the only time PocketAI needs the network." Network Proof shows it instead of hiding it, which is on-brand rather than against it. B also gives refunds and chargebacks actual teeth.

So: B, as the doc already recommends. Two things I'd add that it doesn't say —

  1. Don't over-invest. Any scheme falls to a patched binary. Licensing stops casual sharing, not reverse engineering. The failure mode worth engineering against is a key posted publicly, not a friend telling a friend — and a 5-seat cap covers legitimate multi-device use anyway.
  2. Price for weak enforcement. $39 one-time on a direct notarized download is the right shape: cheap enough that paying beats hunting for a key.

If you want to stay fully offline anyway (defensible — it's the purest version of the promise), then go option A honestly: drop the device-count language from the terms entirely, ship the revocation list + baked denylist, and treat leakage as a cost of the positioning. What you must not do is advertise "5 devices" while enforcing nothing.

Either path, keep Mac distribution direct. The moment the Mac app goes to the Mac App Store, external keys are a Guideline 3.1.1 problem again (§10).


Pending, concrete

  • Revocation is plumbed but inert — verify_license_key(..., revoked: &[String]) is always called with &[]. True on both paths.
  • Entitlement isn't persisted — license_entitlement lives in Inner and dies with the process; launch re-verifies the key rather than a cached token.
  • Generate the Ed25519 keypair offline, paper backup, write the re-issue script before launch, not during the incident. Two keys from day one (current + next) so rotation isn't a flag day.

Seven decisions blocking all of it (§11)

  1. A, B, or C? Seats 4 or 5? User-releasable?
  2. Self-mint or stay on Keygen?
  3. Trial: unit (remote turn / chat / session), budget (5 lifetime / 5 monthly), shared or separate LiveShortly pool, post-trial pairing?
  4. Worker + Supabase, or Supabase alone?
  5. Price — still $39 → $59 with a 5-device key?
  6. Refunds auto-revoke or manual?
  7. Is LiveShortly's paywall even yours to set, or the service's?

Answer 1, 2 and 5 and I'll implement — most of the risk here is ambiguity, not code. Want me to fold this pro/con ledger into §1 of 12-licensing.md so the decision record lives with the plan?

No implementation just thoughts

Understood — no code. Thoughts.

1. The decision tree collapses, and it doesn't land where I said

Walk it honestly:

You want a seat cap, revocation, and refund teeth → that needs one online call per machine → that's option B → Keygen already ships B, and it's free to 100 active licensed users.

Self-minting buys you: no vendor lock-in, and that's it. It costs you: custody of a recall-class secret, a Worker, a Supabase schema, a webhook on the money path, a re-issue script, and a rotation runbook. Taking on a key whose leak invalidates every license you've ever sold — before you have revenue — is paying real risk to save $0.

Revise my earlier answer: B, but via Keygen, not via self-minting. Revisit when the free tier actually binds — around 100 paying users, call it $4k of revenue. The switch stays cheap forever because the envelope is generic: 4 constants in keygen_pubkey.rs. That genericity is the asset. Don't spend it early.

2. You already own a device-identity system, and it suggests a different cap

The doc proposes inventing a fingerprint (salted SHA-256 of IOPlatformUUID). But pairing already does this work: Ed25519 device keys in Keychain, a devices table with pubkey + token hash, pairing_identity(), pinned-TLS handshake.

Which means the Mac can count something with perfect accuracy, fully offline: the phones it serves. Its own table.

So there are two different caps available, and they do different jobs:

  • "≤5 Macs per key" — anti-piracy. Unenforceable offline. Needs the server.
  • "≤N paired phones per Mac" — product shaping. Perfectly enforceable, offline, zero infrastructure, no fingerprint invention.

The second is honest and free but stops no piracy — a leaked key gives everyone a Mac that serves 5 phones. Worth being clear-eyed: it's not a security control, it's a product boundary.

The general point: you cannot buy anti-piracy offline. Not with better crypto, not with cleverness. If you want it, you pay one network call. If that call is unacceptable, stop calling the cap enforcement and call it terms.

3. Piracy isn't the loss function at $39

Pirates at this price point were never going to pay. The actual costs are support burden and paying customers' sense of fairness. Both are served by generosity, not tightness — 5 releasable seats reads as generous, and that's the point of the number, not deterrence.

Corollary: anyone determined enough patches the binary regardless. Enforcement spend is capped by that ceiling. Spend the budget on the trial instead.

4. The trial is the better lever, and it's already offline-enforceable

5 remote turns, counted on the Mac, lifetime — needs no server, no fingerprint, no vendor. Perfectly countable at the one place every remote request funnels through.

Conversion comes from a good trial far more than from a tight cap. The trial is also the part that's purely yours to design, with no custody risk attached. If effort is going anywhere this quarter, it goes there.

5. The funnel has a structural break, and it's Apple's fault

Guideline 3.1.1 means the phone can never show a price or a purchase link. But trial exhaustion happens on the phone — that's where the person is sitting when the 6th remote reply fails.

So intent peaks on a device that is legally forbidden to sell. The phone can only say "unlock on your Mac," and the person has to get up and go to another machine. That's the worst possible place for friction.

Thought: pre-empt it on the Mac. The Mac knows the count — surface "2 free remote replies left" in the HUD or menu bar while the person is still at the Mac, so the sell happens where selling is allowed. Treat the phone's error card as a fallback, not the funnel.

6. Perpetual keys quietly kill two problems

Clock rollback stops mattering (nothing to expire past). The monotonic high-water mark in §9.2 becomes optional. And "survives you" turns into a real differentiator for an audience that got burned by subscription shutdowns — Off Grid's subscription backlash is in your own research notes.

Keep them perpetual. The one-time-purchase positioning and the weak-enforcement reality actually agree with each other here.

7. The question I'd want answered before any of this

§11.7 — is LiveShortly's paywall even yours to set? If LiveShortly is a service with its own account model, gating it behind a PocketAI license may be their call as much as yours. That's not a licensing-architecture question, it's a "do we have the right to sell this" question, and it's upstream of everything in the doc.


Net: the cheapest correct move is to decide nothing structural, stay on Keygen, and spend the time on the trial and the Mac-side sell moment. The self-minting option stays open at a 4-constant cost for as long as you keep the envelope generic.

Document this in wip
… 2 earlier steps
› running Bash
› Bash tail -5 strategy/12-licensing.md && echo "=== lines: $(wc -l < strategy/12-licensing.md)"
› running Bash
› Bash cat >> strategy/12-licensing.md <<'EOF' --- ## 13 · WIP · thinking log — self-minted keys vs Keygen (2026-10-29) > Status: **thoughts only, nothing decided, nothing implemented.** Prompted by > the owner asking to plan a private-key/public-key licence scheme and to weigh > distributable keys. Revises the §1.2 recommendation — see §13.1. > §11's seven questions are still the gate. ### 13.0 What triggered it The owner asked about building a private/public key licence approach. Worth …
› running Bash
› Bash python3 -I - <<'PY' import re p='strategy/12-licensing.md' s=open(p).read() old="### 1.2 Recommendation — B now, C later" new="### 1.2 Recommendation — B now, C later\n\n> **Revised 2026-10-29 — see §13.1.** B still holds for *where* a cap must live;\n> what changed is who mints the keys. Short version: do B via Keygen, not via\n> self-minting, until the free tier binds." assert s.count(old)==1 s=s.replace(old,new) old2="> Owner decisions needed: §1.3, §2.3, §3.4, §11." new2="> Owner …

Documented in strategy/12-licensing.md — new §13 · WIP · thinking log, 451 → 612 lines. Marked thoughts only, nothing decided, nothing implemented.

Sections:

  • 13.0 it's already built — Keygen only mints, the Ed25519 verifier/pinned key/offline activation are ours
  • 13.1 revised recommendation: B via Keygen, not self-minting, until the free tier binds (~100 users / ~$4k)
  • 13.2 distributable-key ledger — 8 pros, 7 cons, and the load-bearing line: anti-piracy can't be bought offline
  • 13.3 we already own device identity → two different caps (≤5 Macs = anti-piracy, needs server; ≤N phones per Mac = product shaping, free and offline-perfect)
  • 13.4 piracy isn't the loss function at $39
  • 13.5 trial is the better lever, already offline-enforceable
  • 13.6 perpetual keys kill clock-rollback and become a feature
  • 13.7 3.1.1 funnel break — intent peaks on the device forbidden to sell; pre-empt on the Mac
  • 13.8 upstream: is LiveShortly's paywall even ours to set
  • 13.9 net recommendation
  • 13.10 pending regardless — inert revocation, unpersisted entitlement, key-custody prep

Two cross-refs added so nobody reads the old advice cold: header line 6 points to §13, and §1.2 carries a Revised 2026-10-29 note (12-licensing.md:73) since its recommendation changed.

Not a separate WIP.md — kept with the plan it revises. Say the word if you'd rather it split out.

Notarization watch still armed on 59d80e95.

For app iphone and mac Add this is design fix, if the user opens the chat thats should land in the latest message not in thr first message
… 24 earlier steps
› running Bash
› Bash python3 -I - <<'PY' # LSChatView (Mac): identical sentinel pattern to Chat/ChatView.swift p='PocketAI.Mac/Sources/LiveShortly/LSChatView.swift' s=open(p).read() import re # anchor the insert just before the first .onChange after the ScrollView close old=""" .onChange(of: scrollActivity.isScrolling) { _, scrolling in""" new=""" // Open on the newest event, not the oldest — same reason as // Chat/ChatView.swift: a top-anchored first layout hides the …
› running Bash
› Bash timeout 15 ssh -o BatchMode=yes -o ConnectTimeout=8 laptop 'hostname; sw_vers -productVersion; xcodebuild -version 2>&1 | head -2; ls -d ~/getpocketai 2>/dev/null || echo "no synced copy"' 2>&1 | head -12
› running Bash
› Bash ssh -o BatchMode=yes -o ConnectTimeout=8 laptop 'hostname; sw_vers -productVersion; xcodebuild -version 2>&1 | head -2; ls -d ~/getpocketai 2>/dev/null || echo "NO_SYNCED_COPY"' 2>&1 | head -12

Four files, one modifier each — plus one dead block removed.

FileChange
PocketAI.Mac/Sources/Chat/ChatView.swift:~254+ .defaultScrollAnchor(.bottom) — the actual bug
PocketAI.Mac/Sources/LiveShortly/LSChatView.swift:~300+ .defaultScrollAnchor(.bottom) — identical sentinel pattern
PocketAI.Core/swift/Sources/PocketLiveShortly/LSSessionView.swift:~247+ .defaultScrollAnchor(.bottom) — shared by both apps; stops the animated jump down from the first event
PocketAI.Iphone/Sources/Chats/ChatView.swift:296− dead .task that claimed to scroll on open but always hit guard let last = messages.last while empty. Comment left explaining what actually does the work

Why one modifier is the whole fix on Mac: it makes the first layout bottom-anchored, so the "tail" sentinel is on screen, so .onAppear keeps pinnedToBottom = true, so follow() works from then on. Patching follow() or the sentinel instead would have left the initial layout still top-anchored.

defaultScrollAnchor is iOS 17 / macOS 14; floors are iOS 18 / macOS 15, so no availability gate.

Not verified by a build. Two blockers:

  1. laptop is unreachable — ssh: connect to host 192.168.29.51 port 22: Operation timed out. Your CLAUDE.md routes all xcodebuild to the remote MBP, so I stopped rather than silently building here.
  2. Notarization of the Mac app is still in flight and will staple build/dist/export/PocketAI.app when it returns. A local build writes to build/Build, a separate path — but worth knowing before I touch the project.

No test added: scroll position needs a UI test, and the PocketAIUITests suite is itself gated on sudo DevToolsSecurity -enable (OWNER-SIGNOFF §A).

Want me to build locally anyway, or wait for laptop?

Juat document this in design fix
… 6 earlier steps
› running Bash
› Bash tail -20 strategy/DECISIONS.md
› running Bash
› Bash cat >> strategy/DECISIONS.md <<'EOF' | 2026-10-29 | D127 | Design fix — every transcript now OPENS ON THE NEWEST MESSAGE, not the oldest. `02-design-system.md` §7.2 already required `.defaultScrollAnchor(.bottom)`; three of four transcripts never implemented it. (a) **Mac `Chat/ChatView.swift`** — real bug: no anchor, so the first layout was top-anchored, which put the `"tail"` sentinel off-screen, whose `.onDisappear` cleared `pinnedToBottom`, after which `follow()` refused to catch up —…
› running Bash
› Bash python3 -I - <<'PY' p='strategy/02-design-system.md' s=open(p).read() old="- Message list: `LazyVStack` in `ScrollView` with `.defaultScrollAnchor(.bottom)`; auto-scroll only if user is at bottom; \"Jump to latest\" pill otherwise." new="- Message list: `LazyVStack` in `ScrollView` with `.defaultScrollAnchor(.bottom)`; auto-scroll only if user is at bottom; \"Jump to latest\" pill otherwise.\n- **Opening a transcript lands on the newest message, never the first.** This applies to every transcrip…

Documented in two places, both existing files — no new doc.

strategy/DECISIONS.md → D127 (house style, follows D126): what was broken in each of the four transcripts, the mechanism on Mac (top-anchored layout → tail sentinel off-screen → .onDisappear clears pinnedToBottom → follow() refuses), the dead iPhone .task, and why one modifier at the root beat patching follow() or the sentinel. Records that it's unbuilt (laptop timed out, notarization mid-flight) and untested (XCUITest still gated on sudo DevToolsSecurity -enable, per D117/D119/D120).

strategy/02-design-system.md:243 → new spec bullet under §7.2, so the next transcript doesn't inherit it:

Opening a transcript lands on the newest message, never the first. This applies to every transcript in either app — chat and LiveShortly sessions alike. The anchor is what delivers it: a scrollTo in onAppear/.task races the message load and silently no-ops, and a bottom-pinned flag driven by an off-screen tail sentinel starts out wrong on a top-anchored layout. See D127.

Code changes from the previous turn are still in the working tree, unbuilt. Notarization watch still armed on 59d80e95.

Confirm if the map app is noterized ?
› running Bash
› Bash cd ~/rsehgal/github/getpocketai/PocketAI.Mac && date -u && xcrun notarytool info 59d80e95-90be-4501-8473-ed6c07985d69 --keychain-profile pocketai-notary 2>&1 | head -8; echo "=== staple ==="; xcrun stapler validate build/dist/export/PocketAI.app 2>&1 | tail -2; echo "=== gatekeeper ==="; spctl -a -vvv -t exec build/dist/export/PocketAI.app 2>&1 | head -3; echo "=== dmg ==="; ls -lh build/dist/*.dmg 2>&1 | tail -1; echo "=== script alive? ==="; ps aux | grep -E 'notarize-mac|notarytool…

Not notarized. Still in progress — 41 minutes so far.

status: In Progress          submission 59d80e95, created 10:17:52Z (now 10:58:39Z)
stapler: PocketAI.app does not have a ticket stapled to it
spctl:   rejected — source=Unnotarized Developer ID
dmg:     does not exist

Signed, yes — Developer ID Application: Rohit Sehgal (4RK6YA795V), hardened runtime, timestamped. Notarized, no.

notarize-mac.sh + notarytool both still alive (2 processes), so the pipeline hasn't died — it's waiting on Apple. Once Accepted it staples and builds build/dist/PocketAI.dmg.

41 min is long but not alarming; Apple's typical turnaround is under 15 min and the queue occasionally drags. The earlier attempt f0fb898c came back Invalid in ~9 min, so the account isn't blocked.

My watch is armed and will report the moment it flips either way.

Another design fix. Iphone strem should be auto refresh and there should be manual refresh button in … dots
… 20 earlier steps
› running Bash
› Bash cd ~/rsehgal/github/getpocketai && grep -n 'stream dropped\|catch\|reconnect\|Refresh\|arrow.clockwise\|refreshable' PocketAI.Mac/Sources/LiveShortly/LSChatView.swift | head -20
› running Bash
› Bash grep -rn 'LSSessionView\|LSChatView' PocketAI.Mac/Sources PocketAI.Iphone/Sources --include='*.swift' | grep -v '^.*LSChatView.swift:' | head
› running Bash
› Bash grep -n 'catch\|stream\|retry\|reconnect\|Task.sleep' PocketAI.Mac/Sources/LiveShortly/LSSessionModel.swift | head -20

Done, type-checked. iPhone uses the shared LSSessionView, Mac uses its own LSChatView — so this landed exactly where you asked.

PocketAI.Core/swift/Sources/PocketLiveShortly/LSSessionView.swift — 4 changes:

1. Auto-refresh. startStream now wraps the SSE consumption in a while !Task.isCancelled retry loop. On a drop it backs off 1s → 2s → 4s → 8s → 15s → 15s and reconnects; after 6 failed attempts it surfaces the old error. Catch-up is free: the server replays its buffer on connect and the existing events.contains(where: { $0.id == e.id }) dedupe absorbs the overlap. Clean session_ended still exits rather than reconnecting.

Marked the ceiling rather than engineering around it:

// ponytail: a disconnect longer than the server's replay buffer
// loses the events in between; re-fetch the session on reconnect
// if that ever shows up in practice.

2. Manual Refresh in the … menu. The menu previously only rendered if let detail, detail.isOwner == true — a shared-session viewer had no … at all. Now the Menu is unconditional with Refresh (arrow.clockwise) at the top for everyone; owner-only items moved behind the binding with a Divider().

3. "Reconnecting…" status, not an error. loadError renders in PocketColor.statusBad (red), which would misreport a self-healing drop. Added a reconnecting flag and a separate banner in textTertiary.

4. Manual Refresh clears a pending retry. open() now resets reconnecting too — otherwise tapping Refresh mid-backoff cancels the retry task but leaves the banner stuck.

Verified with swift build --target PocketLiveShortly → clean, 10.9s. Real type-check, not just a parse.

Mac has the same gap, left alone — LSSessionModel.swift:134 sets "Stream dropped — reopen the session to catch up." with no retry. Same defect, different file; say the word and it gets the same treatment.

Want D128 logged in DECISIONS.md like the last fix?

Yes
… 4 earlier steps
› running Bash
› Bash sed -n '119,133p' strategy/11-liveshortly-integration.md
› running Bash
› Bash cd ~/rsehgal/github/getpocketai/PocketAI.Core/swift/Sources/PocketLiveShortly && python3 -I - <<'PY' p='LSSessionView.swift' s=open(p).read() old = """ attempt += 1 if attempt > 6 { reconnecting = false loadError = "stream dropped — pull to refresh" return } reconnecting = true // 1s, 2s, 4s, 8s, 15s, 15s — then give up and say so. …
› running Bash
› Bash cd ~/rsehgal/github/getpocketai && python3 -I - <<'PY' p='strategy/DECISIONS.md' s=open(p).read() old = """it is now a `while !Task.isCancelled` loop that backs off 1/2/4/8/15/15s and reopens the stream, surfacing the old error only after 6 failed attempts.""" new = """it is now a `while !Task.isCancelled` loop that backs off 1/2/4/8/16/30s and reopens the stream for as long as the view is open, per LSI-04's existing "auto-reconnect with backoff (1 s -> 30 s max) while the view is ope…

D128 logged in strategy/DECISIONS.md, house style, lane L3.

While writing it I checked the spec and found the same pattern as D127 — 11-liveshortly-integration.md:126 (LSI-04) already required "auto-reconnect with backoff (1 s → 30 s max) while the view is open." The shipped code never implemented it. So I aligned the fix rather than leave it at my own numbers:

  • backoff now 1/2/4/8/16/30s, capped at 30s, retrying for as long as the view is open
  • after 6 attempts it shows "Can't reach the server — still retrying." in red while still retrying underneath — a long outage gets reported without the retry quietly stopping. Clears on the next frame.

Rebuilt: swift build --target PocketLiveShortly clean, 3.6s.

Two things recorded in D128 as honest caveats:

  • Placement deviates from spec. LSI-04 wanted reconnect inside a PocketKit/LiveShortly/LiveShortlyStream.swift abstraction; the shipped client is PocketLiveShortly/LSClient.stream, so the loop lives in the view's stream task. Behaviour matches, structure doesn't.
  • Mac still has the defect — LSSessionModel.swift:134, same give-up-on-drop, untouched.

Two spec lines now have code that didn't match them (D127 §7.2 anchor, D128 LSI-04 reconnect). Worth a sweep for other "spec said it, code skipped it" gaps if you want one.

Still outstanding from earlier: nothing built through Xcode — laptop unreachable, and notarization of 59d80e95 was still In Progress as of 10:58Z.

Now is it noterized ?
… 2 earlier steps
› running Bash
› Bash ps aux | grep -c '[w]atch-notary.sh'; echo "--- elapsed since submit: $(( ($(date -u +%s) - $(date -u -j -f '%Y-%m-%dT%H:%M:%S' '2026-10-29T10:17:52' +%s)) / 60 )) min"
› running Bash
› Bash cat > /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/scratchpad/watch-notary2.sh <<'EOF' #!/usr/bin/env bash # Poll until the notarization leaves "In Progress". ~4h horizon. set -u ID=59d80e95-90be-4501-8473-ed6c07985d69 APP=~/rsehgal/github/getpocketai/PocketAI.Mac/build/dist/export/PocketAI.app DIST=~/rsehgal/github/getpocketai/PocketAI.Mac/build/dist STATUS="" for i in $(seq 1 160); do STATUS=$(xcrun notarytool info "$ID" --key…
› running Bash
› Bash /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/scratchpad/watch-notary2.sh

New watcher armed — polls every 90s for up to ~4 hours, reports status plus Apple's log if it comes back Invalid, or the stapled ticket + Gatekeeper verdict + DMG if Accepted.

The old 45-minute watcher is about to expire and will report a final In Progress line. Ignore that one — it's the timeout, not a verdict.

53 minutes is slow but within what Apple's queue does on a bad day; the account isn't blocked, since the earlier attempt f0fb898c came back Invalid in ~9 minutes. Nothing to do but wait. If it's still pending in an hour or two, Apple's system status page for Developer ID Notary Service is the thing to check.

Normally how much time apples takes to nortarized

Apple's own guidance: most submissions finish in under 15 minutes, and they tell developers to design CI around a worst case of about an hour. Apple publishes live health for it as "Developer ID Notary Service" on their system status page.

Your own baseline backs that up — submission f0fb898c came back Invalid in ~9 minutes this morning, same account, same 20MB zip. So the pipeline is normally fast here.

What makes it drag:

  • queue backlog or an active Notary Service incident (hours, not minutes)
  • first submission of a new bundle id, occasionally slower
  • large uploads — not your case at 20MB

So 53+ minutes is outside normal but not broken, and there's no action that speeds it up. Resubmitting would just put a second copy at the back of the same queue.

Worth knowing about your script: notarytool submit --wait with no --timeout waits indefinitely, so it won't give up on its own — it'll staple and build the DMG whenever Apple answers, even hours from now.

Want me to check Apple's system status page for a live Notary Service incident? That'd tell us whether this is your submission or everyone's.

I will wait

Watcher's armed — I'll report the moment Apple answers, either way.

Working tree meanwhile has the two design fixes uncommitted and unbuilt through Xcode (laptop still down):

  • D127 — bottom anchor on 3 transcripts, dead task removed on iPhone
  • D128 — LiveShortly auto-reconnect + Refresh in … (this one type-checked via swift build)

Easy things available while you wait, none needing Xcode or the phone:

  • Mac LSSessionModel.swift:134 — same give-up-on-drop as D128
  • Localization gate fix — strip the 7 junk catalog keys, cut knownRegions/CFBundleLocalizations to [en] so the stores stop advertising 8 languages you don't ship
  • Sweep for other "spec said it, code skipped it" gaps, since D127 and D128 were both that

Or I hold here.

<task-notification> <task-id>bgfgrb3rj</task-id> <tool-use-id>toolu_013cyBFsqYwfrRBywh5V3aGm</tool-use-id> <output-file>/private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/tasks/bgfgrb3rj.output</output-file> <status>failed</status> <summary>Background command "Watch notarization until it completes" failed with exit code 1</summary> </task-notification>
› running Bash
› Bash cat /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/tasks/bgfgrb3rj.output | head -20; echo "=== new watcher alive? ==="; ps aux | grep -c '[w]atch-notary2.sh'

That's the old 45-minute watcher expiring, exactly as flagged — not a verdict.

notarization status: In Progress
Submission log is not yet available or submissionId does not exist

The log being unavailable is consistent with still-queued: Apple only publishes one once it reaches a verdict. Exit code 1 is just my script's "not Accepted" branch firing on timeout.

Replacement watcher (watch-notary2.sh, ~4h horizon) is alive and polling. Still waiting on Apple.

<task-notification> <task-id>bsxjo3rv4</task-id> <tool-use-id>toolu_01AeCWPYUTsG6d2xrjcaD4HL</tool-use-id> <output-file>/private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/tasks/bsxjo3rv4.output</output-file> <status>completed</status> <summary>Background command "Watch notarization on longer horizon" completed (exit code 0)</summary> </task-notification>
› running Bash
› Bash cat /private/tmp/claude-502/~/889eb54e-0f9a-4ee2-8da1-cb2692df486f/tasks/bsxjo3rv4.output

Notarized. Took 71 minutes.

status:  Accepted
stapler: The validate action worked!          ← ticket stapled
spctl:   accepted — source=Notarized Developer ID
         origin=Developer ID Application: Rohit Sehgal (4RK6YA795V)
dmg:     build/dist/PocketAI.dmg — 23M, 17:00

Full chain green: Developer ID signed, hardened runtime, secure timestamp, notarized, ticket stapled, Gatekeeper accepts. PocketAI.dmg is shippable from getpocketai.app — opens on any Mac with no warning, including offline, since the ticket is stapled into the bundle.

Worth noting: the DMG was built from the pre-fix source. The D127/D128 transcript and LiveShortly changes aren't in it — they came after the 15:47 archive. Next build needs the whole archive → notarize → staple → DMG cycle again.

~/rsehgal/github/getpocketai/PocketAI.Mac/build/dist/PocketAI.dmg

Add notification feature into deaign todo for app and mac
… 6 earlier steps
› running Bash
› Bash grep -E '^\| (UI|MAC)-' strategy/BOARD.md | tail -6; echo "=== tail of board ==="; tail -6 strategy/BOARD.md
› running Bash
› Bash grep -cE '^\| UI-' strategy/BOARD.md && grep -E '^\| UI-1[0-9]' strategy/BOARD.md | tail -4
› running Bash
› Bash python3 -I - <<'PY' p='strategy/BOARD.md' s=open(p).read() # place the two new cards after the last UI- / MAC- rows respectively, by appending # at the end of the table (board is read top-to-bottom; new work goes last). s = s.rstrip('\n') + """ | UI-20 | Notifications (S-NOTIF) — local only; download/reply done, approval-on-open | L5 | next | todo | | MAC-16 | Notifications (M-NOTIF) — approval Allow/Deny actions, downloads, devices, licence | L6 | next | todo | """ open(p,'w').write(s) prin…

Added as a spec + two tracked cards.

strategy/02-design-system.md → new §7.10 Notifications (S-NOTIF / M-NOTIF), marked TODO, not built. Grounded in what exists: the only notification anywhere today is Mac's PairingNotifier (AppModel.swift:439) for "Allow this iPhone?"; iPhone has none.

The constraint I led with, because it determines the whole design: iOS suspends a backgrounded app in seconds and APNs needs a provider server you don't run (00-README.md §3; push is out of scope per 11-liveshortly-integration.md §1/§6). A sleeping iPhone cannot be reached. Everything specced is a local notification posted by an already-running process, and the copy has to say so — an approval request that only arrives when the app opens would otherwise be the one dishonest promise in a product built on honest instruments.

Mac (M-NOTIF) — per-type switches:

  • Approval needed — LiveShortly input_requested with Allow/Deny as UNNotificationActions, so a verdict never needs the window. Hero case, and the seam next-release/agents.md consumes.
  • Reply finished (suppressed when frontmost) · Download finished/failed with reason · Device paired/revoked, phone connected (fold PairingNotifier in rather than keep two paths) · Licence activated/failed.

iPhone (S-NOTIF) — deliberately narrower:

  • Download finished/failed from the background URLSession completion — the one case iOS does wake us for.
  • Reply finished, foreground or on return.
  • Approval needed, labelled honestly as surfacing on next open. No notification type that silently never fires.

Rules: permission via the existing PermissionPrimer.swift pattern, never cold · declining is first-class, every type has an in-app equivalent so nothing is reachable only by notification · no marketing or engagement nudges (D5, and the no-nags commitment) · grouped by thread id per conversation.

Three open questions left for you in the doc: whether "Reply finished" earns its place on Mac over the Pulse strip; whether Allow/Deny from the lock screen should need authentication; and whether the iPhone's honest-but-narrow set justifies asking for the permission in V1 or should wait for the agents release.

Also: §7.8 Settings now lists a Notifications section, and strategy/BOARD.md has two todo cards — UI-20 (L5) and MAC-16 (L6), wave next.

Add - mac and iphone app can cache some of the response and will just ask the server for the next set of message not whole this will make the interaction faster with server - while chats are loading in phone then loading button should be there and refresh as well drag to refresh - iphone app is showing thinking as working while server is working that can be fixed. Add all these for design fixes
… 8 earlier steps
› running Bash
› Bash cd ~/rsehgal/github/getpocketai && sed -n '100,120p;160,180p' PocketAI.Core/swift/Sources/PocketLiveShortly/LSSessionsSection.swift; echo "=== where used ==="; grep -rn 'LSSessionsSection' PocketAI.Iphone/Sources PocketAI.Mac/Sources --include='*.swift'
› running Bash
› Bash python3 -I - <<'PY' p='strategy/11-liveshortly-integration.md' s=open(p).read() old = "## 4. Risks / edge cases" new = '''### LSI-10 · Incremental event fetch + local cache (design fix) L3 · M · LSI-03, LSI-04 - **Observed:** every open refetches the whole log. `LSSessionView.open()` calls `client.session(id)` → `GET /api/sessions/{id}`, which returns the complete event list each time; `LSSessionsSection.load()` carries its own admission in a `ponytail:` comment — *"15s staleness gu…
› running Bash
› Bash python3 -I - <<'PY' p='strategy/BOARD.md' s=open(p).read().rstrip('\n') s += """ | LSI-10 | Incremental event fetch + local cache (delta needs a server param — §1 scope call) | L3 | next | todo | | LSI-11 | Sessions list loading state + explicit Refresh + Retry on error | L5 | next | todo | | LSI-12 | Status line says thinking/running/editing, not always "working" | L5 | next | todo | """ open(p,'w').write(s) print('board updated') PY tail -5 strategy/BOARD.md; echo "=== existing LSI rows? ==…

All three added as task cards in strategy/11-liveshortly-integration.md §3 (LSI-10/11/12), plus rows on strategy/BOARD.md. Each grounded in the code rather than restated as a wish.

LSI-10 · Incremental fetch + cache — confirmed: LSSessionView.open() calls client.session(id) → GET /api/sessions/{id}, which returns the whole event log every time, and LSSessionsSection.load() already admits it in a ponytail: comment — "15s staleness guard, no cache layer — the full list fetch is the slow part." Also LSClient.stream(_ id:) takes no Last-Event-ID, so even reconnects replay the server buffer instead of resuming from the last seq held.

Split into three wins by cost, because one of them isn't free:

  1. Resume the stream with Last-Event-ID — client-only, SSEParser already supports the header, strictly less data.
  2. Cache the transcript on disk per session — client-only, renders instantly then reconciles against the stream.
  3. Delta fetch on cold open (?since_seq=) — needs the server, and §1 of that same doc says "Server changes: none." So it's a scope decision for you, not a task I can just file. Either 1+2 is the V1 answer, or §1 gets reopened deliberately.

Side benefit worth having: 1 and 2 also shrink the LiveShortly line in Network Proof.

LSI-11 · Loading + refresh on the list — LSSessionsSection (reached from Chats/ChatsListView.swift:315) has .task and .refreshable, so drag-to-refresh already works, but there's no loading indicator: loaded only flips after the fetch returns, so a slow list renders empty and reads as "no sessions." LSSessionView does this correctly with "Loading session…", the list doesn't. Spec: match that treatment, keep the drag, add an explicit Refresh (gesture-free and VoiceOver-friendly), and give the error row a Retry instead of being a dead end.

LSI-12 · "working" is hardcoded — LSSessionView.swift:166 and :244 print a literal Text("working") whenever agentBusy, which comes from a coarse event-type switch at :303–:309. A model generating a reply and a tool editing a file both read as "working." The data to do better is already in hand — the server distinguishes prompt/tool_call/file_write/output, and runningLabel (:894) already does it properly inside work runs. So: prompt → thinking, tool_call → running <tool>, file_write → editing <file>, output → running, falling back to "working" only for unrecognised types so a new server event degrades to today's wording.

One decision needed from you: whether LSI-10's step 3 justifies reopening the no-server-changes rule, or whether steps 1+2 are enough.