Skip to content

Commit ce8a9aa

Browse files
chore(release): v3.0.0-beta.1 (#432)
* chore(release): v3.0.0-beta.1 version and release notes from the changelog * chore(release): changelog for v3.0.0-beta.1 * fix(packaging): the frozen engine starts, calls models and loads tools for Claude Code The first v3.0.0-beta.1 build shipped an engine that crashed on start (no numpy: release.yaml left out the voice extra), broke every model call (tokenizers excluded, but litellm imports it), and could not run claude-code-tools (missing from engine_entry's commands). build_engine.py only checked `version`, which imports none of that. - release.yaml installs .[dev,voice,whatsapp] - the spec no longer excludes tokenizers - engine_entry knows every CLI command; a test fails when they drift - a hidden `selfcheck` command imports the runtime modules and the migrations; build_engine.py runs it and a real `serve` until /api/health answers * fix(release): an entry that names its own issue is not listed twice * fix(release): use real backreferences in the duplicate-issue check
1 parent 4b29834 commit ce8a9aa

16 files changed

Lines changed: 201 additions & 19 deletions

‎.github/workflows/release.yaml‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ jobs:
3434
run: |
3535
uv venv .venv --python 3.12
3636
if [ "$RUNNER_OS" = "Windows" ]; then PY=.venv/Scripts/python.exe; else PY=.venv/bin/python; fi
37-
uv pip install --python "$PY" -e ".[dev,whatsapp]"
37+
# voice brings numpy, which the engine imports on start (sentient.voice); the heavy voice runtimes stay
38+
# out of the frozen engine through the EXCLUDES in packaging/sentient-engine.spec
39+
uv pip install --python "$PY" -e ".[dev,voice,whatsapp]"
3840
- name: Build installer
3941
working-directory: desktop
4042
env:
@@ -57,13 +59,29 @@ jobs:
5759
if: startsWith(github.ref, 'refs/tags/v')
5860
runs-on: ubuntu-latest
5961
steps:
62+
- uses: actions/checkout@v7
6063
- uses: actions/download-artifact@v7
6164
with:
6265
path: dist
6366
merge-multiple: true
67+
# The release notes are this version's section of CHANGELOG.md (built from changes/ by
68+
# packaging/release_changelog.py), so users read the same plain-language notes everywhere.
69+
- name: Release notes from CHANGELOG.md
70+
env:
71+
VERSION: ${{ github.ref_name }}
72+
run: |
73+
v="${VERSION#v}"
74+
awk -v head="## [$v]" '
75+
index($0, head) == 1 { on = 1; next }
76+
on && /^## \[/ { exit }
77+
on && /^\[[^]]+\]: / { exit }
78+
on { print }
79+
' CHANGELOG.md > notes.md
80+
if [ ! -s notes.md ]; then echo "::error::CHANGELOG.md has no section for $v"; exit 1; fi
81+
printf '\n---\nInstallers are not code-signed yet, so Windows SmartScreen and macOS Gatekeeper will warn the first time you open them.\n' >> notes.md
6482
- uses: softprops/action-gh-release@v3
6583
with:
6684
draft: true
6785
prerelease: ${{ contains(github.ref_name, '-') }}
68-
generate_release_notes: true
86+
body_path: notes.md
6987
files: dist/*

‎CHANGELOG.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ released.
1010

1111
<!-- towncrier release notes start -->
1212

13+
## [3.0.0-beta.1] - 2026-10-17
14+
1315
### Added
16+
- **Disconnecting can also mean forgetting.** When you disconnect Gmail, IMAP, Home Assistant or another app, remove an
17+
MCP server, a device or a folder of notes, or disconnect Telegram, Discord or WhatsApp, you can tick "Also forget what
18+
Sentient learned from it". The dialog says how much that is first, then the memories, chats, summaries, saved emails
19+
and app results that came from it are deleted, and Activity notes it. A memory you also shared somewhere else stays.
20+
Leave the box clear and Sentient keeps what it learned (#153).
1421
- **Skills can say how to check they work.** A skill can list a file that must exist or a few lines of Python with the
1522
output they should print. Sentient checks a skill it drafted for itself before it shows it to you, and doesn't
1623
propose one that fails. A check that runs code waits until you approve the skill (the button says "Approve and run
@@ -444,6 +451,7 @@ released.
444451
skip. Nothing catches up while Stop everything is on; it happens when you resume.
445452

446453
### Changed
454+
- Database changes are now numbered files in `sentient/store/migrations/`, and the schema version is the highest of them, so two changes can no longer silently take the same version. The database moves to version 13, so the first start after updating takes a snapshot first, as with any database update. (#424)
447455
- **Incognito chats leave nothing behind:** when an incognito chat is deleted, the long results it saved
448456
while reading pages or files go with it. Files you asked it to write and tasks it created stay.
449457
- A task that repeats the same failing step three times now pauses as stuck and asks what to do, instead of failing.
@@ -668,5 +676,6 @@ Sentient v3: a rewrite as a desktop app that runs on your own computer. No accou
668676

669677
The cloud web app. Its history is on the [`v2` branch](https://github.com/existence-master/Sentient/tree/v2).
670678

671-
[Unreleased]: https://github.com/existence-master/Sentient/compare/v2-final...main
679+
[Unreleased]: https://github.com/existence-master/Sentient/compare/v3.0.0-beta.1...main
680+
[3.0.0-beta.1]: https://github.com/existence-master/Sentient/releases/tag/v3.0.0-beta.1
672681
[3.0.0-alpha.0]: https://github.com/existence-master/Sentient/tree/main

‎changes/153.added.md‎

Lines changed: 0 additions & 5 deletions
This file was deleted.

‎changes/424.changed.md‎

Lines changed: 0 additions & 1 deletion
This file was deleted.

‎desktop/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sentient-desktop",
33
"productName": "Sentient",
4-
"version": "3.0.0-alpha.0",
4+
"version": "3.0.0-beta.1",
55
"description": "Sentient - your local-first personal assistant",
66
"author": "Existence",
77
"private": true,

‎docs/DEVELOPING.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,17 @@ npm run package:win # just the installer (reuses desktop/build/engine)
229229
```
230230

231231
Take `desktop/.build.lock` first: packaging rebuilds the shared `desktop/out`.
232-
The Windows result is `desktop/dist/Sentient-Setup-<version>.exe` (~216 MiB), a
232+
The Windows result is `desktop/dist/Sentient-Setup-<version>.exe` (~246 MiB), a
233233
per-user NSIS installer that needs no administrator rights and installs into
234234
`%LOCALAPPDATA%\Programs\Sentient`. `/S` installs and uninstalls silently.
235235
Uninstalling leaves `~/.sentient` alone: that is the user's memories and tasks.
236236

237+
`packaging/build_engine.py` checks the frozen engine before staging it: `version`, then `selfcheck` (a hidden
238+
command that imports everything the engine needs at runtime, including litellm and numpy, and the database
239+
migrations), then a real `serve` on a throwaway data folder until `GET /api/health` answers. Before a release, also
240+
run the unpacked app (`desktop/dist/win-unpacked`) against a fresh `SENTIENT_HOME` and send one chat: the checks
241+
above can't see what only a model call or the Claude Code backend exercises.
242+
237243
| File | What it is |
238244
|---|---|
239245
| `packaging/sentient-engine.spec` | PyInstaller spec: hidden imports, data files, exclusions |

‎packaging/build_engine.py‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,65 @@ def freeze(clean: bool) -> None:
6868
raise SystemExit(f"PyInstaller failed with exit code {result.returncode}")
6969

7070

71+
def _free_port() -> int:
72+
import socket
73+
74+
with socket.socket() as s:
75+
s.bind(("127.0.0.1", 0))
76+
return s.getsockname()[1]
77+
78+
79+
def _serve_check(exe: Path) -> None:
80+
"""Start `serve` on a throwaway data folder and wait for GET /api/health.
81+
82+
`version` never imports the app, so a frozen engine missing a runtime module still passes it; v3.0.0-beta.1's
83+
first build crashed on start for want of numpy that way.
84+
"""
85+
import tempfile
86+
import urllib.request
87+
88+
port = _free_port()
89+
with tempfile.TemporaryDirectory(prefix="sentient-verify-") as home:
90+
env = {**os.environ, "SENTIENT_HOME": home, "SENTIENT_GATEWAY_TOKEN": os.urandom(16).hex()}
91+
proc = subprocess.Popen([str(exe), "serve", "--host", "127.0.0.1", "--port", str(port)], env=env,
92+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
93+
try:
94+
deadline = time.monotonic() + 180
95+
while time.monotonic() < deadline:
96+
if proc.poll() is not None:
97+
raise SystemExit(f"frozen engine exited during `serve` (code {proc.returncode}):\n"
98+
f"{proc.stdout.read() if proc.stdout else ''}")
99+
try:
100+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/health", timeout=5) as r:
101+
if r.status == 200:
102+
log(f"frozen engine serves: {r.read().decode(errors='replace').strip()}")
103+
return
104+
except OSError:
105+
pass
106+
time.sleep(2)
107+
raise SystemExit("frozen engine did not answer /api/health within 180 s")
108+
finally:
109+
proc.terminate()
110+
try:
111+
proc.wait(timeout=30)
112+
except subprocess.TimeoutExpired:
113+
proc.kill()
114+
115+
71116
def verify() -> None:
72-
"""The frozen engine must at least import and answer `version`."""
117+
"""The frozen engine must answer `version`, import everything it needs at runtime, and really serve."""
73118
exe = BUNDLE / EXE_NAME
74119
if not exe.exists():
75120
raise SystemExit(f"expected {exe} to exist")
76121
out = subprocess.run([str(exe), "version"], capture_output=True, text=True, timeout=180)
77122
if out.returncode != 0 or "sentient" not in out.stdout.lower():
78123
raise SystemExit(f"frozen engine failed `version`:\n{out.stdout}\n{out.stderr}")
79124
log(f"frozen engine says: {out.stdout.strip()}")
125+
out = subprocess.run([str(exe), "selfcheck"], capture_output=True, text=True, timeout=300)
126+
if out.returncode != 0:
127+
raise SystemExit(f"frozen engine failed `selfcheck`:\n{out.stdout}\n{out.stderr}")
128+
log(out.stdout.strip())
129+
_serve_check(exe)
80130

81131

82132
def stage() -> None:

‎packaging/engine_entry.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919

2020
# Commands understood by sentient.cli. Anything else on argv[1] is treated as an
2121
# option for `serve` (so `sentient-engine.exe --port 7777` behaves sensibly).
22-
_COMMANDS = {"serve", "node", "doctor", "config", "version"}
22+
# tests/test_packaging.py fails when this drifts from the CLI: the installed app spawns
23+
# `sentient-engine.exe claude-code-tools <file>` for the Claude Code backend, and v3.0.0-beta.1's
24+
# first build turned that into `serve claude-code-tools` because it was missing here.
25+
_COMMANDS = {
26+
"serve", "node", "doctor", "config", "version",
27+
"claude-code-tools", "backup", "restore", "selfcheck",
28+
}
2329

2430

2531
def main() -> None:

‎packaging/release_changelog.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
REPO_URL = "https://github.com/existence-master/Sentient"
2525
# Keep a Changelog order; anything else keeps its place after these
2626
ORDER = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
27+
# An entry that already ends with its own "(#123)" gets a second one from towncrier: "(#123) (#123)".
28+
_DUP_REF = re.compile(r"(\(#\d+\))(?:\s*\1)+")
2729

2830

2931
def _trim(lines: list[str]) -> list[str]:
@@ -58,7 +60,7 @@ def fold_release(changelog: str, version: str) -> str:
5860
out += [*_trim(intro), ""]
5961
rank = {name: i for i, name in enumerate(ORDER)}
6062
for name in sorted(groups, key=lambda n: rank.get(n, len(ORDER))):
61-
items = [line for chunk in groups[name] for line in _trim(chunk)]
63+
items = [_DUP_REF.sub(r"\1", line) for chunk in groups[name] for line in _trim(chunk)]
6264
if items:
6365
out += [f"### {name}", *items, ""]
6466
text = changelog[:start] + "\n".join(out) + "\n" + changelog[end:]

0 commit comments

Comments
 (0)