Skip to content

feat(observability): OS signposts for pressure / pageout / freeze-tier#51

Open
froggychips wants to merge 48 commits into
mainfrom
feat/observability-signposts
Open

feat(observability): OS signposts for pressure / pageout / freeze-tier#51
froggychips wants to merge 48 commits into
mainfrom
feat/observability-signposts

Conversation

@froggychips

Copy link
Copy Markdown
Owner

Summary

Adds time-correlated signposts in three places that produce the clearest pressure → freeze decision → pageout outcome timeline in Instruments:

  • MemoryPressureSource — signpost event on each level change (.normal / .warning / .critical), with PointsOfInterest twin so the standard PoI track shows pressure changes alongside any other component's events.
  • PageoutChain.pageout — interval per strategy attempt, with strategy + outcome (success / skipped / failed). Closes the validation-gate question from ADR-0007 / 0011: "which pageout strategy actually fires on this machine".
  • VortexCoordinator.freezeTier — interval per freeze cycle, with tier + candidate count + final frozen count.

Why

Today's live session showed the three biggest gaps in current observability:

  1. We could not tell from the unified log alone whether mach_vm_behavior_set was actually fired or skipped under fallback to jetsam.
  2. We could not see the latency between a .warning dispatch event and the actual freezeTier call.
  3. We had no time-correlated picture of pressure level vs freeze decisions vs pageout outcomes.

All three are now visible as a single Instruments timeline.

How to use

xcrun instruments -t Logging /path/to/.build/release/FroggyDaemon
# or
open /Applications/Xcode.app/Contents/Applications/Instruments.app
# Choose the "Logging" template, attach to FroggyDaemon, filter
# subsystem == com.froggychips.froggy

Behavior change

None. Existing os.Logger lines preserved. Signposts are observability-only.

Test plan

  • make build clean
  • Manual: run daemon, push pressure to .warning and .critical, verify Instruments shows correlated pressure-level events + freeze-tier intervals + pageout-attempt intervals
  • Verify no regressions in existing tests (make test)

Follow-ups (not in this PR)

  • ADR for "Observability via OS signposts" recording categories + intent
  • Signposts for VortexActor.thawProcess / thawAll (thaw timeline)
  • Signposts for VisionActor OCR cycles
  • Signposts for full generation pipeline in MLXSupervisor (partial coverage exists)

Related

  • ADR-0006 reactive memory pressure
  • ADR-0007 pageout strategies
  • ADR-0011 validation gate (substrate-level honesty)
  • ADR-0014 design docs after implementation

froggychips and others added 30 commits May 6, 2026 19:55
Fixes everything that blocks Froggy from being safely built and run:

VortexCore
- Replace broken sysctlbyname("kern.memo_status_level") with
  host_statistics64 / HOST_VM_INFO64; pressure is now correct percentage.
- freezeProcess() now validates pid: rejects pid<=100, self, foreign EUID
  (via kill(pid,0) probe), and a hard-coded blacklist of system executables
  (launchd, WindowServer, loginwindow, coreaudiod, ...). Throws VortexError
  instead of silently kill()-ing anything.
- thawProcess/thawAll preserved; thawAll is idempotent and called by daemon
  on shutdown.

MLXActor
- Switch dependency from non-existent MLX product to ml-explore/mlx-swift-lm
  (MLXLLM + MLXLMCommon + MLXHuggingFace) plus huggingface/swift-transformers
  (Tokenizers).
- Use real public API: LLMModelFactory.shared.loadContainer(from:using:),
  ModelContainer.prepare(input:) + ModelContainer.generate(input:parameters:),
  GenerateParameters(maxTokens:temperature:), MLX.Memory.memoryLimit.
- Drop bogus autoreleasepool around await; default GPU memory limit is now
  60% of physical RAM, not a hard-coded 4GB.

VisionActor
- Replace deprecated CGDisplayCreateImage with
  SCScreenshotManager.captureImage(contentFilter:configuration:).
- Recognition languages now ["ru-RU","en-US"] with usesLanguageCorrection.
- OCR runs nonisolated so it doesn't block the actor.
- State file moved to ~/Library/Application Support/Froggy/state.json with
  POSIX 0600 perms (was world-readable in $HOME with raw OCR text — privacy
  leak: passwords/tokens visible on screen would leak).
- Cooperative cancellation via Task.isCancelled.

FroggyDaemon
- Remove hard-coded /Users/yaroslav/models/... path. Model path now from
  --model-path CLI flag or FROGGY_MODEL_PATH env var.
- SIGINT/SIGTERM handlers via DispatchSourceSignal: on signal, vortex.thawAll()
  is called before exit. Without this, SIGSTOP-ed processes would stay frozen
  forever after Ctrl-C.
- Replace print() with os.Logger throughout.
- Capture Task is now retained and cancelled on shutdown.

Package.swift
- Strict concurrency + ExistentialAny upcoming features enabled per target.
- Add Package.resolved + .swiftpm + DerivedData to .gitignore (was missing,
  and the file itself was a single line with literal "\n" instead of newlines).

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
VortexCoordinator (new)
- Wraps MLXActor + VortexActor.
- loadModel() snapshots PIDs of running NSRunningApplications matching
  config.freezeBundleIds (Slack/Discord/Spotify/Teams/Dropbox by default)
  and SIGSTOPs them before MLX load. Failed loads fall through to thaw.
- unloadModel() and emergencyThaw() restore the exact set frozen by this
  coordinator (so concurrently-frozen pids aren't accidentally resumed).
- generate() proxies to MLXActor for clean external API.

FroggyConfig (new)
- Codable JSON config loaded from
  ~/Library/Application Support/Froggy/config.json with sensible defaults.
- Saved with mode 0600. CLI flags + env vars override config at the daemon.
- Fields: modelPath, gpuMemoryLimitBytes, captureIntervalSeconds,
  freezeBundleIds, ipcSocketPath.

IPCServer + IPCProtocol (new)
- Unix domain socket (default ~/Library/Application Support/Froggy/froggy.sock,
  mode 0600) with a one-line-JSON-per-direction protocol.
- Commands: status, generate, freeze, thawAll. Backed by an
  IPCRequestHandler protocol so the daemon (or future tests) wire their own.
- Concurrent connections handled via Task.detached.

FroggyDaemon
- Loads FroggyConfig, applies CLI/env overrides, builds VortexCoordinator
  and IPCServer.
- Signal handlers now call coordinator.emergencyThaw() (was vortex.thawAll
  directly) so coordinator-owned pids are released too.
- Renamed in-file Config struct -> CLIArgs to avoid shadowing FroggyConfig.

Tests
- VortexCoreTests: VortexActor (low/zero/self pid validation, thawAll
  idempotence, memory pressure 0..100, initial suspendedCount=0),
  Config (defaults, JSON round-trip, missing-file → defaults, save/load
  round-trip with 0600 perm check, malformed-JSON throws),
  IPC protocol Codable round-trip, IPCServer integration (start, connect
  via real unix socket, exchange one request/response, stop).
- LushaBridgeTests: VisionActor capturing()=false initial, state path
  lands in Application Support/Froggy/state.json.
- 18 tests, all green locally.

CI
- .github/workflows/ci.yml: macos-15, swift build -c debug + swift test
  --parallel, .build cached on Package.swift hash.
…kaging (#3)

LushaBridge
- FrameDigest: 32x32 grayscale fingerprint of a CGImage. similarity()
  returns 0..1 by mean absolute pixel diff. VisionActor consults it before
  running OCR — if similarity to last frame >= config.frameSimilarityThreshold
  (default 0.98) the OCR pass is skipped. ~80% CPU saved on idle screens.
- Redactor: regex-based stripping of secrets from OCR output before it
  hits the on-disk state file or the context window. Detects:
  PEM private-key blocks, AWS access keys, GitHub PATs (legacy + fine-grained),
  Anthropic + OpenAI keys, Slack tokens, JWTs, bearer headers,
  password/api_key/secret/token labels, and credit cards (Luhn-validated to
  avoid false positives on order numbers).
- ContextStore: actor with a ring buffer of the last N (default 30)
  redacted OCR snapshots + timestamps. recentContext(maxChars:) joins
  newest-first within a char budget for prompt augmentation.
- VisionActor: now takes Redactor + ContextStore + frameSimilarityThreshold;
  pushes redacted snapshots to the store and applies frame-diff skipping.

VortexCore
- FroggyConfig grew three fields: frameSimilarityThreshold (0.98),
  contextWindowSize (30), contextMaxChars (4096). Custom init(from:) keeps
  older config.json files loadable — missing keys fall back to defaults.
- IPCProtocol: new request field maxChars; new response fields context,
  snapshots.
- New IPC command "context" returns recentContext from the store. "status"
  now also reports snapshots count.
- os_signpost intervals around VortexCoordinator.loadModel,
  MLXActor.loadModel + generate, and VisionActor.runCycle/ocr (with a
  frameSkipped event when the diff short-circuits OCR). Visible in
  Instruments → os_signpost lane under subsystem com.froggychips.froggy.

FroggyDaemon
- Builds + wires Redactor and ContextStore into VisionActor and the IPC
  handler.
- DaemonIPCHandler: new "context" branch.

packaging/
- com.froggychips.froggy.plist: per-user LaunchAgent template (RunAtLoad,
  KeepAlive on non-zero exit, Interactive process type, log to
  /tmp/froggy-daemon.log).
- packaging/README.md: codesign + notarytool + launchctl bootstrap recipe.
  Out of CI scope — needs Developer ID secrets.

Tests
- RedactorTests: 11 cases — every secret class plus the must-not-redact
  paths (clean text, random-but-Luhn-invalid 16-digit number).
- FrameDigestTests: identical=1.0, white-vs-black=0.0, near-identical>0.99,
  size mismatch=0.0.
- ContextStoreTests: starts empty, ring buffer evicts oldest beyond capacity,
  recentContext respects maxChars, clear works.
- 37 tests total, all green locally.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
…ements (#4)

VortexCore
- IPCClient actor: AF_UNIX SOCK_STREAM round-trip with timeout-via-task-group.
  Convenience methods: status, generate, context, loadModel, unloadModel,
  freeze, thawAll, accessors, snapshot. Other Swift consumers (the menubar
  app, future CLI tools, integration tests) talk to the daemon through this
  one type.
- IPCProtocol: new request fields path, accessor; new response fields
  modelPath, lines, accessors. Wire-compatible with existing Phase 1 clients
  (all-optional Codable).
- MLXActor: tracks loadedModelPath, exposes currentModelPath() so status can
  show what's loaded.

LushaBridge
- LushaAccessor protocol + AccessorRegistry actor: pluggable context
  collectors keyed by id. Built-ins: OCRAccessor (last entry from
  ContextStore), FrontmostAppAccessor (NSWorkspace.frontmostApplication, on
  MainActor).

FroggyDaemon
- New IPC commands: loadModel {path}, unloadModel, accessors, snapshot
  {accessor}. Hot-swap works without restarting the daemon — old container
  is dropped, MLX.Memory.clearCache() runs, new model loads with the same
  freeze-allowlist policy.
- Builds + registers the two built-in accessors at startup; IPC handler
  dispatches "snapshot" through the registry.
- Status response now carries modelPath.

FroggyMenuBar (new executable target)
- SwiftUI MenuBarExtra app. Polls daemon every 5s via IPCClient and shows:
  capturing flag, model loaded + path, memory pressure %, frozen procs,
  snapshots count; a TextField + Load/Unload buttons for hot-swap; a
  scrollable recent-context viewer; thaw-all and quit buttons. Errors
  surface inline. Connects to FroggyConfig.defaultSocketPath.

docs/adr/
- 0001 actors over locks: why Swift 6 actors instead of NSLock.
- 0002 unix socket over XPC: why filesystem ACL'd AF_UNIX rather than
  NSXPCConnection (no bundle/codesign needed to develop).
- 0003 codable JSON config: why not TOML/YAML (zero deps, custom init for
  forward-compat).
- 0004 coordinator vs direct coupling: why VortexCoordinator owns the
  freeze policy instead of MLXActor knowing about Vortex.

packaging/
- Froggy.entitlements: app-sandbox=false (Vortex needs raw kill() across
  pids it doesn't own), all other capabilities off until needed.
- README updated with codesign --entitlements invocation and a note on
  why sandbox stays off vs. what TCC still gates.

Tests
- IPCClientTests (5): round-trip status, loadModel echo, accessors+snapshot,
  unknown-cmd error path, connection failure when socket missing.
  Note: socket paths under /tmp/ to stay under sockaddr_un's 104-byte limit
  (the longer /var/folders/... default temporaryDirectory blew it up).
- LushaAccessorTests (5): registry list+snapshot, unknown id returns nil,
  re-register overwrites, OCRAccessor reads last ContextStore snapshot,
  empty store returns empty.
- 47 tests total, all green locally.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
… recovery) (#5)

* phase-4: production hardening — CI, default-deny, persistent SCStream, streaming, recovery

CI (P0)
- Switch runs-on macos-15 → macos-14 (the previous setting produced
  startup_failure on every run since Phase 1 — tests were never actually
  exercised on push). Add workflow_dispatch + explicit permissions: contents:
  read.

ProcessClassifier (default-deny)
- New struct in VortexCore. classify(pid) → .freezable(executablePath) or
  .forbidden(reason).
- Allowed only if: pid > 100, not self, same EUID (kill(pid,0) probe), and
  executable path under /Applications/, ~/Applications/, or
  /opt/homebrew/Cellar/. Anything else — including /System/, /usr/, /Library/,
  /sbin/ — is rejected by default. Replaces the old ~16-name forbidden
  blacklist (which was inherently incomplete).
- VortexActor.freezeProcess delegates here. Drops the ad-hoc validate() and
  the @_silgen_name proc_name shim.

FrozenPidsStore (boot-time recovery)
- Persisted JSON list of {pid, executablePath, frozenAt} at
  ~/Library/Application Support/Froggy/frozen.pids (mode 0600).
- VortexActor.freezeProcess writes there on every successful SIGSTOP;
  thawProcess removes; thawAll clears.
- FroggyDaemon.main calls store.recover() on startup → SIGCONT every entry,
  then truncates. Closes the "daemon crashed mid-load with apps frozen
  forever" hole.

ScreenStream (persistent SCStream + delegate)
- New actor in LushaBridge. Wraps SCStream with an SCStreamOutput
  delegate that converts each CMSampleBuffer to CGImage and stores the
  latest. VisionActor pulls latestFrame() per cycle instead of running
  SCShareableContent.excludingDesktopWindows + SCScreenshotManager.captureImage
  on every tick (saved ~100–200 ms / cycle).
- Surfaces lastErrorMessage(); VisionActor.lastCaptureError() exposes it,
  status IPC includes lastCaptureError so denied screen-recording permission
  is no longer silent.

Streaming IPC
- IPCResponse gains final: Bool? marker.
- IPCRequestHandler protocol gains optional handleStream(_:) returning
  AsyncThrowingStream<IPCResponse, Error>?. Default implementation returns
  nil → existing one-shot handlers still work.
- IPCServer routes through handleStream when provided; emits one JSON line
  per chunk; closes connection after final == true (or appends a synthetic
  trailer).
- MLXActor.generateStream(prompt:maxTokens:) yields tokens as they arrive.
- DaemonIPCHandler streams the "generate" command. One-shot path preserved
  for compatibility.

IPCClient
- Switches both send() and the new sendStream() to use SO_RCVTIMEO/SO_SNDTIMEO
  on the socket — gates the "timeout fired but blocking syscall still holding
  fd" leak from Phase 1.
- generateStream(prompt:maxTokens:) returns AsyncThrowingStream<String,
  Error>.
- Errno is now captured inside the write closure.
- Buffer index math uses distance(from:startIndex) so a Data with a non-zero
  startIndex (slice base) doesn't corrupt subsequent line splits.

IPCServer hardening
- start() now refuses to bind if another process is already listening on the
  socket (prevents two daemons silently stealing each other's path).
- stop() shutdown(fd, SHUT_RDWR) before close() — wakes the blocking accept(2)
  in the detached task instead of leaking it.
- listen() backlog 8 → 32. Accept loop also handles ECONNABORTED.

VortexActor refactor
- Constructor now takes (classifier:, pidStore:); both injectable for tests.
- thawProcess and thawAll became async (need to await pidStore writes).
- Removed forbiddenExecutables and the @_silgen_name proc_name import.

README.md rewritten to current reality
- Drops the old "Python 3.11+" claim. Documents MenuBar app, hot-swap,
  IPC commands, config schema, packaging/, ADR index. Adds quick-start that
  actually matches the code.

Tests
- ProcessClassifierTests (7): low/zero pid, self, nonexistent, exec path
  retrieval for self, default-allowed-prefixes, and a real subprocess
  /bin/sleep that must be rejected by path.
- FrozenPidsStoreTests (6): empty start, add+remove, duplicate replaces,
  persist across instances + 0600 perms, recover() clears file even if
  pids no longer exist, clear().
- IPCStreamingTests (3): handler emits N chunks then final, one-shot still
  works on the same server, zero-chunk stream still emits final marker.
- 63 tests total (was 47), all green locally.

* ci: simplify workflow, try macos-latest (macos-14 also startup_failure)

* ci: add minimal probe workflow to diagnose startup_failure

* ci: remove probe workflow (startup_failure persists across all yamls — account-level issue)

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
… headline integration test (#6)

LushaBridge / Redactor
- Pre-compile NSRegularExpression once at struct init instead of on every
  apply() call (was 12 regexes × N lines × 0.5 Hz = thousands of unnecessary
  compilations per hour at idle).
- Built-in rules now exposed as `Redactor.builtInRules: [RedactionRule]`.
- Optional ~/Library/Application Support/Froggy/redaction-rules.json — user
  can append corporate token patterns without rebuilding. Loaded by default
  in `Redactor()`; opt out with `Redactor(loadUserRules: false)`.
- New `Redactor(rules:)` for tests / custom-only setups.
- New RedactionRule Codable struct (name, pattern, replacement,
  caseInsensitive).

FroggyMenuBar
- TCC pre-flight banner: lights up when status.lastCaptureError != nil OR
  when capturing has been "yes" for >10s with snapshots still 0 (a soft
  signal the user denied screen recording at the OS prompt). Banner has
  "Open Privacy Settings" button that jumps directly to
  System Settings → Privacy → Screen Recording via x-apple.systempreferences:
  URL. Menubar label switches to 🐸 ⚠︎ during the warning state.
- New "Generate" panel: prompt TextField, Generate/Cancel buttons backed by
  IPCClient.generateStream — tokens stream into a ScrollView in real time
  instead of waiting for the whole answer. Cancel cancels the upstream Task.
  Generate disabled until model is loaded.
- VortexCore import wired so the same IPCClient/types are shared.

Tests
- VortexIntegrationTests (3): real /bin/sleep child process, freezeProcess
  via VortexActor with extraAllowedPrefixes=["/bin/"], verify ps -o stat
  shows 'T' (stopped), thaw, verify it no longer shows 'T'. Covers the
  Vortex<->FrozenPidsStore round-trip and the recover() path. This is the
  headline-feature integration test the previous review flagged as missing.
- RedactorTests +3 cases: custom rules apply alongside built-ins, user rules
  load from a temp JSON file, 1000-redaction smoke must finish under 2s
  (sanity check the compile-once optimization).
- 69 tests total (was 66), all green locally.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
…error reset (#7)

LushaBridge / SimilarityScorer (new)
- Protocol SimilarityScorer with similarity(_:_:) async -> Double in 0..1.
- JaccardSimilarityScorer: tokenize on whitespace + punctuation, lowercase
  by default, configurable minTokenLength, return |A∩B|/|A∪B|. Cheap,
  zero deps, catches "same screen as 2 sec ago".
- NoopSimilarityScorer: always returns 0 — used when dedup is off.
- Slot left for an MLXEmbedders-backed scorer to drop in later (same
  protocol, swap the implementation in main.swift).

ContextStore — opt-in dedup
- Init now takes (capacity, scorer, dedupThreshold). When push() is called,
  similarity to the most recent snapshot is computed; if ≥ threshold, the
  snapshot is skipped. Default scorer is NoopSimilarityScorer → unchanged
  behavior for callers that don't pass a scorer.
- FroggyConfig: contextDedupEnabled (default true), contextDedupThreshold
  (default 0.85). Older config.json files still load (custom init(from:)
  falls back to defaults).
- FroggyDaemon wires JaccardSimilarityScorer when contextDedupEnabled.

ContextStore.recentContext truncation
- Old behavior: if adding a snapshot would overshoot maxChars, skip it
  whole. For Cyrillic/emoji OCR this could overshoot by 2× or undershoot
  silently — `block.count` is grapheme count, not byte count.
- New behavior: truncate the offending block via String.prefix(remaining),
  guaranteeing the result.count <= maxChars. Empty store and zero budget
  still return "".

ScreenStream stale error
- FrameSink.didOutputSampleBuffer now clears lastError on every successful
  frame. Previously, if the user denied screen recording, then granted it
  later, the menubar TCC banner stayed lit forever because lastError was
  never cleared.

Tests (+14, 83 total)
- SimilarityScorerTests (8): identical, disjoint, partial overlap, case
  insensitivity, punctuation handling, both-empty-is-1.0, minTokenLength
  filtering, Noop always 0.
- ContextStoreTests +6: dedup skips duplicate neighbors, dedup keeps
  different content, default ContextStore (Noop) doesn't dedup,
  threshold=0 still drops identical (Jaccard=1.0 ≥ 0.0), Cyrillic + emoji
  truncation are bounded by maxChars and non-empty.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
VortexCore / PromptAugmenter (new)
- Stitches user prompt with recent OCR context using a configurable
  template. Default template: short system instruction → CONTEXT block
  with the OCR text → User: → Assistant:. Empty context → bare prompt
  (don't waste tokens on a "nothing here" block).
- maxContextChars cap: when supplied context overflows, takes the suffix
  (newest part) so timestamps stay relevant.

IPC / DaemonIPCHandler
- IPCRequest gains useContext: Bool? (Codable optional, backward compatible).
- "generate" (one-shot + streaming) now augments via DaemonIPCHandler.augmentedPrompt
  when useContext == true: fetches ContextStore.recentContext capped by
  config.contextMaxChars, runs through PromptAugmenter, sends to MLX. The
  context the model sees is the snapshot at generation time, not whatever
  was current when the client made its first call.
- IPCClient.generate / generateStream gain a useContext parameter (default
  nil → bare prompt for old callers).

FroggyCLI (new executable, product name `froggy`)
- Subcommands: status (pretty rows), gen [--context|-c] [-n N] <prompt>
  (streaming to stdout), ctx [--max N], load <path>, unload, accessors,
  snap <id>, thaw, help. Exit codes: 0 ok, 1 daemon error, 2 bad args.
- Honors FROGGY_IPC_SOCKET env override.
- New target in Package.swift, depends on VortexCore.
- Beats `nc -U` for shells, scripts, and CI smoke tests; uses the same
  IPCClient as MenuBar so future protocol changes only need updating
  in one place.

Tests (+5, 88 total)
- PromptAugmenterTests (5): empty context returns bare prompt, whitespace-
  only context returns bare prompt, non-empty context wraps with header
  and footer, maxContextChars enforced (counted via U+2603 marker char so
  the default template's text doesn't pollute the count), custom template
  honored.

Docs
- ADR 0005 explains why augmentation lives daemon-side (avoids race
  between client.context() and client.generate(); centralizes the
  template; doesn't couple MLXActor to ContextStore — that was the whole
  point of the Coordinator from ADR 0004).
- README: new "Context-aware generation" section with example;
  quick-start now shows `froggy` subcommands; IPC table updated.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
)

VortexCore / MemoryPressureSource (новый)
- MemoryPressureLevel: .normal < .warning < .critical, Comparable.
- MemoryPressureSource protocol — абстракция над источником событий.
- DispatchMemoryPressureSource — обёртка вокруг
  DispatchSource.makeMemoryPressureSource, broadcast в N подписчиков.
- FakeMemoryPressureSource — управляемый тестовый источник.

VortexCore / MemoryPressureMonitor (новый)
- Подписывается на источник, публикует AsyncStream<MemoryPressureLevel>
  с debounce понижения: эскалация мгновенно, понижение — через
  cooldownSeconds (default 60), upgrade в окне cooldown'а отменяет
  pending-downgrade.
- nudge(level, durationSeconds) — виртуальное давление от calling-кода
  (loadModel это и использует), max(observed, nudge).
- start()/stop() идемпотентны, события — nonisolated let, чтобы listen-
  task мог итерироваться без actor-hops на каждый next().

VortexCore / VortexCoordinator (переписан под tier'ы)
- init теперь принимает (mlx, vortex, monitor, tier1BundleIds,
  tier2BundleIds, finder, gradualThawDelaySeconds=10).
- startMonitoring/stopMonitoring управляют listen-task.
- applyPolicy:
  * .warning → freezeTier(.tier1)
  * .critical → freezeTier(.tier1) + freezeTier(.tier2)
  * .normal → thawTier(.tier2), +gradualThawDelay c → thawTier(.tier1).
    Pending-thaw отменяется при upgrade.
- loadModel(path) делает monitor.nudge(.warning, 60) — политика
  срабатывает общим путём.
- emergencyThaw() — синхронная оттепель обоих tier'ов + vortex.thawAll().
- pressureSnapshot() — для IPC.
- ProcessFinder protocol + NSWorkspaceProcessFinder вынесли pids-fetch
  в инжектируемый компонент (тесты подменяют на StubFinder).
- VortexFreezing protocol — тесты подменяют VortexActor на StubVortex
  без kill().

VortexCore / FroggyConfig
- Новые поля: freezeTier1BundleIds, freezeTier2BundleIds (defaults:
  Spotify/Discord/Telegram/Dropbox для tier1; Slack/Notion/Teams для
  tier2), pressureCooldownSeconds (60).
- Старое freezeBundleIds: [String] стало deprecated optional. Custom
  init(from:) маппит legacy → tier1 если новое поле отсутствует;
  если оба указаны — побеждает новое.

VortexCore / MLXActor
- MLX.Memory.memoryLimit перенёс из init в loadModel, иначе parallel
  xctest без metallib падал с "library not found" при создании
  MLXActor() в моках Coordinator-тестов.

IPC / FroggyDaemon
- IPCResponse: pressureLevel, tier1Frozen[], tier2Frozen[],
  secondsInLevel.
- Новая команда "pressure" в DaemonIPCHandler — отдаёт
  coordinator.pressureSnapshot().
- main: создаёт DispatchMemoryPressureSource → MemoryPressureMonitor →
  Coordinator с tier'ами; вызывает coordinator.startMonitoring()
  после loadModel.

Tests (+12, 100 total)
- MemoryPressureMonitorTests (6): начальный normal публикуется,
  эскалация мгновенная, downgrade ждёт cooldown, upgrade отменяет
  pending downgrade, nudge поднимает до warning и истекает,
  observed > nudge перекрывает nudge.
- VortexCoordinatorPolicyTests (4): warning → только tier1, critical →
  оба, cooldown реально 0.5s, upgrade отменяет pending thaw.
- ConfigTests +2: legacy freezeBundleIds → tier1, новое поле
  побеждает legacy.

Docs
- ADR 0006 reactive-memory-pressure.md.
- README: новый раздел про реактивную политику, IPC-таблица + pressure,
  пример config.json под новые поля.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
#10)

* mem-2: принудительный pageout — machVM/jetsam/scratch + chain + entitlement

VortexCore / Pageout.swift (новый)
- PageoutStrategy enum: machVM, jetsam, scratch.
- PageoutImpl protocol — для testability.
- MachVMPageoutImpl — task_for_pid + mach_vm_region enumerate +
  mach_vm_behavior_set(VM_BEHAVIOR_PAGEOUT) для writable не-executable
  regions. На dev-подписи без task_for_pid-allow entitlement'a возвращает
  KERN_FAILURE.
- JetsamPageoutImpl — memorystatus_control(SET_PRIORITY_PROPERTIES) с
  JETSAM_PRIORITY_IDLE. Через @_silgen_name (приватный API из
  <sys/kern_memorystatus.h>). Default стратегия.
- ScratchPageoutImpl — malloc(N MB)+memset+free на background-Task,
  провоцирует компрессор. Default scratch=256 MB.
- PageoutChain actor: пробует preferred → fallbacks по цепочке
  machVM→jetsam→scratch (или jetsam→scratch при .jetsam preferred).
  Лог-варн один раз за сессию для каждой проваленной стратегии.

VortexCore / VortexActor
- init теперь принимает (..., pageout: PageoutChain?). После успешного
  SIGSTOP вызываем pageout.pageout(pid:). Ошибка pageout НЕ фейлит
  freeze — лог-варн, и пошли дальше: SIGSTOP уже сработал.

VortexCore / Config
- Новые поля: pageoutStrategy (default .jetsam), pageoutScratchMB
  (default 256). Custom init(from:) с decodeIfPresent — старые
  config.json грузятся.

FroggyDaemon main
- Создаёт PageoutChain из config'а и передаёт в VortexActor.

packaging/
- Froggy.entitlements: добавлен com.apple.security.cs.debugger=true
  (нужен для task_for_pid). Реальный task_for_pid-allow требует
  Developer ID + provisioning profile.
- README: новый раздел про pageout-стратегии и ограничения dev-подписи.

Tests (+7 unit + 1 skipped benchmark = 107 total)
- PageoutChainTests (6): jetsam-preferred succeeds, machVM→jetsam fallback,
  full-chain fallback к scratch, all-fail, scratch-preferred не дёргает
  others, jetsam-preferred не дёргает machVM.
- PageoutBenchmarkTests (1, skip по умолчанию через
  FROGGY_RUN_PAGEOUT_BENCHMARK=1) — spawn python с 200 MB heap, freeze +
  pageout, замерить RSS до/после через `ps -o rss=`.

Docs
- ADR 0007 pageout-strategies.md.
- README: новый bullet про pageout, обновлённый пример config.json.

* mem-2: дописать README + ADR-индекс (предыдущий коммит пропустил из-за read-state worktree)

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
…#11)

Главная цель: гарантированный возврат peak unified memory ядру после
unloadModel. Сейчас MLX держит ~500 MB резидентного даже после
clearCache() — единственный надёжный способ вернуть это ядру — kill
дочернего процесса. Это закрывает основную дыру 8 GB Mac.

MLXWorkerProtocol (новый library target)
- MLXWorkerCommand / MLXWorkerEvent: Codable wire-формат, JSON-line.
- Команды: load{path}, generate{prompt, maxTokens, temperature},
  shutdown, ping. Каждая с requestId.
- События: ready, error{message}, chunk{text}, done, goodbye, pong.
- Общая зависимость и для демона, и для worker'а — никто не знает
  про другого, оба знают про этот target.

FroggyMLXWorker (новый executable)
- Sources/FroggyMLXWorker/main.swift: WorkerRuntime actor читает
  stdin построчно, парсит MLXWorkerCommand, диспатчит. На load —
  LLMModelFactory.shared.loadContainer + ready event. На generate —
  ModelContainer.prepare + .generate, стримит chunks. На shutdown —
  goodbye + exit. MLX.Memory.memoryLimit ставится перед первой
  загрузкой (lazy), чтобы parallel-xctest не дёргал metallib.
- mlx-swift-lm + swift-transformers — теперь зависимости только
  этого target'а. Демон больше не тянет MLX runtime в своё
  адресное пространство, и весит ~50 MB без модели.

VortexCore / MLXSupervisor (заменяет MLXActor)
- Зеркалит публичный API старого MLXActor: loadModel, unloadModel,
  isLoaded, currentModelPath, generate, generateStream.
- Внутри держит Foundation.Process, Pipe для stdin/stdout, диспатчит
  события по requestId в pendingRequests-словарь continuation'ов.
- readabilityHandler парсит stdout построчно, передаёт data в actor
  через nonisolated ReadBridge.
- На unloadModel — shutdown JSON, ждёт goodbye до 3 секунд (через
  withTaskGroup с timeout-task'ой), потом SIGKILL.
- На крах worker'а — terminationHandler триггерит cleanup: все
  pending continuation'ы получают .workerCrashed, isLoaded → false.
  Status в IPC отражает разгрузку.

VortexCore удалил импорт mlx-swift-lm/swift-transformers
- Sources/VortexCore/MLXActor.swift удалён.
- Package.swift: VortexCore зависит только от MLXWorkerProtocol.
- Все call-sites (VortexCoordinator, FroggyDaemon, MenuBar VM)
  работают на MLXSupervisor с тем же API.

VortexCore / FrozenPidsStore.Entry получил `category: String?`
- Worker спавнится с category="worker". На startup demon recover()
  обрабатывает worker-сирот SIGKILL'ом (а не SIGCONT, как обычные
  замороженные приложения) — после краха демона worker не нужен,
  модель в его памяти некому использовать.

VortexCore / Config
- mlxWorkerPath: String? — путь к executable'у worker'а. nil →
  default: рядом с демоном через Bundle.main.executableURL.

FroggyDaemon main
- MLXSupervisor создаётся с workerExecutableURL = config.mlxWorkerPath
  и pidStore — supervisor сам регистрирует worker-pid в frozen.pids.

packaging/
- README: codesign теперь для двух бинарей. Подчёркнуто, что worker
  должен лежать рядом с демоном.

Tests (+1, 108 total, 1 skipped)
- MLXSupervisorTests (1): testWorkerNotFoundIsExplicitError —
  проверяет fast-path ошибки без spawn'a. Полноценный pipe-lifecycle
  тест намеренно не делается здесь (хрупкий через python-stub),
  оставлен на Mem-3.1.

Docs
- ADR 0008-mlx-subprocess-isolation.md.
- README: новый bullet про child-process MLX, обновлён config-пример,
  ADR-индекс.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Чтобы Froggy находился в поиске и читателю сразу было ясно зачем он:

- README.md: блок на английском в самом верху — кратко «зачем
  существует», основной use-case (8 GB Apple Silicon), стек, статус
  «personal-use scaffolding, not a product». Существующая русская
  документация перенесена под якорь "Русская документация" без
  правок содержания.
- docs/POSITIONING.md: явный список «что это / что это не» — отсекает
  wrong-expectations issues (Windows, Intel Mac, production
  deployments, замена Rewind).
- Контакт: @froggychips в Telegram.
- Repo description и 19 topics обновлены через gh CLI отдельно
  (не в этом коммите).

LICENSE сознательно не добавляю — это решение автора, в POSITIONING
оставлен placeholder.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MIT — стандартный выбор для open foundation-проекта. Не GPL/AGPL
(избыточно ограничивает контрибьюторов и закрытые надстройки),
не Apache-2.0 (избыточен без патентного риска).

Copyright записан на handle `froggychips`, без раскрытия имени.
В POSITIONING.md заменён placeholder про «source-available» на
явную ссылку на MIT.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Чтобы Froggy был findable и понятен international посетителям с
первой секунды:

- README.md теперь полностью на английском (полный перевод существующих
  секций: Features, Stack, Project layout, Quick start, Context-aware
  generation, Configuration, IPC commands, Installing as a LaunchAgent,
  Documentation).
- README.ru.md — оригинальная русская версия как отдельный файл, без
  потери контента.
- В шапке обоих — переключатель `🌐 English · Русский`.

Code-блоки (config JSON, IPC table, CLI snippets, шаблон промта) не
тронуты — они и так универсальны.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onest-doc (#15)

Этап 0 нового плана: убираем ручные циклы build/test/format и
готовим параллельный поток работы. Этап 0.5 заодно — лживая
documentация про cs.debugger как замену task-for-pid-allow.

.claude/settings.json (новый, project-scope)
- permissions.allow: read-only Bash команды (swift build/test/run/package,
  git status/diff/log/branch/fetch/worktree, gh pr/run/api на Froggy
  репо, jq, grep, rg, ls, cat, head, tail, wc, find, ps, vm_stat,
  memory_pressure, echo, pwd) — 30 правил. Не нужно аппрувить каждый
  раз.
- hooks.PostToolUse Edit|Write: для *.swift запускает swift-format
  format --in-place, не падает если swift-format не установлен (warn
  в stderr).
- hooks.PreToolUse Bash if=Bash(git commit*): запускает swift test
  --parallel --quiet, блокирует commit если красный.
- hooks.Stop: git status --short в stdout, чтобы summary в конце турна.

.claude/agents/macos-internals.md
- Subagent для дебага низкоуровневых вызовов (mach/xnu/TCC/codesign/
  task_for_pid/memorystatus_control). Tools: Read, Grep, Glob, Bash,
  WebFetch.

.claude/agents/swift6-concurrency-reviewer.md
- Pre-merge ревьюер actor-кода: actor reentrancy, @unchecked Sendable,
  AsyncStream lifecycle, MainActor hops, ExistentialAny. Tools: Read,
  Grep, Glob, Edit (одного файла).

.claude/commands/froggy-bench.md
- Slash command для baseline-снимка: vm_stat, memory_pressure, RSS
  демона/worker'а, frontmost RSS, status/pressure через IPC,
  time-to-first-token. Сравнивает с bench/baseline.json.
  --save записывает текущий snapshot как новый baseline. Сценарий
  определяется автоматически (idle/model-loaded/under-pressure).

.claude/commands/froggy-pr.md
- Slash command для PR'а по конвенции: phase-<phase>/<slug> ветка,
  PR-шаблон по образцу #9–11.

packaging/Froggy.entitlements
- Удалено `com.apple.security.cs.debugger` (бывшее false-equivalent
  для task_for_pid-allow). Заменено на длинный комментарий, объясняющий
  что это разные права и что machVM требует Apple-issued
  provisioning profile, а не hardened-runtime entitlement.

packaging/README.md
- Раздел про machVM перепиcан на честный язык: для активации
  нужен либо approved Apple provisioning profile (Apple обычно
  отказывает третьим сторонам), либо отключённый SIP. Не «просто
  Developer ID». TL;DR в конце.

docs/adr/0007-pageout-strategies.md
- Раздел "Default = jetsam" обновлён. Раздел "Последствия" теперь
  явно говорит что синхронный pageout требует approved Apple
  profile или disabled SIP, а не «Developer ID».

Долги мем-серии (открытые на момент Этапа 0):
- Mem-3.1 pipe-lifecycle xctest — отдельный PR (Worktree A).
- IPC pressure pageout-счётчики — отдельный мини-PR.
- Mem-4 KV-cache, Mem-5 FreezeRanker — Worktree A/B.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Цель — компас для архитектурных решений и защита от infrastructure
gravity trap. Документ закрепляет три уровня выводов из последней
итерации обсуждения:

1. Что Froggy есть: memory-orchestration runtime + trust-governance
   layer для local AI на constrained Apple Silicon. Не «assistant»,
   не «wrapper», не «product».
2. Anti-compromise design: модель остаётся useful-sized, а ОС вокруг
   ужимается. Удаление freeze layer'а коллапсирует thesis.
3. Qualitative substrate, не quantitative — критерий приоритизации
   любой substrate-работы.

Зафиксированы success criteria (3 уровня) и primary failure mode
(infrastructure gravity trap), плюс операциональные принципы:
aggression в memory, trust как non-negotiable, privacy, hardware
target, author-as-first-user, qualitative > quantitative.

Документ помечен как living: изменения thesis — через ADR, не молча
в фичевом PR.

POSITIONING.md, README.md, README.ru.md — обновлены ссылками на THESIS.md.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PageoutChain теперь хранит кумулятивные счётчики per-стратегия (attempted/
succeeded/failed). Без них непонятно, реально ли работает jetsam/machVM
на конкретной машине.

VortexCore / Pageout
- Новый PageoutCounters (Codable, Sendable, Equatable) — 9 полей
  (по 3 на каждую стратегию).
- PageoutChain.counters bump-ается в pageout(pid:): perстратегии
  attempted перед вызовом impl, потом succeeded или failed по результату.
- PageoutChain.currentCounters() — async getter.

VortexCore / VortexFreezing
- Новый требование `pageoutCounters() async -> PageoutCounters?`.
- Default-extension возвращает nil (для тестовых стабов).
- VortexActor реализует через `pageout?.currentCounters()`.

VortexCore / VortexCoordinator
- PressureSnapshot гainедлся pageoutCounters: PageoutCounters?
- pressureSnapshot() читает их у vortex.

IPC / FroggyDaemon
- IPCResponse.pageoutCounters: PageoutCounters?
- handle "pressure" возвращает counters.

Tests (+3, 111 total)
- PageoutChainTests: testCountersTrackSuccess (счёт успеха),
  testCountersTrackFallback (fallback chain считает обе стратегии),
  testCountersAccumulate (counters кумулятивны на 3 pageout'а).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
…#18)

Первый design-doc для Trust Governance. Покрывает только сигнал-слой
и confidence-скоринг — decision-логика и explainability в отдельных
документах позже.

Что зафиксировано:

- Где сидит в стеке: новый actor `ActivityDetector` в VortexCore,
  параллельно `MemoryPressureMonitor`. Coordinator дёргает его
  синхронно на каждый кандидат-pid в момент freeze-решения.
- 9 сигналов с разными источниками и весами: frontmost (hard veto
  при 1.0), audio/camera через CoreAudio/CoreMediaIO HAL,
  recent-input через AX, media-playing через MediaRemote (private,
  с feature-detection fallback), network/fullscreen/recent-frontmost/
  cpu-burst как тай-брейкеры.
- Асимметричный fail-safe: false positive (не заморозили idle-app)
  дешёво, false negative (заморозили активный звонок) — катастрофа,
  поэтому веса смещены в сторону «лучше не трогать».
- Confidence-thresholds per pressure-tier: warning=0.3 для tier-1,
  critical=0.5/0.4 для tier-1/tier-2.
- API: protocol-driven (ActivitySignalSource), полная testability
  через fake-источники, как в Mem-1/Mem-2.
- Privacy guard'ы: AX наблюдает только timestamps событий, MediaRemote
  отдаёт только pid (никогда — track info).
- Implementation phasing AD-1 .. AD-5, чтобы не получился 1500-строчный
  PR. AD-1 (frontmost + recent-frontmost) уже закрывает главную дыру:
  freeze frontmost-app становится невозможным.

Связь с THESIS: явно проговорено что activity detection — qualitative
substrate (enables новый класс), а не quantitative (faster on N%), и
что этот слой — *первая user-visible capability* trust layer'а, а не
«infrastructure before capability». Это якорь против gravity trap'а.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Второй design-doc для Trust Governance. Описывает decision-логику
между ActivityDetector (signal layer) и Vortex.freeze (action layer).

Что зафиксировано:

- FreezePolicyEngine как новый actor в VortexCore. Coordinator
  становится тонким — реактивно дёргает engine на каждый кандидат,
  применяет результат.
- AppFreezeState per-bundle-id (не per-pid): cooldown, sliding
  60-min freeze budget, currentlyFrozenSince, consecutive count.
  Sliding window вместо hour-bucket — нет cliff-effects.
- Decision flow: exclusion → override → cooldown → budget →
  activity confidence → threshold compare. Trace накапливается
  пошагово, fail-closed на каждом этапе.
- 6 auto-thaw triggers с приоритетами; «external activity detected»
  как trust-canary (если сработал — наш upstream activity score
  был неправильным).
- Defaults с осмысленными числами (15 min budget, 1 min cooldown,
  15 min max duration, 10 min rest period) и инвариантом
  rest < cooldown < maxDuration < budget.
- API protocol-driven (`FreezeStateStore`, injectable Clock,
  `liveDecisions()` AsyncStream для menubar).
- Persistence через SQLite — пережить рестарт демона без потери
  доверия. Crash recovery: orphan freeze из frozen.pids → force-thaw,
  состояние восстановлено.
- Implementation phasing FCP-1..FCP-7. FCP-1+FCP-2 — минимально
  жизнеспособный trust governance: responsive + non-spammy.
- 4 open questions, главный — что делать когда catastrophic pressure
  и все кандидаты выше threshold. Кандидат: жертвовать MLX worker'ом
  до freeze'а активных юзеровских apps.

Связь с THESIS: явно проговорена тройка
(activity detection → policy → explainability) как
«trust layer is itself a capability». Этот документ — средний член.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Третий и финальный design-doc для Trust Governance. Закрывает тройку
(activity detection → policy → explainability). Это presentation
layer — нулевая business-логика, только показывает что произошло
выше по стеку.

Что зафиксировано:

- 4 информационных слоя (L1 glance / L2 status / L3 per-app /
  L4 trace) с прогрессивной детализацией. User pays attention
  proportional to context: L1 за 0.5s, L4 для bug-report.
- Критическое правило: «no string interpolation that introduces
  information not present in the trace». Если trace это говорит —
  показываем; если не говорит — не выдумываем. Это якорь от drift'а
  между объяснением и реальностью.
- L1 icon states: idle / active / managing / critical / anomaly.
- Notification rules: silent operation by default. Уведомления
  только на 4 события: critical+stuck / budget exhausted /
  decision failed / canary triggered.
- Tone rules: specific not vague, honest about uncertainty,
  no jargon at L1-L3.
- Push-based via AsyncStream, no polling. Reconnect-pill при
  потере связи с демоном.
- IPC additions: `decisions`, `decision <id>`, `decisionsLive`
  для menubar и для bug-reports.
- Localization: English first, structured templates с phase 2
  Russian когда появится appetite.
- 6 implementation phases EXP-1..EXP-6. EXP-1+EXP-2+EXP-3 = minimum
  viable trust UX.

Связь с THESIS: явно проговорено что этот слой —
*proof* что activity detection + policy были worth building, и
explicit resist gravity trap: the day EXP-3 ships, Уровень 1.5
has shipped a user-facing feature. После этого следующее
решение — не «ещё substrate», а «какая qualitative capability
сверху первой».

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Закрытие двух объективных пробелов в документации после того как
репо стал public + MIT + discoverable через 19 topics + v0.1.0
release.

CONTRIBUTING.md (короткий, ~80 строк):
- Перед issue/PR — POSITIONING + THESIS как фильтр.
- Architectural changes идут через ADR в составе PR.
- Новые компоненты — design-doc первым, имплементация второй.
- Cross-reference на ADR-0009 (отдельный PR) о том что design-doc'и
  не гонятся вперёд имплементации.
- Code conventions кратко (Swift 6 strict, ExistentialAny, OS-syscall
  через wrapper, без новых deps).
- Формат PR на примере существующих.
- Code of conduct: don't be a jerk.

SECURITY.md (короткий, ~70 строк):
- Reporting — Telegram, не публичный issue.
- Threat model: local non-adversarial user.
- Out of scope явно: malicious local user, exposed IPC, supply
  chain, side channels, untrusted MLX models.
- Sensitive surface areas: где смотреть при аудите (Redactor, IPC,
  Pageout, entitlements, frozen.pids).
- Privacy notes: screen capture in-memory только, redaction перед
  диском, ничего не уходит за пределы машины.
- Known limitations: regex-redaction incomplete by design,
  task_for_pid-allow в публичной поставке не работает.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Структурный антидот от documentation gravity trap'а — специфической
разновидности infrastructure gravity trap из THESIS, применённой к
документации.

Контекст: к моменту этого ADR в docs/design/ уже три полных
design-doc'а на ~1200 строк forward-looking спецификации для кода,
которого ещё не существует (ActivityDetector, FreezePolicyEngine,
extensions FroggyMenuBar). Соотношение doc/code для этой фазы —
бесконечность. Это локальный warning sign, и без явного правила
соблазн «давай ещё один полезный документ» будет brut force'ить
структуру каждый раз.

Решение: после того как design-doc для слоя N написан, следующий
новый design-doc принимается только если хотя бы один
имплементационный PR для слоя N уже смерджен. Update'ы и пост-фактум
ADR'ы разрешены всегда — они не forward-looking.

Альтернативные формулировки рассмотрены и отвергнуты:
- mechanical doc/code ratio — ломается на естественных перекосах
- design-doc только в составе PR с кодом — слишком ограничительно
- time-based ограничение — хрупкое и обходится

Выбранное правило структурное и proof-of-progress'ное:
требует чтобы предыдущий слой физически работал до открытия
проектной фазы следующего. Нельзя сфальсифицировать.

Cross-reference добавлен в THESIS.md operating-principles
(точнее, в mitigations секцию gravity trap'а — где он
концептуально и принадлежит).

Текущие три design-doc'а Уровня 1.5 правило не нарушают —
они uno momento покрывают один связный слой. Следующий
design-doc принимается после AD-1 + FCP-1 + EXP-1 в main.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Сбор данных для будущего ranking-overlay (PR через неделю-две, когда
данные накопятся). Этот PR ничего не меняет в политике freeze.

VortexCore / FreezeStatsStore (новый)
- Persistent SQLite-БД в ~/Library/Application Support/Froggy/
  freeze_stats.sqlite (mode 0600).
- Через системный sqlite3 C-API (`import SQLite3` +
  .linkedLibrary("sqlite3") в Package.swift) — без новых SwiftPM-deps.
- Schema v1: events(id, ts, bundle_id, pid, rss_before, rss_after,
  pageout_strategy, recovery_ms) + индексы по bundle_id и ts.
- PRAGMA user_version для миграций.
- API: record(event), topByMedianFreed(limit, daysBack), count, clear.
- Открытие/миграция вынесены в openAndMigrate() (отдельно от init,
  потому что actor-init не async).
- Sendable AggregatedStats для IPC.

VortexCore / ProcessRusage (новый)
- Тонкая обёртка над BSD proc_pid_rusage (RUSAGE_INFO_V4) — для
  снятия RSS у живого pid.

VortexCore / FreezeRanker (новый)
- recordFreeze(pid, bundleId, strategy): снимает RSS до, через 5с —
  RSS после, пишет в БД.
- recordThaw(pid, bundleId): поллит RSS pid'а с шагом 100мс, фиксирует
  recovery_ms по первому |Δ| > 1 MB. Таймаут 5с.
- rssReader инжектируется — тесты подменяют на mock без реальных pids.

VortexCore / VortexActor
- init теперь принимает (..., ranker: FreezeRanker?). На freeze после
  pageout — recordFreeze. На thaw — recordThaw. Если ranker == nil —
  поведение прежнее.

VortexCore / Config
- freezeRankingEnabled: Bool (default false). Опт-ин телеметрии.
- Custom init(from:) грузит старые config.json без изменений.

IPC / FroggyDaemon
- IPCResponse.freezeStats: [AggregatedStats]?.
- Новая IPC-команда "freezeStats" — отдаёт топ-N bundle_id по медиане
  rss_before-rss_after за 7 дней. Если телеметрия off — failure
  с пояснением.
- main.swift: создаёт store + ranker когда freezeRankingEnabled=true,
  пробрасывает в VortexActor и DaemonIPCHandler.

Tests (+6, на этой ветке локально все зелёные через --filter)
- FreezeStatsStoreTests (6): open+migrate, record + count,
  topByMedianFreed (Heavy.app > Light.app), persist через reopen,
  clear, cutoff по daysBack (старые события игнорируются).
- Полный prod-test path через VortexActor.freeze + ranker — отложен
  на ranking-overlay PR (нужны realistic pid'ы).

Docs
- ADR 0010-profile-guided-freeze.md.
- README — отдельным fix'ом перед merge'ом.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Зафиксировано:
- Mem-3.1 + Mem-4 (Worktree A) — ждёт kill зависших test-процессов и
  затем merge с обновлённым unloadModel.
- Mem-5 этап 2 (ranking-overlay) — через ~неделю, когда телеметрия
  накопит данные.
- Этап 1 (bench baseline) — gate; делается пользователем на живой
  машине, не в моей среде.
- Уровень 2 (ROI OCR, persona, voice, etc.) — не трогать.
- Хвосты: /security-review на Mem-5, /simplify на supervisor, CI activation,
  hooks подхватятся следующей сессией.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Worktree A нового плана. Cherry-pick локального Mem-4 поверх свежего
main + добавлен FroggyMLXWorkerFake + интеграционные тесты supervisor↔
worker через настоящий pipe (то, что было отложено в Mem-3.1).

FroggyMLXWorker — kv-cache квантизация
- CLI-флаг --kv-bits 4|8|16, default 8 (16 → kvBits=nil, без квантизации).
- Per-request override через MLXWorkerCommand.kvBits.
- Используем публичный mlx-swift-lm GenerateParameters(kvBits:) — без
  ручной MLXArray.asType квантизации.

VortexCore / MLXSupervisor
- init теперь принимает kvCacheBits + extraArgs. KV-bits и extra-args
  передаются worker'у через Process.arguments при posix_spawn.
- currentKVCacheBits() — для IPC status.
- **fix unloadModel**: была хитрая `withTaskGroup` + `AsyncThrowingStream`
  логика — стрим без `goodbye` событий не финишировался, ветка
  `for try await` висла навсегда после `cancelAll()`. Заменено на
  прямой polling `process.isRunning` (30 × 100ms = 3s timeout) → SIGKILL.

VortexCore / Config
- kvCacheBits: Int = 8. Custom init(from:) с decodeIfPresent для
  обратной совместимости.

IPC
- IPCResponse.kvCacheBits: Int? — отдаётся в `status`.

FroggyMLXWorkerFake (новый executable target)
- Тестовый-двойник worker'а: Swift binary, понимающий тот же JSON-line
  протокол, без MLX-зависимостей.
- Non-blocking чтение stdin через FileHandle.readabilityHandler — это
  закрывает баг python-stub'а, который висел на блокирующем
  `for line in sys.stdin`.
- CLI режимы: --mode happy (default), ignore-shutdown, crash-on-generate.

Tests (+5 integration + 4 KV-cache config = +9, 120 total, 1 skipped)
- MLXSupervisorIntegrationTests (5):
  * testHappyPathLoadAndUnload — load → ready → unload → process gone.
  * testGenerateStreamsChunks — fake шлёт 5 chunks, supervisor
    собирает их и завершается.
  * testRapidLoadUnloadDoesNotHang — 10 циклов load/unload без
    зависания.
  * testShutdownTimeoutForcesSIGKILL — fake --mode ignore-shutdown,
    supervisor ждёт 3с polling и SIGKILL'ит. **Этот тест без fix'a
    unloadModel висел в xctest на 47 минут — теперь укладывается
    в 3.26s.**
  * testWorkerCrashYieldsContinuationError — fake --mode
    crash-on-generate, AsyncThrowingStream получает .workerCrashed,
    isLoaded → false.
- KVCacheConfigTests (4): default = 8, round-trip 16/8/4, legacy
  config без поля → default, supervisor читает kvCacheBits.

Docs
- ADR 0009 kv-cache-quantization.md.
- README: новый bullet про KV-cache, kvCacheBits в config-примере.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
ADR 0011-code-first-design-second-for-level-2.md
- Формальный stop-сигнал: design-doc'и Уровня 2 (voice/VLM/persona/
  router/Takeout-ingest) заблокированы до AD-1+FCP-1+EXP-1 в main.
- «Заблокированы» — конкретные правила: не открывать ADR 0012+ под
  Уровень 2; не писать design RFC; не оставлять заглушек в Sources/;
  не создавать SwiftPM-target'ов под voice/VLM.
- Дополнительный gate перед AD-1: bench/baseline.json в main + честная
  проверка цифр. Если pageout-counters show succeeded=0 на jetsam, или
  secondsInLevel не выходит за .normal под нагрузкой — остановиться,
  не идти в AD-1.
- Что считается «AD-1/FCP-1/EXP-1 завершены» — критерии прописаны.
- Mitigation: правило отменяется отдельным ADR, не молчаливый bypass.
- Note про нумерацию: пользователь называет это «ADR-0009»; в этом
  репо 0009 уже занят KV-cache, поэтому 0011.

TODO.md
- Новая секция «Validation gate (блокирует всё)» с командами
  /froggy-bench × 3 сценария + критерии «остановиться если…».
- Новая секция «Уровень 1.5 (после baseline-bench)» — AD-1/FCP-1/EXP-1
  с критериями завершения.
- Уровень 2 секция переписана: «заблокирован до AD-1+FCP-1+EXP-1 в
  main, см. ADR 0011».
- Mem-3.1+Mem-4 помечен как закрытый (#26).

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Update'ы существующих design-doc'ов — ADR-0011 (он же ADR-0009 в
старом дубликате) такие правки разрешает явно: «update'ы существующих
design-doc'ов разрешены всегда — это не forward-looking активность,
а синхронизация спецификации с реальностью имплементации».

## explainability-menubar.md

Per-app inline actions в L3 row:
- `[thaw]` — immediate SIGCONT для конкретного pid (не addAll). Не
  меняет exclusion list — one-shot override.
- `[never freeze]` — добавляет bundleId в FroggyConfig.freezeExclusion
  и триггерит немедленный thaw. Trust-recovery механизм после плохого
  freeze'а (когда юзер во фрустрации не пойдёт редактировать JSON и
  рестартить демона).

Скорректирован non-goal про «not a control panel»: action разрешён
inline если его смысл однозначен из соседней explanation. Абстрактные
controls по-прежнему мимо.

API additions: новые IPC команды `thaw <pid>`, `addExclusion <bundleId>`,
`removeExclusion <bundleId>`. `thaw <pid>` валидирует, что pid
действительно frozen Froggy'ем — нельзя escalate'ить через эту команду.

## freeze-confidence-policy.md

Новая секция «Editing exclusions and overrides at runtime»:
два эквивалентных пути изменить freezeExclusion / activityConfidenceOverride
— через config.json (стабильный, scriptable) и через menubar IPC
(trust-recovery после плохого freeze'а). Atomic write для persistence.

## TODO.md

Зерна из Grok-обзора, которые не нарушают ADR-0011:
- VortexCoordinator responsibility split — defer до следующего касания
  Coordinator'а, не делать ради рефакторинга.
- Pressure-aware model swap pattern — для будущего VLM/voice design'а.
- VLM layout analysis через VNDetect* — промежуточная ступень между
  плоским OCR и полной VLM.
- Apple Speech как TTS-fallback под critical pressure.
- «Hey Froggy» wake word — privacy/battery review prerequisite, не
  делать без отдельного ADR.

Также записан долг ADR-нумерации:
0009-design-docs-after-implementation.md — дубликат 0011, удалить
при следующем касании ADR-инфраструктуры.

Что НЕ интегрировано из Grok'а и почему:
- Proactive Context Agent — scope creep, требует thesis change.
- Tool Use на №4 priority — security blindspot, отдельная фаза с
  permission model + threat model expansion.
- Rest of the priority order — не используем, держим собственный
  по THESIS.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st signing doc (#29)

* bench: scaffolding + 1/3 baseline snapshot (under-pressure)

bench/run.sh — авто-определение сценария (idle / model-loaded /
under-pressure) по pgrep+froggy status+pressureLevel; пишет JSON-snapshot
в bench/baseline.json.

bench/baseline.json — пока 1 snapshot: under-pressure, захвачен реально
(машина была в critical 10+ минут). Подтверждает прогноз ADR 0007:
jetsamAttempted=1 / jetsamFailed=1 (default jetsam без энтайтлмента
EPERM'ит как и ожидалось), scratchAttempted=1 / scratchSucceeded=1
(scratch-fallback реально срабатывает).

bench/README.md — как добить остальные два сценария + что считать
«разумным» по validation-gate из ADR 0011.

idle и model-loaded не захвачены автоматически: idle требует чтобы
давление спало (не моя зона контроля), model-loaded — путь к MLX-модели
на диске. Шаги повторения — в bench/README.md.

Surprise-цифра, заслуживающая внимания до AD-1: daemon_rss_kb=195488
(195 MB без модели) при ожидании ≤70 MB по ADR 0011. Возможные причины:
накопленные DispatchSource'ы, SCStream, SQLite cache, что-то ещё —
требуется отдельный профиль.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* bench v2 + ADR 0012 + ADR 0011 update: distribution-based + any-strategy

Что показал реальный профайл daemon'а через vmmap/heap:

- НЕТ leak'а: CRImageReaderOutput=3816 константно после 10+ мин — Vision
  держит фиксированный pool, не растущий.
- НЕТ Mem-3 регрессии: ни VortexCore/LushaBridge/FroggyDaemon не
  импортируют MLX/Tokenizers/HuggingFace — изоляция чистая.
- RSS под critical pressure'ом — sawtooth 30-150 MB на интервалах ~секунд,
  потому что Vision/SCStream держат IOSurface буферы clean-mapped, и
  kernel под давлением периодически их evict'ит. Это не ошибка — это
  как macOS работает.
- Median RSS на холостом ходу (10 сэмплов × 1s): 117 MB. Min 29 MB,
  max 137 MB. Footprint peak (cumulative dirty + clean) — 328 MB.
- 195 MB из первого bench'а был high-end сэмпл этого sawtooth'a, не
  steady-state. Фиксим через distribution-based capture, а не оптимизацию.

bench/run.sh schema v2:
- 10 сэмплов RSS × 1s интервал → {min, median, max, mean, samples}.
- daemon_rss_kb осталось на верхнем уровне для backward-compat (= median).

bench/README.md:
- Объясняет sawtooth явление и почему single-sample обманчив.
- Threshold обновлён: median ≤ 130 MB (idle/model-loaded без worker'а),
  max ≤ 400 MB. Floor от Vision+SCStream+AppKit (transitive через
  ScreenCaptureKit) неустраним.

ADR 0011 § «Validation gate»:
- Критерий pageout: jetsam-specific → any-strategy. На personal dev
  signing jetsam EPERM'ит, scratch-fallback подхватывает — substrate
  функционально работает.
- Добавлен distribution-based daemon RSS критерий (median > 200 MB —
  регрессия).

ADR 0012 — новый honest-doc:
- Матрица работоспособности pageout-стратегий по уровню подписи
  (personal dev / Apple Dev ID + task-for-pid-allow / SIP off).
- Зафиксировано наблюдение: jetsamFailed=1 в нашей сборке (ADR 0007
  предполагал jetsam без entitlement'ов работает; на практике — EPERM).
- Решение: НЕ блокировать substrate на получении task-for-pid-allow.
  scratch достаточен, capability-фазы не ждут Apple-procedure.

bench/baseline.json — добавлен 3-й snapshot (idle, schema v2,
distribution): median 117 MB, в пределах нового порога. Validation gate
ADR 0011 теперь PASS по всем критериям, кроме «нужен также model-loaded
snapshot» — требует MLX-модель на диске пользователя.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ADR 0013 + cycles tooling: validation gate поймал metallib-регрессию

Попытка захватить model-loaded snapshot (5 циклов load/unload через
IPC по плану «после Mem-3 контракт: worker_rss_kb=null после unload»)
показала: MLX worker умирает на первой реальной операции с

  MLX error: Failed to load the default metallib. library not found
  library not found library not found library not found
    at .build/checkouts/mlx-swift/Source/Cmlx/mlx-c/mlx/c/memory.cpp:78

Проверка: `find .build -name "*.metallib"` — пусто. `swift build`
не компилирует Metal-shader'ы в `default.metallib` (это Xcode-only build
phase), и Cmlx target в mlx-swift Package.swift не объявляет .metal
файлы как resources. SwiftPM-bundle создаётся, но без metallib внутри.

Регрессия не ловилась раньше потому что MLXSupervisorIntegrationTests
используют `FroggyMLXWorkerFake` — Swift bin без `import MLX`.
Real-model loading никогда не запускался end-to-end. Validation gate
ADR 0011 ровно тут и поймал, до того как пошли в AD-1 строить
frontmost-veto на сломанной основе.

Не пытаюсь чинить в составе bench'а (per user plan: «отдельный issue/PR»).

Что вошло в этот PR:

- ADR 0013 — honest-doc регрессии: что ищется, в каком порядке, почему
  не находится, 4 возможных пути фикса (pre-build script + SwiftPM
  resources / параллельный xcodebuild target / binary XCFramework /
  собирать через xcodebuild в .app). Решение откладывается до пост-сессии.

- bench/cycles_test.sh — orchestrator для 5-цикловой gate-проверки
  (loadModel × N → bench → unloadModel → bench, проверка worker_rss_kb=null
  + daemon RSS не растёт). Сейчас не работает из-за metallib, оставлен
  для использования после фикса.

- bench/run.sh — мелкий фикс: scenario auto-detection искал
  `*modelLoaded*yes*` (camelCase) вместо реального `model_loaded     yes`
  (snake_case в froggy status output). Без фикса model-loaded snapshot
  всегда тэгался idle/under-pressure.

- TODO.md — секция «Metallib regression (БЛОКЕР AD-1)» с явной
  блокировкой Уровня 1.5 до фикса.

Substrate жив, изоляция Mem-3 работает — daemon не падает когда worker
не может загрузиться. Просто Уровень 1.5 на паузе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* README/ADR-0013: known-issue notice + upstream-проверка

Upstream-проверка mlx-swift (перед написанием своего fix'а):

* mlx-swift 0.31.3 = latest, bump не поможет.
* Issue #349 (открыт фев'26, ровно наша симптоматика): maintainer
  явно ответил «swiftpm has no mechanism to build the metal shaders ...
  using xcodebuild (or CMake) is a workaround». Официального
  SwiftPM-fix'а не будет.
* Issue #345 (открыт янв'26): без решения.
* PR #313 «MetalCompilerPlugin support» — community-PR, CONFLICTING +
  REVIEW_REQUIRED, без review 5 месяцев, зависит от ml-explore/mlx#2885.
  Не путь.
* Из комментариев #349 — community workaround через SwiftPM
  BuildToolPlugin + локальный copy-metallib скрипт. Это совпадает с
  Path 1 в нашем ADR 0013.

→ ADR 0013 пополнен секцией «Upstream state», с явным выводом, что
fix решать локально.

README + README.ru — known-issue notice в шапке (рядом со «Status»):

  ⚠️ Known issue (2026-05-07): MLX inference сломан в release-сборке
  через `swift build` (нет default.metallib). Substrate работает.
  См. ADR-0013 / mlx-swift#349 / PR #30.

Это для читателя, который найдёт Froggy сегодня и попытается
использовать — упрётся в эту же стену. Honest-flag, не secret.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR 0013 был honest-doc'ом регрессии metallib (default.metallib не
собирается через swift build). Эта PR реализует Path 1: pre-build
script + Makefile + post-build copy.

Содержание:

* scripts/compile-metallib.sh — компилирует 9 metal-kernel'ов из
  mlx-swift checkout'а через xcrun metal/metallib. Список идентичен
  upstream KERNEL_LIST. Флаги (-std=metal3.1, -fno-fast-math) совпадают
  с CMakeLists. ~3.1 MB на выходе. Idempotent.

* Makefile — `make build` (default = release) делает pre-build вызов
  скрипта, потом swift build, потом copy metallib в
  `.build/<config>/Resources/default.metallib`. Это путь 4 в search
  order'е mlx-swift (co-located <binary-dir>/Resources/) — единственный
  reliable способ положить metallib без xcodebuild.

* Tests/MLXWorkerMetallibTests/ — два теста (presence в source-tree +
  reasonable size). Зелёные после `make build`. Ловят регрессию
  pre-build шага без MLX-модели на диске.

* Sources/FroggyMLXWorker/main.swift → Entry.swift — переименовано на
  случай добавления SwiftPM resource declaration в будущем (избегаем
  конфликта @main vs «module containing top-level code»).

* bench/baseline.json — добавлен 4-й snapshot: model-loaded c
  worker_rss_kb_distribution не-null. Закрывает 4-й пункт gate'а.

* README + README.ru — убрана warning-секция «Known issue». Quick start
  обновлён на `make build` вместо `swift build`.

* TODO.md — секция «Metallib regression БЛОКЕР AD-1» → «закрыто».
  Уровень 1.5 (AD-1/FCP-1/EXP-1) разблокирован.

* ADR 0013 — статус Resolved + § «Что фактически сделано» с описанием
  почему SwiftPM resource declaration не подошёл (bundle не попадает в
  NSBundle.allBundles).

Validation gate (ADR 0011) — 4/4:

* pageoutCounters.<any>.succeeded ≥ 1 — ✅ scratchSucceeded=1
* median daemon_rss_kb ≤ 200 MB без модели — ✅ 117 MB (idle), 26 MB (model-loaded)
* unloadModel → worker_rss_kb=null — ✅ 5/5 циклов в bench/cycles_test.sh
* secondsInLevel warning ≥ 1× / 5 мин — ✅ critical 10+ мин в реальной нагрузке

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
froggychips and others added 18 commits May 7, 2026 19:32
* docs: TODO.md — Power-1 (energy/thermal management) как deferred эпик

Тот же скелет, что у memory pressure: PressureSource → VortexCoordinator
→ SIGSTOP, переиспользует VortexFreezing/FrozenPidsStore/FreezeRanker.
Разбор composite-сигналов (thermalState/isLowPowerMode/IOPSCopyPowerSourcesInfo/ri_energy),
caveats (RAM≠power tiers, frontmost дороже, App Nap уже работает) и
validation gate с null-result stop — по аналогии с ADR-0011.
Заблокирован до Уровня 1.5 в main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: TODO.md — Obs-1 (jetsam observer / unified log) + privacy audit + logbundle хвосты

Obs-1 закрывает honest-signal gap из ADR-0011: прямой kernel jetsam
events вместо косвенных pageout counters. Caveat про private-redaction
в prod честно вынесен — dev/baseline-friendly, не user-facing
observability. Два мелких хвоста: privacy audit os_log call-sites
(префикс к Obs-1, прежде чем читать unified log — перестать в него
лить чужое) и make logbundle (log collect для bug reports).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: TODO.md — API-ресерч macOS: AD-1 scope, MetricKit в Obs-1, 3 хвоста, зерна

* AD-1 description: явный выбор minimal (NSWorkspace) vs extended
  (Accessibility API + typing-veto) — design-decision для ADR.
* Obs-1: добавлен MetricKit `MXAppExitMetric` как complement к
  log_stream — Apple-blessed prod ground-truth без developer-mode.
* Меньшие хвосты +3: NSWorkspace notifications (замена polling'у в
  ProcessFinder + termination cleanup), DispatchSource process exit
  для MLXSupervisor (закрывает race из Mem-3.1), OSSignposter
  инструментация перед FCP-1.
* Новая секция «Зерна из API-ресерча» (NSCache, SMAppService,
  UserNotifications, NaturalLanguage, FSEvents) + явный «не для нас»
  список (ESF, XPC, CGEventTap, SwiftData) чтобы не возвращаться.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub-hosted macOS-runner'ы на этом аккаунте выдают 95/95 startup_failure
(0 успешных билдов за всю историю репо, см. /actions). Это блокирует CI
независимо от кода. Self-hosted даёт зелёный CI без зависимости от
GitHub-billing/runner-pool.

* runs-on: [self-hosted, macOS, ARM64] — на твой Mac, требует
  зарегистрированный self-hosted runner с дефолтными labels.
* clean: false — сохраняем .build/checkouts/mlx-swift между запусками
  (persistent runner; экономит ~minutes на git-clone зависимостей).
* concurrency cancel-in-progress — single runner, нет смысла очередить
  параллельные сборки одной ветки.
* make release / make test — вся логика (resolve → metallib pre-build →
  swift build → post-build copy) уже инкапсулирована в Makefile из #31.

Старый ci.yml (macos-latest) оставлен — если billing-причину когда-нибудь
найдём и поправим, GitHub-hosted CI вернётся в строй сам по себе.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(adr): resolve 0009 collision — rename design-docs ADR → 0014

В docs/adr/ одновременно лежали два файла под номером 0009: первым
(PR #22) был добавлен design-docs principle, позже (PR #26) — KV-cache
квантизация на тот же номер. Index в adr/README.md показывал только
KV-cache, design-docs ADR висел orphaned. Note в ADR 0011 про
"Содержание идентично" ссылается на личные заметки автора, а не на
0009-design-docs — но был неправильно прочитан и попал в TODO как
"дубликат, удалить".

Реально это два разных ADR: 0014 — общий принцип (load-bearing из
THESIS/CONTRIBUTING), 0011 — конкретный gate Уровня 2.
Resolution: переименовать design-docs → 0014 (KV-cache остаётся 0009,
README.md L66 user-facing reference не трогается). Note в 0011
переписан, чтобы не путал. Index в adr/README.md дополнен 0012/0013/0014.

* docs/adr/0009-design-docs-after-implementation.md → 0014- (git mv)
* 0014: title 0009→0014, добавлена история нумерации и cross-link на 0011
* 0011: «Примечание о нумерации» переписано — явно не дубликат 0014
* docs/adr/README.md: добавлены 0012/0013/0014 в индекс
* docs/THESIS.md L121: путь 0009→0014
* CONTRIBUTING.md L42: путь 0009→0014
* TODO.md: удалён misdiagnosed «Долг ADR-нумерации» — collision закрыт

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: README sync (RU↔EN, workflows) + TODO startup_failure cleanup

* README.md L101: workflow path обновлён — теперь два workflow,
  primary self-hosted (PR #33).
* README.ru.md приведён в соответствие с английской версией:
  - Project layout: добавлен FroggyMLXWorker/, расширен список
    VortexCore (MLXSupervisor, MemoryPressureMonitor, PageoutChain),
    Tests «63 теста» → «100+ тестов», ADR строка обобщена,
    workflows path синхронизирован с README.md.
  - Quick start: `swift build -c release` → `make build` с пояснением
    про metallib pre-build (ADR-0013), `swift run FroggyDaemon` →
    `.build/release/FroggyDaemon`.
  - Features: добавлен раздел про KV-cache квантизацию (отсутствовал).
  - Configuration JSON: добавлен `kvCacheBits` (был только в EN).
  - Documentation footer: расширен список ключевых решений.
* TODO.md: удалён хвост «CI workflow startup_failure» — закрыт PR #33.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#35)

Новый эпик параллельный Power-1 / Obs-1: вместо того чтобы наши
ContextStore/FrameDigest/Vision staging уходили в compressor/swapfile
под pressure'ом — помечать как VM_PURGABLE_VOLATILE / MADV_FREE_REUSABLE,
kernel дискардит без swap I/O. Сильнее PageoutChain'а для своих
данных, не требует entitlement'ов, работает поверх current
MemoryPressureMonitor без изменений.

Поглощает ранее существовавший Уровень-2 entry «File cache flush через
purgeable API» — удалён оттуда.

Caveats обязательная часть ADR: не drop-in (markNonVolatile перед
чтением), used-after-reclaim bug class, page-granularity, win
стремится к нулю на 16+ GB без давления, artificially-pressure'd
тесты обязательны. Validation gate с null-result stop при saving <
50 MB на 8 GB сценарии — тот же паттерн ADR-0011.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Зачем: чтобы внешний пользователь мог одной командой собрать
unified-log архив с правильным предикатом (`subsystem ==
"com.froggychips.froggy"`) и приложить его к issue. Без обёртки
предикат легко опечатать в комментарии, а `log collect` без
`--predicate` тащит десятки MB системного лога с приватными
данными других приложений.

Что:
* `scripts/logbundle.sh` — bash-обёртка вокруг `log collect`.
  Поддерживает `-o <output_path>` (default: `./froggy.logarchive`)
  и `--last <duration>` (1h, 30m, ...). Sanity-check на наличие
  `log` в PATH, понятные ошибки на невалидные аргументы. Печатает
  финальный путь и размер (через `du -sh`, т.к. `.logarchive` это
  bundle-директория, не файл). Файл с исполняемым битом через
  `git update-index --chmod=+x`.
* `Makefile` — target `logbundle` (в `.PHONY`), вызывает скрипт
  с дефолтными аргументами. Строчка в `help`. Передавать args
  в make неудобно — для `--last 1h` запускать скрипт напрямую,
  это задокументировано в комментарии в Makefile.
* `README.md` и `README.ru.md` — однопараграфная секция
  "Troubleshooting" между "Installing as a LaunchAgent" и
  "Documentation" / "Документация".

Без entitlement'ов, без Swift-кода — задача из TODO.md → «Меньшие
хвосты» → `make logbundle`.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…it) (#37)

Зачем
=====
В `MLXSupervisor.unloadModel` graceful shutdown ждал exit'а worker-процесса
polling'ом `process.isRunning` с шагом 100мс (до 30 итераций). Это работало,
но имело несколько проблем:

* Race-условие между чтением `p.isRunning` и `kill()`. Если процесс exit'нулся
  ровно между этими двумя точками, мы могли отправлять SIGKILL уже зомби'ю
  (no-op, но шумно в логах) либо наоборот — promote'нуть `!p.isRunning`
  слишком рано до того, как Process отработал terminationHandler.
* Polling-латентность: «happy path» exit (worker отвечает goodbye за 1мс)
  всё равно ждал хотя бы 100мс до следующей tick'а polling'а.
* `terminationHandler` от старого процесса мог приехать в actor queue
  ПОСЛЕ того, как `loadModel` уже spawn'нул новый worker — старый
  `cleanup(reason: "exit")` гасил pendingRequests нового процесса
  ошибкой `.workerCrashed`. На polling-варианте баг был замаскирован
  тем, что polling-loop сам await'ил per-tick'е и actor успевал
  обрабатывать termination handler ДО того, как loadModel'у второй
  итерации удавалось submit'нуть новый request. На kqueue-варианте
  гонка стала реальной (тест `testRapidLoadUnloadDoesNotHang` падал
  стабильно).

Что
===
* `unloadModel` теперь использует `DispatchSource.makeProcessSource(.exit)` —
  kernel-level kqueue NOTE_EXIT. Continuation резолвится в момент реального
  exit'а pid'а, без timer'ов.
* Race-guard на старте: после `src.activate()` проверяем `proc.isRunning`
  ещё раз. Если процесс уже exit'нулся ДО регистрации kqueue — NOTE_EXIT
  не придёт, и без этой ручной проверки мы бы повисли до timeout'а.
* Timeout deterministic: `queue.asyncAfter` ставит cancel + resolve(false).
  `OneShotResolver` (NSLock-guarded) гарантирует, что кто бы ни сработал
  первым — event-handler или timeout — `cont.resume` будет вызван ровно
  один раз (двойной resume `CheckedContinuation` — runtime-trap).
* Если timeout сработал — отправляем SIGKILL и делаем второй waitForExit
  (1с timeout) для reap'а zombie'я и срабатывания terminationHandler'а.
* `handleWorkerExit(pid:status:)` теперь принимает pid и игнорирует
  termination события от уже не текущего процесса (`process?.pid != pid`).
  Закрывает race описанный выше — старый terminationHandler не топчет
  pendingRequests нового worker'а.

Тесты
=====
* `testHappyPathLoadAndUnload` — pass (0.13с, было ~3с с polling'ом).
* `testGenerateStreamsChunks` — pass.
* `testShutdownTimeoutForcesSIGKILL` — pass за ~3.07с (timeout 3с + epsilon).
* `testWorkerCrashYieldsContinuationError` — pass.
* `testRapidLoadUnloadDoesNotHang` — pass (10 циклов load/unload, race-guard
  справляется).
* `testWorkerNotFoundIsExplicitError` — pass.
* Полный VortexCore suite (126 тестов) — pass.

Что осталось
============
В TODO.md упоминалось также применить reactive exit-watch к frozen-pid
watcher'ам (non-app helpers, Electron renderers) — это отдельный PR,
пересекается с параллельной работой по NSWorkspace notifications. В этом
PR — только MLXSupervisor.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Зачем: polling `NSWorkspace.shared.runningApplications` на каждый
applyPolicy стоит main-actor хоп + линейное сканирование, но главное —
он пропускает «pid вышел» между опросами. Когда замороженный pid
убивают извне (Activity Monitor, OOM-kill, jetsam), запись в
FrozenPidsStore остаётся жить вечно: на следующем boot recover()
шлёт SIGCONT мёртвому pid'у, ESRCH игнорируется, но мусор копится.
Reactive-подписка на NSWorkspace notifications закрывает race-окно
между polling-cycle'ами и даёт O(1) lookup в bundleId→pids карте.

Что:
* `WorkspaceEventSource` protocol + `RealWorkspaceEventSource`
  (поверх NSWorkspace.shared.notificationCenter) +
  `FakeWorkspaceEventSource` для тестов. Один общий enum
  `WorkspaceEvent` чтобы не плодить N подписок.
* `ReactiveProcessFinder` — actor с in-memory картой bundleId→pids,
  сидится один раз через runningApplications(), дальше живёт по
  событиям. Реализует существующий `ProcessFinder` protocol —
  callers (VortexCoordinator) не меняются.
* `WorkspaceTerminationWatcher` — на каждый appTerminated чистит
  FrozenPidsStore и зовёт hook на координаторе (он убирает pid из
  своих in-memory tier-set'ов).
* `VortexCoordinator` теперь опционально подписан на
  WorkspaceEventSource: willSleep → emergencyThaw + sleep-gate
  (policy не морозит во время сна), didWake снимает gate.
  Реализует `WorkspaceTerminationWatcher.Sink`.
* `FroggyDaemon/main.swift` — wiring: один RealWorkspaceEventSource на
  finder + coordinator + termination-watcher + screen-gate task,
  который stop/start'ит VisionActor по screensDidSleep/Wake.

Тесты: 14 новых тестов в трёх файлах. Главные сценарии —
* frozen pid убили извне → удалён из FrozenPidsStore и из
  in-memory tier-set'а координатора (end-to-end через watcher);
* reactive-finder корректно отвечает после activate/terminate
  и без явного start();
* willSleep делает emergency thaw и блокирует policy;
* didWake снимает gate.

`NSWorkspaceProcessFinder` polling-вариант сохранён для совместимости
и как fallback. Старый `VortexCoordinatorPolicyTests` не тронут.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…proc хвосты (#39)

* MLX-LM-1 — inference config + advanced features audit. Sampling
  parameters в IPC, flash-attention status check, MLX.Memory.reclaim,
  chat template integration test, speculative decoding ROI на 8 GB.
  Hygiene-аудит current MLX path; null-result outcome — успешный.
* RFC-Foundation-Models-Path — закладка для архитектурного решения
  между закрытием Уровня 1.5 и стартом Уровня 2 design'а. Apple
  FoundationModels framework (macOS 26+, M-series, Apple
  Intelligence) — потенциальный второй inference-путь, не drop-in
  замена MLX. Без exploration перед Уровнем 2 — риск год тратить
  substrate-cycles на проблему, которую Apple предоставила.
* Зерна из API-ресерча +2: VNGenerateImageFeaturePrintRequest как
  замена FrameDigest, VNClassifyImageRequest как pre-OCR router.
* Меньшие хвосты +3: Mach exception ports для self-crash forensics,
  os_proc_available_memory в VortexActor, actions/checkout@v4
  Node.js 20 deprecation (Q3 2026).

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sion анализа (#41)

Зачем: после 3-4 часов реальной работы с Froggy данные накопились в
нескольких источниках (unified log + SQLite freeze_stats + frozen.pids
+ config.json + IPC live state + bench), но собирать вручную в один
tarball неудобно — шесть отдельных команд легко забыть. Скрипт делает
single-shot snapshot-сбор для closing validation gate ADR-0011, AD-1
scope decision и UX-debt list'а.

Что:
* `scripts/session-summary.sh` — bash-скрипт, аггрегирует:
  1. log.logarchive (через scripts/logbundle.sh, --last 1h по умолчанию)
  2. freeze_events.tsv — SQLite дамп таблицы events
  3. frozen_pids.txt + config.snapshot.json — state файлы
  4. system.txt — vm_stat + memory_pressure + uname snapshot
  5. ipc/status.json + pressure.json + accessors.json — IPC live snapshots
     если демон запущен, иначе DAEMON_DOWN.txt
  6. notes.md — заглушка-шаблон для ручных пометок (timeline,
     embarrassing freeze events, THESIS criterion #2 check)
  7. MANIFEST.txt — что собрано, что пропущено и почему
  Args: -o <dir>, --last <duration>, --no-tar. Best-effort на каждом
  шаге — отсутствие artifact'а не валит сбор. По умолчанию tarball'ит
  результат и удаляет директорию.
* `Makefile` — target `session-summary` (.PHONY), добавлен в help.
* `README.md` и `README.ru.md` — параграф в "Troubleshooting".

Не блокирует AD-1 / FCP-1 / EXP-1, не требует Swift-кода. Утилита
для самой важной активности проекта прямо сейчас — реальная сессия
использования substrate'а на живой нагрузке.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…est (#40)

Зачем
=====
Перед FCP-1 нужен dev-tool для визуализации frame-budget'а, freeze-cycle
длительности и MLX/IPC латентности в Instruments. Без signposts в hot
paths приходится либо инструментировать собственным timing-кодом
(который потом надо чистить), либо запускать `xctrace` без custom
events — track'и тогда показывают только generic CPU/Allocations,
без бизнес-логики.

Категория `PointsOfInterest` — magic-string, по которой Instruments
автоматически визуализирует signposts в одноимённом track'е без
ручной конфигурации `.instrpkg`. Subsystem остаётся унифицированным
`com.froggychips.froggy`, как у существующих `Logger`/`OSSignposter`-ов.

В release-сборке `-O` без `--instruments-runtime` все signpost-вызовы
компилируются в no-op'ы, поэтому overhead на production-пути нулевой.
Дополнительные данные в metadata (frame_id, ocr_chars, pressure_level,
chunk_count, cmd) минимальны — не раздуваем payload, но даём enough
context чтобы видеть конкретный кадр / запрос в Instruments timeline.

Что
===
* `LushaBridge/VisionActor.swift` — добавлен POI signposter и interval
  `frame_pipeline` в `runCycle()`. Покрывает весь pipeline от
  `screenStream.latestFrame()` через digest → ocr → redact → ContextStore.
  Metadata: frame_id (монотонный счётчик), ocr_chars, skipped (1 если
  пропустили из-за отсутствия кадра либо frame-diff'а). Существующий
  `vision`-category interval `captureCycle` сохранён — POI канал
  параллельный, не миграция.
* `VortexCore/VortexCoordinator.swift` — POI signposter и interval
  `freeze_cycle` в `applyPolicy(_:)`. Один interval per pressure-event,
  metadata `pressure_level=normal|warning|critical` + tier1/tier2 size
  на end. На `.normal` interval короткий (только cancel'ит thawTask),
  основная work летит в детач'е — это OK для observability, видим
  reaction-latency, не cleanup-task duration.
* `VortexCore/MLXSupervisor.swift` — три отдельных POI interval'а:
  - `mlx_load` — от `loadModel()` entry до return (covers ensureWorkerSpawned
    + первый IPC ack). Metadata: model_path.
  - `mlx_unload` — от shutdown-команды до full reap'а. Metadata: pid,
    graceful (1 если worker exit'нулся за timeout, 0 если SIGKILL).
  - `mlx_generate` — внутри `runGenerate`, от prompt-write до final-token.
    Metadata: max_tokens, prompt_chars (на begin), chunks (на end).
  Существующий `mlx-supervisor`-category `mlx.load` interval сохранён.
* `VortexCore/IPCServer.swift` — POI signposter и interval `ipc_request`
  в `processLine`. Покрывает roundtrip от parse'а до response-write
  (включая streaming до final-chunk'а). Metadata: cmd.

Тесты
=====
Signposts не имеют функционального эффекта, поэтому новых тестов нет.
Существующий `swift test --parallel` остаётся зелёным (142 теста,
1 skipped — MLXWorkerMetallibPresence в sandbox без xcrun metal,
known issue из ADR 0013, не блокер).

Что осталось
============
* `bench/run.sh` интеграция с `xctrace record --template ...` — упомянуто
  в TODO как parallel possibility, отдельный PR.
* Migrate существующих legacy-style категорий (`vision`/`coordinator`/
  `mlx-supervisor`) под унифицированный subsystem-only подход — отдельная
  задача, чтобы не смешивать с PoI-инструментацией.
* Дополнительные точки instrumentation'а (ScreenStream `captureFrame`,
  IPCClient `send`) — минимальный scope этого PR ограничен 4 hot path'ами.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ivate (#42)

Аудит всех call-sites os.Logger в Sources/ — 4 строки в 2 файлах
писали error.localizedDescription без privacy-маркера, попадая в
unified system log как .public:

- VisionActor: screen stream / Vision / state write — error может
  содержать display-имя или путь под /Users/.../FroggyState/...
- FrozenPidsStore: write error на frozen.pids — путь под /Users/...

Все остальные call-sites уже корректны: либо помечены явно
(modelPath/dbPath/ipcSocketPath → .public как осознанное решение,
reason/level.rawValue → .public для енумов), либо интерполируют
числовые величины (pid/errno/count/status/Hz) — для них default
.public безопасен и адекватен.

Префикс к Obs-1: прежде чем читать unified log на jetsam-events
предметно — закрываем приватные leak'и в этот же лог. Иначе мы
сначала повышаем surface (читаем log), потом уже думаем что туда
не должно литься — это inverted.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ый target (#43)

Закрывает EXP-1 из validation gate ADR 0011 (Уровень 1.5). Добавление
нового experimental-аксессора больше не требует правки FroggyDaemon/
main.swift — registration становится конвенциональной через
AccessorRegistrar.

Что добавлено:

1. Protocol AccessorRegistrar — generic entry-point регистрации.
   main.swift хранит [any AccessorRegistrar] и call'ит .register(into:)
   для каждого. Новый модуль = новый registrar в списке, не правка
   инициализации конкретных типов.

2. LushaBridgeRegistrar — выносит OCRAccessor + FrontmostAppAccessor
   из main.swift в LushaBridge. main теперь не знает о конкретных
   core-аксессорах.

3. Новый target Sources/LushaExperimental:
   - LushaExperimentalRegistrar — регистратор опытных аксессоров.
   - ThermalStateAccessor (sample) — читает ProcessInfo.thermalState,
     без system permissions, deterministic для теста. Существует,
     чтобы EXP-1 канал был непустой и проверяемый сразу.

4. LushaAccessor.experimental: Bool { get } с default false через
   protocol extension — existing accessors не требуют правки.
   AccessorRegistry.Descriptor.experimental пробрасывается в IPC.

5. IPC:
   - IPCRequest.experimental: Bool? — фильтр для cmd `accessors`
     (true=только experimental, false=только core, nil=все).
   - IPCResponse.Accessor.experimental: Bool? — wire-флаг,
     опциональный для backward-compat.
   - IPCClient.accessors(experimental:) — convenience.
   - froggy CLI: `accessors --experimental | --core`, тег
     `[experimental]` в выводе.

6. Тесты:
   - LushaBridgeTests: registrar accumulation, фильтр, default flag.
   - LushaExperimentalTests: registrar регистрирует ровно experimental,
     thermal accessor, snapshot-roundtrip, mixed core+experimental.
   - IPCProtocolTests: roundtrip experimental поля (request+response).
   - IPCClientTests: e2e фильтр через AF_UNIX.

Снятие блокировки: после AD-1 + FCP-1 (см. ADR 0011) Уровень 2
становится доступен — voice/VLM/persona-router. EXP-1 — последний
из трёх gate-PR'ов архитектурно; AD-1/FCP-1 идут отдельными PR'ами.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 0015) (#44)

Закрывает embarassing failure mode «freeze посередине набора текста»:
координатор морозил pid'ы по bundleId-allowlist'у даже если приложение
было в фокусе у пользователя (Slack/Telegram во время набора, Xcode во
время редактирования и т.д.). Теперь pid frontmost-app никогда не
попадает ни в tier-1, ни в tier-2 freeze, даже если bundleId в
allowlist'е.

Источник истины — NSWorkspace.frontmostApplication + подписка на
didActivateApplicationNotification через WorkspaceEventSource (PR #38),
без polling'а. Добавлен новый event-case .frontmostChanged(pid, bundleId)
— эмитится дополнительно к .appActivated (две разные семантики:
appActivated = «launch-or-activate» для reactive-finder'a,
frontmostChanged = строго «теперь фокус у этого pid»). Initial seed
через WorkspaceEventSource.initialFrontmostPid() — закрывает окно
между startMonitoring и первым переключением фокуса.

Race-окно «pressure прилетел до frontmost-event'а» закрыто mid-freeze
thaw'ом: если новый frontmost pid уже заморожен, applyWorkspaceEvent
моментально его оттаивает.

Scope minimal vs extended: minimal (NSWorkspace-only) выбран потому что
extended (Accessibility API typing-veto через AXFocusedUIElement +
AXValueChanged) требует TCC permission prompt — ухудшает first-run UX
+ расширяет threat model в SECURITY.md (AX позволяет читать любые UI
elements). Frontmost покрывает ~95% случаев; extended — отдельный
future PR с opt-in flag'ом.

* Sources/VortexCore/WorkspaceEventSource.swift: новый case
  .frontmostChanged + initialFrontmostPid() в протоколе; Real source
  эмитит .frontmostChanged на didActivate (но не на didLaunch);
  Fake source поддерживает frontmostPid seed.
* Sources/VortexCore/VortexCoordinator.swift: frontmostPid cache,
  seed на startMonitoring, veto в freezeTier, mid-freeze thaw в
  applyWorkspaceEvent(.frontmostChanged).
* Sources/VortexCore/ProcessFinder.swift: ReactiveProcessFinder.apply
  обрабатывает новый case (no-op).
* Tests/VortexCoreTests/VortexCoordinatorFrontmostVetoTests.swift:
  6 тестов на seed/event-update/tier-2/nil-frontmost/mid-freeze-thaw.
* docs/adr/0015-frontmost-veto-minimal.md: ADR с явным обоснованием
  minimal-scope'а.

Закрывает AD-1 из validation gate Уровня 1.5 (ADR 0011). После merge'a
AD-1 + FCP-1 + EXP-1 в main — открывается дизайн-этап Уровня 2.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Зачем:
ADR 0011 разблокирует Уровень 2 только когда AD-1+FCP-1+EXP-1 в main.
Текущий pacing — внешний `Task.sleep(for: captureInterval)` между cycles.
Этого недостаточно:

- SCStream может выдавать кадры быстрее, чем `captureIntervalSeconds`:
  при анимациях / scrolling'е compositor поднимает frame rate
  принудительно (SwiftUI-окна, видеоплееры).
- Один длинный cycle сдвигает фазу sleep'а — следующий запускается
  сразу, без throttle'а, выпускающий burst в hot OCR.
- Внешний sleep — это интервал «между завершением одного cycle и началом
  следующего», а не «между frames». Реальный gate должен стоять на
  frame-entry-point.

Что:
1. `FramePacer` — маленький value-type gate с `shouldAdmit() -> Bool`.
   Хранит `lastAdmitted: ContinuousClock.Instant?` (монотонные часы —
   Date сломал бы pacing при system time sync). Edge cases:
   - `interval == .zero` → throttle отключён, всё пропускается.
   - первый кадр / long-idle → admitted сразу (без backlog'а).
   - burst-кадр в окне → drop без буферизации (FCP-1: «без буферизации»).
   Time-source инжектится через closure для unit-тестов.

2. `VisionActor.runCycle()` — после `screenStream.latestFrame()` идёт
   `pacer.shouldAdmit()` guard перед digest/OCR/redact/ContextStore.
   Drop — это `signposter.emitEvent("framePacerDropped")` плюс
   `skipped=1` в POI-marker'е (видно в Instruments).

3. Внешний sleep оставлен, но больше не authoritative-pacing: poll-interval
   = `min(captureInterval/4, 100ms)` (минимум 10ms). Это cooperative-yield
   против hot-spin, когда `latestFrame()` возвращает один и тот же кадр
   многократно — реальный gate теперь pacer.

4. Test-seam'ы `_setPacerClock` / `_admitForTest` (internal-видимость) —
   единственный способ протестировать pacer на actor-уровне без SCStream
   и TCC.

Тесты:
- `FramePacerTests` (6 тестов): burst 10 frames @100Ms / interval=1s →
  admitted ≤ 2; interval=.zero → все 100 frames проходят; long-idle →
  admit без задержки; boundary @ ровно interval → admit; regular rate →
  никаких дропов; ContinuousClock-only contract.
- `VisionActorPacingTests` (3 теста): то же самое через actor seam.
- Existing `VisionActorTests` не сломаны.

Существующие 150 тестов проходят. MLXWorkerMetallibPresenceTests падает
в sandbox'е — `xcrun metal` blocked, known issue (см. ADR 0013), не
блокер.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ault + Bug-2 SIGPIPE (#46)

Headline finding из live session 2026-05-08: substrate видит pressure
correctly (50 min critical) но никого не морозит. Investigation root
cause: design gap в `VortexCoordinator.applyWorkspaceEvent` —
`.appActivated` event при sustained .warning/.critical шёл в
`default: break`, freeze policy не пере-оценивалась для tier-1/tier-2
apps запущенных post-pressure-transition.

Хронология из bundle'а: 09:25:29 freezeTier(.tier1) поймал Discord
(pid 1837), 09:26:56 frontmost-veto thaw'нул, 09:27:17 escalation в
.critical с Discord-как-frontmost (vetoed), Telegram запущен ~09:50
во время sustained critical — никогда не морозился.

Fixes:

* **Bug-3** (`VortexCoordinator.applyWorkspaceEvent`): новый case
  `.appActivated` re-evaluates `freezeTier(.tier1/.tier2)` если
  `bundleId` в соответствующем tier list'е и pressure ≥ соответствующего
  threshold'а (.warning для tier-1, .critical для tier-2). `freezeTier`
  идемпотентен (skip already-frozen + frontmost-veto), безопасно
  повторно вызывать. Sleep-gate соблюдён.
* **Bug-5** (`Config.swift`): `freezeRankingEnabled` default `false →
  true`. Mem-5 этап 1 телеметрия теперь passive collected by default;
  overlay (этап 2) пока no-op в коде, безопасно. Это закрывает
  «freeze_stats.sqlite не создаётся никогда» наблюдение.
* **Bug-2** (`FroggyDaemon/main.swift`): `signal(SIGPIPE, SIG_IGN)` в
  начале main. Без этого abrupt client disconnect посреди streaming
  IPC response'а → SIGPIPE → daemon dies exit 141. Один плохой client
  кладёт весь сервис. Hardening hygiene.

Tests:

* `testAppActivatedTriggersFreezeUnderSustainedCritical` — regression
  test для Bug-3 точно по сценарию из live session
* `testAppActivatedUnderNormalDoesNotFreeze` — negative case (под
  .normal новые apps не морозятся)
* `testAppActivatedTier2RespectsCriticalThreshold` — tier-2 порог
  (.warning не trigger'ит, .critical через level-change path
  морозит)
* `MutableStubFinder` actor — helper для тестов где pid появляется
  post-startup'ом (новый app launched под session)

174 existing tests pass + 3 new = 177 green. Bug-1 (CLI non-tty
crash) и Bug-6 (orphan worker on shutdown) — отдельные PR'ы scope'ом.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`FroggyCLI/main.swift:runGenerate` вызывал `FileHandle.standardOutput.synchronizeFile()`
(= fsync) после каждого streaming chunk'а. Это **не определено** для
non-tty FileHandle'ов (pipe, redirect, /dev/null) — кидало
`NSFileHandleOperationException: synchronizeFile: Operation not supported`
и крашило CLI при любом запуске не из interactive shell'а:

* `echo x | froggy gen "..."`
* `froggy gen "..." > out.log`
* CI/automation/harness invocations
* Любой non-tty parent process

Daemon при этом дополнительно умирал по SIGPIPE (закрыто PR #46).

Replace `synchronizeFile()` → `fflush(stdout)`. Канонический «flush stdio
buffer», работает на любом FILE* (tty AND pipe). Streaming UX
сохранён — токены идут немедленно. `import Darwin` уже был.

Bug-1 из notes сессии 2026-05-08. Tests: ручной smoke `echo x |
froggy gen "..."` после merge'а должен работать. Unit-test heavy
(требует IPC client mock), пока без.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`installSignalHandlers` вызывал `coordinator.emergencyThaw()` (отпускает
SIGSTOP'нутые pids) и `exit(0)`, но **не убивал MLX worker'а**. Worker
становился orphan'ом (reparented to launchd, PID 1), eat'ил ~935 MB
RAM до manual cleanup / reboot'а.

Repro из live session 2026-05-08:
1. `FroggyDaemon --model-path ...` (worker spawned, model loaded)
2. `kill <daemon_pid>` (SIGTERM)
3. `pgrep FroggyMLXWorker` → жив, ~935 MB

Это нарушает ADR-0008: subprocess isolation должна включать **lifecycle
isolation**, не только crash-domain.

Fix: в signal handler **до** `exit(0)` добавлен `await
coordinator.unloadModel()`. `MLXSupervisor.unloadModel` шлёт SIGTERM →
3s wait → SIGKILL fallback. Cleanup завершается до exit'а демона.

`unloadModel` идёт **до** `emergencyThaw` чтобы worker не получил
pressure-induced SIGSTOP в последний момент (race с unloadModel'ом).
Эмпирически порядок не критичен — worker отдельный subprocess не в
tier-1/tier-2 — но defensive ordering.

Tests: integration testing graceful shutdown via signal handler heavy
(требует subprocess-level test fixture). После merge'а — manual
verify: `kill <daemon_pid>` → `pgrep FroggyMLXWorker` пусто.

Связанные bugs: Bug-2 (SIGPIPE → SIG_IGN, #46) — закрытие отдельного
ингредиента daemon-stability hardening'а.

Co-authored-by: Yaroslav <yaroslav@JabBook-Air-m3.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-tier

Adds time-correlated signposts in the three places that produce the
clearest "pressure → freeze decision → pageout outcome" timeline in
Instruments:

* MemoryPressureSource: signpost event on each level change (.normal /
  .warning / .critical), with PointsOfInterest twin so the standard PoI
  track shows pressure changes alongside any other component's events.
* PageoutChain.pageout: interval per strategy attempt, with strategy
  name + outcome (success / skipped / failed). Closes the validation-gate
  question from ADR-0007 / 0011: "which pageout strategy actually fires
  on this machine".
* VortexCoordinator.freezeTier: interval per freeze cycle, with tier +
  candidate count + final frozen count.

No behavior change. Existing logs preserved; signposts are
observability-only.

Categories used (subsystem com.froggychips.froggy):
- "pressure"  — pressure level events
- "pageout"   — pageout strategy attempts
- "coordinator" — freeze/thaw cycles (was already partial; freeze-tier added)
- "PointsOfInterest" — duplicates for the standard PoI track

Workflow: open Instruments → Logging template → record FroggyDaemon →
filter by subsystem to see correlated timeline.
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