feat(observability): OS signposts for pressure / pageout / freeze-tier#51
Open
froggychips wants to merge 48 commits into
Open
feat(observability): OS signposts for pressure / pageout / freeze-tier#51froggychips wants to merge 48 commits into
froggychips wants to merge 48 commits into
Conversation
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>
* 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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), withPointsOfInteresttwin so the standard PoI track shows pressure changes alongside any other component's events.PageoutChain.pageout— interval per strategy attempt, withstrategy+ 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, withtier+ candidate count + final frozen count.Why
Today's live session showed the three biggest gaps in current observability:
mach_vm_behavior_setwas actually fired or skipped under fallback to jetsam..warningdispatch event and the actualfreezeTiercall.All three are now visible as a single Instruments timeline.
How to use
Behavior change
None. Existing
os.Loggerlines preserved. Signposts are observability-only.Test plan
make buildclean.warningand.critical, verify Instruments shows correlatedpressure-levelevents +freeze-tierintervals +pageout-attemptintervalsmake test)Follow-ups (not in this PR)
VortexActor.thawProcess/thawAll(thaw timeline)VisionActorOCR cyclesMLXSupervisor(partial coverage exists)Related