onoats 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. onoats-1.1.0/.githooks/pre-commit +14 -0
  2. onoats-1.1.0/.github/workflows/ci.yml +44 -0
  3. onoats-1.1.0/.github/workflows/release.yml +78 -0
  4. onoats-1.1.0/.gitignore +17 -0
  5. onoats-1.1.0/AGENTS.md +226 -0
  6. onoats-1.1.0/CHANGELOG.md +249 -0
  7. onoats-1.1.0/LICENSE +24 -0
  8. onoats-1.1.0/PKG-INFO +322 -0
  9. onoats-1.1.0/README.md +294 -0
  10. onoats-1.1.0/docs/audio-socket-contract.md +420 -0
  11. onoats-1.1.0/docs/blackhole-fallback.md +99 -0
  12. onoats-1.1.0/docs/dev_plans/20260607-feature-menubar-coreaudio-socket-transport.md +871 -0
  13. onoats-1.1.0/docs/dev_plans/20260609-feature-milestone-b-macos-capture-menubar.md +925 -0
  14. onoats-1.1.0/docs/dev_plans/20260611-chore-release-0.9-to-1.0.md +820 -0
  15. onoats-1.1.0/native/.gitignore +2 -0
  16. onoats-1.1.0/native/Makefile +185 -0
  17. onoats-1.1.0/native/README.md +243 -0
  18. onoats-1.1.0/native/make_cert.sh +73 -0
  19. onoats-1.1.0/native/onoats-capturer/Sources/FrameWriter.swift +133 -0
  20. onoats-1.1.0/native/onoats-capturer/Sources/Maintenance.swift +113 -0
  21. onoats-1.1.0/native/onoats-capturer/Sources/MicCapture.swift +336 -0
  22. onoats-1.1.0/native/onoats-capturer/Sources/Resampler.swift +234 -0
  23. onoats-1.1.0/native/onoats-capturer/Sources/SocketServer.swift +123 -0
  24. onoats-1.1.0/native/onoats-capturer/Sources/Support.swift +74 -0
  25. onoats-1.1.0/native/onoats-capturer/Sources/SystemCapture.swift +267 -0
  26. onoats-1.1.0/native/onoats-capturer/Sources/main.swift +464 -0
  27. onoats-1.1.0/native/onoats-menubar/Info.plist +29 -0
  28. onoats-1.1.0/native/onoats-menubar/Sources/AudioDevices.swift +116 -0
  29. onoats-1.1.0/native/onoats-menubar/Sources/ConfigStore.swift +181 -0
  30. onoats-1.1.0/native/onoats-menubar/Sources/OnoatsMenuBarApp.swift +172 -0
  31. onoats-1.1.0/native/onoats-menubar/Sources/RecorderModel.swift +425 -0
  32. onoats-1.1.0/native/residue_check.sh +105 -0
  33. onoats-1.1.0/native/smoke_wire_check.sh +61 -0
  34. onoats-1.1.0/native/wire_check.py +171 -0
  35. onoats-1.1.0/pyproject.toml +70 -0
  36. onoats-1.1.0/scripts/check_review_markers.py +107 -0
  37. onoats-1.1.0/scripts/release_check.sh +77 -0
  38. onoats-1.1.0/src/onoats/__init__.py +0 -0
  39. onoats-1.1.0/src/onoats/__main__.py +444 -0
  40. onoats-1.1.0/src/onoats/_vendor/__init__.py +4 -0
  41. onoats-1.1.0/src/onoats/_vendor/dictionary.py +385 -0
  42. onoats-1.1.0/src/onoats/_vendor/pid.py +226 -0
  43. onoats-1.1.0/src/onoats/_vendor/session_queue.py +216 -0
  44. onoats-1.1.0/src/onoats/_vendor/store.py +69 -0
  45. onoats-1.1.0/src/onoats/agents/__init__.py +0 -0
  46. onoats-1.1.0/src/onoats/categories.py +72 -0
  47. onoats-1.1.0/src/onoats/cli.py +1361 -0
  48. onoats-1.1.0/src/onoats/config/__init__.py +401 -0
  49. onoats-1.1.0/src/onoats/config/audio_devices.py +798 -0
  50. onoats-1.1.0/src/onoats/convert.py +169 -0
  51. onoats-1.1.0/src/onoats/dual.py +742 -0
  52. onoats-1.1.0/src/onoats/frames/__init__.py +38 -0
  53. onoats-1.1.0/src/onoats/frames/branch_vad.py +32 -0
  54. onoats-1.1.0/src/onoats/init.py +601 -0
  55. onoats-1.1.0/src/onoats/jsonl.py +89 -0
  56. onoats-1.1.0/src/onoats/pipeline.py +60 -0
  57. onoats-1.1.0/src/onoats/processors/__init__.py +0 -0
  58. onoats-1.1.0/src/onoats/processors/audio_dump.py +203 -0
  59. onoats-1.1.0/src/onoats/processors/dual_silence_detector.py +292 -0
  60. onoats-1.1.0/src/onoats/processors/heartbeat_notifier.py +40 -0
  61. onoats-1.1.0/src/onoats/processors/live_terminal.py +31 -0
  62. onoats-1.1.0/src/onoats/processors/silence_detector.py +222 -0
  63. onoats-1.1.0/src/onoats/processors/smart_turn_shadow.py +276 -0
  64. onoats-1.1.0/src/onoats/processors/source_tagger.py +100 -0
  65. onoats-1.1.0/src/onoats/processors/transcript_buffer.py +485 -0
  66. onoats-1.1.0/src/onoats/render.py +99 -0
  67. onoats-1.1.0/src/onoats/runtime.py +1215 -0
  68. onoats-1.1.0/src/onoats/status.py +482 -0
  69. onoats-1.1.0/src/onoats/stt/__init__.py +1 -0
  70. onoats-1.1.0/src/onoats/stt/websocket_stt_service.py +521 -0
  71. onoats-1.1.0/src/onoats/transports/__init__.py +38 -0
  72. onoats-1.1.0/src/onoats/transports/socket_audio.py +864 -0
  73. onoats-1.1.0/tests/test_audio_socket_contract_parity.py +102 -0
  74. onoats-1.1.0/tests/test_categories.py +92 -0
  75. onoats-1.1.0/tests/test_cli.py +461 -0
  76. onoats-1.1.0/tests/test_config.py +87 -0
  77. onoats-1.1.0/tests/test_convert.py +186 -0
  78. onoats-1.1.0/tests/test_data_dir.py +59 -0
  79. onoats-1.1.0/tests/test_dual_socket_source.py +639 -0
  80. onoats-1.1.0/tests/test_init.py +292 -0
  81. onoats-1.1.0/tests/test_native_contract_parity.py +532 -0
  82. onoats-1.1.0/tests/test_no_sqlite.py +63 -0
  83. onoats-1.1.0/tests/test_onoats_imports.py +50 -0
  84. onoats-1.1.0/tests/test_package_layout.py +29 -0
  85. onoats-1.1.0/tests/test_pipeline_steps.py +64 -0
  86. onoats-1.1.0/tests/test_release_meta.py +127 -0
  87. onoats-1.1.0/tests/test_render_date.py +74 -0
  88. onoats-1.1.0/tests/test_shutdown_drain.py +116 -0
  89. onoats-1.1.0/tests/test_socket_audio_transport.py +1011 -0
  90. onoats-1.1.0/tests/test_socket_supervisor.py +1966 -0
  91. onoats-1.1.0/tests/test_speaker_labels.py +119 -0
  92. onoats-1.1.0/tests/test_status_file.py +503 -0
  93. onoats-1.1.0/tests/test_storage_data_dir.py +86 -0
  94. onoats-1.1.0/tests/test_stt_config_wiring.py +211 -0
  95. onoats-1.1.0/uv.lock +3892 -0
@@ -0,0 +1,14 @@
1
+ #!/bin/sh
2
+ # onoats pre-commit hook. Enable once per clone with:
3
+ # git config core.hooksPath .githooks
4
+ #
5
+ # Validates that any staged dev-plan's reviewed contract section still matches
6
+ # its review-marker hash (the same check CI runs on every PR). Catches an
7
+ # above-marker edit that forgot to refresh the marker before it lands.
8
+ set -e
9
+
10
+ staged=$(git diff --cached --name-only --diff-filter=ACM | grep '^docs/dev_plans/.*\.md$' || true)
11
+ if [ -n "$staged" ]; then
12
+ # Pass the staged paths explicitly; stdlib + git only, no uv/venv needed.
13
+ python3 scripts/check_review_markers.py $staged
14
+ fi
@@ -0,0 +1,44 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint-and-test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+
14
+ # Cheap, dependency-free gate (stdlib + git only) — fail fast before the
15
+ # heavy portaudio/uv install if a dev-plan's reviewed contract section was
16
+ # edited without refreshing its review marker.
17
+ - name: Dev-plan review-marker check
18
+ run: python3 scripts/check_review_markers.py
19
+
20
+ # pyaudio (via pipecat-ai[local]) builds a C extension against PortAudio,
21
+ # whose dev headers ship with macOS/Homebrew but not ubuntu-latest. This
22
+ # is the off-mac system dep the cross-platform baseline assumes.
23
+ - name: Install system deps (PortAudio for pyaudio)
24
+ run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v7
28
+ with:
29
+ python-version: "3.12"
30
+
31
+ - name: Sync dependencies
32
+ run: uv sync --dev
33
+
34
+ - name: Ruff format check
35
+ run: uv run ruff format --check src/onoats tests
36
+
37
+ - name: Ruff lint
38
+ run: uv run ruff check src/onoats tests
39
+
40
+ - name: Tests
41
+ run: uv run pytest -q
42
+
43
+ - name: CLI help smoke
44
+ run: uv run onoats --help
@@ -0,0 +1,78 @@
1
+ name: Release
2
+
3
+ # Publish to PyPI via trusted publishing (OIDC — no stored token) when a
4
+ # version tag is pushed. The publish job is additionally gated behind the
5
+ # `pypi` GitHub environment, and PyPI's trusted-publisher config pins
6
+ # repo + this workflow filename + that environment, so no other workflow
7
+ # (or fork) can mint a publishable token.
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v5
20
+
21
+ - name: Guard — tag must match pyproject version
22
+ run: |
23
+ TAG="${GITHUB_REF_NAME#v}"
24
+ VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
25
+ if [ "$TAG" != "$VERSION" ]; then
26
+ echo "Tag v$TAG does not match pyproject version $VERSION" >&2
27
+ exit 1
28
+ fi
29
+
30
+ # Same baseline system dep as ci.yml — the release gate re-runs the
31
+ # suite so an artifact is never published from an untested tree.
32
+ - name: Install system deps (PortAudio for pyaudio)
33
+ run: sudo apt-get update && sudo apt-get install -y portaudio19-dev
34
+
35
+ - name: Install uv
36
+ uses: astral-sh/setup-uv@v7
37
+ with:
38
+ python-version: "3.12"
39
+
40
+ - name: Sync dependencies
41
+ run: uv sync --dev
42
+
43
+ - name: Tests
44
+ run: uv run pytest -q
45
+
46
+ - name: Build sdist + wheel
47
+ run: uv build
48
+
49
+ - name: Guard — no direct-URL dependencies in wheel metadata
50
+ run: |
51
+ if unzip -p dist/*.whl '*/METADATA' | grep -E '^Requires-Dist:.*@ (git\+|https?://)'; then
52
+ echo "Direct-URL dependency in metadata — PyPI will reject this" >&2
53
+ exit 1
54
+ fi
55
+
56
+ - uses: actions/upload-artifact@v4
57
+ with:
58
+ name: dist
59
+ path: dist/
60
+ if-no-files-found: error
61
+
62
+ publish:
63
+ needs: build
64
+ runs-on: ubuntu-latest
65
+ environment:
66
+ name: pypi
67
+ url: https://pypi.org/p/onoats
68
+ permissions:
69
+ # OIDC token for PyPI trusted publishing — granted ONLY to this job.
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v4
73
+ with:
74
+ name: dist
75
+ path: dist/
76
+
77
+ - name: Publish to PyPI
78
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,17 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ build/
8
+ dist/
9
+
10
+ # uv / venv
11
+ .venv/
12
+
13
+ # onoats runtime/data (XDG dirs are out-of-tree; guard local overrides)
14
+ .conduct/
15
+ *.db
16
+ secrets.env
17
+ .deep-review/
onoats-1.1.0/AGENTS.md ADDED
@@ -0,0 +1,226 @@
1
+ # AGENTS.md — onoats maintainer & agent guide
2
+
3
+ Conventions an agent (or human) needs before changing onoats. Scoped to the
4
+ non-obvious parts; the code and `docs/` cover the rest.
5
+
6
+ ## Tooling
7
+
8
+ - Package manager is **`uv`**. Install: `uv sync`. Run: `uv run <cmd>`.
9
+ - Tests: `uv run pytest`. Lint/format before every push: `uv run ruff format`
10
+ **and** `uv run ruff check` (a PostToolUse hook formats on edit, but verify).
11
+ - Prefer the **pipecat-context-hub MCP** tools for Pipecat framework questions
12
+ over reading `.venv` source.
13
+ - **Dev-plan review markers are CI-gated.** `python3 scripts/check_review_markers.py`
14
+ (a CI step, stdlib + git only) fails if a reviewed plan's contract section —
15
+ everything above its `<!-- reviewed: YYYY-MM-DD @ <sha1> -->` marker — was edited
16
+ without refreshing the hash. The marker hashes only the above-marker bytes
17
+ (same convention as the skein `/review-plan` + `/conduct` tooling), so editing
18
+ the `## Progress` / `## Findings` workspace below it is free. Enable the same
19
+ check locally with `git config core.hooksPath .githooks`. To clear a failure:
20
+ re-run `/review-plan`, or recompute the hash for a purely administrative
21
+ above-marker edit (`head -n <marker_line-1> plan.md | git hash-object --stdin`).
22
+
23
+ ## Audio capture: PortAudio vs socket (`AUDIO_SOURCE`)
24
+
25
+ `onoats bot` has two capture backends, selected by `AUDIO_SOURCE` (env /
26
+ `config.toml [audio].source`), branched in exactly one place each:
27
+
28
+ - **`portaudio`** (default) — today's `LocalAudioTransport` path, unchanged.
29
+ - **`socket`** — reads framed PCM16 from two per-branch unix sockets via
30
+ `onoats.transports.socket_audio.UnixSocketAudioTransport`.
31
+
32
+ Load-bearing invariants — do not break these without updating the tests that
33
+ pin them (`tests/test_dual_socket_source.py`, `tests/test_socket_audio_transport.py`,
34
+ `tests/test_socket_supervisor.py`, `tests/test_status_file.py`,
35
+ `tests/test_native_contract_parity.py` — the last pins Swift literals to their
36
+ Python contract constants):
37
+
38
+ - **Never-mix.** One socket → one branch → one STT session → one `SourceTagger`.
39
+ Nothing fans a socket to both branches. `_build_socket_transports` refuses to
40
+ start if the two socket paths resolve (`Path.expanduser().resolve()`) to the
41
+ same file.
42
+ - **No PortAudio on the socket path.** `AUDIO_SOURCE=socket` must neither import
43
+ nor invoke the PyAudio device-enumeration path (`select_dual_input_devices`).
44
+ `import onoats.dual` must succeed with **no native binary present** (CI is
45
+ native-free).
46
+ - **EOF / read-idle is fatal, no self-reconnect.** The transport surfaces a
47
+ **fatal `ErrorFrame`** (`push_error(..., fatal=True)`, which goes *upstream* —
48
+ that's what cancels the pipeline) on EOF, a framing error, a read-idle timeout,
49
+ a BLOCK consumer-stall, or a staging-pump failure. The supervisor owns restart;
50
+ the transport never reconnects itself.
51
+ - **Bounded staging + downstream gate.** The reader stages into a buffer bounded by
52
+ **both** a frame count (`max_buffered_frames`) and total bytes
53
+ (`max_buffered_bytes`); the drop policy (default `drop-oldest`, with a WARNING)
54
+ keeps memory capped under a faster-than-consumer writer. Because pipecat's
55
+ `_audio_in_queue` is unbounded, the pump also **gates on the base queue's depth**
56
+ (`_await_downstream_room`, high-water = `max_buffered_frames`) so a *stalled
57
+ consumer* can't grow it without limit — the frame cap bounds both queues (~2×
58
+ worst case). Don't reintroduce an unconditional `push_audio_frame` drain in the
59
+ pump; that's the bug Codex caught (re-verified via the integrated reader+pump
60
+ regression tests in `tests/test_socket_audio_transport.py`).
61
+
62
+ Release metadata (canonical BSD-2-Clause LICENSE body, pyproject SPDX
63
+ `license` field, `hatchling>=1.27` pin for PEP 639) is pinned by
64
+ `tests/test_release_meta.py` — don't weaken those without touching the
65
+ licensing decision in the release plan. The version lives on **three
66
+ surfaces** that must agree — `pyproject.toml`, the `uv.lock` onoats entry
67
+ (regenerate via `uv lock`), and the menu-bar Info.plist
68
+ `CFBundleShortVersionString` — pinned by the same test file and gated at
69
+ tag time by `scripts/release_check.sh vX.Y.Z <commit>` (run before pushing
70
+ any release tag).
71
+
72
+ Releasing to PyPI (v1.1.0+): pushing a `v*` tag triggers
73
+ `.github/workflows/release.yml` — full suite + two guards (tag must equal
74
+ the pyproject version; wheel metadata must carry no direct-URL deps), then
75
+ publish via PyPI **trusted publishing** (OIDC, no stored token) gated behind
76
+ the GitHub `pypi` environment. One-time prerequisites: a PyPI trusted
77
+ publisher (project `onoats`, repo `vr000m/onoats-bot`, workflow
78
+ `release.yml`, environment `pypi`) and the GitHub `pypi` environment itself.
79
+ PyPI rejects direct-URL `Requires-Dist` entries — dependencies must come
80
+ from PyPI (this is why `pipecat-local-stt-server` is a `>=0.1.2,<0.2`
81
+ registry dep, not a git pin).
82
+
83
+ ## Supervisor ↔ capturer lifecycle (`cli._run_socket_supervisor`)
84
+
85
+ - The **supervisor owns the capturer process.** It mints a private `0700` socket
86
+ dir + a fresh generation **nonce**, exports `ONOATS_MIC_SOCKET` /
87
+ `ONOATS_SYSTEM_SOCKET` / `ONOATS_CAPTURER_NONCE`, spawns `ONOATS_CAPTURER_BIN`
88
+ (paths + nonce via **both** argv and env), waits (bounded) for both sockets,
89
+ then runs the recorder. The capturer's tap preflight (release-plan Phase 7)
90
+ makes the TCC-prompting tap call **before** binding sockets, announced by
91
+ `ONOATS-EVENT waiting-for-permission`; if the base socket wait expires with
92
+ that event seen, the supervisor extends the wait once (+120 s) and surfaces
93
+ the pending prompt in the status file.
94
+ - **Nonce gating end-to-end:** supervisor mints → `cfg.capturer_nonce` →
95
+ transport `expected_nonce`. A capturer presenting a missing/stale nonce on the
96
+ supervisor's paths is rejected at handshake. Ungated (None) when socket mode is
97
+ driven manually without the supervisor.
98
+ - **Fail-loud is the contract.** Every failure path (missing/unspawnable capturer,
99
+ sockets that never appear, capturer death mid-session, STT preflight) yields a
100
+ non-zero exit + a WARNING/ERROR log, and a partial session still rotates into
101
+ `pending/` — never a hang.
102
+ - **Capturer-exit-before-recorder is always fail-loud, even on `rc=0`** — the
103
+ recording is truncated regardless of exit code. Default-input-device changes
104
+ (e.g. AirPods removal) are the **capturer's** job to absorb by re-binding and
105
+ continuing to stream; see `docs/audio-socket-contract.md`.
106
+ - Shared recorder arg handling lives in `dual._apply_recorder_args` — both
107
+ `dual.main` and the supervisor route through it, so interactive/category
108
+ handling can't drift.
109
+
110
+ ## Reviewing a subprocess / process-boundary change
111
+
112
+ When a change spawns a child process (`create_subprocess_*` / `Popen` / `exec`)
113
+ or otherwise crosses an OS boundary, the general review lenses tend to stay on
114
+ the in-process logic and miss the boundary itself. The capturer supervisor's
115
+ three post-review findings (one `[high]` no-ship) all came from this blind spot —
116
+ the heavyweight gate stack passed; a single adversarial pass caught them. Run this
117
+ checklist explicitly for any new spawn:
118
+
119
+ - **Signals / session.** Does the child inherit the parent's controlling-terminal
120
+ signals (Ctrl+C / SIGTERM via the foreground process group)? If it must not,
121
+ spawn with `start_new_session=True`. (Without it, a graceful shutdown can be
122
+ mis-read as the child dying — the capturer's `[high]` finding.)
123
+ - **Environment / secrets.** Never pass `dict(os.environ)` to a child. Build a
124
+ deny-by-default allowlist (see `cli._CAPTURER_ENV_POLICY`) so STT/application
125
+ secrets — and dylib-**injection** vars like `DYLD_INSERT_LIBRARIES` — never reach
126
+ a child that does not need them.
127
+ - **Teardown reaches the whole group.** A session-leader child may spawn its own
128
+ helpers; signal the **process group** (`os.killpg(os.getpgid(pid), …)`), not just
129
+ the leader PID, on **both** the graceful and crash (leader-already-reaped) paths,
130
+ so nothing outlives the session holding a resource (e.g. the audio device).
131
+ - **File descriptors.** Does the child inherit fds it should not (sockets, pipes,
132
+ log handles)? Pass only what is intended.
133
+ - **Working dir + argv.** cwd is inherited; spawn via argv (no shell) so there is
134
+ no shell-injection surface — but verify `argv[0]` resolves to a trusted path.
135
+ - **Failure is loud + bounded.** Spawn failure, child death, and a silent/hung
136
+ child each yield a non-zero exit + a WARNING/ERROR log + a bounded wait (no
137
+ hang) — see the fail-loud contract above.
138
+
139
+ Each item should map to a regression test that fails against the pre-fix code
140
+ (signal: spawn-kwarg + `rc`; env: a planted secret stripped from the child env;
141
+ teardown: a spawned child PID is gone after stop). The supervisor's tests in
142
+ `tests/test_socket_supervisor.py` are the worked example.
143
+
144
+ ## Wire-format contract
145
+
146
+ `docs/audio-socket-contract.md` is the versioned (`v1`) capturer↔recorder
147
+ contract and the source of truth for framing/handshake/constants. **Extending the
148
+ wire format** means bumping `WIRE_VERSION` in `socket_audio.py` **and** the doc
149
+ together — `tests/test_audio_socket_contract_parity.py` fails CI if the doc's
150
+ constants table and the module drift.
151
+
152
+ ## Review Checklist
153
+
154
+ Dismissed review findings (`won't-fix` / `analysis-error`) that future reviews
155
+ should NOT re-flag go here, one per line:
156
+
157
+ `- **[Category] disposition**: description (YYYY-MM-DD)`
158
+
159
+ - **[Architecture] won't-fix**: `dual._apply_recorder_args` (and `_parse_args`)
160
+ keep their leading underscore despite being imported cross-module by `cli.py` —
161
+ this matches the established in-repo convention for shared-but-internal helpers
162
+ (`_parse_args`, `_build_socket_transports`); they are intentionally cross-module,
163
+ not a public API. (2026-06-09)
164
+ - **[Architecture] won't-fix**: `max_buffered_bytes` is clamped to `max(1, …)`
165
+ only inside `UnixSocketAudioInputTransport`, not at the `UnixSocketAudioTransport`
166
+ facade — the inner clamp is the single point of use and mirrors how
167
+ `max_buffered_frames` is handled (`Queue(maxsize=max(1, …))`); forwarding the
168
+ raw value through the facade is intentional. (2026-06-09)
169
+ - **[Logic] analysis-error**: `nonce[:8]` in the handshake log assumes a `str` —
170
+ unreachable: `parse_handshake` validates `nonce` is `str | None` and the log
171
+ guards on truthiness, so a non-string nonce can never reach the slice. (2026-06-09)
172
+ - **[Architecture] won't-fix**: `native/spike/Info.plist` shares the production
173
+ `CFBundleIdentifier` — intentional: Spike 3's entire purpose was validating
174
+ TCC persistence on the PRODUCTION designated requirement (bundle id + cert),
175
+ so a distinct spike identity would invalidate the spike evidence. The spike
176
+ tree was deleted in Phase 6 of the 0.9→1.0 plan (preserved at the
177
+ `spike-archive` tag). (2026-06-10; resolved 2026-06-11)
178
+ - **[Performance] won't-fix (scoped)**: the capturer IOProcs perform one bounded
179
+ heap copy (`Data(bytes:count:)`, ~10 ms chunk) and take an `NSCondition` lock
180
+ per callback (`MicCapture.enqueueChunk` / `SystemCapture.enqueueChunk`). A
181
+ textbook RT path would use a pre-allocated lock-free ring buffer; we keep the
182
+ copy+lock because (a) the worker holds the lock only for a queue pop —
183
+ microsecond contention window, (b) the pattern is hardware-verified across
184
+ the full Phase 4/5b smoke incl. hours-long real-call sessions with zero HAL
185
+ starvation, and (c) a ring-buffer rewrite would invalidate that evidence and
186
+ force a re-smoke for a latent, never-observed risk. What we DID fix
187
+ (2026-06-10): the drop-path `logLine` that ran on the realtime thread —
188
+ drops are now counted under the lock and reported from the worker thread.
189
+ Revisit only if a real session ever logs HAL silence/dropouts. (2026-06-10)
190
+ - **[Logic] analysis-error**: `FrameChunker.append` back-extrapolating from the
191
+ total `pending.count` "over-counts leftover samples" — the math is exact while
192
+ capture is contiguous (leftovers are contiguous with the next buffer); only a
193
+ frame straddling a capture gap inherits a bounded <20 ms skew, governed by the
194
+ existing `lastEmittedEndNs` clamp. Comment added at the site. (2026-06-10)
195
+ - **[Architecture] won't-fix**: `onoats bot --source` sets `AUDIO_SOURCE` in
196
+ `os.environ` rather than threading a parameter — deliberate: the env var is
197
+ the pre-existing public contract (config.toml/env already select the source),
198
+ the flag is a convenience alias onto that contract, and downstream re-parses
199
+ argv independently (documented at the site). Threading a parameter would
200
+ create a second, competing resolution path. (2026-06-11)
201
+ - **[Security] won't-fix**: `make_cert.sh` passes the p12 transport password on
202
+ `security import -P` argv — `security import` has no file/stdin password
203
+ option. Residual is a one-shot random secret guarding a file that lives
204
+ seconds inside a 0700 tmpdir; documented at the site. (2026-06-11)
205
+ - **[Architecture] won't-fix**: `LateBoundWriter` stays defined at file scope in
206
+ `main.swift` rather than moving to `Support.swift` — it is private plumbing of
207
+ main.swift's startup reorder (preflight-before-sockets), used nowhere else,
208
+ and the Swift sources just passed the full Phase 7 live smokes; relocating
209
+ code in a file with no Swift test runner would invalidate that evidence for
210
+ zero behavioural gain. (2026-06-11)
211
+ - **[Architecture] analysis-error**: `permission_event` "is not documented in
212
+ the data-flow comment near `device_state`" — it is: the comment block at the
213
+ declaration site in `_supervise_socket_session` (directly under the
214
+ `device_state` comment) documents who sets it and who reads it. (2026-06-11)
215
+ - **[Architecture] won't-fix**: the `auto`→`None` STT-language mapping lives in
216
+ `runtime._resolve_stt_language`, not in `OnoatsConfig.stt_language` —
217
+ deliberate: the property stays a plain `str` for parity with `stt_model`,
218
+ its docstring states the runtime does the mapping, and `_create_stt_service`
219
+ is the single consumer. Moving it would change the property's type contract
220
+ (`str | None`) for no caller. (2026-06-12)
221
+ - **[Architecture] won't-fix**: `[stt].language` has no Swift menu-bar picker
222
+ or parity-test entry — intentional: the key is CLI/file-managed (like
223
+ `ws_socket`); the menu bar exposes config.toml via its open-config dropdown,
224
+ which is the supported edit path, and `ConfigStore` round-trips arbitrary
225
+ keys without code changes. A GUI picker would be a separate feature.
226
+ (2026-06-12)
@@ -0,0 +1,249 @@
1
+ # Changelog
2
+
3
+ All notable changes to onoats are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ All listed versions — including the reconstructed pre-0.9 era — are licensed
8
+ under BSD-2-Clause (see [LICENSE](LICENSE)).
9
+
10
+ Versions before 0.9.0 were never published or tagged; they are reconstructed
11
+ retrospectively from the merged-PR history (nothing was ever distributed, so
12
+ no backdated tags exist). PR numbers `#1`–`#7` refer to this repository;
13
+ older history predates the extraction and is cited by merge-commit SHA.
14
+ Annotated tags exist from `v0.9.0` forward.
15
+
16
+ ## [1.1.0] - 2026-06-12
17
+
18
+ First PyPI release (`pip install onoats` / `uv tool install onoats`).
19
+
20
+ ### Changed
21
+ - `pipecat-local-stt-server` is now consumed from PyPI (`>=0.1.2,<0.2`)
22
+ instead of a git-URL pin — `0.1.2` is the release of the exact commit
23
+ previously pinned (`5062b98` == tag `v0.1.2`). This removes the
24
+ direct-reference metadata PyPI rejects, unblocking publication. Packaging
25
+ metadata (readme, authors, URLs, classifiers) added for the PyPI page.
26
+
27
+ ### Added
28
+ - Tag-triggered PyPI publish via GitHub Actions trusted publishing
29
+ (`.github/workflows/release.yml`): pushing a `v*` tag runs the full suite
30
+ plus two guards (tag == pyproject version; no direct-URL deps in wheel
31
+ metadata), then publishes via OIDC behind the `pypi` GitHub environment —
32
+ no stored token.
33
+ - `[stt] language` in `config.toml`: the STT decode language is now a
34
+ first-class config key (env `STT_LANGUAGE` > legacy alias `STT_WS_LANGUAGE`
35
+ > `[stt].language` > `en`),
36
+ shared by every launch path (CLI, menu-bar app, `onoats init`). `auto`
37
+ means auto-detect and maps to `None` at the backend boundary. The local
38
+ whisper/MLX branches now honour it too (they previously hardcoded `en`);
39
+ Deepgram does not consume it. `onoats init` prompts for it in the local
40
+ STT branch. (PR #22)
41
+
42
+ ### Fixed
43
+ - Review fixes on the language key (PR #22): a whitespace-only
44
+ `[stt].language` now falls back to `en` instead of reaching the backend as
45
+ `language=""`; non-interactive `onoats init` re-runs carry an existing
46
+ `language` forward instead of silently erasing it; switching the wizard to
47
+ Deepgram preserves the key for a later switch back.
48
+
49
+ ## [1.0.0] - 2026-06-12
50
+
51
+ First stable release. Closes out the 0.9.x series' 1.0.0 gates: pre-socket
52
+ tap preflight (PR #17), BlackHole prose pruning + the `setup-cli` install
53
+ path (PR #18), and the ConfigStore TOML-subset parity suite with its CRLF
54
+ fix (PR #19).
55
+
56
+ ### Added
57
+ - `make -C native setup-cli` (release-plan Phase 8): one-command CLI +
58
+ native-capture install (cert → capturer build/sign → CLI shim → `onoats
59
+ init`) that skips the menu-bar app bundle. README now documents the three
60
+ install paths side by side: menubar (`setup`), CLI + native capture
61
+ (`setup-cli`, macOS 14.4+), and CLI + PortAudio (toolchain-free, see
62
+ `docs/blackhole-fallback.md`).
63
+
64
+ ### Changed
65
+ - BlackHole prose pruned to the conservative keep-list (release-plan
66
+ Phase 8): redundant README mentions and the `pyproject.toml` `[macos]`
67
+ comment now point at `docs/blackhole-fallback.md`; the `_LOOPBACK_HINTS`
68
+ auto-detection, no-loopback NOTE, backend help text, and all config-wiring
69
+ tests are unchanged.
70
+ - Pre-socket tap preflight (release-plan Phase 7, PR #17): the capturer makes
71
+ the TCC-prompting tap call **before** binding its sockets, announced by
72
+ `ONOATS-EVENT waiting-for-permission`. A first Start with the Screen &
73
+ System Audio Recording dialog unanswered no longer dies at ~10 s — the
74
+ supervisor extends its socket wait once (+120 s) and surfaces "waiting for
75
+ the system-audio permission prompt" in the status file / menu bar.
76
+ - rc=11 `exit_reason` renamed `system-audio-denied` → `system-audio-failed`:
77
+ a TCC denial never exits the capturer (denied taps deliver zeros and
78
+ surface as the zero-run warning); rc=11 fires only on genuine tap API
79
+ failure.
80
+
81
+ ### Fixed
82
+ - Menu-bar settings edits no longer corrupt a `config.toml` with CRLF line
83
+ endings (release-plan Phase 9): ConfigStore's line scan trimmed with a
84
+ whitespace set that excludes `\r`, so CRLF section headers were never
85
+ matched and `writeValue` appended a duplicate section that fails TOML
86
+ parsing. Untouched lines keep their CRLF bytes verbatim; the edited line
87
+ is written with LF. Pinned by the new TOML-subset parity suite
88
+ (12 cases: comments, whitespace variants, absent section/key, duplicate
89
+ keys, non-string neighbours, byte-identity, CRLF).
90
+ - Mic silence pacer now activates before the HAL bind, so a slow bind
91
+ (pending TCC dialog, Bluetooth device activation) can no longer trip the
92
+ recorder's 10 s read-idle and kill the session.
93
+ - Latent start-timeout stamping bug: the prestart failure stamp read the
94
+ capturer's exit code after reaping it, so a hung-but-alive capturer was
95
+ mislabeled `capturer-start-failed`; `capturer-start-timeout` is now
96
+ reachable end-to-end.
97
+
98
+ ## [0.9.0] - 2026-06-11
99
+
100
+ Milestone B: native macOS capture + menu-bar app (PR #5, `16da012`; docs
101
+ ride-alongs PR #6 `6aa0025`, PR #7 `8db8840`).
102
+
103
+ ### Added
104
+ - Native macOS system-audio capture via a Core Audio process tap
105
+ (`onoats-capturer` Swift binary); no loopback driver needed on macOS 14.4+.
106
+ - SwiftUI menu-bar app (`Onoats.app`): Start/Flush/Stop, inline mic picker
107
+ (sets the macOS default input), STT service picker, data-dir chooser,
108
+ status display.
109
+ - Per-chunk capture-generation stamping — each queued audio chunk carries its
110
+ generation's format and resampler, preventing stale-format races on device
111
+ switch.
112
+ - Self-signed signing identity workflow: `make -C native cert` (refuses to
113
+ regenerate an existing cert) + `make -C native install` / `install-cli`.
114
+ - Sustained all-zero-input detector (30 s) in the capturer — surfaces a
115
+ denied system-audio grant as a WARNING instead of silent empty recordings.
116
+ - Kill-×3 tap/aggregate residue smoke (`native/residue_check.sh`) and
117
+ one-command wire smoke (`native/smoke_wire_check.sh`).
118
+
119
+ ### Changed
120
+ - The native capturer is the default macOS capture story;
121
+ BlackHole/PortAudio demoted to the documented fallback (macOS 13.x–14.3 /
122
+ off-mac).
123
+ - `ConfigStore` (menu bar) writes TOML with correct basic-string escaping;
124
+ data-dir handling made canonical and per-session.
125
+
126
+ ### Fixed
127
+ - Pre-start capturer death now writes a status record — a denied grant no
128
+ longer surfaces as "failed: graceful".
129
+ - Realtime-thread logging and state races; fail-loud flush path; IOProc
130
+ zero-guard.
131
+
132
+ ### Notes
133
+ - The runtime dependency `pipecat-local-stt-server` is intentionally
134
+ git-pinned (`pyproject.toml`) — correct for the from-source install story;
135
+ revisit only if PyPI publishing is ever taken up.
136
+
137
+ ## [0.8.0] - 2026-06-09
138
+
139
+ Milestone A: socket audio transport + supervisor (PR #4, `1f9dfdc`).
140
+
141
+ ### Added
142
+ - `UnixSocketAudioTransport` — framed-PCM16 Unix-socket audio input pipeline
143
+ with bounded staging, downstream-queue gating, and fail-loud fatal errors.
144
+ - `AUDIO_SOURCE=portaudio|socket` backend switch, branched in exactly one
145
+ place per layer.
146
+ - CLI supervisor: private `0700` socket dir, per-session generation nonce
147
+ (handshake-enforced), bounded socket-appearance wait, process-group
148
+ teardown on both crash and graceful paths.
149
+ - Audio-socket wire contract document (`docs/audio-socket-contract.md`) with
150
+ a parity test that fails CI when doc and code constants drift.
151
+ - `AGENTS.md` contract notes; dev-plan review-marker CI gate
152
+ (`scripts/check_review_markers.py`).
153
+
154
+ ### Changed
155
+ - Capturer environment built from a deny-by-default allowlist (blocks DYLD
156
+ injection); capturer spawned in an isolated session so terminal signals
157
+ don't trip the fail-loud path.
158
+
159
+ ### Fixed
160
+ - Transport failures are fatal upstream errors, so the recorder terminates
161
+ instead of hanging; handshake reads bounded by the idle watchdog; tilde
162
+ expansion on socket paths; capturer group swept on the crash path.
163
+
164
+ ## [0.7.0] - 2026-06-08
165
+
166
+ Packaging and extraction era: standalone `onoats` package (untagged;
167
+ PR #1 `7f33e72`, PR #2 `645d90b`, PR #3 `8e6c165`, plus direct commits).
168
+
169
+ ### Added
170
+ - `src/onoats/` installable package layout, extracted from the `bot/`
171
+ monolith; `onoats init` CLI; CI with ruff + PortAudio build steps.
172
+ - Configurable `[storage].data_dir` (`config.toml` + `onoats init
173
+ --data-dir`); XDG data paths.
174
+ - `STT_WS_LANGUAGE` env to control the websocket decoder language.
175
+
176
+ ### Changed
177
+ - `config.toml` honoured in the recorder process for STT and device
178
+ settings; SQLite overlay removed; transcript date grouping uses local
179
+ date, not UTC.
180
+ - STT server renamed `onoats-stt` → `pipecat-stt` (server v0.2.0): socket
181
+ paths and labels updated.
182
+
183
+ ### Fixed
184
+ - STT RSS probe resolves `stt_server` from the config-layered env (PR #1).
185
+ - Flush (SIGUSR1) verifies live process identity before signalling — a stale
186
+ PID file can no longer kill an unrelated process (PR #2).
187
+ - Shutdown drains the final transcript segment before flush; Ctrl+C cancel
188
+ timeout capped (no more ~20 s hang); device provenance logged accurately
189
+ (PR #3).
190
+
191
+ ## [0.5.0] - 2026-05-21
192
+
193
+ Dual-input diarization and STT-service era (untagged; pre-extraction
194
+ history, cited by merge SHA).
195
+
196
+ ### Added
197
+ - Standalone Whisper WebSocket transcription server with a Pipecat
198
+ `STTService` wrapper and launchd auto-restart (`c4f08d5`).
199
+ - STT health preflight, status probe, and `./onoats stt` wrapper
200
+ (`e40f2de`).
201
+ - Live passive transcript view with rotation and orphan guards (`2536687`).
202
+ - SmartTurn V3 read-only shadow observer on the `me` branch + raw PCM dump
203
+ for offline A/B testing (`0e4ae64`).
204
+ - Parakeet ASR backend with per-server multi-ASR selection (`21ebfae`).
205
+ - Cron-driven post-processing worker decoupled from the bot harness; the
206
+ bot rotates session files instead of processing in-flight (`4d54b88`,
207
+ the `stt-extraction-base` tag's commit).
208
+
209
+ ### Changed
210
+ - Dual-input pipeline became the default bot path — coarse Me/Them
211
+ diarization via two PortAudio branches (`196aef2`).
212
+ - Post-processing queue rebuilt as an FSM; the bot harness no longer owns
213
+ worker state (`4d54b88`).
214
+
215
+ ### Fixed
216
+ - Continuation-flush session-file swap race; speaker-label plumbing and
217
+ source resolution; STT reconnect backoff made exponential (0.5 → 8 s);
218
+ long turns chunked.
219
+
220
+ ## [0.3.0] - 2026-04-16
221
+
222
+ Processing-pipeline era (untagged; pre-extraction history).
223
+
224
+ ### Added
225
+ - Topic discovery, collation, and management pipeline (`6124d56`).
226
+ - Multi-task LLM benchmark, LM Studio provider, per-task LLM routing, and
227
+ manual transcript flush via Ctrl+T / `./onoats flush` (`ced152a`).
228
+ - "seminars" processing category + `--category` bot CLI flag (`81afbc0`).
229
+
230
+ ### Fixed
231
+ - Timeline corruption: ownership-aware rollback + cross-midnight concat in
232
+ segmenter/classifier/merge (`7b97d7e`).
233
+ - PID-file identity hardened; pipeline cancel interruptible on Ctrl+C
234
+ (`ced152a`).
235
+ - `ONOATS_DATA_DIR` tilde expansion across all entry points.
236
+
237
+ ## [0.1.0] - 2026-04-04
238
+
239
+ Initial recorder (untagged; root commit `d645650`, 2026-03-30).
240
+
241
+ ### Added
242
+ - Pipecat-based meeting recorder: PortAudio input, silence detection,
243
+ transcript buffering, Ctrl+C graceful shutdown with the current session
244
+ processed before exit.
245
+ - SQLite overlay, web intelligence layer, and transcript management
246
+ (`3d36ae3`).
247
+ - Collation service: living documents aggregated from related idea
248
+ transcripts (`5cd908c`).
249
+ - LLM transcript cleanup + dictionary/provenance plumbing (`9d974c1`).
onoats-1.1.0/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2025–2026 Varun Singh
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.