personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

wrapper: bump managed-version to 3, export PYTHONUNBUFFERED=1

The supervisor branch of the managed sol wrapper redirects stdout/stderr
to journal/health/service.log. Python block-buffers stdout when fd1 is
a regular file, so daemon output stayed invisible until graceful shutdown
flushed the libc 8 KB buffer. Exporting PYTHONUNBUFFERED=1 inside the
supervisor branch makes the daemon flush in real time, so `sol service
logs` reflects live state without a restart.

Scope is limited to the supervisor branch; CLI invocations (`$1` not
`supervisor`) skip the if-block and keep Python's default TTY/pipe
buffering. parse_wrapper accepts v1/v2/v3 markers and rejects v4+;
check_alias_detail's strict version-equality gate auto-reports
`upgrade` for installed v1 and v2 wrappers, so the next
`make install-service` rewrites them to v3.

Validation note: `make ci` currently stops during the pre-existing
`.installed` bootstrap path because observe.transcribe.overlap imports
onnxruntime, which is not installed in this system-python environment.
The targeted pytest, layer-hygiene, ruff check, and ruff format checks
passed for this change.

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

+173 -19
+1 -1
docs/DOCTOR.md
··· 44 44 | What | Where | 45 45 |------|-------| 46 46 | Current service logs | `journal/health/{service}.log` (symlinks) | 47 - | Daemon stdout/stderr | `journal/health/service.log` (combined, append-only) | 47 + | Daemon stdout/stderr | `journal/health/service.log` (combined, append-only). The managed wrapper exports `PYTHONUNBUFFERED=1` for supervisor runs so stdout/stderr flush in real time and show up in `sol service logs` without a restart. | 48 48 | Day's process logs | `journal/{YYYYMMDD}/health/{ref}_{name}.log` | 49 49 | Agent execution | `journal/talents/<name>/*.jsonl` | 50 50 | Journal task log | `journal/task_log.txt` |
+1 -1
tests/test_config_cli.py
··· 63 63 assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 64 64 "journal": journal, 65 65 "sol_bin": sol_bin, 66 - "version": 2, 66 + "version": 3, 67 67 } 68 68 69 69
+166 -13
tests/test_install_guard.py
··· 5 5 6 6 import os 7 7 import subprocess 8 + import sys 9 + import time 8 10 from pathlib import Path 9 11 from unittest.mock import Mock 10 12 ··· 22 24 if [ ! -x "$SOL_BIN" ]; then 23 25 printf 'sol: venv binary missing or not executable: %s\\n' "$SOL_BIN" >&2 24 26 exit 127 27 + fi 28 + exec "$SOL_BIN" "$@" 29 + """ 30 + 31 + V2_WRAPPER_TEMPLATE = """\ 32 + #!/bin/sh 33 + # sol — managed by 'sol config'. Edits will be overwritten. 34 + # managed-version: 2 35 + : "${{SOLSTONE_JOURNAL:={journal}}}" 36 + export SOLSTONE_JOURNAL 37 + SOL_BIN='{sol_bin}' 38 + if [ ! -x "$SOL_BIN" ]; then 39 + printf 'sol: venv binary missing or not executable: %s\\n' "$SOL_BIN" >&2 40 + exit 127 41 + fi 42 + if [ "$1" = "supervisor" ]; then 43 + mkdir -p "$SOLSTONE_JOURNAL/health" 44 + exec >>"$SOLSTONE_JOURNAL/health/service.log" 2>&1 25 45 fi 26 46 exec "$SOL_BIN" "$@" 27 47 """ ··· 81 101 return V1_WRAPPER_TEMPLATE.format(journal=journal, sol_bin=escaped_sol_bin) 82 102 83 103 104 + def render_v2_wrapper(*, journal: str, sol_bin: str) -> str: 105 + escaped_sol_bin = sol_bin.replace("'", "'\\''") 106 + return V2_WRAPPER_TEMPLATE.format(journal=journal, sol_bin=escaped_sol_bin) 107 + 108 + 84 109 def make_v1_wrapper( 85 110 home_root: Path, 86 111 *, ··· 91 116 alias = home_root / ".local" / "bin" / "sol" 92 117 alias.parent.mkdir(parents=True, exist_ok=True) 93 118 alias.write_text(render_v1_wrapper(journal, sol_bin), encoding="utf-8") 119 + alias.chmod(mode) 120 + return alias 121 + 122 + 123 + def make_v2_wrapper( 124 + home_root: Path, 125 + *, 126 + journal: str, 127 + sol_bin: str, 128 + mode: int = 0o755, 129 + ) -> Path: 130 + alias = home_root / ".local" / "bin" / "sol" 131 + alias.parent.mkdir(parents=True, exist_ok=True) 132 + alias.write_text( 133 + render_v2_wrapper(journal=journal, sol_bin=sol_bin), 134 + encoding="utf-8", 135 + ) 94 136 alias.chmod(mode) 95 137 return alias 96 138 ··· 156 198 assert install_guard.parse_wrapper(content) == { 157 199 "journal": journal, 158 200 "sol_bin": sol_bin, 159 - "version": 2, 201 + "version": 3, 160 202 } 161 203 162 204 def test_render_wrapper_round_trip_tricky_paths(self): ··· 168 210 assert install_guard.parse_wrapper(content) == { 169 211 "journal": journal, 170 212 "sol_bin": sol_bin, 171 - "version": 2, 213 + "version": 3, 172 214 } 173 215 174 216 def test_render_wrapper_matches_spec_template(self): ··· 180 222 assert ( 181 223 content == "#!/bin/sh\n" 182 224 "# sol — managed by 'sol config'. Edits will be overwritten.\n" 183 - "# managed-version: 2\n" 225 + "# managed-version: 3\n" 184 226 ': "${SOLSTONE_JOURNAL:=/Users/jer/Documents/Solstone}"\n' 185 227 "export SOLSTONE_JOURNAL\n" 186 228 "SOL_BIN='/Users/jer/projects/solstone/.venv/bin/sol'\n" ··· 191 233 'if [ "$1" = "supervisor" ]; then\n' 192 234 ' mkdir -p "$SOLSTONE_JOURNAL/health"\n' 193 235 ' exec >>"$SOLSTONE_JOURNAL/health/service.log" 2>&1\n' 236 + " export PYTHONUNBUFFERED=1\n" 194 237 "fi\n" 195 238 'exec "$SOL_BIN" "$@"\n' 196 239 ) ··· 211 254 journal = "/tmp/solstone" 212 255 sol_bin = "/tmp/repo/.venv/bin/sol" 213 256 214 - content = install_guard.render_wrapper(journal, sol_bin) 257 + content = render_v2_wrapper(journal=journal, sol_bin=sol_bin) 215 258 216 259 assert install_guard.parse_wrapper(content) == { 217 260 "journal": journal, ··· 219 262 "version": 2, 220 263 } 221 264 222 - def test_parse_wrapper_rejects_v3(self): 265 + def test_parse_wrapper_accepts_v3(self): 266 + journal = "/tmp/solstone" 267 + sol_bin = "/tmp/repo/.venv/bin/sol" 268 + 269 + content = install_guard.render_wrapper(journal, sol_bin) 270 + 271 + assert install_guard.parse_wrapper(content) == { 272 + "journal": journal, 273 + "sol_bin": sol_bin, 274 + "version": 3, 275 + } 276 + 277 + def test_parse_wrapper_rejects_v4(self): 223 278 content = install_guard.render_wrapper( 224 279 "/tmp/solstone", "/tmp/repo/.venv/bin/sol" 225 280 ) 226 - content = content.replace("# managed-version: 2", "# managed-version: 3") 281 + content = content.replace("# managed-version: 3", "# managed-version: 4") 227 282 228 283 assert install_guard.parse_wrapper(content) is None 229 284 ··· 337 392 assert captured.out == "fresh\n" 338 393 assert captured.err == "" 339 394 340 - def test_check_reports_current_for_v2_wrapper_with_matching_paths( 395 + def test_check_reports_current_for_v3_wrapper_with_matching_paths( 341 396 self, home_root, tmp_path, capsys 342 397 ): 343 398 repo = make_repo(tmp_path) ··· 361 416 repo = make_repo(tmp_path) 362 417 target = ensure_expected_target(repo) 363 418 make_v1_wrapper( 419 + home_root, 420 + journal=install_guard._current_journal_for_alias(), 421 + sol_bin=str(target), 422 + ) 423 + 424 + state, token = install_guard.check_alias_detail(repo) 425 + 426 + assert state is install_guard.AliasState.OWNED 427 + assert token == "upgrade" 428 + 429 + def test_check_reports_upgrade_for_v2_wrapper_with_matching_paths( 430 + self, home_root, tmp_path 431 + ): 432 + repo = make_repo(tmp_path) 433 + target = ensure_expected_target(repo) 434 + make_v2_wrapper( 364 435 home_root, 365 436 journal=install_guard._current_journal_for_alias(), 366 437 sol_bin=str(target), ··· 480 551 assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 481 552 "journal": install_guard._current_journal_for_alias(), 482 553 "sol_bin": str(target), 483 - "version": 2, 554 + "version": 3, 484 555 } 485 556 486 557 def test_install_refuses_foreign_regular_file_without_force( ··· 519 590 assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 520 591 "journal": install_guard._current_journal_for_alias(), 521 592 "sol_bin": str(target), 522 - "version": 2, 593 + "version": 3, 523 594 } 524 595 assert os.access(alias, os.X_OK) 525 596 ··· 543 614 assert install_guard.parse_wrapper(first_content) == { 544 615 "journal": install_guard._current_journal_for_alias(), 545 616 "sol_bin": str(target), 546 - "version": 2, 617 + "version": 3, 547 618 } 548 619 549 - def test_v1_wrapper_upgrades_to_v2_end_to_end( 620 + def test_v1_wrapper_upgrades_to_v3_end_to_end( 550 621 self, home_root, tmp_path, monkeypatch, capsys 551 622 ): 552 623 repo = make_repo(tmp_path) ··· 570 641 assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 571 642 "journal": install_guard._current_journal_for_alias(), 572 643 "sol_bin": str(target), 573 - "version": 2, 644 + "version": 3, 645 + } 646 + 647 + assert install_guard.cmd_check(repo) == 0 648 + captured = capsys.readouterr() 649 + assert captured.out == "current\n" 650 + assert captured.err == "" 651 + 652 + def test_v2_wrapper_upgrades_to_v3_end_to_end( 653 + self, home_root, tmp_path, monkeypatch, capsys 654 + ): 655 + repo = make_repo(tmp_path) 656 + target = ensure_expected_target(repo) 657 + alias = make_v2_wrapper( 658 + home_root, 659 + journal=install_guard._current_journal_for_alias(), 660 + sol_bin=str(target), 661 + ) 662 + 663 + assert install_guard.cmd_check(repo) == 0 664 + captured = capsys.readouterr() 665 + assert captured.out == "upgrade\n" 666 + assert captured.err == "" 667 + 668 + rc, out, err = run_main(monkeypatch, capsys, repo, "install") 669 + 670 + assert rc == 0 671 + assert out == "installed\npath: ~/.local/bin already on PATH\n" 672 + assert err == "" 673 + assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 674 + "journal": install_guard._current_journal_for_alias(), 675 + "sol_bin": str(target), 676 + "version": 3, 574 677 } 575 678 576 679 assert install_guard.cmd_check(repo) == 0 ··· 617 720 assert install_guard.parse_wrapper(alias.read_text(encoding="utf-8")) == { 618 721 "journal": install_guard._current_journal_for_alias(), 619 722 "sol_bin": str(target), 620 - "version": 2, 723 + "version": 3, 621 724 } 622 725 append_mock.assert_called_once_with( 623 726 str(alias.parent), ··· 708 811 return write_executable_script( 709 812 tmp_path / "stub-sol", 710 813 "#!/bin/sh\nprintf 'OUT %s\\n' \"$*\"\nprintf 'ERR %s\\n' \"$*\" >&2\n", 814 + ) 815 + 816 + @staticmethod 817 + def _write_python_stub_sol_bin(tmp_path: Path, body: str) -> Path: 818 + return write_executable_script( 819 + tmp_path / "stub-sol", f"#!{sys.executable}\n{body}" 711 820 ) 712 821 713 822 @staticmethod ··· 753 862 assert result.returncode == 0 754 863 assert result.stdout == "OUT not-supervisor\n" 755 864 assert result.stderr == "ERR not-supervisor\n" 865 + assert not (journal / "health" / "service.log").exists() 866 + 867 + def test_wrapper_exports_pythonunbuffered_for_supervisor(self, tmp_path): 868 + journal = tmp_path / "j" 869 + sol_bin = self._write_python_stub_sol_bin( 870 + tmp_path, 871 + 'import time\nprint("FAST_LINE")\ntime.sleep(2)\nprint("SLOW_LINE")\n', 872 + ) 873 + wrapper = self._write_wrapper(tmp_path, journal=journal, sol_bin=sol_bin) 874 + 875 + proc = subprocess.Popen([str(wrapper), "supervisor"], env=self._wrapper_env()) 876 + log = journal / "health" / "service.log" 877 + deadline = time.time() + 1.0 878 + saw_fast_early = False 879 + while time.time() < deadline: 880 + if log.exists() and "FAST_LINE" in log.read_text(encoding="utf-8"): 881 + saw_fast_early = True 882 + break 883 + time.sleep(0.05) 884 + 885 + assert saw_fast_early 886 + assert proc.wait(timeout=5) == 0 887 + content = log.read_text(encoding="utf-8") 888 + assert "FAST_LINE" in content 889 + assert "SLOW_LINE" in content 890 + 891 + def test_wrapper_does_not_export_pythonunbuffered_for_cli(self, tmp_path): 892 + journal = tmp_path / "j" 893 + sol_bin = self._write_python_stub_sol_bin( 894 + tmp_path, 895 + 'import os\nprint(os.environ.get("PYTHONUNBUFFERED", "<unset>"))\n', 896 + ) 897 + wrapper = self._write_wrapper(tmp_path, journal=journal, sol_bin=sol_bin) 898 + 899 + result = subprocess.run( 900 + [str(wrapper), "not-supervisor"], 901 + capture_output=True, 902 + text=True, 903 + check=False, 904 + env=self._wrapper_env(), 905 + ) 906 + 907 + assert result.returncode == 0 908 + assert result.stdout.strip() == "<unset>" 756 909 assert not (journal / "health" / "service.log").exists() 757 910 758 911 def test_wrapper_appends_not_truncates(self, tmp_path):
+5 -4
think/install_guard.py
··· 24 24 WRAPPER_TEMPLATE = """\ 25 25 #!/bin/sh 26 26 # sol — managed by 'sol config'. Edits will be overwritten. 27 - # managed-version: 2 27 + # managed-version: 3 28 28 : "${{SOLSTONE_JOURNAL:={journal}}}" 29 29 export SOLSTONE_JOURNAL 30 30 SOL_BIN='{sol_bin}' ··· 35 35 if [ "$1" = "supervisor" ]; then 36 36 mkdir -p "$SOLSTONE_JOURNAL/health" 37 37 exec >>"$SOLSTONE_JOURNAL/health/service.log" 2>&1 38 + export PYTHONUNBUFFERED=1 38 39 fi 39 40 exec "$SOL_BIN" "$@" 40 41 """ 41 42 42 - WRAPPER_MARKER = "# managed-version: 2" 43 - WRAPPER_VERSION = 2 43 + WRAPPER_MARKER = "# managed-version: 3" 44 + WRAPPER_VERSION = 3 44 45 45 - _RE_MARKER = re.compile(r"(?m)^# managed-version: (?P<version>[12])$") 46 + _RE_MARKER = re.compile(r"(?m)^# managed-version: (?P<version>[123])$") 46 47 _RE_JOURNAL = re.compile(r'(?m)^: "\$\{SOLSTONE_JOURNAL:=(?P<journal>[^\n]*)\}"$') 47 48 _RE_SOL_BIN = re.compile(r"(?m)^SOL_BIN='(?P<sol_bin>(?:[^']|'\\'')*)'$") 48 49